@myagentroam/node 0.11.0 → 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({
@@ -1718,24 +1730,7 @@ export class NodeConnector {
1718
1730
  }
1719
1731
  this.emitWorkbenchEvent('session', payload);
1720
1732
  };
1721
- emitWorkbenchEvent = (category, payload) => {
1722
- payload = this.runEventBridge.projectWorkbenchPayload(category, payload);
1723
- if (payload === undefined)
1724
- return;
1725
- const event = this.workbenchEventService.next(category, payload);
1726
- if (event === undefined)
1727
- return;
1728
- const notification = category === 'conversation' &&
1729
- isPlainRecord(payload) &&
1730
- isPlainRecord(payload.event) &&
1731
- payload.event.eventType === 'notification';
1732
- const sessionId = workbenchPayloadSessionId(this.runtime, category, payload);
1733
- if (!notification &&
1734
- sessionId !== undefined &&
1735
- this.nativeSessionWatchService.emitRealtime(sessionId, event))
1736
- return;
1737
- this.workbenchEventService.publish(event);
1738
- };
1733
+ emitWorkbenchEvent = (category, payload) => this.workbenchEventService.dispatch(category, payload);
1739
1734
  discoverWorkspaceSessions = (workspaceId, onRunnerDiscovered) => this.nativeSessionProjectionService.discoverWorkspace(workspaceId, onRunnerDiscovered);
1740
1735
  invalidateWorkspaceWatchTopics = (workspaceId) => this.workspaceCoordinator.invalidate(workspaceId);
1741
1736
  reconcilePersistedRuns = () => undefined;
@@ -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.11.0",
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.11.0",
41
- "@myagentroam/marctl": "0.11.0",
40
+ "@myagentroam/agent": "0.11.1",
42
41
  "@myagentroam/code-navigation": "0.11.0",
43
- "@myagentroam/protocol": "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"