@myagentroam/node 0.10.35 → 0.10.37
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 +115 -18
- package/dist/database.js +29 -12
- package/dist/main.js +18 -2
- package/dist/migrations/v019.js +9 -0
- package/dist/migrations/v020.js +46 -0
- package/dist/runner/mar-agent/computer-use-probe.js +126 -0
- package/dist/runner/mar-agent/computer-use.js +50 -20
- package/dist/runner/mar-agent/conversation-normalizer.js +28 -0
- package/dist/runner/mar-agent/marctl-skill.js +3 -2
- package/dist/runner/mar-agent-runner.js +32 -0
- package/dist/runtime-state.js +15 -10
- package/dist/service/continuous-run-coordinator.js +58 -5
- 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/hosted-conversation-node-service.js +5 -4
- 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/scheduled-session-coordinator.js +13 -0
- package/dist/service/scheduled-session-store.js +24 -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 +80 -15
- 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 +15 -8
- 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,10 @@ export class NodeConnector {
|
|
|
1435
1476
|
onConnected: () => {
|
|
1436
1477
|
this.runtimeCredentials.connected();
|
|
1437
1478
|
this.scheduledRunAuthorization.connected();
|
|
1479
|
+
this.scheduledSessions.connected();
|
|
1480
|
+
this.restartAuthorization.connected();
|
|
1481
|
+
if (!this.stopped)
|
|
1482
|
+
void this.restartHandoff.recover();
|
|
1438
1483
|
this.mailNotificationControl.connected();
|
|
1439
1484
|
this.recentRunProjectionService.publishSnapshot();
|
|
1440
1485
|
}
|
|
@@ -1444,10 +1489,13 @@ export class NodeConnector {
|
|
|
1444
1489
|
throw new Error('NODE_DATABASE_UNAVAILABLE');
|
|
1445
1490
|
return this.database;
|
|
1446
1491
|
};
|
|
1447
|
-
this.scheduledRunAuthorization = new
|
|
1492
|
+
this.scheduledRunAuthorization = new RunAuthorizationControl((envelope) => this.controlChannel.sendEnvelope(envelope));
|
|
1493
|
+
this.restartAuthorization = new RunAuthorizationControl((envelope) => this.controlChannel.sendEnvelope(envelope), 'restart');
|
|
1448
1494
|
this.scheduledSessions = new ScheduledSessionCoordinator({
|
|
1449
1495
|
store: () => new ScheduledSessionStore(requireDatabase().raw),
|
|
1450
|
-
busy: (task) => this.
|
|
1496
|
+
busy: (task) => this.stopped ||
|
|
1497
|
+
this.restartHandoff.blocks(task.sessionId) ||
|
|
1498
|
+
this.runtime.activeSessionRun(task.sessionId) !== undefined ||
|
|
1451
1499
|
this.runtime.sessionQueue(task.sessionId).itemIds.length > 0,
|
|
1452
1500
|
authorize: async (task, occurrenceId) => {
|
|
1453
1501
|
const session = this.runtime.getAgentSession(task.sessionId) ??
|
|
@@ -1800,6 +1848,8 @@ export class NodeConnector {
|
|
|
1800
1848
|
await this.stopping;
|
|
1801
1849
|
this.stopping = undefined;
|
|
1802
1850
|
this.stopped = false;
|
|
1851
|
+
this.continuousRunCoordinator.resumeScheduling();
|
|
1852
|
+
this.sessionMessageService.setAccepting(true);
|
|
1803
1853
|
this.config ??= await loadNodeConfig(this.configPath);
|
|
1804
1854
|
this.promptCompletion.initializeJournal(nodeDataDirectory(this.config, this.configPath));
|
|
1805
1855
|
await this.upgradeCoordinator.loadResult(resolve(dirname(nodeDatabasePath(this.config, this.configPath)), 'upgrade-result.json'));
|
|
@@ -1816,6 +1866,8 @@ export class NodeConnector {
|
|
|
1816
1866
|
});
|
|
1817
1867
|
this.usageOutbox.recoverInterrupted();
|
|
1818
1868
|
this.database.closeRunningTerminalSessions();
|
|
1869
|
+
await this.restartHandoff.load();
|
|
1870
|
+
this.scheduledSessions.recoverInterrupted();
|
|
1819
1871
|
this.scheduledSessions.start();
|
|
1820
1872
|
await this.initializeConversationHosting();
|
|
1821
1873
|
this.computerUseEnablement.initialize();
|
|
@@ -1838,6 +1890,7 @@ export class NodeConnector {
|
|
|
1838
1890
|
const upload = await this.workspaceUploadService.createDirect(workspace, {
|
|
1839
1891
|
path: descriptor.resource.path,
|
|
1840
1892
|
size: descriptor.size,
|
|
1893
|
+
sha256: descriptor.sha256,
|
|
1841
1894
|
overwrite: descriptor.overwrite === true,
|
|
1842
1895
|
...(descriptor.composerAttachment === undefined
|
|
1843
1896
|
? {}
|
|
@@ -1846,7 +1899,7 @@ export class NodeConnector {
|
|
|
1846
1899
|
return {
|
|
1847
1900
|
offset: upload.receivedBytes,
|
|
1848
1901
|
write: (bytes) => this.workspaceUploadService.writeDirect(upload.uploadId, bytes),
|
|
1849
|
-
complete: (size) => this.workspaceUploadService.completeDirect(upload.uploadId, size),
|
|
1902
|
+
complete: (size, signal) => this.workspaceUploadService.completeDirect(upload.uploadId, size, signal),
|
|
1850
1903
|
cancel: () => this.workspaceUploadService.cancel(upload.uploadId)
|
|
1851
1904
|
};
|
|
1852
1905
|
},
|
|
@@ -2141,10 +2194,32 @@ export class NodeConnector {
|
|
|
2141
2194
|
closeInput: '/goal --clear'
|
|
2142
2195
|
};
|
|
2143
2196
|
}
|
|
2144
|
-
stop() {
|
|
2197
|
+
stop(options = {}) {
|
|
2145
2198
|
if (this.stopping !== undefined)
|
|
2146
2199
|
return this.stopping;
|
|
2147
2200
|
this.stopped = true;
|
|
2201
|
+
this.continuousRunCoordinator.suspend();
|
|
2202
|
+
this.scheduledSessions.stop();
|
|
2203
|
+
this.stopping = (async () => {
|
|
2204
|
+
let snapshot;
|
|
2205
|
+
try {
|
|
2206
|
+
// In-flight recovery must rebuild accepted inputs before preparation closes.
|
|
2207
|
+
await this.restartHandoff.wait();
|
|
2208
|
+
this.sessionMessageService.setAccepting(false);
|
|
2209
|
+
await Promise.allSettled([...this.inFlightOperations]);
|
|
2210
|
+
await this.sessionMessageService.drain();
|
|
2211
|
+
if (options.handoff && this.config?.nodeId)
|
|
2212
|
+
snapshot = this.restartHandoff.capture();
|
|
2213
|
+
}
|
|
2214
|
+
finally {
|
|
2215
|
+
await this.disposeRuntime();
|
|
2216
|
+
}
|
|
2217
|
+
if (snapshot !== undefined)
|
|
2218
|
+
await this.restartHandoff.publish(snapshot);
|
|
2219
|
+
})();
|
|
2220
|
+
return this.stopping;
|
|
2221
|
+
}
|
|
2222
|
+
disposeRuntime() {
|
|
2148
2223
|
this.recentRunProjectionService.dispose();
|
|
2149
2224
|
this.upgradeCoordinator.stop();
|
|
2150
2225
|
this.connectionLifecycle.stop();
|
|
@@ -2202,6 +2277,7 @@ export class NodeConnector {
|
|
|
2202
2277
|
this.chatgptLogins.close();
|
|
2203
2278
|
this.scheduledSessions.stop();
|
|
2204
2279
|
this.scheduledRunAuthorization.close();
|
|
2280
|
+
this.restartAuthorization.close();
|
|
2205
2281
|
this.mailNotificationControl.close();
|
|
2206
2282
|
const mailMcpDrain = this.mailNotificationMcpServer.close();
|
|
2207
2283
|
const webToolDrain = this.webToolService.close();
|
|
@@ -2220,21 +2296,24 @@ export class NodeConnector {
|
|
|
2220
2296
|
mailMcpDrain,
|
|
2221
2297
|
webToolDrain
|
|
2222
2298
|
]);
|
|
2223
|
-
|
|
2299
|
+
return (async () => {
|
|
2300
|
+
let failed;
|
|
2224
2301
|
try {
|
|
2225
2302
|
await conversationHostingMaintenanceDrain;
|
|
2226
2303
|
}
|
|
2227
2304
|
finally {
|
|
2228
2305
|
await hostedConversationRuntime?.dispose().catch(() => undefined);
|
|
2229
|
-
await drains;
|
|
2306
|
+
const results = await drains;
|
|
2230
2307
|
await this.gitExecutionService?.close();
|
|
2231
2308
|
this.usageOutbox?.disconnected();
|
|
2232
2309
|
this.usageOutbox = undefined;
|
|
2233
2310
|
this.modelUsage.clear();
|
|
2234
2311
|
database?.close();
|
|
2312
|
+
failed = results.find((result) => result.status === 'rejected');
|
|
2235
2313
|
}
|
|
2314
|
+
if (failed !== undefined)
|
|
2315
|
+
throw failed.reason;
|
|
2236
2316
|
})();
|
|
2237
|
-
return this.stopping;
|
|
2238
2317
|
}
|
|
2239
2318
|
/** @internal Thin compatibility delegates; all behavior remains in the extracted services. */
|
|
2240
2319
|
handleMessage = (raw) => this.controlMessages.handle(raw);
|
|
@@ -2375,12 +2454,30 @@ export class NodeConnector {
|
|
|
2375
2454
|
startCapabilityRefresh = () => this.connectionLifecycle.startCapabilityRefresh();
|
|
2376
2455
|
refreshCapabilities = () => this.connectionLifecycle.refreshCapabilities();
|
|
2377
2456
|
async executeNodeOperation(operation, data) {
|
|
2457
|
+
if (this.stopped)
|
|
2458
|
+
throw new Error('NODE_STOPPED');
|
|
2459
|
+
const input = isPlainRecord(data) ? data : {};
|
|
2460
|
+
const sessionId = typeof input.sessionId === 'string' ? input.sessionId : undefined;
|
|
2461
|
+
if (this.restartHandoff.blocksOperation(operation, input))
|
|
2462
|
+
throw new Error('SESSION_RESTART_PENDING');
|
|
2378
2463
|
if (this.database === undefined)
|
|
2379
2464
|
throw new Error('NODE_DATABASE_UNAVAILABLE');
|
|
2380
2465
|
if (this.upgradeCoordinator.blocks(operation))
|
|
2381
2466
|
throw new Error('NODE_UPGRADE_PENDING');
|
|
2382
2467
|
nodeLog('node.operation', { nodeId: this.config?.nodeId, operation });
|
|
2383
|
-
|
|
2468
|
+
const task = this.gitOperationService.execute(operation, data, (input) => this.operationRouter.execute(operation, input));
|
|
2469
|
+
this.inFlightOperations.add(task);
|
|
2470
|
+
try {
|
|
2471
|
+
const result = await task;
|
|
2472
|
+
if ((operation === 'session.command.execute' || operation === 'session.message') &&
|
|
2473
|
+
sessionId !== undefined &&
|
|
2474
|
+
Number.isSafeInteger(input.__requestUserId))
|
|
2475
|
+
this.restartHandoff.rememberUser(sessionId, input.__requestUserId);
|
|
2476
|
+
return result;
|
|
2477
|
+
}
|
|
2478
|
+
finally {
|
|
2479
|
+
this.inFlightOperations.delete(task);
|
|
2480
|
+
}
|
|
2384
2481
|
}
|
|
2385
2482
|
gitExecution() {
|
|
2386
2483
|
this.gitExecutionService ??= new GitExecutionService({
|
package/dist/database.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
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';
|
|
4
|
+
import { NODE_SCHEMA_MIGRATION_V20 } from './migrations/v020.js';
|
|
3
5
|
import { normalizeSessionPrompt } from './util/personal-instructions.js';
|
|
4
6
|
import { randomUUID } from 'node:crypto';
|
|
5
7
|
import { closeSync, existsSync, mkdirSync, openSync, rmSync } from 'node:fs';
|
|
@@ -19,7 +21,9 @@ export const NODE_SCHEMA_MIGRATIONS = [
|
|
|
19
21
|
NODE_SCHEMA_MIGRATION_V15,
|
|
20
22
|
NODE_SCHEMA_MIGRATION_V16,
|
|
21
23
|
NODE_SCHEMA_MIGRATION_V17,
|
|
22
|
-
NODE_SCHEMA_MIGRATION_V18
|
|
24
|
+
NODE_SCHEMA_MIGRATION_V18,
|
|
25
|
+
NODE_SCHEMA_MIGRATION_V19,
|
|
26
|
+
NODE_SCHEMA_MIGRATION_V20
|
|
23
27
|
];
|
|
24
28
|
const LEGACY_NODE_SCHEMA_V12 = NODE_SCHEMA_VERSION_V13 - 1;
|
|
25
29
|
const OBSOLETE_NODE_SCHEMA_V16 = 16;
|
|
@@ -288,8 +292,10 @@ export class NodeDatabase {
|
|
|
288
292
|
const model = input.model === undefined ? (current?.model ?? null) : input.model;
|
|
289
293
|
const effort = input.effort === undefined ? (current?.effort ?? null) : input.effort;
|
|
290
294
|
const access = input.access === undefined ? (current?.access ?? null) : input.access;
|
|
295
|
+
const parentSessionId = current?.parentSessionId ?? input.parentSessionId ?? null;
|
|
291
296
|
const metadata = JSON.stringify({
|
|
292
297
|
version: 1,
|
|
298
|
+
parentSessionId,
|
|
293
299
|
customTitle,
|
|
294
300
|
sessionPrompt,
|
|
295
301
|
runnerTitle,
|
|
@@ -328,6 +334,7 @@ export class NodeDatabase {
|
|
|
328
334
|
}
|
|
329
335
|
return {
|
|
330
336
|
customTitle,
|
|
337
|
+
parentSessionId,
|
|
331
338
|
sessionPrompt,
|
|
332
339
|
runnerTitle,
|
|
333
340
|
runnerTitlePending,
|
|
@@ -341,17 +348,13 @@ export class NodeDatabase {
|
|
|
341
348
|
listSessionRecords(workspaceId) {
|
|
342
349
|
return this.raw
|
|
343
350
|
.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
|
-
}));
|
|
351
|
+
.all(workspaceId).map(sessionRecord);
|
|
352
|
+
}
|
|
353
|
+
getSessionRecord(sessionId) {
|
|
354
|
+
const row = this.raw
|
|
355
|
+
.prepare('SELECT id, workspace_id, runner, pin_order, metadata FROM agent_sessions WHERE id = ?')
|
|
356
|
+
.get(sessionId);
|
|
357
|
+
return row === undefined ? undefined : sessionRecord(row);
|
|
355
358
|
}
|
|
356
359
|
movePinnedSession(sessionId, direction) {
|
|
357
360
|
const current = this.raw
|
|
@@ -565,6 +568,19 @@ function mcpInstallation(row) {
|
|
|
565
568
|
installedAt: numberValue(row, 'installed_at')
|
|
566
569
|
};
|
|
567
570
|
}
|
|
571
|
+
function sessionRecord(row) {
|
|
572
|
+
return {
|
|
573
|
+
id: stringValue(row, 'id'),
|
|
574
|
+
workspaceId: stringValue(row, 'workspace_id'),
|
|
575
|
+
runner: stringValue(row, 'runner'),
|
|
576
|
+
pinOrder: nullableNumber(row, 'pin_order'),
|
|
577
|
+
metadata: {
|
|
578
|
+
...sessionMetadata(row),
|
|
579
|
+
pinnedAt: row['pin_order'] === null ? null : 1,
|
|
580
|
+
pinOrder: nullableNumber(row, 'pin_order')
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
}
|
|
568
584
|
function sessionMetadata(row) {
|
|
569
585
|
const raw = JSON.parse(stringValue(row, 'metadata'));
|
|
570
586
|
if (raw['version'] !== 1)
|
|
@@ -572,6 +588,7 @@ function sessionMetadata(row) {
|
|
|
572
588
|
const value = (key) => typeof raw[key] === 'string' ? raw[key] : null;
|
|
573
589
|
return {
|
|
574
590
|
customTitle: value('customTitle'),
|
|
591
|
+
parentSessionId: value('parentSessionId'),
|
|
575
592
|
sessionPrompt: value('sessionPrompt'),
|
|
576
593
|
runnerTitle: value('runnerTitle'),
|
|
577
594
|
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
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { NODE_SCHEMA_V13 } from './v013.js';
|
|
3
|
+
import { NODE_SCHEMA_MIGRATION_V14 } from './v014.js';
|
|
4
|
+
import { NODE_SCHEMA_MIGRATION_V15 } from './v015.js';
|
|
5
|
+
import { NODE_SCHEMA_MIGRATION_V16 } from './v016.js';
|
|
6
|
+
import { NODE_SCHEMA_MIGRATION_V17 } from './v017.js';
|
|
7
|
+
import { NODE_SCHEMA_MIGRATION_V18 } from './v018.js';
|
|
8
|
+
import { NODE_SCHEMA_MIGRATION_V19 } from './v019.js';
|
|
9
|
+
const sql = `CREATE INDEX scheduled_session_occurrences_unfinished
|
|
10
|
+
ON scheduled_session_occurrences(status) WHERE status IN ('CLAIMED','SUBMITTED');`;
|
|
11
|
+
export const NODE_SCHEMA_MIGRATION_V20 = {
|
|
12
|
+
version: 20,
|
|
13
|
+
sql,
|
|
14
|
+
preflight: (database) => NODE_SCHEMA_MIGRATION_V19.assert(database),
|
|
15
|
+
assert(database) {
|
|
16
|
+
const reference = new DatabaseSync(':memory:');
|
|
17
|
+
try {
|
|
18
|
+
reference.exec(NODE_SCHEMA_V13);
|
|
19
|
+
for (const migration of [
|
|
20
|
+
NODE_SCHEMA_MIGRATION_V14,
|
|
21
|
+
NODE_SCHEMA_MIGRATION_V15,
|
|
22
|
+
NODE_SCHEMA_MIGRATION_V16,
|
|
23
|
+
NODE_SCHEMA_MIGRATION_V17,
|
|
24
|
+
NODE_SCHEMA_MIGRATION_V18,
|
|
25
|
+
NODE_SCHEMA_MIGRATION_V19
|
|
26
|
+
])
|
|
27
|
+
reference.exec(migration.sql);
|
|
28
|
+
reference.exec(sql);
|
|
29
|
+
const schema = (db) => db
|
|
30
|
+
.prepare("SELECT type,name,sql FROM sqlite_master WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%' ORDER BY type,name")
|
|
31
|
+
.all()
|
|
32
|
+
.map((row) => [
|
|
33
|
+
row.type,
|
|
34
|
+
row.name,
|
|
35
|
+
row.name === 'node_schema_migrations'
|
|
36
|
+
? String(row.sql).replace(/\s+/g, '')
|
|
37
|
+
: String(row.sql).replace(/\s+/g, ' ').trim()
|
|
38
|
+
]);
|
|
39
|
+
if (JSON.stringify(schema(reference)) !== JSON.stringify(schema(database)))
|
|
40
|
+
throw new Error('NODE_DATABASE_SCHEMA_INVALID:scheduled_session_occurrences_unfinished');
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
reference.close();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/** Read-only platform evidence; never inject input or replace clipboard contents. */
|
|
2
|
+
export const PYTHON_PROBE_SOURCE = String.raw `
|
|
3
|
+
import ctypes
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
result = dict(supportsScreenshot=False, supportsPrimaryDisplay=False,
|
|
11
|
+
supportsInput=False, supportsUnicodePaste=False,
|
|
12
|
+
reasonCode="COMPUTER_RUNTIME_UNAVAILABLE")
|
|
13
|
+
|
|
14
|
+
def signature(library, name, arguments, returns):
|
|
15
|
+
function = getattr(library, name)
|
|
16
|
+
function.argtypes = arguments
|
|
17
|
+
function.restype = returns
|
|
18
|
+
return function
|
|
19
|
+
|
|
20
|
+
def windows_desktop():
|
|
21
|
+
user = ctypes.WinDLL("user32", use_last_error=True)
|
|
22
|
+
kernel = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
23
|
+
handle, integer = ctypes.c_void_p, ctypes.c_uint32
|
|
24
|
+
open_desktop = signature(user, "OpenInputDesktop", [integer, ctypes.c_int, integer], handle)
|
|
25
|
+
close_desktop = signature(user, "CloseDesktop", [handle], ctypes.c_int)
|
|
26
|
+
thread_id = signature(kernel, "GetCurrentThreadId", [], integer)
|
|
27
|
+
thread_desktop = signature(user, "GetThreadDesktop", [integer], handle)
|
|
28
|
+
station = signature(user, "GetProcessWindowStation", [], handle)
|
|
29
|
+
information = signature(user, "GetUserObjectInformationW",
|
|
30
|
+
[handle, ctypes.c_int, handle, integer, handle], ctypes.c_int)
|
|
31
|
+
def name(object_handle):
|
|
32
|
+
buffer = ctypes.create_unicode_buffer(256)
|
|
33
|
+
needed = integer()
|
|
34
|
+
if not information(object_handle, 2, buffer, ctypes.sizeof(buffer), ctypes.byref(needed)):
|
|
35
|
+
return None
|
|
36
|
+
return buffer.value
|
|
37
|
+
# DESKTOP_READOBJECTS | DESKTOP_WRITEOBJECTS. Do not switch or attach desktops.
|
|
38
|
+
desktop = open_desktop(0, False, 0x0001 | 0x0080)
|
|
39
|
+
if not desktop:
|
|
40
|
+
return False
|
|
41
|
+
try:
|
|
42
|
+
desktop_name = name(desktop)
|
|
43
|
+
return (name(station()) == "WinSta0" and desktop_name is not None
|
|
44
|
+
and desktop_name == name(thread_desktop(thread_id())))
|
|
45
|
+
finally:
|
|
46
|
+
close_desktop(desktop)
|
|
47
|
+
|
|
48
|
+
def input_supported(gui):
|
|
49
|
+
if sys.platform == "win32":
|
|
50
|
+
return windows_desktop()
|
|
51
|
+
if sys.platform == "darwin":
|
|
52
|
+
services = ctypes.CDLL("/System/Library/Frameworks/ApplicationServices.framework/ApplicationServices")
|
|
53
|
+
graphics = ctypes.CDLL("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics")
|
|
54
|
+
trusted = signature(services, "AXIsProcessTrusted", [], ctypes.c_bool)
|
|
55
|
+
post_access = signature(graphics, "CGPreflightPostEventAccess", [], ctypes.c_bool)
|
|
56
|
+
return bool(trusted() and post_access())
|
|
57
|
+
if sys.platform == "linux":
|
|
58
|
+
display = gui._pyautogui_x11._display
|
|
59
|
+
if not display.has_extension("XTEST"):
|
|
60
|
+
return False
|
|
61
|
+
display.xtest_get_version(2, 2)
|
|
62
|
+
return True
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
def clipboard_supported():
|
|
66
|
+
import pyperclip
|
|
67
|
+
copy, paste = pyperclip.determine_clipboard()
|
|
68
|
+
if not callable(copy) or not callable(paste) or not copy or not paste:
|
|
69
|
+
return False
|
|
70
|
+
names = (getattr(copy, "__name__", ""), getattr(paste, "__name__", ""))
|
|
71
|
+
if sys.platform == "win32" and names == ("copy_windows", "paste_windows"):
|
|
72
|
+
user = ctypes.WinDLL("user32", use_last_error=True)
|
|
73
|
+
open_clipboard = signature(user, "OpenClipboard", [ctypes.c_void_p], ctypes.c_int)
|
|
74
|
+
close_clipboard = signature(user, "CloseClipboard", [], ctypes.c_int)
|
|
75
|
+
if not open_clipboard(None):
|
|
76
|
+
return False
|
|
77
|
+
# Opening and closing does not empty, read, or take ownership of the clipboard.
|
|
78
|
+
return bool(close_clipboard())
|
|
79
|
+
if sys.platform == "darwin":
|
|
80
|
+
import AppKit
|
|
81
|
+
board = AppKit.NSPasteboard.generalPasteboard()
|
|
82
|
+
if board is None:
|
|
83
|
+
return False
|
|
84
|
+
# Newer systems expose pasteboard policy. Do not provoke an access prompt.
|
|
85
|
+
if hasattr(board, "accessBehavior"):
|
|
86
|
+
allowed = getattr(AppKit, "NSPasteboardAccessBehaviorAlwaysAllow", None)
|
|
87
|
+
if allowed is None or board.accessBehavior() != allowed:
|
|
88
|
+
return False
|
|
89
|
+
if names == ("copy_osx_pyobjc", "paste_osx_pyobjc"):
|
|
90
|
+
return True
|
|
91
|
+
return (names == ("copy_osx_pbcopy", "paste_osx_pbcopy")
|
|
92
|
+
and shutil.which("pbcopy") is not None and shutil.which("pbpaste") is not None)
|
|
93
|
+
if sys.platform == "linux":
|
|
94
|
+
commands = {("copy_xclip", "paste_xclip"): ["xclip", "-version"],
|
|
95
|
+
("copy_xsel", "paste_xsel"): ["xsel", "--version"]}
|
|
96
|
+
command = commands.get(names)
|
|
97
|
+
executable = shutil.which(command[0]) if command is not None else None
|
|
98
|
+
if executable is None:
|
|
99
|
+
return False
|
|
100
|
+
# X11 access was established above. Verify the selected backend can run,
|
|
101
|
+
# without consuming a one-shot selection or requesting clipboard contents.
|
|
102
|
+
subprocess.run([executable, *command[1:]], stdin=subprocess.DEVNULL,
|
|
103
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True, timeout=2)
|
|
104
|
+
return True
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
if sys.platform not in ("win32", "darwin", "linux") or (sys.platform == "linux" and
|
|
109
|
+
(not os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY"))):
|
|
110
|
+
result["reasonCode"] = "COMPUTER_DESKTOP_UNSUPPORTED"
|
|
111
|
+
else:
|
|
112
|
+
import pyautogui
|
|
113
|
+
image = pyautogui.screenshot()
|
|
114
|
+
if image.width > 0 and image.height > 0:
|
|
115
|
+
result["supportsScreenshot"] = result["supportsPrimaryDisplay"] = True
|
|
116
|
+
result["reasonCode"] = "COMPUTER_INPUT_PERMISSION_DENIED"
|
|
117
|
+
if input_supported(pyautogui):
|
|
118
|
+
result["supportsInput"] = True
|
|
119
|
+
result["reasonCode"] = "COMPUTER_RUNTIME_UNAVAILABLE"
|
|
120
|
+
if clipboard_supported():
|
|
121
|
+
result["supportsUnicodePaste"] = True
|
|
122
|
+
result["reasonCode"] = None
|
|
123
|
+
except Exception:
|
|
124
|
+
pass
|
|
125
|
+
print(json.dumps(result))
|
|
126
|
+
`;
|