feat(service,host): a Mac command carries a kind
Merged
DA
10d ago+15891+15891+01234567890123456789012345678901234567890123456789-167-167-012345678901234567890123456789
PR MB of the "Browser agent tab automation setup" rollout (Mac path, M5). This PR gives a Mac command a kind with typed params, so later PRs can send the Mac a browser command. shell is the only kind that runs. run_on_mac behaves exactly as before.
What changed
- Database (
20261020221922_mac_command_kinds). The migration is additive and online-safe:mac_command.kind text NOT NULL DEFAULT 'shell'. A constant default is a catalog-only change in PG 11+, so every row already standing reads asshell.mac_command.params jsonb, nullable.commanddropsNOT NULL, since a browser row has no shell text. This is also catalog-only. The table only holds rows while their calls wait.- A new table
mac_command_listener_kind (user_id, kind, seen_at), keyed by(user_id, kind). - Every statement is idempotent.
- Wire (
mac-command-wire.ts,contract/mac-commands.ts):MAC_COMMAND_KIND, andmacCommandSchemaas a union:shell {command}|browser {params}.paramsis any JSON whose text is at mostMAX_BROWSER_PARAMS_CHARS(100k). It stays opaque until MD defines it.- The Mac's read decodes a kind this build doesn't know as
{id, kind}alone, using the same pattern as chat's unknown block types, so one unknown command doesn't spoil the read. - A result is now
shell result | {refused}.
- Version tolerance:
- The held read names the kinds the host runs in a
kindsquery (?kinds=shell,browser). An older desktop sends none, which means shell only. Shell is always included, and a kind the service doesn't know is ignored, never refused. - The service claims and waits only on rows of the named kinds. A browser row neither wakes nor gets claimed by an older Mac's read.
- Each read marks the account listening, and also marks each kind it named beyond shell. If no Mac that runs browser commands listened recently, a browser call gets
not-runwith a reason at once instead of timing out. With two Macs on different versions, each is counted for the kinds it runs. - The host dispatches by kind:
shellruns as before. Any other kind (browser for now, or an unknown one) is settled with{refused}, and the call returns that asnot-runwith the Mac's reason.
- The held read names the kinds the host runs in a
- Service:
askMac(userId, ask)is the generic path for any kind of command.runOnMacToolsends it a shell ask, and ME will call it for browser asks. This PR adds no new agent tool.
Tests
- Store, against a real dialect:
- A read naming shell only neither waits on nor claims a browser row, and a read naming browser claims it.
- A row inserted without a kind, the way a pre-migration build wrote one, reads as
shelland is claimed as a shell command. - Listening is tracked per kind, and an older Mac's read doesn't wipe a newer Mac's browser mark.
- Route, byte for byte:
- An older Mac's read (no
kinds) gets only the shell row, and the browser row stays waiting until a newer read takes it. - A browser call with only an older Mac listening gets
not-runat once, and nothing is left queued. - A browser command the Mac refuses comes back
not-runwith the Mac's reason.
- An older Mac's read (no
- Host: each read names
[shell]. Out of a shell, a browser and an unknown-kind command, only the shell one is spawned. The other two are settled as refusals, and the reads carry on. - Client: the read sends
kinds=shell,browser, and decodes an unknown kind as{id, kind}.
Out of scope: image results (MC), browser driving (MD), and the tool's routing (ME).
Made with Luke