@myagentroam/node 0.9.124 → 0.9.126
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/codex-app-server.js +30 -0
- package/dist/connector.js +217 -6
- package/dist/database.js +2 -1
- package/dist/migrations/v014.js +107 -0
- package/dist/runner/abstract-runner.js +11 -5
- package/dist/runner/claude-code-runner.js +3 -0
- package/dist/runner/codex/conversation-parser.js +142 -2
- package/dist/runner/codex/managed-run-controller.js +26 -18
- package/dist/runner/codex-runner.js +135 -72
- package/dist/runner/opencode-runner.js +14 -12
- package/dist/runtime-state.js +6 -1
- package/dist/service/continuous-run-coordinator.js +8 -2
- package/dist/service/conversation-history-service.js +14 -8
- package/dist/service/managed-runner-session-service.js +2 -1
- package/dist/service/native-session-projection-service.js +3 -1
- package/dist/service/node-control-message-service.js +2 -0
- package/dist/service/run-event-service.js +18 -2
- package/dist/service/runner-interaction-service.js +9 -8
- package/dist/service/scheduled-run-authorization-control.js +72 -0
- package/dist/service/scheduled-session-coordinator.js +95 -0
- package/dist/service/scheduled-session-recurrence.js +72 -0
- package/dist/service/scheduled-session-store.js +234 -0
- package/dist/service/scheduled-task-mcp-server.js +179 -0
- package/dist/service/scheduled-task-mcp-stdio.js +63 -0
- package/dist/service/session-catalog-service.js +3 -1
- package/dist/service/session-lifecycle-service.js +6 -0
- package/dist/service/session-message-service.js +25 -2
- package/dist/service/session-presentation-service.js +19 -2
- package/dist/service/session-schedule-service.js +153 -0
- package/dist/util/runner-native-session-parsers.js +19 -11
- package/package.json +4 -4
package/dist/codex-app-server.js
CHANGED
|
@@ -139,6 +139,36 @@ export class CodexAppServerClient {
|
|
|
139
139
|
this.lifecycleRevision += 1;
|
|
140
140
|
this.stopManaged();
|
|
141
141
|
}
|
|
142
|
+
async stopAndWait() {
|
|
143
|
+
const child = this.child;
|
|
144
|
+
if (child === undefined) {
|
|
145
|
+
this.stop();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const exited = new Promise((resolve, reject) => {
|
|
149
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
150
|
+
resolve();
|
|
151
|
+
else {
|
|
152
|
+
let forcedTimeout;
|
|
153
|
+
const onExit = () => {
|
|
154
|
+
clearTimeout(timeout);
|
|
155
|
+
if (forcedTimeout !== undefined)
|
|
156
|
+
clearTimeout(forcedTimeout);
|
|
157
|
+
resolve();
|
|
158
|
+
};
|
|
159
|
+
const timeout = setTimeout(() => {
|
|
160
|
+
child.kill('SIGKILL');
|
|
161
|
+
forcedTimeout = setTimeout(() => {
|
|
162
|
+
child.off('exit', onExit);
|
|
163
|
+
reject(new Error('CODEX_APP_SERVER_STOP_TIMEOUT'));
|
|
164
|
+
}, 2_000);
|
|
165
|
+
}, 2_000);
|
|
166
|
+
child.once('exit', onExit);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
this.stop();
|
|
170
|
+
await exited;
|
|
171
|
+
}
|
|
142
172
|
stopManaged() {
|
|
143
173
|
this.transport?.destroy();
|
|
144
174
|
this.transport = undefined;
|
package/dist/connector.js
CHANGED
|
@@ -61,6 +61,11 @@ import { RunnerInteractionService } from './service/runner-interaction-service.j
|
|
|
61
61
|
import { SessionLifecycleService } from './service/session-lifecycle-service.js';
|
|
62
62
|
import { ExternalSessionResumeService } from './service/external-session-resume-service.js';
|
|
63
63
|
import { SessionMessageService } from './service/session-message-service.js';
|
|
64
|
+
import { SessionScheduleService } from './service/session-schedule-service.js';
|
|
65
|
+
import { ScheduledSessionCoordinator } from './service/scheduled-session-coordinator.js';
|
|
66
|
+
import { ScheduledSessionStore } from './service/scheduled-session-store.js';
|
|
67
|
+
import { ScheduledRunAuthorizationControl } from './service/scheduled-run-authorization-control.js';
|
|
68
|
+
import { ScheduledTaskMcpServer } from './service/scheduled-task-mcp-server.js';
|
|
64
69
|
import { SessionCatalogService } from './service/session-catalog-service.js';
|
|
65
70
|
import { SessionCommandService } from './service/session-command-service.js';
|
|
66
71
|
import { SessionPresentationService } from './service/session-presentation-service.js';
|
|
@@ -151,6 +156,8 @@ export class NodeConnector {
|
|
|
151
156
|
nodeGeneration = crypto.randomUUID().replace(/-/g, '');
|
|
152
157
|
controlChannel;
|
|
153
158
|
controlMessages;
|
|
159
|
+
scheduledRunAuthorization;
|
|
160
|
+
scheduledSessions;
|
|
154
161
|
nodeRequests;
|
|
155
162
|
connectionLifecycle;
|
|
156
163
|
runtimeCredentials = new RuntimeCredentialControlService({
|
|
@@ -253,6 +260,7 @@ export class NodeConnector {
|
|
|
253
260
|
goalSubmissionAttempts = new Map();
|
|
254
261
|
goalContinuationEpochs = new Map();
|
|
255
262
|
goalExecutionProgress = new Set();
|
|
263
|
+
sessionScheduleService;
|
|
256
264
|
goalMcpServer = new GoalMcpServer(this.goalRuntime, (sessionId) => {
|
|
257
265
|
const goal = this.goalRuntime.get(sessionId);
|
|
258
266
|
const session = this.runtime.getAgentSession(sessionId);
|
|
@@ -264,6 +272,22 @@ export class NodeConnector {
|
|
|
264
272
|
}
|
|
265
273
|
this.publishGoalState(session, goal);
|
|
266
274
|
});
|
|
275
|
+
scheduledTaskMcpServer = new ScheduledTaskMcpServer(() => {
|
|
276
|
+
if (this.database === undefined)
|
|
277
|
+
throw new Error('NODE_DATABASE_UNAVAILABLE');
|
|
278
|
+
return new ScheduledSessionStore(this.database.raw);
|
|
279
|
+
}, (sessionId) => {
|
|
280
|
+
const session = this.runtime.getAgentSession(sessionId);
|
|
281
|
+
if (session !== undefined)
|
|
282
|
+
this.emitSessionWorkbenchEvent({
|
|
283
|
+
session: this.sessionPresentationService.present(session)
|
|
284
|
+
});
|
|
285
|
+
this.scheduledSessions.changed();
|
|
286
|
+
}, (sessionId, proposalId) => this.emitScheduledProposalNotification(sessionId, proposalId), (input) => {
|
|
287
|
+
if (this.sessionScheduleService === undefined)
|
|
288
|
+
throw new Error('SCHEDULE_SERVICE_UNAVAILABLE');
|
|
289
|
+
return this.sessionScheduleService.createFromMcp(input);
|
|
290
|
+
}, (sessionId) => this.runtime.getAgentSession(sessionId)?.externalSessionId ?? sessionId);
|
|
267
291
|
runnerUsageReader;
|
|
268
292
|
terminalManager;
|
|
269
293
|
upgradeCoordinator;
|
|
@@ -314,6 +338,17 @@ export class NodeConnector {
|
|
|
314
338
|
expireImages: (runId) => this.runAttachmentService.expire(runId),
|
|
315
339
|
scheduleContextRefresh: (sessionId) => this.sessionContextService.schedule(sessionId),
|
|
316
340
|
refreshWorkspaceSessions: (workspaceId) => this.workspaceWorkbenchService.ensureTopicSnapshot(workspaceId, 'sessions'),
|
|
341
|
+
onTerminalRun: (run, status) => {
|
|
342
|
+
if (run.source !== 'SCHEDULED' || this.database === undefined)
|
|
343
|
+
return;
|
|
344
|
+
const store = new ScheduledSessionStore(this.database.raw);
|
|
345
|
+
store.finish(run.clientMessageId, status === 'SUCCEEDED' ? 'SUCCEEDED' : 'FAILED', status === 'SUCCEEDED' ? undefined : `SCHEDULE_RUN_${status}`, run.id);
|
|
346
|
+
if (status !== 'SUCCEEDED') {
|
|
347
|
+
const task = store.get(run.sessionId);
|
|
348
|
+
if (task !== undefined)
|
|
349
|
+
this.emitScheduledNotification(task, run.clientMessageId, `SCHEDULE_RUN_${status}`, 'FAILED');
|
|
350
|
+
}
|
|
351
|
+
},
|
|
317
352
|
dispatchQueue: async (sessionId, delayMs = 0) => {
|
|
318
353
|
if (this.upgradeCoordinator.pending())
|
|
319
354
|
return;
|
|
@@ -386,6 +421,7 @@ export class NodeConnector {
|
|
|
386
421
|
releaseExecution: (executionId) => {
|
|
387
422
|
this.goalExecutionProgress.delete(executionId);
|
|
388
423
|
this.goalMcpServer.revoke(executionId);
|
|
424
|
+
this.scheduledTaskMcpServer.revoke(executionId);
|
|
389
425
|
this.continuousRunCoordinator.remove(executionId);
|
|
390
426
|
}
|
|
391
427
|
});
|
|
@@ -463,7 +499,7 @@ export class NodeConnector {
|
|
|
463
499
|
options.fakeRunner === undefined ? undefined : new FakeTestRunner(options.fakeRunner);
|
|
464
500
|
this.fakeScript = options.fakeScript;
|
|
465
501
|
this.runAttachmentService = new RunAttachmentService((session) => this.runners.require(session.runner).nativeImageRoots(session));
|
|
466
|
-
this.codexClient = new CodexRunner(options.codexClient, options.codexProfileClientFactory);
|
|
502
|
+
this.codexClient = new CodexRunner(options.codexClient, options.codexProfileClientFactory, options.codexRunClientFactory);
|
|
467
503
|
this.codexRunController = new CodexManagedRunController(this.codexClient, {
|
|
468
504
|
available: () => this.capabilities.codex.available,
|
|
469
505
|
intercepted: () => this.fakeRunner !== undefined,
|
|
@@ -483,7 +519,7 @@ export class NodeConnector {
|
|
|
483
519
|
emit: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
|
|
484
520
|
cancel: (runId, threadId, turnId, cwd, status) => this.runnerInteractionService.cancelCodexTurn(runId, threadId, turnId, cwd, status),
|
|
485
521
|
createApproval: (input) => this.runtime.createApproval(input),
|
|
486
|
-
publishApproval: (input) => this.
|
|
522
|
+
publishApproval: (input) => this.publishApprovalForRun(input),
|
|
487
523
|
emitItem: (runId, item) => this.emitConversationItem(runId, item),
|
|
488
524
|
appendImages: (runId, images) => this.runAttachmentService.append(runId, images.filter(isRunnerImageAttachment)),
|
|
489
525
|
syncTitle: (sessionId, nativeSessionId, cwd) => this.managedRunnerSessions.refreshTitle(sessionId, nativeSessionId, cwd),
|
|
@@ -574,7 +610,7 @@ export class NodeConnector {
|
|
|
574
610
|
expireApproval: (approvalId) => {
|
|
575
611
|
this.runtime.expireApproval(approvalId);
|
|
576
612
|
},
|
|
577
|
-
publishApproval: (input) => this.
|
|
613
|
+
publishApproval: (input) => this.publishApprovalForRun(input),
|
|
578
614
|
commandState: (sessionId, commandId) => this.commandStates.get(sessionId, commandId),
|
|
579
615
|
setCommandState: (sessionId, state) => this.commandStates.set(sessionId, state),
|
|
580
616
|
clearCommandState: (sessionId, commandId) => this.commandStates.clear(sessionId, commandId),
|
|
@@ -622,7 +658,7 @@ export class NodeConnector {
|
|
|
622
658
|
emitRun: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
|
|
623
659
|
emitItem: (runId, item) => this.emitConversationItem(runId, item),
|
|
624
660
|
approvalTimeoutMs: this.claudeApprovalTimeoutMs,
|
|
625
|
-
publishApproval: (input) => this.
|
|
661
|
+
publishApproval: (input) => this.publishApprovalForRun(input)
|
|
626
662
|
});
|
|
627
663
|
this.runnerChannelMessages = new RunnerChannelMessageService({
|
|
628
664
|
interactions: this.runnerInteractionService,
|
|
@@ -763,6 +799,38 @@ export class NodeConnector {
|
|
|
763
799
|
},
|
|
764
800
|
secrets: resolved.secrets
|
|
765
801
|
};
|
|
802
|
+
},
|
|
803
|
+
scheduledMcp: async (input) => {
|
|
804
|
+
if (this.database === undefined)
|
|
805
|
+
return undefined;
|
|
806
|
+
const task = new ScheduledSessionStore(this.database.raw).get(input.sessionId);
|
|
807
|
+
const run = this.runtime.getRun(input.runId);
|
|
808
|
+
if (run?.initiatedByUserId === undefined)
|
|
809
|
+
return undefined;
|
|
810
|
+
const resolved = await this.scheduledTaskMcpServer.installation({
|
|
811
|
+
executionId: input.runId,
|
|
812
|
+
sessionId: input.sessionId,
|
|
813
|
+
workspaceId: input.workspaceId,
|
|
814
|
+
...(task === undefined ? {} : { taskId: task.id, revision: task.revision }),
|
|
815
|
+
actorUserId: run.initiatedByUserId,
|
|
816
|
+
readonly: run.source === 'SCHEDULED'
|
|
817
|
+
});
|
|
818
|
+
return {
|
|
819
|
+
installation: {
|
|
820
|
+
targetKind: 'WORKSPACE',
|
|
821
|
+
targetId: input.workspaceId,
|
|
822
|
+
catalogEntryId: 'mar-internal-schedule',
|
|
823
|
+
serverName: 'mar_schedule',
|
|
824
|
+
displayName: 'MAR scheduled task',
|
|
825
|
+
version: '1.0.0',
|
|
826
|
+
enabled: true,
|
|
827
|
+
runtime: { transport: 'HTTP', url: 'https://127.0.0.1.invalid/mcp', headers: {} },
|
|
828
|
+
configuration: resolved.installation,
|
|
829
|
+
installedAt: Date.now(),
|
|
830
|
+
...resolved.installation
|
|
831
|
+
},
|
|
832
|
+
secrets: resolved.secrets
|
|
833
|
+
};
|
|
766
834
|
}
|
|
767
835
|
});
|
|
768
836
|
this.workspaceQueueWorkbenchService = new WorkspaceQueueWorkbenchService({
|
|
@@ -1077,7 +1145,7 @@ export class NodeConnector {
|
|
|
1077
1145
|
runtime: this.runtime,
|
|
1078
1146
|
state: this.runState,
|
|
1079
1147
|
emitRun: (runId, type, payload, status) => this.runEventBridge.emitRun(runId, type, payload, status),
|
|
1080
|
-
publishApproval: (input) => this.
|
|
1148
|
+
publishApproval: (input) => this.publishApprovalForRun(input),
|
|
1081
1149
|
disconnect: () => this.controlChannel.close(4010, 'FAKE_RUNNER_DISCONNECT')
|
|
1082
1150
|
});
|
|
1083
1151
|
this.fakeRunner?.bindControl({
|
|
@@ -1122,6 +1190,7 @@ export class NodeConnector {
|
|
|
1122
1190
|
onClose: (code, reason) => {
|
|
1123
1191
|
this.directTransferService?.close();
|
|
1124
1192
|
this.runtimeCredentials.disconnected();
|
|
1193
|
+
this.scheduledRunAuthorization.disconnected();
|
|
1125
1194
|
this.terminalChannel.disconnect();
|
|
1126
1195
|
if (code === 4000 && reason === 'NODE_CONNECTION_REPLACED')
|
|
1127
1196
|
nodeLog('node.connection.replaced', {
|
|
@@ -1139,6 +1208,7 @@ export class NodeConnector {
|
|
|
1139
1208
|
inspectWorkspace: (envelope) => this.workspaceCoordinator.inspect(envelope),
|
|
1140
1209
|
handleNodeRequest: (envelope) => this.nodeRequests.handle(envelope),
|
|
1141
1210
|
handleRuntimeCredentialResponse: (envelope) => this.runtimeCredentials.handle(envelope),
|
|
1211
|
+
handleScheduledRunAuthorizeResponse: (envelope) => this.scheduledRunAuthorization.handle(envelope),
|
|
1142
1212
|
handleDirectTransfer: (envelope) => this.handleDirectTransfer(envelope),
|
|
1143
1213
|
handleTerminalDirect: (envelope) => this.handleTerminalDirect(envelope),
|
|
1144
1214
|
reconcileRuns: (envelope) => this.runEventBridge.reconcile(envelope),
|
|
@@ -1200,6 +1270,7 @@ export class NodeConnector {
|
|
|
1200
1270
|
},
|
|
1201
1271
|
onConnected: () => {
|
|
1202
1272
|
this.runtimeCredentials.connected();
|
|
1273
|
+
this.scheduledRunAuthorization.connected();
|
|
1203
1274
|
this.recentRunProjectionService.publishSnapshot();
|
|
1204
1275
|
}
|
|
1205
1276
|
});
|
|
@@ -1208,6 +1279,46 @@ export class NodeConnector {
|
|
|
1208
1279
|
throw new Error('NODE_DATABASE_UNAVAILABLE');
|
|
1209
1280
|
return this.database;
|
|
1210
1281
|
};
|
|
1282
|
+
this.scheduledRunAuthorization = new ScheduledRunAuthorizationControl((envelope) => this.controlChannel.sendEnvelope(envelope));
|
|
1283
|
+
this.scheduledSessions = new ScheduledSessionCoordinator({
|
|
1284
|
+
store: () => new ScheduledSessionStore(requireDatabase().raw),
|
|
1285
|
+
busy: (task) => this.runtime.activeSessionRun(task.sessionId) !== undefined ||
|
|
1286
|
+
this.runtime.sessionQueue(task.sessionId).itemIds.length > 0,
|
|
1287
|
+
authorize: async (task, occurrenceId) => {
|
|
1288
|
+
const session = this.runtime.getAgentSession(task.sessionId) ??
|
|
1289
|
+
(await this.nativeSessionProjectionService.resolveCurrent(task.sessionId));
|
|
1290
|
+
if (session === undefined ||
|
|
1291
|
+
session.workspaceId !== task.workspaceId ||
|
|
1292
|
+
session.runner !== task.runner)
|
|
1293
|
+
throw new Error('SCHEDULE_SESSION_NOT_FOUND');
|
|
1294
|
+
const configuration = this.nativeSessionProjectionService.resumedConfiguration(session);
|
|
1295
|
+
return this.scheduledRunAuthorization.authorize({
|
|
1296
|
+
taskId: task.id,
|
|
1297
|
+
revision: task.revision,
|
|
1298
|
+
occurrenceId,
|
|
1299
|
+
sessionId: task.sessionId,
|
|
1300
|
+
workspaceId: task.workspaceId,
|
|
1301
|
+
ownerUserId: task.ownerUserId,
|
|
1302
|
+
runner: task.runner,
|
|
1303
|
+
model: configuration.model
|
|
1304
|
+
});
|
|
1305
|
+
},
|
|
1306
|
+
submit: async (task, occurrenceId, context) => {
|
|
1307
|
+
const result = (await this.sessionMessageService.submitScheduled({
|
|
1308
|
+
sessionId: task.sessionId,
|
|
1309
|
+
occurrenceId,
|
|
1310
|
+
objective: task.objective,
|
|
1311
|
+
initiatedByUserId: task.ownerUserId,
|
|
1312
|
+
secretEnvironment: context.secretEnvironment,
|
|
1313
|
+
personalInstructions: context.personalInstructions,
|
|
1314
|
+
marAgentModel: context.marAgentModel,
|
|
1315
|
+
marAgentModels: context.marAgentModels,
|
|
1316
|
+
marAgentRuntimeScopeId: context.marAgentRuntimeScopeId
|
|
1317
|
+
}));
|
|
1318
|
+
return { runId: result.run.id };
|
|
1319
|
+
},
|
|
1320
|
+
notify: (task, occurrenceId, code, status) => this.emitScheduledNotification(task, occurrenceId, code, status)
|
|
1321
|
+
});
|
|
1211
1322
|
this.runnerService = new RunnerService(requireDatabase, () => this.capabilities, this.runners, this.runnerUsageReader);
|
|
1212
1323
|
this.workbenchManifestService = new WorkbenchManifestService({
|
|
1213
1324
|
capabilities: () => this.capabilities,
|
|
@@ -1273,6 +1384,23 @@ export class NodeConnector {
|
|
|
1273
1384
|
this.operationRouter.registerAll(this.nativeSessionWatchService.operations());
|
|
1274
1385
|
this.operationRouter.registerAll(this.sessionQueryService.operations());
|
|
1275
1386
|
this.operationRouter.registerAll(this.sessionLifecycleService.operations());
|
|
1387
|
+
this.sessionScheduleService = new SessionScheduleService({
|
|
1388
|
+
database: requireDatabase,
|
|
1389
|
+
runtime: this.runtime,
|
|
1390
|
+
projection: this.nativeSessionProjectionService,
|
|
1391
|
+
awaitNativeSessionId: (sessionId) => {
|
|
1392
|
+
const nativeId = this.runtime.getAgentSession(sessionId)?.externalSessionId;
|
|
1393
|
+
return nativeId === null || nativeId === undefined
|
|
1394
|
+
? this.sessionIdentityService.wait(sessionId)
|
|
1395
|
+
: Promise.resolve(nativeId);
|
|
1396
|
+
},
|
|
1397
|
+
present: (session) => this.sessionPresentationService.present(session),
|
|
1398
|
+
emitSession: (session) => this.emitSessionWorkbenchEvent({
|
|
1399
|
+
session: this.sessionPresentationService.present(session)
|
|
1400
|
+
}),
|
|
1401
|
+
changed: () => this.scheduledSessions.changed()
|
|
1402
|
+
});
|
|
1403
|
+
this.operationRouter.registerAll(this.sessionScheduleService.operations());
|
|
1276
1404
|
this.operationRouter.registerAll(this.sessionMessageService.operations());
|
|
1277
1405
|
this.operationRouter.registerAll(this.sessionCatalogService.operations());
|
|
1278
1406
|
this.operationRouter.registerAll(this.sessionCommandService.operations());
|
|
@@ -1453,6 +1581,7 @@ export class NodeConnector {
|
|
|
1453
1581
|
await assertLocalSqlitePath(nodeDatabasePath(this.config, this.configPath));
|
|
1454
1582
|
this.database ??= new NodeDatabase(nodeDatabasePath(this.config, this.configPath), () => this.config?.nodeId ?? 'unregistered');
|
|
1455
1583
|
this.database.closeRunningTerminalSessions();
|
|
1584
|
+
this.scheduledSessions.start();
|
|
1456
1585
|
await this.initializeConversationHosting();
|
|
1457
1586
|
this.computerUseEnablement.initialize();
|
|
1458
1587
|
this.directTransferService ??= this.createDirectTransferService();
|
|
@@ -1786,11 +1915,14 @@ export class NodeConnector {
|
|
|
1786
1915
|
this.workspaceChangeService.clear();
|
|
1787
1916
|
const runnerDrain = this.runners.stop();
|
|
1788
1917
|
const goalMcpDrain = this.goalMcpServer.close();
|
|
1918
|
+
const scheduledMcpDrain = this.scheduledTaskMcpServer.close();
|
|
1789
1919
|
// close() transitions metadata before awaiting process-tree termination,
|
|
1790
1920
|
// so the lifecycle is persisted before this synchronous shutdown returns.
|
|
1791
1921
|
const terminalDrain = this.terminalManager.shutdown();
|
|
1792
1922
|
this.terminalChannel.stop();
|
|
1793
1923
|
this.runtimeCredentials.close();
|
|
1924
|
+
this.scheduledSessions.stop();
|
|
1925
|
+
this.scheduledRunAuthorization.close();
|
|
1794
1926
|
this.directTransferService?.dispose();
|
|
1795
1927
|
this.directTransferService = undefined;
|
|
1796
1928
|
this.controlChannel.stop();
|
|
@@ -1801,7 +1933,8 @@ export class NodeConnector {
|
|
|
1801
1933
|
runnerDrain,
|
|
1802
1934
|
terminalDrain,
|
|
1803
1935
|
uploadDrain,
|
|
1804
|
-
goalMcpDrain
|
|
1936
|
+
goalMcpDrain,
|
|
1937
|
+
scheduledMcpDrain
|
|
1805
1938
|
]);
|
|
1806
1939
|
this.stopping = (async () => {
|
|
1807
1940
|
try {
|
|
@@ -1818,6 +1951,84 @@ export class NodeConnector {
|
|
|
1818
1951
|
}
|
|
1819
1952
|
/** @internal Thin compatibility delegates; all behavior remains in the extracted services. */
|
|
1820
1953
|
handleMessage = (raw) => this.controlMessages.handle(raw);
|
|
1954
|
+
publishApprovalForRun = (input) => {
|
|
1955
|
+
const run = this.runtime.getRun(this.runtime.canonicalRunId(input.runId));
|
|
1956
|
+
if (run?.source !== 'SCHEDULED') {
|
|
1957
|
+
this.runEventBridge.publishApproval(input);
|
|
1958
|
+
return;
|
|
1959
|
+
}
|
|
1960
|
+
this.runEventBridge.emitRun(input.runId, 'approval.request', input);
|
|
1961
|
+
queueMicrotask(() => {
|
|
1962
|
+
try {
|
|
1963
|
+
this.runtime.decideApproval(input.approvalId, 'APPROVED');
|
|
1964
|
+
this.runEventBridge.emitRun(input.runId, 'approval.decided', {
|
|
1965
|
+
approvalId: input.approvalId,
|
|
1966
|
+
decision: 'APPROVED',
|
|
1967
|
+
source: 'SCHEDULED'
|
|
1968
|
+
});
|
|
1969
|
+
this.runners.decideApproval({
|
|
1970
|
+
runId: input.runId,
|
|
1971
|
+
approvalId: input.approvalId,
|
|
1972
|
+
decision: 'APPROVED'
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
catch {
|
|
1976
|
+
// A cancelled execution cannot receive an approval after its pending request expires.
|
|
1977
|
+
}
|
|
1978
|
+
});
|
|
1979
|
+
};
|
|
1980
|
+
emitScheduledNotification = (task, occurrenceId, code, status) => {
|
|
1981
|
+
const occurredAt = Date.now();
|
|
1982
|
+
const session = this.runtime.getAgentSession(task.sessionId);
|
|
1983
|
+
this.emitWorkbenchEvent('conversation', {
|
|
1984
|
+
event: {
|
|
1985
|
+
runId: occurrenceId,
|
|
1986
|
+
sequence: 1,
|
|
1987
|
+
eventType: 'notification',
|
|
1988
|
+
payload: {
|
|
1989
|
+
targetType: 'WORKSPACE_SESSION',
|
|
1990
|
+
userId: task.ownerUserId,
|
|
1991
|
+
workspaceId: task.workspaceId,
|
|
1992
|
+
sessionId: task.sessionId,
|
|
1993
|
+
sessionTitle: session?.customTitle ?? session?.runnerTitle ?? task.sessionId,
|
|
1994
|
+
runId: occurrenceId,
|
|
1995
|
+
notificationId: `schedule:${task.id}:${occurrenceId}`,
|
|
1996
|
+
kind: status === 'FAILED' ? 'SCHEDULE_FAILED' : 'SCHEDULE_SKIPPED',
|
|
1997
|
+
code,
|
|
1998
|
+
occurredAt
|
|
1999
|
+
},
|
|
2000
|
+
createdAt: occurredAt
|
|
2001
|
+
}
|
|
2002
|
+
});
|
|
2003
|
+
};
|
|
2004
|
+
emitScheduledProposalNotification = (sessionId, proposalId) => {
|
|
2005
|
+
if (this.database === undefined)
|
|
2006
|
+
return;
|
|
2007
|
+
const task = new ScheduledSessionStore(this.database.raw).get(sessionId);
|
|
2008
|
+
if (task === undefined)
|
|
2009
|
+
return;
|
|
2010
|
+
const occurredAt = Date.now();
|
|
2011
|
+
const session = this.runtime.getAgentSession(sessionId);
|
|
2012
|
+
this.emitWorkbenchEvent('conversation', {
|
|
2013
|
+
event: {
|
|
2014
|
+
runId: proposalId,
|
|
2015
|
+
sequence: 1,
|
|
2016
|
+
eventType: 'notification',
|
|
2017
|
+
payload: {
|
|
2018
|
+
targetType: 'WORKSPACE_SESSION',
|
|
2019
|
+
userId: task.ownerUserId,
|
|
2020
|
+
workspaceId: task.workspaceId,
|
|
2021
|
+
sessionId,
|
|
2022
|
+
sessionTitle: session?.customTitle ?? session?.runnerTitle ?? sessionId,
|
|
2023
|
+
runId: proposalId,
|
|
2024
|
+
notificationId: `schedule-proposal:${task.id}:${proposalId}`,
|
|
2025
|
+
kind: 'SCHEDULE_PROPOSAL',
|
|
2026
|
+
occurredAt
|
|
2027
|
+
},
|
|
2028
|
+
createdAt: occurredAt
|
|
2029
|
+
}
|
|
2030
|
+
});
|
|
2031
|
+
};
|
|
1821
2032
|
emitRunEvent = (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status);
|
|
1822
2033
|
emitConversationItem = (runId, item) => {
|
|
1823
2034
|
if (this.continuousRunCoordinator.pending(runId)?.goal !== undefined &&
|
package/dist/database.js
CHANGED
|
@@ -5,8 +5,9 @@ import { DatabaseSync } from 'node:sqlite';
|
|
|
5
5
|
import { runnerDefaultConfigurationSchema } from '@myagentroam/protocol';
|
|
6
6
|
import { mcpCatalogEntrySchema, mcpInstallationConfigurationSchema, mcpRuntimeDescriptorSchema } from '@myagentroam/protocol';
|
|
7
7
|
import { assertNodeSchemaV13, NODE_SCHEMA_V13, NODE_SCHEMA_VERSION_V13 } from './migrations/v013.js';
|
|
8
|
+
import { NODE_SCHEMA_MIGRATION_V14 } from './migrations/v014.js';
|
|
8
9
|
// Future released migrations must be contiguous, immutable and begin at v14; Demo is excluded.
|
|
9
|
-
export const NODE_SCHEMA_MIGRATIONS = [];
|
|
10
|
+
export const NODE_SCHEMA_MIGRATIONS = [NODE_SCHEMA_MIGRATION_V14];
|
|
10
11
|
const LEGACY_NODE_SCHEMA_V12 = NODE_SCHEMA_VERSION_V13 - 1;
|
|
11
12
|
export class NodeDatabase {
|
|
12
13
|
now;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
export const NODE_SCHEMA_MIGRATION_V14 = {
|
|
2
|
+
version: 14,
|
|
3
|
+
sql: `CREATE TABLE scheduled_session_tasks (
|
|
4
|
+
id TEXT PRIMARY KEY,
|
|
5
|
+
session_id TEXT NOT NULL UNIQUE REFERENCES agent_sessions(id) ON DELETE CASCADE,
|
|
6
|
+
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
7
|
+
runner TEXT NOT NULL CHECK (runner IN ('mar-agent','codex','claude-code','opencode')),
|
|
8
|
+
owner_user_id INTEGER NOT NULL CHECK (owner_user_id > 0),
|
|
9
|
+
objective TEXT NOT NULL CHECK (length(objective) BETWEEN 1 AND 20000),
|
|
10
|
+
cron TEXT NOT NULL,
|
|
11
|
+
timezone TEXT NOT NULL,
|
|
12
|
+
enabled INTEGER NOT NULL CHECK (enabled IN (0,1)),
|
|
13
|
+
revision INTEGER NOT NULL CHECK (revision > 0),
|
|
14
|
+
next_run_at INTEGER CHECK (next_run_at IS NULL OR next_run_at >= 0),
|
|
15
|
+
created_at INTEGER NOT NULL CHECK (created_at >= 0),
|
|
16
|
+
updated_at INTEGER NOT NULL CHECK (updated_at >= 0)
|
|
17
|
+
) STRICT;
|
|
18
|
+
CREATE INDEX scheduled_session_tasks_due
|
|
19
|
+
ON scheduled_session_tasks(enabled, next_run_at);
|
|
20
|
+
CREATE TABLE scheduled_session_occurrences (
|
|
21
|
+
id TEXT PRIMARY KEY,
|
|
22
|
+
task_id TEXT NOT NULL REFERENCES scheduled_session_tasks(id) ON DELETE CASCADE,
|
|
23
|
+
due_at INTEGER NOT NULL CHECK (due_at >= 0),
|
|
24
|
+
status TEXT NOT NULL CHECK (status IN ('CLAIMED','SUBMITTED','SUCCEEDED','SKIPPED','FAILED')),
|
|
25
|
+
code TEXT,
|
|
26
|
+
run_id TEXT,
|
|
27
|
+
created_at INTEGER NOT NULL CHECK (created_at >= 0),
|
|
28
|
+
updated_at INTEGER NOT NULL CHECK (updated_at >= 0),
|
|
29
|
+
UNIQUE(task_id, due_at)
|
|
30
|
+
) STRICT;
|
|
31
|
+
CREATE INDEX scheduled_session_occurrences_recent
|
|
32
|
+
ON scheduled_session_occurrences(task_id, due_at DESC);
|
|
33
|
+
CREATE TABLE scheduled_session_proposals (
|
|
34
|
+
id TEXT PRIMARY KEY,
|
|
35
|
+
task_id TEXT NOT NULL REFERENCES scheduled_session_tasks(id) ON DELETE CASCADE,
|
|
36
|
+
proposed_by_user_id INTEGER NOT NULL CHECK (proposed_by_user_id > 0),
|
|
37
|
+
base_revision INTEGER NOT NULL CHECK (base_revision > 0),
|
|
38
|
+
objective TEXT CHECK (objective IS NULL OR length(objective) BETWEEN 1 AND 20000),
|
|
39
|
+
cron TEXT,
|
|
40
|
+
timezone TEXT,
|
|
41
|
+
status TEXT NOT NULL CHECK (status IN ('PENDING','ACCEPTED','REJECTED','STALE')),
|
|
42
|
+
created_at INTEGER NOT NULL CHECK (created_at >= 0),
|
|
43
|
+
resolved_at INTEGER
|
|
44
|
+
) STRICT;
|
|
45
|
+
CREATE INDEX scheduled_session_proposals_pending
|
|
46
|
+
ON scheduled_session_proposals(task_id, status, created_at DESC);`,
|
|
47
|
+
assert(database) {
|
|
48
|
+
const tables = {
|
|
49
|
+
scheduled_session_tasks: [
|
|
50
|
+
'id',
|
|
51
|
+
'session_id',
|
|
52
|
+
'workspace_id',
|
|
53
|
+
'runner',
|
|
54
|
+
'owner_user_id',
|
|
55
|
+
'objective',
|
|
56
|
+
'cron',
|
|
57
|
+
'timezone',
|
|
58
|
+
'enabled',
|
|
59
|
+
'revision',
|
|
60
|
+
'next_run_at',
|
|
61
|
+
'created_at',
|
|
62
|
+
'updated_at'
|
|
63
|
+
],
|
|
64
|
+
scheduled_session_occurrences: [
|
|
65
|
+
'id',
|
|
66
|
+
'task_id',
|
|
67
|
+
'due_at',
|
|
68
|
+
'status',
|
|
69
|
+
'code',
|
|
70
|
+
'run_id',
|
|
71
|
+
'created_at',
|
|
72
|
+
'updated_at'
|
|
73
|
+
],
|
|
74
|
+
scheduled_session_proposals: [
|
|
75
|
+
'id',
|
|
76
|
+
'task_id',
|
|
77
|
+
'proposed_by_user_id',
|
|
78
|
+
'base_revision',
|
|
79
|
+
'objective',
|
|
80
|
+
'cron',
|
|
81
|
+
'timezone',
|
|
82
|
+
'status',
|
|
83
|
+
'created_at',
|
|
84
|
+
'resolved_at'
|
|
85
|
+
]
|
|
86
|
+
};
|
|
87
|
+
for (const [table, required] of Object.entries(tables)) {
|
|
88
|
+
const row = database
|
|
89
|
+
.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name=?")
|
|
90
|
+
.get(table);
|
|
91
|
+
const columns = new Set(database.prepare(`PRAGMA table_info(${table})`).all().map((column) => column.name));
|
|
92
|
+
if (row === undefined ||
|
|
93
|
+
!row.sql.endsWith('STRICT') ||
|
|
94
|
+
required.some((column) => !columns.has(column)))
|
|
95
|
+
throw new Error('NODE_DATABASE_SCHEMA_INVALID');
|
|
96
|
+
}
|
|
97
|
+
for (const name of [
|
|
98
|
+
'scheduled_session_tasks_due',
|
|
99
|
+
'scheduled_session_occurrences_recent',
|
|
100
|
+
'scheduled_session_proposals_pending'
|
|
101
|
+
])
|
|
102
|
+
if (database
|
|
103
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='index' AND name=?")
|
|
104
|
+
.get(name) === undefined)
|
|
105
|
+
throw new Error('NODE_DATABASE_SCHEMA_INVALID');
|
|
106
|
+
}
|
|
107
|
+
};
|
|
@@ -173,7 +173,7 @@ function resolvedMcpServers(raw, secrets) {
|
|
|
173
173
|
const record = value;
|
|
174
174
|
if (typeof record['serverName'] !== 'string' || record['enabled'] !== true)
|
|
175
175
|
continue;
|
|
176
|
-
if (record['__marGoal'] === true &&
|
|
176
|
+
if ((record['__marGoal'] === true || record['__marScheduled'] === true) &&
|
|
177
177
|
typeof record['command'] === 'string' &&
|
|
178
178
|
Array.isArray(record['args']) &&
|
|
179
179
|
record['args'].every((value) => typeof value === 'string') &&
|
|
@@ -186,10 +186,16 @@ function resolvedMcpServers(raw, secrets) {
|
|
|
186
186
|
transport: 'STDIO',
|
|
187
187
|
command: record['command'],
|
|
188
188
|
args: record['args'],
|
|
189
|
-
environment:
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
189
|
+
environment: record['__marGoal'] === true
|
|
190
|
+
? {
|
|
191
|
+
MAR_GOAL_MCP_ENDPOINT: record['endpoint'],
|
|
192
|
+
MAR_GOAL_MCP_TOKEN: secrets[record['secretName']]
|
|
193
|
+
}
|
|
194
|
+
: {
|
|
195
|
+
MAR_SCHEDULE_MCP_ENDPOINT: record['endpoint'],
|
|
196
|
+
MAR_SCHEDULE_MCP_TOKEN: secrets[record['secretName']],
|
|
197
|
+
MAR_SCHEDULE_MCP_READ_ONLY: record['readOnly'] === true ? '1' : '0'
|
|
198
|
+
}
|
|
193
199
|
});
|
|
194
200
|
continue;
|
|
195
201
|
}
|
|
@@ -230,6 +230,9 @@ export class ClaudeCodeRunner extends AbstractRunner {
|
|
|
230
230
|
externalSessionId: item.externalSessionId,
|
|
231
231
|
cwd: item.cwd,
|
|
232
232
|
...(item.title === undefined ? {} : { title: item.title }),
|
|
233
|
+
...('titleOrigin' in item && item.titleOrigin !== undefined
|
|
234
|
+
? { titleOrigin: item.titleOrigin }
|
|
235
|
+
: {}),
|
|
233
236
|
...(isNativeHistory(item) ? { lastActivityAt: latestNativeActivity(item) } : {})
|
|
234
237
|
});
|
|
235
238
|
const index = sessions.findIndex((candidate) => candidate.id === session.id);
|