Skip to content

Commit dcbd443

Browse files
perf(orchestration): per-thread shell deltas, visit throttling, event compaction (#4971)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 866ccb2 commit dcbd443

17 files changed

Lines changed: 875 additions & 188 deletions

‎apps/server/src/orchestration-v2/FoundationPersistence.test.ts‎

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,145 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {
265265
}),
266266
);
267267

268+
it.effect("compacts superseded state events, imported v1 events, and legacy receipts", () =>
269+
Effect.gen(function* () {
270+
const eventSink = yield* EventSinkV2;
271+
const maintenance = yield* ProjectionMaintenanceV2;
272+
const sql = yield* SqlClient.SqlClient;
273+
const now = yield* DateTime.now;
274+
const nowIso = DateTime.formatIso(now);
275+
const threadId = ThreadId.make("thread:foundation-compact");
276+
const thread = makeThread(threadId, now);
277+
const messageId = MessageId.make("message:foundation-compact");
278+
const threadStateEvent = (
279+
suffix: string,
280+
type: "thread.visited" | "thread.metadata-updated",
281+
): OrchestrationV2DomainEvent => ({
282+
id: EventId.make(`event:foundation-compact:${suffix}`),
283+
type,
284+
threadId,
285+
providerInstanceId,
286+
occurredAt: now,
287+
payload: { ...thread, lastVisitedAt: now },
288+
});
289+
const messageEvent = (suffix: string, text: string): OrchestrationV2DomainEvent => ({
290+
id: EventId.make(`event:foundation-compact:${suffix}`),
291+
type: "message.updated",
292+
threadId,
293+
occurredAt: now,
294+
payload: {
295+
createdBy: "user",
296+
creationSource: "web",
297+
id: messageId,
298+
threadId,
299+
runId: null,
300+
nodeId: null,
301+
role: "user",
302+
text,
303+
attachments: [],
304+
streaming: false,
305+
createdAt: now,
306+
updatedAt: now,
307+
},
308+
});
309+
310+
yield* eventSink.write({
311+
events: [
312+
threadCreatedEvent({ id: "event:foundation-compact:create", thread, now }),
313+
threadStateEvent("meta", "thread.metadata-updated"),
314+
threadStateEvent("visit-1", "thread.visited"),
315+
threadStateEvent("visit-2", "thread.visited"),
316+
messageEvent("message-1", "streaming"),
317+
messageEvent("message-2", "final"),
318+
],
319+
});
320+
321+
// A fully imported legacy thread: its v1 events and pre-migration
322+
// receipts are dead weight; a still-pending import keeps its rows.
323+
const importedV1ThreadId = "thread:foundation-compact-v1-imported";
324+
const pendingV1ThreadId = "thread:foundation-compact-v1-pending";
325+
const insertV1Event = (threadIdValue: string, version: number) => sql`
326+
INSERT INTO orchestration_events (
327+
event_id, aggregate_kind, stream_id, stream_version, event_type,
328+
occurred_at, actor_kind, payload_json, metadata_json, application_event_version
329+
)
330+
VALUES (
331+
${`event:v1:${threadIdValue}:${version}`}, 'thread', ${threadIdValue}, ${version},
332+
'thread.message-appended', ${nowIso}, 'user', '{}', '{}', 1
333+
)
334+
`;
335+
yield* insertV1Event(importedV1ThreadId, 1);
336+
yield* insertV1Event(importedV1ThreadId, 2);
337+
yield* insertV1Event(pendingV1ThreadId, 1);
338+
yield* sql`
339+
INSERT INTO orchestration_v2_legacy_imports (
340+
thread_id, source_updated_at, shell_imported_at, transcript_imported_at,
341+
imported_message_count, last_error
342+
)
343+
VALUES
344+
(${importedV1ThreadId}, ${nowIso}, ${nowIso}, ${nowIso}, 2, NULL),
345+
(${pendingV1ThreadId}, ${nowIso}, ${nowIso}, NULL, 0, NULL)
346+
ON CONFLICT(thread_id) DO NOTHING
347+
`;
348+
yield* sql`
349+
INSERT INTO orchestration_command_receipts (
350+
command_id, aggregate_kind, aggregate_id, accepted_at, result_sequence, status, command_type
351+
)
352+
VALUES
353+
('command:foundation-compact:legacy', 'thread', ${importedV1ThreadId}, ${nowIso}, 1, 'accepted', 'legacy'),
354+
('command:foundation-compact:pending', 'thread', ${pendingV1ThreadId}, ${nowIso}, 1, 'accepted', 'legacy')
355+
ON CONFLICT(command_id) DO NOTHING
356+
`;
357+
358+
const summary = yield* maintenance.compactEventStore;
359+
// meta + visit-1 (superseded thread state), message-1, and the two
360+
// imported v1 events.
361+
assert.isAtLeast(summary.deletedEventCount, 5);
362+
assert.isAtLeast(summary.deletedReceiptCount, 1);
363+
364+
const remaining = yield* sql<{ readonly event_id: string }>`
365+
SELECT event_id
366+
FROM orchestration_events
367+
WHERE aggregate_kind = 'thread'
368+
AND stream_id = ${threadId}
369+
ORDER BY sequence ASC
370+
`;
371+
assert.deepEqual(
372+
remaining.map((row) => row.event_id),
373+
[
374+
"event:foundation-compact:create",
375+
"event:foundation-compact:visit-2",
376+
"event:foundation-compact:message-2",
377+
],
378+
);
379+
380+
const remainingV1 = yield* sql<{ readonly stream_id: string }>`
381+
SELECT stream_id
382+
FROM orchestration_events
383+
WHERE application_event_version = 1
384+
AND stream_id IN (${importedV1ThreadId}, ${pendingV1ThreadId})
385+
`;
386+
assert.deepEqual(
387+
remainingV1.map((row) => row.stream_id),
388+
[pendingV1ThreadId],
389+
);
390+
391+
const remainingReceipts = yield* sql<{ readonly command_id: string }>`
392+
SELECT command_id
393+
FROM orchestration_command_receipts
394+
WHERE command_id IN ('command:foundation-compact:legacy', 'command:foundation-compact:pending')
395+
`;
396+
assert.deepEqual(
397+
remainingReceipts.map((row) => row.command_id),
398+
["command:foundation-compact:pending"],
399+
);
400+
401+
// Replay across the deletion gaps must still produce a valid projection.
402+
assert.isTrue((yield* maintenance.verify).valid);
403+
assert.isTrue((yield* maintenance.rebuild).valid);
404+
}),
405+
);
406+
268407
it.effect("verifies and rebuilds projections with cross-thread subagent relations", () =>
269408
Effect.gen(function* () {
270409
const eventSink = yield* EventSinkV2;

‎apps/server/src/orchestration-v2/LegacyV1ThreadImporter.ts‎

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,13 @@ const make = Effect.gen(function* () {
488488
),
489489
);
490490

491+
// Threads whose transcript import this process has already confirmed.
492+
// `transcript_imported_at` is never reset to NULL, so a positive answer
493+
// stays valid for the process lifetime; ensureTranscript runs on most
494+
// thread reads and command dispatches, so skipping the lock + lookup here
495+
// keeps that path off the database entirely after first confirmation.
496+
const confirmedTranscriptThreadIds = new Set<ThreadId>();
497+
491498
const ensureTranscriptBase = (threadId: ThreadId) =>
492499
transcriptImports.withLock(
493500
threadId,
@@ -500,6 +507,9 @@ const make = Effect.gen(function* () {
500507
`;
501508
const imported = imports[0];
502509
if (imported === undefined || imported.transcript_imported_at !== null) {
510+
if (imported !== undefined) {
511+
confirmedTranscriptThreadIds.add(threadId);
512+
}
503513
return { importedThreadCount: 0, importedMessageCount: 0 };
504514
}
505515
const messages = yield* listMessages(threadId);
@@ -546,6 +556,7 @@ const make = Effect.gen(function* () {
546556
last_error = NULL
547557
WHERE thread_id = ${threadId}
548558
`;
559+
confirmedTranscriptThreadIds.add(threadId);
549560
return {
550561
importedThreadCount: 1,
551562
importedMessageCount: missing.length,
@@ -554,16 +565,18 @@ const make = Effect.gen(function* () {
554565
);
555566

556567
const ensureTranscript = (threadId: ThreadId) =>
557-
ensureTranscriptBase(threadId).pipe(
558-
Effect.mapError(
559-
(cause) =>
560-
new LegacyV1ThreadImportError({
561-
operation: "hydrate transcript for",
562-
threadId,
563-
cause,
564-
}),
565-
),
566-
);
568+
confirmedTranscriptThreadIds.has(threadId)
569+
? Effect.succeed({ importedThreadCount: 0, importedMessageCount: 0 })
570+
: ensureTranscriptBase(threadId).pipe(
571+
Effect.mapError(
572+
(cause) =>
573+
new LegacyV1ThreadImportError({
574+
operation: "hydrate transcript for",
575+
threadId,
576+
cause,
577+
}),
578+
),
579+
);
567580

568581
const importPendingTranscripts = Effect.gen(function* () {
569582
const rows = yield* sql<LegacyImportRow>`

‎apps/server/src/orchestration-v2/Orchestrator.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
type OrchestrationV2ProviderTurn,
1616
type OrchestrationV2Run,
1717
type OrchestrationV2RunAttempt,
18+
type OrchestrationV2ThreadShell,
1819
type OrchestrationV2ThreadShellSnapshot,
1920
type OrchestrationV2StoredEvent,
2021
type OrchestrationV2Subagent,
@@ -160,6 +161,9 @@ export interface OrchestratorV2Shape {
160161
OrchestrationV2ThreadShellSnapshot,
161162
OrchestratorV2Error
162163
>;
164+
readonly getThreadShell: (
165+
threadId: ThreadId,
166+
) => Effect.Effect<OrchestrationV2ThreadShell | null, OrchestratorV2Error>;
163167
readonly getThreadEventSequence: (
164168
threadId: ThreadId,
165169
) => Effect.Effect<number, OrchestratorV2Error>;
@@ -5933,6 +5937,10 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
59335937
}),
59345938
),
59355939
),
5940+
getThreadShell: (threadId) =>
5941+
projectionStore
5942+
.getThreadShell(threadId)
5943+
.pipe(Effect.mapError((cause) => new OrchestratorProjectionError({ threadId, cause }))),
59365944
getThreadEventSequence: (threadId) =>
59375945
eventSink
59385946
.latestSequence({ threadId })
@@ -6022,6 +6030,13 @@ export const layerUnavailable: Layer.Layer<OrchestratorV2> = Layer.succeed(
60226030
cause: "Orchestration V2 live runtime is not configured.",
60236031
}),
60246032
),
6033+
getThreadShell: (threadId) =>
6034+
Effect.fail(
6035+
new OrchestratorProjectionError({
6036+
threadId,
6037+
cause: "Orchestration V2 live runtime is not configured.",
6038+
}),
6039+
),
60256040
getThreadEventSequence: (threadId) =>
60266041
Effect.fail(
60276042
new OrchestratorProjectionError({

0 commit comments

Comments
 (0)