agent-tower 0.6.0-beta.11 → 0.6.0-beta.13

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 (204) hide show
  1. package/dist/app.d.ts.map +1 -1
  2. package/dist/app.js +4 -0
  3. package/dist/app.js.map +1 -1
  4. package/dist/cli.js +39 -19
  5. package/dist/cli.js.map +1 -1
  6. package/dist/executors/__tests__/base.executor.test.js +133 -1
  7. package/dist/executors/__tests__/base.executor.test.js.map +1 -1
  8. package/dist/executors/__tests__/stdin-prompt.executors.test.js +18 -0
  9. package/dist/executors/__tests__/stdin-prompt.executors.test.js.map +1 -1
  10. package/dist/executors/base.executor.d.ts +8 -0
  11. package/dist/executors/base.executor.d.ts.map +1 -1
  12. package/dist/executors/base.executor.js +336 -56
  13. package/dist/executors/base.executor.js.map +1 -1
  14. package/dist/executors/index.d.ts +2 -1
  15. package/dist/executors/index.d.ts.map +1 -1
  16. package/dist/executors/index.js +1 -1
  17. package/dist/executors/index.js.map +1 -1
  18. package/dist/executors/start-error.d.ts +17 -0
  19. package/dist/executors/start-error.d.ts.map +1 -1
  20. package/dist/executors/start-error.js +36 -1
  21. package/dist/executors/start-error.js.map +1 -1
  22. package/dist/index.js +39 -29
  23. package/dist/index.js.map +1 -1
  24. package/dist/mcp/http-client.test.js +20 -0
  25. package/dist/mcp/http-client.test.js.map +1 -1
  26. package/dist/mcp/tools/__tests__/sessions.test.d.ts +2 -0
  27. package/dist/mcp/tools/__tests__/sessions.test.d.ts.map +1 -0
  28. package/dist/mcp/tools/__tests__/sessions.test.js +33 -0
  29. package/dist/mcp/tools/__tests__/sessions.test.js.map +1 -0
  30. package/dist/routes/__tests__/sessions-runtime.test.js +75 -3
  31. package/dist/routes/__tests__/sessions-runtime.test.js.map +1 -1
  32. package/dist/routes/conversations.d.ts.map +1 -1
  33. package/dist/routes/conversations.js +5 -1
  34. package/dist/routes/conversations.js.map +1 -1
  35. package/dist/routes/sessions.d.ts.map +1 -1
  36. package/dist/routes/sessions.js +13 -2
  37. package/dist/routes/sessions.js.map +1 -1
  38. package/dist/routes/team-runs.d.ts.map +1 -1
  39. package/dist/routes/team-runs.js +13 -1
  40. package/dist/routes/team-runs.js.map +1 -1
  41. package/dist/runtime/__tests__/acp-driver-lifecycle.test.js +86 -0
  42. package/dist/runtime/__tests__/acp-driver-lifecycle.test.js.map +1 -1
  43. package/dist/runtime/__tests__/acp-process-manager.test.js +393 -6
  44. package/dist/runtime/__tests__/acp-process-manager.test.js.map +1 -1
  45. package/dist/runtime/__tests__/cli-driver-lifecycle.test.d.ts +2 -0
  46. package/dist/runtime/__tests__/cli-driver-lifecycle.test.d.ts.map +1 -0
  47. package/dist/runtime/__tests__/cli-driver-lifecycle.test.js +244 -0
  48. package/dist/runtime/__tests__/cli-driver-lifecycle.test.js.map +1 -0
  49. package/dist/runtime/__tests__/launch-cleanup-registry.test.d.ts +2 -0
  50. package/dist/runtime/__tests__/launch-cleanup-registry.test.d.ts.map +1 -0
  51. package/dist/runtime/__tests__/launch-cleanup-registry.test.js +154 -0
  52. package/dist/runtime/__tests__/launch-cleanup-registry.test.js.map +1 -0
  53. package/dist/runtime/__tests__/runtime-coordinator.test.js +117 -1
  54. package/dist/runtime/__tests__/runtime-coordinator.test.js.map +1 -1
  55. package/dist/runtime/__tests__/server-entry-shutdown.test.d.ts +2 -0
  56. package/dist/runtime/__tests__/server-entry-shutdown.test.d.ts.map +1 -0
  57. package/dist/runtime/__tests__/server-entry-shutdown.test.js +38 -0
  58. package/dist/runtime/__tests__/server-entry-shutdown.test.js.map +1 -0
  59. package/dist/runtime/__tests__/shutdown-coordinator.test.d.ts +2 -0
  60. package/dist/runtime/__tests__/shutdown-coordinator.test.d.ts.map +1 -0
  61. package/dist/runtime/__tests__/shutdown-coordinator.test.js +28 -0
  62. package/dist/runtime/__tests__/shutdown-coordinator.test.js.map +1 -0
  63. package/dist/runtime/acp/acp-driver.d.ts.map +1 -1
  64. package/dist/runtime/acp/acp-driver.js +108 -35
  65. package/dist/runtime/acp/acp-driver.js.map +1 -1
  66. package/dist/runtime/acp/agents/managed-directory.d.ts.map +1 -1
  67. package/dist/runtime/acp/agents/managed-directory.js +3 -1
  68. package/dist/runtime/acp/agents/managed-directory.js.map +1 -1
  69. package/dist/runtime/acp/agents/managed-directory.test.d.ts +2 -0
  70. package/dist/runtime/acp/agents/managed-directory.test.d.ts.map +1 -0
  71. package/dist/runtime/acp/agents/managed-directory.test.js +25 -0
  72. package/dist/runtime/acp/agents/managed-directory.test.js.map +1 -0
  73. package/dist/runtime/acp/launch-cleanup-registry.d.ts +32 -0
  74. package/dist/runtime/acp/launch-cleanup-registry.d.ts.map +1 -0
  75. package/dist/runtime/acp/launch-cleanup-registry.js +176 -0
  76. package/dist/runtime/acp/launch-cleanup-registry.js.map +1 -0
  77. package/dist/runtime/acp/process-manager.d.ts +58 -0
  78. package/dist/runtime/acp/process-manager.d.ts.map +1 -1
  79. package/dist/runtime/acp/process-manager.js +489 -33
  80. package/dist/runtime/acp/process-manager.js.map +1 -1
  81. package/dist/runtime/cli-driver.d.ts.map +1 -1
  82. package/dist/runtime/cli-driver.js +230 -46
  83. package/dist/runtime/cli-driver.js.map +1 -1
  84. package/dist/runtime/contracts.d.ts +21 -2
  85. package/dist/runtime/contracts.d.ts.map +1 -1
  86. package/dist/runtime/index.d.ts +1 -0
  87. package/dist/runtime/index.d.ts.map +1 -1
  88. package/dist/runtime/index.js +1 -0
  89. package/dist/runtime/index.js.map +1 -1
  90. package/dist/runtime/runtime-coordinator.d.ts +8 -1
  91. package/dist/runtime/runtime-coordinator.d.ts.map +1 -1
  92. package/dist/runtime/runtime-coordinator.js +118 -18
  93. package/dist/runtime/runtime-coordinator.js.map +1 -1
  94. package/dist/runtime/server-entry-shutdown.d.ts +12 -0
  95. package/dist/runtime/server-entry-shutdown.d.ts.map +1 -0
  96. package/dist/runtime/server-entry-shutdown.js +23 -0
  97. package/dist/runtime/server-entry-shutdown.js.map +1 -0
  98. package/dist/runtime/shutdown-coordinator.d.ts +17 -0
  99. package/dist/runtime/shutdown-coordinator.d.ts.map +1 -0
  100. package/dist/runtime/shutdown-coordinator.js +67 -0
  101. package/dist/runtime/shutdown-coordinator.js.map +1 -0
  102. package/dist/services/__tests__/conversation.service.test.js +160 -0
  103. package/dist/services/__tests__/conversation.service.test.js.map +1 -1
  104. package/dist/services/__tests__/database-maintenance.service.test.js +97 -0
  105. package/dist/services/__tests__/database-maintenance.service.test.js.map +1 -1
  106. package/dist/services/__tests__/member-heartbeat-scheduler.test.js +6 -0
  107. package/dist/services/__tests__/member-heartbeat-scheduler.test.js.map +1 -1
  108. package/dist/services/__tests__/member-heartbeat.test.js +7 -11
  109. package/dist/services/__tests__/member-heartbeat.test.js.map +1 -1
  110. package/dist/services/__tests__/session-manager.lifecycle.test.js +402 -12
  111. package/dist/services/__tests__/session-manager.lifecycle.test.js.map +1 -1
  112. package/dist/services/__tests__/session-manager.team-run.test.js +422 -29
  113. package/dist/services/__tests__/session-manager.team-run.test.js.map +1 -1
  114. package/dist/services/__tests__/team-queue-recovery.integration.test.js +1 -0
  115. package/dist/services/__tests__/team-queue-recovery.integration.test.js.map +1 -1
  116. package/dist/services/__tests__/team-reconciler.service.test.js +1282 -4
  117. package/dist/services/__tests__/team-reconciler.service.test.js.map +1 -1
  118. package/dist/services/__tests__/team-run.service.test.js +161 -1
  119. package/dist/services/__tests__/team-run.service.test.js.map +1 -1
  120. package/dist/services/__tests__/team-scheduler.service.test.js +334 -6
  121. package/dist/services/__tests__/team-scheduler.service.test.js.map +1 -1
  122. package/dist/services/conversation.service.d.ts.map +1 -1
  123. package/dist/services/conversation.service.js +21 -2
  124. package/dist/services/conversation.service.js.map +1 -1
  125. package/dist/services/database-maintenance.service.d.ts.map +1 -1
  126. package/dist/services/database-maintenance.service.js +68 -17
  127. package/dist/services/database-maintenance.service.js.map +1 -1
  128. package/dist/services/member-heartbeat-scheduler.d.ts +6 -5
  129. package/dist/services/member-heartbeat-scheduler.d.ts.map +1 -1
  130. package/dist/services/member-heartbeat-scheduler.js +7 -5
  131. package/dist/services/member-heartbeat-scheduler.js.map +1 -1
  132. package/dist/services/session-manager.d.ts +124 -14
  133. package/dist/services/session-manager.d.ts.map +1 -1
  134. package/dist/services/session-manager.js +1035 -116
  135. package/dist/services/session-manager.js.map +1 -1
  136. package/dist/services/session-runtime-cleanup-gate.d.ts +25 -0
  137. package/dist/services/session-runtime-cleanup-gate.d.ts.map +1 -0
  138. package/dist/services/session-runtime-cleanup-gate.js +284 -0
  139. package/dist/services/session-runtime-cleanup-gate.js.map +1 -0
  140. package/dist/services/task.service.d.ts +2 -2
  141. package/dist/services/team-member-admission-barrier.d.ts +3 -0
  142. package/dist/services/team-member-admission-barrier.d.ts.map +1 -0
  143. package/dist/services/team-member-admission-barrier.js +26 -0
  144. package/dist/services/team-member-admission-barrier.js.map +1 -0
  145. package/dist/services/team-reconciler.service.d.ts +30 -7
  146. package/dist/services/team-reconciler.service.d.ts.map +1 -1
  147. package/dist/services/team-reconciler.service.js +528 -111
  148. package/dist/services/team-reconciler.service.js.map +1 -1
  149. package/dist/services/team-run.service.d.ts.map +1 -1
  150. package/dist/services/team-run.service.js +37 -13
  151. package/dist/services/team-run.service.js.map +1 -1
  152. package/dist/services/team-scheduler.service.d.ts +8 -6
  153. package/dist/services/team-scheduler.service.d.ts.map +1 -1
  154. package/dist/services/team-scheduler.service.js +323 -120
  155. package/dist/services/team-scheduler.service.js.map +1 -1
  156. package/dist/services/workspace-background-process-manager.d.ts.map +1 -1
  157. package/dist/services/workspace-background-process-manager.js +2 -5
  158. package/dist/services/workspace-background-process-manager.js.map +1 -1
  159. package/dist/services/workspace.service.d.ts +15 -8
  160. package/dist/services/workspace.service.d.ts.map +1 -1
  161. package/dist/socket/index.d.ts.map +1 -1
  162. package/dist/socket/index.js +27 -14
  163. package/dist/socket/index.js.map +1 -1
  164. package/dist/utils/__tests__/unix-process-identity.test.d.ts +2 -0
  165. package/dist/utils/__tests__/unix-process-identity.test.d.ts.map +1 -0
  166. package/dist/utils/__tests__/unix-process-identity.test.js +73 -0
  167. package/dist/utils/__tests__/unix-process-identity.test.js.map +1 -0
  168. package/dist/utils/error-log.d.ts +2 -0
  169. package/dist/utils/error-log.d.ts.map +1 -1
  170. package/dist/utils/error-log.js +40 -3
  171. package/dist/utils/error-log.js.map +1 -1
  172. package/dist/utils/error-log.test.js +14 -2
  173. package/dist/utils/error-log.test.js.map +1 -1
  174. package/dist/utils/process-launch.d.ts +2 -2
  175. package/dist/utils/process-launch.d.ts.map +1 -1
  176. package/dist/utils/process-launch.js +339 -79
  177. package/dist/utils/process-launch.js.map +1 -1
  178. package/dist/utils/process-launch.test.js +168 -10
  179. package/dist/utils/process-launch.test.js.map +1 -1
  180. package/dist/utils/spawned-process-identity.d.ts +7 -0
  181. package/dist/utils/spawned-process-identity.d.ts.map +1 -0
  182. package/dist/utils/spawned-process-identity.js +36 -0
  183. package/dist/utils/spawned-process-identity.js.map +1 -0
  184. package/dist/utils/tree-cleanup-channel.d.ts +18 -0
  185. package/dist/utils/tree-cleanup-channel.d.ts.map +1 -0
  186. package/dist/utils/tree-cleanup-channel.js +16 -0
  187. package/dist/utils/tree-cleanup-channel.js.map +1 -0
  188. package/dist/utils/tree-cleanup-channel.test.d.ts +2 -0
  189. package/dist/utils/tree-cleanup-channel.test.d.ts.map +1 -0
  190. package/dist/utils/tree-cleanup-channel.test.js +15 -0
  191. package/dist/utils/tree-cleanup-channel.test.js.map +1 -0
  192. package/dist/utils/unix-process-identity.d.ts +13 -1
  193. package/dist/utils/unix-process-identity.d.ts.map +1 -1
  194. package/dist/utils/unix-process-identity.js +146 -25
  195. package/dist/utils/unix-process-identity.js.map +1 -1
  196. package/node_modules/@agent-tower/shared/dist/types.d.ts +1 -0
  197. package/node_modules/@agent-tower/shared/dist/types.d.ts.map +1 -1
  198. package/node_modules/@agent-tower/shared/dist/types.js.map +1 -1
  199. package/package.json +1 -1
  200. package/prisma/migrations/20260824000000_add_execution_process_ownership/migration.sql +10 -0
  201. package/prisma/migrations/20260824010000_add_runtime_cleanup_recovery/migration.sql +10 -0
  202. package/prisma/migrations/20260824020000_add_runtime_launch_admission/migration.sql +72 -0
  203. package/prisma/migrations/20260826000000_add_conversation_turn_queue/migration.sql +19 -0
  204. package/prisma/schema.prisma +50 -7
@@ -1,7 +1,11 @@
1
- import { spawn } from 'node:child_process';
1
+ import { execFileSync, spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
2
3
  import { Readable, Transform, Writable } from 'node:stream';
3
4
  import { StringDecoder } from 'node:string_decoder';
4
5
  import { AgentRuntimeError } from '../errors.js';
6
+ import { markPreChildProcessFailure } from '../../executors/start-error.js';
7
+ import { createUnixProcessIdentityAdapter, } from '../../utils/unix-process-identity.js';
8
+ import { acpLaunchCleanupRegistry } from './launch-cleanup-registry.js';
5
9
  const MAX_STDOUT_LINE_BYTES = 1024 * 1024;
6
10
  const MAX_STDERR_BYTES = 8 * 1024;
7
11
  const DEFAULT_KILL_GRACE_MS = 2_000;
@@ -13,9 +17,23 @@ export class AcpProcessManager {
13
17
  settled;
14
18
  stopPromise;
15
19
  stderr = '';
20
+ platform;
21
+ unixProcessAdapter;
22
+ windowsProcessAdapter;
23
+ ownershipToken = randomUUID();
24
+ rootIdentity;
25
+ rootWindowsIdentity;
26
+ ownedWindowsProcesses = new Map();
16
27
  exitListeners = new Set();
17
- constructor(launch) {
28
+ cleanupOwnerId;
29
+ cleanupRetryPromise;
30
+ constructor(launch, options = {}) {
18
31
  this.launch = launch;
32
+ this.platform = options.platform ?? process.platform;
33
+ this.unixProcessAdapter = options.unixProcessAdapter
34
+ ?? createUnixProcessIdentityAdapter(this.platform);
35
+ this.windowsProcessAdapter = options.windowsProcessAdapter
36
+ ?? createWindowsProcessTreeAdapter();
19
37
  }
20
38
  async start() {
21
39
  if (this.child) {
@@ -25,14 +43,18 @@ export class AcpProcessManager {
25
43
  try {
26
44
  child = spawn(this.launch.command, this.launch.args, {
27
45
  cwd: this.launch.cwd,
28
- env: this.launch.env,
29
- detached: process.platform !== 'win32',
46
+ env: {
47
+ ...this.launch.env,
48
+ AGENT_TOWER_PROCESS_IDENTITY: this.ownershipToken,
49
+ AGENT_TOWER_PROCESS_BIRTH_MARKER: this.ownershipToken,
50
+ },
51
+ detached: this.platform !== 'win32',
30
52
  shell: false,
31
53
  stdio: ['pipe', 'pipe', 'pipe'],
32
54
  });
33
55
  }
34
56
  catch (error) {
35
- throw new AgentRuntimeError('spawn_failed', 'spawn', 'Could not start the ACP adapter', true, { cause: error });
57
+ throw markPreChildProcessFailure(new AgentRuntimeError('spawn_failed', 'spawn', 'Could not start the ACP adapter', true, { cause: error }));
36
58
  }
37
59
  this.child = child;
38
60
  this.exitPromise = new Promise((resolve) => {
@@ -51,7 +73,7 @@ export class AcpProcessManager {
51
73
  child.once('error', (error) => {
52
74
  this.settle({ exitCode: null, signal: null, stderrExcerpt: this.stderr });
53
75
  if (!spawned) {
54
- reject(new AgentRuntimeError('spawn_failed', 'spawn', 'Could not start the ACP adapter', true, { cause: error }));
76
+ reject(markPreChildProcessFailure(new AgentRuntimeError('spawn_failed', 'spawn', 'Could not start the ACP adapter', true, { cause: error })));
55
77
  }
56
78
  });
57
79
  child.once('close', (exitCode, signal) => {
@@ -59,17 +81,59 @@ export class AcpProcessManager {
59
81
  });
60
82
  });
61
83
  if (!child.pid) {
62
- await this.stop();
63
- throw new AgentRuntimeError('spawn_failed', 'spawn', 'ACP adapter did not report a process id', true);
84
+ return this.failAfterSpawn(new AgentRuntimeError('spawn_failed', 'spawn', 'ACP adapter did not report a process id', true));
85
+ }
86
+ // Attach the stdout validator before the identity probe. A short-lived
87
+ // adapter may exit while its launch identity is being sampled; attaching
88
+ // later would miss already-buffered protocol frames.
89
+ let output;
90
+ try {
91
+ const validatedOutput = createValidatedOutput(child.stdout, {
92
+ maxInputFrameBytes: this.launch.maxStdoutFrameBytes ?? MAX_STDOUT_LINE_BYTES,
93
+ transformFrame: this.launch.transformStdoutFrame,
94
+ });
95
+ output = Readable.toWeb(validatedOutput);
96
+ if (this.platform === 'win32') {
97
+ this.rootWindowsIdentity = await this.captureRootWindowsIdentity(child.pid);
98
+ if (this.rootWindowsIdentity) {
99
+ this.ownedWindowsProcesses.set(this.rootWindowsIdentity.pid, this.rootWindowsIdentity);
100
+ }
101
+ else if (!this.settled) {
102
+ return this.failAfterSpawn(new AgentRuntimeError('spawn_failed', 'spawn', 'ACP adapter process identity could not be verified', true));
103
+ }
104
+ else {
105
+ // The exact ChildProcess handle is settled. Only accept a missing
106
+ // root when the adapter can also prove that no descendant remains.
107
+ await this.captureWindowsDescendants();
108
+ }
109
+ }
110
+ else {
111
+ this.rootIdentity = await this.captureRootIdentity(child.pid);
112
+ if (!this.rootIdentity && !this.settled) {
113
+ return this.failAfterSpawn(new AgentRuntimeError('spawn_failed', 'spawn', 'ACP adapter process identity could not be verified', true));
114
+ }
115
+ }
116
+ }
117
+ catch (error) {
118
+ return this.failAfterSpawn(error);
119
+ }
120
+ // A short-lived adapter can settle before the OS identity probe observes
121
+ // it. The ChildProcess exit event is authoritative for that exact handle;
122
+ // persist a generation-unique marker without signalling any PID. A live
123
+ // child without a verifiable OS identity remains an unresolved launch.
124
+ const birthMarker = this.rootIdentity?.birthIdentity
125
+ ?? this.rootWindowsIdentity?.birthMarker
126
+ ?? (this.settled ? `settled:${this.ownershipToken}` : undefined);
127
+ if (!birthMarker) {
128
+ return this.failAfterSpawn(new AgentRuntimeError('process_identity_mismatch', 'spawn', 'ACP adapter launched without a durable birth identity', true));
64
129
  }
65
- const validatedOutput = createValidatedOutput(child.stdout, {
66
- maxInputFrameBytes: this.launch.maxStdoutFrameBytes ?? MAX_STDOUT_LINE_BYTES,
67
- transformFrame: this.launch.transformStdoutFrame,
68
- });
69
130
  return {
70
131
  pid: child.pid,
132
+ processGroupId: this.rootIdentity ? String(this.rootIdentity.pgid) : String(child.pid),
133
+ birthMarker,
134
+ ownershipToken: this.ownershipToken,
71
135
  input: Writable.toWeb(child.stdin),
72
- output: Readable.toWeb(validatedOutput),
136
+ output,
73
137
  };
74
138
  }
75
139
  onExit(listener) {
@@ -78,43 +142,205 @@ export class AcpProcessManager {
78
142
  listener(this.settled);
79
143
  return () => this.exitListeners.delete(listener);
80
144
  }
145
+ /** Exposes the retry owner for startup/recovery diagnostics and tests. */
146
+ getPostSpawnCleanupOwnerId() {
147
+ return this.cleanupOwnerId;
148
+ }
81
149
  async stop() {
82
150
  if (this.stopPromise)
83
151
  return this.stopPromise;
84
- this.stopPromise = this.stopOnce();
152
+ this.stopPromise = this.stopOnce().catch((error) => {
153
+ this.stopPromise = undefined;
154
+ // A caller may lose the manager after an initialization failure. Keep a
155
+ // registry owner reachable even when this stop attempt is the first
156
+ // post-spawn failure observed by the driver.
157
+ void this.retryCleanupOwner().catch(() => undefined);
158
+ throw error;
159
+ });
85
160
  return this.stopPromise;
86
161
  }
87
162
  async stopOnce() {
88
163
  if (!this.child || !this.exitPromise)
89
164
  return undefined;
90
- if (this.settled)
91
- return this.settled;
92
- this.signal('SIGTERM');
93
- const graceful = await within(this.exitPromise, DEFAULT_KILL_GRACE_MS);
94
- if (graceful)
165
+ const settled = this.settled;
166
+ if (!this.child.pid) {
167
+ if (settled)
168
+ return settled;
169
+ await this.terminateChildHandle('SIGTERM');
170
+ const graceful = await within(this.exitPromise, DEFAULT_KILL_GRACE_MS);
171
+ if (graceful)
172
+ return graceful;
173
+ await this.terminateChildHandle('SIGKILL');
174
+ const forced = await within(this.exitPromise, DEFAULT_KILL_GRACE_MS);
175
+ if (forced)
176
+ return forced;
177
+ throw processTreeTimeout();
178
+ }
179
+ let groups = this.platform === 'win32' ? [] : await this.captureOwnedGroups();
180
+ if (this.platform === 'win32') {
181
+ await this.captureWindowsDescendants();
182
+ await this.signal('SIGTERM', groups);
183
+ }
184
+ else {
185
+ await this.signal('SIGTERM', groups);
186
+ }
187
+ const graceful = settled ?? await within(this.exitPromise, DEFAULT_KILL_GRACE_MS);
188
+ const treeGraceMs = graceful ? Math.min(DEFAULT_KILL_GRACE_MS, 500) : DEFAULT_KILL_GRACE_MS;
189
+ if (graceful && await this.waitForOwnedTreeExit(groups, treeGraceMs))
95
190
  return graceful;
96
- this.signal('SIGKILL');
191
+ if (this.platform !== 'win32') {
192
+ // The ownership marker is launch-scoped. Once the root exits, descendants
193
+ // may have moved to a new session/process group; filtering by the root
194
+ // PGID would permanently hide those escaped groups from cleanup.
195
+ groups = mergeUnixGroups(groups, await this.captureOwnedGroups());
196
+ }
197
+ await this.signal('SIGKILL', groups);
97
198
  const forced = await within(this.exitPromise, DEFAULT_KILL_GRACE_MS);
98
- if (forced)
199
+ if (forced && await this.waitForOwnedTreeExit(groups, DEFAULT_KILL_GRACE_MS))
99
200
  return forced;
100
201
  throw new AgentRuntimeError('process_exit_timeout', 'close', 'ACP adapter did not confirm termination before the shutdown deadline', true);
101
202
  }
102
- signal(signal) {
203
+ async signal(signal, groups) {
103
204
  const child = this.child;
104
- if (!child?.pid || this.settled)
205
+ if (!child?.pid)
206
+ return;
207
+ if (this.platform === 'win32') {
208
+ await this.captureWindowsDescendants();
209
+ const candidates = [...this.ownedWindowsProcesses.values()]
210
+ .sort((left, right) => right.pid - left.pid);
211
+ for (const identity of candidates) {
212
+ if (!await this.windowsProcessAdapter.isProcessAlive(identity))
213
+ continue;
214
+ await this.windowsProcessAdapter.terminateTree(identity.pid);
215
+ }
105
216
  return;
106
- try {
107
- if (process.platform === 'win32')
108
- child.kill(signal);
109
- else
110
- process.kill(-child.pid, signal);
111
217
  }
112
- catch {
113
- try {
114
- child.kill(signal);
218
+ await Promise.all(groups.map((group) => this.unixProcessAdapter.signalProcessGroup(group, signal)));
219
+ if (this.rootIdentity) {
220
+ await this.unixProcessAdapter.signalProcess(this.rootIdentity, signal);
221
+ }
222
+ }
223
+ async waitForOwnedTreeExit(initialGroups, timeoutMs) {
224
+ let groups = initialGroups;
225
+ const deadline = Date.now() + timeoutMs;
226
+ while (Date.now() < deadline) {
227
+ if (!await this.isOwnedTreeAlive(groups)) {
228
+ if (this.platform === 'win32') {
229
+ await this.captureWindowsDescendants();
230
+ }
231
+ else {
232
+ groups = mergeUnixGroups(groups, await this.captureOwnedGroups());
233
+ }
234
+ if (!await this.isOwnedTreeAlive(groups))
235
+ return true;
115
236
  }
116
- catch {
117
- // A concurrent close event will settle the process.
237
+ await new Promise((resolve) => setTimeout(resolve, 25));
238
+ }
239
+ if (this.platform === 'win32') {
240
+ await this.captureWindowsDescendants();
241
+ }
242
+ else {
243
+ groups = mergeUnixGroups(groups, await this.captureOwnedGroups());
244
+ }
245
+ return !await this.isOwnedTreeAlive(groups);
246
+ }
247
+ async isOwnedTreeAlive(groups) {
248
+ const pid = this.child?.pid;
249
+ if (!pid)
250
+ return false;
251
+ if (this.platform === 'win32') {
252
+ for (const identity of this.ownedWindowsProcesses.values()) {
253
+ if (await this.windowsProcessAdapter.isProcessAlive(identity))
254
+ return true;
255
+ }
256
+ return false;
257
+ }
258
+ if (!this.settled && this.rootIdentity && await this.unixProcessAdapter.isProcessAlive(this.rootIdentity))
259
+ return true;
260
+ const liveGroups = groups
261
+ .map((group) => this.settled
262
+ ? { ...group, members: group.members.filter((member) => member.pid !== pid) }
263
+ : group)
264
+ .filter((group) => group.members.length > 0);
265
+ return (await Promise.all(liveGroups.map((group) => this.unixProcessAdapter.isProcessGroupAlive(group)))).some(Boolean);
266
+ }
267
+ async captureRootIdentity(pid) {
268
+ for (let attempt = 0; attempt < 10; attempt += 1) {
269
+ const identity = await this.unixProcessAdapter.captureProcess(pid, this.ownershipToken);
270
+ if (identity)
271
+ return identity;
272
+ await new Promise((resolve) => setTimeout(resolve, 10));
273
+ }
274
+ return undefined;
275
+ }
276
+ async captureRootWindowsIdentity(pid) {
277
+ for (let attempt = 0; attempt < 10; attempt += 1) {
278
+ const identity = await this.windowsProcessAdapter.captureProcess(pid);
279
+ if (identity)
280
+ return identity;
281
+ if (this.settled)
282
+ return undefined;
283
+ await new Promise((resolve) => setTimeout(resolve, 10));
284
+ }
285
+ return undefined;
286
+ }
287
+ async captureOwnedGroups(processGroupId) {
288
+ if (this.platform === 'win32')
289
+ return [];
290
+ const groups = this.unixProcessAdapter.captureOwnedGroups
291
+ ? processGroupId === undefined
292
+ ? await this.unixProcessAdapter.captureOwnedGroups(this.ownershipToken)
293
+ : await this.unixProcessAdapter.captureOwnedGroups(this.ownershipToken, processGroupId)
294
+ : this.rootIdentity
295
+ ? await this.unixProcessAdapter.captureDescendantGroups(this.rootIdentity)
296
+ : [];
297
+ if (!this.settled || !this.child?.pid)
298
+ return groups;
299
+ return groups
300
+ .map((group) => ({ ...group, members: group.members.filter((member) => member.pid !== this.child.pid) }))
301
+ .filter((group) => group.members.length > 0);
302
+ }
303
+ async captureWindowsDescendants() {
304
+ const rootPid = this.child?.pid;
305
+ if (!rootPid)
306
+ return;
307
+ const currentRoot = await this.windowsProcessAdapter.captureProcess(rootPid);
308
+ // Once the root disappears, a fresh parent/child snapshot cannot prove
309
+ // ownership: the PID may already have been reused. Only descendants that
310
+ // were captured while the root identity was still observable remain safe.
311
+ if (!currentRoot) {
312
+ if (this.settled) {
313
+ const descendants = await this.windowsProcessAdapter.captureDescendants(rootPid);
314
+ for (const identity of descendants) {
315
+ const existing = this.ownedWindowsProcesses.get(identity.pid);
316
+ if (!existing || existing.birthMarker === identity.birthMarker) {
317
+ this.ownedWindowsProcesses.set(identity.pid, identity);
318
+ }
319
+ }
320
+ if (descendants.length > 0) {
321
+ throw processOwnershipUnverifiable();
322
+ }
323
+ if (![...this.ownedWindowsProcesses].some(([pid]) => pid !== rootPid)) {
324
+ return;
325
+ }
326
+ }
327
+ if (![...this.ownedWindowsProcesses].some(([pid]) => pid !== rootPid)) {
328
+ throw processOwnershipUnverifiable();
329
+ }
330
+ return;
331
+ }
332
+ if (!this.rootWindowsIdentity
333
+ || currentRoot.birthMarker !== this.rootWindowsIdentity.birthMarker) {
334
+ if (![...this.ownedWindowsProcesses].some(([pid]) => pid !== rootPid)) {
335
+ throw processOwnershipUnverifiable();
336
+ }
337
+ return;
338
+ }
339
+ const descendants = await this.windowsProcessAdapter.captureDescendants(rootPid);
340
+ for (const identity of descendants) {
341
+ const existing = this.ownedWindowsProcesses.get(identity.pid);
342
+ if (!existing || existing.birthMarker === identity.birthMarker) {
343
+ this.ownedWindowsProcesses.set(identity.pid, identity);
118
344
  }
119
345
  }
120
346
  }
@@ -126,6 +352,236 @@ export class AcpProcessManager {
126
352
  for (const listener of [...this.exitListeners])
127
353
  listener(this.settled);
128
354
  }
355
+ /**
356
+ * Keep a post-spawn manager reachable even when `start()` rejects before a
357
+ * DriverSession exists. The shared registry retries tree cleanup and is
358
+ * drained with the application runtime.
359
+ */
360
+ async failAfterSpawn(error) {
361
+ const ownerId = this.ensureCleanupOwner();
362
+ try {
363
+ await this.stop();
364
+ }
365
+ catch {
366
+ // Continue to the registry retries even when the first stop fails.
367
+ }
368
+ try {
369
+ await this.retryCleanupOwner(ownerId);
370
+ }
371
+ catch {
372
+ // The registry retains the owner and schedules bounded retries.
373
+ }
374
+ throw error;
375
+ }
376
+ ensureCleanupOwner() {
377
+ return this.cleanupOwnerId ??= acpLaunchCleanupRegistry.register(() => this.stop().then(() => undefined), `acp-process:${this.launch.command}`);
378
+ }
379
+ retryCleanupOwner(ownerId = this.ensureCleanupOwner()) {
380
+ if (!this.cleanupRetryPromise) {
381
+ const retry = acpLaunchCleanupRegistry.runWithImmediateRetries(ownerId);
382
+ let tracked;
383
+ tracked = retry.finally(() => {
384
+ if (this.cleanupRetryPromise === tracked)
385
+ this.cleanupRetryPromise = undefined;
386
+ if (!acpLaunchCleanupRegistry.getState(ownerId))
387
+ this.cleanupOwnerId = undefined;
388
+ });
389
+ this.cleanupRetryPromise = tracked;
390
+ }
391
+ return this.cleanupRetryPromise;
392
+ }
393
+ async terminateChildHandle(signal) {
394
+ try {
395
+ this.child?.kill(signal);
396
+ }
397
+ catch {
398
+ // The child may have exited between the handle check and kill.
399
+ }
400
+ }
401
+ }
402
+ export async function cleanupPersistedAcpProcessTree(identity, options = {}) {
403
+ const platform = options.platform ?? process.platform;
404
+ const graceMs = options.graceMs ?? DEFAULT_KILL_GRACE_MS;
405
+ if (platform === 'win32') {
406
+ const adapter = options.windowsProcessAdapter ?? createWindowsProcessTreeAdapter();
407
+ if (!identity.birthMarker) {
408
+ throw processOwnershipUnverifiable();
409
+ }
410
+ const root = { pid: identity.pid, parentPid: 0, birthMarker: identity.birthMarker };
411
+ const currentRoot = await adapter.captureProcess(identity.pid);
412
+ // A missing root is not evidence that its descendants belong to the old
413
+ // launch. Without a platform-level persisted ownership primitive, refuse
414
+ // recovery rather than risking taskkill against a reused PID tree.
415
+ if (!currentRoot || currentRoot.birthMarker !== root.birthMarker) {
416
+ throw processOwnershipUnverifiable();
417
+ }
418
+ const owned = new Map();
419
+ owned.set(root.pid, root);
420
+ for (const descendant of await adapter.captureDescendants(identity.pid)) {
421
+ owned.set(descendant.pid, descendant);
422
+ }
423
+ for (const processIdentity of [...owned.values()].sort((left, right) => right.pid - left.pid)) {
424
+ if (await adapter.isProcessAlive(processIdentity)) {
425
+ await adapter.terminateTree(processIdentity.pid);
426
+ }
427
+ }
428
+ if (!await waitForWindowsTreeExit(adapter, owned, graceMs)) {
429
+ throw processTreeTimeout();
430
+ }
431
+ return;
432
+ }
433
+ const adapter = options.unixProcessAdapter ?? createUnixProcessIdentityAdapter(platform);
434
+ if (!adapter.captureOwnedGroups || !identity.processGroupId || !identity.birthMarker) {
435
+ throw processOwnershipUnverifiable();
436
+ }
437
+ const pgid = Number(identity.processGroupId);
438
+ const root = Number.isInteger(pgid) && pgid > 0
439
+ ? {
440
+ pid: identity.pid,
441
+ pgid,
442
+ birthIdentity: identity.birthMarker,
443
+ ownershipToken: identity.ownershipToken,
444
+ }
445
+ : undefined;
446
+ const captureGroups = async () => adapter.captureOwnedGroups(identity.ownershipToken);
447
+ let groups = await captureGroups();
448
+ if (root && await adapter.isProcessAlive(root)) {
449
+ const descendants = await adapter.captureDescendantGroups(root);
450
+ groups = mergeUnixGroups(groups, descendants);
451
+ }
452
+ await Promise.all(groups.map((group) => adapter.signalProcessGroup(group, 'SIGTERM')));
453
+ if (root)
454
+ await adapter.signalProcess(root, 'SIGTERM');
455
+ if (await waitForUnixTreeExit(adapter, root, groups, graceMs, captureGroups))
456
+ return;
457
+ groups = mergeUnixGroups(groups, await captureGroups());
458
+ await Promise.all(groups.map((group) => adapter.signalProcessGroup(group, 'SIGKILL')));
459
+ if (root)
460
+ await adapter.signalProcess(root, 'SIGKILL');
461
+ if (!await waitForUnixTreeExit(adapter, root, groups, graceMs, captureGroups)) {
462
+ throw processTreeTimeout();
463
+ }
464
+ }
465
+ function createWindowsProcessTreeAdapter() {
466
+ const listProcesses = () => {
467
+ const script = [
468
+ 'Get-CimInstance Win32_Process',
469
+ 'Select-Object ProcessId,ParentProcessId,CreationDate',
470
+ 'ConvertTo-Json -Compress',
471
+ ].join(' | ');
472
+ try {
473
+ const output = execFileSync('powershell.exe', [
474
+ '-NoProfile',
475
+ '-NonInteractive',
476
+ '-Command',
477
+ script,
478
+ ], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
479
+ if (!output)
480
+ return [];
481
+ const parsed = JSON.parse(output);
482
+ const rows = Array.isArray(parsed) ? parsed : [parsed];
483
+ return rows.map((row) => {
484
+ const pid = Number(row.ProcessId);
485
+ const parentPid = Number(row.ParentProcessId);
486
+ const birthMarker = typeof row.CreationDate === 'string' ? row.CreationDate : '';
487
+ return pid > 0 && Number.isFinite(parentPid) && birthMarker
488
+ ? { pid, parentPid, birthMarker }
489
+ : null;
490
+ }).filter((row) => row !== null);
491
+ }
492
+ catch (error) {
493
+ throw processEnumerationFailed(error);
494
+ }
495
+ };
496
+ return {
497
+ async captureProcess(pid) {
498
+ return listProcesses().find((processIdentity) => processIdentity.pid === pid) ?? null;
499
+ },
500
+ async captureDescendants(rootPid) {
501
+ const rows = listProcesses();
502
+ const descendantIds = new Set([rootPid]);
503
+ let changed = true;
504
+ while (changed) {
505
+ changed = false;
506
+ for (const row of rows) {
507
+ if (descendantIds.has(row.pid) || !descendantIds.has(row.parentPid))
508
+ continue;
509
+ descendantIds.add(row.pid);
510
+ changed = true;
511
+ }
512
+ }
513
+ return rows.filter((row) => row.pid !== rootPid && descendantIds.has(row.pid));
514
+ },
515
+ async isProcessAlive(identity) {
516
+ const current = listProcesses().find((row) => row.pid === identity.pid);
517
+ return current?.birthMarker === identity.birthMarker;
518
+ },
519
+ async terminateTree(pid) {
520
+ try {
521
+ execFileSync('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore' });
522
+ }
523
+ catch {
524
+ // The identity is rechecked before signalling; a concurrent exit is harmless.
525
+ }
526
+ },
527
+ };
528
+ }
529
+ function mergeUnixGroups(left, right) {
530
+ const groups = new Map();
531
+ for (const group of [...left, ...right]) {
532
+ const members = groups.get(group.pgid) ?? [];
533
+ const pids = new Set(members.map((member) => member.pid));
534
+ for (const member of group.members) {
535
+ if (!pids.has(member.pid)) {
536
+ members.push(member);
537
+ pids.add(member.pid);
538
+ }
539
+ }
540
+ groups.set(group.pgid, members);
541
+ }
542
+ return [...groups].map(([pgid, members]) => ({ pgid, members }));
543
+ }
544
+ async function waitForUnixTreeExit(adapter, root, groups, timeoutMs, captureGroups) {
545
+ let knownGroups = groups;
546
+ const deadline = Date.now() + timeoutMs;
547
+ do {
548
+ const rootAlive = root ? await adapter.isProcessAlive(root) : false;
549
+ const groupAlive = (await Promise.all(knownGroups.map((group) => adapter.isProcessGroupAlive(group)))).some(Boolean);
550
+ if (!rootAlive && !groupAlive) {
551
+ if (captureGroups) {
552
+ knownGroups = mergeUnixGroups(knownGroups, await captureGroups());
553
+ }
554
+ const refreshedGroupAlive = (await Promise.all(knownGroups.map((group) => adapter.isProcessGroupAlive(group)))).some(Boolean);
555
+ if (!refreshedGroupAlive)
556
+ return true;
557
+ }
558
+ await new Promise((resolve) => setTimeout(resolve, 25));
559
+ } while (Date.now() < deadline);
560
+ if (captureGroups) {
561
+ knownGroups = mergeUnixGroups(knownGroups, await captureGroups());
562
+ }
563
+ const rootAlive = root ? await adapter.isProcessAlive(root) : false;
564
+ const groupAlive = (await Promise.all(knownGroups.map((group) => adapter.isProcessGroupAlive(group)))).some(Boolean);
565
+ return !rootAlive && !groupAlive;
566
+ }
567
+ async function waitForWindowsTreeExit(adapter, owned, timeoutMs) {
568
+ const deadline = Date.now() + timeoutMs;
569
+ do {
570
+ const alive = (await Promise.all([...owned.values()].map((item) => adapter.isProcessAlive(item)))).some(Boolean);
571
+ if (!alive)
572
+ return true;
573
+ await new Promise((resolve) => setTimeout(resolve, 25));
574
+ } while (Date.now() < deadline);
575
+ return false;
576
+ }
577
+ function processTreeTimeout() {
578
+ return new AgentRuntimeError('process_exit_timeout', 'close', 'ACP adapter did not confirm termination before the shutdown deadline', true);
579
+ }
580
+ function processOwnershipUnverifiable() {
581
+ return new AgentRuntimeError('process_identity_mismatch', 'close', 'ACP adapter process ownership could not be verified safely', true);
582
+ }
583
+ function processEnumerationFailed(cause) {
584
+ return new AgentRuntimeError('process_identity_mismatch', 'close', 'ACP adapter process enumeration failed; ownership could not be verified safely', true, cause === undefined ? undefined : { cause });
129
585
  }
130
586
  function createValidatedOutput(input, options) {
131
587
  const decoder = new StringDecoder('utf8');