Skip to content

fix: keep no-change OMP watcher turns silent - #265

Merged
dnth merged 4 commits into
mainfrom
fm/fm-silent-wake-turns
Oct 20, 2026
Merged

dnth merged 4 commits into
mainfrom
fm/fm-silent-wake-turns

Conversation

@dnth

@dnth dnth commented Oct 20, 2026

Copy link
Copy Markdown
Owner

Intent

Stop the primary firstmate (OMP harness) from sending "no-change" chat lines to the captain on turns that were started only by a watcher wake and found nothing for him. Captain approved building this on 20 Oct. This is firstmate shared tracked material (.omp/extensions, docs, tests), so firstmate-coding-guidelines apply.

Problem (measured): a 48 h audit found 43% of firstmate's chat texts were no-change lines ("Captain, standing by." 84x, "Nothing new needs you." ...), burying real captain asks. AGENTS.md section 8 already says waiting is silent and no-change updates are not captain-facing, but on OMP a turn that ends with no visible text triggers OMP's own recovery nudge ("You said you would continue with a tool call or action but stopped ... Give the final answer now", up to 3 attempts), so firstmate emits a filler line to stop the nudge loop.

Requirement: trace the real cause first (not a phrase filter first); find under exactly which conditions the nudge fires and whether OMP offers a supported way to end a turn deliberately silent (extension hook, turn-end API, setting, tool); prefer the supported mechanism; only if none exists, use the smallest extension-side approach (a fixed marker the Firstmate OMP extension recognises, which stops the nudge and hides the marker). A phrase filter may only be a backstop for wake-only turns and must never hide anything on a turn the captain started.

Constraints: captain-initiated turns are never suppressed. Real outcomes, decisions, PR-ready and failure reports from wake turns still show. No change to watcher or wake semantics, wake draining or acknowledgement, or turn-end supervision guards (docs/turnend-guard.md). Nothing was tested on the live primary session; everything ran in isolation.

Acceptance criteria:

  • AC1: state the exact trigger condition of the OMP recovery nudge with file:line evidence, and whether a supported silent-stop mechanism exists.
  • AC2: an automated test shows a wake-only turn with nothing captain-facing ends with no visible chat text and triggers no recovery nudge, while a captain-initiated turn and a wake turn with a real outcome still show their text. Later requirement (review advisory): the proof must cover every streamed frame, not only the final render, so a marker that streams visibly before being rewritten does not satisfy AC2.
  • AC3: replay the suppression rule over the 19-20 Oct primary transcript (streamed, never loaded whole); report how many messages would be hidden and list every hidden message that was not a no-change line (target zero).
  • AC4: AGENTS.md or the OMP supervision protocol doc describes the silent-turn mechanism in one or two lines.

Findings (put these in the PR body, per firstmate):

  • The live omp is the compiled binary 18.10.1; the global node_modules copy (17.3.4) is stale, so the evidence cites @oh-my-pi/pi-coding-agent 18.10.1 source.
  • Trigger: features.unexpectedStopDetection is an enum none|mechanical|smart defaulting to mechanical (src/session/settings.ts:1052-1056); no override is configured. src/session/turn-recovery.ts:1108-1195 (#handleUnexpectedAssistantStop, invoked at src/session/agent-session.ts:4381-4385) retries a final message with stopReason "stop", no tool call, no visible text, and a thinking block with non-empty thinking and signature (src/session/unexpected-stop-classifier.ts:42-61), in both mechanical and smart modes (:1127-1135), up to 3 times (:93). A stop with visible text is never retried in mechanical mode (:1136-1138). A signed thinking block counts as actionable, so these stops bypass the empty-stop path (src/session/messages.ts:544, :568). Reproduced in isolation on the 18.10.1 SDK and on the installed binary with a local stand-in model: a thinking-only reply drew 3 retries, a text reply none.
  • No supported per-turn silent stop exists: the setting is session-wide (captain turns included) and not settable by extensions; acceptTerminalEmptyStop is internal, absent from the extension sendMessage options type, and covers only empty stops, not the signed thinking-only stop Opus produces; yield is the subagent terminal protocol; rewriting text to empty via the supported assistant_message hook turns the message back into a retried thinking-only stop.
  • Replay on the 19-20 Oct transcript through the real handlers: 149 of 440 final text messages would be hidden (an earlier run against the then-shorter live file gave 146 of 434), all pure no-change lines; hidden messages that were not no-change lines: 0. Caveat: the backstop phrase list was written after reading this same transcript, so zero false positives shows the rule is strict, not that the phrase list generalizes.

Decisions and tradeoffs:

  • Marker is "…" (one ellipsis character), emitted by the model as its whole final message. It is text, so the mechanical retry never fires, and OMP's chat view (pi-tui canonicalizeMessage) renders text made only of dots, ellipses, and blank space as nothing in every streamed frame. An earlier [fm:silent] marker rewritten by the assistant_message hook was dropped because it streamed visibly before the rewrite.
  • installSilentWakeTurns in .omp/extensions/fm-primary-omp.ts tracks an episode (every message since the last final assistant message that carried text). Backstop: within an episode a watcher wake opened, with no captain input (user message or attribution "user" custom message such as a skill prompt) and no captain-facing branch merge (fm-branch-merge without the routine boat prefix), a final message that is one or two whole no-change sentences is rewritten to the marker; a backstop-caught line can still stream briefly, which is why the marker is the primary path. Captain guard: in an episode the captain joined, a dot-only final text is emptied so OMP re-prompts for a visible answer as for any silent stop; providers drop empty text blocks. Episodes started by async job results are deliberately out of scope.
  • The routine branch-merge prefix constant moved into the shared lib/fm-branch-dispatch.ts so the primary can tell routine notes from captain-facing merges; branch behavior is unchanged.
  • The logic lives inline in fm-primary-omp.ts as a named export (no new closure file), registered before the primary's own message_start handler.
  • Tests: tests/fm-omp-primary.test.sh gains a portable episode-rule test and an opt-in real-SDK test (FM_OMP_PACKAGE_DIR, local stand-in model streaming delayed chunks, every streamed frame rendered through OMP's assistant message component). docs/verification/runtime-backends.md records the dated 18.10.1 run. Known limits: smart mode's judge on "…" is unproven; OMP older than 18.4.4 lacks the assistant_message hook (marker still works, backstop and captain guard do not); tests/fm-omp-branch-types.test.sh already fails with two unrelated errors.

What Changed

  • End empty watcher-wake turns with …, which OMP renders invisibly throughout streaming without triggering mechanical recovery. Add a wake-only no-change backstop, preserve substantive outcomes and captain replies, and request a visible answer when the captain receives a dot-only response.
  • Share the routine branch-merge prefix so captain-facing merges prevent suppression. Document the silent-turn protocol and OMP 18.10.1 findings: signed thinking-only stops trigger recovery, and no supported per-turn silent-stop API exists.
  • Add episode-rule coverage and an opt-in real-SDK regression that checks every streamed frame, recovery nudges, captain replies, and real outcomes. Record isolated verification and compatibility limits, including possible brief streaming of backstop-caught lines.

Risk Assessment

✅ Low: Captain, the change is bounded, preserves captain-facing outcomes, and the prior fix correctly resets episode state on session replacement and aborts.

Testing

Focused regressions and isolated OMP 18.10.1 sessions passed streaming visibility, recovery, captain guards, substantive reports, branch classification, session replacement, and abort checks. Captured rendered HTML, frame data, and CLI transcripts. Historical replay remains untested because the required private transcript was unavailable within the authorized boundary. No source changes remain.

  • Live validation: ✅ go - 7 of 8 scenarios driven live against the product
Scenario Result Live Evidence
A quiet watcher turn ends with the marker, renders nothing in every streamed frame, and receives no recovery nudge ✅ pass live Live OMP scenario transcript: thinking-only wake; marked wake. Every streamed frame.
A captain receives a visible reply, including a recovery prompt when the model attempts a silent marker ✅ pass live Live OMP scenario transcript: captain no-change; captain marker
Watcher PR-ready, failure, decision, and mixed no-change-plus-fact reports remain visible ✅ pass live Live OMP scenario transcript: wake with outcome and wake report cases
The backstop hides a settled watcher no-change line but preserves the same reply after an unrelated async result ✅ pass live Live OMP scenario transcript: no-change wake; async result outside watcher scope
Captain-attributed input and substantive branch merges prevent suppression, while routine boat notes permit it ✅ pass live Live OMP scenario transcript: captain skill joins wake; captain-facing branch merge joins wake; routine branch merge joins wake
Starting a new session clears both leftover episode flags ✅ pass live Live OMP scenario transcript: new session clears unfinished captain episode; new session clears unfinished wake episode
After deliberately aborting a captain or watcher turn, the next quiet wake ends silently without retry ✅ pass live Live OMP scenario transcript: deliberate aborts and subsequent wakes; focused episode-rule regression covers error retention
Replay the October 19–20 primary transcript and enumerate any suppressed substantive messages ⏸️ untested no A bounded worktree search found no historical transcript. This task explicitly prohibits reading operator private data, so accessing the original session is outside the available authority. Disposable…
Evidence: Live OMP scenario transcript
{"label":"thinking-only wake","calls":2,"nudged":true,"text":"…","lines":[],"streamed":[]}
{"label":"marked wake","calls":1,"nudged":false,"text":"…","lines":[],"streamed":[]}
{"label":"no-change wake","calls":1,"nudged":false,"text":"…","lines":[],"streamed":[["Cap"],["Cap"],["Captai"],["Captain,"],["Captain, sta"],["Captain, standi"],["Captain, standing"],["Captain, standing by."],["Captain, standing by."]]}
{"label":"wake with outcome","calls":1,"nudged":false,"text":"Captain, the fix is ready for review: https://github.com/example/repo/pull/7","lines":["Captain, the fix is ready for review: https://github.com/example/repo/pull/7"],"streamed":[["Cap"],["Cap"],["Captai"],["Captain,"],["Captain, the"],["Captain, the fi"],["Captain, the fix i"],["Captain, the fix is ready for"],["Captain, the fix is ready for"],["Captain, the fix is ready for"],["Captain, the fix is ready for"],["Captain, the fix is ready for rev"],["Captain, the fix is ready for review"],["Captain, the fix is ready for review: h"],["Captain, the fix is ready for review: http"],["Captain, the fix is ready for review: https:/"],["Captain, the fix is ready for review: https://gi"],["Captain, the fix is ready for review: https://githu"],["Captain, the fix is ready for review: https://github.c"],["Captain, the fix is ready for review: https://github.com/"],["Captain, the fix is ready for review: https://github.com/example/repo"],["Captain, the fix is ready for review: https://github.com/example/repo"],["Captain, the fix is ready for review: https://github.com/example/repo"],["Captain, the fix is ready for review: https://github.com/example/repo"],["Captain, the fix is ready for review: https://github.com/example/repo/pu"],["Captain, the fix is ready for review: https://github.com/example/repo/pull/"],["Captain, the fix is ready for review: https://github.com/example/repo/pull/7"],["Captain, the fix is ready for review: https://github.com/example/repo/pull/7"]]}
{"label":"captain no-change","calls":1,"nudged":false,"text":"Captain, standing by.","lines":["Captain, standing by."],"streamed":[["Cap"],["Cap"],["Captai"],["Captain,"],["Captain, sta"],["Captain, standi"],["Captain, standing"],["Captain, standing by."],["Captain, standing by."]]}
{"label":"captain marker","calls":2,"nudged":true,"text":"Captain, CI on main is green.","lines":["Captain, CI on main is green."],"streamed":[["Captain,"],["Captain,"],["Captain,"],["Captain,"],["Captain,"],["Captain, CI"],["Captain, CI on"],["Captain, CI on mai"],["Captain, CI on main i"],["Captain, CI on main is g"],["Captain, CI on main is gree"],["Captain, CI on main is green."],["Captain, CI on main is green."]]}
{"label":"wake report: Captain, no change. Deployment failed; I need your decision.","calls":1,"nudged":false,"text":"Captain, no change. Deployment failed; I need your decision.","lines":["Captain, no change. Deployment failed; I need your decision."],"streamed":[["Cap"],["Cap"],["Captai"],["Captain,"],["Captain, no"],["Captain, no change. Depl"],["Captain, no change. Depl"],["Captain, no change. Depl"],["Captain, no change. Depl"],["Captain, no change. Deploym"],["Captain, no change. Deployment"],["Captain, no change. Deployment fa"],["Captain, no change. Deployment faile"],["Captain, no change. Deployment failed;"],["Captain, no change. Deployment failed; I n"],["Captain, no change. Deployment failed; I need"],["Captain, no change. Deployment failed; I need yo"],["Captain, no change. Deployment failed; I need your"],["Captain, no change. Deployment failed; I need your decision."],["Captain, no change. Deployment failed; I need your decision."],["Captain, no change. Deployment failed; I need your decision."],["Captain, no change. Deployment failed; I need your decision."]]}
{"label":"wake report: Captain, CI failed on the release branch.","calls":1,"nudged":false,"text":"Captain, CI failed on the release branch.","lines":["Captain, CI failed on the release branch."],"streamed":[["Cap"],["Cap"],["Captai"],["Captain,"],["Captain, CI"],["Captain, CI fai"],["Captain, CI failed"],["Captain, CI failed on the rele"],["Captain, CI failed on the rele"],["Captain, CI failed on the rele"],["Captain, CI failed on the rele"],["Captain, CI failed on the release"],["Captain, CI failed on the release br"],["Captain, CI failed on the release branc"],["Captain, CI failed on the release branch."],["Captain, CI failed on the release branch."]]}
{"label":"wake report: Captain, choose whether to land PR 7.","calls":1,"nudged":false,"text":"Captain, choose whether to land PR 7.","lines":["Captain, choose whether to land PR 7."],"streamed":[["Cap"],["Cap"],["Captai"],["Captain, choose wh"],["Captain, choose wh"],["Captain, choose wh"],["Captain, choose wh"],["Captain, choose wheth"],["Captain, choose whether"],["Captain, choose whether to"],["Captain, choose whether to lan"],["Captain, choose whether to land P"],["Captain, choose whether to land PR 7"],["Captain, choose whether to land PR 7."],["Captain, choose whether to land PR 7."]]}
{"label":"async result outside watcher scope","calls":1,"nudged":false,"text":"Captain, standing by.","lines":["Captain, standing by."],"streamed":[["Cap"],["Cap"],["Captai"],["Captain,"],["Captain, sta"],["Captain, standi"],["Captain, standing"],["Captain, standing by."],["Captain, standing by."]]}
{"label":"captain skill joins wake","calls":1,"nudged":false,"text":"Captain, standing by.","lines":["Captain, standing by."],"streamed":[["Cap"],["Cap"],["Captai"],["Captain,"],["Captain, sta"],["Captain, standi"],["Captain, standing"],["Captain, standing by."],["Captain, standing by."]]}
{"label":"captain-facing branch merge joins wake","calls":1,"nudged":false,"text":"Captain, standing by.","lines":["Captain, standing by."],"streamed":[["Cap"],["Cap"],["Captai"],["Captain,"],["Captain, sta"],["Captain, standi"],["Captain, standing"],["Captain, standing by."],["Captain, standing by."]]}
{"label":"routine branch merge joins wake","calls":1,"nudged":false,"text":"…","lines":[],"streamed":[["Cap"],["Cap"],["Captai"],["Captain,"],["Captain, standing by."],["Captain, standing by."],["Captain, standing by."],["Captain, standing by."],["Captain, standing by."]]}
{"label":"new session clears unfinished captain episode","calls":1,"nudged":false,"text":"…","lines":[],"streamed":[]}
{"label":"new session clears unfinished wake episode","calls":1,"nudged":false,"text":"Captain, standing by.","lines":["Captain, standing by."],"streamed":[["Cap"],["Cap"],["Captai"],["Captain,"],["Captain, sta"],["Captain, standi"],["Captain, standing"],["Captain, standing by."],["Captain, standing by."]]}
{"label":"wake after captain abort","calls":1,"nudged":false,"text":"…","lines":[],"streamed":[]}
{"label":"wake after wake abort","calls":1,"nudged":false,"text":"…","lines":[],"streamed":[]}
{"label":"wake after new session","calls":1,"nudged":false,"text":"…","lines":[],"streamed":[]}
{"label":"captain after new session","calls":1,"nudged":false,"text":"Captain, standing by.","lines":["Captain, standing by."],"streamed":[["Captain, standi"],["Captain, standi"],["Captain, standi"],["Captain, standi"],["Captain, standi"],["Captain, standi"],["Captain, standing"],["Captain, standing by."],["Captain, standing by."]]}
ok - real OMP: silent wake turns draw no retry and render nothing in any streamed frame, captain turns and real outcomes stay visible
Evidence: Actual OMP rendered chat frames
<!doctype html><meta charset="utf-8"><title>OMP captured frames</title><h1>OMP 18.10.1 captured chat frames</h1><p>Exact lines from the real AssistantMessageComponent.render(80), ANSI colors removed. Empty output leaves the frame blank. Component capture, not the full terminal shell.</p>
<h2>thinking-only wake</h2>
<p>Provider calls: 2; recovery nudge: True</p><h3>Final output</h3><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre><details><summary>Every streamed frame (14)</summary>
<p>Frame 1</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 2</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 3</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 4</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 5</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 6</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 7</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 8</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 9</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 10</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 11</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 12</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 13</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 14</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
</details>
<h2>marked wake</h2>
<p>Provider calls: 1; recovery nudge: False</p><h3>Final output</h3><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre><details><summary>Every streamed frame (10)</summary>
<p>Frame 1</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 2</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 3</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 4</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 5</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 6</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 7</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 8</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 9</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 10</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
</details>
<h2>no-change wake</h2>
<p>Provider calls: 1; recovery nudge: False</p><h3>Final output</h3><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre><details><summary>Every streamed frame (12)</summary>
<p>Frame 1</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 2</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 3</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 4</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Cap</pre>
<p>Frame 5</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Cap</pre>
<p>Frame 6</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captai</pre>
<p>Frame 7</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain,</pre>
<p>Frame 8</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, sta</pre>
<p>Frame 9</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standi</pre>
<p>Frame 10</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standing</pre>
<p>Frame 11</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standing by.</pre>
<p>Frame 12</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standing by.</pre>
</details>
<h2>wake with outcome</h2>
<p>Provider calls: 1; recovery nudge: False</p><h3>Final output</h3><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, the fix is ready for review: https://github.com/example/repo/pull/7</pre><details><summary>Every streamed frame (32)</summary>
<p>Frame 1</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 2</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 3</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 4</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 5</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Cap</pre>
<p>Frame 6</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Cap</pre>
<p>Frame 7</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captai</pre>
<p>Frame 8</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain,</pre>
<p>Frame 9</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, the</pre>
<p>Frame 10</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, the fi</pre>
<p>Frame 11</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, the fix i</pre>
<p>Frame 12</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, the fix is ready for</pre>
<p>Frame 13</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, the fix is ready for</pre>
<p>Frame 14</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, the fix is ready for</pre>
<p>Frame 15</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, the fix is ready for</pre>
<p>Frame 16</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, the fix is ready for rev</pre>
<p>Frame 17</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, the fix is ready for review</pre>
<p>Frame 18</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, the fix is ready for review: h</pre>
<p>Frame 19</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, the fix is ready for review: http</pre>
<p>Frame 20</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, the fix is ready for review: https:/</pre>
<p>Frame 21</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, the fix is ready for review: https://gi</pre>
<p>Frame 22</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, the fix is ready for review: https://githu</pre>
<p>Frame 23</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, the fix is ready for review: https://github.c</pre>
<p>Frame 24</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, the fix is ready for review: https://github.com/</pre>
<p>Frame 25</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, the fix is ready for review: https://github.com/example/repo</pre>
<p>Frame 26</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, the fix is ready for review: https://github.com/example/repo</pre>
<p>Frame 27</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, the fix is ready for review: https://github.com/example/repo</pre>
<p>Frame 28</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, the fix is ready for review: https://github.com/example/repo</pre>
<p>Frame 29</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, the fix is ready for review: https://github.com/example/repo/pu</pre>
<p>Frame 30</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, the fix is ready for review: https://github.com/example/repo/pull/</pre>
<p>Frame 31</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, the fix is ready for review: https://github.com/example/repo/pull/7</pre>
<p>Frame 32</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, the fix is ready for review: https://github.com/example/repo/pull/

... [12243 bytes truncated] ...

 solid #aaa;padding:8px">Captain, standi</pre>
<p>Frame 7</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standing</pre>
<p>Frame 8</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standing by.</pre>
<p>Frame 9</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standing by.</pre>
</details>
<h2>captain-facing branch merge joins wake</h2>
<p>Provider calls: 1; recovery nudge: False</p><h3>Final output</h3><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standing by.</pre><details><summary>Every streamed frame (9)</summary>
<p>Frame 1</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Cap</pre>
<p>Frame 2</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Cap</pre>
<p>Frame 3</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captai</pre>
<p>Frame 4</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain,</pre>
<p>Frame 5</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, sta</pre>
<p>Frame 6</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standi</pre>
<p>Frame 7</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standing</pre>
<p>Frame 8</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standing by.</pre>
<p>Frame 9</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standing by.</pre>
</details>
<h2>routine branch merge joins wake</h2>
<p>Provider calls: 1; recovery nudge: False</p><h3>Final output</h3><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre><details><summary>Every streamed frame (9)</summary>
<p>Frame 1</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Cap</pre>
<p>Frame 2</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Cap</pre>
<p>Frame 3</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captai</pre>
<p>Frame 4</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain,</pre>
<p>Frame 5</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standing by.</pre>
<p>Frame 6</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standing by.</pre>
<p>Frame 7</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standing by.</pre>
<p>Frame 8</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standing by.</pre>
<p>Frame 9</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standing by.</pre>
</details>
<h2>new session clears unfinished captain episode</h2>
<p>Provider calls: 1; recovery nudge: False</p><h3>Final output</h3><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre><details><summary>Every streamed frame (6)</summary>
<p>Frame 1</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 2</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 3</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 4</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 5</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 6</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
</details>
<h2>new session clears unfinished wake episode</h2>
<p>Provider calls: 1; recovery nudge: False</p><h3>Final output</h3><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standing by.</pre><details><summary>Every streamed frame (9)</summary>
<p>Frame 1</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Cap</pre>
<p>Frame 2</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Cap</pre>
<p>Frame 3</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captai</pre>
<p>Frame 4</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain,</pre>
<p>Frame 5</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, sta</pre>
<p>Frame 6</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standi</pre>
<p>Frame 7</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standing</pre>
<p>Frame 8</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standing by.</pre>
<p>Frame 9</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standing by.</pre>
</details>
<h2>captain turn deliberately aborted</h2>
<pre>aborted</pre>
<h2>wake after captain abort</h2>
<p>Provider calls: 1; recovery nudge: False</p><h3>Final output</h3><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre><details><summary>Every streamed frame (7)</summary>
<p>Frame 1</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 2</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 3</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 4</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 5</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 6</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 7</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
</details>
<h2>wake turn deliberately aborted</h2>
<pre>aborted</pre>
<h2>wake after wake abort</h2>
<p>Provider calls: 1; recovery nudge: False</p><h3>Final output</h3><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre><details><summary>Every streamed frame (7)</summary>
<p>Frame 1</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 2</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 3</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 4</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 5</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 6</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 7</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
</details>
<h2>wake after new session</h2>
<p>Provider calls: 1; recovery nudge: False</p><h3>Final output</h3><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre><details><summary>Every streamed frame (7)</summary>
<p>Frame 1</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 2</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 3</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 4</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 5</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 6</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
<p>Frame 7</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px"></pre>
</details>
<h2>captain after new session</h2>
<p>Provider calls: 1; recovery nudge: False</p><h3>Final output</h3><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standing by.</pre><details><summary>Every streamed frame (9)</summary>
<p>Frame 1</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standi</pre>
<p>Frame 2</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standi</pre>
<p>Frame 3</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standi</pre>
<p>Frame 4</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standi</pre>
<p>Frame 5</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standi</pre>
<p>Frame 6</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standi</pre>
<p>Frame 7</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standing</pre>
<p>Frame 8</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standing by.</pre>
<p>Frame 9</p><pre style="min-height:1em;border:1px solid #aaa;padding:8px">Captain, standing by.</pre>
</details>
Evidence: Focused regression results
ok - OMP primary silences only marker or no-change finals of wake-only episodes and re-asks silent captain turns
ok - real OMP: silent wake turns draw no retry and render nothing in any streamed frame, captain turns and real outcomes stay visible
  • Evidence: Every streamed frame (local file: ~/.no-mistakes/evidence/01M5BGZB4PZNGE3E9FE5XNNJGY/streamed-frames.json)
  • Evidence: Live driver (local file: ~/.no-mistakes/evidence/01M5BGZB4PZNGE3E9FE5XNNJGY/live-driver.mjs)
Evidence: Cause verification and validation limits
OMP binary: omp/18.10.1; isolated SDK: @oh-my-pi/pi-coding-agent 18.10.1; Bun 1.4.3.
Baseline: test_silent_wake_turn_episode_rules and test_silent_wake_turn_with_real_sdk in tests/fm-omp-primary.test.sh.
Real product: createAgentSession, initializeExtensions, installSilentWakeTurns, local delayed SSE provider, AssistantMessageComponent.render(80) on every message_update. Scripted model replies; real OMP recovery, event dispatch, lifecycle, handlers and renderer.
Cause verified in isolated 18.10.1 source: src/session/settings.ts:1052-1056 defaults unexpectedStopDetection to mechanical; src/session/unexpected-stop-classifier.ts:42-61 requires stop, no tool call, and nonblank text or signed thinking. src/session/turn-recovery.ts:1108-1195 retries signed-thinking-only replies; mechanical text replies return without retry at 1136-1138; max retries 3 at 93. Invoked at src/session/agent-session.ts:4381. Extension sendMessage options at src/extensibility/extensions/types.ts:1732-1736 omit internal acceptTerminalEmptyStop; no supported per-turn extension silent stop identified.
Extra guard setup initially used hidden idle custom append, which emits no message_start. Corrected to displayed append and awaited event delivery; all cases re-driven successfully. No product changes.
Historical replay: scoped rg --files found no October 19-20 transcript. Operator private session reads forbidden. Synthetic data cannot establish historical totals; provide sanitized transcript in worktree to repeat AC3.

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • 🚨 .omp/extensions/fm-primary-omp.ts:261 - Episode flags survive native session replacement. If a captain turn is interrupted before a text-bearing final, captainJoined remains true across /new or /resume; the next watcher-only turn has its ellipsis erased at line 284, restoring the recovery nudge this change should prevent. Both flags (lines 261–262) reset only on a text-bearing stop (lines 297–302), so wakeOpened can likewise leak into another session and incorrectly enable the backstop at line 286. Reset the episode classification on session_switch before the existing replacement handler delivers wakes (.omp/extensions/fm-primary-omp.ts:487).

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 7 of 8 scenarios driven live against the product
Scenario Result Live Evidence
A quiet watcher turn ends with the marker, renders nothing in every streamed frame, and receives no recovery nudge ✅ pass live Live OMP scenario transcript: thinking-only wake; marked wake. Every streamed frame.
A captain receives a visible reply, including a recovery prompt when the model attempts a silent marker ✅ pass live Live OMP scenario transcript: captain no-change; captain marker
Watcher PR-ready, failure, decision, and mixed no-change-plus-fact reports remain visible ✅ pass live Live OMP scenario transcript: wake with outcome and wake report cases
The backstop hides a settled watcher no-change line but preserves the same reply after an unrelated async result ✅ pass live Live OMP scenario transcript: no-change wake; async result outside watcher scope
Captain-attributed input and substantive branch merges prevent suppression, while routine boat notes permit it ✅ pass live Live OMP scenario transcript: captain skill joins wake; captain-facing branch merge joins wake; routine branch merge joins wake
Starting a new session clears both leftover episode flags ✅ pass live Live OMP scenario transcript: new session clears unfinished captain episode; new session clears unfinished wake episode
After deliberately aborting a captain or watcher turn, the next quiet wake ends silently without retry ✅ pass live Live OMP scenario transcript: deliberate aborts and subsequent wakes; focused episode-rule regression covers error retention
Replay the October 19–20 primary transcript and enumerate any suppressed substantive messages ⏸️ untested no A bounded worktree search found no historical transcript. This task explicitly prohibits reading operator private data, so accessing the original session is outside the available authority. Disposable…
  • omp --version confirmed 18.10.1.
  • bun add @oh-my-pi/pi-coding-agent@18.10.1 installed the matching SDK inside the disposable worktree directory.
  • Ran test_silent_wake_turn_episode_rules and test_silent_wake_turn_with_real_sdk from tests/fm-omp-primary.test.sh with workspace-local temporary directories.
  • Ran the evidence-preserved live-driver.mjs through Bun against real OMP sessions and a local delayed streaming provider.
  • Captured every assistant update through OMP's AssistantMessageComponent.render(80) and exported the actual rendered lines to JSON and HTML.
  • Inspected matching OMP recovery settings, classifier, retry implementation, and extension API types to verify the documented cause and API limitation.
  • Removed the disposable SDK, test homes, and drivers; git status --short was empty.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

dnth added 4 commits October 20, 2026 10:00
OMP 18.10.1 re-prompts a final message whose only content is signed
thinking (features.unexpectedStopDetection defaults to "mechanical";
turn-recovery.ts #handleUnexpectedAssistantStop), so a wake turn with
nothing for the captain could not end silently and firstmate wrote a
filler line such as "Captain, standing by." to stop the retry loop.

OMP has no per-turn silent-stop API, so the primary extension now:
- lets a wake-only turn end with the marker [fm:silent], which is text
  and therefore never retried in mechanical mode;
- rewrites that marker, or a whole-message no-change line, to "…" via
  the assistant_message hook; OMP's chat view renders dot-only text as
  nothing;
- only does so in an episode a watcher wake opened with no captain
  input (user message or user-attributed custom message) and no
  captain-facing branch merge; every other turn keeps its text.

The routine branch-merge prefix moves into the shared dispatch lib so
the primary can tell routine notes from captain-facing merges.

Replay on the 19-20 Oct primary transcript hides 146 of 434 final text
messages, all pure no-change lines.
The [fm:silent] marker streamed visibly before the assistant_message
rewrite replaced it. A wake-only turn now ends with "…" directly: it is
text, so OMP's mechanical unexpected-stop path never retries it, and
OMP's chat view renders dot-only text as nothing in every streamed
frame.

The no-change backstop keeps rewriting whole-message no-change lines on
wake-only episodes to the marker. In an episode the captain joined, a
dot-only final text is emptied so OMP re-prompts for a visible answer,
as it does for any silent stop.

The real-SDK test now streams replies in delayed chunks and asserts the
silent wake turn draws zero visible lines in every streamed frame.
@dnth
dnth merged commit 11a31af into main Oct 20, 2026
18 checks passed
@dnth
dnth deleted the fm/fm-silent-wake-turns branch October 20, 2026 03:51
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