pi2dsh 0.17.0 → 0.18.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.
@@ -6,7 +6,7 @@ import { W as getKeybindings, _t as stripTerminalSequences } from "./pi-tui-DruS
6
6
  import { c as resolvePiProviderAuth, i as oauthCredentialRef, l as storedOAuthCredential, n as joinSignals, o as providerSupportsOAuth, r as loginPiProvider, t as FileCredentialStore } from "./oauth-bridge-C_MAPMGA.mjs";
7
7
  import { createRequire } from "node:module";
8
8
  import { access, mkdir, readFile, writeFile } from "node:fs/promises";
9
- import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
9
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
10
10
  import { basename, dirname, join, resolve } from "node:path";
11
11
  import { fileURLToPath } from "node:url";
12
12
  import { tmpdir } from "node:os";
@@ -27,6 +27,81 @@ function entrySeq(id) {
27
27
  const match = /^dsh-(\d+)$/.exec(id);
28
28
  return match === null ? void 0 : Number(match[1]);
29
29
  }
30
+ /** One Pi-only record as the Pi session-file entry line it is stored as. */
31
+ function piEntryLineOf(record) {
32
+ const base = {
33
+ id: record.id,
34
+ parentId: null,
35
+ timestamp: record.timestamp
36
+ };
37
+ switch (record.kind) {
38
+ case "custom": return JSON.stringify({
39
+ type: "custom",
40
+ ...base,
41
+ customType: record.customType,
42
+ ...record.data === void 0 ? {} : { data: record.data }
43
+ });
44
+ case "label": return JSON.stringify({
45
+ type: "label",
46
+ ...base,
47
+ targetId: record.targetId,
48
+ ...record.label === void 0 ? {} : { label: record.label }
49
+ });
50
+ case "branch_summary": return JSON.stringify({
51
+ type: "branch_summary",
52
+ ...base,
53
+ fromId: record.fromId,
54
+ summary: record.summary
55
+ });
56
+ case "name": return JSON.stringify({
57
+ type: "session_info",
58
+ ...base,
59
+ name: record.name
60
+ });
61
+ }
62
+ }
63
+ /**
64
+ * Normalize one parsed session-file line back into a record. Understands the
65
+ * Pi entry shapes this bridge writes today AND the pre-0.18 private
66
+ * `{kind: …}` lines, so archives written by older engines keep loading. A Pi
67
+ * header line and anything unrecognized return undefined.
68
+ */
69
+ function recordOfParsedLine(parsed) {
70
+ if (typeof parsed.kind === "string") return parsed;
71
+ const id = typeof parsed.id === "string" ? parsed.id : void 0;
72
+ const timestamp = typeof parsed.timestamp === "string" ? parsed.timestamp : void 0;
73
+ if (id === void 0 || timestamp === void 0) return void 0;
74
+ switch (parsed.type) {
75
+ case "custom": return {
76
+ kind: "custom",
77
+ id,
78
+ timestamp,
79
+ customType: String(parsed.customType ?? ""),
80
+ ...parsed.data === void 0 ? {} : { data: parsed.data }
81
+ };
82
+ case "label": return {
83
+ kind: "label",
84
+ id,
85
+ timestamp,
86
+ ...typeof parsed.targetId === "string" ? { targetId: parsed.targetId } : {},
87
+ ...typeof parsed.label === "string" ? { label: parsed.label } : {}
88
+ };
89
+ case "branch_summary": return {
90
+ kind: "branch_summary",
91
+ id,
92
+ timestamp,
93
+ ...typeof parsed.summary === "string" ? { summary: parsed.summary } : {},
94
+ ...typeof parsed.fromId === "string" ? { fromId: parsed.fromId } : {}
95
+ };
96
+ case "session_info": return {
97
+ kind: "name",
98
+ id,
99
+ timestamp,
100
+ ...typeof parsed.name === "string" ? { name: parsed.name } : {}
101
+ };
102
+ default: return;
103
+ }
104
+ }
30
105
  function sidecarDir() {
31
106
  return join(getAgentDir(), "session-entries");
32
107
  }
@@ -72,17 +147,24 @@ var PiSessionBridge = class {
72
147
  /**
73
148
  * The Pi-visible archive file for one DSH session — the established
74
149
  * `<id>.jsonl` convention (getSessionFile/switchSession use the same one).
75
- * Guaranteed to EXIST on return: Pi consumers treat the session file as the
76
- * durable identity a conversation can be reopened by (pi-subagents guards
77
- * its tombstone resurrect with existsSync, and Pi's SessionManager.open
78
- * tolerates an empty file), so a merely virtual path would read as "the
79
- * conversation is gone".
150
+ * Guaranteed to EXIST on return, and to start with a genuine Pi
151
+ * `{type:"session"}` header line: Pi consumers treat the session file as
152
+ * the durable identity a conversation can be reopened by (pi-subagents
153
+ * guards its tombstone resurrect with existsSync), and real Pi parsing
154
+ * (`SessionManager.open` reads the header for id/cwd) must see a Pi file,
155
+ * not a bare inode. An empty pre-existing file is upgraded in place.
80
156
  */
81
- archiveFileFor(sessionId) {
157
+ archiveFileFor(sessionId, cwd) {
82
158
  const path = this.sidecarPath(sessionId);
83
- if (!existsSync(path)) {
159
+ if (!existsSync(path) || readFileSync(path, "utf8").trim().length === 0) {
84
160
  mkdirSync(sidecarDir(), { recursive: true });
85
- appendFileSync(path, "");
161
+ appendFileSync(path, `${JSON.stringify({
162
+ type: "session",
163
+ version: 3,
164
+ id: sessionId,
165
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
166
+ cwd: cwd ?? process.cwd()
167
+ })}\n`);
86
168
  }
87
169
  return path;
88
170
  }
@@ -100,6 +182,19 @@ var PiSessionBridge = class {
100
182
  const base = basename(resolved);
101
183
  return base.endsWith(".jsonl") ? base.slice(0, -6) : void 0;
102
184
  }
185
+ /**
186
+ * Retire one archive this bridge minted: the file whose existence tells Pi
187
+ * consumers "this conversation can be reopened". Called ONLY on a positive
188
+ * persistence-layer verdict that the DSH session is gone, so existsSync
189
+ * answers honestly again.
190
+ */
191
+ discardArchive(sessionId) {
192
+ try {
193
+ unlinkSync(this.sidecarPath(sessionId));
194
+ } catch {}
195
+ this.records.delete(sessionId);
196
+ this.loaded.delete(sessionId);
197
+ }
103
198
  load(sessionId) {
104
199
  if (this.loaded.has(sessionId)) return;
105
200
  this.loaded.add(sessionId);
@@ -112,7 +207,8 @@ var PiSessionBridge = class {
112
207
  for (const line of readFileSync(path, "utf8").split("\n")) {
113
208
  if (line.trim().length === 0) continue;
114
209
  try {
115
- parsed.push(JSON.parse(line));
210
+ const record = recordOfParsedLine(JSON.parse(line));
211
+ if (record !== void 0) parsed.push(record);
116
212
  } catch {}
117
213
  }
118
214
  this.records.set(sessionId, parsed);
@@ -122,8 +218,8 @@ var PiSessionBridge = class {
122
218
  const list = this.records.get(sessionId) ?? [];
123
219
  list.push(record);
124
220
  this.records.set(sessionId, list);
125
- mkdirSync(sidecarDir(), { recursive: true });
126
- appendFileSync(this.sidecarPath(sessionId), `${JSON.stringify(record)}\n`);
221
+ this.archiveFileFor(sessionId);
222
+ appendFileSync(this.sidecarPath(sessionId), `${piEntryLineOf(record)}\n`);
127
223
  }
128
224
  appendCustomEntry(sessionId, customType, data) {
129
225
  const id = randomUUID();
@@ -338,7 +434,7 @@ var PiSessionBridge = class {
338
434
  getCwd: () => cwd,
339
435
  getSessionDir: () => sidecarDir(),
340
436
  getSessionId: () => session.id,
341
- getSessionFile: () => this.archiveFileFor(session.id),
437
+ getSessionFile: () => this.archiveFileFor(session.id, cwd),
342
438
  getLeafId: () => leafOf()?.id ?? null,
343
439
  getLeafEntry: () => leafOf(),
344
440
  getEntry: (id) => entriesOf().find((entry) => entry.id === id),
@@ -949,7 +1045,14 @@ async function createBridgedAgentSession(host, options) {
949
1045
  setup
950
1046
  });
951
1047
  } catch (error) {
952
- throw new Error(resumeSessionId !== void 0 ? `pi2dsh: reopening child session ${JSON.stringify(resumeSessionId)} failed — its persisted log may be gone or this composition has no session persistence (${error instanceof Error ? error.message : String(error)})` : `pi2dsh: subagent creation needs the DSH host loop (model runtime) to provide the agent factory; this composition cannot run one (${error instanceof Error ? error.message : String(error)})`);
1048
+ if (resumeSessionId !== void 0) {
1049
+ if (await host.sessionGoneFromPersistence?.(resumeSessionId) === true) {
1050
+ host.discardStaleArchive?.(resumeSessionId);
1051
+ throw new Error(`pi2dsh: child session ${JSON.stringify(resumeSessionId)} no longer exists in the host's session persistence — the conversation is gone and its archive identity has been retired (${error instanceof Error ? error.message : String(error)})`);
1052
+ }
1053
+ throw new Error(`pi2dsh: reopening child session ${JSON.stringify(resumeSessionId)} failed — this composition may lack session persistence (${error instanceof Error ? error.message : String(error)})`);
1054
+ }
1055
+ throw new Error(`pi2dsh: subagent creation needs the DSH host loop (model runtime) to provide the agent factory; this composition cannot run one (${error instanceof Error ? error.message : String(error)})`);
953
1056
  }
954
1057
  const tools = [...Array.isArray(options.tools) ? options.tools : [], ...Array.isArray(options.customTools) ? options.customTools : []];
955
1058
  const childSession = handle.agent.session;
@@ -6779,6 +6882,16 @@ async function applyPiPackage(ctx, options) {
6779
6882
  return state.bridge.readonlySessionManager(session, cwd);
6780
6883
  },
6781
6884
  resumeSessionIdFor: (file) => state.bridge.sessionIdOfArchiveFile(file),
6885
+ sessionGoneFromPersistence: async (sessionId) => {
6886
+ const persistence = optionalService(ctx, "sessionPersistence");
6887
+ if (typeof persistence?.list !== "function") return void 0;
6888
+ try {
6889
+ return !(await persistence.list()).some((header) => String(header?.id ?? "") === sessionId);
6890
+ } catch {
6891
+ return;
6892
+ }
6893
+ },
6894
+ discardStaleArchive: (sessionId) => state.bridge.discardArchive(sessionId),
6782
6895
  parentModelRoute: () => {
6783
6896
  const parent = currentAgent(state);
6784
6897
  if (parent === void 0) return void 0;
@@ -6849,4 +6962,4 @@ const runtimeInternals = {
6849
6962
  //#endregion
6850
6963
  export { registerVisionCompanions as a, overlayProviderConfig as i, mergeProviderRegistration as n, runtimeInternals as o, normalizeToolSchema as r, applyPiPackage as t };
6851
6964
 
6852
- //# sourceMappingURL=runtime-DE2dU1cA.mjs.map
6965
+ //# sourceMappingURL=runtime-C-cmLDY0.mjs.map