Skip to content

feat(orchestration): a wrap-up turn when a worker stops without reporting - #88

Merged
ANT1OFF merged 2 commits into
mainfrom
feat/worker-wrapup-prompts
Nov 2, 2026
Merged

ANT1OFF merged 2 commits into
mainfrom
feat/worker-wrapup-prompts

Conversation

@ANT1OFF

@ANT1OFF ANT1OFF commented Nov 2, 2026

Copy link
Copy Markdown
Owner

What changes

When a worker ends a turn with no report and nothing pending, or its orchestrator stops it with stop_thread, Gram sends the worker one more turn in the same session: stop working and report. The orchestrator's digest then carries that report instead of "ended its turn without reporting".

How a silent stop was found, and where this hooks in

onTurnEnd (service/observers.ts) already marks a worker's memo quiet: turnEnded when a turn ends with an empty queue, no report in the turn and no armed automation. The ledger (quietOfWorker) turns that into quiet.kind === "turnEnded" unless something else explains the stop: parked, user hold, pending request, background work, a finished, done, blocked or question state, or a report that still stands. digestDiff then writes "ended its turn without reporting".

The wrap-up hooks in right after that memo write. The decision is a pure function, wrapUpDue in rules.ts. The service sends the turn with sendNotice (as server-authored, so it never counts as the user typing), then marks the memo wrapUp: true, clears its quiet and skips the turn end's wake. While the wrap-up is pending, digestDiff doesn't call the stop silent (wrappingUp). The next turn end clears the mark:

  • If the wrap-up turn reports, the digest carries the report.
  • If it ends silently too, the digest says "ended its turn without reporting", as before.

That stop never gets a second wrap-up.

When it fires (wrapUpDue)

  • Once per stop: never for the stop that ends a wrap-up turn.
  • Only for a stop the ledger reads as silent (quiet.kind === "turnEnded") whose turn ended with an empty queue and no report in it.
  • Turn ends that count: a turn the agent ended itself (completed, maxTurns), or an interrupt the worker's own orchestrator asked for. stop_thread on its own worker records the stop through the new Orchestration.noteStop. An interrupt from anyone else (the user's stop button, a restart's interrupt) is treated as theirs and gets no wrap-up.
  • Never when the worker is:
    • paused (the workspace or its feature);
    • held by the user, who typed into it since its last report (userIntervened);
    • archived or marked completed;
    • waiting out a usage limit (limitWait; that path returns earlier and is unchanged).
  • Never when the turn:
    • errored;
    • was synthesized by a crash, a closed session or a restart;
    • was opened by Gram to compact an idle thread.
  • Never while something is pending for the worker, so the stop is a wait:
    • a scheduled wake, a PR or review monitor, a run monitor, or merge-when-ready;
    • a declared PR dependency (depends_on);
    • a background shell, subagent or workflow, from the live session summary and the memo;
    • its own open ask_question;
    • an approval or question card waiting on the user.

New text the worker receives (quoted exactly)

When its turn ended silently:

Your turn ended without a report. Don't start new work. Call report once, saying what is done, what you tried and ruled out, and what remains, then end your turn.

When its orchestrator stopped it:

Your orchestrator stopped you. Don't start new work. Call report once, saying what is done, what you tried and ruled out, and what remains, then end your turn.

No role prompt or tool description changes in this PR.

Tests

  • New wrap-up.test.ts, 29 tests:
    • 20 unit tests of wrapUpDue: the two firing cases, one case per exclusion, and the notice text.
    • 9 service tests:
      • the digest carries the wrap-up's report and not "without reporting";
      • once only: a silent wrap-up is reported as silent and gets no second, and a later stop gets its own;
      • stop_thread sends the "stopped" text;
      • an interrupt by someone other than the orchestrator sends none;
      • a user who steered into the turn holds it, so none;
      • a paused feature gets none;
      • a scheduled wake gets none;
      • a running background shell gets none, then one once it ended;
      • an open ask_question gets none.
  • 53 existing tests changed: 48 in orchestration and 5 scenarios of one app-tools test. In 12 files, fake workers end turns with nothing pending and no report, which is exactly a silent stop, so those tests now get the wrap-up and assert it. On this PR's base, all 200 tests in the 11 orchestration files pass, so every one of those 48 failures came from this change.
    • The new harness helper stopSilently(session) ends the turn, waits for the wrap-up and requires its exact text (otherwise the test dies), then ends that turn silently too.
    • Assertions on send counts and indices moved by one where the wrap-up is now among what the worker was sent, each with a comment.
    • One assertion changed meaning: "delivers a foreground stall…" now sees last tool none, because the stop the orchestrator hears of is the wrap-up turn's. It no longer sees the original turn's Bash.
    • Per file: service-closing 12, app-tools/pr-tools-merge-launch 5 (one line shared by the scenarios of one test), service-digests 7, service-features 7, service-restarts 5, tools 5, service-sessions 4, service-wakes 3, workspace-pause 2, feature-cards 1, held-briefs 1, service 1.
  • Harness change for review: completeTurn used to poll for the thread's idle status and then wait for the service to count the turn end. For a worker the service follows, the wrap-up turn can start before any poll sees idle, so it failed every time. For those workers it now waits only for the service to count the turn end. The service counts it only after the runtime has ended the turn, so I don't think the wait is any looser. Threads the service doesn't follow still wait for idle.
  • No assertion was removed or skipped, and no timeout or permission guard was weakened.

Commands

  • bun run test packages/server/src/orchestration packages/server/src/settings packages/server/src/app-tools packages/server/src/git/merge-after-local-check.test.ts: 74 files, 1333 passed and 5 failed. All 5 were scenarios of an armed worker PR in pr-tools-merge-launch.test.ts. After fixing them, that file passes on its own (32/32).
  • bun run test packages/server/src/orchestration/wrap-up.test.ts: 29 passed
  • GOMEMLIMIT=2GiB bun run typecheck --singleThreaded: pass
  • bun run check:scaffold: pass
  • bun run fmt:check: pass

Known limits

  • Only synthetic: fake agents in the service harness. Not qualified against a live Claude or Codex worker.
  • If the wrap-up notice is queued but never runs (the agent fails to open, say), the stop stays unreported until the worker's next turn end or error. Errors still wake the orchestrator as before.
  • A wrap-up is not sent through a workspace or feature pause, and the pause's own resume notice is unchanged.

…ting

When a worker's turn ends with no report and nothing pending, or its
orchestrator stops it with stop_thread, Gram sends it one turn in the same
session asking it to stop and report what is done, what it tried and ruled
out, and what remains. The digest waits for that report instead of saying
the worker ended its turn without reporting.

Fires once per stop, and never for a worker that is paused, user-held,
archived, at a usage limit, errored, cut off by a restart, or waiting on a
wake, monitor, merge, dependency, background task, open question or
pending request.
@ANT1OFF
ANT1OFF merged commit 36a07fb into main Nov 2, 2026
23 checks passed
@ANT1OFF
ANT1OFF deleted the feat/worker-wrapup-prompts branch November 2, 2026 08:16
ANT1OFF added a commit that referenced this pull request Nov 2, 2026
Desktop release metadata proposal only. Merging this PR does not publish
or tag Gram; follow docs/RELEASING.md to release.
---


## [1.4.0](v1.3.1...v1.4.0)
(2026-11-02)


### Features

* **agents:** identify Gram's Claude processes as the gram entrypoint
([#91](#91))
([f7dd703](f7dd703))
* **desktop:** run on Linux
([#82](#82))
([1d37deb](1d37deb))
* **desktop:** turn voice input off on Linux with one check
([#79](#79))
([470ff8b](470ff8b))
* **orchestration:** a person's PR comment wakes the worker that owns it
([#86](#86))
([19bdc1c](19bdc1c))
* **orchestration:** a wrap-up turn when a worker stops without
reporting ([#88](#88))
([36a07fb](36a07fb))
* **orchestration:** workers say what they ruled out and check before
repeating ([#90](#90))
([8bdce01](8bdce01))
* **release:** publish and update Linux AppImage and .deb
([#95](#95))
([c33c464](c33c464))
* **voice:** an accurate mode with a Parakeet pass on each pause
([#83](#83))
([2b04a52](2b04a52))
* **voice:** hold one key to talk, with a tone at each end
([#87](#87))
([1a9b3ef](1a9b3ef))
* **voice:** start capture at the press, and an opt-in ready microphone
([#81](#81))
([505d240](505d240))
* **web:** a Done section that keeps the last 7 days
([#65](#65))
([8c7bd49](8c7bd49))
* **web:** Done lists every finished chat
([#67](#67))
([c91d9ac](c91d9ac))
* **web:** one attention count, quieter rows, a sidebar and board that
agree ([#92](#92))
([f193da4](f193da4))
* **web:** orchestrator → workers in the sidebar, a board that matches
([#80](#80))
([686e737](686e737))


### Bug Fixes

* **desktop:** harden the Linux key vault and autostart entry
([#85](#85))
([4b35eac](4b35eac))
* **e2e:** hold the t-flaky turn while the smoke test reads its opening
line ([#93](#93))
([2edc4ac](2edc4ac))
* **e2e:** release the dictation tap as soon as a word is heard
([#94](#94))
([7781194](7781194))
* prepare only approved local-check sources
([#32](#32))
([af3fab7](af3fab7))
* repair main after [#32](#32)
([#68](#68))
([ce352e6](ce352e6))
* **voice:** keep the last word after release
([#78](#78))
([cc40a47](cc40a47))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: gram-release-please[bot] <351321620+gram-release-please[bot]@users.noreply.github.com>
Co-authored-by: Johan <32440065+ANT1OFF@users.noreply.github.com>
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