@robiteame/dsh-pi-agent-session-tree 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js ADDED
@@ -0,0 +1,1190 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : /* @__PURE__ */ Symbol.for("Symbol." + name);
5
+ var __typeError = (msg) => {
6
+ throw TypeError(msg);
7
+ };
8
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
9
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
10
+ var __decoratorStart = (base) => [, , , __create(base?.[__knownSymbol("metadata")] ?? null)];
11
+ var __decoratorStrings = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
12
+ var __expectFn = (fn) => fn !== void 0 && typeof fn !== "function" ? __typeError("Function expected") : fn;
13
+ var __decoratorContext = (kind, name, done, metadata, fns) => ({ kind: __decoratorStrings[kind], name, metadata, addInitializer: (fn) => done._ ? __typeError("Already initialized") : fns.push(__expectFn(fn || null)) });
14
+ var __decoratorMetadata = (array, target) => __defNormalProp(target, __knownSymbol("metadata"), array[3]);
15
+ var __runInitializers = (array, flags, self, value) => {
16
+ for (var i = 0, fns = array[flags >> 1], n = fns && fns.length; i < n; i++) flags & 1 ? fns[i].call(self) : value = fns[i].call(self, value);
17
+ return value;
18
+ };
19
+ var __decorateElement = (array, flags, name, decorators, target, extra) => {
20
+ var fn, it, done, ctx, access, k = flags & 7, s = !!(flags & 8), p = !!(flags & 16);
21
+ var j = k > 3 ? array.length + 1 : k ? s ? 1 : 2 : 0, key = __decoratorStrings[k + 5];
22
+ var initializers = k > 3 && (array[j - 1] = []), extraInitializers = array[j] || (array[j] = []);
23
+ var desc = k && (!p && !s && (target = target.prototype), k < 5 && (k > 3 || !p) && __getOwnPropDesc(k < 4 ? target : { get [name]() {
24
+ return __privateGet(this, extra);
25
+ }, set [name](x) {
26
+ return __privateSet(this, extra, x);
27
+ } }, name));
28
+ k ? p && k < 4 && __name(extra, (k > 2 ? "set " : k > 1 ? "get " : "") + name) : __name(target, name);
29
+ for (var i = decorators.length - 1; i >= 0; i--) {
30
+ ctx = __decoratorContext(k, name, done = {}, array[3], extraInitializers);
31
+ if (k) {
32
+ ctx.static = s, ctx.private = p, access = ctx.access = { has: p ? (x) => __privateIn(target, x) : (x) => name in x };
33
+ if (k ^ 3) access.get = p ? (x) => (k ^ 1 ? __privateGet : __privateMethod)(x, target, k ^ 4 ? extra : desc.get) : (x) => x[name];
34
+ if (k > 2) access.set = p ? (x, y) => __privateSet(x, target, y, k ^ 4 ? extra : desc.set) : (x, y) => x[name] = y;
35
+ }
36
+ it = (0, decorators[i])(k ? k < 4 ? p ? extra : desc[key] : k > 4 ? void 0 : { get: desc.get, set: desc.set } : target, ctx), done._ = 1;
37
+ if (k ^ 4 || it === void 0) __expectFn(it) && (k > 4 ? initializers.unshift(it) : k ? p ? extra = it : desc[key] = it : target = it);
38
+ else if (typeof it !== "object" || it === null) __typeError("Object expected");
39
+ else __expectFn(fn = it.get) && (desc.get = fn), __expectFn(fn = it.set) && (desc.set = fn), __expectFn(fn = it.init) && initializers.unshift(fn);
40
+ }
41
+ return k || __decoratorMetadata(array, target), desc && __defProp(target, name, desc), p ? k ^ 4 ? extra : desc : target;
42
+ };
43
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
44
+ var __privateIn = (member, obj) => Object(obj) !== obj ? __typeError('Cannot use the "in" operator on this value') : member.has(obj);
45
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
46
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
47
+ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
48
+
49
+ // packages/extensions/pi-agent-session-tree/src/index.ts
50
+ import { isSurfaceEvent } from "@deepseek-ai/dsh-session";
51
+ import { TypertRemoteService, Remote } from "@deepseek-ai/dsh-typert-protocol";
52
+
53
+ // packages/extensions/pi-agent-session-tree/src/session-tree-marker.ts
54
+ function sessionTreeMarkerOf(event) {
55
+ const data = event.data;
56
+ const marker = data.treeRestore;
57
+ if (typeof marker !== "object" || marker === null) return void 0;
58
+ const candidate = marker;
59
+ if (candidate.kind !== "cursor") return void 0;
60
+ if (candidate.nodeId !== null && typeof candidate.nodeId !== "string") return void 0;
61
+ return { kind: "cursor", nodeId: candidate.nodeId };
62
+ }
63
+ function isSessionTreeRestoreEvent(event) {
64
+ return sessionTreeMarkerOf(event) !== void 0;
65
+ }
66
+
67
+ // packages/extensions/pi-agent-session-tree/src/session-event-adapter.ts
68
+ function toolResultOf(event) {
69
+ if (event.type !== "tool/result") return void 0;
70
+ const block = event.data.message.content.find((part) => part.type === "tool-result");
71
+ if (block?.type !== "tool-result") return void 0;
72
+ const failure = event.data.error;
73
+ const isError = block.isError === true || failure !== void 0;
74
+ return {
75
+ callId: String(block.toolCallId),
76
+ text: textOf(block.content),
77
+ isError,
78
+ ...isError ? { error: failure !== void 0 ? `${failure.name}: ${failure.code}` : "tool call failed" } : {}
79
+ };
80
+ }
81
+ function attachToolResult(node, result, resultEventSeq) {
82
+ const alreadyMerged = node.content?.some((part) => part.type === "tool_result" && part.toolCallId === result.callId) === true;
83
+ if (alreadyMerged) return node;
84
+ const resultPart = {
85
+ type: "tool_result",
86
+ toolCallId: result.callId,
87
+ content: result.text,
88
+ ...result.isError ? { isError: true } : {}
89
+ };
90
+ const base = node.summary.replace(/\.{3}$/u, "").trim();
91
+ const suffix = result.text.trim() === "" ? result.error ?? "" : result.text;
92
+ return {
93
+ ...node,
94
+ summary: summarize(suffix === "" ? base : `${base} \u2192 ${suffix}`),
95
+ message: { role: "tool", content: result.text, toolCallId: result.callId },
96
+ content: [...node.content ?? [], resultPart],
97
+ ...result.error === void 0 ? {} : { error: result.error },
98
+ metadata: {
99
+ ...node.metadata,
100
+ ...resultEventSeq === void 0 ? {} : { toolResultEventSeq: resultEventSeq }
101
+ }
102
+ };
103
+ }
104
+ function sessionEventsToTreeNodes(events, initialParentId = null) {
105
+ const nodes = [];
106
+ let parentId = initialParentId;
107
+ const pendingToolCalls = /* @__PURE__ */ new Map();
108
+ for (const event of events) {
109
+ if (event.type === "tool/result") {
110
+ const result = toolResultOf(event);
111
+ if (result !== void 0) {
112
+ const pending = pendingToolCalls.get(result.callId);
113
+ if (pending !== void 0) {
114
+ const index = nodes.indexOf(pending);
115
+ if (index >= 0) nodes[index] = attachToolResult(pending, result, event.seq);
116
+ pendingToolCalls.delete(result.callId);
117
+ continue;
118
+ }
119
+ }
120
+ }
121
+ const projected = projectEvent(event, parentId);
122
+ if (projected === void 0) continue;
123
+ nodes.push(projected);
124
+ parentId = projected.nodeId;
125
+ if (event.type === "tool/call") pendingToolCalls.set(String(event.data.callId), projected);
126
+ }
127
+ return nodes;
128
+ }
129
+ function projectEvent(event, parentId) {
130
+ if (isSessionTreeRestoreEvent(event)) return void 0;
131
+ let message;
132
+ let content;
133
+ let type = "custom";
134
+ let summary = event.type;
135
+ let usage;
136
+ let model;
137
+ let error;
138
+ let metadata = {
139
+ sessionEventType: event.type,
140
+ sessionEventSeq: event.seq,
141
+ sourceEventSeq: event.seq
142
+ };
143
+ if (event.type === "session-tree/node") {
144
+ return { ...event.data.node, metadata: { ...event.data.node.metadata, sessionEventType: event.type, sessionEventSeq: event.seq, sourceEventSeq: event.seq } };
145
+ } else if (event.type === "session-tree/snapshot") {
146
+ return void 0;
147
+ } else if (event.type === "user/message") {
148
+ type = "message";
149
+ message = { role: "user", content: textOf(event.data.content) };
150
+ content = partsOf(event.data.content);
151
+ summary = message.content;
152
+ } else if (event.type === "assistant/message") {
153
+ type = "message";
154
+ message = { role: "assistant", content: textOf(event.data.message.content) };
155
+ content = partsOf(event.data.message.content);
156
+ summary = message.content;
157
+ if (event.data.usage !== void 0) usage = event.data.usage;
158
+ if (event.data.interrupted === true) metadata = { ...metadata, interrupted: true };
159
+ } else if (event.type === "tool/call") {
160
+ type = "tool_call";
161
+ const args = parseJson(event.data.arguments);
162
+ content = [{ type: "tool_call", id: String(event.data.callId), name: event.data.name, arguments: args }];
163
+ summary = `${event.data.name}(${event.data.arguments})`;
164
+ } else if (event.type === "request/context") {
165
+ type = "model_change";
166
+ model = event.data.model;
167
+ summary = `model: ${event.data.provider}/${event.data.model}`;
168
+ content = [{ type: "text", text: summary }];
169
+ } else if (event.type === "tool/result") {
170
+ const result = toolResultOf(event);
171
+ if (result === void 0) return void 0;
172
+ type = "tool_result";
173
+ message = { role: "tool", content: result.text, toolCallId: result.callId };
174
+ content = [{ type: "tool_result", toolCallId: result.callId, content: result.text, ...result.isError ? { isError: true } : {} }];
175
+ summary = result.text;
176
+ error = result.error;
177
+ } else {
178
+ return void 0;
179
+ }
180
+ const surfaceOp = event.surfaceOp;
181
+ if (typeof surfaceOp === "object" && surfaceOp !== null && surfaceOp.op === "replace") {
182
+ const replacement = surfaceOp;
183
+ const sourceEventSeqs = event.sourceEventSeqs;
184
+ type = "compaction";
185
+ metadata = {
186
+ ...metadata,
187
+ surfaceReplacement: true,
188
+ ...typeof replacement.start === "number" ? { replaceStart: replacement.start } : {},
189
+ ...typeof replacement.end === "number" ? { replaceEnd: replacement.end } : {},
190
+ sourceEventSeqs: Array.isArray(sourceEventSeqs) ? sourceEventSeqs.filter((seq) => typeof seq === "number") : []
191
+ };
192
+ summary = `compaction: ${summary}`;
193
+ }
194
+ return {
195
+ nodeId: `session-event-${event.seq}`,
196
+ parentId,
197
+ forkCount: 0,
198
+ type,
199
+ branch: "main",
200
+ summary: summarize(summary),
201
+ createdAt: new Date(event.time).toISOString(),
202
+ ...message === void 0 ? {} : { message },
203
+ ...content === void 0 ? {} : { content },
204
+ ...model === void 0 ? {} : { model },
205
+ ...usage === void 0 ? {} : { usage },
206
+ ...error === void 0 ? {} : { error },
207
+ metadata
208
+ };
209
+ }
210
+ function textOf(value) {
211
+ if (typeof value === "string") return value;
212
+ if (Array.isArray(value)) return value.map((item) => typeof item === "object" && item !== null && "text" in item ? String(item.text ?? "") : "").join("");
213
+ return String(value ?? "");
214
+ }
215
+ function partsOf(value) {
216
+ if (!Array.isArray(value)) return [{ type: "text", text: textOf(value) }];
217
+ const parts = [];
218
+ for (const item of value) {
219
+ if (typeof item !== "object" || item === null || !("type" in item)) continue;
220
+ const block = item;
221
+ if (block.type === "text" && typeof block.text === "string") parts.push({ type: "text", text: block.text });
222
+ else if (block.type === "reasoning" && typeof block.text === "string") parts.push({ type: "reasoning", text: block.text });
223
+ else if (block.type === "tool-call" && typeof block.id === "string" && typeof block.name === "string" && typeof block.arguments === "string") parts.push({ type: "tool_call", id: block.id, name: block.name, arguments: parseJson(block.arguments) });
224
+ else if (block.type === "tool-result" && typeof block.toolCallId === "string") parts.push({ type: "tool_result", toolCallId: block.toolCallId, content: textOf(block.content), ...typeof block.isError === "boolean" ? { isError: block.isError } : {} });
225
+ }
226
+ return parts;
227
+ }
228
+ function parseJson(raw) {
229
+ try {
230
+ return JSON.parse(raw);
231
+ } catch {
232
+ return raw;
233
+ }
234
+ }
235
+ function summarize(value) {
236
+ const flat = value.replace(/\s+/gu, " ").trim();
237
+ return flat.length <= 300 ? flat : `${flat.slice(0, 297)}...`;
238
+ }
239
+
240
+ // packages/extensions/pi-agent-session-tree/src/session-tree.ts
241
+ var SNAPSHOT_VERSION = 1;
242
+ var fail = (code, message) => ({ ok: false, error: { code, message } });
243
+ var clone = (value) => JSON.parse(JSON.stringify(value));
244
+ function nodeId() {
245
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
246
+ }
247
+ var SessionTree = class {
248
+ /**
249
+ * Create an empty tree, or restore one from a versioned snapshot.
250
+ * @param sessionId - owning session identity.
251
+ * @param snapshot - optional durable snapshot to restore.
252
+ * @throws Error when the snapshot is malformed (version, ownership, cursor).
253
+ */
254
+ constructor(sessionId, snapshot) {
255
+ this.sessionId = sessionId;
256
+ if (snapshot === void 0) return;
257
+ if (!isSnapshot(snapshot, sessionId)) throw new Error("invalid session tree snapshot");
258
+ const migrated = foldLegacyToolResultPairs(snapshot);
259
+ for (const node of migrated.nodes) this.nodesById.set(node.nodeId, clone(node));
260
+ this.cursorId = migrated.cursor;
261
+ this.selectedNodeId = migrated.selectedNodeId ?? null;
262
+ if (typeof migrated.nativeEventSeq === "number") this.syncedSessionEventSeq = migrated.nativeEventSeq;
263
+ if (typeof migrated.activeBranch === "string") this.activeBranchName = migrated.activeBranch;
264
+ if (migrated.branchHeads !== void 0) {
265
+ for (const [name, head] of Object.entries(migrated.branchHeads)) {
266
+ if (this.nodesById.has(head)) this.branchHeads.set(name, head);
267
+ }
268
+ }
269
+ if (this.cursorId !== null && this.branchHeads.size === 0) {
270
+ this.branchHeads.set(this.activeBranchName, this.cursorId);
271
+ }
272
+ }
273
+ sessionId;
274
+ nodesById = /* @__PURE__ */ new Map();
275
+ cursorId = null;
276
+ selectedNodeId = null;
277
+ activeBranchName = "main";
278
+ branchHeads = /* @__PURE__ */ new Map();
279
+ syncedSessionEventSeq = -1;
280
+ /** Mint a node id guaranteed absent from this tree (collision-safe). */
281
+ mintNodeId() {
282
+ let id = nodeId();
283
+ while (this.nodesById.has(id)) id = nodeId();
284
+ return id;
285
+ }
286
+ /** @returns the current cursor node id, or null before the first append. */
287
+ get cursor() {
288
+ return this.cursorId;
289
+ }
290
+ /** Explicit node bound to context-aware UI commands. */
291
+ get selectedNode() {
292
+ return this.selectedNodeId;
293
+ }
294
+ /** Bind /fork and /clone to an existing node without changing cursor semantics. */
295
+ select(nodeId2) {
296
+ if (!this.nodesById.has(nodeId2)) return fail("NODE_NOT_FOUND", `node '${nodeId2}' was not found`);
297
+ this.selectedNodeId = nodeId2;
298
+ return { ok: true, value: { nodeId: nodeId2 } };
299
+ }
300
+ /** @returns the branch label the next append joins. */
301
+ get activeBranch() {
302
+ return this.activeBranchName;
303
+ }
304
+ /**
305
+ * Append one new node as a child of the cursor. The cursor's parent chain
306
+ * is never touched; the new node joins the active branch unless one is
307
+ * named. Historical nodes stay byte-identical.
308
+ * @param message - standard LLM message to carry.
309
+ * @param options - optional branch override, summary, and JSON extras.
310
+ * @returns the created node, or a standard error for invalid input.
311
+ */
312
+ /** Capture mutable projection state so an external durable write can commit atomically. */
313
+ checkpoint() {
314
+ return this.snapshot();
315
+ }
316
+ /** Roll back to a previously captured checkpoint after a durable append fails. */
317
+ rollback(snapshot) {
318
+ if (!isSnapshot(snapshot, this.sessionId)) return fail("INVALID_SNAPSHOT", "checkpoint does not belong to this session");
319
+ this.nodesById.clear();
320
+ for (const node of snapshot.nodes) this.nodesById.set(node.nodeId, clone(node));
321
+ this.cursorId = snapshot.cursor;
322
+ this.selectedNodeId = snapshot.selectedNodeId ?? null;
323
+ this.activeBranchName = snapshot.activeBranch;
324
+ this.branchHeads.clear();
325
+ for (const [name, head] of Object.entries(snapshot.branchHeads ?? {})) this.branchHeads.set(name, head);
326
+ this.syncedSessionEventSeq = snapshot.nativeEventSeq ?? -1;
327
+ return { ok: true, value: void 0 };
328
+ }
329
+ append(message, options = {}) {
330
+ if (!isMessage(message)) return fail("INVALID_ARGUMENT", "message.role and message.content are required");
331
+ const branch = options.branch === void 0 ? this.activeBranchName : options.branch.trim();
332
+ if (branch.length === 0) return fail("INVALID_ARGUMENT", "branch must not be empty");
333
+ const node = {
334
+ nodeId: this.mintNodeId(),
335
+ parentId: this.cursorId,
336
+ forkCount: 0,
337
+ type: "message",
338
+ branch,
339
+ summary: options.summary?.trim() || summarize2(message.content),
340
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
341
+ message: clone(message),
342
+ content: clone(options.content ?? [{ type: "text", text: message.content }]),
343
+ ...options.model === void 0 ? {} : { model: options.model },
344
+ ...options.usage === void 0 ? {} : { usage: clone(options.usage) },
345
+ ...options.cost === void 0 ? {} : { cost: options.cost },
346
+ ...options.error === void 0 ? {} : { error: options.error },
347
+ ...options.metadata === void 0 ? {} : { metadata: clone(options.metadata) }
348
+ };
349
+ this.nodesById.set(node.nodeId, node);
350
+ this.cursorId = node.nodeId;
351
+ this.activeBranchName = branch;
352
+ this.branchHeads.set(branch, node.nodeId);
353
+ return { ok: true, value: clone(node) };
354
+ }
355
+ /**
356
+ * Move the cursor to an existing node (root-to-node path replay). Old
357
+ * branches remain intact; the next append forks from the target.
358
+ * @param target - target node id, or null to reset before any node.
359
+ * @returns the new cursor and the reconstructed root-to-cursor messages.
360
+ */
361
+ jump(target) {
362
+ if (target !== null && !this.nodesById.has(target)) {
363
+ return fail("NODE_NOT_FOUND", `node '${target}' was not found`);
364
+ }
365
+ this.cursorId = target;
366
+ const node = target === null ? void 0 : this.nodesById.get(target);
367
+ this.activeBranchName = node?.branch ?? this.activeBranchName;
368
+ return { ok: true, value: { cursor: this.cursorId, messages: this.messagesFrom(target) } };
369
+ }
370
+ /**
371
+ * Fork the active cursor from a historical node. This is the Pi `/fork`
372
+ * primitive: old nodes remain intact and the next tree append becomes a child.
373
+ * The owning service selects the matching Harness model surface path.
374
+ */
375
+ fork(target, branch = "fork") {
376
+ const result = this.branch(target, branch);
377
+ if (!result.ok) return fail(result.error.code, result.error.message);
378
+ const count = this.directChildren(target).length;
379
+ return { ok: true, value: { cursor: target, branch: result.value.branch, forkCount: count } };
380
+ }
381
+ /**
382
+ * Set the branch label the next append joins and park the cursor at the
383
+ * chosen node, without appending anything.
384
+ * @param target - node to branch from (must exist).
385
+ * @param branch - new branch label (non-empty).
386
+ * @returns the new cursor and branch.
387
+ */
388
+ branch(target, branch) {
389
+ if (!this.nodesById.has(target)) return fail("NODE_NOT_FOUND", `node '${target}' was not found`);
390
+ const trimmed = branch.trim();
391
+ if (trimmed.length === 0) return fail("INVALID_ARGUMENT", "branch must not be empty");
392
+ this.cursorId = target;
393
+ this.activeBranchName = trimmed;
394
+ this.branchHeads.set(trimmed, target);
395
+ return { ok: true, value: { cursor: target, branch: trimmed } };
396
+ }
397
+ /**
398
+ * Branch with a summary of the abandoned path: park the cursor at `target`
399
+ * (like {@link branch}), then append a summary node whose parent is the
400
+ * target. The abandoned branch's nodes stay untouched.
401
+ * @param target - node to branch from.
402
+ * @param summary - summary text stored on the new node.
403
+ * @returns the created summary node.
404
+ */
405
+ branchWithSummary(target, summary) {
406
+ if (!this.nodesById.has(target)) return fail("NODE_NOT_FOUND", `node '${target}' was not found`);
407
+ if (summary.trim().length === 0) return fail("INVALID_ARGUMENT", "summary must not be empty");
408
+ this.cursorId = target;
409
+ const node = {
410
+ nodeId: this.mintNodeId(),
411
+ parentId: target,
412
+ forkCount: 0,
413
+ type: "branch_summary",
414
+ branch: this.activeBranchName,
415
+ summary: summary.trim(),
416
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
417
+ content: [{ type: "text", text: summary.trim() }],
418
+ metadata: { kind: "branch_summary", from: target }
419
+ };
420
+ this.nodesById.set(node.nodeId, node);
421
+ this.cursorId = node.nodeId;
422
+ this.branchHeads.set(this.activeBranchName, node.nodeId);
423
+ return { ok: true, value: clone(node) };
424
+ }
425
+ /**
426
+ * Reconstruct the standard LLM messages array for the path root→node
427
+ * (defaults to the cursor). System/user/assistant/tool messages keep their
428
+ * roles; branch-summary nodes carry no message and stay out of the array.
429
+ * @param from - target node id, or null for the empty path.
430
+ * @returns messages in conversation order (root first).
431
+ */
432
+ messages(from = this.cursorId) {
433
+ return this.messagesFrom(from);
434
+ }
435
+ /**
436
+ * @returns all nodes in creation order (defensive copies).
437
+ */
438
+ log(fromSeq = 0) {
439
+ return this.list().slice(fromSeq).map((node, index) => ({ seq: fromSeq + index, node }));
440
+ }
441
+ replay(records) {
442
+ let expected = this.nodesById.size;
443
+ const known = new Set(this.nodesById.keys());
444
+ for (const record of records) {
445
+ if (record.seq !== expected || known.has(record.node.nodeId)) return fail("INVALID_SNAPSHOT", "session tree log sequence or node identity is invalid");
446
+ if (!isNode(record.node) || record.node.parentId !== null && !known.has(record.node.parentId)) return fail("INVALID_SNAPSHOT", "session tree log topology is invalid");
447
+ known.add(record.node.nodeId);
448
+ expected += 1;
449
+ }
450
+ for (const record of records) {
451
+ this.nodesById.set(record.node.nodeId, clone(record.node));
452
+ const nativeSeq = record.node.metadata?.sessionEventSeq;
453
+ if (typeof nativeSeq === "number") this.markSessionEventSeq(nativeSeq);
454
+ }
455
+ const last = records[records.length - 1]?.node;
456
+ if (last !== void 0) {
457
+ this.cursorId = last.nodeId;
458
+ this.activeBranchName = last.branch;
459
+ this.branchHeads.set(last.branch, last.nodeId);
460
+ }
461
+ return { ok: true, value: { applied: records.length } };
462
+ }
463
+ list() {
464
+ return clone([...this.nodesById.values()]);
465
+ }
466
+ /**
467
+ * @returns named branch pointers and their reachable nodes. Branch identity
468
+ * comes from the session-level head map; node.branch is display metadata only.
469
+ */
470
+ branches() {
471
+ return [...this.branchHeads].map(([name, headId]) => {
472
+ const nodeIds = [];
473
+ let current = headId;
474
+ while (current !== null) {
475
+ const node = this.nodesById.get(current);
476
+ if (node === void 0) break;
477
+ nodeIds.push(current);
478
+ current = node.parentId;
479
+ }
480
+ nodeIds.reverse();
481
+ return { name, headId, nodeIds };
482
+ });
483
+ }
484
+ /** Return a compact Pi-style session status projection. */
485
+ info() {
486
+ const usage = {};
487
+ let cost = 0;
488
+ for (const node of this.nodesById.values()) {
489
+ if (node.usage !== void 0) {
490
+ for (const [key, value] of Object.entries(node.usage)) {
491
+ if (typeof value === "number") usage[key] = (usage[key] ?? 0) + value;
492
+ }
493
+ }
494
+ if (typeof node.cost === "number") cost += node.cost;
495
+ }
496
+ const tokenCount = Object.entries(usage).reduce((total, [key, value]) => key.endsWith("Tokens") ? total + value : total, 0);
497
+ return {
498
+ sessionId: this.sessionId,
499
+ branchHeads: Object.fromEntries(this.branchHeads),
500
+ nodeCount: this.nodesById.size,
501
+ messageCount: [...this.nodesById.values()].filter((node) => node.message !== void 0).length,
502
+ branchCount: this.branches().length,
503
+ cursor: this.cursorId,
504
+ activeBranch: this.activeBranchName,
505
+ selectedNodeId: this.selectedNodeId,
506
+ currentPathLength: this.currentPath().length,
507
+ ...Object.keys(usage).length > 0 ? { usage } : {},
508
+ ...tokenCount !== 0 ? { tokenCount } : {},
509
+ ...cost !== 0 ? { cost } : {},
510
+ snapshotVersion: SNAPSHOT_VERSION
511
+ };
512
+ }
513
+ /** @returns the full read view served to the browser panel. */
514
+ view() {
515
+ return {
516
+ sessionId: this.sessionId,
517
+ cursor: this.cursorId,
518
+ activeBranch: this.activeBranchName,
519
+ selectedNodeId: this.selectedNodeId,
520
+ branchHeads: Object.fromEntries(this.branchHeads),
521
+ nodes: this.list(),
522
+ branches: this.branches()
523
+ };
524
+ }
525
+ /** @returns a versioned durable snapshot (defensive copy). */
526
+ snapshot() {
527
+ return clone({
528
+ version: SNAPSHOT_VERSION,
529
+ sessionId: this.sessionId,
530
+ cursor: this.cursorId,
531
+ activeBranch: this.activeBranchName,
532
+ ...this.syncedSessionEventSeq < 0 ? {} : { nativeEventSeq: this.syncedSessionEventSeq },
533
+ branchHeads: Object.fromEntries(this.branchHeads),
534
+ selectedNodeId: this.selectedNodeId,
535
+ nodes: [...this.nodesById.values()]
536
+ });
537
+ }
538
+ /** Return the current leaf's root-to-leaf node path. */
539
+ currentPath(from = this.cursorId) {
540
+ const path = [];
541
+ let current = from;
542
+ while (current !== null) {
543
+ const node = this.nodesById.get(current);
544
+ if (node === void 0) break;
545
+ path.push(clone(node));
546
+ current = node.parentId;
547
+ }
548
+ return path.reverse();
549
+ }
550
+ /** Return the greatest native Session event seq already projected into this tree. */
551
+ lastSessionEventSeq() {
552
+ return this.syncedSessionEventSeq;
553
+ }
554
+ /** Advance the native Session event watermark after a successful sync/write. */
555
+ markSessionEventSeq(seq) {
556
+ if (Number.isSafeInteger(seq) && seq > this.syncedSessionEventSeq) this.syncedSessionEventSeq = seq;
557
+ }
558
+ /** Rewind the watermark when a persisted native tail is shorter than the sidecar expected. */
559
+ limitSessionEventSeq(seq) {
560
+ if (Number.isSafeInteger(seq) && seq < this.syncedSessionEventSeq) this.syncedSessionEventSeq = seq;
561
+ }
562
+ /** @returns true when a node with this id exists. */
563
+ has(nodeId2) {
564
+ return this.nodesById.has(nodeId2);
565
+ }
566
+ /**
567
+ * Find the dedicated tool-call node carrying this call id. Assistant
568
+ * message nodes may echo the same tool-call block in their content, but a
569
+ * tool result must never fold into them: only `type === 'tool_call'` nodes
570
+ * represent the interaction entry the panel renders.
571
+ */
572
+ findToolCallNode(callId) {
573
+ for (const node of this.nodesById.values()) {
574
+ if (node.type !== "tool_call") continue;
575
+ if (node.content?.some((part) => part.type === "tool_call" && part.id === callId) === true) return node;
576
+ }
577
+ return void 0;
578
+ }
579
+ /**
580
+ * Replace a projected tool-call node with its result-merged form, so one
581
+ * call and its result stay one tree entry even when the result arrives in a
582
+ * later sync batch than the call. The tree is a deterministic projection of
583
+ * the durable Session log, so this derived update never rewrites history in
584
+ * the log itself and replays identically from scratch.
585
+ */
586
+ attachToolResult(callId, merged) {
587
+ const existing = this.findToolCallNode(callId);
588
+ if (existing === void 0) return fail("NODE_NOT_FOUND", `no tool-call node for call '${callId}'`);
589
+ if (merged.nodeId !== existing.nodeId) return fail("INVALID_ARGUMENT", "merged tool node does not match the stored tool-call node");
590
+ this.nodesById.set(existing.nodeId, clone(merged));
591
+ return { ok: true, value: clone(merged) };
592
+ }
593
+ /** @returns direct child ids, used to expose the derived fork count. */
594
+ directChildren(parentId) {
595
+ return [...this.nodesById.values()].filter((node) => node.parentId === parentId).map((node) => node.nodeId);
596
+ }
597
+ messagesFrom(from) {
598
+ const path = [];
599
+ let current = from;
600
+ while (current !== null) {
601
+ const node = this.nodesById.get(current);
602
+ if (node === void 0) break;
603
+ if (node.message !== void 0) path.push(clone(node.message));
604
+ current = node.parentId;
605
+ }
606
+ return path.reverse();
607
+ }
608
+ };
609
+ var SessionTreeStore = class {
610
+ trees = /* @__PURE__ */ new Map();
611
+ /** Look up a tree without creating one. */
612
+ get(sessionId) {
613
+ return this.trees.get(sessionId);
614
+ }
615
+ /** Look up a tree, or return the standard SESSION_NOT_FOUND error. */
616
+ require(sessionId) {
617
+ const tree = this.trees.get(sessionId);
618
+ return tree === void 0 ? fail("SESSION_NOT_FOUND", `session '${sessionId}' was not found`) : { ok: true, value: tree };
619
+ }
620
+ /** Create a new tree session. Existing ids are never silently reset. */
621
+ create(sessionId) {
622
+ if (this.trees.has(sessionId)) throw new Error(`session '${sessionId}' already exists`);
623
+ const tree = new SessionTree(sessionId);
624
+ this.trees.set(sessionId, tree);
625
+ return tree;
626
+ }
627
+ /** Clone the active branch into an independent session with fresh node ids. */
628
+ clone(sourceSessionId, targetSessionId) {
629
+ if (this.trees.has(targetSessionId)) return fail("SESSION_ALREADY_EXISTS", `session '${targetSessionId}' already exists`);
630
+ const source = this.trees.get(sourceSessionId);
631
+ if (source === void 0) return fail("SESSION_NOT_FOUND", `session '${sourceSessionId}' was not found`);
632
+ const snapshot = source.snapshot();
633
+ const byId = new Map(snapshot.nodes.map((node) => [node.nodeId, node]));
634
+ const activeIds = /* @__PURE__ */ new Set();
635
+ let current = snapshot.cursor;
636
+ while (current !== null) {
637
+ activeIds.add(current);
638
+ current = byId.get(current)?.parentId ?? null;
639
+ }
640
+ const activeNodes = snapshot.nodes.filter((node) => activeIds.has(node.nodeId));
641
+ const ids = /* @__PURE__ */ new Map();
642
+ for (const node of activeNodes) ids.set(node.nodeId, nodeId());
643
+ const cloned = {
644
+ ...snapshot,
645
+ sessionId: targetSessionId,
646
+ cursor: snapshot.cursor === null ? null : ids.get(snapshot.cursor) ?? null,
647
+ branchHeads: Object.fromEntries(Object.entries(snapshot.branchHeads ?? {}).map(([name, head]) => [name, ids.get(head)]).filter((entry) => entry[1] !== void 0)),
648
+ nodes: activeNodes.map((node) => ({
649
+ ...node,
650
+ nodeId: ids.get(node.nodeId) ?? nodeId(),
651
+ parentId: node.parentId === null ? null : ids.get(node.parentId) ?? null
652
+ }))
653
+ };
654
+ this.trees.set(targetSessionId, new SessionTree(targetSessionId, cloned));
655
+ return { ok: true, value: { sessionId: targetSessionId } };
656
+ }
657
+ /** Replace one session's tree from a previously validated candidate. */
658
+ replace(sessionId, tree) {
659
+ if (tree.sessionId !== sessionId) throw new Error("tree does not belong to session");
660
+ this.trees.set(sessionId, tree);
661
+ }
662
+ /** Replace one session's tree from a snapshot. */
663
+ load(snapshot) {
664
+ try {
665
+ const tree = new SessionTree(snapshot.sessionId, snapshot);
666
+ this.trees.set(tree.sessionId, tree);
667
+ return { ok: true, value: { sessionId: tree.sessionId } };
668
+ } catch (error) {
669
+ return fail("INVALID_SNAPSHOT", error instanceof Error ? error.message : "invalid snapshot");
670
+ }
671
+ }
672
+ /** @returns all session ids with a live tree, in creation order. */
673
+ list() {
674
+ return [...this.trees.keys()];
675
+ }
676
+ };
677
+ function isMessage(value) {
678
+ if (typeof value !== "object" || value === null) return false;
679
+ const record = value;
680
+ return typeof record.role === "string" && ["system", "user", "assistant", "tool"].includes(record.role) && typeof record.content === "string";
681
+ }
682
+ function isSnapshot(value, sessionId) {
683
+ if (typeof value !== "object" || value === null) return false;
684
+ const record = value;
685
+ if (record.version !== SNAPSHOT_VERSION || record.sessionId !== sessionId) return false;
686
+ const nodes = record.nodes;
687
+ if (!Array.isArray(nodes) || nodes.some((node) => !isNode(node))) return false;
688
+ if (!hasValidTopology(nodes)) return false;
689
+ const cursor = record.cursor;
690
+ if (cursor !== null && typeof cursor !== "string") return false;
691
+ if (typeof cursor === "string" && !nodes.some((node) => node.nodeId === cursor)) return false;
692
+ if (record.selectedNodeId !== void 0 && record.selectedNodeId !== null && (typeof record.selectedNodeId !== "string" || !nodes.some((node) => node.nodeId === record.selectedNodeId))) return false;
693
+ if (record.nativeEventSeq !== void 0 && (typeof record.nativeEventSeq !== "number" || !Number.isSafeInteger(record.nativeEventSeq) || record.nativeEventSeq < 0)) return false;
694
+ if (record.branchHeads !== void 0 && !isJsonRecord(record.branchHeads)) return false;
695
+ if (record.branchHeads !== void 0 && Object.entries(record.branchHeads).some(([name, head]) => name.length === 0 || typeof head !== "string" || !nodes.some((node) => node.nodeId === head))) return false;
696
+ if (typeof record.activeBranch !== "string" || record.activeBranch.length === 0) return false;
697
+ if (record.branchHeads !== void 0 && record.branchHeads[record.activeBranch] === void 0 && cursor !== null) return false;
698
+ return true;
699
+ }
700
+ function isNode(value) {
701
+ if (typeof value !== "object" || value === null) return false;
702
+ const record = value;
703
+ if (typeof record.nodeId !== "string" || record.parentId !== null && typeof record.parentId !== "string" || typeof record.branch !== "string" || typeof record.summary !== "string" || typeof record.createdAt !== "string") return false;
704
+ if (record.type !== void 0 && !["message", "tool_call", "tool_result", "model_change", "compaction", "branch_summary", "custom"].includes(record.type)) return false;
705
+ if (record.forkCount !== void 0 && (typeof record.forkCount !== "number" || !Number.isInteger(record.forkCount) || record.forkCount < 0)) return false;
706
+ if (record.message !== void 0 && !isMessage(record.message)) return false;
707
+ if (record.content !== void 0 && (!Array.isArray(record.content) || record.content.some((part) => !isContentPart(part)))) return false;
708
+ if (record.cost !== void 0 && (typeof record.cost !== "number" || !Number.isFinite(record.cost))) return false;
709
+ if (record.usage !== void 0 && !isJsonRecord(record.usage)) return false;
710
+ return record.error === void 0 || typeof record.error === "string";
711
+ }
712
+ function isContentPart(value) {
713
+ if (typeof value !== "object" || value === null) return false;
714
+ const record = value;
715
+ if (record.type === "text" || record.type === "reasoning") return typeof record.text === "string";
716
+ if (record.type === "tool_call") return typeof record.id === "string" && typeof record.name === "string" && isJsonValue(record.arguments);
717
+ if (record.type === "tool_result") return typeof record.toolCallId === "string" && typeof record.content === "string" && (record.isError === void 0 || typeof record.isError === "boolean");
718
+ return false;
719
+ }
720
+ function isJsonRecord(value) {
721
+ return typeof value === "object" && value !== null && !Array.isArray(value) && isJsonValue(value);
722
+ }
723
+ function isJsonValue(value) {
724
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
725
+ if (typeof value === "number") return Number.isFinite(value);
726
+ if (Array.isArray(value)) return value.every(isJsonValue);
727
+ if (typeof value === "object") return Object.values(value).every(isJsonValue);
728
+ return false;
729
+ }
730
+ function hasValidTopology(nodes) {
731
+ const ids = new Set(nodes.map((node) => node.nodeId));
732
+ if (ids.size !== nodes.length) return false;
733
+ const parentOf = new Map(nodes.map((node) => [node.nodeId, node.parentId]));
734
+ for (const node of nodes) {
735
+ if (node.parentId !== null && !ids.has(node.parentId)) return false;
736
+ }
737
+ for (const node of nodes) {
738
+ let steps = 0;
739
+ let current = node.nodeId;
740
+ while (current !== null) {
741
+ if (steps > nodes.length) return false;
742
+ current = parentOf.get(current) ?? null;
743
+ steps += 1;
744
+ }
745
+ }
746
+ return true;
747
+ }
748
+ function foldLegacyToolResultPairs(snapshot) {
749
+ const byId = new Map(snapshot.nodes.map((node) => [node.nodeId, node]));
750
+ const mergedByParent = /* @__PURE__ */ new Map();
751
+ const removedToParent = /* @__PURE__ */ new Map();
752
+ for (const node of snapshot.nodes) {
753
+ if (node.type !== "tool_result" || node.parentId === null) continue;
754
+ const parent = byId.get(node.parentId);
755
+ if (parent === void 0 || parent.type !== "tool_call" || mergedByParent.has(parent.nodeId)) continue;
756
+ const resultPart = node.content?.find((part) => part.type === "tool_result");
757
+ if (resultPart === void 0) continue;
758
+ const matchesCall = parent.content?.some((part) => part.type === "tool_call" && part.id === resultPart.toolCallId) === true;
759
+ if (!matchesCall) continue;
760
+ const resultSeq = typeof node.metadata?.sessionEventSeq === "number" ? node.metadata.sessionEventSeq : void 0;
761
+ mergedByParent.set(parent.nodeId, attachToolResult(parent, {
762
+ callId: resultPart.toolCallId,
763
+ text: resultPart.content,
764
+ isError: resultPart.isError === true || node.error !== void 0,
765
+ ...node.error === void 0 ? {} : { error: node.error }
766
+ }, resultSeq));
767
+ removedToParent.set(node.nodeId, parent.nodeId);
768
+ }
769
+ if (mergedByParent.size === 0) return snapshot;
770
+ const reparent = (id) => id === null ? null : removedToParent.get(id) ?? id;
771
+ const nodes = snapshot.nodes.filter((node) => !removedToParent.has(node.nodeId)).map((node) => {
772
+ const merged = mergedByParent.get(node.nodeId);
773
+ return merged === void 0 ? { ...node, parentId: reparent(node.parentId) } : { ...merged, parentId: reparent(merged.parentId) };
774
+ });
775
+ return {
776
+ ...snapshot,
777
+ cursor: reparent(snapshot.cursor),
778
+ selectedNodeId: snapshot.selectedNodeId === void 0 ? void 0 : reparent(snapshot.selectedNodeId),
779
+ ...snapshot.branchHeads === void 0 ? {} : { branchHeads: Object.fromEntries(Object.entries(snapshot.branchHeads).map(([name, head]) => [name, reparent(head) ?? head])) },
780
+ nodes
781
+ };
782
+ }
783
+ function summarize2(content) {
784
+ const flat = content.replace(/\s+/gu, " ").trim();
785
+ return flat.length <= 300 ? flat : `${flat.slice(0, 297)}...`;
786
+ }
787
+ var sessionTreeStore = new SessionTreeStore();
788
+
789
+ // packages/extensions/pi-agent-session-tree/src/session-tree-sidecar.ts
790
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
791
+ import { dirname, join } from "node:path";
792
+ var SIDECAR_VERSION = 1;
793
+ function defaultSidecarRoot() {
794
+ const home = process.env.DSH_HOME ?? process.env.HOME ?? ".";
795
+ return join(home, "storages", "session-tree");
796
+ }
797
+ function sidecarFilename(sessionId) {
798
+ return `${sessionId.replaceAll(/[^A-Za-z0-9._-]/g, "_")}.json`;
799
+ }
800
+ var SessionTreeSidecar = class {
801
+ constructor(root = defaultSidecarRoot()) {
802
+ this.root = root;
803
+ }
804
+ root;
805
+ /** Read and validate the latest snapshot for one session. */
806
+ load(sessionId) {
807
+ const path = join(this.root, sidecarFilename(sessionId));
808
+ if (!existsSync(path)) return void 0;
809
+ try {
810
+ const record = JSON.parse(readFileSync(path, "utf8"));
811
+ if (record.version !== SIDECAR_VERSION || record.sessionId !== sessionId || typeof record.savedAt !== "number" || record.snapshot === void 0) {
812
+ return void 0;
813
+ }
814
+ return new SessionTree(sessionId, record.snapshot);
815
+ } catch {
816
+ return void 0;
817
+ }
818
+ }
819
+ /**
820
+ * Atomically persist one tree. I/O errors are deliberately contained:
821
+ * the native Session log remains the source of truth and can rebuild a
822
+ * linear projection even when the sidecar directory is unavailable.
823
+ */
824
+ save(sessionId, tree) {
825
+ const record = {
826
+ version: SIDECAR_VERSION,
827
+ sessionId,
828
+ savedAt: Date.now(),
829
+ snapshot: tree.snapshot()
830
+ };
831
+ const path = join(this.root, sidecarFilename(sessionId));
832
+ const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
833
+ try {
834
+ mkdirSync(dirname(path), { recursive: true });
835
+ writeFileSync(temporary, JSON.stringify(record), { encoding: "utf8", mode: 384 });
836
+ renameSync(temporary, path);
837
+ return true;
838
+ } catch {
839
+ try {
840
+ rmSync(temporary, { force: true });
841
+ } catch {
842
+ }
843
+ return false;
844
+ }
845
+ }
846
+ };
847
+ var activeSidecar = new SessionTreeSidecar();
848
+ function getSessionTreeSidecar() {
849
+ return activeSidecar;
850
+ }
851
+ function setSessionTreeSidecar(sidecar) {
852
+ activeSidecar = sidecar;
853
+ }
854
+ function persistSessionTree(tree) {
855
+ return activeSidecar.save(tree.sessionId, tree);
856
+ }
857
+
858
+ // packages/extensions/pi-agent-session-tree/src/index.ts
859
+ function supportsSelectedMessageSurface(session) {
860
+ const candidate = session;
861
+ return typeof candidate.selectMessageSurface === "function" && typeof candidate.messageSurfaceNodes === "function";
862
+ }
863
+ var projectionFallbackSessions = /* @__PURE__ */ new WeakSet();
864
+ var surfaceNoticesSent = /* @__PURE__ */ new WeakMap();
865
+ function sessionTreeSurfaceMode(session) {
866
+ if (supportsSelectedMessageSurface(session)) return "native";
867
+ return projectionFallbackSessions.has(session) ? "projection" : "stock";
868
+ }
869
+ function noticeSurfaceMode(session, mode, detail) {
870
+ let sent = surfaceNoticesSent.get(session);
871
+ if (sent === void 0) {
872
+ sent = /* @__PURE__ */ new Set();
873
+ surfaceNoticesSent.set(session, sent);
874
+ }
875
+ if (sent.has(mode)) return;
876
+ sent.add(mode);
877
+ console.warn(`[session-tree] ${String(session.id)}: ${detail}`);
878
+ }
879
+ function markProjectionFallback(session, cause) {
880
+ if (projectionFallbackSessions.has(session)) return;
881
+ projectionFallbackSessions.add(session);
882
+ const reason = cause instanceof Error ? cause.message : String(cause);
883
+ noticeSurfaceMode(
884
+ session,
885
+ "projection",
886
+ `projection mode: the live message surface is not writable here (${reason}); jump/fork move the tree cursor and panel only, and the next model turn keeps the canonical history. Apply dev/session-branch-surface.patch (or upgrade Harness) for native branch switching.`
887
+ );
888
+ }
889
+ function supportsDurableSessionTreeEvents(session) {
890
+ return supportsSelectedMessageSurface(session);
891
+ }
892
+ function appendSessionTreeEvent(session, type, data) {
893
+ if (!supportsDurableSessionTreeEvents(session)) return void 0;
894
+ return session.append(type, data);
895
+ }
896
+ function syncSessionTree(agent) {
897
+ const sessionId = agent.session.id;
898
+ const existing = sessionTreeStore.get(sessionId);
899
+ const restored = existing === void 0 ? getSessionTreeSidecar().load(sessionId) : void 0;
900
+ let tree = existing === void 0 ? restored ?? new SessionTree(sessionId) : new SessionTree(sessionId, existing.snapshot());
901
+ const actualLatestSeq = agent.session.events.at(-1)?.seq ?? -1;
902
+ tree.limitSessionEventSeq(actualLatestSeq);
903
+ const lastSeq = tree.lastSessionEventSeq();
904
+ const freshEvents = agent.session.events.filter((event) => event.seq > lastSeq);
905
+ let nativeParentId = tree.cursor;
906
+ for (const event of freshEvents) {
907
+ if (event.type === "session-tree/snapshot") {
908
+ if (event.data.snapshot.sessionId !== sessionId) throw new Error("INVALID_SNAPSHOT: snapshot session does not match the owning Session");
909
+ try {
910
+ tree = new SessionTree(sessionId, event.data.snapshot);
911
+ } catch (error) {
912
+ throw new Error(`INVALID_SNAPSHOT: ${error instanceof Error ? error.message : "invalid snapshot"}`);
913
+ }
914
+ tree.markSessionEventSeq(event.seq);
915
+ nativeParentId = tree.cursor;
916
+ continue;
917
+ }
918
+ if (event.type === "session-tree/cursor") {
919
+ const moved = tree.jump(event.data.nodeId);
920
+ if (!moved.ok) throw new Error(`${moved.error.code}: ${moved.error.message}`);
921
+ nativeParentId = event.data.nodeId;
922
+ continue;
923
+ }
924
+ if (event.type === "session-tree/branch") {
925
+ const branched = tree.branch(event.data.nodeId, event.data.branch);
926
+ if (!branched.ok) throw new Error(`${branched.error.code}: ${branched.error.message}`);
927
+ nativeParentId = event.data.nodeId;
928
+ continue;
929
+ }
930
+ if (event.type === "session-tree/selection") {
931
+ const selected = tree.select(event.data.nodeId);
932
+ if (!selected.ok) throw new Error(`${selected.error.code}: ${selected.error.message}`);
933
+ continue;
934
+ }
935
+ const isExplicitTreeNode = event.type === "session-tree/node";
936
+ const isTreeMetadataEvent = event.type === "tool/call" || event.type === "request/context";
937
+ if (!isExplicitTreeNode && !isTreeMetadataEvent && !isSurfaceEvent(event)) continue;
938
+ if (event.type === "tool/result") {
939
+ const result = toolResultOf(event);
940
+ const callNode = result === void 0 ? void 0 : tree.findToolCallNode(result.callId);
941
+ if (callNode !== void 0 && result !== void 0) {
942
+ const merged = attachToolResult(callNode, result, event.seq);
943
+ const attached = tree.attachToolResult(result.callId, merged);
944
+ if (!attached.ok) throw new Error(`${attached.error.code}: ${attached.error.message}`);
945
+ continue;
946
+ }
947
+ }
948
+ const nodes = sessionEventsToTreeNodes([event], nativeParentId);
949
+ if (nodes.length === 0) continue;
950
+ const first = nodes[0];
951
+ if (first === void 0) continue;
952
+ const projected = isExplicitTreeNode ? first : { ...first, branch: tree.activeBranch };
953
+ const restored2 = tree.replay([{ seq: tree.list().length, node: projected }]);
954
+ if (!restored2.ok) throw new Error(`${restored2.error.code}: ${restored2.error.message}`);
955
+ nativeParentId = projected.nodeId;
956
+ }
957
+ const newestSeq = freshEvents[freshEvents.length - 1]?.seq;
958
+ if (newestSeq !== void 0) tree.markSessionEventSeq(newestSeq);
959
+ sessionTreeStore.replace(sessionId, tree);
960
+ applyTreeCursorToSession(agent, tree);
961
+ persistSessionTree(tree);
962
+ return tree;
963
+ }
964
+ function selectedSurfaceSeqs(tree, session) {
965
+ const seqs = [];
966
+ for (const node of tree.currentPath()) {
967
+ const nativeSeqs = [];
968
+ const primary = node.metadata?.sessionEventSeq;
969
+ if (typeof primary === "number") nativeSeqs.push(primary);
970
+ const resultSeq = node.metadata?.toolResultEventSeq;
971
+ if (typeof resultSeq === "number") nativeSeqs.push(resultSeq);
972
+ for (const seq of nativeSeqs) {
973
+ const event = session.events[seq];
974
+ if (event !== void 0 && isSurfaceEvent(event)) seqs.push(seq);
975
+ }
976
+ }
977
+ return seqs;
978
+ }
979
+ function appendStockCursorEvent(session, tree, logSurfaceNodes) {
980
+ if (logSurfaceNodes.length === 0) return void 0;
981
+ const context = session.requestContext();
982
+ const marker = { kind: "cursor", nodeId: tree.cursor };
983
+ const data = {
984
+ turn: 0,
985
+ step: 0,
986
+ message: {
987
+ role: "assistant",
988
+ content: [],
989
+ id: `session-tree-cursor-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
990
+ source: {
991
+ kind: "model",
992
+ provider: context?.provider ?? "session-tree",
993
+ model: context?.model ?? "cursor"
994
+ }
995
+ },
996
+ treeRestore: marker
997
+ };
998
+ return session.append("assistant/message", data, {
999
+ surfaceOp: { op: "replace", start: logSurfaceNodes[0], end: logSurfaceNodes[logSurfaceNodes.length - 1] },
1000
+ sourceEventSeqs: [...logSurfaceNodes]
1001
+ });
1002
+ }
1003
+ function canonicalSurfaceNodes(session) {
1004
+ const nodes = [];
1005
+ for (const event of session.events) {
1006
+ if (!isSurfaceEvent(event)) continue;
1007
+ if (event.surfaceOp === "append") {
1008
+ nodes.push(event.seq);
1009
+ continue;
1010
+ }
1011
+ if (event.surfaceOp !== void 0 && event.surfaceOp.op === "replace") {
1012
+ const start = nodes.indexOf(event.surfaceOp.start);
1013
+ const end = nodes.indexOf(event.surfaceOp.end);
1014
+ if (start < 0 || end < start) continue;
1015
+ nodes.splice(start, end - start + 1, event.seq);
1016
+ }
1017
+ }
1018
+ return nodes;
1019
+ }
1020
+ function noticeStockMode(session) {
1021
+ noticeSurfaceMode(
1022
+ session,
1023
+ "stock",
1024
+ "stock-surface mode: this Harness lacks Session.selectMessageSurface(); branch switches use the official replace-surface emulation and the session-tree sidecar for durability"
1025
+ );
1026
+ }
1027
+ function stockSurfaceIsWritable(session) {
1028
+ const nodes = session.surface.nodes;
1029
+ return Array.isArray(nodes) && !Object.isFrozen(nodes);
1030
+ }
1031
+ function setStockSurfaceNodes(session, seqs) {
1032
+ const nodes = session.surface.nodes;
1033
+ if (!Array.isArray(nodes) || Object.isFrozen(nodes)) {
1034
+ throw new Error("stock Harness message surface is not writable in this revision");
1035
+ }
1036
+ nodes.splice(0, nodes.length, ...seqs);
1037
+ }
1038
+ function sameSurfaceNodes(left, right) {
1039
+ return left.length === right.length && left.every((seq, index) => seq === right[index]);
1040
+ }
1041
+ function normalizeSurfaceNodeSeqs(values) {
1042
+ const seqs = [];
1043
+ for (const value of values) {
1044
+ if (Array.isArray(value)) {
1045
+ for (const child of value) {
1046
+ if (Number.isSafeInteger(child)) seqs.push(child);
1047
+ }
1048
+ } else if (Number.isSafeInteger(value)) {
1049
+ seqs.push(value);
1050
+ }
1051
+ }
1052
+ return seqs;
1053
+ }
1054
+ function applyTreeCursorToSession(agent, tree) {
1055
+ const session = agent.session;
1056
+ const seqs = selectedSurfaceSeqs(tree, session);
1057
+ if (supportsSelectedMessageSurface(session)) {
1058
+ session.selectMessageSurface(seqs);
1059
+ return;
1060
+ }
1061
+ const stockSession = session;
1062
+ const currentNodes = normalizeSurfaceNodeSeqs(stockSession.surface.nodes);
1063
+ if (sameSurfaceNodes(currentNodes, seqs)) return;
1064
+ if (process.env.DSH_SESSION_TREE_DEBUG === "1") {
1065
+ console.error("[session-tree] stock surface rewrite", { sessionId: stockSession.id, currentNodes, seqs, cursor: tree.cursor });
1066
+ }
1067
+ if (!stockSurfaceIsWritable(stockSession)) {
1068
+ markProjectionFallback(stockSession, new Error("surface nodes are not a writable array"));
1069
+ persistSessionTree(tree);
1070
+ return;
1071
+ }
1072
+ try {
1073
+ noticeStockMode(stockSession);
1074
+ const logSurfaceNodes = canonicalSurfaceNodes(stockSession);
1075
+ if (!sameSurfaceNodes(currentNodes, logSurfaceNodes)) {
1076
+ setStockSurfaceNodes(stockSession, logSurfaceNodes);
1077
+ }
1078
+ const event = appendStockCursorEvent(stockSession, tree, logSurfaceNodes);
1079
+ if (event !== void 0) tree.markSessionEventSeq(event.seq);
1080
+ setStockSurfaceNodes(stockSession, seqs);
1081
+ } catch (error) {
1082
+ markProjectionFallback(stockSession, error);
1083
+ }
1084
+ persistSessionTree(tree);
1085
+ }
1086
+ var _session_dec, _fork_dec, _jump_dec, _list_dec, _a, _init;
1087
+ var SessionTreeService = class extends (_a = TypertRemoteService, _list_dec = [Remote("list")], _jump_dec = [Remote("jump")], _fork_dec = [Remote("fork")], _session_dec = [Remote("session")], _a) {
1088
+ /**
1089
+ * Register the service under `sessionTree`.
1090
+ * @param ctx - owning Cordis Context.
1091
+ */
1092
+ constructor(ctx) {
1093
+ super(ctx, "sessionTree");
1094
+ __runInitializers(_init, 5, this);
1095
+ ctx.on("agent/pre-step", ({ agent }, next) => {
1096
+ syncSessionTree(agent);
1097
+ return next();
1098
+ });
1099
+ ctx.on("session/flush", (session) => {
1100
+ const tree = sessionTreeStore.get(session.id);
1101
+ if (tree !== void 0) persistSessionTree(tree);
1102
+ });
1103
+ }
1104
+ list(agent) {
1105
+ return this.synced(agent).view();
1106
+ }
1107
+ /** Synchronize native history, falling back to the last committed tree when replay fails. */
1108
+ synced(agent) {
1109
+ try {
1110
+ return syncSessionTree(agent);
1111
+ } catch {
1112
+ const committed = sessionTreeStore.require(agent.session.id);
1113
+ if (!committed.ok) throw new Error(`${committed.error.code}: ${committed.error.message}`);
1114
+ return committed.value;
1115
+ }
1116
+ }
1117
+ jump(agent, nodeId2) {
1118
+ const tree = syncSessionTree(agent);
1119
+ const checkpoint = tree.checkpoint();
1120
+ const result = tree.jump(nodeId2);
1121
+ if (!result.ok) throw new Error(`${result.error.code}: ${result.error.message}`);
1122
+ try {
1123
+ const event = appendSessionTreeEvent(agent.session, "session-tree/cursor", { nodeId: nodeId2 });
1124
+ if (event !== void 0) tree.markSessionEventSeq(event.seq);
1125
+ if (nodeId2 !== null) {
1126
+ const selected = tree.select(nodeId2);
1127
+ if (!selected.ok) throw new Error(`${selected.error.code}: ${selected.error.message}`);
1128
+ const selection = appendSessionTreeEvent(agent.session, "session-tree/selection", { nodeId: nodeId2 });
1129
+ if (selection !== void 0) tree.markSessionEventSeq(selection.seq);
1130
+ }
1131
+ applyTreeCursorToSession(agent, tree);
1132
+ } catch (error) {
1133
+ tree.rollback(checkpoint);
1134
+ throw error;
1135
+ }
1136
+ return result.value;
1137
+ }
1138
+ fork(agent, nodeId2, branch) {
1139
+ const branchName = branch === "" ? "fork" : branch;
1140
+ const tree = syncSessionTree(agent);
1141
+ const checkpoint = tree.checkpoint();
1142
+ const result = tree.fork(nodeId2, branchName);
1143
+ if (!result.ok) throw new Error(`${result.error.code}: ${result.error.message}`);
1144
+ try {
1145
+ const selected = tree.select(nodeId2);
1146
+ if (!selected.ok) throw new Error(`${selected.error.code}: ${selected.error.message}`);
1147
+ const event = appendSessionTreeEvent(agent.session, "session-tree/branch", { nodeId: nodeId2, branch: result.value.branch });
1148
+ if (event !== void 0) tree.markSessionEventSeq(event.seq);
1149
+ const selection = appendSessionTreeEvent(agent.session, "session-tree/selection", { nodeId: nodeId2 });
1150
+ if (selection !== void 0) tree.markSessionEventSeq(selection.seq);
1151
+ applyTreeCursorToSession(agent, tree);
1152
+ } catch (error) {
1153
+ tree.rollback(checkpoint);
1154
+ throw error;
1155
+ }
1156
+ return result.value;
1157
+ }
1158
+ session(agent) {
1159
+ return this.synced(agent).info();
1160
+ }
1161
+ };
1162
+ _init = __decoratorStart(_a);
1163
+ __decorateElement(_init, 1, "list", _list_dec, SessionTreeService);
1164
+ __decorateElement(_init, 1, "jump", _jump_dec, SessionTreeService);
1165
+ __decorateElement(_init, 1, "fork", _fork_dec, SessionTreeService);
1166
+ __decorateElement(_init, 1, "session", _session_dec, SessionTreeService);
1167
+ __decoratorMetadata(_init, SessionTreeService);
1168
+ var index_default = SessionTreeService;
1169
+ export {
1170
+ SessionTree,
1171
+ SessionTreeService,
1172
+ SessionTreeSidecar,
1173
+ SessionTreeStore,
1174
+ appendSessionTreeEvent,
1175
+ applyTreeCursorToSession,
1176
+ attachToolResult,
1177
+ index_default as default,
1178
+ getSessionTreeSidecar,
1179
+ isSessionTreeRestoreEvent,
1180
+ persistSessionTree,
1181
+ sessionEventsToTreeNodes,
1182
+ sessionTreeMarkerOf,
1183
+ sessionTreeStore,
1184
+ sessionTreeSurfaceMode,
1185
+ setSessionTreeSidecar,
1186
+ supportsDurableSessionTreeEvents,
1187
+ supportsSelectedMessageSurface,
1188
+ syncSessionTree,
1189
+ toolResultOf
1190
+ };