@myagentroam/node 0.9.122 → 0.9.125

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.
Files changed (31) hide show
  1. package/dist/codex-app-server.js +6 -79
  2. package/dist/connector.js +217 -6
  3. package/dist/database.js +2 -1
  4. package/dist/migrations/v014.js +107 -0
  5. package/dist/runner/abstract-runner.js +11 -5
  6. package/dist/runner/claude-code-runner.js +3 -0
  7. package/dist/runner/codex/conversation-parser.js +142 -2
  8. package/dist/runner/codex/managed-run-controller.js +26 -18
  9. package/dist/runner/codex-runner.js +127 -57
  10. package/dist/runner/opencode-runner.js +14 -12
  11. package/dist/runtime-state.js +6 -1
  12. package/dist/service/continuous-run-coordinator.js +8 -2
  13. package/dist/service/conversation-history-service.js +51 -10
  14. package/dist/service/managed-runner-session-service.js +2 -1
  15. package/dist/service/native-session-projection-service.js +3 -1
  16. package/dist/service/node-control-message-service.js +2 -0
  17. package/dist/service/run-event-service.js +18 -2
  18. package/dist/service/runner-interaction-service.js +9 -8
  19. package/dist/service/scheduled-run-authorization-control.js +72 -0
  20. package/dist/service/scheduled-session-coordinator.js +95 -0
  21. package/dist/service/scheduled-session-recurrence.js +72 -0
  22. package/dist/service/scheduled-session-store.js +234 -0
  23. package/dist/service/scheduled-task-mcp-server.js +179 -0
  24. package/dist/service/scheduled-task-mcp-stdio.js +63 -0
  25. package/dist/service/session-catalog-service.js +3 -1
  26. package/dist/service/session-lifecycle-service.js +6 -0
  27. package/dist/service/session-message-service.js +25 -2
  28. package/dist/service/session-presentation-service.js +19 -2
  29. package/dist/service/session-schedule-service.js +153 -0
  30. package/dist/util/runner-native-session-parsers.js +19 -11
  31. package/package.json +4 -4
@@ -499,7 +499,14 @@ export function codexOfficialItem(session, nativeTurnId, value, turnSequence, it
499
499
  summaryCode: 'CONTEXT_COMPACTED'
500
500
  }
501
501
  };
502
- return codexUnknownOfficialItem(base, value);
502
+ const special = codexSpecialItem(value);
503
+ return special === undefined
504
+ ? codexUnknownOfficialItem(base, value)
505
+ : {
506
+ ...base,
507
+ ...special,
508
+ status: value.type === 'collabAgentToolCall' ? codexItemStatus(value.status, true) : base.status
509
+ };
503
510
  }
504
511
  export function codexLiveConversationItem(value, completed, imageAttachments = []) {
505
512
  if (typeof value.type !== 'string')
@@ -687,7 +694,16 @@ export function codexLiveConversationItem(value, completed, imageAttachments = [
687
694
  merge: true
688
695
  };
689
696
  }
690
- return codexUnknownLiveItem(itemId, value.type, completed);
697
+ const special = codexSpecialItem(value);
698
+ return special === undefined
699
+ ? codexUnknownLiveItem(itemId, value.type, completed)
700
+ : {
701
+ itemId,
702
+ kind: special.kind,
703
+ status: special.kind === 'tool_call' ? codexItemStatus(value.status, completed) : 'COMPLETED',
704
+ payload: special.payload,
705
+ merge: special.kind === 'tool_call'
706
+ };
691
707
  }
692
708
  function normalizeCodexFileChanges(changes) {
693
709
  return changes.flatMap((value) => {
@@ -718,6 +734,128 @@ function normalizeCodexFileChanges(changes) {
718
734
  function nonnegativeIntegerOrNull(value) {
719
735
  return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : null;
720
736
  }
737
+ function codexSpecialItem(value) {
738
+ if (value.type === 'sleep') {
739
+ const durationMs = nonnegativeIntegerOrNull(value.durationMs);
740
+ return {
741
+ kind: 'tool_call',
742
+ payload: normalizeCodexTool({
743
+ name: 'clock.sleep',
744
+ namespace: 'clock',
745
+ inputSummary: durationMs === null ? null : `${durationMs} ms`
746
+ }).payload
747
+ };
748
+ }
749
+ if (value.type === 'imageView')
750
+ return {
751
+ kind: 'tool_call',
752
+ payload: normalizeCodexTool({
753
+ name: 'view_image',
754
+ namespace: 'codex',
755
+ inputSummary: typeof value.path === 'string' ? compactRunnerText(value.path, 4_096) : null
756
+ }).payload
757
+ };
758
+ if (value.type === 'subAgentActivity') {
759
+ const activity = typeof value.kind === 'string' ? value.kind : '';
760
+ const operation = activity === 'started'
761
+ ? 'START'
762
+ : activity === 'interrupted'
763
+ ? 'CANCEL'
764
+ : activity === 'interacted' || activity === 'completed'
765
+ ? 'STATUS'
766
+ : undefined;
767
+ if (operation === undefined)
768
+ return undefined;
769
+ const agentId = typeof value.agentThreadId === 'string' ? compactRunnerText(value.agentThreadId, 128) : '';
770
+ const agentPath = typeof value.agentPath === 'string' ? compactRunnerText(value.agentPath, 500) : '';
771
+ return {
772
+ kind: 'tool_call',
773
+ payload: {
774
+ ...normalizeCodexTool({
775
+ name: 'subagent_activity',
776
+ namespace: 'codex',
777
+ outputSummary: activity
778
+ }).payload,
779
+ ...(agentPath ? { title: agentPath } : {}),
780
+ subagentOperation: operation,
781
+ subagentId: agentId || null
782
+ }
783
+ };
784
+ }
785
+ if (value.type === 'collabAgentToolCall') {
786
+ const names = {
787
+ spawnAgent: 'spawn_agent',
788
+ sendInput: 'send_input',
789
+ resumeAgent: 'resume_agent',
790
+ wait: 'wait_agent',
791
+ closeAgent: 'close_agent',
792
+ sendMessage: 'send_message',
793
+ followupTask: 'followup_task',
794
+ interruptAgent: 'interrupt_agent',
795
+ listAgents: 'list_agents'
796
+ };
797
+ const toolName = typeof value.tool === 'string' ? names[value.tool] : undefined;
798
+ if (toolName === undefined)
799
+ return undefined;
800
+ const targets = Array.isArray(value.receiverThreadIds)
801
+ ? value.receiverThreadIds.filter((id) => typeof id === 'string')
802
+ : [];
803
+ const input = {
804
+ ...(typeof value.prompt === 'string' ? { prompt: value.prompt } : {}),
805
+ ...(targets.length === 1 ? { target: targets[0] } : {}),
806
+ ...(targets.length > 1 ? { receiverThreadIds: targets } : {}),
807
+ ...(typeof value.model === 'string' ? { model: value.model } : {})
808
+ };
809
+ const normalized = normalizeCodexTool({
810
+ name: toolName,
811
+ namespace: 'collaboration',
812
+ rawInput: input,
813
+ inputSummary: codexCompactJson(input) || null,
814
+ outputSummary: codexCompactJson({
815
+ status: value.status,
816
+ agentsStates: value.agentsStates
817
+ }) || null
818
+ });
819
+ const effort = value.reasoningEffort;
820
+ const modelId = typeof value.model === 'string' ? compactRunnerText(value.model, 128) : '';
821
+ return {
822
+ kind: 'tool_call',
823
+ payload: {
824
+ ...normalized.payload,
825
+ ...(modelId ? { subagentModelId: modelId } : {}),
826
+ ...(typeof effort === 'string' &&
827
+ ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'].includes(effort)
828
+ ? { subagentReasoningEffort: effort }
829
+ : {})
830
+ }
831
+ };
832
+ }
833
+ const notice = codexNotice(value);
834
+ return notice === undefined
835
+ ? undefined
836
+ : {
837
+ kind: 'warning',
838
+ payload: { code: notice.code, message: notice.message, recoverable: true }
839
+ };
840
+ }
841
+ function codexNotice(value) {
842
+ if (value.type === 'hookPrompt') {
843
+ const count = Array.isArray(value.fragments) ? value.fragments.length : 0;
844
+ return {
845
+ code: 'CODEX_HOOK_PROMPT',
846
+ message: `Hook prompt added (${count} ${count === 1 ? 'fragment' : 'fragments'}).`
847
+ };
848
+ }
849
+ if (value.type === 'enteredReviewMode' || value.type === 'exitedReviewMode') {
850
+ const action = value.type === 'enteredReviewMode' ? 'Entered' : 'Exited';
851
+ const review = typeof value.review === 'string' ? compactRunnerText(value.review, 400) : '';
852
+ return {
853
+ code: value.type === 'enteredReviewMode' ? 'CODEX_REVIEW_STARTED' : 'CODEX_REVIEW_ENDED',
854
+ message: `${action} review mode${review ? `: ${review}` : ''}`
855
+ };
856
+ }
857
+ return undefined;
858
+ }
721
859
  function codexUnknownOfficialItem(base, value) {
722
860
  if (value.type === 'userMessage' || value.type === 'agentMessage')
723
861
  return undefined;
@@ -835,6 +973,8 @@ export function codexItemStatus(value, completed) {
835
973
  return 'FAILED';
836
974
  if (value === 'declined')
837
975
  return 'DECLINED';
976
+ if (value === 'interrupted')
977
+ return 'INTERRUPTED';
838
978
  return completed || value === 'completed' ? 'COMPLETED' : 'IN_PROGRESS';
839
979
  }
840
980
  export function codexContentText(value) {
@@ -35,8 +35,8 @@ export class CodexManagedRunController {
35
35
  this.runner.prepareEnvironment(context.secretEnvironment, environmentLeaseId);
36
36
  let modes;
37
37
  try {
38
- await this.runner.start();
39
- modes = await this.runner.listCollaborationModes();
38
+ await this.runner.start(environmentLeaseId);
39
+ modes = await this.runner.listCollaborationModes(environmentLeaseId);
40
40
  }
41
41
  finally {
42
42
  this.runner.releaseEnvironment(environmentLeaseId);
@@ -67,7 +67,7 @@ export class CodexManagedRunController {
67
67
  const runId = crypto.randomUUID();
68
68
  this.runner.prepareEnvironment(context.secretEnvironment, runId);
69
69
  try {
70
- await this.runner.start();
70
+ await this.runner.start(runId);
71
71
  return await this.compact(session, threadId, runId, context.initiatedByUserId);
72
72
  }
73
73
  catch (error) {
@@ -99,8 +99,8 @@ export class CodexManagedRunController {
99
99
  });
100
100
  this.host.emit(runId, 'run.started', { command: 'compact' }, 'STARTING');
101
101
  try {
102
- await this.runner.resumeThread(threadId, session.cwd);
103
- await this.runner.request('thread/compact/start', { threadId });
102
+ await this.runner.resumeThread(threadId, session.cwd, undefined, runId);
103
+ await this.runner.request('thread/compact/start', { threadId }, runId);
104
104
  return {
105
105
  command: 'compact',
106
106
  run: this.host.run(runId),
@@ -158,15 +158,15 @@ export class CodexManagedRunController {
158
158
  }
159
159
  if (notification.requestId !== undefined &&
160
160
  notification.method === 'mcpServer/elicitation/request' &&
161
- activeRun.goalMcp === true &&
162
- params.serverName === 'myagentroam_goal' &&
161
+ ((activeRun.goalMcp === true && params.serverName === 'myagentroam_goal') ||
162
+ (activeRun.scheduleMcp === true && params.serverName === 'mar_schedule')) &&
163
163
  isPlainRecord(params._meta) &&
164
164
  params._meta.codex_approval_kind === 'mcp_tool_call') {
165
165
  this.runner.respond(notification.requestId, {
166
166
  action: 'accept',
167
167
  content: {},
168
168
  _meta: null
169
- });
169
+ }, runId);
170
170
  return;
171
171
  }
172
172
  if (notification.method === 'item/agentMessage/delta' && typeof params.delta === 'string') {
@@ -252,9 +252,12 @@ export class CodexManagedRunController {
252
252
  }
253
253
  }
254
254
  requestApproval(runId, notification) {
255
- const approvalId = `codex:${notification.requestId}`;
255
+ const approvalId = `codex:${runId}:${notification.requestId}`;
256
256
  const expiresAt = Date.now() + 10 * 60_000;
257
- this.runner.execution.approvals.set(approvalId, notification.requestId);
257
+ this.runner.execution.approvals.set(approvalId, {
258
+ runId,
259
+ requestId: notification.requestId
260
+ });
258
261
  try {
259
262
  this.host.createApproval({
260
263
  runId,
@@ -278,7 +281,7 @@ export class CodexManagedRunController {
278
281
  const questions = codexUserInputQuestions(params.questions);
279
282
  if (questions.length === 0) {
280
283
  try {
281
- this.runner.respond(nativeRequestId, { answers: {} });
284
+ this.runner.respond(nativeRequestId, { answers: {} }, runId);
282
285
  }
283
286
  catch {
284
287
  // A disconnected App Server cannot receive a malformed-request response.
@@ -286,7 +289,7 @@ export class CodexManagedRunController {
286
289
  return;
287
290
  }
288
291
  const requestId = String(nativeRequestId);
289
- this.runner.execution.userInputs.set(requestId, {
292
+ this.runner.execution.userInputs.set(`${runId}\0${requestId}`, {
290
293
  runId,
291
294
  requestId: nativeRequestId,
292
295
  questions: questions.map((question) => ({
@@ -391,7 +394,7 @@ export class CodexManagedRunController {
391
394
  async run(payload, attachments) {
392
395
  const { runId, sessionId, input, cwd } = payload;
393
396
  try {
394
- await this.runner.start();
397
+ await this.runner.start(runId);
395
398
  if (this.host.completeCancelled(runId, cwd)) {
396
399
  this.runner.releaseEnvironment(runId);
397
400
  return;
@@ -420,8 +423,8 @@ export class CodexManagedRunController {
420
423
  for (let attempt = 0; attempt < 2; attempt += 1) {
421
424
  try {
422
425
  const threadResult = typeof payload.externalSessionId === 'string'
423
- ? await this.runner.resumeThread(payload.externalSessionId, cwd, configuration)
424
- : await this.runner.startThread(cwd, configuration);
426
+ ? await this.runner.resumeThread(payload.externalSessionId, cwd, configuration, runId)
427
+ : await this.runner.startThread(cwd, configuration, runId);
425
428
  if (this.host.completeCancelled(runId, cwd)) {
426
429
  this.runner.releaseEnvironment(runId);
427
430
  return;
@@ -431,9 +434,10 @@ export class CodexManagedRunController {
431
434
  sessionId,
432
435
  threadId,
433
436
  cwd,
434
- goalMcp: hasGoalMcp(payload.mcpServers)
437
+ goalMcp: hasGoalMcp(payload.mcpServers),
438
+ scheduleMcp: hasScheduleMcp(payload.mcpServers)
435
439
  });
436
- turnResult = await this.runner.startTurn(threadId, input, cwd, turnConfiguration, materializedAttachments);
440
+ turnResult = await this.runner.startTurn(threadId, input, cwd, turnConfiguration, materializedAttachments, runId);
437
441
  break;
438
442
  }
439
443
  catch (error) {
@@ -457,7 +461,8 @@ export class CodexManagedRunController {
457
461
  threadId,
458
462
  turnId,
459
463
  cwd,
460
- goalMcp: hasGoalMcp(payload.mcpServers)
464
+ goalMcp: hasGoalMcp(payload.mcpServers),
465
+ scheduleMcp: hasScheduleMcp(payload.mcpServers)
461
466
  });
462
467
  this.runner.execution.managedTurns.set(turnId, { sessionId: publicSessionId, runId });
463
468
  if (this.host.runStatus(runId) === 'CANCELLING') {
@@ -492,3 +497,6 @@ export class CodexManagedRunController {
492
497
  function hasGoalMcp(value) {
493
498
  return isPlainRecord(value) && isPlainRecord(value.myagentroam_goal);
494
499
  }
500
+ function hasScheduleMcp(value) {
501
+ return isPlainRecord(value) && isPlainRecord(value.mar_schedule);
502
+ }
@@ -11,30 +11,36 @@ import { codexSessionControl } from '../codex-app-server.js';
11
11
  import { extractId } from '../util/runner-native-session-parsers.js';
12
12
  import { nodeLog } from '../operational.js';
13
13
  const CODEX_MODEL_CACHE_MS = 10 * 60 * 1_000;
14
- const CODEX_PROFILE_ENTRY_TTL_MS = 30 * 60 * 1_000;
15
14
  const CODEX_MODEL_FAILURE_RETRY_MS = 30 * 1_000;
16
15
  const CODEX_MODEL_PAGE_SIZE = 100;
17
16
  const CODEX_MODEL_MAX_PAGES = 6;
18
17
  export class CodexRunner extends AbstractRunner {
19
- client;
20
18
  profileClientFactory;
21
19
  name = 'codex';
22
20
  discoveryScope = 'ALL_WORKSPACES';
23
21
  execution = new CodexExecutionState();
24
22
  fastEnabled = false;
25
- environmentFingerprint;
26
- environmentRuns = new Set();
23
+ environmentClients = new Map();
24
+ runClients = new Map();
25
+ notificationListeners = new Set();
26
+ clientNotificationDisposers = new Map();
27
27
  profileCache;
28
28
  profileCacheVersion;
29
29
  profileCacheTimer;
30
30
  profileRefresh;
31
+ profileRefreshVersion;
31
32
  profileClients = new Set();
32
33
  profileGeneration = 0;
33
- constructor(client = new CodexAppServerClient(), profileClientFactory = () => new CodexAppServerClient()) {
34
+ runClientFactory;
35
+ constructor(client, profileClientFactory = () => new CodexAppServerClient(), runClientFactory) {
34
36
  super();
35
- this.client = client;
36
37
  this.profileClientFactory = profileClientFactory;
38
+ this.client = client ?? new CodexAppServerClient();
39
+ this.runClientFactory =
40
+ runClientFactory ??
41
+ (client === undefined ? () => new CodexAppServerClient() : () => this.client);
37
42
  }
43
+ client;
38
44
  profile(capabilities) {
39
45
  const base = declaredRunnerProfiles({
40
46
  codexAvailable: capabilities.codex.available,
@@ -56,6 +62,14 @@ export class CodexRunner extends AbstractRunner {
56
62
  if (!capabilities.codex.available)
57
63
  return { ...base, models: [] };
58
64
  const version = capabilities.codex.version;
65
+ if (this.profileRefresh !== undefined && this.profileRefreshVersion !== version) {
66
+ this.profileGeneration += 1;
67
+ this.profileRefresh = undefined;
68
+ this.profileRefreshVersion = undefined;
69
+ if (this.profileCacheTimer !== undefined)
70
+ clearTimeout(this.profileCacheTimer);
71
+ this.profileCacheTimer = undefined;
72
+ }
59
73
  const now = Date.now();
60
74
  const cached = this.profileCacheVersion === version ? this.profileCache : undefined;
61
75
  if (cached?.successful === true && now < cached.fetchedAt + CODEX_MODEL_CACHE_MS)
@@ -85,8 +99,10 @@ export class CodexRunner extends AbstractRunner {
85
99
  successful: true,
86
100
  retryAt: 0
87
101
  };
88
- if (generation === this.profileGeneration)
102
+ if (generation === this.profileGeneration) {
89
103
  this.cacheProfile(version, cache);
104
+ this.scheduleProfileRefresh(CODEX_MODEL_CACHE_MS, version, base, environment);
105
+ }
90
106
  return cache.profile;
91
107
  })
92
108
  .catch((error) => {
@@ -98,6 +114,7 @@ export class CodexRunner extends AbstractRunner {
98
114
  const cached = this.profileCacheVersion === version ? this.profileCache : undefined;
99
115
  if (cached !== undefined) {
100
116
  cached.retryAt = Date.now() + CODEX_MODEL_FAILURE_RETRY_MS;
117
+ this.scheduleProfileRefresh(CODEX_MODEL_FAILURE_RETRY_MS, version, base, environment);
101
118
  return cached.profile;
102
119
  }
103
120
  this.cacheProfile(version, {
@@ -107,25 +124,30 @@ export class CodexRunner extends AbstractRunner {
107
124
  successful: false,
108
125
  retryAt: Date.now() + CODEX_MODEL_FAILURE_RETRY_MS
109
126
  });
127
+ this.scheduleProfileRefresh(CODEX_MODEL_FAILURE_RETRY_MS, version, base, environment);
110
128
  return { ...base, models: [] };
111
129
  })
112
130
  .finally(() => {
113
- if (this.profileRefresh === refresh)
131
+ if (this.profileRefresh === refresh) {
114
132
  this.profileRefresh = undefined;
133
+ this.profileRefreshVersion = undefined;
134
+ }
115
135
  });
116
136
  this.profileRefresh = refresh;
137
+ this.profileRefreshVersion = version;
117
138
  return refresh;
118
139
  }
119
140
  cacheProfile(version, cache) {
120
- if (this.profileCacheTimer !== undefined)
121
- clearTimeout(this.profileCacheTimer);
122
141
  this.profileCache = cache;
123
142
  this.profileCacheVersion = version;
143
+ }
144
+ scheduleProfileRefresh(delayMs, version, base, environment) {
145
+ if (this.profileCacheTimer !== undefined)
146
+ clearTimeout(this.profileCacheTimer);
124
147
  this.profileCacheTimer = setTimeout(() => {
125
148
  this.profileCacheTimer = undefined;
126
- this.profileCache = undefined;
127
- this.profileCacheVersion = undefined;
128
- }, CODEX_PROFILE_ENTRY_TTL_MS);
149
+ void this.refreshCodexProfile(version, base, environment);
150
+ }, delayMs);
129
151
  this.profileCacheTimer.unref();
130
152
  }
131
153
  async fetchCodexProfile(base, environment) {
@@ -255,7 +277,8 @@ export class CodexRunner extends AbstractRunner {
255
277
  async readManagedRunnerTitle(session, nativeSessionId) {
256
278
  void session;
257
279
  await this.start();
258
- return extractCodexThreads(await this.listThreads()).find((thread) => thread.id === nativeSessionId)?.title;
280
+ const thread = extractCodexThreads(await this.listThreads()).find((candidate) => candidate.id === nativeSessionId);
281
+ return thread?.titleOrigin === 'OFFICIAL' ? thread.title : undefined;
259
282
  }
260
283
  nativeImageRoots(session) {
261
284
  return [
@@ -343,7 +366,7 @@ export class CodexRunner extends AbstractRunner {
343
366
  externalSessionId: thread.id,
344
367
  cwd,
345
368
  ...(thread.title === undefined ? {} : { title: thread.title }),
346
- titleOrigin: 'OFFICIAL'
369
+ ...(thread.titleOrigin === undefined ? {} : { titleOrigin: thread.titleOrigin })
347
370
  }));
348
371
  }
349
372
  for (const entry of await discoverAllNativeSessions(this.name)) {
@@ -412,77 +435,124 @@ export class CodexRunner extends AbstractRunner {
412
435
  };
413
436
  }
414
437
  onNotification(listener) {
415
- return this.client.onNotification(listener);
416
- }
417
- start() {
418
- return this.client.start();
438
+ this.notificationListeners.add(listener);
439
+ this.attachNotifications(this.client);
440
+ for (const { client } of this.environmentClients.values())
441
+ this.attachNotifications(client);
442
+ return () => this.notificationListeners.delete(listener);
443
+ }
444
+ async start(runId) {
445
+ const client = this.clientFor(runId);
446
+ try {
447
+ await client.start();
448
+ }
449
+ catch (error) {
450
+ if (runId === undefined ||
451
+ !(error instanceof Error) ||
452
+ error.message !== 'CODEX_APP_SERVER_STOPPED' ||
453
+ this.runClients.get(runId) !== client)
454
+ throw error;
455
+ await client.start();
456
+ }
419
457
  }
420
458
  async recoverMissingRollout(runId, error) {
421
459
  if (!isCodexRolloutMissingError(error))
422
460
  return false;
423
- if ([...this.environmentRuns].some((activeRunId) => activeRunId !== runId))
461
+ const client = this.clientFor(runId);
462
+ const entry = [...this.environmentClients.values()].find((value) => value.client === client);
463
+ if (entry !== undefined && [...entry.runIds].some((activeRunId) => activeRunId !== runId))
424
464
  return false;
425
- if ([...this.execution.runs.keys()].some((activeRunId) => activeRunId !== runId))
465
+ if ([...this.execution.runs].some(([activeRunId]) => activeRunId !== runId && this.runClients.get(activeRunId) === client))
426
466
  return false;
427
467
  nodeLog('runner.codex.rollout.recovering', { runId });
428
- await this.client.restart();
468
+ await client.restart();
429
469
  return true;
430
470
  }
431
471
  prepareEnvironment(environment, runId) {
432
- if (Object.keys(environment).length === 0 && this.environmentFingerprint === undefined) {
433
- this.environmentRuns.add(runId);
434
- return;
435
- }
436
472
  const fingerprint = createHash('sha256')
437
473
  .update(JSON.stringify(Object.entries(environment).sort(([left], [right]) => left.localeCompare(right))))
438
474
  .digest('base64url');
439
- if (fingerprint === this.environmentFingerprint) {
440
- this.environmentRuns.add(runId);
475
+ const existing = this.environmentClients.get(fingerprint);
476
+ if (existing !== undefined) {
477
+ existing.runIds.add(runId);
478
+ this.runClients.set(runId, existing.client);
441
479
  return;
442
480
  }
443
- if (this.environmentRuns.size > 0 || this.execution.runs.size > 0)
444
- throw new Error('CODEX_SECRET_ENVIRONMENT_CONFLICT');
445
- this.client.stop();
446
- this.client.configureEnvironment(environment);
447
- this.environmentFingerprint = fingerprint;
448
- this.environmentRuns.add(runId);
481
+ const client = this.runClientFactory();
482
+ if (Object.keys(environment).length > 0)
483
+ client.configureEnvironment(environment);
484
+ this.attachNotifications(client);
485
+ this.environmentClients.set(fingerprint, { client, runIds: new Set([runId]) });
486
+ this.runClients.set(runId, client);
449
487
  }
450
488
  releaseEnvironment(runId) {
451
- this.environmentRuns.delete(runId);
452
- if (this.environmentRuns.size !== 0 || this.environmentFingerprint === undefined)
489
+ const client = this.runClients.get(runId);
490
+ if (client === undefined)
453
491
  return;
454
- this.client.stop();
455
- this.client.clearEnvironment();
456
- this.environmentFingerprint = undefined;
492
+ this.runClients.delete(runId);
493
+ for (const [fingerprint, entry] of this.environmentClients) {
494
+ if (entry.client !== client)
495
+ continue;
496
+ entry.runIds.delete(runId);
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;
506
+ }
457
507
  }
458
508
  stop() {
459
509
  this.profileGeneration += 1;
460
510
  for (const profileClient of this.profileClients)
461
511
  profileClient.stop();
462
512
  this.profileClients.clear();
513
+ for (const { client } of this.environmentClients.values())
514
+ if (client !== this.client) {
515
+ this.detachNotifications(client);
516
+ client.stop();
517
+ client.clearEnvironment();
518
+ }
519
+ this.environmentClients.clear();
520
+ this.runClients.clear();
463
521
  this.client.stop();
464
- if (this.environmentFingerprint !== undefined)
465
- this.client.clearEnvironment();
466
- this.environmentFingerprint = undefined;
467
- this.environmentRuns.clear();
468
522
  if (this.profileCacheTimer !== undefined)
469
523
  clearTimeout(this.profileCacheTimer);
470
524
  this.profileCacheTimer = undefined;
471
525
  this.profileCache = undefined;
472
526
  this.profileCacheVersion = undefined;
473
527
  this.profileRefresh = undefined;
528
+ this.profileRefreshVersion = undefined;
529
+ }
530
+ clientFor(runId) {
531
+ return runId === undefined ? this.client : (this.runClients.get(runId) ?? this.client);
532
+ }
533
+ attachNotifications(client) {
534
+ if (this.clientNotificationDisposers.has(client))
535
+ return;
536
+ this.clientNotificationDisposers.set(client, client.onNotification((notification) => {
537
+ for (const listener of this.notificationListeners)
538
+ listener(notification);
539
+ }));
540
+ }
541
+ detachNotifications(client) {
542
+ this.clientNotificationDisposers.get(client)?.();
543
+ this.clientNotificationDisposers.delete(client);
474
544
  }
475
- request(method, params) {
476
- return this.client.request(method, params);
545
+ request(method, params, runId) {
546
+ return this.clientFor(runId).request(method, params);
477
547
  }
478
- respond(requestId, result) {
479
- this.client.respond(requestId, result);
548
+ respond(requestId, result, runId) {
549
+ this.clientFor(runId).respond(requestId, result);
480
550
  }
481
551
  listThreads() {
482
552
  return this.client.listThreads();
483
553
  }
484
- startThread(cwd, configuration) {
485
- return this.client.startThread(cwd, configuration);
554
+ startThread(cwd, configuration, runId) {
555
+ return this.clientFor(runId).startThread(cwd, configuration);
486
556
  }
487
557
  forkThread(threadId, cwd, lastTurnId, configuration) {
488
558
  return this.client.forkThread(threadId, cwd, lastTurnId, configuration);
@@ -499,20 +569,20 @@ export class CodexRunner extends AbstractRunner {
499
569
  await this.setThreadName(threadId, title);
500
570
  return threadId;
501
571
  }
502
- resumeThread(threadId, cwd, configuration) {
503
- return this.client.resumeThread(threadId, cwd, configuration);
572
+ resumeThread(threadId, cwd, configuration, runId) {
573
+ return this.clientFor(runId).resumeThread(threadId, cwd, configuration);
504
574
  }
505
- startTurn(threadId, text, cwd, configuration, attachments) {
506
- return this.client.startTurn(threadId, text, cwd, configuration, attachments);
575
+ startTurn(threadId, text, cwd, configuration, attachments, runId) {
576
+ return this.clientFor(runId).startTurn(threadId, text, cwd, configuration, attachments);
507
577
  }
508
- interruptTurn(threadId, turnId) {
509
- return this.client.interruptTurn(threadId, turnId);
578
+ interruptTurn(threadId, turnId, runId) {
579
+ return this.clientFor(runId).interruptTurn(threadId, turnId);
510
580
  }
511
581
  setThreadName(threadId, name) {
512
582
  return this.client.setThreadName(threadId, name);
513
583
  }
514
- listCollaborationModes() {
515
- return this.client.listCollaborationModes();
584
+ listCollaborationModes(runId) {
585
+ return this.clientFor(runId).listCollaborationModes();
516
586
  }
517
587
  }
518
588
  function emptyCodexConfiguration() {