@myagentroam/node 0.10.68 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/connector.js CHANGED
@@ -87,7 +87,8 @@ import { NodeControlChannel } from './service/node-control-channel.js';
87
87
  import { NodeControlMessageService } from './service/node-control-message-service.js';
88
88
  import { NodeRequestService } from './service/node-request-service.js';
89
89
  import { WorkbenchManifestService } from './service/workbench-manifest-service.js';
90
- import { NodeRunEventBridge, workbenchPayloadSessionId } from './service/node-run-event-bridge.js';
90
+ import { NodeRunEventBridge } from './service/node-run-event-bridge.js';
91
+ import { workbenchPayloadSessionId } from './service/workbench-event-projection.js';
91
92
  import { NodeWorkspaceCoordinator } from './service/node-workspace-coordinator.js';
92
93
  import { FakeManagedRunService } from './service/fake-managed-run-service.js';
93
94
  import { RunnerChannelMessageService } from './service/runner-channel-message-service.js';
@@ -203,9 +204,18 @@ export class NodeConnector {
203
204
  configPath;
204
205
  reconnect;
205
206
  claudeApprovalTimeoutMs;
206
- workbenchEventService = new WorkbenchEventService((envelope) => this.sendEnvelope(envelope), (category, payload) => {
207
- const sessionId = workbenchPayloadSessionId(this.runtime, category, payload);
208
- return sessionId === undefined || !this.sessionIdentityService.bootstrapIds.has(sessionId);
207
+ workbenchEventService = new WorkbenchEventService({
208
+ send: (envelope) => this.sendEnvelope(envelope),
209
+ accepts: (category, payload) => {
210
+ const sessionId = workbenchPayloadSessionId((id) => this.runtime.getRun(id), category, payload);
211
+ return sessionId === undefined || !this.sessionIdentityService.bootstrapIds.has(sessionId);
212
+ },
213
+ projection: {
214
+ getRun: (id) => this.runtime.getRun(id),
215
+ conversationTurnForRun: (id) => this.runtime.conversationTurnForRun(id),
216
+ progress: (sessionId) => marctlSessionProgress(this.runtime, sessionId)
217
+ },
218
+ emitRealtime: (sessionId, event) => this.nativeSessionWatchService.emitRealtime(sessionId, event)
209
219
  });
210
220
  fakeRunner;
211
221
  fakeScript;
@@ -1146,8 +1156,10 @@ export class NodeConnector {
1146
1156
  nodeLog('node.connection.replaced', {
1147
1157
  message: 'This Node connection was replaced by a newer instance with the same identity. Check for duplicate processes.'
1148
1158
  });
1149
- if (code === 4003 || code === 1001)
1150
- void this.terminalManager.shutdown();
1159
+ // Credential revocation ends terminals, but the control channel can still reconnect.
1160
+ // Only final Node disposal shuts down the manager and its idle collector.
1161
+ if (code === 4003)
1162
+ void this.terminalManager.closeAll();
1151
1163
  }
1152
1164
  });
1153
1165
  this.controlMessages = new NodeControlMessageService({
@@ -1329,6 +1341,7 @@ export class NodeConnector {
1329
1341
  this.workspaceCoordinator.invalidateChanges(workspaceId);
1330
1342
  this.workspaceSessionService.invalidate(workspaceId);
1331
1343
  this.workspaceCoordinator.clearWatches(workspaceId);
1344
+ this.workspaceWorkbenchService.clearWorkspace(workspaceId);
1332
1345
  this.promptCompletion?.invalidateWorkspace(workspaceId);
1333
1346
  });
1334
1347
  this.workspaceWorkbenchService = new WorkspaceWorkbenchService({
@@ -1717,24 +1730,7 @@ export class NodeConnector {
1717
1730
  }
1718
1731
  this.emitWorkbenchEvent('session', payload);
1719
1732
  };
1720
- emitWorkbenchEvent = (category, payload) => {
1721
- payload = this.runEventBridge.projectWorkbenchPayload(category, payload);
1722
- if (payload === undefined)
1723
- return;
1724
- const event = this.workbenchEventService.next(category, payload);
1725
- if (event === undefined)
1726
- return;
1727
- const notification = category === 'conversation' &&
1728
- isPlainRecord(payload) &&
1729
- isPlainRecord(payload.event) &&
1730
- payload.event.eventType === 'notification';
1731
- const sessionId = workbenchPayloadSessionId(this.runtime, category, payload);
1732
- if (!notification &&
1733
- sessionId !== undefined &&
1734
- this.nativeSessionWatchService.emitRealtime(sessionId, event))
1735
- return;
1736
- this.workbenchEventService.publish(event);
1737
- };
1733
+ emitWorkbenchEvent = (category, payload) => this.workbenchEventService.dispatch(category, payload);
1738
1734
  discoverWorkspaceSessions = (workspaceId, onRunnerDiscovered) => this.nativeSessionProjectionService.discoverWorkspace(workspaceId, onRunnerDiscovered);
1739
1735
  invalidateWorkspaceWatchTopics = (workspaceId) => this.workspaceCoordinator.invalidate(workspaceId);
1740
1736
  reconcilePersistedRuns = () => undefined;
@@ -277,13 +277,13 @@ export class OpenCodeManagedRunController {
277
277
  ? existingImagePaths
278
278
  : await this.host.persistAttachments(input.cwd, input.runId, input.attachments);
279
279
  const execution = this.runner.execution.get(input.runId);
280
- if (execution !== undefined) {
281
- execution.responseLatency = new ResponseLatencyTracker();
282
- execution.responsePartKinds = new Map();
283
- execution.responseStartedAt = Date.now();
284
- execution.responseMessages = new Set();
285
- execution.generationTiming = new GenerationTimingTracker();
286
- }
280
+ if (execution?.abort !== abort || abort.signal.aborted)
281
+ return;
282
+ execution.responseLatency = new ResponseLatencyTracker();
283
+ execution.responsePartKinds = new Map();
284
+ execution.responseStartedAt = Date.now();
285
+ execution.responseMessages = new Set();
286
+ execution.generationTiming = new GenerationTimingTracker();
287
287
  await client.prompt({
288
288
  directory: input.cwd,
289
289
  sessionID: nativeSessionId,
@@ -198,6 +198,8 @@ export class ContinuousRunCoordinator {
198
198
  return 'ACCEPTED';
199
199
  }
200
200
  if (startsAcceptedRun) {
201
+ if (this.options.runtime.sessionQueue(sessionId).version !== queueVersionBeforeSettle)
202
+ this.publish(sessionId);
201
203
  this.schedule(sessionId);
202
204
  return 'STARTED';
203
205
  }
@@ -1,122 +1,9 @@
1
- import { conversationSegmentIdentityForItem, conversationSegments } from './conversation-segment-service.js';
2
- import { runConversationItemId } from '../runtime-state.js';
3
- import { marctlSessionProgress } from './session-recent-activity.js';
4
- import { isNodeAgentRun, isNodeConversationTurn, isPlainRecord } from '../runner/codex/conversation-parser.js';
5
- export function workbenchPayloadSessionId(runtime, category, payload) {
6
- if (!isPlainRecord(payload))
7
- return undefined;
8
- if (category === 'session') {
9
- if (isPlainRecord(payload.session)) {
10
- const id = payload.session.id;
11
- if (typeof id === 'string')
12
- return id;
13
- }
14
- return typeof payload.sessionId === 'string' ? payload.sessionId : undefined;
15
- }
16
- if (category === 'run' && isNodeAgentRun(payload.run))
17
- return payload.run.sessionId;
18
- if (category === 'conversation') {
19
- if (isNodeConversationTurn(payload.turn))
20
- return payload.turn.sessionId;
21
- if (typeof payload.sessionId === 'string')
22
- return payload.sessionId;
23
- if (isPlainRecord(payload.event)) {
24
- const runId = payload.event.runId;
25
- return typeof runId === 'string' ? runtime.getRun(runId)?.sessionId : undefined;
26
- }
27
- }
28
- if (category === 'queue') {
29
- const id = payload.sessionId;
30
- return typeof id === 'string' ? id : undefined;
31
- }
32
- return undefined;
33
- }
34
1
  /** Bridges process-local Run state to normalized control and Workbench events. */
35
2
  export class NodeRunEventBridge {
36
3
  options;
37
4
  constructor(options) {
38
5
  this.options = options;
39
6
  }
40
- /** The sole public projection boundary for normalized Run callbacks. */
41
- projectWorkbenchPayload(category, payload) {
42
- if (category === 'conversation' &&
43
- isPlainRecord(payload) &&
44
- isPlainRecord(payload.event) &&
45
- payload.event.eventType !== 'notification') {
46
- const runId = typeof payload.event.runId === 'string' ? payload.event.runId : undefined;
47
- const sequence = typeof payload.event.sequence === 'number' ? payload.event.sequence : undefined;
48
- const turn = runId === undefined ? undefined : this.options.runtime.conversationTurnForRun(runId);
49
- if (turn === undefined)
50
- return;
51
- const rawPayload = isPlainRecord(payload.event.payload) ? payload.event.payload : undefined;
52
- const text = payload.event.eventType === 'text.delta' && typeof rawPayload?.text === 'string'
53
- ? rawPayload.text
54
- : undefined;
55
- const nativeItemId = typeof rawPayload?.itemId === 'string'
56
- ? rawPayload.itemId
57
- : typeof rawPayload?.approvalId === 'string'
58
- ? `approval:${rawPayload.approvalId}`
59
- : undefined;
60
- const item = text !== undefined && sequence !== undefined
61
- ? turn.items.find((candidate) => candidate.kind === 'assistant_message' && candidate.revision === sequence)
62
- : nativeItemId === undefined
63
- ? undefined
64
- : turn.items.find((candidate) => candidate.id ===
65
- runConversationItemId(runId, nativeItemId, typeof rawPayload?.kind === 'string' ? rawPayload.kind : undefined));
66
- if (text !== undefined && item?.kind === 'assistant_message') {
67
- const segment = conversationSegmentIdentityForItem(turn, item.id, item.kind);
68
- payload = {
69
- sessionId: turn.sessionId,
70
- delta: {
71
- turnId: turn.id,
72
- segmentId: segment.id,
73
- segmentIndex: segment.index,
74
- runId: turn.runId,
75
- itemId: item.id,
76
- text,
77
- createdAt: typeof payload.event.createdAt === 'number' ? payload.event.createdAt : Date.now()
78
- }
79
- };
80
- }
81
- else if (item !== undefined) {
82
- const segment = conversationSegmentIdentityForItem(turn, item.id, item.kind);
83
- payload = {
84
- sessionId: turn.sessionId,
85
- runId: turn.runId,
86
- ...(['assistant_message', 'tool_call', 'command_execution', 'file_change'].includes(item.kind) &&
87
- (rawPayload?.append === undefined || item.status !== 'IN_PROGRESS')
88
- ? { progress: marctlSessionProgress(this.options.runtime, turn.sessionId) }
89
- : {}),
90
- item: {
91
- turnId: turn.id,
92
- itemId: item.id,
93
- kind: item.kind,
94
- status: item.status,
95
- payload: item.payload,
96
- startedAt: item.startedAt,
97
- completedAt: item.completedAt,
98
- segmentId: segment.id,
99
- segmentIndex: segment.index
100
- }
101
- };
102
- }
103
- else {
104
- if (payload.event.eventType !== 'run.started' && payload.event.eventType !== 'run.running')
105
- return;
106
- payload = { turn };
107
- }
108
- }
109
- if (category === 'conversation' &&
110
- isPlainRecord(payload) &&
111
- isNodeConversationTurn(payload.turn))
112
- return { ...payload, segments: conversationSegments(payload.turn) };
113
- if (category === 'run' && isPlainRecord(payload) && isNodeAgentRun(payload.run))
114
- return {
115
- ...payload,
116
- progress: marctlSessionProgress(this.options.runtime, payload.run.sessionId)
117
- };
118
- return payload;
119
- }
120
7
  emitConversationItem(runId, item) {
121
8
  this.emitRun(runId, 'conversation.item', item);
122
9
  }
@@ -16,24 +16,22 @@ export class ScheduledSessionCoordinator {
16
16
  const store = this.options.store();
17
17
  store.finish(run.clientMessageId, status === 'SUCCEEDED' ? 'SUCCEEDED' : 'FAILED', status === 'SUCCEEDED' ? undefined : `SCHEDULE_RUN_${status}`, run.id);
18
18
  const task = store.getById(run.clientMessageId.split(':', 1)[0]);
19
- if (task !== undefined) {
20
- if (task.notifyOnTerminal)
21
- this.publishTaskNotification(task, run.clientMessageId, {
22
- kind: 'SCHEDULE_TERMINAL',
23
- outcome: status,
24
- runId: run.id
25
- });
26
- else if (status !== 'SUCCEEDED')
27
- this.notify(task, run.clientMessageId, `SCHEDULE_RUN_${status}`, 'FAILED');
28
- }
19
+ if (task !== undefined)
20
+ this.publishTaskNotification(task, run.clientMessageId, {
21
+ kind: 'SCHEDULE_TERMINAL',
22
+ outcome: status,
23
+ runId: run.id
24
+ });
29
25
  }
30
26
  notify = (task, occurrenceId, code, status) => this.publishTaskNotification(task, occurrenceId, {
31
27
  kind: status === 'FAILED' ? 'SCHEDULE_FAILED' : 'SCHEDULE_SKIPPED',
32
28
  code
33
29
  });
34
30
  publishTaskNotification = (task, occurrenceId, event) => {
31
+ const currentTask = this.options.store().getById(task.id) ?? task;
32
+ if (!currentTask.notifyOnTerminal)
33
+ return;
35
34
  const occurredAt = Date.now();
36
- const recipient = this.options.store().getById(task.id)?.ownerUserId ?? task.ownerUserId;
37
35
  this.options.emitNotification({
38
36
  event: {
39
37
  runId: event.kind === 'SCHEDULE_TERMINAL' ? event.runId : occurrenceId,
@@ -41,7 +39,7 @@ export class ScheduledSessionCoordinator {
41
39
  eventType: 'notification',
42
40
  payload: {
43
41
  targetType: 'WORKSPACE_SESSION',
44
- userId: recipient,
42
+ userId: currentTask.ownerUserId,
45
43
  workspaceId: task.workspaceId,
46
44
  sessionId: task.sessionId,
47
45
  sessionTitle: this.options.sessionTitle(task.sessionId),
@@ -0,0 +1,112 @@
1
+ import { conversationSegmentIdentityForItem, conversationSegments } from './conversation-segment-service.js';
2
+ import { runConversationItemId } from '../runtime-state.js';
3
+ import { isNodeAgentRun, isNodeConversationTurn, isPlainRecord } from '../runner/codex/conversation-parser.js';
4
+ export function workbenchPayloadSessionId(getRun, category, payload) {
5
+ if (!isPlainRecord(payload))
6
+ return undefined;
7
+ if (category === 'session') {
8
+ if (isPlainRecord(payload.session)) {
9
+ const id = payload.session.id;
10
+ if (typeof id === 'string')
11
+ return id;
12
+ }
13
+ return typeof payload.sessionId === 'string' ? payload.sessionId : undefined;
14
+ }
15
+ if (category === 'run' && isNodeAgentRun(payload.run))
16
+ return payload.run.sessionId;
17
+ if (category === 'conversation') {
18
+ if (isNodeConversationTurn(payload.turn))
19
+ return payload.turn.sessionId;
20
+ if (typeof payload.sessionId === 'string')
21
+ return payload.sessionId;
22
+ if (isPlainRecord(payload.event)) {
23
+ const runId = payload.event.runId;
24
+ return typeof runId === 'string' ? getRun(runId)?.sessionId : undefined;
25
+ }
26
+ }
27
+ if (category === 'queue') {
28
+ const id = payload.sessionId;
29
+ return typeof id === 'string' ? id : undefined;
30
+ }
31
+ return undefined;
32
+ }
33
+ /** Projects normalized runtime facts without publishing or mutating them. */
34
+ export function projectWorkbenchPayload(queries, category, payload) {
35
+ if (category === 'conversation' &&
36
+ isPlainRecord(payload) &&
37
+ isPlainRecord(payload.event) &&
38
+ payload.event.eventType !== 'notification') {
39
+ const runId = typeof payload.event.runId === 'string' ? payload.event.runId : undefined;
40
+ const sequence = typeof payload.event.sequence === 'number' ? payload.event.sequence : undefined;
41
+ const turn = runId === undefined ? undefined : queries.conversationTurnForRun(runId);
42
+ if (turn === undefined)
43
+ return;
44
+ const rawPayload = isPlainRecord(payload.event.payload) ? payload.event.payload : undefined;
45
+ const text = payload.event.eventType === 'text.delta' && typeof rawPayload?.text === 'string'
46
+ ? rawPayload.text
47
+ : undefined;
48
+ const nativeItemId = typeof rawPayload?.itemId === 'string'
49
+ ? rawPayload.itemId
50
+ : typeof rawPayload?.approvalId === 'string'
51
+ ? `approval:${rawPayload.approvalId}`
52
+ : undefined;
53
+ const item = text !== undefined && sequence !== undefined
54
+ ? turn.items.find((candidate) => candidate.kind === 'assistant_message' && candidate.revision === sequence)
55
+ : nativeItemId === undefined
56
+ ? undefined
57
+ : turn.items.find((candidate) => candidate.id ===
58
+ runConversationItemId(runId, nativeItemId, typeof rawPayload?.kind === 'string' ? rawPayload.kind : undefined));
59
+ if (text !== undefined && item?.kind === 'assistant_message') {
60
+ const segment = conversationSegmentIdentityForItem(turn, item.id, item.kind);
61
+ payload = {
62
+ sessionId: turn.sessionId,
63
+ delta: {
64
+ turnId: turn.id,
65
+ segmentId: segment.id,
66
+ segmentIndex: segment.index,
67
+ runId: turn.runId,
68
+ itemId: item.id,
69
+ text,
70
+ createdAt: typeof payload.event.createdAt === 'number'
71
+ ? payload.event.createdAt
72
+ : (queries.now ?? Date.now)()
73
+ }
74
+ };
75
+ }
76
+ else if (item !== undefined) {
77
+ const segment = conversationSegmentIdentityForItem(turn, item.id, item.kind);
78
+ payload = {
79
+ sessionId: turn.sessionId,
80
+ runId: turn.runId,
81
+ ...(['assistant_message', 'tool_call', 'command_execution', 'file_change'].includes(item.kind) &&
82
+ (rawPayload?.append === undefined || item.status !== 'IN_PROGRESS')
83
+ ? { progress: queries.progress(turn.sessionId) }
84
+ : {}),
85
+ item: {
86
+ turnId: turn.id,
87
+ itemId: item.id,
88
+ kind: item.kind,
89
+ status: item.status,
90
+ payload: item.payload,
91
+ startedAt: item.startedAt,
92
+ completedAt: item.completedAt,
93
+ segmentId: segment.id,
94
+ segmentIndex: segment.index
95
+ }
96
+ };
97
+ }
98
+ else {
99
+ if (payload.event.eventType !== 'run.started' && payload.event.eventType !== 'run.running')
100
+ return;
101
+ payload = { turn };
102
+ }
103
+ }
104
+ if (category === 'conversation' && isPlainRecord(payload) && isNodeConversationTurn(payload.turn))
105
+ return { ...payload, segments: conversationSegments(payload.turn) };
106
+ if (category === 'run' && isPlainRecord(payload) && isNodeAgentRun(payload.run))
107
+ return {
108
+ ...payload,
109
+ progress: queries.progress(payload.run.sessionId)
110
+ };
111
+ return payload;
112
+ }
@@ -1,19 +1,36 @@
1
1
  import { createEnvelope } from '@myagentroam/protocol';
2
+ import { isPlainRecord } from '../runner/codex/conversation-parser.js';
3
+ import { projectWorkbenchPayload, workbenchPayloadSessionId } from './workbench-event-projection.js';
2
4
  export class WorkbenchEventService {
3
- send;
4
- accepts;
5
+ options;
5
6
  sequence = 0;
6
- constructor(send, accepts) {
7
- this.send = send;
8
- this.accepts = accepts;
7
+ constructor(options) {
8
+ this.options = options;
9
+ }
10
+ dispatch(category, payload) {
11
+ payload = projectWorkbenchPayload(this.options.projection, category, payload);
12
+ if (payload === undefined)
13
+ return;
14
+ const event = this.next(category, payload);
15
+ if (event === undefined)
16
+ return;
17
+ const notification = category === 'conversation' &&
18
+ isPlainRecord(payload) &&
19
+ isPlainRecord(payload.event) &&
20
+ payload.event.eventType === 'notification';
21
+ const sessionId = workbenchPayloadSessionId(this.options.projection.getRun, category, payload);
22
+ // A Watch owns pending buffering and observation drops as well as ready delivery.
23
+ if (!notification && sessionId !== undefined && this.options.emitRealtime(sessionId, event))
24
+ return;
25
+ this.publish(event);
9
26
  }
10
27
  next(category, payload) {
11
- if (!this.accepts(category, payload))
28
+ if (!this.options.accepts(category, payload))
12
29
  return undefined;
13
30
  return { sequence: ++this.sequence, category, payload };
14
31
  }
15
32
  publish(event) {
16
- this.send(createEnvelope('workbench.event', event));
33
+ this.options.send(createEnvelope('workbench.event', event));
17
34
  }
18
35
  snapshotSequence() {
19
36
  return this.sequence;
package/dist/terminal.js CHANGED
@@ -318,17 +318,14 @@ export class TerminalManager {
318
318
  const summary = this.notifySummary(terminal);
319
319
  try {
320
320
  if (terminal.pty !== undefined) {
321
- if (this.platform === 'win32') {
322
- // Killing only the shell bypasses node-pty's native console cleanup.
323
- try {
324
- terminal.pty.kill();
325
- }
326
- catch {
327
- await this.terminate(terminal.pty, this.platform);
328
- }
321
+ // Native PTY hangup reaches POSIX foreground jobs in a separate process group
322
+ // and releases the native console on Windows; shell-group TERM cannot do this.
323
+ try {
324
+ terminal.pty.kill();
329
325
  }
330
- else
326
+ catch {
331
327
  await this.terminate(terminal.pty, this.platform);
328
+ }
332
329
  }
333
330
  if (terminal.pipe !== undefined)
334
331
  await this.terminate(terminal.pipe, this.platform);
@@ -338,13 +335,17 @@ export class TerminalManager {
338
335
  }
339
336
  return summary;
340
337
  }
338
+ /** Ends current terminals without disposing the manager's idle collection. */
339
+ async closeAll() {
340
+ await Promise.all([...this.terminals.values()]
341
+ .filter((terminal) => terminal.state === 'RUNNING')
342
+ .map((terminal) => this.close(terminal.id)));
343
+ }
341
344
  async shutdown() {
342
345
  if (this.idleTimer !== undefined)
343
346
  clearInterval(this.idleTimer);
344
347
  this.idleTimer = undefined;
345
- await Promise.all([...this.terminals.values()]
346
- .filter((terminal) => terminal.state === 'RUNNING')
347
- .map((terminal) => this.close(terminal.id)));
348
+ await this.closeAll();
348
349
  }
349
350
  onFrame(listener) {
350
351
  this.frameListeners.add(listener);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/node",
3
- "version": "0.10.68",
3
+ "version": "0.11.1",
4
4
  "description": "MyAgentRoam Node runtime CLI.",
5
5
  "license": "Apache-2.0 AND GPL-2.0-or-later",
6
6
  "type": "module",
@@ -37,10 +37,10 @@
37
37
  "playwright": "1.62.1",
38
38
  "ws": "^8.21.3",
39
39
  "zod": "4.4.3",
40
- "@myagentroam/agent": "0.10.68",
41
- "@myagentroam/code-navigation": "0.10.1",
42
- "@myagentroam/marctl": "0.10.12",
43
- "@myagentroam/protocol": "0.10.68"
40
+ "@myagentroam/agent": "0.11.1",
41
+ "@myagentroam/code-navigation": "0.11.0",
42
+ "@myagentroam/marctl": "0.11.0",
43
+ "@myagentroam/protocol": "0.11.1"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/ws": "^8.18.1"