@guuey/threads 0.29.0 → 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;
@@ -102,6 +126,39 @@ export declare function splitStoredThreadMemory(stored: readonly JsonValue[]): P
102
126
  * turn, so write order stays time order.
103
127
  */
104
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[];
105
162
  /**
106
163
  * Synthetic events that seed a reducer with prior latest-replace state before
107
164
  * folding a turn (design §8.1). Seeds state + thread-memory ONLY — never
@@ -114,6 +171,10 @@ export declare function joinStoredThreadMemory(row: Pick<ThreadSnapshotRow, "thr
114
171
  * Each synthetic `memory.write` carries `turnId` so the reducer's SET handler
115
172
  * lands it back onto the AgMemoryRecord — without it, a re-seeded thread-memory
116
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.
117
178
  * (`threadId` is NOT carried: `memory.write` has no threadId on its event arm,
118
179
  * so it cannot round-trip through a live write — that is expected.)
119
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,uBAAuB,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,GAAG,IAAI,CAAC,iBAAiB,EAAE,cAAc,CAAC,GAAG;IAC/G,mBAAmB,EAAE,SAAS,EAAE,CAAC;CAClC,CAoBA;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,IAAI,CAAC,iBAAiB,EAAE,cAAc,GAAG,qBAAqB,CAAC,GAAG,SAAS,EAAE,CAExH;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
@@ -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";
@@ -330,6 +388,78 @@ export function splitStoredThreadMemory(stored) {
330
388
  export function joinStoredThreadMemory(row) {
331
389
  return [...(row.carriedThreadMemory ?? []), ...row.threadMemory];
332
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
+ }
333
463
  /**
334
464
  * Synthetic events that seed a reducer with prior latest-replace state before
335
465
  * folding a turn (design §8.1). Seeds state + thread-memory ONLY — never
@@ -342,6 +472,10 @@ export function joinStoredThreadMemory(row) {
342
472
  * Each synthetic `memory.write` carries `turnId` so the reducer's SET handler
343
473
  * lands it back onto the AgMemoryRecord — without it, a re-seeded thread-memory
344
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.
345
479
  * (`threadId` is NOT carried: `memory.write` has no threadId on its event arm,
346
480
  * so it cannot round-trip through a live write — that is expected.)
347
481
  */
@@ -361,6 +495,7 @@ export function seedEventsForReducer(priorState, priorThreadMemory) {
361
495
  ...(rec.reason !== undefined ? { reason: rec.reason } : {}),
362
496
  ...(rec.durable !== undefined ? { durable: rec.durable } : {}),
363
497
  ...(rec.turnId !== undefined ? { turnId: rec.turnId } : {}),
498
+ ...(rec._meta !== undefined ? { _meta: rec._meta } : {}),
364
499
  });
365
500
  }
366
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, joinStoredThreadMemory, reassembleFold, rowToAgMessage, seedEventsForReducer, splitStoredThreadMemory, 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,sBAAsB,EACtB,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,uBAAuB,EACvB,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, joinStoredThreadMemory, reassembleFold, rowToAgMessage, seedEventsForReducer, splitStoredThreadMemory, 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/store.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AgReduceResult, JsonValue } 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. */
@@ -64,6 +64,13 @@ export interface AppendFoldInput {
64
64
  * own thread records, so they are never dropped at rest. Absent = none.
65
65
  */
66
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[];
67
74
  }
68
75
  export interface AppendFoldResult {
69
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,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAQtE,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;CAC5C;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;CAoGpE"}
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guuey/threads",
3
- "version": "0.29.0",
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.9.0",
26
- "@guuey/mcp-apps-host": "0.29.0"
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",