dsh-thoughtdag 0.4.4 → 0.4.6
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/README.md +22 -6
- package/cordis.patch.yml +5 -0
- package/dist-app/assets/{canonical-DbPqsMYz.js → canonical-DLhL4I9r.js} +2 -2
- package/dist-app/assets/{canvas-record-C1A8ExlM.js → canvas-record-CkE2kiAl.js} +1 -1
- package/dist-app/assets/{claude-code-session-DQRgWy00.js → claude-code-session-C6SC-rR3.js} +1 -1
- package/dist-app/assets/{codex-session--KVj3_vb.js → codex-session-BPnTP7aa.js} +1 -1
- package/dist-app/assets/{dsh-session-6OCSm4h5.js → dsh-session-DvePmOkr.js} +2 -2
- package/dist-app/assets/{experiment-loop-BfxITu2e.js → experiment-loop-Ch2BbUUd.js} +1 -1
- package/dist-app/assets/{index-6m02Pb4Q.js → index-BNzCrLOq.js} +147 -145
- package/dist-app/assets/index-ByFqFIuH.js +3 -0
- package/dist-app/assets/index-CaLqpoXT.css +1 -0
- package/dist-app/assets/{index-BL39OXsY.js → index-CyiaXC1W.js} +1 -1
- package/dist-app/assets/{index-BH-U0zVI.js → index-DoQT2Wn3.js} +1 -1
- package/dist-app/assets/{live-mirror-BElcCk_a.js → live-mirror-Dg0Om8mP.js} +2 -2
- package/dist-app/assets/pi-session-BG_vrEOO.js +14 -0
- package/dist-app/assets/{sensitive-scan-L0rFzufE.js → sensitive-scan-B7Rgdm8T.js} +1 -1
- package/dist-app/assets/{session-handoff-CGrowwZp.js → session-handoff-Bxj5OdHr.js} +2 -2
- package/dist-app/assets/{shared-CURSZnXt.js → shared-B6lVzmly.js} +1 -1
- package/dist-app/assets/{turndown-plugin-gfm.cjs-p3SpZgsj.js → turndown-plugin-gfm.cjs-DMq7YfJM.js} +1 -1
- package/dist-app/assets/{update-check-D5daO3Cz.js → update-check-Cqj9cGM2.js} +1 -1
- package/dist-app/index.html +2 -2
- package/lib/index.js +96 -2
- package/lib/why.mjs +2771 -0
- package/package.json +1 -1
- package/dist-app/assets/index-D-BTv0n0.js +0 -3
- package/dist-app/assets/index-DEHbIrRr.css +0 -1
package/lib/why.mjs
ADDED
|
@@ -0,0 +1,2771 @@
|
|
|
1
|
+
// <define:import.meta.env>
|
|
2
|
+
var define_import_meta_env_default = {};
|
|
3
|
+
|
|
4
|
+
// ../cli/src/lib.ts
|
|
5
|
+
import { promises as fsp, createReadStream, createWriteStream } from "node:fs";
|
|
6
|
+
import { createInterface } from "node:readline";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import * as zlib from "node:zlib";
|
|
10
|
+
|
|
11
|
+
// ../src/utils.ts
|
|
12
|
+
var idCounter = 0;
|
|
13
|
+
function generateId() {
|
|
14
|
+
return `node-${Date.now()}-${idCounter++}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ../src/lib/graph.ts
|
|
18
|
+
function getDescendantIds(nodeId, edges) {
|
|
19
|
+
const descendants = [];
|
|
20
|
+
const queue = [nodeId];
|
|
21
|
+
while (queue.length > 0) {
|
|
22
|
+
const current = queue.shift();
|
|
23
|
+
const children = edges.filter((e) => e.source === current && !e.data?.isCrossLink).map((e) => e.target);
|
|
24
|
+
descendants.push(...children);
|
|
25
|
+
queue.push(...children);
|
|
26
|
+
}
|
|
27
|
+
return descendants;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ../src/lib/constants.ts
|
|
31
|
+
var API_BASE = define_import_meta_env_default.VITE_API_BASE ?? (define_import_meta_env_default.DEV ? "http://localhost:3001" : "");
|
|
32
|
+
var LAYOUT_COL_WIDTH = 540;
|
|
33
|
+
var LAYOUT_H_GAP = 48;
|
|
34
|
+
var LAYOUT_V_GAP = 72;
|
|
35
|
+
var COLLAPSED_LAYOUT_HEIGHT = 240;
|
|
36
|
+
|
|
37
|
+
// ../src/lib/layout.ts
|
|
38
|
+
function estimateNodeHeight(node) {
|
|
39
|
+
if (node.data.isCollapsed) return COLLAPSED_LAYOUT_HEIGHT;
|
|
40
|
+
const questionH = Math.min(180, 40 + (node.data.question || "").length / 1.2);
|
|
41
|
+
const responseH = Math.min(400, (node.data.response || "").length / 1.05);
|
|
42
|
+
const highlightsH = (node.data.highlights?.length ?? 0) * 26;
|
|
43
|
+
const insightH = (node.data.attachments?.length ?? 0) > 0 ? 30 : 0;
|
|
44
|
+
const estimated = 215 + questionH + responseH + highlightsH + insightH;
|
|
45
|
+
return Math.max(260, Math.min(930, estimated));
|
|
46
|
+
}
|
|
47
|
+
function nodeHeight(node) {
|
|
48
|
+
return Math.max(node.measured?.height ?? 0, estimateNodeHeight(node));
|
|
49
|
+
}
|
|
50
|
+
function autoLayout(allNodes, allEdges) {
|
|
51
|
+
if (allNodes.length === 0) return allNodes;
|
|
52
|
+
const contentIds = new Set(
|
|
53
|
+
allNodes.filter((n) => ["note", "file", "link", "frame"].includes(n.data.stepKind ?? "")).map((n) => n.id)
|
|
54
|
+
);
|
|
55
|
+
const nodes = allNodes.filter((n) => !contentIds.has(n.id));
|
|
56
|
+
const edges = allEdges.filter((e) => !contentIds.has(e.source) && !contentIds.has(e.target));
|
|
57
|
+
const NODE_WIDTH = LAYOUT_COL_WIDTH;
|
|
58
|
+
const H_GAP = LAYOUT_H_GAP;
|
|
59
|
+
const V_GAP = LAYOUT_V_GAP;
|
|
60
|
+
const V_PAD = 24;
|
|
61
|
+
const structuralEdges = edges.filter((e) => !e.data?.isCrossLink);
|
|
62
|
+
const hasStructuralParent = new Set(structuralEdges.map((e) => e.target));
|
|
63
|
+
for (const e of edges) {
|
|
64
|
+
if (e.data?.isCrossLink && !e.data?.isWatch && !hasStructuralParent.has(e.target)) {
|
|
65
|
+
structuralEdges.push(e);
|
|
66
|
+
hasStructuralParent.add(e.target);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const targetIds = new Set(structuralEdges.map((e) => e.target));
|
|
70
|
+
const roots = nodes.filter((n) => !targetIds.has(n.id));
|
|
71
|
+
const structuralParents = /* @__PURE__ */ new Map();
|
|
72
|
+
for (const e of structuralEdges) {
|
|
73
|
+
const list = structuralParents.get(e.target) || [];
|
|
74
|
+
list.push(e.source);
|
|
75
|
+
structuralParents.set(e.target, list);
|
|
76
|
+
}
|
|
77
|
+
const materialAnchors = /* @__PURE__ */ new Map();
|
|
78
|
+
const perMaterialCount = /* @__PURE__ */ new Map();
|
|
79
|
+
for (const root of roots) {
|
|
80
|
+
const mats = allEdges.filter((e) => e.target === root.id && !e.data?.isCrossLink && contentIds.has(e.source)).map((e) => allNodes.find((n) => n.id === e.source)).filter((m) => !!m);
|
|
81
|
+
if (mats.length === 0) continue;
|
|
82
|
+
const lowest = mats.reduce((a, b) => a.position.y + nodeHeight(a) > b.position.y + nodeHeight(b) ? a : b);
|
|
83
|
+
const k = perMaterialCount.get(lowest.id) ?? 0;
|
|
84
|
+
perMaterialCount.set(lowest.id, k + 1);
|
|
85
|
+
materialAnchors.set(root.id, {
|
|
86
|
+
x: lowest.position.x - 60 + k * (LAYOUT_COL_WIDTH + LAYOUT_H_GAP),
|
|
87
|
+
y: lowest.position.y + nodeHeight(lowest) + LAYOUT_V_GAP
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const childrenMap = /* @__PURE__ */ new Map();
|
|
91
|
+
for (const edge of structuralEdges) {
|
|
92
|
+
const list = childrenMap.get(edge.source) || [];
|
|
93
|
+
list.push(edge.target);
|
|
94
|
+
childrenMap.set(edge.source, list);
|
|
95
|
+
}
|
|
96
|
+
const edgeKey = (parent, child) => `${parent}\0${child}`;
|
|
97
|
+
const exploreEdges = new Set(
|
|
98
|
+
edges.filter((e) => e.data?.isBranchFromSelection).map((e) => edgeKey(e.source, e.target))
|
|
99
|
+
);
|
|
100
|
+
function classifyChildren(parentId, claimant2) {
|
|
101
|
+
const children = (childrenMap.get(parentId) || []).filter(
|
|
102
|
+
(c) => (claimant2.get(c) ?? parentId) === parentId
|
|
103
|
+
);
|
|
104
|
+
const nonExplore = children.filter((c) => !exploreEdges.has(edgeKey(parentId, c)));
|
|
105
|
+
const explores = children.filter((c) => exploreEdges.has(edgeKey(parentId, c)));
|
|
106
|
+
return {
|
|
107
|
+
continuation: nonExplore[0] ?? null,
|
|
108
|
+
regenerates: nonExplore.slice(1),
|
|
109
|
+
explores
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const VIRT_BASE = 1e5;
|
|
113
|
+
function assignAllColumns(claimant2) {
|
|
114
|
+
const nodeColumn2 = /* @__PURE__ */ new Map();
|
|
115
|
+
let nextColumn = 0;
|
|
116
|
+
let nextVirt = VIRT_BASE;
|
|
117
|
+
const colXOverride = /* @__PURE__ */ new Map();
|
|
118
|
+
const colX2 = (col) => colXOverride.get(col) ?? col * (NODE_WIDTH + H_GAP);
|
|
119
|
+
function assignColumns(nodeId, col) {
|
|
120
|
+
if (nodeColumn2.has(nodeId)) return;
|
|
121
|
+
nodeColumn2.set(nodeId, col);
|
|
122
|
+
const { continuation, regenerates, explores } = classifyChildren(nodeId, claimant2);
|
|
123
|
+
if (continuation) assignColumns(continuation, col);
|
|
124
|
+
for (let i = 0; i < regenerates.length; i++) {
|
|
125
|
+
let regenCol;
|
|
126
|
+
if (col >= VIRT_BASE) {
|
|
127
|
+
regenCol = nextVirt++;
|
|
128
|
+
colXOverride.set(regenCol, colX2(col) + (i + 1) * (NODE_WIDTH + H_GAP));
|
|
129
|
+
} else {
|
|
130
|
+
regenCol = col + 1 + i;
|
|
131
|
+
nextColumn = Math.max(nextColumn, regenCol + 1);
|
|
132
|
+
}
|
|
133
|
+
assignColumns(regenerates[i], regenCol);
|
|
134
|
+
}
|
|
135
|
+
let exploreOffset = 0;
|
|
136
|
+
for (const ec of explores) {
|
|
137
|
+
let exploreCol;
|
|
138
|
+
if (col >= VIRT_BASE) {
|
|
139
|
+
exploreCol = nextVirt++;
|
|
140
|
+
colXOverride.set(exploreCol, colX2(col) + (regenerates.length + 1 + exploreOffset) * (NODE_WIDTH + H_GAP));
|
|
141
|
+
exploreOffset++;
|
|
142
|
+
} else {
|
|
143
|
+
exploreCol = nextColumn;
|
|
144
|
+
nextColumn++;
|
|
145
|
+
}
|
|
146
|
+
assignColumns(ec, exploreCol);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
for (const root of roots) {
|
|
150
|
+
const anchor = materialAnchors.get(root.id);
|
|
151
|
+
if (anchor) {
|
|
152
|
+
const virtCol = nextVirt++;
|
|
153
|
+
colXOverride.set(virtCol, anchor.x);
|
|
154
|
+
assignColumns(root.id, virtCol);
|
|
155
|
+
} else {
|
|
156
|
+
const rootCol = nextColumn;
|
|
157
|
+
nextColumn++;
|
|
158
|
+
assignColumns(root.id, rootCol);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (let guard = 0; guard < nodes.length; guard++) {
|
|
162
|
+
let progressed = false;
|
|
163
|
+
for (const node of nodes) {
|
|
164
|
+
if (nodeColumn2.has(node.id)) continue;
|
|
165
|
+
const cols = (structuralParents.get(node.id) || []).map((p) => nodeColumn2.get(p)).filter((c) => c !== void 0).sort((a, b) => a - b);
|
|
166
|
+
if (!cols.length) continue;
|
|
167
|
+
assignColumns(node.id, cols[Math.floor((cols.length - 1) / 2)]);
|
|
168
|
+
progressed = true;
|
|
169
|
+
}
|
|
170
|
+
if (!progressed) break;
|
|
171
|
+
}
|
|
172
|
+
return { nodeColumn: nodeColumn2, colX: colX2 };
|
|
173
|
+
}
|
|
174
|
+
const claimant = /* @__PURE__ */ new Map();
|
|
175
|
+
for (const [child, ps] of structuralParents) {
|
|
176
|
+
if (ps.length < 2) continue;
|
|
177
|
+
const continued = ps.filter((p) => !exploreEdges.has(edgeKey(p, child)));
|
|
178
|
+
const pool = continued.length ? continued : ps;
|
|
179
|
+
claimant.set(child, pool[0]);
|
|
180
|
+
}
|
|
181
|
+
const { nodeColumn, colX } = assignAllColumns(claimant);
|
|
182
|
+
const nodeHeightMap = /* @__PURE__ */ new Map();
|
|
183
|
+
for (const node of nodes) {
|
|
184
|
+
nodeHeightMap.set(node.id, nodeHeight(node));
|
|
185
|
+
}
|
|
186
|
+
const positioned = /* @__PURE__ */ new Map();
|
|
187
|
+
const visited = /* @__PURE__ */ new Set();
|
|
188
|
+
const queue = [...roots.map((r) => r.id)];
|
|
189
|
+
for (const rootId of queue) {
|
|
190
|
+
const col = nodeColumn.get(rootId) ?? 0;
|
|
191
|
+
const anchor = materialAnchors.get(rootId);
|
|
192
|
+
positioned.set(rootId, { x: colX(col), y: anchor?.y ?? 0 });
|
|
193
|
+
visited.add(rootId);
|
|
194
|
+
}
|
|
195
|
+
while (queue.length > 0) {
|
|
196
|
+
const current = queue.shift();
|
|
197
|
+
const parentPos = positioned.get(current);
|
|
198
|
+
const parentHeight = nodeHeightMap.get(current) || 220;
|
|
199
|
+
const { continuation, regenerates, explores } = classifyChildren(current, claimant);
|
|
200
|
+
if (continuation && !visited.has(continuation)) {
|
|
201
|
+
visited.add(continuation);
|
|
202
|
+
const col = nodeColumn.get(continuation) ?? 0;
|
|
203
|
+
positioned.set(continuation, {
|
|
204
|
+
x: colX(col),
|
|
205
|
+
y: parentPos.y + parentHeight + V_GAP
|
|
206
|
+
});
|
|
207
|
+
queue.push(continuation);
|
|
208
|
+
}
|
|
209
|
+
const continuationY = parentPos.y + parentHeight + V_GAP;
|
|
210
|
+
for (const rc of regenerates) {
|
|
211
|
+
if (visited.has(rc)) continue;
|
|
212
|
+
visited.add(rc);
|
|
213
|
+
const col = nodeColumn.get(rc) ?? 0;
|
|
214
|
+
positioned.set(rc, {
|
|
215
|
+
x: colX(col),
|
|
216
|
+
y: continuationY
|
|
217
|
+
});
|
|
218
|
+
queue.push(rc);
|
|
219
|
+
}
|
|
220
|
+
for (const ec of explores) {
|
|
221
|
+
if (visited.has(ec)) continue;
|
|
222
|
+
visited.add(ec);
|
|
223
|
+
const col = nodeColumn.get(ec) ?? 0;
|
|
224
|
+
positioned.set(ec, {
|
|
225
|
+
x: colX(col),
|
|
226
|
+
y: parentPos.y + parentHeight * 0.25
|
|
227
|
+
});
|
|
228
|
+
queue.push(ec);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
for (let guard = 0; guard < nodes.length; guard++) {
|
|
232
|
+
let progressed = false;
|
|
233
|
+
for (const node of nodes) {
|
|
234
|
+
if (positioned.has(node.id)) continue;
|
|
235
|
+
const ps = (structuralParents.get(node.id) || []).filter((p) => positioned.has(p));
|
|
236
|
+
if (!ps.length) continue;
|
|
237
|
+
const bottom = Math.max(
|
|
238
|
+
...ps.map((p) => positioned.get(p).y + (nodeHeightMap.get(p) || 220))
|
|
239
|
+
);
|
|
240
|
+
positioned.set(node.id, { x: colX(nodeColumn.get(node.id) ?? 0), y: bottom + V_GAP });
|
|
241
|
+
progressed = true;
|
|
242
|
+
}
|
|
243
|
+
if (!progressed) break;
|
|
244
|
+
}
|
|
245
|
+
for (const node of nodes) {
|
|
246
|
+
if (!positioned.has(node.id)) {
|
|
247
|
+
positioned.set(node.id, { x: 0, y: 0 });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
for (let pass = 0; pass < 5; pass++) {
|
|
251
|
+
let moved = false;
|
|
252
|
+
for (const node of nodes) {
|
|
253
|
+
const pos = positioned.get(node.id);
|
|
254
|
+
if (!pos) continue;
|
|
255
|
+
const all = (structuralParents.get(node.id) || []).filter((p) => positioned.has(p));
|
|
256
|
+
if (all.length < 2) continue;
|
|
257
|
+
const continued = all.filter((p) => !exploreEdges.has(`${p}\0${node.id}`));
|
|
258
|
+
const explored = all.filter((p) => exploreEdges.has(`${p}\0${node.id}`));
|
|
259
|
+
let floor = -Infinity;
|
|
260
|
+
for (const p of continued) {
|
|
261
|
+
floor = Math.max(floor, positioned.get(p).y + (nodeHeightMap.get(p) || 220) + V_GAP);
|
|
262
|
+
}
|
|
263
|
+
for (const p of explored) {
|
|
264
|
+
floor = Math.max(floor, positioned.get(p).y);
|
|
265
|
+
}
|
|
266
|
+
if (floor > -Infinity && pos.y < floor) {
|
|
267
|
+
const delta = floor - pos.y;
|
|
268
|
+
pos.y = floor;
|
|
269
|
+
for (const dId of new Set(getDescendantIds(node.id, structuralEdges))) {
|
|
270
|
+
const dPos = positioned.get(dId);
|
|
271
|
+
if (dPos) dPos.y += delta;
|
|
272
|
+
}
|
|
273
|
+
moved = true;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (!moved) break;
|
|
277
|
+
}
|
|
278
|
+
const columnNodes = /* @__PURE__ */ new Map();
|
|
279
|
+
for (const node of nodes) {
|
|
280
|
+
const col = nodeColumn.get(node.id) ?? 0;
|
|
281
|
+
const list = columnNodes.get(col) || [];
|
|
282
|
+
list.push(node.id);
|
|
283
|
+
columnNodes.set(col, list);
|
|
284
|
+
}
|
|
285
|
+
for (let pass = 0; pass < 5; pass++) {
|
|
286
|
+
let moved = false;
|
|
287
|
+
for (const [, colNodeIds] of columnNodes) {
|
|
288
|
+
colNodeIds.sort((a, b) => positioned.get(a).y - positioned.get(b).y);
|
|
289
|
+
for (let i = 1; i < colNodeIds.length; i++) {
|
|
290
|
+
const prevId = colNodeIds[i - 1];
|
|
291
|
+
const currId = colNodeIds[i];
|
|
292
|
+
const prevPos = positioned.get(prevId);
|
|
293
|
+
const currPos = positioned.get(currId);
|
|
294
|
+
const prevHeight = nodeHeightMap.get(prevId) || 220;
|
|
295
|
+
const minY = prevPos.y + prevHeight + V_PAD;
|
|
296
|
+
if (currPos.y < minY) {
|
|
297
|
+
const delta = minY - currPos.y;
|
|
298
|
+
currPos.y = minY;
|
|
299
|
+
moved = true;
|
|
300
|
+
for (const dId of new Set(getDescendantIds(currId, structuralEdges))) {
|
|
301
|
+
const dPos = positioned.get(dId);
|
|
302
|
+
if (dPos) dPos.y += delta;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (!moved) break;
|
|
308
|
+
}
|
|
309
|
+
const FOLD_HEIGHT = 8e3;
|
|
310
|
+
const FOLD_COL_GAP = 180;
|
|
311
|
+
const importerNotes = allNodes.filter((n) => n.data.stepKind === "note" && n.data.importSource);
|
|
312
|
+
const noteTargets = new Set(
|
|
313
|
+
allEdges.filter((e) => importerNotes.some((n) => n.id === e.source)).map((e) => e.target)
|
|
314
|
+
);
|
|
315
|
+
for (const root of roots) {
|
|
316
|
+
const chain = [];
|
|
317
|
+
let cur = root.id;
|
|
318
|
+
let pure = true;
|
|
319
|
+
while (cur) {
|
|
320
|
+
chain.push(cur);
|
|
321
|
+
const kids = childrenMap.get(cur) || [];
|
|
322
|
+
if (kids.length > 1) {
|
|
323
|
+
pure = false;
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
cur = kids[0];
|
|
327
|
+
}
|
|
328
|
+
if (!pure || chain.length < 8) continue;
|
|
329
|
+
const chainSet = new Set(chain);
|
|
330
|
+
const first = positioned.get(chain[0]);
|
|
331
|
+
const lastId = chain[chain.length - 1];
|
|
332
|
+
const totalH = positioned.get(lastId).y + (nodeHeightMap.get(lastId) || 220) - first.y;
|
|
333
|
+
if (totalH <= FOLD_HEIGHT) continue;
|
|
334
|
+
const rightOccupied = nodes.some((n) => !chainSet.has(n.id) && positioned.get(n.id).x > first.x + NODE_WIDTH / 2);
|
|
335
|
+
if (rightOccupied) continue;
|
|
336
|
+
let foldCol = 0;
|
|
337
|
+
let y = first.y;
|
|
338
|
+
for (const id of chain) {
|
|
339
|
+
const h = nodeHeightMap.get(id) || 220;
|
|
340
|
+
const used = y - first.y;
|
|
341
|
+
const mustFold = used + h > FOLD_HEIGHT;
|
|
342
|
+
const chapterFold = noteTargets.has(id) && used > FOLD_HEIGHT * 0.6;
|
|
343
|
+
if ((mustFold || chapterFold) && y !== first.y) {
|
|
344
|
+
foldCol++;
|
|
345
|
+
y = first.y;
|
|
346
|
+
}
|
|
347
|
+
const p = positioned.get(id);
|
|
348
|
+
p.x = first.x + foldCol * (NODE_WIDTH + FOLD_COL_GAP);
|
|
349
|
+
p.y = y;
|
|
350
|
+
y += h + V_GAP;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const solidRects = nodes.map((n) => {
|
|
354
|
+
const p = positioned.get(n.id);
|
|
355
|
+
return { x: p.x, y: p.y, w: NODE_WIDTH, h: nodeHeightMap.get(n.id) || 220 };
|
|
356
|
+
});
|
|
357
|
+
const NOTE_W = 460;
|
|
358
|
+
const NOTE_H = 130;
|
|
359
|
+
const collides = (x, y) => solidRects.some((r) => x < r.x + r.w && x + NOTE_W > r.x && y < r.y + r.h && y + NOTE_H > r.y);
|
|
360
|
+
const colTopStacks = /* @__PURE__ */ new Map();
|
|
361
|
+
for (const note of importerNotes) {
|
|
362
|
+
const targetId = allEdges.find((e) => e.source === note.id)?.target;
|
|
363
|
+
const tp = targetId ? positioned.get(targetId) : void 0;
|
|
364
|
+
if (!tp) continue;
|
|
365
|
+
let x = tp.x - 520;
|
|
366
|
+
let y = tp.y;
|
|
367
|
+
if (collides(x, y)) {
|
|
368
|
+
x = tp.x;
|
|
369
|
+
y = tp.y - NOTE_H - 40;
|
|
370
|
+
}
|
|
371
|
+
if (collides(x, y)) {
|
|
372
|
+
const colKey = Math.round(tp.x);
|
|
373
|
+
const colMinY = Math.min(...nodes.filter((n) => Math.round(positioned.get(n.id).x) === colKey).map((n) => positioned.get(n.id).y));
|
|
374
|
+
const k = colTopStacks.get(colKey) ?? 0;
|
|
375
|
+
colTopStacks.set(colKey, k + 1);
|
|
376
|
+
x = tp.x;
|
|
377
|
+
y = colMinY - NOTE_H - 40 - k * (NOTE_H + 30);
|
|
378
|
+
}
|
|
379
|
+
positioned.set(note.id, { x, y });
|
|
380
|
+
}
|
|
381
|
+
return allNodes.map((node) => {
|
|
382
|
+
const pos = positioned.get(node.id);
|
|
383
|
+
return pos ? { ...node, position: pos } : node;
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ../src/lib/import-chat.ts
|
|
388
|
+
function makeNode(question, response, isRoot) {
|
|
389
|
+
return {
|
|
390
|
+
id: generateId(),
|
|
391
|
+
type: "thought",
|
|
392
|
+
position: { x: 0, y: 0 },
|
|
393
|
+
dragHandle: ".drag-handle",
|
|
394
|
+
data: {
|
|
395
|
+
question,
|
|
396
|
+
response,
|
|
397
|
+
responses: [response],
|
|
398
|
+
responseIndex: 0,
|
|
399
|
+
isCollapsed: true,
|
|
400
|
+
isEditing: false,
|
|
401
|
+
isEditingResponse: false,
|
|
402
|
+
isLoading: false,
|
|
403
|
+
tokenCount: Math.ceil((question + response).length / 4),
|
|
404
|
+
highlights: [],
|
|
405
|
+
highlightMode: "tag",
|
|
406
|
+
attachments: [],
|
|
407
|
+
excludedAttachmentIds: [],
|
|
408
|
+
includedAttachmentIds: [],
|
|
409
|
+
roleMode: "inherit",
|
|
410
|
+
isRoot,
|
|
411
|
+
isBranch: false
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ../src/lib/turn-insight.ts
|
|
417
|
+
function conclusionOf(response, max = 140) {
|
|
418
|
+
const paras = response.replace(/```[\s\S]*?```/g, "").split(/\n\s*\n/).map((p) => p.replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[#*`>|_~]/g, "").replace(/^\s*[-•\d.)]+\s*/gm, "").replace(/\s+/g, " ").trim()).filter((p) => p.length > 12 && !/[::]$/.test(p) && !/^(sources?|references?|来源|参考)\b/i.test(p));
|
|
419
|
+
const substantive = paras.filter((p) => p.length >= 30);
|
|
420
|
+
const last = substantive[substantive.length - 1] ?? paras[paras.length - 1] ?? response.replace(/[#*`>-]/g, "").replace(/\s+/g, " ").trim();
|
|
421
|
+
return last.length > max ? `${last.slice(0, max)}\u2026` : last;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ../src/lib/adapters/shared.ts
|
|
425
|
+
var TOOL_OPS = [
|
|
426
|
+
[/^(Read|NotebookRead|read_file|view_image)$/i, "read"],
|
|
427
|
+
[/^(Write|write_file)$/i, "write"],
|
|
428
|
+
[/^(Edit|MultiEdit|NotebookEdit|apply_patch)$/i, "edit"],
|
|
429
|
+
[/^(Bash|BashOutput|Shell|exec_command|local_shell|write_stdin)$/i, "run"],
|
|
430
|
+
[/^(Grep|Glob|LS|Search|WebSearch|web_search)$/i, "search"],
|
|
431
|
+
[/^(WebFetch|Fetch)$/i, "fetch"],
|
|
432
|
+
[/^(Task|Agent)$/i, "agent"]
|
|
433
|
+
];
|
|
434
|
+
function toolOpOf(name) {
|
|
435
|
+
return TOOL_OPS.find(([re]) => re.test(name))?.[1] ?? "other";
|
|
436
|
+
}
|
|
437
|
+
var TOOL_CALL_LIMIT = 800;
|
|
438
|
+
var ARTIFACT_CALL_LIMIT = 32e3;
|
|
439
|
+
var TOOL_RESULT_LIMIT = 4e3;
|
|
440
|
+
var clipText = (s, limit) => s.length > limit ? { text: `${s.slice(0, limit)}
|
|
441
|
+
\u2026[truncated, ${s.length} chars total]`, truncated: true } : { text: s, truncated: false };
|
|
442
|
+
var SELF_MARKS = ["<command-name>/thoughtdag</command-name>", "[[thoughtdag:command]]"];
|
|
443
|
+
function dropSelfCommandTurns(turns) {
|
|
444
|
+
return turns.filter((t) => !SELF_MARKS.some((m) => t.question.includes(m)));
|
|
445
|
+
}
|
|
446
|
+
function markImporterNote(note) {
|
|
447
|
+
note.width = 460;
|
|
448
|
+
}
|
|
449
|
+
function seedPlaque(node) {
|
|
450
|
+
const line = conclusionOf(node.data.response, 90);
|
|
451
|
+
if (line) node.data.summaries = [line];
|
|
452
|
+
}
|
|
453
|
+
function toolAttachments(turn) {
|
|
454
|
+
return turn.tools.map((tool) => ({
|
|
455
|
+
id: generateId(),
|
|
456
|
+
name: `tool: ${tool.name}${tool.truncated ? " (truncated)" : ""}`,
|
|
457
|
+
type: "text/plain",
|
|
458
|
+
size: tool.call.length + tool.result.length,
|
|
459
|
+
content: `[call] ${tool.call}
|
|
460
|
+
[result]
|
|
461
|
+
${tool.result}`,
|
|
462
|
+
...tool.paths?.length ? { paths: tool.paths } : {},
|
|
463
|
+
...tool.op ? { op: tool.op } : {}
|
|
464
|
+
}));
|
|
465
|
+
}
|
|
466
|
+
function toolScope(input) {
|
|
467
|
+
const i = input ?? {};
|
|
468
|
+
const out = {};
|
|
469
|
+
if (typeof i.url === "string" && /^https?:\/\//.test(i.url)) out.url = i.url;
|
|
470
|
+
const loc = {};
|
|
471
|
+
if (typeof i.pages === "string" && i.pages.trim()) loc.pages = i.pages.trim();
|
|
472
|
+
if (typeof i.offset === "number" || typeof i.limit === "number") {
|
|
473
|
+
const start = Math.max(1, typeof i.offset === "number" ? i.offset : 1);
|
|
474
|
+
const end = typeof i.limit === "number" ? start + Math.max(0, i.limit) - 1 : start;
|
|
475
|
+
loc.lines = [start, Math.max(start, end)];
|
|
476
|
+
}
|
|
477
|
+
if (Object.keys(loc).length) out.locator = loc;
|
|
478
|
+
return out;
|
|
479
|
+
}
|
|
480
|
+
function toolPaths(input) {
|
|
481
|
+
const i = input ?? {};
|
|
482
|
+
const p = i.file_path ?? i.notebook_path;
|
|
483
|
+
return typeof p === "string" && p ? [p] : [];
|
|
484
|
+
}
|
|
485
|
+
function renderCall(name, input) {
|
|
486
|
+
const i = input ?? {};
|
|
487
|
+
const op = toolOpOf(name);
|
|
488
|
+
if (op === "write" && typeof i.content === "string") return `${i.file_path ?? ""}
|
|
489
|
+
|
|
490
|
+
${i.content}`;
|
|
491
|
+
if (op === "edit" && typeof i.new_string === "string") {
|
|
492
|
+
return `${i.file_path ?? i.notebook_path ?? ""}${i.replace_all ? " (replace all)" : ""}
|
|
493
|
+
--- old
|
|
494
|
+
${i.old_string ?? ""}
|
|
495
|
+
+++ new
|
|
496
|
+
${i.new_string}`;
|
|
497
|
+
}
|
|
498
|
+
return JSON.stringify(input ?? {});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ../src/lib/adapters/claude-code-session.ts
|
|
502
|
+
var NOTIFICATION_LIMIT = 16e3;
|
|
503
|
+
function textParts(content) {
|
|
504
|
+
if (typeof content === "string") return content;
|
|
505
|
+
if (!Array.isArray(content)) return "";
|
|
506
|
+
return content.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
|
|
507
|
+
}
|
|
508
|
+
function resultText(content) {
|
|
509
|
+
if (typeof content === "string") return content;
|
|
510
|
+
if (!Array.isArray(content)) return "";
|
|
511
|
+
return content.map((p) => p.type === "text" && p.text ? p.text : p.type === "image" ? "[image result omitted]" : "").filter(Boolean).join("\n");
|
|
512
|
+
}
|
|
513
|
+
var clip = clipText;
|
|
514
|
+
var ClaudeSessionCollector = class {
|
|
515
|
+
turns = [];
|
|
516
|
+
// tool_use id → registration, so results pair up even across lines
|
|
517
|
+
pendingTools = /* @__PURE__ */ new Map();
|
|
518
|
+
current = null;
|
|
519
|
+
pendingCompaction;
|
|
520
|
+
sessionId = null;
|
|
521
|
+
customTitle = null;
|
|
522
|
+
summary = null;
|
|
523
|
+
slug = null;
|
|
524
|
+
firstQuestion = null;
|
|
525
|
+
cwd = null;
|
|
526
|
+
// decided by the first message line: what kind of file this is
|
|
527
|
+
mode = null;
|
|
528
|
+
feedLine(raw) {
|
|
529
|
+
const t = raw.trim();
|
|
530
|
+
if (!t) return;
|
|
531
|
+
let line;
|
|
532
|
+
try {
|
|
533
|
+
line = JSON.parse(t);
|
|
534
|
+
} catch {
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
this.feed(line);
|
|
538
|
+
}
|
|
539
|
+
flush() {
|
|
540
|
+
const c = this.current;
|
|
541
|
+
if (c && (c.question || c.response || c.tools.length)) this.turns.push(c);
|
|
542
|
+
this.current = null;
|
|
543
|
+
}
|
|
544
|
+
feed(line) {
|
|
545
|
+
if (line.type === "custom-title" && line.customTitle) this.customTitle = line.customTitle;
|
|
546
|
+
if (line.type === "summary" && line.summary && !this.summary) this.summary = line.summary;
|
|
547
|
+
if (line.slug && !this.slug) this.slug = line.slug;
|
|
548
|
+
if (line.cwd && !this.cwd) this.cwd = line.cwd;
|
|
549
|
+
if (this.mode === null && (line.type === "user" || line.type === "assistant") && line.uuid) {
|
|
550
|
+
this.mode = line.isSidechain && line.agentId ? "sidechain" : "main";
|
|
551
|
+
if (this.mode === "sidechain") this.sessionId = line.agentId;
|
|
552
|
+
}
|
|
553
|
+
if (!!line.isSidechain !== (this.mode === "sidechain")) return;
|
|
554
|
+
if (line.type === "system" && line.subtype === "compact_boundary") {
|
|
555
|
+
this.flush();
|
|
556
|
+
const m = line.compactMetadata;
|
|
557
|
+
this.pendingCompaction = `[Compaction] The source runner compacted its history here${m?.preTokens ? ` (${m.preTokens} \u2192 ${m.postTokens ?? "?"} tokens)` : ""}. Everything above this point reached later turns only as a summary.`;
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if ((line.type === "user" || line.type === "assistant") && line.uuid && line.sessionId && !this.sessionId) {
|
|
561
|
+
this.sessionId = line.sessionId;
|
|
562
|
+
}
|
|
563
|
+
if (line.type === "user") {
|
|
564
|
+
const content = line.message?.content;
|
|
565
|
+
if (Array.isArray(content)) {
|
|
566
|
+
for (const p of content) {
|
|
567
|
+
if (p.type === "tool_result" && p.tool_use_id) {
|
|
568
|
+
const reg = this.pendingTools.get(p.tool_use_id);
|
|
569
|
+
if (reg && this.current) {
|
|
570
|
+
const res = clip(resultText(p.content), TOOL_RESULT_LIMIT);
|
|
571
|
+
this.current.tools.push({
|
|
572
|
+
name: reg.name,
|
|
573
|
+
call: reg.call,
|
|
574
|
+
result: res.text,
|
|
575
|
+
truncated: res.truncated,
|
|
576
|
+
...reg.paths.length ? { paths: reg.paths } : {},
|
|
577
|
+
op: reg.op,
|
|
578
|
+
nativeCallId: p.tool_use_id,
|
|
579
|
+
...reg.url ? { url: reg.url } : {},
|
|
580
|
+
...reg.locator ? { locator: reg.locator } : {}
|
|
581
|
+
});
|
|
582
|
+
this.pendingTools.delete(p.tool_use_id);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const text = textParts(content);
|
|
588
|
+
if (!text.trim()) return;
|
|
589
|
+
if (/^\s*<task-notification>/.test(text)) {
|
|
590
|
+
if (this.current) {
|
|
591
|
+
const res = clip(text, NOTIFICATION_LIMIT);
|
|
592
|
+
this.current.tools.push({ name: "Agent", call: "(task notification)", result: res.text, truncated: res.truncated, op: "agent" });
|
|
593
|
+
}
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
{
|
|
597
|
+
this.flush();
|
|
598
|
+
if (!this.firstQuestion) this.firstQuestion = text.trim();
|
|
599
|
+
this.current = {
|
|
600
|
+
question: text,
|
|
601
|
+
response: "",
|
|
602
|
+
itemIds: line.uuid ? [line.uuid] : [],
|
|
603
|
+
tools: [],
|
|
604
|
+
parentItemId: line.parentUuid ?? void 0,
|
|
605
|
+
...line.timestamp ? { at: line.timestamp } : {}
|
|
606
|
+
};
|
|
607
|
+
if (this.pendingCompaction) {
|
|
608
|
+
this.current.compactionBefore = this.pendingCompaction;
|
|
609
|
+
this.pendingCompaction = void 0;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if (line.type === "assistant" && this.current) {
|
|
615
|
+
if (line.uuid) this.current.itemIds.push(line.uuid);
|
|
616
|
+
const content = line.message?.content;
|
|
617
|
+
const text = textParts(content);
|
|
618
|
+
if (text.trim()) this.current.response = this.current.response ? `${this.current.response}
|
|
619
|
+
|
|
620
|
+
${text}` : text;
|
|
621
|
+
if (Array.isArray(content)) {
|
|
622
|
+
for (const p of content) {
|
|
623
|
+
if (p.type === "tool_use" && p.id && p.name) {
|
|
624
|
+
const op = toolOpOf(p.name);
|
|
625
|
+
const call = clip(renderCall(p.name, p.input), op === "write" || op === "edit" ? ARTIFACT_CALL_LIMIT : TOOL_CALL_LIMIT);
|
|
626
|
+
this.pendingTools.set(p.id, { name: p.name, call: call.text, paths: toolPaths(p.input), op, ...toolScope(p.input) });
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
finish() {
|
|
633
|
+
this.flush();
|
|
634
|
+
if (!this.sessionId) return null;
|
|
635
|
+
const asked = this.firstQuestion?.split("\n").map((l) => l.trim()).find((l) => l && !l.startsWith("<"))?.slice(0, 80) ?? null;
|
|
636
|
+
const title = this.customTitle ?? this.summary ?? asked ?? this.slug ?? `session ${this.sessionId.slice(0, 8)}`;
|
|
637
|
+
return { sessionId: this.sessionId, title, turns: dropSelfCommandTurns(this.turns), ...this.cwd ? { cwd: this.cwd } : {} };
|
|
638
|
+
}
|
|
639
|
+
toConversation() {
|
|
640
|
+
const s = this.finish();
|
|
641
|
+
if (!s || s.turns.length === 0) return null;
|
|
642
|
+
return {
|
|
643
|
+
title: s.title,
|
|
644
|
+
messageCount: s.turns.length,
|
|
645
|
+
source: "claude-code",
|
|
646
|
+
sessionId: s.sessionId,
|
|
647
|
+
build: () => buildGraphFromTurns(s.turns, s.sessionId, s.cwd)
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
function noteNode(text) {
|
|
652
|
+
const n = makeNode(text, "", false);
|
|
653
|
+
n.data.stepKind = "note";
|
|
654
|
+
markImporterNote(n);
|
|
655
|
+
return n;
|
|
656
|
+
}
|
|
657
|
+
function buildGraphFromTurns(turns, sessionId, cwd) {
|
|
658
|
+
const origin = cwd ? { cwd } : {};
|
|
659
|
+
const nodes = [];
|
|
660
|
+
const edges = [];
|
|
661
|
+
let prev = null;
|
|
662
|
+
const byItem = /* @__PURE__ */ new Map();
|
|
663
|
+
const link = (source, target) => {
|
|
664
|
+
edges.push({ id: generateId(), source: source.id, target: target.id, type: "smoothstep" });
|
|
665
|
+
};
|
|
666
|
+
for (const turn of turns) {
|
|
667
|
+
let noteToWire = null;
|
|
668
|
+
if (turn.compactionBefore) {
|
|
669
|
+
const note = noteNode(turn.compactionBefore);
|
|
670
|
+
note.data.importSource = { runner: "claude-code", sessionId, itemIds: [], ...origin };
|
|
671
|
+
nodes.push(note);
|
|
672
|
+
noteToWire = note;
|
|
673
|
+
}
|
|
674
|
+
const node = makeNode(turn.question, turn.response, prev === null);
|
|
675
|
+
node.data.importSource = { runner: "claude-code", sessionId, itemIds: turn.itemIds, ...origin };
|
|
676
|
+
node.data.source = { question: node.data.question, response: node.data.response };
|
|
677
|
+
seedPlaque(node);
|
|
678
|
+
node.data.attachments = toolAttachments(turn);
|
|
679
|
+
nodes.push(node);
|
|
680
|
+
const parent = turn.parentItemId ? byItem.get(turn.parentItemId) : void 0;
|
|
681
|
+
if (parent) link(parent, node);
|
|
682
|
+
else if (prev) link(prev, node);
|
|
683
|
+
if (noteToWire) link(noteToWire, node);
|
|
684
|
+
for (const id of turn.itemIds) byItem.set(id, node);
|
|
685
|
+
prev = node;
|
|
686
|
+
}
|
|
687
|
+
return { nodes: autoLayout(nodes, edges), edges };
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// ../src/lib/adapters/codex-session.ts
|
|
691
|
+
function patchPaths(patch) {
|
|
692
|
+
const out = [];
|
|
693
|
+
for (const m of patch.matchAll(/^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm)) out.push(m[1].trim());
|
|
694
|
+
return out;
|
|
695
|
+
}
|
|
696
|
+
var clip2 = clipText;
|
|
697
|
+
var partText = (content) => (content ?? []).filter((p) => p.text && (p.type === "input_text" || p.type === "output_text" || p.type === "text")).map((p) => p.text).join("\n");
|
|
698
|
+
var INJECTED = /^\s*<\/?[a-zA-Z_][\w-]*[^>]*>/;
|
|
699
|
+
var isInjectedUserText = (text) => INJECTED.test(text) || text.trimStart().startsWith("# AGENTS.md instructions for ");
|
|
700
|
+
var userPartText = (content) => (content ?? []).filter((p) => p.text && (p.type === "input_text" || p.type === "text") && !isInjectedUserText(p.text)).map((p) => p.text).join("\n");
|
|
701
|
+
var outputText = (output) => {
|
|
702
|
+
if (typeof output === "string") return output;
|
|
703
|
+
if (Array.isArray(output)) return output.map((p) => p?.text ?? "").filter(Boolean).join("\n");
|
|
704
|
+
return output == null ? "" : JSON.stringify(output);
|
|
705
|
+
};
|
|
706
|
+
var CodexSessionCollector = class {
|
|
707
|
+
turns = [];
|
|
708
|
+
pendingCalls = /* @__PURE__ */ new Map();
|
|
709
|
+
current = null;
|
|
710
|
+
pendingCompaction;
|
|
711
|
+
sessionId = null;
|
|
712
|
+
cwd = "";
|
|
713
|
+
day = "";
|
|
714
|
+
sawItems = false;
|
|
715
|
+
firstQuestion = null;
|
|
716
|
+
parentThreadId = null;
|
|
717
|
+
feedLine(raw) {
|
|
718
|
+
const t = raw.trim();
|
|
719
|
+
if (!t) return;
|
|
720
|
+
let line;
|
|
721
|
+
try {
|
|
722
|
+
line = JSON.parse(t);
|
|
723
|
+
} catch {
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
this.feed(line);
|
|
727
|
+
}
|
|
728
|
+
flush() {
|
|
729
|
+
const c = this.current;
|
|
730
|
+
if (c && (c.question || c.response || c.tools.length)) this.turns.push(c);
|
|
731
|
+
this.current = null;
|
|
732
|
+
}
|
|
733
|
+
ensure() {
|
|
734
|
+
if (!this.current) {
|
|
735
|
+
this.current = { question: "", response: "", itemIds: [], tools: [] };
|
|
736
|
+
if (this.pendingCompaction) {
|
|
737
|
+
this.current.compactionBefore = this.pendingCompaction;
|
|
738
|
+
this.pendingCompaction = void 0;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return this.current;
|
|
742
|
+
}
|
|
743
|
+
feed(line) {
|
|
744
|
+
const p = line.payload ?? {};
|
|
745
|
+
if (line.type === "session_meta" && p.id && !this.sessionId) {
|
|
746
|
+
this.sessionId = p.id;
|
|
747
|
+
this.cwd = p.cwd ?? "";
|
|
748
|
+
this.day = p.timestamp?.slice(0, 10) ?? "";
|
|
749
|
+
this.parentThreadId = p.parent_thread_id ?? null;
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
if (line.type === "response_item" || line.type === "event_msg") this.sawItems = true;
|
|
753
|
+
if (line.type === "compacted") {
|
|
754
|
+
this.flush();
|
|
755
|
+
this.pendingCompaction = "[Compaction] The source runner compacted its history here. Everything above this point reached later turns only as a summary.";
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
if (line.type === "turn_context" || line.type === "event_msg" && p.type === "task_started") {
|
|
759
|
+
this.flush();
|
|
760
|
+
const t = this.ensure();
|
|
761
|
+
if (p.turn_id) t.itemIds.push(p.turn_id);
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
if (line.type !== "response_item") return;
|
|
765
|
+
if (p.type === "message") {
|
|
766
|
+
if (p.role === "user") {
|
|
767
|
+
const text = userPartText(p.content);
|
|
768
|
+
if (text.trim()) {
|
|
769
|
+
if (!this.firstQuestion) this.firstQuestion = text.trim();
|
|
770
|
+
const t = this.ensure();
|
|
771
|
+
if (!t.at && line.timestamp) t.at = line.timestamp;
|
|
772
|
+
t.question = t.question ? `${t.question}
|
|
773
|
+
|
|
774
|
+
${text}` : text;
|
|
775
|
+
}
|
|
776
|
+
} else if (p.role === "assistant") {
|
|
777
|
+
const text = partText(p.content);
|
|
778
|
+
if (text.trim()) {
|
|
779
|
+
const t = this.ensure();
|
|
780
|
+
t.response = t.response ? `${t.response}
|
|
781
|
+
|
|
782
|
+
${text}` : text;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
} else if ((p.type === "function_call" || p.type === "custom_tool_call") && p.call_id && p.name) {
|
|
786
|
+
const raw = String(p.arguments ?? p.input ?? "");
|
|
787
|
+
const op = toolOpOf(p.name);
|
|
788
|
+
const call = clip2(raw, op === "edit" || op === "write" ? ARTIFACT_CALL_LIMIT : TOOL_CALL_LIMIT);
|
|
789
|
+
const viewed = op === "read" ? (() => {
|
|
790
|
+
try {
|
|
791
|
+
const a = JSON.parse(raw);
|
|
792
|
+
return typeof a.path === "string" && a.path ? [a.path] : [];
|
|
793
|
+
} catch {
|
|
794
|
+
return [];
|
|
795
|
+
}
|
|
796
|
+
})() : [];
|
|
797
|
+
this.pendingCalls.set(p.call_id, { name: p.name, call: call.text, paths: op === "edit" ? patchPaths(raw) : viewed, op });
|
|
798
|
+
} else if ((p.type === "function_call_output" || p.type === "custom_tool_call_output") && p.call_id) {
|
|
799
|
+
const reg = this.pendingCalls.get(p.call_id);
|
|
800
|
+
if (reg) {
|
|
801
|
+
const t = this.ensure();
|
|
802
|
+
const res = clip2(outputText(p.output), TOOL_RESULT_LIMIT);
|
|
803
|
+
t.tools.push({
|
|
804
|
+
name: reg.name,
|
|
805
|
+
call: reg.call,
|
|
806
|
+
result: res.text,
|
|
807
|
+
truncated: res.truncated,
|
|
808
|
+
...reg.paths.length ? { paths: reg.paths } : {},
|
|
809
|
+
op: reg.op,
|
|
810
|
+
nativeCallId: p.call_id
|
|
811
|
+
});
|
|
812
|
+
this.pendingCalls.delete(p.call_id);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
finish() {
|
|
817
|
+
this.flush();
|
|
818
|
+
if (!this.sessionId || !this.sawItems) return null;
|
|
819
|
+
const folded = [];
|
|
820
|
+
let carriedCompaction;
|
|
821
|
+
for (const turn of this.turns) {
|
|
822
|
+
const prev = folded[folded.length - 1];
|
|
823
|
+
if (!turn.question.trim() && prev) {
|
|
824
|
+
if (turn.compactionBefore) carriedCompaction = carriedCompaction ?? turn.compactionBefore;
|
|
825
|
+
if (turn.response) prev.response = prev.response ? `${prev.response}
|
|
826
|
+
|
|
827
|
+
${turn.response}` : turn.response;
|
|
828
|
+
prev.tools.push(...turn.tools);
|
|
829
|
+
prev.itemIds.push(...turn.itemIds);
|
|
830
|
+
} else {
|
|
831
|
+
if (carriedCompaction && !turn.compactionBefore) turn.compactionBefore = carriedCompaction;
|
|
832
|
+
carriedCompaction = void 0;
|
|
833
|
+
folded.push(turn);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
this.turns = folded;
|
|
837
|
+
const dir = this.cwd.split("/").filter(Boolean).pop();
|
|
838
|
+
const title = this.firstQuestion?.split("\n")[0].slice(0, 60) || ["codex", dir, this.day].filter(Boolean).join(" \xB7 ");
|
|
839
|
+
return { sessionId: this.sessionId, title, turns: dropSelfCommandTurns(this.turns), subagent: !!this.parentThreadId, ...this.cwd ? { cwd: this.cwd } : {} };
|
|
840
|
+
}
|
|
841
|
+
toConversation() {
|
|
842
|
+
const s = this.finish();
|
|
843
|
+
if (!s || s.turns.length === 0) return null;
|
|
844
|
+
return {
|
|
845
|
+
title: s.title,
|
|
846
|
+
messageCount: s.turns.length,
|
|
847
|
+
source: "codex",
|
|
848
|
+
sessionId: s.sessionId,
|
|
849
|
+
build: () => buildGraphFromTurns2(s.turns, s.sessionId, s.cwd)
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
function buildGraphFromTurns2(turns, sessionId, cwd) {
|
|
854
|
+
const origin = cwd ? { cwd } : {};
|
|
855
|
+
const nodes = [];
|
|
856
|
+
const edges = [];
|
|
857
|
+
let prev = null;
|
|
858
|
+
for (const turn of turns) {
|
|
859
|
+
let noteToWire = null;
|
|
860
|
+
if (turn.compactionBefore) {
|
|
861
|
+
const note = makeNode(turn.compactionBefore, "", false);
|
|
862
|
+
note.data.stepKind = "note";
|
|
863
|
+
note.data.importSource = { runner: "codex", sessionId, itemIds: [], ...origin };
|
|
864
|
+
markImporterNote(note);
|
|
865
|
+
nodes.push(note);
|
|
866
|
+
noteToWire = note;
|
|
867
|
+
}
|
|
868
|
+
const node = makeNode(turn.question || "(tool-only turn)", turn.response, prev === null);
|
|
869
|
+
node.data.importSource = { runner: "codex", sessionId, itemIds: turn.itemIds, ...origin };
|
|
870
|
+
node.data.source = { question: node.data.question, response: node.data.response };
|
|
871
|
+
node.data.attachments = toolAttachments(turn);
|
|
872
|
+
seedPlaque(node);
|
|
873
|
+
nodes.push(node);
|
|
874
|
+
if (prev) edges.push({ id: generateId(), source: prev.id, target: node.id, type: "smoothstep" });
|
|
875
|
+
if (noteToWire) edges.push({ id: generateId(), source: noteToWire.id, target: node.id, type: "smoothstep" });
|
|
876
|
+
prev = node;
|
|
877
|
+
}
|
|
878
|
+
return { nodes: autoLayout(nodes, edges), edges };
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// ../src/lib/adapters/dsh-session.ts
|
|
882
|
+
function partsText(content, depth = 0) {
|
|
883
|
+
if (typeof content === "string") return content;
|
|
884
|
+
if (!Array.isArray(content) || depth > 2) return "";
|
|
885
|
+
return content.map((p) => {
|
|
886
|
+
if (!p || typeof p !== "object") return "";
|
|
887
|
+
const o = p;
|
|
888
|
+
if (o.type === "text" && typeof o.text === "string") return o.text;
|
|
889
|
+
if (o.type === "image") return "[image omitted]";
|
|
890
|
+
if (o.type === "reasoning") return "";
|
|
891
|
+
return partsText(o.text ?? o.content, depth + 1);
|
|
892
|
+
}).filter(Boolean).join("\n");
|
|
893
|
+
}
|
|
894
|
+
function renderRootCall(name, argumentsJson, args) {
|
|
895
|
+
const op = toolOpOf(name);
|
|
896
|
+
if (op === "run" || /^run_code$/i.test(name)) {
|
|
897
|
+
const a = args ?? {};
|
|
898
|
+
return clipText(typeof a.code === "string" ? a.code : argumentsJson, ARTIFACT_CALL_LIMIT);
|
|
899
|
+
}
|
|
900
|
+
const body = args && typeof args === "object" ? renderCall(name, args) : argumentsJson;
|
|
901
|
+
return clipText(body, op === "write" || op === "edit" ? ARTIFACT_CALL_LIMIT : TOOL_CALL_LIMIT);
|
|
902
|
+
}
|
|
903
|
+
var parseArgs = (json) => {
|
|
904
|
+
try {
|
|
905
|
+
return JSON.parse(json ?? "");
|
|
906
|
+
} catch {
|
|
907
|
+
return void 0;
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
var DshSessionCollector = class {
|
|
911
|
+
turns = [];
|
|
912
|
+
pendingTools = /* @__PURE__ */ new Map();
|
|
913
|
+
current = null;
|
|
914
|
+
sessionId = null;
|
|
915
|
+
cwd = null;
|
|
916
|
+
title = null;
|
|
917
|
+
firstQuestion = null;
|
|
918
|
+
feedLine(raw) {
|
|
919
|
+
const t = raw.trim();
|
|
920
|
+
if (!t) return;
|
|
921
|
+
let line;
|
|
922
|
+
try {
|
|
923
|
+
line = JSON.parse(t);
|
|
924
|
+
} catch {
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
this.feed(line);
|
|
928
|
+
}
|
|
929
|
+
flush() {
|
|
930
|
+
const c = this.current;
|
|
931
|
+
if (c && (c.question || c.response || c.tools.length)) this.turns.push(c);
|
|
932
|
+
this.current = null;
|
|
933
|
+
}
|
|
934
|
+
ensure() {
|
|
935
|
+
if (!this.current) this.current = { question: "", response: "", itemIds: [], tools: [] };
|
|
936
|
+
return this.current;
|
|
937
|
+
}
|
|
938
|
+
feed(line) {
|
|
939
|
+
if (line.type === "session" && line.id && !("timestamp" in line)) {
|
|
940
|
+
if (!this.sessionId) this.sessionId = line.id;
|
|
941
|
+
if (line.cwd && !this.cwd) this.cwd = line.cwd;
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
if (line.type === "session/title" && line.data?.title && !this.title) {
|
|
945
|
+
this.title = line.data.title;
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
if (line.type === "user/message") {
|
|
949
|
+
const um = line;
|
|
950
|
+
const kind = um.data?.source?.kind;
|
|
951
|
+
if (kind && kind !== "user") return;
|
|
952
|
+
const text = partsText(um.data?.content).trim();
|
|
953
|
+
if (!text) return;
|
|
954
|
+
if (!this.firstQuestion) this.firstQuestion = text.split("\n")[0].slice(0, 80);
|
|
955
|
+
this.flush();
|
|
956
|
+
const t = this.ensure();
|
|
957
|
+
t.question = text;
|
|
958
|
+
t.itemIds = [um.data?.id ?? `u${um.seq}`];
|
|
959
|
+
if (um.time) t.at = new Date(um.time).toISOString();
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
if (line.type === "assistant/message" && this.current) {
|
|
963
|
+
const am = line;
|
|
964
|
+
const parts = am.data?.message?.content;
|
|
965
|
+
const text = partsText(parts).trim();
|
|
966
|
+
const id = am.data?.message?.id;
|
|
967
|
+
if (id) this.current.itemIds.push(id);
|
|
968
|
+
if (text) this.current.response = this.current.response ? `${this.current.response}
|
|
969
|
+
|
|
970
|
+
${text}` : text;
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
if (line.type === "tool/call") {
|
|
974
|
+
const tc = line;
|
|
975
|
+
if (!tc.data?.callId || !tc.data?.name) return;
|
|
976
|
+
const op = /^run_code$/i.test(tc.data.name) ? "run" : toolOpOf(tc.data.name);
|
|
977
|
+
const args = parseArgs(tc.data.arguments);
|
|
978
|
+
const call = renderRootCall(tc.data.name, tc.data.arguments ?? "", args);
|
|
979
|
+
const scope = op === "run" ? {} : toolScope(args);
|
|
980
|
+
this.pendingTools.set(tc.data.callId, { name: tc.data.name, call: call.text, truncated: call.truncated, op, paths: op === "run" ? [] : toolPaths(args), ...scope });
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
if (line.type === "tool/result") {
|
|
984
|
+
const tr = line;
|
|
985
|
+
const callId = tr.data?.message?.source?.callId;
|
|
986
|
+
if (!callId) return;
|
|
987
|
+
const reg = this.pendingTools.get(callId);
|
|
988
|
+
if (!reg) return;
|
|
989
|
+
const res = clipText(partsText(tr.data?.message?.content), TOOL_RESULT_LIMIT);
|
|
990
|
+
const t = this.ensure();
|
|
991
|
+
t.tools.push({
|
|
992
|
+
name: reg.name,
|
|
993
|
+
call: reg.call,
|
|
994
|
+
result: res.text,
|
|
995
|
+
truncated: reg.truncated || res.truncated,
|
|
996
|
+
op: reg.op,
|
|
997
|
+
nativeCallId: callId,
|
|
998
|
+
...reg.paths.length ? { paths: reg.paths } : {},
|
|
999
|
+
...reg.url ? { url: reg.url } : {},
|
|
1000
|
+
...reg.locator ? { locator: reg.locator } : {}
|
|
1001
|
+
});
|
|
1002
|
+
this.pendingTools.delete(callId);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
if (line.type === "tool/code-dispatch") {
|
|
1006
|
+
const d = line.data;
|
|
1007
|
+
if (!d?.name || !d.subCallId) return;
|
|
1008
|
+
const paths = toolPaths(d.arguments);
|
|
1009
|
+
const scope = toolScope(d.arguments);
|
|
1010
|
+
if (!paths.length && !scope.url) return;
|
|
1011
|
+
const op = toolOpOf(d.name);
|
|
1012
|
+
const call = clipText(renderCall(d.name, d.arguments), op === "write" || op === "edit" ? ARTIFACT_CALL_LIMIT : TOOL_CALL_LIMIT);
|
|
1013
|
+
const res = clipText(partsText(d.content), TOOL_RESULT_LIMIT);
|
|
1014
|
+
this.ensure().tools.push({
|
|
1015
|
+
name: d.name,
|
|
1016
|
+
call: call.text,
|
|
1017
|
+
result: res.text,
|
|
1018
|
+
truncated: call.truncated || res.truncated,
|
|
1019
|
+
op,
|
|
1020
|
+
nativeCallId: d.subCallId,
|
|
1021
|
+
...paths.length ? { paths } : {},
|
|
1022
|
+
...scope.url ? { url: scope.url } : {},
|
|
1023
|
+
...scope.locator ? { locator: scope.locator } : {}
|
|
1024
|
+
});
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
finish() {
|
|
1029
|
+
this.flush();
|
|
1030
|
+
if (!this.sessionId) return null;
|
|
1031
|
+
const asked = this.firstQuestion ?? null;
|
|
1032
|
+
const title = this.title ?? asked ?? `session ${this.sessionId.replace(/^session-/, "").slice(0, 8)}`;
|
|
1033
|
+
return { sessionId: this.sessionId, title, turns: dropSelfCommandTurns(this.turns), ...this.cwd ? { cwd: this.cwd } : {} };
|
|
1034
|
+
}
|
|
1035
|
+
toConversation() {
|
|
1036
|
+
const s = this.finish();
|
|
1037
|
+
if (!s || s.turns.length === 0) return null;
|
|
1038
|
+
return {
|
|
1039
|
+
title: s.title,
|
|
1040
|
+
messageCount: s.turns.length,
|
|
1041
|
+
source: "dsh",
|
|
1042
|
+
sessionId: s.sessionId,
|
|
1043
|
+
build: () => buildGraphFromTurns3(s.turns, s.sessionId, s.cwd)
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
function buildGraphFromTurns3(turns, sessionId, cwd) {
|
|
1048
|
+
const origin = cwd ? { cwd } : {};
|
|
1049
|
+
const nodes = [];
|
|
1050
|
+
const edges = [];
|
|
1051
|
+
let prev = null;
|
|
1052
|
+
for (const turn of turns) {
|
|
1053
|
+
const node = makeNode(turn.question || "(tool-only turn)", turn.response, prev === null);
|
|
1054
|
+
node.data.importSource = { runner: "dsh", sessionId, itemIds: turn.itemIds, ...origin };
|
|
1055
|
+
node.data.source = { question: node.data.question, response: node.data.response };
|
|
1056
|
+
node.data.attachments = toolAttachments(turn);
|
|
1057
|
+
seedPlaque(node);
|
|
1058
|
+
nodes.push(node);
|
|
1059
|
+
if (prev) edges.push({ id: generateId(), source: prev.id, target: node.id, type: "smoothstep" });
|
|
1060
|
+
prev = node;
|
|
1061
|
+
}
|
|
1062
|
+
return { nodes: autoLayout(nodes, edges), edges };
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// ../src/lib/adapters/pi-session.ts
|
|
1066
|
+
var textOf = (content) => {
|
|
1067
|
+
if (typeof content === "string") return content;
|
|
1068
|
+
if (!Array.isArray(content)) return "";
|
|
1069
|
+
return content.filter((b) => b && b.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n");
|
|
1070
|
+
};
|
|
1071
|
+
function renderCall2(name, args) {
|
|
1072
|
+
const a = args && typeof args === "object" ? args : {};
|
|
1073
|
+
const op = toolOpOf(name);
|
|
1074
|
+
const paths = typeof a.path === "string" && a.path && (op === "read" || op === "edit" || op === "write") ? [a.path] : [];
|
|
1075
|
+
let locator;
|
|
1076
|
+
if (op === "read" && (typeof a.offset === "number" || typeof a.limit === "number")) {
|
|
1077
|
+
const start = Math.max(1, typeof a.offset === "number" ? a.offset : 1);
|
|
1078
|
+
const end = typeof a.limit === "number" ? start + Math.max(0, a.limit) - 1 : start;
|
|
1079
|
+
locator = { lines: [start, Math.max(start, end)] };
|
|
1080
|
+
}
|
|
1081
|
+
if (op === "write" && typeof a.content === "string") {
|
|
1082
|
+
const c2 = clipText(`${a.path ?? ""}
|
|
1083
|
+
|
|
1084
|
+
${a.content}`, ARTIFACT_CALL_LIMIT);
|
|
1085
|
+
return { ...c2, paths };
|
|
1086
|
+
}
|
|
1087
|
+
if (op === "edit") {
|
|
1088
|
+
const edits = Array.isArray(a.edits) ? a.edits : typeof a.newText === "string" ? [{ oldText: a.oldText, newText: a.newText }] : [];
|
|
1089
|
+
if (edits.length) {
|
|
1090
|
+
const body = edits.map((e) => `${a.path ?? ""}
|
|
1091
|
+
--- old
|
|
1092
|
+
${e.oldText ?? ""}
|
|
1093
|
+
+++ new
|
|
1094
|
+
${e.newText ?? ""}`).join("\n\n");
|
|
1095
|
+
const c2 = clipText(body, ARTIFACT_CALL_LIMIT);
|
|
1096
|
+
return { ...c2, paths };
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
if (op === "run" && typeof a.command === "string") {
|
|
1100
|
+
const c2 = clipText(a.command, TOOL_CALL_LIMIT);
|
|
1101
|
+
return { ...c2, paths };
|
|
1102
|
+
}
|
|
1103
|
+
const c = clipText(JSON.stringify(args ?? {}), TOOL_CALL_LIMIT);
|
|
1104
|
+
return { ...c, paths, ...locator ? { locator } : {} };
|
|
1105
|
+
}
|
|
1106
|
+
var PiSessionCollector = class {
|
|
1107
|
+
turns = [];
|
|
1108
|
+
pending = /* @__PURE__ */ new Map();
|
|
1109
|
+
current = null;
|
|
1110
|
+
sessionId = null;
|
|
1111
|
+
cwd = null;
|
|
1112
|
+
firstQuestion = null;
|
|
1113
|
+
feedLine(raw) {
|
|
1114
|
+
const t = raw.trim();
|
|
1115
|
+
if (!t) return;
|
|
1116
|
+
let e;
|
|
1117
|
+
try {
|
|
1118
|
+
e = JSON.parse(t);
|
|
1119
|
+
} catch {
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
this.feed(e);
|
|
1123
|
+
}
|
|
1124
|
+
flush() {
|
|
1125
|
+
const c = this.current;
|
|
1126
|
+
if (c && (c.question || c.response || c.tools.length)) this.turns.push(c);
|
|
1127
|
+
this.current = null;
|
|
1128
|
+
}
|
|
1129
|
+
feed(e) {
|
|
1130
|
+
if (e.type === "session" && typeof e.id === "string") {
|
|
1131
|
+
if (!this.sessionId) this.sessionId = e.id;
|
|
1132
|
+
if (typeof e.cwd === "string" && !this.cwd) this.cwd = e.cwd;
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
if (e.type !== "message") {
|
|
1136
|
+
if (this.current && typeof e.id === "string") this.current.itemIds.push(e.id);
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
const m = e.message;
|
|
1140
|
+
if (!m) return;
|
|
1141
|
+
if (m.role === "user") {
|
|
1142
|
+
const text = textOf(m.content).trim();
|
|
1143
|
+
if (!text) return;
|
|
1144
|
+
this.flush();
|
|
1145
|
+
if (!this.firstQuestion) this.firstQuestion = text.split("\n")[0].slice(0, 80);
|
|
1146
|
+
const at = typeof e.timestamp === "string" ? e.timestamp : typeof m.timestamp === "number" ? new Date(m.timestamp).toISOString() : void 0;
|
|
1147
|
+
this.current = {
|
|
1148
|
+
question: text,
|
|
1149
|
+
response: "",
|
|
1150
|
+
itemIds: typeof e.id === "string" ? [e.id] : [],
|
|
1151
|
+
tools: [],
|
|
1152
|
+
...typeof e.parentId === "string" ? { parentItemId: e.parentId } : {},
|
|
1153
|
+
...at ? { at } : {}
|
|
1154
|
+
};
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
if (m.role === "assistant") {
|
|
1158
|
+
const cur = this.current ?? (this.current = { question: "", response: "", itemIds: [], tools: [] });
|
|
1159
|
+
if (typeof e.id === "string") cur.itemIds.push(e.id);
|
|
1160
|
+
const text = textOf(m.content).trim();
|
|
1161
|
+
if (text) cur.response = cur.response ? `${cur.response}
|
|
1162
|
+
|
|
1163
|
+
${text}` : text;
|
|
1164
|
+
if (Array.isArray(m.content)) {
|
|
1165
|
+
for (const b of m.content) {
|
|
1166
|
+
if (b?.type !== "toolCall" || typeof b.id !== "string" || typeof b.name !== "string") continue;
|
|
1167
|
+
const r = renderCall2(b.name, b.arguments);
|
|
1168
|
+
this.pending.set(b.id, { name: b.name, call: r.text, truncated: r.truncated, op: toolOpOf(b.name), paths: r.paths, ...r.locator ? { locator: r.locator } : {} });
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
if (m.role === "toolResult") {
|
|
1174
|
+
const id = m.toolCallId;
|
|
1175
|
+
if (!id) return;
|
|
1176
|
+
const reg = this.pending.get(id);
|
|
1177
|
+
if (!reg) return;
|
|
1178
|
+
const cur = this.current ?? (this.current = { question: "", response: "", itemIds: [], tools: [] });
|
|
1179
|
+
if (typeof e.id === "string") cur.itemIds.push(e.id);
|
|
1180
|
+
const res = clipText(textOf(m.content), TOOL_RESULT_LIMIT);
|
|
1181
|
+
cur.tools.push({
|
|
1182
|
+
name: reg.name,
|
|
1183
|
+
call: reg.call,
|
|
1184
|
+
result: res.text,
|
|
1185
|
+
truncated: reg.truncated || res.truncated,
|
|
1186
|
+
op: reg.op,
|
|
1187
|
+
nativeCallId: id,
|
|
1188
|
+
...reg.paths.length ? { paths: reg.paths } : {},
|
|
1189
|
+
...reg.locator ? { locator: reg.locator } : {}
|
|
1190
|
+
});
|
|
1191
|
+
this.pending.delete(id);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
finish() {
|
|
1195
|
+
this.flush();
|
|
1196
|
+
if (!this.sessionId) return null;
|
|
1197
|
+
const title = this.firstQuestion ?? `session ${this.sessionId.slice(0, 8)}`;
|
|
1198
|
+
return { sessionId: this.sessionId, title, turns: dropSelfCommandTurns(this.turns), ...this.cwd ? { cwd: this.cwd } : {} };
|
|
1199
|
+
}
|
|
1200
|
+
toConversation() {
|
|
1201
|
+
const s = this.finish();
|
|
1202
|
+
if (!s || s.turns.length === 0) return null;
|
|
1203
|
+
return {
|
|
1204
|
+
title: s.title,
|
|
1205
|
+
messageCount: s.turns.length,
|
|
1206
|
+
source: "pi",
|
|
1207
|
+
sessionId: s.sessionId,
|
|
1208
|
+
build: () => buildGraphFromTurns4(s.turns, s.sessionId, s.cwd)
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
function buildGraphFromTurns4(turns, sessionId, cwd) {
|
|
1213
|
+
const origin = cwd ? { cwd } : {};
|
|
1214
|
+
const nodes = [];
|
|
1215
|
+
const edges = [];
|
|
1216
|
+
const byItem = /* @__PURE__ */ new Map();
|
|
1217
|
+
const link = (a, b) => edges.push({ id: generateId(), source: a.id, target: b.id, type: "smoothstep" });
|
|
1218
|
+
let prev = null;
|
|
1219
|
+
for (const turn of turns) {
|
|
1220
|
+
const node = makeNode(turn.question || "(tool-only turn)", turn.response, prev === null);
|
|
1221
|
+
node.data.importSource = { runner: "pi", sessionId, itemIds: turn.itemIds, ...origin };
|
|
1222
|
+
node.data.source = { question: node.data.question, response: node.data.response };
|
|
1223
|
+
node.data.attachments = toolAttachments(turn);
|
|
1224
|
+
seedPlaque(node);
|
|
1225
|
+
nodes.push(node);
|
|
1226
|
+
const parent = turn.parentItemId ? byItem.get(turn.parentItemId) : void 0;
|
|
1227
|
+
if (parent) link(parent, node);
|
|
1228
|
+
else if (prev) link(prev, node);
|
|
1229
|
+
for (const id of turn.itemIds) byItem.set(id, node);
|
|
1230
|
+
prev = node;
|
|
1231
|
+
}
|
|
1232
|
+
return { nodes: autoLayout(nodes, edges), edges };
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// ../src/lib/adapters/dsh-zstd.ts
|
|
1236
|
+
var ZSTD_MAGIC = 4247762216;
|
|
1237
|
+
function scanZstdFrames(buf) {
|
|
1238
|
+
const frames = [];
|
|
1239
|
+
const u32 = (o) => (buf[o] | buf[o + 1] << 8 | buf[o + 2] << 16 | buf[o + 3] << 24) >>> 0;
|
|
1240
|
+
let offset = 0;
|
|
1241
|
+
while (offset < buf.length) {
|
|
1242
|
+
const start = offset;
|
|
1243
|
+
if (buf.length - offset < 4) break;
|
|
1244
|
+
if (u32(offset) !== ZSTD_MAGIC) return frames;
|
|
1245
|
+
offset += 4;
|
|
1246
|
+
if (offset >= buf.length) break;
|
|
1247
|
+
const descriptor = buf[offset];
|
|
1248
|
+
offset += 1;
|
|
1249
|
+
const contentSizeFlag = descriptor >>> 6;
|
|
1250
|
+
const singleSegment = (descriptor & 32) !== 0;
|
|
1251
|
+
const checksum = (descriptor & 4) !== 0;
|
|
1252
|
+
const dictFlag = descriptor & 3;
|
|
1253
|
+
const dictBytes = dictFlag === 3 ? 4 : dictFlag;
|
|
1254
|
+
const contentSizeBytes = contentSizeFlag === 0 ? singleSegment ? 1 : 0 : 1 << contentSizeFlag;
|
|
1255
|
+
const restOfHeader = (singleSegment ? 0 : 1) + dictBytes + contentSizeBytes;
|
|
1256
|
+
if (buf.length - offset < restOfHeader) break;
|
|
1257
|
+
offset += restOfHeader;
|
|
1258
|
+
for (; ; ) {
|
|
1259
|
+
if (buf.length - offset < 3) return frames;
|
|
1260
|
+
const header = buf[offset] | buf[offset + 1] << 8 | buf[offset + 2] << 16;
|
|
1261
|
+
offset += 3;
|
|
1262
|
+
const last = (header & 1) !== 0;
|
|
1263
|
+
const type = header >>> 1 & 3;
|
|
1264
|
+
const size = header >>> 3;
|
|
1265
|
+
const payload = type === 1 ? 1 : size;
|
|
1266
|
+
if (buf.length - offset < payload) return frames;
|
|
1267
|
+
offset += payload;
|
|
1268
|
+
if (last) break;
|
|
1269
|
+
}
|
|
1270
|
+
if (checksum) {
|
|
1271
|
+
if (buf.length - offset < 4) return frames;
|
|
1272
|
+
offset += 4;
|
|
1273
|
+
}
|
|
1274
|
+
frames.push({ start, end: offset });
|
|
1275
|
+
}
|
|
1276
|
+
return frames;
|
|
1277
|
+
}
|
|
1278
|
+
function decompressZstdFrames(buf, inflate) {
|
|
1279
|
+
const parts = scanZstdFrames(buf).map((f) => inflate(buf.subarray(f.start, f.end)));
|
|
1280
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
1281
|
+
let o = 0;
|
|
1282
|
+
for (const p of parts) {
|
|
1283
|
+
out.set(p, o);
|
|
1284
|
+
o += p.length;
|
|
1285
|
+
}
|
|
1286
|
+
return out;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// ../src/lib/events/project.ts
|
|
1290
|
+
var EXCERPT_CHARS = 200;
|
|
1291
|
+
var OBS_FULL = { basis: "observed", completeness: "full" };
|
|
1292
|
+
var OBS_PART = { basis: "observed", completeness: "partial" };
|
|
1293
|
+
var clip3 = (s, max = EXCERPT_CHARS) => {
|
|
1294
|
+
const t = s.trim();
|
|
1295
|
+
return t.length > max ? `${t.slice(0, max)}\u2026` : t;
|
|
1296
|
+
};
|
|
1297
|
+
var sessionKey = (runner, nativeId) => `${runner}:${nativeId}`;
|
|
1298
|
+
function fragmentTag(sourceId) {
|
|
1299
|
+
let h = 5381;
|
|
1300
|
+
for (let i = 0; i < sourceId.length; i++) h = (h << 5) + h + sourceId.charCodeAt(i) | 0;
|
|
1301
|
+
return (h >>> 0).toString(36);
|
|
1302
|
+
}
|
|
1303
|
+
function turnKeys(session, turns) {
|
|
1304
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1305
|
+
return turns.map((t, i) => {
|
|
1306
|
+
const base = t.itemIds[0] ?? `i${i}`;
|
|
1307
|
+
const n = (seen.get(base) ?? 0) + 1;
|
|
1308
|
+
seen.set(base, n);
|
|
1309
|
+
return n === 1 ? `${session}#${base}` : `${session}#${base}~${n}`;
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
var isWindowsAbs = (p) => /^[A-Za-z]:[\\/]/.test(p);
|
|
1313
|
+
function absolutePath(observed, cwd) {
|
|
1314
|
+
const p = observed.replace(/\\/g, "/");
|
|
1315
|
+
let base;
|
|
1316
|
+
if (p.startsWith("/") || isWindowsAbs(p)) base = p;
|
|
1317
|
+
else if (cwd) base = `${cwd.replace(/\\/g, "/").replace(/\/+$/, "")}/${p}`;
|
|
1318
|
+
else return null;
|
|
1319
|
+
const drive = isWindowsAbs(base) ? base.slice(0, 2) : "";
|
|
1320
|
+
const parts = [];
|
|
1321
|
+
for (const seg of base.slice(drive.length).split("/")) {
|
|
1322
|
+
if (!seg || seg === ".") continue;
|
|
1323
|
+
if (seg === "..") {
|
|
1324
|
+
parts.pop();
|
|
1325
|
+
continue;
|
|
1326
|
+
}
|
|
1327
|
+
parts.push(seg);
|
|
1328
|
+
}
|
|
1329
|
+
return `${drive}/${parts.join("/")}`;
|
|
1330
|
+
}
|
|
1331
|
+
function fileUri(absPath) {
|
|
1332
|
+
const drive = isWindowsAbs(absPath) ? absPath.slice(0, 2) : "";
|
|
1333
|
+
const rest = absPath.slice(drive.length);
|
|
1334
|
+
const encoded = rest.split("/").map((seg) => encodeURIComponent(seg)).join("/");
|
|
1335
|
+
return `file://${drive ? `/${drive}` : ""}${encoded}`;
|
|
1336
|
+
}
|
|
1337
|
+
function filePathOf(id) {
|
|
1338
|
+
if (!id.startsWith("file://")) return null;
|
|
1339
|
+
const raw = id.slice("file://".length);
|
|
1340
|
+
const decoded = raw.split("/").map((seg) => {
|
|
1341
|
+
try {
|
|
1342
|
+
return decodeURIComponent(seg);
|
|
1343
|
+
} catch {
|
|
1344
|
+
return seg;
|
|
1345
|
+
}
|
|
1346
|
+
}).join("/");
|
|
1347
|
+
return /^\/[A-Za-z]:\//.test(decoded) ? decoded.slice(1) : decoded;
|
|
1348
|
+
}
|
|
1349
|
+
function fileArtifact(observed, cwd) {
|
|
1350
|
+
const abs = absolutePath(observed, cwd);
|
|
1351
|
+
return abs ? { id: fileUri(abs), observedPath: observed } : null;
|
|
1352
|
+
}
|
|
1353
|
+
var ARXIV_RE = /^\/(?:abs|pdf|html)\/(\d{4}\.\d{4,5})(?:v\d+)?(?:\.pdf)?\/?$/;
|
|
1354
|
+
function urlArtifact(url) {
|
|
1355
|
+
let u;
|
|
1356
|
+
try {
|
|
1357
|
+
u = new URL(url);
|
|
1358
|
+
} catch {
|
|
1359
|
+
return null;
|
|
1360
|
+
}
|
|
1361
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") return null;
|
|
1362
|
+
const host = u.hostname.toLowerCase();
|
|
1363
|
+
if (host === "arxiv.org" || host === "www.arxiv.org") {
|
|
1364
|
+
const m = u.pathname.match(ARXIV_RE);
|
|
1365
|
+
if (m) return { id: `arxiv:${m[1]}`, observedPath: url };
|
|
1366
|
+
}
|
|
1367
|
+
u.hash = "";
|
|
1368
|
+
return { id: u.href, observedPath: url };
|
|
1369
|
+
}
|
|
1370
|
+
function arxivArtifact(text) {
|
|
1371
|
+
const m = text.trim().match(/^(?:arxiv:)?(\d{4}\.\d{4,5})(?:v\d+)?$/i);
|
|
1372
|
+
return m ? { id: `arxiv:${m[1]}`, observedPath: text } : null;
|
|
1373
|
+
}
|
|
1374
|
+
function toolArtifacts(tool, cwd) {
|
|
1375
|
+
const out = [];
|
|
1376
|
+
for (const p of tool.paths ?? []) {
|
|
1377
|
+
const a = fileArtifact(p, cwd);
|
|
1378
|
+
if (a) out.push(tool.locator ? { ...a, locator: tool.locator } : a);
|
|
1379
|
+
}
|
|
1380
|
+
if (tool.url) {
|
|
1381
|
+
const a = urlArtifact(tool.url);
|
|
1382
|
+
if (a) out.push(a);
|
|
1383
|
+
}
|
|
1384
|
+
return out;
|
|
1385
|
+
}
|
|
1386
|
+
function changeHead(t) {
|
|
1387
|
+
const one = (s, max = 70) => {
|
|
1388
|
+
const l = s.trim();
|
|
1389
|
+
return l.length > max ? `${l.slice(0, max)}\u2026` : l;
|
|
1390
|
+
};
|
|
1391
|
+
if (t.op === "edit" && /^\*\*\* Begin Patch/m.test(t.call)) {
|
|
1392
|
+
const lines = t.call.split("\n");
|
|
1393
|
+
const minus = lines.find((l) => l.startsWith("-") && !l.startsWith("---"))?.slice(1) ?? "";
|
|
1394
|
+
const plus = lines.find((l) => l.startsWith("+") && !l.startsWith("+++"))?.slice(1) ?? "";
|
|
1395
|
+
if (minus || plus) return `${one(minus) || "\u2205"} \u2192 ${one(plus) || "\u2205"}`;
|
|
1396
|
+
}
|
|
1397
|
+
if (t.op === "edit") {
|
|
1398
|
+
const m = t.call.match(/\n--- old\n([\s\S]*?)\n\+\+\+ new\n([\s\S]*)$/);
|
|
1399
|
+
if (!m) return void 0;
|
|
1400
|
+
const a = m[1].split("\n");
|
|
1401
|
+
const b = m[2].split("\n");
|
|
1402
|
+
let k = 0;
|
|
1403
|
+
while (k < a.length && k < b.length && a[k] === b[k]) k++;
|
|
1404
|
+
const before = a.slice(k).find((l) => l.trim()) ?? "";
|
|
1405
|
+
const after = b.slice(k).find((l) => l.trim()) ?? "";
|
|
1406
|
+
return `${one(before) || "\u2205"} \u2192 ${one(after) || "\u2205"}`;
|
|
1407
|
+
}
|
|
1408
|
+
if (t.op === "write") {
|
|
1409
|
+
const body = t.call.replace(/^[^\n]*\n\n?/, "");
|
|
1410
|
+
const first = body.split("\n").map((l) => l.trim()).find(Boolean) ?? "";
|
|
1411
|
+
return `new file, ${body.length} chars: ${one(first, 80)}`;
|
|
1412
|
+
}
|
|
1413
|
+
return void 0;
|
|
1414
|
+
}
|
|
1415
|
+
var opOf = (t) => t.op ?? "unknown";
|
|
1416
|
+
var ANCHOR_RE = /\[ThoughtDAG anchor: project=([\w-]+) node=([\w-]+) bundle=([\w-]+)(?: mode=(branch|continue))?\]/;
|
|
1417
|
+
function anchorOf(text) {
|
|
1418
|
+
const m = text.match(ANCHOR_RE);
|
|
1419
|
+
return m ? { project: m[1], node: m[2], bundle: m[3], mode: m[4] === "continue" ? "continue" : "branch" } : void 0;
|
|
1420
|
+
}
|
|
1421
|
+
function sessionToEvents(s) {
|
|
1422
|
+
const sid = sessionKey(s.runner, s.nativeId);
|
|
1423
|
+
const src = (ref) => ({ runner: s.runner, file: s.file, ref, schema: s.schema });
|
|
1424
|
+
const out = [];
|
|
1425
|
+
const sourceId = s.sourceId ?? s.file;
|
|
1426
|
+
const started = {
|
|
1427
|
+
id: `${sid}/session@${fragmentTag(sourceId)}`,
|
|
1428
|
+
kind: "session.started",
|
|
1429
|
+
sessionId: sid,
|
|
1430
|
+
source: src(s.nativeId),
|
|
1431
|
+
...OBS_FULL,
|
|
1432
|
+
runner: s.runner,
|
|
1433
|
+
nativeId: s.nativeId,
|
|
1434
|
+
sourceId,
|
|
1435
|
+
title: s.title,
|
|
1436
|
+
...s.cwd ? { cwd: s.cwd } : {},
|
|
1437
|
+
...s.workspace ? { workspace: s.workspace } : {},
|
|
1438
|
+
...s.parentSessionId ? { parentSessionId: s.parentSessionId } : {},
|
|
1439
|
+
...s.subagent ? { subagent: true } : {}
|
|
1440
|
+
};
|
|
1441
|
+
const anchor = anchorOf(s.turns[0]?.question ?? "");
|
|
1442
|
+
if (anchor) started.anchor = anchor;
|
|
1443
|
+
out.push(started);
|
|
1444
|
+
const keys = turnKeys(sid, s.turns);
|
|
1445
|
+
const byItem = /* @__PURE__ */ new Map();
|
|
1446
|
+
s.turns.forEach((t, i) => {
|
|
1447
|
+
for (const id of t.itemIds) if (!byItem.has(id)) byItem.set(id, keys[i]);
|
|
1448
|
+
});
|
|
1449
|
+
s.turns.forEach((t, i) => {
|
|
1450
|
+
const tid = keys[i];
|
|
1451
|
+
const at = t.at;
|
|
1452
|
+
const base = { sessionId: sid, turnId: tid, turnIndex: i, ...at ? { at } : {} };
|
|
1453
|
+
if (t.compactionBefore) {
|
|
1454
|
+
const b = {
|
|
1455
|
+
id: `${tid}/compaction`,
|
|
1456
|
+
kind: "boundary.compaction",
|
|
1457
|
+
sessionId: sid,
|
|
1458
|
+
source: src(t.itemIds[0] ?? `turn-${i}`),
|
|
1459
|
+
...OBS_PART,
|
|
1460
|
+
...at ? { at } : {},
|
|
1461
|
+
summaryExcerpt: clip3(t.compactionBefore)
|
|
1462
|
+
};
|
|
1463
|
+
out.push(b);
|
|
1464
|
+
}
|
|
1465
|
+
const parent = t.parentItemId ? byItem.get(t.parentItemId) : void 0;
|
|
1466
|
+
const ts = {
|
|
1467
|
+
id: `${tid}/turn`,
|
|
1468
|
+
kind: "turn.started",
|
|
1469
|
+
...base,
|
|
1470
|
+
source: src(t.itemIds[0] ?? `turn-${i}`),
|
|
1471
|
+
...OBS_FULL,
|
|
1472
|
+
...parent && parent !== tid ? { parentTurnId: parent } : {},
|
|
1473
|
+
humanAuthored: !!t.question.trim()
|
|
1474
|
+
};
|
|
1475
|
+
out.push(ts);
|
|
1476
|
+
if (t.question.trim()) {
|
|
1477
|
+
const m = {
|
|
1478
|
+
id: `${tid}/q`,
|
|
1479
|
+
kind: "message.recorded",
|
|
1480
|
+
...base,
|
|
1481
|
+
source: src(t.itemIds[0] ?? `turn-${i}`),
|
|
1482
|
+
...OBS_FULL,
|
|
1483
|
+
role: "user",
|
|
1484
|
+
actor: "human",
|
|
1485
|
+
modelVisible: true,
|
|
1486
|
+
excerpt: clip3(t.question),
|
|
1487
|
+
length: t.question.length
|
|
1488
|
+
};
|
|
1489
|
+
out.push(m);
|
|
1490
|
+
}
|
|
1491
|
+
t.tools.forEach((tool, k) => {
|
|
1492
|
+
const callId = `${tid}/t${k}`;
|
|
1493
|
+
const nativeRef = tool.nativeCallId ?? `${t.itemIds[0] ?? `turn-${i}`}:tool${k}`;
|
|
1494
|
+
const artifacts = toolArtifacts(tool, s.cwd);
|
|
1495
|
+
const change = changeHead(tool);
|
|
1496
|
+
const called = {
|
|
1497
|
+
id: callId,
|
|
1498
|
+
kind: "tool.called",
|
|
1499
|
+
...base,
|
|
1500
|
+
source: src(nativeRef),
|
|
1501
|
+
...OBS_PART,
|
|
1502
|
+
callId: tool.nativeCallId ?? callId,
|
|
1503
|
+
name: tool.name,
|
|
1504
|
+
op: opOf(tool),
|
|
1505
|
+
artifacts,
|
|
1506
|
+
excerpt: clip3(tool.call),
|
|
1507
|
+
length: tool.call.length,
|
|
1508
|
+
...change ? { change } : {}
|
|
1509
|
+
};
|
|
1510
|
+
out.push(called);
|
|
1511
|
+
const done = {
|
|
1512
|
+
id: `${callId}/result`,
|
|
1513
|
+
kind: "tool.completed",
|
|
1514
|
+
...base,
|
|
1515
|
+
source: src(nativeRef),
|
|
1516
|
+
basis: "observed",
|
|
1517
|
+
completeness: tool.truncated ? "partial" : "full",
|
|
1518
|
+
calledEventId: callId,
|
|
1519
|
+
excerpt: clip3(tool.result),
|
|
1520
|
+
length: tool.result.length,
|
|
1521
|
+
truncated: tool.truncated
|
|
1522
|
+
};
|
|
1523
|
+
out.push(done);
|
|
1524
|
+
});
|
|
1525
|
+
if (t.response.trim()) {
|
|
1526
|
+
const m = {
|
|
1527
|
+
id: `${tid}/a`,
|
|
1528
|
+
kind: "message.recorded",
|
|
1529
|
+
...base,
|
|
1530
|
+
source: src(t.itemIds[t.itemIds.length - 1] ?? `turn-${i}`),
|
|
1531
|
+
...OBS_PART,
|
|
1532
|
+
role: "assistant",
|
|
1533
|
+
actor: "model",
|
|
1534
|
+
modelVisible: true,
|
|
1535
|
+
excerpt: clip3(t.response),
|
|
1536
|
+
length: t.response.length
|
|
1537
|
+
};
|
|
1538
|
+
out.push(m);
|
|
1539
|
+
}
|
|
1540
|
+
});
|
|
1541
|
+
return out;
|
|
1542
|
+
}
|
|
1543
|
+
function deriveTouches(events) {
|
|
1544
|
+
const rank = { read: 1, fetch: 1, attach: 1, write: 2, edit: 2 };
|
|
1545
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1546
|
+
for (const raw of events) {
|
|
1547
|
+
const e = raw.kind === "artifact.attached" ? { kind: "tool.called", id: raw.id, sessionId: raw.sessionId, turnId: raw.turnId, turnIndex: raw.turnIndex, at: raw.at, op: "attach", artifacts: [raw.artifact], change: void 0 } : raw;
|
|
1548
|
+
if (e.kind !== "tool.called" || !(e.op in rank)) continue;
|
|
1549
|
+
for (const a of e.artifacts) {
|
|
1550
|
+
const key = `${e.turnId}|${a.id}`;
|
|
1551
|
+
const prev = byKey.get(key);
|
|
1552
|
+
const stronger = !prev || rank[e.op] > rank[prev.op];
|
|
1553
|
+
const locators = [...prev?.locators ?? []];
|
|
1554
|
+
if (a.locator && !locators.some((l) => JSON.stringify(l) === JSON.stringify(a.locator))) locators.push(a.locator);
|
|
1555
|
+
if (!prev || stronger) {
|
|
1556
|
+
const change = stronger ? e.change ?? prev?.change : prev?.change;
|
|
1557
|
+
byKey.set(key, {
|
|
1558
|
+
artifact: a.id,
|
|
1559
|
+
op: e.op,
|
|
1560
|
+
sessionId: e.sessionId,
|
|
1561
|
+
turnId: e.turnId,
|
|
1562
|
+
turnIndex: e.turnIndex ?? 0,
|
|
1563
|
+
derivedFrom: e.id,
|
|
1564
|
+
...e.at ? { at: e.at } : {},
|
|
1565
|
+
...change ? { change } : {},
|
|
1566
|
+
...locators.length ? { locators } : {}
|
|
1567
|
+
});
|
|
1568
|
+
} else {
|
|
1569
|
+
if (!prev.change && e.change) prev.change = e.change;
|
|
1570
|
+
if (locators.length) prev.locators = locators;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
return [...byKey.values()];
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// ../src/lib/events/manifests.ts
|
|
1578
|
+
var OBS_FULL2 = { basis: "observed", completeness: "full" };
|
|
1579
|
+
var OBS_PART2 = { basis: "observed", completeness: "partial" };
|
|
1580
|
+
var UNKNOWN = { basis: "inferred", completeness: "unknown" };
|
|
1581
|
+
var CLAUDE_CODE_MANIFEST = {
|
|
1582
|
+
runner: "claude-code",
|
|
1583
|
+
schema: "cc-jsonl/2026-09",
|
|
1584
|
+
turns: OBS_FULL2,
|
|
1585
|
+
parenting: OBS_FULL2,
|
|
1586
|
+
// parentUuid is written on every message
|
|
1587
|
+
messages: OBS_PART2,
|
|
1588
|
+
// one assistant record per turn
|
|
1589
|
+
toolPairing: OBS_FULL2,
|
|
1590
|
+
// tool_use.id ↔ tool_result.tool_use_id
|
|
1591
|
+
artifactTouch: OBS_PART2,
|
|
1592
|
+
// shell commands are not parsed for files
|
|
1593
|
+
compaction: OBS_PART2,
|
|
1594
|
+
// the boundary is marked; what it replaced is not
|
|
1595
|
+
contextSurface: UNKNOWN
|
|
1596
|
+
};
|
|
1597
|
+
var CODEX_MANIFEST = {
|
|
1598
|
+
runner: "codex",
|
|
1599
|
+
schema: "codex-rollout/2026-09",
|
|
1600
|
+
turns: OBS_FULL2,
|
|
1601
|
+
parenting: OBS_PART2,
|
|
1602
|
+
// rollouts are linear; forks live in the app-server thread store
|
|
1603
|
+
messages: OBS_PART2,
|
|
1604
|
+
toolPairing: OBS_FULL2,
|
|
1605
|
+
// call_id pairs function_call with its output
|
|
1606
|
+
artifactTouch: OBS_PART2,
|
|
1607
|
+
// only apply_patch names files
|
|
1608
|
+
compaction: OBS_PART2,
|
|
1609
|
+
contextSurface: UNKNOWN
|
|
1610
|
+
};
|
|
1611
|
+
var THOUGHTDAG_MANIFEST = {
|
|
1612
|
+
runner: "thoughtdag",
|
|
1613
|
+
schema: "thoughtdag-canvas/1",
|
|
1614
|
+
turns: OBS_FULL2,
|
|
1615
|
+
parenting: OBS_FULL2,
|
|
1616
|
+
// the wires ARE the structure
|
|
1617
|
+
messages: OBS_FULL2,
|
|
1618
|
+
// hand-made nodes carry their text whole
|
|
1619
|
+
toolPairing: UNKNOWN,
|
|
1620
|
+
// no tools run on the canvas
|
|
1621
|
+
artifactTouch: OBS_FULL2,
|
|
1622
|
+
// attachments and references are explicit
|
|
1623
|
+
compaction: UNKNOWN,
|
|
1624
|
+
// condensing is a new node, not a replacement
|
|
1625
|
+
contextSurface: OBS_PART2
|
|
1626
|
+
// exact for logged commits, upstream-only before
|
|
1627
|
+
};
|
|
1628
|
+
var PI_MANIFEST = {
|
|
1629
|
+
runner: "pi",
|
|
1630
|
+
schema: "pi-session/v3",
|
|
1631
|
+
turns: OBS_FULL2,
|
|
1632
|
+
// one user message opens every turn
|
|
1633
|
+
parenting: OBS_FULL2,
|
|
1634
|
+
// every entry names its parent; the session is a tree
|
|
1635
|
+
messages: OBS_PART2,
|
|
1636
|
+
// assistant text blocks fold per turn; thinking is dropped
|
|
1637
|
+
toolPairing: OBS_FULL2,
|
|
1638
|
+
// toolCall blocks ↔ toolResult messages pair by toolCallId
|
|
1639
|
+
artifactTouch: OBS_FULL2,
|
|
1640
|
+
// `path` on read/edit/write; shell commands stay opaque, as for every runner
|
|
1641
|
+
compaction: UNKNOWN,
|
|
1642
|
+
// not projected yet
|
|
1643
|
+
contextSurface: UNKNOWN
|
|
1644
|
+
// the log records messages, not requests
|
|
1645
|
+
};
|
|
1646
|
+
var DSH_MANIFEST = {
|
|
1647
|
+
runner: "dsh",
|
|
1648
|
+
schema: "dsh-events/2026-09",
|
|
1649
|
+
turns: OBS_FULL2,
|
|
1650
|
+
// user/message events (source.kind=user) bound every turn
|
|
1651
|
+
parenting: OBS_FULL2,
|
|
1652
|
+
// turns are linear; a continued session is a new file
|
|
1653
|
+
messages: OBS_PART2,
|
|
1654
|
+
// assistant text folds per step; reasoning is dropped
|
|
1655
|
+
toolPairing: OBS_FULL2,
|
|
1656
|
+
// tool/call ↔ tool/result pair by callId
|
|
1657
|
+
artifactTouch: OBS_FULL2,
|
|
1658
|
+
// file_path on read/write/edit — top-level calls and run_code dispatches alike; shell commands stay opaque, as for every runner
|
|
1659
|
+
compaction: UNKNOWN,
|
|
1660
|
+
// no boundary event observed in the log yet
|
|
1661
|
+
contextSurface: OBS_PART2
|
|
1662
|
+
// request/context carries provider+model+window, never messages
|
|
1663
|
+
};
|
|
1664
|
+
var MANIFESTS = {
|
|
1665
|
+
"claude-code": CLAUDE_CODE_MANIFEST,
|
|
1666
|
+
codex: CODEX_MANIFEST,
|
|
1667
|
+
dsh: DSH_MANIFEST,
|
|
1668
|
+
pi: PI_MANIFEST,
|
|
1669
|
+
thoughtdag: THOUGHTDAG_MANIFEST
|
|
1670
|
+
};
|
|
1671
|
+
|
|
1672
|
+
// ../src/lib/adapters/thoughtdag-canvas.ts
|
|
1673
|
+
var OBS_FULL3 = { basis: "observed", completeness: "full" };
|
|
1674
|
+
var OBS_PART3 = { basis: "observed", completeness: "partial" };
|
|
1675
|
+
var clip4 = (s, max = EXCERPT_CHARS) => {
|
|
1676
|
+
const t = s.trim();
|
|
1677
|
+
return t.length > max ? `${t.slice(0, max)}\u2026` : t;
|
|
1678
|
+
};
|
|
1679
|
+
function isCanvasBackup(v) {
|
|
1680
|
+
return !!v && typeof v === "object" && Array.isArray(v.nodes) && Array.isArray(v.edges);
|
|
1681
|
+
}
|
|
1682
|
+
function canvasNativeId(file) {
|
|
1683
|
+
const base = file.replace(/\\/g, "/").split("/").pop() ?? file;
|
|
1684
|
+
return base.replace(/\.thoughtdag\.json$/i, "");
|
|
1685
|
+
}
|
|
1686
|
+
function canvasToEvents(backup, opts) {
|
|
1687
|
+
const nativeId = backup.projectId?.trim() || canvasNativeId(opts.file);
|
|
1688
|
+
const sid = sessionKey("thoughtdag", nativeId);
|
|
1689
|
+
const sourceId = opts.sourceId ?? opts.file;
|
|
1690
|
+
const schema = THOUGHTDAG_MANIFEST.schema;
|
|
1691
|
+
const src = (ref) => ({ runner: "thoughtdag", file: opts.file, ref, schema });
|
|
1692
|
+
const out = [];
|
|
1693
|
+
const texts = /* @__PURE__ */ new Map();
|
|
1694
|
+
const title = backup.name?.trim() || nativeId;
|
|
1695
|
+
const started = {
|
|
1696
|
+
id: `${sid}/session@${fragmentTag(sourceId)}`,
|
|
1697
|
+
kind: "session.started",
|
|
1698
|
+
sessionId: sid,
|
|
1699
|
+
source: src("canvas"),
|
|
1700
|
+
...OBS_FULL3,
|
|
1701
|
+
runner: "thoughtdag",
|
|
1702
|
+
nativeId,
|
|
1703
|
+
sourceId,
|
|
1704
|
+
title,
|
|
1705
|
+
...backup.exportedAt ? { at: backup.exportedAt } : {}
|
|
1706
|
+
};
|
|
1707
|
+
out.push(started);
|
|
1708
|
+
const turnIdOf = (nodeId) => `${sid}#${nodeId}`;
|
|
1709
|
+
const index = /* @__PURE__ */ new Map();
|
|
1710
|
+
backup.nodes.forEach((n, i) => index.set(n.id, i));
|
|
1711
|
+
for (const n of backup.nodes) {
|
|
1712
|
+
const d = n.data;
|
|
1713
|
+
const tid = turnIdOf(n.id);
|
|
1714
|
+
const i = index.get(n.id) ?? 0;
|
|
1715
|
+
const at = d.createdAt ?? d.lastGeneratedAt;
|
|
1716
|
+
const base = { sessionId: sid, turnId: tid, turnIndex: i, ...at ? { at } : {} };
|
|
1717
|
+
const mirror = d.importSource ? { sessionId: sessionKey(d.importSource.runner, d.importSource.sessionId), item: d.importSource.itemIds[0] ?? n.id } : void 0;
|
|
1718
|
+
const isMaterial = d.stepKind === "file" || d.stepKind === "link" || d.stepKind === "frame";
|
|
1719
|
+
const isNote = d.stepKind === "note";
|
|
1720
|
+
const ts = {
|
|
1721
|
+
id: `${tid}/turn`,
|
|
1722
|
+
kind: "turn.started",
|
|
1723
|
+
...base,
|
|
1724
|
+
source: src(n.id),
|
|
1725
|
+
...OBS_FULL3,
|
|
1726
|
+
humanAuthored: !isMaterial,
|
|
1727
|
+
...mirror ? { mirrorOf: mirror } : {}
|
|
1728
|
+
};
|
|
1729
|
+
out.push(ts);
|
|
1730
|
+
if (isMaterial) {
|
|
1731
|
+
const held = (d.attachments ?? []).filter((a) => !a.name.startsWith("tool: ")).map((a) => a.name);
|
|
1732
|
+
texts.set(tid, { question: held.length ? `[${d.stepKind}] ${held.join(", ")}` : `[${d.stepKind}] ${clip4(d.question ?? "", 80)}`, response: "" });
|
|
1733
|
+
}
|
|
1734
|
+
if (!mirror && !isMaterial) {
|
|
1735
|
+
texts.set(tid, { question: d.question ?? "", response: d.response ?? "" });
|
|
1736
|
+
if (d.question?.trim()) {
|
|
1737
|
+
const m = {
|
|
1738
|
+
id: `${tid}/q`,
|
|
1739
|
+
kind: "message.recorded",
|
|
1740
|
+
...base,
|
|
1741
|
+
source: src(`${n.id}:question`),
|
|
1742
|
+
...OBS_FULL3,
|
|
1743
|
+
role: isNote ? "custom" : "user",
|
|
1744
|
+
actor: "human",
|
|
1745
|
+
modelVisible: true,
|
|
1746
|
+
excerpt: clip4(d.question),
|
|
1747
|
+
length: d.question.length
|
|
1748
|
+
};
|
|
1749
|
+
out.push(m);
|
|
1750
|
+
}
|
|
1751
|
+
if (d.response?.trim() && !isNote) {
|
|
1752
|
+
const model = d.generatedBy?.[d.responseIndex ?? 0] ?? void 0;
|
|
1753
|
+
const a = {
|
|
1754
|
+
id: `${tid}/a`,
|
|
1755
|
+
kind: "message.recorded",
|
|
1756
|
+
...base,
|
|
1757
|
+
source: src(`${n.id}:response`),
|
|
1758
|
+
...OBS_FULL3,
|
|
1759
|
+
...d.lastGeneratedAt ? { at: d.lastGeneratedAt } : {},
|
|
1760
|
+
role: "assistant",
|
|
1761
|
+
actor: model ? "model" : "unknown",
|
|
1762
|
+
modelVisible: true,
|
|
1763
|
+
excerpt: clip4(d.response),
|
|
1764
|
+
length: d.response.length
|
|
1765
|
+
};
|
|
1766
|
+
out.push(a);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
if (mirror && d.source) {
|
|
1770
|
+
for (const field of ["question", "response"]) {
|
|
1771
|
+
const now = d[field] ?? "";
|
|
1772
|
+
const was = d.source[field] ?? "";
|
|
1773
|
+
if (now !== was) {
|
|
1774
|
+
const e = {
|
|
1775
|
+
id: `${tid}/edit:${field}`,
|
|
1776
|
+
kind: "record.edited",
|
|
1777
|
+
...base,
|
|
1778
|
+
source: src(`${n.id}:${field}`),
|
|
1779
|
+
...OBS_FULL3,
|
|
1780
|
+
field,
|
|
1781
|
+
excerpt: clip4(now),
|
|
1782
|
+
length: now.length
|
|
1783
|
+
};
|
|
1784
|
+
out.push(e);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
for (const h of d.highlights ?? []) {
|
|
1789
|
+
const visible = d.highlightMode === "filter" || d.highlightMode === "tag";
|
|
1790
|
+
const m = {
|
|
1791
|
+
id: `${tid}/hl:${h.id}`,
|
|
1792
|
+
kind: "message.recorded",
|
|
1793
|
+
...base,
|
|
1794
|
+
...h.at ? { at: h.at } : {},
|
|
1795
|
+
source: src(`${n.id}:highlight:${h.id}`),
|
|
1796
|
+
...OBS_FULL3,
|
|
1797
|
+
role: "custom",
|
|
1798
|
+
actor: "human",
|
|
1799
|
+
modelVisible: visible,
|
|
1800
|
+
excerpt: clip4(h.text),
|
|
1801
|
+
length: h.text.length
|
|
1802
|
+
};
|
|
1803
|
+
out.push(m);
|
|
1804
|
+
}
|
|
1805
|
+
const excluded = new Set(d.excludedAttachmentIds ?? []);
|
|
1806
|
+
for (const att of d.attachments ?? []) {
|
|
1807
|
+
if (att.name.startsWith("tool: ") || att.op) continue;
|
|
1808
|
+
const artifact = {
|
|
1809
|
+
id: `thoughtdag:attachment/${att.id}`,
|
|
1810
|
+
observedPath: att.name,
|
|
1811
|
+
...d.anchor && d.anchor.attId === att.id ? { locator: { pages: String(d.anchor.page) } } : {}
|
|
1812
|
+
};
|
|
1813
|
+
const e = {
|
|
1814
|
+
id: `${tid}/att:${att.id}`,
|
|
1815
|
+
kind: "artifact.attached",
|
|
1816
|
+
...base,
|
|
1817
|
+
...att.addedAt ? { at: att.addedAt } : {},
|
|
1818
|
+
source: src(`${n.id}:attachment:${att.id}`),
|
|
1819
|
+
...OBS_FULL3,
|
|
1820
|
+
artifact,
|
|
1821
|
+
via: "attachment",
|
|
1822
|
+
mediaType: att.type,
|
|
1823
|
+
inContext: !excluded.has(att.id)
|
|
1824
|
+
};
|
|
1825
|
+
out.push(e);
|
|
1826
|
+
}
|
|
1827
|
+
if (d.anchor?.attId && !(d.attachments ?? []).some((a) => a.id === d.anchor?.attId)) {
|
|
1828
|
+
const e = {
|
|
1829
|
+
id: `${tid}/anchor`,
|
|
1830
|
+
kind: "artifact.attached",
|
|
1831
|
+
...base,
|
|
1832
|
+
source: src(`${n.id}:anchor`),
|
|
1833
|
+
...OBS_FULL3,
|
|
1834
|
+
artifact: { id: `thoughtdag:attachment/${d.anchor.attId}`, locator: { pages: String(d.anchor.page) } },
|
|
1835
|
+
via: "anchor",
|
|
1836
|
+
inContext: "unknown"
|
|
1837
|
+
};
|
|
1838
|
+
out.push(e);
|
|
1839
|
+
}
|
|
1840
|
+
for (const [k, r] of (d.references ?? []).entries()) {
|
|
1841
|
+
const a = r.url ? urlArtifact(r.url) : null;
|
|
1842
|
+
if (!a) continue;
|
|
1843
|
+
const e = {
|
|
1844
|
+
id: `${tid}/ref:${k}`,
|
|
1845
|
+
kind: "artifact.attached",
|
|
1846
|
+
...base,
|
|
1847
|
+
source: src(`${n.id}:reference:${k}`),
|
|
1848
|
+
...OBS_FULL3,
|
|
1849
|
+
artifact: a,
|
|
1850
|
+
via: "reference",
|
|
1851
|
+
inContext: "unknown"
|
|
1852
|
+
};
|
|
1853
|
+
out.push(e);
|
|
1854
|
+
}
|
|
1855
|
+
const material = (d.attachments ?? []).filter((a) => !a.name.startsWith("tool: ") && !a.op && typeof a.extractedText === "string" && a.extractedText.trim()).map((a) => `[${a.name}]
|
|
1856
|
+
${a.extractedText.slice(0, 4e3)}`).join("\n\n");
|
|
1857
|
+
if (material) {
|
|
1858
|
+
const prev = texts.get(tid) ?? { question: "", response: "" };
|
|
1859
|
+
texts.set(tid, { ...prev, material });
|
|
1860
|
+
}
|
|
1861
|
+
if (d.lastContextHash && !mirror) {
|
|
1862
|
+
const c = {
|
|
1863
|
+
id: `${tid}/ctx:upstream`,
|
|
1864
|
+
kind: "context.committed",
|
|
1865
|
+
...base,
|
|
1866
|
+
...d.lastGeneratedAt ? { at: d.lastGeneratedAt } : {},
|
|
1867
|
+
source: src(`${n.id}:lastContextHash`),
|
|
1868
|
+
...OBS_PART3,
|
|
1869
|
+
requestId: `${n.id}@${d.lastGeneratedAt ?? "unknown"}`,
|
|
1870
|
+
members: [],
|
|
1871
|
+
contentHash: d.lastContextHash,
|
|
1872
|
+
hashOf: "upstream",
|
|
1873
|
+
decidedBy: "user",
|
|
1874
|
+
confirmed: true
|
|
1875
|
+
};
|
|
1876
|
+
out.push(c);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
for (const e of backup.edges) {
|
|
1880
|
+
if (!index.has(e.source) || !index.has(e.target)) continue;
|
|
1881
|
+
const ev = {
|
|
1882
|
+
id: `${sid}/edge:${e.id}`,
|
|
1883
|
+
kind: "edge.recorded",
|
|
1884
|
+
sessionId: sid,
|
|
1885
|
+
source: src(e.id),
|
|
1886
|
+
...OBS_FULL3,
|
|
1887
|
+
edgeType: "context",
|
|
1888
|
+
via: e.data?.isCrossLink ? "reference" : "mainline",
|
|
1889
|
+
fromTurnId: turnIdOf(e.source),
|
|
1890
|
+
toTurnId: turnIdOf(e.target)
|
|
1891
|
+
};
|
|
1892
|
+
out.push(ev);
|
|
1893
|
+
}
|
|
1894
|
+
for (const ev of backup.events ?? []) {
|
|
1895
|
+
if (ev.op !== "commit" || !ev.id || !ev.d) continue;
|
|
1896
|
+
const sha = typeof ev.d.sha === "string" ? ev.d.sha : "";
|
|
1897
|
+
if (!sha) continue;
|
|
1898
|
+
const kind = ev.d.kind === "bundle" ? "bundle" : "request";
|
|
1899
|
+
const members = typeof ev.d.m === "string" && ev.d.m ? ev.d.m.split(",").filter(Boolean).map((nodeId) => ({ nodeId })) : [];
|
|
1900
|
+
const truncated = ev.d.more === true;
|
|
1901
|
+
const c = {
|
|
1902
|
+
id: `${sid}/commit:${ev.id}@${ev.t}${kind === "bundle" ? ":bundle" : ""}`,
|
|
1903
|
+
kind: "context.committed",
|
|
1904
|
+
sessionId: sid,
|
|
1905
|
+
turnId: turnIdOf(ev.id),
|
|
1906
|
+
turnIndex: index.get(ev.id) ?? 0,
|
|
1907
|
+
at: ev.t,
|
|
1908
|
+
source: src(`event:commit:${ev.id}@${ev.t}`),
|
|
1909
|
+
basis: "observed",
|
|
1910
|
+
completeness: truncated || !members.length ? "partial" : "full",
|
|
1911
|
+
requestId: typeof ev.d.bundle === "string" ? ev.d.bundle : `${ev.id}@${ev.t}`,
|
|
1912
|
+
members,
|
|
1913
|
+
contentHash: sha.startsWith("sha256:") ? sha : `sha256:${sha}`,
|
|
1914
|
+
hashOf: kind,
|
|
1915
|
+
...typeof ev.d.n === "number" ? { messageCount: ev.d.n } : {},
|
|
1916
|
+
...typeof ev.d.model === "string" && ev.d.model ? { model: ev.d.model } : {},
|
|
1917
|
+
decidedBy: "user",
|
|
1918
|
+
confirmed: true
|
|
1919
|
+
};
|
|
1920
|
+
out.push(c);
|
|
1921
|
+
}
|
|
1922
|
+
return { sessionId: sid, nativeId, title, events: out, texts };
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
// ../cli/src/lib.ts
|
|
1926
|
+
var readLike = (op) => op === "read" || op === "fetch" || op === "attach";
|
|
1927
|
+
var INDEX_VERSION = 10;
|
|
1928
|
+
var EXCERPT = 200;
|
|
1929
|
+
var HOME = process.env.THOUGHTDAG_HOME ?? path.join(os.homedir(), ".thoughtdag");
|
|
1930
|
+
var FACT_FILE = path.join(HOME, "fact-index.json");
|
|
1931
|
+
var CACHE_FILE = path.join(HOME, "interpretation-cache.json");
|
|
1932
|
+
var TEXT_FILE = path.join(HOME, "text-index.json");
|
|
1933
|
+
var TEXT_LINES = path.join(HOME, "text-index.jsonl");
|
|
1934
|
+
var LEGACY_FILE = path.join(HOME, "why-index.json");
|
|
1935
|
+
var CONFIG_FILE = path.join(HOME, "config.json");
|
|
1936
|
+
var DSH_ROOT = path.join(process.env.DSH_HOME ?? path.join(os.homedir(), ".dsh"), "sessions");
|
|
1937
|
+
var PI_ROOT = path.join(os.homedir(), ".pi", "agent", "sessions");
|
|
1938
|
+
var ROOTS = process.env.THOUGHTDAG_SESSION_ROOTS?.split(path.delimiter).filter(Boolean) ?? [path.join(os.homedir(), ".claude", "projects"), path.join(os.homedir(), ".codex", "sessions"), DSH_ROOT, PI_ROOT];
|
|
1939
|
+
var CANVAS_RECORDS = path.join(HOME, "canvases");
|
|
1940
|
+
async function canvasRoots() {
|
|
1941
|
+
const env = process.env.THOUGHTDAG_CANVAS_ROOTS?.split(path.delimiter).filter(Boolean);
|
|
1942
|
+
if (env) return [CANVAS_RECORDS, ...env];
|
|
1943
|
+
try {
|
|
1944
|
+
const c = JSON.parse(await fsp.readFile(CONFIG_FILE, "utf8"));
|
|
1945
|
+
return [CANVAS_RECORDS, ...c.canvasRoots ?? []];
|
|
1946
|
+
} catch {
|
|
1947
|
+
return [CANVAS_RECORDS];
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
async function rememberCanvasRoot(dir) {
|
|
1951
|
+
const roots = await canvasRoots();
|
|
1952
|
+
const abs = path.resolve(dir);
|
|
1953
|
+
if (!roots.includes(abs)) await writePrivate(CONFIG_FILE, { canvasRoots: [...roots, abs] });
|
|
1954
|
+
}
|
|
1955
|
+
var isCanvasFile = (name) => /\.thoughtdag\.json$/i.test(name);
|
|
1956
|
+
var isSessionFile = (name) => name.endsWith(".jsonl") || name.endsWith(".jsonl.zstd");
|
|
1957
|
+
async function walk(dir, depth, out, accept) {
|
|
1958
|
+
if (depth > 5) return;
|
|
1959
|
+
let entries;
|
|
1960
|
+
try {
|
|
1961
|
+
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
1962
|
+
} catch {
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1965
|
+
for (const ent of entries) {
|
|
1966
|
+
const p = path.join(dir, ent.name);
|
|
1967
|
+
if (ent.isDirectory()) await walk(p, depth + 1, out, accept);
|
|
1968
|
+
else if (ent.isFile() && accept(ent.name)) {
|
|
1969
|
+
try {
|
|
1970
|
+
const st = await fsp.stat(p);
|
|
1971
|
+
out.push({ file: p, mtime: st.mtimeMs, size: st.size });
|
|
1972
|
+
} catch {
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
async function listSources() {
|
|
1978
|
+
const files = [];
|
|
1979
|
+
for (const root of ROOTS) await walk(root, 0, files, isSessionFile);
|
|
1980
|
+
for (const root of await canvasRoots()) await walk(root, 0, files, isCanvasFile);
|
|
1981
|
+
return files;
|
|
1982
|
+
}
|
|
1983
|
+
async function firstLine(file) {
|
|
1984
|
+
const rl = createInterface({ input: createReadStream(file, { encoding: "utf8", highWaterMark: 65536 }) });
|
|
1985
|
+
for await (const line of rl) {
|
|
1986
|
+
rl.close();
|
|
1987
|
+
return line;
|
|
1988
|
+
}
|
|
1989
|
+
return "";
|
|
1990
|
+
}
|
|
1991
|
+
async function realOr(p) {
|
|
1992
|
+
try {
|
|
1993
|
+
return await fsp.realpath(p);
|
|
1994
|
+
} catch {
|
|
1995
|
+
return p;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
async function canonicalPath(p, memo) {
|
|
1999
|
+
const hit = memo?.get(p);
|
|
2000
|
+
if (hit) return hit;
|
|
2001
|
+
let out;
|
|
2002
|
+
try {
|
|
2003
|
+
out = await fsp.realpath(p);
|
|
2004
|
+
} catch {
|
|
2005
|
+
const parent = path.dirname(p);
|
|
2006
|
+
out = parent === p ? p : path.join(await canonicalPath(parent, memo), path.basename(p));
|
|
2007
|
+
}
|
|
2008
|
+
memo?.set(p, out);
|
|
2009
|
+
return out;
|
|
2010
|
+
}
|
|
2011
|
+
async function workspaceOf(dir) {
|
|
2012
|
+
let cur = await realOr(dir);
|
|
2013
|
+
for (; ; ) {
|
|
2014
|
+
try {
|
|
2015
|
+
await fsp.stat(path.join(cur, ".git"));
|
|
2016
|
+
return cur;
|
|
2017
|
+
} catch {
|
|
2018
|
+
}
|
|
2019
|
+
const up = path.dirname(cur);
|
|
2020
|
+
if (up === cur) return await realOr(dir);
|
|
2021
|
+
cur = up;
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
var strongest = (a, b) => a === "edit" || a === "write" ? a : b;
|
|
2025
|
+
async function canonicalArtifact(id, memo) {
|
|
2026
|
+
const p = filePathOf(id);
|
|
2027
|
+
return p ? fileUri(await canonicalPath(p, memo)) : id;
|
|
2028
|
+
}
|
|
2029
|
+
function mentionKey(id) {
|
|
2030
|
+
const p = filePathOf(id);
|
|
2031
|
+
if (p) return path.basename(p);
|
|
2032
|
+
if (id.startsWith("arxiv:")) return id.slice("arxiv:".length);
|
|
2033
|
+
try {
|
|
2034
|
+
const u = new URL(id);
|
|
2035
|
+
return u.pathname.split("/").filter(Boolean).pop() ?? u.hostname;
|
|
2036
|
+
} catch {
|
|
2037
|
+
return id;
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
async function displayOf(id, names = {}) {
|
|
2041
|
+
const p = filePathOf(id);
|
|
2042
|
+
if (!p) return names[id] ? `${names[id]} (${id})` : id;
|
|
2043
|
+
const rel = path.relative(await realOr(process.cwd()), await realOr(p));
|
|
2044
|
+
return rel.startsWith("..") || path.isAbsolute(rel) ? p : rel;
|
|
2045
|
+
}
|
|
2046
|
+
var clipLine = (s, max) => {
|
|
2047
|
+
const l = s.trim();
|
|
2048
|
+
return l.length > max ? `${l.slice(0, max)}\u2026` : l;
|
|
2049
|
+
};
|
|
2050
|
+
function mentionOf(response, key) {
|
|
2051
|
+
const base = key.toLowerCase();
|
|
2052
|
+
if (base.length < 4) return void 0;
|
|
2053
|
+
const paras = response.replace(/```[\s\S]*?```/g, "").split(/\n\s*\n/).map((p) => p.replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[#*`>|_~]/g, "").replace(/\s+/g, " ").trim()).filter((p) => p.length > 12 && p.toLowerCase().includes(base) && !/[::]$/.test(p) && !/^(sources?|references?|来源|参考)\b/i.test(p));
|
|
2054
|
+
const last = paras[paras.length - 1];
|
|
2055
|
+
return last ? clipLine(last, 240) : void 0;
|
|
2056
|
+
}
|
|
2057
|
+
function questionExcerpt(text) {
|
|
2058
|
+
const cmd = text.match(/<command-name>([^<]+)<\/command-name>/);
|
|
2059
|
+
const line = (cmd ? cmd[1] : text).split("\n").map((l) => l.trim()).find((l) => l && !l.startsWith("<")) ?? "";
|
|
2060
|
+
return clipLine(line, EXCERPT);
|
|
2061
|
+
}
|
|
2062
|
+
var zstdInflate = zlib.zstdDecompressSync;
|
|
2063
|
+
var ZSTD_MAX = 512 * 1024 * 1024;
|
|
2064
|
+
var zstdSkipped = 0;
|
|
2065
|
+
async function eventsOf(file, sourceId) {
|
|
2066
|
+
if (isCanvasFile(file)) {
|
|
2067
|
+
let parsed;
|
|
2068
|
+
try {
|
|
2069
|
+
parsed = JSON.parse(await fsp.readFile(file, "utf8"));
|
|
2070
|
+
} catch {
|
|
2071
|
+
return null;
|
|
2072
|
+
}
|
|
2073
|
+
if (!isCanvasBackup(parsed)) return null;
|
|
2074
|
+
const c = canvasToEvents(parsed, { file, sourceId });
|
|
2075
|
+
return { runner: "thoughtdag", nativeId: c.nativeId, title: c.title, events: c.events, texts: c.texts, manifest: MANIFESTS.thoughtdag };
|
|
2076
|
+
}
|
|
2077
|
+
let meta = {};
|
|
2078
|
+
let lines;
|
|
2079
|
+
if (file.endsWith(".zstd")) {
|
|
2080
|
+
if (!zstdInflate) {
|
|
2081
|
+
zstdSkipped++;
|
|
2082
|
+
return null;
|
|
2083
|
+
}
|
|
2084
|
+
let raw;
|
|
2085
|
+
try {
|
|
2086
|
+
raw = await fsp.readFile(file);
|
|
2087
|
+
} catch {
|
|
2088
|
+
return null;
|
|
2089
|
+
}
|
|
2090
|
+
if (raw.length > ZSTD_MAX) return null;
|
|
2091
|
+
let text;
|
|
2092
|
+
try {
|
|
2093
|
+
text = Buffer.from(decompressZstdFrames(raw, zstdInflate)).toString("utf8");
|
|
2094
|
+
} catch {
|
|
2095
|
+
return null;
|
|
2096
|
+
}
|
|
2097
|
+
const arr = text.split("\n");
|
|
2098
|
+
try {
|
|
2099
|
+
meta = JSON.parse(arr[0] ?? "");
|
|
2100
|
+
} catch {
|
|
2101
|
+
}
|
|
2102
|
+
lines = arr;
|
|
2103
|
+
} else {
|
|
2104
|
+
try {
|
|
2105
|
+
meta = JSON.parse(await firstLine(file));
|
|
2106
|
+
} catch {
|
|
2107
|
+
}
|
|
2108
|
+
lines = createInterface({ input: createReadStream(file, { encoding: "utf8", highWaterMark: 1 << 20 }) });
|
|
2109
|
+
}
|
|
2110
|
+
const runner = meta.type === "session_meta" ? "codex" : meta.type === "session" && typeof meta.id === "string" ? typeof meta.timestamp === "string" ? "pi" : "dsh" : "claude-code";
|
|
2111
|
+
const collector = runner === "codex" ? new CodexSessionCollector() : runner === "dsh" ? new DshSessionCollector() : runner === "pi" ? new PiSessionCollector() : new ClaudeSessionCollector();
|
|
2112
|
+
for await (const line of lines) collector.feedLine(line);
|
|
2113
|
+
const s = collector.finish();
|
|
2114
|
+
if (!s) return null;
|
|
2115
|
+
const subagent = "subagent" in s ? !!s.subagent : runner === "claude-code" && /\/subagents\//.test(file);
|
|
2116
|
+
const cwd = "cwd" in s && s.cwd ? s.cwd : void 0;
|
|
2117
|
+
const session = {
|
|
2118
|
+
runner,
|
|
2119
|
+
nativeId: s.sessionId,
|
|
2120
|
+
title: s.title,
|
|
2121
|
+
file,
|
|
2122
|
+
...sourceId ? { sourceId } : {},
|
|
2123
|
+
schema: MANIFESTS[runner].schema,
|
|
2124
|
+
...cwd ? { cwd } : {},
|
|
2125
|
+
...subagent ? { subagent } : {},
|
|
2126
|
+
turns: s.turns
|
|
2127
|
+
};
|
|
2128
|
+
const events = sessionToEvents(session);
|
|
2129
|
+
const texts = /* @__PURE__ */ new Map();
|
|
2130
|
+
for (const e of events) if (e.kind === "turn.started") {
|
|
2131
|
+
const t = s.turns[e.turnIndex];
|
|
2132
|
+
if (t) texts.set(e.turnId, { question: t.question, response: t.response });
|
|
2133
|
+
}
|
|
2134
|
+
const started = events.find((e) => e.kind === "session.started");
|
|
2135
|
+
const anchor = started?.anchor ? { project: started.anchor.project, node: started.anchor.node, bundle: started.anchor.bundle } : void 0;
|
|
2136
|
+
return { runner, nativeId: s.sessionId, title: s.title, ...anchor ? { anchor } : {}, ...cwd ? { cwd } : {}, ...subagent ? { subagent } : {}, events, texts, manifest: MANIFESTS[runner], turns: s.turns };
|
|
2137
|
+
}
|
|
2138
|
+
async function parseSession(f) {
|
|
2139
|
+
const memo = /* @__PURE__ */ new Map();
|
|
2140
|
+
const sourceId = await canonicalPath(f.file, memo);
|
|
2141
|
+
const p = await eventsOf(f.file, sourceId);
|
|
2142
|
+
if (!p) return null;
|
|
2143
|
+
const cwd = p.cwd ? await canonicalPath(p.cwd, memo) : "";
|
|
2144
|
+
const workspace = cwd ? await workspaceOf(cwd) : "";
|
|
2145
|
+
const names = {};
|
|
2146
|
+
const opsByTurn = /* @__PURE__ */ new Map();
|
|
2147
|
+
for (const t of deriveTouches(p.events)) {
|
|
2148
|
+
const id = await canonicalArtifact(t.artifact, memo);
|
|
2149
|
+
const ops = opsByTurn.get(t.turnId) ?? opsByTurn.set(t.turnId, {}).get(t.turnId);
|
|
2150
|
+
const prev = ops[id];
|
|
2151
|
+
const d = prev?.d ?? t.change;
|
|
2152
|
+
const l = [...prev?.l ?? [], ...t.locators ?? []].filter((x, i, arr) => arr.findIndex((y) => JSON.stringify(y) === JSON.stringify(x)) === i);
|
|
2153
|
+
ops[id] = { op: strongest(prev?.op, t.op), ...d ? { d } : {}, ...l.length ? { l } : {} };
|
|
2154
|
+
}
|
|
2155
|
+
for (const e of p.events) if (e.kind === "artifact.attached" && e.artifact.observedPath && !e.artifact.id.startsWith("file://")) names[e.artifact.id] = e.artifact.observedPath;
|
|
2156
|
+
const bundles = {};
|
|
2157
|
+
for (const e of p.events) if (e.kind === "context.committed" && e.hashOf === "bundle" && e.turnId) bundles[e.requestId] = { canvas: p.nativeId, node: e.turnId.split("#").pop() ?? "" };
|
|
2158
|
+
const questions = /* @__PURE__ */ new Map();
|
|
2159
|
+
for (const e of p.events) if (e.kind === "message.recorded" && e.role === "user" && !questions.has(e.turnId)) questions.set(e.turnId, e.excerpt);
|
|
2160
|
+
const turns = [];
|
|
2161
|
+
const cache = {};
|
|
2162
|
+
const text_ = {};
|
|
2163
|
+
let lastSubstantive = "";
|
|
2164
|
+
const starts = p.events.filter((e) => e.kind === "turn.started").sort((a, b) => a.turnIndex - b.turnIndex);
|
|
2165
|
+
for (const ts of starts) {
|
|
2166
|
+
const text = p.texts.get(ts.turnId);
|
|
2167
|
+
const q = text ? questionExcerpt(text.question) : questions.get(ts.turnId) ?? (ts.mirrorOf ? "(mirrored turn)" : "");
|
|
2168
|
+
const item = ts.mirrorOf?.item ?? (p.runner === "thoughtdag" ? ts.turnId : ts.turnId.split("#").pop());
|
|
2169
|
+
turns.push({ i: ts.turnIndex, t: ts.turnId, ...item ? { item } : {}, ...ts.at ? { at: ts.at } : {}, q, ops: opsByTurn.get(ts.turnId) ?? {} });
|
|
2170
|
+
if (!text) continue;
|
|
2171
|
+
if (text.question.trim() || text.response.trim() || text.material?.trim()) {
|
|
2172
|
+
text_[String(ts.turnIndex)] = { q: text.question, a: text.response, ...text.material ? { m: text.material } : {} };
|
|
2173
|
+
}
|
|
2174
|
+
const entry = {};
|
|
2175
|
+
const con = conclusionOf(text.response, 240);
|
|
2176
|
+
if (con) entry.c = con;
|
|
2177
|
+
if (q.length < 12 && lastSubstantive) entry.p = lastSubstantive;
|
|
2178
|
+
if (q.length >= 12) lastSubstantive = q;
|
|
2179
|
+
for (const id of Object.keys(opsByTurn.get(ts.turnId) ?? {})) {
|
|
2180
|
+
const m = mentionOf(text.response, mentionKey(names[id] ? `file://${names[id]}` : id));
|
|
2181
|
+
if (m) (entry.m ??= {})[id] = m;
|
|
2182
|
+
}
|
|
2183
|
+
if (Object.keys(entry).length) cache[String(ts.turnIndex)] = entry;
|
|
2184
|
+
}
|
|
2185
|
+
return {
|
|
2186
|
+
fact: { id: p.nativeId, runner: p.runner, file: f.file, mtime: f.mtime, size: f.size, cwd, workspace, title: p.title, ...p.subagent ? { subagent: true } : {}, ...p.anchor ? { anchor: p.anchor } : {}, turns },
|
|
2187
|
+
cache,
|
|
2188
|
+
text: text_,
|
|
2189
|
+
names,
|
|
2190
|
+
bundles
|
|
2191
|
+
};
|
|
2192
|
+
}
|
|
2193
|
+
var emptyFacts = () => ({ version: INDEX_VERSION, builtAt: "", sessions: {}, skipped: {}, names: {}, bundles: {} });
|
|
2194
|
+
var emptyCache = () => ({ version: INDEX_VERSION, sessions: {} });
|
|
2195
|
+
var emptyText = () => ({ version: INDEX_VERSION, sessions: {} });
|
|
2196
|
+
async function readJson(file, empty) {
|
|
2197
|
+
try {
|
|
2198
|
+
const v = JSON.parse(await fsp.readFile(file, "utf8"));
|
|
2199
|
+
return v.version === INDEX_VERSION ? v : empty();
|
|
2200
|
+
} catch {
|
|
2201
|
+
return empty();
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
var tmpFor = (file) => `${file}.${process.pid}.${Date.now().toString(36)}.tmp`;
|
|
2205
|
+
var LOCK_FILE = path.join(HOME, "index.lock");
|
|
2206
|
+
var LOCK_STALE_MS = 10 * 6e4;
|
|
2207
|
+
async function acquireLock() {
|
|
2208
|
+
await fsp.mkdir(HOME, { recursive: true, mode: 448 });
|
|
2209
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
2210
|
+
try {
|
|
2211
|
+
const fh = await fsp.open(LOCK_FILE, "wx", 384);
|
|
2212
|
+
await fh.writeFile(JSON.stringify({ pid: process.pid, at: Date.now() }));
|
|
2213
|
+
await fh.close();
|
|
2214
|
+
return async () => {
|
|
2215
|
+
await fsp.rm(LOCK_FILE, { force: true }).catch(() => void 0);
|
|
2216
|
+
};
|
|
2217
|
+
} catch (err) {
|
|
2218
|
+
if (err.code !== "EEXIST") throw err;
|
|
2219
|
+
let holder = {};
|
|
2220
|
+
try {
|
|
2221
|
+
holder = JSON.parse(await fsp.readFile(LOCK_FILE, "utf8"));
|
|
2222
|
+
} catch {
|
|
2223
|
+
}
|
|
2224
|
+
const alive = (() => {
|
|
2225
|
+
if (!holder.pid) return false;
|
|
2226
|
+
try {
|
|
2227
|
+
process.kill(holder.pid, 0);
|
|
2228
|
+
return true;
|
|
2229
|
+
} catch {
|
|
2230
|
+
return false;
|
|
2231
|
+
}
|
|
2232
|
+
})();
|
|
2233
|
+
const stale = !alive || !holder.at || Date.now() - holder.at > LOCK_STALE_MS;
|
|
2234
|
+
if (!stale) return null;
|
|
2235
|
+
await fsp.rm(LOCK_FILE, { force: true }).catch(() => void 0);
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
return null;
|
|
2239
|
+
}
|
|
2240
|
+
async function waitForLock(maxMs = 2e4) {
|
|
2241
|
+
const t0 = Date.now();
|
|
2242
|
+
while (Date.now() - t0 < maxMs) {
|
|
2243
|
+
try {
|
|
2244
|
+
await fsp.access(LOCK_FILE);
|
|
2245
|
+
} catch {
|
|
2246
|
+
return true;
|
|
2247
|
+
}
|
|
2248
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
2249
|
+
}
|
|
2250
|
+
return false;
|
|
2251
|
+
}
|
|
2252
|
+
async function writePrivate(file, data) {
|
|
2253
|
+
await fsp.mkdir(HOME, { recursive: true, mode: 448 });
|
|
2254
|
+
await fsp.chmod(HOME, 448).catch(() => void 0);
|
|
2255
|
+
const tmp = tmpFor(file);
|
|
2256
|
+
await fsp.writeFile(tmp, JSON.stringify(data), { mode: 384 });
|
|
2257
|
+
await fsp.chmod(tmp, 384).catch(() => void 0);
|
|
2258
|
+
await fsp.rename(tmp, file);
|
|
2259
|
+
}
|
|
2260
|
+
var loadFacts = async () => {
|
|
2261
|
+
const f = await readJson(FACT_FILE, emptyFacts);
|
|
2262
|
+
f.skipped ??= {};
|
|
2263
|
+
f.sessions ??= {};
|
|
2264
|
+
f.names ??= {};
|
|
2265
|
+
f.bundles ??= {};
|
|
2266
|
+
return f;
|
|
2267
|
+
};
|
|
2268
|
+
var loadCache = () => readJson(CACHE_FILE, emptyCache);
|
|
2269
|
+
var loadText = () => readJson(TEXT_FILE, emptyText);
|
|
2270
|
+
async function rewriteTextLines(replaced, removed, full) {
|
|
2271
|
+
await fsp.mkdir(HOME, { recursive: true, mode: 448 });
|
|
2272
|
+
const tmp = tmpFor(TEXT_LINES);
|
|
2273
|
+
const out = createWriteStream(tmp, { mode: 384 });
|
|
2274
|
+
const write = (line) => new Promise((res, rej) => {
|
|
2275
|
+
out.write(line + "\n", (e) => e ? rej(e) : res());
|
|
2276
|
+
});
|
|
2277
|
+
if (!full) {
|
|
2278
|
+
let old = null;
|
|
2279
|
+
try {
|
|
2280
|
+
await fsp.access(TEXT_LINES);
|
|
2281
|
+
old = createInterface({ input: createReadStream(TEXT_LINES, { encoding: "utf8", highWaterMark: 1 << 20 }) });
|
|
2282
|
+
} catch {
|
|
2283
|
+
}
|
|
2284
|
+
if (old) {
|
|
2285
|
+
for await (const line of old) {
|
|
2286
|
+
const at = line.indexOf('"k":"');
|
|
2287
|
+
if (at < 0) continue;
|
|
2288
|
+
const k = JSON.parse(line.slice(at + 4, line.indexOf('"', at + 5) + 1));
|
|
2289
|
+
if (replaced.has(k) || removed.has(k)) continue;
|
|
2290
|
+
await write(line);
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
for (const [k, turns] of replaced) {
|
|
2295
|
+
for (const [i, t] of Object.entries(turns)) await write(JSON.stringify({ k, i: Number(i), ...t }));
|
|
2296
|
+
}
|
|
2297
|
+
await new Promise((res, rej) => out.end((e) => e ? rej(e) : res()));
|
|
2298
|
+
await fsp.chmod(tmp, 384).catch(() => void 0);
|
|
2299
|
+
await fsp.rename(tmp, TEXT_LINES);
|
|
2300
|
+
}
|
|
2301
|
+
async function buildIndex(full, canvasDir) {
|
|
2302
|
+
const t0 = Date.now();
|
|
2303
|
+
if (canvasDir) await rememberCanvasRoot(canvasDir);
|
|
2304
|
+
const release = await acquireLock();
|
|
2305
|
+
if (!release) {
|
|
2306
|
+
const done = await waitForLock();
|
|
2307
|
+
return { parsed: 0, kept: 0, skipped: 0, removed: 0, seconds: (Date.now() - t0) / 1e3, waited: done };
|
|
2308
|
+
}
|
|
2309
|
+
try {
|
|
2310
|
+
return await buildIndexLocked(full, t0);
|
|
2311
|
+
} finally {
|
|
2312
|
+
await release();
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
async function buildIndexLocked(full, t0) {
|
|
2316
|
+
await fsp.rm(LEGACY_FILE, { force: true }).catch(() => void 0);
|
|
2317
|
+
const facts = full ? emptyFacts() : await loadFacts();
|
|
2318
|
+
const cache = full ? emptyCache() : await loadCache();
|
|
2319
|
+
const text = full ? emptyText() : await loadText();
|
|
2320
|
+
const files = await listSources();
|
|
2321
|
+
let parsed = 0, kept = 0, skipped = 0, removed = 0;
|
|
2322
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2323
|
+
const replacedText = /* @__PURE__ */ new Map();
|
|
2324
|
+
const removedKeys = /* @__PURE__ */ new Set();
|
|
2325
|
+
for (const f of files) {
|
|
2326
|
+
const key = await canonicalPath(f.file);
|
|
2327
|
+
seen.add(key);
|
|
2328
|
+
const prev = facts.sessions[key];
|
|
2329
|
+
const sk = facts.skipped[key];
|
|
2330
|
+
if (sk && sk.mtime === f.mtime && sk.size === f.size) {
|
|
2331
|
+
skipped++;
|
|
2332
|
+
continue;
|
|
2333
|
+
}
|
|
2334
|
+
if (prev && prev.mtime === f.mtime && prev.size === f.size && key in cache.sessions && key in text.sessions) {
|
|
2335
|
+
kept++;
|
|
2336
|
+
continue;
|
|
2337
|
+
}
|
|
2338
|
+
const r = await parseSession(f).catch(() => null);
|
|
2339
|
+
if (!r) {
|
|
2340
|
+
facts.skipped[key] = { mtime: f.mtime, size: f.size };
|
|
2341
|
+
skipped++;
|
|
2342
|
+
continue;
|
|
2343
|
+
}
|
|
2344
|
+
delete facts.skipped[key];
|
|
2345
|
+
facts.sessions[key] = r.fact;
|
|
2346
|
+
cache.sessions[key] = r.cache;
|
|
2347
|
+
replacedText.set(key, r.text);
|
|
2348
|
+
text.sessions[key] = Object.keys(r.text).length;
|
|
2349
|
+
Object.assign(facts.names, r.names);
|
|
2350
|
+
Object.assign(facts.bundles, r.bundles);
|
|
2351
|
+
parsed++;
|
|
2352
|
+
}
|
|
2353
|
+
for (const key of Object.keys(facts.sessions)) {
|
|
2354
|
+
if (!seen.has(key)) {
|
|
2355
|
+
delete facts.sessions[key];
|
|
2356
|
+
delete cache.sessions[key];
|
|
2357
|
+
delete text.sessions[key];
|
|
2358
|
+
removedKeys.add(key);
|
|
2359
|
+
removed++;
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
for (const key of Object.keys(facts.skipped)) if (!seen.has(key)) delete facts.skipped[key];
|
|
2363
|
+
facts.builtAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2364
|
+
await writePrivate(FACT_FILE, facts);
|
|
2365
|
+
await writePrivate(CACHE_FILE, cache);
|
|
2366
|
+
if (full || replacedText.size || removedKeys.size) await rewriteTextLines(replacedText, removedKeys, full);
|
|
2367
|
+
await writePrivate(TEXT_FILE, text);
|
|
2368
|
+
return { parsed, kept, skipped, removed, seconds: (Date.now() - t0) / 1e3 };
|
|
2369
|
+
}
|
|
2370
|
+
var quiet = false;
|
|
2371
|
+
function setQuiet(v) {
|
|
2372
|
+
quiet = v;
|
|
2373
|
+
}
|
|
2374
|
+
async function ensureFresh() {
|
|
2375
|
+
const facts = await loadFacts();
|
|
2376
|
+
const cache = await loadCache();
|
|
2377
|
+
const text = await loadText();
|
|
2378
|
+
const files = await listSources();
|
|
2379
|
+
const keyed = await Promise.all(files.map(async (f) => ({ ...f, key: await canonicalPath(f.file) })));
|
|
2380
|
+
const known = (f) => {
|
|
2381
|
+
const p = facts.sessions[f.key];
|
|
2382
|
+
if (p) return p.mtime === f.mtime && p.size === f.size && f.key in cache.sessions && f.key in text.sessions;
|
|
2383
|
+
const sk = facts.skipped[f.key];
|
|
2384
|
+
return !!sk && sk.mtime === f.mtime && sk.size === f.size;
|
|
2385
|
+
};
|
|
2386
|
+
const stale = !facts.builtAt || keyed.some((f) => !known(f)) || Object.keys(facts.sessions).length + Object.keys(facts.skipped).length !== keyed.length;
|
|
2387
|
+
if (!stale) return facts;
|
|
2388
|
+
const r = await buildIndex(false);
|
|
2389
|
+
if (!quiet) {
|
|
2390
|
+
if (r.waited !== void 0) console.error(r.waited ? "(index refreshed by another process)" : "(index refresh in progress elsewhere; answering from the current index)");
|
|
2391
|
+
else console.error(`(index refreshed: ${r.parsed} session${r.parsed === 1 ? "" : "s"} re-read, ${r.removed} gone, ${r.seconds.toFixed(1)}s)`);
|
|
2392
|
+
}
|
|
2393
|
+
return loadFacts();
|
|
2394
|
+
}
|
|
2395
|
+
var schemeOf = (id) => id.startsWith("file://") ? "file" : id.startsWith("arxiv:") ? "arxiv" : /^https?:\/\//.test(id) ? "url" : "other";
|
|
2396
|
+
var artifactsLine = (a) => [`${a.file} files`, a.url ? `${a.url} urls` : "", a.arxiv ? `${a.arxiv} papers` : "", a.other ? `${a.other} other` : ""].filter(Boolean).join(" \xB7 ");
|
|
2397
|
+
function summarize(facts, cache) {
|
|
2398
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2399
|
+
const artifacts = { file: 0, url: 0, arxiv: 0, other: 0 };
|
|
2400
|
+
let turns = 0, touches = 0, changes = 0, withChangeHead = 0, withMention = 0;
|
|
2401
|
+
for (const [key, s] of Object.entries(facts.sessions)) {
|
|
2402
|
+
turns += s.turns.length;
|
|
2403
|
+
for (const t of s.turns) {
|
|
2404
|
+
const ct = cache.sessions[key]?.[String(t.i)];
|
|
2405
|
+
for (const [p, x] of Object.entries(t.ops)) {
|
|
2406
|
+
if (!seen.has(p)) {
|
|
2407
|
+
seen.add(p);
|
|
2408
|
+
artifacts[schemeOf(p)]++;
|
|
2409
|
+
}
|
|
2410
|
+
touches++;
|
|
2411
|
+
if (!readLike(x.op)) {
|
|
2412
|
+
changes++;
|
|
2413
|
+
if (x.d) withChangeHead++;
|
|
2414
|
+
}
|
|
2415
|
+
if (ct?.m?.[p]) withMention++;
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
const sessions = new Set(Object.values(facts.sessions).map((s) => s.id)).size;
|
|
2420
|
+
return { sessions, sources: Object.keys(facts.sessions).length, turns, artifacts, touches, changes, withChangeHead, withMention };
|
|
2421
|
+
}
|
|
2422
|
+
var allPaths = (facts) => {
|
|
2423
|
+
const all = /* @__PURE__ */ new Set();
|
|
2424
|
+
for (const s of Object.values(facts.sessions)) for (const t of s.turns) for (const p of Object.keys(t.ops)) all.add(p);
|
|
2425
|
+
return all;
|
|
2426
|
+
};
|
|
2427
|
+
async function resolveQuery(facts, rawArg, all) {
|
|
2428
|
+
const arg = rawArg.trim().replace(/^@/, "").replace(/^["'`]|["'`:]+$/g, "");
|
|
2429
|
+
const ids = allPaths(facts);
|
|
2430
|
+
const web = urlArtifact(arg) ?? arxivArtifact(arg);
|
|
2431
|
+
if (web) return ids.has(web.id) ? { path: web.id, candidates: [web.id], elsewhere: 0 } : { path: null, candidates: [], elsewhere: 0 };
|
|
2432
|
+
if (arg.startsWith("thoughtdag:")) return ids.has(arg) ? { path: arg, candidates: [arg], elsewhere: 0 } : { path: null, candidates: [], elsewhere: 0 };
|
|
2433
|
+
const named = Object.entries(facts.names).filter(([, name]) => name === arg || name.endsWith(`/${arg}`)).map(([id]) => id);
|
|
2434
|
+
if (named.length === 1) return { path: named[0], candidates: named, elsewhere: 0 };
|
|
2435
|
+
const abs = absolutePath(arg, process.cwd());
|
|
2436
|
+
if (abs) {
|
|
2437
|
+
const typed = fileUri(abs);
|
|
2438
|
+
if (ids.has(typed)) return { path: typed, candidates: [typed], elsewhere: 0 };
|
|
2439
|
+
const real = fileUri(await canonicalPath(abs));
|
|
2440
|
+
if (ids.has(real)) return { path: real, candidates: [real], elsewhere: 0 };
|
|
2441
|
+
}
|
|
2442
|
+
const needle = arg.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
2443
|
+
const files = [...ids].flatMap((id) => {
|
|
2444
|
+
const p = filePathOf(id);
|
|
2445
|
+
return p ? [{ id, p }] : [];
|
|
2446
|
+
});
|
|
2447
|
+
const bySuffix = files.filter(({ p }) => p === needle || p.endsWith(`/${needle}`));
|
|
2448
|
+
const ws = (await workspaceOf(process.cwd())).replace(/\\/g, "/");
|
|
2449
|
+
const inside = all ? bySuffix : bySuffix.filter(({ p }) => p.startsWith(`${ws}/`));
|
|
2450
|
+
if (inside.length === 1) return { path: inside[0].id, candidates: [inside[0].p], elsewhere: bySuffix.length - inside.length };
|
|
2451
|
+
return { path: null, candidates: inside.map((x) => x.p).sort(), elsewhere: bySuffix.length - inside.length };
|
|
2452
|
+
}
|
|
2453
|
+
function hitsFor(facts, artifact, includeRead) {
|
|
2454
|
+
const all = [];
|
|
2455
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2456
|
+
const sources = Object.entries(facts.sessions).sort((a, b) => a[1].mtime - b[1].mtime);
|
|
2457
|
+
for (const [sourceKey, session] of sources) {
|
|
2458
|
+
for (const turn of session.turns) {
|
|
2459
|
+
const touch = turn.ops[artifact];
|
|
2460
|
+
if (!touch) continue;
|
|
2461
|
+
if (turn.item) {
|
|
2462
|
+
if (seen.has(turn.item)) continue;
|
|
2463
|
+
seen.add(turn.item);
|
|
2464
|
+
}
|
|
2465
|
+
all.push({ session, sourceKey, turn, touch });
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
const hasChanges = all.some((h) => !readLike(h.touch.op));
|
|
2469
|
+
if (includeRead || !hasChanges) return { hits: all, readsHidden: 0 };
|
|
2470
|
+
const hits = all.filter((h) => !readLike(h.touch.op));
|
|
2471
|
+
return { hits, readsHidden: all.length - hits.length };
|
|
2472
|
+
}
|
|
2473
|
+
function anchorStatus(facts, a) {
|
|
2474
|
+
const b = facts.bundles[a.bundle];
|
|
2475
|
+
if (!b) return "unverified";
|
|
2476
|
+
return b.node === a.node ? "matched" : "mismatch";
|
|
2477
|
+
}
|
|
2478
|
+
function openLink(h) {
|
|
2479
|
+
if (h.session.runner === "thoughtdag") return `thoughtdag://open?canvas=${encodeURIComponent(h.session.id)}&node=${encodeURIComponent(h.turn.t.split("#").pop() ?? "")}`;
|
|
2480
|
+
return `thoughtdag://open?session=${h.session.id}${h.turn.item ? `&turn=${encodeURIComponent(h.turn.item)}` : ""}`;
|
|
2481
|
+
}
|
|
2482
|
+
var OP_MARK = { edit: "\u270F\uFE0F edit ", write: "\u270F\uFE0F write", read: "\u{1F4D6} read ", fetch: "\u{1F310} fetch", attach: "\u{1F4CE} attach" };
|
|
2483
|
+
function when(t, s) {
|
|
2484
|
+
const d = new Date(t.at ?? s.mtime);
|
|
2485
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
2486
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
|
2487
|
+
}
|
|
2488
|
+
var EVIDENCE = {
|
|
2489
|
+
footprint: "observed",
|
|
2490
|
+
change: "observed, partial (first differing line)",
|
|
2491
|
+
askedBefore: "inferred (the earlier question a bare reply answers)",
|
|
2492
|
+
about: "inferred (a paragraph of the answer naming the file)",
|
|
2493
|
+
conclusion: "inferred (the answer's closing paragraph)"
|
|
2494
|
+
};
|
|
2495
|
+
function fragmentsOf(facts, sessionId) {
|
|
2496
|
+
const frags = Object.entries(facts.sessions).filter(([, s]) => s.id === sessionId).sort((a, b) => a[1].mtime - b[1].mtime);
|
|
2497
|
+
let offset = 0;
|
|
2498
|
+
return frags.map(([key, s]) => {
|
|
2499
|
+
const f = { key, s, offset };
|
|
2500
|
+
offset += s.turns.length;
|
|
2501
|
+
return f;
|
|
2502
|
+
});
|
|
2503
|
+
}
|
|
2504
|
+
function turnNumber(facts, h) {
|
|
2505
|
+
return (fragmentsOf(facts, h.session.id).find((f) => f.key === h.sourceKey)?.offset ?? 0) + h.turn.i;
|
|
2506
|
+
}
|
|
2507
|
+
async function renderWhy(facts, file, hits, readsHidden, cache, limit, json) {
|
|
2508
|
+
const out = [];
|
|
2509
|
+
const bySession = /* @__PURE__ */ new Map();
|
|
2510
|
+
for (const h of hits) (bySession.get(h.session.id) ?? bySession.set(h.session.id, []).get(h.session.id)).push(h);
|
|
2511
|
+
const groups = [...bySession.values()].map((hs) => hs.sort((a, b) => a.turn.i - b.turn.i)).sort((a, b) => (b[b.length - 1].turn.at ?? "").localeCompare(a[a.length - 1].turn.at ?? ""));
|
|
2512
|
+
const shown = /* @__PURE__ */ new Set();
|
|
2513
|
+
for (const g of groups) for (const h of g) if (shown.size < limit) shown.add(h);
|
|
2514
|
+
const interp = (h) => cache.sessions[h.sourceKey]?.[String(h.turn.i)] ?? {};
|
|
2515
|
+
if (json) {
|
|
2516
|
+
out.push(JSON.stringify({
|
|
2517
|
+
artifact: file,
|
|
2518
|
+
file: filePathOf(file),
|
|
2519
|
+
turns: hits.length,
|
|
2520
|
+
sessions: bySession.size,
|
|
2521
|
+
readsHidden,
|
|
2522
|
+
evidence: EVIDENCE,
|
|
2523
|
+
hits: [...shown].map((h) => {
|
|
2524
|
+
const c = interp(h);
|
|
2525
|
+
return {
|
|
2526
|
+
session: h.session.id,
|
|
2527
|
+
runner: h.session.runner,
|
|
2528
|
+
title: h.session.title,
|
|
2529
|
+
subagent: !!h.session.subagent,
|
|
2530
|
+
anchor: h.session.anchor ? { ...h.session.anchor, status: anchorStatus(facts, h.session.anchor) } : null,
|
|
2531
|
+
turn: turnNumber(facts, h),
|
|
2532
|
+
at: h.turn.at ?? null,
|
|
2533
|
+
op: h.touch.op,
|
|
2534
|
+
locators: h.touch.l ?? null,
|
|
2535
|
+
question: h.turn.q,
|
|
2536
|
+
askedBefore: c.p ?? null,
|
|
2537
|
+
change: h.touch.d ?? null,
|
|
2538
|
+
about: c.m?.[file] ?? null,
|
|
2539
|
+
conclusion: c.c ?? null,
|
|
2540
|
+
open: openLink(h)
|
|
2541
|
+
};
|
|
2542
|
+
})
|
|
2543
|
+
}, null, 1));
|
|
2544
|
+
return out.join("\n");
|
|
2545
|
+
}
|
|
2546
|
+
const label = await displayOf(file, facts.names);
|
|
2547
|
+
const n = hits.length;
|
|
2548
|
+
const head = `why ${label} \xB7 ${n} turn${n === 1 ? "" : "s"} in ${bySession.size} session${bySession.size === 1 ? "" : "s"}`;
|
|
2549
|
+
const notes = [
|
|
2550
|
+
readsHidden ? `${readsHidden} read${readsHidden === 1 ? "" : "s"} hidden, --include-read` : "",
|
|
2551
|
+
n > limit ? `showing ${limit}, --limit for more` : ""
|
|
2552
|
+
].filter(Boolean);
|
|
2553
|
+
out.push(`${head}${notes.length ? ` (${notes.join("; ")})` : ""}
|
|
2554
|
+
`);
|
|
2555
|
+
let printed = 0;
|
|
2556
|
+
for (const g of groups) {
|
|
2557
|
+
const mine = g.filter((h) => shown.has(h));
|
|
2558
|
+
if (!mine.length) continue;
|
|
2559
|
+
const s = g[0].session;
|
|
2560
|
+
out.push(`${s.runner} \u300C${s.title.slice(0, 70)}\u300D${s.subagent ? " (subagent)" : ""}${s.anchor ? ` \u21A9 from canvas node ${s.anchor.node} (${s.anchor.bundle}, ${anchorStatus(facts, s.anchor)})` : ""}`);
|
|
2561
|
+
for (const h of mine) {
|
|
2562
|
+
const c = interp(h);
|
|
2563
|
+
const where = h.touch.l?.map((l) => l.pages ? `p.${l.pages}` : l.lines ? `L${l.lines[0]}-${l.lines[1]}` : "").filter(Boolean).join(" ");
|
|
2564
|
+
out.push(` ${when(h.turn, s)} ${OP_MARK[h.touch.op]} #${turnNumber(facts, h)}${where ? ` ${where}` : ""} ${openLink(h)}`);
|
|
2565
|
+
out.push(` Q: ${h.turn.q}${c.p ? ` \u2934 ${c.p}` : ""}`);
|
|
2566
|
+
if (h.touch.d) out.push(` \u0394 ${h.touch.d}`);
|
|
2567
|
+
const about = c.m?.[file];
|
|
2568
|
+
if (about) out.push(` \u2248 ${about}`);
|
|
2569
|
+
else if (c.c) out.push(` \u2248 ${c.c} (closing line)`);
|
|
2570
|
+
printed++;
|
|
2571
|
+
}
|
|
2572
|
+
out.push("");
|
|
2573
|
+
}
|
|
2574
|
+
if (!printed) out.push("(no turns)\n");
|
|
2575
|
+
out.push("\u0394 observed change \xB7 \u2248 read from the answer, a candidate explanation, not a verified reason \xB7 \u2934 the earlier question this reply answers");
|
|
2576
|
+
return out.join("\n");
|
|
2577
|
+
}
|
|
2578
|
+
function snippetAround(text, needle, width = 70) {
|
|
2579
|
+
const at = text.toLowerCase().indexOf(needle);
|
|
2580
|
+
if (at < 0) return "";
|
|
2581
|
+
const start = Math.max(0, at - width);
|
|
2582
|
+
const end = Math.min(text.length, at + needle.length + width);
|
|
2583
|
+
return `${start > 0 ? "\u2026" : ""}${text.slice(start, end).replace(/\s+/g, " ").trim()}${end < text.length ? "\u2026" : ""}`;
|
|
2584
|
+
}
|
|
2585
|
+
async function findHits(facts, phrase, scope) {
|
|
2586
|
+
const needle = phrase.toLowerCase();
|
|
2587
|
+
const turnsOf = /* @__PURE__ */ new Map();
|
|
2588
|
+
const lookup = (k, i) => {
|
|
2589
|
+
let m = turnsOf.get(k);
|
|
2590
|
+
if (!m) {
|
|
2591
|
+
m = new Map(facts.sessions[k]?.turns.map((t) => [t.i, t]) ?? []);
|
|
2592
|
+
turnsOf.set(k, m);
|
|
2593
|
+
}
|
|
2594
|
+
return m.get(i);
|
|
2595
|
+
};
|
|
2596
|
+
const raw = [];
|
|
2597
|
+
let lines = null;
|
|
2598
|
+
try {
|
|
2599
|
+
await fsp.access(TEXT_LINES);
|
|
2600
|
+
lines = createInterface({ input: createReadStream(TEXT_LINES, { encoding: "utf8", highWaterMark: 1 << 20 }) });
|
|
2601
|
+
} catch {
|
|
2602
|
+
return [];
|
|
2603
|
+
}
|
|
2604
|
+
for await (const line of lines) {
|
|
2605
|
+
if (!line.toLowerCase().includes(needle)) continue;
|
|
2606
|
+
let t;
|
|
2607
|
+
try {
|
|
2608
|
+
t = JSON.parse(line);
|
|
2609
|
+
} catch {
|
|
2610
|
+
continue;
|
|
2611
|
+
}
|
|
2612
|
+
const session = facts.sessions[t.k];
|
|
2613
|
+
const turn = lookup(t.k, t.i);
|
|
2614
|
+
if (!session || !turn) continue;
|
|
2615
|
+
const fields = [["Q", t.q], ["A", t.a], ["M", t.m]];
|
|
2616
|
+
for (const [where, body] of fields) {
|
|
2617
|
+
if (!body) continue;
|
|
2618
|
+
if (scope !== "all" && where.toLowerCase() !== scope) continue;
|
|
2619
|
+
if (!body.toLowerCase().includes(needle)) continue;
|
|
2620
|
+
raw.push({ session, sourceKey: t.k, turn, where, snippet: snippetAround(body, needle) });
|
|
2621
|
+
break;
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2625
|
+
const hits = [];
|
|
2626
|
+
for (const h of raw.sort((a, b) => a.session.mtime - b.session.mtime)) {
|
|
2627
|
+
if (h.turn.item) {
|
|
2628
|
+
if (seen.has(h.turn.item)) continue;
|
|
2629
|
+
seen.add(h.turn.item);
|
|
2630
|
+
}
|
|
2631
|
+
hits.push(h);
|
|
2632
|
+
}
|
|
2633
|
+
return hits.sort((a, b) => (b.turn.at ?? "").localeCompare(a.turn.at ?? ""));
|
|
2634
|
+
}
|
|
2635
|
+
function renderFind(facts, phrase, hits, limit, json) {
|
|
2636
|
+
const out = [];
|
|
2637
|
+
const shown = hits.slice(0, limit);
|
|
2638
|
+
const sessions = new Set(hits.map((h) => h.session.id)).size;
|
|
2639
|
+
if (json) {
|
|
2640
|
+
out.push(JSON.stringify({
|
|
2641
|
+
phrase,
|
|
2642
|
+
turns: hits.length,
|
|
2643
|
+
sessions,
|
|
2644
|
+
evidence: { Q: "observed (a question, verbatim)", A: "observed (an answer, verbatim)", M: "observed (an attached material's text, verbatim)" },
|
|
2645
|
+
hits: shown.map((h) => ({ session: h.session.id, runner: h.session.runner, title: h.session.title, turn: turnNumber(facts, h), at: h.turn.at ?? null, where: h.where, snippet: h.snippet, open: openLink(h) }))
|
|
2646
|
+
}, null, 1));
|
|
2647
|
+
return out.join("\n");
|
|
2648
|
+
}
|
|
2649
|
+
out.push(`find "${phrase}" \xB7 ${hits.length} turn${hits.length === 1 ? "" : "s"} in ${sessions} session${sessions === 1 ? "" : "s"}${hits.length > limit ? ` (showing ${limit}, --limit for more)` : ""}
|
|
2650
|
+
`);
|
|
2651
|
+
for (const h of shown) {
|
|
2652
|
+
const s = h.session;
|
|
2653
|
+
out.push(`${when(h.turn, s)} ${s.runner} \u300C${s.title.slice(0, 60)}\u300D #${turnNumber(facts, h)} ${openLink(h)}`);
|
|
2654
|
+
out.push(` ${h.where}: ${h.snippet}`);
|
|
2655
|
+
}
|
|
2656
|
+
if (!shown.length) out.push("(nothing asked, answered or attached in those words \u2014 try another wording; matching is exact)");
|
|
2657
|
+
else out.push("\nQ: asked \xB7 A: answered \xB7 M: in an attached material \u2014 all verbatim; the phrase must appear in those words");
|
|
2658
|
+
return out.join("\n");
|
|
2659
|
+
}
|
|
2660
|
+
var CHECK_FRESH_MS = 10 * 6e4;
|
|
2661
|
+
async function factsForCheck(force) {
|
|
2662
|
+
const facts = await loadFacts();
|
|
2663
|
+
const age = facts.builtAt ? Date.now() - new Date(facts.builtAt).getTime() : Infinity;
|
|
2664
|
+
return force || age > CHECK_FRESH_MS ? ensureFresh() : facts;
|
|
2665
|
+
}
|
|
2666
|
+
function renderCheck(facts, arg, file, hits, json) {
|
|
2667
|
+
const changes = hits.filter((h) => !readLike(h.touch.op)).length;
|
|
2668
|
+
const reads = hits.length - changes;
|
|
2669
|
+
const sessions = new Set(hits.map((h) => h.session.id)).size;
|
|
2670
|
+
const latest = hits.map((h) => h.turn.at ?? "").filter(Boolean).sort().pop();
|
|
2671
|
+
const has = hits.length > 0;
|
|
2672
|
+
if (json) return { text: JSON.stringify({ query: arg, artifact: file, history: has, turns: hits.length, sessions, changes, reads, latest: latest ?? null }), has };
|
|
2673
|
+
if (!has) return { text: `${arg}: no history`, has };
|
|
2674
|
+
const parts = [`${hits.length} turn${hits.length === 1 ? "" : "s"} in ${sessions} session${sessions === 1 ? "" : "s"}`, changes ? `${changes} edit${changes === 1 ? "" : "s"}/write${changes === 1 ? "" : "s"}` : "", reads ? `${reads} read${reads === 1 ? "" : "s"}` : "", latest ? `latest ${latest.slice(0, 10)}` : ""].filter(Boolean);
|
|
2675
|
+
return { text: `${arg}: ${parts.join(" \xB7 ")} \u2192 thoughtdag why ${arg}`, has };
|
|
2676
|
+
}
|
|
2677
|
+
async function renderRecall(facts, sidPrefix, n) {
|
|
2678
|
+
const out = [];
|
|
2679
|
+
const any = Object.values(facts.sessions).find((x) => x.id.startsWith(sidPrefix));
|
|
2680
|
+
if (!any) throw new Error(`no session starts with ${sidPrefix}`);
|
|
2681
|
+
const frag = fragmentsOf(facts, any.id).find((f) => n >= f.offset && n < f.offset + f.s.turns.length);
|
|
2682
|
+
const s = frag?.s ?? any;
|
|
2683
|
+
const local = frag ? n - frag.offset : n;
|
|
2684
|
+
const p = await eventsOf(s.file);
|
|
2685
|
+
const turn = p?.turns?.[local] ?? (() => {
|
|
2686
|
+
const tid = s.turns[local]?.t;
|
|
2687
|
+
const text = tid ? p?.texts.get(tid) : void 0;
|
|
2688
|
+
return text ? { question: text.question, response: text.response, tools: [], at: s.turns[local]?.at } : void 0;
|
|
2689
|
+
})();
|
|
2690
|
+
if (!turn) throw new Error(`session ${s.id.slice(0, 8)} has no turn #${n}`);
|
|
2691
|
+
out.push(`${s.runner} \u300C${s.title}\u300D turn #${n}${turn.at ? ` ${turn.at.slice(0, 16).replace("T", " ")}` : ""}
|
|
2692
|
+
`);
|
|
2693
|
+
out.push(`## Question
|
|
2694
|
+
|
|
2695
|
+
${turn.question.trim()}
|
|
2696
|
+
`);
|
|
2697
|
+
out.push(`## Answer
|
|
2698
|
+
|
|
2699
|
+
${turn.response.trim() || "(none)"}
|
|
2700
|
+
`);
|
|
2701
|
+
if (turn.tools.length) {
|
|
2702
|
+
out.push(`## Tools (${turn.tools.length})
|
|
2703
|
+
`);
|
|
2704
|
+
for (const t of turn.tools) {
|
|
2705
|
+
out.push(`- ${t.name}${t.paths?.length ? ` ${t.paths.join(", ")}` : ""}`);
|
|
2706
|
+
if (t.op === "edit" || t.op === "write") out.push(t.call.slice(0, 1200).split("\n").map((l) => ` ${l}`).join("\n"));
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
return out.join("\n");
|
|
2710
|
+
}
|
|
2711
|
+
var CLI_VERSION = "0.1.0";
|
|
2712
|
+
var MCP_TOOLS = [
|
|
2713
|
+
{ name: "why_check", description: "Cheap first question before editing a file: does this artifact have any history in local agent sessions? One line; history true/false.", inputSchema: { type: "object", properties: { path: { type: "string", description: "file path (absolute or relative to cwd), URL, or arxiv:<id>" } }, required: ["path"] } },
|
|
2714
|
+
{ name: "why_file", description: "The turns across local Claude Code, Codex, DeepSeek Harness, Pi and ThoughtDAG sessions that touched a file, URL or paper: when, what changed (\u0394, observed), what was asked, what the answer said about it (\u2248, a candidate explanation, not a verified reason). Each hit carries a deep link.", inputSchema: { type: "object", properties: { path: { type: "string" }, include_read: { type: "boolean", description: "also list turns that only read it (default false)" }, limit: { type: "number", description: "max hits (default 10)" } }, required: ["path"] } },
|
|
2715
|
+
{ name: "find", description: "Where these exact words were asked (Q), answered (A) or attached (M) across local sessions and canvases. Exact, case-insensitive match; every hit is a verbatim snippet with a pointer.", inputSchema: { type: "object", properties: { phrase: { type: "string" }, in: { type: "string", enum: ["q", "a", "m", "all"] }, limit: { type: "number" } }, required: ["phrase"] } },
|
|
2716
|
+
{ name: "recall_turn", description: "One turn in full \u2014 the question, the answer, the tool calls with their diffs \u2014 by session id (or prefix) and turn number as shown by why_file.", inputSchema: { type: "object", properties: { session: { type: "string" }, turn: { type: "number" } }, required: ["session", "turn"] } }
|
|
2717
|
+
];
|
|
2718
|
+
async function mcpCall(name, a) {
|
|
2719
|
+
if (name === "why_check") {
|
|
2720
|
+
const facts = await factsForCheck(false);
|
|
2721
|
+
const q = String(a.path ?? "");
|
|
2722
|
+
const { path: file } = await resolveQuery(facts, q, false);
|
|
2723
|
+
return renderCheck(facts, q, file, file ? hitsFor(facts, file, true).hits : [], false).text;
|
|
2724
|
+
}
|
|
2725
|
+
if (name === "why_file") {
|
|
2726
|
+
const facts = await ensureFresh();
|
|
2727
|
+
const q = String(a.path ?? "");
|
|
2728
|
+
const { path: file, candidates, elsewhere } = await resolveQuery(facts, q, false);
|
|
2729
|
+
if (!file) return candidates.length ? `${candidates.length} files match "${q}" \u2014 pick one:
|
|
2730
|
+
${candidates.slice(0, 20).map((c) => ` ${c}`).join("\n")}` : elsewhere ? `no match in this workspace (${elsewhere} elsewhere)` : `no session touched ${q}`;
|
|
2731
|
+
const { hits, readsHidden } = hitsFor(facts, file, a.include_read === true);
|
|
2732
|
+
return renderWhy(facts, file, hits, readsHidden, await loadCache(), Number(a.limit ?? 10) || 10, false);
|
|
2733
|
+
}
|
|
2734
|
+
if (name === "find") {
|
|
2735
|
+
const facts = await ensureFresh();
|
|
2736
|
+
const scope = ["q", "a", "m"].find((x) => x === a.in) ?? "all";
|
|
2737
|
+
return renderFind(facts, String(a.phrase ?? ""), await findHits(facts, String(a.phrase ?? ""), scope), Number(a.limit ?? 10) || 10, false);
|
|
2738
|
+
}
|
|
2739
|
+
if (name === "recall_turn") return renderRecall(await ensureFresh(), String(a.session ?? ""), Number(a.turn ?? 0));
|
|
2740
|
+
throw new Error(`unknown tool: ${name}`);
|
|
2741
|
+
}
|
|
2742
|
+
export {
|
|
2743
|
+
CACHE_FILE,
|
|
2744
|
+
CLI_VERSION,
|
|
2745
|
+
FACT_FILE,
|
|
2746
|
+
HOME,
|
|
2747
|
+
LEGACY_FILE,
|
|
2748
|
+
LOCK_FILE,
|
|
2749
|
+
MCP_TOOLS,
|
|
2750
|
+
TEXT_FILE,
|
|
2751
|
+
TEXT_LINES,
|
|
2752
|
+
artifactsLine,
|
|
2753
|
+
buildIndex,
|
|
2754
|
+
ensureFresh,
|
|
2755
|
+
eventsOf,
|
|
2756
|
+
factsForCheck,
|
|
2757
|
+
findHits,
|
|
2758
|
+
hitsFor,
|
|
2759
|
+
loadCache,
|
|
2760
|
+
loadFacts,
|
|
2761
|
+
mcpCall,
|
|
2762
|
+
renderCheck,
|
|
2763
|
+
renderFind,
|
|
2764
|
+
renderRecall,
|
|
2765
|
+
renderWhy,
|
|
2766
|
+
resolveQuery,
|
|
2767
|
+
setQuiet,
|
|
2768
|
+
summarize,
|
|
2769
|
+
when,
|
|
2770
|
+
zstdSkipped
|
|
2771
|
+
};
|