Skip to content

feat(cli): server-executed backfill client (#30, #26, PR-2b)#41

Merged
kfastov merged 1 commit into
mainfrom
feat/issue-30-backfill-cli-2b
Jun 3, 2026
Merged

feat(cli): server-executed backfill client (#30, #26, PR-2b)#41
kfastov merged 1 commit into
mainfrom
feat/issue-30-backfill-cli-2b

Conversation

@kfastov
Copy link
Copy Markdown
Owner

@kfastov kfastov commented Jun 3, 2026

PR-2b of #30 — the CLI thin client over the PR-2a control API. Backfill is now executed by the long-lived server, not the CLI process, so a send (which needs the store write lock) is never blocked by a CLI that's busy backfilling.

Closes #30. Closes #26.

New surface

  • backfill --chat <id> [--depth N] [--min-date ISO] [--background]ensureServer (auto-starts tgcli server --idle-exit 60s if none is up) → enqueue via the control API.
    • foreground (default): follows progress to terminal, polling listJobs under a brief acquire→read→release read lock each ~1s (never held across the wait, so it can't block other CLI writers), renders progress to stderr (respects --quiet), final summary to stdout. Long command → no default timeout (a user --timeout is still honored). Ctrl-C = detach (prints a note, exits 0) — it does NOT cancel; the server keeps draining.
    • --background: prints { jobId, channelId, status } and exits.
  • backfill status [--json] — enhanced: queue overview + per-chat in_progress/pending progress (done/target, %, cursor) + whether the server is up.
  • backfill count [--json] — number of in_progress backfills (cheap poll).
  • backfill wait [--json]ensureServer then drain the queue.
  • backfill cancel --chat <id> — routes through the control API when a server is up; falls back to a direct cancelJobs when not.
  • Bare backfill (no --chat) keeps the unchanged legacy in-process sync; sync stays a silent alias.

Principles honored (from our review)

  • No bicycles: core/control-client.js uses Node's built-in global fetch (+ AbortSignal.timeout) — no hand-rolled http.request. Reuses CONTROL_FILE/CONTROL_TOKEN_HEADER from control-server.js and core/duration.js.
  • No functional duplication: enqueue/cancel go through the existing PR-2a endpoints — never reimplemented client-side. status/count/wait reuse existing service read methods.
  • No premature abstraction: one small shared withReadSnapshot helper for the read paths; nothing parameterized for the sake of it.

Notable find (flagged separately, not fixed here)

Pre-existing bug: MessageSyncService.shutdown() resets all in_progress jobs to pending (message-sync-service.js:2100-2106). Any read command that does createServices() + shutdown() would, if run while the server is mid-backfill, reset the server's running job. PR-2b's new read paths avoid this (they open the DB read-only and close the handle directly, never calling shutdown()); the existing read commands are flagged as a separate cleanup task.

Tests

npm test279 passing (21 files). New: tests/control-client.test.js (fetch + spawn mocked: ensureServer reuse vs spawn-and-wait, the start race, ping/enqueue/cancel) and tests/backfill-client.test.js (real commander surface, services/locks/store/client stubbed: foreground poll-to-completion, --background, error exit, --quiet, SIGINT-detach-without-cancel, count/status, cancel routing up/down). No real network/Telegram.

Size note

~450 lines production (control-client.js 166 + cli.js client surface) / ~394 lines new tests / ~50 docs — net-new CLI functionality, not churn.

…tatus/count/wait/cancel (#30, #26)

Make the CLI a thin one-shot client over the always-on control API that PR-2a
added. Backfill work is executed by the long-lived server, not the CLI process,
so a `send` (which needs the store write lock) is never blocked by a CLI that is
busy backfilling.

core/control-client.js (new): loopback control client built on Node's global
`fetch` (no hand-rolled http.request). readControlFile/pingServer/enqueueBackfill/
cancelBackfill hit the existing PR-2a endpoints (so enqueue/cancel logic is never
reimplemented), all authed with the token from control.json. ensureServer reuses
a reachable server (covering the race where another process started one) or
spawns `tgcli server --idle-exit 60s` detached + unref'd and polls ping for ~10s;
the idle-exit lets the auto-started server shut itself down once idle.

cli.js: branch the `backfill` action on `--chat`. With `--chat` it ensureServer →
enqueueBackfill; `--background` prints { jobId, channelId, status } and exits,
otherwise it follows progress by polling listJobs under a BRIEF acquire→read→
release read lock each ~1s tick (never held across the wait), rendering progress
to stderr (respecting --quiet) and a final summary to stdout; exits non-zero on
job error. Ctrl-C installs a SIGINT handler that detaches (prints a note, exits 0)
without cancelling. Long command, so no send default timeout is applied (a user
--timeout is still honored). New `count`/`wait`/`cancel` subcommands and an
enhanced `status` (queue + per-chat in_progress/pending progress + server up/down
via pingServer) all use the same brief read-snapshot helper, which closes the DB
handle directly rather than via shutdown() to avoid mutating server-owned jobs.
`cancel` routes through the control client when a server is up and falls back to a
direct cancelJobs when not. The bare `backfill` (no --chat) keeps the unchanged
legacy in-process sync, and `sync` stays a silent alias.

Because the parent `backfill` now declares --chat/--depth/--min-date for
`backfill --chat`, commander binds those duplicated flags to the parent scope;
`backfill jobs add` and `backfill cancel` read them via optsWithGlobals so both
the bare command and the subcommands keep working.

Tests: control-client (fetch + spawn mocked) covers ensureServer reuse vs
spawn-and-wait, the race, ping/enqueue/cancel; backfill-client drives the real
commander surface (services/locks/store/control-client stubbed) for foreground
poll-to-completion, --background, error exit, --quiet, SIGINT-detach-without-
cancel, count, status, and cancel routing up/down. 279 tests pass.

Docs: docs/cli.md and SKILL.md document the server-executed/auto-start model and
the new foreground/--background/status/count/wait/cancel surface.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rename sync to backfill, improve discoverability for AI agents sync --once times out silently on large groups, no progress indication

1 participant