@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.
- package/LICENSE +1 -1
- package/README.md +31 -93
- package/dist/ai/asyncQueue.d.ts.map +1 -1
- package/dist/ai/asyncQueue.js +2 -0
- package/dist/ai/asyncQueue.js.map +1 -1
- package/dist/ai/attachmentStaging.d.ts +71 -0
- package/dist/ai/attachmentStaging.d.ts.map +1 -0
- package/dist/ai/attachmentStaging.js +243 -0
- package/dist/ai/attachmentStaging.js.map +1 -0
- package/dist/ai/attachmentStore.d.ts +40 -0
- package/dist/ai/attachmentStore.d.ts.map +1 -0
- package/dist/ai/attachmentStore.js +173 -0
- package/dist/ai/attachmentStore.js.map +1 -0
- package/dist/ai/cardBuilder.d.ts +6 -6
- package/dist/ai/cardBuilder.d.ts.map +1 -1
- package/dist/ai/cardBuilder.js +164 -16
- package/dist/ai/cardBuilder.js.map +1 -1
- package/dist/ai/claudeCliProvider.d.ts +34 -15
- package/dist/ai/claudeCliProvider.d.ts.map +1 -1
- package/dist/ai/claudeCliProvider.js +108 -64
- package/dist/ai/claudeCliProvider.js.map +1 -1
- package/dist/ai/claudeCodeProvider.d.ts.map +1 -1
- package/dist/ai/claudeCodeProvider.js +2 -0
- package/dist/ai/claudeCodeProvider.js.map +1 -1
- package/dist/ai/claudeSdkProvider.d.ts +6 -3
- package/dist/ai/claudeSdkProvider.d.ts.map +1 -1
- package/dist/ai/claudeSdkProvider.js +97 -29
- package/dist/ai/claudeSdkProvider.js.map +1 -1
- package/dist/ai/codexAppServer/approvalMapping.d.ts.map +1 -1
- package/dist/ai/codexAppServer/approvalMapping.js.map +1 -1
- package/dist/ai/codexAppServer/cardAdapter.d.ts +0 -3
- package/dist/ai/codexAppServer/cardAdapter.d.ts.map +1 -1
- package/dist/ai/codexAppServer/cardAdapter.js +40 -5
- package/dist/ai/codexAppServer/cardAdapter.js.map +1 -1
- package/dist/ai/codexAppServer/index.d.ts.map +1 -1
- package/dist/ai/codexAppServer/index.js +2 -0
- package/dist/ai/codexAppServer/index.js.map +1 -1
- package/dist/ai/codexAppServer/overrideStore.d.ts.map +1 -1
- package/dist/ai/codexAppServer/overrideStore.js.map +1 -1
- package/dist/ai/codexAppServer/permissionMapping.d.ts.map +1 -1
- package/dist/ai/codexAppServer/permissionMapping.js.map +1 -1
- package/dist/ai/codexAppServer/processManager.d.ts.map +1 -1
- package/dist/ai/codexAppServer/processManager.js +2 -0
- package/dist/ai/codexAppServer/processManager.js.map +1 -1
- package/dist/ai/codexAppServer/provider.d.ts.map +1 -1
- package/dist/ai/codexAppServer/provider.js +9 -24
- package/dist/ai/codexAppServer/provider.js.map +1 -1
- package/dist/ai/codexAppServer/rpcClient.d.ts.map +1 -1
- package/dist/ai/codexAppServer/rpcClient.js.map +1 -1
- package/dist/ai/codexAppServer/schema/index.d.ts.map +1 -1
- package/dist/ai/codexAppServer/schema/index.js +2 -0
- package/dist/ai/codexAppServer/schema/index.js.map +1 -1
- package/dist/ai/codexAppServer/stdioTransport.d.ts.map +1 -1
- package/dist/ai/codexAppServer/stdioTransport.js.map +1 -1
- package/dist/ai/codexAppServer/tokenAccounting.d.ts.map +1 -1
- package/dist/ai/codexAppServer/tokenAccounting.js.map +1 -1
- package/dist/ai/codexAppServer/version.d.ts.map +1 -1
- package/dist/ai/codexAppServer/version.js +2 -0
- package/dist/ai/codexAppServer/version.js.map +1 -1
- package/dist/ai/codexLogin.d.ts.map +1 -1
- package/dist/ai/codexLogin.js +2 -0
- package/dist/ai/codexLogin.js.map +1 -1
- package/dist/ai/codexMcpProvider.d.ts.map +1 -1
- package/dist/ai/codexMcpProvider.js +7 -10
- package/dist/ai/codexMcpProvider.js.map +1 -1
- package/dist/ai/commitSummary.d.ts.map +1 -1
- package/dist/ai/commitSummary.js +2 -0
- package/dist/ai/commitSummary.js.map +1 -1
- package/dist/ai/commitSummaryCli.d.ts.map +1 -1
- package/dist/ai/commitSummaryCli.js +2 -0
- package/dist/ai/commitSummaryCli.js.map +1 -1
- package/dist/ai/commitSummaryStore.d.ts.map +1 -1
- package/dist/ai/commitSummaryStore.js +2 -0
- package/dist/ai/commitSummaryStore.js.map +1 -1
- package/dist/ai/contentBlocks.d.ts +9 -0
- package/dist/ai/contentBlocks.d.ts.map +1 -0
- package/dist/ai/contentBlocks.js +86 -0
- package/dist/ai/contentBlocks.js.map +1 -0
- package/dist/ai/debugLogger.d.ts.map +1 -1
- package/dist/ai/debugLogger.js +2 -0
- package/dist/ai/debugLogger.js.map +1 -1
- package/dist/ai/enrichEntry.d.ts.map +1 -1
- package/dist/ai/enrichEntry.js.map +1 -1
- package/dist/ai/pdfMeta.d.ts +8 -0
- package/dist/ai/pdfMeta.d.ts.map +1 -0
- package/dist/ai/pdfMeta.js +51 -0
- package/dist/ai/pdfMeta.js.map +1 -0
- package/dist/ai/provider.d.ts +21 -8
- package/dist/ai/provider.d.ts.map +1 -1
- package/dist/ai/provider.js.map +1 -1
- package/dist/ai/sandboxMcp.d.ts.map +1 -1
- package/dist/ai/sandboxMcp.js +2 -0
- package/dist/ai/sandboxMcp.js.map +1 -1
- package/dist/ai/sandboxMcpStdio.d.ts.map +1 -1
- package/dist/ai/sandboxMcpStdio.js +2 -0
- package/dist/ai/sandboxMcpStdio.js.map +1 -1
- package/dist/ai/sessionManager.d.ts +19 -7
- package/dist/ai/sessionManager.d.ts.map +1 -1
- package/dist/ai/sessionManager.js +81 -23
- package/dist/ai/sessionManager.js.map +1 -1
- package/dist/ai/sessionRegistry.d.ts.map +1 -1
- package/dist/ai/sessionRegistry.js +2 -0
- package/dist/ai/sessionRegistry.js.map +1 -1
- package/dist/ai/sessionStore.d.ts.map +1 -1
- package/dist/ai/sessionStore.js +2 -0
- package/dist/ai/sessionStore.js.map +1 -1
- package/dist/ai/systemPrompt.d.ts.map +1 -1
- package/dist/ai/systemPrompt.js +3 -0
- package/dist/ai/systemPrompt.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/connection/connection.d.ts.map +1 -1
- package/dist/connection/connection.js +2 -0
- package/dist/connection/connection.js.map +1 -1
- package/dist/connection/e2eHarness.d.ts +1 -1
- package/dist/connection/e2eHarness.d.ts.map +1 -1
- package/dist/connection/e2eHarness.js +26 -8
- package/dist/connection/e2eHarness.js.map +1 -1
- package/dist/connection/fakeRelay.d.ts.map +1 -1
- package/dist/connection/fakeRelay.js +2 -0
- package/dist/connection/fakeRelay.js.map +1 -1
- package/dist/connection/pubsub.d.ts.map +1 -1
- package/dist/connection/pubsub.js +2 -0
- package/dist/connection/pubsub.js.map +1 -1
- package/dist/connection/relay.d.ts.map +1 -1
- package/dist/connection/relay.js +2 -0
- package/dist/connection/relay.js.map +1 -1
- package/dist/files/fileBrowser.d.ts.map +1 -1
- package/dist/files/fileBrowser.js +44 -2
- package/dist/files/fileBrowser.js.map +1 -1
- package/dist/git/operations.d.ts.map +1 -1
- package/dist/git/operations.js +2 -0
- package/dist/git/operations.js.map +1 -1
- package/dist/handlers/legacyBusAdapter.d.ts +1 -1
- package/dist/handlers/legacyBusAdapter.d.ts.map +1 -1
- package/dist/handlers/legacyBusAdapter.js +13 -1
- package/dist/handlers/legacyBusAdapter.js.map +1 -1
- package/dist/handlers/messageHandler.d.ts +23 -0
- package/dist/handlers/messageHandler.d.ts.map +1 -1
- package/dist/handlers/messageHandler.js +226 -14
- package/dist/handlers/messageHandler.js.map +1 -1
- package/dist/index.js +42 -2
- package/dist/index.js.map +1 -1
- package/dist/messageBus/busServerTransport.d.ts.map +1 -1
- package/dist/messageBus/busServerTransport.js +2 -0
- package/dist/messageBus/busServerTransport.js.map +1 -1
- package/dist/service/debugHttpServer.d.ts.map +1 -1
- package/dist/service/debugHttpServer.js +2 -0
- package/dist/service/debugHttpServer.js.map +1 -1
- package/dist/service/ensureDaemon.d.ts.map +1 -1
- package/dist/service/ensureDaemon.js +27 -2
- package/dist/service/ensureDaemon.js.map +1 -1
- package/dist/service/index.d.ts.map +1 -1
- package/dist/service/index.js +2 -0
- package/dist/service/index.js.map +1 -1
- package/dist/service/ipcClient.d.ts.map +1 -1
- package/dist/service/ipcClient.js +3 -1
- package/dist/service/ipcClient.js.map +1 -1
- package/dist/service/ipcServer.d.ts.map +1 -1
- package/dist/service/ipcServer.js +2 -0
- package/dist/service/ipcServer.js.map +1 -1
- package/dist/service/pushClient.d.ts.map +1 -1
- package/dist/service/pushClient.js +2 -0
- package/dist/service/pushClient.js.map +1 -1
- package/dist/service/run.d.ts.map +1 -1
- package/dist/service/run.js +14 -1
- package/dist/service/run.js.map +1 -1
- package/dist/service/singleton.d.ts +1 -0
- package/dist/service/singleton.d.ts.map +1 -1
- package/dist/service/singleton.js +5 -0
- package/dist/service/singleton.js.map +1 -1
- package/dist/service/stateStore.d.ts.map +1 -1
- package/dist/service/stateStore.js +2 -0
- package/dist/service/stateStore.js.map +1 -1
- package/dist/service/systemdUnit.d.ts +149 -0
- package/dist/service/systemdUnit.d.ts.map +1 -0
- package/dist/service/systemdUnit.js +322 -0
- package/dist/service/systemdUnit.js.map +1 -0
- package/dist/service/types.d.ts +6 -0
- package/dist/service/types.d.ts.map +1 -1
- package/dist/service/types.js +3 -1
- package/dist/service/types.js.map +1 -1
- package/dist/storage/eventStore.d.ts.map +1 -1
- package/dist/storage/eventStore.js +2 -0
- package/dist/storage/eventStore.js.map +1 -1
- package/dist/terminal/terminalManager.d.ts.map +1 -1
- package/dist/terminal/terminalManager.js +2 -0
- package/dist/terminal/terminalManager.js.map +1 -1
- package/dist/tombstoneCheck.d.ts.map +1 -1
- package/dist/tombstoneCheck.js +2 -0
- package/dist/tombstoneCheck.js.map +1 -1
- 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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"}
|
package/dist/ai/cardBuilder.d.ts
CHANGED
|
@@ -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,
|
|
25
|
+
constructor(sessionId: string, cwd: string);
|
|
27
26
|
updateSessionId(sessionId: string): void;
|
|
28
|
-
/** Start a new turn:
|
|
29
|
-
startNewTurn(
|
|
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":"
|
|
1
|
+
{"version":3,"file":"cardBuilder.d.ts","sourceRoot":"","sources":["../../src/ai/cardBuilder.ts"],"names":[],"mappings":"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"}
|
package/dist/ai/cardBuilder.js
CHANGED
|
@@ -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,
|
|
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:
|
|
217
|
-
startNewTurn(
|
|
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}:${
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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
|
-
|
|
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 === '
|
|
721
|
-
|
|
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
|
}
|