Skip to content

[FEATURE] background.extend() cannot inject messages into active session loops #35728

Closed as not planned

Description

@cardil

Feature hasn't been suggested before.

  • I have verified this feature I'm about to request hasn't been suggested before.

Note: #27898 is related but broader (observing progress + streaming output). This issue is specifically about injecting a pending user message into a running session -- the mechanism the TUI already uses.

Describe the enhancement you want to request

OpenCode's background.extend() queues a new runTask() after the current run finishes, rather than injecting a user message into the active session loop. This means there is no API mechanism for a parent agent to send mid-execution instructions to a running subagent -- even though the TUI allows users to type messages while an agent is working.

Context

This was discovered while investigating why OmO's task(task_id=..., run_in_background=true) continuation messages never reach active subagents. The primary bug is in OmO (manager.resume() skips running tasks entirely -- filed separately on code-yeongyu/oh-my-openagent). OmO can work around the issue with a cancel-and-resume pattern, but that interrupts the subagent mid-execution and risks side effects (e.g., a bash command interrupted mid-way).

The proper fix requires OpenCode to support injecting a pending user message into a running session -- the same mechanism the TUI already uses when a user types while an agent is working.

How extend() works today

packages/core/src/background-job.ts:256-290:

const extend: Interface["extend"] = Effect.fn("BackgroundJob.extend")(function* (input) {
  return yield* Effect.uninterruptibleMask((restore) =>
    Effect.gen(function* () {
      const tail = yield* Deferred.make<void>()
      const result = yield* SynchronizedRef.modify(state.jobs, (jobs) => {
        const job = jobs.get(input.id)
        if (!job || job.info.status !== "running") return [{ extended: false }, jobs]
        return [
          { extended: true, previous: job.tail, ... },
          new Map(jobs).set(input.id, { ...job, pending: job.pending + 1, ... }),
        ]
      })
      if (!result.extended) return false
      yield* fork(
        result.scope, input.id, result.token, result.sequence,
        Deferred.await(result.previous).pipe(     // ← waits for current run to finish
          Effect.andThen(restore(input.run)),       // ← only THEN starts the new prompt
          Effect.ensuring(Deferred.succeed(result.tail, undefined)),
        ),
      )
      return true
    }),
  )
})

extend() returns true ("extended") but the new run is queued behind the current run's tail Deferred. It executes only after the subagent's entire loop finishes. In practice, the subagent completes and the session closes before the queued run starts -- so the prompt is effectively lost.

How task.ts uses extend()

packages/opencode/src/tool/task.ts:242-257:

if (yield* background.extend({ id: nextSession.id, run: runTask() })) {
  return {
    output: renderOutput({
      sessionID: nextSession.id,
      state: "running",
      summary: "Background task updated",
      text: BACKGROUND_UPDATED,
    }),
  }
}

When extend() returns true, the tool reports "Background task updated" -- implying immediate delivery. The caller has no way to know the message is queued, not delivered.

Both code paths in task.ts fail for active sessions

Currently OmO's manager.resume() skips running tasks before reaching OpenCode's task tool. But if OmO were to fix its side and pass the prompt through, the OpenCode layer would still fail:

Path What happens Result
background.extend() returns true New run queued behind current run's Deferred Prompt waits indefinitely, lost when session closes
background.extend() returns false Falls through to background.start() start() checks existing?.info.status === "running" and returns existing info -- no-op

Expected Behavior

Either:

  1. extend() should inject the prompt as a user message into the active session (via session/prompt API), delivering it between tool call turns
  2. Or extend() should return a distinct status indicating "queued, not delivered" so the caller can handle it appropriately

The TUI already supports this

Users can type messages in the TUI while an agent is working. The message becomes pending and is injected at the next safe boundary between tool call turns. This mechanism already exists in OpenCode -- it just is not exposed through the programmatic API (task tool / background.extend()).

Exposing the same pending-message capability to the task tool would allow plugins like OmO to safely relay user instructions to running subagents without cancelling them.

Related

Environment

  • OpenCode: 1.17.13
  • oh-my-openagent: 4.15.1

Session IDs (from the incident that revealed this)

  • Parent (Sisyphus): ses_0ccebb6e7ffemS6uDn4OXsXywZ
  • Subagent (Sisyphus-Junior): ses_0cccbc488ffeobMDPzuMAPt3EY
  • Timeframe: 2026-07-05 16:54 -- 17:17 UTC

Activity

  1. github-actions commented on Jul 7, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

  2. cardil commented on Jul 7, 2026

    @cardil
    Author

    #27898 is broader (observing progress + streaming output). This issue is specifically about injecting a pending user message into a running session -- the mechanism the TUI already uses when a user types while an agent is working. If #27898 covers this, happy to close as duplicate.

  3. github-actions commented on Sep 6, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions