@myagentroam/node 0.11.38 → 0.11.40

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.
Files changed (65) hide show
  1. package/dist/computer-use/contracts.js +8 -0
  2. package/dist/{runner/mar-agent/computer-use-indicator.js → computer-use/indicator.js} +61 -38
  3. package/dist/computer-use/probe.js +95 -0
  4. package/dist/computer-use/provider.js +159 -0
  5. package/dist/computer-use/session.js +334 -0
  6. package/dist/computer-use/worker-source.js +135 -0
  7. package/dist/computer-use/worker.js +321 -0
  8. package/dist/connector.js +181 -22
  9. package/dist/database.js +35 -11
  10. package/dist/main.js +12 -1
  11. package/dist/marctl-guidance.js +18 -27
  12. package/dist/mcp/naming.js +6 -1
  13. package/dist/migrations/v022.js +58 -0
  14. package/dist/runner/abstract-runner.js +14 -0
  15. package/dist/runner/claude/tool-normalizer.js +6 -1
  16. package/dist/runner/codex/conversation-parser.js +23 -4
  17. package/dist/runner/codex/managed-run-controller.js +4 -2
  18. package/dist/runner/codex/tool-normalizer.js +9 -0
  19. package/dist/runner/execution-presentation.js +56 -0
  20. package/dist/runner/mar-agent/agent-network-extension.js +168 -0
  21. package/dist/runner/mar-agent/agent-network-wait-output.js +72 -0
  22. package/dist/runner/mar-agent/computer-use-skill.js +11 -2
  23. package/dist/runner/mar-agent/computer-use.js +104 -728
  24. package/dist/runner/mar-agent/conversation-normalizer.js +241 -121
  25. package/dist/runner/mar-agent/managed-run-controller.js +13 -5
  26. package/dist/runner/mar-agent/marctl-skill.js +3 -3
  27. package/dist/runner/mar-agent/runtime-model-factory.js +1 -0
  28. package/dist/runner/mar-agent/session-runtime.js +34 -1
  29. package/dist/runner/mar-agent-runner.js +36 -0
  30. package/dist/runner/opencode/tool-normalizer.js +6 -1
  31. package/dist/runtime-state.js +57 -0
  32. package/dist/service/agent-network-bindings.js +483 -0
  33. package/dist/service/agent-network-goal.js +19 -0
  34. package/dist/service/agent-network-invoke.js +179 -0
  35. package/dist/service/agent-network-observations.js +377 -0
  36. package/dist/service/agent-network-service.js +315 -0
  37. package/dist/service/agent-network-wait.js +130 -0
  38. package/dist/service/composer-message-input.js +9 -2
  39. package/dist/service/computer-use-enablement-service.js +13 -7
  40. package/dist/service/continuous-run-coordinator.js +24 -1
  41. package/dist/service/conversation-hosting-enablement-service.js +2 -2
  42. package/dist/service/goal-continuation-coordinator.js +12 -2
  43. package/dist/service/marctl-caller.js +80 -0
  44. package/dist/service/marctl-execution-credential-control.js +65 -0
  45. package/dist/service/model-usage-service.js +9 -0
  46. package/dist/service/node-config-mutations.js +27 -0
  47. package/dist/service/node-control-message-service.js +10 -0
  48. package/dist/service/node-request-service.js +52 -7
  49. package/dist/service/node-upgrade-coordinator.js +79 -8
  50. package/dist/service/node-upgrade-credential-control.js +57 -0
  51. package/dist/service/node-upgrade-reporter.js +108 -0
  52. package/dist/service/run-event-service.js +8 -0
  53. package/dist/service/run-workbench-service.js +11 -0
  54. package/dist/service/supervised-node-child.js +153 -0
  55. package/dist/service/supervisor-upgrade-handoff.js +104 -0
  56. package/dist/session/native-session-projection-service.js +69 -6
  57. package/dist/session/operation-admission.js +7 -0
  58. package/dist/session/runtime/conversation-projection.js +25 -3
  59. package/dist/session/session-catalog-service.js +156 -32
  60. package/dist/session/session-lifecycle-service.js +68 -42
  61. package/dist/session/session-message-service.js +16 -6
  62. package/dist/supervisor.js +209 -20
  63. package/dist/terminal.js +41 -21
  64. package/package.json +6 -5
  65. /package/dist/{runner/mar-agent/computer-use-probe.js → computer-use/probe-source.js} +0 -0
@@ -0,0 +1,80 @@
1
+ import { isPlainRecord } from '../util/records.js';
2
+ import { assertSessionControlOwner } from './run-ownership-policy.js';
3
+ const executionReadOperations = new Set([
4
+ 'agent.network',
5
+ 'session.cli.context',
6
+ 'session.get',
7
+ 'session.list',
8
+ 'session.graph',
9
+ 'session.runs',
10
+ 'session.goal.get',
11
+ 'session.turns.list',
12
+ 'workspace.queue.get',
13
+ 'session.schedule.list',
14
+ 'session.schedule.get',
15
+ 'runner.profiles.list',
16
+ 'session.watch',
17
+ 'session.unwatch'
18
+ ]);
19
+ const executionManagementOperations = new Set([
20
+ 'session.metadata.update',
21
+ 'session.config.update',
22
+ 'session.schedule.agent.save'
23
+ ]);
24
+ /** Prevent execution credentials from reaching legacy writes outside their Session tree. */
25
+ export async function authorizeMarctlExecutionOperation(input) {
26
+ if (!isPlainRecord(input.caller))
27
+ throw new Error('MARCTL_EXECUTION_CREDENTIAL_INVALID');
28
+ if (input.caller.type === 'terminal')
29
+ return;
30
+ if (input.caller.type !== 'execution' || typeof input.caller.sessionId !== 'string')
31
+ throw new Error('MARCTL_EXECUTION_CREDENTIAL_INVALID');
32
+ if (executionReadOperations.has(input.operation))
33
+ return;
34
+ if (!executionManagementOperations.has(input.operation) ||
35
+ !isPlainRecord(input.data) ||
36
+ typeof input.data.sessionId !== 'string' ||
37
+ typeof input.data.workspaceId !== 'string')
38
+ throw new Error('MARCTL_AGENT_NETWORK_REQUIRED');
39
+ const source = input.runtime.getAgentSession(input.caller.sessionId);
40
+ const target = input.runtime.getAgentSession(input.data.sessionId) ??
41
+ (await input.resolveSession(input.data.sessionId));
42
+ if (!source ||
43
+ !target ||
44
+ source.workspaceId !== input.data.workspaceId ||
45
+ target.workspaceId !== source.workspaceId ||
46
+ (target.id !== source.id && target.parentSessionId !== source.id))
47
+ throw new Error('AGENT_TARGET_NOT_AUTHORIZED');
48
+ assertSessionControlOwner(input.runtime, target.id, input.userId);
49
+ }
50
+ /** Resolve only Server-verified credentials against the current Node Execution owner. */
51
+ export function resolveMarctlCaller(input) {
52
+ if (!isPlainRecord(input.caller))
53
+ return undefined;
54
+ if (input.caller.type === 'terminal')
55
+ return { type: 'terminal' };
56
+ if (input.caller.type !== 'execution' ||
57
+ typeof input.caller.sessionId !== 'string' ||
58
+ typeof input.caller.executionId !== 'string' ||
59
+ !isPlainRecord(input.data) ||
60
+ typeof input.data.workspaceId !== 'string')
61
+ return undefined;
62
+ const session = input.runtime.getAgentSession(input.caller.sessionId);
63
+ const execution = input.runtime.executionSnapshot(input.caller.executionId);
64
+ const prepared = input.coordinator.pending(input.caller.executionId);
65
+ const run = execution && input.runtime.getRun(execution.runId);
66
+ if (!session ||
67
+ !execution ||
68
+ !prepared ||
69
+ !run ||
70
+ session.workspaceId !== input.data.workspaceId ||
71
+ execution.sessionId !== session.id ||
72
+ prepared.sessionId !== session.id ||
73
+ prepared.workspaceId !== session.workspaceId ||
74
+ run.initiatedByUserId !== input.userId ||
75
+ !['STARTING', 'RUNNING'].includes(execution.status) ||
76
+ input.runtime.activeSessionRun(session.id)?.id !== run.id ||
77
+ input.runtime.currentExecutionId(run.id) !== execution.executionId)
78
+ return undefined;
79
+ return { type: 'execution', sessionId: session.id, executionId: execution.executionId };
80
+ }
@@ -0,0 +1,65 @@
1
+ import { createEnvelope, marctlExecutionCredentialRequestSchema, marctlExecutionCredentialResponseSchema } from '@myagentroam/protocol';
2
+ /** Correlates private credential exchanges on the authenticated Node connection. */
3
+ export class MarctlExecutionCredentialControl {
4
+ send;
5
+ pending = new Map();
6
+ available = false;
7
+ connectedToServer = false;
8
+ constructor(send) {
9
+ this.send = send;
10
+ }
11
+ setFeatures(payload) {
12
+ const features = payload;
13
+ this.available = features?.marctlExecutionCredential?.apiVersion === 1;
14
+ }
15
+ connected() {
16
+ this.connectedToServer = true;
17
+ }
18
+ disconnected() {
19
+ this.connectedToServer = false;
20
+ this.available = false;
21
+ for (const entry of this.pending.values()) {
22
+ clearTimeout(entry.timer);
23
+ entry.reject(new Error('MARCTL_EXECUTION_AUTHORIZATION_UNAVAILABLE'));
24
+ }
25
+ this.pending.clear();
26
+ }
27
+ handle(envelope) {
28
+ if (envelope.type !== 'marctl.execution-credential.response')
29
+ return false;
30
+ const entry = envelope.replyTo ? this.pending.get(envelope.replyTo) : undefined;
31
+ if (!entry)
32
+ return true;
33
+ this.pending.delete(envelope.replyTo);
34
+ clearTimeout(entry.timer);
35
+ const response = marctlExecutionCredentialResponseSchema.safeParse(envelope.payload);
36
+ if (!response.success || 'error' in response.data)
37
+ entry.reject(new Error('MARCTL_EXECUTION_AUTHORIZATION_INVALID'));
38
+ else
39
+ entry.resolve(response.data.token);
40
+ return true;
41
+ }
42
+ request(input) {
43
+ if (!this.connectedToServer || !this.available)
44
+ return Promise.reject(new Error('MARCTL_EXECUTION_AUTHORIZATION_UNAVAILABLE'));
45
+ const request = marctlExecutionCredentialRequestSchema.safeParse(input);
46
+ if (!request.success)
47
+ return Promise.reject(new Error('MARCTL_EXECUTION_AUTHORIZATION_INVALID'));
48
+ const envelope = createEnvelope('marctl.execution-credential.request', request.data);
49
+ return new Promise((resolve, reject) => {
50
+ const timer = setTimeout(() => {
51
+ this.pending.delete(envelope.id);
52
+ reject(new Error('MARCTL_EXECUTION_AUTHORIZATION_UNAVAILABLE'));
53
+ }, 15_000);
54
+ this.pending.set(envelope.id, { resolve, reject, timer });
55
+ try {
56
+ this.send(envelope);
57
+ }
58
+ catch {
59
+ clearTimeout(timer);
60
+ this.pending.delete(envelope.id);
61
+ reject(new Error('MARCTL_EXECUTION_AUTHORIZATION_UNAVAILABLE'));
62
+ }
63
+ });
64
+ }
65
+ }
@@ -19,6 +19,15 @@ export class NodeModelUsageService {
19
19
  if (!this.bindings.has(runId))
20
20
  this.bind(runId, this.scope.getStore(), sessionId, runner);
21
21
  }
22
+ /** Reuse only the current Run's attribution for an authorized child submission. */
23
+ grantForRun(runId, sessionId, userId) {
24
+ const binding = this.bindings.get(runId);
25
+ if (!binding ||
26
+ binding.sessionId !== sessionId ||
27
+ (userId !== undefined && binding.userId !== userId))
28
+ return undefined;
29
+ return binding.grant;
30
+ }
22
31
  bind(runId, grant, sessionId, runner) {
23
32
  this.bindings.delete(runId);
24
33
  if (grant === undefined)
@@ -0,0 +1,27 @@
1
+ /** Serializes runtime configuration writes, including transient state and rollback. */
2
+ export class NodeConfigMutations {
3
+ #pending;
4
+ run(operation) {
5
+ const previous = this.#pending;
6
+ let release;
7
+ const pending = new Promise((resolve) => {
8
+ release = resolve;
9
+ });
10
+ this.#pending = pending;
11
+ const finish = () => {
12
+ if (this.#pending === pending)
13
+ this.#pending = undefined;
14
+ release();
15
+ };
16
+ let result;
17
+ try {
18
+ // Start an uncontended mutation synchronously so disable closes admission before yielding.
19
+ result = previous === undefined ? operation() : previous.then(operation);
20
+ }
21
+ catch (error) {
22
+ result = Promise.reject(error);
23
+ }
24
+ void result.then(finish, finish);
25
+ return result;
26
+ }
27
+ }
@@ -33,12 +33,22 @@ export class NodeControlMessageService {
33
33
  void this.options.handleNodeRequest(envelope);
34
34
  return;
35
35
  }
36
+ if (envelope.type === 'node.request.cancel') {
37
+ const payload = envelope.payload;
38
+ if (typeof payload?.requestId === 'string')
39
+ this.options.cancelNodeWait?.(payload.requestId);
40
+ return;
41
+ }
36
42
  if (this.options.handleRuntimeCredentialResponse(envelope))
37
43
  return;
38
44
  if (this.options.handleScheduledRunAuthorizeResponse?.(envelope) === true)
39
45
  return;
40
46
  if (this.options.handleMailNotificationResponse?.(envelope) === true)
41
47
  return;
48
+ if (this.options.handleMarctlExecutionCredentialResponse?.(envelope) === true)
49
+ return;
50
+ if (this.options.handleUpgradeCredentialResponse?.(envelope) === true)
51
+ return;
42
52
  if (envelope.type.startsWith('direct.') &&
43
53
  this.options.handleDirectTransfer?.(envelope) === true)
44
54
  return;
@@ -4,9 +4,18 @@ import { safePublicError } from '../util/safe-error.js';
4
4
  /** Validates and executes the closed Admin-facing Node operation envelope. */
5
5
  export class NodeRequestService {
6
6
  options;
7
+ waits = new Map();
7
8
  constructor(options) {
8
9
  this.options = options;
9
10
  }
11
+ cancelWait(requestId) {
12
+ this.waits.get(requestId)?.abort();
13
+ }
14
+ disconnect() {
15
+ for (const controller of this.waits.values())
16
+ controller.abort();
17
+ this.waits.clear();
18
+ }
10
19
  async handle(envelope) {
11
20
  const payload = envelope.payload;
12
21
  if (typeof payload.operation !== 'string' || !this.options.databaseAvailable()) {
@@ -18,16 +27,48 @@ export class NodeRequestService {
18
27
  return;
19
28
  }
20
29
  const startedAt = performance.now();
30
+ const waiting = payload.operation === 'agent.network' &&
31
+ payload.data !== null &&
32
+ typeof payload.data === 'object' &&
33
+ payload.data.action === 'wait';
34
+ const controller = waiting ? new AbortController() : undefined;
35
+ if (controller)
36
+ this.waits.set(envelope.id, controller);
21
37
  try {
38
+ const authorization = payload.userAuthorization;
39
+ const marctlCaller = authorization.marctlCaller === undefined
40
+ ? undefined
41
+ : this.options.resolveMarctlCaller?.(authorization.marctlCaller, authorization.userId, payload.data);
42
+ if (authorization.marctlCaller !== undefined && marctlCaller === undefined)
43
+ throw new Error('MARCTL_EXECUTION_CREDENTIAL_INVALID');
44
+ if (marctlCaller !== undefined) {
45
+ if (!this.options.authorizeMarctlOperation)
46
+ throw new Error('MARCTL_EXECUTION_CREDENTIAL_INVALID');
47
+ await this.options.authorizeMarctlOperation(marctlCaller, payload.operation, payload.data, authorization.userId);
48
+ }
22
49
  const usageGrant = payload.userAuthorization.usageGrant;
23
50
  const data = payload.data !== null && typeof payload.data === 'object'
24
- ? {
25
- ...payload.data,
26
- __workspaceWatchSessionHash: payload.userAuthorization
27
- .sessionHash,
28
- __requestUserId: payload.userAuthorization.userId,
29
- ...(usageGrant === undefined ? {} : { __usageGrant: usageGrant })
30
- }
51
+ ? (() => {
52
+ const { __marctlCaller: _untrustedCaller, __usageGrant: _untrustedGrant, __marctlExecutionResources: _untrustedExecutionResources, __requestAbortSignal: _untrustedSignal, ...input } = payload.data;
53
+ return {
54
+ ...input,
55
+ __workspaceWatchSessionHash: payload.userAuthorization
56
+ .sessionHash,
57
+ __requestUserId: payload.userAuthorization.userId,
58
+ ...(marctlCaller === undefined ? {} : { __marctlCaller: marctlCaller }),
59
+ ...(payload.operation === 'agent.network' &&
60
+ marctlCaller !== null &&
61
+ typeof marctlCaller === 'object' &&
62
+ marctlCaller.type === 'execution' &&
63
+ authorization.marctlExecutionResources !== null &&
64
+ typeof authorization.marctlExecutionResources === 'object' &&
65
+ !Array.isArray(authorization.marctlExecutionResources)
66
+ ? { __marctlExecutionResources: authorization.marctlExecutionResources }
67
+ : {}),
68
+ ...(usageGrant === undefined ? {} : { __usageGrant: usageGrant }),
69
+ ...(controller === undefined ? {} : { __requestAbortSignal: controller.signal })
70
+ };
71
+ })()
31
72
  : payload.data;
32
73
  const result = await this.options.execute(payload.operation, data);
33
74
  this.options.send('node.response', { result }, envelope.id);
@@ -52,6 +93,10 @@ export class NodeRequestService {
52
93
  });
53
94
  this.options.send('node.response', nodeResponseFailure(failure), envelope.id);
54
95
  }
96
+ finally {
97
+ if (controller && this.waits.get(envelope.id) === controller)
98
+ this.waits.delete(envelope.id);
99
+ }
55
100
  }
56
101
  sendError(replyTo, error, fallback) {
57
102
  const failure = safePublicError(error, fallback);
@@ -7,14 +7,17 @@ export class NodeUpgradeCoordinator {
7
7
  onStatus;
8
8
  onReady;
9
9
  now;
10
+ prepareHandoff;
10
11
  status = { state: 'IDLE', updatedAt: Date.now() };
11
12
  timer;
12
- constructor(requestPath, activeRunCount, onStatus, onReady, now = Date.now) {
13
+ attemptId;
14
+ constructor(requestPath, activeRunCount, onStatus, onReady, now = Date.now, prepareHandoff) {
13
15
  this.requestPath = requestPath;
14
16
  this.activeRunCount = activeRunCount;
15
17
  this.onStatus = onStatus;
16
18
  this.onReady = onReady;
17
19
  this.now = now;
20
+ this.prepareHandoff = prepareHandoff;
18
21
  }
19
22
  snapshot() {
20
23
  return this.status;
@@ -34,14 +37,22 @@ export class NodeUpgradeCoordinator {
34
37
  }
35
38
  }
36
39
  request(data) {
37
- const targetVersion = record(data).targetVersion;
40
+ const input = record(data);
41
+ const targetVersion = input.targetVersion;
38
42
  if (typeof targetVersion !== 'string' || !STABLE_VERSION.test(targetVersion))
39
43
  throw new Error('NODE_UPGRADE_VERSION_INVALID');
44
+ const attemptId = input.attemptId;
45
+ if (attemptId !== undefined &&
46
+ (typeof attemptId !== 'string' ||
47
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(attemptId) ||
48
+ this.prepareHandoff === undefined))
49
+ throw new Error('NODE_UPGRADE_REQUEST_INVALID');
40
50
  if (this.status.state !== 'IDLE' && this.status.state !== 'FAILED') {
41
- if (this.status.targetVersion !== targetVersion)
51
+ if (this.status.targetVersion !== targetVersion || this.attemptId !== attemptId)
42
52
  throw new Error('NODE_UPGRADE_ALREADY_PENDING');
43
53
  return { status: 'ALREADY_PENDING', targetVersion, activeRunCount: this.activeRunCount() };
44
54
  }
55
+ this.attemptId = attemptId;
45
56
  this.update({
46
57
  state: 'WAITING_FOR_RUNS',
47
58
  targetVersion,
@@ -56,7 +67,13 @@ export class NodeUpgradeCoordinator {
56
67
  if (this.status.state !== 'WAITING_FOR_RUNS')
57
68
  throw new Error('NODE_UPGRADE_NOT_CANCELLABLE');
58
69
  this.clearTimer();
59
- this.update({ state: 'IDLE', updatedAt: this.now() });
70
+ const attemptId = this.attemptId;
71
+ this.attemptId = undefined;
72
+ this.update({
73
+ state: 'IDLE',
74
+ ...(attemptId === undefined ? {} : { attemptId }),
75
+ updatedAt: this.now()
76
+ });
60
77
  return { cancelled: true };
61
78
  }
62
79
  blocks(operation) {
@@ -95,12 +112,42 @@ export class NodeUpgradeCoordinator {
95
112
  const path = this.requestPath();
96
113
  const temporary = `${path}.${process.pid}.tmp`;
97
114
  await mkdir(dirname(path), { recursive: true, mode: 0o700 });
98
- await writeFile(temporary, `${JSON.stringify({ schemaVersion: 1, targetVersion, createdAt: this.now() })}\n`, { mode: 0o600 });
115
+ await writeFile(temporary, `${JSON.stringify({
116
+ schemaVersion: 1,
117
+ targetVersion,
118
+ createdAt: this.now(),
119
+ ...(this.attemptId === undefined ? {} : { attemptId: this.attemptId })
120
+ })}\n`, { mode: 0o600 });
99
121
  await rename(temporary, path).catch(async (error) => {
100
122
  await rm(temporary, { force: true });
101
123
  throw error;
102
124
  });
103
- this.onReady();
125
+ try {
126
+ if (this.attemptId !== undefined)
127
+ await this.prepareHandoff(this.attemptId, targetVersion);
128
+ this.onReady();
129
+ }
130
+ catch (error) {
131
+ if (error instanceof Error && error.message === 'NODE_UPGRADE_HANDOFF_UNCERTAIN') {
132
+ this.update({
133
+ state: 'READY_TO_RESTART',
134
+ targetVersion,
135
+ activeRunCount: 0,
136
+ errorCode: 'NODE_UPGRADE_HANDOFF_UNCERTAIN',
137
+ updatedAt: this.now()
138
+ });
139
+ return;
140
+ }
141
+ await rm(path, { force: true });
142
+ this.update({
143
+ state: 'FAILED',
144
+ targetVersion,
145
+ errorCode: error instanceof Error && /^NODE_UPGRADE_[A-Z_]+$/u.test(error.message)
146
+ ? error.message
147
+ : 'NODE_UPGRADE_HANDOFF_UNAVAILABLE',
148
+ updatedAt: this.now()
149
+ });
150
+ }
104
151
  }
105
152
  async checkReadySafely() {
106
153
  try {
@@ -117,8 +164,11 @@ export class NodeUpgradeCoordinator {
117
164
  }
118
165
  }
119
166
  update(status) {
120
- this.status = status;
121
- this.onStatus(status);
167
+ this.status =
168
+ status.state !== 'IDLE' && this.attemptId !== undefined
169
+ ? { ...status, attemptId: this.attemptId }
170
+ : status;
171
+ this.onStatus(this.status);
122
172
  }
123
173
  clearTimer() {
124
174
  if (this.timer !== undefined)
@@ -134,3 +184,24 @@ function record(value) {
134
184
  throw new Error('NODE_UPGRADE_REQUEST_INVALID');
135
185
  return value;
136
186
  }
187
+ export function upgradeMetadata(metadata, status, progressAvailable) {
188
+ const next = { ...(metadata ?? {}) };
189
+ if (process.env['MAR_NODE_SUPERVISED'] === '1')
190
+ next.supervisedUpgrade = true;
191
+ else
192
+ delete next.supervisedUpgrade;
193
+ if (progressAvailable)
194
+ next.upgradeProgress = { apiVersion: 1 };
195
+ else
196
+ delete next.upgradeProgress;
197
+ const attemptId = process.env['MAR_NODE_UPGRADE_ATTEMPT_ID'];
198
+ if (progressAvailable && attemptId && /^[0-9a-f-]{36}$/iu.test(attemptId))
199
+ next.upgradeAttemptId = attemptId;
200
+ else
201
+ delete next.upgradeAttemptId;
202
+ if (status.state === 'IDLE' && status.attemptId === undefined)
203
+ delete next.upgrade;
204
+ else
205
+ next.upgrade = status;
206
+ return next;
207
+ }
@@ -0,0 +1,57 @@
1
+ import { createEnvelope } from '@myagentroam/protocol';
2
+ /** One short-lived request over the authenticated Node control connection. */
3
+ export class NodeUpgradeCredentialControl {
4
+ send;
5
+ pending;
6
+ available = false;
7
+ constructor(send) {
8
+ this.send = send;
9
+ }
10
+ setFeatures(payload) {
11
+ const features = payload;
12
+ this.available = features?.nodeUpgradeProgress?.apiVersion === 1;
13
+ }
14
+ disconnect() {
15
+ this.available = false;
16
+ if (this.pending) {
17
+ clearTimeout(this.pending.timer);
18
+ this.pending.reject(new Error('NODE_UPGRADE_CREDENTIAL_UNAVAILABLE'));
19
+ this.pending = undefined;
20
+ }
21
+ }
22
+ handle(envelope) {
23
+ if (envelope.type !== 'node.upgrade.credential.response')
24
+ return false;
25
+ const pending = this.pending;
26
+ if (!pending || pending.id !== envelope.replyTo)
27
+ return true;
28
+ this.pending = undefined;
29
+ clearTimeout(pending.timer);
30
+ const payload = envelope.payload;
31
+ if (typeof payload?.token !== 'string' || !/^[A-Za-z0-9_-]{43}$/u.test(payload.token))
32
+ pending.reject(new Error('NODE_UPGRADE_CREDENTIAL_REJECTED'));
33
+ else
34
+ pending.resolve(payload.token);
35
+ return true;
36
+ }
37
+ request(attemptId) {
38
+ if (!this.available || this.pending)
39
+ return Promise.reject(new Error('NODE_UPGRADE_CREDENTIAL_UNAVAILABLE'));
40
+ const envelope = createEnvelope('node.upgrade.credential.request', { attemptId });
41
+ return new Promise((resolve, reject) => {
42
+ const timer = setTimeout(() => {
43
+ this.pending = undefined;
44
+ reject(new Error('NODE_UPGRADE_CREDENTIAL_UNAVAILABLE'));
45
+ }, 15_000);
46
+ this.pending = { id: envelope.id, resolve, reject, timer };
47
+ try {
48
+ this.send(envelope);
49
+ }
50
+ catch {
51
+ clearTimeout(timer);
52
+ this.pending = undefined;
53
+ reject(new Error('NODE_UPGRADE_CREDENTIAL_UNAVAILABLE'));
54
+ }
55
+ });
56
+ }
57
+ }
@@ -0,0 +1,108 @@
1
+ /** A single in-flight report; changes replace the pending snapshot rather than queuing events. */
2
+ export class NodeUpgradeReporter {
3
+ credential;
4
+ send;
5
+ endpoint;
6
+ timer;
7
+ pending;
8
+ current;
9
+ inFlight = false;
10
+ stopped = false;
11
+ controller;
12
+ idleWaiters = new Set();
13
+ constructor(serverUrl, nodeId, credential, send = fetch) {
14
+ this.credential = credential;
15
+ this.send = send;
16
+ const endpoint = new URL(serverUrl);
17
+ if (endpoint.protocol === 'ws:')
18
+ endpoint.protocol = 'http:';
19
+ if (endpoint.protocol === 'wss:')
20
+ endpoint.protocol = 'https:';
21
+ if (endpoint.username ||
22
+ endpoint.password ||
23
+ (endpoint.protocol !== 'https:' &&
24
+ !(endpoint.protocol === 'http:' &&
25
+ ['localhost', '127.0.0.1', '[::1]'].includes(endpoint.hostname))))
26
+ throw new Error('NODE_UPGRADE_REPORT_ORIGIN_INVALID');
27
+ endpoint.pathname = `/api/nodes/${encodeURIComponent(nodeId)}/upgrade/report`;
28
+ endpoint.search = '';
29
+ endpoint.hash = '';
30
+ this.endpoint = endpoint;
31
+ this.timer = setInterval(() => {
32
+ if (this.current !== undefined)
33
+ this.update(this.current);
34
+ }, 15_000);
35
+ this.timer.unref();
36
+ }
37
+ update(report) {
38
+ if (this.stopped)
39
+ return;
40
+ this.current = report;
41
+ this.pending = report;
42
+ void this.flush();
43
+ }
44
+ stop() {
45
+ this.stopped = true;
46
+ this.pending = undefined;
47
+ clearInterval(this.timer);
48
+ this.controller?.abort();
49
+ }
50
+ async finish(report) {
51
+ this.update(report);
52
+ if (this.inFlight || this.pending !== undefined) {
53
+ await new Promise((resolve) => {
54
+ const finish = () => {
55
+ clearTimeout(timeout);
56
+ this.idleWaiters.delete(finish);
57
+ resolve();
58
+ };
59
+ const timeout = setTimeout(finish, 5_000);
60
+ this.idleWaiters.add(finish);
61
+ });
62
+ }
63
+ this.stop();
64
+ }
65
+ async flush() {
66
+ if (this.stopped || this.inFlight || this.pending === undefined)
67
+ return;
68
+ const report = this.pending;
69
+ this.pending = undefined;
70
+ this.inFlight = true;
71
+ const controller = new AbortController();
72
+ this.controller = controller;
73
+ const timeout = setTimeout(() => controller.abort(), 5_000);
74
+ timeout.unref();
75
+ try {
76
+ const response = await this.send(this.endpoint, {
77
+ method: 'POST',
78
+ redirect: 'manual',
79
+ signal: controller.signal,
80
+ headers: { authorization: `Bearer ${this.credential}`, 'content-type': 'application/json' },
81
+ body: JSON.stringify(report)
82
+ });
83
+ if (response.status === 401)
84
+ this.stop();
85
+ else if (response.ok) {
86
+ const payload = await response.json();
87
+ if (typeof payload === 'object' &&
88
+ payload !== null &&
89
+ 'state' in payload &&
90
+ payload.state === 'SUCCEEDED')
91
+ this.stop();
92
+ }
93
+ }
94
+ catch {
95
+ // A failed report does not affect a local install; the next interval sends the latest snapshot.
96
+ }
97
+ finally {
98
+ clearTimeout(timeout);
99
+ this.controller = undefined;
100
+ this.inFlight = false;
101
+ if (this.pending !== undefined && !this.stopped)
102
+ void this.flush();
103
+ else
104
+ for (const waiter of this.idleWaiters)
105
+ waiter();
106
+ }
107
+ }
108
+ }
@@ -155,6 +155,14 @@ export class RunEventService {
155
155
  if (!terminalExecution)
156
156
  this.options.refreshAttachments?.(run.sessionId);
157
157
  const publicStatus = terminalExecution ? finalStatus : status;
158
+ if (recorded !== undefined && targetsCurrentTurn)
159
+ this.options.observeAgentEvent?.({
160
+ run,
161
+ executionId,
162
+ eventType,
163
+ payload,
164
+ ...(terminalExecution && finalStatus !== undefined ? { terminalStatus: finalStatus } : {})
165
+ });
158
166
  const notificationPayload = this.notificationPayload(runId, eventType, payload, publicStatus, manuallyInterrupted || !targetsCurrentTurn);
159
167
  if (publicStatus !== undefined) {
160
168
  const currentRun = this.options.runtime.getRun(runId);
@@ -22,6 +22,17 @@ export class RunWorkbenchService {
22
22
  'run.approval.decide': (data) => this.decideApproval(record(data))
23
23
  };
24
24
  }
25
+ /** Atomically validate the target Execution before invoking the existing stop path. */
26
+ stopAgentExecution(input) {
27
+ const run = this.options.runtime.activeSessionRun(input.sessionId);
28
+ if (run === undefined ||
29
+ (this.options.runtime.currentExecutionId(run.id) ?? run.id) !== input.executionId)
30
+ throw new Error('RUN_EXECUTION_STALE');
31
+ return this.cancelOrInterrupt('run.cancel', {
32
+ runId: run.id,
33
+ ...(input.requestUserId === undefined ? {} : { __requestUserId: input.requestUserId })
34
+ });
35
+ }
25
36
  async modelResource(input) {
26
37
  const parsed = modelResourceReadInputSchema.safeParse(nodeOperationData(input));
27
38
  if (!parsed.success)