@myagentroam/node 0.10.34 → 0.10.36
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 +113 -18
- package/dist/database.js +27 -12
- package/dist/main.js +18 -2
- package/dist/migrations/v019.js +9 -0
- package/dist/runner/mar-agent/conversation-normalizer.js +28 -0
- package/dist/runner/mar-agent/marctl-skill.js +7 -5
- package/dist/runner/mar-agent-runner.js +32 -0
- package/dist/runtime-state.js +15 -10
- package/dist/service/continuous-run-coordinator.js +31 -0
- package/dist/service/direct-transfer-service.js +58 -11
- package/dist/service/external-session-resume-service.js +2 -1
- package/dist/service/goal-runtime.js +13 -0
- package/dist/service/native-session-projection-service.js +77 -11
- package/dist/service/planned-restart-handoff.js +279 -0
- package/dist/service/restart-session-restorer.js +141 -0
- package/dist/service/{scheduled-run-authorization-control.js → run-authorization-control.js} +17 -10
- package/dist/service/run-event-service.js +2 -0
- package/dist/service/session-catalog-service.js +57 -4
- package/dist/service/session-command-service.js +1 -0
- package/dist/service/session-lifecycle-service.js +25 -8
- package/dist/service/session-message-service.js +60 -14
- package/dist/service/session-presentation-service.js +2 -0
- package/dist/service/session-query-service.js +14 -9
- package/dist/service/workspace-queue-workbench-service.js +8 -3
- package/dist/service/workspace-upload-service.js +128 -29
- package/dist/supervisor.js +7 -3
- package/package.json +4 -4
package/dist/connector.js
CHANGED
|
@@ -68,7 +68,9 @@ import { composerResourceInputTags } from './service/composer-message-input.js';
|
|
|
68
68
|
import { SessionScheduleService } from './service/session-schedule-service.js';
|
|
69
69
|
import { ScheduledSessionCoordinator } from './service/scheduled-session-coordinator.js';
|
|
70
70
|
import { ScheduledSessionStore } from './service/scheduled-session-store.js';
|
|
71
|
-
import {
|
|
71
|
+
import { RunAuthorizationControl } from './service/run-authorization-control.js';
|
|
72
|
+
import { PlannedRestartHandoff } from './service/planned-restart-handoff.js';
|
|
73
|
+
import { RestartSessionRestorer } from './service/restart-session-restorer.js';
|
|
72
74
|
import { ScheduledTaskMcpServer } from './service/scheduled-task-mcp-server.js';
|
|
73
75
|
import { MailNotificationControl } from './service/mail-notification-control.js';
|
|
74
76
|
import { MailNotificationMcpServer, resolveMailNotificationCapability } from './service/mail-notification-mcp-server.js';
|
|
@@ -169,6 +171,9 @@ export class NodeConnector {
|
|
|
169
171
|
controlChannel;
|
|
170
172
|
controlMessages;
|
|
171
173
|
scheduledRunAuthorization;
|
|
174
|
+
restartAuthorization;
|
|
175
|
+
restartHandoff;
|
|
176
|
+
inFlightOperations = new Set();
|
|
172
177
|
mailNotificationControl = new MailNotificationControl((envelope) => this.sendEnvelope(envelope));
|
|
173
178
|
scheduledSessions;
|
|
174
179
|
nodeRequests;
|
|
@@ -397,6 +402,8 @@ export class NodeConnector {
|
|
|
397
402
|
this.goalRuntime.get(sessionId)?.status === 'active',
|
|
398
403
|
executionTerminating: (executionId, status, permissionStop) => {
|
|
399
404
|
this.gitOperationService.executionFinished(executionId);
|
|
405
|
+
if (this.stopped)
|
|
406
|
+
return;
|
|
400
407
|
const pending = this.continuousRunCoordinator.pending(executionId);
|
|
401
408
|
if (pending?.goal === undefined)
|
|
402
409
|
return;
|
|
@@ -779,7 +786,8 @@ export class NodeConnector {
|
|
|
779
786
|
goal: (sessionId) => this.goalRuntime.get(sessionId),
|
|
780
787
|
runtime: this.runtime,
|
|
781
788
|
runners: this.runners,
|
|
782
|
-
resolve: (sessionId) => this.nativeSessionProjectionService.resolve(sessionId),
|
|
789
|
+
resolve: (sessionId, registeredOnly) => this.nativeSessionProjectionService.resolve(sessionId, registeredOnly),
|
|
790
|
+
similar: (sessionId, workspaceId) => this.nativeSessionProjectionService.similarSessionDetails(sessionId, workspaceId),
|
|
783
791
|
present: (session) => this.sessionPresentationService.present(session),
|
|
784
792
|
history: (session, input) => this.conversationHistoryService.read(session, input),
|
|
785
793
|
segments: (session, input) => this.conversationSegmentService.read(session, input),
|
|
@@ -937,6 +945,7 @@ export class NodeConnector {
|
|
|
937
945
|
});
|
|
938
946
|
this.workspaceQueueWorkbenchService = new WorkspaceQueueWorkbenchService({
|
|
939
947
|
runtime: this.runtime,
|
|
948
|
+
resolve: (sessionId, registeredOnly) => this.nativeSessionProjectionService.resolve(sessionId, registeredOnly),
|
|
940
949
|
coordinator: this.continuousRunCoordinator,
|
|
941
950
|
emit: (workspaceId, sessionId) => this.runEventBridge.emitQueue(workspaceId, sessionId),
|
|
942
951
|
prepareInput: (runner, input, collaborationMode) => this.runners.require(runner).prepareMessageInput(input, collaborationMode)
|
|
@@ -947,15 +956,18 @@ export class NodeConnector {
|
|
|
947
956
|
void this.runAttachmentService.releaseSession(sessionId);
|
|
948
957
|
},
|
|
949
958
|
persist: (session, nativeSessionId) => {
|
|
950
|
-
if (session.model === null || session.effort === null || session.access === null)
|
|
951
|
-
return;
|
|
952
959
|
this.database?.saveNativeSessionMetadata({
|
|
960
|
+
...(session.parentSessionId == null ? {} : { parentSessionId: session.parentSessionId }),
|
|
953
961
|
runner: session.runner,
|
|
954
962
|
externalSessionId: nativeSessionId,
|
|
955
963
|
workspaceId: session.workspaceId,
|
|
956
|
-
model: session.model,
|
|
957
|
-
effort: session.effort,
|
|
958
|
-
access: session.access
|
|
964
|
+
...(session.model === null ? {} : { model: session.model }),
|
|
965
|
+
...(session.effort === null ? {} : { effort: session.effort }),
|
|
966
|
+
...(session.access === null ? {} : { access: session.access }),
|
|
967
|
+
...(session.customTitle === null ? {} : { customTitle: session.customTitle }),
|
|
968
|
+
...(session.runnerTitle === null
|
|
969
|
+
? {}
|
|
970
|
+
: { runnerTitle: session.runnerTitle.slice(0, 160) })
|
|
959
971
|
});
|
|
960
972
|
},
|
|
961
973
|
migrateCommands: (previousId, nextId) => this.commandStates.promoteSessionIdentity(previousId, nextId),
|
|
@@ -1073,7 +1085,7 @@ export class NodeConnector {
|
|
|
1073
1085
|
goalAccessChanged: prepared.changed
|
|
1074
1086
|
};
|
|
1075
1087
|
},
|
|
1076
|
-
goal: async ({ session, objective, initiatedByUserId, secretEnvironment, personalInstructions, marAgentModel, marAgentModels, marAgentCompact, marAgentRuntimeScopeId, usageGrant }) => {
|
|
1088
|
+
goal: async ({ session, objective, initiatedByUserId, suppressNotifications, secretEnvironment, personalInstructions, marAgentModel, marAgentModels, marAgentCompact, marAgentRuntimeScopeId, usageGrant }) => {
|
|
1077
1089
|
this.invalidateGoalContinuation(session.id);
|
|
1078
1090
|
const prepared = this.prepareGoalSession(session);
|
|
1079
1091
|
const goalSession = prepared.session;
|
|
@@ -1093,6 +1105,7 @@ export class NodeConnector {
|
|
|
1093
1105
|
objective: goal.objective,
|
|
1094
1106
|
deliveryIntent: replacing ? 'REPLACE_CURRENT' : 'SEND',
|
|
1095
1107
|
goal: goalRef,
|
|
1108
|
+
...(suppressNotifications === true ? { suppressNotifications: true } : {}),
|
|
1096
1109
|
...(usageGrant === undefined ? {} : { usageGrant }),
|
|
1097
1110
|
...(initiatedByUserId === undefined ? {} : { initiatedByUserId }),
|
|
1098
1111
|
secretEnvironment,
|
|
@@ -1190,6 +1203,31 @@ export class NodeConnector {
|
|
|
1190
1203
|
},
|
|
1191
1204
|
goalPreparationFailed: (session, goal, activated) => this.handleGoalPreparationFailed(session, goal, activated)
|
|
1192
1205
|
});
|
|
1206
|
+
const inputState = (entry, message) => this.marAgentClient.restartInputState(entry.session.id, message.clientMessageId);
|
|
1207
|
+
const restorer = new RestartSessionRestorer({
|
|
1208
|
+
runtime: this.runtime,
|
|
1209
|
+
goals: this.goalRuntime,
|
|
1210
|
+
commands: this.commandStates,
|
|
1211
|
+
coordinator: this.continuousRunCoordinator,
|
|
1212
|
+
messages: this.sessionMessageService,
|
|
1213
|
+
resolve: (id) => this.externalSessionResumeService.resolveForMessage(id),
|
|
1214
|
+
authorize: (request) => this.restartAuthorization.authorize(request),
|
|
1215
|
+
inputState,
|
|
1216
|
+
withGrant: (context, work) => this.modelUsage.withGrant(context.usageGrant, work),
|
|
1217
|
+
publishGoal: (session, goal) => this.publishGoalState(session, goal),
|
|
1218
|
+
publishQueue: (session) => this.continuousRunCoordinator.publish(session.id)
|
|
1219
|
+
});
|
|
1220
|
+
this.restartHandoff = new PlannedRestartHandoff({
|
|
1221
|
+
directory: () => nodeDataDirectory(this.config, this.configPath),
|
|
1222
|
+
identity: () => ({ nodeId: this.config.nodeId, serverUrl: this.config.serverUrl }),
|
|
1223
|
+
runtime: this.runtime,
|
|
1224
|
+
coordinator: this.continuousRunCoordinator,
|
|
1225
|
+
goals: this.goalRuntime,
|
|
1226
|
+
commandActive: (id, command) => this.commandStates.get(id, command)?.active === true,
|
|
1227
|
+
inputState,
|
|
1228
|
+
restore: (entry, id) => restorer.restore(entry, id),
|
|
1229
|
+
diagnose: (code, sessionId) => nodeLog('node.restart.diagnostic', { code, sessionId })
|
|
1230
|
+
});
|
|
1193
1231
|
this.sessionCatalogService = new SessionCatalogService({
|
|
1194
1232
|
database: () => {
|
|
1195
1233
|
if (this.database === undefined)
|
|
@@ -1216,6 +1254,7 @@ export class NodeConnector {
|
|
|
1216
1254
|
refreshActivity: (session) => this.conversationHistoryService.refreshExternalActivity(session),
|
|
1217
1255
|
externalActivity: (sessionId) => this.sessionState.externalActivity.get(sessionId),
|
|
1218
1256
|
isWatching: (sessionId) => this.nativeSessionWatchService.isWatching(sessionId),
|
|
1257
|
+
isRecovering: (sessionId) => this.restartHandoff.blocks(sessionId),
|
|
1219
1258
|
history: (session, input) => this.conversationHistoryService.read(session, input),
|
|
1220
1259
|
invalidateHistory: (sessionId, workspaceId) => {
|
|
1221
1260
|
this.nativeSessionWatchService.invalidate(sessionId);
|
|
@@ -1323,10 +1362,11 @@ export class NodeConnector {
|
|
|
1323
1362
|
onClose: (code, reason) => {
|
|
1324
1363
|
this.promptCompletion?.disconnect();
|
|
1325
1364
|
this.usageOutbox?.disconnected();
|
|
1326
|
-
this.directTransferService?.close();
|
|
1365
|
+
this.directTransferService?.close('DIRECT_TRANSFER_CONTROL_UNAVAILABLE');
|
|
1327
1366
|
this.runtimeCredentials.disconnected();
|
|
1328
1367
|
this.chatgptLogins.close();
|
|
1329
1368
|
this.scheduledRunAuthorization.disconnected();
|
|
1369
|
+
this.restartAuthorization.disconnected();
|
|
1330
1370
|
this.mailNotificationControl.disconnected();
|
|
1331
1371
|
this.mailNotificationMcpServer.revokeAll();
|
|
1332
1372
|
this.terminalChannel.disconnect();
|
|
@@ -1364,7 +1404,8 @@ export class NodeConnector {
|
|
|
1364
1404
|
inspectWorkspace: (envelope) => this.workspaceCoordinator.inspect(envelope),
|
|
1365
1405
|
handleNodeRequest: (envelope) => this.nodeRequests.handle(envelope),
|
|
1366
1406
|
handleRuntimeCredentialResponse: (envelope) => this.runtimeCredentials.handle(envelope),
|
|
1367
|
-
handleScheduledRunAuthorizeResponse: (envelope) => this.scheduledRunAuthorization.handle(envelope)
|
|
1407
|
+
handleScheduledRunAuthorizeResponse: (envelope) => this.scheduledRunAuthorization.handle(envelope) ||
|
|
1408
|
+
this.restartAuthorization.handle(envelope),
|
|
1368
1409
|
handleMailNotificationResponse: (envelope) => this.mailNotificationControl.handle(envelope),
|
|
1369
1410
|
handleDirectTransfer: (envelope) => this.handleDirectTransfer(envelope),
|
|
1370
1411
|
handleTerminalDirect: (envelope) => this.handleTerminalDirect(envelope),
|
|
@@ -1435,6 +1476,9 @@ export class NodeConnector {
|
|
|
1435
1476
|
onConnected: () => {
|
|
1436
1477
|
this.runtimeCredentials.connected();
|
|
1437
1478
|
this.scheduledRunAuthorization.connected();
|
|
1479
|
+
this.restartAuthorization.connected();
|
|
1480
|
+
if (!this.stopped)
|
|
1481
|
+
void this.restartHandoff.recover();
|
|
1438
1482
|
this.mailNotificationControl.connected();
|
|
1439
1483
|
this.recentRunProjectionService.publishSnapshot();
|
|
1440
1484
|
}
|
|
@@ -1444,10 +1488,13 @@ export class NodeConnector {
|
|
|
1444
1488
|
throw new Error('NODE_DATABASE_UNAVAILABLE');
|
|
1445
1489
|
return this.database;
|
|
1446
1490
|
};
|
|
1447
|
-
this.scheduledRunAuthorization = new
|
|
1491
|
+
this.scheduledRunAuthorization = new RunAuthorizationControl((envelope) => this.controlChannel.sendEnvelope(envelope));
|
|
1492
|
+
this.restartAuthorization = new RunAuthorizationControl((envelope) => this.controlChannel.sendEnvelope(envelope), 'restart');
|
|
1448
1493
|
this.scheduledSessions = new ScheduledSessionCoordinator({
|
|
1449
1494
|
store: () => new ScheduledSessionStore(requireDatabase().raw),
|
|
1450
|
-
busy: (task) => this.
|
|
1495
|
+
busy: (task) => this.stopped ||
|
|
1496
|
+
this.restartHandoff.blocks(task.sessionId) ||
|
|
1497
|
+
this.runtime.activeSessionRun(task.sessionId) !== undefined ||
|
|
1451
1498
|
this.runtime.sessionQueue(task.sessionId).itemIds.length > 0,
|
|
1452
1499
|
authorize: async (task, occurrenceId) => {
|
|
1453
1500
|
const session = this.runtime.getAgentSession(task.sessionId) ??
|
|
@@ -1800,6 +1847,8 @@ export class NodeConnector {
|
|
|
1800
1847
|
await this.stopping;
|
|
1801
1848
|
this.stopping = undefined;
|
|
1802
1849
|
this.stopped = false;
|
|
1850
|
+
this.continuousRunCoordinator.resumeScheduling();
|
|
1851
|
+
this.sessionMessageService.setAccepting(true);
|
|
1803
1852
|
this.config ??= await loadNodeConfig(this.configPath);
|
|
1804
1853
|
this.promptCompletion.initializeJournal(nodeDataDirectory(this.config, this.configPath));
|
|
1805
1854
|
await this.upgradeCoordinator.loadResult(resolve(dirname(nodeDatabasePath(this.config, this.configPath)), 'upgrade-result.json'));
|
|
@@ -1816,6 +1865,7 @@ export class NodeConnector {
|
|
|
1816
1865
|
});
|
|
1817
1866
|
this.usageOutbox.recoverInterrupted();
|
|
1818
1867
|
this.database.closeRunningTerminalSessions();
|
|
1868
|
+
await this.restartHandoff.load();
|
|
1819
1869
|
this.scheduledSessions.start();
|
|
1820
1870
|
await this.initializeConversationHosting();
|
|
1821
1871
|
this.computerUseEnablement.initialize();
|
|
@@ -1838,6 +1888,7 @@ export class NodeConnector {
|
|
|
1838
1888
|
const upload = await this.workspaceUploadService.createDirect(workspace, {
|
|
1839
1889
|
path: descriptor.resource.path,
|
|
1840
1890
|
size: descriptor.size,
|
|
1891
|
+
sha256: descriptor.sha256,
|
|
1841
1892
|
overwrite: descriptor.overwrite === true,
|
|
1842
1893
|
...(descriptor.composerAttachment === undefined
|
|
1843
1894
|
? {}
|
|
@@ -1846,7 +1897,7 @@ export class NodeConnector {
|
|
|
1846
1897
|
return {
|
|
1847
1898
|
offset: upload.receivedBytes,
|
|
1848
1899
|
write: (bytes) => this.workspaceUploadService.writeDirect(upload.uploadId, bytes),
|
|
1849
|
-
complete: (size) => this.workspaceUploadService.completeDirect(upload.uploadId, size),
|
|
1900
|
+
complete: (size, signal) => this.workspaceUploadService.completeDirect(upload.uploadId, size, signal),
|
|
1850
1901
|
cancel: () => this.workspaceUploadService.cancel(upload.uploadId)
|
|
1851
1902
|
};
|
|
1852
1903
|
},
|
|
@@ -2141,10 +2192,32 @@ export class NodeConnector {
|
|
|
2141
2192
|
closeInput: '/goal --clear'
|
|
2142
2193
|
};
|
|
2143
2194
|
}
|
|
2144
|
-
stop() {
|
|
2195
|
+
stop(options = {}) {
|
|
2145
2196
|
if (this.stopping !== undefined)
|
|
2146
2197
|
return this.stopping;
|
|
2147
2198
|
this.stopped = true;
|
|
2199
|
+
this.continuousRunCoordinator.suspend();
|
|
2200
|
+
this.scheduledSessions.stop();
|
|
2201
|
+
this.stopping = (async () => {
|
|
2202
|
+
let snapshot;
|
|
2203
|
+
try {
|
|
2204
|
+
// In-flight recovery must rebuild accepted inputs before preparation closes.
|
|
2205
|
+
await this.restartHandoff.wait();
|
|
2206
|
+
this.sessionMessageService.setAccepting(false);
|
|
2207
|
+
await Promise.allSettled([...this.inFlightOperations]);
|
|
2208
|
+
await this.sessionMessageService.drain();
|
|
2209
|
+
if (options.handoff && this.config?.nodeId)
|
|
2210
|
+
snapshot = this.restartHandoff.capture();
|
|
2211
|
+
}
|
|
2212
|
+
finally {
|
|
2213
|
+
await this.disposeRuntime();
|
|
2214
|
+
}
|
|
2215
|
+
if (snapshot !== undefined)
|
|
2216
|
+
await this.restartHandoff.publish(snapshot);
|
|
2217
|
+
})();
|
|
2218
|
+
return this.stopping;
|
|
2219
|
+
}
|
|
2220
|
+
disposeRuntime() {
|
|
2148
2221
|
this.recentRunProjectionService.dispose();
|
|
2149
2222
|
this.upgradeCoordinator.stop();
|
|
2150
2223
|
this.connectionLifecycle.stop();
|
|
@@ -2202,6 +2275,7 @@ export class NodeConnector {
|
|
|
2202
2275
|
this.chatgptLogins.close();
|
|
2203
2276
|
this.scheduledSessions.stop();
|
|
2204
2277
|
this.scheduledRunAuthorization.close();
|
|
2278
|
+
this.restartAuthorization.close();
|
|
2205
2279
|
this.mailNotificationControl.close();
|
|
2206
2280
|
const mailMcpDrain = this.mailNotificationMcpServer.close();
|
|
2207
2281
|
const webToolDrain = this.webToolService.close();
|
|
@@ -2220,21 +2294,24 @@ export class NodeConnector {
|
|
|
2220
2294
|
mailMcpDrain,
|
|
2221
2295
|
webToolDrain
|
|
2222
2296
|
]);
|
|
2223
|
-
|
|
2297
|
+
return (async () => {
|
|
2298
|
+
let failed;
|
|
2224
2299
|
try {
|
|
2225
2300
|
await conversationHostingMaintenanceDrain;
|
|
2226
2301
|
}
|
|
2227
2302
|
finally {
|
|
2228
2303
|
await hostedConversationRuntime?.dispose().catch(() => undefined);
|
|
2229
|
-
await drains;
|
|
2304
|
+
const results = await drains;
|
|
2230
2305
|
await this.gitExecutionService?.close();
|
|
2231
2306
|
this.usageOutbox?.disconnected();
|
|
2232
2307
|
this.usageOutbox = undefined;
|
|
2233
2308
|
this.modelUsage.clear();
|
|
2234
2309
|
database?.close();
|
|
2310
|
+
failed = results.find((result) => result.status === 'rejected');
|
|
2235
2311
|
}
|
|
2312
|
+
if (failed !== undefined)
|
|
2313
|
+
throw failed.reason;
|
|
2236
2314
|
})();
|
|
2237
|
-
return this.stopping;
|
|
2238
2315
|
}
|
|
2239
2316
|
/** @internal Thin compatibility delegates; all behavior remains in the extracted services. */
|
|
2240
2317
|
handleMessage = (raw) => this.controlMessages.handle(raw);
|
|
@@ -2375,12 +2452,30 @@ export class NodeConnector {
|
|
|
2375
2452
|
startCapabilityRefresh = () => this.connectionLifecycle.startCapabilityRefresh();
|
|
2376
2453
|
refreshCapabilities = () => this.connectionLifecycle.refreshCapabilities();
|
|
2377
2454
|
async executeNodeOperation(operation, data) {
|
|
2455
|
+
if (this.stopped)
|
|
2456
|
+
throw new Error('NODE_STOPPED');
|
|
2457
|
+
const input = isPlainRecord(data) ? data : {};
|
|
2458
|
+
const sessionId = typeof input.sessionId === 'string' ? input.sessionId : undefined;
|
|
2459
|
+
if (this.restartHandoff.blocksOperation(operation, input))
|
|
2460
|
+
throw new Error('SESSION_RESTART_PENDING');
|
|
2378
2461
|
if (this.database === undefined)
|
|
2379
2462
|
throw new Error('NODE_DATABASE_UNAVAILABLE');
|
|
2380
2463
|
if (this.upgradeCoordinator.blocks(operation))
|
|
2381
2464
|
throw new Error('NODE_UPGRADE_PENDING');
|
|
2382
2465
|
nodeLog('node.operation', { nodeId: this.config?.nodeId, operation });
|
|
2383
|
-
|
|
2466
|
+
const task = this.gitOperationService.execute(operation, data, (input) => this.operationRouter.execute(operation, input));
|
|
2467
|
+
this.inFlightOperations.add(task);
|
|
2468
|
+
try {
|
|
2469
|
+
const result = await task;
|
|
2470
|
+
if ((operation === 'session.command.execute' || operation === 'session.message') &&
|
|
2471
|
+
sessionId !== undefined &&
|
|
2472
|
+
Number.isSafeInteger(input.__requestUserId))
|
|
2473
|
+
this.restartHandoff.rememberUser(sessionId, input.__requestUserId);
|
|
2474
|
+
return result;
|
|
2475
|
+
}
|
|
2476
|
+
finally {
|
|
2477
|
+
this.inFlightOperations.delete(task);
|
|
2478
|
+
}
|
|
2384
2479
|
}
|
|
2385
2480
|
gitExecution() {
|
|
2386
2481
|
this.gitExecutionService ??= new GitExecutionService({
|
package/dist/database.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NODE_SCHEMA_MIGRATION_V17 } from './migrations/v017.js';
|
|
2
2
|
import { NODE_SCHEMA_MIGRATION_V18 } from './migrations/v018.js';
|
|
3
|
+
import { NODE_SCHEMA_MIGRATION_V19 } from './migrations/v019.js';
|
|
3
4
|
import { normalizeSessionPrompt } from './util/personal-instructions.js';
|
|
4
5
|
import { randomUUID } from 'node:crypto';
|
|
5
6
|
import { closeSync, existsSync, mkdirSync, openSync, rmSync } from 'node:fs';
|
|
@@ -19,7 +20,8 @@ export const NODE_SCHEMA_MIGRATIONS = [
|
|
|
19
20
|
NODE_SCHEMA_MIGRATION_V15,
|
|
20
21
|
NODE_SCHEMA_MIGRATION_V16,
|
|
21
22
|
NODE_SCHEMA_MIGRATION_V17,
|
|
22
|
-
NODE_SCHEMA_MIGRATION_V18
|
|
23
|
+
NODE_SCHEMA_MIGRATION_V18,
|
|
24
|
+
NODE_SCHEMA_MIGRATION_V19
|
|
23
25
|
];
|
|
24
26
|
const LEGACY_NODE_SCHEMA_V12 = NODE_SCHEMA_VERSION_V13 - 1;
|
|
25
27
|
const OBSOLETE_NODE_SCHEMA_V16 = 16;
|
|
@@ -288,8 +290,10 @@ export class NodeDatabase {
|
|
|
288
290
|
const model = input.model === undefined ? (current?.model ?? null) : input.model;
|
|
289
291
|
const effort = input.effort === undefined ? (current?.effort ?? null) : input.effort;
|
|
290
292
|
const access = input.access === undefined ? (current?.access ?? null) : input.access;
|
|
293
|
+
const parentSessionId = current?.parentSessionId ?? input.parentSessionId ?? null;
|
|
291
294
|
const metadata = JSON.stringify({
|
|
292
295
|
version: 1,
|
|
296
|
+
parentSessionId,
|
|
293
297
|
customTitle,
|
|
294
298
|
sessionPrompt,
|
|
295
299
|
runnerTitle,
|
|
@@ -328,6 +332,7 @@ export class NodeDatabase {
|
|
|
328
332
|
}
|
|
329
333
|
return {
|
|
330
334
|
customTitle,
|
|
335
|
+
parentSessionId,
|
|
331
336
|
sessionPrompt,
|
|
332
337
|
runnerTitle,
|
|
333
338
|
runnerTitlePending,
|
|
@@ -341,17 +346,13 @@ export class NodeDatabase {
|
|
|
341
346
|
listSessionRecords(workspaceId) {
|
|
342
347
|
return this.raw
|
|
343
348
|
.prepare('SELECT id, workspace_id, runner, pin_order, metadata FROM agent_sessions WHERE workspace_id = ? ORDER BY pin_order IS NULL, pin_order, id')
|
|
344
|
-
.all(workspaceId).map(
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
pinnedAt: row['pin_order'] === null ? null : 1,
|
|
352
|
-
pinOrder: nullableNumber(row, 'pin_order')
|
|
353
|
-
}
|
|
354
|
-
}));
|
|
349
|
+
.all(workspaceId).map(sessionRecord);
|
|
350
|
+
}
|
|
351
|
+
getSessionRecord(sessionId) {
|
|
352
|
+
const row = this.raw
|
|
353
|
+
.prepare('SELECT id, workspace_id, runner, pin_order, metadata FROM agent_sessions WHERE id = ?')
|
|
354
|
+
.get(sessionId);
|
|
355
|
+
return row === undefined ? undefined : sessionRecord(row);
|
|
355
356
|
}
|
|
356
357
|
movePinnedSession(sessionId, direction) {
|
|
357
358
|
const current = this.raw
|
|
@@ -565,6 +566,19 @@ function mcpInstallation(row) {
|
|
|
565
566
|
installedAt: numberValue(row, 'installed_at')
|
|
566
567
|
};
|
|
567
568
|
}
|
|
569
|
+
function sessionRecord(row) {
|
|
570
|
+
return {
|
|
571
|
+
id: stringValue(row, 'id'),
|
|
572
|
+
workspaceId: stringValue(row, 'workspace_id'),
|
|
573
|
+
runner: stringValue(row, 'runner'),
|
|
574
|
+
pinOrder: nullableNumber(row, 'pin_order'),
|
|
575
|
+
metadata: {
|
|
576
|
+
...sessionMetadata(row),
|
|
577
|
+
pinnedAt: row['pin_order'] === null ? null : 1,
|
|
578
|
+
pinOrder: nullableNumber(row, 'pin_order')
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
}
|
|
568
582
|
function sessionMetadata(row) {
|
|
569
583
|
const raw = JSON.parse(stringValue(row, 'metadata'));
|
|
570
584
|
if (raw['version'] !== 1)
|
|
@@ -572,6 +586,7 @@ function sessionMetadata(row) {
|
|
|
572
586
|
const value = (key) => typeof raw[key] === 'string' ? raw[key] : null;
|
|
573
587
|
return {
|
|
574
588
|
customTitle: value('customTitle'),
|
|
589
|
+
parentSessionId: value('parentSessionId'),
|
|
575
590
|
sessionPrompt: value('sessionPrompt'),
|
|
576
591
|
runnerTitle: value('runnerTitle'),
|
|
577
592
|
runnerTitlePending: raw['runnerTitlePending'] === true,
|
package/dist/main.js
CHANGED
|
@@ -7,6 +7,7 @@ import { getNodeHealth } from './health.js';
|
|
|
7
7
|
import { browserCachePath, removeServiceDefinition, writeServiceDefinition } from './service.js';
|
|
8
8
|
import { superviseNode } from './supervisor.js';
|
|
9
9
|
import { configureNodeLog } from './rotating-log.js';
|
|
10
|
+
import { nodeLog } from './operational.js';
|
|
10
11
|
const [command = 'status', ...args] = process.argv.slice(2);
|
|
11
12
|
const config = readOption(args, '--config') ?? nodeConfigPath();
|
|
12
13
|
const output = readOption(args, '--output') ?? defaultServicePath();
|
|
@@ -17,11 +18,26 @@ if (command === 'run') {
|
|
|
17
18
|
const { NodeConnector } = await import('./connector.js');
|
|
18
19
|
configureNodeLog(nodeDataDirectory(await loadNodeConfig(config), config));
|
|
19
20
|
const connector = new NodeConnector({ configPath: config });
|
|
21
|
+
let shuttingDown = false;
|
|
20
22
|
const shutdown = () => {
|
|
21
|
-
|
|
23
|
+
if (shuttingDown)
|
|
24
|
+
return;
|
|
25
|
+
shuttingDown = true;
|
|
26
|
+
const deadline = setTimeout(() => {
|
|
27
|
+
nodeLog('node.restart.diagnostic', { code: 'RESTART_HANDOFF_SHUTDOWN_TIMEOUT' });
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}, 25_000);
|
|
30
|
+
void connector.stop({ handoff: true }).then(() => {
|
|
31
|
+
clearTimeout(deadline);
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}, () => {
|
|
34
|
+
clearTimeout(deadline);
|
|
35
|
+
nodeLog('node.restart.diagnostic', { code: 'RESTART_HANDOFF_SHUTDOWN_FAILED' });
|
|
36
|
+
process.exit(1);
|
|
37
|
+
});
|
|
22
38
|
};
|
|
23
39
|
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
24
|
-
process.
|
|
40
|
+
process.on(signal, shutdown);
|
|
25
41
|
}
|
|
26
42
|
await connector.start();
|
|
27
43
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { NODE_SCHEMA_MIGRATION_V18 } from './v018.js';
|
|
2
|
+
export const NODE_SCHEMA_MIGRATION_V19 = {
|
|
3
|
+
version: 19,
|
|
4
|
+
sql: `UPDATE agent_sessions
|
|
5
|
+
SET metadata = json_set(metadata, '$.parentSessionId', NULL)
|
|
6
|
+
WHERE json_type(metadata, '$.parentSessionId') IS NULL;`,
|
|
7
|
+
preflight: (database) => NODE_SCHEMA_MIGRATION_V18.assert(database),
|
|
8
|
+
assert: (database) => NODE_SCHEMA_MIGRATION_V18.assert(database)
|
|
9
|
+
};
|
|
@@ -579,6 +579,34 @@ function genericToolItem(id, toolName, status, input, outputSummary = null, erro
|
|
|
579
579
|
truncated: truncated || source.length > 10_000 || output.length > 10_000
|
|
580
580
|
}, status);
|
|
581
581
|
}
|
|
582
|
+
if (toolName === 'code_exec_wait') {
|
|
583
|
+
const inputRecord = record(input);
|
|
584
|
+
const action = typeof inputRecord?.action === 'string' ? inputRecord.action : 'poll';
|
|
585
|
+
const inputSummary = compactRunnerValue(input, 10_000);
|
|
586
|
+
const result = output === undefined ? null : compactRunnerValue(output, 10_000);
|
|
587
|
+
const resultSummary = [outputSummary, result?.text]
|
|
588
|
+
.filter((value, index, values) => typeof value === 'string' && value.length > 0 && values.indexOf(value) === index)
|
|
589
|
+
.join('\n');
|
|
590
|
+
const details = [
|
|
591
|
+
`Action: ${compactRunnerText(action, 128)}`,
|
|
592
|
+
`Input: ${inputSummary.text || '{}'}`,
|
|
593
|
+
resultSummary.length === 0 ? '' : `Result: ${compactRunnerText(resultSummary, 10_000)}`,
|
|
594
|
+
errorCode === null ? '' : `Error: ${compactRunnerText(errorCode, 80)}`
|
|
595
|
+
]
|
|
596
|
+
.filter(Boolean)
|
|
597
|
+
.join('\n');
|
|
598
|
+
return commandItem({
|
|
599
|
+
itemId: id,
|
|
600
|
+
command: toolName,
|
|
601
|
+
cwd: null,
|
|
602
|
+
outputPreview: details,
|
|
603
|
+
exitCode: null,
|
|
604
|
+
truncated: truncated ||
|
|
605
|
+
inputSummary.truncated ||
|
|
606
|
+
result?.truncated === true ||
|
|
607
|
+
resultSummary.length > 10_000
|
|
608
|
+
}, status);
|
|
609
|
+
}
|
|
582
610
|
const subagent = marAgentSubagentProjection(toolName, input, output, subagentTitle, knownSubagentDetails);
|
|
583
611
|
const toolCategory = isImageGenerationTool(toolName) ? 'IMAGE_GENERATION' : undefined;
|
|
584
612
|
const inputSummary = compactRunnerValue(input, 10_000);
|
|
@@ -18,18 +18,20 @@ export function marctlHostSkill(environment = {}) {
|
|
|
18
18
|
const marctlInstructions = [
|
|
19
19
|
'## MAR Workspace CLI',
|
|
20
20
|
'Use exec to run marctl. Reuse loaded guidance while it remains in context. Consult local marctl help sessions for unclear options and marctl --version when version matters; neither needs a network request. Capabilities and permissions still apply.',
|
|
21
|
-
'Use sessions current --json only when the current identity or scope is unknown. For a known target ID, use sessions show <session-id> --json only when fresh state is needed, without current/list first. Use sessions list --limit 10 --json to discover an unknown target;
|
|
21
|
+
'Use sessions current --json only when the current identity or scope is unknown. For a known target ID, use sessions show <session-id> --json only when fresh state is needed, without current/list first. Use sessions list --limit 10 --json to discover an unknown target; narrow with --query <title> or --parent <session-id>. These query Node-known Sessions without scanning Runners. Use sessions graph <session-id> --json only for a creation tree (bounded to 100 nodes with truncated); start automatically records the current Session as parent, not task ownership. Stop once found and follow nextCursor only if needed. Do not scan the Workspace to prove capacity: check assigned work, recorded worker IDs and overlapping write scopes.',
|
|
22
22
|
'MAR binds sessions to the current Node and Workspace. No --node or --workspace override exists. Use returned native Session IDs, not titles, for later operations. A Workspace terminal may have no current Session. Missing scope, an old Node, expired authorization or an unavailable Runner is an explicit error, not permission to bypass the API.',
|
|
23
23
|
'## Create and coordinate independent Sessions',
|
|
24
|
+
'Change an existing Session with sessions config <session-id> --model <id> --effort high --json; either option can be set alone. Use show for current settings; active execution is not restarted.',
|
|
24
25
|
'Prefer -m / --message for short text; use --message-file for longer UTF-8 text or an existing file (- reads stdin). These options are mutually exclusive. Example: marctl sessions start --runner codex --title "fixer#42 Handle empty totals" -m "Implement issue #42 and report test evidence" --json. Other Runner names are mar-agent, claude-code and opencode. An omitted model uses the target Runner configuration, not the Coordinator model.',
|
|
25
26
|
'For start/send, repeat --image <local-path> to attach PNG, JPEG, GIF or WebP images, with or without text. Images are read on this CLI host, not by remote path. Limits: 5 images, 5 MiB each, 10 MiB total. Actual Runner/model image capabilities apply; goal set accepts text only.',
|
|
26
|
-
'Create workers only as tasks need them, named fixer#<task-id> <description> or reviewer#<task-or-change-id> <description>. These are conventions, not product roles or a fixed pool. Put role, scope, acceptance criteria and delivery requirements in the first message. Session Prompt is edited in the Web UI only; marctl has no editing command, and sends to existing Sessions use normal prompt injection.',
|
|
27
|
-
'Reuse recorded Session/Run IDs and receipts. Refresh volatile Run, queue and Goal state with sessions show only when the next action depends on it.
|
|
28
|
-
'
|
|
27
|
+
'Create workers only as tasks need them, named fixer#<task-id> <description> or reviewer#<task-or-change-id> <description>. These are conventions, not product roles or a fixed pool. Put role, scope, acceptance criteria and delivery requirements in the first message. Follow the repository workflow for development and review. Session Prompt is edited in the Web UI only; marctl has no editing command, and sends to existing Sessions use normal prompt injection.',
|
|
28
|
+
'Reuse recorded Session/Run IDs and receipts. Refresh volatile Run, queue and Goal state with sessions show only when the next action depends on it. Select needed JSON fields instead of dumping full Session lists or tool transcripts. Hidden reasoning, credentials and raw model requests are not collaboration artifacts.',
|
|
29
|
+
'Read results with sessions history <session-id> --limit 10 --json (default: latest 10 SEGMENTs, not messages; --limit 1..100). Extract needed assistant_message payload.text from segments[].items[]; keep segment runId and user_message.payload.clientMessageId for queued-input correlation. Reuse the returned page; follow --cursor <nextCursor> only for missing evidence.',
|
|
30
|
+
'Send urgent corrections with sessions send <session-id> --forced -m "Use the corrected acceptance criteria" --json. Always choose exactly one of --forced or --queue, even when idle; missing or conflicting modes are rejected locally. --forced inserts: send immediately when idle; if busy, interrupt the current execution and prioritize the new input after release, not live steering. Use --queue to wait for an eligible FIFO turn without interrupting or resuming a paused queue; eligible input can start immediately when idle. For non-urgent follow-ups, use sessions send <session-id> --queue -m "Include test evidence" --json. There are no queue-item edit/delete/reorder commands; show includes queue state. start/send return clientMessageId and the accepted/queued result. A queued result may refer to the currently active Run: associate queueItemId/clientMessageId with history before deciding that input finished.',
|
|
29
31
|
'Wait with sessions wait <session-id> --run <run-id> --timeout 5m --json through the existing Watch. Prefer --timeout 5m when blocked on task completion; use 30s for a short deadline. Continue useful independent work before waiting. If exec returns a running process or cell, reuse its process/cell handle rather than starting another wait or polling show/list. Timeout or cancelled waiting does not stop the task. Use sessions stop <session-id> --run <run-id> --json only to stop that active Run; ownership rules still apply.',
|
|
30
32
|
'Goal get/set/clear use the existing Session Goal semantics; see marctl help sessions. A Coordinator can work from ordinary messages or its existing schedule without a Goal. Worker completion never automatically wakes it. Actively wait/read results, or finish and reconcile on the next user message or scheduled input.',
|
|
31
33
|
'## Verify, recover and respect authorization',
|
|
32
|
-
'An accepted message or SUCCEEDED Run is not proof of business completion. Inspect artifacts and test evidence.
|
|
34
|
+
'An accepted message or SUCCEEDED Run is not proof of business completion. Inspect artifacts and test evidence.',
|
|
33
35
|
'Network loss can leave a mutation outcome unknown. Preserve the reported clientMessageId and inspect current state/history before retrying; do not blindly resend or create another Session. Server/Node restart does not guarantee recovery of in-memory Runs or queues.',
|
|
34
36
|
'MAR injects user credentials for 24 hours from issue, without automatic renewal. Never print, copy into task files or persist authorization environment values. CLI calls use the real initiating user, including scheduled provenance; worker roles do not elevate access or inherit an auto-approval mode.',
|
|
35
37
|
'Sessions share Workspace files and the Git index. Agree on non-overlapping changes and respect repository locking rules. There is no automatic worktree isolation, workflow engine, reliable callback or completion receipt store.',
|
|
@@ -369,6 +369,38 @@ export class MarAgentRunner extends AbstractRunner {
|
|
|
369
369
|
await catalog.close().catch(() => undefined);
|
|
370
370
|
}
|
|
371
371
|
}
|
|
372
|
+
async restartInputState(sessionId, clientMessageId) {
|
|
373
|
+
const catalog = await openMarAgentSessionCatalog({ homeDirectory: this.homeDirectory });
|
|
374
|
+
try {
|
|
375
|
+
let cursor;
|
|
376
|
+
const states = new Map();
|
|
377
|
+
do {
|
|
378
|
+
const page = await catalog.history(sessionId, {
|
|
379
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
380
|
+
limit: 1000
|
|
381
|
+
});
|
|
382
|
+
for (const entry of page.entries) {
|
|
383
|
+
if (entry.kind !== 'event' || entry.event.turnId === undefined)
|
|
384
|
+
continue;
|
|
385
|
+
const event = entry.event;
|
|
386
|
+
if (event.type === 'execution.completed')
|
|
387
|
+
states.set(event.turnId, event.permissionStop === undefined ? 'completed' : 'paused');
|
|
388
|
+
else if (event.type === 'execution.failed')
|
|
389
|
+
states.set(event.turnId, 'completed');
|
|
390
|
+
else if (event.type === 'execution.cancelled')
|
|
391
|
+
states.set(event.turnId, 'interrupted');
|
|
392
|
+
}
|
|
393
|
+
const user = page.entries.find((entry) => entry.kind === 'user' && entry.clientMessageId === clientMessageId);
|
|
394
|
+
if (user?.kind === 'user')
|
|
395
|
+
return states.get(user.turnId) ?? 'interrupted';
|
|
396
|
+
cursor = page.nextCursor;
|
|
397
|
+
} while (cursor !== undefined);
|
|
398
|
+
return 'missing';
|
|
399
|
+
}
|
|
400
|
+
finally {
|
|
401
|
+
await catalog.close();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
372
404
|
async readOfficialConversation(session, input) {
|
|
373
405
|
if (session.externalSessionId === null)
|
|
374
406
|
return undefined;
|
package/dist/runtime-state.js
CHANGED
|
@@ -84,6 +84,7 @@ export class NodeRuntimeState {
|
|
|
84
84
|
// A first user prompt is only a process-local temporary title. A
|
|
85
85
|
// Runner/native title can replace it on the next discovery refresh.
|
|
86
86
|
titleSource: customTitle === null ? 'AUTO' : 'CUSTOM',
|
|
87
|
+
parentSessionId: input.parentSessionId ?? null,
|
|
87
88
|
lastActivityAt: input.lastActivityAt ?? now,
|
|
88
89
|
createdAt: input.createdAt ?? now
|
|
89
90
|
};
|
|
@@ -163,9 +164,9 @@ export class NodeRuntimeState {
|
|
|
163
164
|
}
|
|
164
165
|
updateAgentSessionConfiguration(input) {
|
|
165
166
|
return this.replaceSession(input.sessionId, {
|
|
166
|
-
model: input.model,
|
|
167
|
-
effort: input.effort,
|
|
168
|
-
access: input.access
|
|
167
|
+
...(input.model === undefined ? {} : { model: input.model }),
|
|
168
|
+
...(input.effort === undefined ? {} : { effort: input.effort }),
|
|
169
|
+
...(input.access === undefined ? {} : { access: input.access })
|
|
169
170
|
});
|
|
170
171
|
}
|
|
171
172
|
updateAgentSessionAccess(sessionId, access) {
|
|
@@ -338,6 +339,7 @@ export class NodeRuntimeState {
|
|
|
338
339
|
version: 0,
|
|
339
340
|
clientMessageId: input.clientMessageId,
|
|
340
341
|
...(input.source === undefined ? {} : { source: input.source }),
|
|
342
|
+
...(input.suppressNotifications === true ? { suppressNotifications: true } : {}),
|
|
341
343
|
...(input.initiatedByUserId === undefined
|
|
342
344
|
? {}
|
|
343
345
|
: { initiatedByUserId: input.initiatedByUserId }),
|
|
@@ -416,6 +418,7 @@ export class NodeRuntimeState {
|
|
|
416
418
|
runner: session.runner,
|
|
417
419
|
clientMessageId: input.clientMessageId,
|
|
418
420
|
content: input.content,
|
|
421
|
+
...(input.suppressNotifications === true ? { suppressNotifications: true } : {}),
|
|
419
422
|
attachments: input.attachments ?? [],
|
|
420
423
|
...(input.initiatedByUserId === undefined
|
|
421
424
|
? {}
|
|
@@ -449,11 +452,14 @@ export class NodeRuntimeState {
|
|
|
449
452
|
if (itemId === undefined)
|
|
450
453
|
return undefined;
|
|
451
454
|
const item = this.requireQueueItem(itemId);
|
|
452
|
-
const run =
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
455
|
+
const run = {
|
|
456
|
+
...active,
|
|
457
|
+
...(active.initiatedByUserId === undefined && item.initiatedByUserId !== undefined
|
|
458
|
+
? { initiatedByUserId: item.initiatedByUserId }
|
|
459
|
+
: {}),
|
|
460
|
+
suppressNotifications: item.suppressNotifications === true
|
|
461
|
+
};
|
|
462
|
+
this.runs.set(run.id, run);
|
|
457
463
|
this.queuedItems.set(sessionId, queue.slice(1));
|
|
458
464
|
this.bumpQueueVersion(sessionId);
|
|
459
465
|
this.queueItems.delete(itemId);
|
|
@@ -482,6 +488,7 @@ export class NodeRuntimeState {
|
|
|
482
488
|
clientMessageId: item.clientMessageId,
|
|
483
489
|
content: content ?? item.content,
|
|
484
490
|
recentUserMessage: item.content,
|
|
491
|
+
...(item.suppressNotifications === true ? { suppressNotifications: true } : {}),
|
|
485
492
|
...(item.attachments.length === 0 ? {} : { attachments: item.attachments }),
|
|
486
493
|
...(item.initiatedByUserId === undefined ? {} : { initiatedByUserId: item.initiatedByUserId })
|
|
487
494
|
});
|
|
@@ -607,8 +614,6 @@ export class NodeRuntimeState {
|
|
|
607
614
|
return { kind: 'run', run: this.requireRun(submission.id) };
|
|
608
615
|
const item = this.requireQueueItem(submission.id);
|
|
609
616
|
const run = this.activeSessionRun(sessionId);
|
|
610
|
-
if (run === undefined)
|
|
611
|
-
throw new Error('RUN_NOT_FOUND');
|
|
612
617
|
return { kind: 'queue', item, run };
|
|
613
618
|
}
|
|
614
619
|
messagePreparationFailure(sessionId, clientMessageId) {
|