@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
@@ -1,5 +1,8 @@
1
1
  import { describe, it, expect, vi } from "vitest";
2
- import { executeHumanInputTask } from "../../tasks/human-input.js";
2
+ import {
3
+ HUMAN_INPUT_FAIL_TIMEOUT_RESOLVED_PATCH,
4
+ executeHumanInputTask,
5
+ } from "../../tasks/human-input.js";
3
6
  import { createState } from "../../state.js";
4
7
  import { evaluateExpressionBatch } from "../../expression.js";
5
8
  import { TaskStatusAccumulator } from "../../task-status-accumulator.js";
@@ -8,6 +11,7 @@ import type {
8
11
  TaskExecutionContext,
9
12
  HumanInputExecutionConfig,
10
13
  HumanInputResult,
14
+ HumanInputTimeout,
11
15
  AwaitHumanInputFn,
12
16
  EmitEventsFn,
13
17
  ExpressionEvaluator,
@@ -17,12 +21,16 @@ import type {
17
21
 
18
22
  const notAvailable = () => { throw new Error("not available in test"); };
19
23
 
24
+ /** What the workflow layer reports when the gate's timer wins. */
25
+ const TIMED_OUT: HumanInputTimeout = { timedOut: true };
26
+
20
27
  interface CtxOptions {
21
28
  readonly awaitFn?: AwaitHumanInputFn;
22
29
  readonly emitFn?: EmitEventsFn;
23
30
  readonly evaluateExpressions?: ExpressionEvaluator;
24
31
  readonly promoteTaskOutput?: PromoteTaskOutputFn;
25
32
  readonly taskStatusAccumulator?: TaskStatusAccumulator;
33
+ readonly isPatched?: (changeId: string) => boolean;
26
34
  }
27
35
 
28
36
  function makeCtx(
@@ -45,6 +53,7 @@ function makeCtx(
45
53
  emitEvents: emitFn,
46
54
  promoteTaskOutput: options?.promoteTaskOutput,
47
55
  taskStatusAccumulator: options?.taskStatusAccumulator,
56
+ isPatched: options?.isPatched,
48
57
  };
49
58
  }
50
59
 
@@ -156,11 +165,11 @@ describe("executeHumanInputTask", () => {
156
165
  expect(capturedConfig!.timeoutSeconds).toBe(0);
157
166
  });
158
167
 
159
- it("passes onTimeout policy from config", async () => {
168
+ it("hands the wait only the signal and the timeout; the policy stays with the kernel", async () => {
160
169
  let capturedConfig: HumanInputExecutionConfig | undefined;
161
170
  const awaitFn: AwaitHumanInputFn = async (config) => {
162
171
  capturedConfig = config;
163
- return { outcome: "approve", auto_resolved: true, reason: "timeout" };
172
+ return { outcome: "approve" };
164
173
  };
165
174
 
166
175
  const taskDef: HumanInputTaskDef = {
@@ -170,31 +179,27 @@ describe("executeHumanInputTask", () => {
170
179
 
171
180
  await executeHumanInputTask(taskDef, "t", createState(), makeCtx(awaitFn));
172
181
 
173
- expect(capturedConfig!.onTimeout).toBe("approve");
182
+ expect(capturedConfig).toEqual({ signalName: "human_input_t", timeoutSeconds: 60 });
174
183
  });
175
184
 
176
- it("defaults onTimeout to 'fail' when not set", async () => {
177
- let capturedConfig: HumanInputExecutionConfig | undefined;
178
- const awaitFn: AwaitHumanInputFn = async (config) => {
179
- capturedConfig = config;
180
- return { outcome: "approve" };
181
- };
182
-
185
+ it("fails the task on a timeout when onTimeout is not set", async () => {
183
186
  const taskDef: HumanInputTaskDef = {
184
187
  kind: "human_input",
185
188
  humanInput: { prompt: "Review", timeout: 60 },
186
189
  };
187
190
 
188
- await executeHumanInputTask(taskDef, "t", createState(), makeCtx(awaitFn));
189
-
190
- expect(capturedConfig!.onTimeout).toBe("fail");
191
+ await expect(
192
+ executeHumanInputTask(taskDef, "t", createState(), makeCtx(async () => TIMED_OUT)),
193
+ ).rejects.toThrow("human_input task timed out waiting for signal 'human_input_t'");
191
194
  });
192
195
  });
193
196
 
194
197
  describe("error propagation", () => {
195
- it("propagates timeout failure errors from orchestrator", async () => {
198
+ it("propagates an error from the wait untouched and logs no resolution", async () => {
199
+ const emitted: WorkflowEventDescriptor[][] = [];
200
+ const emitFn: EmitEventsFn = async (events) => { emitted.push(events); };
196
201
  const awaitFn: AwaitHumanInputFn = async () => {
197
- throw new Error("human_input task timed out");
202
+ throw new Error("workflow cancelled");
198
203
  };
199
204
 
200
205
  const taskDef: HumanInputTaskDef = {
@@ -203,18 +208,86 @@ describe("executeHumanInputTask", () => {
203
208
  };
204
209
 
205
210
  await expect(
206
- executeHumanInputTask(taskDef, "t", createState(), makeCtx(awaitFn)),
211
+ executeHumanInputTask(taskDef, "t", createState(), makeCtx(awaitFn, emitFn)),
212
+ ).rejects.toThrow("workflow cancelled");
213
+ expect(emitted.flat().map((e) => e.type)).toEqual(["approval_requested"]);
214
+ });
215
+ });
216
+
217
+ // A fail-policy timeout ends the gate without a decision. It still closes
218
+ // the approval in the event log, so a reader never sees a request left
219
+ // open, and then fails the task with the error it always raised
220
+ // (stigmer/stigmer#1442).
221
+ describe("fail-policy timeout", () => {
222
+ const taskDef: HumanInputTaskDef = {
223
+ kind: "human_input",
224
+ humanInput: { prompt: "Review", timeout: 10, onTimeout: "fail" },
225
+ };
226
+
227
+ it("logs one approval_resolved with auto_resolved and no outcome, then fails the task", async () => {
228
+ const emitted: WorkflowEventDescriptor[][] = [];
229
+ const emitFn: EmitEventsFn = async (events) => { emitted.push(events); };
230
+ const state = createState();
231
+
232
+ await expect(
233
+ executeHumanInputTask(taskDef, "gate", state, makeCtx(async () => TIMED_OUT, emitFn)),
234
+ ).rejects.toThrow("human_input task timed out waiting for signal 'human_input_gate'");
235
+
236
+ const resolutions = emitted.flat().filter((e) => e.type === "approval_resolved");
237
+ expect(resolutions).toHaveLength(1);
238
+ expect(resolutions[0]).toMatchObject({
239
+ taskName: "gate",
240
+ outcome: "",
241
+ resolvedBy: "",
242
+ resolvedByActor: undefined,
243
+ comment: "",
244
+ autoResolved: true,
245
+ });
246
+ expect(state.data.gate, "a failed gate records no decision").toBeUndefined();
247
+ });
248
+
249
+ it("asks the patch gate by its pinned id before logging", async () => {
250
+ const isPatched = vi.fn(() => true);
251
+
252
+ await expect(
253
+ executeHumanInputTask(taskDef, "gate", createState(), makeCtx(async () => TIMED_OUT, undefined, { isPatched })),
207
254
  ).rejects.toThrow("timed out");
255
+
256
+ expect(HUMAN_INPUT_FAIL_TIMEOUT_RESOLVED_PATCH).toBe("human-input-fail-timeout-resolved");
257
+ expect(isPatched).toHaveBeenCalledWith(HUMAN_INPUT_FAIL_TIMEOUT_RESOLVED_PATCH);
258
+ });
259
+
260
+ it("replays a history from before the patch in the old order: no resolution, the same failure", async () => {
261
+ const emitted: WorkflowEventDescriptor[][] = [];
262
+ const emitFn: EmitEventsFn = async (events) => { emitted.push(events); };
263
+
264
+ await expect(
265
+ executeHumanInputTask(
266
+ taskDef, "gate", createState(),
267
+ makeCtx(async () => TIMED_OUT, emitFn, { isPatched: () => false }),
268
+ ),
269
+ ).rejects.toThrow("human_input task timed out waiting for signal 'human_input_gate'");
270
+
271
+ expect(emitted.flat().map((e) => e.type)).toEqual(["approval_requested"]);
272
+ });
273
+
274
+ it("never asks the patch gate for a gate a policy or a reviewer decided", async () => {
275
+ const isPatched = vi.fn(() => true);
276
+ const approveOnTimeout: HumanInputTaskDef = {
277
+ kind: "human_input",
278
+ humanInput: { prompt: "Review", timeout: 10, onTimeout: "approve" },
279
+ };
280
+
281
+ await executeHumanInputTask(approveOnTimeout, "a", createState(), makeCtx(async () => TIMED_OUT, undefined, { isPatched }));
282
+ await executeHumanInputTask(taskDef, "b", createState(), makeCtx(async () => ({ outcome: "approve" }), undefined, { isPatched }));
283
+
284
+ expect(isPatched).not.toHaveBeenCalled();
208
285
  });
209
286
  });
210
287
 
211
288
  describe("auto-resolved results", () => {
212
289
  it("handles auto-approve timeout result", async () => {
213
- const awaitFn: AwaitHumanInputFn = async () => ({
214
- outcome: "approve",
215
- auto_resolved: true,
216
- reason: "timeout",
217
- });
290
+ const awaitFn: AwaitHumanInputFn = async () => TIMED_OUT;
218
291
 
219
292
  const taskDef: HumanInputTaskDef = {
220
293
  kind: "human_input",
@@ -229,11 +302,7 @@ describe("executeHumanInputTask", () => {
229
302
  });
230
303
 
231
304
  it("handles auto-deny timeout result", async () => {
232
- const awaitFn: AwaitHumanInputFn = async () => ({
233
- outcome: "deny",
234
- auto_resolved: true,
235
- reason: "timeout",
236
- });
305
+ const awaitFn: AwaitHumanInputFn = async () => TIMED_OUT;
237
306
 
238
307
  const taskDef: HumanInputTaskDef = {
239
308
  kind: "human_input",
@@ -260,11 +329,7 @@ describe("executeHumanInputTask", () => {
260
329
  ];
261
330
 
262
331
  it("maps timeout auto-approve to the FIRST outcome and routes its then", async () => {
263
- const awaitFn: AwaitHumanInputFn = async () => ({
264
- outcome: "approve",
265
- auto_resolved: true,
266
- reason: "timeout",
267
- });
332
+ const awaitFn: AwaitHumanInputFn = async () => TIMED_OUT;
268
333
 
269
334
  const taskDef: HumanInputTaskDef = {
270
335
  kind: "human_input",
@@ -288,11 +353,7 @@ describe("executeHumanInputTask", () => {
288
353
  });
289
354
 
290
355
  it("maps timeout auto-deny to the LAST outcome", async () => {
291
- const awaitFn: AwaitHumanInputFn = async () => ({
292
- outcome: "deny",
293
- auto_resolved: true,
294
- reason: "timeout",
295
- });
356
+ const awaitFn: AwaitHumanInputFn = async () => TIMED_OUT;
296
357
 
297
358
  const taskDef: HumanInputTaskDef = {
298
359
  kind: "human_input",
@@ -310,11 +371,7 @@ describe("executeHumanInputTask", () => {
310
371
  // — the ordinary name lookup finds the "escalate" outcome and routes its
311
372
  // `then`, regardless of the outcome's position in the list.
312
373
  it("resolves timeout escalate to the escalate-named outcome and routes its then", async () => {
313
- const awaitFn: AwaitHumanInputFn = async () => ({
314
- outcome: "escalate",
315
- auto_resolved: true,
316
- reason: "timeout",
317
- });
374
+ const awaitFn: AwaitHumanInputFn = async () => TIMED_OUT;
318
375
 
319
376
  const escalateOutcomes = [
320
377
  { name: "proceed", label: "Proceed", then: "deployStep" },
@@ -347,11 +404,7 @@ describe("executeHumanInputTask", () => {
347
404
  it("reports the mapped outcome on the approval_resolved event", async () => {
348
405
  const emitted: WorkflowEventDescriptor[][] = [];
349
406
  const emitFn: EmitEventsFn = async (events) => { emitted.push(events); };
350
- const awaitFn: AwaitHumanInputFn = async () => ({
351
- outcome: "approve",
352
- auto_resolved: true,
353
- reason: "timeout",
354
- });
407
+ const awaitFn: AwaitHumanInputFn = async () => TIMED_OUT;
355
408
 
356
409
  const taskDef: HumanInputTaskDef = {
357
410
  kind: "human_input",
@@ -385,11 +438,7 @@ describe("executeHumanInputTask", () => {
385
438
  });
386
439
 
387
440
  it("keeps plain approve/deny for binary gates without custom outcomes", async () => {
388
- const awaitFn: AwaitHumanInputFn = async () => ({
389
- outcome: "approve",
390
- auto_resolved: true,
391
- reason: "timeout",
392
- });
441
+ const awaitFn: AwaitHumanInputFn = async () => TIMED_OUT;
393
442
 
394
443
  const taskDef: HumanInputTaskDef = {
395
444
  kind: "human_input",
@@ -474,10 +523,40 @@ describe("executeHumanInputTask", () => {
474
523
  expect(resolved.outcome).toBe("reject");
475
524
  expect(resolved.resolvedBy).toBe("carol");
476
525
  expect(resolved.resolvedByActor).toBeUndefined();
526
+ expect(resolved.comment).toBe("");
477
527
  expect(resolved.autoResolved).toBe(false);
478
528
  expect(resolved.waitDurationMs).toBeGreaterThanOrEqual(0);
479
529
  });
480
530
 
531
+ it("carries the reviewer's comment into approval_resolved and the task output", async () => {
532
+ const emitted: WorkflowEventDescriptor[][] = [];
533
+ const emitFn: EmitEventsFn = async (events) => { emitted.push(events); };
534
+ const awaitFn: AwaitHumanInputFn = async () => ({
535
+ outcome: "deny",
536
+ reviewer: "ida_01abc",
537
+ comment: "Rollback plan is missing",
538
+ responded_at: "2026-05-20T10:00:00Z",
539
+ });
540
+
541
+ const taskDef: HumanInputTaskDef = {
542
+ kind: "human_input",
543
+ humanInput: { prompt: "Confirm?" },
544
+ };
545
+
546
+ const state = createState();
547
+ await executeHumanInputTask(taskDef, "gate", state, makeCtx(awaitFn, emitFn));
548
+
549
+ const resolved = emitted[1][0];
550
+ if (resolved.type !== "approval_resolved") throw new Error("unexpected");
551
+ expect(resolved.comment).toBe("Rollback plan is missing");
552
+ expect(state.data.gate).toEqual({
553
+ outcome: "deny",
554
+ reviewer: "ida_01abc",
555
+ comment: "Rollback plan is missing",
556
+ responded_at: "2026-05-20T10:00:00Z",
557
+ });
558
+ });
559
+
481
560
  it("carries the reviewer_actor display snapshot into approval_resolved and the task output", async () => {
482
561
  const emitted: WorkflowEventDescriptor[][] = [];
483
562
  const emitFn: EmitEventsFn = async (events) => { emitted.push(events); };
@@ -607,11 +686,7 @@ describe("executeHumanInputTask", () => {
607
686
  it("emits approval_resolved with autoResolved true on timeout auto-approve", async () => {
608
687
  const emitted: WorkflowEventDescriptor[][] = [];
609
688
  const emitFn: EmitEventsFn = async (events) => { emitted.push(events); };
610
- const awaitFn: AwaitHumanInputFn = async () => ({
611
- outcome: "approve",
612
- auto_resolved: true,
613
- reason: "timeout",
614
- });
689
+ const awaitFn: AwaitHumanInputFn = async () => TIMED_OUT;
615
690
 
616
691
  const taskDef: HumanInputTaskDef = {
617
692
  kind: "human_input",
@@ -625,6 +700,7 @@ describe("executeHumanInputTask", () => {
625
700
 
626
701
  expect(resolved.autoResolved).toBe(true);
627
702
  expect(resolved.outcome).toBe("approve");
703
+ expect(resolved.comment).toBe("");
628
704
  });
629
705
  });
630
706
 
@@ -32,6 +32,16 @@
32
32
  * activation and attached to the approval_requested event — inline when
33
33
  * small, as an artifact reference when at/above the promotion threshold —
34
34
  * so the approval record captures exactly what the reviewer saw.
35
+ *
36
+ * Timeout policy lives here, whole. `ctx.awaitHumanInput()` only reports
37
+ * that the timer won; this module decides what that means. approve, deny and
38
+ * escalate resolve the gate to a declared outcome. fail logs an
39
+ * approval_resolved with `auto_resolved` set and no outcome, then fails the
40
+ * task with the error it has always raised, so every approval_requested in
41
+ * the event log is closed by an approval_resolved (stigmer/stigmer#1442).
42
+ * That emit is a new workflow command on an existing path, so it runs behind
43
+ * {@link HUMAN_INPUT_FAIL_TIMEOUT_RESOLVED_PATCH}: a history recorded before
44
+ * it replays the old order (no emit, then the throw).
35
45
  */
36
46
 
37
47
  import type {
@@ -41,6 +51,9 @@ import type {
41
51
  WorkflowState,
42
52
  TaskExecutionContext,
43
53
  HumanInputResult,
54
+ HumanInputResponse,
55
+ HumanInputTimeout,
56
+ HumanInputTimeoutPolicy,
44
57
  } from "../types.js";
45
58
  import { resolveConfigExpressions, resolveEmbeddedExpressions } from "../resolve.js";
46
59
  import { isStrictExpr } from "../expression-utils.js";
@@ -49,6 +62,13 @@ const SIGNAL_PREFIX = "human_input_";
49
62
  const DEFAULT_TIMEOUT_SECONDS = 0;
50
63
  const DEFAULT_ON_TIMEOUT = "fail" as const;
51
64
 
65
+ /**
66
+ * Temporal patch id for logging approval_resolved before a fail-policy
67
+ * timeout fails the task. Wire bytes: a rename makes every history that
68
+ * carries the marker replay down the old path.
69
+ */
70
+ export const HUMAN_INPUT_FAIL_TIMEOUT_RESOLVED_PATCH = "human-input-fail-timeout-resolved";
71
+
52
72
  /**
53
73
  * Executes a human_input task. Called from `runSingleTask` in do-executor
54
74
  * when the task kind is "human_input".
@@ -97,29 +117,22 @@ export async function executeHumanInputTask(
97
117
  ]);
98
118
  }
99
119
 
100
- const result: HumanInputResult = applyTimeoutOutcomeContract(
101
- await ctx.awaitHumanInput({
102
- signalName,
103
- timeoutSeconds,
104
- onTimeout,
105
- }),
106
- config.outcomes,
107
- );
120
+ const response = await ctx.awaitHumanInput({ signalName, timeoutSeconds });
121
+ const result = isTimeout(response)
122
+ ? resolveTimeout(onTimeout, config.outcomes)
123
+ : response;
108
124
 
109
- if (ctx.emitEvents) {
110
- await ctx.emitEvents([{
111
- type: "approval_resolved",
112
- taskName,
113
- occurredAt: new Date().toISOString(),
114
- outcome: result.outcome,
115
- resolvedBy: result.reviewer ?? "",
116
- resolvedByActor: result.reviewer_actor,
117
- comment: "",
118
- waitDurationMs: Date.now() - approvalRequestedAt,
119
- autoResolved: result.auto_resolved ?? false,
120
- }]);
125
+ if (result === undefined) {
126
+ if (ctx.isPatched?.(HUMAN_INPUT_FAIL_TIMEOUT_RESOLVED_PATCH) ?? true) {
127
+ await emitApprovalResolved(ctx, taskName, approvalRequestedAt, {
128
+ outcome: "", auto_resolved: true, reason: "timeout",
129
+ });
130
+ }
131
+ throw new Error(`human_input task timed out waiting for signal '${signalName}'`);
121
132
  }
122
133
 
134
+ await emitApprovalResolved(ctx, taskName, approvalRequestedAt, result);
135
+
123
136
  state.addData({ [taskName]: result });
124
137
 
125
138
  const selectedOutcome = config.outcomes?.find(o => o.name === result.outcome);
@@ -136,35 +149,72 @@ function validateConfig(config: HumanInputConfig, taskName: string): void {
136
149
  }
137
150
  }
138
151
 
152
+ function isTimeout(response: HumanInputResponse): response is HumanInputTimeout {
153
+ return "timedOut" in response;
154
+ }
155
+
139
156
  /**
140
- * Applies the proto contract for timeout auto-resolution with custom
141
- * outcomes (HumanInputTaskConfig.outcomes doc): auto-approve resolves to
142
- * the FIRST declared outcome and auto-deny to the LAST, so `then` routing
143
- * and downstream outcome switches see declared outcome names — never the
144
- * orchestrator's internal approve/deny words, which a reviewer of a
145
- * custom-outcome gate was never offered. Binary gates (no custom outcomes)
146
- * keep the plain approve/deny result.
157
+ * The decision a timeout policy makes, or `undefined` when the policy is
158
+ * fail and the gate ends without one.
159
+ *
160
+ * With custom outcomes the proto contract (HumanInputTaskConfig.outcomes doc)
161
+ * applies: auto-approve resolves to the FIRST declared outcome and auto-deny
162
+ * to the LAST, so `then` routing and downstream outcome switches see declared
163
+ * outcome names, never approve/deny words a reviewer of a custom-outcome gate
164
+ * was never offered. Binary gates (no custom outcomes) keep plain approve/deny.
147
165
  *
148
166
  * Escalate needs no remapping by construction (stigmer/stigmer#781): its
149
- * policy word IS the declared outcome's name — the loader (and the server
167
+ * policy word IS the declared outcome's name. The loader (and the server
150
168
  * validator) only accept the escalate policy when an outcome named
151
- * "escalate" with `then` exists, so the caller's ordinary name lookup
152
- * routes the escalation branch.
169
+ * "escalate" with `then` exists, so the caller's ordinary name lookup routes
170
+ * the escalation branch.
153
171
  */
154
- function applyTimeoutOutcomeContract(
155
- result: HumanInputResult,
172
+ function resolveTimeout(
173
+ policy: HumanInputTimeoutPolicy,
156
174
  outcomes: HumanInputConfig["outcomes"],
157
- ): HumanInputResult {
158
- if (!result.auto_resolved || result.reason !== "timeout" || !outcomes?.length) {
159
- return result;
160
- }
161
- if (result.outcome === "approve") {
162
- return { ...result, outcome: outcomes[0].name };
163
- }
164
- if (result.outcome === "deny") {
165
- return { ...result, outcome: outcomes[outcomes.length - 1].name };
175
+ ): HumanInputResult | undefined {
176
+ const decided = (outcome: string): HumanInputResult => ({
177
+ outcome, auto_resolved: true, reason: "timeout",
178
+ });
179
+ switch (policy) {
180
+ case "approve":
181
+ return decided(outcomes?.length ? outcomes[0].name : "approve");
182
+ case "deny":
183
+ return decided(outcomes?.length ? outcomes[outcomes.length - 1].name : "deny");
184
+ case "escalate":
185
+ return decided("escalate");
186
+ case "fail":
187
+ return undefined;
188
+ default: {
189
+ const unreachable: never = policy;
190
+ throw new Error(`human_input: unknown timeout policy '${String(unreachable)}'`);
191
+ }
166
192
  }
167
- return result;
193
+ }
194
+
195
+ /**
196
+ * Logs how the gate's approval ended. A timeout's decision carries no
197
+ * reviewer, no comment and `auto_resolved`; a fail timeout's carries no
198
+ * outcome either.
199
+ */
200
+ async function emitApprovalResolved(
201
+ ctx: TaskExecutionContext,
202
+ taskName: string,
203
+ approvalRequestedAt: number,
204
+ result: HumanInputResult,
205
+ ): Promise<void> {
206
+ if (!ctx.emitEvents) return;
207
+ await ctx.emitEvents([{
208
+ type: "approval_resolved",
209
+ taskName,
210
+ occurredAt: new Date().toISOString(),
211
+ outcome: result.outcome,
212
+ resolvedBy: result.reviewer ?? "",
213
+ resolvedByActor: result.reviewer_actor,
214
+ comment: result.comment ?? "",
215
+ waitDurationMs: Date.now() - approvalRequestedAt,
216
+ autoResolved: result.auto_resolved ?? false,
217
+ }]);
168
218
  }
169
219
 
170
220
  interface ResolvedReviewPayload {
@@ -561,10 +561,20 @@ export interface RunWorkflowExecutionConfig {
561
561
  }
562
562
 
563
563
  /**
564
- * Blocks workflow execution until a human provides input via signal.
565
- * Supports configurable timeout with policies (fail, approve, deny, escalate).
564
+ * Blocks workflow execution until a human provides input via signal, or
565
+ * until the timeout elapses. Reports which happened and nothing more: the
566
+ * timeout policy (fail, approve, deny, escalate) is the kernel's to apply
567
+ * (`tasks/human-input.ts`), so one module owns every way a gate can end.
566
568
  */
567
- export type AwaitHumanInputFn = (config: HumanInputExecutionConfig) => Promise<HumanInputResult>;
569
+ export type AwaitHumanInputFn = (config: HumanInputExecutionConfig) => Promise<HumanInputResponse>;
570
+
571
+ /** What a wait at a human_input gate ended with: the reviewer's decision, or the timeout. */
572
+ export type HumanInputResponse = HumanInputResult | HumanInputTimeout;
573
+
574
+ /** The gate's timeout elapsed before any decision arrived. */
575
+ export interface HumanInputTimeout {
576
+ readonly timedOut: true;
577
+ }
568
578
 
569
579
  /**
570
580
  * Internal timeout-policy vocabulary — the loader-normalized form of the
@@ -577,8 +587,8 @@ export type HumanInputTimeoutPolicy = "fail" | "approve" | "deny" | "escalate";
577
587
 
578
588
  export interface HumanInputExecutionConfig {
579
589
  readonly signalName: string;
590
+ /** Seconds to wait before reporting a {@link HumanInputTimeout}; 0 waits indefinitely. */
580
591
  readonly timeoutSeconds: number;
581
- readonly onTimeout: HumanInputTimeoutPolicy;
582
592
  }
583
593
 
584
594
  export interface HumanInputResult {
@@ -594,6 +604,8 @@ export interface HumanInputResult {
594
604
  readonly reviewer_actor?: HumanInputReviewerActor;
595
605
  readonly responded_at?: string;
596
606
  readonly form_data?: Record<string, unknown>;
607
+ /** Free-text comment the reviewer attached. Absent when none was written, and always on a timeout. */
608
+ readonly comment?: string;
597
609
  readonly auto_resolved?: boolean;
598
610
  readonly reason?: string;
599
611
  }
@@ -65,6 +65,13 @@ type AgentActivities = ReturnType<typeof createCallAgentActivities>;
65
65
  type StatusActivities = ReturnType<typeof createCallAgentStatusActivities>;
66
66
  type EventActivities = ReturnType<typeof createWorkflowEventActivities>;
67
67
 
68
+ /**
69
+ * Every activity an agent call schedules. The engine's own activity type
70
+ * includes it, so a worker (or a test mock) that registers the engine's
71
+ * activities is checked against this list by the compiler.
72
+ */
73
+ export type AgentCallActivities = AgentActivities & StatusActivities & EventActivities;
74
+
68
75
  // The 1h ceiling is a deliberate platform constant, not a user knob: the
69
76
  // old AgentCallTaskConfig declared a per-task `timeout` that nothing ever
70
77
  // read, and #358 deleted it rather than declaring what isn't honored. The
@@ -28,6 +28,7 @@ import type { createRunCommandActivities } from "../activities/run-command.js";
28
28
  import type { createWorkflowEventActivities } from "../activities/workflow-event-activities.js";
29
29
  import type { createPromoteTaskOutputActivities } from "../activities/promote-task-output.js";
30
30
  import { orchestrateAgentCall } from "./call-agent-orchestrator.js";
31
+ import type { AgentCallActivities } from "./call-agent-orchestrator.js";
31
32
  import { orchestrateListenTask } from "./listen-orchestrator.js";
32
33
  import { orchestrateRunWorkflow } from "./run-orchestrator.js";
33
34
  import { orchestrateHumanInput } from "./human-input-orchestrator.js";
@@ -65,6 +66,23 @@ type RunActivities = ReturnType<typeof createRunCommandActivities>;
65
66
  type EventActivities = ReturnType<typeof createWorkflowEventActivities>;
66
67
  type PromoteActivities = ReturnType<typeof createPromoteTaskOutputActivities>;
67
68
 
69
+ /**
70
+ * Every activity the engine can schedule, by name. A worker that runs the
71
+ * engine must register each one; the test mocks declare
72
+ * `satisfies Record<keyof EngineActivities, unknown>` so a new activity
73
+ * without a mock fails the typecheck instead of failing the smoke run at
74
+ * run time (stigmer#1300: four missing local activities made the golden
75
+ * suite report passes for months without running a golden).
76
+ */
77
+ export type EngineActivities = EvalActivities &
78
+ HttpActivities &
79
+ GrpcActivities &
80
+ FunctionActivities &
81
+ RunActivities &
82
+ EventActivities &
83
+ PromoteActivities &
84
+ AgentCallActivities;
85
+
68
86
  const evalProxy = proxyLocalActivities<EvalActivities>({
69
87
  startToCloseTimeout: "10s",
70
88
  });
@@ -31,7 +31,8 @@ import type { createHydrateWorkflowActivities } from "../activities/hydrate-work
31
31
  // Hydration Activity Proxy
32
32
  // ─────────────────────────────────────────────────────────────────────────────
33
33
 
34
- type HydrateActivities = ReturnType<typeof createHydrateWorkflowActivities>;
34
+ /** The activity this entry point schedules before it hands the model to the engine. */
35
+ export type HydrateActivities = ReturnType<typeof createHydrateWorkflowActivities>;
35
36
 
36
37
  const hydrateProxy = proxyActivities<HydrateActivities>({
37
38
  startToCloseTimeout: "2m",