Skip to content

🤖 refactor: collapse the AIService/StreamManager turn relay into one turn engine - #3999

Merged
ibetitsmike merged 42 commits into
mainfrom
mike/turn-engine
Aug 29, 2026
Merged

🤖 refactor: collapse the AIService/StreamManager turn relay into one turn engine#3999
ibetitsmike merged 42 commits into
mainfrom
mike/turn-engine

Conversation

@ibetitsmike

@ibetitsmike ibetitsmike commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Merges AIService and StreamManager into one turn-execution module with a single authoritative per-turn completion signal. The 13-event re-emission relay between them is deleted, the 32-positional-argument startStream seam becomes one typed TurnExecutionOptions object, and AgentSession's dual-delivery dedupe latches (activeStreamErrorEventReceived, activeStreamFailureHandled) are removed because the two failure-delivery paths are now mutually exclusive by construction. Strictly behavior-preserving: no renderer, IPC, or persisted-shape changes.

Background

AIService was a shallow relay: it re-emitted 13 StreamManager events (10 verbatim, 3 with enrichment) and forwarded stream-control calls one-for-one, while turn failures were delivered twice, once via the error event and once via the returned Result. AgentSession kept two boolean latches purely to dedupe those arrival paths. StreamManager was instantiated only inside AIService and had zero external consumers, so the seam carried no isolation value.

Implementation

  • streamManager.ts becomes the module-private engine core of AIService: it no longer extends EventEmitter; events flow through a typed TurnEngineEventSink into AIService.emitEngineEvent, the single emission point, where the three existing enrichments (DevTools pending-run cleanup, debug LLM snapshot capture, partial commit/delete on abort) run unchanged.
  • startStream takes one typed TurnExecutionOptions object and returns a TurnStreamHandle whose completion promise settles exactly once (completed, aborted, or failed with the classified error) after terminal stream cleanup. Request construction shares a StreamRequestOptions base between the primary turn and model-fallback hops; no internal positional cascades remain. streamMessage keeps its fast-return semantics and returns the handle.
  • AgentSession consumes exactly one resolution path per turn: pre-start failures arrive as the returned Err (handled inline, including the runtime startup special case), post-start outcomes arrive only through handle.completion. Both dedupe latches are deleted. Failures that emit an error event before startup resolves (runtime readiness, strict agent resolution) are collected per call via onPreStartError (no mutable session-level capture window) and drained exactly once, so their recovery decisions always resolve.
  • Mock playback returns a pending TurnStreamHandle that settles from the scripted terminal events (scripted stream-error → failed, stream-end → completed, stop/cleanup → aborted), so streamMessage callers see one contract everywhere; a disposed session drops late failed completions instead of persisting retry/goal state post-teardown (review-round fixes).

Net LOC

Architecture PRs must be net simplifications. Final diff: +1,264 / −1,213 (net +51) across 26 files, down from +799 at first draft and +309 at review round 1. Deleted outright: the EventEmitter relay and setupStreamEventForwarding, both AgentSession latches, the session-level pre-start capture window, the facade completion observer, the dead streamToken handle property, TurnCompletion.messageId, the latch-era dual-delivery test, a tautological reflection test, the pure-forwarding relay test, the real-API concurrency test (duplicated mock-covered behavior at API cost), the Ok(undefined) mock-normalization shim, EventEmitter-era noop listeners, redundant explicit default stream mocks, duplicated per-suite turn-handle helpers (8 suites), and every positional cascade (32-arg startStream, 22-arg buildStreamRequestConfig, five positional .apply test fixtures).

Each material surviving addition, one line each:

  • streamManager.ts +48: the seam contract itself (TurnEngineEvent union, TurnCompletion/TurnStreamHandle, the exactly-once completion controller, StreamRequestOptions/TurnExecutionOptions), replacing implicit EventEmitter event strings and the 32-argument call boundary.
  • agentSession.disposeRace.test.ts +37: review-round-2 regression test (failed completions delivered after disposal are dropped), verified red without the production guard.
  • agentSession.ts +28: single-path completion consumption plus per-call onPreStartError collection, the review-round-1 fix for handle-less failures leaving recovery decisions unresolved.
  • streamManager.testHarness.ts +24: typed sink observer, required because StreamManager no longer exposes .on; consumed by two suites.
  • agentSession.postCompactionRetry.test.ts +20: mocks must construct real failed/started turn handles under the new contract where they previously returned undefined.
  • mockAiStreamPlayer.ts +18: review-round-2 fix, pending handles settled exactly once from scripted terminal events.
  • agentSession.testHarness.ts +18: shared createStartedTurnHandle/createFailedTurnHandle factories that deleted eight per-suite copies.
  • Behavior tests at the new seam replaced wiring/latch assertion suites: the remaining test files net −168 (streamManager.test.ts −79, aiService.test.ts −28, preStreamError −25, autoCompaction −24), so tests overall are net −40.

None of the +51 is deletable at zero risk. Levers evaluated and rejected with evidence: settling completions at their set-points (reintroduces a mid-cleanup race; settlement is deliberately ordered after workspaceStreams.delete), collapsing the pre-start error event + Err dual delivery into one path (the event has three non-AgentSession consumers: renderer forwarding via serviceContainer, workspaceService, taskService), and inheritance-merging the two classes (~60 lines at the cost of colliding private state in a UAT-validated core). Future work if further shrink is wanted: LOC-neutral consolidations only (test assertion helpers), which do not reduce the net.

Validation

  • Full seam sweep (14 suites): 792/793 green; the single failure (workspaceService wake-store reconciliation) fails identically at the merge-base on this host and passes in CI, so it is pre-existing and unrelated.
  • Full bun test src unit suite and make static-check green on the final head.
  • The three CI Unit failures on the previous head were stale test mocks returning Ok(undefined) where streamMessage now returns a handle; fixed in the mocks, no production guards added. Integration (compaction) and E2E (linux) streaming-error suites reproduce green locally under xvfb at the final head, and again with the branch merged into latest main; the remaining local review scenario failure reproduces at the merge-base (host-environmental, passes in CI on main).
  • New behavioral coverage: completion settles exactly once per terminal path (real and mock engines), post-disposal failed completions are dropped, pre-start vs mid-turn failure exclusivity, debug-injected errors settle the turn. Each review-round regression test verified red with its production fix removed.
  • Remote dogfood UAT (real Opus/Haiku models, real UI) passed on branch commit c4d3453a8: streaming with reasoning and tool calls, hard and mid-tool interrupts, provider errors surfacing exactly once with auto-retry, live and persisted token/cost accounting, crash-kill recovery mid-text and mid-tool, and reconnect replay without duplication. Production changes since that SHA are the two review-round fixes (mock playback settlement, mock-mode only; disposal guard), the per-call pre-start collection, TurnCompletion.messageId removal, and request-options type restructuring, each covered by the red-green suites and the E2E/integration runs above rather than a full re-UAT.

Risks

This is the streaming core, so the main regression surfaces are event ordering, interrupt/resume semantics, crash recovery of partials, and cost accounting. Mitigations: the enrichment and cleanup code was moved verbatim to the single emission point, the abort path still emits asynchronously and always forwards even when partial cleanup fails, and completion settles only after terminal cleanup deregisters the stream. The existing test suites for all three files pin these invariants and passed unmodified in intent (whitebox seams were adapted to the new signature).


Generated with xum • Model: anthropic:claude-fable-5 • Thinking: xhigh • Cost: $259.68

@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

This comment has been minimized.

@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: 95ac2b6ab1

ℹ️ 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 thread src/node/services/agentSession.ts
Comment thread src/node/services/streamManager.ts
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

This comment has been minimized.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: 019eb2c1c8

ℹ️ 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".

@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

This comment has been minimized.

@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: faa20a726b

ℹ️ 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 thread src/node/services/agentSession.ts
Comment thread src/node/services/aiService.ts Outdated
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

This comment has been minimized.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: 03cdbca31f

ℹ️ 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".

_Generated with `xum` • Model: `openai:gpt-5.6-sol` • Thinking: `high`_

<!-- mux-attribution: model=openai:gpt-5.6-sol thinking=high -->
Review follow-up: the four per-command handlers are only reachable through
processSlashCommand now, and /plan open lost its dedicated tests in the
result-based rewrite.

_Generated with `xum` • Model: `anthropic:claude-fable-5` • Thinking: `xhigh`_

<!-- mux-attribution: model=anthropic:claude-fable-5 thinking=xhigh -->
Codex P1: getDraft captured at command invocation reports that render's
input, so async commands cleared newer drafts on consume and never fired
restore-if-empty.

_Generated with `xum` • Model: `anthropic:claude-fable-5` • Thinking: `xhigh`_

<!-- mux-attribution: model=anthropic:claude-fable-5 thinking=xhigh -->
Codex P2: text equality cannot distinguish a retyped identical draft from
the original invocation. Commands already clear through their own
clear-input actions (matching trunk), so the terminal clear was additive
and could only destroy mid-phase drafts.

_Generated with `xum` • Model: `anthropic:claude-fable-5` • Thinking: `xhigh`_

<!-- mux-attribution: model=anthropic:claude-fable-5 thinking=xhigh -->
Codex P2 follow-up: with the terminal consume-path clear gone, /dream and
/refine left the executed command re-runnable in the composer. Emit
clear-input from the handlers so commands own their composer effects.

_Generated with `xum` • Model: `anthropic:claude-fable-5` • Thinking: `xhigh`_

<!-- mux-attribution: model=anthropic:claude-fable-5 thinking=xhigh -->
The seam file is now the single documented home for ArchiveWorkspaceOptions,
SendMessageInternalOptions, and WorkspaceLiveActivity instead of duplicating
workspaceService declarations. StreamErrorRecoveryOutcome comes from its
canonical agentSession export. The AgentTaskStatus re-export shim is gone;
importers use the seam. Method-level optional chaining on both typed ports is
removed: the interfaces guarantee the methods, so the narrow-mock hedges and
their comments no longer apply.
… suite

The typed AgentTaskIntegration port makes several tests unwritable or
redundant: the phantom cleanupReportedDescendantsAfterArchive guard (method
never existed in production), the archive lock pass-through wiring assertion,
and four private updateAgentStatus non-invocation spies whose positive
registerSession behavior tests remain. Near-identical send/resume lifecycle,
winding-down, auto-resume, and foreground-wait-backgrounding siblings collapse
into table-driven tests preserving every case, and dead fake stubs the code
under test never reads are dropped.
@ibetitsmike
ibetitsmike changed the base branch from main to mike/task-workspace-seam August 28, 2026 19:22

@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: 3eccf33214

ℹ️ 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 thread src/node/services/agentSession.ts
Codex round: handleStreamWithHistoryFailure could persist retry state or
error rows after dispose() when a startup await (commitPartial, history
reads) settled with a failure post-teardown. Guard it like
consumeTurnCompletion, settling collected pre-start recovery decisions
in memory so waiters cannot hang. Red-green verified via the new
dispose-race regression test.
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

This comment has been minimized.

@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: ef4d5116a1

ℹ️ 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 thread src/node/services/streamManager.ts Outdated
Codex round: TurnEngineEventSink may return a promise, but emitTurnEvent
void'd it, so a rejecting async sink became an unhandled rejection and a
slow terminal sink could outlast the settled completion unobserved.
Contain rejections with a logged catch; also catch the two abort-side
mirrors (abortDelivery.finally re-propagated rejections after settling,
and stopStream's no-stream emit was raw void). Red-green verified via a
new unhandled-rejection regression test.
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

This comment has been minimized.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: 769f149f35

ℹ️ 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".

@ibetitsmike
ibetitsmike added this pull request to the merge queue Aug 29, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue because a pull request earlier in the stack was removed Aug 29, 2026
@ibetitsmike
ibetitsmike added this pull request to the merge queue Aug 29, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue because a pull request earlier in the stack was removed Aug 29, 2026
@ibetitsmike
ibetitsmike added this pull request to the merge queue Aug 29, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue because a pull request earlier in the stack was removed Aug 29, 2026
@ibetitsmike
ibetitsmike added this pull request to the merge queue Aug 29, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue because a pull request earlier in the stack was removed Aug 29, 2026
@ibetitsmike
ibetitsmike added this pull request to the merge queue Aug 29, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue because a pull request earlier in the stack was unmerged and was not in the queue Aug 29, 2026
Base automatically changed from mike/task-workspace-seam to main August 29, 2026 05:58
@ibetitsmike ibetitsmike reopened this Aug 29, 2026
@ibetitsmike
ibetitsmike enabled auto-merge August 29, 2026 06:03
@ibetitsmike
ibetitsmike added this pull request to the merge queue Aug 29, 2026
Merged via the queue into main with commit 5387b36 Aug 29, 2026
55 of 57 checks passed
@ibetitsmike
ibetitsmike deleted the mike/turn-engine branch August 29, 2026 07:23
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