@stigmer/runner 3.40.1 → 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 (248) 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/streaming-side-effects.d.ts +20 -3
  34. package/dist/activities/execute-deep-agent/streaming-side-effects.js +26 -4
  35. package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
  36. package/dist/activities/execute-deep-agent/translator.d.ts +5 -4
  37. package/dist/activities/execute-deep-agent/translator.js +5 -4
  38. package/dist/activities/execute-deep-agent/translator.js.map +1 -1
  39. package/dist/activities/execute-deep-agent/turn-settle.js +6 -6
  40. package/dist/activities/execute-deep-agent/turn-settle.js.map +1 -1
  41. package/dist/activities/execute-deep-agent/turn-setup.js +7 -7
  42. package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
  43. package/dist/activities/execute-deep-agent/turn-stream.d.ts +10 -6
  44. package/dist/activities/execute-deep-agent/turn-stream.js +6 -16
  45. package/dist/activities/execute-deep-agent/turn-stream.js.map +1 -1
  46. package/dist/activities/execute-deep-agent/turn.d.ts +4 -1
  47. package/dist/activities/execute-deep-agent/turn.js +15 -6
  48. package/dist/activities/execute-deep-agent/turn.js.map +1 -1
  49. package/dist/activities/workflow-event-activities.js +2 -0
  50. package/dist/activities/workflow-event-activities.js.map +1 -1
  51. package/dist/config.d.ts +9 -0
  52. package/dist/config.js +2 -0
  53. package/dist/config.js.map +1 -1
  54. package/dist/exit-backstop.d.ts +64 -0
  55. package/dist/exit-backstop.js +79 -0
  56. package/dist/exit-backstop.js.map +1 -0
  57. package/dist/harness/terminal-table.d.ts +12 -5
  58. package/dist/harness/terminal-table.js +13 -13
  59. package/dist/harness/terminal-table.js.map +1 -1
  60. package/dist/harness/transcript/builder.d.ts +21 -0
  61. package/dist/harness/transcript/builder.js +53 -31
  62. package/dist/harness/transcript/builder.js.map +1 -1
  63. package/dist/harness/transcript/events.d.ts +22 -26
  64. package/dist/harness/transcript/events.js +6 -5
  65. package/dist/harness/transcript/events.js.map +1 -1
  66. package/dist/harness/turn-context.d.ts +3 -1
  67. package/dist/harness/turn-context.js +5 -3
  68. package/dist/harness/turn-context.js.map +1 -1
  69. package/dist/main.js +10 -4
  70. package/dist/main.js.map +1 -1
  71. package/dist/middleware/cost-advisory.d.ts +6 -3
  72. package/dist/middleware/cost-advisory.js +6 -3
  73. package/dist/middleware/cost-advisory.js.map +1 -1
  74. package/dist/middleware/execution-budget.d.ts +22 -7
  75. package/dist/middleware/execution-budget.js +57 -35
  76. package/dist/middleware/execution-budget.js.map +1 -1
  77. package/dist/middleware/index.d.ts +10 -7
  78. package/dist/middleware/index.js +10 -7
  79. package/dist/middleware/index.js.map +1 -1
  80. package/dist/middleware/types.d.ts +6 -1
  81. package/dist/runner-manager.js +3 -2
  82. package/dist/runner-manager.js.map +1 -1
  83. package/dist/runner.d.ts +25 -0
  84. package/dist/runner.js +41 -14
  85. package/dist/runner.js.map +1 -1
  86. package/dist/shared/approval-policy.d.ts +23 -13
  87. package/dist/shared/approval-policy.js +47 -14
  88. package/dist/shared/approval-policy.js.map +1 -1
  89. package/dist/shared/caller-identity.d.ts +4 -5
  90. package/dist/shared/caller-identity.js +4 -5
  91. package/dist/shared/caller-identity.js.map +1 -1
  92. package/dist/shared/channel-attachment.js +3 -2
  93. package/dist/shared/channel-attachment.js.map +1 -1
  94. package/dist/shared/connect-backfill.d.ts +2 -1
  95. package/dist/shared/connect-backfill.js +2 -2
  96. package/dist/shared/connect-backfill.js.map +1 -1
  97. package/dist/shared/mcp-resolver.d.ts +10 -1
  98. package/dist/shared/mcp-resolver.js +11 -2
  99. package/dist/shared/mcp-resolver.js.map +1 -1
  100. package/dist/shared/memory-attachment.js +3 -2
  101. package/dist/shared/memory-attachment.js.map +1 -1
  102. package/dist/shared/model-error.d.ts +25 -3
  103. package/dist/shared/model-error.js +78 -10
  104. package/dist/shared/model-error.js.map +1 -1
  105. package/dist/shared/platform-server-address.d.ts +71 -0
  106. package/dist/shared/platform-server-address.js +103 -0
  107. package/dist/shared/platform-server-address.js.map +1 -0
  108. package/dist/shared/synthesized-attachment.d.ts +0 -5
  109. package/dist/shared/synthesized-attachment.js +0 -12
  110. package/dist/shared/synthesized-attachment.js.map +1 -1
  111. package/dist/shared/tool-rounds.d.ts +51 -23
  112. package/dist/shared/tool-rounds.js +67 -24
  113. package/dist/shared/tool-rounds.js.map +1 -1
  114. package/dist/shared/workspace/platform-dir.d.ts +15 -0
  115. package/dist/shared/workspace/platform-dir.js +18 -1
  116. package/dist/shared/workspace/platform-dir.js.map +1 -1
  117. package/dist/shared/workspace/stigmer-link.d.ts +21 -14
  118. package/dist/shared/workspace/stigmer-link.js +91 -29
  119. package/dist/shared/workspace/stigmer-link.js.map +1 -1
  120. package/dist/tools/url-guard.d.ts +5 -1
  121. package/dist/tools/url-guard.js +9 -2
  122. package/dist/tools/url-guard.js.map +1 -1
  123. package/dist/worker.d.ts +11 -2
  124. package/dist/worker.js +1 -1
  125. package/dist/worker.js.map +1 -1
  126. package/dist/workflow-engine/tasks/human-input.d.ts +16 -0
  127. package/dist/workflow-engine/tasks/human-input.js +79 -37
  128. package/dist/workflow-engine/tasks/human-input.js.map +1 -1
  129. package/dist/workflow-engine/types.d.ts +14 -4
  130. package/dist/workflow-engine/types.js.map +1 -1
  131. package/dist/workflows/call-agent-orchestrator.d.ts +13 -0
  132. package/dist/workflows/call-agent-orchestrator.js.map +1 -1
  133. package/dist/workflows/engine-core.d.ts +25 -0
  134. package/dist/workflows/engine-core.js.map +1 -1
  135. package/dist/workflows/execute-from-execution.d.ts +3 -0
  136. package/dist/workflows/execute-from-execution.js.map +1 -1
  137. package/dist/workflows/human-input-orchestrator.d.ts +9 -12
  138. package/dist/workflows/human-input-orchestrator.js +12 -44
  139. package/dist/workflows/human-input-orchestrator.js.map +1 -1
  140. package/package.json +5 -5
  141. package/src/__test-utils__/config-fixture.ts +1 -0
  142. package/src/__test-utils__/execution-record-fixture.ts +2 -1
  143. package/src/__test-utils__/harness-contract/runtime-contract.ts +5 -3
  144. package/src/__test-utils__/test-gate.ts +8 -0
  145. package/src/__tests__/exit-backstop.test.ts +91 -0
  146. package/src/__tests__/golden-e2e.test.ts +80 -62
  147. package/src/__tests__/history-encryption-e2e.test.ts +51 -27
  148. package/src/__tests__/runner.test.ts +60 -2
  149. package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
  150. package/src/activities/__tests__/discover-mcp-server.hang.test.ts +5 -1
  151. package/src/activities/__tests__/discover-mcp-server.test.ts +90 -49
  152. package/src/activities/__tests__/workflow-event-activities.test.ts +28 -7
  153. package/src/activities/call-agent.ts +1 -1
  154. package/src/activities/discover-mcp-server.ts +27 -37
  155. package/src/activities/execute-cursor/__test-utils__/fold.ts +6 -4
  156. package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +1 -0
  157. package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +4 -29
  158. package/src/activities/execute-cursor/__tests__/hermetic/bypassed-tool-call.test.ts +120 -0
  159. package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +5 -5
  160. package/src/activities/execute-cursor/__tests__/hermetic/goldens/file-review-capture.status.json +0 -2
  161. package/src/activities/execute-cursor/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
  162. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.error-delta.status.json +0 -3
  163. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.output-first.status.json +0 -2
  164. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.status.json +0 -2
  165. package/src/activities/execute-cursor/__tests__/hermetic/goldens/stall.status.json +0 -2
  166. package/src/activities/execute-cursor/__tests__/hermetic/goldens/unattributed-hook-block.status.json +0 -3
  167. package/src/activities/execute-cursor/__tests__/hermetic/shell-output-delta.test.ts +9 -7
  168. package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +6 -1
  169. package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +8 -11
  170. package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +1 -3
  171. package/src/activities/execute-cursor/__tests__/translator.test.ts +68 -15
  172. package/src/activities/execute-cursor/approval-policy.ts +11 -36
  173. package/src/activities/execute-cursor/boundary-rows.ts +3 -3
  174. package/src/activities/execute-cursor/translator.ts +15 -32
  175. package/src/activities/execute-cursor/turn-stream.ts +4 -1
  176. package/src/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.ts +33 -2
  177. package/src/activities/execute-deep-agent/__tests__/adapter.test.ts +1 -0
  178. package/src/activities/execute-deep-agent/__tests__/hermetic/file-review-capture.test.ts +4 -0
  179. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/file-review-capture.status.json +1 -1
  180. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn1.status.json +1 -1
  181. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn2.status.json +1 -1
  182. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/recursion-limit.persisted.status.json +8 -73
  183. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
  184. package/src/activities/execute-deep-agent/__tests__/hermetic/inline-artifact-across-gate.test.ts +17 -0
  185. package/src/activities/execute-deep-agent/__tests__/hermetic/recursion-limit.test.ts +86 -26
  186. package/src/activities/execute-deep-agent/__tests__/hermetic/resolution-error.test.ts +9 -5
  187. package/src/activities/execute-deep-agent/__tests__/hermetic/thrown-turn-drains-publishes.test.ts +159 -0
  188. package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +84 -0
  189. package/src/activities/execute-deep-agent/__tests__/platform-route.test.ts +47 -10
  190. package/src/activities/execute-deep-agent/__tests__/streaming-side-effects.test.ts +34 -0
  191. package/src/activities/execute-deep-agent/__tests__/turn-stream.test.ts +13 -5
  192. package/src/activities/execute-deep-agent/inline-publisher.ts +47 -7
  193. package/src/activities/execute-deep-agent/platform-route.ts +109 -20
  194. package/src/activities/execute-deep-agent/streaming-side-effects.ts +27 -4
  195. package/src/activities/execute-deep-agent/translator.ts +5 -4
  196. package/src/activities/execute-deep-agent/turn-settle.ts +6 -6
  197. package/src/activities/execute-deep-agent/turn-setup.ts +7 -7
  198. package/src/activities/execute-deep-agent/turn-stream.ts +15 -22
  199. package/src/activities/execute-deep-agent/turn.ts +16 -7
  200. package/src/activities/workflow-event-activities.ts +2 -0
  201. package/src/config.ts +11 -0
  202. package/src/exit-backstop.ts +97 -0
  203. package/src/harness/README.md +5 -5
  204. package/src/harness/terminal-table.ts +13 -13
  205. package/src/harness/transcript/__tests__/builder.test.ts +93 -21
  206. package/src/harness/transcript/builder.ts +53 -30
  207. package/src/harness/transcript/events.ts +22 -27
  208. package/src/harness/turn-context.ts +5 -3
  209. package/src/main.ts +9 -5
  210. package/src/middleware/__tests__/execution-budget.test.ts +181 -103
  211. package/src/middleware/cost-advisory.ts +6 -3
  212. package/src/middleware/execution-budget.ts +60 -34
  213. package/src/middleware/index.ts +10 -7
  214. package/src/middleware/types.ts +6 -1
  215. package/src/runner-manager.ts +3 -2
  216. package/src/runner.ts +57 -15
  217. package/src/shared/__tests__/approval-policy.test.ts +41 -0
  218. package/src/shared/__tests__/artifact-storage.test.ts +1 -0
  219. package/src/shared/__tests__/connect-backfill.test.ts +15 -11
  220. package/src/shared/__tests__/mcp-resolver.test.ts +72 -10
  221. package/src/shared/__tests__/model-error.test.ts +71 -3
  222. package/src/shared/__tests__/platform-server-address.test.ts +122 -0
  223. package/src/shared/__tests__/tool-rounds.test.ts +64 -14
  224. package/src/shared/approval-policy.ts +57 -14
  225. package/src/shared/caller-identity.ts +4 -5
  226. package/src/shared/channel-attachment.ts +2 -2
  227. package/src/shared/connect-backfill.ts +5 -1
  228. package/src/shared/mcp-resolver.ts +11 -1
  229. package/src/shared/memory-attachment.ts +2 -2
  230. package/src/shared/model-error.ts +87 -9
  231. package/src/shared/platform-server-address.ts +123 -0
  232. package/src/shared/synthesized-attachment.ts +0 -12
  233. package/src/shared/tool-rounds.ts +70 -24
  234. package/src/shared/workspace/__tests__/stigmer-link.test.ts +107 -10
  235. package/src/shared/workspace/platform-dir.ts +19 -1
  236. package/src/shared/workspace/stigmer-link.ts +99 -29
  237. package/src/tools/__tests__/url-guard.test.ts +19 -1
  238. package/src/tools/url-guard.ts +9 -1
  239. package/src/worker.ts +12 -2
  240. package/src/workflow-engine/__tests__/golden-execution.test.ts +2 -1
  241. package/src/workflow-engine/__tests__/loader.test.ts +1 -1
  242. package/src/workflow-engine/__tests__/tasks/human-input.test.ts +133 -57
  243. package/src/workflow-engine/tasks/human-input.ts +92 -42
  244. package/src/workflow-engine/types.ts +16 -4
  245. package/src/workflows/call-agent-orchestrator.ts +7 -0
  246. package/src/workflows/engine-core.ts +18 -0
  247. package/src/workflows/execute-from-execution.ts +2 -1
  248. package/src/workflows/human-input-orchestrator.ts +15 -56
@@ -50,15 +50,17 @@
50
50
  * mid-stream is WAITING until the boundary runs), so they are indexed once
51
51
  * at construction and consumed on match (a declined row is a
52
52
  * closed gate and never matched).
53
- * - The GATE and the PROVENANCE. `gate` is the POLICY's word —
54
- * this MCP tool's merged policy, or this built-in's category, requires
55
- * approval — resolved exactly as the accumulator did, lease- and
56
- * bypass-blind; it is NOT the hook's verdict (the hook has arms this side
57
- * cannot evaluate: capture mode, grants, bypass, secret blocks — the
58
- * lesson behind dropping a parked-at-creation arm that had no producer).
59
- * A gated call that STARTED runs until the boundary parks
60
- * it through `approval_proposed`. Provenance is the shared read-side
61
- * `resolveApprovalProvenance` over the same policies and the run's leases.
53
+ * - The PROVENANCE, and never the hold. A row's provenance is the shared
54
+ * read-side `resolveApprovalProvenance` over the merged policies and the
55
+ * run's leases: which layer governs the call. Whether the call was HELD
56
+ * for a person's decision is not answered here, because at tool start
57
+ * nobody knows yet: the preToolUse hook decides, with arms this side
58
+ * cannot evaluate (capture mode, grants, bypass, secret blocks), and a
59
+ * call the hook denies is parked by the turn boundary through
60
+ * `approval_proposed` — the one writer of a row's approval fields on both
61
+ * harnesses (#1117). Until then this translator stamped the policy's
62
+ * category verdict at start, lease- and bypass-blind, so a leased,
63
+ * bypassed or capture-mode call that ran read "requires approval".
62
64
  * - The RESULT string (`tool-result.ts`): a failure's text is its `error`
63
65
  * alone.
64
66
  * - The SUB-AGENT: a `task` tool call opens a sub-agent keyed by
@@ -81,12 +83,11 @@
81
83
  import type { InteractionUpdate, SDKMessage } from "@cursor/sdk";
82
84
  import type { AgentMessage } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
83
85
  import { ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
84
- import type { GateAnswer, ToolStartedEvent, TranscriptEvent } from "../../harness/transcript/events.js";
86
+ import type { ToolStartedEvent, TranscriptEvent } from "../../harness/transcript/events.js";
85
87
  import { resolveApprovalProvenance, type MergedToolPolicy, type PolicySource } from "../../shared/approval-policy.js";
86
88
  import { utcTimestamp } from "../../shared/status.js";
87
89
  import { isDeclinedRow } from "../../shared/tool-row.js";
88
90
  import type { ToolApprovalCategory } from "../../shared/tool-kind.js";
89
- import { builtInRequiresApproval, getBuiltInApprovalMessage, lookupMcpToolPolicy, resolveApprovalMessage } from "./approval-policy.js";
90
91
  import { toolCallIdentityToken, toolIdentity, primaryToken } from "./approval-state.js";
91
92
  import { contentDigest } from "../../shared/file-tools.js";
92
93
  import { subAgentStepEvents } from "./sub-agent-steps.js";
@@ -97,7 +98,7 @@ type ToolCallEvent = Extract<SDKMessage, { type: "tool_call" }>;
97
98
  /** The delegation tool: the one tool name this translator knows, because its start opens a sub-agent. */
98
99
  const TASK_TOOL = "task";
99
100
 
100
- /** The run's approval posture the translator answers `gate` and `provenance` from. */
101
+ /** The run's approval posture the translator answers `provenance` from. */
101
102
  export interface CursorTranslatorOptions {
102
103
  /** The merged four-level MCP policy map (`mergeApprovalPolicies`). */
103
104
  readonly policies: ReadonlyMap<string, MergedToolPolicy>;
@@ -303,7 +304,7 @@ export class CursorTranslator {
303
304
  }
304
305
  this.announced.add(rowId);
305
306
  }
306
- out.push(this.toolStarted(rowId, name, mcp !== undefined, mcpServerSlug, input));
307
+ out.push(this.toolStarted(rowId, name, mcpServerSlug, input));
307
308
  // Deltas that arrived before the stream announced this call follow its
308
309
  // start, targeted at the row it resolved to.
309
310
  const heldMakers = this.held.get(event.call_id);
@@ -359,7 +360,7 @@ export class CursorTranslator {
359
360
  return seededId;
360
361
  }
361
362
 
362
- private toolStarted(callId: string, name: string, isMcp: boolean, mcpServerSlug: string, input: Record<string, unknown>): ToolStartedEvent {
363
+ private toolStarted(callId: string, name: string, mcpServerSlug: string, input: Record<string, unknown>): ToolStartedEvent {
363
364
  const { policies, leases } = this.options;
364
365
  const provenance: PolicySource | undefined = resolveApprovalProvenance(
365
366
  name,
@@ -368,7 +369,6 @@ export class CursorTranslator {
368
369
  leases.categories,
369
370
  leases.global,
370
371
  );
371
- const gate = this.gateOf(name, isMcp, mcpServerSlug, input);
372
372
  return {
373
373
  kind: "tool_started",
374
374
  callId,
@@ -376,26 +376,9 @@ export class CursorTranslator {
376
376
  input,
377
377
  mcpServerSlug,
378
378
  ...(provenance !== undefined ? { provenance } : {}),
379
- ...(gate !== undefined ? { gate } : {}),
380
379
  };
381
380
  }
382
381
 
383
- /**
384
- * The policy's word that this call requires approval, and the card's
385
- * message — or nothing for an ungated call. An MCP call (the `mcp` envelope,
386
- * whatever its provider field says) is governed by the merged policy map
387
- * alone; a built-in by its category.
388
- */
389
- private gateOf(name: string, isMcp: boolean, mcpServerSlug: string, input: Record<string, unknown>): GateAnswer | undefined {
390
- if (isMcp) {
391
- const policy = lookupMcpToolPolicy(name, mcpServerSlug, this.options.policies);
392
- return policy ? { message: resolveApprovalMessage(policy.approvalMessage, name, input) } : undefined;
393
- }
394
- if (!builtInRequiresApproval(name)) return undefined;
395
- const template = getBuiltInApprovalMessage(name);
396
- return template ? { message: resolveApprovalMessage(template, name, input) } : undefined;
397
- }
398
-
399
382
  // ── The delta queue ─────────────────────────────────────────────
400
383
 
401
384
  private rowIdOf(callId: string): string {
@@ -220,7 +220,10 @@ export async function consumeCursorTurnStream(
220
220
  // The stream event, then the deltas that arrived since the last one —
221
221
  // the enricher's order, kept so a completion the delta channel reported
222
222
  // in the same window as the stream's own defers to the stream's instant.
223
- for (const fact of translator.translate(event)) transcript.apply(fact);
223
+ // The stream event is one observation, folded at one instant (a
224
+ // completion with no running before it starts and finishes together);
225
+ // each drained delta is its own, and carries `observedAt` where it stamps.
226
+ transcript.applyObservation(translator.translate(event));
224
227
  for (const fact of translator.drainDeltas()) transcript.apply(fact);
225
228
 
226
229
  // First-denial stop (HITL clean pause). In CAPTURE mode this fires only for
@@ -146,6 +146,7 @@ export function hermeticDeepAgentConfig(env: HermeticEnvironment, checkpointerTy
146
146
  temporalConnection: {},
147
147
  stigmerBackendEndpoint: "http://localhost:7234",
148
148
  mcpBridgeEndpoint: null,
149
+ mcpPublicEndpoint: null,
149
150
  stigmerTokenRef: { current: null },
150
151
  cursorApiKey: "",
151
152
  workspaceRootDir: env.workspaceRootDir,
@@ -228,6 +229,8 @@ export class DeepAgentScenario {
228
229
  readonly config: Config,
229
230
  script: ScriptSelector,
230
231
  onTurn: DeepAgentScenarioOptions["onTurn"],
232
+ /** Tool calls an EARLIER message of the session settled, in its own record ({@link continueDeepAgentScenario}). */
233
+ settledEarlier: ReadonlySet<string> = new Set(),
231
234
  ) {
232
235
  this.model = new ScriptedModel(script, [], {
233
236
  onTurn: async (info) => {
@@ -236,8 +239,10 @@ export class DeepAgentScenario {
236
239
  // lets the producer run a turn ahead of the consumer. Wait for that
237
240
  // ground before ticking, so every stamp of the previous turn precedes
238
241
  // this turn's instant, whatever the persist's I/O took (the barrier's
239
- // own header on `ExecutionRecord` carries the whole reason).
240
- await this.record.whenToolCallsSettled(info.priorToolCallIds);
242
+ // own header on `ExecutionRecord` carries the whole reason). A
243
+ // follow-up message's first turn answers the previous message's last
244
+ // results, which that message's record settled, never this one.
245
+ await this.record.whenToolCallsSettled(info.priorToolCallIds.filter((id) => !settledEarlier.has(id)));
241
246
  this.clock.tick();
242
247
  if (onTurn && this.controls) await onTurn(info, this.controls);
243
248
  },
@@ -285,6 +290,32 @@ export function beginDeepAgentScenario(options: DeepAgentScenarioOptions): DeepA
285
290
  return scenario;
286
291
  }
287
292
 
293
+ /**
294
+ * The session's NEXT message on the scenario's thread: a new record and a new
295
+ * script over the same environment, runner config and checkpointer, WITHOUT
296
+ * the reset {@link beginDeepAgentScenario} does. On the `sqlite` checkpointer
297
+ * the new message therefore runs on the checkpoint the previous one left: the
298
+ * production shape of "send another message to continue".
299
+ */
300
+ export function continueDeepAgentScenario(
301
+ previous: DeepAgentScenario,
302
+ options: Pick<DeepAgentScenarioOptions, "record" | "script" | "onTurn" | "clientOverrides">,
303
+ ): DeepAgentScenario {
304
+ const settledEarlier = new Set(previous.record.toolCalls().map((tc) => tc.id));
305
+ const scenario = new DeepAgentScenario(
306
+ previous.env,
307
+ previous.clock,
308
+ options.record,
309
+ previous.config,
310
+ options.script,
311
+ options.onTurn,
312
+ settledEarlier,
313
+ );
314
+ bindScriptedModel(scenario.model);
315
+ bindHermeticClient(options.record.client(options.clientOverrides));
316
+ return scenario;
317
+ }
318
+
288
319
  export interface DeepAgentTurnOptions {
289
320
  /** The HITL cycle index (0 on the first turn); mints the change-set id. */
290
321
  readonly turnSeq?: number;
@@ -26,6 +26,7 @@ const CONFIG: Config = {
26
26
  temporalConnection: {},
27
27
  stigmerBackendEndpoint: "http://localhost:7234",
28
28
  mcpBridgeEndpoint: null,
29
+ mcpPublicEndpoint: null,
29
30
  stigmerTokenRef: { current: "tok" },
30
31
  cursorApiKey: "",
31
32
  workspaceRootDir: "/tmp/adapter-test",
@@ -27,6 +27,10 @@
27
27
  * a status the host could reach), and `goldens/file-review-capture.status.json`
28
28
  * pins the whole ledger beside Cursor's.
29
29
  *
30
+ * Golden hunk since #1443: the inline artifact's `createdAt` is t=1, the
31
+ * edit's finish, where it was t=2, the moment the publisher's upload
32
+ * returned (it reads the clock before its first await now).
33
+ *
30
34
  * Parent phase rows exercised beyond the plain arms: a `local_path` workspace
31
35
  * entry (mounted as-is in local mode, the agent rooted at the entry per
32
36
  * `provisionWorkspace`); capture-mode derivation on a git tree; the baseline
@@ -40,7 +40,7 @@
40
40
  "kind": "EXECUTION_ARTIFACT_KIND_FILE",
41
41
  "sizeBytes": "22",
42
42
  "storageKey": "artifacts/aex_hermetic_0001/notes.md",
43
- "createdAt": "2026-01-01T00:00:02.000Z",
43
+ "createdAt": "2026-01-01T00:00:01.000Z",
44
44
  "contentHash": "e4cdd09e8ca79e376a6ec5766d190754bc8ee4a2d6c5c7f4d86a9dc7fdf25a0f"
45
45
  }
46
46
  ],
@@ -57,7 +57,7 @@
57
57
  "kind": "EXECUTION_ARTIFACT_KIND_FILE",
58
58
  "sizeBytes": "32",
59
59
  "storageKey": "artifacts/aex_hermetic_0001/report.md",
60
- "createdAt": "2026-01-01T00:00:02.000Z",
60
+ "createdAt": "2026-01-01T00:00:01.000Z",
61
61
  "contentHash": "62eae4d09b2b051cf4ac5aa1fd70847b34c47994a1732ff999f47749e71b0441"
62
62
  }
63
63
  ],
@@ -67,7 +67,7 @@
67
67
  "kind": "EXECUTION_ARTIFACT_KIND_FILE",
68
68
  "sizeBytes": "32",
69
69
  "storageKey": "artifacts/aex_hermetic_0001/report.md",
70
- "createdAt": "2026-01-01T00:00:02.000Z",
70
+ "createdAt": "2026-01-01T00:00:01.000Z",
71
71
  "contentHash": "62eae4d09b2b051cf4ac5aa1fd70847b34c47994a1732ff999f47749e71b0441"
72
72
  }
73
73
  ],
@@ -200,89 +200,24 @@
200
200
  }
201
201
  ]
202
202
  },
203
- {
204
- "type": "MESSAGE_AI",
205
- "content": "Reading file 10.",
206
- "timestamp": "2026-01-01T00:00:11.000Z",
207
- "toolCalls": [
208
- {
209
- "id": "call-hermetic-loop-0010",
210
- "name": "read_file",
211
- "args": {
212
- "file_path": "/f10.md"
213
- },
214
- "result": "[{\"type\":\"text\",\"text\":\"Error: File '/f10.md' not found\"}]",
215
- "status": "TOOL_CALL_COMPLETED",
216
- "startedAt": "2026-01-01T00:00:11.000Z",
217
- "completedAt": "2026-01-01T00:00:11.000Z",
218
- "argsPreview": "{\"file_path\":\"/f10.md\"}",
219
- "toolKind": "TOOL_KIND_FILE_READ"
220
- }
221
- ]
222
- },
223
- {
224
- "type": "MESSAGE_AI",
225
- "content": "Reading file 11.",
226
- "timestamp": "2026-01-01T00:00:12.000Z",
227
- "toolCalls": [
228
- {
229
- "id": "call-hermetic-loop-0011",
230
- "name": "read_file",
231
- "args": {
232
- "file_path": "/f11.md"
233
- },
234
- "result": "[{\"type\":\"text\",\"text\":\"Error: File '/f11.md' not found\"}]",
235
- "status": "TOOL_CALL_COMPLETED",
236
- "startedAt": "2026-01-01T00:00:12.000Z",
237
- "completedAt": "2026-01-01T00:00:12.000Z",
238
- "argsPreview": "{\"file_path\":\"/f11.md\"}",
239
- "toolKind": "TOOL_KIND_FILE_READ"
240
- }
241
- ]
242
- },
243
- {
244
- "type": "MESSAGE_AI",
245
- "content": "Reading file 12.",
246
- "timestamp": "2026-01-01T00:00:13.000Z",
247
- "toolCalls": [
248
- {
249
- "id": "call-hermetic-loop-0012",
250
- "name": "read_file",
251
- "args": {
252
- "file_path": "/f12.md"
253
- },
254
- "result": "[{\"type\":\"text\",\"text\":\"Error: File '/f12.md' not found\"}]",
255
- "status": "TOOL_CALL_COMPLETED",
256
- "startedAt": "2026-01-01T00:00:13.000Z",
257
- "completedAt": "2026-01-01T00:00:13.000Z",
258
- "argsPreview": "{\"file_path\":\"/f12.md\"}",
259
- "toolKind": "TOOL_KIND_FILE_READ"
260
- }
261
- ]
262
- },
263
- {
264
- "type": "MESSAGE_AI",
265
- "content": "Reading file 13.",
266
- "timestamp": "2026-01-01T00:00:14.000Z"
267
- },
268
203
  {
269
204
  "type": "MESSAGE_SYSTEM",
270
205
  "content": "The agent reached the tool-call limit for this message. Work completed so far has been saved. Send another message to continue where the agent left off.",
271
- "timestamp": "2026-01-01T00:00:14.000Z"
206
+ "timestamp": "2026-01-01T00:00:10.000Z"
272
207
  }
273
208
  ],
274
209
  "phase": "EXECUTION_TERMINATED",
275
210
  "error": "Agent reached the tool-call limit for this message. Send another message to continue.",
276
211
  "startedAt": "2026-01-01T00:00:00.000Z",
277
- "completedAt": "2026-01-01T00:00:14.000Z",
212
+ "completedAt": "2026-01-01T00:00:10.000Z",
278
213
  "streamingUsage": {
279
- "inputTokens": "14000",
280
- "outputTokens": "280",
281
- "totalTokens": "14280",
282
- "turnCount": 14,
283
- "estimatedCostUsd": 0.015400000000000002,
214
+ "inputTokens": "10000",
215
+ "outputTokens": "200",
216
+ "totalTokens": "10200",
217
+ "turnCount": 10,
218
+ "estimatedCostUsd": 0.011000000000000001,
284
219
  "model": "claude-haiku-4.5",
285
- "observedAt": "2026-01-01T00:00:14.000Z",
220
+ "observedAt": "2026-01-01T00:00:10.000Z",
286
221
  "requestedServiceTier": "SERVICE_TIER_STANDARD",
287
222
  "requestedThinkingMode": "THINKING_MODE_DISABLED"
288
223
  },
@@ -12,7 +12,7 @@
12
12
  }
13
13
  ],
14
14
  "phase": "EXECUTION_FAILED",
15
- "error": "Execution failed: [Error] hermetic fault injected at getAgent",
15
+ "error": "[Error] hermetic fault injected at getAgent",
16
16
  "startedAt": "2026-01-01T00:00:00.000Z",
17
17
  "completedAt": "2026-01-01T00:00:00.000Z"
18
18
  }
@@ -24,6 +24,11 @@
24
24
  * this pair of goldens (`inline-artifact.turn1`, `inline-artifact.turn2`) is
25
25
  * what proves it did.
26
26
  *
27
+ * Turn 2 completes, so its post-stream safety net walks the seeded history
28
+ * and asks for `report.md` again. The publisher re-reads it, finds the bytes
29
+ * the seeded row lists, and uploads nothing: the store sees turn 1's write
30
+ * only (stigmer/stigmer#1448).
31
+ *
27
32
  * Also recorded (a shape fact, as found): the write's tool row
28
33
  * shows `requiresApproval: true` with `approvalPolicySource` BUILTIN_CATEGORY
29
34
  * — the category policy's verdict — while the row is COMPLETED because
@@ -50,6 +55,12 @@
50
55
  * to persist "0 files" beside a one-file change set. The runtime now
51
56
  * refreshes the strip on every write, the terminal one included.
52
57
  *
58
+ * Golden hunk since #1443, in both goldens: the artifact's `createdAt` is
59
+ * t=1, the write's finish, where it was t=2. The publisher read the clock
60
+ * after its upload, which runs beside the stream, so the stamp landed on
61
+ * either side of the next model turn's tick and flaked under the full
62
+ * suite; it reads the clock before its first await now.
63
+ *
53
64
  * Regenerate ONLY after a deliberate behavior change:
54
65
  * npx vitest run src/activities/execute-deep-agent/__tests__/hermetic -u
55
66
  */
@@ -88,6 +99,7 @@ import {
88
99
  runDeepAgentTurn,
89
100
  } from "../../__test-utils__/hermetic-deep-agent.js";
90
101
  import { CLOSING_TURN, EXECUTE_CALL_A } from "../../__test-utils__/hitl-script.js";
102
+ import { LocalArtifactStorage } from "../../../../shared/artifact-storage.js";
91
103
 
92
104
  const REPORT = "report.md";
93
105
  const REPORT_BODY = "# Report\n\nAll fixtures nominal.\n";
@@ -148,6 +160,7 @@ describe("ExecuteDeepAgent hermetic — inline artifact across a gate (sqlite)",
148
160
  expect(existsSync(join(env.artifactPath, run1.artifacts[0].storageKey)), "the bytes reached the store").toBe(true);
149
161
  const writeRow = record.toolCalls().find((tc) => tc.id === WRITE_CALL_ID)!;
150
162
  expect(writeRow.status, "capture mode let the write flow").toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
163
+ expect(run1.artifacts[0].createdAt, "the artifact is stamped at its write's finish").toBe(writeRow.completedAt);
151
164
  expect(record.waitingToolCalls().map((tc) => tc.id)).toEqual([EXECUTE_CALL_A.id]);
152
165
  await expect(statusJson(run1)).toMatchFileSnapshot("./goldens/inline-artifact.turn1.status.json");
153
166
 
@@ -155,13 +168,17 @@ describe("ExecuteDeepAgent hermetic — inline artifact across a gate (sqlite)",
155
168
  expect(record.decideWaitingToolCalls(ApprovalAction.APPROVE, DECIDED_AT)).toBe(1);
156
169
 
157
170
  // ── Turn 2 ───────────────────────────────────────────────────────────────
171
+ const uploads = vi.spyOn(LocalArtifactStorage.prototype, "upload");
158
172
  const turn2 = await runDeepAgentTurn(scenario, { turnSeq: 1 });
173
+ const turn2Keys = uploads.mock.calls.map(([key]) => key);
174
+ uploads.mockRestore();
159
175
  expect(turn2.outcome.kind).toBe("returned");
160
176
  expect((turn2.outcome as { value: Record<string, unknown> }).value.phase).toBe("EXECUTION_COMPLETED");
161
177
  const final = record.lastFullStatus!;
162
178
  expect(final.artifacts.map((a) => [a.name, a.storageKey, a.contentHash]), "the seed carried the artifact").toEqual(
163
179
  run1.artifacts.map((a) => [a.name, a.storageKey, a.contentHash]),
164
180
  );
181
+ expect(turn2Keys, "turn 2 does not upload the unchanged report again").not.toContain(run1.artifacts[0].storageKey);
165
182
  expect(final.messages.flatMap((m) => m.toolCalls).map((tc) => [tc.id, tc.status])).toEqual([
166
183
  [WRITE_CALL_ID, ToolCallStatus.TOOL_CALL_COMPLETED],
167
184
  [EXECUTE_CALL_A.id, ToolCallStatus.TOOL_CALL_COMPLETED],
@@ -3,13 +3,15 @@
3
3
  * model that keeps calling tools.
4
4
  *
5
5
  * What the arm does: `max_tool_rounds = 10` (the clamp floor,
6
- * `shared/tool-rounds.ts` MIN_TOOL_ROUNDS) becomes LangGraph's
7
- * `recursionLimit = 60` (SUPER_STEPS_PER_ROUND); the script proposes a
8
- * DISTINCT `read_file` every round (distinct ids and paths, so the
9
- * loop-detection middleware's "repetitive pattern" warning does not end the
10
- * run first); the graph throws `GraphRecursionError`; the adapter's turn
11
- * classifies it as the `tool_call_limit` outcome (`shared/tool-rounds.ts`
12
- * `isGraphRecursionError`), and the runtime's `toolCallLimitArm` writes
6
+ * `shared/tool-rounds.ts` MIN_TOOL_ROUNDS) is the execution budget
7
+ * middleware's round limit; the script proposes a DISTINCT `read_file` every
8
+ * round (distinct ids and paths, so the loop-detection middleware's
9
+ * "repetitive pattern" warning does not end the run first); after round 10's
10
+ * results the middleware throws `ToolRoundLimitError` in place of round 11's
11
+ * model call; the adapter's turn classifies it, through LangChain's
12
+ * `MiddlewareError` wrapping, as the `tool_call_limit` outcome
13
+ * (`shared/tool-rounds.ts` `isToolCallBudgetStop`), and the runtime's
14
+ * `toolCallLimitArm` writes
13
15
  * TERMINATED with the cross-repo prefix `TOOL_CALL_LIMIT_ERROR_PREFIX`
14
16
  * (matched with `startsWith` by stigmer-cloud's `reply-extractor.ts`) and
15
17
  * its row, PERSISTS it, and returns the slim.
@@ -29,16 +31,26 @@
29
31
  * empty message once and its `currentAiMessage[""]` then caught every later
30
32
  * row.
31
33
  *
32
- * How many rounds fit is the middleware stack's shape, not the knob's
33
- * (ruled 2026-09-13, #1096): LangChain makes every
34
- * middleware hook its own graph node, and `max_tool_rounds` becomes a
35
- * super-step budget at the ×6 floor estimate, so a node fewer per round
36
- * means more rounds within the same 60 steps. When `graceful-stop.ts` (an
37
- * `afterModel` node) was deleted in #1096, this golden went from 11 tool
38
- * calls to 13 before TERMINATED — the only hunk, the copy unchanged. That the
39
- * knob counts super-steps and not rounds is recorded as #1113
40
- * (a round counter in the budget middleware would make it mean what it
41
- * says); this file pins what the stack delivers today.
34
+ * How many rounds run is the knob's, exactly (#1113): TEN tool rows, on any
35
+ * middleware stack. Until #1113 the knob was LangGraph's recursion limit at
36
+ * 10 × 6 super-steps, and since LangChain makes every middleware hook its own
37
+ * graph node, the rows moved with the stack: 11 until #1096 deleted
38
+ * `graceful-stop.ts` (an `afterModel` node), 13 after it. #1113 moved this
39
+ * golden from 13 rows to 10, the copy unchanged. It also dropped the golden's
40
+ * last AI message, "Reading file 13.": the step limit had cut that fourteenth
41
+ * model call's proposal before its tool ran, so the transcript ended on a
42
+ * call that never executed. The round stop replaces the model call instead,
43
+ * so the transcript ends on round 10's results, and usage counts ten model
44
+ * calls, not fourteen.
45
+ *
46
+ * The second arm is the copy's promise, "send another message to continue":
47
+ * on the durable (`sqlite`) checkpointer, a follow-up message on the same
48
+ * thread starts from the checkpoint the stop left. The stop is a throw inside
49
+ * the model node rather than a limit between super-steps, so this is proven,
50
+ * not assumed: the follow-up's first model turn sees all ten rounds in its
51
+ * transcript (the scripted model indexes its script by the rounds the
52
+ * transcript holds), the run completes, and the budget is the new message's
53
+ * own.
42
54
  *
43
55
  * Regenerate ONLY after a deliberate behavior change:
44
56
  * npx vitest run src/activities/execute-deep-agent/__tests__/hermetic -u
@@ -65,14 +77,25 @@ import { stubRegistryFetch } from "../../../../__test-utils__/model-registry-fix
65
77
  import { MIN_TOOL_ROUNDS } from "../../../../shared/tool-rounds.js";
66
78
  import {
67
79
  beginDeepAgentScenario,
80
+ continueDeepAgentScenario,
68
81
  deepAgentExecutionRecord,
69
82
  runDeepAgentTurn,
70
83
  } from "../../__test-utils__/hermetic-deep-agent.js";
84
+ import type { ScriptedTurn } from "../../__test-utils__/scripted-model.js";
71
85
  import { TOOL_CALL_LIMIT_ERROR_PREFIX } from "../../../../shared/tool-rounds.js";
72
86
 
73
87
  /** More rounds than the budget allows; the graph stops the script, not the other way round. */
74
88
  const SCRIPTED_ROUNDS = 40;
75
89
 
90
+ /** The model that keeps calling tools: a distinct `read_file` per round. */
91
+ function readingRounds(): ScriptedTurn[] {
92
+ return Array.from({ length: SCRIPTED_ROUNDS }, (_, i) => ({
93
+ text: `Reading file ${i}.`,
94
+ toolCalls: [{ id: `call-hermetic-loop-${String(i).padStart(4, "0")}`, name: "read_file", args: { file_path: `/f${i}.md` } }],
95
+ usage: { inputTokens: 1_000, outputTokens: 20 },
96
+ }));
97
+ }
98
+
76
99
  describe("ExecuteDeepAgent hermetic — tool-call budget exhausted", () => {
77
100
  let env: HermeticEnvironment;
78
101
  let registry: ReturnType<typeof stubRegistryFetch>;
@@ -97,13 +120,7 @@ describe("ExecuteDeepAgent hermetic — tool-call budget exhausted", () => {
97
120
  env,
98
121
  clock,
99
122
  record,
100
- script: () => ({
101
- turns: Array.from({ length: SCRIPTED_ROUNDS }, (_, i) => ({
102
- text: `Reading file ${i}.`,
103
- toolCalls: [{ id: `call-hermetic-loop-${String(i).padStart(4, "0")}`, name: "read_file", args: { file_path: `/f${i}.md` } }],
104
- usage: { inputTokens: 1_000, outputTokens: 20 },
105
- })),
106
- }),
123
+ script: () => ({ turns: readingRounds() }),
107
124
  });
108
125
 
109
126
  // ── Act ──────────────────────────────────────────────────────────────────
@@ -125,8 +142,7 @@ describe("ExecuteDeepAgent hermetic — tool-call budget exhausted", () => {
125
142
  expect(persisted.error, "the event count left the copy").not.toMatch(/\d+ events?/);
126
143
  expect(persisted.completedAt).not.toBe("");
127
144
  const rows = record.toolCalls();
128
- expect(rows.length, "fewer rounds ran than were scripted: the budget stopped the graph").toBeLessThan(SCRIPTED_ROUNDS);
129
- expect(rows.length).toBeGreaterThanOrEqual(MIN_TOOL_ROUNDS);
145
+ expect(rows.length, "exactly max_tool_rounds rounds ran: the budget counts rounds, not graph steps").toBe(MIN_TOOL_ROUNDS);
130
146
  // The message boundary: every row sits on the message whose text proposed it
131
147
  // — one "Reading file N." message per row, no empty host (before #1097 all
132
148
  // thirteen rows sat on ONE empty message).
@@ -144,4 +160,48 @@ describe("ExecuteDeepAgent hermetic — tool-call budget exhausted", () => {
144
160
  const json = JSON.stringify(toJson(AgentExecutionStatusSchema, persisted), null, 2) + "\n";
145
161
  await expect(json).toMatchFileSnapshot("./goldens/recursion-limit.persisted.status.json");
146
162
  });
163
+
164
+ it("continues on the next message from the checkpoint the stop left (sqlite)", async () => {
165
+ // ── Arrange: the first message exhausts its budget ───────────────────────
166
+ const first = beginDeepAgentScenario({
167
+ env,
168
+ clock,
169
+ record: deepAgentExecutionRecord({ message: "Keep reading files.", maxToolRounds: MIN_TOOL_ROUNDS }),
170
+ checkpointer: "sqlite",
171
+ script: () => ({ turns: readingRounds() }),
172
+ });
173
+ await runDeepAgentTurn(first);
174
+ expect(first.record.lastFullStatus!.phase).toBe(ExecutionPhase.EXECUTION_TERMINATED);
175
+ expect(first.record.toolCalls()).toHaveLength(MIN_TOOL_ROUNDS);
176
+
177
+ // The follow-up's script is indexed by the rounds its transcript holds: a
178
+ // model that saw the ten rounds answers in text; one that lost them would
179
+ // play turn 0 and read a file.
180
+ const LOST_THREAD: ScriptedTurn = {
181
+ text: "Starting over.",
182
+ toolCalls: [{ id: "call-hermetic-lost-thread", name: "read_file", args: { file_path: "/lost.md" } }],
183
+ };
184
+ const PICKED_UP: ScriptedTurn = { text: "Picking up where I left off: all ten files are read." };
185
+ const roundsSeen: number[] = [];
186
+ const record = deepAgentExecutionRecord({ message: "Continue.", maxToolRounds: MIN_TOOL_ROUNDS });
187
+ const next = continueDeepAgentScenario(first, {
188
+ record,
189
+ script: () => ({ turns: [...Array.from({ length: MIN_TOOL_ROUNDS }, () => LOST_THREAD), PICKED_UP] }),
190
+ onTurn: (info) => {
191
+ roundsSeen.push(info.round);
192
+ },
193
+ });
194
+
195
+ // ── Act ──────────────────────────────────────────────────────────────────
196
+ const invocation = await runDeepAgentTurn(next);
197
+
198
+ // ── Assert ───────────────────────────────────────────────────────────────
199
+ expect(invocation.outcome.kind).toBe("returned");
200
+ expect(roundsSeen, "the follow-up's one model turn saw the ten rounds the stop checkpointed").toEqual([MIN_TOOL_ROUNDS]);
201
+ expect(record.persistedPhases).toEqual([ExecutionPhase.EXECUTION_IN_PROGRESS, ExecutionPhase.EXECUTION_COMPLETED]);
202
+ const persisted = record.lastFullStatus!;
203
+ expect(persisted.error).toBe("");
204
+ const answer = persisted.messages.filter((m) => m.type === MessageType.MESSAGE_AI).at(-1);
205
+ expect(answer?.content).toBe(PICKED_UP.text);
206
+ });
147
207
  });
@@ -5,7 +5,7 @@
5
5
  * Invariant pinned: the runtime's blueprint phase throws, the runtime's
6
6
  * catch takes its generic-error arm (`terminal-table.ts` `unexpectedErrorArm`),
7
7
  * persists ONE EXECUTION_FAILED status carrying
8
- * `status.error = "Execution failed: [Error] <message>"` and two system rows
8
+ * `status.error = "[Error] <message>"` and two system rows
9
9
  * — the boilerplate "Internal system error occurred. Please contact support
10
10
  * if this issue persists." and `"Error details: [Error] <message>"` — and
11
11
  * RETURNS the slim status (the workflow reads the phase; a throw would make
@@ -14,10 +14,14 @@
14
14
  *
15
15
  * The row shape is one of the alignments #1096 made on purpose: the
16
16
  * orchestrator wrote one row `"Error: [Error] <message>"`; the runtime's arm
17
- * writes the boilerplate plus the details row. `status.error` itself did
18
- * not move (the conformance suite pins it). The setup labels are the
17
+ * writes the boilerplate plus the details row. The setup labels are the
19
18
  * runtime's resolution phases'.
20
19
  *
20
+ * Golden hunk since stigmer#1122: `status.error` is `[Error] <message>`, the
21
+ * details row's text, where it was `Execution failed: [Error] <message>`.
22
+ * This arm alone framed the error; every other FAILED arm, and this
23
+ * harness's own thrown-turn classification, writes the failure unframed.
24
+ *
21
25
  * Carried from `index.test.ts` ("returns EXECUTION_FAILED status when setup
22
26
  * fails", "includes error message in failed status", "always returns a
23
27
  * serializable result").
@@ -70,7 +74,7 @@ describe("ExecuteDeepAgent hermetic — resolution error", () => {
70
74
  env.dispose();
71
75
  });
72
76
 
73
- it("fails with the fault's message in status.error and one system row, and returns", async () => {
77
+ it("fails with the fault's message in status.error and two system rows, and returns", async () => {
74
78
  // ── Arrange ──────────────────────────────────────────────────────────────
75
79
  const record = deepAgentExecutionRecord({ message: "Never reaches a model." });
76
80
  const scenario = beginDeepAgentScenario({
@@ -101,7 +105,7 @@ describe("ExecuteDeepAgent hermetic — resolution error", () => {
101
105
 
102
106
  // ── Assert: the copy ─────────────────────────────────────────────────────
103
107
  const final = record.lastFullStatus!;
104
- expect(final.error).toBe(`Execution failed: [Error] ${CONTROL_PLANE_FAULT}`);
108
+ expect(final.error).toBe(`[Error] ${CONTROL_PLANE_FAULT}`);
105
109
  expect(final.messages.filter((m) => m.type === MessageType.MESSAGE_SYSTEM).map((m) => m.content)).toEqual([
106
110
  "Internal system error occurred. Please contact support if this issue persists.",
107
111
  `Error details: [Error] ${CONTROL_PLANE_FAULT}`,