@wrongstack/core 0.9.20 → 0.10.2

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.
Files changed (67) hide show
  1. package/README.md +37 -33
  2. package/dist/{agent-bridge-Dti3KXGk.d.ts → agent-bridge-DBBGP5O4.d.ts} +1 -1
  3. package/dist/{agent-subagent-runner-U-rs7kk7.d.ts → agent-subagent-runner-KwufTWwn.d.ts} +3 -3
  4. package/dist/{compactor-D7X96RLZ.d.ts → compactor-D1RHFRmF.d.ts} +1 -1
  5. package/dist/{config-CLXMDOSs.d.ts → config-CJXBka2r.d.ts} +38 -2
  6. package/dist/{context-zkZeILpr.d.ts → context-7u93AcGD.d.ts} +94 -0
  7. package/dist/coordination/index.d.ts +12 -12
  8. package/dist/coordination/index.js +130 -58
  9. package/dist/coordination/index.js.map +1 -1
  10. package/dist/{default-config-Uzla85H-.d.ts → default-config-DEXI4jsl.d.ts} +10 -1
  11. package/dist/defaults/index.d.ts +24 -24
  12. package/dist/defaults/index.js +611 -95
  13. package/dist/defaults/index.js.map +1 -1
  14. package/dist/{events-DH-9r-_C.d.ts → events-Bw1K5E6K.d.ts} +8 -1
  15. package/dist/execution/index.d.ts +18 -15
  16. package/dist/execution/index.js +309 -61
  17. package/dist/execution/index.js.map +1 -1
  18. package/dist/extension/index.d.ts +6 -6
  19. package/dist/extension/index.js.map +1 -1
  20. package/dist/goal-store-BeRsj7YX.d.ts +188 -0
  21. package/dist/{index-CFO9QmJo.d.ts → index-DIt5TIaF.d.ts} +8 -8
  22. package/dist/{index-BIHJ4uII.d.ts → index-DgQWnZxP.d.ts} +5 -5
  23. package/dist/index.d.ts +56 -35
  24. package/dist/index.js +742 -203
  25. package/dist/index.js.map +1 -1
  26. package/dist/infrastructure/index.d.ts +6 -6
  27. package/dist/kernel/index.d.ts +9 -9
  28. package/dist/kernel/index.js +57 -8
  29. package/dist/kernel/index.js.map +1 -1
  30. package/dist/{mcp-servers-DkESgh0G.d.ts → mcp-servers-BgINZzuo.d.ts} +3 -3
  31. package/dist/models/index.d.ts +3 -3
  32. package/dist/models/index.js +163 -10
  33. package/dist/models/index.js.map +1 -1
  34. package/dist/models-registry-gwMAo6E3.d.ts +86 -0
  35. package/dist/{multi-agent-DNp6lAzg.d.ts → multi-agent-C1O8EFzb.d.ts} +45 -4
  36. package/dist/{multi-agent-coordinator-CAhsegPz.d.ts → multi-agent-coordinator-DoVSBFNx.d.ts} +2 -2
  37. package/dist/{null-fleet-bus-Dnl19vmf.d.ts → null-fleet-bus-T-s3JaOt.d.ts} +8 -6
  38. package/dist/observability/index.d.ts +2 -2
  39. package/dist/{path-resolver-CHiBL0DD.d.ts → path-resolver-BfyK-I7w.d.ts} +2 -2
  40. package/dist/{permission-H35s9Evv.d.ts → permission-B6sldrSp.d.ts} +1 -1
  41. package/dist/{permission-policy-CT-nRmTn.d.ts → permission-policy-CtNscWOA.d.ts} +10 -9
  42. package/dist/{plan-templates-Bs8iRwi6.d.ts → plan-templates-BWs6Je7j.d.ts} +8 -8
  43. package/dist/{provider-runner-BZdDrWrS.d.ts → provider-runner-BmPe2RuU.d.ts} +3 -3
  44. package/dist/{retry-policy-J9N_PM40.d.ts → retry-policy-KF18W4dg.d.ts} +1 -1
  45. package/dist/sdd/index.d.ts +8 -8
  46. package/dist/sdd/index.js +130 -58
  47. package/dist/sdd/index.js.map +1 -1
  48. package/dist/security/index.d.ts +65 -4
  49. package/dist/security/index.js +67 -19
  50. package/dist/security/index.js.map +1 -1
  51. package/dist/{selector-CFTh3Z6p.d.ts → selector-DmXxpFyM.d.ts} +1 -1
  52. package/dist/{session-reader-C7JJlxOw.d.ts → session-reader-CCSBfQmd.d.ts} +1 -1
  53. package/dist/storage/index.d.ts +7 -7
  54. package/dist/storage/index.js +125 -6
  55. package/dist/storage/index.js.map +1 -1
  56. package/dist/{system-prompt-CneIxVip.d.ts → system-prompt-CM6zOhd2.d.ts} +19 -1
  57. package/dist/{tool-executor-flTuxsqO.d.ts → tool-executor-D7RxLtg4.d.ts} +4 -4
  58. package/dist/types/index.d.ts +16 -16
  59. package/dist/types/index.js +344 -14
  60. package/dist/types/index.js.map +1 -1
  61. package/dist/utils/index.d.ts +23 -3
  62. package/dist/utils/index.js +66 -1
  63. package/dist/utils/index.js.map +1 -1
  64. package/dist/{wstack-paths-BCgmTNlG.d.ts → wstack-paths-eMXnY1_X.d.ts} +8 -0
  65. package/package.json +1 -1
  66. package/dist/goal-store-C7jcumEh.d.ts +0 -96
  67. package/dist/models-registry-OG_30xqZ.d.ts +0 -46
@@ -1,4 +1,4 @@
1
- import { $ as Usage, d as Context, X as ToolProgressEvent, Q as Tool } from './context-zkZeILpr.js';
1
+ import { $ as Usage, d as Context, X as ToolProgressEvent, Q as Tool } from './context-7u93AcGD.js';
2
2
 
3
3
  /**
4
4
  * EventBus — observe-only typed event bus.
@@ -612,6 +612,13 @@ declare class ScopedEventBus extends EventBus {
612
612
  * Uses EventBus's public API directly to avoid triggering our own `on()`
613
613
  * override (which would consume a key slot for the wrapper, then orphan
614
614
  * our registration entry under a different key).
615
+ *
616
+ * When the wrapper fires, it cleans up BOTH the underlying EventBus
617
+ * listener AND the tracking entry — so `scopedListenerCount` returns to
618
+ * its pre-`once()` value without requiring the caller to invoke the
619
+ * returned unsubscribe. The returned `unsub` is still safe to call
620
+ * after auto-removal (its delete is a no-op and its off() finds
621
+ * nothing to remove).
615
622
  */
616
623
  once<E extends EventName>(event: E, fn: Listener<E>): () => void;
617
624
  /**
@@ -1,23 +1,23 @@
1
- export { C as CompactorOptions, a as DefaultErrorHandler, b as DefaultRetryPolicy, H as HybridCompactor, T as ToolExecutor } from '../tool-executor-flTuxsqO.js';
2
- import { m as Provider, d as Context } from '../context-zkZeILpr.js';
3
- import { a as Compactor, C as CompactReport } from '../compactor-D7X96RLZ.js';
4
- import { M as MessageSelector } from '../selector-CFTh3Z6p.js';
5
- import { E as EventBus } from '../events-DH-9r-_C.js';
6
- import { b as MiddlewareHandler } from '../system-prompt-CneIxVip.js';
7
- import { e as ContextWindowAggressiveOn, i as ContextWindowPolicy } from '../config-CLXMDOSs.js';
8
- import { c as Agent, R as RunResult, w as SystemPromptContributor } from '../index-BIHJ4uII.js';
9
- import { D as DoneCondition } from '../multi-agent-DNp6lAzg.js';
10
- import { J as JournalEntry } from '../goal-store-C7jcumEh.js';
11
- import { A as AgentFactory } from '../agent-subagent-runner-U-rs7kk7.js';
12
- import { f as DispatchClassifier, d as DefaultMultiAgentCoordinator } from '../multi-agent-coordinator-CAhsegPz.js';
1
+ export { C as CompactorOptions, a as DefaultErrorHandler, b as DefaultRetryPolicy, H as HybridCompactor, T as ToolExecutor } from '../tool-executor-D7RxLtg4.js';
2
+ import { m as Provider, d as Context } from '../context-7u93AcGD.js';
3
+ import { a as Compactor, C as CompactReport } from '../compactor-D1RHFRmF.js';
4
+ import { M as MessageSelector } from '../selector-DmXxpFyM.js';
5
+ import { E as EventBus } from '../events-Bw1K5E6K.js';
6
+ import { b as MiddlewareHandler } from '../system-prompt-CM6zOhd2.js';
7
+ import { a as SessionEventBridge, J as JournalEntry } from '../goal-store-BeRsj7YX.js';
8
+ import { e as ContextWindowAggressiveOn, i as ContextWindowPolicy } from '../config-CJXBka2r.js';
9
+ import { c as Agent, R as RunResult, w as SystemPromptContributor } from '../index-DgQWnZxP.js';
10
+ import { D as DoneCondition } from '../multi-agent-C1O8EFzb.js';
11
+ import { A as AgentFactory } from '../agent-subagent-runner-KwufTWwn.js';
12
+ import { f as DispatchClassifier, d as DefaultMultiAgentCoordinator } from '../multi-agent-coordinator-DoVSBFNx.js';
13
13
  import { a as SkillLoader, b as SkillManifest, S as SkillEntry } from '../skill-CxuWrsKK.js';
14
- import { a as WstackPaths } from '../wstack-paths-BCgmTNlG.js';
15
- import '../retry-policy-J9N_PM40.js';
14
+ import { a as WstackPaths } from '../wstack-paths-eMXnY1_X.js';
15
+ import '../retry-policy-KF18W4dg.js';
16
16
  import '../models-registry-BcYJDKLm.js';
17
17
  import '../logger-DDd5C--Z.js';
18
18
  import '../observability-BhnVLBLS.js';
19
19
  import '../secret-scrubber-3MHDDAtm.js';
20
- import '../permission-H35s9Evv.js';
20
+ import '../permission-B6sldrSp.js';
21
21
  import 'node:events';
22
22
 
23
23
  interface SkillLoaderOptions {
@@ -195,6 +195,8 @@ interface AutoCompactionOptions {
195
195
  events?: EventBus;
196
196
  failureMode?: CompactionFailureMode;
197
197
  policyProvider?: (ctx: Context) => Pick<ContextWindowPolicy, 'thresholds' | 'aggressiveOn'> | null | undefined;
198
+ /** Optional bridge for writing compaction events into the persistent session log. */
199
+ sessionBridge?: SessionEventBridge;
198
200
  }
199
201
  /**
200
202
  * Pipeline middleware that monitors context token load and automatically
@@ -219,6 +221,7 @@ declare class AutoCompactionMiddleware {
219
221
  private readonly events?;
220
222
  private readonly failureMode;
221
223
  private readonly policyProvider?;
224
+ private readonly sessionBridge?;
222
225
  /**
223
226
  * Once a compaction attempt reduces nothing (preserveK protects everything,
224
227
  * no oversized tool_results remain to elide), retrying on every iteration
@@ -1113,6 +1113,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
1113
1113
  events;
1114
1114
  failureMode;
1115
1115
  policyProvider;
1116
+ sessionBridge;
1116
1117
  /**
1117
1118
  * Once a compaction attempt reduces nothing (preserveK protects everything,
1118
1119
  * no oversized tool_results remain to elide), retrying on every iteration
@@ -1149,6 +1150,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
1149
1150
  this.events = opts.events;
1150
1151
  this.failureMode = opts.failureMode ?? "throw_on_hard";
1151
1152
  this.policyProvider = opts.policyProvider;
1153
+ this.sessionBridge = opts.sessionBridge;
1152
1154
  }
1153
1155
  /** Allow callers (e.g. model-switch in WebUI) to update the context window
1154
1156
  * denominator when the active model changes. */
@@ -1212,6 +1214,15 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
1212
1214
  report,
1213
1215
  aggressive
1214
1216
  });
1217
+ await this.sessionBridge?.append({
1218
+ type: "compaction",
1219
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1220
+ before: report.before,
1221
+ after: report.after,
1222
+ level: pressure.level,
1223
+ aggressive,
1224
+ reductions: report.reductions?.map((r) => ({ phase: r.phase, saved: r.saved }))
1225
+ });
1215
1226
  ctx.clearFileTracking();
1216
1227
  } catch (err) {
1217
1228
  const error = err instanceof Error ? err : new Error(String(err));
@@ -1242,6 +1253,141 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
1242
1253
  }
1243
1254
  };
1244
1255
 
1256
+ // src/security/capabilities.ts
1257
+ var ToolCapabilities = {
1258
+ /** Can execute arbitrary commands in the user's shell (the `bash` tool). */
1259
+ SHELL_ARBITRARY: "shell.arbitrary",
1260
+ /** Can write / modify / delete files inside the project. */
1261
+ FS_WRITE: "fs.write",
1262
+ /** Can write files outside the current project root (very high risk). */
1263
+ FS_WRITE_OUTSIDE_PROJECT: "fs.write.outside-project",
1264
+ /** Proxies tools from external MCP servers (unknown capability). */
1265
+ MCP_PROXY: "mcp.proxy",
1266
+ /** Can spawn or manage subagents / multi-agent tasks. */
1267
+ SUBAGENT_SPAWN: "subagent.spawn",
1268
+ /** Can mutate global or session configuration / trust state. */
1269
+ CONFIG_MUTATE: "config.mutate",
1270
+ /** Can install packages or run package managers with side effects. */
1271
+ PACKAGE_INSTALL: "package.install"
1272
+ };
1273
+ var DANGEROUS_FOR_SUBAGENTS = [
1274
+ ToolCapabilities.SHELL_ARBITRARY,
1275
+ ToolCapabilities.FS_WRITE,
1276
+ ToolCapabilities.FS_WRITE_OUTSIDE_PROJECT,
1277
+ ToolCapabilities.MCP_PROXY,
1278
+ ToolCapabilities.SUBAGENT_SPAWN,
1279
+ ToolCapabilities.CONFIG_MUTATE,
1280
+ ToolCapabilities.PACKAGE_INSTALL
1281
+ ];
1282
+ function hasDangerousCapabilityForSubagents(toolOrCaps) {
1283
+ if (!toolOrCaps) return false;
1284
+ const input = toolOrCaps;
1285
+ const caps = Array.isArray(input) ? input : input?.capabilities ?? [];
1286
+ return caps.some((c) => DANGEROUS_FOR_SUBAGENTS.includes(c));
1287
+ }
1288
+ function getDangerousCapabilities(toolOrCaps) {
1289
+ if (!toolOrCaps) return [];
1290
+ const input = toolOrCaps;
1291
+ const caps = Array.isArray(input) ? input : input?.capabilities ?? [];
1292
+ return caps.filter(
1293
+ (c) => DANGEROUS_FOR_SUBAGENTS.includes(c)
1294
+ );
1295
+ }
1296
+
1297
+ // src/utils/json-schema-validate.ts
1298
+ function validateAgainstSchema(value, schema) {
1299
+ const errors = [];
1300
+ walk(value, schema, "", errors);
1301
+ return { ok: errors.length === 0, errors };
1302
+ }
1303
+ function walk(value, schema, path4, errors) {
1304
+ if (schema.enum !== void 0) {
1305
+ if (!schema.enum.some((e) => deepEqual(e, value))) {
1306
+ errors.push({
1307
+ path: path4 || "<root>",
1308
+ message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
1309
+ });
1310
+ return;
1311
+ }
1312
+ }
1313
+ if (typeof schema.type === "string") {
1314
+ if (!checkType(value, schema.type)) {
1315
+ errors.push({
1316
+ path: path4 || "<root>",
1317
+ message: `expected ${schema.type}, got ${describeType(value)}`
1318
+ });
1319
+ return;
1320
+ }
1321
+ }
1322
+ if (schema.type === "object" && isPlainObject(value)) {
1323
+ const obj = value;
1324
+ for (const req of schema.required ?? []) {
1325
+ if (!(req in obj)) {
1326
+ errors.push({ path: joinPath(path4, req), message: "required property missing" });
1327
+ }
1328
+ }
1329
+ if (schema.properties) {
1330
+ for (const [key, subSchema] of Object.entries(schema.properties)) {
1331
+ if (key in obj) {
1332
+ walk(obj[key], subSchema, joinPath(path4, key), errors);
1333
+ }
1334
+ }
1335
+ }
1336
+ }
1337
+ if (schema.type === "array" && Array.isArray(value) && schema.items) {
1338
+ value.forEach((item, i) => walk(item, schema.items, `${path4}[${i}]`, errors));
1339
+ }
1340
+ }
1341
+ function checkType(value, type) {
1342
+ switch (type) {
1343
+ case "string":
1344
+ return typeof value === "string";
1345
+ case "number":
1346
+ return typeof value === "number" && !Number.isNaN(value);
1347
+ case "integer":
1348
+ return typeof value === "number" && Number.isInteger(value);
1349
+ case "boolean":
1350
+ return typeof value === "boolean";
1351
+ case "null":
1352
+ return value === null;
1353
+ case "array":
1354
+ return Array.isArray(value);
1355
+ case "object":
1356
+ return isPlainObject(value);
1357
+ default:
1358
+ return true;
1359
+ }
1360
+ }
1361
+ function isPlainObject(v) {
1362
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1363
+ }
1364
+ function describeType(v) {
1365
+ if (v === null) return "null";
1366
+ if (Array.isArray(v)) return "array";
1367
+ return typeof v;
1368
+ }
1369
+ function joinPath(parent, key) {
1370
+ if (!parent) return key;
1371
+ return `${parent}.${key}`;
1372
+ }
1373
+ function deepEqual(a, b) {
1374
+ if (a === b) return true;
1375
+ if (typeof a !== typeof b) return false;
1376
+ if (a === null || b === null) return a === b;
1377
+ if (Array.isArray(a) && Array.isArray(b)) {
1378
+ return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
1379
+ }
1380
+ if (typeof a === "object" && typeof b === "object") {
1381
+ const ak = Object.keys(a);
1382
+ const bk = Object.keys(b);
1383
+ if (ak.length !== bk.length) return false;
1384
+ return ak.every(
1385
+ (k) => deepEqual(a[k], b[k])
1386
+ );
1387
+ }
1388
+ return false;
1389
+ }
1390
+
1245
1391
  // src/utils/tool-output-serializer.ts
1246
1392
  function createToolOutputSerializer(opts = {}) {
1247
1393
  const capBytes = opts.perIterationOutputCapBytes ?? 1e5;
@@ -1323,18 +1469,45 @@ var ToolExecutor = class {
1323
1469
  budget = this.decrementBudget(result, budget);
1324
1470
  return { result, tool, durationMs: Date.now() - start };
1325
1471
  }
1472
+ const validation = validateAgainstSchema(use.input, tool.inputSchema);
1473
+ if (!validation.ok) {
1474
+ const errorDetails = validation.errors.map((e) => ` - ${e.path || "input"}: ${e.message}`).join("\n");
1475
+ const result = {
1476
+ type: "tool_result",
1477
+ tool_use_id: use.id,
1478
+ content: `Invalid arguments for tool "${tool.name}".
1479
+
1480
+ Validation errors:
1481
+ ${errorDetails}
1482
+
1483
+ 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.`,
1484
+ is_error: true
1485
+ };
1486
+ budget = this.decrementBudget(result, budget);
1487
+ return { result, tool, durationMs: Date.now() - start };
1488
+ }
1489
+ const toolDangerousCaps = getDangerousCapabilities(tool);
1490
+ if (toolDangerousCaps.length > 0) {
1491
+ if (this.opts.events) ;
1492
+ }
1326
1493
  if (hasMalformedArguments(use.input)) {
1327
1494
  const result = this.malformedInputResult(use, extractMalformedRaw(use.input));
1328
1495
  budget = this.decrementBudget(result, budget);
1329
1496
  return { result, tool, durationMs: Date.now() - start };
1330
1497
  }
1331
1498
  const decision = await this.opts.permissionPolicy.evaluate(tool, use.input, ctx);
1332
- if (decision.permission === "deny") {
1499
+ let effectivePermission = decision.permission;
1500
+ const policy = this.opts.permissionPolicy;
1501
+ const yolo = policy.getYolo?.() === true || policy.getForceAllYolo?.() === true;
1502
+ if (toolDangerousCaps.length > 0 && effectivePermission === "auto" && !yolo) {
1503
+ effectivePermission = "confirm";
1504
+ }
1505
+ if (effectivePermission === "deny") {
1333
1506
  const result = this.deniedResult(use, decision.reason);
1334
1507
  budget = this.decrementBudget(result, budget);
1335
1508
  return { result, tool, durationMs: Date.now() - start };
1336
1509
  }
1337
- if (decision.permission === "confirm") {
1510
+ if (effectivePermission === "confirm") {
1338
1511
  if (this.opts.confirmAwaiter) {
1339
1512
  const choice = await this.opts.confirmAwaiter(tool, use.input, use.id, tool.name);
1340
1513
  if (choice !== "yes" && choice !== "always") {
@@ -1359,10 +1532,13 @@ var ToolExecutor = class {
1359
1532
  return { result: pending, tool, durationMs: Date.now() - start };
1360
1533
  }
1361
1534
  }
1535
+ const toolCapsForAudit = hasDangerousCapabilityForSubagents(tool) ? tool.capabilities ?? [] : [];
1362
1536
  const span = this.opts.tracer?.startSpan(`tool.${tool.name}`, {
1363
1537
  "tool.name": tool.name,
1364
1538
  "tool.mutating": tool.mutating,
1365
- "tool.permission": tool.permission
1539
+ "tool.permission": tool.permission,
1540
+ "tool.capabilities": JSON.stringify(tool.capabilities ?? []),
1541
+ "tool.has_dangerous_capabilities": toolCapsForAudit.length > 0
1366
1542
  });
1367
1543
  try {
1368
1544
  const result = await this.executeTool(tool, use, ctx, budget);
@@ -2531,6 +2707,13 @@ var SubagentBudget = class _SubagentBudget {
2531
2707
  tokenOutput = 0;
2532
2708
  costUsd = 0;
2533
2709
  startTime = null;
2710
+ /**
2711
+ * Timestamp of the most recent activity (iteration / tool call / token
2712
+ * usage / streamed progress). Drives the idle timeout — reset by
2713
+ * `markActivity()`. Initialised to `start()` time so a never-active agent
2714
+ * still eventually trips its idle window.
2715
+ */
2716
+ lastActivityTime = null;
2534
2717
  _onThreshold;
2535
2718
  /**
2536
2719
  * Hard cap on how long `_negotiateExtension` waits for the coordinator to
@@ -2574,6 +2757,24 @@ var SubagentBudget = class _SubagentBudget {
2574
2757
  }
2575
2758
  start() {
2576
2759
  this.startTime = Date.now();
2760
+ this.lastActivityTime = this.startTime;
2761
+ }
2762
+ /**
2763
+ * Reset the idle clock. Called on any sign of forward progress —
2764
+ * iterations, tool calls, token usage, and streamed tool/text progress —
2765
+ * so a long-but-productive subagent never trips its `idleTimeoutMs`.
2766
+ */
2767
+ markActivity() {
2768
+ this.lastActivityTime = Date.now();
2769
+ }
2770
+ /**
2771
+ * Milliseconds since the last activity signal. Returns 0 before `start()`
2772
+ * (nothing to measure yet). Used by the coordinator watchdog to decide
2773
+ * whether to re-arm (still active) or reap (genuinely idle).
2774
+ */
2775
+ idleMs() {
2776
+ const since = this.lastActivityTime ?? this.startTime;
2777
+ return since === null ? 0 : Date.now() - since;
2577
2778
  }
2578
2779
  /** Returns true if we're within 10% of any limit — useful for pre-flight checks. */
2579
2780
  isNearLimit() {
@@ -2621,8 +2822,13 @@ var SubagentBudget = class _SubagentBudget {
2621
2822
  if (this.limits.maxCostUsd !== void 0 && this.costUsd > this.limits.maxCostUsd) {
2622
2823
  exceeded.push({ kind: "cost", used: this.costUsd, limit: this.limits.maxCostUsd });
2623
2824
  }
2624
- if (elapsedMs !== void 0 && this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs) {
2625
- exceeded.push({ kind: "timeout", used: elapsedMs, limit: this.limits.timeoutMs });
2825
+ if (elapsedMs !== void 0) {
2826
+ const idle = this.idleMs();
2827
+ if (this.limits.idleTimeoutMs !== void 0 && idle > this.limits.idleTimeoutMs) {
2828
+ exceeded.push({ kind: "timeout", used: idle, limit: this.limits.idleTimeoutMs });
2829
+ } else if (this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs) {
2830
+ exceeded.push({ kind: "timeout", used: elapsedMs, limit: this.limits.timeoutMs });
2831
+ }
2626
2832
  }
2627
2833
  if (exceeded.length === 0) return [];
2628
2834
  if (!this._onThreshold) {
@@ -2727,6 +2933,9 @@ var SubagentBudget = class _SubagentBudget {
2727
2933
  if (ext.timeoutMs !== void 0) {
2728
2934
  this.limits.timeoutMs = ext.timeoutMs;
2729
2935
  }
2936
+ if (ext.idleTimeoutMs !== void 0) {
2937
+ this.limits.idleTimeoutMs = ext.idleTimeoutMs;
2938
+ }
2730
2939
  return decision;
2731
2940
  } finally {
2732
2941
  this._pendingNegotiations.delete(kind);
@@ -2734,16 +2943,19 @@ var SubagentBudget = class _SubagentBudget {
2734
2943
  }
2735
2944
  recordIteration() {
2736
2945
  this.iterations++;
2946
+ this.markActivity();
2737
2947
  void this.checkLimits();
2738
2948
  }
2739
2949
  recordToolCall() {
2740
2950
  this.toolCalls++;
2951
+ this.markActivity();
2741
2952
  void this.checkLimits();
2742
2953
  }
2743
2954
  recordUsage(usage, costUsd = 0) {
2744
2955
  this.tokenInput += usage.input;
2745
2956
  this.tokenOutput += usage.output;
2746
2957
  this.costUsd += costUsd;
2958
+ this.markActivity();
2747
2959
  void this.checkLimits();
2748
2960
  }
2749
2961
  /**
@@ -2758,15 +2970,22 @@ var SubagentBudget = class _SubagentBudget {
2758
2970
  * - `mode === 'auto'` + listener → throw `BudgetExceededError` (timeout is not extendable)
2759
2971
  */
2760
2972
  checkTimeout() {
2761
- if (this.startTime === null || this.limits.timeoutMs === void 0) return;
2973
+ if (this.startTime === null) return;
2974
+ const { timeoutMs, idleTimeoutMs } = this.limits;
2975
+ if (timeoutMs === void 0 && idleTimeoutMs === void 0) return;
2762
2976
  const elapsed = Date.now() - this.startTime;
2763
- if (elapsed <= this.limits.timeoutMs) return;
2977
+ const wallTripped = timeoutMs !== void 0 && elapsed > timeoutMs;
2978
+ const idleTripped = idleTimeoutMs !== void 0 && this.idleMs() > idleTimeoutMs;
2979
+ if (!wallTripped && !idleTripped) return;
2764
2980
  void this.checkLimits(elapsed);
2765
2981
  }
2766
- /** Returns true if a timeout has occurred without throwing. Useful for races. */
2982
+ /** Returns true if a wall-clock or idle timeout has occurred without throwing. */
2767
2983
  isTimedOut() {
2768
- if (this.startTime === null || this.limits.timeoutMs === void 0) return false;
2769
- return Date.now() - this.startTime > this.limits.timeoutMs;
2984
+ if (this.startTime === null) return false;
2985
+ const { timeoutMs, idleTimeoutMs } = this.limits;
2986
+ if (timeoutMs !== void 0 && Date.now() - this.startTime > timeoutMs) return true;
2987
+ if (idleTimeoutMs !== void 0 && this.idleMs() > idleTimeoutMs) return true;
2988
+ return false;
2770
2989
  }
2771
2990
  usage() {
2772
2991
  return {
@@ -2873,6 +3092,7 @@ function makeAgentSubagentRunner(opts) {
2873
3092
  // Cheap: O(1) per progress event, and the budget short-circuits
2874
3093
  // when timeoutMs is unset (most subagents have one set anyway).
2875
3094
  events.on("tool.progress", () => {
3095
+ ctx.budget.markActivity();
2876
3096
  try {
2877
3097
  ctx.budget.checkTimeout();
2878
3098
  } catch (e) {
@@ -2889,6 +3109,7 @@ function makeAgentSubagentRunner(opts) {
2889
3109
  currentToolName = e.name;
2890
3110
  }),
2891
3111
  events.on("provider.text_delta", (e) => {
3112
+ ctx.budget.markActivity();
2892
3113
  streamingTextAcc = (streamingTextAcc + e.text).slice(-200);
2893
3114
  })
2894
3115
  );
@@ -5282,6 +5503,7 @@ function providerStatusToCode(status, type) {
5282
5503
  ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.config])
5283
5504
  )
5284
5505
  });
5506
+ var DEFAULT_IDLE_TIMEOUT_MS = 15 * 60 * 1e3;
5285
5507
  var FLEET_ROSTER_BUDGETS = {
5286
5508
  "audit-log": { timeoutMs: 7.5 * 60 * 60 * 1e3, maxIterations: 5e3, maxToolCalls: 15e3 },
5287
5509
  "bug-hunter": { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 },
@@ -5293,7 +5515,7 @@ var FLEET_ROSTER_BUDGETS = {
5293
5515
  )
5294
5516
  };
5295
5517
  var GENERIC_SUBAGENT_BUDGET = {
5296
- timeoutMs: 3 * 60 * 60 * 1e3,
5518
+ idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
5297
5519
  maxIterations: 5e3,
5298
5520
  maxToolCalls: 15e3
5299
5521
  };
@@ -5303,7 +5525,13 @@ function applyRosterBudget(cfg) {
5303
5525
  if (!defaultBudget) return cfg;
5304
5526
  return {
5305
5527
  ...cfg,
5306
- timeoutMs: cfg.timeoutMs ?? defaultBudget.timeoutMs,
5528
+ // Wall-clock cap is opt-in only: forward an explicit `cfg.timeoutMs`, but
5529
+ // do NOT impose the roster's historical multi-hour wall-clock default — it
5530
+ // killed agents that were still actively working. Reaping is idle-based.
5531
+ timeoutMs: cfg.timeoutMs,
5532
+ // Idle window is the default reaper. Resets on activity, so a long-but-
5533
+ // productive run is never killed; only a genuine stall is reaped.
5534
+ idleTimeoutMs: cfg.idleTimeoutMs ?? defaultBudget.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS,
5307
5535
  maxIterations: cfg.maxIterations ?? defaultBudget.maxIterations,
5308
5536
  maxToolCalls: cfg.maxToolCalls ?? defaultBudget.maxToolCalls,
5309
5537
  maxTokens: cfg.maxTokens ?? defaultBudget.maxTokens,
@@ -5863,13 +6091,17 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
5863
6091
  const rawMaxTokens = subagent.config.maxTokens;
5864
6092
  const rawMaxCostUsd = subagent.config.maxCostUsd;
5865
6093
  const rawTimeoutMs = subagent.config.timeoutMs;
6094
+ const rawIdleTimeoutMs = subagent.config.idleTimeoutMs;
5866
6095
  const configWithRosterDefaults = applyRosterBudget(subagent.config);
5867
6096
  const budget = new SubagentBudget({
5868
6097
  maxIterations: rawMaxIterations ?? this.config.defaultBudget?.maxIterations ?? configWithRosterDefaults.maxIterations,
5869
6098
  maxToolCalls: rawMaxToolCalls ?? this.config.defaultBudget?.maxToolCalls ?? configWithRosterDefaults.maxToolCalls,
5870
6099
  maxTokens: rawMaxTokens ?? this.config.defaultBudget?.maxTokens ?? configWithRosterDefaults.maxTokens,
5871
6100
  maxCostUsd: rawMaxCostUsd ?? this.config.defaultBudget?.maxCostUsd ?? configWithRosterDefaults.maxCostUsd,
5872
- timeoutMs: rawTimeoutMs ?? this.config.defaultBudget?.timeoutMs ?? configWithRosterDefaults.timeoutMs
6101
+ // Wall-clock cap is opt-in (explicit config / defaultBudget only); the
6102
+ // roster no longer supplies one. Idle is the default reaper.
6103
+ timeoutMs: rawTimeoutMs ?? this.config.defaultBudget?.timeoutMs ?? configWithRosterDefaults.timeoutMs,
6104
+ idleTimeoutMs: rawIdleTimeoutMs ?? this.config.defaultBudget?.idleTimeoutMs ?? configWithRosterDefaults.idleTimeoutMs
5873
6105
  });
5874
6106
  subagent.activeBudget = budget;
5875
6107
  if (!this.runner) {
@@ -5916,63 +6148,79 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
5916
6148
  }
5917
6149
  async executeWithTimeout(runner, task, ctx, budget) {
5918
6150
  const initialTimeoutMs = budget.limits.timeoutMs;
5919
- if (initialTimeoutMs === void 0) return runner(task, ctx);
6151
+ const idleLimitMs = budget.limits.idleTimeoutMs;
6152
+ if (initialTimeoutMs === void 0 && idleLimitMs === void 0) {
6153
+ return runner(task, ctx);
6154
+ }
5920
6155
  const start = Date.now();
5921
6156
  let timer = null;
5922
6157
  const timeoutPromise = new Promise((_, reject) => {
5923
6158
  const armFor = (ms) => {
5924
6159
  if (timer) clearTimeout(timer);
5925
- timer = setTimeout(async () => {
5926
- const elapsed = Date.now() - start;
5927
- const limit = budget.limits.timeoutMs ?? initialTimeoutMs;
5928
- if (!budget.onThreshold) {
5929
- this.subagents.get(ctx.subagentId)?.abortController.abort();
5930
- reject(new BudgetExceededError("timeout", limit, elapsed));
6160
+ timer = setTimeout(onTick, Math.max(0, ms));
6161
+ };
6162
+ const scheduleNext = () => {
6163
+ const wallRemaining = initialTimeoutMs === void 0 ? Number.POSITIVE_INFINITY : (budget.limits.timeoutMs ?? initialTimeoutMs) - (Date.now() - start);
6164
+ const idleRemaining = idleLimitMs === void 0 ? Number.POSITIVE_INFINITY : (budget.limits.idleTimeoutMs ?? idleLimitMs) - budget.idleMs();
6165
+ armFor(Math.max(25, Math.min(wallRemaining, idleRemaining)));
6166
+ };
6167
+ const onTick = async () => {
6168
+ const elapsed = Date.now() - start;
6169
+ const wallLimit = initialTimeoutMs === void 0 ? void 0 : budget.limits.timeoutMs ?? initialTimeoutMs;
6170
+ const idleLimit = idleLimitMs === void 0 ? void 0 : budget.limits.idleTimeoutMs ?? idleLimitMs;
6171
+ const wallExceeded = wallLimit !== void 0 && elapsed >= wallLimit;
6172
+ const idleExceeded = idleLimit !== void 0 && budget.idleMs() >= idleLimit;
6173
+ if (idleExceeded && !wallExceeded) {
6174
+ this.subagents.get(ctx.subagentId)?.abortController.abort();
6175
+ reject(new BudgetExceededError("timeout", idleLimit, budget.idleMs()));
6176
+ return;
6177
+ }
6178
+ if (!wallExceeded) {
6179
+ scheduleNext();
6180
+ return;
6181
+ }
6182
+ const limit = wallLimit;
6183
+ if (!budget.onThreshold) {
6184
+ this.subagents.get(ctx.subagentId)?.abortController.abort();
6185
+ reject(new BudgetExceededError("timeout", limit, elapsed));
6186
+ return;
6187
+ }
6188
+ try {
6189
+ const result = budget.onThreshold({
6190
+ kind: "timeout",
6191
+ used: elapsed,
6192
+ limit,
6193
+ requestDecision: () => new Promise((resolveDecision) => {
6194
+ budget._events?.emit("budget.threshold_reached", {
6195
+ kind: "timeout",
6196
+ used: elapsed,
6197
+ limit,
6198
+ timeoutMs: 6e4,
6199
+ extend: (extra) => resolveDecision({ extend: extra }),
6200
+ deny: () => resolveDecision("stop")
6201
+ });
6202
+ })
6203
+ });
6204
+ const decision = typeof result === "string" ? result : await result;
6205
+ if (decision === "continue" || decision === "throw" || decision === "stop") {
6206
+ armFor(Math.max(1e3, limit));
5931
6207
  return;
5932
6208
  }
5933
- try {
5934
- const result = budget.onThreshold({
5935
- kind: "timeout",
5936
- used: elapsed,
5937
- limit,
5938
- requestDecision: () => new Promise((resolveDecision) => {
5939
- budget._events?.emit("budget.threshold_reached", {
5940
- kind: "timeout",
5941
- used: elapsed,
5942
- limit,
5943
- timeoutMs: 6e4,
5944
- extend: (extra) => resolveDecision({ extend: extra }),
5945
- deny: () => resolveDecision("stop")
5946
- });
5947
- })
5948
- });
5949
- const decision = typeof result === "string" ? result : await result;
5950
- if (decision === "continue") {
5951
- armFor(Math.max(1e3, limit));
5952
- return;
5953
- }
5954
- if (decision === "throw" || decision === "stop") {
5955
- armFor(Math.max(1e3, limit));
5956
- return;
5957
- }
5958
- if (decision.extend.timeoutMs !== void 0) {
5959
- budget.limits.timeoutMs = decision.extend.timeoutMs;
5960
- const newLimit = decision.extend.timeoutMs;
5961
- const remaining = Math.max(1e3, newLimit - elapsed);
5962
- armFor(remaining);
5963
- return;
5964
- }
5965
- this.subagents.get(ctx.subagentId)?.abortController.abort();
5966
- reject(new BudgetExceededError("timeout", limit, elapsed));
5967
- } catch (err) {
5968
- this.subagents.get(ctx.subagentId)?.abortController.abort();
5969
- reject(
5970
- err instanceof BudgetExceededError ? err : new BudgetExceededError("timeout", limit, elapsed)
5971
- );
6209
+ if (decision.extend.timeoutMs !== void 0) {
6210
+ budget.limits.timeoutMs = decision.extend.timeoutMs;
6211
+ scheduleNext();
6212
+ return;
5972
6213
  }
5973
- }, ms);
6214
+ this.subagents.get(ctx.subagentId)?.abortController.abort();
6215
+ reject(new BudgetExceededError("timeout", limit, elapsed));
6216
+ } catch (err) {
6217
+ this.subagents.get(ctx.subagentId)?.abortController.abort();
6218
+ reject(
6219
+ err instanceof BudgetExceededError ? err : new BudgetExceededError("timeout", limit, elapsed)
6220
+ );
6221
+ }
5974
6222
  };
5975
- armFor(initialTimeoutMs);
6223
+ scheduleNext();
5976
6224
  });
5977
6225
  try {
5978
6226
  return await Promise.race([runner(task, ctx), timeoutPromise]);