@myagentroam/node 0.11.39 → 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.
- package/dist/connector.js +176 -21
- package/dist/database.js +35 -11
- package/dist/main.js +12 -1
- package/dist/marctl-guidance.js +18 -27
- package/dist/migrations/v022.js +58 -0
- package/dist/runner/abstract-runner.js +14 -0
- package/dist/runner/claude/tool-normalizer.js +6 -1
- package/dist/runner/codex/conversation-parser.js +23 -4
- package/dist/runner/codex/managed-run-controller.js +4 -2
- package/dist/runner/codex/tool-normalizer.js +9 -0
- package/dist/runner/execution-presentation.js +56 -0
- package/dist/runner/mar-agent/agent-network-extension.js +168 -0
- package/dist/runner/mar-agent/agent-network-wait-output.js +72 -0
- package/dist/runner/mar-agent/conversation-normalizer.js +233 -119
- package/dist/runner/mar-agent/managed-run-controller.js +5 -0
- package/dist/runner/mar-agent/marctl-skill.js +3 -3
- package/dist/runner/mar-agent/runtime-model-factory.js +1 -0
- package/dist/runner/mar-agent/session-runtime.js +34 -1
- package/dist/runner/mar-agent-runner.js +36 -0
- package/dist/runner/opencode/tool-normalizer.js +6 -1
- package/dist/runtime-state.js +57 -0
- package/dist/service/agent-network-bindings.js +483 -0
- package/dist/service/agent-network-goal.js +19 -0
- package/dist/service/agent-network-invoke.js +179 -0
- package/dist/service/agent-network-observations.js +377 -0
- package/dist/service/agent-network-service.js +315 -0
- package/dist/service/agent-network-wait.js +130 -0
- package/dist/service/composer-message-input.js +9 -2
- package/dist/service/continuous-run-coordinator.js +24 -1
- package/dist/service/goal-continuation-coordinator.js +12 -2
- package/dist/service/marctl-caller.js +80 -0
- package/dist/service/marctl-execution-credential-control.js +65 -0
- package/dist/service/model-usage-service.js +9 -0
- package/dist/service/node-control-message-service.js +10 -0
- package/dist/service/node-request-service.js +52 -7
- package/dist/service/node-upgrade-coordinator.js +79 -8
- package/dist/service/node-upgrade-credential-control.js +57 -0
- package/dist/service/node-upgrade-reporter.js +108 -0
- package/dist/service/run-event-service.js +8 -0
- package/dist/service/run-workbench-service.js +11 -0
- package/dist/service/supervised-node-child.js +153 -0
- package/dist/service/supervisor-upgrade-handoff.js +104 -0
- package/dist/session/native-session-projection-service.js +69 -6
- package/dist/session/operation-admission.js +7 -0
- package/dist/session/runtime/conversation-projection.js +25 -3
- package/dist/session/session-catalog-service.js +156 -32
- package/dist/session/session-lifecycle-service.js +68 -42
- package/dist/session/session-message-service.js +16 -6
- package/dist/supervisor.js +209 -20
- package/dist/terminal.js +41 -21
- package/package.json +4 -4
|
@@ -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)
|
|
@@ -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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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 =
|
|
121
|
-
|
|
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)
|