Skip to content

Show a document's people why its Planner cannot start on a machine - #773

Merged
lavaman131 merged 3 commits into
mainfrom
planner-start-failure
Nov 4, 2026
Merged

lavaman131 merged 3 commits into
mainfrom
planner-start-failure

Conversation

@lavaman131

Copy link
Copy Markdown
Collaborator

When a machine's Planner backend keeps failing to start, the document now says why. Until now Chat said Planner: starting on studio… for as long as the connector's restart loop went on, and the reason (for example unsafe-checkout: …) was only in the connector's log on that machine, which the document's members, often not the machine's owner, cannot read.

What people see

From the second failed launch in a row, the line under the composer changes. It is still the starting state (data-planner-state="starting", still a polite status), because the machine keeps trying:

  • Planner: can’t start on studio, in the danger colour, with a red dot
  • why, in the ordinary secondary ink (wraps; the dot stays with the first line)
  • Tried 3 times in a row, last at 06:29 PM. Still trying., in the quietest ink

The machine's owner sees its name; a teammate in the same document sees @mona’s machine, as the starting line does today. One failed launch is treated as transient and changes nothing. An @chopin message sent meanwhile is refused with the reason instead of "try again in a moment". Once the Planner attaches, or a launch stays up, it all goes.

Screenshots from the end-to-end run (CHOPIN_SHOTS):

The machine's owner, desktop

The machine's owner, desktop

A teammate with the document open, desktop

A teammate with the document open, desktop

Under the composer on a phone

Under the composer on a phone

How it works

Three commits, one per layer.

  1. Connector (apps/connector). It counts launches in a row that exited before staying up and posts after each to POST /api/connector/planner/failure: the document (by id, which the Planner credential names), the count, how the backend ended and why. The reason is the last stderr line that says why: not blank, and not a line that only announces the exit (Exiting with code 1; …, Shutting down, …), with terminal colors and a leading timestamp removed. It is masked with the connector's existing redacted (the Planner credential, GitHub tokens, --env values, token-shaped strings), the home directory shown as ~, and cut to 300 characters. When a launch stays up for the healthy interval it posts that nothing is failing. Reporting never waits on the restart loop and gives up after ten seconds.
  2. Protocol and server (packages/protocol, apps/server). The server accepts the report only for a document it asked that machine to run, keeps the latest per document and machine in memory (like the rest of the Planner's attachment state) and forgets it when the Planner attaches, when the connector says a launch stayed up, when the machine is no longer asked for the document, and on restart. Session.PlannerAttachment's starting gains an optional failure: { attempts, reason, at }, sent to the room from the second launch on. The reason is sanitised again server-side (colors and control characters out, one line, 300 characters) and at is the server's own clock.
  3. Web and e2e (apps/web, e2e). The line described above.

Compatibility

  • No connector protocol bump. The server refuses a connector whose protocol is newer than its own, so a bump would have broken new connectors on older servers. The report is a new route instead.
  • Older server, newer connector: the route answers 404; the connector takes it to mean the server never will and stops asking for that document (covered by a test that fails with that flag disabled).
  • Newer server, older connector: it never calls the route, so nothing changes.
  • Older web, newer server: failure is optional on starting; an older client ignores it.

Things to know

  • The failure is shown to everyone in the room, read-only viewers included, like the login already is. The reason is the backend's own stderr line, so it can name things on the machine (paths under the home directory are shortened to ~, tokens masked). If that is too much for read-only viewers, it is one condition in plannerStatus in main.ts.
  • chopin status is unchanged: it still shows the first stderr line. The line sent to Chopin is the last one that says why, because the first is often a banner.
  • The @chopin refusal is covered end to end, and that waits the server's 30-second start window, so the new e2e test takes about 36 s.

Verification

  • bun test in apps/connector (271 pass, 14 skip, 0 fail) and apps/server (3966 pass, 6 skip, 0 fail); apps/web/src/chat 227 pass.
  • New tests: connector (reason picked and masked, home directory shortened, cleared once up, 404 asks once), server (second failure shown, first not, only for a machine asked, cleared on attach / on up / on choosing elsewhere, sanitised, malformed refused, @chopin message), web (the three lines, still starting, reason escaped, teammate's wording) and two e2e tests against a real remote-Planner server (desktop with a teammate, and phone).
  • bun run e2e e2e/planner-picker.e2e.ts: all 8 pass, the 6 existing ones included.
  • bun run ci (including the design contract and the impeccable check, no new findings) and bun run types pass.
  • Red before green: the new server tests failed on the unchanged server and caught a crash on a report with no reason; the connector's 404 test fails with its guard disabled.

Not covered

No real connector was run against a real backend that refuses its checkout; the connector side is tested with a script that prints the same shape of stderr (reason, then an exit notice).

A connector starts a document's Planner backend and starts it again whenever it
exits early. When the backend could not start, for example because it refused
its checkout, the reason reached only the connector's own log on that machine,
and the document showed "Planner: starting" for as long as the loop went on.
Its members, who are often not the machine's owner, had nothing to read.

The connector now counts the launches in a row that exited before staying up
and, after each one, posts to /api/connector/planner/failure: the document (by
id, which the Planner credential names), how many times, how the backend ended
and why. The reason is the last line it wrote to stderr that says why, so a
closing "Exiting with code 1" notice never stands in for it, without terminal
colors or a leading time, with credentials masked as for everything else the
connector prints, the home directory shortened to ~ and cut to 300 characters.
When a launch stays up for the healthy interval the connector tells Chopin that
nothing is failing any more.

It is a new route, not a protocol bump, because the server refuses a connector
whose protocol is newer than its own. A server that does not know the route
answers 404, which the connector takes to mean it never will and stops asking
for that document, and a server that has the route ignores a connector that
never calls it. Reporting never waits on the restart loop and is cut off after
ten seconds. docs/chopin-cli.md says what people in the document then read.

Assistant-model: Claude Opus 5.5
Assistant-workflow: inline
Assistant-verification: GIT_CONFIG_GLOBAL=/dev/null bun test in apps/connector passed: 271 pass, 14 skip, 0 fail before the final edits, and src/planner.test.ts 20 pass, 0 fail after them
Assistant-verification: mutation check passed: with the unsupported-server flag disabled the 404 test fails (3 reports instead of 1) and passes with it
Assistant-verification: bun run ci and bun run types passed
Co-authored-by: Alex Lavaee <lavaman131@github.com>
A machine's connector starts a document's Planner backend and starts it again
whenever it exits early. Until now Chat said "Planner: starting on <machine>…"
for as long as that went on, and the reason was only in the connector's log on
that machine.

The server takes the connector's report at POST /api/connector/planner/failure,
only for a document it asked that machine to run (the connector names it by id,
or by address, which is read with the machine's own token), and keeps the
latest per document and machine in memory, as it keeps the rest of the
Planner's attachment state. It is forgotten when the Planner attaches, when the
connector says a launch stayed up, when the machine is no longer asked for the
document (chosen elsewhere, disconnected or no longer shared) and with a
restart. The reason is stripped of colors and control characters and kept to
one line of at most 300 characters; the time is the server's own, not the
connector's clock.

From the second launch in a row, the "starting" state sent to the room carries
the failure: how many launches, why, and when the last one exited. A single
failure is a transient one and still just says starting. An @chopin request
sent meanwhile is told why instead of "try again in a moment". Only the login
is still named, never the machine.

The report is a new route, not a connector protocol bump, so a server that
does not know it simply answers 404 and a connector that never reports works
as before. docs/agent-connection.md and docs/hosted-agent.md describe it.

Assistant-model: Claude Opus 5.5
Assistant-workflow: inline
Assistant-verification: GIT_CONFIG_GLOBAL=/dev/null bun test in apps/server passed: 3966 pass, 6 skip, 0 fail before the final edits, and src/harness/remote 118 pass, 0 fail after them
Assistant-verification: red-before-green passed: the new planner-dispatch tests failed on the unchanged server (404, no machineId) and caught a crash on a report without a reason
Assistant-verification: bun run ci and bun run types passed
Co-authored-by: Alex Lavaee <lavaman131@github.com>
The line under the composer that says "Planner: starting on studio…" now says
what is wrong once the machine's backend has failed to start more than once in
a row. It is still the starting state, so data-planner-state stays "starting"
and the line is still a polite status, but the dot turns red and the line has
three: "Planner: can’t start on studio" in the danger colour, why in the
ordinary secondary ink, and "Tried 3 times in a row, last at 06:29 PM. Still
trying." in the quietest. The words keep the "Planner:" prefix every other
state has. Everyone in the document reads it: the machine's owner sees their
machine's name and a teammate sees "@mona’s machine", as before. The reason
is text, never markup, and wraps, so on a phone the dot stays with the first
line.

The end-to-end test runs a real remote-Planner server with a fake connector: one
failed launch changes nothing, the second says why to the owner and to a
teammate with the document open, a teammate's @chopin message is told why
instead of to try again in a moment, and the line goes once the Planner
attaches. A second test checks it under the composer on a phone.

Assistant-model: Claude Opus 5.5
Assistant-workflow: inline
Assistant-verification: GIT_CONFIG_GLOBAL=/dev/null bun test in apps/web/src/chat passed: 227 pass, 0 fail
Assistant-verification: GIT_CONFIG_GLOBAL=/dev/null bun run e2e e2e/planner-picker.e2e.ts passed: 8 passed, including the two new tests
Assistant-verification: screenshots passed: looked at the owner, teammate and phone captures in /tmp/chopin-shots-planner-failure
Assistant-verification: bun run ci passed, including the design contract and the impeccable check (no new findings), and bun run types passed
Co-authored-by: Alex Lavaee <lavaman131@github.com>
@lavaman131
lavaman131 force-pushed the planner-start-failure branch from 0afc973 to bea856d Compare November 4, 2026 08:02
@lavaman131
lavaman131 merged commit b3c629e into main Nov 4, 2026
24 checks passed
@lavaman131
lavaman131 deleted the planner-start-failure branch November 4, 2026 08:07
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.

1 participant