@poncho-ai/harness 0.59.2 → 0.59.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +19 -0
- package/dist/index.d.ts +39 -118
- package/dist/index.js +30 -324
- package/package.json +1 -1
- package/src/index.ts +3 -11
- package/src/orchestrator/entries-dual-write.ts +20 -209
- package/src/orchestrator/index.ts +1 -6
- package/src/orchestrator/orchestrator.ts +22 -115
- package/src/orchestrator/run-conversation-turn.ts +0 -108
- package/src/state.ts +3 -1
- package/src/storage/entries.ts +47 -182
- package/src/storage/memory-engine.ts +2 -2
- package/src/storage/sql-dialect.ts +18 -8
- package/test/entries-dual-write.test.ts +21 -144
- package/test/entries-store.test.ts +43 -53
- package/test/entries.test.ts +37 -105
|
@@ -1,41 +1,29 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
|
-
//
|
|
2
|
+
// Subagent delivery-queue writers.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// `conversation_entries` is the append-only queue that carries a finished
|
|
5
|
+
// subagent's result to its parent conversation (see storage/entries.ts for
|
|
6
|
+
// the full rationale — short version: subagent results are the one
|
|
7
|
+
// conversation field with concurrent writers, so they're delivered by
|
|
8
|
+
// INSERT instead of blob read-modify-write). This module owns the write
|
|
9
|
+
// side: a safe append wrapper + the two entry builders.
|
|
9
10
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// rebuilds LLM context + display snapshot from the entry log and diffs
|
|
16
|
-
// them against the blob's `_harnessMessages` / `messages`. Logs mismatches
|
|
17
|
-
// under `[entries-parity]`. Never throws.
|
|
11
|
+
// (This file once held a full transcript dual-write + parity checker — the
|
|
12
|
+
// Phase 3 groundwork for replacing the conversation blob. That migration
|
|
13
|
+
// was deliberately abandoned after the 0.58.0 cutover incident; the unread
|
|
14
|
+
// entry types and their writers were deleted rather than maintained as
|
|
15
|
+
// drift-prone dead weight.)
|
|
18
16
|
// ---------------------------------------------------------------------------
|
|
19
17
|
|
|
20
18
|
import { randomUUID } from "node:crypto";
|
|
21
|
-
import { getTextContent, type Message } from "@poncho-ai/sdk";
|
|
22
19
|
import type { Logger } from "@poncho-ai/sdk";
|
|
23
20
|
import type { Conversation, ConversationStore, PendingSubagentResult } from "../state.js";
|
|
24
|
-
import {
|
|
25
|
-
buildDisplaySnapshot,
|
|
26
|
-
buildLlmContext,
|
|
27
|
-
type ConversationEntry,
|
|
28
|
-
type NewConversationEntry,
|
|
29
|
-
} from "../storage/entries.js";
|
|
30
|
-
|
|
31
|
-
/** True when dual-write parity verification is opted in via env. */
|
|
32
|
-
export const entriesParityEnabled = (): boolean =>
|
|
33
|
-
process.env.PONCHO_VERIFY_ENTRIES === "1";
|
|
21
|
+
import type { ConversationEntry, NewConversationEntry } from "../storage/entries.js";
|
|
34
22
|
|
|
35
23
|
// DISTRIBUTIVE omit (same reasoning as NewConversationEntry in entries.ts): a
|
|
36
24
|
// plain Omit<NewConversationEntry, "id"> over a union collapses to the keys
|
|
37
|
-
// common to every member, dropping `
|
|
38
|
-
// union so each member keeps its own discriminant fields.
|
|
25
|
+
// common to every member, dropping `result`/`consumedSeqs`. Distribute over
|
|
26
|
+
// the union so each member keeps its own discriminant fields.
|
|
39
27
|
type NewEntryNoId = NewConversationEntry extends infer T
|
|
40
28
|
? T extends NewConversationEntry
|
|
41
29
|
? Omit<T, "id">
|
|
@@ -43,15 +31,13 @@ type NewEntryNoId = NewConversationEntry extends infer T
|
|
|
43
31
|
: never;
|
|
44
32
|
|
|
45
33
|
/**
|
|
46
|
-
* Append entries to the conversation's
|
|
47
|
-
* blob write. Best-effort and non-blocking by contract:
|
|
34
|
+
* Append entries to the conversation's queue. Best-effort by contract:
|
|
48
35
|
* - stamps a fresh uuid `id` on each entry (required input column),
|
|
49
36
|
* - never throws (logs and returns [] on failure),
|
|
50
|
-
* -
|
|
37
|
+
* - safe to `void` when the caller doesn't need the stored rows.
|
|
51
38
|
*
|
|
52
|
-
* Returns the stored entries (with seq/createdAt)
|
|
53
|
-
*
|
|
54
|
-
* empty input / failure.
|
|
39
|
+
* Returns the stored entries (with seq/createdAt) — the callback path needs
|
|
40
|
+
* the seqs to record consumption.
|
|
55
41
|
*/
|
|
56
42
|
export const appendEntriesSafe = async (
|
|
57
43
|
store: ConversationStore,
|
|
@@ -72,7 +58,7 @@ export const appendEntriesSafe = async (
|
|
|
72
58
|
);
|
|
73
59
|
} catch (err) {
|
|
74
60
|
log.error(
|
|
75
|
-
`[entries-
|
|
61
|
+
`[entries-queue] append failed for ${conversation.conversationId}: ${
|
|
76
62
|
err instanceof Error ? err.message : String(err)
|
|
77
63
|
}`,
|
|
78
64
|
);
|
|
@@ -80,48 +66,6 @@ export const appendEntriesSafe = async (
|
|
|
80
66
|
}
|
|
81
67
|
};
|
|
82
68
|
|
|
83
|
-
// --- entry builders (pure; centralize the best-effort derivation) ----------
|
|
84
|
-
|
|
85
|
-
export const userMessageEntry = (
|
|
86
|
-
message: Message,
|
|
87
|
-
turnId: string,
|
|
88
|
-
opts?: { hidden?: boolean },
|
|
89
|
-
): NewEntryNoId => ({
|
|
90
|
-
type: "user_message",
|
|
91
|
-
message,
|
|
92
|
-
turnId,
|
|
93
|
-
...(opts?.hidden ? { hidden: true } : {}),
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
export const assistantMessageEntry = (
|
|
97
|
-
message: Message,
|
|
98
|
-
turnId: string,
|
|
99
|
-
runId: string,
|
|
100
|
-
): NewEntryNoId => ({
|
|
101
|
-
type: "assistant_message",
|
|
102
|
-
message,
|
|
103
|
-
turnId,
|
|
104
|
-
runId,
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
export const harnessMessageEntries = (
|
|
108
|
-
messages: Message[],
|
|
109
|
-
turnId: string,
|
|
110
|
-
): NewEntryNoId[] =>
|
|
111
|
-
messages.map((message) => ({ type: "harness_message", message, turnId }));
|
|
112
|
-
|
|
113
|
-
export const compactionEntry = (
|
|
114
|
-
summaryMessage: Message,
|
|
115
|
-
firstKeptSeq: number,
|
|
116
|
-
opts?: { tokensBefore?: number; tokensAfter?: number },
|
|
117
|
-
): NewEntryNoId => ({
|
|
118
|
-
type: "compaction",
|
|
119
|
-
summaryMessage,
|
|
120
|
-
firstKeptSeq,
|
|
121
|
-
...(opts?.tokensBefore !== undefined ? { tokensBefore: opts.tokensBefore } : {}),
|
|
122
|
-
...(opts?.tokensAfter !== undefined ? { tokensAfter: opts.tokensAfter } : {}),
|
|
123
|
-
});
|
|
124
|
-
|
|
125
69
|
export const subagentResultEntry = (
|
|
126
70
|
result: PendingSubagentResult,
|
|
127
71
|
): NewEntryNoId => ({ type: "subagent_result", result });
|
|
@@ -130,136 +74,3 @@ export const callbackStartedEntry = (consumedSeqs: number[]): NewEntryNoId => ({
|
|
|
130
74
|
type: "callback_started",
|
|
131
75
|
consumedSeqs,
|
|
132
76
|
});
|
|
133
|
-
|
|
134
|
-
export const assistantAmendmentEntry = (
|
|
135
|
-
targetEntryId: string,
|
|
136
|
-
appendText: string,
|
|
137
|
-
): NewEntryNoId => ({
|
|
138
|
-
type: "assistant_amendment",
|
|
139
|
-
targetEntryId,
|
|
140
|
-
...(appendText ? { appendText } : {}),
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
// --- "new harness messages this turn" diff ---------------------------------
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* The harness messages added during the just-finished turn — i.e. the suffix
|
|
147
|
-
* of the new `_harnessMessages` array beyond what was there before the turn.
|
|
148
|
-
*
|
|
149
|
-
* BEST-EFFORT: the blob replaces `_harnessMessages` wholesale (it's not an
|
|
150
|
-
* append log), so we recover "what's new" by length-diffing prev vs next.
|
|
151
|
-
* When a compaction collapsed history this turn, `next` can be SHORTER than
|
|
152
|
-
* `prev`; in that case there's no clean suffix and we return the whole `next`
|
|
153
|
-
* so the entry log still ends up with the model-visible context (parity will
|
|
154
|
-
* flag the over-count for review). The compaction entry (appended separately)
|
|
155
|
-
* is what makes rebuild correct in that case.
|
|
156
|
-
*/
|
|
157
|
-
export const newHarnessMessagesThisTurn = (
|
|
158
|
-
prev: Message[] | undefined,
|
|
159
|
-
next: Message[] | undefined,
|
|
160
|
-
): { messages: Message[]; approximate: boolean } => {
|
|
161
|
-
const prevArr = prev ?? [];
|
|
162
|
-
const nextArr = next ?? [];
|
|
163
|
-
if (nextArr.length === 0) return { messages: [], approximate: false };
|
|
164
|
-
if (prevArr.length === 0) return { messages: nextArr, approximate: false };
|
|
165
|
-
if (nextArr.length >= prevArr.length) {
|
|
166
|
-
// Assume the new array is prev + appended suffix (the common case).
|
|
167
|
-
return { messages: nextArr.slice(prevArr.length), approximate: false };
|
|
168
|
-
}
|
|
169
|
-
// next shorter than prev — compaction or a rebuild reshaped the array.
|
|
170
|
-
return { messages: nextArr, approximate: true };
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
// --- parity checker ---------------------------------------------------------
|
|
174
|
-
|
|
175
|
-
/** Normalized text projection for length-insensitive content comparison. */
|
|
176
|
-
const projectText = (m: Message): string => {
|
|
177
|
-
const role = m.role;
|
|
178
|
-
const text = getTextContent(m).replace(/\s+/g, " ").trim();
|
|
179
|
-
return `${role}:${text}`;
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
const projectAll = (msgs: Message[]): string[] => msgs.map(projectText);
|
|
183
|
-
|
|
184
|
-
const countMismatch = (label: string, a: number, b: number): string | null =>
|
|
185
|
-
a === b ? null : `${label} length ${a} (entries) vs ${b} (blob)`;
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Rebuild LLM context + display snapshot from the entry log and diff against
|
|
189
|
-
* the blob. Logs under `[entries-parity]` with the conversationId. Never
|
|
190
|
-
* throws. No-op unless PONCHO_VERIFY_ENTRIES === "1".
|
|
191
|
-
*/
|
|
192
|
-
export const verifyEntriesParity = async (
|
|
193
|
-
store: ConversationStore,
|
|
194
|
-
conversationId: string,
|
|
195
|
-
blob: { harnessMessages?: Message[]; displayMessages?: Message[] },
|
|
196
|
-
log: Logger,
|
|
197
|
-
): Promise<void> => {
|
|
198
|
-
if (!entriesParityEnabled()) return;
|
|
199
|
-
try {
|
|
200
|
-
const entries = await store.readEntries(conversationId);
|
|
201
|
-
const mismatches: string[] = [];
|
|
202
|
-
|
|
203
|
-
if (blob.harnessMessages) {
|
|
204
|
-
const llm = buildLlmContext(entries);
|
|
205
|
-
const lenMismatch = countMismatch(
|
|
206
|
-
"llmContext",
|
|
207
|
-
llm.length,
|
|
208
|
-
blob.harnessMessages.length,
|
|
209
|
-
);
|
|
210
|
-
if (lenMismatch) mismatches.push(lenMismatch);
|
|
211
|
-
// Compare a trailing normalized text projection. We don't require
|
|
212
|
-
// byte-equality — metadata, tool-call framing, and exact whitespace
|
|
213
|
-
// differ by construction between the two representations.
|
|
214
|
-
const entriesProj = projectAll(llm);
|
|
215
|
-
const blobProj = projectAll(blob.harnessMessages);
|
|
216
|
-
const tail = Math.min(entriesProj.length, blobProj.length, 5);
|
|
217
|
-
for (let i = 1; i <= tail; i++) {
|
|
218
|
-
const ep = entriesProj[entriesProj.length - i];
|
|
219
|
-
const bp = blobProj[blobProj.length - i];
|
|
220
|
-
if (ep !== bp) {
|
|
221
|
-
mismatches.push(
|
|
222
|
-
`llmContext tail[-${i}] differs: entries=${JSON.stringify(ep).slice(0, 120)} blob=${JSON.stringify(bp).slice(0, 120)}`,
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (blob.displayMessages) {
|
|
229
|
-
// tailN large enough to cover the whole transcript for the diff.
|
|
230
|
-
const snap = buildDisplaySnapshot(entries, Number.MAX_SAFE_INTEGER);
|
|
231
|
-
const lenMismatch = countMismatch(
|
|
232
|
-
"display",
|
|
233
|
-
snap.totalMessages,
|
|
234
|
-
blob.displayMessages.length,
|
|
235
|
-
);
|
|
236
|
-
if (lenMismatch) mismatches.push(lenMismatch);
|
|
237
|
-
const entriesProj = projectAll(snap.messages);
|
|
238
|
-
const blobProj = projectAll(blob.displayMessages);
|
|
239
|
-
const tail = Math.min(entriesProj.length, blobProj.length, 5);
|
|
240
|
-
for (let i = 1; i <= tail; i++) {
|
|
241
|
-
const ep = entriesProj[entriesProj.length - i];
|
|
242
|
-
const bp = blobProj[blobProj.length - i];
|
|
243
|
-
if (ep !== bp) {
|
|
244
|
-
mismatches.push(
|
|
245
|
-
`display tail[-${i}] differs: entries=${JSON.stringify(ep).slice(0, 120)} blob=${JSON.stringify(bp).slice(0, 120)}`,
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (mismatches.length > 0) {
|
|
252
|
-
log.warn(
|
|
253
|
-
`[entries-parity] ${conversationId} MISMATCH (${mismatches.length}): ${mismatches.join(" | ")}`,
|
|
254
|
-
);
|
|
255
|
-
} else {
|
|
256
|
-
log.info(`[entries-parity] ${conversationId} OK`);
|
|
257
|
-
}
|
|
258
|
-
} catch (err) {
|
|
259
|
-
log.error(
|
|
260
|
-
`[entries-parity] ${conversationId} checker threw (ignored): ${
|
|
261
|
-
err instanceof Error ? err.message : String(err)
|
|
262
|
-
}`,
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
};
|
|
@@ -62,9 +62,4 @@ export {
|
|
|
62
62
|
type RunConversationTurnResult,
|
|
63
63
|
} from "./run-conversation-turn.js";
|
|
64
64
|
|
|
65
|
-
export {
|
|
66
|
-
appendEntriesSafe,
|
|
67
|
-
verifyEntriesParity,
|
|
68
|
-
entriesParityEnabled,
|
|
69
|
-
newHarnessMessagesThisTurn,
|
|
70
|
-
} from "./entries-dual-write.js";
|
|
65
|
+
export { appendEntriesSafe } from "./entries-dual-write.js";
|
|
@@ -29,15 +29,11 @@ import {
|
|
|
29
29
|
} from "./subagents.js";
|
|
30
30
|
import {
|
|
31
31
|
appendEntriesSafe,
|
|
32
|
-
assistantAmendmentEntry,
|
|
33
|
-
assistantMessageEntry,
|
|
34
32
|
callbackStartedEntry,
|
|
35
33
|
subagentResultEntry,
|
|
36
|
-
userMessageEntry,
|
|
37
|
-
verifyEntriesParity,
|
|
38
34
|
} from "./entries-dual-write.js";
|
|
39
35
|
|
|
40
|
-
const
|
|
36
|
+
const entriesQueueLog = createLogger("orchestrator:entries");
|
|
41
37
|
|
|
42
38
|
// ── Subagent result extraction ──
|
|
43
39
|
|
|
@@ -502,11 +498,6 @@ export class AgentOrchestrator {
|
|
|
502
498
|
if (!checkpointedRun) {
|
|
503
499
|
const conv = await this.conversationStore.get(conversationId);
|
|
504
500
|
if (conv) {
|
|
505
|
-
// Track which dual-write branch the blob took: an in-place merge onto
|
|
506
|
-
// the previous assistant bubble (→ assistant_amendment) or a fresh
|
|
507
|
-
// bubble (→ assistant_message).
|
|
508
|
-
let amendmentText: string | undefined;
|
|
509
|
-
let pushedAssistant: Message | undefined;
|
|
510
501
|
const hasAssistantContent =
|
|
511
502
|
draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draft.sections.length > 0;
|
|
512
503
|
if (hasAssistantContent) {
|
|
@@ -535,14 +526,15 @@ export class AgentOrchestrator {
|
|
|
535
526
|
} as Message["metadata"],
|
|
536
527
|
},
|
|
537
528
|
];
|
|
538
|
-
amendmentText = draft.assistantResponse;
|
|
539
529
|
} else {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
530
|
+
conv.messages = [
|
|
531
|
+
...prevMessages,
|
|
532
|
+
{
|
|
533
|
+
role: "assistant" as const,
|
|
534
|
+
content: draft.assistantResponse,
|
|
535
|
+
metadata: buildAssistantMetadata(draft),
|
|
536
|
+
},
|
|
537
|
+
];
|
|
546
538
|
}
|
|
547
539
|
}
|
|
548
540
|
applyTurnMetadata(conv, {
|
|
@@ -552,62 +544,6 @@ export class AgentOrchestrator {
|
|
|
552
544
|
harnessMessages: execution?.runHarnessMessages,
|
|
553
545
|
}, { shouldRebuildCanonical: true });
|
|
554
546
|
await this.conversationStore.update(conv);
|
|
555
|
-
|
|
556
|
-
// DUAL-WRITE (mirrors the resume merge/push above). In-place merge →
|
|
557
|
-
// assistant_amendment targeting the latest assistant_message entry
|
|
558
|
-
// (BEST-EFFORT: the blob carries no entry id, so we resolve the target
|
|
559
|
-
// by reading the log's last assistant_message). Fresh bubble →
|
|
560
|
-
// assistant_message. Then parity-check. Fire-and-forget.
|
|
561
|
-
if (amendmentText !== undefined || pushedAssistant) {
|
|
562
|
-
const finalConv = conv;
|
|
563
|
-
const amendText = amendmentText;
|
|
564
|
-
const pushed = pushedAssistant;
|
|
565
|
-
void (async () => {
|
|
566
|
-
try {
|
|
567
|
-
if (pushed) {
|
|
568
|
-
await appendEntriesSafe(
|
|
569
|
-
this.conversationStore,
|
|
570
|
-
finalConv,
|
|
571
|
-
[assistantMessageEntry(pushed, `resume-${conversationId}`, latestRunId)],
|
|
572
|
-
dualWriteLog,
|
|
573
|
-
);
|
|
574
|
-
} else if (amendText !== undefined) {
|
|
575
|
-
const existing = await this.conversationStore.readEntries(
|
|
576
|
-
conversationId,
|
|
577
|
-
{ types: ["assistant_message"] },
|
|
578
|
-
);
|
|
579
|
-
const target = existing[existing.length - 1];
|
|
580
|
-
if (target) {
|
|
581
|
-
await appendEntriesSafe(
|
|
582
|
-
this.conversationStore,
|
|
583
|
-
finalConv,
|
|
584
|
-
[assistantAmendmentEntry(target.id, amendText)],
|
|
585
|
-
dualWriteLog,
|
|
586
|
-
);
|
|
587
|
-
} else {
|
|
588
|
-
dualWriteLog.warn(
|
|
589
|
-
`[entries-dual-write] resume amendment for ${conversationId}: no assistant_message entry to target; skipped`,
|
|
590
|
-
);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
await verifyEntriesParity(
|
|
594
|
-
this.conversationStore,
|
|
595
|
-
conversationId,
|
|
596
|
-
{
|
|
597
|
-
harnessMessages: finalConv._harnessMessages,
|
|
598
|
-
displayMessages: finalConv.messages,
|
|
599
|
-
},
|
|
600
|
-
dualWriteLog,
|
|
601
|
-
);
|
|
602
|
-
} catch (err) {
|
|
603
|
-
dualWriteLog.error(
|
|
604
|
-
`[entries-dual-write] resume finalize append failed for ${conversationId}: ${
|
|
605
|
-
err instanceof Error ? err.message : String(err)
|
|
606
|
-
}`,
|
|
607
|
-
);
|
|
608
|
-
}
|
|
609
|
-
})();
|
|
610
|
-
}
|
|
611
547
|
}
|
|
612
548
|
} else {
|
|
613
549
|
const conv = await this.conversationStore.get(conversationId);
|
|
@@ -1263,13 +1199,13 @@ export class AgentOrchestrator {
|
|
|
1263
1199
|
conversation.updatedAt = Date.now();
|
|
1264
1200
|
await this.conversationStore.update(conversation);
|
|
1265
1201
|
|
|
1266
|
-
//
|
|
1267
|
-
//
|
|
1268
|
-
//
|
|
1269
|
-
//
|
|
1270
|
-
//
|
|
1202
|
+
// QUEUE CONSUMPTION: append a callback_started entry listing the consumed
|
|
1203
|
+
// subagent_result seqs (resolved by matching subagentId against the queue
|
|
1204
|
+
// — the blob's pending array carries no seq), so the consumed results stop
|
|
1205
|
+
// being "pending" on the entry-sourced read path. Mirrors the blob's
|
|
1206
|
+
// consume-pending write above (kept fresh for the PONCHO_READ_ENTRIES=0
|
|
1207
|
+
// kill-switch).
|
|
1271
1208
|
if (pendingResults.length > 0) {
|
|
1272
|
-
const turnId = `callback-${callbackCount}-${conversation.conversationId}`;
|
|
1273
1209
|
void (async () => {
|
|
1274
1210
|
try {
|
|
1275
1211
|
const resultEntries = await this.conversationStore.readEntries(
|
|
@@ -1287,17 +1223,12 @@ export class AgentOrchestrator {
|
|
|
1287
1223
|
await appendEntriesSafe(
|
|
1288
1224
|
this.conversationStore,
|
|
1289
1225
|
conversation,
|
|
1290
|
-
[
|
|
1291
|
-
|
|
1292
|
-
...injectedCallbackMessages.map((m) =>
|
|
1293
|
-
userMessageEntry(m, turnId, { hidden: true }),
|
|
1294
|
-
),
|
|
1295
|
-
],
|
|
1296
|
-
dualWriteLog,
|
|
1226
|
+
[callbackStartedEntry(consumedSeqs)],
|
|
1227
|
+
entriesQueueLog,
|
|
1297
1228
|
);
|
|
1298
1229
|
} catch (err) {
|
|
1299
|
-
|
|
1300
|
-
`[entries-
|
|
1230
|
+
entriesQueueLog.error(
|
|
1231
|
+
`[entries-queue] callback_started append failed for ${conversation.conversationId}: ${
|
|
1301
1232
|
err instanceof Error ? err.message : String(err)
|
|
1302
1233
|
}`,
|
|
1303
1234
|
);
|
|
@@ -1395,30 +1326,6 @@ export class AgentOrchestrator {
|
|
|
1395
1326
|
freshConv.runningCallbackSince = undefined;
|
|
1396
1327
|
await this.conversationStore.update(freshConv);
|
|
1397
1328
|
|
|
1398
|
-
// DUAL-WRITE (mirrors the assistant push above): the callback re-run's
|
|
1399
|
-
// final assistant bubble. Only when not continuing (a continuation has
|
|
1400
|
-
// no final bubble yet). Then run the parity check on the rebuilt
|
|
1401
|
-
// transcript. Fire-and-forget; never blocks.
|
|
1402
|
-
if (callbackAssistantMsg) {
|
|
1403
|
-
const finalMsg = callbackAssistantMsg;
|
|
1404
|
-
void appendEntriesSafe(
|
|
1405
|
-
this.conversationStore,
|
|
1406
|
-
freshConv,
|
|
1407
|
-
[assistantMessageEntry(finalMsg, `callback-${conversationId}`, execution.latestRunId)],
|
|
1408
|
-
dualWriteLog,
|
|
1409
|
-
).then(() =>
|
|
1410
|
-
verifyEntriesParity(
|
|
1411
|
-
this.conversationStore,
|
|
1412
|
-
conversationId,
|
|
1413
|
-
{
|
|
1414
|
-
harnessMessages: freshConv._harnessMessages,
|
|
1415
|
-
displayMessages: freshConv.messages,
|
|
1416
|
-
},
|
|
1417
|
-
dualWriteLog,
|
|
1418
|
-
),
|
|
1419
|
-
);
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
1329
|
// Proactive messaging notification
|
|
1423
1330
|
if (freshConv.channelMeta && execution.draft.assistantResponse.length > 0) {
|
|
1424
1331
|
this.hooks?.onMessagingNotify?.(conversationId, execution.draft.assistantResponse);
|
|
@@ -1887,11 +1794,11 @@ export class AgentOrchestrator {
|
|
|
1887
1794
|
this.conversationStore,
|
|
1888
1795
|
parent,
|
|
1889
1796
|
[subagentResultEntry(result)],
|
|
1890
|
-
|
|
1797
|
+
entriesQueueLog,
|
|
1891
1798
|
);
|
|
1892
1799
|
} catch (err) {
|
|
1893
|
-
|
|
1894
|
-
`[entries-
|
|
1800
|
+
entriesQueueLog.error(
|
|
1801
|
+
`[entries-queue] subagent_result append failed for ${parentConversationId}: ${
|
|
1895
1802
|
err instanceof Error ? err.message : String(err)
|
|
1896
1803
|
}`,
|
|
1897
1804
|
);
|
|
@@ -36,15 +36,6 @@ import {
|
|
|
36
36
|
executeConversationTurn,
|
|
37
37
|
flushTurnDraft,
|
|
38
38
|
} from "./turn.js";
|
|
39
|
-
import {
|
|
40
|
-
appendEntriesSafe,
|
|
41
|
-
assistantMessageEntry,
|
|
42
|
-
compactionEntry,
|
|
43
|
-
harnessMessageEntries,
|
|
44
|
-
newHarnessMessagesThisTurn,
|
|
45
|
-
userMessageEntry,
|
|
46
|
-
verifyEntriesParity,
|
|
47
|
-
} from "./entries-dual-write.js";
|
|
48
39
|
|
|
49
40
|
const log = createLogger("orchestrator");
|
|
50
41
|
|
|
@@ -207,14 +198,6 @@ export const runConversationTurn = async (
|
|
|
207
198
|
await opts.conversationStore.update(conversation);
|
|
208
199
|
};
|
|
209
200
|
|
|
210
|
-
// Snapshot the harness-message array as it stood BEFORE this turn so the
|
|
211
|
-
// finalize path can diff out the messages this turn appended (dual-write).
|
|
212
|
-
const preTurnHarnessMessages = conversation._harnessMessages
|
|
213
|
-
? [...conversation._harnessMessages]
|
|
214
|
-
: undefined;
|
|
215
|
-
// The stable per-turn id used to group dual-write entries.
|
|
216
|
-
const turnId = assistantId;
|
|
217
|
-
|
|
218
201
|
// Persist the user turn immediately so a crash mid-run still records what
|
|
219
202
|
// the user said. Fire-and-forget — don't block the run.
|
|
220
203
|
conversation.messages = [...historyMessages, userMessage];
|
|
@@ -227,15 +210,6 @@ export const runConversationTurn = async (
|
|
|
227
210
|
);
|
|
228
211
|
});
|
|
229
212
|
|
|
230
|
-
// DUAL-WRITE (additive, mirrors the user-turn blob write above): append a
|
|
231
|
-
// user_message entry. Fire-and-forget — never blocks or breaks the turn.
|
|
232
|
-
void appendEntriesSafe(
|
|
233
|
-
opts.conversationStore,
|
|
234
|
-
conversation,
|
|
235
|
-
[userMessageEntry(userMessage, turnId)],
|
|
236
|
-
log,
|
|
237
|
-
);
|
|
238
|
-
|
|
239
213
|
try {
|
|
240
214
|
const execution = await executeConversationTurn({
|
|
241
215
|
harness: opts.harness,
|
|
@@ -286,48 +260,6 @@ export const runConversationTurn = async (
|
|
|
286
260
|
...existingHistory,
|
|
287
261
|
...preRunMessages.slice(0, removedCount),
|
|
288
262
|
];
|
|
289
|
-
|
|
290
|
-
// DUAL-WRITE (mirrors the compactedHistory blob write above): the
|
|
291
|
-
// compacted array is [summaryMessage, ...keptMessages]. BEST-EFFORT
|
|
292
|
-
// firstKeptSeq: the entry-log seqs of the kept harness messages
|
|
293
|
-
// aren't known here, so we derive a sentinel from the kept-count by
|
|
294
|
-
// reading the current max harness_message seq and pointing at the
|
|
295
|
-
// tail. We read the existing entries to compute it.
|
|
296
|
-
const summaryMessage = event.compactedMessages[0];
|
|
297
|
-
const keptCount = Math.max(0, event.compactedMessages.length - 1);
|
|
298
|
-
if (summaryMessage) {
|
|
299
|
-
void (async () => {
|
|
300
|
-
try {
|
|
301
|
-
const existing = await opts.conversationStore.readEntries(
|
|
302
|
-
opts.conversationId,
|
|
303
|
-
{ types: ["harness_message"] },
|
|
304
|
-
);
|
|
305
|
-
// firstKeptSeq = seq of the (keptCount)-th-from-last existing
|
|
306
|
-
// harness message, so rebuild keeps exactly that many.
|
|
307
|
-
const harnessSeqs = existing.map((e) => e.seq);
|
|
308
|
-
const firstKeptSeq =
|
|
309
|
-
harnessSeqs.length >= keptCount && keptCount > 0
|
|
310
|
-
? harnessSeqs[harnessSeqs.length - keptCount]!
|
|
311
|
-
: (harnessSeqs[harnessSeqs.length - 1] ?? 0) + 1;
|
|
312
|
-
await appendEntriesSafe(
|
|
313
|
-
opts.conversationStore,
|
|
314
|
-
conversation,
|
|
315
|
-
[
|
|
316
|
-
compactionEntry(summaryMessage, firstKeptSeq, {
|
|
317
|
-
tokensBefore: conversation.contextTokens,
|
|
318
|
-
}),
|
|
319
|
-
],
|
|
320
|
-
log,
|
|
321
|
-
);
|
|
322
|
-
} catch (err) {
|
|
323
|
-
log.error(
|
|
324
|
-
`[entries-dual-write] compaction append failed: ${
|
|
325
|
-
err instanceof Error ? err.message : String(err)
|
|
326
|
-
}`,
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
})();
|
|
330
|
-
}
|
|
331
263
|
}
|
|
332
264
|
}
|
|
333
265
|
if (event.type === "step:completed") {
|
|
@@ -468,46 +400,6 @@ export const runConversationTurn = async (
|
|
|
468
400
|
{ shouldRebuildCanonical },
|
|
469
401
|
);
|
|
470
402
|
await opts.conversationStore.update(conversation);
|
|
471
|
-
|
|
472
|
-
// DUAL-WRITE at finalize (mirrors applyTurnMetadata's _harnessMessages
|
|
473
|
-
// write + the final assistant bubble in conversation.messages):
|
|
474
|
-
// 1. harness_message entries for the messages this turn appended,
|
|
475
|
-
// 2. the final assistant_message entry.
|
|
476
|
-
// Best-effort + fire-and-forget; never blocks the return.
|
|
477
|
-
const finalAssistant =
|
|
478
|
-
conversation.messages[conversation.messages.length - 1];
|
|
479
|
-
const { messages: newHarness, approximate } = newHarnessMessagesThisTurn(
|
|
480
|
-
preTurnHarnessMessages,
|
|
481
|
-
conversation._harnessMessages,
|
|
482
|
-
);
|
|
483
|
-
if (approximate) {
|
|
484
|
-
log.warn(
|
|
485
|
-
`[entries-dual-write] ${opts.conversationId} harness-message diff approximate ` +
|
|
486
|
-
`(blob array shrank this turn — likely compaction); appended full context`,
|
|
487
|
-
);
|
|
488
|
-
}
|
|
489
|
-
const finalizeEntries = [
|
|
490
|
-
...harnessMessageEntries(newHarness, turnId),
|
|
491
|
-
...(finalAssistant && finalAssistant.role === "assistant"
|
|
492
|
-
? [assistantMessageEntry(finalAssistant, turnId, latestRunId)]
|
|
493
|
-
: []),
|
|
494
|
-
];
|
|
495
|
-
void appendEntriesSafe(
|
|
496
|
-
opts.conversationStore,
|
|
497
|
-
conversation,
|
|
498
|
-
finalizeEntries,
|
|
499
|
-
log,
|
|
500
|
-
).then(() =>
|
|
501
|
-
verifyEntriesParity(
|
|
502
|
-
opts.conversationStore,
|
|
503
|
-
opts.conversationId,
|
|
504
|
-
{
|
|
505
|
-
harnessMessages: conversation._harnessMessages,
|
|
506
|
-
displayMessages: conversation.messages,
|
|
507
|
-
},
|
|
508
|
-
log,
|
|
509
|
-
),
|
|
510
|
-
);
|
|
511
403
|
}
|
|
512
404
|
|
|
513
405
|
return {
|
package/src/state.ts
CHANGED
|
@@ -278,7 +278,9 @@ export class InMemoryConversationStore implements ConversationStore {
|
|
|
278
278
|
// Phase 3c read cutover: rebuild reader-facing fields from the entry log
|
|
279
279
|
// (blob fallback for un-migrated conversations). Clone first — the map
|
|
280
280
|
// holds a live mutable reference and the rebuild overrides fields.
|
|
281
|
-
return rebuildConversationFromEntries({ ...c }, (id) =>
|
|
281
|
+
return rebuildConversationFromEntries({ ...c }, (id) =>
|
|
282
|
+
this.readEntries(id, { types: ["subagent_result", "callback_started"] }),
|
|
283
|
+
);
|
|
282
284
|
}
|
|
283
285
|
|
|
284
286
|
// In-memory stores already hold the full conversation object, so there's
|