@wrongstack/core 0.289.0 → 0.291.1

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 (177) hide show
  1. package/dist/boot.d.ts.map +1 -1
  2. package/dist/chronicle/domain-adapter.d.ts.map +1 -1
  3. package/dist/chronicle/index.d.ts +1 -1
  4. package/dist/chronicle/index.d.ts.map +1 -1
  5. package/dist/chronicle/index.js +964 -245
  6. package/dist/chronicle/index.js.map +3 -3
  7. package/dist/chronicle/journal.d.ts +35 -3
  8. package/dist/chronicle/journal.d.ts.map +1 -1
  9. package/dist/chronicle/query.d.ts +8 -5
  10. package/dist/chronicle/query.d.ts.map +1 -1
  11. package/dist/chronicle/tool-adapter.d.ts.map +1 -1
  12. package/dist/coordination/agent-subagent-runner.d.ts +6 -0
  13. package/dist/coordination/agent-subagent-runner.d.ts.map +1 -1
  14. package/dist/coordination/agents/phase6-domain.d.ts.map +1 -1
  15. package/dist/coordination/brain.d.ts +2 -1
  16. package/dist/coordination/brain.d.ts.map +1 -1
  17. package/dist/coordination/collab-debug.d.ts +0 -9
  18. package/dist/coordination/collab-debug.d.ts.map +1 -1
  19. package/dist/coordination/coordinator/error-classifier.d.ts.map +1 -1
  20. package/dist/coordination/dep-watcher.d.ts +2 -1
  21. package/dist/coordination/dep-watcher.d.ts.map +1 -1
  22. package/dist/coordination/director/director-collab.d.ts +1 -1
  23. package/dist/coordination/director/director-collab.d.ts.map +1 -1
  24. package/dist/coordination/director-tools.d.ts.map +1 -1
  25. package/dist/coordination/director.d.ts +19 -1
  26. package/dist/coordination/director.d.ts.map +1 -1
  27. package/dist/coordination/fleet-event-validation.d.ts +0 -7
  28. package/dist/coordination/fleet-event-validation.d.ts.map +1 -1
  29. package/dist/coordination/fleet-spawn.d.ts.map +1 -1
  30. package/dist/coordination/fleet-supervisor.d.ts +1 -1
  31. package/dist/coordination/fleet-supervisor.d.ts.map +1 -1
  32. package/dist/coordination/fleet.d.ts +0 -44
  33. package/dist/coordination/fleet.d.ts.map +1 -1
  34. package/dist/coordination/index.d.ts +2 -2
  35. package/dist/coordination/index.d.ts.map +1 -1
  36. package/dist/coordination/index.js +733 -111
  37. package/dist/coordination/index.js.map +4 -4
  38. package/dist/coordination/knowledge-graph.d.ts +2 -1
  39. package/dist/coordination/knowledge-graph.d.ts.map +1 -1
  40. package/dist/coordination/large-answer-store.d.ts +0 -6
  41. package/dist/coordination/large-answer-store.d.ts.map +1 -1
  42. package/dist/coordination/mail-tools.d.ts.map +1 -1
  43. package/dist/coordination/mailbox-http-router.d.ts.map +1 -1
  44. package/dist/coordination/mailbox-message-codec.d.ts +35 -0
  45. package/dist/coordination/mailbox-message-codec.d.ts.map +1 -1
  46. package/dist/coordination/mailbox-tool.d.ts +1 -1
  47. package/dist/coordination/mailbox-tool.d.ts.map +1 -1
  48. package/dist/coordination/mailbox-types.d.ts +151 -0
  49. package/dist/coordination/mailbox-types.d.ts.map +1 -1
  50. package/dist/coordination/provider-status-tracker.d.ts +27 -2
  51. package/dist/coordination/provider-status-tracker.d.ts.map +1 -1
  52. package/dist/coordination/subagent-budget.d.ts +2 -1
  53. package/dist/coordination/subagent-budget.d.ts.map +1 -1
  54. package/dist/core/agent-response.d.ts +2 -1
  55. package/dist/core/agent-response.d.ts.map +1 -1
  56. package/dist/core/agent-tools.d.ts.map +1 -1
  57. package/dist/core/context.d.ts +51 -0
  58. package/dist/core/context.d.ts.map +1 -1
  59. package/dist/core/fallback-model.d.ts +1 -1
  60. package/dist/core/fallback-model.d.ts.map +1 -1
  61. package/dist/core/fallback-profile-manager.d.ts +1 -1
  62. package/dist/core/fallback-profile-manager.d.ts.map +1 -1
  63. package/dist/core/fleet-pulse.d.ts +2 -1
  64. package/dist/core/fleet-pulse.d.ts.map +1 -1
  65. package/dist/core/iteration-limit.d.ts +2 -1
  66. package/dist/core/iteration-limit.d.ts.map +1 -1
  67. package/dist/core/mailbox-loop.d.ts +11 -1
  68. package/dist/core/mailbox-loop.d.ts.map +1 -1
  69. package/dist/core/model-availability-calendar.d.ts +27 -0
  70. package/dist/core/model-availability-calendar.d.ts.map +1 -0
  71. package/dist/core/system-prompt-builder.d.ts +2 -1
  72. package/dist/core/system-prompt-builder.d.ts.map +1 -1
  73. package/dist/defaults/index.js +1459 -560
  74. package/dist/defaults/index.js.map +4 -4
  75. package/dist/execution/auto-compaction-middleware.d.ts +3 -2
  76. package/dist/execution/auto-compaction-middleware.d.ts.map +1 -1
  77. package/dist/execution/enhance-recovery.d.ts +8 -0
  78. package/dist/execution/enhance-recovery.d.ts.map +1 -1
  79. package/dist/execution/error-handler.d.ts +2 -2
  80. package/dist/execution/error-handler.d.ts.map +1 -1
  81. package/dist/execution/index.js +643 -134
  82. package/dist/execution/index.js.map +4 -4
  83. package/dist/execution/intelligent-compactor.d.ts +0 -16
  84. package/dist/execution/intelligent-compactor.d.ts.map +1 -1
  85. package/dist/execution/one-shot-llm.d.ts.map +1 -1
  86. package/dist/execution/retry-policy.d.ts +0 -13
  87. package/dist/execution/retry-policy.d.ts.map +1 -1
  88. package/dist/execution/skill-loader.d.ts.map +1 -1
  89. package/dist/execution/tool-executor.d.ts.map +1 -1
  90. package/dist/hq/auth-store.d.ts.map +1 -1
  91. package/dist/hq/index.js +39 -1
  92. package/dist/hq/index.js.map +3 -3
  93. package/dist/index.d.ts +32 -30
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/index.js +22263 -19999
  96. package/dist/index.js.map +4 -4
  97. package/dist/infrastructure/index.js +59 -5
  98. package/dist/infrastructure/index.js.map +2 -2
  99. package/dist/kernel/events/agent-events.d.ts +2 -0
  100. package/dist/kernel/events/agent-events.d.ts.map +1 -1
  101. package/dist/kernel/events/file-events.d.ts +46 -0
  102. package/dist/kernel/events/file-events.d.ts.map +1 -1
  103. package/dist/kernel/events/memory-events.d.ts +9 -0
  104. package/dist/kernel/events/memory-events.d.ts.map +1 -1
  105. package/dist/kernel/events/provider-events.d.ts +2 -1
  106. package/dist/kernel/events/provider-events.d.ts.map +1 -1
  107. package/dist/kernel/events/tool-events.d.ts +25 -0
  108. package/dist/kernel/events/tool-events.d.ts.map +1 -1
  109. package/dist/mailbox-attach.d.ts.map +1 -1
  110. package/dist/plugins/auto-review-plugin.d.ts +10 -0
  111. package/dist/plugins/auto-review-plugin.d.ts.map +1 -1
  112. package/dist/plugins/chimera-plugin.d.ts +5 -0
  113. package/dist/plugins/chimera-plugin.d.ts.map +1 -1
  114. package/dist/security/index.d.ts +1 -0
  115. package/dist/security/index.d.ts.map +1 -1
  116. package/dist/security/index.js +317 -9
  117. package/dist/security/index.js.map +4 -4
  118. package/dist/security/kanban-boundary.d.ts +10 -0
  119. package/dist/security/kanban-boundary.d.ts.map +1 -0
  120. package/dist/security/permission-policy.d.ts +8 -1
  121. package/dist/security/permission-policy.d.ts.map +1 -1
  122. package/dist/storage/config-loader.d.ts +15 -0
  123. package/dist/storage/config-loader.d.ts.map +1 -1
  124. package/dist/storage/index.d.ts +2 -2
  125. package/dist/storage/index.d.ts.map +1 -1
  126. package/dist/storage/index.js +627 -428
  127. package/dist/storage/index.js.map +4 -4
  128. package/dist/storage/memory-consolidator.d.ts +49 -4
  129. package/dist/storage/memory-consolidator.d.ts.map +1 -1
  130. package/dist/storage/memory-graph-backend.d.ts +11 -0
  131. package/dist/storage/memory-graph-backend.d.ts.map +1 -1
  132. package/dist/storage/provider-config-watcher.d.ts +17 -0
  133. package/dist/storage/provider-config-watcher.d.ts.map +1 -1
  134. package/dist/storage/session-event-bridge.d.ts +2 -2
  135. package/dist/storage/session-event-bridge.d.ts.map +1 -1
  136. package/dist/tools/fallback-manage-tools.d.ts.map +1 -1
  137. package/dist/tools/index.js +166 -26
  138. package/dist/tools/index.js.map +3 -3
  139. package/dist/types/config.d.ts +12 -4
  140. package/dist/types/config.d.ts.map +1 -1
  141. package/dist/types/file-event-record.d.ts +68 -0
  142. package/dist/types/file-event-record.d.ts.map +1 -0
  143. package/dist/types/index.d.ts +1 -0
  144. package/dist/types/index.d.ts.map +1 -1
  145. package/dist/types/index.js +13 -8
  146. package/dist/types/index.js.map +3 -3
  147. package/dist/types/permission.d.ts +38 -0
  148. package/dist/types/permission.d.ts.map +1 -1
  149. package/dist/types/provider.d.ts +1 -1
  150. package/dist/types/provider.d.ts.map +1 -1
  151. package/dist/types/session.d.ts +27 -0
  152. package/dist/types/session.d.ts.map +1 -1
  153. package/dist/types/tool-executor.d.ts +2 -0
  154. package/dist/types/tool-executor.d.ts.map +1 -1
  155. package/dist/utils/config-backup.d.ts +20 -0
  156. package/dist/utils/config-backup.d.ts.map +1 -0
  157. package/dist/utils/index.d.ts +12 -11
  158. package/dist/utils/index.d.ts.map +1 -1
  159. package/dist/utils/index.js +436 -290
  160. package/dist/utils/index.js.map +4 -4
  161. package/dist/utils/message-invariants.d.ts +12 -0
  162. package/dist/utils/message-invariants.d.ts.map +1 -1
  163. package/dist/utils/safe-json.d.ts +1 -1
  164. package/dist/utils/safe-json.d.ts.map +1 -1
  165. package/dist/utils/term.d.ts +6 -0
  166. package/dist/utils/term.d.ts.map +1 -1
  167. package/dist/utils/tool-wire-compact.d.ts +9 -0
  168. package/dist/utils/tool-wire-compact.d.ts.map +1 -1
  169. package/dist/utils/wstack-paths.d.ts +21 -1
  170. package/dist/utils/wstack-paths.d.ts.map +1 -1
  171. package/instructions/agents/ios.md +113 -0
  172. package/instructions/llm/memory-consolidator.md +4 -12
  173. package/instructions/system.md +4 -1
  174. package/package.json +3 -3
  175. package/skills/auto-review/SKILL.md +176 -0
  176. package/skills/mnemosyne/SKILL.md +156 -0
  177. package/skills/mnemosyne/instructions/agent-prompt.md +200 -0
@@ -542,14 +542,28 @@ function mapContent(msg, fn) {
542
542
  }
543
543
  return { ...msg, content: next };
544
544
  }
545
+ function hasMeaningfulContent(content) {
546
+ if (typeof content === "string") return content.trim().length > 0;
547
+ for (const block of content) {
548
+ if (block.type === "text") {
549
+ if (block.text.trim().length > 0) return true;
550
+ continue;
551
+ }
552
+ if (block.type === "thinking") {
553
+ if (block.thinking.trim().length > 0 || block.signature) return true;
554
+ continue;
555
+ }
556
+ return true;
557
+ }
558
+ return false;
559
+ }
545
560
  function isEmptyMessage(msg) {
546
- if (typeof msg.content === "string") return msg.content.trim().length === 0;
547
- return msg.content.length === 0;
561
+ return !hasMeaningfulContent(msg.content);
548
562
  }
549
563
 
550
564
  // src/utils/safe-json.ts
551
565
  function safeParse(input, maxBytes = 5e6) {
552
- if (input.length > maxBytes) {
566
+ if (Buffer.byteLength(input, "utf8") > maxBytes) {
553
567
  return { ok: false, error: `Input exceeds limit (${maxBytes} bytes)` };
554
568
  }
555
569
  try {
@@ -605,6 +619,11 @@ function invalid(sessionId) {
605
619
  });
606
620
  }
607
621
 
622
+ // src/utils/string.ts
623
+ function truncate(s, max) {
624
+ return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
625
+ }
626
+
608
627
  // src/utils/ulid.ts
609
628
  import { randomBytes as randomBytes2 } from "node:crypto";
610
629
  var ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
@@ -633,11 +652,6 @@ function ulid(seedTime = Date.now()) {
633
652
  return encodeTime(seedTime, TIME_LEN) + encodeRandom(RANDOM_LEN);
634
653
  }
635
654
 
636
- // src/utils/string.ts
637
- function truncate(s, max) {
638
- return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
639
- }
640
-
641
655
  // src/utils/wstack-paths.ts
642
656
  import { createHash } from "node:crypto";
643
657
  import * as fs2 from "node:fs";
@@ -1643,8 +1657,34 @@ function makeAgentSubagentRunner(opts) {
1643
1657
  const taskStartedAt = Date.now();
1644
1658
  const factoryResult = await opts.factory(ctx.config, task);
1645
1659
  const { agent, events } = factoryResult;
1660
+ const taskContext = task.context ?? {};
1661
+ const hasExplicitTelemetryTask = typeof taskContext["telemetryTaskId"] === "string";
1662
+ const telemetryTaskId = hasExplicitTelemetryTask ? taskContext["telemetryTaskId"] : task.id;
1663
+ const telemetryRunId = typeof taskContext["telemetryRunId"] === "string" ? taskContext["telemetryRunId"] : void 0;
1664
+ const telemetryBoardId = typeof taskContext["telemetryBoardId"] === "string" ? taskContext["telemetryBoardId"] : void 0;
1646
1665
  const agentContext = agent.ctx;
1647
- if (agentContext?.meta) agentContext.meta["subagentTaskId"] = task.id;
1666
+ if (agentContext?.meta) {
1667
+ agentContext.meta["subagentTaskId"] = task.id;
1668
+ const kanbanContext = taskContext["kanban"];
1669
+ if (kanbanContext && typeof kanbanContext === "object") {
1670
+ agentContext.meta["kanban"] = kanbanContext;
1671
+ }
1672
+ if (hasExplicitTelemetryTask) {
1673
+ agentContext.meta["telemetryTaskId"] = telemetryTaskId;
1674
+ if (telemetryRunId) agentContext.meta["telemetryRunId"] = telemetryRunId;
1675
+ }
1676
+ }
1677
+ if (hasExplicitTelemetryTask) {
1678
+ agentContext?.setCurrentKanbanTask?.(telemetryTaskId, telemetryBoardId);
1679
+ } else {
1680
+ const kanbanContext = taskContext["kanban"];
1681
+ if (kanbanContext && typeof kanbanContext === "object") {
1682
+ const kanban = kanbanContext;
1683
+ const boardId = typeof kanban["boardId"] === "string" ? kanban["boardId"] : void 0;
1684
+ const taskId = typeof kanban["taskId"] === "string" ? kanban["taskId"] : void 0;
1685
+ if (taskId) agentContext?.setCurrentKanbanTask?.(taskId, boardId);
1686
+ }
1687
+ }
1648
1688
  const detachFleet = opts.fleetBus?.attach(ctx.subagentId, events, task.id);
1649
1689
  const aborter = new AbortController();
1650
1690
  ctx.budget._events = events;
@@ -1669,6 +1709,27 @@ function makeAgentSubagentRunner(opts) {
1669
1709
  const unsub = [];
1670
1710
  unsub.push(
1671
1711
  events.on("tool.executed", (e) => {
1712
+ if (hasExplicitTelemetryTask) {
1713
+ opts.hostEvents?.emit("subagent.tool_executed", {
1714
+ sessionId: e.sessionId,
1715
+ subagentId: ctx.subagentId,
1716
+ agentSessionId: e.sessionId,
1717
+ agentName: e.agentName ?? ctx.config.name,
1718
+ taskId: telemetryTaskId,
1719
+ runId: telemetryRunId,
1720
+ traceId: e.traceId,
1721
+ id: e.id,
1722
+ name: e.name,
1723
+ durationMs: e.durationMs,
1724
+ ok: e.ok,
1725
+ // Raw tool inputs may contain credentials or sensitive arguments.
1726
+ // Task telemetry crosses from the isolated worker bus to the shared
1727
+ // host bus, so expose only execution metadata at this boundary.
1728
+ outputBytes: e.outputBytes,
1729
+ outputTokens: e.outputTokens,
1730
+ outputLines: e.outputLines
1731
+ });
1732
+ }
1672
1733
  try {
1673
1734
  ctx.budget.recordToolCall();
1674
1735
  } catch (eb) {
@@ -1680,6 +1741,17 @@ function makeAgentSubagentRunner(opts) {
1680
1741
  lastToolFailed = null;
1681
1742
  }
1682
1743
  }),
1744
+ events.on("file.event", (e) => {
1745
+ if (!hasExplicitTelemetryTask) return;
1746
+ if (e.runId === telemetryRunId && e.taskId === telemetryTaskId) return;
1747
+ opts.hostEvents?.emit("file.event", {
1748
+ ...e,
1749
+ scope: "task",
1750
+ taskId: telemetryTaskId,
1751
+ boardId: telemetryBoardId,
1752
+ runId: telemetryRunId
1753
+ });
1754
+ }),
1683
1755
  events.on("provider.response", (e) => {
1684
1756
  try {
1685
1757
  ctx.budget.recordUsage(e.usage);
@@ -2973,6 +3045,40 @@ var DOMAIN_AGENTS = [
2973
3045
  "information architecture"
2974
3046
  ]
2975
3047
  }
3048
+ },
3049
+ {
3050
+ config: {
3051
+ id: "ios",
3052
+ name: "iOS",
3053
+ role: "ios",
3054
+ tools: [...TOOLS.build, "fetch"],
3055
+ prompt: agentPrompt("ios")
3056
+ },
3057
+ budget: HEAVY_BUDGET,
3058
+ capability: {
3059
+ phase: "domain",
3060
+ summary: "Apple-platform app development in Swift against the latest Xcode/iOS-SDK line: SwiftUI, UIKit, SwiftData, concurrency, accessibility, and App Store submission.",
3061
+ keywords: [
3062
+ "ios",
3063
+ "iphone",
3064
+ "ipad",
3065
+ "ipados",
3066
+ "watchos",
3067
+ "tvos",
3068
+ "visionos",
3069
+ "macos",
3070
+ "swift",
3071
+ "swiftui",
3072
+ "uikit",
3073
+ "xcode",
3074
+ "swiftdata",
3075
+ "app store",
3076
+ "app intents",
3077
+ "swift package manager",
3078
+ "cocoapods",
3079
+ "foundation models"
3080
+ ]
3081
+ }
2976
3082
  }
2977
3083
  ];
2978
3084
 
@@ -6218,6 +6324,8 @@ function rosterSummaryFromConfigs(roster) {
6218
6324
  import { randomUUID as randomUUID5 } from "node:crypto";
6219
6325
  import {
6220
6326
  claimReadyTask,
6327
+ describeKanbanBoundary,
6328
+ heartbeatTaskAssignment,
6221
6329
  listReadyTasks,
6222
6330
  updateTaskAssignment
6223
6331
  } from "@wrongstack/kanban";
@@ -6972,13 +7080,19 @@ function makeKanbanQueueTool(director, roster) {
6972
7080
  const projectRoot = ctx.projectRoot;
6973
7081
  if (!projectRoot) return { error: "kanban_queue requires ctx.projectRoot." };
6974
7082
  const maxTasks = Math.max(1, Math.min(20, Math.floor(i.maxTasks ?? 1)));
6975
- const leaseTtlMs = Math.max(1e3, Math.floor(i.leaseTtlMs ?? 5 * 60 * 1e3));
7083
+ const baseLeaseTtlMs = Math.max(1e3, Math.floor(i.leaseTtlMs ?? 5 * 60 * 1e3));
7084
+ const leaseTtlMs = i.awaitCompletion === true && i.timeoutMs !== void 0 ? Math.max(baseLeaseTtlMs, i.timeoutMs + 6e4) : baseLeaseTtlMs;
7085
+ const heartbeatIntervalMs = Math.max(
7086
+ 5e3,
7087
+ i.heartbeatIntervalMs ?? Math.floor(leaseTtlMs / 2)
7088
+ );
7089
+ const effectiveLeaseTtlMs = Math.max(leaseTtlMs, heartbeatIntervalMs * 2);
6976
7090
  const claimedAt = nowIso();
6977
7091
  const leaseSeeding = {
6978
7092
  leaseId: randomUUID5(),
6979
7093
  claimedAt,
6980
7094
  heartbeatAt: claimedAt,
6981
- leaseExpiresAt: new Date(Date.now() + leaseTtlMs).toISOString()
7095
+ leaseExpiresAt: new Date(Date.now() + effectiveLeaseTtlMs).toISOString()
6982
7096
  };
6983
7097
  const candidateTaskIds = i.taskId !== void 0 ? [i.taskId] : i.query ? (await listReadyTasks(projectRoot, {
6984
7098
  ...i.boardId !== void 0 ? { boardId: i.boardId } : {}
@@ -6986,9 +7100,11 @@ function makeKanbanQueueTool(director, roster) {
6986
7100
  const dispatches = [];
6987
7101
  const errors = [];
6988
7102
  const resultFailures = [];
7103
+ const budgetRejectedTaskIds = /* @__PURE__ */ new Set();
6989
7104
  for (let index = 0; index < maxTasks; index++) {
6990
7105
  const candidateTaskId = candidateTaskIds?.[index];
6991
7106
  if (candidateTaskIds && !candidateTaskId) break;
7107
+ if (candidateTaskId && budgetRejectedTaskIds.has(candidateTaskId)) continue;
6992
7108
  const claim = await claimReadyTask(projectRoot, {
6993
7109
  ...i.boardId !== void 0 ? { boardId: i.boardId } : {},
6994
7110
  ...candidateTaskId !== void 0 ? { taskId: candidateTaskId } : {},
@@ -7013,14 +7129,22 @@ function makeKanbanQueueTool(director, roster) {
7013
7129
  if (costCeiling !== void 0) {
7014
7130
  const remaining = director.getRemainingBudgetUsd();
7015
7131
  if (remaining !== void 0 && remaining < costCeiling) {
7016
- await updateTaskAssignment(projectRoot, claim.board.id, claim.task.id, {
7017
- status: "failed",
7018
- error: `Cost ceiling ${costCeiling} exceeds remaining budget ${remaining.toFixed(4)}`,
7019
- lastResult: "Skipped by kanban_queue cost gate (Sprint 3)"
7020
- });
7132
+ budgetRejectedTaskIds.add(claim.task.id);
7133
+ const budgetError = `Cost ceiling ${costCeiling} exceeds remaining budget ${remaining.toFixed(4)}`;
7134
+ await updateTaskAssignment(
7135
+ projectRoot,
7136
+ claim.board.id,
7137
+ claim.task.id,
7138
+ {
7139
+ ...claim.task.assignment ?? {},
7140
+ status: "failed",
7141
+ error: budgetError
7142
+ },
7143
+ { expectedLeaseId: leaseSeeding.leaseId }
7144
+ );
7021
7145
  errors.push({
7022
7146
  taskId: claim.task.id,
7023
- error: `Cost ceiling ${costCeiling} exceeds remaining budget ${remaining.toFixed(4)}`
7147
+ error: budgetError
7024
7148
  });
7025
7149
  continue;
7026
7150
  }
@@ -7028,12 +7152,20 @@ function makeKanbanQueueTool(director, roster) {
7028
7152
  try {
7029
7153
  const config = buildKanbanSubagentConfig(claim.task, i, roster);
7030
7154
  subagentId = await director.spawn(config);
7031
- runTaskId = await director.assign({
7032
- id: randomUUID5(),
7155
+ const dispatchTaskId = randomUUID5();
7156
+ const taskSpec = {
7157
+ id: dispatchTaskId,
7033
7158
  subagentId,
7034
7159
  description: buildKanbanFleetTaskPrompt(claim.board, claim.task, {
7035
- heartbeatIntervalMs: i.heartbeatIntervalMs ?? 6e4,
7036
- leaseTtlMs,
7160
+ // The worker must heartbeat against the lease it actually holds.
7161
+ // effectiveLeaseTtlMs is the real lease window (raised from the
7162
+ // requested TTL when needed so the first heartbeat always lands
7163
+ // before expiry), and the computed heartbeatIntervalMs is always
7164
+ // strictly shorter than that window. Passing the raw requested
7165
+ // values here would give the worker a 60s cadence against a
7166
+ // 10s lease, letting the lease expire before its first renewal.
7167
+ heartbeatIntervalMs,
7168
+ leaseTtlMs: effectiveLeaseTtlMs,
7037
7169
  leaseId: leaseSeeding.leaseId,
7038
7170
  leaseExpiresAt: leaseSeeding.leaseExpiresAt
7039
7171
  }),
@@ -7044,16 +7176,51 @@ function makeKanbanQueueTool(director, roster) {
7044
7176
  kanban: {
7045
7177
  boardId: claim.board.id,
7046
7178
  taskId: claim.task.id,
7047
- origin: claim.task.origin
7179
+ origin: claim.task.origin,
7180
+ projectRoot
7048
7181
  }
7049
7182
  }
7050
- });
7051
- await updateTaskAssignment(projectRoot, claim.board.id, claim.task.id, {
7052
- ...claim.task.assignment ?? {},
7053
- status: "running",
7054
- subagentId,
7055
- runTaskId
7056
- });
7183
+ };
7184
+ const dispatched = await updateTaskAssignment(
7185
+ projectRoot,
7186
+ claim.board.id,
7187
+ claim.task.id,
7188
+ {
7189
+ ...claim.task.assignment ?? {},
7190
+ status: "running",
7191
+ subagentId,
7192
+ runTaskId: dispatchTaskId,
7193
+ // Renew the lease from the actual dispatch moment, not from claim
7194
+ // time. The claim-time lease starts before spawn + queueing, so
7195
+ // `timeoutMs + 60_000` can expire while the worker is still
7196
+ // queued or running, letting recover_stale reclaim and duplicate
7197
+ // the assignment (see auto-extend.ts: timeout extensions can push
7198
+ // execution well past the original timeoutMs). Both heartbeatAt
7199
+ // and leaseExpiresAt are stamped from now so the worker has a
7200
+ // full lease window from the moment it was actually dispatched.
7201
+ heartbeatAt: (/* @__PURE__ */ new Date()).toISOString(),
7202
+ leaseExpiresAt: new Date(Date.now() + effectiveLeaseTtlMs).toISOString()
7203
+ },
7204
+ { expectedLeaseId: leaseSeeding.leaseId }
7205
+ );
7206
+ if (dispatched === null) {
7207
+ try {
7208
+ await director.terminate(subagentId);
7209
+ } catch (cleanupErr) {
7210
+ errors.push({
7211
+ taskId: claim.task.id,
7212
+ error: `Lease lost during dispatch; orphan subagent ${subagentId} termination failed: ${toErrorMessage(cleanupErr)}`
7213
+ });
7214
+ continue;
7215
+ }
7216
+ errors.push({
7217
+ taskId: claim.task.id,
7218
+ error: `Lease lost during dispatch (reassigned by recovery); subagent ${subagentId} terminated.`
7219
+ });
7220
+ continue;
7221
+ }
7222
+ await director.assign(taskSpec);
7223
+ runTaskId = dispatchTaskId;
7057
7224
  dispatches.push({
7058
7225
  boardId: claim.board.id,
7059
7226
  taskId: claim.task.id,
@@ -7072,28 +7239,97 @@ function makeKanbanQueueTool(director, roster) {
7072
7239
  message = `${message}; cleanup failed for spawned subagent ${subagentId}: ${toErrorMessage(cleanupErr)}`;
7073
7240
  }
7074
7241
  }
7075
- await updateTaskAssignment(projectRoot, claim.board.id, claim.task.id, {
7076
- ...claim.task.assignment ?? {},
7077
- status: "failed",
7078
- ...subagentId !== void 0 ? { subagentId } : {},
7079
- ...runTaskId !== void 0 ? { runTaskId } : {},
7080
- error: message
7081
- });
7242
+ await updateTaskAssignment(
7243
+ projectRoot,
7244
+ claim.board.id,
7245
+ claim.task.id,
7246
+ {
7247
+ ...claim.task.assignment ?? {},
7248
+ status: "failed",
7249
+ ...subagentId !== void 0 ? { subagentId } : {},
7250
+ ...runTaskId !== void 0 ? { runTaskId } : {},
7251
+ error: message
7252
+ },
7253
+ { expectedLeaseId: leaseSeeding.leaseId }
7254
+ );
7082
7255
  errors.push({ taskId: claim.task.id, error: message });
7083
7256
  }
7084
7257
  }
7258
+ if (!i.awaitCompletion && dispatches.length > 0 && typeof director.fleet?.subscribe === "function") {
7259
+ const ourLeaseId = leaseSeeding.leaseId;
7260
+ const disposers = /* @__PURE__ */ new Map();
7261
+ const renewalStartedAt = Date.now();
7262
+ const maxRenewalWindowMs = 24 * 60 * 6e4;
7263
+ const stopAll = () => {
7264
+ clearInterval(renewalTimer);
7265
+ for (const dispose of disposers.values()) dispose();
7266
+ disposers.clear();
7267
+ };
7268
+ const renewalTimer = setInterval(() => {
7269
+ if (disposers.size === 0 || Date.now() - renewalStartedAt > maxRenewalWindowMs) {
7270
+ stopAll();
7271
+ return;
7272
+ }
7273
+ const refreshedExpiry = new Date(Date.now() + effectiveLeaseTtlMs).toISOString();
7274
+ for (const dispatch of dispatches) {
7275
+ if (!disposers.has(dispatch.runTaskId)) continue;
7276
+ void heartbeatTaskAssignment(projectRoot, dispatch.boardId, dispatch.taskId, {
7277
+ leaseExpiresAt: refreshedExpiry,
7278
+ expectedLeaseId: ourLeaseId
7279
+ }).catch(() => {
7280
+ });
7281
+ }
7282
+ }, heartbeatIntervalMs);
7283
+ renewalTimer.unref?.();
7284
+ for (const dispatch of dispatches) {
7285
+ const dispose = director.fleet.subscribe(dispatch.subagentId, (event) => {
7286
+ if (event.type !== "subagent.completed" && event.type !== "subagent.stopped") return;
7287
+ if (event.type === "subagent.completed" && event.taskId !== dispatch.runTaskId) return;
7288
+ disposers.get(dispatch.runTaskId)?.();
7289
+ disposers.delete(dispatch.runTaskId);
7290
+ if (disposers.size === 0) clearInterval(renewalTimer);
7291
+ });
7292
+ disposers.set(dispatch.runTaskId, dispose);
7293
+ }
7294
+ }
7085
7295
  let results;
7086
7296
  if (i.awaitCompletion && dispatches.length > 0) {
7087
- results = await director.awaitTasks(dispatches.map((dispatch) => dispatch.runTaskId));
7297
+ const ourLeaseId = leaseSeeding.leaseId;
7298
+ const runTaskIds = new Set(dispatches.map((dispatch) => dispatch.runTaskId));
7299
+ const heartbeatTimer = setInterval(async () => {
7300
+ const refreshedExpiry = new Date(Date.now() + effectiveLeaseTtlMs).toISOString();
7301
+ for (const dispatch of dispatches) {
7302
+ if (!runTaskIds.has(dispatch.runTaskId)) continue;
7303
+ try {
7304
+ await heartbeatTaskAssignment(projectRoot, dispatch.boardId, dispatch.taskId, {
7305
+ leaseExpiresAt: refreshedExpiry,
7306
+ expectedLeaseId: ourLeaseId
7307
+ });
7308
+ } catch {
7309
+ }
7310
+ }
7311
+ }, heartbeatIntervalMs);
7312
+ try {
7313
+ results = await director.awaitTasks(dispatches.map((dispatch) => dispatch.runTaskId));
7314
+ } finally {
7315
+ clearInterval(heartbeatTimer);
7316
+ }
7088
7317
  for (const result of results) {
7089
7318
  const dispatch = dispatches.find((candidate) => candidate.runTaskId === result.taskId);
7090
7319
  if (!dispatch) continue;
7091
- await updateTaskAssignment(projectRoot, dispatch.boardId, dispatch.taskId, {
7092
- status: result.status === "success" ? "completed" : "failed",
7093
- subagentId: result.subagentId,
7094
- runTaskId: result.taskId,
7095
- ...result.status === "success" ? { lastResult: resultToText(result) } : { error: resultErrorText(result) }
7096
- });
7320
+ runTaskIds.delete(dispatch.runTaskId);
7321
+ await updateTaskAssignment(
7322
+ projectRoot,
7323
+ dispatch.boardId,
7324
+ dispatch.taskId,
7325
+ {
7326
+ status: result.status === "success" ? "completed" : "failed",
7327
+ subagentId: result.subagentId,
7328
+ runTaskId: result.taskId,
7329
+ ...result.status === "success" ? { lastResult: resultToText(result) } : { error: resultErrorText(result) }
7330
+ },
7331
+ { expectedLeaseId: ourLeaseId }
7332
+ );
7097
7333
  if (result.status !== "success") {
7098
7334
  resultFailures.push({
7099
7335
  taskId: dispatch.taskId,
@@ -7201,6 +7437,16 @@ function buildKanbanFleetTaskPrompt(board, task, lease) {
7201
7437
  task.origin.phaseId ? `phaseId: ${task.origin.phaseId}` : "",
7202
7438
  task.origin.taskId ? `sourceTaskId: ${task.origin.taskId}` : ""
7203
7439
  ].filter(Boolean).join("\n") : "";
7440
+ const boundaries = [
7441
+ board.boundary?.enabled ? `board: ${describeKanbanBoundary(board.boundary)}` : "",
7442
+ task.boundary?.enabled ? `task: ${describeKanbanBoundary(task.boundary)}` : "",
7443
+ ...(board.boundary?.allow ?? []).map(
7444
+ (selector) => `board allow ${selector.access} ${selector.kind}:${selector.path}`
7445
+ ),
7446
+ ...(task.boundary?.allow ?? []).map(
7447
+ (selector) => `task allow ${selector.access} ${selector.kind}:${selector.path}`
7448
+ )
7449
+ ].filter(Boolean);
7204
7450
  return [
7205
7451
  "You are processing a claimed WrongStack Kanban task.",
7206
7452
  "",
@@ -7223,6 +7469,8 @@ ${checks}` : "",
7223
7469
  metrics ? `Goal metrics:
7224
7470
  ${metrics}` : "",
7225
7471
  task.labels?.length ? `Labels: ${task.labels.join(", ")}` : "",
7472
+ boundaries.length ? `BOUNDARY CONTRACT (enforced by the tool runtime; do not attempt to bypass):
7473
+ ${boundaries.map((line) => `- ${line}`).join("\n")}` : "",
7226
7474
  "Lease contract (Sprint 1):",
7227
7475
  `- leaseId: ${lease.leaseId}`,
7228
7476
  `- claimedAt: ${task.assignment?.claimedAt ?? "<unknown>"}`,
@@ -7238,10 +7486,21 @@ ${metrics}` : "",
7238
7486
  "Work this task end-to-end. Treat the board as a live plan, not a frozen assignment snapshot.",
7239
7487
  `Before material work and whenever evidence changes the plan, call kanban with action "get_board" and boardId "${board.id}" to reassess current tasks, dependencies, priorities, and peer changes.`,
7240
7488
  "You may use kanban add_task, update_task, split_task, merge_tasks, move_task, delete_task, or dependency actions when reassessment shows the plan should change. Preserve traceability and do not keep obsolete work merely because it existed at dispatch time.",
7489
+ "",
7490
+ "FIELD MAINTENANCE \u2014 keep every task detail current:",
7491
+ '- Call "update_task" to refresh description, priority, type, labels, or estimatedHours as the work evolves.',
7492
+ '- Call "add_note" with author and content to record progress, decisions, or roadblock context.',
7493
+ '- Call "add_check" / "update_check" with description and status (pending/passed/failed) to track acceptance criteria.',
7494
+ '- Call "add_goal_metric" / "update_goal_metric" with name and current/target to report measurable progress.',
7495
+ '- Call "add_link" with url and type (issue/pr/doc/commit/design/file/url/other) to attach evidence.',
7496
+ '- Call "update_task" to set actualHours when you have a final time estimate.',
7497
+ "Do not leave stale or placeholder field values \u2014 the kanban board is the shared source of truth for every stakeholder.",
7498
+ "",
7499
+ "LEASE & LIFECYCLE:",
7241
7500
  "Every successful board mutation updates shared pending work and notifies the owning session. Re-read the board after mutations and adapt to changes made by other agents; never assume your initial todo list remains authoritative.",
7242
- `When you start, call kanban with action "mark_assignment", boardId "${board.id}", taskId "${task.id}", and assignmentStatus "running". Include subagentId and runTaskId when you have them.`,
7243
- `To stay alive in the queue, call kanban with action "heartbeat_assignment", boardId "${board.id}", taskId "${task.id}", and heartbeatAt set to the current time. Cadence must be <= heartbeatIntervalMs and well before leaseExpiresAt.`,
7244
- `When you finish, call kanban with action "mark_assignment", boardId "${board.id}", taskId "${task.id}", and assignmentStatus "completed" or "failed". Include lastResult or error.`,
7501
+ `When you start, call kanban with action "mark_assignment", boardId "${board.id}", taskId "${task.id}", assignmentStatus "running", and expectedLeaseId "${lease.leaseId}". Include subagentId and runTaskId when you have them. The expectedLeaseId fence makes the write a safe no-op if recover_stale already reassigned the task because your lease expired.`,
7502
+ `To stay alive in the queue, call kanban with action "heartbeat_assignment", boardId "${board.id}", taskId "${task.id}", heartbeatAt set to the current time, and expectedLeaseId "${lease.leaseId}". Cadence must be <= heartbeatIntervalMs and well before leaseExpiresAt. The expectedLeaseId fence makes the heartbeat a safe no-op if the lease was reassigned, so a stale worker cannot renew a successor's lease.`,
7503
+ `When you finish, call kanban with action "mark_assignment", boardId "${board.id}", taskId "${task.id}", assignmentStatus "completed" or "failed", and expectedLeaseId "${lease.leaseId}". Include lastResult or error. Populate every relevant detail field before the terminal mark_assignment so the card's final state is complete.`,
7245
7504
  `If you cannot finish in time, call kanban with action "heartbeat_assignment" to extend the lease, or with action "release_task" to release so another worker can claim. Do NOT silently abandon the assignment.`,
7246
7505
  'On failure the host may call kanban with action "recover_stale" (mode: retry/release/fail); respect its decisions and do not duplicate work in parallel.',
7247
7506
  "When finished, report what changed, what you verified, and any remaining blockers."
@@ -7935,18 +8194,20 @@ function hashStr(s) {
7935
8194
  // src/types/provider.ts
7936
8195
  var CONTEXT_OVERFLOW_RE = /context.length|context.window|max.*tokens?.*exceeded|prompt is too long|too long|exceeds the context|\btokens\b.*exceed|context_length_exceeded/i;
7937
8196
  var CONTENT_FILTER_RE = /content.(filter|policy|moderation)|safety (system|filter)/i;
8197
+ var QUOTA_EXHAUSTED_RE = /(?:insufficient|exhausted|depleted|exceeded|no|not enough)[-_\s]*(?:quota|credit|balance)|(?:quota|credit|balance)[-_\s]*(?:exhausted|depleted|exceeded|insufficient)|billing[_\s-]*(?:hard[_\s-]*)?limit|payment required|spending limit|plan limit/i;
7938
8198
  function classifyProviderError(status, body, message) {
7939
8199
  const type = body?.type;
8200
+ const text = [message, body?.message, type, body?.raw].filter(Boolean).join("\n");
7940
8201
  if (status === 0) return "network";
7941
8202
  if (status === 408) return "timeout";
7942
8203
  if (status === 599) return "stream_hang";
8204
+ if (status === 402 || QUOTA_EXHAUSTED_RE.test(text)) return "quota_exhausted";
7943
8205
  if (type === "rate_limit_error" || status === 429) return "rate_limit";
7944
8206
  if (type === "overloaded_error" || status === 529) return "overloaded";
7945
8207
  if (status >= 500) return "server";
7946
8208
  if (type === "authentication_error" || type === "permission_error" || status === 401 || status === 403) {
7947
8209
  return "auth";
7948
8210
  }
7949
- const text = [message, body?.message, type, body?.raw].filter(Boolean).join("\n");
7950
8211
  if (type === "content_filter" || CONTENT_FILTER_RE.test(text)) return "content_filter";
7951
8212
  if (status === 413 || status >= 400 && CONTEXT_OVERFLOW_RE.test(text)) {
7952
8213
  return "context_overflow";
@@ -8021,6 +8282,7 @@ var KIND_TO_CODE = {
8021
8282
  network: ERROR_CODES.PROVIDER_NETWORK_ERROR,
8022
8283
  timeout: ERROR_CODES.PROVIDER_NETWORK_ERROR,
8023
8284
  rate_limit: ERROR_CODES.PROVIDER_RATE_LIMITED,
8285
+ quota_exhausted: ERROR_CODES.PROVIDER_RATE_LIMITED,
8024
8286
  auth: ERROR_CODES.PROVIDER_AUTH_FAILED,
8025
8287
  overloaded: ERROR_CODES.PROVIDER_OVERLOADED,
8026
8288
  context_overflow: ERROR_CODES.PROVIDER_CONTEXT_OVERFLOW,
@@ -8034,6 +8296,84 @@ function kindToCode(kind) {
8034
8296
  return KIND_TO_CODE[kind];
8035
8297
  }
8036
8298
 
8299
+ // src/core/model-availability-calendar.ts
8300
+ function logicalCalendarTarget(providerId, model) {
8301
+ if (providerId !== "omniroute") return { providerId, model };
8302
+ const slash = model.indexOf("/");
8303
+ return slash > 0 && slash < model.length - 1 ? { providerId: model.slice(0, slash), model: model.slice(slash + 1) } : { providerId, model };
8304
+ }
8305
+ var WEEKDAYS = {
8306
+ Sun: 0,
8307
+ Mon: 1,
8308
+ Tue: 2,
8309
+ Wed: 3,
8310
+ Thu: 4,
8311
+ Fri: 5,
8312
+ Sat: 6
8313
+ };
8314
+ function minuteOfDay(value) {
8315
+ const match = /^(\d{2}):(\d{2})$/.exec(value);
8316
+ if (!match) return void 0;
8317
+ const hour = Number(match[1]);
8318
+ const minute = Number(match[2]);
8319
+ if (hour > 23 || minute > 59) return void 0;
8320
+ return hour * 60 + minute;
8321
+ }
8322
+ function clockAt(date, timezone) {
8323
+ try {
8324
+ const parts = new Intl.DateTimeFormat("en-US", {
8325
+ timeZone: timezone,
8326
+ weekday: "short",
8327
+ hour: "2-digit",
8328
+ minute: "2-digit",
8329
+ hourCycle: "h23"
8330
+ }).formatToParts(date);
8331
+ const weekday = parts.find((part) => part.type === "weekday")?.value;
8332
+ const hour = Number(parts.find((part) => part.type === "hour")?.value);
8333
+ const minute = Number(parts.find((part) => part.type === "minute")?.value);
8334
+ if (!weekday || WEEKDAYS[weekday] === void 0 || !Number.isFinite(hour + minute))
8335
+ return void 0;
8336
+ return { day: WEEKDAYS[weekday], minute: hour * 60 + minute };
8337
+ } catch {
8338
+ return void 0;
8339
+ }
8340
+ }
8341
+ function targetMatches(rule, providerId, model) {
8342
+ if (rule.provider && rule.provider !== providerId) return false;
8343
+ if (rule.model && rule.model !== model) return false;
8344
+ return Boolean(rule.provider || rule.model);
8345
+ }
8346
+ function timeMatches(rule, day, minute) {
8347
+ const start = minuteOfDay(rule.start);
8348
+ const end = minuteOfDay(rule.end);
8349
+ if (start === void 0 || end === void 0) return false;
8350
+ const days = rule.days?.length ? new Set(rule.days) : void 0;
8351
+ if (start === end) return !days || days.has(day);
8352
+ if (start < end) return (!days || days.has(day)) && minute >= start && minute < end;
8353
+ if (minute >= start) return !days || days.has(day);
8354
+ const previousDay = (day + 6) % 7;
8355
+ return minute < end && (!days || days.has(previousDay));
8356
+ }
8357
+ function evaluateModelCalendar(rules, providerId, model, at = /* @__PURE__ */ new Date()) {
8358
+ ({ providerId, model } = logicalCalendarTarget(providerId, model));
8359
+ const allowRules = [];
8360
+ let allowMatched = false;
8361
+ for (const rule of rules ?? []) {
8362
+ if (rule.enabled === false || !targetMatches(rule, providerId, model)) continue;
8363
+ const clock = clockAt(at, rule.timezone);
8364
+ if (!clock || minuteOfDay(rule.start) === void 0 || minuteOfDay(rule.end) === void 0)
8365
+ continue;
8366
+ if (rule.mode === "allow_only") {
8367
+ allowRules.push(rule);
8368
+ if (timeMatches(rule, clock.day, clock.minute)) allowMatched = true;
8369
+ } else if (timeMatches(rule, clock.day, clock.minute)) {
8370
+ return { allowed: false, rule };
8371
+ }
8372
+ }
8373
+ if (allowRules.length > 0 && !allowMatched) return { allowed: false, rule: allowRules[0] };
8374
+ return { allowed: true };
8375
+ }
8376
+
8037
8377
  // src/core/fallback-profile-manager.ts
8038
8378
  function hasText(value) {
8039
8379
  return typeof value === "string" && value.trim().length > 0;
@@ -8042,7 +8382,8 @@ function providerHasKey(entry) {
8042
8382
  if (!entry) return false;
8043
8383
  if (hasText(entry.apiKey)) return true;
8044
8384
  if (Array.isArray(entry.apiKeys) && entry.apiKeys.some((k) => hasText(k?.apiKey))) return true;
8045
- if (Array.isArray(entry.envVars) && entry.envVars.some((v) => hasText(process.env[v]))) return true;
8385
+ if (Array.isArray(entry.envVars) && entry.envVars.some((v) => hasText(process.env[v])))
8386
+ return true;
8046
8387
  return false;
8047
8388
  }
8048
8389
  function visibleProviderModels(config, providerId, providerModels) {
@@ -8114,6 +8455,8 @@ var FallbackProfileManager = class {
8114
8455
  const health = this.checkProvider(providerId);
8115
8456
  if (!health.usable) continue;
8116
8457
  if (this.statusTracker && !this.statusTracker.isAvailable(providerId, parsed.model)) continue;
8458
+ if (!evaluateModelCalendar(this.config.modelAvailabilitySchedule, providerId, parsed.model).allowed)
8459
+ continue;
8117
8460
  const allowedModels = this.config.providers?.[providerId]?.models;
8118
8461
  if (allowedModels && !allowedModels.includes(parsed.model)) continue;
8119
8462
  resolved.push({
@@ -8186,6 +8529,8 @@ var FallbackProfileManager = class {
8186
8529
  seen.add(key);
8187
8530
  if (excludeKey && key === excludeKey) continue;
8188
8531
  if (this.statusTracker && !this.statusTracker.isAvailable(providerId, parsed.model)) continue;
8532
+ if (!evaluateModelCalendar(this.config.modelAvailabilitySchedule, providerId, parsed.model).allowed)
8533
+ continue;
8189
8534
  resolved.push({
8190
8535
  providerId,
8191
8536
  model: parsed.model,
@@ -8230,6 +8575,8 @@ var FallbackProfileManager = class {
8230
8575
  seen.add(ref);
8231
8576
  if (excludeKey && ref === excludeKey) continue;
8232
8577
  if (this.statusTracker && !this.statusTracker.isAvailable(id, model)) continue;
8578
+ if (!evaluateModelCalendar(this.config.modelAvailabilitySchedule, id, model).allowed)
8579
+ continue;
8233
8580
  if (favoriteSet.has(ref)) {
8234
8581
  favorites.push(ref);
8235
8582
  continue;
@@ -8507,6 +8854,7 @@ function classifySubagentError(err, hints = {}) {
8507
8854
  function providerErrorToSubagentError(err, message, cause) {
8508
8855
  switch (err.kind) {
8509
8856
  case "rate_limit":
8857
+ case "quota_exhausted":
8510
8858
  return {
8511
8859
  kind: "provider_rate_limit",
8512
8860
  message,
@@ -9896,6 +10244,10 @@ var Director = class _Director {
9896
10244
  largeAnswerStore;
9897
10245
  /** Shared provider/model status tracker, or undefined. */
9898
10246
  statusTracker;
10247
+ /** Session/leader's provider id — absolute last-resort fallback for every spawn. */
10248
+ sessionProvider;
10249
+ /** Session/leader's model id — paired with sessionProvider above. */
10250
+ sessionModel;
9899
10251
  constructor(opts) {
9900
10252
  this.id = opts.config.coordinatorId || randomUUID7();
9901
10253
  this.brain = opts.brain;
@@ -9936,6 +10288,8 @@ var Director = class _Director {
9936
10288
  this.fleetManager = opts.fleetManager;
9937
10289
  this.statusTracker = opts.statusTracker;
9938
10290
  this.logger = opts.logger;
10291
+ this.sessionProvider = opts.sessionProvider;
10292
+ this.sessionModel = opts.sessionModel;
9939
10293
  if (this.sharedScratchpadPath) {
9940
10294
  void fsp5.mkdir(this.sharedScratchpadPath, { recursive: true }).catch((err) => this.logShutdownError("shared_scratchpad_mkdir", err));
9941
10295
  }
@@ -10444,20 +10798,31 @@ var Director = class _Director {
10444
10798
  return result.subagentId;
10445
10799
  }
10446
10800
  resolveSpawnModel(config) {
10801
+ if (config.provider?.trim() === "") config.provider = void 0;
10802
+ if (config.model?.trim() === "") config.model = void 0;
10447
10803
  if (!config.model && this.modelMatrix) {
10448
10804
  const matrix = typeof this.modelMatrix === "function" ? this.modelMatrix() : this.modelMatrix;
10449
10805
  const resolution = resolveModelMatrixResolution(matrix, config.role);
10450
10806
  const entry = resolution?.source === "default" && roleNeedsIndependentReviewModel(config.role) ? void 0 : resolution?.entry;
10451
- if (entry?.model) {
10452
- config.model = entry.model;
10807
+ if (entry) {
10808
+ if (entry.model) config.model = entry.model;
10453
10809
  if (entry.provider) config.provider = entry.provider;
10454
10810
  if (entry.fallbackProfile) config.fallbackProfile = entry.fallbackProfile;
10455
10811
  if (entry.modelRuntime) config.modelRuntime = entry.modelRuntime;
10456
- } else if (entry?.fallbackProfile) {
10457
- config.fallbackProfile = entry.fallbackProfile;
10458
- if (entry.modelRuntime) config.modelRuntime = entry.modelRuntime;
10459
10812
  }
10460
10813
  }
10814
+ if (!config.provider && this.sessionProvider) {
10815
+ config.provider = this.sessionProvider;
10816
+ this.logger?.info(
10817
+ `spawn: provider="${config.provider}" for role "${config.role ?? "?"}" fell back to session provider (matrix resolution left it undefined)`
10818
+ );
10819
+ }
10820
+ if (!config.model && this.sessionModel) {
10821
+ config.model = this.sessionModel;
10822
+ this.logger?.info(
10823
+ `spawn: model="${config.model}" for role "${config.role ?? "?"}" fell back to session model (matrix resolution left it undefined)`
10824
+ );
10825
+ }
10461
10826
  if (this.statusTracker && config.provider && config.model) {
10462
10827
  if (!this.statusTracker.isAvailable(config.provider, config.model)) {
10463
10828
  this.logger?.warn(
@@ -14456,6 +14821,145 @@ var AUTO_COMPACT_INTERVAL_MS = 3e5;
14456
14821
  var AUTO_COMPACT_READ_MAX_AGE_MS = 6e5;
14457
14822
  var AUTO_COMPACT_DEFAULT_TTL_MS = 864e5;
14458
14823
 
14824
+ // src/coordination/mailbox-types.ts
14825
+ var MAILBOX_TYPE_PROPERTIES = {
14826
+ note: {
14827
+ category: "informational",
14828
+ expectsReply: false,
14829
+ requiresAction: false,
14830
+ backgroundEligible: false,
14831
+ outOfBand: false,
14832
+ renderPriority: 20,
14833
+ recipientObligation: "Read for context; no reply needed.",
14834
+ senderGuidance: "General-purpose FYI. Use when no more specific type applies."
14835
+ },
14836
+ ask: {
14837
+ category: "actionable",
14838
+ expectsReply: true,
14839
+ requiresAction: true,
14840
+ backgroundEligible: true,
14841
+ outOfBand: false,
14842
+ renderPriority: 10,
14843
+ recipientObligation: "Answer as soon as possible \u2014 the sender is waiting.",
14844
+ senderGuidance: "Blocking question. Only use when you need an answer to proceed."
14845
+ },
14846
+ assign: {
14847
+ category: "actionable",
14848
+ expectsReply: false,
14849
+ requiresAction: true,
14850
+ backgroundEligible: true,
14851
+ outOfBand: false,
14852
+ renderPriority: 10,
14853
+ recipientObligation: "Accept or decline; act on it when current operation allows.",
14854
+ senderGuidance: 'Task delegation. Must be directed to a specific recipient (not "*").'
14855
+ },
14856
+ steer: {
14857
+ category: "actionable",
14858
+ expectsReply: false,
14859
+ requiresAction: true,
14860
+ backgroundEligible: true,
14861
+ outOfBand: false,
14862
+ renderPriority: 0,
14863
+ // Always rendered first
14864
+ recipientObligation: "Pause current approach, adjust per instruction, then resume.",
14865
+ senderGuidance: "Mid-task direction change. The recipient is already working on something."
14866
+ },
14867
+ btw: {
14868
+ category: "informational",
14869
+ expectsReply: false,
14870
+ requiresAction: false,
14871
+ backgroundEligible: false,
14872
+ outOfBand: false,
14873
+ renderPriority: 30,
14874
+ recipientObligation: "Absorb the information and stay on current task; no reply needed.",
14875
+ senderGuidance: "Low-priority aside. Non-urgent info that can wait."
14876
+ },
14877
+ broadcast: {
14878
+ category: "routing",
14879
+ expectsReply: false,
14880
+ requiresAction: false,
14881
+ backgroundEligible: false,
14882
+ outOfBand: false,
14883
+ renderPriority: 20,
14884
+ recipientObligation: 'Read if addressed to you (direct recipient, alias, or "*").',
14885
+ senderGuidance: 'Multi-recipient envelope. Auto-selected when to is "*" or "@session".'
14886
+ },
14887
+ status: {
14888
+ category: "informational",
14889
+ expectsReply: false,
14890
+ requiresAction: false,
14891
+ backgroundEligible: false,
14892
+ outOfBand: false,
14893
+ renderPriority: 40,
14894
+ recipientObligation: "Use to avoid redundant work; never act on as a task or question.",
14895
+ senderGuidance: "Agent/system status update. Machine-generated, not for human-originated messages."
14896
+ },
14897
+ result: {
14898
+ category: "informational",
14899
+ expectsReply: false,
14900
+ requiresAction: false,
14901
+ backgroundEligible: true,
14902
+ outOfBand: false,
14903
+ renderPriority: 10,
14904
+ recipientObligation: "Factor into next decision; treat as evidence, not a new task.",
14905
+ senderGuidance: "Task completion notice. Share the outcome of finished work."
14906
+ },
14907
+ review: {
14908
+ category: "actionable",
14909
+ expectsReply: false,
14910
+ requiresAction: true,
14911
+ backgroundEligible: true,
14912
+ outOfBand: false,
14913
+ renderPriority: 10,
14914
+ recipientObligation: "Inspect when convenient; no immediate reply required.",
14915
+ senderGuidance: "Passive review request (code/doc/PR). No reply required."
14916
+ },
14917
+ control: {
14918
+ category: "control_signal",
14919
+ expectsReply: false,
14920
+ requiresAction: false,
14921
+ backgroundEligible: false,
14922
+ outOfBand: true,
14923
+ renderPriority: 999,
14924
+ // Never rendered
14925
+ recipientObligation: 'Handled by the agent loop, NOT folded into conversation. "interrupt" causes cooperative halt.',
14926
+ senderGuidance: "RESERVED for runtime use. Agents must NOT send control messages."
14927
+ }
14928
+ };
14929
+ function validateSendType(type, to) {
14930
+ if (type === "control") {
14931
+ throw new TypeError(
14932
+ 'Type "control" is reserved for runtime use and cannot be set by agents'
14933
+ );
14934
+ }
14935
+ const isMultiRecipient = to === "*" || to.startsWith("@session:");
14936
+ if (type === "assign" && isMultiRecipient) {
14937
+ throw new TypeError(
14938
+ `Type "assign" requires a specific recipient \u2014 multi-recipient target "${to}" is ambiguous`
14939
+ );
14940
+ }
14941
+ if (type === "steer" && isMultiRecipient) {
14942
+ throw new TypeError(
14943
+ `Type "steer" requires a specific recipient \u2014 multi-recipient target "${to}" is ambiguous`
14944
+ );
14945
+ }
14946
+ }
14947
+ var SESSION_RECIPIENT_PREFIX = "@session:";
14948
+ function sessionRecipient(sessionId) {
14949
+ const normalizedSessionId = sessionId.trim();
14950
+ if (!normalizedSessionId) {
14951
+ throw new TypeError('sessionId is required for the "@session" recipient');
14952
+ }
14953
+ return `${SESSION_RECIPIENT_PREFIX}${normalizedSessionId}`;
14954
+ }
14955
+ function normalizeRecipient(to, sessionId) {
14956
+ const trimmed = to.trim();
14957
+ const normalized = trimmed.toLowerCase();
14958
+ if (normalized === "all") return "*";
14959
+ if (normalized === "@session") return sessionRecipient(sessionId ?? "");
14960
+ return trimmed;
14961
+ }
14962
+
14459
14963
  // src/coordination/mailbox-message-codec.ts
14460
14964
  var MESSAGE_TYPES = /* @__PURE__ */ new Set([
14461
14965
  "note",
@@ -14512,6 +15016,19 @@ function parseMessageType(value) {
14512
15016
  function normalizeMailboxMessageType(value) {
14513
15017
  return parseMessageType(value);
14514
15018
  }
15019
+ function resolveSendType(type, to) {
15020
+ const normalizedTo = normalizeRecipient(to);
15021
+ const resolved = type ?? (normalizedTo === "*" || normalizedTo.startsWith("@session:") ? "broadcast" : "note");
15022
+ validateSendType(resolved, normalizedTo);
15023
+ return resolved;
15024
+ }
15025
+ function resolveSendTypeSafe(type, to) {
15026
+ try {
15027
+ return { ok: true, type: resolveSendType(type, to) };
15028
+ } catch (err) {
15029
+ return { ok: false, error: err.message };
15030
+ }
15031
+ }
14515
15032
  function parsePriority(value) {
14516
15033
  if (typeof value === "string" && PRIORITIES.has(value)) {
14517
15034
  return value;
@@ -14623,23 +15140,6 @@ function serializeAckRecord(ack) {
14623
15140
  return JSON.stringify(ack) + "\n";
14624
15141
  }
14625
15142
 
14626
- // src/coordination/mailbox-types.ts
14627
- var SESSION_RECIPIENT_PREFIX = "@session:";
14628
- function sessionRecipient(sessionId) {
14629
- const normalizedSessionId = sessionId.trim();
14630
- if (!normalizedSessionId) {
14631
- throw new TypeError('sessionId is required for the "@session" recipient');
14632
- }
14633
- return `${SESSION_RECIPIENT_PREFIX}${normalizedSessionId}`;
14634
- }
14635
- function normalizeRecipient(to, sessionId) {
14636
- const trimmed = to.trim();
14637
- const normalized = trimmed.toLowerCase();
14638
- if (normalized === "all") return "*";
14639
- if (normalized === "@session") return sessionRecipient(sessionId ?? "");
14640
- return trimmed;
14641
- }
14642
-
14643
15143
  // src/coordination/global-mailbox.ts
14644
15144
  var MAILBOX_FILE = "_mailbox.jsonl";
14645
15145
  var CLIENT_REGISTRY_FILE = "_mailbox.clients.json";
@@ -15948,7 +16448,7 @@ function makeMailboxTool(opts = {}) {
15948
16448
  description: "Which mailbox operation to perform."
15949
16449
  },
15950
16450
  to: { type: "string", description: "Recipient agent id, base alias, '@session' for the sender's session, or '*' / 'all' for project broadcast." },
15951
- type: { type: "string", enum: ["note", "ask", "assign", "steer", "btw", "broadcast", "status", "result", "review", "control"], description: "Message type." },
16451
+ type: { type: "string", enum: ["note", "ask", "assign", "steer", "btw", "broadcast", "status", "result", "review", "control"], description: "Required message intent. Actionable: ask (blocking question), assign (task delegation, must have specific to), steer (mid-course direction), review (passive ask). Informational: note (general FYI), btw (low-priority aside), result (completion notice), status (system update). Routing: broadcast (multi-recipient). Reserved: control (runtime only, agents cannot send)." },
15952
16452
  subject: { type: "string", description: "Short subject line." },
15953
16453
  body: { type: "string", description: "Full message content." },
15954
16454
  priority: { type: "string", enum: ["low", "normal", "high"] },
@@ -16058,10 +16558,18 @@ async function executeSend(mb, agentId, sessionId, i) {
16058
16558
  if (!tp) return { ok: false, error: '"type" is required.' };
16059
16559
  if (!subject) return { ok: false, error: '"subject" is required.' };
16060
16560
  if (body === void 0 || body === null) return { ok: false, error: '"body" is required.' };
16561
+ let normalizedTo;
16562
+ try {
16563
+ normalizedTo = normalizeRecipient(to, sessionId);
16564
+ } catch (err) {
16565
+ return { ok: false, error: `"to" is invalid: ${toErrorMessage(err)}` };
16566
+ }
16567
+ const typeResult = resolveSendTypeSafe(tp, normalizedTo);
16568
+ if (!typeResult.ok) return { ok: false, error: `"type" is invalid: ${typeResult.error}` };
16061
16569
  const msg = await mb.send({
16062
16570
  from: agentId,
16063
- to,
16064
- type: tp,
16571
+ to: normalizedTo,
16572
+ type: typeResult.type,
16065
16573
  subject,
16066
16574
  body,
16067
16575
  priority: i.priority ?? "normal",
@@ -17163,18 +17671,7 @@ function optionalBoolean(object, key) {
17163
17671
  }
17164
17672
  return value;
17165
17673
  }
17166
- var VALID_TYPES = /* @__PURE__ */ new Set([
17167
- "note",
17168
- "ask",
17169
- "assign",
17170
- "steer",
17171
- "btw",
17172
- "broadcast",
17173
- "status",
17174
- "result",
17175
- "review",
17176
- "control"
17177
- ]);
17674
+ var VALID_TYPES = new Set(Object.keys(MAILBOX_TYPE_PROPERTIES));
17178
17675
  var VALID_PRIORITIES = /* @__PURE__ */ new Set(["low", "normal", "high"]);
17179
17676
  var RESERVED_FROM_IDS = /* @__PURE__ */ new Set([
17180
17677
  "leader",
@@ -17219,9 +17716,15 @@ function validateSend(body) {
17219
17716
  `field "from" must not use reserved internal agent id "${fromBase}" \u2014 external agents must use their own identity`
17220
17717
  );
17221
17718
  }
17719
+ const to = normalizeRecipient(requireString(object, "to"));
17720
+ try {
17721
+ resolveSendType(type, to);
17722
+ } catch (err) {
17723
+ throw validationError(`field "type" is invalid: ${err.message}`);
17724
+ }
17222
17725
  const result = {
17223
17726
  from,
17224
- to: requireString(object, "to"),
17727
+ to,
17225
17728
  type,
17226
17729
  subject: requireString(object, "subject"),
17227
17730
  body: requireString(object, "body"),
@@ -17455,7 +17958,7 @@ function makeMailSendTool(opts = {}) {
17455
17958
  const resolveMailbox = makeResolver(opts);
17456
17959
  return {
17457
17960
  name: "mail_send",
17458
- description: 'Send mail to any agent on this canonical project, across clients, processes, sessions, branches, and linked Git worktrees (CLI, TUI, WebUI, ACP/MCP/HTTP). Use it to hand off work, ask questions, announce what you just did, or request a review (type="review" \u2014 passive ask, no immediate reply required). to="*" broadcasts to everyone; to="@session" reaches agents in your current session; to="leader" reaches every leader process; an exact id like "leader@a1b2c3d4" reaches one agent. Use project-wide scope (`*` or a base alias) whenever another session may be affected. Recipients see your mail automatically before their next step. Pick the type that matches the intent: note (default), ask (blocking question), assign (task), steer (mid-task direction), result (completion notice), review (passive ask), btw/status/broadcast/control (informational).',
17961
+ description: 'Send mail to any agent on this canonical project, across clients, processes, sessions, branches, and linked Git worktrees (CLI, TUI, WebUI, ACP/MCP/HTTP). Use it to hand off work, ask questions, announce what you just did, or request a review. to="*" broadcasts to everyone; to="@session" reaches agents in your current session; to="leader" reaches every leader process; an exact id like "leader@a1b2c3d4" reaches one agent. Use project-wide scope (`*` or a base alias) whenever another session may be affected. Recipients see your mail automatically before their next step. \n\n\u2500\u2500 TYPE SEMANTICS \u2500\u2500\nThe `type` parameter determines how the recipient must handle your message:\n actionable (requires a response):\n ask \u2014 blocking question, sender waits for an answer\n assign \u2014 task delegation, act when current op allows (requires specific to, NOT "*")\n steer \u2014 mid-task direction change, recipient adjusts course NOW\n review \u2014 passive ask, inspect when convenient, no reply needed\n informational (consume for context):\n note \u2014 general FYI (default for directed sends)\n btw \u2014 low-priority aside, absorb and stay on task\n result \u2014 subagent completion notice, factor into next decision\n status \u2014 agent/system status update, avoid redundant work\n broadcast \u2014 multi-recipient envelope (default when to="*" or "@session")\n control (reserved for runtime use \u2014 agents cannot send this type)\n\nWhen no `type` is provided: broadcast for `*`/`@session`, otherwise note.\nassign/steer with to="*" is rejected (ambiguous). control is rejected (runtime-reserved).\n\nType determines dispatch: steer renders first, control is out-of-band, actionable types show "Action required" footer.',
17459
17962
  usageHint: 'mail_send to="<id>" type="review" body="please skim <file>"',
17460
17963
  category: "Coordination",
17461
17964
  permission: "auto",
@@ -17491,11 +17994,14 @@ function makeMailSendTool(opts = {}) {
17491
17994
  const mb = resolveMailbox(ctx);
17492
17995
  const identity = await register(mb, ctx);
17493
17996
  const to = normalizeRecipient(rawTo, identity.sessionId);
17494
- const type = i.type ?? (to === "*" || to.startsWith("@session:") ? "broadcast" : "note");
17997
+ const resolvedType = resolveSendType(
17998
+ i.type,
17999
+ to
18000
+ );
17495
18001
  const msg = await mb.send({
17496
18002
  from: identity.callerId,
17497
18003
  to,
17498
- type,
18004
+ type: resolvedType,
17499
18005
  subject,
17500
18006
  body,
17501
18007
  priority: i.priority ?? "normal",
@@ -18760,6 +19266,7 @@ var DEFAULTS2 = {
18760
19266
  blockAfterRateLimitHits: 3,
18761
19267
  blockAfterFailures: 5,
18762
19268
  blockDurationMs: 3e5,
19269
+ quotaBlockDurationMs: 9e5,
18763
19270
  recoverAfterSuccesses: 3,
18764
19271
  maxErrorHistory: 50
18765
19272
  };
@@ -18772,11 +19279,16 @@ var ProviderModelStatusTracker = class {
18772
19279
  this.events = opts?.events;
18773
19280
  }
18774
19281
  // ── Public API ──────────────────────────────────────────────────────────
19282
+ /** Resolve the logical health/display identity without changing wire routing. */
19283
+ logicalIdentity(providerId, model) {
19284
+ return statusIdentity(providerId, model);
19285
+ }
18775
19286
  /**
18776
19287
  * Record a successful provider call. Resets consecutive failure counters
18777
19288
  * and may transition out of degraded/blocked.
18778
19289
  */
18779
19290
  recordSuccess(providerId, model, _meta) {
19291
+ ({ providerId, model } = statusIdentity(providerId, model));
18780
19292
  const key = pairKey(providerId, model);
18781
19293
  const s = this.getOrCreate(key, providerId, model);
18782
19294
  s.consecutiveFailures = 0;
@@ -18796,9 +19308,11 @@ var ProviderModelStatusTracker = class {
18796
19308
  * @returns The new state after recording this failure.
18797
19309
  */
18798
19310
  recordFailure(providerId, model, kind, status, message, meta) {
19311
+ ({ providerId, model } = statusIdentity(providerId, model));
18799
19312
  const key = pairKey(providerId, model);
18800
19313
  const s = this.getOrCreate(key, providerId, model);
18801
19314
  const now = Date.now();
19315
+ const previousExpiry = s.stateExpiresAt;
18802
19316
  s.consecutiveFailures += 1;
18803
19317
  s.totalFailures += 1;
18804
19318
  s.consecutiveSuccesses = 0;
@@ -18811,6 +19325,7 @@ var ProviderModelStatusTracker = class {
18811
19325
  if (s.firstFailureAt === null) s.firstFailureAt = now;
18812
19326
  switch (kind) {
18813
19327
  case "rate_limit":
19328
+ case "quota_exhausted":
18814
19329
  s.rateLimitHits += 1;
18815
19330
  break;
18816
19331
  case "overloaded":
@@ -18838,14 +19353,20 @@ var ProviderModelStatusTracker = class {
18838
19353
  }
18839
19354
  let newState = s.state;
18840
19355
  let reason = "";
18841
- if (s.state === "healthy") {
19356
+ const quotaExhausted = kind === "quota_exhausted" || isQuotaExhausted(kind, status, message);
19357
+ if (quotaExhausted) {
19358
+ newState = "blocked";
19359
+ reason = "quota_exhausted";
19360
+ s.stateExpiresAt = now + this.cfg.quotaBlockDurationMs;
19361
+ }
19362
+ if (!quotaExhausted && s.state === "healthy") {
18842
19363
  if (s.consecutiveFailures >= this.cfg.degradedAfterFailures) {
18843
19364
  newState = "degraded";
18844
19365
  reason = `consecutive_failures_${s.consecutiveFailures}`;
18845
19366
  s.stateExpiresAt = now + this.cfg.degradedDurationMs;
18846
19367
  }
18847
19368
  }
18848
- if (s.state === "degraded" || s.state === "healthy") {
19369
+ if (!quotaExhausted && (s.state === "degraded" || s.state === "healthy")) {
18849
19370
  if (s.rateLimitHits >= this.cfg.blockAfterRateLimitHits) {
18850
19371
  newState = "blocked";
18851
19372
  reason = `rate_limit_threshold_${this.cfg.blockAfterRateLimitHits}`;
@@ -18857,7 +19378,7 @@ var ProviderModelStatusTracker = class {
18857
19378
  }
18858
19379
  }
18859
19380
  if (newState !== "healthy" && meta?.retryAfterMs && meta.retryAfterMs > 0) {
18860
- const hintExpiry = now + Math.min(meta.retryAfterMs, 6e4);
19381
+ const hintExpiry = now + meta.retryAfterMs;
18861
19382
  if (s.stateExpiresAt === null || hintExpiry > s.stateExpiresAt) {
18862
19383
  s.stateExpiresAt = hintExpiry;
18863
19384
  }
@@ -18866,6 +19387,8 @@ var ProviderModelStatusTracker = class {
18866
19387
  const oldState = s.state;
18867
19388
  s.state = newState;
18868
19389
  this.emitStatusChanged(providerId, model, oldState, newState, reason);
19390
+ } else if (s.state !== "healthy" && s.stateExpiresAt !== previousExpiry) {
19391
+ this.emitStatusChanged(providerId, model, s.state, s.state, "cooldown_extended");
18869
19392
  }
18870
19393
  return newState;
18871
19394
  }
@@ -18876,6 +19399,7 @@ var ProviderModelStatusTracker = class {
18876
19399
  * kind is `auth` (no point retrying ever).
18877
19400
  */
18878
19401
  isAvailable(providerId, model) {
19402
+ ({ providerId, model } = statusIdentity(providerId, model));
18879
19403
  const key = pairKey(providerId, model);
18880
19404
  const s = this.map.get(key);
18881
19405
  if (!s) return true;
@@ -18884,26 +19408,14 @@ var ProviderModelStatusTracker = class {
18884
19408
  s.state = "healthy";
18885
19409
  s.stateExpiresAt = null;
18886
19410
  s.consecutiveFailures = Math.max(0, s.consecutiveFailures - 1);
18887
- this.emitStatusChanged(
18888
- providerId,
18889
- model,
18890
- oldState,
18891
- "healthy",
18892
- "cooldown_expired"
18893
- );
19411
+ this.emitStatusChanged(providerId, model, oldState, "healthy", "cooldown_expired");
18894
19412
  return true;
18895
19413
  }
18896
19414
  if (s.state === "degraded" && s.stateExpiresAt !== null && Date.now() >= s.stateExpiresAt) {
18897
19415
  const oldState = s.state;
18898
19416
  s.state = "healthy";
18899
19417
  s.stateExpiresAt = null;
18900
- this.emitStatusChanged(
18901
- providerId,
18902
- model,
18903
- oldState,
18904
- "healthy",
18905
- "degraded_timeout_expired"
18906
- );
19418
+ this.emitStatusChanged(providerId, model, oldState, "healthy", "degraded_timeout_expired");
18907
19419
  return true;
18908
19420
  }
18909
19421
  return s.state !== "blocked";
@@ -18915,6 +19427,7 @@ var ProviderModelStatusTracker = class {
18915
19427
  * than skipped permanently.
18916
19428
  */
18917
19429
  isRateLimited(providerId, model) {
19430
+ ({ providerId, model } = statusIdentity(providerId, model));
18918
19431
  const key = pairKey(providerId, model);
18919
19432
  const s = this.map.get(key);
18920
19433
  if (!s) return false;
@@ -18925,6 +19438,7 @@ var ProviderModelStatusTracker = class {
18925
19438
  * if no failures have been recorded.
18926
19439
  */
18927
19440
  getStatus(providerId, model) {
19441
+ ({ providerId, model } = statusIdentity(providerId, model));
18928
19442
  const key = pairKey(providerId, model);
18929
19443
  const s = this.map.get(key);
18930
19444
  if (!s) return void 0;
@@ -18944,6 +19458,45 @@ var ProviderModelStatusTracker = class {
18944
19458
  }
18945
19459
  return out;
18946
19460
  }
19461
+ /**
19462
+ * Move every expired waiting-room entry back to healthy. Runtime routing
19463
+ * already performs this check lazily; this explicit sweep lets a UI/status
19464
+ * timer refresh the room even while no model calls are being made.
19465
+ *
19466
+ * @returns Number of entries released by this sweep.
19467
+ */
19468
+ sweepExpired() {
19469
+ let released = 0;
19470
+ for (const [key, s] of this.map) {
19471
+ if (s.state === "healthy" || s.stateExpiresAt === null || Date.now() < s.stateExpiresAt)
19472
+ continue;
19473
+ const [providerId, model] = unpairKey(key);
19474
+ const previous = s.state;
19475
+ s.state = "healthy";
19476
+ s.stateExpiresAt = null;
19477
+ s.consecutiveFailures = 0;
19478
+ s.consecutiveSuccesses = 0;
19479
+ this.emitStatusChanged(providerId, model, previous, "healthy", "waiting_room_expired");
19480
+ released += 1;
19481
+ }
19482
+ return released;
19483
+ }
19484
+ /**
19485
+ * Release one entry for an immediate half-open probe on its next real use.
19486
+ * History and totals are retained so operators do not lose diagnostics.
19487
+ */
19488
+ retryNow(providerId, model) {
19489
+ ({ providerId, model } = statusIdentity(providerId, model));
19490
+ const s = this.map.get(pairKey(providerId, model));
19491
+ if (!s || s.state === "healthy") return false;
19492
+ const previous = s.state;
19493
+ s.state = "healthy";
19494
+ s.stateExpiresAt = null;
19495
+ s.consecutiveFailures = 0;
19496
+ s.consecutiveSuccesses = 0;
19497
+ this.emitStatusChanged(providerId, model, previous, "healthy", "manual_half_open");
19498
+ return true;
19499
+ }
18947
19500
  /**
18948
19501
  * Returns only currently blocked entries.
18949
19502
  */
@@ -18969,6 +19522,7 @@ var ProviderModelStatusTracker = class {
18969
19522
  * where we don't want to mutate state during a quick peek.
18970
19523
  */
18971
19524
  isBlocked(providerId, model) {
19525
+ ({ providerId, model } = statusIdentity(providerId, model));
18972
19526
  const key = pairKey(providerId, model);
18973
19527
  const s = this.map.get(key);
18974
19528
  if (!s) return false;
@@ -18984,6 +19538,7 @@ var ProviderModelStatusTracker = class {
18984
19538
  */
18985
19539
  clear(providerId, model) {
18986
19540
  if (providerId && model) {
19541
+ ({ providerId, model } = statusIdentity(providerId, model));
18987
19542
  const key = pairKey(providerId, model);
18988
19543
  const old = this.map.get(key);
18989
19544
  if (old && old.state !== "healthy") {
@@ -19024,6 +19579,34 @@ var ProviderModelStatusTracker = class {
19024
19579
  statuses: all
19025
19580
  };
19026
19581
  }
19582
+ /** Restore non-expired waiting-room entries from a previous process. */
19583
+ restoreSnapshot(value) {
19584
+ if (!value || typeof value !== "object") return 0;
19585
+ const statuses = value.statuses;
19586
+ if (!Array.isArray(statuses)) return 0;
19587
+ let restored = 0;
19588
+ const now = Date.now();
19589
+ for (const raw of statuses) {
19590
+ if (!raw || typeof raw !== "object") continue;
19591
+ const item = raw;
19592
+ if (typeof item["providerId"] !== "string" || typeof item["model"] !== "string") continue;
19593
+ if (item["state"] !== "blocked" && item["state"] !== "degraded") continue;
19594
+ if (typeof item["stateExpiresAt"] !== "number" || item["stateExpiresAt"] <= now) continue;
19595
+ const { providerId, model } = statusIdentity(item["providerId"], item["model"]);
19596
+ const s = this.getOrCreate(pairKey(providerId, model), providerId, model);
19597
+ s.state = item["state"];
19598
+ s.stateExpiresAt = item["stateExpiresAt"];
19599
+ s.consecutiveFailures = safeCount(item["consecutiveFailures"]);
19600
+ s.totalFailures = safeCount(item["totalFailures"]);
19601
+ s.rateLimitHits = safeCount(item["rateLimitHits"]);
19602
+ s.lastFailureAt = safeTimestamp(item["lastFailureAt"]);
19603
+ s.lastErrorStatus = safeNullableNumber(item["lastErrorStatus"]);
19604
+ s.lastErrorMessage = typeof item["lastErrorMessage"] === "string" ? item["lastErrorMessage"] : null;
19605
+ s.lastErrorKind = isProviderErrorKind(item["lastErrorKind"]) ? item["lastErrorKind"] : null;
19606
+ restored += 1;
19607
+ }
19608
+ return restored;
19609
+ }
19027
19610
  // ── Private helpers ─────────────────────────────────────────────────────
19028
19611
  getOrCreate(key, _providerId, _model) {
19029
19612
  let s = this.map.get(key);
@@ -19088,13 +19671,23 @@ var ProviderModelStatusTracker = class {
19088
19671
  oldState,
19089
19672
  newState,
19090
19673
  reason,
19091
- timestamp: Date.now()
19674
+ timestamp: Date.now(),
19675
+ stateExpiresAt: this.map.get(pairKey(providerId, model))?.stateExpiresAt ?? void 0
19092
19676
  });
19093
19677
  } catch {
19094
19678
  }
19095
19679
  }
19096
19680
  };
19097
19681
  var KEY_SEP = "\0";
19682
+ function statusIdentity(providerId, model) {
19683
+ if (providerId !== "omniroute") return { providerId, model };
19684
+ const slash = model.indexOf("/");
19685
+ if (slash <= 0 || slash === model.length - 1) return { providerId, model };
19686
+ return {
19687
+ providerId: model.slice(0, slash),
19688
+ model: model.slice(slash + 1)
19689
+ };
19690
+ }
19098
19691
  function pairKey(providerId, model) {
19099
19692
  return `${providerId}${KEY_SEP}${model}`;
19100
19693
  }
@@ -19103,6 +19696,39 @@ function unpairKey(key) {
19103
19696
  if (idx === -1) return [key, ""];
19104
19697
  return [key.slice(0, idx), key.slice(idx + 1)];
19105
19698
  }
19699
+ var QUOTA_EXHAUSTED_RE2 = /(?:insufficient|exhausted|depleted|exceeded|no|not enough)[-_\s]*(?:quota|credit|balance)|(?:quota|credit|balance)[-_\s]*(?:exhausted|depleted|exceeded|insufficient)|billing[_\s-]*(?:hard[_\s-]*)?limit|payment required|spending limit|plan limit/i;
19700
+ function isQuotaExhausted(kind, status, message) {
19701
+ if (status === 402) return true;
19702
+ if (kind !== "rate_limit" && kind !== "quota_exhausted" && kind !== "invalid_request" && kind !== "auth")
19703
+ return false;
19704
+ return QUOTA_EXHAUSTED_RE2.test(message);
19705
+ }
19706
+ function safeCount(value) {
19707
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : 0;
19708
+ }
19709
+ function safeTimestamp(value) {
19710
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
19711
+ }
19712
+ function safeNullableNumber(value) {
19713
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
19714
+ }
19715
+ var PROVIDER_ERROR_KINDS = /* @__PURE__ */ new Set([
19716
+ "rate_limit",
19717
+ "quota_exhausted",
19718
+ "overloaded",
19719
+ "server",
19720
+ "timeout",
19721
+ "network",
19722
+ "stream_hang",
19723
+ "auth",
19724
+ "context_overflow",
19725
+ "content_filter",
19726
+ "invalid_request",
19727
+ "unknown"
19728
+ ]);
19729
+ function isProviderErrorKind(value) {
19730
+ return typeof value === "string" && PROVIDER_ERROR_KINDS.has(value);
19731
+ }
19106
19732
 
19107
19733
  // src/coordination/adaptive-concurrency.ts
19108
19734
  var DEFAULT_CONFIG = Object.freeze({
@@ -22226,7 +22852,6 @@ export {
22226
22852
  ACP_AGENTS,
22227
22853
  AGENTS_BY_PHASE,
22228
22854
  AGENT_CATALOG,
22229
- TOOLS as AGENT_TOOL_PRESETS,
22230
22855
  ALL_AGENT_DEFINITIONS,
22231
22856
  ALL_FLEET_AGENTS,
22232
22857
  AUDIT_LOG_AGENT,
@@ -22348,11 +22973,8 @@ export {
22348
22973
  makeDependencyWatcherConfig,
22349
22974
  makeDirectorSessionFactory,
22350
22975
  makeFleetEmitTool,
22351
- makeFleetTool as makeFleetHealthTool,
22352
- makeFleetTool as makeFleetSessionTool,
22353
22976
  makeFleetStatusTool,
22354
22977
  makeFleetTool,
22355
- makeFleetTool as makeFleetUsageTool,
22356
22978
  makeKanbanQueueTool,
22357
22979
  makeLLMClassifier,
22358
22980
  makeMailInboxTool,