@myagentroam/node 0.12.4 → 0.12.5
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/application/scheduled-execution-service.js +60 -0
- package/dist/connector.js +206 -260
- package/dist/marctl/package.json +1 -1
- package/dist/runner/mar-agent/agent-network-extension.js +2 -1
- package/dist/service/agent-network-run-projection.js +60 -0
- package/dist/session/session-catalog-service.js +11 -0
- package/dist/workspace/workspace-session-service.js +43 -6
- package/package.json +5 -5
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** Prepares and submits scheduled input through existing Session/Runner owners.
|
|
2
|
+
* The scheduler retains occurrence state, admission, in-flight work and terminal settlement;
|
|
3
|
+
* this application service owns no timers or alternate execution pipeline.
|
|
4
|
+
*/
|
|
5
|
+
export class ScheduledExecutionService {
|
|
6
|
+
options;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.options = options;
|
|
9
|
+
}
|
|
10
|
+
authorize = async (task, occurrenceId) => {
|
|
11
|
+
const session = this.options.runtime.getAgentSession(task.sessionId) ??
|
|
12
|
+
(await this.options.projection.resolveCurrent(task.sessionId));
|
|
13
|
+
if (session === undefined ||
|
|
14
|
+
session.workspaceId !== task.workspaceId ||
|
|
15
|
+
session.runner !== task.runner)
|
|
16
|
+
throw new Error('SCHEDULE_SESSION_NOT_FOUND');
|
|
17
|
+
const configured = this.options.projection.ensureResumedConfiguration(session);
|
|
18
|
+
const context = await this.options.authorization.authorize({
|
|
19
|
+
taskId: task.id,
|
|
20
|
+
revision: task.revision,
|
|
21
|
+
occurrenceId,
|
|
22
|
+
sessionId: task.sessionId,
|
|
23
|
+
workspaceId: task.workspaceId,
|
|
24
|
+
ownerUserId: task.ownerUserId,
|
|
25
|
+
runner: task.runner,
|
|
26
|
+
modelId: configured.modelId
|
|
27
|
+
});
|
|
28
|
+
return {
|
|
29
|
+
...(context.gitConfiguration === undefined
|
|
30
|
+
? {}
|
|
31
|
+
: { gitConfiguration: context.gitConfiguration }),
|
|
32
|
+
secretEnvironment: context.secretEnvironment,
|
|
33
|
+
personalInstructions: context.personalInstructions,
|
|
34
|
+
usageGrant: context.usageGrant,
|
|
35
|
+
executionContext: this.options.runners.require(task.runner).prepareExecutionContext(context)
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
submit = async (task, occurrenceId, context) => {
|
|
39
|
+
const result = (await this.options.git.execute('session.message', {
|
|
40
|
+
sessionId: task.sessionId,
|
|
41
|
+
workspaceId: task.workspaceId,
|
|
42
|
+
__requestUserId: task.ownerUserId,
|
|
43
|
+
secretEnvironment: context.secretEnvironment,
|
|
44
|
+
...(context.gitConfiguration === undefined
|
|
45
|
+
? {}
|
|
46
|
+
: { gitConfiguration: context.gitConfiguration })
|
|
47
|
+
}, async (data) => this.options.messages.submitScheduled({
|
|
48
|
+
sessionId: task.sessionId,
|
|
49
|
+
occurrenceId,
|
|
50
|
+
objective: task.objective,
|
|
51
|
+
initiatedByUserId: task.ownerUserId,
|
|
52
|
+
secretEnvironment: data
|
|
53
|
+
.secretEnvironment,
|
|
54
|
+
personalInstructions: context.personalInstructions,
|
|
55
|
+
...(context.usageGrant === undefined ? {} : { usageGrant: context.usageGrant }),
|
|
56
|
+
executionContext: context.executionContext
|
|
57
|
+
})));
|
|
58
|
+
return { runId: result.run.id };
|
|
59
|
+
};
|
|
60
|
+
}
|
package/dist/connector.js
CHANGED
|
@@ -6,6 +6,7 @@ import { ExecutionLifecycle } from './application/execution-lifecycle.js';
|
|
|
6
6
|
import { NodeOperationAdmission } from './application/node-operation-admission.js';
|
|
7
7
|
import { normalizeMarAgentUsage } from './runner/mar-agent/conversation-normalizer.js';
|
|
8
8
|
import { NodeLifecycle } from './application/node-lifecycle.js';
|
|
9
|
+
import { ScheduledExecutionService } from './application/scheduled-execution-service.js';
|
|
9
10
|
import { isImageAttachment } from './session/attachments.js';
|
|
10
11
|
import { ModelUsageOutbox } from './service/model-usage-outbox.js';
|
|
11
12
|
import { NodeModelUsageService } from './service/model-usage-service.js';
|
|
@@ -24,7 +25,8 @@ import { CommandStateStore } from './runner-command-engine.js';
|
|
|
24
25
|
import { GoalRuntime } from './service/goal-runtime.js';
|
|
25
26
|
import { GoalContinuationCoordinator } from './service/goal-continuation-coordinator.js';
|
|
26
27
|
import { GoalMcpBridge } from './service/goal-mcp-bridge.js';
|
|
27
|
-
import { AgentNetworkObservations
|
|
28
|
+
import { AgentNetworkObservations } from './service/agent-network-observations.js';
|
|
29
|
+
import { AgentNetworkRunProjection } from './service/agent-network-run-projection.js';
|
|
28
30
|
import { createNodeAgentNetwork } from './service/agent-network-bindings.js';
|
|
29
31
|
import { agentNetworkExecutionCommand, agentNetworkInvoke } from './service/agent-network-invoke.js';
|
|
30
32
|
import { RunnerUsageReader } from './runner-usage.js';
|
|
@@ -54,7 +56,7 @@ import { SessionWatchSlotCoordinator } from './service/session-watch-slot-coordi
|
|
|
54
56
|
import { SessionContextService } from './session/session-context-service.js';
|
|
55
57
|
import { RunWorkbenchService } from './service/run-workbench-service.js';
|
|
56
58
|
import { SessionQueryService } from './session/session-query-service.js';
|
|
57
|
-
import {
|
|
59
|
+
import { marctlSessionProgress } from './service/session-recent-activity.js';
|
|
58
60
|
import { WorkspaceQueueWorkbenchService } from './workspace/workspace-queue-workbench-service.js';
|
|
59
61
|
import { ContinuousRunCoordinator } from './service/continuous-run-coordinator.js';
|
|
60
62
|
import { SessionIdentityService } from './session/session-identity-service.js';
|
|
@@ -154,6 +156,12 @@ const CONVERSATION_HOSTING_MAINTENANCE_OPERATIONS = [
|
|
|
154
156
|
export function nextReconnectDelay(attempt) {
|
|
155
157
|
return Math.min(RETRY_MAX_MS, RETRY_MIN_MS * 2 ** Math.min(Math.max(attempt, 0), 16));
|
|
156
158
|
}
|
|
159
|
+
/** Application composition and resource-lifetime root, not a domain workflow owner.
|
|
160
|
+
* Field initializers create local state/ports; the constructor creates domain owners, connects
|
|
161
|
+
* runtime event feedback, registers operations, then connects lifecycle. Callbacks for genuine
|
|
162
|
+
* Session/Workspace feedback are dormant until composition completes; start() activates resources.
|
|
163
|
+
* Session queries, Agent observations and execution preparation stay with their respective owners.
|
|
164
|
+
*/
|
|
157
165
|
export class NodeConnector {
|
|
158
166
|
usageOutbox;
|
|
159
167
|
modelUsage = new NodeModelUsageService(() => this.usageOutbox, () => this.config?.nodeId);
|
|
@@ -184,31 +192,7 @@ export class NodeConnector {
|
|
|
184
192
|
throw new Error('NODE_CONFIG_UNAVAILABLE');
|
|
185
193
|
return nodeDataDirectory(this.config, this.configPath);
|
|
186
194
|
});
|
|
187
|
-
lifecycle
|
|
188
|
-
this.continuousRunCoordinator.suspend();
|
|
189
|
-
this.goalCoordinator.freeze();
|
|
190
|
-
this.scheduledSessions.stop();
|
|
191
|
-
}, {
|
|
192
|
-
waitForRecovery: () => this.restartHandoff.wait(),
|
|
193
|
-
drainInput: async () => {
|
|
194
|
-
this.sessionMessageService.setAccepting(false);
|
|
195
|
-
await this.operationAdmission.drain();
|
|
196
|
-
await this.sessionMessageService.drain();
|
|
197
|
-
},
|
|
198
|
-
closeResume: () => this.externalSessionResumeService.close(),
|
|
199
|
-
capture: () => {
|
|
200
|
-
if (!this.config?.nodeId)
|
|
201
|
-
return;
|
|
202
|
-
return this.restartHandoff.capture();
|
|
203
|
-
},
|
|
204
|
-
dispose: () => this.disposeRuntime(),
|
|
205
|
-
publish: (snapshot) => this.restartHandoff.publish(snapshot),
|
|
206
|
-
diagnose: (stage, error) => nodeLog('node.shutdown.failed', {
|
|
207
|
-
stage,
|
|
208
|
-
errorCode: safeErrorCode(error, 'NODE_SHUTDOWN_FAILED'),
|
|
209
|
-
...(stage === 'capture' ? plannedRestartCaptureDiagnostic(error) : undefined)
|
|
210
|
-
})
|
|
211
|
-
});
|
|
195
|
+
lifecycle;
|
|
212
196
|
get stopped() {
|
|
213
197
|
return this.lifecycle.stopped;
|
|
214
198
|
}
|
|
@@ -323,24 +307,12 @@ export class NodeConnector {
|
|
|
323
307
|
runnerUsageReader;
|
|
324
308
|
terminalManager;
|
|
325
309
|
upgradeCoordinator;
|
|
326
|
-
workspaceResources
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
this.workspaceCoordinator.invalidate(workspaceId);
|
|
333
|
-
},
|
|
334
|
-
releaseWatch: (workspaceId) => this.workspaceWorkbenchService?.releaseWorkspace(workspaceId),
|
|
335
|
-
removeWatch: (watch) => this.workspaceWorkbenchService?.cancelFilePrewarm(watch),
|
|
336
|
-
temporaryRoot: () => this.attachmentTemporaryRoot(),
|
|
337
|
-
attachmentResource: (id, path) => this.runAttachmentService.fileResource(id, path)
|
|
338
|
-
});
|
|
339
|
-
workspaceWatchService = this.workspaceResources.watch;
|
|
340
|
-
workspaceSessionService = this.workspaceResources.sessions;
|
|
341
|
-
workspaceFileService = this.workspaceResources.files;
|
|
342
|
-
workspaceUploadService = this.workspaceResources.uploads;
|
|
343
|
-
invalidateWorkspaceFiles = this.workspaceResources.invalidateFiles;
|
|
310
|
+
workspaceResources;
|
|
311
|
+
workspaceWatchService;
|
|
312
|
+
workspaceSessionService;
|
|
313
|
+
workspaceFileService;
|
|
314
|
+
workspaceUploadService;
|
|
315
|
+
invalidateWorkspaceFiles;
|
|
344
316
|
directTransferService;
|
|
345
317
|
attachmentTemporaryRoot() {
|
|
346
318
|
if (this.config === undefined)
|
|
@@ -354,109 +326,13 @@ export class NodeConnector {
|
|
|
354
326
|
recentRunProjectionService = new RecentRunProjectionService((message) => this.send('recent-runs', message));
|
|
355
327
|
/** Runtime/session state is intentionally not persisted to Node SQLite. */
|
|
356
328
|
runtime = new NodeRuntimeState(() => this.config?.nodeId ?? 'runtime-node', (workspaceId) => this.database?.getWorkspace(workspaceId), Date.now, (run, session, userMessage) => this.recentRunProjectionService.record(run, session, userMessage));
|
|
357
|
-
goalCoordinator
|
|
358
|
-
goals: this.goalRuntime,
|
|
359
|
-
runners: () => this.runners,
|
|
360
|
-
runtime: this.runtime,
|
|
361
|
-
commands: this.commandStates,
|
|
362
|
-
queue: () => this.continuousRunCoordinator,
|
|
363
|
-
submit: (input) => this.sessionMessageService.submitGoal(input),
|
|
364
|
-
emitRun: (...args) => this.runEventBridge.emitRun(...args),
|
|
365
|
-
present: (session) => this.sessionPresentationService.present(session),
|
|
366
|
-
emitSession: (session) => this.emitSessionWorkbenchEvent({ session: this.sessionPresentationService.present(session) }),
|
|
367
|
-
database: () => this.database
|
|
368
|
-
});
|
|
329
|
+
goalCoordinator;
|
|
369
330
|
metrics = new NodeMetrics();
|
|
370
331
|
agentNetworkObservations = new AgentNetworkObservations();
|
|
371
|
-
|
|
372
|
-
runEventService
|
|
373
|
-
runtime: this.runtime,
|
|
374
|
-
metrics: this.metrics,
|
|
375
|
-
isBootstrapSession: (sessionId) => this.sessionIdentityService.bootstrapIds.has(sessionId),
|
|
376
|
-
presentSession: (session) => this.sessionPresentationService.present(session),
|
|
377
|
-
rejectBootstrap: (sessionId, code) => this.sessionIdentityService.reject(sessionId, code),
|
|
378
|
-
emitWorkbench: (category, payload) => this.publishWorkbenchEvent(category, payload),
|
|
379
|
-
cleanupAttachments: (runId) => this.runAttachmentService.cleanup(runId),
|
|
380
|
-
refreshAttachments: (sessionId) => this.runAttachmentService.refreshSession(sessionId),
|
|
381
|
-
expireImages: (runId) => this.runAttachmentService.expire(runId),
|
|
382
|
-
scheduleContextRefresh: (sessionId) => this.sessionContextService.schedule(sessionId),
|
|
383
|
-
refreshWorkspaceSessions: (workspaceId) => this.workspaceWorkbenchService.ensureTopicSnapshot(workspaceId, 'sessions'),
|
|
384
|
-
onTerminalRun: (run, status) => this.executionLifecycle.terminal(run, status),
|
|
385
|
-
observeAgentEvent: ({ run, executionId, eventType, payload, terminalStatus }) => {
|
|
386
|
-
const child = this.runtime.getAgentSession(run.sessionId);
|
|
387
|
-
if (!child)
|
|
388
|
-
return;
|
|
389
|
-
const parent = child.parentSessionId == null
|
|
390
|
-
? undefined
|
|
391
|
-
: this.runtime.getAgentSession(child.parentSessionId);
|
|
392
|
-
let observation = agentObservationFromRunEvent({ eventType, payload, terminalStatus });
|
|
393
|
-
if (eventType === 'approval.request') {
|
|
394
|
-
const approvals = marctlSessionDetail(this.runtime, child, 0).pendingApprovals;
|
|
395
|
-
if (approvals.length)
|
|
396
|
-
observation = {
|
|
397
|
-
kind: 'message',
|
|
398
|
-
value: JSON.stringify({
|
|
399
|
-
pendingApprovals: approvals
|
|
400
|
-
})
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
if (observation)
|
|
404
|
-
this.agentNetworkObservations.publishChild({
|
|
405
|
-
...(parent?.workspaceId === child.workspaceId ? { parentId: parent.id } : {}),
|
|
406
|
-
childId: child.id,
|
|
407
|
-
runId: run.id,
|
|
408
|
-
executionId,
|
|
409
|
-
...observation
|
|
410
|
-
});
|
|
411
|
-
if (terminalStatus !== undefined &&
|
|
412
|
-
this.transientAgentObservers.get(run.sessionId) === run.id) {
|
|
413
|
-
this.transientAgentObservers.delete(run.sessionId);
|
|
414
|
-
this.agentNetworkObservations.get(run.sessionId)?.close();
|
|
415
|
-
}
|
|
416
|
-
},
|
|
417
|
-
dispatchQueue: (sessionId, delayMs) => this.executionLifecycle.dispatchQueue(sessionId, delayMs),
|
|
418
|
-
executionStarted: (executionId) => this.continuousRunCoordinator.executionStarted(executionId),
|
|
419
|
-
acceptExecutionTerminal: (input) => this.continuousRunCoordinator.acceptExecutionTerminal(input),
|
|
420
|
-
publishGoalUpdate: ({ session, goal }) => this.goalCoordinator.publishGoalState(session, goal),
|
|
421
|
-
publishQueueChange: (sessionId) => this.continuousRunCoordinator.publish(sessionId),
|
|
422
|
-
releaseExecution: (executionId) => this.executionLifecycle.release(executionId)
|
|
423
|
-
});
|
|
332
|
+
agentNetworkProjection;
|
|
333
|
+
runEventService;
|
|
424
334
|
sessionWatchSlots = new SessionWatchSlotCoordinator();
|
|
425
|
-
nativeSessionWatchService
|
|
426
|
-
resolve: async (sessionId) => this.runtime.getAgentSession(sessionId) ??
|
|
427
|
-
this.nativeSessionProjectionService.resolveCurrent(sessionId),
|
|
428
|
-
managedActive: (session) => hasManagedActiveRun(this.runtime, session),
|
|
429
|
-
suspended: (sessionId) => this.externalSessionResumeService.transitioning(sessionId),
|
|
430
|
-
refreshActivity: (session) => this.conversationHistoryService.refreshExternalActivity(session),
|
|
431
|
-
present: (session) => this.sessionPresentationService.present(session),
|
|
432
|
-
readPage: (session, limit) => this.conversationHistoryService.read(session, { limit }),
|
|
433
|
-
readHistoryPage: (session, limit) => this.conversationHistoryService.readHistoryPage(session, { limit }),
|
|
434
|
-
projectInitialPage: (session, page, _unit, limit) => {
|
|
435
|
-
const context = this.sessionContextService.snapshot(session.id);
|
|
436
|
-
this.sessionContextService.schedule(session);
|
|
437
|
-
return {
|
|
438
|
-
kind: 'session-watch-snapshot',
|
|
439
|
-
sequence: this.workbenchEventService.snapshotSequence(),
|
|
440
|
-
session: this.sessionPresentationService.present(session),
|
|
441
|
-
runs: this.runtime.listRunsForSession(session.id, { includeTerminal: true }),
|
|
442
|
-
progress: marctlSessionProgress(this.runtime, session.id),
|
|
443
|
-
performance: this.runtime.sessionPerformance(session.id),
|
|
444
|
-
queue: this.workspaceQueueWorkbenchService.snapshot(session.id),
|
|
445
|
-
context,
|
|
446
|
-
conversation: this.conversationSegmentService.projectInitial(session, page, limit)
|
|
447
|
-
};
|
|
448
|
-
},
|
|
449
|
-
emitSession: (session) => this.emitSessionWorkbenchEvent({ session }),
|
|
450
|
-
emitPage: (session, page) => this.emitWorkbenchEvent('conversation', {
|
|
451
|
-
sessionId: session.id,
|
|
452
|
-
snapshot: this.conversationSegmentService.projectInitial(session, page, 3)
|
|
453
|
-
}),
|
|
454
|
-
nodeId: () => this.config?.nodeId ?? 'runtime-node',
|
|
455
|
-
nodeGeneration: () => this.nodeGeneration,
|
|
456
|
-
emitWatchEvent: (payload) => this.send('watch.event', payload),
|
|
457
|
-
sessionWatchSlots: this.sessionWatchSlots,
|
|
458
|
-
log: nodeLog
|
|
459
|
-
});
|
|
335
|
+
nativeSessionWatchService;
|
|
460
336
|
constructor(options = {}) {
|
|
461
337
|
this.config = options.config;
|
|
462
338
|
this.configPath = options.configPath;
|
|
@@ -1076,6 +952,100 @@ export class NodeConnector {
|
|
|
1076
952
|
goal: (input) => this.goalCoordinator.start(input),
|
|
1077
953
|
clearGoal: (session) => this.goalCoordinator.clear(session)
|
|
1078
954
|
});
|
|
955
|
+
this.nativeSessionWatchService = new NativeSessionWatchService({
|
|
956
|
+
resolve: async (sessionId) => this.runtime.getAgentSession(sessionId) ??
|
|
957
|
+
this.nativeSessionProjectionService.resolveCurrent(sessionId),
|
|
958
|
+
managedActive: (session) => hasManagedActiveRun(this.runtime, session),
|
|
959
|
+
suspended: (sessionId) => this.externalSessionResumeService.transitioning(sessionId),
|
|
960
|
+
refreshActivity: (session) => this.conversationHistoryService.refreshExternalActivity(session),
|
|
961
|
+
present: (session) => this.sessionPresentationService.present(session),
|
|
962
|
+
readPage: (session, limit) => this.conversationHistoryService.read(session, { limit }),
|
|
963
|
+
readHistoryPage: (session, limit) => this.conversationHistoryService.readHistoryPage(session, { limit }),
|
|
964
|
+
projectInitialPage: (session, page, _unit, limit) => {
|
|
965
|
+
const context = this.sessionContextService.snapshot(session.id);
|
|
966
|
+
this.sessionContextService.schedule(session);
|
|
967
|
+
return {
|
|
968
|
+
kind: 'session-watch-snapshot',
|
|
969
|
+
sequence: this.workbenchEventService.snapshotSequence(),
|
|
970
|
+
session: this.sessionPresentationService.present(session),
|
|
971
|
+
runs: this.runtime.listRunsForSession(session.id, { includeTerminal: true }),
|
|
972
|
+
progress: marctlSessionProgress(this.runtime, session.id),
|
|
973
|
+
performance: this.runtime.sessionPerformance(session.id),
|
|
974
|
+
queue: this.workspaceQueueWorkbenchService.snapshot(session.id),
|
|
975
|
+
context,
|
|
976
|
+
conversation: this.conversationSegmentService.projectInitial(session, page, limit)
|
|
977
|
+
};
|
|
978
|
+
},
|
|
979
|
+
emitSession: (session) => this.emitSessionWorkbenchEvent({ session }),
|
|
980
|
+
emitPage: (session, page) => this.emitWorkbenchEvent('conversation', {
|
|
981
|
+
sessionId: session.id,
|
|
982
|
+
snapshot: this.conversationSegmentService.projectInitial(session, page, 3)
|
|
983
|
+
}),
|
|
984
|
+
nodeId: () => this.config?.nodeId ?? 'runtime-node',
|
|
985
|
+
nodeGeneration: () => this.nodeGeneration,
|
|
986
|
+
emitWatchEvent: (payload) => this.send('watch.event', payload),
|
|
987
|
+
sessionWatchSlots: this.sessionWatchSlots,
|
|
988
|
+
log: nodeLog
|
|
989
|
+
});
|
|
990
|
+
// Create the Session query owner, then connect its Workspace cache/observation ports below.
|
|
991
|
+
this.sessionCatalogService = new SessionCatalogService({
|
|
992
|
+
resolveExecutionDirectory,
|
|
993
|
+
database: () => {
|
|
994
|
+
if (this.database === undefined)
|
|
995
|
+
throw new Error('NODE_DATABASE_UNAVAILABLE');
|
|
996
|
+
return this.database;
|
|
997
|
+
},
|
|
998
|
+
nodeId: () => this.config?.nodeId ?? 'recorded-session',
|
|
999
|
+
runtime: this.runtime,
|
|
1000
|
+
runners: this.runners,
|
|
1001
|
+
runnerAvailable: (runner) => this.runners.require(runner).available(this.capabilities),
|
|
1002
|
+
projection: this.nativeSessionProjectionService,
|
|
1003
|
+
cachedNative: () => this.nativeSessionProjectionService.cachedNative(),
|
|
1004
|
+
cachedWorkspace: (workspaceId) => this.workspaceSessionService.cached(workspaceId),
|
|
1005
|
+
discoveryPending: (workspaceId) => this.workspaceSessionService.discoveryPending(workspaceId),
|
|
1006
|
+
discoverWorkspace: (workspaceId, cleanup) => this.nativeSessionProjectionService.discoverWorkspace(workspaceId, undefined, cleanup),
|
|
1007
|
+
present: (session) => this.sessionPresentationService.present(session),
|
|
1008
|
+
nativeIds: (session) => this.nativeSessionProjectionService.nativeIds(session),
|
|
1009
|
+
forget: (session, nativeIds) => {
|
|
1010
|
+
this.nativeSessionProjectionService.forget(session, nativeIds);
|
|
1011
|
+
this.promptCompletion.invalidateSession(session.workspaceId, session.id);
|
|
1012
|
+
void this.runAttachmentService.releaseSession(session.id);
|
|
1013
|
+
},
|
|
1014
|
+
refreshActivity: (session) => this.conversationHistoryService.refreshExternalActivity(session),
|
|
1015
|
+
externalActivity: (sessionId) => this.externalSessionResumeService.activity(sessionId),
|
|
1016
|
+
isWatching: (sessionId) => this.nativeSessionWatchService.isWatching(sessionId),
|
|
1017
|
+
isRecovering: (sessionId) => this.restartHandoff.blocks(sessionId),
|
|
1018
|
+
history: (session, input) => this.conversationHistoryService.read(session, input),
|
|
1019
|
+
invalidateHistory: (sessionId, workspaceId) => {
|
|
1020
|
+
this.nativeSessionWatchService.invalidate(sessionId);
|
|
1021
|
+
this.promptCompletion.invalidateSession(workspaceId, sessionId);
|
|
1022
|
+
},
|
|
1023
|
+
invalidateWorkspaceSessions: (workspaceId) => {
|
|
1024
|
+
this.workspaceSessionService.invalidateCache(workspaceId);
|
|
1025
|
+
void this.workspaceWorkbenchService?.invalidateTopic(workspaceId, 'sessions');
|
|
1026
|
+
},
|
|
1027
|
+
emitSession: (session) => this.emitSessionWorkbenchEvent({
|
|
1028
|
+
session: this.sessionPresentationService.present(session)
|
|
1029
|
+
})
|
|
1030
|
+
});
|
|
1031
|
+
this.workspaceResources = composeWorkspaceResources({
|
|
1032
|
+
discover: (workspaceId, onRunnerDiscovered) => this.discoverWorkspaceSessions(workspaceId, onRunnerDiscovered),
|
|
1033
|
+
listPage: (workspaceId, group) => this.sessionCatalogService.listWorkspacePage(workspaceId, group),
|
|
1034
|
+
invalidateSessions: (workspaceId) => void this.workspaceWorkbenchService?.invalidateTopic(workspaceId, 'sessions'),
|
|
1035
|
+
invalidateFiles: (workspaceId) => {
|
|
1036
|
+
this.workspaceWorkbenchService?.invalidateWorkspaceFiles(workspaceId);
|
|
1037
|
+
this.workspaceCoordinator.invalidate(workspaceId);
|
|
1038
|
+
},
|
|
1039
|
+
releaseWatch: (workspaceId) => this.workspaceWorkbenchService?.releaseWorkspace(workspaceId),
|
|
1040
|
+
removeWatch: (watch) => this.workspaceWorkbenchService?.cancelFilePrewarm(watch),
|
|
1041
|
+
temporaryRoot: () => this.attachmentTemporaryRoot(),
|
|
1042
|
+
attachmentResource: (id, path) => this.runAttachmentService.fileResource(id, path)
|
|
1043
|
+
});
|
|
1044
|
+
this.workspaceWatchService = this.workspaceResources.watch;
|
|
1045
|
+
this.workspaceSessionService = this.workspaceResources.sessions;
|
|
1046
|
+
this.workspaceFileService = this.workspaceResources.files;
|
|
1047
|
+
this.workspaceUploadService = this.workspaceResources.uploads;
|
|
1048
|
+
this.invalidateWorkspaceFiles = this.workspaceResources.invalidateFiles;
|
|
1079
1049
|
this.sessionMessageService = new SessionMessageService({
|
|
1080
1050
|
resolveExecutionDirectory,
|
|
1081
1051
|
runtime: this.runtime,
|
|
@@ -1098,6 +1068,21 @@ export class NodeConnector {
|
|
|
1098
1068
|
prepareGoalInsertion: (sessionId, submissionId, goal) => this.goalCoordinator.prepareInsertion(sessionId, submissionId, goal),
|
|
1099
1069
|
goalPreparationFailed: (session, goal, activated) => this.goalCoordinator.handleGoalPreparationFailed(session, goal, activated)
|
|
1100
1070
|
});
|
|
1071
|
+
this.goalCoordinator = new GoalContinuationCoordinator({
|
|
1072
|
+
goals: this.goalRuntime,
|
|
1073
|
+
runners: () => this.runners,
|
|
1074
|
+
runtime: this.runtime,
|
|
1075
|
+
commands: this.commandStates,
|
|
1076
|
+
queue: () => this.continuousRunCoordinator,
|
|
1077
|
+
submit: (input) => this.sessionMessageService.submitGoal(input),
|
|
1078
|
+
emitRun: (...args) => this.runEventBridge.emitRun(...args),
|
|
1079
|
+
present: (session) => this.sessionPresentationService.present(session),
|
|
1080
|
+
emitSession: (session) => this.emitSessionWorkbenchEvent({
|
|
1081
|
+
session: this.sessionPresentationService.present(session)
|
|
1082
|
+
}),
|
|
1083
|
+
database: () => this.database
|
|
1084
|
+
});
|
|
1085
|
+
this.agentNetworkProjection = new AgentNetworkRunProjection(this.runtime, this.agentNetworkObservations);
|
|
1101
1086
|
this.agentNetworkService = createNodeAgentNetwork({
|
|
1102
1087
|
runtime: this.runtime,
|
|
1103
1088
|
database: () => this.database,
|
|
@@ -1110,27 +1095,9 @@ export class NodeConnector {
|
|
|
1110
1095
|
goals: this.goalRuntime,
|
|
1111
1096
|
runs: this.runWorkbenchService,
|
|
1112
1097
|
observations: this.agentNetworkObservations,
|
|
1113
|
-
ensureObservations:
|
|
1114
|
-
const existing = this.agentNetworkObservations.get(actor.observationId ?? actor.sessionId);
|
|
1115
|
-
if (existing)
|
|
1116
|
-
return existing;
|
|
1117
|
-
const run = actor.runId === undefined ? undefined : this.runtime.getRun(actor.runId);
|
|
1118
|
-
if (!run ||
|
|
1119
|
-
run.sessionId !== actor.sessionId ||
|
|
1120
|
-
this.runtime.activeSessionRun(actor.sessionId)?.id !== run.id)
|
|
1121
|
-
throw new Error('AGENT_ACTOR_NOT_ACTIVE');
|
|
1122
|
-
const observer = this.agentNetworkObservations.attach(actor.sessionId);
|
|
1123
|
-
this.transientAgentObservers.set(actor.sessionId, run.id);
|
|
1124
|
-
return observer;
|
|
1125
|
-
},
|
|
1098
|
+
ensureObservations: this.agentNetworkProjection.ensureObservations,
|
|
1126
1099
|
withGrant: (actor, work) => this.modelUsage.withGrant(actor.usageGrant, work)
|
|
1127
1100
|
});
|
|
1128
|
-
this.operationRouter.register('agent.network', agentNetworkExecutionCommand({
|
|
1129
|
-
runtime: this.runtime,
|
|
1130
|
-
coordinator: this.continuousRunCoordinator,
|
|
1131
|
-
service: this.agentNetworkService,
|
|
1132
|
-
observations: this.agentNetworkObservations
|
|
1133
|
-
}));
|
|
1134
1101
|
const inputState = (entry, message) => this.marAgentClient.restartInputState(entry.session.id, message.clientMessageId);
|
|
1135
1102
|
const restorer = new RestartSessionRestorer({
|
|
1136
1103
|
runtime: this.runtime,
|
|
@@ -1187,46 +1154,7 @@ export class NodeConnector {
|
|
|
1187
1154
|
restore: (entry, id) => restorer.restore(entry, id),
|
|
1188
1155
|
diagnose: (code, sessionId, errorCode) => nodeLog('node.restart.diagnostic', { code, sessionId, errorCode })
|
|
1189
1156
|
});
|
|
1190
|
-
|
|
1191
|
-
resolveExecutionDirectory,
|
|
1192
|
-
database: () => {
|
|
1193
|
-
if (this.database === undefined)
|
|
1194
|
-
throw new Error('NODE_DATABASE_UNAVAILABLE');
|
|
1195
|
-
return this.database;
|
|
1196
|
-
},
|
|
1197
|
-
nodeId: () => this.config?.nodeId ?? 'recorded-session',
|
|
1198
|
-
runtime: this.runtime,
|
|
1199
|
-
runners: this.runners,
|
|
1200
|
-
runnerAvailable: (runner) => this.runners.require(runner).available(this.capabilities),
|
|
1201
|
-
projection: this.nativeSessionProjectionService,
|
|
1202
|
-
cachedNative: () => this.nativeSessionProjectionService.cachedNative(),
|
|
1203
|
-
cachedWorkspace: (workspaceId) => this.workspaceCoordinator.cachedSessions(workspaceId),
|
|
1204
|
-
discoveryPending: (workspaceId) => this.workspaceSessionService.discoveryPending(workspaceId),
|
|
1205
|
-
discoverWorkspace: (workspaceId, cleanup) => this.nativeSessionProjectionService.discoverWorkspace(workspaceId, undefined, cleanup),
|
|
1206
|
-
present: (session) => this.sessionPresentationService.present(session),
|
|
1207
|
-
nativeIds: (session) => this.nativeSessionProjectionService.nativeIds(session),
|
|
1208
|
-
forget: (session, nativeIds) => {
|
|
1209
|
-
this.nativeSessionProjectionService.forget(session, nativeIds);
|
|
1210
|
-
this.promptCompletion.invalidateSession(session.workspaceId, session.id);
|
|
1211
|
-
void this.runAttachmentService.releaseSession(session.id);
|
|
1212
|
-
},
|
|
1213
|
-
refreshActivity: (session) => this.conversationHistoryService.refreshExternalActivity(session),
|
|
1214
|
-
externalActivity: (sessionId) => this.externalSessionResumeService.activity(sessionId),
|
|
1215
|
-
isWatching: (sessionId) => this.nativeSessionWatchService.isWatching(sessionId),
|
|
1216
|
-
isRecovering: (sessionId) => this.restartHandoff.blocks(sessionId),
|
|
1217
|
-
history: (session, input) => this.conversationHistoryService.read(session, input),
|
|
1218
|
-
invalidateHistory: (sessionId, workspaceId) => {
|
|
1219
|
-
this.nativeSessionWatchService.invalidate(sessionId);
|
|
1220
|
-
this.promptCompletion.invalidateSession(workspaceId, sessionId);
|
|
1221
|
-
},
|
|
1222
|
-
invalidateWorkspaceSessions: (workspaceId) => {
|
|
1223
|
-
this.workspaceSessionService.invalidateCache(workspaceId);
|
|
1224
|
-
void this.workspaceWorkbenchService?.invalidateTopic(workspaceId, 'sessions');
|
|
1225
|
-
},
|
|
1226
|
-
emitSession: (session) => this.emitSessionWorkbenchEvent({
|
|
1227
|
-
session: this.sessionPresentationService.present(session)
|
|
1228
|
-
})
|
|
1229
|
-
});
|
|
1157
|
+
// Connect Runner control feedback after Session owners and Workspace resources exist.
|
|
1230
1158
|
this.codexClient.bindControl({
|
|
1231
1159
|
start: (envelope) => this.codexRunController.start(envelope),
|
|
1232
1160
|
cancel: (runId, intent) => {
|
|
@@ -1428,11 +1356,7 @@ export class NodeConnector {
|
|
|
1428
1356
|
terminals: this.terminalManager,
|
|
1429
1357
|
resources: this.workspaceResources,
|
|
1430
1358
|
changes: this.workspaceChangeService,
|
|
1431
|
-
listSessions: (workspaceId, sessionGroup) => this.
|
|
1432
|
-
workspaceId,
|
|
1433
|
-
limit: 30,
|
|
1434
|
-
...(sessionGroup === undefined ? {} : { sessionGroup })
|
|
1435
|
-
}),
|
|
1359
|
+
listSessions: (workspaceId, sessionGroup) => this.workspaceSessionService.listPage(workspaceId, sessionGroup),
|
|
1436
1360
|
sessionNavigation: async (workspaceId, query) => this.sessionCatalogService.navigation({ workspaceId, ...query }),
|
|
1437
1361
|
invalidatePrompt: (id) => this.promptCompletion?.invalidateWorkspace(id),
|
|
1438
1362
|
nodeId: () => this.config?.nodeId ?? 'runtime-node',
|
|
@@ -1445,6 +1369,28 @@ export class NodeConnector {
|
|
|
1445
1369
|
this.workspaceService = workspaceApplication.workspace;
|
|
1446
1370
|
this.workspaceWorkbenchService = workspaceApplication.workbench;
|
|
1447
1371
|
this.publishWorkspaceSession = workspaceApplication.publishSession;
|
|
1372
|
+
// Connect normalized event projection and terminal feedback; no execution starts here.
|
|
1373
|
+
this.runEventService = new RunEventService({
|
|
1374
|
+
runtime: this.runtime,
|
|
1375
|
+
metrics: this.metrics,
|
|
1376
|
+
isBootstrapSession: (sessionId) => this.sessionIdentityService.bootstrapIds.has(sessionId),
|
|
1377
|
+
presentSession: (session) => this.sessionPresentationService.present(session),
|
|
1378
|
+
rejectBootstrap: (sessionId, code) => this.sessionIdentityService.reject(sessionId, code),
|
|
1379
|
+
emitWorkbench: (category, payload) => this.publishWorkbenchEvent(category, payload),
|
|
1380
|
+
cleanupAttachments: (runId) => this.runAttachmentService.cleanup(runId),
|
|
1381
|
+
refreshAttachments: (sessionId) => this.runAttachmentService.refreshSession(sessionId),
|
|
1382
|
+
expireImages: (runId) => this.runAttachmentService.expire(runId),
|
|
1383
|
+
scheduleContextRefresh: (sessionId) => this.sessionContextService.schedule(sessionId),
|
|
1384
|
+
refreshWorkspaceSessions: (workspaceId) => this.workspaceWorkbenchService.ensureTopicSnapshot(workspaceId, 'sessions'),
|
|
1385
|
+
onTerminalRun: (run, status) => this.executionLifecycle.terminal(run, status),
|
|
1386
|
+
observeAgentEvent: this.agentNetworkProjection.accept,
|
|
1387
|
+
dispatchQueue: (sessionId, delayMs) => this.executionLifecycle.dispatchQueue(sessionId, delayMs),
|
|
1388
|
+
executionStarted: (executionId) => this.continuousRunCoordinator.executionStarted(executionId),
|
|
1389
|
+
acceptExecutionTerminal: (input) => this.continuousRunCoordinator.acceptExecutionTerminal(input),
|
|
1390
|
+
publishGoalUpdate: ({ session, goal }) => this.goalCoordinator.publishGoalState(session, goal),
|
|
1391
|
+
publishQueueChange: (sessionId) => this.continuousRunCoordinator.publish(sessionId),
|
|
1392
|
+
releaseExecution: (executionId) => this.executionLifecycle.release(executionId)
|
|
1393
|
+
});
|
|
1448
1394
|
this.runEventBridge = new NodeRunEventBridge({
|
|
1449
1395
|
bindModelUsage: (runId) => {
|
|
1450
1396
|
const run = this.runtime.getRun(runId);
|
|
@@ -1497,62 +1443,22 @@ export class NodeConnector {
|
|
|
1497
1443
|
return this.database;
|
|
1498
1444
|
};
|
|
1499
1445
|
this.scheduledRunAuthorization = new RunAuthorizationControl((envelope) => this.controlChannel.sendEnvelope(envelope));
|
|
1446
|
+
const scheduledExecution = new ScheduledExecutionService({
|
|
1447
|
+
runtime: this.runtime,
|
|
1448
|
+
projection: this.nativeSessionProjectionService,
|
|
1449
|
+
authorization: this.scheduledRunAuthorization,
|
|
1450
|
+
runners: this.runners,
|
|
1451
|
+
git: this.gitOperationService,
|
|
1452
|
+
messages: this.sessionMessageService
|
|
1453
|
+
});
|
|
1500
1454
|
this.scheduledSessions = new ScheduledSessionCoordinator({
|
|
1501
1455
|
store: () => new ScheduledSessionStore(requireDatabase().raw),
|
|
1502
1456
|
busy: (task) => this.stopped ||
|
|
1503
1457
|
this.restartHandoff.blocks(task.sessionId) ||
|
|
1504
1458
|
this.runtime.activeSessionRun(task.sessionId) !== undefined ||
|
|
1505
1459
|
this.runtime.sessionQueue(task.sessionId).itemIds.length > 0,
|
|
1506
|
-
authorize:
|
|
1507
|
-
|
|
1508
|
-
(await this.nativeSessionProjectionService.resolveCurrent(task.sessionId));
|
|
1509
|
-
if (session === undefined ||
|
|
1510
|
-
session.workspaceId !== task.workspaceId ||
|
|
1511
|
-
session.runner !== task.runner)
|
|
1512
|
-
throw new Error('SCHEDULE_SESSION_NOT_FOUND');
|
|
1513
|
-
const configuredSession = this.nativeSessionProjectionService.ensureResumedConfiguration(session);
|
|
1514
|
-
const context = await this.scheduledRunAuthorization.authorize({
|
|
1515
|
-
taskId: task.id,
|
|
1516
|
-
revision: task.revision,
|
|
1517
|
-
occurrenceId,
|
|
1518
|
-
sessionId: task.sessionId,
|
|
1519
|
-
workspaceId: task.workspaceId,
|
|
1520
|
-
ownerUserId: task.ownerUserId,
|
|
1521
|
-
runner: task.runner,
|
|
1522
|
-
modelId: configuredSession.modelId
|
|
1523
|
-
});
|
|
1524
|
-
return {
|
|
1525
|
-
...(context.gitConfiguration === undefined
|
|
1526
|
-
? {}
|
|
1527
|
-
: { gitConfiguration: context.gitConfiguration }),
|
|
1528
|
-
secretEnvironment: context.secretEnvironment,
|
|
1529
|
-
personalInstructions: context.personalInstructions,
|
|
1530
|
-
usageGrant: context.usageGrant,
|
|
1531
|
-
executionContext: this.runners.require(task.runner).prepareExecutionContext(context)
|
|
1532
|
-
};
|
|
1533
|
-
},
|
|
1534
|
-
submit: async (task, occurrenceId, context) => {
|
|
1535
|
-
const result = (await this.gitOperationService.execute('session.message', {
|
|
1536
|
-
sessionId: task.sessionId,
|
|
1537
|
-
workspaceId: task.workspaceId,
|
|
1538
|
-
__requestUserId: task.ownerUserId,
|
|
1539
|
-
secretEnvironment: context.secretEnvironment,
|
|
1540
|
-
...(context.gitConfiguration === undefined
|
|
1541
|
-
? {}
|
|
1542
|
-
: { gitConfiguration: context.gitConfiguration })
|
|
1543
|
-
}, async (data) => this.sessionMessageService.submitScheduled({
|
|
1544
|
-
sessionId: task.sessionId,
|
|
1545
|
-
occurrenceId,
|
|
1546
|
-
objective: task.objective,
|
|
1547
|
-
initiatedByUserId: task.ownerUserId,
|
|
1548
|
-
secretEnvironment: data
|
|
1549
|
-
.secretEnvironment,
|
|
1550
|
-
personalInstructions: context.personalInstructions,
|
|
1551
|
-
...(context.usageGrant === undefined ? {} : { usageGrant: context.usageGrant }),
|
|
1552
|
-
executionContext: context.executionContext
|
|
1553
|
-
})));
|
|
1554
|
-
return { runId: result.run.id };
|
|
1555
|
-
},
|
|
1460
|
+
authorize: scheduledExecution.authorize,
|
|
1461
|
+
submit: scheduledExecution.submit,
|
|
1556
1462
|
sessionTitle: (sessionId) => {
|
|
1557
1463
|
const session = this.runtime.getAgentSession(sessionId);
|
|
1558
1464
|
return session?.customTitle ?? session?.runnerTitle ?? sessionId;
|
|
@@ -1562,6 +1468,8 @@ export class NodeConnector {
|
|
|
1562
1468
|
{}),
|
|
1563
1469
|
emitNotification: (payload) => this.emitWorkbenchEvent('conversation', payload)
|
|
1564
1470
|
});
|
|
1471
|
+
this.executionLifecycle = new ExecutionLifecycle(() => this.database !== undefined, this.scheduledSessions, this.continuousRunCoordinator, this.goalCoordinator, this.modelUsage, this.goalMcpBridge, this.scheduleMcpBridge, this.notificationMcpBridge, this.runEventBridge, this.upgradeCoordinator);
|
|
1472
|
+
// Register public operations only after domain workflows are connected.
|
|
1565
1473
|
this.runnerService = new RunnerService(requireDatabase, () => this.capabilities, this.runners, this.runnerUsageReader);
|
|
1566
1474
|
this.workbenchManifestService = new WorkbenchManifestService({
|
|
1567
1475
|
capabilities: () => this.capabilities,
|
|
@@ -1616,6 +1524,12 @@ export class NodeConnector {
|
|
|
1616
1524
|
readPage: (input, actor, signal) => completionWorkspace.readPage(input, actor, signal),
|
|
1617
1525
|
references: (workspaceId, _actor, signal) => completionWorkspace.references(workspaceId, signal)
|
|
1618
1526
|
});
|
|
1527
|
+
this.operationRouter.register('agent.network', agentNetworkExecutionCommand({
|
|
1528
|
+
runtime: this.runtime,
|
|
1529
|
+
coordinator: this.continuousRunCoordinator,
|
|
1530
|
+
service: this.agentNetworkService,
|
|
1531
|
+
observations: this.agentNetworkObservations
|
|
1532
|
+
}));
|
|
1619
1533
|
this.operationRouter.registerAll(this.promptCompletion.operations());
|
|
1620
1534
|
for (const operation of CONVERSATION_TEMPLATE_OPERATIONS)
|
|
1621
1535
|
this.operationRouter.register(operation, (data) => {
|
|
@@ -1704,8 +1618,35 @@ export class NodeConnector {
|
|
|
1704
1618
|
manifest: await this.workbenchManifestService.read(),
|
|
1705
1619
|
binaryTerminal: true
|
|
1706
1620
|
}));
|
|
1707
|
-
this.executionLifecycle = new ExecutionLifecycle(() => this.database !== undefined, this.scheduledSessions, this.continuousRunCoordinator, this.goalCoordinator, this.modelUsage, this.goalMcpBridge, this.scheduleMcpBridge, this.notificationMcpBridge, this.runEventBridge, this.upgradeCoordinator);
|
|
1708
1621
|
this.operationAdmission = new NodeOperationAdmission(() => this.stopped, () => this.database !== undefined, () => this.config?.nodeId, this.restartHandoff, this.upgradeCoordinator, this.gitOperationService, this.operationRouter);
|
|
1622
|
+
// Lifecycle is connected last; start() is the only resource activation boundary.
|
|
1623
|
+
this.lifecycle = new NodeLifecycle(() => this.startResources(), () => {
|
|
1624
|
+
this.continuousRunCoordinator.suspend();
|
|
1625
|
+
this.goalCoordinator.freeze();
|
|
1626
|
+
this.scheduledSessions.stop();
|
|
1627
|
+
void this.workspaceSessionService.stop();
|
|
1628
|
+
}, {
|
|
1629
|
+
waitForRecovery: () => this.restartHandoff.wait(),
|
|
1630
|
+
drainInput: async () => {
|
|
1631
|
+
this.sessionMessageService.setAccepting(false);
|
|
1632
|
+
await this.operationAdmission.drain();
|
|
1633
|
+
await this.sessionMessageService.drain();
|
|
1634
|
+
await this.workspaceSessionService.stop();
|
|
1635
|
+
},
|
|
1636
|
+
closeResume: () => this.externalSessionResumeService.close(),
|
|
1637
|
+
capture: () => {
|
|
1638
|
+
if (!this.config?.nodeId)
|
|
1639
|
+
return;
|
|
1640
|
+
return this.restartHandoff.capture();
|
|
1641
|
+
},
|
|
1642
|
+
dispose: () => this.disposeRuntime(),
|
|
1643
|
+
publish: (snapshot) => this.restartHandoff.publish(snapshot),
|
|
1644
|
+
diagnose: (stage, error) => nodeLog('node.shutdown.failed', {
|
|
1645
|
+
stage,
|
|
1646
|
+
errorCode: safeErrorCode(error, 'NODE_SHUTDOWN_FAILED'),
|
|
1647
|
+
...(stage === 'capture' ? plannedRestartCaptureDiagnostic(error) : undefined)
|
|
1648
|
+
})
|
|
1649
|
+
});
|
|
1709
1650
|
}
|
|
1710
1651
|
projectComputerUseCapability(capabilities) {
|
|
1711
1652
|
return {
|
|
@@ -1717,6 +1658,7 @@ export class NodeConnector {
|
|
|
1717
1658
|
return this.lifecycle.start();
|
|
1718
1659
|
}
|
|
1719
1660
|
async startResources() {
|
|
1661
|
+
this.workspaceSessionService.resume();
|
|
1720
1662
|
this.continuousRunCoordinator.resumeScheduling();
|
|
1721
1663
|
this.sessionMessageService.setAccepting(true);
|
|
1722
1664
|
this.config ??= await loadNodeConfig(this.configPath);
|
|
@@ -1820,6 +1762,8 @@ export class NodeConnector {
|
|
|
1820
1762
|
return this.lifecycle.stop(options.handoff === true);
|
|
1821
1763
|
}
|
|
1822
1764
|
disposeRuntime() {
|
|
1765
|
+
// Earlier shutdown stages may fail; disposal must still join internal reads before DB release.
|
|
1766
|
+
const workspaceSessionDrain = this.workspaceSessionService.stop();
|
|
1823
1767
|
this.recentRunProjectionService.dispose();
|
|
1824
1768
|
this.upgradeCoordinator.stop();
|
|
1825
1769
|
this.connectionLifecycle.stop();
|
|
@@ -1831,6 +1775,7 @@ export class NodeConnector {
|
|
|
1831
1775
|
this.gitOperationService.clear();
|
|
1832
1776
|
this.goalCoordinator.dispose();
|
|
1833
1777
|
this.runEventService.clear();
|
|
1778
|
+
this.agentNetworkProjection.dispose();
|
|
1834
1779
|
this.sessionContextService.dispose();
|
|
1835
1780
|
this.runAttachmentService.dispose();
|
|
1836
1781
|
this.modelResourcePublicationService.dispose();
|
|
@@ -1863,6 +1808,7 @@ export class NodeConnector {
|
|
|
1863
1808
|
this.controlChannel.stop();
|
|
1864
1809
|
const database = this.database;
|
|
1865
1810
|
const drains = Promise.allSettled([
|
|
1811
|
+
workspaceSessionDrain,
|
|
1866
1812
|
scheduledDrain,
|
|
1867
1813
|
marAgentDrain,
|
|
1868
1814
|
runnerDrain,
|
package/dist/marctl/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"type":"module","version":"0.12.
|
|
1
|
+
{"type":"module","version":"0.12.5"}
|
|
@@ -129,7 +129,8 @@ const collaborationPrompt = [
|
|
|
129
129
|
"Use native agent_* tools to collaborate within the Agent Network when another Session's context or ongoing work materially helps. Prefer focused requests and avoid unnecessary cross-Session coordination. Peer Sessions retain their own work and lifecycle; do not treat them as task-owned children or stop them merely for task cleanup.",
|
|
130
130
|
'Prefer reusing known Session IDs rather than rediscovering them. Updates are most useful when requested or when the recipient needs to decide or change course; prefer combining related updates.',
|
|
131
131
|
'An accepted message normally needs no separate acknowledgment. Requests for receipt-only replies and immediate agent_view calls solely to confirm receipt are usually unnecessary.',
|
|
132
|
-
'
|
|
132
|
+
'The parent automatically collects the latest unread public reply and terminal state from each observed direct child before its next natural model request. Usually reply in the child Session itself rather than sending duplicate progress or completion reports with agent_send.',
|
|
133
|
+
'Contact the parent proactively when timely action or a decision is needed, such as a blocker, scope conflict or major risk, or when explicitly asked to report. Routine child updates usually need no reply; prefer combining related guidance.'
|
|
133
134
|
].join('\n\n');
|
|
134
135
|
const managementPrompt = [
|
|
135
136
|
'# Workspace management',
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { agentObservationFromRunEvent } from './agent-network-observations.js';
|
|
2
|
+
import { marctlSessionDetail } from './session-recent-activity.js';
|
|
3
|
+
/** Projects normalized Run events and owns transient observers from attachment through terminal cleanup.
|
|
4
|
+
* Run-keyed handles survive Session identity promotion without borrowing a replacement's lifetime.
|
|
5
|
+
* Runner-owned context observers remain with the Runner; this owner only borrows their delivery port.
|
|
6
|
+
*/
|
|
7
|
+
export class AgentNetworkRunProjection {
|
|
8
|
+
runtime;
|
|
9
|
+
observations;
|
|
10
|
+
transient = new Map();
|
|
11
|
+
constructor(runtime, observations) {
|
|
12
|
+
this.runtime = runtime;
|
|
13
|
+
this.observations = observations;
|
|
14
|
+
}
|
|
15
|
+
ensureObservations = (actor) => {
|
|
16
|
+
const existing = this.observations.get(actor.observationId ?? actor.sessionId);
|
|
17
|
+
if (existing)
|
|
18
|
+
return existing;
|
|
19
|
+
const run = actor.runId === undefined ? undefined : this.runtime.getRun(actor.runId);
|
|
20
|
+
if (!run ||
|
|
21
|
+
run.sessionId !== actor.sessionId ||
|
|
22
|
+
this.runtime.activeSessionRun(actor.sessionId)?.id !== run.id)
|
|
23
|
+
throw new Error('AGENT_ACTOR_NOT_ACTIVE');
|
|
24
|
+
const observer = this.observations.attach(actor.sessionId);
|
|
25
|
+
this.transient.set(run.id, observer);
|
|
26
|
+
return observer;
|
|
27
|
+
};
|
|
28
|
+
accept = ({ run, executionId, eventType, payload, terminalStatus }) => {
|
|
29
|
+
const child = this.runtime.getAgentSession(run.sessionId);
|
|
30
|
+
if (!child)
|
|
31
|
+
return;
|
|
32
|
+
const parent = child.parentSessionId == null
|
|
33
|
+
? undefined
|
|
34
|
+
: this.runtime.getAgentSession(child.parentSessionId);
|
|
35
|
+
let observation = agentObservationFromRunEvent({ eventType, payload, terminalStatus });
|
|
36
|
+
if (eventType === 'approval.request') {
|
|
37
|
+
const approvals = marctlSessionDetail(this.runtime, child, 0).pendingApprovals;
|
|
38
|
+
if (approvals.length)
|
|
39
|
+
observation = { kind: 'message', value: JSON.stringify({ pendingApprovals: approvals }) };
|
|
40
|
+
}
|
|
41
|
+
if (observation)
|
|
42
|
+
this.observations.publishChild({
|
|
43
|
+
...(parent?.workspaceId === child.workspaceId ? { parentId: parent.id } : {}),
|
|
44
|
+
childId: child.id,
|
|
45
|
+
runId: run.id,
|
|
46
|
+
executionId,
|
|
47
|
+
...observation
|
|
48
|
+
});
|
|
49
|
+
if (terminalStatus !== undefined) {
|
|
50
|
+
const observer = this.transient.get(run.id);
|
|
51
|
+
this.transient.delete(run.id);
|
|
52
|
+
observer?.close();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
dispose() {
|
|
56
|
+
for (const observer of this.transient.values())
|
|
57
|
+
observer.close();
|
|
58
|
+
this.transient.clear();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -7,12 +7,23 @@ import { sessionPage } from './pagination.js';
|
|
|
7
7
|
import { latestSessionActivityAt } from './session-presentation-service.js';
|
|
8
8
|
import { marctlSessionSummary, sessionActivityStatus } from '../service/session-recent-activity.js';
|
|
9
9
|
import { WorkspaceSessionCleanupBudget, WorkspaceSessionCleanupDeadline } from '../service/workspace-session-cleanup-budget.js';
|
|
10
|
+
/** Owns catalog queries and Session catalog mutations; borrows the Workspace native cache.
|
|
11
|
+
* Public operations and internal observation share query rules, not transport/admission routing.
|
|
12
|
+
*/
|
|
10
13
|
export class SessionCatalogService {
|
|
11
14
|
options;
|
|
12
15
|
rewindQueue = Promise.resolve();
|
|
13
16
|
constructor(options) {
|
|
14
17
|
this.options = options;
|
|
15
18
|
}
|
|
19
|
+
/** Internal observation uses the same catalog query without public operation dispatch. */
|
|
20
|
+
listWorkspacePage(workspaceId, sessionGroup) {
|
|
21
|
+
return this.list({
|
|
22
|
+
workspaceId,
|
|
23
|
+
limit: 30,
|
|
24
|
+
...(sessionGroup === undefined ? {} : { sessionGroup })
|
|
25
|
+
});
|
|
26
|
+
}
|
|
16
27
|
operations() {
|
|
17
28
|
return {
|
|
18
29
|
'session.list': (data) => this.list(record(data)),
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { nodeLog } from '../operational.js';
|
|
2
2
|
const WATCH_INTERVAL_MS = 30_000;
|
|
3
3
|
const CACHE_TTL_MS = 15_000;
|
|
4
|
+
/** Owns native discovery caches, periodic observation and the lifetime of internal page reads. */
|
|
4
5
|
export class WorkspaceSessionService {
|
|
5
6
|
options;
|
|
7
|
+
accepting = true;
|
|
8
|
+
pageReads = new Set();
|
|
9
|
+
discoveryReads = new Set();
|
|
6
10
|
cacheGenerations = new Map();
|
|
7
11
|
initialReads = new Map();
|
|
8
12
|
incompleteDiscoveries = new Set();
|
|
@@ -15,10 +19,36 @@ export class WorkspaceSessionService {
|
|
|
15
19
|
constructor(options) {
|
|
16
20
|
this.options = options;
|
|
17
21
|
}
|
|
22
|
+
async listPage(workspaceId, sessionGroup) {
|
|
23
|
+
if (!this.accepting)
|
|
24
|
+
throw new Error('NODE_STOPPED');
|
|
25
|
+
const read = this.options.listPage(workspaceId, sessionGroup);
|
|
26
|
+
this.pageReads.add(read);
|
|
27
|
+
try {
|
|
28
|
+
return await read;
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
this.pageReads.delete(read);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
resume() {
|
|
35
|
+
this.accepting = true;
|
|
36
|
+
}
|
|
37
|
+
async stop() {
|
|
38
|
+
this.accepting = false;
|
|
39
|
+
for (const id of this.sessionWatchTimers.keys())
|
|
40
|
+
this.stopWatch(id);
|
|
41
|
+
await Promise.allSettled([...this.pageReads, ...this.sessionWatchReads.values()]);
|
|
42
|
+
// A first page can return a partial batch while native discovery still owns I/O.
|
|
43
|
+
// Retired cache generations also remain owned until their reads actually settle.
|
|
44
|
+
await Promise.allSettled([...this.discoveryReads]);
|
|
45
|
+
}
|
|
18
46
|
cachedSnapshot(workspaceId) {
|
|
19
47
|
return this.sessionCache.get(workspaceId)?.sessions ?? [];
|
|
20
48
|
}
|
|
21
49
|
ensureWatch(workspaceId) {
|
|
50
|
+
if (!this.accepting)
|
|
51
|
+
return;
|
|
22
52
|
if (this.sessionWatchTimers.has(workspaceId) || this.sessionWatchReads.has(workspaceId))
|
|
23
53
|
return;
|
|
24
54
|
void this.runWatchCycle(workspaceId);
|
|
@@ -66,7 +96,8 @@ export class WorkspaceSessionService {
|
|
|
66
96
|
sessions,
|
|
67
97
|
expiresAt: this.staleDiscoveries.has(workspaceId) ? 0 : Date.now() + CACHE_TTL_MS
|
|
68
98
|
});
|
|
69
|
-
if (
|
|
99
|
+
if (this.accepting &&
|
|
100
|
+
notify &&
|
|
70
101
|
previous !== undefined &&
|
|
71
102
|
sessionSignature(previous) !== sessionSignature(sessions))
|
|
72
103
|
this.options.invalidate(workspaceId);
|
|
@@ -109,6 +140,7 @@ export class WorkspaceSessionService {
|
|
|
109
140
|
throw error;
|
|
110
141
|
})
|
|
111
142
|
.finally(() => {
|
|
143
|
+
this.discoveryReads.delete(read);
|
|
112
144
|
acceptingBatches = false;
|
|
113
145
|
if (this.sessionCacheReads.get(workspaceId) === read) {
|
|
114
146
|
this.sessionCacheReads.delete(workspaceId);
|
|
@@ -118,7 +150,9 @@ export class WorkspaceSessionService {
|
|
|
118
150
|
}
|
|
119
151
|
if (this.initialReads.get(workspaceId) === initial)
|
|
120
152
|
this.initialReads.delete(workspaceId);
|
|
121
|
-
if (
|
|
153
|
+
if (this.accepting &&
|
|
154
|
+
notifyCompletion &&
|
|
155
|
+
this.cacheGenerations.get(workspaceId) === generation)
|
|
122
156
|
this.options.invalidate(workspaceId);
|
|
123
157
|
if (!this.sessionCacheReads.has(workspaceId) &&
|
|
124
158
|
!this.sessionCache.has(workspaceId) &&
|
|
@@ -126,6 +160,7 @@ export class WorkspaceSessionService {
|
|
|
126
160
|
this.cacheGenerations.delete(workspaceId);
|
|
127
161
|
});
|
|
128
162
|
discovery = read;
|
|
163
|
+
this.discoveryReads.add(read);
|
|
129
164
|
this.sessionCacheReads.set(workspaceId, discovery);
|
|
130
165
|
// Later Runner failures cannot become unhandled after the first batch was returned.
|
|
131
166
|
void discovery.catch(() => undefined);
|
|
@@ -176,7 +211,9 @@ export class WorkspaceSessionService {
|
|
|
176
211
|
}
|
|
177
212
|
async runWatchCycle(workspaceId) {
|
|
178
213
|
await this.refreshWatch(workspaceId);
|
|
179
|
-
if (!this.
|
|
214
|
+
if (!this.accepting ||
|
|
215
|
+
!this.options.active(workspaceId) ||
|
|
216
|
+
this.sessionWatchTimers.has(workspaceId))
|
|
180
217
|
return;
|
|
181
218
|
const timer = setTimeout(() => {
|
|
182
219
|
this.sessionWatchTimers.delete(workspaceId);
|
|
@@ -186,11 +223,11 @@ export class WorkspaceSessionService {
|
|
|
186
223
|
this.sessionWatchTimers.set(workspaceId, timer);
|
|
187
224
|
}
|
|
188
225
|
async performRefresh(workspaceId) {
|
|
189
|
-
if (!this.options.active(workspaceId))
|
|
226
|
+
if (!this.accepting || !this.options.active(workspaceId))
|
|
190
227
|
return this.stopWatch(workspaceId);
|
|
191
228
|
try {
|
|
192
|
-
const signature = JSON.stringify(await this.
|
|
193
|
-
if (!this.options.active(workspaceId))
|
|
229
|
+
const signature = JSON.stringify(await this.listPage(workspaceId));
|
|
230
|
+
if (!this.accepting || !this.options.active(workspaceId))
|
|
194
231
|
return this.stopWatch(workspaceId);
|
|
195
232
|
const previous = this.sessionWatchSignatures.get(workspaceId);
|
|
196
233
|
this.sessionWatchSignatures.set(workspaceId, signature);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myagentroam/node",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.5",
|
|
4
4
|
"description": "MyAgentRoam Node runtime CLI.",
|
|
5
5
|
"license": "Apache-2.0 AND GPL-2.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -42,14 +42,14 @@
|
|
|
42
42
|
"turndown-plugin-gfm": "^1.0.2",
|
|
43
43
|
"ws": "^8.21.3",
|
|
44
44
|
"zod": "4.4.3",
|
|
45
|
-
"@myagentroam/agent": "0.12.
|
|
46
|
-
"@myagentroam/protocol": "0.12.
|
|
45
|
+
"@myagentroam/agent": "0.12.5",
|
|
46
|
+
"@myagentroam/protocol": "0.12.5"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/turndown": "^5.0.6",
|
|
50
50
|
"@types/ws": "^8.18.1",
|
|
51
|
-
"@myagentroam/
|
|
52
|
-
"@myagentroam/
|
|
51
|
+
"@myagentroam/code-navigation": "0.11.1",
|
|
52
|
+
"@myagentroam/marctl": "0.12.5"
|
|
53
53
|
},
|
|
54
54
|
"exports": {
|
|
55
55
|
"./config": {
|