@wrongstack/core 0.309.1 → 0.310.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.
- package/dist/chronicle/index.js +113 -41
- package/dist/chronicle/metrics-ingest.d.ts +7 -0
- package/dist/chronicle/metrics-schema.d.ts +4 -1
- package/dist/chronicle/project-server.js +94 -38
- package/dist/chronicle/query.d.ts +1 -0
- package/dist/chronicle/sqlite-journal-schema.d.ts +2 -1
- package/dist/chronicle/types.d.ts +2 -0
- package/dist/{agent-status-helpers.d.ts → coordination/agent-status-helpers.d.ts} +1 -1
- package/dist/{agent-status-tracker.d.ts → coordination/agent-status-tracker.d.ts} +2 -2
- package/dist/{middleware → coordination}/collab-pause.d.ts +1 -1
- package/dist/coordination/director-kanban-queue-helpers.d.ts +1 -1
- package/dist/coordination/fleet-status-tool.d.ts +1 -1
- package/dist/coordination/index.d.ts +5 -1
- package/dist/coordination/index.js +2061 -346
- package/dist/coordination/kanban-dispatch-port.d.ts +30 -0
- package/dist/coordination/kanban-ops-port.d.ts +21 -0
- package/dist/coordination/mailbox-hooks.d.ts +1 -1
- package/dist/coordination/mailbox-project-server.js +14 -9
- package/dist/core/context.d.ts +35 -44
- package/dist/core/conversation-state.d.ts +16 -52
- package/dist/core/index.js +53 -16
- package/dist/core/provider-runner.d.ts +2 -2
- package/dist/core/run-env.d.ts +7 -28
- package/dist/core/streaming-response-builder.d.ts +2 -2
- package/dist/execution/index.js +111 -153
- package/dist/goal/index.js +100 -22
- package/dist/hq/auth-store.d.ts +9 -0
- package/dist/hq/cost-bridge.d.ts +1 -1
- package/dist/hq/index.js +24 -2
- package/dist/index.d.ts +12 -5
- package/dist/index.js +27806 -33937
- package/dist/infrastructure/index.js +210 -132
- package/dist/infrastructure/provider-cache-ledger.d.ts +1 -1
- package/dist/infrastructure/token-counter.d.ts +5 -1
- package/dist/kernel/events/file-events.d.ts +6 -0
- package/dist/kernel/events/provider-events.d.ts +10 -7
- package/dist/kernel/events/session-events.d.ts +4 -4
- package/dist/kernel/events/tool-events.d.ts +12 -2
- package/dist/plugin/index.js +105 -35
- package/dist/security/index.d.ts +1 -0
- package/dist/security/index.js +47 -18
- package/dist/security/kanban-boundary.d.ts +1 -1
- package/dist/security/kanban-governance-port.d.ts +28 -0
- package/dist/session-catalog/index.js +2 -3
- package/dist/session-catalog/project-server.js +2 -3
- package/dist/session-catalog/protocol.d.ts +1 -1
- package/dist/session-catalog/registry.d.ts +1 -1
- package/dist/session-catalog/store-schema.d.ts +1 -1
- package/dist/session-catalog/store.d.ts +1 -1
- package/dist/storage/annotations-store.d.ts +1 -1
- package/dist/storage/board-store-port.d.ts +45 -0
- package/dist/storage/completed-work-checkpoint.d.ts +1 -1
- package/dist/storage/config-loader/types.d.ts +1 -1
- package/dist/storage/event-bus-port.d.ts +27 -0
- package/dist/storage/file-session-writer.d.ts +47 -5
- package/dist/storage/goal-coordination.d.ts +1 -1
- package/dist/storage/goal-store.d.ts +1 -1
- package/dist/storage/index.d.ts +3 -4
- package/dist/storage/index.js +1063 -1459
- package/dist/storage/plan-store.d.ts +1 -1
- package/dist/storage/queue-store.d.ts +1 -1
- package/dist/storage/replay-log-store.d.ts +1 -1
- package/dist/storage/session-recovery.d.ts +10 -0
- package/dist/storage/session-resume-validation.d.ts +13 -1
- package/dist/storage/session-store/events.d.ts +1 -1
- package/dist/storage/session-store/load-session-data.d.ts +1 -1
- package/dist/storage/session-store/rename-session.d.ts +1 -1
- package/dist/storage/session-store/resume-session.d.ts +3 -1
- package/dist/storage/session-store/session-store-index.d.ts +2 -1
- package/dist/storage/session-store/types.d.ts +1 -1
- package/dist/storage/session-store.d.ts +70 -0
- package/dist/storage/session-summary-tracker.d.ts +8 -0
- package/dist/storage/session-write-buffer.d.ts +31 -2
- package/dist/storage/task-store.d.ts +1 -1
- package/dist/storage/todos-checkpoint.d.ts +1 -1
- package/dist/storage/tool-audit-log.d.ts +1 -1
- package/dist/tasking/index.js +100 -22
- package/dist/tasking/task-tracker.d.ts +24 -1
- package/dist/types/compactor.d.ts +2 -2
- package/dist/types/context.d.ts +212 -0
- package/dist/types/conversation-state.d.ts +109 -0
- package/dist/types/error-handler.d.ts +2 -2
- package/dist/types/file-event-record.d.ts +6 -0
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.js +17 -1
- package/dist/types/permission.d.ts +3 -3
- package/dist/types/plugin.d.ts +3 -3
- package/dist/types/provider-runner.d.ts +2 -2
- package/dist/types/provider.d.ts +10 -0
- package/dist/types/run-env.d.ts +32 -0
- package/dist/types/session.d.ts +3 -0
- package/dist/types/slash-command.d.ts +2 -2
- package/dist/types/token-counter.d.ts +30 -1
- package/dist/types/tool-executor.d.ts +2 -2
- package/dist/types/tool.d.ts +5 -5
- package/dist/utils/context-breakdown.d.ts +2 -2
- package/dist/utils/context-evidence.d.ts +12 -12
- package/dist/utils/crash-shield.d.ts +9 -0
- package/dist/utils/heap-watchdog.js +11 -3
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +72 -156
- package/dist/utils/regex-guard.d.ts +7 -30
- package/dist/utils/todos-format.d.ts +1 -1
- package/dist/utils/tree-kill.d.ts +2 -0
- package/dist/utils/tree-kill.js +1 -0
- package/instructions/coordination/subagent-baseline.md +10 -0
- package/instructions/system-lite.md +2 -0
- package/instructions/system-pro.md +40 -0
- package/instructions/system.md +15 -0
- package/package.json +6 -9
- package/dist/defaults/index.d.ts +0 -69
- package/dist/defaults/index.js +0 -38133
- /package/dist/{fleet-notifier.d.ts → coordination/fleet-notifier.d.ts} +0 -0
- /package/dist/{session-registry-atomic-file.d.ts → session-catalog/session-registry-atomic-file.d.ts} +0 -0
- /package/dist/{session-registry-types.d.ts → session-catalog/session-registry-types.d.ts} +0 -0
- /package/dist/{session-registry.d.ts → session-catalog/session-registry.d.ts} +0 -0
|
@@ -224,7 +224,7 @@ var InMemoryAgentBridge = class {
|
|
|
224
224
|
});
|
|
225
225
|
}
|
|
226
226
|
this.inflightGuards.add(correlationId);
|
|
227
|
-
return new Promise((
|
|
227
|
+
return new Promise((resolve17, reject) => {
|
|
228
228
|
const timer = setTimeout(() => {
|
|
229
229
|
this.inflightGuards.delete(correlationId);
|
|
230
230
|
this.pendingRequests.delete(correlationId);
|
|
@@ -243,7 +243,7 @@ var InMemoryAgentBridge = class {
|
|
|
243
243
|
return;
|
|
244
244
|
}
|
|
245
245
|
this.pendingRequests.set(correlationId, {
|
|
246
|
-
resolve:
|
|
246
|
+
resolve: resolve17,
|
|
247
247
|
reject,
|
|
248
248
|
timer
|
|
249
249
|
});
|
|
@@ -714,13 +714,13 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
714
714
|
if (!bus?.hasListenerFor("budget.threshold_reached")) {
|
|
715
715
|
return Promise.resolve("stop");
|
|
716
716
|
}
|
|
717
|
-
return new Promise((
|
|
717
|
+
return new Promise((resolve17) => {
|
|
718
718
|
let resolved = false;
|
|
719
719
|
const respond = (d) => {
|
|
720
720
|
if (resolved) return;
|
|
721
721
|
resolved = true;
|
|
722
722
|
clearTimeout(fallback);
|
|
723
|
-
|
|
723
|
+
resolve17(d);
|
|
724
724
|
};
|
|
725
725
|
const fallback = setTimeout(() => respond("stop"), _SubagentBudget.DECISION_TIMEOUT_MS);
|
|
726
726
|
const sessionId = this.currentSessionId();
|
|
@@ -7649,13 +7649,13 @@ var BrainDecisionQueue = class {
|
|
|
7649
7649
|
options: request.options,
|
|
7650
7650
|
rationale: "Decision escalated to human authority."
|
|
7651
7651
|
};
|
|
7652
|
-
const pending = new Promise((
|
|
7653
|
-
const entry = { request, resolve:
|
|
7652
|
+
const pending = new Promise((resolve17) => {
|
|
7653
|
+
const entry = { request, resolve: resolve17 };
|
|
7654
7654
|
if (this.opts.timeoutMs && this.opts.timeoutMs > 0) {
|
|
7655
7655
|
entry.timer = setTimeout(() => {
|
|
7656
7656
|
this.pending.delete(request.id);
|
|
7657
7657
|
markDecisionTier(request, "terminal");
|
|
7658
|
-
|
|
7658
|
+
resolve17(
|
|
7659
7659
|
this.opts.onTimeout?.(request) ?? {
|
|
7660
7660
|
type: "deny",
|
|
7661
7661
|
reason: "Brain human decision timed out."
|
|
@@ -8439,26 +8439,26 @@ var BrainMonitor = class {
|
|
|
8439
8439
|
trackFileChurn(toolName, ok, input) {
|
|
8440
8440
|
if (!this.signals.fileChurn) return;
|
|
8441
8441
|
if (!ok || !this.fileEditTools.has(toolName.toLowerCase())) return;
|
|
8442
|
-
const
|
|
8443
|
-
if (!
|
|
8442
|
+
const path44 = editedPath(input);
|
|
8443
|
+
if (!path44) return;
|
|
8444
8444
|
const now = Date.now();
|
|
8445
|
-
const stamps = (this.editTimestamps.get(
|
|
8445
|
+
const stamps = (this.editTimestamps.get(path44) ?? []).filter(
|
|
8446
8446
|
(t) => now - t <= this.fileChurnWindowMs
|
|
8447
8447
|
);
|
|
8448
8448
|
stamps.push(now);
|
|
8449
8449
|
if (stamps.length >= this.fileChurnThreshold) {
|
|
8450
|
-
this.editTimestamps.delete(
|
|
8450
|
+
this.editTimestamps.delete(path44);
|
|
8451
8451
|
void this.engage("file_churn", {
|
|
8452
|
-
question: `The file "${
|
|
8452
|
+
question: `The file "${path44}" has been edited ${stamps.length} times within ${Math.round(this.fileChurnWindowMs / 6e4)} minutes \u2014 the agent may be oscillating (edit/revert loop) instead of converging. Should it be steered?`,
|
|
8453
8453
|
context: [
|
|
8454
|
-
`File: ${
|
|
8454
|
+
`File: ${path44}`,
|
|
8455
8455
|
`Edits in window: ${stamps.length}`,
|
|
8456
8456
|
`Window: ${Math.round(this.fileChurnWindowMs / 1e3)}s`
|
|
8457
8457
|
].join("\n")
|
|
8458
8458
|
});
|
|
8459
8459
|
return;
|
|
8460
8460
|
}
|
|
8461
|
-
if (this.editTimestamps.size >= 500 && !this.editTimestamps.has(
|
|
8461
|
+
if (this.editTimestamps.size >= 500 && !this.editTimestamps.has(path44)) {
|
|
8462
8462
|
for (const [p, times] of this.editTimestamps) {
|
|
8463
8463
|
if (times.every((t) => now - t > this.fileChurnWindowMs)) {
|
|
8464
8464
|
this.editTimestamps.delete(p);
|
|
@@ -8469,7 +8469,7 @@ var BrainMonitor = class {
|
|
|
8469
8469
|
if (oldest !== void 0) this.editTimestamps.delete(oldest);
|
|
8470
8470
|
}
|
|
8471
8471
|
}
|
|
8472
|
-
this.editTimestamps.set(
|
|
8472
|
+
this.editTimestamps.set(path44, stamps);
|
|
8473
8473
|
}
|
|
8474
8474
|
async engage(kind, input) {
|
|
8475
8475
|
const last = this.lastEngagedAt.get(kind) ?? 0;
|
|
@@ -9289,49 +9289,57 @@ async function expandGlob(pattern) {
|
|
|
9289
9289
|
async function walk(dir, pat) {
|
|
9290
9290
|
let entries;
|
|
9291
9291
|
try {
|
|
9292
|
-
entries = await fsp.readdir(dir);
|
|
9292
|
+
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
9293
9293
|
} catch {
|
|
9294
9294
|
return;
|
|
9295
9295
|
}
|
|
9296
|
-
|
|
9297
|
-
|
|
9298
|
-
|
|
9296
|
+
if (pat.startsWith("**/")) {
|
|
9297
|
+
const rest = pat.slice(3);
|
|
9298
|
+
await walk(dir, rest);
|
|
9299
9299
|
for (const e of entries) {
|
|
9300
|
-
if (
|
|
9301
|
-
const
|
|
9302
|
-
|
|
9300
|
+
if (e.isDirectory()) {
|
|
9301
|
+
const subDir = `${dir}${SEP}${e.name}`;
|
|
9302
|
+
await walk(subDir, pat);
|
|
9303
9303
|
}
|
|
9304
9304
|
}
|
|
9305
9305
|
return;
|
|
9306
9306
|
}
|
|
9307
|
-
|
|
9308
|
-
const rest = pat.slice(firstGlob);
|
|
9309
|
-
if (before.endsWith("**")) {
|
|
9310
|
-
await walk(dir, rest);
|
|
9307
|
+
if (pat === "**") {
|
|
9311
9308
|
for (const e of entries) {
|
|
9312
|
-
const full = `${dir}${SEP}${e}`;
|
|
9313
|
-
|
|
9314
|
-
|
|
9315
|
-
|
|
9316
|
-
} catch {
|
|
9309
|
+
const full = `${dir}${SEP}${e.name}`;
|
|
9310
|
+
results.add(abs ? resolve4(full) : full);
|
|
9311
|
+
if (e.isDirectory()) {
|
|
9312
|
+
await walk(full, "**");
|
|
9317
9313
|
}
|
|
9318
9314
|
}
|
|
9319
|
-
|
|
9320
|
-
|
|
9315
|
+
return;
|
|
9316
|
+
}
|
|
9317
|
+
const firstSlash = pat.indexOf("/");
|
|
9318
|
+
if (firstSlash < 0) {
|
|
9319
|
+
const re = globToRegex(pat);
|
|
9321
9320
|
for (const e of entries) {
|
|
9322
|
-
if (re.test(e)) {
|
|
9323
|
-
const full = `${dir}${SEP}${e}`;
|
|
9321
|
+
if (re.test(e.name)) {
|
|
9322
|
+
const full = `${dir}${SEP}${e.name}`;
|
|
9324
9323
|
results.add(abs ? resolve4(full) : full);
|
|
9325
9324
|
}
|
|
9326
9325
|
}
|
|
9326
|
+
return;
|
|
9327
|
+
}
|
|
9328
|
+
const currentSeg = pat.slice(0, firstSlash);
|
|
9329
|
+
const remainingPat = pat.slice(firstSlash + 1);
|
|
9330
|
+
if (isGlob(currentSeg)) {
|
|
9331
|
+
const re = globToRegex(currentSeg);
|
|
9332
|
+
for (const e of entries) {
|
|
9333
|
+
if (e.isDirectory() && re.test(e.name)) {
|
|
9334
|
+
const subDir = `${dir}${SEP}${e.name}`;
|
|
9335
|
+
await walk(subDir, remainingPat);
|
|
9336
|
+
}
|
|
9337
|
+
}
|
|
9327
9338
|
} else {
|
|
9328
|
-
const
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
const stat13 = await fsp.stat(full);
|
|
9333
|
-
if (stat13.isDirectory()) await walk(full, rest);
|
|
9334
|
-
} catch {
|
|
9339
|
+
for (const e of entries) {
|
|
9340
|
+
if (e.isDirectory() && e.name === currentSeg) {
|
|
9341
|
+
const subDir = `${dir}${SEP}${e.name}`;
|
|
9342
|
+
await walk(subDir, remainingPat);
|
|
9335
9343
|
}
|
|
9336
9344
|
}
|
|
9337
9345
|
}
|
|
@@ -10466,7 +10474,7 @@ async function gitOtherWorktrees(cwd, signal) {
|
|
|
10466
10474
|
return branches.slice(1);
|
|
10467
10475
|
}
|
|
10468
10476
|
function runGit(args, cwd, signal) {
|
|
10469
|
-
return new Promise((
|
|
10477
|
+
return new Promise((resolve17, reject) => {
|
|
10470
10478
|
let stdout = "";
|
|
10471
10479
|
let stderr = "";
|
|
10472
10480
|
const child = spawn("git", args, {
|
|
@@ -10485,7 +10493,7 @@ function runGit(args, cwd, signal) {
|
|
|
10485
10493
|
});
|
|
10486
10494
|
child.on("error", (err) => reject(err));
|
|
10487
10495
|
child.on("close", (code) => {
|
|
10488
|
-
if (code === 0)
|
|
10496
|
+
if (code === 0) resolve17(stdout);
|
|
10489
10497
|
else reject(new Error(stderr || `git ${args[0]} exited ${code}`));
|
|
10490
10498
|
});
|
|
10491
10499
|
});
|
|
@@ -11163,7 +11171,7 @@ function createDelegateTool(opts) {
|
|
|
11163
11171
|
};
|
|
11164
11172
|
}
|
|
11165
11173
|
async function awaitDelegateAttempt(director, subagentId, taskId, timeoutMs, abortSignal) {
|
|
11166
|
-
return new Promise((
|
|
11174
|
+
return new Promise((resolve17) => {
|
|
11167
11175
|
let settled = false;
|
|
11168
11176
|
let timer;
|
|
11169
11177
|
let offAbort = () => {
|
|
@@ -11176,7 +11184,7 @@ async function awaitDelegateAttempt(director, subagentId, taskId, timeoutMs, abo
|
|
|
11176
11184
|
offIter();
|
|
11177
11185
|
offProgress();
|
|
11178
11186
|
offAbort();
|
|
11179
|
-
|
|
11187
|
+
resolve17(value);
|
|
11180
11188
|
};
|
|
11181
11189
|
const arm = () => {
|
|
11182
11190
|
if (timer) clearTimeout(timer);
|
|
@@ -11797,31 +11805,31 @@ var ExploreCompanion = class {
|
|
|
11797
11805
|
// ── signal handlers ──────────────────────────────────────────────────────
|
|
11798
11806
|
trackToolExecuted(e) {
|
|
11799
11807
|
const tool = e.name.toLowerCase();
|
|
11800
|
-
const
|
|
11801
|
-
if (e.ok && this.cfg.signals.editUnreadFile && this.cfg.fileEditTools.has(tool) &&
|
|
11802
|
-
if (!this.readSet.has(
|
|
11808
|
+
const path44 = extractedPath(e.input);
|
|
11809
|
+
if (e.ok && this.cfg.signals.editUnreadFile && this.cfg.fileEditTools.has(tool) && path44) {
|
|
11810
|
+
if (!this.readSet.has(path44)) {
|
|
11803
11811
|
this.engage({
|
|
11804
11812
|
id: randomUUID6(),
|
|
11805
|
-
probe: `Map file ${
|
|
11806
|
-
hint: { file:
|
|
11807
|
-
context: `Leader edited ${
|
|
11813
|
+
probe: `Map file ${path44}: role, exports, dependencies, and callers \u2014 the leader is about to edit it.`,
|
|
11814
|
+
hint: { file: path44 },
|
|
11815
|
+
context: `Leader edited ${path44} without reading it first.`,
|
|
11808
11816
|
source: "edit_unread_file",
|
|
11809
|
-
subject: `file:${
|
|
11817
|
+
subject: `file:${path44}`,
|
|
11810
11818
|
createdAt: this.now()
|
|
11811
11819
|
});
|
|
11812
11820
|
}
|
|
11813
11821
|
return;
|
|
11814
11822
|
}
|
|
11815
|
-
if (e.ok && this.cfg.signals.unfamiliarRead && tool === "read" &&
|
|
11816
|
-
if (!this.readSet.has(
|
|
11817
|
-
this.readSet.add(
|
|
11823
|
+
if (e.ok && this.cfg.signals.unfamiliarRead && tool === "read" && path44) {
|
|
11824
|
+
if (!this.readSet.has(path44)) {
|
|
11825
|
+
this.readSet.add(path44);
|
|
11818
11826
|
this.engage({
|
|
11819
11827
|
id: randomUUID6(),
|
|
11820
|
-
probe: `Skeleton + callers + dependents of ${
|
|
11821
|
-
hint: { file:
|
|
11822
|
-
context: `Leader read unfamiliar file ${
|
|
11828
|
+
probe: `Skeleton + callers + dependents of ${path44}: what it exports, who imports it, and how it fits the feature flow.`,
|
|
11829
|
+
hint: { file: path44 },
|
|
11830
|
+
context: `Leader read unfamiliar file ${path44}.`,
|
|
11823
11831
|
source: "unfamiliar_read",
|
|
11824
|
-
subject: `file:${
|
|
11832
|
+
subject: `file:${path44}`,
|
|
11825
11833
|
createdAt: this.now()
|
|
11826
11834
|
});
|
|
11827
11835
|
}
|
|
@@ -12010,13 +12018,13 @@ function makeDependencyWatcherConfig(opts) {
|
|
|
12010
12018
|
const globPatterns = patterns.filter((p) => p.includes("*"));
|
|
12011
12019
|
const plainPatterns = patterns.filter((p) => !p.includes("*"));
|
|
12012
12020
|
function matchesPattern(filePath) {
|
|
12013
|
-
const
|
|
12014
|
-
if (plainPatterns.includes(
|
|
12021
|
+
const basename7 = filePath.split("/").pop()?.split("\\").pop() ?? "";
|
|
12022
|
+
if (plainPatterns.includes(basename7)) return true;
|
|
12015
12023
|
for (const gp of globPatterns) {
|
|
12016
12024
|
const regex = new RegExp(
|
|
12017
12025
|
"^" + gp.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$"
|
|
12018
12026
|
);
|
|
12019
|
-
if (regex.test(
|
|
12027
|
+
if (regex.test(basename7)) return true;
|
|
12020
12028
|
}
|
|
12021
12029
|
return false;
|
|
12022
12030
|
}
|
|
@@ -12988,10 +12996,10 @@ var DirectorTaskRegistry = class _DirectorTaskRegistry {
|
|
|
12988
12996
|
pending: taskIds.filter((id) => !done.has(id))
|
|
12989
12997
|
});
|
|
12990
12998
|
}
|
|
12991
|
-
return new Promise((
|
|
12999
|
+
return new Promise((resolve17) => {
|
|
12992
13000
|
const entry = {
|
|
12993
13001
|
ids: new Set(taskIds),
|
|
12994
|
-
resolve: (result) =>
|
|
13002
|
+
resolve: (result) => resolve17({
|
|
12995
13003
|
completed: [result],
|
|
12996
13004
|
pending: taskIds.filter((id) => id !== result.taskId)
|
|
12997
13005
|
})
|
|
@@ -12999,7 +13007,7 @@ var DirectorTaskRegistry = class _DirectorTaskRegistry {
|
|
|
12999
13007
|
if (opts?.timeoutMs !== void 0) {
|
|
13000
13008
|
entry.timer = setTimeout(() => {
|
|
13001
13009
|
this.anyWaiters.delete(entry);
|
|
13002
|
-
|
|
13010
|
+
resolve17({ completed: [], pending: [...taskIds], timedOut: true });
|
|
13003
13011
|
}, opts.timeoutMs);
|
|
13004
13012
|
}
|
|
13005
13013
|
this.anyWaiters.add(entry);
|
|
@@ -13117,11 +13125,11 @@ ${JSON.stringify(result.result, null, 2)}
|
|
|
13117
13125
|
this.makeStoppedResult(taskId, "director", `Unknown task id "${taskId}" \u2014 never assigned`)
|
|
13118
13126
|
);
|
|
13119
13127
|
}
|
|
13120
|
-
let
|
|
13128
|
+
let resolve17;
|
|
13121
13129
|
const promise = new Promise((done) => {
|
|
13122
|
-
|
|
13130
|
+
resolve17 = done;
|
|
13123
13131
|
});
|
|
13124
|
-
this.taskWaiters.set(taskId, { promise, resolve:
|
|
13132
|
+
this.taskWaiters.set(taskId, { promise, resolve: resolve17 });
|
|
13125
13133
|
return promise;
|
|
13126
13134
|
}
|
|
13127
13135
|
recordAssignment(task) {
|
|
@@ -13182,16 +13190,21 @@ ${JSON.stringify(result.result, null, 2)}
|
|
|
13182
13190
|
|
|
13183
13191
|
// src/coordination/director-tools.ts
|
|
13184
13192
|
import { randomUUID as randomUUID12 } from "node:crypto";
|
|
13185
|
-
|
|
13186
|
-
|
|
13187
|
-
|
|
13188
|
-
|
|
13189
|
-
|
|
13190
|
-
|
|
13191
|
-
|
|
13192
|
-
|
|
13193
|
-
|
|
13194
|
-
|
|
13193
|
+
|
|
13194
|
+
// src/coordination/kanban-dispatch-port.ts
|
|
13195
|
+
var notWired = () => {
|
|
13196
|
+
throw new Error(
|
|
13197
|
+
"Kanban dispatch port is not wired \u2014 register the implementation at the CLI composition root (see setKanbanDispatch)."
|
|
13198
|
+
);
|
|
13199
|
+
};
|
|
13200
|
+
var port = void 0;
|
|
13201
|
+
function setKanbanDispatch(impl) {
|
|
13202
|
+
port = impl;
|
|
13203
|
+
}
|
|
13204
|
+
function kanbanDispatch() {
|
|
13205
|
+
if (!port) notWired();
|
|
13206
|
+
return port;
|
|
13207
|
+
}
|
|
13195
13208
|
|
|
13196
13209
|
// src/coordination/director-input-helpers.ts
|
|
13197
13210
|
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
@@ -13212,8 +13225,21 @@ function instantiateRosterConfig2(role, base) {
|
|
|
13212
13225
|
};
|
|
13213
13226
|
}
|
|
13214
13227
|
|
|
13228
|
+
// src/coordination/kanban-ops-port.ts
|
|
13229
|
+
var notWired2 = () => {
|
|
13230
|
+
throw new Error(
|
|
13231
|
+
"KanbanBoundaryOpsPort is not wired \u2014 register the implementation at the CLI composition root (see setKanbanBoundaryOps)."
|
|
13232
|
+
);
|
|
13233
|
+
};
|
|
13234
|
+
var port2 = void 0;
|
|
13235
|
+
function setKanbanBoundaryOps(impl) {
|
|
13236
|
+
port2 = impl;
|
|
13237
|
+
}
|
|
13238
|
+
function kanbanBoundaryOps() {
|
|
13239
|
+
return port2 ?? notWired2();
|
|
13240
|
+
}
|
|
13241
|
+
|
|
13215
13242
|
// src/coordination/director-kanban-queue-helpers.ts
|
|
13216
|
-
import { describeKanbanBoundary } from "@wrongstack/kanban";
|
|
13217
13243
|
function normalizeKanbanQueueInput(input) {
|
|
13218
13244
|
const raw = input ?? {};
|
|
13219
13245
|
return {
|
|
@@ -13297,8 +13323,8 @@ function buildKanbanFleetTaskPrompt(board, task, lease) {
|
|
|
13297
13323
|
task.origin.taskId ? `sourceTaskId: ${task.origin.taskId}` : ""
|
|
13298
13324
|
].filter(Boolean).join("\n") : "";
|
|
13299
13325
|
const boundaries = [
|
|
13300
|
-
board.boundary?.enabled ? `board: ${describeKanbanBoundary(board.boundary)}` : "",
|
|
13301
|
-
task.boundary?.enabled ? `task: ${describeKanbanBoundary(task.boundary)}` : "",
|
|
13326
|
+
board.boundary?.enabled ? `board: ${kanbanBoundaryOps().describeKanbanBoundary(board.boundary)}` : "",
|
|
13327
|
+
task.boundary?.enabled ? `task: ${kanbanBoundaryOps().describeKanbanBoundary(task.boundary)}` : "",
|
|
13302
13328
|
...(board.boundary?.allow ?? []).map(
|
|
13303
13329
|
(selector) => `board allow ${selector.access} ${selector.kind}:${selector.path}`
|
|
13304
13330
|
),
|
|
@@ -15231,7 +15257,7 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
15231
15257
|
i.heartbeatIntervalMs ?? Math.floor(leaseTtlMs / 2)
|
|
15232
15258
|
);
|
|
15233
15259
|
const effectiveLeaseTtlMs = Math.max(leaseTtlMs, heartbeatIntervalMs * 2);
|
|
15234
|
-
const candidateTaskIds = i.taskId !== void 0 ? [i.taskId] : i.query ? (await listReadyTasks(projectRoot, {
|
|
15260
|
+
const candidateTaskIds = i.taskId !== void 0 ? [i.taskId] : i.query ? (await kanbanDispatch().listReadyTasks(projectRoot, {
|
|
15235
15261
|
...i.boardId !== void 0 ? { boardId: i.boardId } : {}
|
|
15236
15262
|
})).filter((candidate) => matchesKanbanQueueQuery(candidate.task, i.query ?? "")).slice(0, maxTasks).map((candidate) => candidate.task.id) : void 0;
|
|
15237
15263
|
const dispatches = [];
|
|
@@ -15242,7 +15268,7 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
15242
15268
|
const candidateTaskId = candidateTaskIds?.[index];
|
|
15243
15269
|
if (candidateTaskIds && !candidateTaskId) break;
|
|
15244
15270
|
if (candidateTaskId && budgetRejectedTaskIds.has(candidateTaskId)) continue;
|
|
15245
|
-
const reserved = await reserveKanbanDispatch(projectRoot, {
|
|
15271
|
+
const reserved = await kanbanDispatch().reserveKanbanDispatch(projectRoot, {
|
|
15246
15272
|
...i.boardId !== void 0 ? { boardId: i.boardId } : {},
|
|
15247
15273
|
...candidateTaskId !== void 0 ? { taskId: candidateTaskId } : {},
|
|
15248
15274
|
routing: {
|
|
@@ -15272,7 +15298,7 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
15272
15298
|
if (remaining !== void 0 && remaining < costCeiling) {
|
|
15273
15299
|
budgetRejectedTaskIds.add(claim.task.id);
|
|
15274
15300
|
const budgetError = `Cost ceiling ${costCeiling} exceeds remaining budget ${remaining.toFixed(4)}`;
|
|
15275
|
-
await updateTaskAssignment(
|
|
15301
|
+
await kanbanDispatch().updateTaskAssignment(
|
|
15276
15302
|
projectRoot,
|
|
15277
15303
|
claim.board.id,
|
|
15278
15304
|
claim.task.id,
|
|
@@ -15323,7 +15349,7 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
15323
15349
|
}
|
|
15324
15350
|
}
|
|
15325
15351
|
};
|
|
15326
|
-
const started = await startKanbanDispatch(projectRoot, {
|
|
15352
|
+
const started = await kanbanDispatch().startKanbanDispatch(projectRoot, {
|
|
15327
15353
|
boardId: claim.board.id,
|
|
15328
15354
|
taskId: claim.task.id,
|
|
15329
15355
|
leaseId: ourLeaseId,
|
|
@@ -15368,7 +15394,7 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
15368
15394
|
message = `${message}; cleanup failed for spawned subagent ${subagentId}: ${toErrorMessage(cleanupErr)}`;
|
|
15369
15395
|
}
|
|
15370
15396
|
}
|
|
15371
|
-
await failKanbanDispatch(projectRoot, {
|
|
15397
|
+
await kanbanDispatch().failKanbanDispatch(projectRoot, {
|
|
15372
15398
|
boardId: claim.board.id,
|
|
15373
15399
|
taskId: claim.task.id,
|
|
15374
15400
|
leaseId: ourLeaseId,
|
|
@@ -15431,7 +15457,7 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
15431
15457
|
for (const dispatch of dispatches) {
|
|
15432
15458
|
if (!runTaskIds.has(dispatch.runTaskId)) continue;
|
|
15433
15459
|
try {
|
|
15434
|
-
const board = await getBoard(projectRoot, dispatch.boardId);
|
|
15460
|
+
const board = await kanbanDispatch().getBoard(projectRoot, dispatch.boardId);
|
|
15435
15461
|
const liveTask = board?.tasks.find((t) => t.id === dispatch.taskId);
|
|
15436
15462
|
const liveLeaseId = liveTask?.assignment?.leaseId;
|
|
15437
15463
|
if (liveLeaseId !== void 0 && liveLeaseId !== dispatch.leaseId) {
|
|
@@ -15443,10 +15469,15 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
15443
15469
|
} catch {
|
|
15444
15470
|
}
|
|
15445
15471
|
try {
|
|
15446
|
-
await heartbeatTaskAssignment(
|
|
15447
|
-
|
|
15448
|
-
|
|
15449
|
-
|
|
15472
|
+
await kanbanDispatch().heartbeatTaskAssignment(
|
|
15473
|
+
projectRoot,
|
|
15474
|
+
dispatch.boardId,
|
|
15475
|
+
dispatch.taskId,
|
|
15476
|
+
{
|
|
15477
|
+
leaseExpiresAt: refreshedExpiry,
|
|
15478
|
+
expectedLeaseId: dispatch.leaseId
|
|
15479
|
+
}
|
|
15480
|
+
);
|
|
15450
15481
|
} catch {
|
|
15451
15482
|
}
|
|
15452
15483
|
}
|
|
@@ -15469,7 +15500,7 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
15469
15500
|
if (!dispatch) continue;
|
|
15470
15501
|
runTaskIds.delete(dispatch.runTaskId);
|
|
15471
15502
|
if (result.status === "success") {
|
|
15472
|
-
const completed = await completeKanbanDispatch(projectRoot, {
|
|
15503
|
+
const completed = await kanbanDispatch().completeKanbanDispatch(projectRoot, {
|
|
15473
15504
|
boardId: dispatch.boardId,
|
|
15474
15505
|
taskId: dispatch.taskId,
|
|
15475
15506
|
leaseId: dispatch.leaseId,
|
|
@@ -15485,7 +15516,7 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
15485
15516
|
});
|
|
15486
15517
|
}
|
|
15487
15518
|
} else {
|
|
15488
|
-
const failed = await failKanbanDispatch(projectRoot, {
|
|
15519
|
+
const failed = await kanbanDispatch().failKanbanDispatch(projectRoot, {
|
|
15489
15520
|
boardId: dispatch.boardId,
|
|
15490
15521
|
taskId: dispatch.taskId,
|
|
15491
15522
|
leaseId: dispatch.leaseId,
|
|
@@ -15537,7 +15568,7 @@ async function renewAndRevokeLease(opts) {
|
|
|
15537
15568
|
} = opts;
|
|
15538
15569
|
let revoked = false;
|
|
15539
15570
|
try {
|
|
15540
|
-
const board = await getBoard(projectRoot, boardId);
|
|
15571
|
+
const board = await kanbanDispatch().getBoard(projectRoot, boardId);
|
|
15541
15572
|
const liveTask = board?.tasks.find((t) => t.id === taskId);
|
|
15542
15573
|
const liveLeaseId = liveTask?.assignment?.leaseId;
|
|
15543
15574
|
if (liveLeaseId !== void 0 && liveLeaseId !== ourLeaseId) {
|
|
@@ -15550,7 +15581,7 @@ async function renewAndRevokeLease(opts) {
|
|
|
15550
15581
|
} catch {
|
|
15551
15582
|
}
|
|
15552
15583
|
if (!revoked) {
|
|
15553
|
-
await heartbeatTaskAssignment(projectRoot, boardId, taskId, {
|
|
15584
|
+
await kanbanDispatch().heartbeatTaskAssignment(projectRoot, boardId, taskId, {
|
|
15554
15585
|
leaseExpiresAt: refreshedExpiry,
|
|
15555
15586
|
expectedLeaseId: ourLeaseId
|
|
15556
15587
|
}).catch(() => {
|
|
@@ -15653,12 +15684,12 @@ function rosterSummaryFromConfigs(roster) {
|
|
|
15653
15684
|
|
|
15654
15685
|
// src/coordination/director-session.ts
|
|
15655
15686
|
import * as fsp24 from "node:fs/promises";
|
|
15656
|
-
import * as
|
|
15687
|
+
import * as path33 from "node:path";
|
|
15657
15688
|
|
|
15658
15689
|
// src/storage/session-store.ts
|
|
15659
15690
|
import { randomUUID as randomUUID14 } from "node:crypto";
|
|
15660
15691
|
import * as fsp23 from "node:fs/promises";
|
|
15661
|
-
import * as
|
|
15692
|
+
import * as path32 from "node:path";
|
|
15662
15693
|
|
|
15663
15694
|
// src/session-catalog/client.ts
|
|
15664
15695
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -15736,7 +15767,7 @@ function normalize2(value) {
|
|
|
15736
15767
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
15737
15768
|
}
|
|
15738
15769
|
function delay(ms) {
|
|
15739
|
-
return new Promise((
|
|
15770
|
+
return new Promise((resolve17) => setTimeout(resolve17, ms));
|
|
15740
15771
|
}
|
|
15741
15772
|
var SessionCatalogProjectClient = class {
|
|
15742
15773
|
constructor(options) {
|
|
@@ -15826,8 +15857,8 @@ var SessionCatalogProjectClient = class {
|
|
|
15826
15857
|
this.socket = null;
|
|
15827
15858
|
this.info = null;
|
|
15828
15859
|
if (socket && !socket.destroyed)
|
|
15829
|
-
await new Promise((
|
|
15830
|
-
socket.once("close",
|
|
15860
|
+
await new Promise((resolve17) => {
|
|
15861
|
+
socket.once("close", resolve17);
|
|
15831
15862
|
socket.end();
|
|
15832
15863
|
});
|
|
15833
15864
|
}
|
|
@@ -15865,7 +15896,7 @@ var SessionCatalogProjectClient = class {
|
|
|
15865
15896
|
this.info = null;
|
|
15866
15897
|
this.authToken = void 0;
|
|
15867
15898
|
this.buffer = "";
|
|
15868
|
-
return new Promise((
|
|
15899
|
+
return new Promise((resolve17, reject) => {
|
|
15869
15900
|
const socket = net.createConnection(this.endpoint);
|
|
15870
15901
|
this.socket = socket;
|
|
15871
15902
|
socket.setEncoding("utf8");
|
|
@@ -15878,7 +15909,7 @@ var SessionCatalogProjectClient = class {
|
|
|
15878
15909
|
clearTimeout(timer);
|
|
15879
15910
|
this.connectResolve = null;
|
|
15880
15911
|
this.connectReject = null;
|
|
15881
|
-
|
|
15912
|
+
resolve17();
|
|
15882
15913
|
};
|
|
15883
15914
|
this.connectReject = (error) => {
|
|
15884
15915
|
clearTimeout(timer);
|
|
@@ -15923,7 +15954,7 @@ var SessionCatalogProjectClient = class {
|
|
|
15923
15954
|
});
|
|
15924
15955
|
if (encoded.length > SESSION_CATALOG_MAX_FRAME_CHARS)
|
|
15925
15956
|
return Promise.reject(new Error("Session Catalog request exceeded frame limit"));
|
|
15926
|
-
return new Promise((
|
|
15957
|
+
return new Promise((resolve17, reject) => {
|
|
15927
15958
|
const timer = setTimeout(() => {
|
|
15928
15959
|
const pending = this.pending.get(id);
|
|
15929
15960
|
if (!pending) return;
|
|
@@ -15935,7 +15966,7 @@ var SessionCatalogProjectClient = class {
|
|
|
15935
15966
|
);
|
|
15936
15967
|
}, timeoutMs);
|
|
15937
15968
|
timer.unref?.();
|
|
15938
|
-
this.pending.set(id, { resolve:
|
|
15969
|
+
this.pending.set(id, { resolve: resolve17, reject, timer });
|
|
15939
15970
|
socket.write(encoded);
|
|
15940
15971
|
});
|
|
15941
15972
|
}
|
|
@@ -16349,6 +16380,29 @@ var SessionSummaryTracker = class {
|
|
|
16349
16380
|
get currentSummary() {
|
|
16350
16381
|
return this.summary;
|
|
16351
16382
|
}
|
|
16383
|
+
/**
|
|
16384
|
+
* Non-destructive materialization of every live counter into a summary
|
|
16385
|
+
* snapshot. Unlike finalize(), it stamps no endedAt, applies no final
|
|
16386
|
+
* outcome, and resolves no name — mid-session metadata checkpoints use it
|
|
16387
|
+
* so a killed process leaves accurate listing metadata behind without
|
|
16388
|
+
* pretending the session ended cleanly.
|
|
16389
|
+
*/
|
|
16390
|
+
snapshot() {
|
|
16391
|
+
const { lastUserMessage: _lastUserMessage, ...rest } = this.summary;
|
|
16392
|
+
return {
|
|
16393
|
+
...rest,
|
|
16394
|
+
messageCount: this.messageCount,
|
|
16395
|
+
...this.lastUserMessage !== void 0 ? { lastUserMessage: this.lastUserMessage } : {},
|
|
16396
|
+
iterationCount: this.iterationCount,
|
|
16397
|
+
toolCallCount: this.toolCallCount,
|
|
16398
|
+
toolErrorCount: this.toolErrorCount,
|
|
16399
|
+
fileChangeCount: this.fileChangeCount,
|
|
16400
|
+
compactionCount: this.compactionCount > 0 ? this.compactionCount : void 0,
|
|
16401
|
+
toolBreakdown: { ...this.toolBreakdown },
|
|
16402
|
+
lastActivityAt: this.lastActivityAt,
|
|
16403
|
+
...this.outcome !== void 0 ? { outcome: this.outcome } : {}
|
|
16404
|
+
};
|
|
16405
|
+
}
|
|
16352
16406
|
get pendingToolUses() {
|
|
16353
16407
|
return Array.from(this.openToolUses);
|
|
16354
16408
|
}
|
|
@@ -16541,6 +16595,12 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
|
|
|
16541
16595
|
lastAppendWarnAt = 0;
|
|
16542
16596
|
writeChain = Promise.resolve();
|
|
16543
16597
|
flushPromise = null;
|
|
16598
|
+
/**
|
|
16599
|
+
* Batch currently inside enqueueWrite. `flushSync` may steal it if the
|
|
16600
|
+
* async append has not started, so a dying process writes in-flight +
|
|
16601
|
+
* remaining buffer as one ordered append instead of racing a second fd.
|
|
16602
|
+
*/
|
|
16603
|
+
inFlight = null;
|
|
16544
16604
|
get isClosed() {
|
|
16545
16605
|
return false;
|
|
16546
16606
|
}
|
|
@@ -16581,14 +16641,20 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
|
|
|
16581
16641
|
return true;
|
|
16582
16642
|
}
|
|
16583
16643
|
enqueueWrite(data) {
|
|
16644
|
+
return this.enqueueFlight({ data, stolen: false, started: false });
|
|
16645
|
+
}
|
|
16646
|
+
enqueueFlight(flight) {
|
|
16584
16647
|
const write = this.writeChain.then(async () => {
|
|
16648
|
+
if (flight.stolen) return;
|
|
16649
|
+
flight.started = true;
|
|
16650
|
+
if (flight.stolen) return;
|
|
16585
16651
|
try {
|
|
16586
|
-
return await this.opts.getHandle().appendFile(data, "utf8");
|
|
16652
|
+
return await this.opts.getHandle().appendFile(flight.data, "utf8");
|
|
16587
16653
|
} catch (err) {
|
|
16588
16654
|
if (isClosedHandleError(err)) {
|
|
16589
16655
|
const reloaded = await fsp7.open(this.opts.filePath, "a", 384);
|
|
16590
16656
|
this.opts.setHandle(reloaded);
|
|
16591
|
-
return await reloaded.appendFile(data, "utf8");
|
|
16657
|
+
return await reloaded.appendFile(flight.data, "utf8");
|
|
16592
16658
|
}
|
|
16593
16659
|
throw err;
|
|
16594
16660
|
}
|
|
@@ -16613,17 +16679,29 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
|
|
|
16613
16679
|
this.flushTimer = null;
|
|
16614
16680
|
}
|
|
16615
16681
|
}
|
|
16616
|
-
async flushBuffer(isClosed = false) {
|
|
16617
|
-
if (this.flushPromise)
|
|
16682
|
+
async flushBuffer(isClosed = false, opts = {}) {
|
|
16683
|
+
if (this.flushPromise) {
|
|
16684
|
+
const joined = this.flushPromise;
|
|
16685
|
+
const continueIfDirty = () => {
|
|
16686
|
+
if (this.writeBuffer.length === 0) return Promise.resolve();
|
|
16687
|
+
return this.flushBuffer(isClosed, opts);
|
|
16688
|
+
};
|
|
16689
|
+
if (opts.datasync === true) {
|
|
16690
|
+
return joined.then(
|
|
16691
|
+
() => this.opts.getHandle().datasync().catch(() => void 0).then(continueIfDirty)
|
|
16692
|
+
);
|
|
16693
|
+
}
|
|
16694
|
+
return joined.then(continueIfDirty);
|
|
16695
|
+
}
|
|
16618
16696
|
const flush = (async () => {
|
|
16619
|
-
while (this.writeBuffer.length > 0) await this.flushBufferOnce(isClosed);
|
|
16697
|
+
while (this.writeBuffer.length > 0) await this.flushBufferOnce(isClosed, opts);
|
|
16620
16698
|
})().finally(() => {
|
|
16621
16699
|
if (this.flushPromise === flush) this.flushPromise = null;
|
|
16622
16700
|
});
|
|
16623
16701
|
this.flushPromise = flush;
|
|
16624
16702
|
return flush;
|
|
16625
16703
|
}
|
|
16626
|
-
async flushBufferOnce(isClosed) {
|
|
16704
|
+
async flushBufferOnce(isClosed, opts) {
|
|
16627
16705
|
if (this.writeBuffer.length === 0) return;
|
|
16628
16706
|
const events = this.writeBuffer;
|
|
16629
16707
|
const eventCount = events.length;
|
|
@@ -16631,12 +16709,21 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
|
|
|
16631
16709
|
const batch = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
16632
16710
|
this.writeBuffer = [];
|
|
16633
16711
|
this.writeBufferBytes = 0;
|
|
16712
|
+
const flight = { data: batch, stolen: false, started: false };
|
|
16713
|
+
this.inFlight = flight;
|
|
16634
16714
|
const t0 = Date.now();
|
|
16635
16715
|
let outcome = "success";
|
|
16636
16716
|
let errorMsg;
|
|
16637
16717
|
try {
|
|
16638
|
-
await this.
|
|
16718
|
+
await this.enqueueFlight(flight);
|
|
16719
|
+
if (flight.stolen) {
|
|
16720
|
+
return;
|
|
16721
|
+
}
|
|
16722
|
+
if (opts?.datasync === true) {
|
|
16723
|
+
await this.opts.getHandle().datasync().catch(() => void 0);
|
|
16724
|
+
}
|
|
16639
16725
|
} catch (err) {
|
|
16726
|
+
if (flight.stolen) return;
|
|
16640
16727
|
outcome = "failure";
|
|
16641
16728
|
errorMsg = toErrorMessage(err);
|
|
16642
16729
|
const newer = this.writeBuffer;
|
|
@@ -16674,6 +16761,7 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
|
|
|
16674
16761
|
...eventCount !== void 0 ? { eventCount } : {},
|
|
16675
16762
|
...this.opts.getTraceId?.() ? { traceId: this.opts.getTraceId() } : {}
|
|
16676
16763
|
});
|
|
16764
|
+
if (this.inFlight === flight) this.inFlight = null;
|
|
16677
16765
|
}
|
|
16678
16766
|
}
|
|
16679
16767
|
async drainWriteChain() {
|
|
@@ -16682,18 +16770,56 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
|
|
|
16682
16770
|
async drainFlushPromise() {
|
|
16683
16771
|
await this.flushPromise?.catch(() => void 0);
|
|
16684
16772
|
}
|
|
16773
|
+
/**
|
|
16774
|
+
* Last-gasp synchronous append (SIGKILL/SIGTERM traps, `process.on('exit')`).
|
|
16775
|
+
*
|
|
16776
|
+
* Failure contract: nothing is discarded before the write is known to have
|
|
16777
|
+
* landed. Buffered events stay in `writeBuffer` — it is cleared only after
|
|
16778
|
+
* `fsyncSync` returns — and a stolen in-flight batch is handed back to the
|
|
16779
|
+
* async write chain, so a survivable failure (EACCES, ENOSPC, EMFILE) loses
|
|
16780
|
+
* nothing. Failure is never silent: a structured `session.flush_sync_failed`
|
|
16781
|
+
* warning names exactly what was still pending, which is what SIGKILL-trap
|
|
16782
|
+
* tests assert against.
|
|
16783
|
+
*/
|
|
16685
16784
|
flushSync() {
|
|
16686
|
-
if (
|
|
16785
|
+
if (!this.opts.filePath) return;
|
|
16687
16786
|
this.cancelTimer();
|
|
16688
|
-
const
|
|
16689
|
-
|
|
16690
|
-
|
|
16787
|
+
const chunks = [];
|
|
16788
|
+
const flight = this.inFlight;
|
|
16789
|
+
const stole = flight !== null && !flight.started && !flight.stolen;
|
|
16790
|
+
if (stole && flight) {
|
|
16791
|
+
flight.stolen = true;
|
|
16792
|
+
chunks.push(flight.data);
|
|
16793
|
+
}
|
|
16794
|
+
const events = this.writeBuffer;
|
|
16795
|
+
if (events.length > 0) {
|
|
16796
|
+
chunks.push(events.map((e) => JSON.stringify(e)).join("\n") + "\n");
|
|
16797
|
+
}
|
|
16798
|
+
if (chunks.length === 0) return;
|
|
16691
16799
|
let fd;
|
|
16692
16800
|
try {
|
|
16693
16801
|
fd = openSync(this.opts.filePath, "a");
|
|
16694
|
-
writeSync(fd,
|
|
16802
|
+
writeSync(fd, chunks.join(""), null, "utf8");
|
|
16695
16803
|
fsyncSync(fd);
|
|
16696
|
-
|
|
16804
|
+
if (this.writeBuffer === events) {
|
|
16805
|
+
this.writeBuffer = [];
|
|
16806
|
+
this.writeBufferBytes = 0;
|
|
16807
|
+
}
|
|
16808
|
+
} catch (err) {
|
|
16809
|
+
if (stole && flight) flight.stolen = false;
|
|
16810
|
+
console.warn(
|
|
16811
|
+
JSON.stringify({
|
|
16812
|
+
level: "error",
|
|
16813
|
+
event: "session.flush_sync_failed",
|
|
16814
|
+
sessionId: this.opts.sessionId,
|
|
16815
|
+
filePath: this.opts.filePath,
|
|
16816
|
+
message: toErrorMessage(err),
|
|
16817
|
+
pendingEvents: events.length,
|
|
16818
|
+
pendingBytes: this.writeBufferBytes,
|
|
16819
|
+
hadInFlightBatch: stole,
|
|
16820
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
16821
|
+
})
|
|
16822
|
+
);
|
|
16697
16823
|
} finally {
|
|
16698
16824
|
if (fd !== void 0) {
|
|
16699
16825
|
try {
|
|
@@ -16861,6 +16987,17 @@ function isClosedHandleError2(err) {
|
|
|
16861
16987
|
const code = err?.code;
|
|
16862
16988
|
return code === "EBADF" || code === "ERR_CLOSED_RESOURCE" || code === "ERR_INVALID_HANDLE";
|
|
16863
16989
|
}
|
|
16990
|
+
var CRITICAL_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
16991
|
+
"user_input",
|
|
16992
|
+
"llm_response",
|
|
16993
|
+
"checkpoint",
|
|
16994
|
+
"in_flight_start",
|
|
16995
|
+
"in_flight_end"
|
|
16996
|
+
]);
|
|
16997
|
+
function isCriticalEvent(event) {
|
|
16998
|
+
return CRITICAL_EVENT_TYPES.has(event.type);
|
|
16999
|
+
}
|
|
17000
|
+
var METADATA_CHECKPOINT_INTERVAL_MS = 1e4;
|
|
16864
17001
|
var FileSessionWriter = class _FileSessionWriter {
|
|
16865
17002
|
constructor(id, handle, startedAt, meta, events, opts = {}, traceId) {
|
|
16866
17003
|
this.id = id;
|
|
@@ -16876,6 +17013,8 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
16876
17013
|
this._onAppend = opts.onAppend;
|
|
16877
17014
|
this._onAppendBatch = opts.onAppendBatch;
|
|
16878
17015
|
this.onCloseCb = opts.onClose;
|
|
17016
|
+
this.onMetadataCheckpointCb = opts.onMetadataCheckpoint;
|
|
17017
|
+
this.metadataCheckpointMs = opts.metadataCheckpointMs ?? METADATA_CHECKPOINT_INTERVAL_MS;
|
|
16879
17018
|
this.summaryTracker = new SessionSummaryTracker({
|
|
16880
17019
|
id,
|
|
16881
17020
|
startedAt,
|
|
@@ -16943,6 +17082,15 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
16943
17082
|
this._onAppendBatch = cb;
|
|
16944
17083
|
}
|
|
16945
17084
|
onCloseCb;
|
|
17085
|
+
/** Mid-session metadata checkpoint throttle. 0 disables checkpointing. */
|
|
17086
|
+
metadataCheckpointMs;
|
|
17087
|
+
/** One-shot guard for the "interval set but no sink" warning below. */
|
|
17088
|
+
_checkpointNoSinkWarned = false;
|
|
17089
|
+
onMetadataCheckpointCb;
|
|
17090
|
+
/** Set whenever summary counters changed since the last metadata checkpoint. */
|
|
17091
|
+
metadataDirty = false;
|
|
17092
|
+
metadataTimer = null;
|
|
17093
|
+
metadataCheckpointInFlight = null;
|
|
16946
17094
|
/** Implements SessionWriter.traceId — propagated from ContextInit.traceId. */
|
|
16947
17095
|
traceId;
|
|
16948
17096
|
/**
|
|
@@ -16974,18 +17122,31 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
16974
17122
|
void this.ensureInit();
|
|
16975
17123
|
const appendEvent = scrubSessionWriterEvent(event, this.secretScrubber);
|
|
16976
17124
|
this.summaryTracker.observe(appendEvent);
|
|
17125
|
+
this.metadataDirty = true;
|
|
17126
|
+
this.scheduleMetadataCheckpoint();
|
|
16977
17127
|
try {
|
|
16978
17128
|
this._onAppend?.(appendEvent);
|
|
16979
17129
|
} catch {
|
|
16980
17130
|
}
|
|
17131
|
+
const critical = isCriticalEvent(appendEvent);
|
|
16981
17132
|
if (!this.buffer.push(appendEvent)) {
|
|
16982
17133
|
this.buffer.cancelTimer();
|
|
16983
|
-
void this.buffer.flushBuffer(this.closed).catch(() => void 0).then(() => {
|
|
16984
|
-
this.buffer.push(appendEvent)
|
|
17134
|
+
void this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => void 0).then(() => {
|
|
17135
|
+
if (this.buffer.push(appendEvent)) {
|
|
17136
|
+
if (!critical) return;
|
|
17137
|
+
this.buffer.cancelTimer();
|
|
17138
|
+
void this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => void 0);
|
|
17139
|
+
return;
|
|
17140
|
+
}
|
|
17141
|
+
void this.buffer.drainWriteChain().then(() => this.buffer.enqueueWrite(`${JSON.stringify(appendEvent)}
|
|
17142
|
+
`)).then(() => {
|
|
17143
|
+
if (!critical) return;
|
|
17144
|
+
return this.handle.datasync().catch(() => void 0);
|
|
17145
|
+
}).catch(() => void 0);
|
|
16985
17146
|
});
|
|
16986
|
-
} else if (this.buffer.shouldFlushNow()) {
|
|
17147
|
+
} else if (critical || this.buffer.shouldFlushNow()) {
|
|
16987
17148
|
this.buffer.cancelTimer();
|
|
16988
|
-
void this.buffer.flushBuffer(this.closed).catch(() => {
|
|
17149
|
+
void this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => {
|
|
16989
17150
|
});
|
|
16990
17151
|
} else {
|
|
16991
17152
|
this.buffer.scheduleFlush(this.closed);
|
|
@@ -17056,23 +17217,112 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
17056
17217
|
provider: this.meta.provider ?? "unknown"
|
|
17057
17218
|
});
|
|
17058
17219
|
}
|
|
17220
|
+
/**
|
|
17221
|
+
* Arm the mid-session metadata checkpoint timer if it is not already armed.
|
|
17222
|
+
* Called after every observed event; the timer itself is unref'd so an idle
|
|
17223
|
+
* session never keeps the process alive for a cosmetic sidecar refresh.
|
|
17224
|
+
*/
|
|
17225
|
+
scheduleMetadataCheckpoint() {
|
|
17226
|
+
if (this.closed || this.metadataTimer) return;
|
|
17227
|
+
if (this.metadataCheckpointMs <= 0) return;
|
|
17228
|
+
if (!this.onMetadataCheckpointCb && !this.manifestFile) {
|
|
17229
|
+
if (!this._checkpointNoSinkWarned) {
|
|
17230
|
+
this._checkpointNoSinkWarned = true;
|
|
17231
|
+
console.warn(
|
|
17232
|
+
JSON.stringify({
|
|
17233
|
+
level: "warn",
|
|
17234
|
+
event: "session.metadata_checkpoint_no_sink",
|
|
17235
|
+
sessionId: this.id,
|
|
17236
|
+
message: "metadataCheckpointMs set but neither onMetadataCheckpoint nor manifestFile configured; mid-session checkpoints disabled.",
|
|
17237
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
17238
|
+
})
|
|
17239
|
+
);
|
|
17240
|
+
}
|
|
17241
|
+
return;
|
|
17242
|
+
}
|
|
17243
|
+
this.metadataTimer = setTimeout(() => {
|
|
17244
|
+
this.metadataTimer = null;
|
|
17245
|
+
void this.runMetadataCheckpoint();
|
|
17246
|
+
}, this.metadataCheckpointMs);
|
|
17247
|
+
this.metadataTimer.unref?.();
|
|
17248
|
+
}
|
|
17249
|
+
/**
|
|
17250
|
+
* Persist a mid-session summary snapshot: the `.summary.json` sidecar under
|
|
17251
|
+
* the manifest lock, then the store-level index row / catalog upsert via
|
|
17252
|
+
* `onMetadataCheckpoint`. Runs at most once per throttle interval and only
|
|
17253
|
+
* when summary counters changed since the last checkpoint; a failed
|
|
17254
|
+
* checkpoint stays dirty and retries on the next armed tick.
|
|
17255
|
+
*/
|
|
17256
|
+
runMetadataCheckpoint() {
|
|
17257
|
+
if (this.closed || !this.metadataDirty) return Promise.resolve();
|
|
17258
|
+
if (this.metadataCheckpointInFlight) return this.metadataCheckpointInFlight;
|
|
17259
|
+
const {
|
|
17260
|
+
endedAt: _priorEndedAt,
|
|
17261
|
+
outcome: _priorOutcome,
|
|
17262
|
+
...snapshot
|
|
17263
|
+
} = this.summaryTracker.snapshot();
|
|
17264
|
+
const run = (async () => {
|
|
17265
|
+
const t0 = Date.now();
|
|
17266
|
+
let outcome = "success";
|
|
17267
|
+
let errorMsg;
|
|
17268
|
+
try {
|
|
17269
|
+
if (this.manifestFile) {
|
|
17270
|
+
await withFileLock(this.manifestFile, async () => {
|
|
17271
|
+
await atomicWrite(this.manifestFile, JSON.stringify(snapshot), { mode: 384 });
|
|
17272
|
+
});
|
|
17273
|
+
}
|
|
17274
|
+
this.metadataDirty = false;
|
|
17275
|
+
await this.onMetadataCheckpointCb?.(snapshot);
|
|
17276
|
+
if (this.metadataDirty && !this.closed) this.scheduleMetadataCheckpoint();
|
|
17277
|
+
} catch (err) {
|
|
17278
|
+
outcome = "failure";
|
|
17279
|
+
errorMsg = toErrorMessage(err);
|
|
17280
|
+
this.metadataDirty = true;
|
|
17281
|
+
this.scheduleMetadataCheckpoint();
|
|
17282
|
+
} finally {
|
|
17283
|
+
this.metadataCheckpointInFlight = null;
|
|
17284
|
+
this.events?.emit("storage.write", {
|
|
17285
|
+
sessionId: this.id,
|
|
17286
|
+
store: "session",
|
|
17287
|
+
filePath: this.manifestFile || this.filePath,
|
|
17288
|
+
operation: "metadata_checkpoint",
|
|
17289
|
+
outcome,
|
|
17290
|
+
durationMs: Date.now() - t0,
|
|
17291
|
+
...errorMsg !== void 0 ? { error: errorMsg } : {},
|
|
17292
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
17293
|
+
});
|
|
17294
|
+
}
|
|
17295
|
+
})();
|
|
17296
|
+
this.metadataCheckpointInFlight = run;
|
|
17297
|
+
return run;
|
|
17298
|
+
}
|
|
17059
17299
|
async append(event) {
|
|
17060
17300
|
if (this.closed) return;
|
|
17061
17301
|
await this.ensureInit();
|
|
17062
17302
|
const scrubbed = scrubSessionWriterEvent(event, this.secretScrubber);
|
|
17063
17303
|
this.summaryTracker.observe(scrubbed);
|
|
17304
|
+
this.metadataDirty = true;
|
|
17305
|
+
this.scheduleMetadataCheckpoint();
|
|
17064
17306
|
try {
|
|
17065
17307
|
this._onAppend?.(scrubbed);
|
|
17066
17308
|
} catch {
|
|
17067
17309
|
}
|
|
17068
|
-
|
|
17310
|
+
let pushed = this.buffer.push(scrubbed);
|
|
17311
|
+
if (!pushed) {
|
|
17069
17312
|
this.buffer.cancelTimer();
|
|
17070
|
-
await this.buffer.flushBuffer(this.closed).catch(() => void 0);
|
|
17071
|
-
this.buffer.push(scrubbed);
|
|
17313
|
+
await this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => void 0);
|
|
17314
|
+
pushed = this.buffer.push(scrubbed);
|
|
17315
|
+
if (!pushed) {
|
|
17316
|
+
await this.buffer.drainWriteChain().then(() => this.buffer.enqueueWrite(`${JSON.stringify(scrubbed)}
|
|
17317
|
+
`)).then(() => {
|
|
17318
|
+
if (!isCriticalEvent(scrubbed)) return;
|
|
17319
|
+
return this.handle.datasync().catch(() => void 0);
|
|
17320
|
+
}).catch(() => void 0);
|
|
17321
|
+
}
|
|
17072
17322
|
}
|
|
17073
|
-
if (this.buffer.shouldFlushNow()) {
|
|
17323
|
+
if (isCriticalEvent(scrubbed) || this.buffer.shouldFlushNow()) {
|
|
17074
17324
|
this.buffer.cancelTimer();
|
|
17075
|
-
await this.buffer.flushBuffer(this.closed).catch(() => {
|
|
17325
|
+
await this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => {
|
|
17076
17326
|
});
|
|
17077
17327
|
} else {
|
|
17078
17328
|
this.buffer.scheduleFlush(this.closed);
|
|
@@ -17089,20 +17339,33 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
17089
17339
|
this._onAppend?.(scrubbed);
|
|
17090
17340
|
} catch {
|
|
17091
17341
|
}
|
|
17092
|
-
|
|
17342
|
+
let pushed = this.buffer.push(scrubbed);
|
|
17343
|
+
if (!pushed) {
|
|
17093
17344
|
this.buffer.cancelTimer();
|
|
17094
|
-
await this.buffer.flushBuffer(this.closed).catch(() => void 0);
|
|
17095
|
-
this.buffer.push(scrubbed);
|
|
17345
|
+
await this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => void 0);
|
|
17346
|
+
pushed = this.buffer.push(scrubbed);
|
|
17347
|
+
if (!pushed) {
|
|
17348
|
+
await this.buffer.drainWriteChain().then(() => this.buffer.enqueueWrite(`${JSON.stringify(scrubbed)}
|
|
17349
|
+
`)).then(() => {
|
|
17350
|
+
if (!isCriticalEvent(scrubbed)) return;
|
|
17351
|
+
return this.handle.datasync().catch(() => void 0);
|
|
17352
|
+
}).catch(() => void 0);
|
|
17353
|
+
}
|
|
17096
17354
|
}
|
|
17097
17355
|
scrubbedBatch.push(scrubbed);
|
|
17098
17356
|
}
|
|
17357
|
+
if (scrubbedBatch.length > 0) {
|
|
17358
|
+
this.metadataDirty = true;
|
|
17359
|
+
this.scheduleMetadataCheckpoint();
|
|
17360
|
+
}
|
|
17099
17361
|
try {
|
|
17100
17362
|
this._onAppendBatch?.(scrubbedBatch);
|
|
17101
17363
|
} catch {
|
|
17102
17364
|
}
|
|
17103
|
-
|
|
17365
|
+
const hasCritical = scrubbedBatch.some(isCriticalEvent);
|
|
17366
|
+
if (hasCritical || this.buffer.shouldFlushNow()) {
|
|
17104
17367
|
this.buffer.cancelTimer();
|
|
17105
|
-
await this.buffer.flushBuffer(this.closed).catch(() => {
|
|
17368
|
+
await this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => {
|
|
17106
17369
|
});
|
|
17107
17370
|
} else {
|
|
17108
17371
|
this.buffer.scheduleFlush(this.closed);
|
|
@@ -17110,8 +17373,10 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
17110
17373
|
}
|
|
17111
17374
|
/**
|
|
17112
17375
|
* Flush buffered events to disk immediately. Critical events
|
|
17113
|
-
* (user_input, llm_response
|
|
17114
|
-
*
|
|
17376
|
+
* (user_input, llm_response, checkpoint, in_flight_*) already flush
|
|
17377
|
+
* themselves inside append()/appendBatch(), so calling this matters for
|
|
17378
|
+
* non-critical tails that would otherwise sit in the in-memory buffer
|
|
17379
|
+
* for up to 500ms.
|
|
17115
17380
|
*
|
|
17116
17381
|
* Idempotent — cancels any pending timer, writes whatever has accumulated,
|
|
17117
17382
|
* then asks the OS to synchronize the file data before resolving. Even an
|
|
@@ -17120,7 +17385,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
17120
17385
|
async flush() {
|
|
17121
17386
|
if (this.closed) return;
|
|
17122
17387
|
this.buffer.cancelTimer();
|
|
17123
|
-
await this.buffer.flushBuffer(this.closed);
|
|
17388
|
+
await this.buffer.flushBuffer(this.closed, { datasync: true });
|
|
17124
17389
|
await this.buffer.drainWriteChain();
|
|
17125
17390
|
try {
|
|
17126
17391
|
await this.handle.datasync();
|
|
@@ -17136,23 +17401,35 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
17136
17401
|
* Last-gasp synchronous drain for hard-exit paths (process.exit after
|
|
17137
17402
|
* rapid Ctrl+C). The async write chain cannot be awaited when the process
|
|
17138
17403
|
* is about to die, but whatever still sits in the in-memory buffer CAN be
|
|
17139
|
-
* saved with a blocking append.
|
|
17140
|
-
*
|
|
17404
|
+
* saved with a blocking append. A failed sync append leaves the buffer
|
|
17405
|
+
* intact so a subsequent close()/flush() can retry. An in-flight async
|
|
17406
|
+
* write may still be cut off by a hard exit.
|
|
17141
17407
|
*/
|
|
17142
17408
|
flushSync() {
|
|
17143
17409
|
this.buffer.flushSync();
|
|
17144
17410
|
}
|
|
17145
17411
|
async close() {
|
|
17146
17412
|
if (this.closePromise) return this.closePromise;
|
|
17147
|
-
this.closePromise = this.doClose().catch((err) => {
|
|
17413
|
+
this.closePromise = this.doClose().catch(async (err) => {
|
|
17414
|
+
if (this.metadataTimer) {
|
|
17415
|
+
clearTimeout(this.metadataTimer);
|
|
17416
|
+
this.metadataTimer = null;
|
|
17417
|
+
}
|
|
17418
|
+
await this.metadataCheckpointInFlight?.catch(() => void 0);
|
|
17148
17419
|
this.closed = false;
|
|
17149
17420
|
this.closePromise = null;
|
|
17150
17421
|
if (this.buffer.length > 0) this.buffer.scheduleFlush(this.closed);
|
|
17422
|
+
if (this.metadataDirty) this.scheduleMetadataCheckpoint();
|
|
17151
17423
|
throw err;
|
|
17152
17424
|
});
|
|
17153
17425
|
return this.closePromise;
|
|
17154
17426
|
}
|
|
17155
17427
|
async doClose() {
|
|
17428
|
+
if (this.metadataTimer) {
|
|
17429
|
+
clearTimeout(this.metadataTimer);
|
|
17430
|
+
this.metadataTimer = null;
|
|
17431
|
+
}
|
|
17432
|
+
await this.metadataCheckpointInFlight?.catch(() => void 0);
|
|
17156
17433
|
await this.ensureInit();
|
|
17157
17434
|
if (this.pendingFileSnapshots.length > 0) {
|
|
17158
17435
|
await this.writeFileSnapshot(this.activePromptIndex ?? 0, [...this.pendingFileSnapshots]);
|
|
@@ -17160,8 +17437,13 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
17160
17437
|
this.pendingFileSnapshotBytes = 0;
|
|
17161
17438
|
}
|
|
17162
17439
|
this.closed = true;
|
|
17440
|
+
if (this.metadataTimer) {
|
|
17441
|
+
clearTimeout(this.metadataTimer);
|
|
17442
|
+
this.metadataTimer = null;
|
|
17443
|
+
}
|
|
17444
|
+
await this.metadataCheckpointInFlight?.catch(() => void 0);
|
|
17163
17445
|
this.buffer.cancelTimer();
|
|
17164
|
-
await this.buffer.flushBuffer(this.closed);
|
|
17446
|
+
await this.buffer.flushBuffer(this.closed, { datasync: true });
|
|
17165
17447
|
await this.buffer.drainWriteChain();
|
|
17166
17448
|
try {
|
|
17167
17449
|
await this.handle.datasync();
|
|
@@ -17276,10 +17558,23 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
17276
17558
|
async truncateToCheckpoint(targetPromptIndex, revertedFiles = []) {
|
|
17277
17559
|
if (!this.filePath) return 0;
|
|
17278
17560
|
this.buffer.cancelTimer();
|
|
17279
|
-
await this.buffer.flushBuffer(this.closed);
|
|
17561
|
+
await this.buffer.flushBuffer(this.closed, { datasync: true });
|
|
17280
17562
|
await this.buffer.drainWriteChain();
|
|
17281
|
-
|
|
17282
|
-
|
|
17563
|
+
if (this.metadataTimer) {
|
|
17564
|
+
clearTimeout(this.metadataTimer);
|
|
17565
|
+
this.metadataTimer = null;
|
|
17566
|
+
}
|
|
17567
|
+
await this.metadataCheckpointInFlight?.catch(() => void 0);
|
|
17568
|
+
const plan = await findSessionCheckpointTruncatePlan(this.filePath, targetPromptIndex).catch(
|
|
17569
|
+
(err) => {
|
|
17570
|
+
this.scheduleMetadataCheckpoint();
|
|
17571
|
+
throw err;
|
|
17572
|
+
}
|
|
17573
|
+
);
|
|
17574
|
+
if (!plan) {
|
|
17575
|
+
this.scheduleMetadataCheckpoint();
|
|
17576
|
+
return 0;
|
|
17577
|
+
}
|
|
17283
17578
|
await this.buffer.drainWriteChain();
|
|
17284
17579
|
try {
|
|
17285
17580
|
await this.handle.close();
|
|
@@ -17290,6 +17585,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
17290
17585
|
this.handle = await fsp9.open(this.filePath, "a", 384);
|
|
17291
17586
|
} catch (err) {
|
|
17292
17587
|
this.handle = await fsp9.open(this.filePath, "a", 384).catch(() => this.handle);
|
|
17588
|
+
this.scheduleMetadataCheckpoint();
|
|
17293
17589
|
throw err;
|
|
17294
17590
|
}
|
|
17295
17591
|
await this.summaryTracker.recomputeFromDisk(this.filePath);
|
|
@@ -17315,6 +17611,11 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
17315
17611
|
await this.buffer.drainFlushPromise();
|
|
17316
17612
|
this.buffer.clear();
|
|
17317
17613
|
await this.buffer.drainWriteChain();
|
|
17614
|
+
if (this.metadataTimer) {
|
|
17615
|
+
clearTimeout(this.metadataTimer);
|
|
17616
|
+
this.metadataTimer = null;
|
|
17617
|
+
}
|
|
17618
|
+
await this.metadataCheckpointInFlight?.catch(() => void 0);
|
|
17318
17619
|
const resetAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
17319
17620
|
const record = `${JSON.stringify({
|
|
17320
17621
|
type: "session_start",
|
|
@@ -17325,8 +17626,10 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
17325
17626
|
})}
|
|
17326
17627
|
`;
|
|
17327
17628
|
await this.handle.close();
|
|
17328
|
-
await
|
|
17629
|
+
await atomicWrite(this.filePath, record, { mode: 384 });
|
|
17329
17630
|
this.summaryTracker.reset(resetAt);
|
|
17631
|
+
this.metadataDirty = true;
|
|
17632
|
+
this.scheduleMetadataCheckpoint();
|
|
17330
17633
|
this.activePromptIndex = null;
|
|
17331
17634
|
this.pendingFileSnapshots = [];
|
|
17332
17635
|
this.pendingFileSnapshotBytes = 0;
|
|
@@ -17717,7 +18020,7 @@ var SessionCheckpointCas = class {
|
|
|
17717
18020
|
}
|
|
17718
18021
|
};
|
|
17719
18022
|
function defaultRunGit(args, cwd) {
|
|
17720
|
-
return new Promise((
|
|
18023
|
+
return new Promise((resolve17) => {
|
|
17721
18024
|
const stdoutChunks = [];
|
|
17722
18025
|
const stderrChunks = [];
|
|
17723
18026
|
let stdoutBytes = 0;
|
|
@@ -17760,8 +18063,8 @@ function defaultRunGit(args, cwd) {
|
|
|
17760
18063
|
stdoutTruncated,
|
|
17761
18064
|
stderrTruncated
|
|
17762
18065
|
});
|
|
17763
|
-
child.on("error", (err) =>
|
|
17764
|
-
child.on("close", (code) =>
|
|
18066
|
+
child.on("error", (err) => resolve17(result(1, err.message)));
|
|
18067
|
+
child.on("close", (code) => resolve17(result(code ?? 1)));
|
|
17765
18068
|
});
|
|
17766
18069
|
}
|
|
17767
18070
|
|
|
@@ -18575,7 +18878,7 @@ async function executeRenameSession(params) {
|
|
|
18575
18878
|
|
|
18576
18879
|
// src/storage/session-store/resume-session.ts
|
|
18577
18880
|
import * as fsp16 from "node:fs/promises";
|
|
18578
|
-
import * as
|
|
18881
|
+
import * as path31 from "node:path";
|
|
18579
18882
|
|
|
18580
18883
|
// src/storage/session-resume-validation.ts
|
|
18581
18884
|
import { createHash as createHash5 } from "node:crypto";
|
|
@@ -18671,8 +18974,22 @@ async function validateResumeFileObservations(events, projectRoot) {
|
|
|
18671
18974
|
}
|
|
18672
18975
|
var RESUME_NOTICE_HEADERS = [
|
|
18673
18976
|
"[SESSION RESUME FILE VALIDATION]",
|
|
18674
|
-
"[SESSION RESUME INTERRUPTED WORK]"
|
|
18977
|
+
"[SESSION RESUME INTERRUPTED WORK]",
|
|
18978
|
+
"[SESSION RESUME CRASH RECOVERY]"
|
|
18675
18979
|
];
|
|
18980
|
+
function formatCrashRecoveryNotice(interruptedTools, lastContext) {
|
|
18981
|
+
if (interruptedTools.length === 0) return null;
|
|
18982
|
+
const plural = interruptedTools.length === 1 ? "call was" : "calls were";
|
|
18983
|
+
const lines = [
|
|
18984
|
+
"[SESSION RESUME CRASH RECOVERY]",
|
|
18985
|
+
`The previous run stopped mid-iteration${lastContext ? ` while: ${lastContext}` : ""} \u2014 ${interruptedTools.length} tool ${plural} left without a recorded result.`,
|
|
18986
|
+
"Those interrupted tool calls were removed from the restored conversation and were NOT re-executed; their workspace side effects were NOT rolled back."
|
|
18987
|
+
];
|
|
18988
|
+
for (const tool of interruptedTools.slice(0, NOTICE_PATH_LIMIT)) {
|
|
18989
|
+
lines.push(`- ${tool.name}${tool.argsSummary ? ` (${tool.argsSummary})` : ""}`);
|
|
18990
|
+
}
|
|
18991
|
+
return lines.join("\n");
|
|
18992
|
+
}
|
|
18676
18993
|
function isResumeNoticeMessage(message) {
|
|
18677
18994
|
if (message.role !== "system" || typeof message.content !== "string") return false;
|
|
18678
18995
|
return RESUME_NOTICE_HEADERS.some((header) => message.content === header || message.content.startsWith(`${header}
|
|
@@ -18705,9 +19022,345 @@ function formatInterruptedToolNotice(pendingToolUseCount) {
|
|
|
18705
19022
|
].join("\n");
|
|
18706
19023
|
}
|
|
18707
19024
|
|
|
18708
|
-
// src/storage/session-
|
|
19025
|
+
// src/storage/session-recovery.ts
|
|
18709
19026
|
import { createReadStream as createReadStream4 } from "node:fs";
|
|
19027
|
+
import * as fs4 from "node:fs/promises";
|
|
19028
|
+
import * as path30 from "node:path";
|
|
18710
19029
|
import { createInterface as createInterface4 } from "node:readline";
|
|
19030
|
+
function extractInterruptedTools(plan) {
|
|
19031
|
+
const tools = [];
|
|
19032
|
+
const openCalls = /* @__PURE__ */ new Map();
|
|
19033
|
+
let anonymousSeq = 0;
|
|
19034
|
+
for (const ev of plan.pendingEvents) {
|
|
19035
|
+
if ((ev.type === "tool_use" || ev.type === "tool_call_start") && typeof ev.name === "string") {
|
|
19036
|
+
const toolName = ev.name;
|
|
19037
|
+
const rawId = ev.id;
|
|
19038
|
+
const callId = rawId ?? toolName;
|
|
19039
|
+
openCalls.set(callId, {
|
|
19040
|
+
id: rawId,
|
|
19041
|
+
name: toolName,
|
|
19042
|
+
args: ev.input ?? ev.args,
|
|
19043
|
+
ts: ev.ts
|
|
19044
|
+
});
|
|
19045
|
+
} else if (ev.type === "tool_result" || ev.type === "tool_call_end") {
|
|
19046
|
+
const callId = ev.id ?? ev.toolUseId ?? ev.name;
|
|
19047
|
+
if (callId) openCalls.delete(callId);
|
|
19048
|
+
} else if (ev.type === "llm_response" && Array.isArray(ev.content)) {
|
|
19049
|
+
for (const block of ev.content) {
|
|
19050
|
+
if (block && block.type === "tool_use" && typeof block.name === "string") {
|
|
19051
|
+
const rawId = block.id;
|
|
19052
|
+
const callId = rawId ?? `${block.name}#${++anonymousSeq}`;
|
|
19053
|
+
openCalls.set(callId, {
|
|
19054
|
+
id: rawId,
|
|
19055
|
+
name: block.name,
|
|
19056
|
+
args: block.input,
|
|
19057
|
+
ts: ev.ts
|
|
19058
|
+
});
|
|
19059
|
+
}
|
|
19060
|
+
}
|
|
19061
|
+
} else if (ev.type === "message_appended" && ev.message) {
|
|
19062
|
+
const msg = ev.message;
|
|
19063
|
+
if (Array.isArray(msg.content)) {
|
|
19064
|
+
for (const block of msg.content) {
|
|
19065
|
+
if (block && block.type === "tool_use" && typeof block.name === "string") {
|
|
19066
|
+
const rawId = block.id;
|
|
19067
|
+
const callId = rawId ?? `${block.name}#${++anonymousSeq}`;
|
|
19068
|
+
openCalls.set(callId, {
|
|
19069
|
+
id: rawId,
|
|
19070
|
+
name: block.name,
|
|
19071
|
+
args: block.input,
|
|
19072
|
+
ts: ev.ts
|
|
19073
|
+
});
|
|
19074
|
+
} else if (block && block.type === "tool_result") {
|
|
19075
|
+
const callId = block.tool_use_id ?? block.id;
|
|
19076
|
+
if (callId) openCalls.delete(callId);
|
|
19077
|
+
}
|
|
19078
|
+
}
|
|
19079
|
+
}
|
|
19080
|
+
}
|
|
19081
|
+
}
|
|
19082
|
+
for (const call of openCalls.values()) {
|
|
19083
|
+
let argsSummary;
|
|
19084
|
+
if (call.args && typeof call.args === "object") {
|
|
19085
|
+
try {
|
|
19086
|
+
const str = JSON.stringify(call.args);
|
|
19087
|
+
argsSummary = str.length > 80 ? `${str.slice(0, 77)}...` : str;
|
|
19088
|
+
} catch {
|
|
19089
|
+
}
|
|
19090
|
+
}
|
|
19091
|
+
tools.push({
|
|
19092
|
+
id: call.id,
|
|
19093
|
+
name: call.name,
|
|
19094
|
+
argsSummary,
|
|
19095
|
+
ts: call.ts
|
|
19096
|
+
});
|
|
19097
|
+
}
|
|
19098
|
+
return tools;
|
|
19099
|
+
}
|
|
19100
|
+
var SessionRecovery = class _SessionRecovery {
|
|
19101
|
+
constructor(dir) {
|
|
19102
|
+
this.dir = dir;
|
|
19103
|
+
}
|
|
19104
|
+
dir;
|
|
19105
|
+
static MAX_PENDING_EVENTS = 1e4;
|
|
19106
|
+
static MAX_PENDING_BYTES = 16 * 1024 * 1024;
|
|
19107
|
+
/**
|
|
19108
|
+
* Build a recovery plan from ALREADY-LOADED events without touching disk.
|
|
19109
|
+
* executeResumeSession uses this because load() has already paid for the
|
|
19110
|
+
* transcript read; recover()'s file scan would duplicate it.
|
|
19111
|
+
*/
|
|
19112
|
+
static buildRecoveryPlan(events, sessionId) {
|
|
19113
|
+
const pendingEvents = [];
|
|
19114
|
+
const pendingSizes = [];
|
|
19115
|
+
let pendingBytes = 0;
|
|
19116
|
+
let lastCheckpoint = null;
|
|
19117
|
+
let latestBoundary = null;
|
|
19118
|
+
for (const event of events) {
|
|
19119
|
+
if (!event || typeof event !== "object" || typeof event.type !== "string") continue;
|
|
19120
|
+
if (event.type === "checkpoint") {
|
|
19121
|
+
lastCheckpoint = event;
|
|
19122
|
+
pendingEvents.length = 0;
|
|
19123
|
+
pendingSizes.length = 0;
|
|
19124
|
+
pendingBytes = 0;
|
|
19125
|
+
continue;
|
|
19126
|
+
}
|
|
19127
|
+
if (isLifecycleBoundary(event)) latestBoundary = event;
|
|
19128
|
+
const bytes = Buffer.byteLength(JSON.stringify(event), "utf8");
|
|
19129
|
+
if (bytes > _SessionRecovery.MAX_PENDING_BYTES) continue;
|
|
19130
|
+
while (pendingEvents.length >= _SessionRecovery.MAX_PENDING_EVENTS || pendingBytes + bytes > _SessionRecovery.MAX_PENDING_BYTES) {
|
|
19131
|
+
pendingBytes -= pendingSizes.shift();
|
|
19132
|
+
pendingEvents.shift();
|
|
19133
|
+
}
|
|
19134
|
+
pendingEvents.push(event);
|
|
19135
|
+
pendingSizes.push(bytes);
|
|
19136
|
+
pendingBytes += bytes;
|
|
19137
|
+
}
|
|
19138
|
+
const inFlightStart = latestBoundary?.type === "in_flight_start" ? latestBoundary : null;
|
|
19139
|
+
return {
|
|
19140
|
+
sessionId,
|
|
19141
|
+
stale: inFlightStart !== null,
|
|
19142
|
+
lastCheckpoint,
|
|
19143
|
+
pendingEvents,
|
|
19144
|
+
inFlightStart,
|
|
19145
|
+
context: inFlightStart?.context ?? null
|
|
19146
|
+
};
|
|
19147
|
+
}
|
|
19148
|
+
/**
|
|
19149
|
+
* Scan a session log and return a `StaleSession` if and only if the newest
|
|
19150
|
+
* lifecycle boundary is an `in_flight_start` without a later
|
|
19151
|
+
* `in_flight_end`/`session_end`. Ordinary provider/tool events after the
|
|
19152
|
+
* marker do not make the session clean. Returns `null` when:
|
|
19153
|
+
* - the log does not exist;
|
|
19154
|
+
* - the log is empty;
|
|
19155
|
+
* - the latest lifecycle boundary is `in_flight_end` or `session_end`;
|
|
19156
|
+
* - there is no lifecycle boundary (legacy/pre-marker log).
|
|
19157
|
+
*
|
|
19158
|
+
* The reverse scanner is chunked and line-aware. It can cross arbitrarily
|
|
19159
|
+
* large JSONL records (for example a large tool result) without imposing a
|
|
19160
|
+
* fixed tail-size correctness limit. Clean logs normally return after the
|
|
19161
|
+
* first chunk; stale logs continue counting lines so `eventCount` remains
|
|
19162
|
+
* the documented total rather than a tail-only approximation.
|
|
19163
|
+
*/
|
|
19164
|
+
async resolveId(query) {
|
|
19165
|
+
const resolution = resolveSessionId(query, await collectSessionIds(this.dir));
|
|
19166
|
+
if (resolution.status === "resolved") return resolution.id;
|
|
19167
|
+
if (resolution.status === "missing") return resolution.query;
|
|
19168
|
+
throw sessionIdResolutionError(resolution);
|
|
19169
|
+
}
|
|
19170
|
+
async detectStale(sessionId) {
|
|
19171
|
+
const canonicalId = await this.resolveId(sessionId);
|
|
19172
|
+
return this.detectStaleExact(canonicalId);
|
|
19173
|
+
}
|
|
19174
|
+
async detectStaleExact(sessionId) {
|
|
19175
|
+
const fp = this.filePath(sessionId);
|
|
19176
|
+
let stat13;
|
|
19177
|
+
try {
|
|
19178
|
+
stat13 = await fs4.stat(fp);
|
|
19179
|
+
} catch {
|
|
19180
|
+
return null;
|
|
19181
|
+
}
|
|
19182
|
+
if (stat13.size === 0) return null;
|
|
19183
|
+
try {
|
|
19184
|
+
const scan = await scanLatestLifecycleBoundary(fp, stat13.size);
|
|
19185
|
+
if (scan?.boundary.type !== "in_flight_start") return null;
|
|
19186
|
+
return {
|
|
19187
|
+
sessionId,
|
|
19188
|
+
path: fp,
|
|
19189
|
+
lastEventTs: scan.boundary.ts,
|
|
19190
|
+
context: scan.boundary.context,
|
|
19191
|
+
eventCount: scan.eventCount
|
|
19192
|
+
};
|
|
19193
|
+
} catch {
|
|
19194
|
+
return null;
|
|
19195
|
+
}
|
|
19196
|
+
}
|
|
19197
|
+
/**
|
|
19198
|
+
* Generate a recovery plan for a session. The plan describes
|
|
19199
|
+
* the persisted tail after the last checkpoint, plus the dangling in-flight
|
|
19200
|
+
* marker if present. SessionStore.resume() independently reconstructs state
|
|
19201
|
+
* from the journal and does not re-run these events as commands.
|
|
19202
|
+
*
|
|
19203
|
+
* Returns a non-null plan for ANY session that has at least
|
|
19204
|
+
* one event after a checkpoint (or, for legacy sessions, at
|
|
19205
|
+
* least one event). Pure read; no mutation.
|
|
19206
|
+
*/
|
|
19207
|
+
async recover(sessionId) {
|
|
19208
|
+
const canonicalId = await this.resolveId(sessionId);
|
|
19209
|
+
const fp = this.filePath(canonicalId);
|
|
19210
|
+
const pendingEvents = [];
|
|
19211
|
+
const pendingSizes = [];
|
|
19212
|
+
let pendingBytes = 0;
|
|
19213
|
+
let lastCheckpoint = null;
|
|
19214
|
+
let latestBoundary = null;
|
|
19215
|
+
let sawEvent = false;
|
|
19216
|
+
const stream = createReadStream4(fp, { encoding: "utf8" });
|
|
19217
|
+
const lines = createInterface4({ input: stream, crlfDelay: Infinity });
|
|
19218
|
+
try {
|
|
19219
|
+
for await (const line of lines) {
|
|
19220
|
+
if (!line.trim()) continue;
|
|
19221
|
+
let event;
|
|
19222
|
+
try {
|
|
19223
|
+
event = JSON.parse(line);
|
|
19224
|
+
} catch {
|
|
19225
|
+
continue;
|
|
19226
|
+
}
|
|
19227
|
+
if (!event || typeof event !== "object" || typeof event.type !== "string") continue;
|
|
19228
|
+
sawEvent = true;
|
|
19229
|
+
if (event.type === "checkpoint") {
|
|
19230
|
+
lastCheckpoint = event;
|
|
19231
|
+
pendingEvents.length = 0;
|
|
19232
|
+
pendingSizes.length = 0;
|
|
19233
|
+
pendingBytes = 0;
|
|
19234
|
+
} else {
|
|
19235
|
+
const bytes = Buffer.byteLength(line, "utf8");
|
|
19236
|
+
if (bytes <= _SessionRecovery.MAX_PENDING_BYTES) {
|
|
19237
|
+
while (pendingEvents.length >= _SessionRecovery.MAX_PENDING_EVENTS || pendingBytes + bytes > _SessionRecovery.MAX_PENDING_BYTES) {
|
|
19238
|
+
pendingEvents.shift();
|
|
19239
|
+
pendingBytes = Math.max(0, pendingBytes - pendingSizes.shift());
|
|
19240
|
+
}
|
|
19241
|
+
pendingEvents.push(event);
|
|
19242
|
+
pendingSizes.push(bytes);
|
|
19243
|
+
pendingBytes += bytes;
|
|
19244
|
+
}
|
|
19245
|
+
}
|
|
19246
|
+
if (isLifecycleBoundary(event)) latestBoundary = event;
|
|
19247
|
+
}
|
|
19248
|
+
} catch {
|
|
19249
|
+
return null;
|
|
19250
|
+
} finally {
|
|
19251
|
+
lines.close();
|
|
19252
|
+
stream.close();
|
|
19253
|
+
}
|
|
19254
|
+
if (!sawEvent) return null;
|
|
19255
|
+
const inFlightStart = latestBoundary?.type === "in_flight_start" ? latestBoundary : null;
|
|
19256
|
+
const context = inFlightStart && inFlightStart.type === "in_flight_start" ? inFlightStart.context : null;
|
|
19257
|
+
return {
|
|
19258
|
+
sessionId: canonicalId,
|
|
19259
|
+
stale: inFlightStart !== null,
|
|
19260
|
+
lastCheckpoint,
|
|
19261
|
+
pendingEvents,
|
|
19262
|
+
inFlightStart,
|
|
19263
|
+
context
|
|
19264
|
+
};
|
|
19265
|
+
}
|
|
19266
|
+
/**
|
|
19267
|
+
* List every stale session in a directory. Returns an array
|
|
19268
|
+
* (possibly empty) sorted by `lastEventTs` descending — most
|
|
19269
|
+
* recent crash first.
|
|
19270
|
+
*/
|
|
19271
|
+
async listResumable() {
|
|
19272
|
+
const out = [];
|
|
19273
|
+
const collect = async (dir, prefix, depth) => {
|
|
19274
|
+
let entries;
|
|
19275
|
+
try {
|
|
19276
|
+
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
19277
|
+
} catch {
|
|
19278
|
+
return;
|
|
19279
|
+
}
|
|
19280
|
+
for (const entry of entries) {
|
|
19281
|
+
if (entry.name.startsWith(".")) continue;
|
|
19282
|
+
if (entry.name === "shared" || entry.name === "subagents" || entry.name === "attachments")
|
|
19283
|
+
continue;
|
|
19284
|
+
if (entry.isDirectory()) {
|
|
19285
|
+
if (depth === 0) {
|
|
19286
|
+
await collect(path30.join(dir, entry.name), entry.name, depth + 1);
|
|
19287
|
+
}
|
|
19288
|
+
continue;
|
|
19289
|
+
}
|
|
19290
|
+
if (!entry.isFile() || !isSessionTranscriptFileName(entry.name)) continue;
|
|
19291
|
+
const base = entry.name.slice(0, -".jsonl".length);
|
|
19292
|
+
const sessionId = prefix ? `${prefix}/${base}` : base;
|
|
19293
|
+
const stale = await this.detectStaleExact(sessionId);
|
|
19294
|
+
if (stale) out.push(stale);
|
|
19295
|
+
}
|
|
19296
|
+
};
|
|
19297
|
+
await collect(this.dir, "", 0);
|
|
19298
|
+
return out.sort((a, b) => b.lastEventTs.localeCompare(a.lastEventTs));
|
|
19299
|
+
}
|
|
19300
|
+
// ── Internals ──────────────────────────────────────────────────────────
|
|
19301
|
+
filePath(sessionId) {
|
|
19302
|
+
return sessionScopedPath(this.dir, sessionId, ".jsonl");
|
|
19303
|
+
}
|
|
19304
|
+
};
|
|
19305
|
+
function isLifecycleBoundary(event) {
|
|
19306
|
+
return event.type === "in_flight_start" || event.type === "in_flight_end" || event.type === "session_end";
|
|
19307
|
+
}
|
|
19308
|
+
function parseLifecycleBoundary(line) {
|
|
19309
|
+
try {
|
|
19310
|
+
const parsed = JSON.parse(line.toString("utf8"));
|
|
19311
|
+
return isLifecycleBoundary(parsed) ? parsed : null;
|
|
19312
|
+
} catch {
|
|
19313
|
+
return null;
|
|
19314
|
+
}
|
|
19315
|
+
}
|
|
19316
|
+
function hasNonWhitespace(line) {
|
|
19317
|
+
for (const byte of line) {
|
|
19318
|
+
if (byte !== 9 && byte !== 10 && byte !== 13 && byte !== 32) return true;
|
|
19319
|
+
}
|
|
19320
|
+
return false;
|
|
19321
|
+
}
|
|
19322
|
+
async function scanLatestLifecycleBoundary(filePath, size) {
|
|
19323
|
+
const CHUNK_SIZE2 = 64 * 1024;
|
|
19324
|
+
const handle = await fs4.open(filePath, "r");
|
|
19325
|
+
let position = size;
|
|
19326
|
+
let laterLineFragment = Buffer.alloc(0);
|
|
19327
|
+
let latestBoundary = null;
|
|
19328
|
+
let eventCount = 0;
|
|
19329
|
+
const observeLine = (line) => {
|
|
19330
|
+
if (!hasNonWhitespace(line)) return null;
|
|
19331
|
+
eventCount++;
|
|
19332
|
+
return parseLifecycleBoundary(line);
|
|
19333
|
+
};
|
|
19334
|
+
try {
|
|
19335
|
+
while (position > 0) {
|
|
19336
|
+
const length = Math.min(CHUNK_SIZE2, position);
|
|
19337
|
+
position -= length;
|
|
19338
|
+
const chunk = Buffer.allocUnsafe(length);
|
|
19339
|
+
const { bytesRead } = await handle.read(chunk, 0, length, position);
|
|
19340
|
+
const data = Buffer.concat([chunk.subarray(0, bytesRead), laterLineFragment]);
|
|
19341
|
+
let lineEnd = data.length;
|
|
19342
|
+
for (let i = data.length - 1; i >= 0; i--) {
|
|
19343
|
+
if (data[i] !== 10) continue;
|
|
19344
|
+
const boundary2 = observeLine(data.subarray(i + 1, lineEnd));
|
|
19345
|
+
if (!latestBoundary && boundary2) latestBoundary = boundary2;
|
|
19346
|
+
lineEnd = i;
|
|
19347
|
+
}
|
|
19348
|
+
laterLineFragment = data.subarray(0, lineEnd);
|
|
19349
|
+
if (latestBoundary && latestBoundary.type !== "in_flight_start") {
|
|
19350
|
+
return { boundary: latestBoundary, eventCount };
|
|
19351
|
+
}
|
|
19352
|
+
}
|
|
19353
|
+
const boundary = observeLine(laterLineFragment);
|
|
19354
|
+
if (!latestBoundary && boundary) latestBoundary = boundary;
|
|
19355
|
+
return latestBoundary ? { boundary: latestBoundary, eventCount } : null;
|
|
19356
|
+
} finally {
|
|
19357
|
+
await handle.close();
|
|
19358
|
+
}
|
|
19359
|
+
}
|
|
19360
|
+
|
|
19361
|
+
// src/storage/session-store/summary-builder.ts
|
|
19362
|
+
import { createReadStream as createReadStream5 } from "node:fs";
|
|
19363
|
+
import { createInterface as createInterface5 } from "node:readline";
|
|
18711
19364
|
async function summarizeSessionFile(opts) {
|
|
18712
19365
|
return summarizeSessionEventSequence({
|
|
18713
19366
|
id: opts.id,
|
|
@@ -18841,8 +19494,8 @@ async function summarizeSessionEventSequence(opts) {
|
|
|
18841
19494
|
}
|
|
18842
19495
|
}
|
|
18843
19496
|
async function* iterateSessionEvents(file, secretScrubber) {
|
|
18844
|
-
const stream =
|
|
18845
|
-
const lines =
|
|
19497
|
+
const stream = createReadStream5(file, { encoding: "utf8" });
|
|
19498
|
+
const lines = createInterface5({ input: stream, crlfDelay: Infinity });
|
|
18846
19499
|
try {
|
|
18847
19500
|
for await (const line of lines) {
|
|
18848
19501
|
if (!line.trim()) continue;
|
|
@@ -18875,7 +19528,8 @@ async function executeResumeSession(params) {
|
|
|
18875
19528
|
readSummaryManifest,
|
|
18876
19529
|
searchEvents,
|
|
18877
19530
|
persistCatalogSummary,
|
|
18878
|
-
logWarn
|
|
19531
|
+
logWarn,
|
|
19532
|
+
sessionsDir
|
|
18879
19533
|
} = params;
|
|
18880
19534
|
const t0 = Date.now();
|
|
18881
19535
|
const data = await load(canonicalId);
|
|
@@ -18896,6 +19550,24 @@ async function executeResumeSession(params) {
|
|
|
18896
19550
|
...derivedSummary,
|
|
18897
19551
|
...persistedSummary?.name !== void 0 ? { name: persistedSummary.name } : {}
|
|
18898
19552
|
};
|
|
19553
|
+
let recoveryPlan = SessionRecovery.buildRecoveryPlan(data.events, canonicalId);
|
|
19554
|
+
if (eventsDropped > 0 && sessionsDir) {
|
|
19555
|
+
const fromDisk = await new SessionRecovery(sessionsDir).recover(canonicalId);
|
|
19556
|
+
if (fromDisk) recoveryPlan = fromDisk;
|
|
19557
|
+
}
|
|
19558
|
+
const interruptedTools = recoveryPlan.stale ? extractInterruptedTools(recoveryPlan) : [];
|
|
19559
|
+
const synthesizedResults = interruptedTools.flatMap(
|
|
19560
|
+
(tool) => typeof tool.id === "string" ? [
|
|
19561
|
+
{
|
|
19562
|
+
type: "tool_result",
|
|
19563
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19564
|
+
id: tool.id,
|
|
19565
|
+
content: "[interrupted] No result was recorded \u2014 the previous process stopped before this call completed. Re-run it if still needed.",
|
|
19566
|
+
isError: true
|
|
19567
|
+
}
|
|
19568
|
+
] : []
|
|
19569
|
+
);
|
|
19570
|
+
if (synthesizedResults.length > 0) data.events.push(...synthesizedResults);
|
|
18899
19571
|
const noticeMessages = [];
|
|
18900
19572
|
let resumeValidation;
|
|
18901
19573
|
if (projectRoot) {
|
|
@@ -18924,7 +19596,7 @@ async function executeResumeSession(params) {
|
|
|
18924
19596
|
);
|
|
18925
19597
|
}
|
|
18926
19598
|
}
|
|
18927
|
-
const interruptedNotice = formatInterruptedToolNotice(data.pendingToolUseCount ?? 0);
|
|
19599
|
+
const interruptedNotice = recoveryPlan.stale ? null : formatInterruptedToolNotice(data.pendingToolUseCount ?? 0);
|
|
18928
19600
|
if (interruptedNotice) {
|
|
18929
19601
|
noticeMessages.push({
|
|
18930
19602
|
role: "system",
|
|
@@ -18932,6 +19604,14 @@ async function executeResumeSession(params) {
|
|
|
18932
19604
|
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
18933
19605
|
});
|
|
18934
19606
|
}
|
|
19607
|
+
const crashNotice = formatCrashRecoveryNotice(interruptedTools, recoveryPlan.context);
|
|
19608
|
+
if (crashNotice) {
|
|
19609
|
+
noticeMessages.push({
|
|
19610
|
+
role: "system",
|
|
19611
|
+
content: crashNotice,
|
|
19612
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
19613
|
+
});
|
|
19614
|
+
}
|
|
18935
19615
|
const carriedMessages = data.messages.filter((message) => !isResumeNoticeMessage(message));
|
|
18936
19616
|
const resumedData = {
|
|
18937
19617
|
...data,
|
|
@@ -18962,7 +19642,7 @@ async function executeResumeSession(params) {
|
|
|
18962
19642
|
{
|
|
18963
19643
|
resumed: true,
|
|
18964
19644
|
initialSummary,
|
|
18965
|
-
dir:
|
|
19645
|
+
dir: path31.dirname(file),
|
|
18966
19646
|
filePath: file,
|
|
18967
19647
|
secretScrubber,
|
|
18968
19648
|
checkpointCas,
|
|
@@ -18973,9 +19653,21 @@ async function executeResumeSession(params) {
|
|
|
18973
19653
|
if (!current) return null;
|
|
18974
19654
|
return current.name === void 0 ? {} : { name: sessionContentText(secretScrubber.scrub(current.name)) };
|
|
18975
19655
|
},
|
|
18976
|
-
onClose: (s) => persistCatalogSummary(s)
|
|
19656
|
+
onClose: (s) => persistCatalogSummary(s),
|
|
19657
|
+
// Resumed sessions checkpoint their index/catalog metadata mid-flight
|
|
19658
|
+
// too, so a kill during a resumed session still leaves fresh listing
|
|
19659
|
+
// state behind.
|
|
19660
|
+
onMetadataCheckpoint: (s) => persistCatalogSummary(s)
|
|
18977
19661
|
}
|
|
18978
19662
|
);
|
|
19663
|
+
if (synthesizedResults.length > 0) {
|
|
19664
|
+
await writer.appendBatch(synthesizedResults);
|
|
19665
|
+
await writer.flush();
|
|
19666
|
+
}
|
|
19667
|
+
if (recoveryPlan.stale) {
|
|
19668
|
+
await writer.clearInFlightMarker("recovered");
|
|
19669
|
+
await writer.flush();
|
|
19670
|
+
}
|
|
18979
19671
|
emitSessionStoreWrite(events, canonicalId, file, "resume", "success", Date.now() - t0);
|
|
18980
19672
|
return { writer, data: resumedData };
|
|
18981
19673
|
} catch (err) {
|
|
@@ -19194,6 +19886,11 @@ function applySessionIndexLines(raw, byId, deleted) {
|
|
|
19194
19886
|
byId.delete(entry.id);
|
|
19195
19887
|
continue;
|
|
19196
19888
|
}
|
|
19889
|
+
if (entry.action === "create" && entry.id) {
|
|
19890
|
+
deleted.delete(entry.id);
|
|
19891
|
+
byId.delete(entry.id);
|
|
19892
|
+
continue;
|
|
19893
|
+
}
|
|
19197
19894
|
if (entry.id && !deleted.has(entry.id)) {
|
|
19198
19895
|
byId.set(entry.id, entry);
|
|
19199
19896
|
}
|
|
@@ -19238,6 +19935,7 @@ async function readFileRange(file, start, end) {
|
|
|
19238
19935
|
|
|
19239
19936
|
// src/storage/session-store/session-store-index.ts
|
|
19240
19937
|
var COMPACT_EVERY = 30;
|
|
19938
|
+
var NO_DELETED_IDS = /* @__PURE__ */ new Set();
|
|
19241
19939
|
async function appendToIndexStrict(dir, indexFile, summary, invalidateShard, onAppended, compactInner) {
|
|
19242
19940
|
await ensureDir(dir);
|
|
19243
19941
|
let shouldCompact = false;
|
|
@@ -19270,10 +19968,14 @@ async function readIndexFile(indexFile, currentCache) {
|
|
|
19270
19968
|
const s = await fsp20.stat(indexFile);
|
|
19271
19969
|
stat13 = { mtimeMs: s.mtimeMs, size: s.size, ino: s.ino, birthtimeMs: s.birthtimeMs };
|
|
19272
19970
|
} catch {
|
|
19273
|
-
return { summaries: [], cache: null };
|
|
19971
|
+
return { summaries: [], deletedIds: NO_DELETED_IDS, cache: null };
|
|
19274
19972
|
}
|
|
19275
19973
|
if (currentCache !== null && currentCache.mtimeMs === stat13.mtimeMs && currentCache.size === stat13.size && currentCache.ino === stat13.ino && currentCache.birthtimeMs === stat13.birthtimeMs) {
|
|
19276
|
-
return {
|
|
19974
|
+
return {
|
|
19975
|
+
summaries: currentCache.summaries,
|
|
19976
|
+
deletedIds: currentCache.deleted,
|
|
19977
|
+
cache: currentCache
|
|
19978
|
+
};
|
|
19277
19979
|
}
|
|
19278
19980
|
const cached = currentCache;
|
|
19279
19981
|
const sameFile = cached !== null && cached.ino === stat13.ino && cached.birthtimeMs === stat13.birthtimeMs;
|
|
@@ -19289,14 +19991,14 @@ async function readIndexFile(indexFile, currentCache) {
|
|
|
19289
19991
|
byId: cached.byId,
|
|
19290
19992
|
deleted: cached.deleted
|
|
19291
19993
|
};
|
|
19292
|
-
return { summaries: summaries2, cache: nextCache2 };
|
|
19994
|
+
return { summaries: summaries2, deletedIds: cached.deleted, cache: nextCache2 };
|
|
19293
19995
|
}
|
|
19294
19996
|
}
|
|
19295
19997
|
let raw;
|
|
19296
19998
|
try {
|
|
19297
19999
|
raw = await fsp20.readFile(indexFile, "utf8");
|
|
19298
20000
|
} catch {
|
|
19299
|
-
return { summaries: [], cache: null };
|
|
20001
|
+
return { summaries: [], deletedIds: NO_DELETED_IDS, cache: null };
|
|
19300
20002
|
}
|
|
19301
20003
|
const deleted = /* @__PURE__ */ new Set();
|
|
19302
20004
|
const byId = /* @__PURE__ */ new Map();
|
|
@@ -19304,7 +20006,18 @@ async function readIndexFile(indexFile, currentCache) {
|
|
|
19304
20006
|
const summaries = Array.from(byId.values());
|
|
19305
20007
|
summaries.sort(compareSessionSummaries);
|
|
19306
20008
|
const nextCache = { ...stat13, summaries, byId, deleted };
|
|
19307
|
-
return { summaries, cache: nextCache };
|
|
20009
|
+
return { summaries, deletedIds: deleted, cache: nextCache };
|
|
20010
|
+
}
|
|
20011
|
+
async function compactIndexInner(indexFile, entries, deletedIds) {
|
|
20012
|
+
const parts = entries.map((s) => JSON.stringify(s));
|
|
20013
|
+
if (deletedIds) {
|
|
20014
|
+
for (const id of deletedIds) {
|
|
20015
|
+
parts.push(JSON.stringify({ action: "delete", id }));
|
|
20016
|
+
}
|
|
20017
|
+
}
|
|
20018
|
+
if (parts.length === 0) return;
|
|
20019
|
+
const lines = parts.join("\n") + "\n";
|
|
20020
|
+
await atomicWrite(indexFile, lines, { mode: 384 });
|
|
19308
20021
|
}
|
|
19309
20022
|
|
|
19310
20023
|
// src/storage/session-store/shard-manifest.ts
|
|
@@ -19342,12 +20055,12 @@ async function readOrBuildShardManifestEntry(opts) {
|
|
|
19342
20055
|
}
|
|
19343
20056
|
|
|
19344
20057
|
// src/storage/session-store/strict-empty-check.ts
|
|
19345
|
-
import { createReadStream as
|
|
19346
|
-
import { createInterface as
|
|
20058
|
+
import { createReadStream as createReadStream6 } from "node:fs";
|
|
20059
|
+
import { createInterface as createInterface6 } from "node:readline";
|
|
19347
20060
|
var EMPTY_SESSION_EVENT_TYPES = /* @__PURE__ */ new Set(["session_start", "session_resumed", "session_end"]);
|
|
19348
20061
|
async function isStrictlyEmptySessionFile(file) {
|
|
19349
|
-
const input =
|
|
19350
|
-
const lines =
|
|
20062
|
+
const input = createReadStream6(file, { encoding: "utf8" });
|
|
20063
|
+
const lines = createInterface6({ input, crlfDelay: Infinity });
|
|
19351
20064
|
let sawSessionStart = false;
|
|
19352
20065
|
try {
|
|
19353
20066
|
for await (const line of lines) {
|
|
@@ -19423,6 +20136,7 @@ function damagedSummary(id, startedAt) {
|
|
|
19423
20136
|
}
|
|
19424
20137
|
|
|
19425
20138
|
// src/storage/session-store.ts
|
|
20139
|
+
var SESSION_FILTER_POOL_LIMIT = 1e4;
|
|
19426
20140
|
var DefaultSessionStore = class _DefaultSessionStore {
|
|
19427
20141
|
dir;
|
|
19428
20142
|
events;
|
|
@@ -19438,14 +20152,36 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
19438
20152
|
_loadCache = /* @__PURE__ */ new Map();
|
|
19439
20153
|
loadCache = new SessionLoadCache(this._loadCache);
|
|
19440
20154
|
_indexCache = null;
|
|
20155
|
+
/**
|
|
20156
|
+
* Tombstoned ids — hidden even if their JSONL remains on disk.
|
|
20157
|
+
* Convention: readIndex() REASSIGNS this set from the parsed index file
|
|
20158
|
+
* MERGED with _manualTombstones; writeTombstone() adds in-place immediately
|
|
20159
|
+
* so an incremental cache rebuild can never resurrect a just-deleted
|
|
20160
|
+
* session.
|
|
20161
|
+
*/
|
|
20162
|
+
_indexDeletedIds = /* @__PURE__ */ new Set();
|
|
20163
|
+
/**
|
|
20164
|
+
* Tombstones added by THIS store between reads. Merged into every fresh
|
|
20165
|
+
* snapshot so a read racing writeTombstone cannot drop an in-flight
|
|
20166
|
+
* deletion; entries are pruned once the parsed index file itself carries
|
|
20167
|
+
* them.
|
|
20168
|
+
*/
|
|
20169
|
+
_manualTombstones = /* @__PURE__ */ new Set();
|
|
20170
|
+
/**
|
|
20171
|
+
* File-truth tombstones from the last readIndex() parse (EXCLUDES
|
|
20172
|
+
* _manualTombstones additions). compactIndexInner persists THIS snapshot so
|
|
20173
|
+
* concurrent writeTombstones that landed after the parse are not written
|
|
20174
|
+
* prematurely — they persist through their own append path instead.
|
|
20175
|
+
*/
|
|
20176
|
+
_indexFileDeletedIds = /* @__PURE__ */ new Set();
|
|
19441
20177
|
shardManifestCache = /* @__PURE__ */ new Map();
|
|
19442
20178
|
static LIST_SCAN_CONCURRENCY = 32;
|
|
19443
20179
|
indexAppendCount = 0;
|
|
19444
20180
|
constructor(opts) {
|
|
19445
20181
|
this.dir = opts.dir;
|
|
19446
|
-
this.projectRoot = opts.projectRoot ?
|
|
20182
|
+
this.projectRoot = opts.projectRoot ? path32.resolve(opts.projectRoot) : void 0;
|
|
19447
20183
|
this.checkpointCas = this.projectRoot ? new SessionCheckpointCas({
|
|
19448
|
-
rootDir:
|
|
20184
|
+
rootDir: path32.join(this.dir, "_cas"),
|
|
19449
20185
|
projectRoot: this.projectRoot
|
|
19450
20186
|
}) : void 0;
|
|
19451
20187
|
this.events = opts.events;
|
|
@@ -19456,7 +20192,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
19456
20192
|
this.onAppendBatch = opts.onAppendBatch;
|
|
19457
20193
|
const builtRuntime = import.meta.url.includes("/dist/");
|
|
19458
20194
|
this.catalogClient = this.projectRoot && (builtRuntime || process.env["WRONGSTACK_SESSION_CATALOG_FORCE"] === "1") && resolveSessionCatalogProjectServerUrl() ? new SessionCatalogProjectClient({
|
|
19459
|
-
projectDir:
|
|
20195
|
+
projectDir: path32.dirname(this.dir),
|
|
19460
20196
|
projectRoot: this.projectRoot
|
|
19461
20197
|
}) : void 0;
|
|
19462
20198
|
}
|
|
@@ -19478,7 +20214,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
19478
20214
|
this.clearLoadCache();
|
|
19479
20215
|
}
|
|
19480
20216
|
get indexFile() {
|
|
19481
|
-
return
|
|
20217
|
+
return path32.join(this.dir, "_index.jsonl");
|
|
19482
20218
|
}
|
|
19483
20219
|
sessionPath(id, ext) {
|
|
19484
20220
|
return sessionPath(this.dir, id, ext);
|
|
@@ -19504,19 +20240,94 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
19504
20240
|
async ensureShardDir(id) {
|
|
19505
20241
|
return ensureShardDir(this.dir, id);
|
|
19506
20242
|
}
|
|
20243
|
+
/**
|
|
20244
|
+
* Create a fresh session writer.
|
|
20245
|
+
*
|
|
20246
|
+
* @threadSafety Failure-prone steps (manifest invalidation, sidecar
|
|
20247
|
+
* removal, catalog upsert, the durable `{action:'create'}` index row)
|
|
20248
|
+
* run BEFORE the truncating `'w'` open, so no rejection path can destroy
|
|
20249
|
+
* prior bytes. Ordinary index summary rows never undelete a tombstone
|
|
20250
|
+
* (the parser only honors `{action:'create'}`), so a swallowed create-row
|
|
20251
|
+
* would leave a live writer whose id stays hidden forever — that append
|
|
20252
|
+
* is therefore required, not best-effort.
|
|
20253
|
+
*/
|
|
19507
20254
|
async create(meta) {
|
|
19508
20255
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
19509
20256
|
const id = meta.id && meta.id.length > 0 ? meta.id : generateSessionId(startedAt);
|
|
19510
20257
|
const shardDir = await this.ensureShardDir(id);
|
|
19511
20258
|
const file = this.sessionPath(id, ".jsonl");
|
|
20259
|
+
const inUseBy = this.isSessionInUse ? await this.isSessionInUse(id) : null;
|
|
20260
|
+
if (inUseBy) {
|
|
20261
|
+
throw new Error(`Refusing to create session ${id}: in use (${inUseBy}).`);
|
|
20262
|
+
}
|
|
19512
20263
|
const t0 = Date.now();
|
|
20264
|
+
try {
|
|
20265
|
+
await this.invalidateShardManifestBySessionId(id);
|
|
20266
|
+
} catch (cause) {
|
|
20267
|
+
throw new Error(
|
|
20268
|
+
`Failed to invalidate stale shard manifest for ${id}: ${toErrorMessage(cause)}`,
|
|
20269
|
+
{ cause }
|
|
20270
|
+
);
|
|
20271
|
+
}
|
|
20272
|
+
const sidecar = path32.join(shardDir, `${path32.basename(id)}.summary.json`);
|
|
20273
|
+
try {
|
|
20274
|
+
await fsp23.rm(sidecar, { force: true });
|
|
20275
|
+
} catch (cause) {
|
|
20276
|
+
emitSessionStoreError(this.events, id, sidecar, "create", toErrorMessage(cause), true);
|
|
20277
|
+
try {
|
|
20278
|
+
await fsp23.access(sidecar);
|
|
20279
|
+
throw new Error(
|
|
20280
|
+
`Failed to remove stale session sidecar for ${id}: ${toErrorMessage(cause)}`,
|
|
20281
|
+
{ cause }
|
|
20282
|
+
);
|
|
20283
|
+
} catch (accessErr) {
|
|
20284
|
+
const code = accessErr.code;
|
|
20285
|
+
if (code !== "ENOENT" && code !== "ENAMETOOLONG") throw accessErr;
|
|
20286
|
+
}
|
|
20287
|
+
}
|
|
20288
|
+
if (this.catalogClient) {
|
|
20289
|
+
await this.catalogClient.call("upsert_summary", {
|
|
20290
|
+
summary: {
|
|
20291
|
+
id,
|
|
20292
|
+
title: meta.title ?? "",
|
|
20293
|
+
startedAt,
|
|
20294
|
+
model: meta.model ?? "",
|
|
20295
|
+
provider: meta.provider ?? "",
|
|
20296
|
+
tokenTotal: 0,
|
|
20297
|
+
lastActivityAt: startedAt
|
|
20298
|
+
},
|
|
20299
|
+
transcriptRelativePath: `${id}.jsonl`,
|
|
20300
|
+
summaryRelativePath: `${id}.summary.json`
|
|
20301
|
+
});
|
|
20302
|
+
}
|
|
20303
|
+
try {
|
|
20304
|
+
await withFileLock(this.indexFile, async () => {
|
|
20305
|
+
try {
|
|
20306
|
+
await fsp23.appendFile(this.indexFile, `${JSON.stringify({ action: "create", id })}
|
|
20307
|
+
`, {
|
|
20308
|
+
encoding: "utf8",
|
|
20309
|
+
mode: 384
|
|
20310
|
+
});
|
|
20311
|
+
} finally {
|
|
20312
|
+
this._indexCache = null;
|
|
20313
|
+
}
|
|
20314
|
+
this._manualTombstones.delete(id);
|
|
20315
|
+
this._indexDeletedIds.delete(id);
|
|
20316
|
+
});
|
|
20317
|
+
} catch (cause) {
|
|
20318
|
+
throw new Error(
|
|
20319
|
+
`Failed to record session create in the index for ${id}: ${toErrorMessage(cause)}`,
|
|
20320
|
+
{ cause }
|
|
20321
|
+
);
|
|
20322
|
+
}
|
|
19513
20323
|
let handle;
|
|
19514
20324
|
try {
|
|
19515
|
-
handle = await fsp23.open(file, "
|
|
20325
|
+
handle = await fsp23.open(file, "w", 384);
|
|
19516
20326
|
} catch (err) {
|
|
19517
20327
|
emitSessionStoreError(this.events, id, file, "create", toErrorMessage(err), false);
|
|
19518
20328
|
throw new Error(`Failed to open session file: ${toErrorMessage(err)}`, { cause: err });
|
|
19519
20329
|
}
|
|
20330
|
+
await this.invalidateShardManifestBySessionId(id).catch(() => void 0);
|
|
19520
20331
|
try {
|
|
19521
20332
|
const writer = new FileSessionWriter(id, handle, startedAt, meta, this.events, {
|
|
19522
20333
|
dir: shardDir,
|
|
@@ -19530,23 +20341,11 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
19530
20341
|
if (!current) return null;
|
|
19531
20342
|
return current.name === void 0 ? {} : { name: sessionContentText(this.secretScrubber.scrub(current.name)) };
|
|
19532
20343
|
},
|
|
19533
|
-
onClose: (s) => this.persistCatalogSummary(s)
|
|
20344
|
+
onClose: (s) => this.persistCatalogSummary(s),
|
|
20345
|
+
// Mid-session metadata checkpoints reuse the same sink as close so
|
|
20346
|
+
// killed sessions leave accurate index rows / catalog entries behind.
|
|
20347
|
+
onMetadataCheckpoint: (s) => this.persistCatalogSummary(s)
|
|
19534
20348
|
});
|
|
19535
|
-
if (this.catalogClient) {
|
|
19536
|
-
await this.catalogClient.call("upsert_summary", {
|
|
19537
|
-
summary: {
|
|
19538
|
-
id,
|
|
19539
|
-
title: meta.title ?? "",
|
|
19540
|
-
startedAt,
|
|
19541
|
-
model: meta.model ?? "",
|
|
19542
|
-
provider: meta.provider ?? "",
|
|
19543
|
-
tokenTotal: 0,
|
|
19544
|
-
lastActivityAt: startedAt
|
|
19545
|
-
},
|
|
19546
|
-
transcriptRelativePath: `${id}.jsonl`,
|
|
19547
|
-
summaryRelativePath: `${id}.summary.json`
|
|
19548
|
-
});
|
|
19549
|
-
}
|
|
19550
20349
|
emitSessionStoreWrite(this.events, id, file, "create", "success", Date.now() - t0);
|
|
19551
20350
|
return writer;
|
|
19552
20351
|
} catch (err) {
|
|
@@ -19608,7 +20407,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
19608
20407
|
readSummaryManifest: (summaryId) => this.readSummaryManifest(summaryId),
|
|
19609
20408
|
searchEvents: (searchId, pred) => this.searchEvents(searchId, pred),
|
|
19610
20409
|
persistCatalogSummary: (sum) => this.persistCatalogSummary(sum),
|
|
19611
|
-
logWarn: (msg, ctx) => this.logWarn(msg, ctx)
|
|
20410
|
+
logWarn: (msg, ctx) => this.logWarn(msg, ctx),
|
|
20411
|
+
sessionsDir: this.dir
|
|
19612
20412
|
});
|
|
19613
20413
|
}
|
|
19614
20414
|
async load(id) {
|
|
@@ -19682,11 +20482,14 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
19682
20482
|
return this.scrubSummaries(records);
|
|
19683
20483
|
}
|
|
19684
20484
|
try {
|
|
19685
|
-
const indexed = await
|
|
19686
|
-
|
|
19687
|
-
|
|
19688
|
-
|
|
19689
|
-
|
|
20485
|
+
const [indexed, scanned] = await Promise.all([
|
|
20486
|
+
this.readIndex(),
|
|
20487
|
+
// Wide scan bound: mergeIndexWithScan slices to `limit`, so killed
|
|
20488
|
+
// sessions deep in history stay visible instead of being dropped by
|
|
20489
|
+
// the user-facing page size before the union runs.
|
|
20490
|
+
this.listFromDirectoryScan(SESSION_FILTER_POOL_LIMIT).catch(() => [])
|
|
20491
|
+
]);
|
|
20492
|
+
return this.scrubSummaries(this.mergeIndexWithScan(indexed, scanned, limit));
|
|
19690
20493
|
} catch {
|
|
19691
20494
|
return [];
|
|
19692
20495
|
}
|
|
@@ -19701,15 +20504,14 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
19701
20504
|
return this.scrubSummaries(records);
|
|
19702
20505
|
}
|
|
19703
20506
|
try {
|
|
19704
|
-
const indexed = await
|
|
19705
|
-
|
|
19706
|
-
|
|
19707
|
-
|
|
19708
|
-
|
|
19709
|
-
|
|
19710
|
-
|
|
19711
|
-
);
|
|
19712
|
-
return filtered.slice(0, limit);
|
|
20507
|
+
const [indexed, scanned] = await Promise.all([
|
|
20508
|
+
this.readIndex(),
|
|
20509
|
+
// Same best-effort contract as list(): scan failures enrich nothing
|
|
20510
|
+
// but must not blank the filtered result set.
|
|
20511
|
+
this.listFromDirectoryScan(SESSION_FILTER_POOL_LIMIT).catch(() => [])
|
|
20512
|
+
]);
|
|
20513
|
+
const pool = this.mergeIndexWithScan(indexed, scanned, SESSION_FILTER_POOL_LIMIT);
|
|
20514
|
+
return this.scrubSummaries(pool).filter((s) => matchesSessionFilter(s, criteria)).slice(0, limit);
|
|
19713
20515
|
} catch {
|
|
19714
20516
|
return [];
|
|
19715
20517
|
}
|
|
@@ -19751,6 +20553,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
19751
20553
|
id,
|
|
19752
20554
|
(sid) => this.invalidateShardManifestBySessionId(sid),
|
|
19753
20555
|
() => {
|
|
20556
|
+
this._manualTombstones.add(id);
|
|
20557
|
+
this._indexDeletedIds.add(id);
|
|
19754
20558
|
this._indexCache = null;
|
|
19755
20559
|
this.indexAppendCount++;
|
|
19756
20560
|
}
|
|
@@ -19778,34 +20582,92 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
19778
20582
|
);
|
|
19779
20583
|
}
|
|
19780
20584
|
}
|
|
20585
|
+
/**
|
|
20586
|
+
* Compact the local index in place.
|
|
20587
|
+
*
|
|
20588
|
+
* Contract carried into the shared compactIndexInner helper
|
|
20589
|
+
* (session-store-index.ts): `entries` MUST already exclude tombstoned ids,
|
|
20590
|
+
* and the deleted-set argument is persisted VERBATIM — neither the helper
|
|
20591
|
+
* nor its callers may resurrect filtered rows or invent deletions.
|
|
20592
|
+
* Locking: callers MUST already hold the indexFile lock (both do:
|
|
20593
|
+
* compactIndex() below and the appendToIndexStrict compaction hook);
|
|
20594
|
+
* readIndex() inside reads that same locked file, so no second lock may
|
|
20595
|
+
* be taken here (non-reentrant → deadlock).
|
|
20596
|
+
*
|
|
20597
|
+
* That same lock is what makes the _indexFileDeletedIds snapshot safe to
|
|
20598
|
+
* pass across the await below: writeTombstone() appends under the identical
|
|
20599
|
+
* non-reentrant indexFile lock, so no tombstone can land between our
|
|
20600
|
+
* readIndex() and the snapshot handed to the helper. Compaction would
|
|
20601
|
+
* otherwise be racing a delete it cannot see.
|
|
20602
|
+
*/
|
|
19781
20603
|
async compactIndexInner() {
|
|
19782
20604
|
const entries = await this.readIndex();
|
|
19783
|
-
|
|
19784
|
-
const lines = entries.map((s) => JSON.stringify(s)).join("\n") + "\n";
|
|
19785
|
-
await atomicWrite(this.indexFile, lines, { mode: 384 });
|
|
20605
|
+
await compactIndexInner(this.indexFile, entries, this._indexFileDeletedIds);
|
|
19786
20606
|
this._indexCache = null;
|
|
19787
20607
|
}
|
|
19788
20608
|
async readIndex() {
|
|
19789
|
-
const { summaries, cache } = await readIndexFile(this.indexFile, this._indexCache);
|
|
20609
|
+
const { summaries, deletedIds, cache } = await readIndexFile(this.indexFile, this._indexCache);
|
|
19790
20610
|
this._indexCache = cache;
|
|
20611
|
+
const merged = new Set(deletedIds);
|
|
20612
|
+
for (const manual of this._manualTombstones) {
|
|
20613
|
+
merged.add(manual);
|
|
20614
|
+
if (deletedIds.has(manual)) this._manualTombstones.delete(manual);
|
|
20615
|
+
}
|
|
20616
|
+
this._indexFileDeletedIds = deletedIds;
|
|
20617
|
+
this._indexDeletedIds = merged;
|
|
19791
20618
|
return summaries;
|
|
19792
20619
|
}
|
|
20620
|
+
/**
|
|
20621
|
+
* Merge close-time index rows with directory-scan results, keyed by id.
|
|
20622
|
+
* Scanned entries win — their metadata is re-derived from the transcript,
|
|
20623
|
+
* so it reflects mid-session activity that index rows (written on close)
|
|
20624
|
+
* cannot know about. Indexed-only ids fill gaps; duplicates within the
|
|
20625
|
+
* index resolve last-wins, matching append order.
|
|
20626
|
+
*/
|
|
20627
|
+
mergeIndexWithScan(indexed, scanned, limit) {
|
|
20628
|
+
const byId = /* @__PURE__ */ new Map();
|
|
20629
|
+
for (const row of indexed) byId.set(row.id, row);
|
|
20630
|
+
for (const row of scanned) byId.set(row.id, row);
|
|
20631
|
+
return [...byId.values()].filter((row) => !this._indexDeletedIds.has(row.id)).sort(compareSessionSummaries).slice(0, limit);
|
|
20632
|
+
}
|
|
20633
|
+
/**
|
|
20634
|
+
* Rebuild the index from what is actually on disk.
|
|
20635
|
+
*
|
|
20636
|
+
* @returns the number of healthy, live entries in the rebuilt index. Both
|
|
20637
|
+
* backends report that same quantity: ids whose summary could not be derived
|
|
20638
|
+
* are excluded (the catalog counts them as `damaged`; the local scan drops
|
|
20639
|
+
* them when `summaryFor` rejects), and ids carrying a surviving tombstone are
|
|
20640
|
+
* excluded (the catalog rebuilds only from live files; the local branch skips
|
|
20641
|
+
* them explicitly). It is NOT a count of rows written to the file — tombstone
|
|
20642
|
+
* rows are persisted but never counted.
|
|
20643
|
+
*/
|
|
19793
20644
|
async rebuildIndex() {
|
|
19794
20645
|
if (this.catalogClient) {
|
|
19795
20646
|
const result = await this.catalogClient.call("rebuild_catalog", {}, { timeoutMs: 12e4 });
|
|
19796
20647
|
return result.indexed;
|
|
19797
20648
|
}
|
|
19798
|
-
|
|
19799
|
-
|
|
19800
|
-
ids
|
|
19801
|
-
|
|
19802
|
-
|
|
19803
|
-
|
|
19804
|
-
|
|
20649
|
+
return withFileLock(this.indexFile, async () => {
|
|
20650
|
+
await this.readIndex();
|
|
20651
|
+
const ids = await this.collectSessionIds(this.dir);
|
|
20652
|
+
const summaries = await Promise.all(ids.map((id) => this.summaryFor(id).catch(() => null)));
|
|
20653
|
+
const valid = summaries.filter((s) => s !== null);
|
|
20654
|
+
const parts = [];
|
|
20655
|
+
let tombstoned = 0;
|
|
20656
|
+
for (const s of valid) {
|
|
20657
|
+
if (this._indexDeletedIds.has(s.id)) {
|
|
20658
|
+
tombstoned++;
|
|
20659
|
+
continue;
|
|
20660
|
+
}
|
|
20661
|
+
parts.push(JSON.stringify(s));
|
|
20662
|
+
}
|
|
20663
|
+
for (const id of this._indexDeletedIds) {
|
|
20664
|
+
parts.push(JSON.stringify({ action: "delete", id }));
|
|
20665
|
+
}
|
|
20666
|
+
const lines = parts.join("\n") + "\n";
|
|
19805
20667
|
await atomicWrite(this.indexFile, lines, { mode: 384 });
|
|
19806
20668
|
this._indexCache = null;
|
|
20669
|
+
return valid.length - tombstoned;
|
|
19807
20670
|
});
|
|
19808
|
-
return valid.length;
|
|
19809
20671
|
}
|
|
19810
20672
|
async listFromDirectoryScan(limit) {
|
|
19811
20673
|
const shardKeys = await this.collectShardKeys();
|
|
@@ -19887,7 +20749,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
19887
20749
|
return void 0;
|
|
19888
20750
|
}
|
|
19889
20751
|
async collectSessionFilesInShard(shardKey) {
|
|
19890
|
-
const dir = shardKey ?
|
|
20752
|
+
const dir = shardKey ? path32.join(this.dir, shardKey) : this.dir;
|
|
19891
20753
|
const entries = await this.collectSessionFiles(dir, shardKey);
|
|
19892
20754
|
return shardKey ? entries.filter((entry) => entry.id.startsWith(`${shardKey}/`)) : entries.filter((entry) => !entry.id.includes("/"));
|
|
19893
20755
|
}
|
|
@@ -20063,9 +20925,9 @@ function makeDirectorSessionFactory(opts) {
|
|
|
20063
20925
|
let dir;
|
|
20064
20926
|
if (opts.store) {
|
|
20065
20927
|
store = opts.store;
|
|
20066
|
-
dir = opts.sessionsRoot ?
|
|
20928
|
+
dir = opts.sessionsRoot ? path33.join(opts.sessionsRoot, runId) : "(caller-managed)";
|
|
20067
20929
|
} else if (opts.sessionsRoot) {
|
|
20068
|
-
dir =
|
|
20930
|
+
dir = path33.join(opts.sessionsRoot, runId);
|
|
20069
20931
|
store = new DefaultSessionStore({ dir });
|
|
20070
20932
|
} else {
|
|
20071
20933
|
throw new Error("makeDirectorSessionFactory requires either `store` or `sessionsRoot`");
|
|
@@ -20089,7 +20951,7 @@ function makeDirectorSessionFactory(opts) {
|
|
|
20089
20951
|
}
|
|
20090
20952
|
async function readDirectorSubagentSession(args) {
|
|
20091
20953
|
if (!args.sessionsRoot) return null;
|
|
20092
|
-
const filePath =
|
|
20954
|
+
const filePath = path33.join(args.sessionsRoot, args.directorRunId, `${args.subagentId}.jsonl`);
|
|
20093
20955
|
let raw;
|
|
20094
20956
|
try {
|
|
20095
20957
|
raw = await fsp24.readFile(filePath, "utf8");
|
|
@@ -21789,12 +22651,12 @@ async function executeSubagentWithTimeout({
|
|
|
21789
22651
|
}
|
|
21790
22652
|
return new Promise((resolveDecision) => {
|
|
21791
22653
|
let settled = false;
|
|
21792
|
-
const
|
|
22654
|
+
const resolve17 = (d) => {
|
|
21793
22655
|
if (settled) return;
|
|
21794
22656
|
settled = true;
|
|
21795
22657
|
resolveDecision(d);
|
|
21796
22658
|
};
|
|
21797
|
-
const fallback = setTimeout(() =>
|
|
22659
|
+
const fallback = setTimeout(() => resolve17("stop"), DECISION_TIMEOUT_MS);
|
|
21798
22660
|
const sessionId = currentSessionId();
|
|
21799
22661
|
budget._events?.emit("budget.threshold_reached", {
|
|
21800
22662
|
...sessionId ? { sessionId } : {},
|
|
@@ -21804,11 +22666,11 @@ async function executeSubagentWithTimeout({
|
|
|
21804
22666
|
timeoutMs: DECISION_TIMEOUT_MS,
|
|
21805
22667
|
extend: (extra) => {
|
|
21806
22668
|
clearTimeout(fallback);
|
|
21807
|
-
queueMicrotask(() =>
|
|
22669
|
+
queueMicrotask(() => resolve17({ extend: extra }));
|
|
21808
22670
|
},
|
|
21809
22671
|
deny: () => {
|
|
21810
22672
|
clearTimeout(fallback);
|
|
21811
|
-
|
|
22673
|
+
resolve17("stop");
|
|
21812
22674
|
}
|
|
21813
22675
|
});
|
|
21814
22676
|
});
|
|
@@ -22215,7 +23077,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
22215
23077
|
taskIds.map((id) => {
|
|
22216
23078
|
const cached = this.completedResults.find((r) => r.taskId === id);
|
|
22217
23079
|
if (cached) return cached;
|
|
22218
|
-
return new Promise((
|
|
23080
|
+
return new Promise((resolve17, reject) => {
|
|
22219
23081
|
const timeout = setTimeout(() => {
|
|
22220
23082
|
this.off("task.completed", handler);
|
|
22221
23083
|
reject(new Error(`awaitTasks timed out waiting for task "${id}"`));
|
|
@@ -22224,7 +23086,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
22224
23086
|
if (result.taskId === id) {
|
|
22225
23087
|
clearTimeout(timeout);
|
|
22226
23088
|
this.off("task.completed", handler);
|
|
22227
|
-
|
|
23089
|
+
resolve17(result);
|
|
22228
23090
|
}
|
|
22229
23091
|
};
|
|
22230
23092
|
this.on("task.completed", handler);
|
|
@@ -22249,13 +23111,13 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
22249
23111
|
const done = new Set(completed.map((r) => r.taskId));
|
|
22250
23112
|
return { completed, pending: taskIds.filter((id) => !done.has(id)) };
|
|
22251
23113
|
}
|
|
22252
|
-
return new Promise((
|
|
23114
|
+
return new Promise((resolve17) => {
|
|
22253
23115
|
let timer;
|
|
22254
23116
|
const handler = ({ result }) => {
|
|
22255
23117
|
if (!ids.has(result.taskId)) return;
|
|
22256
23118
|
if (timer) clearTimeout(timer);
|
|
22257
23119
|
this.off("task.completed", handler);
|
|
22258
|
-
|
|
23120
|
+
resolve17({
|
|
22259
23121
|
completed: [result],
|
|
22260
23122
|
pending: taskIds.filter((id) => id !== result.taskId)
|
|
22261
23123
|
});
|
|
@@ -22263,7 +23125,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
22263
23125
|
if (opts?.timeoutMs !== void 0) {
|
|
22264
23126
|
timer = setTimeout(() => {
|
|
22265
23127
|
this.off("task.completed", handler);
|
|
22266
|
-
|
|
23128
|
+
resolve17({ completed: [], pending: [...taskIds], timedOut: true });
|
|
22267
23129
|
}, opts.timeoutMs);
|
|
22268
23130
|
}
|
|
22269
23131
|
this.on("task.completed", handler);
|
|
@@ -23531,7 +24393,7 @@ var Director = class _Director {
|
|
|
23531
24393
|
// src/coordination/fleet-manager.ts
|
|
23532
24394
|
import { randomUUID as randomUUID17 } from "node:crypto";
|
|
23533
24395
|
import * as fsp26 from "node:fs/promises";
|
|
23534
|
-
import * as
|
|
24396
|
+
import * as path34 from "node:path";
|
|
23535
24397
|
var FleetManager = class {
|
|
23536
24398
|
/** The fleet-wide event bus. */
|
|
23537
24399
|
fleet;
|
|
@@ -23865,7 +24727,7 @@ var FleetManager = class {
|
|
|
23865
24727
|
})),
|
|
23866
24728
|
usage: this.usage.snapshot()
|
|
23867
24729
|
};
|
|
23868
|
-
await fsp26.mkdir(
|
|
24730
|
+
await fsp26.mkdir(path34.dirname(this.manifestPath), { recursive: true });
|
|
23869
24731
|
await atomicWrite(this.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
|
|
23870
24732
|
return this.manifestPath;
|
|
23871
24733
|
}
|
|
@@ -24028,7 +24890,7 @@ var FleetManager = class {
|
|
|
24028
24890
|
};
|
|
24029
24891
|
|
|
24030
24892
|
// src/coordination/remote-mailbox.ts
|
|
24031
|
-
import * as
|
|
24893
|
+
import * as path38 from "node:path";
|
|
24032
24894
|
|
|
24033
24895
|
// src/coordination/mailbox-constants.ts
|
|
24034
24896
|
var HQ_MAILBOX_SNAPSHOT_MIN_INTERVAL_MS = 1e4;
|
|
@@ -24038,15 +24900,15 @@ var MAILBOX_MAX_ACK_BATCH = 500;
|
|
|
24038
24900
|
|
|
24039
24901
|
// src/coordination/mailbox-project-server-client.ts
|
|
24040
24902
|
import { spawn as spawn5 } from "node:child_process";
|
|
24041
|
-
import * as
|
|
24903
|
+
import * as fs5 from "node:fs";
|
|
24042
24904
|
import * as net2 from "node:net";
|
|
24043
|
-
import * as
|
|
24905
|
+
import * as path36 from "node:path";
|
|
24044
24906
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
24045
24907
|
|
|
24046
24908
|
// src/coordination/mailbox-project-server-endpoint.ts
|
|
24047
24909
|
import { createHash as createHash6 } from "node:crypto";
|
|
24048
24910
|
import * as os3 from "node:os";
|
|
24049
|
-
import * as
|
|
24911
|
+
import * as path35 from "node:path";
|
|
24050
24912
|
|
|
24051
24913
|
// src/coordination/mailbox-project-server-protocol.ts
|
|
24052
24914
|
var MAILBOX_PROJECT_SERVER_PROTOCOL_VERSION = 4;
|
|
@@ -24079,7 +24941,7 @@ function encodeMailboxProjectServerMessage(message) {
|
|
|
24079
24941
|
// src/coordination/mailbox-project-server-endpoint.ts
|
|
24080
24942
|
var MAILBOX_PROJECT_SERVER_METADATA_FILE = ".mailbox-server.json";
|
|
24081
24943
|
function normalizeLocalPath(value) {
|
|
24082
|
-
const resolved =
|
|
24944
|
+
const resolved = path35.resolve(value);
|
|
24083
24945
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
24084
24946
|
}
|
|
24085
24947
|
function mailboxProjectServerKey(projectDir) {
|
|
@@ -24090,14 +24952,14 @@ function mailboxProjectServerEndpoint(projectDir) {
|
|
|
24090
24952
|
if (process.platform === "win32") {
|
|
24091
24953
|
return `\\\\.\\pipe\\wrongstack-mailbox-v${MAILBOX_PROJECT_SERVER_PROTOCOL_VERSION}-${key}`;
|
|
24092
24954
|
}
|
|
24093
|
-
return
|
|
24955
|
+
return path35.join(
|
|
24094
24956
|
os3.tmpdir(),
|
|
24095
24957
|
`wsmb-v${MAILBOX_PROJECT_SERVER_PROTOCOL_VERSION}`,
|
|
24096
24958
|
`${key}.sock`
|
|
24097
24959
|
);
|
|
24098
24960
|
}
|
|
24099
24961
|
function mailboxProjectServerMetadataPath(projectDir) {
|
|
24100
|
-
return
|
|
24962
|
+
return path35.join(path35.resolve(projectDir), MAILBOX_PROJECT_SERVER_METADATA_FILE);
|
|
24101
24963
|
}
|
|
24102
24964
|
|
|
24103
24965
|
// src/coordination/mailbox-project-server-client.ts
|
|
@@ -24108,7 +24970,7 @@ var DEFAULT_HEARTBEAT_INTERVAL_MS = 1e4;
|
|
|
24108
24970
|
var AUTH_RETRY_DELAY_MS = 150;
|
|
24109
24971
|
var AUTH_RETRY_MAX_ATTEMPTS = 13;
|
|
24110
24972
|
function normalizePath(value) {
|
|
24111
|
-
const resolved =
|
|
24973
|
+
const resolved = path36.resolve(value);
|
|
24112
24974
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
24113
24975
|
}
|
|
24114
24976
|
function resolveProjectServerUrl() {
|
|
@@ -24118,7 +24980,7 @@ function resolveProjectServerUrl() {
|
|
|
24118
24980
|
]) {
|
|
24119
24981
|
try {
|
|
24120
24982
|
const url = new URL(relative4, import.meta.url);
|
|
24121
|
-
if (url.protocol === "file:" &&
|
|
24983
|
+
if (url.protocol === "file:" && fs5.existsSync(fileURLToPath4(url))) return url;
|
|
24122
24984
|
} catch {
|
|
24123
24985
|
}
|
|
24124
24986
|
}
|
|
@@ -24128,7 +24990,7 @@ function isMailboxProjectServerAvailable() {
|
|
|
24128
24990
|
return resolveProjectServerUrl() !== null;
|
|
24129
24991
|
}
|
|
24130
24992
|
function delay2(ms) {
|
|
24131
|
-
return new Promise((
|
|
24993
|
+
return new Promise((resolve17) => setTimeout(resolve17, ms));
|
|
24132
24994
|
}
|
|
24133
24995
|
function isUnauthorizedMailboxError(error) {
|
|
24134
24996
|
return error instanceof Error && error.name === "UnauthorizedMailboxRequest";
|
|
@@ -24136,7 +24998,7 @@ function isUnauthorizedMailboxError(error) {
|
|
|
24136
24998
|
var MailboxProjectServerConnection = class {
|
|
24137
24999
|
constructor(projectDir) {
|
|
24138
25000
|
this.projectDir = projectDir;
|
|
24139
|
-
this.projectDir =
|
|
25001
|
+
this.projectDir = path36.resolve(projectDir);
|
|
24140
25002
|
this.endpoint = mailboxProjectServerEndpoint(this.projectDir);
|
|
24141
25003
|
this.state = {
|
|
24142
25004
|
status: isMailboxProjectServerAvailable() ? "offline" : "unavailable",
|
|
@@ -24301,7 +25163,7 @@ var MailboxProjectServerConnection = class {
|
|
|
24301
25163
|
this.info = null;
|
|
24302
25164
|
this.buffer = "";
|
|
24303
25165
|
this.authToken = void 0;
|
|
24304
|
-
return new Promise((
|
|
25166
|
+
return new Promise((resolve17, reject) => {
|
|
24305
25167
|
const socket = net2.createConnection(this.endpoint);
|
|
24306
25168
|
this.socket = socket;
|
|
24307
25169
|
socket.setEncoding("utf8");
|
|
@@ -24316,7 +25178,7 @@ var MailboxProjectServerConnection = class {
|
|
|
24316
25178
|
this.connectReject = null;
|
|
24317
25179
|
this.startHeartbeat();
|
|
24318
25180
|
this.transition("connected");
|
|
24319
|
-
|
|
25181
|
+
resolve17();
|
|
24320
25182
|
void this.request({ type: "request", op: "ping", args: {} }, 3e3).catch(
|
|
24321
25183
|
() => void 0
|
|
24322
25184
|
);
|
|
@@ -24345,7 +25207,7 @@ var MailboxProjectServerConnection = class {
|
|
|
24345
25207
|
currentAuthToken() {
|
|
24346
25208
|
if (this.authToken === void 0) {
|
|
24347
25209
|
try {
|
|
24348
|
-
const raw =
|
|
25210
|
+
const raw = fs5.readFileSync(mailboxProjectServerMetadataPath(this.projectDir), "utf8");
|
|
24349
25211
|
const parsed = JSON.parse(raw);
|
|
24350
25212
|
if (typeof parsed.authToken === "string" && parsed.authToken.length > 0) {
|
|
24351
25213
|
this.authToken = parsed.authToken;
|
|
@@ -24369,7 +25231,7 @@ var MailboxProjectServerConnection = class {
|
|
|
24369
25231
|
if (encoded.length > MAILBOX_PROJECT_SERVER_MAX_FRAME_CHARS) {
|
|
24370
25232
|
return Promise.reject(new Error("Mailbox project server request exceeded frame limit"));
|
|
24371
25233
|
}
|
|
24372
|
-
return new Promise((
|
|
25234
|
+
return new Promise((resolve17, reject) => {
|
|
24373
25235
|
const timer = setTimeout(() => {
|
|
24374
25236
|
const pending = this.pending.get(id);
|
|
24375
25237
|
if (!pending) return;
|
|
@@ -24377,7 +25239,7 @@ var MailboxProjectServerConnection = class {
|
|
|
24377
25239
|
pending.reject(new Error(`Mailbox ${message.type} exceeded its ${timeoutMs}ms timeout`));
|
|
24378
25240
|
}, timeoutMs);
|
|
24379
25241
|
timer.unref?.();
|
|
24380
|
-
this.pending.set(id, { resolve:
|
|
25242
|
+
this.pending.set(id, { resolve: resolve17, reject, timer });
|
|
24381
25243
|
socket.write(encoded);
|
|
24382
25244
|
});
|
|
24383
25245
|
}
|
|
@@ -24492,7 +25354,7 @@ var MailboxProjectServerConnection = class {
|
|
|
24492
25354
|
spawnDetachedServer() {
|
|
24493
25355
|
const url = resolveProjectServerUrl();
|
|
24494
25356
|
if (!url) throw new Error("Mailbox project server entrypoint is unavailable");
|
|
24495
|
-
|
|
25357
|
+
fs5.mkdirSync(this.projectDir, { recursive: true });
|
|
24496
25358
|
const child = spawn5(process.execPath, [fileURLToPath4(url), "--project-dir", this.projectDir], {
|
|
24497
25359
|
cwd: this.projectDir,
|
|
24498
25360
|
detached: process.platform !== "win32",
|
|
@@ -24593,9 +25455,9 @@ var CredentialVerifyThrottle = class {
|
|
|
24593
25455
|
var credentialVerifyThrottle = new CredentialVerifyThrottle();
|
|
24594
25456
|
|
|
24595
25457
|
// src/coordination/global-mailbox-paths.ts
|
|
24596
|
-
import * as
|
|
25458
|
+
import * as path37 from "node:path";
|
|
24597
25459
|
function resolveProjectDir(projectRoot, globalRoot) {
|
|
24598
|
-
return
|
|
25460
|
+
return path37.join(globalRoot, "projects", projectSlug(projectRoot));
|
|
24599
25461
|
}
|
|
24600
25462
|
|
|
24601
25463
|
// src/coordination/sqlite-mailbox.ts
|
|
@@ -24645,8 +25507,8 @@ var RemoteMailbox = class {
|
|
|
24645
25507
|
hqPublisher,
|
|
24646
25508
|
eventEmitter
|
|
24647
25509
|
} : optionsOrProjectDir;
|
|
24648
|
-
this.projectDir =
|
|
24649
|
-
this.messagePath =
|
|
25510
|
+
this.projectDir = path38.resolve(options.projectDir);
|
|
25511
|
+
this.messagePath = path38.join(this.projectDir, SQLITE_MAILBOX_FILE);
|
|
24650
25512
|
this.registryPath = this.messagePath;
|
|
24651
25513
|
this.clientRegistryPath = this.messagePath;
|
|
24652
25514
|
this.events = options.events;
|
|
@@ -24892,7 +25754,7 @@ var RemoteMailbox = class {
|
|
|
24892
25754
|
this.hqEventPending.clear();
|
|
24893
25755
|
return;
|
|
24894
25756
|
}
|
|
24895
|
-
const mailboxId = `${
|
|
25757
|
+
const mailboxId = `${path38.basename(this.projectDir)}:mailbox`;
|
|
24896
25758
|
const inFlight = this.query({ includeDeleted: true, limit: 100 }).then((messages) => {
|
|
24897
25759
|
const message = messages.find((candidate) => candidate.id === event.messageId);
|
|
24898
25760
|
const action = event.type === "message.sent" ? "message.sent" : "message.updated";
|
|
@@ -24915,7 +25777,7 @@ var RemoteMailbox = class {
|
|
|
24915
25777
|
if (!publisher || this.closed || !event.startsWith("mailbox.agent_") && !event.startsWith("mailbox.client_")) {
|
|
24916
25778
|
return;
|
|
24917
25779
|
}
|
|
24918
|
-
const mailboxId = `${
|
|
25780
|
+
const mailboxId = `${path38.basename(this.projectDir)}:mailbox`;
|
|
24919
25781
|
const record = typeof payload === "object" && payload !== null ? payload : {};
|
|
24920
25782
|
const agentId = typeof record["agentId"] === "string" ? record["agentId"] : void 0;
|
|
24921
25783
|
const action = event === "mailbox.agent_registered" ? "agent.registered" : event === "mailbox.agent_heartbeat" ? "agent.heartbeat" : event === "mailbox.agent_deregistered" ? "agent.deregistered" : void 0;
|
|
@@ -24948,7 +25810,7 @@ function createProjectMailbox(options) {
|
|
|
24948
25810
|
return new RemoteMailbox(options);
|
|
24949
25811
|
}
|
|
24950
25812
|
function getSharedProjectMailbox(projectDir, events, hqPublisher) {
|
|
24951
|
-
const key =
|
|
25813
|
+
const key = path38.resolve(projectDir);
|
|
24952
25814
|
let projectCache = sharedRemoteMailboxes.get(key);
|
|
24953
25815
|
if (!projectCache) {
|
|
24954
25816
|
projectCache = {
|
|
@@ -27298,25 +28160,25 @@ function requiredSendCapability(type) {
|
|
|
27298
28160
|
}
|
|
27299
28161
|
return "mail.send.informational";
|
|
27300
28162
|
}
|
|
27301
|
-
function requiredCredentialCapability(method,
|
|
27302
|
-
if (method === "POST" &&
|
|
27303
|
-
if (method === "POST" && (
|
|
28163
|
+
function requiredCredentialCapability(method, path44) {
|
|
28164
|
+
if (method === "POST" && path44 === "/mailbox/send") return "mail.send.informational";
|
|
28165
|
+
if (method === "POST" && (path44 === "/mailbox/query" || path44 === "/mailbox/check")) {
|
|
27304
28166
|
return "mail.read.self";
|
|
27305
28167
|
}
|
|
27306
|
-
if (method === "POST" && (
|
|
28168
|
+
if (method === "POST" && (path44 === "/mailbox/ack" || path44 === "/mailbox/ack-many")) {
|
|
27307
28169
|
return "mail.ack.self";
|
|
27308
28170
|
}
|
|
27309
|
-
if (method === "POST" &&
|
|
27310
|
-
if (method === "POST" &&
|
|
28171
|
+
if (method === "POST" && path44 === "/mailbox/unread-count") return "mail.read.self";
|
|
28172
|
+
if (method === "POST" && path44 === "/mailbox/agents/register") {
|
|
27311
28173
|
return "mail.presence.register.self";
|
|
27312
28174
|
}
|
|
27313
|
-
if (method === "POST" &&
|
|
28175
|
+
if (method === "POST" && path44 === "/mailbox/agents/heartbeat") {
|
|
27314
28176
|
return "mail.presence.heartbeat.self";
|
|
27315
28177
|
}
|
|
27316
|
-
if (method === "GET" && (
|
|
28178
|
+
if (method === "GET" && (path44 === "/mailbox/agents" || path44 === "/mailbox/agents/online")) {
|
|
27317
28179
|
return "mail.presence.read";
|
|
27318
28180
|
}
|
|
27319
|
-
if (method === "GET" &&
|
|
28181
|
+
if (method === "GET" && path44 === "/mailbox/events") return "mail.events.self";
|
|
27320
28182
|
return void 0;
|
|
27321
28183
|
}
|
|
27322
28184
|
async function checkMailbox(mailbox, input, minTimestampIso, includeReceiptState = false, eligibleRecipients, readerRole) {
|
|
@@ -27536,7 +28398,7 @@ function createMailboxHttpRouter(options) {
|
|
|
27536
28398
|
return;
|
|
27537
28399
|
}
|
|
27538
28400
|
const customAccess = options.authorize ? await options.authorize(request) : void 0;
|
|
27539
|
-
let
|
|
28401
|
+
let access6 = customAccess ?? { allowed: true };
|
|
27540
28402
|
const presentedCredential = parseCredentialAuthorization(request);
|
|
27541
28403
|
if (options.credentialStore !== void 0 && presentedCredential !== void 0) {
|
|
27542
28404
|
const persistedAccess = await authorizePersistedMailboxCredential(
|
|
@@ -27544,30 +28406,30 @@ function createMailboxHttpRouter(options) {
|
|
|
27544
28406
|
options.credentialStore
|
|
27545
28407
|
);
|
|
27546
28408
|
if (!persistedAccess.allowed) {
|
|
27547
|
-
|
|
28409
|
+
access6 = persistedAccess;
|
|
27548
28410
|
} else if (persistedAccess.actor === void 0) {
|
|
27549
|
-
|
|
27550
|
-
} else if (
|
|
27551
|
-
if (
|
|
27552
|
-
|
|
28411
|
+
access6 = { allowed: false };
|
|
28412
|
+
} else if (access6.allowed) {
|
|
28413
|
+
if (access6.actor !== void 0 && (access6.actor.actorId !== persistedAccess.actor.actorId || access6.actor.projectId !== persistedAccess.actor.projectId)) {
|
|
28414
|
+
access6 = { allowed: false };
|
|
27553
28415
|
} else {
|
|
27554
|
-
|
|
27555
|
-
...
|
|
28416
|
+
access6 = {
|
|
28417
|
+
...access6,
|
|
27556
28418
|
actor: persistedAccess.actor,
|
|
27557
|
-
...
|
|
28419
|
+
...access6.rateLimitKey ?? persistedAccess.rateLimitKey ? { rateLimitKey: access6.rateLimitKey ?? persistedAccess.rateLimitKey } : {}
|
|
27558
28420
|
};
|
|
27559
28421
|
}
|
|
27560
28422
|
}
|
|
27561
28423
|
} else if (customAccess === void 0 && options.credentialStore !== void 0) {
|
|
27562
|
-
|
|
28424
|
+
access6 = { allowed: false };
|
|
27563
28425
|
}
|
|
27564
|
-
if (
|
|
28426
|
+
if (access6.allowed && access6.actor !== void 0 && options.projectId !== void 0 && access6.actor.projectId !== options.projectId) {
|
|
27565
28427
|
writeJson(response, 403, {
|
|
27566
28428
|
error: { code: "FORBIDDEN", message: "credential is scoped to a different project" }
|
|
27567
28429
|
});
|
|
27568
28430
|
return;
|
|
27569
28431
|
}
|
|
27570
|
-
if (!
|
|
28432
|
+
if (!access6.allowed) {
|
|
27571
28433
|
const forwardedFor = request.headers["x-forwarded-for"];
|
|
27572
28434
|
const clientIp = (Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor)?.split(",")[0]?.trim() ?? request.socket?.remoteAddress ?? "unknown";
|
|
27573
28435
|
console.warn(
|
|
@@ -27583,14 +28445,14 @@ function createMailboxHttpRouter(options) {
|
|
|
27583
28445
|
);
|
|
27584
28446
|
writeJson(
|
|
27585
28447
|
response,
|
|
27586
|
-
|
|
27587
|
-
|
|
28448
|
+
access6.status ?? 401,
|
|
28449
|
+
access6.body ?? {
|
|
27588
28450
|
error: { code: "UNAUTHORIZED", message: "invalid or missing authorization credential" }
|
|
27589
28451
|
}
|
|
27590
28452
|
);
|
|
27591
28453
|
return;
|
|
27592
28454
|
}
|
|
27593
|
-
if (options.rateLimiter &&
|
|
28455
|
+
if (options.rateLimiter && access6.rateLimitKey !== void 0 && !options.rateLimiter.allow(access6.rateLimitKey)) {
|
|
27594
28456
|
writeJson(response, 429, {
|
|
27595
28457
|
error: {
|
|
27596
28458
|
code: "RATE_LIMITED",
|
|
@@ -27610,7 +28472,7 @@ function createMailboxHttpRouter(options) {
|
|
|
27610
28472
|
defaultMaxAgeMs,
|
|
27611
28473
|
closeSseStreams,
|
|
27612
28474
|
routePath,
|
|
27613
|
-
|
|
28475
|
+
access6.actor,
|
|
27614
28476
|
options.credentialStore
|
|
27615
28477
|
);
|
|
27616
28478
|
} catch (error) {
|
|
@@ -27638,16 +28500,16 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
|
|
|
27638
28500
|
throw validationError(`routePath must not start with '?' (got ${JSON.stringify(routePath)})`);
|
|
27639
28501
|
}
|
|
27640
28502
|
const queryIndex = url.indexOf("?");
|
|
27641
|
-
const
|
|
28503
|
+
const path44 = queryIndex === -1 ? url : url.slice(0, queryIndex);
|
|
27642
28504
|
if (actor !== void 0) {
|
|
27643
|
-
const requiredCapability = requiredCredentialCapability(method,
|
|
28505
|
+
const requiredCapability = requiredCredentialCapability(method, path44);
|
|
27644
28506
|
if (requiredCapability === void 0) {
|
|
27645
28507
|
writeJson(response, 403, {
|
|
27646
|
-
error: { code: "FORBIDDEN", message: `credential access is not permitted for ${method} ${
|
|
28508
|
+
error: { code: "FORBIDDEN", message: `credential access is not permitted for ${method} ${path44}` }
|
|
27647
28509
|
});
|
|
27648
28510
|
return;
|
|
27649
28511
|
}
|
|
27650
|
-
if (
|
|
28512
|
+
if (path44 !== "/mailbox/send" && !hasMailboxCapability(actor, requiredCapability)) {
|
|
27651
28513
|
writeJson(response, 403, {
|
|
27652
28514
|
error: {
|
|
27653
28515
|
code: "FORBIDDEN",
|
|
@@ -27657,7 +28519,7 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
|
|
|
27657
28519
|
return;
|
|
27658
28520
|
}
|
|
27659
28521
|
}
|
|
27660
|
-
if (method === "POST" &&
|
|
28522
|
+
if (method === "POST" && path44 === "/mailbox/send") {
|
|
27661
28523
|
const input = validateSend(
|
|
27662
28524
|
await readJsonBody(request, maxBodyBytes),
|
|
27663
28525
|
actor?.actorId,
|
|
@@ -27679,7 +28541,7 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
|
|
|
27679
28541
|
writeJson(response, 201, await mailbox.send(input));
|
|
27680
28542
|
return;
|
|
27681
28543
|
}
|
|
27682
|
-
if (method === "POST" &&
|
|
28544
|
+
if (method === "POST" && path44 === "/mailbox/query") {
|
|
27683
28545
|
const queryContext = parseSinceMs(url, defaultMaxAgeMs);
|
|
27684
28546
|
if ("error" in queryContext) {
|
|
27685
28547
|
writeJson(response, 400, { error: queryContext.error });
|
|
@@ -27699,7 +28561,7 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
|
|
|
27699
28561
|
writeJson(response, 200, { data: projected, count: projected.length });
|
|
27700
28562
|
return;
|
|
27701
28563
|
}
|
|
27702
|
-
if (method === "POST" &&
|
|
28564
|
+
if (method === "POST" && path44 === "/mailbox/check") {
|
|
27703
28565
|
const queryContext = parseSinceMs(url, defaultMaxAgeMs);
|
|
27704
28566
|
if ("error" in queryContext) {
|
|
27705
28567
|
writeJson(response, 400, { error: queryContext.error });
|
|
@@ -27733,7 +28595,7 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
|
|
|
27733
28595
|
writeJson(response, 200, { data: projected, count: projected.length });
|
|
27734
28596
|
return;
|
|
27735
28597
|
}
|
|
27736
|
-
if (method === "POST" &&
|
|
28598
|
+
if (method === "POST" && path44 === "/mailbox/ack") {
|
|
27737
28599
|
const input = validateAck(await readJsonBody(request, maxBodyBytes), actor?.actorId);
|
|
27738
28600
|
if (actor !== void 0) {
|
|
27739
28601
|
input.readerId = actor.actorId;
|
|
@@ -27747,7 +28609,7 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
|
|
|
27747
28609
|
writeJson(response, 200, { updated: projectedAck });
|
|
27748
28610
|
return;
|
|
27749
28611
|
}
|
|
27750
|
-
if (method === "POST" &&
|
|
28612
|
+
if (method === "POST" && path44 === "/mailbox/ack-many") {
|
|
27751
28613
|
const input = validateAckMany(await readJsonBody(request, maxBodyBytes), actor?.actorId);
|
|
27752
28614
|
if (actor !== void 0) {
|
|
27753
28615
|
const requestedIds = new Set(input.acks.map((ack) => ack.messageId));
|
|
@@ -27763,54 +28625,54 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
|
|
|
27763
28625
|
writeJson(response, 200, { updated: projectedMany, count: projectedMany.length });
|
|
27764
28626
|
return;
|
|
27765
28627
|
}
|
|
27766
|
-
if (method === "POST" &&
|
|
28628
|
+
if (method === "POST" && path44 === "/mailbox/unread-count") {
|
|
27767
28629
|
const body = await readJsonBody(request, maxBodyBytes);
|
|
27768
28630
|
const count = actor === void 0 ? await mailbox.unreadCount(requireString2(body, "forAgentId")) : await unreadCountForActor(mailbox, actor);
|
|
27769
28631
|
writeJson(response, 200, { count });
|
|
27770
28632
|
return;
|
|
27771
28633
|
}
|
|
27772
|
-
if (method === "POST" &&
|
|
28634
|
+
if (method === "POST" && path44 === "/mailbox/agents/register") {
|
|
27773
28635
|
const input = validateAgentRegistration(await readJsonBody(request, maxBodyBytes), actor);
|
|
27774
28636
|
await mailbox.registerAgent(input);
|
|
27775
28637
|
writeJson(response, 200, { ok: true });
|
|
27776
28638
|
return;
|
|
27777
28639
|
}
|
|
27778
|
-
if (method === "POST" &&
|
|
28640
|
+
if (method === "POST" && path44 === "/mailbox/agents/heartbeat") {
|
|
27779
28641
|
const input = validateAgentHeartbeat(await readJsonBody(request, maxBodyBytes), actor?.actorId);
|
|
27780
28642
|
if (actor !== void 0) input.agentId = actor.actorId;
|
|
27781
28643
|
await mailbox.heartbeat(input);
|
|
27782
28644
|
writeJson(response, 200, { ok: true });
|
|
27783
28645
|
return;
|
|
27784
28646
|
}
|
|
27785
|
-
if (method === "POST" &&
|
|
28647
|
+
if (method === "POST" && path44 === "/mailbox/register-client") {
|
|
27786
28648
|
await mailbox.registerClient(
|
|
27787
28649
|
validateClientRegistration(await readJsonBody(request, maxBodyBytes))
|
|
27788
28650
|
);
|
|
27789
28651
|
writeJson(response, 200, { ok: true });
|
|
27790
28652
|
return;
|
|
27791
28653
|
}
|
|
27792
|
-
if (method === "POST" &&
|
|
28654
|
+
if (method === "POST" && path44 === "/mailbox/heartbeat") {
|
|
27793
28655
|
await mailbox.clientHeartbeat(
|
|
27794
28656
|
validateClientHeartbeat(await readJsonBody(request, maxBodyBytes))
|
|
27795
28657
|
);
|
|
27796
28658
|
writeJson(response, 200, { ok: true });
|
|
27797
28659
|
return;
|
|
27798
28660
|
}
|
|
27799
|
-
if (method === "POST" &&
|
|
28661
|
+
if (method === "POST" && path44 === "/mailbox/purge-clients") {
|
|
27800
28662
|
writeJson(response, 200, { ok: true, purged: await mailbox.purgeClients() });
|
|
27801
28663
|
return;
|
|
27802
28664
|
}
|
|
27803
|
-
if (method === "GET" &&
|
|
28665
|
+
if (method === "GET" && path44 === "/mailbox/agents") {
|
|
27804
28666
|
const agents = await mailbox.getAgentStatuses();
|
|
27805
28667
|
writeJson(response, 200, { data: agents, count: agents.length });
|
|
27806
28668
|
return;
|
|
27807
28669
|
}
|
|
27808
|
-
if (method === "GET" &&
|
|
28670
|
+
if (method === "GET" && path44 === "/mailbox/agents/online") {
|
|
27809
28671
|
const agents = await mailbox.getOnlineAgents();
|
|
27810
28672
|
writeJson(response, 200, { data: agents, count: agents.length });
|
|
27811
28673
|
return;
|
|
27812
28674
|
}
|
|
27813
|
-
if (method === "GET" &&
|
|
28675
|
+
if (method === "GET" && path44 === "/mailbox/events" && eventEmitter) {
|
|
27814
28676
|
const queryContext = parseSinceMs(url, defaultMaxAgeMs);
|
|
27815
28677
|
if ("error" in queryContext) {
|
|
27816
28678
|
writeJson(response, 400, { error: queryContext.error });
|
|
@@ -27870,16 +28732,16 @@ function writeJson(response, status, body) {
|
|
|
27870
28732
|
var NULL_FLEET_BUS = new FleetBus();
|
|
27871
28733
|
|
|
27872
28734
|
// src/coordination/package-author-tracker.ts
|
|
27873
|
-
import * as
|
|
27874
|
-
import * as
|
|
28735
|
+
import * as fs6 from "node:fs/promises";
|
|
28736
|
+
import * as path39 from "node:path";
|
|
27875
28737
|
var DEFAULT_MAX_ENTRIES2 = 1e4;
|
|
27876
28738
|
var LOG_FILENAME2 = "package-authors.json";
|
|
27877
28739
|
function logPath2(storageDir) {
|
|
27878
|
-
return
|
|
28740
|
+
return path39.join(storageDir, LOG_FILENAME2);
|
|
27879
28741
|
}
|
|
27880
28742
|
async function loadLog2(storageDir, projectRoot) {
|
|
27881
28743
|
try {
|
|
27882
|
-
const raw = await
|
|
28744
|
+
const raw = await fs6.readFile(logPath2(storageDir), "utf-8");
|
|
27883
28745
|
const parsed = JSON.parse(raw);
|
|
27884
28746
|
if (!parsed.entries || !Array.isArray(parsed.entries)) {
|
|
27885
28747
|
return { projectRoot, entries: [] };
|
|
@@ -27897,7 +28759,7 @@ async function saveLog2(storageDir, log) {
|
|
|
27897
28759
|
`);
|
|
27898
28760
|
}
|
|
27899
28761
|
function detectEcosystem(manifestPath) {
|
|
27900
|
-
const name =
|
|
28762
|
+
const name = path39.win32.basename(manifestPath).toLowerCase();
|
|
27901
28763
|
if (name === "package.json") return "npm";
|
|
27902
28764
|
if (name === "go.mod") return "go";
|
|
27903
28765
|
if (name === "cargo.toml") return "cargo";
|
|
@@ -28787,12 +29649,12 @@ import { randomBytes as randomBytes2 } from "node:crypto";
|
|
|
28787
29649
|
import * as fsp27 from "node:fs/promises";
|
|
28788
29650
|
import { execFile } from "node:child_process";
|
|
28789
29651
|
import * as os4 from "node:os";
|
|
28790
|
-
import * as
|
|
29652
|
+
import * as path40 from "node:path";
|
|
28791
29653
|
var MAILBOX_BRIDGE_LOCK_FILENAME = ".mailbox-bridge.lock";
|
|
28792
29654
|
var MAILBOX_BRIDGE_TOKEN_FILENAME = ".mailbox.token";
|
|
28793
29655
|
async function acquireOrJoin(opts) {
|
|
28794
|
-
const lockPath =
|
|
28795
|
-
const tokenPath =
|
|
29656
|
+
const lockPath = path40.join(opts.projectDir, MAILBOX_BRIDGE_LOCK_FILENAME);
|
|
29657
|
+
const tokenPath = path40.join(opts.projectDir, MAILBOX_BRIDGE_TOKEN_FILENAME);
|
|
28796
29658
|
const inspected = await readLockForInspection(lockPath);
|
|
28797
29659
|
if (inspected.kind === "live") {
|
|
28798
29660
|
const existing = inspected.lock;
|
|
@@ -28820,8 +29682,8 @@ async function acquireOrJoin(opts) {
|
|
|
28820
29682
|
return { kind: "acquired", lock: tentative, tokenPath };
|
|
28821
29683
|
}
|
|
28822
29684
|
async function finalize(projectDir, tentative, boundPort) {
|
|
28823
|
-
const lockPath =
|
|
28824
|
-
const tokenPath =
|
|
29685
|
+
const lockPath = path40.join(projectDir, MAILBOX_BRIDGE_LOCK_FILENAME);
|
|
29686
|
+
const tokenPath = path40.join(projectDir, MAILBOX_BRIDGE_TOKEN_FILENAME);
|
|
28825
29687
|
const finalized = {
|
|
28826
29688
|
...tentative,
|
|
28827
29689
|
port: boundPort,
|
|
@@ -28832,8 +29694,8 @@ async function finalize(projectDir, tentative, boundPort) {
|
|
|
28832
29694
|
return finalized;
|
|
28833
29695
|
}
|
|
28834
29696
|
async function release(projectDir, generation) {
|
|
28835
|
-
const lockPath =
|
|
28836
|
-
const tokenPath =
|
|
29697
|
+
const lockPath = path40.join(projectDir, MAILBOX_BRIDGE_LOCK_FILENAME);
|
|
29698
|
+
const tokenPath = path40.join(projectDir, MAILBOX_BRIDGE_TOKEN_FILENAME);
|
|
28837
29699
|
try {
|
|
28838
29700
|
const raw = await fsp27.readFile(lockPath, "utf-8");
|
|
28839
29701
|
const parsed = JSON.parse(raw);
|
|
@@ -28867,7 +29729,7 @@ async function readLockForInspection(lockPath) {
|
|
|
28867
29729
|
return { kind: "live", lock: parsed };
|
|
28868
29730
|
}
|
|
28869
29731
|
async function readLiveLock(projectDir) {
|
|
28870
|
-
const lockPath =
|
|
29732
|
+
const lockPath = path40.join(projectDir, MAILBOX_BRIDGE_LOCK_FILENAME);
|
|
28871
29733
|
const result = await readLockForInspection(lockPath);
|
|
28872
29734
|
if (result.kind === "live") {
|
|
28873
29735
|
return { kind: "live", lock: result.lock };
|
|
@@ -28878,7 +29740,7 @@ async function readLiveLock(projectDir) {
|
|
|
28878
29740
|
return { kind: "absent" };
|
|
28879
29741
|
}
|
|
28880
29742
|
async function atomicWriteJson(targetPath, value) {
|
|
28881
|
-
const dir =
|
|
29743
|
+
const dir = path40.dirname(targetPath);
|
|
28882
29744
|
await fsp27.mkdir(dir, { recursive: true });
|
|
28883
29745
|
const tmp = `${targetPath}.tmp.${process.pid}.${randomBytes2(4).toString("hex")}`;
|
|
28884
29746
|
const body = JSON.stringify(value, null, 2) + "\n";
|
|
@@ -28894,7 +29756,7 @@ async function isProcessAlive(pid) {
|
|
|
28894
29756
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
28895
29757
|
if (pid === process.pid) return true;
|
|
28896
29758
|
if (os4.platform() === "win32") {
|
|
28897
|
-
return new Promise((
|
|
29759
|
+
return new Promise((resolve17) => {
|
|
28898
29760
|
execFile(
|
|
28899
29761
|
"tasklist",
|
|
28900
29762
|
["/FI", `PID eq ${pid}`, "/NH", "/FO", "CSV"],
|
|
@@ -28906,11 +29768,11 @@ async function isProcessAlive(pid) {
|
|
|
28906
29768
|
},
|
|
28907
29769
|
(error, out) => {
|
|
28908
29770
|
if (error) {
|
|
28909
|
-
|
|
29771
|
+
resolve17(false);
|
|
28910
29772
|
return;
|
|
28911
29773
|
}
|
|
28912
29774
|
const match = /(?:^|\n)"[^"]*","(\d+)"/.exec(out);
|
|
28913
|
-
|
|
29775
|
+
resolve17(match !== null && match[1] === String(pid));
|
|
28914
29776
|
}
|
|
28915
29777
|
);
|
|
28916
29778
|
});
|
|
@@ -29061,8 +29923,8 @@ function acceptManifestCandidate(candidate) {
|
|
|
29061
29923
|
if (normalized.split("/").includes("..")) return false;
|
|
29062
29924
|
return isManifestFile(normalized);
|
|
29063
29925
|
}
|
|
29064
|
-
function isManifestFile(
|
|
29065
|
-
const name = pathBasename(
|
|
29926
|
+
function isManifestFile(path44) {
|
|
29927
|
+
const name = pathBasename(path44).toLowerCase();
|
|
29066
29928
|
const manifests = [
|
|
29067
29929
|
"package.json",
|
|
29068
29930
|
"package-lock.json",
|
|
@@ -29142,7 +30004,7 @@ function buildTechStackTask(msg, manifestPath) {
|
|
|
29142
30004
|
].join("\n");
|
|
29143
30005
|
}
|
|
29144
30006
|
|
|
29145
|
-
// src/
|
|
30007
|
+
// src/coordination/collab-pause.ts
|
|
29146
30008
|
function collabPauseMiddleware(bus, opts = {}) {
|
|
29147
30009
|
const timeoutMs = opts.defaultTimeoutMs ?? 6e4;
|
|
29148
30010
|
const logger = opts.logger;
|
|
@@ -29373,10 +30235,10 @@ var AdaptiveConcurrencyController = class {
|
|
|
29373
30235
|
};
|
|
29374
30236
|
|
|
29375
30237
|
// src/coordination/agent-monitor.ts
|
|
29376
|
-
import { createReadStream as
|
|
29377
|
-
import * as
|
|
29378
|
-
import * as
|
|
29379
|
-
import { createInterface as
|
|
30238
|
+
import { createReadStream as createReadStream7 } from "node:fs";
|
|
30239
|
+
import * as fs7 from "node:fs/promises";
|
|
30240
|
+
import * as path41 from "node:path";
|
|
30241
|
+
import { createInterface as createInterface7 } from "node:readline";
|
|
29380
30242
|
var AgentMonitorService = class _AgentMonitorService {
|
|
29381
30243
|
_fleetBus;
|
|
29382
30244
|
_events;
|
|
@@ -29455,7 +30317,7 @@ var AgentMonitorService = class _AgentMonitorService {
|
|
|
29455
30317
|
async loadSessionsFromDisk() {
|
|
29456
30318
|
let subagentIds;
|
|
29457
30319
|
try {
|
|
29458
|
-
const dirents = await
|
|
30320
|
+
const dirents = await fs7.readdir(this._transcriptsDir, { withFileTypes: true });
|
|
29459
30321
|
subagentIds = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
|
|
29460
30322
|
} catch {
|
|
29461
30323
|
return this.getAllSessions();
|
|
@@ -29479,12 +30341,12 @@ var AgentMonitorService = class _AgentMonitorService {
|
|
|
29479
30341
|
return this.getAllSessions();
|
|
29480
30342
|
}
|
|
29481
30343
|
async _readTranscriptFile(subagentId) {
|
|
29482
|
-
const file =
|
|
29483
|
-
const accessible = await
|
|
30344
|
+
const file = path41.join(this._transcriptsDir, subagentId, "transcript.jsonl");
|
|
30345
|
+
const accessible = await fs7.access(file).then(() => true).catch(() => false);
|
|
29484
30346
|
if (!accessible) return [];
|
|
29485
30347
|
const out = [];
|
|
29486
|
-
const input =
|
|
29487
|
-
const lines =
|
|
30348
|
+
const input = createReadStream7(file, { encoding: "utf8" });
|
|
30349
|
+
const lines = createInterface7({ input, crlfDelay: Number.POSITIVE_INFINITY });
|
|
29488
30350
|
try {
|
|
29489
30351
|
for await (const line of lines) {
|
|
29490
30352
|
const trimmed = line.trim();
|
|
@@ -29725,9 +30587,9 @@ var AgentMonitorService = class _AgentMonitorService {
|
|
|
29725
30587
|
* firehose (or, worse, only the first word).
|
|
29726
30588
|
*/
|
|
29727
30589
|
_appendStreamDelta(subagentId, session, kind, text, iteration) {
|
|
29728
|
-
const
|
|
29729
|
-
if (
|
|
29730
|
-
|
|
30590
|
+
const open10 = this._openStreams.get(subagentId);
|
|
30591
|
+
if (open10 && open10.entry.kind === kind && open10.entry.iteration === iteration && open10.entry.content.length + text.length <= _AgentMonitorService._MAX_SEGMENT_CHARS) {
|
|
30592
|
+
open10.entry.content += text;
|
|
29731
30593
|
return;
|
|
29732
30594
|
}
|
|
29733
30595
|
this._closeStream(subagentId);
|
|
@@ -29751,11 +30613,11 @@ var AgentMonitorService = class _AgentMonitorService {
|
|
|
29751
30613
|
* announce the completed entry on the local bus + HQ callback.
|
|
29752
30614
|
*/
|
|
29753
30615
|
_closeStream(subagentId) {
|
|
29754
|
-
const
|
|
29755
|
-
if (!
|
|
30616
|
+
const open10 = this._openStreams.get(subagentId);
|
|
30617
|
+
if (!open10) return;
|
|
29756
30618
|
this._openStreams.delete(subagentId);
|
|
29757
|
-
this._enqueueWrite(subagentId,
|
|
29758
|
-
this._emitEntry(
|
|
30619
|
+
this._enqueueWrite(subagentId, open10.entry);
|
|
30620
|
+
this._emitEntry(open10.entry);
|
|
29759
30621
|
}
|
|
29760
30622
|
_addEntry(subagentId, entry) {
|
|
29761
30623
|
const session = this._sessions.get(subagentId);
|
|
@@ -29820,14 +30682,14 @@ var AgentMonitorService = class _AgentMonitorService {
|
|
|
29820
30682
|
this._writeDrain = drain;
|
|
29821
30683
|
}
|
|
29822
30684
|
async _appendToFile(subagentId, line) {
|
|
29823
|
-
const dir =
|
|
30685
|
+
const dir = path41.join(this._transcriptsDir, subagentId);
|
|
29824
30686
|
if (!this._ensuredDirs.has(dir)) {
|
|
29825
|
-
await
|
|
30687
|
+
await fs7.mkdir(dir, { recursive: true });
|
|
29826
30688
|
this._ensuredDirs.add(dir);
|
|
29827
30689
|
}
|
|
29828
|
-
const filePath =
|
|
30690
|
+
const filePath = path41.join(dir, "transcript.jsonl");
|
|
29829
30691
|
try {
|
|
29830
|
-
await
|
|
30692
|
+
await fs7.appendFile(filePath, line, { encoding: "utf8", mode: SECRET_FILE_MODE });
|
|
29831
30693
|
} catch (error) {
|
|
29832
30694
|
this._ensuredDirs.delete(dir);
|
|
29833
30695
|
throw error;
|
|
@@ -30218,7 +31080,7 @@ import { randomUUID as randomUUID22 } from "node:crypto";
|
|
|
30218
31080
|
// src/coordination/knowledge-graph.ts
|
|
30219
31081
|
import { randomUUID as randomUUID20 } from "node:crypto";
|
|
30220
31082
|
import * as fsp28 from "node:fs/promises";
|
|
30221
|
-
import * as
|
|
31083
|
+
import * as path42 from "node:path";
|
|
30222
31084
|
var DEFAULT_MAX_NODES = 2e3;
|
|
30223
31085
|
var MAX_SUBSCRIPTIONS = 1e3;
|
|
30224
31086
|
var MAX_PENDING_DELIVERIES_PER_SUBSCRIPTION = 1e3;
|
|
@@ -30253,8 +31115,8 @@ var KnowledgeGraph = class _KnowledgeGraph {
|
|
|
30253
31115
|
return this.index;
|
|
30254
31116
|
}
|
|
30255
31117
|
constructor(sessionDir, maxNodes = DEFAULT_MAX_NODES, compactEveryWrites = Math.max(1e3, maxNodes * 4)) {
|
|
30256
|
-
this.filePath =
|
|
30257
|
-
this.graphFilePath =
|
|
31118
|
+
this.filePath = path42.join(sessionDir, "_knowledge_graph");
|
|
31119
|
+
this.graphFilePath = path42.join(this.filePath, "graph.jsonl");
|
|
30258
31120
|
this.maxNodes = maxNodes;
|
|
30259
31121
|
this.compactEveryWrites = Math.max(1, Math.floor(compactEveryWrites));
|
|
30260
31122
|
}
|
|
@@ -31358,6 +32220,11 @@ var ConsensusProtocol = class {
|
|
|
31358
32220
|
if (change?.type !== "change") {
|
|
31359
32221
|
throw new Error(`ConsensusProtocol: no change found with id "${changeId}"`);
|
|
31360
32222
|
}
|
|
32223
|
+
if (change.status === "approved" || change.status === "rejected") {
|
|
32224
|
+
throw new Error(
|
|
32225
|
+
`ConsensusProtocol: cannot initiate vote on already resolved change "${changeId}" (status: "${change.status}")`
|
|
32226
|
+
);
|
|
32227
|
+
}
|
|
31361
32228
|
await this.graph.update(changeId, { status: "proposed", votes: [] });
|
|
31362
32229
|
const eligible = this._eligibleVoters(change);
|
|
31363
32230
|
this._notifyVoters(change, eligible, "vote_initiated");
|
|
@@ -31371,6 +32238,11 @@ var ConsensusProtocol = class {
|
|
|
31371
32238
|
if (change?.type !== "change") {
|
|
31372
32239
|
throw new Error(`ConsensusProtocol: no change found for "${changeId}"`);
|
|
31373
32240
|
}
|
|
32241
|
+
if (change.status !== "proposed") {
|
|
32242
|
+
throw new Error(
|
|
32243
|
+
`ConsensusProtocol: cannot cast vote on change "${changeId}" with status "${change.status}" (ballot is closed)`
|
|
32244
|
+
);
|
|
32245
|
+
}
|
|
31374
32246
|
const voter = this.voters.get(voterId);
|
|
31375
32247
|
if (!voter) {
|
|
31376
32248
|
throw new Error(`ConsensusProtocol: unknown voter "${voterId}"`);
|
|
@@ -31445,8 +32317,8 @@ var ConsensusProtocol = class {
|
|
|
31445
32317
|
(sum, v) => sum + (this.voters.get(v.agentId)?.weight ?? 1),
|
|
31446
32318
|
0
|
|
31447
32319
|
);
|
|
31448
|
-
const totalWeight =
|
|
31449
|
-
(sum,
|
|
32320
|
+
const totalWeight = eligible.reduce(
|
|
32321
|
+
(sum, id) => sum + (this.voters.get(id)?.weight ?? 1),
|
|
31450
32322
|
0
|
|
31451
32323
|
);
|
|
31452
32324
|
const castCount = votes.length;
|
|
@@ -32166,17 +33038,17 @@ ${input.detail}`
|
|
|
32166
33038
|
_waitForDagProgress(timeoutMs) {
|
|
32167
33039
|
const before = this._dagProgressKey();
|
|
32168
33040
|
if (this.dag.isDone()) return Promise.resolve();
|
|
32169
|
-
return new Promise((
|
|
33041
|
+
return new Promise((resolve17) => {
|
|
32170
33042
|
let off;
|
|
32171
33043
|
const timer = setTimeout(() => {
|
|
32172
33044
|
off?.();
|
|
32173
|
-
|
|
33045
|
+
resolve17();
|
|
32174
33046
|
}, timeoutMs);
|
|
32175
33047
|
off = this.dag.onEvent(() => {
|
|
32176
33048
|
if (this._dagProgressKey() === before) return;
|
|
32177
33049
|
clearTimeout(timer);
|
|
32178
33050
|
off?.();
|
|
32179
|
-
|
|
33051
|
+
resolve17();
|
|
32180
33052
|
});
|
|
32181
33053
|
});
|
|
32182
33054
|
}
|
|
@@ -32484,8 +33356,8 @@ var CollaborationBus = class _CollaborationBus {
|
|
|
32484
33356
|
if (this.isPaused()) return false;
|
|
32485
33357
|
this.pausedAtMs = Date.now();
|
|
32486
33358
|
this.pausedBy = byParticipant;
|
|
32487
|
-
this.pausePromise = new Promise((
|
|
32488
|
-
this.pauseResolve =
|
|
33359
|
+
this.pausePromise = new Promise((resolve17) => {
|
|
33360
|
+
this.pauseResolve = resolve17;
|
|
32489
33361
|
});
|
|
32490
33362
|
return true;
|
|
32491
33363
|
}
|
|
@@ -32521,8 +33393,8 @@ var CollaborationBus = class _CollaborationBus {
|
|
|
32521
33393
|
return true;
|
|
32522
33394
|
}
|
|
32523
33395
|
let timer;
|
|
32524
|
-
const timeoutPromise = new Promise((
|
|
32525
|
-
timer = setTimeout(() =>
|
|
33396
|
+
const timeoutPromise = new Promise((resolve17) => {
|
|
33397
|
+
timer = setTimeout(() => resolve17("timeout"), timeoutMs);
|
|
32526
33398
|
});
|
|
32527
33399
|
const resumedPromise = this.pausePromise.then(() => "resumed").catch(() => "resumed");
|
|
32528
33400
|
const winner = await Promise.race([resumedPromise, timeoutPromise]);
|
|
@@ -32615,6 +33487,843 @@ var CollaborationBus = class _CollaborationBus {
|
|
|
32615
33487
|
}
|
|
32616
33488
|
}
|
|
32617
33489
|
};
|
|
33490
|
+
|
|
33491
|
+
// src/coordination/agent-status-helpers.ts
|
|
33492
|
+
var TOOL_TEXT_CAP = 360;
|
|
33493
|
+
var TOUCHED_FILE_LIMIT = 200;
|
|
33494
|
+
var TODO_TEXT_CAP = 360;
|
|
33495
|
+
var TODO_LIMIT = 32;
|
|
33496
|
+
function lineCount(value) {
|
|
33497
|
+
return value.length === 0 ? 0 : value.split(/\r?\n/).length;
|
|
33498
|
+
}
|
|
33499
|
+
function patchDelta(value) {
|
|
33500
|
+
let addedLines = 0;
|
|
33501
|
+
let removedLines = 0;
|
|
33502
|
+
const hunkStart = value.indexOf("@@");
|
|
33503
|
+
if (hunkStart === -1) return { addedLines, removedLines };
|
|
33504
|
+
for (const line of value.slice(hunkStart).split(/\r?\n/)) {
|
|
33505
|
+
if (line.startsWith("+++") || line.startsWith("---")) continue;
|
|
33506
|
+
if (line.startsWith("+")) addedLines += 1;
|
|
33507
|
+
else if (line.startsWith("-")) removedLines += 1;
|
|
33508
|
+
}
|
|
33509
|
+
return { addedLines, removedLines };
|
|
33510
|
+
}
|
|
33511
|
+
function compactText(value) {
|
|
33512
|
+
return value.length <= TOOL_TEXT_CAP ? value : `${value.slice(0, TOOL_TEXT_CAP - 1)}\u2026`;
|
|
33513
|
+
}
|
|
33514
|
+
function boundedText(value, cap) {
|
|
33515
|
+
const trimmed = value.trim();
|
|
33516
|
+
return trimmed.length <= cap ? trimmed : `${trimmed.slice(0, cap - 1)}\u2026`;
|
|
33517
|
+
}
|
|
33518
|
+
function compactTodos(value) {
|
|
33519
|
+
if (!Array.isArray(value)) return void 0;
|
|
33520
|
+
const todos = [];
|
|
33521
|
+
for (const candidate of value) {
|
|
33522
|
+
if (typeof candidate !== "object" || candidate === null) continue;
|
|
33523
|
+
const todo = candidate;
|
|
33524
|
+
const id = typeof todo["id"] === "string" ? todo["id"].trim() : "";
|
|
33525
|
+
const content = typeof todo["content"] === "string" ? boundedText(todo["content"], TODO_TEXT_CAP) : "";
|
|
33526
|
+
const status = todo["status"];
|
|
33527
|
+
if (!id || !content || status !== "pending" && status !== "in_progress" && status !== "completed") {
|
|
33528
|
+
continue;
|
|
33529
|
+
}
|
|
33530
|
+
const activeForm = typeof todo["activeForm"] === "string" ? boundedText(todo["activeForm"], TODO_TEXT_CAP) : void 0;
|
|
33531
|
+
todos.push({ id, content, status, ...activeForm ? { activeForm } : {} });
|
|
33532
|
+
if (todos.length >= TODO_LIMIT) break;
|
|
33533
|
+
}
|
|
33534
|
+
return todos;
|
|
33535
|
+
}
|
|
33536
|
+
function compactToolInput(input) {
|
|
33537
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
33538
|
+
return typeof input === "string" ? { input: compactText(input) } : {};
|
|
33539
|
+
}
|
|
33540
|
+
const source = input;
|
|
33541
|
+
const safe = {};
|
|
33542
|
+
const safeKeys = [
|
|
33543
|
+
"file_path",
|
|
33544
|
+
"filePath",
|
|
33545
|
+
"path",
|
|
33546
|
+
"filename",
|
|
33547
|
+
"target_file",
|
|
33548
|
+
"targetFile",
|
|
33549
|
+
"line",
|
|
33550
|
+
"start_line",
|
|
33551
|
+
"startLine",
|
|
33552
|
+
"end_line",
|
|
33553
|
+
"endLine",
|
|
33554
|
+
"offset",
|
|
33555
|
+
"limit",
|
|
33556
|
+
"command",
|
|
33557
|
+
"cmd",
|
|
33558
|
+
"url",
|
|
33559
|
+
"href",
|
|
33560
|
+
"uri",
|
|
33561
|
+
"query",
|
|
33562
|
+
"pattern"
|
|
33563
|
+
];
|
|
33564
|
+
for (const key of safeKeys) {
|
|
33565
|
+
const value = source[key];
|
|
33566
|
+
if (typeof value === "string") safe[key] = compactText(value);
|
|
33567
|
+
else if (typeof value === "number" || typeof value === "boolean") safe[key] = value;
|
|
33568
|
+
}
|
|
33569
|
+
const content = [source["content"], source["text"], source["data"]].find(
|
|
33570
|
+
(value) => typeof value === "string"
|
|
33571
|
+
);
|
|
33572
|
+
const oldText = [source["old_string"], source["oldString"], source["search"]].find(
|
|
33573
|
+
(value) => typeof value === "string"
|
|
33574
|
+
);
|
|
33575
|
+
const newText = [source["new_string"], source["newString"], source["replacement"]].find(
|
|
33576
|
+
(value) => typeof value === "string"
|
|
33577
|
+
);
|
|
33578
|
+
const patch = [source["patch"], source["diff"]].find(
|
|
33579
|
+
(value) => typeof value === "string"
|
|
33580
|
+
);
|
|
33581
|
+
const delta = patch ? patchDelta(patch) : void 0;
|
|
33582
|
+
return {
|
|
33583
|
+
...Object.keys(safe).length > 0 ? { input: safe } : {},
|
|
33584
|
+
...content !== void 0 ? { inputLines: lineCount(content) } : {},
|
|
33585
|
+
...oldText !== void 0 ? { oldLines: lineCount(oldText) } : {},
|
|
33586
|
+
...newText !== void 0 ? { newLines: lineCount(newText) } : {},
|
|
33587
|
+
...delta && delta.addedLines > 0 ? { addedLines: delta.addedLines } : {},
|
|
33588
|
+
...delta && delta.removedLines > 0 ? { removedLines: delta.removedLines } : {}
|
|
33589
|
+
};
|
|
33590
|
+
}
|
|
33591
|
+
function completedToolReceipt(payload, pending) {
|
|
33592
|
+
const completedAt = Date.now();
|
|
33593
|
+
const durationMs = Math.max(
|
|
33594
|
+
0,
|
|
33595
|
+
payload.durationMs ?? completedAt - (pending?.startedAt ?? completedAt)
|
|
33596
|
+
);
|
|
33597
|
+
const compact = compactToolInput(payload.input ?? pending?.input);
|
|
33598
|
+
return {
|
|
33599
|
+
id: payload.id ?? `${payload.name}:${completedAt}`,
|
|
33600
|
+
name: payload.name,
|
|
33601
|
+
startedAt: pending?.startedAt ?? Math.max(0, completedAt - durationMs),
|
|
33602
|
+
completedAt,
|
|
33603
|
+
durationMs,
|
|
33604
|
+
ok: payload.ok !== false,
|
|
33605
|
+
...compact,
|
|
33606
|
+
...payload.output !== void 0 ? { output: compactText(payload.output) } : {},
|
|
33607
|
+
...payload.outputLines !== void 0 ? { outputLines: payload.outputLines } : {},
|
|
33608
|
+
...payload.outputBytes !== void 0 ? { outputBytes: payload.outputBytes } : {},
|
|
33609
|
+
...payload.outputTokens !== void 0 ? { outputTokens: payload.outputTokens } : {}
|
|
33610
|
+
};
|
|
33611
|
+
}
|
|
33612
|
+
function emptyActivityTotals() {
|
|
33613
|
+
return {
|
|
33614
|
+
filesTouched: [],
|
|
33615
|
+
reads: 0,
|
|
33616
|
+
writes: 0,
|
|
33617
|
+
edits: 0,
|
|
33618
|
+
terminalCalls: 0,
|
|
33619
|
+
webCalls: 0,
|
|
33620
|
+
searches: 0,
|
|
33621
|
+
otherCalls: 0,
|
|
33622
|
+
mailReceived: 0,
|
|
33623
|
+
mailSent: 0,
|
|
33624
|
+
linesRead: 0,
|
|
33625
|
+
linesWritten: 0,
|
|
33626
|
+
linesAdded: 0,
|
|
33627
|
+
linesRemoved: 0
|
|
33628
|
+
};
|
|
33629
|
+
}
|
|
33630
|
+
function addMailTotal(current, direction) {
|
|
33631
|
+
const next = { ...current ?? emptyActivityTotals() };
|
|
33632
|
+
if (direction === "incoming") next.mailReceived += 1;
|
|
33633
|
+
else next.mailSent += 1;
|
|
33634
|
+
return next;
|
|
33635
|
+
}
|
|
33636
|
+
function toolActivityKind(name) {
|
|
33637
|
+
const normalized = name.toLowerCase().replace(/[.:/-]+/g, "_");
|
|
33638
|
+
if (/^(read|view|open_file|read_file)|file_read/.test(normalized)) return "read";
|
|
33639
|
+
if (/^(write|create|save)|file_write/.test(normalized)) return "write";
|
|
33640
|
+
if (/edit|update|patch|replace|apply_patch/.test(normalized)) return "edit";
|
|
33641
|
+
if (/bash|shell|terminal|exec|command|powershell|cmd/.test(normalized)) return "terminal";
|
|
33642
|
+
if (/fetch|browser|browse|http|url|web/.test(normalized)) return "web";
|
|
33643
|
+
if (/search|grep|find|glob|query/.test(normalized)) return "search";
|
|
33644
|
+
return "other";
|
|
33645
|
+
}
|
|
33646
|
+
function receiptFilePath(receipt) {
|
|
33647
|
+
if (!receipt.input || typeof receipt.input !== "object" || Array.isArray(receipt.input)) {
|
|
33648
|
+
return void 0;
|
|
33649
|
+
}
|
|
33650
|
+
const input = receipt.input;
|
|
33651
|
+
for (const key of ["file_path", "filePath", "path", "filename", "target_file", "targetFile"]) {
|
|
33652
|
+
const value = input[key];
|
|
33653
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
33654
|
+
}
|
|
33655
|
+
return void 0;
|
|
33656
|
+
}
|
|
33657
|
+
function addToolActivity(current, receipt) {
|
|
33658
|
+
const next = {
|
|
33659
|
+
...current ?? emptyActivityTotals(),
|
|
33660
|
+
filesTouched: [...current?.filesTouched ?? []]
|
|
33661
|
+
};
|
|
33662
|
+
const kind = toolActivityKind(receipt.name);
|
|
33663
|
+
if (kind === "read") {
|
|
33664
|
+
next.reads += 1;
|
|
33665
|
+
next.linesRead += receipt.outputLines ?? 0;
|
|
33666
|
+
} else if (kind === "write") {
|
|
33667
|
+
next.writes += 1;
|
|
33668
|
+
next.linesWritten += receipt.inputLines ?? 0;
|
|
33669
|
+
} else if (kind === "edit") {
|
|
33670
|
+
next.edits += 1;
|
|
33671
|
+
next.linesAdded += receipt.addedLines ?? receipt.newLines ?? 0;
|
|
33672
|
+
next.linesRemoved += receipt.removedLines ?? receipt.oldLines ?? 0;
|
|
33673
|
+
} else if (kind === "terminal") next.terminalCalls += 1;
|
|
33674
|
+
else if (kind === "web") next.webCalls += 1;
|
|
33675
|
+
else if (kind === "search") next.searches += 1;
|
|
33676
|
+
else next.otherCalls += 1;
|
|
33677
|
+
const filePath = receiptFilePath(receipt);
|
|
33678
|
+
if (filePath && !next.filesTouched.includes(filePath) && next.filesTouched.length < TOUCHED_FILE_LIMIT) {
|
|
33679
|
+
next.filesTouched.push(filePath);
|
|
33680
|
+
}
|
|
33681
|
+
return next;
|
|
33682
|
+
}
|
|
33683
|
+
function clampPct(pct) {
|
|
33684
|
+
if (!Number.isFinite(pct)) return 0;
|
|
33685
|
+
return Math.max(0, Math.min(100, pct));
|
|
33686
|
+
}
|
|
33687
|
+
|
|
33688
|
+
// src/coordination/agent-status-tracker.ts
|
|
33689
|
+
var AGENT_REAP_MS = 3e4;
|
|
33690
|
+
var AGENT_SWEEP_INTERVAL_MS = 1e4;
|
|
33691
|
+
var PENDING_TOOL_TTL_MS = 3e5;
|
|
33692
|
+
var PARTIAL_TEXT_CAP = 1200;
|
|
33693
|
+
var PARTIAL_FLUSH_THROTTLE_MS = 300;
|
|
33694
|
+
var RECENT_TOOL_LIMIT = 12;
|
|
33695
|
+
var RECENT_MAIL_LIMIT = 12;
|
|
33696
|
+
var TASK_TEXT_CAP = 1200;
|
|
33697
|
+
var PROMPT_TEXT_CAP = 6e3;
|
|
33698
|
+
var AgentStatusTracker = class _AgentStatusTracker {
|
|
33699
|
+
events;
|
|
33700
|
+
registry;
|
|
33701
|
+
sessionId;
|
|
33702
|
+
leaderName;
|
|
33703
|
+
// Live agent map: agentId → AgentEntry
|
|
33704
|
+
agents = /* @__PURE__ */ new Map();
|
|
33705
|
+
// Last full agent list flushed (leader + subagents). Lets external consumers
|
|
33706
|
+
// read the current state synchronously without re-deriving it.
|
|
33707
|
+
lastAgents = [];
|
|
33708
|
+
// Leader tracking
|
|
33709
|
+
leaderStatus = "idle";
|
|
33710
|
+
leaderCurrentTool;
|
|
33711
|
+
leaderCurrentTask;
|
|
33712
|
+
leaderIterations = 0;
|
|
33713
|
+
leaderToolCalls = 0;
|
|
33714
|
+
leaderCostUsd = 0;
|
|
33715
|
+
leaderTokensIn = 0;
|
|
33716
|
+
leaderTokensOut = 0;
|
|
33717
|
+
leaderCtxPct;
|
|
33718
|
+
leaderModel;
|
|
33719
|
+
leaderPartialText = "";
|
|
33720
|
+
leaderStartedAt;
|
|
33721
|
+
leaderRecentTools = [];
|
|
33722
|
+
leaderRecentMail = [];
|
|
33723
|
+
leaderTodos = [];
|
|
33724
|
+
leaderLatestPrompt;
|
|
33725
|
+
leaderLatestPromptAt;
|
|
33726
|
+
leaderActivity = emptyActivityTotals();
|
|
33727
|
+
leaderPendingTools = /* @__PURE__ */ new Map();
|
|
33728
|
+
subagentPendingTools = /* @__PURE__ */ new Map();
|
|
33729
|
+
seenIncomingMail = /* @__PURE__ */ new Set();
|
|
33730
|
+
seenOutgoingMail = /* @__PURE__ */ new Set();
|
|
33731
|
+
static SEEN_MAIL_LIMIT = 1e4;
|
|
33732
|
+
rememberMailId(seen, messageId) {
|
|
33733
|
+
if (seen.has(messageId)) return false;
|
|
33734
|
+
seen.add(messageId);
|
|
33735
|
+
if (seen.size > _AgentStatusTracker.SEEN_MAIL_LIMIT) {
|
|
33736
|
+
const oldest = seen.values().next().value;
|
|
33737
|
+
if (oldest !== void 0) seen.delete(oldest);
|
|
33738
|
+
}
|
|
33739
|
+
return true;
|
|
33740
|
+
}
|
|
33741
|
+
unsubscribers = [];
|
|
33742
|
+
onUpdate;
|
|
33743
|
+
sweepTimer = null;
|
|
33744
|
+
partialTimer = null;
|
|
33745
|
+
/** Serialize registry writes so concurrent flush() calls don't race. */
|
|
33746
|
+
flushPromise = null;
|
|
33747
|
+
constructor(opts) {
|
|
33748
|
+
this.events = opts.events;
|
|
33749
|
+
this.registry = opts.registry;
|
|
33750
|
+
this.sessionId = opts.sessionId;
|
|
33751
|
+
this.leaderName = opts.leaderName ?? "leader";
|
|
33752
|
+
this.onUpdate = opts.onUpdate;
|
|
33753
|
+
}
|
|
33754
|
+
/** Current full agent list (leader + subagents) as of the last flush. */
|
|
33755
|
+
getAgents() {
|
|
33756
|
+
return this.lastAgents.length > 0 ? [...this.lastAgents] : [];
|
|
33757
|
+
}
|
|
33758
|
+
start() {
|
|
33759
|
+
this.stop();
|
|
33760
|
+
const on = (pattern, fn) => this.events.onPattern(pattern, (event, payload) => {
|
|
33761
|
+
if (!this.acceptsSession(payload)) return;
|
|
33762
|
+
fn(event, payload);
|
|
33763
|
+
});
|
|
33764
|
+
this.unsubscribers.push(
|
|
33765
|
+
on("agent.run.started", (_event, payload) => {
|
|
33766
|
+
const p = payload;
|
|
33767
|
+
this.markLeaderStarted(p?.at);
|
|
33768
|
+
this.captureLeaderContext(p?.ctx);
|
|
33769
|
+
if (p?.model) this.leaderModel = p.model;
|
|
33770
|
+
if (p?.inputText?.trim()) {
|
|
33771
|
+
const prompt = boundedText(p.inputText, PROMPT_TEXT_CAP);
|
|
33772
|
+
this.leaderLatestPrompt = prompt;
|
|
33773
|
+
this.leaderLatestPromptAt = p.at ? Date.parse(p.at) : Date.now();
|
|
33774
|
+
if (!Number.isFinite(this.leaderLatestPromptAt)) this.leaderLatestPromptAt = Date.now();
|
|
33775
|
+
this.leaderCurrentTask = boundedText(p.inputText, TASK_TEXT_CAP);
|
|
33776
|
+
}
|
|
33777
|
+
this.leaderStatus = "running";
|
|
33778
|
+
this.leaderIterations++;
|
|
33779
|
+
this.flush();
|
|
33780
|
+
})
|
|
33781
|
+
);
|
|
33782
|
+
this.unsubscribers.push(
|
|
33783
|
+
on("iteration.started", (_e, payload) => {
|
|
33784
|
+
const p = payload;
|
|
33785
|
+
const ctx = p?.ctx;
|
|
33786
|
+
this.markLeaderStarted();
|
|
33787
|
+
this.leaderStatus = "running";
|
|
33788
|
+
if (typeof p?.index === "number") {
|
|
33789
|
+
this.leaderIterations = Math.max(this.leaderIterations, p.index + 1);
|
|
33790
|
+
}
|
|
33791
|
+
if (!ctx) {
|
|
33792
|
+
this.flush();
|
|
33793
|
+
return;
|
|
33794
|
+
}
|
|
33795
|
+
this.captureLeaderContext(ctx);
|
|
33796
|
+
this.flush();
|
|
33797
|
+
})
|
|
33798
|
+
);
|
|
33799
|
+
this.unsubscribers.push(
|
|
33800
|
+
on("agent.run.completed", (_event, payload) => {
|
|
33801
|
+
const p = payload;
|
|
33802
|
+
this.captureLeaderContext(p?.ctx);
|
|
33803
|
+
this.leaderStatus = p?.status === "failed" ? "error" : "idle";
|
|
33804
|
+
this.leaderCurrentTool = void 0;
|
|
33805
|
+
this.leaderCurrentTask = void 0;
|
|
33806
|
+
this.leaderPartialText = "";
|
|
33807
|
+
if (this.leaderStatus === "idle") this.leaderStartedAt = void 0;
|
|
33808
|
+
this.flush();
|
|
33809
|
+
})
|
|
33810
|
+
);
|
|
33811
|
+
this.unsubscribers.push(
|
|
33812
|
+
on("agent.run.error", (_event, payload) => {
|
|
33813
|
+
const p = payload;
|
|
33814
|
+
this.captureLeaderContext(p?.ctx);
|
|
33815
|
+
this.leaderStatus = "error";
|
|
33816
|
+
this.leaderCurrentTool = void 0;
|
|
33817
|
+
this.leaderCurrentTask = void 0;
|
|
33818
|
+
this.leaderPartialText = "";
|
|
33819
|
+
this.flush();
|
|
33820
|
+
})
|
|
33821
|
+
);
|
|
33822
|
+
this.unsubscribers.push(
|
|
33823
|
+
on("iteration.completed", (_event, payload) => {
|
|
33824
|
+
const p = payload;
|
|
33825
|
+
this.captureLeaderContext(p?.ctx);
|
|
33826
|
+
this.flush();
|
|
33827
|
+
})
|
|
33828
|
+
);
|
|
33829
|
+
this.unsubscribers.push(
|
|
33830
|
+
on("tool.started", (_event, payload) => {
|
|
33831
|
+
const p = payload;
|
|
33832
|
+
if (p?.name) {
|
|
33833
|
+
this.markLeaderStarted();
|
|
33834
|
+
this.leaderCurrentTool = p.name;
|
|
33835
|
+
this.leaderToolCalls++;
|
|
33836
|
+
this.leaderPendingTools.set(p.id ?? p.name, {
|
|
33837
|
+
name: p.name,
|
|
33838
|
+
input: p.input,
|
|
33839
|
+
startedAt: Date.now()
|
|
33840
|
+
});
|
|
33841
|
+
}
|
|
33842
|
+
this.leaderStatus = "running";
|
|
33843
|
+
this.flush();
|
|
33844
|
+
})
|
|
33845
|
+
);
|
|
33846
|
+
this.unsubscribers.push(
|
|
33847
|
+
on("tool.executed", (_event, payload) => {
|
|
33848
|
+
const p = payload;
|
|
33849
|
+
if (p?.name) {
|
|
33850
|
+
const key = p.id ?? p.name;
|
|
33851
|
+
const receipt = completedToolReceipt(p, this.leaderPendingTools.get(key));
|
|
33852
|
+
this.leaderRecentTools = [receipt, ...this.leaderRecentTools].slice(0, RECENT_TOOL_LIMIT);
|
|
33853
|
+
this.leaderActivity = addToolActivity(this.leaderActivity, receipt);
|
|
33854
|
+
this.leaderPendingTools.delete(key);
|
|
33855
|
+
}
|
|
33856
|
+
this.leaderCurrentTool = void 0;
|
|
33857
|
+
this.flush();
|
|
33858
|
+
})
|
|
33859
|
+
);
|
|
33860
|
+
this.unsubscribers.push(
|
|
33861
|
+
on("brain.ask_human", () => {
|
|
33862
|
+
this.markLeaderStarted();
|
|
33863
|
+
this.leaderStatus = "waiting_user";
|
|
33864
|
+
this.flush();
|
|
33865
|
+
})
|
|
33866
|
+
);
|
|
33867
|
+
const recordMail = (direction, payload) => {
|
|
33868
|
+
const p = payload;
|
|
33869
|
+
if (!p?.messageId) return;
|
|
33870
|
+
const seen = direction === "incoming" ? this.seenIncomingMail : this.seenOutgoingMail;
|
|
33871
|
+
if (!this.rememberMailId(seen, p.messageId)) return;
|
|
33872
|
+
const receipt = {
|
|
33873
|
+
id: p.messageId,
|
|
33874
|
+
direction,
|
|
33875
|
+
from: p.from ?? "?",
|
|
33876
|
+
to: p.to ?? (direction === "incoming" ? "leader" : "?"),
|
|
33877
|
+
type: p.type ?? "note",
|
|
33878
|
+
subject: p.subject ?? "Message",
|
|
33879
|
+
at: Date.now()
|
|
33880
|
+
};
|
|
33881
|
+
const directAgentId = direction === "outgoing" ? p.from : p.to;
|
|
33882
|
+
const entry = directAgentId ? this.agents.get(directAgentId) : void 0;
|
|
33883
|
+
if (entry) {
|
|
33884
|
+
entry.recentMail = [receipt, ...entry.recentMail ?? []].slice(0, RECENT_MAIL_LIMIT);
|
|
33885
|
+
entry.activity = addMailTotal(entry.activity, direction);
|
|
33886
|
+
} else {
|
|
33887
|
+
this.leaderRecentMail = [receipt, ...this.leaderRecentMail].slice(0, RECENT_MAIL_LIMIT);
|
|
33888
|
+
this.leaderActivity = addMailTotal(this.leaderActivity, direction);
|
|
33889
|
+
}
|
|
33890
|
+
this.flush();
|
|
33891
|
+
};
|
|
33892
|
+
this.unsubscribers.push(
|
|
33893
|
+
on("mailbox.message_sent", (_event, payload) => recordMail("outgoing", payload)),
|
|
33894
|
+
on("mailbox.received", (_event, payload) => recordMail("incoming", payload))
|
|
33895
|
+
);
|
|
33896
|
+
this.unsubscribers.push(
|
|
33897
|
+
on("llm.stream_started", () => {
|
|
33898
|
+
this.markLeaderStarted();
|
|
33899
|
+
this.leaderStatus = "streaming";
|
|
33900
|
+
this.leaderPartialText = "";
|
|
33901
|
+
this.flush();
|
|
33902
|
+
})
|
|
33903
|
+
);
|
|
33904
|
+
this.unsubscribers.push(
|
|
33905
|
+
on("provider.text_delta", (_e, payload) => {
|
|
33906
|
+
const p = payload;
|
|
33907
|
+
const text = p?.text;
|
|
33908
|
+
if (!text) return;
|
|
33909
|
+
this.markLeaderStarted();
|
|
33910
|
+
this.captureLeaderContext(p?.ctx);
|
|
33911
|
+
this.leaderStatus = "streaming";
|
|
33912
|
+
const next = this.leaderPartialText + text;
|
|
33913
|
+
this.leaderPartialText = next.length > PARTIAL_TEXT_CAP ? next.slice(next.length - PARTIAL_TEXT_CAP) : next;
|
|
33914
|
+
this.schedulePartialFlush();
|
|
33915
|
+
})
|
|
33916
|
+
);
|
|
33917
|
+
this.unsubscribers.push(
|
|
33918
|
+
on("provider.response", (_e, payload) => {
|
|
33919
|
+
const p = payload;
|
|
33920
|
+
this.captureLeaderContext(p?.ctx);
|
|
33921
|
+
this.flush();
|
|
33922
|
+
})
|
|
33923
|
+
);
|
|
33924
|
+
this.unsubscribers.push(
|
|
33925
|
+
on("provider.fallback", (_e, payload) => {
|
|
33926
|
+
const p = payload;
|
|
33927
|
+
if (p?.to?.model) {
|
|
33928
|
+
this.leaderModel = p.to.providerId ? `${p.to.providerId}/${p.to.model}` : p.to.model;
|
|
33929
|
+
this.flush();
|
|
33930
|
+
}
|
|
33931
|
+
})
|
|
33932
|
+
);
|
|
33933
|
+
this.unsubscribers.push(
|
|
33934
|
+
on("ctx.pct", (_e, payload) => {
|
|
33935
|
+
const p = payload;
|
|
33936
|
+
if (typeof p?.load === "number" && Number.isFinite(p.load)) {
|
|
33937
|
+
this.leaderCtxPct = clampPct(Math.round(p.load * 100));
|
|
33938
|
+
this.flush();
|
|
33939
|
+
}
|
|
33940
|
+
})
|
|
33941
|
+
);
|
|
33942
|
+
this.unsubscribers.push(
|
|
33943
|
+
on("token.accounted", (_e, payload) => {
|
|
33944
|
+
const p = payload;
|
|
33945
|
+
if (!p) return;
|
|
33946
|
+
this.leaderTokensIn += p.usage?.input ?? 0;
|
|
33947
|
+
this.leaderTokensOut += p.usage?.output ?? 0;
|
|
33948
|
+
this.leaderCostUsd += p.cost?.total ?? 0;
|
|
33949
|
+
this.flush();
|
|
33950
|
+
})
|
|
33951
|
+
);
|
|
33952
|
+
const touch = (id) => {
|
|
33953
|
+
let entry = this.agents.get(id);
|
|
33954
|
+
if (!entry) {
|
|
33955
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
33956
|
+
entry = {
|
|
33957
|
+
id,
|
|
33958
|
+
name: id,
|
|
33959
|
+
status: "idle",
|
|
33960
|
+
iterations: 0,
|
|
33961
|
+
toolCalls: 0,
|
|
33962
|
+
startedAt: now,
|
|
33963
|
+
lastActivityAt: now
|
|
33964
|
+
};
|
|
33965
|
+
this.agents.set(id, entry);
|
|
33966
|
+
}
|
|
33967
|
+
entry.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
33968
|
+
return entry;
|
|
33969
|
+
};
|
|
33970
|
+
this.unsubscribers.push(
|
|
33971
|
+
on("subagent.spawned", (_e, payload) => {
|
|
33972
|
+
const p = payload;
|
|
33973
|
+
if (!p?.subagentId) return;
|
|
33974
|
+
const entry = touch(p.subagentId);
|
|
33975
|
+
entry.name = p.name?.trim() || entry.name;
|
|
33976
|
+
if (p.model) entry.model = p.model;
|
|
33977
|
+
if (p.taskId) entry.taskId = p.taskId;
|
|
33978
|
+
if (p.description?.trim()) entry.currentTask = boundedText(p.description, TASK_TEXT_CAP);
|
|
33979
|
+
if (!entry.startedAt) entry.startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
33980
|
+
entry.status = "running";
|
|
33981
|
+
this.flush();
|
|
33982
|
+
})
|
|
33983
|
+
);
|
|
33984
|
+
this.unsubscribers.push(
|
|
33985
|
+
on("subagent.ctx_pct", (_e, payload) => {
|
|
33986
|
+
const p = payload;
|
|
33987
|
+
if (!p?.subagentId) return;
|
|
33988
|
+
const entry = touch(p.subagentId);
|
|
33989
|
+
if (typeof p.load === "number") entry.ctxPct = clampPct(Math.round(p.load * 100));
|
|
33990
|
+
this.flush();
|
|
33991
|
+
})
|
|
33992
|
+
);
|
|
33993
|
+
this.unsubscribers.push(
|
|
33994
|
+
on("subagent.task_started", (_e, payload) => {
|
|
33995
|
+
const p = payload;
|
|
33996
|
+
if (!p?.subagentId) return;
|
|
33997
|
+
const entry = touch(p.subagentId);
|
|
33998
|
+
entry.status = "running";
|
|
33999
|
+
if (p.taskId) entry.taskId = p.taskId;
|
|
34000
|
+
if (p.description?.trim()) entry.currentTask = boundedText(p.description, TASK_TEXT_CAP);
|
|
34001
|
+
if (!entry.startedAt) entry.startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
34002
|
+
entry.iterations++;
|
|
34003
|
+
this.flush();
|
|
34004
|
+
})
|
|
34005
|
+
);
|
|
34006
|
+
this.unsubscribers.push(
|
|
34007
|
+
on("subagent.tool_started", (_e, payload) => {
|
|
34008
|
+
const p = payload;
|
|
34009
|
+
if (!p?.subagentId || !p.name) return;
|
|
34010
|
+
const entry = touch(p.subagentId);
|
|
34011
|
+
entry.status = "running";
|
|
34012
|
+
entry.currentTool = p.name;
|
|
34013
|
+
this.subagentPendingTools.set(`${p.subagentId}:${p.id ?? p.name}`, {
|
|
34014
|
+
name: p.name,
|
|
34015
|
+
input: p.input,
|
|
34016
|
+
startedAt: Date.now()
|
|
34017
|
+
});
|
|
34018
|
+
this.flush();
|
|
34019
|
+
})
|
|
34020
|
+
);
|
|
34021
|
+
this.unsubscribers.push(
|
|
34022
|
+
on("subagent.tool_executed", (_e, payload) => {
|
|
34023
|
+
const p = payload;
|
|
34024
|
+
if (!p?.subagentId || !p.name) return;
|
|
34025
|
+
const entry = touch(p.subagentId);
|
|
34026
|
+
entry.status = "running";
|
|
34027
|
+
if (!entry.startedAt) entry.startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
34028
|
+
const key = `${p.subagentId}:${p.id ?? p.name}`;
|
|
34029
|
+
const receipt = completedToolReceipt(p, this.subagentPendingTools.get(key));
|
|
34030
|
+
entry.recentTools = [receipt, ...entry.recentTools ?? []].slice(0, RECENT_TOOL_LIMIT);
|
|
34031
|
+
entry.activity = addToolActivity(entry.activity, receipt);
|
|
34032
|
+
this.subagentPendingTools.delete(key);
|
|
34033
|
+
entry.currentTool = void 0;
|
|
34034
|
+
entry.toolCalls++;
|
|
34035
|
+
this.flush();
|
|
34036
|
+
})
|
|
34037
|
+
);
|
|
34038
|
+
this.unsubscribers.push(
|
|
34039
|
+
on("subagent.iteration_summary", (_e, payload) => {
|
|
34040
|
+
const p = payload;
|
|
34041
|
+
if (!p?.subagentId) return;
|
|
34042
|
+
const entry = touch(p.subagentId);
|
|
34043
|
+
entry.status = "running";
|
|
34044
|
+
if (!entry.startedAt) entry.startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
34045
|
+
if (typeof p.iteration === "number") entry.iterations = p.iteration;
|
|
34046
|
+
if (typeof p.toolCalls === "number") entry.toolCalls = p.toolCalls;
|
|
34047
|
+
if (typeof p.costUsd === "number") entry.costUsd = p.costUsd;
|
|
34048
|
+
if (p.currentTool) entry.currentTool = p.currentTool;
|
|
34049
|
+
if (typeof p.partialText === "string") {
|
|
34050
|
+
entry.partialText = p.partialText.length > PARTIAL_TEXT_CAP ? p.partialText.slice(p.partialText.length - PARTIAL_TEXT_CAP) : p.partialText;
|
|
34051
|
+
}
|
|
34052
|
+
this.flush();
|
|
34053
|
+
})
|
|
34054
|
+
);
|
|
34055
|
+
this.unsubscribers.push(
|
|
34056
|
+
on("subagent.task_completed", (_e, payload) => {
|
|
34057
|
+
const p = payload;
|
|
34058
|
+
if (!p?.subagentId) return;
|
|
34059
|
+
const entry = this.agents.get(p.subagentId);
|
|
34060
|
+
if (!entry) return;
|
|
34061
|
+
entry.status = p.status === "failed" || p.status === "timeout" ? "error" : "idle";
|
|
34062
|
+
entry.currentTool = void 0;
|
|
34063
|
+
entry.currentTask = void 0;
|
|
34064
|
+
entry.taskId = void 0;
|
|
34065
|
+
entry.partialText = void 0;
|
|
34066
|
+
if (typeof p.iterations === "number") entry.iterations = p.iterations;
|
|
34067
|
+
if (typeof p.toolCalls === "number") entry.toolCalls = p.toolCalls;
|
|
34068
|
+
entry.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
34069
|
+
this.flush();
|
|
34070
|
+
})
|
|
34071
|
+
);
|
|
34072
|
+
this.unsubscribers.push(
|
|
34073
|
+
on("subagent.stopped", (_e, payload) => {
|
|
34074
|
+
const p = payload;
|
|
34075
|
+
if (!p?.subagentId) return;
|
|
34076
|
+
if (this.agents.delete(p.subagentId)) this.flush();
|
|
34077
|
+
})
|
|
34078
|
+
);
|
|
34079
|
+
this.unsubscribers.push(
|
|
34080
|
+
on("subagent.removed", (_e, payload) => {
|
|
34081
|
+
const p = payload;
|
|
34082
|
+
if (!p?.subagentId) return;
|
|
34083
|
+
if (this.agents.delete(p.subagentId)) this.flush();
|
|
34084
|
+
})
|
|
34085
|
+
);
|
|
34086
|
+
this.sweepTimer = setInterval(() => this.sweep(), AGENT_SWEEP_INTERVAL_MS);
|
|
34087
|
+
if (typeof this.sweepTimer.unref === "function") this.sweepTimer.unref();
|
|
34088
|
+
}
|
|
34089
|
+
stop() {
|
|
34090
|
+
for (const unsub of this.unsubscribers) {
|
|
34091
|
+
try {
|
|
34092
|
+
unsub();
|
|
34093
|
+
} catch {
|
|
34094
|
+
}
|
|
34095
|
+
}
|
|
34096
|
+
this.unsubscribers = [];
|
|
34097
|
+
if (this.sweepTimer) {
|
|
34098
|
+
clearInterval(this.sweepTimer);
|
|
34099
|
+
this.sweepTimer = null;
|
|
34100
|
+
}
|
|
34101
|
+
if (this.partialTimer) {
|
|
34102
|
+
clearTimeout(this.partialTimer);
|
|
34103
|
+
this.partialTimer = null;
|
|
34104
|
+
}
|
|
34105
|
+
this.leaderPendingTools.clear();
|
|
34106
|
+
this.subagentPendingTools.clear();
|
|
34107
|
+
}
|
|
34108
|
+
/**
|
|
34109
|
+
* Coalesce streamed-text flushes: at most one registry write per
|
|
34110
|
+
* {@link PARTIAL_FLUSH_THROTTLE_MS} while text streams in, so per-token
|
|
34111
|
+
* deltas never thrash the cross-process registry file.
|
|
34112
|
+
*/
|
|
34113
|
+
schedulePartialFlush() {
|
|
34114
|
+
if (this.partialTimer) return;
|
|
34115
|
+
this.partialTimer = setTimeout(() => {
|
|
34116
|
+
this.partialTimer = null;
|
|
34117
|
+
this.flush();
|
|
34118
|
+
}, PARTIAL_FLUSH_THROTTLE_MS);
|
|
34119
|
+
if (typeof this.partialTimer.unref === "function") this.partialTimer.unref();
|
|
34120
|
+
}
|
|
34121
|
+
/**
|
|
34122
|
+
* Remove subagents that have been finished (idle/error) for longer than
|
|
34123
|
+
* {@link AGENT_REAP_MS}. Running / streaming / waiting_user agents are kept
|
|
34124
|
+
* regardless of age — only *not-working* agents are reaped.
|
|
34125
|
+
*
|
|
34126
|
+
* Also trims orphaned PendingTool entries older than {@link PENDING_TOOL_TTL_MS}
|
|
34127
|
+
* from both leader and subagent pending-tools Maps. Piggybacks on the same
|
|
34128
|
+
* 10-second interval to avoid a second timer.
|
|
34129
|
+
*/
|
|
34130
|
+
sweep() {
|
|
34131
|
+
const now = Date.now();
|
|
34132
|
+
let removed = false;
|
|
34133
|
+
for (const [id, a] of this.agents) {
|
|
34134
|
+
const finished = a.status !== "running" && a.status !== "streaming" && a.status !== "waiting_user";
|
|
34135
|
+
const age = now - Date.parse(a.lastActivityAt);
|
|
34136
|
+
if (finished && Number.isFinite(age) && age > AGENT_REAP_MS) {
|
|
34137
|
+
this.agents.delete(id);
|
|
34138
|
+
removed = true;
|
|
34139
|
+
}
|
|
34140
|
+
}
|
|
34141
|
+
this.trimPendingTools(this.leaderPendingTools, now);
|
|
34142
|
+
this.trimPendingTools(this.subagentPendingTools, now);
|
|
34143
|
+
if (removed) this.flush();
|
|
34144
|
+
}
|
|
34145
|
+
/**
|
|
34146
|
+
* Evict pending-tool entries whose `startedAt` is older than
|
|
34147
|
+
* {@link PENDING_TOOL_TTL_MS}. These are tool.started / subagent.tool_started
|
|
34148
|
+
* events whose matching tool.executed / subagent.tool_executed never arrived
|
|
34149
|
+
* (provider crash, abort, process kill). Without periodic cleanup the
|
|
34150
|
+
* Maps retain the full tool-input objects for the process lifetime.
|
|
34151
|
+
*/
|
|
34152
|
+
trimPendingTools(map, now) {
|
|
34153
|
+
const cutoff = now - PENDING_TOOL_TTL_MS;
|
|
34154
|
+
for (const [key, entry] of map) {
|
|
34155
|
+
if (entry.startedAt < cutoff) map.delete(key);
|
|
34156
|
+
}
|
|
34157
|
+
}
|
|
34158
|
+
flush() {
|
|
34159
|
+
const leaderEntry = {
|
|
34160
|
+
id: "leader",
|
|
34161
|
+
name: this.leaderName,
|
|
34162
|
+
startedAt: this.leaderStartedAt,
|
|
34163
|
+
status: this.leaderStatus,
|
|
34164
|
+
currentTool: this.leaderCurrentTool,
|
|
34165
|
+
currentTask: this.leaderCurrentTask,
|
|
34166
|
+
iterations: this.leaderIterations,
|
|
34167
|
+
toolCalls: this.leaderToolCalls,
|
|
34168
|
+
costUsd: this.leaderCostUsd,
|
|
34169
|
+
tokensIn: this.leaderTokensIn,
|
|
34170
|
+
tokensOut: this.leaderTokensOut,
|
|
34171
|
+
ctxPct: this.leaderCtxPct,
|
|
34172
|
+
model: this.leaderModel,
|
|
34173
|
+
partialText: this.leaderPartialText || void 0,
|
|
34174
|
+
recentTools: this.leaderRecentTools,
|
|
34175
|
+
recentMail: this.leaderRecentMail,
|
|
34176
|
+
todos: this.leaderTodos,
|
|
34177
|
+
latestPrompt: this.leaderLatestPrompt,
|
|
34178
|
+
latestPromptAt: this.leaderLatestPromptAt,
|
|
34179
|
+
activity: this.leaderActivity,
|
|
34180
|
+
lastActivityAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
34181
|
+
};
|
|
34182
|
+
const allAgents = [leaderEntry, ...this.agents.values()];
|
|
34183
|
+
this.lastAgents = allAgents;
|
|
34184
|
+
try {
|
|
34185
|
+
this.events.emit("session.agents_updated", {
|
|
34186
|
+
sessionId: this.currentSessionId(),
|
|
34187
|
+
agents: allAgents
|
|
34188
|
+
});
|
|
34189
|
+
} catch {
|
|
34190
|
+
}
|
|
34191
|
+
const write = this.registry.updateAgents(allAgents);
|
|
34192
|
+
const chain = write.then(() => {
|
|
34193
|
+
try {
|
|
34194
|
+
this.onUpdate?.();
|
|
34195
|
+
} catch {
|
|
34196
|
+
}
|
|
34197
|
+
});
|
|
34198
|
+
if (this.flushPromise) {
|
|
34199
|
+
this.flushPromise = this.flushPromise.then(
|
|
34200
|
+
() => chain,
|
|
34201
|
+
() => chain
|
|
34202
|
+
).catch(() => void 0);
|
|
34203
|
+
} else {
|
|
34204
|
+
this.flushPromise = chain.catch(() => void 0);
|
|
34205
|
+
}
|
|
34206
|
+
}
|
|
34207
|
+
currentSessionId() {
|
|
34208
|
+
return typeof this.sessionId === "function" ? this.sessionId() : this.sessionId;
|
|
34209
|
+
}
|
|
34210
|
+
acceptsSession(payload) {
|
|
34211
|
+
const expected = this.currentSessionId();
|
|
34212
|
+
if (!expected) return true;
|
|
34213
|
+
if (typeof payload !== "object" || payload === null) return true;
|
|
34214
|
+
const actual = payload.sessionId;
|
|
34215
|
+
return typeof actual !== "string" || actual.length === 0 || actual === expected;
|
|
34216
|
+
}
|
|
34217
|
+
markLeaderStarted(startedAt) {
|
|
34218
|
+
if (this.leaderStartedAt && (this.leaderStatus === "running" || this.leaderStatus === "streaming" || this.leaderStatus === "waiting_user")) {
|
|
34219
|
+
return;
|
|
34220
|
+
}
|
|
34221
|
+
this.leaderStartedAt = startedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
34222
|
+
}
|
|
34223
|
+
captureLeaderContext(ctx) {
|
|
34224
|
+
if (typeof ctx !== "object" || ctx === null) return;
|
|
34225
|
+
const c = ctx;
|
|
34226
|
+
if (typeof c.model === "string" && c.model.length > 0) this.leaderModel = c.model;
|
|
34227
|
+
const todos = compactTodos(c.todos);
|
|
34228
|
+
if (todos !== void 0) this.leaderTodos = todos;
|
|
34229
|
+
const metaLimit = c.meta?.["effectiveMaxContext"];
|
|
34230
|
+
const providerMax = c.provider?.capabilities?.maxContext;
|
|
34231
|
+
const maxContext = typeof metaLimit === "number" && metaLimit > 0 ? metaLimit : typeof providerMax === "number" && providerMax > 0 ? providerMax : void 0;
|
|
34232
|
+
if (typeof c.lastRequestTokens === "number" && c.lastRequestTokens > 0 && maxContext !== void 0) {
|
|
34233
|
+
this.leaderCtxPct = clampPct(Math.round(c.lastRequestTokens / maxContext * 100));
|
|
34234
|
+
}
|
|
34235
|
+
}
|
|
34236
|
+
};
|
|
34237
|
+
|
|
34238
|
+
// src/coordination/fleet-notifier.ts
|
|
34239
|
+
import * as fs8 from "node:fs/promises";
|
|
34240
|
+
import * as path43 from "node:path";
|
|
34241
|
+
var INSTANCES_FILE = "webui-instances.json";
|
|
34242
|
+
var DISCOVERY_TTL_MS = 2500;
|
|
34243
|
+
var COALESCE_MS = 50;
|
|
34244
|
+
var POST_TIMEOUT_MS = 500;
|
|
34245
|
+
var pidAlive = isPidAlive;
|
|
34246
|
+
function normRoot(root) {
|
|
34247
|
+
const resolved = path43.resolve(root);
|
|
34248
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
34249
|
+
}
|
|
34250
|
+
var FleetNotifier = class {
|
|
34251
|
+
baseDir;
|
|
34252
|
+
projectRoot;
|
|
34253
|
+
selfPid;
|
|
34254
|
+
doPost;
|
|
34255
|
+
cache = null;
|
|
34256
|
+
timer = null;
|
|
34257
|
+
disposed = false;
|
|
34258
|
+
constructor(opts) {
|
|
34259
|
+
this.baseDir = opts.baseDir;
|
|
34260
|
+
this.projectRoot = normRoot(opts.projectRoot);
|
|
34261
|
+
this.selfPid = opts.selfPid ?? process.pid;
|
|
34262
|
+
this.doPost = opts.post ?? defaultPost;
|
|
34263
|
+
}
|
|
34264
|
+
/** Coalesced, best-effort nudge. Safe to call on every status change. */
|
|
34265
|
+
notify() {
|
|
34266
|
+
if (this.disposed || this.timer) return;
|
|
34267
|
+
this.timer = setTimeout(() => {
|
|
34268
|
+
this.timer = null;
|
|
34269
|
+
void this.flush();
|
|
34270
|
+
}, COALESCE_MS);
|
|
34271
|
+
if (typeof this.timer.unref === "function") this.timer.unref();
|
|
34272
|
+
}
|
|
34273
|
+
/** Resolve same-project WebUI ping URLs (cached briefly). Exposed for tests. */
|
|
34274
|
+
async endpoints() {
|
|
34275
|
+
return (await this.targets()).map((t) => t.url);
|
|
34276
|
+
}
|
|
34277
|
+
/** Ping targets with their tokens (cached briefly). */
|
|
34278
|
+
async targets() {
|
|
34279
|
+
const now = Date.now();
|
|
34280
|
+
if (this.cache && now - this.cache.at < DISCOVERY_TTL_MS) return this.cache.targets;
|
|
34281
|
+
const targets = await this.discover();
|
|
34282
|
+
this.cache = { at: now, targets };
|
|
34283
|
+
return targets;
|
|
34284
|
+
}
|
|
34285
|
+
dispose() {
|
|
34286
|
+
this.disposed = true;
|
|
34287
|
+
if (this.timer) {
|
|
34288
|
+
clearTimeout(this.timer);
|
|
34289
|
+
this.timer = null;
|
|
34290
|
+
}
|
|
34291
|
+
}
|
|
34292
|
+
/** Re-scope notifications after an in-process project switch. */
|
|
34293
|
+
setProjectRoot(projectRoot) {
|
|
34294
|
+
this.projectRoot = normRoot(projectRoot);
|
|
34295
|
+
this.cache = null;
|
|
34296
|
+
}
|
|
34297
|
+
async flush() {
|
|
34298
|
+
const targets = await this.targets();
|
|
34299
|
+
await Promise.all(targets.map((t) => this.doPost(t.url, t.token).catch(() => void 0)));
|
|
34300
|
+
}
|
|
34301
|
+
async discover() {
|
|
34302
|
+
try {
|
|
34303
|
+
const raw = await fs8.readFile(path43.join(this.baseDir, INSTANCES_FILE), "utf8");
|
|
34304
|
+
const data = JSON.parse(raw);
|
|
34305
|
+
const list = Array.isArray(data?.instances) ? data.instances : [];
|
|
34306
|
+
return list.filter((i) => i && typeof i.httpPort === "number").filter((i) => i.pid !== this.selfPid).filter((i) => normRoot(i.projectRoot) === this.projectRoot).filter((i) => pidAlive(i.pid)).map((i) => {
|
|
34307
|
+
const host = i.host === "0.0.0.0" || i.host === "::" || !i.host ? "127.0.0.1" : i.host;
|
|
34308
|
+
return {
|
|
34309
|
+
url: `http://${host}:${i.httpPort}/api/fleet/ping`,
|
|
34310
|
+
token: typeof i.authToken === "string" ? i.authToken : void 0
|
|
34311
|
+
};
|
|
34312
|
+
});
|
|
34313
|
+
} catch {
|
|
34314
|
+
return [];
|
|
34315
|
+
}
|
|
34316
|
+
}
|
|
34317
|
+
};
|
|
34318
|
+
async function defaultPost(url, token) {
|
|
34319
|
+
await fetch(url, {
|
|
34320
|
+
method: "POST",
|
|
34321
|
+
// The API requires a token on every bind (H3). Sent as a header rather
|
|
34322
|
+
// than a query param so it never lands in an access log or the URL.
|
|
34323
|
+
...token ? { headers: { "x-ws-token": token } } : {},
|
|
34324
|
+
signal: AbortSignal.timeout(POST_TIMEOUT_MS)
|
|
34325
|
+
});
|
|
34326
|
+
}
|
|
32618
34327
|
export {
|
|
32619
34328
|
ACP_AGENTS,
|
|
32620
34329
|
AGENTS_BY_PHASE,
|
|
@@ -32624,6 +34333,7 @@ export {
|
|
|
32624
34333
|
AUDIT_LOG_AGENT,
|
|
32625
34334
|
AdaptiveConcurrencyController,
|
|
32626
34335
|
AgentMonitorService,
|
|
34336
|
+
AgentStatusTracker,
|
|
32627
34337
|
AutonomousBrain,
|
|
32628
34338
|
AutonomousCoordinator,
|
|
32629
34339
|
BUG_HUNTER_AGENT,
|
|
@@ -32677,6 +34387,7 @@ export {
|
|
|
32677
34387
|
FleetBus,
|
|
32678
34388
|
FleetCostCapError,
|
|
32679
34389
|
FleetManager,
|
|
34390
|
+
FleetNotifier,
|
|
32680
34391
|
FleetSpawnBudgetError,
|
|
32681
34392
|
FleetSupervisor,
|
|
32682
34393
|
FleetTokenCapError,
|
|
@@ -32798,6 +34509,8 @@ export {
|
|
|
32798
34509
|
isMailboxProjectServerAvailable,
|
|
32799
34510
|
isProvenDirective,
|
|
32800
34511
|
isValidMatrixKey,
|
|
34512
|
+
kanbanBoundaryOps,
|
|
34513
|
+
kanbanDispatch,
|
|
32801
34514
|
listProjectAgentLearnedEntries,
|
|
32802
34515
|
listProjectAgentRoles,
|
|
32803
34516
|
listProjectSkillAugmentations,
|
|
@@ -32889,6 +34602,8 @@ export {
|
|
|
32889
34602
|
scoreSkillAffinity,
|
|
32890
34603
|
scrubRetiredLines,
|
|
32891
34604
|
sessionRecipient,
|
|
34605
|
+
setKanbanBoundaryOps,
|
|
34606
|
+
setKanbanDispatch,
|
|
32892
34607
|
setSkillPinned,
|
|
32893
34608
|
slugifyProjectAgentRole,
|
|
32894
34609
|
startPackageOutdatedWatcher,
|