@duckmind/dm-darwin-x64 0.13.5 → 0.13.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dm +0 -0
  2. package/extensions/.dm-extensions.json +39 -15
  3. package/extensions/dm-multicodex/package-lock.json +302 -1814
  4. package/extensions/dm-phone/README.md +23 -0
  5. package/extensions/dm-phone/index.ts +12 -0
  6. package/extensions/dm-phone/node_modules/.package-lock.json +29 -0
  7. package/extensions/dm-phone/node_modules/ws/LICENSE +20 -0
  8. package/extensions/dm-phone/node_modules/ws/README.md +548 -0
  9. package/extensions/dm-phone/node_modules/ws/browser.js +8 -0
  10. package/extensions/dm-phone/node_modules/ws/index.js +22 -0
  11. package/extensions/dm-phone/node_modules/ws/lib/buffer-util.js +131 -0
  12. package/extensions/dm-phone/node_modules/ws/lib/constants.js +19 -0
  13. package/extensions/dm-phone/node_modules/ws/lib/event-target.js +292 -0
  14. package/extensions/dm-phone/node_modules/ws/lib/extension.js +203 -0
  15. package/extensions/dm-phone/node_modules/ws/lib/limiter.js +55 -0
  16. package/extensions/dm-phone/node_modules/ws/lib/permessage-deflate.js +528 -0
  17. package/extensions/dm-phone/node_modules/ws/lib/receiver.js +706 -0
  18. package/extensions/dm-phone/node_modules/ws/lib/sender.js +602 -0
  19. package/extensions/dm-phone/node_modules/ws/lib/stream.js +161 -0
  20. package/extensions/dm-phone/node_modules/ws/lib/subprotocol.js +62 -0
  21. package/extensions/dm-phone/node_modules/ws/lib/validation.js +152 -0
  22. package/extensions/dm-phone/node_modules/ws/lib/websocket-server.js +554 -0
  23. package/extensions/dm-phone/node_modules/ws/lib/websocket.js +1393 -0
  24. package/extensions/dm-phone/node_modules/ws/package.json +70 -0
  25. package/extensions/dm-phone/node_modules/ws/wrapper.mjs +21 -0
  26. package/extensions/dm-phone/package-lock.json +66 -0
  27. package/extensions/dm-phone/package.json +35 -0
  28. package/extensions/dm-phone/phone-session-pool.ts +8 -0
  29. package/extensions/dm-phone/public/app/attachments.js +233 -0
  30. package/extensions/dm-phone/public/app/autocomplete-controller.js +81 -0
  31. package/extensions/dm-phone/public/app/autocomplete.js +135 -0
  32. package/extensions/dm-phone/public/app/bindings.js +178 -0
  33. package/extensions/dm-phone/public/app/command-catalog.js +76 -0
  34. package/extensions/dm-phone/public/app/commands.js +370 -0
  35. package/extensions/dm-phone/public/app/constants.js +60 -0
  36. package/extensions/dm-phone/public/app/formatters.js +131 -0
  37. package/extensions/dm-phone/public/app/handlers.js +442 -0
  38. package/extensions/dm-phone/public/app/main.js +6 -0
  39. package/extensions/dm-phone/public/app/markdown.js +105 -0
  40. package/extensions/dm-phone/public/app/messages.js +418 -0
  41. package/extensions/dm-phone/public/app/sheet-actions.js +113 -0
  42. package/extensions/dm-phone/public/app/sheet-navigation.js +19 -0
  43. package/extensions/dm-phone/public/app/sheets-view.js +272 -0
  44. package/extensions/dm-phone/public/app/state.js +95 -0
  45. package/extensions/dm-phone/public/app/tool-rendering.js +562 -0
  46. package/extensions/dm-phone/public/app/transport.js +176 -0
  47. package/extensions/dm-phone/public/app/ui.js +409 -0
  48. package/extensions/dm-phone/public/app.js +1 -0
  49. package/extensions/dm-phone/public/icon.svg +15 -0
  50. package/extensions/dm-phone/public/index.html +147 -0
  51. package/extensions/dm-phone/public/manifest.webmanifest +17 -0
  52. package/extensions/dm-phone/public/styles.css +1139 -0
  53. package/extensions/dm-phone/public/sw.js +78 -0
  54. package/extensions/dm-phone/src/extension/phone-args.ts +121 -0
  55. package/extensions/dm-phone/src/extension/phone-paths.ts +250 -0
  56. package/extensions/dm-phone/src/extension/phone-quota.ts +188 -0
  57. package/extensions/dm-phone/src/extension/phone-runtime.ts +154 -0
  58. package/extensions/dm-phone/src/extension/phone-server-runtime.ts +1217 -0
  59. package/extensions/dm-phone/src/extension/phone-sessions.ts +139 -0
  60. package/extensions/dm-phone/src/extension/phone-static.ts +30 -0
  61. package/extensions/dm-phone/src/extension/phone-tailscale.ts +148 -0
  62. package/extensions/dm-phone/src/extension/phone-theme.ts +85 -0
  63. package/extensions/dm-phone/src/extension/register-phone-child-extension.ts +112 -0
  64. package/extensions/dm-phone/src/extension/register-phone-extension.ts +106 -0
  65. package/extensions/dm-phone/src/extension/types.ts +73 -0
  66. package/extensions/dm-phone/src/session-pool/parent-session-worker.ts +881 -0
  67. package/extensions/dm-phone/src/session-pool/session-pool.ts +470 -0
  68. package/extensions/dm-phone/src/session-pool/session-worker.ts +734 -0
  69. package/extensions/dm-phone/src/session-pool/types.ts +105 -0
  70. package/extensions/dm-phone/src/session-pool/utils.ts +23 -0
  71. package/extensions/dm-subagents/artifacts.ts +11 -5
  72. package/extensions/dm-subagents/async-execution.ts +4 -1
  73. package/extensions/dm-subagents/index.ts +1 -1
  74. package/extensions/dm-subagents/schemas.ts +1 -1
  75. package/extensions/dm-subagents/settings.ts +6 -4
  76. package/extensions/dm-subagents/subagent-runner.ts +167 -50
  77. package/extensions/dm-subagents/types.ts +62 -2
  78. package/package.json +1 -1
@@ -0,0 +1,105 @@
1
+ import type { WebSocket } from "ws";
2
+
3
+ export type SessionKind = "parent" | "parallel";
4
+
5
+ export type SessionSummary = {
6
+ id: string;
7
+ kind: SessionKind;
8
+ sessionId: string | null;
9
+ sessionFile: string | null;
10
+ sessionName: string | null;
11
+ label: string;
12
+ secondaryLabel: string;
13
+ firstUserPreview: string | null;
14
+ lastUserPreview: string | null;
15
+ model: { id: string; name: string; provider: string } | null;
16
+ isRunning: boolean;
17
+ isStreaming: boolean;
18
+ isCompacting: boolean;
19
+ messageCount: number;
20
+ pendingMessageCount: number;
21
+ hasPendingUiRequest: boolean;
22
+ lastError: string;
23
+ lastActivityAt: number;
24
+ childPid: number | null;
25
+ cwd?: string | null;
26
+ mirrorsCli?: boolean;
27
+ };
28
+
29
+ export type PendingRequest = {
30
+ resolve: (value: any) => void;
31
+ reject: (error: Error) => void;
32
+ timer: NodeJS.Timeout;
33
+ };
34
+
35
+ export type PendingClientResponse = {
36
+ ws: WebSocket;
37
+ responseCommand?: string;
38
+ responseData?: Record<string, unknown>;
39
+ onSuccess?: (payload: any) => void;
40
+ onError?: (payload: any) => void;
41
+ };
42
+
43
+ export type SessionSnapshot = {
44
+ state: any;
45
+ messages: any[];
46
+ commands: any[];
47
+ liveAssistantMessage: any;
48
+ liveTools: any[];
49
+ };
50
+
51
+ export type ClientState = {
52
+ activeSessionId: string | null;
53
+ };
54
+
55
+ export type SessionWorkerOptions<TWorker> = {
56
+ cwd: string;
57
+ send: (ws: WebSocket, payload: unknown) => void;
58
+ onActivity: () => void;
59
+ onStateChange: () => void;
60
+ onEnvelope: (worker: TWorker, envelope: any) => void;
61
+ shouldAutoRestart: (worker: TWorker) => boolean;
62
+ };
63
+
64
+ export type SessionStatus = {
65
+ childRunning: boolean;
66
+ cwd: string;
67
+ previousCwd: string | null;
68
+ isStreaming: boolean;
69
+ isCompacting: boolean;
70
+ lastError: string;
71
+ childPid: number | null;
72
+ sessionWorkerId: string;
73
+ sessionKind: SessionKind;
74
+ };
75
+
76
+ export interface SessionController {
77
+ id: string;
78
+ kind: SessionKind;
79
+ cwd: string;
80
+ previousCwd: string | null;
81
+ currentSessionFile: string | null;
82
+ lastError: string;
83
+ lastActivityAt: number;
84
+ pendingUiRequest: any;
85
+ ensureStarted(startOptions?: { sessionFile?: string | null }): Promise<void>;
86
+ request(command: Record<string, unknown>, timeoutMs?: number): Promise<any>;
87
+ refreshCachedSnapshot(timeoutMs?: number): Promise<SessionSnapshot>;
88
+ getSnapshot(): Promise<SessionSnapshot>;
89
+ sendClientCommand(command: Record<string, unknown>, meta?: PendingClientResponse): Promise<string | undefined>;
90
+ reload(): Promise<void>;
91
+ dispose(): Promise<void>;
92
+ getStatus(): SessionStatus;
93
+ getSummary(): SessionSummary;
94
+ getCachedSnapshot(): SessionSnapshot;
95
+ setTrackedCwd?(cwd: string, previousCwd?: string | null): void;
96
+ }
97
+
98
+ export type PhoneSessionPoolOptions = {
99
+ cwd: string;
100
+ send: (ws: WebSocket, payload: unknown) => void;
101
+ onActivity: () => void;
102
+ buildStatusMeta: () => Record<string, unknown>;
103
+ createDefaultSession: () => SessionController;
104
+ createParallelSession: (sessionFile?: string | null) => SessionController;
105
+ };
@@ -0,0 +1,23 @@
1
+ export function contentToPreviewText(content: unknown): string {
2
+ if (typeof content === "string") {
3
+ return content.replace(/\s+/g, " ").trim();
4
+ }
5
+
6
+ if (!Array.isArray(content)) {
7
+ return "";
8
+ }
9
+
10
+ return content
11
+ .map((part: any) => {
12
+ if (part?.type === "text") return part.text || "";
13
+ if (part?.type === "image") return "[image]";
14
+ return "";
15
+ })
16
+ .join(" ")
17
+ .replace(/\s+/g, " ")
18
+ .trim();
19
+ }
20
+
21
+ export function shortId(value: unknown): string {
22
+ return String(value || "").trim().slice(0, 8);
23
+ }
@@ -1,9 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import type { ArtifactPaths } from "./types.ts";
5
-
6
- const TEMP_ARTIFACTS_DIR = path.join(os.tmpdir(), "dm-subagent-artifacts");
4
+ import { TEMP_ARTIFACTS_DIR, type ArtifactPaths } from "./types.ts";
7
5
  const CLEANUP_MARKER_FILE = ".last-cleanup";
8
6
 
9
7
  export function getArtifactsDir(sessionFile: string | null): string {
@@ -64,7 +62,10 @@ export function cleanupOldArtifacts(dir: string, maxAgeDays: number): void {
64
62
  if (stat.mtimeMs < cutoff) {
65
63
  fs.unlinkSync(filePath);
66
64
  }
67
- } catch {}
65
+ } catch {
66
+ // Artifact cleanup is best-effort housekeeping. Skip files that disappear
67
+ // or become unreadable while scanning so one bad entry does not block the rest.
68
+ }
68
69
  }
69
70
 
70
71
  fs.writeFileSync(markerPath, String(now));
@@ -80,6 +81,8 @@ export function cleanupAllArtifactDirs(maxAgeDays: number): void {
80
81
  try {
81
82
  dirs = fs.readdirSync(sessionsBase);
82
83
  } catch {
84
+ // Session artifact cleanup is best-effort. If the sessions root cannot be read,
85
+ // skip cleanup instead of failing extension startup.
83
86
  return;
84
87
  }
85
88
 
@@ -87,6 +90,9 @@ export function cleanupAllArtifactDirs(maxAgeDays: number): void {
87
90
  const artifactsDir = path.join(sessionsBase, dir, "subagent-artifacts");
88
91
  try {
89
92
  cleanupOldArtifacts(artifactsDir, maxAgeDays);
90
- } catch {}
93
+ } catch {
94
+ // Session cleanup is best-effort. Keep going so one unreadable session dir
95
+ // does not block cleanup for the rest.
96
+ }
91
97
  }
92
98
  }
@@ -23,6 +23,8 @@ import {
23
23
  type MaxOutputConfig,
24
24
  ASYNC_DIR,
25
25
  RESULTS_DIR,
26
+ TEMP_ROOT_DIR,
27
+ getAsyncConfigPath,
26
28
  resolveChildMaxSubagentDepth,
27
29
  } from "./types.ts";
28
30
 
@@ -113,7 +115,8 @@ export function isAsyncAvailable(): boolean {
113
115
  function spawnRunner(cfg: object, suffix: string, cwd: string): number | undefined {
114
116
  if (!jitiCliPath) return undefined;
115
117
 
116
- const cfgPath = path.join(os.tmpdir(), `dm-async-cfg-${suffix}.json`);
118
+ fs.mkdirSync(TEMP_ROOT_DIR, { recursive: true });
119
+ const cfgPath = getAsyncConfigPath(suffix);
117
120
  fs.writeFileSync(cfgPath, JSON.stringify(cfg));
118
121
  const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
119
122
 
@@ -259,7 +259,7 @@ EXECUTION (use exactly ONE mode):
259
259
  CHAIN TEMPLATE VARIABLES (use in task strings):
260
260
  • {task} - The original task/request from the user
261
261
  • {previous} - Text response from the previous step (empty for first step)
262
- • {chain_dir} - Shared directory for chain files (e.g., <tmpdir>/dm-chain-runs/abc123/)
262
+ • {chain_dir} - Shared directory for chain files (e.g., <tmpdir>/pi-subagents-<scope>/chain-runs/abc123/)
263
263
 
264
264
  Example: { chain: [{agent:"scout", task:"Analyze {task}"}, {agent:"planner", task:"Plan based on {previous}"}] }
265
265
 
@@ -83,7 +83,7 @@ export const SubagentParams = Type.Object({
83
83
  enum: ["fresh", "fork"],
84
84
  description: "'fresh' (default) or 'fork' to branch from parent session",
85
85
  })),
86
- chainDir: Type.Optional(Type.String({ description: "Persistent directory for chain artifacts. Default: <tmpdir>/dm-chain-runs/ (auto-cleaned after 24h)" })),
86
+ chainDir: Type.Optional(Type.String({ description: "Persistent directory for chain artifacts. Default: a user-scoped temp directory under <tmpdir>/ (auto-cleaned after 24h)" })),
87
87
  async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })),
88
88
  agentScope: Type.Optional(Type.String({ description: "Agent discovery scope: 'user', 'project', or 'both' (default: 'both'; project wins on name collisions)" })),
89
89
  cwd: Type.Optional(Type.String()),
@@ -3,12 +3,10 @@
3
3
  */
4
4
 
5
5
  import * as fs from "node:fs";
6
- import * as os from "node:os";
7
6
  import * as path from "node:path";
8
7
  import type { AgentConfig } from "./agents.ts";
9
8
  import { normalizeSkillInput } from "./skills.ts";
10
-
11
- const CHAIN_RUNS_DIR = path.join(os.tmpdir(), "dm-chain-runs");
9
+ import { CHAIN_RUNS_DIR } from "./types.ts";
12
10
  const CHAIN_DIR_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
13
11
 
14
12
  // =============================================================================
@@ -100,7 +98,9 @@ export function createChainDir(runId: string, baseDir?: string): string {
100
98
  export function removeChainDir(chainDir: string): void {
101
99
  try {
102
100
  fs.rmSync(chainDir, { recursive: true });
103
- } catch {}
101
+ } catch {
102
+ // Chain cleanup is best-effort. Runs can already have cleaned their temp dir.
103
+ }
104
104
  }
105
105
 
106
106
  export function cleanupOldChainDirs(): void {
@@ -110,6 +110,8 @@ export function cleanupOldChainDirs(): void {
110
110
  try {
111
111
  dirs = fs.readdirSync(CHAIN_RUNS_DIR);
112
112
  } catch {
113
+ // Startup cleanup is best-effort. If the scoped temp root is unreadable,
114
+ // skip cleanup instead of failing extension startup.
113
115
  return;
114
116
  }
115
117
 
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
3
3
  import { createRequire } from "node:module";
4
4
  import * as path from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
+ import type { Message } from "@mariozechner/pi-ai";
6
7
  import { appendJsonl, getArtifactPaths } from "./artifacts.ts";
7
8
  import { getPiSpawnCommand } from "./pi-spawn.ts";
8
9
  import { captureSingleOutputSnapshot, resolveSingleOutput } from "./single-output.ts";
@@ -27,6 +28,7 @@ import {
27
28
  } from "./parallel-utils.ts";
28
29
  import { buildPiArgs, cleanupTempDir } from "./pi-args.ts";
29
30
  import { formatModelAttemptNote, isRetryableModelFailure } from "./model-fallback.ts";
31
+ import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput } from "./utils.ts";
30
32
  import {
31
33
  cleanupWorktrees,
32
34
  createWorktrees,
@@ -123,32 +125,44 @@ function emptyUsage(): Usage {
123
125
  return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
124
126
  }
125
127
 
126
- function parseRunOutput(output: string): { usage: Usage; model?: string; error?: string } {
127
- const usage = emptyUsage();
128
- let model: string | undefined;
129
- let error: string | undefined;
130
- for (const line of output.split("\n")) {
131
- if (!line.trim()) continue;
132
- try {
133
- const evt = JSON.parse(line) as { type?: string; message?: { role?: string; model?: string; errorMessage?: string; usage?: any } };
134
- if (evt.type !== "message_end" || evt.message?.role !== "assistant") continue;
135
- const msg = evt.message;
136
- if (msg.model) model = msg.model;
137
- if (msg.errorMessage) error = msg.errorMessage;
138
- const u = msg.usage;
139
- if (u) {
140
- usage.turns++;
141
- usage.input += u.input ?? u.inputTokens ?? 0;
142
- usage.output += u.output ?? u.outputTokens ?? 0;
143
- usage.cacheRead += u.cacheRead ?? 0;
144
- usage.cacheWrite += u.cacheWrite ?? 0;
145
- usage.cost += u.cost?.total ?? 0;
146
- }
147
- } catch {
148
- // Ignore malformed stdout lines.
149
- }
150
- }
151
- return { usage, model, error };
128
+ interface ChildEventContext {
129
+ eventsPath: string;
130
+ runId: string;
131
+ stepIndex: number;
132
+ agent: string;
133
+ }
134
+
135
+ interface ChildUsage {
136
+ input?: number;
137
+ inputTokens?: number;
138
+ output?: number;
139
+ outputTokens?: number;
140
+ cacheRead?: number;
141
+ cacheWrite?: number;
142
+ cost?: { total?: number };
143
+ }
144
+
145
+ type ChildMessage = Message & {
146
+ model?: string;
147
+ errorMessage?: string;
148
+ usage?: ChildUsage;
149
+ };
150
+
151
+ interface ChildEvent {
152
+ type?: string;
153
+ message?: ChildMessage;
154
+ toolName?: string;
155
+ args?: Record<string, unknown>;
156
+ }
157
+
158
+ interface RunPiStreamingResult {
159
+ stderr: string;
160
+ exitCode: number | null;
161
+ messages: Message[];
162
+ usage: Usage;
163
+ model?: string;
164
+ error?: string;
165
+ finalOutput: string;
152
166
  }
153
167
 
154
168
  function runPiStreaming(
@@ -159,7 +173,8 @@ function runPiStreaming(
159
173
  piPackageRoot?: string,
160
174
  piArgv1?: string,
161
175
  maxSubagentDepth?: number,
162
- ): Promise<{ stdout: string; stderr: string; exitCode: number | null }> {
176
+ childEventContext?: ChildEventContext,
177
+ ): Promise<RunPiStreamingResult> {
163
178
  return new Promise((resolve) => {
164
179
  const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
165
180
  const spawnEnv = { ...process.env, ...(env ?? {}), ...getSubagentDepthEnv(maxSubagentDepth) };
@@ -168,29 +183,119 @@ function runPiStreaming(
168
183
  ...(piArgv1 ? { argv1: piArgv1 } : {}),
169
184
  });
170
185
  const child = spawn(spawnSpec.command, spawnSpec.args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: spawnEnv });
171
- let stdout = "";
172
186
  let stderr = "";
187
+ let stdoutBuf = "";
188
+ let stderrBuf = "";
189
+ const messages: Message[] = [];
190
+ const usage = emptyUsage();
191
+ let model: string | undefined;
192
+ let error: string | undefined;
193
+ const rawStdoutLines: string[] = [];
194
+
195
+ const writeOutputLine = (line: string) => {
196
+ if (!line.trim()) return;
197
+ outputStream.write(`${line}\n`);
198
+ };
199
+
200
+ const writeOutputText = (text: string) => {
201
+ for (const line of text.split("\n")) {
202
+ writeOutputLine(line);
203
+ }
204
+ };
205
+
206
+ const appendChildEvent = (event: Record<string, unknown>) => {
207
+ if (!childEventContext) return;
208
+ appendJsonl(childEventContext.eventsPath, JSON.stringify({
209
+ ...event,
210
+ subagentSource: "child",
211
+ subagentRunId: childEventContext.runId,
212
+ subagentStepIndex: childEventContext.stepIndex,
213
+ subagentAgent: childEventContext.agent,
214
+ observedAt: Date.now(),
215
+ }));
216
+ };
217
+
218
+ const appendChildLine = (type: "subagent.child.stdout" | "subagent.child.stderr", line: string) => {
219
+ appendChildEvent({ type, line });
220
+ };
221
+
222
+ const processStdoutLine = (line: string) => {
223
+ if (!line.trim()) return;
224
+ let event: ChildEvent;
225
+ try {
226
+ event = JSON.parse(line) as ChildEvent;
227
+ } catch {
228
+ rawStdoutLines.push(line);
229
+ writeOutputLine(line);
230
+ appendChildLine("subagent.child.stdout", line);
231
+ return;
232
+ }
233
+
234
+ appendChildEvent(event);
235
+
236
+ if (event.type === "tool_execution_start" && event.toolName) {
237
+ const toolArgs = extractToolArgsPreview(event.args ?? {});
238
+ writeOutputLine(toolArgs ? `${event.toolName}: ${toolArgs}` : event.toolName);
239
+ return;
240
+ }
241
+
242
+ if ((event.type === "message_end" || event.type === "tool_result_end") && event.message) {
243
+ messages.push(event.message);
244
+ const text = extractTextFromContent(event.message.content);
245
+ if (text) writeOutputText(text);
246
+
247
+ if (event.type !== "message_end" || event.message.role !== "assistant") return;
248
+ if (event.message.model) model = event.message.model;
249
+ if (event.message.errorMessage) error = event.message.errorMessage;
250
+ const eventUsage = event.message.usage;
251
+ if (!eventUsage) return;
252
+ usage.turns++;
253
+ usage.input += eventUsage.input ?? eventUsage.inputTokens ?? 0;
254
+ usage.output += eventUsage.output ?? eventUsage.outputTokens ?? 0;
255
+ usage.cacheRead += eventUsage.cacheRead ?? 0;
256
+ usage.cacheWrite += eventUsage.cacheWrite ?? 0;
257
+ usage.cost += eventUsage.cost?.total ?? 0;
258
+ }
259
+ };
260
+
261
+ const processStderrText = (text: string) => {
262
+ stderr += text;
263
+ stderrBuf += text;
264
+ outputStream.write(text);
265
+ if (!childEventContext) return;
266
+ const lines = stderrBuf.split("\n");
267
+ stderrBuf = lines.pop() || "";
268
+ for (const line of lines) {
269
+ if (!line.trim()) continue;
270
+ appendChildLine("subagent.child.stderr", line);
271
+ }
272
+ };
173
273
 
174
274
  child.stdout.on("data", (chunk: Buffer) => {
175
275
  const text = chunk.toString();
176
- stdout += text;
177
- outputStream.write(text);
276
+ stdoutBuf += text;
277
+ const lines = stdoutBuf.split("\n");
278
+ stdoutBuf = lines.pop() || "";
279
+ for (const line of lines) processStdoutLine(line);
178
280
  });
179
281
 
180
282
  child.stderr.on("data", (chunk: Buffer) => {
181
- const text = chunk.toString();
182
- stderr += text;
183
- outputStream.write(text);
283
+ processStderrText(chunk.toString());
184
284
  });
185
285
 
186
286
  child.on("close", (exitCode) => {
287
+ if (stdoutBuf.trim()) processStdoutLine(stdoutBuf);
288
+ if (stderrBuf.trim()) appendChildLine("subagent.child.stderr", stderrBuf);
187
289
  outputStream.end();
188
- resolve({ stdout, stderr, exitCode });
290
+ const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
291
+ resolve({ stderr, exitCode, messages, usage, model, error, finalOutput });
189
292
  });
190
293
 
191
- child.on("error", () => {
294
+ child.on("error", (spawnError) => {
192
295
  outputStream.end();
193
- resolve({ stdout, stderr, exitCode: 1 });
296
+ const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
297
+ const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
298
+ resolve({ stderr, exitCode: 1, messages, usage, model, error: error ?? spawnErrorMessage, finalOutput });
194
299
  });
195
300
  });
196
301
  }
@@ -395,14 +500,13 @@ async function runSingleStep(
395
500
  const attemptedModels: string[] = [];
396
501
  const modelAttempts: ModelAttempt[] = [];
397
502
  const attemptNotes: string[] = [];
398
- let finalResult:
399
- | { stdout: string; stderr: string; exitCode: number | null; usage: Usage; model?: string; error?: string }
400
- | undefined;
503
+ const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
504
+ let finalResult: RunPiStreamingResult | undefined;
401
505
 
402
506
  for (let index = 0; index < candidates.length; index++) {
403
507
  const candidate = candidates[index];
404
508
  const { args, env, tempDir } = buildPiArgs({
405
- baseArgs: ["-p"],
509
+ baseArgs: ["--mode", "json", "-p"],
406
510
  task,
407
511
  sessionEnabled,
408
512
  sessionDir,
@@ -415,28 +519,41 @@ async function runSingleStep(
415
519
  mcpDirectTools: step.mcpDirectTools,
416
520
  promptFileStem: step.agent,
417
521
  });
418
- const outputFile = index === 0 ? ctx.outputFile : `${ctx.outputFile}.attempt-${index + 1}`;
419
- const run = await runPiStreaming(args, step.cwd ?? ctx.cwd, outputFile, env, ctx.piPackageRoot, ctx.piArgv1, step.maxSubagentDepth);
522
+ const run = await runPiStreaming(
523
+ args,
524
+ step.cwd ?? ctx.cwd,
525
+ ctx.outputFile,
526
+ env,
527
+ ctx.piPackageRoot,
528
+ ctx.piArgv1,
529
+ step.maxSubagentDepth,
530
+ { eventsPath, runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
531
+ );
420
532
  cleanupTempDir(tempDir);
421
533
 
422
- const parsed = parseRunOutput(run.stdout);
423
- const error = parsed.error || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined);
534
+ const hiddenError = run.exitCode === 0 && !run.error ? detectSubagentError(run.messages) : null;
535
+ const effectiveExitCode = hiddenError?.hasError ? (hiddenError.exitCode ?? 1) : run.exitCode;
536
+ const error = hiddenError?.hasError
537
+ ? hiddenError.details
538
+ ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
539
+ : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
540
+ : run.error || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined);
424
541
  const attempt: ModelAttempt = {
425
- model: candidate ?? parsed.model ?? step.model ?? "default",
426
- success: run.exitCode === 0 && !error,
427
- exitCode: run.exitCode,
542
+ model: candidate ?? run.model ?? step.model ?? "default",
543
+ success: effectiveExitCode === 0 && !error,
544
+ exitCode: effectiveExitCode,
428
545
  error,
429
- usage: parsed.usage,
546
+ usage: run.usage,
430
547
  };
431
548
  modelAttempts.push(attempt);
432
549
  if (candidate) attemptedModels.push(candidate);
433
- finalResult = { ...run, usage: parsed.usage, model: candidate ?? parsed.model, error };
550
+ finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error };
434
551
  if (attempt.success) break;
435
552
  if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
436
553
  attemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
437
554
  }
438
555
 
439
- const rawOutput = (finalResult?.stdout || "").trim();
556
+ const rawOutput = finalResult?.finalOutput ?? "";
440
557
  const resolvedOutput = step.outputPath && finalResult?.exitCode === 0
441
558
  ? resolveSingleOutput(step.outputPath, rawOutput, outputSnapshot)
442
559
  : { fullOutput: rawOutput };
@@ -309,10 +309,66 @@ export const DEFAULT_ARTIFACT_CONFIG: ArtifactConfig = {
309
309
  cleanupDays: 7,
310
310
  };
311
311
 
312
+ function sanitizeTempScopeSegment(value: string): string {
313
+ const sanitized = value
314
+ .trim()
315
+ .replace(/[^A-Za-z0-9._-]+/g, "-")
316
+ .replace(/^-+|-+$/g, "");
317
+ return sanitized || "unknown";
318
+ }
319
+
320
+ export function resolveTempScopeId(options?: {
321
+ env?: NodeJS.ProcessEnv;
322
+ getuid?: (() => number) | undefined;
323
+ userInfo?: (() => { username?: string | null }) | undefined;
324
+ homedir?: (() => string) | undefined;
325
+ }): string {
326
+ const env = options?.env ?? process.env;
327
+ const getuid = options && Object.hasOwn(options, "getuid")
328
+ ? options.getuid
329
+ : process.getuid?.bind(process);
330
+ if (typeof getuid === "function") {
331
+ return `uid-${getuid()}`;
332
+ }
333
+
334
+ for (const key of ["USERNAME", "USER", "LOGNAME"] as const) {
335
+ const value = env[key];
336
+ if (value) return `user-${sanitizeTempScopeSegment(value)}`;
337
+ }
338
+
339
+ const userInfo = options && Object.hasOwn(options, "userInfo")
340
+ ? options.userInfo
341
+ : os.userInfo;
342
+ try {
343
+ const username = userInfo?.().username;
344
+ if (username) return `user-${sanitizeTempScopeSegment(username)}`;
345
+ } catch {
346
+ // Fall through to home-directory-based scoping.
347
+ }
348
+
349
+ const homedir = env.USERPROFILE ?? env.HOME;
350
+ if (homedir) return `home-${sanitizeTempScopeSegment(homedir)}`;
351
+
352
+ const resolveHomedir = options && Object.hasOwn(options, "homedir")
353
+ ? options.homedir
354
+ : os.homedir;
355
+ try {
356
+ const fallbackHomedir = resolveHomedir?.();
357
+ if (fallbackHomedir) return `home-${sanitizeTempScopeSegment(fallbackHomedir)}`;
358
+ } catch {
359
+ // Fall through to the last-resort shared scope.
360
+ }
361
+
362
+ return "shared";
363
+ }
364
+
312
365
  export const MAX_PARALLEL = 8;
313
366
  export const MAX_CONCURRENCY = 4;
314
- export const RESULTS_DIR = path.join(os.tmpdir(), "dm-async-subagent-results");
315
- export const ASYNC_DIR = path.join(os.tmpdir(), "dm-async-subagent-runs");
367
+ export const TEMP_ROOT_DIR = path.join(os.tmpdir(), `pi-subagents-${resolveTempScopeId()}`);
368
+ export const RESULTS_DIR = path.join(TEMP_ROOT_DIR, "async-subagent-results");
369
+ export const ASYNC_DIR = path.join(TEMP_ROOT_DIR, "async-subagent-runs");
370
+ export const CHAIN_RUNS_DIR = path.join(TEMP_ROOT_DIR, "chain-runs");
371
+ export const TEMP_ARTIFACTS_DIR = path.join(TEMP_ROOT_DIR, "artifacts");
316
372
  export const WIDGET_KEY = "subagent-async";
317
373
  export const SLASH_RESULT_TYPE = "subagent-slash-result";
318
374
  export const SLASH_SUBAGENT_REQUEST_EVENT = "subagent:slash:request";
@@ -329,6 +385,10 @@ export const DEFAULT_FORK_PREAMBLE =
329
385
  "Your sole job is to execute the task below. Do not continue or respond to the prior conversation " +
330
386
  "— focus exclusively on completing this task using your tools.";
331
387
 
388
+ export function getAsyncConfigPath(suffix: string): string {
389
+ return path.join(TEMP_ROOT_DIR, `async-cfg-${suffix}.json`);
390
+ }
391
+
332
392
  export function wrapForkTask(task: string, preamble?: string | false): string {
333
393
  if (preamble === false) return task;
334
394
  const effectivePreamble = preamble ?? DEFAULT_FORK_PREAMBLE;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duckmind/dm-darwin-x64",
3
- "version": "0.13.5",
3
+ "version": "0.13.7",
4
4
  "description": "DuckMind (dm) binary payload for darwin x64",
5
5
  "license": "MIT",
6
6
  "os": [