@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.
Files changed (31) hide show
  1. package/dist/codex-app-server.js +30 -0
  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 +135 -72
  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 +14 -8
  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
+ }
@@ -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';
@@ -11,30 +10,35 @@ import { codexSessionControl } from '../codex-app-server.js';
11
10
  import { extractId } from '../util/runner-native-session-parsers.js';
12
11
  import { nodeLog } from '../operational.js';
13
12
  const CODEX_MODEL_CACHE_MS = 10 * 60 * 1_000;
14
- const CODEX_PROFILE_ENTRY_TTL_MS = 30 * 60 * 1_000;
15
13
  const CODEX_MODEL_FAILURE_RETRY_MS = 30 * 1_000;
16
14
  const CODEX_MODEL_PAGE_SIZE = 100;
17
15
  const CODEX_MODEL_MAX_PAGES = 6;
18
16
  export class CodexRunner extends AbstractRunner {
19
- client;
20
17
  profileClientFactory;
21
18
  name = 'codex';
22
19
  discoveryScope = 'ALL_WORKSPACES';
23
20
  execution = new CodexExecutionState();
24
21
  fastEnabled = false;
25
- environmentFingerprint;
26
- environmentRuns = new Set();
22
+ runClients = new Map();
23
+ notificationListeners = new Set();
24
+ clientNotificationDisposers = new Map();
27
25
  profileCache;
28
26
  profileCacheVersion;
29
27
  profileCacheTimer;
30
28
  profileRefresh;
29
+ profileRefreshVersion;
31
30
  profileClients = new Set();
32
31
  profileGeneration = 0;
33
- constructor(client = new CodexAppServerClient(), profileClientFactory = () => new CodexAppServerClient()) {
32
+ runClientFactory;
33
+ constructor(client, profileClientFactory = () => new CodexAppServerClient(), runClientFactory) {
34
34
  super();
35
- this.client = client;
36
35
  this.profileClientFactory = profileClientFactory;
36
+ this.client = client ?? new CodexAppServerClient();
37
+ this.runClientFactory =
38
+ runClientFactory ??
39
+ (client === undefined ? () => new CodexAppServerClient() : () => this.client);
37
40
  }
41
+ client;
38
42
  profile(capabilities) {
39
43
  const base = declaredRunnerProfiles({
40
44
  codexAvailable: capabilities.codex.available,
@@ -56,6 +60,14 @@ export class CodexRunner extends AbstractRunner {
56
60
  if (!capabilities.codex.available)
57
61
  return { ...base, models: [] };
58
62
  const version = capabilities.codex.version;
63
+ if (this.profileRefresh !== undefined && this.profileRefreshVersion !== version) {
64
+ this.profileGeneration += 1;
65
+ this.profileRefresh = undefined;
66
+ this.profileRefreshVersion = undefined;
67
+ if (this.profileCacheTimer !== undefined)
68
+ clearTimeout(this.profileCacheTimer);
69
+ this.profileCacheTimer = undefined;
70
+ }
59
71
  const now = Date.now();
60
72
  const cached = this.profileCacheVersion === version ? this.profileCache : undefined;
61
73
  if (cached?.successful === true && now < cached.fetchedAt + CODEX_MODEL_CACHE_MS)
@@ -85,8 +97,10 @@ export class CodexRunner extends AbstractRunner {
85
97
  successful: true,
86
98
  retryAt: 0
87
99
  };
88
- if (generation === this.profileGeneration)
100
+ if (generation === this.profileGeneration) {
89
101
  this.cacheProfile(version, cache);
102
+ this.scheduleProfileRefresh(CODEX_MODEL_CACHE_MS, version, base, environment);
103
+ }
90
104
  return cache.profile;
91
105
  })
92
106
  .catch((error) => {
@@ -98,6 +112,7 @@ export class CodexRunner extends AbstractRunner {
98
112
  const cached = this.profileCacheVersion === version ? this.profileCache : undefined;
99
113
  if (cached !== undefined) {
100
114
  cached.retryAt = Date.now() + CODEX_MODEL_FAILURE_RETRY_MS;
115
+ this.scheduleProfileRefresh(CODEX_MODEL_FAILURE_RETRY_MS, version, base, environment);
101
116
  return cached.profile;
102
117
  }
103
118
  this.cacheProfile(version, {
@@ -107,25 +122,30 @@ export class CodexRunner extends AbstractRunner {
107
122
  successful: false,
108
123
  retryAt: Date.now() + CODEX_MODEL_FAILURE_RETRY_MS
109
124
  });
125
+ this.scheduleProfileRefresh(CODEX_MODEL_FAILURE_RETRY_MS, version, base, environment);
110
126
  return { ...base, models: [] };
111
127
  })
112
128
  .finally(() => {
113
- if (this.profileRefresh === refresh)
129
+ if (this.profileRefresh === refresh) {
114
130
  this.profileRefresh = undefined;
131
+ this.profileRefreshVersion = undefined;
132
+ }
115
133
  });
116
134
  this.profileRefresh = refresh;
135
+ this.profileRefreshVersion = version;
117
136
  return refresh;
118
137
  }
119
138
  cacheProfile(version, cache) {
120
- if (this.profileCacheTimer !== undefined)
121
- clearTimeout(this.profileCacheTimer);
122
139
  this.profileCache = cache;
123
140
  this.profileCacheVersion = version;
141
+ }
142
+ scheduleProfileRefresh(delayMs, version, base, environment) {
143
+ if (this.profileCacheTimer !== undefined)
144
+ clearTimeout(this.profileCacheTimer);
124
145
  this.profileCacheTimer = setTimeout(() => {
125
146
  this.profileCacheTimer = undefined;
126
- this.profileCache = undefined;
127
- this.profileCacheVersion = undefined;
128
- }, CODEX_PROFILE_ENTRY_TTL_MS);
147
+ void this.refreshCodexProfile(version, base, environment);
148
+ }, delayMs);
129
149
  this.profileCacheTimer.unref();
130
150
  }
131
151
  async fetchCodexProfile(base, environment) {
@@ -255,7 +275,8 @@ export class CodexRunner extends AbstractRunner {
255
275
  async readManagedRunnerTitle(session, nativeSessionId) {
256
276
  void session;
257
277
  await this.start();
258
- return extractCodexThreads(await this.listThreads()).find((thread) => thread.id === nativeSessionId)?.title;
278
+ const thread = extractCodexThreads(await this.listThreads()).find((candidate) => candidate.id === nativeSessionId);
279
+ return thread?.titleOrigin === 'OFFICIAL' ? thread.title : undefined;
259
280
  }
260
281
  nativeImageRoots(session) {
261
282
  return [
@@ -295,13 +316,14 @@ export class CodexRunner extends AbstractRunner {
295
316
  return this.profileCache;
296
317
  }
297
318
  async resumeExternal(external, context) {
319
+ const threadId = external.externalSessionId;
298
320
  if (external.runner !== this.name ||
299
321
  external.nativeControl !== 'EXTERNAL' ||
300
- external.externalSessionId === null ||
322
+ threadId === null ||
301
323
  !context.available)
302
324
  return { failureCode: 'RUNNER_UNAVAILABLE' };
303
325
  try {
304
- await this.resumeThread(external.externalSessionId, external.cwd);
326
+ await this.withTemporaryClient((client) => client.resumeThread(threadId, external.cwd));
305
327
  const projection = context.projectionFor(external);
306
328
  if (projection !== undefined) {
307
329
  const promoted = context.promote(projection.id);
@@ -343,7 +365,7 @@ export class CodexRunner extends AbstractRunner {
343
365
  externalSessionId: thread.id,
344
366
  cwd,
345
367
  ...(thread.title === undefined ? {} : { title: thread.title }),
346
- titleOrigin: 'OFFICIAL'
368
+ ...(thread.titleOrigin === undefined ? {} : { titleOrigin: thread.titleOrigin })
347
369
  }));
348
370
  }
349
371
  for (const entry of await discoverAllNativeSessions(this.name)) {
@@ -412,107 +434,148 @@ export class CodexRunner extends AbstractRunner {
412
434
  };
413
435
  }
414
436
  onNotification(listener) {
415
- return this.client.onNotification(listener);
416
- }
417
- start() {
418
- return this.client.start();
437
+ this.notificationListeners.add(listener);
438
+ this.attachNotifications(this.client);
439
+ for (const client of this.runClients.values())
440
+ this.attachNotifications(client);
441
+ return () => this.notificationListeners.delete(listener);
442
+ }
443
+ async start(runId) {
444
+ const client = this.clientFor(runId);
445
+ try {
446
+ await client.start();
447
+ }
448
+ catch (error) {
449
+ if (runId === undefined ||
450
+ !(error instanceof Error) ||
451
+ error.message !== 'CODEX_APP_SERVER_STOPPED' ||
452
+ this.runClients.get(runId) !== client)
453
+ throw error;
454
+ await client.start();
455
+ }
419
456
  }
420
457
  async recoverMissingRollout(runId, error) {
421
458
  if (!isCodexRolloutMissingError(error))
422
459
  return false;
423
- if ([...this.environmentRuns].some((activeRunId) => activeRunId !== runId))
460
+ const client = this.clientFor(runId);
461
+ if ([...this.runClients].some(([activeRunId, activeClient]) => activeRunId !== runId && activeClient === client))
424
462
  return false;
425
- if ([...this.execution.runs.keys()].some((activeRunId) => activeRunId !== runId))
463
+ if ([...this.execution.runs].some(([activeRunId]) => activeRunId !== runId && this.runClients.get(activeRunId) === client))
426
464
  return false;
427
465
  nodeLog('runner.codex.rollout.recovering', { runId });
428
- await this.client.restart();
466
+ await client.restart();
429
467
  return true;
430
468
  }
431
469
  prepareEnvironment(environment, runId) {
432
- if (Object.keys(environment).length === 0 && this.environmentFingerprint === undefined) {
433
- this.environmentRuns.add(runId);
470
+ if (this.runClients.has(runId))
434
471
  return;
435
- }
436
- const fingerprint = createHash('sha256')
437
- .update(JSON.stringify(Object.entries(environment).sort(([left], [right]) => left.localeCompare(right))))
438
- .digest('base64url');
439
- if (fingerprint === this.environmentFingerprint) {
440
- this.environmentRuns.add(runId);
441
- return;
442
- }
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);
472
+ const client = this.runClientFactory();
473
+ if (Object.keys(environment).length > 0)
474
+ client.configureEnvironment(environment);
475
+ this.attachNotifications(client);
476
+ this.runClients.set(runId, client);
449
477
  }
450
478
  releaseEnvironment(runId) {
451
- this.environmentRuns.delete(runId);
452
- if (this.environmentRuns.size !== 0 || this.environmentFingerprint === undefined)
479
+ const client = this.runClients.get(runId);
480
+ if (client === undefined)
453
481
  return;
454
- this.client.stop();
455
- this.client.clearEnvironment();
456
- this.environmentFingerprint = undefined;
482
+ this.runClients.delete(runId);
483
+ if (client !== this.client && ![...this.runClients.values()].includes(client)) {
484
+ this.detachNotifications(client);
485
+ client.stop();
486
+ client.clearEnvironment();
487
+ }
457
488
  }
458
489
  stop() {
459
490
  this.profileGeneration += 1;
460
491
  for (const profileClient of this.profileClients)
461
492
  profileClient.stop();
462
493
  this.profileClients.clear();
494
+ for (const client of new Set(this.runClients.values()))
495
+ if (client !== this.client) {
496
+ this.detachNotifications(client);
497
+ client.stop();
498
+ client.clearEnvironment();
499
+ }
500
+ this.runClients.clear();
463
501
  this.client.stop();
464
- if (this.environmentFingerprint !== undefined)
465
- this.client.clearEnvironment();
466
- this.environmentFingerprint = undefined;
467
- this.environmentRuns.clear();
468
502
  if (this.profileCacheTimer !== undefined)
469
503
  clearTimeout(this.profileCacheTimer);
470
504
  this.profileCacheTimer = undefined;
471
505
  this.profileCache = undefined;
472
506
  this.profileCacheVersion = undefined;
473
507
  this.profileRefresh = undefined;
508
+ this.profileRefreshVersion = undefined;
509
+ }
510
+ clientFor(runId) {
511
+ return runId === undefined ? this.client : (this.runClients.get(runId) ?? this.client);
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
+ }
524
+ attachNotifications(client) {
525
+ if (this.clientNotificationDisposers.has(client))
526
+ return;
527
+ this.clientNotificationDisposers.set(client, client.onNotification((notification) => {
528
+ for (const listener of this.notificationListeners)
529
+ listener(notification);
530
+ }));
531
+ }
532
+ detachNotifications(client) {
533
+ this.clientNotificationDisposers.get(client)?.();
534
+ this.clientNotificationDisposers.delete(client);
474
535
  }
475
- request(method, params) {
476
- return this.client.request(method, params);
536
+ request(method, params, runId) {
537
+ return this.clientFor(runId).request(method, params);
477
538
  }
478
- respond(requestId, result) {
479
- this.client.respond(requestId, result);
539
+ respond(requestId, result, runId) {
540
+ this.clientFor(runId).respond(requestId, result);
480
541
  }
481
542
  listThreads() {
482
543
  return this.client.listThreads();
483
544
  }
484
- startThread(cwd, configuration) {
485
- return this.client.startThread(cwd, configuration);
545
+ startThread(cwd, configuration, runId) {
546
+ return this.clientFor(runId).startThread(cwd, configuration);
486
547
  }
487
548
  forkThread(threadId, cwd, lastTurnId, configuration) {
488
549
  return this.client.forkThread(threadId, cwd, lastTurnId, configuration);
489
550
  }
490
551
  async forkNative(session, boundary, title) {
491
- if (session.externalSessionId === null)
552
+ const threadId = session.externalSessionId;
553
+ if (threadId === null)
492
554
  throw new Error('SESSION_REWIND_UNAVAILABLE');
493
- await this.start();
494
555
  const configuration = { model: session.model, effort: session.effort, access: session.access };
495
- const result = boundary === null
496
- ? await this.startThread(session.cwd, configuration)
497
- : await this.forkThread(session.externalSessionId, session.cwd, boundary, configuration);
498
- const threadId = extractId(result, 'thread');
499
- await this.setThreadName(threadId, title);
500
- return threadId;
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
+ });
501
564
  }
502
- resumeThread(threadId, cwd, configuration) {
503
- return this.client.resumeThread(threadId, cwd, configuration);
565
+ resumeThread(threadId, cwd, configuration, runId) {
566
+ return this.clientFor(runId).resumeThread(threadId, cwd, configuration);
504
567
  }
505
- startTurn(threadId, text, cwd, configuration, attachments) {
506
- return this.client.startTurn(threadId, text, cwd, configuration, attachments);
568
+ startTurn(threadId, text, cwd, configuration, attachments, runId) {
569
+ return this.clientFor(runId).startTurn(threadId, text, cwd, configuration, attachments);
507
570
  }
508
- interruptTurn(threadId, turnId) {
509
- return this.client.interruptTurn(threadId, turnId);
571
+ interruptTurn(threadId, turnId, runId) {
572
+ return this.clientFor(runId).interruptTurn(threadId, turnId);
510
573
  }
511
574
  setThreadName(threadId, name) {
512
575
  return this.client.setThreadName(threadId, name);
513
576
  }
514
- listCollaborationModes() {
515
- return this.client.listCollaborationModes();
577
+ listCollaborationModes(runId) {
578
+ return this.clientFor(runId).listCollaborationModes();
516
579
  }
517
580
  }
518
581
  function emptyCodexConfiguration() {