Skip to content

Say how each build ended, let a decision's write-up survive unrelated edits, and stop an archived document's runs - #889

Merged
lavaman131 merged 3 commits into
mainfrom
build-end-states
Nov 13, 2026
Merged

lavaman131 merged 3 commits into
mainfrom
build-end-states

Conversation

@lavaman131

Copy link
Copy Markdown
Collaborator

What the maintainer saw

A live end-to-end run on a hosted deployment (plan, then Build, then archive) found three problems.

  1. Build end states were misleading. Pressing Stop on a running Build showed "Returned for changes" in the Build tab, the same card as a task the agent blocked. The connector printed "✗ Failed: Chopin ended this run (build-forbidden)" for that Stop. For the first Build, whose agent blocked its task with "Missing source", the connector printed "✓ Finished".
  2. A decision's write-up kept failing. Three decisions were answered close together. Two wrote up; the first showed "Couldn't write this up", and Retry failed again.
  3. Archiving left the document's run behind. Around the archive the connector logged an HTTP 502, the Planner exited with code 1, and its workflow run was left paused on the machine. On Chopin's side the archived document still held the run's card as live, waiting on a question.

Causes

  1. End states. Every ended first build released its run as a revision request, and the Build tab drew every revision request as "Returned for changes". Nothing recorded who pressed Stop. The Stop route released the machine's connection, so the machine's next heartbeat was refused as build-forbidden, which the connector reports as a failure. When an agent ended its turn, the connector reported stopped and printed "Finished" without asking Chopin how the build ended.
  2. Write-up. The hosted job record for the failed card says: decision-followup, failed, 2 attempts, "The Planner's write_decision_scope failed twice." A second card later failed the same way. The scoped write refused any write whose revision was not the document's current one. While the job worked, the workflow run was publishing its plan into the document and the other two write-ups were landing, so the revision moved between the job's read and its write, and again after its re-read. Its second refusal ended the turn. Retry ran into the same churn. A test reproduces this on main: one unrelated edit between the read and the write refuses with "stale; read_decision_scope again".
  3. Archive. Archiving ended a run only through the Planner's link: the link closing marks its cards stopped. A Planner that was disconnected with its work kept, or a link closed while a deploy was handing over (which reports nothing, since nothing was stopped), left the cards as last reported. Nothing could end them afterwards, because attaching to an archived document is refused. Opening the document again brought them back live while a Planner hold still claimed them. The connector log shows the archive coincided with a server restart: two other documents' links dropped with 1012 at 01:55:36Z, and the 502 was the connector's request during that restart.

What changed

1. One ending, said the same way everywhere. A new build-ending module on the server decides how a first build ended. The same wording is used for the run record, Chat, the Build tab and the connector.

Ending Run record reason Chat Build tab Connector
A person pressed Stop Stopped in Chopin by @login. Build stopped by @login. Stopped by @login · At 5:54 PM. Build plan builds it again. ✗ Stopped in Chopin by @login.
The agent blocked a task “Task” is blocked: reason Build blocked on “Task”: reason. Blocked · reason. Build plan builds it again. ✗ Blocked: “Task” is blocked: reason.
It failed the error Build failed: error. Build failed · error. Build plan builds it again. ✗ Failed: error
It finished (implemented) Build finished: 1 task, #12. Built ✓ Finished: 1 task,
  • The Stop route records stoppedBy (the login of the person who pressed it). The build's error stays "Stopped in Chopin." for builds stored before this change.
  • The machine's next heartbeat after a Stop is refused with build-stopped and that sentence, after a restart too. The connector prints it as a stop, journals the run as { state: "stopped", reason }, and sends no failure report.
  • The answer to a build's last report now carries ending. The connector prints Blocked or Finished with pull requests, and journals a blocked run as { state: "blocked", reason }. An older server sends no ending, and the connector prints plain "Finished" as before.
  • A run returned by its agent or a reviewer still reads "Returned for changes", now with its reason.

2. Write-ups land when the document moved on elsewhere, and say why when they fail.

  • A decision-followup turn keeps what it read of its scope. A write that names an older revision lands when that turn read it at that revision, and the passage, its headings and the saved answers are unchanged. A change to any of these still refuses.
  • When a write-up does fail, its reason says why in plain words: the document changed while it was being written, another answer changed, the passage was removed, it needs a diagram or table, or a build is running. It never echoes the tool's arguments or names a tool. The card shows the reason beside Retry.

3. Archiving ends the document's runs itself.

  • Archiving stops every live run card itself, with no question left waiting, and saves this before it releases the link.
  • An archived document always opens with its runs stopped, whatever a Planner hold says.

Evidence

Build tab before and after, from the browser spec's real Stop and block flows (desktop and phone):

Before After
Stop, 1280 px
Stop, 390 px
Blocked, 1280 px
Blocked, 390 px

Each change is copy inside existing components, with no new colours or CSS. The card title and detail keep their tokens, so contrast is unchanged and there are no new colour pairs to measure. In my dark-mode attempts the page stayed in its light theme, so I kept only the light shots. The critique notes are in critique.md.

Checks

  • bun run types: pass
  • bun run ci: pass
  • bun test apps/server/src/tasks apps/connector apps/web/src/build-model.test.ts apps/server/src/chat/archive-runs.test.ts: 699 pass, 0 fail. Run with commit signing off: on this machine, workspace.test.ts's spike fixture commit fails under signing, both here and on main.
  • bun test apps/server/src/questions apps/server/src/chat apps/server/src/conversation-plan apps/server/src/agent apps/server/src/harness packages/editor/src/widgets: 3671 pass, 0 fail
  • bun run design:behavior: 106 passed
  • Playwright with --workers=2: implementation-launcher, build-restart, connector, decision-prose, planner-decisions and harness-decisions-deck: 39 passed, 1 skipped (the opt-in live ACP case)
  • bun run build: initial JavaScript 259,958 B raw / 82,492 B gzip; initial stylesheet 184,465 B raw / 32,681 B gzip (budget 185,000 B). This change adds no CSS.
  • The new tests fail on main (eec9491):
    • the route tests for Stop, blocked and finished
    • the connector run tests
    • the endedCard tests
    • the reopened archived-document test
    • the scoped-write test, which refuses "stale; read_decision_scope again"
    • the reason tests

Judgement calls

  • A Stop's error stays "Stopped in Chopin." and who stopped it is a new field, stoppedBy. Existing records and specs keep their meaning, and only the owner can press Stop anyway.
  • The blocked card leads with the agent's reason, not the task title. At phone width the card's single line truncated before the reason, and the open task row already names the task.
  • The connector treats a Stop pressed in Chopin like Ctrl-C (✗ … on stdout, not a red failure), because nothing failed.
  • I fixed the write-up at its cause (the strict revision check) and did not add automatic retries. A failure that remains now says why, and Retry works once the cause has passed.
  • write_decision_prose, the conversation-decision write-up, has the same exact-revision check, but no evidence shows it failing. Its target can be an insertion point, so loosening that check needs its own care. I left it for a follow-up.

Needs a change in the Planner client (not in this repository)

The hosted Planner client treats a document-archived refusal as a crash. It exits with code 1, and chopin connect starts it again only to be told to stop. It also records its runs as paused "link lost" rather than ending them. An archived document's refusal is final and deliberate. The client should exit cleanly, as it does for the deliberate stops it already recognises, and end those runs rather than keep them for a resume that cannot happen.

… the connector

A person pressing Stop on a running build showed "Returned for changes" in the Build tab, the
same as a task its agent blocked, and the connector printed "Failed: Chopin ended this run
(build-forbidden)" for it. A build whose agent blocked a task ("Missing source") printed
"Finished" in the connector while Chopin said it was blocked. Chopin released the run of every
ended build as a revision request with the build's error, and nothing kept who stopped it; the
connector only ever heard a refusal, or nothing about how the build ended.

A person's Stop now records who pressed it (stoppedBy). The run's recorded reason says "Stopped
in Chopin by @login.", a blocked task's says "“Task” is blocked: reason", and Chat's end line
says "Build stopped by @login.", "Build blocked on “Task”: reason." or "Build failed: error.".
The machine's next heartbeat is refused with build-stopped and that sentence, so the connector
prints "✗ Stopped in Chopin by @login." and journals the run as stopped, with nothing failed and
nothing reported back. The answer to a build's last report now carries how it ended, so the
connector prints "✗ Blocked: …" or "✓ Finished: 1 task, <pull request>" and journals a blocked
run as blocked. The Build tab's card says Stopped by @login with the time, Blocked with the task
and reason, Build failed with the error, or Returned for changes with the reason.

Assistant-model: Claude Opus 5.5
Assistant-workflow: inline (subagent)
Assistant-verification: bun run types
Assistant-verification: bun test apps/server/src/tasks apps/connector apps/web/src/build-model.test.ts (684 pass; the one failure, workspace.test.ts's spike commit, is this machine's commit signing and passes with signing off)
Assistant-verification: new route, connector and build-model tests fail on origin/main 16c55ff
User-preference: things should be polished like the Slack UX and beautiful and elegant animations
Co-authored-by: Alex Lavaee <lavaman131@github.com>
Archiving the scratch document left its workflow run card live, still waiting on a question,
after the Planner was released. Archiving ended runs only through the Planner's link: the link
closing marks its cards stopped. A Planner that was disconnected with its work kept, or a link
closed while a deploy was handing over (which reports nothing, as nothing was stopped), left the
cards as last reported, and nothing could end them afterwards because attaching to an archived
document is refused. Opening the document again brought them back live when a Planner hold still
claimed them.

Archiving now stops every live run card itself, with no question waiting, and saves that before it
releases the link. An archived document always opens with its runs stopped, whatever a Planner
hold says.

Assistant-model: Claude Opus 5.5
Assistant-workflow: inline (subagent)
Assistant-verification: bun run types
Assistant-verification: bun test apps/server/src/chat/archive-runs.test.ts (the reopen test fails on origin/main's plan service; endRuns is new)
User-preference: things should be polished like the Slack UX and beautiful and elegant animations
Co-authored-by: Alex Lavaee <lavaman131@github.com>
…and say why one failed

One decision's write-up showed "Couldn't write this up", and Retry failed again, while the two
decisions answered beside it wrote up. Its job had failed both times with "The Planner's
write_decision_scope failed twice." The scoped write refused any write whose revision was not the
document's current one, so every edit anywhere between the job reading its passage and writing it
refused it as stale. A workflow run was publishing its plan into the document and the other
decisions' write-ups were landing, so the revision moved under it on its re-read too, and the turn
ended after its second refusal. Retry ran into the same churn.

A decision-followup turn now keeps what it read of its scope. A write naming an older revision
lands when that turn read it at that revision and the passage, its headings and the saved answers
are unchanged; a change to any of them still refuses. When a write-up does fail, its reason says
why in plain words (the document changed while it was being written, another answer changed, the
passage was removed, it needs a diagram or table, a build is running) without echoing the tool's
arguments, and the card shows it beside Retry.

Assistant-model: Claude Opus 5.5
Assistant-workflow: inline (subagent)
Assistant-verification: bun run types
Assistant-verification: bun test apps/server/src/questions apps/server/src/chat apps/server/src/conversation-plan apps/server/src/agent apps/server/src/harness packages/editor/src/widgets (3671 pass, 0 fail)
Assistant-verification: the new scoped-write test refuses with "stale; read_decision_scope again" on origin/main eec9491; the reason tests fail there too
Assistant-verification: the hosted job record for the failed card: decision-followup, failed, 2 attempts, "The Planner's write_decision_scope failed twice."
Assistant-verification: bun run ci
Assistant-verification: bun run design:behavior (106 passed)
Assistant-verification: playwright implementation-launcher, build-restart, connector, decision-prose, planner-decisions, harness-decisions-deck (39 passed, 1 skipped)
Assistant-verification: bun run build (initial JS 259,958 B raw, initial CSS 184,465 B raw)
User-preference: things should be polished like the Slack UX and beautiful and elegant animations
Co-authored-by: Alex Lavaee <lavaman131@github.com>
@lavaman131
lavaman131 merged commit 95d0a77 into main Nov 13, 2026
24 checks passed
@lavaman131
lavaman131 deleted the build-end-states branch November 13, 2026 03:15
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