Skip to content

feat: route all commands through the warm server — thin clients on shared ops (#45, Step 1)#48

Merged
kfastov merged 2 commits into
mainfrom
feat/route-all-commands-through-server
Jun 4, 2026
Merged

feat: route all commands through the warm server — thin clients on shared ops (#45, Step 1)#48
kfastov merged 2 commits into
mainfrom
feat/route-all-commands-through-server

Conversation

@kfastov

@kfastov kfastov commented Jun 4, 2026

Copy link
Copy Markdown
Owner

Variant B, Step 1 of #45 — all Telegram/archive CLI commands are now thin clients over the warm server (auto-started), executing operation handlers shared with the MCP tools. The local execution fallback is removed.

Core

  • runOperation simplified to ensureServer(idleExit '60s') → invoke(op, args). No local fallback, no need/lock/requireAuth — the server owns services, auth, and locking. (withCommand is retained for the server's own startup, auth, and the legacy sync handlers.)
  • OPERATIONS — 44 ops covering the full surface (channels, messages, send, media, topics, tags, metadata, contacts, groups, folders). Message ops own the shared source: archive|live|both logic + archive→live fallback; the message-formatting helpers now live once in operations.js (removed from cli.js and mcp-server.js).
  • MCP tools unified onto the same ops against the same warm services — an MCP tool and its CLI command run identical logic.

Send (single writer)

send text/photo/file run server-side: the ops wrap the existing executeSendWithRetries (retry / FLOOD_WAIT / rate-limit) against the warm client. On failure, /control/invoke serializes the SendCommandError.details as sendError, which the control-client re-throws so the CLI renders the identical human/--json error. The 30s default bounds the client's wait on the invoke. (The --quiet "Connecting…" status is dropped — the client no longer connects; --quiet stays honored.)

Kept local

config, service, doctor, and auth (interactive login is the bootstrap: it writes the session locally; the warm server uses it).

Scale

cli.js handlers collapse to validate → runOperation → format; mcp-server.js −483; operations.js +983. Net +330 with major consolidation.

Known follow-ups (not blocking; mostly Step 2)

  • Legacy in-process worker (sync/backfill --once/--follow, backfill jobs ...) still runs locally — Step 2 retires it (server as sole writer, sync→shim) and closes Unify on a warm backend: route CLI commands through the server (thin clients); withCommand as the seam #45.
  • logSendRetry is now unused (retries are server-side) — a small follow-up cleanup.
  • Send retry progress is no longer streamed to the CLI (it happens server-side).
  • Re-running auth while a server is already running needs a server restart to pick up the new session (pre-existing; auth was always local).

Tests

npm test325 passing (312 + 13): the simplified seam (ensureServer/invoke, no fallback), the new ops registry, the send op's server-side retry/flood + the sendError relay, /control/invoke for the new ops, and the migrated commands. No node_modules in the commit; comments are behavioral (no history).

kfastov added 2 commits June 4, 2026 11:48
Make every Telegram and archive CLI command a thin client over the
always-on control server. Each handler validates args, calls
runOperation(op, args), and renders the result; the server executes the
operation against its warm MTProto connection and open database as the
single writer.

- Simplify runOperation to ensureServer (auto-start) then invoke; remove
  the local-execution fallback and the need/lock/requireAuth params. The
  server owns services, auth, and locking. withCommand stays for the
  server's own startup, auth, and the remaining legacy sync-jobs/doctor
  commands.
- Expand OPERATIONS to the full surface (channels, messages, send, media,
  topics, tags, metadata, contacts, groups, folders) and migrate the MCP
  tools onto the same handlers, so one implementation serves CLI and MCP.
- Route send text/photo/file through the server, wrapping the existing
  send-utils retry / FLOOD_WAIT logic server-side. The control server
  relays a failed send's structured details so the CLI renders the same
  human/JSON error; the 30s default send timeout bounds the CLI's wait on
  the invoke.
- Keep config, service, doctor, and auth local.

Tests updated for the server-routed model and new ops; full suite green.
A fresh command after the server is down could fail with "Timed out
waiting for the control server to start" even though the server came up,
because startup blocked on the full Telegram connect + dialog refresh
(tens of seconds on a large account) before the control listener bound
and control.json was written. The client polls only ~10s for a ping, so
it gave up first.

Start the listeners first and connect lazily:

- Bind the MCP/control listeners and write control.json before any
  Telegram work, so GET /control/ping succeeds within ~1s of spawn.
- Connect the warm client in the background via a single shared promise
  (ensureTelegramConnected). The ensureLogin hook the control handlers
  and MCP tools run awaits that same promise, so the first operation —
  not the readiness probe — pays the connect cost, and concurrent callers
  share one connect.
- Take the full dialog refresh (refreshChannelsFromDialogs over every
  dialog) off the readiness path: it, realtime sync, and pending-job
  resume now run in the background after connect, so a fresh connect is
  only login + the updates loop. Read ops do their own live fetch and no
  longer wait on a global dialog seed.

The ping window stays at 10s and the cold read invoke budget stays at
30s, both comfortable now that the server is pingable immediately and a
fresh connect is a few seconds.

Add an integration test proving control.json is written and ping succeeds
while the connect is still pending, and that an op blocks until it resolves.
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.

Unify on a warm backend: route CLI commands through the server (thin clients); withCommand as the seam

1 participant