pi2dsh 0.24.0 → 0.25.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.
Files changed (47) hide show
  1. package/README.md +11 -3
  2. package/README.zh.md +10 -3
  3. package/dist/{analyzer-DqsrD1FG.mjs → analyzer-DbgL3neo.mjs} +2 -2
  4. package/dist/{analyzer-DqsrD1FG.mjs.map → analyzer-DbgL3neo.mjs.map} +1 -1
  5. package/dist/cli.mjs +3 -3
  6. package/dist/compat/pi-ai.d.mts +2 -2
  7. package/dist/compat/pi-ai.mjs +2 -2
  8. package/dist/compat/pi-coding-agent.d.mts +2 -2
  9. package/dist/compat/pi-coding-agent.mjs +2 -2
  10. package/dist/{compatibility-CZ0QpE76.mjs → compatibility-DS6POFZD.mjs} +28 -25
  11. package/dist/compatibility-DS6POFZD.mjs.map +1 -0
  12. package/dist/credentials-oauth.mjs +2 -2
  13. package/dist/{google-generative-ai-Dpb_X-Kt.mjs → google-generative-ai-DYSbNJT7.mjs} +2 -2
  14. package/dist/{google-generative-ai-Dpb_X-Kt.mjs.map → google-generative-ai-DYSbNJT7.mjs.map} +1 -1
  15. package/dist/{google-shared-CL1Za-ev.mjs → google-shared-LEpwok5n.mjs} +2 -2
  16. package/dist/{google-shared-CL1Za-ev.mjs.map → google-shared-LEpwok5n.mjs.map} +1 -1
  17. package/dist/{google-vertex-BXRKa49Y.mjs → google-vertex-BgGRE4WU.mjs} +2 -2
  18. package/dist/{google-vertex-BXRKa49Y.mjs.map → google-vertex-BgGRE4WU.mjs.map} +1 -1
  19. package/dist/host.mjs +2 -2
  20. package/dist/index.d.mts.map +1 -1
  21. package/dist/index.mjs +5 -5
  22. package/dist/{multipart-parser-CBAi6PxA.mjs → multipart-parser-BOEZ-KGK.mjs} +2 -2
  23. package/dist/{multipart-parser-CBAi6PxA.mjs.map → multipart-parser-BOEZ-KGK.mjs.map} +1 -1
  24. package/dist/{pi-ai-hGbYES_v.d.mts → pi-ai-BHgA3JWd.d.mts} +14 -6
  25. package/dist/{pi-ai-hGbYES_v.d.mts.map → pi-ai-BHgA3JWd.d.mts.map} +1 -1
  26. package/dist/{pi-ai-DQPHUR9S.mjs → pi-ai-Cy_BRMmd.mjs} +23 -6
  27. package/dist/pi-ai-Cy_BRMmd.mjs.map +1 -0
  28. package/dist/pi-cli-client.mjs +25 -0
  29. package/dist/{pi-coding-agent-Pvy0m9tS.d.mts → pi-coding-agent-Cas0oHSx.d.mts} +18 -3
  30. package/dist/{pi-coding-agent-Pvy0m9tS.d.mts.map → pi-coding-agent-Cas0oHSx.d.mts.map} +1 -1
  31. package/dist/{pi-coding-agent-CPXFo0UO.mjs → pi-coding-agent-lcNs9Wos.mjs} +0 -0
  32. package/dist/pi-coding-agent-lcNs9Wos.mjs.map +1 -0
  33. package/dist/{runtime-BiHMcD_8.mjs → runtime-B3N7s93Q.mjs} +868 -126
  34. package/dist/runtime-B3N7s93Q.mjs.map +1 -0
  35. package/dist/runtime.d.mts +34 -3
  36. package/dist/runtime.d.mts.map +1 -1
  37. package/dist/runtime.mjs +1 -1
  38. package/dist/{source-0sA5z08z.mjs → source-l77aIDwD.mjs} +76 -3
  39. package/dist/source-l77aIDwD.mjs.map +1 -0
  40. package/dist/{src-85HOaW5W.mjs → src-ChZlNrRV.mjs} +3 -3
  41. package/dist/{src-85HOaW5W.mjs.map → src-ChZlNrRV.mjs.map} +1 -1
  42. package/package.json +24 -14
  43. package/dist/compatibility-CZ0QpE76.mjs.map +0 -1
  44. package/dist/pi-ai-DQPHUR9S.mjs.map +0 -1
  45. package/dist/pi-coding-agent-CPXFo0UO.mjs.map +0 -1
  46. package/dist/runtime-BiHMcD_8.mjs.map +0 -1
  47. package/dist/source-0sA5z08z.mjs.map +0 -1
@@ -1,27 +1,193 @@
1
1
 
2
- import { Dn as PiCapabilityError, En as CapabilityLedger, F as generateBranchSummary, b as Theme, d as ExtensionRunner, w as __runWithSubagentSessionFactory } from "./pi-coding-agent-CPXFo0UO.mjs";
2
+ import { Dn as CapabilityLedger, F as generateBranchSummary, On as PiCapabilityError, b as Theme, d as ExtensionRunner, fn as getDefaultSessionDir, w as __runWithSubagentSessionFactory } from "./pi-coding-agent-lcNs9Wos.mjs";
3
3
  import { t as getAgentDir } from "./pi-config-shim-CZ1wFzqM.mjs";
4
- import { $ as AssistantMessageEventStream, S as getSupportedThinkingLevels, c as builtinProviders, et as isContextOverflow, o as __runWithPiAiRuntime, r as __createPiAiRuntimeRegistry } from "./pi-ai-DQPHUR9S.mjs";
4
+ import { C as getSupportedThinkingLevels, c as builtinProviders, nt as isContextOverflow, o as __runWithPiAiRuntime, r as __createPiAiRuntimeRegistry, tt as AssistantMessageEventStream } from "./pi-ai-Cy_BRMmd.mjs";
5
5
  import { W as getKeybindings, _t as stripTerminalSequences } from "./pi-tui-DruSeKmd.mjs";
6
6
  import { r as piMcpConfigPaths, t as collectPiMcpServers } from "./mcp-config-C1K-IzzB.mjs";
7
7
  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";
8
8
  import { createRequire } from "node:module";
9
- import { access, mkdir, readFile, writeFile } from "node:fs/promises";
10
- import { appendFileSync, existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
11
- import { basename, dirname, join, resolve } from "node:path";
9
+ import { access, chmod, mkdir, mkdtemp, readFile, realpath, rm, stat, writeFile } from "node:fs/promises";
10
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
11
+ import { basename, dirname, join, relative, resolve } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
+ import { createServer } from "node:net";
14
+ import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
13
15
  import { homedir, tmpdir } from "node:os";
14
16
  import { EventEmitter } from "node:events";
15
17
  import { AsyncLocalStorage } from "node:async_hooks";
16
18
  import { createJiti } from "jiti";
19
+ import * as llm from "@deepseek-ai/dsh-llm";
17
20
  import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, QUOTA_EXCEEDED_CODE, createUserMessage, isContextWindowExceededError, isQuotaExceededError } from "@deepseek-ai/dsh-llm";
18
21
  import { scopeOf } from "@deepseek-ai/dsh-scope";
19
22
  import { renderPrompt } from "@deepseek-ai/dsh-system-prompt";
20
- import { randomUUID } from "node:crypto";
21
23
  import { foldSurface } from "@deepseek-ai/dsh-session";
22
24
  import { spawn } from "node:child_process";
23
25
  import { Compile } from "typebox/compile";
24
26
  import { Value } from "typebox/value";
27
+ //#region src/session-events.ts
28
+ function readSessionEvents(session) {
29
+ if (typeof session !== "object" || session === null) return [];
30
+ const value = session;
31
+ const events = typeof value.snapshotEvents === "function" ? value.snapshotEvents.call(session) : typeof value.events === "function" ? value.events.call(session) : value.events;
32
+ if (!Array.isArray(events)) throw new Error("pi2dsh: the DSH session exposes no supported synchronous history reader");
33
+ return events;
34
+ }
35
+ /** Host-authored context is not a human turn; relay messages intentionally are. */
36
+ function isPiUserMessage(data) {
37
+ const source = data.source;
38
+ return source?.kind === void 0 || source.kind === "user" || source.form === "relay";
39
+ }
40
+ /** The existing native descriptor convention for Pi in-memory side sessions. */
41
+ function isPiEphemeralSession(session) {
42
+ return readSessionEvents(session).some((event) => {
43
+ const data = event.data;
44
+ return event.type === "subagent/descriptor" && data?.provider === "pi2dsh" && data.label?.startsWith("Side: ") === true;
45
+ });
46
+ }
47
+ //#endregion
48
+ //#region src/pi-cli-bridge.ts
49
+ async function parsePiPrintArgs(args, cwd) {
50
+ const parsed = {
51
+ cwd,
52
+ prompt: "",
53
+ extensions: [],
54
+ noExtensions: false,
55
+ noSession: false
56
+ };
57
+ let print = false;
58
+ const prompts = [];
59
+ const value = (index) => {
60
+ const found = args[index + 1];
61
+ if (found === void 0) throw new Error(`pi2dsh CLI: ${args[index]} needs a value`);
62
+ return found;
63
+ };
64
+ for (let i = 0; i < args.length; i++) {
65
+ const arg = args[i];
66
+ if (arg === "-p" || arg === "--print") print = true;
67
+ else if (arg === "--no-session") parsed.noSession = true;
68
+ else if (arg === "--no-extensions") parsed.noExtensions = true;
69
+ else if (arg === "--no-tools") parsed.tools = [];
70
+ else if (arg === "-e" || arg === "--extension") parsed.extensions.push(await realpath(resolve(cwd, value(i++))));
71
+ else if (arg === "--model") parsed.model = value(i++);
72
+ else if (arg === "--provider") parsed.provider = value(i++);
73
+ else if (arg === "--thinking") parsed.thinking = value(i++);
74
+ else if (arg === "--tools") parsed.tools = value(i++).split(",").filter(Boolean);
75
+ else if (arg === "--system-prompt") parsed.systemPrompt = value(i++);
76
+ else if (arg === "--append-system-prompt") parsed.appendSystemPrompt = value(i++);
77
+ else if (arg === "--mode") {
78
+ if (value(i++) !== "text") throw new Error("pi2dsh CLI: only non-interactive text output is supported");
79
+ } else if (arg.startsWith("-")) throw new Error(`pi2dsh CLI: unsupported option ${arg}`);
80
+ else if (arg.startsWith("@")) {
81
+ const path = resolve(cwd, arg.slice(1));
82
+ prompts.push(`<file name=${JSON.stringify(path)}>\n${await readFile(path, "utf8")}\n</file>`);
83
+ } else prompts.push(arg);
84
+ }
85
+ if (!print) throw new Error("pi2dsh CLI: only pi -p/--print is supported; use the DSH surface for interactive work");
86
+ if (prompts.length === 0) throw new Error("pi2dsh CLI: a prompt or @file is required");
87
+ parsed.prompt = prompts.join("\n\n");
88
+ return parsed;
89
+ }
90
+ async function withPiPrintTransport(invoke, execute) {
91
+ if (process.platform === "win32") return execute({});
92
+ const directory = await mkdtemp(join(tmpdir(), "p2cli-"));
93
+ await chmod(directory, 448);
94
+ const socketPath = join(directory, "s");
95
+ const token = randomBytes(32).toString("hex");
96
+ const sockets = /* @__PURE__ */ new Set();
97
+ const work = /* @__PURE__ */ new Set();
98
+ const controllers = /* @__PURE__ */ new Set();
99
+ const server = createServer((socket) => {
100
+ sockets.add(socket);
101
+ const controller = new AbortController();
102
+ controllers.add(controller);
103
+ let input = "";
104
+ let claimed = false;
105
+ let finished = false;
106
+ socket.setEncoding("utf8");
107
+ socket.on("error", () => controller.abort());
108
+ socket.on("close", () => {
109
+ sockets.delete(socket);
110
+ controllers.delete(controller);
111
+ if (!finished) controller.abort(/* @__PURE__ */ new Error("Pi print subprocess disconnected"));
112
+ });
113
+ socket.on("data", (chunk) => {
114
+ if (claimed) return;
115
+ input += chunk;
116
+ if (input.length > 4194304) {
117
+ socket.destroy();
118
+ return;
119
+ }
120
+ if (!input.includes("\n")) return;
121
+ claimed = true;
122
+ const task = (async () => {
123
+ try {
124
+ const request = JSON.parse(input.slice(0, input.indexOf("\n")));
125
+ const supplied = typeof request.token === "string" ? Buffer.from(request.token) : Buffer.alloc(0);
126
+ const expected = Buffer.from(token);
127
+ if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) throw new Error("pi2dsh CLI: invalid execution capability");
128
+ if (!Array.isArray(request.args) || request.args.some((arg) => typeof arg !== "string") || typeof request.cwd !== "string") throw new Error("pi2dsh CLI: invalid request");
129
+ const parsed = await parsePiPrintArgs(request.args, request.cwd);
130
+ controller.signal.throwIfAborted();
131
+ const result = await invoke(parsed, controller.signal);
132
+ finished = true;
133
+ socket.end(`${JSON.stringify(result)}\n`);
134
+ } catch (error) {
135
+ finished = true;
136
+ console.warn("[pi2dsh CLI]", error instanceof Error ? error.message : String(error));
137
+ socket.end(`${JSON.stringify({
138
+ code: 1,
139
+ stdout: "",
140
+ stderr: error instanceof Error ? error.message : String(error)
141
+ })}\n`);
142
+ }
143
+ })();
144
+ work.add(task);
145
+ task.finally(() => work.delete(task));
146
+ });
147
+ });
148
+ try {
149
+ await new Promise((resolveListen, reject) => {
150
+ server.once("error", reject);
151
+ server.listen(socketPath, resolveListen);
152
+ });
153
+ await chmod(socketPath, 384);
154
+ const quote = (text) => `'${text.replaceAll("'", "'\\''")}'`;
155
+ const client = fileURLToPath(new URL("./pi-cli-client.mjs", import.meta.url));
156
+ await writeFile(join(directory, "pi"), `#!/bin/sh\nexec ${quote(process.execPath)} ${quote(client)} "$@"\n`, { mode: 448 });
157
+ return await execute({
158
+ PATH: `${directory}:${process.env.PATH ?? ""}`,
159
+ PI2DSH_CLI_SOCKET: socketPath,
160
+ PI2DSH_CLI_TOKEN: token
161
+ });
162
+ } finally {
163
+ for (const controller of controllers) controller.abort(/* @__PURE__ */ new Error("Pi exec completed"));
164
+ for (const socket of sockets) socket.destroy();
165
+ await Promise.allSettled(work);
166
+ await new Promise((resolveClose) => server.close(() => resolveClose()));
167
+ await rm(directory, {
168
+ recursive: true,
169
+ force: true
170
+ });
171
+ }
172
+ }
173
+ //#endregion
174
+ //#region src/dsh-content.ts
175
+ /** A file is text on the Pi side, using the host's own execution-world locator. */
176
+ function fileContentForPi(attachment, ctx) {
177
+ const format = llm.fileHandleText;
178
+ if (format === void 0) throw new Error("pi2dsh: this DSH host cannot project file attachments");
179
+ let path;
180
+ try {
181
+ const hostPath = (ctx?.get("attachments"))?.fileHostPath?.(attachment);
182
+ const fs = ctx?.get("fs");
183
+ if (hostPath !== void 0) path = fs?.processPathFromHostPath(hostPath);
184
+ } catch {}
185
+ return {
186
+ type: "text",
187
+ text: format(attachment, path)
188
+ };
189
+ }
190
+ //#endregion
25
191
  //#region src/session-bridge.ts
26
192
  /** The durable seq behind a projected entry id (`dsh-<seq>`), when it has one. */
27
193
  function entrySeq(id) {
@@ -107,10 +273,9 @@ function sidecarDir() {
107
273
  return join(getAgentDir(), "session-entries");
108
274
  }
109
275
  function sessionEvents(session) {
110
- const events = session.events;
111
- return typeof events === "function" ? events.call(session) : events ?? [];
276
+ return readSessionEvents(session);
112
277
  }
113
- function dshToPiContent$1(content) {
278
+ function dshToPiContent$1(content, fileContent = fileContentForPi) {
114
279
  if (!Array.isArray(content)) return [{
115
280
  type: "text",
116
281
  text: String(content ?? "")
@@ -121,6 +286,7 @@ function dshToPiContent$1(content) {
121
286
  text: String(block)
122
287
  };
123
288
  const record = block;
289
+ if (record.type === "file") return fileContent(record.attachment);
124
290
  if (record.type === "text") return {
125
291
  type: "text",
126
292
  text: String(record.text ?? "")
@@ -139,8 +305,90 @@ function dshToPiContent$1(content) {
139
305
  });
140
306
  }
141
307
  var PiSessionBridge = class {
308
+ fileContent;
309
+ constructor(fileContent = fileContentForPi) {
310
+ this.fileContent = fileContent;
311
+ }
142
312
  records = /* @__PURE__ */ new Map();
143
313
  loaded = /* @__PURE__ */ new Set();
314
+ exportedSessions = /* @__PURE__ */ new Map();
315
+ async exportStoredSessions(persistence) {
316
+ const priorExports = [];
317
+ const root = join(getAgentDir(), "sessions");
318
+ if (existsSync(root)) for (const directory of readdirSync(root, { withFileTypes: true })) {
319
+ if (!directory.isDirectory()) continue;
320
+ for (const entry of readdirSync(join(root, directory.name), { withFileTypes: true })) {
321
+ if (!entry.isFile() || !entry.name.startsWith("dsh-")) continue;
322
+ const path = join(root, directory.name, entry.name);
323
+ const id = this.sessionIdOfArchiveFile(path);
324
+ if (id !== void 0) priorExports.push({
325
+ path,
326
+ id
327
+ });
328
+ }
329
+ }
330
+ const snapshots = await persistence.list();
331
+ const present = /* @__PURE__ */ new Set();
332
+ const hidden = /* @__PURE__ */ new Set();
333
+ for (const snapshot of snapshots) {
334
+ const header = snapshot.header ?? snapshot;
335
+ if (typeof header.id !== "string") continue;
336
+ present.add(header.id);
337
+ if (typeof header.cwd !== "string") continue;
338
+ if (typeof persistence.open === "function") {
339
+ const handle = await persistence.open(header.id, "read");
340
+ try {
341
+ const { events } = await handle.read();
342
+ const session = {
343
+ id: header.id,
344
+ header: handle.header,
345
+ events
346
+ };
347
+ if (isPiEphemeralSession(session)) hidden.add(header.id);
348
+ else this.exportSessionFile(session, header.cwd);
349
+ } finally {
350
+ await handle.close();
351
+ }
352
+ } else if (typeof persistence.inspect === "function") {
353
+ const { meta, events } = await persistence.inspect(header.id);
354
+ const session = {
355
+ id: header.id,
356
+ header: meta,
357
+ events
358
+ };
359
+ if (isPiEphemeralSession(session)) hidden.add(header.id);
360
+ else this.exportSessionFile(session, header.cwd);
361
+ }
362
+ this.exportedSessions.delete(header.id);
363
+ }
364
+ for (const exported of priorExports) if ((!present.has(exported.id) || hidden.has(exported.id)) && existsSync(exported.path)) unlinkSync(exported.path);
365
+ }
366
+ /** Materialize the public Pi file contract from the native session, never vice versa. */
367
+ exportSessionFile(session, cwd) {
368
+ this.exportedSessions.set(session.id, {
369
+ session,
370
+ cwd
371
+ });
372
+ const safe = session.id.replace(/[^a-zA-Z0-9._-]+/gu, "_");
373
+ const path = join(getDefaultSessionDir(cwd, getAgentDir()), `dsh-${safe}.jsonl`);
374
+ const text = `${[{
375
+ type: "session",
376
+ version: 3,
377
+ id: session.id,
378
+ cwd,
379
+ timestamp: new Date(Number(session.header?.createdAt ?? sessionEvents(session)[0]?.time ?? Date.now())).toISOString(),
380
+ pi2dshExport: { nativeSessionId: session.id }
381
+ }, ...this.projectEntries(session)].map((value) => JSON.stringify(value)).join("\n")}\n`;
382
+ if (existsSync(path) && readFileSync(path, "utf8") === text) return path;
383
+ const temporary = `${path}.${randomUUID()}.tmp`;
384
+ try {
385
+ writeFileSync(temporary, text, { mode: 384 });
386
+ renameSync(temporary, path);
387
+ } finally {
388
+ if (existsSync(temporary)) unlinkSync(temporary);
389
+ }
390
+ return path;
391
+ }
144
392
  sidecarPath(sessionId) {
145
393
  const safe = sessionId.replace(/[^a-zA-Z0-9._-]+/gu, "_");
146
394
  return join(sidecarDir(), `${safe}.jsonl`);
@@ -179,9 +427,16 @@ var PiSessionBridge = class {
179
427
  sessionIdOfArchiveFile(path) {
180
428
  if (typeof path !== "string" || path.length === 0) return void 0;
181
429
  const resolved = resolve(path);
182
- if (resolve(dirname(resolved)) !== resolve(sidecarDir())) return void 0;
183
430
  const base = basename(resolved);
184
- return base.endsWith(".jsonl") ? base.slice(0, -6) : void 0;
431
+ if (resolve(dirname(resolved)) === resolve(sidecarDir())) return base.endsWith(".jsonl") ? base.slice(0, -6) : void 0;
432
+ if (relative(join(getAgentDir(), "sessions"), resolved).startsWith("..") || !base.startsWith("dsh-") || !base.endsWith(".jsonl")) return void 0;
433
+ try {
434
+ const header = JSON.parse(readFileSync(resolved, "utf8").split("\n")[0]);
435
+ const marker = header.pi2dshExport;
436
+ return typeof marker?.nativeSessionId === "string" && marker.nativeSessionId === header.id ? marker.nativeSessionId : void 0;
437
+ } catch {
438
+ return;
439
+ }
185
440
  }
186
441
  /**
187
442
  * Retire one archive this bridge minted: the file whose existence tells Pi
@@ -195,6 +450,13 @@ var PiSessionBridge = class {
195
450
  } catch {}
196
451
  this.records.delete(sessionId);
197
452
  this.loaded.delete(sessionId);
453
+ this.exportedSessions.delete(sessionId);
454
+ const root = join(getAgentDir(), "sessions");
455
+ if (existsSync(root)) for (const directory of readdirSync(root, { withFileTypes: true })) {
456
+ if (!directory.isDirectory()) continue;
457
+ const file = join(root, directory.name, `dsh-${sessionId.replace(/[^a-zA-Z0-9._-]+/gu, "_")}.jsonl`);
458
+ if (this.sessionIdOfArchiveFile(file) === sessionId) unlinkSync(file);
459
+ }
198
460
  }
199
461
  load(sessionId) {
200
462
  if (this.loaded.has(sessionId)) return;
@@ -221,6 +483,8 @@ var PiSessionBridge = class {
221
483
  this.records.set(sessionId, list);
222
484
  this.archiveFileFor(sessionId);
223
485
  appendFileSync(this.sidecarPath(sessionId), `${piEntryLineOf(record)}\n`);
486
+ const exported = this.exportedSessions.get(sessionId);
487
+ if (exported !== void 0) this.exportSessionFile(exported.session, exported.cwd);
224
488
  }
225
489
  appendCustomEntry(sessionId, customType, data) {
226
490
  const id = randomUUID();
@@ -330,12 +594,19 @@ var PiSessionBridge = class {
330
594
  if (type === "user/message") merged.push({
331
595
  time,
332
596
  entry: {
333
- type: "message",
334
597
  id: `dsh-${seq}`,
335
598
  timestamp: new Date(time).toISOString(),
336
- message: {
337
- role: "user",
338
- content: dshToPiContent$1(data.content)
599
+ ...isPiUserMessage(data) ? {
600
+ type: "message",
601
+ message: {
602
+ role: "user",
603
+ content: dshToPiContent$1(data.content, this.fileContent)
604
+ }
605
+ } : {
606
+ type: "custom_message",
607
+ customType: String(data.source?.piCustomType ?? data.source?.kind ?? "context"),
608
+ content: dshToPiContent$1(data.content, this.fileContent),
609
+ display: false
339
610
  }
340
611
  }
341
612
  });
@@ -349,7 +620,7 @@ var PiSessionBridge = class {
349
620
  timestamp: new Date(time).toISOString(),
350
621
  message: {
351
622
  role: "assistant",
352
- content: dshToPiContent$1(message.content)
623
+ content: dshToPiContent$1(message.content, this.fileContent)
353
624
  }
354
625
  }
355
626
  });
@@ -433,9 +704,9 @@ var PiSessionBridge = class {
433
704
  const leafOf = () => entriesOf().at(-1);
434
705
  return {
435
706
  getCwd: () => cwd,
436
- getSessionDir: () => sidecarDir(),
707
+ getSessionDir: () => getDefaultSessionDir(cwd, getAgentDir()),
437
708
  getSessionId: () => session.id,
438
- getSessionFile: () => this.archiveFileFor(session.id, cwd),
709
+ getSessionFile: () => isPiEphemeralSession(session) ? void 0 : this.exportSessionFile(session, cwd),
439
710
  getLeafId: () => leafOf()?.id ?? null,
440
711
  getLeafEntry: () => leafOf(),
441
712
  getEntry: (id) => entriesOf().find((entry) => entry.id === id),
@@ -500,9 +771,9 @@ function nativeToolNameOf(name) {
500
771
  * roster-owned surfaces the preset tools live above the global layer, so the
501
772
  * scoped read is the one that sees them.
502
773
  */
503
- function childSchemas(toolsService, childCtx) {
774
+ function childSchemas(toolsService, childAgent) {
504
775
  try {
505
- if (childCtx.agent !== void 0) return toolsService.schemas(childCtx.agent);
776
+ if (childAgent !== void 0) return toolsService.schemas(childAgent);
506
777
  } catch {}
507
778
  return toolsService.schemas();
508
779
  }
@@ -561,6 +832,8 @@ var PiBridgedAgentSession = class {
561
832
  /** Keeps message projections (which await attachment reads) in log order. */
562
833
  #messageProjection = Promise.resolve();
563
834
  #aborted = false;
835
+ #liveStreamAttempt;
836
+ #streamingText = "";
564
837
  constructor(host, handle, tools, thinkingLevel) {
565
838
  this.#thinkingLevel = typeof thinkingLevel === "string" && thinkingLevel.length > 0 ? thinkingLevel : "off";
566
839
  this.#host = host;
@@ -572,8 +845,32 @@ var PiBridgedAgentSession = class {
572
845
  const cordis = unscopedEventContext(host.cordis);
573
846
  const offEvents = cordis.on("session/event", (session, event) => {
574
847
  if (session !== this.#session) return;
848
+ if (event.type === "assistant/chunk" && this.#liveStreamAttempt !== void 0) return;
575
849
  this.#project(event);
576
850
  });
851
+ const offStream = cordis.on("agent/assistant-stream", (payload) => {
852
+ if (payload.agent !== this.#handle.agent) return;
853
+ const frame = payload.frame;
854
+ if (frame.type === "start") {
855
+ this.#liveStreamAttempt = {
856
+ id: frame.attemptId,
857
+ index: -1,
858
+ ended: false
859
+ };
860
+ this.#streamingText = "";
861
+ } else {
862
+ const active = this.#liveStreamAttempt;
863
+ if (active === void 0 || active.ended || active.id !== frame.attemptId) return;
864
+ if (frame.type === "end") active.ended = true;
865
+ if (frame.type === "chunk" && Number(frame.index) > active.index) {
866
+ active.index = Number(frame.index);
867
+ this.#project({
868
+ type: "assistant/chunk",
869
+ data: { chunk: frame.chunk }
870
+ });
871
+ }
872
+ }
873
+ });
577
874
  const offPre = cordis.on("tools/pre-execute", async (exec, next) => {
578
875
  if (exec.agent !== this.#handle.agent || this.agent.beforeToolCall === void 0) return next();
579
876
  const decision = await this.agent.beforeToolCall({ toolCall: {
@@ -587,7 +884,7 @@ var PiBridgedAgentSession = class {
587
884
  };
588
885
  return next();
589
886
  });
590
- this.#disposers.push(offEvents, offPre);
887
+ this.#disposers.push(offEvents, offStream, offPre);
591
888
  const session = this;
592
889
  this.#state = {
593
890
  get systemPrompt() {
@@ -712,22 +1009,26 @@ var PiBridgedAgentSession = class {
712
1009
  args
713
1010
  });
714
1011
  }
1012
+ if (type === "assistant/message") this.#streamingText = "";
715
1013
  if (type === "assistant/chunk") {
716
1014
  const chunk = event.data.chunk ?? {};
1015
+ if (chunk.type !== void 0 && chunk.type !== "text-delta") return;
717
1016
  const delta = typeof chunk.text === "string" ? chunk.text : typeof chunk.delta === "string" ? chunk.delta : "";
1017
+ const message = {
1018
+ role: "assistant",
1019
+ content: [{
1020
+ type: "text",
1021
+ text: this.#streamingText += delta
1022
+ }]
1023
+ };
718
1024
  emit({
719
1025
  type: "message_update",
720
- message: {
721
- role: "assistant",
722
- content: [{
723
- type: "text",
724
- text: delta
725
- }]
726
- },
1026
+ message,
727
1027
  assistantMessageEvent: {
728
1028
  type: "text_delta",
1029
+ contentIndex: 0,
729
1030
  delta,
730
- ...chunk
1031
+ partial: message
731
1032
  }
732
1033
  });
733
1034
  }
@@ -809,7 +1110,8 @@ var PiBridgedAgentSession = class {
809
1110
  content: blocks,
810
1111
  source: {
811
1112
  kind: "plugin",
812
- plugin: this.#host.messageSource
1113
+ plugin: this.#host.messageSource,
1114
+ form: "relay"
813
1115
  }
814
1116
  }), "followup");
815
1117
  await completed;
@@ -964,6 +1266,9 @@ async function createBridgedAgentSession(host, options) {
964
1266
  const archiveFile = typeof providedManager?.getSessionFile === "function" ? providedManager.getSessionFile() : void 0;
965
1267
  const resumeSessionId = host.resumeSessionIdFor?.(archiveFile);
966
1268
  const sideline = providedManager !== void 0 && archiveFile === void 0;
1269
+ const parentId = host.parentSessionId();
1270
+ const parentAgent = host.parentAgent?.() ?? (parentId === void 0 ? void 0 : agents.get?.(parentId));
1271
+ const parentOwnership = parentAgent === void 0 ? {} : { parentAgent };
967
1272
  subagentSerial += 1;
968
1273
  const sessionId = `pi2dsh-sub-${Date.now().toString(36)}-${subagentSerial}`;
969
1274
  let handle;
@@ -986,8 +1291,9 @@ async function createBridgedAgentSession(host, options) {
986
1291
  ...typeof requestedModel.provider === "string" && requestedModel.provider.length > 0 ? { provider: requestedModel.provider } : {}
987
1292
  } : host.parentModelRoute?.();
988
1293
  const agentOptionsFragment = typeof route?.model === "string" && route.model.length > 0 ? { agentOptions: route } : {};
989
- const setup = async (childCtx) => {
990
- const childSession = childCtx.agent?.session;
1294
+ const setup = async (childCtx, agent) => {
1295
+ const childAgent = agent ?? childCtx.agent;
1296
+ const childSession = childAgent?.session;
991
1297
  if (typeof childSession?.append === "function") {
992
1298
  if (delegated.sandboxMode !== void 0) childSession.append("sandbox/mode", {
993
1299
  mode: delegated.sandboxMode,
@@ -1017,7 +1323,7 @@ async function createBridgedAgentSession(host, options) {
1017
1323
  if (restriction !== void 0) {
1018
1324
  const toolsService = typeof childCtx.get === "function" ? childCtx.get("tools") : void 0;
1019
1325
  if (toolsService !== void 0) {
1020
- const known = new Set(childSchemas(toolsService, childCtx).map((schema) => schema.name));
1326
+ const known = new Set(childSchemas(toolsService, childAgent).map((schema) => schema.name));
1021
1327
  initialRestrictionDispose = toolsService.restrict({ allow: restriction.filter((name) => known.has(name)) });
1022
1328
  }
1023
1329
  }
@@ -1025,7 +1331,7 @@ async function createBridgedAgentSession(host, options) {
1025
1331
  if (excluded.length > 0) {
1026
1332
  const toolsService = typeof childCtx.get === "function" ? childCtx.get("tools") : void 0;
1027
1333
  if (toolsService !== void 0) {
1028
- const known = new Set(childSchemas(toolsService, childCtx).map((schema) => schema.name));
1334
+ const known = new Set(childSchemas(toolsService, childAgent).map((schema) => schema.name));
1029
1335
  toolsService.restrict({ deny: excluded.map(nativeToolNameOf).filter((name) => known.has(name)) });
1030
1336
  }
1031
1337
  }
@@ -1045,11 +1351,14 @@ async function createBridgedAgentSession(host, options) {
1045
1351
  if (typeof agents.resume !== "function") throw new Error("pi2dsh: reopening a child conversation needs the DSH agent registry's persisted-resume seam, which this composition does not provide");
1046
1352
  handle = await agents.resume({
1047
1353
  resumeSessionId,
1354
+ ...parentOwnership,
1048
1355
  ...agentOptionsFragment,
1049
- setup
1356
+ setup,
1357
+ ...options.signal === void 0 ? {} : { signal: options.signal }
1050
1358
  });
1051
1359
  } else handle = await agents.create({
1052
1360
  sessionId,
1361
+ ...parentOwnership,
1053
1362
  meta: {
1054
1363
  cwd: typeof options.cwd === "string" ? options.cwd : host.cwd(),
1055
1364
  origin: "subagent",
@@ -1058,7 +1367,8 @@ async function createBridgedAgentSession(host, options) {
1058
1367
  ...typeof composedPreset === "string" && composedPreset.length > 0 ? { agentPreset: composedPreset } : {}
1059
1368
  },
1060
1369
  ...agentOptionsFragment,
1061
- setup
1370
+ setup,
1371
+ ...options.signal === void 0 ? {} : { signal: options.signal }
1062
1372
  });
1063
1373
  } catch (error) {
1064
1374
  if (resumeSessionId !== void 0) {
@@ -1068,7 +1378,7 @@ async function createBridgedAgentSession(host, options) {
1068
1378
  }
1069
1379
  throw new Error(`pi2dsh: reopening child session ${JSON.stringify(resumeSessionId)} failed — this composition may lack session persistence (${error instanceof Error ? error.message : String(error)})`);
1070
1380
  }
1071
- 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)})`);
1381
+ throw new Error(`pi2dsh: native subagent creation failed (${error instanceof Error ? error.message : String(error)})`, { cause: error });
1072
1382
  }
1073
1383
  const tools = [...Array.isArray(options.tools) ? options.tools : [], ...Array.isArray(options.customTools) ? options.customTools : []];
1074
1384
  const childSession = handle.agent.session;
@@ -2021,6 +2331,7 @@ var ModelCatalog = class {
2021
2331
  if (exact.contextWindow !== void 0) model.contextWindow = exact.contextWindow;
2022
2332
  if (exact.maxTokens !== void 0) model.maxTokens = exact.maxTokens;
2023
2333
  model.reasoning = exact.reasoning;
2334
+ if (exact.thinkingLevelMap !== void 0) model.thinkingLevelMap = exact.thinkingLevelMap;
2024
2335
  resolved.set(`${model.provider}${model.id}`, model);
2025
2336
  }));
2026
2337
  this.#models = next;
@@ -2046,7 +2357,17 @@ var ModelCatalog = class {
2046
2357
  const defaultMaxTokens = info.defaultMaxTokens;
2047
2358
  if (typeof defaultMaxTokens === "number") model.maxTokens = defaultMaxTokens;
2048
2359
  const reasoning = info.reasoning;
2049
- model.reasoning = Array.isArray(reasoning?.efforts) && reasoning.efforts.length > 0;
2360
+ const efforts = new Set((reasoning?.efforts ?? []).map((effort) => String(effort?.id ?? "")));
2361
+ model.reasoning = [...efforts].some((effort) => effort !== "" && effort !== "off");
2362
+ if (efforts.size > 0) model.thinkingLevelMap = Object.fromEntries([
2363
+ "off",
2364
+ "minimal",
2365
+ "low",
2366
+ "medium",
2367
+ "high",
2368
+ "xhigh",
2369
+ "max"
2370
+ ].map((level) => [level, efforts.has(level) ? level : null]));
2050
2371
  return model;
2051
2372
  } catch {
2052
2373
  return;
@@ -2171,6 +2492,7 @@ function streamViaDshLlm(llm, request) {
2171
2492
  ...Array.isArray(request.context.tools) ? { tools: normalizeToolSchemas(request.context.tools) } : {},
2172
2493
  ...typeof options.maxTokens === "number" ? { maxTokens: options.maxTokens } : {},
2173
2494
  ...typeof options.temperature === "number" ? { temperature: options.temperature } : {},
2495
+ ...typeof options.reasoning === "string" ? { reasoningEffort: options.reasoning } : {},
2174
2496
  ...options.signal instanceof AbortSignal ? { signal: options.signal } : {}
2175
2497
  };
2176
2498
  (async () => {
@@ -2378,15 +2700,21 @@ function normalizeToolSchemas(tools) {
2378
2700
  /** DSH GenerateOptions parts → the Pi request context a provider.stream expects. */
2379
2701
  async function dshRequestToPiContext(options, attachments) {
2380
2702
  const piMessages = [];
2703
+ const messages = Array.isArray(options.messages) ? options.messages : [];
2704
+ const first = messages[0];
2705
+ const hasExplicitSystem = typeof options.system === "string";
2706
+ const systemPrompt = hasExplicitSystem ? options.system : first?.role === "system" ? (Array.isArray(first.content) ? first.content : []).filter((block) => block.type === "text").map((block) => String(block.text ?? "")).join("\n") : void 0;
2707
+ const history = !hasExplicitSystem && first?.role === "system" ? messages.slice(1) : messages;
2381
2708
  const toolNamesById = /* @__PURE__ */ new Map();
2382
- for (const message of Array.isArray(options.messages) ? options.messages : []) {
2709
+ for (const message of history) {
2383
2710
  if (message.role !== "assistant") continue;
2384
2711
  for (const block of Array.isArray(message.content) ? message.content : []) if (block.type === "tool-call" && typeof block.id === "string" && typeof block.name === "string") toolNamesById.set(block.id, block.name);
2385
2712
  }
2386
- for (const message of Array.isArray(options.messages) ? options.messages : []) {
2713
+ for (const message of history) {
2387
2714
  const content = Array.isArray(message.content) ? message.content : [];
2388
2715
  const source = message.source;
2389
- if (message.role === "user" && source?.kind === "tool") {
2716
+ if (message.role === "system") throw new Error("pi2dsh: this Pi provider cannot represent an in-history system prompt update");
2717
+ else if (message.role === "user" && source?.kind === "tool") {
2390
2718
  const result = content[0];
2391
2719
  const toolCallId = String(result?.toolCallId ?? "");
2392
2720
  const toolName = toolNamesById.get(toolCallId);
@@ -2407,7 +2735,7 @@ async function dshRequestToPiContext(options, attachments) {
2407
2735
  });
2408
2736
  }
2409
2737
  return {
2410
- ...typeof options.system === "string" && options.system.length > 0 ? { systemPrompt: options.system } : {},
2738
+ ...systemPrompt !== void 0 && systemPrompt.length > 0 ? { systemPrompt } : {},
2411
2739
  messages: piMessages,
2412
2740
  ...Array.isArray(options.tools) ? { tools: options.tools } : {}
2413
2741
  };
@@ -4040,9 +4368,10 @@ function isSubagentOrigin(subject) {
4040
4368
  function currentPiModel(state, agent) {
4041
4369
  const override = state.modelOverrides.get(agent);
4042
4370
  const options = agent.options;
4043
- const selectedProvider = String(override?.provider ?? options?.provider ?? "");
4371
+ const durable = lastRequestRouteOf(agentSession(agent));
4372
+ const selectedProvider = String(override?.provider ?? durable?.provider ?? options?.provider ?? "");
4044
4373
  const provider = state.companionRoutes.get(selectedProvider) ?? selectedProvider;
4045
- const id = String(override?.model ?? options?.model ?? "");
4374
+ const id = String(override?.model ?? durable?.model ?? options?.model ?? "");
4046
4375
  if (id.length === 0) return override;
4047
4376
  return (provider.length > 0 ? state.modelCatalog?.find(provider, id) : void 0) ?? {
4048
4377
  id,
@@ -4093,9 +4422,7 @@ function resolveCallerRoute(override, durable, snapshot) {
4093
4422
  }
4094
4423
  /** A session's durable events, whether the store exposes them as value or method. */
4095
4424
  function eventsOf(session) {
4096
- const events = session.events;
4097
- if (typeof events === "function") return events.call(session) ?? [];
4098
- return Array.isArray(events) ? events : [];
4425
+ return readSessionEvents(session);
4099
4426
  }
4100
4427
  /** The session id a presentation call belongs to, or '' outside a session. */
4101
4428
  function sessionIdOf(state, agent) {
@@ -4295,6 +4622,16 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride,
4295
4622
  if (headlessUi) return void 0;
4296
4623
  const surfaces = state.tuiSurfaces;
4297
4624
  if (surfaces !== void 0) return surfaces.custom(factory, state.theme, getKeybindings(), options);
4625
+ if (humanAnswererAvailable(userQuestions, agent, state.shared)) {
4626
+ const gap = {
4627
+ capability: "ui.custom",
4628
+ reason: "native dialogs are available, but this surface has no terminal-component renderer.",
4629
+ guidance: "Use the package's non-terminal fallback or a DSH terminal surface.",
4630
+ packageName: state.packageName
4631
+ };
4632
+ capabilityLedgerOf(ctx, state).reportDegraded(gap);
4633
+ throw new PiCapabilityError(gap);
4634
+ }
4298
4635
  },
4299
4636
  pasteToEditor(text) {
4300
4637
  const surfaces = state.shared.browserSurfaces;
@@ -4516,7 +4853,10 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride,
4516
4853
  reason: "pi2dsh: aborted by migrated Pi extension"
4517
4854
  });
4518
4855
  },
4519
- hasPendingMessages: () => agent?.inbox?.hasPending === true,
4856
+ hasPendingMessages: () => {
4857
+ const inbox = agent?.inbox;
4858
+ return inbox?.nextStep !== void 0 || inbox?.nextTurn !== void 0 ? (inbox.nextStep?.length ?? 0) + (inbox.nextTurn?.length ?? 0) > 0 : inbox?.hasPending === true;
4859
+ },
4520
4860
  shutdown: () => {
4521
4861
  capabilityLedgerOf(ctx, state).reportHostDecision({
4522
4862
  capability: "ctx.shutdown",
@@ -4617,7 +4957,7 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride,
4617
4957
  const seq = durableSeqOf(String(entryId));
4618
4958
  if (seq === void 0) throw new Error(`pi2dsh: ctx.fork(${JSON.stringify(String(entryId))}) — only durable-log entries (projected ids "dsh-<seq>") can anchor a fork; package-appended sidecar entries are not part of the DSH durable log`);
4619
4959
  const position = options?.position ?? "before";
4620
- const events = source.events ?? [];
4960
+ const events = readSessionEvents(source);
4621
4961
  const requested = Math.min(position === "at" ? seq : seq - 1, events.length - 1);
4622
4962
  const boundary = requested < 0 ? -1 : shrinkToTurnBoundary(events, requested);
4623
4963
  const child = boundary < 0 ? sessions.create(void 0, { meta: {
@@ -4648,7 +4988,7 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride,
4648
4988
  }
4649
4989
  const seq = durableSeqOf(String(targetId));
4650
4990
  if (seq === void 0) throw new Error(`pi2dsh: ctx.navigateTree(${JSON.stringify(String(targetId))}) — only durable-log entries (projected ids "dsh-<seq>") can be navigation targets; package-appended sidecar entries are not part of the DSH durable log`);
4651
- const events = source.events ?? [];
4991
+ const events = readSessionEvents(source);
4652
4992
  const capped = Math.min(seq, events.length - 1);
4653
4993
  const boundary = capped < 0 ? -1 : shrinkToTurnBoundary(events, capped);
4654
4994
  const child = boundary < 0 ? sessions.create(void 0, { meta: {
@@ -4681,7 +5021,7 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride,
4681
5021
  }
4682
5022
  const raw = String(sessionPath);
4683
5023
  const base = raw.replace(/\\/g, "/").split("/").pop() ?? raw;
4684
- const candidate = base.endsWith(".jsonl") ? base.slice(0, -6) : base;
5024
+ const candidate = state.bridge.sessionIdOfArchiveFile(raw) ?? (base.endsWith(".jsonl") ? base.slice(0, -6) : base);
4685
5025
  const target = sessions.get(candidate) ?? sessions.list().find((entry) => String(entry.id) === candidate);
4686
5026
  if (target === void 0) throw new Error(`pi2dsh: ctx.switchSession(${JSON.stringify(raw)}) — no live DSH session ${JSON.stringify(candidate)}; switching to persisted sessions is host-owned (resume them from the DSH surface first)`);
4687
5027
  const withSession = options?.withSession;
@@ -4718,6 +5058,20 @@ async function dispatch(state, eventName, event, eventContext) {
4718
5058
  for (const handler of state.handlers.get(eventName) ?? []) results.push(await runInPiRuntime(state, agent, () => handler(event, eventContext)));
4719
5059
  return results;
4720
5060
  }
5061
+ function shutdownPiSession(ctx, state, agent, reason, signal) {
5062
+ const existing = state.shutdownTasks.get(agent);
5063
+ if (existing !== void 0) return existing;
5064
+ if (state.piShutdownAgents.has(agent)) return Promise.resolve();
5065
+ state.piShutdownAgents.add(agent);
5066
+ const task = dispatch(state, "session_shutdown", {
5067
+ type: "session_shutdown",
5068
+ reason
5069
+ }, contextFor(ctx, state, agent, signal)).then(() => {});
5070
+ state.shutdownTasks.set(agent, task);
5071
+ state.pendingShutdowns.add(task);
5072
+ task.then(() => state.pendingShutdowns.delete(task), () => state.pendingShutdowns.delete(task));
5073
+ return task;
5074
+ }
4721
5075
  /**
4722
5076
  * Bind every compatibility singleton to this exact Agent runtime for the
4723
5077
  * duration of extension-owned work. AsyncLocalStorage follows promises and
@@ -4809,7 +5163,8 @@ async function dshToPiContent(ctx, content) {
4809
5163
  else if (block.type === "image") {
4810
5164
  const image = await piImageBlock(ctx, block.attachment);
4811
5165
  if (image !== void 0) out.push(image);
4812
- } else out.push({ type: block.type });
5166
+ } else if (block.type === "file") out.push(fileContentForPi(block.attachment, ctx));
5167
+ else out.push({ type: block.type });
4813
5168
  return out;
4814
5169
  }
4815
5170
  /**
@@ -4839,7 +5194,11 @@ async function messageFromSessionEvent(ctx, event) {
4839
5194
  if (typeof data !== "object" || data === null) return void 0;
4840
5195
  const record = data;
4841
5196
  if (type === "user/message") return {
4842
- role: "user",
5197
+ role: isPiUserMessage(record) ? "user" : "custom",
5198
+ ...!isPiUserMessage(record) ? {
5199
+ customType: String(record.source?.piCustomType ?? record.source?.kind ?? "context"),
5200
+ display: false
5201
+ } : {},
4843
5202
  content: await dshToPiContent(ctx, record.content ?? [])
4844
5203
  };
4845
5204
  if (type === "assistant/message") {
@@ -4937,6 +5296,20 @@ function untrackRuntimeSession(state, agent) {
4937
5296
  states?.delete(state);
4938
5297
  if (states?.size === 0) state.shared.runtimeStatesBySession.delete(id);
4939
5298
  }
5299
+ const sessionExportTasks = /* @__PURE__ */ new WeakMap();
5300
+ async function prepareSessionFileExports(ctx, state) {
5301
+ const persistence = optionalService(ctx, "sessionPersistence");
5302
+ if (typeof persistence?.list !== "function" || typeof persistence.open !== "function" && typeof persistence.inspect !== "function") return;
5303
+ let task = sessionExportTasks.get(ctx.root);
5304
+ if (task === void 0) {
5305
+ task = state.bridge.exportStoredSessions(persistence);
5306
+ sessionExportTasks.set(ctx.root, task);
5307
+ task.catch(() => {
5308
+ sessionExportTasks.delete(ctx.root);
5309
+ });
5310
+ }
5311
+ await task;
5312
+ }
4940
5313
  /** Dispatch or join the one in-flight Pi session_start for this session. */
4941
5314
  async function startPiSession(ctx, state, agent, reason, signal) {
4942
5315
  if (!acceptsAgent(state, agent)) return;
@@ -4949,24 +5322,24 @@ async function startPiSession(ctx, state, agent, reason, signal) {
4949
5322
  const previous = state.hostAgent;
4950
5323
  const replacing = typeof previous === "object" && previous !== null && previous !== agent;
4951
5324
  const transitionReason = replacing ? replacementReason(reason) : reason;
4952
- if (replacing && !state.piShutdownAgents.has(previous)) {
4953
- state.piShutdownAgents.add(previous);
5325
+ if (replacing) {
4954
5326
  releasePiSessionClaim(state, previous);
4955
- await runInPiRuntime(state, previous, () => dispatch(state, "session_shutdown", {
4956
- type: "session_shutdown",
4957
- reason: replacementReason(reason)
4958
- }, contextFor(ctx, state, previous, signal)));
5327
+ await shutdownPiSession(ctx, state, previous, replacementReason(reason), signal);
4959
5328
  }
4960
5329
  if (typeof agent === "object" && agent !== null) {
4961
5330
  state.hostAgent = agent;
4962
5331
  state.piShutdownAgents.delete(agent);
5332
+ state.shutdownTasks.delete(agent);
4963
5333
  }
4964
5334
  if (!claimPiSessionStart(state, agent)) return;
4965
5335
  try {
5336
+ await prepareSessionFileExports(ctx, state);
4966
5337
  await dispatch(state, "session_start", {
4967
5338
  type: "session_start",
4968
5339
  reason: transitionReason
4969
5340
  }, contextFor(ctx, state, agent, signal));
5341
+ await discoverPiResources(ctx, state, agent, transitionReason, signal);
5342
+ await refreshNativeSkillCommands(ctx, state, agent, signal);
4970
5343
  } catch (error) {
4971
5344
  releasePiSessionClaim(state, agent);
4972
5345
  throw error;
@@ -5011,15 +5384,65 @@ function subscribeLifecycle(ctx, state) {
5011
5384
  state.disposedAgents.add(agent);
5012
5385
  releasePiSessionClaim(state, agent);
5013
5386
  if (ownsHostSession && !state.piShutdownAgents.has(agent)) {
5014
- state.piShutdownAgents.add(agent);
5015
5387
  state.hostAgent = void 0;
5016
- dispatch(state, "session_shutdown", {
5017
- type: "session_shutdown",
5018
- reason: "quit"
5019
- }, contextFor(ctx, state, agent, void 0)).catch((error) => warn("session_shutdown", error));
5388
+ shutdownPiSession(ctx, state, agent, "quit").catch((error) => warn("session_shutdown", error));
5020
5389
  }
5021
5390
  }
5022
5391
  });
5392
+ const liveStreams = /* @__PURE__ */ new WeakMap();
5393
+ const projectAssistantChunk = (session, data, chunk, eventContext) => {
5394
+ if ((state.handlers.get("message_update")?.length ?? 0) === 0) return;
5395
+ if (chunk.type !== void 0 && chunk.type !== "text-delta") return;
5396
+ const key = `${String(session.id ?? "")}:${String(data.turn ?? 0)}:${String(data.step ?? 0)}`;
5397
+ const delta = typeof chunk.text === "string" ? chunk.text : typeof chunk.delta === "string" ? chunk.delta : "";
5398
+ const accumulated = (state.streamingTexts.get(key) ?? "") + delta;
5399
+ state.streamingTexts.set(key, accumulated);
5400
+ const message = {
5401
+ role: "assistant",
5402
+ content: [{
5403
+ type: "text",
5404
+ text: accumulated
5405
+ }]
5406
+ };
5407
+ dispatch(state, "message_update", {
5408
+ type: "message_update",
5409
+ message,
5410
+ assistantMessageEvent: {
5411
+ type: "text_delta",
5412
+ contentIndex: 0,
5413
+ delta,
5414
+ partial: message
5415
+ }
5416
+ }, eventContext).catch((error) => warn("message_update", error));
5417
+ };
5418
+ cordis.on("agent/assistant-stream", (payload) => {
5419
+ const agent = payload.agent;
5420
+ if (agent === void 0 || state.hostAnchor || !acceptsAgent(state, agent)) return;
5421
+ const session = agentSession(agent);
5422
+ if (session === void 0) return;
5423
+ const frame = payload.frame;
5424
+ if (frame.type === "start") {
5425
+ liveStreams.set(session, {
5426
+ attemptId: frame.attemptId,
5427
+ turn: frame.turn,
5428
+ step: frame.step,
5429
+ index: -1,
5430
+ ended: false
5431
+ });
5432
+ state.streamingTexts.delete(`${String(session.id ?? "")}:${String(frame.turn ?? 0)}:${String(frame.step ?? 0)}`);
5433
+ return;
5434
+ }
5435
+ const active = liveStreams.get(session);
5436
+ if (active === void 0 || active.ended || active.attemptId !== frame.attemptId) return;
5437
+ if (frame.type === "chunk" && Number(frame.index) > active.index) {
5438
+ active.index = Number(frame.index);
5439
+ projectAssistantChunk(session, active, frame.chunk, contextFor(ctx, state, agent, void 0));
5440
+ }
5441
+ if (frame.type === "end") {
5442
+ active.ended = true;
5443
+ state.streamingTexts.delete(`${String(session.id ?? "")}:${String(active.turn ?? 0)}:${String(active.step ?? 0)}`);
5444
+ }
5445
+ });
5023
5446
  const projectSessionEvent = (session, event) => {
5024
5447
  const agent = [...state.activeAgents].find((candidate) => candidate.session === session);
5025
5448
  const eventContext = contextFor(ctx, state, agent, void 0);
@@ -5041,7 +5464,7 @@ function subscribeLifecycle(ctx, state) {
5041
5464
  const index = state.piTurnIndex.get(key) ?? 0;
5042
5465
  state.piTurnIndex.set(key, index + 1);
5043
5466
  state.projection = state.projection.then(async () => {
5044
- const stepEvents = (session.events ?? []).filter((entry) => {
5467
+ const stepEvents = readSessionEvents(session).filter((entry) => {
5045
5468
  const entryData = entry.data;
5046
5469
  return Number(entryData?.turn ?? -1) === turn && Number(entryData?.step ?? -1) === step;
5047
5470
  });
@@ -5074,27 +5497,12 @@ function subscribeLifecycle(ctx, state) {
5074
5497
  type: "tool_execution_start",
5075
5498
  toolCallId: data.callId,
5076
5499
  toolName: data.name,
5077
- args
5500
+ args: piViewOfToolArguments(state, String(data.name), args)
5078
5501
  }, eventContext).catch((error) => warn("tool_execution_start", error));
5079
5502
  }
5080
- if (type === "assistant/chunk" && (state.handlers.get("message_update")?.length ?? 0) > 0) {
5503
+ if (type === "assistant/chunk" && !liveStreams.has(session)) {
5081
5504
  const data = event.data;
5082
- const chunk = data.chunk ?? {};
5083
- const key = `${String(session.id ?? "")}:${String(data.turn ?? 0)}:${String(data.step ?? 0)}`;
5084
- const delta = typeof chunk.text === "string" ? chunk.text : typeof chunk.delta === "string" ? chunk.delta : "";
5085
- const accumulated = (state.streamingTexts.get(key) ?? "") + delta;
5086
- state.streamingTexts.set(key, accumulated);
5087
- dispatch(state, "message_update", {
5088
- type: "message_update",
5089
- message: {
5090
- role: "assistant",
5091
- content: [{
5092
- type: "text",
5093
- text: accumulated
5094
- }]
5095
- },
5096
- assistantMessageEvent: chunk
5097
- }, eventContext).catch((error) => warn("message_update", error));
5505
+ projectAssistantChunk(session, data, data.chunk ?? {}, eventContext);
5098
5506
  }
5099
5507
  if (type === "assistant/message") {
5100
5508
  const data = event.data;
@@ -5107,18 +5515,21 @@ function subscribeLifecycle(ctx, state) {
5107
5515
  name: typeof data.title === "string" ? data.title : void 0
5108
5516
  }, eventContext).catch((error) => warn("session_info_changed", error));
5109
5517
  }
5110
- if (type === "compaction/start") dispatch(state, "session_before_compact", {
5111
- type: "session_before_compact",
5112
- preparation: { ...event.data },
5113
- branchEntries: [],
5114
- reason: compactionReason(event.data),
5115
- willRetry: false
5116
- }, eventContext).catch((error) => warn("session_before_compact", error));
5518
+ if (type === "compaction/start") {
5519
+ const before = dispatch(state, "session_before_compact", {
5520
+ type: "session_before_compact",
5521
+ preparation: { ...event.data },
5522
+ branchEntries: [],
5523
+ reason: compactionReason(event.data),
5524
+ willRetry: false
5525
+ }, eventContext).catch((error) => warn("session_before_compact", error));
5526
+ state.projection = Promise.all([state.projection, before]).then(() => {});
5527
+ }
5117
5528
  if (type === "compaction/summary") {
5118
5529
  const data = event.data;
5119
5530
  const shadowed = data.shadowedRange;
5120
5531
  const usage = data.usage;
5121
- dispatch(state, "session_compact", {
5532
+ state.projection = state.projection.then(() => dispatch(state, "session_compact", {
5122
5533
  type: "session_compact",
5123
5534
  compactionEntry: {
5124
5535
  type: "compaction",
@@ -5132,7 +5543,7 @@ function subscribeLifecycle(ctx, state) {
5132
5543
  fromExtension: false,
5133
5544
  reason: compactionReason(data),
5134
5545
  willRetry: false
5135
- }, eventContext).catch((error) => warn("session_compact", error));
5546
+ }, eventContext)).then(() => {}).catch((error) => warn("session_compact", error));
5136
5547
  }
5137
5548
  if (type === "request/header") {
5138
5549
  const header = event.data.header ?? {};
@@ -5187,19 +5598,17 @@ function subscribeLifecycle(ctx, state) {
5187
5598
  ...config,
5188
5599
  ...override?.provider === void 0 ? {} : { provider: override.provider },
5189
5600
  ...override?.model === void 0 ? {} : { model: override.model },
5190
- ...thinking === void 0 || thinking === "off" ? {} : { reasoningEffort: thinking }
5601
+ ...thinking === void 0 || thinking === "off" && (currentPiModel(state, agent)?.thinkingLevelMap)?.off === null ? {} : { reasoningEffort: thinking }
5191
5602
  };
5192
5603
  });
5193
5604
  cordis.effect(() => async () => {
5605
+ await state.projection;
5194
5606
  const agent = state.hostAgent;
5195
- if (typeof agent === "object" && agent !== null && !state.piShutdownAgents.has(agent)) {
5196
- state.piShutdownAgents.add(agent);
5607
+ if (typeof agent === "object" && agent !== null) {
5197
5608
  state.disposedAgents.add(agent);
5198
- await dispatch(state, "session_shutdown", {
5199
- type: "session_shutdown",
5200
- reason: "quit"
5201
- }, contextFor(ctx, state, agent, void 0));
5609
+ await shutdownPiSession(ctx, state, agent, "quit");
5202
5610
  }
5611
+ await Promise.all(state.pendingShutdowns);
5203
5612
  state.hostAgent = void 0;
5204
5613
  for (const active of state.activeAgents) untrackRuntimeSession(state, active);
5205
5614
  state.activeAgents.clear();
@@ -5230,7 +5639,7 @@ async function flushPendingSessionStarts(ctx, state) {
5230
5639
  if (state.ownerAgent !== void 0 && state.projectSessionEvent !== void 0) {
5231
5640
  const session = agentSession(state.ownerAgent);
5232
5641
  if (session !== void 0) {
5233
- const events = session.events ?? [];
5642
+ const events = readSessionEvents(session);
5234
5643
  let from = -1;
5235
5644
  for (let index = events.length - 1; index >= 0; index -= 1) {
5236
5645
  if (events[index].type === "turn/end") break;
@@ -5276,13 +5685,144 @@ async function restartReloadedPiSessions(ctx, state) {
5276
5685
  type: "session_start",
5277
5686
  reason: pending.get(agent) ?? "resume"
5278
5687
  }, contextFor(ctx, state, agent, void 0)));
5688
+ await discoverPiResources(ctx, state, agent, pending.get(agent) ?? "resume", void 0);
5689
+ await refreshNativeSkillCommands(ctx, state, agent, void 0);
5279
5690
  }
5280
5691
  }
5692
+ /**
5693
+ * Pi's `resources_discover`, fired right after session_start (Pi:
5694
+ * AgentSession.extendResourcesFromExtensions): handlers return extra
5695
+ * `skillPaths` (and prompt/theme paths) for the session's cwd, and Pi extends
5696
+ * its resource loader with them. On DSH the skills registry is the host's
5697
+ * `skills` service, so every discovered skill ROOT is handed to the official
5698
+ * filesystem provider (dsh-skill-filesystem) exactly like a package's static
5699
+ * skill directories — once per root, per package (the registry is host-level
5700
+ * and provider names must stay unique). Prompt templates and TUI themes have
5701
+ * no DSH seat and are reported, not silently dropped. A single SKILL.md file
5702
+ * path (Pi accepts those too) has no root to mount and is reported likewise.
5703
+ */
5704
+ async function refreshNativeSkillCommands(ctx, state, agent, signal) {
5705
+ const skills = optionalService(ctx, "skills");
5706
+ if (skills === void 0) {
5707
+ state.nativeSkillCommands = [];
5708
+ return;
5709
+ }
5710
+ const options = {
5711
+ cwd: cwdOf(agent),
5712
+ scope: scopeOf(ctx),
5713
+ signal: signal ?? new AbortController().signal
5714
+ };
5715
+ try {
5716
+ const entries = [];
5717
+ for (const summary of await skills.list(options)) {
5718
+ if (summary.invocation?.userInvocable === false) continue;
5719
+ const skill = await skills.get(String(summary.name), options);
5720
+ if (typeof skill?.path !== "string") continue;
5721
+ entries.push({
5722
+ name: `skill:${String(summary.name)}`,
5723
+ description: String(summary.description ?? ""),
5724
+ source: "skill",
5725
+ sourceInfo: {
5726
+ path: skill.path,
5727
+ source: skill.path,
5728
+ scope: String(summary.source ?? "").startsWith("project") ? "project" : "user",
5729
+ origin: "top-level",
5730
+ baseDir: dirname(skill.path)
5731
+ }
5732
+ });
5733
+ }
5734
+ state.nativeSkillCommands = entries;
5735
+ } catch (error) {
5736
+ state.nativeSkillCommands = [];
5737
+ logger(ctx).warn(`[pi2dsh] skill command metadata could not be refreshed: ${error instanceof Error ? error.message : String(error)}`);
5738
+ }
5739
+ }
5740
+ async function discoverPiResources(ctx, state, agent, reason, signal) {
5741
+ if ((state.handlers.get("resources_discover")?.length ?? 0) === 0) return;
5742
+ const cwd = cwdOf(agent);
5743
+ const results = await dispatch(state, "resources_discover", {
5744
+ type: "resources_discover",
5745
+ cwd,
5746
+ reason: reason === "reload" ? "reload" : "startup"
5747
+ }, contextFor(ctx, state, agent, signal));
5748
+ const roots = [];
5749
+ const skipped = [];
5750
+ let promptPaths = 0;
5751
+ let themePaths = 0;
5752
+ for (const result of results) {
5753
+ if (typeof result !== "object" || result === null) continue;
5754
+ const record = result;
5755
+ for (const entry of Array.isArray(record.skillPaths) ? record.skillPaths : []) {
5756
+ if (typeof entry !== "string" || entry.length === 0) continue;
5757
+ const absolute = resolve(cwd, entry);
5758
+ let isDirectory = false;
5759
+ try {
5760
+ isDirectory = (await stat(absolute)).isDirectory();
5761
+ } catch {
5762
+ continue;
5763
+ }
5764
+ if (!isDirectory) {
5765
+ skipped.push(absolute);
5766
+ continue;
5767
+ }
5768
+ if (!state.discoveredSkillRoots.has(absolute) && !roots.includes(absolute)) roots.push(absolute);
5769
+ }
5770
+ if (Array.isArray(record.promptPaths)) promptPaths += record.promptPaths.length;
5771
+ if (Array.isArray(record.themePaths)) themePaths += record.themePaths.length;
5772
+ }
5773
+ if (promptPaths > 0 || themePaths > 0 || skipped.length > 0) logger(ctx).info(`[pi2dsh] ${state.packageName}: resources_discover returned ${promptPaths} prompt path(s), ${themePaths} theme path(s) and ${skipped.length} single skill file(s) that DSH has no seat for; they were not mounted`);
5774
+ if (roots.length === 0) return;
5775
+ if (ctx.get("skills") === void 0) {
5776
+ logger(ctx).warn(`[pi2dsh] ${state.packageName}: resources_discover found ${roots.length} skill root(s) but this DSH composition has no ctx.skills`);
5777
+ return;
5778
+ }
5779
+ for (const root of roots) state.discoveredSkillRoots.add(root);
5780
+ state.discoveredSkillProviders += 1;
5781
+ const { apply: applyFilesystemSkills } = await import("@deepseek-ai/dsh-skill-filesystem");
5782
+ const config = {
5783
+ providerName: `pi2dsh-${state.packageName.replace(/[^a-zA-Z0-9_-]+/gu, "-").replace(/^-+|-+$/gu, "")}-discovered-${state.discoveredSkillProviders}`,
5784
+ includeDefaultRoots: false,
5785
+ customSkillDirs: roots,
5786
+ watch: false
5787
+ };
5788
+ await ctx.plugin(Object.assign((skillCtx) => applyFilesystemSkills(skillCtx, config), { inject: ["skills"] }));
5789
+ logger(ctx).info(`[pi2dsh] ${state.packageName}: resources_discover mounted ${roots.length} skill root(s) as ${config.providerName}`);
5790
+ }
5791
+ /**
5792
+ * The Pi view of a host tool's arguments. DSH's built-in file tools carry
5793
+ * Claude-style argument names (`file_path`, `old_string`/`new_string`,
5794
+ * grep's `include`); Pi's same-named built-ins carry `path`,
5795
+ * `oldText`/`newText`, `glob`. Extensions read `event.input` by Pi's names —
5796
+ * pi-code attaches a path-scoped rule to the read of a matching `input.path`,
5797
+ * and its hooks translate Pi shapes to Claude's `tool_input` — so a host call
5798
+ * is projected into Pi's shape for the tool_call / tool_result /
5799
+ * tool_execution_start events. Migrated Pi tools already speak Pi and pass
5800
+ * through untouched; unknown keys are kept, so nothing is lost.
5801
+ */
5802
+ const HOST_TOOL_ARGUMENT_PROJECTION = {
5803
+ read: { file_path: "path" },
5804
+ write: { file_path: "path" },
5805
+ edit: {
5806
+ file_path: "path",
5807
+ old_string: "oldText",
5808
+ new_string: "newText"
5809
+ },
5810
+ grep: { include: "glob" }
5811
+ };
5812
+ function piViewOfToolArguments(state, name, args) {
5813
+ if (state.tools.has(name)) return cloneJson(args);
5814
+ const mapping = HOST_TOOL_ARGUMENT_PROJECTION[name];
5815
+ if (mapping === void 0 || typeof args !== "object" || args === null || Array.isArray(args)) return cloneJson(args);
5816
+ const projected = {};
5817
+ for (const [key, value] of Object.entries(args)) projected[mapping[key] ?? key] = value;
5818
+ return cloneJson(projected);
5819
+ }
5281
5820
  function subscribeInterceptors(ctx, state) {
5282
5821
  const cordis = ctx;
5283
5822
  cordis.on("tools/pre-execute", async (exec, next) => {
5284
5823
  if (!acceptsAgent(state, exec.agent)) return next();
5285
- const input = cloneJson(exec.arguments);
5824
+ const original = piViewOfToolArguments(state, exec.name, exec.arguments);
5825
+ const input = cloneJson(original);
5286
5826
  const event = {
5287
5827
  type: "tool_call",
5288
5828
  toolName: exec.name,
@@ -5290,7 +5830,7 @@ function subscribeInterceptors(ctx, state) {
5290
5830
  input
5291
5831
  };
5292
5832
  const results = await dispatch(state, "tool_call", event, contextFor(ctx, state, exec.agent, exec.signal));
5293
- if (!jsonEqual(event.input, exec.arguments)) {
5833
+ if (!jsonEqual(event.input, original)) {
5294
5834
  if (state.tools.has(exec.name)) state.argMutations.set(exec, cloneJson(event.input));
5295
5835
  else return {
5296
5836
  kind: "deny",
@@ -5336,7 +5876,7 @@ function subscribeInterceptors(ctx, state) {
5336
5876
  type: "tool_result",
5337
5877
  toolName: exec.name,
5338
5878
  toolCallId: exec.callId,
5339
- input: cloneJson(exec.arguments),
5879
+ input: piViewOfToolArguments(state, exec.name, exec.arguments),
5340
5880
  content: await dshToPiContent(ctx, result.content),
5341
5881
  details: result.meta ?? null,
5342
5882
  isError: result.isError,
@@ -5471,18 +6011,20 @@ async function runBeforeAgentStart(ctx, state, agent, signal, assembled) {
5471
6011
  prompt,
5472
6012
  ...images.length > 0 ? { images } : {},
5473
6013
  systemPrompt: assembled,
5474
- systemPromptOptions: {}
6014
+ systemPromptOptions: await piSystemPromptOptions(ctx, state, agent, claimed.map((entry) => entry.message))
5475
6015
  };
5476
- const results = await dispatch(state, "before_agent_start", event, contextFor(ctx, state, agent, signal));
6016
+ const eventContext = contextFor(ctx, state, agent, signal);
6017
+ const results = [];
6018
+ for (const handler of state.handlers.get("before_agent_start") ?? []) {
6019
+ const result = await runInPiRuntime(state, agent, () => handler(event, eventContext));
6020
+ results.push(result);
6021
+ if (typeof result === "object" && result !== null && typeof result.systemPrompt === "string") event.systemPrompt = result.systemPrompt;
6022
+ }
6023
+ if (event.systemPrompt !== assembled) state.turnSystemPromptOverrides.set(agent, event.systemPrompt);
5477
6024
  const injected = [];
5478
6025
  for (const result of results) {
5479
6026
  if (typeof result !== "object" || result === null) continue;
5480
- const record = result;
5481
- if (typeof record.systemPrompt === "string") {
5482
- state.turnSystemPromptOverrides.set(agent, record.systemPrompt);
5483
- event.systemPrompt = record.systemPrompt;
5484
- }
5485
- const message = record.message;
6027
+ const message = result.message;
5486
6028
  if (message === void 0) continue;
5487
6029
  const content = message.content;
5488
6030
  const blocks = await piToDshContent(ctx, typeof content === "string" ? [{
@@ -5500,6 +6042,79 @@ async function runBeforeAgentStart(ctx, state, agent, signal, assembled) {
5500
6042
  }
5501
6043
  if (injected.length > 0) state.pendingInjections.set(agent, injected);
5502
6044
  }
6045
+ /**
6046
+ * Pi's `BuildSystemPromptOptions` for the before_agent_start event: the
6047
+ * structured view of "what the host loaded", which extensions read instead of
6048
+ * re-discovering resources. Two fields carry weight on DSH:
6049
+ * - `cwd`: the session's working directory (pi-code's context-imports
6050
+ * otherwise falls back to process.cwd(), the CLI's directory).
6051
+ * - `contextFiles`: the AGENTS.md/CLAUDE.md chain the host loaded, as
6052
+ * `{path, content}`. DSH delivers those as an `agent-instructions` user
6053
+ * message rather than system-prompt text, so the set is recovered from the
6054
+ * official loader itself: the baseline message records the exact
6055
+ * discovery/budget identity DSH used, and `loadBaselineInstructionSet`
6056
+ * from `@deepseek-ai/dsh-agent-instructions` re-runs that same walk. No
6057
+ * parallel discovery logic lives here; without the package or a baseline
6058
+ * the list is empty, exactly Pi's value when nothing was loaded.
6059
+ * (pi-code's CLAUDE.md `@import` expansion reads this list — with `{}` it
6060
+ * had nothing to expand and the model "found" the imported file with `read`.)
6061
+ */
6062
+ async function piSystemPromptOptions(ctx, state, agent, claimed) {
6063
+ const cwd = cwdOf(agent);
6064
+ const baselineOf = (message) => {
6065
+ const source = message?.source;
6066
+ return source?.kind === "agent-instructions" && source.baseline === true ? source : void 0;
6067
+ };
6068
+ let baseline;
6069
+ for (const message of claimed) {
6070
+ baseline = baselineOf(message);
6071
+ if (baseline !== void 0) break;
6072
+ }
6073
+ if (baseline === void 0) {
6074
+ const events = readSessionEvents(agentSession(agent));
6075
+ for (let index = events.length - 1; index >= 0 && baseline === void 0; index -= 1) {
6076
+ const event = events[index];
6077
+ const data = event.data;
6078
+ if (event.type === "user/message") baseline = baselineOf(data);
6079
+ else if (event.type === "agent/inbox/spliced" && Array.isArray(data?.inserted)) for (const inserted of data.inserted) {
6080
+ baseline = baselineOf(inserted);
6081
+ if (baseline !== void 0) break;
6082
+ }
6083
+ }
6084
+ }
6085
+ try {
6086
+ const identity = typeof baseline?.baselineIdentity === "string" ? JSON.parse(baseline.baselineIdentity) : {};
6087
+ const { discoverBaselineInstructionFiles } = await import("@deepseek-ai/dsh-agent-instructions");
6088
+ const list = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string") ? value : void 0;
6089
+ const discovered = await discoverBaselineInstructionFiles({
6090
+ cwd,
6091
+ ...typeof identity.projectRoot === "string" && identity.projectRoot.length > 0 ? { projectRoot: identity.projectRoot } : {},
6092
+ ...list(identity.projectRootMarkers) !== void 0 ? { projectRootMarkers: list(identity.projectRootMarkers) } : {},
6093
+ ...list(identity.instructionFileCandidates) !== void 0 ? { instructionFileCandidates: list(identity.instructionFileCandidates) } : {},
6094
+ ...list(identity.localInstructionFileCandidates) !== void 0 ? { localInstructionFileCandidates: list(identity.localInstructionFileCandidates) } : {}
6095
+ });
6096
+ const maxSourceBytes = typeof identity.maxSourceBytes === "number" ? identity.maxSourceBytes : 1048576;
6097
+ const contextFiles = [];
6098
+ for (const file of discovered) try {
6099
+ const bytes = await readFile(file.absolutePath);
6100
+ if (bytes.byteLength > maxSourceBytes) continue;
6101
+ contextFiles.push({
6102
+ path: file.absolutePath,
6103
+ content: bytes.toString("utf8")
6104
+ });
6105
+ } catch {}
6106
+ return {
6107
+ cwd,
6108
+ contextFiles
6109
+ };
6110
+ } catch (error) {
6111
+ logger(ctx).warn(`[pi2dsh] could not recover the host's instruction files for before_agent_start (contextFiles left empty): ${error instanceof Error ? error.message : String(error)}`);
6112
+ return {
6113
+ cwd,
6114
+ contextFiles: []
6115
+ };
6116
+ }
6117
+ }
5503
6118
  async function piShapeOfPending(ctx, message) {
5504
6119
  const source = message.source;
5505
6120
  const content = await dshToPiContent(ctx, message.content ?? []);
@@ -5516,7 +6131,7 @@ async function applyPiContextTransform(ctx, state, agent, signal, pending) {
5516
6131
  if ((state.handlers.get("context")?.length ?? 0) === 0) return pending;
5517
6132
  const session = agentSession(agent);
5518
6133
  const history = [];
5519
- for (const event of session?.events ?? []) {
6134
+ for (const event of readSessionEvents(session)) {
5520
6135
  const projected = await messageFromSessionEvent(ctx, event);
5521
6136
  if (projected === void 0) continue;
5522
6137
  if (event.type === "user/message") {
@@ -5564,12 +6179,18 @@ async function applyPiContextTransform(ctx, state, agent, signal, pending) {
5564
6179
  rebuilt.push(original);
5565
6180
  continue;
5566
6181
  }
6182
+ if (JSON.stringify(shape.content) === JSON.stringify(projectedPending[index]?.content)) {
6183
+ rebuilt.push(original);
6184
+ continue;
6185
+ }
5567
6186
  const blocks = await piToDshContent(ctx, typeof shape.content === "string" ? [{
5568
6187
  type: "text",
5569
6188
  text: shape.content
5570
6189
  }] : shape.content ?? []);
6190
+ const files = /* @__PURE__ */ new Map();
6191
+ for (const block of original.content ?? []) if (block.type === "file") files.set(fileContentForPi(block.attachment, ctx).text, block);
5571
6192
  rebuilt.push(createUserMessage({
5572
- content: blocks,
6193
+ content: blocks.map((block) => block.type === "text" ? files.get(block.text) ?? block : block),
5573
6194
  source: original.source ?? {
5574
6195
  kind: "plugin",
5575
6196
  plugin: state.messageSource
@@ -6640,7 +7261,7 @@ async function sendPiMessage(ctx, state, content, mode, sessionOverride, customT
6640
7261
  source: {
6641
7262
  kind: "plugin",
6642
7263
  plugin: state.messageSource,
6643
- ...customType === void 0 ? {} : { piCustomType: customType }
7264
+ ...customType === void 0 ? { form: "relay" } : { piCustomType: customType }
6644
7265
  }
6645
7266
  });
6646
7267
  if (sessionOverride !== void 0) {
@@ -6698,16 +7319,17 @@ function combineExecSignal(options) {
6698
7319
  }
6699
7320
  };
6700
7321
  }
6701
- async function executePiCommand(service, cwd, command, args, options) {
7322
+ async function executePiCommand(service, cwd, command, args, options, environment) {
6702
7323
  const operation = combineExecSignal(options);
6703
7324
  try {
6704
7325
  if (typeof command !== "string" || command.length === 0) throw new TypeError("Pi exec command must be a non-empty string");
6705
7326
  if (!Array.isArray(args) || args.some((value) => typeof value !== "string")) throw new TypeError("Pi exec args must be strings");
6706
- const executable = await service.resolveExecutable(command, void 0, operation.signal);
7327
+ const executable = await service.resolveExecutable(command, environment, operation.signal);
6707
7328
  const collect = { maxBytes: 67108864 };
6708
7329
  const handle = service.spawn({
6709
7330
  argv: [executable, ...args],
6710
7331
  cwd: options.cwd ?? cwd,
7332
+ ...environment === void 0 ? {} : { env: environment },
6711
7333
  stdio: {
6712
7334
  stdin: "ignore",
6713
7335
  stdout: collect,
@@ -6779,6 +7401,7 @@ function registerCommand(ctx, state, command) {
6779
7401
  const live = state.commands.get(command.name) ?? command;
6780
7402
  await runInPiRuntime(state, agent, async () => {
6781
7403
  await ensurePiSessionStarted(ctx, state, agent, invocation.signal);
7404
+ await refreshNativeSkillCommands(ctx, state, agent, invocation.signal);
6782
7405
  await live.handler(String(invocation.rawInput ?? "").trimStart(), commandContext);
6783
7406
  });
6784
7407
  const notices = commandContext.__notices;
@@ -6909,6 +7532,85 @@ function registerSharedProviderRoute(ctx, state, name, value) {
6909
7532
  state.ownedProviderRoutes.add(name);
6910
7533
  return route;
6911
7534
  }
7535
+ async function runPiPrintRequest(ctx, state, parent, request, signal) {
7536
+ if (parent === void 0 || state.subagentSessionFactory === void 0) throw new Error("pi2dsh CLI: native print work requires an active DSH agent");
7537
+ const catalog = state.shared.childExtensions;
7538
+ const requestedPaths = [];
7539
+ for (const requested of request.extensions) {
7540
+ let matched;
7541
+ for (const path of catalog?.packageByEntryPath.keys() ?? []) if (await realpath(path).catch(() => path) === requested) {
7542
+ matched = path;
7543
+ break;
7544
+ }
7545
+ if (matched === void 0) throw new Error(`pi2dsh CLI: extension ${requested} is not installed in this DSH profile`);
7546
+ requestedPaths.push(matched);
7547
+ }
7548
+ const paths = [.../* @__PURE__ */ new Set([...request.noExtensions ? [] : catalog?.packageByEntryPath.keys() ?? [], ...requestedPaths])];
7549
+ let model;
7550
+ if (request.model !== void 0) {
7551
+ model = state.modelCatalog?.all().find((entry) => `${entry.provider}/${entry.id}` === request.model || entry.id === request.model && (request.provider === void 0 || entry.provider === request.provider));
7552
+ if (model === void 0) throw new Error(`pi2dsh CLI: model ${request.model} is unavailable in the DSH model directory`);
7553
+ } else if (request.provider !== void 0) {
7554
+ const inherited = currentPiModel(state, parent);
7555
+ if (inherited?.provider !== request.provider) throw new Error("pi2dsh CLI: --provider requires an explicit --model for a different route");
7556
+ model = inherited;
7557
+ }
7558
+ const created = await runInPiRuntime(state, parent, () => state.subagentSessionFactory({
7559
+ cwd: request.cwd,
7560
+ signal,
7561
+ ...model === void 0 ? {} : { model },
7562
+ ...request.thinking === void 0 ? {} : { thinkingLevel: request.thinking },
7563
+ ...request.tools === void 0 ? {} : { tools: request.tools },
7564
+ ...request.noSession ? { sessionManager: { getSessionFile: () => void 0 } } : {},
7565
+ label: "Background task",
7566
+ resourceLoader: {
7567
+ getExtensions: () => ({
7568
+ extensions: paths.map((path) => ({ path })),
7569
+ errors: []
7570
+ }),
7571
+ getSystemPrompt: () => request.systemPrompt,
7572
+ getAppendSystemPrompt: () => request.appendSystemPrompt === void 0 ? [] : [request.appendSystemPrompt]
7573
+ }
7574
+ }));
7575
+ if (request.noSession) capabilityLedgerOf(ctx, state).reportHostDecision({
7576
+ capability: "pi print --no-session",
7577
+ reason: "the print worker has no Pi session file; DSH retains its native child audit log.",
7578
+ guidance: "Native retention remains a DSH host policy.",
7579
+ packageName: state.packageName
7580
+ });
7581
+ const child = created.session;
7582
+ let rejectCancellation;
7583
+ const cancellation = new Promise((_resolve, reject) => {
7584
+ rejectCancellation = reject;
7585
+ });
7586
+ cancellation.catch(() => {});
7587
+ const abort = () => {
7588
+ child.abort();
7589
+ rejectCancellation(signal.reason ?? /* @__PURE__ */ new Error("Pi print worker cancelled"));
7590
+ };
7591
+ signal.addEventListener("abort", abort, { once: true });
7592
+ try {
7593
+ const failures = [];
7594
+ await child.bindExtensions({ onError: (error) => {
7595
+ failures.push(error);
7596
+ } });
7597
+ if (failures.length > 0) throw new Error(`pi2dsh CLI: child extension mount failed: ${JSON.stringify(failures)}`);
7598
+ signal.throwIfAborted();
7599
+ await Promise.race([child.prompt(request.prompt), cancellation]);
7600
+ signal.throwIfAborted();
7601
+ const session = optionalService(ctx, "sessions")?.get(child.sessionManager?.getSessionId() ?? "");
7602
+ const end = readSessionEvents(session).findLast((event) => event.type === "turn/end");
7603
+ if ((end?.data)?.reason?.kind !== "completed") throw new Error(`pi2dsh CLI: native child did not complete: ${JSON.stringify(end?.data ?? {})}`);
7604
+ return {
7605
+ code: 0,
7606
+ stdout: textBlocks(child.messages.findLast((message) => message.role === "assistant")?.content).map((block) => block.text).join("\n")
7607
+ };
7608
+ } finally {
7609
+ signal.removeEventListener("abort", abort);
7610
+ if (signal.aborted) child.abort();
7611
+ await child.dispose();
7612
+ }
7613
+ }
6912
7614
  function createPiApi(ctx, state) {
6913
7615
  return {
6914
7616
  on(event, handler) {
@@ -7000,10 +7702,22 @@ function createPiApi(ctx, state) {
7000
7702
  const session = requireSession(state, "setLabel");
7001
7703
  state.bridge.appendLabel(session.id, String(entryId), label);
7002
7704
  },
7003
- exec(command, args = [], options = {}) {
7705
+ async exec(command, args = [], options = {}) {
7004
7706
  const service = optionalService(ctx, "subprocess");
7005
7707
  if (service === void 0) unsupported("exec");
7006
- return executePiCommand(service, cwdOf(currentAgent(state)), command, args, options);
7708
+ const parent = currentAgent(state);
7709
+ let local = false;
7710
+ try {
7711
+ const { LocalSubprocessRuntime } = await import("@deepseek-ai/dsh-subprocess-local");
7712
+ local = service instanceof LocalSubprocessRuntime;
7713
+ } catch {}
7714
+ if (!local) return executePiCommand(service, cwdOf(parent), command, args, options);
7715
+ return withPiPrintTransport((request, signal) => runPiPrintRequest(ctx, state, parent, request, signal), (environment) => executePiCommand(service, cwdOf(parent), command, args, options, environment)).catch((error) => ({
7716
+ stdout: "",
7717
+ stderr: error instanceof Error ? error.message : String(error),
7718
+ code: 1,
7719
+ killed: options.signal?.aborted === true
7720
+ }));
7007
7721
  },
7008
7722
  getActiveTools: () => getActiveTools(ctx, state),
7009
7723
  getAllTools: () => {
@@ -7022,7 +7736,7 @@ function createPiApi(ctx, state) {
7022
7736
  }));
7023
7737
  },
7024
7738
  setActiveTools: (names) => setActiveTools(ctx, state, names),
7025
- getCommands: () => [...state.commands.values()].map((command) => ({
7739
+ getCommands: () => [...[...state.commands.values()].map((command) => ({
7026
7740
  name: command.name,
7027
7741
  description: command.description,
7028
7742
  source: "extension",
@@ -7032,7 +7746,7 @@ function createPiApi(ctx, state) {
7032
7746
  scope: "user",
7033
7747
  origin: "package"
7034
7748
  }
7035
- })),
7749
+ })), ...state.nativeSkillCommands],
7036
7750
  async setModel(model) {
7037
7751
  const agent = currentAgent(state);
7038
7752
  if (agent === void 0) return false;
@@ -7205,7 +7919,7 @@ async function loadExtensions(rootDir, manifest, api, onExtensionError, onCapabi
7205
7919
  for (const extension of manifest.extensions) {
7206
7920
  try {
7207
7921
  const source = await readFile(join(rootDir, extension), "utf8");
7208
- const infraImport = /import[^;]*?\b(ModelRuntime|DefaultPackageManager)\b[^;]*?from\s*['"]@(?:earendil-works|mariozechner)\/pi-coding-agent['"]/su.exec(source);
7922
+ const infraImport = /import[^;]*?\b(DefaultPackageManager)\b[^;]*?from\s*['"]@(?:earendil-works|mariozechner)\/pi-coding-agent['"]/su.exec(source);
7209
7923
  if (infraImport !== null) onHostInfraReference?.(infraImport[1], extension);
7210
7924
  } catch {}
7211
7925
  try {
@@ -7255,6 +7969,7 @@ async function applyPiPackage(ctx, options) {
7255
7969
  toolDisposers: /* @__PURE__ */ new Map(),
7256
7970
  toolRestrictions: /* @__PURE__ */ new WeakMap(),
7257
7971
  commands: /* @__PURE__ */ new Map(),
7972
+ nativeSkillCommands: [],
7258
7973
  commandDisposers: /* @__PURE__ */ new Map(),
7259
7974
  dshCommandOwners: /* @__PURE__ */ new Map(),
7260
7975
  companionRoutes: shared.companionRoutes,
@@ -7263,6 +7978,8 @@ async function applyPiPackage(ctx, options) {
7263
7978
  activeAgents: /* @__PURE__ */ new Set(),
7264
7979
  hostAgent: void 0,
7265
7980
  piShutdownAgents: /* @__PURE__ */ new WeakSet(),
7981
+ shutdownTasks: /* @__PURE__ */ new WeakMap(),
7982
+ pendingShutdowns: /* @__PURE__ */ new Set(),
7266
7983
  disposedAgents: /* @__PURE__ */ new WeakSet(),
7267
7984
  startedSessions: /* @__PURE__ */ new Set(),
7268
7985
  sessionStartTasks: /* @__PURE__ */ new Map(),
@@ -7270,6 +7987,8 @@ async function applyPiPackage(ctx, options) {
7270
7987
  sessionlessStartTasks: /* @__PURE__ */ new WeakMap(),
7271
7988
  extensionsReady: false,
7272
7989
  pendingSessionStarts: /* @__PURE__ */ new Map(),
7990
+ discoveredSkillRoots: /* @__PURE__ */ new Set(),
7991
+ discoveredSkillProviders: 0,
7273
7992
  currentSystemPrompt: "",
7274
7993
  messageSource: `pi2dsh:${options.manifest.package.name}`,
7275
7994
  eventBus: sharedEventBusFor(shared, ownerAgent),
@@ -7278,7 +7997,7 @@ async function applyPiPackage(ctx, options) {
7278
7997
  llmBridge: void 0,
7279
7998
  piAiRegistry: __createPiAiRuntimeRegistry(),
7280
7999
  subagentSessionFactory: void 0,
7281
- bridge: new PiSessionBridge(),
8000
+ bridge: new PiSessionBridge((ref) => fileContentForPi(ref, ctx)),
7282
8001
  theme: sharedThemeFor(shared, ownerAgent),
7283
8002
  shortcuts: /* @__PURE__ */ new Map(),
7284
8003
  messageRenderers: /* @__PURE__ */ new Map(),
@@ -7651,6 +8370,7 @@ async function applyPiPackage(ctx, options) {
7651
8370
  messageSource: state.messageSource,
7652
8371
  packageName: state.packageName,
7653
8372
  parentAgentContext: () => currentAgent(state)?.ctx,
8373
+ parentAgent: () => currentAgent(state),
7654
8374
  registerChildTools: (childCtx, tools) => {
7655
8375
  for (const tool of tools) registerChildPiTool(childCtx, state, tool);
7656
8376
  },
@@ -7689,7 +8409,10 @@ async function applyPiPackage(ctx, options) {
7689
8409
  const persistence = optionalService(ctx, "sessionPersistence");
7690
8410
  if (typeof persistence?.list !== "function") return void 0;
7691
8411
  try {
7692
- return !(await persistence.list()).some((header) => String(header?.id ?? "") === sessionId);
8412
+ return !(await persistence.list()).some((snapshot) => {
8413
+ const header = snapshot.header ?? snapshot;
8414
+ return String(header.id ?? "") === sessionId;
8415
+ });
7693
8416
  } catch {
7694
8417
  return;
7695
8418
  }
@@ -7707,7 +8430,19 @@ async function applyPiPackage(ctx, options) {
7707
8430
  adoptChildAgent: (child, thinkingLevel) => {
7708
8431
  if (typeof child !== "object" || child === null) return;
7709
8432
  state.childAgents.add(child);
7710
- if (typeof thinkingLevel === "string" && thinkingLevel.length > 0 && thinkingLevel !== "off") state.thinkingLevels.set(child, thinkingLevel);
8433
+ if (typeof thinkingLevel === "string" && thinkingLevel.length > 0) {
8434
+ state.thinkingLevels.set(child, thinkingLevel);
8435
+ child.ctx?.on?.("agent/request", async (payload, next) => {
8436
+ const config = await next();
8437
+ if (payload.agent !== child) return config;
8438
+ const level = state.thinkingLevels.get(child);
8439
+ if (level === void 0 || level === "off" && (currentPiModel(state, child)?.thinkingLevelMap)?.off === null) return config;
8440
+ return {
8441
+ ...config,
8442
+ reasoningEffort: level
8443
+ };
8444
+ });
8445
+ }
7711
8446
  }
7712
8447
  });
7713
8448
  await registerPromptCommands(ctx, state, rootDir, options.manifest);
@@ -7720,7 +8455,7 @@ async function applyPiPackage(ctx, options) {
7720
8455
  });
7721
8456
  const onHostInfraReference = (symbol) => capabilityLedgerOf(ctx, state).reportStartupReference({
7722
8457
  capability: symbol,
7723
- reason: symbol === "DefaultPackageManager" ? "installing packages is owned by the DSH host and its security gates (dsh plugin add/remove)." : "standalone model stacks are owned by the DSH host llm configuration (packages read ctx.modelRegistry).",
8458
+ reason: "installing packages is owned by the DSH host and its security gates (dsh plugin add/remove).",
7724
8459
  guidance: "",
7725
8460
  packageName: state.packageName
7726
8461
  });
@@ -7729,6 +8464,13 @@ async function applyPiPackage(ctx, options) {
7729
8464
  await flushPendingSessionStarts(ctx, state);
7730
8465
  const remounts = packageRemountsOf(ctx, shared);
7731
8466
  remounts.set(state.packageName, async () => {
8467
+ await state.projection;
8468
+ const previousAgent = state.hostAgent ?? currentAgent(state);
8469
+ if (previousAgent !== void 0) {
8470
+ await shutdownPiSession(ctx, state, previousAgent, "reload");
8471
+ state.piShutdownAgents.delete(previousAgent);
8472
+ state.shutdownTasks.delete(previousAgent);
8473
+ }
7732
8474
  state.extensionsReady = false;
7733
8475
  state.handlers.clear();
7734
8476
  for (const off of state.eventBusOffs.splice(0)) off();
@@ -7766,4 +8508,4 @@ const runtimeInternals = {
7766
8508
  //#endregion
7767
8509
  export { overlayProviderConfig as a, runtimeInternals as c, normalizeToolSchema as i, getSharedChildExtensionCatalog as n, registerChildExtensionCatalog as o, mergeProviderRegistration as r, registerVisionCompanions as s, applyPiPackage as t };
7768
8510
 
7769
- //# sourceMappingURL=runtime-BiHMcD_8.mjs.map
8511
+ //# sourceMappingURL=runtime-B3N7s93Q.mjs.map