Visitar URL original
fix: add explicit wait_for_user for text-only HITL by nettee · Pull Request #861 · alvinunreal/oh-my-opencode-slim · GitHub
Skip to content

fix: add explicit wait_for_user for text-only HITL - #861

Merged
alvinunreal merged 1 commit into
alvinunreal:masterfrom
nettee:fix/text-only-hitl-wait
Jul 21, 2026
Merged

alvinunreal merged 1 commit into
alvinunreal:masterfrom
nettee:fix/text-only-hitl-wait

Conversation

@nettee

@nettee nettee commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor

What changed, and why was it needed?

Fixes #860.

Follow-up to #819 / #815. Builds on the process-global continuation gate from #856 / #854.

Related to #852; this addresses only the text-only external-user-wait path, not premature wakeups from subagent-completion events.

Root cause

The incomplete-todo continuation hook treats an idle orchestrator with pending work as abandoned and queues an internal promptAsync nudge.

Native question and permission flows already suppress that behavior because they emit machine-readable wait events. A normal assistant message asking the user to perform external work emits no such event, so the session becomes idle and continues before the user can respond.

Inferring a HITL boundary from assistant prose would be brittle. This change makes that state transition explicit.

Solution

  • Add an orchestrator-only wait_for_user tool for text-only HITL boundaries. The orchestrator gives concrete manual steps first, then calls the tool as its final action. Immediate answers, choices, clarifications, and pasted output continue to use the native question tool.
  • Encode the explicit wait as waiting-for-user in the existing process-global continuation-attempt gate.
    • Arming the wait cancels scheduled continuation work and revokes in-flight reservations.
    • Pre-upgrade continuation reservation/commit logic sharing the gate also fails closed.
  • Release the wait only when a distinct external user text, file, or image message arrives.
    • Duplicate delivery of the preceding user message does not release it.
    • Synthetic/internal messages and foreground-fallback replays do not release it.
  • Preserve the wait across hook/plugin recreation and fallback teardown in the same process, while clearing it on genuine session deletion.
  • Mark fallback replays with internal provenance and share fallback-in-progress state across manager instances so teardown races cannot mimic a new external message or genuine deletion.
  • Register the tool, orchestrator permissions, prompt guidance, docs, and disabled_tools behavior. When the tool is disabled, the prompt uses question as the blocking boundary and plugin health thresholds account for the intentionally disabled tool.

The tool's reason is diagnostic only. The implementation does not parse assistant prose. The latch is intentionally process-local and does not survive a process restart.

Impact

Users can receive manual instructions without an incomplete todo immediately restarting the orchestrator. Normal continuation becomes eligible again after their next distinct message.

This adds a tool and changes the static orchestrator prompt, so deployment has the expected one-time prompt-cache prefix change. It introduces no per-turn mutation of the static/system prompt; variable reason text appears only in the ordinary tool result.

Packaged RED -> GREEN reproduction

The same isolated harness was run against the public root export of two packed artifacts. It redirects OpenCode config, data, cache, logs, and project paths to a fresh temporary directory, then:

  1. submits an orchestrator user message;
  2. invokes wait_for_user when the package exposes it;
  3. emits session.idle with one pending todo and no active sessions; and
  4. counts todo reads and automatic promptAsync calls.

Both package manifests resolve the root export to ./dist/index.js. Each extracted package/dist/index.js matched its source checkout byte-for-byte.

Result 2.2.5 baseline This fix
waitToolAvailable false true
todoCalls 1 0
promptAsyncCalls 1 0
Harness exit code 1 0

A clean pre-patch build of master at 9105d5268681b6eae8e3fb78732588e89e9ef039 produced the same RED result, so the GREEN result is isolated from the commits between release 2.2.5 and current master.

Artifact SHA-256 values
  • 2.2.5 baseline tarball: 9f6f14cd0d18a07585f455323545f4aae9addbcd74b83b86caad17c97552f29e
  • Fixed tarball: ed1e7386d6418ddffeaacff20771359527926d51286a3c3b3d827527745dd953
  • Baseline extracted dist/index.js: c5694504a177b54b4b6cde4b0a7d6f7907605381a4c57a20d55ced9875d3d81d
  • Fixed extracted dist/index.js: 301e1ca8df1d569872d514f7e1fd3a64fff07144bfe044f219158456a54e1d44

Validation

  • bun test: 1,694 passed, 0 failed; 3 snapshots and 3,759 expectations across 102 files.
  • bun run lint: passed.
  • bun run typecheck: passed.
  • bun run build: passed.
  • bun run verify:release: passed machine-path scanning, npm packing, clean temporary installation, and package entrypoint import.
  • bunx biome check <16 changed TypeScript files>: passed.
  • git diff --check upstream/master: passed.

bun run check:ci also checks formatting for the entire repository and reports pre-existing formatter drift in unchanged src/utils/env.test.ts on current master. This PR does not modify that file; all changed TypeScript files pass the same Biome check. The repository's PR CI uses bun run lint, which passes.

@nettee
nettee marked this pull request as ready for review July 21, 2026 13:22
@greptile-apps

greptile-apps Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds an explicit text-only human-in-the-loop wait. The main changes are:

  • A new orchestrator-only wait_for_user tool.
  • A shared continuation latch released by distinct external input.
  • Internal provenance for foreground-fallback replays.
  • Updated permissions, prompt guidance, health checks, tests, and docs.

Confidence Score: 5/5

The changed flow looks mergeable after hardening fallback deletion cleanup.

  • The normal explicit-wait and distinct-message release paths are covered.
  • A deletion during fallback can preserve process-global state beyond the intended session lifetime.

src/hooks/foreground-fallback/index.ts and the corresponding session-deletion path

Important Files Changed

Filename Overview
src/hooks/task-session-manager/continuation-attempt-gate.ts Adds the process-global waiting-for-user state to continuation reservation handling.
src/hooks/task-session-manager/index.ts Arms explicit waits and excludes internal replay messages from user-input rearming.
src/hooks/foreground-fallback/index.ts Shares fallback progress across manager recreation and adds replay provenance, with a stale-state edge case during genuine deletion.
src/tools/wait-for-user.ts Defines the validated orchestrator-only wait tool and its result protocol.
src/index.ts Registers the tool and updates disabled-tool health thresholds.
src/agents/orchestrator.ts Adds prompt guidance and the question-tool fallback for disabled configurations.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant O as Orchestrator
    participant W as wait_for_user
    participant G as Continuation Gate
    participant U as User

    O->>U: Provide manual steps
    O->>W: wait_for_user(reason)
    W->>G: Arm waiting-for-user latch
    G-->>O: Suppress automatic continuation
    U->>G: Send distinct external message
    G->>G: Clear latch and open next epoch
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant O as Orchestrator
    participant W as wait_for_user
    participant G as Continuation Gate
    participant U as User

    O->>U: Provide manual steps
    O->>W: wait_for_user(reason)
    W->>G: Arm waiting-for-user latch
    G-->>O: Suppress automatic continuation
    U->>G: Send distinct external message
    G->>G: Clear latch and open next epoch
Loading

Reviews (1): Last reviewed commit: "fix: add explicit user wait for text-onl..." | Re-trigger Greptile

Comment on lines +187 to +197
const FALLBACK_IN_PROGRESS_KEY = Symbol.for(
'oh-my-opencode-slim.foreground-fallback.in-progress',
);

function getProcessFallbacksInProgress(): Set<string> {
const globalWithStore = globalThis as typeof globalThis & {
[FALLBACK_IN_PROGRESS_KEY]?: Set<string>;
};
globalWithStore[FALLBACK_IN_PROGRESS_KEY] ??= new Set();
return globalWithStore[FALLBACK_IN_PROGRESS_KEY];
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Fallback State Can Outlive Deletion

The process-global set keeps a session marked as fallback-in-progress until the original fallback reaches its cleanup path. If a genuine session.deleted event arrives during that window, task-session cleanup treats it as fallback teardown and preserves the new waiting-for-user latch. When the fallback never completes cleanly or the session ID is reused, the stale latch can indefinitely suppress continuation for the replacement session.

@alvinunreal

Copy link
Copy Markdown
Owner

thanks @nettee

@alvinunreal
alvinunreal merged commit c0c2ccf into alvinunreal:master Jul 21, 2026
2 checks passed
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.

[Bug]: Continuation nudge still fires during text-only waits for user input

2 participants