evolcore 0.0.22 → 0.0.23
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/AGENTS.md +62 -0
- package/CHANGELOG.md +24 -14
- package/README.md +2 -0
- package/bin/codex-managed-acl.ps1 +155 -0
- package/bin/install-codex-managed-hooks.mjs +373 -95
- package/dist/agents/baseagent-availability.js +48 -0
- package/dist/agents/baseagent.js +1 -1
- package/dist/agents/claude-runner.js +30 -226
- package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +73 -13
- package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
- package/dist/agents/codex/catalog.js +70 -0
- package/dist/agents/codex/diagnostics.js +154 -0
- package/dist/agents/codex-runner.js +407 -139
- package/dist/agents/ecagent-runner.js +10 -31
- package/dist/agents/gemini-runner.js +2 -6
- package/dist/aun/aid/store.js +2 -0
- package/dist/aun/menu-client.js +272 -0
- package/dist/aun/msg/group.js +65 -4
- package/dist/aun/msg/index.js +1 -1
- package/dist/aun/msg/managed-operation.js +51 -4
- package/dist/aun/rpc/client.js +8 -1
- package/dist/channels/aun.js +31 -1
- package/dist/channels/wechat.js +81 -10
- package/dist/cli/aun-commands.js +48 -4
- package/dist/cli/command-log.js +21 -2
- package/dist/cli/daemon-commands.js +142 -45
- package/dist/cli/index.js +20 -0
- package/dist/cli/init.js +2 -10
- package/dist/cli/menu-client.js +95 -0
- package/dist/cli/model.js +23 -8
- package/dist/cli/task-context.js +12 -2
- package/dist/cli/trigger-command.js +9 -1
- package/dist/config/schema-migration.js +14 -2
- package/dist/core/auth/agent-delegation.js +10 -7
- package/dist/core/auth/authorization-audit.js +4 -0
- package/dist/core/auth/operation-catalog.js +4 -0
- package/dist/core/auth/remote-access-store.js +135 -0
- package/dist/core/command/command-handler.js +41 -5
- package/dist/core/command/daemon-diagnostics.js +448 -0
- package/dist/core/command/menu-catalog.js +3 -0
- package/dist/core/command/menu-handler.js +141 -33
- package/dist/core/command/menu-protocol.js +119 -0
- package/dist/core/command/slash-gate.js +3 -3
- package/dist/core/command/slash-handler.js +42 -22
- package/dist/core/message/message-queue.js +20 -15
- package/dist/core/message/response-engine.js +74 -73
- package/dist/core/model/catalog-service.js +308 -0
- package/dist/core/model/model-catalog.js +124 -334
- package/dist/core/model/model-diagnostics.js +4 -5
- package/dist/core/model/model-fallback.js +9 -16
- package/dist/core/model/model-permission.js +1 -1
- package/dist/core/permission/approval-gateway.js +16 -1
- package/dist/core/permission/protected-paths.js +63 -4
- package/dist/core/permission/sandbox-runtime.js +4 -2
- package/dist/core/permission/tool-policy.js +244 -25
- package/dist/core/runtime-lock.js +13 -8
- package/dist/index.js +40 -29
- package/dist/ipc.js +123 -81
- package/dist/trigger/manager.js +3 -1
- package/dist/trigger/parser.js +77 -13
- package/dist/trigger/patch.js +104 -1
- package/dist/utils/ecweb-supervisor.js +11 -2
- package/dist/utils/ecweb-utils.js +8 -26
- package/dist/utils/error-utils.js +7 -0
- package/dist/utils/npm-ops.js +47 -15
- package/kits/docs/evolcore/INDEX.md +1 -0
- package/kits/docs/evolcore/agent-create.md +13 -0
- package/kits/docs/evolcore/group-collaboration.md +7 -2
- package/kits/docs/evolcore/menu-client.md +34 -0
- package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
- package/kits/migrations/migrate-role-config-v4.mjs +11 -5
- package/kits/migrations/migrate-role-config-v5.mjs +8 -2
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/bootstrap.md +8 -1
- package/package.json +11 -3
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
* Codex Agent Runner
|
|
3
|
-
*
|
|
4
|
-
* Integrates Codex app-server as an agent backend.
|
|
5
|
-
* Implements the same interface surface as AgentRunner (claude-runner.ts)
|
|
6
|
-
* so MessageProcessor and CommandHandler can work with it transparently.
|
|
7
|
-
*/
|
|
1
|
+
import { getRunnerCatalog, catalogEfforts, assertCatalogSelectable } from '../core/model/model-catalog.js';
|
|
8
2
|
import { hasTrustedFullAccessContext } from './runner-types.js';
|
|
9
3
|
import { requestDangerousCommandPermission } from '../core/permission/approval-gateway.js';
|
|
10
4
|
import { checkReadonly, checkDangerousCommand, evaluateToolPreflight } from '../core/permission/tool-policy.js';
|
|
11
5
|
import { normalizeExecutionPermissionMode, normalizePermissionMode, resolvePhaseOneExecutionSandbox } from '../core/permission/mode.js';
|
|
12
6
|
import { buildCodexProtectedFilesystemRules, isSameOrDescendant, resolveProtectedCandidate } from '../core/permission/protected-paths.js';
|
|
13
|
-
import { CodexAppServerClient } from './codex
|
|
7
|
+
import { CODEX_THREAD_ACTIVE_WRITER, CodexAppServerClient, } from './codex/app-server-client.js';
|
|
14
8
|
import { resolveOpenaiConfig } from './baseagent.js';
|
|
15
9
|
import { logger } from '../utils/logger.js';
|
|
16
10
|
import { summarizeToolInputForAudit } from '../utils/tool-summary.js';
|
|
@@ -47,6 +41,7 @@ export function formatApprovalLogSummary(summary) {
|
|
|
47
41
|
return singleLine;
|
|
48
42
|
return singleLine.slice(0, APPROVAL_LOG_SUMMARY_MAX_LENGTH - 3).trimEnd() + '...';
|
|
49
43
|
}
|
|
44
|
+
// ── Codex 模型目录(动态获取,含 effort) ──
|
|
50
45
|
class AsyncEventQueue {
|
|
51
46
|
queue = [];
|
|
52
47
|
done = false;
|
|
@@ -94,6 +89,7 @@ function errorCodeForToolDenial(decisionSource, policyCode) {
|
|
|
94
89
|
return 'INVALID_ARGUMENT';
|
|
95
90
|
return 'EXECUTION_FAILED';
|
|
96
91
|
}
|
|
92
|
+
const CODEX_THREAD_HANDOFF_TIMEOUT_MS = 2_000;
|
|
97
93
|
const managedTempDirsForExitCleanup = new Map();
|
|
98
94
|
let managedTempExitCleanupRegistered = false;
|
|
99
95
|
function isTrustedManagedTempDirectory(directory, parent) {
|
|
@@ -109,9 +105,9 @@ function isTrustedManagedTempDirectory(directory, parent) {
|
|
|
109
105
|
const stat = fs.lstatSync(candidate);
|
|
110
106
|
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
111
107
|
return false;
|
|
112
|
-
if (typeof process.getuid === 'function' && stat.uid !== process.getuid())
|
|
108
|
+
if (process.platform !== 'win32' && typeof process.getuid === 'function' && stat.uid !== process.getuid())
|
|
113
109
|
return false;
|
|
114
|
-
return (stat.mode & 0o077) === 0;
|
|
110
|
+
return process.platform === 'win32' || (stat.mode & 0o077) === 0;
|
|
115
111
|
};
|
|
116
112
|
if (!trustedDirectory(parentPath) || !trustedDirectory(directoryPath))
|
|
117
113
|
return false;
|
|
@@ -218,20 +214,17 @@ function outputTextForManagedHook(value) {
|
|
|
218
214
|
}
|
|
219
215
|
return String(value);
|
|
220
216
|
}
|
|
221
|
-
const CODEX_CATALOG_FALLBACK = [
|
|
222
|
-
{ slug: 'gpt-5.5', efforts: ['low', 'medium', 'high', 'xhigh'] },
|
|
223
|
-
{ slug: 'gpt-5.4', efforts: ['low', 'medium', 'high', 'xhigh'] },
|
|
224
|
-
{ slug: 'gpt-5.4-mini', efforts: ['low', 'medium', 'high', 'xhigh'] },
|
|
225
|
-
{ slug: 'gpt-5.3-codex', efforts: ['low', 'medium', 'high', 'xhigh'] },
|
|
226
|
-
{ slug: 'gpt-5.2', efforts: ['low', 'medium', 'high', 'xhigh'] },
|
|
227
|
-
];
|
|
228
217
|
const CODEX_DELEGATION_CARRIER_METADATA_KEY = '__codexDelegationCarrierV1';
|
|
229
218
|
const CODEX_NO_ROLLOUT_RE = /no rollout found for thread id/i;
|
|
219
|
+
const CODEX_ACTIVE_WRITER_RE = /already has an active writer/i;
|
|
230
220
|
function isCodexNoRolloutError(error) {
|
|
231
221
|
return error?.code === 'CODEX_ROLLOUT_NOT_FOUND'
|
|
232
222
|
|| CODEX_NO_ROLLOUT_RE.test(error instanceof Error ? error.message : String(error));
|
|
233
223
|
}
|
|
234
|
-
|
|
224
|
+
function isCodexActiveWriterError(error) {
|
|
225
|
+
return error?.code === CODEX_THREAD_ACTIVE_WRITER
|
|
226
|
+
|| CODEX_ACTIVE_WRITER_RE.test(error instanceof Error ? error.message : String(error));
|
|
227
|
+
}
|
|
235
228
|
// The permission bridge is version-locked to the audited approval schema and
|
|
236
229
|
// managed PreToolUse hook behavior used by proactive first-tool enforcement.
|
|
237
230
|
export const MIN_CODEX_CLI_VERSION = '0.144.1';
|
|
@@ -286,37 +279,6 @@ export function getCodexAppServerAvailability() {
|
|
|
286
279
|
export function isCodexAppServerAvailable() {
|
|
287
280
|
return getCodexAppServerAvailability().available;
|
|
288
281
|
}
|
|
289
|
-
function fetchCodexCatalog() {
|
|
290
|
-
if (codexCatalogCache)
|
|
291
|
-
return codexCatalogCache;
|
|
292
|
-
try {
|
|
293
|
-
const output = execCodexCliSync(['debug', 'models'], {
|
|
294
|
-
encoding: 'utf-8',
|
|
295
|
-
timeout: 5000,
|
|
296
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
297
|
-
});
|
|
298
|
-
const catalog = JSON.parse(output);
|
|
299
|
-
const models = catalog.models
|
|
300
|
-
.filter(m => m.visibility === 'list')
|
|
301
|
-
.map(m => ({
|
|
302
|
-
slug: m.slug,
|
|
303
|
-
efforts: (m.supported_reasoning_levels || []).map(l => l.effort),
|
|
304
|
-
}));
|
|
305
|
-
if (models.length > 0) {
|
|
306
|
-
codexCatalogCache = models;
|
|
307
|
-
return models;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
catch (e) {
|
|
311
|
-
logger.debug(`[CodexRunner] Failed to fetch model catalog, using fallback: ${e}`);
|
|
312
|
-
}
|
|
313
|
-
return CODEX_CATALOG_FALLBACK;
|
|
314
|
-
}
|
|
315
|
-
export function getCodexEfforts(model) {
|
|
316
|
-
const catalog = fetchCodexCatalog();
|
|
317
|
-
const entry = catalog.find(m => m.slug === model);
|
|
318
|
-
return entry?.efforts ?? catalog[0]?.efforts ?? ['low', 'medium', 'high'];
|
|
319
|
-
}
|
|
320
282
|
// ── Codex Runner ──
|
|
321
283
|
export class CodexRunner {
|
|
322
284
|
name = 'codex';
|
|
@@ -347,7 +309,12 @@ export class CodexRunner {
|
|
|
347
309
|
steeringTails = new Map();
|
|
348
310
|
threadOperationLocks = new Map();
|
|
349
311
|
activeOperationReleases = new Map();
|
|
312
|
+
/** Last app-server client that loaded each thread, including unsubscribed threads. */
|
|
313
|
+
threadSubscriptions = new Map();
|
|
314
|
+
/** Ordinary sessions stay behind the EvolCore H/L filesystem guard. */
|
|
350
315
|
appServerClient = null;
|
|
316
|
+
/** Trusted fullaccess calls use a separate unmasked app-server process. */
|
|
317
|
+
fullAccessAppServerClient = null;
|
|
351
318
|
onSessionIdUpdate;
|
|
352
319
|
onCompactStart;
|
|
353
320
|
permissionGateway;
|
|
@@ -388,24 +355,203 @@ export class CodexRunner {
|
|
|
388
355
|
this.effort = this.resolvedConfig.effort;
|
|
389
356
|
this.onSessionIdUpdate = callbacks.onSessionIdUpdate;
|
|
390
357
|
}
|
|
391
|
-
getAppServerClient() {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
358
|
+
getAppServerClient(fullAccess = false) {
|
|
359
|
+
const existing = fullAccess ? this.fullAccessAppServerClient : this.appServerClient;
|
|
360
|
+
if (existing)
|
|
361
|
+
return existing;
|
|
362
|
+
const client = new CodexAppServerClient({
|
|
363
|
+
apiKey: this.resolvedConfig.apiKey,
|
|
364
|
+
baseUrl: this.resolvedConfig.baseUrl,
|
|
365
|
+
provider: this.resolvedConfig.provider,
|
|
366
|
+
headers: this.resolvedConfig.headers,
|
|
367
|
+
queryParams: this.resolvedConfig.queryParams,
|
|
368
|
+
model: this.model,
|
|
369
|
+
effort: this.effort,
|
|
370
|
+
enableRequestUserInput: this.resolvedConfig.enableRequestUserInput,
|
|
371
|
+
approvalsReviewer: 'user',
|
|
372
|
+
protectEvolcoreFilesystem: !fullAccess,
|
|
373
|
+
// A completed turn is explicitly unsubscribed below. This is only an
|
|
374
|
+
// optimization on newer Codex versions; old versions retain the thread
|
|
375
|
+
// until the cross-client archive fallback runs.
|
|
376
|
+
onServerRequest: request => this.handleAppServerRequest(request),
|
|
377
|
+
});
|
|
378
|
+
client.onNotification(notification => {
|
|
379
|
+
this.trackApprovalItemNotification(notification);
|
|
380
|
+
if (notification.method !== 'thread/closed')
|
|
381
|
+
return;
|
|
382
|
+
const params = (notification.params || {});
|
|
383
|
+
const threadId = params.threadId ?? params.thread_id;
|
|
384
|
+
if (typeof threadId !== 'string')
|
|
385
|
+
return;
|
|
386
|
+
const subscription = this.threadSubscriptions.get(threadId);
|
|
387
|
+
if (subscription?.client === client) {
|
|
388
|
+
// Keep the last client identity so a later same-mode turn resumes on
|
|
389
|
+
// the same app-server even after the runtime has fully unloaded.
|
|
390
|
+
this.threadSubscriptions.set(threadId, { client, state: 'unsubscribed' });
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
if (fullAccess) {
|
|
394
|
+
this.fullAccessAppServerClient = client;
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
406
397
|
this.appServerClient = client;
|
|
407
398
|
}
|
|
408
|
-
return
|
|
399
|
+
return client;
|
|
400
|
+
}
|
|
401
|
+
getAppServerClientForThread(threadId) {
|
|
402
|
+
const subscribed = this.threadSubscriptions.get(threadId);
|
|
403
|
+
if (subscribed)
|
|
404
|
+
return subscribed.client;
|
|
405
|
+
const sessionId = this.findSessionKeyByThread(threadId);
|
|
406
|
+
return this.getAppServerClient(sessionId ? this.chatModes.get(sessionId) === 'fullaccess' : false);
|
|
407
|
+
}
|
|
408
|
+
markThreadSubscribed(client, threadId) {
|
|
409
|
+
const existing = this.threadSubscriptions.get(threadId);
|
|
410
|
+
if (existing?.client === client && existing.state === 'subscribed' && !existing.release)
|
|
411
|
+
return;
|
|
412
|
+
this.threadSubscriptions.set(threadId, { client, state: 'subscribed' });
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Release only one thread from one app-server connection. Unsubscribe is
|
|
416
|
+
* intentionally not treated as an immediate unload: current Codex builds can
|
|
417
|
+
* keep an unsubscribed thread loaded for a long grace period. The owner therefore
|
|
418
|
+
* remains in the map until a different client successfully resumes it.
|
|
419
|
+
*/
|
|
420
|
+
async releaseThreadSubscription(client, threadId, reason, options = {}) {
|
|
421
|
+
const existing = this.threadSubscriptions.get(threadId);
|
|
422
|
+
if (existing && existing.client !== client)
|
|
423
|
+
return false;
|
|
424
|
+
if (existing?.release)
|
|
425
|
+
return await existing.release.catch(error => {
|
|
426
|
+
logger.warn(`[CodexRunner] Existing thread release failed: thread=${threadId} `
|
|
427
|
+
+ `error=${error instanceof Error ? error.message : String(error)}`);
|
|
428
|
+
return false;
|
|
429
|
+
});
|
|
430
|
+
if (existing?.state === 'unsubscribed')
|
|
431
|
+
return true;
|
|
432
|
+
let releasePromise;
|
|
433
|
+
releasePromise = Promise.resolve().then(async () => {
|
|
434
|
+
let released = false;
|
|
435
|
+
try {
|
|
436
|
+
const unsubscribe = client.threadUnsubscribe;
|
|
437
|
+
if (typeof unsubscribe === 'function') {
|
|
438
|
+
if (options.waitForClose && typeof client.threadUnsubscribeAndWait === 'function') {
|
|
439
|
+
try {
|
|
440
|
+
await client.threadUnsubscribeAndWait.call(client, threadId, options.timeoutMs ?? CODEX_THREAD_HANDOFF_TIMEOUT_MS);
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
// The unsubscribe RPC has already succeeded when this is a
|
|
444
|
+
// close-timeout. Keep the owner marked unsubscribed and let the
|
|
445
|
+
// target resume decide whether the archive fallback is needed.
|
|
446
|
+
if (error?.code !== 'CODEX_THREAD_CLOSE_TIMEOUT')
|
|
447
|
+
throw error;
|
|
448
|
+
logger.debug(`[CodexRunner] thread/closed not observed before handoff timeout: `
|
|
449
|
+
+ `thread=${threadId} reason=${reason}`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
await unsubscribe.call(client, threadId);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
logger.debug(`[CodexRunner] Released thread subscription: thread=${threadId} reason=${reason}`);
|
|
457
|
+
released = true;
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
logger.warn(`[CodexRunner] Failed to release thread subscription: thread=${threadId} reason=${reason} `
|
|
462
|
+
+ `error=${error instanceof Error ? error.message : String(error)}`);
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
finally {
|
|
466
|
+
const current = this.threadSubscriptions.get(threadId);
|
|
467
|
+
if (current?.client === client && current.release === releasePromise) {
|
|
468
|
+
if (released) {
|
|
469
|
+
this.threadSubscriptions.set(threadId, { client, state: 'unsubscribed' });
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
this.threadSubscriptions.set(threadId, { client, state: current.state });
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
this.threadSubscriptions.set(threadId, {
|
|
478
|
+
client,
|
|
479
|
+
state: existing?.state ?? 'subscribed',
|
|
480
|
+
release: releasePromise,
|
|
481
|
+
});
|
|
482
|
+
return await releasePromise;
|
|
483
|
+
}
|
|
484
|
+
async prepareThreadSubscription(client, threadId) {
|
|
485
|
+
const existing = this.threadSubscriptions.get(threadId);
|
|
486
|
+
if (!existing)
|
|
487
|
+
return undefined;
|
|
488
|
+
if (existing.client === client) {
|
|
489
|
+
if (existing.release)
|
|
490
|
+
await existing.release.catch(() => undefined);
|
|
491
|
+
return undefined;
|
|
492
|
+
}
|
|
493
|
+
if (existing.state === 'subscribed') {
|
|
494
|
+
const released = await this.releaseThreadSubscription(existing.client, threadId, 'mode-handoff', {
|
|
495
|
+
waitForClose: true,
|
|
496
|
+
timeoutMs: CODEX_THREAD_HANDOFF_TIMEOUT_MS,
|
|
497
|
+
});
|
|
498
|
+
if (!released) {
|
|
499
|
+
const error = new Error(`Codex thread ${threadId} is still owned by another app-server connection`);
|
|
500
|
+
error.code = CODEX_THREAD_ACTIVE_WRITER;
|
|
501
|
+
throw error;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return existing.client;
|
|
505
|
+
}
|
|
506
|
+
async archiveThreadForHandoff(sourceClient, targetClient, threadId) {
|
|
507
|
+
const existing = this.threadSubscriptions.get(threadId);
|
|
508
|
+
if (existing && existing.client !== sourceClient) {
|
|
509
|
+
const error = new Error(`Codex thread ${threadId} ownership changed during handoff`);
|
|
510
|
+
error.code = CODEX_THREAD_ACTIVE_WRITER;
|
|
511
|
+
throw error;
|
|
512
|
+
}
|
|
513
|
+
const sessionId = this.findSessionKeyByThread(threadId);
|
|
514
|
+
if ([...this.activeSubagentTurns.values()].some(active => active.sessionId === sessionId && active.parentThreadId === threadId)) {
|
|
515
|
+
const error = new Error(`Codex thread ${threadId} has an active descendant turn`);
|
|
516
|
+
error.code = CODEX_THREAD_ACTIVE_WRITER;
|
|
517
|
+
throw error;
|
|
518
|
+
}
|
|
519
|
+
// Old app-server versions can acknowledge unsubscribe while retaining the
|
|
520
|
+
// writer. Archive is the only per-thread operation that synchronously
|
|
521
|
+
// tears down that runtime; the target client restores it before resume.
|
|
522
|
+
const archive = sourceClient.threadArchive;
|
|
523
|
+
const unarchive = targetClient.threadUnarchive;
|
|
524
|
+
if (typeof archive !== 'function' || typeof unarchive !== 'function') {
|
|
525
|
+
const error = new Error(`Codex thread ${threadId} requires archive/unarchive handoff support`);
|
|
526
|
+
error.code = CODEX_THREAD_ACTIVE_WRITER;
|
|
527
|
+
throw error;
|
|
528
|
+
}
|
|
529
|
+
let archived = false;
|
|
530
|
+
try {
|
|
531
|
+
await archive.call(sourceClient, threadId);
|
|
532
|
+
archived = true;
|
|
533
|
+
await unarchive.call(targetClient, threadId);
|
|
534
|
+
}
|
|
535
|
+
catch (error) {
|
|
536
|
+
if (archived) {
|
|
537
|
+
// Do not strand the user's rollout under archived_sessions when the
|
|
538
|
+
// target client fails midway through the compatibility handoff.
|
|
539
|
+
try {
|
|
540
|
+
await unarchive.call(sourceClient, threadId);
|
|
541
|
+
this.threadSubscriptions.set(threadId, { client: sourceClient, state: 'subscribed' });
|
|
542
|
+
}
|
|
543
|
+
catch (restoreError) {
|
|
544
|
+
logger.error(`[CodexRunner] Failed to restore archived thread after handoff failure: thread=${threadId} `
|
|
545
|
+
+ `error=${restoreError instanceof Error ? restoreError.message : String(restoreError)}`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
logger.warn(`[CodexRunner] Archive handoff failed: thread=${threadId} `
|
|
549
|
+
+ `error=${error instanceof Error ? error.message : String(error)}`);
|
|
550
|
+
const handoffError = new Error(`Codex thread ${threadId} is still owned by another app-server connection`);
|
|
551
|
+
handoffError.code = CODEX_THREAD_ACTIVE_WRITER;
|
|
552
|
+
throw handoffError;
|
|
553
|
+
}
|
|
554
|
+
this.threadSubscriptions.set(threadId, { client: targetClient, state: 'subscribed' });
|
|
409
555
|
}
|
|
410
556
|
createDelegationCarrier() {
|
|
411
557
|
return randomBytes(32).toString('base64url');
|
|
@@ -681,8 +827,11 @@ export class CodexRunner {
|
|
|
681
827
|
}
|
|
682
828
|
}
|
|
683
829
|
resetAppServerClient() {
|
|
684
|
-
const
|
|
830
|
+
const clients = [this.appServerClient, this.fullAccessAppServerClient]
|
|
831
|
+
.filter((client) => !!client);
|
|
685
832
|
this.appServerClient = null;
|
|
833
|
+
this.fullAccessAppServerClient = null;
|
|
834
|
+
this.threadSubscriptions.clear();
|
|
686
835
|
this.threadExternalToolFingerprints.clear();
|
|
687
836
|
this.threadExternalToolBoundaries.clear();
|
|
688
837
|
for (const childThreadId of this.childThreadSessions.keys())
|
|
@@ -695,9 +844,11 @@ export class CodexRunner {
|
|
|
695
844
|
this.trackedApprovalItems.clear();
|
|
696
845
|
this.pendingToolDenials.clear();
|
|
697
846
|
this.threadDelegationCarriers.clear();
|
|
698
|
-
client
|
|
699
|
-
|
|
700
|
-
|
|
847
|
+
for (const client of clients) {
|
|
848
|
+
client.close().catch(error => {
|
|
849
|
+
logger.debug(`[CodexRunner] Failed to close stale app-server client: ${error}`);
|
|
850
|
+
});
|
|
851
|
+
}
|
|
701
852
|
}
|
|
702
853
|
mergeThreadConfig(...configs) {
|
|
703
854
|
const merged = {};
|
|
@@ -945,19 +1096,16 @@ export class CodexRunner {
|
|
|
945
1096
|
// ── ModelSwitcher ──
|
|
946
1097
|
setModel(model) { this.model = model; this.resetAppServerClient(); }
|
|
947
1098
|
getModel() { return this.model; }
|
|
1099
|
+
getModelCatalog() {
|
|
1100
|
+
return getRunnerCatalog('codex', this.resolvedConfig.agentAid, this.resolvedConfig);
|
|
1101
|
+
}
|
|
948
1102
|
async listModels() {
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
return ids;
|
|
956
|
-
}
|
|
957
|
-
catch (error) {
|
|
958
|
-
logger.debug(`[CodexRunner] app-server model/list failed, using catalog fallback: ${error}`);
|
|
959
|
-
}
|
|
960
|
-
return fetchCodexCatalog().map(m => m.slug);
|
|
1103
|
+
const catalog = await this.getModelCatalog();
|
|
1104
|
+
assertCatalogSelectable(catalog);
|
|
1105
|
+
return catalog.models.map(m => m.id);
|
|
1106
|
+
}
|
|
1107
|
+
async getAvailableEfforts(model) {
|
|
1108
|
+
return catalogEfforts(await this.getModelCatalog(), model, 'codex');
|
|
961
1109
|
}
|
|
962
1110
|
// ── Effort ──
|
|
963
1111
|
setEffort(effort) { this.effort = effort; this.resetAppServerClient(); }
|
|
@@ -1014,7 +1162,9 @@ export class CodexRunner {
|
|
|
1014
1162
|
if (!looksLikeGitMetadataDirectory(resolved))
|
|
1015
1163
|
return;
|
|
1016
1164
|
paths.add(resolved);
|
|
1017
|
-
paths
|
|
1165
|
+
// Keep exact paths native, but Codex requires POSIX separators for
|
|
1166
|
+
// recursive filesystem globs even on Windows.
|
|
1167
|
+
paths.add(`${resolved}/**`);
|
|
1018
1168
|
};
|
|
1019
1169
|
try {
|
|
1020
1170
|
const stat = fs.statSync(marker);
|
|
@@ -1028,7 +1178,7 @@ export class CodexRunner {
|
|
|
1028
1178
|
}
|
|
1029
1179
|
}
|
|
1030
1180
|
catch { }
|
|
1031
|
-
for (const gitDirectory of [...paths].filter(candidate => !candidate.endsWith(
|
|
1181
|
+
for (const gitDirectory of [...paths].filter(candidate => !candidate.endsWith('/**'))) {
|
|
1032
1182
|
try {
|
|
1033
1183
|
const commonDirectory = fs.readFileSync(path.join(gitDirectory, 'commondir'), 'utf8').trim();
|
|
1034
1184
|
if (commonDirectory)
|
|
@@ -1137,7 +1287,7 @@ export class CodexRunner {
|
|
|
1137
1287
|
return;
|
|
1138
1288
|
}
|
|
1139
1289
|
const input = [{ type: 'text', text, text_elements: [] }];
|
|
1140
|
-
const client = this.
|
|
1290
|
+
const client = this.getAppServerClientForThread(activeTurn.threadId);
|
|
1141
1291
|
if (typeof client.turnSteer !== 'function') {
|
|
1142
1292
|
logger.warn(`[CodexRunner] turn steer unavailable in app-server client: session=${sessionId}`);
|
|
1143
1293
|
return;
|
|
@@ -1265,6 +1415,10 @@ export class CodexRunner {
|
|
|
1265
1415
|
const hadController = this.activeAbortControllers.has(key);
|
|
1266
1416
|
const hadTurn = this.activeTurns.has(key);
|
|
1267
1417
|
const provisionalThread = this.provisionalSessions.get(key);
|
|
1418
|
+
const subscribedThreadId = this.activeTurns.get(key)?.threadId
|
|
1419
|
+
?? this.activeSessions.get(key)
|
|
1420
|
+
?? provisionalThread;
|
|
1421
|
+
const subscription = subscribedThreadId ? this.threadSubscriptions.get(subscribedThreadId) : undefined;
|
|
1268
1422
|
this.activeStreams.delete(key);
|
|
1269
1423
|
this.activeAbortControllers.delete(key);
|
|
1270
1424
|
this.pendingInterrupts.delete(key);
|
|
@@ -1276,9 +1430,18 @@ export class CodexRunner {
|
|
|
1276
1430
|
}
|
|
1277
1431
|
this.chatModes.delete(key);
|
|
1278
1432
|
const release = this.activeOperationReleases.get(key);
|
|
1433
|
+
let releaseAfterSubscription;
|
|
1434
|
+
if (subscription) {
|
|
1435
|
+
releaseAfterSubscription = this.releaseThreadSubscription(subscription.client, subscribedThreadId, 'stream-cleanup');
|
|
1436
|
+
}
|
|
1279
1437
|
if (release) {
|
|
1280
1438
|
this.activeOperationReleases.delete(key);
|
|
1281
|
-
|
|
1439
|
+
if (releaseAfterSubscription) {
|
|
1440
|
+
void releaseAfterSubscription.then(release, release);
|
|
1441
|
+
}
|
|
1442
|
+
else {
|
|
1443
|
+
release();
|
|
1444
|
+
}
|
|
1282
1445
|
}
|
|
1283
1446
|
}
|
|
1284
1447
|
hasActiveStream(key) {
|
|
@@ -1455,7 +1618,7 @@ export class CodexRunner {
|
|
|
1455
1618
|
this.chatModes.set(sessionId, callMode);
|
|
1456
1619
|
const callApprovalPolicy = this.toApprovalPolicy(callMode);
|
|
1457
1620
|
const executionSandbox = this.buildExecutionSandboxOptions(sessionId, callMode, projectPath, modelOverride?.bootstrapAgentMdPath);
|
|
1458
|
-
const appServer = this.getAppServerClient();
|
|
1621
|
+
const appServer = this.getAppServerClient(callMode === 'fullaccess');
|
|
1459
1622
|
this.dropExpiredPendingInterrupt(sessionId);
|
|
1460
1623
|
const effectiveApprovalPolicy = callApprovalPolicy;
|
|
1461
1624
|
const capabilityConfig = await this.resolveCapabilityThreadConfig(projectPath);
|
|
@@ -1484,47 +1647,60 @@ export class CodexRunner {
|
|
|
1484
1647
|
approvalPolicy: effectiveApprovalPolicy,
|
|
1485
1648
|
approvalsReviewer: 'user',
|
|
1486
1649
|
...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
|
|
1487
|
-
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, runtimeEnv ? { shell_environment_policy: { set: runtimeEnv } } : undefined, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionId, delegationCarrierCandidate), this.buildLifecycleLockdownConfig(callMode)), callMode),
|
|
1650
|
+
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, runtimeEnv ? { shell_environment_policy: { set: runtimeEnv } } : undefined, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionId, delegationCarrierCandidate, callMode === 'fullaccess' ? runtimeEnv?.[AGENT_DELEGATION_TOKEN_ENV] : undefined), this.buildLifecycleLockdownConfig(callMode)), callMode),
|
|
1488
1651
|
...(developerInstructions ? { developerInstructions } : {}),
|
|
1489
1652
|
};
|
|
1490
1653
|
logger.info(`[CodexRunner] runQuery permMode=${requestedPermissionMode}->${callMode} ` +
|
|
1491
1654
|
`role=${executionSandbox.decision.role ?? 'none'} sandbox=${executionSandbox.decision.state}`);
|
|
1492
1655
|
let threadResponse;
|
|
1656
|
+
let previousAppServer;
|
|
1493
1657
|
if (agentSessionId) {
|
|
1658
|
+
// A thread may have been loaded by the other permission-mode client.
|
|
1659
|
+
// Release that one thread before asking this client to resume it.
|
|
1660
|
+
previousAppServer = await this.prepareThreadSubscription(appServer, agentSessionId);
|
|
1494
1661
|
try {
|
|
1495
1662
|
threadResponse = await appServer.threadResume(agentSessionId, projectPath, threadOptions);
|
|
1496
1663
|
}
|
|
1497
1664
|
catch (error) {
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1665
|
+
if (isCodexActiveWriterError(error) && previousAppServer && previousAppServer !== appServer) {
|
|
1666
|
+
// Some Codex builds acknowledge unsubscribe before unloading the
|
|
1667
|
+
// runtime. Archive/unarchive is a synchronous per-thread teardown
|
|
1668
|
+
// and preserves the same persisted thread id for the retry.
|
|
1669
|
+
await this.archiveThreadForHandoff(previousAppServer, appServer, agentSessionId);
|
|
1670
|
+
threadResponse = await appServer.threadResume(agentSessionId, projectPath, threadOptions);
|
|
1671
|
+
}
|
|
1672
|
+
else {
|
|
1673
|
+
// A legacy main session can retain a provider ID after its rollout was
|
|
1674
|
+
// removed externally. It is safe to recover before input submission;
|
|
1675
|
+
// topic bindings remain authoritative and fail closed.
|
|
1676
|
+
if (topicBinding || !isCodexNoRolloutError(error))
|
|
1677
|
+
throw error;
|
|
1678
|
+
if (typeof sessionManager?.clearMainSessionBackendIfMatches !== 'function')
|
|
1679
|
+
throw error;
|
|
1680
|
+
const missingThreadId = agentSessionId;
|
|
1681
|
+
const cleared = await sessionManager.clearMainSessionBackendIfMatches(sessionId, missingThreadId, [CODEX_DELEGATION_CARRIER_METADATA_KEY]);
|
|
1682
|
+
if (cleared !== 'cleared' && cleared !== 'already_clear') {
|
|
1683
|
+
const recoveryError = new Error(`Codex missing-rollout recovery rejected: ${cleared}`);
|
|
1684
|
+
recoveryError.code = 'CODEX_MISSING_ROLLOUT_RECOVERY_REJECTED';
|
|
1685
|
+
throw recoveryError;
|
|
1686
|
+
}
|
|
1687
|
+
if (this.activeSessions.get(sessionId) === missingThreadId)
|
|
1688
|
+
this.activeSessions.delete(sessionId);
|
|
1689
|
+
this.threadProjectPaths.delete(missingThreadId);
|
|
1690
|
+
this.threadDelegationCarriers.delete(missingThreadId);
|
|
1691
|
+
this.unbindExternalToolBoundary(missingThreadId);
|
|
1692
|
+
this.clearTrackedApprovalItemsForThread(missingThreadId);
|
|
1693
|
+
agentSessionId = undefined;
|
|
1694
|
+
resumingThread = false;
|
|
1695
|
+
knownDelegationCarrier = undefined;
|
|
1696
|
+
delegationCarrierCandidate = this.createDelegationCarrier();
|
|
1697
|
+
threadOptions = {
|
|
1698
|
+
...threadOptions,
|
|
1699
|
+
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, runtimeEnv ? { shell_environment_policy: { set: runtimeEnv } } : undefined, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionId, delegationCarrierCandidate, callMode === 'fullaccess' ? runtimeEnv?.[AGENT_DELEGATION_TOKEN_ENV] : undefined), this.buildLifecycleLockdownConfig(callMode)), callMode),
|
|
1700
|
+
};
|
|
1701
|
+
logger.warn(`[CodexRunner] recovered missing rollout before input submission: session=${sessionId} thread=${missingThreadId}`);
|
|
1702
|
+
threadResponse = await appServer.threadStart(projectPath, threadOptions);
|
|
1511
1703
|
}
|
|
1512
|
-
if (this.activeSessions.get(sessionId) === missingThreadId)
|
|
1513
|
-
this.activeSessions.delete(sessionId);
|
|
1514
|
-
this.threadProjectPaths.delete(missingThreadId);
|
|
1515
|
-
this.threadDelegationCarriers.delete(missingThreadId);
|
|
1516
|
-
this.unbindExternalToolBoundary(missingThreadId);
|
|
1517
|
-
this.clearTrackedApprovalItemsForThread(missingThreadId);
|
|
1518
|
-
agentSessionId = undefined;
|
|
1519
|
-
resumingThread = false;
|
|
1520
|
-
knownDelegationCarrier = undefined;
|
|
1521
|
-
delegationCarrierCandidate = this.createDelegationCarrier();
|
|
1522
|
-
threadOptions = {
|
|
1523
|
-
...threadOptions,
|
|
1524
|
-
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, runtimeEnv ? { shell_environment_policy: { set: runtimeEnv } } : undefined, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionId, delegationCarrierCandidate), this.buildLifecycleLockdownConfig(callMode)), callMode),
|
|
1525
|
-
};
|
|
1526
|
-
logger.warn(`[CodexRunner] recovered missing rollout before input submission: session=${sessionId} thread=${missingThreadId}`);
|
|
1527
|
-
threadResponse = await appServer.threadStart(projectPath, threadOptions);
|
|
1528
1704
|
}
|
|
1529
1705
|
}
|
|
1530
1706
|
else {
|
|
@@ -1533,6 +1709,7 @@ export class CodexRunner {
|
|
|
1533
1709
|
let threadId = threadResponse.thread?.id || agentSessionId;
|
|
1534
1710
|
if (!threadId)
|
|
1535
1711
|
throw new Error('Codex app-server did not return a thread id');
|
|
1712
|
+
this.markThreadSubscribed(appServer, threadId);
|
|
1536
1713
|
if (!resumingThread) {
|
|
1537
1714
|
carrierToPublish = { threadId, carrier: delegationCarrierCandidate };
|
|
1538
1715
|
}
|
|
@@ -1559,6 +1736,9 @@ export class CodexRunner {
|
|
|
1559
1736
|
const forkedThreadId = forked.thread?.id;
|
|
1560
1737
|
if (!forkedThreadId)
|
|
1561
1738
|
throw new Error('Codex delegation-carrier recovery fork did not return a thread id');
|
|
1739
|
+
this.markThreadSubscribed(appServer, forkedThreadId);
|
|
1740
|
+
await this.releaseThreadSubscription(appServer, staleThreadId, 'carrier-recovery-fork-source');
|
|
1741
|
+
await this.releaseThreadSubscription(appServer, forkedThreadId, 'carrier-recovery-fork');
|
|
1562
1742
|
threadId = forkedThreadId;
|
|
1563
1743
|
recoveredCarrier = delegationCarrierCandidate;
|
|
1564
1744
|
}
|
|
@@ -1591,6 +1771,7 @@ export class CodexRunner {
|
|
|
1591
1771
|
if (!bindingAccepted) {
|
|
1592
1772
|
const error = new Error(`backend discovery rejected: ${bindingResult}`);
|
|
1593
1773
|
error.code = 'TOPIC_BACKEND_DISCOVERY_REJECTED';
|
|
1774
|
+
await this.releaseThreadSubscription(appServer, threadId, 'backend-binding-rejected');
|
|
1594
1775
|
throw error;
|
|
1595
1776
|
}
|
|
1596
1777
|
this.activeSessions.set(sessionId, threadId);
|
|
@@ -1633,7 +1814,7 @@ export class CodexRunner {
|
|
|
1633
1814
|
&& this.childThreadSessions.get(notifThreadId) === sessionId;
|
|
1634
1815
|
if (notifThreadId !== undefined && notifThreadId !== threadId && !emittedByChild)
|
|
1635
1816
|
return;
|
|
1636
|
-
this.trackSubagentLifecycle(notification, state);
|
|
1817
|
+
this.trackSubagentLifecycle(notification, state, appServer);
|
|
1637
1818
|
if (emittedByChild && notifThreadId !== threadId) {
|
|
1638
1819
|
const childTurnId = this.extractTurnId(notification);
|
|
1639
1820
|
if (notification.method === 'turn/started' && childTurnId) {
|
|
@@ -1649,6 +1830,7 @@ export class CodexRunner {
|
|
|
1649
1830
|
this.activeSubagentTurns.delete(notifThreadId);
|
|
1650
1831
|
}
|
|
1651
1832
|
state.activeSubagentThreads.delete(notifThreadId);
|
|
1833
|
+
void this.releaseThreadSubscription(appServer, notifThreadId, 'subagent-turn-finished');
|
|
1652
1834
|
this.unregisterChildThread(notifThreadId);
|
|
1653
1835
|
this.endAppServerQueueWhenSubagentsDone(queue, state);
|
|
1654
1836
|
}
|
|
@@ -1689,7 +1871,7 @@ export class CodexRunner {
|
|
|
1689
1871
|
if (!resumingThread)
|
|
1690
1872
|
this.cleanupProvisionalThread(sessionId, threadId);
|
|
1691
1873
|
logger.info(`[CodexRunner] Applied pending interrupt before turn start: ${sessionId}`);
|
|
1692
|
-
return this.transformAppServerStream(queue, sessionId, state, controller, unsubscribe, tempFiles);
|
|
1874
|
+
return this.transformAppServerStream(queue, sessionId, state, controller, unsubscribe, appServer, tempFiles);
|
|
1693
1875
|
}
|
|
1694
1876
|
try {
|
|
1695
1877
|
const turnResponse = await appServer.turnStart(threadId, input, {
|
|
@@ -1721,7 +1903,7 @@ export class CodexRunner {
|
|
|
1721
1903
|
}
|
|
1722
1904
|
const status = turnResponse.turn?.status;
|
|
1723
1905
|
if (status === 'completed' || status === 'failed') {
|
|
1724
|
-
this.primeSubagentStateFromTurn(turnResponse.turn, state);
|
|
1906
|
+
this.primeSubagentStateFromTurn(turnResponse.turn, state, appServer);
|
|
1725
1907
|
queue.push({ method: 'turn/completed', params: { threadId, turn: turnResponse.turn } });
|
|
1726
1908
|
state.turnCompleted = true;
|
|
1727
1909
|
this.endAppServerQueueWhenSubagentsDone(queue, state);
|
|
@@ -1729,6 +1911,7 @@ export class CodexRunner {
|
|
|
1729
1911
|
}
|
|
1730
1912
|
catch (error) {
|
|
1731
1913
|
unsubscribe();
|
|
1914
|
+
await this.releaseThreadSubscription(appServer, threadId, 'turn-start-failed');
|
|
1732
1915
|
this.activeAbortControllers.delete(sessionId);
|
|
1733
1916
|
this.activeTurns.delete(sessionId);
|
|
1734
1917
|
this.pendingInterrupts.delete(sessionId);
|
|
@@ -1737,7 +1920,7 @@ export class CodexRunner {
|
|
|
1737
1920
|
this.cleanupProvisionalThread(sessionId, threadId);
|
|
1738
1921
|
throw error;
|
|
1739
1922
|
}
|
|
1740
|
-
return this.transformAppServerStream(queue, sessionId, state, controller, unsubscribe, tempFiles);
|
|
1923
|
+
return this.transformAppServerStream(queue, sessionId, state, controller, unsubscribe, appServer, tempFiles);
|
|
1741
1924
|
}
|
|
1742
1925
|
// ── Interrupt ──
|
|
1743
1926
|
async interrupt(sessionKey) {
|
|
@@ -1789,7 +1972,12 @@ export class CodexRunner {
|
|
|
1789
1972
|
}
|
|
1790
1973
|
async interruptAppServerTurn(threadId, turnId) {
|
|
1791
1974
|
try {
|
|
1792
|
-
|
|
1975
|
+
const sessionId = this.findSessionKeyByThread(threadId);
|
|
1976
|
+
const subscribed = this.threadSubscriptions.get(threadId);
|
|
1977
|
+
const client = subscribed
|
|
1978
|
+
? subscribed.client
|
|
1979
|
+
: this.getAppServerClient(sessionId ? this.chatModes.get(sessionId) === 'fullaccess' : false);
|
|
1980
|
+
await client.turnInterrupt(threadId, turnId);
|
|
1793
1981
|
}
|
|
1794
1982
|
catch (error) {
|
|
1795
1983
|
logger.debug(`[CodexRunner] app-server turn interrupt failed: ${error}`);
|
|
@@ -1853,11 +2041,16 @@ export class CodexRunner {
|
|
|
1853
2041
|
await this.interruptSubagentTurns(sessionId);
|
|
1854
2042
|
}
|
|
1855
2043
|
capturedController?.abort('Session closed');
|
|
2044
|
+
const capturedSubscription = capturedThreadId ? this.threadSubscriptions.get(capturedThreadId) : undefined;
|
|
1856
2045
|
const lifecycleReplaced = (capturedStream !== undefined && this.activeStreams.get(sessionId) !== capturedStream)
|
|
1857
2046
|
|| (capturedController !== undefined && this.activeAbortControllers.get(sessionId) !== capturedController)
|
|
1858
2047
|
|| (capturedTurn !== undefined && this.activeTurns.get(sessionId) !== capturedTurn)
|
|
1859
2048
|
|| (capturedPermissionContext !== undefined && this.permissionContexts.get(sessionId) !== capturedPermissionContext)
|
|
1860
2049
|
|| (capturedThreadId !== undefined && this.activeSessions.get(sessionId) !== capturedThreadId);
|
|
2050
|
+
if (capturedSubscription && !lifecycleReplaced
|
|
2051
|
+
&& this.threadSubscriptions.get(capturedThreadId) === capturedSubscription) {
|
|
2052
|
+
await this.releaseThreadSubscription(capturedSubscription.client, capturedThreadId, 'session-closed');
|
|
2053
|
+
}
|
|
1861
2054
|
if (capturedStream !== undefined && this.activeStreams.get(sessionId) === capturedStream)
|
|
1862
2055
|
this.activeStreams.delete(sessionId);
|
|
1863
2056
|
if (capturedController !== undefined && this.activeAbortControllers.get(sessionId) === capturedController)
|
|
@@ -1913,6 +2106,10 @@ export class CodexRunner {
|
|
|
1913
2106
|
async clearSession(sessionId, _agentSessionId, _projectPath) {
|
|
1914
2107
|
// Codex: 清空会话 = 下次 runQuery 不传 resumeId,自动创建新 thread
|
|
1915
2108
|
const threadId = this.activeSessions.get(sessionId) ?? _agentSessionId;
|
|
2109
|
+
const subscription = this.threadSubscriptions.get(threadId);
|
|
2110
|
+
if (subscription) {
|
|
2111
|
+
await this.releaseThreadSubscription(subscription.client, threadId, 'session-cleared');
|
|
2112
|
+
}
|
|
1916
2113
|
this.activeSessions.delete(sessionId);
|
|
1917
2114
|
this.activeTurns.delete(sessionId);
|
|
1918
2115
|
this.steeringTails.delete(sessionId);
|
|
@@ -1933,8 +2130,12 @@ export class CodexRunner {
|
|
|
1933
2130
|
return result;
|
|
1934
2131
|
};
|
|
1935
2132
|
const release = await this.acquireThreadOperation(this.threadOperationKey(agentSessionId, _sessionId), 'compact');
|
|
2133
|
+
let compactAppServer;
|
|
1936
2134
|
try {
|
|
1937
|
-
const
|
|
2135
|
+
const compactMode = normalizeExecutionPermissionMode(modelOverride?.permissionMode ?? this.chatModes.get(_sessionId) ?? this.currentMode);
|
|
2136
|
+
const appServer = this.getAppServerClient(compactMode === 'fullaccess');
|
|
2137
|
+
compactAppServer = appServer;
|
|
2138
|
+
await this.prepareThreadSubscription(appServer, agentSessionId);
|
|
1938
2139
|
this.onCompactStart?.(_sessionId);
|
|
1939
2140
|
try {
|
|
1940
2141
|
await this.startAndWaitForCompact(appServer, agentSessionId);
|
|
@@ -1944,7 +2145,6 @@ export class CodexRunner {
|
|
|
1944
2145
|
if (!this.isThreadNotFoundError(error))
|
|
1945
2146
|
throw error;
|
|
1946
2147
|
logger.info(`[CodexRunner] Compact thread not loaded, resuming before compact: ${agentSessionId}`);
|
|
1947
|
-
const compactMode = normalizeExecutionPermissionMode(modelOverride?.permissionMode ?? this.chatModes.get(_sessionId) ?? this.currentMode);
|
|
1948
2148
|
const compactModel = modelOverride?.model || this.model;
|
|
1949
2149
|
const compactEffort = modelOverride?.effortMode === 'model_default'
|
|
1950
2150
|
? undefined
|
|
@@ -1967,6 +2167,7 @@ export class CodexRunner {
|
|
|
1967
2167
|
...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
|
|
1968
2168
|
config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(_sessionId, compactDelegationCarrier), this.buildLifecycleLockdownConfig(compactMode)), compactMode),
|
|
1969
2169
|
});
|
|
2170
|
+
this.markThreadSubscribed(appServer, agentSessionId);
|
|
1970
2171
|
this.threadProjectPaths.set(agentSessionId, path.resolve(_projectPath));
|
|
1971
2172
|
this.threadDelegationCarriers.set(agentSessionId, compactDelegationCarrier);
|
|
1972
2173
|
this.bindExternalToolBoundary(agentSessionId, externalToolConfig);
|
|
@@ -1979,6 +2180,9 @@ export class CodexRunner {
|
|
|
1979
2180
|
return finish({ ok: false, code: 'sdk_error', message, durationMs: Date.now() - startedAt });
|
|
1980
2181
|
}
|
|
1981
2182
|
finally {
|
|
2183
|
+
if (compactAppServer) {
|
|
2184
|
+
await this.releaseThreadSubscription(compactAppServer, agentSessionId, 'compact-finished');
|
|
2185
|
+
}
|
|
1982
2186
|
release();
|
|
1983
2187
|
}
|
|
1984
2188
|
}
|
|
@@ -2119,7 +2323,8 @@ export class CodexRunner {
|
|
|
2119
2323
|
const mode = this.chatModes.get(sessionKey) ?? this.currentMode;
|
|
2120
2324
|
const executionSandbox = this.buildExecutionSandboxOptions(sessionKey, mode, projectPath);
|
|
2121
2325
|
const capabilityConfig = await this.resolveCapabilityThreadConfig(projectPath);
|
|
2122
|
-
const appServer = this.
|
|
2326
|
+
const appServer = this.threadSubscriptions.get(agentSessionId)?.client
|
|
2327
|
+
?? this.getAppServerClient(mode === 'fullaccess');
|
|
2123
2328
|
const externalToolConfig = await this.resolveExternalToolApprovalConfig(appServer, projectPath, capabilityConfig, mode);
|
|
2124
2329
|
if (sessionKey)
|
|
2125
2330
|
await this.assertProactiveHookAvailability(sessionKey, appServer);
|
|
@@ -2135,9 +2340,11 @@ export class CodexRunner {
|
|
|
2135
2340
|
const forkedThreadId = response.thread?.id;
|
|
2136
2341
|
if (!forkedThreadId)
|
|
2137
2342
|
throw new Error('Codex fork did not return a thread id');
|
|
2343
|
+
this.markThreadSubscribed(appServer, forkedThreadId);
|
|
2138
2344
|
this.threadDelegationCarriers.set(forkedThreadId, delegationCarrier);
|
|
2139
2345
|
this.threadProjectPaths.set(forkedThreadId, path.resolve(projectPath));
|
|
2140
2346
|
this.bindExternalToolBoundary(forkedThreadId, externalToolConfig);
|
|
2347
|
+
await this.releaseThreadSubscription(appServer, forkedThreadId, 'fork-finished');
|
|
2141
2348
|
return forkedThreadId;
|
|
2142
2349
|
}
|
|
2143
2350
|
async forkSessionAt(agentSessionId, projectPath, assistantMessageId, title) {
|
|
@@ -2167,14 +2374,14 @@ export class CodexRunner {
|
|
|
2167
2374
|
}
|
|
2168
2375
|
}
|
|
2169
2376
|
async setSessionName(agentSessionId, name) {
|
|
2170
|
-
return this.
|
|
2377
|
+
return this.getAppServerClientForThread(agentSessionId).threadSetName(agentSessionId, name);
|
|
2171
2378
|
}
|
|
2172
2379
|
async updateSessionMetadata(agentSessionId, metadata) {
|
|
2173
2380
|
const gitInfo = metadata?.gitInfo && typeof metadata.gitInfo === 'object' ? metadata.gitInfo : undefined;
|
|
2174
|
-
return this.
|
|
2381
|
+
return this.getAppServerClientForThread(agentSessionId).threadMetadataUpdate(agentSessionId, gitInfo);
|
|
2175
2382
|
}
|
|
2176
2383
|
async getSessionMessages(agentSessionId, projectPath) {
|
|
2177
|
-
const response = await this.
|
|
2384
|
+
const response = await this.getAppServerClientForThread(agentSessionId).threadRead(agentSessionId, true);
|
|
2178
2385
|
return this.mapThreadToSessionMessages(response, agentSessionId);
|
|
2179
2386
|
}
|
|
2180
2387
|
approvalItemKey(threadId, turnId, itemId) {
|
|
@@ -2406,6 +2613,10 @@ export class CodexRunner {
|
|
|
2406
2613
|
setImmediate(() => this.steerPolicyReason(sessionKey, 'fullaccess_context_untrusted', reason));
|
|
2407
2614
|
return this.toAppServerApprovalResponse(request.method, 'deny', toolInput, unattended);
|
|
2408
2615
|
}
|
|
2616
|
+
// Fullaccess uses the current task delegation token in its shell
|
|
2617
|
+
// environment, so it must not enter the ordinary workspace/carrier
|
|
2618
|
+
// approval path even if the app-server asks for an infrastructure
|
|
2619
|
+
// approval despite approvalPolicy=never.
|
|
2409
2620
|
const response = this.toAppServerApprovalResponse(request.method, 'allow', toolInput, false);
|
|
2410
2621
|
this.logAppServerApprovalAudit(request, sessionKey, toolName, 'allow', 'approval', undefined, 'trusted fullaccess execution');
|
|
2411
2622
|
return response;
|
|
@@ -2413,7 +2624,9 @@ export class CodexRunner {
|
|
|
2413
2624
|
// Keep the policy check in the approval path as a fail-closed fallback.
|
|
2414
2625
|
// Some app-server tool routes can reach approval without invoking the
|
|
2415
2626
|
// command hook; allowing here would bypass the proactive gate.
|
|
2416
|
-
const policyResult = this.
|
|
2627
|
+
const policyResult = (this.chatModes.get(sessionKey) ?? this.currentMode) === 'fullaccess'
|
|
2628
|
+
? undefined
|
|
2629
|
+
: this.permissionContexts.get(sessionKey)?.policyHook?.(toolName, toolInput);
|
|
2417
2630
|
if (policyResult?.block) {
|
|
2418
2631
|
logger.info(`[CodexRunner] app-server approval declined by session policy: session=${sessionKey} ` +
|
|
2419
2632
|
`requestId=${request.id ?? '<missing>'} code=${policyResult.policyCode ?? 'session_policy_hook'} ` +
|
|
@@ -2439,9 +2652,33 @@ export class CodexRunner {
|
|
|
2439
2652
|
}
|
|
2440
2653
|
const summary = this.summarizeAppServerRequest(request.method, params);
|
|
2441
2654
|
const reason = params.reason || params.decisionReason || undefined;
|
|
2655
|
+
const auditSummary = (summarizeToolInputForAudit(toolName, toolInput)
|
|
2656
|
+
|| summarizeToolInputForAudit('Bash', { command: summary })).slice(0, 512);
|
|
2657
|
+
const sessionMode = this.chatModes.get(sessionKey) ?? this.currentMode;
|
|
2442
2658
|
const workspacePath = this.resolvePermissionWorkspacePath(params);
|
|
2443
2659
|
if (!workspacePath) {
|
|
2444
2660
|
logger.warn(`[CodexRunner] approval denied because thread workspace is unknown: method=${request.method} thread=${params.threadId ?? params.conversationId ?? '<missing>'}`);
|
|
2661
|
+
try {
|
|
2662
|
+
await permissionContext?.recordExecutionAnomaly?.({
|
|
2663
|
+
code: 'operation_blocked',
|
|
2664
|
+
severity: 'warning',
|
|
2665
|
+
phase: 'execution',
|
|
2666
|
+
occurredAt: Date.now(),
|
|
2667
|
+
toolName,
|
|
2668
|
+
...(request.id !== undefined ? { requestId: String(request.id) } : {}),
|
|
2669
|
+
policyCode: 'approval_workspace_unknown',
|
|
2670
|
+
decisionSource: 'infrastructure',
|
|
2671
|
+
agentAid: permissionContext?.selfAid,
|
|
2672
|
+
agentName: permissionContext?.agentName,
|
|
2673
|
+
sessionId: sessionKey,
|
|
2674
|
+
permissionMode: normalizeExecutionPermissionMode(sessionMode),
|
|
2675
|
+
summary: auditSummary,
|
|
2676
|
+
effect: 'operation_skipped',
|
|
2677
|
+
});
|
|
2678
|
+
}
|
|
2679
|
+
catch (error) {
|
|
2680
|
+
logger.warn(`[CodexRunner] failed to record unknown-workspace approval denial: session=${sessionKey} error=${error instanceof Error ? error.message : String(error)}`);
|
|
2681
|
+
}
|
|
2445
2682
|
const response = this.toAppServerApprovalResponse(request.method, 'deny', toolInput, unattended);
|
|
2446
2683
|
this.rememberToolDenial(request, params, {
|
|
2447
2684
|
decision: 'deny',
|
|
@@ -2454,8 +2691,7 @@ export class CodexRunner {
|
|
|
2454
2691
|
return response;
|
|
2455
2692
|
}
|
|
2456
2693
|
const operationCwd = this.resolvePermissionOperationCwd(params, workspacePath, toolName);
|
|
2457
|
-
const
|
|
2458
|
-
const logSummary = formatApprovalLogSummary(summary);
|
|
2694
|
+
const logSummary = formatApprovalLogSummary(auditSummary || summary);
|
|
2459
2695
|
logger.info(`[CodexRunner] app-server approval request id=${request.id} method=${request.method} session=${sessionKey} mode=${sessionMode} tool=${toolName} summary=${logSummary}`);
|
|
2460
2696
|
const isCommandApproval = request.method === 'item/commandExecution/requestApproval'
|
|
2461
2697
|
|| request.method === 'execCommandApproval';
|
|
@@ -2567,6 +2803,16 @@ export class CodexRunner {
|
|
|
2567
2803
|
const params = (request.params || {});
|
|
2568
2804
|
const rawCallId = params.itemId ?? params.item_id ?? params.callId ?? params.call_id;
|
|
2569
2805
|
const callId = typeof rawCallId === 'string' && rawCallId ? rawCallId : undefined;
|
|
2806
|
+
const toolInput = this.buildPermissionInput(request.method, params);
|
|
2807
|
+
const rawSummary = summarizeToolInputForAudit(toolName, toolInput);
|
|
2808
|
+
const auditSummary = (rawSummary
|
|
2809
|
+
|| summarizeToolInputForAudit('Bash', { command: this.summarizeAppServerRequest(request.method, params) })).slice(0, 512);
|
|
2810
|
+
const threadId = this.requestThreadId(params);
|
|
2811
|
+
const rawCwd = typeof toolInput.cwd === 'string' && toolInput.cwd
|
|
2812
|
+
? toolInput.cwd
|
|
2813
|
+
: typeof params.cwd === 'string' && params.cwd
|
|
2814
|
+
? params.cwd
|
|
2815
|
+
: undefined;
|
|
2570
2816
|
auditCodexApprovalDecision({
|
|
2571
2817
|
requestId: request.id !== undefined ? String(request.id) : undefined,
|
|
2572
2818
|
callId,
|
|
@@ -2581,6 +2827,10 @@ export class CodexRunner {
|
|
|
2581
2827
|
policyCode,
|
|
2582
2828
|
permissionMode: normalizeExecutionPermissionMode(this.chatModes.get(sessionKey) ?? this.currentMode),
|
|
2583
2829
|
role: context?.role,
|
|
2830
|
+
taskId: context?.taskId,
|
|
2831
|
+
summary: auditSummary,
|
|
2832
|
+
threadId,
|
|
2833
|
+
cwd: rawCwd,
|
|
2584
2834
|
reason: reason ?? policyCode ?? (decision === 'allow' ? 'approval accepted' : 'approval denied'),
|
|
2585
2835
|
parseFailure,
|
|
2586
2836
|
});
|
|
@@ -2594,6 +2844,7 @@ export class CodexRunner {
|
|
|
2594
2844
|
decisionSource,
|
|
2595
2845
|
...(policyCode ? { policyCode } : {}),
|
|
2596
2846
|
...(parseFailure ? { parseFailure } : {}),
|
|
2847
|
+
...(auditSummary ? { summary: auditSummary } : {}),
|
|
2597
2848
|
reason: reason ?? policyCode ?? (decision === 'allow' ? 'approval accepted' : 'approval denied'),
|
|
2598
2849
|
executed: false,
|
|
2599
2850
|
})}`);
|
|
@@ -3364,7 +3615,7 @@ export class CodexRunner {
|
|
|
3364
3615
|
async rollbackSessionTurnsLocked(agentSessionId, numTurns) {
|
|
3365
3616
|
if (numTurns < 1)
|
|
3366
3617
|
return true;
|
|
3367
|
-
const response = await this.
|
|
3618
|
+
const response = await this.getAppServerClientForThread(agentSessionId).threadRollback(agentSessionId, numTurns);
|
|
3368
3619
|
return !!response.thread;
|
|
3369
3620
|
}
|
|
3370
3621
|
async rewindFiles(agentSessionId, projectPath, userMessageId) {
|
|
@@ -3611,7 +3862,7 @@ export class CodexRunner {
|
|
|
3611
3862
|
this.unregisterChildThread(childThreadId);
|
|
3612
3863
|
}
|
|
3613
3864
|
}
|
|
3614
|
-
trackSubagentLifecycle(notification, state) {
|
|
3865
|
+
trackSubagentLifecycle(notification, state, appServer) {
|
|
3615
3866
|
const params = (notification.params || {});
|
|
3616
3867
|
const item = params.item && typeof params.item === 'object' ? params.item : undefined;
|
|
3617
3868
|
if (!this.isSubagentActivityItem(item))
|
|
@@ -3623,13 +3874,17 @@ export class CodexRunner {
|
|
|
3623
3874
|
if (kind === 'started') {
|
|
3624
3875
|
state.activeSubagentThreads.add(childThreadId);
|
|
3625
3876
|
this.registerChildThread(state.sessionId, childThreadId, state.projectPath);
|
|
3877
|
+
if (appServer)
|
|
3878
|
+
this.markThreadSubscribed(appServer, childThreadId);
|
|
3626
3879
|
}
|
|
3627
3880
|
if (kind === 'completed' || kind === 'failed' || kind === 'stopped' || kind === 'killed') {
|
|
3628
3881
|
state.activeSubagentThreads.delete(childThreadId);
|
|
3882
|
+
if (appServer)
|
|
3883
|
+
void this.releaseThreadSubscription(appServer, childThreadId, 'subagent-finished');
|
|
3629
3884
|
this.unregisterChildThread(childThreadId);
|
|
3630
3885
|
}
|
|
3631
3886
|
}
|
|
3632
|
-
primeSubagentStateFromTurn(turn, state) {
|
|
3887
|
+
primeSubagentStateFromTurn(turn, state, appServer) {
|
|
3633
3888
|
for (const item of this.getTurnItems(turn)) {
|
|
3634
3889
|
if (!this.isSubagentActivityItem(item))
|
|
3635
3890
|
continue;
|
|
@@ -3640,9 +3895,13 @@ export class CodexRunner {
|
|
|
3640
3895
|
if (kind === 'started') {
|
|
3641
3896
|
state.activeSubagentThreads.add(childThreadId);
|
|
3642
3897
|
this.registerChildThread(state.sessionId, childThreadId, state.projectPath);
|
|
3898
|
+
if (appServer)
|
|
3899
|
+
this.markThreadSubscribed(appServer, childThreadId);
|
|
3643
3900
|
}
|
|
3644
3901
|
else if (kind === 'completed' || kind === 'failed' || kind === 'stopped' || kind === 'killed') {
|
|
3645
3902
|
state.activeSubagentThreads.delete(childThreadId);
|
|
3903
|
+
if (appServer)
|
|
3904
|
+
void this.releaseThreadSubscription(appServer, childThreadId, 'subagent-finished');
|
|
3646
3905
|
this.unregisterChildThread(childThreadId);
|
|
3647
3906
|
}
|
|
3648
3907
|
}
|
|
@@ -3657,7 +3916,7 @@ export class CodexRunner {
|
|
|
3657
3916
|
}
|
|
3658
3917
|
queue.end();
|
|
3659
3918
|
}
|
|
3660
|
-
async *transformAppServerStream(notifications, sessionId, state, controller, unsubscribe, tempFiles) {
|
|
3919
|
+
async *transformAppServerStream(notifications, sessionId, state, controller, unsubscribe, appServer, tempFiles) {
|
|
3661
3920
|
try {
|
|
3662
3921
|
let sessionIdEmitted = state.committed;
|
|
3663
3922
|
if (sessionIdEmitted)
|
|
@@ -3686,6 +3945,7 @@ export class CodexRunner {
|
|
|
3686
3945
|
}
|
|
3687
3946
|
finally {
|
|
3688
3947
|
unsubscribe();
|
|
3948
|
+
await this.releaseThreadSubscription(appServer, state.threadId, 'turn-finished');
|
|
3689
3949
|
if (this.activeAbortControllers.get(sessionId) === controller) {
|
|
3690
3950
|
this.activeAbortControllers.delete(sessionId);
|
|
3691
3951
|
}
|
|
@@ -4266,10 +4526,10 @@ export class CodexRunner {
|
|
|
4266
4526
|
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
4267
4527
|
throw new Error(`managed temp path is not a real directory: ${directory}`);
|
|
4268
4528
|
}
|
|
4269
|
-
if (typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
|
|
4529
|
+
if (process.platform !== 'win32' && typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
|
|
4270
4530
|
throw new Error(`managed temp path is owned by another uid: ${directory}`);
|
|
4271
4531
|
}
|
|
4272
|
-
if ((stat.mode & 0o077) !== 0)
|
|
4532
|
+
if (process.platform !== 'win32' && (stat.mode & 0o077) !== 0)
|
|
4273
4533
|
fs.chmodSync(directory, 0o700);
|
|
4274
4534
|
}
|
|
4275
4535
|
ensureSessionTempDir(sessionId) {
|
|
@@ -4345,7 +4605,7 @@ export class CodexRunner {
|
|
|
4345
4605
|
return undefined;
|
|
4346
4606
|
return '临时文件必须使用环境变量 `$TMPDIR`。它指向当前 EvolCore 会话的受管临时工作区;不要直接使用 `/tmp`,也不要使用其他会话的临时目录。当前是可持久化 thread 的恢复轮次,早先上下文中的 `/tmp/evolcore-codex-*` 绝对路径可能已经失效;不要重用旧路径。若文件不存在,请先在当前 `$TMPDIR` 重新创建,再继续执行后续 `ec` 命令。';
|
|
4347
4607
|
}
|
|
4348
|
-
buildEvolcoreShellEnvironmentConfig(sessionId, delegationCarrier) {
|
|
4608
|
+
buildEvolcoreShellEnvironmentConfig(sessionId, delegationCarrier, taskDelegationToken) {
|
|
4349
4609
|
const managedTempDir = this.ensureSessionTempDir(sessionId);
|
|
4350
4610
|
return {
|
|
4351
4611
|
shell_environment_policy: {
|
|
@@ -4354,7 +4614,9 @@ export class CodexRunner {
|
|
|
4354
4614
|
EVOLCORE_HOME: resolveRoot(),
|
|
4355
4615
|
...(managedTempDir ? { EVOLCORE_SESSION_RUNTIME_DIR: managedTempDir } : {}),
|
|
4356
4616
|
...(managedTempDir ? { EVOLCORE_RUNTIME_LOCK_DIR: path.join(path.dirname(managedTempDir), 'evolcore-locks') } : {}),
|
|
4357
|
-
...(
|
|
4617
|
+
...(taskDelegationToken || delegationCarrier
|
|
4618
|
+
? { [AGENT_DELEGATION_TOKEN_ENV]: taskDelegationToken ?? delegationCarrier }
|
|
4619
|
+
: {}),
|
|
4358
4620
|
...(managedTempDir ? { TMPDIR: managedTempDir } : {}),
|
|
4359
4621
|
},
|
|
4360
4622
|
},
|
|
@@ -4384,6 +4646,8 @@ export class CodexRunner {
|
|
|
4384
4646
|
controller.abort('dispose');
|
|
4385
4647
|
}
|
|
4386
4648
|
await Promise.all([...this.activeSubagentTurns.entries()].map(([threadId, active]) => this.interruptAppServerTurn(threadId, active.turnId).catch(() => { })));
|
|
4649
|
+
await Promise.all([...this.threadSubscriptions.entries()].map(([threadId, subscription]) => this.releaseThreadSubscription(subscription.client, threadId, 'runner-dispose')));
|
|
4650
|
+
this.threadSubscriptions.clear();
|
|
4387
4651
|
this.activeAbortControllers.clear();
|
|
4388
4652
|
this.activeStreams.clear();
|
|
4389
4653
|
this.activeSessions.clear();
|
|
@@ -4404,8 +4668,12 @@ export class CodexRunner {
|
|
|
4404
4668
|
this.threadOperationLocks.clear();
|
|
4405
4669
|
for (const sessionId of [...this.sessionTempDirs.keys()])
|
|
4406
4670
|
this.cleanupSessionTempDir(sessionId);
|
|
4407
|
-
await
|
|
4671
|
+
await Promise.all([
|
|
4672
|
+
this.appServerClient?.close(),
|
|
4673
|
+
this.fullAccessAppServerClient?.close(),
|
|
4674
|
+
]);
|
|
4408
4675
|
this.appServerClient = null;
|
|
4676
|
+
this.fullAccessAppServerClient = null;
|
|
4409
4677
|
}
|
|
4410
4678
|
}
|
|
4411
4679
|
// ── Plugin ──
|