Skip to content

fix(workers): keep pi-family workers active when agent_end will continue - #11

Closed
guilhermexp wants to merge 5 commits into
mainfrom
repair/worker-activity-20260913
Closed

guilhermexp wants to merge 5 commits into
mainfrom
repair/worker-activity-20260913

Conversation

@guilhermexp

@guilhermexp guilhermexp commented Sep 18, 2026 •

Copy link
Copy Markdown
Owner

Intent

Revisar e publicar como PR para a main do fork (guilhermexp/comet) a branch repair/worker-activity-20260913. Objetivo: corrigir o lifecycle de workers pi-family — agent_end com willContinue=true é continuação já agendada pelo OMP, não settle terminal. O lifecycle-extension.js anunciava Stop incondicionalmente, o que fazia o painel piscar ocioso no meio do turno e, pior, alimentava o relógio de ociosidade da hibernação (crates/workers-unpeel/src/activity_bridge.rs usa o último evento de hook durável como sinal de atividade), tornando o worker candidato a hibernação no meio de um turno. O fix faz o emitter da família pi conservar Start até o fim real: pula o Stop quando event.willContinue === true, com testes no adapter setup.rs e artefatos OpenSpec do change fix-worker-turn-lifecycle (proposal/design/specs/tasks). A branch também contém um merge commit que apenas traz 24e08d1, já presente em main — o conteúdo efetivo novo é só esse fix. Trabalho 100% do fork: third_party/unpeel e crates/workers-unpeel não existem no upstream. Após o PR ser criado, a branch local e o worktree serão deletados; a remota fica até o merge.

What Changed

  • lifecycle-extension.js (the vendored pi-family lifecycle extension) no longer sends a Stop hook when agent_end arrives with willContinue === true, which means OMP has already scheduled a continuation or retry. Stop is still sent on a terminal end (willContinue === false) and on older events that don't have the flag. As a result, the panel no longer shows the worker as idle partway through a turn, and the hibernation idle clock no longer counts that false Stop as activity.
  • The test harness in adapter/setup.rs now collects every hook payload the extension emits. lifecycle_extension_reports_provider_session_identity checks two cases. In the first, a continuation followed by a terminal end sends only Start and then Stop. In the second, a legacy agent_end without the flag still sends Stop. Both cases also check that the session id and transcript path are kept.
  • Adds the OpenSpec change fix-worker-turn-lifecycle (proposal, design, spec delta, tasks). Updates crates/workers-unpeel/AGENTS.md with the willContinue rule and the test command, and adds a note to third_party/unpeel-upstream.toml with the new vendored_tree. The branch also commits files under graft/.cache/: four session JSON files and telemetry-repo-id.json.

🤖 Generated with Claude Code

Risk Assessment

✅ Low: The change is a one-guard fix in the pi-family lifecycle emitter. It skips Stop only when willContinue is strictly true, so the legacy path without the flag and terminal ends still emit Stop. A behavioral Bun harness test covers the continuation, terminal and legacy cases, and the vendored_tree hash matches the committed third_party/unpeel tree.

Testing

I ran the targeted adapter test in unpeel-core, which passed, but that is a unit test, so its scenario is marked untested for the live contract. I then drove the real OMP 18.2.5 binary with the shipped lifecycle extension against a fake provider that drops the connection mid-stream, which makes OMP send agent_end with willContinue: true. The pre-fix extension sent a Stop in the middle of the turn (the bug, reproduced). The fixed extension sends only Start until the real end of the turn, including across chained retries, and still sends exactly one Stop on success, on retry-budget exhaustion and on plain turns. The effect on the comet hibernation bridge is also untested, because that needs the full GUI app. This change has no visual surface; the evidence is the captured hook transcripts. I removed the cargo target dir from the worktree.

  • Live validation: ✅ go - 5 of 7 scenarios driven live against the product
Scenario Result Live Evidence
Before the fix (reproduction): OMP retries a dropped stream on its own (agent_end willContinue=true), and the old extension sends a Stop in the middle of the turn ✅ pass live live/capture-drop-before.jsonl + probe-drop-before.jsonl: OMP events start,end(willContinue:true),start,end → hooks Start,Stop,Start,Stop (see SUMMARY.txt)
After the fix: during an OMP retry continuation the worker stays in Start with no Stop in the middle of the turn, then gets exactly one Stop when the turn succeeds ✅ pass live live/capture-drop-after.jsonl: hooks Start,Start,Stop for the same willContinue:true sequence; OMP printed 'pong'
After the fix: two chained continuations (two drops) still send no Stop until the real end of the turn ✅ pass live live/capture-drop2-after.jsonl: OMP events end(willContinue:true) x2 then terminal end → hooks Start,Start,Start,Stop
Adversarial: every request fails, so OMP uses up its 10-retry budget and exits 1; the worker still gets exactly one terminal Stop and does not stay stuck in Start ✅ pass live live/capture-dropall-after.jsonl + omp-dropall-after.out ('Retry budget exhausted after 10 retries'): 10 willContinue:true ends, 11 Start hooks, 1 final Stop
Normal turns with no continuation (a single prompt, or two prompts sent one after another) still send a Start/Stop pair per turn, and the provider session identity is still in the payload ✅ pass live live/capture-after.jsonl (Start,Stop with session_id); live/capture-multi.jsonl (Start,Stop,Start,Stop for two terminal turns)
Adapter test (setup.rs): the rendered extension, run with bun, turns continuation, terminal and legacy agent_end events into the expected hooks for the pi, omp and prime-agent adapters ⏸️ untested no The previous payload only recorded this as a unit/harness test with a stubbed extension host (cargo test -p unpeel-core lifecycle_extension, 6 passed). That is not a live run of the product, so it doe…
Hibernation idle clock (crates/workers-unpeel activity_bridge) no longer sees a mid-turn Stop as the worker settling ⏸️ untested no This needs the full comet app with an unpeel-hosted OMP worker running long enough to reach hibernation, and there is no gpui render/automation harness here. Check it by running scripts/dev-demo.sh wi…
Evidence: Live OMP before/after hook transcripts summary
\### drop-before
provider requests: 2
-- raw OMP agent events (probe):
{"probe":"agent_start"}
{"probe":"agent_end","willContinue":true}
{"probe":"agent_start"}
{"probe":"agent_end"}
-- hooks emitted by lifecycle extension:
"Start" "Stop" "Start" "Stop" 
pong

\### drop-after
provider requests: 2
-- raw OMP agent events (probe):
{"probe":"agent_start"}
{"probe":"agent_end","willContinue":true}
{"probe":"agent_start"}
{"probe":"agent_end"}
-- hooks emitted by lifecycle extension:
"Start" "Start" "Stop" 
pong

\### drop2-after
provider requests: 3
-- raw OMP agent events (probe):
{"probe":"agent_start"}
{"probe":"agent_end","willContinue":true}
{"probe":"agent_start"}
{"probe":"agent_end","willContinue":true}
{"probe":"agent_start"}
{"probe":"agent_end"}
-- hooks emitted by lifecycle extension:
"Start" "Start" "Start" "Stop" 
pong

\### dropall-after
provider requests: 11
-- raw OMP agent events (probe):
{"probe":"agent_start"}
{"probe":"agent_end","willContinue":true}
{"probe":"agent_start"}
{"probe":"agent_end","willContinue":true}
{"probe":"agent_start"}
{"probe":"agent_end","willContinue":true}
{"probe":"agent_start"}
{"probe":"agent_end","willContinue":true}
{"probe":"agent_start"}
{"probe":"agent_end","willContinue":true}
{"probe":"agent_start"}
{"probe":"agent_end","willContinue":true}
{"probe":"agent_start"}
{"probe":"agent_end","willContinue":true}
{"probe":"agent_start"}
{"probe":"agent_end","willContinue":true}
{"probe":"agent_start"}
{"probe":"agent_end","willContinue":true}
{"probe":"agent_start"}
{"probe":"agent_end","willContinue":true}
{"probe":"agent_start"}
{"probe":"agent_end"}
-- hooks emitted by lifecycle extension:
"Start" "Start" "Start" "Start" "Start" "Start" "Start" "Start" "Start" "Start" "Start" "Stop" 
Retry budget exhausted after 10 retries: OpenAI completions stream closed before a finish_reason was received

\### multi
provider requests: 2
-- raw OMP agent events (probe):
{"probe":"agent_start"}
{"probe":"agent_end"}
{"probe":"agent_start"}
{"probe":"agent_end"}
-- hooks emitted by lifecycle extension:
"Start" "Stop" "Start" "Stop" 
pong

\### errchunk-before
provider requests: 2
-- raw OMP agent events (probe):
{"probe":"agent_start"}
{"probe":"agent_end"}
-- hooks emitted by lifecycle extension:
"Start" "Stop" 
pong
Evidence: Pre-fix hooks on OMP retry (bug reproduced)

OMP: agent_end{willContinue:true} → hooks Start, Stop, Start, Stop

{"hook_event_name":"Start","session_id":"01a0b2b2-4d74-73b2-8f04-61869cba857c"}
{"hook_event_name":"Stop","session_id":"01a0b2b2-4d74-73b2-8f04-61869cba857c"}
{"hook_event_name":"Start","session_id":"01a0b2b2-4d74-73b2-8f04-61869cba857c"}
{"hook_event_name":"Stop","session_id":"01a0b2b2-4d74-73b2-8f04-61869cba857c"}
Evidence: Post-fix hooks on OMP retry

OMP: agent_end{willContinue:true} → hooks Start, Start, Stop

{"hook_event_name":"Start","session_id":"01a0b2b2-8409-70e9-bd05-dae31133a983"}
{"hook_event_name":"Start","session_id":"01a0b2b2-8409-70e9-bd05-dae31133a983"}
{"hook_event_name":"Stop","session_id":"01a0b2b2-8409-70e9-bd05-dae31133a983"}
Evidence: Post-fix hooks when retry budget runs out
{"hook_event_name":"Start","session_id":"01a0b2b2-a2cc-73b1-b684-bb08e4f1959f"}
{"hook_event_name":"Start","session_id":"01a0b2b2-a2cc-73b1-b684-bb08e4f1959f"}
{"hook_event_name":"Start","session_id":"01a0b2b2-a2cc-73b1-b684-bb08e4f1959f"}
{"hook_event_name":"Start","session_id":"01a0b2b2-a2cc-73b1-b684-bb08e4f1959f"}
{"hook_event_name":"Start","session_id":"01a0b2b2-a2cc-73b1-b684-bb08e4f1959f"}
{"hook_event_name":"Start","session_id":"01a0b2b2-a2cc-73b1-b684-bb08e4f1959f"}
{"hook_event_name":"Start","session_id":"01a0b2b2-a2cc-73b1-b684-bb08e4f1959f"}
{"hook_event_name":"Start","session_id":"01a0b2b2-a2cc-73b1-b684-bb08e4f1959f"}
{"hook_event_name":"Start","session_id":"01a0b2b2-a2cc-73b1-b684-bb08e4f1959f"}
{"hook_event_name":"Start","session_id":"01a0b2b2-a2cc-73b1-b684-bb08e4f1959f"}
{"hook_event_name":"Start","session_id":"01a0b2b2-a2cc-73b1-b684-bb08e4f1959f"}
{"hook_event_name":"Stop","session_id":"01a0b2b2-a2cc-73b1-b684-bb08e4f1959f"}
Evidence: Fake provider used to force OMP continuations
import json, sys, time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
FAIL_FIRST = int(sys.argv[2]) if len(sys.argv) > 2 else 1
MODE = sys.argv[4] if len(sys.argv) > 4 else "503"
state = {"n": 0}
log = open(sys.argv[3] if len(sys.argv) > 3 else "provider.log", "a")
class H(BaseHTTPRequestHandler):
    def log_message(self, *a): pass
    def do_GET(self):
        body = json.dumps({"data":[{"id":"fake-model","object":"model"}]}).encode()
        self.send_response(200); self.send_header("Content-Type","application/json"); self.send_header("Content-Length",str(len(body))); self.end_headers(); self.wfile.write(body)
    def do_POST(self):
        n = int(self.headers.get("Content-Length","0")); self.rfile.read(n)
        state["n"] += 1
        log.write(f"{time.time():.3f} POST {self.path} #{state['n']}\n"); log.flush()
        if state["n"] <= FAIL_FIRST and MODE == "drop":
            self.send_response(200); self.send_header("Content-Type","text/event-stream"); self.end_headers()
            base={"id":"c0","object":"chat.completion.chunk","created":int(time.time()),"model":"fake-model"}
            self.wfile.write(("data: "+json.dumps({**base,"choices":[{"index":0,"delta":{"role":"assistant","content":"po"},"finish_reason":None}]})+"\n\n").encode()); self.wfile.flush()
            import socket; self.connection.shutdown(socket.SHUT_RDWR); self.close_connection=True; return
        if state["n"] <= FAIL_FIRST and MODE == "error-chunk":
            self.send_response(200); self.send_header("Content-Type","text/event-stream"); self.end_headers()
            self.wfile.write(("data: "+json.dumps({"error":{"message":"Internal server error, please retry","type":"server_error","code":500}})+"\n\n").encode()); self.wfile.flush(); return
        if state["n"] <= FAIL_FIRST and MODE == "empty":
            self.send_response(200); self.send_header("Content-Type","text/event-stream"); self.end_headers()
            base={"id":"c0","object":"chat.completion.chunk","created":int(time.time()),"model":"fake-model"}
            self.wfile.write(("data: "+json.dumps({**base,"choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":"stop"}]})+"\n\ndata: [DONE]\n\n").encode()); self.wfile.flush(); return
        if state["n"] <= FAIL_FIRST:
            body = b'{"error":{"message":"overloaded, retry","type":"server_error"}}'
            self.send_response(503); self.send_header("Content-Type","application/json"); self.send_header("Content-Length",str(len(body))); self.end_headers(); self.wfile.write(body); return
        self.send_response(200); self.send_header("Content-Type","text/event-stream"); self.end_headers()
        def ev(d): self.wfile.write(("data: "+json.dumps(d)+"\n\n").encode()); self.wfile.flush()
        base={"id":"c1","object":"chat.completion.chunk","created":int(time.time()),"model":"fake-model"}
        ev({**base,"choices":[{"index":0,"delta":{"role":"assistant","content":"pong"},"finish_reason":None}]})
        ev({**base,"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":5,"completion_tokens":1,"total_tokens":6}})
        self.wfile.write(b"data: [DONE]\n\n"); self.wfile.flush()
ThreadingHTTPServer(("127.0.0.1", int(sys.argv[1])), H).serve_forever()

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 5 of 7 scenarios driven live against the product
Scenario Result Live Evidence
Before the fix (reproduction): OMP retries a dropped stream on its own (agent_end willContinue=true), and the old extension sends a Stop in the middle of the turn ✅ pass live live/capture-drop-before.jsonl + probe-drop-before.jsonl: OMP events start,end(willContinue:true),start,end → hooks Start,Stop,Start,Stop (see SUMMARY.txt)
After the fix: during an OMP retry continuation the worker stays in Start with no Stop in the middle of the turn, then gets exactly one Stop when the turn succeeds ✅ pass live live/capture-drop-after.jsonl: hooks Start,Start,Stop for the same willContinue:true sequence; OMP printed 'pong'
After the fix: two chained continuations (two drops) still send no Stop until the real end of the turn ✅ pass live live/capture-drop2-after.jsonl: OMP events end(willContinue:true) x2 then terminal end → hooks Start,Start,Start,Stop
Adversarial: every request fails, so OMP uses up its 10-retry budget and exits 1; the worker still gets exactly one terminal Stop and does not stay stuck in Start ✅ pass live live/capture-dropall-after.jsonl + omp-dropall-after.out ('Retry budget exhausted after 10 retries'): 10 willContinue:true ends, 11 Start hooks, 1 final Stop
Normal turns with no continuation (a single prompt, or two prompts sent one after another) still send a Start/Stop pair per turn, and the provider session identity is still in the payload ✅ pass live live/capture-after.jsonl (Start,Stop with session_id); live/capture-multi.jsonl (Start,Stop,Start,Stop for two terminal turns)
Adapter test (setup.rs): the rendered extension, run with bun, turns continuation, terminal and legacy agent_end events into the expected hooks for the pi, omp and prime-agent adapters ⏸️ untested no The previous payload only recorded this as a unit/harness test with a stubbed extension host (cargo test -p unpeel-core lifecycle_extension, 6 passed). That is not a live run of the product, so it doe…
Hibernation idle clock (crates/workers-unpeel activity_bridge) no longer sees a mid-turn Stop as the worker settling ⏸️ untested no This needs the full comet app with an unpeel-hosted OMP worker running long enough to reach hibernation, and there is no gpui render/automation harness here. Check it by running scripts/dev-demo.sh wi…
  • cd third_party/unpeel/crates &amp;&amp; cargo test -p unpeel-core lifecycle_extension (6 passed: the setup.rs harness tests for pi/omp/prime-agent plus the --extension wiring tests)
  • Filled in lifecycle-extension.js from base 78c39bfe and target fda33e5c, the same way render_lifecycle_extension does, pointing it at a capturing notify script
  • Ran real omp -p --model fakeprov/fake-model -e probe.js -e ext-{before,after}.js in an isolated HOME, against a local fake provider (fake_provider.py) with these modes: 503, empty stop, error chunk, mid-stream drop x1/x2/always, and two separate prompts
  • The probe extension logged OMP's raw agent_start/agent_end events (including willContinue) next to the hooks the lifecycle extension sent
✅ **Document** - passed

✅ No issues found.

⚠️ **Lint** - 1 info
  • ℹ️ third_party/unpeel/runtimes/_shared/pi-family/adapter/setup.rs:136 - Standalone rustfmt wants to collapse the new run_lifecycle_harness(r#&#34;...&#34;#) call onto one line (and a pre-existing write_executable_script call at line 43). The file is outside the cargo fmt --all workspace, and the vendored code was never formatted. Reformatting it would also change the vendored_tree hash recorded in third_party/unpeel-upstream.toml, so it was left unchanged.
✅ **Push** - passed

✅ No issues found.

agent_end com willContinue=true é continuação já agendada, não settle terminal; o emitter da família pi conserva Start até o fim real.
@coderabbitai

coderabbitai Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: a4221eb2-d7c1-4eda-9e33-2674fa3c1743


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…ck fails at its first step, `cargo fmt --all -- --check`, on two files: `crates/ui/src/shell.rs:10371` (the `compaction_marker` test asserts) and `third_party/zui/crates/gpui/src/elements/img.rs:354`. This branch doesn't change either file. `git diff 78c39bf..HEAD` on both is empty, and running `cargo fmt --all -- --check` locally flags the same two spots. So the formatting drift was already on the base branch (`main` at 78c39bf) and isn't caused by the worker-lifecycle fix. The user intent says the only new content in this PR is the pi-family `willContinue` fix and its OpenSpec change. Reformatting unrelated UI and vendored zui code here would widen that scope. The right fix is a separate `cargo fmt --all` commit on `main`. Until that lands, this check will stay red on any PR based on the current `main`
…ck still fails at its first step, `cargo fmt --all -- --check`, on the same two files as last round: `crates/ui/src/shell.rs:10371` (the `compaction_marker` test asserts) and `third_party/zui/crates/gpui/src/elements/img.rs:354`. This branch doesn't touch either file. `git diff 78c39bf..HEAD` on both is empty. The PR only changes the pi-family lifecycle files (`lifecycle-extension.js`, `adapter/setup.rs`), the fix-worker-turn-lifecycle OpenSpec change, `crates/workers-unpeel/AGENTS.md`, `third_party/unpeel-upstream.toml` and some graft cache files. So the formatting drift was already on the base branch (`main` at 78c39bf). Reformatting unrelated UI code and vendored zui code here would go beyond the stated intent, which is that the only new content is the `willContinue` fix. The fix is a separate `cargo fmt --all` commit on `main`. Until that lands, this check will stay red on every PR based on the current `main`
…ck fails at its first step, `cargo fmt --all -- --check`. It flags only two files: `crates/ui/src/shell.rs:10371` (the `compaction_marker` test asserts) and `third_party/zui/crates/gpui/src/elements/img.rs:354`. I re-checked with `git diff 78c39bf..HEAD`, and this branch changes neither file. It only touches the pi-family lifecycle files (`lifecycle-extension.js` and `adapter/setup.rs`), the fix-worker-turn-lifecycle OpenSpec artifacts, `crates/workers-unpeel/AGENTS.md`, `third_party/unpeel-upstream.toml` and some graft cache files. So the formatting drift was already on the base branch (`main` at 78c39bf), not introduced by the `willContinue` fix. Reformatting unrelated UI code and vendored zui code here would go beyond the stated intent, which is that the only new content is the pi-family lifecycle fix. The fix is a separate `cargo fmt --all` commit on `main`. After that lands and this branch is rebased, the check can go green. Until then it will fail on every PR based on the current `main`
@guilhermexp guilhermexp closed this Oct 4, 2026
@guilhermexp
guilhermexp deleted the repair/worker-activity-20260913 branch October 4, 2026 16:06
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