Skip to content

Commit c8babc7

Browse files
authored
perf: reduce hashing and query overhead in history processing (#277)
* perf: batch semantic hashing and stream history revisions * perf: reuse prepared queries for streaming turn lookups * perf: retain owned history across the durable store boundary
1 parent 4357fe4 commit c8babc7

6 files changed

Lines changed: 280 additions & 13 deletions

File tree

‎packages/local-runtime-v2/src/infra/file/canonical-history-jsonl.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,14 @@ function decodeRecords(records: readonly CanonicalHistoryEnvelope[]): CanonicalH
564564
}
565565

566566
function revisionOfNormalized(records: readonly CanonicalHistoryEnvelope[]): string {
567-
return `sha256:${createHash('sha256').update(canonicalJson(records), 'utf8').digest('hex')}`;
567+
// Preserve the canonical JSON array bytes without building a sorted copy and
568+
// serialized string of the entire history at once.
569+
const hash = createHash('sha256').update('[');
570+
for (let index = 0; index < records.length; index += 1) {
571+
if (index > 0) hash.update(',');
572+
hash.update(canonicalJson(records[index]), 'utf8');
573+
}
574+
return `sha256:${hash.update(']').digest('hex')}`;
568575
}
569576

570577
function decodeMessage(value: unknown): CanonicalHistoryMessage {

‎packages/local-runtime-v2/src/service/session-system/query-collapse-state.ts‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,19 +217,30 @@ function readByCurrentTurn(db: AppDb, sessionId: string, currentTurnId: string)
217217
.get();
218218
}
219219

220+
const processingQueries = new WeakMap<AppDb, ReturnType<typeof prepareProcessingQuery>>();
221+
220222
function readProcessingByCurrentTurn(db: AppDb, sessionId: string, currentTurnId: string) {
223+
let query = processingQueries.get(db);
224+
if (!query) {
225+
query = prepareProcessingQuery(db);
226+
processingQueries.set(db, query);
227+
}
228+
return query.get({ sessionId, currentTurnId });
229+
}
230+
231+
function prepareProcessingQuery(db: AppDb) {
221232
return db
222233
.select()
223234
.from(queryCollapseViewStates)
224235
.where(
225236
and(
226-
eq(queryCollapseViewStates.sessionId, sessionId),
227-
eq(queryCollapseViewStates.currentTurnId, currentTurnId),
237+
eq(queryCollapseViewStates.sessionId, sql.placeholder('sessionId')),
238+
eq(queryCollapseViewStates.currentTurnId, sql.placeholder('currentTurnId')),
228239
isNull(queryCollapseViewStates.processingFinishedAtMs),
229240
),
230241
)
231242
.orderBy(desc(queryCollapseViewStates.updatedAtMs), desc(queryCollapseViewStates.queryKey))
232-
.get();
243+
.prepare();
233244
}
234245

235246
function readByKey(db: AppDb, sessionId: string, queryKey: string) {

‎packages/local-runtime-v2/src/service/turn-system/agent-host/history/durable-canonical-history-store.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,11 +146,12 @@ export class DurableCanonicalHistoryStore implements CanonicalHistoryStore {
146146
const detached = captureSemanticSnapshot(snapshot).value;
147147
validateCanonicalHistorySnapshot(detached);
148148
assertCanonicalIdentityVector(detached);
149-
return Object.freeze({
149+
return captureSemanticSnapshot({
150150
revision: detached.revision.trim(),
151-
messages: Object.freeze([...detached.messages]),
152-
identityVector: Object.freeze([...detached.identityVector]),
153-
});
151+
// Keep the separately owned arrays reusable at the next snapshot boundary.
152+
messages: captureSemanticSnapshot([...detached.messages]).value,
153+
identityVector: captureSemanticSnapshot([...detached.identityVector]).value,
154+
}).value;
154155
}
155156
}
156157

‎packages/local-runtime-v2/src/service/turn-system/agent-host/history/incremental-sha256.ts‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,42 @@ import { createHash } from 'node:crypto';
22

33
const UTF8_CHUNK_CODE_UNITS = 8 * 1_024;
44

5-
/** Native SHA-256 with bounded UTF-8 chunks and the existing update boundaries. */
5+
/** Native SHA-256 with bounded batching and independent UTF-8 update boundaries. */
66
export class IncrementalSha256 {
77
private readonly hash = createHash('sha256');
88
private finalized = false;
9+
private pending = '';
910

1011
update(value: string): void {
1112
if (this.finalized) throw new Error('SHA-256 digest is already finalized.');
1213
let offset = 0;
1314
while (offset < value.length) {
1415
const end = safeUtf8ChunkEnd(value, offset);
15-
this.hash.update(value.slice(offset, end), 'utf8');
16+
let chunk = value.slice(offset, end);
17+
// Each update encodes separately. A trailing high surrogate must not pair
18+
// with a low surrogate supplied by a later update when we batch strings.
19+
if (isHighSurrogate(chunk.charCodeAt(chunk.length - 1))) {
20+
chunk = chunk.slice(0, -1) + '\ufffd';
21+
}
22+
if (this.pending.length + chunk.length > UTF8_CHUNK_CODE_UNITS) this.flush();
23+
this.pending += chunk;
24+
if (this.pending.length === UTF8_CHUNK_CODE_UNITS) this.flush();
1625
offset = end;
1726
}
1827
}
1928

2029
digestHex(): string {
2130
if (this.finalized) throw new Error('SHA-256 digest is already finalized.');
31+
this.flush();
2232
this.finalized = true;
2333
return this.hash.digest('hex');
2434
}
35+
36+
private flush(): void {
37+
if (this.pending.length === 0) return;
38+
this.hash.update(this.pending, 'utf8');
39+
this.pending = '';
40+
}
2541
}
2642

2743
function safeUtf8ChunkEnd(value: string, offset: number): number {

‎packages/local-runtime-v2/src/service/turn-system/persistence/turn.repository.ts‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { randomUUID } from 'node:crypto';
22

3-
import { and, desc, eq, gt, inArray } from 'drizzle-orm';
3+
import { and, desc, eq, gt, inArray, sql } from 'drizzle-orm';
44

55
import {
66
sessionLocks,
@@ -491,11 +491,29 @@ async function completeTurnSessionDeletion(
491491
);
492492
}
493493

494+
const receiptQueries = new WeakMap<
495+
TurnRepositoryOptions['db'],
496+
ReturnType<typeof prepareReceiptQuery>
497+
>();
498+
499+
function prepareReceiptQuery(db: TurnRepositoryOptions['db']) {
500+
return db
501+
.select()
502+
.from(turnIngress)
503+
.where(eq(turnIngress.turnId, sql.placeholder('turnId')))
504+
.prepare();
505+
}
506+
494507
async function findTurnReceipt(
495508
options: TurnRepositoryOptions,
496509
turnId: string,
497510
): ReturnType<TurnRepository['findReceipt']> {
498-
const receipt = options.db.select().from(turnIngress).where(eq(turnIngress.turnId, turnId)).get();
511+
let query = receiptQueries.get(options.db);
512+
if (!query) {
513+
query = prepareReceiptQuery(options.db);
514+
receiptQueries.set(options.db, query);
515+
}
516+
const receipt = query.get({ turnId });
499517
if (!receipt) return undefined;
500518
if (
501519
!storedBusyReason(receipt.busyReason) ||

‎test/history-processing.test.ts‎

Lines changed: 215 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
11
import { createHash } from 'node:crypto';
2-
import { CanonicalHistoryJsonlDataSource } from '../packages/local-runtime-v2/src/infra/file/canonical-history-jsonl.js';
2+
import {
3+
canonicalActiveHistoryRevision,
4+
canonicalHistoryRevision,
5+
CanonicalHistoryJsonlDataSource,
6+
decodeCanonicalHistoryEnvelope,
7+
} from '../packages/local-runtime-v2/src/infra/file/canonical-history-jsonl.js';
8+
import { canonicalJson } from '../packages/local-runtime-v2/src/infra/file/canonical-history-json-value.js';
39
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
410
import { tmpdir } from 'node:os';
511
import { join } from 'node:path';
612
import { describe, expect, it, vi } from 'vitest';
13+
import { eq } from 'drizzle-orm';
14+
import { DatabaseClient } from '../packages/local-runtime-v2/src/infra/db/client.js';
15+
import { initializeDatabase } from '../packages/local-runtime-v2/src/infra/db/initialize.js';
16+
import { queryCollapseViewStates } from '../packages/local-runtime-v2/src/infra/db/schema/query-collapse.js';
17+
import { turnIngress } from '../packages/local-runtime-v2/src/infra/db/schema/turn.js';
18+
import { createQueryCollapseState } from '../packages/local-runtime-v2/src/service/session-system/query-collapse-state.js';
19+
import { createQueueTurnAdmissionPriorityFence } from '../packages/local-runtime-v2/src/service/session-system/index.js';
20+
import { createTurnRepository } from '../packages/local-runtime-v2/src/service/turn-system/persistence/turn.repository.js';
721
import { IncrementalSha256 } from '../packages/local-runtime-v2/src/service/turn-system/agent-host/history/incremental-sha256.js';
822
import { captureSemanticSnapshot } from '../packages/local-runtime-v2/src/service/turn-system/agent-host/history/semantic-identity.js';
923
import { DurableCanonicalHistoryStore } from '../packages/local-runtime-v2/src/service/turn-system/agent-host/history/durable-canonical-history-store.js';
@@ -18,6 +32,106 @@ import {
1832
} from '../packages/local-runtime-v2/src/service/session-system/messages/history/session-history-paths.js';
1933
import type { SessionRecord } from '../packages/local-runtime-v2/src/service/session-system/sessions/repo/contract.js';
2034

35+
describe('prepared runtime reads', () => {
36+
async function withDatabase(
37+
run: (client: DatabaseClient, writer: DatabaseClient) => Promise<void>,
38+
) {
39+
const dataDir = await mkdtemp(join(tmpdir(), 'mcode-prepared-reads-'));
40+
const client = new DatabaseClient({ dataDir });
41+
const writer = new DatabaseClient({ dataDir });
42+
try {
43+
await initializeDatabase({ database: client, dataDir });
44+
await run(client, writer);
45+
} finally {
46+
writer.close();
47+
client.close();
48+
await rm(dataDir, { recursive: true, force: true });
49+
}
50+
}
51+
52+
it('keeps processing reads fresh across sessions, completion and another connection', async () => {
53+
await withDatabase(async (client, writer) => {
54+
const state = createQueryCollapseState({ db: client.db, nowMs: () => 1 });
55+
expect(await state.findProcessingByCurrentTurn('s1', 't1')).toBeUndefined();
56+
await state.start({ sessionId: 's1', currentTurnId: 't1', queryKey: 'a' });
57+
await state.start({ sessionId: 's1', currentTurnId: 't1', queryKey: 'b' });
58+
await state.start({ sessionId: 's2', currentTurnId: 't1', queryKey: 'other' });
59+
expect((await state.findProcessingByCurrentTurn('s1', 't1'))?.queryKey).toBe('b');
60+
expect((await state.findProcessingByCurrentTurn('s2', 't1'))?.queryKey).toBe('other');
61+
expect(await state.findProcessingByCurrentTurn('s1', 'missing')).toBeUndefined();
62+
await state.finish({
63+
sessionId: 's1',
64+
currentTurnId: 't1',
65+
queryKey: 'b',
66+
forceExpanded: false,
67+
});
68+
expect((await state.findProcessingByCurrentTurn('s1', 't1'))?.queryKey).toBe('a');
69+
writer.db
70+
.update(queryCollapseViewStates)
71+
.set({ processingFinishedAtMs: 2 })
72+
.where(eq(queryCollapseViewStates.sessionId, 's1'))
73+
.run();
74+
expect(await state.findProcessingByCurrentTurn('s1', 't1')).toBeUndefined();
75+
expect((await state.findProcessingByCurrentTurn('s2', 't1'))?.queryKey).toBe('other');
76+
client.close();
77+
const reopened = createQueryCollapseState({ db: client.db });
78+
expect((await reopened.findProcessingByCurrentTurn('s2', 't1'))?.queryKey).toBe('other');
79+
});
80+
});
81+
82+
it('rereads receipts and validates corruption after a previous successful lookup', async () => {
83+
await withDatabase(async (client, writer) => {
84+
const repository = createTurnRepository({
85+
db: client.db,
86+
priorityFence: createQueueTurnAdmissionPriorityFence(),
87+
sessionAdmission: { rejectionInTransaction: () => undefined },
88+
});
89+
expect(await repository.findReceipt('turn-1')).toBeUndefined();
90+
for (let i = 1; i <= 2; i += 1) {
91+
writer.db
92+
.insert(turnIngress)
93+
.values({
94+
turnId: `turn-${i}`,
95+
sessionId: `s${i}`,
96+
busyReason: 'turn',
97+
inputJson: '{}',
98+
status: 'accepted',
99+
acceptedAtMs: 1,
100+
acceptedSequence: i,
101+
inputDigest: `digest-${i}`,
102+
})
103+
.run();
104+
}
105+
expect(await repository.findReceipt('turn-1')).toMatchObject({
106+
sessionId: 's1',
107+
acceptedSequence: 1,
108+
});
109+
expect(await repository.findReceipt('turn-2')).toMatchObject({
110+
sessionId: 's2',
111+
acceptedSequence: 2,
112+
});
113+
writer.db
114+
.update(turnIngress)
115+
.set({ inputDigest: '' })
116+
.where(eq(turnIngress.turnId, 'turn-1'))
117+
.run();
118+
await expect(repository.findReceipt('turn-1')).rejects.toThrow('malformed');
119+
writer.db
120+
.update(turnIngress)
121+
.set({ inputDigest: 'edited', acceptedSequence: 3 })
122+
.where(eq(turnIngress.turnId, 'turn-1'))
123+
.run();
124+
expect(await repository.findReceipt('turn-1')).toMatchObject({
125+
inputDigest: 'edited',
126+
acceptedSequence: 3,
127+
});
128+
writer.db.delete(turnIngress).where(eq(turnIngress.turnId, 'turn-1')).run();
129+
expect(await repository.findReceipt('turn-1')).toBeUndefined();
130+
expect(await repository.findReceipt("turn-2' OR 1=1 --")).toBeUndefined();
131+
});
132+
});
133+
});
134+
21135
describe('native incremental semantic hashing', () => {
22136
const inputs = ['', 'abc', '中文🙂', '\ud800', '\udc00', 'a'.repeat(8191) + '🙂tail'];
23137
for (const length of [55, 56, 63, 64, 65, 8191, 8192, 8193, 32769]) {
@@ -38,6 +152,63 @@ describe('native incremental semantic hashing', () => {
38152
createHash('sha256').update('\ud83d').update('\ude42').digest('hex'),
39153
);
40154
});
155+
it('matches native updates across repeated batches and split surrogate pairs', () => {
156+
const actual = new IncrementalSha256();
157+
const expected = createHash('sha256');
158+
const parts = ['key', ':', '', '\ud83d', '\ude42', '中文🙂', 'x'.repeat(8191), '🙂tail'];
159+
for (let index = 0; index < 257; index += 1) {
160+
for (const part of parts) {
161+
actual.update(part);
162+
expected.update(part, 'utf8');
163+
}
164+
}
165+
expect(actual.digestHex()).toBe(expected.digest('hex'));
166+
});
167+
});
168+
169+
describe('streamed canonical history revisions', () => {
170+
it.each([0, 1, 100])('preserves the canonical JSON digest for %i records', (length) => {
171+
const records = Array.from({ length }, (_, index) => ({
172+
message_id: `msg-${index}`,
173+
turn_id: `turn-${index}`,
174+
message: {
175+
role: 'user',
176+
timestamp: index,
177+
content: '中文🙂\ud800'.repeat(2048),
178+
metadata: { z: [null, true, -0], '10': 'ten', '2': 'two', a: { b: '"\\\n' } },
179+
},
180+
}));
181+
const expected = `sha256:${createHash('sha256')
182+
.update(canonicalJson(records.map(decodeCanonicalHistoryEnvelope)), 'utf8')
183+
.digest('hex')}`;
184+
expect(canonicalHistoryRevision(records)).toBe(expected);
185+
expect(canonicalActiveHistoryRevision(records)).toBe(expected);
186+
if (records.length > 0) {
187+
records[0]!.message.content = 'edited';
188+
expect(canonicalHistoryRevision(records)).not.toBe(expected);
189+
}
190+
});
191+
it('keeps active and settled sequence validation distinct', () => {
192+
const pending = [
193+
{
194+
message_id: 'msg-assistant',
195+
turn_id: 'turn-1',
196+
message: {
197+
role: 'assistant',
198+
timestamp: 1,
199+
content: [
200+
{ type: 'toolCall', id: 'call-1', name: 'bash', arguments: { command: 'pwd' } },
201+
],
202+
},
203+
},
204+
];
205+
const expected = `sha256:${createHash('sha256')
206+
.update(canonicalJson(pending.map(decodeCanonicalHistoryEnvelope)), 'utf8')
207+
.digest('hex')}`;
208+
expect(canonicalActiveHistoryRevision(pending)).toBe(expected);
209+
expect(() => canonicalHistoryRevision(pending)).toThrow('tool results');
210+
expect(() => canonicalActiveHistoryRevision([pending[0]!, pending[0]!])).toThrow();
211+
});
41212
});
42213

43214
describe('semantic snapshots', () => {
@@ -486,6 +657,49 @@ describe('committed history read reuse', () => {
486657
expect(readActive).not.toHaveBeenCalled();
487658
expect((await store.read('synthetic-session')).revision).toBe('r2');
488659
});
660+
it('preserves owned history through the store and the next commit snapshot', async () => {
661+
const original = {
662+
revision: ' r1 ',
663+
messages: [{ role: 'user', timestamp: 1, content: 'original'.repeat(4096) }],
664+
identityVector: ['msg-1'],
665+
};
666+
const store = new DurableCanonicalHistoryStore({
667+
read: async () => original,
668+
readActive: async () => original,
669+
append: async () => original,
670+
replace: async () => original,
671+
});
672+
const committed = await store.append(change);
673+
const expectedContent = original.messages[0]!.content;
674+
original.messages[0]!.content = 'changed';
675+
original.identityVector[0] = 'changed';
676+
expect(committed.revision).toBe('r1');
677+
expect(committed.messages).toEqual([{ role: 'user', timestamp: 1, content: expectedContent }]);
678+
expect(committed.identityVector).toEqual(['msg-1']);
679+
expect(Object.isFrozen(committed.messages[0])).toBe(true);
680+
expect(Object.isFrozen(committed.messages)).toBe(true);
681+
expect(Object.isFrozen(committed.identityVector)).toBe(true);
682+
const clone = vi.spyOn(globalThis, 'structuredClone');
683+
try {
684+
expect(captureSemanticSnapshot(committed).value).toBe(committed);
685+
expect(
686+
captureSemanticSnapshot({ committedMessages: committed.messages }).value.committedMessages,
687+
).toBe(committed.messages);
688+
expect(clone).not.toHaveBeenCalled();
689+
} finally {
690+
clone.mockRestore();
691+
}
692+
const sharedEmpty: never[] = [];
693+
const emptyStore = new DurableCanonicalHistoryStore({
694+
read: async () => ({ revision: 'r2', messages: sharedEmpty, identityVector: sharedEmpty }),
695+
readActive: async () => original,
696+
append: async () => original,
697+
replace: async () => original,
698+
});
699+
const empty = await emptyStore.read('synthetic-session');
700+
expect(empty.messages).not.toBe(empty.identityVector);
701+
expect(empty.messages).toEqual([]);
702+
});
489703
it('retains legacy rereads and rejects invalid commits or write failures', async () => {
490704
const readActive = vi.fn(async () => ({
491705
revision: 'r1',

0 commit comments

Comments
 (0)