@guuey/threads 0.28.1 → 0.30.0

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.
@@ -20,13 +20,37 @@ export interface RowCtx {
20
20
  untrustedOrigin?: boolean;
21
21
  }
22
22
  /**
23
- * Plain-text projection of an AgMessage. Distinct text blocks are distinct
24
- * paragraphs, so they join with a paragraph break — the same block-boundary
25
- * contract as `@guuey/agent-client`'s live flat-text fold (guuey#98); raw
26
- * concatenation jammed "…instead.Here's your packing…". Empty blocks
27
- * contribute nothing (no stacked separators).
23
+ * The ANSWER as plain text: the message's text blocks WITHOUT interim
24
+ * narration. Distinct text blocks are distinct paragraphs, so they join with
25
+ * a paragraph break, the same block-boundary contract as `@guuey/agent-client`'s
26
+ * live flat-text fold (guuey#98); raw concatenation jammed "…instead.Here's
27
+ * your packing…". Empty blocks contribute nothing (no stacked separators).
28
+ * This is the row's `text`: what a reloaded thread, a preview and a hook read.
28
29
  */
29
30
  export declare function messageText(msg: AgMessage): string;
31
+ /** The message's interim narration, one entry per non-empty narration block, in order. */
32
+ export declare function messageNarration(msg: AgMessage): string[];
33
+ /**
34
+ * EVERY text block, narration included, joined as the answer text was joined
35
+ * before narration split out of it: the MODEL's view of a stored message.
36
+ * `loadHistory` replays this, so the model's conversation history is the same
37
+ * bytes whether a row was written before the split or after it.
38
+ */
39
+ export declare function messageFullText(msg: AgMessage): string;
40
+ /** A stored message's text, split: the answer, the narration, and everything (the model's view). */
41
+ export interface StoredTextParts {
42
+ answer: string;
43
+ narration: string[];
44
+ all: string;
45
+ }
46
+ /**
47
+ * Split a stored row's `content` into answer, narration and the model's full
48
+ * text, when `content` is a stored `AgMessage` (an object, or the JSON string
49
+ * an AppSync writer stores). Anything else is null, and a reader falls back to
50
+ * the row's `text`. It is read through core's lenient stored-record reader, so
51
+ * an unknown block is omitted rather than failing the row.
52
+ */
53
+ export declare function storedTextParts(content: unknown): StoredTextParts | null;
30
54
  export declare function agMessageToRow(msg: AgMessage, ctx: RowCtx & {
31
55
  turnRecord?: AgTurnRecord;
32
56
  }): ThreadMessageRow;
@@ -76,6 +100,65 @@ export declare function cardRowToAgArtifact(row: ThreadMessageRow): AgArtifact |
76
100
  * artifactIds there.
77
101
  */
78
102
  export declare function reassembleFold(rows: ThreadMessageRow[], snapshot: ThreadSnapshotRow | undefined): AgReduceResult;
103
+ /**
104
+ * Split a snapshot's STORED thread-memory array (what a binding reads back)
105
+ * into the view this runtime reads and the elements it carries (AgJSON
106
+ * draft.4 §0.2's stored-record reader). Stored data is a wire across a rolling
107
+ * release, so it is never taken as `AgMemoryRecord[]`:
108
+ * - `threadMemory`: the records the reader materializes with scope `thread`;
109
+ * the view a fold is seeded with and a prompt recalls;
110
+ * - `carriedThreadMemory`: every other stored element, verbatim and in stored
111
+ * order: a record the reader cannot materialize (an undefined `scope`, a
112
+ * missing `value`) and a readable record of another scope.
113
+ * The reader either materializes an element or reports it at `[i]`. If a core
114
+ * version ever did both for one element (a partial report), pairing the view
115
+ * back to the stored elements would desync, so then EVERY element is carried
116
+ * and nothing is seeded: safe at rest, and visible as a full carry.
117
+ */
118
+ export declare function splitStoredThreadMemory(stored: readonly JsonValue[]): Pick<ThreadSnapshotRow, "threadMemory"> & {
119
+ carriedThreadMemory: JsonValue[];
120
+ };
121
+ /**
122
+ * The one stored thread-memory array a binding writes: the carried elements
123
+ * FIRST, then this runtime's thread records. Stored order is precedence for a
124
+ * reader that seeds every record (the reducer keys memory by scope and key, and
125
+ * the last write wins), and the carried elements were written before this
126
+ * turn, so write order stays time order.
127
+ */
128
+ export declare function joinStoredThreadMemory(row: Pick<ThreadSnapshotRow, "threadMemory" | "carriedThreadMemory">): JsonValue[];
129
+ /**
130
+ * The thread records a turn persists: each record of the fold, except that a
131
+ * record the turn did NOT change is persisted as the PRIOR stored record
132
+ * instead of the fold's rebuild of it (guuey#1669).
133
+ *
134
+ * The prior view comes from the stored-record reader, whose elements are raw
135
+ * copies that keep members this runtime does not know. The fold cannot
136
+ * carry those: {@link seedEventsForReducer} rebuilds each record as a
137
+ * `memory.write` from its known fields, and the reducer keeps only those. So
138
+ * persisting the fold alone would drop a record's unknown members on the first
139
+ * turn that re-persists the thread, even though the turn never touched it. A
140
+ * record keeps what it was stored with until a turn changes it.
141
+ *
142
+ * - **Identity** is the reducer's: scope plus `key ?? ""`. A fold record meets
143
+ * the LAST prior record of that identity, the one the seed left standing,
144
+ * because the last write wins. Several key-less records of one scope were
145
+ * already collapsed to that one by the reducer before this existed; that
146
+ * limit predates the overlay and is not papered over here.
147
+ * - **Unchanged** means the known fields a turn can write are deep-equal:
148
+ * `scope`, `key`, `value`, `reason`, `durable`, `turnId` and `_meta`
149
+ * (draft.8: a `memory.write` sets it, and a patch can replace it without
150
+ * touching the value or the turnId, so a `_meta`-only change must count as
151
+ * a change or the prior `_meta` would be persisted over it). A live write
152
+ * stamps the current turn's `turnId`, so a turn that rewrites the SAME value
153
+ * still changes the record, and the fold's record wins (the unknown members
154
+ * described the previous write). **`threadId` is deliberately left out:** a
155
+ * `memory.write` has no `threadId` on its event arm, so no turn can set it and
156
+ * the seed cannot carry it. Comparing it would mark every stored record that
157
+ * has one as changed, which is exactly the loss this prevents.
158
+ * - Records the turn removed are absent from the fold and stay absent; a
159
+ * record the turn added has no prior and is the fold's own.
160
+ */
161
+ export declare function keepUnchangedThreadMemory(priorView: readonly AgMemoryRecord[], folded: readonly AgMemoryRecord[]): AgMemoryRecord[];
79
162
  /**
80
163
  * Synthetic events that seed a reducer with prior latest-replace state before
81
164
  * folding a turn (design §8.1). Seeds state + thread-memory ONLY — never
@@ -88,6 +171,10 @@ export declare function reassembleFold(rows: ThreadMessageRow[], snapshot: Threa
88
171
  * Each synthetic `memory.write` carries `turnId` so the reducer's SET handler
89
172
  * lands it back onto the AgMemoryRecord — without it, a re-seeded thread-memory
90
173
  * record loses its turnId and memory byte-identity breaks from turn 2 on.
174
+ * It carries the record's `_meta` the same way: AgJSON draft.8 declares
175
+ * `_meta` on both the record and `memory.write`, and the reducer folds it
176
+ * onto the record (set on create, replaced on update), so a record's host
177
+ * side-channel (e.g. `_meta.provenance`) survives every re-seeded turn.
91
178
  * (`threadId` is NOT carried: `memory.write` has no threadId on its event arm,
92
179
  * so it cannot round-trip through a live write — that is expected.)
93
180
  */
@@ -1 +1 @@
1
- {"version":3,"file":"fold-rows.d.ts","sourceRoot":"","sources":["../src/fold-rows.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EACV,SAAS,EACT,UAAU,EACV,YAAY,EAEZ,cAAc,EACd,OAAO,EACP,cAAc,EACd,SAAS,EACV,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EAAE,gBAAgB,EAAwC,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAG3G,MAAM,WAAW,MAAM;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;IACX,eAAe,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AASD;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,SAAS,GAAG,MAAM,CAMlD;AAED,wBAAgB,cAAc,CAC5B,GAAG,EAAE,SAAS,EACd,GAAG,EAAE,MAAM,GAAG;IAAE,UAAU,CAAC,EAAE,YAAY,CAAA;CAAE,GAC1C,gBAAgB,CA2BlB;AA+DD,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,UAAU,EAAE,CAmC/E;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAU5E;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,UAAU,EACf,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GACrC,MAAM,GAAG,SAAS,CAQpB;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,gBAAgB,CA0BrG;AAOD;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,gBAAgB,GAAG,SAAS,CAY/D;AAED,gFAAgF;AAChF,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,gBAAgB,GAAG,UAAU,GAAG,SAAS,CAMjF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,gBAAgB,EAAE,EACxB,QAAQ,EAAE,iBAAiB,GAAG,SAAS,GACtC,cAAc,CA2BhB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,SAAS,GAAG,SAAS,EACjC,iBAAiB,EAAE,cAAc,EAAE,GAClC,OAAO,EAAE,CAmBX"}
1
+ {"version":3,"file":"fold-rows.d.ts","sourceRoot":"","sources":["../src/fold-rows.ts"],"names":[],"mappings":"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"}
package/dist/fold-rows.js CHANGED
@@ -1,4 +1,4 @@
1
- import { readStoredAgMessage } from "@silverprotocol/core";
1
+ import { readStoredAgMemoryRecords, readStoredAgMessage } from "@silverprotocol/core";
2
2
  import { asUiResource, scanProviderRawForUiResource, toolResultLocator } from "@guuey/mcp-apps-host/narrowing";
3
3
  /** AgRole → the row's coarse authorRole projection (secondary; role of record lives in content). */
4
4
  function roleToAuthor(role) {
@@ -9,20 +9,78 @@ function roleToAuthor(role) {
9
9
  return "agent"; // assistant | tool
10
10
  }
11
11
  /**
12
- * Plain-text projection of an AgMessage. Distinct text blocks are distinct
13
- * paragraphs, so they join with a paragraph break — the same block-boundary
14
- * contract as `@guuey/agent-client`'s live flat-text fold (guuey#98); raw
15
- * concatenation jammed "…instead.Here's your packing…". Empty blocks
16
- * contribute nothing (no stacked separators).
12
+ * A text block the producer marked as INTERIM narration (AgJSON's open-string
13
+ * `phase`; an OpenAI `commentary` message arrives this way), not the answer.
14
+ * Only the one value this package knows splits; any other phase is answer text.
17
15
  */
18
- export function messageText(msg) {
16
+ function isNarration(block) {
17
+ return block.type === "text" && block.phase === "interim";
18
+ }
19
+ /** The non-empty text blocks, in order, that `keep` admits, joined as paragraphs. */
20
+ function joinText(msg, keep) {
19
21
  const parts = [];
20
22
  for (const block of msg.content) {
21
- if (block.type === "text" && block.text)
23
+ if (block.type === "text" && block.text && keep(block))
22
24
  parts.push(block.text);
23
25
  }
24
26
  return parts.join("\n\n");
25
27
  }
28
+ /**
29
+ * The ANSWER as plain text: the message's text blocks WITHOUT interim
30
+ * narration. Distinct text blocks are distinct paragraphs, so they join with
31
+ * a paragraph break, the same block-boundary contract as `@guuey/agent-client`'s
32
+ * live flat-text fold (guuey#98); raw concatenation jammed "…instead.Here's
33
+ * your packing…". Empty blocks contribute nothing (no stacked separators).
34
+ * This is the row's `text`: what a reloaded thread, a preview and a hook read.
35
+ */
36
+ export function messageText(msg) {
37
+ return joinText(msg, (block) => !isNarration(block));
38
+ }
39
+ /** The message's interim narration, one entry per non-empty narration block, in order. */
40
+ export function messageNarration(msg) {
41
+ const out = [];
42
+ for (const block of msg.content) {
43
+ if (isNarration(block) && block.type === "text" && block.text)
44
+ out.push(block.text);
45
+ }
46
+ return out;
47
+ }
48
+ /**
49
+ * EVERY text block, narration included, joined as the answer text was joined
50
+ * before narration split out of it: the MODEL's view of a stored message.
51
+ * `loadHistory` replays this, so the model's conversation history is the same
52
+ * bytes whether a row was written before the split or after it.
53
+ */
54
+ export function messageFullText(msg) {
55
+ return joinText(msg, () => true);
56
+ }
57
+ /**
58
+ * Split a stored row's `content` into answer, narration and the model's full
59
+ * text, when `content` is a stored `AgMessage` (an object, or the JSON string
60
+ * an AppSync writer stores). Anything else is null, and a reader falls back to
61
+ * the row's `text`. It is read through core's lenient stored-record reader, so
62
+ * an unknown block is omitted rather than failing the row.
63
+ */
64
+ export function storedTextParts(content) {
65
+ let raw = content;
66
+ if (typeof content === "string") {
67
+ try {
68
+ raw = JSON.parse(content);
69
+ }
70
+ catch (err) {
71
+ if (err instanceof SyntaxError)
72
+ return null; // not JSON: not a stored AgMessage
73
+ throw err;
74
+ }
75
+ }
76
+ if (raw === null || typeof raw !== "object")
77
+ return null;
78
+ const read = readStoredAgMessage(raw);
79
+ if (read.value === undefined)
80
+ return null;
81
+ const msg = read.value;
82
+ return { answer: messageText(msg), narration: messageNarration(msg), all: messageFullText(msg) };
83
+ }
26
84
  export function agMessageToRow(msg, ctx) {
27
85
  const text = messageText(msg);
28
86
  const kind = "text";
@@ -282,6 +340,126 @@ export function reassembleFold(rows, snapshot) {
282
340
  ...(snapshot?.workingState !== undefined ? { state: snapshot.workingState } : {}),
283
341
  };
284
342
  }
343
+ /**
344
+ * Split a snapshot's STORED thread-memory array (what a binding reads back)
345
+ * into the view this runtime reads and the elements it carries (AgJSON
346
+ * draft.4 §0.2's stored-record reader). Stored data is a wire across a rolling
347
+ * release, so it is never taken as `AgMemoryRecord[]`:
348
+ * - `threadMemory`: the records the reader materializes with scope `thread`;
349
+ * the view a fold is seeded with and a prompt recalls;
350
+ * - `carriedThreadMemory`: every other stored element, verbatim and in stored
351
+ * order: a record the reader cannot materialize (an undefined `scope`, a
352
+ * missing `value`) and a readable record of another scope.
353
+ * The reader either materializes an element or reports it at `[i]`. If a core
354
+ * version ever did both for one element (a partial report), pairing the view
355
+ * back to the stored elements would desync, so then EVERY element is carried
356
+ * and nothing is seeded: safe at rest, and visible as a full carry.
357
+ */
358
+ export function splitStoredThreadMemory(stored) {
359
+ const read = readStoredAgMemoryRecords(stored);
360
+ const omitted = new Set(read.reports.flatMap((r) => (r.path.length === 1 && typeof r.path[0] === "number" ? [r.path[0]] : [])));
361
+ if (omitted.size !== read.reports.length || read.value.length + omitted.size !== stored.length) {
362
+ return { threadMemory: [], carriedThreadMemory: [...stored] };
363
+ }
364
+ const threadMemory = [];
365
+ const carriedThreadMemory = [];
366
+ // Readable elements come back in stored order, so the k-th one not omitted is `read.value[k]`.
367
+ let k = 0;
368
+ stored.forEach((element, i) => {
369
+ if (omitted.has(i)) {
370
+ carriedThreadMemory.push(element);
371
+ return;
372
+ }
373
+ const rec = read.value[k++];
374
+ if (rec !== undefined && rec.scope === "thread")
375
+ threadMemory.push(rec);
376
+ else
377
+ carriedThreadMemory.push(element);
378
+ });
379
+ return { threadMemory, carriedThreadMemory };
380
+ }
381
+ /**
382
+ * The one stored thread-memory array a binding writes: the carried elements
383
+ * FIRST, then this runtime's thread records. Stored order is precedence for a
384
+ * reader that seeds every record (the reducer keys memory by scope and key, and
385
+ * the last write wins), and the carried elements were written before this
386
+ * turn, so write order stays time order.
387
+ */
388
+ export function joinStoredThreadMemory(row) {
389
+ return [...(row.carriedThreadMemory ?? []), ...row.threadMemory];
390
+ }
391
+ /** Structural equality over JSON values (object key order does not matter). */
392
+ function jsonEqual(a, b) {
393
+ if (a === b)
394
+ return true;
395
+ if (a === undefined || b === undefined || a === null || b === null)
396
+ return false;
397
+ if (Array.isArray(a) || Array.isArray(b)) {
398
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
399
+ return false;
400
+ return a.every((v, i) => jsonEqual(v, b[i]));
401
+ }
402
+ if (typeof a !== "object" || typeof b !== "object")
403
+ return false;
404
+ const keys = Object.keys(a);
405
+ if (keys.length !== Object.keys(b).length)
406
+ return false;
407
+ return keys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && jsonEqual(a[k], b[k]));
408
+ }
409
+ /** The reducer's own memory identity (`${scope}${key ?? ""}`): a key-less record is its scope's singleton. */
410
+ function memoryIdentity(rec) {
411
+ return `${rec.scope}${rec.key ?? ""}`;
412
+ }
413
+ /**
414
+ * The thread records a turn persists: each record of the fold, except that a
415
+ * record the turn did NOT change is persisted as the PRIOR stored record
416
+ * instead of the fold's rebuild of it (guuey#1669).
417
+ *
418
+ * The prior view comes from the stored-record reader, whose elements are raw
419
+ * copies that keep members this runtime does not know. The fold cannot
420
+ * carry those: {@link seedEventsForReducer} rebuilds each record as a
421
+ * `memory.write` from its known fields, and the reducer keeps only those. So
422
+ * persisting the fold alone would drop a record's unknown members on the first
423
+ * turn that re-persists the thread, even though the turn never touched it. A
424
+ * record keeps what it was stored with until a turn changes it.
425
+ *
426
+ * - **Identity** is the reducer's: scope plus `key ?? ""`. A fold record meets
427
+ * the LAST prior record of that identity, the one the seed left standing,
428
+ * because the last write wins. Several key-less records of one scope were
429
+ * already collapsed to that one by the reducer before this existed; that
430
+ * limit predates the overlay and is not papered over here.
431
+ * - **Unchanged** means the known fields a turn can write are deep-equal:
432
+ * `scope`, `key`, `value`, `reason`, `durable`, `turnId` and `_meta`
433
+ * (draft.8: a `memory.write` sets it, and a patch can replace it without
434
+ * touching the value or the turnId, so a `_meta`-only change must count as
435
+ * a change or the prior `_meta` would be persisted over it). A live write
436
+ * stamps the current turn's `turnId`, so a turn that rewrites the SAME value
437
+ * still changes the record, and the fold's record wins (the unknown members
438
+ * described the previous write). **`threadId` is deliberately left out:** a
439
+ * `memory.write` has no `threadId` on its event arm, so no turn can set it and
440
+ * the seed cannot carry it. Comparing it would mark every stored record that
441
+ * has one as changed, which is exactly the loss this prevents.
442
+ * - Records the turn removed are absent from the fold and stay absent; a
443
+ * record the turn added has no prior and is the fold's own.
444
+ */
445
+ export function keepUnchangedThreadMemory(priorView, folded) {
446
+ const prior = new Map();
447
+ for (const rec of priorView)
448
+ prior.set(memoryIdentity(rec), rec);
449
+ return folded.map((rec) => {
450
+ const before = prior.get(memoryIdentity(rec));
451
+ if (before === undefined)
452
+ return rec;
453
+ const unchanged = before.scope === rec.scope &&
454
+ before.key === rec.key &&
455
+ jsonEqual(before.value, rec.value) &&
456
+ before.reason === rec.reason &&
457
+ before.durable === rec.durable &&
458
+ before.turnId === rec.turnId &&
459
+ jsonEqual(before._meta, rec._meta);
460
+ return unchanged ? before : rec;
461
+ });
462
+ }
285
463
  /**
286
464
  * Synthetic events that seed a reducer with prior latest-replace state before
287
465
  * folding a turn (design §8.1). Seeds state + thread-memory ONLY — never
@@ -294,6 +472,10 @@ export function reassembleFold(rows, snapshot) {
294
472
  * Each synthetic `memory.write` carries `turnId` so the reducer's SET handler
295
473
  * lands it back onto the AgMemoryRecord — without it, a re-seeded thread-memory
296
474
  * record loses its turnId and memory byte-identity breaks from turn 2 on.
475
+ * It carries the record's `_meta` the same way: AgJSON draft.8 declares
476
+ * `_meta` on both the record and `memory.write`, and the reducer folds it
477
+ * onto the record (set on create, replaced on update), so a record's host
478
+ * side-channel (e.g. `_meta.provenance`) survives every re-seeded turn.
297
479
  * (`threadId` is NOT carried: `memory.write` has no threadId on its event arm,
298
480
  * so it cannot round-trip through a live write — that is expected.)
299
481
  */
@@ -313,6 +495,7 @@ export function seedEventsForReducer(priorState, priorThreadMemory) {
313
495
  ...(rec.reason !== undefined ? { reason: rec.reason } : {}),
314
496
  ...(rec.durable !== undefined ? { durable: rec.durable } : {}),
315
497
  ...(rec.turnId !== undefined ? { turnId: rec.turnId } : {}),
498
+ ...(rec._meta !== undefined ? { _meta: rec._meta } : {}),
316
499
  });
317
500
  }
318
501
  return events;
package/dist/index.d.ts CHANGED
@@ -21,5 +21,5 @@ export type { HandoffEvent, ReportEvent, StoredHistoryMessage, ThreadMessageEven
21
21
  export { ThreadStore, type AppendFoldInput, type AppendFoldResult, type AppendMessageInput, type AppendMessageResult, type EnsureThreadInput, } from "./store.js";
22
22
  export { InMemoryThreadPersistence } from "./in-memory.js";
23
23
  export { HttpThreadPersistence, HttpThreadStoreError, type HttpThreadPersistenceOptions, type ThreadScope, } from "./http.js";
24
- export { agArtifactToCardRow, agMessageToRow, cardRowToAgArtifact, messageText, producingToolName, reassembleFold, rowToAgMessage, seedEventsForReducer, toolNamesByCallId, uiCardArtifactsFromMessages, type RowCtx, } from "./fold-rows.js";
24
+ export { agArtifactToCardRow, agMessageToRow, cardRowToAgArtifact, messageFullText, messageNarration, messageText, producingToolName, storedTextParts, type StoredTextParts, joinStoredThreadMemory, reassembleFold, rowToAgMessage, seedEventsForReducer, splitStoredThreadMemory, toolNamesByCallId, uiCardArtifactsFromMessages, type RowCtx, } from "./fold-rows.js";
25
25
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,YAAY,EACV,YAAY,EACZ,WAAW,EACX,oBAAoB,EACpB,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,qBAAqB,EACrB,SAAS,EACT,iBAAiB,GAClB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,WAAW,EACX,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,GACvB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EACpB,KAAK,4BAA4B,EACjC,KAAK,WAAW,GACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,mBAAmB,EACnB,WAAW,EACX,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,iBAAiB,EACjB,2BAA2B,EAC3B,KAAK,MAAM,GACZ,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,YAAY,EACV,YAAY,EACZ,WAAW,EACX,oBAAoB,EACpB,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,qBAAqB,EACrB,SAAS,EACT,iBAAiB,GAClB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,WAAW,EACX,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,GACvB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EACpB,KAAK,4BAA4B,EACjC,KAAK,WAAW,GACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,mBAAmB,EACnB,eAAe,EACf,gBAAgB,EAChB,WAAW,EACX,iBAAiB,EACjB,eAAe,EACf,KAAK,eAAe,EACpB,sBAAsB,EACtB,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,uBAAuB,EACvB,iBAAiB,EACjB,2BAA2B,EAC3B,KAAK,MAAM,GACZ,MAAM,gBAAgB,CAAC"}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export { ThreadStore, } from "./store.js";
2
2
  export { InMemoryThreadPersistence } from "./in-memory.js";
3
3
  export { HttpThreadPersistence, HttpThreadStoreError, } from "./http.js";
4
- export { agArtifactToCardRow, agMessageToRow, cardRowToAgArtifact, messageText, producingToolName, reassembleFold, rowToAgMessage, seedEventsForReducer, toolNamesByCallId, uiCardArtifactsFromMessages, } from "./fold-rows.js";
4
+ export { agArtifactToCardRow, agMessageToRow, cardRowToAgArtifact, messageFullText, messageNarration, messageText, producingToolName, storedTextParts, joinStoredThreadMemory, reassembleFold, rowToAgMessage, seedEventsForReducer, splitStoredThreadMemory, toolNamesByCallId, uiCardArtifactsFromMessages, } from "./fold-rows.js";
package/dist/rows.d.ts CHANGED
@@ -123,8 +123,21 @@ export interface ThreadSnapshotRow {
123
123
  userId: string;
124
124
  /** AgReduceResult.state — opaque working blob. */
125
125
  workingState?: JsonValue;
126
- /** AgReduceResult.memory filtered to scope='thread'. */
126
+ /**
127
+ * The thread's memory records this runtime reads: `AgReduceResult.memory`
128
+ * filtered to scope='thread'. A binding returns the readable view here, and
129
+ * stored elements it could not read travel in {@link carriedThreadMemory}.
130
+ */
127
131
  threadMemory: AgMemoryRecord[];
132
+ /**
133
+ * Stored thread-memory elements this runtime does not read or own: a record
134
+ * written in a newer vocabulary (an unknown scope, a missing value) or of
135
+ * another scope, verbatim and in stored order. A binding splits them off on
136
+ * read (`splitStoredThreadMemory`) and joins them back into the one stored
137
+ * array on write (`joinStoredThreadMemory`), so they are never dropped at
138
+ * rest by a runtime that cannot read them. Absent = none.
139
+ */
140
+ carriedThreadMemory?: JsonValue[];
128
141
  lastTurnId?: string;
129
142
  updatedAt: string;
130
143
  }
@@ -1 +1 @@
1
- {"version":3,"file":"rows.d.ts","sourceRoot":"","sources":["../src/rows.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"rows.d.ts","sourceRoot":"","sources":["../src/rows.ts"],"names":[],"mappings":"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"}
package/dist/store.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AgReduceResult } from "@silverprotocol/core";
1
+ import type { AgMemoryRecord, AgReduceResult, JsonValue } from "@silverprotocol/core";
2
2
  import type { ThreadMessageEvent, StoredHistoryMessage, ThreadMessageKind, ThreadMessageRole, ThreadPersistencePort, ThreadSnapshotRow } from "./rows.js";
3
3
  export interface EnsureThreadInput {
4
4
  /** Client-supplied thread id (localStorage). Absent on first contact. */
@@ -58,6 +58,19 @@ export interface AppendFoldInput {
58
58
  * {absent, true} vocabulary as {@link AppendMessageInput.untrustedOrigin}.
59
59
  */
60
60
  untrustedOrigin?: boolean;
61
+ /**
62
+ * The prior snapshot's `carriedThreadMemory`: stored thread-memory elements
63
+ * this runtime did not read or own. Written back unchanged beside the fold's
64
+ * own thread records, so they are never dropped at rest. Absent = none.
65
+ */
66
+ carriedThreadMemory?: readonly JsonValue[];
67
+ /**
68
+ * The prior snapshot's `threadMemory`: the view the reducer was seeded
69
+ * from. A record this turn did not change is persisted as this prior record,
70
+ * with the members it was stored with (`keepUnchangedThreadMemory`), never as
71
+ * the fold's rebuild of it. Absent = the fold's records as they are.
72
+ */
73
+ priorThreadMemory?: readonly AgMemoryRecord[];
61
74
  }
62
75
  export interface AppendFoldResult {
63
76
  messageSeqs: number[];
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAQ3D,OAAO,KAAK,EACV,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EAEjB,qBAAqB,EAErB,iBAAiB,EAClB,MAAM,WAAW,CAAC;AAOnB,MAAM,WAAW,iBAAiB;IAChC,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yCAAyC;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,iBAAiB,CAAC;IACxB,yEAAyE;IACzE,OAAO,EAAE,OAAO,CAAC;IACjB,iEAAiE;IACjE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,eAAe,EAAE,MAAM,CAAC;IACxB,IAAI,CAAC,EAAE,iBAAiB,CAAC;IACzB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,KAAK,CAAC,EAAE,kBAAkB,CAAC;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,0EAA0E;IAC1E,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,cAAc,CAAC;IACrB,2EAA2E;IAC3E,mBAAmB,EAAE,MAAM,CAAC;IAC5B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,kEAAkE;IAClE,oBAAoB,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAC;CAClB;AAMD,qBAAa,WAAW;IACV,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,qBAAqB;IAEtD;;;;;;;;;;;;;OAaG;IACG,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC;IA4B7D;;;;;;;OAOG;IACG,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKpE,4EAA4E;IACtE,WAAW,CACf,QAAQ,EAAE,MAAM,EAChB,KAAK,GAAE,MAA8B,GACpC,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAqBlC;;;;OAIG;IACG,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC;IA8B5E,+EAA+E;IACzE,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IAI3E;;;;;;;;;;;;;;;;;OAiBG;IACG,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC;CAiGpE"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAUtF,OAAO,KAAK,EACV,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EAEjB,qBAAqB,EAErB,iBAAiB,EAClB,MAAM,WAAW,CAAC;AAOnB,MAAM,WAAW,iBAAiB;IAChC,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yCAAyC;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,iBAAiB,CAAC;IACxB,yEAAyE;IACzE,OAAO,EAAE,OAAO,CAAC;IACjB,iEAAiE;IACjE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,eAAe,EAAE,MAAM,CAAC;IACxB,IAAI,CAAC,EAAE,iBAAiB,CAAC;IACzB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,KAAK,CAAC,EAAE,kBAAkB,CAAC;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,0EAA0E;IAC1E,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,cAAc,CAAC;IACrB,2EAA2E;IAC3E,mBAAmB,EAAE,MAAM,CAAC;IAC5B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;IAC3C;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;CAC/C;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,kEAAkE;IAClE,oBAAoB,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAC;CAClB;AAMD,qBAAa,WAAW;IACV,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,qBAAqB;IAEtD;;;;;;;;;;;;;OAaG;IACG,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC;IA4B7D;;;;;;;OAOG;IACG,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKpE,4EAA4E;IACtE,WAAW,CACf,QAAQ,EAAE,MAAM,EAChB,KAAK,GAAE,MAA8B,GACpC,OAAO,CAAC,oBAAoB,EAAE,CAAC;IA2BlC;;;;OAIG;IACG,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC;IA8B5E,+EAA+E;IACzE,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IAI3E;;;;;;;;;;;;;;;;;OAiBG;IACG,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC;CAqGpE"}
package/dist/store.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * DynamoDB).
9
9
  */
10
10
  import { randomUUID } from "node:crypto";
11
- import { agMessageToRow, agArtifactToCardRow, producingToolName, toolNamesByCallId, uiCardArtifactsFromMessages, } from "./fold-rows.js";
11
+ import { agMessageToRow, agArtifactToCardRow, keepUnchangedThreadMemory, producingToolName, toolNamesByCallId, uiCardArtifactsFromMessages, storedTextParts, } from "./fold-rows.js";
12
12
  /** Max history messages fed back as context — bounds latency on long threads. */
13
13
  const DEFAULT_HISTORY_LIMIT = 40;
14
14
  const PREVIEW_MAX_LEN = 240;
@@ -85,11 +85,17 @@ export class ThreadStore {
85
85
  // Known trade-off: cards still consume the DynamoDB Limit before this
86
86
  // filter, so card-heavy threads under-fill the window (bounded, most-
87
87
  // recent-first; revisit with an over-fetch if it bites).
88
+ // The model's text is EVERY text block from the stored message, narration
89
+ // included (`storedTextParts(...).all`): the row's `text` is the answer only
90
+ // since narration split out of it, so reading `text` here would change what
91
+ // the model is replayed. For rows written before the split, `all` is the same
92
+ // bytes their `text` holds. A row whose content is not a stored AgMessage
93
+ // (an event, a managed-adapter row) keeps its `text`.
88
94
  return rows.filter((r) => r.kind !== 'card').map((r) => ({
89
95
  seq: r.seq,
90
96
  authorRole: r.authorRole,
91
97
  kind: r.kind,
92
- text: r.text ?? null,
98
+ text: storedTextParts(r.content)?.all || (r.text ?? null),
93
99
  content: r.content ?? null,
94
100
  at: r.at,
95
101
  }));
@@ -161,7 +167,8 @@ export class ThreadStore {
161
167
  const msg = fold.messages[i];
162
168
  const clientMessageId = sentinelClaimed ? `${clientMessageIdBase}#agent#${i}` : sentinelKey;
163
169
  sentinelClaimed = true;
164
- const text = msg.content.reduce((acc, b) => (b.type === 'text' ? acc + b.text : acc), '');
170
+ // The preview is the ANSWER: interim narration (`phase: "interim"`) never becomes a thread's preview.
171
+ const text = msg.content.reduce((acc, b) => (b.type === 'text' && b.phase !== 'interim' ? acc + b.text : acc), '');
165
172
  const now = new Date().toISOString();
166
173
  // Text-less agent messages (tool-call + tool-result only — the canonical
167
174
  // card-producing turn) must not blank the preview: '' takes the SET
@@ -210,7 +217,7 @@ export class ThreadStore {
210
217
  }, producingToolName(art, toolNames)));
211
218
  artifactSeqs.push(seq);
212
219
  }
213
- const threadMemory = fold.memory.filter((m) => m.scope === 'thread');
220
+ const threadMemory = keepUnchangedThreadMemory(input.priorThreadMemory ?? [], fold.memory.filter((m) => m.scope === 'thread'));
214
221
  const droppedDurableMemory = fold.memory.length - threadMemory.length;
215
222
  if (skipSnapshot !== true) {
216
223
  const lastTurnId = fold.turns.length ? fold.turns[fold.turns.length - 1].turnId : undefined;
@@ -218,6 +225,9 @@ export class ThreadStore {
218
225
  threadId,
219
226
  userId,
220
227
  threadMemory,
228
+ ...(input.carriedThreadMemory !== undefined && input.carriedThreadMemory.length > 0
229
+ ? { carriedThreadMemory: [...input.carriedThreadMemory] }
230
+ : {}),
221
231
  updatedAt: new Date().toISOString(),
222
232
  ...(fold.state !== undefined ? { workingState: fold.state } : {}),
223
233
  ...(lastTurnId ? { lastTurnId } : {}),
@@ -1 +1 @@
1
- {"version":3,"file":"contract-suite.d.ts","sourceRoot":"","sources":["../../src/testing/contract-suite.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,qBAAqB,EAAa,MAAM,YAAY,CAAC;AAEnE,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,qBAAqB,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACtC;AAiCD;;;GAGG;AACH,wBAAgB,iCAAiC,CAC/C,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,OAAO,CAAC,wBAAwB,CAAC,GAC5C,IAAI,CAmJN"}
1
+ {"version":3,"file":"contract-suite.d.ts","sourceRoot":"","sources":["../../src/testing/contract-suite.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,qBAAqB,EAAa,MAAM,YAAY,CAAC;AAEnE,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,qBAAqB,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACtC;AAiCD;;;GAGG;AACH,wBAAgB,iCAAiC,CAC/C,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,OAAO,CAAC,wBAAwB,CAAC,GAC5C,IAAI,CA0KN"}
@@ -174,5 +174,27 @@ export function runThreadPersistenceContractSuite(name, make) {
174
174
  expect(snap?.workingState).toBeUndefined();
175
175
  });
176
176
  });
177
+ it("carried thread memory round-trips unchanged beside the readable records", async () => {
178
+ await withHarness(make, async (port) => {
179
+ await port.createThread(threadRow("t1"));
180
+ // Elements this runtime cannot read or does not own: an unknown scope,
181
+ // a record with no value, a readable record of another scope.
182
+ const carried = [
183
+ { scope: "team", key: "plan", value: { tier: "pro" } },
184
+ { scope: "thread", key: "orphan" },
185
+ { scope: "user", key: "name", value: "Ada" },
186
+ ];
187
+ await port.putSnapshot({
188
+ threadId: "t1",
189
+ userId: "g_contract",
190
+ threadMemory: [{ scope: "thread", key: "k", value: "v1" }],
191
+ carriedThreadMemory: carried,
192
+ updatedAt: "2026-08-07T00:00:03.000Z",
193
+ });
194
+ const snap = await port.getSnapshot("t1");
195
+ expect(snap?.threadMemory).toEqual([{ scope: "thread", key: "k", value: "v1" }]);
196
+ expect(JSON.stringify(snap?.carriedThreadMemory)).toBe(JSON.stringify(carried));
197
+ });
198
+ });
177
199
  });
178
200
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guuey/threads",
3
- "version": "0.28.1",
3
+ "version": "0.30.0",
4
4
  "description": "Universal session/thread persistence for AgJSON agents — the ThreadStore contract (append-fold, history, snapshots), the fold↔row mapping, and an in-memory binding. Guuey's hosted runtime is one binding; implement the port against your own store and run the exported contract suite for the same guarantees.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -22,8 +22,8 @@
22
22
  }
23
23
  },
24
24
  "dependencies": {
25
- "@silverprotocol/core": "0.7.2",
26
- "@guuey/mcp-apps-host": "0.28.1"
25
+ "@silverprotocol/core": "0.11.0",
26
+ "@guuey/mcp-apps-host": "0.30.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^24.0.0",