@myagentroam/node 0.9.125 → 0.9.127
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 +65 -11
- package/dist/database.js +56 -1
- package/dist/migrations/v015.js +12 -0
- package/dist/runner/abstract-runner.js +13 -6
- package/dist/runner/codex-runner.js +32 -39
- package/dist/runner/mar-agent/computer-use-indicator.js +484 -0
- package/dist/runner/mar-agent/computer-use.js +54 -26
- package/dist/service/continuous-run-coordinator.js +12 -3
- package/dist/service/mail-notification-control.js +69 -0
- package/dist/service/mail-notification-mcp-server.js +150 -0
- package/dist/service/mail-notification-mcp-stdio.js +56 -0
- package/dist/service/mcp-node-operation-service.js +7 -1
- package/dist/service/node-control-message-service.js +2 -0
- package/dist/service/scheduled-session-store.js +13 -6
- package/dist/service/scheduled-task-mcp-server.js +4 -1
- package/dist/service/session-schedule-service.js +6 -1
- 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
|
@@ -66,6 +66,8 @@ import { ScheduledSessionCoordinator } from './service/scheduled-session-coordin
|
|
|
66
66
|
import { ScheduledSessionStore } from './service/scheduled-session-store.js';
|
|
67
67
|
import { ScheduledRunAuthorizationControl } from './service/scheduled-run-authorization-control.js';
|
|
68
68
|
import { ScheduledTaskMcpServer } from './service/scheduled-task-mcp-server.js';
|
|
69
|
+
import { MailNotificationControl } from './service/mail-notification-control.js';
|
|
70
|
+
import { MailNotificationMcpServer, resolveMailNotificationCapability } from './service/mail-notification-mcp-server.js';
|
|
69
71
|
import { SessionCatalogService } from './service/session-catalog-service.js';
|
|
70
72
|
import { SessionCommandService } from './service/session-command-service.js';
|
|
71
73
|
import { SessionPresentationService } from './service/session-presentation-service.js';
|
|
@@ -157,6 +159,7 @@ export class NodeConnector {
|
|
|
157
159
|
controlChannel;
|
|
158
160
|
controlMessages;
|
|
159
161
|
scheduledRunAuthorization;
|
|
162
|
+
mailNotificationControl = new MailNotificationControl((envelope) => this.sendEnvelope(envelope));
|
|
160
163
|
scheduledSessions;
|
|
161
164
|
nodeRequests;
|
|
162
165
|
connectionLifecycle;
|
|
@@ -288,6 +291,7 @@ export class NodeConnector {
|
|
|
288
291
|
throw new Error('SCHEDULE_SERVICE_UNAVAILABLE');
|
|
289
292
|
return this.sessionScheduleService.createFromMcp(input);
|
|
290
293
|
}, (sessionId) => this.runtime.getAgentSession(sessionId)?.externalSessionId ?? sessionId);
|
|
294
|
+
mailNotificationMcpServer = new MailNotificationMcpServer((capability) => resolveMailNotificationCapability(this.runtime, capability), (request) => this.mailNotificationControl.request(request));
|
|
291
295
|
runnerUsageReader;
|
|
292
296
|
terminalManager;
|
|
293
297
|
upgradeCoordinator;
|
|
@@ -343,9 +347,15 @@ export class NodeConnector {
|
|
|
343
347
|
return;
|
|
344
348
|
const store = new ScheduledSessionStore(this.database.raw);
|
|
345
349
|
store.finish(run.clientMessageId, status === 'SUCCEEDED' ? 'SUCCEEDED' : 'FAILED', status === 'SUCCEEDED' ? undefined : `SCHEDULE_RUN_${status}`, run.id);
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if (task
|
|
350
|
+
const task = store.getById(run.clientMessageId.split(':', 1)[0]);
|
|
351
|
+
if (task !== undefined) {
|
|
352
|
+
if (task.notifyOnTerminal)
|
|
353
|
+
this.publishScheduledTaskNotification(task, run.clientMessageId, {
|
|
354
|
+
kind: 'SCHEDULE_TERMINAL',
|
|
355
|
+
outcome: status,
|
|
356
|
+
runId: run.id
|
|
357
|
+
});
|
|
358
|
+
else if (status !== 'SUCCEEDED')
|
|
349
359
|
this.emitScheduledNotification(task, run.clientMessageId, `SCHEDULE_RUN_${status}`, 'FAILED');
|
|
350
360
|
}
|
|
351
361
|
},
|
|
@@ -422,6 +432,7 @@ export class NodeConnector {
|
|
|
422
432
|
this.goalExecutionProgress.delete(executionId);
|
|
423
433
|
this.goalMcpServer.revoke(executionId);
|
|
424
434
|
this.scheduledTaskMcpServer.revoke(executionId);
|
|
435
|
+
this.mailNotificationMcpServer.revoke(executionId);
|
|
425
436
|
this.continuousRunCoordinator.remove(executionId);
|
|
426
437
|
}
|
|
427
438
|
});
|
|
@@ -831,6 +842,34 @@ export class NodeConnector {
|
|
|
831
842
|
},
|
|
832
843
|
secrets: resolved.secrets
|
|
833
844
|
};
|
|
845
|
+
},
|
|
846
|
+
mailMcp: async (input) => {
|
|
847
|
+
const run = this.runtime.getRun(input.runId);
|
|
848
|
+
if (run?.initiatedByUserId === undefined)
|
|
849
|
+
return undefined;
|
|
850
|
+
const resolved = await this.mailNotificationMcpServer.installation({
|
|
851
|
+
executionId: input.runId,
|
|
852
|
+
sessionId: input.sessionId,
|
|
853
|
+
workspaceId: input.workspaceId,
|
|
854
|
+
userId: run.initiatedByUserId,
|
|
855
|
+
source: run.source === 'SCHEDULED' ? 'SCHEDULED' : 'MANUAL'
|
|
856
|
+
});
|
|
857
|
+
return {
|
|
858
|
+
installation: {
|
|
859
|
+
targetKind: 'WORKSPACE',
|
|
860
|
+
targetId: input.workspaceId,
|
|
861
|
+
catalogEntryId: 'mar-internal-mail',
|
|
862
|
+
serverName: 'mar_notification',
|
|
863
|
+
displayName: 'MAR email notification',
|
|
864
|
+
version: '1.0.0',
|
|
865
|
+
enabled: true,
|
|
866
|
+
runtime: { transport: 'HTTP', url: 'https://127.0.0.1.invalid/mcp', headers: {} },
|
|
867
|
+
configuration: resolved.installation,
|
|
868
|
+
installedAt: Date.now(),
|
|
869
|
+
...resolved.installation
|
|
870
|
+
},
|
|
871
|
+
secrets: resolved.secrets
|
|
872
|
+
};
|
|
834
873
|
}
|
|
835
874
|
});
|
|
836
875
|
this.workspaceQueueWorkbenchService = new WorkspaceQueueWorkbenchService({
|
|
@@ -1191,6 +1230,8 @@ export class NodeConnector {
|
|
|
1191
1230
|
this.directTransferService?.close();
|
|
1192
1231
|
this.runtimeCredentials.disconnected();
|
|
1193
1232
|
this.scheduledRunAuthorization.disconnected();
|
|
1233
|
+
this.mailNotificationControl.disconnected();
|
|
1234
|
+
this.mailNotificationMcpServer.revokeAll();
|
|
1194
1235
|
this.terminalChannel.disconnect();
|
|
1195
1236
|
if (code === 4000 && reason === 'NODE_CONNECTION_REPLACED')
|
|
1196
1237
|
nodeLog('node.connection.replaced', {
|
|
@@ -1209,6 +1250,7 @@ export class NodeConnector {
|
|
|
1209
1250
|
handleNodeRequest: (envelope) => this.nodeRequests.handle(envelope),
|
|
1210
1251
|
handleRuntimeCredentialResponse: (envelope) => this.runtimeCredentials.handle(envelope),
|
|
1211
1252
|
handleScheduledRunAuthorizeResponse: (envelope) => this.scheduledRunAuthorization.handle(envelope),
|
|
1253
|
+
handleMailNotificationResponse: (envelope) => this.mailNotificationControl.handle(envelope),
|
|
1212
1254
|
handleDirectTransfer: (envelope) => this.handleDirectTransfer(envelope),
|
|
1213
1255
|
handleTerminalDirect: (envelope) => this.handleTerminalDirect(envelope),
|
|
1214
1256
|
reconcileRuns: (envelope) => this.runEventBridge.reconcile(envelope),
|
|
@@ -1271,6 +1313,7 @@ export class NodeConnector {
|
|
|
1271
1313
|
onConnected: () => {
|
|
1272
1314
|
this.runtimeCredentials.connected();
|
|
1273
1315
|
this.scheduledRunAuthorization.connected();
|
|
1316
|
+
this.mailNotificationControl.connected();
|
|
1274
1317
|
this.recentRunProjectionService.publishSnapshot();
|
|
1275
1318
|
}
|
|
1276
1319
|
});
|
|
@@ -1923,6 +1966,8 @@ export class NodeConnector {
|
|
|
1923
1966
|
this.runtimeCredentials.close();
|
|
1924
1967
|
this.scheduledSessions.stop();
|
|
1925
1968
|
this.scheduledRunAuthorization.close();
|
|
1969
|
+
this.mailNotificationControl.close();
|
|
1970
|
+
const mailMcpDrain = this.mailNotificationMcpServer.close();
|
|
1926
1971
|
this.directTransferService?.dispose();
|
|
1927
1972
|
this.directTransferService = undefined;
|
|
1928
1973
|
this.controlChannel.stop();
|
|
@@ -1934,7 +1979,8 @@ export class NodeConnector {
|
|
|
1934
1979
|
terminalDrain,
|
|
1935
1980
|
uploadDrain,
|
|
1936
1981
|
goalMcpDrain,
|
|
1937
|
-
scheduledMcpDrain
|
|
1982
|
+
scheduledMcpDrain,
|
|
1983
|
+
mailMcpDrain
|
|
1938
1984
|
]);
|
|
1939
1985
|
this.stopping = (async () => {
|
|
1940
1986
|
try {
|
|
@@ -1977,24 +2023,32 @@ export class NodeConnector {
|
|
|
1977
2023
|
}
|
|
1978
2024
|
});
|
|
1979
2025
|
};
|
|
1980
|
-
emitScheduledNotification = (task, occurrenceId, code, status) => {
|
|
2026
|
+
emitScheduledNotification = (task, occurrenceId, code, status) => this.publishScheduledTaskNotification(task, occurrenceId, {
|
|
2027
|
+
kind: status === 'FAILED' ? 'SCHEDULE_FAILED' : 'SCHEDULE_SKIPPED',
|
|
2028
|
+
code
|
|
2029
|
+
});
|
|
2030
|
+
publishScheduledTaskNotification = (task, occurrenceId, event) => {
|
|
1981
2031
|
const occurredAt = Date.now();
|
|
2032
|
+
const recipient = (this.database === undefined
|
|
2033
|
+
? undefined
|
|
2034
|
+
: new ScheduledSessionStore(this.database.raw).getById(task.id))?.ownerUserId ?? task.ownerUserId;
|
|
1982
2035
|
const session = this.runtime.getAgentSession(task.sessionId);
|
|
1983
2036
|
this.emitWorkbenchEvent('conversation', {
|
|
1984
2037
|
event: {
|
|
1985
|
-
runId: occurrenceId,
|
|
2038
|
+
runId: event.kind === 'SCHEDULE_TERMINAL' ? event.runId : occurrenceId,
|
|
1986
2039
|
sequence: 1,
|
|
1987
2040
|
eventType: 'notification',
|
|
1988
2041
|
payload: {
|
|
1989
2042
|
targetType: 'WORKSPACE_SESSION',
|
|
1990
|
-
userId:
|
|
2043
|
+
userId: recipient,
|
|
1991
2044
|
workspaceId: task.workspaceId,
|
|
1992
2045
|
sessionId: task.sessionId,
|
|
1993
2046
|
sessionTitle: session?.customTitle ?? session?.runnerTitle ?? task.sessionId,
|
|
1994
|
-
runId: occurrenceId,
|
|
1995
|
-
notificationId:
|
|
1996
|
-
|
|
1997
|
-
|
|
2047
|
+
runId: event.kind === 'SCHEDULE_TERMINAL' ? event.runId : occurrenceId,
|
|
2048
|
+
notificationId: event.kind === 'SCHEDULE_TERMINAL'
|
|
2049
|
+
? `schedule-terminal:${task.id}:${occurrenceId}`
|
|
2050
|
+
: `schedule:${task.id}:${occurrenceId}`,
|
|
2051
|
+
...event,
|
|
1998
2052
|
occurredAt
|
|
1999
2053
|
},
|
|
2000
2054
|
createdAt: occurredAt
|
package/dist/database.js
CHANGED
|
@@ -6,9 +6,14 @@ 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
8
|
import { NODE_SCHEMA_MIGRATION_V14 } from './migrations/v014.js';
|
|
9
|
+
import { NODE_SCHEMA_MIGRATION_V15 } from './migrations/v015.js';
|
|
9
10
|
// Future released migrations must be contiguous, immutable and begin at v14; Demo is excluded.
|
|
10
|
-
export const NODE_SCHEMA_MIGRATIONS = [
|
|
11
|
+
export const NODE_SCHEMA_MIGRATIONS = [
|
|
12
|
+
NODE_SCHEMA_MIGRATION_V14,
|
|
13
|
+
NODE_SCHEMA_MIGRATION_V15
|
|
14
|
+
];
|
|
11
15
|
const LEGACY_NODE_SCHEMA_V12 = NODE_SCHEMA_VERSION_V13 - 1;
|
|
16
|
+
const OBSOLETE_NODE_SCHEMA_V16 = 16;
|
|
12
17
|
export class NodeDatabase {
|
|
13
18
|
now;
|
|
14
19
|
raw;
|
|
@@ -392,6 +397,13 @@ export function migrate(database, catalog = NODE_SCHEMA_MIGRATIONS) {
|
|
|
392
397
|
const migratedVersions = database
|
|
393
398
|
.prepare('SELECT version FROM node_schema_migrations ORDER BY version')
|
|
394
399
|
.all().map((row) => numberValue(row, 'version'));
|
|
400
|
+
if (latest === NODE_SCHEMA_MIGRATION_V15.version &&
|
|
401
|
+
migratedVersions.length === 4 &&
|
|
402
|
+
migratedVersions.every((version, index) => version === NODE_SCHEMA_VERSION_V13 + index) &&
|
|
403
|
+
migratedVersions.at(-1) === OBSOLETE_NODE_SCHEMA_V16) {
|
|
404
|
+
downgradeLegacyRecipientSchema(database);
|
|
405
|
+
migratedVersions.pop();
|
|
406
|
+
}
|
|
395
407
|
const current = migratedVersions.at(-1);
|
|
396
408
|
if (current === undefined ||
|
|
397
409
|
current < NODE_SCHEMA_VERSION_V13 ||
|
|
@@ -421,6 +433,49 @@ export function migrate(database, catalog = NODE_SCHEMA_MIGRATIONS) {
|
|
|
421
433
|
if (catalog.length > 0)
|
|
422
434
|
catalog[catalog.length - 1].assert(database);
|
|
423
435
|
}
|
|
436
|
+
function downgradeLegacyRecipientSchema(database) {
|
|
437
|
+
const reference = new DatabaseSync(':memory:');
|
|
438
|
+
try {
|
|
439
|
+
migrate(reference, [NODE_SCHEMA_MIGRATION_V14]);
|
|
440
|
+
reference.exec(`ALTER TABLE scheduled_session_tasks
|
|
441
|
+
ADD COLUMN notify_user_id INTEGER NOT NULL DEFAULT 1 CHECK (notify_user_id > 0);
|
|
442
|
+
${NODE_SCHEMA_MIGRATION_V15.sql}`);
|
|
443
|
+
const definitions = (source) => new Map(source
|
|
444
|
+
.prepare('SELECT type,name,sql FROM sqlite_master WHERE sql IS NOT NULL ORDER BY type,name')
|
|
445
|
+
.all()
|
|
446
|
+
.map((row) => {
|
|
447
|
+
const type = row['type'];
|
|
448
|
+
const name = row['name'];
|
|
449
|
+
const sql = row['sql'];
|
|
450
|
+
if (typeof type !== 'string' || typeof name !== 'string' || typeof sql !== 'string')
|
|
451
|
+
throw new Error('NODE_DATABASE_VERSION_UNSUPPORTED');
|
|
452
|
+
return [`${type}:${name}`, sql.replace(/\s+/gu, ' ').trim()];
|
|
453
|
+
}));
|
|
454
|
+
const expected = definitions(reference);
|
|
455
|
+
const actual = definitions(database);
|
|
456
|
+
if (expected.size !== actual.size ||
|
|
457
|
+
[...expected].some(([name, sql]) => actual.get(name) !== sql))
|
|
458
|
+
throw new Error('NODE_DATABASE_VERSION_UNSUPPORTED');
|
|
459
|
+
}
|
|
460
|
+
finally {
|
|
461
|
+
reference.close();
|
|
462
|
+
}
|
|
463
|
+
database.exec('BEGIN IMMEDIATE');
|
|
464
|
+
try {
|
|
465
|
+
if (database.prepare('SELECT 1 FROM scheduled_session_tasks LIMIT 1').get() !== undefined)
|
|
466
|
+
throw new Error('NODE_DATABASE_DOWNGRADE_UNSAFE');
|
|
467
|
+
database.exec('ALTER TABLE scheduled_session_tasks DROP COLUMN notify_user_id');
|
|
468
|
+
NODE_SCHEMA_MIGRATION_V15.assert(database);
|
|
469
|
+
database
|
|
470
|
+
.prepare('DELETE FROM node_schema_migrations WHERE version=?')
|
|
471
|
+
.run(OBSOLETE_NODE_SCHEMA_V16);
|
|
472
|
+
database.exec('COMMIT');
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
database.exec('ROLLBACK');
|
|
476
|
+
throw error;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
424
479
|
function mcpInstallation(row) {
|
|
425
480
|
const runtime = mcpRuntimeDescriptorSchema.parse(JSON.parse(stringValue(row, 'runtime')));
|
|
426
481
|
const configuration = mcpInstallationConfigurationSchema.parse(JSON.parse(stringValue(row, 'configuration')));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { NODE_SCHEMA_MIGRATION_V14 } from './v014.js';
|
|
2
|
+
export const NODE_SCHEMA_MIGRATION_V15 = {
|
|
3
|
+
version: 15,
|
|
4
|
+
sql: `ALTER TABLE scheduled_session_tasks
|
|
5
|
+
ADD COLUMN notify_on_terminal INTEGER NOT NULL DEFAULT 0 CHECK (notify_on_terminal IN (0,1));`,
|
|
6
|
+
assert(database) {
|
|
7
|
+
NODE_SCHEMA_MIGRATION_V14.assert(database);
|
|
8
|
+
const columns = new Set(database.prepare('PRAGMA table_info(scheduled_session_tasks)').all().map((column) => column.name));
|
|
9
|
+
if (!columns.has('notify_on_terminal') || columns.has('notify_user_id'))
|
|
10
|
+
throw new Error('NODE_DATABASE_SCHEMA_INVALID');
|
|
11
|
+
}
|
|
12
|
+
};
|
|
@@ -173,7 +173,9 @@ 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 ||
|
|
177
|
+
record['__marScheduled'] === true ||
|
|
178
|
+
record['__marMail'] === true) &&
|
|
177
179
|
typeof record['command'] === 'string' &&
|
|
178
180
|
Array.isArray(record['args']) &&
|
|
179
181
|
record['args'].every((value) => typeof value === 'string') &&
|
|
@@ -191,11 +193,16 @@ function resolvedMcpServers(raw, secrets) {
|
|
|
191
193
|
MAR_GOAL_MCP_ENDPOINT: record['endpoint'],
|
|
192
194
|
MAR_GOAL_MCP_TOKEN: secrets[record['secretName']]
|
|
193
195
|
}
|
|
194
|
-
:
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
196
|
+
: record['__marScheduled'] === true
|
|
197
|
+
? {
|
|
198
|
+
MAR_SCHEDULE_MCP_ENDPOINT: record['endpoint'],
|
|
199
|
+
MAR_SCHEDULE_MCP_TOKEN: secrets[record['secretName']],
|
|
200
|
+
MAR_SCHEDULE_MCP_READ_ONLY: record['readOnly'] === true ? '1' : '0'
|
|
201
|
+
}
|
|
202
|
+
: {
|
|
203
|
+
MAR_MAIL_MCP_ENDPOINT: record['endpoint'],
|
|
204
|
+
MAR_MAIL_MCP_TOKEN: secrets[record['secretName']]
|
|
205
|
+
}
|
|
199
206
|
});
|
|
200
207
|
continue;
|
|
201
208
|
}
|
|
@@ -3,7 +3,6 @@ import { AbstractRunner } from './abstract-runner.js';
|
|
|
3
3
|
import { declaredRunnerProfiles } from '../runner-profiles.js';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { resolve } from 'node:path';
|
|
6
|
-
import { createHash } from 'node:crypto';
|
|
7
6
|
import { discoverAllNativeSessions } from '../native-session-history.js';
|
|
8
7
|
import { readCodexNativeContextUsage } from '../native-session-history.js';
|
|
9
8
|
import { codexThreadCwd, deduplicateDirectSessions, extractCodexThreads, isWithinWorkspace, latestNativeActivity } from './codex/conversation-parser.js';
|
|
@@ -20,7 +19,6 @@ export class CodexRunner extends AbstractRunner {
|
|
|
20
19
|
discoveryScope = 'ALL_WORKSPACES';
|
|
21
20
|
execution = new CodexExecutionState();
|
|
22
21
|
fastEnabled = false;
|
|
23
|
-
environmentClients = new Map();
|
|
24
22
|
runClients = new Map();
|
|
25
23
|
notificationListeners = new Set();
|
|
26
24
|
clientNotificationDisposers = new Map();
|
|
@@ -318,13 +316,14 @@ export class CodexRunner extends AbstractRunner {
|
|
|
318
316
|
return this.profileCache;
|
|
319
317
|
}
|
|
320
318
|
async resumeExternal(external, context) {
|
|
319
|
+
const threadId = external.externalSessionId;
|
|
321
320
|
if (external.runner !== this.name ||
|
|
322
321
|
external.nativeControl !== 'EXTERNAL' ||
|
|
323
|
-
|
|
322
|
+
threadId === null ||
|
|
324
323
|
!context.available)
|
|
325
324
|
return { failureCode: 'RUNNER_UNAVAILABLE' };
|
|
326
325
|
try {
|
|
327
|
-
await this.resumeThread(
|
|
326
|
+
await this.withTemporaryClient((client) => client.resumeThread(threadId, external.cwd));
|
|
328
327
|
const projection = context.projectionFor(external);
|
|
329
328
|
if (projection !== undefined) {
|
|
330
329
|
const promoted = context.promote(projection.id);
|
|
@@ -437,7 +436,7 @@ export class CodexRunner extends AbstractRunner {
|
|
|
437
436
|
onNotification(listener) {
|
|
438
437
|
this.notificationListeners.add(listener);
|
|
439
438
|
this.attachNotifications(this.client);
|
|
440
|
-
for (const
|
|
439
|
+
for (const client of this.runClients.values())
|
|
441
440
|
this.attachNotifications(client);
|
|
442
441
|
return () => this.notificationListeners.delete(listener);
|
|
443
442
|
}
|
|
@@ -459,8 +458,7 @@ export class CodexRunner extends AbstractRunner {
|
|
|
459
458
|
if (!isCodexRolloutMissingError(error))
|
|
460
459
|
return false;
|
|
461
460
|
const client = this.clientFor(runId);
|
|
462
|
-
|
|
463
|
-
if (entry !== undefined && [...entry.runIds].some((activeRunId) => activeRunId !== runId))
|
|
461
|
+
if ([...this.runClients].some(([activeRunId, activeClient]) => activeRunId !== runId && activeClient === client))
|
|
464
462
|
return false;
|
|
465
463
|
if ([...this.execution.runs].some(([activeRunId]) => activeRunId !== runId && this.runClients.get(activeRunId) === client))
|
|
466
464
|
return false;
|
|
@@ -469,20 +467,12 @@ export class CodexRunner extends AbstractRunner {
|
|
|
469
467
|
return true;
|
|
470
468
|
}
|
|
471
469
|
prepareEnvironment(environment, runId) {
|
|
472
|
-
|
|
473
|
-
.update(JSON.stringify(Object.entries(environment).sort(([left], [right]) => left.localeCompare(right))))
|
|
474
|
-
.digest('base64url');
|
|
475
|
-
const existing = this.environmentClients.get(fingerprint);
|
|
476
|
-
if (existing !== undefined) {
|
|
477
|
-
existing.runIds.add(runId);
|
|
478
|
-
this.runClients.set(runId, existing.client);
|
|
470
|
+
if (this.runClients.has(runId))
|
|
479
471
|
return;
|
|
480
|
-
}
|
|
481
472
|
const client = this.runClientFactory();
|
|
482
473
|
if (Object.keys(environment).length > 0)
|
|
483
474
|
client.configureEnvironment(environment);
|
|
484
475
|
this.attachNotifications(client);
|
|
485
|
-
this.environmentClients.set(fingerprint, { client, runIds: new Set([runId]) });
|
|
486
476
|
this.runClients.set(runId, client);
|
|
487
477
|
}
|
|
488
478
|
releaseEnvironment(runId) {
|
|
@@ -490,19 +480,10 @@ export class CodexRunner extends AbstractRunner {
|
|
|
490
480
|
if (client === undefined)
|
|
491
481
|
return;
|
|
492
482
|
this.runClients.delete(runId);
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
if (entry.runIds.size === 0) {
|
|
498
|
-
this.environmentClients.delete(fingerprint);
|
|
499
|
-
if (client !== this.client) {
|
|
500
|
-
this.detachNotifications(client);
|
|
501
|
-
client.stop();
|
|
502
|
-
client.clearEnvironment();
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
return;
|
|
483
|
+
if (client !== this.client && ![...this.runClients.values()].includes(client)) {
|
|
484
|
+
this.detachNotifications(client);
|
|
485
|
+
client.stop();
|
|
486
|
+
client.clearEnvironment();
|
|
506
487
|
}
|
|
507
488
|
}
|
|
508
489
|
stop() {
|
|
@@ -510,13 +491,12 @@ export class CodexRunner extends AbstractRunner {
|
|
|
510
491
|
for (const profileClient of this.profileClients)
|
|
511
492
|
profileClient.stop();
|
|
512
493
|
this.profileClients.clear();
|
|
513
|
-
for (const
|
|
494
|
+
for (const client of new Set(this.runClients.values()))
|
|
514
495
|
if (client !== this.client) {
|
|
515
496
|
this.detachNotifications(client);
|
|
516
497
|
client.stop();
|
|
517
498
|
client.clearEnvironment();
|
|
518
499
|
}
|
|
519
|
-
this.environmentClients.clear();
|
|
520
500
|
this.runClients.clear();
|
|
521
501
|
this.client.stop();
|
|
522
502
|
if (this.profileCacheTimer !== undefined)
|
|
@@ -530,6 +510,17 @@ export class CodexRunner extends AbstractRunner {
|
|
|
530
510
|
clientFor(runId) {
|
|
531
511
|
return runId === undefined ? this.client : (this.runClients.get(runId) ?? this.client);
|
|
532
512
|
}
|
|
513
|
+
async withTemporaryClient(operation) {
|
|
514
|
+
const client = this.runClientFactory();
|
|
515
|
+
try {
|
|
516
|
+
await client.start();
|
|
517
|
+
return await operation(client);
|
|
518
|
+
}
|
|
519
|
+
finally {
|
|
520
|
+
if (client !== this.client)
|
|
521
|
+
await client.stopAndWait();
|
|
522
|
+
}
|
|
523
|
+
}
|
|
533
524
|
attachNotifications(client) {
|
|
534
525
|
if (this.clientNotificationDisposers.has(client))
|
|
535
526
|
return;
|
|
@@ -558,16 +549,18 @@ export class CodexRunner extends AbstractRunner {
|
|
|
558
549
|
return this.client.forkThread(threadId, cwd, lastTurnId, configuration);
|
|
559
550
|
}
|
|
560
551
|
async forkNative(session, boundary, title) {
|
|
561
|
-
|
|
552
|
+
const threadId = session.externalSessionId;
|
|
553
|
+
if (threadId === null)
|
|
562
554
|
throw new Error('SESSION_REWIND_UNAVAILABLE');
|
|
563
|
-
await this.start();
|
|
564
555
|
const configuration = { model: session.model, effort: session.effort, access: session.access };
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
556
|
+
return this.withTemporaryClient(async (client) => {
|
|
557
|
+
const result = boundary === null
|
|
558
|
+
? await client.startThread(session.cwd, configuration)
|
|
559
|
+
: await client.forkThread(threadId, session.cwd, boundary, configuration);
|
|
560
|
+
const forkedThreadId = extractId(result, 'thread');
|
|
561
|
+
await client.setThreadName(forkedThreadId, title);
|
|
562
|
+
return forkedThreadId;
|
|
563
|
+
});
|
|
571
564
|
}
|
|
572
565
|
resumeThread(threadId, cwd, configuration, runId) {
|
|
573
566
|
return this.clientFor(runId).resumeThread(threadId, cwd, configuration);
|