Summary
In non-interactive opencode run, ULW/Ralph continuation can be injected after session.idle while run teardown/disposal is already starting. The second loop turn may be created with an empty assistant payload ("parts": []).
This is reproducible with /ulw-loop ... --max-iterations=2 under both --strategy=continue and --strategy=reset.
Environment
- OpenCode CLI:
1.2.14
- oh-my-opencode plugin:
3.8.5
- OS: Linux
Reproduction
opencode run "/ulw-loop Write TRACE_TWICE and do not promise completion --max-iterations=2"
opencode run "/ulw-loop Write TRACE_TWICE_RESET and do not promise completion --max-iterations=2 --strategy=reset"
Then inspect the session:
opencode session list --format json -n 3
opencode export <session_id> > /tmp/<session_id>.json
Expected
- If continuation is injected, the continuation assistant turn should contain a non-empty response payload.
- If run mode is intentionally single-pass, continuation should not be injected.
Actual
- Continuation directive is injected for loop 2.
- A follow-up assistant message exists but has empty content (
"parts": []).
Evidence
Exported sessions (directive present + empty assistant payload)
/tmp/ses_368d1cd6affeagO950PZdu6tid.json:279
/tmp/ses_368d1cd6affeagO950PZdu6tid.json:314
/tmp/ses_368d192b2ffeyCZLYWUS8MUyo8.json:295
/tmp/ses_368d192b2ffeyCZLYWUS8MUyo8.json:330
Trace ordering (idle/dispose timing overlaps continuation enqueue)
/tmp/ulw_trace_run.log:360
/tmp/ulw_trace_run.log:364
/tmp/ulw_trace_run.log:365
/tmp/ulw_trace_run.log:382
/tmp/ulw_trace_run.log:501
Related runtime signal during continuation startup
/tmp/ulw_trace_run.log:487
Working hypothesis
session.idle continuation enqueue and run-mode disposal are not serialized, allowing continuation to be posted during teardown and resulting in a partial second turn.
Local mitigation (plugin-side, not core fix)
Continuation hooks were disabled in run mode in local plugin bundle:
/home/wagner/.config/opencode/node_modules/oh-my-opencode/dist/index.js:58415
/home/wagner/.config/opencode/node_modules/oh-my-opencode/dist/index.js:58488
/home/wagner/.config/opencode/node_modules/oh-my-opencode/dist/index.js:58808
/home/wagner/.config/opencode/node_modules/oh-my-opencode/dist/index.js:58812
/home/wagner/.config/opencode/node_modules/oh-my-opencode/dist/index.js:58838
This avoids partial loop behavior in run mode but does not address runtime sequencing.
Requested core fix
- Serialize run teardown with post-idle continuation enqueue.
- If continuation is posted, defer disposal until that turn reaches a terminal state (completed/failed/rejected).
- If run mode disallows continuation, reject/skip enqueue before posting the directive.
Acceptance criteria (testable)
- Running
/ulw-loop ... --max-iterations=2 in opencode run never produces continuation assistant messages with "parts": [].
- Behavior is deterministic in both
--strategy=continue and --strategy=reset.
- Exactly one mode is enforced per run:
- continuation allowed: directive is injected and continuation response is non-empty, or
- continuation disallowed: no continuation directive is injected.
- A regression test covers the idle->dispose sequencing case where continuation posting and teardown are concurrent candidates.
Summary
In non-interactive
opencode run, ULW/Ralph continuation can be injected aftersession.idlewhile run teardown/disposal is already starting. The second loop turn may be created with an empty assistant payload ("parts": []).This is reproducible with
/ulw-loop ... --max-iterations=2under both--strategy=continueand--strategy=reset.Environment
1.2.143.8.5Reproduction
Then inspect the session:
Expected
Actual
"parts": []).Evidence
Exported sessions (directive present + empty assistant payload)
/tmp/ses_368d1cd6affeagO950PZdu6tid.json:279/tmp/ses_368d1cd6affeagO950PZdu6tid.json:314/tmp/ses_368d192b2ffeyCZLYWUS8MUyo8.json:295/tmp/ses_368d192b2ffeyCZLYWUS8MUyo8.json:330Trace ordering (idle/dispose timing overlaps continuation enqueue)
/tmp/ulw_trace_run.log:360/tmp/ulw_trace_run.log:364/tmp/ulw_trace_run.log:365/tmp/ulw_trace_run.log:382/tmp/ulw_trace_run.log:501Related runtime signal during continuation startup
/tmp/ulw_trace_run.log:487Working hypothesis
session.idlecontinuation enqueue and run-mode disposal are not serialized, allowing continuation to be posted during teardown and resulting in a partial second turn.Local mitigation (plugin-side, not core fix)
Continuation hooks were disabled in run mode in local plugin bundle:
/home/wagner/.config/opencode/node_modules/oh-my-opencode/dist/index.js:58415/home/wagner/.config/opencode/node_modules/oh-my-opencode/dist/index.js:58488/home/wagner/.config/opencode/node_modules/oh-my-opencode/dist/index.js:58808/home/wagner/.config/opencode/node_modules/oh-my-opencode/dist/index.js:58812/home/wagner/.config/opencode/node_modules/oh-my-opencode/dist/index.js:58838This avoids partial loop behavior in
runmode but does not address runtime sequencing.Requested core fix
Acceptance criteria (testable)
/ulw-loop ... --max-iterations=2inopencode runnever produces continuation assistant messages with"parts": [].--strategy=continueand--strategy=reset.