@duckmind/dm-darwin-arm64 0.13.5 → 0.13.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dm +0 -0
- package/extensions/.dm-extensions.json +15 -15
- package/extensions/dm-multicodex/package-lock.json +302 -1814
- package/extensions/dm-subagents/artifacts.ts +11 -5
- package/extensions/dm-subagents/async-execution.ts +4 -1
- package/extensions/dm-subagents/index.ts +1 -1
- package/extensions/dm-subagents/schemas.ts +1 -1
- package/extensions/dm-subagents/settings.ts +6 -4
- package/extensions/dm-subagents/subagent-runner.ts +167 -50
- package/extensions/dm-subagents/types.ts +62 -2
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
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>/
|
|
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>/
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
399
|
-
|
|
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
|
|
419
|
-
|
|
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
|
|
423
|
-
const
|
|
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 ??
|
|
426
|
-
success:
|
|
427
|
-
exitCode:
|
|
542
|
+
model: candidate ?? run.model ?? step.model ?? "default",
|
|
543
|
+
success: effectiveExitCode === 0 && !error,
|
|
544
|
+
exitCode: effectiveExitCode,
|
|
428
545
|
error,
|
|
429
|
-
usage:
|
|
546
|
+
usage: run.usage,
|
|
430
547
|
};
|
|
431
548
|
modelAttempts.push(attempt);
|
|
432
549
|
if (candidate) attemptedModels.push(candidate);
|
|
433
|
-
finalResult = { ...run,
|
|
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 =
|
|
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
|
|
315
|
-
export const
|
|
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;
|