zeitlich 0.2.35 → 0.2.37

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 (199) hide show
  1. package/README.md +146 -92
  2. package/dist/{activities-BVI2lTwr.d.ts → activities-Bb-nAjwQ.d.ts} +2 -2
  3. package/dist/{activities-hd4aNnZE.d.cts → activities-vkI4_3CC.d.cts} +2 -2
  4. package/dist/adapters/sandbox/bedrock/index.cjs +14 -11
  5. package/dist/adapters/sandbox/bedrock/index.cjs.map +1 -1
  6. package/dist/adapters/sandbox/bedrock/index.d.cts +4 -3
  7. package/dist/adapters/sandbox/bedrock/index.d.ts +4 -3
  8. package/dist/adapters/sandbox/bedrock/index.js +14 -11
  9. package/dist/adapters/sandbox/bedrock/index.js.map +1 -1
  10. package/dist/adapters/sandbox/bedrock/workflow.cjs +2 -0
  11. package/dist/adapters/sandbox/bedrock/workflow.cjs.map +1 -1
  12. package/dist/adapters/sandbox/bedrock/workflow.d.cts +2 -2
  13. package/dist/adapters/sandbox/bedrock/workflow.d.ts +2 -2
  14. package/dist/adapters/sandbox/bedrock/workflow.js +2 -0
  15. package/dist/adapters/sandbox/bedrock/workflow.js.map +1 -1
  16. package/dist/adapters/sandbox/daytona/index.cjs +35 -6
  17. package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
  18. package/dist/adapters/sandbox/daytona/index.d.cts +3 -1
  19. package/dist/adapters/sandbox/daytona/index.d.ts +3 -1
  20. package/dist/adapters/sandbox/daytona/index.js +35 -6
  21. package/dist/adapters/sandbox/daytona/index.js.map +1 -1
  22. package/dist/adapters/sandbox/daytona/workflow.cjs +2 -0
  23. package/dist/adapters/sandbox/daytona/workflow.cjs.map +1 -1
  24. package/dist/adapters/sandbox/daytona/workflow.d.cts +1 -1
  25. package/dist/adapters/sandbox/daytona/workflow.d.ts +1 -1
  26. package/dist/adapters/sandbox/daytona/workflow.js +2 -0
  27. package/dist/adapters/sandbox/daytona/workflow.js.map +1 -1
  28. package/dist/adapters/sandbox/e2b/index.cjs +59 -10
  29. package/dist/adapters/sandbox/e2b/index.cjs.map +1 -1
  30. package/dist/adapters/sandbox/e2b/index.d.cts +5 -3
  31. package/dist/adapters/sandbox/e2b/index.d.ts +5 -3
  32. package/dist/adapters/sandbox/e2b/index.js +59 -10
  33. package/dist/adapters/sandbox/e2b/index.js.map +1 -1
  34. package/dist/adapters/sandbox/e2b/workflow.cjs +2 -0
  35. package/dist/adapters/sandbox/e2b/workflow.cjs.map +1 -1
  36. package/dist/adapters/sandbox/e2b/workflow.d.cts +1 -1
  37. package/dist/adapters/sandbox/e2b/workflow.d.ts +1 -1
  38. package/dist/adapters/sandbox/e2b/workflow.js +2 -0
  39. package/dist/adapters/sandbox/e2b/workflow.js.map +1 -1
  40. package/dist/adapters/sandbox/inmemory/index.cjs +5 -0
  41. package/dist/adapters/sandbox/inmemory/index.cjs.map +1 -1
  42. package/dist/adapters/sandbox/inmemory/index.d.cts +2 -1
  43. package/dist/adapters/sandbox/inmemory/index.d.ts +2 -1
  44. package/dist/adapters/sandbox/inmemory/index.js +5 -0
  45. package/dist/adapters/sandbox/inmemory/index.js.map +1 -1
  46. package/dist/adapters/sandbox/inmemory/workflow.cjs +2 -0
  47. package/dist/adapters/sandbox/inmemory/workflow.cjs.map +1 -1
  48. package/dist/adapters/sandbox/inmemory/workflow.d.cts +1 -1
  49. package/dist/adapters/sandbox/inmemory/workflow.d.ts +1 -1
  50. package/dist/adapters/sandbox/inmemory/workflow.js +2 -0
  51. package/dist/adapters/sandbox/inmemory/workflow.js.map +1 -1
  52. package/dist/adapters/thread/anthropic/index.cjs +71 -36
  53. package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
  54. package/dist/adapters/thread/anthropic/index.d.cts +5 -5
  55. package/dist/adapters/thread/anthropic/index.d.ts +5 -5
  56. package/dist/adapters/thread/anthropic/index.js +71 -36
  57. package/dist/adapters/thread/anthropic/index.js.map +1 -1
  58. package/dist/adapters/thread/anthropic/workflow.cjs +5 -1
  59. package/dist/adapters/thread/anthropic/workflow.cjs.map +1 -1
  60. package/dist/adapters/thread/anthropic/workflow.d.cts +5 -5
  61. package/dist/adapters/thread/anthropic/workflow.d.ts +5 -5
  62. package/dist/adapters/thread/anthropic/workflow.js +5 -1
  63. package/dist/adapters/thread/anthropic/workflow.js.map +1 -1
  64. package/dist/adapters/thread/google-genai/index.cjs +50 -25
  65. package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
  66. package/dist/adapters/thread/google-genai/index.d.cts +5 -5
  67. package/dist/adapters/thread/google-genai/index.d.ts +5 -5
  68. package/dist/adapters/thread/google-genai/index.js +50 -25
  69. package/dist/adapters/thread/google-genai/index.js.map +1 -1
  70. package/dist/adapters/thread/google-genai/workflow.cjs +5 -1
  71. package/dist/adapters/thread/google-genai/workflow.cjs.map +1 -1
  72. package/dist/adapters/thread/google-genai/workflow.d.cts +5 -5
  73. package/dist/adapters/thread/google-genai/workflow.d.ts +5 -5
  74. package/dist/adapters/thread/google-genai/workflow.js +5 -1
  75. package/dist/adapters/thread/google-genai/workflow.js.map +1 -1
  76. package/dist/adapters/thread/langchain/index.cjs +34 -7
  77. package/dist/adapters/thread/langchain/index.cjs.map +1 -1
  78. package/dist/adapters/thread/langchain/index.d.cts +5 -5
  79. package/dist/adapters/thread/langchain/index.d.ts +5 -5
  80. package/dist/adapters/thread/langchain/index.js +34 -7
  81. package/dist/adapters/thread/langchain/index.js.map +1 -1
  82. package/dist/adapters/thread/langchain/workflow.cjs +5 -1
  83. package/dist/adapters/thread/langchain/workflow.cjs.map +1 -1
  84. package/dist/adapters/thread/langchain/workflow.d.cts +5 -5
  85. package/dist/adapters/thread/langchain/workflow.d.ts +5 -5
  86. package/dist/adapters/thread/langchain/workflow.js +5 -1
  87. package/dist/adapters/thread/langchain/workflow.js.map +1 -1
  88. package/dist/index.cjs +206 -120
  89. package/dist/index.cjs.map +1 -1
  90. package/dist/index.d.cts +17 -11
  91. package/dist/index.d.ts +17 -11
  92. package/dist/index.js +207 -121
  93. package/dist/index.js.map +1 -1
  94. package/dist/{proxy-BjdFGPTm.d.ts → proxy-0smGKvx8.d.ts} +1 -1
  95. package/dist/{proxy-7RnVaPdJ.d.cts → proxy-DEtowJyd.d.cts} +1 -1
  96. package/dist/{thread-manager-DjN5JYul.d.ts → thread-manager-3fszQih4.d.ts} +2 -2
  97. package/dist/{thread-manager-CbpiGq1L.d.ts → thread-manager-C-C4pI2z.d.ts} +2 -2
  98. package/dist/{thread-manager-BBzNgQWH.d.cts → thread-manager-CzYln2OC.d.cts} +2 -2
  99. package/dist/{thread-manager-DzXm9eeI.d.cts → thread-manager-D4vgzYrh.d.cts} +2 -2
  100. package/dist/{types-yiXmqedU.d.ts → types-B37hKoWA.d.ts} +1 -1
  101. package/dist/{types-DQ1l_gXL.d.cts → types-BO7Yju20.d.cts} +63 -14
  102. package/dist/{types-wiGLvxWf.d.ts → types-CNuWnvy9.d.ts} +1 -1
  103. package/dist/{types-CADc5V_P.d.ts → types-CPKDl-y_.d.ts} +63 -14
  104. package/dist/{types-Mc_4BCfT.d.cts → types-D08CXPh8.d.cts} +1 -1
  105. package/dist/{types-CBH54cwr.d.cts → types-DWEUmYAJ.d.cts} +1 -1
  106. package/dist/{types-DxCpFNv_.d.cts → types-tQL9njTu.d.cts} +25 -0
  107. package/dist/{types-DxCpFNv_.d.ts → types-tQL9njTu.d.ts} +25 -0
  108. package/dist/{workflow-P2pTSfKu.d.ts → workflow-CjXHbZZc.d.ts} +2 -2
  109. package/dist/{workflow-DhtWRovz.d.cts → workflow-Do_lzJpT.d.cts} +2 -2
  110. package/dist/workflow.cjs +182 -114
  111. package/dist/workflow.cjs.map +1 -1
  112. package/dist/workflow.d.cts +3 -3
  113. package/dist/workflow.d.ts +3 -3
  114. package/dist/workflow.js +183 -115
  115. package/dist/workflow.js.map +1 -1
  116. package/package.json +1 -1
  117. package/src/adapters/sandbox/bedrock/filesystem.ts +6 -12
  118. package/src/adapters/sandbox/bedrock/index.ts +10 -8
  119. package/src/adapters/sandbox/bedrock/proxy.ts +2 -0
  120. package/src/adapters/sandbox/daytona/filesystem.ts +29 -6
  121. package/src/adapters/sandbox/daytona/index.ts +6 -0
  122. package/src/adapters/sandbox/daytona/proxy.ts +2 -0
  123. package/src/adapters/sandbox/e2b/filesystem.ts +5 -4
  124. package/src/adapters/sandbox/e2b/index.ts +63 -12
  125. package/src/adapters/sandbox/e2b/proxy.ts +2 -0
  126. package/src/adapters/sandbox/inmemory/index.ts +5 -0
  127. package/src/adapters/sandbox/inmemory/proxy.ts +2 -0
  128. package/src/adapters/thread/anthropic/activities.ts +49 -26
  129. package/src/adapters/thread/anthropic/model-invoker.ts +15 -6
  130. package/src/adapters/thread/anthropic/proxy.ts +6 -2
  131. package/src/adapters/thread/anthropic/thread-manager.test.ts +26 -7
  132. package/src/adapters/thread/anthropic/thread-manager.ts +60 -46
  133. package/src/adapters/thread/google-genai/activities.ts +7 -2
  134. package/src/adapters/thread/google-genai/model-invoker.ts +26 -8
  135. package/src/adapters/thread/google-genai/proxy.ts +6 -2
  136. package/src/adapters/thread/google-genai/thread-manager.test.ts +13 -3
  137. package/src/adapters/thread/google-genai/thread-manager.ts +54 -33
  138. package/src/adapters/thread/langchain/activities.ts +46 -24
  139. package/src/adapters/thread/langchain/hooks.test.ts +36 -49
  140. package/src/adapters/thread/langchain/hooks.ts +18 -5
  141. package/src/adapters/thread/langchain/model-invoker.ts +3 -3
  142. package/src/adapters/thread/langchain/proxy.ts +6 -2
  143. package/src/adapters/thread/langchain/thread-manager.test.ts +5 -1
  144. package/src/adapters/thread/langchain/thread-manager.ts +20 -9
  145. package/src/index.ts +4 -1
  146. package/src/lib/activity.ts +16 -6
  147. package/src/lib/hooks/types.ts +6 -6
  148. package/src/lib/lifecycle.ts +9 -1
  149. package/src/lib/model/proxy.ts +2 -2
  150. package/src/lib/observability/hooks.ts +4 -5
  151. package/src/lib/observability/index.ts +1 -4
  152. package/src/lib/sandbox/manager.ts +21 -4
  153. package/src/lib/sandbox/node-fs.ts +3 -6
  154. package/src/lib/sandbox/sandbox.test.ts +36 -3
  155. package/src/lib/sandbox/tree.integration.test.ts +10 -3
  156. package/src/lib/sandbox/types.ts +35 -1
  157. package/src/lib/session/session-edge-cases.integration.test.ts +51 -13
  158. package/src/lib/session/session.integration.test.ts +139 -0
  159. package/src/lib/session/session.ts +50 -19
  160. package/src/lib/session/types.ts +13 -5
  161. package/src/lib/skills/fs-provider.ts +12 -8
  162. package/src/lib/skills/handler.ts +1 -1
  163. package/src/lib/skills/parse.ts +3 -1
  164. package/src/lib/skills/register.ts +1 -3
  165. package/src/lib/skills/skills.integration.test.ts +25 -15
  166. package/src/lib/state/manager.integration.test.ts +12 -2
  167. package/src/lib/subagent/define.ts +1 -1
  168. package/src/lib/subagent/handler.ts +186 -71
  169. package/src/lib/subagent/index.ts +1 -5
  170. package/src/lib/subagent/register.ts +3 -2
  171. package/src/lib/subagent/signals.ts +1 -10
  172. package/src/lib/subagent/subagent.integration.test.ts +438 -156
  173. package/src/lib/subagent/tool.ts +4 -3
  174. package/src/lib/subagent/types.ts +50 -20
  175. package/src/lib/subagent/workflow.ts +9 -49
  176. package/src/lib/thread/id.test.ts +1 -1
  177. package/src/lib/thread/id.ts +1 -2
  178. package/src/lib/thread/proxy.ts +3 -4
  179. package/src/lib/thread/types.ts +11 -3
  180. package/src/lib/tool-router/index.ts +1 -5
  181. package/src/lib/tool-router/router-edge-cases.integration.test.ts +1 -1
  182. package/src/lib/tool-router/router.ts +3 -2
  183. package/src/lib/tool-router/types.ts +11 -3
  184. package/src/lib/tool-router/with-sandbox.ts +19 -5
  185. package/src/lib/virtual-fs/filesystem.ts +1 -1
  186. package/src/lib/virtual-fs/index.ts +5 -1
  187. package/src/lib/virtual-fs/mutations.ts +2 -4
  188. package/src/lib/virtual-fs/queries.ts +9 -5
  189. package/src/lib/virtual-fs/types.ts +4 -1
  190. package/src/lib/virtual-fs/virtual-fs.test.ts +9 -11
  191. package/src/lib/workflow.test.ts +7 -4
  192. package/src/lib/workflow.ts +1 -5
  193. package/src/tools/ask-user-question/tool.ts +1 -3
  194. package/src/tools/glob/handler.ts +1 -4
  195. package/src/tools/task-get/handler.ts +4 -5
  196. package/src/tools/task-list/handler.ts +1 -4
  197. package/src/tools/task-update/handler.ts +4 -5
  198. package/src/workflow.ts +20 -7
  199. package/tsup.config.ts +9 -6
@@ -1,8 +1,6 @@
1
1
  import { describe, expect, it, vi, afterEach } from "vitest";
2
2
  import { z } from "zod";
3
3
 
4
- const capturedSignalHandlers = new Map<unknown, (...args: unknown[]) => void>();
5
-
6
4
  let nextStartChildResult: ((prompt: string) => unknown) | null = null;
7
5
 
8
6
  vi.mock("@temporalio/workflow", () => {
@@ -34,11 +32,7 @@ vi.mock("@temporalio/workflow", () => {
34
32
  parent: { workflowId: "parent-wf-1" },
35
33
  }),
36
34
  defineSignal: vi.fn((name: string) => ({ __signal: true, name })),
37
- setHandler: vi.fn(
38
- (signal: unknown, handler: (...a: unknown[]) => void) => {
39
- capturedSignalHandlers.set(signal, handler);
40
- }
41
- ),
35
+ setHandler: vi.fn(),
42
36
  condition: vi.fn(async (fn: () => boolean) => {
43
37
  if (!fn()) throw new Error("condition predicate was not satisfied");
44
38
  }),
@@ -57,12 +51,6 @@ vi.mock("@temporalio/workflow", () => {
57
51
  usage: { inputTokens: 100, outputTokens: 50 },
58
52
  };
59
53
 
60
- for (const [signal, handler] of capturedSignalHandlers.entries()) {
61
- if ((signal as { name?: string }).name === "childResult") {
62
- handler({ childWorkflowId: opts.workflowId, result });
63
- }
64
- }
65
-
66
54
  return {
67
55
  signal: vi.fn(),
68
56
  result: () => Promise.resolve(result),
@@ -104,7 +92,6 @@ import type {
104
92
  } from "./types";
105
93
  afterEach(() => {
106
94
  nextStartChildResult = null;
107
- capturedSignalHandlers.clear();
108
95
  });
109
96
 
110
97
  function mockWorkflow(name?: string): SubagentWorkflow {
@@ -117,6 +104,27 @@ function mockWorkflow(name?: string): SubagentWorkflow {
117
104
  return fn as SubagentWorkflow;
118
105
  }
119
106
 
107
+ function makeMockSandboxOps() {
108
+ return {
109
+ createSandbox: vi.fn(),
110
+ destroySandbox: vi.fn(),
111
+ pauseSandbox: vi.fn(),
112
+ resumeSandbox: vi.fn(),
113
+ snapshotSandbox: vi.fn(),
114
+ restoreSandbox: vi.fn(),
115
+ deleteSandboxSnapshot: vi.fn(),
116
+ forkSandbox: vi.fn(),
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Default no-op sandbox proxy factory — satisfies the runtime check that
122
+ * every sandbox-using subagent must declare `sandbox.proxy`. Tests that
123
+ * need to assert on destroy/cleanup create their own mock ops and wire
124
+ * them through `sandbox: { ..., proxy: () => opsMock }`.
125
+ */
126
+ const noopSandboxProxy = () => makeMockSandboxOps();
127
+
120
128
  // ---------------------------------------------------------------------------
121
129
  // createSubagentTool
122
130
  // ---------------------------------------------------------------------------
@@ -377,7 +385,11 @@ describe("createSubagentHandler", () => {
377
385
  agentName: "inherit-agent",
378
386
  description: "Inherits sandbox",
379
387
  workflow: mockWorkflow(),
380
- sandbox: { source: "inherit", continuation: "continue" },
388
+ sandbox: {
389
+ source: "inherit",
390
+ continuation: "continue",
391
+ proxy: noopSandboxProxy,
392
+ },
381
393
  };
382
394
 
383
395
  const { handler } = createSubagentHandler([inheritSubagent]);
@@ -406,7 +418,11 @@ describe("createSubagentHandler", () => {
406
418
  agentName: "inherit-agent",
407
419
  description: "Inherits sandbox",
408
420
  workflow: mockWorkflow(),
409
- sandbox: { source: "inherit", continuation: "continue" },
421
+ sandbox: {
422
+ source: "inherit",
423
+ continuation: "continue",
424
+ proxy: noopSandboxProxy,
425
+ },
410
426
  };
411
427
 
412
428
  const { handler } = createSubagentHandler([inheritSubagent]);
@@ -427,7 +443,7 @@ describe("createSubagentHandler", () => {
427
443
  agentName: "own-agent",
428
444
  description: "Own sandbox",
429
445
  workflow: mockWorkflow(),
430
- sandbox: { source: "own", continuation: "fork" },
446
+ sandbox: { source: "own", continuation: "fork", proxy: noopSandboxProxy },
431
447
  };
432
448
 
433
449
  const { handler } = createSubagentHandler([ownSubagent]);
@@ -508,7 +524,7 @@ describe("createSubagentHandler", () => {
508
524
  agentName: "own-agent",
509
525
  description: "Own sandbox",
510
526
  workflow: mockWorkflow(),
511
- sandbox: { source: "own", continuation: "fork" },
527
+ sandbox: { source: "own", continuation: "fork", proxy: noopSandboxProxy },
512
528
  };
513
529
 
514
530
  const { handler } = createSubagentHandler([ownSubagent]);
@@ -670,7 +686,7 @@ describe("createSubagentHandler", () => {
670
686
  description: "Sandbox continuation",
671
687
  workflow: mockWorkflow(),
672
688
  thread: "fork",
673
- sandbox: { source: "own", continuation: "fork" },
689
+ sandbox: { source: "own", continuation: "fork", proxy: noopSandboxProxy },
674
690
  };
675
691
 
676
692
  const { handler } = createSubagentHandler([contSandboxSubagent]);
@@ -726,7 +742,7 @@ describe("createSubagentHandler", () => {
726
742
  description: "Sandbox continuation",
727
743
  workflow: mockWorkflow(),
728
744
  thread: "fork",
729
- sandbox: { source: "own", continuation: "fork" },
745
+ sandbox: { source: "own", continuation: "fork", proxy: noopSandboxProxy },
730
746
  };
731
747
 
732
748
  const { handler } = createSubagentHandler([contSandboxSubagent]);
@@ -755,10 +771,7 @@ describe("createSubagentHandler", () => {
755
771
  expect(workflowInput.thread).toBeUndefined();
756
772
  });
757
773
 
758
- it("does not signal destroy for fork-mode subagent without pause-until-parent-close", async () => {
759
- const { startChild } = await import("@temporalio/workflow");
760
- const startMock = startChild as ReturnType<typeof vi.fn>;
761
-
774
+ it("does not destroy fork-mode subagent sandbox without pause-until-parent-close", async () => {
762
775
  const contSandboxSubagent: SubagentConfig = {
763
776
  agentName: "sb-cont",
764
777
  description: "Sandbox continuation",
@@ -775,25 +788,16 @@ describe("createSubagentHandler", () => {
775
788
  { threadId: "t", toolCallId: "tc", toolName: "Subagent" }
776
789
  );
777
790
 
778
- const lastResult = startMock.mock.results.at(-1);
779
- if (!lastResult) throw new Error("expected startChild call");
780
- const childHandle = await lastResult.value;
781
- childHandle.signal.mockClear();
782
-
783
- await destroySubagentSandboxes();
784
-
785
- expect(childHandle.signal).not.toHaveBeenCalled();
791
+ await expect(destroySubagentSandboxes()).resolves.toBeUndefined();
786
792
  });
787
793
 
788
- it("does not signal destroy for sandbox=own with default shutdown", async () => {
789
- const { startChild } = await import("@temporalio/workflow");
790
- const startMock = startChild as ReturnType<typeof vi.fn>;
791
-
794
+ it("does not destroy sandbox=own with default shutdown", async () => {
795
+ const opsMock = makeMockSandboxOps();
792
796
  const ownSubagent: SubagentConfig = {
793
797
  agentName: "own-agent",
794
798
  description: "Own sandbox",
795
799
  workflow: mockWorkflow(),
796
- sandbox: { source: "own", continuation: "fork" },
800
+ sandbox: { source: "own", continuation: "fork", proxy: () => opsMock },
797
801
  };
798
802
 
799
803
  const { handler, destroySubagentSandboxes } = createSubagentHandler([
@@ -805,20 +809,20 @@ describe("createSubagentHandler", () => {
805
809
  { threadId: "t", toolCallId: "tc", toolName: "Subagent" }
806
810
  );
807
811
 
808
- const lastResult = startMock.mock.results.at(-1);
809
- if (!lastResult) throw new Error("expected startChild call");
810
- const childHandle = await lastResult.value;
811
- childHandle.signal.mockClear();
812
-
813
812
  await destroySubagentSandboxes();
814
813
 
815
- expect(childHandle.signal).not.toHaveBeenCalled();
814
+ expect(opsMock.destroySandbox).not.toHaveBeenCalled();
816
815
  });
817
816
 
818
- it("signals destroy for sandbox=own with pause-until-parent-close shutdown", async () => {
819
- const { startChild } = await import("@temporalio/workflow");
820
- const startMock = startChild as ReturnType<typeof vi.fn>;
817
+ it("destroys sandbox via sandbox.proxy for own + pause-until-parent-close", async () => {
818
+ nextStartChildResult = () => ({
819
+ toolResponse: "done",
820
+ data: null,
821
+ threadId: "child-t",
822
+ sandboxId: "child-sb-99",
823
+ });
821
824
 
825
+ const opsMock = makeMockSandboxOps();
822
826
  const ownSubagent: SubagentConfig = {
823
827
  agentName: "own-agent",
824
828
  description: "Own sandbox",
@@ -827,6 +831,7 @@ describe("createSubagentHandler", () => {
827
831
  source: "own",
828
832
  continuation: "fork",
829
833
  shutdown: "pause-until-parent-close",
834
+ proxy: () => opsMock,
830
835
  },
831
836
  };
832
837
 
@@ -841,21 +846,20 @@ describe("createSubagentHandler", () => {
841
846
 
842
847
  await destroySubagentSandboxes();
843
848
 
844
- const lastResult = startMock.mock.results.at(-1);
845
- if (!lastResult) throw new Error("expected startChild call");
846
- const childHandle = await lastResult.value;
847
- expect(childHandle.signal).toHaveBeenCalled();
849
+ expect(opsMock.destroySandbox).toHaveBeenCalledWith("child-sb-99");
848
850
  });
849
851
 
850
- it("does not signal destroy for inherit subagents", async () => {
851
- const { startChild } = await import("@temporalio/workflow");
852
- const startMock = startChild as ReturnType<typeof vi.fn>;
853
-
852
+ it("does not destroy inherit subagents' sandbox", async () => {
853
+ const opsMock = makeMockSandboxOps();
854
854
  const inheritSubagent: SubagentConfig = {
855
855
  agentName: "inherit-agent",
856
856
  description: "Inherits sandbox",
857
857
  workflow: mockWorkflow(),
858
- sandbox: { source: "inherit", continuation: "continue" },
858
+ sandbox: {
859
+ source: "inherit",
860
+ continuation: "continue",
861
+ proxy: () => opsMock,
862
+ },
859
863
  };
860
864
 
861
865
  const { handler, destroySubagentSandboxes } = createSubagentHandler([
@@ -872,14 +876,9 @@ describe("createSubagentHandler", () => {
872
876
  }
873
877
  );
874
878
 
875
- const lastResult = startMock.mock.results.at(-1);
876
- if (!lastResult) throw new Error("expected startChild call");
877
- const childHandle = await lastResult.value;
878
- childHandle.signal.mockClear();
879
-
880
879
  await destroySubagentSandboxes();
881
880
 
882
- expect(childHandle.signal).not.toHaveBeenCalled();
881
+ expect(opsMock.destroySandbox).not.toHaveBeenCalled();
883
882
  });
884
883
 
885
884
  it("does not pass sandboxId when sandbox is none (default)", async () => {
@@ -939,10 +938,7 @@ describe("createSubagentHandler", () => {
939
938
  expect(workflowInput.sandbox).toBeUndefined();
940
939
  });
941
940
 
942
- it("does not signal destroy for none subagents", async () => {
943
- const { startChild } = await import("@temporalio/workflow");
944
- const startMock = startChild as ReturnType<typeof vi.fn>;
945
-
941
+ it("destroySubagentSandboxes is a no-op for none subagents", async () => {
946
942
  const noneSubagent: SubagentConfig = {
947
943
  agentName: "none-agent",
948
944
  description: "No sandbox",
@@ -959,14 +955,7 @@ describe("createSubagentHandler", () => {
959
955
  { threadId: "t", toolCallId: "tc", toolName: "Subagent" }
960
956
  );
961
957
 
962
- const lastResult = startMock.mock.results.at(-1);
963
- if (!lastResult) throw new Error("expected startChild call");
964
- const childHandle = await lastResult.value;
965
- childHandle.signal.mockClear();
966
-
967
- await destroySubagentSandboxes();
968
-
969
- expect(childHandle.signal).not.toHaveBeenCalled();
958
+ await expect(destroySubagentSandboxes()).resolves.toBeUndefined();
970
959
  });
971
960
 
972
961
  // --- inherit + continuation: fork ---
@@ -979,7 +968,11 @@ describe("createSubagentHandler", () => {
979
968
  agentName: "inherit-fork",
980
969
  description: "Inherit fork",
981
970
  workflow: mockWorkflow(),
982
- sandbox: { source: "inherit", continuation: "fork" },
971
+ sandbox: {
972
+ source: "inherit",
973
+ continuation: "fork",
974
+ proxy: noopSandboxProxy,
975
+ },
983
976
  };
984
977
 
985
978
  const { handler } = createSubagentHandler([config]);
@@ -1021,7 +1014,11 @@ describe("createSubagentHandler", () => {
1021
1014
  description: "Own continue",
1022
1015
  workflow: mockWorkflow(),
1023
1016
  thread: "continue",
1024
- sandbox: { source: "own", continuation: "continue" },
1017
+ sandbox: {
1018
+ source: "own",
1019
+ continuation: "continue",
1020
+ proxy: noopSandboxProxy,
1021
+ },
1025
1022
  };
1026
1023
 
1027
1024
  const { handler } = createSubagentHandler([config]);
@@ -1075,7 +1072,12 @@ describe("createSubagentHandler", () => {
1075
1072
  agentName: "lazy-fork",
1076
1073
  description: "Lazy fork",
1077
1074
  workflow: mockWorkflow(),
1078
- sandbox: { source: "own", init: "once", continuation: "fork" },
1075
+ sandbox: {
1076
+ source: "own",
1077
+ init: "once",
1078
+ continuation: "fork",
1079
+ proxy: noopSandboxProxy,
1080
+ },
1079
1081
  };
1080
1082
 
1081
1083
  const { handler } = createSubagentHandler([config]);
@@ -1131,7 +1133,12 @@ describe("createSubagentHandler", () => {
1131
1133
  agentName: "lazy-cont",
1132
1134
  description: "Lazy continue",
1133
1135
  workflow: mockWorkflow(),
1134
- sandbox: { source: "own", init: "once", continuation: "continue" },
1136
+ sandbox: {
1137
+ source: "own",
1138
+ init: "once",
1139
+ continuation: "continue",
1140
+ proxy: noopSandboxProxy,
1141
+ },
1135
1142
  };
1136
1143
 
1137
1144
  const { handler } = createSubagentHandler([config]);
@@ -1165,10 +1172,7 @@ describe("createSubagentHandler", () => {
1165
1172
 
1166
1173
  // --- init: once cleanup ---
1167
1174
 
1168
- it("adds first-call child handle to pendingDestroys for init=once", async () => {
1169
- const { startChild } = await import("@temporalio/workflow");
1170
- const startMock = startChild as ReturnType<typeof vi.fn>;
1171
-
1175
+ it("destroys the persistent sandbox for init=once at parent shutdown", async () => {
1172
1176
  nextStartChildResult = () => ({
1173
1177
  toolResponse: "done",
1174
1178
  data: null,
@@ -1176,11 +1180,17 @@ describe("createSubagentHandler", () => {
1176
1180
  sandboxId: "persistent-sb",
1177
1181
  });
1178
1182
 
1183
+ const opsMock = makeMockSandboxOps();
1179
1184
  const config: SubagentConfig = {
1180
1185
  agentName: "lazy-cleanup",
1181
1186
  description: "Lazy cleanup",
1182
1187
  workflow: mockWorkflow(),
1183
- sandbox: { source: "own", init: "once", continuation: "fork" },
1188
+ sandbox: {
1189
+ source: "own",
1190
+ init: "once",
1191
+ continuation: "fork",
1192
+ proxy: () => opsMock,
1193
+ },
1184
1194
  };
1185
1195
 
1186
1196
  const { handler, destroySubagentSandboxes } = createSubagentHandler([
@@ -1194,10 +1204,7 @@ describe("createSubagentHandler", () => {
1194
1204
 
1195
1205
  await destroySubagentSandboxes();
1196
1206
 
1197
- const lastResult = startMock.mock.results.at(-1);
1198
- if (!lastResult) throw new Error("expected startChild call");
1199
- const childHandle = await lastResult.value;
1200
- expect(childHandle.signal).toHaveBeenCalled();
1207
+ expect(opsMock.destroySandbox).toHaveBeenCalledWith("persistent-sb");
1201
1208
  });
1202
1209
 
1203
1210
  it("returns sandboxId in response when child creates a sandbox", async () => {
@@ -1212,7 +1219,7 @@ describe("createSubagentHandler", () => {
1212
1219
  agentName: "own-agent",
1213
1220
  description: "Own sandbox",
1214
1221
  workflow: mockWorkflow(),
1215
- sandbox: { source: "own", continuation: "fork" },
1222
+ sandbox: { source: "own", continuation: "fork", proxy: noopSandboxProxy },
1216
1223
  };
1217
1224
 
1218
1225
  const { handler } = createSubagentHandler([ownSubagent]);
@@ -1325,10 +1332,15 @@ describe("createSubagentHandler", () => {
1325
1332
 
1326
1333
  // --- keep-until-parent-close ---
1327
1334
 
1328
- it("signals destroy for sandbox=own with keep-until-parent-close shutdown", async () => {
1329
- const { startChild } = await import("@temporalio/workflow");
1330
- const startMock = startChild as ReturnType<typeof vi.fn>;
1335
+ it("destroys sandbox for own + keep-until-parent-close", async () => {
1336
+ nextStartChildResult = () => ({
1337
+ toolResponse: "done",
1338
+ data: null,
1339
+ threadId: "child-t",
1340
+ sandboxId: "child-sb-77",
1341
+ });
1331
1342
 
1343
+ const opsMock = makeMockSandboxOps();
1332
1344
  const ownSubagent: SubagentConfig = {
1333
1345
  agentName: "own-keep",
1334
1346
  description: "Own sandbox kept",
@@ -1337,6 +1349,7 @@ describe("createSubagentHandler", () => {
1337
1349
  source: "own",
1338
1350
  continuation: "fork",
1339
1351
  shutdown: "keep-until-parent-close",
1352
+ proxy: () => opsMock,
1340
1353
  },
1341
1354
  };
1342
1355
 
@@ -1351,21 +1364,21 @@ describe("createSubagentHandler", () => {
1351
1364
 
1352
1365
  await destroySubagentSandboxes();
1353
1366
 
1354
- const lastResult = startMock.mock.results.at(-1);
1355
- if (!lastResult) throw new Error("expected startChild call");
1356
- const childHandle = await lastResult.value;
1357
- expect(childHandle.signal).toHaveBeenCalled();
1367
+ expect(opsMock.destroySandbox).toHaveBeenCalledWith("child-sb-77");
1358
1368
  });
1359
1369
 
1360
- it("does not signal destroy for sandbox=own with keep shutdown (without parent-close)", async () => {
1361
- const { startChild } = await import("@temporalio/workflow");
1362
- const startMock = startChild as ReturnType<typeof vi.fn>;
1363
-
1370
+ it("does not destroy sandbox for own + keep (without parent-close)", async () => {
1371
+ const opsMock = makeMockSandboxOps();
1364
1372
  const ownSubagent: SubagentConfig = {
1365
1373
  agentName: "own-keep-plain",
1366
1374
  description: "Own sandbox keep",
1367
1375
  workflow: mockWorkflow(),
1368
- sandbox: { source: "own", continuation: "fork", shutdown: "keep" },
1376
+ sandbox: {
1377
+ source: "own",
1378
+ continuation: "fork",
1379
+ shutdown: "keep",
1380
+ proxy: () => opsMock,
1381
+ },
1369
1382
  };
1370
1383
 
1371
1384
  const { handler, destroySubagentSandboxes } = createSubagentHandler([
@@ -1377,14 +1390,9 @@ describe("createSubagentHandler", () => {
1377
1390
  { threadId: "t", toolCallId: "tc", toolName: "Subagent" }
1378
1391
  );
1379
1392
 
1380
- const lastResult = startMock.mock.results.at(-1);
1381
- if (!lastResult) throw new Error("expected startChild call");
1382
- const childHandle = await lastResult.value;
1383
- childHandle.signal.mockClear();
1384
-
1385
1393
  await destroySubagentSandboxes();
1386
1394
 
1387
- expect(childHandle.signal).not.toHaveBeenCalled();
1395
+ expect(opsMock.destroySandbox).not.toHaveBeenCalled();
1388
1396
  });
1389
1397
 
1390
1398
  // --- mustSurvive does not override user shutdown ---
@@ -1409,6 +1417,7 @@ describe("createSubagentHandler", () => {
1409
1417
  init: "once",
1410
1418
  continuation: "fork",
1411
1419
  shutdown: "keep-until-parent-close",
1420
+ proxy: noopSandboxProxy,
1412
1421
  },
1413
1422
  };
1414
1423
 
@@ -1445,6 +1454,7 @@ describe("createSubagentHandler", () => {
1445
1454
  init: "once",
1446
1455
  continuation: "fork",
1447
1456
  shutdown: "pause",
1457
+ proxy: noopSandboxProxy,
1448
1458
  },
1449
1459
  };
1450
1460
 
@@ -1477,7 +1487,12 @@ describe("createSubagentHandler", () => {
1477
1487
  description: "Continue keep",
1478
1488
  workflow: mockWorkflow(),
1479
1489
  thread: "continue",
1480
- sandbox: { source: "own", continuation: "continue", shutdown: "keep" },
1490
+ sandbox: {
1491
+ source: "own",
1492
+ continuation: "continue",
1493
+ shutdown: "keep",
1494
+ proxy: noopSandboxProxy,
1495
+ },
1481
1496
  };
1482
1497
 
1483
1498
  const { handler } = createSubagentHandler([config]);
@@ -1509,7 +1524,12 @@ describe("createSubagentHandler", () => {
1509
1524
  description: "Continue destroy",
1510
1525
  workflow: mockWorkflow(),
1511
1526
  thread: "continue",
1512
- sandbox: { source: "own", continuation: "continue", shutdown: "destroy" },
1527
+ sandbox: {
1528
+ source: "own",
1529
+ continuation: "continue",
1530
+ shutdown: "destroy",
1531
+ proxy: noopSandboxProxy,
1532
+ },
1513
1533
  };
1514
1534
 
1515
1535
  const { handler } = createSubagentHandler([config]);
@@ -1524,6 +1544,309 @@ describe("createSubagentHandler", () => {
1524
1544
  const firstInput = firstCall[1].args[1] as SubagentWorkflowInput;
1525
1545
  expect(firstInput.sandboxShutdown).toBe("pause");
1526
1546
  });
1547
+
1548
+ // --- snapshot continuation ---
1549
+
1550
+ it("forces sandboxShutdown=snapshot and passes no sandbox on first call (continuation=snapshot)", async () => {
1551
+ const { startChild } = await import("@temporalio/workflow");
1552
+ const startMock = startChild as ReturnType<typeof vi.fn>;
1553
+
1554
+ nextStartChildResult = () => ({
1555
+ toolResponse: "first",
1556
+ data: null,
1557
+ threadId: "child-snap-1",
1558
+ baseSnapshot: {
1559
+ sandboxId: "sb-first",
1560
+ providerId: "test",
1561
+ data: { tag: "base" },
1562
+ createdAt: new Date().toISOString(),
1563
+ },
1564
+ snapshot: {
1565
+ sandboxId: "sb-first",
1566
+ providerId: "test",
1567
+ data: { tag: "exit-1" },
1568
+ createdAt: new Date().toISOString(),
1569
+ },
1570
+ });
1571
+
1572
+ const config: SubagentConfig = {
1573
+ agentName: "snap-agent",
1574
+ description: "Snapshot-driven",
1575
+ workflow: mockWorkflow(),
1576
+ thread: "continue",
1577
+ sandbox: {
1578
+ source: "own",
1579
+ init: "once",
1580
+ continuation: "snapshot",
1581
+ proxy: noopSandboxProxy,
1582
+ },
1583
+ };
1584
+
1585
+ const { handler } = createSubagentHandler([config]);
1586
+
1587
+ await handler(
1588
+ { subagent: "snap-agent", description: "test", prompt: "first" },
1589
+ { threadId: "t", toolCallId: "tc-1", toolName: "Subagent" }
1590
+ );
1591
+
1592
+ const firstCall = startMock.mock.calls.at(-1);
1593
+ if (!firstCall) throw new Error("expected startChild call");
1594
+ const firstInput = firstCall[1].args[1] as SubagentWorkflowInput;
1595
+ expect(firstInput.sandbox).toBeUndefined();
1596
+ expect(firstInput.sandboxShutdown).toBe("snapshot");
1597
+ });
1598
+
1599
+ it("boots follow-up from stored thread snapshot on continuation=snapshot", async () => {
1600
+ const { startChild } = await import("@temporalio/workflow");
1601
+ const startMock = startChild as ReturnType<typeof vi.fn>;
1602
+
1603
+ nextStartChildResult = () => ({
1604
+ toolResponse: "first",
1605
+ data: null,
1606
+ threadId: "child-snap-2",
1607
+ baseSnapshot: {
1608
+ sandboxId: "sb-first",
1609
+ providerId: "test",
1610
+ data: { tag: "base" },
1611
+ createdAt: new Date().toISOString(),
1612
+ },
1613
+ snapshot: {
1614
+ sandboxId: "sb-first",
1615
+ providerId: "test",
1616
+ data: { tag: "exit-1" },
1617
+ createdAt: new Date().toISOString(),
1618
+ },
1619
+ });
1620
+
1621
+ const config: SubagentConfig = {
1622
+ agentName: "snap-agent",
1623
+ description: "Snapshot-driven",
1624
+ workflow: mockWorkflow(),
1625
+ thread: "continue",
1626
+ sandbox: {
1627
+ source: "own",
1628
+ init: "once",
1629
+ continuation: "snapshot",
1630
+ proxy: noopSandboxProxy,
1631
+ },
1632
+ };
1633
+
1634
+ const { handler } = createSubagentHandler([config]);
1635
+
1636
+ await handler(
1637
+ { subagent: "snap-agent", description: "test", prompt: "first" },
1638
+ { threadId: "t", toolCallId: "tc-1", toolName: "Subagent" }
1639
+ );
1640
+
1641
+ nextStartChildResult = () => ({
1642
+ toolResponse: "second",
1643
+ data: null,
1644
+ threadId: "child-snap-2", // same thread — continuation
1645
+ snapshot: {
1646
+ sandboxId: "sb-second",
1647
+ providerId: "test",
1648
+ data: { tag: "exit-2" },
1649
+ createdAt: new Date().toISOString(),
1650
+ },
1651
+ });
1652
+
1653
+ await handler(
1654
+ {
1655
+ subagent: "snap-agent",
1656
+ description: "test",
1657
+ prompt: "second",
1658
+ threadId: "child-snap-2",
1659
+ },
1660
+ { threadId: "t", toolCallId: "tc-2", toolName: "Subagent" }
1661
+ );
1662
+
1663
+ const secondCall = startMock.mock.calls.at(-1);
1664
+ if (!secondCall) throw new Error("expected startChild call");
1665
+ const secondInput = secondCall[1].args[1] as SubagentWorkflowInput;
1666
+ expect(secondInput.sandbox).toEqual({
1667
+ mode: "from-snapshot",
1668
+ snapshot: expect.objectContaining({ data: { tag: "exit-1" } }),
1669
+ });
1670
+ expect(secondInput.sandboxShutdown).toBe("snapshot");
1671
+ });
1672
+
1673
+ it("uses per-agent base snapshot for a new thread when init=once + continuation=snapshot", async () => {
1674
+ const { startChild } = await import("@temporalio/workflow");
1675
+ const startMock = startChild as ReturnType<typeof vi.fn>;
1676
+
1677
+ // First call — establishes base snapshot.
1678
+ nextStartChildResult = () => ({
1679
+ toolResponse: "first",
1680
+ data: null,
1681
+ threadId: "child-snap-A",
1682
+ baseSnapshot: {
1683
+ sandboxId: "sb-first",
1684
+ providerId: "test",
1685
+ data: { tag: "base" },
1686
+ createdAt: new Date().toISOString(),
1687
+ },
1688
+ snapshot: {
1689
+ sandboxId: "sb-first",
1690
+ providerId: "test",
1691
+ data: { tag: "exit-A" },
1692
+ createdAt: new Date().toISOString(),
1693
+ },
1694
+ });
1695
+
1696
+ const config: SubagentConfig = {
1697
+ agentName: "snap-agent",
1698
+ description: "Snapshot-driven",
1699
+ workflow: mockWorkflow(),
1700
+ thread: "continue",
1701
+ sandbox: {
1702
+ source: "own",
1703
+ init: "once",
1704
+ continuation: "snapshot",
1705
+ proxy: noopSandboxProxy,
1706
+ },
1707
+ };
1708
+
1709
+ const { handler } = createSubagentHandler([config]);
1710
+
1711
+ await handler(
1712
+ { subagent: "snap-agent", description: "test", prompt: "first" },
1713
+ { threadId: "t", toolCallId: "tc-1", toolName: "Subagent" }
1714
+ );
1715
+
1716
+ // Third call: different thread → should use base snapshot.
1717
+ nextStartChildResult = () => ({
1718
+ toolResponse: "new-thread",
1719
+ data: null,
1720
+ threadId: "child-snap-B",
1721
+ snapshot: {
1722
+ sandboxId: "sb-B",
1723
+ providerId: "test",
1724
+ data: { tag: "exit-B" },
1725
+ createdAt: new Date().toISOString(),
1726
+ },
1727
+ });
1728
+
1729
+ await handler(
1730
+ { subagent: "snap-agent", description: "test", prompt: "new" },
1731
+ { threadId: "t", toolCallId: "tc-2", toolName: "Subagent" }
1732
+ );
1733
+
1734
+ const secondCall = startMock.mock.calls.at(-1);
1735
+ if (!secondCall) throw new Error("expected startChild call");
1736
+ const secondInput = secondCall[1].args[1] as SubagentWorkflowInput;
1737
+ expect(secondInput.sandbox).toEqual({
1738
+ mode: "from-snapshot",
1739
+ snapshot: expect.objectContaining({ data: { tag: "base" } }),
1740
+ });
1741
+ });
1742
+
1743
+ it("deletes every stored snapshot via sandbox.proxy during cleanup sweep", async () => {
1744
+ const opsMock = makeMockSandboxOps();
1745
+ const config: SubagentConfig = {
1746
+ agentName: "snap-agent",
1747
+ description: "Snapshot-driven",
1748
+ workflow: mockWorkflow(),
1749
+ thread: "continue",
1750
+ sandbox: {
1751
+ source: "own",
1752
+ init: "once",
1753
+ continuation: "snapshot",
1754
+ proxy: () => opsMock,
1755
+ },
1756
+ };
1757
+
1758
+ const { handler, cleanupSubagentSnapshots } = createSubagentHandler([
1759
+ config,
1760
+ ]);
1761
+
1762
+ // Call 1 — produces base + exit-1.
1763
+ nextStartChildResult = () => ({
1764
+ toolResponse: "first",
1765
+ data: null,
1766
+ threadId: "child-t",
1767
+ baseSnapshot: {
1768
+ sandboxId: "sb-first",
1769
+ providerId: "test",
1770
+ data: { tag: "base" },
1771
+ createdAt: new Date().toISOString(),
1772
+ },
1773
+ snapshot: {
1774
+ sandboxId: "sb-first",
1775
+ providerId: "test",
1776
+ data: { tag: "exit-1" },
1777
+ createdAt: new Date().toISOString(),
1778
+ },
1779
+ });
1780
+ await handler(
1781
+ { subagent: "snap-agent", description: "test", prompt: "first" },
1782
+ { threadId: "t", toolCallId: "tc-1", toolName: "Subagent" }
1783
+ );
1784
+
1785
+ // Call 2 — same thread, produces exit-2 (supersedes exit-1).
1786
+ nextStartChildResult = () => ({
1787
+ toolResponse: "second",
1788
+ data: null,
1789
+ threadId: "child-t",
1790
+ snapshot: {
1791
+ sandboxId: "sb-second",
1792
+ providerId: "test",
1793
+ data: { tag: "exit-2" },
1794
+ createdAt: new Date().toISOString(),
1795
+ },
1796
+ });
1797
+ await handler(
1798
+ {
1799
+ subagent: "snap-agent",
1800
+ description: "test",
1801
+ prompt: "second",
1802
+ threadId: "child-t",
1803
+ },
1804
+ { threadId: "t", toolCallId: "tc-2", toolName: "Subagent" }
1805
+ );
1806
+
1807
+ await cleanupSubagentSnapshots();
1808
+
1809
+ // Parent should have deleted both the latest thread snapshot and the
1810
+ // per-agent base snapshot through the subagent's sandbox.proxy.
1811
+ const deletedTags = opsMock.deleteSandboxSnapshot.mock.calls.map(
1812
+ (call) => (call[0] as { data: { tag: string } }).data.tag
1813
+ );
1814
+ expect(deletedTags.sort()).toEqual(["base", "exit-2"]);
1815
+ });
1816
+
1817
+ it("does not call deleteSandboxSnapshot for children that produced no snapshots", async () => {
1818
+ const opsMock = makeMockSandboxOps();
1819
+ const config: SubagentConfig = {
1820
+ agentName: "snap-agent",
1821
+ description: "Snapshot-driven",
1822
+ workflow: mockWorkflow(),
1823
+ thread: "continue",
1824
+ sandbox: {
1825
+ source: "own",
1826
+ init: "once",
1827
+ continuation: "snapshot",
1828
+ proxy: () => opsMock,
1829
+ },
1830
+ };
1831
+
1832
+ const { handler, cleanupSubagentSnapshots } = createSubagentHandler([
1833
+ config,
1834
+ ]);
1835
+
1836
+ nextStartChildResult = () => ({
1837
+ toolResponse: "no-snap",
1838
+ data: null,
1839
+ threadId: "child-t",
1840
+ });
1841
+ await handler(
1842
+ { subagent: "snap-agent", description: "test", prompt: "run" },
1843
+ { threadId: "t", toolCallId: "tc-1", toolName: "Subagent" }
1844
+ );
1845
+
1846
+ await cleanupSubagentSnapshots();
1847
+
1848
+ expect(opsMock.deleteSandboxSnapshot).not.toHaveBeenCalled();
1849
+ });
1527
1850
  });
1528
1851
 
1529
1852
  // ---------------------------------------------------------------------------
@@ -1865,64 +2188,23 @@ describe("defineSubagentWorkflow", () => {
1865
2188
  });
1866
2189
  });
1867
2190
 
1868
- it("validates destroySandbox required for keep-until-parent-close", async () => {
1869
- // @ts-expect-error — deliberately omitting destroySandbox to test runtime validation
1870
- const workflow = defineSubagentWorkflow(
1871
- {
1872
- name: "test",
1873
- description: "test agent",
1874
- sandboxShutdown: "keep-until-parent-close",
1875
- },
1876
- async () => ({
1877
- toolResponse: "ok",
1878
- data: null,
1879
- threadId: "t",
1880
- sandboxId: "sb-1",
1881
- })
1882
- );
1883
-
1884
- await expect(workflow("go", {})).rejects.toThrow(
1885
- /keep-until-parent-close.*destroySandbox/
1886
- );
1887
- });
1888
-
1889
- it("validates sandboxId required for keep-until-parent-close", async () => {
1890
- // @ts-expect-error — deliberately omitting sandboxId to test runtime validation
1891
- const workflow = defineSubagentWorkflow(
1892
- {
1893
- name: "test",
1894
- description: "test agent",
1895
- sandboxShutdown: "keep-until-parent-close",
1896
- },
1897
- async () => ({
1898
- toolResponse: "ok",
1899
- data: null,
1900
- threadId: "t",
1901
- destroySandbox: async () => {},
1902
- })
1903
- );
1904
-
1905
- await expect(workflow("go", {})).rejects.toThrow(
1906
- /keep-until-parent-close.*sandboxId/
1907
- );
1908
- });
1909
-
1910
2191
  it("uses keep-until-parent-close from workflowInput override in sessionInput", async () => {
1911
2192
  let capturedSession: SubagentSessionInput | undefined;
1912
2193
  const workflow = defineSubagentWorkflow(
1913
2194
  { name: "test", description: "test agent" },
1914
2195
  async (_prompt, sessionInput) => {
1915
2196
  capturedSession = sessionInput;
1916
- return { toolResponse: "ok", data: null, threadId: "t" };
2197
+ return {
2198
+ toolResponse: "ok",
2199
+ data: null,
2200
+ threadId: "t",
2201
+ sandboxId: "sb-1",
2202
+ };
1917
2203
  }
1918
2204
  );
1919
2205
 
1920
- // Validation will throw because destroySandbox is missing, but sessionInput is captured first
1921
- try {
1922
- await workflow("go", { sandboxShutdown: "keep-until-parent-close" });
1923
- } catch {
1924
- // expected — no destroySandbox callback
1925
- }
2206
+ await workflow("go", { sandboxShutdown: "keep-until-parent-close" });
2207
+
1926
2208
  expect(capturedSession?.sandboxShutdown).toBe("keep-until-parent-close");
1927
2209
  });
1928
2210
  });