Skip to content

feat: keep injected CLI turns in one opt-in stream - #217

Open
krislavten wants to merge 1 commit into
ben-vargas:ai-sdk-v6from
krislavten:feat/injected-turn-continuation
Open

krislavten wants to merge 1 commit into
ben-vargas:ai-sdk-v6from
krislavten:feat/injected-turn-continuation

Conversation

@krislavten

Copy link
Copy Markdown

Summary

Add an opt-in continueInjectedTurns streaming mode for callers that submit messages through onStreamStart while a CLI turn is running. The provider keeps reading intermediate native results until the tracked injected UUIDs are confirmed, emitting one AI SDK finish with the sum of per-result token usage.

inject(text, { uuid })
  track delivery separately from consumption and result confirmation
  retain streamed text and tool output across native results
  finish once all tracked messages are confirmed, or the absolute tail budget expires

This targets the maintained ai-sdk-v6 branch. Default streaming behavior is unchanged. onStreamEnd detaches a failed channel synchronously before queued delivery callbacks are rejected, allowing a caller to reinject unconfirmed messages into an explicit recovery stream. Recovery duplicate-completion receipts require both a frozen prior main-thread consumption witness for the exact resume session and a successful result for the new initial prompt; a completion notification alone never confirms consumption.

Evidence

  • Before: a provider result could end the AI SDK stream while an injected CLI continuation was still pending. A caller-controlled recovery gap could send a new message through an already-closed injector. Pre-result truncation could turn a pending-message failure into a successful length finish.
  • After: new tests cover multi-result continuation, UUID attribution, absolute deadlines, guarded recovery joins, known partial usage on errors and EOF, and synchronous stream-end notification before queued rejection. The pre-result truncation regression failed with an unexpected length finish before the guard was added, then passed.
  • npx vitest run --project=node src/injection-session.test.ts src/injected-stream-usage.test.ts src/claude-code-language-model.test.ts src/validation.test.ts --maxWorkers=1: 363 passed.
  • The same selected files with --project=edge: 363 passed.
  • npm run typecheck, npm run build: passed.
  • Targeted ESLint on the six changed source/test files: exit 0, three non-null assertion warnings. No generated dist files are committed.
  • The matching backport to 3.6.0 also passed the same Node/Edge selection: 336 passed in each environment. Local application tests exercised real query() against a scripted Anthropic HTTP peer for final-answer injection, hidden CLI continuation, recovery reinjection without repeating a tool side effect, and connection closure at timeout. These are local tests, not a live-provider release claim.

Merge Danger

Door: two-way. All continuation behavior requires explicit opt-in and the tracked injection callback; removing the opt-in restores the existing result boundary.

Blast Radius: streaming. The native SDK UUID and result attribution semantics remain the confirmation boundary. resumedInjectionUuids is an advanced caller-supplied recovery witness set, not an inference from a queued or completed notification. Timeouts close the query and report pending IDs; callers must decide their application-level dropped state. Existing cumulative cost/model usage metadata is retained, while per-result token usage is summed.

Track injected UUID receipts across native result boundaries, bound continuation, and expose stream lifecycle callbacks for caller-controlled recovery.

Co-Authored-By: Codex <noreply@openai.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-10T13:40:48.082846Z b84c90c PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b84c90c577

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +4787 to +4788
notifyStreamEnd();
done();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Close the injection channel before post-result draining

With continuation disabled, streaming input enabled, and onStreamStart plus an active onPromptSuggestion callback configured, deferring done() until this finally keeps the injector open throughout the post-result suggestion drain, potentially for 10 seconds after the AI SDK stream finishes. A late injection is now accepted and written to the native query while the drain discards continuation output. Replaying the parent and target implementations confirmed that the parent reports delivery as false, whereas this commit sends a second input message and reports true. End streaming input at the terminal result, after the synchronous lifecycle notification but before awaiting the drain.

Useful? React with 👍 / 👎.

Comment thread src/injection-session.ts
Comment on lines +185 to +188
((message.type === 'system' && message.subtype === 'init') ||
(message.type === 'stream_event' && message.event.type === 'message_start'))
) {
this.accepting = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reopen admission for non-partial continuation messages

After an intermediate result, accepting is set to false, but these conditions restore it only for another init or a partial message_start. When a continuation contains complete assistant messages without another init, as supported by includePartialMessages: false, admission remains closed throughout that turn. Further MessageInjector.inject() calls therefore immediately receive onResult(false) even while a tracked continuation is running. Reopen admission for main-thread assistant or command-start activity as well, or keep it open until the actual terminal boundary.

Useful? React with 👍 / 👎.

Comment thread src/injection-session.ts
Comment on lines +113 to +114
if (lastResult) yield { ...lastResult, usage: { ...lastResult.usage, ...totals } };
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Report an error when recovery expires before its first result

In the supported recovery path where the initial command's completion starts the deadline but its result never arrives, lastResult is undefined at expiry. This branch then returns normally, and doStream closes with neither an error nor a finish containing timeout metadata. A runtime probe produced only stream-start; callers without the optional receipt or lifecycle callbacks cannot observe the timeout through the stream API despite messages remaining unconfirmed. Emit a timeout error on this path without synthesizing usage, analogous to the pending-message EOF handling below.

Useful? React with 👍 / 👎.

Comment thread src/injection-session.ts
Comment on lines +99 to +102
: await Promise.race([
iterator.next(),
new Promise<typeof timeout>((resolve) => {
timer = setTimeout(() => resolve(timeout), Math.max(0, deadline! - Date.now()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Enforce the deadline before consuming buffered messages

If the native iterator has buffered frames, iterator.next() can resolve through microtasks on every iteration. Even after the absolute deadline has passed, it beats the newly scheduled setTimeout(0), which is then cleared in finally. Activity can consequently postpone expiry and allow a late result to finish with continuationTimedOut: false; a dependency-free probe with a 5 ms budget and 30 ms of buffered frames reproduced this. Check the wall-clock deadline before consuming another frame rather than relying solely on a newly created timer race.

Useful? React with 👍 / 👎.

Comment on lines +3531 to +3533
const responseMessages =
this.settings.continueInjectedTurns && this.settings.onStreamStart
? injectionSession.messages(response, () => response.close())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve permission denials from intermediate results

When a non-final native result contains permission_denials that the final result does not repeat, this wrapper hides that result from the main loop's mergeResultPermissionDenials() call. As that helper and its existing tests document, PreToolUse-hook denials can have no corresponding permission_denied system event, so this removes their only source and silently omits denied tool calls from the final provider metadata. A two-result runtime probe reproduced the omission. Merge each native result's denial metadata before filtering it, or aggregate and deduplicate these lists across intermediate results.

Useful? React with 👍 / 👎.

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