pi-orchestrator-config 4.3.11 → 4.3.12
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/extensions/orchestrator/async-agents.ts +78 -19
- package/extensions/orchestrator/async-result-format.ts +58 -0
- package/extensions/orchestrator/async-runner.ts +20 -4
- package/extensions/orchestrator/reviewer-output-archive.ts +69 -0
- package/extensions/pidiff/pidiff-ui/package.json +1 -1
- package/extensions/pitasks/task-tools.ts +2 -2
- package/package.json +1 -1
- package/scripts/build-extension-uis.sh +6 -6
|
@@ -13,8 +13,7 @@ import { fileURLToPath } from "node:url";
|
|
|
13
13
|
const require = createRequire(import.meta.url);
|
|
14
14
|
|
|
15
15
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
16
|
-
import type { AgentConfig } from "./agents.js";
|
|
17
|
-
import { discoverAgents } from "./agents.js";
|
|
16
|
+
import type { AgentConfig, AgentDiscoveryResult } from "./agents.js";
|
|
18
17
|
import { resolveAgentModelProvider } from "./resolve-agent-model.js";
|
|
19
18
|
import { getPiInvocation, getProjectTmpDir, parseProcStartTime, djb2Hash } from "./utils.js";
|
|
20
19
|
import { addReviewerPending, recordReviewerResult, countFindings, readReviewState, markTestsPassed, markTestsFailed } from "./pi-config-review-state.js";
|
|
@@ -22,7 +21,8 @@ import {
|
|
|
22
21
|
getMainBranch,
|
|
23
22
|
} from "./git-helpers.js";
|
|
24
23
|
import { waitForResultFiles } from "./async-wait.js";
|
|
25
|
-
import {
|
|
24
|
+
import { formatAsyncResultOutput, reviewerOutputArchivePath } from "./async-result-format.js";
|
|
25
|
+
import { cleanupReviewerOutputArchives } from "./reviewer-output-archive.js";
|
|
26
26
|
const log = createLogger("async_agents");
|
|
27
27
|
|
|
28
28
|
export { autoCompleteTask, autoMarkInProgress } from "./task-lifecycle.js";
|
|
@@ -117,6 +117,10 @@ export function formatDuration(ms: number): string {
|
|
|
117
117
|
export function registerAsyncAgents(
|
|
118
118
|
pi: ExtensionAPI,
|
|
119
119
|
terminalNotify: (title: string, body: string) => void,
|
|
120
|
+
runtime: {
|
|
121
|
+
spawnProcess?: typeof spawn;
|
|
122
|
+
discoverAgents?: (cwd: string, scope: "both") => Pick<AgentDiscoveryResult, "agents">;
|
|
123
|
+
} = {},
|
|
120
124
|
): {
|
|
121
125
|
spawnAsyncAgent: (agentName: string, task: string, cwd: string, agents: AgentConfig[], options?: { fireAndForget?: boolean; name?: string; parentModelId?: string; parentProvider?: string; groupId?: string; taskId?: string; onComplete?: () => void; persistSession?: boolean; explicit?: { model?: string; provider?: string } }) => { id: string; error?: string; model?: string };
|
|
122
126
|
killAsyncAgent: (target: string) => { killed: string[]; errors: string[] };
|
|
@@ -125,6 +129,28 @@ export function registerAsyncAgents(
|
|
|
125
129
|
let PROJECT_TMP_DIR = path.join(process.cwd(), ".pi", "tmp"); // Computed only; created on session_start
|
|
126
130
|
let ASYNC_RESULTS_DIR = ""; // Set on session_start to project-scoped dir
|
|
127
131
|
|
|
132
|
+
function resultOutputPath(job: Pick<AsyncJob, "agent" | "id" | "workerDir">): string {
|
|
133
|
+
const workerOutputPath = path.join(job.workerDir, "output.log");
|
|
134
|
+
const outputPath = job.agent.startsWith("code-reviewer-")
|
|
135
|
+
? reviewerOutputArchivePath(PROJECT_TMP_DIR, job.id)
|
|
136
|
+
: workerOutputPath;
|
|
137
|
+
log.debug("async_result_output_path", { agent: job.agent, archived: outputPath !== workerOutputPath });
|
|
138
|
+
return outputPath;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function preserveReviewerOutput(job: Pick<AsyncJob, "agent" | "id">, output: string): void {
|
|
142
|
+
if (!job.agent.startsWith("code-reviewer-")) return;
|
|
143
|
+
const outputPath = reviewerOutputArchivePath(PROJECT_TMP_DIR, job.id);
|
|
144
|
+
try {
|
|
145
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true, mode: 0o700 });
|
|
146
|
+
fs.writeFileSync(outputPath, output, { mode: 0o600 });
|
|
147
|
+
fs.chmodSync(outputPath, 0o600);
|
|
148
|
+
cleanupReviewerOutputArchives(path.dirname(outputPath));
|
|
149
|
+
log.info("preserved_reviewer_output", { agent: job.agent, bytes: Buffer.byteLength(output, "utf8") });
|
|
150
|
+
} catch (e: any) {
|
|
151
|
+
log.error(`preserve reviewer output failed for ${job.id}: ${e?.message}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
128
154
|
|
|
129
155
|
const asyncState: AsyncState = {
|
|
130
156
|
jobs: new Map(),
|
|
@@ -392,9 +418,14 @@ export function registerAsyncAgents(
|
|
|
392
418
|
log.error(`reconcile: auto-complete failed for task #${safeTaskId}: ${e?.message}`);
|
|
393
419
|
}
|
|
394
420
|
}
|
|
395
|
-
// Enforce same output-length budget as processResultFile
|
|
396
|
-
const maxOutput = 3000 - autoCompleteError.length;
|
|
397
|
-
const output = (
|
|
421
|
+
// Enforce same output-length budget as processResultFile.
|
|
422
|
+
const maxOutput = Math.max(3000 - autoCompleteError.length, 500);
|
|
423
|
+
const output = formatAsyncResultOutput(
|
|
424
|
+
job.agent,
|
|
425
|
+
job.output || "",
|
|
426
|
+
resultOutputPath(job),
|
|
427
|
+
maxOutput,
|
|
428
|
+
);
|
|
398
429
|
// Emit lifecycle events
|
|
399
430
|
if (job.status === "complete") {
|
|
400
431
|
pi.events.emit("subagents:completed", { id: job.id, result: job.output || "" });
|
|
@@ -481,6 +512,7 @@ export function registerAsyncAgents(
|
|
|
481
512
|
try {
|
|
482
513
|
const data = JSON.parse(fs.readFileSync(rp, "utf-8"));
|
|
483
514
|
j.output = data.output;
|
|
515
|
+
preserveReviewerOutput(j, typeof data.output === "string" ? data.output : JSON.stringify(data.output ?? ""));
|
|
484
516
|
j.exitCode = data.exitCode;
|
|
485
517
|
j.durationMs = data.durationMs;
|
|
486
518
|
if (data.success !== undefined) j.status = data.success ? "complete" : "failed";
|
|
@@ -549,7 +581,11 @@ export function registerAsyncAgents(
|
|
|
549
581
|
if (j.fireAndForget) { j.delivered = true; continue; }
|
|
550
582
|
const resultStatus = j.status === "complete" ? "✅ completed" : "❌ failed";
|
|
551
583
|
const displayName = j.name || j.agent;
|
|
552
|
-
const output = (
|
|
584
|
+
const output = formatAsyncResultOutput(
|
|
585
|
+
j.agent,
|
|
586
|
+
j.output || "",
|
|
587
|
+
resultOutputPath(j),
|
|
588
|
+
);
|
|
553
589
|
let autoCompleteError = "";
|
|
554
590
|
// Auto-complete linked task directly in the store file (no AI involvement)
|
|
555
591
|
if (j.taskId && j.taskId !== "-1" && j.status === "complete" && j.cwd) {
|
|
@@ -639,6 +675,7 @@ export function registerAsyncAgents(
|
|
|
639
675
|
job.exitCode = data.exitCode;
|
|
640
676
|
job.durationMs = data.durationMs;
|
|
641
677
|
job.updatedAt = Date.now();
|
|
678
|
+
preserveReviewerOutput(job, typeof data.output === "string" ? data.output : JSON.stringify(data.output ?? ""));
|
|
642
679
|
|
|
643
680
|
// Notify terminal per-agent (lightweight, non-conversational)
|
|
644
681
|
const displayName = job.name || data.agent;
|
|
@@ -703,7 +740,11 @@ export function registerAsyncAgents(
|
|
|
703
740
|
try {
|
|
704
741
|
const statusPath = path.join(job.workerDir, "status.json");
|
|
705
742
|
const existing = JSON.parse(fs.readFileSync(statusPath, "utf-8"));
|
|
706
|
-
existing.output = (
|
|
743
|
+
existing.output = formatAsyncResultOutput(
|
|
744
|
+
job.agent,
|
|
745
|
+
data.output || "",
|
|
746
|
+
resultOutputPath(job),
|
|
747
|
+
);
|
|
707
748
|
existing.state = job.status;
|
|
708
749
|
existing.exitCode = job.exitCode;
|
|
709
750
|
existing.durationMs = job.durationMs;
|
|
@@ -748,8 +789,13 @@ export function registerAsyncAgents(
|
|
|
748
789
|
log.error(`auto-complete failed for task #${job.taskId}: ${e?.message}`);
|
|
749
790
|
}
|
|
750
791
|
}
|
|
751
|
-
const maxOutput = 3000 - autoCompleteError.length;
|
|
752
|
-
const output = (
|
|
792
|
+
const maxOutput = Math.max(3000 - autoCompleteError.length, 500);
|
|
793
|
+
const output = formatAsyncResultOutput(
|
|
794
|
+
job.agent,
|
|
795
|
+
data.output || "",
|
|
796
|
+
resultOutputPath(job),
|
|
797
|
+
maxOutput,
|
|
798
|
+
);
|
|
753
799
|
const pContent = `## Async Agent Result: ${displayName} ${resultStatus}\n\nTask: ${data.task}\nDuration: ${formatDuration(data.durationMs)}\n\n${output}${autoCompleteError}`;
|
|
754
800
|
if (wasAlreadyDelivered(job.id)) {
|
|
755
801
|
// Already delivered in previous lifecycle — skip send AND onComplete
|
|
@@ -815,7 +861,7 @@ export function registerAsyncAgents(
|
|
|
815
861
|
setTimeout(() => processResultFile(resultPath), 100);
|
|
816
862
|
});
|
|
817
863
|
if (asyncState.watcher.unref) asyncState.watcher.unref();
|
|
818
|
-
} catch (e: any) {
|
|
864
|
+
} catch (e: any) { log.error(`watcher setup failed: ${e?.message || e}`); }
|
|
819
865
|
}
|
|
820
866
|
|
|
821
867
|
function spawnAsyncAgent(
|
|
@@ -844,7 +890,7 @@ export function registerAsyncAgents(
|
|
|
844
890
|
} catch { /* retry */ }
|
|
845
891
|
}
|
|
846
892
|
if (!parentStartTime) {
|
|
847
|
-
|
|
893
|
+
log.warn("async-spawn: could not read /proc/self/stat starttime after 3 attempts");
|
|
848
894
|
}
|
|
849
895
|
const { model: effectiveModel, provider: effectiveProvider } = resolveAgentModelProvider(agentName, agent, options?.parentModelId, options?.parentProvider, cwd, options?.explicit);
|
|
850
896
|
fs.writeFileSync(path.join(workerDir, "session.json"), JSON.stringify({
|
|
@@ -912,6 +958,9 @@ export function registerAsyncAgents(
|
|
|
912
958
|
model: effectiveModel,
|
|
913
959
|
contextWindow: asyncState.lastCtx?.model?.contextWindow || 0,
|
|
914
960
|
resultPath,
|
|
961
|
+
reviewerOutputPath: agentName.startsWith("code-reviewer-")
|
|
962
|
+
? reviewerOutputArchivePath(PROJECT_TMP_DIR, id)
|
|
963
|
+
: undefined,
|
|
915
964
|
workerDir,
|
|
916
965
|
sessionId: `${process.pid}:${process.cwd()}`,
|
|
917
966
|
piCommand: inv.command,
|
|
@@ -982,7 +1031,7 @@ export function registerAsyncAgents(
|
|
|
982
1031
|
|
|
983
1032
|
// Fail fast with a self-explanatory error instead of spawning a child that
|
|
984
1033
|
// cannot load TypeScript (bare node dies on ".js"→".ts" specifiers).
|
|
985
|
-
if (!jitiCliPath) {
|
|
1034
|
+
if (!jitiCliPath && !runtime.spawnProcess) {
|
|
986
1035
|
log.error(`async-spawn: ${id} — no jiti-cli.mjs found. Probes:\n ${(jitiProbes.slice(-14)).join("\n ")}`);
|
|
987
1036
|
// Clean up the artifacts we already created — nothing will consume them.
|
|
988
1037
|
try { fs.rmSync(configPath, { force: true }); } catch {}
|
|
@@ -1056,7 +1105,7 @@ export function registerAsyncAgents(
|
|
|
1056
1105
|
stderrLog.on("error", () => {}); // never let a log write failure crash the session
|
|
1057
1106
|
log.debug(`async-spawn: ${job.id} stderr → ${stderrLogPath}`);
|
|
1058
1107
|
|
|
1059
|
-
const proc = spawn(process.execPath, spawnArgs, {
|
|
1108
|
+
const proc = (runtime.spawnProcess ?? spawn)(process.execPath, spawnArgs, {
|
|
1060
1109
|
cwd,
|
|
1061
1110
|
stdio: ["ignore", "ignore", "pipe"],
|
|
1062
1111
|
windowsHide: true,
|
|
@@ -1153,6 +1202,8 @@ export function registerAsyncAgents(
|
|
|
1153
1202
|
// Export as env var so prompts/CLI commands can reference it
|
|
1154
1203
|
process.env.PROJECT_TMP_DIR = PROJECT_TMP_DIR;
|
|
1155
1204
|
|
|
1205
|
+
cleanupReviewerOutputArchives(path.join(PROJECT_TMP_DIR, "reviewer-results"));
|
|
1206
|
+
|
|
1156
1207
|
// Scan worker directories for jobs with delivered=true in status.json
|
|
1157
1208
|
// This is more reliable than content hashing — uses job IDs directly
|
|
1158
1209
|
try {
|
|
@@ -1205,7 +1256,7 @@ export function registerAsyncAgents(
|
|
|
1205
1256
|
try { fs.rmSync(jobDir, { recursive: true, force: true }); } catch {}
|
|
1206
1257
|
} catch {} // skip unreadable dirs
|
|
1207
1258
|
}
|
|
1208
|
-
} catch (e: any) {
|
|
1259
|
+
} catch (e: any) { log.error(`zombie cleanup failed: ${e?.message?.slice(0, 100)}`); }
|
|
1209
1260
|
|
|
1210
1261
|
// Clean up stale result directories from dead sessions
|
|
1211
1262
|
try {
|
|
@@ -1296,13 +1347,13 @@ export function registerAsyncAgents(
|
|
|
1296
1347
|
log.info(`restored job: ${id} state=${job.status}`);
|
|
1297
1348
|
}
|
|
1298
1349
|
}
|
|
1299
|
-
} catch (e: any) {
|
|
1350
|
+
} catch (e: any) { log.error(`job restore failed: ${e?.message || e}`); }
|
|
1300
1351
|
|
|
1301
1352
|
// Start poller if we have jobs or unprocessed result files
|
|
1302
1353
|
let hasResultFiles = false;
|
|
1303
1354
|
try {
|
|
1304
1355
|
hasResultFiles = fs.existsSync(ASYNC_RESULTS_DIR) && fs.readdirSync(ASYNC_RESULTS_DIR).some(f => f.endsWith(".json"));
|
|
1305
|
-
} catch (e: any) {
|
|
1356
|
+
} catch (e: any) { log.error(`result files check failed: ${e?.message || e}`); }
|
|
1306
1357
|
if (asyncState.jobs.size > 0 || hasResultFiles) {
|
|
1307
1358
|
ensureAsyncPoller();
|
|
1308
1359
|
}
|
|
@@ -1321,6 +1372,8 @@ export function registerAsyncAgents(
|
|
|
1321
1372
|
// /async-status — fullscreen overlay list → live output detail
|
|
1322
1373
|
async function handleAsyncStatus(ctx: any): Promise<void> {
|
|
1323
1374
|
if (!ctx.hasUI) return;
|
|
1375
|
+
log.debug("opening_async_status_overlay", { jobs: asyncState.jobs.size });
|
|
1376
|
+
const { openAsyncStatusOverlay } = await import("./async-status-ui.js");
|
|
1324
1377
|
await openAsyncStatusOverlay(ctx, {
|
|
1325
1378
|
listJobs: () => Array.from(asyncState.jobs.values()),
|
|
1326
1379
|
killJob: (id) => {
|
|
@@ -1413,7 +1466,11 @@ export function registerAsyncAgents(
|
|
|
1413
1466
|
const displayName = job.name || job.agent;
|
|
1414
1467
|
const duration = job.durationMs || (Date.now() - job.startedAt);
|
|
1415
1468
|
const rawOutput = typeof job.output === "string" ? job.output : "Killed by user";
|
|
1416
|
-
const output =
|
|
1469
|
+
const output = formatAsyncResultOutput(
|
|
1470
|
+
job.agent,
|
|
1471
|
+
rawOutput,
|
|
1472
|
+
resultOutputPath(job),
|
|
1473
|
+
);
|
|
1417
1474
|
const killContent = `## Async Agent Result: ${displayName} ❌ failed\n\nTask: ${job.task}\nDuration: ${formatDuration(duration)}\n\n${output}`;
|
|
1418
1475
|
if (wasAlreadyDelivered(job.id)) {
|
|
1419
1476
|
job.delivered = true;
|
|
@@ -1524,13 +1581,15 @@ export function registerAsyncAgents(
|
|
|
1524
1581
|
|
|
1525
1582
|
// Spawn — create an async agent from pitasks' TaskExecute
|
|
1526
1583
|
handleRpc<{ requestId: string; type: string; prompt: string; options?: any }>(
|
|
1527
|
-
"subagents:rpc:spawn", ({ type, prompt, options }) => {
|
|
1584
|
+
"subagents:rpc:spawn", async ({ type, prompt, options }) => {
|
|
1528
1585
|
const ctx = asyncState.lastCtx;
|
|
1529
1586
|
if (!ctx) throw new Error("No active session");
|
|
1530
1587
|
if (!type || typeof type !== "string") throw new Error("Missing or invalid 'type' parameter");
|
|
1531
1588
|
if (!prompt || typeof prompt !== "string") throw new Error("Missing or invalid 'prompt' parameter");
|
|
1532
1589
|
|
|
1533
1590
|
const cwd = options?.cwd || ctx.cwd;
|
|
1591
|
+
log.debug("rpc_spawn_requested", { type, customCwd: Boolean(options?.cwd) });
|
|
1592
|
+
const discoverAgents = runtime.discoverAgents ?? (await import("./agents.js")).discoverAgents;
|
|
1534
1593
|
const discovery = discoverAgents(cwd, "both");
|
|
1535
1594
|
const agents = discovery.agents;
|
|
1536
1595
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Format async-agent output for bounded delivery and status persistence. */
|
|
2
|
+
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { createLogger } from "../shared/logger.js";
|
|
6
|
+
|
|
7
|
+
const log = createLogger("async_agents");
|
|
8
|
+
const MAX_OUTPUT_CHARS = 3000;
|
|
9
|
+
|
|
10
|
+
/** Return the completed-result archive path, outside cleanup-prone worker directories. */
|
|
11
|
+
export function reviewerOutputArchivePath(projectTmpDir: string, jobId: string): string {
|
|
12
|
+
const archiveDir = path.resolve(projectTmpDir, "reviewer-results");
|
|
13
|
+
const filename = `${createHash("sha256").update(jobId).digest("hex")}.json`;
|
|
14
|
+
const archivePath = path.resolve(archiveDir, filename);
|
|
15
|
+
if (path.dirname(archivePath) !== archiveDir) {
|
|
16
|
+
throw new Error("Reviewer archive destination escapes its archive root");
|
|
17
|
+
}
|
|
18
|
+
log.debug("reviewer_output_archive_path", { jobId, archivePath });
|
|
19
|
+
return archivePath;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Preserve valid reviewer JSON unless it exceeds the delivery budget.
|
|
24
|
+
* Oversized reviewer output is available only in its worker output.log.
|
|
25
|
+
*/
|
|
26
|
+
export function formatAsyncResultOutput(
|
|
27
|
+
agent: string,
|
|
28
|
+
output: string,
|
|
29
|
+
outputPath: string,
|
|
30
|
+
maxChars = MAX_OUTPUT_CHARS,
|
|
31
|
+
): string {
|
|
32
|
+
log.debug("format_async_result_output", { agent, outputChars: output.length, maxChars });
|
|
33
|
+
if (agent.startsWith("code-reviewer-") && output.length > maxChars) {
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(output);
|
|
36
|
+
if (parsed && typeof parsed === "object" && Array.isArray(parsed.findings)) {
|
|
37
|
+
const metadata = {
|
|
38
|
+
truncated: true,
|
|
39
|
+
format: "json",
|
|
40
|
+
outputBytes: Buffer.byteLength(output, "utf8"),
|
|
41
|
+
outputPath,
|
|
42
|
+
};
|
|
43
|
+
const serialized = JSON.stringify(metadata);
|
|
44
|
+
if (serialized.length <= maxChars) return serialized;
|
|
45
|
+
log.warn("reviewer_metadata_path_exceeds_budget", { agent, maxChars });
|
|
46
|
+
return JSON.stringify({
|
|
47
|
+
truncated: true,
|
|
48
|
+
format: "json",
|
|
49
|
+
outputBytes: metadata.outputBytes,
|
|
50
|
+
outputPathOmitted: true,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// Invalid reviewer output retains ordinary bounded-text behavior.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return output.slice(0, maxChars);
|
|
58
|
+
}
|
|
@@ -9,6 +9,10 @@ import * as fs from "node:fs";
|
|
|
9
9
|
import * as path from "node:path";
|
|
10
10
|
import { createRequire } from "node:module";
|
|
11
11
|
import { getSetting } from "./project-settings.js";
|
|
12
|
+
import { createLogger } from "../shared/logger.js";
|
|
13
|
+
import { archiveReviewerOutput } from "./reviewer-output-archive.js";
|
|
14
|
+
|
|
15
|
+
const log = createLogger("async_agents");
|
|
12
16
|
|
|
13
17
|
interface RunConfig {
|
|
14
18
|
id: string;
|
|
@@ -20,6 +24,7 @@ interface RunConfig {
|
|
|
20
24
|
tools?: string[];
|
|
21
25
|
systemPrompt?: string;
|
|
22
26
|
resultPath: string;
|
|
27
|
+
reviewerOutputPath?: string;
|
|
23
28
|
workerDir: string;
|
|
24
29
|
sessionId?: string;
|
|
25
30
|
piCommand: string;
|
|
@@ -72,7 +77,7 @@ async function run(config: RunConfig): Promise<void> {
|
|
|
72
77
|
writeJson(statusPath, status);
|
|
73
78
|
let lastUsage: any = null;
|
|
74
79
|
|
|
75
|
-
const outputStream = fs.createWriteStream(outputPath, { flags: "w" });
|
|
80
|
+
const outputStream = fs.createWriteStream(outputPath, { flags: "w", mode: 0o600 });
|
|
76
81
|
|
|
77
82
|
// Connect to pidash server to stream events
|
|
78
83
|
const pidashPort = getSetting(config.cwd, "pidash_port");
|
|
@@ -157,7 +162,7 @@ async function run(config: RunConfig): Promise<void> {
|
|
|
157
162
|
if (pidashWs?.readyState === 1) {
|
|
158
163
|
pidashWs.send(JSON.stringify({ type: "async_event", id: config.id, event: ev }));
|
|
159
164
|
}
|
|
160
|
-
} catch (e: any) {
|
|
165
|
+
} catch (e: any) { log.warn(`event processing skipped: ${e?.message || e}`); }
|
|
161
166
|
}
|
|
162
167
|
|
|
163
168
|
// Update status periodically (every ~10 lines)
|
|
@@ -193,7 +198,7 @@ async function run(config: RunConfig): Promise<void> {
|
|
|
193
198
|
if (p.type === "text") finalOutput = p.text;
|
|
194
199
|
}
|
|
195
200
|
}
|
|
196
|
-
} catch (e: any) {
|
|
201
|
+
} catch (e: any) { log.error(`final line parse failed: ${e?.message || e}`); }
|
|
197
202
|
}
|
|
198
203
|
|
|
199
204
|
// Write final status
|
|
@@ -283,7 +288,18 @@ async function run(config: RunConfig): Promise<void> {
|
|
|
283
288
|
}
|
|
284
289
|
|
|
285
290
|
// End the output stream after all retries are done
|
|
286
|
-
|
|
291
|
+
await new Promise<void>((resolve, reject) => {
|
|
292
|
+
outputStream.once("error", reject);
|
|
293
|
+
outputStream.end(resolve);
|
|
294
|
+
});
|
|
295
|
+
if (config.reviewerOutputPath && config.agent.startsWith("code-reviewer-")) {
|
|
296
|
+
try {
|
|
297
|
+
archiveReviewerOutput(outputPath, config.reviewerOutputPath);
|
|
298
|
+
log.info("archived_reviewer_output", { agent: config.agent, outputPath: config.reviewerOutputPath });
|
|
299
|
+
} catch (err) {
|
|
300
|
+
log.error("archive_reviewer_output_failed", err);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
287
303
|
|
|
288
304
|
// Write result for the watcher to pick up
|
|
289
305
|
writeJson(config.resultPath, {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** Durable reviewer-output archival and bounded retention. */
|
|
2
|
+
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { createLogger } from "../shared/logger.js";
|
|
6
|
+
|
|
7
|
+
const log = createLogger("async_agents");
|
|
8
|
+
|
|
9
|
+
export const REVIEWER_ARCHIVE_RETENTION = {
|
|
10
|
+
maxAgeMs: 7 * 24 * 60 * 60 * 1000,
|
|
11
|
+
maxFiles: 100,
|
|
12
|
+
maxBytes: 50 * 1024 * 1024,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type Retention = typeof REVIEWER_ARCHIVE_RETENTION;
|
|
16
|
+
|
|
17
|
+
/** Copy reviewer output into a durable owner-only archive. */
|
|
18
|
+
export function archiveReviewerOutput(outputPath: string, archivePath: string): void {
|
|
19
|
+
fs.chmodSync(outputPath, 0o600);
|
|
20
|
+
fs.mkdirSync(path.dirname(archivePath), { recursive: true, mode: 0o700 });
|
|
21
|
+
fs.copyFileSync(outputPath, archivePath);
|
|
22
|
+
fs.chmodSync(archivePath, 0o600);
|
|
23
|
+
cleanupReviewerOutputArchives(path.dirname(archivePath));
|
|
24
|
+
log.info("archived_reviewer_output", { outputPath: archivePath });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Remove expired and oldest regular-file archives while preserving bounded storage. */
|
|
28
|
+
export function cleanupReviewerOutputArchives(
|
|
29
|
+
archiveDir: string,
|
|
30
|
+
retention: Retention = REVIEWER_ARCHIVE_RETENTION,
|
|
31
|
+
): void {
|
|
32
|
+
let archives: Array<{ path: string; mtimeMs: number; size: number }>;
|
|
33
|
+
try {
|
|
34
|
+
archives = fs.readdirSync(archiveDir).flatMap((entry) => {
|
|
35
|
+
const archivePath = path.join(archiveDir, entry);
|
|
36
|
+
try {
|
|
37
|
+
const stat = fs.lstatSync(archivePath);
|
|
38
|
+
return stat.isFile() ? [{ path: archivePath, mtimeMs: stat.mtimeMs, size: stat.size }] : [];
|
|
39
|
+
} catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
} catch (error: any) {
|
|
44
|
+
if (error?.code !== "ENOENT") log.warn("reviewer_archive_cleanup_scan_failed", { archiveDir, error: error?.message });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
const retained = archives
|
|
50
|
+
.filter((archive) => {
|
|
51
|
+
if (now - archive.mtimeMs <= retention.maxAgeMs) return true;
|
|
52
|
+
try { fs.unlinkSync(archive.path); } catch (error: any) { log.warn("reviewer_archive_expiry_delete_failed", { archivePath: archive.path, error: error?.message }); return true; }
|
|
53
|
+
return false;
|
|
54
|
+
})
|
|
55
|
+
.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
|
56
|
+
|
|
57
|
+
let totalBytes = retained.reduce((total, archive) => total + archive.size, 0);
|
|
58
|
+
for (let index = retained.length - 1; index >= 0; index--) {
|
|
59
|
+
const archive = retained[index];
|
|
60
|
+
if (index < retention.maxFiles && totalBytes <= retention.maxBytes) continue;
|
|
61
|
+
try {
|
|
62
|
+
fs.unlinkSync(archive.path);
|
|
63
|
+
totalBytes -= archive.size;
|
|
64
|
+
} catch (error: any) {
|
|
65
|
+
log.warn("reviewer_archive_retention_delete_failed", { archivePath: archive.path, error: error?.message });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
log.debug("reviewer_archive_cleanup_complete", { archiveDir, retainedFiles: retained.length, totalBytes });
|
|
69
|
+
}
|
|
@@ -61,7 +61,7 @@ All tasks are created with status \`pending\`.
|
|
|
61
61
|
- Include enough detail in the description for another agent to understand and complete the task
|
|
62
62
|
- After creating tasks, use TaskUpdate to set up dependencies (blocks/blockedBy) if needed
|
|
63
63
|
- Check TaskList first to avoid creating duplicate tasks
|
|
64
|
-
-
|
|
64
|
+
- Use \`agentType\` only as optional descriptive metadata; it does not dispatch or execute an agent`,
|
|
65
65
|
promptGuidelines: [
|
|
66
66
|
"When working on complex multi-step tasks, use TaskCreate to track progress and TaskUpdate to update status.",
|
|
67
67
|
"Mark tasks as in_progress before starting work and completed when done.",
|
|
@@ -71,7 +71,7 @@ All tasks are created with status \`pending\`.
|
|
|
71
71
|
subject: Type.String({ description: "A brief title for the task" }),
|
|
72
72
|
description: Type.String({ description: "A detailed description of what needs to be done" }),
|
|
73
73
|
activeForm: Type.Optional(Type.String({ description: "Present continuous form shown in spinner when in_progress (e.g., 'Running tests')" })),
|
|
74
|
-
agentType: Type.Optional(Type.String({ description: "
|
|
74
|
+
agentType: Type.Optional(Type.String({ description: "Optional descriptive metadata for the task. It does not dispatch or execute an agent." })),
|
|
75
75
|
metadata: Type.Optional(Type.Record(Type.String(), Type.Any(), { description: "Arbitrary metadata to attach to the task" })),
|
|
76
76
|
}),
|
|
77
77
|
execute(_toolCallId: string, params: any) {
|
package/package.json
CHANGED
|
@@ -7,17 +7,17 @@ REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
|
7
7
|
|
|
8
8
|
build_ui() {
|
|
9
9
|
local ui_dir="$1"
|
|
10
|
-
echo "Building ${ui_dir}..."
|
|
10
|
+
echo "Building ${ui_dir}..." >&2
|
|
11
11
|
cd "${REPO_DIR}/${ui_dir}"
|
|
12
12
|
if [ ! -d node_modules ]; then
|
|
13
|
-
npm install --production=false
|
|
13
|
+
npm install --production=false >&2
|
|
14
14
|
fi
|
|
15
|
-
npm run build
|
|
15
|
+
npm run build >&2
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
build_ui extensions/pidash/pidash-ui
|
|
19
19
|
build_ui extensions/pidiff/pidiff-ui
|
|
20
20
|
|
|
21
|
-
echo "UI builds complete."
|
|
22
|
-
echo " ${REPO_DIR}/extensions/pidash/pidash-ui/dist/index.html"
|
|
23
|
-
echo " ${REPO_DIR}/extensions/pidiff/pidiff-ui/dist/index.html"
|
|
21
|
+
echo "UI builds complete." >&2
|
|
22
|
+
echo " ${REPO_DIR}/extensions/pidash/pidash-ui/dist/index.html" >&2
|
|
23
|
+
echo " ${REPO_DIR}/extensions/pidiff/pidiff-ui/dist/index.html" >&2
|