@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.
@@ -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, agentObservationFromRunEvent } from './service/agent-network-observations.js';
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 { marctlSessionDetail, marctlSessionProgress } from './service/session-recent-activity.js';
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 = new NodeLifecycle(() => this.startResources(), () => {
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 = composeWorkspaceResources({
327
- discover: (workspaceId, onRunnerDiscovered) => this.discoverWorkspaceSessions(workspaceId, onRunnerDiscovered),
328
- listPage: (workspaceId) => this.executeNodeOperation('session.list', { workspaceId, limit: 30 }),
329
- invalidateSessions: (workspaceId) => void this.workspaceWorkbenchService?.invalidateTopic(workspaceId, 'sessions'),
330
- invalidateFiles: (workspaceId) => {
331
- this.workspaceWorkbenchService?.invalidateWorkspaceFiles(workspaceId);
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 = new GoalContinuationCoordinator({
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
- transientAgentObservers = new Map();
372
- runEventService = new 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 = new 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: (actor) => {
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
- this.sessionCatalogService = new SessionCatalogService({
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.executeNodeOperation('session.list', {
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: async (task, occurrenceId) => {
1507
- const session = this.runtime.getAgentSession(task.sessionId) ??
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,
@@ -1 +1 @@
1
- {"type":"module","version":"0.12.4"}
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
- 'For delegated work, prefer a concise final handoff over routine progress reports or duplicate completion messages. Early contact is most useful for requested updates, blockers, scope conflicts, or information needed for a decision.'
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 (notify &&
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 (notifyCompletion && this.cacheGenerations.get(workspaceId) === generation)
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.options.active(workspaceId) || this.sessionWatchTimers.has(workspaceId))
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.options.listPage(workspaceId));
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.4",
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.4",
46
- "@myagentroam/protocol": "0.12.4"
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/marctl": "0.12.4",
52
- "@myagentroam/code-navigation": "0.11.1"
51
+ "@myagentroam/code-navigation": "0.11.1",
52
+ "@myagentroam/marctl": "0.12.5"
53
53
  },
54
54
  "exports": {
55
55
  "./config": {