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
@@ -7,6 +7,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vites
7
7
  import { AgentType, SessionStatus } from '../../types/index.js';
8
8
  import { EventBus } from '../../core/event-bus.js';
9
9
  import { withWorkspaceBackgroundServicePolicy } from '../../prompts/workspace-background-service-policy.js';
10
+ import { appendAgentOutputIntentInstructions } from '../../prompts/agent-output-intents.js';
10
11
  import { TeamLockService } from '../team-lock.service.js';
11
12
  import { AGENT_SUBPROCESS_BLOCKED_ENV_KEYS, AGENT_TOWER_MCP_IDENTITY_ENV_KEYS, AGENT_TOWER_MCP_SERVICE_ENV_KEYS, } from '../../executors/execution-env.js';
12
13
  import { AGENT_API_CREDENTIAL_ENV, validateAgentApiCredential, } from '../../utils/agent-api-credential.js';
@@ -122,15 +123,39 @@ function expectServiceEnvFiltered(fullEnv) {
122
123
  expect(validateAgentApiCredential(fullEnv[AGENT_API_CREDENTIAL_ENV])).toMatchObject({ sessionId: fullEnv.AGENT_TOWER_SESSION_ID });
123
124
  }
124
125
  function createPty() {
126
+ let exited = false;
127
+ let exitListeners = [];
128
+ const emitExit = (event) => {
129
+ if (exited)
130
+ return;
131
+ exited = true;
132
+ for (const listener of [...exitListeners])
133
+ listener(event);
134
+ };
135
+ const kill = vi.fn(() => emitExit({ exitCode: 0 }));
125
136
  return {
126
137
  pid: 12345,
127
138
  onData: vi.fn(() => ({ dispose: vi.fn() })),
128
- onExit: vi.fn((_listener) => ({
129
- dispose: vi.fn(),
130
- })),
139
+ onExit: vi.fn((listener) => {
140
+ exitListeners.push(listener);
141
+ return {
142
+ dispose: vi.fn(() => {
143
+ exitListeners = exitListeners.filter((candidate) => candidate !== listener);
144
+ }),
145
+ };
146
+ }),
131
147
  write: vi.fn(),
132
148
  resize: vi.fn(),
133
- kill: vi.fn(),
149
+ kill,
150
+ };
151
+ }
152
+ function spawnResult(pid, pty = createPty()) {
153
+ return {
154
+ pid,
155
+ processGroupId: String(pid),
156
+ birthMarker: `test-birth:${pid}`,
157
+ ownershipToken: `test-owner:${pid}`,
158
+ pty,
134
159
  };
135
160
  }
136
161
  async function createWorkspace() {
@@ -183,14 +208,8 @@ describe('SessionManager TeamRun env injection', () => {
183
208
  beforeEach(async () => {
184
209
  seedServiceEnv();
185
210
  vi.clearAllMocks();
186
- spawnMock.mockImplementation(async () => ({
187
- pid: 12345,
188
- pty: createPty(),
189
- }));
190
- spawnFollowUpMock.mockImplementation(async () => ({
191
- pid: 12346,
192
- pty: createPty(),
193
- }));
211
+ spawnMock.mockImplementation(async () => spawnResult(12345));
212
+ spawnFollowUpMock.mockImplementation(async () => spawnResult(12346));
194
213
  manager = new SessionManager(new EventBus());
195
214
  await prisma.executionProcess.deleteMany();
196
215
  await prisma.agentInvocation.deleteMany();
@@ -261,6 +280,16 @@ describe('SessionManager TeamRun env injection', () => {
261
280
  status: 'RUNNING',
262
281
  },
263
282
  });
283
+ spawnMock.mockImplementationOnce(async (config) => {
284
+ expect(config.env.toObject().AGENT_TOWER_SESSION_ID).toBe(session.id);
285
+ await expect(prisma.session.findUnique({ where: { id: session.id } })).resolves.toMatchObject({
286
+ status: SessionStatus.RUNNING,
287
+ runtimeLaunchState: 'CLAIMED',
288
+ runtimeLaunchClaimCount: 1,
289
+ runtimeLaunchResolvedCount: 0,
290
+ });
291
+ return spawnResult(12345);
292
+ });
264
293
  await manager.start(session.id);
265
294
  const spawnConfig = spawnMock.mock.calls[0][0];
266
295
  expect(spawnConfig.env.toObject()).toMatchObject({
@@ -277,7 +306,74 @@ describe('SessionManager TeamRun env injection', () => {
277
306
  AGENT_TOWER_TEAM_RUN_ID: teamRun.id,
278
307
  AGENT_TOWER_MEMBER_ID: member.id,
279
308
  });
280
- manager.destroyAll();
309
+ await expect(prisma.session.findUnique({ where: { id: session.id } })).resolves.toMatchObject({
310
+ runtimeLaunchState: 'PROCESS_RECORDED',
311
+ runtimeLaunchClaimCount: 1,
312
+ runtimeLaunchResolvedCount: 1,
313
+ runtimeLaunchProcessCount: 1,
314
+ });
315
+ await manager.destroyAll();
316
+ });
317
+ it('revokes TeamRun dispatch before a direct Session stop waits for unresolved cleanup', async () => {
318
+ vi.spyOn(console, 'warn').mockImplementation(() => undefined);
319
+ const { task, workspace } = await createWorkspace();
320
+ const teamRun = await prisma.teamRun.create({ data: { taskId: task.id, mode: 'AUTO' } });
321
+ const member = await prisma.teamMember.create({
322
+ data: {
323
+ teamRunId: teamRun.id,
324
+ name: 'Direct stop member',
325
+ aliases: '[]',
326
+ providerId: 'codex-default',
327
+ rolePrompt: 'Role',
328
+ capabilities: '{}',
329
+ workspacePolicy: 'shared',
330
+ triggerPolicy: 'MENTION_ONLY',
331
+ },
332
+ });
333
+ const request = await prisma.workRequest.create({
334
+ data: {
335
+ teamRunId: teamRun.id,
336
+ requesterType: 'user',
337
+ targetMemberId: member.id,
338
+ triggerMessageId: 'direct-stop-cleanup-pending',
339
+ instruction: 'Run',
340
+ status: 'STARTED',
341
+ },
342
+ });
343
+ const session = await prisma.session.create({
344
+ data: {
345
+ workspaceId: workspace.id,
346
+ agentType: AgentType.CODEX,
347
+ providerId: 'codex-default',
348
+ prompt: 'prompt',
349
+ status: SessionStatus.RUNNING,
350
+ runtimeLaunchState: 'CLAIMED',
351
+ runtimeLaunchClaimCount: 1,
352
+ },
353
+ });
354
+ const invocation = await prisma.agentInvocation.create({
355
+ data: {
356
+ teamRunId: teamRun.id,
357
+ workRequestId: request.id,
358
+ memberId: member.id,
359
+ workspaceId: workspace.id,
360
+ sessionId: session.id,
361
+ status: 'RUNNING',
362
+ },
363
+ });
364
+ await manager.stop(session.id);
365
+ await expect(prisma.agentInvocation.findUniqueOrThrow({ where: { id: invocation.id } })).resolves.toMatchObject({
366
+ status: 'RUNNING',
367
+ dispatchRevokedAt: expect.any(Date),
368
+ });
369
+ await expect(prisma.workRequest.findUniqueOrThrow({ where: { id: request.id } })).resolves.toMatchObject({
370
+ status: 'STARTED',
371
+ });
372
+ await expect(prisma.session.findUniqueOrThrow({ where: { id: session.id } })).resolves.toMatchObject({
373
+ status: SessionStatus.RUNNING,
374
+ runtimeLaunchState: 'QUARANTINED',
375
+ runtimeLaunchResolvedCount: 0,
376
+ });
281
377
  });
282
378
  it('injects targeted test port env when the invocation has allocated ports', async () => {
283
379
  const { task, workspace } = await createWorkspace();
@@ -352,7 +448,7 @@ describe('SessionManager TeamRun env injection', () => {
352
448
  VITE_PORT: '21001',
353
449
  E2E_PORT: '21002',
354
450
  });
355
- manager.destroyAll();
451
+ await manager.destroyAll();
356
452
  });
357
453
  it('injects only the workspace-bound session identity for a solo session', async () => {
358
454
  const { workspace } = await createWorkspace();
@@ -377,7 +473,7 @@ describe('SessionManager TeamRun env injection', () => {
377
473
  expect(fullEnv).not.toHaveProperty('AGENT_TOWER_INVOCATION_ID');
378
474
  expect(fullEnv).not.toHaveProperty('AGENT_TOWER_TEAM_RUN_ID');
379
475
  expect(fullEnv).not.toHaveProperty('AGENT_TOWER_MEMBER_ID');
380
- manager.destroyAll();
476
+ await manager.destroyAll();
381
477
  });
382
478
  it('starts a new Tower session as an executor follow-up while injecting the new invocation env', async () => {
383
479
  const { task, workspace } = await createWorkspace();
@@ -441,6 +537,16 @@ describe('SessionManager TeamRun env injection', () => {
441
537
  status: 'RUNNING',
442
538
  },
443
539
  });
540
+ spawnFollowUpMock.mockImplementationOnce(async (config) => {
541
+ expect(config.env.toObject().AGENT_TOWER_SESSION_ID).toBe(nextSession.id);
542
+ await expect(prisma.session.findUnique({ where: { id: nextSession.id } })).resolves.toMatchObject({
543
+ status: SessionStatus.RUNNING,
544
+ runtimeLaunchState: 'CLAIMED',
545
+ runtimeLaunchClaimCount: 1,
546
+ runtimeLaunchResolvedCount: 0,
547
+ });
548
+ return spawnResult(12346);
549
+ });
444
550
  const startTurnSpy = vi.spyOn(manager.runtimeCoordinator, 'startTurn');
445
551
  await manager.startFollowUp(nextSession.id, previousSession.id);
446
552
  expect(spawnFollowUpMock).toHaveBeenCalledTimes(1);
@@ -452,7 +558,7 @@ describe('SessionManager TeamRun env injection', () => {
452
558
  expect(spawnFollowUpMock.mock.calls[0][1]).toBe('agent-native-session-1');
453
559
  expect(spawnMock).not.toHaveBeenCalled();
454
560
  const spawnConfig = spawnFollowUpMock.mock.calls[0][0];
455
- expect(spawnConfig.prompt).toBe(withWorkspaceBackgroundServicePolicy('next prompt'));
561
+ expect(spawnConfig.prompt).toBe(appendAgentOutputIntentInstructions(withWorkspaceBackgroundServicePolicy('next prompt')));
456
562
  expect(spawnConfig.env.toObject()).toMatchObject({
457
563
  AGENT_TOWER_SESSION_ID: nextSession.id,
458
564
  AGENT_TOWER_INVOCATION_ID: invocation.id,
@@ -469,10 +575,14 @@ describe('SessionManager TeamRun env injection', () => {
469
575
  });
470
576
  await expect(prisma.session.findUnique({ where: { id: nextSession.id } })).resolves.toMatchObject({
471
577
  status: SessionStatus.RUNNING,
578
+ runtimeLaunchState: 'PROCESS_RECORDED',
579
+ runtimeLaunchClaimCount: 1,
580
+ runtimeLaunchResolvedCount: 1,
581
+ runtimeLaunchProcessCount: 1,
472
582
  });
473
- manager.destroyAll();
583
+ await manager.destroyAll();
474
584
  });
475
- it('kills a spawned process when the task is deleted during session start', async () => {
585
+ it('quarantines a spawned process when its process row cannot be written during session start', async () => {
476
586
  const { task, workspace } = await createWorkspace();
477
587
  const pty = createPty();
478
588
  spawnMock.mockImplementationOnce(async () => {
@@ -480,7 +590,7 @@ describe('SessionManager TeamRun env injection', () => {
480
590
  where: { id: task.id },
481
591
  data: { deletedAt: new Date() },
482
592
  });
483
- return { pid: 22345, pty };
593
+ return spawnResult(22345, pty);
484
594
  });
485
595
  const session = await prisma.session.create({
486
596
  data: {
@@ -498,7 +608,10 @@ describe('SessionManager TeamRun env injection', () => {
498
608
  expect(pty.kill).toHaveBeenCalled();
499
609
  await expect(prisma.executionProcess.count({ where: { sessionId: session.id } })).resolves.toBe(0);
500
610
  await expect(prisma.session.findUnique({ where: { id: session.id } })).resolves.toMatchObject({
501
- status: SessionStatus.CANCELLED,
611
+ status: SessionStatus.RUNNING,
612
+ runtimeLaunchState: 'QUARANTINED',
613
+ runtimeLaunchClaimCount: 1,
614
+ runtimeLaunchResolvedCount: 0,
502
615
  });
503
616
  });
504
617
  it('normalizes a real CommandBuildError as a deterministic start error', async () => {
@@ -517,6 +630,13 @@ describe('SessionManager TeamRun env injection', () => {
517
630
  code: 'AGENT_COMMAND_UNAVAILABLE',
518
631
  statusCode: 400,
519
632
  });
633
+ await expect(prisma.session.findUnique({ where: { id: session.id } })).resolves.toMatchObject({
634
+ status: SessionStatus.CANCELLED,
635
+ runtimeLaunchState: 'SAFE_PRE_CHILD_FAILURE',
636
+ runtimeLaunchClaimCount: 1,
637
+ runtimeLaunchResolvedCount: 1,
638
+ runtimeLaunchProcessCount: 0,
639
+ });
520
640
  });
521
641
  it('reports a missing executor as a deterministic start error', async () => {
522
642
  const { workspace } = await createWorkspace();
@@ -646,7 +766,7 @@ describe('SessionManager TeamRun env injection', () => {
646
766
  },
647
767
  });
648
768
  const pty = createPty();
649
- spawnMock.mockResolvedValueOnce({ pid: 52345, pty });
769
+ spawnMock.mockResolvedValueOnce(spawnResult(52345, pty));
650
770
  await manager.start(session.id);
651
771
  await expect(prisma.executionProcess.findFirst({ where: { sessionId: session.id } })).resolves.toMatchObject({
652
772
  pid: 52345,
@@ -661,7 +781,163 @@ describe('SessionManager TeamRun env injection', () => {
661
781
  });
662
782
  });
663
783
  });
664
- it('kills a spawned follow-up process when the task is deleted during follow-up start', async () => {
784
+ it('rejects an initial start after direct stop has cancelled the Session', async () => {
785
+ const { workspace } = await createWorkspace();
786
+ const session = await prisma.session.create({
787
+ data: {
788
+ workspaceId: workspace.id,
789
+ agentType: AgentType.CODEX,
790
+ providerId: 'codex-default',
791
+ prompt: 'cancelled before spawn',
792
+ status: SessionStatus.CANCELLED,
793
+ },
794
+ });
795
+ await expect(manager.start(session.id)).rejects.toMatchObject({
796
+ code: 'SESSION_NOT_ADMITTED',
797
+ });
798
+ expect(spawnMock).not.toHaveBeenCalled();
799
+ await expect(prisma.session.findUnique({ where: { id: session.id } })).resolves.toMatchObject({
800
+ status: SessionStatus.CANCELLED,
801
+ });
802
+ });
803
+ it('rejects a cross-Tower resume start after direct stop cancelled the new Session', async () => {
804
+ const { workspace } = await createWorkspace();
805
+ const previousSession = await prisma.session.create({
806
+ data: {
807
+ workspaceId: workspace.id,
808
+ agentType: AgentType.CODEX,
809
+ providerId: 'codex-default',
810
+ prompt: 'previous prompt',
811
+ status: SessionStatus.COMPLETED,
812
+ externalSessionId: 'native-resume-session',
813
+ },
814
+ });
815
+ const nextSession = await prisma.session.create({
816
+ data: {
817
+ workspaceId: workspace.id,
818
+ agentType: AgentType.CODEX,
819
+ providerId: 'codex-default',
820
+ prompt: 'cancelled resume',
821
+ status: SessionStatus.CANCELLED,
822
+ },
823
+ });
824
+ await expect(manager.startFollowUp(nextSession.id, previousSession.id)).rejects.toMatchObject({
825
+ code: 'SESSION_NOT_ADMITTED',
826
+ });
827
+ expect(spawnFollowUpMock).not.toHaveBeenCalled();
828
+ await expect(prisma.session.findUnique({ where: { id: nextSession.id } })).resolves.toMatchObject({
829
+ status: SessionStatus.CANCELLED,
830
+ });
831
+ });
832
+ it('does not spawn a real resume_last follow-up when pre-invocation direct stop wins', async () => {
833
+ const { task, workspace } = await createWorkspace();
834
+ const teamRun = await prisma.teamRun.create({ data: { taskId: task.id, mode: 'AUTO' } });
835
+ const member = await prisma.teamMember.create({
836
+ data: {
837
+ teamRunId: teamRun.id,
838
+ name: 'Resume member',
839
+ aliases: '[]',
840
+ providerId: 'codex-default',
841
+ rolePrompt: 'Continue prior work',
842
+ capabilities: JSON.stringify({
843
+ readRoom: true,
844
+ postRoomMessage: true,
845
+ mentionMembers: true,
846
+ stopMemberWork: false,
847
+ markReadyForReview: false,
848
+ readFiles: true,
849
+ writeFiles: true,
850
+ runCommands: false,
851
+ readDiff: true,
852
+ mergeWorkspace: false,
853
+ }),
854
+ workspacePolicy: 'shared',
855
+ triggerPolicy: 'MENTION_ONLY',
856
+ sessionPolicy: 'resume_last',
857
+ queueManagementPolicy: 'own_only',
858
+ },
859
+ });
860
+ const previousRequest = await prisma.workRequest.create({
861
+ data: {
862
+ teamRunId: teamRun.id,
863
+ requesterType: 'user',
864
+ targetMemberId: member.id,
865
+ triggerMessageId: 'previous-resume-request',
866
+ instruction: 'Previous work',
867
+ status: 'COMPLETED',
868
+ },
869
+ });
870
+ const previousSession = await prisma.session.create({
871
+ data: {
872
+ workspaceId: workspace.id,
873
+ agentType: AgentType.CODEX,
874
+ providerId: member.providerId,
875
+ prompt: 'previous prompt',
876
+ status: SessionStatus.COMPLETED,
877
+ externalSessionId: 'native-resume-context',
878
+ },
879
+ });
880
+ await prisma.agentInvocation.create({
881
+ data: {
882
+ teamRunId: teamRun.id,
883
+ workRequestId: previousRequest.id,
884
+ memberId: member.id,
885
+ workspaceId: workspace.id,
886
+ sessionId: previousSession.id,
887
+ status: 'COMPLETED',
888
+ },
889
+ });
890
+ const nextRequest = await prisma.workRequest.create({
891
+ data: {
892
+ teamRunId: teamRun.id,
893
+ requesterType: 'user',
894
+ targetMemberId: member.id,
895
+ triggerMessageId: 'next-resume-request',
896
+ instruction: 'Continue with the native context',
897
+ status: 'QUEUED',
898
+ },
899
+ });
900
+ let resolveSessionCreated;
901
+ const sessionCreated = new Promise((resolve) => {
902
+ resolveSessionCreated = resolve;
903
+ });
904
+ let allowCreateReturn;
905
+ const createReturn = new Promise((resolve) => {
906
+ allowCreateReturn = resolve;
907
+ });
908
+ const originalCreate = manager.create.bind(manager);
909
+ vi.spyOn(manager, 'create').mockImplementationOnce(async (...args) => {
910
+ const session = await originalCreate(...args);
911
+ resolveSessionCreated(session);
912
+ await createReturn;
913
+ return session;
914
+ });
915
+ const scheduler = new TeamSchedulerService(new TeamLockService(), {
916
+ workspaceService: { create: vi.fn(async () => workspace) },
917
+ sessionManager: manager,
918
+ getProviderById: vi.fn(() => ({
919
+ id: member.providerId,
920
+ name: 'Codex',
921
+ agentType: AgentType.CODEX,
922
+ env: {},
923
+ config: {},
924
+ isDefault: true,
925
+ })),
926
+ });
927
+ const starting = scheduler.startNextSessions(teamRun.id);
928
+ const createdSession = await sessionCreated;
929
+ await scheduler.stopSession(createdSession.id);
930
+ allowCreateReturn();
931
+ await expect(starting).resolves.toEqual([]);
932
+ expect(spawnFollowUpMock).not.toHaveBeenCalled();
933
+ await expect(prisma.session.findUnique({ where: { id: createdSession.id } })).resolves.toMatchObject({
934
+ status: SessionStatus.CANCELLED,
935
+ });
936
+ await expect(prisma.workRequest.findUnique({ where: { id: nextRequest.id } })).resolves.toMatchObject({
937
+ status: 'CANCELLED',
938
+ });
939
+ });
940
+ it('quarantines a spawned follow-up when its process row cannot be written', async () => {
665
941
  const { task, workspace } = await createWorkspace();
666
942
  const previousSession = await prisma.session.create({
667
943
  data: {
@@ -688,7 +964,7 @@ describe('SessionManager TeamRun env injection', () => {
688
964
  where: { id: task.id },
689
965
  data: { deletedAt: new Date() },
690
966
  });
691
- return { pid: 32345, pty };
967
+ return spawnResult(32345, pty);
692
968
  });
693
969
  await expect(manager.startFollowUp(nextSession.id, previousSession.id)).rejects.toMatchObject({
694
970
  code: 'NOT_FOUND',
@@ -698,10 +974,13 @@ describe('SessionManager TeamRun env injection', () => {
698
974
  expect(pty.kill).toHaveBeenCalled();
699
975
  await expect(prisma.executionProcess.count({ where: { sessionId: nextSession.id } })).resolves.toBe(0);
700
976
  await expect(prisma.session.findUnique({ where: { id: nextSession.id } })).resolves.toMatchObject({
701
- status: SessionStatus.CANCELLED,
977
+ status: SessionStatus.RUNNING,
978
+ runtimeLaunchState: 'QUARANTINED',
979
+ runtimeLaunchClaimCount: 1,
980
+ runtimeLaunchResolvedCount: 0,
702
981
  });
703
982
  });
704
- it('kills a spawned reply process when the task is deleted during sendMessage', async () => {
983
+ it('quarantines a spawned reply process when its process row cannot be written', async () => {
705
984
  const { task, workspace } = await createWorkspace();
706
985
  const session = await prisma.session.create({
707
986
  data: {
@@ -718,7 +997,7 @@ describe('SessionManager TeamRun env injection', () => {
718
997
  where: { id: task.id },
719
998
  data: { deletedAt: new Date() },
720
999
  });
721
- return { pid: 42345, pty };
1000
+ return spawnResult(42345, pty);
722
1001
  });
723
1002
  await expect(manager.sendMessage(session.id, 'continue')).rejects.toMatchObject({
724
1003
  code: 'NOT_FOUND',
@@ -727,7 +1006,121 @@ describe('SessionManager TeamRun env injection', () => {
727
1006
  expect(pty.kill).toHaveBeenCalled();
728
1007
  await expect(prisma.executionProcess.count({ where: { sessionId: session.id } })).resolves.toBe(0);
729
1008
  await expect(prisma.session.findUnique({ where: { id: session.id } })).resolves.toMatchObject({
730
- status: SessionStatus.CANCELLED,
1009
+ status: SessionStatus.RUNNING,
1010
+ runtimeLaunchState: 'QUARANTINED',
1011
+ runtimeLaunchClaimCount: 1,
1012
+ runtimeLaunchResolvedCount: 0,
1013
+ });
1014
+ });
1015
+ it('rejects a revoked TeamRun reminder before replacing the active turn', async () => {
1016
+ const { task, workspace } = await createWorkspace();
1017
+ const teamRun = await prisma.teamRun.create({ data: { taskId: task.id, mode: 'AUTO' } });
1018
+ const member = await prisma.teamMember.create({
1019
+ data: {
1020
+ teamRunId: teamRun.id,
1021
+ name: 'Reminder member',
1022
+ aliases: '[]',
1023
+ providerId: 'codex-default',
1024
+ rolePrompt: 'Role',
1025
+ capabilities: '{}',
1026
+ workspacePolicy: 'shared',
1027
+ triggerPolicy: 'MENTION_ONLY',
1028
+ sessionPolicy: 'new_per_request',
1029
+ queueManagementPolicy: 'own_only',
1030
+ },
1031
+ });
1032
+ const request = await prisma.workRequest.create({
1033
+ data: {
1034
+ teamRunId: teamRun.id,
1035
+ requesterType: 'user',
1036
+ targetMemberId: member.id,
1037
+ triggerMessageId: 'revoked-reminder-request',
1038
+ instruction: 'Run',
1039
+ status: 'STARTED',
1040
+ },
1041
+ });
1042
+ const session = await prisma.session.create({
1043
+ data: {
1044
+ workspaceId: workspace.id,
1045
+ agentType: AgentType.CODEX,
1046
+ providerId: 'codex-default',
1047
+ prompt: 'active prompt',
1048
+ status: SessionStatus.PENDING,
1049
+ },
1050
+ });
1051
+ const invocation = await prisma.agentInvocation.create({
1052
+ data: {
1053
+ teamRunId: teamRun.id,
1054
+ workRequestId: request.id,
1055
+ memberId: member.id,
1056
+ workspaceId: workspace.id,
1057
+ sessionId: session.id,
1058
+ status: 'RUNNING',
1059
+ },
1060
+ });
1061
+ const activePty = createPty();
1062
+ spawnMock.mockResolvedValueOnce({ pid: 43001, pty: activePty });
1063
+ await manager.start(session.id);
1064
+ await prisma.agentInvocation.update({
1065
+ where: { id: invocation.id },
1066
+ data: { dispatchRevokedAt: new Date() },
1067
+ });
1068
+ await expect(manager.sendMessage(session.id, 'late reminder', undefined, invocation.id)).rejects.toMatchObject({ code: 'SESSION_NOT_ADMITTED' });
1069
+ expect(spawnMock).toHaveBeenCalledTimes(1);
1070
+ expect(activePty.kill).not.toHaveBeenCalled();
1071
+ await expect(prisma.session.findUnique({ where: { id: session.id } })).resolves.toMatchObject({
1072
+ status: SessionStatus.RUNNING,
1073
+ });
1074
+ await manager.stop(session.id, { skipTeamRunReconcile: true });
1075
+ });
1076
+ it('rejects a direct follow-up bound to a terminal TeamRun invocation', async () => {
1077
+ const { task, workspace } = await createWorkspace();
1078
+ const teamRun = await prisma.teamRun.create({ data: { taskId: task.id, mode: 'AUTO' } });
1079
+ const member = await prisma.teamMember.create({
1080
+ data: {
1081
+ teamRunId: teamRun.id,
1082
+ name: 'Terminal member',
1083
+ aliases: '[]',
1084
+ providerId: 'codex-default',
1085
+ rolePrompt: 'Role',
1086
+ capabilities: '{}',
1087
+ workspacePolicy: 'shared',
1088
+ triggerPolicy: 'MENTION_ONLY',
1089
+ },
1090
+ });
1091
+ const request = await prisma.workRequest.create({
1092
+ data: {
1093
+ teamRunId: teamRun.id,
1094
+ requesterType: 'user',
1095
+ targetMemberId: member.id,
1096
+ triggerMessageId: 'terminal-direct-send',
1097
+ instruction: 'Run',
1098
+ status: 'COMPLETED',
1099
+ },
1100
+ });
1101
+ const session = await prisma.session.create({
1102
+ data: {
1103
+ workspaceId: workspace.id,
1104
+ agentType: AgentType.CODEX,
1105
+ providerId: 'codex-default',
1106
+ prompt: 'completed prompt',
1107
+ status: SessionStatus.COMPLETED,
1108
+ },
1109
+ });
1110
+ const invocation = await prisma.agentInvocation.create({
1111
+ data: {
1112
+ teamRunId: teamRun.id,
1113
+ workRequestId: request.id,
1114
+ memberId: member.id,
1115
+ workspaceId: workspace.id,
1116
+ sessionId: session.id,
1117
+ status: 'COMPLETED',
1118
+ },
1119
+ });
1120
+ await expect(manager.sendMessage(session.id, 'late direct follow-up', undefined, invocation.id)).rejects.toMatchObject({ code: 'SESSION_NOT_ADMITTED' });
1121
+ expect(spawnMock).not.toHaveBeenCalled();
1122
+ await expect(prisma.session.findUnique({ where: { id: session.id } })).resolves.toMatchObject({
1123
+ status: SessionStatus.COMPLETED,
731
1124
  });
732
1125
  });
733
1126
  it('does not record a TeamRun heartbeat from the local user_message patch created by sendMessage', async () => {
@@ -787,14 +1180,14 @@ describe('SessionManager TeamRun env injection', () => {
787
1180
  nextRoomReplyReminderAt,
788
1181
  },
789
1182
  });
790
- await manager.sendMessage(session.id, 'heartbeat nudge');
1183
+ await manager.sendMessage(session.id, 'heartbeat nudge', undefined, invocation.id);
791
1184
  await new Promise((resolve) => setTimeout(resolve, 20));
792
1185
  const reloaded = await prisma.agentInvocation.findUniqueOrThrow({ where: { id: invocation.id } });
793
1186
  expect(reloaded.lastHeartbeatAt?.toISOString()).toBe(lastHeartbeatAt.toISOString());
794
1187
  expect(reloaded.roomReplyReminderCount).toBe(3);
795
1188
  expect(reloaded.nextRoomReplyReminderAt?.toISOString()).toBe(nextRoomReplyReminderAt.toISOString());
796
1189
  expect(spawnMock).toHaveBeenCalledTimes(1);
797
- manager.destroyAll();
1190
+ await manager.destroyAll();
798
1191
  });
799
1192
  });
800
1193
  //# sourceMappingURL=session-manager.team-run.test.js.map