@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
@@ -19,10 +19,13 @@
19
19
  * rows, finalize, artifacts and write-backs, the dirty flag, the error
20
20
  * guard — joined commit by commit in the same PR. The observer arm (the
21
21
  * runtime's fold-watcher: told after the fold, guarded apart from the
22
- * handlers, silent on seed) joined with the turn timeline.
22
+ * handlers, silent on seed) joined with the turn timeline. The
23
+ * one-observation arms (`applyObservation`: one clock read for every stamp a
24
+ * raw event's facts make) joined with stigmer#1390, whose Cursor flake they
25
+ * make deterministic by moving the clock between folds.
23
26
  */
24
27
 
25
- import { describe, it, expect } from "vitest";
28
+ import { afterEach, beforeEach, describe, it, expect, vi } from "vitest";
26
29
  import { create } from "@bufbuild/protobuf";
27
30
  import { type AgentExecutionStatus, AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
28
31
  import {
@@ -124,21 +127,18 @@ describe("TranscriptBuilder — tool_finished and tool_error are upserts", () =>
124
127
  expect(row.error).toBe("EACCES");
125
128
  });
126
129
 
127
- it("a gated row's error stamps approvalRequestedAt once — the boundary's shape (Cursor)", () => {
128
- const { sb, status } = feed(builder(), [
130
+ it("an error alone writes no approval fact — a denied call is an ordinary failure until a proposal parks it (#1117)", () => {
131
+ const { status } = feed(builder(), [
129
132
  { kind: "message_start", runId: "run-1" },
130
133
  { kind: "text_delta", runId: "run-1", text: "Running the build." },
131
- { kind: "tool_started", callId: "sh-1", name: "Shell", input: { command: "make" }, mcpServerSlug: "", gate: { message: "Run command: make" } },
134
+ { kind: "tool_started", callId: "sh-1", name: "Shell", input: { command: "make" }, mcpServerSlug: "", provenance: "builtin_category" },
132
135
  { kind: "tool_error", callId: "sh-1", message: "Blocked by hook" },
133
136
  ]);
134
137
  const row = rowOf(status, "sh-1");
135
- expect(row.requiresApproval).toBe(true);
136
- expect(row.approvalMessage).toBe("Run command: make");
137
138
  expect(row.status).toBe(ToolCallStatus.TOOL_CALL_FAILED);
138
- const stamped = row.approvalRequestedAt;
139
- expect(stamped).toContain("T");
140
- sb.apply({ kind: "tool_error", callId: "sh-1", message: "again" });
141
- expect(rowOf(status, "sh-1").approvalRequestedAt, "stamped once").toBe(stamped);
139
+ expect(row.requiresApproval).toBe(false);
140
+ expect(row.approvalMessage).toBe("");
141
+ expect(row.approvalRequestedAt).toBe("");
142
142
  });
143
143
 
144
144
  it("an ungated row's error stamps no approvalRequestedAt", () => {
@@ -152,12 +152,11 @@ describe("TranscriptBuilder — tool_finished and tool_error are upserts", () =>
152
152
  { kind: "tool_finished", callId: "obs-1", result: "", observedAt: "2026-01-01T00:00:07.000Z" },
153
153
  ]);
154
154
  expect(rowOf(status, "obs-1").completedAt, "the delta's own instant").toBe("2026-01-01T00:00:07.000Z");
155
- sb.apply({ kind: "tool_started", callId: "obs-2", name: "Shell", input: { command: "make" }, mcpServerSlug: "", gate: { message: "Run command: make" } });
155
+ sb.apply({ kind: "tool_started", callId: "obs-2", name: "Shell", input: { command: "make" }, mcpServerSlug: "" });
156
156
  sb.apply({ kind: "tool_error", callId: "obs-2", message: "", observedAt: "2026-01-01T00:00:09.000Z" });
157
- const gated = rowOf(status, "obs-2");
158
- expect(gated.status).toBe(ToolCallStatus.TOOL_CALL_FAILED);
159
- expect(gated.completedAt).toBe("2026-01-01T00:00:09.000Z");
160
- expect(gated.approvalRequestedAt, "the gate's stamp shares the observed instant").toBe("2026-01-01T00:00:09.000Z");
157
+ const failed = rowOf(status, "obs-2");
158
+ expect(failed.status).toBe(ToolCallStatus.TOOL_CALL_FAILED);
159
+ expect(failed.completedAt).toBe("2026-01-01T00:00:09.000Z");
161
160
  // The stream's own error, arriving later with the text, fills the message
162
161
  // and moves no stamp.
163
162
  sb.apply({ kind: "tool_error", callId: "obs-2", message: "blocked" });
@@ -174,6 +173,75 @@ describe("TranscriptBuilder — tool_finished and tool_error are upserts", () =>
174
173
  });
175
174
  });
176
175
 
176
+ describe("TranscriptBuilder — one observation is folded at one instant", () => {
177
+ const T0 = "2026-01-01T00:00:00.000Z";
178
+
179
+ // The clock moves 1 ms after every fold, so any stamp that read it twice
180
+ // within one observation would show two instants.
181
+ function ticking(): Fold {
182
+ const status = create(AgentExecutionStatusSchema, {});
183
+ return { sb: new TranscriptBuilder("exec-instant", status, () => vi.advanceTimersByTime(1)), status };
184
+ }
185
+
186
+ beforeEach(() => {
187
+ vi.useFakeTimers({ toFake: ["Date"] });
188
+ vi.setSystemTime(new Date(T0));
189
+ });
190
+ afterEach(() => {
191
+ vi.useRealTimers();
192
+ });
193
+
194
+ it("every stamp of the batch is one instant: a row's start and finish, a sub-agent's open and close, the child's rows and messages", () => {
195
+ const { sb, status } = ticking();
196
+ sb.applyObservation([
197
+ { kind: "message_finish", runId: "run-1" },
198
+ { kind: "sub_agent_started", subAgentId: "task-1", name: "helper", subject: "Look.", input: "Look." },
199
+ { kind: "tool_started", callId: "task-1", name: "task", input: { subagent_type: "helper" }, mcpServerSlug: "" },
200
+ { kind: "tool_finished", callId: "task-1", result: "found" },
201
+ { kind: "message_start", runId: "sub-1", subAgentId: "task-1" },
202
+ { kind: "text_delta", runId: "sub-1", text: "Searching.", subAgentId: "task-1" },
203
+ { kind: "tool_started", callId: "g", name: "grep", input: { pattern: "x" }, mcpServerSlug: "", subAgentId: "task-1" },
204
+ { kind: "tool_finished", callId: "g", result: "x.ts", subAgentId: "task-1" },
205
+ { kind: "sub_agent_finished", subAgentId: "task-1", output: "found" },
206
+ ]);
207
+ const task = rowOf(status, "task-1");
208
+ const [sub] = status.subAgentExecutions;
209
+ const child = sub.messages.flatMap((m) => m.toolCalls)[0];
210
+ expect([task.startedAt, task.completedAt, sub.startedAt, sub.completedAt, child.startedAt, child.completedAt, sub.messages[0].timestamp]).toEqual(Array(7).fill(T0));
211
+ expect(new Date().toISOString(), "the clock did move between folds").not.toBe(T0);
212
+ });
213
+
214
+ it("a fact that carries its own observedAt keeps it inside a batch", () => {
215
+ const { sb, status } = ticking();
216
+ sb.applyObservation([
217
+ { kind: "tool_started", callId: "c1", name: "read_file", input: {}, mcpServerSlug: "" },
218
+ { kind: "tool_finished", callId: "c1", result: "", observedAt: "2025-12-31T23:59:58.000Z" },
219
+ ]);
220
+ expect([rowOf(status, "c1").startedAt, rowOf(status, "c1").completedAt]).toEqual([T0, "2025-12-31T23:59:58.000Z"]);
221
+ });
222
+
223
+ it("a re-emitted start inside a batch leaves the row's first start alone", () => {
224
+ const { sb, status } = ticking();
225
+ sb.applyObservation([{ kind: "tool_started", callId: "c1", name: "read_file", input: {}, mcpServerSlug: "" }]);
226
+ vi.setSystemTime(new Date("2026-01-01T00:00:05.000Z"));
227
+ sb.applyObservation([
228
+ { kind: "tool_started", callId: "c1", name: "read_file", input: {}, mcpServerSlug: "" },
229
+ { kind: "tool_finished", callId: "c1", result: "ok" },
230
+ ]);
231
+ expect([rowOf(status, "c1").startedAt, rowOf(status, "c1").completedAt]).toEqual([T0, "2026-01-01T00:00:05.000Z"]);
232
+ });
233
+
234
+ it("apply alone reads the clock per fact, and a batch ends with its call", () => {
235
+ const { sb, status } = ticking();
236
+ sb.apply({ kind: "tool_started", callId: "c1", name: "read_file", input: {}, mcpServerSlug: "" });
237
+ sb.apply({ kind: "tool_finished", callId: "c1", result: "ok" });
238
+ expect(rowOf(status, "c1").completedAt).toBe("2026-01-01T00:00:00.001Z");
239
+ sb.applyObservation([{ kind: "tool_started", callId: "c2", name: "read_file", input: {}, mcpServerSlug: "" }]);
240
+ sb.apply({ kind: "tool_finished", callId: "c2", result: "ok" });
241
+ expect([rowOf(status, "c2").startedAt, rowOf(status, "c2").completedAt]).toEqual(["2026-01-01T00:00:00.002Z", "2026-01-01T00:00:00.003Z"]);
242
+ });
243
+ });
244
+
177
245
  describe("TranscriptBuilder — every row with args carries them redacted, and an elided, redacted argsPreview", () => {
178
246
  it("an ungated row's preview is its args, secret keys redacted", () => {
179
247
  const { status } = feed(builder(), [
@@ -327,17 +395,21 @@ describe("TranscriptBuilder — approval_proposed is the one place a row is ever
327
395
  const { sb, status } = feed(builder(), [
328
396
  { kind: "message_start", runId: "run-1" },
329
397
  { kind: "text_delta", runId: "run-1", text: "Running the build." },
330
- { kind: "tool_started", callId: "sh-1", name: "Shell", input: { command: "make" }, mcpServerSlug: "", gate: { message: "Run command: make" } },
398
+ { kind: "tool_started", callId: "sh-1", name: "Shell", input: { command: "make" }, mcpServerSlug: "" },
331
399
  { kind: "tool_error", callId: "sh-1", message: "Blocked by hook" },
332
400
  ]);
333
- const stamped = rowOf(status, "sh-1").approvalRequestedAt;
334
401
  sb.apply(proposal("sh-1", { name: "Shell", message: "Run command: make", contentDigest: "d-1" }));
335
402
  const row = rowOf(status, "sh-1");
336
403
  expect(row.status).toBe(ToolCallStatus.TOOL_CALL_WAITING_APPROVAL);
404
+ expect(row.requiresApproval, "the proposal is the one writer of the hold").toBe(true);
405
+ expect(row.approvalMessage).toBe("Run command: make");
337
406
  expect(row.error).toBe("");
338
407
  expect(row.result).toBe("");
339
408
  expect(row.completedAt).toBe("");
340
- expect(row.approvalRequestedAt, "the first stamp stands").toBe(stamped);
409
+ const stamped = row.approvalRequestedAt;
410
+ expect(stamped).toContain("T");
411
+ sb.apply(proposal("sh-1", { name: "Shell", message: "Run command: make", contentDigest: "d-1" }));
412
+ expect(rowOf(status, "sh-1").approvalRequestedAt, "the first stamp stands").toBe(stamped);
341
413
  expect(row.approvalContentDigest).toBe("d-1");
342
414
  expect(status.messages.flatMap((m) => m.toolCalls), "no second row").toHaveLength(1);
343
415
  expect(sb.awaitingApproval).toBe(true);
@@ -347,7 +419,7 @@ describe("TranscriptBuilder — approval_proposed is the one place a row is ever
347
419
  const { sb, status } = feed(builder(), [
348
420
  { kind: "message_start", runId: "run-1" },
349
421
  { kind: "text_delta", runId: "run-1", text: "Editing notes." },
350
- { kind: "tool_started", callId: "ed-1", name: "edit", input: { path: "notes.md", content: "partial" }, mcpServerSlug: "", gate: { message: "Write file: notes.md" } },
422
+ { kind: "tool_started", callId: "ed-1", name: "edit", input: { path: "notes.md", content: "partial" }, mcpServerSlug: "" },
351
423
  { kind: "tool_error", callId: "ed-1", message: "Blocked by hook" },
352
424
  ]);
353
425
  // The boundary's overlay carries the hook's captured input — the whole
@@ -368,7 +440,7 @@ describe("TranscriptBuilder — approval_proposed is the one place a row is ever
368
440
 
369
441
  it("no tool_started ever sets awaitingApproval; only a proposal does", () => {
370
442
  const { sb, status } = feed(builder(), [
371
- { kind: "tool_started", callId: "sh-1", name: "Shell", input: {}, mcpServerSlug: "", gate: { message: "Run command" } },
443
+ { kind: "tool_started", callId: "sh-1", name: "Shell", input: {}, mcpServerSlug: "", provenance: "builtin_category" },
372
444
  ]);
373
445
  expect(sb.awaitingApproval).toBe(false);
374
446
  expect(rowOf(status, "sh-1").status).toBe(ToolCallStatus.TOOL_CALL_RUNNING);
@@ -48,6 +48,11 @@
48
48
  * observed earlier and delivers later stamps its own instant
49
49
  * (`observedAt`); an output chunk for a settled row is stale
50
50
  * and dropped — the completion's result is the whole output.
51
+ * - One observation, one instant: the facts a translator made of one
52
+ * raw engine event, folded through {@link applyObservation}, share
53
+ * one clock read for every stamp they make (a Cursor completion with
54
+ * no running before it starts and finishes at the same instant,
55
+ * stigmer#1390); {@link apply} alone reads the clock per fact.
51
56
  * - The AI-message boundary (native's own rule, made correct by
52
57
  * scoping): a tool row attaches to the scope's current AI message — the
53
58
  * latest with text, the seed's last AI message over a seeded transcript
@@ -148,6 +153,8 @@ export class TranscriptBuilder {
148
153
  private readonly observer: TranscriptObserver | undefined;
149
154
  private _dirty = false;
150
155
  private _awaitingApproval = false;
156
+ /** The one instant of the observation being folded, while {@link applyObservation} runs; absent otherwise. */
157
+ private observationInstant: string | undefined;
151
158
 
152
159
  /**
153
160
  * Builds INTO `status`, by reference: its `messages`, `subAgentExecutions`
@@ -233,6 +240,32 @@ export class TranscriptBuilder {
233
240
  }
234
241
  }
235
242
 
243
+ /**
244
+ * Fold the facts ONE harness observation yields — the events a translator
245
+ * made of one raw engine event — at one instant: the clock is read once,
246
+ * and every stamp the facts make (a row's start and finish, a sub-agent's
247
+ * open and close, a message's timestamp) is that instant. A Cursor
248
+ * completion the stream reports with no running before it is a start and a
249
+ * finish in one event, and it must carry one instant, not two reads a
250
+ * millisecond apart (stigmer#1390). A fact that carries its own
251
+ * `observedAt` keeps it. Each fact is applied exactly as {@link apply}
252
+ * applies it, observer included.
253
+ */
254
+ applyObservation(facts: readonly TranscriptEvent[]): void {
255
+ const outer = this.observationInstant;
256
+ this.observationInstant = outer ?? utcTimestamp();
257
+ try {
258
+ for (const fact of facts) this.apply(fact);
259
+ } finally {
260
+ this.observationInstant = outer;
261
+ }
262
+ }
263
+
264
+ /** The instant a stamp records: the observation's, while one is being folded, else now. */
265
+ private instant(): string {
266
+ return this.observationInstant ?? utcTimestamp();
267
+ }
268
+
236
269
  private fold(event: TranscriptEvent): void {
237
270
  try {
238
271
  switch (event.kind) {
@@ -354,7 +387,7 @@ export class TranscriptBuilder {
354
387
  subject: event.subject,
355
388
  input: event.input,
356
389
  status: SubAgentStatus.SUB_AGENT_IN_PROGRESS,
357
- startedAt: utcTimestamp(),
390
+ startedAt: this.instant(),
358
391
  });
359
392
  this.state.openSubAgent(row);
360
393
  this._dirty = true;
@@ -364,7 +397,7 @@ export class TranscriptBuilder {
364
397
  const scope = this.state.subAgent(event.subAgentId);
365
398
  if (!scope) return;
366
399
  scope.row.status = SubAgentStatus.SUB_AGENT_COMPLETED;
367
- scope.row.completedAt = utcTimestamp();
400
+ scope.row.completedAt = this.instant();
368
401
  if (event.output !== undefined) scope.row.output = event.output;
369
402
  finalizeStreaming(scope.transcript);
370
403
  this._dirty = true;
@@ -374,7 +407,7 @@ export class TranscriptBuilder {
374
407
  const scope = this.state.subAgent(event.subAgentId);
375
408
  if (!scope) return;
376
409
  scope.row.status = SubAgentStatus.SUB_AGENT_FAILED;
377
- scope.row.completedAt = utcTimestamp();
410
+ scope.row.completedAt = this.instant();
378
411
  scope.row.error = event.error;
379
412
  finalizeStreaming(scope.transcript);
380
413
  this._dirty = true;
@@ -427,7 +460,7 @@ export class TranscriptBuilder {
427
460
  const msg = create(AgentMessageSchema, {
428
461
  type: MessageType.MESSAGE_THINKING,
429
462
  content: text,
430
- timestamp: utcTimestamp(),
463
+ timestamp: this.instant(),
431
464
  isStreaming: true,
432
465
  });
433
466
  scope.messages.push(msg);
@@ -466,17 +499,16 @@ export class TranscriptBuilder {
466
499
  // only when the scope has no message yet.
467
500
  const parentMsg = scope.currentAiMessage ?? this.ensureAiMessageForToolCall(scope);
468
501
 
469
- // A started call is RUNNING, gated or not: a gated call that has started
470
- // runs until the harness's boundary parks it through `approval_proposed`
471
- // (Cursor), and on native a held call never starts at all — the engine
472
- // interrupts before the tool runs and the post-stream seed proposes it.
473
- // The builder writes the gate's word it is told and decides nothing
474
- // about approval.
502
+ // A started call is RUNNING and carries no approval fact: whether it is
503
+ // held is known only after the engine's gate has run, and a held call
504
+ // reaches the transcript as `approval_proposed` (Cursor's boundary after
505
+ // the hook denied it; native's post-stream seed, where a held call never
506
+ // starts at all). The builder decides nothing about approval.
475
507
  const tc = create(ToolCallSchema, {
476
508
  id: callId,
477
509
  name,
478
510
  status: ToolCallStatus.TOOL_CALL_RUNNING,
479
- startedAt: utcTimestamp(),
511
+ startedAt: this.instant(),
480
512
  });
481
513
 
482
514
  if (Object.keys(input).length > 0) {
@@ -503,13 +535,6 @@ export class TranscriptBuilder {
503
535
  tc.policyEngineVersion = POLICY_ENGINE_VERSION;
504
536
  }
505
537
 
506
- // The gate's word: the row says it is gated and what the card will ask;
507
- // `approvalRequestedAt` is stamped when the boundary actually parks it
508
- // (`approval_proposed`, or a gated row's `tool_error`), never here.
509
- if (event.gate) {
510
- tc.requiresApproval = true;
511
- tc.approvalMessage = event.gate.message;
512
- }
513
538
 
514
539
  parentMsg.toolCalls.push(tc);
515
540
  scope.toolCalls.set(callId, tc);
@@ -543,8 +568,8 @@ export class TranscriptBuilder {
543
568
  const resultChanged = !!result && result !== tc.result;
544
569
  if (!wasSettled) tc.status = ToolCallStatus.TOOL_CALL_COMPLETED;
545
570
  // The instant the harness OBSERVED the completion when it delivers it
546
- // later (Cursor's queued delta); otherwise now.
547
- if (!tc.completedAt) tc.completedAt = observedAt ?? utcTimestamp();
571
+ // later (Cursor's queued delta); otherwise the fold's instant.
572
+ if (!tc.completedAt) tc.completedAt = observedAt ?? this.instant();
548
573
  if (resultChanged) tc.result = result;
549
574
  stopStreaming(tc);
550
575
  scope.argBuffers.delete(callId);
@@ -571,17 +596,15 @@ export class TranscriptBuilder {
571
596
  const tc = scope.toolCalls.get(callId);
572
597
  if (!tc) return;
573
598
 
574
- // The same upsert as a finish. A gated row that fails is the
575
- // Cursor boundary's shape — the hook denied the call and the stream
576
- // reports `error`; the boundary parks it after the stream — so the
577
- // moment approval became due is stamped here if nothing stamped it yet.
599
+ // The same upsert as a finish. A call the Cursor hook denied also reports
600
+ // `error` here; it is an ordinary failure until the boundary parks it
601
+ // through `approval_proposed`, which stamps the approval fields.
578
602
  const wasSettled = isSettled(tc.status);
579
603
  const messageChanged = !!message && message !== tc.error;
580
- const at = observedAt ?? utcTimestamp();
604
+ const at = observedAt ?? this.instant();
581
605
  if (!wasSettled) tc.status = ToolCallStatus.TOOL_CALL_FAILED;
582
606
  if (!tc.completedAt) tc.completedAt = at;
583
607
  if (messageChanged) tc.error = message;
584
- if (tc.requiresApproval && !tc.approvalRequestedAt) tc.approvalRequestedAt = at;
585
608
  stopStreaming(tc);
586
609
  scope.argBuffers.delete(callId);
587
610
 
@@ -629,7 +652,7 @@ export class TranscriptBuilder {
629
652
  // ── Post-Stream Facts ──────────────────────────────────────────────
630
653
 
631
654
  private handleApprovalProposed(scope: Transcript, event: ApprovalProposedEvent): void {
632
- const now = utcTimestamp();
655
+ const now = this.instant();
633
656
  const existing = scope.toolCalls.get(event.callId);
634
657
  if (existing) {
635
658
  // Reopen (Cursor's `markWaitingApproval`): the boundary is re-proposing
@@ -693,7 +716,7 @@ export class TranscriptBuilder {
693
716
  scope.messages.push(create(AgentMessageSchema, {
694
717
  type: MessageType.MESSAGE_SYSTEM,
695
718
  content: text,
696
- timestamp: utcTimestamp(),
719
+ timestamp: this.instant(),
697
720
  }));
698
721
  this._dirty = true;
699
722
  }
@@ -712,7 +735,7 @@ export class TranscriptBuilder {
712
735
  const msg = create(AgentMessageSchema, {
713
736
  type: MessageType.MESSAGE_AI,
714
737
  content: "",
715
- timestamp: utcTimestamp(),
738
+ timestamp: this.instant(),
716
739
  isStreaming: true,
717
740
  });
718
741
 
@@ -737,7 +760,7 @@ export class TranscriptBuilder {
737
760
  const msg = create(AgentMessageSchema, {
738
761
  type: MessageType.MESSAGE_AI,
739
762
  content: "",
740
- timestamp: utcTimestamp(),
763
+ timestamp: this.instant(),
741
764
  isStreaming: false,
742
765
  });
743
766
 
@@ -18,11 +18,12 @@
18
18
  * what a row or a message carries; usage is neither.
19
19
  * - `tool_finished` carries `result: string`; the engine's output
20
20
  * envelope is the translator's to render.
21
- * - `tool_started` carries the attribution (`mcpServerSlug`), the
22
- * provenance and — where the harness runs a gated call before its
23
- * boundary parks it — the gate's word. The translator resolves them from
24
- * the harness's policy state; the builder writes what it is told and
25
- * decides nothing about approval.
21
+ * - `tool_started` carries the attribution (`mcpServerSlug`) and the
22
+ * provenance, which the translator resolves from the harness's policy
23
+ * state. It never says whether the call is held: a started call is
24
+ * running, and a held one reaches the transcript as `approval_proposed`
25
+ * on both harnesses, the one writer of a row's approval fields (#1117).
26
+ * The builder writes what it is told and decides nothing about approval.
26
27
  * - Scope is `subAgentId?` — the only scope either harness has is
27
28
  * "the root, or one sub-agent by the id its row carries" — and the
28
29
  * translator says when a sub-agent opens and closes
@@ -89,25 +90,18 @@ export interface MessageFinishEvent extends Scoped {
89
90
  // ── Tool Events ───────────────────────────────────────────────────
90
91
 
91
92
  /**
92
- * The translator's word that a call is GATED — the fields the row carries,
93
- * never a decision the builder makes (#1097, 2026-09-14). A gated call that
94
- * has STARTED is running until the
95
- * harness's boundary parks it through `approval_proposed`: that is Cursor's
96
- * shape (the SDK runs the tool; the deny-and-retry boundary parks it after
97
- * the stream). Native never emits this member — LangGraph's `interrupt()`
98
- * fires before the tool handler runs, so a held call never produces a
99
- * `tool_started` at all, and the arrival of one IS the engine's word that
100
- * the call was authorized (capture mode let it flow, a lease or policy
101
- * cleared it, or the user approved it). Native's gate fact reaches the
102
- * transcript through `approval_proposed` from the post-stream seed. So a
103
- * row is never WAITING at creation on either harness; the plan's `parked:
104
- * true` arm had no producer and was dropped.
93
+ * A tool call has started. It carries no approval fact, on either harness
94
+ * (#1117): whether a call is HELD for a person's decision is known only after
95
+ * the engine's own gate has run, and it reaches the transcript as
96
+ * `approval_proposed` — native's post-stream seed after LangGraph's
97
+ * `interrupt()` (which fires before the tool handler, so a held call never
98
+ * starts), Cursor's deny-and-retry boundary after the preToolUse hook denied
99
+ * it. A call that runs under a lease, a bypass or capture mode is therefore
100
+ * never marked as requiring approval. A `gate` member carried the policy's
101
+ * category verdict here until #1117; it had one producer (Cursor), which made
102
+ * the field mean different things per harness, and it was dropped, as a
103
+ * `parked: true` arm with no producer was before it.
105
104
  */
106
- export interface GateAnswer {
107
- /** The approval card's message, placeholders already resolved. */
108
- readonly message: string;
109
- }
110
-
111
105
  export interface ToolStartedEvent extends Scoped {
112
106
  readonly kind: "tool_started";
113
107
  readonly callId: string;
@@ -121,7 +115,6 @@ export interface ToolStartedEvent extends Scoped {
121
115
  * on both harnesses today, #1133) or no layer governs it.
122
116
  */
123
117
  readonly provenance?: PolicySource;
124
- readonly gate?: GateAnswer;
125
118
  }
126
119
 
127
120
  export interface ToolArgDeltaEvent extends Scoped {
@@ -139,15 +132,17 @@ export interface ToolOutputDeltaEvent extends Scoped {
139
132
  /**
140
133
  * A fact the harness observed at one instant and delivers at another. The
141
134
  * builder is the transcript's one clock — every row stamp is `utcTimestamp()`
142
- * at apply — except here: a Cursor completion arrives first on the SDK's
135
+ * at apply, read once per observation when the loop folds a raw event's
136
+ * facts together (`TranscriptBuilder.applyObservation`) — except here: a
137
+ * Cursor completion arrives first on the SDK's
143
138
  * delta channel, and the translator must QUEUE it for the loop to apply after
144
139
  * the current stream event (a delta applied mid-persist would land on a row
145
140
  * the offload is replacing). Without the observed
146
141
  * instant the row's `completedAt` would be the fold's moment, one stream
147
142
  * event late — seconds late while the model narrates — and the tool's
148
143
  * duration in the console would lie (fixed 2026-09-15, #1097). Absent
149
- * means "now": native, and every fact a translator emits as it sees it,
150
- * never set it.
144
+ * means the fold's instant: native, and every fact a translator emits as it
145
+ * sees it, never set it.
151
146
  */
152
147
  interface Observed {
153
148
  readonly observedAt?: string;
@@ -649,7 +649,9 @@ export async function reconcileReinvocation(
649
649
  * The MCP-bound env map (and ONLY it, never the agent process env) carries
650
650
  * the reserved caller-identity keys, so a server that declares them in
651
651
  * spec.env can template the platform-verified caller into its headers;
652
- * filterEnvToDeclaredKeys keeps every other server blind. The resolved list
652
+ * filterEnvToDeclaredKeys keeps every other server blind. Resolution fills a
653
+ * missing STIGMER_SERVER_ADDRESS per server from `config`'s endpoints
654
+ * (shared/platform-server-address.ts). The resolved list
653
655
  * then mutates through the Connect backfill and three synthesized
654
656
  * attachments, deliberately AFTER resolve + backfill so the backfill's
655
657
  * destructiveHint tightener can never force-gate an attachment's tools:
@@ -702,7 +704,7 @@ export async function resolveMcpServersAndPolicies(
702
704
  sessionId,
703
705
  );
704
706
  let servers = (await resolveMcpServers(
705
- client, blueprint.mergedMcpServerUsages, mcpEnvVars, transportPosture,
707
+ client, blueprint.mergedMcpServerUsages, mcpEnvVars, transportPosture, config,
706
708
  )).resolvedServers;
707
709
  deps.timing.mark("resolve_mcp_servers");
708
710
 
@@ -710,7 +712,7 @@ export async function resolveMcpServersAndPolicies(
710
712
  const sessionOrg = session.metadata?.org ?? "";
711
713
  servers = await backfillMcpServersIfNeeded(
712
714
  client, servers, blueprint.mergedMcpServerUsages, mcpEnvVars, sessionOrg,
713
- transportPosture, deps.heartbeat, environment.secretKeys,
715
+ transportPosture, config, deps.heartbeat, environment.secretKeys,
714
716
  );
715
717
  deps.timing.mark("backfill_mcp");
716
718
 
package/src/main.ts CHANGED
@@ -46,6 +46,7 @@ import type { IpcCommand, IpcResponse } from "./ipc-protocol.js";
46
46
 
47
47
  import { handleUnhandledRejection } from "./activities/execute-cursor/rejection-capture.js";
48
48
  import { installProcessPipeGuards, reportFatal } from "./pipe-safety.js";
49
+ import { armExitBackstop } from "./exit-backstop.js";
49
50
 
50
51
  // Guard the host pipes before anything writes to them. A dropped stderr/stdout
51
52
  // reader otherwise turns the next write into an uncaught EPIPE loop that starves
@@ -510,17 +511,20 @@ async function main(): Promise<void> {
510
511
  await runManagerMode(config);
511
512
  // Manager mode is driven by the host over stdin. Once it returns — via the IPC `shutdown`
512
513
  // command or stdin EOF when the host process dies — exit deterministically so a stray open
513
- // handle can't keep a shut-down runner alive as an orphan (issue #177). Static mode is left
514
- // to exit naturally so its OTel flush completes.
514
+ // handle can't keep a shut-down runner alive as an orphan (issue #177).
515
515
  process.exit(0);
516
516
  }
517
517
 
518
+ // Static and pool modes exit naturally once their shutdown has returned (worker drained,
519
+ // OTel flushed). The backstop is the safety net, not the path: a healthy runner exits in
520
+ // milliseconds and it never fires; a leaked handle gets a named bound and a log line naming
521
+ // it, instead of holding the process for the orchestrator's whole grace (exit-backstop.ts).
518
522
  if (runnerMode === "pool") {
519
523
  await runPoolMode(config, poolMemberId!);
520
- return;
524
+ } else {
525
+ await runStaticMode(config);
521
526
  }
522
-
523
- await runStaticMode(config);
527
+ armExitBackstop();
524
528
  }
525
529
 
526
530
  main().catch((err) => {