From 7f2f225dac699be73d40082b1d8f8e693341b39e Mon Sep 17 00:00:00 2001 From: Neonforge <48338160+Neonforge98@users.noreply.github.com> Date: Sat, 26 Sep 2026 11:26:23 -0700 Subject: [PATCH 1/4] fix(session-replay): preserve native materialization fidelity Pre-commit hook ran. Total eslint: 0, total circular: 4 --- .../NativeConversationMaterialization.md | 57 +++++++++++++ .../nativeConversationMaterializer.test.ts | 82 +++++++++++++++++-- .../nativeConversationMaterializer.ts | 40 ++++----- .../nativeConversationProjection.ts | 9 +- 4 files changed, 157 insertions(+), 31 deletions(-) create mode 100644 docs/org2-performance-guard-2026-09-26/NativeConversationMaterialization.md diff --git a/docs/org2-performance-guard-2026-09-26/NativeConversationMaterialization.md b/docs/org2-performance-guard-2026-09-26/NativeConversationMaterialization.md new file mode 100644 index 0000000000..a1dc3c41a1 --- /dev/null +++ b/docs/org2-performance-guard-2026-09-26/NativeConversationMaterialization.md @@ -0,0 +1,57 @@ +# Native conversation materialization fidelity + +## Source and root cause + +Read-only inspection matched the reported SDE conversation and its Codex child in the local session database. The failed child was pending with no user_input, PID, accepted turn intent or usage. It retained one failed optimistic user event, one native-ID ledger row and 18 copied shell replays, but its native binding/file had been discarded after read-back verification failed. No private transcript content or identifiers are included here. + +The source SDE events use built-in names such as `run_shell`; Rust native ingestion resolves them to storage names such as `run_command_line`. TypeScript previously wrote the unnormalized name and compared it strictly against the normalized read-back. The first send therefore failed despite identical arguments/output. The catch path then removed a published native artifact without removing its dependent ledger/replay records. Retry could no longer bracket the child's history with a stable revision. + +The canonical IR projection now uses the Rust-owned storage alias registry and lowercase fallback, matching native ingestion for both calls and results. Comparisons still validate call identity, arguments, output, error flags and ordering. Read-back failure still prevents dispatch but retains the published artifact/binding for subsequent inspection and prefix validation. A transient read failure can recover through existing synchronization; a real mismatch still fails closed. + +## Historical remediation + +No live database, transcript or dependent replay was modified. Previously discarded native files are not recreated by this PR. The reported already-stranded child therefore needs separate, explicitly authorized recovery after inventory; its failed prompt and audit records must be preserved. Keeping future failed materializations avoids producing this state again. No schema, IPC or persistence-format change; rollback is a code revert, with retained artifacts remaining ordinary provider transcripts. + +## Architecture review + +| Layer | Verdict / scope | +| -------------------- | ----------------------------------------------------------------------------------------------- | +| 1 Compilation | Full tsgo check and changed-file ESLint passed | +| 2 Ownership | Existing IR producer and native materializer remain the only owners | +| 3 Naming | Storage canonical names replace raw built-in names at the transport boundary | +| 4 Semantics | UI canonical, storage canonical and provider call identity remain distinct | +| 5 Defaults | Unknown tools follow Rust's lowercase fallback; mismatches remain errors | +| 6 Boundaries | Source normalization precedes native writes; no UI predicate or alias exception | +| 7 Discoverability | Comments document alias and rollback failure mechanisms | +| 8 Wire | Outgoing item names asserted through materialize invocation; Rust registry contract tested | +| 9 Init parity | Existing bundled registry initialization reused; no startup changes | +| 10 Resolver symmetry | Calls/results share normalization; materialize/synchronize/prefix consumers use same projection | + +## Lifecycle review + +| Area | Verdict | Evidence | Change or reason kept | Verification | +| ------------------ | ------- | ---------------------------------------- | ------------------------------------------------------- | --------------------------------------------- | +| Background work | keep | User-triggered materialization/read-back | No timers/retries/subscriptions added | Call-chain review and invocation assertions | +| Memory | keep | Existing bounded IR and registry | No retained transcript cache | Source inspection | +| Scope/isolation | fix | Session-bound native receipt | Retain exact published artifact on verification failure | Failure and subsequent synchronize regression | +| Rendering/hot path | keep | No React/streaming changes | No runtime performance claim | Source inspection | + +| Provider | Raw transition | App/UI state | Topology/boundary | Expected invariant | Observed evidence | +| ------------------- | ------------------------------------------------------- | --------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------ | +| SDE → Codex | Native seed write/read-back | Existing local historical failure | Read-only database inventory | Tool aliases preserve semantic identity | Exact error and orphan evidence confirmed | +| Shared native IR | Built-in/CLI/custom names, failed read, synchronization | Unit fixture | Production projection/materializer with mocked IPC | Stable canonical names; no discard; strict changed-output rejection | Targeted regressions passed | +| Codex / Claude Code | GUI switch, retry, restart | Fixed desktop artifact | Native runtime | Successful send and stable reloaded history | Not run | +| All | Idle/hidden/close/delete | Runtime | CPU/RSS | No added background work | Source review only; runtime measurements not run | + +Performance verdict: blocked for runtime acceptance: fixed-build GUI switch/retry/restart and CPU/RSS measurements were not run. No new runtime resource is introduced; this is not a measured performance improvement. + +## Verification + +- `pnpm test src/engines/SessionCore/conversations src/engines/SessionCore/__tests__/rustTsContract.test.ts`: 13 files, 541 tests passed +- `pnpm typecheck:fast`: passed +- `pnpm exec eslint src/engines/SessionCore/conversations/nativeConversationProjection.ts src/engines/SessionCore/conversations/nativeConversationMaterializer.ts src/engines/SessionCore/conversations/nativeConversationMaterializer.test.ts --max-warnings 0`: passed +- `pnpm check:circular`: passed, 8,166 modules +- `pnpm check:test-placement`: passed, 634 directories +- `git diff --check`: passed +- No Rust production code changed; Rust compilation/native provider execution not run +- No visual layout changed; screenshots would not establish native round-trip correctness diff --git a/src/engines/SessionCore/conversations/nativeConversationMaterializer.test.ts b/src/engines/SessionCore/conversations/nativeConversationMaterializer.test.ts index 68f49e9f28..e8c643062c 100644 --- a/src/engines/SessionCore/conversations/nativeConversationMaterializer.test.ts +++ b/src/engines/SessionCore/conversations/nativeConversationMaterializer.test.ts @@ -886,7 +886,7 @@ describe("native conversation materialization", () => { ).rejects.toThrow("round-trip verification failed"); }); - it("removes a failed CLI materialization without touching other native history", async () => { + it("retains a failed CLI materialization for authoritative retry inspection", async () => { mocks.invokeTauri.mockResolvedValueOnce({ nativeSessionId: "native-1", itemCount: 1, @@ -902,12 +902,82 @@ describe("native conversation materialization", () => { timeline: [message("u1", "user", "hello")], }) ).rejects.toThrow("round-trip verification failed"); - expect(mocks.invokeTauri).toHaveBeenNthCalledWith( - 2, - "discard_native_conversation_materialization", - { sessionId: "cliagent-target", nativeSessionId: "native-1" } - ); + expect(mocks.invokeTauri).toHaveBeenCalledTimes(1); + }); + + it("can synchronize a retained materialization after a transient read failure", async () => { + const timeline = [message("u1", "user", "hello")]; + mocks.invokeTauri.mockResolvedValue({ + nativeSessionId: "native-1", + itemCount: 1, + }); + mocks.loadEvents.mockRejectedValueOnce(new Error("read interrupted")); + await expect( + materializeNativeConversation({ sessionId: "cliagent-target", timeline }) + ).rejects.toThrow("read interrupted"); + mocks.loadEvents.mockResolvedValue({ + events: timeline, + source: "native_store", + }); + await expect( + synchronizeNativeConversation({ sessionId: "cliagent-target", timeline }) + ).resolves.toMatchObject({ receipt: { nativeSessionId: "native-1" } }); + expect(mocks.invokeTauri.mock.calls.map(([command]) => command)).toEqual([ + "materialize_native_conversation", + "synchronize_native_conversation", + ]); }); + + it.each([ + ["run_shell", "run_command_line"], + ["Bash", "run_command_line"], + ["Read", "read_file"], + ["Edit", "edit_file_by_replace"], + ["CustomTool", "customtool"], + ])( + "materializes %s using the native reader's %s vocabulary", + async (sourceName, storedName) => { + const source = { + ...tool(), + functionName: sourceName, + result: { output: "done" }, + }; + const replay = { ...source, functionName: storedName }; + mocks.invokeTauri.mockResolvedValue({ + nativeSessionId: "native-1", + itemCount: 2, + }); + mocks.loadEvents.mockResolvedValue({ + events: [replay], + source: "native_store", + }); + await expect( + materializeNativeConversation({ + sessionId: "cliagent-target", + timeline: [source], + }) + ).resolves.toMatchObject({ receipt: { itemCount: 2 } }); + const items = mocks.invokeTauri.mock.calls[0][1].items; + expect(items.map((item: { name: string }) => item.name)).toEqual([ + storedName, + storedName, + ]); + expect( + nativeConversationItemsAreProviderPortablePrefix( + projectNativeConversationItems([source]), + projectNativeConversationItems([replay]) + ) + ).toBe(true); + expect( + nativeConversationItemsEqual( + projectNativeConversationItems([source]), + projectNativeConversationItems([ + { ...replay, result: { output: "changed" } }, + ]) + ) + ).toBe(false); + } + ); }); describe("structured tool names and arguments", () => { diff --git a/src/engines/SessionCore/conversations/nativeConversationMaterializer.ts b/src/engines/SessionCore/conversations/nativeConversationMaterializer.ts index d0d901f982..e091ca23ee 100644 --- a/src/engines/SessionCore/conversations/nativeConversationMaterializer.ts +++ b/src/engines/SessionCore/conversations/nativeConversationMaterializer.ts @@ -9,7 +9,6 @@ import type { SessionEvent } from "@src/engines/SessionCore/core/types"; import { loadAuthoritativeSessionEvents } from "@src/engines/SessionCore/sync/authoritativeSessionEvents"; import { invokeTauri } from "@src/util/platform/tauri/init"; -import { isCliSession } from "@src/util/session/sessionDispatch"; import { type LocalConversationTarget, @@ -141,29 +140,24 @@ export async function materializeNativeConversation(params: { { sessionId: params.sessionId, items } ); const receipt: NativeMaterializationReceipt = { ...wireReceipt, fidelity }; - try { - if (receipt.itemCount !== items.length) { - throw new Error( - `native materializer wrote ${receipt.itemCount} of ${items.length} items` - ); - } - const { events } = await loadAuthoritativeSessionEvents(params.sessionId); - const roundTripped = projectNativeConversationItems(events); - if (!nativeConversationItemsEqual(items, roundTripped)) { - throw new Error( - `native transcript round-trip verification failed; the target session was not started (${nativeConversationMismatch(items, roundTripped)})` - ); - } - return { events, receipt }; - } catch (error) { - if (isCliSession(params.sessionId)) { - await invokeTauri("discard_native_conversation_materialization", { - sessionId: params.sessionId, - nativeSessionId: receipt.nativeSessionId, - }).catch(() => undefined); - } - throw error; + // Rust has already published this artifact and its native binding. Keep + // them on a failed read/verification so Retry can read a stable revision + // and revalidate the prefix. Deleting only the file and binding leaves + // ledger/replay witnesses behind and permanently poisons canonical reads. + // The caller still fails closed before dispatch on every mismatch. + if (receipt.itemCount !== items.length) { + throw new Error( + `native materializer wrote ${receipt.itemCount} of ${items.length} items` + ); } + const { events } = await loadAuthoritativeSessionEvents(params.sessionId); + const roundTripped = projectNativeConversationItems(events); + if (!nativeConversationItemsEqual(items, roundTripped)) { + throw new Error( + `native transcript round-trip verification failed; the target session was not started (${nativeConversationMismatch(items, roundTripped)})` + ); + } + return { events, receipt }; } /** diff --git a/src/engines/SessionCore/conversations/nativeConversationProjection.ts b/src/engines/SessionCore/conversations/nativeConversationProjection.ts index e24d5215ee..efa0a08212 100644 --- a/src/engines/SessionCore/conversations/nativeConversationProjection.ts +++ b/src/engines/SessionCore/conversations/nativeConversationProjection.ts @@ -5,6 +5,7 @@ */ import type { SessionEvent } from "@src/engines/SessionCore/core/types"; import { isInternalLifecycleEvent } from "@src/engines/SessionCore/ingestion/visibilityFilters"; +import { getCliStorageCanonical } from "@src/engines/SessionCore/rendering/registry/initToolRegistry"; import { conversationSenderStampOf } from "./conversationSenderMetadata"; import type { @@ -209,10 +210,14 @@ export function projectNativeConversationItems( continue; } const callId = portableToolCallId(event); - const name = event.functionName.trim(); - if (!name) { + const rawName = event.functionName.trim(); + if (!rawName) { throw new Error(`native transcript tool event ${event.id} has no name`); } + // SDE events retain built-in names (run_shell), while native replay + // resolves storage aliases (run_command_line). Project both through + // the same Rust-owned alias registry before writing or comparing IR. + const name = getCliStorageCanonical(rawName).toLowerCase(); items.push({ kind: "tool_call", id: `${nativeSourceEventId(event)}:call`, From 3c67fa7d21733b9f1392e349e32a9d14550aa8ed Mon Sep 17 00:00:00 2001 From: Neonforge <48338160+Neonforge98@users.noreply.github.com> Date: Sat, 26 Sep 2026 13:18:43 -0700 Subject: [PATCH 2/4] fix(session-replay): recover native provider switch retries Pre-commit hook ran. Total eslint: 0, total circular: 0 --- src-tauri/src/agent_sessions/cli/native_ir.rs | 41 ++++++++++++- .../cli/session_runner/codex_reserve.rs | 1 + .../cli/session_runner/session.rs | 33 ++++++++--- .../cli/session_runner/session/tests.rs | 26 +++++++++ .../__tests__/useEditUserMessage.test.ts | 58 +++++++++++++++++++ .../ChatHistory/hooks/useEditUserMessage.ts | 35 ++++++++--- 6 files changed, 176 insertions(+), 18 deletions(-) diff --git a/src-tauri/src/agent_sessions/cli/native_ir.rs b/src-tauri/src/agent_sessions/cli/native_ir.rs index ff1f34e1e0..41fdb2fd2f 100644 --- a/src-tauri/src/agent_sessions/cli/native_ir.rs +++ b/src-tauri/src/agent_sessions/cli/native_ir.rs @@ -485,7 +485,12 @@ pub(super) fn native_items_from_agent_history(history: &[Value]) -> Vec Vec, +) -> Option { + if let Some(account_id) = account_id { + return key_service.get_key_by_id(account_id); + } + // Claude Code's Default model means the native CLI login, not an + // arbitrary saved account. Picking the oldest credential here can select + // a disabled account and make every retry fail before the CLI starts. + if key_source == KeySource::OwnKey && !matches!(agent, ModelType::ClaudeCode) { + return key_service.get_key(agent, None); + } + None +} + /// Routing/auth variables owned by an explicit Claude account selection. /// `tokio::process::Command` inherits the desktop process environment, so a /// variable that is absent from the newly selected account must be explicitly @@ -539,14 +557,13 @@ pub(crate) async fn run_session_with_ide_context( // handles the routing. // Keep dynamic-source ownership alive through spawn, retries and finalization. let managed_execution = crate::cli_managed_proxy::prepare_execution_profile(&session).await?; - let mut selected_key = session - .account_id - .as_deref() - .and_then(|id| key_vault::key_store::KEY_SERVICE.get_key_by_id(id)); + let mut selected_key = initial_cli_key( + &KEY_SERVICE, + &agent, + session.key_source, + session.account_id.as_deref(), + ); if session.key_source == KeySource::OwnKey { - if session.account_id.is_none() { - selected_key = KEY_SERVICE.get_key(&agent, None); - } if let Some(account_id) = session.account_id.as_deref() { selected_key = match agent { ModelType::Codex => { diff --git a/src-tauri/src/agent_sessions/cli/session_runner/session/tests.rs b/src-tauri/src/agent_sessions/cli/session_runner/session/tests.rs index f6b3465baf..7589d71d56 100644 --- a/src-tauri/src/agent_sessions/cli/session_runner/session/tests.rs +++ b/src-tauri/src/agent_sessions/cli/session_runner/session/tests.rs @@ -41,6 +41,32 @@ fn codex_turns_always_use_the_desktop_visible_native_transport() { } } +#[test] +fn ambient_claude_launch_does_not_inherit_a_saved_account() { + let home = tempfile::tempdir().expect("temporary credential home"); + let service = KeyService::new(Some(home.path().to_path_buf())); + let mut stale = ModelKey::new(ModelType::ClaudeCode); + stale.enabled = false; + stale.name = Some("old disabled Claude login".to_string()); + let stale = service.save_key(stale).expect("save stale account"); + + assert!( + initial_cli_key(&service, &ModelType::ClaudeCode, KeySource::OwnKey, None).is_none(), + "the Default Claude CLI model must use ambient auth, even when saved accounts exist" + ); + assert_eq!( + initial_cli_key( + &service, + &ModelType::ClaudeCode, + KeySource::OwnKey, + Some(&stale.id), + ) + .map(|key| key.id), + Some(stale.id), + "an explicitly selected account must retain its identity for launch validation" + ); +} + #[test] fn command_logging_redacts_mcp_config_values() { let raw = vec![ diff --git a/src/engines/ChatPanel/ChatHistory/hooks/__tests__/useEditUserMessage.test.ts b/src/engines/ChatPanel/ChatHistory/hooks/__tests__/useEditUserMessage.test.ts index 6f9ab3ab13..9e96faf888 100644 --- a/src/engines/ChatPanel/ChatHistory/hooks/__tests__/useEditUserMessage.test.ts +++ b/src/engines/ChatPanel/ChatHistory/hooks/__tests__/useEditUserMessage.test.ts @@ -37,10 +37,12 @@ const { evictSessionSpy, invokeTauriSpy, flushMessageQueueSpy, + getEventsSpy, hydrateMessageQueueSpy, messageQueueHydrated, queuedDeliveries, realQueueStore, + reconcileOrphanSpy, removeByIdPrefixSpy, updateByIdSpy, upsertSpy, @@ -57,10 +59,12 @@ const { evictSessionSpy: vi.fn(async () => undefined), invokeTauriSpy: vi.fn(async () => 0), flushMessageQueueSpy: vi.fn(async () => undefined), + getEventsSpy: vi.fn(async () => [] as SessionEvent[]), hydrateMessageQueueSpy: vi.fn(async () => undefined), messageQueueHydrated: { current: true }, queuedDeliveries: { current: [] as Array> }, realQueueStore: { current: null as Store | null }, + reconcileOrphanSpy: vi.fn(async () => false), removeByIdPrefixSpy: vi.fn(async () => 1), updateByIdSpy: vi.fn(async () => true), upsertSpy: vi.fn(async (..._args: unknown[]) => undefined), @@ -140,6 +144,7 @@ vi.mock("@src/engines/SessionCore/core/atoms", () => ({ vi.mock("@src/engines/SessionCore/core/store/EventStoreProxy", () => ({ eventStoreProxy: { + getEvents: getEventsSpy, removeByIdPrefix: removeByIdPrefixSpy, updateById: updateByIdSpy, upsert: upsertSpy, @@ -153,6 +158,7 @@ vi.mock( () => ({ flushMessageQueuePersistence: flushMessageQueueSpy, hydrateMessageQueue: hydrateMessageQueueSpy, + reconcileOrphanedOptimisticQueueProjections: reconcileOrphanSpy, refreshMessageDeliveries: refreshMessageDeliveriesSpy, }) ); @@ -260,7 +266,11 @@ describe("useEditUserMessage resend projection", () => { evictSessionSpy.mockClear(); invokeTauriSpy.mockClear(); flushMessageQueueSpy.mockClear(); + getEventsSpy.mockReset(); + getEventsSpy.mockResolvedValue([]); hydrateMessageQueueSpy.mockClear(); + reconcileOrphanSpy.mockReset(); + reconcileOrphanSpy.mockResolvedValue(false); hydrateMessageQueueSpy.mockImplementation(async () => { queuedDeliveries.current = [...durableHydrationRows.current]; messageQueueHydrated.current = true; @@ -491,6 +501,54 @@ describe("useEditUserMessage resend projection", () => { ); }); + it("repairs a missing durable owner on Retry before resubmitting the failed turn", async () => { + storeSessionId.current = "sdeagent-root"; + const failed = { + event: { + id: "queued-user:queue-orphan:", + sessionId: "cliagent-child", + source: "user", + displayText: "continue the conversation", + displayStatus: "failed", + result: { + syntheticUserInput: true, + deliveryStatus: "failed", + queueMessageId: "queue-orphan", + turnIntentId: "intent-orphan", + deliveryError: "native transcript mismatch", + }, + }, + chunk_id: "queued-user:queue-orphan:", + } as unknown as OptimizedChatItem; + reconcileOrphanSpy.mockResolvedValueOnce(true); + const failedEvent = failed.event as SessionEvent; + getEventsSpy.mockResolvedValueOnce([ + { + ...failedEvent, + result: { + ...failedEvent.result, + queueMessageId: undefined, + }, + } as SessionEvent, + ]); + + await act(async () => { + await editUserMessage?.(failed, "continue the conversation"); + }); + + expect(reconcileOrphanSpy).toHaveBeenCalledWith("cliagent-child"); + expect(submitUserIntentSpy).toHaveBeenCalledWith( + expect.objectContaining({ + sessionId: "sdeagent-root", + turnIntentId: "intent-orphan", + }) + ); + expect(removeByIdPrefixSpy).toHaveBeenCalledWith( + "queued-user:queue-orphan:", + "cliagent-child" + ); + }); + it.each(["delivery", "execution"] as const)( "retries a hydrated %s failure in place without losing attachments", async (kind) => { diff --git a/src/engines/ChatPanel/ChatHistory/hooks/useEditUserMessage.ts b/src/engines/ChatPanel/ChatHistory/hooks/useEditUserMessage.ts index 066a242acc..9cd2fb8637 100644 --- a/src/engines/ChatPanel/ChatHistory/hooks/useEditUserMessage.ts +++ b/src/engines/ChatPanel/ChatHistory/hooks/useEditUserMessage.ts @@ -38,6 +38,7 @@ import { eventStoreProxy } from "@src/engines/SessionCore/core/store/EventStoreP import { flushMessageQueuePersistence, hydrateMessageQueue, + reconcileOrphanedOptimisticQueueProjections, refreshMessageDeliveries, } from "@src/engines/SessionCore/hooks/session/messageQueuePersistence"; import { @@ -157,6 +158,8 @@ export function useEditUserMessage( // replaced. A retired accepted turn must remain in the history; never // truncate later turns or offer a file rewind for this case. if (failedSyntheticIntent && initiatedSessionId && chatItem.event) { + const failedProjectionSessionId = + chatItem.event.sessionId ?? initiatedSessionId; const originalText = chatItem.event.displayText ?? ""; const originalTurnIntentId = turnIntentIdOf(chatItem.event); const ownerRetired = @@ -202,12 +205,25 @@ export function useEditUserMessage( } if (queueMessageId && !durableFailedQueueRow) { if (chatItem.event.result?.deliveryOwnerRetired !== true) { - // queueMessageId is an ownership claim, not a hint. Falling - // back to a new submit here would delete the only visible root - // row and create a second delivery on whichever Session is - // currently mounted. Preserve the failed bubble until its owner - // is readable. - throw new Error("failed delivery owner is not available yet"); + // A restart can leave the failed EventStore projection after + // its durable queue owner has gone away. The passive orphan + // scan may have run before this session was mounted, so prove + // absence at Retry and remove only that stale ownership claim. + await reconcileOrphanedOptimisticQueueProjections( + failedProjectionSessionId + ); + const currentFailedRow = ( + await eventStoreProxy.getEvents(failedProjectionSessionId) + ).find((event) => event.id === eventId); + if ( + currentFailedRow?.displayStatus !== "failed" || + currentFailedRow.result?.deliveryStatus !== "failed" || + currentFailedRow.result?.queueMessageId != null + ) { + // A still-owned, accepted, or concurrently changed row must + // never become a second dispatch. + throw new Error("failed delivery owner is not available yet"); + } } // The dispatcher retired this owner after a terminal provider/ // Cloud verdict and stamped the row. The failed bubble is the only @@ -302,14 +318,17 @@ export function useEditUserMessage( }); } if (!preserveAcceptedHistory) { - await eventStoreProxy.removeByIdPrefix(eventId, initiatedSessionId); + await eventStoreProxy.removeByIdPrefix( + eventId, + failedProjectionSessionId + ); } } catch (error) { // A send-stage error already produced the replacement failed row. // A preparation/storage error did not, so retain the original row. if (!preserveAcceptedHistory && isUserIntentSendError(error)) { await eventStoreProxy - .removeByIdPrefix(eventId, initiatedSessionId) + .removeByIdPrefix(eventId, failedProjectionSessionId) .catch(() => 0); } log.error( From b52348120182f5d523ccc2ac2b0e861df5c98481 Mon Sep 17 00:00:00 2001 From: Neonforge <48338160+Neonforge98@users.noreply.github.com> Date: Sat, 26 Sep 2026 13:25:59 -0700 Subject: [PATCH 3/4] docs(session-replay): record native validation and retry scope Pre-commit hook ran. Total eslint: 0, total circular: 0 --- .../NativeConversationMaterialization.md | 64 ++++++++++--------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/docs/org2-performance-guard-2026-09-26/NativeConversationMaterialization.md b/docs/org2-performance-guard-2026-09-26/NativeConversationMaterialization.md index a1dc3c41a1..e094902c53 100644 --- a/docs/org2-performance-guard-2026-09-26/NativeConversationMaterialization.md +++ b/docs/org2-performance-guard-2026-09-26/NativeConversationMaterialization.md @@ -4,46 +4,48 @@ Read-only inspection matched the reported SDE conversation and its Codex child in the local session database. The failed child was pending with no user_input, PID, accepted turn intent or usage. It retained one failed optimistic user event, one native-ID ledger row and 18 copied shell replays, but its native binding/file had been discarded after read-back verification failed. No private transcript content or identifiers are included here. -The source SDE events use built-in names such as `run_shell`; Rust native ingestion resolves them to storage names such as `run_command_line`. TypeScript previously wrote the unnormalized name and compared it strictly against the normalized read-back. The first send therefore failed despite identical arguments/output. The catch path then removed a published native artifact without removing its dependent ledger/replay records. Retry could no longer bracket the child's history with a stable revision. +The source SDE events use built-in names such as `run_shell`; Rust native ingestion resolves them to storage names such as `run_command_line`. TypeScript previously wrote the unnormalized name and compared it strictly against the normalized read-back. The first send therefore failed despite identical arguments/output. The catch path then removed a published native artifact without removing its dependent ledger/replay records. Retry could no longer bracket the child's history with a stable revision. A native app run also reached Codex CLI → SDE and reproduced the reverse mismatch: Rust projected the retained SDE history using the raw executable name while canonical SessionEvents used the storage name. -The canonical IR projection now uses the Rust-owned storage alias registry and lowercase fallback, matching native ingestion for both calls and results. Comparisons still validate call identity, arguments, output, error flags and ordering. Read-back failure still prevents dispatch but retains the published artifact/binding for subsequent inspection and prefix validation. A transient read failure can recover through existing synchronization; a real mismatch still fails closed. +The canonical TS materialization and Rust SDE history read-back projections now use the Rust-owned storage alias registry and lowercase fallback, matching native ingestion for both calls and results. The native executable history remains unchanged. Comparisons still validate call identity, arguments, output, error flags and ordering. Read-back failure still prevents dispatch but retains the published artifact/binding for subsequent inspection and prefix validation. A transient read failure can recover through existing synchronization; a real mismatch still fails closed. + +After a restart, the native run exposed a second Retry blocker: its failed EventStore projection retained a `queueMessageId`, while the durable queue had no corresponding delivery. The Retry path now asks the existing orphan reconciler to inspect that exact session, reads the row back, and proceeds only if it is still failed and its ownership claim has been removed. This is bounded work on a user click, not a background scan. Claude Code Default is also resolved through the ambient native CLI login rather than an unrelated saved credential; an explicitly selected account still uses its own key. ## Historical remediation -No live database, transcript or dependent replay was modified. Previously discarded native files are not recreated by this PR. The reported already-stranded child therefore needs separate, explicitly authorized recovery after inventory; its failed prompt and audit records must be preserved. Keeping future failed materializations avoids producing this state again. No schema, IPC or persistence-format change; rollback is a code revert, with retained artifacts remaining ordinary provider transcripts. +No live database, transcript or dependent replay was manually modified. Previously discarded native files are not recreated by this PR. The reported already-stranded child therefore needs separate, explicitly authorized recovery after inventory; its failed prompt and audit records must be preserved. Future failed queue projections with no durable owner can be reconciled narrowly when their Retry button is clicked. Keeping future failed materializations avoids producing the missing-native-artifact state again. No schema, IPC or persistence-format change; rollback is a code revert, with retained artifacts remaining ordinary provider transcripts. ## Architecture review -| Layer | Verdict / scope | -| -------------------- | ----------------------------------------------------------------------------------------------- | -| 1 Compilation | Full tsgo check and changed-file ESLint passed | -| 2 Ownership | Existing IR producer and native materializer remain the only owners | -| 3 Naming | Storage canonical names replace raw built-in names at the transport boundary | -| 4 Semantics | UI canonical, storage canonical and provider call identity remain distinct | -| 5 Defaults | Unknown tools follow Rust's lowercase fallback; mismatches remain errors | -| 6 Boundaries | Source normalization precedes native writes; no UI predicate or alias exception | -| 7 Discoverability | Comments document alias and rollback failure mechanisms | -| 8 Wire | Outgoing item names asserted through materialize invocation; Rust registry contract tested | -| 9 Init parity | Existing bundled registry initialization reused; no startup changes | -| 10 Resolver symmetry | Calls/results share normalization; materialize/synchronize/prefix consumers use same projection | +| Layer | Verdict / scope | +| -------------------- | ------------------------------------------------------------------------------------------------ | +| 1 Compilation | Full tsgo check, changed-file ESLint, Rust tests and clippy passed | +| 2 Ownership | Canonical TS/Rust projections own aliasing; the queue reconciler owns orphan claims | +| 3 Naming | Storage canonical names replace raw built-in names only at comparison boundaries | +| 4 Semantics | Executable native history, storage canonical names and provider call identity remain distinct | +| 5 Defaults | Unknown tools follow Rust's lowercase fallback; Claude Default uses ambient native login | +| 6 Boundaries | Projection normalization precedes native comparison; Retry proves absent queue ownership | +| 7 Discoverability | Comments document alias, credential and orphan Retry mechanisms | +| 8 Wire | Materialize invocation and Rust prefix regressions assert canonical names; wire format unchanged | +| 9 Init parity | Existing bundled registry initialization reused; native CLI launch profile tested | +| 10 Resolver symmetry | Calls/results share normalization in both TS and Rust projections | ## Lifecycle review -| Area | Verdict | Evidence | Change or reason kept | Verification | -| ------------------ | ------- | ---------------------------------------- | ------------------------------------------------------- | --------------------------------------------- | -| Background work | keep | User-triggered materialization/read-back | No timers/retries/subscriptions added | Call-chain review and invocation assertions | -| Memory | keep | Existing bounded IR and registry | No retained transcript cache | Source inspection | -| Scope/isolation | fix | Session-bound native receipt | Retain exact published artifact on verification failure | Failure and subsequent synchronize regression | -| Rendering/hot path | keep | No React/streaming changes | No runtime performance claim | Source inspection | +| Area | Verdict | Evidence | Change or reason kept | Verification | +| ------------------ | ------- | ------------------------------------------------- | ------------------------------------------------------------------- | ---------------------------------------------- | +| Background work | keep | User-triggered materialization/read-back/Retry | No timers or subscriptions added; Retry reconciles once | Call-chain review and invocation assertions | +| Memory | keep | Existing bounded IR and registry | No retained transcript cache | Source inspection | +| Scope/isolation | fix | Session-bound native receipt and failed queue row | Retain exact artifact; reconcile only the failed projection session | Failure, synchronization and Retry regressions | +| Rendering/hot path | keep | No React/streaming changes | No runtime performance claim | Source inspection | -| Provider | Raw transition | App/UI state | Topology/boundary | Expected invariant | Observed evidence | -| ------------------- | ------------------------------------------------------- | --------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------ | -| SDE → Codex | Native seed write/read-back | Existing local historical failure | Read-only database inventory | Tool aliases preserve semantic identity | Exact error and orphan evidence confirmed | -| Shared native IR | Built-in/CLI/custom names, failed read, synchronization | Unit fixture | Production projection/materializer with mocked IPC | Stable canonical names; no discard; strict changed-output rejection | Targeted regressions passed | -| Codex / Claude Code | GUI switch, retry, restart | Fixed desktop artifact | Native runtime | Successful send and stable reloaded history | Not run | -| All | Idle/hidden/close/delete | Runtime | CPU/RSS | No added background work | Source review only; runtime measurements not run | +| Provider | Raw transition | App/UI state | Topology/boundary | Expected invariant | Observed evidence | +| ------------------- | ------------------------------------------------------- | ---------------- | ------------------------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| SDE → Codex | Native seed write/read-back | Native macOS app | Local synthetic conversation | Tool aliases preserve semantic identity | Send succeeded after TS fix | +| Shared native IR | Built-in/CLI/custom names, failed read, synchronization | Unit fixture | TS materializer and Rust history projection | Stable canonical names; no discard; strict changed-output rejection | Targeted TS/Rust regressions passed | +| Codex / Claude Code | GUI switch, retry, restart | Native macOS app | Same synthetic conversation | Successful send and stable reloaded history | First four chain legs passed; reverse SDE mismatch reproduced before Rust fix; fixed-build Retry pending unlock | +| All | Idle/hidden/close/delete | Runtime | CPU/RSS | No added background work | Source review only; runtime measurements not run | -Performance verdict: blocked for runtime acceptance: fixed-build GUI switch/retry/restart and CPU/RSS measurements were not run. No new runtime resource is introduced; this is not a measured performance improvement. +Performance verdict: blocked for runtime acceptance: fixed-build Retry/restart and CPU/RSS measurements remain pending. No new retained runtime resource is introduced; this is not a measured performance improvement. ## Verification @@ -52,6 +54,10 @@ Performance verdict: blocked for runtime acceptance: fixed-build GUI switch/retr - `pnpm exec eslint src/engines/SessionCore/conversations/nativeConversationProjection.ts src/engines/SessionCore/conversations/nativeConversationMaterializer.ts src/engines/SessionCore/conversations/nativeConversationMaterializer.test.ts --max-warnings 0`: passed - `pnpm check:circular`: passed, 8,166 modules - `pnpm check:test-placement`: passed, 634 directories +- `pnpm test src/engines/ChatPanel/ChatHistory/hooks/__tests__/useEditUserMessage.test.ts src/engines/SessionCore/hooks/session/__tests__/messageQueuePersistence.test.ts`: 2 files, 56 tests passed +- `cargo test -p org2 --lib agent_sessions::cli::native_ir::tests`: 11 passed; `cargo test -p org2 --lib ambient_claude_launch_does_not_inherit_a_saved_account`: 1 passed +- Changed-file rustfmt and pre-commit `cargo clippy -p org2`: passed - `git diff --check`: passed -- No Rust production code changed; Rust compilation/native provider execution not run +- Native macOS app completed SDE → Codex CLI → Claude CLI → Codex CLI, then reproduced the reverse mismatch on Codex CLI → SDE before the Rust fix; fixed-build Retry awaits unlocked UI access +- Full `cargo fmt --all -- --check` reports pre-existing formatting differences outside this diff; changed Rust files passed rustfmt individually - No visual layout changed; screenshots would not establish native round-trip correctness From bfd318d61f380e63b2126fbdae79e850d531a9f1 Mon Sep 17 00:00:00 2001 From: Neonforge <48338160+Neonforge98@users.noreply.github.com> Date: Sat, 26 Sep 2026 15:37:27 -0700 Subject: [PATCH 4/4] docs(session-replay): record native continuation verification Pre-commit hook ran. Total eslint: 0, total circular: 0 --- .../NativeConversationMaterialization.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/org2-performance-guard-2026-09-26/NativeConversationMaterialization.md b/docs/org2-performance-guard-2026-09-26/NativeConversationMaterialization.md index e094902c53..c2ccd6f31d 100644 --- a/docs/org2-performance-guard-2026-09-26/NativeConversationMaterialization.md +++ b/docs/org2-performance-guard-2026-09-26/NativeConversationMaterialization.md @@ -38,14 +38,14 @@ No live database, transcript or dependent replay was manually modified. Previous | Scope/isolation | fix | Session-bound native receipt and failed queue row | Retain exact artifact; reconcile only the failed projection session | Failure, synchronization and Retry regressions | | Rendering/hot path | keep | No React/streaming changes | No runtime performance claim | Source inspection | -| Provider | Raw transition | App/UI state | Topology/boundary | Expected invariant | Observed evidence | -| ------------------- | ------------------------------------------------------- | ---------------- | ------------------------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | -| SDE → Codex | Native seed write/read-back | Native macOS app | Local synthetic conversation | Tool aliases preserve semantic identity | Send succeeded after TS fix | -| Shared native IR | Built-in/CLI/custom names, failed read, synchronization | Unit fixture | TS materializer and Rust history projection | Stable canonical names; no discard; strict changed-output rejection | Targeted TS/Rust regressions passed | -| Codex / Claude Code | GUI switch, retry, restart | Native macOS app | Same synthetic conversation | Successful send and stable reloaded history | First four chain legs passed; reverse SDE mismatch reproduced before Rust fix; fixed-build Retry pending unlock | -| All | Idle/hidden/close/delete | Runtime | CPU/RSS | No added background work | Source review only; runtime measurements not run | +| Provider | Raw transition | App/UI state | Topology/boundary | Expected invariant | Observed evidence | +| ------------------- | ------------------------------------------------------- | ---------------- | ------------------------------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| SDE → Codex | Native seed write/read-back | Native macOS app | Local synthetic conversation | Tool aliases preserve semantic identity | Send succeeded after TS fix | +| Shared native IR | Built-in/CLI/custom names, failed read, synchronization | Unit fixture | TS materializer and Rust history projection | Stable canonical names; no discard; strict changed-output rejection | Targeted TS/Rust regressions passed | +| Codex / Claude Code | GUI switch, retry, restart | Native macOS app | Same synthetic conversation | Successful send and stable reloaded history | Six sequential SDE → Codex CLI → Claude CLI → Codex CLI → SDE → Claude CLI sends passed; reverse SDE mismatch reproduced before the Rust fix, then fixed-build Retry and cold restart passed | +| All | Idle/hidden/close/delete | Runtime | CPU/RSS | No added background work | Source review only; runtime measurements not run | -Performance verdict: blocked for runtime acceptance: fixed-build Retry/restart and CPU/RSS measurements remain pending. No new retained runtime resource is introduced; this is not a measured performance improvement. +Performance verdict: native round-trip, Retry and cold-restart acceptance passed. CPU/RSS and idle/hidden resource use were not measured; no performance improvement is claimed. No new retained runtime resource is introduced. ## Verification @@ -58,6 +58,7 @@ Performance verdict: blocked for runtime acceptance: fixed-build Retry/restart a - `cargo test -p org2 --lib agent_sessions::cli::native_ir::tests`: 11 passed; `cargo test -p org2 --lib ambient_claude_launch_does_not_inherit_a_saved_account`: 1 passed - Changed-file rustfmt and pre-commit `cargo clippy -p org2`: passed - `git diff --check`: passed -- Native macOS app completed SDE → Codex CLI → Claude CLI → Codex CLI, then reproduced the reverse mismatch on Codex CLI → SDE before the Rust fix; fixed-build Retry awaits unlocked UI access +- Native macOS app completed SDE → Codex CLI → Claude CLI → Codex CLI → SDE → Claude CLI in one synthetic conversation. Codex CLI → SDE reproduced the reverse mismatch before the Rust fix; fixed-build Retry passed, and cold restart preserved all six prompt/response pairs once +- Two isolated native app data homes and local clones exchanged Team Chat messages without starting an Agent run. A subsequent Agent prompt recovered both human marker strings from context; this is an integration check, not a change to Team Chat's writer - Full `cargo fmt --all -- --check` reports pre-existing formatting differences outside this diff; changed Rust files passed rustfmt individually - No visual layout changed; screenshots would not establish native round-trip correctness