@poncho-ai/harness 0.59.1 → 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.
@@ -1,27 +1,42 @@
1
- import { createLogger, type Message } from "@poncho-ai/sdk";
1
+ import { createLogger } from "@poncho-ai/sdk";
2
2
  import type { Conversation, PendingSubagentResult } from "../state.js";
3
3
 
4
4
  const entriesReadLog = createLogger("entries-read");
5
5
 
6
6
  /**
7
- * Append-only conversation entries (Phase 3 substrate).
7
+ * The subagent delivery queue: append-only `conversation_entries` rows that
8
+ * carry a finished subagent's result to its parent conversation.
8
9
  *
9
- * The eventual replacement for the mutable per-conversation JSON blob: a
10
- * conversation becomes an ordered, append-only list of entries, and the
11
- * mutable-blob clobber race (two writers serializing a stale whole-blob
12
- * snapshot over each other the root cause behind lost subagent results)
13
- * stops being expressible.
10
+ * Why this exists: subagent results are the ONE conversation field with
11
+ * concurrent writers. A subagent finishes whenever it finishes possibly
12
+ * while the parent turn is mid-stream doing whole-blob writes — so a
13
+ * read-modify-write on the mutable conversation row could serialize a stale
14
+ * snapshot over the result (the historical "lost subagent result" clobber).
15
+ * An append-only INSERT can't express that race. Everything single-writer
16
+ * (message history, metadata) stays on the conversation row, where the
17
+ * orchestrator's per-conversation turn serialization already makes mutation
18
+ * safe.
14
19
  *
15
- * This module is intentionally PURE: it defines the entry shapes and the
16
- * functions that rebuild a conversation's LLM context / display transcript
17
- * / pending-subagent-results from an entry list. No storage engine, no DB,
18
- * no wiring into the live run loop yet — so it deploys nothing and is
19
- * fully unit-testable. The engine implementations (append/read on
20
- * postgres/sqlite/memory) and the write-site conversions come in later PRs
21
- * once this rebuild logic is proven.
20
+ * Two entry types:
21
+ * - `subagent_result`: a finished subagent's result, appended by the
22
+ * orchestrator's result-delivery path.
23
+ * - `callback_started`: marks which result entries a callback turn
24
+ * consumed (by seq). Consumption is an append, never a delete — a
25
+ * result is "pending" until a later callback_started lists its seq.
22
26
  *
23
- * Ordering: every entry carries a monotonic per-conversation `seq`. Entries
24
- * are assumed sorted by `seq` ascending when passed to the rebuild fns.
27
+ * Historical note: this module once defined a full transcript's worth of
28
+ * entry types (user/assistant/harness messages, compaction overlays) as
29
+ * groundwork for replacing the conversation blob entirely. The full read
30
+ * cutover shipped briefly (harness 0.58.0), proved unfaithful for callback
31
+ * turns, and was reverted; the unread types + dual-writes were then deleted
32
+ * rather than maintained as drift-prone dead weight. If a future feature
33
+ * needs real history semantics (editing, branching, audit), design that
34
+ * migration fresh — and remember the 0.58.0 lesson: an append-only log is
35
+ * only as good as the completeness of its writers.
36
+ *
37
+ * Ordering: every entry carries a monotonic per-conversation `seq`,
38
+ * assigned by the engine at append time. Entries are sorted by `seq`
39
+ * ascending when passed to the rebuild fn.
25
40
  */
26
41
 
27
42
  interface BaseEntry {
@@ -32,53 +47,6 @@ interface BaseEntry {
32
47
  createdAt: number;
33
48
  }
34
49
 
35
- /** A user-role display message (incl. typed subagent-callback messages). */
36
- export interface UserMessageEntry extends BaseEntry {
37
- type: "user_message";
38
- message: Message;
39
- turnId: string;
40
- /** Hidden from the display transcript (e.g. a framed job prompt, an
41
- * onboarding seed, or an injected subagent-result message). Still part
42
- * of the record; just not rendered as a chat bubble. */
43
- hidden?: boolean;
44
- }
45
-
46
- /** The final assistant bubble for a completed/cancelled/errored turn. */
47
- export interface AssistantMessageEntry extends BaseEntry {
48
- type: "assistant_message";
49
- message: Message;
50
- turnId: string;
51
- runId: string;
52
- }
53
-
54
- /** A post-hoc edit to an already-emitted assistant message — replaces the
55
- * orchestrator/resume "mutate the last assistant message in place" writes
56
- * with an append. Applied at rebuild time. */
57
- export interface AssistantAmendmentEntry extends BaseEntry {
58
- type: "assistant_amendment";
59
- targetEntryId: string;
60
- appendText?: string;
61
- }
62
-
63
- /** One LLM-transcript message (the model-visible form). Appended from the
64
- * run loop per step — never diffed from an array. */
65
- export interface HarnessMessageEntry extends BaseEntry {
66
- type: "harness_message";
67
- message: Message;
68
- turnId: string;
69
- }
70
-
71
- /** Compaction overlay: nothing is deleted. At rebuild, the LLM context is
72
- * the latest compaction's `summaryMessage` followed by the harness
73
- * messages from `firstKeptSeq` onward. */
74
- export interface CompactionEntry extends BaseEntry {
75
- type: "compaction";
76
- summaryMessage: Message;
77
- firstKeptSeq: number;
78
- tokensBefore?: number;
79
- tokensAfter?: number;
80
- }
81
-
82
50
  /** A finished subagent's result arriving for the parent. Pending = a
83
51
  * subagent_result whose seq is not listed in any later callback_started. */
84
52
  export interface SubagentResultEntry extends BaseEntry {
@@ -93,21 +61,14 @@ export interface CallbackStartedEntry extends BaseEntry {
93
61
  consumedSeqs: number[];
94
62
  }
95
63
 
96
- export type ConversationEntry =
97
- | UserMessageEntry
98
- | AssistantMessageEntry
99
- | AssistantAmendmentEntry
100
- | HarnessMessageEntry
101
- | CompactionEntry
102
- | SubagentResultEntry
103
- | CallbackStartedEntry;
64
+ export type ConversationEntry = SubagentResultEntry | CallbackStartedEntry;
104
65
 
105
66
  /**
106
67
  * An entry to append, before the engine assigns `seq` and `createdAt`. This
107
68
  * is a DISTRIBUTIVE omit — `Omit<ConversationEntry, K>` over a union would
108
- * collapse to only the keys common to every member (dropping `message`,
109
- * `summaryMessage`, etc.), so we distribute over the union with a
110
- * conditional type to omit those fields from each member individually.
69
+ * collapse to only the keys common to every member, so we distribute over
70
+ * the union with a conditional type to omit those fields from each member
71
+ * individually.
111
72
  */
112
73
  export type NewConversationEntry = ConversationEntry extends infer T
113
74
  ? T extends ConversationEntry
@@ -115,88 +76,6 @@ export type NewConversationEntry = ConversationEntry extends infer T
115
76
  : never
116
77
  : never;
117
78
 
118
- /**
119
- * Rebuild the LLM-visible message context from the entry log.
120
- *
121
- * If a compaction overlay exists, the context is its summary message
122
- * followed by every harness message with seq >= firstKeptSeq (a later
123
- * compaction's firstKeptSeq can point at an earlier summary that was
124
- * itself appended as a harness message, so layered compactions just work).
125
- * With no compaction, it's every harness message in order.
126
- */
127
- export function buildLlmContext(entries: ConversationEntry[]): Message[] {
128
- let latestCompaction: CompactionEntry | undefined;
129
- for (const e of entries) {
130
- if (e.type === "compaction" && (!latestCompaction || e.seq > latestCompaction.seq)) {
131
- latestCompaction = e;
132
- }
133
- }
134
-
135
- const harnessMsgs = entries.filter(
136
- (e): e is HarnessMessageEntry => e.type === "harness_message",
137
- );
138
-
139
- if (latestCompaction) {
140
- const kept = harnessMsgs
141
- .filter((e) => e.seq >= latestCompaction!.firstKeptSeq)
142
- .map((e) => e.message);
143
- return [latestCompaction.summaryMessage, ...kept];
144
- }
145
- return harnessMsgs.map((e) => e.message);
146
- }
147
-
148
- export interface DisplaySnapshot {
149
- messages: Message[];
150
- /** Total display messages available (for pagination UIs). */
151
- totalMessages: number;
152
- /** seq of the first message returned (a `beforeSeq` pagination cursor). */
153
- headSeq: number | null;
154
- }
155
-
156
- /**
157
- * Rebuild the display transcript (the user-visible chat) from the entry
158
- * log, returning the trailing `tailN` messages. Amendments are folded into
159
- * their target assistant message; hidden user messages are dropped.
160
- */
161
- export function buildDisplaySnapshot(
162
- entries: ConversationEntry[],
163
- tailN: number,
164
- ): DisplaySnapshot {
165
- const amendmentsByTarget = new Map<string, AssistantAmendmentEntry[]>();
166
- for (const e of entries) {
167
- if (e.type === "assistant_amendment") {
168
- const list = amendmentsByTarget.get(e.targetEntryId) ?? [];
169
- list.push(e);
170
- amendmentsByTarget.set(e.targetEntryId, list);
171
- }
172
- }
173
-
174
- const built: { seq: number; message: Message }[] = [];
175
- for (const e of entries) {
176
- if (e.type === "user_message") {
177
- if (e.hidden) continue;
178
- built.push({ seq: e.seq, message: e.message });
179
- } else if (e.type === "assistant_message") {
180
- let content = typeof e.message.content === "string" ? e.message.content : "";
181
- const amendments = amendmentsByTarget.get(e.id);
182
- if (amendments) {
183
- for (const a of amendments.sort((x, y) => x.seq - y.seq)) {
184
- if (a.appendText) content += a.appendText;
185
- }
186
- }
187
- built.push({ seq: e.seq, message: { ...e.message, content } });
188
- }
189
- }
190
-
191
- const total = built.length;
192
- const tail = tailN >= total ? built : built.slice(total - tailN);
193
- return {
194
- messages: tail.map((b) => b.message),
195
- totalMessages: total,
196
- headSeq: tail.length > 0 ? tail[0]!.seq : null,
197
- };
198
- }
199
-
200
79
  /**
201
80
  * Subagent results that have arrived but not yet been consumed by a
202
81
  * callback turn — the append-only replacement for the mutable
@@ -219,25 +98,20 @@ export function getPendingSubagentResults(
219
98
  }
220
99
 
221
100
  /**
222
- * Phase 3c read cutover: rebuild a conversation's reader-facing fields from
223
- * the append-only entry log, with a blob fallback for conversations that
224
- * predate dual-write.
225
- *
226
- * Call this in every conversation `get`/`getWithArchive` path AFTER the
227
- * Conversation has been constructed from the stored row/blob. It:
228
- * - reads the entry log via `readEntries`,
229
- * - if NON-EMPTY, overrides `_harnessMessages`, `messages`, and
230
- * `pendingSubagentResults` with entry-derived values,
231
- * - if EMPTY (un-migrated conversation), leaves the blob-derived fields
232
- * untouched (fallback),
233
- * - on ANY error, logs and falls back to the blob (never throws — this is
234
- * a hot read path).
101
+ * Read-path override: rebuild `pendingSubagentResults` from the queue.
235
102
  *
236
- * `_continuationMessages` and `pendingApprovals` are NOT modeled as entries
237
- * yet and are intentionally left as blob fields.
103
+ * Called in every conversation `get`/`getWithArchive` path AFTER the
104
+ * Conversation has been constructed from the stored row/blob. Only
105
+ * `pendingSubagentResults` is overridden — it's the only field with a write
106
+ * race; message history is written solely by the serialized turn finalize
107
+ * and stays on the blob. If the queue is EMPTY (conversation predates it,
108
+ * or simply has no subagent traffic recorded) the blob-derived value is
109
+ * left untouched; on ANY error this logs and falls back to the blob (hot
110
+ * read path — never throws).
238
111
  *
239
112
  * Kill-switch: set `PONCHO_READ_ENTRIES=0` to instantly revert to pure blob
240
- * reads without a deploy (rebuild is ON by default).
113
+ * reads without a deploy (queue reads are ON by default). The blob field is
114
+ * still dual-written for exactly this reason.
241
115
  *
242
116
  * NOTE: mutates `conversation` in place and returns it. Callers that hand
243
117
  * back a shared/mutable Conversation reference (the in-memory stores) MUST
@@ -247,20 +121,11 @@ export async function rebuildConversationFromEntries(
247
121
  conversation: Conversation,
248
122
  readEntries: (conversationId: string) => Promise<ConversationEntry[]>,
249
123
  ): Promise<Conversation> {
250
- // Targeted append-only: only `pendingSubagentResults` is read from the
251
- // entry log, because it's the ONLY conversation field with a write race
252
- // (a subagent finishing mid-turn vs. the parent turn's whole-blob write).
253
- // The message history (`messages` / `_harnessMessages`) is written solely
254
- // by the turn finalize, which the orchestrator serializes per
255
- // conversation — never raced — so it stays on the blob (known-good, and
256
- // far simpler than faithfully rebuilding the LLM transcript from entries).
257
- //
258
- // Kill-switch: ON by default; PONCHO_READ_ENTRIES="0" reverts to the blob.
259
124
  if (process.env.PONCHO_READ_ENTRIES === "0") return conversation;
260
125
 
261
126
  try {
262
127
  const entries = await readEntries(conversation.conversationId);
263
- if (entries.length === 0) return conversation; // fallback: pre-dual-write
128
+ if (entries.length === 0) return conversation; // fallback: pre-queue conversations
264
129
  conversation.pendingSubagentResults = getPendingSubagentResults(entries);
265
130
  return conversation;
266
131
  } catch (err) {
@@ -113,7 +113,7 @@ export class InMemoryEngine implements StorageEngine {
113
113
  // log (blob fallback for un-migrated conversations). Clone first — the
114
114
  // map holds a live mutable reference and the rebuild overrides fields.
115
115
  return rebuildConversationFromEntries({ ...c }, (id) =>
116
- this.conversations.readEntries(id),
116
+ this.conversations.readEntries(id, { types: ["subagent_result", "callback_started"] }),
117
117
  );
118
118
  },
119
119
 
@@ -123,7 +123,7 @@ export class InMemoryEngine implements StorageEngine {
123
123
  const c = this.convs.get(conversationId);
124
124
  if (!c) return undefined;
125
125
  return rebuildConversationFromEntries({ ...c }, (id) =>
126
- this.conversations.readEntries(id),
126
+ this.conversations.readEntries(id, { types: ["subagent_result", "callback_started"] }),
127
127
  );
128
128
  },
129
129
 
@@ -333,7 +333,7 @@ export abstract class SqlStorageEngine implements StorageEngine {
333
333
  // append-only entry log, falling back to the blob for un-migrated
334
334
  // conversations. parseConversation returns a fresh object, so no clone.
335
335
  return rebuildConversationFromEntries(conv, (id) =>
336
- this.conversations.readEntries(id),
336
+ this.conversations.readEntries(id, { types: ["subagent_result", "callback_started"] }),
337
337
  );
338
338
  },
339
339
 
@@ -420,7 +420,7 @@ export abstract class SqlStorageEngine implements StorageEngine {
420
420
  // Phase 3c read cutover: rebuild reader-facing fields from the entry
421
421
  // log (blob fallback for un-migrated conversations).
422
422
  return rebuildConversationFromEntries(conv, (id) =>
423
- this.conversations.readEntries(id),
423
+ this.conversations.readEntries(id, { types: ["subagent_result", "callback_started"] }),
424
424
  );
425
425
  },
426
426
 
@@ -557,11 +557,31 @@ export abstract class SqlStorageEngine implements StorageEngine {
557
557
  conversationId: string,
558
558
  title: string,
559
559
  ): Promise<Conversation | undefined> => {
560
- const conv = await this.conversations.get(conversationId);
561
- if (!conv) return undefined;
562
- conv.title = normalizeTitle(title);
563
- await this.conversations.update(conv);
564
- return conv;
560
+ // Targeted update deliberately NOT get→mutate→update(). The
561
+ // whole-row read-modify-write races a streaming turn's per-step
562
+ // draft persist: rename reads the row at T0, the turn persists step
563
+ // N's draft at T1, rename writes T0's stale blob back at T2 and
564
+ // silently reverts the turn's progress.
565
+ //
566
+ // Title lives in BOTH the `title` column and the `data` blob (reads
567
+ // parse the blob), so update both — the blob via the database's own
568
+ // JSON-set function INSIDE the same UPDATE. That keeps the write
569
+ // atomic and server-side: no stale snapshot is ever serialized back.
570
+ const normalized = normalizeTitle(title);
571
+ // Distinct placeholders for the two title occurrences: rewrite()
572
+ // converts $N → ? positionally for sqlite, so reusing $1 would
573
+ // desync the param array.
574
+ const dataExpr = this.dialect.tag === "sqlite"
575
+ ? `json_set(data, '$.title', $2)`
576
+ : `jsonb_set(data, '{title}', to_jsonb($2::text))`;
577
+ await this.executor.run(
578
+ rewrite(
579
+ `UPDATE conversations SET title = $1, data = ${dataExpr}, updated_at = $3 WHERE id = $4`,
580
+ this.dialect,
581
+ ),
582
+ [normalized, normalized, new Date().toISOString(), conversationId],
583
+ );
584
+ return this.conversations.get(conversationId);
565
585
  },
566
586
 
567
587
  delete: async (conversationId: string): Promise<boolean> => {
@@ -1,172 +1,49 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import type { Message } from "@poncho-ai/sdk";
3
- import { createLogger } from "@poncho-ai/sdk";
4
2
  import { InMemoryConversationStore } from "../src/state.js";
5
- import {
6
- buildDisplaySnapshot,
7
- buildLlmContext,
8
- getPendingSubagentResults,
9
- } from "../src/storage/entries.js";
3
+ import { createLogger } from "@poncho-ai/sdk";
10
4
  import {
11
5
  appendEntriesSafe,
12
- assistantMessageEntry,
13
6
  callbackStartedEntry,
14
- compactionEntry,
15
- harnessMessageEntries,
16
- newHarnessMessagesThisTurn,
17
7
  subagentResultEntry,
18
- userMessageEntry,
19
8
  } from "../src/orchestrator/entries-dual-write.js";
20
9
 
21
10
  const log = createLogger("test");
22
- const msg = (role: Message["role"], content: string): Message => ({
23
- role,
24
- content,
25
- metadata: { id: `${role}-${content}` },
26
- });
27
-
28
- const conv = (id: string) => ({
29
- conversationId: id,
30
- ownerId: "owner-1",
31
- tenantId: null as string | null,
32
- });
11
+ const convRef = { conversationId: "c1", ownerId: "owner", tenantId: null };
33
12
 
34
- describe("entries dual-write", () => {
35
- it("rebuilds llm context + display from a simulated chat turn's appends", async () => {
13
+ describe("appendEntriesSafe (queue writer)", () => {
14
+ it("stamps a uuid id and returns stored entries with seq/createdAt", async () => {
36
15
  const store = new InMemoryConversationStore();
37
- const c = conv("c1");
38
- const turnId = "turn-1";
39
-
40
- // Turn start: user message.
41
- await appendEntriesSafe(store, c, [userMessageEntry(msg("user", "hi"), turnId)], log);
42
-
43
- // During the turn the harness produced two model-visible messages and a
44
- // final assistant bubble.
45
- const harness1 = msg("user", "hi");
46
- const harness2 = msg("assistant", "hello there");
47
- const finalAssistant = msg("assistant", "hello there");
48
- await appendEntriesSafe(
16
+ const stored = await appendEntriesSafe(
49
17
  store,
50
- c,
18
+ convRef,
51
19
  [
52
- ...harnessMessageEntries([harness1, harness2], turnId),
53
- assistantMessageEntry(finalAssistant, turnId, "run-1"),
20
+ subagentResultEntry({ subagentId: "s1", task: "t", status: "completed", timestamp: 1 }),
21
+ callbackStartedEntry([1]),
54
22
  ],
55
23
  log,
56
24
  );
57
-
58
- const entries = await store.readEntries("c1");
59
-
60
- // LLM context == the harness messages in order.
61
- const llm = buildLlmContext(entries);
62
- expect(llm.map((m) => m.content)).toEqual(["hi", "hello there"]);
63
-
64
- // Display == [user, assistant] (final assistant bubble; harness msgs hidden).
65
- const snap = buildDisplaySnapshot(entries, 100);
66
- expect(snap.messages.map((m) => [m.role, m.content])).toEqual([
67
- ["user", "hi"],
68
- ["assistant", "hello there"],
69
- ]);
70
- expect(snap.totalMessages).toBe(2);
25
+ expect(stored).toHaveLength(2);
26
+ expect(stored.every((e) => typeof e.id === "string" && e.id.length > 0)).toBe(true);
27
+ expect(stored.map((e) => e.seq)).toEqual([1, 2]);
71
28
  });
72
29
 
73
- it("compaction overlay keeps summary + tail at rebuild", async () => {
30
+ it("returns [] on empty input without touching the store", async () => {
74
31
  const store = new InMemoryConversationStore();
75
- const c = conv("c2");
76
-
77
- await appendEntriesSafe(
78
- store,
79
- c,
80
- harnessMessageEntries(
81
- [msg("user", "m1"), msg("assistant", "m2"), msg("user", "m3")],
82
- "t",
83
- ),
84
- log,
85
- );
86
- const before = await store.readEntries("c2");
87
- // Keep only the last harness message (seq 3) after compaction.
88
- const firstKeptSeq = before[before.length - 1]!.seq;
89
- await appendEntriesSafe(
90
- store,
91
- c,
92
- [compactionEntry(msg("assistant", "SUMMARY"), firstKeptSeq)],
93
- log,
94
- );
95
-
96
- const llm = buildLlmContext(await store.readEntries("c2"));
97
- expect(llm.map((m) => m.content)).toEqual(["SUMMARY", "m3"]);
32
+ expect(await appendEntriesSafe(store, convRef, [], log)).toEqual([]);
33
+ expect(await store.readEntries("c1")).toEqual([]);
98
34
  });
99
35
 
100
- it("subagent_result + callback_started track pending consumption", async () => {
36
+ it("never throws swallows store failures and returns []", async () => {
101
37
  const store = new InMemoryConversationStore();
102
- const c = conv("c3");
103
-
38
+ store.appendEntries = async () => {
39
+ throw new Error("boom");
40
+ };
104
41
  const stored = await appendEntriesSafe(
105
42
  store,
106
- c,
107
- [
108
- subagentResultEntry({
109
- subagentId: "sa-1",
110
- task: "do thing",
111
- status: "completed",
112
- timestamp: 1,
113
- }),
114
- ],
115
- log,
116
- );
117
- const resultSeq = stored[0]!.seq;
118
-
119
- // Before consumption: pending.
120
- expect(getPendingSubagentResults(await store.readEntries("c3"))).toHaveLength(1);
121
-
122
- // The callback consumes it + injects a hidden user message.
123
- await appendEntriesSafe(
124
- store,
125
- c,
126
- [
127
- callbackStartedEntry([resultSeq]),
128
- userMessageEntry(msg("user", "[Subagent Result] ..."), "cb-1", { hidden: true }),
129
- ],
130
- log,
131
- );
132
-
133
- const after = await store.readEntries("c3");
134
- expect(getPendingSubagentResults(after)).toHaveLength(0);
135
- // Hidden injected message does not appear in the display transcript.
136
- expect(buildDisplaySnapshot(after, 100).messages).toHaveLength(0);
137
- });
138
-
139
- it("newHarnessMessagesThisTurn diffs the suffix and flags shrinks", () => {
140
- const a = msg("user", "a");
141
- const b = msg("assistant", "b");
142
- const cc = msg("user", "c");
143
-
144
- expect(newHarnessMessagesThisTurn(undefined, [a, b])).toEqual({
145
- messages: [a, b],
146
- approximate: false,
147
- });
148
- expect(newHarnessMessagesThisTurn([a], [a, b, cc])).toEqual({
149
- messages: [b, cc],
150
- approximate: false,
151
- });
152
- // Shrink (compaction reshaped the array) → approximate, returns full next.
153
- const shrink = newHarnessMessagesThisTurn([a, b, cc], [a]);
154
- expect(shrink.approximate).toBe(true);
155
- expect(shrink.messages).toEqual([a]);
156
- });
157
-
158
- it("appendEntriesSafe swallows store errors and returns []", async () => {
159
- const brokenStore = {
160
- appendEntries: async () => {
161
- throw new Error("boom");
162
- },
163
- } as unknown as InMemoryConversationStore;
164
- const result = await appendEntriesSafe(
165
- brokenStore,
166
- conv("c4"),
167
- [userMessageEntry(msg("user", "x"), "t")],
43
+ convRef,
44
+ [callbackStartedEntry([1])],
168
45
  log,
169
46
  );
170
- expect(result).toEqual([]);
47
+ expect(stored).toEqual([]);
171
48
  });
172
49
  });