agent-tower 0.6.0-beta.13 → 0.6.0-beta.15
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/executors/default-providers.json +1 -0
- package/dist/runtime/__tests__/acp-driver-lifecycle.test.js +232 -6
- package/dist/runtime/__tests__/acp-driver-lifecycle.test.js.map +1 -1
- package/dist/runtime/__tests__/acp-driver.test.js +24 -0
- package/dist/runtime/__tests__/acp-driver.test.js.map +1 -1
- package/dist/runtime/__tests__/runtime-coordinator.test.js +205 -7
- package/dist/runtime/__tests__/runtime-coordinator.test.js.map +1 -1
- package/dist/runtime/acp/acp-driver.d.ts.map +1 -1
- package/dist/runtime/acp/acp-driver.js +188 -61
- package/dist/runtime/acp/acp-driver.js.map +1 -1
- package/dist/runtime/acp/codex-provider-config.d.ts.map +1 -1
- package/dist/runtime/acp/codex-provider-config.js +4 -0
- package/dist/runtime/acp/codex-provider-config.js.map +1 -1
- package/dist/runtime/contracts.d.ts +12 -0
- package/dist/runtime/contracts.d.ts.map +1 -1
- package/dist/runtime/runtime-coordinator.d.ts +22 -1
- package/dist/runtime/runtime-coordinator.d.ts.map +1 -1
- package/dist/runtime/runtime-coordinator.js +214 -26
- package/dist/runtime/runtime-coordinator.js.map +1 -1
- package/dist/services/__tests__/session-manager.lifecycle.test.js +578 -68
- package/dist/services/__tests__/session-manager.lifecycle.test.js.map +1 -1
- package/dist/services/__tests__/session-manager.team-run.test.js +2 -1
- package/dist/services/__tests__/session-manager.team-run.test.js.map +1 -1
- package/dist/services/session-manager.d.ts +14 -8
- package/dist/services/session-manager.d.ts.map +1 -1
- package/dist/services/session-manager.js +359 -176
- package/dist/services/session-manager.js.map +1 -1
- package/dist/web/assets/{AgentDemoPage-aLGw8pYk.js → AgentDemoPage-04hKRdNk.js} +1 -1
- package/dist/web/assets/{AgentEnvironmentSettingsPage-BYcH_wMz.js → AgentEnvironmentSettingsPage-TnYB_gn_.js} +1 -1
- package/dist/web/assets/{AgentLogo-Dyqxcj5s.js → AgentLogo-BQ4AhPCM.js} +1 -1
- package/dist/web/assets/ConversationPage-GSAFcHuX.js +3 -0
- package/dist/web/assets/CreateTaskInput-DSl73Wwc.js +1 -0
- package/dist/web/assets/{DemoPage-m92AArTk.js → DemoPage-BKlcChAw.js} +1 -1
- package/dist/web/assets/{GeneralSettingsPage-P5HcwCXo.js → GeneralSettingsPage-sVFvK9Oe.js} +1 -1
- package/dist/web/assets/{Icons-BFOphLw-.js → Icons-DGahMuvC.js} +1 -1
- package/dist/web/assets/{LoadingPreviewPage-CfT4FIyb.js → LoadingPreviewPage-CZ2N3qZc.js} +1 -1
- package/dist/web/assets/{McpSettingsPage-BI_b78L_.js → McpSettingsPage-KZFzkxCs.js} +1 -1
- package/dist/web/assets/{MemberAvatar-1UlgMZMM.js → MemberAvatar-5bEbGww5.js} +1 -1
- package/dist/web/assets/{NotificationSettingsPage-CQCyq9QZ.js → NotificationSettingsPage-hVClKCOc.js} +1 -1
- package/dist/web/assets/{ProfileSettingsPage-ojhJXLWb.js → ProfileSettingsPage-BICdSvU4.js} +1 -1
- package/dist/web/assets/{ProjectKanbanPage-DK3rZJzY.js → ProjectKanbanPage-D62MacD4.js} +27 -27
- package/dist/web/assets/{ProjectSettingsPage-MGB631ia.js → ProjectSettingsPage-aMerK2A7.js} +1 -1
- package/dist/web/assets/{ProviderSettingsPage-CMZvEWMa.js → ProviderSettingsPage-Djd3iQfB.js} +1 -1
- package/dist/web/assets/{SettingsMasterDetail-9CnCBuNZ.js → SettingsMasterDetail-Czojx5vD.js} +1 -1
- package/dist/web/assets/{TeamSettingsPage-BUXKd5UF.js → TeamSettingsPage-B3-4MXXx.js} +1 -1
- package/dist/web/assets/{arc-2leUEhVa.js → arc-DzPiJSKL.js} +1 -1
- package/dist/web/assets/{architectureDiagram-3BPJPVTR-CDgYR44L.js → architectureDiagram-3BPJPVTR-EhKxHp4t.js} +1 -1
- package/dist/web/assets/{arrow-left-Jn-v17JF.js → arrow-left-2dTz1a0q.js} +1 -1
- package/dist/web/assets/{blockDiagram-GPEHLZMM-CzfIh31g.js → blockDiagram-GPEHLZMM-BHcWmA7M.js} +1 -1
- package/dist/web/assets/{c4Diagram-AAUBKEIU-BzzBXKEF.js → c4Diagram-AAUBKEIU-D59zNlIS.js} +1 -1
- package/dist/web/assets/channel-BpGJqMDu.js +1 -0
- package/dist/web/assets/check-CYQXIceH.js +1 -0
- package/dist/web/assets/{chevron-down-CpKODb7Y.js → chevron-down-BTGC8Sed.js} +1 -1
- package/dist/web/assets/{chevron-right-CBoUSXVq.js → chevron-right-BEw6jjbJ.js} +1 -1
- package/dist/web/assets/{chevron-up-KZ1hsL8y.js → chevron-up-D_Hmx6ug.js} +1 -1
- package/dist/web/assets/{chunk-2J33WTMH-GJE6LCGS.js → chunk-2J33WTMH-nk1Ieiq7.js} +1 -1
- package/dist/web/assets/{chunk-4BX2VUAB-Bw_ITp1Q.js → chunk-4BX2VUAB-xl3tQkpA.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-DQ-XuWbD.js → chunk-55IACEB6-Bj12k-M-.js} +1 -1
- package/dist/web/assets/{chunk-727SXJPM-BgL49Dtw.js → chunk-727SXJPM-DeL4hVho.js} +1 -1
- package/dist/web/assets/{chunk-AQP2D5EJ-dRTVohoy.js → chunk-AQP2D5EJ-xyt_hZ7N.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-BZtRjoH7.js → chunk-FMBD7UC4-DMpougEb.js} +1 -1
- package/dist/web/assets/{chunk-ND2GUHAM-CN3wND6y.js → chunk-ND2GUHAM-D2qjylxW.js} +1 -1
- package/dist/web/assets/{chunk-QZHKN3VN-CMwbobMj.js → chunk-QZHKN3VN-BjCtAePb.js} +1 -1
- package/dist/web/assets/{circle-alert-kELlE07n.js → circle-alert-BGbIBYEs.js} +1 -1
- package/dist/web/assets/classDiagram-4FO5ZUOK-M-OAZSzx.js +1 -0
- package/dist/web/assets/classDiagram-v2-Q7XG4LA2-M-OAZSzx.js +1 -0
- package/dist/web/assets/{code-block-OCS4YCEC-B1YC7dLa.js → code-block-OCS4YCEC-DnfSnuHM.js} +1 -1
- package/dist/web/assets/{confirm-dialog-DDWlvtJs.js → confirm-dialog-C8rPqdFE.js} +1 -1
- package/dist/web/assets/{cose-bilkent-S5V4N54A-C6xD7kmY.js → cose-bilkent-S5V4N54A-DD3H-OxN.js} +1 -1
- package/dist/web/assets/{cssMode-lB5vNGvS.js → cssMode-DR6Jf3-g.js} +1 -1
- package/dist/web/assets/{dagre-BM42HDAG-BqTCYMO6.js → dagre-BM42HDAG-Bw6aZrG9.js} +1 -1
- package/dist/web/assets/{diagram-2AECGRRQ-BsMq3s5l.js → diagram-2AECGRRQ-Bm8oLz0N.js} +1 -1
- package/dist/web/assets/{diagram-5GNKFQAL-l4izpv7Z.js → diagram-5GNKFQAL-CL9Xaz3k.js} +1 -1
- package/dist/web/assets/{diagram-KO2AKTUF-uWDq6Yzo.js → diagram-KO2AKTUF-KbdYV5XJ.js} +1 -1
- package/dist/web/assets/{diagram-LMA3HP47-Dbd4fnFa.js → diagram-LMA3HP47-CYo7CoGp.js} +1 -1
- package/dist/web/assets/{diagram-OG6HWLK6-BC-27mkr.js → diagram-OG6HWLK6-CFPSh1Sm.js} +1 -1
- package/dist/web/assets/{erDiagram-TEJ5UH35-DrCP0Xt8.js → erDiagram-TEJ5UH35-Dh9qfvJz.js} +1 -1
- package/dist/web/assets/{flowDiagram-I6XJVG4X-BqNsQuUr.js → flowDiagram-I6XJVG4X-8EMyvj0V.js} +1 -1
- package/dist/web/assets/{folder-picker-CBVEWiuS.js → folder-picker-iHGLWmJF.js} +1 -1
- package/dist/web/assets/{freemarker2-Bos_2jdw.js → freemarker2-Dhyh-DXo.js} +1 -1
- package/dist/web/assets/{ganttDiagram-6RSMTGT7-CZOrG-5M.js → ganttDiagram-6RSMTGT7-BE5FBwwf.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-PVQCEYII-Dp3i5ehs.js → gitGraphDiagram-PVQCEYII-Dx2x_Y6A.js} +1 -1
- package/dist/web/assets/{handlebars-C7Elz3nH.js → handlebars-BB4WaAAq.js} +1 -1
- package/dist/web/assets/{html-EvsHOk5n.js → html-BluMUCI1.js} +1 -1
- package/dist/web/assets/{htmlMode-CTlhNZgJ.js → htmlMode-Dk0meNHs.js} +1 -1
- package/dist/web/assets/{index-hPp73HMS.js → index-9oo7US_J.js} +4 -4
- package/dist/web/assets/{index-BGajgi5H.js → index-B0gHfTIK.js} +3 -3
- package/dist/web/assets/{infoDiagram-5YYISTIA-C6NnR_Zb.js → infoDiagram-5YYISTIA-0BPLGHMf.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-YF4QCWOH-C1SOf-Ko.js → ishikawaDiagram-YF4QCWOH-BnoV5etH.js} +1 -1
- package/dist/web/assets/javascript-Cwz9LBET.js +1 -0
- package/dist/web/assets/{journeyDiagram-JHISSGLW--GKMSbPz.js → journeyDiagram-JHISSGLW-D8_dQMoV.js} +1 -1
- package/dist/web/assets/{jsonMode-Bbh3Gbb8.js → jsonMode-DWAIY3Zm.js} +1 -1
- package/dist/web/assets/{kanban-definition-UN3LZRKU-Car5iy0R.js → kanban-definition-UN3LZRKU-CcP-UBWq.js} +1 -1
- package/dist/web/assets/{layers-WOwm63Fo.js → layers-B4Hb2k1E.js} +1 -1
- package/dist/web/assets/{linear-ChUj9SvT.js → linear-CFeIj_93.js} +1 -1
- package/dist/web/assets/{liquid-BXFWITFq.js → liquid-DszhNRew.js} +1 -1
- package/dist/web/assets/{lspLanguageFeatures-DQ2CrOBI.js → lspLanguageFeatures-DyajWL04.js} +1 -1
- package/dist/web/assets/{mdx-vHLpslzI.js → mdx-D3Dg3IU3.js} +1 -1
- package/dist/web/assets/{mermaid-NOHMQCX5-CzwPLFrI.js → mermaid-NOHMQCX5-De7jApSL.js} +4 -4
- package/dist/web/assets/{message-square-DgdFa1Kc.js → message-square-Dly_bein.js} +1 -1
- package/dist/web/assets/{mindmap-definition-RKZ34NQL-CfzbnHY_.js → mindmap-definition-RKZ34NQL-BBl0Q2SW.js} +1 -1
- package/dist/web/assets/{modal-QnCJhV9P.js → modal-CyT9BpRm.js} +1 -1
- package/dist/web/assets/{monaco-CbFQByO7.js → monaco-DBlH7Gvr.js} +3 -3
- package/dist/web/assets/{pencil-CNVU3zfM.js → pencil-BLlfV3fH.js} +1 -1
- package/dist/web/assets/{pieDiagram-4H26LBE5-CDqYJneU.js → pieDiagram-4H26LBE5-BVrsUXUt.js} +1 -1
- package/dist/web/assets/{python-BhFSY_J2.js → python-C1GF9JX6.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-W4KKPZXB-B6ChDIx2.js → quadrantDiagram-W4KKPZXB-Brnd7Z-x.js} +1 -1
- package/dist/web/assets/{razor-Cx_DrE-V.js → razor-CNwR1Ppb.js} +1 -1
- package/dist/web/assets/{requirementDiagram-4Y6WPE33-DefLT9NZ.js → requirementDiagram-4Y6WPE33-BEL5JUv5.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-5OEKKPKP-CQHnovPz.js → sankeyDiagram-5OEKKPKP-Dg3pq8MF.js} +1 -1
- package/dist/web/assets/{select-CmbeAAVh.js → select-BQuOHpgx.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-3UESZ5HK-DZuaKm7C.js → sequenceDiagram-3UESZ5HK-DvXxqwal.js} +1 -1
- package/dist/web/assets/{server-BUivqMYo.js → server-DmNZ4TcQ.js} +1 -1
- package/dist/web/assets/{stateDiagram-AJRCARHV-DvxpChEC.js → stateDiagram-AJRCARHV-BSXeM4wR.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-BHNVJYJU-Cjr-lS2a.js +1 -0
- package/dist/web/assets/{switch-CiOyM13e.js → switch-BDzVWx67.js} +1 -1
- package/dist/web/assets/{textarea-D4HkYa3u.js → textarea-D4CrProF.js} +1 -1
- package/dist/web/assets/{timeline-definition-PNZ67QCA-BRu-tS-p.js → timeline-definition-PNZ67QCA-3rH0QWp7.js} +1 -1
- package/dist/web/assets/{trash-2-DPxBnxJM.js → trash-2-DHu1aC-u.js} +1 -1
- package/dist/web/assets/{tsMode-CHcZwUmC.js → tsMode-Z5KzKNgl.js} +1 -1
- package/dist/web/assets/{typescript-CvtjMxoq.js → typescript-Cg8IFK7o.js} +1 -1
- package/dist/web/assets/{upload-YmLtnZsU.js → upload-DQPwfdPh.js} +1 -1
- package/dist/web/assets/{use-profiles-OJAwfc90.js → use-profiles-DI5-xqHU.js} +1 -1
- package/dist/web/assets/{use-projects-iob5C7Xk.js → use-projects-Cs0-FMed.js} +1 -1
- package/dist/web/assets/{use-providers-Cj9S9_2q.js → use-providers-BoVFTPYO.js} +1 -1
- package/dist/web/assets/{useNormalizedLogs-BhN4AN4f.js → useNormalizedLogs-DhoOmBJo.js} +1 -1
- package/dist/web/assets/{vennDiagram-CIIHVFJN-CtZ_3Ld0.js → vennDiagram-CIIHVFJN-DAjUhsg9.js} +1 -1
- package/dist/web/assets/{wardley-L42UT6IY-h9yfTmIn.js → wardley-L42UT6IY-Db8PSpCZ.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-YWT4CUSO-CWXtZtix.js → wardleyDiagram-YWT4CUSO-DfXfIOXM.js} +1 -1
- package/dist/web/assets/{xml-DhyGRwiG.js → xml-UOLWFxtD.js} +1 -1
- package/dist/web/assets/{xychartDiagram-2RQKCTM6-BLuWVWlL.js → xychartDiagram-2RQKCTM6-C41mtVgh.js} +1 -1
- package/dist/web/assets/{yaml-Cpi6SvoR.js → yaml-CeY5SySy.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +2 -2
- package/dist/web/assets/ConversationPage-BBnK1qA_.js +0 -3
- package/dist/web/assets/CreateTaskInput-B11wGhDW.js +0 -1
- package/dist/web/assets/channel-XuYe6lH7.js +0 -1
- package/dist/web/assets/check-FwR4dMNY.js +0 -1
- package/dist/web/assets/classDiagram-4FO5ZUOK-DWSNJHiT.js +0 -1
- package/dist/web/assets/classDiagram-v2-Q7XG4LA2-DWSNJHiT.js +0 -1
- package/dist/web/assets/javascript-Desq1P0G.js +0 -1
- package/dist/web/assets/stateDiagram-v2-BHNVJYJU-BbrRYMxK.js +0 -1
|
@@ -98,6 +98,13 @@ export class SessionManager {
|
|
|
98
98
|
*/
|
|
99
99
|
followUpReservations = new Map();
|
|
100
100
|
followUpReservationReleases = new Set();
|
|
101
|
+
/** Serializes user stop and direct follow-up actions for each Session. */
|
|
102
|
+
sessionActionReservations = new Map();
|
|
103
|
+
sessionActionReservationReleases = new Set();
|
|
104
|
+
/** Public initial starts are revocable before they enter RuntimeCoordinator admission. */
|
|
105
|
+
initialStartOperations = new Map();
|
|
106
|
+
/** A synchronous stop intent rejects starts registered while stop is still in flight. */
|
|
107
|
+
sessionStopIntentCounts = new Map();
|
|
101
108
|
/** Incremented for every start/send cycle so late post-processing cannot affect a new turn. */
|
|
102
109
|
sessionGenerations = new Map();
|
|
103
110
|
// 每个 session 上次写入 TeamRun 心跳时间戳的时刻,用于节流 lastHeartbeatAt 落库。
|
|
@@ -114,12 +121,6 @@ export class SessionManager {
|
|
|
114
121
|
/** Durable conversation turns are consumed one at a time per Session. */
|
|
115
122
|
conversationQueueWorkers = new Map();
|
|
116
123
|
conversationQueueStopping = false;
|
|
117
|
-
/** Admission boundary covering the short initial-start -> process-owning handoff. */
|
|
118
|
-
pendingInitialStarts = new Map();
|
|
119
|
-
initialStartResolvers = new Map();
|
|
120
|
-
/** Same boundary for follow-up/reply turns, which can race a direct stop. */
|
|
121
|
-
pendingProcessStarts = new Map();
|
|
122
|
-
processStartResolvers = new Map();
|
|
123
124
|
artifactService = new AgentArtifactService();
|
|
124
125
|
static SNAPSHOT_CHECKPOINT_MS = 15_000;
|
|
125
126
|
static HEARTBEAT_THROTTLE_MS = 30_000;
|
|
@@ -145,8 +146,37 @@ export class SessionManager {
|
|
|
145
146
|
onDriverSessionDisposed: (sessionId) => {
|
|
146
147
|
revokeAgentApiCredential(sessionId);
|
|
147
148
|
},
|
|
148
|
-
onDriverSessionDisposedInstance: (sessionId, runtimeInstanceId, launchClaimNumber) => {
|
|
149
|
-
|
|
149
|
+
onDriverSessionDisposedInstance: async (sessionId, runtimeInstanceId, launchClaimNumber) => {
|
|
150
|
+
await this.markRuntimeProcessCleanupState(sessionId, runtimeInstanceId, 'CONFIRMED', undefined, launchClaimNumber);
|
|
151
|
+
const resolvedClaim = Number.isInteger(launchClaimNumber) && launchClaimNumber > 0
|
|
152
|
+
? launchClaimNumber
|
|
153
|
+
: null;
|
|
154
|
+
const [process, launch] = resolvedClaim == null
|
|
155
|
+
? [null, null]
|
|
156
|
+
: await Promise.all([
|
|
157
|
+
prisma.executionProcess.findFirst({
|
|
158
|
+
where: { sessionId, runtimeInstanceId, launchClaimNumber: resolvedClaim },
|
|
159
|
+
select: { cleanupState: true },
|
|
160
|
+
}),
|
|
161
|
+
prisma.session.findUnique({
|
|
162
|
+
where: { id: sessionId },
|
|
163
|
+
select: {
|
|
164
|
+
runtimeLaunchState: true,
|
|
165
|
+
runtimeLaunchClaimCount: true,
|
|
166
|
+
runtimeLaunchResolvedCount: true,
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
]);
|
|
170
|
+
const processConfirmed = process?.cleanupState === 'CONFIRMED'
|
|
171
|
+
&& !this.hasRuntimeProcessOwner(runtimeInstanceId, sessionId, resolvedClaim);
|
|
172
|
+
const safelyNeverStarted = process == null
|
|
173
|
+
&& launch?.runtimeLaunchState === RUNTIME_LAUNCH_STATES.SAFE_PRE_CHILD_FAILURE
|
|
174
|
+
&& launch.runtimeLaunchClaimCount === resolvedClaim
|
|
175
|
+
&& launch.runtimeLaunchResolvedCount === resolvedClaim;
|
|
176
|
+
if (!processConfirmed
|
|
177
|
+
&& !safelyNeverStarted) {
|
|
178
|
+
throw new AgentRuntimeError('runtime_cleanup_pending', 'close', `Runtime process cleanup evidence for '${runtimeInstanceId}' is not durably confirmed`, true);
|
|
179
|
+
}
|
|
150
180
|
},
|
|
151
181
|
onDriverSessionDisposeFailed: (sessionId, runtimeInstanceId, error, launchClaimNumber) => {
|
|
152
182
|
this.logSessionError('session.runtimeDispose', error, { sessionId, runtimeInstanceId });
|
|
@@ -299,7 +329,10 @@ export class SessionManager {
|
|
|
299
329
|
}
|
|
300
330
|
try {
|
|
301
331
|
if (this.runtimeCoordinator.hasRuntimeInstance(input.sessionId, input.runtimeInstanceId)) {
|
|
302
|
-
await this.runtimeCoordinator.
|
|
332
|
+
const handled = await this.runtimeCoordinator.retryDisposedSessionCleanup(input.sessionId, input.runtimeInstanceId);
|
|
333
|
+
if (!handled && this.runtimeCoordinator.hasRuntimeInstance(input.sessionId, input.runtimeInstanceId)) {
|
|
334
|
+
await this.runtimeCoordinator.disposeSession(input.sessionId, input.runtimeInstanceId);
|
|
335
|
+
}
|
|
303
336
|
return;
|
|
304
337
|
}
|
|
305
338
|
await cleanupPersistedAcpProcessTree(input);
|
|
@@ -404,15 +437,16 @@ export class SessionManager {
|
|
|
404
437
|
}
|
|
405
438
|
async start(id) {
|
|
406
439
|
console.log('[SessionManager] 🚀 Starting session:', id);
|
|
407
|
-
const
|
|
408
|
-
if (!session) {
|
|
409
|
-
console.log('[SessionManager] ❌ Session not found:', id);
|
|
410
|
-
return null;
|
|
411
|
-
}
|
|
412
|
-
const initialStart = this.reserveInitialStart(id);
|
|
440
|
+
const startOperation = this.reserveInitialStartOperation(id);
|
|
413
441
|
try {
|
|
414
|
-
await this.
|
|
415
|
-
|
|
442
|
+
const session = await startOperation.waitFor(() => this.findSessionExecutionRecord(id));
|
|
443
|
+
startOperation.throwIfStopped();
|
|
444
|
+
if (!session) {
|
|
445
|
+
console.log('[SessionManager] ❌ Session not found:', id);
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
await startOperation.waitFor(() => this.waitForPendingAutoCommit(id));
|
|
449
|
+
startOperation.throwIfStopped();
|
|
416
450
|
this.ensureExecutionRecordIsLive(session);
|
|
417
451
|
const workingDir = this.getExecutionWorkingDir(session);
|
|
418
452
|
console.log('[SessionManager] Session details:', {
|
|
@@ -422,30 +456,36 @@ export class SessionManager {
|
|
|
422
456
|
prompt: summarizeTextForLog(session.prompt),
|
|
423
457
|
workingDir,
|
|
424
458
|
});
|
|
459
|
+
// No await separates this final operation check from Coordinator lease
|
|
460
|
+
// publication inside startRuntimeTurn(). A later stop is then observed by
|
|
461
|
+
// the runtime admission signal instead of being lost between the gates.
|
|
462
|
+
startOperation.throwIfStopped();
|
|
425
463
|
await this.startRuntimeTurn(session, session.prompt, session.externalSessionId, session.providerId, 'load', undefined, true);
|
|
426
464
|
return session;
|
|
427
465
|
}
|
|
428
466
|
finally {
|
|
429
|
-
|
|
467
|
+
startOperation.release();
|
|
430
468
|
}
|
|
431
469
|
}
|
|
432
470
|
async startFollowUp(id, resumeFromSessionId) {
|
|
433
471
|
console.log('[SessionManager] 🚀 Starting follow-up session:', id);
|
|
434
472
|
console.log('[SessionManager] Resume from Tower session:', resumeFromSessionId);
|
|
435
|
-
const
|
|
436
|
-
if (!session) {
|
|
437
|
-
console.log('[SessionManager] ❌ Session not found:', id);
|
|
438
|
-
return null;
|
|
439
|
-
}
|
|
440
|
-
const initialStart = this.reserveInitialStart(id);
|
|
473
|
+
const startOperation = this.reserveInitialStartOperation(id);
|
|
441
474
|
try {
|
|
442
|
-
await this.
|
|
443
|
-
|
|
475
|
+
const session = await startOperation.waitFor(() => this.findSessionExecutionRecord(id));
|
|
476
|
+
startOperation.throwIfStopped();
|
|
477
|
+
if (!session) {
|
|
478
|
+
console.log('[SessionManager] ❌ Session not found:', id);
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
await startOperation.waitFor(() => this.waitForPendingAutoCommit(id));
|
|
482
|
+
startOperation.throwIfStopped();
|
|
444
483
|
this.ensureExecutionRecordIsLive(session);
|
|
445
|
-
const resumeFromSession = await prisma.session.findUnique({
|
|
484
|
+
const resumeFromSession = await startOperation.waitFor(() => prisma.session.findUnique({
|
|
446
485
|
where: { id: resumeFromSessionId },
|
|
447
486
|
select: { logSnapshot: true, externalSessionId: true },
|
|
448
|
-
});
|
|
487
|
+
}));
|
|
488
|
+
startOperation.throwIfStopped();
|
|
449
489
|
const agentSessionId = resumeFromSession
|
|
450
490
|
? resumeFromSession.externalSessionId
|
|
451
491
|
?? this.resolveAgentSessionId(resumeFromSessionId, resumeFromSession.logSnapshot)
|
|
@@ -459,11 +499,12 @@ export class SessionManager {
|
|
|
459
499
|
prompt: summarizeTextForLog(session.prompt),
|
|
460
500
|
workingDir: this.getExecutionWorkingDir(session),
|
|
461
501
|
});
|
|
502
|
+
startOperation.throwIfStopped();
|
|
462
503
|
await this.startRuntimeTurn(session, session.prompt, agentSessionId, session.providerId, 'resume', undefined, true);
|
|
463
504
|
return session;
|
|
464
505
|
}
|
|
465
506
|
finally {
|
|
466
|
-
|
|
507
|
+
startOperation.release();
|
|
467
508
|
}
|
|
468
509
|
}
|
|
469
510
|
async sendMessage(id, message, providerId, expectedTeamRunInvocationId, options = {}) {
|
|
@@ -473,16 +514,12 @@ export class SessionManager {
|
|
|
473
514
|
console.log('[SessionManager] Switching provider to:', providerId);
|
|
474
515
|
}
|
|
475
516
|
const reservation = this.reserveFollowUp(id);
|
|
517
|
+
let actionReservation;
|
|
476
518
|
let releaseTeamRunAdmission;
|
|
477
519
|
try {
|
|
478
520
|
// Serialize concurrent follow-ups for the same session while retaining
|
|
479
521
|
// the reservation in the map so the old finalizer cannot reconcile.
|
|
480
522
|
await reservation.previous;
|
|
481
|
-
const session = await this.findSessionExecutionRecord(id);
|
|
482
|
-
if (!session) {
|
|
483
|
-
console.log('[SessionManager] ❌ Session not found:', id);
|
|
484
|
-
return null;
|
|
485
|
-
}
|
|
486
523
|
// TeamRun follow-ups are admission-controlled just like initial starts.
|
|
487
524
|
// A REST/MCP caller must carry the current invocation identity; browser
|
|
488
525
|
// or stale/terminal callers must never be able to resurrect a member.
|
|
@@ -501,7 +538,21 @@ export class SessionManager {
|
|
|
501
538
|
else if (expectedTeamRunInvocationId) {
|
|
502
539
|
await this.assertTeamRunDispatchAdmitted(id, expectedTeamRunInvocationId);
|
|
503
540
|
}
|
|
541
|
+
// TeamRun stop owns member admission before entering SessionManager.
|
|
542
|
+
// Preserve that lock order, then serialize the runtime mutation itself.
|
|
543
|
+
actionReservation = this.reserveSessionAction(id);
|
|
544
|
+
await actionReservation.previous;
|
|
545
|
+
const session = await this.findSessionExecutionRecord(id);
|
|
546
|
+
if (!session) {
|
|
547
|
+
console.log('[SessionManager] ❌ Session not found:', id);
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
504
550
|
this.ensureExecutionRecordIsLive(session);
|
|
551
|
+
// A failed explicit stop leaves the DISPOSED DriverSession attached so
|
|
552
|
+
// its process owner can be retried. Confirm that cleanup before clearing
|
|
553
|
+
// the terminal gate, advancing the generation, issuing a credential, or
|
|
554
|
+
// claiming another launch; otherwise a never-opened turn is quarantined.
|
|
555
|
+
await this.runtimeCoordinator.retryDisposedSessionCleanup(id);
|
|
505
556
|
const resumeMode = this.normalizeRuntimeType(session.runtimeType) === RuntimeType.ACP
|
|
506
557
|
&& hasCompletePersistedSnapshot(session)
|
|
507
558
|
? 'resume'
|
|
@@ -530,12 +581,6 @@ export class SessionManager {
|
|
|
530
581
|
});
|
|
531
582
|
console.log(`[SessionManager] ✅ Provider switched to: ${switchedProvider?.name ?? providerId}`);
|
|
532
583
|
}
|
|
533
|
-
// A rejected follow-up must not cancel the completed turn's post-exit
|
|
534
|
-
// work. Advance the generation only after the session/provider checks and
|
|
535
|
-
// any provider persistence have succeeded, immediately before replacing
|
|
536
|
-
// the old execution with the new one.
|
|
537
|
-
this.invalidateSessionGeneration(id);
|
|
538
|
-
this.beginSessionExecution(id);
|
|
539
584
|
// Stop the previous turn before waiting for its auto-commit boundary. The
|
|
540
585
|
// coordinator suppresses that superseded turn's terminal event so it cannot
|
|
541
586
|
// finalize the newly reserved generation.
|
|
@@ -556,30 +601,62 @@ export class SessionManager {
|
|
|
556
601
|
if (providerId && providerId !== session.providerId) {
|
|
557
602
|
await this.runtimeCoordinator.disposeSession(id);
|
|
558
603
|
}
|
|
559
|
-
|
|
604
|
+
await this.startRuntimeTurn(session, message, agentSessionId, effectiveProviderId, resumeMode, userEntryId, false, expectedTeamRunInvocationId, true);
|
|
605
|
+
return session;
|
|
606
|
+
}
|
|
607
|
+
finally {
|
|
608
|
+
releaseTeamRunAdmission?.();
|
|
609
|
+
reservation.release();
|
|
610
|
+
actionReservation?.release();
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
async stop(id, options = {}) {
|
|
614
|
+
// Publish stop intent synchronously, before the session-action queue can
|
|
615
|
+
// yield. Initial starts still doing DB/auto-commit preparation are revoked
|
|
616
|
+
// without making stop wait for those unrelated operations.
|
|
617
|
+
const releaseStopIntent = this.beginSessionStopIntent(id);
|
|
618
|
+
const actionReservation = this.reserveSessionAction(id);
|
|
619
|
+
let pendingFinalization;
|
|
620
|
+
let stoppedSession = null;
|
|
621
|
+
try {
|
|
560
622
|
try {
|
|
561
|
-
await
|
|
562
|
-
|
|
623
|
+
await actionReservation.previous;
|
|
624
|
+
const result = await this.stopWithinSessionAction(id, options);
|
|
625
|
+
stoppedSession = result.session;
|
|
626
|
+
pendingFinalization = result.pendingFinalization;
|
|
563
627
|
}
|
|
564
628
|
finally {
|
|
565
|
-
|
|
629
|
+
// A follow-up queued after stop may already be blocking the old terminal
|
|
630
|
+
// finalizer. Release the action boundary before waiting for that finalizer.
|
|
631
|
+
actionReservation.release();
|
|
566
632
|
}
|
|
633
|
+
await pendingFinalization;
|
|
634
|
+
return stoppedSession;
|
|
567
635
|
}
|
|
568
636
|
finally {
|
|
569
|
-
|
|
570
|
-
reservation.release();
|
|
637
|
+
releaseStopIntent();
|
|
571
638
|
}
|
|
572
639
|
}
|
|
573
|
-
async
|
|
640
|
+
async stopWithinSessionAction(id, options) {
|
|
574
641
|
const session = await prisma.session.findUnique({ where: { id } });
|
|
575
642
|
if (!session)
|
|
576
|
-
return null;
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
643
|
+
return { session: null };
|
|
644
|
+
const hasActiveTurn = this.runtimeCoordinator.hasActiveTurn(id);
|
|
645
|
+
const runtimeDisposal = this.normalizeRuntimeType(session.runtimeType) === RuntimeType.ACP && hasActiveTurn
|
|
646
|
+
? this.runtimeCoordinator.cancelAndDisposeSession(id)
|
|
647
|
+
: this.runtimeCoordinator.disposeSession(id);
|
|
648
|
+
// TeamRun reconciliation and snapshot persistence may run before cleanup is
|
|
649
|
+
// awaited. Observe an early rejection while retaining it for the boundary below.
|
|
650
|
+
void runtimeDisposal.catch(() => undefined);
|
|
651
|
+
if (hasActiveTurn && this.normalizeRuntimeType(session.runtimeType) !== RuntimeType.ACP) {
|
|
652
|
+
void this.runtimeCoordinator.cancelTurn(id).catch((error) => {
|
|
653
|
+
this.logSessionError('session.runtimeCancel', error, { sessionId: id });
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
// The disposal request above has cancelled any startup lease that can mint
|
|
657
|
+
// a DriverSession-bound credential. Revoke it before fallible persistence
|
|
658
|
+
// while the runtime owner remains available for a later cleanup retry.
|
|
659
|
+
revokeAgentApiCredential(id);
|
|
583
660
|
// Revoke TeamRun dispatch before waiting for runtime cleanup. A direct
|
|
584
661
|
// Session stop does not pass through TeamSchedulerService, so delaying this
|
|
585
662
|
// until cleanup succeeds would leave heartbeat/follow-up admission open.
|
|
@@ -587,7 +664,6 @@ export class SessionManager {
|
|
|
587
664
|
await this.teamReconciler.handleSessionStopped(id);
|
|
588
665
|
}
|
|
589
666
|
const terminalStatus = this.terminalSessions.get(id);
|
|
590
|
-
const hasActiveTurn = this.runtimeCoordinator.hasActiveTurn(id);
|
|
591
667
|
const persistedTerminal = [
|
|
592
668
|
SessionStatus.COMPLETED,
|
|
593
669
|
SessionStatus.FAILED,
|
|
@@ -599,44 +675,35 @@ export class SessionManager {
|
|
|
599
675
|
// clean up the PTY, but it must not regress the persisted status. The
|
|
600
676
|
// backing TeamRun invocation may still be waiting for a room reply, so
|
|
601
677
|
// it must still pass through the cancellation reconciler.
|
|
602
|
-
await
|
|
678
|
+
await runtimeDisposal;
|
|
603
679
|
if (!await this.isRuntimeCleanupConfirmed(id)) {
|
|
604
680
|
this.terminalSessions.delete(id);
|
|
605
|
-
return session;
|
|
681
|
+
return { session };
|
|
606
682
|
}
|
|
607
683
|
this.maybeClearTerminalState(id);
|
|
608
|
-
await pendingFinalization;
|
|
609
684
|
if (terminalStatus && !persistedTerminal) {
|
|
610
685
|
await prisma.session.updateMany({
|
|
611
686
|
where: { id, status: { notIn: [SessionStatus.COMPLETED, SessionStatus.FAILED, SessionStatus.CANCELLED] } },
|
|
612
687
|
data: { status: terminalStatus },
|
|
613
688
|
});
|
|
614
689
|
}
|
|
615
|
-
|
|
616
|
-
|
|
690
|
+
return {
|
|
691
|
+
session,
|
|
692
|
+
pendingFinalization: options.skipTeamRunReconcile ? undefined : pendingFinalization,
|
|
693
|
+
};
|
|
617
694
|
}
|
|
618
695
|
this.terminalSessions.set(id, SessionStatus.CANCELLED);
|
|
619
|
-
|
|
620
|
-
await this.runtimeCoordinator.abandonTurn(id).catch((error) => {
|
|
621
|
-
this.logSessionError('session.runtimeCancel', error, { sessionId: id });
|
|
622
|
-
return false;
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
else {
|
|
626
|
-
await this.runtimeCoordinator.cancelTurn(id).catch((error) => {
|
|
627
|
-
this.logSessionError('session.runtimeCancel', error, { sessionId: id });
|
|
628
|
-
});
|
|
629
|
-
}
|
|
696
|
+
await runtimeDisposal;
|
|
630
697
|
// Explicit stop revokes the DriverSession-bound workspace-service credential.
|
|
631
698
|
// A later follow-up reopens from the persisted external session id with a new credential.
|
|
632
699
|
// Do not report a stopped TeamRun member until the owned process tree has
|
|
633
700
|
// confirmed cleanup. The scheduler will retain the invocation and block
|
|
634
701
|
// the next admission when this rejects, allowing a later retry/recovery.
|
|
635
|
-
await this.runtimeCoordinator.disposeSession(id);
|
|
636
702
|
if (!await this.isRuntimeCleanupConfirmed(id)) {
|
|
637
703
|
this.terminalSessions.delete(id);
|
|
638
|
-
return session;
|
|
704
|
+
return { session };
|
|
639
705
|
}
|
|
706
|
+
await this.externalSessionPersistence.get(id);
|
|
640
707
|
const msgStore = sessionMsgStoreManager.get(id);
|
|
641
708
|
if (msgStore) {
|
|
642
709
|
msgStore.pushFinished();
|
|
@@ -662,8 +729,7 @@ export class SessionManager {
|
|
|
662
729
|
// store after the CANCELLED snapshot has been persisted above.
|
|
663
730
|
sessionMsgStoreManager.delete(id);
|
|
664
731
|
this.releaseSnapshotPersistenceState(id);
|
|
665
|
-
|
|
666
|
-
return session;
|
|
732
|
+
return { session };
|
|
667
733
|
}
|
|
668
734
|
/** @deprecated Use hasActiveTurn(). */
|
|
669
735
|
hasActivePipeline(sessionId) {
|
|
@@ -703,14 +769,6 @@ export class SessionManager {
|
|
|
703
769
|
async destroyAll() {
|
|
704
770
|
this.stopConversationQueue();
|
|
705
771
|
await this.runtimeCoordinator.destroyAll();
|
|
706
|
-
for (const resolve of this.initialStartResolvers.values())
|
|
707
|
-
resolve();
|
|
708
|
-
this.initialStartResolvers.clear();
|
|
709
|
-
this.pendingInitialStarts.clear();
|
|
710
|
-
for (const resolve of this.processStartResolvers.values())
|
|
711
|
-
resolve();
|
|
712
|
-
this.processStartResolvers.clear();
|
|
713
|
-
this.pendingProcessStarts.clear();
|
|
714
772
|
this.pendingRuntimeProcessEvents.clear();
|
|
715
773
|
for (const timer of this.pendingRuntimeEventExpiryTimers.values())
|
|
716
774
|
clearTimeout(timer);
|
|
@@ -726,6 +784,17 @@ export class SessionManager {
|
|
|
726
784
|
release();
|
|
727
785
|
this.followUpReservationReleases.clear();
|
|
728
786
|
this.followUpReservations.clear();
|
|
787
|
+
for (const release of [...this.sessionActionReservationReleases])
|
|
788
|
+
release();
|
|
789
|
+
this.sessionActionReservationReleases.clear();
|
|
790
|
+
this.sessionActionReservations.clear();
|
|
791
|
+
for (const operations of this.initialStartOperations.values()) {
|
|
792
|
+
for (const controller of operations) {
|
|
793
|
+
controller.abort(this.sessionStartStoppedError());
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
this.initialStartOperations.clear();
|
|
797
|
+
this.sessionStopIntentCounts.clear();
|
|
729
798
|
clearAgentApiCredentials();
|
|
730
799
|
}
|
|
731
800
|
kickConversationQueue(sessionId) {
|
|
@@ -868,80 +937,129 @@ export class SessionManager {
|
|
|
868
937
|
}
|
|
869
938
|
return null;
|
|
870
939
|
}
|
|
871
|
-
async startRuntimeTurn(session, prompt, resumeExternalSessionId, providerId = session.providerId, resumeMode = 'load', historyBoundaryEntryId, initialStart = false, expectedTeamRunInvocationId) {
|
|
940
|
+
async startRuntimeTurn(session, prompt, resumeExternalSessionId, providerId = session.providerId, resumeMode = 'load', historyBoundaryEntryId, initialStart = false, expectedTeamRunInvocationId, advanceGeneration = false) {
|
|
872
941
|
const workingDir = this.getExecutionWorkingDir(session);
|
|
873
|
-
const env = ExecutionEnv.default(workingDir);
|
|
874
|
-
if (providerId) {
|
|
875
|
-
const provider = getProviderById(providerId);
|
|
876
|
-
if (provider && Object.keys(provider.env).length > 0) {
|
|
877
|
-
env.merge(filterAgentSubprocessExternalEnv(provider.env));
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
if (!this.isConversationSession(session)) {
|
|
881
|
-
await this.injectTeamRunInvocationEnv(session.id, env);
|
|
882
|
-
}
|
|
883
|
-
this.injectAgentTowerMcpServiceEnv(session.id, env);
|
|
884
|
-
const isNewStore = !sessionMsgStoreManager.has(session.id);
|
|
885
|
-
const msgStore = sessionMsgStoreManager.getOrCreate(session.id);
|
|
886
|
-
if (isNewStore && session.logSnapshot) {
|
|
887
|
-
try {
|
|
888
|
-
msgStore.restoreFromSnapshot(JSON.parse(session.logSnapshot));
|
|
889
|
-
}
|
|
890
|
-
catch (error) {
|
|
891
|
-
this.logSessionError('session.snapshotRestore', error, { sessionId: session.id });
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
942
|
let launchClaimNumber = null;
|
|
943
|
+
let admissionEntered = false;
|
|
944
|
+
let reportedError;
|
|
895
945
|
try {
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
946
|
+
let handle;
|
|
947
|
+
await this.runtimeCoordinator.withStartAdmission(session.id, async (admission) => {
|
|
948
|
+
admissionEntered = true;
|
|
949
|
+
try {
|
|
950
|
+
this.assertPreChildAdmission(admission);
|
|
951
|
+
// A rejected follow-up must not supersede the completed generation
|
|
952
|
+
// until it owns the same disposal boundary as credential/claim setup.
|
|
953
|
+
if (advanceGeneration)
|
|
954
|
+
this.invalidateSessionGeneration(session.id);
|
|
955
|
+
this.beginSessionExecution(session.id);
|
|
956
|
+
const env = ExecutionEnv.default(workingDir);
|
|
957
|
+
if (providerId) {
|
|
958
|
+
const provider = getProviderById(providerId);
|
|
959
|
+
if (provider && Object.keys(provider.env).length > 0) {
|
|
960
|
+
env.merge(filterAgentSubprocessExternalEnv(provider.env));
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
if (!this.isConversationSession(session)) {
|
|
964
|
+
await this.injectTeamRunInvocationEnv(session.id, env);
|
|
965
|
+
this.assertPreChildAdmission(admission);
|
|
966
|
+
}
|
|
967
|
+
this.injectAgentTowerMcpServiceEnv(session.id, env);
|
|
968
|
+
this.assertPreChildAdmission(admission);
|
|
969
|
+
const isNewStore = !sessionMsgStoreManager.has(session.id);
|
|
970
|
+
const msgStore = sessionMsgStoreManager.getOrCreate(session.id);
|
|
971
|
+
if (isNewStore && session.logSnapshot) {
|
|
972
|
+
try {
|
|
973
|
+
msgStore.restoreFromSnapshot(JSON.parse(session.logSnapshot));
|
|
974
|
+
}
|
|
975
|
+
catch (error) {
|
|
976
|
+
this.logSessionError('session.snapshotRestore', error, { sessionId: session.id });
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
// Every turn claim is durable before RuntimeCoordinator can call a driver.
|
|
980
|
+
// The lease remains held until runTurn has either failed before spawn or
|
|
981
|
+
// handed the child/transport to its DriverSession owner.
|
|
982
|
+
launchClaimNumber = await this.claimRuntimeLaunch(session.id, initialStart);
|
|
983
|
+
this.assertPreChildAdmission(admission);
|
|
984
|
+
try {
|
|
985
|
+
await this.assertTeamRunDispatchAdmitted(session.id, expectedTeamRunInvocationId);
|
|
986
|
+
}
|
|
987
|
+
catch (error) {
|
|
988
|
+
throw markPreChildProcessFailure(error);
|
|
989
|
+
}
|
|
990
|
+
this.assertPreChildAdmission(admission);
|
|
991
|
+
const runtimePrompt = buildWorkspaceRuntimePrompt(session, prompt);
|
|
992
|
+
handle = await this.runtimeCoordinator.startTurn({
|
|
993
|
+
towerSessionId: session.id,
|
|
994
|
+
agentType: session.agentType,
|
|
995
|
+
runtimeType: this.normalizeRuntimeType(session.runtimeType),
|
|
996
|
+
variant: session.variant ?? 'DEFAULT',
|
|
997
|
+
providerId,
|
|
998
|
+
workingDir,
|
|
999
|
+
env,
|
|
1000
|
+
externalSessionId: session.externalSessionId,
|
|
1001
|
+
msgStore,
|
|
1002
|
+
prompt: session.purpose === SessionPurpose.CHAT
|
|
1003
|
+
? appendAgentOutputIntentInstructions(runtimePrompt)
|
|
1004
|
+
: runtimePrompt,
|
|
1005
|
+
resumeExternalSessionId,
|
|
1006
|
+
resumeMode,
|
|
1007
|
+
historyBoundaryEntryId,
|
|
1008
|
+
launchClaimNumber,
|
|
1009
|
+
admission,
|
|
1010
|
+
});
|
|
1011
|
+
await this.resolveReusedRuntimeLaunch(session.id, launchClaimNumber);
|
|
1012
|
+
this.eventBus.emit('session:started', { sessionId: session.id });
|
|
1013
|
+
}
|
|
1014
|
+
catch (error) {
|
|
1015
|
+
const preChildFailure = launchClaimNumber != null && (isPreChildProcessFailure(error)
|
|
1016
|
+
|| (error instanceof AgentRuntimeError && error.code === 'runtime_admission_cancelled'));
|
|
1017
|
+
let launchResolutionError;
|
|
1018
|
+
if (preChildFailure) {
|
|
1019
|
+
try {
|
|
1020
|
+
// Finish the durable no-child proof before disposal may close or
|
|
1021
|
+
// delete the managed session attached to this lease.
|
|
1022
|
+
await this.resolvePreChildRuntimeLaunchFailure(session.id, launchClaimNumber, error);
|
|
1023
|
+
}
|
|
1024
|
+
catch (persistError) {
|
|
1025
|
+
launchResolutionError = persistError;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
revokeAgentApiCredential(session.id);
|
|
1029
|
+
if (launchClaimNumber != null) {
|
|
1030
|
+
if (!preChildFailure) {
|
|
1031
|
+
await this.quarantineRuntimeLaunch(session.id, launchClaimNumber, error);
|
|
1032
|
+
}
|
|
1033
|
+
else if (launchResolutionError) {
|
|
1034
|
+
await this.quarantineRuntimeLaunch(session.id, launchClaimNumber, launchResolutionError);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
reportedError = launchResolutionError ?? error;
|
|
1038
|
+
throw reportedError;
|
|
1039
|
+
}
|
|
925
1040
|
});
|
|
926
|
-
await this.resolveReusedRuntimeLaunch(session.id, launchClaimNumber);
|
|
927
1041
|
// Terminal persistence is driven by Runtime turn events. Attach a catch
|
|
928
1042
|
// so the public start/message methods do not leave a rejected handle
|
|
929
1043
|
// unobserved after they have returned to the HTTP caller.
|
|
930
1044
|
void handle.completion.catch(() => undefined);
|
|
931
|
-
this.eventBus.emit('session:started', { sessionId: session.id });
|
|
932
1045
|
await this.checkTaskAutoRevert(session.id);
|
|
933
1046
|
}
|
|
934
1047
|
catch (error) {
|
|
1048
|
+
// Admission may fail while waiting for an older disposal. Do not turn
|
|
1049
|
+
// that retryable gate failure into a fresh disposal attempt.
|
|
1050
|
+
const finalError = reportedError ?? error;
|
|
1051
|
+
if (!admissionEntered) {
|
|
1052
|
+
this.logSessionError('session.runtimeAdmission', finalError, {
|
|
1053
|
+
sessionId: session.id,
|
|
1054
|
+
agentType: session.agentType,
|
|
1055
|
+
runtimeType: session.runtimeType,
|
|
1056
|
+
providerId,
|
|
1057
|
+
workingDir,
|
|
1058
|
+
});
|
|
1059
|
+
throw normalizeExecutorStartError(finalError);
|
|
1060
|
+
}
|
|
935
1061
|
await this.runtimeCoordinator.disposeSession(session.id).catch(() => undefined);
|
|
936
1062
|
revokeAgentApiCredential(session.id);
|
|
937
|
-
if (launchClaimNumber != null) {
|
|
938
|
-
if (isPreChildProcessFailure(error)) {
|
|
939
|
-
await this.resolvePreChildRuntimeLaunchFailure(session.id, launchClaimNumber, error);
|
|
940
|
-
}
|
|
941
|
-
else {
|
|
942
|
-
await this.quarantineRuntimeLaunch(session.id, launchClaimNumber, error);
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
1063
|
const cleanupConfirmed = await this.isRuntimeCleanupConfirmed(session.id).catch(() => false);
|
|
946
1064
|
if (cleanupConfirmed) {
|
|
947
1065
|
await prisma.session.update({
|
|
@@ -951,14 +1069,22 @@ export class SessionManager {
|
|
|
951
1069
|
}
|
|
952
1070
|
sessionMsgStoreManager.delete(session.id);
|
|
953
1071
|
this.releaseSnapshotPersistenceState(session.id);
|
|
954
|
-
this.logSessionError('session.runtimeStart',
|
|
1072
|
+
this.logSessionError('session.runtimeStart', finalError, {
|
|
955
1073
|
sessionId: session.id,
|
|
956
1074
|
agentType: session.agentType,
|
|
957
1075
|
runtimeType: session.runtimeType,
|
|
958
1076
|
providerId,
|
|
959
1077
|
workingDir,
|
|
960
1078
|
});
|
|
961
|
-
throw normalizeExecutorStartError(
|
|
1079
|
+
throw normalizeExecutorStartError(finalError);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
assertPreChildAdmission(admission) {
|
|
1083
|
+
try {
|
|
1084
|
+
admission.throwIfCancelled();
|
|
1085
|
+
}
|
|
1086
|
+
catch (error) {
|
|
1087
|
+
throw markPreChildProcessFailure(error);
|
|
962
1088
|
}
|
|
963
1089
|
}
|
|
964
1090
|
async claimRuntimeLaunch(sessionId, initialStart) {
|
|
@@ -1054,36 +1180,6 @@ export class SessionManager {
|
|
|
1054
1180
|
});
|
|
1055
1181
|
console.warn(`[SessionManager] Session ${sessionId} runtime launch quarantined: ${diagnostic}`);
|
|
1056
1182
|
}
|
|
1057
|
-
reserveInitialStart(sessionId) {
|
|
1058
|
-
let resolve;
|
|
1059
|
-
const promise = new Promise((done) => { resolve = done; });
|
|
1060
|
-
this.pendingInitialStarts.set(sessionId, promise);
|
|
1061
|
-
this.initialStartResolvers.set(sessionId, resolve);
|
|
1062
|
-
return {
|
|
1063
|
-
release: () => {
|
|
1064
|
-
if (this.pendingInitialStarts.get(sessionId) !== promise)
|
|
1065
|
-
return;
|
|
1066
|
-
this.pendingInitialStarts.delete(sessionId);
|
|
1067
|
-
this.initialStartResolvers.delete(sessionId);
|
|
1068
|
-
resolve();
|
|
1069
|
-
},
|
|
1070
|
-
};
|
|
1071
|
-
}
|
|
1072
|
-
reserveProcessStart(sessionId) {
|
|
1073
|
-
let resolve;
|
|
1074
|
-
const promise = new Promise((done) => { resolve = done; });
|
|
1075
|
-
this.pendingProcessStarts.set(sessionId, promise);
|
|
1076
|
-
this.processStartResolvers.set(sessionId, resolve);
|
|
1077
|
-
return {
|
|
1078
|
-
release: () => {
|
|
1079
|
-
if (this.pendingProcessStarts.get(sessionId) !== promise)
|
|
1080
|
-
return;
|
|
1081
|
-
this.pendingProcessStarts.delete(sessionId);
|
|
1082
|
-
this.processStartResolvers.delete(sessionId);
|
|
1083
|
-
resolve();
|
|
1084
|
-
},
|
|
1085
|
-
};
|
|
1086
|
-
}
|
|
1087
1183
|
async assertTeamRunDispatchAdmitted(sessionId, expectedInvocationId) {
|
|
1088
1184
|
const invocation = await prisma.agentInvocation.findFirst({
|
|
1089
1185
|
where: {
|
|
@@ -2230,6 +2326,93 @@ export class SessionManager {
|
|
|
2230
2326
|
this.followUpReservationReleases.add(release);
|
|
2231
2327
|
return { previous, release };
|
|
2232
2328
|
}
|
|
2329
|
+
reserveSessionAction(sessionId) {
|
|
2330
|
+
const previous = this.sessionActionReservations.get(sessionId) ?? Promise.resolve();
|
|
2331
|
+
let resolveReservation;
|
|
2332
|
+
const reservation = new Promise((resolve) => {
|
|
2333
|
+
resolveReservation = resolve;
|
|
2334
|
+
});
|
|
2335
|
+
let released = false;
|
|
2336
|
+
const release = () => {
|
|
2337
|
+
if (released)
|
|
2338
|
+
return;
|
|
2339
|
+
released = true;
|
|
2340
|
+
resolveReservation();
|
|
2341
|
+
this.sessionActionReservationReleases.delete(release);
|
|
2342
|
+
if (this.sessionActionReservations.get(sessionId) === reservation) {
|
|
2343
|
+
this.sessionActionReservations.delete(sessionId);
|
|
2344
|
+
}
|
|
2345
|
+
};
|
|
2346
|
+
this.sessionActionReservations.set(sessionId, reservation);
|
|
2347
|
+
this.sessionActionReservationReleases.add(release);
|
|
2348
|
+
return { previous, release };
|
|
2349
|
+
}
|
|
2350
|
+
reserveInitialStartOperation(sessionId) {
|
|
2351
|
+
const controller = new AbortController();
|
|
2352
|
+
const operations = this.initialStartOperations.get(sessionId) ?? new Set();
|
|
2353
|
+
operations.add(controller);
|
|
2354
|
+
this.initialStartOperations.set(sessionId, operations);
|
|
2355
|
+
if ((this.sessionStopIntentCounts.get(sessionId) ?? 0) > 0) {
|
|
2356
|
+
controller.abort(this.sessionStartStoppedError());
|
|
2357
|
+
}
|
|
2358
|
+
let released = false;
|
|
2359
|
+
return {
|
|
2360
|
+
waitFor: (operation) => this.waitForInitialStartOperation(operation, controller.signal),
|
|
2361
|
+
throwIfStopped: () => controller.signal.throwIfAborted(),
|
|
2362
|
+
release: () => {
|
|
2363
|
+
if (released)
|
|
2364
|
+
return;
|
|
2365
|
+
released = true;
|
|
2366
|
+
operations.delete(controller);
|
|
2367
|
+
if (operations.size === 0 && this.initialStartOperations.get(sessionId) === operations) {
|
|
2368
|
+
this.initialStartOperations.delete(sessionId);
|
|
2369
|
+
}
|
|
2370
|
+
},
|
|
2371
|
+
};
|
|
2372
|
+
}
|
|
2373
|
+
async waitForInitialStartOperation(operation, signal) {
|
|
2374
|
+
signal.throwIfAborted();
|
|
2375
|
+
let rejectStopped;
|
|
2376
|
+
const stopped = new Promise((_resolve, reject) => {
|
|
2377
|
+
rejectStopped = reject;
|
|
2378
|
+
});
|
|
2379
|
+
const onStop = () => rejectStopped(signal.reason ?? this.sessionStartStoppedError());
|
|
2380
|
+
signal.addEventListener('abort', onStop, { once: true });
|
|
2381
|
+
try {
|
|
2382
|
+
// Attach the race before starting fallible work. The second signal check
|
|
2383
|
+
// prevents a stop published during setup from launching the operation,
|
|
2384
|
+
// while the race continues to observe any operation that already started.
|
|
2385
|
+
const pending = Promise.resolve().then(() => {
|
|
2386
|
+
signal.throwIfAborted();
|
|
2387
|
+
return operation();
|
|
2388
|
+
});
|
|
2389
|
+
return await Promise.race([pending, stopped]);
|
|
2390
|
+
}
|
|
2391
|
+
finally {
|
|
2392
|
+
signal.removeEventListener('abort', onStop);
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
beginSessionStopIntent(sessionId) {
|
|
2396
|
+
this.sessionStopIntentCounts.set(sessionId, (this.sessionStopIntentCounts.get(sessionId) ?? 0) + 1);
|
|
2397
|
+
const error = this.sessionStartStoppedError();
|
|
2398
|
+
for (const controller of this.initialStartOperations.get(sessionId) ?? []) {
|
|
2399
|
+
controller.abort(error);
|
|
2400
|
+
}
|
|
2401
|
+
let released = false;
|
|
2402
|
+
return () => {
|
|
2403
|
+
if (released)
|
|
2404
|
+
return;
|
|
2405
|
+
released = true;
|
|
2406
|
+
const remaining = (this.sessionStopIntentCounts.get(sessionId) ?? 1) - 1;
|
|
2407
|
+
if (remaining > 0)
|
|
2408
|
+
this.sessionStopIntentCounts.set(sessionId, remaining);
|
|
2409
|
+
else
|
|
2410
|
+
this.sessionStopIntentCounts.delete(sessionId);
|
|
2411
|
+
};
|
|
2412
|
+
}
|
|
2413
|
+
sessionStartStoppedError() {
|
|
2414
|
+
return new ServiceError('Session start was stopped before runtime admission', 'SESSION_NOT_ADMITTED', 409);
|
|
2415
|
+
}
|
|
2233
2416
|
async waitForFollowUpReservation(sessionId) {
|
|
2234
2417
|
// Follow-ups can queue behind one another. Re-check after each reservation
|
|
2235
2418
|
// resolves so a finalizer never observes only an earlier queue item.
|