@stigmer/runner 3.40.0 → 3.41.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 (278) hide show
  1. package/README.md +3 -1
  2. package/dist/.build-fingerprint +1 -1
  3. package/dist/__test-utils__/execution-record-fixture.d.ts +2 -1
  4. package/dist/__test-utils__/execution-record-fixture.js.map +1 -1
  5. package/dist/activities/call-agent.js +1 -1
  6. package/dist/activities/discover-mcp-server.d.ts +7 -1
  7. package/dist/activities/discover-mcp-server.js +13 -29
  8. package/dist/activities/discover-mcp-server.js.map +1 -1
  9. package/dist/activities/execute-cursor/__test-utils__/fold.d.ts +4 -2
  10. package/dist/activities/execute-cursor/__test-utils__/fold.js +3 -4
  11. package/dist/activities/execute-cursor/__test-utils__/fold.js.map +1 -1
  12. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +1 -0
  13. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
  14. package/dist/activities/execute-cursor/approval-policy.d.ts +1 -29
  15. package/dist/activities/execute-cursor/approval-policy.js +11 -35
  16. package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
  17. package/dist/activities/execute-cursor/boundary-rows.js +4 -4
  18. package/dist/activities/execute-cursor/boundary-rows.js.map +1 -1
  19. package/dist/activities/execute-cursor/translator.d.ts +12 -17
  20. package/dist/activities/execute-cursor/translator.js +13 -30
  21. package/dist/activities/execute-cursor/translator.js.map +1 -1
  22. package/dist/activities/execute-cursor/turn-stream.js +4 -2
  23. package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
  24. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.d.ts +11 -1
  25. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js +22 -3
  26. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js.map +1 -1
  27. package/dist/activities/execute-deep-agent/inline-publisher.d.ts +33 -2
  28. package/dist/activities/execute-deep-agent/inline-publisher.js +33 -7
  29. package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
  30. package/dist/activities/execute-deep-agent/platform-route.d.ts +48 -13
  31. package/dist/activities/execute-deep-agent/platform-route.js +87 -19
  32. package/dist/activities/execute-deep-agent/platform-route.js.map +1 -1
  33. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +70 -35
  34. package/dist/activities/execute-deep-agent/prompt-builder.js +70 -55
  35. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  36. package/dist/activities/execute-deep-agent/streaming-side-effects.d.ts +20 -3
  37. package/dist/activities/execute-deep-agent/streaming-side-effects.js +26 -4
  38. package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
  39. package/dist/activities/execute-deep-agent/translator.d.ts +5 -4
  40. package/dist/activities/execute-deep-agent/translator.js +5 -4
  41. package/dist/activities/execute-deep-agent/translator.js.map +1 -1
  42. package/dist/activities/execute-deep-agent/turn-settle.js +6 -6
  43. package/dist/activities/execute-deep-agent/turn-settle.js.map +1 -1
  44. package/dist/activities/execute-deep-agent/turn-setup.d.ts +18 -13
  45. package/dist/activities/execute-deep-agent/turn-setup.js +42 -36
  46. package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
  47. package/dist/activities/execute-deep-agent/turn-stream.d.ts +10 -6
  48. package/dist/activities/execute-deep-agent/turn-stream.js +6 -16
  49. package/dist/activities/execute-deep-agent/turn-stream.js.map +1 -1
  50. package/dist/activities/execute-deep-agent/turn.d.ts +4 -1
  51. package/dist/activities/execute-deep-agent/turn.js +15 -6
  52. package/dist/activities/execute-deep-agent/turn.js.map +1 -1
  53. package/dist/activities/workflow-event-activities.js +2 -0
  54. package/dist/activities/workflow-event-activities.js.map +1 -1
  55. package/dist/config.d.ts +9 -0
  56. package/dist/config.js +2 -0
  57. package/dist/config.js.map +1 -1
  58. package/dist/exit-backstop.d.ts +64 -0
  59. package/dist/exit-backstop.js +79 -0
  60. package/dist/exit-backstop.js.map +1 -0
  61. package/dist/harness/terminal-table.d.ts +12 -5
  62. package/dist/harness/terminal-table.js +13 -13
  63. package/dist/harness/terminal-table.js.map +1 -1
  64. package/dist/harness/transcript/builder.d.ts +21 -0
  65. package/dist/harness/transcript/builder.js +53 -31
  66. package/dist/harness/transcript/builder.js.map +1 -1
  67. package/dist/harness/transcript/events.d.ts +22 -26
  68. package/dist/harness/transcript/events.js +6 -5
  69. package/dist/harness/transcript/events.js.map +1 -1
  70. package/dist/harness/turn-context.d.ts +3 -1
  71. package/dist/harness/turn-context.js +5 -3
  72. package/dist/harness/turn-context.js.map +1 -1
  73. package/dist/main.js +10 -4
  74. package/dist/main.js.map +1 -1
  75. package/dist/middleware/cost-advisory.d.ts +6 -3
  76. package/dist/middleware/cost-advisory.js +6 -3
  77. package/dist/middleware/cost-advisory.js.map +1 -1
  78. package/dist/middleware/execution-budget.d.ts +22 -7
  79. package/dist/middleware/execution-budget.js +57 -35
  80. package/dist/middleware/execution-budget.js.map +1 -1
  81. package/dist/middleware/index.d.ts +10 -7
  82. package/dist/middleware/index.js +10 -7
  83. package/dist/middleware/index.js.map +1 -1
  84. package/dist/middleware/types.d.ts +6 -1
  85. package/dist/runner-manager.js +3 -2
  86. package/dist/runner-manager.js.map +1 -1
  87. package/dist/runner.d.ts +25 -0
  88. package/dist/runner.js +41 -14
  89. package/dist/runner.js.map +1 -1
  90. package/dist/shared/approval-policy.d.ts +23 -13
  91. package/dist/shared/approval-policy.js +47 -14
  92. package/dist/shared/approval-policy.js.map +1 -1
  93. package/dist/shared/caller-identity.d.ts +4 -5
  94. package/dist/shared/caller-identity.js +4 -5
  95. package/dist/shared/caller-identity.js.map +1 -1
  96. package/dist/shared/channel-attachment.js +3 -2
  97. package/dist/shared/channel-attachment.js.map +1 -1
  98. package/dist/shared/connect-backfill.d.ts +2 -1
  99. package/dist/shared/connect-backfill.js +2 -2
  100. package/dist/shared/connect-backfill.js.map +1 -1
  101. package/dist/shared/mcp-resolver.d.ts +10 -1
  102. package/dist/shared/mcp-resolver.js +11 -2
  103. package/dist/shared/mcp-resolver.js.map +1 -1
  104. package/dist/shared/memory-attachment.js +3 -2
  105. package/dist/shared/memory-attachment.js.map +1 -1
  106. package/dist/shared/model-error.d.ts +25 -3
  107. package/dist/shared/model-error.js +78 -10
  108. package/dist/shared/model-error.js.map +1 -1
  109. package/dist/shared/platform-server-address.d.ts +71 -0
  110. package/dist/shared/platform-server-address.js +103 -0
  111. package/dist/shared/platform-server-address.js.map +1 -0
  112. package/dist/shared/prompt-sections.d.ts +6 -28
  113. package/dist/shared/prompt-sections.js +6 -35
  114. package/dist/shared/prompt-sections.js.map +1 -1
  115. package/dist/shared/skill-resolver.d.ts +2 -3
  116. package/dist/shared/skill-resolver.js +2 -3
  117. package/dist/shared/skill-resolver.js.map +1 -1
  118. package/dist/shared/synthesized-attachment.d.ts +0 -5
  119. package/dist/shared/synthesized-attachment.js +0 -12
  120. package/dist/shared/synthesized-attachment.js.map +1 -1
  121. package/dist/shared/tool-rounds.d.ts +51 -23
  122. package/dist/shared/tool-rounds.js +67 -24
  123. package/dist/shared/tool-rounds.js.map +1 -1
  124. package/dist/shared/workspace/platform-dir.d.ts +15 -0
  125. package/dist/shared/workspace/platform-dir.js +18 -1
  126. package/dist/shared/workspace/platform-dir.js.map +1 -1
  127. package/dist/shared/workspace/sources/git.js +13 -6
  128. package/dist/shared/workspace/sources/git.js.map +1 -1
  129. package/dist/shared/workspace/stigmer-link.d.ts +21 -14
  130. package/dist/shared/workspace/stigmer-link.js +91 -29
  131. package/dist/shared/workspace/stigmer-link.js.map +1 -1
  132. package/dist/tools/url-guard.d.ts +5 -1
  133. package/dist/tools/url-guard.js +9 -2
  134. package/dist/tools/url-guard.js.map +1 -1
  135. package/dist/worker.d.ts +11 -2
  136. package/dist/worker.js +1 -1
  137. package/dist/worker.js.map +1 -1
  138. package/dist/workflow-engine/tasks/human-input.d.ts +16 -0
  139. package/dist/workflow-engine/tasks/human-input.js +79 -37
  140. package/dist/workflow-engine/tasks/human-input.js.map +1 -1
  141. package/dist/workflow-engine/types.d.ts +14 -4
  142. package/dist/workflow-engine/types.js.map +1 -1
  143. package/dist/workflows/call-agent-orchestrator.d.ts +13 -0
  144. package/dist/workflows/call-agent-orchestrator.js.map +1 -1
  145. package/dist/workflows/engine-core.d.ts +25 -0
  146. package/dist/workflows/engine-core.js.map +1 -1
  147. package/dist/workflows/execute-from-execution.d.ts +3 -0
  148. package/dist/workflows/execute-from-execution.js.map +1 -1
  149. package/dist/workflows/human-input-orchestrator.d.ts +9 -12
  150. package/dist/workflows/human-input-orchestrator.js +12 -44
  151. package/dist/workflows/human-input-orchestrator.js.map +1 -1
  152. package/package.json +5 -5
  153. package/src/__test-utils__/config-fixture.ts +1 -0
  154. package/src/__test-utils__/execution-record-fixture.ts +2 -1
  155. package/src/__test-utils__/harness-contract/runtime-contract.ts +5 -3
  156. package/src/__test-utils__/test-gate.ts +8 -0
  157. package/src/__tests__/exit-backstop.test.ts +91 -0
  158. package/src/__tests__/golden-e2e.test.ts +80 -62
  159. package/src/__tests__/history-encryption-e2e.test.ts +51 -27
  160. package/src/__tests__/runner.test.ts +60 -2
  161. package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
  162. package/src/activities/__tests__/discover-mcp-server.hang.test.ts +5 -1
  163. package/src/activities/__tests__/discover-mcp-server.test.ts +90 -49
  164. package/src/activities/__tests__/workflow-event-activities.test.ts +28 -7
  165. package/src/activities/call-agent.ts +1 -1
  166. package/src/activities/discover-mcp-server.ts +27 -37
  167. package/src/activities/execute-cursor/__test-utils__/fold.ts +6 -4
  168. package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +1 -0
  169. package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +4 -29
  170. package/src/activities/execute-cursor/__tests__/hermetic/bypassed-tool-call.test.ts +120 -0
  171. package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +5 -5
  172. package/src/activities/execute-cursor/__tests__/hermetic/goldens/file-review-capture.status.json +0 -2
  173. package/src/activities/execute-cursor/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
  174. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.error-delta.status.json +0 -3
  175. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.output-first.status.json +0 -2
  176. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.status.json +0 -2
  177. package/src/activities/execute-cursor/__tests__/hermetic/goldens/stall.status.json +0 -2
  178. package/src/activities/execute-cursor/__tests__/hermetic/goldens/unattributed-hook-block.status.json +0 -3
  179. package/src/activities/execute-cursor/__tests__/hermetic/shell-output-delta.test.ts +9 -7
  180. package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +6 -1
  181. package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +8 -11
  182. package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +1 -3
  183. package/src/activities/execute-cursor/__tests__/translator.test.ts +68 -15
  184. package/src/activities/execute-cursor/approval-policy.ts +11 -36
  185. package/src/activities/execute-cursor/boundary-rows.ts +3 -3
  186. package/src/activities/execute-cursor/translator.ts +15 -32
  187. package/src/activities/execute-cursor/turn-stream.ts +4 -1
  188. package/src/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.ts +33 -2
  189. package/src/activities/execute-deep-agent/__tests__/adapter.test.ts +1 -0
  190. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.build-from-plan.prompt.md +1 -27
  191. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.everything.prompt.md +26 -30
  192. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.plan-mode.prompt.md +1 -26
  193. package/src/activities/execute-deep-agent/__tests__/goldens/user-message.everything.md +36 -0
  194. package/src/activities/execute-deep-agent/__tests__/hermetic/file-review-capture.test.ts +4 -0
  195. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/file-review-capture.status.json +1 -1
  196. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn1.status.json +1 -1
  197. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn2.status.json +1 -1
  198. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/recursion-limit.persisted.status.json +8 -73
  199. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
  200. package/src/activities/execute-deep-agent/__tests__/hermetic/inline-artifact-across-gate.test.ts +17 -0
  201. package/src/activities/execute-deep-agent/__tests__/hermetic/recursion-limit.test.ts +86 -26
  202. package/src/activities/execute-deep-agent/__tests__/hermetic/resolution-error.test.ts +9 -5
  203. package/src/activities/execute-deep-agent/__tests__/hermetic/thrown-turn-drains-publishes.test.ts +159 -0
  204. package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +84 -0
  205. package/src/activities/execute-deep-agent/__tests__/platform-route.test.ts +47 -10
  206. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +127 -199
  207. package/src/activities/execute-deep-agent/__tests__/prompt-goldens.test.ts +31 -26
  208. package/src/activities/execute-deep-agent/__tests__/streaming-side-effects.test.ts +34 -0
  209. package/src/activities/execute-deep-agent/__tests__/turn-stream.test.ts +13 -5
  210. package/src/activities/execute-deep-agent/inline-publisher.ts +47 -7
  211. package/src/activities/execute-deep-agent/platform-route.ts +109 -20
  212. package/src/activities/execute-deep-agent/prompt-builder.ts +104 -92
  213. package/src/activities/execute-deep-agent/streaming-side-effects.ts +27 -4
  214. package/src/activities/execute-deep-agent/translator.ts +5 -4
  215. package/src/activities/execute-deep-agent/turn-settle.ts +6 -6
  216. package/src/activities/execute-deep-agent/turn-setup.ts +43 -38
  217. package/src/activities/execute-deep-agent/turn-stream.ts +15 -22
  218. package/src/activities/execute-deep-agent/turn.ts +16 -7
  219. package/src/activities/workflow-event-activities.ts +2 -0
  220. package/src/config.ts +11 -0
  221. package/src/exit-backstop.ts +97 -0
  222. package/src/harness/README.md +5 -5
  223. package/src/harness/terminal-table.ts +13 -13
  224. package/src/harness/transcript/__tests__/builder.test.ts +93 -21
  225. package/src/harness/transcript/builder.ts +53 -30
  226. package/src/harness/transcript/events.ts +22 -27
  227. package/src/harness/turn-context.ts +5 -3
  228. package/src/main.ts +9 -5
  229. package/src/middleware/__tests__/execution-budget.test.ts +181 -103
  230. package/src/middleware/cost-advisory.ts +6 -3
  231. package/src/middleware/execution-budget.ts +60 -34
  232. package/src/middleware/index.ts +10 -7
  233. package/src/middleware/types.ts +6 -1
  234. package/src/runner-manager.ts +3 -2
  235. package/src/runner.ts +57 -15
  236. package/src/shared/__tests__/approval-policy.test.ts +41 -0
  237. package/src/shared/__tests__/artifact-storage.test.ts +1 -0
  238. package/src/shared/__tests__/connect-backfill.test.ts +15 -11
  239. package/src/shared/__tests__/mcp-resolver.test.ts +72 -10
  240. package/src/shared/__tests__/model-error.test.ts +71 -3
  241. package/src/shared/__tests__/platform-server-address.test.ts +122 -0
  242. package/src/shared/__tests__/prompt-sections.test.ts +2 -54
  243. package/src/shared/__tests__/tool-rounds.test.ts +64 -14
  244. package/src/shared/approval-policy.ts +57 -14
  245. package/src/shared/caller-identity.ts +4 -5
  246. package/src/shared/channel-attachment.ts +2 -2
  247. package/src/shared/connect-backfill.ts +5 -1
  248. package/src/shared/mcp-resolver.ts +11 -1
  249. package/src/shared/memory-attachment.ts +2 -2
  250. package/src/shared/model-error.ts +87 -9
  251. package/src/shared/platform-server-address.ts +123 -0
  252. package/src/shared/prompt-sections.ts +6 -55
  253. package/src/shared/skill-resolver.ts +2 -3
  254. package/src/shared/synthesized-attachment.ts +0 -12
  255. package/src/shared/tool-rounds.ts +70 -24
  256. package/src/shared/workspace/__tests__/git-source.test.ts +11 -5
  257. package/src/shared/workspace/__tests__/stigmer-link.test.ts +107 -10
  258. package/src/shared/workspace/platform-dir.ts +19 -1
  259. package/src/shared/workspace/sources/git.ts +14 -8
  260. package/src/shared/workspace/stigmer-link.ts +99 -29
  261. package/src/tools/__tests__/url-guard.test.ts +19 -1
  262. package/src/tools/url-guard.ts +9 -1
  263. package/src/worker.ts +12 -2
  264. package/src/workflow-engine/__tests__/golden-execution.test.ts +2 -1
  265. package/src/workflow-engine/__tests__/loader.test.ts +1 -1
  266. package/src/workflow-engine/__tests__/tasks/human-input.test.ts +133 -57
  267. package/src/workflow-engine/tasks/human-input.ts +92 -42
  268. package/src/workflow-engine/types.ts +16 -4
  269. package/src/workflows/call-agent-orchestrator.ts +7 -0
  270. package/src/workflows/engine-core.ts +18 -0
  271. package/src/workflows/execute-from-execution.ts +2 -1
  272. package/src/workflows/human-input-orchestrator.ts +15 -56
  273. package/dist/shared/skill-relevance.d.ts +0 -65
  274. package/dist/shared/skill-relevance.js +0 -175
  275. package/dist/shared/skill-relevance.js.map +0 -1
  276. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.skills-below-threshold.prompt.md +0 -146
  277. package/src/shared/__tests__/skill-relevance.test.ts +0 -292
  278. package/src/shared/skill-relevance.ts +0 -222
@@ -16,8 +16,17 @@
16
16
  * Until #1096 the same correlation also fed the write-back
17
17
  * coordinator's per-file commit (`onFileModified`); write-back is
18
18
  * finalize-only now, the turn runtime's, so publishing is this trigger's one
19
- * effect. The loop (`turn-stream.ts`) drains {@link pendingPublishPromises}
20
- * in the settle.
19
+ * effect.
20
+ *
21
+ * One instance per turn, owned by the turn's transcript
22
+ * (`turn-stream.ts` `createDeepAgentTranscript`) and not by the stream loop,
23
+ * so the in-flight publishes outlive a stream that throws: the settle
24
+ * drains them before its safety net, and the turn's `finally`
25
+ * (`turn.ts`) drains them on every exit, a thrown turn included, before the
26
+ * runtime writes the terminal status (stigmer#1116). Until then the loop
27
+ * built this and handed the list out only on its return values, so a turn
28
+ * that threw left its uploads to land on a status already persisted for the
29
+ * last time.
21
30
  */
22
31
 
23
32
  import type { TranscriptEvent } from "../../harness/transcript/events.js";
@@ -33,12 +42,26 @@ export class StreamingSideEffects {
33
42
  private readonly inputCache = new Map<string, CachedToolInput>();
34
43
  private readonly inlinePublisher: InlinePublisher | undefined;
35
44
 
36
- readonly pendingPublishPromises: Promise<void>[] = [];
45
+ private readonly pending: Promise<void>[] = [];
37
46
 
38
47
  constructor(opts: { inlinePublisher?: InlinePublisher }) {
39
48
  this.inlinePublisher = opts.inlinePublisher;
40
49
  }
41
50
 
51
+ /** The publishes fired so far, in order; each resolves (a publish logs and swallows its own failure). */
52
+ get pendingPublishPromises(): readonly Promise<void>[] {
53
+ return this.pending;
54
+ }
55
+
56
+ /**
57
+ * Wait for every publish fired so far to land on the status. Idempotent:
58
+ * a second call over settled publishes returns at once. Bounded by the
59
+ * artifact storage's own per-call timeouts, as every drain has been.
60
+ */
61
+ async drainPublishes(): Promise<void> {
62
+ if (this.pending.length > 0) await Promise.allSettled(this.pending);
63
+ }
64
+
42
65
  onEvent(event: TranscriptEvent): void {
43
66
  if (!this.inlinePublisher) return;
44
67
 
@@ -54,7 +77,7 @@ export class StreamingSideEffects {
54
77
  if (!isFileModifyingTool(cached.toolName)) return;
55
78
  const filePath = extractFilePath(cached.input);
56
79
  if (!filePath) return;
57
- this.pendingPublishPromises.push(this.inlinePublisher.publish(filePath));
80
+ this.pending.push(this.inlinePublisher.publish(filePath));
58
81
  }
59
82
  }
60
83
  }
@@ -30,10 +30,11 @@
30
30
  * mode asks the workspace's git state, asynchronously;
31
31
  * `middleware/approval-gate.ts`), and duplicating them would be a second copy
32
32
  * of the decision — exactly the drift one writer per fact exists to end. So
33
- * native emits no `tool_started.gate`; the held call reaches the transcript
34
- * as `approval_proposed` from the post-stream seed. Until #1097 the builder
35
- * carried a copy of the decision that gated MCP tools only, a path
36
- * production never took.
33
+ * the held call reaches the transcript as `approval_proposed` from the
34
+ * post-stream seed, the one writer of a row's approval fields; the Cursor
35
+ * harness answers the same way since #1117. Until #1097 the builder carried a
36
+ * copy of the decision that gated MCP tools only, a path production never
37
+ * took.
37
38
  *
38
39
  * Attribution and provenance are answered for ROOT-scope calls only: a
39
40
  * sub-agent's rows carry none on either harness today (#1133), and one
@@ -59,12 +59,12 @@ export async function settleDeepAgentTurn(deps: DeepAgentSettleDeps): Promise<Tu
59
59
  const { executionId } = input;
60
60
  const { status } = sink;
61
61
 
62
- // Drain the publishes the stream fired (fire-and-forget completions); the
63
- // safety net that scans tool calls for files the stream missed runs only
64
- // on a finished turn, as it always did.
65
- if (stream.pendingPublishPromises.length > 0) {
66
- await Promise.allSettled(stream.pendingPublishPromises);
67
- }
62
+ // Drain the publishes the stream fired (fire-and-forget completions) BEFORE
63
+ // the safety net: its publisher skips a file only when the file's row is
64
+ // already on the status, so an upload still in flight would be sent twice.
65
+ // The net scans tool calls for files the stream missed and runs only on a
66
+ // finished turn, as it always did.
67
+ await transcript.sideEffects.drainPublishes();
68
68
  if (stream.reason === "completed") {
69
69
  try {
70
70
  await autoPublishWrittenFiles(status, transcript.publisher);
@@ -67,10 +67,11 @@ import { buildChatModel } from "../../shared/model-client.js";
67
67
  import { graphThinks, toAnthropicThinking } from "../../shared/thinking-mode.js";
68
68
  import { isUnattendedApprovalMode, type MergedToolPolicy } from "../../shared/approval-policy.js";
69
69
  import type { ToolApprovalCategory } from "../../shared/tool-kind.js";
70
- import { alsoAvailableSkillsNote, selectSkillsForPrompt, visionPromptInfoOf } from "../../shared/prompt-sections.js";
70
+ import { visionPromptInfoOf } from "../../shared/prompt-sections.js";
71
+ import { findApprovedPlanPath } from "../../shared/implement-plan-prompt.js";
71
72
  import type { RecalledMemoriesContent } from "../../shared/recalled-memories.js";
72
73
  import { toLangChainImageBlocks } from "../../shared/attachment-vision.js";
73
- import { resolveRecursionLimit, UNBOUNDED_ADVISORY_RECURSION_LIMIT } from "../../shared/tool-rounds.js";
74
+ import { backstopRecursionLimit, resolveToolRoundLimit } from "../../shared/tool-rounds.js";
74
75
  import { jsonSchemaToZod } from "../../shared/json-schema-to-zod.js";
75
76
  import { CasCaptureObserver } from "./cas-capture-observer.js";
76
77
  import { createCasCaptureBackend } from "./cas-capture-backend.js";
@@ -260,25 +261,6 @@ export function readGateState(input: TurnInput, tools: DeepAgentTools): DeepAgen
260
261
  };
261
262
  }
262
263
 
263
- /**
264
- * The `## Skills` section of the root prompt: the skills the shared selection
265
- * highlights for this turn's message (`shared/prompt-sections.ts`
266
- * `selectSkillsForPrompt`: every skill below the threshold, the relevant ones
267
- * at and above it), rendered with this engine's activation protocol, plus —
268
- * when the selection left any out — an `### Also Available` note naming them
269
- * (the sentence is the shared, tool-neutral one; the heading is this
270
- * harness's markdown framing).
271
- */
272
- export function renderRootSkillsSection(input: TurnInput): string {
273
- const { highlighted, alsoAvailable } = selectSkillsForPrompt(input.execution.spec?.message ?? "", input.skills.root);
274
- if (alsoAvailable.length === 0) return renderSkillsSection(highlighted);
275
- console.log(
276
- `[turn-setup] Skill relevance filter: ${highlighted.length} included, ` +
277
- `${alsoAvailable.length} excluded: ${alsoAvailable.join(", ")}`,
278
- );
279
- return renderSkillsSection(highlighted) + "\n### Also Available\n\n" + alsoAvailableSkillsNote(alsoAvailable) + "\n";
280
- }
281
-
282
264
  /**
283
265
  * The system prompt over the runtime's resolved record: every input the
284
266
  * builder renders is read from `TurnInput` here, in one place, so the mapping
@@ -287,6 +269,12 @@ export function renderRootSkillsSection(input: TurnInput): string {
287
269
  * that is not on the record is the memoized memory selection, which the
288
270
  * caller awaits (the runtime stamps the report on the status the first time
289
271
  * it is pulled, before this turn's first persist).
272
+ *
273
+ * Nothing here reads the turn's message or payload: the system prompt is the
274
+ * session's (`prompt-builder.ts` says why), so every mounted skill is
275
+ * described and the turn's files ride its message (`composeTurnMessage`).
276
+ * The approved plan's path is the one fact taken from the attachments, and
277
+ * only on a build-from-plan turn, where the mode already changes the prompt.
290
278
  */
291
279
  export function composeSystemPrompt(input: TurnInput, recalledMemories: RecalledMemoriesContent | undefined): string {
292
280
  const spec = input.execution.spec!;
@@ -296,19 +284,17 @@ export function composeSystemPrompt(input: TurnInput, recalledMemories: Recalled
296
284
  instructions: input.blueprint.instructions,
297
285
  provisionResults: input.workspace.provision.provisionResults,
298
286
  containerRoot: primaryDir,
299
- skillsPromptSection: renderRootSkillsSection(input),
287
+ skillsPromptSection: renderSkillsSection(input.skills.root),
300
288
  // "" (nothing sendable) threads as undefined: the tool alone still serves
301
289
  // text sends inside a 24-hour window (DD-006 D6).
302
290
  channelTemplatesPromptSection: input.mcp.channelMessaging.length > 0
303
291
  ? formatChannelTemplatesSection(input.mcp.channelMessaging) || undefined
304
292
  : undefined,
305
- workspaceFileRefs: spec.workspaceFileRefs ?? [],
306
- workspaceRoot: primaryDir,
307
- inputFiles: input.attachments.results,
308
- vision: visionPromptInfoOf(input.attachments),
309
- downloadUrlKind: input.artifactStorage?.downloadUrlKind,
310
293
  interactionMode: execConfig?.interactionMode,
311
294
  buildFromPlan: execConfig?.buildFromPlan,
295
+ ...(execConfig?.buildFromPlan
296
+ ? { approvedPlanPath: findApprovedPlanPath(input.attachments.results.map((f) => f.relativePath)) }
297
+ : {}),
312
298
  contextBridge: input.standing.contextBridge,
313
299
  senderIdentity: input.standing.senderIdentity,
314
300
  sessionContext: input.standing.sessionContext,
@@ -317,6 +303,25 @@ export function composeSystemPrompt(input: TurnInput, recalledMemories: Recalled
317
303
  });
318
304
  }
319
305
 
306
+ /**
307
+ * The turn's user message over the runtime's resolved record: what the user
308
+ * typed, with this turn's payload (its input files and referenced paths) and
309
+ * the conversation catchup composed around it (`prompt-builder.ts`
310
+ * `composeUserMessage`). The twin of `composeSystemPrompt` for the turn's
311
+ * half, so the goldens pin this mapping and not a copy of it.
312
+ */
313
+ export function composeTurnMessage(input: TurnInput): string {
314
+ const spec = input.execution.spec!;
315
+ return composeUserMessage({
316
+ message: spec.message,
317
+ conversationCatchup: input.standing.conversationCatchup,
318
+ inputFiles: input.attachments.results,
319
+ vision: visionPromptInfoOf(input.attachments),
320
+ downloadUrlKind: input.artifactStorage?.downloadUrlKind,
321
+ workspaceFileRefs: spec.workspaceFileRefs ?? [],
322
+ });
323
+ }
324
+
320
325
  /**
321
326
  * Build the engine: the model, the middleware stack with the gate, the
322
327
  * tools, the compiled sub-agents, the system prompt and `createDeepAgent`.
@@ -431,10 +436,10 @@ export async function buildEngine(
431
436
  : null;
432
437
 
433
438
  const maxCostUsd = execConfig?.maxCostUsd ?? 0;
434
- const recursionLimit = resolveRecursionLimit(execConfig?.maxToolRounds);
439
+ const toolRoundLimit = resolveToolRoundLimit(execConfig?.maxToolRounds);
435
440
  const { middleware, costAdvisory } = buildMiddlewareStack({
436
441
  loopDetection: { historySize: 20, consecutiveThreshold: 7, totalThreshold: 20 },
437
- executionBudget: { recursionLimit: recursionLimit ?? UNBOUNDED_ADVISORY_RECURSION_LIMIT, warningPct: 80 },
442
+ executionBudget: { maxToolRounds: toolRoundLimit, warningPct: 80 },
438
443
  toolTruncation: { maxChars: execConfig?.maxToolResultChars || 30_000 },
439
444
  costAdvisory: maxCostUsd > 0
440
445
  ? {
@@ -541,10 +546,10 @@ export async function buildEngine(
541
546
 
542
547
  const langgraphConfig: Record<string, unknown> = {
543
548
  configurable: { thread_id: input.threadId },
544
- // Hard enforcement of max_tool_rounds (null = unlimited): when the graph
545
- // exhausts this, the stream throws GraphRecursionError and the adapter
546
- // ends the turn `tool_call_limit` with the work checkpointed.
547
- ...(recursionLimit !== null ? { recursionLimit } : {}),
549
+ // max_tool_rounds is enforced by the execution budget middleware, round
550
+ // by round; this is only its backstop against a graph that loops without
551
+ // calling the model (`shared/tool-rounds.ts`). Unset = unlimited = none.
552
+ ...(toolRoundLimit !== null ? { recursionLimit: backstopRecursionLimit(toolRoundLimit) } : {}),
548
553
  };
549
554
 
550
555
  console.log(
@@ -577,9 +582,10 @@ export async function buildEngine(
577
582
  * What the graph is invoked with. The checkpoint is read once (on the
578
583
  * durable http saver an extra `getState` is a network round-trip): pending
579
584
  * interrupts that the runtime's decisions answer make a `Command(resume)`;
580
- * anything else is the turn's user message — the conversation catchup
581
- * framed before it (cloud DD-006; in the USER MESSAGE so it enters the
582
- * checkpointer with the turn), the structured-output contract appended, and
585
+ * anything else is the turn's user message — this turn's payload and the
586
+ * conversation catchup framed before it (`composeTurnMessage`; in the USER
587
+ * MESSAGE so it enters the checkpointer with the turn and the system prompt
588
+ * stays the session's), the structured-output contract appended, and
583
589
  * the inline images FIRST as content blocks when the turn carries any (per
584
590
  * Anthropic's images-before-text guidance; a plain string otherwise).
585
591
  */
@@ -590,8 +596,7 @@ export async function composeGraphInput(input: TurnInput, engine: DeepAgentEngin
590
596
  return { kind: "resume", command: resume.graphInput };
591
597
  }
592
598
 
593
- const spec = input.execution.spec!;
594
- let userMessage = composeUserMessage(spec.message, input.standing.conversationCatchup);
599
+ let userMessage = composeTurnMessage(input);
595
600
  if (engine.hasStructuredOutput) {
596
601
  userMessage += `\n\n---\nIMPORTANT: When your analysis is complete, provide your findings as structured output matching the required schema. The system will capture your structured response automatically.`;
597
602
  }
@@ -89,8 +89,6 @@ export interface DeepAgentStreamResult {
89
89
  readonly eventsProcessed: number;
90
90
  /** The graph's final state (`structuredResponse` rides it); absent unless the stream completed and the output resolved in time. */
91
91
  readonly runOutput: Record<string, unknown> | undefined;
92
- /** Inline publishes fired mid-turn, drained by the settle. */
93
- readonly pendingPublishPromises: readonly Promise<void>[];
94
92
  }
95
93
 
96
94
  /**
@@ -98,14 +96,19 @@ export interface DeepAgentStreamResult {
98
96
  * the engine's events into canonical ones (over the gate's posture, so every
99
97
  * tool start carries its attribution and gate answer), and the publisher
100
98
  * that registers artifacts on the runtime's builder (`sink.transcript`, so
101
- * a published artifact still dirties the next persist). The builder itself
102
- * is the runtime's, one per turn, and is read from the sink where it is
103
- * needed. Built by the adapter's turn once and shared by the stream and the
104
- * settle.
99
+ * a published artifact still dirties the next persist) and checks the rows
100
+ * already on `sink.status` before uploading, so an earlier turn's unchanged
101
+ * file is not sent again; and the side effects that fire the publisher
102
+ * mid-turn and hold its in-flight uploads until they are drained. The
103
+ * builder itself is the runtime's, one per turn, and is read from the sink
104
+ * where it is needed. Built by the adapter's turn once and shared by the
105
+ * stream, the settle and the turn's `finally`, which drains the publishes
106
+ * on every exit (`streaming-side-effects.ts`).
105
107
  */
106
108
  export interface DeepAgentTranscript {
107
109
  readonly translator: DeepAgentTranslator;
108
110
  readonly publisher: InlinePublisher;
111
+ readonly sideEffects: StreamingSideEffects;
109
112
  }
110
113
 
111
114
  export function createDeepAgentTranscript(
@@ -119,9 +122,10 @@ export function createDeepAgentTranscript(
119
122
  workspaceBackend: workspace.backend,
120
123
  artifactStorage: input.artifactStorage,
121
124
  artifacts: sink.transcript,
125
+ record: sink.status,
122
126
  executionId: input.executionId,
123
127
  });
124
- return { translator, publisher };
128
+ return { translator, publisher, sideEffects: new StreamingSideEffects({ inlinePublisher: publisher }) };
125
129
  }
126
130
 
127
131
  /**
@@ -166,12 +170,11 @@ export interface DeepAgentStreamDeps {
166
170
  export async function consumeDeepAgentStream(deps: DeepAgentStreamDeps): Promise<DeepAgentStreamResult> {
167
171
  const { input, sink, engine, graphInput, transcript } = deps;
168
172
  const { executionId } = input;
169
- const { translator, publisher } = transcript;
173
+ const { translator, sideEffects } = transcript;
170
174
  const builder = sink.transcript;
171
175
 
172
176
  const scheduler = new StreamingUpdateScheduler(loadStreamingConfig());
173
177
  const recorder = createV3EventRecorder(executionId, process.env.V3_EVENT_RECORD_DIR);
174
- const sideEffects = new StreamingSideEffects({ inlinePublisher: publisher });
175
178
  const abortController = new AbortController();
176
179
  let forceAbort: ReturnType<typeof setTimeout> | undefined;
177
180
  const abortRun = (): void => {
@@ -188,12 +191,7 @@ export async function consumeDeepAgentStream(deps: DeepAgentStreamDeps): Promise
188
191
  /** The stop, taken at a boundary: abort the run now and settle. */
189
192
  const interrupted = (): DeepAgentStreamResult => {
190
193
  abortRun();
191
- return {
192
- reason: "interrupted",
193
- eventsProcessed,
194
- runOutput: undefined,
195
- pendingPublishPromises: sideEffects.pendingPublishPromises,
196
- };
194
+ return { reason: "interrupted", eventsProcessed, runOutput: undefined };
197
195
  };
198
196
 
199
197
  try {
@@ -254,18 +252,13 @@ export async function consumeDeepAgentStream(deps: DeepAgentStreamDeps): Promise
254
252
 
255
253
  if (builder.awaitingApproval) {
256
254
  console.log(`[turn-stream] execution=${executionId} stream ended at a gate; awaiting approval`);
257
- return {
258
- reason: "awaiting_approval",
259
- eventsProcessed,
260
- runOutput: undefined,
261
- pendingPublishPromises: sideEffects.pendingPublishPromises,
262
- };
255
+ return { reason: "awaiting_approval", eventsProcessed, runOutput: undefined };
263
256
  }
264
257
 
265
258
  console.log(`[turn-stream] execution=${executionId} stream finished — processed ${eventsProcessed} events`);
266
259
  const runOutput = await extractRunOutput(run, executionId);
267
260
  sink.recordActivity();
268
- return { reason: "completed", eventsProcessed, runOutput, pendingPublishPromises: sideEffects.pendingPublishPromises };
261
+ return { reason: "completed", eventsProcessed, runOutput };
269
262
  } finally {
270
263
  sink.stopSignal.removeEventListener("abort", onStop);
271
264
  if (forceAbort !== undefined) clearTimeout(forceAbort);
@@ -24,7 +24,10 @@
24
24
  * other exception — a provider error unwrapped from LangChain's middleware
25
25
  * wrapper, an MCP connect failure, an empty stream — is `failed` on the
26
26
  * `internal` surface with `describeExecutionError`'s sentence as
27
- * `status.error` whole. Never branches on why `stopSignal`
27
+ * `status.error` whole, and it returns only after the inline publishes the
28
+ * turn already fired have landed on the status (the drain in the `finally`),
29
+ * so an artifact the agent wrote before the throw is on the terminal status
30
+ * the runtime then persists (stigmer#1116). Never branches on why `stopSignal`
28
31
  * aborted, never writes a phase or a terminal copy, never imports
29
32
  * `@temporalio/*` (`__tests__/adapter-is-temporal-free.test.ts`).
30
33
  *
@@ -44,7 +47,7 @@ import type { TurnInput, TurnOutcome, TurnSink } from "../../harness/types.js";
44
47
  import type { McpConnectionResult } from "../../shared/mcp-manager.js";
45
48
  import { tryInferProvider } from "../../shared/llm-proxy.js";
46
49
  import { describeExecutionError } from "../../shared/model-error.js";
47
- import { isGraphRecursionError } from "../../shared/tool-rounds.js";
50
+ import { isToolCallBudgetStop } from "../../shared/tool-rounds.js";
48
51
  import {
49
52
  buildDeepAgentWorkspace,
50
53
  buildEngine,
@@ -55,7 +58,7 @@ import {
55
58
  resolveModelName,
56
59
  type DeepAgentAdapterConfig,
57
60
  } from "./turn-setup.js";
58
- import { consumeDeepAgentStream, createDeepAgentTranscript } from "./turn-stream.js";
61
+ import { consumeDeepAgentStream, createDeepAgentTranscript, type DeepAgentTranscript } from "./turn-stream.js";
59
62
  import { settleDeepAgentTurn } from "./turn-settle.js";
60
63
 
61
64
  export async function runDeepAgentTurn(input: TurnInput, sink: TurnSink, config: DeepAgentAdapterConfig): Promise<TurnOutcome> {
@@ -64,6 +67,7 @@ export async function runDeepAgentTurn(input: TurnInput, sink: TurnSink, config:
64
67
  let checkpointer: BaseCheckpointSaver | undefined;
65
68
  let connection: McpConnectionResult | undefined;
66
69
  let modelName: string | undefined;
70
+ let transcript: DeepAgentTranscript | undefined;
67
71
 
68
72
  try {
69
73
  modelName = await resolveModelName(input);
@@ -83,12 +87,16 @@ export async function runDeepAgentTurn(input: TurnInput, sink: TurnSink, config:
83
87
  const graphInput = await composeGraphInput(input, engine);
84
88
  sink.recordActivity();
85
89
 
86
- const transcript = createDeepAgentTranscript(input, sink, engine, workspace);
90
+ transcript = createDeepAgentTranscript(input, sink, engine, workspace);
87
91
  const stream = await consumeDeepAgentStream({ input, sink, engine, graphInput, transcript });
88
92
  return await settleDeepAgentTurn({ input, sink, engine, transcript, stream });
89
93
  } catch (err) {
90
94
  return classifyThrown(err, input, config, modelName);
91
95
  } finally {
96
+ // First, on every exit: the runtime persists the terminal status as soon
97
+ // as this returns, so an upload still in flight must land before then.
98
+ // A settled turn already drained in its settle; this is then a no-op.
99
+ await transcript?.sideEffects.drainPublishes();
92
100
  await closeQuietly(input.executionId, "MCP connection", () => connection?.client.close());
93
101
  // Only the durable sqlite saver holds an OS handle (an open DB file);
94
102
  // memory/http savers have no close(), so this is duck-typed.
@@ -98,8 +106,9 @@ export async function runDeepAgentTurn(input: TurnInput, sink: TurnSink, config:
98
106
  }
99
107
 
100
108
  /**
101
- * An exception that escaped the turn, classified. The graph's recursion
102
- * limit is the budget's own signal (`shared/tool-rounds.ts`), not an error:
109
+ * An exception that escaped the turn, classified. The tool-call budget's stop
110
+ * (the execution budget's round limit, or its recursion-limit backstop;
111
+ * `shared/tool-rounds.ts`) is the budget's own signal, not an error:
103
112
  * the work is checkpointed and the conversation continues on the next
104
113
  * message. Anything else is described by `describeExecutionError` (a model
105
114
  * error arrives MiddlewareError-wrapped with raw provider prose and is
@@ -108,7 +117,7 @@ export async function runDeepAgentTurn(input: TurnInput, sink: TurnSink, config:
108
117
  * broke.
109
118
  */
110
119
  function classifyThrown(err: unknown, input: TurnInput, config: DeepAgentAdapterConfig, modelName: string | undefined): TurnOutcome {
111
- if (isGraphRecursionError(err)) {
120
+ if (isToolCallBudgetStop(err)) {
112
121
  console.log(`ExecuteDeepAgent reached the tool-call limit: execution=${input.executionId}`);
113
122
  return { kind: "tool_call_limit" };
114
123
  }
@@ -337,6 +337,8 @@ export function toProtoEvent(desc: WorkflowEventDescriptor): WorkflowExecutionEv
337
337
  : undefined,
338
338
  comment: desc.comment,
339
339
  waitDurationMs: BigInt(desc.waitDurationMs),
340
+ outcome: desc.outcome,
341
+ autoResolved: desc.autoResolved,
340
342
  }),
341
343
  };
342
344
  break;
package/src/config.ts CHANGED
@@ -113,6 +113,15 @@ export interface Config {
113
113
  * and shared/conversation-attachment.ts.
114
114
  */
115
115
  readonly mcpBridgeEndpoint: string | null;
116
+ /**
117
+ * The Stigmer server's public address (STIGMER_MCP_PUBLIC_ENDPOINT), which
118
+ * the server hands every sandbox it provisions or an operator names to a
119
+ * runner started by hand. When set, it fills a missing
120
+ * STIGMER_SERVER_ADDRESS for any MCP server that declares the key; null
121
+ * leaves a remote server's address unfilled. See
122
+ * shared/platform-server-address.ts.
123
+ */
124
+ readonly mcpPublicEndpoint: string | null;
116
125
  /**
117
126
  * The Cursor API key for direct mode (authenticates with Cursor directly).
118
127
  * In proxy mode this is a placeholder (`"proxy-managed"` unless the operator
@@ -232,6 +241,7 @@ export function loadConfig(): Config {
232
241
  const stigmerTokenRef: TokenRef = { current: stigmerTokenValue ?? null };
233
242
 
234
243
  const mcpBridgeEndpoint = process.env.STIGMER_MCP_BRIDGE_ENDPOINT ?? null;
244
+ const mcpPublicEndpoint = process.env.STIGMER_MCP_PUBLIC_ENDPOINT || null;
235
245
 
236
246
  // In proxy mode the SDK's API key is the control-plane credential, which the
237
247
  // Cursor adapter reads from the ref per turn (execute-cursor/turn-setup.ts);
@@ -285,6 +295,7 @@ export function loadConfig(): Config {
285
295
  stigmerBackendEndpoint,
286
296
  stigmerTokenRef,
287
297
  mcpBridgeEndpoint,
298
+ mcpPublicEndpoint,
288
299
  cursorApiKey,
289
300
  workspaceRootDir,
290
301
  mode,
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The exit backstop for static and pool modes: once the runner's own
3
+ * shutdown has finished (worker drained, harnesses released, Temporal
4
+ * connection closed, OTel flushed), the process must end within a named
5
+ * bound, and when it has to be MADE to end, the log names what kept it
6
+ * alive.
7
+ *
8
+ * Static and pool modes end when the Node event loop drains. That is the
9
+ * normal path and it stays the normal path: a healthy runner exits in
10
+ * milliseconds on its own. The failure it guards against is one leaked
11
+ * handle — a timer, a socket, a child — that outlives the worker and holds
12
+ * the process until the orchestrator's grace expires. One uncleared timer
13
+ * in our own code did exactly that to every pool-member roll for three
14
+ * months (issue #1008), and the next one may sit in a vendor SDK we do not
15
+ * own. A hosted sandbox pod's grace is ten minutes, so the cost of a leak
16
+ * is not bounded by anything the runner controls — until this module.
17
+ *
18
+ * Why these choices:
19
+ *
20
+ * - The timer is `unref()`'d, so arming it never keeps the process alive.
21
+ * A drained loop exits naturally and the timer dies with it; the
22
+ * backstop only ever fires when something else is still holding the
23
+ * loop. An unref'd timer is also absent from
24
+ * `process.getActiveResourcesInfo()`, so the line never names the
25
+ * backstop itself.
26
+ * - The bound sits above the conformance harness's slow-exit threshold
27
+ * (2 s, `test/conformance/src/harness/runner-process.ts`), so a leak is
28
+ * still reported there as a slow exit instead of being hidden under it,
29
+ * and far below the harness's 30 s and the hosted 10-minute graces.
30
+ * - A forced exit exits 0. By the time the backstop is armed the drain and
31
+ * the flush have completed; the lingering handle is a defect named in the
32
+ * log, not a failed shutdown, and a non-zero code would make Kubernetes
33
+ * and the desktop host report a crash for a clean stop.
34
+ * - It is armed only after everything the shutdown awaits. Work that
35
+ * nobody awaits (a fire-and-forget promise) is the only thing it can cut;
36
+ * if such work matters at exit, the fix is to await it, and this line
37
+ * names it the first time it happens.
38
+ *
39
+ * Manager mode does not use this: it is driven by its host over stdin and
40
+ * exits at once after its shutdown (issue #177, `main.ts`).
41
+ */
42
+
43
+ /** How long the process may outlive its own completed shutdown before it is made to exit. */
44
+ export const EXIT_BACKSTOP_MS = 5_000;
45
+
46
+ export interface ExitBackstopOptions {
47
+ /** The bound; defaults to {@link EXIT_BACKSTOP_MS}. */
48
+ readonly boundMs?: number;
49
+ /** Ends the process; defaults to `process.exit`. */
50
+ readonly exit?: (code: number) => void;
51
+ /** Names what still holds the loop; defaults to `process.getActiveResourcesInfo`. */
52
+ readonly activeResources?: () => string[];
53
+ /** Writes the forced-exit line; defaults to `console.warn`. */
54
+ readonly log?: (line: string) => void;
55
+ }
56
+
57
+ /**
58
+ * Arm the backstop. Call it once, after the mode's shutdown has returned.
59
+ * Returns the armed timer so a test can observe that it does not hold the
60
+ * loop; production callers ignore it.
61
+ */
62
+ export function armExitBackstop(
63
+ options: ExitBackstopOptions = {},
64
+ ): ReturnType<typeof setTimeout> {
65
+ const boundMs = options.boundMs ?? EXIT_BACKSTOP_MS;
66
+ const exit = options.exit ?? ((code: number) => process.exit(code));
67
+ const activeResources = options.activeResources ?? (() => process.getActiveResourcesInfo());
68
+ const log = options.log ?? ((line: string) => console.warn(line));
69
+
70
+ const timer = setTimeout(() => {
71
+ log(
72
+ `[runner] Process still alive ${boundMs}ms after shutdown; forcing exit. ` +
73
+ `Active resources: ${describeResources(activeResources())}`,
74
+ );
75
+ exit(0);
76
+ }, boundMs);
77
+ timer.unref();
78
+ return timer;
79
+ }
80
+
81
+ /**
82
+ * `["Timeout", "TCPSocketWrap", "TCPSocketWrap"]` → `"TCPSocketWrap×2, Timeout×1"`:
83
+ * grouped and sorted, so the same leak reads the same on every exit.
84
+ */
85
+ export function describeResources(resources: readonly string[]): string {
86
+ if (resources.length === 0) {
87
+ return "none reported";
88
+ }
89
+ const counts = new Map<string, number>();
90
+ for (const resource of resources) {
91
+ counts.set(resource, (counts.get(resource) ?? 0) + 1);
92
+ }
93
+ return [...counts.entries()]
94
+ .sort(([a], [b]) => a.localeCompare(b))
95
+ .map(([resource, count]) => `${resource}×${count}`)
96
+ .join(", ");
97
+ }
@@ -200,11 +200,10 @@ Every hit falls into one of three classes:
200
200
  | `apis/ai/stigmer/agentic/agentexecution/v1/enum.proto` | A comment on tool-name casing per harness. |
201
201
  | `backend/README.md` | Lists the harnesses and the SDK behind each. |
202
202
  | `client-apps/desktop/scripts/stage-runner-slim.sh`, `client-apps/desktop/scripts/macos-codesign-tree.sh` | Stage and sign the vendor SDKs' native helpers for the desktop app; a harness whose SDK ships one touches both. |
203
- | `sdk/react/src/models/harness.ts` | The SDK hub: `HarnessOption`, `HARNESS_META`, `HARNESS_LABELS`, `HARNESS_OPTIONS`, `DEFAULT_HARNESS`, `toProtoHarness`, `fromProtoHarness`. `HarnessOption` carries four values beyond the proto's two; `toProtoHarness` maps them to `NATIVE` through its `default` arm. |
204
- | `sdk/react/src/models/HarnessSelector.tsx` | `OPTIONS`, the two selectable values. |
205
- | `sdk/react/src/models/ModelSelector.tsx` | A Cursor-only thinking rule, and the filter that hides the extra options by name. |
203
+ | `sdk/react/src/models/harness.ts` | The SDK hub: `HarnessOption`, `HARNESS_META`, `HARNESS_LABELS`, `HARNESS_OPTIONS`, `DEFAULT_HARNESS`, `isHarnessOption`, `toProtoHarness`, `fromProtoHarness`, all read from one option-to-enum table; its reverse is typed over the whole enum, so a new member fails the SDK's typecheck here. |
204
+ | `sdk/react/src/models/HarnessSelector.tsx` | The premium mark on the Cursor segment; the segments themselves come from `HARNESS_OPTIONS`. |
205
+ | `sdk/react/src/models/ModelSelector.tsx` | A Cursor-only thinking rule. |
206
206
  | `sdk/react/src/identity-account/AccountPreferencesPanel.tsx` | One `HarnessOptionRow` per harness, placed by hand. |
207
- | `sdk/react/src/identity-account/useAccountExecutionDefaults.ts` | Validates the preference against the two names. |
208
207
  | `sdk/react/src/workflow/inspector/forms/AgentCallForm.tsx` | Two radio buttons, placed by hand. |
209
208
  | `sdk/react/src/pricing-governance/BaselineEditor.tsx` | A select with one option per harness. |
210
209
  | `site/src/components/pages/pricing/ModelPricingTable.tsx` | Harness to display label. |
@@ -234,7 +233,8 @@ Nothing to touch. Listed so a probe's hit here is a stop, not a lead.
234
233
  (read the session's harness or name the activities in prose).
235
234
  - The React SDK: `sdk/react/src/models/registry.ts`,
236
235
  `sdk/react/src/models/useModelRegistry.ts`,
237
- `sdk/react/src/models/service-tier.ts`; everything under
236
+ `sdk/react/src/models/service-tier.ts`,
237
+ `sdk/react/src/identity-account/useAccountExecutionDefaults.ts`; everything under
238
238
  `sdk/react/src/channel/`, `sdk/react/src/composer/`,
239
239
  `sdk/react/src/execution/`, `sdk/react/src/schedule/`,
240
240
  `sdk/react/src/session/` (typed by the option or defaulting to one value).
@@ -11,6 +11,12 @@
11
11
  * harness once kept one function per arm (`streaming-terminal.ts`, retired
12
12
  * in #1096); since #1096 both harnesses' turns end through this one table.
13
13
  *
14
+ * The `error` of a FAILED arm is the failure itself, never framed: the
15
+ * `Execution failed:` wording belongs to the rows and to each consumer's
16
+ * own copy (the console's badge, the CLI's epilogue), so a consumer meets
17
+ * one grammar whichever arm wrote it. The interruption arms' `Execution
18
+ * interrupted:` errors are a different outcome, one the user resumes.
19
+ *
14
20
  * The throw-vs-return rule, unchanged from the orchestrator it replaces:
15
21
  * - RETURN when a Temporal retry would only repeat the outcome — a stall
16
22
  * would wedge again, an exhausted budget would burn again, a platform
@@ -293,21 +299,15 @@ export function failedArm(message: string, surface: FailureSurface): TerminalArm
293
299
 
294
300
  /**
295
301
  * An error the runtime caught itself, outside any adapter (a resolution
296
- * phase, the epilogue): the boilerplate row and the described error as the
297
- * details, with `status.error` carrying the `Execution failed:` prefix the
298
- * orchestrator's generic arm always wrote (`describeExecutionError` supplies
299
- * the two parts). The one arm whose `error` and details row differ, kept as
300
- * it was because the resolution-error golden pins both.
302
+ * phase, the epilogue): an `internal` failure like any adapter's, described
303
+ * as `[<type>] <message>` (`describeExecutionError` supplies the two parts),
304
+ * so `status.error` reads the same whichever side caught the fault. Until
305
+ * stigmer#1122 this arm alone prefixed `status.error` with `Execution
306
+ * failed:`, and a consumer that adds its own failure copy (the CLI's
307
+ * epilogue) printed the words twice.
301
308
  */
302
309
  export function unexpectedErrorArm(errorType: string, errorMessage: string): TerminalArm {
303
- const described = `[${errorType}] ${errorMessage}`;
304
- return {
305
- phase: ExecutionPhase.EXECUTION_FAILED,
306
- error: `Execution failed: ${described}`,
307
- rows: [TERMINAL_COPY.internalFailure.row, `Error details: ${described}`],
308
- completes: true,
309
- disposition: RETURN,
310
- };
310
+ return failedArm(`[${errorType}] ${errorMessage}`, "internal");
311
311
  }
312
312
 
313
313
  // ── Applying an arm ─────────────────────────────────────────────────────────