@wrongstack/core 0.9.19 → 0.10.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/README.md +37 -33
- package/dist/{agent-bridge-DMVOX0cF.d.ts → agent-bridge-DBBGP5O4.d.ts} +1 -1
- package/dist/{agent-subagent-runner-C4qt9e5Y.d.ts → agent-subagent-runner-KwufTWwn.d.ts} +3 -4
- package/dist/compactor-D1RHFRmF.d.ts +41 -0
- package/dist/{config-CWva0qoL.d.ts → config-CJXBka2r.d.ts} +38 -2
- package/dist/{context-BRNbHmRM.d.ts → context-7u93AcGD.d.ts} +140 -0
- package/dist/coordination/index.d.ts +13 -13
- package/dist/coordination/index.js +786 -200
- package/dist/coordination/index.js.map +1 -1
- package/dist/{default-config-Uzla85H-.d.ts → default-config-DEXI4jsl.d.ts} +10 -1
- package/dist/defaults/index.d.ts +24 -24
- package/dist/defaults/index.js +1612 -528
- package/dist/defaults/index.js.map +1 -1
- package/dist/{events-CiG9qUM_.d.ts → events-Bw1K5E6K.d.ts} +49 -1
- package/dist/execution/index.d.ts +46 -32
- package/dist/execution/index.js +663 -169
- package/dist/execution/index.js.map +1 -1
- package/dist/extension/index.d.ts +6 -6
- package/dist/extension/index.js.map +1 -1
- package/dist/goal-store-BeRsj7YX.d.ts +188 -0
- package/dist/{index-p95HQ22A.d.ts → index-DIt5TIaF.d.ts} +8 -8
- package/dist/{index-aizK8olO.d.ts → index-DgQWnZxP.d.ts} +11 -8
- package/dist/index.d.ts +338 -41
- package/dist/index.js +2747 -787
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/index.d.ts +6 -6
- package/dist/infrastructure/index.js +36 -0
- package/dist/infrastructure/index.js.map +1 -1
- package/dist/kernel/index.d.ts +9 -9
- package/dist/kernel/index.js +57 -8
- package/dist/kernel/index.js.map +1 -1
- package/dist/{mcp-servers-BkVEqkRe.d.ts → mcp-servers-BgINZzuo.d.ts} +25 -3
- package/dist/models/index.d.ts +3 -3
- package/dist/models/index.js +164 -11
- package/dist/models/index.js.map +1 -1
- package/dist/models-registry-gwMAo6E3.d.ts +86 -0
- package/dist/{multi-agent-3ZnTB1aT.d.ts → multi-agent-C1O8EFzb.d.ts} +77 -24
- package/dist/{multi-agent-coordinator-bRaI_aD1.d.ts → multi-agent-coordinator-DoVSBFNx.d.ts} +20 -2
- package/dist/{null-fleet-bus-DKM3Iy9d.d.ts → null-fleet-bus-T-s3JaOt.d.ts} +413 -110
- package/dist/observability/index.d.ts +2 -2
- package/dist/{path-resolver-TcJfc29Y.d.ts → path-resolver-BfyK-I7w.d.ts} +2 -2
- package/dist/{permission-bPuzAy4x.d.ts → permission-B6sldrSp.d.ts} +1 -1
- package/dist/{permission-policy-BUQSutpl.d.ts → permission-policy-CtNscWOA.d.ts} +10 -9
- package/dist/{plan-templates-fkQTyz3U.d.ts → plan-templates-BWs6Je7j.d.ts} +8 -8
- package/dist/{provider-runner-BEpikbbN.d.ts → provider-runner-BmPe2RuU.d.ts} +3 -3
- package/dist/{retry-policy-BYkq0ugs.d.ts → retry-policy-KF18W4dg.d.ts} +1 -1
- package/dist/sdd/index.d.ts +9 -10
- package/dist/sdd/index.js +350 -122
- package/dist/sdd/index.js.map +1 -1
- package/dist/security/index.d.ts +65 -4
- package/dist/security/index.js +67 -19
- package/dist/security/index.js.map +1 -1
- package/dist/{selector-pox8abg0.d.ts → selector-DmXxpFyM.d.ts} +1 -1
- package/dist/{session-reader-CSWcb5Ga.d.ts → session-reader-CCSBfQmd.d.ts} +2 -2
- package/dist/skills/index.d.ts +1 -1
- package/dist/skills/index.js +1 -1
- package/dist/skills/index.js.map +1 -1
- package/dist/storage/index.d.ts +392 -8
- package/dist/storage/index.js +796 -40
- package/dist/storage/index.js.map +1 -1
- package/dist/{system-prompt-Bs-Wliab.d.ts → system-prompt-CM6zOhd2.d.ts} +19 -1
- package/dist/{tool-executor-Boo3dekH.d.ts → tool-executor-D7RxLtg4.d.ts} +9 -4
- package/dist/types/index.d.ts +16 -16
- package/dist/types/index.js +404 -14
- package/dist/types/index.js.map +1 -1
- package/dist/utils/index.d.ts +40 -8
- package/dist/utils/index.js +127 -57
- package/dist/utils/index.js.map +1 -1
- package/dist/{wstack-paths-BCgmTNlG.d.ts → wstack-paths-eMXnY1_X.d.ts} +8 -0
- package/package.json +1 -1
- package/dist/compactor-DVTKL7XD.d.ts +0 -23
- package/dist/goal-store-C7jcumEh.d.ts +0 -96
- package/dist/models-registry-OG_30xqZ.d.ts +0 -46
package/dist/index.js
CHANGED
|
@@ -56,8 +56,8 @@ async function atomicWrite(targetPath, content, opts = {}) {
|
|
|
56
56
|
}
|
|
57
57
|
let mode;
|
|
58
58
|
try {
|
|
59
|
-
const
|
|
60
|
-
mode =
|
|
59
|
+
const stat8 = await fsp2.stat(targetPath);
|
|
60
|
+
mode = stat8.mode & 511;
|
|
61
61
|
} catch {
|
|
62
62
|
mode = opts.mode;
|
|
63
63
|
}
|
|
@@ -142,8 +142,10 @@ var ERROR_CODES = {
|
|
|
142
142
|
// Container / Registry
|
|
143
143
|
CONTAINER_TOKEN_ALREADY_BOUND: "CONTAINER_TOKEN_ALREADY_BOUND",
|
|
144
144
|
CONTAINER_TOKEN_NOT_BOUND: "CONTAINER_TOKEN_NOT_BOUND",
|
|
145
|
+
CONTAINER_CIRCULAR_DEPENDENCY: "CONTAINER_CIRCULAR_DEPENDENCY",
|
|
145
146
|
REGISTRY_DUPLICATE: "REGISTRY_DUPLICATE",
|
|
146
147
|
REGISTRY_NOT_FOUND: "REGISTRY_NOT_FOUND",
|
|
148
|
+
REGISTRY_INVALID: "REGISTRY_INVALID",
|
|
147
149
|
// File system
|
|
148
150
|
FS_READ_FAILED: "FS_READ_FAILED",
|
|
149
151
|
FS_WRITE_FAILED: "FS_WRITE_FAILED",
|
|
@@ -306,6 +308,16 @@ function isFsError(err) {
|
|
|
306
308
|
// src/kernel/container.ts
|
|
307
309
|
var Container = class {
|
|
308
310
|
entries = /* @__PURE__ */ new Map();
|
|
311
|
+
/**
|
|
312
|
+
* Tokens currently mid-resolve. Tracked so we can detect circular
|
|
313
|
+
* dependencies (A → B → A) and throw a structured error instead of
|
|
314
|
+
* overflowing the call stack with "Maximum call stack size exceeded".
|
|
315
|
+
*
|
|
316
|
+
* Not a memoization cache — the per-entry `cache` field is the source
|
|
317
|
+
* of truth for "have I built this before?". This set only lives for
|
|
318
|
+
* the duration of a single resolve call.
|
|
319
|
+
*/
|
|
320
|
+
resolving = /* @__PURE__ */ new Set();
|
|
309
321
|
bind(token, factory, opts = {}) {
|
|
310
322
|
if (this.entries.has(token)) {
|
|
311
323
|
throw new WrongStackError({
|
|
@@ -366,14 +378,43 @@ var Container = class {
|
|
|
366
378
|
if (entry.singleton && entry.cache !== void 0) {
|
|
367
379
|
return entry.cache;
|
|
368
380
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
381
|
+
if (this.resolving.has(token)) {
|
|
382
|
+
const cycle = this.describeCycle(token);
|
|
383
|
+
throw new WrongStackError({
|
|
384
|
+
message: `Container: circular dependency detected \u2014 ${cycle}`,
|
|
385
|
+
code: ERROR_CODES.CONTAINER_CIRCULAR_DEPENDENCY,
|
|
386
|
+
subsystem: "container",
|
|
387
|
+
context: { token: token.description, cycle }
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
this.resolving.add(token);
|
|
391
|
+
try {
|
|
392
|
+
let value = entry.factory(this);
|
|
393
|
+
for (const d of entry.decorators) {
|
|
394
|
+
value = d(value, this);
|
|
395
|
+
}
|
|
396
|
+
if (entry.singleton) {
|
|
397
|
+
entry.cache = value;
|
|
398
|
+
}
|
|
399
|
+
return value;
|
|
400
|
+
} finally {
|
|
401
|
+
this.resolving.delete(token);
|
|
372
402
|
}
|
|
373
|
-
|
|
374
|
-
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Build a human-readable description of the dependency cycle that
|
|
406
|
+
* caused the resolution to re-enter. Lists the tokens in the order
|
|
407
|
+
* they were entered, then appends the re-entered token to close the
|
|
408
|
+
* loop. Falls back to a generic message if the resolving set is
|
|
409
|
+
* somehow empty (shouldn't happen, but defensive).
|
|
410
|
+
*/
|
|
411
|
+
describeCycle(reentry) {
|
|
412
|
+
const descs = [];
|
|
413
|
+
for (const t2 of this.resolving) {
|
|
414
|
+
descs.push(t2.description ?? "unknown");
|
|
375
415
|
}
|
|
376
|
-
|
|
416
|
+
descs.push(reentry.description ?? "unknown");
|
|
417
|
+
return descs.join(" \u2192 ");
|
|
377
418
|
}
|
|
378
419
|
has(token) {
|
|
379
420
|
return this.entries.has(token);
|
|
@@ -644,7 +685,8 @@ var EventBus = class {
|
|
|
644
685
|
}
|
|
645
686
|
if (this.wildcards.length > 0) {
|
|
646
687
|
const name = event;
|
|
647
|
-
|
|
688
|
+
const snapshot = this.wildcards.slice();
|
|
689
|
+
for (const { match, fn } of snapshot) {
|
|
648
690
|
if (!match(name)) continue;
|
|
649
691
|
try {
|
|
650
692
|
fn(name, payload);
|
|
@@ -715,11 +757,19 @@ var ScopedEventBus = class extends EventBus {
|
|
|
715
757
|
* Uses EventBus's public API directly to avoid triggering our own `on()`
|
|
716
758
|
* override (which would consume a key slot for the wrapper, then orphan
|
|
717
759
|
* our registration entry under a different key).
|
|
760
|
+
*
|
|
761
|
+
* When the wrapper fires, it cleans up BOTH the underlying EventBus
|
|
762
|
+
* listener AND the tracking entry — so `scopedListenerCount` returns to
|
|
763
|
+
* its pre-`once()` value without requiring the caller to invoke the
|
|
764
|
+
* returned unsubscribe. The returned `unsub` is still safe to call
|
|
765
|
+
* after auto-removal (its delete is a no-op and its off() finds
|
|
766
|
+
* nothing to remove).
|
|
718
767
|
*/
|
|
719
768
|
once(event, fn) {
|
|
720
769
|
const key = this.nextKey++;
|
|
721
770
|
const wrapper = (payload) => {
|
|
722
771
|
EventBus.prototype.off.call(this, event, wrapper);
|
|
772
|
+
this.registrations.delete(key);
|
|
723
773
|
fn(payload);
|
|
724
774
|
};
|
|
725
775
|
EventBus.prototype.on.call(this, event, wrapper);
|
|
@@ -1024,6 +1074,14 @@ var DEFAULT_CONTEXT_CONFIG = Object.freeze({
|
|
|
1024
1074
|
var DEFAULT_AUTONOMY_CONFIG = Object.freeze({
|
|
1025
1075
|
autoProceedDelayMs: 45e3
|
|
1026
1076
|
});
|
|
1077
|
+
var DEFAULT_SESSION_LOGGING_CONFIG = Object.freeze({
|
|
1078
|
+
auditLevel: "standard",
|
|
1079
|
+
sampling: {
|
|
1080
|
+
toolProgress: {
|
|
1081
|
+
sampleRate: 8
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1027
1085
|
|
|
1028
1086
|
// src/types/secret-vault.ts
|
|
1029
1087
|
var ENCRYPTED_PREFIX = "enc:v1:";
|
|
@@ -1676,6 +1734,7 @@ var HybridCompactor = class {
|
|
|
1676
1734
|
}
|
|
1677
1735
|
async compact(ctx, opts = {}) {
|
|
1678
1736
|
const beforeTokens = this.estimateMessages(ctx.messages);
|
|
1737
|
+
const beforeFull = this.estimateFullRequest(ctx);
|
|
1679
1738
|
const reductions = [];
|
|
1680
1739
|
const policy = readContextWindowPolicy(ctx);
|
|
1681
1740
|
const preserveK = policy?.preserveK ?? this.preserveK;
|
|
@@ -1691,9 +1750,12 @@ var HybridCompactor = class {
|
|
|
1691
1750
|
ctx.state.replaceMessages(repaired.messages);
|
|
1692
1751
|
}
|
|
1693
1752
|
const afterTokens = this.estimateMessages(ctx.messages);
|
|
1753
|
+
const afterFull = this.estimateFullRequest(ctx);
|
|
1694
1754
|
return {
|
|
1695
1755
|
before: beforeTokens,
|
|
1696
1756
|
after: afterTokens,
|
|
1757
|
+
fullRequestTokensBefore: beforeFull,
|
|
1758
|
+
fullRequestTokensAfter: afterFull,
|
|
1697
1759
|
reductions,
|
|
1698
1760
|
repaired: repaired.report.changed ? {
|
|
1699
1761
|
removedToolUses: repaired.report.removedToolUses,
|
|
@@ -1702,6 +1764,14 @@ var HybridCompactor = class {
|
|
|
1702
1764
|
} : void 0
|
|
1703
1765
|
};
|
|
1704
1766
|
}
|
|
1767
|
+
/**
|
|
1768
|
+
* Estimate the full API request token count: messages + systemPrompt + toolDefs.
|
|
1769
|
+
* This is the accurate figure for context-window pressure monitoring.
|
|
1770
|
+
*/
|
|
1771
|
+
estimateFullRequest(ctx) {
|
|
1772
|
+
const breakdown = estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []);
|
|
1773
|
+
return breakdown.total;
|
|
1774
|
+
}
|
|
1705
1775
|
eliseOldToolResults(ctx, preserveK = this.preserveK, eliseThreshold = this.eliseThreshold) {
|
|
1706
1776
|
const messages = ctx.messages;
|
|
1707
1777
|
let pairCount = 0;
|
|
@@ -2128,6 +2198,71 @@ var DefaultSecretScrubber = class {
|
|
|
2128
2198
|
|
|
2129
2199
|
// src/models/models-registry.ts
|
|
2130
2200
|
init_atomic_write();
|
|
2201
|
+
|
|
2202
|
+
// src/utils/merge-models-payload.ts
|
|
2203
|
+
function mergeModelsPayload(base, overlay) {
|
|
2204
|
+
const out = {};
|
|
2205
|
+
for (const [id, provider] of Object.entries(base)) {
|
|
2206
|
+
out[id] = cloneProvider(provider);
|
|
2207
|
+
}
|
|
2208
|
+
for (const [id, ovProvider] of Object.entries(overlay)) {
|
|
2209
|
+
const existing = out[id];
|
|
2210
|
+
out[id] = existing ? mergeProvider(existing, ovProvider) : cloneProvider(ovProvider);
|
|
2211
|
+
}
|
|
2212
|
+
return out;
|
|
2213
|
+
}
|
|
2214
|
+
function mergeProvider(base, overlay) {
|
|
2215
|
+
const models = {};
|
|
2216
|
+
for (const [mid, m] of Object.entries(base.models ?? {})) {
|
|
2217
|
+
models[mid] = { ...m };
|
|
2218
|
+
}
|
|
2219
|
+
for (const [mid, ovModel] of Object.entries(overlay.models ?? {})) {
|
|
2220
|
+
const existing = models[mid];
|
|
2221
|
+
models[mid] = existing ? mergeModel(existing, ovModel) : { ...ovModel };
|
|
2222
|
+
}
|
|
2223
|
+
return {
|
|
2224
|
+
...base,
|
|
2225
|
+
// Overlay scalar fields win when explicitly provided; otherwise keep base.
|
|
2226
|
+
...stripUndefined({
|
|
2227
|
+
id: overlay.id,
|
|
2228
|
+
name: overlay.name,
|
|
2229
|
+
npm: overlay.npm,
|
|
2230
|
+
api: overlay.api,
|
|
2231
|
+
env: overlay.env,
|
|
2232
|
+
doc: overlay.doc
|
|
2233
|
+
}),
|
|
2234
|
+
models
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
function mergeModel(base, overlay) {
|
|
2238
|
+
const merged = { ...base, ...overlay };
|
|
2239
|
+
if (base.limit || overlay.limit) {
|
|
2240
|
+
merged.limit = { ...base.limit, ...overlay.limit };
|
|
2241
|
+
}
|
|
2242
|
+
if (base.cost || overlay.cost) {
|
|
2243
|
+
merged.cost = { ...base.cost, ...overlay.cost };
|
|
2244
|
+
}
|
|
2245
|
+
if (base.modalities || overlay.modalities) {
|
|
2246
|
+
merged.modalities = { ...base.modalities, ...overlay.modalities };
|
|
2247
|
+
}
|
|
2248
|
+
return merged;
|
|
2249
|
+
}
|
|
2250
|
+
function cloneProvider(p) {
|
|
2251
|
+
const models = {};
|
|
2252
|
+
for (const [mid, m] of Object.entries(p.models ?? {})) {
|
|
2253
|
+
models[mid] = { ...m };
|
|
2254
|
+
}
|
|
2255
|
+
return { ...p, models };
|
|
2256
|
+
}
|
|
2257
|
+
function stripUndefined(obj) {
|
|
2258
|
+
const out = {};
|
|
2259
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
2260
|
+
if (v !== void 0) out[k] = v;
|
|
2261
|
+
}
|
|
2262
|
+
return out;
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
// src/models/models-registry.ts
|
|
2131
2266
|
var DEFAULT_URL = "https://models.dev/api.json";
|
|
2132
2267
|
var DEFAULT_TTL_SECONDS = 24 * 3600;
|
|
2133
2268
|
var FAMILY_BY_NPM = {
|
|
@@ -2155,7 +2290,10 @@ function classifyFamily(npm) {
|
|
|
2155
2290
|
return FAMILY_BY_NPM[npm] ?? "unsupported";
|
|
2156
2291
|
}
|
|
2157
2292
|
var DefaultModelsRegistry = class {
|
|
2293
|
+
/** Merged (base + overlay) payload — what every reader sees. */
|
|
2158
2294
|
payload;
|
|
2295
|
+
/** Memoised overlay payload (in-memory / fetched / file). */
|
|
2296
|
+
overlayPayload;
|
|
2159
2297
|
fetchedAt;
|
|
2160
2298
|
cacheFile;
|
|
2161
2299
|
url;
|
|
@@ -2163,6 +2301,10 @@ var DefaultModelsRegistry = class {
|
|
|
2163
2301
|
fetchImpl;
|
|
2164
2302
|
seed;
|
|
2165
2303
|
maxStaleAgeMs;
|
|
2304
|
+
overlay;
|
|
2305
|
+
overlayUrl;
|
|
2306
|
+
overlayFile;
|
|
2307
|
+
overlayCacheFile;
|
|
2166
2308
|
constructor(opts) {
|
|
2167
2309
|
this.cacheFile = opts.cacheFile;
|
|
2168
2310
|
this.url = opts.url ?? DEFAULT_URL;
|
|
@@ -2171,6 +2313,10 @@ var DefaultModelsRegistry = class {
|
|
|
2171
2313
|
this.seed = opts.seed;
|
|
2172
2314
|
const maxStaleSeconds = opts.maxStaleAgeSeconds ?? 7 * 24 * 3600;
|
|
2173
2315
|
this.maxStaleAgeMs = maxStaleSeconds * 1e3;
|
|
2316
|
+
this.overlay = opts.overlay;
|
|
2317
|
+
this.overlayUrl = opts.overlayUrl;
|
|
2318
|
+
this.overlayFile = opts.overlayFile;
|
|
2319
|
+
this.overlayCacheFile = opts.overlayCacheFile ?? (opts.overlayUrl ? path6.join(path6.dirname(opts.cacheFile), "models-overlay-cache.json") : void 0);
|
|
2174
2320
|
}
|
|
2175
2321
|
async load(opts = {}) {
|
|
2176
2322
|
if (this.payload && !opts.force) return this.payload;
|
|
@@ -2179,27 +2325,45 @@ var DefaultModelsRegistry = class {
|
|
|
2179
2325
|
this.fetchedAt = /* @__PURE__ */ new Date();
|
|
2180
2326
|
return this.payload;
|
|
2181
2327
|
}
|
|
2328
|
+
const overlay = await this.loadOverlay(opts);
|
|
2329
|
+
const base = await this.loadBase(opts, Object.keys(overlay).length > 0);
|
|
2330
|
+
this.payload = mergeModelsPayload(base, overlay);
|
|
2331
|
+
return this.payload;
|
|
2332
|
+
}
|
|
2333
|
+
/**
|
|
2334
|
+
* Load the models.dev base payload: fresh cache → network → stale cache.
|
|
2335
|
+
* On total failure, degrade to `{}` (so a non-empty overlay still drives
|
|
2336
|
+
* the catalog) rather than throwing — unless there's no curated overlay to
|
|
2337
|
+
* fall back on, in which case the original error propagates so pure-
|
|
2338
|
+
* models.dev setups still surface the problem.
|
|
2339
|
+
*/
|
|
2340
|
+
async loadBase(opts = {}, overlayAvailable = false) {
|
|
2182
2341
|
if (!opts.force) {
|
|
2183
|
-
const cached = await this.
|
|
2342
|
+
const cached = await this.readCacheAt(this.cacheFile);
|
|
2184
2343
|
if (cached && this.isFresh(cached.fetchedAt)) {
|
|
2185
|
-
this.payload = cached.payload;
|
|
2186
2344
|
this.fetchedAt = new Date(cached.fetchedAt);
|
|
2187
2345
|
return cached.payload;
|
|
2188
2346
|
}
|
|
2189
2347
|
}
|
|
2190
2348
|
try {
|
|
2191
|
-
return await this.
|
|
2349
|
+
return await this.refreshBase();
|
|
2192
2350
|
} catch (err) {
|
|
2193
|
-
const cached = await this.
|
|
2351
|
+
const cached = await this.readCacheAt(this.cacheFile);
|
|
2194
2352
|
if (cached && this.isWithinMaxStaleAge(cached.fetchedAt)) {
|
|
2195
|
-
this.payload = cached.payload;
|
|
2196
2353
|
this.fetchedAt = new Date(cached.fetchedAt);
|
|
2197
2354
|
return cached.payload;
|
|
2198
2355
|
}
|
|
2356
|
+
if (overlayAvailable) {
|
|
2357
|
+
console.warn(
|
|
2358
|
+
`ModelsRegistry: models.dev unavailable (${err instanceof Error ? err.message : String(err)}); serving curated overlay only.`
|
|
2359
|
+
);
|
|
2360
|
+
return {};
|
|
2361
|
+
}
|
|
2199
2362
|
throw err;
|
|
2200
2363
|
}
|
|
2201
2364
|
}
|
|
2202
|
-
|
|
2365
|
+
/** Fetch + cache the models.dev base. Throws on failure (used by `refresh`). */
|
|
2366
|
+
async refreshBase() {
|
|
2203
2367
|
const res = await this.fetchImpl(this.url, {
|
|
2204
2368
|
method: "GET",
|
|
2205
2369
|
headers: { accept: "application/json" }
|
|
@@ -2208,7 +2372,6 @@ var DefaultModelsRegistry = class {
|
|
|
2208
2372
|
throw new Error(`ModelsRegistry: HTTP ${res.status} fetching ${this.url}`);
|
|
2209
2373
|
}
|
|
2210
2374
|
const json = await res.json();
|
|
2211
|
-
this.payload = json;
|
|
2212
2375
|
this.fetchedAt = /* @__PURE__ */ new Date();
|
|
2213
2376
|
const envelope = {
|
|
2214
2377
|
fetchedAt: this.fetchedAt.toISOString(),
|
|
@@ -2218,6 +2381,68 @@ var DefaultModelsRegistry = class {
|
|
|
2218
2381
|
await atomicWrite(this.cacheFile, JSON.stringify(envelope));
|
|
2219
2382
|
return json;
|
|
2220
2383
|
}
|
|
2384
|
+
/**
|
|
2385
|
+
* Resolve the curated overlay, memoised. Order: in-memory `overlay` →
|
|
2386
|
+
* fetched `overlayUrl` (cached, same TTL/stale rules) → `overlayFile` on
|
|
2387
|
+
* disk. Never throws — a missing/broken overlay yields `{}`.
|
|
2388
|
+
*/
|
|
2389
|
+
async loadOverlay(opts = {}) {
|
|
2390
|
+
if (this.overlayPayload && !opts.force) return this.overlayPayload;
|
|
2391
|
+
if (this.overlay) {
|
|
2392
|
+
this.overlayPayload = this.overlay;
|
|
2393
|
+
return this.overlayPayload;
|
|
2394
|
+
}
|
|
2395
|
+
const fetched = await this.loadOverlayFromUrl(opts);
|
|
2396
|
+
if (fetched) {
|
|
2397
|
+
this.overlayPayload = fetched;
|
|
2398
|
+
return fetched;
|
|
2399
|
+
}
|
|
2400
|
+
const fromFile = await this.readOverlayFile();
|
|
2401
|
+
this.overlayPayload = fromFile ?? {};
|
|
2402
|
+
return this.overlayPayload;
|
|
2403
|
+
}
|
|
2404
|
+
async loadOverlayFromUrl(opts) {
|
|
2405
|
+
if (!this.overlayUrl || !this.overlayCacheFile) return void 0;
|
|
2406
|
+
if (!opts.force) {
|
|
2407
|
+
const cached = await this.readCacheAt(this.overlayCacheFile);
|
|
2408
|
+
if (cached && this.isFresh(cached.fetchedAt)) return cached.payload;
|
|
2409
|
+
}
|
|
2410
|
+
try {
|
|
2411
|
+
const res = await this.fetchImpl(this.overlayUrl, {
|
|
2412
|
+
method: "GET",
|
|
2413
|
+
headers: { accept: "application/json" }
|
|
2414
|
+
});
|
|
2415
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
2416
|
+
const json = await res.json();
|
|
2417
|
+
const envelope = {
|
|
2418
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2419
|
+
url: this.overlayUrl,
|
|
2420
|
+
payload: json
|
|
2421
|
+
};
|
|
2422
|
+
await atomicWrite(this.overlayCacheFile, JSON.stringify(envelope)).catch(() => {
|
|
2423
|
+
});
|
|
2424
|
+
return json;
|
|
2425
|
+
} catch {
|
|
2426
|
+
const cached = await this.readCacheAt(this.overlayCacheFile);
|
|
2427
|
+
if (cached && this.isWithinMaxStaleAge(cached.fetchedAt)) return cached.payload;
|
|
2428
|
+
return void 0;
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
async readOverlayFile() {
|
|
2432
|
+
if (!this.overlayFile) return void 0;
|
|
2433
|
+
try {
|
|
2434
|
+
const raw = await fsp2.readFile(this.overlayFile, "utf8");
|
|
2435
|
+
return JSON.parse(raw);
|
|
2436
|
+
} catch {
|
|
2437
|
+
return void 0;
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
async refresh() {
|
|
2441
|
+
const base = await this.refreshBase();
|
|
2442
|
+
const overlay = await this.loadOverlay({ force: true });
|
|
2443
|
+
this.payload = mergeModelsPayload(base, overlay);
|
|
2444
|
+
return this.payload;
|
|
2445
|
+
}
|
|
2221
2446
|
async listProviders() {
|
|
2222
2447
|
const payload = await this.load();
|
|
2223
2448
|
return Object.values(payload).map((p) => this.resolveProvider(p));
|
|
@@ -2258,7 +2483,7 @@ var DefaultModelsRegistry = class {
|
|
|
2258
2483
|
}
|
|
2259
2484
|
async ageSeconds() {
|
|
2260
2485
|
if (!this.fetchedAt) {
|
|
2261
|
-
const cached = await this.
|
|
2486
|
+
const cached = await this.readCacheAt(this.cacheFile);
|
|
2262
2487
|
if (!cached) return Number.POSITIVE_INFINITY;
|
|
2263
2488
|
return (Date.now() - new Date(cached.fetchedAt).getTime()) / 1e3;
|
|
2264
2489
|
}
|
|
@@ -2282,9 +2507,9 @@ var DefaultModelsRegistry = class {
|
|
|
2282
2507
|
isWithinMaxStaleAge(fetchedAtIso) {
|
|
2283
2508
|
return Date.now() - new Date(fetchedAtIso).getTime() < this.maxStaleAgeMs;
|
|
2284
2509
|
}
|
|
2285
|
-
async
|
|
2510
|
+
async readCacheAt(file) {
|
|
2286
2511
|
try {
|
|
2287
|
-
const raw = await fsp2.readFile(
|
|
2512
|
+
const raw = await fsp2.readFile(file, "utf8");
|
|
2288
2513
|
return JSON.parse(raw);
|
|
2289
2514
|
} catch {
|
|
2290
2515
|
return void 0;
|
|
@@ -2856,6 +3081,141 @@ function topologicalSort(graph) {
|
|
|
2856
3081
|
return result;
|
|
2857
3082
|
}
|
|
2858
3083
|
|
|
3084
|
+
// src/security/capabilities.ts
|
|
3085
|
+
var ToolCapabilities = {
|
|
3086
|
+
/** Can execute arbitrary commands in the user's shell (the `bash` tool). */
|
|
3087
|
+
SHELL_ARBITRARY: "shell.arbitrary",
|
|
3088
|
+
/** Can write / modify / delete files inside the project. */
|
|
3089
|
+
FS_WRITE: "fs.write",
|
|
3090
|
+
/** Can write files outside the current project root (very high risk). */
|
|
3091
|
+
FS_WRITE_OUTSIDE_PROJECT: "fs.write.outside-project",
|
|
3092
|
+
/** Proxies tools from external MCP servers (unknown capability). */
|
|
3093
|
+
MCP_PROXY: "mcp.proxy",
|
|
3094
|
+
/** Can spawn or manage subagents / multi-agent tasks. */
|
|
3095
|
+
SUBAGENT_SPAWN: "subagent.spawn",
|
|
3096
|
+
/** Can mutate global or session configuration / trust state. */
|
|
3097
|
+
CONFIG_MUTATE: "config.mutate",
|
|
3098
|
+
/** Can install packages or run package managers with side effects. */
|
|
3099
|
+
PACKAGE_INSTALL: "package.install"
|
|
3100
|
+
};
|
|
3101
|
+
var DANGEROUS_FOR_SUBAGENTS = [
|
|
3102
|
+
ToolCapabilities.SHELL_ARBITRARY,
|
|
3103
|
+
ToolCapabilities.FS_WRITE,
|
|
3104
|
+
ToolCapabilities.FS_WRITE_OUTSIDE_PROJECT,
|
|
3105
|
+
ToolCapabilities.MCP_PROXY,
|
|
3106
|
+
ToolCapabilities.SUBAGENT_SPAWN,
|
|
3107
|
+
ToolCapabilities.CONFIG_MUTATE,
|
|
3108
|
+
ToolCapabilities.PACKAGE_INSTALL
|
|
3109
|
+
];
|
|
3110
|
+
function hasDangerousCapabilityForSubagents(toolOrCaps) {
|
|
3111
|
+
if (!toolOrCaps) return false;
|
|
3112
|
+
const input = toolOrCaps;
|
|
3113
|
+
const caps = Array.isArray(input) ? input : input?.capabilities ?? [];
|
|
3114
|
+
return caps.some((c) => DANGEROUS_FOR_SUBAGENTS.includes(c));
|
|
3115
|
+
}
|
|
3116
|
+
function getDangerousCapabilities(toolOrCaps) {
|
|
3117
|
+
if (!toolOrCaps) return [];
|
|
3118
|
+
const input = toolOrCaps;
|
|
3119
|
+
const caps = Array.isArray(input) ? input : input?.capabilities ?? [];
|
|
3120
|
+
return caps.filter(
|
|
3121
|
+
(c) => DANGEROUS_FOR_SUBAGENTS.includes(c)
|
|
3122
|
+
);
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
// src/utils/json-schema-validate.ts
|
|
3126
|
+
function validateAgainstSchema(value, schema) {
|
|
3127
|
+
const errors = [];
|
|
3128
|
+
walk2(value, schema, "", errors);
|
|
3129
|
+
return { ok: errors.length === 0, errors };
|
|
3130
|
+
}
|
|
3131
|
+
function walk2(value, schema, path35, errors) {
|
|
3132
|
+
if (schema.enum !== void 0) {
|
|
3133
|
+
if (!schema.enum.some((e) => deepEqual(e, value))) {
|
|
3134
|
+
errors.push({
|
|
3135
|
+
path: path35 || "<root>",
|
|
3136
|
+
message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
|
|
3137
|
+
});
|
|
3138
|
+
return;
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
if (typeof schema.type === "string") {
|
|
3142
|
+
if (!checkType(value, schema.type)) {
|
|
3143
|
+
errors.push({
|
|
3144
|
+
path: path35 || "<root>",
|
|
3145
|
+
message: `expected ${schema.type}, got ${describeType(value)}`
|
|
3146
|
+
});
|
|
3147
|
+
return;
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
if (schema.type === "object" && isPlainObject(value)) {
|
|
3151
|
+
const obj = value;
|
|
3152
|
+
for (const req of schema.required ?? []) {
|
|
3153
|
+
if (!(req in obj)) {
|
|
3154
|
+
errors.push({ path: joinPath(path35, req), message: "required property missing" });
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
if (schema.properties) {
|
|
3158
|
+
for (const [key, subSchema] of Object.entries(schema.properties)) {
|
|
3159
|
+
if (key in obj) {
|
|
3160
|
+
walk2(obj[key], subSchema, joinPath(path35, key), errors);
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
if (schema.type === "array" && Array.isArray(value) && schema.items) {
|
|
3166
|
+
value.forEach((item, i) => walk2(item, schema.items, `${path35}[${i}]`, errors));
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
function checkType(value, type) {
|
|
3170
|
+
switch (type) {
|
|
3171
|
+
case "string":
|
|
3172
|
+
return typeof value === "string";
|
|
3173
|
+
case "number":
|
|
3174
|
+
return typeof value === "number" && !Number.isNaN(value);
|
|
3175
|
+
case "integer":
|
|
3176
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
3177
|
+
case "boolean":
|
|
3178
|
+
return typeof value === "boolean";
|
|
3179
|
+
case "null":
|
|
3180
|
+
return value === null;
|
|
3181
|
+
case "array":
|
|
3182
|
+
return Array.isArray(value);
|
|
3183
|
+
case "object":
|
|
3184
|
+
return isPlainObject(value);
|
|
3185
|
+
default:
|
|
3186
|
+
return true;
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
function isPlainObject(v) {
|
|
3190
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
3191
|
+
}
|
|
3192
|
+
function describeType(v) {
|
|
3193
|
+
if (v === null) return "null";
|
|
3194
|
+
if (Array.isArray(v)) return "array";
|
|
3195
|
+
return typeof v;
|
|
3196
|
+
}
|
|
3197
|
+
function joinPath(parent, key) {
|
|
3198
|
+
if (!parent) return key;
|
|
3199
|
+
return `${parent}.${key}`;
|
|
3200
|
+
}
|
|
3201
|
+
function deepEqual(a, b) {
|
|
3202
|
+
if (a === b) return true;
|
|
3203
|
+
if (typeof a !== typeof b) return false;
|
|
3204
|
+
if (a === null || b === null) return a === b;
|
|
3205
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
3206
|
+
return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
|
|
3207
|
+
}
|
|
3208
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
3209
|
+
const ak = Object.keys(a);
|
|
3210
|
+
const bk = Object.keys(b);
|
|
3211
|
+
if (ak.length !== bk.length) return false;
|
|
3212
|
+
return ak.every(
|
|
3213
|
+
(k) => deepEqual(a[k], b[k])
|
|
3214
|
+
);
|
|
3215
|
+
}
|
|
3216
|
+
return false;
|
|
3217
|
+
}
|
|
3218
|
+
|
|
2859
3219
|
// src/utils/tool-output-serializer.ts
|
|
2860
3220
|
function createToolOutputSerializer(opts = {}) {
|
|
2861
3221
|
const capBytes = opts.perIterationOutputCapBytes ?? 1e5;
|
|
@@ -2937,18 +3297,45 @@ var ToolExecutor = class {
|
|
|
2937
3297
|
budget = this.decrementBudget(result, budget);
|
|
2938
3298
|
return { result, tool, durationMs: Date.now() - start };
|
|
2939
3299
|
}
|
|
3300
|
+
const validation = validateAgainstSchema(use.input, tool.inputSchema);
|
|
3301
|
+
if (!validation.ok) {
|
|
3302
|
+
const errorDetails = validation.errors.map((e) => ` - ${e.path || "input"}: ${e.message}`).join("\n");
|
|
3303
|
+
const result = {
|
|
3304
|
+
type: "tool_result",
|
|
3305
|
+
tool_use_id: use.id,
|
|
3306
|
+
content: `Invalid arguments for tool "${tool.name}".
|
|
3307
|
+
|
|
3308
|
+
Validation errors:
|
|
3309
|
+
${errorDetails}
|
|
3310
|
+
|
|
3311
|
+
Please call the tool again with arguments that match its inputSchema. You can use the "tool-help" tool with name="${tool.name}" to see the exact expected schema.`,
|
|
3312
|
+
is_error: true
|
|
3313
|
+
};
|
|
3314
|
+
budget = this.decrementBudget(result, budget);
|
|
3315
|
+
return { result, tool, durationMs: Date.now() - start };
|
|
3316
|
+
}
|
|
3317
|
+
const toolDangerousCaps = getDangerousCapabilities(tool);
|
|
3318
|
+
if (toolDangerousCaps.length > 0) {
|
|
3319
|
+
if (this.opts.events) ;
|
|
3320
|
+
}
|
|
2940
3321
|
if (hasMalformedArguments(use.input)) {
|
|
2941
3322
|
const result = this.malformedInputResult(use, extractMalformedRaw(use.input));
|
|
2942
3323
|
budget = this.decrementBudget(result, budget);
|
|
2943
3324
|
return { result, tool, durationMs: Date.now() - start };
|
|
2944
3325
|
}
|
|
2945
3326
|
const decision = await this.opts.permissionPolicy.evaluate(tool, use.input, ctx);
|
|
2946
|
-
|
|
3327
|
+
let effectivePermission = decision.permission;
|
|
3328
|
+
const policy = this.opts.permissionPolicy;
|
|
3329
|
+
const yolo = policy.getYolo?.() === true || policy.getForceAllYolo?.() === true;
|
|
3330
|
+
if (toolDangerousCaps.length > 0 && effectivePermission === "auto" && !yolo) {
|
|
3331
|
+
effectivePermission = "confirm";
|
|
3332
|
+
}
|
|
3333
|
+
if (effectivePermission === "deny") {
|
|
2947
3334
|
const result = this.deniedResult(use, decision.reason);
|
|
2948
3335
|
budget = this.decrementBudget(result, budget);
|
|
2949
3336
|
return { result, tool, durationMs: Date.now() - start };
|
|
2950
3337
|
}
|
|
2951
|
-
if (
|
|
3338
|
+
if (effectivePermission === "confirm") {
|
|
2952
3339
|
if (this.opts.confirmAwaiter) {
|
|
2953
3340
|
const choice = await this.opts.confirmAwaiter(tool, use.input, use.id, tool.name);
|
|
2954
3341
|
if (choice !== "yes" && choice !== "always") {
|
|
@@ -2973,10 +3360,13 @@ var ToolExecutor = class {
|
|
|
2973
3360
|
return { result: pending, tool, durationMs: Date.now() - start };
|
|
2974
3361
|
}
|
|
2975
3362
|
}
|
|
3363
|
+
const toolCapsForAudit = hasDangerousCapabilityForSubagents(tool) ? tool.capabilities ?? [] : [];
|
|
2976
3364
|
const span = this.opts.tracer?.startSpan(`tool.${tool.name}`, {
|
|
2977
3365
|
"tool.name": tool.name,
|
|
2978
3366
|
"tool.mutating": tool.mutating,
|
|
2979
|
-
"tool.permission": tool.permission
|
|
3367
|
+
"tool.permission": tool.permission,
|
|
3368
|
+
"tool.capabilities": JSON.stringify(tool.capabilities ?? []),
|
|
3369
|
+
"tool.has_dangerous_capabilities": toolCapsForAudit.length > 0
|
|
2980
3370
|
});
|
|
2981
3371
|
try {
|
|
2982
3372
|
const result = await this.executeTool(tool, use, ctx, budget);
|
|
@@ -3919,9 +4309,11 @@ function resolveWstackPaths(opts) {
|
|
|
3919
4309
|
globalPrompts: path6.join(globalRoot, "prompts"),
|
|
3920
4310
|
cacheDir: path6.join(globalRoot, "cache"),
|
|
3921
4311
|
modelsCache: path6.join(globalRoot, "cache", "models.dev.json"),
|
|
4312
|
+
modelsOverlayCache: path6.join(globalRoot, "cache", "models-overlay.json"),
|
|
3922
4313
|
historyFile: path6.join(globalRoot, "history"),
|
|
3923
4314
|
logFile: path6.join(globalRoot, "logs", "wrongstack.log"),
|
|
3924
4315
|
projectDir,
|
|
4316
|
+
projectCodebaseIndex: path6.join(projectDir, "codebase-index"),
|
|
3925
4317
|
projectMemory: path6.join(projectDir, "memory.md"),
|
|
3926
4318
|
projectSessions: path6.join(projectDir, "sessions"),
|
|
3927
4319
|
projectTrust: path6.join(projectDir, "trust.json"),
|
|
@@ -4030,175 +4422,86 @@ function buildChildEnv(optsOrSessionId) {
|
|
|
4030
4422
|
return out;
|
|
4031
4423
|
}
|
|
4032
4424
|
|
|
4033
|
-
// src/utils/json-
|
|
4034
|
-
function
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4425
|
+
// src/utils/json-repair.ts
|
|
4426
|
+
function completePartialObject(s) {
|
|
4427
|
+
if (!s.trim().startsWith("{")) return s;
|
|
4428
|
+
if (tryParse(s).ok) return s;
|
|
4429
|
+
const stack = [];
|
|
4430
|
+
let inString = false;
|
|
4431
|
+
let escaped = false;
|
|
4432
|
+
let sawKey = false;
|
|
4433
|
+
let prevSig = "";
|
|
4434
|
+
let contentEnd = 0;
|
|
4435
|
+
let stringBraceDepth = 0;
|
|
4436
|
+
for (let i = 0; i < s.length; i++) {
|
|
4437
|
+
const ch = s[i];
|
|
4438
|
+
if (inString) {
|
|
4439
|
+
contentEnd = i + 1;
|
|
4440
|
+
if (escaped) {
|
|
4441
|
+
escaped = false;
|
|
4442
|
+
continue;
|
|
4443
|
+
}
|
|
4444
|
+
if (ch === "\\") {
|
|
4445
|
+
escaped = true;
|
|
4446
|
+
continue;
|
|
4447
|
+
}
|
|
4448
|
+
if (ch === '"') {
|
|
4449
|
+
inString = false;
|
|
4450
|
+
prevSig = '"';
|
|
4451
|
+
stringBraceDepth = 0;
|
|
4452
|
+
continue;
|
|
4453
|
+
}
|
|
4454
|
+
if (ch === "{") stringBraceDepth++;
|
|
4455
|
+
else if (ch === "}" && stringBraceDepth > 0) stringBraceDepth--;
|
|
4456
|
+
continue;
|
|
4047
4457
|
}
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
if (
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4458
|
+
if (ch === " " || ch === " " || ch === "\n" || ch === "\r") continue;
|
|
4459
|
+
contentEnd = i + 1;
|
|
4460
|
+
if (ch === '"') {
|
|
4461
|
+
inString = true;
|
|
4462
|
+
sawKey = true;
|
|
4463
|
+
stringBraceDepth = 0;
|
|
4464
|
+
prevSig = '"';
|
|
4465
|
+
} else if (ch === "{" || ch === "[") {
|
|
4466
|
+
stack.push(ch);
|
|
4467
|
+
prevSig = ch;
|
|
4468
|
+
} else if (ch === "}" || ch === "]") {
|
|
4469
|
+
stack.pop();
|
|
4470
|
+
prevSig = ch;
|
|
4471
|
+
} else {
|
|
4472
|
+
prevSig = ch;
|
|
4056
4473
|
}
|
|
4057
4474
|
}
|
|
4058
|
-
if (
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
if (schema.properties) {
|
|
4066
|
-
for (const [key, subSchema] of Object.entries(schema.properties)) {
|
|
4067
|
-
if (key in obj) {
|
|
4068
|
-
walk2(obj[key], subSchema, joinPath(path31, key), errors);
|
|
4069
|
-
}
|
|
4070
|
-
}
|
|
4475
|
+
if (!sawKey && !inString) return s;
|
|
4476
|
+
let result = s.slice(0, contentEnd);
|
|
4477
|
+
if (inString) {
|
|
4478
|
+
if (escaped) {
|
|
4479
|
+
result = result.slice(0, -1);
|
|
4480
|
+
} else if (endsWithInvalidEscape(result)) {
|
|
4481
|
+
result = result.slice(0, -2);
|
|
4071
4482
|
}
|
|
4483
|
+
if (stringBraceDepth > 0) result += "}".repeat(stringBraceDepth);
|
|
4484
|
+
result += '"';
|
|
4485
|
+
} else if (prevSig === ":") {
|
|
4486
|
+
result += "null";
|
|
4072
4487
|
}
|
|
4073
|
-
|
|
4074
|
-
|
|
4488
|
+
for (let k = stack.length - 1; k >= 0; k--) {
|
|
4489
|
+
result += stack[k] === "{" ? "}" : "]";
|
|
4075
4490
|
}
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
case "string":
|
|
4080
|
-
return typeof value === "string";
|
|
4081
|
-
case "number":
|
|
4082
|
-
return typeof value === "number" && !Number.isNaN(value);
|
|
4083
|
-
case "integer":
|
|
4084
|
-
return typeof value === "number" && Number.isInteger(value);
|
|
4085
|
-
case "boolean":
|
|
4086
|
-
return typeof value === "boolean";
|
|
4087
|
-
case "null":
|
|
4088
|
-
return value === null;
|
|
4089
|
-
case "array":
|
|
4090
|
-
return Array.isArray(value);
|
|
4091
|
-
case "object":
|
|
4092
|
-
return isPlainObject(value);
|
|
4093
|
-
default:
|
|
4094
|
-
return true;
|
|
4491
|
+
if (!tryParse(result).ok) {
|
|
4492
|
+
const patched = result.replace(/:(\s*)([}\]])/g, ":null$2");
|
|
4493
|
+
if (tryParse(patched).ok) result = patched;
|
|
4095
4494
|
}
|
|
4495
|
+
return result;
|
|
4096
4496
|
}
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
if (
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
function joinPath(parent, key) {
|
|
4106
|
-
if (!parent) return key;
|
|
4107
|
-
return `${parent}.${key}`;
|
|
4108
|
-
}
|
|
4109
|
-
function deepEqual(a, b) {
|
|
4110
|
-
if (a === b) return true;
|
|
4111
|
-
if (typeof a !== typeof b) return false;
|
|
4112
|
-
if (a === null || b === null) return a === b;
|
|
4113
|
-
if (Array.isArray(a) && Array.isArray(b)) {
|
|
4114
|
-
return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
|
|
4115
|
-
}
|
|
4116
|
-
if (typeof a === "object" && typeof b === "object") {
|
|
4117
|
-
const ak = Object.keys(a);
|
|
4118
|
-
const bk = Object.keys(b);
|
|
4119
|
-
if (ak.length !== bk.length) return false;
|
|
4120
|
-
return ak.every(
|
|
4121
|
-
(k) => deepEqual(a[k], b[k])
|
|
4122
|
-
);
|
|
4123
|
-
}
|
|
4124
|
-
return false;
|
|
4125
|
-
}
|
|
4126
|
-
|
|
4127
|
-
// src/utils/json-repair.ts
|
|
4128
|
-
function completePartialObject(s) {
|
|
4129
|
-
let result = s;
|
|
4130
|
-
const trimmed = result.trim();
|
|
4131
|
-
if (!trimmed.startsWith("{")) return s;
|
|
4132
|
-
for (let pass = 0; pass < 3; pass++) {
|
|
4133
|
-
let braceDepth = 0;
|
|
4134
|
-
let inString2 = false;
|
|
4135
|
-
let escaped2 = false;
|
|
4136
|
-
let foundClose = false;
|
|
4137
|
-
for (const ch of result) {
|
|
4138
|
-
if (escaped2) {
|
|
4139
|
-
escaped2 = false;
|
|
4140
|
-
continue;
|
|
4141
|
-
}
|
|
4142
|
-
if (ch === "\\") {
|
|
4143
|
-
escaped2 = true;
|
|
4144
|
-
continue;
|
|
4145
|
-
}
|
|
4146
|
-
if (ch === '"') {
|
|
4147
|
-
inString2 = !inString2;
|
|
4148
|
-
continue;
|
|
4149
|
-
}
|
|
4150
|
-
if (inString2) continue;
|
|
4151
|
-
if (ch === "{") {
|
|
4152
|
-
braceDepth++;
|
|
4153
|
-
foundClose = false;
|
|
4154
|
-
} else if (ch === "}") {
|
|
4155
|
-
braceDepth--;
|
|
4156
|
-
if (braceDepth === 0) foundClose = true;
|
|
4157
|
-
}
|
|
4158
|
-
}
|
|
4159
|
-
if (foundClose || braceDepth <= 0) break;
|
|
4160
|
-
result += "}".repeat(braceDepth);
|
|
4161
|
-
}
|
|
4162
|
-
if (tryParse(result).ok) return result;
|
|
4163
|
-
let inString = false;
|
|
4164
|
-
let escaped = false;
|
|
4165
|
-
for (let i = result.length - 1; i >= 0; i--) {
|
|
4166
|
-
const ch = result[i];
|
|
4167
|
-
if (escaped) {
|
|
4168
|
-
escaped = false;
|
|
4169
|
-
continue;
|
|
4170
|
-
}
|
|
4171
|
-
if (ch === "\\") {
|
|
4172
|
-
escaped = true;
|
|
4173
|
-
continue;
|
|
4174
|
-
}
|
|
4175
|
-
if (ch === '"') {
|
|
4176
|
-
let nextNonWs;
|
|
4177
|
-
for (let j = i + 1; j < result.length; j++) {
|
|
4178
|
-
const nc = result[j];
|
|
4179
|
-
if (nc === " " || nc === " " || nc === "\n" || nc === "\r") continue;
|
|
4180
|
-
nextNonWs = result[j];
|
|
4181
|
-
break;
|
|
4182
|
-
}
|
|
4183
|
-
if (nextNonWs === ":") {
|
|
4184
|
-
continue;
|
|
4185
|
-
} else {
|
|
4186
|
-
inString = !inString;
|
|
4187
|
-
continue;
|
|
4188
|
-
}
|
|
4189
|
-
}
|
|
4190
|
-
}
|
|
4191
|
-
if (inString) {
|
|
4192
|
-
result = result.trimEnd();
|
|
4193
|
-
if (result.endsWith("\\")) result = result.slice(0, -1);
|
|
4194
|
-
let depth = 0;
|
|
4195
|
-
for (const ch of result) {
|
|
4196
|
-
if (ch === "{") depth++;
|
|
4197
|
-
else if (ch === "}") depth = Math.max(0, depth - 1);
|
|
4198
|
-
}
|
|
4199
|
-
result += '"' + "}".repeat(Math.max(1, depth));
|
|
4200
|
-
}
|
|
4201
|
-
return result;
|
|
4497
|
+
var VALID_ESCAPE = /* @__PURE__ */ new Set(['"', "\\", "/", "b", "f", "n", "r", "t", "u"]);
|
|
4498
|
+
function endsWithInvalidEscape(str) {
|
|
4499
|
+
const last = str[str.length - 1];
|
|
4500
|
+
if (str[str.length - 2] !== "\\" || last === void 0) return false;
|
|
4501
|
+
if (VALID_ESCAPE.has(last)) return false;
|
|
4502
|
+
let backslashes = 0;
|
|
4503
|
+
for (let k = str.length - 2; k >= 0 && str[k] === "\\"; k--) backslashes++;
|
|
4504
|
+
return backslashes % 2 === 1;
|
|
4202
4505
|
}
|
|
4203
4506
|
function tryParse(s) {
|
|
4204
4507
|
try {
|
|
@@ -4340,8 +4643,8 @@ var DefaultSessionStore = class {
|
|
|
4340
4643
|
return JSON.parse(raw);
|
|
4341
4644
|
} catch {
|
|
4342
4645
|
const full = this.sessionPath(id, ".jsonl");
|
|
4343
|
-
const
|
|
4344
|
-
const summary = await this.summarize(id,
|
|
4646
|
+
const stat8 = await fsp2.stat(full);
|
|
4647
|
+
const summary = await this.summarize(id, stat8.mtime.toISOString());
|
|
4345
4648
|
await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
|
|
4346
4649
|
console.warn(
|
|
4347
4650
|
`[session-store] Failed to write manifest for "${id}":`,
|
|
@@ -4534,7 +4837,6 @@ var FileSessionWriter = class {
|
|
|
4534
4837
|
}
|
|
4535
4838
|
return event;
|
|
4536
4839
|
}
|
|
4537
|
-
promptIndex = 0;
|
|
4538
4840
|
pendingFileSnapshots = [];
|
|
4539
4841
|
/** Tracks open tool_use IDs during the current run to serialize on close for resume. */
|
|
4540
4842
|
openToolUses = /* @__PURE__ */ new Set();
|
|
@@ -4625,7 +4927,6 @@ var FileSessionWriter = class {
|
|
|
4625
4927
|
await this.writeFileSnapshot(promptIndex, [...this.pendingFileSnapshots]);
|
|
4626
4928
|
this.pendingFileSnapshots = [];
|
|
4627
4929
|
}
|
|
4628
|
-
this.promptIndex = promptIndex + 1;
|
|
4629
4930
|
await this.append({
|
|
4630
4931
|
type: "checkpoint",
|
|
4631
4932
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -4721,6 +5022,38 @@ var FileSessionWriter = class {
|
|
|
4721
5022
|
`;
|
|
4722
5023
|
await fsp2.writeFile(this.filePath, record, "utf8");
|
|
4723
5024
|
}
|
|
5025
|
+
/**
|
|
5026
|
+
* Idea #1 — write an in-flight marker. The agent loop should call
|
|
5027
|
+
* this at the start of each long-running operation; a matching
|
|
5028
|
+
* `clearInFlightMarker` follows on clean exit. A stale marker
|
|
5029
|
+
* (no end) is what `SessionRecovery.detectStale` looks for.
|
|
5030
|
+
*/
|
|
5031
|
+
async writeInFlightMarker(context) {
|
|
5032
|
+
if (!context || context.length > 500) {
|
|
5033
|
+
throw new Error("In-flight context must be 1..500 chars");
|
|
5034
|
+
}
|
|
5035
|
+
await this.append({
|
|
5036
|
+
type: "in_flight_start",
|
|
5037
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5038
|
+
context
|
|
5039
|
+
});
|
|
5040
|
+
this.events?.emit("in_flight.started", { context, ts: (/* @__PURE__ */ new Date()).toISOString() });
|
|
5041
|
+
}
|
|
5042
|
+
/**
|
|
5043
|
+
* Idea #1 — close the in-flight marker. Idempotent in spirit
|
|
5044
|
+
* (you can call it after a successful iteration even if you
|
|
5045
|
+
* didn't open one this round) — but the session log records
|
|
5046
|
+
* every call so postmortem tooling can see "the agent finished
|
|
5047
|
+
* cleanly X times, then died without finishing Y".
|
|
5048
|
+
*/
|
|
5049
|
+
async clearInFlightMarker(reason) {
|
|
5050
|
+
await this.append({
|
|
5051
|
+
type: "in_flight_end",
|
|
5052
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5053
|
+
reason
|
|
5054
|
+
});
|
|
5055
|
+
this.events?.emit("in_flight.ended", { reason, ts: (/* @__PURE__ */ new Date()).toISOString() });
|
|
5056
|
+
}
|
|
4724
5057
|
};
|
|
4725
5058
|
function userInputTitle(content) {
|
|
4726
5059
|
const text = typeof content === "string" ? content : content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
|
|
@@ -4783,7 +5116,7 @@ function isPersistedQueueItem(v) {
|
|
|
4783
5116
|
// src/storage/attachment-store.ts
|
|
4784
5117
|
init_atomic_write();
|
|
4785
5118
|
var DEFAULT_SPOOL_THRESHOLD = 256 * 1024;
|
|
4786
|
-
var PLACEHOLDER_RE = /\[(pasted|image|file) #(\d+)\]/g;
|
|
5119
|
+
var PLACEHOLDER_RE = /\[(pasted|image|file) #(\d+)[^\]]*\]|\[file:([^\]]+)\]/g;
|
|
4787
5120
|
var DefaultAttachmentStore = class {
|
|
4788
5121
|
items = /* @__PURE__ */ new Map();
|
|
4789
5122
|
refs = [];
|
|
@@ -4837,9 +5170,15 @@ var DefaultAttachmentStore = class {
|
|
|
4837
5170
|
const idx = m.index ?? 0;
|
|
4838
5171
|
const before = text.slice(lastIndex, idx);
|
|
4839
5172
|
if (before) blocks.push({ type: "text", text: before });
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
5173
|
+
let ref;
|
|
5174
|
+
if (m[3] !== void 0) {
|
|
5175
|
+
const wantPath = m[3];
|
|
5176
|
+
ref = findLast(this.refs, (r) => r.kind === "file" && refPath(r) === wantPath);
|
|
5177
|
+
} else {
|
|
5178
|
+
const kind = prefixToKind(m[1]);
|
|
5179
|
+
const seq = Number(m[2]);
|
|
5180
|
+
ref = this.refs.find((r) => r.kind === kind && r.seq === seq);
|
|
5181
|
+
}
|
|
4843
5182
|
const att = ref ? this.items.get(ref.id) : void 0;
|
|
4844
5183
|
if (!att) {
|
|
4845
5184
|
blocks.push({ type: "text", text: m[0] });
|
|
@@ -4892,6 +5231,15 @@ function prefixToKind(prefix) {
|
|
|
4892
5231
|
if (prefix === "image") return "image";
|
|
4893
5232
|
return "file";
|
|
4894
5233
|
}
|
|
5234
|
+
function refPath(ref) {
|
|
5235
|
+
return ref.meta.filename ?? ref.meta.label;
|
|
5236
|
+
}
|
|
5237
|
+
function findLast(arr, pred) {
|
|
5238
|
+
for (let i = arr.length - 1; i >= 0; i--) {
|
|
5239
|
+
if (pred(arr[i])) return arr[i];
|
|
5240
|
+
}
|
|
5241
|
+
return void 0;
|
|
5242
|
+
}
|
|
4895
5243
|
function mergeAdjacentText(blocks) {
|
|
4896
5244
|
const out = [];
|
|
4897
5245
|
for (const b of blocks) {
|
|
@@ -5219,7 +5567,8 @@ var BEHAVIOR_DEFAULTS = {
|
|
|
5219
5567
|
memory: true,
|
|
5220
5568
|
modelsRegistry: true,
|
|
5221
5569
|
skills: true
|
|
5222
|
-
}
|
|
5570
|
+
},
|
|
5571
|
+
session: { ...DEFAULT_SESSION_LOGGING_CONFIG }
|
|
5223
5572
|
};
|
|
5224
5573
|
var ENV_MAP = {
|
|
5225
5574
|
WRONGSTACK_PROVIDER: (c, v) => {
|
|
@@ -5727,6 +6076,101 @@ var SessionAnalyzer = class {
|
|
|
5727
6076
|
}
|
|
5728
6077
|
};
|
|
5729
6078
|
|
|
6079
|
+
// src/storage/session-event-bridge.ts
|
|
6080
|
+
var CORE_RECONSTRUCT_EVENTS = /* @__PURE__ */ new Set([
|
|
6081
|
+
"session_start",
|
|
6082
|
+
"session_resumed",
|
|
6083
|
+
"user_input",
|
|
6084
|
+
"llm_response",
|
|
6085
|
+
"tool_result",
|
|
6086
|
+
"checkpoint",
|
|
6087
|
+
"file_snapshot",
|
|
6088
|
+
"rewound",
|
|
6089
|
+
"in_flight_start",
|
|
6090
|
+
"in_flight_end",
|
|
6091
|
+
"session_end"
|
|
6092
|
+
]);
|
|
6093
|
+
var STANDARD_AUDIT_EVENTS = /* @__PURE__ */ new Set([
|
|
6094
|
+
"llm_request",
|
|
6095
|
+
"tool_use",
|
|
6096
|
+
"tool_call_start",
|
|
6097
|
+
"tool_call_end",
|
|
6098
|
+
"compaction",
|
|
6099
|
+
"error",
|
|
6100
|
+
"message_truncated",
|
|
6101
|
+
"provider_retry",
|
|
6102
|
+
"provider_error"
|
|
6103
|
+
]);
|
|
6104
|
+
var FULL_ONLY_EVENTS = /* @__PURE__ */ new Set([
|
|
6105
|
+
"tool_progress"
|
|
6106
|
+
]);
|
|
6107
|
+
function isAllowed(type, level) {
|
|
6108
|
+
if (CORE_RECONSTRUCT_EVENTS.has(type)) return true;
|
|
6109
|
+
if (level === "minimal") return false;
|
|
6110
|
+
if (STANDARD_AUDIT_EVENTS.has(type)) return true;
|
|
6111
|
+
if (level === "standard") return false;
|
|
6112
|
+
if (FULL_ONLY_EVENTS.has(type)) {
|
|
6113
|
+
return level === "full";
|
|
6114
|
+
}
|
|
6115
|
+
return true;
|
|
6116
|
+
}
|
|
6117
|
+
function createSessionEventBridge(writer, level = "standard", options = {}) {
|
|
6118
|
+
const normalizedLevel = level ?? "standard";
|
|
6119
|
+
const progressCounters = /* @__PURE__ */ new Map();
|
|
6120
|
+
const toolProgressConfig = options.sampling?.toolProgress ?? {};
|
|
6121
|
+
const TOOL_PROGRESS_SAMPLE_RATE = toolProgressConfig.sampleRate ?? 8;
|
|
6122
|
+
function shouldSample(event) {
|
|
6123
|
+
if (event.type !== "tool_progress") return true;
|
|
6124
|
+
const progEvent = event;
|
|
6125
|
+
const innerType = progEvent.event?.type;
|
|
6126
|
+
if (innerType === "warning" || innerType === "metric" || innerType === "file_changed") {
|
|
6127
|
+
return true;
|
|
6128
|
+
}
|
|
6129
|
+
if (innerType === "log" || innerType === "partial_output") {
|
|
6130
|
+
const key = progEvent.id || progEvent.name;
|
|
6131
|
+
const count = (progressCounters.get(key) || 0) + 1;
|
|
6132
|
+
progressCounters.set(key, count);
|
|
6133
|
+
return count === 1 || count % TOOL_PROGRESS_SAMPLE_RATE === 0;
|
|
6134
|
+
}
|
|
6135
|
+
return true;
|
|
6136
|
+
}
|
|
6137
|
+
return {
|
|
6138
|
+
level: normalizedLevel,
|
|
6139
|
+
allows(type) {
|
|
6140
|
+
return isAllowed(type, normalizedLevel);
|
|
6141
|
+
},
|
|
6142
|
+
async append(event) {
|
|
6143
|
+
if (!writer) return;
|
|
6144
|
+
if (!isAllowed(event.type, normalizedLevel)) return;
|
|
6145
|
+
if (!shouldSample(event)) return;
|
|
6146
|
+
try {
|
|
6147
|
+
await writer.append(event);
|
|
6148
|
+
} catch (err) {
|
|
6149
|
+
}
|
|
6150
|
+
}
|
|
6151
|
+
};
|
|
6152
|
+
}
|
|
6153
|
+
function resolveAuditLevel(cfg) {
|
|
6154
|
+
const raw = cfg?.session?.auditLevel;
|
|
6155
|
+
if (raw === "minimal" || raw === "standard" || raw === "full") {
|
|
6156
|
+
return raw;
|
|
6157
|
+
}
|
|
6158
|
+
return "standard";
|
|
6159
|
+
}
|
|
6160
|
+
function resolveSessionLoggingConfig(cfg) {
|
|
6161
|
+
const session = cfg?.session ?? {};
|
|
6162
|
+
const auditLevel = resolveAuditLevel(cfg);
|
|
6163
|
+
const toolProgressSampleRate = session.sampling?.toolProgress?.sampleRate ?? 8;
|
|
6164
|
+
return {
|
|
6165
|
+
auditLevel,
|
|
6166
|
+
sampling: {
|
|
6167
|
+
toolProgress: {
|
|
6168
|
+
sampleRate: Math.max(1, Math.floor(toolProgressSampleRate))
|
|
6169
|
+
}
|
|
6170
|
+
}
|
|
6171
|
+
};
|
|
6172
|
+
}
|
|
6173
|
+
|
|
5730
6174
|
// src/storage/todos-checkpoint.ts
|
|
5731
6175
|
init_atomic_write();
|
|
5732
6176
|
async function loadTodosCheckpoint(filePath) {
|
|
@@ -6215,8 +6659,6 @@ var DirectorStateCheckpoint = class {
|
|
|
6215
6659
|
}
|
|
6216
6660
|
}
|
|
6217
6661
|
};
|
|
6218
|
-
|
|
6219
|
-
// src/security/permission-policy.ts
|
|
6220
6662
|
init_atomic_write();
|
|
6221
6663
|
var DefaultPermissionPolicy = class {
|
|
6222
6664
|
policy = {};
|
|
@@ -6457,31 +6899,21 @@ var DefaultPermissionPolicy = class {
|
|
|
6457
6899
|
};
|
|
6458
6900
|
var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
|
|
6459
6901
|
/**
|
|
6460
|
-
*
|
|
6461
|
-
*
|
|
6462
|
-
* and cannot answer prompts, but inherited authorization does not
|
|
6463
|
-
* imply blanket permission for destructive or privilege-escalating
|
|
6464
|
-
* operations. These tools remain at their declared `permission`
|
|
6465
|
-
* level so the leader must explicitly allow them per-spawn.
|
|
6902
|
+
* Legacy name-based denylist.
|
|
6903
|
+
* @deprecated Prefer declaring `capabilities` on the Tool and using capability-based checks.
|
|
6466
6904
|
*/
|
|
6467
|
-
static
|
|
6905
|
+
static LEGACY_NAME_DENY = /* @__PURE__ */ new Set([
|
|
6468
6906
|
"bash",
|
|
6469
|
-
// arbitrary shell — use exec for constrained shell
|
|
6470
6907
|
"write",
|
|
6471
|
-
// arbitrary file write
|
|
6472
6908
|
"edit",
|
|
6473
|
-
// arbitrary in-project file modification (equivalent to write)
|
|
6474
6909
|
"replace",
|
|
6475
|
-
// arbitrary multi-file find/replace (equivalent to write)
|
|
6476
6910
|
"scaffold",
|
|
6477
|
-
// arbitrary file generation outside project root
|
|
6478
6911
|
"patch",
|
|
6479
|
-
// arbitrary diff application
|
|
6480
6912
|
"install",
|
|
6481
|
-
// installs from arbitrary package sources
|
|
6482
6913
|
"exec"
|
|
6483
|
-
// restricted shell but with arbitrary command args
|
|
6484
6914
|
]);
|
|
6915
|
+
// Note: hasDangerousCapabilityForSubagents is now the shared helper from capabilities.ts
|
|
6916
|
+
// The old private method was removed in favor of the centralized utility.
|
|
6485
6917
|
/**
|
|
6486
6918
|
* Tools from MCP servers (`mcp__<server>__<tool>`) are external code of
|
|
6487
6919
|
* unknown capability — they may wrap a shell or filesystem. They are
|
|
@@ -6492,12 +6924,16 @@ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
|
|
|
6492
6924
|
return name.startsWith("mcp__");
|
|
6493
6925
|
}
|
|
6494
6926
|
async evaluate(tool) {
|
|
6495
|
-
const
|
|
6496
|
-
|
|
6927
|
+
const hasDangerousCap = hasDangerousCapabilityForSubagents(tool);
|
|
6928
|
+
const legacyNameBlock = _AutoApprovePermissionPolicy.LEGACY_NAME_DENY.has(tool.name);
|
|
6929
|
+
const isMcp = _AutoApprovePermissionPolicy.isMcpTool(tool.name);
|
|
6930
|
+
const blocked = tool.permission === "deny" || hasDangerousCap || legacyNameBlock || isMcp;
|
|
6931
|
+
if (blocked) {
|
|
6932
|
+
const reason = hasDangerousCap ? `tool declares dangerous capability (${tool.capabilities?.join(", ")}) \u2014 not auto-approved for subagents` : legacyNameBlock || isMcp ? `tool ${tool.name} is not auto-approved for subagents \u2014 ask the leader to allow it explicitly` : "tool default deny";
|
|
6497
6933
|
return {
|
|
6498
6934
|
permission: "deny",
|
|
6499
6935
|
source: "subagent_guard",
|
|
6500
|
-
reason
|
|
6936
|
+
reason
|
|
6501
6937
|
};
|
|
6502
6938
|
}
|
|
6503
6939
|
return { permission: "auto", source: "yolo" };
|
|
@@ -6976,8 +7412,9 @@ var IntelligentCompactor = class {
|
|
|
6976
7412
|
}
|
|
6977
7413
|
async compact(ctx, opts = {}) {
|
|
6978
7414
|
const beforeTokens = this.estimateTokens(ctx.messages);
|
|
7415
|
+
const beforeFull = this.estimateFullRequest(ctx);
|
|
6979
7416
|
const reductions = [];
|
|
6980
|
-
const load =
|
|
7417
|
+
const load = beforeFull / this.maxContext;
|
|
6981
7418
|
const aggressive = load >= this.hardThreshold ? true : opts.aggressive ?? load >= this.softThreshold;
|
|
6982
7419
|
const saved1 = this.eliseOldToolResults(ctx);
|
|
6983
7420
|
if (saved1 > 0) reductions.push({ phase: "elision", saved: saved1 });
|
|
@@ -6991,9 +7428,12 @@ var IntelligentCompactor = class {
|
|
|
6991
7428
|
const repaired = repairToolUseAdjacency(ctx.messages);
|
|
6992
7429
|
if (repaired.report.changed) ctx.state.replaceMessages(repaired.messages);
|
|
6993
7430
|
const afterTokens = this.estimateTokens(ctx.messages);
|
|
7431
|
+
const afterFull = this.estimateFullRequest(ctx);
|
|
6994
7432
|
return {
|
|
6995
7433
|
before: beforeTokens,
|
|
6996
7434
|
after: afterTokens,
|
|
7435
|
+
fullRequestTokensBefore: beforeFull,
|
|
7436
|
+
fullRequestTokensAfter: afterFull,
|
|
6997
7437
|
reductions,
|
|
6998
7438
|
repaired: repaired.report.changed ? {
|
|
6999
7439
|
removedToolUses: repaired.report.removedToolUses,
|
|
@@ -7002,6 +7442,14 @@ var IntelligentCompactor = class {
|
|
|
7002
7442
|
} : void 0
|
|
7003
7443
|
};
|
|
7004
7444
|
}
|
|
7445
|
+
/**
|
|
7446
|
+
* Estimate the full API request token count: messages + systemPrompt + toolDefs.
|
|
7447
|
+
* This is the accurate figure for context-window pressure monitoring.
|
|
7448
|
+
*/
|
|
7449
|
+
estimateFullRequest(ctx) {
|
|
7450
|
+
const breakdown = estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []);
|
|
7451
|
+
return breakdown.total;
|
|
7452
|
+
}
|
|
7005
7453
|
async summarizeAncientTurns(ctx) {
|
|
7006
7454
|
const messages = ctx.messages;
|
|
7007
7455
|
const cutoff = Math.max(0, messages.length - this.preserveK * 2);
|
|
@@ -7014,29 +7462,26 @@ var IntelligentCompactor = class {
|
|
|
7014
7462
|
try {
|
|
7015
7463
|
summaryText = await this.callSummarizer(toSummarize, ctx);
|
|
7016
7464
|
} catch {
|
|
7017
|
-
const
|
|
7018
|
-
const filePaths = /* @__PURE__ */ new Set();
|
|
7019
|
-
let userTurns = 0;
|
|
7020
|
-
let assistantTurns = 0;
|
|
7465
|
+
const preservedMessages = [];
|
|
7021
7466
|
for (const m of toSummarize) {
|
|
7022
|
-
if (m.role === "
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7467
|
+
if (m.role === "system") continue;
|
|
7468
|
+
if (typeof m.content === "string") {
|
|
7469
|
+
preservedMessages.push(m);
|
|
7470
|
+
} else if (Array.isArray(m.content)) {
|
|
7471
|
+
const textParts = m.content.filter(isTextBlock);
|
|
7472
|
+
const toolParts = m.content.filter((b) => b.type === "tool_use" || b.type === "tool_result");
|
|
7473
|
+
const next = {
|
|
7474
|
+
role: m.role,
|
|
7475
|
+
content: [
|
|
7476
|
+
...textParts,
|
|
7477
|
+
...toolParts.length > 0 ? [{ type: "text", text: `[${toolParts.length} tool call(s) omitted]` }] : []
|
|
7478
|
+
]
|
|
7479
|
+
};
|
|
7480
|
+
preservedMessages.push(next);
|
|
7030
7481
|
}
|
|
7031
|
-
const text = typeof m.content === "string" ? m.content : "";
|
|
7032
|
-
const matches = text.matchAll(/(?:[\w.,\-/@]+\/)*[\w.,\-/@]+\.\w+/g);
|
|
7033
|
-
for (const m_ of matches) filePaths.add(m_[0]);
|
|
7034
7482
|
}
|
|
7035
|
-
|
|
7036
|
-
if (
|
|
7037
|
-
if (filePaths.size > 0) parts.push(`files: ${[...filePaths].slice(0, 10).join(", ")}`);
|
|
7038
|
-
summaryText = parts.join(" | ");
|
|
7039
|
-
if (!summaryText) summaryText = `${toSummarize.length} earlier turns omitted`;
|
|
7483
|
+
summaryText = preservedMessages.map((m) => `[${m.role}]: ${typeof m.content === "string" ? m.content : m.content.filter(isTextBlock).map((b) => b.text).join(" ")}`).join("\n");
|
|
7484
|
+
if (!summaryText) summaryText = `${toSummarize.length} earlier turns (semantic content preserved)`;
|
|
7040
7485
|
}
|
|
7041
7486
|
const summaryMsg = {
|
|
7042
7487
|
role: "system",
|
|
@@ -7282,7 +7727,7 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
|
|
|
7282
7727
|
const res = await this.provider.complete(req, { signal: ac.signal });
|
|
7283
7728
|
const textBlocks = res.content.filter(isTextBlock);
|
|
7284
7729
|
raw = textBlocks.map((b) => b.text).join("\n").trim();
|
|
7285
|
-
} catch (
|
|
7730
|
+
} catch (_err) {
|
|
7286
7731
|
return this.fallbackSelect(messages, effectiveBudget);
|
|
7287
7732
|
}
|
|
7288
7733
|
return this.parseSelectorOutput(raw, messages.length);
|
|
@@ -7374,15 +7819,24 @@ var SelectiveCompactor = class {
|
|
|
7374
7819
|
}
|
|
7375
7820
|
async compact(ctx, opts = {}) {
|
|
7376
7821
|
const beforeTokens = this.estimateTokens(ctx.messages);
|
|
7822
|
+
const beforeFull = this.estimateFullRequest(ctx);
|
|
7377
7823
|
const reductions = [];
|
|
7378
|
-
const load =
|
|
7824
|
+
const load = beforeFull / this.maxContext;
|
|
7379
7825
|
const shouldCompact = load >= this.warnThreshold || opts.aggressive;
|
|
7380
7826
|
if (!shouldCompact) {
|
|
7381
7827
|
const saved = this.eliseOldToolResults(ctx);
|
|
7382
7828
|
if (saved > 0) reductions.push({ phase: "elision", saved });
|
|
7383
7829
|
const repair2 = this.repairProtocolAdjacency(ctx);
|
|
7384
7830
|
const afterTokens2 = this.estimateTokens(ctx.messages);
|
|
7385
|
-
|
|
7831
|
+
const afterFull2 = this.estimateFullRequest(ctx);
|
|
7832
|
+
return {
|
|
7833
|
+
before: beforeTokens,
|
|
7834
|
+
after: afterTokens2,
|
|
7835
|
+
fullRequestTokensBefore: beforeFull,
|
|
7836
|
+
fullRequestTokensAfter: afterFull2,
|
|
7837
|
+
reductions,
|
|
7838
|
+
repaired: repair2
|
|
7839
|
+
};
|
|
7386
7840
|
}
|
|
7387
7841
|
const savedElision = this.eliseOldToolResults(ctx);
|
|
7388
7842
|
if (savedElision > 0) reductions.push({ phase: "elision", saved: savedElision });
|
|
@@ -7394,7 +7848,23 @@ var SelectiveCompactor = class {
|
|
|
7394
7848
|
}
|
|
7395
7849
|
const repair = this.repairProtocolAdjacency(ctx);
|
|
7396
7850
|
const afterTokens = this.estimateTokens(ctx.messages);
|
|
7397
|
-
|
|
7851
|
+
const afterFull = this.estimateFullRequest(ctx);
|
|
7852
|
+
return {
|
|
7853
|
+
before: beforeTokens,
|
|
7854
|
+
after: afterTokens,
|
|
7855
|
+
fullRequestTokensBefore: beforeFull,
|
|
7856
|
+
fullRequestTokensAfter: afterFull,
|
|
7857
|
+
reductions,
|
|
7858
|
+
repaired: repair
|
|
7859
|
+
};
|
|
7860
|
+
}
|
|
7861
|
+
/**
|
|
7862
|
+
* Estimate the full API request token count: messages + systemPrompt + toolDefs.
|
|
7863
|
+
* This is the accurate figure for context-window pressure monitoring.
|
|
7864
|
+
*/
|
|
7865
|
+
estimateFullRequest(ctx) {
|
|
7866
|
+
const breakdown = estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []);
|
|
7867
|
+
return breakdown.total;
|
|
7398
7868
|
}
|
|
7399
7869
|
repairProtocolAdjacency(ctx) {
|
|
7400
7870
|
const repaired = repairToolUseAdjacency(ctx.messages);
|
|
@@ -7584,7 +8054,8 @@ var LEVEL_RANK2 = { warn: 0, soft: 1, hard: 2 };
|
|
|
7584
8054
|
var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
|
|
7585
8055
|
name = "AutoCompaction";
|
|
7586
8056
|
compactor;
|
|
7587
|
-
|
|
8057
|
+
/** Deprecated. Kept for backward compat with tests that pass simpleEstimator. */
|
|
8058
|
+
_estimator;
|
|
7588
8059
|
warnThreshold;
|
|
7589
8060
|
softThreshold;
|
|
7590
8061
|
hardThreshold;
|
|
@@ -7594,19 +8065,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
|
|
|
7594
8065
|
events;
|
|
7595
8066
|
failureMode;
|
|
7596
8067
|
policyProvider;
|
|
7597
|
-
|
|
7598
|
-
* Calibration factor applied to the estimator output. The estimator is now
|
|
7599
|
-
* expected to already include messages + system prompt + tool definitions
|
|
7600
|
-
* (see `estimateRequestTokens.total`), so no overhead boost is needed. Kept
|
|
7601
|
-
* as a constant so a future calibration against real provider tokenization
|
|
7602
|
-
* (BPE vs the chars/3.5 rough estimator) can dial this without touching the
|
|
7603
|
-
* threshold-check math.
|
|
7604
|
-
*
|
|
7605
|
-
* Historical note: was 1.3 when the estimator only counted messages — that
|
|
7606
|
-
* double-counted system+tools once the estimator was upgraded, firing
|
|
7607
|
-
* compaction ~30% earlier than intended (e.g. real 56% load showed as 73%).
|
|
7608
|
-
*/
|
|
7609
|
-
static OVERHEAD_FACTOR = 1;
|
|
8068
|
+
sessionBridge;
|
|
7610
8069
|
/**
|
|
7611
8070
|
* Once a compaction attempt reduces nothing (preserveK protects everything,
|
|
7612
8071
|
* no oversized tool_results remain to elide), retrying on every iteration
|
|
@@ -7619,8 +8078,11 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
|
|
|
7619
8078
|
lastNoopAttempt = null;
|
|
7620
8079
|
/**
|
|
7621
8080
|
* @param compactor Compactor to use for compaction.
|
|
7622
|
-
* @param maxContext
|
|
7623
|
-
* @param
|
|
8081
|
+
* @param maxContext Provider's max context window in tokens.
|
|
8082
|
+
* @param _estimator Deprecated parameter kept for backward compatibility.
|
|
8083
|
+
* The middleware now uses `estimateRequestTokens` internally
|
|
8084
|
+
* for accurate full-request token counting (messages +
|
|
8085
|
+
* systemPrompt + toolDefs).
|
|
7624
8086
|
* @param thresholds Threshold fractions (0-1) of maxContext.
|
|
7625
8087
|
* @param opts Optional behavior. By default, failures at the
|
|
7626
8088
|
* hard threshold throw AGENT_CONTEXT_OVERFLOW so
|
|
@@ -7628,11 +8090,11 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
|
|
|
7628
8090
|
* provider context overflow. Warn/soft failures
|
|
7629
8091
|
* still emit compaction.failed and continue.
|
|
7630
8092
|
*/
|
|
7631
|
-
constructor(compactor, maxContext,
|
|
8093
|
+
constructor(compactor, maxContext, _estimator, thresholds, optsOrAggressiveOn = {}, events) {
|
|
7632
8094
|
const opts = typeof optsOrAggressiveOn === "string" ? { aggressiveOn: optsOrAggressiveOn, events } : optsOrAggressiveOn;
|
|
7633
8095
|
this.compactor = compactor;
|
|
7634
8096
|
this._maxContext = maxContext;
|
|
7635
|
-
this.
|
|
8097
|
+
this._estimator = _estimator;
|
|
7636
8098
|
this.warnThreshold = thresholds.warn;
|
|
7637
8099
|
this.softThreshold = thresholds.soft;
|
|
7638
8100
|
this.hardThreshold = thresholds.hard;
|
|
@@ -7640,6 +8102,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
|
|
|
7640
8102
|
this.events = opts.events;
|
|
7641
8103
|
this.failureMode = opts.failureMode ?? "throw_on_hard";
|
|
7642
8104
|
this.policyProvider = opts.policyProvider;
|
|
8105
|
+
this.sessionBridge = opts.sessionBridge;
|
|
7643
8106
|
}
|
|
7644
8107
|
/** Allow callers (e.g. model-switch in WebUI) to update the context window
|
|
7645
8108
|
* denominator when the active model changes. */
|
|
@@ -7648,8 +8111,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
|
|
|
7648
8111
|
}
|
|
7649
8112
|
handler() {
|
|
7650
8113
|
return async (ctx, next) => {
|
|
7651
|
-
const
|
|
7652
|
-
const tokens = Math.ceil(rawTokens * _AutoCompactionMiddleware.OVERHEAD_FACTOR);
|
|
8114
|
+
const tokens = this._estimator ? this._estimator(ctx) : estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []).total;
|
|
7653
8115
|
const load = tokens / this._maxContext;
|
|
7654
8116
|
const policy = this.policyProvider?.(ctx);
|
|
7655
8117
|
const thresholds = policy?.thresholds ?? {
|
|
@@ -7684,7 +8146,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
|
|
|
7684
8146
|
return delta < _AutoCompactionMiddleware.NOOP_RETRY_DELTA_TOKENS;
|
|
7685
8147
|
}
|
|
7686
8148
|
recordAttempt(level, tokens, report) {
|
|
7687
|
-
const reduced = report.before > report.after;
|
|
8149
|
+
const reduced = (report.fullRequestTokensBefore ?? report.before) > (report.fullRequestTokensAfter ?? report.after);
|
|
7688
8150
|
const repaired = !!report.repaired;
|
|
7689
8151
|
if (reduced || repaired) {
|
|
7690
8152
|
this.lastNoopAttempt = null;
|
|
@@ -7704,6 +8166,15 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
|
|
|
7704
8166
|
report,
|
|
7705
8167
|
aggressive
|
|
7706
8168
|
});
|
|
8169
|
+
await this.sessionBridge?.append({
|
|
8170
|
+
type: "compaction",
|
|
8171
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8172
|
+
before: report.before,
|
|
8173
|
+
after: report.after,
|
|
8174
|
+
level: pressure.level,
|
|
8175
|
+
aggressive,
|
|
8176
|
+
reductions: report.reductions?.map((r) => ({ phase: r.phase, saved: r.saved }))
|
|
8177
|
+
});
|
|
7707
8178
|
ctx.clearFileTracking();
|
|
7708
8179
|
} catch (err) {
|
|
7709
8180
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
@@ -8615,19 +9086,16 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
8615
9086
|
tokenOutput = 0;
|
|
8616
9087
|
costUsd = 0;
|
|
8617
9088
|
startTime = null;
|
|
8618
|
-
_onThreshold;
|
|
8619
9089
|
/**
|
|
8620
|
-
*
|
|
8621
|
-
*
|
|
8622
|
-
*
|
|
8623
|
-
*
|
|
8624
|
-
* the first one lands and patches limits), flooding the FleetBus
|
|
8625
|
-
* with redundant threshold events. Cleared in `negotiateExtension`'s
|
|
8626
|
-
* `finally`.
|
|
9090
|
+
* Timestamp of the most recent activity (iteration / tool call / token
|
|
9091
|
+
* usage / streamed progress). Drives the idle timeout — reset by
|
|
9092
|
+
* `markActivity()`. Initialised to `start()` time so a never-active agent
|
|
9093
|
+
* still eventually trips its idle window.
|
|
8627
9094
|
*/
|
|
8628
|
-
|
|
9095
|
+
lastActivityTime = null;
|
|
9096
|
+
_onThreshold;
|
|
8629
9097
|
/**
|
|
8630
|
-
* Hard cap on how long `
|
|
9098
|
+
* Hard cap on how long `_negotiateExtension` waits for the coordinator to
|
|
8631
9099
|
* respond before defaulting to 'stop'. Without this fallback an absent
|
|
8632
9100
|
* or hung listener (Director not built / event filter detached mid-run)
|
|
8633
9101
|
* leaves the budget over-limit and never enforces anything.
|
|
@@ -8668,6 +9136,24 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
8668
9136
|
}
|
|
8669
9137
|
start() {
|
|
8670
9138
|
this.startTime = Date.now();
|
|
9139
|
+
this.lastActivityTime = this.startTime;
|
|
9140
|
+
}
|
|
9141
|
+
/**
|
|
9142
|
+
* Reset the idle clock. Called on any sign of forward progress —
|
|
9143
|
+
* iterations, tool calls, token usage, and streamed tool/text progress —
|
|
9144
|
+
* so a long-but-productive subagent never trips its `idleTimeoutMs`.
|
|
9145
|
+
*/
|
|
9146
|
+
markActivity() {
|
|
9147
|
+
this.lastActivityTime = Date.now();
|
|
9148
|
+
}
|
|
9149
|
+
/**
|
|
9150
|
+
* Milliseconds since the last activity signal. Returns 0 before `start()`
|
|
9151
|
+
* (nothing to measure yet). Used by the coordinator watchdog to decide
|
|
9152
|
+
* whether to re-arm (still active) or reap (genuinely idle).
|
|
9153
|
+
*/
|
|
9154
|
+
idleMs() {
|
|
9155
|
+
const since = this.lastActivityTime ?? this.startTime;
|
|
9156
|
+
return since === null ? 0 : Date.now() - since;
|
|
8671
9157
|
}
|
|
8672
9158
|
/** Returns true if we're within 10% of any limit — useful for pre-flight checks. */
|
|
8673
9159
|
isNearLimit() {
|
|
@@ -8689,38 +9175,88 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
8689
9175
|
* - `mode === 'auto'` + no listener → throw `BudgetExceededError` (hard stop; no one to ask)
|
|
8690
9176
|
* - `mode === 'auto'` + listener → throw `BudgetThresholdSignal` with async decision promise
|
|
8691
9177
|
*/
|
|
8692
|
-
|
|
9178
|
+
/**
|
|
9179
|
+
* Collects all exceeded budget kinds into a single NOOP-free negotiation.
|
|
9180
|
+
* Called by recordIteration / recordToolCall / recordUsage — each may call
|
|
9181
|
+
* this for its own kind. The first call starts the negotiation and stores
|
|
9182
|
+
* the Promise in _pendingNegotiation. Subsequent calls for DIFFERENT
|
|
9183
|
+
* kinds (while a negotiation is in flight) are NOOPs — they don't start
|
|
9184
|
+
* new conversations with the coordinator. This prevents an EventBus flood
|
|
9185
|
+
* when multiple budget kinds are exceeded simultaneously in one iteration.
|
|
9186
|
+
*
|
|
9187
|
+
* Returns the kinds that were found to be exceeded (for logging/debugging).
|
|
9188
|
+
*/
|
|
9189
|
+
checkLimits(elapsedMs) {
|
|
9190
|
+
const exceeded = [];
|
|
9191
|
+
if (this.limits.maxIterations !== void 0 && this.iterations > this.limits.maxIterations) {
|
|
9192
|
+
exceeded.push({ kind: "iterations", used: this.iterations, limit: this.limits.maxIterations });
|
|
9193
|
+
}
|
|
9194
|
+
if (this.limits.maxToolCalls !== void 0 && this.toolCalls > this.limits.maxToolCalls) {
|
|
9195
|
+
exceeded.push({ kind: "tool_calls", used: this.toolCalls, limit: this.limits.maxToolCalls });
|
|
9196
|
+
}
|
|
9197
|
+
const totalTokens = this.tokenInput + this.tokenOutput;
|
|
9198
|
+
if (this.limits.maxTokens !== void 0 && totalTokens > this.limits.maxTokens) {
|
|
9199
|
+
exceeded.push({ kind: "tokens", used: totalTokens, limit: this.limits.maxTokens });
|
|
9200
|
+
}
|
|
9201
|
+
if (this.limits.maxCostUsd !== void 0 && this.costUsd > this.limits.maxCostUsd) {
|
|
9202
|
+
exceeded.push({ kind: "cost", used: this.costUsd, limit: this.limits.maxCostUsd });
|
|
9203
|
+
}
|
|
9204
|
+
if (elapsedMs !== void 0) {
|
|
9205
|
+
const idle = this.idleMs();
|
|
9206
|
+
if (this.limits.idleTimeoutMs !== void 0 && idle > this.limits.idleTimeoutMs) {
|
|
9207
|
+
exceeded.push({ kind: "timeout", used: idle, limit: this.limits.idleTimeoutMs });
|
|
9208
|
+
} else if (this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs) {
|
|
9209
|
+
exceeded.push({ kind: "timeout", used: elapsedMs, limit: this.limits.timeoutMs });
|
|
9210
|
+
}
|
|
9211
|
+
}
|
|
9212
|
+
if (exceeded.length === 0) return [];
|
|
8693
9213
|
if (!this._onThreshold) {
|
|
8694
|
-
|
|
9214
|
+
const first2 = exceeded[0];
|
|
9215
|
+
throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
|
|
8695
9216
|
}
|
|
8696
9217
|
if (this._mode === "sync") {
|
|
8697
|
-
|
|
9218
|
+
const first2 = exceeded[0];
|
|
9219
|
+
throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
|
|
8698
9220
|
}
|
|
8699
9221
|
const bus = this._events;
|
|
8700
9222
|
if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
|
|
8701
|
-
|
|
9223
|
+
const first2 = exceeded[0];
|
|
9224
|
+
throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
|
|
8702
9225
|
}
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
9226
|
+
for (const entry of exceeded) {
|
|
9227
|
+
if (this._pendingNegotiations.has(entry.kind)) continue;
|
|
9228
|
+
const decision2 = this._negotiateExtension(entry.kind, exceeded);
|
|
9229
|
+
this._pendingNegotiations.set(entry.kind, decision2);
|
|
9230
|
+
}
|
|
9231
|
+
const first = exceeded[0];
|
|
9232
|
+
const decision = this._pendingNegotiations.get(first.kind);
|
|
9233
|
+
throw new BudgetThresholdSignal(first.kind, first.limit, first.used, decision);
|
|
8707
9234
|
}
|
|
9235
|
+
/**
|
|
9236
|
+
* Per-kind in-flight negotiation Promises. Each budget kind can have its
|
|
9237
|
+
* own concurrent negotiation — e.g. iterations and timeout can both
|
|
9238
|
+
* be exceeded simultaneously without blocking each other. The same kind
|
|
9239
|
+
* cannot start two concurrent negotiations (dedup guard).
|
|
9240
|
+
* Cleared in `_negotiateExtension`'s `finally` block.
|
|
9241
|
+
*/
|
|
9242
|
+
_pendingNegotiations = /* @__PURE__ */ new Map();
|
|
8708
9243
|
/**
|
|
8709
9244
|
* Drive the threshold handler to a decision. Resolves with `'stop'`
|
|
8710
9245
|
* (signal the runner to abort) or `{ extend: ... }` (limits already
|
|
8711
|
-
* patched in-place; the runner should not abort).
|
|
8712
|
-
*
|
|
9246
|
+
* patched in-place; the runner should not abort). Clears the
|
|
9247
|
+
* per-kind slot in `_pendingNegotiations` in `finally`.
|
|
8713
9248
|
*
|
|
8714
9249
|
* The 'continue' return from a sync handler is treated as
|
|
8715
9250
|
* `{ extend: {} }` — keep going without patching; next overrun fires
|
|
8716
9251
|
* a fresh signal.
|
|
8717
9252
|
*/
|
|
8718
|
-
async
|
|
9253
|
+
async _negotiateExtension(kind, exceeded) {
|
|
8719
9254
|
try {
|
|
9255
|
+
const first = exceeded[0];
|
|
8720
9256
|
const result = this._onThreshold({
|
|
8721
|
-
kind,
|
|
8722
|
-
used,
|
|
8723
|
-
limit,
|
|
9257
|
+
kind: first.kind,
|
|
9258
|
+
used: first.used,
|
|
9259
|
+
limit: first.limit,
|
|
8724
9260
|
requestDecision: () => {
|
|
8725
9261
|
const bus = this._events;
|
|
8726
9262
|
if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
|
|
@@ -8737,20 +9273,22 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
8737
9273
|
() => respond("stop"),
|
|
8738
9274
|
_SubagentBudget.DECISION_TIMEOUT_MS
|
|
8739
9275
|
);
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
9276
|
+
for (const { kind: kind2, used, limit } of exceeded) {
|
|
9277
|
+
bus.emit("budget.threshold_reached", {
|
|
9278
|
+
kind: kind2,
|
|
9279
|
+
used,
|
|
9280
|
+
limit,
|
|
9281
|
+
timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
|
|
9282
|
+
extend: (extra) => {
|
|
9283
|
+
clearTimeout(fallback);
|
|
9284
|
+
respond({ extend: extra });
|
|
9285
|
+
},
|
|
9286
|
+
deny: () => {
|
|
9287
|
+
clearTimeout(fallback);
|
|
9288
|
+
respond("stop");
|
|
9289
|
+
}
|
|
9290
|
+
});
|
|
9291
|
+
}
|
|
8754
9292
|
});
|
|
8755
9293
|
}
|
|
8756
9294
|
});
|
|
@@ -8774,54 +9312,59 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
8774
9312
|
if (ext.timeoutMs !== void 0) {
|
|
8775
9313
|
this.limits.timeoutMs = ext.timeoutMs;
|
|
8776
9314
|
}
|
|
9315
|
+
if (ext.idleTimeoutMs !== void 0) {
|
|
9316
|
+
this.limits.idleTimeoutMs = ext.idleTimeoutMs;
|
|
9317
|
+
}
|
|
8777
9318
|
return decision;
|
|
8778
9319
|
} finally {
|
|
8779
|
-
this.
|
|
9320
|
+
this._pendingNegotiations.delete(kind);
|
|
8780
9321
|
}
|
|
8781
9322
|
}
|
|
8782
9323
|
recordIteration() {
|
|
8783
9324
|
this.iterations++;
|
|
8784
|
-
|
|
8785
|
-
|
|
8786
|
-
}
|
|
9325
|
+
this.markActivity();
|
|
9326
|
+
void this.checkLimits();
|
|
8787
9327
|
}
|
|
8788
9328
|
recordToolCall() {
|
|
8789
9329
|
this.toolCalls++;
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
}
|
|
9330
|
+
this.markActivity();
|
|
9331
|
+
void this.checkLimits();
|
|
8793
9332
|
}
|
|
8794
9333
|
recordUsage(usage, costUsd = 0) {
|
|
8795
9334
|
this.tokenInput += usage.input;
|
|
8796
9335
|
this.tokenOutput += usage.output;
|
|
8797
9336
|
this.costUsd += costUsd;
|
|
8798
|
-
|
|
8799
|
-
|
|
8800
|
-
void this.checkLimit("tokens", totalTokens, this.limits.maxTokens);
|
|
8801
|
-
}
|
|
8802
|
-
if (this.limits.maxCostUsd !== void 0 && this.costUsd > this.limits.maxCostUsd) {
|
|
8803
|
-
void this.checkLimit("cost", this.costUsd, this.limits.maxCostUsd);
|
|
8804
|
-
}
|
|
9337
|
+
this.markActivity();
|
|
9338
|
+
void this.checkLimits();
|
|
8805
9339
|
}
|
|
8806
9340
|
/**
|
|
8807
|
-
* Wall-clock budget check. Unlike other limits, timeout
|
|
8808
|
-
*
|
|
8809
|
-
*
|
|
8810
|
-
*
|
|
8811
|
-
*
|
|
8812
|
-
*
|
|
9341
|
+
* Wall-clock budget check. Unlike other limits, timeout is always a hard stop
|
|
9342
|
+
* — wall-clock time cannot be "extended" by the coordinator, so it throws
|
|
9343
|
+
* synchronously rather than entering the negotiation flow.
|
|
9344
|
+
*
|
|
9345
|
+
* Decision table:
|
|
9346
|
+
* - no `onThreshold` handler → throw `BudgetExceededError`
|
|
9347
|
+
* - `mode === 'sync'` → throw `BudgetExceededError`
|
|
9348
|
+
* - `mode === 'auto'` + no listener → throw `BudgetExceededError`
|
|
9349
|
+
* - `mode === 'auto'` + listener → throw `BudgetExceededError` (timeout is not extendable)
|
|
8813
9350
|
*/
|
|
8814
9351
|
checkTimeout() {
|
|
8815
|
-
if (this.startTime === null
|
|
9352
|
+
if (this.startTime === null) return;
|
|
9353
|
+
const { timeoutMs, idleTimeoutMs } = this.limits;
|
|
9354
|
+
if (timeoutMs === void 0 && idleTimeoutMs === void 0) return;
|
|
8816
9355
|
const elapsed = Date.now() - this.startTime;
|
|
8817
|
-
|
|
8818
|
-
|
|
8819
|
-
|
|
9356
|
+
const wallTripped = timeoutMs !== void 0 && elapsed > timeoutMs;
|
|
9357
|
+
const idleTripped = idleTimeoutMs !== void 0 && this.idleMs() > idleTimeoutMs;
|
|
9358
|
+
if (!wallTripped && !idleTripped) return;
|
|
9359
|
+
void this.checkLimits(elapsed);
|
|
8820
9360
|
}
|
|
8821
|
-
/** Returns true if a timeout has occurred without throwing.
|
|
9361
|
+
/** Returns true if a wall-clock or idle timeout has occurred without throwing. */
|
|
8822
9362
|
isTimedOut() {
|
|
8823
|
-
if (this.startTime === null
|
|
8824
|
-
|
|
9363
|
+
if (this.startTime === null) return false;
|
|
9364
|
+
const { timeoutMs, idleTimeoutMs } = this.limits;
|
|
9365
|
+
if (timeoutMs !== void 0 && Date.now() - this.startTime > timeoutMs) return true;
|
|
9366
|
+
if (idleTimeoutMs !== void 0 && this.idleMs() > idleTimeoutMs) return true;
|
|
9367
|
+
return false;
|
|
8825
9368
|
}
|
|
8826
9369
|
usage() {
|
|
8827
9370
|
return {
|
|
@@ -8928,6 +9471,7 @@ function makeAgentSubagentRunner(opts) {
|
|
|
8928
9471
|
// Cheap: O(1) per progress event, and the budget short-circuits
|
|
8929
9472
|
// when timeoutMs is unset (most subagents have one set anyway).
|
|
8930
9473
|
events.on("tool.progress", () => {
|
|
9474
|
+
ctx.budget.markActivity();
|
|
8931
9475
|
try {
|
|
8932
9476
|
ctx.budget.checkTimeout();
|
|
8933
9477
|
} catch (e) {
|
|
@@ -8944,6 +9488,7 @@ function makeAgentSubagentRunner(opts) {
|
|
|
8944
9488
|
currentToolName = e.name;
|
|
8945
9489
|
}),
|
|
8946
9490
|
events.on("provider.text_delta", (e) => {
|
|
9491
|
+
ctx.budget.markActivity();
|
|
8947
9492
|
streamingTextAcc = (streamingTextAcc + e.text).slice(-200);
|
|
8948
9493
|
})
|
|
8949
9494
|
);
|
|
@@ -11513,6 +12058,7 @@ var FLEET_ROSTER = {
|
|
|
11513
12058
|
ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.config])
|
|
11514
12059
|
)
|
|
11515
12060
|
};
|
|
12061
|
+
var DEFAULT_IDLE_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
11516
12062
|
var FLEET_ROSTER_BUDGETS = {
|
|
11517
12063
|
"audit-log": { timeoutMs: 7.5 * 60 * 60 * 1e3, maxIterations: 5e3, maxToolCalls: 15e3 },
|
|
11518
12064
|
"bug-hunter": { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 },
|
|
@@ -11524,7 +12070,7 @@ var FLEET_ROSTER_BUDGETS = {
|
|
|
11524
12070
|
)
|
|
11525
12071
|
};
|
|
11526
12072
|
var GENERIC_SUBAGENT_BUDGET = {
|
|
11527
|
-
|
|
12073
|
+
idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
|
|
11528
12074
|
maxIterations: 5e3,
|
|
11529
12075
|
maxToolCalls: 15e3
|
|
11530
12076
|
};
|
|
@@ -11534,7 +12080,13 @@ function applyRosterBudget(cfg) {
|
|
|
11534
12080
|
if (!defaultBudget) return cfg;
|
|
11535
12081
|
return {
|
|
11536
12082
|
...cfg,
|
|
11537
|
-
|
|
12083
|
+
// Wall-clock cap is opt-in only: forward an explicit `cfg.timeoutMs`, but
|
|
12084
|
+
// do NOT impose the roster's historical multi-hour wall-clock default — it
|
|
12085
|
+
// killed agents that were still actively working. Reaping is idle-based.
|
|
12086
|
+
timeoutMs: cfg.timeoutMs,
|
|
12087
|
+
// Idle window is the default reaper. Resets on activity, so a long-but-
|
|
12088
|
+
// productive run is never killed; only a genuine stall is reaped.
|
|
12089
|
+
idleTimeoutMs: cfg.idleTimeoutMs ?? defaultBudget.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS,
|
|
11538
12090
|
maxIterations: cfg.maxIterations ?? defaultBudget.maxIterations,
|
|
11539
12091
|
maxToolCalls: cfg.maxToolCalls ?? defaultBudget.maxToolCalls,
|
|
11540
12092
|
maxTokens: cfg.maxTokens ?? defaultBudget.maxTokens,
|
|
@@ -11604,84 +12156,213 @@ var FLEET_ROSTER_WITHACP = {
|
|
|
11604
12156
|
...Object.fromEntries(ACP_AGENTS.map((a) => [a.role, a]))
|
|
11605
12157
|
};
|
|
11606
12158
|
|
|
11607
|
-
// src/coordination/
|
|
11608
|
-
var
|
|
11609
|
-
|
|
11610
|
-
|
|
11611
|
-
|
|
11612
|
-
|
|
11613
|
-
|
|
11614
|
-
|
|
11615
|
-
|
|
11616
|
-
|
|
11617
|
-
|
|
11618
|
-
|
|
11619
|
-
|
|
11620
|
-
|
|
11621
|
-
|
|
11622
|
-
|
|
11623
|
-
|
|
11624
|
-
|
|
11625
|
-
|
|
11626
|
-
|
|
11627
|
-
|
|
11628
|
-
|
|
11629
|
-
|
|
11630
|
-
|
|
11631
|
-
|
|
11632
|
-
}
|
|
11633
|
-
|
|
11634
|
-
|
|
11635
|
-
|
|
11636
|
-
|
|
11637
|
-
|
|
11638
|
-
|
|
11639
|
-
|
|
11640
|
-
|
|
11641
|
-
|
|
11642
|
-
|
|
11643
|
-
|
|
11644
|
-
|
|
11645
|
-
|
|
11646
|
-
|
|
11647
|
-
|
|
11648
|
-
}
|
|
11649
|
-
|
|
11650
|
-
|
|
11651
|
-
|
|
11652
|
-
|
|
11653
|
-
|
|
11654
|
-
|
|
11655
|
-
|
|
11656
|
-
|
|
11657
|
-
|
|
11658
|
-
|
|
11659
|
-
|
|
11660
|
-
|
|
11661
|
-
|
|
11662
|
-
|
|
11663
|
-
|
|
11664
|
-
|
|
11665
|
-
|
|
11666
|
-
|
|
11667
|
-
|
|
11668
|
-
|
|
11669
|
-
|
|
11670
|
-
|
|
11671
|
-
|
|
11672
|
-
|
|
11673
|
-
|
|
11674
|
-
|
|
11675
|
-
|
|
11676
|
-
|
|
11677
|
-
|
|
11678
|
-
|
|
11679
|
-
|
|
11680
|
-
|
|
11681
|
-
|
|
11682
|
-
|
|
11683
|
-
|
|
11684
|
-
|
|
12159
|
+
// src/coordination/subagent-nicknames.ts
|
|
12160
|
+
var NICKNAME_POOL = {
|
|
12161
|
+
// Physics & fundamental sciences
|
|
12162
|
+
"einstein": { name: "Einstein", domain: "physics" },
|
|
12163
|
+
"newton": { name: "Newton", domain: "physics" },
|
|
12164
|
+
"feynman": { name: "Feynman", domain: "physics" },
|
|
12165
|
+
"dirac": { name: "Dirac", domain: "physics" },
|
|
12166
|
+
"bohr": { name: "Bohr", domain: "physics" },
|
|
12167
|
+
"planck": { name: "Planck", domain: "physics" },
|
|
12168
|
+
"curie": { name: "Curie", domain: "physics" },
|
|
12169
|
+
"fermi": { name: "Fermi", domain: "physics" },
|
|
12170
|
+
"heisenberg": { name: "Heisenberg", domain: "physics" },
|
|
12171
|
+
"schrodinger": { name: "Schr\xF6dinger", domain: "physics" },
|
|
12172
|
+
// Mathematics
|
|
12173
|
+
"euclid": { name: "Euclid", domain: "math" },
|
|
12174
|
+
"gauss": { name: "Gauss", domain: "math" },
|
|
12175
|
+
"turing": { name: "Turing", domain: "math" },
|
|
12176
|
+
"poincare": { name: "Poincar\xE9", domain: "math" },
|
|
12177
|
+
"riemann": { name: "Riemann", domain: "math" },
|
|
12178
|
+
"hilbert": { name: "Hilbert", domain: "math" },
|
|
12179
|
+
"pythagoras": { name: "Pythagoras", domain: "math" },
|
|
12180
|
+
// Computing & information theory
|
|
12181
|
+
"von-neumann": { name: "Von Neumann", domain: "computing" },
|
|
12182
|
+
"shannon": { name: "Shannon", domain: "computing" },
|
|
12183
|
+
"hopper": { name: "Hopper", domain: "computing" },
|
|
12184
|
+
"backus": { name: "Backus", domain: "computing" },
|
|
12185
|
+
"knuth": { name: "Knuth", domain: "computing" },
|
|
12186
|
+
"torvalds": { name: "Torvalds", domain: "computing" },
|
|
12187
|
+
"stallman": { name: "Stallman", domain: "computing" },
|
|
12188
|
+
"berners-lee": { name: "Berners-Lee", domain: "computing" },
|
|
12189
|
+
"babbage": { name: "Babbage", domain: "computing" },
|
|
12190
|
+
"lovelace": { name: "Lovelace", domain: "computing" },
|
|
12191
|
+
"klein": { name: "Klein", domain: "computing" },
|
|
12192
|
+
// Electronics & electrical engineering
|
|
12193
|
+
"edison": { name: "Edison", domain: "ee" },
|
|
12194
|
+
"tesla": { name: "Tesla", domain: "ee" },
|
|
12195
|
+
"faraday": { name: "Faraday", domain: "ee" },
|
|
12196
|
+
"maxwell": { name: "Maxwell", domain: "ee" },
|
|
12197
|
+
"ohm": { name: "Ohm", domain: "ee" },
|
|
12198
|
+
"bell": { name: "Bell", domain: "ee" },
|
|
12199
|
+
"marconi": { name: "Marconi", domain: "ee" },
|
|
12200
|
+
"lamarr": { name: "Lamarr", domain: "ee" },
|
|
12201
|
+
// General science / multi-disciplinary
|
|
12202
|
+
"darwin": { name: "Darwin", domain: "biology" },
|
|
12203
|
+
"mendel": { name: "Mendel", domain: "biology" },
|
|
12204
|
+
"pasteur": { name: "Pasteur", domain: "biology" },
|
|
12205
|
+
"hawking": { name: "Hawking", domain: "cosmology" },
|
|
12206
|
+
"sagan": { name: "Sagan", domain: "cosmology" },
|
|
12207
|
+
// Chemistry / materials
|
|
12208
|
+
"lavoisier": { name: "Lavoisier", domain: "chemistry" },
|
|
12209
|
+
"mendeleev": { name: "Mendeleev", domain: "chemistry" }
|
|
12210
|
+
};
|
|
12211
|
+
var ALL_NICKNAMES = Object.values(NICKNAME_POOL);
|
|
12212
|
+
var DOMAIN_PREFERENCES = {
|
|
12213
|
+
"security": ["shannon", "turing", "lamarr", "stallman"],
|
|
12214
|
+
"bug-hunter": ["darwin", "curie", "feynman", "fermi"],
|
|
12215
|
+
"refactor": ["gauss", "hilbert", "euclid", "planck"],
|
|
12216
|
+
"audit-log": ["sagan", "hawking", "poincare", "newton"],
|
|
12217
|
+
"planner": ["hilbert", "gauss", "turing", "euclid"],
|
|
12218
|
+
"researcher": ["sagan", "hawking", "darwin", "pasteur"],
|
|
12219
|
+
"explorer": ["marconi", "bell", "columbus", "polo"],
|
|
12220
|
+
"testing": ["pasteur", "curie", "fermi", "bohr"],
|
|
12221
|
+
"frontend": ["lovelace", "hopper", "babbage", "backus"],
|
|
12222
|
+
"backend": ["torvalds", "stallman", "von-neumann", "backus"],
|
|
12223
|
+
"database": ["turing", "shannon", "backus", "knuth"],
|
|
12224
|
+
"devops": ["tesla", "edison", "faraday", "bell"],
|
|
12225
|
+
"security-scanner": ["shannon", "turing", "lamarr", "stallman"],
|
|
12226
|
+
"refactor-planner": ["gauss", "hilbert", "planck", "newton"],
|
|
12227
|
+
"architect": ["von-neumann", "turing", "gauss", "hilbert"],
|
|
12228
|
+
"critic": ["einstein", "feynman", "dirac", "bohr"],
|
|
12229
|
+
"e2e": ["hopper", "bell", "marconi", "tesla"],
|
|
12230
|
+
"performance": ["knuth", "gauss", "planck", "feynman"],
|
|
12231
|
+
"chaos": ["tesla", "edison", "curie", "fermi"],
|
|
12232
|
+
"cost": ["ohm", "bell", "marconi", "tesla"],
|
|
12233
|
+
// default fallback
|
|
12234
|
+
"default": ["einstein", "newton", "curie", "tesla", "edison", "turing", "shannon", "hopper", "knuth", "stallman"]
|
|
12235
|
+
};
|
|
12236
|
+
function assignNickname(role, used) {
|
|
12237
|
+
const preferences = [
|
|
12238
|
+
...DOMAIN_PREFERENCES[role] ?? [],
|
|
12239
|
+
...DOMAIN_PREFERENCES["default"] ?? []
|
|
12240
|
+
];
|
|
12241
|
+
for (const key of preferences) {
|
|
12242
|
+
const entry = NICKNAME_POOL[key];
|
|
12243
|
+
if (entry && !used.has(key)) {
|
|
12244
|
+
return `${entry.name} (${formatRole(role)})`;
|
|
12245
|
+
}
|
|
12246
|
+
}
|
|
12247
|
+
for (const entry of ALL_NICKNAMES) {
|
|
12248
|
+
const key = Object.entries(NICKNAME_POOL).find(([, v]) => v.name === entry.name)?.[0];
|
|
12249
|
+
if (key && !used.has(key)) {
|
|
12250
|
+
return `${entry.name} (${formatRole(role)})`;
|
|
12251
|
+
}
|
|
12252
|
+
}
|
|
12253
|
+
const counter = used.size + 1;
|
|
12254
|
+
return `Scientist #${counter} (${formatRole(role)})`;
|
|
12255
|
+
}
|
|
12256
|
+
function formatRole(role) {
|
|
12257
|
+
return role.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
12258
|
+
}
|
|
12259
|
+
|
|
12260
|
+
// src/coordination/multi-agent-coordinator.ts
|
|
12261
|
+
var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
12262
|
+
coordinatorId;
|
|
12263
|
+
config;
|
|
12264
|
+
runner;
|
|
12265
|
+
fleetBus;
|
|
12266
|
+
subagents = /* @__PURE__ */ new Map();
|
|
12267
|
+
/**
|
|
12268
|
+
* Base nickname keys already handed out this run (e.g. `einstein`, `tesla`).
|
|
12269
|
+
* Prevents two workers sharing a name. Direct `coordinator.spawn()` callers
|
|
12270
|
+
* (parallel/eternal engine, SDD parallel run) don't go through
|
|
12271
|
+
* `Director.spawn()` where nicknames are normally assigned, so the
|
|
12272
|
+
* coordinator upgrades placeholder names ("Executor", "slot-ab12cd", role
|
|
12273
|
+
* names) to memorable ones here — that's what surfaces in the fleet monitor.
|
|
12274
|
+
*/
|
|
12275
|
+
usedNicknames = /* @__PURE__ */ new Set();
|
|
12276
|
+
pendingTasks = [];
|
|
12277
|
+
completedResults = [];
|
|
12278
|
+
totalIterations = 0;
|
|
12279
|
+
inFlight = 0;
|
|
12280
|
+
/**
|
|
12281
|
+
* Subagents currently being stopped. Set on entry to `stop()`, cleared
|
|
12282
|
+
* once `recordCompletion` lands the terminal TaskResult. Used by
|
|
12283
|
+
* `runDispatched` and `findIdleSubagent` to refuse mid-flight dispatch
|
|
12284
|
+
* to a subagent the caller has already asked to terminate — closes the
|
|
12285
|
+
* assign+terminate race where a fresh task could land on a worker that
|
|
12286
|
+
* was about to be killed.
|
|
12287
|
+
*/
|
|
12288
|
+
terminating = /* @__PURE__ */ new Set();
|
|
12289
|
+
constructor(config, options = {}) {
|
|
12290
|
+
super();
|
|
12291
|
+
this.coordinatorId = config.coordinatorId;
|
|
12292
|
+
this.config = config;
|
|
12293
|
+
this.runner = options.runner;
|
|
12294
|
+
}
|
|
12295
|
+
/**
|
|
12296
|
+
* Replace the runner after construction. Used when the runner depends
|
|
12297
|
+
* on infrastructure (e.g. FleetBus) that isn't available until after
|
|
12298
|
+
* the coordinator's owning Director is built.
|
|
12299
|
+
*/
|
|
12300
|
+
setRunner(runner) {
|
|
12301
|
+
this.runner = runner;
|
|
12302
|
+
}
|
|
12303
|
+
/**
|
|
12304
|
+
* Wire a FleetBus for director-mode event emission. Call after the
|
|
12305
|
+
* FleetManager is constructed so the coordinator can emit lifecycle
|
|
12306
|
+
* events the TUI and monitoring tools subscribe to.
|
|
12307
|
+
*/
|
|
12308
|
+
setFleetBus(fleet) {
|
|
12309
|
+
this.fleetBus = fleet;
|
|
12310
|
+
}
|
|
12311
|
+
/**
|
|
12312
|
+
* Change the in-flight dispatch ceiling at runtime. Lowering does NOT
|
|
12313
|
+
* preempt running tasks — already-dispatched subagents finish their
|
|
12314
|
+
* current task; only future dispatches respect the new cap. Raising
|
|
12315
|
+
* immediately tries to fill the freed slots from the pending queue.
|
|
12316
|
+
*/
|
|
12317
|
+
setMaxConcurrent(n) {
|
|
12318
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
12319
|
+
throw new Error(`maxConcurrent must be a finite integer >= 1, got ${n}`);
|
|
12320
|
+
}
|
|
12321
|
+
this.config.maxConcurrent = Math.floor(n);
|
|
12322
|
+
this.tryDispatchNext();
|
|
12323
|
+
}
|
|
12324
|
+
/**
|
|
12325
|
+
* Upgrade a placeholder/role-derived name to a memorable scientist nickname
|
|
12326
|
+
* (e.g. "Einstein (Executor)"). A name is treated as a placeholder when it is
|
|
12327
|
+
* empty, equals the role (case-insensitive), is a generic default
|
|
12328
|
+
* ("subagent"/"adhoc"/"generic"), or is an auto-generated `slot-…` id.
|
|
12329
|
+
* Explicit, human-chosen names — including nicknames already assigned by
|
|
12330
|
+
* `Director.spawn()` — are left untouched, so this never double-assigns.
|
|
12331
|
+
*/
|
|
12332
|
+
withNickname(subagent) {
|
|
12333
|
+
const role = subagent.role ?? "subagent";
|
|
12334
|
+
const name = subagent.name?.trim() ?? "";
|
|
12335
|
+
const isPlaceholder = name === "" || name.toLowerCase() === role.toLowerCase() || name === "subagent" || name === "adhoc" || name === "generic" || /^slot-/.test(name);
|
|
12336
|
+
if (!isPlaceholder) return subagent;
|
|
12337
|
+
const nickname = assignNickname(role, this.usedNicknames);
|
|
12338
|
+
const baseKey = nickname.split(" ")[0].toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
12339
|
+
this.usedNicknames.add(baseKey);
|
|
12340
|
+
return { ...subagent, name: nickname };
|
|
12341
|
+
}
|
|
12342
|
+
async spawn(subagent) {
|
|
12343
|
+
subagent = this.withNickname(subagent);
|
|
12344
|
+
const id = subagent.id || randomUUID();
|
|
12345
|
+
if (this.subagents.has(id)) {
|
|
12346
|
+
throw new Error(`Subagent id "${id}" already exists \u2014 refusing to overwrite`);
|
|
12347
|
+
}
|
|
12348
|
+
const context = {
|
|
12349
|
+
subagentId: id,
|
|
12350
|
+
tasks: [],
|
|
12351
|
+
// Wired later by the caller via setSubagentBridge() once the
|
|
12352
|
+
// bidirectional bridge is created. Readers must null-check / use
|
|
12353
|
+
// hasParentBridge() — the type now reflects this.
|
|
12354
|
+
parentBridge: null,
|
|
12355
|
+
doneCondition: this.config.doneCondition,
|
|
12356
|
+
maxConcurrent: this.config.maxConcurrent ?? 16
|
|
12357
|
+
};
|
|
12358
|
+
this.subagents.set(id, {
|
|
12359
|
+
config: { ...subagent, id },
|
|
12360
|
+
context,
|
|
12361
|
+
status: "idle",
|
|
12362
|
+
abortController: new AbortController()
|
|
12363
|
+
});
|
|
12364
|
+
this.emit("subagent.started", { subagent: { ...subagent, id } });
|
|
12365
|
+
this.fleetBus?.emit({
|
|
11685
12366
|
subagentId: id,
|
|
11686
12367
|
ts: Date.now(),
|
|
11687
12368
|
type: "subagent.assigned",
|
|
@@ -11967,13 +12648,17 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
11967
12648
|
const rawMaxTokens = subagent.config.maxTokens;
|
|
11968
12649
|
const rawMaxCostUsd = subagent.config.maxCostUsd;
|
|
11969
12650
|
const rawTimeoutMs = subagent.config.timeoutMs;
|
|
12651
|
+
const rawIdleTimeoutMs = subagent.config.idleTimeoutMs;
|
|
11970
12652
|
const configWithRosterDefaults = applyRosterBudget(subagent.config);
|
|
11971
12653
|
const budget = new SubagentBudget({
|
|
11972
12654
|
maxIterations: rawMaxIterations ?? this.config.defaultBudget?.maxIterations ?? configWithRosterDefaults.maxIterations,
|
|
11973
12655
|
maxToolCalls: rawMaxToolCalls ?? this.config.defaultBudget?.maxToolCalls ?? configWithRosterDefaults.maxToolCalls,
|
|
11974
12656
|
maxTokens: rawMaxTokens ?? this.config.defaultBudget?.maxTokens ?? configWithRosterDefaults.maxTokens,
|
|
11975
12657
|
maxCostUsd: rawMaxCostUsd ?? this.config.defaultBudget?.maxCostUsd ?? configWithRosterDefaults.maxCostUsd,
|
|
11976
|
-
|
|
12658
|
+
// Wall-clock cap is opt-in (explicit config / defaultBudget only); the
|
|
12659
|
+
// roster no longer supplies one. Idle is the default reaper.
|
|
12660
|
+
timeoutMs: rawTimeoutMs ?? this.config.defaultBudget?.timeoutMs ?? configWithRosterDefaults.timeoutMs,
|
|
12661
|
+
idleTimeoutMs: rawIdleTimeoutMs ?? this.config.defaultBudget?.idleTimeoutMs ?? configWithRosterDefaults.idleTimeoutMs
|
|
11977
12662
|
});
|
|
11978
12663
|
subagent.activeBudget = budget;
|
|
11979
12664
|
if (!this.runner) {
|
|
@@ -12020,63 +12705,79 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
12020
12705
|
}
|
|
12021
12706
|
async executeWithTimeout(runner, task, ctx, budget) {
|
|
12022
12707
|
const initialTimeoutMs = budget.limits.timeoutMs;
|
|
12023
|
-
|
|
12708
|
+
const idleLimitMs = budget.limits.idleTimeoutMs;
|
|
12709
|
+
if (initialTimeoutMs === void 0 && idleLimitMs === void 0) {
|
|
12710
|
+
return runner(task, ctx);
|
|
12711
|
+
}
|
|
12024
12712
|
const start = Date.now();
|
|
12025
12713
|
let timer = null;
|
|
12026
12714
|
const timeoutPromise = new Promise((_, reject) => {
|
|
12027
12715
|
const armFor = (ms) => {
|
|
12028
12716
|
if (timer) clearTimeout(timer);
|
|
12029
|
-
timer = setTimeout(
|
|
12030
|
-
|
|
12031
|
-
|
|
12032
|
-
|
|
12033
|
-
|
|
12034
|
-
|
|
12717
|
+
timer = setTimeout(onTick, Math.max(0, ms));
|
|
12718
|
+
};
|
|
12719
|
+
const scheduleNext = () => {
|
|
12720
|
+
const wallRemaining = initialTimeoutMs === void 0 ? Number.POSITIVE_INFINITY : (budget.limits.timeoutMs ?? initialTimeoutMs) - (Date.now() - start);
|
|
12721
|
+
const idleRemaining = idleLimitMs === void 0 ? Number.POSITIVE_INFINITY : (budget.limits.idleTimeoutMs ?? idleLimitMs) - budget.idleMs();
|
|
12722
|
+
armFor(Math.max(25, Math.min(wallRemaining, idleRemaining)));
|
|
12723
|
+
};
|
|
12724
|
+
const onTick = async () => {
|
|
12725
|
+
const elapsed = Date.now() - start;
|
|
12726
|
+
const wallLimit = initialTimeoutMs === void 0 ? void 0 : budget.limits.timeoutMs ?? initialTimeoutMs;
|
|
12727
|
+
const idleLimit = idleLimitMs === void 0 ? void 0 : budget.limits.idleTimeoutMs ?? idleLimitMs;
|
|
12728
|
+
const wallExceeded = wallLimit !== void 0 && elapsed >= wallLimit;
|
|
12729
|
+
const idleExceeded = idleLimit !== void 0 && budget.idleMs() >= idleLimit;
|
|
12730
|
+
if (idleExceeded && !wallExceeded) {
|
|
12731
|
+
this.subagents.get(ctx.subagentId)?.abortController.abort();
|
|
12732
|
+
reject(new BudgetExceededError("timeout", idleLimit, budget.idleMs()));
|
|
12733
|
+
return;
|
|
12734
|
+
}
|
|
12735
|
+
if (!wallExceeded) {
|
|
12736
|
+
scheduleNext();
|
|
12737
|
+
return;
|
|
12738
|
+
}
|
|
12739
|
+
const limit = wallLimit;
|
|
12740
|
+
if (!budget.onThreshold) {
|
|
12741
|
+
this.subagents.get(ctx.subagentId)?.abortController.abort();
|
|
12742
|
+
reject(new BudgetExceededError("timeout", limit, elapsed));
|
|
12743
|
+
return;
|
|
12744
|
+
}
|
|
12745
|
+
try {
|
|
12746
|
+
const result = budget.onThreshold({
|
|
12747
|
+
kind: "timeout",
|
|
12748
|
+
used: elapsed,
|
|
12749
|
+
limit,
|
|
12750
|
+
requestDecision: () => new Promise((resolveDecision) => {
|
|
12751
|
+
budget._events?.emit("budget.threshold_reached", {
|
|
12752
|
+
kind: "timeout",
|
|
12753
|
+
used: elapsed,
|
|
12754
|
+
limit,
|
|
12755
|
+
timeoutMs: 6e4,
|
|
12756
|
+
extend: (extra) => resolveDecision({ extend: extra }),
|
|
12757
|
+
deny: () => resolveDecision("stop")
|
|
12758
|
+
});
|
|
12759
|
+
})
|
|
12760
|
+
});
|
|
12761
|
+
const decision = typeof result === "string" ? result : await result;
|
|
12762
|
+
if (decision === "continue" || decision === "throw" || decision === "stop") {
|
|
12763
|
+
armFor(Math.max(1e3, limit));
|
|
12035
12764
|
return;
|
|
12036
12765
|
}
|
|
12037
|
-
|
|
12038
|
-
|
|
12039
|
-
|
|
12040
|
-
|
|
12041
|
-
limit,
|
|
12042
|
-
requestDecision: () => new Promise((resolveDecision) => {
|
|
12043
|
-
budget._events?.emit("budget.threshold_reached", {
|
|
12044
|
-
kind: "timeout",
|
|
12045
|
-
used: elapsed,
|
|
12046
|
-
limit,
|
|
12047
|
-
timeoutMs: 6e4,
|
|
12048
|
-
extend: (extra) => resolveDecision({ extend: extra }),
|
|
12049
|
-
deny: () => resolveDecision("stop")
|
|
12050
|
-
});
|
|
12051
|
-
})
|
|
12052
|
-
});
|
|
12053
|
-
const decision = typeof result === "string" ? result : await result;
|
|
12054
|
-
if (decision === "continue") {
|
|
12055
|
-
armFor(Math.max(1e3, limit));
|
|
12056
|
-
return;
|
|
12057
|
-
}
|
|
12058
|
-
if (decision === "throw" || decision === "stop") {
|
|
12059
|
-
armFor(Math.max(1e3, limit));
|
|
12060
|
-
return;
|
|
12061
|
-
}
|
|
12062
|
-
if (decision.extend.timeoutMs !== void 0) {
|
|
12063
|
-
budget.limits.timeoutMs = decision.extend.timeoutMs;
|
|
12064
|
-
const newLimit = decision.extend.timeoutMs;
|
|
12065
|
-
const remaining = Math.max(1e3, newLimit - elapsed);
|
|
12066
|
-
armFor(remaining);
|
|
12067
|
-
return;
|
|
12068
|
-
}
|
|
12069
|
-
this.subagents.get(ctx.subagentId)?.abortController.abort();
|
|
12070
|
-
reject(new BudgetExceededError("timeout", limit, elapsed));
|
|
12071
|
-
} catch (err) {
|
|
12072
|
-
this.subagents.get(ctx.subagentId)?.abortController.abort();
|
|
12073
|
-
reject(
|
|
12074
|
-
err instanceof BudgetExceededError ? err : new BudgetExceededError("timeout", limit, elapsed)
|
|
12075
|
-
);
|
|
12766
|
+
if (decision.extend.timeoutMs !== void 0) {
|
|
12767
|
+
budget.limits.timeoutMs = decision.extend.timeoutMs;
|
|
12768
|
+
scheduleNext();
|
|
12769
|
+
return;
|
|
12076
12770
|
}
|
|
12077
|
-
|
|
12771
|
+
this.subagents.get(ctx.subagentId)?.abortController.abort();
|
|
12772
|
+
reject(new BudgetExceededError("timeout", limit, elapsed));
|
|
12773
|
+
} catch (err) {
|
|
12774
|
+
this.subagents.get(ctx.subagentId)?.abortController.abort();
|
|
12775
|
+
reject(
|
|
12776
|
+
err instanceof BudgetExceededError ? err : new BudgetExceededError("timeout", limit, elapsed)
|
|
12777
|
+
);
|
|
12778
|
+
}
|
|
12078
12779
|
};
|
|
12079
|
-
|
|
12780
|
+
scheduleNext();
|
|
12080
12781
|
});
|
|
12081
12782
|
try {
|
|
12082
12783
|
return await Promise.race([runner(task, ctx), timeoutPromise]);
|
|
@@ -12253,12 +12954,12 @@ var GOAL_COMPLETE_MARKER2 = /^\s*\[goal[_\s-]?complete\]\s*$/im;
|
|
|
12253
12954
|
var ParallelEternalEngine = class {
|
|
12254
12955
|
constructor(opts) {
|
|
12255
12956
|
this.opts = opts;
|
|
12256
|
-
this.goalPath = goalFilePath(opts.projectRoot);
|
|
12957
|
+
this.goalPath = opts.goalPath ?? goalFilePath(opts.projectRoot);
|
|
12257
12958
|
this.slots = Math.min(16, Math.max(1, opts.parallelSlots ?? 4));
|
|
12258
12959
|
this.timeoutMs = opts.iterationTimeoutMs ?? 3e5;
|
|
12259
12960
|
this.dispatchEnabled = opts.dispatch !== false;
|
|
12260
12961
|
this.dispatchClassifier = opts.dispatchClassifier;
|
|
12261
|
-
this.agentFactory = opts.subagentFactory ?? (async (
|
|
12962
|
+
this.agentFactory = opts.subagentFactory ?? (async (_config) => ({
|
|
12262
12963
|
agent: this.opts.agent,
|
|
12263
12964
|
events: this.opts.agent.events
|
|
12264
12965
|
}));
|
|
@@ -12352,7 +13053,6 @@ var ParallelEternalEngine = class {
|
|
|
12352
13053
|
}
|
|
12353
13054
|
const tasks = await this.decomposeGoal(goal);
|
|
12354
13055
|
if (!tasks || tasks.length === 0) {
|
|
12355
|
-
await sleep2(5e3);
|
|
12356
13056
|
return false;
|
|
12357
13057
|
}
|
|
12358
13058
|
const fanOut = await this.fanOut(goal, tasks);
|
|
@@ -12373,6 +13073,7 @@ var ParallelEternalEngine = class {
|
|
|
12373
13073
|
});
|
|
12374
13074
|
if (fanOut.goalComplete) {
|
|
12375
13075
|
this.stopRequested = true;
|
|
13076
|
+
this.state = "stopped";
|
|
12376
13077
|
return true;
|
|
12377
13078
|
}
|
|
12378
13079
|
await this.maybeCompact();
|
|
@@ -12553,7 +13254,11 @@ ${lastFew}` : "No prior iterations.",
|
|
|
12553
13254
|
} finally {
|
|
12554
13255
|
clearTimeout(timer);
|
|
12555
13256
|
}
|
|
12556
|
-
} catch {
|
|
13257
|
+
} catch (err) {
|
|
13258
|
+
this.opts.onError?.(
|
|
13259
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
13260
|
+
this.consecutiveFailures
|
|
13261
|
+
);
|
|
12557
13262
|
return [];
|
|
12558
13263
|
}
|
|
12559
13264
|
}
|
|
@@ -12770,7 +13475,11 @@ Working rules:
|
|
|
12770
13475
|
thrashing, terminate it rather than letting cost climb silently.
|
|
12771
13476
|
6. Never claim a subagent's work as your own without verifying it. If a
|
|
12772
13477
|
result looks wrong, ask_subagent for clarification before passing it
|
|
12773
|
-
to the user
|
|
13478
|
+
to the user.
|
|
13479
|
+
7. Wind down when satisfied. When the results are good enough, call
|
|
13480
|
+
work_complete \u2014 no new subagents will spawn and queued tasks complete
|
|
13481
|
+
as aborted. Running subagents finish naturally. Call terminate_subagent
|
|
13482
|
+
only for ones you need to stop immediately.`;
|
|
12774
13483
|
var DEFAULT_SUBAGENT_BASELINE = `You are a subagent operating under a Director. You were spawned to handle
|
|
12775
13484
|
a specific slice of a larger plan \u2014 do that slice well and report back.
|
|
12776
13485
|
|
|
@@ -12915,14 +13624,12 @@ var FleetBus = class {
|
|
|
12915
13624
|
};
|
|
12916
13625
|
var FleetUsageAggregator = class {
|
|
12917
13626
|
constructor(bus, priceLookup, metaLookup) {
|
|
12918
|
-
this.bus = bus;
|
|
12919
13627
|
this.priceLookup = priceLookup;
|
|
12920
13628
|
this.metaLookup = metaLookup;
|
|
12921
13629
|
bus.filter("provider.response", (e) => this.onProviderResponse(e));
|
|
12922
13630
|
bus.filter("tool.executed", (e) => this.onToolExecuted(e));
|
|
12923
13631
|
bus.filter("iteration.started", (e) => this.onIterationStarted(e));
|
|
12924
13632
|
}
|
|
12925
|
-
bus;
|
|
12926
13633
|
priceLookup;
|
|
12927
13634
|
metaLookup;
|
|
12928
13635
|
perSubagent = /* @__PURE__ */ new Map();
|
|
@@ -12990,106 +13697,6 @@ var FleetUsageAggregator = class {
|
|
|
12990
13697
|
snap.lastEventAt = e.ts;
|
|
12991
13698
|
}
|
|
12992
13699
|
};
|
|
12993
|
-
|
|
12994
|
-
// src/coordination/subagent-nicknames.ts
|
|
12995
|
-
var NICKNAME_POOL = {
|
|
12996
|
-
// Physics & fundamental sciences
|
|
12997
|
-
"einstein": { name: "Einstein", domain: "physics" },
|
|
12998
|
-
"newton": { name: "Newton", domain: "physics" },
|
|
12999
|
-
"feynman": { name: "Feynman", domain: "physics" },
|
|
13000
|
-
"dirac": { name: "Dirac", domain: "physics" },
|
|
13001
|
-
"bohr": { name: "Bohr", domain: "physics" },
|
|
13002
|
-
"planck": { name: "Planck", domain: "physics" },
|
|
13003
|
-
"curie": { name: "Curie", domain: "physics" },
|
|
13004
|
-
"fermi": { name: "Fermi", domain: "physics" },
|
|
13005
|
-
"heisenberg": { name: "Heisenberg", domain: "physics" },
|
|
13006
|
-
"schrodinger": { name: "Schr\xF6dinger", domain: "physics" },
|
|
13007
|
-
// Mathematics
|
|
13008
|
-
"euclid": { name: "Euclid", domain: "math" },
|
|
13009
|
-
"gauss": { name: "Gauss", domain: "math" },
|
|
13010
|
-
"turing": { name: "Turing", domain: "math" },
|
|
13011
|
-
"poincare": { name: "Poincar\xE9", domain: "math" },
|
|
13012
|
-
"riemann": { name: "Riemann", domain: "math" },
|
|
13013
|
-
"hilbert": { name: "Hilbert", domain: "math" },
|
|
13014
|
-
"pythagoras": { name: "Pythagoras", domain: "math" },
|
|
13015
|
-
// Computing & information theory
|
|
13016
|
-
"von-neumann": { name: "Von Neumann", domain: "computing" },
|
|
13017
|
-
"shannon": { name: "Shannon", domain: "computing" },
|
|
13018
|
-
"hopper": { name: "Hopper", domain: "computing" },
|
|
13019
|
-
"backus": { name: "Backus", domain: "computing" },
|
|
13020
|
-
"knuth": { name: "Knuth", domain: "computing" },
|
|
13021
|
-
"torvalds": { name: "Torvalds", domain: "computing" },
|
|
13022
|
-
"stallman": { name: "Stallman", domain: "computing" },
|
|
13023
|
-
"berners-lee": { name: "Berners-Lee", domain: "computing" },
|
|
13024
|
-
"babbage": { name: "Babbage", domain: "computing" },
|
|
13025
|
-
"lovelace": { name: "Lovelace", domain: "computing" },
|
|
13026
|
-
"klein": { name: "Klein", domain: "computing" },
|
|
13027
|
-
// Electronics & electrical engineering
|
|
13028
|
-
"edison": { name: "Edison", domain: "ee" },
|
|
13029
|
-
"tesla": { name: "Tesla", domain: "ee" },
|
|
13030
|
-
"faraday": { name: "Faraday", domain: "ee" },
|
|
13031
|
-
"maxwell": { name: "Maxwell", domain: "ee" },
|
|
13032
|
-
"ohm": { name: "Ohm", domain: "ee" },
|
|
13033
|
-
"bell": { name: "Bell", domain: "ee" },
|
|
13034
|
-
"marconi": { name: "Marconi", domain: "ee" },
|
|
13035
|
-
"lamarr": { name: "Lamarr", domain: "ee" },
|
|
13036
|
-
// General science / multi-disciplinary
|
|
13037
|
-
"darwin": { name: "Darwin", domain: "biology" },
|
|
13038
|
-
"mendel": { name: "Mendel", domain: "biology" },
|
|
13039
|
-
"pasteur": { name: "Pasteur", domain: "biology" },
|
|
13040
|
-
"hawking": { name: "Hawking", domain: "cosmology" },
|
|
13041
|
-
"sagan": { name: "Sagan", domain: "cosmology" },
|
|
13042
|
-
// Chemistry / materials
|
|
13043
|
-
"lavoisier": { name: "Lavoisier", domain: "chemistry" },
|
|
13044
|
-
"mendeleev": { name: "Mendeleev", domain: "chemistry" }
|
|
13045
|
-
};
|
|
13046
|
-
var ALL_NICKNAMES = Object.values(NICKNAME_POOL);
|
|
13047
|
-
var DOMAIN_PREFERENCES = {
|
|
13048
|
-
"security": ["shannon", "turing", "lamarr", "stallman"],
|
|
13049
|
-
"bug-hunter": ["darwin", "curie", "feynman", "fermi"],
|
|
13050
|
-
"refactor": ["gauss", "hilbert", "euclid", "planck"],
|
|
13051
|
-
"audit-log": ["sagan", "hawking", "poincare", "newton"],
|
|
13052
|
-
"planner": ["hilbert", "gauss", "turing", "euclid"],
|
|
13053
|
-
"researcher": ["sagan", "hawking", "darwin", "pasteur"],
|
|
13054
|
-
"explorer": ["marconi", "bell", "columbus", "polo"],
|
|
13055
|
-
"testing": ["pasteur", "curie", "fermi", "bohr"],
|
|
13056
|
-
"frontend": ["lovelace", "hopper", "babbage", "backus"],
|
|
13057
|
-
"backend": ["torvalds", "stallman", "von-neumann", "backus"],
|
|
13058
|
-
"database": ["turing", "shannon", "backus", "knuth"],
|
|
13059
|
-
"devops": ["tesla", "edison", "faraday", "bell"],
|
|
13060
|
-
"security-scanner": ["shannon", "turing", "lamarr", "stallman"],
|
|
13061
|
-
"refactor-planner": ["gauss", "hilbert", "planck", "newton"],
|
|
13062
|
-
"architect": ["von-neumann", "turing", "gauss", "hilbert"],
|
|
13063
|
-
"critic": ["einstein", "feynman", "dirac", "bohr"],
|
|
13064
|
-
"e2e": ["hopper", "bell", "marconi", "tesla"],
|
|
13065
|
-
"performance": ["knuth", "gauss", "planck", "feynman"],
|
|
13066
|
-
"chaos": ["tesla", "edison", "curie", "fermi"],
|
|
13067
|
-
"cost": ["oh", "bell", "marconi", "tesla"],
|
|
13068
|
-
// default fallback
|
|
13069
|
-
"default": ["einstein", "newton", "curie", "tesla", "edison", "turing", "shannon", "hopper", "knuth", "stallman"]
|
|
13070
|
-
};
|
|
13071
|
-
function assignNickname(role, used) {
|
|
13072
|
-
const preferences = [
|
|
13073
|
-
...DOMAIN_PREFERENCES[role] ?? [],
|
|
13074
|
-
...DOMAIN_PREFERENCES["default"] ?? []
|
|
13075
|
-
];
|
|
13076
|
-
for (const key of preferences) {
|
|
13077
|
-
if (!used.has(key)) {
|
|
13078
|
-
return `${NICKNAME_POOL[key].name} (${formatRole(role)})`;
|
|
13079
|
-
}
|
|
13080
|
-
}
|
|
13081
|
-
for (const entry of ALL_NICKNAMES) {
|
|
13082
|
-
const key = Object.entries(NICKNAME_POOL).find(([, v]) => v.name === entry.name)?.[0];
|
|
13083
|
-
if (key && !used.has(key)) {
|
|
13084
|
-
return `${entry.name} (${formatRole(role)})`;
|
|
13085
|
-
}
|
|
13086
|
-
}
|
|
13087
|
-
const counter = used.size + 1;
|
|
13088
|
-
return `Scientist #${counter} (${formatRole(role)})`;
|
|
13089
|
-
}
|
|
13090
|
-
function formatRole(role) {
|
|
13091
|
-
return role.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
13092
|
-
}
|
|
13093
13700
|
function makeSpawnTool(director, roster) {
|
|
13094
13701
|
const inputSchema = {
|
|
13095
13702
|
type: "object",
|
|
@@ -13260,7 +13867,7 @@ function makeRollUpTool(director) {
|
|
|
13260
13867
|
function makeTerminateTool(director) {
|
|
13261
13868
|
return {
|
|
13262
13869
|
name: "terminate_subagent",
|
|
13263
|
-
description:
|
|
13870
|
+
description: 'Forcibly abort a subagent. The subagent finishes its current iteration then exits with status "stopped".',
|
|
13264
13871
|
permission: "auto",
|
|
13265
13872
|
mutating: true,
|
|
13266
13873
|
inputSchema: { type: "object", properties: { subagentId: { type: "string", description: "Subagent to abort." } }, required: ["subagentId"] },
|
|
@@ -13271,6 +13878,19 @@ function makeTerminateTool(director) {
|
|
|
13271
13878
|
}
|
|
13272
13879
|
};
|
|
13273
13880
|
}
|
|
13881
|
+
function makeTerminateAllTool(director) {
|
|
13882
|
+
return {
|
|
13883
|
+
name: "terminate_all",
|
|
13884
|
+
description: 'Forcibly stop every subagent in the fleet and drain the pending task queue. In-flight tasks are terminated mid-execution; pending tasks receive "aborted_by_parent" completion immediately. Use this when the fleet is wedged, looping, or you need a clean slate. Compare: work_complete stops spawning but lets running agents finish naturally.',
|
|
13885
|
+
permission: "auto",
|
|
13886
|
+
mutating: true,
|
|
13887
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
13888
|
+
async execute() {
|
|
13889
|
+
await director.terminateAll();
|
|
13890
|
+
return { ok: true, message: `Fleet shutdown complete \u2014 all subagents stopped, pending tasks drained.` };
|
|
13891
|
+
}
|
|
13892
|
+
};
|
|
13893
|
+
}
|
|
13274
13894
|
function makeFleetStatusTool(director) {
|
|
13275
13895
|
return {
|
|
13276
13896
|
name: "fleet_status",
|
|
@@ -13413,7 +14033,7 @@ function makeCollabDebugTool(director) {
|
|
|
13413
14033
|
function makeFleetEmitTool(director) {
|
|
13414
14034
|
return {
|
|
13415
14035
|
name: "fleet_emit",
|
|
13416
|
-
description: "Emit a structured event on the FleetBus.
|
|
14036
|
+
description: "Emit a structured event on the FleetBus. Any subagent can emit any event type; the Director routes it to all listeners. Use it to stream findings, progress updates, or final results to other agents in real time.",
|
|
13417
14037
|
permission: "auto",
|
|
13418
14038
|
mutating: false,
|
|
13419
14039
|
inputSchema: {
|
|
@@ -13421,14 +14041,14 @@ function makeFleetEmitTool(director) {
|
|
|
13421
14041
|
properties: {
|
|
13422
14042
|
type: {
|
|
13423
14043
|
type: "string",
|
|
13424
|
-
description: "Event type
|
|
14044
|
+
description: "Event type string (e.g. bug.found, refactor.plan, critic.evaluation, progress, result)."
|
|
13425
14045
|
},
|
|
13426
14046
|
payload: {
|
|
13427
14047
|
type: "object",
|
|
13428
|
-
description: "Event payload
|
|
14048
|
+
description: "Event payload. Structure depends on event type. Use null if no payload."
|
|
13429
14049
|
}
|
|
13430
14050
|
},
|
|
13431
|
-
required: ["type"
|
|
14051
|
+
required: ["type"]
|
|
13432
14052
|
},
|
|
13433
14053
|
async execute(input) {
|
|
13434
14054
|
const i = input;
|
|
@@ -13436,12 +14056,25 @@ function makeFleetEmitTool(director) {
|
|
|
13436
14056
|
subagentId: director.id,
|
|
13437
14057
|
ts: Date.now(),
|
|
13438
14058
|
type: i.type,
|
|
13439
|
-
payload: i.payload
|
|
14059
|
+
payload: i.payload ?? {}
|
|
13440
14060
|
});
|
|
13441
14061
|
return { ok: true, event: i.type };
|
|
13442
14062
|
}
|
|
13443
14063
|
};
|
|
13444
14064
|
}
|
|
14065
|
+
function makeWorkCompleteTool(director) {
|
|
14066
|
+
return {
|
|
14067
|
+
name: "work_complete",
|
|
14068
|
+
description: "Signal that the director is satisfied with the results and the fleet should wind down. After calling this, spawn_subagent will refuse with a budget error and assign_task will instantly complete any queued tasks as aborted. Running subagents finish naturally. Call terminate_subagent separately to stop specific subagents immediately.",
|
|
14069
|
+
permission: "auto",
|
|
14070
|
+
mutating: false,
|
|
14071
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
14072
|
+
async execute() {
|
|
14073
|
+
director.workComplete();
|
|
14074
|
+
return { ok: true, message: "Fleet wind-down signaled. No new spawns or task dispatches." };
|
|
14075
|
+
}
|
|
14076
|
+
};
|
|
14077
|
+
}
|
|
13445
14078
|
var CollabSession = class extends EventEmitter {
|
|
13446
14079
|
sessionId;
|
|
13447
14080
|
options;
|
|
@@ -13456,6 +14089,14 @@ var CollabSession = class extends EventEmitter {
|
|
|
13456
14089
|
disposers = new Array();
|
|
13457
14090
|
settled = false;
|
|
13458
14091
|
timeoutMs;
|
|
14092
|
+
cancelled = false;
|
|
14093
|
+
alerts = [];
|
|
14094
|
+
/** Tracks tool call counts per subagent for progress-based timeout decisions. */
|
|
14095
|
+
progressBySubagent = /* @__PURE__ */ new Map();
|
|
14096
|
+
/** Last tool call count when a timeout warning was handled. */
|
|
14097
|
+
lastTimeoutProgress = /* @__PURE__ */ new Map();
|
|
14098
|
+
/** Session-level timeout timer handle (cleared on cancel or natural completion). */
|
|
14099
|
+
_timeoutTimer;
|
|
13459
14100
|
constructor(director, fleetBus, options) {
|
|
13460
14101
|
super();
|
|
13461
14102
|
this.sessionId = randomUUID();
|
|
@@ -13473,11 +14114,23 @@ var CollabSession = class extends EventEmitter {
|
|
|
13473
14114
|
};
|
|
13474
14115
|
}
|
|
13475
14116
|
}
|
|
14117
|
+
get id() {
|
|
14118
|
+
return this.sessionId;
|
|
14119
|
+
}
|
|
14120
|
+
getSessionAlerts() {
|
|
14121
|
+
return [...this.alerts];
|
|
14122
|
+
}
|
|
14123
|
+
isCancelled() {
|
|
14124
|
+
return this.cancelled;
|
|
14125
|
+
}
|
|
13476
14126
|
/**
|
|
13477
|
-
*
|
|
13478
|
-
*
|
|
13479
|
-
*
|
|
14127
|
+
* Snapshot of role → subagentId map. The Director calls coordinator.stop()
|
|
14128
|
+
* for each agent when cancelling the session, using this map to enumerate
|
|
14129
|
+
* all three collab agents.
|
|
13480
14130
|
*/
|
|
14131
|
+
getSubagentIds() {
|
|
14132
|
+
return new Map(this.subagentIds);
|
|
14133
|
+
}
|
|
13481
14134
|
async buildSnapshot() {
|
|
13482
14135
|
if (this.snapshot.files.length > 0) return this.snapshot;
|
|
13483
14136
|
for (const filePath of this.options.targetPaths) {
|
|
@@ -13493,9 +14146,30 @@ var CollabSession = class extends EventEmitter {
|
|
|
13493
14146
|
return this.snapshot;
|
|
13494
14147
|
}
|
|
13495
14148
|
/**
|
|
13496
|
-
*
|
|
13497
|
-
*
|
|
14149
|
+
* Cancel the session. Emits director.cancel_collab on the FleetBus so all
|
|
14150
|
+
* collab agents finish early. The session-level timeout timer is also cleared.
|
|
14151
|
+
* Safe to call multiple times (idempotent after first call).
|
|
13498
14152
|
*/
|
|
14153
|
+
cancel(reason = "Director cancelled collab session") {
|
|
14154
|
+
if (this.settled) return;
|
|
14155
|
+
this.cancelled = true;
|
|
14156
|
+
if (this._timeoutTimer) {
|
|
14157
|
+
clearTimeout(this._timeoutTimer);
|
|
14158
|
+
this._timeoutTimer = void 0;
|
|
14159
|
+
}
|
|
14160
|
+
this.fleetBus.emit({
|
|
14161
|
+
subagentId: this.director.id,
|
|
14162
|
+
ts: Date.now(),
|
|
14163
|
+
type: "director.cancel_collab",
|
|
14164
|
+
payload: { sessionId: this.sessionId, reason, cancelledAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
14165
|
+
});
|
|
14166
|
+
this.fleetBus.emit({
|
|
14167
|
+
subagentId: this.director.id,
|
|
14168
|
+
ts: Date.now(),
|
|
14169
|
+
type: "collab.cancelled",
|
|
14170
|
+
payload: { sessionId: this.sessionId, reason }
|
|
14171
|
+
});
|
|
14172
|
+
}
|
|
13499
14173
|
async start() {
|
|
13500
14174
|
if (this.settled) throw new Error("session already settled");
|
|
13501
14175
|
this.settled = true;
|
|
@@ -13510,10 +14184,10 @@ var CollabSession = class extends EventEmitter {
|
|
|
13510
14184
|
this.subagentIds.set("refactor-planner", refactorPlannerId);
|
|
13511
14185
|
this.subagentIds.set("critic", criticId);
|
|
13512
14186
|
const timeout = new Promise((_, reject) => {
|
|
13513
|
-
setTimeout(
|
|
13514
|
-
(
|
|
13515
|
-
this.timeoutMs
|
|
13516
|
-
);
|
|
14187
|
+
this._timeoutTimer = setTimeout(() => {
|
|
14188
|
+
this.cancel("Session-level timeout reached");
|
|
14189
|
+
reject(new Error(`CollabSession timed out after ${this.timeoutMs}ms`));
|
|
14190
|
+
}, this.timeoutMs);
|
|
13517
14191
|
});
|
|
13518
14192
|
let results;
|
|
13519
14193
|
try {
|
|
@@ -13526,6 +14200,10 @@ var CollabSession = class extends EventEmitter {
|
|
|
13526
14200
|
timeout
|
|
13527
14201
|
]);
|
|
13528
14202
|
} catch (err) {
|
|
14203
|
+
if (this._timeoutTimer) {
|
|
14204
|
+
clearTimeout(this._timeoutTimer);
|
|
14205
|
+
this._timeoutTimer = void 0;
|
|
14206
|
+
}
|
|
13529
14207
|
this.cleanup();
|
|
13530
14208
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
13531
14209
|
this.emit("session.error", error);
|
|
@@ -13539,17 +14217,6 @@ var CollabSession = class extends EventEmitter {
|
|
|
13539
14217
|
this.emit("session.done", report);
|
|
13540
14218
|
return report;
|
|
13541
14219
|
}
|
|
13542
|
-
/**
|
|
13543
|
-
* Parse a completed agent's final text output and route the structured
|
|
13544
|
-
* findings onto the FleetBus. Agents are instructed to emit one JSON object
|
|
13545
|
-
* per finding / plan / evaluation; we scan the output for balanced JSON
|
|
13546
|
-
* objects (tolerating prose, markdown, and ```json fences around them) and
|
|
13547
|
-
* dispatch each by its discriminating key. The wired FleetBus listeners then
|
|
13548
|
-
* collect them into the bugs / plans / evaluations maps.
|
|
13549
|
-
*
|
|
13550
|
-
* Failed or empty results are ignored, and a single malformed object never
|
|
13551
|
-
* aborts the collation pass — see {@link extractJsonObjects}.
|
|
13552
|
-
*/
|
|
13553
14220
|
async parseAndEmit(result) {
|
|
13554
14221
|
if (result.status !== "success" || result.result == null) return;
|
|
13555
14222
|
const text = typeof result.result === "string" ? result.result : JSON.stringify(result.result);
|
|
@@ -13565,12 +14232,6 @@ var CollabSession = class extends EventEmitter {
|
|
|
13565
14232
|
});
|
|
13566
14233
|
}
|
|
13567
14234
|
}
|
|
13568
|
-
/**
|
|
13569
|
-
* Extract every top-level JSON object embedded in free-form agent text.
|
|
13570
|
-
* Scans for balanced braces while respecting string literals and escape
|
|
13571
|
-
* sequences, so prose, markdown headers, and code fences around the JSON
|
|
13572
|
-
* are skipped. Malformed spans are dropped silently rather than thrown.
|
|
13573
|
-
*/
|
|
13574
14235
|
extractJsonObjects(text) {
|
|
13575
14236
|
const objects = [];
|
|
13576
14237
|
let depth = 0;
|
|
@@ -13607,25 +14268,33 @@ var CollabSession = class extends EventEmitter {
|
|
|
13607
14268
|
}
|
|
13608
14269
|
return objects;
|
|
13609
14270
|
}
|
|
14271
|
+
budgetForRole(role) {
|
|
14272
|
+
if (this.options.budgetOverrides?.[role]) {
|
|
14273
|
+
return this.options.budgetOverrides[role];
|
|
14274
|
+
}
|
|
14275
|
+
const defaults = {
|
|
14276
|
+
"bug-hunter": { maxIterations: 2e3, maxToolCalls: 5e3, timeoutMs: 10 * 60 * 1e3 },
|
|
14277
|
+
"refactor-planner": { maxIterations: 1500, maxToolCalls: 4e3, timeoutMs: 8 * 60 * 1e3 },
|
|
14278
|
+
"critic": { maxIterations: 1e3, maxToolCalls: 3e3, timeoutMs: 6 * 60 * 1e3 }
|
|
14279
|
+
};
|
|
14280
|
+
return defaults[role] ?? { maxIterations: 1500, maxToolCalls: 4e3, timeoutMs: 8 * 60 * 1e3 };
|
|
14281
|
+
}
|
|
13610
14282
|
async spawnAgent(role, taskBrief) {
|
|
14283
|
+
const budget = this.budgetForRole(role);
|
|
13611
14284
|
const cfg = {
|
|
13612
14285
|
id: `${role}-${this.sessionId}`,
|
|
13613
14286
|
name: role,
|
|
13614
|
-
role
|
|
13615
|
-
|
|
13616
|
-
|
|
13617
|
-
|
|
14287
|
+
role,
|
|
14288
|
+
tools: ["fleet_emit", "fleet_status", "read", "grep", "glob", "bash", "write"],
|
|
14289
|
+
maxIterations: budget.maxIterations,
|
|
14290
|
+
maxToolCalls: budget.maxToolCalls,
|
|
14291
|
+
timeoutMs: budget.timeoutMs
|
|
13618
14292
|
};
|
|
13619
14293
|
const subagentId = await this.director.spawn(cfg);
|
|
13620
|
-
await this.director.assign({
|
|
13621
|
-
id: randomUUID(),
|
|
13622
|
-
subagentId,
|
|
13623
|
-
description: taskBrief
|
|
13624
|
-
});
|
|
14294
|
+
await this.director.assign({ id: randomUUID(), subagentId, description: taskBrief });
|
|
13625
14295
|
return subagentId;
|
|
13626
14296
|
}
|
|
13627
14297
|
buildBugHunterTask() {
|
|
13628
|
-
this.options.targetPaths.join(", ");
|
|
13629
14298
|
const scratchpad = this.director.sharedScratchpadPath ?? "/tmp";
|
|
13630
14299
|
const fileContents = this.snapshot.files.map((f) => `=== ${f.path} ===
|
|
13631
14300
|
${f.content}`).join("\n\n");
|
|
@@ -13634,13 +14303,17 @@ ${f.content}`).join("\n\n");
|
|
|
13634
14303
|
Target files:
|
|
13635
14304
|
${fileContents}
|
|
13636
14305
|
|
|
13637
|
-
For each bug found,
|
|
13638
|
-
{ "finding": { "id": "<uuid>", "type": "<pattern>", "severity": "<critical|high|medium|low>", "location": { "file": "<path>", "line": <n> }, "description": "<explain>", "suggestedFix": "<optional>" } }
|
|
13639
|
-
|
|
13640
|
-
|
|
14306
|
+
For each bug found, emit it using the fleet_emit tool immediately:
|
|
14307
|
+
{ "type": "bug.found", "payload": { "finding": { "id": "<uuid>", "type": "<pattern>", "severity": "<critical|high|medium|low>", "location": { "file": "<path>", "line": <n> }, "description": "<explain>", "suggestedFix": "<optional>" } } }
|
|
14308
|
+
|
|
14309
|
+
After scanning all files, write your full markdown bug report to:
|
|
14310
|
+
${scratchpad}/bug-hunter-report-${this.sessionId}.md
|
|
14311
|
+
|
|
14312
|
+
Important: emit each finding as soon as you find it. Do not batch or wait until the end.`;
|
|
13641
14313
|
}
|
|
13642
14314
|
buildRefactorPlannerTask() {
|
|
13643
14315
|
const scratchpad = this.director.sharedScratchpadPath ?? "/tmp";
|
|
14316
|
+
const bugHunterReportPath = `${scratchpad}/bug-hunter-report-${this.sessionId}.md`;
|
|
13644
14317
|
const fileContents = this.snapshot.files.map((f) => `=== ${f.path} ===
|
|
13645
14318
|
${f.content}`).join("\n\n");
|
|
13646
14319
|
return `You are RefactorPlanner. Plan refactorings for the following files.
|
|
@@ -13648,26 +14321,146 @@ ${f.content}`).join("\n\n");
|
|
|
13648
14321
|
Target files:
|
|
13649
14322
|
${fileContents}
|
|
13650
14323
|
|
|
13651
|
-
|
|
13652
|
-
|
|
13653
|
-
|
|
13654
|
-
|
|
14324
|
+
Read the BugHunter report at: ${bugHunterReportPath}
|
|
14325
|
+
|
|
14326
|
+
For each bug you can address, emit a refactor plan using fleet_emit:
|
|
14327
|
+
{ "type": "refactor.plan", "payload": { "plan": { "id": "<uuid>", "basedOnBugIds": ["<bug-id>"], "phases": [{ "number": 1, "title": "<phase>", "tasks": ["<task>"], "risk": "<low|medium|high>" }], "riskScore": "<low|medium|high>", "estimatedChangeCount": <n>, "rollbackStrategy": "<text>" } } }
|
|
14328
|
+
|
|
14329
|
+
Also write your full markdown plan to:
|
|
14330
|
+
${scratchpad}/refactor-plan-${this.sessionId}.md
|
|
14331
|
+
|
|
14332
|
+
Emit each plan immediately. Do not wait until planning is complete.`;
|
|
13655
14333
|
}
|
|
13656
14334
|
buildCriticTask() {
|
|
13657
14335
|
const scratchpad = this.director.sharedScratchpadPath ?? "/tmp";
|
|
14336
|
+
const bugHunterReportPath = `${scratchpad}/bug-hunter-report-${this.sessionId}.md`;
|
|
14337
|
+
const refactorPlanPath = `${scratchpad}/refactor-plan-${this.sessionId}.md`;
|
|
13658
14338
|
const fileContents = this.snapshot.files.map((f) => `=== ${f.path} ===
|
|
13659
14339
|
${f.content}`).join("\n\n");
|
|
13660
|
-
return `You are Critic. Evaluate bug findings and refactor plans
|
|
14340
|
+
return `You are Critic. Evaluate bug findings and refactor plans.
|
|
13661
14341
|
|
|
13662
14342
|
Target files:
|
|
13663
14343
|
${fileContents}
|
|
13664
14344
|
|
|
13665
|
-
|
|
13666
|
-
|
|
13667
|
-
|
|
13668
|
-
|
|
14345
|
+
Read the BugHunter report at: ${bugHunterReportPath}
|
|
14346
|
+
Read the RefactorPlanner report at: ${refactorPlanPath}
|
|
14347
|
+
|
|
14348
|
+
For each bug and refactor plan, emit your evaluation using fleet_emit:
|
|
14349
|
+
{ "type": "critic.evaluation", "payload": { "evaluation": { "id": "<uuid>", "subjectType": "<bug_finding|refactor_plan>", "subjectId": "<id>", "score": <0-10>, "verdict": "<approve|needs_revision|reject>", "strengths": ["<strength>"], "weaknesses": ["<weakness>"], "concerns": [{ "description": "<concern>", "severity": "<blocking|advisory>" }] } } }
|
|
14350
|
+
|
|
14351
|
+
After all evaluations, write your markdown report to:
|
|
14352
|
+
${scratchpad}/critic-report-${this.sessionId}.md
|
|
14353
|
+
|
|
14354
|
+
Emit each evaluation immediately. Do not wait until you have read all reports.`;
|
|
13669
14355
|
}
|
|
13670
14356
|
wireFleetBus() {
|
|
14357
|
+
const dTool = this.fleetBus.filter("tool.executed", (e) => {
|
|
14358
|
+
this.progressBySubagent.set(e.subagentId, (this.progressBySubagent.get(e.subagentId) ?? 0) + 1);
|
|
14359
|
+
});
|
|
14360
|
+
this.disposers.push(dTool);
|
|
14361
|
+
const dBudget = this.fleetBus.filter("budget.threshold_reached", (e) => {
|
|
14362
|
+
const payload = e.payload;
|
|
14363
|
+
const role = this.roleFromSubagentId(e.subagentId);
|
|
14364
|
+
if (!role) return;
|
|
14365
|
+
const btwNotes = this.director.getLeaderBtwNotes();
|
|
14366
|
+
const alert = {
|
|
14367
|
+
sessionId: this.sessionId,
|
|
14368
|
+
subagentId: e.subagentId,
|
|
14369
|
+
role,
|
|
14370
|
+
level: "warning" /* WARNING */,
|
|
14371
|
+
message: `${role} hit ${payload.kind} soft limit (${payload.used}/${payload.limit})`,
|
|
14372
|
+
budgetKind: payload.kind,
|
|
14373
|
+
elapsedMs: payload.timeoutMs,
|
|
14374
|
+
limit: payload.limit,
|
|
14375
|
+
btwNotes
|
|
14376
|
+
};
|
|
14377
|
+
this.alerts.push(alert);
|
|
14378
|
+
this.fleetBus.emit({
|
|
14379
|
+
subagentId: e.subagentId,
|
|
14380
|
+
ts: Date.now(),
|
|
14381
|
+
type: "collab.warning",
|
|
14382
|
+
payload: alert
|
|
14383
|
+
});
|
|
14384
|
+
const decision = this.options.onBudgetWarning?.(alert) ?? "ignore";
|
|
14385
|
+
if (decision === "cancel") {
|
|
14386
|
+
this.cancel(`Director cancelled: ${role} ${payload.kind} threshold`);
|
|
14387
|
+
return;
|
|
14388
|
+
}
|
|
14389
|
+
if (payload.kind === "timeout") {
|
|
14390
|
+
const progress = this.progressBySubagent.get(e.subagentId) ?? 0;
|
|
14391
|
+
const lastProgress = this.lastTimeoutProgress.get(e.subagentId) ?? -1;
|
|
14392
|
+
if (progress <= lastProgress) {
|
|
14393
|
+
payload.deny();
|
|
14394
|
+
return;
|
|
14395
|
+
}
|
|
14396
|
+
this.lastTimeoutProgress.set(e.subagentId, progress);
|
|
14397
|
+
const newLimit = Math.min(Math.ceil((payload.timeoutMs ?? payload.limit) * 2), 24 * 60 * 6e4);
|
|
14398
|
+
setImmediate(() => {
|
|
14399
|
+
payload.extend({ timeoutMs: newLimit });
|
|
14400
|
+
});
|
|
14401
|
+
return;
|
|
14402
|
+
}
|
|
14403
|
+
if (decision === "extend") {
|
|
14404
|
+
setImmediate(() => {
|
|
14405
|
+
const base = Math.max(payload.limit, payload.used);
|
|
14406
|
+
const extra = {};
|
|
14407
|
+
switch (payload.kind) {
|
|
14408
|
+
case "iterations":
|
|
14409
|
+
extra.maxIterations = Math.min(Math.ceil(base * 1.5), 5e4);
|
|
14410
|
+
break;
|
|
14411
|
+
case "tool_calls":
|
|
14412
|
+
extra.maxToolCalls = Math.min(Math.ceil(base * 1.5), 1e5);
|
|
14413
|
+
break;
|
|
14414
|
+
case "tokens":
|
|
14415
|
+
extra.maxTokens = Math.min(Math.ceil(base * 1.5), 5e6);
|
|
14416
|
+
break;
|
|
14417
|
+
case "cost":
|
|
14418
|
+
extra.maxCostUsd = Math.min(base * 1.5, 100);
|
|
14419
|
+
break;
|
|
14420
|
+
}
|
|
14421
|
+
payload.extend(extra);
|
|
14422
|
+
});
|
|
14423
|
+
return;
|
|
14424
|
+
}
|
|
14425
|
+
if (payload.kind !== "timeout") {
|
|
14426
|
+
setImmediate(() => {
|
|
14427
|
+
const base = Math.max(payload.limit, payload.used);
|
|
14428
|
+
const extra = {};
|
|
14429
|
+
switch (payload.kind) {
|
|
14430
|
+
case "iterations":
|
|
14431
|
+
extra.maxIterations = Math.min(Math.ceil(base * 1.25), 5e4);
|
|
14432
|
+
break;
|
|
14433
|
+
case "tool_calls":
|
|
14434
|
+
extra.maxToolCalls = Math.min(Math.ceil(base * 1.25), 1e5);
|
|
14435
|
+
break;
|
|
14436
|
+
case "tokens":
|
|
14437
|
+
extra.maxTokens = Math.min(Math.ceil(base * 1.25), 5e6);
|
|
14438
|
+
break;
|
|
14439
|
+
case "cost":
|
|
14440
|
+
extra.maxCostUsd = Math.min(base * 1.25, 100);
|
|
14441
|
+
break;
|
|
14442
|
+
}
|
|
14443
|
+
payload.extend(extra);
|
|
14444
|
+
});
|
|
14445
|
+
}
|
|
14446
|
+
});
|
|
14447
|
+
this.disposers.push(dBudget);
|
|
14448
|
+
const dCancel = this.fleetBus.filter("director.cancel_collab", (e) => {
|
|
14449
|
+
const payload = e.payload;
|
|
14450
|
+
if (payload.sessionId !== this.sessionId) return;
|
|
14451
|
+
this.cancelled = true;
|
|
14452
|
+
if (this._timeoutTimer) {
|
|
14453
|
+
clearTimeout(this._timeoutTimer);
|
|
14454
|
+
this._timeoutTimer = void 0;
|
|
14455
|
+
}
|
|
14456
|
+
this.fleetBus.emit({
|
|
14457
|
+
subagentId: this.director.id,
|
|
14458
|
+
ts: Date.now(),
|
|
14459
|
+
type: "collab.cancelled",
|
|
14460
|
+
payload: { sessionId: this.sessionId, reason: payload.reason }
|
|
14461
|
+
});
|
|
14462
|
+
});
|
|
14463
|
+
this.disposers.push(dCancel);
|
|
13671
14464
|
const d1 = this.fleetBus.filter("bug.found", (e) => {
|
|
13672
14465
|
const payload = e.payload;
|
|
13673
14466
|
if (payload?.finding) {
|
|
@@ -13693,10 +14486,19 @@ ${scratchpad}/critic-report-${this.sessionId}.md`;
|
|
|
13693
14486
|
});
|
|
13694
14487
|
this.disposers.push(d3);
|
|
13695
14488
|
}
|
|
14489
|
+
roleFromSubagentId(subagentId) {
|
|
14490
|
+
for (const [role, id] of this.subagentIds) {
|
|
14491
|
+
if (id === subagentId) return role;
|
|
14492
|
+
}
|
|
14493
|
+
const match = subagentId.match(/^(bug-hunter|refactor-planner|critic)/);
|
|
14494
|
+
return match?.[1] ?? null;
|
|
14495
|
+
}
|
|
13696
14496
|
assembleReport() {
|
|
13697
14497
|
const bugList = Array.from(this.bugs.values());
|
|
13698
14498
|
const planList = Array.from(this.plans.values());
|
|
13699
14499
|
const evalList = Array.from(this.evaluations.values());
|
|
14500
|
+
let disposition = "completed";
|
|
14501
|
+
if (this.cancelled) disposition = "cancelled";
|
|
13700
14502
|
const verdictOrder = {
|
|
13701
14503
|
approve: 0,
|
|
13702
14504
|
needs_revision: 1,
|
|
@@ -13710,33 +14512,41 @@ ${scratchpad}/critic-report-${this.sessionId}.md`;
|
|
|
13710
14512
|
},
|
|
13711
14513
|
"approve"
|
|
13712
14514
|
);
|
|
13713
|
-
const summary = this.buildMarkdownSummary(bugList, planList, evalList, overallVerdict);
|
|
14515
|
+
const summary = this.buildMarkdownSummary(bugList, planList, evalList, overallVerdict, disposition);
|
|
13714
14516
|
return {
|
|
13715
14517
|
sessionId: this.sessionId,
|
|
13716
14518
|
startedAt: this.snapshot.createdAt,
|
|
13717
14519
|
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13718
14520
|
targetPaths: this.options.targetPaths,
|
|
14521
|
+
disposition,
|
|
13719
14522
|
bugs: bugList,
|
|
13720
14523
|
refactorPlans: planList,
|
|
13721
14524
|
evaluations: evalList,
|
|
14525
|
+
alerts: [...this.alerts],
|
|
13722
14526
|
overallVerdict,
|
|
13723
14527
|
summary
|
|
13724
14528
|
};
|
|
13725
14529
|
}
|
|
13726
|
-
buildMarkdownSummary(bugs, plans, evals, overallVerdict) {
|
|
14530
|
+
buildMarkdownSummary(bugs, plans, evals, overallVerdict, disposition) {
|
|
13727
14531
|
const lines = [
|
|
13728
14532
|
`## Collaborative Debugging Report \u2014 ${this.sessionId}`,
|
|
13729
14533
|
"",
|
|
13730
14534
|
`**Target:** ${this.options.targetPaths.join(", ")}`,
|
|
14535
|
+
`**Disposition:** ${disposition.toUpperCase()}`,
|
|
13731
14536
|
`**Overall Verdict:** **${overallVerdict.toUpperCase()}**`,
|
|
13732
14537
|
""
|
|
13733
14538
|
];
|
|
14539
|
+
if (this.alerts.length > 0) {
|
|
14540
|
+
lines.push("### Alerts", "");
|
|
14541
|
+
for (const alert of this.alerts) {
|
|
14542
|
+
lines.push(`- **[${alert.level.toUpperCase()}]** ${alert.role}: ${alert.message}`);
|
|
14543
|
+
}
|
|
14544
|
+
lines.push("");
|
|
14545
|
+
}
|
|
13734
14546
|
if (bugs.length > 0) {
|
|
13735
14547
|
lines.push("### Bugs Found", "");
|
|
13736
14548
|
for (const b of bugs) {
|
|
13737
|
-
lines.push(
|
|
13738
|
-
`- **[${b.severity.toUpperCase()}]** \`${b.location.file}:${b.location.line}\` \u2014 ${b.description}`
|
|
13739
|
-
);
|
|
14549
|
+
lines.push(`- **[${b.severity.toUpperCase()}]** \`${b.location.file}:${b.location.line}\` \u2014 ${b.description}`);
|
|
13740
14550
|
}
|
|
13741
14551
|
lines.push("");
|
|
13742
14552
|
}
|
|
@@ -13755,9 +14565,7 @@ ${scratchpad}/critic-report-${this.sessionId}.md`;
|
|
|
13755
14565
|
for (const e of evals) {
|
|
13756
14566
|
lines.push(`- [${e.subjectType}] score=${e.score}/10 \u2014 **${e.verdict.toUpperCase()}**`);
|
|
13757
14567
|
for (const c of e.concerns) {
|
|
13758
|
-
if (c.severity === "blocking") {
|
|
13759
|
-
lines.push(` - ${c.description}`);
|
|
13760
|
-
}
|
|
14568
|
+
if (c.severity === "blocking") lines.push(` - ${c.description}`);
|
|
13761
14569
|
}
|
|
13762
14570
|
}
|
|
13763
14571
|
lines.push("");
|
|
@@ -13775,10 +14583,9 @@ var FleetSpawnBudgetError = class extends Error {
|
|
|
13775
14583
|
kind;
|
|
13776
14584
|
limit;
|
|
13777
14585
|
observed;
|
|
13778
|
-
constructor(kind, limit, observed) {
|
|
13779
|
-
|
|
13780
|
-
|
|
13781
|
-
);
|
|
14586
|
+
constructor(kind, limit, observed, message) {
|
|
14587
|
+
const defaultMsg = kind === "max_spawns" ? `Director spawn budget exceeded: tried to spawn #${observed} but maxSpawns is ${limit}` : `Director spawn depth budget exceeded: this director is at depth ${observed} and maxSpawnDepth is ${limit}`;
|
|
14588
|
+
super(message ?? defaultMsg);
|
|
13782
14589
|
this.name = "FleetSpawnBudgetError";
|
|
13783
14590
|
this.kind = kind;
|
|
13784
14591
|
this.limit = limit;
|
|
@@ -13799,6 +14606,20 @@ var FleetCostCapError = class extends Error {
|
|
|
13799
14606
|
this.observed = observed;
|
|
13800
14607
|
}
|
|
13801
14608
|
};
|
|
14609
|
+
var FleetContextOverflowError = class extends Error {
|
|
14610
|
+
kind;
|
|
14611
|
+
limit;
|
|
14612
|
+
observed;
|
|
14613
|
+
constructor(limit, observed) {
|
|
14614
|
+
super(
|
|
14615
|
+
`Leader context overflow: leader has ${observed} tokens in flight (limit: ${limit}). Compact the leader context before spawning more subagents.`
|
|
14616
|
+
);
|
|
14617
|
+
this.name = "FleetContextOverflowError";
|
|
14618
|
+
this.kind = "max_context_load";
|
|
14619
|
+
this.limit = limit;
|
|
14620
|
+
this.observed = observed;
|
|
14621
|
+
}
|
|
14622
|
+
};
|
|
13802
14623
|
var Director = class {
|
|
13803
14624
|
/** Alias for the ICoordinator contract. `id` is retained for backward compatibility. */
|
|
13804
14625
|
get coordinatorId() {
|
|
@@ -13815,6 +14636,25 @@ var Director = class {
|
|
|
13815
14636
|
* injected; otherwise own FleetUsageAggregator.
|
|
13816
14637
|
*/
|
|
13817
14638
|
usage;
|
|
14639
|
+
/**
|
|
14640
|
+
* Update the leader agent's current context pressure (full request tokens:
|
|
14641
|
+
* messages + systemPrompt + toolDefs). The director checks this before every
|
|
14642
|
+
* spawn — if the pressure exceeds `maxLeaderContextLoad * maxContext`,
|
|
14643
|
+
* spawning is refused with a `FleetContextOverflowError`.
|
|
14644
|
+
*
|
|
14645
|
+
* Call this after each leader agent iteration to keep the pressure current.
|
|
14646
|
+
* The compactor's `CompactReport.fullRequestTokensAfter` is a natural source.
|
|
14647
|
+
*/
|
|
14648
|
+
setLeaderContextPressure(tokens) {
|
|
14649
|
+
this.leaderContextPressure = tokens;
|
|
14650
|
+
this.fleetManager?.setLeaderContextPressure(tokens);
|
|
14651
|
+
}
|
|
14652
|
+
/**
|
|
14653
|
+
* Read the leader agent's current context pressure.
|
|
14654
|
+
*/
|
|
14655
|
+
getLeaderContextPressure() {
|
|
14656
|
+
return this.leaderContextPressure;
|
|
14657
|
+
}
|
|
13818
14658
|
/**
|
|
13819
14659
|
* Optional fleet-level policy container. When provided the Director
|
|
13820
14660
|
* delegates spawn budgeting, manifest entries, and checkpointing to it
|
|
@@ -13900,6 +14740,23 @@ var Director = class {
|
|
|
13900
14740
|
taskCompletedListener = null;
|
|
13901
14741
|
/** Optional LLM classifier for smart dispatch. Passed from options. */
|
|
13902
14742
|
dispatchClassifier;
|
|
14743
|
+
/** Leader agent's current context pressure (full request tokens). */
|
|
14744
|
+
leaderContextPressure = 0;
|
|
14745
|
+
/** Maximum context load fraction before spawn is refused. */
|
|
14746
|
+
maxLeaderContextLoad;
|
|
14747
|
+
/** Provider's max context window in tokens. */
|
|
14748
|
+
maxContext;
|
|
14749
|
+
/**
|
|
14750
|
+
* When set by `workComplete()`, the director stops dispatching new tasks
|
|
14751
|
+
* and terminates all running subagents. Used when the director's LLM decides
|
|
14752
|
+
* the goal is satisfied and no further spawns are needed — prevents the
|
|
14753
|
+
* coordinator from keeping workers alive for tasks that will never arrive.
|
|
14754
|
+
*/
|
|
14755
|
+
workCompleteFlag = false;
|
|
14756
|
+
/** Pending /btw notes stashed by the leader agent (see setLeaderBtwNote). */
|
|
14757
|
+
_leaderBtwNotes = [];
|
|
14758
|
+
/** Active collab sessions tracked by sessionId (see spawnCollab). */
|
|
14759
|
+
_activeCollabSessions = /* @__PURE__ */ new Map();
|
|
13903
14760
|
constructor(opts) {
|
|
13904
14761
|
this.id = opts.config.coordinatorId || randomUUID();
|
|
13905
14762
|
this.manifestPath = opts.manifestPath;
|
|
@@ -13915,6 +14772,8 @@ var Director = class {
|
|
|
13915
14772
|
this.dispatchClassifier = opts.dispatchClassifier;
|
|
13916
14773
|
this.maxFleetCostUsd = opts.directorBudget?.maxCostUsd ?? Number.POSITIVE_INFINITY;
|
|
13917
14774
|
this.maxBudgetExtensions = opts.maxBudgetExtensions ?? 5;
|
|
14775
|
+
this.maxLeaderContextLoad = opts.maxLeaderContextLoad ?? 0.85;
|
|
14776
|
+
this.maxContext = opts.maxContext ?? 128e3;
|
|
13918
14777
|
this.sessionsRoot = opts.sessionsRoot;
|
|
13919
14778
|
this.directorRunId = opts.directorRunId ?? this.id;
|
|
13920
14779
|
this.stateCheckpoint = opts.stateCheckpointPath ? new DirectorStateCheckpoint(opts.stateCheckpointPath, {
|
|
@@ -13942,7 +14801,7 @@ var Director = class {
|
|
|
13942
14801
|
this.fleet = new FleetBus();
|
|
13943
14802
|
this.usage = new FleetUsageAggregator(
|
|
13944
14803
|
this.fleet,
|
|
13945
|
-
(
|
|
14804
|
+
(_id, provider, model) => {
|
|
13946
14805
|
if (provider && model) return this.priceLookups.get(`${provider}/${model}`);
|
|
13947
14806
|
return void 0;
|
|
13948
14807
|
},
|
|
@@ -14004,6 +14863,9 @@ var Director = class {
|
|
|
14004
14863
|
});
|
|
14005
14864
|
this.fleet.filter("budget.threshold_reached", (e) => {
|
|
14006
14865
|
const payload = e.payload;
|
|
14866
|
+
if (e.subagentId.startsWith("bug-hunter-") || e.subagentId.startsWith("refactor-planner-") || e.subagentId.startsWith("critic-")) {
|
|
14867
|
+
return;
|
|
14868
|
+
}
|
|
14007
14869
|
if (payload.kind === "timeout") {
|
|
14008
14870
|
const progress = progressBySubagent.get(e.subagentId) ?? 0;
|
|
14009
14871
|
const lastProgress = lastTimeoutProgress.get(e.subagentId) ?? -1;
|
|
@@ -14082,6 +14944,107 @@ var Director = class {
|
|
|
14082
14944
|
extensionsFor(subagentId) {
|
|
14083
14945
|
return this.extendTotals.get(subagentId) ?? 0;
|
|
14084
14946
|
}
|
|
14947
|
+
/**
|
|
14948
|
+
* Signal that the director's work is done. Once called:
|
|
14949
|
+
* - `spawn()` throws `FleetSpawnBudgetError('max_spawns', …)` — no new
|
|
14950
|
+
* subagents can be created
|
|
14951
|
+
* - Running subagents are NOT forcibly stopped — they finish naturally,
|
|
14952
|
+
* but no new tasks are dispatched to them
|
|
14953
|
+
*
|
|
14954
|
+
* This lets the director LLM say "I'm satisfied with the results, stop
|
|
14955
|
+
* spawning and wind down" — without killing in-flight work mid-execution.
|
|
14956
|
+
* Call `terminateAll()` separately if you need immediate teardown.
|
|
14957
|
+
*
|
|
14958
|
+
* Idempotent — calling twice is a no-op.
|
|
14959
|
+
*/
|
|
14960
|
+
workComplete() {
|
|
14961
|
+
this.workCompleteFlag = true;
|
|
14962
|
+
this.fleet.emit({ subagentId: this.id, ts: Date.now(), type: "director.work_complete", payload: {} });
|
|
14963
|
+
}
|
|
14964
|
+
/** Returns true if `workComplete()` has been called on this director. */
|
|
14965
|
+
isWorkComplete() {
|
|
14966
|
+
return this.workCompleteFlag;
|
|
14967
|
+
}
|
|
14968
|
+
/**
|
|
14969
|
+
* Stashes a /btw note on the leader agent's context. The leader's agent loop
|
|
14970
|
+
* calls `consumeBtwNotes()` at each iteration boundary and folds pending notes
|
|
14971
|
+
* into the conversation as a visible block — no abort, no restart, just a
|
|
14972
|
+
* "by the way" nudge the model picks up on its next turn.
|
|
14973
|
+
*
|
|
14974
|
+
* This is the entry point for the host (CLI, TUI) to inject /btw notes
|
|
14975
|
+
* programmatically without going through the slash-command path.
|
|
14976
|
+
*/
|
|
14977
|
+
setLeaderBtwNote(note) {
|
|
14978
|
+
const trimmed = note.trim();
|
|
14979
|
+
if (!trimmed) return this._leaderBtwNotes.length;
|
|
14980
|
+
this._leaderBtwNotes = [...this._leaderBtwNotes, trimmed].slice(-20);
|
|
14981
|
+
return this._leaderBtwNotes.length;
|
|
14982
|
+
}
|
|
14983
|
+
/**
|
|
14984
|
+
* Read and clear all pending /btw notes the leader has stashed.
|
|
14985
|
+
* Returns them in FIFO order (empty array when none).
|
|
14986
|
+
*
|
|
14987
|
+
* Called by CollabSession when a budget threshold event fires so the
|
|
14988
|
+
* Director can inspect accumulated /btw notes before deciding whether
|
|
14989
|
+
* to cancel the collab session or let it continue.
|
|
14990
|
+
*/
|
|
14991
|
+
getLeaderBtwNotes() {
|
|
14992
|
+
const notes = this._leaderBtwNotes;
|
|
14993
|
+
this._leaderBtwNotes = [];
|
|
14994
|
+
return notes;
|
|
14995
|
+
}
|
|
14996
|
+
/**
|
|
14997
|
+
* Peek at pending /btw notes without consuming them.
|
|
14998
|
+
* Useful for UI to show "N pending notes" without clearing them.
|
|
14999
|
+
*/
|
|
15000
|
+
peekLeaderBtwNotes() {
|
|
15001
|
+
return [...this._leaderBtwNotes];
|
|
15002
|
+
}
|
|
15003
|
+
/**
|
|
15004
|
+
* Drain (read + clear) all /btw notes in one call.
|
|
15005
|
+
* Alias for getLeaderBtwNotes() — kept for symmetry with consumeBtwNotes()
|
|
15006
|
+
* in the agent's btw.ts. The Director calls this at the point where it
|
|
15007
|
+
* makes a cancellation decision, not on every budget event.
|
|
15008
|
+
*/
|
|
15009
|
+
drainLeaderBtwNotes() {
|
|
15010
|
+
return this.getLeaderBtwNotes();
|
|
15011
|
+
}
|
|
15012
|
+
/**
|
|
15013
|
+
* Cancel an active collab session by its id.
|
|
15014
|
+
* Emits `director.cancel_collab` on the FleetBus so the session's agents
|
|
15015
|
+
* finish early with a 'cancelled' disposition.
|
|
15016
|
+
*
|
|
15017
|
+
* Returns silently if the session id is not tracked or already settled.
|
|
15018
|
+
* The CollabDebugReport will reflect 'cancelled' disposition when awaited.
|
|
15019
|
+
*/
|
|
15020
|
+
cancelCollabSession(sessionId, reason = "Director cancelled") {
|
|
15021
|
+
const session = this._activeCollabSessions.get(sessionId);
|
|
15022
|
+
if (!session || session.isCancelled()) return;
|
|
15023
|
+
session.cancel(reason);
|
|
15024
|
+
for (const [_role, subagentId] of session.getSubagentIds()) {
|
|
15025
|
+
this.coordinator.stop(subagentId).catch(() => {
|
|
15026
|
+
});
|
|
15027
|
+
}
|
|
15028
|
+
}
|
|
15029
|
+
/**
|
|
15030
|
+
* Subscribe a callback to be notified whenever a collab session raises
|
|
15031
|
+
* an alert (warning level). The callback receives the full DirectorAlert
|
|
15032
|
+
* payload so the host (CLI, TUI) can display it to the user.
|
|
15033
|
+
* Returns an unsubscribe function.
|
|
15034
|
+
*/
|
|
15035
|
+
onCollabAlert(handler) {
|
|
15036
|
+
return this.fleet.filter("collab.warning", (e) => {
|
|
15037
|
+
handler(e.payload);
|
|
15038
|
+
});
|
|
15039
|
+
}
|
|
15040
|
+
/**
|
|
15041
|
+
* Returns all active (not yet settled) collab session ids.
|
|
15042
|
+
* Useful for the TUI to render a "N active sessions" badge and for
|
|
15043
|
+
* the host to know what can be cancelled.
|
|
15044
|
+
*/
|
|
15045
|
+
activeCollabSessions() {
|
|
15046
|
+
return Array.from(this._activeCollabSessions.keys());
|
|
15047
|
+
}
|
|
14085
15048
|
/** Best-effort session-writer append. Swallows failures — the director
|
|
14086
15049
|
* must not break a fleet run because the session JSONL handle closed. */
|
|
14087
15050
|
async appendSessionEvent(event) {
|
|
@@ -14120,12 +15083,21 @@ var Director = class {
|
|
|
14120
15083
|
* it the `cost` column in `usage.snapshot()` stays at 0.
|
|
14121
15084
|
*/
|
|
14122
15085
|
async spawn(config, priceLookup) {
|
|
15086
|
+
if (this.workCompleteFlag) {
|
|
15087
|
+
throw new FleetSpawnBudgetError(
|
|
15088
|
+
"max_spawns",
|
|
15089
|
+
this.maxSpawns,
|
|
15090
|
+
this.spawnCount + 1,
|
|
15091
|
+
"workComplete() has been called \u2014 director closed further spawning"
|
|
15092
|
+
);
|
|
15093
|
+
}
|
|
14123
15094
|
if (this.fleetManager) {
|
|
14124
15095
|
const rejection = this.fleetManager.canSpawn(config);
|
|
14125
15096
|
if (rejection) {
|
|
14126
15097
|
if (rejection.kind === "max_spawn_depth") throw new FleetSpawnBudgetError("max_spawn_depth", rejection.limit, rejection.observed);
|
|
14127
15098
|
if (rejection.kind === "max_spawns") throw new FleetSpawnBudgetError("max_spawns", rejection.limit, rejection.observed);
|
|
14128
15099
|
if (rejection.kind === "max_cost_usd") throw new FleetCostCapError(rejection.limit, rejection.observed);
|
|
15100
|
+
if (rejection.kind === "max_context_load") throw new FleetContextOverflowError(rejection.limit, rejection.observed);
|
|
14129
15101
|
}
|
|
14130
15102
|
} else {
|
|
14131
15103
|
if (this.spawnDepth >= this.maxSpawnDepth) {
|
|
@@ -14140,9 +15112,15 @@ var Director = class {
|
|
|
14140
15112
|
throw new FleetCostCapError(this.maxFleetCostUsd, totalCost);
|
|
14141
15113
|
}
|
|
14142
15114
|
}
|
|
15115
|
+
if (this.maxLeaderContextLoad < 1) {
|
|
15116
|
+
const threshold = this.maxContext * this.maxLeaderContextLoad;
|
|
15117
|
+
if (this.leaderContextPressure >= threshold) {
|
|
15118
|
+
throw new FleetContextOverflowError(threshold, this.leaderContextPressure);
|
|
15119
|
+
}
|
|
15120
|
+
}
|
|
14143
15121
|
}
|
|
14144
15122
|
let result;
|
|
14145
|
-
const needsNickname = config.name === config.role || !config.name || config.name === "subagent";
|
|
15123
|
+
const needsNickname = config.name === config.role || !config.name || config.name === "subagent" || config.name === "adhoc";
|
|
14146
15124
|
if (needsNickname) {
|
|
14147
15125
|
const role = config.role ?? "subagent";
|
|
14148
15126
|
if (this.fleetManager) {
|
|
@@ -14367,6 +15345,24 @@ var Director = class {
|
|
|
14367
15345
|
*/
|
|
14368
15346
|
async assign(task) {
|
|
14369
15347
|
const taskWithId = task.id ? task : { ...task, id: randomUUID() };
|
|
15348
|
+
if (this.workCompleteFlag) {
|
|
15349
|
+
const synthetic = {
|
|
15350
|
+
subagentId: taskWithId.subagentId ?? "unassigned",
|
|
15351
|
+
taskId: taskWithId.id,
|
|
15352
|
+
status: "stopped",
|
|
15353
|
+
error: { kind: "aborted_by_parent", message: "Director called workComplete() \u2014 no further tasks will run", retryable: false },
|
|
15354
|
+
iterations: 0,
|
|
15355
|
+
toolCalls: 0,
|
|
15356
|
+
durationMs: 0
|
|
15357
|
+
};
|
|
15358
|
+
this.completed.set(taskWithId.id, synthetic);
|
|
15359
|
+
const waiter = this.taskWaiters.get(taskWithId.id);
|
|
15360
|
+
if (waiter) {
|
|
15361
|
+
waiter.resolve(synthetic);
|
|
15362
|
+
this.taskWaiters.delete(taskWithId.id);
|
|
15363
|
+
}
|
|
15364
|
+
return taskWithId.id;
|
|
15365
|
+
}
|
|
14370
15366
|
if (task.subagentId) {
|
|
14371
15367
|
if (this.fleetManager) {
|
|
14372
15368
|
this.fleetManager.addTaskToSubagent(task.subagentId, taskWithId.id);
|
|
@@ -14589,12 +15585,14 @@ var Director = class {
|
|
|
14589
15585
|
makeAskTool(this),
|
|
14590
15586
|
makeRollUpTool(this),
|
|
14591
15587
|
makeTerminateTool(this),
|
|
15588
|
+
makeTerminateAllTool(this),
|
|
14592
15589
|
makeFleetStatusTool(this),
|
|
14593
15590
|
makeFleetUsageTool(this),
|
|
14594
15591
|
makeFleetSessionTool(this),
|
|
14595
15592
|
makeFleetHealthTool(this),
|
|
14596
15593
|
makeCollabDebugTool(this),
|
|
14597
|
-
makeFleetEmitTool(this)
|
|
15594
|
+
makeFleetEmitTool(this),
|
|
15595
|
+
makeWorkCompleteTool(this)
|
|
14598
15596
|
];
|
|
14599
15597
|
return t2;
|
|
14600
15598
|
}
|
|
@@ -14614,7 +15612,15 @@ var Director = class {
|
|
|
14614
15612
|
* three agents complete or the session times out.
|
|
14615
15613
|
*/
|
|
14616
15614
|
async spawnCollab(options) {
|
|
14617
|
-
const session = new CollabSession(this, this.fleet,
|
|
15615
|
+
const session = new CollabSession(this, this.fleet, {
|
|
15616
|
+
...options,
|
|
15617
|
+
onBudgetWarning: (alert) => {
|
|
15618
|
+
return options.onBudgetWarning?.(alert) ?? "ignore";
|
|
15619
|
+
}
|
|
15620
|
+
});
|
|
15621
|
+
this._activeCollabSessions.set(session.sessionId, session);
|
|
15622
|
+
session.on("session.done", () => this._activeCollabSessions.delete(session.sessionId));
|
|
15623
|
+
session.on("session.error", () => this._activeCollabSessions.delete(session.sessionId));
|
|
14618
15624
|
return session.start();
|
|
14619
15625
|
}
|
|
14620
15626
|
/**
|
|
@@ -15104,8 +16110,8 @@ async function loadProjectModes(modesDir) {
|
|
|
15104
16110
|
for (const entry of entries) {
|
|
15105
16111
|
if (!entry.endsWith(".md") && !entry.endsWith(".txt")) continue;
|
|
15106
16112
|
const filePath = path6.join(modesDir, entry);
|
|
15107
|
-
const
|
|
15108
|
-
if (!
|
|
16113
|
+
const stat8 = await fsp2.stat(filePath);
|
|
16114
|
+
if (!stat8.isFile()) continue;
|
|
15109
16115
|
const content = await fsp2.readFile(filePath, "utf8");
|
|
15110
16116
|
const id = path6.basename(entry, path6.extname(entry));
|
|
15111
16117
|
modes.push({
|
|
@@ -16379,9 +17385,9 @@ var AISpecBuilder = class {
|
|
|
16379
17385
|
if (!this.sessionPath) return;
|
|
16380
17386
|
try {
|
|
16381
17387
|
const fsp19 = await import('fs/promises');
|
|
16382
|
-
const
|
|
17388
|
+
const path35 = await import('path');
|
|
16383
17389
|
const { atomicWrite: atomicWrite2 } = await Promise.resolve().then(() => (init_atomic_write(), atomic_write_exports));
|
|
16384
|
-
await fsp19.mkdir(
|
|
17390
|
+
await fsp19.mkdir(path35.dirname(this.sessionPath), { recursive: true });
|
|
16385
17391
|
await atomicWrite2(this.sessionPath, JSON.stringify(this.session, null, 2));
|
|
16386
17392
|
} catch {
|
|
16387
17393
|
}
|
|
@@ -17033,7 +18039,7 @@ function analyzeCriticalPath(graph) {
|
|
|
17033
18039
|
blockedTasks
|
|
17034
18040
|
};
|
|
17035
18041
|
}
|
|
17036
|
-
function getTransitiveBlocked(
|
|
18042
|
+
function getTransitiveBlocked(_graph, taskId, blocksMap) {
|
|
17037
18043
|
const visited = /* @__PURE__ */ new Set();
|
|
17038
18044
|
const queue = [taskId];
|
|
17039
18045
|
while (queue.length > 0) {
|
|
@@ -17049,7 +18055,7 @@ function getTransitiveBlocked(graph, taskId, blocksMap) {
|
|
|
17049
18055
|
}
|
|
17050
18056
|
return visited;
|
|
17051
18057
|
}
|
|
17052
|
-
function computeCriticalPath(graph,
|
|
18058
|
+
function computeCriticalPath(graph, _topoOrder, blockedByMap) {
|
|
17053
18059
|
const allIds = Array.from(graph.nodes.keys());
|
|
17054
18060
|
if (allIds.length === 0) return [];
|
|
17055
18061
|
const dist = /* @__PURE__ */ new Map();
|
|
@@ -17091,15 +18097,15 @@ function computeCriticalPath(graph, topoOrder, blockedByMap) {
|
|
|
17091
18097
|
maxId = id;
|
|
17092
18098
|
}
|
|
17093
18099
|
}
|
|
17094
|
-
const
|
|
18100
|
+
const path35 = [];
|
|
17095
18101
|
let current = maxId;
|
|
17096
18102
|
const visited = /* @__PURE__ */ new Set();
|
|
17097
18103
|
while (current && !visited.has(current)) {
|
|
17098
18104
|
visited.add(current);
|
|
17099
|
-
|
|
18105
|
+
path35.unshift(current);
|
|
17100
18106
|
current = prev.get(current) ?? null;
|
|
17101
18107
|
}
|
|
17102
|
-
return
|
|
18108
|
+
return path35;
|
|
17103
18109
|
}
|
|
17104
18110
|
function computeParallelGroups(graph, blockedByMap) {
|
|
17105
18111
|
const groups = [];
|
|
@@ -17467,13 +18473,11 @@ function createAutoExecutor(opts) {
|
|
|
17467
18473
|
|
|
17468
18474
|
// src/sdd/sdd-task-decomposer.ts
|
|
17469
18475
|
var SddTaskDecomposer = class {
|
|
17470
|
-
constructor(tracker,
|
|
18476
|
+
constructor(tracker, _graph, opts = {}) {
|
|
17471
18477
|
this.tracker = tracker;
|
|
17472
|
-
this.graph = graph;
|
|
17473
18478
|
this.slots = Math.min(16, Math.max(1, opts.parallelSlots ?? 4));
|
|
17474
18479
|
}
|
|
17475
18480
|
tracker;
|
|
17476
|
-
graph;
|
|
17477
18481
|
slots;
|
|
17478
18482
|
wave = 0;
|
|
17479
18483
|
// -------------------------------------------------------------------
|
|
@@ -17626,7 +18630,7 @@ var SddParallelRun = class {
|
|
|
17626
18630
|
this.coordinator.setRunner?.(runner);
|
|
17627
18631
|
}
|
|
17628
18632
|
defaultFactory() {
|
|
17629
|
-
return async (
|
|
18633
|
+
return async (_config) => ({
|
|
17630
18634
|
agent: this.opts.agent,
|
|
17631
18635
|
events: this.opts.agent.events
|
|
17632
18636
|
});
|
|
@@ -18079,7 +19083,7 @@ async function startMetricsServer(opts) {
|
|
|
18079
19083
|
const tls = opts.tls;
|
|
18080
19084
|
const useHttps = !!(tls?.cert && tls?.key);
|
|
18081
19085
|
const host = opts.host ?? "127.0.0.1";
|
|
18082
|
-
const
|
|
19086
|
+
const path35 = opts.path ?? "/metrics";
|
|
18083
19087
|
const healthPath = opts.healthPath ?? "/healthz";
|
|
18084
19088
|
const healthRegistry = opts.healthRegistry;
|
|
18085
19089
|
const listener = (req, res) => {
|
|
@@ -18089,7 +19093,7 @@ async function startMetricsServer(opts) {
|
|
|
18089
19093
|
return;
|
|
18090
19094
|
}
|
|
18091
19095
|
const url = req.url.split("?")[0];
|
|
18092
|
-
if (url ===
|
|
19096
|
+
if (url === path35) {
|
|
18093
19097
|
let body;
|
|
18094
19098
|
try {
|
|
18095
19099
|
body = renderPrometheus(opts.sink.snapshot());
|
|
@@ -18153,7 +19157,7 @@ async function startMetricsServer(opts) {
|
|
|
18153
19157
|
const protocol = useHttps ? "https" : "http";
|
|
18154
19158
|
return {
|
|
18155
19159
|
port: boundPort,
|
|
18156
|
-
url: `${protocol}://${host}:${boundPort}${
|
|
19160
|
+
url: `${protocol}://${host}:${boundPort}${path35}`,
|
|
18157
19161
|
close: () => new Promise((resolve9, reject) => {
|
|
18158
19162
|
server.close((err) => err ? reject(err) : resolve9());
|
|
18159
19163
|
})
|
|
@@ -18440,6 +19444,12 @@ function roughEstimate(messages) {
|
|
|
18440
19444
|
return total;
|
|
18441
19445
|
}
|
|
18442
19446
|
function createContextManagerTool(opts = {}) {
|
|
19447
|
+
const minCompactThreshold = opts.minCompactThreshold ?? 0;
|
|
19448
|
+
const noopRetryDeltaTokens = opts.noopRetryDeltaTokens ?? 2e3;
|
|
19449
|
+
const maxContext = opts.maxContext ?? 128e3;
|
|
19450
|
+
const compactThresholdFraction = opts.compactThresholdFraction ?? 0.5;
|
|
19451
|
+
const effectiveThreshold = minCompactThreshold > 0 ? minCompactThreshold : Math.floor(maxContext * compactThresholdFraction);
|
|
19452
|
+
let lastNoopTokens = 0;
|
|
18443
19453
|
return {
|
|
18444
19454
|
name: CONTEXT_MANAGER_TOOL_NAME,
|
|
18445
19455
|
description: 'Inspect or reorganize the conversation context window. Use "check" to see token budget. Use "summary" to collapse a message range into a concise note (provide "text" for custom summary). Use "prune" to remove specific messages by index. Use "add_note" to inject a summary note. Use "compact" to run aggressive compaction. Use "repair" to remove orphan tool_use/tool_result blocks after manual context surgery.',
|
|
@@ -18530,11 +19540,41 @@ function createContextManagerTool(opts = {}) {
|
|
|
18530
19540
|
notes: "No compactor registered. Use /compact aggressive via slash command instead."
|
|
18531
19541
|
};
|
|
18532
19542
|
}
|
|
19543
|
+
const fullEstimate = input.systemPrompt != null && Array.isArray(input.tools) ? estimateRequestTokens(messages, input.systemPrompt, input.tools) : { total: beforeTokens};
|
|
19544
|
+
const currentTokens = fullEstimate.total;
|
|
19545
|
+
if (lastNoopTokens > 0) {
|
|
19546
|
+
const delta = currentTokens - lastNoopTokens;
|
|
19547
|
+
if (delta < noopRetryDeltaTokens) {
|
|
19548
|
+
return {
|
|
19549
|
+
action: "compact",
|
|
19550
|
+
beforeTokens,
|
|
19551
|
+
afterTokens: beforeTokens,
|
|
19552
|
+
messageCount: messages.length,
|
|
19553
|
+
notes: `Compact is a NOOP retry: context grew only ${delta} tokens since the last no-op attempt (threshold: ${noopRetryDeltaTokens}). Skip until more content accumulates.`
|
|
19554
|
+
};
|
|
19555
|
+
}
|
|
19556
|
+
}
|
|
19557
|
+
if (currentTokens < effectiveThreshold) {
|
|
19558
|
+
return {
|
|
19559
|
+
action: "compact",
|
|
19560
|
+
beforeTokens,
|
|
19561
|
+
afterTokens: beforeTokens,
|
|
19562
|
+
messageCount: messages.length,
|
|
19563
|
+
notes: `Context tokens (${currentTokens}) below compact threshold (${effectiveThreshold}). Skipping.`
|
|
19564
|
+
};
|
|
19565
|
+
}
|
|
18533
19566
|
const report = await opts.compactor.compact(ctx);
|
|
18534
19567
|
ctx.clearFileTracking();
|
|
18535
19568
|
const repair = applyMessages([...ctx.messages]);
|
|
18536
19569
|
const afterTokens = repair.changed ? roughEstimate(ctx.messages) : report.after;
|
|
18537
19570
|
const repaired = report.repaired ?? (repair.changed ? repair : void 0);
|
|
19571
|
+
const reduced = report.fullRequestTokensBefore > report.fullRequestTokensAfter;
|
|
19572
|
+
const repairedSomething = !!report.repaired;
|
|
19573
|
+
if (reduced || repairedSomething) {
|
|
19574
|
+
lastNoopTokens = 0;
|
|
19575
|
+
} else {
|
|
19576
|
+
lastNoopTokens = currentTokens;
|
|
19577
|
+
}
|
|
18538
19578
|
return {
|
|
18539
19579
|
action: "compact",
|
|
18540
19580
|
beforeTokens,
|
|
@@ -18775,16 +19815,16 @@ var allServers = () => ({
|
|
|
18775
19815
|
function parseSkillRef(input) {
|
|
18776
19816
|
const trimmed = input.trim().replace(/^https?:\/\/github\.com\//, "").replace(/\.git$/, "");
|
|
18777
19817
|
const atIdx = trimmed.indexOf("@");
|
|
18778
|
-
let
|
|
19818
|
+
let refPath2;
|
|
18779
19819
|
let ref;
|
|
18780
19820
|
if (atIdx > 0) {
|
|
18781
|
-
|
|
19821
|
+
refPath2 = trimmed.slice(0, atIdx);
|
|
18782
19822
|
ref = trimmed.slice(atIdx + 1);
|
|
18783
19823
|
} else {
|
|
18784
|
-
|
|
19824
|
+
refPath2 = trimmed;
|
|
18785
19825
|
ref = "main";
|
|
18786
19826
|
}
|
|
18787
|
-
const parts =
|
|
19827
|
+
const parts = refPath2.split("/").filter(Boolean);
|
|
18788
19828
|
if (parts.length < 2) {
|
|
18789
19829
|
throw new Error(`Invalid skill reference "${input}". Expected format: user/repo or user/repo@ref`);
|
|
18790
19830
|
}
|
|
@@ -19000,10 +20040,10 @@ var SkillInstaller = class {
|
|
|
19000
20040
|
if (!resolved.startsWith(path6.resolve(destDir))) {
|
|
19001
20041
|
throw new Error(`Path traversal detected in skill file: ${file}`);
|
|
19002
20042
|
}
|
|
19003
|
-
const
|
|
19004
|
-
if (
|
|
20043
|
+
const stat8 = await fsp2.stat(srcPath);
|
|
20044
|
+
if (stat8.size > MAX_SKILL_FILE_SIZE) {
|
|
19005
20045
|
throw new Error(
|
|
19006
|
-
`Skill file "${file}" is too large (${(
|
|
20046
|
+
`Skill file "${file}" is too large (${(stat8.size / 1024).toFixed(1)}KB). Max: ${MAX_SKILL_FILE_SIZE / 1024}KB`
|
|
19007
20047
|
);
|
|
19008
20048
|
}
|
|
19009
20049
|
await fsp2.mkdir(path6.dirname(destPath), { recursive: true });
|
|
@@ -19042,7 +20082,7 @@ var SkillInstaller = class {
|
|
|
19042
20082
|
* - Name: update that specific skill
|
|
19043
20083
|
* - Name + newRef: update to a different ref
|
|
19044
20084
|
*/
|
|
19045
|
-
async update(nameOrRef,
|
|
20085
|
+
async update(nameOrRef, _opts) {
|
|
19046
20086
|
const result = { updated: [], unchanged: [], errors: [] };
|
|
19047
20087
|
const allEntries = await this.manifest.listAll();
|
|
19048
20088
|
let targets;
|
|
@@ -19121,131 +20161,747 @@ var SkillInstaller = class {
|
|
|
19121
20161
|
if (!entry) {
|
|
19122
20162
|
throw new Error(`Skill "${name}" is not installed${scope === "user" ? " (global)" : ""}.`);
|
|
19123
20163
|
}
|
|
19124
|
-
await this.removeSkillFiles(name, scope);
|
|
19125
|
-
await this.manifest.removeEntry(name, scope);
|
|
19126
|
-
this.invalidateLoaderCache();
|
|
20164
|
+
await this.removeSkillFiles(name, scope);
|
|
20165
|
+
await this.manifest.removeEntry(name, scope);
|
|
20166
|
+
this.invalidateLoaderCache();
|
|
20167
|
+
}
|
|
20168
|
+
/**
|
|
20169
|
+
* List all installed skills from the manifest.
|
|
20170
|
+
*/
|
|
20171
|
+
async listInstalled() {
|
|
20172
|
+
return this.manifest.listAll();
|
|
20173
|
+
}
|
|
20174
|
+
// ── Private helpers ──────────────────────────────────────────────
|
|
20175
|
+
/**
|
|
20176
|
+
* Detect skills in an extracted repository.
|
|
20177
|
+
* Returns an array of detected skills with their files.
|
|
20178
|
+
*/
|
|
20179
|
+
async detectSkills(baseDir) {
|
|
20180
|
+
const results = [];
|
|
20181
|
+
const rootSkillMd = path6.join(baseDir, "SKILL.md");
|
|
20182
|
+
try {
|
|
20183
|
+
await fsp2.access(rootSkillMd);
|
|
20184
|
+
const content = await fsp2.readFile(rootSkillMd, "utf8");
|
|
20185
|
+
const meta = parseFrontmatter2(content);
|
|
20186
|
+
if (meta.name && meta.description) {
|
|
20187
|
+
results.push({
|
|
20188
|
+
name: meta.name,
|
|
20189
|
+
baseDir,
|
|
20190
|
+
files: ["SKILL.md"]
|
|
20191
|
+
});
|
|
20192
|
+
return results;
|
|
20193
|
+
}
|
|
20194
|
+
} catch {
|
|
20195
|
+
}
|
|
20196
|
+
const skillsDir = path6.join(baseDir, "skills");
|
|
20197
|
+
try {
|
|
20198
|
+
const entries = await fsp2.readdir(skillsDir, { withFileTypes: true });
|
|
20199
|
+
for (const entry of entries) {
|
|
20200
|
+
if (!entry.isDirectory()) continue;
|
|
20201
|
+
const skillFile = path6.join(skillsDir, entry.name, "SKILL.md");
|
|
20202
|
+
try {
|
|
20203
|
+
const content = await fsp2.readFile(skillFile, "utf8");
|
|
20204
|
+
const meta = parseFrontmatter2(content);
|
|
20205
|
+
if (meta.name && meta.description) {
|
|
20206
|
+
const skillDir = path6.join(skillsDir, entry.name);
|
|
20207
|
+
const files = await collectFiles(skillDir, skillDir);
|
|
20208
|
+
results.push({
|
|
20209
|
+
name: meta.name,
|
|
20210
|
+
baseDir: skillDir,
|
|
20211
|
+
files
|
|
20212
|
+
});
|
|
20213
|
+
}
|
|
20214
|
+
} catch {
|
|
20215
|
+
}
|
|
20216
|
+
}
|
|
20217
|
+
} catch {
|
|
20218
|
+
}
|
|
20219
|
+
return results;
|
|
20220
|
+
}
|
|
20221
|
+
/**
|
|
20222
|
+
* Remove all files for an installed skill.
|
|
20223
|
+
*/
|
|
20224
|
+
async removeSkillFiles(name, scope) {
|
|
20225
|
+
const targetDir = scope === "project" ? this.opts.projectSkillsDir : this.opts.globalSkillsDir;
|
|
20226
|
+
const skillDir = path6.join(targetDir, name);
|
|
20227
|
+
await fsp2.rm(skillDir, { recursive: true, force: true });
|
|
20228
|
+
}
|
|
20229
|
+
/**
|
|
20230
|
+
* Invalidate the skill loader's cache so newly installed skills appear.
|
|
20231
|
+
*/
|
|
20232
|
+
invalidateLoaderCache() {
|
|
20233
|
+
const loader = this.opts.skillLoader;
|
|
20234
|
+
if (loader && typeof loader.invalidateCache === "function") {
|
|
20235
|
+
loader.invalidateCache();
|
|
20236
|
+
}
|
|
20237
|
+
}
|
|
20238
|
+
};
|
|
20239
|
+
function parseFrontmatter2(raw) {
|
|
20240
|
+
if (!raw.startsWith("---")) return {};
|
|
20241
|
+
const end = raw.indexOf("\n---", 4);
|
|
20242
|
+
if (end === -1) return {};
|
|
20243
|
+
const block = raw.slice(4, end);
|
|
20244
|
+
const out = {};
|
|
20245
|
+
let key = null;
|
|
20246
|
+
let value = [];
|
|
20247
|
+
const flush = () => {
|
|
20248
|
+
if (key) {
|
|
20249
|
+
out[key] = value.join("\n").trim();
|
|
20250
|
+
}
|
|
20251
|
+
key = null;
|
|
20252
|
+
value = [];
|
|
20253
|
+
};
|
|
20254
|
+
for (const line of block.split("\n")) {
|
|
20255
|
+
const m = /^([a-zA-Z_]+):\s*(\|?)\s*(.*)$/.exec(line);
|
|
20256
|
+
if (m) {
|
|
20257
|
+
flush();
|
|
20258
|
+
key = m[1] ?? "";
|
|
20259
|
+
const pipe = m[2];
|
|
20260
|
+
const rest = m[3] ?? "";
|
|
20261
|
+
if (pipe === "|") {
|
|
20262
|
+
value = [];
|
|
20263
|
+
} else if (rest) {
|
|
20264
|
+
value = [rest];
|
|
20265
|
+
} else {
|
|
20266
|
+
value = [];
|
|
20267
|
+
}
|
|
20268
|
+
} else if (key) {
|
|
20269
|
+
value.push(line.replace(/^\s+/, ""));
|
|
20270
|
+
}
|
|
20271
|
+
}
|
|
20272
|
+
flush();
|
|
20273
|
+
return out;
|
|
20274
|
+
}
|
|
20275
|
+
async function collectFiles(dir, baseDir) {
|
|
20276
|
+
const results = [];
|
|
20277
|
+
const entries = await fsp2.readdir(dir, { withFileTypes: true });
|
|
20278
|
+
for (const entry of entries) {
|
|
20279
|
+
const fullPath = path6.join(dir, entry.name);
|
|
20280
|
+
const relPath = path6.relative(baseDir, fullPath);
|
|
20281
|
+
if (entry.isDirectory()) {
|
|
20282
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
20283
|
+
results.push(...await collectFiles(fullPath, baseDir));
|
|
20284
|
+
} else {
|
|
20285
|
+
results.push(relPath);
|
|
20286
|
+
}
|
|
20287
|
+
}
|
|
20288
|
+
return results;
|
|
20289
|
+
}
|
|
20290
|
+
|
|
20291
|
+
// src/storage/annotations-store.ts
|
|
20292
|
+
init_atomic_write();
|
|
20293
|
+
var FILE_VERSION = 1;
|
|
20294
|
+
var MAX_TEXT_LENGTH = 2e3;
|
|
20295
|
+
var MAX_ANNOTATIONS = 1e3;
|
|
20296
|
+
var AnnotationsStore = class {
|
|
20297
|
+
dir;
|
|
20298
|
+
/** Per-session write queue. Created lazily on first add. */
|
|
20299
|
+
writeChains = /* @__PURE__ */ new Map();
|
|
20300
|
+
constructor(opts) {
|
|
20301
|
+
this.dir = opts.dir;
|
|
20302
|
+
}
|
|
20303
|
+
// ── Reads ──────────────────────────────────────────────────────────────
|
|
20304
|
+
/**
|
|
20305
|
+
* Return all annotations for `sessionId` in insertion order
|
|
20306
|
+
* (oldest first). Returns an empty array when no file exists
|
|
20307
|
+
* yet (the normal case for a fresh session).
|
|
20308
|
+
*/
|
|
20309
|
+
async list(sessionId) {
|
|
20310
|
+
const file = await this.readFile(sessionId);
|
|
20311
|
+
return file ? file.annotations : [];
|
|
20312
|
+
}
|
|
20313
|
+
/**
|
|
20314
|
+
* Convenience: only unresolved annotations, newest first — the
|
|
20315
|
+
* common UI rendering for "what still needs attention?".
|
|
20316
|
+
*/
|
|
20317
|
+
async listOpen(sessionId) {
|
|
20318
|
+
const all = await this.list(sessionId);
|
|
20319
|
+
return all.filter((a) => !a.resolved).reverse();
|
|
20320
|
+
}
|
|
20321
|
+
// ── Writes ─────────────────────────────────────────────────────────────
|
|
20322
|
+
/**
|
|
20323
|
+
* Add a new annotation. Returns the persisted record (with id
|
|
20324
|
+
* and timestamps filled in). Throws when `text` is empty or
|
|
20325
|
+
* exceeds `MAX_TEXT_LENGTH`.
|
|
20326
|
+
*/
|
|
20327
|
+
async add(input) {
|
|
20328
|
+
const text = input.text.trim();
|
|
20329
|
+
if (text.length === 0) {
|
|
20330
|
+
throw new Error("Annotation text must be non-empty");
|
|
20331
|
+
}
|
|
20332
|
+
if (text.length > MAX_TEXT_LENGTH) {
|
|
20333
|
+
throw new Error(
|
|
20334
|
+
`Annotation text exceeds ${MAX_TEXT_LENGTH} chars (got ${text.length})`
|
|
20335
|
+
);
|
|
20336
|
+
}
|
|
20337
|
+
if (!Number.isInteger(input.atEventIndex) || input.atEventIndex < 0) {
|
|
20338
|
+
throw new Error("atEventIndex must be a non-negative integer");
|
|
20339
|
+
}
|
|
20340
|
+
const annotation = {
|
|
20341
|
+
id: randomUUID(),
|
|
20342
|
+
sessionId: input.sessionId,
|
|
20343
|
+
atEventIndex: input.atEventIndex,
|
|
20344
|
+
authorId: input.authorId,
|
|
20345
|
+
authorRole: "annotator",
|
|
20346
|
+
text,
|
|
20347
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
20348
|
+
resolved: false
|
|
20349
|
+
};
|
|
20350
|
+
await this.enqueue(input.sessionId, async () => {
|
|
20351
|
+
const all = await this.list(input.sessionId);
|
|
20352
|
+
all.push(annotation);
|
|
20353
|
+
if (all.length > MAX_ANNOTATIONS) {
|
|
20354
|
+
const sorted = all.map((a, i) => ({ a, i })).sort((x, y) => {
|
|
20355
|
+
if (x.a.resolved !== y.a.resolved) return x.a.resolved ? 1 : -1;
|
|
20356
|
+
return x.a.createdAt.localeCompare(y.a.createdAt);
|
|
20357
|
+
});
|
|
20358
|
+
const evictCount = all.length - MAX_ANNOTATIONS;
|
|
20359
|
+
const toEvict = new Set(sorted.slice(0, evictCount).map((s) => s.a.id));
|
|
20360
|
+
const kept = all.filter((a) => !toEvict.has(a.id));
|
|
20361
|
+
await this.writeFile(input.sessionId, { version: FILE_VERSION, annotations: kept });
|
|
20362
|
+
} else {
|
|
20363
|
+
await this.writeFile(input.sessionId, { version: FILE_VERSION, annotations: all });
|
|
20364
|
+
}
|
|
20365
|
+
});
|
|
20366
|
+
return annotation;
|
|
20367
|
+
}
|
|
20368
|
+
/**
|
|
20369
|
+
* Mark an annotation as resolved. Returns the updated record, or
|
|
20370
|
+
* `null` if no annotation with that id exists in this session.
|
|
20371
|
+
* Idempotent: resolving an already-resolved annotation refreshes
|
|
20372
|
+
* `resolvedAt` / `resolvedBy` to the latest call.
|
|
20373
|
+
*/
|
|
20374
|
+
async resolve(input) {
|
|
20375
|
+
let updated = null;
|
|
20376
|
+
await this.enqueue(input.sessionId, async () => {
|
|
20377
|
+
const all = await this.list(input.sessionId);
|
|
20378
|
+
const idx = all.findIndex((a) => a.id === input.annotationId);
|
|
20379
|
+
if (idx === -1) {
|
|
20380
|
+
updated = null;
|
|
20381
|
+
return;
|
|
20382
|
+
}
|
|
20383
|
+
const next = {
|
|
20384
|
+
...all[idx],
|
|
20385
|
+
resolved: true,
|
|
20386
|
+
resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
20387
|
+
resolvedBy: input.resolvedBy
|
|
20388
|
+
};
|
|
20389
|
+
all[idx] = next;
|
|
20390
|
+
await this.writeFile(input.sessionId, { version: FILE_VERSION, annotations: all });
|
|
20391
|
+
updated = next;
|
|
20392
|
+
});
|
|
20393
|
+
return updated;
|
|
20394
|
+
}
|
|
20395
|
+
// ── Internals ──────────────────────────────────────────────────────────
|
|
20396
|
+
filePath(sessionId) {
|
|
20397
|
+
if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
|
|
20398
|
+
throw new Error(`Invalid sessionId: ${sessionId}`);
|
|
20399
|
+
}
|
|
20400
|
+
return path6.join(this.dir, `${sessionId}.annotations.json`);
|
|
20401
|
+
}
|
|
20402
|
+
async readFile(sessionId) {
|
|
20403
|
+
const fp = this.filePath(sessionId);
|
|
20404
|
+
try {
|
|
20405
|
+
const raw = await fsp2.readFile(fp, "utf8");
|
|
20406
|
+
const parsed = JSON.parse(raw);
|
|
20407
|
+
if (parsed.version !== FILE_VERSION) {
|
|
20408
|
+
return { version: FILE_VERSION, annotations: [] };
|
|
20409
|
+
}
|
|
20410
|
+
return parsed;
|
|
20411
|
+
} catch (err) {
|
|
20412
|
+
if (err.code === "ENOENT") return null;
|
|
20413
|
+
return { version: FILE_VERSION, annotations: [] };
|
|
20414
|
+
}
|
|
20415
|
+
}
|
|
20416
|
+
async writeFile(sessionId, file) {
|
|
20417
|
+
const fp = this.filePath(sessionId);
|
|
20418
|
+
await atomicWrite(fp, JSON.stringify(file, null, 2));
|
|
20419
|
+
}
|
|
20420
|
+
/**
|
|
20421
|
+
* Serialize writes per-sessionId. We chain promises instead of
|
|
20422
|
+
* using a Mutex class so the contract is obvious from the
|
|
20423
|
+
* call-site: `enqueue(sid, fn)` runs `fn` after every prior
|
|
20424
|
+
* enqueue for `sid` has settled.
|
|
20425
|
+
*/
|
|
20426
|
+
enqueue(sessionId, fn) {
|
|
20427
|
+
const prev = this.writeChains.get(sessionId) ?? Promise.resolve();
|
|
20428
|
+
const next = prev.then(fn, fn);
|
|
20429
|
+
this.writeChains.set(
|
|
20430
|
+
sessionId,
|
|
20431
|
+
next.catch(() => void 0)
|
|
20432
|
+
);
|
|
20433
|
+
return next;
|
|
20434
|
+
}
|
|
20435
|
+
};
|
|
20436
|
+
|
|
20437
|
+
// src/storage/replay-log-store.ts
|
|
20438
|
+
init_atomic_write();
|
|
20439
|
+
function stableStringify(value) {
|
|
20440
|
+
return JSON.stringify(sortKeys(value));
|
|
20441
|
+
}
|
|
20442
|
+
function sortKeys(value) {
|
|
20443
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
20444
|
+
if (value && typeof value === "object") {
|
|
20445
|
+
const obj = value;
|
|
20446
|
+
const sorted = {};
|
|
20447
|
+
for (const key of Object.keys(obj).sort()) {
|
|
20448
|
+
sorted[key] = sortKeys(obj[key]);
|
|
20449
|
+
}
|
|
20450
|
+
return sorted;
|
|
20451
|
+
}
|
|
20452
|
+
return value;
|
|
20453
|
+
}
|
|
20454
|
+
function hashRequest(request) {
|
|
20455
|
+
const payload = {
|
|
20456
|
+
model: request.model,
|
|
20457
|
+
system: request.system,
|
|
20458
|
+
messages: request.messages,
|
|
20459
|
+
tools: request.tools,
|
|
20460
|
+
maxTokens: request.maxTokens,
|
|
20461
|
+
temperature: request.temperature,
|
|
20462
|
+
topP: request.topP,
|
|
20463
|
+
stopSequences: request.stopSequences,
|
|
20464
|
+
toolChoice: request.toolChoice
|
|
20465
|
+
};
|
|
20466
|
+
const json = stableStringify(payload);
|
|
20467
|
+
const digest = createHash("sha256").update(json, "utf8").digest("hex");
|
|
20468
|
+
return `sha256:${digest}`;
|
|
20469
|
+
}
|
|
20470
|
+
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
20471
|
+
var ReplayLogStore = class {
|
|
20472
|
+
dir;
|
|
20473
|
+
writeChains = /* @__PURE__ */ new Map();
|
|
20474
|
+
/** Per-session hash → entry index, kept in memory after the first load. */
|
|
20475
|
+
cache = /* @__PURE__ */ new Map();
|
|
20476
|
+
maxEntries;
|
|
20477
|
+
constructor(opts) {
|
|
20478
|
+
this.dir = opts.dir;
|
|
20479
|
+
this.maxEntries = opts.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
20480
|
+
}
|
|
20481
|
+
// ── Writes ──────────────────────────────────────────────────────────────
|
|
20482
|
+
/**
|
|
20483
|
+
* Record a request/response pair. Idempotent on hash: a second
|
|
20484
|
+
* `record` for the same hash is a no-op (the existing entry wins).
|
|
20485
|
+
* Returns the hash.
|
|
20486
|
+
*/
|
|
20487
|
+
async record(input) {
|
|
20488
|
+
const hash = hashRequest(input.request);
|
|
20489
|
+
await this.enqueue(input.sessionId, async () => {
|
|
20490
|
+
const cache = await this.ensureCache(input.sessionId);
|
|
20491
|
+
if (cache.has(hash)) return;
|
|
20492
|
+
const entry = {
|
|
20493
|
+
hash,
|
|
20494
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
20495
|
+
request: input.request,
|
|
20496
|
+
response: input.response
|
|
20497
|
+
};
|
|
20498
|
+
let all = [...cache.values(), entry];
|
|
20499
|
+
if (all.length > this.maxEntries) {
|
|
20500
|
+
const evictCount = all.length - this.maxEntries;
|
|
20501
|
+
all = all.slice(evictCount);
|
|
20502
|
+
}
|
|
20503
|
+
await this.writeAll(input.sessionId, all);
|
|
20504
|
+
cache.clear();
|
|
20505
|
+
for (const e of all) cache.set(e.hash, e);
|
|
20506
|
+
});
|
|
20507
|
+
return hash;
|
|
20508
|
+
}
|
|
20509
|
+
// ── Reads ───────────────────────────────────────────────────────────────
|
|
20510
|
+
/**
|
|
20511
|
+
* Look up an entry by hash. Returns `null` when the request has
|
|
20512
|
+
* not been recorded for this session. O(1) after the first call
|
|
20513
|
+
* per session (in-memory cache).
|
|
20514
|
+
*/
|
|
20515
|
+
async lookup(sessionId, hash) {
|
|
20516
|
+
const cache = await this.ensureCache(sessionId);
|
|
20517
|
+
return cache.get(hash) ?? null;
|
|
20518
|
+
}
|
|
20519
|
+
/** All recorded entries for a session, in insertion order. */
|
|
20520
|
+
async load(sessionId) {
|
|
20521
|
+
const cache = await this.ensureCache(sessionId);
|
|
20522
|
+
return [...cache.values()];
|
|
20523
|
+
}
|
|
20524
|
+
/**
|
|
20525
|
+
* List every session id that has a replay log in the store dir.
|
|
20526
|
+
* Returns an array of `{ sessionId, entryCount, path }` sorted
|
|
20527
|
+
* by sessionId for stable output. Used by `wstack replay --list`.
|
|
20528
|
+
*/
|
|
20529
|
+
async list() {
|
|
20530
|
+
let entries;
|
|
20531
|
+
try {
|
|
20532
|
+
entries = await fsp2.readdir(this.dir);
|
|
20533
|
+
} catch (err) {
|
|
20534
|
+
if (err.code === "ENOENT") return [];
|
|
20535
|
+
return [];
|
|
20536
|
+
}
|
|
20537
|
+
const out = [];
|
|
20538
|
+
for (const name of entries) {
|
|
20539
|
+
if (!name.endsWith(".replay.jsonl")) continue;
|
|
20540
|
+
const sessionId = name.slice(0, -".replay.jsonl".length);
|
|
20541
|
+
const all = await this.load(sessionId);
|
|
20542
|
+
out.push({
|
|
20543
|
+
sessionId,
|
|
20544
|
+
entryCount: all.length,
|
|
20545
|
+
path: path6.join(this.dir, name)
|
|
20546
|
+
});
|
|
20547
|
+
}
|
|
20548
|
+
return out.sort((a, b) => a.sessionId.localeCompare(b.sessionId));
|
|
20549
|
+
}
|
|
20550
|
+
// ── Internals ───────────────────────────────────────────────────────────
|
|
20551
|
+
filePath(sessionId) {
|
|
20552
|
+
if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
|
|
20553
|
+
throw new Error(`Invalid sessionId: ${sessionId}`);
|
|
20554
|
+
}
|
|
20555
|
+
return path6.join(this.dir, `${sessionId}.replay.jsonl`);
|
|
20556
|
+
}
|
|
20557
|
+
async readAll(sessionId) {
|
|
20558
|
+
const fp = this.filePath(sessionId);
|
|
20559
|
+
try {
|
|
20560
|
+
const raw = await fsp2.readFile(fp, "utf8");
|
|
20561
|
+
const out = [];
|
|
20562
|
+
for (const line of raw.split("\n")) {
|
|
20563
|
+
if (!line.trim()) continue;
|
|
20564
|
+
try {
|
|
20565
|
+
const parsed = JSON.parse(line);
|
|
20566
|
+
if ("entry" in parsed && parsed.entry) {
|
|
20567
|
+
out.push(parsed.entry);
|
|
20568
|
+
} else {
|
|
20569
|
+
out.push(parsed);
|
|
20570
|
+
}
|
|
20571
|
+
} catch {
|
|
20572
|
+
}
|
|
20573
|
+
}
|
|
20574
|
+
return out;
|
|
20575
|
+
} catch (err) {
|
|
20576
|
+
if (err.code === "ENOENT") return [];
|
|
20577
|
+
return [];
|
|
20578
|
+
}
|
|
20579
|
+
}
|
|
20580
|
+
async writeAll(sessionId, entries) {
|
|
20581
|
+
const fp = this.filePath(sessionId);
|
|
20582
|
+
const body = entries.map((e) => JSON.stringify(e)).join("\n") + (entries.length ? "\n" : "");
|
|
20583
|
+
await atomicWrite(fp, body);
|
|
20584
|
+
}
|
|
20585
|
+
async ensureCache(sessionId) {
|
|
20586
|
+
let cache = this.cache.get(sessionId);
|
|
20587
|
+
if (cache) return cache;
|
|
20588
|
+
const all = await this.readAll(sessionId);
|
|
20589
|
+
cache = /* @__PURE__ */ new Map();
|
|
20590
|
+
for (const e of all) cache.set(e.hash, e);
|
|
20591
|
+
this.cache.set(sessionId, cache);
|
|
20592
|
+
return cache;
|
|
20593
|
+
}
|
|
20594
|
+
enqueue(sessionId, fn) {
|
|
20595
|
+
const prev = this.writeChains.get(sessionId) ?? Promise.resolve();
|
|
20596
|
+
const next = prev.then(fn, fn);
|
|
20597
|
+
this.writeChains.set(
|
|
20598
|
+
sessionId,
|
|
20599
|
+
next.catch(() => void 0)
|
|
20600
|
+
);
|
|
20601
|
+
return next;
|
|
20602
|
+
}
|
|
20603
|
+
};
|
|
20604
|
+
var SessionRecovery = class {
|
|
20605
|
+
constructor(dir) {
|
|
20606
|
+
this.dir = dir;
|
|
20607
|
+
}
|
|
20608
|
+
dir;
|
|
20609
|
+
/**
|
|
20610
|
+
* Scan a session log and return a `StaleSession` if and only
|
|
20611
|
+
* if the last event is an `in_flight_start` without a matching
|
|
20612
|
+
* `in_flight_end`. Returns `null` when:
|
|
20613
|
+
* - the log does not exist;
|
|
20614
|
+
* - the log is empty;
|
|
20615
|
+
* - the last event is `in_flight_end` (clean shutdown);
|
|
20616
|
+
* - the last event is something else (e.g. an unannotated
|
|
20617
|
+
* legacy log without in-flight markers).
|
|
20618
|
+
*/
|
|
20619
|
+
async detectStale(sessionId) {
|
|
20620
|
+
const fp = this.filePath(sessionId);
|
|
20621
|
+
let raw;
|
|
20622
|
+
try {
|
|
20623
|
+
raw = await fsp2.readFile(fp, "utf8");
|
|
20624
|
+
} catch (err) {
|
|
20625
|
+
if (err.code === "ENOENT") return null;
|
|
20626
|
+
return null;
|
|
20627
|
+
}
|
|
20628
|
+
return this.parseForStale(sessionId, fp, raw);
|
|
20629
|
+
}
|
|
20630
|
+
/**
|
|
20631
|
+
* Generate a recovery plan for a session. The plan describes
|
|
20632
|
+
* "what would be re-executed" if the user chose to resume —
|
|
20633
|
+
* everything after the last `checkpoint` event, plus the
|
|
20634
|
+
* dangling in-flight marker if present.
|
|
20635
|
+
*
|
|
20636
|
+
* Returns a non-null plan for ANY session that has at least
|
|
20637
|
+
* one event after a checkpoint (or, for legacy sessions, at
|
|
20638
|
+
* least one event). Pure read; no mutation.
|
|
20639
|
+
*/
|
|
20640
|
+
async recover(sessionId) {
|
|
20641
|
+
const fp = this.filePath(sessionId);
|
|
20642
|
+
let raw;
|
|
20643
|
+
try {
|
|
20644
|
+
raw = await fsp2.readFile(fp, "utf8");
|
|
20645
|
+
} catch (err) {
|
|
20646
|
+
if (err.code === "ENOENT") return null;
|
|
20647
|
+
return null;
|
|
20648
|
+
}
|
|
20649
|
+
const events = [];
|
|
20650
|
+
for (const line of raw.split("\n")) {
|
|
20651
|
+
if (!line.trim()) continue;
|
|
20652
|
+
try {
|
|
20653
|
+
events.push(JSON.parse(line));
|
|
20654
|
+
} catch {
|
|
20655
|
+
}
|
|
20656
|
+
}
|
|
20657
|
+
if (events.length === 0) return null;
|
|
20658
|
+
let lastCheckpoint = null;
|
|
20659
|
+
let lastCheckpointIdx = -1;
|
|
20660
|
+
for (let i = 0; i < events.length; i++) {
|
|
20661
|
+
if (events[i].type === "checkpoint") {
|
|
20662
|
+
lastCheckpoint = events[i];
|
|
20663
|
+
lastCheckpointIdx = i;
|
|
20664
|
+
}
|
|
20665
|
+
}
|
|
20666
|
+
const pendingEvents = lastCheckpointIdx >= 0 ? events.slice(lastCheckpointIdx + 1) : events;
|
|
20667
|
+
const lastEv = events[events.length - 1];
|
|
20668
|
+
const inFlightStart = lastEv.type === "in_flight_start" ? lastEv : null;
|
|
20669
|
+
const context = inFlightStart && inFlightStart.type === "in_flight_start" ? inFlightStart.context : null;
|
|
20670
|
+
return {
|
|
20671
|
+
sessionId,
|
|
20672
|
+
stale: inFlightStart !== null,
|
|
20673
|
+
lastCheckpoint,
|
|
20674
|
+
pendingEvents,
|
|
20675
|
+
inFlightStart,
|
|
20676
|
+
context
|
|
20677
|
+
};
|
|
20678
|
+
}
|
|
20679
|
+
/**
|
|
20680
|
+
* List every stale session in a directory. Returns an array
|
|
20681
|
+
* (possibly empty) sorted by `lastEventTs` descending — most
|
|
20682
|
+
* recent crash first.
|
|
20683
|
+
*/
|
|
20684
|
+
async listResumable() {
|
|
20685
|
+
let entries;
|
|
20686
|
+
try {
|
|
20687
|
+
entries = await fsp2.readdir(this.dir);
|
|
20688
|
+
} catch (err) {
|
|
20689
|
+
if (err.code === "ENOENT") return [];
|
|
20690
|
+
return [];
|
|
20691
|
+
}
|
|
20692
|
+
const out = [];
|
|
20693
|
+
for (const name of entries) {
|
|
20694
|
+
if (!name.endsWith(".jsonl")) continue;
|
|
20695
|
+
const sessionId = name.slice(0, -".jsonl".length);
|
|
20696
|
+
if (sessionId.includes(".replay") || sessionId.includes(".annotations")) continue;
|
|
20697
|
+
const stale = await this.detectStale(sessionId);
|
|
20698
|
+
if (stale) out.push(stale);
|
|
20699
|
+
}
|
|
20700
|
+
return out.sort((a, b) => b.lastEventTs.localeCompare(a.lastEventTs));
|
|
20701
|
+
}
|
|
20702
|
+
// ── Internals ──────────────────────────────────────────────────────────
|
|
20703
|
+
filePath(sessionId) {
|
|
20704
|
+
if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
|
|
20705
|
+
throw new Error(`Invalid sessionId: ${sessionId}`);
|
|
20706
|
+
}
|
|
20707
|
+
return path6.join(this.dir, `${sessionId}.jsonl`);
|
|
20708
|
+
}
|
|
20709
|
+
/**
|
|
20710
|
+
* Stream-parse the last few lines of a JSONL log. We do NOT load
|
|
20711
|
+
* the whole file into memory — for long-running sessions the log
|
|
20712
|
+
* can be megabytes. Instead we read tail-ward and find the last
|
|
20713
|
+
* `in_flight_start` / `in_flight_end` pair.
|
|
20714
|
+
*/
|
|
20715
|
+
async parseForStale(sessionId, fp, raw) {
|
|
20716
|
+
const lines = raw.split("\n");
|
|
20717
|
+
let lastEvent = null;
|
|
20718
|
+
let eventCount = 0;
|
|
20719
|
+
for (const line of lines) {
|
|
20720
|
+
if (!line.trim()) continue;
|
|
20721
|
+
try {
|
|
20722
|
+
const ev = JSON.parse(line);
|
|
20723
|
+
lastEvent = ev;
|
|
20724
|
+
eventCount++;
|
|
20725
|
+
} catch {
|
|
20726
|
+
}
|
|
20727
|
+
}
|
|
20728
|
+
if (!lastEvent) return null;
|
|
20729
|
+
if (lastEvent.type === "in_flight_start") {
|
|
20730
|
+
return {
|
|
20731
|
+
sessionId,
|
|
20732
|
+
path: fp,
|
|
20733
|
+
lastEventTs: lastEvent.ts,
|
|
20734
|
+
context: lastEvent.context,
|
|
20735
|
+
eventCount
|
|
20736
|
+
};
|
|
20737
|
+
}
|
|
20738
|
+
return null;
|
|
20739
|
+
}
|
|
20740
|
+
};
|
|
20741
|
+
|
|
20742
|
+
// src/storage/tool-audit-log.ts
|
|
20743
|
+
init_atomic_write();
|
|
20744
|
+
var GENESIS_PREV = "0".repeat(64);
|
|
20745
|
+
var ToolAuditLog = class {
|
|
20746
|
+
dir;
|
|
20747
|
+
/** In-memory cache of the last entry's hash (per session), to compute chains efficiently. */
|
|
20748
|
+
tailHash = /* @__PURE__ */ new Map();
|
|
20749
|
+
writeChains = /* @__PURE__ */ new Map();
|
|
20750
|
+
constructor(opts) {
|
|
20751
|
+
this.dir = opts.dir;
|
|
19127
20752
|
}
|
|
19128
20753
|
/**
|
|
19129
|
-
*
|
|
20754
|
+
* Append a tool call/result pair to the chain. Returns the
|
|
20755
|
+
* resulting entry. Idempotency is not guaranteed — if you
|
|
20756
|
+
* record the same tool_use twice you get two entries. That's
|
|
20757
|
+
* intentional: the audit log is a record, not a cache.
|
|
19130
20758
|
*/
|
|
19131
|
-
async
|
|
19132
|
-
|
|
20759
|
+
async record(input) {
|
|
20760
|
+
let entry = null;
|
|
20761
|
+
await this.enqueue(input.sessionId, async () => {
|
|
20762
|
+
const prevHash = this.tailHash.get(input.sessionId) ?? GENESIS_PREV;
|
|
20763
|
+
const id = randomUUID();
|
|
20764
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
20765
|
+
const content = {
|
|
20766
|
+
id,
|
|
20767
|
+
ts,
|
|
20768
|
+
prevHash,
|
|
20769
|
+
toolName: input.toolName,
|
|
20770
|
+
toolUseId: input.toolUseId,
|
|
20771
|
+
input: input.input,
|
|
20772
|
+
output: input.output,
|
|
20773
|
+
isError: input.isError
|
|
20774
|
+
};
|
|
20775
|
+
const hash = createHash("sha256").update(stableStringify2(content), "utf8").digest("hex");
|
|
20776
|
+
entry = {
|
|
20777
|
+
index: this.tailHash.has(input.sessionId) ? -1 : 0,
|
|
20778
|
+
// placeholder; recomputed below
|
|
20779
|
+
id,
|
|
20780
|
+
ts,
|
|
20781
|
+
prevHash,
|
|
20782
|
+
hash,
|
|
20783
|
+
toolName: input.toolName,
|
|
20784
|
+
toolUseId: input.toolUseId,
|
|
20785
|
+
input: input.input,
|
|
20786
|
+
output: input.output,
|
|
20787
|
+
isError: input.isError
|
|
20788
|
+
};
|
|
20789
|
+
const existing = await this.readAll(input.sessionId);
|
|
20790
|
+
const index = existing.length;
|
|
20791
|
+
const finalContent = { ...content, index };
|
|
20792
|
+
const finalHash = createHash("sha256").update(stableStringify2(finalContent), "utf8").digest("hex");
|
|
20793
|
+
entry = { ...entry, index, hash: finalHash };
|
|
20794
|
+
await this.appendLine(input.sessionId, entry);
|
|
20795
|
+
this.tailHash.set(input.sessionId, finalHash);
|
|
20796
|
+
});
|
|
20797
|
+
return entry;
|
|
19133
20798
|
}
|
|
19134
|
-
// ── Private helpers ──────────────────────────────────────────────
|
|
19135
20799
|
/**
|
|
19136
|
-
*
|
|
19137
|
-
* Returns
|
|
20800
|
+
* Walk the chain and verify every entry's hash and prevHash.
|
|
20801
|
+
* Returns a structured verdict — never throws.
|
|
19138
20802
|
*/
|
|
19139
|
-
async
|
|
19140
|
-
const
|
|
19141
|
-
|
|
19142
|
-
|
|
19143
|
-
|
|
19144
|
-
|
|
19145
|
-
|
|
19146
|
-
|
|
19147
|
-
|
|
19148
|
-
|
|
19149
|
-
|
|
19150
|
-
|
|
19151
|
-
|
|
19152
|
-
|
|
20803
|
+
async verify(sessionId) {
|
|
20804
|
+
const entries = await this.readAll(sessionId);
|
|
20805
|
+
if (entries.length === 0) return { ok: true, entries: 0 };
|
|
20806
|
+
if (entries[0].prevHash !== GENESIS_PREV) {
|
|
20807
|
+
return {
|
|
20808
|
+
ok: false,
|
|
20809
|
+
brokenAt: 0,
|
|
20810
|
+
reason: "first entry is not the genesis (prevHash != 0\u20260)"
|
|
20811
|
+
};
|
|
20812
|
+
}
|
|
20813
|
+
let prevHash = GENESIS_PREV;
|
|
20814
|
+
for (let i = 0; i < entries.length; i++) {
|
|
20815
|
+
const e = entries[i];
|
|
20816
|
+
if (e.prevHash !== prevHash) {
|
|
20817
|
+
return {
|
|
20818
|
+
ok: false,
|
|
20819
|
+
brokenAt: i,
|
|
20820
|
+
reason: `prevHash mismatch at entry ${i} (expected ${prevHash.slice(0, 8)}\u2026, got ${e.prevHash.slice(0, 8)}\u2026)`
|
|
20821
|
+
};
|
|
19153
20822
|
}
|
|
19154
|
-
|
|
20823
|
+
const content = {
|
|
20824
|
+
id: e.id,
|
|
20825
|
+
ts: e.ts,
|
|
20826
|
+
prevHash: e.prevHash,
|
|
20827
|
+
toolName: e.toolName,
|
|
20828
|
+
toolUseId: e.toolUseId,
|
|
20829
|
+
input: e.input,
|
|
20830
|
+
output: e.output,
|
|
20831
|
+
isError: e.isError,
|
|
20832
|
+
index: e.index
|
|
20833
|
+
};
|
|
20834
|
+
const expectedHash = createHash("sha256").update(stableStringify2(content), "utf8").digest("hex");
|
|
20835
|
+
if (expectedHash !== e.hash) {
|
|
20836
|
+
return {
|
|
20837
|
+
ok: false,
|
|
20838
|
+
brokenAt: i,
|
|
20839
|
+
reason: `hash mismatch at entry ${i} (entry content was modified)`
|
|
20840
|
+
};
|
|
20841
|
+
}
|
|
20842
|
+
prevHash = e.hash;
|
|
19155
20843
|
}
|
|
19156
|
-
|
|
20844
|
+
return { ok: true, entries: entries.length };
|
|
20845
|
+
}
|
|
20846
|
+
/** All entries for a session, in insertion order. */
|
|
20847
|
+
async load(sessionId) {
|
|
20848
|
+
return this.readAll(sessionId);
|
|
20849
|
+
}
|
|
20850
|
+
// ── Internals ────────────────────────────────────────────────────────────
|
|
20851
|
+
filePath(sessionId) {
|
|
20852
|
+
if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
|
|
20853
|
+
throw new Error(`Invalid sessionId: ${sessionId}`);
|
|
20854
|
+
}
|
|
20855
|
+
return path6.join(this.dir, `${sessionId}.audit.jsonl`);
|
|
20856
|
+
}
|
|
20857
|
+
async readAll(sessionId) {
|
|
20858
|
+
const fp = this.filePath(sessionId);
|
|
19157
20859
|
try {
|
|
19158
|
-
const
|
|
19159
|
-
|
|
19160
|
-
|
|
19161
|
-
|
|
20860
|
+
const raw = await fsp2.readFile(fp, "utf8");
|
|
20861
|
+
const out = [];
|
|
20862
|
+
for (const line of raw.split("\n")) {
|
|
20863
|
+
if (!line.trim()) continue;
|
|
19162
20864
|
try {
|
|
19163
|
-
|
|
19164
|
-
const meta = parseFrontmatter2(content);
|
|
19165
|
-
if (meta.name && meta.description) {
|
|
19166
|
-
const skillDir = path6.join(skillsDir, entry.name);
|
|
19167
|
-
const files = await collectFiles(skillDir, skillDir);
|
|
19168
|
-
results.push({
|
|
19169
|
-
name: meta.name,
|
|
19170
|
-
baseDir: skillDir,
|
|
19171
|
-
files
|
|
19172
|
-
});
|
|
19173
|
-
}
|
|
20865
|
+
out.push(JSON.parse(line));
|
|
19174
20866
|
} catch {
|
|
19175
20867
|
}
|
|
19176
20868
|
}
|
|
19177
|
-
|
|
20869
|
+
return out;
|
|
20870
|
+
} catch (err) {
|
|
20871
|
+
if (err.code === "ENOENT") return [];
|
|
20872
|
+
return [];
|
|
19178
20873
|
}
|
|
19179
|
-
return results;
|
|
19180
20874
|
}
|
|
19181
|
-
|
|
19182
|
-
|
|
19183
|
-
|
|
19184
|
-
|
|
19185
|
-
|
|
19186
|
-
const skillDir = path6.join(targetDir, name);
|
|
19187
|
-
await fsp2.rm(skillDir, { recursive: true, force: true });
|
|
20875
|
+
async appendLine(sessionId, entry) {
|
|
20876
|
+
const fp = this.filePath(sessionId);
|
|
20877
|
+
const existing = await this.readAll(sessionId);
|
|
20878
|
+
const all = [...existing, entry];
|
|
20879
|
+
await atomicWrite(fp, all.map((e) => JSON.stringify(e)).join("\n") + (all.length ? "\n" : ""));
|
|
19188
20880
|
}
|
|
19189
|
-
|
|
19190
|
-
|
|
19191
|
-
|
|
19192
|
-
|
|
19193
|
-
|
|
19194
|
-
|
|
19195
|
-
|
|
19196
|
-
|
|
20881
|
+
enqueue(sessionId, fn) {
|
|
20882
|
+
const prev = this.writeChains.get(sessionId) ?? Promise.resolve();
|
|
20883
|
+
const next = prev.then(fn, fn);
|
|
20884
|
+
this.writeChains.set(
|
|
20885
|
+
sessionId,
|
|
20886
|
+
next.catch(() => void 0)
|
|
20887
|
+
);
|
|
20888
|
+
return next;
|
|
19197
20889
|
}
|
|
19198
20890
|
};
|
|
19199
|
-
function
|
|
19200
|
-
|
|
19201
|
-
const end = raw.indexOf("\n---", 4);
|
|
19202
|
-
if (end === -1) return {};
|
|
19203
|
-
const block = raw.slice(4, end);
|
|
19204
|
-
const out = {};
|
|
19205
|
-
let key = null;
|
|
19206
|
-
let value = [];
|
|
19207
|
-
const flush = () => {
|
|
19208
|
-
if (key) {
|
|
19209
|
-
out[key] = value.join("\n").trim();
|
|
19210
|
-
}
|
|
19211
|
-
key = null;
|
|
19212
|
-
value = [];
|
|
19213
|
-
};
|
|
19214
|
-
for (const line of block.split("\n")) {
|
|
19215
|
-
const m = /^([a-zA-Z_]+):\s*(\|?)\s*(.*)$/.exec(line);
|
|
19216
|
-
if (m) {
|
|
19217
|
-
flush();
|
|
19218
|
-
key = m[1] ?? "";
|
|
19219
|
-
const pipe = m[2];
|
|
19220
|
-
const rest = m[3] ?? "";
|
|
19221
|
-
if (pipe === "|") {
|
|
19222
|
-
value = [];
|
|
19223
|
-
} else if (rest) {
|
|
19224
|
-
value = [rest];
|
|
19225
|
-
} else {
|
|
19226
|
-
value = [];
|
|
19227
|
-
}
|
|
19228
|
-
} else if (key) {
|
|
19229
|
-
value.push(line.replace(/^\s+/, ""));
|
|
19230
|
-
}
|
|
19231
|
-
}
|
|
19232
|
-
flush();
|
|
19233
|
-
return out;
|
|
20891
|
+
function stableStringify2(value) {
|
|
20892
|
+
return JSON.stringify(sortKeys2(value));
|
|
19234
20893
|
}
|
|
19235
|
-
|
|
19236
|
-
|
|
19237
|
-
|
|
19238
|
-
|
|
19239
|
-
const
|
|
19240
|
-
const
|
|
19241
|
-
|
|
19242
|
-
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
19243
|
-
results.push(...await collectFiles(fullPath, baseDir));
|
|
19244
|
-
} else {
|
|
19245
|
-
results.push(relPath);
|
|
20894
|
+
function sortKeys2(value) {
|
|
20895
|
+
if (Array.isArray(value)) return value.map(sortKeys2);
|
|
20896
|
+
if (value && typeof value === "object") {
|
|
20897
|
+
const obj = value;
|
|
20898
|
+
const sorted = {};
|
|
20899
|
+
for (const key of Object.keys(obj).sort()) {
|
|
20900
|
+
sorted[key] = sortKeys2(obj[key]);
|
|
19246
20901
|
}
|
|
20902
|
+
return sorted;
|
|
19247
20903
|
}
|
|
19248
|
-
return
|
|
20904
|
+
return value;
|
|
19249
20905
|
}
|
|
19250
20906
|
|
|
19251
20907
|
// src/storage/session-rewinder.ts
|
|
@@ -19421,7 +21077,7 @@ var DefaultPromptStore = class {
|
|
|
19421
21077
|
}
|
|
19422
21078
|
async list() {
|
|
19423
21079
|
await ensureDir(this.dir);
|
|
19424
|
-
|
|
21080
|
+
const entries = [];
|
|
19425
21081
|
try {
|
|
19426
21082
|
const files = await fsp2.readdir(this.dir);
|
|
19427
21083
|
for (const file of files) {
|
|
@@ -19697,8 +21353,8 @@ var CloudSync = class {
|
|
|
19697
21353
|
const localPath = this.categoryToPath(cat);
|
|
19698
21354
|
if (!localPath) continue;
|
|
19699
21355
|
try {
|
|
19700
|
-
const
|
|
19701
|
-
if (
|
|
21356
|
+
const stat8 = await fsp2.stat(localPath);
|
|
21357
|
+
if (stat8.isDirectory()) {
|
|
19702
21358
|
const files = await this.walkDir(localPath, localPath);
|
|
19703
21359
|
for (const file of files) {
|
|
19704
21360
|
const content = await fsp2.readFile(file, "utf8");
|
|
@@ -19723,8 +21379,8 @@ var CloudSync = class {
|
|
|
19723
21379
|
const localPath = this.categoryToPath(cat);
|
|
19724
21380
|
if (!localPath) continue;
|
|
19725
21381
|
try {
|
|
19726
|
-
const
|
|
19727
|
-
if (
|
|
21382
|
+
const stat8 = await fsp2.stat(localPath);
|
|
21383
|
+
if (stat8.isDirectory()) {
|
|
19728
21384
|
const files = await this.walkDir(localPath, localPath);
|
|
19729
21385
|
for (const file of files) {
|
|
19730
21386
|
const content = await fsp2.readFile(file);
|
|
@@ -20325,7 +21981,7 @@ var SkillGenerator = class {
|
|
|
20325
21981
|
};
|
|
20326
21982
|
return [commonInjection, ...stackSpecific[stack] ?? []];
|
|
20327
21983
|
}
|
|
20328
|
-
getConfigPatterns(
|
|
21984
|
+
getConfigPatterns(_stack) {
|
|
20329
21985
|
const commonConfig = [
|
|
20330
21986
|
{
|
|
20331
21987
|
id: "insecure-tls",
|
|
@@ -20907,7 +22563,6 @@ var SecurityScannerOrchestrator = class {
|
|
|
20907
22563
|
retryPolicy;
|
|
20908
22564
|
errorHandler;
|
|
20909
22565
|
detector = defaultTechStackDetector;
|
|
20910
|
-
reportGenerator = defaultReportGenerator;
|
|
20911
22566
|
gitignoreUpdater = defaultGitignoreUpdater;
|
|
20912
22567
|
/**
|
|
20913
22568
|
* Wraps provider.complete with retry logic using the injected RetryPolicy.
|
|
@@ -21091,7 +22746,7 @@ Return ONLY the JSON object, no markdown, no explanation.`;
|
|
|
21091
22746
|
/**
|
|
21092
22747
|
* Scan a batch of files using LLM.
|
|
21093
22748
|
*/
|
|
21094
|
-
async scanFileBatchLLM(provider, model, files, skill,
|
|
22749
|
+
async scanFileBatchLLM(provider, model, files, skill, _techStack) {
|
|
21095
22750
|
const fileContents = [];
|
|
21096
22751
|
for (const file of files) {
|
|
21097
22752
|
try {
|
|
@@ -21288,7 +22943,7 @@ Be specific about the vulnerabilities found and how to fix them.`;
|
|
|
21288
22943
|
/**
|
|
21289
22944
|
* Gather project info for skill generation.
|
|
21290
22945
|
*/
|
|
21291
|
-
async gatherProjectInfo(projectRoot,
|
|
22946
|
+
async gatherProjectInfo(projectRoot, _techStack) {
|
|
21292
22947
|
const info = [];
|
|
21293
22948
|
const keyFiles = [
|
|
21294
22949
|
"package.json",
|
|
@@ -21320,7 +22975,7 @@ ${dirs.join(", ")}`);
|
|
|
21320
22975
|
/**
|
|
21321
22976
|
* Gather files to scan based on patterns.
|
|
21322
22977
|
*/
|
|
21323
|
-
async gatherFiles(root,
|
|
22978
|
+
async gatherFiles(root, _patterns, depth) {
|
|
21324
22979
|
const files = [];
|
|
21325
22980
|
const maxDepth = depth === "quick" ? 2 : depth === "deep" ? 20 : 5;
|
|
21326
22981
|
const extensions = [".ts", ".js", ".jsx", ".tsx", ".py", ".go", ".java", ".cs", ".rs"];
|
|
@@ -21579,22 +23234,22 @@ Use \`/security report <number>\` to view a specific report.` };
|
|
|
21579
23234
|
}
|
|
21580
23235
|
const index = Number.parseInt(reportId, 10) - 1;
|
|
21581
23236
|
if (!Number.isNaN(index) && reports[index]) {
|
|
21582
|
-
const { readFile:
|
|
21583
|
-
const content = await
|
|
23237
|
+
const { readFile: readFile37 } = await import('fs/promises');
|
|
23238
|
+
const content = await readFile37(join(reportsDir, reports[index]), "utf-8");
|
|
21584
23239
|
return { message: `# Security Report
|
|
21585
23240
|
|
|
21586
23241
|
${content}` };
|
|
21587
23242
|
}
|
|
21588
23243
|
const match = reports.find((r) => r.includes(reportId));
|
|
21589
23244
|
if (match) {
|
|
21590
|
-
const { readFile:
|
|
21591
|
-
const content = await
|
|
23245
|
+
const { readFile: readFile37 } = await import('fs/promises');
|
|
23246
|
+
const content = await readFile37(join(reportsDir, match), "utf-8");
|
|
21592
23247
|
return { message: `# Security Report
|
|
21593
23248
|
|
|
21594
23249
|
${content}` };
|
|
21595
23250
|
}
|
|
21596
23251
|
return { message: `\u274C Report "${reportId}" not found. Use \`/security report\` to see available reports.` };
|
|
21597
|
-
} catch (
|
|
23252
|
+
} catch (_error) {
|
|
21598
23253
|
return { message: "\u{1F4ED} No security reports found. Run `/security scan` first." };
|
|
21599
23254
|
}
|
|
21600
23255
|
}
|
|
@@ -21647,7 +23302,6 @@ var FleetManager = class {
|
|
|
21647
23302
|
/** Usage rollup across all subagents. */
|
|
21648
23303
|
usage;
|
|
21649
23304
|
manifestPath;
|
|
21650
|
-
sessionsRoot;
|
|
21651
23305
|
directorRunId;
|
|
21652
23306
|
/** Spawn cap (lifetime total). Infinity means unlimited. */
|
|
21653
23307
|
maxSpawns;
|
|
@@ -21675,9 +23329,14 @@ var FleetManager = class {
|
|
|
21675
23329
|
_usedNicknames = /* @__PURE__ */ new Set();
|
|
21676
23330
|
/** The coordinator (wired via setCoordinator by Director after construction). */
|
|
21677
23331
|
coordinator = null;
|
|
23332
|
+
/** Leader agent's current context pressure (full request tokens). */
|
|
23333
|
+
leaderContextPressure = 0;
|
|
23334
|
+
/** Maximum context load fraction before spawn is refused. */
|
|
23335
|
+
maxLeaderContextLoad;
|
|
23336
|
+
/** Provider's max context window in tokens. */
|
|
23337
|
+
maxContext;
|
|
21678
23338
|
constructor(opts = {}) {
|
|
21679
23339
|
this.manifestPath = opts.manifestPath;
|
|
21680
|
-
this.sessionsRoot = opts.sessionsRoot;
|
|
21681
23340
|
this.directorRunId = opts.directorRunId ?? randomUUID();
|
|
21682
23341
|
this.maxSpawns = opts.maxSpawns ?? Number.POSITIVE_INFINITY;
|
|
21683
23342
|
this.maxSpawnDepth = opts.maxSpawnDepth ?? 2;
|
|
@@ -21685,6 +23344,8 @@ var FleetManager = class {
|
|
|
21685
23344
|
this.sessionWriter = opts.sessionWriter ?? null;
|
|
21686
23345
|
this.manifestDebounceMs = opts.manifestDebounceMs ?? 2e3;
|
|
21687
23346
|
this.maxFleetCostUsd = opts.directorBudget?.maxCostUsd ?? Number.POSITIVE_INFINITY;
|
|
23347
|
+
this.maxLeaderContextLoad = opts.maxLeaderContextLoad ?? 0.85;
|
|
23348
|
+
this.maxContext = opts.maxContext ?? 128e3;
|
|
21688
23349
|
this.stateCheckpoint = opts.stateCheckpointPath ? new DirectorStateCheckpoint(
|
|
21689
23350
|
opts.stateCheckpointPath,
|
|
21690
23351
|
{
|
|
@@ -21699,7 +23360,7 @@ var FleetManager = class {
|
|
|
21699
23360
|
this.fleet = new FleetBus();
|
|
21700
23361
|
this.usage = new FleetUsageAggregator(
|
|
21701
23362
|
this.fleet,
|
|
21702
|
-
(
|
|
23363
|
+
(_id, provider, model) => {
|
|
21703
23364
|
if (provider && model) return this.priceLookups.get(`${provider}/${model}`);
|
|
21704
23365
|
return void 0;
|
|
21705
23366
|
},
|
|
@@ -21738,7 +23399,7 @@ var FleetManager = class {
|
|
|
21738
23399
|
* which cap was exceeded. Does NOT throw — the caller decides
|
|
21739
23400
|
* how to surface the rejection.
|
|
21740
23401
|
*/
|
|
21741
|
-
canSpawn(
|
|
23402
|
+
canSpawn(_config) {
|
|
21742
23403
|
if (this.spawnDepth >= this.maxSpawnDepth) {
|
|
21743
23404
|
return { kind: "max_spawn_depth", limit: this.maxSpawnDepth, observed: this.spawnDepth };
|
|
21744
23405
|
}
|
|
@@ -21751,8 +23412,21 @@ var FleetManager = class {
|
|
|
21751
23412
|
return { kind: "max_cost_usd", limit: this.maxFleetCostUsd, observed: totalCost };
|
|
21752
23413
|
}
|
|
21753
23414
|
}
|
|
23415
|
+
if (this.maxLeaderContextLoad < 1) {
|
|
23416
|
+
const threshold = this.maxContext * this.maxLeaderContextLoad;
|
|
23417
|
+
if (this.leaderContextPressure >= threshold) {
|
|
23418
|
+
return {
|
|
23419
|
+
kind: "max_context_load",
|
|
23420
|
+
limit: threshold,
|
|
23421
|
+
observed: this.leaderContextPressure
|
|
23422
|
+
};
|
|
23423
|
+
}
|
|
23424
|
+
}
|
|
21754
23425
|
return null;
|
|
21755
23426
|
}
|
|
23427
|
+
setLeaderContextPressure(tokens) {
|
|
23428
|
+
this.leaderContextPressure = tokens;
|
|
23429
|
+
}
|
|
21756
23430
|
/**
|
|
21757
23431
|
* Assign a memorable nickname (e.g. "Einstein (Bug Hunter)") to the config,
|
|
21758
23432
|
* record it so the same name is never reused, then record the spawn.
|
|
@@ -21977,7 +23651,7 @@ function createMcpControlTool(opts) {
|
|
|
21977
23651
|
mutating: true,
|
|
21978
23652
|
riskTier: "standard",
|
|
21979
23653
|
inputSchema,
|
|
21980
|
-
async execute(raw
|
|
23654
|
+
async execute(raw) {
|
|
21981
23655
|
const input = raw;
|
|
21982
23656
|
return mcpControlDispatch(input, { getConfig, configPath, registry });
|
|
21983
23657
|
}
|
|
@@ -22525,7 +24199,6 @@ var Agent = class {
|
|
|
22525
24199
|
pipelines;
|
|
22526
24200
|
ctx;
|
|
22527
24201
|
maxIterations;
|
|
22528
|
-
iterationTimeoutMs;
|
|
22529
24202
|
executionStrategy;
|
|
22530
24203
|
perIterationOutputCapBytes;
|
|
22531
24204
|
plugins = [];
|
|
@@ -22543,7 +24216,6 @@ var Agent = class {
|
|
|
22543
24216
|
this.pipelines = init.pipelines;
|
|
22544
24217
|
this.ctx = init.context;
|
|
22545
24218
|
this.maxIterations = init.maxIterations ?? DEFAULT_MAX_ITERATIONS;
|
|
22546
|
-
this.iterationTimeoutMs = init.iterationTimeoutMs ?? 3e5;
|
|
22547
24219
|
this.executionStrategy = init.executionStrategy ?? "smart";
|
|
22548
24220
|
this.perIterationOutputCapBytes = init.perIterationOutputCapBytes ?? 1e5;
|
|
22549
24221
|
this.autoExtendLimit = init.autoExtendLimit ?? true;
|
|
@@ -22565,9 +24237,6 @@ var Agent = class {
|
|
|
22565
24237
|
get permission() {
|
|
22566
24238
|
return this.container.resolve(TOKENS.PermissionPolicy);
|
|
22567
24239
|
}
|
|
22568
|
-
get scrubber() {
|
|
22569
|
-
return this.container.resolve(TOKENS.SecretScrubber);
|
|
22570
|
-
}
|
|
22571
24240
|
get renderer() {
|
|
22572
24241
|
return this.container.has(TOKENS.Renderer) ? this.container.resolve(TOKENS.Renderer) : void 0;
|
|
22573
24242
|
}
|
|
@@ -22697,6 +24366,11 @@ var Agent = class {
|
|
|
22697
24366
|
if (controller.signal.aborted) {
|
|
22698
24367
|
return { status: "aborted", iterations };
|
|
22699
24368
|
}
|
|
24369
|
+
await this.ctx.session.writeInFlightMarker(`iteration ${i} / max ${this.maxIterations}`).catch((err) => {
|
|
24370
|
+
this.logger.debug?.(
|
|
24371
|
+
`in-flight marker write failed: ${err instanceof Error ? err.message : String(err)}`
|
|
24372
|
+
);
|
|
24373
|
+
});
|
|
22700
24374
|
if (autonomousContinue) {
|
|
22701
24375
|
consumeAutonomousContinue(this.ctx);
|
|
22702
24376
|
}
|
|
@@ -22715,6 +24389,15 @@ var Agent = class {
|
|
|
22715
24389
|
this.events.emit("iteration.started", { ctx: this.ctx, index: i });
|
|
22716
24390
|
this.injectPendingBtwNotes();
|
|
22717
24391
|
const req = await this.buildAndRunRequestPipeline(opts);
|
|
24392
|
+
await this.ctx.session.append({
|
|
24393
|
+
type: "llm_request",
|
|
24394
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
24395
|
+
model: req.model,
|
|
24396
|
+
messageCount: req.messages.length,
|
|
24397
|
+
estimatedInputTokens: estimateRequestTokens(req.messages, req.system, req.tools ?? []).total,
|
|
24398
|
+
toolCount: (req.tools ?? []).length
|
|
24399
|
+
}).catch(() => {
|
|
24400
|
+
});
|
|
22718
24401
|
let res;
|
|
22719
24402
|
try {
|
|
22720
24403
|
res = await customRunner(this.ctx, req);
|
|
@@ -22778,6 +24461,7 @@ var Agent = class {
|
|
|
22778
24461
|
finalText = responseResult.finalText;
|
|
22779
24462
|
const toolUses = res.content.filter(isToolUseBlock);
|
|
22780
24463
|
if (toolUses.length === 0) {
|
|
24464
|
+
this.emitContextPct();
|
|
22781
24465
|
this.events.emit("iteration.completed", { ctx: this.ctx, index: i });
|
|
22782
24466
|
if (autonomousContinue && responseResult.directive === "continue") {
|
|
22783
24467
|
await this.compactContextIfNeeded();
|
|
@@ -22791,11 +24475,13 @@ var Agent = class {
|
|
|
22791
24475
|
}
|
|
22792
24476
|
await this.executeTools(toolUses);
|
|
22793
24477
|
if (autonomousContinue && consumeAutonomousContinue(this.ctx)) {
|
|
24478
|
+
this.emitContextPct();
|
|
22794
24479
|
this.events.emit("iteration.completed", { ctx: this.ctx, index: i });
|
|
22795
24480
|
await this.compactContextIfNeeded();
|
|
22796
24481
|
await this.extensions.runAfterIteration(this.ctx, i);
|
|
22797
24482
|
continue;
|
|
22798
24483
|
}
|
|
24484
|
+
this.emitContextPct();
|
|
22799
24485
|
this.events.emit("iteration.completed", { ctx: this.ctx, index: i });
|
|
22800
24486
|
await this.compactContextIfNeeded();
|
|
22801
24487
|
await this.extensions.runAfterIteration(this.ctx, i);
|
|
@@ -22808,6 +24494,12 @@ var Agent = class {
|
|
|
22808
24494
|
}
|
|
22809
24495
|
} finally {
|
|
22810
24496
|
offSubagentDone();
|
|
24497
|
+
const reason = controller.signal.aborted ? "aborted" : "clean";
|
|
24498
|
+
await this.ctx.session.clearInFlightMarker(reason).catch((err) => {
|
|
24499
|
+
this.logger.debug?.(
|
|
24500
|
+
`in-flight marker clear failed: ${err instanceof Error ? err.message : String(err)}`
|
|
24501
|
+
);
|
|
24502
|
+
});
|
|
22811
24503
|
}
|
|
22812
24504
|
}
|
|
22813
24505
|
/**
|
|
@@ -22915,6 +24607,7 @@ var Agent = class {
|
|
|
22915
24607
|
this.executionStrategy
|
|
22916
24608
|
);
|
|
22917
24609
|
const useById = new Map(selectedToolUses.map((u) => [u.id, u]));
|
|
24610
|
+
const resultsForMessage = [];
|
|
22918
24611
|
for (const { result, tool, durationMs } of outputs) {
|
|
22919
24612
|
if (result.type === "tool_confirm_pending") {
|
|
22920
24613
|
const decision = await this.waitForConfirm({
|
|
@@ -22999,8 +24692,10 @@ var Agent = class {
|
|
|
22999
24692
|
});
|
|
23000
24693
|
}
|
|
23001
24694
|
}
|
|
24695
|
+
resultsForMessage.push(reRunResult.result);
|
|
23002
24696
|
continue;
|
|
23003
24697
|
}
|
|
24698
|
+
resultsForMessage.push(result);
|
|
23004
24699
|
const use = useById.get(result.tool_use_id);
|
|
23005
24700
|
if (!use) continue;
|
|
23006
24701
|
await this.pipelines.toolCall.run({
|
|
@@ -23033,7 +24728,7 @@ var Agent = class {
|
|
|
23033
24728
|
}
|
|
23034
24729
|
this.ctx.state.appendMessage({
|
|
23035
24730
|
role: "user",
|
|
23036
|
-
content:
|
|
24731
|
+
content: resultsForMessage
|
|
23037
24732
|
});
|
|
23038
24733
|
await this.extensions.runAfterToolExecution(this.ctx, outputs);
|
|
23039
24734
|
}
|
|
@@ -23100,6 +24795,19 @@ var Agent = class {
|
|
|
23100
24795
|
async compactContextIfNeeded() {
|
|
23101
24796
|
await this.pipelines.contextWindow.run(this.ctx);
|
|
23102
24797
|
}
|
|
24798
|
+
/**
|
|
24799
|
+
* Emit the current context window load as a `ctx.pct` event so subscribers
|
|
24800
|
+
* (FleetBus → TUI) can render a live fill bar per agent.
|
|
24801
|
+
*/
|
|
24802
|
+
emitContextPct() {
|
|
24803
|
+
const maxContext = this.ctx.provider.capabilities.maxContext ?? 2e5;
|
|
24804
|
+
const { total } = estimateRequestTokens(
|
|
24805
|
+
this.ctx.messages,
|
|
24806
|
+
this.ctx.systemPrompt,
|
|
24807
|
+
this.ctx.tools ?? []
|
|
24808
|
+
);
|
|
24809
|
+
this.events.emit("ctx.pct", { load: total / maxContext, tokens: total, maxContext });
|
|
24810
|
+
}
|
|
23103
24811
|
};
|
|
23104
24812
|
function toError(err) {
|
|
23105
24813
|
return err instanceof Error ? err : new Error(String(err));
|
|
@@ -23385,6 +25093,50 @@ var InputBuilder = class {
|
|
|
23385
25093
|
this.refs.push(ref);
|
|
23386
25094
|
return placeholder;
|
|
23387
25095
|
}
|
|
25096
|
+
/**
|
|
25097
|
+
* Register-only variant of `appendPaste`. Always stores the paste and
|
|
25098
|
+
* returns the inline `[pasted #N, L lines]` token WITHOUT mutating
|
|
25099
|
+
* `display` — the caller (TUI) owns its own editable buffer as the single
|
|
25100
|
+
* source of truth, inserts the token there, and expands the buffer at
|
|
25101
|
+
* submit. The collapse decision is the caller's (it gates this call); use
|
|
25102
|
+
* `wouldCollapse()` for that.
|
|
25103
|
+
*/
|
|
25104
|
+
async registerPaste(text) {
|
|
25105
|
+
const ref = await this.store.add({
|
|
25106
|
+
kind: "text",
|
|
25107
|
+
data: text,
|
|
25108
|
+
meta: { label: paragraphLabel(text) }
|
|
25109
|
+
});
|
|
25110
|
+
this.refs.push(ref);
|
|
25111
|
+
const lines = text.split("\n").length;
|
|
25112
|
+
return `[pasted #${ref.seq}, ${lines} lines]`;
|
|
25113
|
+
}
|
|
25114
|
+
/**
|
|
25115
|
+
* Register-only variant of `appendImage` — see `registerPaste`. Returns a
|
|
25116
|
+
* seq-keyed `[image #N, LABEL]` token; does not touch `display`.
|
|
25117
|
+
*/
|
|
25118
|
+
async registerImage(dataBase64, mediaType) {
|
|
25119
|
+
const label = `${mediaType.split("/")[1]?.toUpperCase() ?? "IMG"}`;
|
|
25120
|
+
const ref = await this.store.add({
|
|
25121
|
+
kind: "image",
|
|
25122
|
+
data: dataBase64,
|
|
25123
|
+
meta: { mediaType, label }
|
|
25124
|
+
});
|
|
25125
|
+
this.refs.push(ref);
|
|
25126
|
+
return `[image #${ref.seq}, ${label}]`;
|
|
25127
|
+
}
|
|
25128
|
+
/**
|
|
25129
|
+
* Register-only variant of `appendFile` — see `registerPaste`. Returns a
|
|
25130
|
+
* path-keyed `[file:<path>]` token (resolved by path at expand time); does
|
|
25131
|
+
* not touch `display`. The path is read from `meta.filename` (falling back
|
|
25132
|
+
* to `meta.label`).
|
|
25133
|
+
*/
|
|
25134
|
+
async registerFile(input) {
|
|
25135
|
+
const ref = await this.store.add({ ...input, kind: "file" });
|
|
25136
|
+
this.refs.push(ref);
|
|
25137
|
+
const path35 = ref.meta.filename ?? ref.meta.label ?? String(ref.seq);
|
|
25138
|
+
return `[file:${path35}]`;
|
|
25139
|
+
}
|
|
23388
25140
|
/**
|
|
23389
25141
|
* Whether `appendPaste(text)` would collapse the text to a placeholder
|
|
23390
25142
|
* (rather than inlining it). Lets a frontend decide where to route a paste
|
|
@@ -23776,8 +25528,8 @@ ${mem}`);
|
|
|
23776
25528
|
}
|
|
23777
25529
|
async dirExists(p) {
|
|
23778
25530
|
try {
|
|
23779
|
-
const
|
|
23780
|
-
return
|
|
25531
|
+
const stat8 = await fsp2.stat(p);
|
|
25532
|
+
return stat8.isDirectory();
|
|
23781
25533
|
} catch {
|
|
23782
25534
|
return false;
|
|
23783
25535
|
}
|
|
@@ -23867,6 +25619,14 @@ var ToolRegistry = class _ToolRegistry {
|
|
|
23867
25619
|
context: { tool: tool.name }
|
|
23868
25620
|
});
|
|
23869
25621
|
}
|
|
25622
|
+
if (!tool.inputSchema || typeof tool.inputSchema !== "object") {
|
|
25623
|
+
throw new WrongStackError({
|
|
25624
|
+
message: `Tool "${tool.name}" has an invalid or missing inputSchema`,
|
|
25625
|
+
code: ERROR_CODES.REGISTRY_INVALID,
|
|
25626
|
+
subsystem: "container",
|
|
25627
|
+
context: { tool: tool.name }
|
|
25628
|
+
});
|
|
25629
|
+
}
|
|
23870
25630
|
this.tools.set(tool.name, { tool, owner });
|
|
23871
25631
|
}
|
|
23872
25632
|
/**
|
|
@@ -23876,6 +25636,9 @@ var ToolRegistry = class _ToolRegistry {
|
|
|
23876
25636
|
*/
|
|
23877
25637
|
tryRegister(tool, owner = "core") {
|
|
23878
25638
|
if (this.tools.has(tool.name)) return false;
|
|
25639
|
+
if (!tool.inputSchema || typeof tool.inputSchema !== "object") {
|
|
25640
|
+
return false;
|
|
25641
|
+
}
|
|
23879
25642
|
this.tools.set(tool.name, { tool, owner });
|
|
23880
25643
|
return true;
|
|
23881
25644
|
}
|
|
@@ -26113,6 +27876,203 @@ function assertSafePath(dir, projectRoot) {
|
|
|
26113
27876
|
}
|
|
26114
27877
|
}
|
|
26115
27878
|
|
|
27879
|
+
// src/coordination/collab-bus.ts
|
|
27880
|
+
var CollaborationBus = class {
|
|
27881
|
+
pausePromise = null;
|
|
27882
|
+
pauseResolve = null;
|
|
27883
|
+
pausedAtMs = null;
|
|
27884
|
+
pausedBy = null;
|
|
27885
|
+
// ── State queries ──────────────────────────────────────────────────────
|
|
27886
|
+
isPaused() {
|
|
27887
|
+
return this.pausePromise !== null;
|
|
27888
|
+
}
|
|
27889
|
+
getState() {
|
|
27890
|
+
return {
|
|
27891
|
+
paused: this.isPaused(),
|
|
27892
|
+
pausedAt: this.pausedAtMs ? new Date(this.pausedAtMs).toISOString() : null,
|
|
27893
|
+
pausedBy: this.pausedBy
|
|
27894
|
+
};
|
|
27895
|
+
}
|
|
27896
|
+
// ── Pause / resume control ─────────────────────────────────────────────
|
|
27897
|
+
/**
|
|
27898
|
+
* Pause the agent loop. Idempotent: a second `requestPause` while
|
|
27899
|
+
* already paused is a no-op (the original pause wins; we do not
|
|
27900
|
+
* overwrite `pausedBy`). Returns true when the state actually
|
|
27901
|
+
* transitioned, false when it was already paused.
|
|
27902
|
+
*/
|
|
27903
|
+
requestPause(byParticipant) {
|
|
27904
|
+
if (this.isPaused()) return false;
|
|
27905
|
+
this.pausedAtMs = Date.now();
|
|
27906
|
+
this.pausedBy = byParticipant;
|
|
27907
|
+
this.pausePromise = new Promise((resolve9) => {
|
|
27908
|
+
this.pauseResolve = resolve9;
|
|
27909
|
+
});
|
|
27910
|
+
return true;
|
|
27911
|
+
}
|
|
27912
|
+
/**
|
|
27913
|
+
* Resume the agent loop. Returns true when the state actually
|
|
27914
|
+
* transitioned from paused → running, false when it was already
|
|
27915
|
+
* running (no-op).
|
|
27916
|
+
*/
|
|
27917
|
+
resume() {
|
|
27918
|
+
if (!this.isPaused()) return false;
|
|
27919
|
+
if (this.pauseResolve) this.pauseResolve();
|
|
27920
|
+
this.pausePromise = null;
|
|
27921
|
+
this.pauseResolve = null;
|
|
27922
|
+
this.pausedAtMs = null;
|
|
27923
|
+
this.pausedBy = null;
|
|
27924
|
+
return true;
|
|
27925
|
+
}
|
|
27926
|
+
// ── Wait semantics (consumed by the middleware) ────────────────────────
|
|
27927
|
+
/**
|
|
27928
|
+
* Block until the bus is resumed or the timeout fires. Returns:
|
|
27929
|
+
* - `true` → bus was resumed in time
|
|
27930
|
+
* - `false` → timeout fired; bus was auto-resumed as a side effect
|
|
27931
|
+
*
|
|
27932
|
+
* When `timeoutMs` is `0` the wait is unbounded (the middleware must
|
|
27933
|
+
* be paired with an external AbortSignal in that case — we don't
|
|
27934
|
+
* expose one here to keep the API simple).
|
|
27935
|
+
*/
|
|
27936
|
+
async waitForResume(timeoutMs) {
|
|
27937
|
+
if (!this.isPaused()) return true;
|
|
27938
|
+
if (!this.pausePromise) return true;
|
|
27939
|
+
if (timeoutMs === 0) {
|
|
27940
|
+
await this.pausePromise;
|
|
27941
|
+
return true;
|
|
27942
|
+
}
|
|
27943
|
+
let timer;
|
|
27944
|
+
const timeoutPromise = new Promise((resolve9) => {
|
|
27945
|
+
timer = setTimeout(() => resolve9("timeout"), timeoutMs);
|
|
27946
|
+
});
|
|
27947
|
+
const resumedPromise = this.pausePromise.then(() => "resumed");
|
|
27948
|
+
const winner = await Promise.race([resumedPromise, timeoutPromise]);
|
|
27949
|
+
if (timer) clearTimeout(timer);
|
|
27950
|
+
if (winner === "timeout") {
|
|
27951
|
+
this.resume();
|
|
27952
|
+
return false;
|
|
27953
|
+
}
|
|
27954
|
+
return true;
|
|
27955
|
+
}
|
|
27956
|
+
// ── Manual tool-call injection (Phase 4) ─────────────────────────────────
|
|
27957
|
+
//
|
|
27958
|
+
// A controller can ask the agent loop to use a specific tool_result
|
|
27959
|
+
// for a given tool_use_id — bypassing the real tool execution. This
|
|
27960
|
+
// is "I want the agent to think the read returned THIS content" or
|
|
27961
|
+
// "skip the bash call, just give it the answer I typed". The
|
|
27962
|
+
// injection is matched by tool_use_id, consumed once, and discarded.
|
|
27963
|
+
injectionQueue = /* @__PURE__ */ new Map();
|
|
27964
|
+
/**
|
|
27965
|
+
* Queue a manual tool result. The next time the agent's toolCall
|
|
27966
|
+
* pipeline sees a matching `toolUse.id`, the
|
|
27967
|
+
* `collabInjectMiddleware` consumes this entry and replaces the
|
|
27968
|
+
* real tool execution. Returns `false` if an injection for the
|
|
27969
|
+
* same id is already queued (idempotent — first write wins).
|
|
27970
|
+
*/
|
|
27971
|
+
injectToolResult(input) {
|
|
27972
|
+
if (this.injectionQueue.has(input.toolUseId)) return false;
|
|
27973
|
+
this.injectionQueue.set(input.toolUseId, input);
|
|
27974
|
+
return true;
|
|
27975
|
+
}
|
|
27976
|
+
/**
|
|
27977
|
+
* Pop an injection from the queue. Returns the payload (and
|
|
27978
|
+
* removes it) if one is pending, or `null` when nothing matches.
|
|
27979
|
+
* Called by the middleware on every tool call.
|
|
27980
|
+
*/
|
|
27981
|
+
takeInjection(toolUseId) {
|
|
27982
|
+
const v = this.injectionQueue.get(toolUseId);
|
|
27983
|
+
if (!v) return null;
|
|
27984
|
+
this.injectionQueue.delete(toolUseId);
|
|
27985
|
+
return v;
|
|
27986
|
+
}
|
|
27987
|
+
/** Inspect the queue size (for observability / tests). */
|
|
27988
|
+
pendingInjectionCount() {
|
|
27989
|
+
return this.injectionQueue.size;
|
|
27990
|
+
}
|
|
27991
|
+
};
|
|
27992
|
+
|
|
27993
|
+
// src/middleware/collab-pause.ts
|
|
27994
|
+
function collabPauseMiddleware(bus, opts = {}) {
|
|
27995
|
+
const timeoutMs = opts.defaultTimeoutMs ?? 6e4;
|
|
27996
|
+
const logger = opts.logger;
|
|
27997
|
+
return async function collabPause(payload, next) {
|
|
27998
|
+
if (!bus.isPaused()) {
|
|
27999
|
+
return next();
|
|
28000
|
+
}
|
|
28001
|
+
const state = bus.getState();
|
|
28002
|
+
logger?.debug?.(
|
|
28003
|
+
`collab-pause: tool '${payload.toolUse.name}' blocked \u2014 bus paused by ${state.pausedBy ?? "?"} at ${state.pausedAt ?? "?"}, waiting up to ${timeoutMs}ms for resume`
|
|
28004
|
+
);
|
|
28005
|
+
const resumed = await bus.waitForResume(timeoutMs);
|
|
28006
|
+
if (!resumed) {
|
|
28007
|
+
logger?.warn?.(
|
|
28008
|
+
`collab-pause: timeout after ${timeoutMs}ms \u2014 auto-resuming the bus to unblock the agent loop`
|
|
28009
|
+
);
|
|
28010
|
+
} else {
|
|
28011
|
+
logger?.debug?.(`collab-pause: resumed \u2014 proceeding with tool '${payload.toolUse.name}'`);
|
|
28012
|
+
}
|
|
28013
|
+
return next();
|
|
28014
|
+
};
|
|
28015
|
+
}
|
|
28016
|
+
function collabInjectMiddleware(bus, opts = {}) {
|
|
28017
|
+
const logger = opts.logger;
|
|
28018
|
+
return async function collabInject(payload, next) {
|
|
28019
|
+
const injected = bus.takeInjection(payload.toolUse.id);
|
|
28020
|
+
if (!injected) {
|
|
28021
|
+
return next();
|
|
28022
|
+
}
|
|
28023
|
+
logger?.debug?.(
|
|
28024
|
+
`collab-inject: tool '${payload.toolUse.name}' (id ${payload.toolUse.id}) \u2014 using controller-injected result (reason: ${injected.reason})`
|
|
28025
|
+
);
|
|
28026
|
+
payload.result = {
|
|
28027
|
+
type: "tool_result",
|
|
28028
|
+
tool_use_id: payload.toolUse.id,
|
|
28029
|
+
content: typeof injected.content === "string" ? injected.content : JSON.stringify(injected.content),
|
|
28030
|
+
is_error: injected.isError
|
|
28031
|
+
};
|
|
28032
|
+
};
|
|
28033
|
+
}
|
|
28034
|
+
|
|
28035
|
+
// src/replay/replay-provider-runner.ts
|
|
28036
|
+
var ReplayProviderRunner = class {
|
|
28037
|
+
constructor(inner, opts) {
|
|
28038
|
+
this.inner = inner;
|
|
28039
|
+
this.opts = opts;
|
|
28040
|
+
}
|
|
28041
|
+
inner;
|
|
28042
|
+
opts;
|
|
28043
|
+
async run(runOpts) {
|
|
28044
|
+
const hash = hashRequest(runOpts.request);
|
|
28045
|
+
const cached = await this.opts.log.lookup(this.opts.sessionId, hash);
|
|
28046
|
+
if (this.opts.mode === "replay") {
|
|
28047
|
+
if (!cached) {
|
|
28048
|
+
this.opts.logger?.warn?.(
|
|
28049
|
+
`replay: no recorded response for hash ${hash} (model ${runOpts.request.model})`
|
|
28050
|
+
);
|
|
28051
|
+
throw new Error(
|
|
28052
|
+
`ReplayProviderRunner: no recorded response for hash ${hash} in session ${this.opts.sessionId}. Either the request changed since recording, or this session has no replay log.`
|
|
28053
|
+
);
|
|
28054
|
+
}
|
|
28055
|
+
this.opts.logger?.debug?.(
|
|
28056
|
+
`replay: served cached response for hash ${hash} (recorded ${cached.ts})`
|
|
28057
|
+
);
|
|
28058
|
+
return cached.response;
|
|
28059
|
+
}
|
|
28060
|
+
if (this.opts.mode === "auto" && cached) {
|
|
28061
|
+
this.opts.logger?.debug?.(
|
|
28062
|
+
`replay: auto-hit hash ${hash}, served cached response`
|
|
28063
|
+
);
|
|
28064
|
+
return cached.response;
|
|
28065
|
+
}
|
|
28066
|
+
const response = await this.inner.run(runOpts);
|
|
28067
|
+
await this.opts.log.record({
|
|
28068
|
+
sessionId: this.opts.sessionId,
|
|
28069
|
+
request: runOpts.request,
|
|
28070
|
+
response
|
|
28071
|
+
});
|
|
28072
|
+
return response;
|
|
28073
|
+
}
|
|
28074
|
+
};
|
|
28075
|
+
|
|
26116
28076
|
// src/plugins/prompts-plugin.ts
|
|
26117
28077
|
function createPromptsPlugin(opts) {
|
|
26118
28078
|
let store = null;
|
|
@@ -27170,6 +29130,6 @@ ${formatPlan(updated)}`
|
|
|
27170
29130
|
};
|
|
27171
29131
|
}
|
|
27172
29132
|
|
|
27173
|
-
export { ACP_AGENTS, AGENTS_BY_PHASE, AGENT_CATALOG, AISpecBuilder, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, ALL_SYNC_CATEGORIES, AUDIT_LOG_AGENT, Agent, AgentError, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutoPhasePlanner, AutoPhaseRunner, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, CONTEXT_WINDOW_MODES, CheckpointManager, CloudSync, ConfigError, ConfigMigrationError, Container, Context, ConversationState, DEFAULT_AUTONOMY_CONFIG, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_CONTEXT_CONFIG, DEFAULT_CONTEXT_WINDOW_MODE_ID, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_MAX_ITERATIONS, DEFAULT_MODES, DEFAULT_RECOVERY_STRATEGIES, DEFAULT_SPEC_TEMPLATE, DEFAULT_SUBAGENT_BASELINE, DEFAULT_TOOLS_CONFIG, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultPluginAPI, DefaultPromptStore, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultTaskStore, DefaultTokenCounter, Director, DirectorStateCheckpoint, DoneConditionChecker, ERROR_CODES, EternalAutonomyEngine, EventBus, ExtensionRegistry, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FLEET_ROSTER_WITHACP, FleetBus, FleetCostCapError, FleetManager, FleetSpawnBudgetError, FleetUsageAggregator, FsError, GitignoreUpdater, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, InputBuilder, IntelligentCompactor, KERNEL_API_VERSION, LAYER_1_IDENTITY, LLMSelector, MAX_JOURNAL_ENTRIES, NULL_FLEET_BUS, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, ParallelEternalEngine, PhaseGraphBuilder, PhaseOrchestrator, PhaseStore, Pipeline, PluginError, ProviderError, ProviderRegistry, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, ReportGenerator, RunController, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, ScopedEventBus, SddParallelRun, SddTaskDecomposer, SecurityScanner, SecurityScannerOrchestrator, SelectiveCompactor, SessionAnalyzer, SessionError, SkillGenerator, SkillInstaller, SkillManifestStore, SlashCommandRegistry, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, SubagentBudget, TOKENS, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, TechStackDetector, ToolError, ToolExecutor, ToolRegistry, WorktreeManager, WrongStackError, addPlanItem, allServers, analyzeCriticalPath, appendJournal, applyRosterBudget, asBlocks, asText, assertSafePath, atomicWrite, attachAutoExtend, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildBtwBlock, buildChildEnv, buildGoalPreamble, buildOtlpMetricsRequest, buildOtlpTracesRequest, buildRecoveryStrategies, classifyFamily, clearPlan, color, compileGlob, compileUserRegex, completePartialObject, composeDirectorPrompt, composeSubagentPrompt, computeTaskProgress, consumeBtwNotes, context7Server, contextManagerTool, createAutoExecutor, createAutoPhaseFromTaskGraph, createContextManagerTool, createDefaultPipelines, createDelegateTool, createGitPlugin, createMcpControlTool, createMessage, createObservabilityPlugin, createPlanPlugin, createPromptsPlugin, createSecurityPlugin, createSecuritySlashCommand, createSkillsPlugin, createSyncPlugin, createToolOutputSerializer, decryptConfigSecrets, defaultGitignoreUpdater, defaultOrchestrator, defaultReportGenerator, defaultSecurityScanner, defaultSkillGenerator, defaultTechStackDetector, deriveTodosFromPlanItem, detectNewlineStyle, dispatchAgent, downloadGitHubTarball, emptyGoal, emptyPlan, encryptConfigSecrets, ensureDir, estimateRequestTokens, estimateTextTokens, estimateToolDefTokens, estimateToolInputTokens, estimateToolResultTokens, everArtServer, extractRunEnv, filesystemServer, findCriticalPath, formatContextWindowModeList, formatGoal, formatPlan, formatPlanTemplates, formatTodosList, getAgentDefinition, getContextWindowMode, getPlanTemplate, getTemplate, githubServer, goalFilePath, googleMapsServer, isAgentError, isConfigError, isContextWindowModeId, isFsError, isImageBlock, isPluginError, isSessionError, isTextBlock, isThinkingBlock, isToolError, isToolResultBlock, isToolUseBlock, isWrongStackError, listContextWindowModes, listPlanTemplates, listTemplates, loadDirectorState, loadGoal, loadPlan, loadPlugins, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeAskTool, makeAssignTool, makeAutonomyPromptContributor, makeAwaitTasksTool, makeCollabDebugTool, makeContinueToNextIterationTool, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeRollUpTool, makeSpawnTool, makeTerminateTool, matchAny, matchGlob, migratePlaintextSecrets, miniMaxVisionServer, normalizeToLf, parseContinueDirective, parseSkillRef, pendingBtwCount, projectHash, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, repairToolUseAdjacency, resolveContextWindowPolicy, resolveWstackPaths, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, safeParse, safeStringify, sanitizeJsonString, saveGoal, savePlan, saveTodosCheckpoint, scoreAgents, securitySlashCommand, sentinelServer, setBtwNote, setPlanItemStatus, slackServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, stripAnsi, summarizeUsage, templateToMarkdown, toStyle, toWrongStackError, topologicalSort, unifiedDiff, unloadPlugins, validateAgainstSchema, wireMetricsToEvents, wrapAsState, zaiVisionServer };
|
|
29133
|
+
export { ACP_AGENTS, AGENTS_BY_PHASE, AGENT_CATALOG, AISpecBuilder, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, ALL_SYNC_CATEGORIES, AUDIT_LOG_AGENT, Agent, AgentError, AnnotationsStore, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutoPhasePlanner, AutoPhaseRunner, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, CONTEXT_WINDOW_MODES, CORE_RECONSTRUCT_EVENTS, CheckpointManager, CloudSync, CollaborationBus, ConfigError, ConfigMigrationError, Container, Context, ConversationState, DEFAULT_AUTONOMY_CONFIG, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_CONTEXT_CONFIG, DEFAULT_CONTEXT_WINDOW_MODE_ID, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_MAX_ITERATIONS, DEFAULT_MODES, DEFAULT_RECOVERY_STRATEGIES, DEFAULT_SESSION_LOGGING_CONFIG, DEFAULT_SPEC_TEMPLATE, DEFAULT_SUBAGENT_BASELINE, DEFAULT_TOOLS_CONFIG, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultPluginAPI, DefaultPromptStore, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultTaskStore, DefaultTokenCounter, Director, DirectorStateCheckpoint, DoneConditionChecker, ERROR_CODES, EternalAutonomyEngine, EventBus, ExtensionRegistry, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FLEET_ROSTER_WITHACP, FleetBus, FleetCostCapError, FleetManager, FleetSpawnBudgetError, FleetUsageAggregator, FsError, GitignoreUpdater, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, InputBuilder, IntelligentCompactor, KERNEL_API_VERSION, LAYER_1_IDENTITY, LLMSelector, MAX_JOURNAL_ENTRIES, NULL_FLEET_BUS, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, ParallelEternalEngine, PhaseGraphBuilder, PhaseOrchestrator, PhaseStore, Pipeline, PluginError, ProviderError, ProviderRegistry, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, ReplayLogStore, ReplayProviderRunner, ReportGenerator, RunController, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, STANDARD_AUDIT_EVENTS, ScopedEventBus, SddParallelRun, SddTaskDecomposer, SecurityScanner, SecurityScannerOrchestrator, SelectiveCompactor, SessionAnalyzer, SessionError, SessionRecovery, SkillGenerator, SkillInstaller, SkillManifestStore, SlashCommandRegistry, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, SubagentBudget, TOKENS, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, TechStackDetector, ToolAuditLog, ToolError, ToolExecutor, ToolRegistry, WorktreeManager, WrongStackError, addPlanItem, allServers, analyzeCriticalPath, appendJournal, applyRosterBudget, asBlocks, asText, assertSafePath, atomicWrite, attachAutoExtend, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildBtwBlock, buildChildEnv, buildGoalPreamble, buildOtlpMetricsRequest, buildOtlpTracesRequest, buildRecoveryStrategies, classifyFamily, clearPlan, collabInjectMiddleware, collabPauseMiddleware, color, compileGlob, compileUserRegex, completePartialObject, composeDirectorPrompt, composeSubagentPrompt, computeTaskProgress, consumeBtwNotes, context7Server, contextManagerTool, createAutoExecutor, createAutoPhaseFromTaskGraph, createContextManagerTool, createDefaultPipelines, createDelegateTool, createGitPlugin, createMcpControlTool, createMessage, createObservabilityPlugin, createPlanPlugin, createPromptsPlugin, createSecurityPlugin, createSecuritySlashCommand, createSessionEventBridge, createSkillsPlugin, createSyncPlugin, createToolOutputSerializer, decryptConfigSecrets, defaultGitignoreUpdater, defaultOrchestrator, defaultReportGenerator, defaultSecurityScanner, defaultSkillGenerator, defaultTechStackDetector, deriveTodosFromPlanItem, detectNewlineStyle, dispatchAgent, downloadGitHubTarball, emptyGoal, emptyPlan, encryptConfigSecrets, ensureDir, estimateRequestTokens, estimateTextTokens, estimateToolDefTokens, estimateToolInputTokens, estimateToolResultTokens, everArtServer, extractRunEnv, filesystemServer, findCriticalPath, formatContextWindowModeList, formatGoal, formatPlan, formatPlanTemplates, formatTodosList, getAgentDefinition, getContextWindowMode, getPlanTemplate, getTemplate, githubServer, goalFilePath, googleMapsServer, hashRequest, isAgentError, isConfigError, isContextWindowModeId, isFsError, isImageBlock, isPluginError, isSessionError, isTextBlock, isThinkingBlock, isToolError, isToolResultBlock, isToolUseBlock, isWrongStackError, listContextWindowModes, listPlanTemplates, listTemplates, loadDirectorState, loadGoal, loadPlan, loadPlugins, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeAskTool, makeAssignTool, makeAutonomyPromptContributor, makeAwaitTasksTool, makeCollabDebugTool, makeContinueToNextIterationTool, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeRollUpTool, makeSpawnTool, makeTerminateTool, matchAny, matchGlob, mergeModelsPayload, migratePlaintextSecrets, miniMaxVisionServer, normalizeToLf, parseContinueDirective, parseSkillRef, pendingBtwCount, projectHash, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, repairToolUseAdjacency, resolveAuditLevel, resolveContextWindowPolicy, resolveSessionLoggingConfig, resolveWstackPaths, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, runProviderWithRetry, safeParse, safeStringify, sanitizeJsonString, saveGoal, savePlan, saveTodosCheckpoint, scoreAgents, securitySlashCommand, sentinelServer, setBtwNote, setPlanItemStatus, slackServer, stableStringify, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, stripAnsi, summarizeUsage, templateToMarkdown, toStyle, toWrongStackError, topologicalSort, unifiedDiff, unloadPlugins, validateAgainstSchema, wireMetricsToEvents, wrapAsState, zaiVisionServer };
|
|
27174
29134
|
//# sourceMappingURL=index.js.map
|
|
27175
29135
|
//# sourceMappingURL=index.js.map
|