From 12d479e6400c71904d647de44066fa0eb52db92d Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:39:43 -0800 Subject: [PATCH] fix(native-chat): refresh invalid full work projections Use the existing SQL authority and context validation before preserving a full projection, so refreshed narrow work can be reused after row-free invalidation. Preserve a valid full projection arriving during a narrow read and extend the existing owner scenario for both transitions. Correct the description in b98a2544bffe: the report698 strict misses are small measured overhead, not proven noise. Empty warm currentWork p50 is 3.542 to 3.709 us (+0.167 us, 60/80 pairs slower, ratio 1.04715, 95% CI 1.02291-1.07259). Empty/2-subscriber/changed writer ack p50 rises 5.5%, 0.259459 to 0.273833 ms, with its CI excluding parity, on an unchanged writer path; completion falls 1.546834 to 0.657125 ms. The coordinator accepts these marginal costs; the strict FAILs remain disclosed. Frozen714 patch 6a4ded1ee72e8c0134a9a2121fb39bde618213e3f740b4888fd13c8900274620. Owning 53 tests pass on both configured entries; static gates pass. Private point attribution verifies repeated narrow reuse and all 92 serving/frame outputs match. Focused714 against pre-P7 base still measures a small warm currentWork median overhead; report716 preserves raw samples and CIs. --- ...agent-session-status-journal-projection.ts | 2 +- ...tured-agent-session-status-reentry.test.ts | 29 ++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/main/native-chat/agent-session-wire/observations/structured-agent-session-status-journal-projection.ts b/src/main/native-chat/agent-session-wire/observations/structured-agent-session-status-journal-projection.ts index d7d0119c699..4f7a72ad118 100644 --- a/src/main/native-chat/agent-session-wire/observations/structured-agent-session-status-journal-projection.ts +++ b/src/main/native-chat/agent-session-wire/observations/structured-agent-session-status-journal-projection.ts @@ -141,7 +141,7 @@ export class StructuredAgentSessionJournalProjections { ) { continue } - if (this.currentEntry(journal)?.kind !== 'full') { + if (!this.currentSql(journal, recaptured)) { this.byJournal.set(journal, { kind: 'work-only', answer: result, diff --git a/src/main/native-chat/agent-session-wire/observations/testing/structured-agent-session-status-reentry.test.ts b/src/main/native-chat/agent-session-wire/observations/testing/structured-agent-session-status-reentry.test.ts index f38dfe290c2..08b0de1d8e7 100644 --- a/src/main/native-chat/agent-session-wire/observations/testing/structured-agent-session-status-reentry.test.ts +++ b/src/main/native-chat/agent-session-wire/observations/testing/structured-agent-session-status-reentry.test.ts @@ -122,6 +122,7 @@ it('discards row-free authority changes before publishing an asynchronous observ const journal = await openJournal() const projections = new StructuredAgentSessionJournalProjections() let revision = 0 + let contextId: string | null = null let attached = true const held: string[] = [] const capture = () => { @@ -144,7 +145,7 @@ it('discards row-free authority changes before publishing an asynchronous observ stopRevision: 0, stop: null } - return { authority, contextId: null } + return { authority, contextId } } const narrow = await projections.readWorkSql(journal, capture) expect(narrow?.result.kind).toBe('state') @@ -206,9 +207,18 @@ it('discards row-free authority changes before publishing an asynchronous observ expect(projections.savedStop(journal, null)).toEqual({ value: null }) held.push('later-owner') expect(current?.authority?.child?.heldSends).toEqual(['new-live-owner']) + const refreshedWork = await projections.readWorkSql(journal, capture) + expect(await projections.readWorkSql(journal, capture)).toEqual(refreshedWork) + expect(projections.savedStop(journal, null)).toBeUndefined() expect(await projections.readSql(journal, capture, () => {})).toMatchObject({ authority: { child: { heldSends: ['later-owner', 'new-live-owner'] } } }) + contextId = 'reopened-context' + const contextWork = await projections.readWorkSql(journal, capture) + expect(await projections.readWorkSql(journal, capture)).toEqual(contextWork) + expect(projections.currentSql(journal, capture())).toBeNull() + expect(projections.savedStop(journal, contextId)).toBeUndefined() + await projections.readSql(journal, capture, () => {}) revision = 2 const failed = Promise.withResolvers>>() const failure = new Error('observer read failed') @@ -223,4 +233,21 @@ it('discards row-free authority changes before publishing an asynchronous observ { status: 'rejected', reason: failure } ]) expect(await projections.readSql(journal, capture, () => {})).toMatchObject({ workRevision: 2 }) + revision = 3 + const workEntered = Promise.withResolvers() + const workRelease = Promise.withResolvers() + const workRead = journal.reads.work.bind(journal.reads) + vi.spyOn(journal.reads, 'work').mockImplementationOnce(async (input) => { + const answer = await workRead(input) + workEntered.resolve() + await workRelease.promise + return answer + }) + const pendingWork = projections.readWorkSql(journal, capture) + await workEntered.promise + const published = await projections.readSql(journal, capture, () => {}) + workRelease.resolve() + await pendingWork + expect(projections.currentSql(journal, capture())).toBe(published) + expect(projections.savedStop(journal, contextId)).toEqual({ value: null }) })