zeitlich 0.2.33 → 0.2.34

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 (117) hide show
  1. package/README.md +17 -6
  2. package/dist/{activities-fnX8-vhR.d.cts → activities-JOqPfKP0.d.cts} +2 -2
  3. package/dist/{activities-YBD5BaHh.d.ts → activities-WwMsjRwm.d.ts} +2 -2
  4. package/dist/adapters/sandbox/bedrock/index.cjs +2 -0
  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 +2 -0
  9. package/dist/adapters/sandbox/bedrock/index.js.map +1 -1
  10. package/dist/adapters/sandbox/bedrock/workflow.cjs +1 -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 +1 -0
  15. package/dist/adapters/sandbox/bedrock/workflow.js.map +1 -1
  16. package/dist/adapters/sandbox/daytona/index.cjs +2 -0
  17. package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
  18. package/dist/adapters/sandbox/daytona/index.d.cts +2 -1
  19. package/dist/adapters/sandbox/daytona/index.d.ts +2 -1
  20. package/dist/adapters/sandbox/daytona/index.js +2 -0
  21. package/dist/adapters/sandbox/daytona/index.js.map +1 -1
  22. package/dist/adapters/sandbox/daytona/workflow.cjs +1 -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 +1 -0
  27. package/dist/adapters/sandbox/daytona/workflow.js.map +1 -1
  28. package/dist/adapters/sandbox/e2b/index.cjs +3 -0
  29. package/dist/adapters/sandbox/e2b/index.cjs.map +1 -1
  30. package/dist/adapters/sandbox/e2b/index.d.cts +2 -1
  31. package/dist/adapters/sandbox/e2b/index.d.ts +2 -1
  32. package/dist/adapters/sandbox/e2b/index.js +3 -0
  33. package/dist/adapters/sandbox/e2b/index.js.map +1 -1
  34. package/dist/adapters/sandbox/e2b/workflow.cjs +1 -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 +1 -0
  39. package/dist/adapters/sandbox/e2b/workflow.js.map +1 -1
  40. package/dist/adapters/sandbox/inmemory/index.cjs +2 -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 +2 -0
  45. package/dist/adapters/sandbox/inmemory/index.js.map +1 -1
  46. package/dist/adapters/sandbox/inmemory/workflow.cjs +1 -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 +1 -0
  51. package/dist/adapters/sandbox/inmemory/workflow.js.map +1 -1
  52. package/dist/adapters/thread/anthropic/index.d.cts +5 -5
  53. package/dist/adapters/thread/anthropic/index.d.ts +5 -5
  54. package/dist/adapters/thread/anthropic/workflow.d.cts +5 -5
  55. package/dist/adapters/thread/anthropic/workflow.d.ts +5 -5
  56. package/dist/adapters/thread/google-genai/index.d.cts +5 -5
  57. package/dist/adapters/thread/google-genai/index.d.ts +5 -5
  58. package/dist/adapters/thread/google-genai/workflow.d.cts +5 -5
  59. package/dist/adapters/thread/google-genai/workflow.d.ts +5 -5
  60. package/dist/adapters/thread/langchain/index.d.cts +5 -5
  61. package/dist/adapters/thread/langchain/index.d.ts +5 -5
  62. package/dist/adapters/thread/langchain/workflow.d.cts +5 -5
  63. package/dist/adapters/thread/langchain/workflow.d.ts +5 -5
  64. package/dist/index.cjs +114 -30
  65. package/dist/index.cjs.map +1 -1
  66. package/dist/index.d.cts +10 -9
  67. package/dist/index.d.ts +10 -9
  68. package/dist/index.js +114 -30
  69. package/dist/index.js.map +1 -1
  70. package/dist/{proxy-CTCYWjkr.d.cts → proxy-BesT2ioL.d.cts} +1 -1
  71. package/dist/{proxy-Br4unLTC.d.ts → proxy-Bz6wXYW-.d.ts} +1 -1
  72. package/dist/{thread-manager-Cv_BR28i.d.cts → thread-manager-CCVAOK8g.d.cts} +1 -1
  73. package/dist/{thread-manager-CUubPYPH.d.cts → thread-manager-Cf_34H8w.d.cts} +1 -1
  74. package/dist/{thread-manager-YJLoc1vH.d.ts → thread-manager-ClKAQx78.d.ts} +1 -1
  75. package/dist/{thread-manager-DKWxHUzD.d.ts → thread-manager-DarJIK_b.d.ts} +1 -1
  76. package/dist/{types-Bpq5fDI5.d.cts → types-BGLW5Zyj.d.ts} +35 -20
  77. package/dist/{types-BxiT8w9d.d.ts → types-BVUmLYpj.d.ts} +1 -1
  78. package/dist/{types-DUvEZSDe.d.cts → types-CBH54cwr.d.cts} +1 -1
  79. package/dist/{types-NJDyMyUx.d.cts → types-DPAZ3KCs.d.cts} +1 -1
  80. package/dist/{types-CheCTLeV.d.ts → types-DlLajQcu.d.cts} +35 -20
  81. package/dist/{types-AujBIMMn.d.cts → types-DxCpFNv_.d.cts} +4 -0
  82. package/dist/{types-AujBIMMn.d.ts → types-DxCpFNv_.d.ts} +4 -0
  83. package/dist/{types-DBk-C8zM.d.ts → types-wiGLvxWf.d.ts} +1 -1
  84. package/dist/{workflow-D9nNERvs.d.ts → workflow-_ZGcacCK.d.ts} +3 -3
  85. package/dist/{workflow-Od9vx5Jk.d.cts → workflow-hocXpLwg.d.cts} +3 -3
  86. package/dist/workflow.cjs +108 -30
  87. package/dist/workflow.cjs.map +1 -1
  88. package/dist/workflow.d.cts +3 -3
  89. package/dist/workflow.d.ts +3 -3
  90. package/dist/workflow.js +108 -30
  91. package/dist/workflow.js.map +1 -1
  92. package/package.json +1 -1
  93. package/src/adapters/sandbox/bedrock/index.ts +4 -0
  94. package/src/adapters/sandbox/bedrock/proxy.ts +1 -0
  95. package/src/adapters/sandbox/daytona/index.ts +4 -0
  96. package/src/adapters/sandbox/daytona/proxy.ts +1 -0
  97. package/src/adapters/sandbox/e2b/index.ts +4 -0
  98. package/src/adapters/sandbox/e2b/proxy.ts +1 -0
  99. package/src/adapters/sandbox/inmemory/index.ts +4 -0
  100. package/src/adapters/sandbox/inmemory/proxy.ts +1 -0
  101. package/src/lib/lifecycle.ts +7 -3
  102. package/src/lib/sandbox/manager.ts +7 -0
  103. package/src/lib/sandbox/types.ts +4 -0
  104. package/src/lib/session/session-edge-cases.integration.test.ts +194 -0
  105. package/src/lib/session/session.integration.test.ts +5 -0
  106. package/src/lib/session/session.ts +9 -0
  107. package/src/lib/session/types.ts +5 -0
  108. package/src/lib/subagent/define.ts +1 -1
  109. package/src/lib/subagent/handler.ts +142 -32
  110. package/src/lib/subagent/index.ts +5 -1
  111. package/src/lib/subagent/signals.ts +8 -1
  112. package/src/lib/subagent/subagent.integration.test.ts +532 -25
  113. package/src/lib/subagent/types.ts +32 -15
  114. package/src/lib/subagent/workflow.ts +26 -13
  115. package/src/lib/virtual-fs/manager.ts +1 -1
  116. package/src/lib/virtual-fs/types.ts +2 -2
  117. package/src/lib/virtual-fs/virtual-fs.test.ts +2 -2
@@ -1,9 +1,7 @@
1
1
  import { describe, expect, it, vi, afterEach } from "vitest";
2
2
  import { z } from "zod";
3
3
 
4
- let capturedSignalHandler:
5
- | ((payload: { childWorkflowId: string; result: unknown }) => void)
6
- | null = null;
4
+ const capturedSignalHandlers = new Map<unknown, (...args: unknown[]) => void>();
7
5
 
8
6
  let nextStartChildResult: ((prompt: string) => unknown) | null = null;
9
7
 
@@ -35,10 +33,10 @@ vi.mock("@temporalio/workflow", () => {
35
33
  workflowId: "child-wf-1",
36
34
  parent: { workflowId: "parent-wf-1" },
37
35
  }),
38
- defineSignal: vi.fn((_name: string) => ({ __signal: true })),
36
+ defineSignal: vi.fn((name: string) => ({ __signal: true, name })),
39
37
  setHandler: vi.fn(
40
- (_signal: unknown, handler: (...a: unknown[]) => void) => {
41
- capturedSignalHandler = handler as typeof capturedSignalHandler;
38
+ (signal: unknown, handler: (...a: unknown[]) => void) => {
39
+ capturedSignalHandlers.set(signal, handler);
42
40
  }
43
41
  ),
44
42
  condition: vi.fn(async (fn: () => boolean) => {
@@ -59,8 +57,10 @@ vi.mock("@temporalio/workflow", () => {
59
57
  usage: { inputTokens: 100, outputTokens: 50 },
60
58
  };
61
59
 
62
- if (capturedSignalHandler) {
63
- capturedSignalHandler({ childWorkflowId: opts.workflowId, result });
60
+ for (const [signal, handler] of capturedSignalHandlers.entries()) {
61
+ if ((signal as { name?: string }).name === "childResult") {
62
+ handler({ childWorkflowId: opts.workflowId, result });
63
+ }
64
64
  }
65
65
 
66
66
  return {
@@ -81,7 +81,13 @@ vi.mock("@temporalio/workflow", () => {
81
81
  ).join("");
82
82
  return `${bytes.slice(0, 8)}-${bytes.slice(8, 12)}-${bytes.slice(12, 16)}-${bytes.slice(16, 20)}-${bytes.slice(20, 32)}`;
83
83
  },
84
- log: { trace: () => {}, debug: () => {}, info: () => {}, warn: () => {}, error: () => {} },
84
+ log: {
85
+ trace: () => {},
86
+ debug: () => {},
87
+ info: () => {},
88
+ warn: () => {},
89
+ error: () => {},
90
+ },
85
91
  };
86
92
  });
87
93
 
@@ -98,7 +104,7 @@ import type {
98
104
  } from "./types";
99
105
  afterEach(() => {
100
106
  nextStartChildResult = null;
101
- capturedSignalHandler = null;
107
+ capturedSignalHandlers.clear();
102
108
  });
103
109
 
104
110
  function mockWorkflow(name?: string): SubagentWorkflow {
@@ -371,7 +377,7 @@ describe("createSubagentHandler", () => {
371
377
  agentName: "inherit-agent",
372
378
  description: "Inherits sandbox",
373
379
  workflow: mockWorkflow(),
374
- sandbox: "inherit",
380
+ sandbox: { source: "inherit", continuation: "continue" },
375
381
  };
376
382
 
377
383
  const { handler } = createSubagentHandler([inheritSubagent]);
@@ -400,7 +406,7 @@ describe("createSubagentHandler", () => {
400
406
  agentName: "inherit-agent",
401
407
  description: "Inherits sandbox",
402
408
  workflow: mockWorkflow(),
403
- sandbox: "inherit",
409
+ sandbox: { source: "inherit", continuation: "continue" },
404
410
  };
405
411
 
406
412
  const { handler } = createSubagentHandler([inheritSubagent]);
@@ -410,12 +416,10 @@ describe("createSubagentHandler", () => {
410
416
  { subagent: "inherit-agent", description: "test", prompt: "test" },
411
417
  { threadId: "t", toolCallId: "tc", toolName: "Subagent" }
412
418
  )
413
- ).rejects.toThrow(
414
- 'sandbox: "inherit" but the parent has no sandbox'
415
- );
419
+ ).rejects.toThrow('sandbox: "inherit" but the parent has no sandbox');
416
420
  });
417
421
 
418
- it("does not pass sandboxId to child when sandbox is own", async () => {
422
+ it("does not pass sandboxId to child when sandbox is own (first call)", async () => {
419
423
  const { startChild } = await import("@temporalio/workflow");
420
424
  const startMock = startChild as ReturnType<typeof vi.fn>;
421
425
 
@@ -423,7 +427,7 @@ describe("createSubagentHandler", () => {
423
427
  agentName: "own-agent",
424
428
  description: "Own sandbox",
425
429
  workflow: mockWorkflow(),
426
- sandbox: "own",
430
+ sandbox: { source: "own", continuation: "fork" },
427
431
  };
428
432
 
429
433
  const { handler } = createSubagentHandler([ownSubagent]);
@@ -496,7 +500,7 @@ describe("createSubagentHandler", () => {
496
500
  expect(context).toEqual({ key: "value" });
497
501
  });
498
502
 
499
- it("does not pass sandboxId when sandbox is own", async () => {
503
+ it("does not pass sandbox init when sandbox is own without prior sandbox", async () => {
500
504
  const { startChild } = await import("@temporalio/workflow");
501
505
  const startMock = startChild as ReturnType<typeof vi.fn>;
502
506
 
@@ -504,7 +508,7 @@ describe("createSubagentHandler", () => {
504
508
  agentName: "own-agent",
505
509
  description: "Own sandbox",
506
510
  workflow: mockWorkflow(),
507
- sandbox: "own",
511
+ sandbox: { source: "own", continuation: "fork" },
508
512
  };
509
513
 
510
514
  const { handler } = createSubagentHandler([ownSubagent]);
@@ -666,7 +670,7 @@ describe("createSubagentHandler", () => {
666
670
  description: "Sandbox continuation",
667
671
  workflow: mockWorkflow(),
668
672
  thread: "fork",
669
- sandbox: "own",
673
+ sandbox: { source: "own", continuation: "fork" },
670
674
  };
671
675
 
672
676
  const { handler } = createSubagentHandler([contSandboxSubagent]);
@@ -722,7 +726,7 @@ describe("createSubagentHandler", () => {
722
726
  description: "Sandbox continuation",
723
727
  workflow: mockWorkflow(),
724
728
  thread: "fork",
725
- sandbox: "own",
729
+ sandbox: { source: "own", continuation: "fork" },
726
730
  };
727
731
 
728
732
  const { handler } = createSubagentHandler([contSandboxSubagent]);
@@ -789,7 +793,7 @@ describe("createSubagentHandler", () => {
789
793
  agentName: "own-agent",
790
794
  description: "Own sandbox",
791
795
  workflow: mockWorkflow(),
792
- sandbox: "own",
796
+ sandbox: { source: "own", continuation: "fork" },
793
797
  };
794
798
 
795
799
  const { handler, destroySubagentSandboxes } = createSubagentHandler([
@@ -819,7 +823,11 @@ describe("createSubagentHandler", () => {
819
823
  agentName: "own-agent",
820
824
  description: "Own sandbox",
821
825
  workflow: mockWorkflow(),
822
- sandbox: { source: "own", shutdown: "pause-until-parent-close" },
826
+ sandbox: {
827
+ source: "own",
828
+ continuation: "fork",
829
+ shutdown: "pause-until-parent-close",
830
+ },
823
831
  };
824
832
 
825
833
  const { handler, destroySubagentSandboxes } = createSubagentHandler([
@@ -847,7 +855,7 @@ describe("createSubagentHandler", () => {
847
855
  agentName: "inherit-agent",
848
856
  description: "Inherits sandbox",
849
857
  workflow: mockWorkflow(),
850
- sandbox: "inherit",
858
+ sandbox: { source: "inherit", continuation: "continue" },
851
859
  };
852
860
 
853
861
  const { handler, destroySubagentSandboxes } = createSubagentHandler([
@@ -961,6 +969,237 @@ describe("createSubagentHandler", () => {
961
969
  expect(childHandle.signal).not.toHaveBeenCalled();
962
970
  });
963
971
 
972
+ // --- inherit + continuation: fork ---
973
+
974
+ it("forks from parent sandbox when inherit + continuation=fork", async () => {
975
+ const { startChild } = await import("@temporalio/workflow");
976
+ const startMock = startChild as ReturnType<typeof vi.fn>;
977
+
978
+ const config: SubagentConfig = {
979
+ agentName: "inherit-fork",
980
+ description: "Inherit fork",
981
+ workflow: mockWorkflow(),
982
+ sandbox: { source: "inherit", continuation: "fork" },
983
+ };
984
+
985
+ const { handler } = createSubagentHandler([config]);
986
+
987
+ await handler(
988
+ { subagent: "inherit-fork", description: "test", prompt: "test" },
989
+ {
990
+ threadId: "t",
991
+ toolCallId: "tc",
992
+ toolName: "Subagent",
993
+ sandboxId: "parent-sb",
994
+ }
995
+ );
996
+
997
+ const lastCall = startMock.mock.calls.at(-1);
998
+ if (!lastCall) throw new Error("expected startChild call");
999
+ const workflowInput = lastCall[1].args[1] as SubagentWorkflowInput;
1000
+ expect(workflowInput.sandbox).toEqual({
1001
+ mode: "fork",
1002
+ sandboxId: "parent-sb",
1003
+ });
1004
+ });
1005
+
1006
+ // --- own + continuation: continue ---
1007
+
1008
+ it("passes sandbox continue on thread continuation with continuation=continue", async () => {
1009
+ const { startChild } = await import("@temporalio/workflow");
1010
+ const startMock = startChild as ReturnType<typeof vi.fn>;
1011
+
1012
+ nextStartChildResult = () => ({
1013
+ toolResponse: "first",
1014
+ data: null,
1015
+ threadId: "child-t-1",
1016
+ sandboxId: "child-sb-1",
1017
+ });
1018
+
1019
+ const config: SubagentConfig = {
1020
+ agentName: "own-cont",
1021
+ description: "Own continue",
1022
+ workflow: mockWorkflow(),
1023
+ thread: "continue",
1024
+ sandbox: { source: "own", continuation: "continue" },
1025
+ };
1026
+
1027
+ const { handler } = createSubagentHandler([config]);
1028
+
1029
+ await handler(
1030
+ { subagent: "own-cont", description: "test", prompt: "first" },
1031
+ { threadId: "t", toolCallId: "tc-1", toolName: "Subagent" }
1032
+ );
1033
+
1034
+ nextStartChildResult = () => ({
1035
+ toolResponse: "second",
1036
+ data: null,
1037
+ threadId: "child-t-1",
1038
+ sandboxId: "child-sb-1",
1039
+ });
1040
+
1041
+ await handler(
1042
+ {
1043
+ subagent: "own-cont",
1044
+ description: "test",
1045
+ prompt: "second",
1046
+ threadId: "child-t-1",
1047
+ },
1048
+ { threadId: "t", toolCallId: "tc-2", toolName: "Subagent" }
1049
+ );
1050
+
1051
+ const secondCall = startMock.mock.calls.at(-1);
1052
+ if (!secondCall) throw new Error("expected startChild call");
1053
+ const workflowInput = secondCall[1].args[1] as SubagentWorkflowInput;
1054
+ expect(workflowInput.sandbox).toEqual({
1055
+ mode: "continue",
1056
+ sandboxId: "child-sb-1",
1057
+ });
1058
+ expect(workflowInput.sandboxShutdown).toBe("pause");
1059
+ });
1060
+
1061
+ // --- own + init: once + continuation: fork ---
1062
+
1063
+ it("stores sandbox on first call and forks from it on second call (init=once, continuation=fork)", async () => {
1064
+ const { startChild } = await import("@temporalio/workflow");
1065
+ const startMock = startChild as ReturnType<typeof vi.fn>;
1066
+
1067
+ nextStartChildResult = () => ({
1068
+ toolResponse: "first",
1069
+ data: null,
1070
+ threadId: "child-t-1",
1071
+ sandboxId: "persistent-sb",
1072
+ });
1073
+
1074
+ const config: SubagentConfig = {
1075
+ agentName: "lazy-fork",
1076
+ description: "Lazy fork",
1077
+ workflow: mockWorkflow(),
1078
+ sandbox: { source: "own", init: "once", continuation: "fork" },
1079
+ };
1080
+
1081
+ const { handler } = createSubagentHandler([config]);
1082
+
1083
+ await handler(
1084
+ { subagent: "lazy-fork", description: "test", prompt: "first" },
1085
+ { threadId: "t", toolCallId: "tc-1", toolName: "Subagent" }
1086
+ );
1087
+
1088
+ // First call: no sandbox init (child creates fresh), forced pause-until-parent-close
1089
+ const firstCall = startMock.mock.calls.at(-1);
1090
+ if (!firstCall) throw new Error("expected startChild call");
1091
+ const firstInput = firstCall[1].args[1] as SubagentWorkflowInput;
1092
+ expect(firstInput.sandbox).toBeUndefined();
1093
+ expect(firstInput.sandboxShutdown).toBe("pause-until-parent-close");
1094
+
1095
+ nextStartChildResult = () => ({
1096
+ toolResponse: "second",
1097
+ data: null,
1098
+ threadId: "child-t-2",
1099
+ sandboxId: "forked-sb",
1100
+ });
1101
+
1102
+ // Second call WITHOUT threadId — should still fork from persistent sandbox
1103
+ await handler(
1104
+ { subagent: "lazy-fork", description: "test", prompt: "second" },
1105
+ { threadId: "t", toolCallId: "tc-2", toolName: "Subagent" }
1106
+ );
1107
+
1108
+ const secondCall = startMock.mock.calls.at(-1);
1109
+ if (!secondCall) throw new Error("expected startChild call");
1110
+ const secondInput = secondCall[1].args[1] as SubagentWorkflowInput;
1111
+ expect(secondInput.sandbox).toEqual({
1112
+ mode: "fork",
1113
+ sandboxId: "persistent-sb",
1114
+ });
1115
+ });
1116
+
1117
+ // --- own + init: once + continuation: continue ---
1118
+
1119
+ it("stores sandbox on first call and continues it on second call (init=once, continuation=continue)", async () => {
1120
+ const { startChild } = await import("@temporalio/workflow");
1121
+ const startMock = startChild as ReturnType<typeof vi.fn>;
1122
+
1123
+ nextStartChildResult = () => ({
1124
+ toolResponse: "first",
1125
+ data: null,
1126
+ threadId: "child-t-1",
1127
+ sandboxId: "persistent-sb",
1128
+ });
1129
+
1130
+ const config: SubagentConfig = {
1131
+ agentName: "lazy-cont",
1132
+ description: "Lazy continue",
1133
+ workflow: mockWorkflow(),
1134
+ sandbox: { source: "own", init: "once", continuation: "continue" },
1135
+ };
1136
+
1137
+ const { handler } = createSubagentHandler([config]);
1138
+
1139
+ await handler(
1140
+ { subagent: "lazy-cont", description: "test", prompt: "first" },
1141
+ { threadId: "t", toolCallId: "tc-1", toolName: "Subagent" }
1142
+ );
1143
+
1144
+ nextStartChildResult = () => ({
1145
+ toolResponse: "second",
1146
+ data: null,
1147
+ threadId: "child-t-2",
1148
+ sandboxId: "persistent-sb",
1149
+ });
1150
+
1151
+ await handler(
1152
+ { subagent: "lazy-cont", description: "test", prompt: "second" },
1153
+ { threadId: "t", toolCallId: "tc-2", toolName: "Subagent" }
1154
+ );
1155
+
1156
+ const secondCall = startMock.mock.calls.at(-1);
1157
+ if (!secondCall) throw new Error("expected startChild call");
1158
+ const secondInput = secondCall[1].args[1] as SubagentWorkflowInput;
1159
+ expect(secondInput.sandbox).toEqual({
1160
+ mode: "continue",
1161
+ sandboxId: "persistent-sb",
1162
+ });
1163
+ expect(secondInput.sandboxShutdown).toBe("pause");
1164
+ });
1165
+
1166
+ // --- init: once cleanup ---
1167
+
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
+
1172
+ nextStartChildResult = () => ({
1173
+ toolResponse: "done",
1174
+ data: null,
1175
+ threadId: "child-t-1",
1176
+ sandboxId: "persistent-sb",
1177
+ });
1178
+
1179
+ const config: SubagentConfig = {
1180
+ agentName: "lazy-cleanup",
1181
+ description: "Lazy cleanup",
1182
+ workflow: mockWorkflow(),
1183
+ sandbox: { source: "own", init: "once", continuation: "fork" },
1184
+ };
1185
+
1186
+ const { handler, destroySubagentSandboxes } = createSubagentHandler([
1187
+ config,
1188
+ ]);
1189
+
1190
+ await handler(
1191
+ { subagent: "lazy-cleanup", description: "test", prompt: "run" },
1192
+ { threadId: "t", toolCallId: "tc", toolName: "Subagent" }
1193
+ );
1194
+
1195
+ await destroySubagentSandboxes();
1196
+
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();
1201
+ });
1202
+
964
1203
  it("returns sandboxId in response when child creates a sandbox", async () => {
965
1204
  nextStartChildResult = () => ({
966
1205
  toolResponse: "done",
@@ -973,7 +1212,7 @@ describe("createSubagentHandler", () => {
973
1212
  agentName: "own-agent",
974
1213
  description: "Own sandbox",
975
1214
  workflow: mockWorkflow(),
976
- sandbox: "own",
1215
+ sandbox: { source: "own", continuation: "fork" },
977
1216
  };
978
1217
 
979
1218
  const { handler } = createSubagentHandler([ownSubagent]);
@@ -1083,6 +1322,208 @@ describe("createSubagentHandler", () => {
1083
1322
 
1084
1323
  expect(result.metadata).toBeUndefined();
1085
1324
  });
1325
+
1326
+ // --- keep-until-parent-close ---
1327
+
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>;
1331
+
1332
+ const ownSubagent: SubagentConfig = {
1333
+ agentName: "own-keep",
1334
+ description: "Own sandbox kept",
1335
+ workflow: mockWorkflow(),
1336
+ sandbox: {
1337
+ source: "own",
1338
+ continuation: "fork",
1339
+ shutdown: "keep-until-parent-close",
1340
+ },
1341
+ };
1342
+
1343
+ const { handler, destroySubagentSandboxes } = createSubagentHandler([
1344
+ ownSubagent,
1345
+ ]);
1346
+
1347
+ await handler(
1348
+ { subagent: "own-keep", description: "test", prompt: "run" },
1349
+ { threadId: "t", toolCallId: "tc", toolName: "Subagent" }
1350
+ );
1351
+
1352
+ await destroySubagentSandboxes();
1353
+
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();
1358
+ });
1359
+
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
+
1364
+ const ownSubagent: SubagentConfig = {
1365
+ agentName: "own-keep-plain",
1366
+ description: "Own sandbox keep",
1367
+ workflow: mockWorkflow(),
1368
+ sandbox: { source: "own", continuation: "fork", shutdown: "keep" },
1369
+ };
1370
+
1371
+ const { handler, destroySubagentSandboxes } = createSubagentHandler([
1372
+ ownSubagent,
1373
+ ]);
1374
+
1375
+ await handler(
1376
+ { subagent: "own-keep-plain", description: "test", prompt: "run" },
1377
+ { threadId: "t", toolCallId: "tc", toolName: "Subagent" }
1378
+ );
1379
+
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
+ await destroySubagentSandboxes();
1386
+
1387
+ expect(childHandle.signal).not.toHaveBeenCalled();
1388
+ });
1389
+
1390
+ // --- mustSurvive does not override user shutdown ---
1391
+
1392
+ it("does not override keep-until-parent-close with pause-until-parent-close for init=once", async () => {
1393
+ const { startChild } = await import("@temporalio/workflow");
1394
+ const startMock = startChild as ReturnType<typeof vi.fn>;
1395
+
1396
+ nextStartChildResult = () => ({
1397
+ toolResponse: "first",
1398
+ data: null,
1399
+ threadId: "child-t-1",
1400
+ sandboxId: "persistent-sb",
1401
+ });
1402
+
1403
+ const config: SubagentConfig = {
1404
+ agentName: "lazy-keep",
1405
+ description: "Lazy keep",
1406
+ workflow: mockWorkflow(),
1407
+ sandbox: {
1408
+ source: "own",
1409
+ init: "once",
1410
+ continuation: "fork",
1411
+ shutdown: "keep-until-parent-close",
1412
+ },
1413
+ };
1414
+
1415
+ const { handler } = createSubagentHandler([config]);
1416
+
1417
+ await handler(
1418
+ { subagent: "lazy-keep", description: "test", prompt: "first" },
1419
+ { threadId: "t", toolCallId: "tc-1", toolName: "Subagent" }
1420
+ );
1421
+
1422
+ const firstCall = startMock.mock.calls.at(-1);
1423
+ if (!firstCall) throw new Error("expected startChild call");
1424
+ const firstInput = firstCall[1].args[1] as SubagentWorkflowInput;
1425
+ expect(firstInput.sandboxShutdown).toBe("keep-until-parent-close");
1426
+ });
1427
+
1428
+ it("does not override pause with pause-until-parent-close for init=once", async () => {
1429
+ const { startChild } = await import("@temporalio/workflow");
1430
+ const startMock = startChild as ReturnType<typeof vi.fn>;
1431
+
1432
+ nextStartChildResult = () => ({
1433
+ toolResponse: "first",
1434
+ data: null,
1435
+ threadId: "child-t-1",
1436
+ sandboxId: "persistent-sb",
1437
+ });
1438
+
1439
+ const config: SubagentConfig = {
1440
+ agentName: "lazy-pause",
1441
+ description: "Lazy pause",
1442
+ workflow: mockWorkflow(),
1443
+ sandbox: {
1444
+ source: "own",
1445
+ init: "once",
1446
+ continuation: "fork",
1447
+ shutdown: "pause",
1448
+ },
1449
+ };
1450
+
1451
+ const { handler } = createSubagentHandler([config]);
1452
+
1453
+ await handler(
1454
+ { subagent: "lazy-pause", description: "test", prompt: "first" },
1455
+ { threadId: "t", toolCallId: "tc-1", toolName: "Subagent" }
1456
+ );
1457
+
1458
+ const firstCall = startMock.mock.calls.at(-1);
1459
+ if (!firstCall) throw new Error("expected startChild call");
1460
+ const firstInput = firstCall[1].args[1] as SubagentWorkflowInput;
1461
+ expect(firstInput.sandboxShutdown).toBe("pause");
1462
+ });
1463
+
1464
+ it("does not override keep with pause for continuation=continue", async () => {
1465
+ const { startChild } = await import("@temporalio/workflow");
1466
+ const startMock = startChild as ReturnType<typeof vi.fn>;
1467
+
1468
+ nextStartChildResult = () => ({
1469
+ toolResponse: "first",
1470
+ data: null,
1471
+ threadId: "child-t-1",
1472
+ sandboxId: "child-sb-1",
1473
+ });
1474
+
1475
+ const config: SubagentConfig = {
1476
+ agentName: "cont-keep",
1477
+ description: "Continue keep",
1478
+ workflow: mockWorkflow(),
1479
+ thread: "continue",
1480
+ sandbox: { source: "own", continuation: "continue", shutdown: "keep" },
1481
+ };
1482
+
1483
+ const { handler } = createSubagentHandler([config]);
1484
+
1485
+ await handler(
1486
+ { subagent: "cont-keep", description: "test", prompt: "first" },
1487
+ { threadId: "t", toolCallId: "tc-1", toolName: "Subagent" }
1488
+ );
1489
+
1490
+ const firstCall = startMock.mock.calls.at(-1);
1491
+ if (!firstCall) throw new Error("expected startChild call");
1492
+ const firstInput = firstCall[1].args[1] as SubagentWorkflowInput;
1493
+ expect(firstInput.sandboxShutdown).toBe("keep");
1494
+ });
1495
+
1496
+ it("still overrides destroy with pause for continuation=continue", async () => {
1497
+ const { startChild } = await import("@temporalio/workflow");
1498
+ const startMock = startChild as ReturnType<typeof vi.fn>;
1499
+
1500
+ nextStartChildResult = () => ({
1501
+ toolResponse: "first",
1502
+ data: null,
1503
+ threadId: "child-t-1",
1504
+ sandboxId: "child-sb-1",
1505
+ });
1506
+
1507
+ const config: SubagentConfig = {
1508
+ agentName: "cont-destroy",
1509
+ description: "Continue destroy",
1510
+ workflow: mockWorkflow(),
1511
+ thread: "continue",
1512
+ sandbox: { source: "own", continuation: "continue", shutdown: "destroy" },
1513
+ };
1514
+
1515
+ const { handler } = createSubagentHandler([config]);
1516
+
1517
+ await handler(
1518
+ { subagent: "cont-destroy", description: "test", prompt: "first" },
1519
+ { threadId: "t", toolCallId: "tc-1", toolName: "Subagent" }
1520
+ );
1521
+
1522
+ const firstCall = startMock.mock.calls.at(-1);
1523
+ if (!firstCall) throw new Error("expected startChild call");
1524
+ const firstInput = firstCall[1].args[1] as SubagentWorkflowInput;
1525
+ expect(firstInput.sandboxShutdown).toBe("pause");
1526
+ });
1086
1527
  });
1087
1528
 
1088
1529
  // ---------------------------------------------------------------------------
@@ -1289,6 +1730,7 @@ describe("defineSubagentWorkflow", () => {
1289
1730
  agentName: "test",
1290
1731
  sandboxShutdown: "destroy",
1291
1732
  thread: { mode: "fork", threadId: "prev-42" },
1733
+ onSandboxReady: expect.any(Function),
1292
1734
  });
1293
1735
  });
1294
1736
 
@@ -1307,6 +1749,7 @@ describe("defineSubagentWorkflow", () => {
1307
1749
  agentName: "test",
1308
1750
  sandboxShutdown: "destroy",
1309
1751
  sandbox: { mode: "inherit", sandboxId: "sb-123" },
1752
+ onSandboxReady: expect.any(Function),
1310
1753
  });
1311
1754
  });
1312
1755
 
@@ -1325,6 +1768,7 @@ describe("defineSubagentWorkflow", () => {
1325
1768
  agentName: "test",
1326
1769
  sandboxShutdown: "destroy",
1327
1770
  sandbox: { mode: "fork", sandboxId: "prev-sb-1" },
1771
+ onSandboxReady: expect.any(Function),
1328
1772
  });
1329
1773
  });
1330
1774
 
@@ -1347,6 +1791,7 @@ describe("defineSubagentWorkflow", () => {
1347
1791
  sandboxShutdown: "destroy",
1348
1792
  thread: { mode: "fork", threadId: "prev-t" },
1349
1793
  sandbox: { mode: "fork", sandboxId: "prev-sb" },
1794
+ onSandboxReady: expect.any(Function),
1350
1795
  });
1351
1796
  });
1352
1797
 
@@ -1416,6 +1861,68 @@ describe("defineSubagentWorkflow", () => {
1416
1861
  expect(capturedSession).toEqual({
1417
1862
  agentName: "test",
1418
1863
  sandboxShutdown: "destroy",
1864
+ onSandboxReady: expect.any(Function),
1419
1865
  });
1420
1866
  });
1867
+
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
+ it("uses keep-until-parent-close from workflowInput override in sessionInput", async () => {
1911
+ let capturedSession: SubagentSessionInput | undefined;
1912
+ const workflow = defineSubagentWorkflow(
1913
+ { name: "test", description: "test agent" },
1914
+ async (_prompt, sessionInput) => {
1915
+ capturedSession = sessionInput;
1916
+ return { toolResponse: "ok", data: null, threadId: "t" };
1917
+ }
1918
+ );
1919
+
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
+ }
1926
+ expect(capturedSession?.sandboxShutdown).toBe("keep-until-parent-close");
1927
+ });
1421
1928
  });