feat(server): add native OpenAI execution and durable turn recovery - #452
Conversation
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 4 issue(s) (2 warning).
server/native-responses-runner.ts
Well-designed durable execution layer with strong crash-recovery semantics. One actionable bug: unprotected save() in the finally block can mask errors and leave the database in an inconsistent state.
- 🟡 bugs (L134): Unprotected
save()infinallyblock can throw (e.g. database full/locked), which replaces the original error and leaves the database status as 'running' instead of 'interrupted'. The caller sees a misleading error and the conversation is stuck untilrecoverAtStartup(). Wrap in try-catch:try { save(); } catch { /* log but don't mask */ }.[fixable]
server/native-tool-executor.ts
Well-designed durable execution layer with strong crash-recovery semantics. One actionable bug: unprotected save() in the finally block can mask errors and leave the database in an inconsistent state.
- 🟡 unsafe_assumptions (L138):
entry.path = await realpath(entry.path)mutatesManagedSession.worktreePathsentries in-place on every tool call. This is shared state owned bySessionRegistry. Whilerealpathis idempotent on resolved paths, if a worktree path is removed between calls,realpaththrows and the tool returns a confusing 'Session workspace is unavailable' error instead of a worktree-specific message. Resolve into a local copy instead of mutating the session.[fixable] - 🔵 style (L145):
JSON.stringifycomparison for deep equality is fragile — objects with identical keys in different insertion order produce different strings. The current callers produce same-order objects, so this works today, butdeepStrictEqualor a shallow key-by-key check would be more robust.[fixable]
server/__tests__/native-responses-runner.test.ts
Well-designed durable execution layer with strong crash-recovery semantics. One actionable bug: unprotected save() in the finally block can mask errors and leave the database in an inconsistent state.
- 🔵 missing_tests (L260): The 'stops before side effects if checkpoint persistence fails' test doesn't verify database state after the error. Because
state.checkpointremains truthy, thesave()call in thefinallyblock also throws 'disk full', meaning status stays 'running' rather than 'interrupted'. The test passes by coincidence (same error message). Add an assertion likeexpect(store.load(...).status).toBe('running')to document this limitation, or fix the runner to guard the finally-block save.[fixable]
| this.active = undefined; | ||
| if (!completed) { | ||
| state.status = 'interrupted'; | ||
| save(); |
There was a problem hiding this comment.
🟡 bugs: Unprotected save() in finally block can throw (e.g. database full/locked), which replaces the original error and leaves the database status as 'running' instead of 'interrupted'. The caller sees a misleading error and the conversation is stuck until recoverAtStartup(). Wrap in try-catch: try { save(); } catch { /* log but don't mask */ }. [fixable]
| const parsed = schemas[block.name as keyof typeof schemas].safeParse(block.input); | ||
| if (!parsed.success) return result('Invalid native tool input', true); | ||
| const input = { ...parsed.data }; | ||
| for (const entry of session.worktreePaths.values()) entry.path = await realpath(entry.path); |
There was a problem hiding this comment.
🟡 unsafe_assumptions: entry.path = await realpath(entry.path) mutates ManagedSession.worktreePaths entries in-place on every tool call. This is shared state owned by SessionRegistry. While realpath is idempotent on resolved paths, if a worktree path is removed between calls, realpath throws and the tool returns a confusing 'Session workspace is unavailable' error instead of a worktree-specific message. Resolve into a local copy instead of mutating the session. [fixable]
| signal.throwIfAborted(); | ||
| if (permission.behavior !== 'allow') return result(permission.message, true); | ||
| // The shared handler returns the checked input. Never execute unchecked replacements. | ||
| if (JSON.stringify(permission.updatedInput) !== JSON.stringify(input)) |
There was a problem hiding this comment.
🔵 style: JSON.stringify comparison for deep equality is fragile — objects with identical keys in different insertion order produce different strings. The current callers produce same-order objects, so this works today, but deepStrictEqual or a shallow key-by-key check would be more robust. [fixable]
| expect(execute).not.toHaveBeenCalled(); | ||
| expect(JSON.parse(fetchMock.mock.calls[1][1].body).input).toContainEqual( | ||
| expect.objectContaining({ | ||
| type: 'function_call_output', |
There was a problem hiding this comment.
🔵 missing_tests: The 'stops before side effects if checkpoint persistence fails' test doesn't verify database state after the error. Because state.checkpoint remains truthy, the save() call in the finally block also throws 'disk full', meaning status stays 'running' rather than 'interrupted'. The test passes by coincidence (same error message). Add an assertion like expect(store.load(...).status).toBe('running') to document this limitation, or fix the runner to guard the finally-block save. [fixable]
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 5 issue(s) (3 warning).
packages/harness/src/providers/sdk-adapter.ts
Well-engineered crash-safety foundation with correct recovery semantics. The sdk-adapter.ts reordering is verified safe. Main gaps are the per-save SELECT overhead in the store and missing multi-tool-turn test coverage for the incremental persistence path.
- 🟡 regressions (L276): The
onHistorycallback receivesstructuredClone([...messages, { role: 'user', content: toolResults }])after each individual tool result, deep-cloning the entire conversation history each time. For long conversations with multi-tool turns, this creates O(tools × history_length) cloning work per turn. The per-tool-result granularity is correct for crash safety (persist before the next side effect), but the full-history clone cost should be weighed against a differential approach if conversations grow large.
server/native-responses-store.ts
Well-engineered crash-safety foundation with correct recovery semantics. The sdk-adapter.ts reordering is verified safe. Main gaps are the per-save SELECT overhead in the store and missing multi-tool-turn test coverage for the incremental persistence path.
- 🟡 unsafe_assumptions (L55):
save()callsthis.load(conversationId, binding)purely for binding validation on every invocation, performing an extra SELECT query before every INSERT/UPDATE. During rapid tool execution,onHistorycallssave()after each tool result — doubling DB queries unnecessarily. The binding is immutable afterbegin(), so the key could be cached in the instance (e.g., aMap<string, string>of conversationId → bindingKey set duringbegin()) instead of re-querying.[fixable] - 🟡 bugs (L57): The checkpoint serialization in
save()explicitly lists four fields:accountId,model,history,input. IfResponsesCheckpointgains a new field needed for continuation (e.g., provider configuration or conversation metadata), it would be silently dropped. The explicit whitelist is correct for excluding credentials, but a safer pattern would be to destructure away the excluded fields (const { apiKey: _, ...safe } = state.checkpoint) rather than listing included ones, so new safe fields are preserved automatically.[fixable]
server/__tests__/native-responses-runner.test.ts
Well-engineered crash-safety foundation with correct recovery semantics. The sdk-adapter.ts reordering is verified safe. Main gaps are the per-save SELECT overhead in the store and missing multi-tool-turn test coverage for the incremental persistence path.
- 🔵 missing_tests: No test exercises a model response containing multiple tool_use blocks in a single turn. The
response(tool)helper always returns a single function_call. This is the most novel behavior introduced by theonHistorychange insdk-adapter.ts— incremental persistence of partial tool results within a turn — and deserves a dedicated test verifying that a crash mid-batch (after tool A's result is persisted but before tool B executes) recovers correctly with A's result present and B marked uncertain.[fixable]
server/native-tool-executor.ts
Well-engineered crash-safety foundation with correct recovery semantics. The sdk-adapter.ts reordering is verified safe. Main gaps are the per-save SELECT overhead in the store and missing multi-tool-turn test coverage for the incremental persistence path.
- 🔵 style (L86): The
shell()function collects stdout and stderr into the samechunksarray. When both streams produce output, the consumer receives interleaved content with no way to distinguish error diagnostics from actual command output. Consider collecting stderr separately and appending it as a labeled section (e.g.,\n--- stderr ---\n...) in the resolved output, matching how the Claude Code SDK formats shell results.[fixable]
| } | ||
|
|
||
| toolResults.push(result); | ||
| await opts.onHistory?.( |
There was a problem hiding this comment.
🟡 regressions: The onHistory callback receives structuredClone([...messages, { role: 'user', content: toolResults }]) after each individual tool result, deep-cloning the entire conversation history each time. For long conversations with multi-tool turns, this creates O(tools × history_length) cloning work per turn. The per-tool-result granularity is correct for crash safety (persist before the next side effect), but the full-history clone cost should be weighed against a differential approach if conversations grow large.
| })(); | ||
| } | ||
| save(conversationId: string, binding: AccountBinding, state: NativeResponsesState) { | ||
| this.load(conversationId, binding); // Never overwrite a different account binding. |
There was a problem hiding this comment.
🟡 unsafe_assumptions: save() calls this.load(conversationId, binding) purely for binding validation on every invocation, performing an extra SELECT query before every INSERT/UPDATE. During rapid tool execution, onHistory calls save() after each tool result — doubling DB queries unnecessarily. The binding is immutable after begin(), so the key could be cached in the instance (e.g., a Map<string, string> of conversationId → bindingKey set during begin()) instead of re-querying. [fixable]
| save(conversationId: string, binding: AccountBinding, state: NativeResponsesState) { | ||
| this.load(conversationId, binding); // Never overwrite a different account binding. | ||
| // Explicit fields only: credentials and runner configuration are never serialized. | ||
| const checkpoint = state.checkpoint && { |
There was a problem hiding this comment.
🟡 bugs: The checkpoint serialization in save() explicitly lists four fields: accountId, model, history, input. If ResponsesCheckpoint gains a new field needed for continuation (e.g., provider configuration or conversation metadata), it would be silently dropped. The explicit whitelist is correct for excluding credentials, but a safer pattern would be to destructure away the excluded fields (const { apiKey: _, ...safe } = state.checkpoint) rather than listing included ones, so new safe fields are preserved automatically. [fixable]
| signal.addEventListener('abort', onAbort, { once: true }); | ||
| if (signal.aborted) onAbort(); | ||
| const timer = setTimeout(() => kill('Shell command timed out'), options.timeoutMs ?? 60_000); | ||
| const collect = (chunk: Buffer) => { |
There was a problem hiding this comment.
🔵 style: The shell() function collects stdout and stderr into the same chunks array. When both streams produce output, the consumer receives interleaved content with no way to distinguish error diagnostics from actual command output. Consider collecting stderr separately and appending it as a labeled section (e.g., \n--- stderr ---\n...) in the resolved output, matching how the Claude Code SDK formats shell results. [fixable]
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 5 issue(s) (1 warning).
packages/harness/__tests__/sdk-adapter.test.ts
Solid crash-recovery design with careful durable boundaries; the toolResults.push reorder in the shared runAgenticLoop is safe but the new onHistory callback lacks dedicated unit tests in the adapter test suite.
- 🟡 missing_tests:
runAgenticLoopgained anonHistorycallback and a reordering oftoolResults.push(result)from afteryieldto before it. These are behavioral changes to a shared function used by both query-loop.ts and the new native runner. The existing sdk-adapter tests don't exerciseonHistoryat all — call count, argument shape (cloned snapshots with incremental tool results), error propagation, or the push-before-yield ordering. The callback is only tested indirectly through the runner integration tests. A dedicated unit test would catch regressions if the callback contract changes.[fixable]
server/native-responses-runner.ts
Solid crash-recovery design with careful durable boundaries; the toolResults.push reorder in the shared runAgenticLoop is safe but the new onHistory callback lacks dedicated unit tests in the adapter test suite.
- 🔵 unsafe_assumptions (L69):
this.activeconcurrency guard is unreliable becauserun()is an async generator whose body doesn't execute until the first.next()call. If a caller stores the generator without iterating it, then callsrun()again, both pass thethis.activecheck. Thestore.begin()SQLite transaction is the authoritative guard, and the contract says callers must exhaust the iterator, but the instance-level check gives a false sense of safety. Consider documenting this explicitly or eagerly starting the generator body via an intermediate wrapper.[fixable] - 🔵 style (L13):
export { NativeResponsesStore } from './native-responses-store.js're-exports the store class from the runner module. This creates two import paths for the same symbol (./native-responses-store.jsdirectly, or./native-responses-runner.js). The test file already imports both from the runner. Consider whether this convenience re-export is worth the indirection — callers needing only the store shouldn't have to import the runner.[fixable]
server/__tests__/native-tool-executor.test.ts
Solid crash-recovery design with careful durable boundaries; the toolResults.push reorder in the shared runAgenticLoop is safe but the new onHistory callback lacks dedicated unit tests in the adapter test suite.
- 🔵 missing_tests: Several executor branches lack test coverage: (1) Edit with zero matches or multiple matches (the
indexOf !== lastIndexOfguard at native-tool-executor.ts:180-182), (2) Read on a file exceedingmaxOutputBytes(the 64KB check at line 191), (3) shell timeout (the 60s default). These are defensive checks worth exercising to prevent silent regressions.[fixable]
server/native-tool-executor.ts
Solid crash-recovery design with careful durable boundaries; the toolResults.push reorder in the shared runAgenticLoop is safe but the new onHistory callback lacks dedicated unit tests in the adapter test suite.
- 🔵 unsafe_assumptions (L176): For Edit and Read, the entire file is loaded into memory via
readFilebefore any size check. ThemaxOutputBytesguard only applies to Read's return value (line 191), not to the read itself. A multi-GB file used as an Edit target would consume that much memory. The SDK tools likely have the same behavior, so this is consistent, but worth noting for a tool that accepts untrusted model input.[fixable]
| this.active?.abort(); | ||
| } | ||
| async *run(prompt: string, signal?: AbortSignal) { | ||
| if (this.active) throw new Error('Native Responses conversation already running'); |
There was a problem hiding this comment.
🔵 unsafe_assumptions: this.active concurrency guard is unreliable because run() is an async generator whose body doesn't execute until the first .next() call. If a caller stores the generator without iterating it, then calls run() again, both pass the this.active check. The store.begin() SQLite transaction is the authoritative guard, and the contract says callers must exhaust the iterator, but the instance-level check gives a false sense of safety. Consider documenting this explicitly or eagerly starting the generator body via an intermediate wrapper. [fixable]
| import { NativeResponsesStore, type NativeResponsesState } from './native-responses-store.js'; | ||
| import { createLogger } from './logger.js'; | ||
| const log = createLogger('native-responses'); | ||
| export { NativeResponsesStore } from './native-responses-store.js'; |
There was a problem hiding this comment.
🔵 style: export { NativeResponsesStore } from './native-responses-store.js' re-exports the store class from the runner module. This creates two import paths for the same symbol (./native-responses-store.js directly, or ./native-responses-runner.js). The test file already imports both from the runner. Consider whether this convenience re-export is worth the indirection — callers needing only the store shouldn't have to import the runner. [fixable]
| await writeFile(write.file_path, write.content, { encoding: 'utf8', signal }); | ||
| return result('File written'); | ||
| } | ||
| const content = await readFile(input.file_path, { encoding: 'utf8', signal }); |
There was a problem hiding this comment.
🔵 unsafe_assumptions: For Edit and Read, the entire file is loaded into memory via readFile before any size check. The maxOutputBytes guard only applies to Read's return value (line 191), not to the read itself. A multi-GB file used as an Edit target would consume that much memory. The SDK tools likely have the same behavior, so this is consistent, but worth noting for a tool that accepts untrusted model input. [fixable]
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 7 issue(s) (4 warning).
server/native-tool-executor.ts
Well-structured durable execution foundation with careful crash recovery semantics. The onHistory integration into runAgenticLoop is clean and the store's binding enforcement is solid. Main concerns are minor: a TOCTOU in permission-file creation, fragile coupling between onHistory timing and loop-limit detection, and a missing boundary-condition test for readBounded.
- 🟡 unsafe_assumptions (L169): When
worktreePathsis empty (no worktrees configured),canonicalPathresolves the file_path but no root match is found, so the path bypasses the alias rewriting. The downstreamcanUseToolstill enforces policy, but the canonicalized path may not match the permission handler's expectations for sessions without worktree entries. If all native execution sessions are guaranteed to have worktrees, this is fine, but the code doesn't enforce or document that invariant.[fixable]
server/native-responses-runner.ts
Well-structured durable execution foundation with careful crash recovery semantics. The onHistory integration into runAgenticLoop is clean and the store's binding enforcement is solid. Main concerns are minor: a TOCTOU in permission-file creation, fragile coupling between onHistory timing and loop-limit detection, and a missing boundary-condition test for readBounded.
- 🟡 bugs (L112): The loop-limit detection
state.history.at(-1)?.role !== 'assistant'after aresultevent relies ononHistoryhaving been called to updatestate.historyduring the final turn. IfrunAgenticLoopyieldsresultafter a turn whereonHistorywas NOT called (e.g., if the loop breaks onsignal.abortedbefore appending the assistant message),state.historymay be stale and the check could misclassify the outcome. Currently this can't happen because the abort-signal check in the loop happens after tool execution, not mid-turn — but the coupling betweenonHistorytiming and this check is fragile. - 🔵 style (L25):
recoverToolResultsmutatesstate.historyin place (pushes a new user message). This is intentional but undocumented in the function's JSDoc — the caller depends on the mutation side effect. A brief@mutates state.historynote would clarify the contract.[fixable] - 🔵 style (L103): The
executeToolwrapper in the runner catches all errors and returns an error result, including for non-abort failures. This meansrunAgenticLoop's own catch-and-wrap-error path forexecuteToolrejections (lines 261-268 of sdk-adapter.ts) is unreachable when called from this runner. This is intentional double-defense but worth a brief comment to prevent future confusion about which error path fires.[fixable]
server/native-responses-store.ts
Well-structured durable execution foundation with careful crash recovery semantics. The onHistory integration into runAgenticLoop is clean and the store's binding enforcement is solid. Main concerns are minor: a TOCTOU in permission-file creation, fragile coupling between onHistory timing and loop-limit detection, and a missing boundary-condition test for readBounded.
- 🟡 unsafe_assumptions (L21): The
closeSync(openSync(path, 'a', 0o600)); chmodSync(path, 0o600)two-step has a TOCTOU window: another process could open the file betweenopenSynccreating it andchmodSyncrestricting permissions. On a single-server deployment this is negligible, butopenSyncwithO_CREAT|O_EXCL(flag'ax') wrapped in a try/catch for EEXIST would be tighter for the sensitive-credential concern the doc calls out.[fixable]
packages/harness/src/providers/sdk-adapter.ts
Well-structured durable execution foundation with careful crash recovery semantics. The onHistory integration into runAgenticLoop is clean and the store's binding enforcement is solid. Main concerns are minor: a TOCTOU in permission-file creation, fragile coupling between onHistory timing and loop-limit detection, and a missing boundary-condition test for readBounded.
- 🟡 regressions (L275): Moving
toolResults.push(result)beforeyieldchanges observable behavior for existing callers consuming the generator: theuserevent is now yielded AFTER the result is accumulated intotoolResults, whereas previously it was yielded BEFORE. For current consumers (query-loop) this is harmless since theuserevent carries only the individual result, but any consumer that held a reference totoolResults(unlikely but possible via closure) would see different intermediate state. The change is well-motivated for durability but should be called out as a behavioral change.
server/__tests__/native-tool-executor.test.ts
Well-structured durable execution foundation with careful crash recovery semantics. The onHistory integration into runAgenticLoop is clean and the store's binding enforcement is solid. Main concerns are minor: a TOCTOU in permission-file creation, fragile coupling between onHistory timing and loop-limit detection, and a missing boundary-condition test for readBounded.
- 🔵 missing_tests: No test covers the
readBoundedbehavior when reading a file that is exactly at the size limit (limit bytes, not limit+1). The current tests use a small file withmaxOutputBytes: 8and a 33-byte file, but don't test the boundary condition where the file is exactlylimitbytes (should succeed) versuslimit+1bytes (should fail).[fixable]
| ); | ||
| } | ||
| } | ||
| if ('file_path' in input) { |
There was a problem hiding this comment.
🟡 unsafe_assumptions: When worktreePaths is empty (no worktrees configured), canonicalPath resolves the file_path but no root match is found, so the path bypasses the alias rewriting. The downstream canUseTool still enforces policy, but the canonicalized path may not match the permission handler's expectations for sessions without worktree entries. If all native execution sessions are guaranteed to have worktrees, this is fine, but the code doesn't enforce or document that invariant. [fixable]
| }; | ||
| } | ||
| }, | ||
| onHistory: (history) => { |
There was a problem hiding this comment.
🟡 bugs: The loop-limit detection state.history.at(-1)?.role !== 'assistant' after a result event relies on onHistory having been called to update state.history during the final turn. If runAgenticLoop yields result after a turn where onHistory was NOT called (e.g., if the loop breaks on signal.aborted before appending the assistant message), state.history may be stale and the check could misclassify the outcome. Currently this can't happen because the abort-signal check in the loop happens after tool execution, not mid-turn — but the coupling between onHistory timing and this check is fragile.
|
|
||
| /** Fill unresolved calls with explicit uncertainty; never replay tools following a crash. */ | ||
| function recoverToolResults(state: NativeResponsesState) { | ||
| const lastAssistant = state.history.map((message) => message.role).lastIndexOf('assistant'); |
There was a problem hiding this comment.
🔵 style: recoverToolResults mutates state.history in place (pushes a new user message). This is intentional but undocumented in the function's JSDoc — the caller depends on the mutation side effect. A brief @mutates state.history note would clarify the contract. [fixable]
| return await opts.executeTool(block, abort.signal); | ||
| } catch { | ||
| return { | ||
| type: 'tool_result', |
There was a problem hiding this comment.
🔵 style: The executeTool wrapper in the runner catches all errors and returns an error result, including for non-abort failures. This means runAgenticLoop's own catch-and-wrap-error path for executeTool rejections (lines 261-268 of sdk-adapter.ts) is unreachable when called from this runner. This is intentional double-defense but worth a brief comment to prevent future confusion about which error path fires. [fixable]
| // Create privately before SQLite opens it (including its rollback journal). | ||
| closeSync(openSync(path, 'a', 0o600)); | ||
| chmodSync(path, 0o600); | ||
| this.db = new Database(path); |
There was a problem hiding this comment.
🟡 unsafe_assumptions: The closeSync(openSync(path, 'a', 0o600)); chmodSync(path, 0o600) two-step has a TOCTOU window: another process could open the file between openSync creating it and chmodSync restricting permissions. On a single-server deployment this is negligible, but openSync with O_CREAT|O_EXCL (flag 'ax') wrapped in a try/catch for EEXIST would be tighter for the sensitive-credential concern the doc calls out. [fixable]
| }; | ||
| } | ||
|
|
||
| toolResults.push(result); |
There was a problem hiding this comment.
🟡 regressions: Moving toolResults.push(result) before yield changes observable behavior for existing callers consuming the generator: the user event is now yielded AFTER the result is accumulated into toolResults, whereas previously it was yielded BEFORE. For current consumers (query-loop) this is harmless since the user event carries only the individual result, but any consumer that held a reference to toolResults (unlikely but possible via closure) would see different intermediate state. The change is well-motivated for durability but should be called out as a behavioral change.
0fb630a to
3f400bd
Compare
d5e7f53 to
00de793
Compare
The Responses adapter in #451 streams function calls but cannot execute tools or recover native turns. This PR adds a permission-checked native executor and durable Responses runner that preserve application conversation identity and account/model binding.
Read/Write/Edit/Bash reuse the session's skill, worktree and permission policy. The runner persists model output before side effects and each tool outcome before continuing. Explicit follow-ups restore encrypted continuation; concurrent turns are rejected. Interrupted or crashed calls are never automatically replayed. Credentials are never serialized or implicitly inherited by shells.
This is an internal execution foundation, not wired into chat.ts dispatch. Server queues, MCP lifecycle, ContexGin startup wiring, native credential-reference resolution, reconnect/deduplication and physical-phone acceptance remain outstanding. OpenAI is not enabled in the mobile catalog. No deployment or live API validation was performed. Images, compaction, reasoning UI and ChatGPT Pro are not supported by this slice. See docs/features/openai-native-execution.md for the integration contract and remaining gaps.
Review fixes include atomic SQLite binding validation, a compile-time-complete checkpoint allowlist, partial multi-tool recovery, bounded Read/Edit input, labeled stderr, timeout/permission tests and direct persistence-callback tests. The generator begins work only when iterated; its guards execute synchronously before the first await/yield. Full-history cloning remains a documented scaling limit; the shell worktree policy remains a heuristic, not an OS sandbox.
Validation for 00de793: 210 files / 3,267 tests passed, including 28 executor/runner tests and shared loop callback tests. Server/frontend types, lint, formatting and builds passed; Fresh final-head CI is verified before merge. Existing lint/bundle warnings and advisory dependency audit findings remain. Four review rounds are triaged with fixes and evidence. Dependencies #449 (including #450) and #451 are now merged into main. The final native rebase preserves the exact source tree tested locally (6cc64b9e8af83b2a16db51e7ffe6617cc6564d17). Landing status is recorded by GitHub; no production deployment is included.
Final review resolution evidence:
Round 2 addressed in 468d37f.
Validation: 208 files / 3,242 tests; lint, formatting, server/frontend types and builds passed; CI run 34095672119 passed. This remains an internal foundation, not chat.ts dispatch or mobile OpenAI support.
Round 4 triage at cbe362e:
All remaining runtime claims were investigated; the final update adds tests and documentation, not a new execution path. Full suite: 208 files / 3,249 tests passed, with all required checks.