@myagentroam/node 0.10.62 → 0.10.64
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/connector.js +3 -1
- package/dist/database.js +4 -2
- package/dist/runtime-state.js +30 -4
- package/dist/service/conversation-hosting-lifecycle-service.js +29 -5
- package/dist/service/hosted-conversation-node-service.js +75 -18
- package/dist/service/native-session-projection-service.js +24 -24
- package/dist/service/session-catalog-service.js +67 -48
- package/dist/service/session-identity-service.js +37 -13
- package/dist/service/session-lifecycle-service.js +5 -2
- package/dist/service/workspace-session-service.js +11 -16
- package/package.json +4 -4
package/dist/connector.js
CHANGED
|
@@ -898,6 +898,9 @@ export class NodeConnector {
|
|
|
898
898
|
emitSession: (session) => this.publishWorkbenchEvent('session', {
|
|
899
899
|
session: this.sessionPresentationService.present(session)
|
|
900
900
|
}),
|
|
901
|
+
invalidateWorkspaceSessions: (workspaceId) => {
|
|
902
|
+
void this.workspaceWorkbenchService?.invalidateTopic(workspaceId, 'sessions');
|
|
903
|
+
},
|
|
901
904
|
execute: (operation, data) => this.modelUsage.withGrant(isPlainRecord(data) ? data.__usageGrant : undefined, () => this.executeNodeOperation(operation, data)),
|
|
902
905
|
interceptedSessionId: (clientMessageId) => this.fakeRunner === undefined ? undefined : `fake:${clientMessageId}`
|
|
903
906
|
});
|
|
@@ -998,7 +1001,6 @@ export class NodeConnector {
|
|
|
998
1001
|
discoveryPending: (workspaceId) => this.workspaceSessionService.discoveryPending(workspaceId),
|
|
999
1002
|
discoverWorkspace: (workspaceId) => this.nativeSessionProjectionService.discoverWorkspace(workspaceId),
|
|
1000
1003
|
present: (session) => this.sessionPresentationService.present(session),
|
|
1001
|
-
projectionForNative: (session) => this.nativeSessionProjectionService.projectionForNative(session),
|
|
1002
1004
|
nativeIds: (session) => this.nativeSessionProjectionService.nativeIds(session),
|
|
1003
1005
|
forget: (session, nativeIds) => {
|
|
1004
1006
|
this.nativeSessionProjectionService.forget(session, nativeIds);
|
package/dist/database.js
CHANGED
|
@@ -363,13 +363,14 @@ export class NodeDatabase {
|
|
|
363
363
|
if (current === undefined || current['pin_order'] === null)
|
|
364
364
|
throw new Error('SESSION_NOT_PINNED');
|
|
365
365
|
const order = numberValue(current, 'pin_order');
|
|
366
|
+
const workspaceId = stringValue(current, 'workspace_id');
|
|
366
367
|
const adjacent = this.raw
|
|
367
368
|
.prepare(direction === 'up'
|
|
368
369
|
? 'SELECT id, pin_order FROM agent_sessions WHERE workspace_id = ? AND pin_order < ? ORDER BY pin_order DESC LIMIT 1'
|
|
369
370
|
: 'SELECT id, pin_order FROM agent_sessions WHERE workspace_id = ? AND pin_order > ? ORDER BY pin_order ASC LIMIT 1')
|
|
370
|
-
.get(
|
|
371
|
+
.get(workspaceId, order);
|
|
371
372
|
if (adjacent === undefined)
|
|
372
|
-
return;
|
|
373
|
+
return workspaceId;
|
|
373
374
|
this.raw.exec('BEGIN IMMEDIATE');
|
|
374
375
|
try {
|
|
375
376
|
this.raw
|
|
@@ -384,6 +385,7 @@ export class NodeDatabase {
|
|
|
384
385
|
this.raw.exec('ROLLBACK');
|
|
385
386
|
throw error;
|
|
386
387
|
}
|
|
388
|
+
return workspaceId;
|
|
387
389
|
}
|
|
388
390
|
deleteSessionRecord(sessionId) {
|
|
389
391
|
this.raw.exec('BEGIN IMMEDIATE');
|
package/dist/runtime-state.js
CHANGED
|
@@ -157,15 +157,36 @@ export class NodeRuntimeState {
|
|
|
157
157
|
this.emitRunChanged(run, session);
|
|
158
158
|
return run;
|
|
159
159
|
}
|
|
160
|
+
getAgentSessionById(id) {
|
|
161
|
+
return this.sessions.get(this.resolvePromotedSessionId(id));
|
|
162
|
+
}
|
|
160
163
|
getAgentSession(id) {
|
|
161
|
-
return (this.
|
|
164
|
+
return (this.getAgentSessionById(id) ??
|
|
162
165
|
[...this.sessions.values()].find((value) => value.externalSessionId === id));
|
|
163
166
|
}
|
|
164
167
|
listAgentSessions() {
|
|
165
168
|
return [...this.sessions.values()].sort((a, b) => b.lastActivityAt - a.lastActivityAt);
|
|
166
169
|
}
|
|
167
|
-
findAgentSessionByExternalSession(runner, externalSessionId) {
|
|
168
|
-
return [...this.sessions.values()].find((value) => value.runner === runner &&
|
|
170
|
+
findAgentSessionByExternalSession(runner, externalSessionId, workspaceId) {
|
|
171
|
+
return [...this.sessions.values()].find((value) => value.runner === runner &&
|
|
172
|
+
value.externalSessionId === externalSessionId &&
|
|
173
|
+
(workspaceId === undefined || value.workspaceId === workspaceId));
|
|
174
|
+
}
|
|
175
|
+
/** Request-local identity lookups; callers must consume them synchronously. */
|
|
176
|
+
createSessionLookup() {
|
|
177
|
+
const byId = new Map(this.sessions);
|
|
178
|
+
const byNative = new Map();
|
|
179
|
+
for (const session of byId.values()) {
|
|
180
|
+
if (session.externalSessionId === null)
|
|
181
|
+
continue;
|
|
182
|
+
const key = JSON.stringify([session.runner, session.workspaceId, session.externalSessionId]);
|
|
183
|
+
if (!byNative.has(key))
|
|
184
|
+
byNative.set(key, session);
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
getAgentSessionById: (id) => byId.get(this.resolvePromotedSessionId(id)),
|
|
188
|
+
findAgentSessionByExternalSession: (runner, externalId, workspaceId) => byNative.get(JSON.stringify([runner, workspaceId, externalId]))
|
|
189
|
+
};
|
|
169
190
|
}
|
|
170
191
|
updateAgentSessionConfiguration(input) {
|
|
171
192
|
return this.replaceSession(input.sessionId, {
|
|
@@ -826,8 +847,13 @@ export class NodeRuntimeState {
|
|
|
826
847
|
};
|
|
827
848
|
}
|
|
828
849
|
conversationTurnForRun(runId) {
|
|
850
|
+
const canonicalRunId = this.canonicalRunId(runId);
|
|
851
|
+
const run = this.runs.get(canonicalRunId);
|
|
852
|
+
if (run !== undefined)
|
|
853
|
+
return (this.turns.get(run.sessionId) ?? []).findLast((turn) => turn.runId === canonicalRunId);
|
|
854
|
+
// Terminal Run cleanup may retain Turns after removing the Run.
|
|
829
855
|
for (const turns of this.turns.values()) {
|
|
830
|
-
const turn = turns.findLast((candidate) => candidate.runId ===
|
|
856
|
+
const turn = turns.findLast((candidate) => candidate.runId === canonicalRunId);
|
|
831
857
|
if (turn !== undefined)
|
|
832
858
|
return turn;
|
|
833
859
|
}
|
|
@@ -59,8 +59,12 @@ export class ConversationHostingLifecycleService {
|
|
|
59
59
|
await resources.maintenance.dispose();
|
|
60
60
|
}
|
|
61
61
|
finally {
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
try {
|
|
63
|
+
await resources.service.dispose();
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
await resources.runtime.dispose().catch(() => undefined);
|
|
67
|
+
}
|
|
64
68
|
}
|
|
65
69
|
}
|
|
66
70
|
async rebuild() {
|
|
@@ -159,9 +163,29 @@ export class ConversationHostingLifecycleService {
|
|
|
159
163
|
};
|
|
160
164
|
}
|
|
161
165
|
catch (error) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
166
|
+
try {
|
|
167
|
+
await maintenanceService?.dispose();
|
|
168
|
+
}
|
|
169
|
+
catch (cleanupError) {
|
|
170
|
+
nodeLog('conversation.hosting.cleanup.failed', {
|
|
171
|
+
nodeId: config.nodeId,
|
|
172
|
+
code: safeErrorCode(cleanupError, 'CONVERSATION_HOSTING_CLEANUP_FAILED')
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
try {
|
|
177
|
+
await hostedService?.dispose();
|
|
178
|
+
}
|
|
179
|
+
catch (cleanupError) {
|
|
180
|
+
nodeLog('conversation.hosting.cleanup.failed', {
|
|
181
|
+
nodeId: config.nodeId,
|
|
182
|
+
code: safeErrorCode(cleanupError, 'CONVERSATION_HOSTING_CLEANUP_FAILED')
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
await runtime?.dispose().catch(() => undefined);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
165
189
|
if (this.closed)
|
|
166
190
|
return;
|
|
167
191
|
this.failureCode = safeErrorCode(error, 'CONVERSATION_HOSTING_RECOVERY_FAILED');
|
|
@@ -45,6 +45,8 @@ export class HostedConversationNodeService {
|
|
|
45
45
|
pendingUploadCreates = new Map();
|
|
46
46
|
uploadNameReservations = new Map();
|
|
47
47
|
fileDeletes = new Set();
|
|
48
|
+
fileWrites = new Map();
|
|
49
|
+
closing;
|
|
48
50
|
watchLeases = new Map();
|
|
49
51
|
cleanupLocks = new Set();
|
|
50
52
|
historyMutations = new Map();
|
|
@@ -76,13 +78,13 @@ export class HostedConversationNodeService {
|
|
|
76
78
|
'conversation.hosted.tool-resource.read': (raw) => this.readToolResource(raw),
|
|
77
79
|
'conversation.hosted.files.list': (raw) => this.listFiles(raw),
|
|
78
80
|
'conversation.hosted.file.read': (raw) => this.readFile(raw),
|
|
79
|
-
'conversation.hosted.file.delete': (raw) => this.deleteFile(raw),
|
|
81
|
+
'conversation.hosted.file.delete': (raw) => this.fileWrite(raw, hostedConversationFileDeleteInputSchema, () => this.deleteFile(raw)),
|
|
80
82
|
'conversation.hosted.files.upload.preflight': (raw) => this.preflightUpload(raw),
|
|
81
|
-
'conversation.hosted.files.upload.create': (raw) => this.createUpload(raw),
|
|
82
|
-
'conversation.hosted.files.upload.chunk': (raw) => this.writeUploadChunk(raw),
|
|
83
|
+
'conversation.hosted.files.upload.create': (raw) => this.fileWrite(raw, hostedConversationUploadCreateInputSchema, () => this.createUpload(raw)),
|
|
84
|
+
'conversation.hosted.files.upload.chunk': (raw) => this.fileWrite(raw, hostedConversationUploadChunkInputSchema, () => this.writeUploadChunk(raw)),
|
|
83
85
|
'conversation.hosted.files.upload.status': (raw) => this.uploadStatus(raw),
|
|
84
|
-
'conversation.hosted.files.upload.complete': (raw) => this.completeUpload(raw),
|
|
85
|
-
'conversation.hosted.files.upload.cancel': (raw) => this.cancelUpload(raw),
|
|
86
|
+
'conversation.hosted.files.upload.complete': (raw) => this.fileWrite(raw, hostedConversationUploadControlInputSchema, () => this.completeUpload(raw)),
|
|
87
|
+
'conversation.hosted.files.upload.cancel': (raw) => this.fileWrite(raw, hostedConversationUploadControlInputSchema, () => this.cancelUpload(raw)),
|
|
86
88
|
'conversation.hosted.delete': (raw) => this.delete(raw)
|
|
87
89
|
};
|
|
88
90
|
}
|
|
@@ -293,6 +295,8 @@ export class HostedConversationNodeService {
|
|
|
293
295
|
});
|
|
294
296
|
}
|
|
295
297
|
async watch(raw) {
|
|
298
|
+
if (this.closing)
|
|
299
|
+
throw new Error('CONVERSATION_HOSTING_UNAVAILABLE');
|
|
296
300
|
const payload = hostedRequest(raw);
|
|
297
301
|
const input = hostedConversationWatchInputSchema.parse(payload.data);
|
|
298
302
|
const conversation = this.options.store.requireConversation(payload.userId, input.conversationId);
|
|
@@ -306,8 +310,10 @@ export class HostedConversationNodeService {
|
|
|
306
310
|
!this.sessionWatchSlots.owns(ownerKey, lease.slotToken) ||
|
|
307
311
|
lease.watchId !== input.watchId ||
|
|
308
312
|
lease.conversationId !== input.conversationId ||
|
|
309
|
-
lease.userId !== payload.userId
|
|
310
|
-
|
|
313
|
+
lease.userId !== payload.userId) {
|
|
314
|
+
throw new Error('SESSION_WATCH_NOT_FOUND');
|
|
315
|
+
}
|
|
316
|
+
if (lease.expiresAt <= Date.now()) {
|
|
311
317
|
this.removeWatchLease(ownerKey);
|
|
312
318
|
throw new Error('SESSION_WATCH_NOT_FOUND');
|
|
313
319
|
}
|
|
@@ -376,6 +382,7 @@ export class HostedConversationNodeService {
|
|
|
376
382
|
this.historyMutations.has(conversationId) ||
|
|
377
383
|
this.activeRuns.has(conversationId) ||
|
|
378
384
|
this.runSettlements.has(conversationId) ||
|
|
385
|
+
this.fileWrites.has(conversationId) ||
|
|
379
386
|
[...this.watchLeases.values()].some((lease) => lease.conversationId === conversationId) ||
|
|
380
387
|
[...this.uploads.values()].some((upload) => upload.conversationId === conversationId && upload.status === 'UPLOADING') ||
|
|
381
388
|
[...this.pendingMessages.keys()].some((key) => key.startsWith(`${conversationId}\0`)));
|
|
@@ -399,6 +406,9 @@ export class HostedConversationNodeService {
|
|
|
399
406
|
.catch(() => undefined);
|
|
400
407
|
}
|
|
401
408
|
dispose() {
|
|
409
|
+
if (this.closing)
|
|
410
|
+
return this.closing;
|
|
411
|
+
this.disablePrepared = true;
|
|
402
412
|
for (const [conversationId, handle] of this.activeRuns) {
|
|
403
413
|
const interruptionKey = runInterruptionKey(conversationId, handle.run.id);
|
|
404
414
|
this.expectedInterruptions.add(interruptionKey);
|
|
@@ -407,6 +417,50 @@ export class HostedConversationNodeService {
|
|
|
407
417
|
}
|
|
408
418
|
for (const ownerKey of [...this.watchLeases.keys()])
|
|
409
419
|
this.removeWatchLease(ownerKey);
|
|
420
|
+
this.closing = (async () => {
|
|
421
|
+
await Promise.all([...this.fileWrites.keys()].map((id) => this.waitForFileWrites(id)));
|
|
422
|
+
await Promise.allSettled([...this.uploads.values()].map(async (upload) => {
|
|
423
|
+
if (upload.status === 'UPLOADING') {
|
|
424
|
+
upload.status = 'CANCELLED';
|
|
425
|
+
await rm(upload.temporaryPath, { force: true });
|
|
426
|
+
}
|
|
427
|
+
}));
|
|
428
|
+
this.uploads.clear();
|
|
429
|
+
this.uploadsByRequest.clear();
|
|
430
|
+
this.uploadNameReservations.clear();
|
|
431
|
+
})();
|
|
432
|
+
return this.closing;
|
|
433
|
+
}
|
|
434
|
+
async fileWrite(raw, schema, operation) {
|
|
435
|
+
const payload = hostedRequest(raw);
|
|
436
|
+
const input = schema.parse(payload.data);
|
|
437
|
+
if (this.closing)
|
|
438
|
+
throw new Error('CONVERSATION_HOSTING_UNAVAILABLE');
|
|
439
|
+
this.requireWritableConversation(payload.userId, input.conversationId);
|
|
440
|
+
const writes = this.fileWrites.get(input.conversationId) ?? new Set();
|
|
441
|
+
if (input.uploadId !== undefined &&
|
|
442
|
+
[...writes].some((write) => write.uploadId === input.uploadId))
|
|
443
|
+
throw new Error('HOSTED_CONVERSATION_FILE_CONFLICT');
|
|
444
|
+
// Register before the operation can enter its first asynchronous preparation.
|
|
445
|
+
const write = {
|
|
446
|
+
...(input.uploadId === undefined ? {} : { uploadId: input.uploadId }),
|
|
447
|
+
completion: Promise.resolve().then(operation)
|
|
448
|
+
};
|
|
449
|
+
writes.add(write);
|
|
450
|
+
this.fileWrites.set(input.conversationId, writes);
|
|
451
|
+
try {
|
|
452
|
+
return await write.completion;
|
|
453
|
+
}
|
|
454
|
+
finally {
|
|
455
|
+
writes.delete(write);
|
|
456
|
+
if (writes.size === 0)
|
|
457
|
+
this.fileWrites.delete(input.conversationId);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
async waitForFileWrites(conversationId) {
|
|
461
|
+
const writes = this.fileWrites.get(conversationId);
|
|
462
|
+
if (writes)
|
|
463
|
+
await Promise.allSettled([...writes].map((write) => write.completion));
|
|
410
464
|
}
|
|
411
465
|
message(raw) {
|
|
412
466
|
if (this.disablePrepared)
|
|
@@ -778,17 +832,6 @@ export class HostedConversationNodeService {
|
|
|
778
832
|
let conversation;
|
|
779
833
|
try {
|
|
780
834
|
this.options.store.finishRunMessages(conversationId, run.id, completion.status, completion.completedAt);
|
|
781
|
-
this.publishHostedEvent(conversationId, {
|
|
782
|
-
conversationId,
|
|
783
|
-
kind: 'run',
|
|
784
|
-
run: {
|
|
785
|
-
id: run.id,
|
|
786
|
-
status: completion.status,
|
|
787
|
-
startedAt: run.startedAt,
|
|
788
|
-
completedAt: completion.completedAt
|
|
789
|
-
}
|
|
790
|
-
});
|
|
791
|
-
void this.refreshReadyWatchSnapshots(conversationId, run.id);
|
|
792
835
|
try {
|
|
793
836
|
const files = await this.options.store.reconcileFiles(userId, conversationId);
|
|
794
837
|
this.publishHostedEvent(conversationId, {
|
|
@@ -809,6 +852,17 @@ export class HostedConversationNodeService {
|
|
|
809
852
|
}
|
|
810
853
|
});
|
|
811
854
|
}
|
|
855
|
+
this.publishHostedEvent(conversationId, {
|
|
856
|
+
conversationId,
|
|
857
|
+
kind: 'run',
|
|
858
|
+
run: {
|
|
859
|
+
id: run.id,
|
|
860
|
+
status: completion.status,
|
|
861
|
+
startedAt: run.startedAt,
|
|
862
|
+
completedAt: completion.completedAt
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
void this.refreshReadyWatchSnapshots(conversationId, run.id);
|
|
812
866
|
await this.options.store.advanceLastActivity(conversationId, completion.completedAt);
|
|
813
867
|
await this.applyAutomaticTitle(conversationId, completion.title, completion.completedAt).catch(() => undefined);
|
|
814
868
|
conversation = this.options.store.requireConversation(userId, conversationId);
|
|
@@ -1195,6 +1249,8 @@ export class HostedConversationNodeService {
|
|
|
1195
1249
|
return { upload: hostedConversationUploadSchema.parse(uploadProjection(upload)) };
|
|
1196
1250
|
}
|
|
1197
1251
|
async delete(raw) {
|
|
1252
|
+
if (this.closing)
|
|
1253
|
+
throw new Error('CONVERSATION_HOSTING_UNAVAILABLE');
|
|
1198
1254
|
const payload = hostedRequest(raw);
|
|
1199
1255
|
const input = hostedConversationIdInputSchema.parse(payload.data);
|
|
1200
1256
|
const conversation = this.options.store.requireConversation(payload.userId, input.conversationId);
|
|
@@ -1210,6 +1266,7 @@ export class HostedConversationNodeService {
|
|
|
1210
1266
|
if (!handle.cancel())
|
|
1211
1267
|
this.expectedInterruptions.delete(interruptionKey);
|
|
1212
1268
|
}
|
|
1269
|
+
await this.waitForFileWrites(conversation.id);
|
|
1213
1270
|
await this.cancelConversationUploads(payload.userId, conversation.id);
|
|
1214
1271
|
const paths = this.options.store.pathsForConversation(payload.userId, conversation.id);
|
|
1215
1272
|
await this.options.runtime
|
|
@@ -254,31 +254,42 @@ export class NativeSessionProjectionService {
|
|
|
254
254
|
? undefined
|
|
255
255
|
: [
|
|
256
256
|
'Similar Sessions (suggestions only; no action was redirected):',
|
|
257
|
-
...candidates.map(({ id, title }) =>
|
|
257
|
+
...candidates.map(({ id, title }) =>
|
|
258
|
+
// eslint-disable-next-line no-control-regex -- Strip ASCII controls from user titles.
|
|
259
|
+
id + ' ' + title.replace(/[\u0000-\u001f\u007f]/gu, ' ').slice(0, 160))
|
|
258
260
|
].join('\n');
|
|
259
261
|
}
|
|
260
262
|
projectionForNative(native) {
|
|
263
|
+
return this.resolveNativeProjection(native, this.options.runtime, false);
|
|
264
|
+
}
|
|
265
|
+
projectionsForNative(native) {
|
|
266
|
+
const lookup = this.options.runtime.createSessionLookup();
|
|
267
|
+
return new Map(native.map((session) => [session, this.resolveNativeProjection(session, lookup, false)]));
|
|
268
|
+
}
|
|
269
|
+
managedForNative(native) {
|
|
270
|
+
return this.resolveNativeProjection(native, this.options.runtime, true);
|
|
271
|
+
}
|
|
272
|
+
resolveNativeProjection(native, lookup, managedOnly) {
|
|
261
273
|
if (native.externalSessionId === null)
|
|
262
274
|
return undefined;
|
|
263
|
-
const exact =
|
|
275
|
+
const exact = lookup.findAgentSessionByExternalSession(native.runner, native.externalSessionId, native.workspaceId);
|
|
264
276
|
if (exact !== undefined &&
|
|
265
277
|
exact.runner === native.runner &&
|
|
266
278
|
exact.workspaceId === native.workspaceId &&
|
|
267
|
-
exact.externalSessionId === native.externalSessionId
|
|
268
|
-
|
|
269
|
-
return this.managedForNative(native);
|
|
270
|
-
}
|
|
271
|
-
managedForNative(native) {
|
|
272
|
-
if (native.externalSessionId === null)
|
|
273
|
-
return undefined;
|
|
274
|
-
const exact = this.options.runtime.findAgentSessionByExternalSession(native.runner, native.externalSessionId);
|
|
275
|
-
if (this.isManagedProjection(exact, native, false))
|
|
279
|
+
exact.externalSessionId === native.externalSessionId &&
|
|
280
|
+
(!managedOnly || exact.nativeControl === 'MAR_MANAGED'))
|
|
276
281
|
return exact;
|
|
277
282
|
const aliasId = this.options.runners
|
|
278
283
|
.require(native.runner)
|
|
279
284
|
.managedAliasId(native.externalSessionId);
|
|
280
|
-
const alias = aliasId === undefined ? undefined :
|
|
281
|
-
|
|
285
|
+
const alias = aliasId === undefined ? undefined : lookup.getAgentSessionById(aliasId);
|
|
286
|
+
// The Runner supplied the verified alias; Runtime may have promoted its old ID.
|
|
287
|
+
return alias !== undefined &&
|
|
288
|
+
alias.nativeControl === 'MAR_MANAGED' &&
|
|
289
|
+
alias.runner === native.runner &&
|
|
290
|
+
alias.workspaceId === native.workspaceId
|
|
291
|
+
? alias
|
|
292
|
+
: undefined;
|
|
282
293
|
}
|
|
283
294
|
nativeIds(session) {
|
|
284
295
|
const ids = new Set();
|
|
@@ -331,17 +342,6 @@ export class NativeSessionProjectionService {
|
|
|
331
342
|
this.options.state.externalActivity.delete(projectionId);
|
|
332
343
|
}
|
|
333
344
|
}
|
|
334
|
-
isManagedProjection(candidate, native, allowAlias) {
|
|
335
|
-
const exactNativeId = candidate?.externalSessionId === native.externalSessionId;
|
|
336
|
-
const verifiedAlias = allowAlias &&
|
|
337
|
-
this.options.runners.require(native.runner).managedAliasId(native.externalSessionId ?? '') ===
|
|
338
|
-
candidate?.id;
|
|
339
|
-
return (candidate !== undefined &&
|
|
340
|
-
candidate.nativeControl === 'MAR_MANAGED' &&
|
|
341
|
-
candidate.runner === native.runner &&
|
|
342
|
-
candidate.workspaceId === native.workspaceId &&
|
|
343
|
-
(exactNativeId || verifiedAlias));
|
|
344
|
-
}
|
|
345
345
|
requireDatabase() {
|
|
346
346
|
const database = this.options.database();
|
|
347
347
|
if (database === undefined)
|
|
@@ -84,13 +84,24 @@ export class SessionCatalogService {
|
|
|
84
84
|
: query === undefined
|
|
85
85
|
? await this.options.cachedWorkspace(workspaceId)
|
|
86
86
|
: await this.options.discoverWorkspace(workspaceId);
|
|
87
|
-
const
|
|
87
|
+
const inWorkspace = (session) => workspaceId === undefined || session.workspaceId === workspaceId;
|
|
88
|
+
const native = nativeSessions
|
|
89
|
+
.filter(inWorkspace)
|
|
90
|
+
.map((session) => this.options.present({ ...session, externalDiscovered: true }));
|
|
91
|
+
const projections = this.options.projection.projectionsForNative(native);
|
|
92
|
+
const sources = new Map();
|
|
93
|
+
for (const candidate of native) {
|
|
94
|
+
const projection = projections.get(candidate);
|
|
95
|
+
if (projection !== undefined && !sources.has(projection.id))
|
|
96
|
+
sources.set(projection.id, candidate);
|
|
97
|
+
}
|
|
88
98
|
const projected = this.options.runtime
|
|
89
99
|
.listAgentSessions()
|
|
100
|
+
.filter(inWorkspace)
|
|
90
101
|
.filter((session) => session.nativeControl === 'MAR_MANAGED' ||
|
|
91
102
|
(session.nativeControl === 'EXTERNAL' && session.externalSessionId !== null))
|
|
92
103
|
.map((session) => {
|
|
93
|
-
const source =
|
|
104
|
+
const source = sources.get(session.id);
|
|
94
105
|
const synchronized = session.titleSource !== 'AUTO' ||
|
|
95
106
|
source?.runnerTitle === undefined ||
|
|
96
107
|
source.runnerTitle === null
|
|
@@ -110,8 +121,7 @@ export class SessionCatalogService {
|
|
|
110
121
|
});
|
|
111
122
|
const merged = new Map([
|
|
112
123
|
...projected,
|
|
113
|
-
...native.filter((candidate) => candidate.externalSessionId === null ||
|
|
114
|
-
this.options.projectionForNative(candidate) === undefined)
|
|
124
|
+
...native.filter((candidate) => candidate.externalSessionId === null || projections.get(candidate) === undefined)
|
|
115
125
|
].map((session) => [session.id, session]));
|
|
116
126
|
if (workspaceId !== undefined)
|
|
117
127
|
this.mergePersisted(merged, workspaceId, !registeredOnly && query === undefined && this.options.discoveryPending(workspaceId));
|
|
@@ -268,8 +278,13 @@ export class SessionCatalogService {
|
|
|
268
278
|
(await this.options.projection.resolve(input.sessionId));
|
|
269
279
|
if (session === undefined)
|
|
270
280
|
throw new Error('SESSION_NOT_FOUND');
|
|
271
|
-
|
|
272
|
-
|
|
281
|
+
try {
|
|
282
|
+
await this.removeResolved(session);
|
|
283
|
+
return { deleted: true };
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
this.options.invalidateWorkspaceSessions(session.workspaceId);
|
|
287
|
+
}
|
|
273
288
|
}
|
|
274
289
|
async cleanupWorkspaceSessions(input) {
|
|
275
290
|
const inactiveDays = input.inactiveDays;
|
|
@@ -295,54 +310,58 @@ export class SessionCatalogService {
|
|
|
295
310
|
let skipped = 0;
|
|
296
311
|
let failed = 0;
|
|
297
312
|
const deletedByRunner = {};
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
(
|
|
302
|
-
(
|
|
303
|
-
this.options.
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
.
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
continue;
|
|
318
|
-
}
|
|
319
|
-
try {
|
|
320
|
-
if ((await this.removeResolved(session, true)) === 'deleted') {
|
|
321
|
-
deleted += 1;
|
|
322
|
-
deletedByRunner[session.runner] = (deletedByRunner[session.runner] ?? 0) + 1;
|
|
323
|
-
}
|
|
324
|
-
else
|
|
325
|
-
skipped += 1;
|
|
326
|
-
}
|
|
327
|
-
catch (error) {
|
|
328
|
-
if (isCleanupSkip(error))
|
|
313
|
+
try {
|
|
314
|
+
for (const session of candidates.values()) {
|
|
315
|
+
const missingCandidate = session.externalDiscovered === false;
|
|
316
|
+
if (session.externalSessionId === null ||
|
|
317
|
+
(missingCandidate &&
|
|
318
|
+
(!this.options.runnerAvailable(session.runner) ||
|
|
319
|
+
this.options.runtime.getAgentSession(session.id) !== undefined)) ||
|
|
320
|
+
(!missingCandidate &&
|
|
321
|
+
Number.isFinite(session.lastActivityAt) &&
|
|
322
|
+
session.lastActivityAt > 0 &&
|
|
323
|
+
session.lastActivityAt >= cutoff) ||
|
|
324
|
+
(session.pinOrder !== null && session.pinOrder !== undefined) ||
|
|
325
|
+
this.options.isWatching(session.id) ||
|
|
326
|
+
this.options.isRecovering?.(session.id) ||
|
|
327
|
+
this.options.runtime.sessionQueue(session.id).itemIds.length > 0 ||
|
|
328
|
+
this.options.runtime
|
|
329
|
+
.listRunsForSession(session.id)
|
|
330
|
+
.some((run) => !isTerminalRunStatus(run.status)) ||
|
|
331
|
+
this.options.externalActivity(session.id) === 'BUSY') {
|
|
329
332
|
skipped += 1;
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
runner
|
|
336
|
-
|
|
337
|
-
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
if ((await this.removeResolved(session, true)) === 'deleted') {
|
|
337
|
+
deleted += 1;
|
|
338
|
+
deletedByRunner[session.runner] = (deletedByRunner[session.runner] ?? 0) + 1;
|
|
339
|
+
}
|
|
340
|
+
else
|
|
341
|
+
skipped += 1;
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
if (isCleanupSkip(error))
|
|
345
|
+
skipped += 1;
|
|
346
|
+
else {
|
|
347
|
+
failed += 1;
|
|
348
|
+
nodeLog('workspace.session.cleanup.failed', {
|
|
349
|
+
workspaceId: input.workspaceId,
|
|
350
|
+
sessionId: session.id,
|
|
351
|
+
runner: session.runner,
|
|
352
|
+
...cleanupFailureDiagnostic(error)
|
|
353
|
+
});
|
|
354
|
+
}
|
|
338
355
|
}
|
|
339
356
|
}
|
|
357
|
+
return { deleted, skipped, failed, deletedByRunner };
|
|
358
|
+
}
|
|
359
|
+
finally {
|
|
360
|
+
this.options.invalidateWorkspaceSessions(input.workspaceId);
|
|
340
361
|
}
|
|
341
|
-
this.options.invalidateWorkspaceSessions(input.workspaceId);
|
|
342
|
-
return { deleted, skipped, failed, deletedByRunner };
|
|
343
362
|
}
|
|
344
363
|
cleanupCandidate(native) {
|
|
345
|
-
const projected = this.options.projectionForNative(native);
|
|
364
|
+
const projected = this.options.projection.projectionForNative(native);
|
|
346
365
|
if (projected === undefined)
|
|
347
366
|
return native;
|
|
348
367
|
return {
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { nodeLog } from '../operational.js';
|
|
2
|
+
import { isTerminalRunStatus } from '../util/node-operation-parsers.js';
|
|
1
3
|
export class SessionIdentityService {
|
|
2
4
|
options;
|
|
3
5
|
bootstrapIds = new Set();
|
|
4
6
|
waiters = new Map();
|
|
7
|
+
failedBootstrapIds = new Set();
|
|
5
8
|
constructor(options) {
|
|
6
9
|
this.options = options;
|
|
7
10
|
}
|
|
@@ -9,6 +12,8 @@ export class SessionIdentityService {
|
|
|
9
12
|
this.bootstrapIds.add(sessionId);
|
|
10
13
|
}
|
|
11
14
|
promote(sessionId, nativeSessionId) {
|
|
15
|
+
if (this.failedBootstrapIds.has(sessionId))
|
|
16
|
+
throw new Error('SESSION_START_FAILED');
|
|
12
17
|
const current = this.options.runtime.getAgentSession(sessionId);
|
|
13
18
|
this.options.runtime.promoteSessionIdentity(sessionId, nativeSessionId);
|
|
14
19
|
if (current !== undefined)
|
|
@@ -45,9 +50,10 @@ export class SessionIdentityService {
|
|
|
45
50
|
resolveIdentity = resolve;
|
|
46
51
|
rejectIdentity = reject;
|
|
47
52
|
});
|
|
53
|
+
// Startup may still be awaiting a command or the first message when identity fails.
|
|
54
|
+
void promise.catch(() => undefined);
|
|
48
55
|
const timer = setTimeout(() => {
|
|
49
|
-
this.
|
|
50
|
-
rejectIdentity(new Error('SESSION_NATIVE_ID_TIMEOUT'));
|
|
56
|
+
this.reject(bootstrapSessionId, 'SESSION_NATIVE_ID_TIMEOUT');
|
|
51
57
|
}, 30_000);
|
|
52
58
|
timer.unref();
|
|
53
59
|
this.waiters.set(bootstrapSessionId, {
|
|
@@ -59,22 +65,39 @@ export class SessionIdentityService {
|
|
|
59
65
|
return promise;
|
|
60
66
|
}
|
|
61
67
|
reject(sessionId, code) {
|
|
62
|
-
|
|
68
|
+
const bootstrap = this.bootstrapIds.has(sessionId);
|
|
69
|
+
if (bootstrap) {
|
|
70
|
+
this.failedBootstrapIds.add(sessionId);
|
|
63
71
|
this.options.releaseAttachments?.(sessionId);
|
|
72
|
+
}
|
|
64
73
|
const waiter = this.waiters.get(sessionId);
|
|
65
|
-
if (waiter
|
|
74
|
+
if (waiter !== undefined) {
|
|
75
|
+
clearTimeout(waiter.timer);
|
|
76
|
+
this.waiters.delete(sessionId);
|
|
77
|
+
waiter.reject(new Error(code));
|
|
78
|
+
}
|
|
79
|
+
if (!bootstrap)
|
|
66
80
|
return;
|
|
67
|
-
clearTimeout(waiter.timer);
|
|
68
|
-
this.waiters.delete(sessionId);
|
|
69
|
-
waiter.reject(new Error(code));
|
|
70
81
|
queueMicrotask(() => {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
this.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
82
|
+
const session = this.options.runtime.getAgentSession(sessionId);
|
|
83
|
+
if (session?.id === sessionId) {
|
|
84
|
+
if (this.options.runtime
|
|
85
|
+
.listRunsForSession(sessionId)
|
|
86
|
+
.some((run) => !isTerminalRunStatus(run.status)))
|
|
87
|
+
return;
|
|
88
|
+
try {
|
|
89
|
+
this.options.runtime.removeAgentSession(sessionId);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
nodeLog('session.bootstrap.cleanup.failed', {
|
|
93
|
+
sessionId,
|
|
94
|
+
error: error instanceof Error ? error.message.slice(0, 512) : 'Unknown cleanup error'
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
77
98
|
}
|
|
99
|
+
this.bootstrapIds.delete(sessionId);
|
|
100
|
+
this.failedBootstrapIds.delete(sessionId);
|
|
78
101
|
});
|
|
79
102
|
}
|
|
80
103
|
dispose() {
|
|
@@ -84,5 +107,6 @@ export class SessionIdentityService {
|
|
|
84
107
|
}
|
|
85
108
|
this.waiters.clear();
|
|
86
109
|
this.bootstrapIds.clear();
|
|
110
|
+
this.failedBootstrapIds.clear();
|
|
87
111
|
}
|
|
88
112
|
}
|
|
@@ -69,7 +69,9 @@ export class SessionLifecycleService {
|
|
|
69
69
|
const next = nativeCodexRename
|
|
70
70
|
? this.options.runtime.setRunnerTitleFromNative(updated.id, input.customTitle ?? '')
|
|
71
71
|
: updated;
|
|
72
|
-
if (sessionPrompt !== undefined
|
|
72
|
+
if (sessionPrompt !== undefined ||
|
|
73
|
+
input.customTitle !== undefined ||
|
|
74
|
+
input.pinned !== undefined)
|
|
73
75
|
this.options.emitSession(next);
|
|
74
76
|
return { session: this.options.present(next) };
|
|
75
77
|
}
|
|
@@ -77,7 +79,8 @@ export class SessionLifecycleService {
|
|
|
77
79
|
if (typeof input.sessionId !== 'string' ||
|
|
78
80
|
(input.direction !== 'up' && input.direction !== 'down'))
|
|
79
81
|
throw new Error('SESSION_PIN_MOVE_INVALID');
|
|
80
|
-
this.options.database().movePinnedSession(input.sessionId, input.direction);
|
|
82
|
+
const workspaceId = this.options.database().movePinnedSession(input.sessionId, input.direction);
|
|
83
|
+
this.options.invalidateWorkspaceSessions(workspaceId);
|
|
81
84
|
return { moved: true };
|
|
82
85
|
}
|
|
83
86
|
create(input) {
|
|
@@ -35,7 +35,9 @@ export class WorkspaceSessionService {
|
|
|
35
35
|
return cached.sessions;
|
|
36
36
|
let discovery = this.state.sessionCacheReads.get(workspaceId);
|
|
37
37
|
if (discovery === undefined) {
|
|
38
|
-
|
|
38
|
+
// A retired read must not regain ownership when its Workspace state is reclaimed.
|
|
39
|
+
const generation = Symbol();
|
|
40
|
+
this.cacheGenerations.set(workspaceId, generation);
|
|
39
41
|
let resolveInitial;
|
|
40
42
|
let rejectInitial;
|
|
41
43
|
const initial = new Promise((resolve, reject) => {
|
|
@@ -52,7 +54,7 @@ export class WorkspaceSessionService {
|
|
|
52
54
|
let batchPublished = false;
|
|
53
55
|
let notifyCompletion = false;
|
|
54
56
|
const publish = (sessions, notify = true) => {
|
|
55
|
-
if (
|
|
57
|
+
if (this.cacheGenerations.get(workspaceId) !== generation)
|
|
56
58
|
return;
|
|
57
59
|
const previous = this.state.sessionCache.get(workspaceId)?.sessions;
|
|
58
60
|
this.state.sessionCache.set(workspaceId, {
|
|
@@ -66,7 +68,7 @@ export class WorkspaceSessionService {
|
|
|
66
68
|
};
|
|
67
69
|
const read = this.options
|
|
68
70
|
.discover(workspaceId, (runner, sessions) => {
|
|
69
|
-
if (!acceptingBatches ||
|
|
71
|
+
if (!acceptingBatches || this.cacheGenerations.get(workspaceId) !== generation)
|
|
70
72
|
return;
|
|
71
73
|
const previous = this.state.sessionCache.get(workspaceId)?.sessions;
|
|
72
74
|
const merged = [
|
|
@@ -81,7 +83,7 @@ export class WorkspaceSessionService {
|
|
|
81
83
|
resolveInitial(merged);
|
|
82
84
|
})
|
|
83
85
|
.then((sessions) => {
|
|
84
|
-
if (
|
|
86
|
+
if (this.cacheGenerations.get(workspaceId) === generation)
|
|
85
87
|
this.incompleteDiscoveries.delete(workspaceId);
|
|
86
88
|
const previous = this.state.sessionCache.get(workspaceId)?.sessions;
|
|
87
89
|
notifyCompletion =
|
|
@@ -93,7 +95,7 @@ export class WorkspaceSessionService {
|
|
|
93
95
|
})
|
|
94
96
|
.catch((error) => {
|
|
95
97
|
rejectInitial(error);
|
|
96
|
-
if (
|
|
98
|
+
if (this.cacheGenerations.get(workspaceId) === generation) {
|
|
97
99
|
nodeLog('native.session.discovery.failed', { workspaceId });
|
|
98
100
|
const partial = this.state.sessionCache.get(workspaceId);
|
|
99
101
|
if (partial !== undefined)
|
|
@@ -111,11 +113,11 @@ export class WorkspaceSessionService {
|
|
|
111
113
|
}
|
|
112
114
|
if (this.initialReads.get(workspaceId) === initial)
|
|
113
115
|
this.initialReads.delete(workspaceId);
|
|
114
|
-
if (notifyCompletion &&
|
|
116
|
+
if (notifyCompletion && this.cacheGenerations.get(workspaceId) === generation)
|
|
115
117
|
this.options.invalidate(workspaceId);
|
|
116
118
|
if (!this.state.sessionCacheReads.has(workspaceId) &&
|
|
117
119
|
!this.state.sessionCache.has(workspaceId) &&
|
|
118
|
-
|
|
120
|
+
this.cacheGenerations.get(workspaceId) === generation)
|
|
119
121
|
this.cacheGenerations.delete(workspaceId);
|
|
120
122
|
});
|
|
121
123
|
discovery = read;
|
|
@@ -133,11 +135,7 @@ export class WorkspaceSessionService {
|
|
|
133
135
|
this.state.sessionWatchTimers.clear();
|
|
134
136
|
this.state.sessionWatchSignatures.clear();
|
|
135
137
|
this.state.sessionWatchReads.clear();
|
|
136
|
-
|
|
137
|
-
...this.state.sessionCache.keys(),
|
|
138
|
-
...this.state.sessionCacheReads.keys()
|
|
139
|
-
]))
|
|
140
|
-
this.bumpCacheGeneration(workspaceId);
|
|
138
|
+
this.cacheGenerations.clear();
|
|
141
139
|
this.state.sessionCache.clear();
|
|
142
140
|
this.state.sessionCacheReads.clear();
|
|
143
141
|
this.initialReads.clear();
|
|
@@ -156,16 +154,13 @@ export class WorkspaceSessionService {
|
|
|
156
154
|
this.staleDiscoveries.add(workspaceId);
|
|
157
155
|
}
|
|
158
156
|
invalidateCache(workspaceId) {
|
|
159
|
-
this.
|
|
157
|
+
this.cacheGenerations.delete(workspaceId);
|
|
160
158
|
this.state.sessionCache.delete(workspaceId);
|
|
161
159
|
this.state.sessionCacheReads.delete(workspaceId);
|
|
162
160
|
this.initialReads.delete(workspaceId);
|
|
163
161
|
this.incompleteDiscoveries.delete(workspaceId);
|
|
164
162
|
this.staleDiscoveries.delete(workspaceId);
|
|
165
163
|
}
|
|
166
|
-
bumpCacheGeneration(workspaceId) {
|
|
167
|
-
this.cacheGenerations.set(workspaceId, (this.cacheGenerations.get(workspaceId) ?? 0) + 1);
|
|
168
|
-
}
|
|
169
164
|
async refreshWatch(workspaceId) {
|
|
170
165
|
const current = this.state.sessionWatchReads.get(workspaceId);
|
|
171
166
|
if (current !== undefined)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myagentroam/node",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.64",
|
|
4
4
|
"description": "MyAgentRoam Node runtime CLI.",
|
|
5
5
|
"license": "Apache-2.0 AND GPL-2.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"playwright": "1.62.1",
|
|
38
38
|
"ws": "^8.21.3",
|
|
39
39
|
"zod": "4.4.3",
|
|
40
|
-
"@myagentroam/agent": "0.10.
|
|
40
|
+
"@myagentroam/agent": "0.10.64",
|
|
41
41
|
"@myagentroam/code-navigation": "0.10.1",
|
|
42
|
-
"@myagentroam/
|
|
43
|
-
"@myagentroam/
|
|
42
|
+
"@myagentroam/protocol": "0.10.64",
|
|
43
|
+
"@myagentroam/marctl": "0.10.12"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/ws": "^8.18.1"
|