@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
|
@@ -6,6 +6,8 @@ import { parseSecretEnvironment } from '../mcp/secret-environment.js';
|
|
|
6
6
|
import { ScheduledSessionStore } from '../service/scheduled-session-store.js';
|
|
7
7
|
import { normalizeSessionPrompt } from '../runner/personal-instructions.js';
|
|
8
8
|
import { resolveWorkspaceExecutionDirectory } from '../workspace/path-access.js';
|
|
9
|
+
import { checkSessionAdmission } from './operation-admission.js';
|
|
10
|
+
const AGENT_GOAL_START = Symbol('agent-goal-start');
|
|
9
11
|
export class SessionLifecycleService {
|
|
10
12
|
options;
|
|
11
13
|
starts = new Map();
|
|
@@ -24,6 +26,10 @@ export class SessionLifecycleService {
|
|
|
24
26
|
clear() {
|
|
25
27
|
this.starts.clear();
|
|
26
28
|
}
|
|
29
|
+
/** Use the normal Session bootstrap while its Goal owner submits the first message. */
|
|
30
|
+
startAgentWithGoal(input, submit) {
|
|
31
|
+
return this.start({ ...input, [AGENT_GOAL_START]: submit });
|
|
32
|
+
}
|
|
27
33
|
async updateMetadata(input) {
|
|
28
34
|
if (input.parentSessionId !== undefined ||
|
|
29
35
|
(input.clearParent !== undefined &&
|
|
@@ -47,27 +53,8 @@ export class SessionLifecycleService {
|
|
|
47
53
|
(await this.options.projection.createMetadataProjection(input.sessionId));
|
|
48
54
|
if (current === undefined)
|
|
49
55
|
throw new Error('SESSION_NOT_FOUND');
|
|
50
|
-
if (input.clearParent === true)
|
|
51
|
-
|
|
52
|
-
throw new Error('SESSION_ACTIVATION_REQUIRED');
|
|
53
|
-
const metadata = this.options
|
|
54
|
-
.database()
|
|
55
|
-
.getNativeSessionMetadata(current.runner, current.externalSessionId, current.workspaceId);
|
|
56
|
-
const parentSessionId = metadata === undefined ? current.parentSessionId : metadata.parentSessionId;
|
|
57
|
-
if (parentSessionId == null) {
|
|
58
|
-
const next = current.parentSessionId == null
|
|
59
|
-
? current
|
|
60
|
-
: this.options.runtime.clearSessionParent(current.id);
|
|
61
|
-
return { session: this.options.present(next) };
|
|
62
|
-
}
|
|
63
|
-
this.options
|
|
64
|
-
.database()
|
|
65
|
-
.clearSessionParent(current.runner, current.externalSessionId, current.workspaceId);
|
|
66
|
-
const next = this.options.runtime.clearSessionParent(current.id);
|
|
67
|
-
this.options.parentCleared?.(next);
|
|
68
|
-
this.options.emitSession(next);
|
|
69
|
-
return { session: this.options.present(next) };
|
|
70
|
-
}
|
|
56
|
+
if (input.clearParent === true)
|
|
57
|
+
throw new Error('SESSION_PARENT_IMMUTABLE');
|
|
71
58
|
if (sessionPrompt !== undefined && current.externalSessionId === null)
|
|
72
59
|
throw new Error('SESSION_ACTIVATION_REQUIRED');
|
|
73
60
|
if (input.pinned === false) {
|
|
@@ -142,6 +129,7 @@ export class SessionLifecycleService {
|
|
|
142
129
|
if (workspace === undefined)
|
|
143
130
|
throw new Error('WORKSPACE_NOT_FOUND');
|
|
144
131
|
const cwd = await resolveWorkspaceExecutionDirectory(workspace.path, input.cwd);
|
|
132
|
+
checkSessionAdmission(input);
|
|
145
133
|
if (database.getWorkspace(workspace.id)?.path !== workspace.path)
|
|
146
134
|
throw new Error('WORKSPACE_NOT_FOUND');
|
|
147
135
|
let parentSessionId;
|
|
@@ -157,6 +145,26 @@ export class SessionLifecycleService {
|
|
|
157
145
|
if (liveParent?.externalSessionId === null)
|
|
158
146
|
throw new Error('SESSION_ACTIVATION_REQUIRED');
|
|
159
147
|
parentSessionId = liveParent?.externalSessionId ?? parent.id;
|
|
148
|
+
this.options.runtime.assertSessionHistoryMutable(parent.id);
|
|
149
|
+
if (parentSessionId !== parent.id)
|
|
150
|
+
this.options.runtime.assertSessionHistoryMutable(parentSessionId);
|
|
151
|
+
const visited = new Set();
|
|
152
|
+
let ancestorId = parentSessionId;
|
|
153
|
+
let depth = 0;
|
|
154
|
+
while (ancestorId !== null) {
|
|
155
|
+
if (visited.has(ancestorId))
|
|
156
|
+
throw new Error('SESSION_RELATION_INVALID');
|
|
157
|
+
visited.add(ancestorId);
|
|
158
|
+
const live = this.options.runtime.getAgentSession(ancestorId);
|
|
159
|
+
const stored = database.getSessionRecord(ancestorId);
|
|
160
|
+
const ancestorWorkspace = live?.workspaceId ?? stored?.workspaceId;
|
|
161
|
+
if (ancestorWorkspace !== workspace.id)
|
|
162
|
+
throw new Error('SESSION_RELATION_INVALID');
|
|
163
|
+
depth += 1;
|
|
164
|
+
if (depth >= 3)
|
|
165
|
+
throw new Error('SESSION_DEPTH_EXCEEDED');
|
|
166
|
+
ancestorId = live?.parentSessionId ?? stored?.metadata.parentSessionId ?? null;
|
|
167
|
+
}
|
|
160
168
|
}
|
|
161
169
|
const environment = parseSecretEnvironment(input.secretEnvironment);
|
|
162
170
|
const saved = database.getRunnerDefaults().find((value) => value.runner === runner.name);
|
|
@@ -173,6 +181,8 @@ export class SessionLifecycleService {
|
|
|
173
181
|
};
|
|
174
182
|
if (runner.name === 'mar-agent' && !['auto', 'full-access'].includes(configuration.access))
|
|
175
183
|
throw new Error('SESSION_INVALID');
|
|
184
|
+
if (parentSessionId !== undefined)
|
|
185
|
+
runner.assertChildModel(configuration.model);
|
|
176
186
|
const session = this.options.runtime.createAgentSession({
|
|
177
187
|
...(parentSessionId === undefined ? {} : { parentSessionId }),
|
|
178
188
|
workspaceId: input.workspaceId,
|
|
@@ -211,6 +221,8 @@ export class SessionLifecycleService {
|
|
|
211
221
|
input.access !== undefined &&
|
|
212
222
|
!['auto', 'full-access'].includes(input.access))
|
|
213
223
|
throw new Error('SESSION_INVALID');
|
|
224
|
+
if (current.parentSessionId != null && typeof input.model === 'string')
|
|
225
|
+
this.options.runners.require(current.runner).assertChildModel(input.model);
|
|
214
226
|
const session = this.options.runtime.updateAgentSessionConfiguration({
|
|
215
227
|
sessionId: current.id,
|
|
216
228
|
...(typeof input.model === 'string' ? { model: input.model } : {}),
|
|
@@ -230,6 +242,7 @@ export class SessionLifecycleService {
|
|
|
230
242
|
return { session: this.options.present(session) };
|
|
231
243
|
}
|
|
232
244
|
async start(input) {
|
|
245
|
+
checkSessionAdmission(input);
|
|
233
246
|
const attachments = parseMessageAttachments(input.attachments);
|
|
234
247
|
if (typeof input.clientMessageId !== 'string' ||
|
|
235
248
|
input.clientMessageId.length === 0 ||
|
|
@@ -292,27 +305,40 @@ export class SessionLifecycleService {
|
|
|
292
305
|
const messageSessionId = interceptedId ?? bootstrapId;
|
|
293
306
|
if (interceptedId !== undefined)
|
|
294
307
|
this.options.identity.promote(bootstrapId, interceptedId);
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
:
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
308
|
+
const executionContext = this.options.runners
|
|
309
|
+
.require(created.session.runner)
|
|
310
|
+
.prepareExecutionContext(input);
|
|
311
|
+
const goalStart = input[AGENT_GOAL_START];
|
|
312
|
+
const goalSession = goalStart === undefined
|
|
313
|
+
? undefined
|
|
314
|
+
: this.options.runtime.getAgentSession(messageSessionId);
|
|
315
|
+
if (goalStart !== undefined && goalSession === undefined)
|
|
316
|
+
throw new Error('SESSION_START_FAILED');
|
|
317
|
+
const message = (goalStart === undefined
|
|
318
|
+
? await this.options.execute('session.message', {
|
|
319
|
+
sessionId: messageSessionId,
|
|
320
|
+
clientMessageId: input.clientMessageId,
|
|
321
|
+
content: input.content,
|
|
322
|
+
...(input.__marctlOrigin === undefined
|
|
323
|
+
? {}
|
|
324
|
+
: { __marctlOrigin: input.__marctlOrigin }),
|
|
325
|
+
...(input.__suppressNotifications === true ? { __suppressNotifications: true } : {}),
|
|
326
|
+
...(input.__requestUserId === undefined
|
|
327
|
+
? {}
|
|
328
|
+
: { __requestUserId: input.__requestUserId }),
|
|
329
|
+
...(input.__usageGrant === undefined ? {} : { __usageGrant: input.__usageGrant }),
|
|
330
|
+
...(input.secretEnvironment === undefined
|
|
331
|
+
? {}
|
|
332
|
+
: { secretEnvironment: input.secretEnvironment }),
|
|
333
|
+
...(input.personalInstructions === undefined
|
|
334
|
+
? {}
|
|
335
|
+
: { personalInstructions: input.personalInstructions }),
|
|
336
|
+
[RUNNER_EXECUTION_CONTEXT]: executionContext,
|
|
337
|
+
...(input.computerUse === undefined ? {} : { computerUse: input.computerUse }),
|
|
338
|
+
...(input.attachmentIds === undefined ? {} : { attachmentIds: input.attachmentIds }),
|
|
339
|
+
...(input.attachments === undefined ? {} : { attachments: input.attachments })
|
|
340
|
+
})
|
|
341
|
+
: await goalStart(goalSession, executionContext));
|
|
316
342
|
const nativeSessionId = await identity;
|
|
317
343
|
const session = this.options.runtime.getAgentSession(nativeSessionId);
|
|
318
344
|
const run = this.options.runtime.getRun(message.run.id);
|
|
@@ -5,6 +5,7 @@ import { sessionTitleFromFirstMessage } from './input-validation.js';
|
|
|
5
5
|
import { isImageAttachment, isRunnerImageAttachment, parseComposerAttachmentIds, parseMessageAttachments, withNonImageAttachmentPrompt } from './attachments.js';
|
|
6
6
|
import { parseSecretEnvironment } from '../mcp/secret-environment.js';
|
|
7
7
|
import { personalInstructionsForRunner } from '../runner/personal-instructions.js';
|
|
8
|
+
import { checkSessionAdmission } from './operation-admission.js';
|
|
8
9
|
import { safeErrorCode } from '../util/safe-error.js';
|
|
9
10
|
import { publicGoalStartText } from '../service/goal-runtime.js';
|
|
10
11
|
import { serializeComposerTag, composerVisibleText, steerComposerMessage } from '@myagentroam/protocol';
|
|
@@ -45,12 +46,17 @@ export class SessionMessageService {
|
|
|
45
46
|
__suppressNotifications: input.suppressNotifications === true ||
|
|
46
47
|
(input.kind === 'continue' &&
|
|
47
48
|
this.options.runtime.activeSessionRun(input.sessionId)?.suppressNotifications === true),
|
|
48
|
-
clientMessageId: `goal:${randomUUID()}`,
|
|
49
|
+
clientMessageId: input.clientMessageId ?? `goal:${randomUUID()}`,
|
|
49
50
|
content: input.kind === 'start'
|
|
50
|
-
? publicGoalStartText(input.objective ?? '')
|
|
51
|
+
? [publicGoalStartText(input.objective ?? ''), input.messageContent]
|
|
52
|
+
.filter(Boolean)
|
|
53
|
+
.join('\n')
|
|
51
54
|
: serializeComposerTag('mar_goal_continue', {}),
|
|
52
|
-
__runnerContent: input.content,
|
|
55
|
+
__runnerContent: [input.content, input.messageContent].filter(Boolean).join('\n\n'),
|
|
53
56
|
__goalRef: input.goal,
|
|
57
|
+
...(input.messageOrigin === undefined ? {} : { __marctlOrigin: input.messageOrigin }),
|
|
58
|
+
...(input.attachments === undefined ? {} : { attachments: input.attachments }),
|
|
59
|
+
...(input.attachmentIds === undefined ? {} : { attachmentIds: input.attachmentIds }),
|
|
54
60
|
...(input.initiatedByUserId === undefined
|
|
55
61
|
? {}
|
|
56
62
|
: { __requestUserId: input.initiatedByUserId }),
|
|
@@ -84,6 +90,7 @@ export class SessionMessageService {
|
|
|
84
90
|
return task.finally(() => this.preparations.delete(task));
|
|
85
91
|
}
|
|
86
92
|
async prepareMessage(input) {
|
|
93
|
+
checkSessionAdmission(input);
|
|
87
94
|
if (typeof input.sessionId !== 'string' ||
|
|
88
95
|
typeof input.clientMessageId !== 'string' ||
|
|
89
96
|
input.clientMessageId.length === 0 ||
|
|
@@ -107,6 +114,7 @@ export class SessionMessageService {
|
|
|
107
114
|
if (inlineAttachments.length > 0 && attachmentIds.length > 0)
|
|
108
115
|
throw new Error('MESSAGE_ATTACHMENTS_INVALID');
|
|
109
116
|
const accepted = await this.options.resolve(input.sessionId);
|
|
117
|
+
checkSessionAdmission(input);
|
|
110
118
|
const session = accepted.session;
|
|
111
119
|
if (input.workspaceId !== undefined && session.workspaceId !== input.workspaceId)
|
|
112
120
|
throw new Error('SESSION_NOT_FOUND');
|
|
@@ -135,20 +143,22 @@ export class SessionMessageService {
|
|
|
135
143
|
idempotent: true,
|
|
136
144
|
auditReceipt: auditReceipt(session, existing.kind === 'queue' ? 'QUEUE' : 'SEND', existing.kind === 'queue' ? existing.item.id : existing.run.id, existing.kind === 'queue' ? existing.item.id : undefined)
|
|
137
145
|
};
|
|
146
|
+
if (session.parentSessionId != null)
|
|
147
|
+
this.options.runners.require(session.runner).assertChildModel(session.model);
|
|
138
148
|
const executionContext = input[RUNNER_EXECUTION_CONTEXT] ??
|
|
139
149
|
this.options.runners.require(session.runner).prepareExecutionContext(input);
|
|
140
|
-
const sourceContent = withSessionMessageOrigin(content, input.__marctlOrigin);
|
|
150
|
+
const sourceContent = withSessionMessageOrigin(content, input.__marctlOrigin, session.parentSessionId);
|
|
141
151
|
const goalMessage = internalGoal || internalSchedule || internalRestore
|
|
142
152
|
? undefined
|
|
143
153
|
: this.options.goalMessage(session, content, requestedIntent);
|
|
144
154
|
const executionContent = goalMessage?.runnerContent ?? internalRunnerContent;
|
|
145
155
|
const runnerContent = executionContent === content
|
|
146
156
|
? sourceContent
|
|
147
|
-
: withSessionMessageOrigin(executionContent, input.__marctlOrigin);
|
|
157
|
+
: withSessionMessageOrigin(executionContent, input.__marctlOrigin, session.parentSessionId);
|
|
148
158
|
const publicContent = goalMessage?.activated === true
|
|
149
159
|
? withSessionMessageOrigin([publicGoalStartText(composerVisibleText(content)), composerResourceInputTags(content)]
|
|
150
160
|
.filter(Boolean)
|
|
151
|
-
.join('\n'), input.__marctlOrigin)
|
|
161
|
+
.join('\n'), input.__marctlOrigin, session.parentSessionId)
|
|
152
162
|
: sourceContent;
|
|
153
163
|
const intent = goalMessage?.activated === true ? 'REPLACE_CURRENT' : requestedIntent;
|
|
154
164
|
const releasePreparation = this.options.attachments.protectSession(session.id);
|
package/dist/supervisor.js
CHANGED
|
@@ -6,6 +6,9 @@ import { nodeLog, platformCliInvocation } from './operational.js';
|
|
|
6
6
|
import { nodeUpgradeRequestPath } from './service/node-upgrade-coordinator.js';
|
|
7
7
|
import { safeErrorCode } from './util/safe-error.js';
|
|
8
8
|
import { NODE_OWNER_CONFLICT_EXIT_CODE } from './node-process-lock.js';
|
|
9
|
+
import { runSupervisedNodeChild } from './service/supervised-node-child.js';
|
|
10
|
+
import { NodeUpgradeReporter } from './service/node-upgrade-reporter.js';
|
|
11
|
+
import { terminateProcessTree } from './process-tree.js';
|
|
9
12
|
const UPGRADE_EXIT_CODE = 75;
|
|
10
13
|
const STABLE_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/u;
|
|
11
14
|
export async function superviseNode(configPath, bootstrapEntrypoint) {
|
|
@@ -13,26 +16,92 @@ export async function superviseNode(configPath, bootstrapEntrypoint) {
|
|
|
13
16
|
const dataDirectory = dirname(nodeDatabasePath(config, configPath));
|
|
14
17
|
const activePath = resolve(dataDirectory, 'node-active-entrypoint');
|
|
15
18
|
let entrypoint = await readActiveEntrypoint(activePath, bootstrapEntrypoint);
|
|
19
|
+
let reporter;
|
|
20
|
+
let awaitingOnline;
|
|
16
21
|
for (;;) {
|
|
17
|
-
const
|
|
22
|
+
const requestPath = nodeUpgradeRequestPath(nodeDatabasePath(config, configPath));
|
|
23
|
+
let reportingIdentity;
|
|
24
|
+
const childResult = await runSupervisedNodeChild(entrypoint, configPath, async (handoff) => {
|
|
25
|
+
const currentConfig = await loadNodeConfig(configPath);
|
|
26
|
+
if (currentConfig.nodeId === undefined ||
|
|
27
|
+
!STABLE_VERSION.test(handoff.targetVersion) ||
|
|
28
|
+
!/^[0-9a-f-]{36}$/u.test(handoff.attemptId) ||
|
|
29
|
+
!/^[A-Za-z0-9_-]{43}$/u.test(handoff.credential))
|
|
30
|
+
return false;
|
|
31
|
+
const request = JSON.parse(await readFile(requestPath, 'utf8'));
|
|
32
|
+
const matches = request['schemaVersion'] === 1 &&
|
|
33
|
+
request['attemptId'] === handoff.attemptId &&
|
|
34
|
+
request['targetVersion'] === handoff.targetVersion;
|
|
35
|
+
if (matches)
|
|
36
|
+
reportingIdentity = { serverUrl: currentConfig.serverUrl, nodeId: currentConfig.nodeId };
|
|
37
|
+
return matches;
|
|
38
|
+
}, (handoff) => {
|
|
39
|
+
if (reportingIdentity === undefined)
|
|
40
|
+
throw new Error('NODE_UPGRADE_HANDOFF_UNAVAILABLE');
|
|
41
|
+
const next = new NodeUpgradeReporter(reportingIdentity.serverUrl, reportingIdentity.nodeId, handoff.credential);
|
|
42
|
+
reporter?.stop();
|
|
43
|
+
reporter = next;
|
|
44
|
+
next.update({ attemptId: handoff.attemptId, step: 'CLOSING_NODE' });
|
|
45
|
+
}, awaitingOnline);
|
|
18
46
|
if (childResult.signalled) {
|
|
47
|
+
reporter?.stop();
|
|
19
48
|
process.exitCode = childResult.code ?? 1;
|
|
20
49
|
return;
|
|
21
50
|
}
|
|
51
|
+
if (childResult.reclaimFailed) {
|
|
52
|
+
if (childResult.handoff !== undefined)
|
|
53
|
+
await reporter?.finish({
|
|
54
|
+
attemptId: childResult.handoff.attemptId,
|
|
55
|
+
step: 'CLOSING_NODE',
|
|
56
|
+
result: 'FAILED',
|
|
57
|
+
failure: {
|
|
58
|
+
code: 'NODE_UPGRADE_RECLAIM_FAILED',
|
|
59
|
+
message: 'The old Node process could not be stopped.',
|
|
60
|
+
step: 'CLOSING_NODE',
|
|
61
|
+
kind: 'INSTALL'
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
process.exitCode = 1;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (childResult.forced)
|
|
68
|
+
nodeLog('node.supervisor.upgrade-old-node-reclaimed', {
|
|
69
|
+
code: 'NODE_UPGRADE_EXIT_TIMEOUT_RECLAIMED'
|
|
70
|
+
});
|
|
22
71
|
const code = childResult.code;
|
|
23
|
-
if (
|
|
72
|
+
if (childResult.handoff === undefined &&
|
|
73
|
+
awaitingOnline !== undefined &&
|
|
74
|
+
reporter !== undefined) {
|
|
75
|
+
await reporter.finish({
|
|
76
|
+
attemptId: awaitingOnline,
|
|
77
|
+
step: 'WAITING_FOR_ONLINE',
|
|
78
|
+
result: 'FAILED',
|
|
79
|
+
operationEnded: true,
|
|
80
|
+
failure: {
|
|
81
|
+
code: 'NODE_UPGRADE_START_FAILED',
|
|
82
|
+
message: 'The upgraded Node process exited before reporting online.',
|
|
83
|
+
step: 'WAITING_FOR_ONLINE',
|
|
84
|
+
kind: 'START'
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
reporter = undefined;
|
|
88
|
+
}
|
|
89
|
+
if (code === NODE_OWNER_CONFLICT_EXIT_CODE && childResult.handoff === undefined) {
|
|
24
90
|
process.exitCode = code;
|
|
25
91
|
return;
|
|
26
92
|
}
|
|
27
|
-
if (code !== UPGRADE_EXIT_CODE) {
|
|
93
|
+
if (code !== UPGRADE_EXIT_CODE && childResult.handoff === undefined) {
|
|
28
94
|
if (code === 0)
|
|
29
95
|
return;
|
|
30
96
|
nodeLog('node.supervisor.child-exited', { code });
|
|
31
97
|
await delay(1_000);
|
|
32
98
|
continue;
|
|
33
99
|
}
|
|
34
|
-
const requestPath = nodeUpgradeRequestPath(nodeDatabasePath(config, configPath));
|
|
35
100
|
let stopping = false;
|
|
101
|
+
let switched = false;
|
|
102
|
+
let installReclaimFailed = false;
|
|
103
|
+
let failureStage = 'VALIDATE_REQUEST';
|
|
104
|
+
let failureExitCode;
|
|
36
105
|
try {
|
|
37
106
|
const request = JSON.parse(await readFile(requestPath, 'utf8'));
|
|
38
107
|
const targetVersion = request['targetVersion'];
|
|
@@ -41,6 +110,10 @@ export async function superviseNode(configPath, bootstrapEntrypoint) {
|
|
|
41
110
|
!STABLE_VERSION.test(targetVersion))
|
|
42
111
|
throw new Error('NODE_UPGRADE_REQUEST_INVALID');
|
|
43
112
|
const prefix = resolve(dataDirectory, 'node-versions', targetVersion);
|
|
113
|
+
const installDeadline = Date.now() + 15 * 60_000;
|
|
114
|
+
if (childResult.handoff !== undefined)
|
|
115
|
+
reporter?.update({ attemptId: childResult.handoff.attemptId, step: 'INSTALLING' });
|
|
116
|
+
failureStage = 'PREPARE_INSTALL_DIRECTORY';
|
|
44
117
|
await rm(prefix, { recursive: true, force: true });
|
|
45
118
|
await mkdir(prefix, { recursive: true, mode: 0o700 });
|
|
46
119
|
const invocation = platformCliInvocation('npm', [
|
|
@@ -52,62 +125,166 @@ export async function superviseNode(configPath, bootstrapEntrypoint) {
|
|
|
52
125
|
'--no-fund',
|
|
53
126
|
`@myagentroam/node@${targetVersion}`
|
|
54
127
|
]);
|
|
55
|
-
|
|
128
|
+
failureStage = 'NPM_INSTALL';
|
|
129
|
+
const installResult = await runProcess(invocation.command, invocation.args, false, installDeadline);
|
|
130
|
+
failureExitCode = installResult.code;
|
|
131
|
+
if (installResult.reclaimFailed) {
|
|
132
|
+
installReclaimFailed = true;
|
|
133
|
+
stopping = true;
|
|
134
|
+
throw new Error('NODE_UPGRADE_INSTALL_RECLAIM_FAILED');
|
|
135
|
+
}
|
|
56
136
|
if (installResult.signalled) {
|
|
57
137
|
stopping = true;
|
|
58
138
|
throw new Error('NODE_UPGRADE_INSTALL_INTERRUPTED');
|
|
59
139
|
}
|
|
140
|
+
if (installResult.timedOut)
|
|
141
|
+
throw new Error('NODE_UPGRADE_INSTALL_TIMEOUT');
|
|
60
142
|
if (installResult.code !== 0)
|
|
61
143
|
throw new Error('NODE_UPGRADE_INSTALL_FAILED');
|
|
62
144
|
const rebuild = platformCliInvocation('npm', ['rebuild', '--prefix', prefix, 'node-pty']);
|
|
63
|
-
|
|
145
|
+
failureStage = 'NATIVE_REBUILD';
|
|
146
|
+
failureExitCode = undefined;
|
|
147
|
+
const rebuildResult = await runProcess(rebuild.command, rebuild.args, false, installDeadline);
|
|
148
|
+
failureExitCode = rebuildResult.code;
|
|
149
|
+
if (rebuildResult.reclaimFailed) {
|
|
150
|
+
installReclaimFailed = true;
|
|
151
|
+
stopping = true;
|
|
152
|
+
throw new Error('NODE_UPGRADE_INSTALL_RECLAIM_FAILED');
|
|
153
|
+
}
|
|
64
154
|
if (rebuildResult.signalled) {
|
|
65
155
|
stopping = true;
|
|
66
156
|
throw new Error('NODE_UPGRADE_INSTALL_INTERRUPTED');
|
|
67
157
|
}
|
|
158
|
+
if (rebuildResult.timedOut)
|
|
159
|
+
throw new Error('NODE_UPGRADE_INSTALL_TIMEOUT');
|
|
68
160
|
if (rebuildResult.code !== 0)
|
|
69
161
|
throw new Error('NODE_UPGRADE_NATIVE_REBUILD_FAILED');
|
|
162
|
+
failureStage = 'BROWSER_INSTALL';
|
|
163
|
+
failureExitCode = undefined;
|
|
70
164
|
const browserInstall = await runProcess(process.execPath, [
|
|
71
165
|
resolve(prefix, 'node_modules', '@myagentroam', 'node', 'scripts', 'install-web-browser.mjs')
|
|
72
|
-
], true);
|
|
166
|
+
], true, installDeadline);
|
|
167
|
+
failureExitCode = browserInstall.code;
|
|
168
|
+
if (browserInstall.reclaimFailed) {
|
|
169
|
+
installReclaimFailed = true;
|
|
170
|
+
stopping = true;
|
|
171
|
+
throw new Error('NODE_UPGRADE_INSTALL_RECLAIM_FAILED');
|
|
172
|
+
}
|
|
73
173
|
if (browserInstall.signalled) {
|
|
74
174
|
stopping = true;
|
|
75
175
|
throw new Error('NODE_UPGRADE_INSTALL_INTERRUPTED');
|
|
76
176
|
}
|
|
177
|
+
if (browserInstall.timedOut)
|
|
178
|
+
throw new Error('NODE_UPGRADE_INSTALL_TIMEOUT');
|
|
77
179
|
if (browserInstall.code !== 0)
|
|
78
180
|
throw new Error('NODE_UPGRADE_BROWSER_INSTALL_FAILED');
|
|
181
|
+
failureStage = 'ENTRYPOINT_SWITCH';
|
|
182
|
+
failureExitCode = undefined;
|
|
79
183
|
const nextEntrypoint = resolve(prefix, 'node_modules', '@myagentroam', 'node', 'dist', 'main.js');
|
|
80
184
|
await access(nextEntrypoint);
|
|
81
185
|
await writeAtomic(activePath, `${nextEntrypoint}\n`);
|
|
186
|
+
entrypoint = nextEntrypoint;
|
|
187
|
+
switched = true;
|
|
188
|
+
if (childResult.handoff !== undefined) {
|
|
189
|
+
awaitingOnline = childResult.handoff.attemptId;
|
|
190
|
+
reporter?.update({
|
|
191
|
+
attemptId: childResult.handoff.attemptId,
|
|
192
|
+
step: 'WAITING_FOR_ONLINE',
|
|
193
|
+
result: 'INSTALLED'
|
|
194
|
+
});
|
|
195
|
+
}
|
|
82
196
|
await writeResult(dataDirectory, {
|
|
83
197
|
state: 'SUCCEEDED',
|
|
84
198
|
targetVersion,
|
|
85
199
|
updatedAt: Date.now()
|
|
86
200
|
});
|
|
87
|
-
await rm(requestPath, { force: true });
|
|
88
|
-
entrypoint = nextEntrypoint;
|
|
89
201
|
}
|
|
90
202
|
catch (error) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
203
|
+
if (switched) {
|
|
204
|
+
nodeLog('node.supervisor.upgrade-result-write-failed', {
|
|
205
|
+
code: 'NODE_UPGRADE_RESULT_WRITE_FAILED'
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
if (childResult.handoff !== undefined)
|
|
210
|
+
await reporter?.finish({
|
|
211
|
+
attemptId: childResult.handoff.attemptId,
|
|
212
|
+
step: 'INSTALLING',
|
|
213
|
+
result: 'FAILED',
|
|
214
|
+
...(installReclaimFailed ? {} : { operationEnded: true }),
|
|
215
|
+
failure: {
|
|
216
|
+
code: safeErrorCode(error, 'NODE_UPGRADE_INSTALL_FAILED'),
|
|
217
|
+
message: 'The Node upgrade could not be installed.',
|
|
218
|
+
step: 'INSTALLING',
|
|
219
|
+
kind: 'INSTALL',
|
|
220
|
+
details: `Stage: ${failureStage}${failureExitCode == null ? '' : `; exit code: ${failureExitCode}`}`
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
reporter = undefined;
|
|
224
|
+
try {
|
|
225
|
+
await writeResult(dataDirectory, {
|
|
226
|
+
state: 'FAILED',
|
|
227
|
+
errorCode: safeErrorCode(error, 'NODE_UPGRADE_INSTALL_FAILED'),
|
|
228
|
+
updatedAt: Date.now()
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
nodeLog('node.supervisor.upgrade-result-write-failed', {
|
|
233
|
+
code: 'NODE_UPGRADE_RESULT_WRITE_FAILED'
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
96
239
|
await rm(requestPath, { force: true });
|
|
97
240
|
}
|
|
241
|
+
catch {
|
|
242
|
+
nodeLog('node.supervisor.upgrade-request-cleanup-failed', {
|
|
243
|
+
code: 'NODE_UPGRADE_REQUEST_CLEANUP_FAILED'
|
|
244
|
+
});
|
|
245
|
+
}
|
|
98
246
|
if (stopping)
|
|
99
247
|
return;
|
|
100
248
|
}
|
|
101
249
|
}
|
|
102
|
-
async function runProcess(command, args, inherit,
|
|
250
|
+
async function runProcess(command, args, inherit, deadlineAt) {
|
|
103
251
|
return new Promise((resolveProcess) => {
|
|
104
252
|
const child = spawn(command, args, {
|
|
105
253
|
windowsHide: true,
|
|
106
254
|
stdio: inherit ? 'inherit' : 'ignore',
|
|
107
|
-
env
|
|
255
|
+
env: process.env,
|
|
256
|
+
detached: process.platform !== 'win32'
|
|
108
257
|
});
|
|
109
258
|
let settled = false;
|
|
110
259
|
let signalled = false;
|
|
260
|
+
let timedOut = false;
|
|
261
|
+
let reclaimed = false;
|
|
262
|
+
let exited;
|
|
263
|
+
let reapDeadline;
|
|
264
|
+
const deadline = setTimeout(() => {
|
|
265
|
+
timedOut = true;
|
|
266
|
+
reapDeadline = setTimeout(() => finish(null, true), 10_000);
|
|
267
|
+
void terminateProcessTree(child)
|
|
268
|
+
.then(() => {
|
|
269
|
+
reclaimed = true;
|
|
270
|
+
if (exited !== undefined)
|
|
271
|
+
finish(exited.code);
|
|
272
|
+
})
|
|
273
|
+
.catch(() => finish(null, true));
|
|
274
|
+
if (process.platform !== 'win32' && child.pid !== undefined) {
|
|
275
|
+
try {
|
|
276
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
try {
|
|
280
|
+
child.kill('SIGKILL');
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
/* Wait for exit or timeout. */
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}, Math.max(0, deadlineAt - Date.now()));
|
|
111
288
|
const forward = (signal) => {
|
|
112
289
|
if (signalled)
|
|
113
290
|
return;
|
|
@@ -116,18 +293,30 @@ async function runProcess(command, args, inherit, env = process.env) {
|
|
|
116
293
|
};
|
|
117
294
|
const onSigint = () => forward('SIGINT');
|
|
118
295
|
const onSigterm = () => forward('SIGTERM');
|
|
119
|
-
const finish = (code) => {
|
|
296
|
+
const finish = (code, reclaimFailed = false) => {
|
|
120
297
|
if (settled)
|
|
121
298
|
return;
|
|
122
299
|
settled = true;
|
|
300
|
+
clearTimeout(deadline);
|
|
301
|
+
if (reapDeadline !== undefined)
|
|
302
|
+
clearTimeout(reapDeadline);
|
|
123
303
|
process.off('SIGINT', onSigint);
|
|
124
304
|
process.off('SIGTERM', onSigterm);
|
|
125
|
-
|
|
305
|
+
if (reclaimFailed)
|
|
306
|
+
child.unref();
|
|
307
|
+
resolveProcess({ code, signalled, timedOut, reclaimFailed });
|
|
126
308
|
};
|
|
127
309
|
process.on('SIGINT', onSigint);
|
|
128
310
|
process.on('SIGTERM', onSigterm);
|
|
129
|
-
child.
|
|
130
|
-
|
|
311
|
+
child.on('error', () => {
|
|
312
|
+
if (child.pid === undefined)
|
|
313
|
+
finish(null);
|
|
314
|
+
});
|
|
315
|
+
child.once('exit', (code) => {
|
|
316
|
+
exited = { code };
|
|
317
|
+
if (!timedOut || reclaimed)
|
|
318
|
+
finish(code);
|
|
319
|
+
});
|
|
131
320
|
});
|
|
132
321
|
}
|
|
133
322
|
async function readActiveEntrypoint(path, fallback) {
|
package/dist/terminal.js
CHANGED
|
@@ -32,6 +32,7 @@ export class TerminalManager {
|
|
|
32
32
|
pipeFactory;
|
|
33
33
|
terminate;
|
|
34
34
|
terminals = new Map();
|
|
35
|
+
closing = new Map();
|
|
35
36
|
attachments = new Map();
|
|
36
37
|
frameListeners = new Set();
|
|
37
38
|
summaryListeners = new Set();
|
|
@@ -309,37 +310,56 @@ export class TerminalManager {
|
|
|
309
310
|
}
|
|
310
311
|
await this.close(terminal.id);
|
|
311
312
|
}
|
|
312
|
-
|
|
313
|
+
close(terminalId) {
|
|
313
314
|
const terminal = this.requireTerminal(terminalId);
|
|
315
|
+
const pending = this.closing.get(terminalId);
|
|
316
|
+
if (pending !== undefined)
|
|
317
|
+
return pending;
|
|
314
318
|
if (terminal.state !== 'RUNNING')
|
|
315
|
-
return this.summary(terminal);
|
|
319
|
+
return Promise.resolve(this.summary(terminal));
|
|
316
320
|
terminal.state = 'CLOSED';
|
|
317
321
|
terminal.updatedAt = this.now();
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
322
|
+
let resolveClosing;
|
|
323
|
+
let rejectClosing;
|
|
324
|
+
const closing = new Promise((resolve, reject) => {
|
|
325
|
+
resolveClosing = resolve;
|
|
326
|
+
rejectClosing = reject;
|
|
327
|
+
});
|
|
328
|
+
this.closing.set(terminalId, closing);
|
|
329
|
+
void (async () => {
|
|
330
|
+
this.detachAll(terminal);
|
|
331
|
+
const summary = this.notifySummary(terminal);
|
|
332
|
+
try {
|
|
333
|
+
if (terminal.pty !== undefined) {
|
|
334
|
+
// Native PTY hangup reaches POSIX foreground jobs in a separate process group
|
|
335
|
+
// and releases the native console on Windows; shell-group TERM cannot do this.
|
|
336
|
+
try {
|
|
337
|
+
terminal.pty.kill();
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
await this.terminate(terminal.pty, this.platform);
|
|
341
|
+
}
|
|
329
342
|
}
|
|
343
|
+
if (terminal.pipe !== undefined)
|
|
344
|
+
await this.terminate(terminal.pipe, this.platform);
|
|
330
345
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
346
|
+
finally {
|
|
347
|
+
this.emit({ type: 'exit', terminalId: terminal.id, exitCode: terminal.exitCode });
|
|
348
|
+
}
|
|
349
|
+
return summary;
|
|
350
|
+
})().then((summary) => {
|
|
351
|
+
this.closing.delete(terminalId);
|
|
352
|
+
resolveClosing(summary);
|
|
353
|
+
}, (error) => {
|
|
354
|
+
this.closing.delete(terminalId);
|
|
355
|
+
rejectClosing(error);
|
|
356
|
+
});
|
|
357
|
+
return closing;
|
|
338
358
|
}
|
|
339
359
|
/** Ends current terminals without disposing the manager's idle collection. */
|
|
340
360
|
async closeAll() {
|
|
341
361
|
await Promise.all([...this.terminals.values()]
|
|
342
|
-
.filter((terminal) => terminal.state === 'RUNNING')
|
|
362
|
+
.filter((terminal) => terminal.state === 'RUNNING' || this.closing.has(terminal.id))
|
|
343
363
|
.map((terminal) => this.close(terminal.id)));
|
|
344
364
|
}
|
|
345
365
|
async shutdown() {
|