Skip to content

opencode run teardown race after session.idle can create empty continuation assistant turn ("parts": []) #15267

@twm-dynamo

Description

@twm-dynamo

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

  1. Serialize run teardown with post-idle continuation enqueue.
  2. If continuation is posted, defer disposal until that turn reaches a terminal state (completed/failed/rejected).
  3. If run mode disallows continuation, reject/skip enqueue before posting the directive.

Acceptance criteria (testable)

  1. Running /ulw-loop ... --max-iterations=2 in opencode run never produces continuation assistant messages with "parts": [].
  2. Behavior is deterministic in both --strategy=continue and --strategy=reset.
  3. 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.
  4. A regression test covers the idle->dispose sequencing case where continuation posting and teardown are concurrent candidates.

Metadata

Metadata

Assignees

Labels

coreAnything pertaining to core functionality of the application (opencode server stuff)

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions