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.
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +4 -0
- package/dist/app.js.map +1 -1
- package/dist/cli.js +39 -19
- package/dist/cli.js.map +1 -1
- package/dist/executors/__tests__/base.executor.test.js +133 -1
- package/dist/executors/__tests__/base.executor.test.js.map +1 -1
- package/dist/executors/__tests__/stdin-prompt.executors.test.js +18 -0
- package/dist/executors/__tests__/stdin-prompt.executors.test.js.map +1 -1
- package/dist/executors/base.executor.d.ts +8 -0
- package/dist/executors/base.executor.d.ts.map +1 -1
- package/dist/executors/base.executor.js +336 -56
- package/dist/executors/base.executor.js.map +1 -1
- package/dist/executors/index.d.ts +2 -1
- package/dist/executors/index.d.ts.map +1 -1
- package/dist/executors/index.js +1 -1
- package/dist/executors/index.js.map +1 -1
- package/dist/executors/start-error.d.ts +17 -0
- package/dist/executors/start-error.d.ts.map +1 -1
- package/dist/executors/start-error.js +36 -1
- package/dist/executors/start-error.js.map +1 -1
- package/dist/index.js +39 -29
- package/dist/index.js.map +1 -1
- package/dist/mcp/http-client.test.js +20 -0
- package/dist/mcp/http-client.test.js.map +1 -1
- package/dist/mcp/tools/__tests__/sessions.test.d.ts +2 -0
- package/dist/mcp/tools/__tests__/sessions.test.d.ts.map +1 -0
- package/dist/mcp/tools/__tests__/sessions.test.js +33 -0
- package/dist/mcp/tools/__tests__/sessions.test.js.map +1 -0
- package/dist/routes/__tests__/sessions-runtime.test.js +75 -3
- package/dist/routes/__tests__/sessions-runtime.test.js.map +1 -1
- package/dist/routes/conversations.d.ts.map +1 -1
- package/dist/routes/conversations.js +5 -1
- package/dist/routes/conversations.js.map +1 -1
- package/dist/routes/sessions.d.ts.map +1 -1
- package/dist/routes/sessions.js +13 -2
- package/dist/routes/sessions.js.map +1 -1
- package/dist/routes/team-runs.d.ts.map +1 -1
- package/dist/routes/team-runs.js +13 -1
- package/dist/routes/team-runs.js.map +1 -1
- package/dist/runtime/__tests__/acp-driver-lifecycle.test.js +86 -0
- package/dist/runtime/__tests__/acp-driver-lifecycle.test.js.map +1 -1
- package/dist/runtime/__tests__/acp-process-manager.test.js +393 -6
- package/dist/runtime/__tests__/acp-process-manager.test.js.map +1 -1
- package/dist/runtime/__tests__/cli-driver-lifecycle.test.d.ts +2 -0
- package/dist/runtime/__tests__/cli-driver-lifecycle.test.d.ts.map +1 -0
- package/dist/runtime/__tests__/cli-driver-lifecycle.test.js +244 -0
- package/dist/runtime/__tests__/cli-driver-lifecycle.test.js.map +1 -0
- package/dist/runtime/__tests__/launch-cleanup-registry.test.d.ts +2 -0
- package/dist/runtime/__tests__/launch-cleanup-registry.test.d.ts.map +1 -0
- package/dist/runtime/__tests__/launch-cleanup-registry.test.js +154 -0
- package/dist/runtime/__tests__/launch-cleanup-registry.test.js.map +1 -0
- package/dist/runtime/__tests__/runtime-coordinator.test.js +117 -1
- package/dist/runtime/__tests__/runtime-coordinator.test.js.map +1 -1
- package/dist/runtime/__tests__/server-entry-shutdown.test.d.ts +2 -0
- package/dist/runtime/__tests__/server-entry-shutdown.test.d.ts.map +1 -0
- package/dist/runtime/__tests__/server-entry-shutdown.test.js +38 -0
- package/dist/runtime/__tests__/server-entry-shutdown.test.js.map +1 -0
- package/dist/runtime/__tests__/shutdown-coordinator.test.d.ts +2 -0
- package/dist/runtime/__tests__/shutdown-coordinator.test.d.ts.map +1 -0
- package/dist/runtime/__tests__/shutdown-coordinator.test.js +28 -0
- package/dist/runtime/__tests__/shutdown-coordinator.test.js.map +1 -0
- package/dist/runtime/acp/acp-driver.d.ts.map +1 -1
- package/dist/runtime/acp/acp-driver.js +108 -35
- package/dist/runtime/acp/acp-driver.js.map +1 -1
- package/dist/runtime/acp/agents/managed-directory.d.ts.map +1 -1
- package/dist/runtime/acp/agents/managed-directory.js +3 -1
- package/dist/runtime/acp/agents/managed-directory.js.map +1 -1
- package/dist/runtime/acp/agents/managed-directory.test.d.ts +2 -0
- package/dist/runtime/acp/agents/managed-directory.test.d.ts.map +1 -0
- package/dist/runtime/acp/agents/managed-directory.test.js +25 -0
- package/dist/runtime/acp/agents/managed-directory.test.js.map +1 -0
- package/dist/runtime/acp/launch-cleanup-registry.d.ts +32 -0
- package/dist/runtime/acp/launch-cleanup-registry.d.ts.map +1 -0
- package/dist/runtime/acp/launch-cleanup-registry.js +176 -0
- package/dist/runtime/acp/launch-cleanup-registry.js.map +1 -0
- package/dist/runtime/acp/process-manager.d.ts +58 -0
- package/dist/runtime/acp/process-manager.d.ts.map +1 -1
- package/dist/runtime/acp/process-manager.js +489 -33
- package/dist/runtime/acp/process-manager.js.map +1 -1
- package/dist/runtime/cli-driver.d.ts.map +1 -1
- package/dist/runtime/cli-driver.js +230 -46
- package/dist/runtime/cli-driver.js.map +1 -1
- package/dist/runtime/contracts.d.ts +21 -2
- package/dist/runtime/contracts.d.ts.map +1 -1
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/runtime-coordinator.d.ts +8 -1
- package/dist/runtime/runtime-coordinator.d.ts.map +1 -1
- package/dist/runtime/runtime-coordinator.js +118 -18
- package/dist/runtime/runtime-coordinator.js.map +1 -1
- package/dist/runtime/server-entry-shutdown.d.ts +12 -0
- package/dist/runtime/server-entry-shutdown.d.ts.map +1 -0
- package/dist/runtime/server-entry-shutdown.js +23 -0
- package/dist/runtime/server-entry-shutdown.js.map +1 -0
- package/dist/runtime/shutdown-coordinator.d.ts +17 -0
- package/dist/runtime/shutdown-coordinator.d.ts.map +1 -0
- package/dist/runtime/shutdown-coordinator.js +67 -0
- package/dist/runtime/shutdown-coordinator.js.map +1 -0
- package/dist/services/__tests__/conversation.service.test.js +160 -0
- package/dist/services/__tests__/conversation.service.test.js.map +1 -1
- package/dist/services/__tests__/database-maintenance.service.test.js +97 -0
- package/dist/services/__tests__/database-maintenance.service.test.js.map +1 -1
- package/dist/services/__tests__/member-heartbeat-scheduler.test.js +6 -0
- package/dist/services/__tests__/member-heartbeat-scheduler.test.js.map +1 -1
- package/dist/services/__tests__/member-heartbeat.test.js +7 -11
- package/dist/services/__tests__/member-heartbeat.test.js.map +1 -1
- package/dist/services/__tests__/session-manager.lifecycle.test.js +402 -12
- package/dist/services/__tests__/session-manager.lifecycle.test.js.map +1 -1
- package/dist/services/__tests__/session-manager.team-run.test.js +422 -29
- package/dist/services/__tests__/session-manager.team-run.test.js.map +1 -1
- package/dist/services/__tests__/team-queue-recovery.integration.test.js +1 -0
- package/dist/services/__tests__/team-queue-recovery.integration.test.js.map +1 -1
- package/dist/services/__tests__/team-reconciler.service.test.js +1282 -4
- package/dist/services/__tests__/team-reconciler.service.test.js.map +1 -1
- package/dist/services/__tests__/team-run.service.test.js +161 -1
- package/dist/services/__tests__/team-run.service.test.js.map +1 -1
- package/dist/services/__tests__/team-scheduler.service.test.js +334 -6
- package/dist/services/__tests__/team-scheduler.service.test.js.map +1 -1
- package/dist/services/conversation.service.d.ts.map +1 -1
- package/dist/services/conversation.service.js +21 -2
- package/dist/services/conversation.service.js.map +1 -1
- package/dist/services/database-maintenance.service.d.ts.map +1 -1
- package/dist/services/database-maintenance.service.js +68 -17
- package/dist/services/database-maintenance.service.js.map +1 -1
- package/dist/services/member-heartbeat-scheduler.d.ts +6 -5
- package/dist/services/member-heartbeat-scheduler.d.ts.map +1 -1
- package/dist/services/member-heartbeat-scheduler.js +7 -5
- package/dist/services/member-heartbeat-scheduler.js.map +1 -1
- package/dist/services/session-manager.d.ts +124 -14
- package/dist/services/session-manager.d.ts.map +1 -1
- package/dist/services/session-manager.js +1035 -116
- package/dist/services/session-manager.js.map +1 -1
- package/dist/services/session-runtime-cleanup-gate.d.ts +25 -0
- package/dist/services/session-runtime-cleanup-gate.d.ts.map +1 -0
- package/dist/services/session-runtime-cleanup-gate.js +284 -0
- package/dist/services/session-runtime-cleanup-gate.js.map +1 -0
- package/dist/services/task.service.d.ts +2 -2
- package/dist/services/team-member-admission-barrier.d.ts +3 -0
- package/dist/services/team-member-admission-barrier.d.ts.map +1 -0
- package/dist/services/team-member-admission-barrier.js +26 -0
- package/dist/services/team-member-admission-barrier.js.map +1 -0
- package/dist/services/team-reconciler.service.d.ts +30 -7
- package/dist/services/team-reconciler.service.d.ts.map +1 -1
- package/dist/services/team-reconciler.service.js +528 -111
- package/dist/services/team-reconciler.service.js.map +1 -1
- package/dist/services/team-run.service.d.ts.map +1 -1
- package/dist/services/team-run.service.js +37 -13
- package/dist/services/team-run.service.js.map +1 -1
- package/dist/services/team-scheduler.service.d.ts +8 -6
- package/dist/services/team-scheduler.service.d.ts.map +1 -1
- package/dist/services/team-scheduler.service.js +323 -120
- package/dist/services/team-scheduler.service.js.map +1 -1
- package/dist/services/workspace-background-process-manager.d.ts.map +1 -1
- package/dist/services/workspace-background-process-manager.js +2 -5
- package/dist/services/workspace-background-process-manager.js.map +1 -1
- package/dist/services/workspace.service.d.ts +15 -8
- package/dist/services/workspace.service.d.ts.map +1 -1
- package/dist/socket/index.d.ts.map +1 -1
- package/dist/socket/index.js +27 -14
- package/dist/socket/index.js.map +1 -1
- package/dist/utils/__tests__/unix-process-identity.test.d.ts +2 -0
- package/dist/utils/__tests__/unix-process-identity.test.d.ts.map +1 -0
- package/dist/utils/__tests__/unix-process-identity.test.js +73 -0
- package/dist/utils/__tests__/unix-process-identity.test.js.map +1 -0
- package/dist/utils/error-log.d.ts +2 -0
- package/dist/utils/error-log.d.ts.map +1 -1
- package/dist/utils/error-log.js +40 -3
- package/dist/utils/error-log.js.map +1 -1
- package/dist/utils/error-log.test.js +14 -2
- package/dist/utils/error-log.test.js.map +1 -1
- package/dist/utils/process-launch.d.ts +2 -2
- package/dist/utils/process-launch.d.ts.map +1 -1
- package/dist/utils/process-launch.js +339 -79
- package/dist/utils/process-launch.js.map +1 -1
- package/dist/utils/process-launch.test.js +168 -10
- package/dist/utils/process-launch.test.js.map +1 -1
- package/dist/utils/spawned-process-identity.d.ts +7 -0
- package/dist/utils/spawned-process-identity.d.ts.map +1 -0
- package/dist/utils/spawned-process-identity.js +36 -0
- package/dist/utils/spawned-process-identity.js.map +1 -0
- package/dist/utils/tree-cleanup-channel.d.ts +18 -0
- package/dist/utils/tree-cleanup-channel.d.ts.map +1 -0
- package/dist/utils/tree-cleanup-channel.js +16 -0
- package/dist/utils/tree-cleanup-channel.js.map +1 -0
- package/dist/utils/tree-cleanup-channel.test.d.ts +2 -0
- package/dist/utils/tree-cleanup-channel.test.d.ts.map +1 -0
- package/dist/utils/tree-cleanup-channel.test.js +15 -0
- package/dist/utils/tree-cleanup-channel.test.js.map +1 -0
- package/dist/utils/unix-process-identity.d.ts +13 -1
- package/dist/utils/unix-process-identity.d.ts.map +1 -1
- package/dist/utils/unix-process-identity.js +146 -25
- package/dist/utils/unix-process-identity.js.map +1 -1
- package/node_modules/@agent-tower/shared/dist/types.d.ts +1 -0
- package/node_modules/@agent-tower/shared/dist/types.d.ts.map +1 -1
- package/node_modules/@agent-tower/shared/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20260824000000_add_execution_process_ownership/migration.sql +10 -0
- package/prisma/migrations/20260824010000_add_runtime_cleanup_recovery/migration.sql +10 -0
- package/prisma/migrations/20260824020000_add_runtime_launch_admission/migration.sql +72 -0
- package/prisma/migrations/20260826000000_add_conversation_turn_queue/migration.sql +19 -0
- package/prisma/schema.prisma +50 -7
|
@@ -56,12 +56,14 @@ let SessionManager;
|
|
|
56
56
|
let sessionMsgStoreManager;
|
|
57
57
|
let WorkspaceBackgroundService;
|
|
58
58
|
let WorkspaceBackgroundProcessManager;
|
|
59
|
+
let markPreChildProcessFailure;
|
|
59
60
|
/** 可手动触发事件的 fake PTY,语义对齐 node-pty(不重放事件) */
|
|
60
61
|
class ControlledPty {
|
|
61
62
|
pid = 4242;
|
|
62
63
|
killed = false;
|
|
63
64
|
dataListeners = [];
|
|
64
65
|
exitListeners = [];
|
|
66
|
+
exited = false;
|
|
65
67
|
onData = (cb) => {
|
|
66
68
|
this.dataListeners.push(cb);
|
|
67
69
|
return { dispose: () => { this.dataListeners = this.dataListeners.filter((l) => l !== cb); } };
|
|
@@ -75,17 +77,26 @@ class ControlledPty {
|
|
|
75
77
|
l(data);
|
|
76
78
|
}
|
|
77
79
|
emitExit(exitCode) {
|
|
80
|
+
if (this.exited)
|
|
81
|
+
return;
|
|
82
|
+
this.exited = true;
|
|
78
83
|
for (const l of [...this.exitListeners])
|
|
79
84
|
l({ exitCode });
|
|
80
85
|
}
|
|
81
86
|
write() { }
|
|
82
87
|
resize() { }
|
|
83
|
-
kill() {
|
|
88
|
+
kill() {
|
|
89
|
+
this.killed = true;
|
|
90
|
+
this.emitExit(0);
|
|
91
|
+
}
|
|
84
92
|
}
|
|
85
93
|
function spawnResultFor(pty, earlyEvents = []) {
|
|
86
94
|
let taken = false;
|
|
87
95
|
return {
|
|
88
96
|
pid: pty.pid,
|
|
97
|
+
processGroupId: String(pty.pid),
|
|
98
|
+
birthMarker: `test-birth:${pty.pid}`,
|
|
99
|
+
ownershipToken: `test-owner:${pty.pid}`,
|
|
89
100
|
pty,
|
|
90
101
|
takeEarlyEvents: () => {
|
|
91
102
|
if (taken)
|
|
@@ -152,11 +163,13 @@ describe('SessionManager session status vs real process state', () => {
|
|
|
152
163
|
const outputModule = await import('../../output/index.js');
|
|
153
164
|
const backgroundServiceModule = await import('../workspace-background-service.service.js');
|
|
154
165
|
const backgroundManagerModule = await import('../workspace-background-process-manager.js');
|
|
166
|
+
const startErrorModule = await import('../../executors/start-error.js');
|
|
155
167
|
prisma = utilsModule.prisma;
|
|
156
168
|
SessionManager = sessionManagerModule.SessionManager;
|
|
157
169
|
sessionMsgStoreManager = outputModule.sessionMsgStoreManager;
|
|
158
170
|
WorkspaceBackgroundService = backgroundServiceModule.WorkspaceBackgroundService;
|
|
159
171
|
WorkspaceBackgroundProcessManager = backgroundManagerModule.WorkspaceBackgroundProcessManager;
|
|
172
|
+
markPreChildProcessFailure = startErrorModule.markPreChildProcessFailure;
|
|
160
173
|
});
|
|
161
174
|
beforeEach(async () => {
|
|
162
175
|
vi.clearAllMocks();
|
|
@@ -165,6 +178,7 @@ describe('SessionManager session status vs real process state', () => {
|
|
|
165
178
|
getExecutorByProviderMock.mockImplementation(createMockExecutor);
|
|
166
179
|
clearAgentApiCredentials();
|
|
167
180
|
await prisma.executionProcess.deleteMany();
|
|
181
|
+
await prisma.conversation.deleteMany();
|
|
168
182
|
await prisma.session.deleteMany();
|
|
169
183
|
await prisma.workspace.deleteMany();
|
|
170
184
|
await prisma.task.deleteMany();
|
|
@@ -218,7 +232,7 @@ describe('SessionManager session status vs real process state', () => {
|
|
|
218
232
|
expect(getExecutorByProviderMock).toHaveBeenNthCalledWith(1, 'provider-snapshot');
|
|
219
233
|
expect(getExecutorByProviderMock).toHaveBeenNthCalledWith(2, 'provider-snapshot');
|
|
220
234
|
expect(executorSnapshots).toEqual([false, true]);
|
|
221
|
-
manager.destroyAll();
|
|
235
|
+
await manager.destroyAll();
|
|
222
236
|
});
|
|
223
237
|
it('uses one latest Provider snapshot for follow-up resume and its new-session fallback', async () => {
|
|
224
238
|
const provider = {
|
|
@@ -241,7 +255,7 @@ describe('SessionManager session status vs real process state', () => {
|
|
|
241
255
|
spawnFollowUp: vi.fn(async () => {
|
|
242
256
|
observed.push({ path: 'resume', disabled });
|
|
243
257
|
provider.config.disableResponsesWebsocket = false;
|
|
244
|
-
throw new Error('synthetic resume failure');
|
|
258
|
+
throw markPreChildProcessFailure(new Error('synthetic resume failure'));
|
|
245
259
|
}),
|
|
246
260
|
spawn: vi.fn(async () => {
|
|
247
261
|
observed.push({ path: 'fallback', disabled });
|
|
@@ -261,7 +275,40 @@ describe('SessionManager session status vs real process state', () => {
|
|
|
261
275
|
{ path: 'resume', disabled: true },
|
|
262
276
|
{ path: 'fallback', disabled: true },
|
|
263
277
|
]);
|
|
264
|
-
|
|
278
|
+
await expect(prisma.session.findUnique({ where: { id: session.id } })).resolves.toMatchObject({
|
|
279
|
+
runtimeLaunchState: 'PROCESS_RECORDED',
|
|
280
|
+
runtimeLaunchClaimCount: 1,
|
|
281
|
+
runtimeLaunchResolvedCount: 1,
|
|
282
|
+
runtimeLaunchProcessCount: 1,
|
|
283
|
+
});
|
|
284
|
+
await manager.destroyAll();
|
|
285
|
+
});
|
|
286
|
+
it('keeps terminal conversation follow-ups compatible without a TeamRun invocation identity', async () => {
|
|
287
|
+
const conversation = await prisma.conversation.create({
|
|
288
|
+
data: {
|
|
289
|
+
title: 'Lifecycle conversation',
|
|
290
|
+
directoryName: `lifecycle-conversation-${Date.now()}`,
|
|
291
|
+
workingDir: testDir,
|
|
292
|
+
session: {
|
|
293
|
+
create: {
|
|
294
|
+
context: 'CONVERSATION',
|
|
295
|
+
agentType: AgentType.CODEX,
|
|
296
|
+
variant: 'DEFAULT',
|
|
297
|
+
prompt: 'initial question',
|
|
298
|
+
status: SessionStatus.COMPLETED,
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
include: { session: true },
|
|
303
|
+
});
|
|
304
|
+
spawnMock.mockResolvedValueOnce(spawnResultFor(new ControlledPty()));
|
|
305
|
+
const manager = new SessionManager(new EventBus());
|
|
306
|
+
await expect(manager.sendMessage(conversation.session.id, 'follow-up question'))
|
|
307
|
+
.resolves.toMatchObject({ id: conversation.session.id });
|
|
308
|
+
expect(spawnMock).toHaveBeenCalledTimes(1);
|
|
309
|
+
await expect(prisma.session.findUnique({ where: { id: conversation.session.id } }))
|
|
310
|
+
.resolves.toMatchObject({ status: SessionStatus.RUNNING });
|
|
311
|
+
await manager.destroyAll();
|
|
265
312
|
});
|
|
266
313
|
it('marks a reused ACP follow-up RUNNING without a new process event and allows it to stop', async () => {
|
|
267
314
|
const provider = {
|
|
@@ -280,6 +327,10 @@ describe('SessionManager session status vs real process state', () => {
|
|
|
280
327
|
data: {
|
|
281
328
|
runtimeType: 'ACP',
|
|
282
329
|
status: SessionStatus.COMPLETED,
|
|
330
|
+
runtimeLaunchState: 'PROCESS_RECORDED',
|
|
331
|
+
runtimeLaunchClaimCount: 1,
|
|
332
|
+
runtimeLaunchResolvedCount: 1,
|
|
333
|
+
runtimeLaunchProcessCount: 1,
|
|
283
334
|
externalSessionId: 'external-acp-session',
|
|
284
335
|
logSnapshot: JSON.stringify({
|
|
285
336
|
sessionId: 'external-acp-session',
|
|
@@ -288,14 +339,35 @@ describe('SessionManager session status vs real process state', () => {
|
|
|
288
339
|
}),
|
|
289
340
|
},
|
|
290
341
|
});
|
|
342
|
+
await prisma.executionProcess.create({
|
|
343
|
+
data: {
|
|
344
|
+
sessionId: session.id,
|
|
345
|
+
launchClaimNumber: 1,
|
|
346
|
+
runtimeInstanceId: 'reused-acp-runtime',
|
|
347
|
+
pid: 4301,
|
|
348
|
+
processGroupId: '4301',
|
|
349
|
+
birthMarker: 'test-birth:4301',
|
|
350
|
+
ownershipToken: 'test-owner:4301',
|
|
351
|
+
cleanupState: 'ACTIVE',
|
|
352
|
+
},
|
|
353
|
+
});
|
|
291
354
|
const completion = new Promise(() => undefined);
|
|
292
|
-
const hasActiveTurn = vi.fn()
|
|
355
|
+
const hasActiveTurn = vi.fn()
|
|
356
|
+
.mockReturnValueOnce(false)
|
|
357
|
+
.mockReturnValueOnce(true)
|
|
358
|
+
.mockReturnValue(false);
|
|
293
359
|
const runtimeCoordinator = {
|
|
294
360
|
hasActiveTurn,
|
|
295
361
|
startTurn: vi.fn(async (_input) => ({ turnId: 'turn-2', completion })),
|
|
296
362
|
abandonTurn: vi.fn(async () => true),
|
|
297
363
|
cancelTurn: vi.fn(async () => undefined),
|
|
298
|
-
disposeSession: vi.fn(async () =>
|
|
364
|
+
disposeSession: vi.fn(async () => {
|
|
365
|
+
await prisma.executionProcess.updateMany({
|
|
366
|
+
where: { sessionId: session.id },
|
|
367
|
+
data: { cleanupState: 'CONFIRMED' },
|
|
368
|
+
});
|
|
369
|
+
}),
|
|
370
|
+
hasRuntimeProcessOwner: vi.fn(() => false),
|
|
299
371
|
destroyAll: vi.fn(async () => undefined),
|
|
300
372
|
};
|
|
301
373
|
const manager = new SessionManager(new EventBus());
|
|
@@ -314,7 +386,13 @@ describe('SessionManager session status vs real process state', () => {
|
|
|
314
386
|
.toBe(startInput?.historyBoundaryEntryId);
|
|
315
387
|
expect((await prisma.session.findUnique({ where: { id: session.id } }))?.status)
|
|
316
388
|
.toBe(SessionStatus.RUNNING);
|
|
317
|
-
expect(
|
|
389
|
+
await expect(prisma.session.findUnique({ where: { id: session.id } })).resolves.toMatchObject({
|
|
390
|
+
runtimeLaunchState: 'REUSED',
|
|
391
|
+
runtimeLaunchClaimCount: 2,
|
|
392
|
+
runtimeLaunchResolvedCount: 2,
|
|
393
|
+
runtimeLaunchProcessCount: 1,
|
|
394
|
+
});
|
|
395
|
+
expect(await prisma.executionProcess.count({ where: { sessionId: session.id } })).toBe(1);
|
|
318
396
|
// Even if a stale terminal value is observed, an active Runtime turn wins.
|
|
319
397
|
await prisma.session.update({
|
|
320
398
|
where: { id: session.id },
|
|
@@ -669,7 +747,7 @@ describe('SessionManager session status vs real process state', () => {
|
|
|
669
747
|
providerId: provider.id,
|
|
670
748
|
status: SessionStatus.RUNNING,
|
|
671
749
|
});
|
|
672
|
-
manager.destroyAll();
|
|
750
|
+
await manager.destroyAll();
|
|
673
751
|
});
|
|
674
752
|
it('keeps completed-turn post-processing alive when a follow-up provider was deleted', async () => {
|
|
675
753
|
const { task, workspace, session } = await createSessionFixture();
|
|
@@ -692,11 +770,11 @@ describe('SessionManager session status vs real process state', () => {
|
|
|
692
770
|
await completed;
|
|
693
771
|
await expect(manager.sendMessage(session.id, 'follow-up', 'deleted-provider'))
|
|
694
772
|
.rejects.toThrow('Provider not found: deleted-provider');
|
|
695
|
-
expect(autoCommitSpy).toHaveBeenCalledTimes(1);
|
|
773
|
+
await vi.waitFor(() => expect(autoCommitSpy).toHaveBeenCalledTimes(1));
|
|
696
774
|
expect(spawnMock).toHaveBeenCalledTimes(1);
|
|
697
775
|
releaseAutoCommit();
|
|
698
776
|
await vi.waitFor(async () => {
|
|
699
|
-
expect(reconcileSpy).toHaveBeenCalledWith(session.id);
|
|
777
|
+
expect(reconcileSpy).toHaveBeenCalledWith(session.id, expect.any(String));
|
|
700
778
|
expect((await prisma.task.findUnique({ where: { id: task.id } }))?.status)
|
|
701
779
|
.toBe(TaskStatus.IN_REVIEW);
|
|
702
780
|
expect(commitMessageSpy).toHaveBeenCalledWith(workspace.id);
|
|
@@ -735,7 +813,7 @@ describe('SessionManager session status vs real process state', () => {
|
|
|
735
813
|
expect(spawnMock).toHaveBeenCalledTimes(1);
|
|
736
814
|
releaseAutoCommit();
|
|
737
815
|
await vi.waitFor(async () => {
|
|
738
|
-
expect(reconcileSpy).toHaveBeenCalledWith(session.id);
|
|
816
|
+
expect(reconcileSpy).toHaveBeenCalledWith(session.id, expect.any(String));
|
|
739
817
|
expect((await prisma.task.findUnique({ where: { id: task.id } }))?.status)
|
|
740
818
|
.toBe(TaskStatus.IN_REVIEW);
|
|
741
819
|
expect(commitMessageSpy).toHaveBeenCalledWith(workspace.id);
|
|
@@ -794,7 +872,7 @@ describe('SessionManager session status vs real process state', () => {
|
|
|
794
872
|
await followUp;
|
|
795
873
|
expect(reconcileSpy).not.toHaveBeenCalled();
|
|
796
874
|
expect(spawnMock).toHaveBeenCalledTimes(2);
|
|
797
|
-
manager.destroyAll();
|
|
875
|
+
await manager.destroyAll();
|
|
798
876
|
});
|
|
799
877
|
it('keeps turn.failed terminal when the wrapper exits with code 0', async () => {
|
|
800
878
|
const { task, session } = await createSessionFixture();
|
|
@@ -936,5 +1014,317 @@ describe('SessionManager session status vs real process state', () => {
|
|
|
936
1014
|
await backgroundManager.stopAll().catch(() => undefined);
|
|
937
1015
|
}
|
|
938
1016
|
}, 15_000);
|
|
1017
|
+
it('does not confirm ACP tree cleanup on root exit or let a late old cleanup overwrite a new runtime', async () => {
|
|
1018
|
+
const { session } = await createSessionFixture();
|
|
1019
|
+
const manager = new SessionManager(new EventBus());
|
|
1020
|
+
const handleProcessEvent = manager.handleRuntimeProcessEvent.bind(manager);
|
|
1021
|
+
await prisma.session.update({
|
|
1022
|
+
where: { id: session.id },
|
|
1023
|
+
data: {
|
|
1024
|
+
status: SessionStatus.RUNNING,
|
|
1025
|
+
runtimeLaunchState: 'CLAIMED',
|
|
1026
|
+
runtimeLaunchClaimCount: 1,
|
|
1027
|
+
},
|
|
1028
|
+
});
|
|
1029
|
+
await handleProcessEvent({
|
|
1030
|
+
type: 'started',
|
|
1031
|
+
towerSessionId: session.id,
|
|
1032
|
+
runtimeInstanceId: 'runtime-old',
|
|
1033
|
+
launchClaimNumber: 1,
|
|
1034
|
+
pid: 4101,
|
|
1035
|
+
processGroupId: '4101',
|
|
1036
|
+
birthMarker: 'darwin:old:token-old',
|
|
1037
|
+
ownershipToken: 'token-old',
|
|
1038
|
+
});
|
|
1039
|
+
await handleProcessEvent({
|
|
1040
|
+
type: 'exited',
|
|
1041
|
+
towerSessionId: session.id,
|
|
1042
|
+
runtimeInstanceId: 'runtime-old',
|
|
1043
|
+
exitCode: 0,
|
|
1044
|
+
launchClaimNumber: 1,
|
|
1045
|
+
});
|
|
1046
|
+
await expect(prisma.executionProcess.findFirst({
|
|
1047
|
+
where: { sessionId: session.id, runtimeInstanceId: 'runtime-old' },
|
|
1048
|
+
})).resolves.toMatchObject({ exitCode: 0, cleanupState: 'PENDING' });
|
|
1049
|
+
await prisma.session.update({
|
|
1050
|
+
where: { id: session.id },
|
|
1051
|
+
data: {
|
|
1052
|
+
runtimeLaunchState: 'CLAIMED',
|
|
1053
|
+
runtimeLaunchClaimCount: 2,
|
|
1054
|
+
},
|
|
1055
|
+
});
|
|
1056
|
+
await handleProcessEvent({
|
|
1057
|
+
type: 'started',
|
|
1058
|
+
towerSessionId: session.id,
|
|
1059
|
+
runtimeInstanceId: 'runtime-new',
|
|
1060
|
+
launchClaimNumber: 2,
|
|
1061
|
+
pid: 4102,
|
|
1062
|
+
processGroupId: '4102',
|
|
1063
|
+
birthMarker: 'darwin:new:token-new',
|
|
1064
|
+
ownershipToken: 'token-new',
|
|
1065
|
+
});
|
|
1066
|
+
await handleProcessEvent({
|
|
1067
|
+
type: 'exited',
|
|
1068
|
+
towerSessionId: session.id,
|
|
1069
|
+
runtimeInstanceId: 'runtime-shared',
|
|
1070
|
+
launchClaimNumber: 1,
|
|
1071
|
+
exitCode: 0,
|
|
1072
|
+
signal: null,
|
|
1073
|
+
});
|
|
1074
|
+
await expect(prisma.executionProcess.findFirstOrThrow({
|
|
1075
|
+
where: { sessionId: session.id, launchClaimNumber: 1 },
|
|
1076
|
+
})).resolves.toMatchObject({ cleanupState: 'PENDING' });
|
|
1077
|
+
await expect(prisma.executionProcess.findFirstOrThrow({
|
|
1078
|
+
where: { sessionId: session.id, launchClaimNumber: 2 },
|
|
1079
|
+
})).resolves.toMatchObject({ cleanupState: 'ACTIVE' });
|
|
1080
|
+
await handleProcessEvent({
|
|
1081
|
+
type: 'tree_cleanup_completed',
|
|
1082
|
+
towerSessionId: session.id,
|
|
1083
|
+
runtimeInstanceId: 'runtime-old',
|
|
1084
|
+
launchClaimNumber: 1,
|
|
1085
|
+
});
|
|
1086
|
+
await handleProcessEvent({
|
|
1087
|
+
type: 'exited',
|
|
1088
|
+
towerSessionId: session.id,
|
|
1089
|
+
runtimeInstanceId: 'runtime-old',
|
|
1090
|
+
exitCode: 0,
|
|
1091
|
+
launchClaimNumber: 1,
|
|
1092
|
+
});
|
|
1093
|
+
await expect(prisma.executionProcess.findFirst({
|
|
1094
|
+
where: { sessionId: session.id, runtimeInstanceId: 'runtime-old' },
|
|
1095
|
+
})).resolves.toMatchObject({ cleanupState: 'CONFIRMED' });
|
|
1096
|
+
await expect(prisma.executionProcess.findFirst({
|
|
1097
|
+
where: { sessionId: session.id, runtimeInstanceId: 'runtime-new' },
|
|
1098
|
+
})).resolves.toMatchObject({ cleanupState: 'ACTIVE' });
|
|
1099
|
+
await manager.destroyAll();
|
|
1100
|
+
});
|
|
1101
|
+
it('requires the launch claim when cleanup events share a runtime instance id', async () => {
|
|
1102
|
+
const { session } = await createSessionFixture();
|
|
1103
|
+
const manager = new SessionManager(new EventBus());
|
|
1104
|
+
const handleProcessEvent = manager.handleRuntimeProcessEvent.bind(manager);
|
|
1105
|
+
await prisma.session.update({
|
|
1106
|
+
where: { id: session.id },
|
|
1107
|
+
data: {
|
|
1108
|
+
status: SessionStatus.RUNNING,
|
|
1109
|
+
runtimeLaunchState: 'CLAIMED',
|
|
1110
|
+
runtimeLaunchClaimCount: 1,
|
|
1111
|
+
},
|
|
1112
|
+
});
|
|
1113
|
+
await handleProcessEvent({
|
|
1114
|
+
type: 'started',
|
|
1115
|
+
towerSessionId: session.id,
|
|
1116
|
+
runtimeInstanceId: 'runtime-shared',
|
|
1117
|
+
launchClaimNumber: 1,
|
|
1118
|
+
pid: 4111,
|
|
1119
|
+
processGroupId: '4111',
|
|
1120
|
+
birthMarker: 'darwin:shared:one',
|
|
1121
|
+
ownershipToken: 'token-shared-one',
|
|
1122
|
+
});
|
|
1123
|
+
await prisma.session.update({
|
|
1124
|
+
where: { id: session.id },
|
|
1125
|
+
data: {
|
|
1126
|
+
runtimeLaunchState: 'CLAIMED',
|
|
1127
|
+
runtimeLaunchClaimCount: 2,
|
|
1128
|
+
},
|
|
1129
|
+
});
|
|
1130
|
+
await handleProcessEvent({
|
|
1131
|
+
type: 'started',
|
|
1132
|
+
towerSessionId: session.id,
|
|
1133
|
+
runtimeInstanceId: 'runtime-shared',
|
|
1134
|
+
launchClaimNumber: 2,
|
|
1135
|
+
pid: 4112,
|
|
1136
|
+
processGroupId: '4112',
|
|
1137
|
+
birthMarker: 'darwin:shared:two',
|
|
1138
|
+
ownershipToken: 'token-shared-two',
|
|
1139
|
+
});
|
|
1140
|
+
// A late event without the generation claim cannot be assigned to either
|
|
1141
|
+
// owner, even when runtimeInstanceId is shared across reconnects.
|
|
1142
|
+
await handleProcessEvent({
|
|
1143
|
+
type: 'tree_cleanup_completed',
|
|
1144
|
+
towerSessionId: session.id,
|
|
1145
|
+
runtimeInstanceId: 'runtime-shared',
|
|
1146
|
+
});
|
|
1147
|
+
await expect(prisma.executionProcess.findMany({
|
|
1148
|
+
where: { sessionId: session.id, runtimeInstanceId: 'runtime-shared' },
|
|
1149
|
+
orderBy: { launchClaimNumber: 'asc' },
|
|
1150
|
+
})).resolves.toMatchObject([
|
|
1151
|
+
{ launchClaimNumber: 1, cleanupState: 'ACTIVE' },
|
|
1152
|
+
{ launchClaimNumber: 2, cleanupState: 'ACTIVE' },
|
|
1153
|
+
]);
|
|
1154
|
+
await handleProcessEvent({
|
|
1155
|
+
type: 'tree_cleanup_completed',
|
|
1156
|
+
towerSessionId: session.id,
|
|
1157
|
+
runtimeInstanceId: 'runtime-shared',
|
|
1158
|
+
launchClaimNumber: 1,
|
|
1159
|
+
});
|
|
1160
|
+
await expect(prisma.executionProcess.findFirstOrThrow({
|
|
1161
|
+
where: { sessionId: session.id, launchClaimNumber: 1 },
|
|
1162
|
+
})).resolves.toMatchObject({ cleanupState: 'CONFIRMED' });
|
|
1163
|
+
await expect(prisma.executionProcess.findFirstOrThrow({
|
|
1164
|
+
where: { sessionId: session.id, launchClaimNumber: 2 },
|
|
1165
|
+
})).resolves.toMatchObject({ cleanupState: 'ACTIVE' });
|
|
1166
|
+
await handleProcessEvent({
|
|
1167
|
+
type: 'tree_cleanup_completed',
|
|
1168
|
+
towerSessionId: session.id,
|
|
1169
|
+
runtimeInstanceId: 'runtime-shared',
|
|
1170
|
+
launchClaimNumber: 2,
|
|
1171
|
+
});
|
|
1172
|
+
await expect(prisma.executionProcess.findMany({
|
|
1173
|
+
where: { sessionId: session.id, runtimeInstanceId: 'runtime-shared' },
|
|
1174
|
+
orderBy: { launchClaimNumber: 'asc' },
|
|
1175
|
+
})).resolves.toMatchObject([
|
|
1176
|
+
{ launchClaimNumber: 1, cleanupState: 'CONFIRMED' },
|
|
1177
|
+
{ launchClaimNumber: 2, cleanupState: 'CONFIRMED' },
|
|
1178
|
+
]);
|
|
1179
|
+
await manager.destroyAll();
|
|
1180
|
+
});
|
|
1181
|
+
it('replays early process events after deferred started persistence', async () => {
|
|
1182
|
+
const { session } = await createSessionFixture();
|
|
1183
|
+
const manager = new SessionManager(new EventBus());
|
|
1184
|
+
const handleProcessEvent = manager.handleRuntimeProcessEvent.bind(manager);
|
|
1185
|
+
// Simulate the raw wrapper exit arriving before the started transaction.
|
|
1186
|
+
await handleProcessEvent({
|
|
1187
|
+
type: 'exited',
|
|
1188
|
+
towerSessionId: session.id,
|
|
1189
|
+
runtimeInstanceId: 'runtime-deferred',
|
|
1190
|
+
exitCode: 0,
|
|
1191
|
+
launchClaimNumber: 1,
|
|
1192
|
+
});
|
|
1193
|
+
await handleProcessEvent({
|
|
1194
|
+
type: 'tree_cleanup_completed',
|
|
1195
|
+
towerSessionId: session.id,
|
|
1196
|
+
runtimeInstanceId: 'runtime-deferred',
|
|
1197
|
+
launchClaimNumber: 1,
|
|
1198
|
+
});
|
|
1199
|
+
await expect(prisma.session.findUnique({ where: { id: session.id } }))
|
|
1200
|
+
.resolves.toMatchObject({
|
|
1201
|
+
runtimeLaunchState: 'QUARANTINED',
|
|
1202
|
+
runtimeLaunchDiagnostic: expect.stringContaining('before started persistence'),
|
|
1203
|
+
});
|
|
1204
|
+
await prisma.session.update({
|
|
1205
|
+
where: { id: session.id },
|
|
1206
|
+
data: {
|
|
1207
|
+
status: SessionStatus.RUNNING,
|
|
1208
|
+
runtimeLaunchState: 'CLAIMED',
|
|
1209
|
+
runtimeLaunchClaimCount: 1,
|
|
1210
|
+
},
|
|
1211
|
+
});
|
|
1212
|
+
await handleProcessEvent({
|
|
1213
|
+
type: 'started',
|
|
1214
|
+
towerSessionId: session.id,
|
|
1215
|
+
runtimeInstanceId: 'runtime-deferred',
|
|
1216
|
+
launchClaimNumber: 1,
|
|
1217
|
+
pid: 4301,
|
|
1218
|
+
processGroupId: '4301',
|
|
1219
|
+
birthMarker: 'linux:deferred',
|
|
1220
|
+
ownershipToken: 'owner-deferred',
|
|
1221
|
+
});
|
|
1222
|
+
await expect(prisma.executionProcess.findFirstOrThrow({
|
|
1223
|
+
where: { sessionId: session.id, runtimeInstanceId: 'runtime-deferred' },
|
|
1224
|
+
})).resolves.toMatchObject({ exitCode: 0, cleanupState: 'CONFIRMED' });
|
|
1225
|
+
expect(manager.hasRuntimeProcessOwner('runtime-deferred')).toBe(false);
|
|
1226
|
+
await manager.destroyAll();
|
|
1227
|
+
});
|
|
1228
|
+
it('does not let late runtime events downgrade incomplete ownership quarantine', async () => {
|
|
1229
|
+
const { session } = await createSessionFixture();
|
|
1230
|
+
const manager = new SessionManager(new EventBus());
|
|
1231
|
+
const handleProcessEvent = manager.handleRuntimeProcessEvent.bind(manager);
|
|
1232
|
+
await prisma.session.update({
|
|
1233
|
+
where: { id: session.id },
|
|
1234
|
+
data: {
|
|
1235
|
+
status: SessionStatus.RUNNING,
|
|
1236
|
+
runtimeLaunchState: 'CLAIMED',
|
|
1237
|
+
runtimeLaunchClaimCount: 1,
|
|
1238
|
+
},
|
|
1239
|
+
});
|
|
1240
|
+
await handleProcessEvent({
|
|
1241
|
+
type: 'started',
|
|
1242
|
+
towerSessionId: session.id,
|
|
1243
|
+
runtimeInstanceId: 'runtime-incomplete',
|
|
1244
|
+
launchClaimNumber: 1,
|
|
1245
|
+
pid: 4151,
|
|
1246
|
+
processGroupId: '4151',
|
|
1247
|
+
birthMarker: '',
|
|
1248
|
+
ownershipToken: 'token-incomplete',
|
|
1249
|
+
});
|
|
1250
|
+
await handleProcessEvent({
|
|
1251
|
+
type: 'exited',
|
|
1252
|
+
towerSessionId: session.id,
|
|
1253
|
+
runtimeInstanceId: 'runtime-incomplete',
|
|
1254
|
+
exitCode: 0,
|
|
1255
|
+
launchClaimNumber: 1,
|
|
1256
|
+
});
|
|
1257
|
+
await handleProcessEvent({
|
|
1258
|
+
type: 'tree_cleanup_completed',
|
|
1259
|
+
towerSessionId: session.id,
|
|
1260
|
+
runtimeInstanceId: 'runtime-incomplete',
|
|
1261
|
+
launchClaimNumber: 1,
|
|
1262
|
+
});
|
|
1263
|
+
await expect(prisma.executionProcess.findFirstOrThrow({
|
|
1264
|
+
where: { sessionId: session.id, runtimeInstanceId: 'runtime-incomplete' },
|
|
1265
|
+
})).resolves.toMatchObject({
|
|
1266
|
+
cleanupState: 'QUARANTINED',
|
|
1267
|
+
cleanupError: expect.stringContaining('complete process ownership identity'),
|
|
1268
|
+
});
|
|
1269
|
+
await manager.destroyAll();
|
|
1270
|
+
});
|
|
1271
|
+
it('bounds pending process events per generation and expires them', async () => {
|
|
1272
|
+
vi.useFakeTimers();
|
|
1273
|
+
try {
|
|
1274
|
+
const { session } = await createSessionFixture();
|
|
1275
|
+
const manager = new SessionManager(new EventBus());
|
|
1276
|
+
const handleProcessEvent = manager;
|
|
1277
|
+
for (let index = 0; index < 9; index += 1) {
|
|
1278
|
+
await handleProcessEvent.handleRuntimeProcessEvent({
|
|
1279
|
+
type: 'exited',
|
|
1280
|
+
towerSessionId: session.id,
|
|
1281
|
+
runtimeInstanceId: 'runtime-overflow',
|
|
1282
|
+
launchClaimNumber: 1,
|
|
1283
|
+
exitCode: 1,
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
expect(handleProcessEvent.pendingRuntimeProcessEvents.size).toBe(0);
|
|
1287
|
+
await handleProcessEvent.handleRuntimeProcessEvent({
|
|
1288
|
+
type: 'exited',
|
|
1289
|
+
towerSessionId: session.id,
|
|
1290
|
+
runtimeInstanceId: 'runtime-expiry',
|
|
1291
|
+
launchClaimNumber: 2,
|
|
1292
|
+
exitCode: 1,
|
|
1293
|
+
});
|
|
1294
|
+
expect(handleProcessEvent.pendingRuntimeProcessEvents.size).toBe(1);
|
|
1295
|
+
await vi.advanceTimersByTimeAsync(60_001);
|
|
1296
|
+
expect(handleProcessEvent.pendingRuntimeProcessEvents.size).toBe(0);
|
|
1297
|
+
await manager.destroyAll();
|
|
1298
|
+
}
|
|
1299
|
+
finally {
|
|
1300
|
+
vi.useRealTimers();
|
|
1301
|
+
}
|
|
1302
|
+
});
|
|
1303
|
+
it('persists bounded cleanup retry attempts for recovery scans', async () => {
|
|
1304
|
+
const { session } = await createSessionFixture();
|
|
1305
|
+
const manager = new SessionManager(new EventBus());
|
|
1306
|
+
const processRecord = await prisma.executionProcess.create({
|
|
1307
|
+
data: {
|
|
1308
|
+
sessionId: session.id,
|
|
1309
|
+
launchClaimNumber: 1,
|
|
1310
|
+
runtimeInstanceId: 'runtime-retry',
|
|
1311
|
+
pid: 4201,
|
|
1312
|
+
processGroupId: '4201',
|
|
1313
|
+
birthMarker: 'linux:200:owner-retry',
|
|
1314
|
+
ownershipToken: 'owner-retry',
|
|
1315
|
+
cleanupState: 'PENDING',
|
|
1316
|
+
},
|
|
1317
|
+
});
|
|
1318
|
+
const markCleanup = manager.markRuntimeProcessCleanupState.bind(manager);
|
|
1319
|
+
await markCleanup(session.id, 'runtime-retry', 'FAILED', 'first failure', 1);
|
|
1320
|
+
const first = await prisma.executionProcess.findUniqueOrThrow({ where: { id: processRecord.id } });
|
|
1321
|
+
await markCleanup(session.id, 'runtime-retry', 'FAILED', 'second failure', 1);
|
|
1322
|
+
const second = await prisma.executionProcess.findUniqueOrThrow({ where: { id: processRecord.id } });
|
|
1323
|
+
expect(first).toMatchObject({ cleanupState: 'FAILED', cleanupAttemptCount: 1, cleanupError: 'first failure' });
|
|
1324
|
+
expect(second).toMatchObject({ cleanupState: 'FAILED', cleanupAttemptCount: 2, cleanupError: 'second failure' });
|
|
1325
|
+
expect(first.nextCleanupRetryAt).not.toBeNull();
|
|
1326
|
+
expect(second.nextCleanupRetryAt.getTime()).toBeGreaterThan(first.nextCleanupRetryAt.getTime());
|
|
1327
|
+
await manager.destroyAll();
|
|
1328
|
+
});
|
|
939
1329
|
});
|
|
940
1330
|
//# sourceMappingURL=session-manager.lifecycle.test.js.map
|