@sumicom/quicksave 0.8.5 → 0.8.7

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.
Files changed (193) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +31 -93
  3. package/dist/ai/asyncQueue.d.ts.map +1 -1
  4. package/dist/ai/asyncQueue.js +2 -0
  5. package/dist/ai/asyncQueue.js.map +1 -1
  6. package/dist/ai/attachmentStaging.d.ts +71 -0
  7. package/dist/ai/attachmentStaging.d.ts.map +1 -0
  8. package/dist/ai/attachmentStaging.js +243 -0
  9. package/dist/ai/attachmentStaging.js.map +1 -0
  10. package/dist/ai/attachmentStore.d.ts +40 -0
  11. package/dist/ai/attachmentStore.d.ts.map +1 -0
  12. package/dist/ai/attachmentStore.js +173 -0
  13. package/dist/ai/attachmentStore.js.map +1 -0
  14. package/dist/ai/cardBuilder.d.ts +6 -6
  15. package/dist/ai/cardBuilder.d.ts.map +1 -1
  16. package/dist/ai/cardBuilder.js +164 -16
  17. package/dist/ai/cardBuilder.js.map +1 -1
  18. package/dist/ai/claudeCliProvider.d.ts +34 -15
  19. package/dist/ai/claudeCliProvider.d.ts.map +1 -1
  20. package/dist/ai/claudeCliProvider.js +108 -64
  21. package/dist/ai/claudeCliProvider.js.map +1 -1
  22. package/dist/ai/claudeCodeProvider.d.ts.map +1 -1
  23. package/dist/ai/claudeCodeProvider.js +2 -0
  24. package/dist/ai/claudeCodeProvider.js.map +1 -1
  25. package/dist/ai/claudeSdkProvider.d.ts +6 -3
  26. package/dist/ai/claudeSdkProvider.d.ts.map +1 -1
  27. package/dist/ai/claudeSdkProvider.js +97 -29
  28. package/dist/ai/claudeSdkProvider.js.map +1 -1
  29. package/dist/ai/codexAppServer/approvalMapping.d.ts.map +1 -1
  30. package/dist/ai/codexAppServer/approvalMapping.js.map +1 -1
  31. package/dist/ai/codexAppServer/cardAdapter.d.ts +0 -3
  32. package/dist/ai/codexAppServer/cardAdapter.d.ts.map +1 -1
  33. package/dist/ai/codexAppServer/cardAdapter.js +40 -5
  34. package/dist/ai/codexAppServer/cardAdapter.js.map +1 -1
  35. package/dist/ai/codexAppServer/index.d.ts.map +1 -1
  36. package/dist/ai/codexAppServer/index.js +2 -0
  37. package/dist/ai/codexAppServer/index.js.map +1 -1
  38. package/dist/ai/codexAppServer/overrideStore.d.ts.map +1 -1
  39. package/dist/ai/codexAppServer/overrideStore.js.map +1 -1
  40. package/dist/ai/codexAppServer/permissionMapping.d.ts.map +1 -1
  41. package/dist/ai/codexAppServer/permissionMapping.js.map +1 -1
  42. package/dist/ai/codexAppServer/processManager.d.ts.map +1 -1
  43. package/dist/ai/codexAppServer/processManager.js +2 -0
  44. package/dist/ai/codexAppServer/processManager.js.map +1 -1
  45. package/dist/ai/codexAppServer/provider.d.ts.map +1 -1
  46. package/dist/ai/codexAppServer/provider.js +9 -24
  47. package/dist/ai/codexAppServer/provider.js.map +1 -1
  48. package/dist/ai/codexAppServer/rpcClient.d.ts.map +1 -1
  49. package/dist/ai/codexAppServer/rpcClient.js.map +1 -1
  50. package/dist/ai/codexAppServer/schema/index.d.ts.map +1 -1
  51. package/dist/ai/codexAppServer/schema/index.js +2 -0
  52. package/dist/ai/codexAppServer/schema/index.js.map +1 -1
  53. package/dist/ai/codexAppServer/stdioTransport.d.ts.map +1 -1
  54. package/dist/ai/codexAppServer/stdioTransport.js.map +1 -1
  55. package/dist/ai/codexAppServer/tokenAccounting.d.ts.map +1 -1
  56. package/dist/ai/codexAppServer/tokenAccounting.js.map +1 -1
  57. package/dist/ai/codexAppServer/version.d.ts.map +1 -1
  58. package/dist/ai/codexAppServer/version.js +2 -0
  59. package/dist/ai/codexAppServer/version.js.map +1 -1
  60. package/dist/ai/codexLogin.d.ts.map +1 -1
  61. package/dist/ai/codexLogin.js +2 -0
  62. package/dist/ai/codexLogin.js.map +1 -1
  63. package/dist/ai/codexMcpProvider.d.ts.map +1 -1
  64. package/dist/ai/codexMcpProvider.js +7 -10
  65. package/dist/ai/codexMcpProvider.js.map +1 -1
  66. package/dist/ai/commitSummary.d.ts.map +1 -1
  67. package/dist/ai/commitSummary.js +2 -0
  68. package/dist/ai/commitSummary.js.map +1 -1
  69. package/dist/ai/commitSummaryCli.d.ts.map +1 -1
  70. package/dist/ai/commitSummaryCli.js +2 -0
  71. package/dist/ai/commitSummaryCli.js.map +1 -1
  72. package/dist/ai/commitSummaryStore.d.ts.map +1 -1
  73. package/dist/ai/commitSummaryStore.js +2 -0
  74. package/dist/ai/commitSummaryStore.js.map +1 -1
  75. package/dist/ai/contentBlocks.d.ts +9 -0
  76. package/dist/ai/contentBlocks.d.ts.map +1 -0
  77. package/dist/ai/contentBlocks.js +86 -0
  78. package/dist/ai/contentBlocks.js.map +1 -0
  79. package/dist/ai/debugLogger.d.ts.map +1 -1
  80. package/dist/ai/debugLogger.js +2 -0
  81. package/dist/ai/debugLogger.js.map +1 -1
  82. package/dist/ai/enrichEntry.d.ts.map +1 -1
  83. package/dist/ai/enrichEntry.js.map +1 -1
  84. package/dist/ai/pdfMeta.d.ts +8 -0
  85. package/dist/ai/pdfMeta.d.ts.map +1 -0
  86. package/dist/ai/pdfMeta.js +51 -0
  87. package/dist/ai/pdfMeta.js.map +1 -0
  88. package/dist/ai/provider.d.ts +21 -8
  89. package/dist/ai/provider.d.ts.map +1 -1
  90. package/dist/ai/provider.js.map +1 -1
  91. package/dist/ai/sandboxMcp.d.ts.map +1 -1
  92. package/dist/ai/sandboxMcp.js +2 -0
  93. package/dist/ai/sandboxMcp.js.map +1 -1
  94. package/dist/ai/sandboxMcpStdio.d.ts.map +1 -1
  95. package/dist/ai/sandboxMcpStdio.js +2 -0
  96. package/dist/ai/sandboxMcpStdio.js.map +1 -1
  97. package/dist/ai/sessionManager.d.ts +19 -7
  98. package/dist/ai/sessionManager.d.ts.map +1 -1
  99. package/dist/ai/sessionManager.js +81 -23
  100. package/dist/ai/sessionManager.js.map +1 -1
  101. package/dist/ai/sessionRegistry.d.ts.map +1 -1
  102. package/dist/ai/sessionRegistry.js +2 -0
  103. package/dist/ai/sessionRegistry.js.map +1 -1
  104. package/dist/ai/sessionStore.d.ts.map +1 -1
  105. package/dist/ai/sessionStore.js +2 -0
  106. package/dist/ai/sessionStore.js.map +1 -1
  107. package/dist/ai/systemPrompt.d.ts.map +1 -1
  108. package/dist/ai/systemPrompt.js +3 -0
  109. package/dist/ai/systemPrompt.js.map +1 -1
  110. package/dist/config.d.ts.map +1 -1
  111. package/dist/config.js +2 -0
  112. package/dist/config.js.map +1 -1
  113. package/dist/connection/connection.d.ts.map +1 -1
  114. package/dist/connection/connection.js +2 -0
  115. package/dist/connection/connection.js.map +1 -1
  116. package/dist/connection/e2eHarness.d.ts +1 -1
  117. package/dist/connection/e2eHarness.d.ts.map +1 -1
  118. package/dist/connection/e2eHarness.js +26 -8
  119. package/dist/connection/e2eHarness.js.map +1 -1
  120. package/dist/connection/fakeRelay.d.ts.map +1 -1
  121. package/dist/connection/fakeRelay.js +2 -0
  122. package/dist/connection/fakeRelay.js.map +1 -1
  123. package/dist/connection/pubsub.d.ts.map +1 -1
  124. package/dist/connection/pubsub.js +2 -0
  125. package/dist/connection/pubsub.js.map +1 -1
  126. package/dist/connection/relay.d.ts.map +1 -1
  127. package/dist/connection/relay.js +2 -0
  128. package/dist/connection/relay.js.map +1 -1
  129. package/dist/files/fileBrowser.d.ts.map +1 -1
  130. package/dist/files/fileBrowser.js +44 -2
  131. package/dist/files/fileBrowser.js.map +1 -1
  132. package/dist/git/operations.d.ts.map +1 -1
  133. package/dist/git/operations.js +2 -0
  134. package/dist/git/operations.js.map +1 -1
  135. package/dist/handlers/legacyBusAdapter.d.ts +1 -1
  136. package/dist/handlers/legacyBusAdapter.d.ts.map +1 -1
  137. package/dist/handlers/legacyBusAdapter.js +13 -1
  138. package/dist/handlers/legacyBusAdapter.js.map +1 -1
  139. package/dist/handlers/messageHandler.d.ts +23 -0
  140. package/dist/handlers/messageHandler.d.ts.map +1 -1
  141. package/dist/handlers/messageHandler.js +226 -14
  142. package/dist/handlers/messageHandler.js.map +1 -1
  143. package/dist/index.js +42 -2
  144. package/dist/index.js.map +1 -1
  145. package/dist/messageBus/busServerTransport.d.ts.map +1 -1
  146. package/dist/messageBus/busServerTransport.js +2 -0
  147. package/dist/messageBus/busServerTransport.js.map +1 -1
  148. package/dist/service/debugHttpServer.d.ts.map +1 -1
  149. package/dist/service/debugHttpServer.js +2 -0
  150. package/dist/service/debugHttpServer.js.map +1 -1
  151. package/dist/service/ensureDaemon.d.ts.map +1 -1
  152. package/dist/service/ensureDaemon.js +27 -2
  153. package/dist/service/ensureDaemon.js.map +1 -1
  154. package/dist/service/index.d.ts.map +1 -1
  155. package/dist/service/index.js +2 -0
  156. package/dist/service/index.js.map +1 -1
  157. package/dist/service/ipcClient.d.ts.map +1 -1
  158. package/dist/service/ipcClient.js +3 -1
  159. package/dist/service/ipcClient.js.map +1 -1
  160. package/dist/service/ipcServer.d.ts.map +1 -1
  161. package/dist/service/ipcServer.js +2 -0
  162. package/dist/service/ipcServer.js.map +1 -1
  163. package/dist/service/pushClient.d.ts.map +1 -1
  164. package/dist/service/pushClient.js +2 -0
  165. package/dist/service/pushClient.js.map +1 -1
  166. package/dist/service/run.d.ts.map +1 -1
  167. package/dist/service/run.js +14 -1
  168. package/dist/service/run.js.map +1 -1
  169. package/dist/service/singleton.d.ts +1 -0
  170. package/dist/service/singleton.d.ts.map +1 -1
  171. package/dist/service/singleton.js +5 -0
  172. package/dist/service/singleton.js.map +1 -1
  173. package/dist/service/stateStore.d.ts.map +1 -1
  174. package/dist/service/stateStore.js +2 -0
  175. package/dist/service/stateStore.js.map +1 -1
  176. package/dist/service/systemdUnit.d.ts +149 -0
  177. package/dist/service/systemdUnit.d.ts.map +1 -0
  178. package/dist/service/systemdUnit.js +322 -0
  179. package/dist/service/systemdUnit.js.map +1 -0
  180. package/dist/service/types.d.ts +6 -0
  181. package/dist/service/types.d.ts.map +1 -1
  182. package/dist/service/types.js +3 -1
  183. package/dist/service/types.js.map +1 -1
  184. package/dist/storage/eventStore.d.ts.map +1 -1
  185. package/dist/storage/eventStore.js +2 -0
  186. package/dist/storage/eventStore.js.map +1 -1
  187. package/dist/terminal/terminalManager.d.ts.map +1 -1
  188. package/dist/terminal/terminalManager.js +2 -0
  189. package/dist/terminal/terminalManager.js.map +1 -1
  190. package/dist/tombstoneCheck.d.ts.map +1 -1
  191. package/dist/tombstoneCheck.js +2 -0
  192. package/dist/tombstoneCheck.js.map +1 -1
  193. package/package.json +3 -3
@@ -0,0 +1,173 @@
1
+ // SPDX-FileCopyrightText: 2026 King Young Technology
2
+ // SPDX-License-Identifier: MIT
3
+ // ============================================================================
4
+ // Persistent attachment store on the agent.
5
+ //
6
+ // Once `attachmentStaging.consume(...)` materializes an `Attachment[]` for a
7
+ // `claude:start` / `claude:resume`, the messageHandler also writes each
8
+ // attachment to disk under
9
+ //
10
+ // <state>/attachments/<sessionId>/<attachmentId>.bin (base64-decoded bytes)
11
+ // <state>/attachments/<sessionId>/<attachmentId>.meta.json
12
+ //
13
+ // The PWA fetches bytes on demand via `attachment:fetch { sessionId, id }`,
14
+ // caches them in `attachmentCache` (L1/L2), and renders chip thumbnails
15
+ // without dragging the data through every card-history snapshot.
16
+ //
17
+ // Cleanup happens on session end-task: `removeSessionAttachments(sessionId)`
18
+ // nukes the directory.
19
+ // ============================================================================
20
+ import { mkdir, readFile, writeFile, appendFile, rm, readdir, stat } from 'fs/promises';
21
+ import { join } from 'path';
22
+ import { getAttachmentsDir } from '../service/singleton.js';
23
+ function dirFor(sessionId) {
24
+ return join(getAttachmentsDir(), sessionId);
25
+ }
26
+ function binPathFor(sessionId, attachmentId) {
27
+ return join(dirFor(sessionId), `${attachmentId}.bin`);
28
+ }
29
+ function metaPathFor(sessionId, attachmentId) {
30
+ return join(dirFor(sessionId), `${attachmentId}.meta.json`);
31
+ }
32
+ function turnManifestPathFor(sessionId) {
33
+ return join(dirFor(sessionId), 'turn-manifest.jsonl');
34
+ }
35
+ /**
36
+ * Persist a batch of resolved attachments under one session id. Idempotent
37
+ * per id — re-writing the same id overwrites both files.
38
+ */
39
+ export async function persistAttachments(sessionId, attachments) {
40
+ if (attachments.length === 0)
41
+ return;
42
+ await mkdir(dirFor(sessionId), { recursive: true });
43
+ await Promise.all(attachments.map(async (a) => {
44
+ const bytes = Buffer.from(a.data, 'base64');
45
+ await writeFile(binPathFor(sessionId, a.id), bytes);
46
+ const meta = {
47
+ id: a.id,
48
+ kind: a.kind,
49
+ mimeType: a.mimeType,
50
+ name: a.name,
51
+ size: a.size,
52
+ storedAt: Date.now(),
53
+ };
54
+ await writeFile(metaPathFor(sessionId, a.id), JSON.stringify(meta));
55
+ }));
56
+ // Append a turn-manifest line so the cardBuilder can rebind on JSONL replay
57
+ // by exact UUID instead of falling back to fuzzy (kind+size+name) match.
58
+ // One line per attachment-bearing user turn, in send order.
59
+ const line = JSON.stringify({ ids: attachments.map((a) => a.id) }) + '\n';
60
+ await appendFile(turnManifestPathFor(sessionId), line);
61
+ }
62
+ /**
63
+ * Read the per-session attachment turn manifest in send order. Returns an
64
+ * empty array if no manifest exists yet (e.g. a session whose attachments
65
+ * predate this code), letting callers fall back to fuzzy rebind.
66
+ */
67
+ export async function readTurnManifest(sessionId) {
68
+ let raw;
69
+ try {
70
+ raw = await readFile(turnManifestPathFor(sessionId), 'utf8');
71
+ }
72
+ catch {
73
+ return [];
74
+ }
75
+ const out = [];
76
+ for (const line of raw.split('\n')) {
77
+ if (!line.trim())
78
+ continue;
79
+ try {
80
+ const parsed = JSON.parse(line);
81
+ if (parsed && Array.isArray(parsed.ids) && parsed.ids.every((x) => typeof x === 'string')) {
82
+ out.push({ ids: parsed.ids });
83
+ }
84
+ }
85
+ catch {
86
+ // skip malformed line — don't break the whole replay
87
+ }
88
+ }
89
+ return out;
90
+ }
91
+ /**
92
+ * Read one attachment's bytes back, returning the wire `Attachment` shape
93
+ * (metadata + base64 data). Returns `null` if the attachment isn't on disk
94
+ * — e.g. session was end-tasked, or the JSONL had an attachment we didn't
95
+ * persist (cold-start session before this code shipped).
96
+ */
97
+ export async function loadAttachment(sessionId, attachmentId) {
98
+ let metaRaw;
99
+ let bytes;
100
+ try {
101
+ [metaRaw, bytes] = await Promise.all([
102
+ readFile(metaPathFor(sessionId, attachmentId), 'utf8'),
103
+ readFile(binPathFor(sessionId, attachmentId)),
104
+ ]);
105
+ }
106
+ catch {
107
+ return null;
108
+ }
109
+ let meta;
110
+ try {
111
+ meta = JSON.parse(metaRaw);
112
+ }
113
+ catch {
114
+ return null;
115
+ }
116
+ return {
117
+ id: meta.id,
118
+ kind: meta.kind,
119
+ mimeType: meta.mimeType,
120
+ name: meta.name,
121
+ size: meta.size,
122
+ data: bytes.toString('base64'),
123
+ };
124
+ }
125
+ /** Drop every persisted attachment for one session (called on end-task). */
126
+ export async function removeSessionAttachments(sessionId) {
127
+ try {
128
+ await rm(dirFor(sessionId), { recursive: true, force: true });
129
+ }
130
+ catch {
131
+ // best effort — orphaned files are tolerable, they're local-only
132
+ }
133
+ }
134
+ /**
135
+ * List metadata for every attachment persisted under a session id. Mainly
136
+ * for diagnostics / tests.
137
+ */
138
+ export async function listSessionAttachments(sessionId) {
139
+ let entries;
140
+ try {
141
+ entries = await readdir(dirFor(sessionId));
142
+ }
143
+ catch {
144
+ return [];
145
+ }
146
+ const metas = [];
147
+ for (const name of entries) {
148
+ if (!name.endsWith('.meta.json'))
149
+ continue;
150
+ try {
151
+ const raw = await readFile(join(dirFor(sessionId), name), 'utf8');
152
+ metas.push(JSON.parse(raw));
153
+ }
154
+ catch {
155
+ // skip unreadable metas
156
+ }
157
+ }
158
+ return metas;
159
+ }
160
+ /** True if the meta+bin pair exist on disk — without reading the bytes. */
161
+ export async function hasPersistedAttachment(sessionId, attachmentId) {
162
+ try {
163
+ await Promise.all([
164
+ stat(metaPathFor(sessionId, attachmentId)),
165
+ stat(binPathFor(sessionId, attachmentId)),
166
+ ]);
167
+ return true;
168
+ }
169
+ catch {
170
+ return false;
171
+ }
172
+ }
173
+ //# sourceMappingURL=attachmentStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachmentStore.js","sourceRoot":"","sources":["../../src/ai/attachmentStore.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import type { Card, CardId, CardEvent, CardHistoryResponse, PendingInputAttachment } from '@sumicom/quicksave-shared';
1
+ import type { Attachment, Card, CardId, CardEvent, CardHistoryResponse, PendingInputAttachment } from '@sumicom/quicksave-shared';
2
2
  /**
3
3
  * Load persisted card history for a memory-mode session.
4
4
  * Returns cards in insertion order, or empty array if none exist.
@@ -6,7 +6,6 @@ import type { Card, CardId, CardEvent, CardHistoryResponse, PendingInputAttachme
6
6
  export declare function loadPersistedCards(sessionId: string): Promise<Card[]>;
7
7
  export declare class StreamCardBuilder {
8
8
  private sessionId;
9
- private streamId;
10
9
  private cwd;
11
10
  private seq;
12
11
  private cards;
@@ -23,10 +22,10 @@ export declare class StreamCardBuilder {
23
22
  /** Token identifying an in-flight deferred clear. A later call (new turn, cancel)
24
23
  * replaces this token, causing the pending polling task to bail out. */
25
24
  private _pendingClearToken;
26
- constructor(sessionId: string, streamId: string, cwd: string);
25
+ constructor(sessionId: string, cwd: string);
27
26
  updateSessionId(sessionId: string): void;
28
- /** Start a new turn: update streamId and reset per-turn state, keep accumulated cards. */
29
- startNewTurn(streamId: string): void;
27
+ /** Start a new turn: reset per-turn state, keep accumulated cards. */
28
+ startNewTurn(): void;
30
29
  /** Clear all accumulated cards. Call after a turn completes and JSONL is flushed. */
31
30
  clearCards(): void;
32
31
  /**
@@ -68,7 +67,7 @@ export declare class StreamCardBuilder {
68
67
  private updateEvent;
69
68
  private appendTextEvent;
70
69
  private removeEvent;
71
- userMessage(text: string): CardEvent;
70
+ userMessage(text: string, attachments?: readonly Attachment[]): CardEvent;
72
71
  thinkingBlock(text: string): CardEvent;
73
72
  /**
74
73
  * Append text to current assistant_text card, or create a new one.
@@ -107,6 +106,7 @@ export declare class StreamCardBuilder {
107
106
  subagentProgress(agentId: string, toolUseId: string | undefined, toolUseCount?: number, lastToolName?: string): CardEvent | null;
108
107
  subagentEnd(agentId: string, toolUseId: string | undefined, status: 'completed' | 'failed' | 'stopped', summary?: string): CardEvent | null;
109
108
  systemMessage(text: string, subtype?: 'compacted' | 'cost' | 'error' | 'info' | 'warning'): CardEvent;
109
+ recoverySuggested(reason: string, action: 'compact', label: string): CardEvent;
110
110
  errorMessage(text: string): CardEvent;
111
111
  }
112
112
  export declare function buildCardsFromHistory(sessionId: string, cwd: string, offset?: number, limit?: number, cutoffBytes?: number): Promise<CardHistoryResponse>;
@@ -1 +1 @@
1
- {"version":3,"file":"cardBuilder.d.ts","sourceRoot":"","sources":["../../src/ai/cardBuilder.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cardBuilder.d.ts","sourceRoot":"","sources":["../../src/ai/cardBuilder.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,43 @@ import { createReadStream, existsSync } from 'fs';
4
4
  import { createInterface } from 'readline';
5
5
  import { homedir } from 'os';
6
6
  import { getCardHistoryDir } from '../service/singleton.js';
7
+ import { listSessionAttachments, readTurnManifest } from './attachmentStore.js';
8
+ /** Decoded byte length of a base64 string, accounting for `=` padding. */
9
+ function decodedBase64Bytes(base64) {
10
+ if (typeof base64 !== 'string')
11
+ return 0;
12
+ const len = base64.length;
13
+ const padding = base64.endsWith('==') ? 2 : base64.endsWith('=') ? 1 : 0;
14
+ return Math.floor((len * 3) / 4) - padding;
15
+ }
16
+ /** Pool that lets us look up a persisted attachment by `(kind, size)` —
17
+ * optionally name-tiebroken — and remove it so duplicates match correctly.
18
+ * Used to recover the original upload UUIDs for attachments rebuilt from
19
+ * JSONL (which doesn't carry our ids). */
20
+ function makeAttachmentResolver(metas) {
21
+ const pool = new Map();
22
+ for (const m of metas) {
23
+ const key = `${m.kind}:${m.size}`;
24
+ const list = pool.get(key) ?? [];
25
+ list.push(m);
26
+ pool.set(key, list);
27
+ }
28
+ return function consume(kind, size, name) {
29
+ const list = pool.get(`${kind}:${size}`);
30
+ if (!list || list.length === 0)
31
+ return null;
32
+ let idx = 0;
33
+ if (name !== undefined) {
34
+ const named = list.findIndex((m) => m.name === name);
35
+ if (named >= 0)
36
+ idx = named;
37
+ }
38
+ const [matched] = list.splice(idx, 1);
39
+ if (list.length === 0)
40
+ pool.delete(`${kind}:${size}`);
41
+ return matched;
42
+ };
43
+ }
7
44
  const TOOL_RESULT_TRUNCATE_LENGTH = 500;
8
45
  // ── Card history persistence (for memory-mode providers like Codex) ──
9
46
  function cardHistoryPath(sessionId) {
@@ -188,7 +225,6 @@ function extractToolResultText(content) {
188
225
  // ============================================================================
189
226
  export class StreamCardBuilder {
190
227
  sessionId;
191
- streamId;
192
228
  cwd;
193
229
  seq = 0;
194
230
  cards = new Map();
@@ -205,17 +241,15 @@ export class StreamCardBuilder {
205
241
  /** Token identifying an in-flight deferred clear. A later call (new turn, cancel)
206
242
  * replaces this token, causing the pending polling task to bail out. */
207
243
  _pendingClearToken = null;
208
- constructor(sessionId, streamId, cwd) {
244
+ constructor(sessionId, cwd) {
209
245
  this.sessionId = sessionId;
210
- this.streamId = streamId;
211
246
  this.cwd = cwd;
212
247
  }
213
248
  updateSessionId(sessionId) {
214
249
  this.sessionId = sessionId;
215
250
  }
216
- /** Start a new turn: update streamId and reset per-turn state, keep accumulated cards. */
217
- startNewTurn(streamId) {
218
- this.streamId = streamId;
251
+ /** Start a new turn: reset per-turn state, keep accumulated cards. */
252
+ startNewTurn() {
219
253
  this.currentTextCardId = null;
220
254
  }
221
255
  /** Clear all accumulated cards. Call after a turn completes and JSONL is flushed. */
@@ -344,11 +378,11 @@ export class StreamCardBuilder {
344
378
  this._jsonlCutoff = value;
345
379
  }
346
380
  nextId() {
347
- return `${this.sessionId}:${this.streamId}:${++this.seq}`;
381
+ return `${this.sessionId}:${++this.seq}`;
348
382
  }
349
383
  addEvent(card, afterCardId) {
350
384
  this.cards.set(card.id, card);
351
- return { type: 'add', streamId: this.streamId, sessionId: this.sessionId, card, afterCardId };
385
+ return { type: 'add', sessionId: this.sessionId, card, afterCardId };
352
386
  }
353
387
  updateEvent(cardId, patch) {
354
388
  const existing = this.cards.get(cardId);
@@ -365,23 +399,40 @@ export class StreamCardBuilder {
365
399
  bag[key] = value;
366
400
  }
367
401
  }
368
- return { type: 'update', streamId: this.streamId, sessionId: this.sessionId, cardId, patch };
402
+ return { type: 'update', sessionId: this.sessionId, cardId, patch };
369
403
  }
370
404
  appendTextEvent(cardId, text) {
371
405
  const existing = this.cards.get(cardId);
372
406
  if (existing && 'text' in existing) {
373
407
  existing.text += text;
374
408
  }
375
- return { type: 'append_text', streamId: this.streamId, sessionId: this.sessionId, cardId, text };
409
+ return { type: 'append_text', sessionId: this.sessionId, cardId, text };
376
410
  }
377
411
  removeEvent(cardId) {
378
412
  this.cards.delete(cardId);
379
- return { type: 'remove', streamId: this.streamId, sessionId: this.sessionId, cardId };
413
+ return { type: 'remove', sessionId: this.sessionId, cardId };
380
414
  }
381
415
  // ── Public: produce CardEvents from SDK data ────────────────────────────
382
- userMessage(text) {
416
+ userMessage(text, attachments) {
383
417
  this.currentTextCardId = null;
384
- const card = { type: 'user', id: this.nextId(), timestamp: Date.now(), text };
418
+ // UserCard.attachments only carries metadata bytes are fetched on
419
+ // demand via `attachment:fetch`, not piped through every snapshot.
420
+ const metadata = attachments && attachments.length > 0
421
+ ? attachments.map((a) => ({
422
+ id: a.id,
423
+ kind: a.kind,
424
+ mimeType: a.mimeType,
425
+ name: a.name,
426
+ size: a.size,
427
+ }))
428
+ : undefined;
429
+ const card = {
430
+ type: 'user',
431
+ id: this.nextId(),
432
+ timestamp: Date.now(),
433
+ text,
434
+ ...(metadata ? { attachments: metadata } : {}),
435
+ };
385
436
  return this.addEvent(card);
386
437
  }
387
438
  thinkingBlock(text) {
@@ -553,6 +604,17 @@ export class StreamCardBuilder {
553
604
  const card = { type: 'system', id: this.nextId(), timestamp: Date.now(), text, subtype };
554
605
  return this.addEvent(card);
555
606
  }
607
+ recoverySuggested(reason, action, label) {
608
+ const card = {
609
+ type: 'recovery_suggested',
610
+ id: this.nextId(),
611
+ timestamp: Date.now(),
612
+ reason,
613
+ action,
614
+ label,
615
+ };
616
+ return this.addEvent(card);
617
+ }
556
618
  errorMessage(text) {
557
619
  return this.systemMessage(`Error: ${text}`, 'error');
558
620
  }
@@ -681,6 +743,20 @@ export async function buildCardsFromHistory(sessionId, cwd, offset = 0, limit =
681
743
  toolUseIdToAgentId.set(toolUseId, toolUseId);
682
744
  }
683
745
  }
746
+ // ── Load persisted attachment metas + turn manifest so we can rebind
747
+ // synthetic `replay:N` ids in user blocks back to the real upload UUIDs
748
+ // the PWA needs for `attachment:fetch`. Manifest gives an exact-UUID
749
+ // match per turn (preferred); the fuzzy `(kind, size, name)` resolver is
750
+ // the fallback for sessions whose attachments predate the manifest.
751
+ const [persistedMetas, turnManifest] = await Promise.all([
752
+ listSessionAttachments(sessionId).catch(() => []),
753
+ readTurnManifest(sessionId).catch(() => []),
754
+ ]);
755
+ const metaById = new Map();
756
+ for (const m of persistedMetas)
757
+ metaById.set(m.id, m);
758
+ const resolveAttachment = makeAttachmentResolver(persistedMetas);
759
+ let attachmentTurnCursor = 0;
684
760
  // ── Pass 2: Build Cards from sliced messages ───────────────────────────
685
761
  let seq = Math.max(0, total - offset - limit);
686
762
  const nextId = () => `${sessionId}:h:${++seq}`;
@@ -716,12 +792,84 @@ export async function buildCardsFromHistory(sessionId, cwd, offset = 0, limit =
716
792
  continue;
717
793
  }
718
794
  if (Array.isArray(rawMessage.content)) {
795
+ // A user turn that came in with attachments is one logical card with
796
+ // image/document/text blocks in some order. We collapse all text
797
+ // blocks into a single `text` field and reconstruct AttachmentMetadata
798
+ // records from image/document blocks (no bytes — those live in the
799
+ // attachmentStore on disk and are fetched on demand). Pure text-only
800
+ // turns still produce one card per text block (back-compat).
801
+ //
802
+ // Replayed attachment ids do NOT round-trip through JSONL. We
803
+ // recover the original upload UUIDs by matching each block against
804
+ // the persisted-meta pool by `(kind, decoded byte size [, name])`;
805
+ // an unmatched block falls back to a synthetic `replay:<n>` id and
806
+ // its chip will render as "Unavailable" (bytes lost or not yet
807
+ // persistent).
808
+ const attachments = [];
809
+ const textParts = [];
810
+ let hasNonTextBlock = false;
811
+ // Pre-scan the block array for attachment blocks so we know whether
812
+ // to consume a manifest entry for this turn (an attachment-bearing
813
+ // user turn contributes exactly one manifest line, in send order).
814
+ const turnHasAttachment = rawMessage.content.some((b) => (b?.type === 'image' && b.source?.type === 'base64') ||
815
+ (b?.type === 'document' && b.source?.type === 'base64' && b.source.media_type === 'application/pdf'));
816
+ const turnEntry = turnHasAttachment ? turnManifest[attachmentTurnCursor] : undefined;
817
+ if (turnHasAttachment)
818
+ attachmentTurnCursor++;
819
+ let manifestIdx = 0;
719
820
  for (const block of rawMessage.content) {
720
- if (block.type === 'text') {
721
- cards.push({ type: 'user', id: nextId(), timestamp: Date.now(), text: block.text });
821
+ if (block?.type === 'image' && block.source?.type === 'base64') {
822
+ hasNonTextBlock = true;
823
+ const size = decodedBase64Bytes(block.source.data);
824
+ const mimeType = typeof block.source.media_type === 'string' ? block.source.media_type : 'image/png';
825
+ const manifestId = turnEntry?.ids[manifestIdx++];
826
+ const exact = manifestId ? metaById.get(manifestId) : undefined;
827
+ const matched = exact && exact.kind === 'image' && exact.size === size
828
+ ? exact
829
+ : resolveAttachment('image', size);
830
+ attachments.push(matched
831
+ ? { id: matched.id, kind: matched.kind, mimeType: matched.mimeType, name: matched.name, size: matched.size }
832
+ : { id: `replay:${nextId()}`, kind: 'image', mimeType, name: 'attachment', size });
833
+ }
834
+ else if (block?.type === 'document' && block.source?.type === 'base64' && block.source.media_type === 'application/pdf') {
835
+ hasNonTextBlock = true;
836
+ const size = decodedBase64Bytes(block.source.data);
837
+ const name = typeof block.title === 'string' && block.title.length > 0 ? block.title : 'document.pdf';
838
+ const manifestId = turnEntry?.ids[manifestIdx++];
839
+ const exact = manifestId ? metaById.get(manifestId) : undefined;
840
+ const matched = exact && exact.kind === 'pdf' && exact.size === size
841
+ ? exact
842
+ : resolveAttachment('pdf', size, name);
843
+ attachments.push(matched
844
+ ? { id: matched.id, kind: matched.kind, mimeType: matched.mimeType, name: matched.name, size: matched.size }
845
+ : { id: `replay:${nextId()}`, kind: 'pdf', mimeType: 'application/pdf', name, size });
846
+ }
847
+ else if (block?.type === 'text' && typeof block.text === 'string') {
848
+ textParts.push(block.text);
722
849
  }
723
850
  // tool_result blocks are paired into ToolCallCard.result (handled below)
724
- // Skip them here — they don't need separate cards
851
+ // Skip them here — they don't need separate cards.
852
+ }
853
+ if (hasNonTextBlock) {
854
+ const card = {
855
+ type: 'user',
856
+ id: nextId(),
857
+ timestamp: Date.now(),
858
+ text: textParts.join('\n'),
859
+ ...(attachments.length > 0 ? { attachments } : {}),
860
+ };
861
+ cards.push(card);
862
+ }
863
+ else {
864
+ // No image/pdf — fall back to one card per text block, preserving
865
+ // the older multi-card behavior for plain user turns (e.g. when a
866
+ // tool_result block is present alongside a text block, and only
867
+ // the text block makes a user card).
868
+ for (const block of rawMessage.content) {
869
+ if (block?.type === 'text' && typeof block.text === 'string') {
870
+ cards.push({ type: 'user', id: nextId(), timestamp: Date.now(), text: block.text });
871
+ }
872
+ }
725
873
  }
726
874
  continue;
727
875
  }