@telora/daemon 0.21.134 → 0.21.144

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 (268) hide show
  1. package/build-info.json +2 -2
  2. package/dist/audit/audit-hooks.d.ts +15 -6
  3. package/dist/audit/audit-hooks.d.ts.map +1 -1
  4. package/dist/audit/audit-hooks.js +56 -28
  5. package/dist/audit/audit-hooks.js.map +1 -1
  6. package/dist/ci/ci-escalation.js +1 -1
  7. package/dist/ci/ci-escalation.js.map +1 -1
  8. package/dist/ci/ci-sensor-loop.d.ts +24 -0
  9. package/dist/ci/ci-sensor-loop.d.ts.map +1 -1
  10. package/dist/ci/ci-sensor-loop.js +35 -9
  11. package/dist/ci/ci-sensor-loop.js.map +1 -1
  12. package/dist/cli/connect/config-write.d.ts +87 -0
  13. package/dist/cli/connect/config-write.d.ts.map +1 -0
  14. package/dist/cli/connect/config-write.js +255 -0
  15. package/dist/cli/connect/config-write.js.map +1 -0
  16. package/dist/cli/connect/registration.d.ts +76 -0
  17. package/dist/cli/connect/registration.d.ts.map +1 -0
  18. package/dist/cli/connect/registration.js +207 -0
  19. package/dist/cli/connect/registration.js.map +1 -0
  20. package/dist/cli/connect.d.ts +3 -141
  21. package/dist/cli/connect.d.ts.map +1 -1
  22. package/dist/cli/connect.js +9 -441
  23. package/dist/cli/connect.js.map +1 -1
  24. package/dist/completion/coverage-stabilized.d.ts +1 -1
  25. package/dist/completion/coverage-stabilized.d.ts.map +1 -1
  26. package/dist/completion/coverage-stabilized.js +1 -1
  27. package/dist/completion/coverage-stabilized.js.map +1 -1
  28. package/dist/completion/escalation-provenance.d.ts +2 -1
  29. package/dist/completion/escalation-provenance.d.ts.map +1 -1
  30. package/dist/completion/escalation-provenance.js +2 -1
  31. package/dist/completion/escalation-provenance.js.map +1 -1
  32. package/dist/completion/escalations.js +1 -1
  33. package/dist/completion/escalations.js.map +1 -1
  34. package/dist/completion/event-escalations.d.ts +1 -1
  35. package/dist/completion/event-escalations.d.ts.map +1 -1
  36. package/dist/completion/event-escalations.js +1 -1
  37. package/dist/completion/event-escalations.js.map +1 -1
  38. package/dist/completion/event.d.ts +1 -1
  39. package/dist/completion/event.d.ts.map +1 -1
  40. package/dist/completion/event.js +2 -2
  41. package/dist/completion/event.js.map +1 -1
  42. package/dist/completion/machine-remediable-demotion.d.ts +1 -1
  43. package/dist/completion/machine-remediable-demotion.d.ts.map +1 -1
  44. package/dist/completion/machine-remediable-demotion.js +2 -1
  45. package/dist/completion/machine-remediable-demotion.js.map +1 -1
  46. package/dist/completion/read-only-pristine.d.ts +1 -1
  47. package/dist/completion/read-only-pristine.d.ts.map +1 -1
  48. package/dist/completion/read-only-pristine.js +1 -1
  49. package/dist/completion/read-only-pristine.js.map +1 -1
  50. package/dist/completion/review-exit-escalations.d.ts +1 -1
  51. package/dist/completion/review-exit-escalations.d.ts.map +1 -1
  52. package/dist/completion/review-exit-phase.d.ts +5 -2
  53. package/dist/completion/review-exit-phase.d.ts.map +1 -1
  54. package/dist/completion/review-exit-phase.js +5 -2
  55. package/dist/completion/review-exit-phase.js.map +1 -1
  56. package/dist/completion/state-cascade.js +1 -1
  57. package/dist/completion/state-cascade.js.map +1 -1
  58. package/dist/completion/status-advance-phase.d.ts.map +1 -1
  59. package/dist/completion/status-advance-phase.js +2 -1
  60. package/dist/completion/status-advance-phase.js.map +1 -1
  61. package/dist/completion/team-lifecycle.d.ts.map +1 -1
  62. package/dist/completion/team-lifecycle.js +2 -1
  63. package/dist/completion/team-lifecycle.js.map +1 -1
  64. package/dist/delivery/delivery-advance-block.d.ts +1 -1
  65. package/dist/delivery/delivery-advance-block.d.ts.map +1 -1
  66. package/dist/delivery/delivery-advance-block.js +1 -1
  67. package/dist/delivery/delivery-advance-block.js.map +1 -1
  68. package/dist/delivery/delivery-lifecycle.js +1 -1
  69. package/dist/delivery/delivery-lifecycle.js.map +1 -1
  70. package/dist/delivery/delivery-merge-ops.js +1 -1
  71. package/dist/delivery/delivery-merge-ops.js.map +1 -1
  72. package/dist/delivery/delivery-reconciler-shadow.d.ts +1 -1
  73. package/dist/delivery/delivery-reconciler-shadow.d.ts.map +1 -1
  74. package/dist/delivery/delivery-reconciler-shadow.js +1 -1
  75. package/dist/delivery/delivery-reconciler-shadow.js.map +1 -1
  76. package/dist/delivery/operator-hold.d.ts +1 -1
  77. package/dist/delivery/operator-hold.d.ts.map +1 -1
  78. package/dist/delivery/paused-revival.d.ts.map +1 -1
  79. package/dist/delivery/paused-revival.js +2 -1
  80. package/dist/delivery/paused-revival.js.map +1 -1
  81. package/dist/directive/directive-dispatch.d.ts +1 -1
  82. package/dist/directive/directive-dispatch.d.ts.map +1 -1
  83. package/dist/directive/directive-dispatch.js +1 -1
  84. package/dist/directive/directive-dispatch.js.map +1 -1
  85. package/dist/directive/directive-executor.js +1 -1
  86. package/dist/directive/directive-executor.js.map +1 -1
  87. package/dist/drift/staleness-sensor.js +1 -1
  88. package/dist/drift/staleness-sensor.js.map +1 -1
  89. package/dist/focus/focus-engine.js +3 -2
  90. package/dist/focus/focus-engine.js.map +1 -1
  91. package/dist/focus/focus-lifecycle.d.ts.map +1 -1
  92. package/dist/focus/focus-lifecycle.js +2 -1
  93. package/dist/focus/focus-lifecycle.js.map +1 -1
  94. package/dist/focus/focus-respawn-guard.d.ts +1 -1
  95. package/dist/focus/focus-respawn-guard.d.ts.map +1 -1
  96. package/dist/focus/focus-respawn-guard.js +1 -1
  97. package/dist/focus/focus-respawn-guard.js.map +1 -1
  98. package/dist/focus/listener-auto-advance.d.ts +4 -45
  99. package/dist/focus/listener-auto-advance.d.ts.map +1 -1
  100. package/dist/focus/listener-auto-advance.js +5 -238
  101. package/dist/focus/listener-auto-advance.js.map +1 -1
  102. package/dist/focus/listener-spawn.d.ts +62 -0
  103. package/dist/focus/listener-spawn.d.ts.map +1 -0
  104. package/dist/focus/listener-spawn.js +418 -0
  105. package/dist/focus/listener-spawn.js.map +1 -0
  106. package/dist/focus/listener-state.d.ts +57 -0
  107. package/dist/focus/listener-state.d.ts.map +1 -0
  108. package/dist/focus/listener-state.js +63 -0
  109. package/dist/focus/listener-state.js.map +1 -0
  110. package/dist/focus/listener.d.ts +0 -58
  111. package/dist/focus/listener.d.ts.map +1 -1
  112. package/dist/focus/listener.js +25 -453
  113. package/dist/focus/listener.js.map +1 -1
  114. package/dist/focus/verify-open-issues.d.ts +62 -0
  115. package/dist/focus/verify-open-issues.d.ts.map +1 -0
  116. package/dist/focus/verify-open-issues.js +265 -0
  117. package/dist/focus/verify-open-issues.js.map +1 -0
  118. package/dist/focus-stage-lifecycle/index.d.ts +1 -1
  119. package/dist/focus-stage-lifecycle/index.d.ts.map +1 -1
  120. package/dist/focus-stage-lifecycle/index.js +1 -1
  121. package/dist/focus-stage-lifecycle/index.js.map +1 -1
  122. package/dist/focus-stage-lifecycle/wedge-watchdog.d.ts +1 -1
  123. package/dist/focus-stage-lifecycle/wedge-watchdog.d.ts.map +1 -1
  124. package/dist/focus-stage-lifecycle/wedge-watchdog.js +1 -1
  125. package/dist/focus-stage-lifecycle/wedge-watchdog.js.map +1 -1
  126. package/dist/git/index.d.ts +2 -0
  127. package/dist/git/index.d.ts.map +1 -1
  128. package/dist/git/index.js +2 -0
  129. package/dist/git/index.js.map +1 -1
  130. package/dist/git/merge-strategies.d.ts +80 -0
  131. package/dist/git/merge-strategies.d.ts.map +1 -0
  132. package/dist/git/merge-strategies.js +218 -0
  133. package/dist/git/merge-strategies.js.map +1 -0
  134. package/dist/git/merge-to-main.d.ts +76 -0
  135. package/dist/git/merge-to-main.d.ts.map +1 -0
  136. package/dist/git/merge-to-main.js +256 -0
  137. package/dist/git/merge-to-main.js.map +1 -0
  138. package/dist/git/merge.d.ts +27 -81
  139. package/dist/git/merge.d.ts.map +1 -1
  140. package/dist/git/merge.js +4 -448
  141. package/dist/git/merge.js.map +1 -1
  142. package/dist/git/state-detector.js +1 -1
  143. package/dist/git/state-detector.js.map +1 -1
  144. package/dist/loops/engine.js +1 -1
  145. package/dist/loops/engine.js.map +1 -1
  146. package/dist/merge/focus-merge.d.ts +1 -1
  147. package/dist/merge/focus-merge.d.ts.map +1 -1
  148. package/dist/merge/focus-merge.js +1 -1
  149. package/dist/merge/focus-merge.js.map +1 -1
  150. package/dist/merge/merge-back-loop.d.ts +1 -1
  151. package/dist/merge/merge-back-loop.d.ts.map +1 -1
  152. package/dist/merge/merge-back-loop.js +2 -2
  153. package/dist/merge/merge-back-loop.js.map +1 -1
  154. package/dist/merge/merge-detector.js +1 -1
  155. package/dist/merge/merge-detector.js.map +1 -1
  156. package/dist/nexus/return.js +1 -1
  157. package/dist/nexus/return.js.map +1 -1
  158. package/dist/prd/controller-shared.d.ts +223 -0
  159. package/dist/prd/controller-shared.d.ts.map +1 -0
  160. package/dist/prd/controller-shared.js +77 -0
  161. package/dist/prd/controller-shared.js.map +1 -0
  162. package/dist/prd/controller.d.ts +5 -256
  163. package/dist/prd/controller.d.ts.map +1 -1
  164. package/dist/prd/controller.js +9 -166
  165. package/dist/prd/controller.js.map +1 -1
  166. package/dist/prd/done-sweep.d.ts +55 -0
  167. package/dist/prd/done-sweep.d.ts.map +1 -0
  168. package/dist/prd/done-sweep.js +112 -0
  169. package/dist/prd/done-sweep.js.map +1 -0
  170. package/dist/prompt-sections/execution-sections.d.ts +1 -1
  171. package/dist/prompt-sections/execution-sections.d.ts.map +1 -1
  172. package/dist/prompt-sections/execution-sections.js +12 -2
  173. package/dist/prompt-sections/execution-sections.js.map +1 -1
  174. package/dist/prompt-sections/shared.d.ts +1 -1
  175. package/dist/prompt-sections/shared.d.ts.map +1 -1
  176. package/dist/queries/agent-resolvable-promotion.d.ts +1 -1
  177. package/dist/queries/agent-resolvable-promotion.d.ts.map +1 -1
  178. package/dist/queries/agent-resolvable-promotion.js +2 -1
  179. package/dist/queries/agent-resolvable-promotion.js.map +1 -1
  180. package/dist/queries/deliveries.d.ts +0 -195
  181. package/dist/queries/deliveries.d.ts.map +1 -1
  182. package/dist/queries/deliveries.js +1 -83
  183. package/dist/queries/deliveries.js.map +1 -1
  184. package/dist/queries/escalation-consumption.d.ts +120 -0
  185. package/dist/queries/escalation-consumption.d.ts.map +1 -0
  186. package/dist/queries/escalation-consumption.js +143 -0
  187. package/dist/queries/escalation-consumption.js.map +1 -0
  188. package/dist/queries/escalation-dispositions.d.ts +19 -0
  189. package/dist/queries/escalation-dispositions.d.ts.map +1 -0
  190. package/dist/queries/escalation-dispositions.js +18 -0
  191. package/dist/queries/escalation-dispositions.js.map +1 -0
  192. package/dist/queries/escalation-suppression.d.ts +1 -1
  193. package/dist/queries/escalation-suppression.d.ts.map +1 -1
  194. package/dist/queries/escalation-suppression.js +1 -1
  195. package/dist/queries/escalation-suppression.js.map +1 -1
  196. package/dist/queries/escalations.d.ts +343 -0
  197. package/dist/queries/escalations.d.ts.map +1 -0
  198. package/dist/queries/escalations.js +493 -0
  199. package/dist/queries/escalations.js.map +1 -0
  200. package/dist/queries/focus-discovery.d.ts +204 -0
  201. package/dist/queries/focus-discovery.d.ts.map +1 -0
  202. package/dist/queries/focus-discovery.js +92 -0
  203. package/dist/queries/focus-discovery.js.map +1 -0
  204. package/dist/queries/index.d.ts +4 -2
  205. package/dist/queries/index.d.ts.map +1 -1
  206. package/dist/queries/index.js +4 -2
  207. package/dist/queries/index.js.map +1 -1
  208. package/dist/queries/issues.d.ts +8 -436
  209. package/dist/queries/issues.d.ts.map +1 -1
  210. package/dist/queries/issues.js +7 -615
  211. package/dist/queries/issues.js.map +1 -1
  212. package/dist/resolvers/agent-escalations.d.ts +20 -0
  213. package/dist/resolvers/agent-escalations.d.ts.map +1 -1
  214. package/dist/resolvers/agent-escalations.js +40 -10
  215. package/dist/resolvers/agent-escalations.js.map +1 -1
  216. package/dist/resolvers/index.d.ts +2 -2
  217. package/dist/resolvers/index.d.ts.map +1 -1
  218. package/dist/resolvers/index.js +1 -1
  219. package/dist/resolvers/index.js.map +1 -1
  220. package/dist/resolvers/shared/git-diff.d.ts +99 -18
  221. package/dist/resolvers/shared/git-diff.d.ts.map +1 -1
  222. package/dist/resolvers/shared/git-diff.js +98 -27
  223. package/dist/resolvers/shared/git-diff.js.map +1 -1
  224. package/dist/shell/control-state.d.ts +1 -1
  225. package/dist/shell/control-state.d.ts.map +1 -1
  226. package/dist/shell/control-state.js +1 -1
  227. package/dist/shell/control-state.js.map +1 -1
  228. package/dist/shell/crash-recovery.d.ts +1 -1
  229. package/dist/shell/crash-recovery.d.ts.map +1 -1
  230. package/dist/shell/crash-recovery.js +2 -2
  231. package/dist/shell/crash-recovery.js.map +1 -1
  232. package/dist/shell/install-probe-tick.d.ts +1 -1
  233. package/dist/shell/install-probe-tick.d.ts.map +1 -1
  234. package/dist/shell/install-probe-tick.js +1 -1
  235. package/dist/shell/install-probe-tick.js.map +1 -1
  236. package/dist/shell/unified-shell.js +1 -1
  237. package/dist/shell/unified-shell.js.map +1 -1
  238. package/dist/spawn/model-routing.d.ts +1 -1
  239. package/dist/spawn/model-routing.d.ts.map +1 -1
  240. package/dist/spawn/model-routing.js +1 -1
  241. package/dist/spawn/model-routing.js.map +1 -1
  242. package/dist/spawn/prompt-assembly.d.ts +1 -1
  243. package/dist/spawn/prompt-assembly.d.ts.map +1 -1
  244. package/dist/spawn/prompt-assembly.js +1 -1
  245. package/dist/spawn/prompt-assembly.js.map +1 -1
  246. package/dist/spawn/spawn-decision.js +1 -1
  247. package/dist/spawn/spawn-decision.js.map +1 -1
  248. package/dist/spawn/worktree-session.d.ts +1 -1
  249. package/dist/spawn/worktree-session.d.ts.map +1 -1
  250. package/dist/spawn/worktree-session.js +1 -1
  251. package/dist/spawn/worktree-session.js.map +1 -1
  252. package/dist/templates/claude-md.d.ts +1 -1
  253. package/dist/templates/claude-md.d.ts.map +1 -1
  254. package/dist/templates/claude-md.js +12 -0
  255. package/dist/templates/claude-md.js.map +1 -1
  256. package/dist/verify/verification-default-on.d.ts +1 -1
  257. package/dist/verify/verification-default-on.d.ts.map +1 -1
  258. package/dist/verify/verification-deps.d.ts +1 -1
  259. package/dist/verify/verification-deps.d.ts.map +1 -1
  260. package/dist/verify/verification-engine.d.ts.map +1 -1
  261. package/dist/verify/verification-engine.js +20 -12
  262. package/dist/verify/verification-engine.js.map +1 -1
  263. package/dist/worktree/provision.js +1 -1
  264. package/dist/worktree/provision.js.map +1 -1
  265. package/dist/worktree/scratch-guard.d.ts.map +1 -1
  266. package/dist/worktree/scratch-guard.js +23 -2
  267. package/dist/worktree/scratch-guard.js.map +1 -1
  268. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"listener.d.ts","sourceRoot":"","sources":["../../src/focus/listener.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAa,MAAM,mBAAmB,CAAC;AAkBjE,OAAO,EAAuB,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvE,OAAO,EAAE,YAAY,EAAE,CAAC;AAiCxB;;;;;GAKG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CAEhD;AAsCD;;GAEG;AACH,wBAAsB,YAAY,CAAC,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAyE5E;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAsH5C;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAO3C;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,gBAAgB,GACxB,OAAO,GACP,mBAAmB,GACnB,kBAAkB,GAClB,eAAe,GACf,eAAe,CAAC;AAEpB,MAAM,WAAW,cAAc;IAC7B,2EAA2E;IAC3E,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,OAAO,CAAC;IACvB,eAAe,EAAE,OAAO,CAAC;IACzB,0DAA0D;IAC1D,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,gBAAgB,CAMvE;AAED;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAgLzD;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,0BAA0B,IAAI,OAAO,CAAC,IAAI,CAAC,CAgOhE;AAED;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAmIxD;AAED;;GAEG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAMnD;AAED;;GAEG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAUlD"}
1
+ {"version":3,"file":"listener.d.ts","sourceRoot":"","sources":["../../src/focus/listener.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAiBtD,OAAO,EAAuB,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvE,OAAO,EAAE,YAAY,EAAE,CAAC;AAuCxB;;GAEG;AACH,wBAAsB,YAAY,CAAC,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAyE5E;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAsH5C;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAO3C;AAGD;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAmIxD;AAED;;GAEG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAMnD;AAED;;GAEG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAUlD"}
@@ -7,15 +7,14 @@
7
7
  * single Agent Team per focus that manages all deliveries.
8
8
  */
9
9
  import { getAgentRoles } from '../queries/roles.js';
10
- import { checkDeliveryAutoAdvance, checkVerifyDeliveriesForOpenIssues, checkDeliveryWorkStart } from './listener-auto-advance.js';
10
+ import { checkDeliveryAutoAdvance, checkDeliveryWorkStart } from './listener-auto-advance.js';
11
+ import { checkVerifyDeliveriesForOpenIssues } from './verify-open-issues.js';
11
12
  import { checkPausedDeliveriesForRevival, buildDefaultPausedRevivalDeps } from '../delivery/paused-revival.js';
12
13
  import { runReconcilerShadow } from '../delivery/delivery-reconciler-shadow.js';
13
- import { spawnFocusTeam, hasActiveTeam, getActiveTeamCount, getActiveTeams, checkAndMergeCompletedDeliveries } from './focus-executor.js';
14
- import { applyRespawnGuard } from './focus-respawn-guard.js';
14
+ import { getActiveTeams, checkAndMergeCompletedDeliveries } from './focus-executor.js';
15
15
  import { checkResolvedMergeConflicts } from '../merge/focus-merge.js';
16
16
  import { runCascadeChecks } from '../completion/state-cascade.js';
17
17
  import { runStageLifecycleTransitions } from '../focus-stage-lifecycle/index.js';
18
- import { recordActivity } from '../shell/heartbeat.js';
19
18
  import { safeInterval, withRetry } from '@telora/daemon-core';
20
19
  import { getActiveSessions, updateSession } from '../queries/sessions.js';
21
20
  import { ensureFocusWorktrees, checkForDeactivatedFocuses } from './focus-lifecycle.js';
@@ -26,77 +25,40 @@ import { isDaemonShuttingDown } from '../shell/shutdown-state.js';
26
25
  import { registerPollTrigger, triggerCheck } from '../poll-trigger.js';
27
26
  // Re-exported for existing importers; the live trigger is registered below.
28
27
  export { triggerCheck };
29
- import { configForProduct, findProduct } from '../shell/config.js';
28
+ import { configForProduct } from '../shell/config.js';
30
29
  import { checkIncomingRequests } from '../nexus/index.js';
31
30
  import { checkIntakePendingRequests } from '../nexus/index.js';
32
31
  import { checkResolvedRequests } from '../nexus/index.js';
33
32
  import { checkEndpointClearance } from '../nexus/index.js';
34
- import { checkDirectives, seedLastProcessedStage, getPendingSpawnFocusIds, getPendingSpawnDirective, clearPendingSpawnDirective, shouldReapPendingSpawnDirective } from '../directive/directive-executor.js';
35
- import { decideNullRoleSpawn, resolveReviewerFallbackRole, shouldWarnNullRoleSkip, DEFAULT_REVIEWER_ROLE_NAME } from '../spawn/review-role-fallback.js';
36
- import { getActiveFocuses, getReadyFocuses, clearAnswerRearm } from '../queries/deliveries.js';
37
- import { isAuditPhaseFocusRow } from '../audit/audit-shape.js';
38
- import { isIntakeDraining } from '../shell/intake-control.js';
33
+ import { checkDirectives, seedLastProcessedStage } from '../directive/directive-executor.js';
34
+ import { getActiveFocuses } from '../queries/focus-discovery.js';
39
35
  import { checkDiscoveryRequests } from '../host-control/discovery-poll.js';
40
- // Track focuses that have been logged as idle (no actionable deliveries).
41
- // Only log on state change: first skip after an active session or after startup.
42
- const idleLoggedFocuses = new Set();
43
- // Track focuses already warned this blocked episode for a null-role coding
44
- // off-state pending directive (Play/queue toggle cleared, no operator stop).
45
- // Without this, the "no assigned_agent_role_id; skipping spawn (coding off-state)"
46
- // warning fires every 30s poll for as long as the directive sits queued. We warn
47
- // once on entry and suppress until the focus resumes (role assigned, directive
48
- // cleared, or focus leaves the active set), at which point it is removed so a
49
- // later re-entry warns again. Operator-stop suppression does NOT use this set --
50
- // it drops the directive, so it is inherently once-per-episode.
51
- const nullRoleSkipWarnedFocuses = new Set();
52
- // Depth of the ready-focus queue as of the most recent completed poll. Read by
53
- // the auto-updater's batch-safe idle predicate (auto-update-idle.ts): a drained
54
- // queue (0) plus no active team is a safe restart seam. Stale-but-recent is fine
55
- // -- the idle gate is debounced well past the poll interval.
56
- let lastReadyFocusCount = 0;
57
- /**
58
- * Number of focuses with actionable (queued) work as of the last poll. Zero
59
- * means the focus queue is drained. Used by the batch-safe idle predicate so a
60
- * transient between-delivery gap (which still has queued focuses) does not read
61
- * as idle.
62
- */
63
- export function getReadyFocusQueueDepth() {
64
- return lastReadyFocusCount;
65
- }
66
- let config = null;
67
- const roles = new Map();
68
- let pollInterval = null;
69
- let pollState = 'idle';
70
- // Polling interval in milliseconds (default: 30 seconds)
71
- const POLL_INTERVAL_MS = 30000;
72
- // Roles refresh: re-fetch from DB periodically to pick up changes
73
- const ROLES_REFRESH_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
74
- let lastRolesRefreshAt = 0;
75
- // Health check backoff: track consecutive failures to avoid hammering a broken API
76
- let healthCheckConsecutiveFailures = 0;
77
- const HEALTH_CHECK_MAX_SKIP_CYCLES = 10; // max backoff = 10 * 30s = 5 minutes
78
36
  // Spawn cooldown imported from dedicated module (avoids circular deps)
79
- import { isInSpawnCooldown, isRespawnCycleTerminated, hydrateSpawnCooldownEntry } from '../spawn/spawn-cooldown.js';
37
+ import { hydrateSpawnCooldownEntry } from '../spawn/spawn-cooldown.js';
80
38
  import { hydrateDeliveryAdvancementState } from '../completion/delivery-advancement-guard.js';
81
39
  import { hydrateBelowCoverageDriftCount } from '../completion/coverage-drift-state.js';
82
- import { shouldSkipPendingSpawn, recordPendingSpawnAttempt } from '../spawn/index.js';
83
- import { fileRespawnCycleExhaustedEscalation } from '../spawn/respawn-cycle-escalation.js';
84
40
  import { createLogger } from '../shell/log.js';
85
41
  import { runWithTickDeadline, DEFAULT_TICK_DEADLINE_MS } from './tick-deadline.js';
42
+ // Poll-loop singletons live in listener-state.ts (the leaf); the spawn-processing
43
+ // pass in listener-spawn.ts. Reads use the live bindings; reassignments go
44
+ // through the setters. Both this module and listener-spawn.ts import the state
45
+ // leaf -- no cycle.
46
+ import { config, roles, pollInterval, healthCheckConsecutiveFailures, POLL_INTERVAL_MS, HEALTH_CHECK_MAX_SKIP_CYCLES, setConfig, setPollInterval, setPollState, setHealthCheckConsecutiveFailures, setLastRolesRefreshAt, } from './listener-state.js';
47
+ import { processReadyFocuses, processPendingSpawnFocuses } from './listener-spawn.js';
86
48
  const log = createLogger({ module: 'listener' });
87
49
  /**
88
50
  * Initialize the listener with configuration.
89
51
  */
90
52
  export async function initListener(daemonConfig) {
91
- config = daemonConfig;
53
+ setConfig(daemonConfig);
92
54
  // Load all roles
93
55
  try {
94
- const roleList = await getAgentRoles(config.organizationId);
56
+ const roleList = await getAgentRoles(daemonConfig.organizationId);
95
57
  roles.clear();
96
58
  for (const role of roleList) {
97
59
  roles.set(role.id, role);
98
60
  }
99
- lastRolesRefreshAt = Date.now();
61
+ setLastRolesRefreshAt(Date.now());
100
62
  log.info(`Loaded ${roles.size} agent roles`);
101
63
  // Seed directive executor with current stage IDs to prevent
102
64
  // re-firing directives for stages already processed before restart.
@@ -172,7 +134,7 @@ export function startDeliveryListener() {
172
134
  // Start polling with error boundary — check for work and main advancement
173
135
  // Per-product functions are called with product-scoped configs so that
174
136
  // config.productId and config.repoPath resolve to the correct product.
175
- pollInterval = safeInterval(async () => {
137
+ setPollInterval(safeInterval(async () => {
176
138
  // Deadline watchdog (TEL-3): surface a poll tick that stops making progress.
177
139
  // safeInterval has no deadline and recordActivity() (heartbeat) is emitted
178
140
  // inside this body, so a hung tick is silent AND stops the heartbeat -- from
@@ -269,7 +231,7 @@ export function startDeliveryListener() {
269
231
  // review phase it already fetched) rather than fanning out a second pass.
270
232
  await checkSessionHealth();
271
233
  });
272
- }, POLL_INTERVAL_MS, 'focus-poll', { skipIfRunning: true });
234
+ }, POLL_INTERVAL_MS, 'focus-poll', { skipIfRunning: true }));
273
235
  log.info(`Started focus polling (every ${POLL_INTERVAL_MS / 1000}s)`);
274
236
  }
275
237
  /**
@@ -283,396 +245,6 @@ export function startSessionListener() {
283
245
  // In polling mode, session changes are handled by checking ready focuses
284
246
  log.info('Session monitoring via polling (included in focus polling)');
285
247
  }
286
- export function decideFocusSpawn(gate) {
287
- if (!gate.hasRole)
288
- return 'skip_unknown_role';
289
- if (gate.hasActiveTeam)
290
- return 'skip_active_team';
291
- if (gate.inSpawnCooldown)
292
- return 'skip_cooldown';
293
- if (gate.activeTeamCount >= gate.maxTotalSessions)
294
- return 'stop_capacity';
295
- return 'spawn';
296
- }
297
- /**
298
- * Process any focuses ready for agent work.
299
- *
300
- * For each ready focus without an active team, spawns a single
301
- * Agent Team that manages all deliveries within that focus.
302
- * Also detects focuses that have been deactivated and shuts down
303
- * their teams.
304
- */
305
- export async function processReadyFocuses() {
306
- if (!config)
307
- return;
308
- // Prevent concurrent processing, and skip cycles during health check backoff
309
- if (pollState === 'processing' || pollState === 'backing_off') {
310
- return;
311
- }
312
- pollState = 'processing';
313
- try {
314
- // Periodically refresh agent roles to pick up changes
315
- if (Date.now() - lastRolesRefreshAt > ROLES_REFRESH_INTERVAL_MS) {
316
- try {
317
- const roleList = await getAgentRoles(config.organizationId);
318
- roles.clear();
319
- for (const role of roleList) {
320
- roles.set(role.id, role);
321
- }
322
- lastRolesRefreshAt = Date.now();
323
- log.info(`Refreshed agent roles (${roles.size} roles loaded)`);
324
- }
325
- catch (err) {
326
- log.warn(`Failed to refresh agent roles: ${err.message}`);
327
- }
328
- }
329
- // Record activity: proves the poll ran even if no work was found
330
- recordActivity();
331
- // Snapshot active teams before processing (for idle detection)
332
- const previouslyActiveIds = new Set(getActiveTeams().keys());
333
- // Deactivation detection is now handled by checkForDeactivatedFocuses()
334
- // in the poll loop (called before processReadyFocuses).
335
- // Get ready focuses across all configured products
336
- const readyFocuses = [];
337
- for (const product of config.products) {
338
- try {
339
- const focuses = await getReadyFocuses(config.organizationId, product.id);
340
- readyFocuses.push(...focuses);
341
- }
342
- catch (err) {
343
- log.warn(`Failed to fetch ready focuses for product ${product.id}: ${err.message}`);
344
- }
345
- }
346
- // Publish the queue depth for the auto-updater's batch-safe idle predicate.
347
- // Set before any early-return below so a drained queue is observable.
348
- lastReadyFocusCount = readyFocuses.length;
349
- // Track which focuses are ready this cycle (for idle logging)
350
- const readyFocusIds = new Set(readyFocuses.map(s => s.focus_id));
351
- // Idle queue logging: detect focuses that were previously active
352
- // but now have no ready deliveries. Only log on state change.
353
- for (const focusId of previouslyActiveIds) {
354
- if (!readyFocusIds.has(focusId) && !hasActiveTeam(focusId) && !idleLoggedFocuses.has(focusId)) {
355
- const nextPollSec = POLL_INTERVAL_MS / 1000;
356
- log.info(`Focus queue idle: "${focusId}" -- no actionable deliveries, next poll in ${nextPollSec}s`);
357
- idleLoggedFocuses.add(focusId);
358
- }
359
- }
360
- if (readyFocuses.length === 0) {
361
- return;
362
- }
363
- // Intake drain (auto-update delivery C): a pending update has been stale
364
- // past threshold while the daemon stays busy, so we stop accepting new focus
365
- // teams to let the current batch become the safe seam. Active teams continue
366
- // to completion; we simply do not spawn more. The held queue does not block
367
- // the batch boundary because isBatchSafeIdle honors isIntakeDraining().
368
- if (isIntakeDraining()) {
369
- log.info(`[listener] Intake drained for pending update -- holding ${readyFocuses.length} ready focus(es); no new teams will spawn`);
370
- return;
371
- }
372
- const productLabel = config.products.length > 1
373
- ? ` across ${config.products.length} product(s)`
374
- : '';
375
- log.info(`Found ${readyFocuses.length} ready focuses${productLabel}`);
376
- for (const focus of readyFocuses) {
377
- const role = roles.get(focus.assigned_agent_role_id);
378
- // Resolve the per-focus spawn-gate decision (pure; see decideFocusSpawn).
379
- // The capacity input is the LIVE team count sampled this iteration, so a
380
- // spawn earlier in this loop is reflected for the next focus.
381
- const action = decideFocusSpawn({
382
- hasRole: role !== undefined,
383
- hasActiveTeam: hasActiveTeam(focus.focus_id),
384
- inSpawnCooldown: isInSpawnCooldown(focus.focus_id),
385
- activeTeamCount: getActiveTeamCount(),
386
- maxTotalSessions: config.maxTotalSessions,
387
- });
388
- if (action === 'skip_unknown_role') {
389
- log.error(`Unknown agent role: ${focus.assigned_agent_role_id}`);
390
- continue;
391
- }
392
- if (action === 'skip_active_team') {
393
- // Focus is active -- clear idle state so we re-log if it goes idle again
394
- idleLoggedFocuses.delete(focus.focus_id);
395
- // A re-arm flag may have landed while a team is up. Do NOT clear it here:
396
- // the answer is consumed only at (re)spawn time via the escalation.answers
397
- // resolver, and a still-live team does not re-run assembly mid-session
398
- // (the wake-message handoff does not carry answers). Leaving the flag set
399
- // lets the force-spawn deliver the answer once this team exits; clearing
400
- // it now would strand the answer. The clear happens on the force-spawn
401
- // below (one answer -> one re-spawn).
402
- continue;
403
- }
404
- // Skip focuses that were recently torn down (spawn cooldown).
405
- if (action === 'skip_cooldown') {
406
- continue;
407
- }
408
- // Total capacity reached -- stop the whole loop, not just this focus.
409
- if (action === 'stop_capacity') {
410
- log.info(`Total session limit reached, stopping`);
411
- break;
412
- }
413
- // action === 'spawn'. role is guaranteed defined (hasRole was true);
414
- // this guard narrows the type and is otherwise unreachable.
415
- if (!role)
416
- continue;
417
- const isRearm = focus.answer_rearm_requested_at != null;
418
- // Clear idle state before spawn attempt -- if spawn succeeds,
419
- // the focus is active. If it skips (no actionable deliveries),
420
- // spawnFocusTeam logs the skip and we mark it idle here.
421
- const wasIdle = idleLoggedFocuses.has(focus.focus_id);
422
- idleLoggedFocuses.delete(focus.focus_id);
423
- // Use product-scoped config so the team uses the correct repoPath
424
- const productEntry = focus.product_id ? findProduct(config, focus.product_id) : undefined;
425
- const spawnConfig = productEntry ? configForProduct(config, productEntry) : config;
426
- // Spawn a focus team (one team per focus). forceSpawn lets a re-armed
427
- // focus spawn past findActionableDeliveries' 'skip' to consume the
428
- // pending escalation answer.
429
- // Audit-phase focus (explicit read_only=true OR the second structural
430
- // signal: audit_origin && not-yet-ratified, TEL-3) spawns read-only, so a
431
- // flag-absent audit focus is not driven into scoping/write/commit ahead of
432
- // ratification (2f253da6). See isAuditPhaseFocusRow.
433
- await spawnFocusTeam({ config: spawnConfig, focusId: focus.focus_id, focusName: focus.focus_name, role, pipelineConfig: focus.pipeline_config, readOnly: isAuditPhaseFocusRow(focus), lastClaudeSessionId: focus.last_claude_session_id, claudeSessionIds: focus.claude_session_ids, engineOverride: focus.engine_override, localModelOverride: focus.local_model_override, orgDefaultEngine: focus.org_default_engine, pinnedBranch: focus.branch_name, forceSpawn: isRearm });
434
- // Clear the re-arm flag only once a team is actually live to consume the
435
- // answer: one answer -> one re-spawn. If the force-spawn did NOT produce an
436
- // active team (worktree/governor failure, etc.), leave the flag set so a
437
- // later poll retries delivery rather than stranding the answer. (Cooldown
438
- // and capacity exit above without reaching here, so they also keep the
439
- // flag.) Done before the idle check below so a consumed re-arm is not
440
- // re-surfaced next poll.
441
- if (isRearm && hasActiveTeam(focus.focus_id)) {
442
- await clearAnswerRearm(focus.focus_id).catch((err) => log.warn(`Failed to clear answer re-arm for "${focus.focus_name}": ${err.message}`));
443
- }
444
- // If the spawn was skipped (no active team after spawn), mark as idle
445
- if (!hasActiveTeam(focus.focus_id) && !wasIdle) {
446
- const nextPollSec = POLL_INTERVAL_MS / 1000;
447
- log.info(`Focus queue idle: "${focus.focus_name}" -- no actionable deliveries after pre-spawn check, next poll in ${nextPollSec}s`);
448
- idleLoggedFocuses.add(focus.focus_id);
449
- }
450
- }
451
- }
452
- catch (error) {
453
- log.error('Error processing ready focuses:', error);
454
- }
455
- finally {
456
- pollState = healthCheckConsecutiveFailures > 0 ? 'backing_off' : 'idle';
457
- }
458
- }
459
- /**
460
- * Spawn teams for focuses that have a pending spawn directive but were
461
- * not surfaced by getReadyFocuses.
462
- *
463
- * getReadyFocuses (daemon-focus-handlers.ts) only returns focuses with
464
- * queued/coding deliveries (or zero deliveries). A review-stage spawn
465
- * directive on a focus whose deliveries are all in verify never makes
466
- * it to processReadyFocuses -- the pending directive sits forever and
467
- * the team never starts. This function bridges that gap: scan the
468
- * pending-spawn map directly and spawn for any focus that's not
469
- * already running.
470
- */
471
- export async function processPendingSpawnFocuses() {
472
- if (!config)
473
- return;
474
- if (isDaemonShuttingDown())
475
- return;
476
- const pendingIds = getPendingSpawnFocusIds().filter(id => !hasActiveTeam(id));
477
- if (pendingIds.length === 0)
478
- return;
479
- // Fetch focus details for all configured products. getActiveFocuses
480
- // returns the metadata spawnFocusTeam needs.
481
- const focusDetailsById = new Map();
482
- // Whether ANY product's getActiveFocuses read failed this tick. A fetch failure
483
- // makes the active set incomplete, so a pending focus can look absent purely
484
- // because its product's read failed -- absence is only trustworthy when this is
485
- // false. Gates the zombie-directive reap below.
486
- let anyFetchFailed = false;
487
- for (const product of config.products) {
488
- try {
489
- const focuses = await getActiveFocuses(config.organizationId, product.id);
490
- for (const f of focuses) {
491
- if (pendingIds.includes(f.focus_id)) {
492
- focusDetailsById.set(f.focus_id, f);
493
- }
494
- }
495
- }
496
- catch (err) {
497
- anyFetchFailed = true;
498
- log.warn(`[listener] processPendingSpawnFocuses: failed to fetch active focuses for product ${product.id}: ${err.message}`);
499
- }
500
- }
501
- for (const focusId of pendingIds) {
502
- const focus = focusDetailsById.get(focusId);
503
- if (!focus) {
504
- // Focus is absent from the active set. If the read was complete this tick
505
- // (no fetch failure), the focus has genuinely left the active set -- its
506
- // directive is a zombie: reap it once instead of retrying forever. On a
507
- // fetch-failure tick the absence is untrustworthy, so keep retrying.
508
- if (shouldReapPendingSpawnDirective({ anyFetchFailed, focusPresentInActiveSet: false })) {
509
- clearPendingSpawnDirective(focusId);
510
- // Focus left the active set -- the blocked episode is over. Clear its
511
- // once-per-episode warn marker so a future re-entry warns again.
512
- nullRoleSkipWarnedFocuses.delete(focusId);
513
- log.info(`[listener] Dropping stale spawn directive for focus ${focusId.slice(0, 8)}: ` +
514
- `confirmed absent from every product's active set (focus completed/deactivated).`);
515
- }
516
- else {
517
- log.warn(`[listener] Pending spawn directive for focus ${focusId.slice(0, 8)} but focus not found in active set; ` +
518
- `active-set read incomplete this tick (fetch failure) -- retrying next poll.`);
519
- }
520
- continue;
521
- }
522
- let role;
523
- if (!focus.assigned_agent_role_id) {
524
- // A null assigned_agent_role_id now means ONE OF TWO things, and they are
525
- // NOT interchangeable:
526
- // - operator_stopped_at set => an operator pulled the Play toggle to
527
- // halt the focus. Authoritative: it wins over the review fallback.
528
- // - operator_stopped_at null => auto_terminate_focus_team() cleared the
529
- // role at completion (lifecycle). Here the review fallback applies:
530
- // a review is a SECOND team that must spawn AFTER completion; a coding
531
- // directive is the intended off-state and must NOT auto-spawn.
532
- // decideNullRoleSpawn disambiguates via operatorStopped; the listener then
533
- // acts on the decision (drop the directive on a stop, throttle the
534
- // off-state warning, or spawn the lifecycle review fallback).
535
- const operatorStopped = !!focus.operator_stopped_at;
536
- const pending = getPendingSpawnDirective(focus.focus_id);
537
- const decision = decideNullRoleSpawn({
538
- sessionType: pending?.sessionType,
539
- fallbackRole: resolveReviewerFallbackRole(roles.values()),
540
- operatorStopped,
541
- });
542
- if (decision.action === 'skip') {
543
- if (operatorStopped) {
544
- // Operator STOP wins over the review fallback. Drop any directive
545
- // queued while stopped so it does not silently survive to resume and
546
- // fire the path the stop was meant to prevent. Dropping also removes
547
- // the focus from the pending set, so this suppression is logged once
548
- // per focus (per stop), never once per poll.
549
- if (pending)
550
- clearPendingSpawnDirective(focus.focus_id);
551
- nullRoleSkipWarnedFocuses.delete(focus.focus_id);
552
- log.info(`[listener] Focus ${focusId.slice(0, 8)} ("${focus.focus_name}") is held by an operator stop; ` +
553
- `suppressing the queued ${pending?.sessionType ?? 'spawn'} directive (dropped, not carried to resume).`);
554
- continue;
555
- }
556
- // Coding off-state (Play/queue toggle cleared, no operator stop): the
557
- // directive legitimately stays queued. Bound the warning to once per
558
- // blocked episode so a stopped focus does not spin-log every 30s poll.
559
- if (shouldWarnNullRoleSkip(nullRoleSkipWarnedFocuses, focus.focus_id)) {
560
- log.warn(`[listener] Pending spawn directive for focus ${focusId.slice(0, 8)} ("${focus.focus_name}") but no assigned_agent_role_id; ` +
561
- `skipping spawn (coding off-state). Suppressing further per-poll warnings until it resumes.`);
562
- }
563
- continue;
564
- }
565
- // Past the skip paths: this is a genuine lifecycle clear (operatorStopped
566
- // is false here). The episode is over either way, so clear the marker.
567
- nullRoleSkipWarnedFocuses.delete(focus.focus_id);
568
- if (decision.action === 'clear') {
569
- // Review strand with no resolvable fallback role: drop the directive so
570
- // the stranded-review re-fire suppression (directive-executor) cannot
571
- // spin-log forever. review_window_stalled remains the escalation path,
572
- // and now fires promptly rather than after 6h of churn.
573
- clearPendingSpawnDirective(focus.focus_id);
574
- log.warn(`[listener] Stranded review directive for focus ${focusId.slice(0, 8)} ("${focus.focus_name}") has a null assigned_agent_role_id ` +
575
- `and no seeded '${DEFAULT_REVIEWER_ROLE_NAME}' fallback role resolved -- dropping the directive so it stops re-firing; ` +
576
- `review_window_stalled will escalate.`);
577
- continue;
578
- }
579
- role = decision.role;
580
- // Reachable only when operatorStopped is false (an operator stop takes the
581
- // skip path above), so "(coding team completed)" is a true provenance
582
- // claim here rather than an assumption stamped on every null-role spawn.
583
- log.info(`[listener] Review directive for focus ${focusId.slice(0, 8)} ("${focus.focus_name}") has a null assigned_agent_role_id ` +
584
- `(coding team completed); spawning review with fallback role "${role.name}".`);
585
- }
586
- else {
587
- // Focus has a role again -- any prior null-role blocked episode is over.
588
- nullRoleSkipWarnedFocuses.delete(focus.focus_id);
589
- role = roles.get(focus.assigned_agent_role_id);
590
- if (!role) {
591
- log.warn(`[listener] Pending spawn directive for focus ${focusId.slice(0, 8)} ("${focus.focus_name}") references unknown role ${focus.assigned_agent_role_id}; ` +
592
- `skipping spawn.`);
593
- continue;
594
- }
595
- }
596
- // Park focuses with an unhealed team_failure escalation BEFORE attempting
597
- // the spawn. spawnFocusTeam applies the same guard, but it returns after
598
- // the guard fires without consuming the pending directive -- so the
599
- // directive stays queued and this loop re-attempts (and the guard re-logs)
600
- // every poll. Checking here skips the misleading "Spawning team" log and
601
- // the wasted spawn call; the directive intentionally stays queued so the
602
- // (re)review resumes once a human resolves the escalation. The guard
603
- // throttles its own log to once per blocked episode.
604
- if (await applyRespawnGuard(focus.focus_id, focus.focus_name)) {
605
- continue;
606
- }
607
- // Spawn cooldown: this path spawns teams for focuses that getReadyFocuses
608
- // does NOT surface -- focuses with no actionable/queued deliveries (e.g. a
609
- // reviewing-stage spawn over a focus whose deliveries are all in verify).
610
- // Such a team idle-exits if its directive can't make progress; without a
611
- // gate the directive re-fires every poll and the daemon respawns an
612
- // immediately-idle team every cycle (the reinforcing auto-review/idle-spawn
613
- // loop). See pending-spawn-guard.ts.
614
- // Terminated respawn cycle: the spawn/teardown loop for this focus has
615
- // exhausted its total-duration/count bounds (spawn-cooldown.ts). Respawns are
616
- // halted permanently (isInSpawnCooldown returns true indefinitely), so the
617
- // spawn is skipped either way -- but a silent halt is a strand. File the
618
- // one-per-focus respawn_cycle_exhausted escalation (idempotent, so filing on
619
- // each poll while terminated yields a single open row) so the terminated
620
- // cycle is a legible state a human can see and clear.
621
- if (isRespawnCycleTerminated(focus.focus_id)) {
622
- await fileRespawnCycleExhaustedEscalation({
623
- organizationId: config.organizationId,
624
- focusId: focus.focus_id,
625
- focusName: focus.focus_name,
626
- });
627
- continue;
628
- }
629
- if (shouldSkipPendingSpawn(focus.focus_id)) {
630
- continue;
631
- }
632
- if (getActiveTeamCount() >= config.maxTotalSessions) {
633
- log.info(`Total session limit reached, deferring pending spawn for "${focus.focus_name}"`);
634
- break;
635
- }
636
- const productEntry = focus.product_id ? findProduct(config, focus.product_id) : undefined;
637
- const spawnConfig = productEntry ? configForProduct(config, productEntry) : config;
638
- log.info(`[listener] Spawning team for focus "${focus.focus_name}" via pending spawn directive ` +
639
- `(no actionable deliveries, but directive is queued)`);
640
- // Audit-phase => read-only spawn (see the processReadyFocuses spawn above).
641
- // The pending-spawn focus comes from getActiveFocuses, which carries
642
- // audit_origin + ratified_at, so the second signal (TEL-3) resolves here too.
643
- await spawnFocusTeam({
644
- config: spawnConfig,
645
- focusId: focus.focus_id,
646
- focusName: focus.focus_name,
647
- role,
648
- pipelineConfig: focus.pipeline_config,
649
- readOnly: isAuditPhaseFocusRow(focus),
650
- lastClaudeSessionId: null,
651
- claudeSessionIds: focus.claude_session_ids,
652
- engineOverride: focus.engine_override,
653
- localModelOverride: focus.local_model_override,
654
- orgDefaultEngine: focus.org_default_engine,
655
- pinnedBranch: focus.branch_name,
656
- });
657
- // Record a teardown so repeated spawns through this no-actionable-work path
658
- // back off exponentially instead of recurring every poll. A team that
659
- // genuinely makes progress moves the focus into a state getReadyFocuses
660
- // surfaces (and clearSpawnCooldown can reset it); a team that idle-exits is
661
- // rate-limited by the growing cooldown. This bounds spawn count on a
662
- // wedged-in-verify focus. See pending-spawn-guard.ts.
663
- //
664
- // ONLY when a team actually spawned (TEL, delivery 5): if spawnFocusTeam
665
- // SKIPPED (e.g. a terminal focus whose deliveries are all terminal now
666
- // returns 'skip', or the pre-spawn guards declined), no team exists to tear
667
- // down, so recording a teardown here would accrue the respawn streak against
668
- // a focus that correctly spawned nothing -- penalising quiet correctness. The
669
- // idle-exit backoff is preserved: a genuine spawn leaves hasActiveTeam true at
670
- // this point (the team runs asynchronously and exits later).
671
- if (hasActiveTeam(focus.focus_id)) {
672
- recordPendingSpawnAttempt(focus.focus_id);
673
- }
674
- }
675
- }
676
248
  /**
677
249
  * Check that every session recorded as 'starting' or 'running' in the DB
678
250
  * has a corresponding active team in memory.
@@ -689,7 +261,7 @@ export async function checkSessionHealth() {
689
261
  if (healthCheckConsecutiveFailures > 0) {
690
262
  // Use the failure count as the number of cycles to skip
691
263
  const skipCycles = Math.min(healthCheckConsecutiveFailures, HEALTH_CHECK_MAX_SKIP_CYCLES);
692
- healthCheckConsecutiveFailures++;
264
+ setHealthCheckConsecutiveFailures(healthCheckConsecutiveFailures + 1);
693
265
  if (healthCheckConsecutiveFailures % skipCycles !== 0) {
694
266
  return; // skip this cycle
695
267
  }
@@ -705,13 +277,13 @@ export async function checkSessionHealth() {
705
277
  // Reset backoff on success
706
278
  if (healthCheckConsecutiveFailures > 0) {
707
279
  log.info(`[health] Session health check recovered after ${healthCheckConsecutiveFailures} failures`);
708
- healthCheckConsecutiveFailures = 0;
709
- pollState = 'idle';
280
+ setHealthCheckConsecutiveFailures(0);
281
+ setPollState('idle');
710
282
  }
711
283
  }
712
284
  catch (err) {
713
- healthCheckConsecutiveFailures++;
714
- pollState = 'backing_off';
285
+ setHealthCheckConsecutiveFailures(healthCheckConsecutiveFailures + 1);
286
+ setPollState('backing_off');
715
287
  log.warn(`[health] Failed to fetch active sessions (failure #${healthCheckConsecutiveFailures}): ${err.message}`);
716
288
  return;
717
289
  }
@@ -796,7 +368,7 @@ export async function checkSessionHealth() {
796
368
  export async function stopListeners() {
797
369
  if (pollInterval) {
798
370
  clearInterval(pollInterval);
799
- pollInterval = null;
371
+ setPollInterval(null);
800
372
  }
801
373
  log.info('Stopped polling');
802
374
  }
@@ -811,7 +383,7 @@ export async function refreshRoles() {
811
383
  for (const role of roleList) {
812
384
  roles.set(role.id, role);
813
385
  }
814
- lastRolesRefreshAt = Date.now();
386
+ setLastRolesRefreshAt(Date.now());
815
387
  log.info(`Refreshed ${roles.size} agent roles`);
816
388
  }
817
389
  /**