Commit Graph
10 Commits
Author SHA1 Message Date
pastilhas 5afa2d832e step 3/4: sidecar routing keys become handles — NOT YET VERIFIED LIVE
Committed before restarting, deliberately: this changes the registration wire,
and the restart that proves it also bounces officer-claude-code, which is what
the session doing the work runs on. An uncommitted 25-file protocol change is
worse to inherit than one marked unverified.

`capabilities: ['music']` → `handles: ['music']` on the registration message,
across 20 sidecars, both plugins, the connector, the registry and the protocol
type. findSidecarByCapability → findSidecarHandling, waitForCapability →
waitForHandler.

The name: a sidecar already has `handleCommand`, so the list is literally what
it handles. `provider` was rejected — 390 existing uses and it already means
websocket door. `serves` was rejected — collides with HTTP serving, which
sidecars also do.

THIS IS A BREAKING WIRE CHANGE with no compatibility shim. A platform expecting
`handles` reads `undefined` from a sidecar sending `capabilities`, registers it
with an empty list, and every sendCommand finds nobody — chat, terminal and
music all fail with "No sidecar handling X is connected". So the whole estate
has to restart together; there is no rolling upgrade.

If it goes wrong: `git revert HEAD` and `pm2 restart all` again. The registry is
in-memory and nothing about this touches the database, so a revert is complete.

Found a fifth meaning of the word on the way, correctly named and untouched:
the pty sidecar's terminfo capability queries (XTGETTCAP escape sequences).
That is now five — permissions, the item store, routing keys, Lightning wallet
features, and terminfo.

tsgo clean, 797 tests, 787 pass, same 7. Not exercised against a live sidecar.
2026-08-15 16:23:31 +00:00
pastilhasandClaude Opus 5 e13128846b offscale is a plugin
headscale leaves the platform. 45 files move to plugins/offscale/ and the
platform stops knowing it exists.

  api/router.ts   the thin auth-gated proxy, now at /api/offscale
  sidecar/        18 files, the whole headscale contract and its admin keys
  db/             schema + queries, offscale_servers
  web/            26 files as panels and a layout — no screen, per the rule

removed from the platform: the hono mount, the `headscale` capability, the
App.tsx route pair, the screen and its barrel, the AppRegistry spread, the
officerdev re-exports, the dock tile, the page-title rule, and both database
barrels. tsgo is clean and nothing references it.

the imports tell the story of what the plugin↔host API actually is. the sidecar
takes @@/sidecar/protocol, @@/sidecar/connect, @@/data-path and
@@/officer-url.mjs; the queries take officerdb/db and officerdb/crypto; the
schema takes officerdb/auth/schema for the one reference a plugin may make; the
web half takes useClient, copyToClipboard, WorkspaceView and TerminalView from
the officerdev barrel. all of it resolves because a plugin lives inside the repo
— no publishing, no version negotiation.

AND IT FOUND A REAL BUG IN THE INSTALLER. createSidecarProxy learns its port
from a one-shot `<name>:server` event and subscribes when the plugin's router is
first imported — at mount. install started the sidecar BEFORE mounting, so the
announcement fired into a void: process online, routes mounted, every request
answering `503 sidecar not available` until something forced a reconnect. it
would have hit every plugin with an http sidecar. `example` never caught it
because it has no listener to announce.

install and enable now mount before starting; disable still unmounts before
stopping. neither direction leaves a mounted route in front of a sidecar that
cannot be reached.

verified live: /api/offscale/_officer/servers answers {"servers":[]}, /offscale
and /offscale/nodes serve, the old /api/headscale is 404, the offscale
capability is registered from the manifest, and officer-offscale is online.

757 pass, same 10 pre-existing failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 00:15:38 +00:00
pastilhasandClaude Opus 5 3c7f52ab77 PORT is read in one place, and it has no default
officer-url.mjs is now the only file in the tree that touches process.env.PORT.
Twenty-two others read it and supplied their own default; a value with
twenty-two sources is not configuration, it is twenty-two things to keep in sync,
and they had already drifted three ways.

It throws when PORT is unset rather than guessing. A default only covers the case
where .env was never loaded — which is not a machine anyone wants running,
because POSTGRES_URL is missing in the same breath. What the default bought was a
process that starts, binds somewhere unexpected, and fails later for a reason
that does not name the cause. Same posture as jwt.ts with JWT_SECRET.

It is .mjs, not .ts, and that is the whole reason this could be one file. pm2
launches officer-pty with node (ecosystem.config.cjs) and everything else with
bun; node cannot import TypeScript, so a .ts module would have left the pty
sidecar holding the only surviving copy of the default — precisely the thing
being removed. allowJs is already on, so the TS callers still get types. Verified
both runtimes import it, and that PUBLIC_URL-style overrides still work.

It also exports API_URL and OFFICER_API_URL, because nineteen sidecars were
independently building `ws://127.0.0.1:${PORT}` and two more were building the
http form. Those are one listener described in two protocols — no sidecar binds
anything — so they belong beside the port rather than being rediscovered per
file.

server.tsx now takes PORT as a number, so Number(PORT) at the serve site is gone.

Not typechecked (empty node_modules, frozen installs). Every edited file parses
under `bun build --no-bundle`; node and bun both load the new module; the unset
and non-numeric paths were exercised; the pm2 profile still loads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 23:43:39 +00:00
pastilhasandClaude Opus 5 e72cae4830 one default port, and it is 9000
Every PORT fallback in the tree now says 9000. There were three answers to one
question, each defensible where it was written and none of them visible from the
others:

  server.tsx and 19 sidecars   5000    a default from before there was an installer
  user-instance.ts             9010    what scripts/setup-old/setup.sh really wrote
  .env.example                 9000    what we told people to write

5000 goes first because macOS binds it — AirPlay Receiver has owned it since
Monterey, so a dev server there fails to bind or gets shadowed by something that
answers.

All 22 sites moved together, which is the point. Changing the app alone would have
turned a consistent-but-wrong default into a split one: the app on 9000 while
nineteen sidecars still dialled 5000.

9010 was the interesting one. It was the only value that ever matched a real
machine, because it is what the old installer wrote — and it was in the single
file whose disagreement would have broken chat alone, with nothing else looking
wrong. Its own comment records the same bug being fixed once already, within the
file, by a change that left it disagreeing with everything outside it.

Note what these defaults actually are: the sidecars bind nothing. user-instance.ts
has no listener at all — it builds ws:// and http:// URLs that both address the
app's single listener. So every one of these numbers is a guess at where the app
is, for a value that .env always supplies. Worth removing rather than aligning,
which is a separate change.

Not typechecked (empty node_modules, frozen installs). Every edited file parses
under `bun build --no-bundle`; the pm2 profile loads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 23:38:15 +00:00
pastilhasandClaude Opus 5 e9e144962d email: the sidecar schedules its own syncs
Stage 2, and the end of the inversion. The two sync handlers (1,093 lines) ran in the
platform's queue, which meant the sidecar reached back over its registration socket to ask
the platform to enqueue work, and the credentials travelled through Postgres job metadata to
get there. Option (A) from the plan: they run here now, and the Jobs screen is left to the
things it actually describes.

The handlers moved almost unedited. Their bodies were already a list of steps taking a
context, so sync-runner.ts synthesizes that context and runs them; what went away is the
JobHandler wrapper and the registration. `job.userId` is the OWNER'S EMAIL rather than a
numeric id — the queue's naming — and it resolves the mail store path, so it is called out
in the type. That is the same field whose absence made the mailbox read as empty two commits
ago; it is set from user.email and checked this time.

Deliberately not a queue: one run per account, no persistence, no retry. A failure is picked
up by the ten-minute cron like any other, and a sync interrupted by a restart resumes from
the stored cursor rather than the beginning. PermanentError survives as a local class — it
signalled "do not retry" to the queue and now just carries its message to the sync state.

accounts.ts asks the runner whether an account is syncing instead of scanning job rows, and
the queue-over-WS shim in index.ts is gone: enqueueViaWs, listJobsViaWs, the pending-response
map and the queue branch in the command handler. Nothing but a port crosses that socket now.

The three chat channels stop opening the mail store directly. They each carried their own
copy of count-rows / enqueue / poll / count-again, coupling three chat bridges to the mail
schema — and they enqueued `gmail-sync` unconditionally, the OAuth path, for an
app-password account that syncs over IMAP, so the command was already broken. One shared
helper calls a new POST /sync-now on the sidecar, which syncs and reports what arrived.

queue/handlers/ is now empty; both handlers there were email. The queue is untouched and
still serves the Jobs screen.

Not moved, and fine where they are: scripts/migrate-emails-to-sqlite.ts and
scripts/seed-imap-uids.ts are one-off maintenance scripts that open the store directly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 13:33:46 +00:00
pastilhasandClaude Opus 5 af56eb36ff email: move the mail store and every route into the sidecar
Email was the one sidecar built inside out. The platform held ~1,800 lines — the per-account
SQLite store, all 14 HTTP routes, account CRUD, resync, IMAP validation — while the 314-line
sidecar was a scheduler that reached BACK into the platform to do anything
(`import { performResync } from '../../api/email/resync'`).

The sidecar now serves its own HTTP listener and announces `email:server`, and
/api/email/* on the platform is createSidecarProxy like every other one: 1,801 lines down
to 22, with no mail knowledge left in it — not a message, not a folder, not a credential.

The routes moved verbatim, Hono and all. http.ts only reconstructs what the platform's
middleware used to provide: `user` on the context, from the X-Officer-User header the proxy
injects (trusted because this server binds loopback), and an error handler that turns
custom-errors into status codes.

The /email/events SSE stream went with them, which removes a whole round trip: the IDLE
watcher used to send `email:new` over the registration socket so the platform could push to
its SSE clients. Those clients are here now, so it calls broadcastEmailNew in-process and
`email:new` is gone from the wire protocol.

DELIBERATELY NOT DONE YET, and left backwards on purpose rather than half-moved:

- The two sync handlers (email-sync 381 lines, gmail-sync 712) still run in the platform's
  queue and now import the store from its new home — a platform → sidecar import, which is
  the wrong direction and is temporary. Moving them is option (A) from the plan: the sidecar
  schedules its own syncs, independent of the platform Jobs list.
- accounts.ts still imports queue/init to enqueue a sync and to report sync status, and
  index.ts still carries the queue-over-WS shim that inversion needs.
- The three channel handlers still open the mail store directly rather than asking over HTTP.

Two things worth knowing while testing: a from-scratch sync holds a proxied request open
well past the 60s idle default, hence timeoutSeconds on the proxy; and `gmail-sync` is
hardcoded in all three channel handlers even though the only account is provider=gmail with
auth_type=password, which routes to IMAP — so "sync emails" from a chat channel is
almost certainly already broken, and folds into the next stage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 12:10:35 +00:00
pastilhasandClaude Opus 4.8 4b08d0b99c email: push new-mail to /email via SSE (IMAP IDLE -> sidecar -> server -> EventSource)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 17:29:20 +00:00
pastilhasandClaude Opus 4.8 5a0b9ce70f email: real-time IMAP IDLE watchers (push on new mail); cron kept as backstop
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 17:22:46 +00:00
pastilhasandClaude Opus 4.6 b3f1d10bc1 inline email resync instead of job queue, refresh list on completion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 17:07:06 +00:00
pastilhasandClaude Opus 4.6 d88fe3cac7 task logs: migrate from filesystem to postgresql; refactor sidecars into submodules
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:49:39 +00:00