@myagentroam/node 0.11.48 → 0.11.50
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/application/node-lifecycle.js +1 -1
- package/dist/application/node-operation-admission.js +1 -7
- package/dist/connector.js +14 -20
- package/dist/marctl-guidance.js +7 -5
- package/dist/runner/mar-agent/agent-network-extension.js +30 -39
- package/dist/runner/mar-agent/managed-run-controller.js +38 -0
- package/dist/runner/mar-agent/runtime-model-factory.js +42 -6
- package/dist/service/agent-network-bindings.js +2 -12
- package/dist/service/agent-network-service.js +8 -20
- package/dist/service/continuous-run-coordinator.js +3 -0
- package/dist/service/goal-continuation-coordinator.js +10 -0
- package/dist/service/planned-restart-handoff.js +82 -46
- package/dist/service/restart-execution-state.js +77 -0
- package/dist/service/restart-session-restorer.js +40 -31
- package/dist/service/run-authorization-control.js +5 -9
- package/dist/service/session-recent-activity.js +3 -0
- package/dist/session/runtime/work-process.js +2 -4
- package/dist/session/session-catalog-service.js +3 -2
- package/dist/session/session-message-service.js +15 -6
- package/package.json +4 -4
|
@@ -23,7 +23,6 @@ export class NodeOperationAdmission {
|
|
|
23
23
|
if (this.stopped())
|
|
24
24
|
throw new Error('NODE_STOPPED');
|
|
25
25
|
const input = isPlainRecord(data) ? data : {};
|
|
26
|
-
const sessionId = typeof input.sessionId === 'string' ? input.sessionId : undefined;
|
|
27
26
|
if (this.restart.blocksOperation(operation, input))
|
|
28
27
|
throw new Error('SESSION_RESTART_PENDING');
|
|
29
28
|
if (!this.databaseAvailable())
|
|
@@ -34,12 +33,7 @@ export class NodeOperationAdmission {
|
|
|
34
33
|
const task = this.git.execute(operation, data, (value) => this.router.execute(operation, value));
|
|
35
34
|
this.inFlight.add(task);
|
|
36
35
|
try {
|
|
37
|
-
|
|
38
|
-
if ((operation === 'session.command.execute' || operation === 'session.message') &&
|
|
39
|
-
sessionId !== undefined &&
|
|
40
|
-
Number.isSafeInteger(input.__requestUserId))
|
|
41
|
-
this.restart.rememberUser(sessionId, input.__requestUserId);
|
|
42
|
-
return result;
|
|
36
|
+
return await task;
|
|
43
37
|
}
|
|
44
38
|
finally {
|
|
45
39
|
this.inFlight.delete(task);
|
package/dist/connector.js
CHANGED
|
@@ -67,7 +67,7 @@ import { SessionScheduleService } from './session/session-schedule-service.js';
|
|
|
67
67
|
import { ScheduledSessionCoordinator } from './service/scheduled-session-coordinator.js';
|
|
68
68
|
import { ScheduledSessionStore } from './service/scheduled-session-store.js';
|
|
69
69
|
import { RunAuthorizationControl } from './service/run-authorization-control.js';
|
|
70
|
-
import { PlannedRestartHandoff } from './service/planned-restart-handoff.js';
|
|
70
|
+
import { PlannedRestartHandoff, plannedRestartCaptureDiagnostic } from './service/planned-restart-handoff.js';
|
|
71
71
|
import { NodeUpgradeCredentialControl } from './service/node-upgrade-credential-control.js';
|
|
72
72
|
import { RestartSessionRestorer } from './service/restart-session-restorer.js';
|
|
73
73
|
import { ScheduleMcpBridge } from './service/schedule-mcp-bridge.js';
|
|
@@ -159,7 +159,6 @@ export class NodeConnector {
|
|
|
159
159
|
controlChannel;
|
|
160
160
|
controlMessages;
|
|
161
161
|
scheduledRunAuthorization;
|
|
162
|
-
restartAuthorization;
|
|
163
162
|
restartHandoff;
|
|
164
163
|
operationAdmission;
|
|
165
164
|
executionLifecycle;
|
|
@@ -195,12 +194,18 @@ export class NodeConnector {
|
|
|
195
194
|
await this.sessionMessageService.drain();
|
|
196
195
|
},
|
|
197
196
|
closeResume: () => this.externalSessionResumeService.close(),
|
|
198
|
-
capture: () =>
|
|
197
|
+
capture: async () => {
|
|
198
|
+
if (!this.config?.nodeId)
|
|
199
|
+
return;
|
|
200
|
+
await this.marAgentRuntimeModels.waitForPendingCredentials();
|
|
201
|
+
return this.restartHandoff.capture();
|
|
202
|
+
},
|
|
199
203
|
dispose: () => this.disposeRuntime(),
|
|
200
204
|
publish: (snapshot) => this.restartHandoff.publish(snapshot),
|
|
201
205
|
diagnose: (stage, error) => nodeLog('node.shutdown.failed', {
|
|
202
206
|
stage,
|
|
203
|
-
errorCode: safeErrorCode(error, 'NODE_SHUTDOWN_FAILED')
|
|
207
|
+
errorCode: safeErrorCode(error, 'NODE_SHUTDOWN_FAILED'),
|
|
208
|
+
...(stage === 'capture' ? plannedRestartCaptureDiagnostic(error) : undefined)
|
|
204
209
|
})
|
|
205
210
|
});
|
|
206
211
|
get stopped() {
|
|
@@ -1127,15 +1132,6 @@ export class NodeConnector {
|
|
|
1127
1132
|
coordinator: this.continuousRunCoordinator,
|
|
1128
1133
|
messages: this.sessionMessageService,
|
|
1129
1134
|
resolve: (id) => this.externalSessionResumeService.resolveForMessage(id),
|
|
1130
|
-
authorize: async (request) => {
|
|
1131
|
-
const context = await this.restartAuthorization.authorize(request);
|
|
1132
|
-
return {
|
|
1133
|
-
secretEnvironment: context.secretEnvironment,
|
|
1134
|
-
personalInstructions: context.personalInstructions,
|
|
1135
|
-
usageGrant: context.usageGrant,
|
|
1136
|
-
executionContext: this.runners.require(request.runner).prepareExecutionContext(context)
|
|
1137
|
-
};
|
|
1138
|
-
},
|
|
1139
1135
|
inputState,
|
|
1140
1136
|
withGrant: (context, work) => this.modelUsage.withGrant(context.usageGrant, work),
|
|
1141
1137
|
publishGoal: (session, goal) => this.goalCoordinator.publishGoalState(session, goal),
|
|
@@ -1145,13 +1141,16 @@ export class NodeConnector {
|
|
|
1145
1141
|
this.restartHandoff = new PlannedRestartHandoff({
|
|
1146
1142
|
directory: () => nodeDataDirectory(this.config, this.configPath),
|
|
1147
1143
|
identity: () => ({ nodeId: this.config.nodeId, serverUrl: this.config.serverUrl }),
|
|
1144
|
+
credential: () => this.config.credential,
|
|
1148
1145
|
runtime: this.runtime,
|
|
1149
1146
|
coordinator: this.continuousRunCoordinator,
|
|
1150
1147
|
goals: this.goalRuntime,
|
|
1148
|
+
goalExecution: (id) => this.goalCoordinator.restartExecution(id),
|
|
1149
|
+
executionContext: (id, context, modelId) => this.marAgentRunController.restartExecutionContext(id, context, modelId === undefined ? this.runtime.getAgentSession(id)?.modelId : modelId),
|
|
1151
1150
|
commandActive: (id, command) => this.commandStates.get(id, command)?.active === true,
|
|
1152
1151
|
inputState,
|
|
1153
1152
|
restore: (entry, id) => restorer.restore(entry, id),
|
|
1154
|
-
diagnose: (code, sessionId) => nodeLog('node.restart.diagnostic', { code, sessionId })
|
|
1153
|
+
diagnose: (code, sessionId, errorCode) => nodeLog('node.restart.diagnostic', { code, sessionId, errorCode })
|
|
1155
1154
|
});
|
|
1156
1155
|
this.sessionCatalogService = new SessionCatalogService({
|
|
1157
1156
|
resolveExecutionDirectory,
|
|
@@ -1292,7 +1291,6 @@ export class NodeConnector {
|
|
|
1292
1291
|
this.runtimeCredentials.disconnected();
|
|
1293
1292
|
this.chatgptLogins.close();
|
|
1294
1293
|
this.scheduledRunAuthorization.disconnected();
|
|
1295
|
-
this.restartAuthorization.disconnected();
|
|
1296
1294
|
this.mailNotificationControl.disconnected();
|
|
1297
1295
|
this.marctlExecutionCredentials.disconnected();
|
|
1298
1296
|
this.upgradeCredentials.disconnect();
|
|
@@ -1352,8 +1350,7 @@ export class NodeConnector {
|
|
|
1352
1350
|
handleNodeRequest: (envelope) => this.nodeRequests.handle(envelope),
|
|
1353
1351
|
cancelNodeWait: (requestId) => this.nodeRequests.cancelWait(requestId),
|
|
1354
1352
|
handleRuntimeCredentialResponse: (envelope) => this.runtimeCredentials.handle(envelope),
|
|
1355
|
-
handleScheduledRunAuthorizeResponse: (envelope) => this.scheduledRunAuthorization.handle(envelope)
|
|
1356
|
-
this.restartAuthorization.handle(envelope),
|
|
1353
|
+
handleScheduledRunAuthorizeResponse: (envelope) => this.scheduledRunAuthorization.handle(envelope),
|
|
1357
1354
|
handleMailNotificationResponse: (envelope) => this.mailNotificationControl.handle(envelope),
|
|
1358
1355
|
handleMarctlExecutionCredentialResponse: (envelope) => this.marctlExecutionCredentials.handle(envelope),
|
|
1359
1356
|
handleUpgradeCredentialResponse: (envelope) => this.upgradeCredentials.handle(envelope),
|
|
@@ -1451,7 +1448,6 @@ export class NodeConnector {
|
|
|
1451
1448
|
this.runtimeCredentials.connected();
|
|
1452
1449
|
this.scheduledRunAuthorization.connected();
|
|
1453
1450
|
this.scheduledSessions.connected();
|
|
1454
|
-
this.restartAuthorization.connected();
|
|
1455
1451
|
if (!this.stopped)
|
|
1456
1452
|
void this.restartHandoff.recover();
|
|
1457
1453
|
this.mailNotificationControl.connected();
|
|
@@ -1465,7 +1461,6 @@ export class NodeConnector {
|
|
|
1465
1461
|
return this.database;
|
|
1466
1462
|
};
|
|
1467
1463
|
this.scheduledRunAuthorization = new RunAuthorizationControl((envelope) => this.controlChannel.sendEnvelope(envelope));
|
|
1468
|
-
this.restartAuthorization = new RunAuthorizationControl((envelope) => this.controlChannel.sendEnvelope(envelope), 'restart');
|
|
1469
1464
|
this.scheduledSessions = new ScheduledSessionCoordinator({
|
|
1470
1465
|
store: () => new ScheduledSessionStore(requireDatabase().raw),
|
|
1471
1466
|
busy: (task) => this.stopped ||
|
|
@@ -1810,7 +1805,6 @@ export class NodeConnector {
|
|
|
1810
1805
|
this.chatgptLogins.close();
|
|
1811
1806
|
const scheduledDrain = this.scheduledSessions.stop();
|
|
1812
1807
|
this.scheduledRunAuthorization.close();
|
|
1813
|
-
this.restartAuthorization.close();
|
|
1814
1808
|
this.mailNotificationControl.close();
|
|
1815
1809
|
this.marctlExecutionCredentials.disconnected();
|
|
1816
1810
|
this.upgradeCredentials.disconnect();
|
package/dist/marctl-guidance.js
CHANGED
|
@@ -6,7 +6,7 @@ export function hasMarctlContext(environment) {
|
|
|
6
6
|
!!environment['MAR_CTL_WORKSPACE_ID']);
|
|
7
7
|
}
|
|
8
8
|
export const marctlManagementDescription = 'Use marctl to manage MAR Agent session settings (model, effort, title, persistent prompts, and schedules), discover authorized MAR Nodes, and execute commands or transfer files on those Nodes. Use native agent_* tools for other Agent lifecycle and collaboration operations.';
|
|
9
|
-
const marctlSafety = '
|
|
9
|
+
const marctlSafety = 'If a request result is unknown, inspect history before retrying. Session IDs and parent relationships do not grant authorization.';
|
|
10
10
|
export const marctlManagementInstructions = [
|
|
11
11
|
'## MAR Workspace management',
|
|
12
12
|
'Use native agent_* tools for Agent discovery, creation, communication, observation, waiting and stopping. Do not switch to marctl when a native operation fails.',
|
|
@@ -19,10 +19,12 @@ export const marctlDescription = 'Use marctl agents to create, send to, wait for
|
|
|
19
19
|
export const marctlInstructions = [
|
|
20
20
|
'## MAR Workspace CLI',
|
|
21
21
|
'Run marctl from the shell. Commands use the authenticated Node and Workspace; marctl help agents, nodes, ssh or scp shows options.',
|
|
22
|
-
'Use agents start --title <title> -m <text> [--runner <runner>] [--model-id <id>] [--effort <effort>] [--goal <objective>]. An Agent must supply a nonblank title; a manual terminal may omit it. The default Runner is the current Session Runner; a terminal without a current Session must specify --runner or an explicit --parent. Start returns without waiting.',
|
|
23
|
-
'Use agents send <id> -m <text> [--steer|--queue], agents wait [<id>] [--wait-for message|completion] [--timeout <duration>], agents stop <id> --execution <execution-id>, agents list [--scope tree|active], and agents view <id> [--history].
|
|
24
|
-
'
|
|
25
|
-
'
|
|
22
|
+
'Use agents start --title <title> -m <text> [--runner <runner>] [--model-id <id>] [--effort <effort>] [--goal <objective>]. An Agent must supply a nonblank title; a manual terminal may omit it. The default Runner is the current Session Runner; a terminal without a current Session must specify --runner or an explicit --parent. Start returns without waiting for completion.',
|
|
23
|
+
'Use agents send <id> -m <text> [--steer|--queue], agents wait [<id>] [--wait-for message|completion] [--timeout <duration>], agents stop <id> --execution <execution-id>, agents list [--scope tree|active], and agents view <id> [--history]. --timeout 0ms takes a snapshot. Stop clears the target Session Goal.',
|
|
24
|
+
'While children run, prefer advancing meaningful, non-overlapping task work rather than duplicating delegated work. Waiting is generally useful once no meaningful local task work remains and a required result is needed to unblock or finish the task.',
|
|
25
|
+
'Prefer agents send <id> --steer -m <text> for timely communication or course correction. --queue is best reserved for deferred input explicitly requested by the user or recipient: it is consumed after current work and earlier queued inputs finish, and with an active Goal it is not consumed until that Goal exits. Acceptance is not a guarantee of immediate processing.',
|
|
26
|
+
'An accepted message normally needs no separate acknowledgment. Requests for receipt-only replies and immediate agents view checks solely to confirm receipt are usually unnecessary.',
|
|
27
|
+
'Send can update a Goal alone with --goal <objective> or --clear-goal; omitted Goal leaves it unchanged. Decide an authorized Agent approval with send <id> --approval <approval-id> --decision approve|reject.',
|
|
26
28
|
'List accepts only --scope tree|active, optional --query <title-fragment> and --limit <1..100> (default 100). With a current Session it defaults to the containing collaboration tree, including idle Sessions; without one it defaults to active Workspace Sessions (running or sleeping), and explicit tree is rejected. Returns currentAgentId (null without a Session), flat sessions with id, parentSessionId, title, runner and status, and truncated; no cursor or pagination. Reuse known IDs directly.',
|
|
27
29
|
'Only direct-child updates are collected automatically. Sending to a non-child never subscribes; an explicit wait observes it only for that call.',
|
|
28
30
|
'Use agents config, rename, prompt and schedule for management. marctl nodes, ssh and scp use authorized Nodes.',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AgentError } from '@myagentroam/agent';
|
|
1
2
|
import { agentNetworkWaitModelOutput } from './agent-network-wait-output.js';
|
|
2
3
|
import { agentOperationEvent } from './agent-operation-presentation.js';
|
|
3
4
|
const text = { type: 'string', minLength: 1 };
|
|
@@ -5,22 +6,6 @@ const runner = {
|
|
|
5
6
|
...text,
|
|
6
7
|
description: 'Runner type (for example mar-agent), not a model name; defaults to the current Runner.'
|
|
7
8
|
};
|
|
8
|
-
const attachmentIds = {
|
|
9
|
-
type: 'array',
|
|
10
|
-
items: text,
|
|
11
|
-
maxItems: 32,
|
|
12
|
-
description: 'Existing composer attachment IDs.'
|
|
13
|
-
};
|
|
14
|
-
const attachments = {
|
|
15
|
-
type: 'array',
|
|
16
|
-
maxItems: 5,
|
|
17
|
-
items: {
|
|
18
|
-
type: 'object',
|
|
19
|
-
additionalProperties: false,
|
|
20
|
-
required: ['name', 'mime', 'dataBase64'],
|
|
21
|
-
properties: { name: text, mime: text, dataBase64: text }
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
9
|
const goal = {
|
|
25
10
|
anyOf: [
|
|
26
11
|
{ type: 'null' },
|
|
@@ -38,15 +23,13 @@ const goal = {
|
|
|
38
23
|
}
|
|
39
24
|
]
|
|
40
25
|
};
|
|
41
|
-
const deliveryGuidance = '
|
|
26
|
+
const deliveryGuidance = 'STEER is the default and preferred for timely communication. QUEUE is best reserved for deferred delivery explicitly requested by the user or recipient, not merely to avoid interruption or send a report.';
|
|
42
27
|
const tools = [
|
|
43
28
|
{
|
|
44
29
|
action: 'start',
|
|
45
|
-
description: 'Start a child Agent with a
|
|
30
|
+
description: 'Start a child Agent with a task and optional Goal; returns without waiting for completion.',
|
|
46
31
|
properties: {
|
|
47
|
-
content:
|
|
48
|
-
attachments,
|
|
49
|
-
attachmentIds,
|
|
32
|
+
content: text,
|
|
50
33
|
runner,
|
|
51
34
|
modelId: {
|
|
52
35
|
...text,
|
|
@@ -57,16 +40,14 @@ const tools = [
|
|
|
57
40
|
goal,
|
|
58
41
|
clientMessageId: text
|
|
59
42
|
},
|
|
60
|
-
required: ['title']
|
|
43
|
+
required: ['title', 'content']
|
|
61
44
|
},
|
|
62
45
|
{
|
|
63
46
|
action: 'send',
|
|
64
|
-
description:
|
|
47
|
+
description: 'Send a message to an Agent, update its Goal, or decide an approval.',
|
|
65
48
|
properties: {
|
|
66
49
|
agentId: text,
|
|
67
50
|
content: { type: 'string' },
|
|
68
|
-
attachments,
|
|
69
|
-
attachmentIds,
|
|
70
51
|
goal,
|
|
71
52
|
approval: {
|
|
72
53
|
type: 'object',
|
|
@@ -77,7 +58,7 @@ const tools = [
|
|
|
77
58
|
delivery: {
|
|
78
59
|
type: 'string',
|
|
79
60
|
enum: ['STEER', 'QUEUE'],
|
|
80
|
-
description: deliveryGuidance
|
|
61
|
+
description: `${deliveryGuidance} QUEUE is consumed only after current work and earlier queued inputs finish; an active Goal must exit first.`
|
|
81
62
|
},
|
|
82
63
|
clientMessageId: text
|
|
83
64
|
},
|
|
@@ -85,7 +66,7 @@ const tools = [
|
|
|
85
66
|
},
|
|
86
67
|
{
|
|
87
68
|
action: 'wait',
|
|
88
|
-
description: 'Wait for
|
|
69
|
+
description: 'Wait for Agent messages or execution completion.',
|
|
89
70
|
properties: {
|
|
90
71
|
agentId: text,
|
|
91
72
|
agentIds: { type: 'array', items: text, minItems: 1, maxItems: 8 },
|
|
@@ -102,7 +83,7 @@ const tools = [
|
|
|
102
83
|
},
|
|
103
84
|
{
|
|
104
85
|
action: 'list',
|
|
105
|
-
description:
|
|
86
|
+
description: 'List the current collaboration tree or active Agents in the Workspace.',
|
|
106
87
|
properties: {
|
|
107
88
|
scope: {
|
|
108
89
|
type: 'string',
|
|
@@ -119,7 +100,7 @@ const tools = [
|
|
|
119
100
|
},
|
|
120
101
|
{
|
|
121
102
|
action: 'view',
|
|
122
|
-
description:
|
|
103
|
+
description: "View an Agent's state, Goal and public history.",
|
|
123
104
|
properties: {
|
|
124
105
|
agentId: text,
|
|
125
106
|
includeHistory: { type: 'boolean' },
|
|
@@ -131,17 +112,20 @@ const tools = [
|
|
|
131
112
|
];
|
|
132
113
|
const delegationPrompt = [
|
|
133
114
|
'# Delegation and coordination',
|
|
134
|
-
'
|
|
135
|
-
'
|
|
136
|
-
'
|
|
137
|
-
'
|
|
115
|
+
'Prefer handling focused or tightly coupled work locally.',
|
|
116
|
+
'Consider delegating a clearly separable subtask when parallel progress or context savings materially outweigh coordination overhead.',
|
|
117
|
+
'Avoid splitting work merely because agents are available.',
|
|
118
|
+
'A useful handoff includes an objective, necessary context, expected output, and a read-only boundary or non-overlapping write scope.',
|
|
119
|
+
'Prefer following up through the direct delegate; deeper Session inspection is usually useful only when a concrete issue needs independent verification.',
|
|
120
|
+
'While children run, prefer advancing meaningful, non-overlapping task work rather than duplicating delegated work. Waiting is generally useful once no meaningful local task work remains and a required result is needed to unblock or finish the task.',
|
|
138
121
|
'Verify consequential findings and changes. Join or stop task-required child work before final delivery, and retain responsibility for the final result.'
|
|
139
122
|
].join('\n\n');
|
|
140
123
|
const collaborationPrompt = [
|
|
141
124
|
'# Workspace collaboration',
|
|
142
|
-
"Use native agent_* tools to collaborate within the Agent Network when another Session's context or ongoing work materially helps.
|
|
143
|
-
'
|
|
144
|
-
'
|
|
125
|
+
"Use native agent_* tools to collaborate within the Agent Network when another Session's context or ongoing work materially helps. Prefer focused requests and avoid unnecessary cross-Session coordination. Peer Sessions retain their own work and lifecycle; do not treat them as task-owned children or stop them merely for task cleanup.",
|
|
126
|
+
'Prefer reusing known Session IDs rather than rediscovering them. Updates are most useful when requested or when the recipient needs to decide or change course; prefer combining related updates.',
|
|
127
|
+
'An accepted message normally needs no separate acknowledgment. Requests for receipt-only replies and immediate agent_view calls solely to confirm receipt are usually unnecessary.',
|
|
128
|
+
'For delegated work, prefer a concise final handoff over routine progress reports or duplicate completion messages. Early contact is most useful for requested updates, blockers, scope conflicts, or information needed for a decision.'
|
|
145
129
|
].join('\n\n');
|
|
146
130
|
const managementPrompt = [
|
|
147
131
|
'# Workspace management',
|
|
@@ -152,6 +136,15 @@ export function agentNetworkExtension(invoke, currentExecution) {
|
|
|
152
136
|
id: 'mar.node.agent-network',
|
|
153
137
|
apiVersion: '1.0',
|
|
154
138
|
attachSession(context) {
|
|
139
|
+
// Reject retired input before generic unknown-field normalization can discard it.
|
|
140
|
+
context.hooks.intercept('tool.invoke', (input, _call, next) => {
|
|
141
|
+
if ((input.name === 'agent_start' || input.name === 'agent_send') &&
|
|
142
|
+
input.arguments !== null &&
|
|
143
|
+
typeof input.arguments === 'object' &&
|
|
144
|
+
('attachments' in input.arguments || 'attachmentIds' in input.arguments))
|
|
145
|
+
throw new AgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Agent collaboration accepts text input only.');
|
|
146
|
+
return next(input);
|
|
147
|
+
}, { reentry: 'allow' });
|
|
155
148
|
context.hooks.intercept('prompt.sections', (input, _call, next) => {
|
|
156
149
|
const available = new Set(input.facts?.tools);
|
|
157
150
|
const enabled = input.facts?.mode !== 'compact';
|
|
@@ -224,9 +217,7 @@ export function agentNetworkExtension(invoke, currentExecution) {
|
|
|
224
217
|
let operationInput = args;
|
|
225
218
|
if (tool.action === 'send') {
|
|
226
219
|
const { delivery, ...sendInput } = args;
|
|
227
|
-
const hasMessage =
|
|
228
|
-
(Array.isArray(args.attachments) && args.attachments.length > 0) ||
|
|
229
|
-
(Array.isArray(args.attachmentIds) && args.attachmentIds.length > 0);
|
|
220
|
+
const hasMessage = typeof args.content === 'string' && !!args.content.trim();
|
|
230
221
|
operationInput =
|
|
231
222
|
delivery !== undefined || hasMessage
|
|
232
223
|
? {
|
|
@@ -15,6 +15,7 @@ import { MarAgentConversationNormalizer, normalizeMarAgentUsage } from './conver
|
|
|
15
15
|
import { parseRunnerImageAttachments } from '../../session/attachments.js';
|
|
16
16
|
import { runnerErrorProjection } from '../runner-error.js';
|
|
17
17
|
import { MarAgentRuntimeModelFactory, sdkModelConfigurations, closeImageCredentialProviders } from './runtime-model-factory.js';
|
|
18
|
+
import { snapshotRuntimeModel } from './runtime-model-factory.js';
|
|
18
19
|
import { createImageGenerationExtensions } from './image-generation-extensions.js';
|
|
19
20
|
import { createWebToolsExtensions } from './web-tools-extensions.js';
|
|
20
21
|
import { combineExecutionExtensions } from './execution-extensions.js';
|
|
@@ -94,6 +95,43 @@ export class MarAgentManagedRunController {
|
|
|
94
95
|
releaseSession(sessionId) {
|
|
95
96
|
return this.#runtimes.releaseSession(sessionId);
|
|
96
97
|
}
|
|
98
|
+
restartExecutionContext(sessionId, context, selectedModelId) {
|
|
99
|
+
const runtime = this.#runtimes.findRuntime(sessionId);
|
|
100
|
+
const input = marAgentEnvelopeInput(context);
|
|
101
|
+
if (runtime === undefined || typeof input?.runtimeScopeId !== 'string')
|
|
102
|
+
return context;
|
|
103
|
+
const scope = input.runtimeScopeId;
|
|
104
|
+
const snapshots = new Map(runtime.models.flatMap((model) => {
|
|
105
|
+
const snapshot = snapshotRuntimeModel(model, scope);
|
|
106
|
+
return snapshot === undefined ? [] : [[model.id, snapshot]];
|
|
107
|
+
}));
|
|
108
|
+
if (snapshots.size === 0)
|
|
109
|
+
return context;
|
|
110
|
+
const refresh = (value) => {
|
|
111
|
+
if (value === null || typeof value !== 'object' || !('id' in value))
|
|
112
|
+
return value;
|
|
113
|
+
return typeof value.id === 'string' ? (snapshots.get(value.id) ?? value) : value;
|
|
114
|
+
};
|
|
115
|
+
const models = Array.isArray(input.models) ? input.models.map(refresh) : input.models;
|
|
116
|
+
const selected = Array.isArray(models)
|
|
117
|
+
? models.find((model) => model !== null &&
|
|
118
|
+
typeof model === 'object' &&
|
|
119
|
+
'id' in model &&
|
|
120
|
+
model.id === selectedModelId)
|
|
121
|
+
: undefined;
|
|
122
|
+
const compact = input.compact;
|
|
123
|
+
return {
|
|
124
|
+
runner: 'mar-agent',
|
|
125
|
+
runnerInput: {
|
|
126
|
+
...input,
|
|
127
|
+
model: selected ?? refresh(input.model),
|
|
128
|
+
models,
|
|
129
|
+
compact: compact !== null && typeof compact === 'object' && 'model' in compact
|
|
130
|
+
? { ...compact, model: refresh(compact.model) }
|
|
131
|
+
: compact
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
97
135
|
async executeCommand(session, command, input, context) {
|
|
98
136
|
if (command.id === 'plan') {
|
|
99
137
|
const current = this.host.commandState(session.id, 'plan');
|
|
@@ -21,14 +21,42 @@ class PendingCredentialResolution {
|
|
|
21
21
|
this.initialReason = initialReason;
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
+
const restartConfigurations = new WeakMap();
|
|
25
|
+
export function snapshotRuntimeModel(model, scope) {
|
|
26
|
+
const state = restartConfigurations.get(model);
|
|
27
|
+
if (state === undefined || state.scope !== scope)
|
|
28
|
+
return;
|
|
29
|
+
const configuration = structuredClone(state.configuration);
|
|
30
|
+
for (const credential of [configuration.credential, configuration.imageGeneration?.credential]) {
|
|
31
|
+
if (credential?.type !== 'RUNTIME_TICKET')
|
|
32
|
+
continue;
|
|
33
|
+
const key = `${credential.credentialId}\0${credential.ticket.runtimeScopeId}\0${credential.ticket.purpose ?? 'MODEL'}`;
|
|
34
|
+
const current = state.tickets.get(key);
|
|
35
|
+
if (current === undefined || current.released)
|
|
36
|
+
return;
|
|
37
|
+
credential.ticket = structuredClone(current.ticket);
|
|
38
|
+
}
|
|
39
|
+
return configuration;
|
|
40
|
+
}
|
|
24
41
|
const CREDENTIAL_REFRESH_WINDOW_MS = 60_000;
|
|
25
42
|
const CREDENTIAL_REFRESH_RETRY_MS = 30_000;
|
|
26
43
|
export class MarAgentRuntimeModelFactory {
|
|
27
|
-
control;
|
|
28
44
|
now;
|
|
45
|
+
control;
|
|
46
|
+
pendingCredentials = new Set();
|
|
29
47
|
constructor(control, now = Date.now) {
|
|
30
|
-
this.control = control;
|
|
31
48
|
this.now = now;
|
|
49
|
+
this.control = {
|
|
50
|
+
resolve: (...args) => {
|
|
51
|
+
const pending = control.resolve(...args);
|
|
52
|
+
this.pendingCredentials.add(pending);
|
|
53
|
+
void pending.then(() => this.pendingCredentials.delete(pending), () => this.pendingCredentials.delete(pending));
|
|
54
|
+
return pending;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async waitForPendingCredentials() {
|
|
59
|
+
await Promise.allSettled([...this.pendingCredentials]);
|
|
32
60
|
}
|
|
33
61
|
create(configurations, context) {
|
|
34
62
|
const tickets = new Map();
|
|
@@ -36,6 +64,14 @@ export class MarAgentRuntimeModelFactory {
|
|
|
36
64
|
try {
|
|
37
65
|
return configurations.map((raw) => {
|
|
38
66
|
const configuration = marAgentRuntimeModelConfigurationSchema.parse(raw);
|
|
67
|
+
const remember = (model) => {
|
|
68
|
+
restartConfigurations.set(model, {
|
|
69
|
+
configuration,
|
|
70
|
+
scope: context.runtimeScopeId,
|
|
71
|
+
tickets
|
|
72
|
+
});
|
|
73
|
+
return model;
|
|
74
|
+
};
|
|
39
75
|
const imageGeneration = configuration.imageGeneration === undefined ||
|
|
40
76
|
configuration.imageGeneration.credential === undefined
|
|
41
77
|
? undefined
|
|
@@ -89,10 +125,10 @@ export class MarAgentRuntimeModelFactory {
|
|
|
89
125
|
};
|
|
90
126
|
const { credential } = configuration;
|
|
91
127
|
if (credential.type === 'INLINE_API_KEY')
|
|
92
|
-
return {
|
|
128
|
+
return remember({
|
|
93
129
|
...model,
|
|
94
130
|
apiKey: new SecretValue(credential.value)
|
|
95
|
-
};
|
|
131
|
+
});
|
|
96
132
|
if (credential?.type !== 'RUNTIME_TICKET')
|
|
97
133
|
throw new AgentError('MAR_AGENT_RUNTIME_MODEL_CREDENTIAL_INVALID', 'The runtime model credential is invalid.');
|
|
98
134
|
if (context.runtimeScopeId === undefined ||
|
|
@@ -100,7 +136,7 @@ export class MarAgentRuntimeModelFactory {
|
|
|
100
136
|
!credential.ticket.allowedModelIds.includes(configuration.id))
|
|
101
137
|
throw new AgentError('MAR_AGENT_RUNTIME_CREDENTIAL_SCOPE_INVALID', 'The runtime credential does not authorize this execution or model.');
|
|
102
138
|
const provider = runtimeProvider(credential, configuration.id, context, tickets, providers, this.control, this.now);
|
|
103
|
-
return {
|
|
139
|
+
return remember({
|
|
104
140
|
...model,
|
|
105
141
|
...(configuration.imageGeneration !== undefined &&
|
|
106
142
|
configuration.imageGeneration.credential === undefined
|
|
@@ -117,7 +153,7 @@ export class MarAgentRuntimeModelFactory {
|
|
|
117
153
|
}
|
|
118
154
|
: {}),
|
|
119
155
|
credentialProvider: provider
|
|
120
|
-
};
|
|
156
|
+
});
|
|
121
157
|
});
|
|
122
158
|
}
|
|
123
159
|
catch (error) {
|
|
@@ -221,9 +221,7 @@ export function createNodeAgentNetwork(options) {
|
|
|
221
221
|
...(!terminal && parent ? { __marctlOrigin: origin(parent) } : {}),
|
|
222
222
|
...(input.title === undefined ? {} : { title: input.title }),
|
|
223
223
|
...(model ? { modelId: model } : {}),
|
|
224
|
-
...(input.effort === undefined ? {} : { effort: input.effort })
|
|
225
|
-
...(input.attachments === undefined ? {} : { attachments: input.attachments }),
|
|
226
|
-
...(input.attachmentIds === undefined ? {} : { attachmentIds: input.attachmentIds })
|
|
224
|
+
...(input.effort === undefined ? {} : { effort: input.effort })
|
|
227
225
|
};
|
|
228
226
|
signal?.throwIfAborted();
|
|
229
227
|
const result = await options.withGrant(actor, () => input.goal.kind === 'set'
|
|
@@ -236,10 +234,6 @@ export function createNodeAgentNetwork(options) {
|
|
|
236
234
|
suppressNotifications: true,
|
|
237
235
|
...(!terminal && parent ? { messageOrigin: origin(parent) } : {}),
|
|
238
236
|
clientMessageId: input.clientMessageId,
|
|
239
|
-
...(input.attachments === undefined ? {} : { attachments: input.attachments }),
|
|
240
|
-
...(input.attachmentIds === undefined
|
|
241
|
-
? {}
|
|
242
|
-
: { attachmentIds: input.attachmentIds }),
|
|
243
237
|
...(actor.requestUserId === undefined
|
|
244
238
|
? {}
|
|
245
239
|
: { initiatedByUserId: actor.requestUserId }),
|
|
@@ -287,11 +281,7 @@ export function createNodeAgentNetwork(options) {
|
|
|
287
281
|
: {
|
|
288
282
|
messageContent: message.content,
|
|
289
283
|
clientMessageId: message.clientMessageId,
|
|
290
|
-
messageIntent: message.deliveryIntent
|
|
291
|
-
...(message.attachments === undefined ? {} : { attachments: message.attachments }),
|
|
292
|
-
...(message.attachmentIds === undefined
|
|
293
|
-
? {}
|
|
294
|
-
: { attachmentIds: message.attachmentIds })
|
|
284
|
+
messageIntent: message.deliveryIntent
|
|
295
285
|
})
|
|
296
286
|
}));
|
|
297
287
|
return publicReceipt(result);
|
|
@@ -20,12 +20,9 @@ export class AgentNetworkService {
|
|
|
20
20
|
const goal = parseAgentGoal(input.goal);
|
|
21
21
|
const content = input.content ?? '';
|
|
22
22
|
if (typeof content !== 'string' ||
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
(!Array.isArray(input.attachmentIds) ||
|
|
27
|
-
input.attachmentIds.some((id) => typeof id !== 'string' || !id))) ||
|
|
28
|
-
(!!input.attachments?.length && !!input.attachmentIds?.length) ||
|
|
23
|
+
!content.trim() ||
|
|
24
|
+
'attachments' in input ||
|
|
25
|
+
'attachmentIds' in input ||
|
|
29
26
|
!validOptionalString(input.runner) ||
|
|
30
27
|
'model' in input ||
|
|
31
28
|
!validOptionalString(input.modelId) ||
|
|
@@ -62,16 +59,9 @@ export class AgentNetworkService {
|
|
|
62
59
|
if (typeof input.agentId !== 'string' ||
|
|
63
60
|
!input.agentId ||
|
|
64
61
|
typeof content !== 'string' ||
|
|
65
|
-
(!content.trim() &&
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
goal.kind === 'unchanged' &&
|
|
69
|
-
input.approval === undefined) ||
|
|
70
|
-
(input.attachments !== undefined && !Array.isArray(input.attachments)) ||
|
|
71
|
-
(input.attachmentIds !== undefined &&
|
|
72
|
-
(!Array.isArray(input.attachmentIds) ||
|
|
73
|
-
input.attachmentIds.some((id) => typeof id !== 'string' || !id))) ||
|
|
74
|
-
(!!input.attachments?.length && !!input.attachmentIds?.length) ||
|
|
62
|
+
(!content.trim() && goal.kind === 'unchanged' && input.approval === undefined) ||
|
|
63
|
+
'attachments' in input ||
|
|
64
|
+
'attachmentIds' in input ||
|
|
75
65
|
!validOptionalString(input.clientMessageId) ||
|
|
76
66
|
(input.approval !== undefined &&
|
|
77
67
|
(!isPlainRecord(input.approval) ||
|
|
@@ -83,7 +73,7 @@ export class AgentNetworkService {
|
|
|
83
73
|
if (input.deliveryIntent !== undefined &&
|
|
84
74
|
!['SEND', 'QUEUE', 'REPLACE_CURRENT'].includes(input.deliveryIntent))
|
|
85
75
|
throw new Error('AGENT_SEND_INVALID');
|
|
86
|
-
const hasMessage = !!content.trim()
|
|
76
|
+
const hasMessage = !!content.trim();
|
|
87
77
|
const session = await this.options.target(actor, input.agentId);
|
|
88
78
|
signal?.throwIfAborted();
|
|
89
79
|
if (hasMessage)
|
|
@@ -122,9 +112,7 @@ export class AgentNetworkService {
|
|
|
122
112
|
? {
|
|
123
113
|
clientMessageId: input.clientMessageId ?? randomUUID(),
|
|
124
114
|
content,
|
|
125
|
-
deliveryIntent: input.deliveryIntent ?? 'SEND'
|
|
126
|
-
...(input.attachments === undefined ? {} : { attachments: input.attachments }),
|
|
127
|
-
...(input.attachmentIds === undefined ? {} : { attachmentIds: input.attachmentIds })
|
|
115
|
+
deliveryIntent: input.deliveryIntent ?? 'SEND'
|
|
128
116
|
}
|
|
129
117
|
: undefined;
|
|
130
118
|
let goalResult;
|
|
@@ -580,6 +580,9 @@ export class ContinuousRunCoordinator {
|
|
|
580
580
|
return;
|
|
581
581
|
}
|
|
582
582
|
let executionId = this.options.runtime.currentExecutionId(active.id);
|
|
583
|
+
// Immediate commands own a root Runner handle without a normal message Execution.
|
|
584
|
+
if (executionId === undefined && this.options.runtime.isExecutionActive(active.id))
|
|
585
|
+
return;
|
|
583
586
|
if (executionId !== undefined && this.dispatchedExecutions.has(executionId))
|
|
584
587
|
return;
|
|
585
588
|
let pending = executionId === undefined
|
|
@@ -4,6 +4,7 @@ import { isPlainRecord } from '../util/records.js';
|
|
|
4
4
|
import { safeErrorCode } from '../util/safe-error.js';
|
|
5
5
|
import { NODE_MCP } from '../mcp/naming.js';
|
|
6
6
|
import { composerResourceInputTags } from './composer-message-input.js';
|
|
7
|
+
import { captureRestartExecution } from './restart-execution-state.js';
|
|
7
8
|
/** Owns transient Goal orchestration, not the domain state or execution queue. */
|
|
8
9
|
export class GoalContinuationCoordinator {
|
|
9
10
|
options;
|
|
@@ -47,6 +48,10 @@ export class GoalContinuationCoordinator {
|
|
|
47
48
|
freeze() {
|
|
48
49
|
this.frozen = true;
|
|
49
50
|
}
|
|
51
|
+
restartExecution(sessionId) {
|
|
52
|
+
const retry = this.goalRetryTimers.get(sessionId);
|
|
53
|
+
return captureRestartExecution(retry?.context, retry?.initiatedByUserId);
|
|
54
|
+
}
|
|
50
55
|
dispose() {
|
|
51
56
|
this.frozen = true;
|
|
52
57
|
this.disposed = true;
|
|
@@ -107,6 +112,9 @@ export class GoalContinuationCoordinator {
|
|
|
107
112
|
const continuationEpoch = this.continuationEpoch(pending.sessionId);
|
|
108
113
|
const retry = {
|
|
109
114
|
sessionId: pending.sessionId,
|
|
115
|
+
context: pending,
|
|
116
|
+
initiatedByUserId: this.options.runtime.activeSessionRun(pending.sessionId)
|
|
117
|
+
?.initiatedByUserId,
|
|
110
118
|
timer: setTimeout(() => {
|
|
111
119
|
this.goalRetryTimers.delete(retry.sessionId);
|
|
112
120
|
void this.enqueueGoalContinuation({
|
|
@@ -497,6 +505,8 @@ export class GoalContinuationCoordinator {
|
|
|
497
505
|
clearTimeout(previousRetry.timer);
|
|
498
506
|
const retry = {
|
|
499
507
|
sessionId: attempt.sessionId,
|
|
508
|
+
context: input,
|
|
509
|
+
initiatedByUserId: input.initiatedByUserId,
|
|
500
510
|
timer: setTimeout(() => {
|
|
501
511
|
if (!this.goalGenerationCurrent(retry.sessionId, generation))
|
|
502
512
|
return;
|
|
@@ -3,6 +3,8 @@ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { parseComposerTags } from '@myagentroam/protocol';
|
|
6
|
+
import { safeErrorCode } from '../util/safe-error.js';
|
|
7
|
+
import { captureRestartExecution, openRestartState, restartExecutionSchema, sealRestartState } from './restart-execution-state.js';
|
|
6
8
|
const identifier = z.string().min(1).max(512);
|
|
7
9
|
const count = z.number().int().nonnegative();
|
|
8
10
|
const attachmentSchema = z
|
|
@@ -18,7 +20,7 @@ const messageSchema = z
|
|
|
18
20
|
clientMessageId: identifier,
|
|
19
21
|
content: z.string().max(1_000_000),
|
|
20
22
|
attachments: z.array(attachmentSchema).max(5),
|
|
21
|
-
|
|
23
|
+
execution: restartExecutionSchema.optional(),
|
|
22
24
|
immediate: z.boolean(),
|
|
23
25
|
suppressNotifications: z.boolean().optional()
|
|
24
26
|
})
|
|
@@ -56,7 +58,7 @@ const sessionSchema = z
|
|
|
56
58
|
access: z.string().nullable()
|
|
57
59
|
})
|
|
58
60
|
.strict(),
|
|
59
|
-
|
|
61
|
+
execution: restartExecutionSchema.optional(),
|
|
60
62
|
paused: z.boolean(),
|
|
61
63
|
plan: z.boolean(),
|
|
62
64
|
awaitingGoal: z.boolean(),
|
|
@@ -68,7 +70,7 @@ const sessionSchema = z
|
|
|
68
70
|
.strict();
|
|
69
71
|
export const plannedRestartSchema = z
|
|
70
72
|
.object({
|
|
71
|
-
schemaVersion: z.literal(
|
|
73
|
+
schemaVersion: z.literal(3),
|
|
72
74
|
id: identifier,
|
|
73
75
|
nodeId: identifier,
|
|
74
76
|
serverUrl: z.string().min(1),
|
|
@@ -76,14 +78,44 @@ export const plannedRestartSchema = z
|
|
|
76
78
|
sessions: z.array(sessionSchema)
|
|
77
79
|
})
|
|
78
80
|
.strict();
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
// Only this producer can associate log-safe metadata with an original failure.
|
|
82
|
+
const captureDiagnostics = new WeakMap();
|
|
83
|
+
const captureFields = new Set([
|
|
84
|
+
...Object.keys(plannedRestartSchema.shape),
|
|
85
|
+
...Object.keys(sessionSchema.shape),
|
|
86
|
+
...Object.keys(sessionSchema.shape.session.shape),
|
|
87
|
+
...Object.keys(goalSchema.shape),
|
|
88
|
+
...Object.keys(goalSchema.shape.goal.shape),
|
|
89
|
+
...Object.keys(messageSchema.shape),
|
|
90
|
+
...Object.keys(attachmentSchema.shape)
|
|
91
|
+
]);
|
|
92
|
+
export function plannedRestartCaptureDiagnostic(error) {
|
|
93
|
+
return error instanceof Error ? captureDiagnostics.get(error) : undefined;
|
|
94
|
+
}
|
|
95
|
+
function parseCapture(schema, input, reason) {
|
|
96
|
+
const result = schema.safeParse(input);
|
|
97
|
+
if (result.success)
|
|
98
|
+
return result.data;
|
|
99
|
+
captureDiagnostics.set(result.error, {
|
|
100
|
+
reason,
|
|
101
|
+
issues: result.error.issues.slice(0, 12).map((issue) => ({
|
|
102
|
+
code: issue.code,
|
|
103
|
+
// Only schema-owned fields and array offsets, never unknown keys or input values.
|
|
104
|
+
path: issue.path
|
|
105
|
+
.slice(0, 8)
|
|
106
|
+
.map((part) => typeof part === 'number'
|
|
107
|
+
? `[${part}]`
|
|
108
|
+
: captureFields.has(String(part))
|
|
109
|
+
? String(part)
|
|
110
|
+
: '*')
|
|
111
|
+
.join('.')
|
|
112
|
+
.replaceAll('.[', '[')
|
|
113
|
+
.slice(0, 160)
|
|
114
|
+
})),
|
|
115
|
+
issuesTruncated: result.error.issues.length > 12
|
|
116
|
+
});
|
|
117
|
+
throw result.error;
|
|
118
|
+
}
|
|
87
119
|
export const RESTART_NOTICE = 'The previous execution was interrupted by a system restart. Check the session history and current workspace, then continue unfinished work. Verify completed or uncertain actions before proceeding; do not blindly repeat actions with external side effects.';
|
|
88
120
|
/** A one-shot orderly-shutdown handoff, not a crash-recovery journal. */
|
|
89
121
|
export class PlannedRestartHandoff {
|
|
@@ -91,7 +123,6 @@ export class PlannedRestartHandoff {
|
|
|
91
123
|
pending;
|
|
92
124
|
blocked = new Set();
|
|
93
125
|
recovery;
|
|
94
|
-
owners = new Map();
|
|
95
126
|
constructor(options) {
|
|
96
127
|
this.options = options;
|
|
97
128
|
}
|
|
@@ -120,9 +151,6 @@ export class PlannedRestartHandoff {
|
|
|
120
151
|
'session.metadata.update'
|
|
121
152
|
].includes(operation));
|
|
122
153
|
}
|
|
123
|
-
rememberUser(sessionId, userId) {
|
|
124
|
-
this.owners.set(sessionId, userId);
|
|
125
|
-
}
|
|
126
154
|
async load() {
|
|
127
155
|
this.recovery = undefined;
|
|
128
156
|
this.pending = undefined;
|
|
@@ -137,18 +165,7 @@ export class PlannedRestartHandoff {
|
|
|
137
165
|
return;
|
|
138
166
|
}
|
|
139
167
|
try {
|
|
140
|
-
const
|
|
141
|
-
const legacy = legacyPlannedRestartSchema.safeParse(saved);
|
|
142
|
-
const value = plannedRestartSchema.parse(legacy.success
|
|
143
|
-
? {
|
|
144
|
-
...legacy.data,
|
|
145
|
-
schemaVersion: 2,
|
|
146
|
-
sessions: legacy.data.sessions.map((entry) => {
|
|
147
|
-
const { model, ...session } = entry.session;
|
|
148
|
-
return { ...entry, session: { ...session, modelId: model } };
|
|
149
|
-
})
|
|
150
|
-
}
|
|
151
|
-
: saved);
|
|
168
|
+
const value = plannedRestartSchema.parse(openRestartState(text, this.options.credential()));
|
|
152
169
|
const identity = this.options.identity();
|
|
153
170
|
if (value.nodeId !== identity.nodeId || value.serverUrl !== identity.serverUrl)
|
|
154
171
|
throw new Error('RESTART_HANDOFF_IDENTITY_MISMATCH');
|
|
@@ -174,12 +191,23 @@ export class PlannedRestartHandoff {
|
|
|
174
191
|
const awaitingGoal = goal === undefined && this.options.commandActive(session.id, 'goal');
|
|
175
192
|
const currentId = active && (runtime.currentExecutionId(active.id) ?? active.id);
|
|
176
193
|
const pending = currentId === undefined ? undefined : coordinator.pending(currentId);
|
|
177
|
-
const
|
|
194
|
+
const checkpoint = (execution, activeInput = false) => {
|
|
195
|
+
if (execution !== undefined && activeInput)
|
|
196
|
+
execution.configuration = {
|
|
197
|
+
modelId: session.modelId,
|
|
198
|
+
effort: session.effort,
|
|
199
|
+
access: session.access
|
|
200
|
+
};
|
|
201
|
+
if (execution !== undefined && this.options.executionContext !== undefined)
|
|
202
|
+
execution.executionContext = this.options.executionContext(session.id, execution.executionContext, execution.configuration?.modelId);
|
|
203
|
+
return execution;
|
|
204
|
+
};
|
|
205
|
+
const toMessage = (id, clientMessageId, content, attachments, originatingUser, suppressNotifications, activeInput = false) => ({
|
|
178
206
|
id,
|
|
179
207
|
clientMessageId,
|
|
180
208
|
content: withoutAttachmentTags(content),
|
|
181
209
|
attachments,
|
|
182
|
-
|
|
210
|
+
execution: checkpoint(captureRestartExecution(coordinator.pending(id), originatingUser), activeInput),
|
|
183
211
|
immediate: coordinator.isImmediate(id),
|
|
184
212
|
...(suppressNotifications === true ? { suppressNotifications: true } : {})
|
|
185
213
|
});
|
|
@@ -194,16 +222,20 @@ export class PlannedRestartHandoff {
|
|
|
194
222
|
});
|
|
195
223
|
const current = pending?.restartMessage === undefined || pending.goal !== undefined
|
|
196
224
|
? undefined
|
|
197
|
-
: toMessage(currentId, pending.clientMessageId ?? active.clientMessageId, pending.restartMessage.content, [...pending.restartMessage.attachments], pending.restartMessage.initiatedByUserId ?? active?.initiatedByUserId, active?.suppressNotifications);
|
|
225
|
+
: toMessage(currentId, pending.clientMessageId ?? active.clientMessageId, pending.restartMessage.content, [...pending.restartMessage.attachments], pending.restartMessage.initiatedByUserId ?? active?.initiatedByUserId, active?.suppressNotifications, true);
|
|
198
226
|
if (current === undefined && items.length === 0 && goal === undefined && !awaitingGoal)
|
|
199
227
|
continue;
|
|
200
|
-
if (session.externalSessionId === null)
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
228
|
+
if (session.externalSessionId === null) {
|
|
229
|
+
const error = Object.assign(new Error('RESTART_SESSION_NOT_READY'), {
|
|
230
|
+
code: 'RESTART_SESSION_NOT_READY'
|
|
231
|
+
});
|
|
232
|
+
captureDiagnostics.set(error, { reason: 'session_not_ready' });
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
const goalItem = goalItemId === undefined ? undefined : runtime.getQueueItem(goalItemId);
|
|
236
|
+
const execution = captureRestartExecution(goalItem === undefined ? undefined : coordinator.pending(goalItem.id), goalItem?.initiatedByUserId) ??
|
|
237
|
+
captureRestartExecution(pending, active?.initiatedByUserId) ??
|
|
238
|
+
this.options.goalExecution?.(session.id);
|
|
207
239
|
sessions.push({
|
|
208
240
|
session: {
|
|
209
241
|
id: session.externalSessionId,
|
|
@@ -213,25 +245,29 @@ export class PlannedRestartHandoff {
|
|
|
213
245
|
effort: session.effort,
|
|
214
246
|
access: session.access
|
|
215
247
|
},
|
|
216
|
-
|
|
248
|
+
...(execution === undefined
|
|
249
|
+
? {}
|
|
250
|
+
: { execution: checkpoint(execution, goalItem === undefined) }),
|
|
217
251
|
paused: queue.paused,
|
|
218
252
|
plan: this.options.commandActive(session.id, 'plan'),
|
|
219
253
|
awaitingGoal,
|
|
220
254
|
...(notificationSource?.suppressNotifications === true
|
|
221
255
|
? { suppressNotifications: true }
|
|
222
256
|
: {}),
|
|
223
|
-
...(goal === undefined
|
|
257
|
+
...(goal === undefined
|
|
258
|
+
? {}
|
|
259
|
+
: { goal: parseCapture(goalSchema, goal, 'goal_schema_invalid') }),
|
|
224
260
|
...(current === undefined ? {} : { current }),
|
|
225
261
|
queue: items
|
|
226
262
|
});
|
|
227
263
|
}
|
|
228
|
-
return plannedRestartSchema
|
|
229
|
-
schemaVersion:
|
|
264
|
+
return parseCapture(plannedRestartSchema, {
|
|
265
|
+
schemaVersion: 3,
|
|
230
266
|
id: randomUUID(),
|
|
231
267
|
...this.options.identity(),
|
|
232
268
|
createdAt: Date.now(),
|
|
233
269
|
sessions
|
|
234
|
-
});
|
|
270
|
+
}, 'snapshot_schema_invalid');
|
|
235
271
|
}
|
|
236
272
|
async publish(snapshot) {
|
|
237
273
|
for (const entry of snapshot.sessions) {
|
|
@@ -251,7 +287,7 @@ export class PlannedRestartHandoff {
|
|
|
251
287
|
const temporary = `${this.path()}.${process.pid}.tmp`;
|
|
252
288
|
await mkdir(this.options.directory(), { recursive: true, mode: 0o700 });
|
|
253
289
|
try {
|
|
254
|
-
await writeFile(temporary,
|
|
290
|
+
await writeFile(temporary, sealRestartState(plannedRestartSchema.parse(snapshot), this.options.credential()), {
|
|
255
291
|
mode: 0o600
|
|
256
292
|
});
|
|
257
293
|
await rename(temporary, this.path());
|
|
@@ -276,12 +312,12 @@ export class PlannedRestartHandoff {
|
|
|
276
312
|
await rename(this.path(), this.path(true));
|
|
277
313
|
for (const entry of value.sessions) {
|
|
278
314
|
try {
|
|
279
|
-
this.rememberUser(entry.session.id, entry.ownerUserId);
|
|
280
315
|
await this.options.restore(entry, value.id);
|
|
281
316
|
}
|
|
282
|
-
catch {
|
|
317
|
+
catch (error) {
|
|
283
318
|
failed = true;
|
|
284
|
-
|
|
319
|
+
const errorCode = safeErrorCode(error, 'RESTART_SESSION_RESTORE_FAILED');
|
|
320
|
+
this.options.diagnose('RESTART_SESSION_RESTORE_FAILED', entry.session.id, errorCode === 'RESTART_SESSION_RESTORE_FAILED' ? undefined : errorCode);
|
|
285
321
|
}
|
|
286
322
|
finally {
|
|
287
323
|
this.blocked.delete(entry.session.id);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { modelUsageGrantSchema } from '@myagentroam/protocol';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
export const restartExecutionSchema = z
|
|
5
|
+
.object({
|
|
6
|
+
configuration: z
|
|
7
|
+
.object({
|
|
8
|
+
modelId: z.string().nullable(),
|
|
9
|
+
effort: z.string().nullable(),
|
|
10
|
+
access: z.string().nullable()
|
|
11
|
+
})
|
|
12
|
+
.strict()
|
|
13
|
+
.optional(),
|
|
14
|
+
executionContext: z
|
|
15
|
+
.object({ runner: z.literal('mar-agent'), runnerInput: z.unknown().optional() })
|
|
16
|
+
.strict(),
|
|
17
|
+
secretEnvironment: z.record(z.string(), z.string()),
|
|
18
|
+
personalInstructions: z.string().optional(),
|
|
19
|
+
usageGrant: z
|
|
20
|
+
.custom((value) => modelUsageGrantSchema.safeParse(value).success)
|
|
21
|
+
.optional(),
|
|
22
|
+
initiatedByUserId: z.number().int().positive().optional()
|
|
23
|
+
})
|
|
24
|
+
.strict();
|
|
25
|
+
export function captureRestartExecution(input, initiatedByUserId) {
|
|
26
|
+
if (input === undefined)
|
|
27
|
+
return;
|
|
28
|
+
return restartExecutionSchema.parse({
|
|
29
|
+
...(input.modelId === undefined || input.effort === undefined || input.access === undefined
|
|
30
|
+
? {}
|
|
31
|
+
: { configuration: { modelId: input.modelId, effort: input.effort, access: input.access } }),
|
|
32
|
+
executionContext: structuredClone(input.executionContext),
|
|
33
|
+
secretEnvironment: { ...input.secretEnvironment },
|
|
34
|
+
...(input.personalInstructions === undefined
|
|
35
|
+
? {}
|
|
36
|
+
: { personalInstructions: input.personalInstructions }),
|
|
37
|
+
...(input.usageGrant === undefined ? {} : { usageGrant: { ...input.usageGrant } }),
|
|
38
|
+
...(initiatedByUserId === undefined ? {} : { initiatedByUserId })
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
const sealedSchema = z
|
|
42
|
+
.object({
|
|
43
|
+
format: z.literal('mar-node-restart'),
|
|
44
|
+
version: z.literal(1),
|
|
45
|
+
nonce: z.string().length(16),
|
|
46
|
+
tag: z.string().length(24),
|
|
47
|
+
ciphertext: z.string().min(1)
|
|
48
|
+
})
|
|
49
|
+
.strict();
|
|
50
|
+
function encryptionKey(credential) {
|
|
51
|
+
if (!credential)
|
|
52
|
+
throw new Error('RESTART_HANDOFF_KEY_UNAVAILABLE');
|
|
53
|
+
return createHash('sha256').update('mar-node:orderly-restart:v1\0').update(credential).digest();
|
|
54
|
+
}
|
|
55
|
+
/** The existing local Node credential protects this one-shot execution checkpoint. */
|
|
56
|
+
export function sealRestartState(value, credential) {
|
|
57
|
+
const nonce = randomBytes(12);
|
|
58
|
+
const cipher = createCipheriv('aes-256-gcm', encryptionKey(credential), nonce);
|
|
59
|
+
const ciphertext = Buffer.concat([cipher.update(JSON.stringify(value), 'utf8'), cipher.final()]);
|
|
60
|
+
return JSON.stringify({
|
|
61
|
+
format: 'mar-node-restart',
|
|
62
|
+
version: 1,
|
|
63
|
+
nonce: nonce.toString('base64'),
|
|
64
|
+
tag: cipher.getAuthTag().toString('base64'),
|
|
65
|
+
ciphertext: ciphertext.toString('base64')
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
export function openRestartState(text, credential) {
|
|
69
|
+
const sealed = sealedSchema.parse(JSON.parse(text));
|
|
70
|
+
const decipher = createDecipheriv('aes-256-gcm', encryptionKey(credential), Buffer.from(sealed.nonce, 'base64'));
|
|
71
|
+
decipher.setAuthTag(Buffer.from(sealed.tag, 'base64'));
|
|
72
|
+
const plaintext = Buffer.concat([
|
|
73
|
+
decipher.update(Buffer.from(sealed.ciphertext, 'base64')),
|
|
74
|
+
decipher.final()
|
|
75
|
+
]);
|
|
76
|
+
return JSON.parse(plaintext.toString('utf8'));
|
|
77
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseComposerTags, serializeComposerTag } from '@myagentroam/protocol';
|
|
2
2
|
import { parseMessageAttachments } from '../session/attachments.js';
|
|
3
3
|
import { RUNNER_EXECUTION_CONTEXT } from '../runner/execution-context.js';
|
|
4
|
+
import { resolvedPersonalInstructions } from '../runner/personal-instructions.js';
|
|
4
5
|
import { RESTART_NOTICE } from './planned-restart-handoff.js';
|
|
5
6
|
/** Rebuild inputs through the existing preparation and FIFO boundary. */
|
|
6
7
|
export class RestartSessionRestorer {
|
|
@@ -9,24 +10,24 @@ export class RestartSessionRestorer {
|
|
|
9
10
|
this.options = options;
|
|
10
11
|
}
|
|
11
12
|
async restore(entry, handoffId) {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
handoffId,
|
|
17
|
-
sessionId: entry.session.id,
|
|
18
|
-
workspaceId: entry.session.workspaceId,
|
|
19
|
-
ownerUserId,
|
|
20
|
-
runner: 'mar-agent',
|
|
21
|
-
modelId: entry.session.modelId
|
|
22
|
-
}));
|
|
23
|
-
return contexts.get(ownerUserId);
|
|
13
|
+
const execution = (context) => {
|
|
14
|
+
if (context === undefined)
|
|
15
|
+
throw new Error('RESTART_EXECUTION_CONTEXT_UNAVAILABLE');
|
|
16
|
+
return context;
|
|
24
17
|
};
|
|
25
|
-
|
|
26
|
-
|
|
18
|
+
const current = entry.current &&
|
|
19
|
+
entry.goal?.goal.status !== 'active' &&
|
|
20
|
+
!entry.queue.some((item) => item.immediate)
|
|
21
|
+
? entry.current
|
|
22
|
+
: undefined;
|
|
23
|
+
const currentState = current && (await this.options.inputState(entry, current));
|
|
24
|
+
const inputs = [...entry.queue, ...(current && currentState !== 'completed' ? [current] : [])];
|
|
25
|
+
for (const input of inputs)
|
|
27
26
|
parseMessageAttachments(input.attachments);
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
for (const input of inputs)
|
|
28
|
+
execution(input.execution);
|
|
29
|
+
if (entry.goal?.goal.status === 'active')
|
|
30
|
+
execution(entry.execution);
|
|
30
31
|
let session = await this.options.resolve(entry.session.id);
|
|
31
32
|
if (session.runner !== 'mar-agent' ||
|
|
32
33
|
session.workspaceId !== entry.session.workspaceId ||
|
|
@@ -66,7 +67,7 @@ export class RestartSessionRestorer {
|
|
|
66
67
|
closeInput: '/goal --clear'
|
|
67
68
|
});
|
|
68
69
|
const submit = async (input, content = input.content) => {
|
|
69
|
-
const context =
|
|
70
|
+
const context = execution(input.execution);
|
|
70
71
|
await this.options.withGrant(context, () => this.options.messages.restoreQueued({
|
|
71
72
|
sessionId: session.id,
|
|
72
73
|
content,
|
|
@@ -76,28 +77,31 @@ export class RestartSessionRestorer {
|
|
|
76
77
|
: {}),
|
|
77
78
|
attachments: input.attachments,
|
|
78
79
|
__queueItemId: input.id,
|
|
79
|
-
|
|
80
|
+
...(context.initiatedByUserId === undefined
|
|
81
|
+
? {}
|
|
82
|
+
: { __requestUserId: context.initiatedByUserId }),
|
|
80
83
|
__suppressNotifications: input.suppressNotifications === true,
|
|
81
84
|
secretEnvironment: context.secretEnvironment,
|
|
82
|
-
|
|
85
|
+
...(context.personalInstructions === undefined
|
|
86
|
+
? {}
|
|
87
|
+
: {
|
|
88
|
+
personalInstructions: resolvedPersonalInstructions(context.personalInstructions)
|
|
89
|
+
}),
|
|
83
90
|
[RUNNER_EXECUTION_CONTEXT]: context.executionContext,
|
|
84
91
|
__usageGrant: context.usageGrant
|
|
85
|
-
}));
|
|
92
|
+
}, context.configuration));
|
|
86
93
|
if (input.immediate)
|
|
87
94
|
coordinator.restoreImmediate(input.id);
|
|
88
95
|
};
|
|
89
96
|
try {
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
!entry.queue.some((item) => item.immediate)) {
|
|
93
|
-
const state = await this.options.inputState(entry, entry.current);
|
|
94
|
-
if (state === 'paused')
|
|
97
|
+
if (current) {
|
|
98
|
+
if (currentState === 'paused')
|
|
95
99
|
entry.paused = true;
|
|
96
|
-
if (
|
|
97
|
-
await submit(
|
|
98
|
-
else if (
|
|
100
|
+
if (currentState === 'missing')
|
|
101
|
+
await submit(current);
|
|
102
|
+
else if (currentState !== 'completed')
|
|
99
103
|
await submit({
|
|
100
|
-
...
|
|
104
|
+
...current,
|
|
101
105
|
id: `restart:${handoffId}:${session.id}`,
|
|
102
106
|
clientMessageId: `restart:${handoffId}:${session.id}`,
|
|
103
107
|
attachments: []
|
|
@@ -106,7 +110,7 @@ export class RestartSessionRestorer {
|
|
|
106
110
|
for (const input of entry.queue)
|
|
107
111
|
await submit(input);
|
|
108
112
|
if (entry.goal?.goal.status === 'active') {
|
|
109
|
-
const context =
|
|
113
|
+
const context = execution(entry.execution);
|
|
110
114
|
await this.options.withGrant(context, () => this.options.messages.submitGoal({
|
|
111
115
|
sessionId: session.id,
|
|
112
116
|
kind: 'continue',
|
|
@@ -114,10 +118,15 @@ export class RestartSessionRestorer {
|
|
|
114
118
|
content: `${goals.prompt(session.id, 'continue')}
|
|
115
119
|
${RESTART_NOTICE}`,
|
|
116
120
|
goal: this.options.goalReference(session.id),
|
|
117
|
-
initiatedByUserId
|
|
121
|
+
...(context.initiatedByUserId === undefined
|
|
122
|
+
? {}
|
|
123
|
+
: { initiatedByUserId: context.initiatedByUserId }),
|
|
118
124
|
suppressNotifications: entry.suppressNotifications === true,
|
|
119
125
|
secretEnvironment: context.secretEnvironment,
|
|
120
126
|
restoreQueued: true,
|
|
127
|
+
...(context.configuration === undefined
|
|
128
|
+
? {}
|
|
129
|
+
: { restartConfiguration: context.configuration }),
|
|
121
130
|
...(context.usageGrant === undefined ? {} : { usageGrant: context.usageGrant }),
|
|
122
131
|
...(typeof context.personalInstructions === 'string'
|
|
123
132
|
? { personalInstructions: context.personalInstructions }
|
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
import { createEnvelope, scheduledRunAuthorizeRequestSchema, scheduledRunAuthorizeResponseSchema
|
|
1
|
+
import { createEnvelope, scheduledRunAuthorizeRequestSchema, scheduledRunAuthorizeResponseSchema } from '@myagentroam/protocol';
|
|
2
2
|
export class RunAuthorizationControl {
|
|
3
3
|
send;
|
|
4
|
-
purpose;
|
|
5
4
|
#pending = new Map();
|
|
6
5
|
#generation = 0;
|
|
7
6
|
#connected = false;
|
|
8
7
|
#closed = false;
|
|
9
|
-
constructor(send
|
|
8
|
+
constructor(send) {
|
|
10
9
|
this.send = send;
|
|
11
|
-
this.purpose = purpose;
|
|
12
10
|
}
|
|
13
11
|
code(suffix) {
|
|
14
|
-
return
|
|
12
|
+
return `SCHEDULE_${suffix}`;
|
|
15
13
|
}
|
|
16
14
|
connected() {
|
|
17
15
|
this.disconnected();
|
|
@@ -28,7 +26,7 @@ export class RunAuthorizationControl {
|
|
|
28
26
|
this.disconnected();
|
|
29
27
|
}
|
|
30
28
|
handle(envelope) {
|
|
31
|
-
if (envelope.type !==
|
|
29
|
+
if (envelope.type !== 'schedule.authorize.response')
|
|
32
30
|
return false;
|
|
33
31
|
const pending = envelope.replyTo === undefined ? undefined : this.#pending.get(envelope.replyTo);
|
|
34
32
|
if (pending === undefined || pending.generation !== this.#generation)
|
|
@@ -43,9 +41,7 @@ export class RunAuthorizationControl {
|
|
|
43
41
|
authorize(request) {
|
|
44
42
|
if (!this.#connected || this.#closed)
|
|
45
43
|
return Promise.reject(new Error(this.code('SERVER_UNAVAILABLE')));
|
|
46
|
-
const envelope = createEnvelope(
|
|
47
|
-
? scheduledRunAuthorizeRequestSchema
|
|
48
|
-
: restartRunAuthorizeRequestSchema).parse(request));
|
|
44
|
+
const envelope = createEnvelope('schedule.authorize', scheduledRunAuthorizeRequestSchema.parse(request));
|
|
49
45
|
return new Promise((resolve, reject) => {
|
|
50
46
|
const settle = (action) => {
|
|
51
47
|
const pending = this.#pending.get(envelope.id);
|
|
@@ -81,6 +81,9 @@ export function marctlSessionProgress(runtime, sessionId) {
|
|
|
81
81
|
const { latestActivity: _latest, ...progress } = activity(runtime, sessionId);
|
|
82
82
|
return { ...progress, ...pendingApprovals(runtime, sessionId) };
|
|
83
83
|
}
|
|
84
|
+
export function sessionActivityStatus(runtime, sessionId) {
|
|
85
|
+
return activity(runtime, sessionId).status;
|
|
86
|
+
}
|
|
84
87
|
/** Summarizes current approval facts without exposing tool input or native payloads. */
|
|
85
88
|
function pendingApprovals(runtime, sessionId) {
|
|
86
89
|
const run = runtime.activeSessionRun(sessionId);
|
|
@@ -12,6 +12,7 @@ const boundaries = new Set([
|
|
|
12
12
|
'approval_request',
|
|
13
13
|
'user_input_request',
|
|
14
14
|
'plan',
|
|
15
|
+
'context_compaction',
|
|
15
16
|
'turn_summary'
|
|
16
17
|
]);
|
|
17
18
|
function record(value) {
|
|
@@ -64,12 +65,9 @@ export function projectConversationWorkProcesses(turn) {
|
|
|
64
65
|
let current;
|
|
65
66
|
const assigned = new Map();
|
|
66
67
|
const items = ordered.map((item) => {
|
|
67
|
-
const payload = record(item.payload);
|
|
68
68
|
let group;
|
|
69
69
|
let isStart = false;
|
|
70
|
-
const independent = boundaries.has(item.kind) ||
|
|
71
|
-
(item.kind === 'context_compaction' && payload.mode !== 'BACKGROUND') ||
|
|
72
|
-
(item.kind === 'error' && item.parentItemId === null);
|
|
70
|
+
const independent = boundaries.has(item.kind) || (item.kind === 'error' && item.parentItemId === null);
|
|
73
71
|
if (visible(item)) {
|
|
74
72
|
if (independent) {
|
|
75
73
|
current = undefined;
|
|
@@ -5,7 +5,7 @@ import { nodeLog } from '../operational.js';
|
|
|
5
5
|
import { isTerminalRunStatus } from './runtime/type-guards.js';
|
|
6
6
|
import { sessionPage } from './pagination.js';
|
|
7
7
|
import { latestSessionActivityAt } from './session-presentation-service.js';
|
|
8
|
-
import { marctlSessionSummary } from '../service/session-recent-activity.js';
|
|
8
|
+
import { marctlSessionSummary, sessionActivityStatus } from '../service/session-recent-activity.js';
|
|
9
9
|
import { WorkspaceSessionCleanupBudget, WorkspaceSessionCleanupDeadline } from '../service/workspace-session-cleanup-budget.js';
|
|
10
10
|
export class SessionCatalogService {
|
|
11
11
|
options;
|
|
@@ -92,7 +92,8 @@ export class SessionCatalogService {
|
|
|
92
92
|
session.metadata.runnerTitle ??
|
|
93
93
|
'Untitled session',
|
|
94
94
|
runner: session.runner,
|
|
95
|
-
activity: live && 'activity' in live ? live.activity : 'IDLE'
|
|
95
|
+
activity: live && 'activity' in live ? live.activity : 'IDLE',
|
|
96
|
+
status: sessionActivityStatus(this.options.runtime, session.id)
|
|
96
97
|
});
|
|
97
98
|
const children = [...database.listChildSessionRecords(input.workspaceId, session.id)];
|
|
98
99
|
if (input.includeAncestors === true)
|
|
@@ -14,6 +14,7 @@ import { prepareComposerResourceInput, composerResourceInputTags, withSessionMes
|
|
|
14
14
|
const INTERNAL_GOAL = Symbol('internalGoal');
|
|
15
15
|
const INTERNAL_SCHEDULE = Symbol('internalSchedule');
|
|
16
16
|
const INTERNAL_RESTORE = Symbol('internalRestore');
|
|
17
|
+
const RESTORE_CONFIGURATION = Symbol('restoreConfiguration');
|
|
17
18
|
export class SessionMessageService {
|
|
18
19
|
options;
|
|
19
20
|
pendingMessages = new Map();
|
|
@@ -28,8 +29,12 @@ export class SessionMessageService {
|
|
|
28
29
|
setAccepting(accepting) {
|
|
29
30
|
this.accepting = accepting;
|
|
30
31
|
}
|
|
31
|
-
restoreQueued(input) {
|
|
32
|
-
return this.message({
|
|
32
|
+
restoreQueued(input, configuration) {
|
|
33
|
+
return this.message({
|
|
34
|
+
...input,
|
|
35
|
+
[INTERNAL_RESTORE]: true,
|
|
36
|
+
...(configuration === undefined ? {} : { [RESTORE_CONFIGURATION]: configuration })
|
|
37
|
+
});
|
|
33
38
|
}
|
|
34
39
|
operations() {
|
|
35
40
|
return {
|
|
@@ -42,6 +47,9 @@ export class SessionMessageService {
|
|
|
42
47
|
return this.message({
|
|
43
48
|
[INTERNAL_GOAL]: true,
|
|
44
49
|
[INTERNAL_RESTORE]: input.restoreQueued === true,
|
|
50
|
+
...(input.restoreQueued !== true || input.restartConfiguration === undefined
|
|
51
|
+
? {}
|
|
52
|
+
: { [RESTORE_CONFIGURATION]: input.restartConfiguration }),
|
|
45
53
|
sessionId: input.sessionId,
|
|
46
54
|
__suppressNotifications: input.suppressNotifications === true ||
|
|
47
55
|
(input.kind === 'continue' &&
|
|
@@ -146,10 +154,11 @@ export class SessionMessageService {
|
|
|
146
154
|
};
|
|
147
155
|
const executionContext = input[RUNNER_EXECUTION_CONTEXT] ??
|
|
148
156
|
this.options.runners.require(session.runner).prepareExecutionContext(input);
|
|
157
|
+
const restoredConfiguration = input[RESTORE_CONFIGURATION];
|
|
149
158
|
if (session.parentSessionId != null)
|
|
150
159
|
this.options.runners
|
|
151
160
|
.require(session.runner)
|
|
152
|
-
.assertChildModel(session.modelId, executionContext);
|
|
161
|
+
.assertChildModel((restoredConfiguration ?? session).modelId, executionContext);
|
|
153
162
|
const sourceContent = withSessionMessageOrigin(content, input.__marctlOrigin, session.parentSessionId);
|
|
154
163
|
const goalMessage = internalGoal || internalSchedule || internalRestore
|
|
155
164
|
? undefined
|
|
@@ -297,9 +306,9 @@ export class SessionMessageService {
|
|
|
297
306
|
cwd,
|
|
298
307
|
workspacePath,
|
|
299
308
|
externalSessionId: currentSession.externalSessionId,
|
|
300
|
-
modelId: currentSession.modelId,
|
|
301
|
-
effort: currentSession.effort,
|
|
302
|
-
access: currentSession.access,
|
|
309
|
+
modelId: (restoredConfiguration ?? currentSession).modelId,
|
|
310
|
+
effort: (restoredConfiguration ?? currentSession).effort,
|
|
311
|
+
access: (restoredConfiguration ?? currentSession).access,
|
|
303
312
|
collaborationMode: planActive ? 'plan' : 'default',
|
|
304
313
|
secretEnvironment,
|
|
305
314
|
...(personalInstructions === undefined ? {} : { personalInstructions }),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myagentroam/node",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.50",
|
|
4
4
|
"description": "MyAgentRoam Node runtime CLI.",
|
|
5
5
|
"license": "Apache-2.0 AND GPL-2.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
"sharp": "0.35.5",
|
|
39
39
|
"ws": "^8.21.3",
|
|
40
40
|
"zod": "4.4.3",
|
|
41
|
-
"@myagentroam/agent": "0.11.
|
|
41
|
+
"@myagentroam/agent": "0.11.50",
|
|
42
|
+
"@myagentroam/marctl": "0.11.10",
|
|
42
43
|
"@myagentroam/code-navigation": "0.11.1",
|
|
43
|
-
"@myagentroam/protocol": "0.11.
|
|
44
|
-
"@myagentroam/marctl": "0.11.9"
|
|
44
|
+
"@myagentroam/protocol": "0.11.50"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/ws": "^8.18.1"
|