@poncho-ai/harness 0.59.2 → 0.59.4

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.
@@ -1,41 +1,29 @@
1
1
  // ---------------------------------------------------------------------------
2
- // Phase 3b — dual-write + parity checker (instrumentation only)
2
+ // Subagent delivery-queue writers.
3
3
  //
4
- // At each conversation WRITE site we ALSO append the corresponding
5
- // append-only `ConversationEntry`s alongside the existing mutable-blob write.
6
- // READ paths are untouched: nothing consumes these entries yet, so a bug here
7
- // can only mislog it cannot corrupt behavior. The blob remains the source of
8
- // truth until the read-cutover PR (3c).
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
- // Two public surfaces:
11
- // - `appendEntriesSafe(...)` fire-and-forget wrapper that swallows every
12
- // error (so a dual-write failure never breaks a live turn) and stamps a
13
- // uuid `id` on each entry (the engine inserts `entry.id` as a column).
14
- // - `verifyEntriesParity(...)` — gated on `PONCHO_VERIFY_ENTRIES === "1"`,
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 `message`/`result`/etc. Distribute over the
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 append-only log, mirroring an existing
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
- * - is safe to `void` (callers needn't await).
37
+ * - safe to `void` when the caller doesn't need the stored rows.
51
38
  *
52
- * Returns the stored entries (with seq/createdAt) for callers that want them
53
- * (e.g. to learn the assistant entry's id for a later amendment), or [] on
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-dual-write] append failed for ${conversation.conversationId}: ${
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 dualWriteLog = createLogger("orchestrator:entries");
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
- pushedAssistant = {
541
- role: "assistant" as const,
542
- content: draft.assistantResponse,
543
- metadata: buildAssistantMetadata(draft),
544
- };
545
- conv.messages = [...prevMessages, pushedAssistant];
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
- // DUAL-WRITE (mirrors the consume-pending + message-push blob writes
1267
- // above): append a callback_started entry listing the consumed
1268
- // subagent_result seqs (resolved by matching subagentId against the entry
1269
- // log BEST-EFFORT, since the blob's pending array carries no seq), plus
1270
- // a hidden user_message entry per injected callback message.
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
- callbackStartedEntry(consumedSeqs),
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
- dualWriteLog.error(
1300
- `[entries-dual-write] callback_started append failed for ${conversation.conversationId}: ${
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
- dualWriteLog,
1797
+ entriesQueueLog,
1891
1798
  );
1892
1799
  } catch (err) {
1893
- dualWriteLog.error(
1894
- `[entries-dual-write] subagent_result append failed for ${parentConversationId}: ${
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
 
@@ -83,6 +74,12 @@ export interface RunConversationTurnOpts {
83
74
  * built with an OTLP exporter attached.
84
75
  */
85
76
  suppressTelemetry?: boolean;
77
+ /**
78
+ * Forwarded to `RunInput.model`. Per-run model override, captured once at
79
+ * run start — safe under concurrent runs on a shared harness, unlike
80
+ * mutating the parsed agent's frontmatter.
81
+ */
82
+ model?: string;
86
83
  /** Per-event hook — called for every AgentEvent yielded by the run, in order. */
87
84
  onEvent?: (event: AgentEvent) => void | Promise<void>;
88
85
  }
@@ -207,14 +204,6 @@ export const runConversationTurn = async (
207
204
  await opts.conversationStore.update(conversation);
208
205
  };
209
206
 
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
207
  // Persist the user turn immediately so a crash mid-run still records what
219
208
  // the user said. Fire-and-forget — don't block the run.
220
209
  conversation.messages = [...historyMessages, userMessage];
@@ -227,15 +216,6 @@ export const runConversationTurn = async (
227
216
  );
228
217
  });
229
218
 
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
219
  try {
240
220
  const execution = await executeConversationTurn({
241
221
  harness: opts.harness,
@@ -256,6 +236,7 @@ export const runConversationTurn = async (
256
236
  abortSignal: opts.abortSignal,
257
237
  disablePromptCache: opts.disablePromptCache,
258
238
  suppressTelemetry: opts.suppressTelemetry,
239
+ model: opts.model,
259
240
  },
260
241
  initialContextTokens: conversation.contextTokens ?? 0,
261
242
  initialContextWindow: conversation.contextWindow ?? 0,
@@ -286,48 +267,6 @@ export const runConversationTurn = async (
286
267
  ...existingHistory,
287
268
  ...preRunMessages.slice(0, removedCount),
288
269
  ];
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
270
  }
332
271
  }
333
272
  if (event.type === "step:completed") {
@@ -468,46 +407,6 @@ export const runConversationTurn = async (
468
407
  { shouldRebuildCanonical },
469
408
  );
470
409
  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
410
  }
512
411
 
513
412
  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) => this.readEntries(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