@wrongstack/core 0.287.0 → 0.291.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (339) hide show
  1. package/dist/agent-status-tracker.d.ts +13 -0
  2. package/dist/agent-status-tracker.d.ts.map +1 -1
  3. package/dist/boot.d.ts.map +1 -1
  4. package/dist/chronicle/context.d.ts +13 -0
  5. package/dist/chronicle/context.d.ts.map +1 -0
  6. package/dist/chronicle/decision-adapter.d.ts +13 -0
  7. package/dist/chronicle/decision-adapter.d.ts.map +1 -0
  8. package/dist/chronicle/domain-adapter.d.ts +13 -0
  9. package/dist/chronicle/domain-adapter.d.ts.map +1 -0
  10. package/dist/chronicle/file-observer.d.ts +20 -0
  11. package/dist/chronicle/file-observer.d.ts.map +1 -0
  12. package/dist/chronicle/health-monitor.d.ts +11 -0
  13. package/dist/chronicle/health-monitor.d.ts.map +1 -0
  14. package/dist/chronicle/identity.d.ts +18 -0
  15. package/dist/chronicle/identity.d.ts.map +1 -0
  16. package/dist/chronicle/index.d.ts +16 -0
  17. package/dist/chronicle/index.d.ts.map +1 -0
  18. package/dist/chronicle/index.js +2555 -0
  19. package/dist/chronicle/index.js.map +7 -0
  20. package/dist/chronicle/journal.d.ts +81 -0
  21. package/dist/chronicle/journal.d.ts.map +1 -0
  22. package/dist/chronicle/process-adapter.d.ts +14 -0
  23. package/dist/chronicle/process-adapter.d.ts.map +1 -0
  24. package/dist/chronicle/prompt-manifest.d.ts +34 -0
  25. package/dist/chronicle/prompt-manifest.d.ts.map +1 -0
  26. package/dist/chronicle/provider-adapter.d.ts +13 -0
  27. package/dist/chronicle/provider-adapter.d.ts.map +1 -0
  28. package/dist/chronicle/query.d.ts +108 -0
  29. package/dist/chronicle/query.d.ts.map +1 -0
  30. package/dist/chronicle/rollup-adapter.d.ts +14 -0
  31. package/dist/chronicle/rollup-adapter.d.ts.map +1 -0
  32. package/dist/chronicle/stream-adapter.d.ts +13 -0
  33. package/dist/chronicle/stream-adapter.d.ts.map +1 -0
  34. package/dist/chronicle/tool-adapter.d.ts +15 -0
  35. package/dist/chronicle/tool-adapter.d.ts.map +1 -0
  36. package/dist/chronicle/types.d.ts +82 -0
  37. package/dist/chronicle/types.d.ts.map +1 -0
  38. package/dist/coordination/agent-monitor.d.ts +40 -0
  39. package/dist/coordination/agent-monitor.d.ts.map +1 -1
  40. package/dist/coordination/agent-subagent-runner.d.ts +7 -1
  41. package/dist/coordination/agent-subagent-runner.d.ts.map +1 -1
  42. package/dist/coordination/agents/phase6-domain.d.ts.map +1 -1
  43. package/dist/coordination/autonomous-coordinator.d.ts +2 -0
  44. package/dist/coordination/autonomous-coordinator.d.ts.map +1 -1
  45. package/dist/coordination/brain-monitor.d.ts +1 -1
  46. package/dist/coordination/brain.d.ts +3 -2
  47. package/dist/coordination/brain.d.ts.map +1 -1
  48. package/dist/coordination/collab-debug.d.ts +0 -9
  49. package/dist/coordination/collab-debug.d.ts.map +1 -1
  50. package/dist/coordination/coordinator/error-classifier.d.ts.map +1 -1
  51. package/dist/coordination/dep-watcher.d.ts +2 -1
  52. package/dist/coordination/dep-watcher.d.ts.map +1 -1
  53. package/dist/coordination/director/director-collab.d.ts +1 -1
  54. package/dist/coordination/director/director-collab.d.ts.map +1 -1
  55. package/dist/coordination/director-tools.d.ts.map +1 -1
  56. package/dist/coordination/director.d.ts +10 -1
  57. package/dist/coordination/director.d.ts.map +1 -1
  58. package/dist/coordination/file-author-tracker.d.ts.map +1 -1
  59. package/dist/coordination/fleet-bus.d.ts.map +1 -1
  60. package/dist/coordination/fleet-event-validation.d.ts +0 -7
  61. package/dist/coordination/fleet-event-validation.d.ts.map +1 -1
  62. package/dist/coordination/fleet-spawn.d.ts.map +1 -1
  63. package/dist/coordination/fleet-supervisor.d.ts +1 -1
  64. package/dist/coordination/fleet-supervisor.d.ts.map +1 -1
  65. package/dist/coordination/fleet.d.ts +0 -44
  66. package/dist/coordination/fleet.d.ts.map +1 -1
  67. package/dist/coordination/global-mailbox.d.ts +7 -1
  68. package/dist/coordination/global-mailbox.d.ts.map +1 -1
  69. package/dist/coordination/index.d.ts +5 -3
  70. package/dist/coordination/index.d.ts.map +1 -1
  71. package/dist/coordination/index.js +2426 -299
  72. package/dist/coordination/index.js.map +4 -4
  73. package/dist/coordination/knowledge-graph.d.ts +2 -1
  74. package/dist/coordination/knowledge-graph.d.ts.map +1 -1
  75. package/dist/coordination/large-answer-store.d.ts +0 -6
  76. package/dist/coordination/large-answer-store.d.ts.map +1 -1
  77. package/dist/coordination/mail-tools.d.ts +2 -2
  78. package/dist/coordination/mail-tools.d.ts.map +1 -1
  79. package/dist/coordination/mailbox-hooks.d.ts +2 -0
  80. package/dist/coordination/mailbox-hooks.d.ts.map +1 -1
  81. package/dist/coordination/mailbox-http-router.d.ts +43 -0
  82. package/dist/coordination/mailbox-http-router.d.ts.map +1 -0
  83. package/dist/coordination/mailbox-message-codec.d.ts +35 -0
  84. package/dist/coordination/mailbox-message-codec.d.ts.map +1 -1
  85. package/dist/coordination/mailbox-tool.d.ts +1 -1
  86. package/dist/coordination/mailbox-tool.d.ts.map +1 -1
  87. package/dist/coordination/mailbox-types.d.ts +170 -10
  88. package/dist/coordination/mailbox-types.d.ts.map +1 -1
  89. package/dist/coordination/mailbox.d.ts +1 -1
  90. package/dist/coordination/mailbox.d.ts.map +1 -1
  91. package/dist/coordination/package-author-tracker.d.ts.map +1 -1
  92. package/dist/coordination/provider-status-tracker.d.ts +236 -0
  93. package/dist/coordination/provider-status-tracker.d.ts.map +1 -0
  94. package/dist/coordination/subagent-budget.d.ts +2 -1
  95. package/dist/coordination/subagent-budget.d.ts.map +1 -1
  96. package/dist/core/agent-loop.d.ts +8 -3
  97. package/dist/core/agent-loop.d.ts.map +1 -1
  98. package/dist/core/agent-response.d.ts +5 -4
  99. package/dist/core/agent-response.d.ts.map +1 -1
  100. package/dist/core/agent-tools.d.ts +1 -1
  101. package/dist/core/agent-tools.d.ts.map +1 -1
  102. package/dist/core/agent.d.ts +4 -2
  103. package/dist/core/agent.d.ts.map +1 -1
  104. package/dist/core/context.d.ts +55 -1
  105. package/dist/core/context.d.ts.map +1 -1
  106. package/dist/core/fallback-model.d.ts +14 -8
  107. package/dist/core/fallback-model.d.ts.map +1 -1
  108. package/dist/core/fallback-profile-manager.d.ts +118 -0
  109. package/dist/core/fallback-profile-manager.d.ts.map +1 -0
  110. package/dist/core/fleet-pulse.d.ts +2 -1
  111. package/dist/core/fleet-pulse.d.ts.map +1 -1
  112. package/dist/core/iteration-limit.d.ts +2 -1
  113. package/dist/core/iteration-limit.d.ts.map +1 -1
  114. package/dist/core/mailbox-loop.d.ts +16 -1
  115. package/dist/core/mailbox-loop.d.ts.map +1 -1
  116. package/dist/core/model-availability-calendar.d.ts +27 -0
  117. package/dist/core/model-availability-calendar.d.ts.map +1 -0
  118. package/dist/core/provider-runner.d.ts.map +1 -1
  119. package/dist/core/system-prompt-builder.d.ts +2 -1
  120. package/dist/core/system-prompt-builder.d.ts.map +1 -1
  121. package/dist/defaults/index.js +1920 -598
  122. package/dist/defaults/index.js.map +4 -4
  123. package/dist/execution/auto-compaction-middleware.d.ts +3 -2
  124. package/dist/execution/auto-compaction-middleware.d.ts.map +1 -1
  125. package/dist/execution/compaction-core.d.ts.map +1 -1
  126. package/dist/execution/council-brain.d.ts +28 -49
  127. package/dist/execution/council-brain.d.ts.map +1 -1
  128. package/dist/execution/council-orchestrator.d.ts +30 -0
  129. package/dist/execution/council-orchestrator.d.ts.map +1 -1
  130. package/dist/execution/enhance-recovery.d.ts +8 -0
  131. package/dist/execution/enhance-recovery.d.ts.map +1 -1
  132. package/dist/execution/error-handler.d.ts +2 -2
  133. package/dist/execution/error-handler.d.ts.map +1 -1
  134. package/dist/execution/eternal-autonomy.d.ts.map +1 -1
  135. package/dist/execution/index.js +2600 -1903
  136. package/dist/execution/index.js.map +4 -4
  137. package/dist/execution/intelligent-compactor.d.ts +0 -16
  138. package/dist/execution/intelligent-compactor.d.ts.map +1 -1
  139. package/dist/execution/model-runtime.d.ts.map +1 -1
  140. package/dist/execution/one-shot-llm.d.ts +9 -1
  141. package/dist/execution/one-shot-llm.d.ts.map +1 -1
  142. package/dist/execution/prompt-enhancer.d.ts +36 -0
  143. package/dist/execution/prompt-enhancer.d.ts.map +1 -1
  144. package/dist/execution/prompt-enhancer.js +158 -10
  145. package/dist/execution/prompt-enhancer.js.map +2 -2
  146. package/dist/execution/retry-policy.d.ts +0 -13
  147. package/dist/execution/retry-policy.d.ts.map +1 -1
  148. package/dist/execution/skill-loader.d.ts.map +1 -1
  149. package/dist/execution/tool-executor.d.ts.map +1 -1
  150. package/dist/goal/checkpoint.d.ts.map +1 -0
  151. package/dist/{autophase/auto-phase-planner.d.ts → goal/goal-planner.d.ts} +8 -8
  152. package/dist/goal/goal-planner.d.ts.map +1 -0
  153. package/dist/{autophase/auto-phase-runner.d.ts → goal/goal-runner.d.ts} +10 -10
  154. package/dist/goal/goal-runner.d.ts.map +1 -0
  155. package/dist/{autophase → goal}/index.d.ts +3 -3
  156. package/dist/goal/index.d.ts.map +1 -0
  157. package/dist/goal/phase-graph-builder.d.ts.map +1 -0
  158. package/dist/{autophase → goal}/phase-orchestrator.d.ts +2 -2
  159. package/dist/goal/phase-orchestrator.d.ts.map +1 -0
  160. package/dist/goal/phase-store.d.ts.map +1 -0
  161. package/dist/{autophase → goal}/types.d.ts +2 -2
  162. package/dist/goal/types.d.ts.map +1 -0
  163. package/dist/hq/auth-store.d.ts.map +1 -1
  164. package/dist/hq/commands.d.ts +2 -1
  165. package/dist/hq/commands.d.ts.map +1 -1
  166. package/dist/hq/exposure.d.ts +66 -0
  167. package/dist/hq/exposure.d.ts.map +1 -0
  168. package/dist/hq/index.d.ts +1 -0
  169. package/dist/hq/index.d.ts.map +1 -1
  170. package/dist/hq/index.js +190 -21
  171. package/dist/hq/index.js.map +4 -4
  172. package/dist/hq/mailbox-mapper.d.ts +13 -1
  173. package/dist/hq/mailbox-mapper.d.ts.map +1 -1
  174. package/dist/hq/protocol/browser.d.ts +9 -1
  175. package/dist/hq/protocol/browser.d.ts.map +1 -1
  176. package/dist/hq/protocol/client.d.ts +2 -2
  177. package/dist/hq/protocol/client.d.ts.map +1 -1
  178. package/dist/hq/protocol/core.d.ts.map +1 -1
  179. package/dist/hq/protocol/mailbox.d.ts +6 -0
  180. package/dist/hq/protocol/mailbox.d.ts.map +1 -1
  181. package/dist/hq/protocol/session.d.ts +4 -0
  182. package/dist/hq/protocol/session.d.ts.map +1 -1
  183. package/dist/hq/publisher.d.ts.map +1 -1
  184. package/dist/index.d.ts +34 -25
  185. package/dist/index.d.ts.map +1 -1
  186. package/dist/index.js +30287 -22214
  187. package/dist/index.js.map +4 -4
  188. package/dist/infrastructure/index.js +60 -5
  189. package/dist/infrastructure/index.js.map +2 -2
  190. package/dist/kernel/events/agent-events.d.ts +33 -1
  191. package/dist/kernel/events/agent-events.d.ts.map +1 -1
  192. package/dist/kernel/events/file-events.d.ts +65 -0
  193. package/dist/kernel/events/file-events.d.ts.map +1 -0
  194. package/dist/kernel/events/memory-events.d.ts +36 -0
  195. package/dist/kernel/events/memory-events.d.ts.map +1 -1
  196. package/dist/kernel/events/network-events.d.ts +60 -0
  197. package/dist/kernel/events/network-events.d.ts.map +1 -0
  198. package/dist/kernel/events/process-events.d.ts +44 -0
  199. package/dist/kernel/events/process-events.d.ts.map +1 -0
  200. package/dist/kernel/events/provider-events.d.ts +89 -0
  201. package/dist/kernel/events/provider-events.d.ts.map +1 -1
  202. package/dist/kernel/events/tool-events.d.ts +10 -0
  203. package/dist/kernel/events/tool-events.d.ts.map +1 -1
  204. package/dist/kernel/events/worktree-events.d.ts +1 -1
  205. package/dist/kernel/events.d.ts +14 -1
  206. package/dist/kernel/events.d.ts.map +1 -1
  207. package/dist/kernel/index.js +2 -1
  208. package/dist/kernel/index.js.map +2 -2
  209. package/dist/kernel/tokens.d.ts +3 -1
  210. package/dist/kernel/tokens.d.ts.map +1 -1
  211. package/dist/mailbox-attach.d.ts.map +1 -1
  212. package/dist/models/index.js.map +2 -2
  213. package/dist/observability/index.d.ts +3 -0
  214. package/dist/observability/index.d.ts.map +1 -1
  215. package/dist/observability/index.js +278 -0
  216. package/dist/observability/index.js.map +4 -4
  217. package/dist/observability/network-telemetry.d.ts +15 -0
  218. package/dist/observability/network-telemetry.d.ts.map +1 -0
  219. package/dist/observability/process-telemetry.d.ts +18 -0
  220. package/dist/observability/process-telemetry.d.ts.map +1 -0
  221. package/dist/observability/redact-command.d.ts +20 -0
  222. package/dist/observability/redact-command.d.ts.map +1 -0
  223. package/dist/plugins/auto-review-plugin.d.ts +56 -3
  224. package/dist/plugins/auto-review-plugin.d.ts.map +1 -1
  225. package/dist/plugins/chimera-plugin.d.ts +164 -5
  226. package/dist/plugins/chimera-plugin.d.ts.map +1 -1
  227. package/dist/plugins/review-context-builder.d.ts +52 -0
  228. package/dist/plugins/review-context-builder.d.ts.map +1 -0
  229. package/dist/security/index.d.ts +1 -0
  230. package/dist/security/index.d.ts.map +1 -1
  231. package/dist/security/index.js +186 -9
  232. package/dist/security/index.js.map +4 -4
  233. package/dist/security/kanban-boundary.d.ts +10 -0
  234. package/dist/security/kanban-boundary.d.ts.map +1 -0
  235. package/dist/security/permission-policy.d.ts +1 -0
  236. package/dist/security/permission-policy.d.ts.map +1 -1
  237. package/dist/session-registry.d.ts +80 -0
  238. package/dist/session-registry.d.ts.map +1 -1
  239. package/dist/skills/index.js.map +2 -2
  240. package/dist/storage/config-loader.d.ts +7 -0
  241. package/dist/storage/config-loader.d.ts.map +1 -1
  242. package/dist/storage/file-session-writer.d.ts +20 -7
  243. package/dist/storage/file-session-writer.d.ts.map +1 -1
  244. package/dist/storage/goal-coordination.d.ts +40 -0
  245. package/dist/storage/goal-coordination.d.ts.map +1 -0
  246. package/dist/storage/goal-store.d.ts +9 -5
  247. package/dist/storage/goal-store.d.ts.map +1 -1
  248. package/dist/storage/index.d.ts +4 -2
  249. package/dist/storage/index.d.ts.map +1 -1
  250. package/dist/storage/index.js +1066 -226
  251. package/dist/storage/index.js.map +4 -4
  252. package/dist/storage/memory-consolidator.d.ts +49 -4
  253. package/dist/storage/memory-consolidator.d.ts.map +1 -1
  254. package/dist/storage/memory-graph-backend.d.ts +11 -0
  255. package/dist/storage/memory-graph-backend.d.ts.map +1 -1
  256. package/dist/storage/provider-config-watcher.d.ts +19 -0
  257. package/dist/storage/provider-config-watcher.d.ts.map +1 -1
  258. package/dist/storage/session-event-bridge.d.ts +2 -2
  259. package/dist/storage/session-event-bridge.d.ts.map +1 -1
  260. package/dist/storage/session-resume-validation.d.ts +10 -0
  261. package/dist/storage/session-resume-validation.d.ts.map +1 -1
  262. package/dist/storage/session-rewind-apply.d.ts +53 -0
  263. package/dist/storage/session-rewind-apply.d.ts.map +1 -0
  264. package/dist/storage/session-rewinder.d.ts.map +1 -1
  265. package/dist/storage/session-store.d.ts.map +1 -1
  266. package/dist/tasking/index.js.map +1 -1
  267. package/dist/tasking/task-store.d.ts +1 -1
  268. package/dist/tools/council-tool.d.ts +3 -0
  269. package/dist/tools/council-tool.d.ts.map +1 -1
  270. package/dist/tools/fallback-manage-tools.d.ts +82 -0
  271. package/dist/tools/fallback-manage-tools.d.ts.map +1 -0
  272. package/dist/tools/index.d.ts +1 -0
  273. package/dist/tools/index.d.ts.map +1 -1
  274. package/dist/tools/index.js +3423 -192
  275. package/dist/tools/index.js.map +4 -4
  276. package/dist/tools/one-shot-llm-tool.d.ts +2 -0
  277. package/dist/tools/one-shot-llm-tool.d.ts.map +1 -1
  278. package/dist/types/config.d.ts +76 -7
  279. package/dist/types/config.d.ts.map +1 -1
  280. package/dist/types/file-event-record.d.ts +68 -0
  281. package/dist/types/file-event-record.d.ts.map +1 -0
  282. package/dist/types/index.d.ts +1 -0
  283. package/dist/types/index.d.ts.map +1 -1
  284. package/dist/types/index.js +13 -8
  285. package/dist/types/index.js.map +3 -3
  286. package/dist/types/memory.d.ts +12 -0
  287. package/dist/types/memory.d.ts.map +1 -1
  288. package/dist/types/multi-agent.d.ts +12 -1
  289. package/dist/types/multi-agent.d.ts.map +1 -1
  290. package/dist/types/one-shot-llm.d.ts +16 -7
  291. package/dist/types/one-shot-llm.d.ts.map +1 -1
  292. package/dist/types/provider.d.ts +1 -1
  293. package/dist/types/provider.d.ts.map +1 -1
  294. package/dist/types/session.d.ts +68 -2
  295. package/dist/types/session.d.ts.map +1 -1
  296. package/dist/types/tool-executor.d.ts +2 -0
  297. package/dist/types/tool-executor.d.ts.map +1 -1
  298. package/dist/types/tool.d.ts +5 -0
  299. package/dist/types/tool.d.ts.map +1 -1
  300. package/dist/utils/atomic-write.d.ts.map +1 -1
  301. package/dist/utils/index.d.ts +10 -10
  302. package/dist/utils/index.d.ts.map +1 -1
  303. package/dist/utils/index.js +497 -254
  304. package/dist/utils/index.js.map +4 -4
  305. package/dist/utils/message-invariants.d.ts +9 -0
  306. package/dist/utils/message-invariants.d.ts.map +1 -1
  307. package/dist/utils/safe-json.d.ts +1 -1
  308. package/dist/utils/safe-json.d.ts.map +1 -1
  309. package/dist/utils/term.d.ts +279 -0
  310. package/dist/utils/term.d.ts.map +1 -1
  311. package/dist/utils/tool-wire-compact.d.ts +9 -0
  312. package/dist/utils/tool-wire-compact.d.ts.map +1 -1
  313. package/dist/utils/wstack-paths.d.ts +12 -2
  314. package/dist/utils/wstack-paths.d.ts.map +1 -1
  315. package/dist/worktree/worktree-manager.d.ts +1 -1
  316. package/dist/worktree/worktree-manager.d.ts.map +1 -1
  317. package/instructions/agents/ios.md +113 -0
  318. package/instructions/agents/search.md +1 -1
  319. package/instructions/llm/chimera-review.md +66 -9
  320. package/instructions/llm/memory-consolidator.md +4 -12
  321. package/instructions/llm/prompt-enhancer.md +3 -1
  322. package/instructions/sections/tool/common-patterns.md +3 -3
  323. package/instructions/system.md +40 -5
  324. package/package.json +7 -3
  325. package/skills/auto-review/SKILL.md +176 -0
  326. package/skills/mnemosyne/SKILL.md +156 -0
  327. package/skills/mnemosyne/instructions/agent-prompt.md +200 -0
  328. package/dist/autophase/auto-phase-planner.d.ts.map +0 -1
  329. package/dist/autophase/auto-phase-runner.d.ts.map +0 -1
  330. package/dist/autophase/checkpoint.d.ts.map +0 -1
  331. package/dist/autophase/index.d.ts.map +0 -1
  332. package/dist/autophase/phase-graph-builder.d.ts.map +0 -1
  333. package/dist/autophase/phase-orchestrator.d.ts.map +0 -1
  334. package/dist/autophase/phase-store.d.ts.map +0 -1
  335. package/dist/autophase/types.d.ts.map +0 -1
  336. /package/dist/{autophase → goal}/checkpoint.d.ts +0 -0
  337. /package/dist/{autophase → goal}/phase-graph-builder.d.ts +0 -0
  338. /package/dist/{autophase → goal}/phase-store.d.ts +0 -0
  339. /package/instructions/{autophase → goal}/phase-planner.md +0 -0
@@ -36,8 +36,8 @@ async function atomicWrite(targetPath, content, opts = {}) {
36
36
  }
37
37
  let mode;
38
38
  try {
39
- const stat12 = await fs.stat(targetPath);
40
- mode = stat12.mode & 511;
39
+ const stat13 = await fs.stat(targetPath);
40
+ mode = stat13.mode & 511;
41
41
  } catch {
42
42
  mode = opts.mode;
43
43
  }
@@ -66,7 +66,7 @@ async function withFileLock(targetPath, fn, opts = {}) {
66
66
  const dir = path.dirname(targetPath);
67
67
  await fs.mkdir(dir, { recursive: true });
68
68
  const lockPath = path.join(dir, `.${path.basename(targetPath)}.lock`);
69
- const timeoutMs = opts.timeoutMs ?? 5e3;
69
+ const timeoutMs = opts.timeoutMs ?? 15e3;
70
70
  const staleMs = opts.staleMs ?? 3e4;
71
71
  const started = Date.now();
72
72
  let handle;
@@ -76,6 +76,13 @@ async function withFileLock(targetPath, fn, opts = {}) {
76
76
  await handle.writeFile(`${process.pid}:${Date.now()}`);
77
77
  break;
78
78
  } catch (err) {
79
+ if (handle) {
80
+ await handle.close().catch(() => {
81
+ });
82
+ await fs.unlink(lockPath).catch(() => {
83
+ });
84
+ handle = void 0;
85
+ }
79
86
  const code = err.code;
80
87
  if (code === "ENOENT") {
81
88
  await fs.mkdir(dir, { recursive: true });
@@ -83,8 +90,8 @@ async function withFileLock(targetPath, fn, opts = {}) {
83
90
  }
84
91
  if (code !== "EEXIST" && code !== "EPERM") throw err;
85
92
  try {
86
- const stat12 = await fs.stat(lockPath);
87
- if (Date.now() - stat12.mtimeMs > staleMs) {
93
+ const stat13 = await fs.stat(lockPath);
94
+ if (Date.now() - stat13.mtimeMs > staleMs) {
88
95
  await fs.unlink(lockPath);
89
96
  continue;
90
97
  }
@@ -120,13 +127,13 @@ async function waitForLockRelease(lockPath, remainingMs) {
120
127
  const parentDir = path.dirname(lockPath);
121
128
  const lockName = path.basename(lockPath);
122
129
  const intervalMs = Math.min(remainingMs, 100);
123
- return new Promise((resolve12) => {
130
+ return new Promise((resolve14) => {
124
131
  let settled = false;
125
132
  let watcher = null;
126
133
  const timer = setTimeout(() => {
127
134
  settled = true;
128
135
  watcher?.close();
129
- resolve12();
136
+ resolve14();
130
137
  }, intervalMs);
131
138
  try {
132
139
  watcher = watchDir(parentDir, (eventType, filename) => {
@@ -135,14 +142,14 @@ async function waitForLockRelease(lockPath, remainingMs) {
135
142
  settled = true;
136
143
  clearTimeout(timer);
137
144
  watcher?.close();
138
- resolve12();
145
+ resolve14();
139
146
  }
140
147
  });
141
148
  } catch {
142
149
  clearTimeout(timer);
143
150
  if (!settled) {
144
151
  settled = true;
145
- setTimeout(resolve12, Math.min(remainingMs, 25));
152
+ setTimeout(resolve14, Math.min(remainingMs, 25));
146
153
  }
147
154
  return;
148
155
  }
@@ -154,7 +161,7 @@ async function waitForLockRelease(lockPath, remainingMs) {
154
161
  settled = true;
155
162
  clearTimeout(timer);
156
163
  watcher?.close();
157
- resolve12();
164
+ resolve14();
158
165
  }
159
166
  }
160
167
  );
@@ -178,7 +185,7 @@ async function renameWithRetry(from, to) {
178
185
  if (!code || !TRANSIENT_RENAME_CODES.has(code) || i === delays.length) {
179
186
  throw err;
180
187
  }
181
- await new Promise((resolve12) => setTimeout(resolve12, delays[i]));
188
+ await new Promise((resolve14) => setTimeout(resolve14, delays[i]));
182
189
  }
183
190
  }
184
191
  throw lastErr;
@@ -336,6 +343,12 @@ function writeTo(s, stream) {
336
343
  function writeErr(s, stream = process.stderr) {
337
344
  return writeTo(s, stream);
338
345
  }
346
+ var ESCAPE_TERMINATOR = Object.freeze({
347
+ BEL: "\x07",
348
+ // OSC 0 / OSC 2 (title)
349
+ ST: "\x1B\\"
350
+ // DCS / strict OSC
351
+ });
339
352
 
340
353
  // src/utils/color.ts
341
354
  var isColorTty = () => {
@@ -618,8 +631,8 @@ async function expandGlob(pattern) {
618
631
  for (const e of entries) {
619
632
  const full = `${dir}${SEP}${e}`;
620
633
  try {
621
- const stat12 = await fsp.stat(full);
622
- if (stat12.isDirectory()) await walk3(full, rest);
634
+ const stat13 = await fsp.stat(full);
635
+ if (stat13.isDirectory()) await walk3(full, rest);
623
636
  } catch {
624
637
  }
625
638
  }
@@ -636,8 +649,8 @@ async function expandGlob(pattern) {
636
649
  if (entries.includes(seg)) {
637
650
  const full = `${dir}${SEP}${seg}`;
638
651
  try {
639
- const stat12 = await fsp.stat(full);
640
- if (stat12.isDirectory()) await walk3(full, rest);
652
+ const stat13 = await fsp.stat(full);
653
+ if (stat13.isDirectory()) await walk3(full, rest);
641
654
  } catch {
642
655
  }
643
656
  }
@@ -828,10 +841,10 @@ function validateAgainstSchema(value, schema) {
828
841
  return { ok: errors.length === 0, errors };
829
842
  }
830
843
  var MAX_SCHEMA_DEPTH = 64;
831
- function walk(value, schema, path28, errors, depth) {
844
+ function walk(value, schema, path29, errors, depth) {
832
845
  if (depth > MAX_SCHEMA_DEPTH) {
833
846
  errors.push({
834
- path: path28 || "<root>",
847
+ path: path29 || "<root>",
835
848
  message: `schema nesting exceeds maximum depth (${MAX_SCHEMA_DEPTH})`
836
849
  });
837
850
  return;
@@ -839,7 +852,7 @@ function walk(value, schema, path28, errors, depth) {
839
852
  if (schema.enum !== void 0) {
840
853
  if (!enumIncludes(schema.enum, value)) {
841
854
  errors.push({
842
- path: path28 || "<root>",
855
+ path: path29 || "<root>",
843
856
  message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
844
857
  });
845
858
  return;
@@ -848,7 +861,7 @@ function walk(value, schema, path28, errors, depth) {
848
861
  if (typeof schema.type === "string") {
849
862
  if (!checkType(value, schema.type)) {
850
863
  errors.push({
851
- path: path28 || "<root>",
864
+ path: path29 || "<root>",
852
865
  message: `expected ${schema.type}, got ${describeType(value)} (${previewValue(value)})`
853
866
  });
854
867
  return;
@@ -860,7 +873,7 @@ function walk(value, schema, path28, errors, depth) {
860
873
  if (!(req in obj)) {
861
874
  const expected = schema.properties?.[req]?.type;
862
875
  errors.push({
863
- path: joinPath(path28, req),
876
+ path: joinPath(path29, req),
864
877
  message: `required property missing${typeof expected === "string" ? ` (expected ${expected})` : ""}`
865
878
  });
866
879
  }
@@ -868,14 +881,14 @@ function walk(value, schema, path28, errors, depth) {
868
881
  if (schema.properties) {
869
882
  for (const [key, subSchema] of Object.entries(schema.properties)) {
870
883
  if (key in obj) {
871
- walk(obj[key], subSchema, joinPath(path28, key), errors, depth + 1);
884
+ walk(obj[key], subSchema, joinPath(path29, key), errors, depth + 1);
872
885
  }
873
886
  }
874
887
  }
875
888
  }
876
889
  if (schema.type === "array" && Array.isArray(value) && schema.items) {
877
890
  for (let i = 0; i < value.length; i++) {
878
- walk(value[i], schema.items, `${path28}[${i}]`, errors, depth + 1);
891
+ walk(value[i], schema.items, `${path29}[${i}]`, errors, depth + 1);
879
892
  }
880
893
  }
881
894
  }
@@ -1069,6 +1082,16 @@ function stripUndefined(obj) {
1069
1082
  }
1070
1083
 
1071
1084
  // src/utils/message-invariants.ts
1085
+ function hasMeaningfulContent(content) {
1086
+ if (typeof content === "string") return content.trim().length > 0;
1087
+ return content.some((block) => {
1088
+ if (block.type === "text") return block.text.trim().length > 0;
1089
+ if (block.type === "thinking") {
1090
+ return block.thinking.trim().length > 0 || Boolean(block.signature);
1091
+ }
1092
+ return true;
1093
+ });
1094
+ }
1072
1095
  function repairToolUseAdjacency(messages) {
1073
1096
  const removedToolUses = [];
1074
1097
  const removedToolResults = [];
@@ -1156,8 +1179,7 @@ function mapContent(msg, fn) {
1156
1179
  return { ...msg, content: next };
1157
1180
  }
1158
1181
  function isEmptyMessage(msg) {
1159
- if (typeof msg.content === "string") return msg.content.trim().length === 0;
1160
- return msg.content.length === 0;
1182
+ return !hasMeaningfulContent(msg.content);
1161
1183
  }
1162
1184
 
1163
1185
  // src/utils/regex-guard.ts
@@ -1198,7 +1220,7 @@ function compileUserRegex(pattern, flags) {
1198
1220
 
1199
1221
  // src/utils/safe-json.ts
1200
1222
  function safeParse(input, maxBytes = 5e6) {
1201
- if (input.length > maxBytes) {
1223
+ if (Buffer.byteLength(input, "utf8") > maxBytes) {
1202
1224
  return { ok: false, error: `Input exceeds limit (${maxBytes} bytes)` };
1203
1225
  }
1204
1226
  try {
@@ -1254,84 +1276,21 @@ function invalid(sessionId) {
1254
1276
  });
1255
1277
  }
1256
1278
 
1279
+ // src/utils/sleep.ts
1280
+ function sleep(ms) {
1281
+ return new Promise((resolve14) => setTimeout(resolve14, ms));
1282
+ }
1283
+
1257
1284
  // src/utils/slug.ts
1258
1285
  function slugify(name, fallback = "prompt", maxLen = 64) {
1259
1286
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLen).replace(/-+$/g, "") || fallback;
1260
1287
  }
1261
1288
 
1262
- // src/utils/ulid.ts
1263
- import { randomBytes as randomBytes2 } from "node:crypto";
1264
- var ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
1265
- var ENCODING_LEN = ENCODING.length;
1266
- var TIME_LEN = 10;
1267
- var RANDOM_LEN = 16;
1268
- function encodeTime(now, len) {
1269
- let mod;
1270
- let str = "";
1271
- for (let i = len - 1; i >= 0; i--) {
1272
- mod = now % ENCODING_LEN;
1273
- str = ENCODING[mod] + str;
1274
- now = (now - mod) / ENCODING_LEN;
1275
- }
1276
- return str;
1277
- }
1278
- function encodeRandom(len) {
1279
- const bytes = randomBytes2(len);
1280
- let str = "";
1281
- for (let i = 0; i < len; i++) {
1282
- str += ENCODING[bytes[i] % ENCODING_LEN];
1283
- }
1284
- return str;
1285
- }
1286
- function ulid(seedTime = Date.now()) {
1287
- return encodeTime(seedTime, TIME_LEN) + encodeRandom(RANDOM_LEN);
1288
- }
1289
-
1290
- // src/utils/sleep.ts
1291
- function sleep(ms) {
1292
- return new Promise((resolve12) => setTimeout(resolve12, ms));
1293
- }
1294
-
1295
1289
  // src/utils/string.ts
1296
1290
  function truncate(s, max) {
1297
1291
  return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
1298
1292
  }
1299
1293
 
1300
- // src/utils/tool-subject.ts
1301
- var GLOB_METACHARACTERS = /[*?[\]]/g;
1302
- function escapeGlobSubject(value) {
1303
- return value.replace(GLOB_METACHARACTERS, (char) => `\\${char}`);
1304
- }
1305
- function normalizePathSubject(value) {
1306
- return escapeGlobSubject(value.replace(/\\/g, "/"));
1307
- }
1308
- function isPathSubjectKey(subjectKey) {
1309
- return subjectKey === "path" || subjectKey === "file" || subjectKey === "files";
1310
- }
1311
- function subjectForToolInput(toolName, input, subjectKey) {
1312
- if (!input || typeof input !== "object") return void 0;
1313
- const obj = input;
1314
- if (subjectKey) {
1315
- const value = obj[subjectKey];
1316
- if (typeof value === "string") {
1317
- return isPathSubjectKey(subjectKey) ? normalizePathSubject(value) : escapeGlobSubject(value);
1318
- }
1319
- }
1320
- if (toolName === "bash" && typeof obj.command === "string") {
1321
- return escapeGlobSubject(obj.command);
1322
- }
1323
- if (typeof obj.path === "string") {
1324
- return normalizePathSubject(obj.path);
1325
- }
1326
- if (typeof obj.url === "string") {
1327
- return escapeGlobSubject(obj.url);
1328
- }
1329
- if (typeof obj.name === "string") {
1330
- return escapeGlobSubject(obj.name);
1331
- }
1332
- return void 0;
1333
- }
1334
-
1335
1294
  // src/utils/tool-wire-compact.ts
1336
1295
  var TOOL_DESCRIPTION_MAX_CHARS = 400;
1337
1296
  var SCHEMA_DESCRIPTION_MAX_CHARS = 120;
@@ -1348,9 +1307,11 @@ function compactToolDefinitionForWire(tool, opts = {}) {
1348
1307
  tool.description ?? "",
1349
1308
  opts.descriptionMaxChars ?? TOOL_DESCRIPTION_MAX_CHARS
1350
1309
  ),
1351
- inputSchema: compactSchemaDescriptions(
1352
- tool.inputSchema,
1353
- opts.schemaDescriptionMaxChars ?? SCHEMA_DESCRIPTION_MAX_CHARS
1310
+ inputSchema: normalizeTopLevelToolSchema(
1311
+ compactSchemaDescriptions(
1312
+ tool.inputSchema,
1313
+ opts.schemaDescriptionMaxChars ?? SCHEMA_DESCRIPTION_MAX_CHARS
1314
+ )
1354
1315
  )
1355
1316
  };
1356
1317
  if (useDefaultOptions && typeof tool === "object" && tool !== null) {
@@ -1358,6 +1319,44 @@ function compactToolDefinitionForWire(tool, opts = {}) {
1358
1319
  }
1359
1320
  return compact;
1360
1321
  }
1322
+ function normalizeTopLevelToolSchema(schema) {
1323
+ const combinators = ["oneOf", "anyOf", "allOf"].map((keyword) => ({ keyword, branches: schema[keyword] })).filter(
1324
+ (entry) => Array.isArray(entry.branches)
1325
+ );
1326
+ if (combinators.length === 0) return schema;
1327
+ const out = { ...schema, type: "object" };
1328
+ delete out["oneOf"];
1329
+ delete out["anyOf"];
1330
+ delete out["allOf"];
1331
+ const properties = isRecord(schema["properties"]) ? { ...schema["properties"] } : {};
1332
+ let required = stringSet(schema["required"]);
1333
+ for (const { keyword, branches } of combinators) {
1334
+ const objectBranches = branches.filter(isRecord);
1335
+ for (const branch of objectBranches) {
1336
+ if (isRecord(branch["properties"])) Object.assign(properties, branch["properties"]);
1337
+ }
1338
+ const branchRequired = objectBranches.map((branch) => stringSet(branch["required"]));
1339
+ if (keyword === "allOf") {
1340
+ for (const fields of branchRequired) for (const field of fields) required.add(field);
1341
+ } else if (branchRequired.length > 0) {
1342
+ const common = new Set(
1343
+ [...branchRequired[0]].filter(
1344
+ (field) => branchRequired.slice(1).every((fields) => fields.has(field))
1345
+ )
1346
+ );
1347
+ required = /* @__PURE__ */ new Set([...required, ...common]);
1348
+ }
1349
+ }
1350
+ out["properties"] = properties;
1351
+ if (required.size > 0) out["required"] = [...required];
1352
+ else delete out["required"];
1353
+ return out;
1354
+ }
1355
+ function stringSet(value) {
1356
+ return new Set(
1357
+ Array.isArray(value) ? value.filter((item) => typeof item === "string") : []
1358
+ );
1359
+ }
1361
1360
  function compactSchemaDescriptions(schema, maxDescriptionChars = SCHEMA_DESCRIPTION_MAX_CHARS) {
1362
1361
  const compact = compactSchemaNode(schema, maxDescriptionChars);
1363
1362
  return isRecord(compact) ? compact : { type: "object", properties: {} };
@@ -2252,6 +2251,69 @@ function resolveToolResultRenderMode(modes, toolName) {
2252
2251
  return normalizeToolResultRenderMode(modes?.[toolName]) ?? DEFAULT_TOOL_RESULT_RENDER_MODE;
2253
2252
  }
2254
2253
 
2254
+ // src/utils/tool-subject.ts
2255
+ var GLOB_METACHARACTERS = /[*?[\]]/g;
2256
+ function escapeGlobSubject(value) {
2257
+ return value.replace(GLOB_METACHARACTERS, (char) => `\\${char}`);
2258
+ }
2259
+ function normalizePathSubject(value) {
2260
+ return escapeGlobSubject(value.replace(/\\/g, "/"));
2261
+ }
2262
+ function isPathSubjectKey(subjectKey) {
2263
+ return subjectKey === "path" || subjectKey === "file" || subjectKey === "files";
2264
+ }
2265
+ function subjectForToolInput(toolName, input, subjectKey) {
2266
+ if (!input || typeof input !== "object") return void 0;
2267
+ const obj = input;
2268
+ if (subjectKey) {
2269
+ const value = obj[subjectKey];
2270
+ if (typeof value === "string") {
2271
+ return isPathSubjectKey(subjectKey) ? normalizePathSubject(value) : escapeGlobSubject(value);
2272
+ }
2273
+ }
2274
+ if (toolName === "bash" && typeof obj.command === "string") {
2275
+ return escapeGlobSubject(obj.command);
2276
+ }
2277
+ if (typeof obj.path === "string") {
2278
+ return normalizePathSubject(obj.path);
2279
+ }
2280
+ if (typeof obj.url === "string") {
2281
+ return escapeGlobSubject(obj.url);
2282
+ }
2283
+ if (typeof obj.name === "string") {
2284
+ return escapeGlobSubject(obj.name);
2285
+ }
2286
+ return void 0;
2287
+ }
2288
+
2289
+ // src/utils/ulid.ts
2290
+ import { randomBytes as randomBytes2 } from "node:crypto";
2291
+ var ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
2292
+ var ENCODING_LEN = ENCODING.length;
2293
+ var TIME_LEN = 10;
2294
+ var RANDOM_LEN = 16;
2295
+ function encodeTime(now, len) {
2296
+ let mod;
2297
+ let str = "";
2298
+ for (let i = len - 1; i >= 0; i--) {
2299
+ mod = now % ENCODING_LEN;
2300
+ str = ENCODING[mod] + str;
2301
+ now = (now - mod) / ENCODING_LEN;
2302
+ }
2303
+ return str;
2304
+ }
2305
+ function encodeRandom(len) {
2306
+ const bytes = randomBytes2(len);
2307
+ let str = "";
2308
+ for (let i = 0; i < len; i++) {
2309
+ str += ENCODING[bytes[i] % ENCODING_LEN];
2310
+ }
2311
+ return str;
2312
+ }
2313
+ function ulid(seedTime = Date.now()) {
2314
+ return encodeTime(seedTime, TIME_LEN) + encodeRandom(RANDOM_LEN);
2315
+ }
2316
+
2255
2317
  // src/utils/wstack-paths.ts
2256
2318
  import { createHash } from "node:crypto";
2257
2319
  import * as fs2 from "node:fs";
@@ -2281,8 +2343,8 @@ function projectHash(absRoot) {
2281
2343
  function projectSlug(absRoot) {
2282
2344
  const identityRoot = canonicalProjectRoot(absRoot);
2283
2345
  const base = slugify2(path3.basename(identityRoot));
2284
- const hash = createHash("sha256").update(identityRoot).digest("hex").slice(0, 6);
2285
- return `${base}-${hash}`;
2346
+ const hash2 = createHash("sha256").update(identityRoot).digest("hex").slice(0, 6);
2347
+ return `${base}-${hash2}`;
2286
2348
  }
2287
2349
  function slugify2(name) {
2288
2350
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
@@ -2295,7 +2357,7 @@ function wstackGlobalRoot() {
2295
2357
  function resolveWstackPaths(opts) {
2296
2358
  const globalRoot = opts.globalRoot ?? (opts.userHome ? path3.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
2297
2359
  const homeDir = opts.userHome ?? os.homedir();
2298
- const hash = projectHash(opts.projectRoot);
2360
+ const hash2 = projectHash(opts.projectRoot);
2299
2361
  const slug = projectSlug(opts.projectRoot);
2300
2362
  const projectDir = path3.join(globalRoot, "projects", slug);
2301
2363
  return {
@@ -2304,6 +2366,11 @@ function resolveWstackPaths(opts) {
2304
2366
  homeDir,
2305
2367
  configDir: globalRoot,
2306
2368
  globalConfig: path3.join(globalRoot, "config.json"),
2369
+ profilesDir: path3.join(globalRoot, "profiles"),
2370
+ profileConfig: (name) => {
2371
+ const safe = name.replace(/[/\\:]/g, "_").replace(/\.\./g, "_");
2372
+ return path3.join(globalRoot, "profiles", safe || "default", "config.json");
2373
+ },
2307
2374
  secretsKey: path3.join(globalRoot, ".key"),
2308
2375
  globalMemory: path3.join(globalRoot, "memory.md"),
2309
2376
  globalSkills: path3.join(globalRoot, "skills"),
@@ -2332,7 +2399,7 @@ function resolveWstackPaths(opts) {
2332
2399
  inProjectInstructions: path3.join(opts.projectRoot, ".wrongstack", "instructions"),
2333
2400
  inProjectDesignKits: path3.join(opts.projectRoot, ".wrongstack", "design-kits"),
2334
2401
  inProjectWorktrees: path3.join(opts.projectRoot, ".wrongstack", "worktrees"),
2335
- projectHash: hash,
2402
+ projectHash: hash2,
2336
2403
  projectSlug: slug,
2337
2404
  projectGoal: path3.join(projectDir, "goal.json"),
2338
2405
  projectInputHistory: path3.join(projectDir, "input-history.json"),
@@ -2636,7 +2703,7 @@ var InMemoryAgentBridge = class {
2636
2703
  });
2637
2704
  }
2638
2705
  this.inflightGuards.add(correlationId);
2639
- return new Promise((resolve12, reject) => {
2706
+ return new Promise((resolve14, reject) => {
2640
2707
  const timer = setTimeout(() => {
2641
2708
  this.inflightGuards.delete(correlationId);
2642
2709
  this.pendingRequests.delete(correlationId);
@@ -2655,7 +2722,7 @@ var InMemoryAgentBridge = class {
2655
2722
  return;
2656
2723
  }
2657
2724
  this.pendingRequests.set(correlationId, {
2658
- resolve: resolve12,
2725
+ resolve: resolve14,
2659
2726
  reject,
2660
2727
  timer
2661
2728
  });
@@ -2898,7 +2965,7 @@ var SubagentBudget = class _SubagentBudget {
2898
2965
  *
2899
2966
  * Returns the kinds that were found to be exceeded (for logging/debugging).
2900
2967
  */
2901
- checkLimits(elapsedMs) {
2968
+ checkLimits(elapsedMs2) {
2902
2969
  const exceeded = [];
2903
2970
  if (this.limits.maxIterations !== void 0 && this.iterations > this.limits.maxIterations) {
2904
2971
  exceeded.push({ kind: "iterations", used: this.iterations, limit: this.limits.maxIterations });
@@ -2913,14 +2980,14 @@ var SubagentBudget = class _SubagentBudget {
2913
2980
  if (this.limits.maxCostUsd !== void 0 && this.costUsd > this.limits.maxCostUsd) {
2914
2981
  exceeded.push({ kind: "cost", used: this.costUsd, limit: this.limits.maxCostUsd });
2915
2982
  }
2916
- if (elapsedMs !== void 0) {
2983
+ if (elapsedMs2 !== void 0) {
2917
2984
  const idle = this.idleMs();
2918
2985
  if (this.limits.idleTimeoutMs !== void 0 && idle > this.limits.idleTimeoutMs) {
2919
2986
  exceeded.push({ kind: "idle_timeout", used: idle, limit: this.limits.idleTimeoutMs });
2920
2987
  }
2921
2988
  const wallOwnedByWatchdog = this._onThreshold !== void 0 && this._watchdogActive === this.limits.timeoutMs;
2922
- if (this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs && !wallOwnedByWatchdog) {
2923
- exceeded.push({ kind: "timeout", used: elapsedMs, limit: this.limits.timeoutMs });
2989
+ if (this.limits.timeoutMs !== void 0 && elapsedMs2 > this.limits.timeoutMs && !wallOwnedByWatchdog) {
2990
+ exceeded.push({ kind: "timeout", used: elapsedMs2, limit: this.limits.timeoutMs });
2924
2991
  }
2925
2992
  }
2926
2993
  if (exceeded.length === 0) return [];
@@ -3004,13 +3071,13 @@ var SubagentBudget = class _SubagentBudget {
3004
3071
  if (!bus?.hasListenerFor("budget.threshold_reached")) {
3005
3072
  return Promise.resolve("stop");
3006
3073
  }
3007
- return new Promise((resolve12) => {
3074
+ return new Promise((resolve14) => {
3008
3075
  let resolved = false;
3009
3076
  const respond = (d) => {
3010
3077
  if (resolved) return;
3011
3078
  resolved = true;
3012
3079
  clearTimeout(fallback);
3013
- resolve12(d);
3080
+ resolve14(d);
3014
3081
  };
3015
3082
  const fallback = setTimeout(() => respond("stop"), _SubagentBudget.DECISION_TIMEOUT_MS);
3016
3083
  const sessionId = this.currentSessionId();
@@ -3316,8 +3383,8 @@ function stringArray(value) {
3316
3383
 
3317
3384
  // src/coordination/agent-subagent-runner.ts
3318
3385
  function withDisabledToolFiltering(factory) {
3319
- return async (config) => {
3320
- const result = await factory(config);
3386
+ return async (config, task) => {
3387
+ const result = await factory(config, task);
3321
3388
  const disabled = config.disabledTools ?? [];
3322
3389
  if (disabled.length === 0) return result;
3323
3390
  const registry = result.agent.tools;
@@ -3333,10 +3400,36 @@ function makeAgentSubagentRunner(opts) {
3333
3400
  const format = opts.formatTaskInput ?? defaultFormatTaskInput;
3334
3401
  return async (task, ctx) => {
3335
3402
  const taskStartedAt = Date.now();
3336
- const factoryResult = await opts.factory(ctx.config);
3403
+ const factoryResult = await opts.factory(ctx.config, task);
3337
3404
  const { agent, events } = factoryResult;
3405
+ const taskContext = task.context ?? {};
3406
+ const hasExplicitTelemetryTask = typeof taskContext["telemetryTaskId"] === "string";
3407
+ const telemetryTaskId = hasExplicitTelemetryTask ? taskContext["telemetryTaskId"] : task.id;
3408
+ const telemetryRunId = typeof taskContext["telemetryRunId"] === "string" ? taskContext["telemetryRunId"] : void 0;
3409
+ const telemetryBoardId = typeof taskContext["telemetryBoardId"] === "string" ? taskContext["telemetryBoardId"] : void 0;
3338
3410
  const agentContext = agent.ctx;
3339
- if (agentContext?.meta) agentContext.meta["subagentTaskId"] = task.id;
3411
+ if (agentContext?.meta) {
3412
+ agentContext.meta["subagentTaskId"] = task.id;
3413
+ const kanbanContext = taskContext["kanban"];
3414
+ if (kanbanContext && typeof kanbanContext === "object") {
3415
+ agentContext.meta["kanban"] = kanbanContext;
3416
+ }
3417
+ if (hasExplicitTelemetryTask) {
3418
+ agentContext.meta["telemetryTaskId"] = telemetryTaskId;
3419
+ if (telemetryRunId) agentContext.meta["telemetryRunId"] = telemetryRunId;
3420
+ }
3421
+ }
3422
+ if (hasExplicitTelemetryTask) {
3423
+ agentContext?.setCurrentKanbanTask?.(telemetryTaskId, telemetryBoardId);
3424
+ } else {
3425
+ const kanbanContext = taskContext["kanban"];
3426
+ if (kanbanContext && typeof kanbanContext === "object") {
3427
+ const kanban = kanbanContext;
3428
+ const boardId = typeof kanban["boardId"] === "string" ? kanban["boardId"] : void 0;
3429
+ const taskId = typeof kanban["taskId"] === "string" ? kanban["taskId"] : void 0;
3430
+ if (taskId) agentContext?.setCurrentKanbanTask?.(taskId, boardId);
3431
+ }
3432
+ }
3340
3433
  const detachFleet = opts.fleetBus?.attach(ctx.subagentId, events, task.id);
3341
3434
  const aborter = new AbortController();
3342
3435
  ctx.budget._events = events;
@@ -3361,6 +3454,27 @@ function makeAgentSubagentRunner(opts) {
3361
3454
  const unsub = [];
3362
3455
  unsub.push(
3363
3456
  events.on("tool.executed", (e) => {
3457
+ if (hasExplicitTelemetryTask) {
3458
+ opts.hostEvents?.emit("subagent.tool_executed", {
3459
+ sessionId: e.sessionId,
3460
+ subagentId: ctx.subagentId,
3461
+ agentSessionId: e.sessionId,
3462
+ agentName: e.agentName ?? ctx.config.name,
3463
+ taskId: telemetryTaskId,
3464
+ runId: telemetryRunId,
3465
+ traceId: e.traceId,
3466
+ id: e.id,
3467
+ name: e.name,
3468
+ durationMs: e.durationMs,
3469
+ ok: e.ok,
3470
+ // Raw tool inputs may contain credentials or sensitive arguments.
3471
+ // Task telemetry crosses from the isolated worker bus to the shared
3472
+ // host bus, so expose only execution metadata at this boundary.
3473
+ outputBytes: e.outputBytes,
3474
+ outputTokens: e.outputTokens,
3475
+ outputLines: e.outputLines
3476
+ });
3477
+ }
3364
3478
  try {
3365
3479
  ctx.budget.recordToolCall();
3366
3480
  } catch (eb) {
@@ -3372,6 +3486,17 @@ function makeAgentSubagentRunner(opts) {
3372
3486
  lastToolFailed = null;
3373
3487
  }
3374
3488
  }),
3489
+ events.on("file.event", (e) => {
3490
+ if (!hasExplicitTelemetryTask) return;
3491
+ if (e.runId === telemetryRunId && e.taskId === telemetryTaskId) return;
3492
+ opts.hostEvents?.emit("file.event", {
3493
+ ...e,
3494
+ scope: "task",
3495
+ taskId: telemetryTaskId,
3496
+ boardId: telemetryBoardId,
3497
+ runId: telemetryRunId
3498
+ });
3499
+ }),
3375
3500
  events.on("provider.response", (e) => {
3376
3501
  try {
3377
3502
  ctx.budget.recordUsage(e.usage);
@@ -4665,6 +4790,40 @@ var DOMAIN_AGENTS = [
4665
4790
  "information architecture"
4666
4791
  ]
4667
4792
  }
4793
+ },
4794
+ {
4795
+ config: {
4796
+ id: "ios",
4797
+ name: "iOS",
4798
+ role: "ios",
4799
+ tools: [...TOOLS.build, "fetch"],
4800
+ prompt: agentPrompt("ios")
4801
+ },
4802
+ budget: HEAVY_BUDGET,
4803
+ capability: {
4804
+ phase: "domain",
4805
+ summary: "Apple-platform app development in Swift against the latest Xcode/iOS-SDK line: SwiftUI, UIKit, SwiftData, concurrency, accessibility, and App Store submission.",
4806
+ keywords: [
4807
+ "ios",
4808
+ "iphone",
4809
+ "ipad",
4810
+ "ipados",
4811
+ "watchos",
4812
+ "tvos",
4813
+ "visionos",
4814
+ "macos",
4815
+ "swift",
4816
+ "swiftui",
4817
+ "uikit",
4818
+ "xcode",
4819
+ "swiftdata",
4820
+ "app store",
4821
+ "app intents",
4822
+ "swift package manager",
4823
+ "cocoapods",
4824
+ "foundation models"
4825
+ ]
4826
+ }
4668
4827
  }
4669
4828
  ];
4670
4829
 
@@ -5239,7 +5398,11 @@ var FLEET_ROSTER_BUDGETS = {
5239
5398
  idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
5240
5399
  maxIterations: 1e3,
5241
5400
  maxToolCalls: 2500,
5242
- maxTokens: 3e4,
5401
+ // Monitoring passes routinely include a large system prompt plus fleet
5402
+ // context. 30k was below observed single-pass usage and therefore triggered
5403
+ // a post-response negotiation every run; 96k leaves headroom while the cost
5404
+ // and tool/iteration caps still bound runaway work.
5405
+ maxTokens: 96e3,
5243
5406
  maxCostUsd: 0.5
5244
5407
  },
5245
5408
  ...Object.fromEntries(
@@ -5642,7 +5805,7 @@ function createDelegateTool(opts) {
5642
5805
  };
5643
5806
  }
5644
5807
  async function awaitDelegateAttempt(director, subagentId, taskId, timeoutMs, abortSignal) {
5645
- return new Promise((resolve12) => {
5808
+ return new Promise((resolve14) => {
5646
5809
  let settled = false;
5647
5810
  let timer;
5648
5811
  let offAbort = () => {
@@ -5655,7 +5818,7 @@ async function awaitDelegateAttempt(director, subagentId, taskId, timeoutMs, abo
5655
5818
  offIter();
5656
5819
  offProgress();
5657
5820
  offAbort();
5658
- resolve12(value);
5821
+ resolve14(value);
5659
5822
  };
5660
5823
  const arm = () => {
5661
5824
  if (timer) clearTimeout(timer);
@@ -6351,7 +6514,7 @@ var CollabSession = class extends EventEmitter {
6351
6514
  }
6352
6515
  for (const filePath of allFiles) {
6353
6516
  try {
6354
- const [content, stat12] = await Promise.all([
6517
+ const [content, stat13] = await Promise.all([
6355
6518
  fsp4.readFile(filePath, "utf8"),
6356
6519
  fsp4.stat(filePath)
6357
6520
  ]);
@@ -6361,8 +6524,8 @@ var CollabSession = class extends EventEmitter {
6361
6524
  path: filePath,
6362
6525
  content,
6363
6526
  language,
6364
- snapshotMtimeMs: stat12.mtimeMs,
6365
- snapshotSizeBytes: stat12.size
6527
+ snapshotMtimeMs: stat13.mtimeMs,
6528
+ snapshotSizeBytes: stat13.size
6366
6529
  });
6367
6530
  } catch {
6368
6531
  this.snapshot.files.push({ path: filePath, content: "", language: void 0 });
@@ -6612,7 +6775,9 @@ Emit each evaluation immediately. Do not wait until you have read all reports.`;
6612
6775
  level: "warning" /* WARNING */,
6613
6776
  message: `${role} hit ${payload.kind} soft limit (${payload.used}/${payload.limit})`,
6614
6777
  budgetKind: payload.kind,
6615
- elapsedMs: payload.timeoutMs,
6778
+ // `used` is elapsed milliseconds for timeout kinds. `timeoutMs` is the
6779
+ // negotiation response deadline (normally 60s), not agent runtime.
6780
+ elapsedMs: payload.kind === "timeout" || payload.kind === "idle_timeout" ? payload.used : void 0,
6616
6781
  limit: payload.limit,
6617
6782
  btwNotes
6618
6783
  };
@@ -6636,12 +6801,10 @@ Emit each evaluation immediately. Do not wait until you have read all reports.`;
6636
6801
  return;
6637
6802
  }
6638
6803
  this.lastTimeoutProgress.set(e.subagentId, progress);
6639
- const newLimit = Math.min(
6640
- Math.ceil((payload.timeoutMs ?? payload.limit) * 2),
6641
- 24 * 60 * 6e4
6642
- );
6804
+ const newLimit = Math.min(Math.ceil(payload.limit * 2), 24 * 60 * 6e4);
6643
6805
  setImmediate(() => {
6644
- payload.extend({ timeoutMs: newLimit });
6806
+ const field = payload.kind === "timeout" ? "timeoutMs" : "idleTimeoutMs";
6807
+ payload.extend({ [field]: newLimit });
6645
6808
  });
6646
6809
  return;
6647
6810
  }
@@ -6781,9 +6944,9 @@ Emit each evaluation immediately. Do not wait until you have read all reports.`;
6781
6944
  for (const file of this.snapshot.files) {
6782
6945
  if (file.snapshotMtimeMs === void 0 && file.snapshotSizeBytes === void 0) continue;
6783
6946
  try {
6784
- const stat12 = await fsp4.stat(file.path);
6785
- const mtimeChanged = file.snapshotMtimeMs !== void 0 && stat12.mtimeMs > file.snapshotMtimeMs + 1;
6786
- const sizeChanged = file.snapshotSizeBytes !== void 0 && stat12.size !== file.snapshotSizeBytes;
6947
+ const stat13 = await fsp4.stat(file.path);
6948
+ const mtimeChanged = file.snapshotMtimeMs !== void 0 && stat13.mtimeMs > file.snapshotMtimeMs + 1;
6949
+ const sizeChanged = file.snapshotSizeBytes !== void 0 && stat13.size !== file.snapshotSizeBytes;
6787
6950
  if (mtimeChanged || sizeChanged) {
6788
6951
  warnings.push(`${file.path} changed after the collab snapshot was captured.`);
6789
6952
  }
@@ -7046,6 +7209,8 @@ function rosterSummaryFromConfigs(roster) {
7046
7209
  import { randomUUID as randomUUID4 } from "node:crypto";
7047
7210
  import {
7048
7211
  claimReadyTask,
7212
+ describeKanbanBoundary,
7213
+ heartbeatTaskAssignment,
7049
7214
  listReadyTasks,
7050
7215
  updateTaskAssignment
7051
7216
  } from "@wrongstack/kanban";
@@ -7800,13 +7965,19 @@ function makeKanbanQueueTool(director, roster) {
7800
7965
  const projectRoot = ctx.projectRoot;
7801
7966
  if (!projectRoot) return { error: "kanban_queue requires ctx.projectRoot." };
7802
7967
  const maxTasks = Math.max(1, Math.min(20, Math.floor(i.maxTasks ?? 1)));
7803
- const leaseTtlMs = Math.max(1e3, Math.floor(i.leaseTtlMs ?? 5 * 60 * 1e3));
7968
+ const baseLeaseTtlMs = Math.max(1e3, Math.floor(i.leaseTtlMs ?? 5 * 60 * 1e3));
7969
+ const leaseTtlMs = i.awaitCompletion === true && i.timeoutMs !== void 0 ? Math.max(baseLeaseTtlMs, i.timeoutMs + 6e4) : baseLeaseTtlMs;
7970
+ const heartbeatIntervalMs = Math.max(
7971
+ 5e3,
7972
+ i.heartbeatIntervalMs ?? Math.floor(leaseTtlMs / 2)
7973
+ );
7974
+ const effectiveLeaseTtlMs = Math.max(leaseTtlMs, heartbeatIntervalMs * 2);
7804
7975
  const claimedAt = nowIso();
7805
7976
  const leaseSeeding = {
7806
7977
  leaseId: randomUUID4(),
7807
7978
  claimedAt,
7808
7979
  heartbeatAt: claimedAt,
7809
- leaseExpiresAt: new Date(Date.now() + leaseTtlMs).toISOString()
7980
+ leaseExpiresAt: new Date(Date.now() + effectiveLeaseTtlMs).toISOString()
7810
7981
  };
7811
7982
  const candidateTaskIds = i.taskId !== void 0 ? [i.taskId] : i.query ? (await listReadyTasks(projectRoot, {
7812
7983
  ...i.boardId !== void 0 ? { boardId: i.boardId } : {}
@@ -7814,9 +7985,11 @@ function makeKanbanQueueTool(director, roster) {
7814
7985
  const dispatches = [];
7815
7986
  const errors = [];
7816
7987
  const resultFailures = [];
7988
+ const budgetRejectedTaskIds = /* @__PURE__ */ new Set();
7817
7989
  for (let index = 0; index < maxTasks; index++) {
7818
7990
  const candidateTaskId = candidateTaskIds?.[index];
7819
7991
  if (candidateTaskIds && !candidateTaskId) break;
7992
+ if (candidateTaskId && budgetRejectedTaskIds.has(candidateTaskId)) continue;
7820
7993
  const claim = await claimReadyTask(projectRoot, {
7821
7994
  ...i.boardId !== void 0 ? { boardId: i.boardId } : {},
7822
7995
  ...candidateTaskId !== void 0 ? { taskId: candidateTaskId } : {},
@@ -7841,14 +8014,22 @@ function makeKanbanQueueTool(director, roster) {
7841
8014
  if (costCeiling !== void 0) {
7842
8015
  const remaining = director.getRemainingBudgetUsd();
7843
8016
  if (remaining !== void 0 && remaining < costCeiling) {
7844
- await updateTaskAssignment(projectRoot, claim.board.id, claim.task.id, {
7845
- status: "failed",
7846
- error: `Cost ceiling ${costCeiling} exceeds remaining budget ${remaining.toFixed(4)}`,
7847
- lastResult: "Skipped by kanban_queue cost gate (Sprint 3)"
7848
- });
8017
+ budgetRejectedTaskIds.add(claim.task.id);
8018
+ const budgetError = `Cost ceiling ${costCeiling} exceeds remaining budget ${remaining.toFixed(4)}`;
8019
+ await updateTaskAssignment(
8020
+ projectRoot,
8021
+ claim.board.id,
8022
+ claim.task.id,
8023
+ {
8024
+ ...claim.task.assignment ?? {},
8025
+ status: "failed",
8026
+ error: budgetError
8027
+ },
8028
+ { expectedLeaseId: leaseSeeding.leaseId }
8029
+ );
7849
8030
  errors.push({
7850
8031
  taskId: claim.task.id,
7851
- error: `Cost ceiling ${costCeiling} exceeds remaining budget ${remaining.toFixed(4)}`
8032
+ error: budgetError
7852
8033
  });
7853
8034
  continue;
7854
8035
  }
@@ -7856,31 +8037,75 @@ function makeKanbanQueueTool(director, roster) {
7856
8037
  try {
7857
8038
  const config = buildKanbanSubagentConfig(claim.task, i, roster);
7858
8039
  subagentId = await director.spawn(config);
7859
- runTaskId = await director.assign({
7860
- id: randomUUID4(),
8040
+ const dispatchTaskId = randomUUID4();
8041
+ const taskSpec = {
8042
+ id: dispatchTaskId,
7861
8043
  subagentId,
7862
8044
  description: buildKanbanFleetTaskPrompt(claim.board, claim.task, {
7863
- heartbeatIntervalMs: i.heartbeatIntervalMs ?? 6e4,
7864
- leaseTtlMs,
8045
+ // The worker must heartbeat against the lease it actually holds.
8046
+ // effectiveLeaseTtlMs is the real lease window (raised from the
8047
+ // requested TTL when needed so the first heartbeat always lands
8048
+ // before expiry), and the computed heartbeatIntervalMs is always
8049
+ // strictly shorter than that window. Passing the raw requested
8050
+ // values here would give the worker a 60s cadence against a
8051
+ // 10s lease, letting the lease expire before its first renewal.
8052
+ heartbeatIntervalMs,
8053
+ leaseTtlMs: effectiveLeaseTtlMs,
7865
8054
  leaseId: leaseSeeding.leaseId,
7866
8055
  leaseExpiresAt: leaseSeeding.leaseExpiresAt
7867
8056
  }),
7868
8057
  ...i.maxToolCalls !== void 0 ? { maxToolCalls: i.maxToolCalls } : {},
7869
8058
  ...i.timeoutMs !== void 0 ? { timeoutMs: i.timeoutMs } : {},
7870
8059
  context: {
8060
+ ...claim.task.type !== void 0 ? { taskType: claim.task.type } : {},
7871
8061
  kanban: {
7872
8062
  boardId: claim.board.id,
7873
8063
  taskId: claim.task.id,
7874
- origin: claim.task.origin
8064
+ origin: claim.task.origin,
8065
+ projectRoot
7875
8066
  }
7876
8067
  }
7877
- });
7878
- await updateTaskAssignment(projectRoot, claim.board.id, claim.task.id, {
7879
- ...claim.task.assignment ?? {},
7880
- status: "running",
7881
- subagentId,
7882
- runTaskId
7883
- });
8068
+ };
8069
+ const dispatched = await updateTaskAssignment(
8070
+ projectRoot,
8071
+ claim.board.id,
8072
+ claim.task.id,
8073
+ {
8074
+ ...claim.task.assignment ?? {},
8075
+ status: "running",
8076
+ subagentId,
8077
+ runTaskId: dispatchTaskId,
8078
+ // Renew the lease from the actual dispatch moment, not from claim
8079
+ // time. The claim-time lease starts before spawn + queueing, so
8080
+ // `timeoutMs + 60_000` can expire while the worker is still
8081
+ // queued or running, letting recover_stale reclaim and duplicate
8082
+ // the assignment (see auto-extend.ts: timeout extensions can push
8083
+ // execution well past the original timeoutMs). Both heartbeatAt
8084
+ // and leaseExpiresAt are stamped from now so the worker has a
8085
+ // full lease window from the moment it was actually dispatched.
8086
+ heartbeatAt: (/* @__PURE__ */ new Date()).toISOString(),
8087
+ leaseExpiresAt: new Date(Date.now() + effectiveLeaseTtlMs).toISOString()
8088
+ },
8089
+ { expectedLeaseId: leaseSeeding.leaseId }
8090
+ );
8091
+ if (dispatched === null) {
8092
+ try {
8093
+ await director.terminate(subagentId);
8094
+ } catch (cleanupErr) {
8095
+ errors.push({
8096
+ taskId: claim.task.id,
8097
+ error: `Lease lost during dispatch; orphan subagent ${subagentId} termination failed: ${toErrorMessage(cleanupErr)}`
8098
+ });
8099
+ continue;
8100
+ }
8101
+ errors.push({
8102
+ taskId: claim.task.id,
8103
+ error: `Lease lost during dispatch (reassigned by recovery); subagent ${subagentId} terminated.`
8104
+ });
8105
+ continue;
8106
+ }
8107
+ await director.assign(taskSpec);
8108
+ runTaskId = dispatchTaskId;
7884
8109
  dispatches.push({
7885
8110
  boardId: claim.board.id,
7886
8111
  taskId: claim.task.id,
@@ -7899,28 +8124,97 @@ function makeKanbanQueueTool(director, roster) {
7899
8124
  message = `${message}; cleanup failed for spawned subagent ${subagentId}: ${toErrorMessage(cleanupErr)}`;
7900
8125
  }
7901
8126
  }
7902
- await updateTaskAssignment(projectRoot, claim.board.id, claim.task.id, {
7903
- ...claim.task.assignment ?? {},
7904
- status: "failed",
7905
- ...subagentId !== void 0 ? { subagentId } : {},
7906
- ...runTaskId !== void 0 ? { runTaskId } : {},
7907
- error: message
7908
- });
8127
+ await updateTaskAssignment(
8128
+ projectRoot,
8129
+ claim.board.id,
8130
+ claim.task.id,
8131
+ {
8132
+ ...claim.task.assignment ?? {},
8133
+ status: "failed",
8134
+ ...subagentId !== void 0 ? { subagentId } : {},
8135
+ ...runTaskId !== void 0 ? { runTaskId } : {},
8136
+ error: message
8137
+ },
8138
+ { expectedLeaseId: leaseSeeding.leaseId }
8139
+ );
7909
8140
  errors.push({ taskId: claim.task.id, error: message });
7910
8141
  }
7911
8142
  }
8143
+ if (!i.awaitCompletion && dispatches.length > 0 && typeof director.fleet?.subscribe === "function") {
8144
+ const ourLeaseId = leaseSeeding.leaseId;
8145
+ const disposers = /* @__PURE__ */ new Map();
8146
+ const renewalStartedAt = Date.now();
8147
+ const maxRenewalWindowMs = 24 * 60 * 6e4;
8148
+ const stopAll = () => {
8149
+ clearInterval(renewalTimer);
8150
+ for (const dispose of disposers.values()) dispose();
8151
+ disposers.clear();
8152
+ };
8153
+ const renewalTimer = setInterval(() => {
8154
+ if (disposers.size === 0 || Date.now() - renewalStartedAt > maxRenewalWindowMs) {
8155
+ stopAll();
8156
+ return;
8157
+ }
8158
+ const refreshedExpiry = new Date(Date.now() + effectiveLeaseTtlMs).toISOString();
8159
+ for (const dispatch of dispatches) {
8160
+ if (!disposers.has(dispatch.runTaskId)) continue;
8161
+ void heartbeatTaskAssignment(projectRoot, dispatch.boardId, dispatch.taskId, {
8162
+ leaseExpiresAt: refreshedExpiry,
8163
+ expectedLeaseId: ourLeaseId
8164
+ }).catch(() => {
8165
+ });
8166
+ }
8167
+ }, heartbeatIntervalMs);
8168
+ renewalTimer.unref?.();
8169
+ for (const dispatch of dispatches) {
8170
+ const dispose = director.fleet.subscribe(dispatch.subagentId, (event) => {
8171
+ if (event.type !== "subagent.completed" && event.type !== "subagent.stopped") return;
8172
+ if (event.type === "subagent.completed" && event.taskId !== dispatch.runTaskId) return;
8173
+ disposers.get(dispatch.runTaskId)?.();
8174
+ disposers.delete(dispatch.runTaskId);
8175
+ if (disposers.size === 0) clearInterval(renewalTimer);
8176
+ });
8177
+ disposers.set(dispatch.runTaskId, dispose);
8178
+ }
8179
+ }
7912
8180
  let results;
7913
8181
  if (i.awaitCompletion && dispatches.length > 0) {
7914
- results = await director.awaitTasks(dispatches.map((dispatch) => dispatch.runTaskId));
8182
+ const ourLeaseId = leaseSeeding.leaseId;
8183
+ const runTaskIds = new Set(dispatches.map((dispatch) => dispatch.runTaskId));
8184
+ const heartbeatTimer = setInterval(async () => {
8185
+ const refreshedExpiry = new Date(Date.now() + effectiveLeaseTtlMs).toISOString();
8186
+ for (const dispatch of dispatches) {
8187
+ if (!runTaskIds.has(dispatch.runTaskId)) continue;
8188
+ try {
8189
+ await heartbeatTaskAssignment(projectRoot, dispatch.boardId, dispatch.taskId, {
8190
+ leaseExpiresAt: refreshedExpiry,
8191
+ expectedLeaseId: ourLeaseId
8192
+ });
8193
+ } catch {
8194
+ }
8195
+ }
8196
+ }, heartbeatIntervalMs);
8197
+ try {
8198
+ results = await director.awaitTasks(dispatches.map((dispatch) => dispatch.runTaskId));
8199
+ } finally {
8200
+ clearInterval(heartbeatTimer);
8201
+ }
7915
8202
  for (const result of results) {
7916
8203
  const dispatch = dispatches.find((candidate) => candidate.runTaskId === result.taskId);
7917
8204
  if (!dispatch) continue;
7918
- await updateTaskAssignment(projectRoot, dispatch.boardId, dispatch.taskId, {
7919
- status: result.status === "success" ? "completed" : "failed",
7920
- subagentId: result.subagentId,
7921
- runTaskId: result.taskId,
7922
- ...result.status === "success" ? { lastResult: resultToText(result) } : { error: resultErrorText(result) }
7923
- });
8205
+ runTaskIds.delete(dispatch.runTaskId);
8206
+ await updateTaskAssignment(
8207
+ projectRoot,
8208
+ dispatch.boardId,
8209
+ dispatch.taskId,
8210
+ {
8211
+ status: result.status === "success" ? "completed" : "failed",
8212
+ subagentId: result.subagentId,
8213
+ runTaskId: result.taskId,
8214
+ ...result.status === "success" ? { lastResult: resultToText(result) } : { error: resultErrorText(result) }
8215
+ },
8216
+ { expectedLeaseId: ourLeaseId }
8217
+ );
7924
8218
  if (result.status !== "success") {
7925
8219
  resultFailures.push({
7926
8220
  taskId: dispatch.taskId,
@@ -8028,6 +8322,16 @@ function buildKanbanFleetTaskPrompt(board, task, lease) {
8028
8322
  task.origin.phaseId ? `phaseId: ${task.origin.phaseId}` : "",
8029
8323
  task.origin.taskId ? `sourceTaskId: ${task.origin.taskId}` : ""
8030
8324
  ].filter(Boolean).join("\n") : "";
8325
+ const boundaries = [
8326
+ board.boundary?.enabled ? `board: ${describeKanbanBoundary(board.boundary)}` : "",
8327
+ task.boundary?.enabled ? `task: ${describeKanbanBoundary(task.boundary)}` : "",
8328
+ ...(board.boundary?.allow ?? []).map(
8329
+ (selector) => `board allow ${selector.access} ${selector.kind}:${selector.path}`
8330
+ ),
8331
+ ...(task.boundary?.allow ?? []).map(
8332
+ (selector) => `task allow ${selector.access} ${selector.kind}:${selector.path}`
8333
+ )
8334
+ ].filter(Boolean);
8031
8335
  return [
8032
8336
  "You are processing a claimed WrongStack Kanban task.",
8033
8337
  "",
@@ -8050,6 +8354,8 @@ ${checks}` : "",
8050
8354
  metrics ? `Goal metrics:
8051
8355
  ${metrics}` : "",
8052
8356
  task.labels?.length ? `Labels: ${task.labels.join(", ")}` : "",
8357
+ boundaries.length ? `BOUNDARY CONTRACT (enforced by the tool runtime; do not attempt to bypass):
8358
+ ${boundaries.map((line) => `- ${line}`).join("\n")}` : "",
8053
8359
  "Lease contract (Sprint 1):",
8054
8360
  `- leaseId: ${lease.leaseId}`,
8055
8361
  `- claimedAt: ${task.assignment?.claimedAt ?? "<unknown>"}`,
@@ -8062,10 +8368,24 @@ ${metrics}` : "",
8062
8368
  `- maxAttempts: ${task.assignment?.maxAttempts ?? "<unset>"}`,
8063
8369
  `- costCeilingUsd: ${task.assignment?.costCeilingUsd ?? task.costCeilingUsd ?? "<unset>"}`,
8064
8370
  "",
8065
- "Work this task end-to-end. If scope is too broad or too small, use the kanban tool to split_task or merge_tasks instead of losing traceability.",
8066
- `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.`,
8067
- `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.`,
8068
- `When you finish, call kanban with action "mark_assignment", boardId "${board.id}", taskId "${task.id}", and assignmentStatus "completed" or "failed". Include lastResult or error.`,
8371
+ "Work this task end-to-end. Treat the board as a live plan, not a frozen assignment snapshot.",
8372
+ `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.`,
8373
+ "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.",
8374
+ "",
8375
+ "FIELD MAINTENANCE \u2014 keep every task detail current:",
8376
+ '- Call "update_task" to refresh description, priority, type, labels, or estimatedHours as the work evolves.',
8377
+ '- Call "add_note" with author and content to record progress, decisions, or roadblock context.',
8378
+ '- Call "add_check" / "update_check" with description and status (pending/passed/failed) to track acceptance criteria.',
8379
+ '- Call "add_goal_metric" / "update_goal_metric" with name and current/target to report measurable progress.',
8380
+ '- Call "add_link" with url and type (issue/pr/doc/commit/design/file/url/other) to attach evidence.',
8381
+ '- Call "update_task" to set actualHours when you have a final time estimate.',
8382
+ "Do not leave stale or placeholder field values \u2014 the kanban board is the shared source of truth for every stakeholder.",
8383
+ "",
8384
+ "LEASE & LIFECYCLE:",
8385
+ "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.",
8386
+ `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.`,
8387
+ `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.`,
8388
+ `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.`,
8069
8389
  `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.`,
8070
8390
  '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.',
8071
8391
  "When finished, report what changed, what you verified, and any remaining blockers."
@@ -8549,25 +8869,32 @@ var FleetBus = class {
8549
8869
  }
8550
8870
  emit(event) {
8551
8871
  const byId = this.byId.get(event.subagentId);
8552
- if (byId)
8553
- for (const h of byId) {
8872
+ if (byId && byId.size > 0) {
8873
+ const snapshot = [...byId];
8874
+ for (const h of snapshot) {
8554
8875
  try {
8555
8876
  h(event);
8556
8877
  } catch {
8557
8878
  }
8558
8879
  }
8880
+ }
8559
8881
  const byType = this.byType.get(event.type);
8560
- if (byType)
8561
- for (const h of byType) {
8882
+ if (byType && byType.size > 0) {
8883
+ const snapshot = [...byType];
8884
+ for (const h of snapshot) {
8562
8885
  try {
8563
8886
  h(event);
8564
8887
  } catch {
8565
8888
  }
8566
8889
  }
8567
- for (const h of this.any) {
8568
- try {
8569
- h(event);
8570
- } catch {
8890
+ }
8891
+ if (this.any.size > 0) {
8892
+ const snapshot = [...this.any];
8893
+ for (const h of snapshot) {
8894
+ try {
8895
+ h(event);
8896
+ } catch {
8897
+ }
8571
8898
  }
8572
8899
  }
8573
8900
  }
@@ -8749,21 +9076,28 @@ function hashStr(s) {
8749
9076
  return (h >>> 0).toString(36);
8750
9077
  }
8751
9078
 
9079
+ // src/types/blocks.ts
9080
+ function isTextBlock(b) {
9081
+ return b.type === "text";
9082
+ }
9083
+
8752
9084
  // src/types/provider.ts
8753
9085
  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;
8754
9086
  var CONTENT_FILTER_RE = /content.(filter|policy|moderation)|safety (system|filter)/i;
9087
+ 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;
8755
9088
  function classifyProviderError(status, body, message) {
8756
9089
  const type = body?.type;
9090
+ const text = [message, body?.message, type, body?.raw].filter(Boolean).join("\n");
8757
9091
  if (status === 0) return "network";
8758
9092
  if (status === 408) return "timeout";
8759
9093
  if (status === 599) return "stream_hang";
9094
+ if (status === 402 || QUOTA_EXHAUSTED_RE.test(text)) return "quota_exhausted";
8760
9095
  if (type === "rate_limit_error" || status === 429) return "rate_limit";
8761
9096
  if (type === "overloaded_error" || status === 529) return "overloaded";
8762
9097
  if (status >= 500) return "server";
8763
9098
  if (type === "authentication_error" || type === "permission_error" || status === 401 || status === 403) {
8764
9099
  return "auth";
8765
9100
  }
8766
- const text = [message, body?.message, type, body?.raw].filter(Boolean).join("\n");
8767
9101
  if (type === "content_filter" || CONTENT_FILTER_RE.test(text)) return "content_filter";
8768
9102
  if (status === 413 || status >= 400 && CONTEXT_OVERFLOW_RE.test(text)) {
8769
9103
  return "context_overflow";
@@ -8776,6 +9110,7 @@ function isRetryableKind(kind) {
8776
9110
  }
8777
9111
  var RETRYABLE_BY_KIND = {
8778
9112
  rate_limit: true,
9113
+ quota_exhausted: false,
8779
9114
  overloaded: true,
8780
9115
  server: true,
8781
9116
  timeout: true,
@@ -8854,6 +9189,7 @@ var KIND_TO_CODE = {
8854
9189
  network: ERROR_CODES.PROVIDER_NETWORK_ERROR,
8855
9190
  timeout: ERROR_CODES.PROVIDER_NETWORK_ERROR,
8856
9191
  rate_limit: ERROR_CODES.PROVIDER_RATE_LIMITED,
9192
+ quota_exhausted: ERROR_CODES.PROVIDER_RATE_LIMITED,
8857
9193
  auth: ERROR_CODES.PROVIDER_AUTH_FAILED,
8858
9194
  overloaded: ERROR_CODES.PROVIDER_OVERLOADED,
8859
9195
  context_overflow: ERROR_CODES.PROVIDER_CONTEXT_OVERFLOW,
@@ -8953,6 +9289,7 @@ function classifySubagentError(err, hints = {}) {
8953
9289
  function providerErrorToSubagentError(err, message, cause) {
8954
9290
  switch (err.kind) {
8955
9291
  case "rate_limit":
9292
+ case "quota_exhausted":
8956
9293
  return {
8957
9294
  kind: "provider_rate_limit",
8958
9295
  message,
@@ -9360,7 +9697,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
9360
9697
  taskIds.map((id) => {
9361
9698
  const cached = this.completedResults.find((r) => r.taskId === id);
9362
9699
  if (cached) return cached;
9363
- return new Promise((resolve12, reject) => {
9700
+ return new Promise((resolve14, reject) => {
9364
9701
  const timeout = setTimeout(() => {
9365
9702
  this.off("task.completed", handler);
9366
9703
  reject(new Error(`awaitTasks timed out waiting for task "${id}"`));
@@ -9369,7 +9706,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
9369
9706
  if (result.taskId === id) {
9370
9707
  clearTimeout(timeout);
9371
9708
  this.off("task.completed", handler);
9372
- resolve12(result);
9709
+ resolve14(result);
9373
9710
  }
9374
9711
  };
9375
9712
  this.on("task.completed", handler);
@@ -9394,13 +9731,13 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
9394
9731
  const done = new Set(completed.map((r) => r.taskId));
9395
9732
  return { completed, pending: taskIds.filter((id) => !done.has(id)) };
9396
9733
  }
9397
- return new Promise((resolve12) => {
9734
+ return new Promise((resolve14) => {
9398
9735
  let timer;
9399
9736
  const handler = ({ result }) => {
9400
9737
  if (!ids.has(result.taskId)) return;
9401
9738
  if (timer) clearTimeout(timer);
9402
9739
  this.off("task.completed", handler);
9403
- resolve12({
9740
+ resolve14({
9404
9741
  completed: [result],
9405
9742
  pending: taskIds.filter((id) => id !== result.taskId)
9406
9743
  });
@@ -9408,7 +9745,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
9408
9745
  if (opts?.timeoutMs !== void 0) {
9409
9746
  timer = setTimeout(() => {
9410
9747
  this.off("task.completed", handler);
9411
- resolve12({ completed: [], pending: [...taskIds], timedOut: true });
9748
+ resolve14({ completed: [], pending: [...taskIds], timedOut: true });
9412
9749
  }, opts.timeoutMs);
9413
9750
  }
9414
9751
  this.on("task.completed", handler);
@@ -9690,12 +10027,12 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
9690
10027
  }
9691
10028
  return new Promise((resolveDecision) => {
9692
10029
  let settled = false;
9693
- const resolve12 = (d) => {
10030
+ const resolve14 = (d) => {
9694
10031
  if (settled) return;
9695
10032
  settled = true;
9696
10033
  resolveDecision(d);
9697
10034
  };
9698
- const fallback = setTimeout(() => resolve12("stop"), DECISION_TIMEOUT_MS);
10035
+ const fallback = setTimeout(() => resolve14("stop"), DECISION_TIMEOUT_MS);
9699
10036
  const sessionId = this.currentSessionId();
9700
10037
  budget._events?.emit("budget.threshold_reached", {
9701
10038
  ...sessionId ? { sessionId } : {},
@@ -9713,11 +10050,11 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
9713
10050
  // disagreeing, resolves as a stop). Async grants still resolve.
9714
10051
  extend: (extra) => {
9715
10052
  clearTimeout(fallback);
9716
- queueMicrotask(() => resolve12({ extend: extra }));
10053
+ queueMicrotask(() => resolve14({ extend: extra }));
9717
10054
  },
9718
10055
  deny: () => {
9719
10056
  clearTimeout(fallback);
9720
- resolve12("stop");
10057
+ resolve14("stop");
9721
10058
  }
9722
10059
  });
9723
10060
  });
@@ -10340,6 +10677,8 @@ var Director = class _Director {
10340
10677
  collab;
10341
10678
  /** Prevents large `ask_subagent` answers from bloating the leader's context window. */
10342
10679
  largeAnswerStore;
10680
+ /** Shared provider/model status tracker, or undefined. */
10681
+ statusTracker;
10343
10682
  constructor(opts) {
10344
10683
  this.id = opts.config.coordinatorId || randomUUID6();
10345
10684
  this.brain = opts.brain;
@@ -10378,6 +10717,7 @@ var Director = class _Director {
10378
10717
  opts.checkpointDebounceMs ?? 250
10379
10718
  ) : null;
10380
10719
  this.fleetManager = opts.fleetManager;
10720
+ this.statusTracker = opts.statusTracker;
10381
10721
  this.logger = opts.logger;
10382
10722
  if (this.sharedScratchpadPath) {
10383
10723
  void fsp5.mkdir(this.sharedScratchpadPath, { recursive: true }).catch((err) => this.logShutdownError("shared_scratchpad_mkdir", err));
@@ -10500,6 +10840,14 @@ var Director = class _Director {
10500
10840
  title
10501
10841
  }
10502
10842
  );
10843
+ if (failed) {
10844
+ void this.appendSessionEvent({
10845
+ type: "agent_error",
10846
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
10847
+ agentId: r.subagentId,
10848
+ error: errorString ?? r.status
10849
+ });
10850
+ }
10503
10851
  if (this.fleetManager) {
10504
10852
  void this.fleetManager.flushManifest();
10505
10853
  } else {
@@ -10582,53 +10930,53 @@ var Director = class _Director {
10582
10930
  payload.extend(extra);
10583
10931
  });
10584
10932
  };
10585
- if (this.brain) {
10586
- void this.brain.decide({
10587
- id: `director-budget-${e.subagentId}-${payload.kind}`,
10588
- sessionId: this.currentSessionId(),
10589
- source: "director",
10590
- question: `Should the director extend the ${payload.kind} budget for subagent ${e.subagentId}?`,
10591
- context: [
10592
- e.taskId ? `Task id: ${e.taskId}` : void 0,
10593
- `Used: ${payload.used}`,
10594
- `Limit: ${payload.limit}`,
10595
- `Prior extensions for this kind: ${prior}`
10596
- ].filter(Boolean).join("\n"),
10597
- risk: payload.kind === "cost" ? "high" : "medium",
10598
- fallback: "continue",
10599
- options: [
10600
- {
10601
- id: "extend",
10602
- label: "Grant the director default budget extension",
10603
- consequence: "The subagent continues with a larger per-kind budget.",
10604
- risk: payload.kind === "cost" ? "high" : "medium",
10605
- recommended: true
10606
- },
10607
- {
10608
- id: "stop",
10609
- label: "Stop this subagent at the current budget limit",
10610
- consequence: "The current task will fail or stop due to budget pressure.",
10611
- risk: "low"
10612
- }
10613
- ]
10614
- }).then((decision) => {
10615
- if (decision.type === "deny") {
10616
- payload.deny();
10617
- return;
10618
- }
10619
- if (decision.type === "ask_human") {
10620
- payload.deny();
10621
- return;
10622
- }
10623
- if (decision.optionId === "stop") {
10624
- payload.deny();
10625
- return;
10626
- }
10627
- grantExtension();
10628
- }).catch(() => payload.deny());
10933
+ if (payload.kind !== "cost" || !this.brain) {
10934
+ grantExtension();
10629
10935
  return;
10630
10936
  }
10631
- grantExtension();
10937
+ void this.brain.decide({
10938
+ id: `director-budget-${e.subagentId}-${payload.kind}`,
10939
+ sessionId: this.currentSessionId(),
10940
+ source: "director",
10941
+ question: `Should the director extend the ${payload.kind} budget for subagent ${e.subagentId}?`,
10942
+ context: [
10943
+ e.taskId ? `Task id: ${e.taskId}` : void 0,
10944
+ `Used: ${payload.used}`,
10945
+ `Limit: ${payload.limit}`,
10946
+ `Prior extensions for this kind: ${prior}`
10947
+ ].filter(Boolean).join("\n"),
10948
+ risk: "high",
10949
+ fallback: "continue",
10950
+ options: [
10951
+ {
10952
+ id: "extend",
10953
+ label: "Grant the director default budget extension",
10954
+ consequence: "The subagent continues with a larger cost budget.",
10955
+ risk: "high",
10956
+ recommended: true
10957
+ },
10958
+ {
10959
+ id: "stop",
10960
+ label: "Stop this subagent at the current budget limit",
10961
+ consequence: "The current task will fail or stop due to budget pressure.",
10962
+ risk: "low"
10963
+ }
10964
+ ]
10965
+ }).then((decision) => {
10966
+ if (decision.type === "deny") {
10967
+ payload.deny();
10968
+ return;
10969
+ }
10970
+ if (decision.type === "ask_human") {
10971
+ payload.deny();
10972
+ return;
10973
+ }
10974
+ if (decision.optionId === "stop") {
10975
+ payload.deny();
10976
+ return;
10977
+ }
10978
+ grantExtension();
10979
+ }).catch(() => payload.deny());
10632
10980
  });
10633
10981
  }
10634
10982
  /**
@@ -10886,7 +11234,18 @@ var Director = class _Director {
10886
11234
  if (entry?.model) {
10887
11235
  config.model = entry.model;
10888
11236
  if (entry.provider) config.provider = entry.provider;
11237
+ if (entry.fallbackProfile) config.fallbackProfile = entry.fallbackProfile;
10889
11238
  if (entry.modelRuntime) config.modelRuntime = entry.modelRuntime;
11239
+ } else if (entry?.fallbackProfile) {
11240
+ config.fallbackProfile = entry.fallbackProfile;
11241
+ if (entry.modelRuntime) config.modelRuntime = entry.modelRuntime;
11242
+ }
11243
+ }
11244
+ if (this.statusTracker && config.provider && config.model) {
11245
+ if (!this.statusTracker.isAvailable(config.provider, config.model)) {
11246
+ this.logger?.warn(
11247
+ `spawn: resolved model "${config.provider}/${config.model}" for role "${config.role ?? "?"}" is blocked by the status tracker. The subagent will attempt its fallback chain.`
11248
+ );
10890
11249
  }
10891
11250
  }
10892
11251
  }
@@ -11291,11 +11650,11 @@ var Director = class _Director {
11291
11650
  if (cached) return cached;
11292
11651
  const existing = this.taskWaiters.get(id);
11293
11652
  if (existing) return existing.promise;
11294
- let resolve12;
11653
+ let resolve14;
11295
11654
  const promise = new Promise((res) => {
11296
- resolve12 = res;
11655
+ resolve14 = res;
11297
11656
  });
11298
- this.taskWaiters.set(id, { promise, resolve: resolve12 });
11657
+ this.taskWaiters.set(id, { promise, resolve: resolve14 });
11299
11658
  return promise;
11300
11659
  })
11301
11660
  );
@@ -11324,10 +11683,10 @@ var Director = class _Director {
11324
11683
  pending: taskIds.filter((id) => !done.has(id))
11325
11684
  });
11326
11685
  }
11327
- return new Promise((resolve12) => {
11686
+ return new Promise((resolve14) => {
11328
11687
  const entry = {
11329
11688
  ids: new Set(taskIds),
11330
- resolve: (r) => resolve12({
11689
+ resolve: (r) => resolve14({
11331
11690
  completed: [r],
11332
11691
  pending: taskIds.filter((id) => id !== r.taskId)
11333
11692
  })
@@ -11335,7 +11694,7 @@ var Director = class _Director {
11335
11694
  if (opts?.timeoutMs !== void 0) {
11336
11695
  entry.timer = setTimeout(() => {
11337
11696
  this.anyWaiters.delete(entry);
11338
- resolve12({ completed: [], pending: [...taskIds], timedOut: true });
11697
+ resolve14({ completed: [], pending: [...taskIds], timedOut: true });
11339
11698
  }, opts.timeoutMs);
11340
11699
  }
11341
11700
  this.anyWaiters.add(entry);
@@ -11396,6 +11755,11 @@ var Director = class _Director {
11396
11755
  }
11397
11756
  async remove(subagentId) {
11398
11757
  this.clearSubagentIdleRetirement(subagentId);
11758
+ void this.appendSessionEvent({
11759
+ type: "agent_stopped",
11760
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
11761
+ agentId: subagentId
11762
+ });
11399
11763
  await this.coordinator.remove(subagentId);
11400
11764
  const bridge = this.subagentBridges.get(subagentId);
11401
11765
  if (bridge) {
@@ -11770,24 +12134,24 @@ var FileSessionWriter = class _FileSessionWriter {
11770
12134
  /**
11771
12135
  * Scrub secrets out of conversation-turn events before they are observed
11772
12136
  * for the summary, written to the JSONL log, or surfaced on resume. Only
11773
- * `user_input`, `llm_response`, and `context_snapshot` carry free-form
11774
- * user/model text; other event types either have no secret-bearing content
11775
- * or are already scrubbed upstream (tool results). Snapshot normalization
11776
- * also strips transient token-cache fields even when no scrubber is set.
12137
+ * Conversation events carry free-form user/model text. Snapshot and exact
12138
+ * message-journal normalization also strips transient token-cache fields
12139
+ * even when no scrubber is set.
11777
12140
  */
11778
12141
  scrubEvent(event) {
11779
12142
  const s = this.secretScrubber;
11780
- if (event.type === "context_snapshot") {
12143
+ const persistMessage = (message) => {
12144
+ const { _estTokens: _ignored, ...persisted } = message;
11781
12145
  return {
11782
- ...event,
11783
- messages: event.messages.map((message) => {
11784
- const { _estTokens: _ignored, ...persisted } = message;
11785
- return {
11786
- ...persisted,
11787
- content: typeof persisted.content === "string" ? s?.scrub(persisted.content) ?? persisted.content : s?.scrubObject(persisted.content) ?? persisted.content
11788
- };
11789
- })
12146
+ ...persisted,
12147
+ content: typeof persisted.content === "string" ? s?.scrub(persisted.content) ?? persisted.content : s?.scrubObject(persisted.content) ?? persisted.content
11790
12148
  };
12149
+ };
12150
+ if (event.type === "context_snapshot" || event.type === "messages_replaced") {
12151
+ return { ...event, messages: event.messages.map(persistMessage) };
12152
+ }
12153
+ if (event.type === "message_appended" || event.type === "message_updated") {
12154
+ return { ...event, message: persistMessage(event.message) };
11791
12155
  }
11792
12156
  if (!s) return event;
11793
12157
  if (event.type === "user_input") {
@@ -11842,7 +12206,8 @@ var FileSessionWriter = class _FileSessionWriter {
11842
12206
  this.bufferSynchronousEvent(event);
11843
12207
  }
11844
12208
  recordFileObservation(input) {
11845
- if (!input.path || !/^[a-f\d]{64}$/i.test(input.hash) || !Number.isFinite(input.mtimeMs)) return;
12209
+ if (!input.path || !/^[a-f\d]{64}$/i.test(input.hash) || !Number.isFinite(input.mtimeMs))
12210
+ return;
11846
12211
  this.bufferSynchronousEvent({
11847
12212
  type: "file_observation",
11848
12213
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -12004,12 +12369,7 @@ var FileSessionWriter = class _FileSessionWriter {
12004
12369
  const now = Date.now();
12005
12370
  if (now - this.lastAppendWarnAt > 5e3) {
12006
12371
  const suppressed = this.appendFailCount - 1;
12007
- const tail = suppressed > 0 ? ` (+${suppressed} suppressed)` : "";
12008
- console.warn(
12009
- "[session] flush failed:",
12010
- toErrorMessage(err),
12011
- tail
12012
- );
12372
+ console.warn(JSON.stringify({ level: "warn", event: "session.flush_failed", sessionId: this.id, message: toErrorMessage(err), ...suppressed > 0 ? { suppressed } : {}, timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
12013
12373
  this.lastAppendWarnAt = now;
12014
12374
  this.appendFailCount = 0;
12015
12375
  }
@@ -12029,6 +12389,43 @@ var FileSessionWriter = class _FileSessionWriter {
12029
12389
  });
12030
12390
  }
12031
12391
  }
12392
+ /**
12393
+ * Rebuild every accumulated summary counter by replaying the JSONL as it now
12394
+ * stands on disk.
12395
+ *
12396
+ * Only truncation needs this: the counters are fed by `observeForSummary` as
12397
+ * events go by and cannot un-count. Reading the whole file is fine here —
12398
+ * rewind is a rare, explicitly user-driven operation, and the alternative
12399
+ * (reversing each counter per discarded event) has to stay in lockstep with
12400
+ * observeForSummary forever.
12401
+ *
12402
+ * `title` is deliberately re-derived too: the first surviving user_input may
12403
+ * differ once earlier prompts are gone.
12404
+ */
12405
+ async recomputeSummaryFromDisk() {
12406
+ if (!this.filePath) return;
12407
+ const raw = await fsp6.readFile(this.filePath, "utf8").catch(() => null);
12408
+ if (raw === null) return;
12409
+ this.iterationCount = 0;
12410
+ this.toolCallCount = 0;
12411
+ this.toolErrorCount = 0;
12412
+ this.toolBreakdown = {};
12413
+ this.fileChangeCount = 0;
12414
+ this.compactionCount = 0;
12415
+ this.outcome = void 0;
12416
+ this.openToolUses = /* @__PURE__ */ new Set();
12417
+ this.tokenIn = 0;
12418
+ this.tokenOut = 0;
12419
+ this.summary = { ...this.summary, title: "(empty session)", tokenTotal: 0 };
12420
+ for (const line of raw.split("\n")) {
12421
+ const trimmed = line.trim();
12422
+ if (trimmed.length === 0) continue;
12423
+ try {
12424
+ this.observeForSummary(JSON.parse(trimmed));
12425
+ } catch {
12426
+ }
12427
+ }
12428
+ }
12032
12429
  observeForSummary(event) {
12033
12430
  if (event.type === "llm_response") {
12034
12431
  for (const block of event.content) {
@@ -12079,10 +12476,7 @@ var FileSessionWriter = class _FileSessionWriter {
12079
12476
  }
12080
12477
  async doClose() {
12081
12478
  if (this.pendingFileSnapshots.length > 0) {
12082
- await this.writeFileSnapshot(
12083
- this.activePromptIndex ?? 0,
12084
- [...this.pendingFileSnapshots]
12085
- );
12479
+ await this.writeFileSnapshot(this.activePromptIndex ?? 0, [...this.pendingFileSnapshots]);
12086
12480
  this.pendingFileSnapshots = [];
12087
12481
  }
12088
12482
  this.closed = true;
@@ -12202,7 +12596,7 @@ var FileSessionWriter = class _FileSessionWriter {
12202
12596
  * so post-checkpoint content is never read or parsed — O(1) memory instead
12203
12597
  * of O(N) JSON.parse calls over the full file.
12204
12598
  */
12205
- async truncateToCheckpoint(targetPromptIndex) {
12599
+ async truncateToCheckpoint(targetPromptIndex, revertedFiles = []) {
12206
12600
  if (!this.filePath) return 0;
12207
12601
  if (this.flushTimer) {
12208
12602
  clearTimeout(this.flushTimer);
@@ -12213,7 +12607,7 @@ var FileSessionWriter = class _FileSessionWriter {
12213
12607
  const CHUNK_SIZE = 65536;
12214
12608
  let fd;
12215
12609
  let fileOffset = 0;
12216
- let lineStartOffset = 0;
12610
+ let pendingLine = Buffer.alloc(0);
12217
12611
  let checkpointByteOffset = -1;
12218
12612
  let removedCount = 0;
12219
12613
  let targetCheckpointSeen = false;
@@ -12223,17 +12617,18 @@ var FileSessionWriter = class _FileSessionWriter {
12223
12617
  const buf = Buffer.alloc(CHUNK_SIZE);
12224
12618
  const { bytesRead } = await fd.read(buf, 0, CHUNK_SIZE, fileOffset);
12225
12619
  if (bytesRead === 0) break;
12620
+ const carriedBytes = pendingLine.length;
12621
+ const chunk = carriedBytes === 0 ? buf.subarray(0, bytesRead) : Buffer.concat([pendingLine, buf.subarray(0, bytesRead)]);
12622
+ const chunkFileOffset = fileOffset - carriedBytes;
12226
12623
  let chunkPos = 0;
12227
- while (chunkPos < bytesRead) {
12228
- const idx = buf.indexOf("\n", chunkPos);
12229
- if (idx === -1) {
12230
- lineStartOffset = fileOffset + chunkPos;
12231
- break;
12232
- }
12624
+ while (chunkPos < chunk.length) {
12625
+ const idx = chunk.indexOf("\n", chunkPos);
12626
+ if (idx === -1) break;
12627
+ const lineStartOffset = chunkFileOffset + chunkPos;
12233
12628
  if (checkpointByteOffset !== -1) {
12234
12629
  removedCount++;
12235
12630
  } else {
12236
- const lineBytes = buf.subarray(chunkPos, idx);
12631
+ const lineBytes = chunk.subarray(chunkPos, idx);
12237
12632
  const line = new TextDecoder("utf-8", { fatal: false }).decode(lineBytes);
12238
12633
  if (line.trim()) {
12239
12634
  try {
@@ -12259,12 +12654,9 @@ var FileSessionWriter = class _FileSessionWriter {
12259
12654
  }
12260
12655
  }
12261
12656
  chunkPos = idx + 1;
12262
- lineStartOffset = fileOffset + chunkPos;
12263
12657
  }
12658
+ pendingLine = chunk.subarray(chunkPos);
12264
12659
  fileOffset += bytesRead;
12265
- if (chunkPos >= bytesRead) {
12266
- lineStartOffset = fileOffset;
12267
- }
12268
12660
  }
12269
12661
  } finally {
12270
12662
  await fd?.close();
@@ -12344,17 +12736,19 @@ var FileSessionWriter = class _FileSessionWriter {
12344
12736
  this.handle = await fsp6.open(this.filePath, "a", 384).catch(() => this.handle);
12345
12737
  throw err;
12346
12738
  }
12739
+ await this.recomputeSummaryFromDisk();
12740
+ const reverted = [...revertedFiles];
12347
12741
  await this.append({
12348
12742
  type: "rewound",
12349
12743
  ts: (/* @__PURE__ */ new Date()).toISOString(),
12350
12744
  toPromptIndex: targetPromptIndex,
12351
- revertedFiles: []
12745
+ revertedFiles: reverted
12352
12746
  });
12353
12747
  this.activePromptIndex = targetPromptIndex;
12354
12748
  this.events?.emit("session.rewound", {
12355
12749
  sessionId: this.id,
12356
12750
  toPromptIndex: targetPromptIndex,
12357
- revertedFiles: [],
12751
+ revertedFiles: reverted,
12358
12752
  removedEvents: removedCount
12359
12753
  });
12360
12754
  return removedCount;
@@ -12395,7 +12789,11 @@ var FileSessionWriter = class _FileSessionWriter {
12395
12789
  ts: (/* @__PURE__ */ new Date()).toISOString(),
12396
12790
  context
12397
12791
  });
12398
- this.events?.emit("in_flight.started", { sessionId: this.id, context, ts: (/* @__PURE__ */ new Date()).toISOString() });
12792
+ this.events?.emit("in_flight.started", {
12793
+ sessionId: this.id,
12794
+ context,
12795
+ ts: (/* @__PURE__ */ new Date()).toISOString()
12796
+ });
12399
12797
  }
12400
12798
  /**
12401
12799
  * Close the in-flight marker. Idempotent in spirit
@@ -12406,10 +12804,7 @@ var FileSessionWriter = class _FileSessionWriter {
12406
12804
  */
12407
12805
  async clearInFlightMarker(reason) {
12408
12806
  if (this.pendingFileSnapshots.length > 0) {
12409
- await this.writeFileSnapshot(
12410
- this.activePromptIndex ?? 0,
12411
- [...this.pendingFileSnapshots]
12412
- );
12807
+ await this.writeFileSnapshot(this.activePromptIndex ?? 0, [...this.pendingFileSnapshots]);
12413
12808
  this.pendingFileSnapshots = [];
12414
12809
  }
12415
12810
  await this.append({
@@ -12417,7 +12812,11 @@ var FileSessionWriter = class _FileSessionWriter {
12417
12812
  ts: (/* @__PURE__ */ new Date()).toISOString(),
12418
12813
  reason
12419
12814
  });
12420
- this.events?.emit("in_flight.ended", { sessionId: this.id, reason, ts: (/* @__PURE__ */ new Date()).toISOString() });
12815
+ this.events?.emit("in_flight.ended", {
12816
+ sessionId: this.id,
12817
+ reason,
12818
+ ts: (/* @__PURE__ */ new Date()).toISOString()
12819
+ });
12421
12820
  }
12422
12821
  };
12423
12822
 
@@ -12456,8 +12855,8 @@ function sha256(content) {
12456
12855
  return createHash2("sha256").update(content).digest("hex");
12457
12856
  }
12458
12857
  function isInside(root, target) {
12459
- const relative4 = path9.relative(root, target);
12460
- return relative4 === "" || !relative4.startsWith("..") && !path9.isAbsolute(relative4);
12858
+ const relative5 = path9.relative(root, target);
12859
+ return relative5 === "" || !relative5.startsWith("..") && !path9.isAbsolute(relative5);
12461
12860
  }
12462
12861
  function normalizeRelative(input) {
12463
12862
  if (!input || path9.isAbsolute(input)) return null;
@@ -12469,8 +12868,8 @@ function normalizeRelative(input) {
12469
12868
  function parseNulPaths(output) {
12470
12869
  return output.split("\0").map(normalizeRelative).filter((value) => value !== null);
12471
12870
  }
12472
- function isWrongStackWorktreePath(relative4) {
12473
- return relative4 === ".wrongstack/worktrees" || relative4.startsWith(".wrongstack/worktrees/");
12871
+ function isWrongStackWorktreePath(relative5) {
12872
+ return relative5 === ".wrongstack/worktrees" || relative5.startsWith(".wrongstack/worktrees/");
12474
12873
  }
12475
12874
  var SessionCheckpointCas = class {
12476
12875
  rootDir;
@@ -12496,60 +12895,60 @@ var SessionCheckpointCas = class {
12496
12895
  }
12497
12896
  const relativePaths = [
12498
12897
  .../* @__PURE__ */ new Set([...parseNulPaths(tracked.stdout), ...parseNulPaths(untracked.stdout)])
12499
- ].filter((relative4) => !isWrongStackWorktreePath(relative4)).sort();
12898
+ ].filter((relative5) => !isWrongStackWorktreePath(relative5)).sort();
12500
12899
  const unresolved = [];
12501
12900
  let scheduledBytes = 0;
12502
12901
  const captured = await mapWithConcurrency(
12503
12902
  relativePaths,
12504
12903
  CAPTURE_CONCURRENCY,
12505
- async (relative4) => {
12506
- const absolute = path9.resolve(this.projectRoot, ...relative4.split("/"));
12904
+ async (relative5) => {
12905
+ const absolute = path9.resolve(this.projectRoot, ...relative5.split("/"));
12507
12906
  if (!isInside(this.projectRoot, absolute)) {
12508
- unresolved.push({ path: relative4, reason: "path escapes project root" });
12907
+ unresolved.push({ path: relative5, reason: "path escapes project root" });
12509
12908
  return null;
12510
12909
  }
12511
12910
  try {
12512
- const stat12 = await fsp7.lstat(absolute);
12513
- if (stat12.isSymbolicLink()) {
12911
+ const stat13 = await fsp7.lstat(absolute);
12912
+ if (stat13.isSymbolicLink()) {
12514
12913
  const linkTarget = await fsp7.readlink(absolute);
12515
12914
  const resolvedLink = path9.resolve(path9.dirname(absolute), linkTarget);
12516
12915
  if (path9.isAbsolute(linkTarget) || !isInside(this.projectRoot, resolvedLink)) {
12517
12916
  unresolved.push({
12518
- path: relative4,
12917
+ path: relative5,
12519
12918
  reason: "symlink target escapes project root"
12520
12919
  });
12521
12920
  return null;
12522
12921
  }
12523
- return { path: relative4, state: "symlink", linkTarget };
12922
+ return { path: relative5, state: "symlink", linkTarget };
12524
12923
  }
12525
- if (!stat12.isFile()) {
12526
- unresolved.push({ path: relative4, reason: "changed path is not a regular file" });
12924
+ if (!stat13.isFile()) {
12925
+ unresolved.push({ path: relative5, reason: "changed path is not a regular file" });
12527
12926
  return null;
12528
12927
  }
12529
- if (stat12.size > MAX_BLOB_BYTES) {
12928
+ if (stat13.size > MAX_BLOB_BYTES) {
12530
12929
  unresolved.push({
12531
- path: relative4,
12930
+ path: relative5,
12532
12931
  reason: `file exceeds ${MAX_BLOB_BYTES}-byte checkpoint blob limit`
12533
12932
  });
12534
12933
  return null;
12535
12934
  }
12536
- if (scheduledBytes + stat12.size > MAX_CHECKPOINT_BYTES) {
12935
+ if (scheduledBytes + stat13.size > MAX_CHECKPOINT_BYTES) {
12537
12936
  unresolved.push({
12538
- path: relative4,
12937
+ path: relative5,
12539
12938
  reason: `checkpoint exceeds ${MAX_CHECKPOINT_BYTES}-byte aggregate blob limit`
12540
12939
  });
12541
12940
  return null;
12542
12941
  }
12543
- scheduledBytes += stat12.size;
12942
+ scheduledBytes += stat13.size;
12544
12943
  const content = await fsp7.readFile(absolute);
12545
12944
  const blobHash = sha256(content);
12546
12945
  await this.putBlob(blobHash, content);
12547
- return { path: relative4, state: "file", blobHash, mode: stat12.mode & 511 };
12946
+ return { path: relative5, state: "file", blobHash, mode: stat13.mode & 511 };
12548
12947
  } catch (err) {
12549
12948
  if (err.code === "ENOENT") {
12550
- return { path: relative4, state: "absent" };
12949
+ return { path: relative5, state: "absent" };
12551
12950
  }
12552
- unresolved.push({ path: relative4, reason: toErrorMessage(err) });
12951
+ unresolved.push({ path: relative5, reason: toErrorMessage(err) });
12553
12952
  return null;
12554
12953
  }
12555
12954
  }
@@ -12661,20 +13060,20 @@ var SessionCheckpointCas = class {
12661
13060
  }
12662
13061
  return { targetRoot: target, writtenFiles, deletedFiles, errors };
12663
13062
  }
12664
- objectPath(hash) {
12665
- if (!HASH_RE.test(hash)) throw new Error(`Invalid CAS object hash: ${hash}`);
12666
- return path9.join(this.rootDir, "objects", hash.slice(0, 2), hash.slice(2));
13063
+ objectPath(hash2) {
13064
+ if (!HASH_RE.test(hash2)) throw new Error(`Invalid CAS object hash: ${hash2}`);
13065
+ return path9.join(this.rootDir, "objects", hash2.slice(0, 2), hash2.slice(2));
12667
13066
  }
12668
- manifestPath(hash) {
12669
- if (!HASH_RE.test(hash)) throw new Error(`Invalid checkpoint manifest hash: ${hash}`);
12670
- return path9.join(this.rootDir, "manifests", `${hash}.json`);
13067
+ manifestPath(hash2) {
13068
+ if (!HASH_RE.test(hash2)) throw new Error(`Invalid checkpoint manifest hash: ${hash2}`);
13069
+ return path9.join(this.rootDir, "manifests", `${hash2}.json`);
12671
13070
  }
12672
- async putBlob(hash, content) {
12673
- const target = this.objectPath(hash);
13071
+ async putBlob(hash2, content) {
13072
+ const target = this.objectPath(hash2);
12674
13073
  await fsp7.mkdir(path9.dirname(target), { recursive: true });
12675
13074
  try {
12676
13075
  const existing = await fsp7.readFile(target);
12677
- if (sha256(existing) !== hash) throw new Error(`Corrupt CAS object collision: ${hash}`);
13076
+ if (sha256(existing) !== hash2) throw new Error(`Corrupt CAS object collision: ${hash2}`);
12678
13077
  return;
12679
13078
  } catch (err) {
12680
13079
  if (err.code !== "ENOENT") throw err;
@@ -12695,33 +13094,33 @@ var SessionCheckpointCas = class {
12695
13094
  } catch (err) {
12696
13095
  if (err.code !== "EEXIST") throw err;
12697
13096
  const existing = await fsp7.readFile(target);
12698
- if (sha256(existing) !== hash) throw new Error(`Corrupt CAS object collision: ${hash}`);
13097
+ if (sha256(existing) !== hash2) throw new Error(`Corrupt CAS object collision: ${hash2}`);
12699
13098
  }
12700
13099
  } finally {
12701
13100
  await handle?.close().catch(() => void 0);
12702
13101
  await fsp7.unlink(temp).catch(() => void 0);
12703
13102
  }
12704
13103
  }
12705
- async putManifest(hash, encoded) {
12706
- const target = this.manifestPath(hash);
13104
+ async putManifest(hash2, encoded) {
13105
+ const target = this.manifestPath(hash2);
12707
13106
  const existing = await fsp7.readFile(target).catch(() => null);
12708
13107
  if (existing) {
12709
- if (sha256(existing) !== hash) throw new Error(`Corrupt checkpoint manifest collision: ${hash}`);
13108
+ if (sha256(existing) !== hash2) throw new Error(`Corrupt checkpoint manifest collision: ${hash2}`);
12710
13109
  return;
12711
13110
  }
12712
13111
  await atomicWrite(target, encoded, { mode: 384 });
12713
13112
  }
12714
- async readBlob(hash) {
12715
- const content = await fsp7.readFile(this.objectPath(hash));
12716
- if (sha256(content) !== hash) throw new Error(`CAS blob integrity check failed: ${hash}`);
13113
+ async readBlob(hash2) {
13114
+ const content = await fsp7.readFile(this.objectPath(hash2));
13115
+ if (sha256(content) !== hash2) throw new Error(`CAS blob integrity check failed: ${hash2}`);
12717
13116
  return content;
12718
13117
  }
12719
- async loadManifest(hash) {
12720
- const raw = await fsp7.readFile(this.manifestPath(hash));
12721
- if (sha256(raw) !== hash) throw new Error(`Checkpoint manifest integrity check failed: ${hash}`);
13118
+ async loadManifest(hash2) {
13119
+ const raw = await fsp7.readFile(this.manifestPath(hash2));
13120
+ if (sha256(raw) !== hash2) throw new Error(`Checkpoint manifest integrity check failed: ${hash2}`);
12722
13121
  const parsed = JSON.parse(raw.toString("utf8"));
12723
13122
  if (parsed.version !== 1 || typeof parsed.baseHead !== "string" || parsed.coverage !== "git-head-plus-dirty" || !Array.isArray(parsed.entries) || !Array.isArray(parsed.unresolved)) {
12724
- throw new Error(`Invalid workspace checkpoint manifest: ${hash}`);
13123
+ throw new Error(`Invalid workspace checkpoint manifest: ${hash2}`);
12725
13124
  }
12726
13125
  const entriesValid = parsed.entries.every((entry) => {
12727
13126
  if (!entry || typeof entry !== "object") return false;
@@ -12735,12 +13134,12 @@ var SessionCheckpointCas = class {
12735
13134
  (entry) => entry !== null && typeof entry === "object" && typeof entry.path === "string" && typeof entry.reason === "string"
12736
13135
  );
12737
13136
  if (!entriesValid || !unresolvedValid) {
12738
- throw new Error(`Invalid workspace checkpoint manifest entries: ${hash}`);
13137
+ throw new Error(`Invalid workspace checkpoint manifest entries: ${hash2}`);
12739
13138
  }
12740
13139
  return parsed;
12741
13140
  }
12742
- async safeOutputPath(target, realTarget, relative4) {
12743
- const normalized = normalizeRelative(relative4);
13141
+ async safeOutputPath(target, realTarget, relative5) {
13142
+ const normalized = normalizeRelative(relative5);
12744
13143
  if (!normalized) throw new Error("invalid relative path");
12745
13144
  const output = path9.resolve(target, ...normalized.split("/"));
12746
13145
  if (!isInside(target, output)) throw new Error("path escapes checkpoint target");
@@ -12760,7 +13159,7 @@ var SessionCheckpointCas = class {
12760
13159
  }
12761
13160
  };
12762
13161
  function defaultRunGit(args, cwd) {
12763
- return new Promise((resolve12) => {
13162
+ return new Promise((resolve14) => {
12764
13163
  const stdoutChunks = [];
12765
13164
  const stderrChunks = [];
12766
13165
  let stdoutBytes = 0;
@@ -12803,11 +13202,18 @@ function defaultRunGit(args, cwd) {
12803
13202
  stdoutTruncated,
12804
13203
  stderrTruncated
12805
13204
  });
12806
- child.on("error", (err) => resolve12(result(1, err.message)));
12807
- child.on("close", (code) => resolve12(result(code ?? 1)));
13205
+ child.on("error", (err) => resolve14(result(1, err.message)));
13206
+ child.on("close", (code) => resolve14(result(code ?? 1)));
12808
13207
  });
12809
13208
  }
12810
13209
 
13210
+ // src/storage/session-id.ts
13211
+ function generateSessionId(startedAt, _model) {
13212
+ const date = startedAt.slice(0, 10);
13213
+ const seedTime = Number.isNaN(Date.parse(startedAt)) ? Date.now() : Date.parse(startedAt);
13214
+ return `${date}/sess_${ulid(seedTime)}`;
13215
+ }
13216
+
12811
13217
  // src/storage/session-resume-validation.ts
12812
13218
  import { createHash as createHash3 } from "node:crypto";
12813
13219
  import * as fsp8 from "node:fs/promises";
@@ -12816,8 +13222,8 @@ var MAX_REVALIDATE_BYTES = 5 * 1024 * 1024;
12816
13222
  var VALIDATION_CONCURRENCY = 8;
12817
13223
  var NOTICE_PATH_LIMIT = 20;
12818
13224
  function isInside2(root, target) {
12819
- const relative4 = path10.relative(root, target);
12820
- return relative4 === "" || !relative4.startsWith("..") && !path10.isAbsolute(relative4);
13225
+ const relative5 = path10.relative(root, target);
13226
+ return relative5 === "" || !relative5.startsWith("..") && !path10.isAbsolute(relative5);
12821
13227
  }
12822
13228
  function errno(err) {
12823
13229
  return err && typeof err === "object" && "code" in err ? String(err.code) : void 0;
@@ -12865,11 +13271,11 @@ async function validateOne(observation, lexicalRoot, realRoot) {
12865
13271
  };
12866
13272
  }
12867
13273
  try {
12868
- const stat12 = await fsp8.stat(realFile);
12869
- if (!stat12.isFile()) {
13274
+ const stat13 = await fsp8.stat(realFile);
13275
+ if (!stat13.isFile()) {
12870
13276
  return { ...base, status: "unreadable", detail: "Path is no longer a regular file." };
12871
13277
  }
12872
- if (stat12.size > MAX_REVALIDATE_BYTES) {
13278
+ if (stat13.size > MAX_REVALIDATE_BYTES) {
12873
13279
  return {
12874
13280
  ...base,
12875
13281
  status: "unreadable",
@@ -12904,8 +13310,8 @@ function formatResumeValidationNotice(validation, projectRoot) {
12904
13310
  if (validation.staleFiles.length === 0) return null;
12905
13311
  const root = path10.resolve(projectRoot);
12906
13312
  const shown = validation.staleFiles.slice(0, NOTICE_PATH_LIMIT).map((entry) => {
12907
- const relative4 = path10.relative(root, entry.path);
12908
- const display = isInside2(root, entry.path) ? relative4 || "." : entry.path;
13313
+ const relative5 = path10.relative(root, entry.path);
13314
+ const display = isInside2(root, entry.path) ? relative5 || "." : entry.path;
12909
13315
  return `- ${JSON.stringify(display)} [${entry.status}]`;
12910
13316
  });
12911
13317
  const omitted = validation.staleFiles.length - shown.length;
@@ -12917,12 +13323,14 @@ function formatResumeValidationNotice(validation, projectRoot) {
12917
13323
  ...shown
12918
13324
  ].join("\n");
12919
13325
  }
12920
-
12921
- // src/storage/session-id.ts
12922
- function generateSessionId(startedAt, _model) {
12923
- const date = startedAt.slice(0, 10);
12924
- const seedTime = Number.isNaN(Date.parse(startedAt)) ? Date.now() : Date.parse(startedAt);
12925
- return `${date}/sess_${ulid(seedTime)}`;
13326
+ function formatInterruptedToolNotice(pendingToolUseCount) {
13327
+ if (!Number.isFinite(pendingToolUseCount) || pendingToolUseCount <= 0) return null;
13328
+ const plural = pendingToolUseCount === 1 ? "tool call was" : "tool calls were";
13329
+ return [
13330
+ "[SESSION RESUME INTERRUPTED WORK]",
13331
+ `The previous run left ${pendingToolUseCount} ${plural} in flight \u2014 no result was recorded before it stopped (crash or interrupt).`,
13332
+ "Those interrupted tool calls were removed from the restored conversation and were NOT re-executed. Re-run the work if it still needs doing."
13333
+ ].join("\n");
12926
13334
  }
12927
13335
 
12928
13336
  // src/storage/session-summary.ts
@@ -12968,18 +13376,27 @@ function isReplayableMessage(value) {
12968
13376
  const candidate = value;
12969
13377
  return (candidate.role === "user" || candidate.role === "assistant" || candidate.role === "system") && (typeof candidate.content === "string" || Array.isArray(candidate.content));
12970
13378
  }
13379
+ function trackMessageToolState(message, openToolUses) {
13380
+ if (!Array.isArray(message.content)) return;
13381
+ for (const block of message.content) {
13382
+ if (block.type === "tool_use") openToolUses.add(block.id);
13383
+ else if (block.type === "tool_result") openToolUses.delete(block.tool_use_id);
13384
+ }
13385
+ }
13386
+ function replayableMessage(value, fallbackTs) {
13387
+ if (!isReplayableMessage(value)) return null;
13388
+ const { _estTokens: _ignored, ...message } = value;
13389
+ return message.ts === void 0 && fallbackTs ? { ...message, ts: fallbackTs } : message;
13390
+ }
12971
13391
  function applyContextSnapshot(target, openToolUses, snapshot) {
12972
13392
  if (!Array.isArray(snapshot) || !snapshot.every(isReplayableMessage)) return false;
12973
13393
  target.length = 0;
12974
13394
  openToolUses.clear();
12975
13395
  for (const raw of snapshot) {
12976
- const { _estTokens: _ignored, ...message } = raw;
13396
+ const message = replayableMessage(raw);
13397
+ if (!message) return false;
12977
13398
  target.push(message);
12978
- if (!Array.isArray(message.content)) continue;
12979
- for (const block of message.content) {
12980
- if (block.type === "tool_use") openToolUses.add(block.id);
12981
- else if (block.type === "tool_result") openToolUses.delete(block.tool_use_id);
12982
- }
13399
+ trackMessageToolState(message, openToolUses);
12983
13400
  }
12984
13401
  return true;
12985
13402
  }
@@ -13132,10 +13549,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
13132
13549
  handle = await fsp9.open(file, "a", 384);
13133
13550
  } catch (err) {
13134
13551
  this.emitError(id, file, "create", toErrorMessage(err), false);
13135
- throw new Error(
13136
- `Failed to open session file: ${toErrorMessage(err)}`,
13137
- { cause: err }
13138
- );
13552
+ throw new Error(`Failed to open session file: ${toErrorMessage(err)}`, { cause: err });
13139
13553
  }
13140
13554
  try {
13141
13555
  const writer = new FileSessionWriter(id, handle, startedAt, meta, this.events, {
@@ -13148,10 +13562,12 @@ var DefaultSessionStore = class _DefaultSessionStore {
13148
13562
  this.emitWrite(id, file, "create", "success", Date.now() - t0);
13149
13563
  return writer;
13150
13564
  } catch (err) {
13151
- await handle.close().catch((e) => this.logWarn(
13152
- "Session handle close failed",
13153
- { event: "session_store.handle_close_failed", message: e instanceof Error ? e.message : String(e) }
13154
- ));
13565
+ await handle.close().catch(
13566
+ (e) => this.logWarn("Session handle close failed", {
13567
+ event: "session_store.handle_close_failed",
13568
+ message: e instanceof Error ? e.message : String(e)
13569
+ })
13570
+ );
13155
13571
  this.emitError(id, file, "create", toErrorMessage(err), true);
13156
13572
  throw err;
13157
13573
  }
@@ -13170,9 +13586,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
13170
13586
  }
13171
13587
  }
13172
13588
  if (boundary === -1) {
13173
- throw new Error(
13174
- `Checkpoint ${opts.checkpointPromptIndex} not found in session "${id}"`
13175
- );
13589
+ throw new Error(`Checkpoint ${opts.checkpointPromptIndex} not found in session "${id}"`);
13176
13590
  }
13177
13591
  }
13178
13592
  const parentPrefix = parent.events.slice(0, boundary + 1);
@@ -13219,7 +13633,9 @@ var DefaultSessionStore = class _DefaultSessionStore {
13219
13633
  }
13220
13634
  async materializeWorkspaceCheckpoint(checkpoint, targetRoot) {
13221
13635
  if (!this.checkpointCas) {
13222
- throw new Error("Workspace checkpoint materialization requires a projectRoot-aware session store");
13636
+ throw new Error(
13637
+ "Workspace checkpoint materialization requires a projectRoot-aware session store"
13638
+ );
13223
13639
  }
13224
13640
  return this.checkpointCas.materialize(checkpoint, targetRoot);
13225
13641
  }
@@ -13227,34 +13643,44 @@ var DefaultSessionStore = class _DefaultSessionStore {
13227
13643
  const file = this.sessionPath(id, ".jsonl");
13228
13644
  const t0 = Date.now();
13229
13645
  const data = await this.load(id);
13230
- let resumedData = data;
13646
+ const noticeMessages = [];
13647
+ let resumeValidation;
13231
13648
  if (this.projectRoot) {
13232
13649
  try {
13233
- const resumeValidation = await validateResumeFileObservations(data.events, this.projectRoot);
13650
+ resumeValidation = await validateResumeFileObservations(data.events, this.projectRoot);
13234
13651
  const notice = formatResumeValidationNotice(resumeValidation, this.projectRoot);
13235
- resumedData = {
13236
- ...data,
13237
- resumeValidation,
13238
- ...notice ? {
13239
- messages: [
13240
- ...data.messages,
13241
- { role: "system", content: notice, ts: resumeValidation.checkedAt }
13242
- ]
13243
- } : {}
13244
- };
13652
+ if (notice) {
13653
+ noticeMessages.push({
13654
+ role: "system",
13655
+ content: notice,
13656
+ ts: resumeValidation.checkedAt
13657
+ });
13658
+ }
13245
13659
  } catch (err) {
13246
13660
  this.emitError(id, file, "resume_validation", toErrorMessage(err), true);
13247
13661
  }
13248
13662
  }
13663
+ const interruptedNotice = formatInterruptedToolNotice(data.pendingToolUseCount ?? 0);
13664
+ if (interruptedNotice) {
13665
+ noticeMessages.push({
13666
+ role: "system",
13667
+ content: interruptedNotice,
13668
+ ts: (/* @__PURE__ */ new Date()).toISOString()
13669
+ });
13670
+ }
13671
+ const resumedData = {
13672
+ ...data,
13673
+ ...resumeValidation ? { resumeValidation } : {},
13674
+ ...noticeMessages.length > 0 ? { messages: [...data.messages, ...noticeMessages] } : {}
13675
+ };
13249
13676
  let handle;
13250
13677
  try {
13251
13678
  handle = await fsp9.open(file, "a", 384);
13252
13679
  } catch (err) {
13253
13680
  this.emitError(id, file, "resume", toErrorMessage(err), false);
13254
- throw new Error(
13255
- `Failed to open session "${id}" for append: ${toErrorMessage(err)}`,
13256
- { cause: err }
13257
- );
13681
+ throw new Error(`Failed to open session "${id}" for append: ${toErrorMessage(err)}`, {
13682
+ cause: err
13683
+ });
13258
13684
  }
13259
13685
  try {
13260
13686
  const writer = new FileSessionWriter(
@@ -13282,10 +13708,12 @@ var DefaultSessionStore = class _DefaultSessionStore {
13282
13708
  this.emitWrite(id, file, "resume", "success", Date.now() - t0);
13283
13709
  return { writer, data: resumedData };
13284
13710
  } catch (err) {
13285
- await handle.close().catch((e) => this.logWarn(
13286
- "Session handle close failed",
13287
- { event: "session_store.handle_close_failed", message: e instanceof Error ? e.message : String(e) }
13288
- ));
13711
+ await handle.close().catch(
13712
+ (e) => this.logWarn("Session handle close failed", {
13713
+ event: "session_store.handle_close_failed",
13714
+ message: e instanceof Error ? e.message : String(e)
13715
+ })
13716
+ );
13289
13717
  this.emitError(id, file, "resume", toErrorMessage(err), true);
13290
13718
  throw err;
13291
13719
  }
@@ -13318,9 +13746,9 @@ var DefaultSessionStore = class _DefaultSessionStore {
13318
13746
  let cacheHit = false;
13319
13747
  try {
13320
13748
  const s = await fsp9.stat(file);
13321
- const stat12 = { mtimeMs: s.mtimeMs, size: s.size };
13749
+ const stat13 = { mtimeMs: s.mtimeMs, size: s.size };
13322
13750
  const cached = this._loadCache.get(id);
13323
- if (cached && cached.mtimeMs === stat12.mtimeMs && cached.size === stat12.size) {
13751
+ if (cached && cached.mtimeMs === stat13.mtimeMs && cached.size === stat13.size) {
13324
13752
  cacheHit = true;
13325
13753
  this._loadCache.delete(id);
13326
13754
  this._loadCache.set(id, cached);
@@ -13336,6 +13764,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
13336
13764
  let sessionForkedEvent;
13337
13765
  const messages = mode.full ? [] : void 0;
13338
13766
  const openToolUses = mode.full ? /* @__PURE__ */ new Set() : void 0;
13767
+ let exactJournalActive = false;
13339
13768
  let usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
13340
13769
  const stream = createReadStream(file, { encoding: "utf8" });
13341
13770
  const rl = createInterface({ input: stream, crlfDelay: Infinity });
@@ -13360,20 +13789,59 @@ var DefaultSessionStore = class _DefaultSessionStore {
13360
13789
  sessionForkedEvent = ev;
13361
13790
  }
13362
13791
  if (mode.full && messages !== void 0 && openToolUses !== void 0) {
13363
- if (ev.type === "context_snapshot") {
13792
+ if (ev.type === "message_appended" && ev.version === 1) {
13793
+ const message = replayableMessage(ev.message, ev.ts);
13794
+ if (message) {
13795
+ if (!exactJournalActive) {
13796
+ messages.length = 0;
13797
+ openToolUses.clear();
13798
+ exactJournalActive = true;
13799
+ }
13800
+ messages.push(message);
13801
+ trackMessageToolState(message, openToolUses);
13802
+ } else {
13803
+ this.events?.emit("session.damaged", {
13804
+ sessionId: id,
13805
+ detail: "Ignored malformed message_appended event"
13806
+ });
13807
+ }
13808
+ } else if (ev.type === "message_updated" && ev.version === 1) {
13809
+ const message = replayableMessage(ev.message, ev.ts);
13810
+ if (message && exactJournalActive && ev.index >= 0 && ev.index < messages.length) {
13811
+ messages[ev.index] = message;
13812
+ openToolUses.clear();
13813
+ for (const current of messages) trackMessageToolState(current, openToolUses);
13814
+ } else {
13815
+ this.events?.emit("session.damaged", {
13816
+ sessionId: id,
13817
+ detail: `Ignored malformed message_updated event at index ${ev.index}`
13818
+ });
13819
+ }
13820
+ } else if (ev.type === "messages_replaced" && ev.version === 1) {
13821
+ if (applyContextSnapshot(messages, openToolUses, ev.messages)) {
13822
+ exactJournalActive = true;
13823
+ } else {
13824
+ this.events?.emit("session.damaged", {
13825
+ sessionId: id,
13826
+ detail: "Ignored malformed messages_replaced event"
13827
+ });
13828
+ }
13829
+ } else if (ev.type === "context_snapshot") {
13364
13830
  if (!applyContextSnapshot(messages, openToolUses, ev.messages)) {
13365
13831
  this.events?.emit("session.damaged", {
13366
13832
  sessionId: id,
13367
13833
  detail: "Ignored malformed context_snapshot event"
13368
13834
  });
13369
13835
  }
13370
- } else if (ev.type === "user_input") {
13836
+ } else if (!exactJournalActive && ev.type === "user_input") {
13371
13837
  openToolUses.clear();
13372
13838
  messages.push({ role: "user", content: ev.content, ts: ev.ts });
13373
13839
  } else if (ev.type === "llm_response") {
13374
- messages.push({ role: "assistant", content: ev.content, ts: ev.ts });
13375
- for (const b of ev.content) {
13376
- if (b.type === "tool_use") openToolUses.add(b.id);
13840
+ if (!exactJournalActive) {
13841
+ messages.push({ role: "assistant", content: ev.content, ts: ev.ts });
13842
+ for (const b of ev.content) {
13843
+ if (b.type === "tool_use") openToolUses.add(b.id);
13844
+ }
13377
13845
  }
13378
13846
  usage = {
13379
13847
  input: usage.input + (ev.usage.input ?? 0),
@@ -13381,7 +13849,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
13381
13849
  cacheRead: (usage.cacheRead ?? 0) + (ev.usage.cacheRead ?? 0),
13382
13850
  cacheWrite: (usage.cacheWrite ?? 0) + (ev.usage.cacheWrite ?? 0)
13383
13851
  };
13384
- } else if (ev.type === "tool_result") {
13852
+ } else if (!exactJournalActive && ev.type === "tool_result") {
13385
13853
  if (!openToolUses.has(ev.id)) {
13386
13854
  this.events?.emit("session.damaged", {
13387
13855
  sessionId: id,
@@ -13453,7 +13921,15 @@ var DefaultSessionStore = class _DefaultSessionStore {
13453
13921
  } : void 0
13454
13922
  };
13455
13923
  const toolCallEnds = extractToolCallEnds(events);
13456
- const data = { metadata: meta, events, messages: finalMessages, usage, toolCallEnds };
13924
+ const pendingToolUseCount = openToolUses && openToolUses.size > 0 ? openToolUses.size : void 0;
13925
+ const data = {
13926
+ metadata: meta,
13927
+ events,
13928
+ messages: finalMessages,
13929
+ usage,
13930
+ toolCallEnds,
13931
+ ...pendingToolUseCount !== void 0 ? { pendingToolUseCount } : {}
13932
+ };
13457
13933
  if (mode.full) {
13458
13934
  if (this._loadCache.size >= _DefaultSessionStore.LOAD_CACHE_MAX_ENTRIES) {
13459
13935
  const oldest = this._loadCache.keys().next().value;
@@ -13461,7 +13937,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
13461
13937
  this._loadCache.delete(oldest);
13462
13938
  }
13463
13939
  }
13464
- this._loadCache.set(id, { mtimeMs: stat12.mtimeMs, size: stat12.size, data });
13940
+ this._loadCache.set(id, { mtimeMs: stat13.mtimeMs, size: stat13.size, data });
13465
13941
  }
13466
13942
  return data;
13467
13943
  } catch (err) {
@@ -13469,7 +13945,14 @@ var DefaultSessionStore = class _DefaultSessionStore {
13469
13945
  errorMsg = toErrorMessage(err);
13470
13946
  throw err;
13471
13947
  } finally {
13472
- this.emitRead(id, file, mode.full ? "load" : "load_events_only", outcome, Date.now() - t0, errorMsg);
13948
+ this.emitRead(
13949
+ id,
13950
+ file,
13951
+ mode.full ? "load" : "load_events_only",
13952
+ outcome,
13953
+ Date.now() - t0,
13954
+ errorMsg
13955
+ );
13473
13956
  if (cacheHit) {
13474
13957
  this.events?.emit("storage.cache_hit", {
13475
13958
  sessionId: id,
@@ -13503,14 +13986,14 @@ var DefaultSessionStore = class _DefaultSessionStore {
13503
13986
  const limit = opts?.limit;
13504
13987
  const signal = opts?.signal;
13505
13988
  const out = [];
13506
- let stat12;
13989
+ let stat13;
13507
13990
  try {
13508
- stat12 = await fsp9.stat(file);
13991
+ stat13 = await fsp9.stat(file);
13509
13992
  } catch (err) {
13510
13993
  if (err.code === "ENOENT") return [];
13511
13994
  throw err;
13512
13995
  }
13513
- if (stat12.size === 0) return [];
13996
+ if (stat13.size === 0) return [];
13514
13997
  let fh;
13515
13998
  try {
13516
13999
  fh = await fsp9.open(file, "r");
@@ -13671,7 +14154,15 @@ var DefaultSessionStore = class _DefaultSessionStore {
13671
14154
  outcome = "failure";
13672
14155
  errorMsg = toErrorMessage(err);
13673
14156
  } finally {
13674
- this.emitWrite("~compact~", this.indexFile, "compact", outcome, Date.now() - t0, void 0, errorMsg);
14157
+ this.emitWrite(
14158
+ "~compact~",
14159
+ this.indexFile,
14160
+ "compact",
14161
+ outcome,
14162
+ Date.now() - t0,
14163
+ void 0,
14164
+ errorMsg
14165
+ );
13675
14166
  }
13676
14167
  }
13677
14168
  /**
@@ -13693,15 +14184,15 @@ var DefaultSessionStore = class _DefaultSessionStore {
13693
14184
  * Returns empty array when the index doesn't exist or is corrupt.
13694
14185
  */
13695
14186
  async readIndex() {
13696
- let stat12;
14187
+ let stat13;
13697
14188
  try {
13698
14189
  const s = await fsp9.stat(this.indexFile);
13699
- stat12 = { mtimeMs: s.mtimeMs, size: s.size };
14190
+ stat13 = { mtimeMs: s.mtimeMs, size: s.size };
13700
14191
  } catch {
13701
14192
  this._indexCache = null;
13702
14193
  return [];
13703
14194
  }
13704
- if (this._indexCache !== null && this._indexCache.mtimeMs === stat12.mtimeMs && this._indexCache.size === stat12.size) {
14195
+ if (this._indexCache !== null && this._indexCache.mtimeMs === stat13.mtimeMs && this._indexCache.size === stat13.size) {
13705
14196
  return [...this._indexCache.summaries];
13706
14197
  }
13707
14198
  let raw;
@@ -13734,7 +14225,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
13734
14225
  if (a.startedAt > b.startedAt) return -1;
13735
14226
  return a.id.localeCompare(b.id);
13736
14227
  });
13737
- this._indexCache = { ...stat12, summaries };
14228
+ this._indexCache = { ...stat13, summaries };
13738
14229
  return [...summaries];
13739
14230
  }
13740
14231
  /**
@@ -13743,7 +14234,9 @@ var DefaultSessionStore = class _DefaultSessionStore {
13743
14234
  */
13744
14235
  async rebuildIndex() {
13745
14236
  const ids = await this.collectSessionIds(this.dir);
13746
- const summaries = await Promise.all(ids.map((id) => this.summaryFor(id).catch(() => null)));
14237
+ const summaries = await Promise.all(
14238
+ ids.map((id) => this.summaryFor(id).catch(() => null))
14239
+ );
13747
14240
  const valid = summaries.filter((s) => s !== null);
13748
14241
  const lines = valid.map((s) => JSON.stringify(s)).join("\n") + "\n";
13749
14242
  await withFileLock(this.indexFile, async () => {
@@ -13784,7 +14277,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
13784
14277
  const shardKeys = [""];
13785
14278
  for (const entry of entries) {
13786
14279
  if (entry.name.startsWith(".") && entry.name !== ".wrongstack") continue;
13787
- if (entry.name === "shared" || entry.name === "subagents" || entry.name === "attachments") continue;
14280
+ if (entry.name === "shared" || entry.name === "subagents" || entry.name === "attachments")
14281
+ continue;
13788
14282
  if (entry.isDirectory()) shardKeys.push(entry.name);
13789
14283
  }
13790
14284
  return shardKeys;
@@ -13901,15 +14395,16 @@ var DefaultSessionStore = class _DefaultSessionStore {
13901
14395
  if (fromManifest) return fromManifest;
13902
14396
  try {
13903
14397
  const full = this.sessionPath(id, ".jsonl");
13904
- const stat12 = await fsp9.stat(full);
13905
- const summary = await this.summarize(id, stat12.mtime.toISOString());
14398
+ const stat13 = await fsp9.stat(full);
14399
+ const summary = await this.summarize(id, stat13.mtime.toISOString());
13906
14400
  await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
13907
14401
  const msg = toErrorMessage(err);
13908
14402
  this.emitError(id, manifest, "summary_fallback", msg, true);
13909
- this.logWarn(
13910
- "Session manifest write failed",
13911
- { event: "session_store.manifest_write_failed", sessionId: id, message: msg }
13912
- );
14403
+ this.logWarn("Session manifest write failed", {
14404
+ event: "session_store.manifest_write_failed",
14405
+ sessionId: id,
14406
+ message: msg
14407
+ });
13913
14408
  });
13914
14409
  outcome = "failure";
13915
14410
  errorMsg = "summary fallback \xE2\u20AC\u201D manifest rebuilt";
@@ -13942,18 +14437,18 @@ var DefaultSessionStore = class _DefaultSessionStore {
13942
14437
  async summaryHeaderFor(ref) {
13943
14438
  let mtime = (/* @__PURE__ */ new Date(0)).toISOString();
13944
14439
  try {
13945
- const stat12 = await fsp9.stat(ref.filePath);
13946
- if (!stat12.isFile()) {
14440
+ const stat13 = await fsp9.stat(ref.filePath);
14441
+ if (!stat13.isFile()) {
13947
14442
  return {
13948
14443
  id: ref.id,
13949
14444
  title: "(damaged)",
13950
- startedAt: stat12.mtime.toISOString(),
14445
+ startedAt: stat13.mtime.toISOString(),
13951
14446
  model: "unknown",
13952
14447
  provider: "unknown",
13953
14448
  tokenTotal: 0
13954
14449
  };
13955
14450
  }
13956
- mtime = stat12.mtime.toISOString();
14451
+ mtime = stat13.mtime.toISOString();
13957
14452
  } catch {
13958
14453
  return null;
13959
14454
  }
@@ -14016,24 +14511,28 @@ var DefaultSessionStore = class _DefaultSessionStore {
14016
14511
  if (r.status === "rejected") {
14017
14512
  const msg = r.reason instanceof Error ? r.reason.message : String(r.reason);
14018
14513
  if (r.reason?.code !== "ENOENT") {
14019
- console.warn(JSON.stringify({
14020
- level: "warn",
14021
- event: "session_store.delete_failed",
14022
- sessionId: id,
14023
- message: msg,
14024
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
14025
- }));
14514
+ console.warn(
14515
+ JSON.stringify({
14516
+ level: "warn",
14517
+ event: "session_store.delete_failed",
14518
+ sessionId: id,
14519
+ message: msg,
14520
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
14521
+ })
14522
+ );
14026
14523
  }
14027
14524
  }
14028
14525
  }
14029
14526
  await fsp9.rm(sessDir, { recursive: true, force: true }).catch((err) => {
14030
- console.warn(JSON.stringify({
14031
- level: "warn",
14032
- event: "session_store.rmdir_failed",
14033
- sessionId: id,
14034
- message: toErrorMessage(err),
14035
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
14036
- }));
14527
+ console.warn(
14528
+ JSON.stringify({
14529
+ level: "warn",
14530
+ event: "session_store.rmdir_failed",
14531
+ sessionId: id,
14532
+ message: toErrorMessage(err),
14533
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
14534
+ })
14535
+ );
14037
14536
  });
14038
14537
  await this.writeTombstone(id);
14039
14538
  }
@@ -14109,8 +14608,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
14109
14608
  const pruneFile = async (dir, name, prefix) => {
14110
14609
  const jsonlPath = path11.join(dir, name);
14111
14610
  try {
14112
- const stat12 = await fsp9.stat(jsonlPath);
14113
- if (stat12.mtimeMs >= cutoff) return;
14611
+ const stat13 = await fsp9.stat(jsonlPath);
14612
+ if (stat13.mtimeMs >= cutoff) return;
14114
14613
  } catch {
14115
14614
  return;
14116
14615
  }
@@ -14998,11 +15497,6 @@ ${journalTail.join("\n")}` : "Recent journal: (none \u2014 this is the first ite
14998
15497
  };
14999
15498
  }
15000
15499
 
15001
- // src/types/blocks.ts
15002
- function isTextBlock(b) {
15003
- return b.type === "text";
15004
- }
15005
-
15006
15500
  // src/execution/compaction-core.ts
15007
15501
  var FAILURE_PATTERN = /(error|fail|exception|timeout|enonet|eacces|eperm|enoent|abort)/i;
15008
15502
  var CORRECTION_PATTERN = /\b(wrong|no\b|stop\b|don'?t\b|actually|fix that|undo|revert|forget|ignore|skip)\b/i;
@@ -15110,6 +15604,14 @@ function hasMatchingToolPair(resultContent, useContent) {
15110
15604
  }
15111
15605
  function eliseOldToolResults(messages, opts) {
15112
15606
  const preserveStart = findPreserveStart(messages, opts.preserveK);
15607
+ const tokenCache = /* @__PURE__ */ new Map();
15608
+ const tokensFor = (b) => {
15609
+ const cached = tokenCache.get(b);
15610
+ if (cached !== void 0) return cached;
15611
+ const t = b.type === "tool_result" ? estimateToolResultTokens(b.content) : b.type === "tool_use" ? estimateToolInputTokens(b.input) : 0;
15612
+ tokenCache.set(b, t);
15613
+ return t;
15614
+ };
15113
15615
  let hasOversized = false;
15114
15616
  let firstOversizedIndex = -1;
15115
15617
  let fastPathIterations = 0;
@@ -15120,7 +15622,7 @@ function eliseOldToolResults(messages, opts) {
15120
15622
  if (!msg || !Array.isArray(msg.content)) continue;
15121
15623
  for (const b of msg.content) {
15122
15624
  fastPathInnerIterations++;
15123
- const oversized = b.type === "tool_result" && estimateToolResultTokens(b.content) >= opts.eliseThreshold || b.type === "tool_use" && estimateToolInputTokens(b.input) >= opts.eliseThreshold;
15625
+ const oversized = tokensFor(b) >= opts.eliseThreshold;
15124
15626
  if (oversized) {
15125
15627
  hasOversized = true;
15126
15628
  firstOversizedIndex = i;
@@ -15158,7 +15660,7 @@ function eliseOldToolResults(messages, opts) {
15158
15660
  const b = original[idx];
15159
15661
  if (!b) continue;
15160
15662
  if (b.type === "tool_use") {
15161
- const tokens2 = estimateToolInputTokens(b.input);
15663
+ const tokens2 = tokensFor(b);
15162
15664
  if (tokens2 < opts.eliseThreshold) continue;
15163
15665
  const elidedInput = summarizeToolUseInputElision(b, tokens2);
15164
15666
  saved += Math.max(0, tokens2 - estimateToolInputTokens(elidedInput));
@@ -15167,7 +15669,7 @@ function eliseOldToolResults(messages, opts) {
15167
15669
  continue;
15168
15670
  }
15169
15671
  if (b.type !== "tool_result") continue;
15170
- const tokens = estimateToolResultTokens(b.content);
15672
+ const tokens = tokensFor(b);
15171
15673
  if (tokens < opts.eliseThreshold) continue;
15172
15674
  saved += tokens;
15173
15675
  const elided = {
@@ -16095,12 +16597,12 @@ function verifyFiles(tokens, files) {
16095
16597
  const violations = [];
16096
16598
  let onPalette = 0;
16097
16599
  let offPalette = 0;
16098
- for (const { path: path28, text } of files) {
16600
+ for (const { path: path29, text } of files) {
16099
16601
  const lines = text.split("\n");
16100
16602
  lines.forEach((lineText, i) => {
16101
16603
  const lineNo = i + 1;
16102
16604
  const flag = (snippet, reason) => {
16103
- violations.push({ file: path28, line: lineNo, snippet: snippet.slice(0, 80), reason });
16605
+ violations.push({ file: path29, line: lineNo, snippet: snippet.slice(0, 80), reason });
16104
16606
  };
16105
16607
  for (const re of [HEX_RE, FUNC_COLOR_RE]) {
16106
16608
  re.lastIndex = 0;
@@ -16609,16 +17111,20 @@ function buildRecoveryStrategies(opts) {
16609
17111
  const delayMs = err.body?.retryAfterMs ?? 5e3;
16610
17112
  const delay = Math.min(6e4, Math.max(1e3, delayMs));
16611
17113
  if (ctx?.signal) {
16612
- await new Promise((resolve12) => {
17114
+ await new Promise((resolve14) => {
16613
17115
  if (ctx.signal.aborted) {
16614
- resolve12();
17116
+ resolve14();
16615
17117
  return;
16616
17118
  }
16617
- const timer = setTimeout(resolve12, delay);
16618
- ctx.signal.addEventListener("abort", () => {
16619
- clearTimeout(timer);
16620
- resolve12();
16621
- }, { once: true });
17119
+ const timer = setTimeout(resolve14, delay);
17120
+ ctx.signal.addEventListener(
17121
+ "abort",
17122
+ () => {
17123
+ clearTimeout(timer);
17124
+ resolve14();
17125
+ },
17126
+ { once: true }
17127
+ );
16622
17128
  });
16623
17129
  } else {
16624
17130
  await new Promise((r) => setTimeout(r, delay));
@@ -16709,6 +17215,7 @@ function buildRecoveryStrategies(opts) {
16709
17215
  var DEFAULT_RECOVERY_STRATEGIES = buildRecoveryStrategies();
16710
17216
  var HANDLER_KIND_BY_PROVIDER_KIND = {
16711
17217
  rate_limit: "rate_limit",
17218
+ quota_exhausted: "rate_limit",
16712
17219
  overloaded: "overloaded",
16713
17220
  server: "server",
16714
17221
  stream_hang: "server",
@@ -16756,6 +17263,217 @@ var DefaultErrorHandler = class {
16756
17263
  import { execFile } from "node:child_process";
16757
17264
  import { promisify } from "node:util";
16758
17265
 
17266
+ // src/storage/goal-coordination.ts
17267
+ import { updateTask } from "@wrongstack/kanban";
17268
+
17269
+ // src/storage/goal-kanban.ts
17270
+ import {
17271
+ addColumn,
17272
+ addTask,
17273
+ createBoard,
17274
+ getBoard,
17275
+ listBoards,
17276
+ removeBoard
17277
+ } from "@wrongstack/kanban";
17278
+ var GOAL_BOARD_TAG_PREFIX = "goal:";
17279
+ async function findGoalKanbanBoard(projectRoot, boardId) {
17280
+ if (!projectRoot || !boardId) return null;
17281
+ try {
17282
+ return await getBoard(projectRoot, boardId);
17283
+ } catch {
17284
+ return null;
17285
+ }
17286
+ }
17287
+ async function findGoalBoardByTag(projectRoot, goalFile) {
17288
+ if (!projectRoot) return null;
17289
+ const tag = goalTag(goalFile);
17290
+ const boards = await listBoards(projectRoot);
17291
+ const matched = boards.find((b) => b.tags?.includes(tag));
17292
+ if (!matched) return null;
17293
+ return getBoard(projectRoot, matched.id).catch(() => null);
17294
+ }
17295
+ function goalTag(goalFile) {
17296
+ const goal = (goalFile.refinedGoal || goalFile.goal).replace(/\s+/g, "-").slice(0, 24).toLowerCase();
17297
+ return `${GOAL_BOARD_TAG_PREFIX}${goal}`;
17298
+ }
17299
+
17300
+ // src/storage/goal-coordination.ts
17301
+ var DONE_MARKER = /\[done:\s*([^\]]+)\]/gi;
17302
+ var DONE_PREFIX = /^\s*(?:✅|\[[x✓]\]|\(done\))\s*/i;
17303
+ function parseCompletedGoalDeliverables(finalText, deliverables) {
17304
+ if (!finalText || deliverables.length === 0) return [];
17305
+ const seen = /* @__PURE__ */ new Set();
17306
+ const completed = [];
17307
+ for (const match of finalText.matchAll(DONE_MARKER)) {
17308
+ const raw = match[1]?.trim();
17309
+ if (!raw) continue;
17310
+ const numeric = /^\d+$/.test(raw) ? Number.parseInt(raw, 10) : void 0;
17311
+ let index = numeric === void 0 ? -1 : numeric === 0 ? 0 : numeric - 1;
17312
+ if (index < 0 || index >= deliverables.length) {
17313
+ const prefix = raw.toLowerCase();
17314
+ index = deliverables.findIndex(
17315
+ (item) => stripGoalDeliverableMarker(item).toLowerCase().startsWith(prefix)
17316
+ );
17317
+ }
17318
+ if (index < 0 || index >= deliverables.length || seen.has(index)) continue;
17319
+ const source = deliverables[index] ?? "";
17320
+ if (isGoalDeliverableComplete(source)) continue;
17321
+ seen.add(index);
17322
+ completed.push({ index, text: stripGoalDeliverableMarker(source) });
17323
+ }
17324
+ return completed;
17325
+ }
17326
+ function isGoalDeliverableComplete(value) {
17327
+ return DONE_PREFIX.test(value);
17328
+ }
17329
+ function stripGoalDeliverableMarker(value) {
17330
+ return value.replace(DONE_PREFIX, "").trim();
17331
+ }
17332
+ function applyGoalDeliverableCompletions(goal, completed) {
17333
+ if (completed.length === 0) return recomputeGoalProgress(goal);
17334
+ const completedIndices = new Set(completed.map((item) => item.index));
17335
+ let next = {
17336
+ ...goal,
17337
+ deliverables: (goal.deliverables ?? []).map(
17338
+ (item, index) => completedIndices.has(index) && !isGoalDeliverableComplete(item) ? `\u2705 ${item}` : item
17339
+ )
17340
+ };
17341
+ for (const item of completed) {
17342
+ next = appendJournal(next, {
17343
+ source: "deliverable",
17344
+ task: `Deliverable completed: ${item.text}`.slice(0, 240),
17345
+ status: "success",
17346
+ note: `[DONE: ${item.index + 1}]`
17347
+ });
17348
+ }
17349
+ return recomputeGoalProgress(next);
17350
+ }
17351
+ function recomputeGoalProgress(goal) {
17352
+ const deliverables = goal.deliverables ?? [];
17353
+ if (deliverables.length === 0) return goal;
17354
+ const done = deliverables.filter(isGoalDeliverableComplete).length;
17355
+ const progress = Math.round(done / deliverables.length * 100);
17356
+ return recordProgress(goal, progress, `${done}/${deliverables.length} deliverables complete`);
17357
+ }
17358
+ async function coordinateGoalIteration(options) {
17359
+ const current = await loadGoal(options.goalPath, options.events);
17360
+ if (!current) return null;
17361
+ const completed = parseCompletedGoalDeliverables(
17362
+ options.finalText,
17363
+ current.deliverables ?? []
17364
+ );
17365
+ let goal = applyGoalDeliverableCompletions(current, completed);
17366
+ await saveGoal(options.goalPath, goal, options.events);
17367
+ const kanbanUpdatedTaskIds = await refreshGoalKanban(
17368
+ options.projectRoot,
17369
+ goal,
17370
+ completed
17371
+ );
17372
+ goal = await recomputeGoalProgressFromKanban(options.projectRoot, goal);
17373
+ let reached = false;
17374
+ const allComplete = (goal.deliverables?.length ?? 0) > 0 && goal.deliverables?.every(isGoalDeliverableComplete) === true;
17375
+ if (allComplete && options.brain) {
17376
+ const decision = await options.brain.decide({
17377
+ id: `goal-reached-${goal.iterations}`,
17378
+ sessionId: options.sessionId,
17379
+ source: "system",
17380
+ question: "Have all goal deliverables been reached and verified?",
17381
+ context: [
17382
+ `Goal: ${goal.refinedGoal ?? goal.goal}`,
17383
+ `Progress: ${goal.progress ?? 0}%`,
17384
+ "Completed deliverables:",
17385
+ ...(goal.deliverables ?? []).map(
17386
+ (item, index) => `${index + 1}. ${stripGoalDeliverableMarker(item)}`
17387
+ )
17388
+ ].join("\n"),
17389
+ risk: "high",
17390
+ fallback: "continue",
17391
+ options: [
17392
+ {
17393
+ id: "goal_reached",
17394
+ label: "Goal reached",
17395
+ consequence: "Persist the completed goal and stop the eternal loop."
17396
+ },
17397
+ {
17398
+ id: "keep_working",
17399
+ label: "Keep working",
17400
+ consequence: "Leave the goal active for another iteration."
17401
+ }
17402
+ ]
17403
+ });
17404
+ reached = decision.type === "answer" && decision.optionId === "goal_reached";
17405
+ }
17406
+ if (reached) {
17407
+ const reachedAt = (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
17408
+ goal = appendJournal(
17409
+ {
17410
+ ...goal,
17411
+ goalState: "completed",
17412
+ engineState: "stopped",
17413
+ progress: 100,
17414
+ progressNote: "goal reached",
17415
+ reachedAt,
17416
+ reachedNote: "goal reached"
17417
+ },
17418
+ {
17419
+ source: "deliverable",
17420
+ task: "GOAL REACHED",
17421
+ status: "success",
17422
+ note: "goal reached"
17423
+ }
17424
+ );
17425
+ }
17426
+ await saveGoal(options.goalPath, goal, options.events);
17427
+ return { goal, completed, kanbanUpdatedTaskIds, reached };
17428
+ }
17429
+ async function resolveGoalBoard(projectRoot, goal) {
17430
+ const boardId = goal.kanbanBoardId;
17431
+ if (boardId) {
17432
+ const board = await findGoalKanbanBoard(projectRoot, boardId);
17433
+ if (board) return board;
17434
+ }
17435
+ return findGoalBoardByTag(projectRoot, goal);
17436
+ }
17437
+ async function refreshGoalKanban(projectRoot, goal, completed) {
17438
+ if (completed.length === 0) return [];
17439
+ const board = await resolveGoalBoard(projectRoot, goal);
17440
+ if (!board) return [];
17441
+ const doneColumn = board.columns.find((column) => column.title.toLowerCase() === "done");
17442
+ if (!doneColumn) return [];
17443
+ const completedTitles = new Set(completed.map((item) => normalizeTitle(item.text)));
17444
+ const updated = [];
17445
+ for (const task of board.tasks) {
17446
+ if (!completedTitles.has(normalizeTitle(task.title))) continue;
17447
+ const result = await updateTask(
17448
+ projectRoot,
17449
+ board.id,
17450
+ task.id,
17451
+ { columnId: doneColumn.id, status: "completed" },
17452
+ {
17453
+ actor: "goal-coordinator",
17454
+ note: "deliverable-completed: Updated from [DONE:] marker."
17455
+ }
17456
+ );
17457
+ if (result) updated.push(task.id);
17458
+ }
17459
+ return updated;
17460
+ }
17461
+ async function recomputeGoalProgressFromKanban(projectRoot, goal) {
17462
+ const board = await resolveGoalBoard(projectRoot, goal);
17463
+ if (!board || !goal.deliverables?.length) return recomputeGoalProgress(goal);
17464
+ const completedTitles = new Set(
17465
+ board.tasks.filter((task) => task.status === "completed").map((task) => normalizeTitle(task.title))
17466
+ );
17467
+ const deliverables = goal.deliverables.map((item) => {
17468
+ if (isGoalDeliverableComplete(item)) return item;
17469
+ return completedTitles.has(normalizeTitle(item)) ? `\u2705 ${item}` : item;
17470
+ });
17471
+ return recomputeGoalProgress({ ...goal, deliverables });
17472
+ }
17473
+ function normalizeTitle(value) {
17474
+ return stripGoalDeliverableMarker(value).replace(/\s+/g, " ").trim().toLowerCase();
17475
+ }
17476
+
16759
17477
  // src/execution/autonomy-brain.ts
16760
17478
  function formatDecisionSummary(decision, request) {
16761
17479
  const question = request.question.length > 80 ? request.question.slice(0, 77) + "\u2026" : request.question;
@@ -16773,6 +17491,7 @@ function formatDecisionSummary(decision, request) {
16773
17491
  var execFileP = promisify(execFile);
16774
17492
  var BRAINSTORM_DONE = /* @__PURE__ */ Symbol("brainstorm-done");
16775
17493
  var GOAL_COMPLETE_MARKER = /^\s*\[goal[_\s-]*complete\]\s*$/im;
17494
+ var DONE_DELIVERABLE_MARKER = /\[done:\s*[^\]]+\]/i;
16776
17495
  var GOAL_CLEAR_MARKER = /^\s*\[\/?goal\s*clear\]\s*$/im;
16777
17496
  var EternalAutonomyEngine = class {
16778
17497
  constructor(opts) {
@@ -17027,6 +17746,24 @@ var EternalAutonomyEngine = class {
17027
17746
  const cycleGapMs = this.opts.cycleGapMs ?? 1e3;
17028
17747
  emit({ phase: "sleep", ms: cycleGapMs });
17029
17748
  await sleep(cycleGapMs);
17749
+ let coordinatedProgress;
17750
+ if (DONE_DELIVERABLE_MARKER.test(finalText)) {
17751
+ const coordinated = await coordinateGoalIteration({
17752
+ projectRoot: this.opts.projectRoot,
17753
+ goalPath: this.goalPath,
17754
+ finalText,
17755
+ brain: this.opts.brain,
17756
+ sessionId: this.opts.agent.ctx.session?.id,
17757
+ events: this.opts.events,
17758
+ now: this.opts.now
17759
+ });
17760
+ coordinatedProgress = coordinated?.goal.progress;
17761
+ if (coordinated?.reached) {
17762
+ this.stopRequested = true;
17763
+ this.opts.onEternalStop?.();
17764
+ return true;
17765
+ }
17766
+ }
17030
17767
  if (GOAL_COMPLETE_MARKER.test(finalText)) {
17031
17768
  await this.clearGoalManually(finalText);
17032
17769
  this.stopRequested = true;
@@ -17039,8 +17776,9 @@ var EternalAutonomyEngine = class {
17039
17776
  }
17040
17777
  const parsed = parseProgressFromText(finalText);
17041
17778
  if (parsed) {
17042
- await this.updateProgress(parsed.progress, parsed.note);
17043
- if (parsed.progress >= 100) {
17779
+ const progress = Math.max(parsed.progress, coordinatedProgress ?? 0);
17780
+ await this.updateProgress(progress, parsed.note);
17781
+ if (parsed.progress >= 100 && coordinatedProgress === void 0) {
17044
17782
  await this.markGoalCompleted(
17045
17783
  { source: action.source, task: action.task, directive: action.directive },
17046
17784
  `progress reached 100%: ${parsed.note ?? "goal complete"}`
@@ -17605,7 +18343,7 @@ ${summaryText}` : summaryText;
17605
18343
  timeoutMs: 3e4,
17606
18344
  maxTokens: 1024,
17607
18345
  signal: ctx.signal,
17608
- fallbackProfile: "summary"
18346
+ fallbackModels: ["/summary"]
17609
18347
  });
17610
18348
  if (result.text && !result.error) return result.text;
17611
18349
  } else {
@@ -18372,8 +19110,8 @@ async function streamProviderToResponse(provider, req, signal, ctx, events, logg
18372
19110
  });
18373
19111
  await Promise.race([
18374
19112
  drainPromise,
18375
- new Promise((resolve12) => {
18376
- drainTimer = setTimeout(resolve12, STREAM_DRAIN_TIMEOUT_MS);
19113
+ new Promise((resolve14) => {
19114
+ drainTimer = setTimeout(resolve14, STREAM_DRAIN_TIMEOUT_MS);
18377
19115
  })
18378
19116
  ]);
18379
19117
  } finally {
@@ -18396,7 +19134,145 @@ async function streamProviderToResponse(provider, req, signal, ctx, events, logg
18396
19134
  return buildResponse(state);
18397
19135
  }
18398
19136
 
19137
+ // src/chronicle/prompt-manifest.ts
19138
+ import { createHash as createHash5 } from "node:crypto";
19139
+ function createChroniclePromptManifest(request) {
19140
+ const roleCounts = {}, blockCounts = {};
19141
+ const messages = request.messages.map((message, index) => messageSummary(message, index, roleCounts, blockCounts));
19142
+ const systemText = (request.system ?? []).map((block) => block.text).join("\n");
19143
+ const toolRecords = (request.tools ?? []).map((tool) => ({
19144
+ name: tool.name,
19145
+ schemaHash: hash(stable(tool.inputSchema)),
19146
+ permission: tool.permission,
19147
+ mutating: tool.mutating,
19148
+ riskTier: tool.riskTier,
19149
+ capabilities: tool.capabilities,
19150
+ estimatedTokens: tool._estDefTokens ?? 0
19151
+ }));
19152
+ const core = {
19153
+ systemHash: hash(systemText),
19154
+ messages: messages.map(({ hash: contentHash, ...rest }) => ({ ...rest, contentHash })),
19155
+ tools: toolRecords,
19156
+ model: request.model,
19157
+ maxTokens: request.maxTokens,
19158
+ temperature: request.temperature,
19159
+ topP: request.topP,
19160
+ topK: request.topK,
19161
+ seed: request.seed,
19162
+ toolChoice: request.toolChoice,
19163
+ reasoning: request.reasoning,
19164
+ cache: request.cache,
19165
+ responseFormat: request.responseFormat?.type
19166
+ };
19167
+ return {
19168
+ manifestId: `prompt_${hash(stable(core))}`,
19169
+ messageCount: messages.length,
19170
+ estimatedMessageTokens: messages.reduce((sum, message) => sum + (message.estimatedTokens ?? 0), 0),
19171
+ contentBytes: messages.reduce((sum, message) => sum + message.bytes, 0) + Buffer.byteLength(systemText),
19172
+ roleCounts,
19173
+ blockCounts,
19174
+ system: { blockCount: request.system?.length ?? 0, bytes: Buffer.byteLength(systemText), hash: hash(systemText) },
19175
+ messages,
19176
+ tools: {
19177
+ count: toolRecords.length,
19178
+ estimatedDefinitionTokens: toolRecords.reduce((sum, tool) => sum + tool.estimatedTokens, 0),
19179
+ manifestHash: hash(stable(toolRecords)),
19180
+ names: toolRecords.map((tool) => tool.name),
19181
+ mutating: toolRecords.filter((tool) => tool.mutating).length,
19182
+ destructive: toolRecords.filter((tool) => tool.riskTier === "destructive").length
19183
+ },
19184
+ request: {
19185
+ maxTokens: request.maxTokens,
19186
+ temperature: request.temperature,
19187
+ topP: request.topP,
19188
+ topK: request.topK,
19189
+ frequencyPenalty: request.frequencyPenalty,
19190
+ presencePenalty: request.presencePenalty,
19191
+ seed: request.seed,
19192
+ candidateCount: request.candidateCount,
19193
+ logprobs: request.logprobs,
19194
+ topLogprobs: request.topLogprobs,
19195
+ stopSequenceCount: request.stopSequences?.length ?? 0,
19196
+ toolChoice: request.toolChoice,
19197
+ reasoning: request.reasoning,
19198
+ cache: request.cache,
19199
+ responseFormat: request.responseFormat?.type,
19200
+ safetySettingCount: request.safetySettings?.length ?? 0,
19201
+ userHash: request.user ? hash(request.user) : void 0
19202
+ }
19203
+ };
19204
+ }
19205
+ function messageSummary(message, index, roles, totals) {
19206
+ roles[message.role] = (roles[message.role] ?? 0) + 1;
19207
+ const blocks = typeof message.content === "string" ? { text: 1 } : countBlocks(message.content);
19208
+ for (const [type, count] of Object.entries(blocks)) totals[type] = (totals[type] ?? 0) + count;
19209
+ const content = contentIdentity(message.content);
19210
+ return {
19211
+ index,
19212
+ role: message.role,
19213
+ bytes: content.bytes,
19214
+ ...message._estTokens !== void 0 ? { estimatedTokens: message._estTokens } : {},
19215
+ hash: content.hash,
19216
+ blocks
19217
+ };
19218
+ }
19219
+ function countBlocks(blocks) {
19220
+ const counts = {};
19221
+ for (const block of blocks) counts[block.type] = (counts[block.type] ?? 0) + 1;
19222
+ return counts;
19223
+ }
19224
+ function hash(value) {
19225
+ return createHash5("sha256").update(value).digest("hex");
19226
+ }
19227
+ function contentIdentity(content) {
19228
+ if (typeof content === "string") return { hash: hash(content), bytes: Buffer.byteLength(content) };
19229
+ const digest = createHash5("sha256");
19230
+ let bytes = 0;
19231
+ const add = (value) => {
19232
+ if (!value) return;
19233
+ digest.update(value);
19234
+ bytes += Buffer.byteLength(value);
19235
+ };
19236
+ for (const block of content) {
19237
+ add(block.type);
19238
+ if (block.type === "text") add(block.text);
19239
+ else if (block.type === "thinking") {
19240
+ add(block.thinking);
19241
+ add(block.signature);
19242
+ } else if (block.type === "tool_use") {
19243
+ add(block.id);
19244
+ add(block.name);
19245
+ add(stable(block.input));
19246
+ } else if (block.type === "tool_result") {
19247
+ add(block.tool_use_id);
19248
+ add(block.name);
19249
+ add(block.content);
19250
+ add(String(block.is_error ?? false));
19251
+ } else if (block.type === "image") {
19252
+ add(block.source.type);
19253
+ add(block.source.media_type);
19254
+ add(block.source.url);
19255
+ add(block.source.data);
19256
+ }
19257
+ }
19258
+ return { hash: digest.digest("hex"), bytes };
19259
+ }
19260
+ function stable(value) {
19261
+ if (value === void 0) return "undefined";
19262
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
19263
+ if (Array.isArray(value)) return `[${value.map(stable).join(",")}]`;
19264
+ return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, child]) => `${JSON.stringify(key)}:${stable(child)}`).join(",")}}`;
19265
+ }
19266
+
19267
+ // src/observability/network-telemetry.ts
19268
+ import { AsyncLocalStorage } from "node:async_hooks";
19269
+ var storage = new AsyncLocalStorage();
19270
+ function runWithNetworkTelemetry(context, run) {
19271
+ return storage.run(context, run);
19272
+ }
19273
+
18399
19274
  // src/core/provider-runner.ts
19275
+ import { randomUUID as randomUUID10 } from "node:crypto";
18400
19276
  function providerLogCtx(p, r) {
18401
19277
  return {
18402
19278
  providerId: p.id,
@@ -18408,8 +19284,31 @@ function providerLogCtx(p, r) {
18408
19284
  }
18409
19285
  async function runProviderWithRetry(opts) {
18410
19286
  const { provider, request, signal, ctx, events, retry, logger, tracer } = opts;
19287
+ const logicalRequestId = randomUUID10();
19288
+ const promptManifest = createChroniclePromptManifest(request);
18411
19289
  let attempt = 0;
18412
19290
  for (; ; ) {
19291
+ const attemptId = randomUUID10();
19292
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
19293
+ const startedNs = process.hrtime.bigint();
19294
+ const correlation = {
19295
+ sessionId: ctx.session.id,
19296
+ ...ctx.traceId ? { traceId: ctx.traceId } : {},
19297
+ ...ctx.agentId ? { agentId: ctx.agentId } : {},
19298
+ logicalRequestId,
19299
+ attemptId,
19300
+ attempt,
19301
+ providerId: provider.id,
19302
+ model: request.model
19303
+ };
19304
+ events.emit("provider.attempt.started", {
19305
+ ...correlation,
19306
+ streaming: provider.capabilities.streaming,
19307
+ messageCount: request.messages.length,
19308
+ toolCount: request.tools?.length ?? 0,
19309
+ promptManifest,
19310
+ startedAt
19311
+ });
18413
19312
  const span = tracer?.startSpan("provider.complete", {
18414
19313
  "provider.id": provider.id,
18415
19314
  "provider.model": request.model,
@@ -18418,11 +19317,27 @@ async function runProviderWithRetry(opts) {
18418
19317
  });
18419
19318
  logger.debug(`Provider attempt ${attempt + 1} starting`, providerLogCtx(provider, request));
18420
19319
  try {
18421
- const res = provider.capabilities.streaming ? await streamProviderToResponse(provider, request, signal, ctx, events, logger) : await provider.complete(request, { signal });
19320
+ const res = await runWithNetworkTelemetry({
19321
+ events,
19322
+ sessionId: ctx.session.id,
19323
+ ...ctx.traceId ? { traceId: ctx.traceId } : {},
19324
+ ...ctx.agentId ? { agentId: ctx.agentId } : {},
19325
+ attemptId,
19326
+ initiator: "provider",
19327
+ operationName: `${provider.id}.complete`
19328
+ }, () => provider.capabilities.streaming ? streamProviderToResponse(provider, request, signal, ctx, events, logger) : provider.complete(request, { signal }));
18422
19329
  span?.setAttribute("provider.stopReason", res.stopReason);
18423
19330
  span?.setAttribute("provider.usage_in", res.usage.input);
18424
19331
  span?.setAttribute("provider.usage_out", res.usage.output);
18425
19332
  span?.end();
19333
+ events.emit("provider.attempt.completed", {
19334
+ ...correlation,
19335
+ startedAt,
19336
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
19337
+ durationMs: elapsedMs(startedNs),
19338
+ stopReason: res.stopReason,
19339
+ usage: res.usage
19340
+ });
18426
19341
  logger.debug("Provider call succeeded", {
18427
19342
  ...providerLogCtx(provider, request),
18428
19343
  stopReason: res.stopReason,
@@ -18441,16 +19356,28 @@ async function runProviderWithRetry(opts) {
18441
19356
  const errAsErr = err instanceof Error ? err : new Error(String(err));
18442
19357
  const canRetry = retry.shouldRetry(isProviderErr ? err : errAsErr, attempt);
18443
19358
  const description = isProviderErr ? err.describe() : errAsErr.message;
19359
+ const delay = canRetry ? Math.round(retry.delayMs(attempt, isProviderErr ? err : errAsErr)) : void 0;
19360
+ events.emit("provider.attempt.failed", {
19361
+ ...correlation,
19362
+ startedAt,
19363
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
19364
+ durationMs: elapsedMs(startedNs),
19365
+ status: isProviderErr ? err.status : 0,
19366
+ failureKind: isProviderErr ? err.kind : "unknown",
19367
+ description,
19368
+ retryable: canRetry,
19369
+ retryScheduled: canRetry,
19370
+ ...delay !== void 0 ? { retryDelayMs: delay } : {},
19371
+ ...isProviderErr && err.body?.requestId ? { providerRequestId: err.body.requestId } : {}
19372
+ });
18444
19373
  if (!canRetry) {
18445
- if (isProviderErr) {
18446
- events.emit("provider.error", {
18447
- sessionId: ctx.session.id,
18448
- providerId: err.providerId,
18449
- status: err.status,
18450
- description,
18451
- retryable: false
18452
- });
18453
- }
19374
+ events.emit("provider.error", {
19375
+ sessionId: ctx.session.id,
19376
+ providerId: isProviderErr ? err.providerId : provider.id,
19377
+ status: isProviderErr ? err.status : 0,
19378
+ description,
19379
+ retryable: false
19380
+ });
18454
19381
  logger.error(`Provider call failed after ${attempt + 1} attempt(s) \u2014 ${description}`, {
18455
19382
  ...providerLogCtx(provider, request),
18456
19383
  attempts: attempt + 1,
@@ -18461,9 +19388,6 @@ async function runProviderWithRetry(opts) {
18461
19388
  });
18462
19389
  throw toWrongStackError(err);
18463
19390
  }
18464
- const delay = Math.round(
18465
- retry.delayMs(attempt, isProviderErr ? err : errAsErr)
18466
- );
18467
19391
  const attemptNum = attempt + 1;
18468
19392
  const maxAttempts = retry.maxAttempts(isProviderErr ? err : errAsErr);
18469
19393
  logger.warn(`Provider retry ${attemptNum}/${maxAttempts} in ${delay}ms \u2014 ${description}`, {
@@ -18474,17 +19398,15 @@ async function runProviderWithRetry(opts) {
18474
19398
  errorDescription: description,
18475
19399
  status: isProviderErr ? err.status : void 0
18476
19400
  });
18477
- if (isProviderErr) {
18478
- events.emit("provider.retry", {
18479
- sessionId: ctx.session.id,
18480
- providerId: err.providerId,
18481
- attempt: attemptNum,
18482
- delayMs: delay,
18483
- status: err.status,
18484
- description
18485
- });
18486
- }
18487
- await new Promise((resolve12, reject) => {
19401
+ events.emit("provider.retry", {
19402
+ sessionId: ctx.session.id,
19403
+ providerId: isProviderErr ? err.providerId : provider.id,
19404
+ attempt: attemptNum,
19405
+ delayMs: delay,
19406
+ status: isProviderErr ? err.status : 0,
19407
+ description
19408
+ });
19409
+ await new Promise((resolve14, reject) => {
18488
19410
  let settled = false;
18489
19411
  const cleanup = () => {
18490
19412
  clearTimeout(t);
@@ -18500,7 +19422,7 @@ async function runProviderWithRetry(opts) {
18500
19422
  if (settled) return;
18501
19423
  settled = true;
18502
19424
  cleanup();
18503
- resolve12();
19425
+ resolve14();
18504
19426
  }, delay);
18505
19427
  if (signal.aborted) {
18506
19428
  onAbort();
@@ -18512,6 +19434,9 @@ async function runProviderWithRetry(opts) {
18512
19434
  }
18513
19435
  }
18514
19436
  }
19437
+ function elapsedMs(startedNs) {
19438
+ return Number(process.hrtime.bigint() - startedNs) / 1e6;
19439
+ }
18515
19440
 
18516
19441
  // src/execution/provider-runner-impl.ts
18517
19442
  var DefaultProviderRunner = class {
@@ -18525,6 +19450,7 @@ import { randomInt } from "node:crypto";
18525
19450
  var MAX_RETRY_AFTER_MS = 6e4;
18526
19451
  var MAX_ATTEMPTS_BY_KIND = {
18527
19452
  rate_limit: 5,
19453
+ quota_exhausted: 0,
18528
19454
  stream_hang: 5,
18529
19455
  // transient, worth retrying aggressively
18530
19456
  overloaded: 3,
@@ -19263,7 +20189,9 @@ var DefaultSkillLoader = class {
19263
20189
  const seen = /* @__PURE__ */ new Set();
19264
20190
  for (const { dir, source, originTool } of this.dirs) {
19265
20191
  try {
19266
- const entries = await fs5.readdir(dir, { withFileTypes: true });
20192
+ const entries = (await fs5.readdir(dir, { withFileTypes: true })).sort(
20193
+ (a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0
20194
+ );
19267
20195
  for (const e of entries) {
19268
20196
  if (!await entryIsDirectory(dir, e)) continue;
19269
20197
  const skillFile = path15.join(dir, e.name, "SKILL.md");
@@ -19890,10 +20818,178 @@ function readPolicy(ctx) {
19890
20818
  }
19891
20819
 
19892
20820
  // src/execution/tool-executor.ts
19893
- import { randomUUID as randomUUID10 } from "node:crypto";
20821
+ import { randomUUID as randomUUID11 } from "node:crypto";
20822
+
20823
+ // src/observability/process-telemetry.ts
20824
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
20825
+ var storage2 = new AsyncLocalStorage2();
20826
+ function runWithProcessTelemetry(context, run) {
20827
+ return storage2.run(context, run);
20828
+ }
20829
+
20830
+ // src/execution/tool-executor.ts
19894
20831
  import { isDeepStrictEqual } from "node:util";
19895
20832
  import * as fs8 from "node:fs/promises";
20833
+ import * as path19 from "node:path";
20834
+
20835
+ // src/security/kanban-boundary.ts
20836
+ import { realpath as realpath3 } from "node:fs/promises";
19896
20837
  import * as path18 from "node:path";
20838
+ import {
20839
+ evaluateKanbanBoundaryOpaque,
20840
+ evaluateKanbanBoundaryPath,
20841
+ readBoard,
20842
+ resolveKanbanBoundaryLayers
20843
+ } from "@wrongstack/kanban";
20844
+ var PATH_KEYS = /* @__PURE__ */ new Set([
20845
+ "path",
20846
+ "paths",
20847
+ "file",
20848
+ "files",
20849
+ "directory",
20850
+ "dir",
20851
+ "cwd",
20852
+ "root",
20853
+ "baseDir",
20854
+ "out",
20855
+ "outputPath",
20856
+ "sourcePath",
20857
+ "targetPath",
20858
+ "destinationPath",
20859
+ "fromPath",
20860
+ "toPath",
20861
+ "worktreePath"
20862
+ ]);
20863
+ var TOOL_PATH_KEYS = {
20864
+ // `target` is an identifier in tools such as plan/task/document, but a
20865
+ // filesystem target in language_info. Keep ambiguous names tool-specific.
20866
+ language_info: ["target"]
20867
+ };
20868
+ async function evaluateToolKanbanBoundary(tool, input, ctx) {
20869
+ const identity = resolveKanbanIdentity(ctx);
20870
+ if (!identity.boardId) return { decision: "allow" };
20871
+ const board = await readBoard(identity.policyRoot ?? ctx.projectRoot, identity.boardId);
20872
+ if (!board) return { decision: "allow" };
20873
+ const task = identity.taskId ? board.tasks.find((candidate) => candidate.id === identity.taskId) : void 0;
20874
+ const layers = resolveKanbanBoundaryLayers(board, task);
20875
+ if (layers.length === 0) return { decision: "allow" };
20876
+ const caps = tool.capabilities ?? [];
20877
+ const access2 = caps.includes("fs.write") || caps.includes("fs.write.outside-project") ? "write" : caps.includes("fs.read") ? "read" : void 0;
20878
+ const shellLike = caps.some((capability) => capability.startsWith("shell."));
20879
+ const candidates = access2 ? await extractCandidatePaths(tool.name, input, ctx) : [];
20880
+ let evaluation;
20881
+ if (shellLike || access2 && candidates.length === 0) {
20882
+ evaluation = evaluateKanbanBoundaryOpaque(layers, tool.name);
20883
+ } else if (access2) {
20884
+ const results = candidates.map(
20885
+ (candidate) => evaluateKanbanBoundaryPath(layers, candidate, access2)
20886
+ );
20887
+ evaluation = results.find((result) => result.decision === "block") ?? results.find((result) => result.decision === "confirm") ?? { decision: "allow" };
20888
+ } else {
20889
+ evaluation = { decision: "allow" };
20890
+ }
20891
+ return {
20892
+ ...evaluation,
20893
+ boardId: board.id,
20894
+ ...task ? { taskId: task.id } : {}
20895
+ };
20896
+ }
20897
+ function resolveKanbanIdentity(ctx) {
20898
+ const metaKanban = ctx.meta["kanban"];
20899
+ const record = metaKanban && typeof metaKanban === "object" ? metaKanban : void 0;
20900
+ const boardId = ctx.currentKanbanBoardId ?? (typeof record?.["boardId"] === "string" ? record["boardId"] : void 0);
20901
+ const taskId = ctx.currentKanbanTaskId ?? (typeof record?.["taskId"] === "string" ? record["taskId"] : void 0);
20902
+ const policyRoot = typeof record?.["projectRoot"] === "string" ? record["projectRoot"] : void 0;
20903
+ return {
20904
+ ...boardId ? { boardId } : {},
20905
+ ...taskId ? { taskId } : {},
20906
+ ...policyRoot ? { policyRoot } : {}
20907
+ };
20908
+ }
20909
+ async function extractCandidatePaths(toolName, input, ctx) {
20910
+ if (toolName === "patch" && typeof input["patch"] === "string") {
20911
+ const directoryInput = stringValue(input["directory"]) ?? ctx.workingDir;
20912
+ const directory = path18.isAbsolute(directoryInput) ? directoryInput : path18.resolve(ctx.workingDir, directoryInput);
20913
+ const strip = Math.max(1, numericValue(input["strip"]) ?? 1);
20914
+ const targets = extractPatchTargets(input["patch"], strip).map(
20915
+ (target) => relativeToProject(path18.resolve(directory, target), ctx.projectRoot)
20916
+ );
20917
+ return Promise.all(targets.map((target) => canonicalizeCandidatePath(target, ctx)));
20918
+ }
20919
+ const values = [];
20920
+ const pathKeys = new Set(PATH_KEYS);
20921
+ for (const key of TOOL_PATH_KEYS[toolName] ?? []) pathKeys.add(key);
20922
+ collectPathValues(input, values, pathKeys);
20923
+ if (toolName === "scaffold" && typeof input["name"] === "string") {
20924
+ const cwd = stringValue(input["cwd"]) ?? ctx.workingDir;
20925
+ values.push(path18.join(cwd, input["name"]));
20926
+ }
20927
+ const candidates = [
20928
+ ...new Set(values.flatMap(splitPathList).map((value) => resolveInputPath(value, ctx)))
20929
+ ];
20930
+ return Promise.all(candidates.map((candidate) => canonicalizeCandidatePath(candidate, ctx)));
20931
+ }
20932
+ async function canonicalizeCandidatePath(candidate, ctx) {
20933
+ if (path18.isAbsolute(candidate)) return candidate;
20934
+ const absolute = path18.resolve(ctx.projectRoot, candidate);
20935
+ const canonicalRoot = await realpath3(ctx.projectRoot).catch(() => ctx.projectRoot);
20936
+ let probe = absolute;
20937
+ const missingSegments = [];
20938
+ while (true) {
20939
+ try {
20940
+ const canonical = path18.join(await realpath3(probe), ...missingSegments);
20941
+ return relativeToProject(canonical, canonicalRoot);
20942
+ } catch (cause) {
20943
+ const code = cause.code;
20944
+ if (code !== "ENOENT" && code !== "ENOTDIR") return absolute;
20945
+ const parent = path18.dirname(probe);
20946
+ if (parent === probe) return absolute;
20947
+ missingSegments.unshift(path18.basename(probe));
20948
+ probe = parent;
20949
+ }
20950
+ }
20951
+ }
20952
+ function collectPathValues(value, output, pathKeys, key) {
20953
+ if (typeof value === "string") {
20954
+ if (key && pathKeys.has(key)) output.push(value);
20955
+ return;
20956
+ }
20957
+ if (Array.isArray(value)) {
20958
+ for (const item of value) collectPathValues(item, output, pathKeys, key);
20959
+ return;
20960
+ }
20961
+ if (!value || typeof value !== "object") return;
20962
+ for (const [childKey, child] of Object.entries(value)) {
20963
+ collectPathValues(child, output, pathKeys, childKey);
20964
+ }
20965
+ }
20966
+ function splitPathList(value) {
20967
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
20968
+ }
20969
+ function resolveInputPath(value, ctx) {
20970
+ const absolute = path18.isAbsolute(value) ? value : path18.resolve(ctx.workingDir, value);
20971
+ return relativeToProject(absolute, ctx.projectRoot);
20972
+ }
20973
+ function relativeToProject(absolute, projectRoot) {
20974
+ const relative5 = path18.relative(projectRoot, absolute).replace(/\\/g, "/");
20975
+ return relative5.startsWith("../") || path18.isAbsolute(relative5) ? absolute : relative5 || ".";
20976
+ }
20977
+ function extractPatchTargets(patchText, strip) {
20978
+ const targets = [];
20979
+ for (const match of patchText.matchAll(/^\+\+\+\s+([^\t\r\n]+)/gm)) {
20980
+ const raw = match[1]?.trim();
20981
+ if (!raw || raw === "/dev/null") continue;
20982
+ const parts = raw.replace(/\\/g, "/").split("/").filter((part) => part && part !== ".");
20983
+ if (parts.length > strip) targets.push(parts.slice(strip).join("/"));
20984
+ }
20985
+ return targets;
20986
+ }
20987
+ function stringValue(value) {
20988
+ return typeof value === "string" && value.trim() ? value : void 0;
20989
+ }
20990
+ function numericValue(value) {
20991
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
20992
+ }
19897
20993
 
19898
20994
  // src/types/tool-executor.ts
19899
20995
  var GOVERNED_TOOL_EXECUTOR_META_KEY = "toolExecutor.executeGoverned";
@@ -20114,6 +21210,17 @@ ${errorDetails}`,
20114
21210
  return { result, tool, durationMs: Date.now() - start };
20115
21211
  }
20116
21212
  }
21213
+ const boundary = await evaluateToolKanbanBoundary(tool, use.input, ctx);
21214
+ if (boundary.decision === "block") {
21215
+ const result = {
21216
+ type: "tool_result",
21217
+ tool_use_id: use.id,
21218
+ content: `Tool "${tool.name}" blocked by Kanban boundary. ${boundary.reason ?? ""}`.trim(),
21219
+ is_error: true
21220
+ };
21221
+ budget = this.budgetForString(result.content, budget);
21222
+ return { result, tool, durationMs: Date.now() - start };
21223
+ }
20117
21224
  const decision = await this.opts.permissionPolicy.evaluate(tool, use.input, ctx);
20118
21225
  let effectivePermission = decision.permission;
20119
21226
  const policy = this.opts.permissionPolicy;
@@ -20122,27 +21229,31 @@ ${errorDetails}`,
20122
21229
  if (toolDangerousCaps.length > 0 && effectivePermission === "auto" && !yolo && !authoritativeAuto) {
20123
21230
  effectivePermission = "confirm";
20124
21231
  }
21232
+ if (boundary.decision === "confirm" && effectivePermission !== "deny") {
21233
+ effectivePermission = "confirm";
21234
+ }
20125
21235
  if (effectivePermission === "deny") {
20126
21236
  const result = this.deniedResult(use, decision.reason);
20127
21237
  budget = this.budgetForString(result.content, budget);
20128
21238
  return { result, tool, durationMs: Date.now() - start };
20129
21239
  }
20130
21240
  if (effectivePermission === "confirm") {
21241
+ const suggestedPattern = boundary.decision === "confirm" ? `kanban-boundary:${boundary.path ?? tool.name}` : subjectForToolInput(tool.name, use.input, tool.subjectKey) ?? tool.name;
20131
21242
  if (this.opts.confirmAwaiter) {
20132
21243
  const awaiter = this.opts.confirmAwaiter;
20133
21244
  const choice = await new Promise(
20134
- (resolve12, reject) => {
21245
+ (resolve14, reject) => {
20135
21246
  const signal = ctx.signal;
20136
- const onAbort = () => resolve12("abort");
21247
+ const onAbort = () => resolve14("abort");
20137
21248
  if (signal.aborted) {
20138
- resolve12("abort");
21249
+ resolve14("abort");
20139
21250
  return;
20140
21251
  }
20141
21252
  signal.addEventListener("abort", onAbort, { once: true });
20142
- awaiter(tool, use.input, use.id, tool.name).then(
21253
+ awaiter(tool, use.input, use.id, suggestedPattern).then(
20143
21254
  (c) => {
20144
21255
  signal.removeEventListener("abort", onAbort);
20145
- resolve12(c);
21256
+ resolve14(c);
20146
21257
  },
20147
21258
  (e) => {
20148
21259
  signal.removeEventListener("abort", onAbort);
@@ -20162,7 +21273,6 @@ ${errorDetails}`,
20162
21273
  return { result, tool, durationMs: Date.now() - start };
20163
21274
  }
20164
21275
  } else {
20165
- const suggestedPattern = subjectForToolInput(tool.name, use.input, tool.subjectKey) ?? tool.name;
20166
21276
  const pending = {
20167
21277
  type: "tool_confirm_pending",
20168
21278
  toolUseId: use.id,
@@ -20170,7 +21280,8 @@ ${errorDetails}`,
20170
21280
  input: use.input,
20171
21281
  suggestedPattern,
20172
21282
  decisionSource: decision.source,
20173
- riskTier: decision.riskTier ?? tool.riskTier
21283
+ riskTier: decision.riskTier ?? tool.riskTier,
21284
+ ...boundary.decision === "confirm" && boundary.reason ? { boundaryReason: boundary.reason } : {}
20174
21285
  };
20175
21286
  return { result: pending, tool, durationMs: Date.now() - start };
20176
21287
  }
@@ -20184,6 +21295,17 @@ ${errorDetails}`,
20184
21295
  "tool.has_dangerous_capabilities": toolCapsForAudit.length > 0
20185
21296
  });
20186
21297
  try {
21298
+ const inputPath = use.input && typeof use.input === "object" ? use.input.path : void 0;
21299
+ const caps = tool.capabilities ?? [];
21300
+ const hasFileCapability = caps.includes("fs.read") || caps.includes("fs.write");
21301
+ const absPath = hasFileCapability && typeof inputPath === "string" ? path19.isAbsolute(inputPath) ? inputPath : path19.resolve(ctx.projectRoot, inputPath) : void 0;
21302
+ let writeTargetExisted;
21303
+ if (tool.name === "write" && caps.includes("fs.write") && absPath) {
21304
+ writeTargetExisted = await fs8.stat(absPath).then(
21305
+ (stat13) => stat13.isFile(),
21306
+ (error2) => error2.code === "ENOENT" ? false : void 0
21307
+ );
21308
+ }
20187
21309
  const producedText = await this.produceToolOutput(tool, use, ctx, budget);
20188
21310
  let { block: result, bytes } = this.settleToolOutput(tool, use, producedText, budget);
20189
21311
  budget -= bytes;
@@ -20212,6 +21334,35 @@ ${post.additionalContext}`;
20212
21334
  span?.setAttribute("tool.is_error", !!result.is_error);
20213
21335
  span?.setAttribute("tool.output_bytes", outputChars);
20214
21336
  this.logToolSuccess(ctx, use, tool.name, Date.now() - start, outputChars);
21337
+ if (!result.is_error && typeof inputPath === "string" && absPath) {
21338
+ const operation = tool.name === "read" ? "read" : caps.includes("fs.write") && tool.name === "write" ? writeTargetExisted === false ? "create" : "update" : caps.includes("fs.write") ? "update" : "read";
21339
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
21340
+ ctx.recordFileEvent?.({
21341
+ operation,
21342
+ filePath: inputPath,
21343
+ absPath,
21344
+ toolName: tool.name,
21345
+ toolUseId: use.id,
21346
+ durationMs: Date.now() - start
21347
+ });
21348
+ this.opts.events?.emit("file.event", {
21349
+ operation,
21350
+ filePath: inputPath,
21351
+ absPath,
21352
+ sessionId: ctx.session.id,
21353
+ agentId: ctx.agentId,
21354
+ agentName: ctx.agentName,
21355
+ provider: typeof ctx.provider === "object" ? ctx.provider.id : String(ctx.provider),
21356
+ model: ctx.model,
21357
+ toolName: tool.name,
21358
+ toolUseId: use.id,
21359
+ scope: ctx.currentKanbanTaskId ? "task" : "session",
21360
+ taskId: ctx.currentKanbanTaskId,
21361
+ boardId: ctx.currentKanbanBoardId,
21362
+ timestamp: ts,
21363
+ durationMs: Date.now() - start
21364
+ });
21365
+ }
20215
21366
  return { result, tool, durationMs: Date.now() - start };
20216
21367
  } catch (err) {
20217
21368
  if (isWrongStackError(err)) {
@@ -20309,7 +21460,7 @@ ${post.additionalContext}`;
20309
21460
  const bridge = async (toolName, input) => {
20310
21461
  const nestedUse = {
20311
21462
  type: "tool_use",
20312
- id: `nested-${randomUUID10()}`,
21463
+ id: `nested-${randomUUID11()}`,
20313
21464
  name: toolName,
20314
21465
  input
20315
21466
  };
@@ -20363,6 +21514,9 @@ ${post.additionalContext}`;
20363
21514
  async produceToolOutput(tool, use, ctx, budgetHint) {
20364
21515
  this.opts.events?.emit("tool.started", {
20365
21516
  sessionId: ctx.session.id,
21517
+ ...ctx.traceId ? { traceId: ctx.traceId } : {},
21518
+ agentId: ctx.agentId,
21519
+ agentName: ctx.agentName,
20366
21520
  name: tool.name,
20367
21521
  id: use.id,
20368
21522
  input: use.input
@@ -20413,11 +21567,31 @@ ${post.additionalContext}`;
20413
21567
  )
20414
21568
  ]);
20415
21569
  let output;
20416
- const toolPromise = typeof tool.executeStream === "function" ? this.runStreamedTool(tool, input, ctx, combined, toolUseId) : (async () => tool.execute(input, ctx, { signal: combined }))();
21570
+ const execute = () => typeof tool.executeStream === "function" ? this.runStreamedTool(tool, input, ctx, combined, toolUseId) : (async () => tool.execute(input, ctx, { signal: combined }))();
21571
+ const telemetryToolCallId = toolUseId ?? `nested-${randomUUID11()}`;
21572
+ const toolPromise = this.opts.events ? runWithNetworkTelemetry({
21573
+ events: this.opts.events,
21574
+ sessionId: ctx.session.id,
21575
+ ...ctx.traceId ? { traceId: ctx.traceId } : {},
21576
+ ...ctx.agentId ? { agentId: ctx.agentId } : {},
21577
+ toolCallId: telemetryToolCallId,
21578
+ initiator: "tool",
21579
+ operationName: tool.name
21580
+ }, () => runWithProcessTelemetry(
21581
+ {
21582
+ events: this.opts.events,
21583
+ sessionId: ctx.session.id,
21584
+ ...ctx.traceId ? { traceId: ctx.traceId } : {},
21585
+ ...ctx.agentId ? { agentId: ctx.agentId } : {},
21586
+ toolCallId: telemetryToolCallId,
21587
+ toolName: tool.name
21588
+ },
21589
+ execute
21590
+ )) : execute();
20417
21591
  toolPromise.catch(() => {
20418
21592
  });
20419
21593
  try {
20420
- output = await new Promise((resolve12, reject) => {
21594
+ output = await new Promise((resolve14, reject) => {
20421
21595
  const onAbort = () => {
20422
21596
  setTimeout(() => reject(abortReasonToError(combined.reason)), 0);
20423
21597
  };
@@ -20425,7 +21599,7 @@ ${post.additionalContext}`;
20425
21599
  toolPromise.then(
20426
21600
  (v) => {
20427
21601
  combined.removeEventListener("abort", onAbort);
20428
- resolve12(v);
21602
+ resolve14(v);
20429
21603
  },
20430
21604
  (e) => {
20431
21605
  combined.removeEventListener("abort", onAbort);
@@ -20471,6 +21645,9 @@ ${post.additionalContext}`;
20471
21645
  const emitProgress = (ev) => {
20472
21646
  this.opts.events?.emit("tool.progress", {
20473
21647
  sessionId: ctx.session.id,
21648
+ ...ctx.traceId ? { traceId: ctx.traceId } : {},
21649
+ agentId: ctx.agentId,
21650
+ agentName: ctx.agentName,
20474
21651
  name: tool.name,
20475
21652
  id: toolUseId ?? "<unknown>",
20476
21653
  event: ev
@@ -20685,13 +21862,25 @@ function httpStatusToCategory(status) {
20685
21862
  return { category: "transient" /* TRANSIENT */, retryable: true, detail: `HTTP ${status}` };
20686
21863
  }
20687
21864
  if (status === 404 || status === 410) {
20688
- return { category: "not_found" /* NOT_FOUND */, retryable: false, detail: `HTTP ${status}` };
21865
+ return {
21866
+ category: "not_found" /* NOT_FOUND */,
21867
+ retryable: false,
21868
+ detail: `HTTP ${status}`
21869
+ };
20689
21870
  }
20690
21871
  if (status === 401 || status === 403) {
20691
- return { category: "permission" /* PERMISSION */, retryable: false, detail: `HTTP ${status}` };
21872
+ return {
21873
+ category: "permission" /* PERMISSION */,
21874
+ retryable: false,
21875
+ detail: `HTTP ${status}`
21876
+ };
20692
21877
  }
20693
21878
  if (status === 400) {
20694
- return { category: "validation" /* VALIDATION */, retryable: false, detail: `HTTP ${status}` };
21879
+ return {
21880
+ category: "validation" /* VALIDATION */,
21881
+ retryable: false,
21882
+ detail: `HTTP ${status}`
21883
+ };
20695
21884
  }
20696
21885
  return { category: "fatal" /* FATAL */, retryable: false, detail: `HTTP ${status}` };
20697
21886
  }
@@ -20701,11 +21890,11 @@ async function maybePersistLargeToolOutput(toolName, content, budget) {
20701
21890
  return content;
20702
21891
  }
20703
21892
  try {
20704
- const dir = path18.join(wstackGlobalRoot(), "tool-output");
21893
+ const dir = path19.join(wstackGlobalRoot(), "tool-output");
20705
21894
  await fs8.mkdir(dir, { recursive: true });
20706
21895
  const safeTool = toolName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 40) || "tool";
20707
21896
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
20708
- const filePath = path18.join(dir, `${stamp}-${safeTool}-${randomUUID10()}.log`);
21897
+ const filePath = path19.join(dir, `${stamp}-${safeTool}-${randomUUID11()}.log`);
20709
21898
  await fs8.writeFile(filePath, content, "utf8");
20710
21899
  const marker = `[full tool output: ${bytes} bytes at ${filePath}; read/grep that file selectively instead of re-running or requesting more output]`;
20711
21900
  const fixedBytes = Buffer.byteLength(marker + TOOL_OUTPUT_ARTIFACT_OMISSION, "utf8");
@@ -21017,7 +22206,7 @@ var contextManagerTool = createContextManagerTool();
21017
22206
 
21018
22207
  // src/infrastructure/logger.ts
21019
22208
  import * as fsp11 from "node:fs/promises";
21020
- import * as path19 from "node:path";
22209
+ import * as path20 from "node:path";
21021
22210
  var LEVEL_RANK2 = {
21022
22211
  error: 0,
21023
22212
  warn: 1,
@@ -21081,7 +22270,7 @@ var DefaultLogger = class _DefaultLogger {
21081
22270
  this.stderr = opts.stderr !== false;
21082
22271
  this.maxFileBytes = opts.maxFileBytes ?? 10 * 1024 * 1024;
21083
22272
  if (this.file) {
21084
- const dir = path19.dirname(this.file);
22273
+ const dir = path20.dirname(this.file);
21085
22274
  this._tail = this._tail.then(async () => {
21086
22275
  await fsp11.mkdir(dir, { recursive: true });
21087
22276
  }).catch(() => void 0);
@@ -21403,27 +22592,27 @@ function codexModelMeta(id) {
21403
22592
 
21404
22593
  // src/models/mode-store.ts
21405
22594
  import * as fs9 from "node:fs/promises";
21406
- import * as path21 from "node:path";
22595
+ import * as path22 from "node:path";
21407
22596
 
21408
22597
  // src/types/mode-prompts.ts
21409
22598
  import { readFileSync as readFileSync4, statSync as statSync4 } from "node:fs";
21410
- import * as path20 from "node:path";
22599
+ import * as path21 from "node:path";
21411
22600
  import { fileURLToPath as fileURLToPath4 } from "node:url";
21412
22601
  function modePrompt(id) {
21413
22602
  for (const dir of modePromptDirCandidates()) {
21414
22603
  try {
21415
- return readFileSync4(path20.join(dir, `${id}.md`), "utf8").trimEnd();
22604
+ return readFileSync4(path21.join(dir, `${id}.md`), "utf8").trimEnd();
21416
22605
  } catch {
21417
22606
  }
21418
22607
  }
21419
22608
  return "";
21420
22609
  }
21421
22610
  function modePromptDirCandidates() {
21422
- const here = path20.dirname(fileURLToPath4(import.meta.url));
22611
+ const here = path21.dirname(fileURLToPath4(import.meta.url));
21423
22612
  const candidates = [
21424
- path20.resolve(here, "../../instructions/modes"),
21425
- path20.resolve(here, "../instructions/modes"),
21426
- path20.resolve(here, "instructions/modes")
22613
+ path21.resolve(here, "../../instructions/modes"),
22614
+ path21.resolve(here, "../instructions/modes"),
22615
+ path21.resolve(here, "instructions/modes")
21427
22616
  ];
21428
22617
  return candidates.sort((a, b) => Number(!isDirectory3(a)) - Number(!isDirectory3(b)));
21429
22618
  }
@@ -21655,7 +22844,7 @@ var DefaultModeStore = class {
21655
22844
  }
21656
22845
  async loadActiveMode() {
21657
22846
  try {
21658
- const configPath = path21.join(this.configDir, "mode.json");
22847
+ const configPath = path22.join(this.configDir, "mode.json");
21659
22848
  const content = await fs9.readFile(configPath, "utf8");
21660
22849
  const data = JSON.parse(content);
21661
22850
  this.activeModeId = data.activeMode ?? null;
@@ -21666,7 +22855,7 @@ var DefaultModeStore = class {
21666
22855
  async saveActiveMode() {
21667
22856
  try {
21668
22857
  await fs9.mkdir(this.configDir, { recursive: true });
21669
- const configPath = path21.join(this.configDir, "mode.json");
22858
+ const configPath = path22.join(this.configDir, "mode.json");
21670
22859
  await atomicWrite(
21671
22860
  configPath,
21672
22861
  JSON.stringify({ activeMode: this.activeModeId }, null, 2)
@@ -21681,11 +22870,11 @@ async function loadProjectModes(modesDir) {
21681
22870
  const entries = await fs9.readdir(modesDir);
21682
22871
  for (const entry of entries) {
21683
22872
  if (!entry.endsWith(".md") && !entry.endsWith(".txt")) continue;
21684
- const filePath = path21.join(modesDir, entry);
21685
- const stat12 = await fs9.stat(filePath);
21686
- if (!stat12.isFile()) continue;
22873
+ const filePath = path22.join(modesDir, entry);
22874
+ const stat13 = await fs9.stat(filePath);
22875
+ if (!stat13.isFile()) continue;
21687
22876
  const content = await fs9.readFile(filePath, "utf8");
21688
- const id = path21.basename(entry, path21.extname(entry));
22877
+ const id = path22.basename(entry, path22.extname(entry));
21689
22878
  modes.push({
21690
22879
  id,
21691
22880
  name: id.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
@@ -21701,7 +22890,7 @@ async function loadProjectModes(modesDir) {
21701
22890
  async function loadUserModes(modesDir) {
21702
22891
  const modes = [];
21703
22892
  try {
21704
- const manifestPath = path21.join(modesDir, "modes.json");
22893
+ const manifestPath = path22.join(modesDir, "modes.json");
21705
22894
  const content = await fs9.readFile(manifestPath, "utf8");
21706
22895
  const manifest = JSON.parse(content);
21707
22896
  for (const mode of manifest.modes) {
@@ -21714,7 +22903,7 @@ async function loadUserModes(modesDir) {
21714
22903
 
21715
22904
  // src/models/models-registry.ts
21716
22905
  import * as fs10 from "node:fs/promises";
21717
- import * as path22 from "node:path";
22906
+ import * as path23 from "node:path";
21718
22907
  var DEFAULT_URL = "https://models.dev/api.json";
21719
22908
  var ENV_URL_KEY = "WRONGSTACK_MODELS_DEV_URL";
21720
22909
  var DEFAULT_TTL_SECONDS = 24 * 3600;
@@ -21790,7 +22979,7 @@ var DefaultModelsRegistry = class {
21790
22979
  this.overlay = opts.overlay;
21791
22980
  this.overlayUrl = opts.overlayUrl;
21792
22981
  this.overlayFile = opts.overlayFile;
21793
- this.overlayCacheFile = opts.overlayCacheFile ?? (opts.overlayUrl ? path22.join(path22.dirname(opts.cacheFile), "models-overlay-cache.json") : void 0);
22982
+ this.overlayCacheFile = opts.overlayCacheFile ?? (opts.overlayUrl ? path23.join(path23.dirname(opts.cacheFile), "models-overlay-cache.json") : void 0);
21794
22983
  }
21795
22984
  async load(opts = {}) {
21796
22985
  if (this.payload && !opts.force) return this.payload;
@@ -22042,7 +23231,7 @@ var DefaultModelsRegistry = class {
22042
23231
  }
22043
23232
  /** Used by `wstack models refresh` to expose where the cache lives. */
22044
23233
  cacheLocation() {
22045
- return path22.resolve(this.cacheFile);
23234
+ return path23.resolve(this.cacheFile);
22046
23235
  }
22047
23236
  };
22048
23237
  function formatAge(seconds) {
@@ -22280,9 +23469,9 @@ var DefaultHealthRegistry = class {
22280
23469
  }
22281
23470
  async runOne(check) {
22282
23471
  let timer = null;
22283
- const timeout = new Promise((resolve12) => {
23472
+ const timeout = new Promise((resolve14) => {
22284
23473
  timer = setTimeout(
22285
- () => resolve12({ status: "unhealthy", detail: `timeout after ${this.timeoutMs}ms` }),
23474
+ () => resolve14({ status: "unhealthy", detail: `timeout after ${this.timeoutMs}ms` }),
22286
23475
  this.timeoutMs
22287
23476
  );
22288
23477
  });
@@ -22466,7 +23655,7 @@ async function startMetricsServer(opts) {
22466
23655
  const tls = opts.tls;
22467
23656
  const useHttps = !!(tls?.cert && tls?.key);
22468
23657
  const host = opts.host ?? "127.0.0.1";
22469
- const path28 = opts.path ?? "/metrics";
23658
+ const path29 = opts.path ?? "/metrics";
22470
23659
  const healthPath = opts.healthPath ?? "/healthz";
22471
23660
  const healthRegistry = opts.healthRegistry;
22472
23661
  const listener = (req, res) => {
@@ -22476,7 +23665,7 @@ async function startMetricsServer(opts) {
22476
23665
  return;
22477
23666
  }
22478
23667
  const url = req.url.split("?")[0];
22479
- if (url === path28) {
23668
+ if (url === path29) {
22480
23669
  let body;
22481
23670
  try {
22482
23671
  body = renderPrometheus(opts.sink.snapshot());
@@ -22522,14 +23711,14 @@ async function startMetricsServer(opts) {
22522
23711
  const { createServer } = await import("node:http");
22523
23712
  server = createServer(listener);
22524
23713
  }
22525
- await new Promise((resolve12, reject) => {
23714
+ await new Promise((resolve14, reject) => {
22526
23715
  const onError = (err) => {
22527
23716
  server.off("listening", onListening);
22528
23717
  reject(err);
22529
23718
  };
22530
23719
  const onListening = () => {
22531
23720
  server.off("error", onError);
22532
- resolve12();
23721
+ resolve14();
22533
23722
  };
22534
23723
  server.once("error", onError);
22535
23724
  server.once("listening", onListening);
@@ -22540,9 +23729,9 @@ async function startMetricsServer(opts) {
22540
23729
  const protocol = useHttps ? "https" : "http";
22541
23730
  return {
22542
23731
  port: boundPort,
22543
- url: `${protocol}://${host}:${boundPort}${path28}`,
22544
- close: () => new Promise((resolve12, reject) => {
22545
- server.close((err) => err ? reject(err) : resolve12());
23732
+ url: `${protocol}://${host}:${boundPort}${path29}`,
23733
+ close: () => new Promise((resolve14, reject) => {
23734
+ server.close((err) => err ? reject(err) : resolve14());
22546
23735
  })
22547
23736
  };
22548
23737
  }
@@ -22857,19 +24046,19 @@ var UNSAFE_PROPERTY_NAMES = /* @__PURE__ */ new Set(["__proto__", "prototype", "
22857
24046
  function isRecord5(value) {
22858
24047
  return typeof value === "object" && value !== null && !Array.isArray(value);
22859
24048
  }
22860
- function error(diagnostics, code, path28, message) {
22861
- diagnostics.push({ severity: "error", code, path: path28, message });
24049
+ function error(diagnostics, code, path29, message) {
24050
+ diagnostics.push({ severity: "error", code, path: path29, message });
22862
24051
  }
22863
- function validatePatterns(value, path28, diagnostics) {
24052
+ function validatePatterns(value, path29, diagnostics) {
22864
24053
  if (!Array.isArray(value)) {
22865
- error(diagnostics, "invalid_pattern_list", path28, "must be an array of strings");
24054
+ error(diagnostics, "invalid_pattern_list", path29, "must be an array of strings");
22866
24055
  return void 0;
22867
24056
  }
22868
24057
  if (value.length > TRUST_POLICY_LIMITS.maxPatternsPerRule) {
22869
24058
  error(
22870
24059
  diagnostics,
22871
24060
  "too_many_patterns",
22872
- path28,
24061
+ path29,
22873
24062
  `must contain at most ${TRUST_POLICY_LIMITS.maxPatternsPerRule} patterns`
22874
24063
  );
22875
24064
  return void 0;
@@ -22877,7 +24066,7 @@ function validatePatterns(value, path28, diagnostics) {
22877
24066
  const patterns = [];
22878
24067
  const seen = /* @__PURE__ */ new Set();
22879
24068
  for (const [index, pattern] of value.entries()) {
22880
- const itemPath = `${path28}[${index}]`;
24069
+ const itemPath = `${path29}[${index}]`;
22881
24070
  if (typeof pattern !== "string" || pattern.length === 0 || pattern.length > TRUST_POLICY_LIMITS.maxPatternChars) {
22882
24071
  error(
22883
24072
  diagnostics,
@@ -22989,8 +24178,8 @@ var LruCache = class {
22989
24178
  this.capacity = capacity;
22990
24179
  }
22991
24180
  get(key) {
24181
+ if (!this.store.has(key)) return void 0;
22992
24182
  const value = this.store.get(key);
22993
- if (value === void 0) return void 0;
22994
24183
  this.store.delete(key);
22995
24184
  this.store.set(key, value);
22996
24185
  return value;
@@ -23247,9 +24436,21 @@ var DefaultPermissionPolicy = class {
23247
24436
  this._evalCache.clear();
23248
24437
  this.loaded = true;
23249
24438
  }
24439
+ _logDeny(tool, subject, reason) {
24440
+ console.warn(JSON.stringify({
24441
+ level: "warn",
24442
+ event: "permission.denied",
24443
+ message: `Permission denied: ${tool}${subject ? ` (subject: ${subject})` : ""} \u2014 ${reason}`,
24444
+ tool,
24445
+ subject,
24446
+ reason,
24447
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
24448
+ }));
24449
+ }
23250
24450
  async evaluate(tool, input, ctx) {
23251
24451
  if (!this.loaded) await this.reload();
23252
24452
  if (this.policyInvalid) {
24453
+ this._logDeny(tool.name, void 0, "trust policy is invalid");
23253
24454
  return {
23254
24455
  permission: "deny",
23255
24456
  source: "deny",
@@ -23265,6 +24466,7 @@ var DefaultPermissionPolicy = class {
23265
24466
  if (cached !== void 0) return cached;
23266
24467
  }
23267
24468
  if (this.sessionDenied.has(cacheKey)) {
24469
+ this._logDeny(tool.name, subject, "session soft deny (user pressed no)");
23268
24470
  const decision = { permission: "deny", source: "deny", reason: "session soft deny (user pressed no)" };
23269
24471
  this._evalCache.set(cacheKey, decision);
23270
24472
  return decision;
@@ -23279,11 +24481,13 @@ var DefaultPermissionPolicy = class {
23279
24481
  return decision;
23280
24482
  }
23281
24483
  if (entry?.deny && subject && matchesTrust(entry.deny, subject)) {
24484
+ this._logDeny(tool.name, subject, "matched deny pattern");
23282
24485
  const decision = { permission: "deny", source: "deny", reason: "matched deny pattern" };
23283
24486
  this._evalCache.set(cacheKey, decision);
23284
24487
  return decision;
23285
24488
  }
23286
24489
  if (tool.permission === "deny") {
24490
+ this._logDeny(tool.name, subject, "tool default deny");
23287
24491
  const decision = { permission: "deny", source: "default", reason: "tool default deny" };
23288
24492
  this._evalCache.set(cacheKey, decision);
23289
24493
  return decision;
@@ -23311,6 +24515,7 @@ var DefaultPermissionPolicy = class {
23311
24515
  }
23312
24516
  if (userDecision === "deny") {
23313
24517
  await this.deny({ tool: tool.name, pattern: subject ?? tool.name });
24518
+ this._logDeny(tool.name, subject, "user denied sensitive read");
23314
24519
  return { permission: "deny", source: "user", reason: "user denied sensitive read" };
23315
24520
  }
23316
24521
  return {
@@ -23363,6 +24568,7 @@ var DefaultPermissionPolicy = class {
23363
24568
  }
23364
24569
  if (decision === "deny") {
23365
24570
  await this.deny({ tool: tool.name, pattern: subject ?? tool.name });
24571
+ this._logDeny(tool.name, subject, "user denied");
23366
24572
  return { permission: "deny", source: "user", reason: "user denied" };
23367
24573
  }
23368
24574
  return { permission: decision === "yes" ? "auto" : "deny", source: "user" };
@@ -23681,7 +24887,7 @@ var DefaultSecretScrubber = class {
23681
24887
  import { createCipheriv, createDecipheriv, randomBytes as randomBytes4, scryptSync } from "node:crypto";
23682
24888
  import * as fs12 from "node:fs";
23683
24889
  import * as fsp12 from "node:fs/promises";
23684
- import * as path23 from "node:path";
24890
+ import * as path24 from "node:path";
23685
24891
 
23686
24892
  // src/types/secret-vault.ts
23687
24893
  var ENCRYPTED_PREFIX_PATTERN = /^enc:v(\d+):/;
@@ -23821,8 +25027,8 @@ function checkKeyFilePermissions(keyFile, opts) {
23821
25027
  if (process.platform === "win32") return;
23822
25028
  const warn = opts?.warn ?? ((msg) => console.warn(msg));
23823
25029
  try {
23824
- const stat12 = fs12.statSync(keyFile);
23825
- const actualMode = stat12.mode & 511;
25030
+ const stat13 = fs12.statSync(keyFile);
25031
+ const actualMode = stat13.mode & 511;
23826
25032
  if (actualMode !== KEY_FILE_MODE) {
23827
25033
  warn(
23828
25034
  `Key file ${keyFile} has mode ${actualMode.toString(8)} \u2014 expected ${KEY_FILE_MODE.toString(8)}. Run: chmod ${KEY_FILE_MODE.toString(8)} ${keyFile}`
@@ -23918,7 +25124,7 @@ var DefaultSecretVault = class {
23918
25124
  const oldVersion = this._keyVersion;
23919
25125
  const newKey = randomBytes4(KEY_BYTES);
23920
25126
  const newVersion = oldVersion + 1;
23921
- fs12.mkdirSync(path23.dirname(this.keyFile), { recursive: true });
25127
+ fs12.mkdirSync(path24.dirname(this.keyFile), { recursive: true });
23922
25128
  const passphrase = getVaultPassphrase();
23923
25129
  if (passphrase) {
23924
25130
  fs12.writeFileSync(this.keyFile, wrapDataKey(newKey, newVersion, passphrase), { mode: 384 });
@@ -24002,7 +25208,7 @@ var DefaultSecretVault = class {
24002
25208
  } catch (err) {
24003
25209
  if (err.code !== "ENOENT") throw err;
24004
25210
  }
24005
- fs12.mkdirSync(path23.dirname(this.keyFile), { recursive: true });
25211
+ fs12.mkdirSync(path24.dirname(this.keyFile), { recursive: true });
24006
25212
  const key = randomBytes4(KEY_BYTES);
24007
25213
  const passphrase = getVaultPassphrase();
24008
25214
  const initialBytes = passphrase ? wrapDataKey(key, 1, passphrase) : key;
@@ -24060,7 +25266,7 @@ async function rewriteConfigEncrypted(configPath, vault, patch) {
24060
25266
  }
24061
25267
  const merged = deepMerge(current, patch ?? {});
24062
25268
  const encrypted = encryptConfigSecrets(merged, vault);
24063
- await fsp12.mkdir(path23.dirname(configPath), { recursive: true });
25269
+ await fsp12.mkdir(path24.dirname(configPath), { recursive: true });
24064
25270
  await atomicWrite(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
24065
25271
  await restrictFilePermissions(configPath);
24066
25272
  }
@@ -24144,7 +25350,7 @@ function walkCount(node, vault, counter) {
24144
25350
  // src/storage/attachment-store.ts
24145
25351
  import { randomBytes as randomBytes5 } from "node:crypto";
24146
25352
  import * as fsp13 from "node:fs/promises";
24147
- import * as path24 from "node:path";
25353
+ import * as path25 from "node:path";
24148
25354
  var DEFAULT_SPOOL_THRESHOLD = 256 * 1024;
24149
25355
  var PLACEHOLDER_RE = /\[(pasted|image|file) #(\d+)[^\]]*\]|\[file:([^\]]+)\]/g;
24150
25356
  var DefaultAttachmentStore = class {
@@ -24165,7 +25371,7 @@ var DefaultAttachmentStore = class {
24165
25371
  let data = input.data;
24166
25372
  if (this.spoolDir && bytes >= this.spoolThreshold) {
24167
25373
  await fsp13.mkdir(this.spoolDir, { recursive: true });
24168
- spooledPath = path24.join(this.spoolDir, `${id}.bin`);
25374
+ spooledPath = path25.join(this.spoolDir, `${id}.bin`);
24169
25375
  await atomicWrite(spooledPath, input.data, {
24170
25376
  encoding: input.kind === "image" ? "base64" : "utf8"
24171
25377
  });
@@ -24285,7 +25491,7 @@ function mergeAdjacentText(blocks) {
24285
25491
 
24286
25492
  // src/storage/config-loader.ts
24287
25493
  import * as fs13 from "node:fs/promises";
24288
- import * as path25 from "node:path";
25494
+ import * as path26 from "node:path";
24289
25495
 
24290
25496
  // src/types/config.ts
24291
25497
  var DEFAULT_TUI_THINKING_WORD = "thinking";
@@ -24545,6 +25751,7 @@ var defaultIndexing = {
24545
25751
  };
24546
25752
  var IN_PROJECT_ALLOWED_KEYS = /* @__PURE__ */ new Set([
24547
25753
  "version",
25754
+ "activeProfile",
24548
25755
  "model",
24549
25756
  "cwd",
24550
25757
  "context",
@@ -24567,6 +25774,7 @@ var IN_PROJECT_ALLOWED_KEYS = /* @__PURE__ */ new Set([
24567
25774
  "fallbackProfiles",
24568
25775
  "favoriteModels",
24569
25776
  "favoriteModelsOnly",
25777
+ "modelAvailabilitySchedule",
24570
25778
  "fallbackAuto",
24571
25779
  "models",
24572
25780
  "modelMatrix",
@@ -24602,6 +25810,7 @@ var KNOWN_DENIED_IN_PROJECT = [
24602
25810
  ];
24603
25811
  var KNOWN_CONFIG_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
24604
25812
  "version",
25813
+ "activeProfile",
24605
25814
  "provider",
24606
25815
  "model",
24607
25816
  "apiKey",
@@ -24618,6 +25827,7 @@ var KNOWN_CONFIG_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
24618
25827
  "fallbackProfiles",
24619
25828
  "favoriteModels",
24620
25829
  "favoriteModelsOnly",
25830
+ "modelAvailabilitySchedule",
24621
25831
  "fallbackAuto",
24622
25832
  "hooks",
24623
25833
  "plugins",
@@ -24739,9 +25949,9 @@ function stripUnsafeInProjectFields(inProject, sourcePath, warn = (msg) => conso
24739
25949
  }
24740
25950
  const outSuperMemory = out["superMemory"];
24741
25951
  if (outSuperMemory && typeof outSuperMemory === "object") {
24742
- const storage = outSuperMemory["storage"];
24743
- if (storage && typeof storage === "object" && "directory" in storage) {
24744
- const clonedStorage = { ...storage };
25952
+ const storage3 = outSuperMemory["storage"];
25953
+ if (storage3 && typeof storage3 === "object" && "directory" in storage3) {
25954
+ const clonedStorage = { ...storage3 };
24745
25955
  delete clonedStorage["directory"];
24746
25956
  out["superMemory"] = {
24747
25957
  ...outSuperMemory,
@@ -24765,8 +25975,8 @@ function stripUnsafeInProjectFields(inProject, sourcePath, warn = (msg) => conso
24765
25975
  return out;
24766
25976
  }
24767
25977
  function samePath(a, b) {
24768
- let ra = path25.resolve(a);
24769
- let rb = path25.resolve(b);
25978
+ let ra = path26.resolve(a);
25979
+ let rb = path26.resolve(b);
24770
25980
  if (process.platform === "win32" || process.platform === "darwin") {
24771
25981
  ra = ra.toLowerCase();
24772
25982
  rb = rb.toLowerCase();
@@ -24822,12 +26032,17 @@ var DefaultConfigLoader = class {
24822
26032
  let cfg = { ...BEHAVIOR_DEFAULTS };
24823
26033
  await this.ensureGlobalDefaults();
24824
26034
  const inProjectCollides = samePath(this.paths.inProjectConfig, this.paths.globalConfig) || samePath(this.paths.inProjectConfig, this.paths.projectLocalConfig);
24825
- const [global, local, inProject] = await Promise.all([
26035
+ const [bootstrap, local, inProject] = await Promise.all([
24826
26036
  this.readJson(this.paths.globalConfig),
24827
26037
  this.readJson(this.paths.projectLocalConfig),
24828
26038
  inProjectCollides ? Promise.resolve({}) : this.readJson(this.paths.inProjectConfig)
24829
26039
  ]);
24830
- cfg = deepMerge2(cfg, global);
26040
+ cfg = deepMerge2(cfg, bootstrap);
26041
+ const profileName = bootstrap.activeProfile ?? "default";
26042
+ const profileCfg = await this.readJson(this.paths.profileConfig(profileName));
26043
+ if (Object.keys(profileCfg).length > 0) {
26044
+ cfg = deepMerge2(cfg, profileCfg);
26045
+ }
24831
26046
  cfg = deepMerge2(cfg, local);
24832
26047
  cfg = deepMerge2(
24833
26048
  cfg,
@@ -24853,10 +26068,11 @@ var DefaultConfigLoader = class {
24853
26068
  cfg = deepMerge2(cfg, patch);
24854
26069
  }
24855
26070
  } catch (err) {
24856
- this.logWarn(
24857
- "Config source load failed",
24858
- { event: "config.source_load_failed", source: src.name, message: toErrorMessage(err) }
24859
- );
26071
+ this.logWarn("Config source load failed", {
26072
+ event: "config.source_load_failed",
26073
+ source: src.name,
26074
+ message: toErrorMessage(err)
26075
+ });
24860
26076
  }
24861
26077
  }
24862
26078
  if (opts.cliFlags) {
@@ -24895,6 +26111,7 @@ var DefaultConfigLoader = class {
24895
26111
  try {
24896
26112
  await withFileLock(fp, async () => {
24897
26113
  let parsed;
26114
+ let fileExisted = true;
24898
26115
  try {
24899
26116
  const raw = await fs13.readFile(fp, "utf8");
24900
26117
  const result = safeParse(raw);
@@ -24915,25 +26132,115 @@ var DefaultConfigLoader = class {
24915
26132
  durationMs: Date.now() - t0,
24916
26133
  ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
24917
26134
  });
24918
- this.logWarn(
24919
- "Config defaults read failed",
24920
- { event: "config.defaults_read_failed", path: fp, message: toErrorMessage(err) }
24921
- );
26135
+ this.logWarn("Config defaults read failed", {
26136
+ event: "config.defaults_read_failed",
26137
+ path: fp,
26138
+ message: toErrorMessage(err)
26139
+ });
24922
26140
  return;
24923
26141
  }
26142
+ fileExisted = false;
24924
26143
  parsed = {};
24925
26144
  }
24926
- const { value, changed } = fillMissingDefaults(
24927
- parsed,
24928
- BEHAVIOR_DEFAULTS
24929
- );
24930
- if (!changed) return;
24931
- await atomicWrite(fp, JSON.stringify(value, null, 2), { mode: 384 });
26145
+ const bootstrap = {
26146
+ version: 1,
26147
+ activeProfile: parsed.activeProfile ?? "default"
26148
+ };
26149
+ let needsBootstrapWrite = false;
26150
+ if (parsed.version !== 1) {
26151
+ needsBootstrapWrite = true;
26152
+ }
26153
+ if (parsed.activeProfile === void 0) {
26154
+ needsBootstrapWrite = true;
26155
+ }
26156
+ if (needsBootstrapWrite) {
26157
+ await atomicWrite(fp, JSON.stringify(bootstrap, null, 2), { mode: 384 });
26158
+ this.events?.emit("storage.write", {
26159
+ sessionId: "~config~",
26160
+ store: "config",
26161
+ filePath: fp,
26162
+ operation: "ensure_defaults",
26163
+ outcome: "success",
26164
+ durationMs: Date.now() - t0,
26165
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
26166
+ });
26167
+ }
26168
+ const profileName = bootstrap.activeProfile ?? "default";
26169
+ const profileFp = this.paths.profileConfig(profileName);
26170
+ await this.ensureProfileConfig(profileFp, parsed, fileExisted);
26171
+ });
26172
+ } catch (err) {
26173
+ this.events?.emit("storage.error", {
26174
+ sessionId: "~config~",
26175
+ store: "config",
26176
+ filePath: fp,
26177
+ operation: "ensure_defaults",
26178
+ outcome: "failure",
26179
+ error: storageErrorString(err),
26180
+ recoverable: false,
26181
+ durationMs: Date.now() - t0,
26182
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
26183
+ });
26184
+ this.logWarn("Config defaults write failed", {
26185
+ event: "config.defaults_write_failed",
26186
+ path: fp,
26187
+ message: toErrorMessage(err)
26188
+ });
26189
+ }
26190
+ }
26191
+ /**
26192
+ * Ensure a profile config file exists at the given path.
26193
+ * On first boot: migrate content from the old flat global config, or seed
26194
+ * with behavior defaults. Subsequent boots: fill any missing keys from
26195
+ * BEHAVIOR_DEFAULTS (keeping user settings intact).
26196
+ */
26197
+ async ensureProfileConfig(profileFp, oldGlobalParsed, oldFileExisted) {
26198
+ const t0 = Date.now();
26199
+ try {
26200
+ await withFileLock(profileFp, async () => {
26201
+ let parsed;
26202
+ let existed = true;
26203
+ try {
26204
+ const raw = await fs13.readFile(profileFp, "utf8");
26205
+ const result = safeParse(raw);
26206
+ if (!result.ok || !isPlainRecord(result.value)) {
26207
+ this.logWarn("Profile config parse failed \u2014 falling back to defaults", {
26208
+ event: "config.profile_parse_failed",
26209
+ path: profileFp
26210
+ });
26211
+ parsed = {};
26212
+ } else {
26213
+ parsed = result.value;
26214
+ }
26215
+ } catch (err) {
26216
+ if (err.code !== "ENOENT") {
26217
+ this.logWarn("Profile config read failed", {
26218
+ event: "config.profile_read_failed",
26219
+ path: profileFp,
26220
+ message: toErrorMessage(err)
26221
+ });
26222
+ return;
26223
+ }
26224
+ existed = false;
26225
+ parsed = {};
26226
+ }
26227
+ let seed;
26228
+ if (!existed && oldFileExisted && Object.keys(oldGlobalParsed).length >= 1) {
26229
+ seed = { ...oldGlobalParsed };
26230
+ delete seed["activeProfile"];
26231
+ const filled = fillMissingDefaults(seed, BEHAVIOR_DEFAULTS);
26232
+ seed = filled.value;
26233
+ } else {
26234
+ const filled = fillMissingDefaults(parsed, BEHAVIOR_DEFAULTS);
26235
+ if (!filled.changed) return;
26236
+ seed = filled.value;
26237
+ }
26238
+ await atomicWrite(profileFp, JSON.stringify(seed, null, 2), { mode: 384 });
24932
26239
  this.events?.emit("storage.write", {
24933
26240
  sessionId: "~config~",
24934
26241
  store: "config",
24935
- filePath: fp,
24936
- operation: "ensure_defaults",
26242
+ filePath: profileFp,
26243
+ operation: "ensure_profile_defaults",
24937
26244
  outcome: "success",
24938
26245
  durationMs: Date.now() - t0,
24939
26246
  ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
@@ -24943,18 +26250,19 @@ var DefaultConfigLoader = class {
24943
26250
  this.events?.emit("storage.error", {
24944
26251
  sessionId: "~config~",
24945
26252
  store: "config",
24946
- filePath: fp,
24947
- operation: "ensure_defaults",
26253
+ filePath: profileFp,
26254
+ operation: "ensure_profile_defaults",
24948
26255
  outcome: "failure",
24949
26256
  error: storageErrorString(err),
24950
26257
  recoverable: false,
24951
26258
  durationMs: Date.now() - t0,
24952
26259
  ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
24953
26260
  });
24954
- this.logWarn(
24955
- "Config defaults write failed",
24956
- { event: "config.defaults_write_failed", path: fp, message: toErrorMessage(err) }
24957
- );
26261
+ this.logWarn("Profile config defaults write failed", {
26262
+ event: "config.profile_write_failed",
26263
+ path: profileFp,
26264
+ message: toErrorMessage(err)
26265
+ });
24958
26266
  }
24959
26267
  }
24960
26268
  /**
@@ -25056,10 +26364,10 @@ var DefaultConfigLoader = class {
25056
26364
  error: storageErrorString(err),
25057
26365
  ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
25058
26366
  });
25059
- this.logWarn(
25060
- "Config sync load failed",
25061
- { event: "config.sync_load_failed", message: toErrorMessage(err) }
25062
- );
26367
+ this.logWarn("Config sync load failed", {
26368
+ event: "config.sync_load_failed",
26369
+ message: toErrorMessage(err)
26370
+ });
25063
26371
  return null;
25064
26372
  }
25065
26373
  }
@@ -25067,8 +26375,8 @@ var DefaultConfigLoader = class {
25067
26375
  const t0 = Date.now();
25068
26376
  let mtimeMs = null;
25069
26377
  try {
25070
- const stat12 = await fs13.stat(file);
25071
- mtimeMs = stat12.mtimeMs;
26378
+ const stat13 = await fs13.stat(file);
26379
+ mtimeMs = stat13.mtimeMs;
25072
26380
  const cached = this.jsonCache.get(file);
25073
26381
  if (cached && cached.mtimeMs === mtimeMs) {
25074
26382
  return structuredClone(cached.value);
@@ -25088,10 +26396,11 @@ var DefaultConfigLoader = class {
25088
26396
  error: storageErrorString(err),
25089
26397
  ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
25090
26398
  });
25091
- this.logWarn(
25092
- "Config read failed (stat)",
25093
- { event: "config.read_failed", path: file, message: toErrorMessage(err) }
25094
- );
26399
+ this.logWarn("Config read failed (stat)", {
26400
+ event: "config.read_failed",
26401
+ path: file,
26402
+ message: toErrorMessage(err)
26403
+ });
25095
26404
  return {};
25096
26405
  }
25097
26406
  let raw;
@@ -25109,10 +26418,11 @@ var DefaultConfigLoader = class {
25109
26418
  error: storageErrorString(err),
25110
26419
  ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
25111
26420
  });
25112
- this.logWarn(
25113
- "Config read failed (read)",
25114
- { event: "config.read_failed", path: file, message: toErrorMessage(err) }
25115
- );
26421
+ this.logWarn("Config read failed (read)", {
26422
+ event: "config.read_failed",
26423
+ path: file,
26424
+ message: toErrorMessage(err)
26425
+ });
25116
26426
  }
25117
26427
  this.jsonCache.set(file, { mtimeMs: null, value: {} });
25118
26428
  return {};
@@ -25129,10 +26439,10 @@ var DefaultConfigLoader = class {
25129
26439
  error: "parse error or empty file",
25130
26440
  ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
25131
26441
  });
25132
- this.logWarn(
25133
- "Config parse failed \u2014 falling back to defaults",
25134
- { event: "config.parse_failed", path: file }
25135
- );
26442
+ this.logWarn("Config parse failed \u2014 falling back to defaults", {
26443
+ event: "config.parse_failed",
26444
+ path: file
26445
+ });
25136
26446
  return {};
25137
26447
  }
25138
26448
  this.jsonCache.set(file, { mtimeMs, value: structuredClone(parsed.value) });
@@ -25328,7 +26638,7 @@ function deepFreeze(obj) {
25328
26638
 
25329
26639
  // src/storage/plan-store.ts
25330
26640
  import * as fsp14 from "node:fs/promises";
25331
- import { randomUUID as randomUUID11 } from "node:crypto";
26641
+ import { randomUUID as randomUUID12 } from "node:crypto";
25332
26642
  async function loadPlan(filePath, events) {
25333
26643
  const t0 = Date.now();
25334
26644
  let raw;
@@ -25426,7 +26736,7 @@ function emptyPlan(sessionId, title) {
25426
26736
  function addPlanItem(plan, title, details) {
25427
26737
  const now = (/* @__PURE__ */ new Date()).toISOString();
25428
26738
  const item = {
25429
- id: `plan_${Date.now()}_${randomUUID11().slice(0, 6)}`,
26739
+ id: `plan_${Date.now()}_${randomUUID12().slice(0, 6)}`,
25430
26740
  title,
25431
26741
  details,
25432
26742
  status: "open",
@@ -25498,7 +26808,7 @@ function deriveTodosFromPlanItem(plan, idOrIndex, subtasks) {
25498
26808
  if (subtasks && subtasks.length > 0) {
25499
26809
  for (const st of subtasks) {
25500
26810
  todos.push({
25501
- id: `todo_${Date.now()}_${randomUUID11().slice(0, 6)}`,
26811
+ id: `todo_${Date.now()}_${randomUUID12().slice(0, 6)}`,
25502
26812
  content: st,
25503
26813
  status: "pending",
25504
26814
  promotedFromPlan: item.id
@@ -25626,7 +26936,7 @@ ${cat}:`);
25626
26936
 
25627
26937
  // src/storage/queue-store.ts
25628
26938
  import * as fsp15 from "node:fs/promises";
25629
- import * as path26 from "node:path";
26939
+ import * as path27 from "node:path";
25630
26940
  var QueueStore = class {
25631
26941
  file;
25632
26942
  // Use `| undefined` (not `?`) so exactOptionalPropertyTypes doesn't
@@ -25635,7 +26945,7 @@ var QueueStore = class {
25635
26945
  traceId;
25636
26946
  logger;
25637
26947
  constructor(opts) {
25638
- this.file = path26.join(opts.dir, "queue.json");
26948
+ this.file = path27.join(opts.dir, "queue.json");
25639
26949
  this.events = opts.events;
25640
26950
  this.traceId = opts.traceId;
25641
26951
  this.logger = opts.logger;
@@ -25793,7 +27103,7 @@ function isPersistedQueueItem(v) {
25793
27103
  // src/storage/recovery-lock.ts
25794
27104
  import * as fsp16 from "node:fs/promises";
25795
27105
  import * as os3 from "node:os";
25796
- import * as path27 from "node:path";
27106
+ import * as path28 from "node:path";
25797
27107
  var LOCK_FILE = "active.json";
25798
27108
  var DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
25799
27109
  var RecoveryLock = class {
@@ -25804,7 +27114,7 @@ var RecoveryLock = class {
25804
27114
  sessionStore;
25805
27115
  probe;
25806
27116
  constructor(opts) {
25807
- this.file = path27.join(opts.dir, LOCK_FILE);
27117
+ this.file = path28.join(opts.dir, LOCK_FILE);
25808
27118
  this.pid = opts.pid ?? process.pid;
25809
27119
  this.hostname = opts.hostname ?? os3.hostname();
25810
27120
  this.maxAgeMs = opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
@@ -25865,7 +27175,7 @@ var RecoveryLock = class {
25865
27175
  * null return before calling this.
25866
27176
  */
25867
27177
  async write(sessionId) {
25868
- await ensureDir(path27.dirname(this.file));
27178
+ await ensureDir(path28.dirname(this.file));
25869
27179
  const lock = {
25870
27180
  v: 1,
25871
27181
  sessionId,
@@ -26040,6 +27350,9 @@ var CORE_RECONSTRUCT_EVENTS = /* @__PURE__ */ new Set([
26040
27350
  "user_input",
26041
27351
  "llm_response",
26042
27352
  "tool_result",
27353
+ "message_appended",
27354
+ "message_updated",
27355
+ "messages_replaced",
26043
27356
  "context_snapshot",
26044
27357
  "checkpoint",
26045
27358
  "file_snapshot",
@@ -26060,11 +27373,22 @@ var STANDARD_AUDIT_EVENTS = /* @__PURE__ */ new Set([
26060
27373
  "provider_retry",
26061
27374
  "provider_error",
26062
27375
  "skill_activated",
26063
- "skill_deactivated"
26064
- ]);
26065
- var FULL_ONLY_EVENTS = /* @__PURE__ */ new Set([
26066
- "tool_progress"
27376
+ "skill_deactivated",
27377
+ // Resume markers and fleet lifecycle. These reach disk today only because
27378
+ // every producer appends to the raw SessionWriter and bypasses this bridge —
27379
+ // routed through it while absent from both sets, `isAllowed` would drop them
27380
+ // at the DEFAULT level and the resume timeline would lose them silently.
27381
+ "mode_changed",
27382
+ "agent_spawned",
27383
+ "agent_stopped",
27384
+ "agent_error",
27385
+ "task_created",
27386
+ "task_updated",
27387
+ "task_completed",
27388
+ "task_failed",
27389
+ "side_effect"
26067
27390
  ]);
27391
+ var FULL_ONLY_EVENTS = /* @__PURE__ */ new Set(["tool_progress"]);
26068
27392
  function isAllowed(type, level) {
26069
27393
  if (CORE_RECONSTRUCT_EVENTS.has(type)) return true;
26070
27394
  if (level === "minimal") return false;
@@ -26119,9 +27443,7 @@ function createSessionEventBridge(writer, level = "standard", options = {}) {
26119
27443
  async appendBatch(events) {
26120
27444
  const target = resolveWriter();
26121
27445
  if (!target || events.length === 0) return;
26122
- const allowed = events.filter(
26123
- (e) => isAllowed(e.type, currentLevel) && shouldSample(e)
26124
- );
27446
+ const allowed = events.filter((e) => isAllowed(e.type, currentLevel) && shouldSample(e));
26125
27447
  if (allowed.length === 0) return;
26126
27448
  try {
26127
27449
  await target.appendBatch(allowed);
@@ -26170,8 +27492,8 @@ var DefaultSessionReader = class _DefaultSessionReader {
26170
27492
  const sessionPath = sessionScopedPath(rootDir, sessionId, ".jsonl");
26171
27493
  let mtimeMs = null;
26172
27494
  try {
26173
- const stat12 = await fs14.stat(sessionPath);
26174
- mtimeMs = stat12.mtimeMs;
27495
+ const stat13 = await fs14.stat(sessionPath);
27496
+ mtimeMs = stat13.mtimeMs;
26175
27497
  } catch {
26176
27498
  this.eventCache.delete(sessionId);
26177
27499
  this.eventCacheMtimes.delete(sessionId);