@wrongstack/core 0.309.0 → 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/director.d.ts +7 -0
- package/dist/coordination/explore-companion.d.ts +9 -6
- package/dist/coordination/fleet-status-tool.d.ts +1 -1
- package/dist/coordination/index.d.ts +5 -1
- package/dist/coordination/index.js +2392 -405
- 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/coordination/mutation-engine.d.ts +5 -3
- package/dist/core/context.d.ts +35 -44
- package/dist/core/conversation-state.d.ts +16 -52
- package/dist/core/index.js +92 -25
- 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 +138 -162
- 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 +69 -7
- package/dist/index.d.ts +12 -5
- package/dist/index.js +21185 -26807
- package/dist/infrastructure/index.js +232 -135
- 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/observability/index.js +1 -1
- package/dist/plugin/index.js +218 -47
- package/dist/prompts/index.js +360 -3
- package/dist/security/auto-approve-policy.d.ts +2 -2
- package/dist/security/index.d.ts +1 -0
- package/dist/security/index.js +224 -68
- package/dist/security/kanban-boundary.d.ts +1 -1
- package/dist/security/kanban-governance-port.d.ts +28 -0
- package/dist/security/permission-helpers.d.ts +11 -0
- package/dist/security/permission-policy.d.ts +10 -1
- package/dist/security/yolo-risk.d.ts +17 -0
- package/dist/session-catalog/index.js +34 -5
- package/dist/session-catalog/project-server.js +34 -5
- 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/skills/index.js +39 -6
- 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 +1107 -1462
- 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 +20 -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 +2 -1
- package/dist/utils/index.js +126 -160
- package/dist/utils/regex-guard.d.ts +7 -30
- package/dist/utils/terminal-sanitize.d.ts +41 -0
- package/dist/utils/todos-format.d.ts +1 -1
- package/dist/utils/tool-subject.d.ts +1 -1
- package/dist/utils/tree-kill.d.ts +2 -0
- package/dist/utils/tree-kill.js +1 -0
- package/instructions/agents/chaos-monkey.md +5 -1
- 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 +7 -10
- package/dist/defaults/index.d.ts +0 -69
- package/dist/defaults/index.js +0 -37755
- /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;
|
|
@@ -8599,6 +8599,14 @@ var PATTERNS = [
|
|
|
8599
8599
|
anchor: "sk-ant-"
|
|
8600
8600
|
},
|
|
8601
8601
|
{ type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g, anchor: "sk-" },
|
|
8602
|
+
{
|
|
8603
|
+
// `xai` is a first-class provider in this codebase, but its key shape was
|
|
8604
|
+
// absent here — so the one credential format WrongStack itself hands users
|
|
8605
|
+
// was the one the scrubber could not recognize (audit 2026-08-20).
|
|
8606
|
+
type: "xai_key",
|
|
8607
|
+
regex: /(?<![A-Za-z0-9])xai-[A-Za-z0-9]{20,}(?![A-Za-z0-9])/g,
|
|
8608
|
+
anchor: "xai-"
|
|
8609
|
+
},
|
|
8602
8610
|
{ type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g, anchor: "ghp_" },
|
|
8603
8611
|
{ type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g, anchor: "github_pat_" },
|
|
8604
8612
|
{ type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g, anchor: "AKIA" },
|
|
@@ -8689,8 +8697,8 @@ var PATTERNS = [
|
|
|
8689
8697
|
// replacement so the separator between adjacent secrets is preserved
|
|
8690
8698
|
// rather than collapsed. Capture groups are therefore: 1=leading
|
|
8691
8699
|
// delimiter, 2=key name, 3=value.
|
|
8692
|
-
regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
|
|
8693
|
-
anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD"]
|
|
8700
|
+
regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD|PASSPHRASE))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
|
|
8701
|
+
anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD", "PASSPHRASE"]
|
|
8694
8702
|
},
|
|
8695
8703
|
{
|
|
8696
8704
|
type: "json_credential_key",
|
|
@@ -8807,6 +8815,27 @@ var JSON_CREDENTIAL_REGEX = PATTERNS.find((p) => p.type === "json_credential_key
|
|
|
8807
8815
|
var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
|
|
8808
8816
|
var SCRUB_CHUNK_BYTES = 64 * 1024;
|
|
8809
8817
|
var SCRUB_OVERLAP_BYTES = 1024;
|
|
8818
|
+
var PEM_PRIVATE_KEY_BEGIN_RE = /-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----/;
|
|
8819
|
+
var PEM_END_MARKER = "-----END";
|
|
8820
|
+
var MAX_PEM_BLOCK_BYTES = 64 * 1024;
|
|
8821
|
+
var PEM_END_LINE_TOLERANCE = 64;
|
|
8822
|
+
function extendChunkBoundaryPastPem(text, chunkStart, proposedEnd) {
|
|
8823
|
+
const head = text.slice(chunkStart, proposedEnd);
|
|
8824
|
+
const lastBegin = head.lastIndexOf("-----BEGIN ");
|
|
8825
|
+
if (lastBegin === -1) return proposedEnd;
|
|
8826
|
+
const fromBegin = text.slice(chunkStart + lastBegin);
|
|
8827
|
+
const marker = PEM_PRIVATE_KEY_BEGIN_RE.exec(fromBegin);
|
|
8828
|
+
if (!marker || marker.index !== 0) return proposedEnd;
|
|
8829
|
+
const bodyStart = marker[0].length;
|
|
8830
|
+
const cap = Math.min(text.length, chunkStart + lastBegin + MAX_PEM_BLOCK_BYTES);
|
|
8831
|
+
const closeIdx = fromBegin.indexOf(PEM_END_MARKER, bodyStart);
|
|
8832
|
+
if (closeIdx === -1 || chunkStart + lastBegin + closeIdx >= cap + PEM_END_LINE_TOLERANCE) {
|
|
8833
|
+
return proposedEnd;
|
|
8834
|
+
}
|
|
8835
|
+
const lineEnd = fromBegin.indexOf("\n", closeIdx);
|
|
8836
|
+
const end = lineEnd === -1 ? text.length : chunkStart + lastBegin + lineEnd + 1;
|
|
8837
|
+
return Math.max(proposedEnd, end);
|
|
8838
|
+
}
|
|
8810
8839
|
var PATTERN_ANCHORS = [
|
|
8811
8840
|
...new Set(
|
|
8812
8841
|
PATTERNS.flatMap(
|
|
@@ -8843,6 +8872,7 @@ var DefaultSecretScrubber = class {
|
|
|
8843
8872
|
}
|
|
8844
8873
|
}
|
|
8845
8874
|
end = safe === -1 ? end : safe + 1;
|
|
8875
|
+
end = extendChunkBoundaryPastPem(text, i, end);
|
|
8846
8876
|
}
|
|
8847
8877
|
out.push(this.scrubOne(text.slice(i, end)));
|
|
8848
8878
|
i = end;
|
|
@@ -9259,49 +9289,57 @@ async function expandGlob(pattern) {
|
|
|
9259
9289
|
async function walk(dir, pat) {
|
|
9260
9290
|
let entries;
|
|
9261
9291
|
try {
|
|
9262
|
-
entries = await fsp.readdir(dir);
|
|
9292
|
+
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
9263
9293
|
} catch {
|
|
9264
9294
|
return;
|
|
9265
9295
|
}
|
|
9266
|
-
|
|
9267
|
-
|
|
9268
|
-
|
|
9296
|
+
if (pat.startsWith("**/")) {
|
|
9297
|
+
const rest = pat.slice(3);
|
|
9298
|
+
await walk(dir, rest);
|
|
9269
9299
|
for (const e of entries) {
|
|
9270
|
-
if (
|
|
9271
|
-
const
|
|
9272
|
-
|
|
9300
|
+
if (e.isDirectory()) {
|
|
9301
|
+
const subDir = `${dir}${SEP}${e.name}`;
|
|
9302
|
+
await walk(subDir, pat);
|
|
9273
9303
|
}
|
|
9274
9304
|
}
|
|
9275
9305
|
return;
|
|
9276
9306
|
}
|
|
9277
|
-
|
|
9278
|
-
const rest = pat.slice(firstGlob);
|
|
9279
|
-
if (before.endsWith("**")) {
|
|
9280
|
-
await walk(dir, rest);
|
|
9307
|
+
if (pat === "**") {
|
|
9281
9308
|
for (const e of entries) {
|
|
9282
|
-
const full = `${dir}${SEP}${e}`;
|
|
9283
|
-
|
|
9284
|
-
|
|
9285
|
-
|
|
9286
|
-
} catch {
|
|
9309
|
+
const full = `${dir}${SEP}${e.name}`;
|
|
9310
|
+
results.add(abs ? resolve4(full) : full);
|
|
9311
|
+
if (e.isDirectory()) {
|
|
9312
|
+
await walk(full, "**");
|
|
9287
9313
|
}
|
|
9288
9314
|
}
|
|
9289
|
-
|
|
9290
|
-
|
|
9315
|
+
return;
|
|
9316
|
+
}
|
|
9317
|
+
const firstSlash = pat.indexOf("/");
|
|
9318
|
+
if (firstSlash < 0) {
|
|
9319
|
+
const re = globToRegex(pat);
|
|
9291
9320
|
for (const e of entries) {
|
|
9292
|
-
if (re.test(e)) {
|
|
9293
|
-
const full = `${dir}${SEP}${e}`;
|
|
9321
|
+
if (re.test(e.name)) {
|
|
9322
|
+
const full = `${dir}${SEP}${e.name}`;
|
|
9294
9323
|
results.add(abs ? resolve4(full) : full);
|
|
9295
9324
|
}
|
|
9296
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
|
+
}
|
|
9297
9338
|
} else {
|
|
9298
|
-
const
|
|
9299
|
-
|
|
9300
|
-
|
|
9301
|
-
|
|
9302
|
-
const stat13 = await fsp.stat(full);
|
|
9303
|
-
if (stat13.isDirectory()) await walk(full, rest);
|
|
9304
|
-
} 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);
|
|
9305
9343
|
}
|
|
9306
9344
|
}
|
|
9307
9345
|
}
|
|
@@ -10436,7 +10474,7 @@ async function gitOtherWorktrees(cwd, signal) {
|
|
|
10436
10474
|
return branches.slice(1);
|
|
10437
10475
|
}
|
|
10438
10476
|
function runGit(args, cwd, signal) {
|
|
10439
|
-
return new Promise((
|
|
10477
|
+
return new Promise((resolve17, reject) => {
|
|
10440
10478
|
let stdout = "";
|
|
10441
10479
|
let stderr = "";
|
|
10442
10480
|
const child = spawn("git", args, {
|
|
@@ -10455,7 +10493,7 @@ function runGit(args, cwd, signal) {
|
|
|
10455
10493
|
});
|
|
10456
10494
|
child.on("error", (err) => reject(err));
|
|
10457
10495
|
child.on("close", (code) => {
|
|
10458
|
-
if (code === 0)
|
|
10496
|
+
if (code === 0) resolve17(stdout);
|
|
10459
10497
|
else reject(new Error(stderr || `git ${args[0]} exited ${code}`));
|
|
10460
10498
|
});
|
|
10461
10499
|
});
|
|
@@ -10547,11 +10585,13 @@ var CHAOS_MONKEY_AGENT = {
|
|
|
10547
10585
|
tools: [...TOOLS.build],
|
|
10548
10586
|
skillNames: ["testing", "typescript-strict"],
|
|
10549
10587
|
spawnBudgetExempt: true,
|
|
10550
|
-
//
|
|
10551
|
-
//
|
|
10552
|
-
//
|
|
10553
|
-
//
|
|
10554
|
-
|
|
10588
|
+
// Run in the live checkout: mutation targets are usually freshly
|
|
10589
|
+
// written and uncommitted — a worktree spawned from HEAD would not
|
|
10590
|
+
// contain them and every mutant would drift. The mutation_test tool
|
|
10591
|
+
// honors this value as its default; callers can still override per
|
|
10592
|
+
// call via its `chaosWorktree` input when targets are committed and
|
|
10593
|
+
// isolation is wanted.
|
|
10594
|
+
worktree: "off",
|
|
10555
10595
|
// Report travels via submit_result + final text, not the leader's stream.
|
|
10556
10596
|
textStream: "silent",
|
|
10557
10597
|
toolStream: "silent"
|
|
@@ -11131,7 +11171,7 @@ function createDelegateTool(opts) {
|
|
|
11131
11171
|
};
|
|
11132
11172
|
}
|
|
11133
11173
|
async function awaitDelegateAttempt(director, subagentId, taskId, timeoutMs, abortSignal) {
|
|
11134
|
-
return new Promise((
|
|
11174
|
+
return new Promise((resolve17) => {
|
|
11135
11175
|
let settled = false;
|
|
11136
11176
|
let timer;
|
|
11137
11177
|
let offAbort = () => {
|
|
@@ -11144,7 +11184,7 @@ async function awaitDelegateAttempt(director, subagentId, taskId, timeoutMs, abo
|
|
|
11144
11184
|
offIter();
|
|
11145
11185
|
offProgress();
|
|
11146
11186
|
offAbort();
|
|
11147
|
-
|
|
11187
|
+
resolve17(value);
|
|
11148
11188
|
};
|
|
11149
11189
|
const arm = () => {
|
|
11150
11190
|
if (timer) clearTimeout(timer);
|
|
@@ -11726,16 +11766,14 @@ var ExploreCompanion = class {
|
|
|
11726
11766
|
this.running = true;
|
|
11727
11767
|
this.unsubscribers.push(
|
|
11728
11768
|
this.opts.events.on("tool.executed", (e) => {
|
|
11729
|
-
|
|
11730
|
-
if (lsid && e.sessionId && e.sessionId !== lsid) return;
|
|
11769
|
+
if (e.sessionId !== this.resolveLeaderSessionId()) return;
|
|
11731
11770
|
this.trackToolExecuted(e);
|
|
11732
11771
|
})
|
|
11733
11772
|
);
|
|
11734
11773
|
if (this.cfg.signals.todoInProgress && this.resolveLeaderAgentId()) {
|
|
11735
11774
|
this.unsubscribers.push(
|
|
11736
11775
|
this.opts.events.on("session.agents_updated", (e) => {
|
|
11737
|
-
|
|
11738
|
-
if (lsid && e.sessionId && e.sessionId !== lsid) return;
|
|
11776
|
+
if (e.sessionId !== this.resolveLeaderSessionId()) return;
|
|
11739
11777
|
this.trackAgentTodos(e.agents);
|
|
11740
11778
|
})
|
|
11741
11779
|
);
|
|
@@ -11743,8 +11781,7 @@ var ExploreCompanion = class {
|
|
|
11743
11781
|
if (this.cfg.signals.errorSymbol) {
|
|
11744
11782
|
this.unsubscribers.push(
|
|
11745
11783
|
this.opts.events.on("error", (e) => {
|
|
11746
|
-
|
|
11747
|
-
if (lsid && e.sessionId && e.sessionId !== lsid) return;
|
|
11784
|
+
if (e.sessionId !== this.resolveLeaderSessionId()) return;
|
|
11748
11785
|
this.trackError(e.err);
|
|
11749
11786
|
})
|
|
11750
11787
|
);
|
|
@@ -11768,31 +11805,31 @@ var ExploreCompanion = class {
|
|
|
11768
11805
|
// ── signal handlers ──────────────────────────────────────────────────────
|
|
11769
11806
|
trackToolExecuted(e) {
|
|
11770
11807
|
const tool = e.name.toLowerCase();
|
|
11771
|
-
const
|
|
11772
|
-
if (e.ok && this.cfg.signals.editUnreadFile && this.cfg.fileEditTools.has(tool) &&
|
|
11773
|
-
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)) {
|
|
11774
11811
|
this.engage({
|
|
11775
11812
|
id: randomUUID6(),
|
|
11776
|
-
probe: `Map file ${
|
|
11777
|
-
hint: { file:
|
|
11778
|
-
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.`,
|
|
11779
11816
|
source: "edit_unread_file",
|
|
11780
|
-
subject: `file:${
|
|
11817
|
+
subject: `file:${path44}`,
|
|
11781
11818
|
createdAt: this.now()
|
|
11782
11819
|
});
|
|
11783
11820
|
}
|
|
11784
11821
|
return;
|
|
11785
11822
|
}
|
|
11786
|
-
if (e.ok && this.cfg.signals.unfamiliarRead && tool === "read" &&
|
|
11787
|
-
if (!this.readSet.has(
|
|
11788
|
-
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);
|
|
11789
11826
|
this.engage({
|
|
11790
11827
|
id: randomUUID6(),
|
|
11791
|
-
probe: `Skeleton + callers + dependents of ${
|
|
11792
|
-
hint: { file:
|
|
11793
|
-
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}.`,
|
|
11794
11831
|
source: "unfamiliar_read",
|
|
11795
|
-
subject: `file:${
|
|
11832
|
+
subject: `file:${path44}`,
|
|
11796
11833
|
createdAt: this.now()
|
|
11797
11834
|
});
|
|
11798
11835
|
}
|
|
@@ -11857,9 +11894,18 @@ var ExploreCompanion = class {
|
|
|
11857
11894
|
limit: 20
|
|
11858
11895
|
});
|
|
11859
11896
|
const lsid = this.resolveLeaderSessionId();
|
|
11897
|
+
const selfRecipients = new Set(
|
|
11898
|
+
[
|
|
11899
|
+
this.cfg.companionAgentId,
|
|
11900
|
+
mailboxIdentityBase(this.cfg.companionAgentId),
|
|
11901
|
+
...lsid != null ? [sessionRecipient(lsid)] : []
|
|
11902
|
+
].map((r) => r.toLowerCase())
|
|
11903
|
+
);
|
|
11860
11904
|
for (const msg of messages) {
|
|
11861
11905
|
if (msg.type !== "ask" && msg.type !== "assign") continue;
|
|
11862
|
-
const
|
|
11906
|
+
const to = msg.to.trim().toLowerCase();
|
|
11907
|
+
if (to !== "*" && !selfRecipients.has(to)) continue;
|
|
11908
|
+
const fromLeader = msg.senderSessionId === void 0 && isMailboxLeader(msg.from) || lsid != null && msg.senderSessionId === lsid;
|
|
11863
11909
|
if (!fromLeader) continue;
|
|
11864
11910
|
this.engage({
|
|
11865
11911
|
id: randomUUID6(),
|
|
@@ -11972,13 +12018,13 @@ function makeDependencyWatcherConfig(opts) {
|
|
|
11972
12018
|
const globPatterns = patterns.filter((p) => p.includes("*"));
|
|
11973
12019
|
const plainPatterns = patterns.filter((p) => !p.includes("*"));
|
|
11974
12020
|
function matchesPattern(filePath) {
|
|
11975
|
-
const
|
|
11976
|
-
if (plainPatterns.includes(
|
|
12021
|
+
const basename7 = filePath.split("/").pop()?.split("\\").pop() ?? "";
|
|
12022
|
+
if (plainPatterns.includes(basename7)) return true;
|
|
11977
12023
|
for (const gp of globPatterns) {
|
|
11978
12024
|
const regex = new RegExp(
|
|
11979
12025
|
"^" + gp.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$"
|
|
11980
12026
|
);
|
|
11981
|
-
if (regex.test(
|
|
12027
|
+
if (regex.test(basename7)) return true;
|
|
11982
12028
|
}
|
|
11983
12029
|
return false;
|
|
11984
12030
|
}
|
|
@@ -12950,10 +12996,10 @@ var DirectorTaskRegistry = class _DirectorTaskRegistry {
|
|
|
12950
12996
|
pending: taskIds.filter((id) => !done.has(id))
|
|
12951
12997
|
});
|
|
12952
12998
|
}
|
|
12953
|
-
return new Promise((
|
|
12999
|
+
return new Promise((resolve17) => {
|
|
12954
13000
|
const entry = {
|
|
12955
13001
|
ids: new Set(taskIds),
|
|
12956
|
-
resolve: (result) =>
|
|
13002
|
+
resolve: (result) => resolve17({
|
|
12957
13003
|
completed: [result],
|
|
12958
13004
|
pending: taskIds.filter((id) => id !== result.taskId)
|
|
12959
13005
|
})
|
|
@@ -12961,7 +13007,7 @@ var DirectorTaskRegistry = class _DirectorTaskRegistry {
|
|
|
12961
13007
|
if (opts?.timeoutMs !== void 0) {
|
|
12962
13008
|
entry.timer = setTimeout(() => {
|
|
12963
13009
|
this.anyWaiters.delete(entry);
|
|
12964
|
-
|
|
13010
|
+
resolve17({ completed: [], pending: [...taskIds], timedOut: true });
|
|
12965
13011
|
}, opts.timeoutMs);
|
|
12966
13012
|
}
|
|
12967
13013
|
this.anyWaiters.add(entry);
|
|
@@ -13079,11 +13125,11 @@ ${JSON.stringify(result.result, null, 2)}
|
|
|
13079
13125
|
this.makeStoppedResult(taskId, "director", `Unknown task id "${taskId}" \u2014 never assigned`)
|
|
13080
13126
|
);
|
|
13081
13127
|
}
|
|
13082
|
-
let
|
|
13128
|
+
let resolve17;
|
|
13083
13129
|
const promise = new Promise((done) => {
|
|
13084
|
-
|
|
13130
|
+
resolve17 = done;
|
|
13085
13131
|
});
|
|
13086
|
-
this.taskWaiters.set(taskId, { promise, resolve:
|
|
13132
|
+
this.taskWaiters.set(taskId, { promise, resolve: resolve17 });
|
|
13087
13133
|
return promise;
|
|
13088
13134
|
}
|
|
13089
13135
|
recordAssignment(task) {
|
|
@@ -13144,16 +13190,21 @@ ${JSON.stringify(result.result, null, 2)}
|
|
|
13144
13190
|
|
|
13145
13191
|
// src/coordination/director-tools.ts
|
|
13146
13192
|
import { randomUUID as randomUUID12 } from "node:crypto";
|
|
13147
|
-
|
|
13148
|
-
|
|
13149
|
-
|
|
13150
|
-
|
|
13151
|
-
|
|
13152
|
-
|
|
13153
|
-
|
|
13154
|
-
|
|
13155
|
-
|
|
13156
|
-
|
|
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
|
+
}
|
|
13157
13208
|
|
|
13158
13209
|
// src/coordination/director-input-helpers.ts
|
|
13159
13210
|
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
@@ -13174,8 +13225,21 @@ function instantiateRosterConfig2(role, base) {
|
|
|
13174
13225
|
};
|
|
13175
13226
|
}
|
|
13176
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
|
+
|
|
13177
13242
|
// src/coordination/director-kanban-queue-helpers.ts
|
|
13178
|
-
import { describeKanbanBoundary } from "@wrongstack/kanban";
|
|
13179
13243
|
function normalizeKanbanQueueInput(input) {
|
|
13180
13244
|
const raw = input ?? {};
|
|
13181
13245
|
return {
|
|
@@ -13259,8 +13323,8 @@ function buildKanbanFleetTaskPrompt(board, task, lease) {
|
|
|
13259
13323
|
task.origin.taskId ? `sourceTaskId: ${task.origin.taskId}` : ""
|
|
13260
13324
|
].filter(Boolean).join("\n") : "";
|
|
13261
13325
|
const boundaries = [
|
|
13262
|
-
board.boundary?.enabled ? `board: ${describeKanbanBoundary(board.boundary)}` : "",
|
|
13263
|
-
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)}` : "",
|
|
13264
13328
|
...(board.boundary?.allow ?? []).map(
|
|
13265
13329
|
(selector) => `board allow ${selector.access} ${selector.kind}:${selector.path}`
|
|
13266
13330
|
),
|
|
@@ -14384,24 +14448,29 @@ var TOKEN_PATTERNS = [
|
|
|
14384
14448
|
kind: "return-null",
|
|
14385
14449
|
// `return <expr>;` where expr is not already null/undefined/void.
|
|
14386
14450
|
regex: /(?<indent>\breturn\b)(?<expr>\s+[^;{}\n]+?)\s*;/g,
|
|
14387
|
-
replace: () => "return null;"
|
|
14451
|
+
replace: () => "return null;",
|
|
14452
|
+
endpointsInCode: true
|
|
14388
14453
|
}
|
|
14389
14454
|
];
|
|
14390
14455
|
function planMutations(file, source, opts = {}) {
|
|
14391
14456
|
const maxPerFile = opts.maxPerFile ?? 25;
|
|
14392
14457
|
const out = [];
|
|
14393
14458
|
const lines = source.split("\n");
|
|
14459
|
+
const masks = computeLineMasks(source);
|
|
14394
14460
|
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
14395
14461
|
const line = lines[lineIdx];
|
|
14396
14462
|
const t = line.trim();
|
|
14397
|
-
if (t.startsWith("//")
|
|
14463
|
+
if (t.startsWith("//")) continue;
|
|
14464
|
+
const codeRanges = masks[lineIdx];
|
|
14465
|
+
const inCode = (start) => codeRanges.some(([s, e]) => start >= s && start < e);
|
|
14398
14466
|
for (const pattern of TOKEN_PATTERNS) {
|
|
14399
14467
|
pattern.regex.lastIndex = 0;
|
|
14400
14468
|
let m;
|
|
14401
14469
|
while ((m = pattern.regex.exec(line)) !== null) {
|
|
14402
14470
|
const token = m.groups?.["op"] ?? m[0];
|
|
14403
14471
|
const tokenStart = m.index + m[0].indexOf(token);
|
|
14404
|
-
if (
|
|
14472
|
+
if (!inCode(tokenStart)) continue;
|
|
14473
|
+
if (pattern.endpointsInCode && !inCode(tokenStart + token.length - 1)) continue;
|
|
14405
14474
|
const original = line.slice(tokenStart, tokenStart + token.length);
|
|
14406
14475
|
const replacement = pattern.replace(token);
|
|
14407
14476
|
if (replacement === original) continue;
|
|
@@ -14420,19 +14489,179 @@ function planMutations(file, source, opts = {}) {
|
|
|
14420
14489
|
}
|
|
14421
14490
|
return out.slice(0, maxPerFile);
|
|
14422
14491
|
}
|
|
14423
|
-
function
|
|
14424
|
-
|
|
14425
|
-
|
|
14426
|
-
|
|
14427
|
-
|
|
14428
|
-
|
|
14429
|
-
|
|
14430
|
-
|
|
14431
|
-
|
|
14492
|
+
function computeLineMasks(source) {
|
|
14493
|
+
const lines = source.split("\n");
|
|
14494
|
+
const masks = lines.map(() => []);
|
|
14495
|
+
const stack = [{ kind: "code", depth: 0, parens: [] }];
|
|
14496
|
+
let inBlockComment = false;
|
|
14497
|
+
let lastToken = null;
|
|
14498
|
+
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
14499
|
+
const line = lines[lineIdx];
|
|
14500
|
+
const ranges = masks[lineIdx];
|
|
14501
|
+
let runStart = null;
|
|
14502
|
+
const closeRun = (end) => {
|
|
14503
|
+
if (runStart !== null && end > runStart) ranges.push([runStart, end]);
|
|
14504
|
+
runStart = null;
|
|
14505
|
+
};
|
|
14506
|
+
let i = 0;
|
|
14507
|
+
if (inBlockComment) {
|
|
14508
|
+
const close = line.indexOf("*/");
|
|
14509
|
+
if (close === -1) continue;
|
|
14510
|
+
inBlockComment = false;
|
|
14511
|
+
i = close + 2;
|
|
14512
|
+
}
|
|
14513
|
+
while (i < line.length) {
|
|
14514
|
+
const top = stack[stack.length - 1];
|
|
14515
|
+
const c = line[i];
|
|
14516
|
+
if (top.kind === "template") {
|
|
14517
|
+
if (c === "\\") {
|
|
14518
|
+
i += 2;
|
|
14519
|
+
continue;
|
|
14520
|
+
}
|
|
14521
|
+
if (c === "`") {
|
|
14522
|
+
stack.pop();
|
|
14523
|
+
lastToken = "`";
|
|
14524
|
+
i++;
|
|
14525
|
+
continue;
|
|
14526
|
+
}
|
|
14527
|
+
if (c === "$" && line[i + 1] === "{") {
|
|
14528
|
+
stack.push({ kind: "code", depth: 0, parens: [] });
|
|
14529
|
+
lastToken = "${";
|
|
14530
|
+
i += 2;
|
|
14531
|
+
continue;
|
|
14532
|
+
}
|
|
14533
|
+
i++;
|
|
14534
|
+
continue;
|
|
14535
|
+
}
|
|
14536
|
+
if (/[\w$]/.test(c)) {
|
|
14537
|
+
let j = i + 1;
|
|
14538
|
+
while (j < line.length && /[\w$]/.test(line[j])) j++;
|
|
14539
|
+
lastToken = line.slice(i, j);
|
|
14540
|
+
if (runStart === null) runStart = i;
|
|
14541
|
+
i = j;
|
|
14542
|
+
continue;
|
|
14543
|
+
}
|
|
14544
|
+
if (c === "'" || c === '"') {
|
|
14545
|
+
closeRun(i);
|
|
14546
|
+
i++;
|
|
14547
|
+
while (i < line.length && line[i] !== c) {
|
|
14548
|
+
if (line[i] === "\\") i++;
|
|
14549
|
+
i++;
|
|
14550
|
+
}
|
|
14551
|
+
i++;
|
|
14552
|
+
lastToken = c;
|
|
14553
|
+
continue;
|
|
14554
|
+
}
|
|
14555
|
+
if (c === "`") {
|
|
14556
|
+
closeRun(i);
|
|
14557
|
+
stack.push({ kind: "template", depth: 0, parens: [] });
|
|
14558
|
+
i++;
|
|
14559
|
+
continue;
|
|
14560
|
+
}
|
|
14561
|
+
if (c === "/" && line[i + 1] === "/") {
|
|
14562
|
+
closeRun(i);
|
|
14563
|
+
break;
|
|
14564
|
+
}
|
|
14565
|
+
if (c === "/" && line[i + 1] === "*") {
|
|
14566
|
+
closeRun(i);
|
|
14567
|
+
const close = line.indexOf("*/", i + 2);
|
|
14568
|
+
if (close === -1) {
|
|
14569
|
+
inBlockComment = true;
|
|
14570
|
+
break;
|
|
14571
|
+
}
|
|
14572
|
+
i = close + 2;
|
|
14573
|
+
continue;
|
|
14574
|
+
}
|
|
14575
|
+
if (c === "/") {
|
|
14576
|
+
if (!tokenCanEndOperand(lastToken)) {
|
|
14577
|
+
closeRun(i);
|
|
14578
|
+
const next = skipRegexLiteral(line, i);
|
|
14579
|
+
lastToken = next > i + 1 ? "regex" : "/";
|
|
14580
|
+
i = next;
|
|
14581
|
+
continue;
|
|
14582
|
+
}
|
|
14583
|
+
}
|
|
14584
|
+
if (c === "(") {
|
|
14585
|
+
top.parens.push(CONTROL_KEYWORDS.has(lastToken ?? "") ? "control" : "expr");
|
|
14586
|
+
lastToken = c;
|
|
14587
|
+
} else if (c === ")") {
|
|
14588
|
+
const kind = top.parens.pop() ?? "expr";
|
|
14589
|
+
lastToken = kind === "control" ? "control-paren-close" : ")";
|
|
14590
|
+
} else if (c === "{") {
|
|
14591
|
+
top.depth++;
|
|
14592
|
+
lastToken = c;
|
|
14593
|
+
} else if (c === "}") {
|
|
14594
|
+
if (top.depth > 0) {
|
|
14595
|
+
top.depth--;
|
|
14596
|
+
lastToken = c;
|
|
14597
|
+
} else if (stack.length > 1) {
|
|
14598
|
+
closeRun(i);
|
|
14599
|
+
stack.pop();
|
|
14600
|
+
i++;
|
|
14601
|
+
continue;
|
|
14602
|
+
} else {
|
|
14603
|
+
lastToken = c;
|
|
14604
|
+
}
|
|
14605
|
+
} else if (c !== " " && c !== " " && c !== "\r") {
|
|
14606
|
+
lastToken = c;
|
|
14607
|
+
}
|
|
14608
|
+
if (runStart === null) runStart = i;
|
|
14609
|
+
i++;
|
|
14610
|
+
}
|
|
14611
|
+
closeRun(line.length);
|
|
14612
|
+
}
|
|
14613
|
+
return masks;
|
|
14614
|
+
}
|
|
14615
|
+
var KEYWORDS_BEFORE_REGEX = /* @__PURE__ */ new Set([
|
|
14616
|
+
"return",
|
|
14617
|
+
"typeof",
|
|
14618
|
+
"instanceof",
|
|
14619
|
+
"in",
|
|
14620
|
+
"of",
|
|
14621
|
+
"new",
|
|
14622
|
+
"delete",
|
|
14623
|
+
"void",
|
|
14624
|
+
"throw",
|
|
14625
|
+
"case",
|
|
14626
|
+
"do",
|
|
14627
|
+
"else",
|
|
14628
|
+
"yield",
|
|
14629
|
+
"await"
|
|
14630
|
+
]);
|
|
14631
|
+
var CONTROL_KEYWORDS = /* @__PURE__ */ new Set(["if", "for", "while", "switch", "catch", "with", "await"]);
|
|
14632
|
+
function tokenCanEndOperand(token) {
|
|
14633
|
+
if (token === null) return false;
|
|
14634
|
+
if (/^[\w$]+$/.test(token)) return !KEYWORDS_BEFORE_REGEX.has(token);
|
|
14635
|
+
return token === ")" || token === "]" || token === "." || token === '"' || token === "'" || token === "`";
|
|
14636
|
+
}
|
|
14637
|
+
function skipRegexLiteral(line, start) {
|
|
14638
|
+
let i = start + 1;
|
|
14639
|
+
let inClass = false;
|
|
14640
|
+
while (i < line.length) {
|
|
14641
|
+
const ch = line[i];
|
|
14642
|
+
if (ch === "\\") {
|
|
14643
|
+
i += 2;
|
|
14644
|
+
continue;
|
|
14645
|
+
}
|
|
14646
|
+
if (inClass) {
|
|
14647
|
+
if (ch === "]") inClass = false;
|
|
14648
|
+
i++;
|
|
14649
|
+
continue;
|
|
14650
|
+
}
|
|
14651
|
+
if (ch === "[") {
|
|
14652
|
+
inClass = true;
|
|
14653
|
+
i++;
|
|
14654
|
+
continue;
|
|
14655
|
+
}
|
|
14656
|
+
if (ch === "/") {
|
|
14657
|
+
i++;
|
|
14658
|
+
break;
|
|
14659
|
+
}
|
|
14660
|
+
if (ch === "\n" || ch === "\r") return line.length;
|
|
14661
|
+
i++;
|
|
14432
14662
|
}
|
|
14433
|
-
|
|
14434
|
-
|
|
14435
|
-
return /['"]/.test(window);
|
|
14663
|
+
while (i < line.length && /[a-z]/.test(line[i])) i++;
|
|
14664
|
+
return i;
|
|
14436
14665
|
}
|
|
14437
14666
|
function parseMutationReport(text) {
|
|
14438
14667
|
const candidates = [];
|
|
@@ -14483,7 +14712,7 @@ function normalizeMutantEntry(value) {
|
|
|
14483
14712
|
const rec = value;
|
|
14484
14713
|
const id = typeof rec["id"] === "string" ? rec["id"] : void 0;
|
|
14485
14714
|
const status = rec["status"];
|
|
14486
|
-
if (!id || status !== "killed" && status !== "survived" && status !== "skipped") {
|
|
14715
|
+
if (!id || status !== "killed" && status !== "survived" && status !== "skipped" && status !== "killed-by-hang") {
|
|
14487
14716
|
return void 0;
|
|
14488
14717
|
}
|
|
14489
14718
|
return {
|
|
@@ -14539,7 +14768,7 @@ function makeMutationTestTool(director, roster, opts = {}) {
|
|
|
14539
14768
|
},
|
|
14540
14769
|
chaosWorktree: {
|
|
14541
14770
|
anyOf: [{ type: "boolean" }, { type: "string", enum: ["auto", "required", "off"] }],
|
|
14542
|
-
description: "Worktree override for the chaos agent.
|
|
14771
|
+
description: "Worktree override for the chaos agent. Defaults to the roster policy for chaos-monkey ('off'), because mutation targets are usually freshly written and uncommitted \u2014 a worktree from HEAD would not contain them and every mutant would drift to skipped. Only pass 'auto' or 'required' when the targets are committed."
|
|
14543
14772
|
},
|
|
14544
14773
|
timeoutMs: { type: "number", minimum: 1, description: "Per-task timeout for chaos/strengthen/rerun tasks." },
|
|
14545
14774
|
reportOnly: {
|
|
@@ -14561,8 +14790,16 @@ function makeMutationTestTool(director, roster, opts = {}) {
|
|
|
14561
14790
|
error: "No mutable sites found in the given targets (after comment/string filtering)."
|
|
14562
14791
|
};
|
|
14563
14792
|
}
|
|
14793
|
+
const chaosBase = roster?.[CHAOS_ROLE];
|
|
14794
|
+
if (!chaosBase) {
|
|
14795
|
+
return {
|
|
14796
|
+
verdict: "inconclusive",
|
|
14797
|
+
passed: false,
|
|
14798
|
+
error: "chaos-monkey role missing from the roster \u2014 refusing to spawn a saboteur without its prompt/tools contract. Build the toolset with a roster that includes 'chaos-monkey' (FLEET_ROSTER does)."
|
|
14799
|
+
};
|
|
14800
|
+
}
|
|
14564
14801
|
const chaosSubagentId = await director.spawn(
|
|
14565
|
-
makeChaosConfig(
|
|
14802
|
+
makeChaosConfig(chaosBase, i.chaosWorktree ?? chaosBase.worktree ?? "off")
|
|
14566
14803
|
);
|
|
14567
14804
|
const chaosTaskId = await director.assign({
|
|
14568
14805
|
id: randomUUID11(),
|
|
@@ -14580,6 +14817,7 @@ function makeMutationTestTool(director, roster, opts = {}) {
|
|
|
14580
14817
|
);
|
|
14581
14818
|
const attempts = [];
|
|
14582
14819
|
let current = survivors;
|
|
14820
|
+
let rerunUnknowns = [];
|
|
14583
14821
|
while (current.length > 0 && attempts.length < maxAttempts && i.repairSubagentId) {
|
|
14584
14822
|
const attemptNo = attempts.length + 1;
|
|
14585
14823
|
const strengthenTaskId = await director.assign({
|
|
@@ -14601,7 +14839,7 @@ function makeMutationTestTool(director, roster, opts = {}) {
|
|
|
14601
14839
|
}
|
|
14602
14840
|
const survivorPlan = plan.filter((p) => current.some((s) => s.id === p.id));
|
|
14603
14841
|
const rerunSubagentId = await director.spawn(
|
|
14604
|
-
makeChaosConfig(
|
|
14842
|
+
makeChaosConfig(chaosBase, i.chaosWorktree ?? chaosBase.worktree ?? "off")
|
|
14605
14843
|
);
|
|
14606
14844
|
const rerunTaskId = await director.assign({
|
|
14607
14845
|
id: randomUUID11(),
|
|
@@ -14611,29 +14849,36 @@ function makeMutationTestTool(director, roster, opts = {}) {
|
|
|
14611
14849
|
});
|
|
14612
14850
|
const [rerunResult] = await director.awaitTasks([rerunTaskId]);
|
|
14613
14851
|
const passN = collectOutcomes(rerunResult, survivorPlan);
|
|
14614
|
-
const stillSurviving = passN.filter((m) => m.status
|
|
14852
|
+
const stillSurviving = passN.filter((m) => !isKill(m.status));
|
|
14853
|
+
rerunUnknowns = passN.filter((m) => m.status === "skipped");
|
|
14615
14854
|
attempts.push({
|
|
14616
14855
|
attempt: attemptNo,
|
|
14617
14856
|
survivorsBefore: current,
|
|
14618
14857
|
strengthenResult: { taskId: strengthenResult.taskId, status: strengthenResult.status },
|
|
14619
14858
|
rerunResult: { taskId: rerunTaskId, status: rerunResult?.status ?? "unknown" },
|
|
14620
14859
|
survivorsAfter: stillSurviving,
|
|
14621
|
-
suspectedEquivalent: stillSurviving.filter((m) => current.some((c) => c.id === m.id)).map((m) => m.id)
|
|
14860
|
+
suspectedEquivalent: stillSurviving.filter((m) => m.status === "survived" && current.some((c) => c.id === m.id)).map((m) => m.id)
|
|
14622
14861
|
});
|
|
14623
|
-
current = stillSurviving
|
|
14624
|
-
if (passN.every((m) => m.status === "skipped")) break;
|
|
14862
|
+
current = stillSurviving;
|
|
14625
14863
|
}
|
|
14626
|
-
const finalSurvivors = current;
|
|
14864
|
+
const finalSurvivors = current.filter((m) => m.status === "survived");
|
|
14627
14865
|
const verifiedCount = pass1.filter((m) => m.status !== "skipped").length;
|
|
14628
14866
|
const skippedCount = pass1.filter((m) => m.status === "skipped").length;
|
|
14629
|
-
const
|
|
14630
|
-
const
|
|
14867
|
+
const rerunUnknownCount = rerunUnknowns.length;
|
|
14868
|
+
const score = plan.length === 0 ? 0 : pass1.filter((m) => isKill(m.status)).length / plan.length;
|
|
14869
|
+
const verdict = verifiedCount === 0 ? "inconclusive" : finalSurvivors.length === 0 ? skippedCount > 0 || rerunUnknownCount > 0 ? "partial" : "pass" : score >= 0.8 ? "partial" : "fail";
|
|
14631
14870
|
return {
|
|
14632
14871
|
verdict,
|
|
14633
14872
|
passed: verdict === "pass",
|
|
14634
14873
|
mutationScore: Number.parseFloat(score.toFixed(3)),
|
|
14635
14874
|
planned: plan.length,
|
|
14636
|
-
killed: pass1.filter((m) => m.status
|
|
14875
|
+
killed: pass1.filter((m) => isKill(m.status)).length,
|
|
14876
|
+
// Breakout of `killed`: how many kills were detected by the test
|
|
14877
|
+
// command hanging rather than by a failing assertion. A subset of
|
|
14878
|
+
// `killed`, surfaced so a director can distinguish a hang-heavy
|
|
14879
|
+
// suite (mutants breaking termination, not assertions) from an
|
|
14880
|
+
// assertion-strong one. hangHeavy = killedByHang === killed.
|
|
14881
|
+
killedByHang: pass1.filter((m) => m.status === "killed-by-hang").length,
|
|
14637
14882
|
survived: pass1.filter((m) => m.status === "survived").length,
|
|
14638
14883
|
skipped: pass1.filter((m) => m.status === "skipped").length,
|
|
14639
14884
|
finalSurvivors: finalSurvivors.map((m) => ({ id: m.id, file: m.file, kind: m.kind })),
|
|
@@ -14641,7 +14886,11 @@ function makeMutationTestTool(director, roster, opts = {}) {
|
|
|
14641
14886
|
strengthenAttempts: attempts.length,
|
|
14642
14887
|
attempts,
|
|
14643
14888
|
chaosTaskId,
|
|
14644
|
-
|
|
14889
|
+
// Unverified leftovers from the strengthen loop: surfaced so the
|
|
14890
|
+
// caller can see WHICH mutants lack kill evidence, and counted by
|
|
14891
|
+
// the verdict gate above.
|
|
14892
|
+
unverifiedFromRerun: rerunUnknowns.map((m) => ({ id: m.id, file: m.file, kind: m.kind })),
|
|
14893
|
+
nextAction: finalSurvivors.length === 0 && rerunUnknownCount === 0 && skippedCount === 0 ? "accept" : attempts.length >= maxAttempts && i.repairSubagentId ? "manual_review_survivors" : "strengthen_tests"
|
|
14645
14894
|
};
|
|
14646
14895
|
}
|
|
14647
14896
|
};
|
|
@@ -14657,7 +14906,7 @@ function normalizeMutationTestInput(input) {
|
|
|
14657
14906
|
maxPerFile: typeof raw["maxPerFile"] === "number" ? raw["maxPerFile"] : void 0,
|
|
14658
14907
|
maxStrengthenAttempts: typeof raw["maxStrengthenAttempts"] === "number" ? raw["maxStrengthenAttempts"] : void 0,
|
|
14659
14908
|
repairSubagentId: typeof raw["repairSubagentId"] === "string" && raw["repairSubagentId"].trim() ? raw["repairSubagentId"].trim() : void 0,
|
|
14660
|
-
chaosWorktree: raw["chaosWorktree"]
|
|
14909
|
+
chaosWorktree: normalizeWorktreeOverride(raw["chaosWorktree"]),
|
|
14661
14910
|
timeoutMs: typeof raw["timeoutMs"] === "number" ? raw["timeoutMs"] : void 0,
|
|
14662
14911
|
reportOnly: raw["reportOnly"] === true
|
|
14663
14912
|
};
|
|
@@ -14679,8 +14928,7 @@ function buildPlan(i, projectRoot) {
|
|
|
14679
14928
|
}
|
|
14680
14929
|
return plan;
|
|
14681
14930
|
}
|
|
14682
|
-
function makeChaosConfig(
|
|
14683
|
-
const base = roster?.[CHAOS_ROLE] ?? getAgentDefinition(CHAOS_ROLE)?.config ?? { name: "Chaos Monkey", role: CHAOS_ROLE };
|
|
14931
|
+
function makeChaosConfig(base, worktree) {
|
|
14684
14932
|
return { ...instantiateRosterConfig2(CHAOS_ROLE, base), worktree };
|
|
14685
14933
|
}
|
|
14686
14934
|
function buildChaosTask(plan, i, pass, priorSurvivors) {
|
|
@@ -14696,7 +14944,7 @@ ${priorSurvivors.map((s) => `- ${s.id} (${s.kind} @ ${s.file}:${s.line})`).join(
|
|
|
14696
14944
|
"For each mutant, in order:",
|
|
14697
14945
|
"1. Apply ONLY that mutation at its exact (file, line, column).",
|
|
14698
14946
|
`2. Run the test command: ${i.testCommand}${i.cwd ? ` (cwd: ${i.cwd})` : ""}`,
|
|
14699
|
-
"3. Record killed (tests failed \u2014 quote first failing assertion)
|
|
14947
|
+
"3. Record killed (tests failed \u2014 quote first failing assertion), survived (suite green), or killed-by-hang (the test command timed out or was aborted \u2014 the mutation broke the suite by non-termination; record the timeout as evidence, do NOT report it as survived).",
|
|
14700
14948
|
"4. Restore the file byte-for-byte before the next mutant.",
|
|
14701
14949
|
"",
|
|
14702
14950
|
"Mutants:",
|
|
@@ -14708,23 +14956,49 @@ ${priorSurvivors.map((s) => `- ${s.id} (${s.kind} @ ${s.file}:${s.line})`).join(
|
|
|
14708
14956
|
].join("\n");
|
|
14709
14957
|
}
|
|
14710
14958
|
function buildStrengthenTask(survivors, i, attempt) {
|
|
14959
|
+
const confirmed = survivors.filter((s) => s.status === "survived");
|
|
14960
|
+
const unverified = survivors.filter((s) => s.status === "skipped");
|
|
14961
|
+
const row = (s) => `- ${s.id} | ${s.file}:${s.line} | ${s.kind}${s.evidence ? ` | ${s.evidence}` : ""}`;
|
|
14711
14962
|
return [
|
|
14712
|
-
`Strengthen the tests so
|
|
14963
|
+
`Strengthen the tests so the mutants below die (attempt ${attempt}).`,
|
|
14713
14964
|
"",
|
|
14714
|
-
|
|
14715
|
-
|
|
14716
|
-
|
|
14717
|
-
|
|
14965
|
+
...confirmed.length > 0 ? [
|
|
14966
|
+
"CONFIRMED SURVIVORS \u2014 each was a deliberate sabotage of production code that the current suite did NOT catch:",
|
|
14967
|
+
...confirmed.map(row),
|
|
14968
|
+
""
|
|
14969
|
+
] : [],
|
|
14970
|
+
...unverified.length > 0 ? [
|
|
14971
|
+
"UNVERIFIED \u2014 these mutations were never actually re-tested (the re-verify pass skipped or did not report them). Do NOT assume the suite misses them: first apply each mutation, run the tests, and confirm it really survives; if the tests already fail, report that instead of writing new assertions.",
|
|
14972
|
+
...unverified.map(row),
|
|
14973
|
+
""
|
|
14974
|
+
] : [],
|
|
14975
|
+
`Test command that must fail under each CONFIRMED mutant: ${i.testCommand}`,
|
|
14718
14976
|
"",
|
|
14719
|
-
"For each survivor add or tighten exactly one assertion that pins the sabotaged boundary/behavior. Do not change production code. Do not weaken other tests. Run the suite green on clean code before finishing."
|
|
14977
|
+
"For each CONFIRMED survivor add or tighten exactly one assertion that pins the sabotaged boundary/behavior. Do not change production code. Do not weaken other tests. Run the suite green on clean code before finishing."
|
|
14720
14978
|
].join("\n");
|
|
14721
14979
|
}
|
|
14722
14980
|
function collectOutcomes(result, plan) {
|
|
14723
14981
|
const fromText = parseTextOutcomes(result);
|
|
14724
14982
|
if (fromText.length > 0) {
|
|
14725
|
-
const
|
|
14726
|
-
const matched =
|
|
14727
|
-
|
|
14983
|
+
const remaining = [...plan];
|
|
14984
|
+
const matched = [];
|
|
14985
|
+
for (const m of fromText) {
|
|
14986
|
+
const idx = remaining.findIndex((p) => p.id === m.id);
|
|
14987
|
+
if (idx === -1) continue;
|
|
14988
|
+
remaining.splice(idx, 1);
|
|
14989
|
+
matched.push(m);
|
|
14990
|
+
}
|
|
14991
|
+
if (matched.length > 0) {
|
|
14992
|
+
const missing = remaining.map((p) => ({
|
|
14993
|
+
id: p.id,
|
|
14994
|
+
file: p.file,
|
|
14995
|
+
line: p.line,
|
|
14996
|
+
kind: p.kind,
|
|
14997
|
+
status: "skipped",
|
|
14998
|
+
evidence: "not reported by chaos task"
|
|
14999
|
+
}));
|
|
15000
|
+
return [...matched, ...missing];
|
|
15001
|
+
}
|
|
14728
15002
|
}
|
|
14729
15003
|
return plan.map((p) => ({
|
|
14730
15004
|
id: p.id,
|
|
@@ -14735,6 +15009,9 @@ function collectOutcomes(result, plan) {
|
|
|
14735
15009
|
evidence: result ? `chaos task ended ${result.status}` : "chaos task produced no result"
|
|
14736
15010
|
}));
|
|
14737
15011
|
}
|
|
15012
|
+
function isKill(status) {
|
|
15013
|
+
return status === "killed" || status === "killed-by-hang";
|
|
15014
|
+
}
|
|
14738
15015
|
function parseTextOutcomes(result) {
|
|
14739
15016
|
const text = typeof result?.result === "string" ? result.result : void 0;
|
|
14740
15017
|
if (!text) return [];
|
|
@@ -14980,7 +15257,7 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
14980
15257
|
i.heartbeatIntervalMs ?? Math.floor(leaseTtlMs / 2)
|
|
14981
15258
|
);
|
|
14982
15259
|
const effectiveLeaseTtlMs = Math.max(leaseTtlMs, heartbeatIntervalMs * 2);
|
|
14983
|
-
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, {
|
|
14984
15261
|
...i.boardId !== void 0 ? { boardId: i.boardId } : {}
|
|
14985
15262
|
})).filter((candidate) => matchesKanbanQueueQuery(candidate.task, i.query ?? "")).slice(0, maxTasks).map((candidate) => candidate.task.id) : void 0;
|
|
14986
15263
|
const dispatches = [];
|
|
@@ -14991,7 +15268,7 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
14991
15268
|
const candidateTaskId = candidateTaskIds?.[index];
|
|
14992
15269
|
if (candidateTaskIds && !candidateTaskId) break;
|
|
14993
15270
|
if (candidateTaskId && budgetRejectedTaskIds.has(candidateTaskId)) continue;
|
|
14994
|
-
const reserved = await reserveKanbanDispatch(projectRoot, {
|
|
15271
|
+
const reserved = await kanbanDispatch().reserveKanbanDispatch(projectRoot, {
|
|
14995
15272
|
...i.boardId !== void 0 ? { boardId: i.boardId } : {},
|
|
14996
15273
|
...candidateTaskId !== void 0 ? { taskId: candidateTaskId } : {},
|
|
14997
15274
|
routing: {
|
|
@@ -15021,7 +15298,7 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
15021
15298
|
if (remaining !== void 0 && remaining < costCeiling) {
|
|
15022
15299
|
budgetRejectedTaskIds.add(claim.task.id);
|
|
15023
15300
|
const budgetError = `Cost ceiling ${costCeiling} exceeds remaining budget ${remaining.toFixed(4)}`;
|
|
15024
|
-
await updateTaskAssignment(
|
|
15301
|
+
await kanbanDispatch().updateTaskAssignment(
|
|
15025
15302
|
projectRoot,
|
|
15026
15303
|
claim.board.id,
|
|
15027
15304
|
claim.task.id,
|
|
@@ -15072,7 +15349,7 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
15072
15349
|
}
|
|
15073
15350
|
}
|
|
15074
15351
|
};
|
|
15075
|
-
const started = await startKanbanDispatch(projectRoot, {
|
|
15352
|
+
const started = await kanbanDispatch().startKanbanDispatch(projectRoot, {
|
|
15076
15353
|
boardId: claim.board.id,
|
|
15077
15354
|
taskId: claim.task.id,
|
|
15078
15355
|
leaseId: ourLeaseId,
|
|
@@ -15117,7 +15394,7 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
15117
15394
|
message = `${message}; cleanup failed for spawned subagent ${subagentId}: ${toErrorMessage(cleanupErr)}`;
|
|
15118
15395
|
}
|
|
15119
15396
|
}
|
|
15120
|
-
await failKanbanDispatch(projectRoot, {
|
|
15397
|
+
await kanbanDispatch().failKanbanDispatch(projectRoot, {
|
|
15121
15398
|
boardId: claim.board.id,
|
|
15122
15399
|
taskId: claim.task.id,
|
|
15123
15400
|
leaseId: ourLeaseId,
|
|
@@ -15180,7 +15457,7 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
15180
15457
|
for (const dispatch of dispatches) {
|
|
15181
15458
|
if (!runTaskIds.has(dispatch.runTaskId)) continue;
|
|
15182
15459
|
try {
|
|
15183
|
-
const board = await getBoard(projectRoot, dispatch.boardId);
|
|
15460
|
+
const board = await kanbanDispatch().getBoard(projectRoot, dispatch.boardId);
|
|
15184
15461
|
const liveTask = board?.tasks.find((t) => t.id === dispatch.taskId);
|
|
15185
15462
|
const liveLeaseId = liveTask?.assignment?.leaseId;
|
|
15186
15463
|
if (liveLeaseId !== void 0 && liveLeaseId !== dispatch.leaseId) {
|
|
@@ -15192,10 +15469,15 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
15192
15469
|
} catch {
|
|
15193
15470
|
}
|
|
15194
15471
|
try {
|
|
15195
|
-
await heartbeatTaskAssignment(
|
|
15196
|
-
|
|
15197
|
-
|
|
15198
|
-
|
|
15472
|
+
await kanbanDispatch().heartbeatTaskAssignment(
|
|
15473
|
+
projectRoot,
|
|
15474
|
+
dispatch.boardId,
|
|
15475
|
+
dispatch.taskId,
|
|
15476
|
+
{
|
|
15477
|
+
leaseExpiresAt: refreshedExpiry,
|
|
15478
|
+
expectedLeaseId: dispatch.leaseId
|
|
15479
|
+
}
|
|
15480
|
+
);
|
|
15199
15481
|
} catch {
|
|
15200
15482
|
}
|
|
15201
15483
|
}
|
|
@@ -15218,7 +15500,7 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
15218
15500
|
if (!dispatch) continue;
|
|
15219
15501
|
runTaskIds.delete(dispatch.runTaskId);
|
|
15220
15502
|
if (result.status === "success") {
|
|
15221
|
-
const completed = await completeKanbanDispatch(projectRoot, {
|
|
15503
|
+
const completed = await kanbanDispatch().completeKanbanDispatch(projectRoot, {
|
|
15222
15504
|
boardId: dispatch.boardId,
|
|
15223
15505
|
taskId: dispatch.taskId,
|
|
15224
15506
|
leaseId: dispatch.leaseId,
|
|
@@ -15234,7 +15516,7 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
15234
15516
|
});
|
|
15235
15517
|
}
|
|
15236
15518
|
} else {
|
|
15237
|
-
const failed = await failKanbanDispatch(projectRoot, {
|
|
15519
|
+
const failed = await kanbanDispatch().failKanbanDispatch(projectRoot, {
|
|
15238
15520
|
boardId: dispatch.boardId,
|
|
15239
15521
|
taskId: dispatch.taskId,
|
|
15240
15522
|
leaseId: dispatch.leaseId,
|
|
@@ -15286,7 +15568,7 @@ async function renewAndRevokeLease(opts) {
|
|
|
15286
15568
|
} = opts;
|
|
15287
15569
|
let revoked = false;
|
|
15288
15570
|
try {
|
|
15289
|
-
const board = await getBoard(projectRoot, boardId);
|
|
15571
|
+
const board = await kanbanDispatch().getBoard(projectRoot, boardId);
|
|
15290
15572
|
const liveTask = board?.tasks.find((t) => t.id === taskId);
|
|
15291
15573
|
const liveLeaseId = liveTask?.assignment?.leaseId;
|
|
15292
15574
|
if (liveLeaseId !== void 0 && liveLeaseId !== ourLeaseId) {
|
|
@@ -15299,7 +15581,7 @@ async function renewAndRevokeLease(opts) {
|
|
|
15299
15581
|
} catch {
|
|
15300
15582
|
}
|
|
15301
15583
|
if (!revoked) {
|
|
15302
|
-
await heartbeatTaskAssignment(projectRoot, boardId, taskId, {
|
|
15584
|
+
await kanbanDispatch().heartbeatTaskAssignment(projectRoot, boardId, taskId, {
|
|
15303
15585
|
leaseExpiresAt: refreshedExpiry,
|
|
15304
15586
|
expectedLeaseId: ourLeaseId
|
|
15305
15587
|
}).catch(() => {
|
|
@@ -15402,12 +15684,12 @@ function rosterSummaryFromConfigs(roster) {
|
|
|
15402
15684
|
|
|
15403
15685
|
// src/coordination/director-session.ts
|
|
15404
15686
|
import * as fsp24 from "node:fs/promises";
|
|
15405
|
-
import * as
|
|
15687
|
+
import * as path33 from "node:path";
|
|
15406
15688
|
|
|
15407
15689
|
// src/storage/session-store.ts
|
|
15408
15690
|
import { randomUUID as randomUUID14 } from "node:crypto";
|
|
15409
15691
|
import * as fsp23 from "node:fs/promises";
|
|
15410
|
-
import * as
|
|
15692
|
+
import * as path32 from "node:path";
|
|
15411
15693
|
|
|
15412
15694
|
// src/session-catalog/client.ts
|
|
15413
15695
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -15485,7 +15767,7 @@ function normalize2(value) {
|
|
|
15485
15767
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
15486
15768
|
}
|
|
15487
15769
|
function delay(ms) {
|
|
15488
|
-
return new Promise((
|
|
15770
|
+
return new Promise((resolve17) => setTimeout(resolve17, ms));
|
|
15489
15771
|
}
|
|
15490
15772
|
var SessionCatalogProjectClient = class {
|
|
15491
15773
|
constructor(options) {
|
|
@@ -15575,8 +15857,8 @@ var SessionCatalogProjectClient = class {
|
|
|
15575
15857
|
this.socket = null;
|
|
15576
15858
|
this.info = null;
|
|
15577
15859
|
if (socket && !socket.destroyed)
|
|
15578
|
-
await new Promise((
|
|
15579
|
-
socket.once("close",
|
|
15860
|
+
await new Promise((resolve17) => {
|
|
15861
|
+
socket.once("close", resolve17);
|
|
15580
15862
|
socket.end();
|
|
15581
15863
|
});
|
|
15582
15864
|
}
|
|
@@ -15614,7 +15896,7 @@ var SessionCatalogProjectClient = class {
|
|
|
15614
15896
|
this.info = null;
|
|
15615
15897
|
this.authToken = void 0;
|
|
15616
15898
|
this.buffer = "";
|
|
15617
|
-
return new Promise((
|
|
15899
|
+
return new Promise((resolve17, reject) => {
|
|
15618
15900
|
const socket = net.createConnection(this.endpoint);
|
|
15619
15901
|
this.socket = socket;
|
|
15620
15902
|
socket.setEncoding("utf8");
|
|
@@ -15627,7 +15909,7 @@ var SessionCatalogProjectClient = class {
|
|
|
15627
15909
|
clearTimeout(timer);
|
|
15628
15910
|
this.connectResolve = null;
|
|
15629
15911
|
this.connectReject = null;
|
|
15630
|
-
|
|
15912
|
+
resolve17();
|
|
15631
15913
|
};
|
|
15632
15914
|
this.connectReject = (error) => {
|
|
15633
15915
|
clearTimeout(timer);
|
|
@@ -15672,7 +15954,7 @@ var SessionCatalogProjectClient = class {
|
|
|
15672
15954
|
});
|
|
15673
15955
|
if (encoded.length > SESSION_CATALOG_MAX_FRAME_CHARS)
|
|
15674
15956
|
return Promise.reject(new Error("Session Catalog request exceeded frame limit"));
|
|
15675
|
-
return new Promise((
|
|
15957
|
+
return new Promise((resolve17, reject) => {
|
|
15676
15958
|
const timer = setTimeout(() => {
|
|
15677
15959
|
const pending = this.pending.get(id);
|
|
15678
15960
|
if (!pending) return;
|
|
@@ -15684,7 +15966,7 @@ var SessionCatalogProjectClient = class {
|
|
|
15684
15966
|
);
|
|
15685
15967
|
}, timeoutMs);
|
|
15686
15968
|
timer.unref?.();
|
|
15687
|
-
this.pending.set(id, { resolve:
|
|
15969
|
+
this.pending.set(id, { resolve: resolve17, reject, timer });
|
|
15688
15970
|
socket.write(encoded);
|
|
15689
15971
|
});
|
|
15690
15972
|
}
|
|
@@ -16098,6 +16380,29 @@ var SessionSummaryTracker = class {
|
|
|
16098
16380
|
get currentSummary() {
|
|
16099
16381
|
return this.summary;
|
|
16100
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
|
+
}
|
|
16101
16406
|
get pendingToolUses() {
|
|
16102
16407
|
return Array.from(this.openToolUses);
|
|
16103
16408
|
}
|
|
@@ -16290,6 +16595,12 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
|
|
|
16290
16595
|
lastAppendWarnAt = 0;
|
|
16291
16596
|
writeChain = Promise.resolve();
|
|
16292
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;
|
|
16293
16604
|
get isClosed() {
|
|
16294
16605
|
return false;
|
|
16295
16606
|
}
|
|
@@ -16330,14 +16641,20 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
|
|
|
16330
16641
|
return true;
|
|
16331
16642
|
}
|
|
16332
16643
|
enqueueWrite(data) {
|
|
16644
|
+
return this.enqueueFlight({ data, stolen: false, started: false });
|
|
16645
|
+
}
|
|
16646
|
+
enqueueFlight(flight) {
|
|
16333
16647
|
const write = this.writeChain.then(async () => {
|
|
16648
|
+
if (flight.stolen) return;
|
|
16649
|
+
flight.started = true;
|
|
16650
|
+
if (flight.stolen) return;
|
|
16334
16651
|
try {
|
|
16335
|
-
return await this.opts.getHandle().appendFile(data, "utf8");
|
|
16652
|
+
return await this.opts.getHandle().appendFile(flight.data, "utf8");
|
|
16336
16653
|
} catch (err) {
|
|
16337
16654
|
if (isClosedHandleError(err)) {
|
|
16338
16655
|
const reloaded = await fsp7.open(this.opts.filePath, "a", 384);
|
|
16339
16656
|
this.opts.setHandle(reloaded);
|
|
16340
|
-
return await reloaded.appendFile(data, "utf8");
|
|
16657
|
+
return await reloaded.appendFile(flight.data, "utf8");
|
|
16341
16658
|
}
|
|
16342
16659
|
throw err;
|
|
16343
16660
|
}
|
|
@@ -16362,17 +16679,29 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
|
|
|
16362
16679
|
this.flushTimer = null;
|
|
16363
16680
|
}
|
|
16364
16681
|
}
|
|
16365
|
-
async flushBuffer(isClosed = false) {
|
|
16366
|
-
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
|
+
}
|
|
16367
16696
|
const flush = (async () => {
|
|
16368
|
-
while (this.writeBuffer.length > 0) await this.flushBufferOnce(isClosed);
|
|
16697
|
+
while (this.writeBuffer.length > 0) await this.flushBufferOnce(isClosed, opts);
|
|
16369
16698
|
})().finally(() => {
|
|
16370
16699
|
if (this.flushPromise === flush) this.flushPromise = null;
|
|
16371
16700
|
});
|
|
16372
16701
|
this.flushPromise = flush;
|
|
16373
16702
|
return flush;
|
|
16374
16703
|
}
|
|
16375
|
-
async flushBufferOnce(isClosed) {
|
|
16704
|
+
async flushBufferOnce(isClosed, opts) {
|
|
16376
16705
|
if (this.writeBuffer.length === 0) return;
|
|
16377
16706
|
const events = this.writeBuffer;
|
|
16378
16707
|
const eventCount = events.length;
|
|
@@ -16380,12 +16709,21 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
|
|
|
16380
16709
|
const batch = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
16381
16710
|
this.writeBuffer = [];
|
|
16382
16711
|
this.writeBufferBytes = 0;
|
|
16712
|
+
const flight = { data: batch, stolen: false, started: false };
|
|
16713
|
+
this.inFlight = flight;
|
|
16383
16714
|
const t0 = Date.now();
|
|
16384
16715
|
let outcome = "success";
|
|
16385
16716
|
let errorMsg;
|
|
16386
16717
|
try {
|
|
16387
|
-
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
|
+
}
|
|
16388
16725
|
} catch (err) {
|
|
16726
|
+
if (flight.stolen) return;
|
|
16389
16727
|
outcome = "failure";
|
|
16390
16728
|
errorMsg = toErrorMessage(err);
|
|
16391
16729
|
const newer = this.writeBuffer;
|
|
@@ -16423,6 +16761,7 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
|
|
|
16423
16761
|
...eventCount !== void 0 ? { eventCount } : {},
|
|
16424
16762
|
...this.opts.getTraceId?.() ? { traceId: this.opts.getTraceId() } : {}
|
|
16425
16763
|
});
|
|
16764
|
+
if (this.inFlight === flight) this.inFlight = null;
|
|
16426
16765
|
}
|
|
16427
16766
|
}
|
|
16428
16767
|
async drainWriteChain() {
|
|
@@ -16431,18 +16770,56 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
|
|
|
16431
16770
|
async drainFlushPromise() {
|
|
16432
16771
|
await this.flushPromise?.catch(() => void 0);
|
|
16433
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
|
+
*/
|
|
16434
16784
|
flushSync() {
|
|
16435
|
-
if (
|
|
16785
|
+
if (!this.opts.filePath) return;
|
|
16436
16786
|
this.cancelTimer();
|
|
16437
|
-
const
|
|
16438
|
-
|
|
16439
|
-
|
|
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;
|
|
16440
16799
|
let fd;
|
|
16441
16800
|
try {
|
|
16442
16801
|
fd = openSync(this.opts.filePath, "a");
|
|
16443
|
-
writeSync(fd,
|
|
16802
|
+
writeSync(fd, chunks.join(""), null, "utf8");
|
|
16444
16803
|
fsyncSync(fd);
|
|
16445
|
-
|
|
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
|
+
);
|
|
16446
16823
|
} finally {
|
|
16447
16824
|
if (fd !== void 0) {
|
|
16448
16825
|
try {
|
|
@@ -16610,6 +16987,17 @@ function isClosedHandleError2(err) {
|
|
|
16610
16987
|
const code = err?.code;
|
|
16611
16988
|
return code === "EBADF" || code === "ERR_CLOSED_RESOURCE" || code === "ERR_INVALID_HANDLE";
|
|
16612
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;
|
|
16613
17001
|
var FileSessionWriter = class _FileSessionWriter {
|
|
16614
17002
|
constructor(id, handle, startedAt, meta, events, opts = {}, traceId) {
|
|
16615
17003
|
this.id = id;
|
|
@@ -16625,6 +17013,8 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
16625
17013
|
this._onAppend = opts.onAppend;
|
|
16626
17014
|
this._onAppendBatch = opts.onAppendBatch;
|
|
16627
17015
|
this.onCloseCb = opts.onClose;
|
|
17016
|
+
this.onMetadataCheckpointCb = opts.onMetadataCheckpoint;
|
|
17017
|
+
this.metadataCheckpointMs = opts.metadataCheckpointMs ?? METADATA_CHECKPOINT_INTERVAL_MS;
|
|
16628
17018
|
this.summaryTracker = new SessionSummaryTracker({
|
|
16629
17019
|
id,
|
|
16630
17020
|
startedAt,
|
|
@@ -16692,6 +17082,15 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
16692
17082
|
this._onAppendBatch = cb;
|
|
16693
17083
|
}
|
|
16694
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;
|
|
16695
17094
|
/** Implements SessionWriter.traceId — propagated from ContextInit.traceId. */
|
|
16696
17095
|
traceId;
|
|
16697
17096
|
/**
|
|
@@ -16723,18 +17122,31 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
16723
17122
|
void this.ensureInit();
|
|
16724
17123
|
const appendEvent = scrubSessionWriterEvent(event, this.secretScrubber);
|
|
16725
17124
|
this.summaryTracker.observe(appendEvent);
|
|
17125
|
+
this.metadataDirty = true;
|
|
17126
|
+
this.scheduleMetadataCheckpoint();
|
|
16726
17127
|
try {
|
|
16727
17128
|
this._onAppend?.(appendEvent);
|
|
16728
17129
|
} catch {
|
|
16729
17130
|
}
|
|
17131
|
+
const critical = isCriticalEvent(appendEvent);
|
|
16730
17132
|
if (!this.buffer.push(appendEvent)) {
|
|
16731
17133
|
this.buffer.cancelTimer();
|
|
16732
|
-
void this.buffer.flushBuffer(this.closed).catch(() => void 0).then(() => {
|
|
16733
|
-
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);
|
|
16734
17146
|
});
|
|
16735
|
-
} else if (this.buffer.shouldFlushNow()) {
|
|
17147
|
+
} else if (critical || this.buffer.shouldFlushNow()) {
|
|
16736
17148
|
this.buffer.cancelTimer();
|
|
16737
|
-
void this.buffer.flushBuffer(this.closed).catch(() => {
|
|
17149
|
+
void this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => {
|
|
16738
17150
|
});
|
|
16739
17151
|
} else {
|
|
16740
17152
|
this.buffer.scheduleFlush(this.closed);
|
|
@@ -16805,23 +17217,112 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
16805
17217
|
provider: this.meta.provider ?? "unknown"
|
|
16806
17218
|
});
|
|
16807
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
|
+
}
|
|
16808
17299
|
async append(event) {
|
|
16809
17300
|
if (this.closed) return;
|
|
16810
17301
|
await this.ensureInit();
|
|
16811
17302
|
const scrubbed = scrubSessionWriterEvent(event, this.secretScrubber);
|
|
16812
17303
|
this.summaryTracker.observe(scrubbed);
|
|
17304
|
+
this.metadataDirty = true;
|
|
17305
|
+
this.scheduleMetadataCheckpoint();
|
|
16813
17306
|
try {
|
|
16814
17307
|
this._onAppend?.(scrubbed);
|
|
16815
17308
|
} catch {
|
|
16816
17309
|
}
|
|
16817
|
-
|
|
17310
|
+
let pushed = this.buffer.push(scrubbed);
|
|
17311
|
+
if (!pushed) {
|
|
16818
17312
|
this.buffer.cancelTimer();
|
|
16819
|
-
await this.buffer.flushBuffer(this.closed).catch(() => void 0);
|
|
16820
|
-
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
|
+
}
|
|
16821
17322
|
}
|
|
16822
|
-
if (this.buffer.shouldFlushNow()) {
|
|
17323
|
+
if (isCriticalEvent(scrubbed) || this.buffer.shouldFlushNow()) {
|
|
16823
17324
|
this.buffer.cancelTimer();
|
|
16824
|
-
await this.buffer.flushBuffer(this.closed).catch(() => {
|
|
17325
|
+
await this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => {
|
|
16825
17326
|
});
|
|
16826
17327
|
} else {
|
|
16827
17328
|
this.buffer.scheduleFlush(this.closed);
|
|
@@ -16838,20 +17339,33 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
16838
17339
|
this._onAppend?.(scrubbed);
|
|
16839
17340
|
} catch {
|
|
16840
17341
|
}
|
|
16841
|
-
|
|
17342
|
+
let pushed = this.buffer.push(scrubbed);
|
|
17343
|
+
if (!pushed) {
|
|
16842
17344
|
this.buffer.cancelTimer();
|
|
16843
|
-
await this.buffer.flushBuffer(this.closed).catch(() => void 0);
|
|
16844
|
-
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
|
+
}
|
|
16845
17354
|
}
|
|
16846
17355
|
scrubbedBatch.push(scrubbed);
|
|
16847
17356
|
}
|
|
17357
|
+
if (scrubbedBatch.length > 0) {
|
|
17358
|
+
this.metadataDirty = true;
|
|
17359
|
+
this.scheduleMetadataCheckpoint();
|
|
17360
|
+
}
|
|
16848
17361
|
try {
|
|
16849
17362
|
this._onAppendBatch?.(scrubbedBatch);
|
|
16850
17363
|
} catch {
|
|
16851
17364
|
}
|
|
16852
|
-
|
|
17365
|
+
const hasCritical = scrubbedBatch.some(isCriticalEvent);
|
|
17366
|
+
if (hasCritical || this.buffer.shouldFlushNow()) {
|
|
16853
17367
|
this.buffer.cancelTimer();
|
|
16854
|
-
await this.buffer.flushBuffer(this.closed).catch(() => {
|
|
17368
|
+
await this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => {
|
|
16855
17369
|
});
|
|
16856
17370
|
} else {
|
|
16857
17371
|
this.buffer.scheduleFlush(this.closed);
|
|
@@ -16859,8 +17373,10 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
16859
17373
|
}
|
|
16860
17374
|
/**
|
|
16861
17375
|
* Flush buffered events to disk immediately. Critical events
|
|
16862
|
-
* (user_input, llm_response
|
|
16863
|
-
*
|
|
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.
|
|
16864
17380
|
*
|
|
16865
17381
|
* Idempotent — cancels any pending timer, writes whatever has accumulated,
|
|
16866
17382
|
* then asks the OS to synchronize the file data before resolving. Even an
|
|
@@ -16869,7 +17385,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
16869
17385
|
async flush() {
|
|
16870
17386
|
if (this.closed) return;
|
|
16871
17387
|
this.buffer.cancelTimer();
|
|
16872
|
-
await this.buffer.flushBuffer(this.closed);
|
|
17388
|
+
await this.buffer.flushBuffer(this.closed, { datasync: true });
|
|
16873
17389
|
await this.buffer.drainWriteChain();
|
|
16874
17390
|
try {
|
|
16875
17391
|
await this.handle.datasync();
|
|
@@ -16885,23 +17401,35 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
16885
17401
|
* Last-gasp synchronous drain for hard-exit paths (process.exit after
|
|
16886
17402
|
* rapid Ctrl+C). The async write chain cannot be awaited when the process
|
|
16887
17403
|
* is about to die, but whatever still sits in the in-memory buffer CAN be
|
|
16888
|
-
* saved with a blocking append.
|
|
16889
|
-
*
|
|
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.
|
|
16890
17407
|
*/
|
|
16891
17408
|
flushSync() {
|
|
16892
17409
|
this.buffer.flushSync();
|
|
16893
17410
|
}
|
|
16894
17411
|
async close() {
|
|
16895
17412
|
if (this.closePromise) return this.closePromise;
|
|
16896
|
-
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);
|
|
16897
17419
|
this.closed = false;
|
|
16898
17420
|
this.closePromise = null;
|
|
16899
17421
|
if (this.buffer.length > 0) this.buffer.scheduleFlush(this.closed);
|
|
17422
|
+
if (this.metadataDirty) this.scheduleMetadataCheckpoint();
|
|
16900
17423
|
throw err;
|
|
16901
17424
|
});
|
|
16902
17425
|
return this.closePromise;
|
|
16903
17426
|
}
|
|
16904
17427
|
async doClose() {
|
|
17428
|
+
if (this.metadataTimer) {
|
|
17429
|
+
clearTimeout(this.metadataTimer);
|
|
17430
|
+
this.metadataTimer = null;
|
|
17431
|
+
}
|
|
17432
|
+
await this.metadataCheckpointInFlight?.catch(() => void 0);
|
|
16905
17433
|
await this.ensureInit();
|
|
16906
17434
|
if (this.pendingFileSnapshots.length > 0) {
|
|
16907
17435
|
await this.writeFileSnapshot(this.activePromptIndex ?? 0, [...this.pendingFileSnapshots]);
|
|
@@ -16909,8 +17437,13 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
16909
17437
|
this.pendingFileSnapshotBytes = 0;
|
|
16910
17438
|
}
|
|
16911
17439
|
this.closed = true;
|
|
17440
|
+
if (this.metadataTimer) {
|
|
17441
|
+
clearTimeout(this.metadataTimer);
|
|
17442
|
+
this.metadataTimer = null;
|
|
17443
|
+
}
|
|
17444
|
+
await this.metadataCheckpointInFlight?.catch(() => void 0);
|
|
16912
17445
|
this.buffer.cancelTimer();
|
|
16913
|
-
await this.buffer.flushBuffer(this.closed);
|
|
17446
|
+
await this.buffer.flushBuffer(this.closed, { datasync: true });
|
|
16914
17447
|
await this.buffer.drainWriteChain();
|
|
16915
17448
|
try {
|
|
16916
17449
|
await this.handle.datasync();
|
|
@@ -17025,10 +17558,23 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
17025
17558
|
async truncateToCheckpoint(targetPromptIndex, revertedFiles = []) {
|
|
17026
17559
|
if (!this.filePath) return 0;
|
|
17027
17560
|
this.buffer.cancelTimer();
|
|
17028
|
-
await this.buffer.flushBuffer(this.closed);
|
|
17561
|
+
await this.buffer.flushBuffer(this.closed, { datasync: true });
|
|
17029
17562
|
await this.buffer.drainWriteChain();
|
|
17030
|
-
|
|
17031
|
-
|
|
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
|
+
}
|
|
17032
17578
|
await this.buffer.drainWriteChain();
|
|
17033
17579
|
try {
|
|
17034
17580
|
await this.handle.close();
|
|
@@ -17039,6 +17585,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
17039
17585
|
this.handle = await fsp9.open(this.filePath, "a", 384);
|
|
17040
17586
|
} catch (err) {
|
|
17041
17587
|
this.handle = await fsp9.open(this.filePath, "a", 384).catch(() => this.handle);
|
|
17588
|
+
this.scheduleMetadataCheckpoint();
|
|
17042
17589
|
throw err;
|
|
17043
17590
|
}
|
|
17044
17591
|
await this.summaryTracker.recomputeFromDisk(this.filePath);
|
|
@@ -17064,6 +17611,11 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
17064
17611
|
await this.buffer.drainFlushPromise();
|
|
17065
17612
|
this.buffer.clear();
|
|
17066
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);
|
|
17067
17619
|
const resetAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
17068
17620
|
const record = `${JSON.stringify({
|
|
17069
17621
|
type: "session_start",
|
|
@@ -17074,8 +17626,10 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
17074
17626
|
})}
|
|
17075
17627
|
`;
|
|
17076
17628
|
await this.handle.close();
|
|
17077
|
-
await
|
|
17629
|
+
await atomicWrite(this.filePath, record, { mode: 384 });
|
|
17078
17630
|
this.summaryTracker.reset(resetAt);
|
|
17631
|
+
this.metadataDirty = true;
|
|
17632
|
+
this.scheduleMetadataCheckpoint();
|
|
17079
17633
|
this.activePromptIndex = null;
|
|
17080
17634
|
this.pendingFileSnapshots = [];
|
|
17081
17635
|
this.pendingFileSnapshotBytes = 0;
|
|
@@ -17466,7 +18020,7 @@ var SessionCheckpointCas = class {
|
|
|
17466
18020
|
}
|
|
17467
18021
|
};
|
|
17468
18022
|
function defaultRunGit(args, cwd) {
|
|
17469
|
-
return new Promise((
|
|
18023
|
+
return new Promise((resolve17) => {
|
|
17470
18024
|
const stdoutChunks = [];
|
|
17471
18025
|
const stderrChunks = [];
|
|
17472
18026
|
let stdoutBytes = 0;
|
|
@@ -17509,8 +18063,8 @@ function defaultRunGit(args, cwd) {
|
|
|
17509
18063
|
stdoutTruncated,
|
|
17510
18064
|
stderrTruncated
|
|
17511
18065
|
});
|
|
17512
|
-
child.on("error", (err) =>
|
|
17513
|
-
child.on("close", (code) =>
|
|
18066
|
+
child.on("error", (err) => resolve17(result(1, err.message)));
|
|
18067
|
+
child.on("close", (code) => resolve17(result(code ?? 1)));
|
|
17514
18068
|
});
|
|
17515
18069
|
}
|
|
17516
18070
|
|
|
@@ -18324,7 +18878,7 @@ async function executeRenameSession(params) {
|
|
|
18324
18878
|
|
|
18325
18879
|
// src/storage/session-store/resume-session.ts
|
|
18326
18880
|
import * as fsp16 from "node:fs/promises";
|
|
18327
|
-
import * as
|
|
18881
|
+
import * as path31 from "node:path";
|
|
18328
18882
|
|
|
18329
18883
|
// src/storage/session-resume-validation.ts
|
|
18330
18884
|
import { createHash as createHash5 } from "node:crypto";
|
|
@@ -18420,8 +18974,22 @@ async function validateResumeFileObservations(events, projectRoot) {
|
|
|
18420
18974
|
}
|
|
18421
18975
|
var RESUME_NOTICE_HEADERS = [
|
|
18422
18976
|
"[SESSION RESUME FILE VALIDATION]",
|
|
18423
|
-
"[SESSION RESUME INTERRUPTED WORK]"
|
|
18977
|
+
"[SESSION RESUME INTERRUPTED WORK]",
|
|
18978
|
+
"[SESSION RESUME CRASH RECOVERY]"
|
|
18424
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
|
+
}
|
|
18425
18993
|
function isResumeNoticeMessage(message) {
|
|
18426
18994
|
if (message.role !== "system" || typeof message.content !== "string") return false;
|
|
18427
18995
|
return RESUME_NOTICE_HEADERS.some((header) => message.content === header || message.content.startsWith(`${header}
|
|
@@ -18454,9 +19022,345 @@ function formatInterruptedToolNotice(pendingToolUseCount) {
|
|
|
18454
19022
|
].join("\n");
|
|
18455
19023
|
}
|
|
18456
19024
|
|
|
18457
|
-
// src/storage/session-
|
|
19025
|
+
// src/storage/session-recovery.ts
|
|
18458
19026
|
import { createReadStream as createReadStream4 } from "node:fs";
|
|
19027
|
+
import * as fs4 from "node:fs/promises";
|
|
19028
|
+
import * as path30 from "node:path";
|
|
18459
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";
|
|
18460
19364
|
async function summarizeSessionFile(opts) {
|
|
18461
19365
|
return summarizeSessionEventSequence({
|
|
18462
19366
|
id: opts.id,
|
|
@@ -18590,8 +19494,8 @@ async function summarizeSessionEventSequence(opts) {
|
|
|
18590
19494
|
}
|
|
18591
19495
|
}
|
|
18592
19496
|
async function* iterateSessionEvents(file, secretScrubber) {
|
|
18593
|
-
const stream =
|
|
18594
|
-
const lines =
|
|
19497
|
+
const stream = createReadStream5(file, { encoding: "utf8" });
|
|
19498
|
+
const lines = createInterface5({ input: stream, crlfDelay: Infinity });
|
|
18595
19499
|
try {
|
|
18596
19500
|
for await (const line of lines) {
|
|
18597
19501
|
if (!line.trim()) continue;
|
|
@@ -18624,7 +19528,8 @@ async function executeResumeSession(params) {
|
|
|
18624
19528
|
readSummaryManifest,
|
|
18625
19529
|
searchEvents,
|
|
18626
19530
|
persistCatalogSummary,
|
|
18627
|
-
logWarn
|
|
19531
|
+
logWarn,
|
|
19532
|
+
sessionsDir
|
|
18628
19533
|
} = params;
|
|
18629
19534
|
const t0 = Date.now();
|
|
18630
19535
|
const data = await load(canonicalId);
|
|
@@ -18645,6 +19550,24 @@ async function executeResumeSession(params) {
|
|
|
18645
19550
|
...derivedSummary,
|
|
18646
19551
|
...persistedSummary?.name !== void 0 ? { name: persistedSummary.name } : {}
|
|
18647
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);
|
|
18648
19571
|
const noticeMessages = [];
|
|
18649
19572
|
let resumeValidation;
|
|
18650
19573
|
if (projectRoot) {
|
|
@@ -18673,7 +19596,7 @@ async function executeResumeSession(params) {
|
|
|
18673
19596
|
);
|
|
18674
19597
|
}
|
|
18675
19598
|
}
|
|
18676
|
-
const interruptedNotice = formatInterruptedToolNotice(data.pendingToolUseCount ?? 0);
|
|
19599
|
+
const interruptedNotice = recoveryPlan.stale ? null : formatInterruptedToolNotice(data.pendingToolUseCount ?? 0);
|
|
18677
19600
|
if (interruptedNotice) {
|
|
18678
19601
|
noticeMessages.push({
|
|
18679
19602
|
role: "system",
|
|
@@ -18681,6 +19604,14 @@ async function executeResumeSession(params) {
|
|
|
18681
19604
|
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
18682
19605
|
});
|
|
18683
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
|
+
}
|
|
18684
19615
|
const carriedMessages = data.messages.filter((message) => !isResumeNoticeMessage(message));
|
|
18685
19616
|
const resumedData = {
|
|
18686
19617
|
...data,
|
|
@@ -18711,7 +19642,7 @@ async function executeResumeSession(params) {
|
|
|
18711
19642
|
{
|
|
18712
19643
|
resumed: true,
|
|
18713
19644
|
initialSummary,
|
|
18714
|
-
dir:
|
|
19645
|
+
dir: path31.dirname(file),
|
|
18715
19646
|
filePath: file,
|
|
18716
19647
|
secretScrubber,
|
|
18717
19648
|
checkpointCas,
|
|
@@ -18722,9 +19653,21 @@ async function executeResumeSession(params) {
|
|
|
18722
19653
|
if (!current) return null;
|
|
18723
19654
|
return current.name === void 0 ? {} : { name: sessionContentText(secretScrubber.scrub(current.name)) };
|
|
18724
19655
|
},
|
|
18725
|
-
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)
|
|
18726
19661
|
}
|
|
18727
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
|
+
}
|
|
18728
19671
|
emitSessionStoreWrite(events, canonicalId, file, "resume", "success", Date.now() - t0);
|
|
18729
19672
|
return { writer, data: resumedData };
|
|
18730
19673
|
} catch (err) {
|
|
@@ -18943,6 +19886,11 @@ function applySessionIndexLines(raw, byId, deleted) {
|
|
|
18943
19886
|
byId.delete(entry.id);
|
|
18944
19887
|
continue;
|
|
18945
19888
|
}
|
|
19889
|
+
if (entry.action === "create" && entry.id) {
|
|
19890
|
+
deleted.delete(entry.id);
|
|
19891
|
+
byId.delete(entry.id);
|
|
19892
|
+
continue;
|
|
19893
|
+
}
|
|
18946
19894
|
if (entry.id && !deleted.has(entry.id)) {
|
|
18947
19895
|
byId.set(entry.id, entry);
|
|
18948
19896
|
}
|
|
@@ -18987,6 +19935,7 @@ async function readFileRange(file, start, end) {
|
|
|
18987
19935
|
|
|
18988
19936
|
// src/storage/session-store/session-store-index.ts
|
|
18989
19937
|
var COMPACT_EVERY = 30;
|
|
19938
|
+
var NO_DELETED_IDS = /* @__PURE__ */ new Set();
|
|
18990
19939
|
async function appendToIndexStrict(dir, indexFile, summary, invalidateShard, onAppended, compactInner) {
|
|
18991
19940
|
await ensureDir(dir);
|
|
18992
19941
|
let shouldCompact = false;
|
|
@@ -19019,10 +19968,14 @@ async function readIndexFile(indexFile, currentCache) {
|
|
|
19019
19968
|
const s = await fsp20.stat(indexFile);
|
|
19020
19969
|
stat13 = { mtimeMs: s.mtimeMs, size: s.size, ino: s.ino, birthtimeMs: s.birthtimeMs };
|
|
19021
19970
|
} catch {
|
|
19022
|
-
return { summaries: [], cache: null };
|
|
19971
|
+
return { summaries: [], deletedIds: NO_DELETED_IDS, cache: null };
|
|
19023
19972
|
}
|
|
19024
19973
|
if (currentCache !== null && currentCache.mtimeMs === stat13.mtimeMs && currentCache.size === stat13.size && currentCache.ino === stat13.ino && currentCache.birthtimeMs === stat13.birthtimeMs) {
|
|
19025
|
-
return {
|
|
19974
|
+
return {
|
|
19975
|
+
summaries: currentCache.summaries,
|
|
19976
|
+
deletedIds: currentCache.deleted,
|
|
19977
|
+
cache: currentCache
|
|
19978
|
+
};
|
|
19026
19979
|
}
|
|
19027
19980
|
const cached = currentCache;
|
|
19028
19981
|
const sameFile = cached !== null && cached.ino === stat13.ino && cached.birthtimeMs === stat13.birthtimeMs;
|
|
@@ -19038,14 +19991,14 @@ async function readIndexFile(indexFile, currentCache) {
|
|
|
19038
19991
|
byId: cached.byId,
|
|
19039
19992
|
deleted: cached.deleted
|
|
19040
19993
|
};
|
|
19041
|
-
return { summaries: summaries2, cache: nextCache2 };
|
|
19994
|
+
return { summaries: summaries2, deletedIds: cached.deleted, cache: nextCache2 };
|
|
19042
19995
|
}
|
|
19043
19996
|
}
|
|
19044
19997
|
let raw;
|
|
19045
19998
|
try {
|
|
19046
19999
|
raw = await fsp20.readFile(indexFile, "utf8");
|
|
19047
20000
|
} catch {
|
|
19048
|
-
return { summaries: [], cache: null };
|
|
20001
|
+
return { summaries: [], deletedIds: NO_DELETED_IDS, cache: null };
|
|
19049
20002
|
}
|
|
19050
20003
|
const deleted = /* @__PURE__ */ new Set();
|
|
19051
20004
|
const byId = /* @__PURE__ */ new Map();
|
|
@@ -19053,7 +20006,18 @@ async function readIndexFile(indexFile, currentCache) {
|
|
|
19053
20006
|
const summaries = Array.from(byId.values());
|
|
19054
20007
|
summaries.sort(compareSessionSummaries);
|
|
19055
20008
|
const nextCache = { ...stat13, summaries, byId, deleted };
|
|
19056
|
-
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 });
|
|
19057
20021
|
}
|
|
19058
20022
|
|
|
19059
20023
|
// src/storage/session-store/shard-manifest.ts
|
|
@@ -19091,12 +20055,12 @@ async function readOrBuildShardManifestEntry(opts) {
|
|
|
19091
20055
|
}
|
|
19092
20056
|
|
|
19093
20057
|
// src/storage/session-store/strict-empty-check.ts
|
|
19094
|
-
import { createReadStream as
|
|
19095
|
-
import { createInterface as
|
|
20058
|
+
import { createReadStream as createReadStream6 } from "node:fs";
|
|
20059
|
+
import { createInterface as createInterface6 } from "node:readline";
|
|
19096
20060
|
var EMPTY_SESSION_EVENT_TYPES = /* @__PURE__ */ new Set(["session_start", "session_resumed", "session_end"]);
|
|
19097
20061
|
async function isStrictlyEmptySessionFile(file) {
|
|
19098
|
-
const input =
|
|
19099
|
-
const lines =
|
|
20062
|
+
const input = createReadStream6(file, { encoding: "utf8" });
|
|
20063
|
+
const lines = createInterface6({ input, crlfDelay: Infinity });
|
|
19100
20064
|
let sawSessionStart = false;
|
|
19101
20065
|
try {
|
|
19102
20066
|
for await (const line of lines) {
|
|
@@ -19172,6 +20136,7 @@ function damagedSummary(id, startedAt) {
|
|
|
19172
20136
|
}
|
|
19173
20137
|
|
|
19174
20138
|
// src/storage/session-store.ts
|
|
20139
|
+
var SESSION_FILTER_POOL_LIMIT = 1e4;
|
|
19175
20140
|
var DefaultSessionStore = class _DefaultSessionStore {
|
|
19176
20141
|
dir;
|
|
19177
20142
|
events;
|
|
@@ -19187,14 +20152,36 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
19187
20152
|
_loadCache = /* @__PURE__ */ new Map();
|
|
19188
20153
|
loadCache = new SessionLoadCache(this._loadCache);
|
|
19189
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();
|
|
19190
20177
|
shardManifestCache = /* @__PURE__ */ new Map();
|
|
19191
20178
|
static LIST_SCAN_CONCURRENCY = 32;
|
|
19192
20179
|
indexAppendCount = 0;
|
|
19193
20180
|
constructor(opts) {
|
|
19194
20181
|
this.dir = opts.dir;
|
|
19195
|
-
this.projectRoot = opts.projectRoot ?
|
|
20182
|
+
this.projectRoot = opts.projectRoot ? path32.resolve(opts.projectRoot) : void 0;
|
|
19196
20183
|
this.checkpointCas = this.projectRoot ? new SessionCheckpointCas({
|
|
19197
|
-
rootDir:
|
|
20184
|
+
rootDir: path32.join(this.dir, "_cas"),
|
|
19198
20185
|
projectRoot: this.projectRoot
|
|
19199
20186
|
}) : void 0;
|
|
19200
20187
|
this.events = opts.events;
|
|
@@ -19205,7 +20192,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
19205
20192
|
this.onAppendBatch = opts.onAppendBatch;
|
|
19206
20193
|
const builtRuntime = import.meta.url.includes("/dist/");
|
|
19207
20194
|
this.catalogClient = this.projectRoot && (builtRuntime || process.env["WRONGSTACK_SESSION_CATALOG_FORCE"] === "1") && resolveSessionCatalogProjectServerUrl() ? new SessionCatalogProjectClient({
|
|
19208
|
-
projectDir:
|
|
20195
|
+
projectDir: path32.dirname(this.dir),
|
|
19209
20196
|
projectRoot: this.projectRoot
|
|
19210
20197
|
}) : void 0;
|
|
19211
20198
|
}
|
|
@@ -19227,7 +20214,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
19227
20214
|
this.clearLoadCache();
|
|
19228
20215
|
}
|
|
19229
20216
|
get indexFile() {
|
|
19230
|
-
return
|
|
20217
|
+
return path32.join(this.dir, "_index.jsonl");
|
|
19231
20218
|
}
|
|
19232
20219
|
sessionPath(id, ext) {
|
|
19233
20220
|
return sessionPath(this.dir, id, ext);
|
|
@@ -19253,19 +20240,94 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
19253
20240
|
async ensureShardDir(id) {
|
|
19254
20241
|
return ensureShardDir(this.dir, id);
|
|
19255
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
|
+
*/
|
|
19256
20254
|
async create(meta) {
|
|
19257
20255
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
19258
20256
|
const id = meta.id && meta.id.length > 0 ? meta.id : generateSessionId(startedAt);
|
|
19259
20257
|
const shardDir = await this.ensureShardDir(id);
|
|
19260
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
|
+
}
|
|
19261
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
|
+
}
|
|
19262
20323
|
let handle;
|
|
19263
20324
|
try {
|
|
19264
|
-
handle = await fsp23.open(file, "
|
|
20325
|
+
handle = await fsp23.open(file, "w", 384);
|
|
19265
20326
|
} catch (err) {
|
|
19266
20327
|
emitSessionStoreError(this.events, id, file, "create", toErrorMessage(err), false);
|
|
19267
20328
|
throw new Error(`Failed to open session file: ${toErrorMessage(err)}`, { cause: err });
|
|
19268
20329
|
}
|
|
20330
|
+
await this.invalidateShardManifestBySessionId(id).catch(() => void 0);
|
|
19269
20331
|
try {
|
|
19270
20332
|
const writer = new FileSessionWriter(id, handle, startedAt, meta, this.events, {
|
|
19271
20333
|
dir: shardDir,
|
|
@@ -19279,23 +20341,11 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
19279
20341
|
if (!current) return null;
|
|
19280
20342
|
return current.name === void 0 ? {} : { name: sessionContentText(this.secretScrubber.scrub(current.name)) };
|
|
19281
20343
|
},
|
|
19282
|
-
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)
|
|
19283
20348
|
});
|
|
19284
|
-
if (this.catalogClient) {
|
|
19285
|
-
await this.catalogClient.call("upsert_summary", {
|
|
19286
|
-
summary: {
|
|
19287
|
-
id,
|
|
19288
|
-
title: meta.title ?? "",
|
|
19289
|
-
startedAt,
|
|
19290
|
-
model: meta.model ?? "",
|
|
19291
|
-
provider: meta.provider ?? "",
|
|
19292
|
-
tokenTotal: 0,
|
|
19293
|
-
lastActivityAt: startedAt
|
|
19294
|
-
},
|
|
19295
|
-
transcriptRelativePath: `${id}.jsonl`,
|
|
19296
|
-
summaryRelativePath: `${id}.summary.json`
|
|
19297
|
-
});
|
|
19298
|
-
}
|
|
19299
20349
|
emitSessionStoreWrite(this.events, id, file, "create", "success", Date.now() - t0);
|
|
19300
20350
|
return writer;
|
|
19301
20351
|
} catch (err) {
|
|
@@ -19357,7 +20407,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
19357
20407
|
readSummaryManifest: (summaryId) => this.readSummaryManifest(summaryId),
|
|
19358
20408
|
searchEvents: (searchId, pred) => this.searchEvents(searchId, pred),
|
|
19359
20409
|
persistCatalogSummary: (sum) => this.persistCatalogSummary(sum),
|
|
19360
|
-
logWarn: (msg, ctx) => this.logWarn(msg, ctx)
|
|
20410
|
+
logWarn: (msg, ctx) => this.logWarn(msg, ctx),
|
|
20411
|
+
sessionsDir: this.dir
|
|
19361
20412
|
});
|
|
19362
20413
|
}
|
|
19363
20414
|
async load(id) {
|
|
@@ -19431,11 +20482,14 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
19431
20482
|
return this.scrubSummaries(records);
|
|
19432
20483
|
}
|
|
19433
20484
|
try {
|
|
19434
|
-
const indexed = await
|
|
19435
|
-
|
|
19436
|
-
|
|
19437
|
-
|
|
19438
|
-
|
|
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));
|
|
19439
20493
|
} catch {
|
|
19440
20494
|
return [];
|
|
19441
20495
|
}
|
|
@@ -19450,15 +20504,14 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
19450
20504
|
return this.scrubSummaries(records);
|
|
19451
20505
|
}
|
|
19452
20506
|
try {
|
|
19453
|
-
const indexed = await
|
|
19454
|
-
|
|
19455
|
-
|
|
19456
|
-
|
|
19457
|
-
|
|
19458
|
-
|
|
19459
|
-
|
|
19460
|
-
);
|
|
19461
|
-
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);
|
|
19462
20515
|
} catch {
|
|
19463
20516
|
return [];
|
|
19464
20517
|
}
|
|
@@ -19500,6 +20553,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
19500
20553
|
id,
|
|
19501
20554
|
(sid) => this.invalidateShardManifestBySessionId(sid),
|
|
19502
20555
|
() => {
|
|
20556
|
+
this._manualTombstones.add(id);
|
|
20557
|
+
this._indexDeletedIds.add(id);
|
|
19503
20558
|
this._indexCache = null;
|
|
19504
20559
|
this.indexAppendCount++;
|
|
19505
20560
|
}
|
|
@@ -19527,34 +20582,92 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
19527
20582
|
);
|
|
19528
20583
|
}
|
|
19529
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
|
+
*/
|
|
19530
20603
|
async compactIndexInner() {
|
|
19531
20604
|
const entries = await this.readIndex();
|
|
19532
|
-
|
|
19533
|
-
const lines = entries.map((s) => JSON.stringify(s)).join("\n") + "\n";
|
|
19534
|
-
await atomicWrite(this.indexFile, lines, { mode: 384 });
|
|
20605
|
+
await compactIndexInner(this.indexFile, entries, this._indexFileDeletedIds);
|
|
19535
20606
|
this._indexCache = null;
|
|
19536
20607
|
}
|
|
19537
20608
|
async readIndex() {
|
|
19538
|
-
const { summaries, cache } = await readIndexFile(this.indexFile, this._indexCache);
|
|
20609
|
+
const { summaries, deletedIds, cache } = await readIndexFile(this.indexFile, this._indexCache);
|
|
19539
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;
|
|
19540
20618
|
return summaries;
|
|
19541
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
|
+
*/
|
|
19542
20644
|
async rebuildIndex() {
|
|
19543
20645
|
if (this.catalogClient) {
|
|
19544
20646
|
const result = await this.catalogClient.call("rebuild_catalog", {}, { timeoutMs: 12e4 });
|
|
19545
20647
|
return result.indexed;
|
|
19546
20648
|
}
|
|
19547
|
-
|
|
19548
|
-
|
|
19549
|
-
ids
|
|
19550
|
-
|
|
19551
|
-
|
|
19552
|
-
|
|
19553
|
-
|
|
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";
|
|
19554
20667
|
await atomicWrite(this.indexFile, lines, { mode: 384 });
|
|
19555
20668
|
this._indexCache = null;
|
|
20669
|
+
return valid.length - tombstoned;
|
|
19556
20670
|
});
|
|
19557
|
-
return valid.length;
|
|
19558
20671
|
}
|
|
19559
20672
|
async listFromDirectoryScan(limit) {
|
|
19560
20673
|
const shardKeys = await this.collectShardKeys();
|
|
@@ -19636,7 +20749,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
19636
20749
|
return void 0;
|
|
19637
20750
|
}
|
|
19638
20751
|
async collectSessionFilesInShard(shardKey) {
|
|
19639
|
-
const dir = shardKey ?
|
|
20752
|
+
const dir = shardKey ? path32.join(this.dir, shardKey) : this.dir;
|
|
19640
20753
|
const entries = await this.collectSessionFiles(dir, shardKey);
|
|
19641
20754
|
return shardKey ? entries.filter((entry) => entry.id.startsWith(`${shardKey}/`)) : entries.filter((entry) => !entry.id.includes("/"));
|
|
19642
20755
|
}
|
|
@@ -19812,9 +20925,9 @@ function makeDirectorSessionFactory(opts) {
|
|
|
19812
20925
|
let dir;
|
|
19813
20926
|
if (opts.store) {
|
|
19814
20927
|
store = opts.store;
|
|
19815
|
-
dir = opts.sessionsRoot ?
|
|
20928
|
+
dir = opts.sessionsRoot ? path33.join(opts.sessionsRoot, runId) : "(caller-managed)";
|
|
19816
20929
|
} else if (opts.sessionsRoot) {
|
|
19817
|
-
dir =
|
|
20930
|
+
dir = path33.join(opts.sessionsRoot, runId);
|
|
19818
20931
|
store = new DefaultSessionStore({ dir });
|
|
19819
20932
|
} else {
|
|
19820
20933
|
throw new Error("makeDirectorSessionFactory requires either `store` or `sessionsRoot`");
|
|
@@ -19838,7 +20951,7 @@ function makeDirectorSessionFactory(opts) {
|
|
|
19838
20951
|
}
|
|
19839
20952
|
async function readDirectorSubagentSession(args) {
|
|
19840
20953
|
if (!args.sessionsRoot) return null;
|
|
19841
|
-
const filePath =
|
|
20954
|
+
const filePath = path33.join(args.sessionsRoot, args.directorRunId, `${args.subagentId}.jsonl`);
|
|
19842
20955
|
let raw;
|
|
19843
20956
|
try {
|
|
19844
20957
|
raw = await fsp24.readFile(filePath, "utf8");
|
|
@@ -21538,12 +22651,12 @@ async function executeSubagentWithTimeout({
|
|
|
21538
22651
|
}
|
|
21539
22652
|
return new Promise((resolveDecision) => {
|
|
21540
22653
|
let settled = false;
|
|
21541
|
-
const
|
|
22654
|
+
const resolve17 = (d) => {
|
|
21542
22655
|
if (settled) return;
|
|
21543
22656
|
settled = true;
|
|
21544
22657
|
resolveDecision(d);
|
|
21545
22658
|
};
|
|
21546
|
-
const fallback = setTimeout(() =>
|
|
22659
|
+
const fallback = setTimeout(() => resolve17("stop"), DECISION_TIMEOUT_MS);
|
|
21547
22660
|
const sessionId = currentSessionId();
|
|
21548
22661
|
budget._events?.emit("budget.threshold_reached", {
|
|
21549
22662
|
...sessionId ? { sessionId } : {},
|
|
@@ -21553,11 +22666,11 @@ async function executeSubagentWithTimeout({
|
|
|
21553
22666
|
timeoutMs: DECISION_TIMEOUT_MS,
|
|
21554
22667
|
extend: (extra) => {
|
|
21555
22668
|
clearTimeout(fallback);
|
|
21556
|
-
queueMicrotask(() =>
|
|
22669
|
+
queueMicrotask(() => resolve17({ extend: extra }));
|
|
21557
22670
|
},
|
|
21558
22671
|
deny: () => {
|
|
21559
22672
|
clearTimeout(fallback);
|
|
21560
|
-
|
|
22673
|
+
resolve17("stop");
|
|
21561
22674
|
}
|
|
21562
22675
|
});
|
|
21563
22676
|
});
|
|
@@ -21964,7 +23077,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
21964
23077
|
taskIds.map((id) => {
|
|
21965
23078
|
const cached = this.completedResults.find((r) => r.taskId === id);
|
|
21966
23079
|
if (cached) return cached;
|
|
21967
|
-
return new Promise((
|
|
23080
|
+
return new Promise((resolve17, reject) => {
|
|
21968
23081
|
const timeout = setTimeout(() => {
|
|
21969
23082
|
this.off("task.completed", handler);
|
|
21970
23083
|
reject(new Error(`awaitTasks timed out waiting for task "${id}"`));
|
|
@@ -21973,7 +23086,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
21973
23086
|
if (result.taskId === id) {
|
|
21974
23087
|
clearTimeout(timeout);
|
|
21975
23088
|
this.off("task.completed", handler);
|
|
21976
|
-
|
|
23089
|
+
resolve17(result);
|
|
21977
23090
|
}
|
|
21978
23091
|
};
|
|
21979
23092
|
this.on("task.completed", handler);
|
|
@@ -21998,13 +23111,13 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
21998
23111
|
const done = new Set(completed.map((r) => r.taskId));
|
|
21999
23112
|
return { completed, pending: taskIds.filter((id) => !done.has(id)) };
|
|
22000
23113
|
}
|
|
22001
|
-
return new Promise((
|
|
23114
|
+
return new Promise((resolve17) => {
|
|
22002
23115
|
let timer;
|
|
22003
23116
|
const handler = ({ result }) => {
|
|
22004
23117
|
if (!ids.has(result.taskId)) return;
|
|
22005
23118
|
if (timer) clearTimeout(timer);
|
|
22006
23119
|
this.off("task.completed", handler);
|
|
22007
|
-
|
|
23120
|
+
resolve17({
|
|
22008
23121
|
completed: [result],
|
|
22009
23122
|
pending: taskIds.filter((id) => id !== result.taskId)
|
|
22010
23123
|
});
|
|
@@ -22012,7 +23125,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
22012
23125
|
if (opts?.timeoutMs !== void 0) {
|
|
22013
23126
|
timer = setTimeout(() => {
|
|
22014
23127
|
this.off("task.completed", handler);
|
|
22015
|
-
|
|
23128
|
+
resolve17({ completed: [], pending: [...taskIds], timedOut: true });
|
|
22016
23129
|
}, opts.timeoutMs);
|
|
22017
23130
|
}
|
|
22018
23131
|
this.on("task.completed", handler);
|
|
@@ -22612,6 +23725,7 @@ function worktreeOwnerLabel(task, config) {
|
|
|
22612
23725
|
}
|
|
22613
23726
|
|
|
22614
23727
|
// src/coordination/director.ts
|
|
23728
|
+
var BUSY_REARM_FLOOR_MS = 1e3;
|
|
22615
23729
|
var Director = class _Director {
|
|
22616
23730
|
/* eslint-disable-next-line @typescript-eslint/no-unused-vars — just a cast helper */
|
|
22617
23731
|
static _asManifestEntry(v) {
|
|
@@ -22677,6 +23791,13 @@ var Director = class _Director {
|
|
|
22677
23791
|
subagentIdleTimeoutMs;
|
|
22678
23792
|
retireSubagentOnTaskComplete;
|
|
22679
23793
|
subagentIdleTimers = /* @__PURE__ */ new Map();
|
|
23794
|
+
/**
|
|
23795
|
+
* Effective idle window per subagent (spawn-time `idleTimeoutMs` override
|
|
23796
|
+
* or the Director-wide default; undefined = no window). Internal-task
|
|
23797
|
+
* completion re-arms with THIS value, not the Director-wide default, so
|
|
23798
|
+
* a subagent-configured window survives its first internal probe.
|
|
23799
|
+
*/
|
|
23800
|
+
subagentIdleDelayMs = /* @__PURE__ */ new Map();
|
|
22680
23801
|
sharedScratchpadPath;
|
|
22681
23802
|
maxSpawns;
|
|
22682
23803
|
maxSpawnDepth;
|
|
@@ -22833,7 +23954,13 @@ var Director = class _Director {
|
|
|
22833
23954
|
handleTaskCompleted(payload) {
|
|
22834
23955
|
const r = payload.result;
|
|
22835
23956
|
const settled = this.tasks.settle(r);
|
|
22836
|
-
if (settled.internal)
|
|
23957
|
+
if (settled.internal) {
|
|
23958
|
+
this.armSubagentIdleRetirement(
|
|
23959
|
+
r.subagentId,
|
|
23960
|
+
this.subagentIdleDelayMs.get(r.subagentId) ?? this.subagentIdleTimeoutMs
|
|
23961
|
+
);
|
|
23962
|
+
return;
|
|
23963
|
+
}
|
|
22837
23964
|
const title = this.tasks.descriptionFor(r.taskId, payload.task.description ?? r.taskId);
|
|
22838
23965
|
if (!settled.consumedInBand && this.taskResultNotifier) {
|
|
22839
23966
|
const resultText = typeof r.result === "string" ? r.result : r.result !== void 0 ? safeStringify(r.result) : void 0;
|
|
@@ -22898,7 +24025,7 @@ var Director = class _Director {
|
|
|
22898
24025
|
}
|
|
22899
24026
|
this.armSubagentIdleRetirement(
|
|
22900
24027
|
r.subagentId,
|
|
22901
|
-
this.retireSubagentOnTaskComplete ? 0 : this.subagentIdleTimeoutMs
|
|
24028
|
+
this.retireSubagentOnTaskComplete ? 0 : this.subagentIdleDelayMs.get(r.subagentId) ?? this.subagentIdleTimeoutMs
|
|
22902
24029
|
);
|
|
22903
24030
|
}
|
|
22904
24031
|
extensionsFor(subagentId) {
|
|
@@ -22984,6 +24111,7 @@ var Director = class _Director {
|
|
|
22984
24111
|
this.resolveSpawnModel(config);
|
|
22985
24112
|
const subagentId = await spawn4(this, config, priceLookup);
|
|
22986
24113
|
const perSubagentIdleMs = typeof config.idleTimeoutMs === "number" && Number.isFinite(config.idleTimeoutMs) && config.idleTimeoutMs >= 0 ? config.idleTimeoutMs : this.subagentIdleTimeoutMs;
|
|
24114
|
+
this.subagentIdleDelayMs.set(subagentId, perSubagentIdleMs);
|
|
22987
24115
|
this.armSubagentIdleRetirement(subagentId, perSubagentIdleMs);
|
|
22988
24116
|
return subagentId;
|
|
22989
24117
|
}
|
|
@@ -23037,6 +24165,7 @@ var Director = class _Director {
|
|
|
23037
24165
|
this.budgetPolicy.dispose();
|
|
23038
24166
|
for (const timer of this.subagentIdleTimers.values()) clearTimeout(timer);
|
|
23039
24167
|
this.subagentIdleTimers.clear();
|
|
24168
|
+
this.subagentIdleDelayMs.clear();
|
|
23040
24169
|
await this.coordinator.stopAll();
|
|
23041
24170
|
this.tasks.resolveWaitersOnShutdown();
|
|
23042
24171
|
for (const b of this.subagentBridges.values()) {
|
|
@@ -23095,6 +24224,7 @@ var Director = class _Director {
|
|
|
23095
24224
|
}
|
|
23096
24225
|
async remove(subagentId) {
|
|
23097
24226
|
this.clearSubagentIdleRetirement(subagentId);
|
|
24227
|
+
this.subagentIdleDelayMs.delete(subagentId);
|
|
23098
24228
|
void this.appendSessionEvent({
|
|
23099
24229
|
type: "agent_stopped",
|
|
23100
24230
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -23147,9 +24277,13 @@ var Director = class _Director {
|
|
|
23147
24277
|
const timer = setTimeout(() => {
|
|
23148
24278
|
this.subagentIdleTimers.delete(subagentId);
|
|
23149
24279
|
const entry = this.coordinator.getStatus().subagents.find((a) => a.id === subagentId);
|
|
23150
|
-
if (entry
|
|
24280
|
+
if (entry === void 0) return;
|
|
24281
|
+
if (entry.status !== "idle") {
|
|
24282
|
+
this.armSubagentIdleRetirement(subagentId, Math.max(delayMs, BUSY_REARM_FLOOR_MS));
|
|
24283
|
+
return;
|
|
24284
|
+
}
|
|
23151
24285
|
if (this.coordinator.listPendingTasks().some((task) => task.subagentId === subagentId)) {
|
|
23152
|
-
this.armSubagentIdleRetirement(subagentId,
|
|
24286
|
+
this.armSubagentIdleRetirement(subagentId, Math.max(delayMs, BUSY_REARM_FLOOR_MS));
|
|
23153
24287
|
return;
|
|
23154
24288
|
}
|
|
23155
24289
|
void this.remove(subagentId).catch(
|
|
@@ -23259,7 +24393,7 @@ var Director = class _Director {
|
|
|
23259
24393
|
// src/coordination/fleet-manager.ts
|
|
23260
24394
|
import { randomUUID as randomUUID17 } from "node:crypto";
|
|
23261
24395
|
import * as fsp26 from "node:fs/promises";
|
|
23262
|
-
import * as
|
|
24396
|
+
import * as path34 from "node:path";
|
|
23263
24397
|
var FleetManager = class {
|
|
23264
24398
|
/** The fleet-wide event bus. */
|
|
23265
24399
|
fleet;
|
|
@@ -23593,7 +24727,7 @@ var FleetManager = class {
|
|
|
23593
24727
|
})),
|
|
23594
24728
|
usage: this.usage.snapshot()
|
|
23595
24729
|
};
|
|
23596
|
-
await fsp26.mkdir(
|
|
24730
|
+
await fsp26.mkdir(path34.dirname(this.manifestPath), { recursive: true });
|
|
23597
24731
|
await atomicWrite(this.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
|
|
23598
24732
|
return this.manifestPath;
|
|
23599
24733
|
}
|
|
@@ -23756,7 +24890,7 @@ var FleetManager = class {
|
|
|
23756
24890
|
};
|
|
23757
24891
|
|
|
23758
24892
|
// src/coordination/remote-mailbox.ts
|
|
23759
|
-
import * as
|
|
24893
|
+
import * as path38 from "node:path";
|
|
23760
24894
|
|
|
23761
24895
|
// src/coordination/mailbox-constants.ts
|
|
23762
24896
|
var HQ_MAILBOX_SNAPSHOT_MIN_INTERVAL_MS = 1e4;
|
|
@@ -23766,15 +24900,15 @@ var MAILBOX_MAX_ACK_BATCH = 500;
|
|
|
23766
24900
|
|
|
23767
24901
|
// src/coordination/mailbox-project-server-client.ts
|
|
23768
24902
|
import { spawn as spawn5 } from "node:child_process";
|
|
23769
|
-
import * as
|
|
24903
|
+
import * as fs5 from "node:fs";
|
|
23770
24904
|
import * as net2 from "node:net";
|
|
23771
|
-
import * as
|
|
24905
|
+
import * as path36 from "node:path";
|
|
23772
24906
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
23773
24907
|
|
|
23774
24908
|
// src/coordination/mailbox-project-server-endpoint.ts
|
|
23775
24909
|
import { createHash as createHash6 } from "node:crypto";
|
|
23776
24910
|
import * as os3 from "node:os";
|
|
23777
|
-
import * as
|
|
24911
|
+
import * as path35 from "node:path";
|
|
23778
24912
|
|
|
23779
24913
|
// src/coordination/mailbox-project-server-protocol.ts
|
|
23780
24914
|
var MAILBOX_PROJECT_SERVER_PROTOCOL_VERSION = 4;
|
|
@@ -23807,7 +24941,7 @@ function encodeMailboxProjectServerMessage(message) {
|
|
|
23807
24941
|
// src/coordination/mailbox-project-server-endpoint.ts
|
|
23808
24942
|
var MAILBOX_PROJECT_SERVER_METADATA_FILE = ".mailbox-server.json";
|
|
23809
24943
|
function normalizeLocalPath(value) {
|
|
23810
|
-
const resolved =
|
|
24944
|
+
const resolved = path35.resolve(value);
|
|
23811
24945
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
23812
24946
|
}
|
|
23813
24947
|
function mailboxProjectServerKey(projectDir) {
|
|
@@ -23818,14 +24952,14 @@ function mailboxProjectServerEndpoint(projectDir) {
|
|
|
23818
24952
|
if (process.platform === "win32") {
|
|
23819
24953
|
return `\\\\.\\pipe\\wrongstack-mailbox-v${MAILBOX_PROJECT_SERVER_PROTOCOL_VERSION}-${key}`;
|
|
23820
24954
|
}
|
|
23821
|
-
return
|
|
24955
|
+
return path35.join(
|
|
23822
24956
|
os3.tmpdir(),
|
|
23823
24957
|
`wsmb-v${MAILBOX_PROJECT_SERVER_PROTOCOL_VERSION}`,
|
|
23824
24958
|
`${key}.sock`
|
|
23825
24959
|
);
|
|
23826
24960
|
}
|
|
23827
24961
|
function mailboxProjectServerMetadataPath(projectDir) {
|
|
23828
|
-
return
|
|
24962
|
+
return path35.join(path35.resolve(projectDir), MAILBOX_PROJECT_SERVER_METADATA_FILE);
|
|
23829
24963
|
}
|
|
23830
24964
|
|
|
23831
24965
|
// src/coordination/mailbox-project-server-client.ts
|
|
@@ -23836,7 +24970,7 @@ var DEFAULT_HEARTBEAT_INTERVAL_MS = 1e4;
|
|
|
23836
24970
|
var AUTH_RETRY_DELAY_MS = 150;
|
|
23837
24971
|
var AUTH_RETRY_MAX_ATTEMPTS = 13;
|
|
23838
24972
|
function normalizePath(value) {
|
|
23839
|
-
const resolved =
|
|
24973
|
+
const resolved = path36.resolve(value);
|
|
23840
24974
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
23841
24975
|
}
|
|
23842
24976
|
function resolveProjectServerUrl() {
|
|
@@ -23846,7 +24980,7 @@ function resolveProjectServerUrl() {
|
|
|
23846
24980
|
]) {
|
|
23847
24981
|
try {
|
|
23848
24982
|
const url = new URL(relative4, import.meta.url);
|
|
23849
|
-
if (url.protocol === "file:" &&
|
|
24983
|
+
if (url.protocol === "file:" && fs5.existsSync(fileURLToPath4(url))) return url;
|
|
23850
24984
|
} catch {
|
|
23851
24985
|
}
|
|
23852
24986
|
}
|
|
@@ -23856,7 +24990,7 @@ function isMailboxProjectServerAvailable() {
|
|
|
23856
24990
|
return resolveProjectServerUrl() !== null;
|
|
23857
24991
|
}
|
|
23858
24992
|
function delay2(ms) {
|
|
23859
|
-
return new Promise((
|
|
24993
|
+
return new Promise((resolve17) => setTimeout(resolve17, ms));
|
|
23860
24994
|
}
|
|
23861
24995
|
function isUnauthorizedMailboxError(error) {
|
|
23862
24996
|
return error instanceof Error && error.name === "UnauthorizedMailboxRequest";
|
|
@@ -23864,7 +24998,7 @@ function isUnauthorizedMailboxError(error) {
|
|
|
23864
24998
|
var MailboxProjectServerConnection = class {
|
|
23865
24999
|
constructor(projectDir) {
|
|
23866
25000
|
this.projectDir = projectDir;
|
|
23867
|
-
this.projectDir =
|
|
25001
|
+
this.projectDir = path36.resolve(projectDir);
|
|
23868
25002
|
this.endpoint = mailboxProjectServerEndpoint(this.projectDir);
|
|
23869
25003
|
this.state = {
|
|
23870
25004
|
status: isMailboxProjectServerAvailable() ? "offline" : "unavailable",
|
|
@@ -24029,7 +25163,7 @@ var MailboxProjectServerConnection = class {
|
|
|
24029
25163
|
this.info = null;
|
|
24030
25164
|
this.buffer = "";
|
|
24031
25165
|
this.authToken = void 0;
|
|
24032
|
-
return new Promise((
|
|
25166
|
+
return new Promise((resolve17, reject) => {
|
|
24033
25167
|
const socket = net2.createConnection(this.endpoint);
|
|
24034
25168
|
this.socket = socket;
|
|
24035
25169
|
socket.setEncoding("utf8");
|
|
@@ -24044,7 +25178,7 @@ var MailboxProjectServerConnection = class {
|
|
|
24044
25178
|
this.connectReject = null;
|
|
24045
25179
|
this.startHeartbeat();
|
|
24046
25180
|
this.transition("connected");
|
|
24047
|
-
|
|
25181
|
+
resolve17();
|
|
24048
25182
|
void this.request({ type: "request", op: "ping", args: {} }, 3e3).catch(
|
|
24049
25183
|
() => void 0
|
|
24050
25184
|
);
|
|
@@ -24073,7 +25207,7 @@ var MailboxProjectServerConnection = class {
|
|
|
24073
25207
|
currentAuthToken() {
|
|
24074
25208
|
if (this.authToken === void 0) {
|
|
24075
25209
|
try {
|
|
24076
|
-
const raw =
|
|
25210
|
+
const raw = fs5.readFileSync(mailboxProjectServerMetadataPath(this.projectDir), "utf8");
|
|
24077
25211
|
const parsed = JSON.parse(raw);
|
|
24078
25212
|
if (typeof parsed.authToken === "string" && parsed.authToken.length > 0) {
|
|
24079
25213
|
this.authToken = parsed.authToken;
|
|
@@ -24097,7 +25231,7 @@ var MailboxProjectServerConnection = class {
|
|
|
24097
25231
|
if (encoded.length > MAILBOX_PROJECT_SERVER_MAX_FRAME_CHARS) {
|
|
24098
25232
|
return Promise.reject(new Error("Mailbox project server request exceeded frame limit"));
|
|
24099
25233
|
}
|
|
24100
|
-
return new Promise((
|
|
25234
|
+
return new Promise((resolve17, reject) => {
|
|
24101
25235
|
const timer = setTimeout(() => {
|
|
24102
25236
|
const pending = this.pending.get(id);
|
|
24103
25237
|
if (!pending) return;
|
|
@@ -24105,7 +25239,7 @@ var MailboxProjectServerConnection = class {
|
|
|
24105
25239
|
pending.reject(new Error(`Mailbox ${message.type} exceeded its ${timeoutMs}ms timeout`));
|
|
24106
25240
|
}, timeoutMs);
|
|
24107
25241
|
timer.unref?.();
|
|
24108
|
-
this.pending.set(id, { resolve:
|
|
25242
|
+
this.pending.set(id, { resolve: resolve17, reject, timer });
|
|
24109
25243
|
socket.write(encoded);
|
|
24110
25244
|
});
|
|
24111
25245
|
}
|
|
@@ -24220,7 +25354,7 @@ var MailboxProjectServerConnection = class {
|
|
|
24220
25354
|
spawnDetachedServer() {
|
|
24221
25355
|
const url = resolveProjectServerUrl();
|
|
24222
25356
|
if (!url) throw new Error("Mailbox project server entrypoint is unavailable");
|
|
24223
|
-
|
|
25357
|
+
fs5.mkdirSync(this.projectDir, { recursive: true });
|
|
24224
25358
|
const child = spawn5(process.execPath, [fileURLToPath4(url), "--project-dir", this.projectDir], {
|
|
24225
25359
|
cwd: this.projectDir,
|
|
24226
25360
|
detached: process.platform !== "win32",
|
|
@@ -24321,9 +25455,9 @@ var CredentialVerifyThrottle = class {
|
|
|
24321
25455
|
var credentialVerifyThrottle = new CredentialVerifyThrottle();
|
|
24322
25456
|
|
|
24323
25457
|
// src/coordination/global-mailbox-paths.ts
|
|
24324
|
-
import * as
|
|
25458
|
+
import * as path37 from "node:path";
|
|
24325
25459
|
function resolveProjectDir(projectRoot, globalRoot) {
|
|
24326
|
-
return
|
|
25460
|
+
return path37.join(globalRoot, "projects", projectSlug(projectRoot));
|
|
24327
25461
|
}
|
|
24328
25462
|
|
|
24329
25463
|
// src/coordination/sqlite-mailbox.ts
|
|
@@ -24373,8 +25507,8 @@ var RemoteMailbox = class {
|
|
|
24373
25507
|
hqPublisher,
|
|
24374
25508
|
eventEmitter
|
|
24375
25509
|
} : optionsOrProjectDir;
|
|
24376
|
-
this.projectDir =
|
|
24377
|
-
this.messagePath =
|
|
25510
|
+
this.projectDir = path38.resolve(options.projectDir);
|
|
25511
|
+
this.messagePath = path38.join(this.projectDir, SQLITE_MAILBOX_FILE);
|
|
24378
25512
|
this.registryPath = this.messagePath;
|
|
24379
25513
|
this.clientRegistryPath = this.messagePath;
|
|
24380
25514
|
this.events = options.events;
|
|
@@ -24620,7 +25754,7 @@ var RemoteMailbox = class {
|
|
|
24620
25754
|
this.hqEventPending.clear();
|
|
24621
25755
|
return;
|
|
24622
25756
|
}
|
|
24623
|
-
const mailboxId = `${
|
|
25757
|
+
const mailboxId = `${path38.basename(this.projectDir)}:mailbox`;
|
|
24624
25758
|
const inFlight = this.query({ includeDeleted: true, limit: 100 }).then((messages) => {
|
|
24625
25759
|
const message = messages.find((candidate) => candidate.id === event.messageId);
|
|
24626
25760
|
const action = event.type === "message.sent" ? "message.sent" : "message.updated";
|
|
@@ -24643,7 +25777,7 @@ var RemoteMailbox = class {
|
|
|
24643
25777
|
if (!publisher || this.closed || !event.startsWith("mailbox.agent_") && !event.startsWith("mailbox.client_")) {
|
|
24644
25778
|
return;
|
|
24645
25779
|
}
|
|
24646
|
-
const mailboxId = `${
|
|
25780
|
+
const mailboxId = `${path38.basename(this.projectDir)}:mailbox`;
|
|
24647
25781
|
const record = typeof payload === "object" && payload !== null ? payload : {};
|
|
24648
25782
|
const agentId = typeof record["agentId"] === "string" ? record["agentId"] : void 0;
|
|
24649
25783
|
const action = event === "mailbox.agent_registered" ? "agent.registered" : event === "mailbox.agent_heartbeat" ? "agent.heartbeat" : event === "mailbox.agent_deregistered" ? "agent.deregistered" : void 0;
|
|
@@ -24676,7 +25810,7 @@ function createProjectMailbox(options) {
|
|
|
24676
25810
|
return new RemoteMailbox(options);
|
|
24677
25811
|
}
|
|
24678
25812
|
function getSharedProjectMailbox(projectDir, events, hqPublisher) {
|
|
24679
|
-
const key =
|
|
25813
|
+
const key = path38.resolve(projectDir);
|
|
24680
25814
|
let projectCache = sharedRemoteMailboxes.get(key);
|
|
24681
25815
|
if (!projectCache) {
|
|
24682
25816
|
projectCache = {
|
|
@@ -27026,25 +28160,25 @@ function requiredSendCapability(type) {
|
|
|
27026
28160
|
}
|
|
27027
28161
|
return "mail.send.informational";
|
|
27028
28162
|
}
|
|
27029
|
-
function requiredCredentialCapability(method,
|
|
27030
|
-
if (method === "POST" &&
|
|
27031
|
-
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")) {
|
|
27032
28166
|
return "mail.read.self";
|
|
27033
28167
|
}
|
|
27034
|
-
if (method === "POST" && (
|
|
28168
|
+
if (method === "POST" && (path44 === "/mailbox/ack" || path44 === "/mailbox/ack-many")) {
|
|
27035
28169
|
return "mail.ack.self";
|
|
27036
28170
|
}
|
|
27037
|
-
if (method === "POST" &&
|
|
27038
|
-
if (method === "POST" &&
|
|
28171
|
+
if (method === "POST" && path44 === "/mailbox/unread-count") return "mail.read.self";
|
|
28172
|
+
if (method === "POST" && path44 === "/mailbox/agents/register") {
|
|
27039
28173
|
return "mail.presence.register.self";
|
|
27040
28174
|
}
|
|
27041
|
-
if (method === "POST" &&
|
|
28175
|
+
if (method === "POST" && path44 === "/mailbox/agents/heartbeat") {
|
|
27042
28176
|
return "mail.presence.heartbeat.self";
|
|
27043
28177
|
}
|
|
27044
|
-
if (method === "GET" && (
|
|
28178
|
+
if (method === "GET" && (path44 === "/mailbox/agents" || path44 === "/mailbox/agents/online")) {
|
|
27045
28179
|
return "mail.presence.read";
|
|
27046
28180
|
}
|
|
27047
|
-
if (method === "GET" &&
|
|
28181
|
+
if (method === "GET" && path44 === "/mailbox/events") return "mail.events.self";
|
|
27048
28182
|
return void 0;
|
|
27049
28183
|
}
|
|
27050
28184
|
async function checkMailbox(mailbox, input, minTimestampIso, includeReceiptState = false, eligibleRecipients, readerRole) {
|
|
@@ -27264,7 +28398,7 @@ function createMailboxHttpRouter(options) {
|
|
|
27264
28398
|
return;
|
|
27265
28399
|
}
|
|
27266
28400
|
const customAccess = options.authorize ? await options.authorize(request) : void 0;
|
|
27267
|
-
let
|
|
28401
|
+
let access6 = customAccess ?? { allowed: true };
|
|
27268
28402
|
const presentedCredential = parseCredentialAuthorization(request);
|
|
27269
28403
|
if (options.credentialStore !== void 0 && presentedCredential !== void 0) {
|
|
27270
28404
|
const persistedAccess = await authorizePersistedMailboxCredential(
|
|
@@ -27272,30 +28406,30 @@ function createMailboxHttpRouter(options) {
|
|
|
27272
28406
|
options.credentialStore
|
|
27273
28407
|
);
|
|
27274
28408
|
if (!persistedAccess.allowed) {
|
|
27275
|
-
|
|
28409
|
+
access6 = persistedAccess;
|
|
27276
28410
|
} else if (persistedAccess.actor === void 0) {
|
|
27277
|
-
|
|
27278
|
-
} else if (
|
|
27279
|
-
if (
|
|
27280
|
-
|
|
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 };
|
|
27281
28415
|
} else {
|
|
27282
|
-
|
|
27283
|
-
...
|
|
28416
|
+
access6 = {
|
|
28417
|
+
...access6,
|
|
27284
28418
|
actor: persistedAccess.actor,
|
|
27285
|
-
...
|
|
28419
|
+
...access6.rateLimitKey ?? persistedAccess.rateLimitKey ? { rateLimitKey: access6.rateLimitKey ?? persistedAccess.rateLimitKey } : {}
|
|
27286
28420
|
};
|
|
27287
28421
|
}
|
|
27288
28422
|
}
|
|
27289
28423
|
} else if (customAccess === void 0 && options.credentialStore !== void 0) {
|
|
27290
|
-
|
|
28424
|
+
access6 = { allowed: false };
|
|
27291
28425
|
}
|
|
27292
|
-
if (
|
|
28426
|
+
if (access6.allowed && access6.actor !== void 0 && options.projectId !== void 0 && access6.actor.projectId !== options.projectId) {
|
|
27293
28427
|
writeJson(response, 403, {
|
|
27294
28428
|
error: { code: "FORBIDDEN", message: "credential is scoped to a different project" }
|
|
27295
28429
|
});
|
|
27296
28430
|
return;
|
|
27297
28431
|
}
|
|
27298
|
-
if (!
|
|
28432
|
+
if (!access6.allowed) {
|
|
27299
28433
|
const forwardedFor = request.headers["x-forwarded-for"];
|
|
27300
28434
|
const clientIp = (Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor)?.split(",")[0]?.trim() ?? request.socket?.remoteAddress ?? "unknown";
|
|
27301
28435
|
console.warn(
|
|
@@ -27311,14 +28445,14 @@ function createMailboxHttpRouter(options) {
|
|
|
27311
28445
|
);
|
|
27312
28446
|
writeJson(
|
|
27313
28447
|
response,
|
|
27314
|
-
|
|
27315
|
-
|
|
28448
|
+
access6.status ?? 401,
|
|
28449
|
+
access6.body ?? {
|
|
27316
28450
|
error: { code: "UNAUTHORIZED", message: "invalid or missing authorization credential" }
|
|
27317
28451
|
}
|
|
27318
28452
|
);
|
|
27319
28453
|
return;
|
|
27320
28454
|
}
|
|
27321
|
-
if (options.rateLimiter &&
|
|
28455
|
+
if (options.rateLimiter && access6.rateLimitKey !== void 0 && !options.rateLimiter.allow(access6.rateLimitKey)) {
|
|
27322
28456
|
writeJson(response, 429, {
|
|
27323
28457
|
error: {
|
|
27324
28458
|
code: "RATE_LIMITED",
|
|
@@ -27338,7 +28472,7 @@ function createMailboxHttpRouter(options) {
|
|
|
27338
28472
|
defaultMaxAgeMs,
|
|
27339
28473
|
closeSseStreams,
|
|
27340
28474
|
routePath,
|
|
27341
|
-
|
|
28475
|
+
access6.actor,
|
|
27342
28476
|
options.credentialStore
|
|
27343
28477
|
);
|
|
27344
28478
|
} catch (error) {
|
|
@@ -27366,16 +28500,16 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
|
|
|
27366
28500
|
throw validationError(`routePath must not start with '?' (got ${JSON.stringify(routePath)})`);
|
|
27367
28501
|
}
|
|
27368
28502
|
const queryIndex = url.indexOf("?");
|
|
27369
|
-
const
|
|
28503
|
+
const path44 = queryIndex === -1 ? url : url.slice(0, queryIndex);
|
|
27370
28504
|
if (actor !== void 0) {
|
|
27371
|
-
const requiredCapability = requiredCredentialCapability(method,
|
|
28505
|
+
const requiredCapability = requiredCredentialCapability(method, path44);
|
|
27372
28506
|
if (requiredCapability === void 0) {
|
|
27373
28507
|
writeJson(response, 403, {
|
|
27374
|
-
error: { code: "FORBIDDEN", message: `credential access is not permitted for ${method} ${
|
|
28508
|
+
error: { code: "FORBIDDEN", message: `credential access is not permitted for ${method} ${path44}` }
|
|
27375
28509
|
});
|
|
27376
28510
|
return;
|
|
27377
28511
|
}
|
|
27378
|
-
if (
|
|
28512
|
+
if (path44 !== "/mailbox/send" && !hasMailboxCapability(actor, requiredCapability)) {
|
|
27379
28513
|
writeJson(response, 403, {
|
|
27380
28514
|
error: {
|
|
27381
28515
|
code: "FORBIDDEN",
|
|
@@ -27385,7 +28519,7 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
|
|
|
27385
28519
|
return;
|
|
27386
28520
|
}
|
|
27387
28521
|
}
|
|
27388
|
-
if (method === "POST" &&
|
|
28522
|
+
if (method === "POST" && path44 === "/mailbox/send") {
|
|
27389
28523
|
const input = validateSend(
|
|
27390
28524
|
await readJsonBody(request, maxBodyBytes),
|
|
27391
28525
|
actor?.actorId,
|
|
@@ -27407,7 +28541,7 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
|
|
|
27407
28541
|
writeJson(response, 201, await mailbox.send(input));
|
|
27408
28542
|
return;
|
|
27409
28543
|
}
|
|
27410
|
-
if (method === "POST" &&
|
|
28544
|
+
if (method === "POST" && path44 === "/mailbox/query") {
|
|
27411
28545
|
const queryContext = parseSinceMs(url, defaultMaxAgeMs);
|
|
27412
28546
|
if ("error" in queryContext) {
|
|
27413
28547
|
writeJson(response, 400, { error: queryContext.error });
|
|
@@ -27427,7 +28561,7 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
|
|
|
27427
28561
|
writeJson(response, 200, { data: projected, count: projected.length });
|
|
27428
28562
|
return;
|
|
27429
28563
|
}
|
|
27430
|
-
if (method === "POST" &&
|
|
28564
|
+
if (method === "POST" && path44 === "/mailbox/check") {
|
|
27431
28565
|
const queryContext = parseSinceMs(url, defaultMaxAgeMs);
|
|
27432
28566
|
if ("error" in queryContext) {
|
|
27433
28567
|
writeJson(response, 400, { error: queryContext.error });
|
|
@@ -27461,7 +28595,7 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
|
|
|
27461
28595
|
writeJson(response, 200, { data: projected, count: projected.length });
|
|
27462
28596
|
return;
|
|
27463
28597
|
}
|
|
27464
|
-
if (method === "POST" &&
|
|
28598
|
+
if (method === "POST" && path44 === "/mailbox/ack") {
|
|
27465
28599
|
const input = validateAck(await readJsonBody(request, maxBodyBytes), actor?.actorId);
|
|
27466
28600
|
if (actor !== void 0) {
|
|
27467
28601
|
input.readerId = actor.actorId;
|
|
@@ -27475,7 +28609,7 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
|
|
|
27475
28609
|
writeJson(response, 200, { updated: projectedAck });
|
|
27476
28610
|
return;
|
|
27477
28611
|
}
|
|
27478
|
-
if (method === "POST" &&
|
|
28612
|
+
if (method === "POST" && path44 === "/mailbox/ack-many") {
|
|
27479
28613
|
const input = validateAckMany(await readJsonBody(request, maxBodyBytes), actor?.actorId);
|
|
27480
28614
|
if (actor !== void 0) {
|
|
27481
28615
|
const requestedIds = new Set(input.acks.map((ack) => ack.messageId));
|
|
@@ -27491,54 +28625,54 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
|
|
|
27491
28625
|
writeJson(response, 200, { updated: projectedMany, count: projectedMany.length });
|
|
27492
28626
|
return;
|
|
27493
28627
|
}
|
|
27494
|
-
if (method === "POST" &&
|
|
28628
|
+
if (method === "POST" && path44 === "/mailbox/unread-count") {
|
|
27495
28629
|
const body = await readJsonBody(request, maxBodyBytes);
|
|
27496
28630
|
const count = actor === void 0 ? await mailbox.unreadCount(requireString2(body, "forAgentId")) : await unreadCountForActor(mailbox, actor);
|
|
27497
28631
|
writeJson(response, 200, { count });
|
|
27498
28632
|
return;
|
|
27499
28633
|
}
|
|
27500
|
-
if (method === "POST" &&
|
|
28634
|
+
if (method === "POST" && path44 === "/mailbox/agents/register") {
|
|
27501
28635
|
const input = validateAgentRegistration(await readJsonBody(request, maxBodyBytes), actor);
|
|
27502
28636
|
await mailbox.registerAgent(input);
|
|
27503
28637
|
writeJson(response, 200, { ok: true });
|
|
27504
28638
|
return;
|
|
27505
28639
|
}
|
|
27506
|
-
if (method === "POST" &&
|
|
28640
|
+
if (method === "POST" && path44 === "/mailbox/agents/heartbeat") {
|
|
27507
28641
|
const input = validateAgentHeartbeat(await readJsonBody(request, maxBodyBytes), actor?.actorId);
|
|
27508
28642
|
if (actor !== void 0) input.agentId = actor.actorId;
|
|
27509
28643
|
await mailbox.heartbeat(input);
|
|
27510
28644
|
writeJson(response, 200, { ok: true });
|
|
27511
28645
|
return;
|
|
27512
28646
|
}
|
|
27513
|
-
if (method === "POST" &&
|
|
28647
|
+
if (method === "POST" && path44 === "/mailbox/register-client") {
|
|
27514
28648
|
await mailbox.registerClient(
|
|
27515
28649
|
validateClientRegistration(await readJsonBody(request, maxBodyBytes))
|
|
27516
28650
|
);
|
|
27517
28651
|
writeJson(response, 200, { ok: true });
|
|
27518
28652
|
return;
|
|
27519
28653
|
}
|
|
27520
|
-
if (method === "POST" &&
|
|
28654
|
+
if (method === "POST" && path44 === "/mailbox/heartbeat") {
|
|
27521
28655
|
await mailbox.clientHeartbeat(
|
|
27522
28656
|
validateClientHeartbeat(await readJsonBody(request, maxBodyBytes))
|
|
27523
28657
|
);
|
|
27524
28658
|
writeJson(response, 200, { ok: true });
|
|
27525
28659
|
return;
|
|
27526
28660
|
}
|
|
27527
|
-
if (method === "POST" &&
|
|
28661
|
+
if (method === "POST" && path44 === "/mailbox/purge-clients") {
|
|
27528
28662
|
writeJson(response, 200, { ok: true, purged: await mailbox.purgeClients() });
|
|
27529
28663
|
return;
|
|
27530
28664
|
}
|
|
27531
|
-
if (method === "GET" &&
|
|
28665
|
+
if (method === "GET" && path44 === "/mailbox/agents") {
|
|
27532
28666
|
const agents = await mailbox.getAgentStatuses();
|
|
27533
28667
|
writeJson(response, 200, { data: agents, count: agents.length });
|
|
27534
28668
|
return;
|
|
27535
28669
|
}
|
|
27536
|
-
if (method === "GET" &&
|
|
28670
|
+
if (method === "GET" && path44 === "/mailbox/agents/online") {
|
|
27537
28671
|
const agents = await mailbox.getOnlineAgents();
|
|
27538
28672
|
writeJson(response, 200, { data: agents, count: agents.length });
|
|
27539
28673
|
return;
|
|
27540
28674
|
}
|
|
27541
|
-
if (method === "GET" &&
|
|
28675
|
+
if (method === "GET" && path44 === "/mailbox/events" && eventEmitter) {
|
|
27542
28676
|
const queryContext = parseSinceMs(url, defaultMaxAgeMs);
|
|
27543
28677
|
if ("error" in queryContext) {
|
|
27544
28678
|
writeJson(response, 400, { error: queryContext.error });
|
|
@@ -27598,16 +28732,16 @@ function writeJson(response, status, body) {
|
|
|
27598
28732
|
var NULL_FLEET_BUS = new FleetBus();
|
|
27599
28733
|
|
|
27600
28734
|
// src/coordination/package-author-tracker.ts
|
|
27601
|
-
import * as
|
|
27602
|
-
import * as
|
|
28735
|
+
import * as fs6 from "node:fs/promises";
|
|
28736
|
+
import * as path39 from "node:path";
|
|
27603
28737
|
var DEFAULT_MAX_ENTRIES2 = 1e4;
|
|
27604
28738
|
var LOG_FILENAME2 = "package-authors.json";
|
|
27605
28739
|
function logPath2(storageDir) {
|
|
27606
|
-
return
|
|
28740
|
+
return path39.join(storageDir, LOG_FILENAME2);
|
|
27607
28741
|
}
|
|
27608
28742
|
async function loadLog2(storageDir, projectRoot) {
|
|
27609
28743
|
try {
|
|
27610
|
-
const raw = await
|
|
28744
|
+
const raw = await fs6.readFile(logPath2(storageDir), "utf-8");
|
|
27611
28745
|
const parsed = JSON.parse(raw);
|
|
27612
28746
|
if (!parsed.entries || !Array.isArray(parsed.entries)) {
|
|
27613
28747
|
return { projectRoot, entries: [] };
|
|
@@ -27625,7 +28759,7 @@ async function saveLog2(storageDir, log) {
|
|
|
27625
28759
|
`);
|
|
27626
28760
|
}
|
|
27627
28761
|
function detectEcosystem(manifestPath) {
|
|
27628
|
-
const name =
|
|
28762
|
+
const name = path39.win32.basename(manifestPath).toLowerCase();
|
|
27629
28763
|
if (name === "package.json") return "npm";
|
|
27630
28764
|
if (name === "go.mod") return "go";
|
|
27631
28765
|
if (name === "cargo.toml") return "cargo";
|
|
@@ -28515,12 +29649,12 @@ import { randomBytes as randomBytes2 } from "node:crypto";
|
|
|
28515
29649
|
import * as fsp27 from "node:fs/promises";
|
|
28516
29650
|
import { execFile } from "node:child_process";
|
|
28517
29651
|
import * as os4 from "node:os";
|
|
28518
|
-
import * as
|
|
29652
|
+
import * as path40 from "node:path";
|
|
28519
29653
|
var MAILBOX_BRIDGE_LOCK_FILENAME = ".mailbox-bridge.lock";
|
|
28520
29654
|
var MAILBOX_BRIDGE_TOKEN_FILENAME = ".mailbox.token";
|
|
28521
29655
|
async function acquireOrJoin(opts) {
|
|
28522
|
-
const lockPath =
|
|
28523
|
-
const tokenPath =
|
|
29656
|
+
const lockPath = path40.join(opts.projectDir, MAILBOX_BRIDGE_LOCK_FILENAME);
|
|
29657
|
+
const tokenPath = path40.join(opts.projectDir, MAILBOX_BRIDGE_TOKEN_FILENAME);
|
|
28524
29658
|
const inspected = await readLockForInspection(lockPath);
|
|
28525
29659
|
if (inspected.kind === "live") {
|
|
28526
29660
|
const existing = inspected.lock;
|
|
@@ -28548,8 +29682,8 @@ async function acquireOrJoin(opts) {
|
|
|
28548
29682
|
return { kind: "acquired", lock: tentative, tokenPath };
|
|
28549
29683
|
}
|
|
28550
29684
|
async function finalize(projectDir, tentative, boundPort) {
|
|
28551
|
-
const lockPath =
|
|
28552
|
-
const tokenPath =
|
|
29685
|
+
const lockPath = path40.join(projectDir, MAILBOX_BRIDGE_LOCK_FILENAME);
|
|
29686
|
+
const tokenPath = path40.join(projectDir, MAILBOX_BRIDGE_TOKEN_FILENAME);
|
|
28553
29687
|
const finalized = {
|
|
28554
29688
|
...tentative,
|
|
28555
29689
|
port: boundPort,
|
|
@@ -28560,8 +29694,8 @@ async function finalize(projectDir, tentative, boundPort) {
|
|
|
28560
29694
|
return finalized;
|
|
28561
29695
|
}
|
|
28562
29696
|
async function release(projectDir, generation) {
|
|
28563
|
-
const lockPath =
|
|
28564
|
-
const tokenPath =
|
|
29697
|
+
const lockPath = path40.join(projectDir, MAILBOX_BRIDGE_LOCK_FILENAME);
|
|
29698
|
+
const tokenPath = path40.join(projectDir, MAILBOX_BRIDGE_TOKEN_FILENAME);
|
|
28565
29699
|
try {
|
|
28566
29700
|
const raw = await fsp27.readFile(lockPath, "utf-8");
|
|
28567
29701
|
const parsed = JSON.parse(raw);
|
|
@@ -28595,7 +29729,7 @@ async function readLockForInspection(lockPath) {
|
|
|
28595
29729
|
return { kind: "live", lock: parsed };
|
|
28596
29730
|
}
|
|
28597
29731
|
async function readLiveLock(projectDir) {
|
|
28598
|
-
const lockPath =
|
|
29732
|
+
const lockPath = path40.join(projectDir, MAILBOX_BRIDGE_LOCK_FILENAME);
|
|
28599
29733
|
const result = await readLockForInspection(lockPath);
|
|
28600
29734
|
if (result.kind === "live") {
|
|
28601
29735
|
return { kind: "live", lock: result.lock };
|
|
@@ -28606,7 +29740,7 @@ async function readLiveLock(projectDir) {
|
|
|
28606
29740
|
return { kind: "absent" };
|
|
28607
29741
|
}
|
|
28608
29742
|
async function atomicWriteJson(targetPath, value) {
|
|
28609
|
-
const dir =
|
|
29743
|
+
const dir = path40.dirname(targetPath);
|
|
28610
29744
|
await fsp27.mkdir(dir, { recursive: true });
|
|
28611
29745
|
const tmp = `${targetPath}.tmp.${process.pid}.${randomBytes2(4).toString("hex")}`;
|
|
28612
29746
|
const body = JSON.stringify(value, null, 2) + "\n";
|
|
@@ -28622,7 +29756,7 @@ async function isProcessAlive(pid) {
|
|
|
28622
29756
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
28623
29757
|
if (pid === process.pid) return true;
|
|
28624
29758
|
if (os4.platform() === "win32") {
|
|
28625
|
-
return new Promise((
|
|
29759
|
+
return new Promise((resolve17) => {
|
|
28626
29760
|
execFile(
|
|
28627
29761
|
"tasklist",
|
|
28628
29762
|
["/FI", `PID eq ${pid}`, "/NH", "/FO", "CSV"],
|
|
@@ -28634,11 +29768,11 @@ async function isProcessAlive(pid) {
|
|
|
28634
29768
|
},
|
|
28635
29769
|
(error, out) => {
|
|
28636
29770
|
if (error) {
|
|
28637
|
-
|
|
29771
|
+
resolve17(false);
|
|
28638
29772
|
return;
|
|
28639
29773
|
}
|
|
28640
29774
|
const match = /(?:^|\n)"[^"]*","(\d+)"/.exec(out);
|
|
28641
|
-
|
|
29775
|
+
resolve17(match !== null && match[1] === String(pid));
|
|
28642
29776
|
}
|
|
28643
29777
|
);
|
|
28644
29778
|
});
|
|
@@ -28789,8 +29923,8 @@ function acceptManifestCandidate(candidate) {
|
|
|
28789
29923
|
if (normalized.split("/").includes("..")) return false;
|
|
28790
29924
|
return isManifestFile(normalized);
|
|
28791
29925
|
}
|
|
28792
|
-
function isManifestFile(
|
|
28793
|
-
const name = pathBasename(
|
|
29926
|
+
function isManifestFile(path44) {
|
|
29927
|
+
const name = pathBasename(path44).toLowerCase();
|
|
28794
29928
|
const manifests = [
|
|
28795
29929
|
"package.json",
|
|
28796
29930
|
"package-lock.json",
|
|
@@ -28870,7 +30004,7 @@ function buildTechStackTask(msg, manifestPath) {
|
|
|
28870
30004
|
].join("\n");
|
|
28871
30005
|
}
|
|
28872
30006
|
|
|
28873
|
-
// src/
|
|
30007
|
+
// src/coordination/collab-pause.ts
|
|
28874
30008
|
function collabPauseMiddleware(bus, opts = {}) {
|
|
28875
30009
|
const timeoutMs = opts.defaultTimeoutMs ?? 6e4;
|
|
28876
30010
|
const logger = opts.logger;
|
|
@@ -29101,10 +30235,10 @@ var AdaptiveConcurrencyController = class {
|
|
|
29101
30235
|
};
|
|
29102
30236
|
|
|
29103
30237
|
// src/coordination/agent-monitor.ts
|
|
29104
|
-
import { createReadStream as
|
|
29105
|
-
import * as
|
|
29106
|
-
import * as
|
|
29107
|
-
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";
|
|
29108
30242
|
var AgentMonitorService = class _AgentMonitorService {
|
|
29109
30243
|
_fleetBus;
|
|
29110
30244
|
_events;
|
|
@@ -29183,7 +30317,7 @@ var AgentMonitorService = class _AgentMonitorService {
|
|
|
29183
30317
|
async loadSessionsFromDisk() {
|
|
29184
30318
|
let subagentIds;
|
|
29185
30319
|
try {
|
|
29186
|
-
const dirents = await
|
|
30320
|
+
const dirents = await fs7.readdir(this._transcriptsDir, { withFileTypes: true });
|
|
29187
30321
|
subagentIds = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
|
|
29188
30322
|
} catch {
|
|
29189
30323
|
return this.getAllSessions();
|
|
@@ -29207,12 +30341,12 @@ var AgentMonitorService = class _AgentMonitorService {
|
|
|
29207
30341
|
return this.getAllSessions();
|
|
29208
30342
|
}
|
|
29209
30343
|
async _readTranscriptFile(subagentId) {
|
|
29210
|
-
const file =
|
|
29211
|
-
const accessible = await
|
|
30344
|
+
const file = path41.join(this._transcriptsDir, subagentId, "transcript.jsonl");
|
|
30345
|
+
const accessible = await fs7.access(file).then(() => true).catch(() => false);
|
|
29212
30346
|
if (!accessible) return [];
|
|
29213
30347
|
const out = [];
|
|
29214
|
-
const input =
|
|
29215
|
-
const lines =
|
|
30348
|
+
const input = createReadStream7(file, { encoding: "utf8" });
|
|
30349
|
+
const lines = createInterface7({ input, crlfDelay: Number.POSITIVE_INFINITY });
|
|
29216
30350
|
try {
|
|
29217
30351
|
for await (const line of lines) {
|
|
29218
30352
|
const trimmed = line.trim();
|
|
@@ -29453,9 +30587,9 @@ var AgentMonitorService = class _AgentMonitorService {
|
|
|
29453
30587
|
* firehose (or, worse, only the first word).
|
|
29454
30588
|
*/
|
|
29455
30589
|
_appendStreamDelta(subagentId, session, kind, text, iteration) {
|
|
29456
|
-
const
|
|
29457
|
-
if (
|
|
29458
|
-
|
|
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;
|
|
29459
30593
|
return;
|
|
29460
30594
|
}
|
|
29461
30595
|
this._closeStream(subagentId);
|
|
@@ -29479,11 +30613,11 @@ var AgentMonitorService = class _AgentMonitorService {
|
|
|
29479
30613
|
* announce the completed entry on the local bus + HQ callback.
|
|
29480
30614
|
*/
|
|
29481
30615
|
_closeStream(subagentId) {
|
|
29482
|
-
const
|
|
29483
|
-
if (!
|
|
30616
|
+
const open10 = this._openStreams.get(subagentId);
|
|
30617
|
+
if (!open10) return;
|
|
29484
30618
|
this._openStreams.delete(subagentId);
|
|
29485
|
-
this._enqueueWrite(subagentId,
|
|
29486
|
-
this._emitEntry(
|
|
30619
|
+
this._enqueueWrite(subagentId, open10.entry);
|
|
30620
|
+
this._emitEntry(open10.entry);
|
|
29487
30621
|
}
|
|
29488
30622
|
_addEntry(subagentId, entry) {
|
|
29489
30623
|
const session = this._sessions.get(subagentId);
|
|
@@ -29548,14 +30682,14 @@ var AgentMonitorService = class _AgentMonitorService {
|
|
|
29548
30682
|
this._writeDrain = drain;
|
|
29549
30683
|
}
|
|
29550
30684
|
async _appendToFile(subagentId, line) {
|
|
29551
|
-
const dir =
|
|
30685
|
+
const dir = path41.join(this._transcriptsDir, subagentId);
|
|
29552
30686
|
if (!this._ensuredDirs.has(dir)) {
|
|
29553
|
-
await
|
|
30687
|
+
await fs7.mkdir(dir, { recursive: true });
|
|
29554
30688
|
this._ensuredDirs.add(dir);
|
|
29555
30689
|
}
|
|
29556
|
-
const filePath =
|
|
30690
|
+
const filePath = path41.join(dir, "transcript.jsonl");
|
|
29557
30691
|
try {
|
|
29558
|
-
await
|
|
30692
|
+
await fs7.appendFile(filePath, line, { encoding: "utf8", mode: SECRET_FILE_MODE });
|
|
29559
30693
|
} catch (error) {
|
|
29560
30694
|
this._ensuredDirs.delete(dir);
|
|
29561
30695
|
throw error;
|
|
@@ -29946,7 +31080,7 @@ import { randomUUID as randomUUID22 } from "node:crypto";
|
|
|
29946
31080
|
// src/coordination/knowledge-graph.ts
|
|
29947
31081
|
import { randomUUID as randomUUID20 } from "node:crypto";
|
|
29948
31082
|
import * as fsp28 from "node:fs/promises";
|
|
29949
|
-
import * as
|
|
31083
|
+
import * as path42 from "node:path";
|
|
29950
31084
|
var DEFAULT_MAX_NODES = 2e3;
|
|
29951
31085
|
var MAX_SUBSCRIPTIONS = 1e3;
|
|
29952
31086
|
var MAX_PENDING_DELIVERIES_PER_SUBSCRIPTION = 1e3;
|
|
@@ -29981,8 +31115,8 @@ var KnowledgeGraph = class _KnowledgeGraph {
|
|
|
29981
31115
|
return this.index;
|
|
29982
31116
|
}
|
|
29983
31117
|
constructor(sessionDir, maxNodes = DEFAULT_MAX_NODES, compactEveryWrites = Math.max(1e3, maxNodes * 4)) {
|
|
29984
|
-
this.filePath =
|
|
29985
|
-
this.graphFilePath =
|
|
31118
|
+
this.filePath = path42.join(sessionDir, "_knowledge_graph");
|
|
31119
|
+
this.graphFilePath = path42.join(this.filePath, "graph.jsonl");
|
|
29986
31120
|
this.maxNodes = maxNodes;
|
|
29987
31121
|
this.compactEveryWrites = Math.max(1, Math.floor(compactEveryWrites));
|
|
29988
31122
|
}
|
|
@@ -31086,6 +32220,11 @@ var ConsensusProtocol = class {
|
|
|
31086
32220
|
if (change?.type !== "change") {
|
|
31087
32221
|
throw new Error(`ConsensusProtocol: no change found with id "${changeId}"`);
|
|
31088
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
|
+
}
|
|
31089
32228
|
await this.graph.update(changeId, { status: "proposed", votes: [] });
|
|
31090
32229
|
const eligible = this._eligibleVoters(change);
|
|
31091
32230
|
this._notifyVoters(change, eligible, "vote_initiated");
|
|
@@ -31099,6 +32238,11 @@ var ConsensusProtocol = class {
|
|
|
31099
32238
|
if (change?.type !== "change") {
|
|
31100
32239
|
throw new Error(`ConsensusProtocol: no change found for "${changeId}"`);
|
|
31101
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
|
+
}
|
|
31102
32246
|
const voter = this.voters.get(voterId);
|
|
31103
32247
|
if (!voter) {
|
|
31104
32248
|
throw new Error(`ConsensusProtocol: unknown voter "${voterId}"`);
|
|
@@ -31173,8 +32317,8 @@ var ConsensusProtocol = class {
|
|
|
31173
32317
|
(sum, v) => sum + (this.voters.get(v.agentId)?.weight ?? 1),
|
|
31174
32318
|
0
|
|
31175
32319
|
);
|
|
31176
|
-
const totalWeight =
|
|
31177
|
-
(sum,
|
|
32320
|
+
const totalWeight = eligible.reduce(
|
|
32321
|
+
(sum, id) => sum + (this.voters.get(id)?.weight ?? 1),
|
|
31178
32322
|
0
|
|
31179
32323
|
);
|
|
31180
32324
|
const castCount = votes.length;
|
|
@@ -31894,17 +33038,17 @@ ${input.detail}`
|
|
|
31894
33038
|
_waitForDagProgress(timeoutMs) {
|
|
31895
33039
|
const before = this._dagProgressKey();
|
|
31896
33040
|
if (this.dag.isDone()) return Promise.resolve();
|
|
31897
|
-
return new Promise((
|
|
33041
|
+
return new Promise((resolve17) => {
|
|
31898
33042
|
let off;
|
|
31899
33043
|
const timer = setTimeout(() => {
|
|
31900
33044
|
off?.();
|
|
31901
|
-
|
|
33045
|
+
resolve17();
|
|
31902
33046
|
}, timeoutMs);
|
|
31903
33047
|
off = this.dag.onEvent(() => {
|
|
31904
33048
|
if (this._dagProgressKey() === before) return;
|
|
31905
33049
|
clearTimeout(timer);
|
|
31906
33050
|
off?.();
|
|
31907
|
-
|
|
33051
|
+
resolve17();
|
|
31908
33052
|
});
|
|
31909
33053
|
});
|
|
31910
33054
|
}
|
|
@@ -32212,8 +33356,8 @@ var CollaborationBus = class _CollaborationBus {
|
|
|
32212
33356
|
if (this.isPaused()) return false;
|
|
32213
33357
|
this.pausedAtMs = Date.now();
|
|
32214
33358
|
this.pausedBy = byParticipant;
|
|
32215
|
-
this.pausePromise = new Promise((
|
|
32216
|
-
this.pauseResolve =
|
|
33359
|
+
this.pausePromise = new Promise((resolve17) => {
|
|
33360
|
+
this.pauseResolve = resolve17;
|
|
32217
33361
|
});
|
|
32218
33362
|
return true;
|
|
32219
33363
|
}
|
|
@@ -32249,8 +33393,8 @@ var CollaborationBus = class _CollaborationBus {
|
|
|
32249
33393
|
return true;
|
|
32250
33394
|
}
|
|
32251
33395
|
let timer;
|
|
32252
|
-
const timeoutPromise = new Promise((
|
|
32253
|
-
timer = setTimeout(() =>
|
|
33396
|
+
const timeoutPromise = new Promise((resolve17) => {
|
|
33397
|
+
timer = setTimeout(() => resolve17("timeout"), timeoutMs);
|
|
32254
33398
|
});
|
|
32255
33399
|
const resumedPromise = this.pausePromise.then(() => "resumed").catch(() => "resumed");
|
|
32256
33400
|
const winner = await Promise.race([resumedPromise, timeoutPromise]);
|
|
@@ -32343,6 +33487,843 @@ var CollaborationBus = class _CollaborationBus {
|
|
|
32343
33487
|
}
|
|
32344
33488
|
}
|
|
32345
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
|
+
}
|
|
32346
34327
|
export {
|
|
32347
34328
|
ACP_AGENTS,
|
|
32348
34329
|
AGENTS_BY_PHASE,
|
|
@@ -32352,6 +34333,7 @@ export {
|
|
|
32352
34333
|
AUDIT_LOG_AGENT,
|
|
32353
34334
|
AdaptiveConcurrencyController,
|
|
32354
34335
|
AgentMonitorService,
|
|
34336
|
+
AgentStatusTracker,
|
|
32355
34337
|
AutonomousBrain,
|
|
32356
34338
|
AutonomousCoordinator,
|
|
32357
34339
|
BUG_HUNTER_AGENT,
|
|
@@ -32405,6 +34387,7 @@ export {
|
|
|
32405
34387
|
FleetBus,
|
|
32406
34388
|
FleetCostCapError,
|
|
32407
34389
|
FleetManager,
|
|
34390
|
+
FleetNotifier,
|
|
32408
34391
|
FleetSpawnBudgetError,
|
|
32409
34392
|
FleetSupervisor,
|
|
32410
34393
|
FleetTokenCapError,
|
|
@@ -32526,6 +34509,8 @@ export {
|
|
|
32526
34509
|
isMailboxProjectServerAvailable,
|
|
32527
34510
|
isProvenDirective,
|
|
32528
34511
|
isValidMatrixKey,
|
|
34512
|
+
kanbanBoundaryOps,
|
|
34513
|
+
kanbanDispatch,
|
|
32529
34514
|
listProjectAgentLearnedEntries,
|
|
32530
34515
|
listProjectAgentRoles,
|
|
32531
34516
|
listProjectSkillAugmentations,
|
|
@@ -32617,6 +34602,8 @@ export {
|
|
|
32617
34602
|
scoreSkillAffinity,
|
|
32618
34603
|
scrubRetiredLines,
|
|
32619
34604
|
sessionRecipient,
|
|
34605
|
+
setKanbanBoundaryOps,
|
|
34606
|
+
setKanbanDispatch,
|
|
32620
34607
|
setSkillPinned,
|
|
32621
34608
|
slugifyProjectAgentRole,
|
|
32622
34609
|
startPackageOutdatedWatcher,
|