@stigmer/runner 3.12.3 → 3.12.5

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 (227) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/call-http.js +12 -0
  3. package/dist/activities/call-http.js.map +1 -1
  4. package/dist/activities/call-llm.d.ts +18 -0
  5. package/dist/activities/call-llm.js +56 -2
  6. package/dist/activities/call-llm.js.map +1 -1
  7. package/dist/activities/execute-cursor/agent-session-cache.d.ts +72 -0
  8. package/dist/activities/execute-cursor/agent-session-cache.js +186 -0
  9. package/dist/activities/execute-cursor/agent-session-cache.js.map +1 -0
  10. package/dist/activities/execute-cursor/cursor-mcp-config.d.ts +45 -0
  11. package/dist/activities/execute-cursor/cursor-mcp-config.js +76 -0
  12. package/dist/activities/execute-cursor/cursor-mcp-config.js.map +1 -0
  13. package/dist/activities/execute-cursor/index.d.ts +10 -1
  14. package/dist/activities/execute-cursor/index.js +108 -57
  15. package/dist/activities/execute-cursor/index.js.map +1 -1
  16. package/dist/activities/execute-cursor/prompt-builder.d.ts +13 -2
  17. package/dist/activities/execute-cursor/prompt-builder.js +11 -0
  18. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  19. package/dist/activities/execute-cursor/service-tier.d.ts +5 -15
  20. package/dist/activities/execute-cursor/service-tier.js +5 -21
  21. package/dist/activities/execute-cursor/service-tier.js.map +1 -1
  22. package/dist/activities/execute-cursor/session-lifecycle.d.ts +1 -1
  23. package/dist/activities/execute-cursor/skill-resolver.d.ts +2 -21
  24. package/dist/activities/execute-cursor/skill-resolver.js +5 -108
  25. package/dist/activities/execute-cursor/skill-resolver.js.map +1 -1
  26. package/dist/activities/execute-deep-agent/approval-file-change.d.ts +9 -0
  27. package/dist/activities/execute-deep-agent/approval-file-change.js +2 -1
  28. package/dist/activities/execute-deep-agent/approval-file-change.js.map +1 -1
  29. package/dist/activities/execute-deep-agent/cas-capture-backend.d.ts +18 -1
  30. package/dist/activities/execute-deep-agent/cas-capture-backend.js +20 -3
  31. package/dist/activities/execute-deep-agent/cas-capture-backend.js.map +1 -1
  32. package/dist/activities/execute-deep-agent/index.js +57 -5
  33. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  34. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +9 -0
  35. package/dist/activities/execute-deep-agent/prompt-builder.js +10 -0
  36. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  37. package/dist/activities/execute-deep-agent/setup.js +31 -16
  38. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  39. package/dist/activities/execute-deep-agent/shell-env.d.ts +8 -5
  40. package/dist/activities/execute-deep-agent/shell-env.js +10 -7
  41. package/dist/activities/execute-deep-agent/shell-env.js.map +1 -1
  42. package/dist/activities/execute-deep-agent/subagent-transformer.js +17 -11
  43. package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
  44. package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +3 -3
  45. package/dist/config.js +10 -5
  46. package/dist/config.js.map +1 -1
  47. package/dist/encryption/config.js +7 -2
  48. package/dist/encryption/config.js.map +1 -1
  49. package/dist/index.d.ts +1 -1
  50. package/dist/index.js +1 -1
  51. package/dist/main.js +12 -6
  52. package/dist/main.js.map +1 -1
  53. package/dist/middleware/path-normalization.d.ts +49 -49
  54. package/dist/middleware/path-normalization.js +73 -84
  55. package/dist/middleware/path-normalization.js.map +1 -1
  56. package/dist/middleware/types.d.ts +3 -3
  57. package/dist/payload-codecs.js +2 -1
  58. package/dist/payload-codecs.js.map +1 -1
  59. package/dist/runner-manager.d.ts +1 -1
  60. package/dist/runner-manager.js +42 -22
  61. package/dist/runner-manager.js.map +1 -1
  62. package/dist/runner.js +30 -6
  63. package/dist/runner.js.map +1 -1
  64. package/dist/shared/args-preview.d.ts +8 -0
  65. package/dist/shared/args-preview.js +14 -3
  66. package/dist/shared/args-preview.js.map +1 -1
  67. package/dist/shared/artifact-storage.d.ts +11 -1
  68. package/dist/shared/artifact-storage.js +10 -1
  69. package/dist/shared/artifact-storage.js.map +1 -1
  70. package/dist/shared/declared-preferences.d.ts +47 -0
  71. package/dist/shared/declared-preferences.js +64 -0
  72. package/dist/shared/declared-preferences.js.map +1 -0
  73. package/dist/shared/fingerprint-secret.d.ts +3 -2
  74. package/dist/shared/fingerprint-secret.js +5 -3
  75. package/dist/shared/fingerprint-secret.js.map +1 -1
  76. package/dist/shared/llm-backend.js +8 -1
  77. package/dist/shared/llm-backend.js.map +1 -1
  78. package/dist/shared/mcp-enabled-tools.d.ts +2 -3
  79. package/dist/shared/mcp-enabled-tools.js +2 -3
  80. package/dist/shared/mcp-enabled-tools.js.map +1 -1
  81. package/dist/shared/mcp-resolver.d.ts +20 -15
  82. package/dist/shared/mcp-resolver.js +11 -12
  83. package/dist/shared/mcp-resolver.js.map +1 -1
  84. package/dist/shared/model-client.d.ts +15 -0
  85. package/dist/shared/model-client.js +57 -13
  86. package/dist/shared/model-client.js.map +1 -1
  87. package/dist/shared/placeholder-resolver.d.ts +9 -2
  88. package/dist/shared/placeholder-resolver.js +9 -2
  89. package/dist/shared/placeholder-resolver.js.map +1 -1
  90. package/dist/shared/plan-mode-permissions.d.ts +26 -46
  91. package/dist/shared/plan-mode-permissions.js +27 -55
  92. package/dist/shared/plan-mode-permissions.js.map +1 -1
  93. package/dist/shared/registry-endpoint.d.ts +5 -0
  94. package/dist/shared/registry-endpoint.js +7 -1
  95. package/dist/shared/registry-endpoint.js.map +1 -1
  96. package/dist/shared/runner-credential-keys.d.ts +26 -1
  97. package/dist/shared/runner-credential-keys.js +34 -1
  98. package/dist/shared/runner-credential-keys.js.map +1 -1
  99. package/dist/shared/runner-credential-store.d.ts +77 -0
  100. package/dist/shared/runner-credential-store.js +111 -0
  101. package/dist/shared/runner-credential-store.js.map +1 -0
  102. package/dist/shared/service-tier.d.ts +55 -0
  103. package/dist/shared/service-tier.js +67 -0
  104. package/dist/shared/service-tier.js.map +1 -0
  105. package/dist/shared/skill-mount.d.ts +89 -0
  106. package/dist/shared/skill-mount.js +142 -0
  107. package/dist/shared/skill-mount.js.map +1 -0
  108. package/dist/shared/skill-writer.d.ts +28 -26
  109. package/dist/shared/skill-writer.js +79 -102
  110. package/dist/shared/skill-writer.js.map +1 -1
  111. package/dist/shared/worker-shutdown.d.ts +67 -0
  112. package/dist/shared/worker-shutdown.js +79 -0
  113. package/dist/shared/worker-shutdown.js.map +1 -0
  114. package/dist/shared/workspace/types.d.ts +3 -2
  115. package/dist/shared/zip-extract.d.ts +10 -3
  116. package/dist/shared/zip-extract.js +10 -3
  117. package/dist/shared/zip-extract.js.map +1 -1
  118. package/dist/workflow-engine/loader.js +38 -1
  119. package/dist/workflow-engine/loader.js.map +1 -1
  120. package/dist/workflow-engine/tasks/call-function.d.ts +14 -0
  121. package/dist/workflow-engine/tasks/call-function.js +49 -5
  122. package/dist/workflow-engine/tasks/call-function.js.map +1 -1
  123. package/dist/workflow-engine/tasks/human-input.js +23 -2
  124. package/dist/workflow-engine/tasks/human-input.js.map +1 -1
  125. package/dist/workflow-engine/types.d.ts +6 -0
  126. package/dist/workflow-engine/types.js.map +1 -1
  127. package/dist/workflows/engine-core.js +36 -8
  128. package/dist/workflows/engine-core.js.map +1 -1
  129. package/package.json +15 -11
  130. package/src/activities/__tests__/call-http.test.ts +36 -0
  131. package/src/activities/__tests__/call-llm.test.ts +77 -0
  132. package/src/activities/call-http.ts +17 -0
  133. package/src/activities/call-llm.ts +78 -2
  134. package/src/activities/execute-cursor/__tests__/agent-session-cache.test.ts +220 -0
  135. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +50 -0
  136. package/src/activities/execute-cursor/__tests__/cursor-mcp-config.test.ts +111 -0
  137. package/src/activities/execute-cursor/__tests__/service-tier.test.ts +1 -1
  138. package/src/activities/execute-cursor/__tests__/skill-resolver.test.ts +3 -94
  139. package/src/activities/execute-cursor/agent-session-cache.ts +229 -0
  140. package/src/activities/execute-cursor/cursor-mcp-config.ts +107 -0
  141. package/src/activities/execute-cursor/index.ts +129 -55
  142. package/src/activities/execute-cursor/prompt-builder.ts +29 -2
  143. package/src/activities/execute-cursor/service-tier.ts +5 -29
  144. package/src/activities/execute-cursor/session-lifecycle.ts +1 -1
  145. package/src/activities/execute-cursor/skill-resolver.ts +10 -137
  146. package/src/activities/execute-deep-agent/__tests__/approval-file-change.test.ts +15 -0
  147. package/src/activities/execute-deep-agent/__tests__/cas-capture-backend.test.ts +75 -0
  148. package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +1 -0
  149. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
  150. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
  151. package/src/activities/execute-deep-agent/__tests__/index.test.ts +1 -0
  152. package/src/activities/execute-deep-agent/__tests__/plan-mode-path-normalization.test.ts +42 -29
  153. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +52 -0
  154. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
  155. package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +11 -8
  156. package/src/activities/execute-deep-agent/approval-file-change.ts +11 -1
  157. package/src/activities/execute-deep-agent/cas-capture-backend.ts +20 -3
  158. package/src/activities/execute-deep-agent/index.ts +57 -5
  159. package/src/activities/execute-deep-agent/prompt-builder.ts +22 -0
  160. package/src/activities/execute-deep-agent/setup.ts +36 -19
  161. package/src/activities/execute-deep-agent/shell-env.ts +10 -7
  162. package/src/activities/execute-deep-agent/subagent-transformer.ts +19 -12
  163. package/src/activities/execute-deep-agent/subagent-wiring.ts +3 -3
  164. package/src/config.ts +10 -5
  165. package/src/encryption/config.ts +8 -2
  166. package/src/index.ts +1 -1
  167. package/src/main.ts +16 -6
  168. package/src/middleware/__tests__/path-normalization.test.ts +32 -32
  169. package/src/middleware/path-normalization.ts +78 -90
  170. package/src/middleware/types.ts +3 -3
  171. package/src/payload-codecs.ts +2 -1
  172. package/src/runner-manager.ts +55 -22
  173. package/src/runner.ts +39 -6
  174. package/src/shared/__tests__/artifact-storage.test.ts +28 -0
  175. package/src/shared/__tests__/bedrock-seam.test.ts +5 -5
  176. package/src/shared/__tests__/declared-preferences.test.ts +109 -0
  177. package/src/shared/__tests__/foundry-seam.test.ts +5 -5
  178. package/src/shared/__tests__/model-client.test.ts +99 -0
  179. package/src/shared/__tests__/plan-mode-permissions.test.ts +15 -44
  180. package/src/shared/__tests__/runner-credential-store.test.ts +155 -0
  181. package/src/shared/__tests__/skill-mount.test.ts +238 -0
  182. package/src/shared/__tests__/skill-writer.test.ts +151 -128
  183. package/src/shared/__tests__/vertex-adapter.test.ts +30 -3
  184. package/src/shared/__tests__/vertex-seam.test.ts +5 -5
  185. package/src/shared/__tests__/worker-shutdown.test.ts +101 -0
  186. package/src/shared/__tests__/zip-extract.test.ts +46 -11
  187. package/src/shared/args-preview.ts +17 -3
  188. package/src/shared/artifact-storage.ts +20 -1
  189. package/src/shared/declared-preferences.ts +84 -0
  190. package/src/shared/fingerprint-secret.ts +5 -3
  191. package/src/shared/llm-backend.ts +7 -1
  192. package/src/shared/mcp-enabled-tools.ts +2 -3
  193. package/src/shared/mcp-resolver.ts +20 -20
  194. package/src/shared/model-client.ts +76 -13
  195. package/src/shared/placeholder-resolver.ts +9 -2
  196. package/src/shared/plan-mode-permissions.ts +27 -58
  197. package/src/shared/registry-endpoint.ts +9 -1
  198. package/src/shared/runner-credential-keys.ts +36 -1
  199. package/src/shared/runner-credential-store.ts +115 -0
  200. package/src/shared/service-tier.ts +78 -0
  201. package/src/shared/skill-mount.ts +179 -0
  202. package/src/shared/skill-writer.ts +96 -130
  203. package/src/shared/worker-shutdown.ts +99 -0
  204. package/src/shared/workspace/types.ts +3 -2
  205. package/src/shared/zip-extract.ts +14 -7
  206. package/src/workflow-engine/__tests__/golden-execution.test.ts +20 -1
  207. package/src/workflow-engine/__tests__/loader.test.ts +51 -0
  208. package/src/workflow-engine/__tests__/tasks/call-function.test.ts +105 -0
  209. package/src/workflow-engine/__tests__/tasks/human-input.test.ts +117 -0
  210. package/src/workflow-engine/loader.ts +46 -1
  211. package/src/workflow-engine/tasks/call-function.ts +74 -13
  212. package/src/workflow-engine/tasks/human-input.ts +33 -5
  213. package/src/workflow-engine/types.ts +6 -0
  214. package/src/workflows/engine-core.ts +39 -8
  215. package/dist/activities/execute-cursor/connect-backfill.d.ts +0 -19
  216. package/dist/activities/execute-cursor/connect-backfill.js +0 -27
  217. package/dist/activities/execute-cursor/connect-backfill.js.map +0 -1
  218. package/dist/activities/execute-cursor/mcp-resolver.d.ts +0 -120
  219. package/dist/activities/execute-cursor/mcp-resolver.js +0 -194
  220. package/dist/activities/execute-cursor/mcp-resolver.js.map +0 -1
  221. package/dist/activities/execute-cursor/placeholder-resolver.d.ts +0 -34
  222. package/dist/activities/execute-cursor/placeholder-resolver.js +0 -82
  223. package/dist/activities/execute-cursor/placeholder-resolver.js.map +0 -1
  224. package/src/activities/execute-cursor/__tests__/mcp-resolver.test.ts +0 -125
  225. package/src/activities/execute-cursor/connect-backfill.ts +0 -52
  226. package/src/activities/execute-cursor/mcp-resolver.ts +0 -324
  227. package/src/activities/execute-cursor/placeholder-resolver.ts +0 -109
@@ -35,36 +35,47 @@ async function argsSeenByHandler(
35
35
  }
36
36
 
37
37
  describe("normalizeWorkspacePathArg", () => {
38
- it("maps a workspace-relative path to its rootDir-joined absolute form", () => {
39
- expect(normalizeWorkspacePathArg("src/main.py", ROOT)).toBe(`${ROOT}/src/main.py`);
38
+ it("maps a workspace-relative path to its virtual-absolute form", () => {
39
+ expect(normalizeWorkspacePathArg("src/main.py", ROOT)).toBe("/src/main.py");
40
40
  });
41
41
 
42
- it("strips a leading ./ before joining", () => {
43
- expect(normalizeWorkspacePathArg("./notes.md", ROOT)).toBe(`${ROOT}/notes.md`);
42
+ it("strips a leading ./ before rooting", () => {
43
+ expect(normalizeWorkspacePathArg("./notes.md", ROOT)).toBe("/notes.md");
44
44
  });
45
45
 
46
46
  it("handles multi-workspace entry-relative paths", () => {
47
47
  expect(normalizeWorkspacePathArg("entry-1/src/main.py", ROOT)).toBe(
48
- `${ROOT}/entry-1/src/main.py`,
48
+ "/entry-1/src/main.py",
49
49
  );
50
50
  });
51
51
 
52
52
  it("resolves interior .. segments that stay inside the root", () => {
53
- expect(normalizeWorkspacePathArg("src/../notes.md", ROOT)).toBe(`${ROOT}/notes.md`);
53
+ expect(normalizeWorkspacePathArg("src/../notes.md", ROOT)).toBe("/notes.md");
54
+ // Same repair on an already-virtual path: the virtual resolver rejects
55
+ // `..` outright, so the safe interior collapse happens at this seam.
56
+ expect(normalizeWorkspacePathArg("/src/../notes.md", ROOT)).toBe("/notes.md");
54
57
  });
55
58
 
56
- it("leaves absolute paths untouched", () => {
59
+ it("maps a REAL-absolute in-root path to its virtual form (legacy-dialect compatibility)", () => {
60
+ expect(normalizeWorkspacePathArg(`${ROOT}/src/main.py`, ROOT)).toBe("/src/main.py");
61
+ // The root itself is the virtual root.
62
+ expect(normalizeWorkspacePathArg(ROOT, ROOT)).toBe("/");
63
+ });
64
+
65
+ it("leaves canonical virtual-absolute paths untouched", () => {
66
+ // In the virtual dialect these are already canonical names — "/etc/hosts"
67
+ // means the WORKSPACE's etc/hosts, not the host file.
57
68
  expect(normalizeWorkspacePathArg("/etc/hosts", ROOT)).toBeUndefined();
58
- expect(normalizeWorkspacePathArg(`${ROOT}/src/main.py`, ROOT)).toBeUndefined();
59
69
  expect(normalizeWorkspacePathArg("/", ROOT)).toBeUndefined();
60
70
  });
61
71
 
62
72
  it("refuses to rewrite relatives that escape the workspace root", () => {
63
- // A naive join would resolve `..` away and hand upstream validation a
64
- // clean out-of-root absolute path an out-of-root read that today's
65
- // validation refuses. The guard keeps the refusal.
73
+ // posix.normalize would silently swallow a leading ".." on an absolute
74
+ // path rewriting the call onto a DIFFERENT in-root file. The escape is
75
+ // detected in relative form and left raw for the upstream refusal.
66
76
  expect(normalizeWorkspacePathArg("../sibling/secret.txt", ROOT)).toBeUndefined();
67
77
  expect(normalizeWorkspacePathArg("src/../../escape.txt", ROOT)).toBeUndefined();
78
+ expect(normalizeWorkspacePathArg("/src/../../escape.txt", ROOT)).toBeUndefined();
68
79
  });
69
80
 
70
81
  it("leaves ~-carrying paths untouched (upstream refuses them either way)", () => {
@@ -84,7 +95,7 @@ describe("createPathNormalizationMiddleware", () => {
84
95
  file_path: "src/main.py",
85
96
  content: "x",
86
97
  });
87
- expect(args.file_path).toBe(`${ROOT}/src/main.py`);
98
+ expect(args.file_path).toBe("/src/main.py");
88
99
  // Sibling args ride along untouched.
89
100
  expect(args.content).toBe("x");
90
101
  }
@@ -93,27 +104,26 @@ describe("createPathNormalizationMiddleware", () => {
93
104
  it("rewrites the base path on ls/glob/grep", async () => {
94
105
  for (const tool of ["ls", "glob", "grep"]) {
95
106
  const args = await argsSeenByHandler(tool, { path: "src", pattern: "**/*.py" });
96
- expect(args.path).toBe(`${ROOT}/src`);
107
+ expect(args.path).toBe("/src");
97
108
  // The pattern is never a path — byte-untouched.
98
109
  expect(args.pattern).toBe("**/*.py");
99
110
  }
100
111
  });
101
112
 
102
- it("passes absolute paths through byte-untouched", async () => {
113
+ it("passes canonical virtual paths through byte-untouched", async () => {
103
114
  const args = await argsSeenByHandler("read_file", { file_path: "/etc/hosts" });
104
115
  expect(args.file_path).toBe("/etc/hosts");
105
116
  });
106
117
 
107
- it("supplies the workspace root when the base path is omitted on ls/glob/grep", async () => {
108
- // Deliberate reversal of the original #429 pin ("leaves an omitted base
109
- // path alone"): the tools' schema default is "/" the OS ROOT under the
110
- // legacy backend applied inside the tool AFTER this seam, so under the
111
- // #528 workspace read boundary an untouched omission would deny the bare
112
- // first listing. The middleware fills the omission with the root it
113
- // already knows.
118
+ it("leaves an omitted base path alone — the tools' '/' default IS the workspace root now", async () => {
119
+ // Deliberate reversal of the #528 injection: under the legacy backend the
120
+ // schema default "/" meant the OS ROOT, so the middleware had to fill the
121
+ // omission. Under the virtual root (issue #754) the default already means
122
+ // the workspace root filling it would just duplicate the tool's own
123
+ // behavior.
114
124
  for (const tool of ["ls", "glob", "grep"]) {
115
125
  const args = await argsSeenByHandler(tool, { pattern: "TOKEN" });
116
- expect(args.path).toBe(ROOT);
126
+ expect(args.path).toBeUndefined();
117
127
  expect(args.pattern).toBe("TOKEN");
118
128
  }
119
129
  });
@@ -125,16 +135,6 @@ describe("createPathNormalizationMiddleware", () => {
125
135
  expect(args.file_path).toBeUndefined();
126
136
  });
127
137
 
128
- it("leaves an explicit '/' untouched on the dir tools — the rules answer it honestly", async () => {
129
- // The model asked for the OS root; under plan mode's read boundary the
130
- // honest answer is the rules' denial, not silently substituted workspace
131
- // contents (the #429 never-convert-a-refusal-into-an-allowance doctrine).
132
- for (const tool of ["ls", "glob", "grep"]) {
133
- const args = await argsSeenByHandler(tool, { path: "/" });
134
- expect(args.path).toBe("/");
135
- }
136
- });
137
-
138
138
  it("never touches tools outside the built-in filesystem set", async () => {
139
139
  // An MCP tool could plausibly carry a same-named arg with different
140
140
  // semantics; only the six built-in names are rewritten.
@@ -1,55 +1,49 @@
1
1
  /**
2
- * Workspace-relative path normalization for permission-rule-bearing graphs
3
- * (issues #429, #528).
2
+ * Canonical virtual-path normalization for the native harness's built-in
3
+ * filesystem tools (issues #429, #528, #754).
4
4
  *
5
- * deepagents' permission enforcement canonicalizes tool-call paths BEFORE any
6
- * rule or backend runs, and its validation refuses non-absolute paths — on
7
- * EVERY filesystem tool call once a graph carries any permission rules, reads
8
- * included. Plan mode is the only rule-bearing production configuration
9
- * (shared/plan-mode-permissions.ts), so there a workspace-relative path
10
- * the shape the multi-workspace prompt explicitly mandates, and one models
11
- * routinely choose in single-workspace sessions — fails with
12
- * `path must be absolute` instead of just working, burning tool rounds until
13
- * the model adapts by switching to absolute paths.
5
+ * The native harness speaks ONE path dialect end to end: the VIRTUAL ROOT,
6
+ * where "/" denotes the workspace root (backends are constructed with
7
+ * `virtualMode: true` see execute-deep-agent/cas-capture-backend.ts). This
8
+ * middleware is the dialect-repair seam at the model boundary. It rewrites,
9
+ * on the deepagents built-in filesystem tools only:
14
10
  *
15
- * This middleware repairs the contract at our own seam: it rewrites
16
- * workspace-relative paths on the deepagents built-in filesystem tools to
17
- * workspace-absolute before the tool (and therefore enforcement) runs. The
18
- * write-deny rule then fires as designed and reads succeed on the first try.
19
- * Rule-less (act-mode) graphs never install it the legacy backend already
20
- * resolves relative paths under the workspace root, so act mode keeps a
21
- * byte-zero delta.
11
+ * - workspace-relative paths ("src/x.py", "./notes.md") to their virtual
12
+ * absolute form ("/src/x.py") deepagents' permission validation refuses
13
+ * non-absolute shapes on rule-bearing graphs (plan mode), and the prompt
14
+ * explicitly mandates entry-relative paths, so without this seam a
15
+ * prompt-compliant call would die with `path must be absolute`;
16
+ * - REAL-absolute paths under the workspace root ("{root}/src/x.py") to
17
+ * their virtual form ("/src/x.py") — the compatibility mapping for the
18
+ * pre-#754 dialect, where tool results and transcripts surfaced real
19
+ * filesystem paths that a model may still echo back;
20
+ * - interior `..` segments that stay inside the root ("src/../notes.md" →
21
+ * "/notes.md") — the virtual resolver rejects `..` outright, so a safe
22
+ * interior collapse is repaired here rather than burning a tool round.
22
23
  *
23
- * It also supplies the workspace root when `ls`/`glob`/`grep` are called
24
- * with NO path argument (issue #528). Those tools' schema default is "/" —
25
- * the OS ROOT under the legacy backend — and the default is applied inside
26
- * the tool, after this seam, so under the workspace read boundary a bare
27
- * first `ls` would otherwise die with `permission denied for read on /`
28
- * (the same first-turn degradation class #429 fixed). An EXPLICIT "/" is
29
- * deliberately NOT rewritten: the model asked for the OS root, and the
30
- * honest answer under plan mode's read boundary is the rules' denial —
31
- * silently substituting workspace contents would be an answer to a
32
- * different question.
24
+ * Nothing becomes newly reachable: escaping relatives ("../x") and
25
+ * `~`-carrying paths are left raw so the upstream refusal keeps speaking,
26
+ * and every rewritten path is workspace-confined by the virtual resolver
27
+ * regardless. The middleware converts false errors into correct behavior
28
+ * never a refusal into an allowance.
33
29
  *
34
- * Invariant nothing becomes newly reachable: only paths whose resolution
35
- * stays INSIDE the workspace root are rewritten. Escaping relatives (`../x`)
36
- * and `~`-carrying paths are left raw so today's validation refusal keeps
37
- * speaking (a naive join would resolve `..` away and smuggle an out-of-root
38
- * read past validation), and absolute paths pass through byte-untouched.
39
- * The middleware converts false errors into correct behavior never a
40
- * refusal into an allowance the rules didn't decide. (The absent-path
41
- * injection honors the same line: it narrows the tool's own OS-root default
42
- * to the workspace, granting nothing the rules would refuse.)
30
+ * Installed on EVERY native graph (parent and sub-agent, act and plan mode)
31
+ * so all downstream consumers the approval gate's capturability checks,
32
+ * the CAS observer, otel spans observe one canonical dialect. The pre-#754
33
+ * version was installed only beside permission rules and also injected the
34
+ * workspace root when `ls`/`glob`/`grep` omitted their path (the tools'
35
+ * schema default "/" was the OS ROOT under the legacy backend); under the
36
+ * virtual root that default already MEANS the workspace root, so the
37
+ * injection is retired.
43
38
  *
44
39
  * Tool matching is by bare built-in name, the house doctrine (an MCP server
45
40
  * is not expected to shadow a built-in name — see shared/tool-kind.ts); the
46
41
  * rewrite touches only the tool's path-bearing argument, never glob/grep
47
42
  * patterns. Install FIRST in the stack so every downstream middleware
48
- * (approval gate, error hints, otel spans) observes canonical paths.
43
+ * observes canonical paths.
49
44
  */
50
45
 
51
- import { isAbsolute, relative } from "node:path";
52
- import { resolveWorkspacePath } from "../shared/file-change.js";
46
+ import { isAbsolute, relative, posix } from "node:path";
53
47
  import type { StigmerMiddleware } from "./types.js";
54
48
 
55
49
  /**
@@ -66,55 +60,65 @@ const PATH_ARG_BY_TOOL: ReadonlyMap<string, string> = new Map([
66
60
  ["edit_file", "file_path"],
67
61
  ]);
68
62
 
69
- /**
70
- * The tools whose path argument is optional with an OS-root ("/") schema
71
- * default. When the model omits it, this middleware supplies the workspace
72
- * root instead (issue #528). The file tools are deliberately excluded: an
73
- * absent `file_path` is a genuine model error, and the tool's own input
74
- * validation gives the better message.
75
- */
76
- const DIR_DEFAULTING_TOOLS: ReadonlySet<string> = new Set(["ls", "glob", "grep"]);
77
-
78
63
  export interface PathNormalizationConfig {
79
- /** The workspace root the graph's filesystem backend resolves against. */
64
+ /**
65
+ * The REAL workspace root the graph's backend is rooted at — used only for
66
+ * the legacy-dialect compatibility mapping (real-absolute in-root paths →
67
+ * virtual). The virtual rewrite itself needs no root.
68
+ */
80
69
  readonly rootDir: string;
81
70
  }
82
71
 
83
72
  /**
84
- * Rewrite `raw` to its workspace-absolute form, or return undefined when the
85
- * value must be left untouched (absolute already, `~`-carrying, or escaping
86
- * the workspace root). Exported for direct unit testing of the mapping table.
73
+ * Rewrite `raw` to its canonical virtual-absolute form, or return undefined
74
+ * when the value must be left untouched. Exported for direct unit testing of
75
+ * the mapping table.
76
+ *
77
+ * Left raw (undefined): empty strings, `~`-carrying paths (refused upstream
78
+ * in every mode), escaping relatives ("../x" — the refusal is the honest
79
+ * answer), real-absolute paths OUTSIDE the root (virtual dialect: they name
80
+ * an in-workspace path that simply does not exist — resolution answers
81
+ * honestly), and paths already in canonical virtual form.
87
82
  */
88
83
  export function normalizeWorkspacePathArg(
89
84
  raw: string,
90
85
  rootDir: string,
91
86
  ): string | undefined {
92
- if (raw.length === 0 || isAbsolute(raw)) return undefined;
93
- // Upstream validation refuses `~` segments even in absolute paths, so a
94
- // rewrite could not make such a call succeed — leave the raw shape (and
95
- // therefore the honest refusal) intact.
87
+ if (raw.length === 0) return undefined;
88
+ // Upstream validation refuses `~` segments in every position, and a rewrite
89
+ // could not make such a call succeed — leave the honest refusal intact.
96
90
  if (raw.split("/").includes("~")) return undefined;
97
91
 
98
- const { absolutePath } = resolveWorkspacePath(raw, rootDir, false);
99
-
100
- // No-new-reachability guard: `join` inside the resolver normalizes `..`
101
- // segments away, so an escaping relative would otherwise pass upstream
102
- // validation as a clean out-of-root absolute path. Today that shape is
103
- // refused; keep it that way.
104
- const rel = relative(rootDir, absolutePath);
105
- if (rel.startsWith("..") || isAbsolute(rel)) return undefined;
106
-
107
- return absolutePath;
92
+ // Legacy-dialect compatibility: a REAL absolute path under the workspace
93
+ // root maps to its virtual form. Any other absolute path is already a
94
+ // virtual-dialect name reduced to relative form for canonicalization.
95
+ let candidate: string;
96
+ if (isAbsolute(raw)) {
97
+ const rel = relative(rootDir, raw);
98
+ if (rel === "") return "/";
99
+ candidate =
100
+ rel && !rel.startsWith("..") && !isAbsolute(rel)
101
+ ? rel
102
+ : raw.replace(/^\/+/, "");
103
+ } else {
104
+ candidate = raw;
105
+ }
106
+
107
+ // Canonicalize in RELATIVE form first: posix.normalize keeps a leading
108
+ // ".." on relative paths but silently swallows it on absolute ones, and a
109
+ // swallowed escape would rewrite the call onto a DIFFERENT in-root path —
110
+ // a refusal converted into an allowance. Detect the escape while it is
111
+ // still visible, and leave it raw for the upstream refusal.
112
+ const normalizedRel = posix.normalize(candidate);
113
+ if (normalizedRel === ".." || normalizedRel.startsWith("../")) return undefined;
114
+
115
+ const virtual = normalizedRel === "." ? "/" : `/${normalizedRel}`;
116
+ return virtual === raw ? undefined : virtual;
108
117
  }
109
118
 
110
119
  /**
111
- * Create middleware that normalizes workspace-relative paths on the built-in
112
- * filesystem tools before permission enforcement sees them.
113
- *
114
- * Installed only on graphs that carry filesystem permission rules — derive
115
- * the install condition from the same expression that supplies the rules
116
- * (setup.ts / compileSubagents) so the rules and their normalization shim
117
- * cannot drift apart.
120
+ * Create the dialect-repair middleware (see module header). Stateless per
121
+ * request; safe on every graph.
118
122
  */
119
123
  export function createPathNormalizationMiddleware(
120
124
  config: PathNormalizationConfig,
@@ -129,22 +133,6 @@ export function createPathNormalizationMiddleware(
129
133
  if (!argKey) return handler(request);
130
134
 
131
135
  const raw = request.toolCall.args[argKey];
132
-
133
- // Absent base directory on ls/glob/grep: the middleware sees the
134
- // model's raw args, BEFORE the tool's zod parse applies the "/"
135
- // (OS root) schema default — so the omission must be filled here,
136
- // where the workspace root is known. An explicit "/" is not this
137
- // case and flows through to an honest rule denial (header doctrine).
138
- if (raw == null && DIR_DEFAULTING_TOOLS.has(request.toolCall.name)) {
139
- return handler({
140
- ...request,
141
- toolCall: {
142
- ...request.toolCall,
143
- args: { ...request.toolCall.args, [argKey]: rootDir },
144
- },
145
- });
146
- }
147
-
148
136
  if (typeof raw !== "string") return handler(request);
149
137
 
150
138
  const normalized = normalizeWorkspacePathArg(raw, rootDir);
@@ -117,9 +117,9 @@ export interface MiddlewareStackConfig {
117
117
  readonly otelSpans?: Partial<OtelSpansConfig>;
118
118
  readonly approvalGate?: ApprovalGateConfig | null;
119
119
  /**
120
- * Workspace-relative path normalization (issue #429). Set iff the graph
121
- * carries filesystem permission rules derive from the same expression
122
- * that supplies the rules so the two cannot drift apart.
120
+ * Virtual-dialect path normalization (issues #429/#754). Set on every
121
+ * native graph: the backends are virtual-rooted, so every graph carries
122
+ * the repair seam that keeps model-supplied paths canonical.
123
123
  */
124
124
  readonly pathNormalization?: PathNormalizationConfig | null;
125
125
  }
@@ -15,6 +15,7 @@
15
15
  import type { PayloadCodec } from "@temporalio/common";
16
16
  import type { Config } from "./config.js";
17
17
  import type { BootstrapKeyMaterial } from "./encryption/config.js";
18
+ import { getRunnerSecret } from "./shared/runner-credential-store.js";
18
19
 
19
20
  export async function createPayloadCodecs(
20
21
  config: Config,
@@ -30,7 +31,7 @@ export async function createPayloadCodecs(
30
31
  const encryptionConfig = loadPayloadEncryptionConfig(bootstrapKeys);
31
32
  if (encryptionConfig) {
32
33
  codecs.push(new EncryptionPayloadCodec(encryptionConfig));
33
- const source = process.env.STIGMER_PAYLOAD_ENCRYPTION_KEY ? "env" : "bootstrap";
34
+ const source = getRunnerSecret("STIGMER_PAYLOAD_ENCRYPTION_KEY") ? "env" : "bootstrap";
34
35
  console.log(
35
36
  `[runner] Payload encryption enabled (source=${source}, ` +
36
37
  `key_id=${encryptionConfig.primary.keyId}` +
@@ -32,6 +32,11 @@ import type { WorkerActivities } from "./worker.js";
32
32
  import { resolveWorkflowSource, OTEL_WORKFLOW_INTERCEPTOR_MODULE } from "./workflow-source.js";
33
33
  import { resolveRunnerBootstrap, refreshRunnerAccessToken } from "./bootstrap.js";
34
34
  import { assertLlmBackendsPreflight } from "./preflight.js";
35
+ import {
36
+ captureRunnerSecrets,
37
+ getRunnerSecret,
38
+ setRunnerSecret,
39
+ } from "./shared/runner-credential-store.js";
35
40
  import { createRunnerTokenCoordinator } from "./runner-token-coordinator.js";
36
41
  // Per-task-queue in-flight activity tracking lives in ./in-flight.ts so the
37
42
  // activity interceptor (no manager-closure handle) and unit tests can reach it.
@@ -44,6 +49,10 @@ import {
44
49
  setQueueDrainCallback,
45
50
  forgetQueue,
46
51
  } from "./in-flight.js";
52
+ import {
53
+ registerWorkerShutdownSignal,
54
+ unregisterWorkerShutdownSignal,
55
+ } from "./shared/worker-shutdown.js";
47
56
 
48
57
  const SESSION_QUEUE_PREFIX = "session:";
49
58
  const WFEXEC_QUEUE_PREFIX = "wfexec:";
@@ -52,16 +61,10 @@ const WFEXEC_QUEUE_PREFIX = "wfexec:";
52
61
  // Mirrors SessionDispatchService's convention: lowercase kind, colon, id.
53
62
  const POOL_CONTROL_QUEUE_PREFIX = "sandbox:";
54
63
 
55
- /**
56
- * Module-level registry of shutdown signals per task queue.
57
- * Activities running in the same process can read this to determine
58
- * whether their worker is being shut down (vs orchestrator pause).
59
- */
60
- const _shutdownSignalRegistry = new Map<string, AbortSignal>();
61
-
62
- export function getShutdownSignalForQueue(taskQueue: string): AbortSignal | undefined {
63
- return _shutdownSignalRegistry.get(taskQueue);
64
- }
64
+ // Re-export for existing importers; the registry itself lives in
65
+ // shared/worker-shutdown.ts so the static runner (runner.ts) and the
66
+ // activities can share it without importing this manager module.
67
+ export { getShutdownSignalForQueue } from "./shared/worker-shutdown.js";
65
68
 
66
69
  export interface RunnerManagerOptions {
67
70
  /**
@@ -214,6 +217,12 @@ export async function createStigmerRunnerManager(
214
217
  ): Promise<StigmerRunnerManager> {
215
218
  validateManagerOptions(options);
216
219
 
220
+ // Take custody of runner secrets BEFORE anything else can read them from
221
+ // env — and before any agent code could spawn with them (#508). Runs here
222
+ // (not only in main.ts) because this factory is a public library boot door:
223
+ // in-process embedders like the desktop never execute main.ts.
224
+ captureRunnerSecrets();
225
+
217
226
  const { registerStigmerDeepagentsProfiles } = await import(
218
227
  "./activities/execute-deep-agent/deepagents-profiles.js"
219
228
  );
@@ -365,7 +374,7 @@ export async function createStigmerRunnerManager(
365
374
  if (
366
375
  !bootstrap.payloadEncryption &&
367
376
  bootstrap.runnerAccessToken &&
368
- !process.env.STIGMER_PAYLOAD_ENCRYPTION_KEY
377
+ !getRunnerSecret("STIGMER_PAYLOAD_ENCRYPTION_KEY")
369
378
  ) {
370
379
  console.warn(
371
380
  "[runner-manager] Server minted a runner token but returned no payload " +
@@ -400,15 +409,12 @@ export async function createStigmerRunnerManager(
400
409
 
401
410
  const sessions = new Map<string, ManagedSession>();
402
411
  const workflowExecutions = new Map<string, ManagedSession>();
403
- const shutdownSignals = new Map<string, AbortController>();
404
412
  // At most one per process: a pool member IS its control worker's identity.
405
413
  let poolControl: { taskQueue: string; managed: ManagedSession } | null = null;
406
414
  let shuttingDown = false;
407
415
 
408
416
  async function createWorkerOnQueue(taskQueue: string): Promise<ManagedSession> {
409
- const shutdownController = new AbortController();
410
- shutdownSignals.set(taskQueue, shutdownController);
411
- _shutdownSignalRegistry.set(taskQueue, shutdownController.signal);
417
+ const shutdownController = registerWorkerShutdownSignal(taskQueue);
412
418
 
413
419
  const worker = await Worker.create({
414
420
  connection,
@@ -470,8 +476,7 @@ export async function createStigmerRunnerManager(
470
476
  managed.worker.shutdown();
471
477
  await managed.runPromise;
472
478
  registry.delete(id);
473
- shutdownSignals.delete(taskQueue);
474
- _shutdownSignalRegistry.delete(taskQueue);
479
+ unregisterWorkerShutdownSignal(taskQueue);
475
480
  forgetQueue(taskQueue);
476
481
  console.log(`[runner-manager] Removed ${kind} ${id} (active=${registry.size})`);
477
482
  }
@@ -530,6 +535,13 @@ export async function createStigmerRunnerManager(
530
535
  await removeManaged(
531
536
  sessions, sessionId, SESSION_QUEUE_PREFIX + sessionId, "session",
532
537
  );
538
+ // The session is done on this host — release its parked agent (and
539
+ // the executor + MCP subprocesses the lease pins) immediately rather
540
+ // than waiting out the idle TTL (#215).
541
+ const { evictSessionAgent } = await import(
542
+ "./activities/execute-cursor/agent-session-cache.js"
543
+ );
544
+ evictSessionAgent(sessionId);
533
545
  },
534
546
 
535
547
  activeSessions(): string[] {
@@ -585,12 +597,11 @@ export async function createStigmerRunnerManager(
585
597
  // proxy credential follows it: only when no token has been minted (so the
586
598
  // pre-mint lockstep is preserved), never once the runner owns a minted
587
599
  // token. See runner-token-coordinator.ts and the staleness changelogs.
600
+ // The credential store is the second leg of the pair (it replaced the
601
+ // process.env.STIGMER_TOKEN write, #508): per-call readers like
602
+ // call-llm and the registry headers resolve the current token there.
588
603
  tokenRef.current = token;
589
- if (token) {
590
- process.env.STIGMER_TOKEN = token;
591
- } else {
592
- delete process.env.STIGMER_TOKEN;
593
- }
604
+ setRunnerSecret("STIGMER_TOKEN", token);
594
605
  tokenCoordinator.onControlPlaneTokenChanged(token);
595
606
  console.log("[runner-manager] Auth token updated");
596
607
  },
@@ -603,6 +614,22 @@ export async function createStigmerRunnerManager(
603
614
  `[runner-manager] Shutting down ${totalWorkers} workers (${sessions.size} sessions, ${workflowExecutions.size} workflow executions)...`,
604
615
  );
605
616
 
617
+ // Mark every queue's shutdown signal BEFORE draining, so an in-flight
618
+ // activity cancelled by the drain classifies it as a worker shutdown
619
+ // rather than a user pause (issue #776 — a SIGTERM'd pool member used
620
+ // to persist EXECUTION_PAUSED and let raw Temporal drain text reach
621
+ // status.error). Aborting is classification-only: it stops nothing;
622
+ // worker.shutdown() below still owns the drain. This is deliberately
623
+ // NOT done in teardownManaged — single-worker teardowns only run once
624
+ // the queue is idle, and aborting there is the old view-close bug.
625
+ for (const session of sessions.values()) {
626
+ session.shutdownController.abort();
627
+ }
628
+ for (const execution of workflowExecutions.values()) {
629
+ execution.shutdownController.abort();
630
+ }
631
+ poolControl?.managed.shutdownController.abort();
632
+
606
633
  const shutdownPromises = [
607
634
  ...Array.from(sessions.entries()).map(
608
635
  async ([id, session]) => {
@@ -629,6 +656,12 @@ export async function createStigmerRunnerManager(
629
656
  }
630
657
 
631
658
  await Promise.all(shutdownPromises);
659
+ // Workers are drained — release every parked session agent so their
660
+ // executor leases dispose (stdio MCP subprocesses die with them) (#215).
661
+ const { closeAllCachedAgents } = await import(
662
+ "./activities/execute-cursor/agent-session-cache.js"
663
+ );
664
+ closeAllCachedAgents();
632
665
  sessions.clear();
633
666
  workflowExecutions.clear();
634
667
  poolControl = null;
package/src/runner.ts CHANGED
@@ -20,6 +20,10 @@ import { DEFAULT_CURSOR_AGENT_RESOLVE_TIMEOUT_MS, DEFAULT_CURSOR_STREAM_STALL_TI
20
20
  import type { WorkerActivities } from "./worker.js";
21
21
  import { resolveRunnerBootstrap } from "./bootstrap.js";
22
22
  import { assertLlmBackendsPreflight } from "./preflight.js";
23
+ import {
24
+ captureRunnerSecrets,
25
+ setRunnerSecret,
26
+ } from "./shared/runner-credential-store.js";
23
27
  import { markBoot, emitRunnerBootTiming } from "./shared/cold-start-timing.js";
24
28
 
25
29
  /**
@@ -118,11 +122,12 @@ export interface StigmerRunner {
118
122
  * Wire up self-renewal for a static cloud sandbox's control-plane credential
119
123
  * (see sandbox-token-renewal.ts for the model). The applied token reaches
120
124
  * every consumer: activity gRPC clients read {@code tokenRef} per request,
121
- * env-reading call sites (call-llm, registry-endpoint) read
122
- * {@code process.env.STIGMER_TOKEN} per call, artifact storage resolves the
123
- * ref per call, and the two Cursor SDK interceptors are updated directly —
124
- * a static runner has no {@code RunnerTokenCoordinator} minting a separate
125
- * proxy credential, so its x-stigmer-auth IS this token.
125
+ * per-call sites (call-llm, registry-endpoint headers) resolve it through
126
+ * the runner credential store (which replaced the process.env.STIGMER_TOKEN
127
+ * channel, #508), artifact storage resolves the ref per call, and the two
128
+ * Cursor SDK interceptors are updated directly — a static runner has no
129
+ * {@code RunnerTokenCoordinator} minting a separate proxy credential, so its
130
+ * x-stigmer-auth IS this token.
126
131
  */
127
132
  async function startStaticSandboxTokenRenewal(
128
133
  config: Config,
@@ -157,7 +162,9 @@ async function startStaticSandboxTokenRenewal(
157
162
  client.getRunnerScopedToken({ renewal: true }, currentToken),
158
163
  applyToken: (token) => {
159
164
  tokenRef.current = token;
160
- process.env.STIGMER_TOKEN = token;
165
+ // Store write replaced the process.env.STIGMER_TOKEN write (#508) —
166
+ // same per-call readers (call-llm, registry headers), no env exposure.
167
+ setRunnerSecret("STIGMER_TOKEN", token);
161
168
  updateInterceptorToken(token);
162
169
  updateHttp2InterceptorToken(token);
163
170
  },
@@ -190,6 +197,12 @@ export async function createStigmerRunner(
190
197
  ): Promise<StigmerRunner> {
191
198
  validateOptions(options);
192
199
 
200
+ // Take custody of runner secrets BEFORE anything else can read them from
201
+ // env — and before any agent code could spawn with them (#508). Runs here
202
+ // (not only in main.ts) because this factory is a public library boot door
203
+ // for in-process embedders.
204
+ captureRunnerSecrets();
205
+
193
206
  const { registerStigmerDeepagentsProfiles } = await import(
194
207
  "./activities/execute-deep-agent/deepagents-profiles.js"
195
208
  );
@@ -311,6 +324,16 @@ export async function createStigmerRunner(
311
324
  // unaffected — their env-injected platform key wins inside the loader.
312
325
  const payloadCodecs = await createPayloadCodecs(config, coordinates.payloadEncryption);
313
326
 
327
+ // Register the queue's shutdown signal BEFORE the worker can poll, so an
328
+ // activity interrupted by shutdown() below classifies the cancellation as a
329
+ // worker shutdown, not a user pause (issue #776). Static mode historically
330
+ // never registered one, leaving CLI daemons with the same misclassification
331
+ // the desktop manager had.
332
+ const { registerWorkerShutdownSignal } = await import(
333
+ "./shared/worker-shutdown.js"
334
+ );
335
+ const shutdownController = registerWorkerShutdownSignal(config.taskQueue);
336
+
314
337
  const { startWorker } = await import("./worker.js");
315
338
  const worker = await startWorker({ config, activities, payloadCodecs });
316
339
  markBoot("worker_created");
@@ -327,9 +350,19 @@ export async function createStigmerRunner(
327
350
  emitRunnerBootTiming({ task_queue: config.taskQueue, mode: config.mode });
328
351
  await worker.run();
329
352
  console.log("Worker stopped");
353
+ // The worker has drained — release any parked session agent so its
354
+ // executor lease disposes with the process (#215).
355
+ const { closeAllCachedAgents } = await import(
356
+ "./activities/execute-cursor/agent-session-cache.js"
357
+ );
358
+ closeAllCachedAgents();
330
359
  },
331
360
  shutdown() {
332
361
  tokenRenewal?.stop();
362
+ // Classification first, drain second: an in-flight activity cancelled
363
+ // by the drain must observe the signal already aborted (see
364
+ // shared/worker-shutdown.ts for the ownership contract).
365
+ shutdownController.abort();
333
366
  worker.shutdown();
334
367
  },
335
368
  };