@guuey/threads 0.2.3 → 0.3.1

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.
@@ -13,7 +13,13 @@ export interface RowCtx {
13
13
  at: string;
14
14
  clientMessageId: string;
15
15
  }
16
- /** Plain-text projection of an AgMessage — concatenated text blocks. */
16
+ /**
17
+ * Plain-text projection of an AgMessage. Distinct text blocks are distinct
18
+ * paragraphs, so they join with a paragraph break — the same block-boundary
19
+ * contract as `@guuey/agent-client`'s live flat-text fold (guuey#98); raw
20
+ * concatenation jammed "…instead.Here's your packing…". Empty blocks
21
+ * contribute nothing (no stacked separators).
22
+ */
17
23
  export declare function messageText(msg: AgMessage): string;
18
24
  export declare function agMessageToRow(msg: AgMessage, ctx: RowCtx & {
19
25
  turnRecord?: AgTurnRecord;
@@ -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;AAE3G,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;CACzB;AASD,wEAAwE;AACxE,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,CAelB;AAgDD,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,UAAU,EAAE,CAqB/E;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,gBAAgB,CAYlF;AAED;;;;;GAKG;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":"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;CACzB;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,CAyBlB;AAkDD,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,UAAU,EAAE,CA2B/E;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,gBAAgB,CAqBlF;AAOD;;;;;GAKG;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"}
package/dist/fold-rows.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { AgMessage as AgMessageSchema } from "@silverprotocol/core";
2
+ import { asUiResource, scanProviderRawForUiResource, uiLocator } from "@guuey/mcp-apps-host/narrowing";
2
3
  /** AgRole → the row's coarse authorRole projection (secondary; role of record lives in content). */
3
4
  function roleToAuthor(role) {
4
5
  if (role === "user")
@@ -7,14 +8,20 @@ function roleToAuthor(role) {
7
8
  return "system";
8
9
  return "agent"; // assistant | tool
9
10
  }
10
- /** Plain-text projection of an AgMessage — concatenated text blocks. */
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).
17
+ */
11
18
  export function messageText(msg) {
12
- let out = "";
19
+ const parts = [];
13
20
  for (const block of msg.content) {
14
- if (block.type === "text")
15
- out += block.text;
21
+ if (block.type === "text" && block.text)
22
+ parts.push(block.text);
16
23
  }
17
- return out;
24
+ return parts.join("\n\n");
18
25
  }
19
26
  export function agMessageToRow(msg, ctx) {
20
27
  const text = messageText(msg);
@@ -28,56 +35,66 @@ export function agMessageToRow(msg, ctx) {
28
35
  kind,
29
36
  authorRole: roleToAuthor(msg.role),
30
37
  ...(text ? { text } : {}),
31
- content: msg,
38
+ // guuey#122: the ONE deliberate byte-identity exception — tool-result
39
+ // `_meta` (live-turn mount material, incl. short-TTL credentials) is
40
+ // stripped before persistence, on the message row exactly as on the
41
+ // card projection.
42
+ content: messageWithoutToolResultMeta(msg),
43
+ // SANCTIONED EXCEPTION to the #122 no-short-TTL rule (explicit decision,
44
+ // 2026-08-08): the turnRecord persists verbatim, INCLUDING paused-outcome
45
+ // asks[].token/expiresAt — reassembleFold round-trips turns into the
46
+ // reducer seed, so stripping could orphan a paused turn's resume after a
47
+ // reload, and the token authorizes the thread owner's OWN resume (not a
48
+ // cross-trust credential like a render wsToken). Revisit on guuey#122.
32
49
  ...(ctx.turnRecord ? { aiContext: ctx.turnRecord } : {}),
33
50
  };
34
51
  }
52
+ /** Strip every tool-result block's `_meta` (guuey#122); identity when none carried one. */
53
+ function messageWithoutToolResultMeta(msg) {
54
+ let changed = false;
55
+ const content = msg.content.map((block) => {
56
+ if (block.type !== "tool-result")
57
+ return block;
58
+ const stripped = withoutToolResultMeta(block);
59
+ if (stripped !== block)
60
+ changed = true;
61
+ return stripped;
62
+ });
63
+ return changed ? { ...msg, content } : msg;
64
+ }
35
65
  /**
36
66
  * MCP-App cards ride `tool-result` blocks' UI channels inside AgMessages —
37
67
  * the Claude facet emits NO `artifact.*` events, so `fold.artifacts` alone
38
68
  * misses every generative-UI card and nothing ever wrote a `kind:'card'`
39
69
  * row for them (guuey#86: cards never rehydrated after a reload). Project
40
70
  * UI-carrying tool-result blocks into synthetic AgArtifacts so the
41
- * EXISTING card-row lane persists them; the client's `cardCardMount`
71
+ * EXISTING card-row lane persists them; the client's `snapshotViewMount`
42
72
  * mounts `{ parts: [block] }` through its inline arm unchanged. Facets
43
73
  * that DO emit artifact events don't stamp `uiData` on tool-results, so
44
74
  * the two sources don't double-write for one card.
45
75
  *
46
- * The narrowing MIRRORS `@guuey/agent-client`'s `toolResultUiResource`
47
- * (@guuey/agent-client's block-ui.ts keep the two in sync):
76
+ * The narrowing is IMPORTED from `@guuey/mcp-apps-host` (the SEP-1865 Host
77
+ * package)one implementation, no mirror to keep in sync:
48
78
  * 1. `uiData` carrying an MCP resource payload (`{uri, text|blob}`,
49
79
  * inlined directly or wrapped as `{ resource: {...} }`) — the explicit
50
80
  * surface channel, no `ui://` gate on purpose;
51
81
  * 2. a `ui://` resource degraded into a `provider-raw` content part —
52
82
  * gated on the scheme, because provider-raw is a lossy catch-all.
53
83
  */
54
- function isJsonObject(v) {
55
- return typeof v === "object" && v !== null && !Array.isArray(v);
56
- }
57
- function isResourcePayload(v) {
58
- if (!isJsonObject(v))
59
- return false;
60
- if (typeof v.uri !== "string")
61
- return false;
62
- return typeof v.text === "string" || typeof v.blob === "string";
63
- }
64
- function resourceUri(v) {
65
- if (!isJsonObject(v))
66
- return undefined;
67
- return typeof v.uri === "string" ? v.uri : undefined;
68
- }
69
- function uiDataCarriesResource(uiData) {
70
- if (!isJsonObject(uiData))
71
- return false;
72
- return isResourcePayload(uiData) || isResourcePayload(uiData.resource);
73
- }
74
- function providerRawCarriesUiResource(raw) {
75
- if (!isJsonObject(raw))
76
- return false;
77
- const candidate = raw.resource !== undefined ? raw.resource : raw;
78
- if (!isResourcePayload(candidate))
79
- return false;
80
- return resourceUri(candidate)?.startsWith("ui://") === true;
84
+ /**
85
+ * Persistence boundary rule (guuey#122): a tool-result block's `_meta` is
86
+ * live-turn MOUNT/TRANSPORT material — vendor slices there routinely carry
87
+ * short-TTL credentials (ggui's render bootstrap `wsToken` is the concrete
88
+ * case) and are dead on arrival when replayed, so `_meta` NEVER persists.
89
+ * Durable identity belongs in `uiData`/`content`, which survive untouched.
90
+ * Deliberately vendor-agnostic: no `_meta` key list to maintain, and the
91
+ * next vendor's expiring credential is unpersistable by construction.
92
+ */
93
+ function withoutToolResultMeta(block) {
94
+ if (block._meta === undefined)
95
+ return block;
96
+ const { _meta: _dropped, ...rest } = block;
97
+ return rest;
81
98
  }
82
99
  export function uiCardArtifactsFromMessages(messages) {
83
100
  const artifacts = [];
@@ -86,21 +103,33 @@ export function uiCardArtifactsFromMessages(messages) {
86
103
  const block = msg.content[i];
87
104
  if (block.type !== "tool-result")
88
105
  continue;
89
- const carriesUi = uiDataCarriesResource(block.uiData) ||
90
- block.content.some((part) => part.type === "provider-raw" && providerRawCarriesUiResource(part.raw));
106
+ // guuey#122: a ui:// locator (uiData.resourceUri — ggui renders are
107
+ // one producer) persists as a PLACEHOLDER row: the locator without
108
+ // mount material (mount = re-fetch of the resourceUri). Before this,
109
+ // locator-only results persisted nothing and vanished from history.
110
+ const carriesUi = asUiResource(block.uiData) !== undefined ||
111
+ uiLocator(block.uiData) !== undefined ||
112
+ block.content.some((part) => part.type === "provider-raw" && scanProviderRawForUiResource(part.raw) !== undefined);
91
113
  if (!carriesUi)
92
114
  continue;
93
115
  artifacts.push({
94
116
  artifactId: `${msg.id}#ui#${i}`,
95
117
  turnId: msg.turnId ?? "",
96
118
  threadId: msg.threadId ?? "",
97
- parts: [block],
119
+ parts: [withoutToolResultMeta(block)],
98
120
  });
99
121
  }
100
122
  }
101
123
  return artifacts;
102
124
  }
103
125
  export function agArtifactToCardRow(art, ctx) {
126
+ // guuey#122: the projection lane strips tool-result _meta upstream, but
127
+ // REAL artifact.* events land here verbatim — apply the same persistence
128
+ // boundary to any tool-result-typed part (and the artifact-level _meta),
129
+ // so no lane can persist a short-TTL credential.
130
+ const parts = art.parts.map((part) => isJsonObjectLike(part) && part.type === "tool-result" ? withoutToolResultMeta(part) : part);
131
+ const { _meta: _artifactMeta, ...artRest } = art;
132
+ const snapshot = { ...artRest, parts };
104
133
  return {
105
134
  threadId: ctx.threadId,
106
135
  seq: ctx.seq,
@@ -110,9 +139,13 @@ export function agArtifactToCardRow(art, ctx) {
110
139
  kind: "card",
111
140
  authorRole: "agent",
112
141
  content: { producedInTurnId: art.turnId },
113
- cardSnapshot: art,
142
+ cardSnapshot: snapshot,
114
143
  };
115
144
  }
145
+ /** Local structural check (narrow enough for the strip dispatch above). */
146
+ function isJsonObjectLike(v) {
147
+ return typeof v === "object" && v !== null && !Array.isArray(v);
148
+ }
116
149
  /**
117
150
  * Reconstruct an AgMessage from a row. Agent-fold rows store the verbatim
118
151
  * AgMessage in `content`; user/system rows (persisted up-front) store plain
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guuey/threads",
3
- "version": "0.2.3",
3
+ "version": "0.3.1",
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,7 +22,8 @@
22
22
  }
23
23
  },
24
24
  "dependencies": {
25
- "@silverprotocol/core": "0.4.1"
25
+ "@silverprotocol/core": "0.4.1",
26
+ "@guuey/mcp-apps-host": "0.3.1"
26
27
  },
27
28
  "devDependencies": {
28
29
  "@types/node": "^24.0.0",