@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.
@@ -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
- if (status !== 'SUCCEEDED') {
347
- const task = store.get(run.sessionId);
348
- if (task !== undefined)
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: task.ownerUserId,
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: `schedule:${task.id}:${occurrenceId}`,
1996
- kind: status === 'FAILED' ? 'SCHEDULE_FAILED' : 'SCHEDULE_SKIPPED',
1997
- code,
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 = [NODE_SCHEMA_MIGRATION_V14];
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 || record['__marScheduled'] === 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
- 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
- }
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
- external.externalSessionId === null ||
322
+ threadId === null ||
324
323
  !context.available)
325
324
  return { failureCode: 'RUNNER_UNAVAILABLE' };
326
325
  try {
327
- await this.resumeThread(external.externalSessionId, external.cwd);
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 { client } of this.environmentClients.values())
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
- const entry = [...this.environmentClients.values()].find((value) => value.client === client);
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
- const fingerprint = createHash('sha256')
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
- 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;
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 { client } of this.environmentClients.values())
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
- if (session.externalSessionId === null)
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
- const result = boundary === null
566
- ? await this.startThread(session.cwd, configuration)
567
- : await this.forkThread(session.externalSessionId, session.cwd, boundary, configuration);
568
- const threadId = extractId(result, 'thread');
569
- await this.setThreadName(threadId, title);
570
- 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
+ });
571
564
  }
572
565
  resumeThread(threadId, cwd, configuration, runId) {
573
566
  return this.clientFor(runId).resumeThread(threadId, cwd, configuration);