evolcore 0.0.33 → 0.0.35-test.0
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/CHANGELOG.md +59 -0
- package/bin/ec.js +3 -2
- package/bin/install-codex-managed-hooks.mjs +38 -4
- package/dist/agents/baseagent-availability.js +20 -36
- package/dist/agents/claude-runner.js +93 -14
- package/dist/agents/codex/diagnostics.js +79 -22
- package/dist/agents/codex-runner.js +245 -47
- package/dist/agents/ecagent-runner.js +90 -27
- package/dist/agents/runner-types.js +5 -6
- package/dist/aun/aid/agentmd.js +13 -2
- package/dist/aun/aid/client.js +1 -1
- package/dist/aun/aid/identity.js +1 -1
- package/dist/aun/msg/managed-operation.js +16 -0
- package/dist/aun/msg/p2p.js +18 -2
- package/dist/channels/aun.js +123 -7
- package/dist/channels/daemon.js +6 -3
- package/dist/cli/aun-commands.js +39 -14
- package/dist/cli/ctl-command.js +3 -1
- package/dist/cli/daemon-commands.js +130 -23
- package/dist/cli/fs-command.js +2 -1
- package/dist/cli/getmeta-command.js +211 -0
- package/dist/cli/index.js +26 -6
- package/dist/cli/integrity-command.js +196 -0
- package/dist/cli/managed-command-guard.js +3 -0
- package/dist/cli/memory-command.js +200 -156
- package/dist/cli/thread-command.js +17 -7
- package/dist/cli/trigger-command.js +6 -1
- package/dist/config/config-field-policy.js +4 -0
- package/dist/config/config-manager.js +19 -0
- package/dist/config/contact-book-store.js +5 -0
- package/dist/config/resolved-config-op.js +25 -8
- package/dist/config/role-service.js +13 -0
- package/dist/config/runtime-env.js +16 -0
- package/dist/config-store.js +30 -0
- package/dist/core/audit/log-integrity.js +3 -1
- package/dist/core/auth/agent-delegation.js +108 -35
- package/dist/core/auth/delegation-diagnostics.js +414 -0
- package/dist/core/auth/operation-authorizer.js +22 -3
- package/dist/core/auth/operation-catalog.js +16 -0
- package/dist/core/channel-runtime.js +1 -1
- package/dist/core/command/agent-control.js +18 -26
- package/dist/core/command/command-handler.js +145 -5
- package/dist/core/command/daemon-diagnostics.js +1 -0
- package/dist/core/command/menu-catalog.js +3 -0
- package/dist/core/command/menu-handler.js +24 -7
- package/dist/core/command/menu-protocol.js +1 -0
- package/dist/core/command/slash-handler.js +2 -0
- package/dist/core/command/topic-slash.js +4 -7
- package/dist/core/daemon-bootstrap.js +4 -7
- package/dist/core/event-catalog.js +14 -0
- package/dist/core/file-integrity/read.js +61 -0
- package/dist/core/file-integrity/store.js +262 -0
- package/dist/core/ipc-registration.js +47 -2
- package/dist/core/memory/batch-meta.js +133 -0
- package/dist/core/memory/consolidation-context.js +132 -28
- package/dist/core/memory/consolidation-ops-records.js +14 -16
- package/dist/core/memory/consolidation-trigger.js +15 -7
- package/dist/core/memory/consolidation.js +146 -26
- package/dist/core/memory/index-manager.js +19 -28
- package/dist/core/memory/memory-admin.js +19 -14
- package/dist/core/memory/memory-document.js +53 -9
- package/dist/core/memory/memory-writer.js +50 -19
- package/dist/core/message/aun-ipc-sender.js +19 -1
- package/dist/core/message/im-renderer.js +20 -4
- package/dist/core/message/message-bridge.js +5 -4
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/peer-mode.js +17 -0
- package/dist/core/message/response-engine.js +598 -50
- package/dist/core/permission/ec-command-parser.js +169 -44
- package/dist/core/permission/protected-paths.js +5 -1
- package/dist/core/permission/readonly-shell-query.js +43 -5
- package/dist/core/permission/session-path-grants.js +334 -0
- package/dist/core/permission/tool-canonical.js +226 -0
- package/dist/core/permission/tool-policy.js +60 -62
- package/dist/core/relation/peer-identity.js +87 -12
- package/dist/core/role/runtime-policy.js +21 -10
- package/dist/core/session/session-fs-store.js +123 -8
- package/dist/core/session/session-manager.js +131 -6
- package/dist/core/session/session-title.js +26 -1
- package/dist/core/skill/index-generator.js +17 -3
- package/dist/core/system-session/grants.js +40 -0
- package/dist/core/system-session/memory.js +289 -0
- package/dist/core/system-session/registry.js +74 -0
- package/dist/core/system-session/types.js +14 -0
- package/dist/core/thread/command-parser.js +6 -1
- package/dist/core/thread/ipc-operation.js +33 -16
- package/dist/core/thread/thread-operation.js +62 -24
- package/dist/core/thread/thread-service.js +258 -51
- package/dist/eck/kit-renderer.js +7 -2
- package/dist/eck/manifest-engine.js +24 -4
- package/dist/index.js +11 -2
- package/dist/ipc.js +51 -40
- package/dist/paths.js +70 -0
- package/dist/stats/billing.js +20 -8
- package/dist/trigger/feedback.js +6 -1
- package/dist/trigger/parser.js +8 -1
- package/dist/trigger/system-definitions.js +3 -3
- package/dist/utils/cross-platform.js +17 -5
- package/dist/utils/error-dict.json +18 -4
- package/dist/utils/error-utils.js +121 -0
- package/dist/utils/instance-registry.js +13 -8
- package/dist/utils/log-writer.js +12 -0
- package/dist/utils/process-introspect.js +11 -0
- package/kits/docs/context-assembly.md +0 -1
- package/kits/docs/evolcore/agent-create.md +5 -4
- package/kits/docs/evolcore/agent.md +2 -2
- package/kits/docs/evolcore/config.md +2 -2
- package/kits/docs/evolcore/ctl.md +2 -2
- package/kits/docs/evolcore/ec-capabilities.md +1 -1
- package/kits/docs/evolcore/fs-architecture.md +9 -10
- package/kits/docs/evolcore/fs.md +2 -2
- package/kits/docs/evolcore/group-collaboration.md +3 -3
- package/kits/docs/evolcore/group.md +14 -12
- package/kits/docs/evolcore/memory-architecture.md +4 -5
- package/kits/docs/evolcore/memory-consolidation.md +19 -16
- package/kits/docs/evolcore/memory-tags.md +3 -1
- package/kits/docs/evolcore/memory.md +24 -12
- package/kits/docs/evolcore/model.md +16 -0
- package/kits/docs/evolcore/msg.md +7 -7
- package/kits/docs/evolcore/stats.md +1 -1
- package/kits/docs/evolcore/thread-chain.md +30 -14
- package/kits/docs/prompt-loading-architecture.md +0 -1
- package/kits/eck_manifest.auxiliary.json +9 -0
- package/kits/eck_manifest.json +20 -20
- package/kits/eck_manifest.memory.json +20 -12
- package/kits/rules/01-overview.md +11 -0
- package/kits/rules/03-identity.md +6 -3
- package/kits/rules/06-channel.md +1 -1
- package/kits/rules/07-memory-and-thread.md +13 -16
- package/kits/schemas/_meta.json +7 -1
- package/kits/schemas/agent-config.schema.13.json +1 -1
- package/kits/schemas/daemon.schema.7.json +140 -0
- package/kits/skills/ec-group/SKILL.md +8 -5
- package/kits/skills/ec-skill/scripts/cli.mjs +1 -1
- package/kits/skills/ec-skill/scripts/skill.mjs +1 -1
- package/kits/skills/thread-ops/SKILL.md +18 -18
- package/kits/templates/memory-consolidation/forget.md +2 -2
- package/kits/templates/memory-consolidation/generalize.md +4 -4
- package/kits/templates/memory-consolidation/notes.md +12 -12
- package/kits/templates/memory-consolidation/profile.md +3 -3
- package/kits/templates/memory-consolidation/promote.md +3 -3
- package/kits/templates/memory-consolidation/workflow.md +42 -7
- package/kits/templates/message-fragments/handoff-request-to-target.md +1 -1
- package/kits/templates/system-fragments/bash-description.md +9 -0
- package/kits/templates/system-fragments/bootstrap.md +4 -3
- package/kits/templates/system-fragments/channel.md +2 -2
- package/kits/templates/system-fragments/memory.md +16 -9
- package/kits/templates/system-fragments/reply.md +6 -6
- package/kits/templates/system-fragments/session.md +1 -1
- package/package.json +4 -3
- package/dist/core/memory/memory-session-policy.js +0 -41
- package/dist/core/memory/memory-signature.js +0 -257
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { getRunnerCatalog, catalogEfforts, assertCatalogSelectable } from '../core/model/model-catalog.js';
|
|
2
2
|
// Codex/OpenAI API 不认识 Claude Code SDK 的窗口后缀,必须传入基础模型 ID。
|
|
3
|
-
import { stripContextWindowSuffix } from '../core/model/context-window-suffix.js';
|
|
3
|
+
import { parseModelId, stripContextWindowSuffix } from '../core/model/context-window-suffix.js';
|
|
4
4
|
import { formatAsyncQuestionAnswer, hasTrustedFullAccessContext, parseAsyncUserQuestions, unsettledInterruptError, withInterruptDeadline } from './runner-types.js';
|
|
5
5
|
import { requestDangerousCommandPermission } from '../core/permission/approval-gateway.js';
|
|
6
6
|
import { checkReadonly, checkDangerousCommand, evaluateToolPreflight } from '../core/permission/tool-policy.js';
|
|
7
|
+
import { isShellToolName } from '../core/permission/tool-canonical.js';
|
|
7
8
|
import { normalizeExecutionPermissionMode, normalizePermissionMode, resolvePhaseOneExecutionSandbox } from '../core/permission/mode.js';
|
|
8
9
|
import { assertWindowsCodexFilesystemRuleBudget, buildCodexProtectedFilesystemRules, getExistingRelationAttachmentDirs, isSameOrDescendant, resolveProtectedCandidate, } from '../core/permission/protected-paths.js';
|
|
9
10
|
import { CODEX_THREAD_ACTIVE_WRITER, CodexAppServerClient, } from './codex/app-server-client.js';
|
|
@@ -11,7 +12,7 @@ import { resolveOpenaiConfig, resolveCodexCliPath, resolveCodexPowerShellShimPat
|
|
|
11
12
|
import { logger } from '../utils/logger.js';
|
|
12
13
|
import { summarizeToolInputForAudit } from '../utils/tool-summary.js';
|
|
13
14
|
import { auditCodexApprovalDecision, auditPermissionLifecycle, auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
|
|
14
|
-
import { classifyError, ErrorType, isOutputTokenLimitError, isRetryableError } from '../utils/error-utils.js';
|
|
15
|
+
import { classifyError, ErrorType, getToolResultErrorText, isOutputTokenLimitError, isRetryableError } from '../utils/error-utils.js';
|
|
15
16
|
import { renderActionAsText } from '../core/interaction-router.js';
|
|
16
17
|
import { buildEnvelope, isInteractionSendAccepted, sendInteractionPayload } from '../core/message/message-utils.js';
|
|
17
18
|
import { resolveCodexCapabilityThreadConfigForProject } from '../core/capability/capability-manager.js';
|
|
@@ -20,6 +21,7 @@ import { sanitizeShellExecutionEnvironment } from '../core/permission/shell-envi
|
|
|
20
21
|
import { classifyEvolcoreShellCommand, containsLiteralManagedTmpDirOutsideSendContent, containsUnresolvedManagedTmpDirReference, hasCodexCmdCarrierEcIntent, hasPowerShellCredentialAssignment, normalizeEvolcoreCommandArgv, canonicalizeEvolcoreCommandArgv, parseCodexToolCommand, parseCodexCarrierCommand, parseLiteralShellCommand, parseCodexShellCommandArgv, resolveCodexShellCarrierArgv, parseShellApplyPatchInvocation, } from '../core/permission/ec-command-parser.js';
|
|
21
22
|
import { compareVersions } from '../utils/npm-ops.js';
|
|
22
23
|
import { agentCodexTokenPath, resolvePaths, resolveRoot } from '../paths.js';
|
|
24
|
+
import { findModelSpec } from '../stats/billing.js';
|
|
23
25
|
import { ensureSessionRuntimeDir, getManagedTaskTempDir } from '../cli/task-context.js';
|
|
24
26
|
import { buildTranscriptTurns } from '../core/session/session-transcript.js';
|
|
25
27
|
import { CodexSessionFileAdapter } from '../core/session/adapters/codex-session-file-adapter.js';
|
|
@@ -86,6 +88,24 @@ class AsyncEventQueue {
|
|
|
86
88
|
}
|
|
87
89
|
}
|
|
88
90
|
}
|
|
91
|
+
function parseCodexRetryProgress(message) {
|
|
92
|
+
const match = message.match(/\bReconnecting\.\.\.\s*(\d+)\s*\/\s*(\d+)\b/i);
|
|
93
|
+
if (!match)
|
|
94
|
+
return undefined;
|
|
95
|
+
const count = Number(match[1]);
|
|
96
|
+
const limit = Number(match[2]);
|
|
97
|
+
if (!Number.isSafeInteger(count) || !Number.isSafeInteger(limit) || count < 1 || limit < count)
|
|
98
|
+
return undefined;
|
|
99
|
+
return { count, limit };
|
|
100
|
+
}
|
|
101
|
+
function createCodexRetryMetadataError(message, state) {
|
|
102
|
+
const error = new Error(message);
|
|
103
|
+
if (state.modelRetryCount !== undefined)
|
|
104
|
+
error.codexRetryCount = state.modelRetryCount;
|
|
105
|
+
if (state.modelRetryLimit !== undefined)
|
|
106
|
+
error.codexRetryLimit = state.modelRetryLimit;
|
|
107
|
+
return error;
|
|
108
|
+
}
|
|
89
109
|
function fallbackErrorCodeForToolDenial(decisionSource, policyCode) {
|
|
90
110
|
if (decisionSource === 'policy' && policyCode === 'apply_patch_invalid_argument')
|
|
91
111
|
return 'INVALID_ARGUMENT';
|
|
@@ -128,6 +148,24 @@ function codexItemErrorFields(item) {
|
|
|
128
148
|
const errorDetailCode = normalizeToolErrorDetailCode(rawCode);
|
|
129
149
|
return errorDetailCode ? { errorDetailCode } : {};
|
|
130
150
|
}
|
|
151
|
+
function waitForCodexOperation(operation, signal) {
|
|
152
|
+
return new Promise((resolve, reject) => {
|
|
153
|
+
const onAbort = () => {
|
|
154
|
+
signal.removeEventListener('abort', onAbort);
|
|
155
|
+
reject(signal.reason);
|
|
156
|
+
};
|
|
157
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
158
|
+
operation.then(value => {
|
|
159
|
+
signal.removeEventListener('abort', onAbort);
|
|
160
|
+
resolve(value);
|
|
161
|
+
}, error => {
|
|
162
|
+
signal.removeEventListener('abort', onAbort);
|
|
163
|
+
reject(error);
|
|
164
|
+
});
|
|
165
|
+
if (signal.aborted)
|
|
166
|
+
onAbort();
|
|
167
|
+
});
|
|
168
|
+
}
|
|
131
169
|
function isCodexReasoningNotification(method) {
|
|
132
170
|
return method === 'item/reasoning/textDelta'
|
|
133
171
|
|| method === 'item/reasoning/summaryTextDelta'
|
|
@@ -335,6 +373,11 @@ export async function getCodexAppServerAvailabilityAsync() {
|
|
|
335
373
|
}
|
|
336
374
|
let version;
|
|
337
375
|
try {
|
|
376
|
+
// Each probe is a full Codex CLI launch (~0.3-0.45s on Windows). Run the
|
|
377
|
+
// app-server probe concurrently; its result is only consulted after the
|
|
378
|
+
// version gate passes, so the decision order is unchanged.
|
|
379
|
+
const appServerProbe = probeCli('codex', ['app-server', '--help'], options);
|
|
380
|
+
appServerProbe.catch(() => { });
|
|
338
381
|
version = parseCodexCliVersion(await probeCli('codex', ['--version'], options)) ?? undefined;
|
|
339
382
|
if (!version)
|
|
340
383
|
return { available: false, reason: '无法读取 Codex CLI 版本。' };
|
|
@@ -342,7 +385,7 @@ export async function getCodexAppServerAvailabilityAsync() {
|
|
|
342
385
|
return { available: false, version, requiresUpgrade: true,
|
|
343
386
|
reason: `Codex CLI ${version} 低于最低要求 ${MIN_CODEX_CLI_VERSION}。请升级 Codex CLI:npm install -g @openai/codex@latest` };
|
|
344
387
|
}
|
|
345
|
-
await
|
|
388
|
+
await appServerProbe;
|
|
346
389
|
return { available: true, version };
|
|
347
390
|
}
|
|
348
391
|
catch (error) {
|
|
@@ -359,6 +402,7 @@ export class CodexRunner {
|
|
|
359
402
|
retryInputSemantics = 'at_most_once';
|
|
360
403
|
capabilities;
|
|
361
404
|
model;
|
|
405
|
+
modelContextId;
|
|
362
406
|
effort;
|
|
363
407
|
activeAbortControllers = new Map();
|
|
364
408
|
activeStreams = new Map();
|
|
@@ -378,6 +422,7 @@ export class CodexRunner {
|
|
|
378
422
|
unconfirmedInterrupts = new Map();
|
|
379
423
|
steeringTails = new Map();
|
|
380
424
|
threadOperationLocks = new Map();
|
|
425
|
+
queryOperations = new Map();
|
|
381
426
|
activeOperationReleases = new Map();
|
|
382
427
|
/** Last app-server client that loaded each thread, including unsubscribed threads. */
|
|
383
428
|
threadSubscriptions = new Map();
|
|
@@ -431,7 +476,8 @@ export class CodexRunner {
|
|
|
431
476
|
fileRewind: 'git-head',
|
|
432
477
|
fullaccess: true,
|
|
433
478
|
};
|
|
434
|
-
this.
|
|
479
|
+
this.modelContextId = this.resolvedConfig.model;
|
|
480
|
+
this.model = stripContextWindowSuffix(this.modelContextId);
|
|
435
481
|
if (this.resolvedConfig.effort)
|
|
436
482
|
this.effort = this.resolvedConfig.effort;
|
|
437
483
|
this.onSessionIdUpdate = callbacks.onSessionIdUpdate;
|
|
@@ -1179,7 +1225,11 @@ export class CodexRunner {
|
|
|
1179
1225
|
}
|
|
1180
1226
|
}
|
|
1181
1227
|
// ── ModelSwitcher ──
|
|
1182
|
-
setModel(model) {
|
|
1228
|
+
setModel(model) {
|
|
1229
|
+
this.modelContextId = model;
|
|
1230
|
+
this.model = stripContextWindowSuffix(model);
|
|
1231
|
+
this.resetAppServerClient();
|
|
1232
|
+
}
|
|
1183
1233
|
getModel() { return this.model; }
|
|
1184
1234
|
getModelCatalog() {
|
|
1185
1235
|
return getRunnerCatalog('codex', this.resolvedConfig.agentAid, this.resolvedConfig);
|
|
@@ -1222,6 +1272,16 @@ export class CodexRunner {
|
|
|
1222
1272
|
const digest = createHash('sha256').update(sessionId).digest('hex').slice(0, 24);
|
|
1223
1273
|
return `__evolcore_${label}_${digest}_hclass_v1`;
|
|
1224
1274
|
}
|
|
1275
|
+
/**
|
|
1276
|
+
* 系统会话的写入洞:只对模块声明的可写文件逐个开 write 规则,不开目录、不开 glob。
|
|
1277
|
+
* 声明为空(缺批次目录等)时返回空表,沙箱保持 :read-only。
|
|
1278
|
+
*/
|
|
1279
|
+
resolveSystemSessionWriteRules(sessionId) {
|
|
1280
|
+
const writeFiles = this.permissionContexts.get(sessionId)?.systemSessionGrants?.writeFiles ?? [];
|
|
1281
|
+
return Object.fromEntries(writeFiles
|
|
1282
|
+
.filter(file => path.isAbsolute(file))
|
|
1283
|
+
.map(file => [path.resolve(file), 'write']));
|
|
1284
|
+
}
|
|
1225
1285
|
resolveGitMetadataWriteRules(projectPath, mode) {
|
|
1226
1286
|
if (mode === 'readonly')
|
|
1227
1287
|
return {};
|
|
@@ -1298,6 +1358,7 @@ export class CodexRunner {
|
|
|
1298
1358
|
expandLClassReadDeny: process.platform !== 'linux',
|
|
1299
1359
|
}),
|
|
1300
1360
|
...this.resolveGitMetadataWriteRules(projectPath, mode),
|
|
1361
|
+
...this.resolveSystemSessionWriteRules(sessionId),
|
|
1301
1362
|
...(bootstrapAgentMdPath ? {
|
|
1302
1363
|
[path.resolve(bootstrapAgentMdPath)]: mode === 'readonly' ? 'read' : 'write',
|
|
1303
1364
|
} : {}),
|
|
@@ -1330,12 +1391,18 @@ export class CodexRunner {
|
|
|
1330
1391
|
}
|
|
1331
1392
|
buildExecutionSandboxOptions(sessionId, mode, projectPath, bootstrapAgentMdPath, personalMdPath, permissionContext = this.permissionContexts.get(sessionId) ?? {}) {
|
|
1332
1393
|
const decision = resolvePhaseOneExecutionSandbox(mode, permissionContext.role);
|
|
1394
|
+
// 系统会话明确声明不要沙箱:Codex 的 permission profile 以项目目录为中心,
|
|
1395
|
+
// 而系统会话的读写边界($EVOLCORE_HOME 下的批次目录与记忆目录)与 cwd 无关,
|
|
1396
|
+
// 硬套 profile 只会 fail closed。边界改由 systemSessionCheck 逐次调用判定。
|
|
1397
|
+
const sessionSandboxOff = this.permissionContexts.get(sessionId)?.systemSessionSandbox === 'off';
|
|
1398
|
+
const sandboxOff = decision.state === 'off' || sessionSandboxOff;
|
|
1333
1399
|
return {
|
|
1334
1400
|
decision,
|
|
1335
|
-
permissionProfileConfig:
|
|
1336
|
-
?
|
|
1337
|
-
:
|
|
1338
|
-
sandbox:
|
|
1401
|
+
permissionProfileConfig: sandboxOff
|
|
1402
|
+
? undefined
|
|
1403
|
+
: this.buildPermissionProfileConfig(sessionId, decision.permissionMode, projectPath, bootstrapAgentMdPath, personalMdPath),
|
|
1404
|
+
sandbox: sandboxOff ? 'danger-full-access' : undefined,
|
|
1405
|
+
sessionSandboxOff,
|
|
1339
1406
|
};
|
|
1340
1407
|
}
|
|
1341
1408
|
isTrustedFullAccessSession(sessionKey) {
|
|
@@ -1465,7 +1532,7 @@ export class CodexRunner {
|
|
|
1465
1532
|
const normalizedInput = toolInput && typeof toolInput === 'object' && !Array.isArray(toolInput)
|
|
1466
1533
|
? toolInput
|
|
1467
1534
|
: {};
|
|
1468
|
-
const isShellTool = toolName
|
|
1535
|
+
const isShellTool = isShellToolName(toolName); // canonical normalisation via tool-canonical.ts
|
|
1469
1536
|
const recordPreToolUseDenial = async (policyCode, reason) => {
|
|
1470
1537
|
const summary = summarizeToolInputForAudit(toolName, normalizedInput).slice(0, 512);
|
|
1471
1538
|
auditToolPreflightDenial({
|
|
@@ -1533,6 +1600,14 @@ export class CodexRunner {
|
|
|
1533
1600
|
return { ok: true, applicable: true, decision: 'deny', reason };
|
|
1534
1601
|
}
|
|
1535
1602
|
}
|
|
1603
|
+
// 系统会话:模块判定即最终结论,不进入 preflight / 角色 / permissionMode 分支
|
|
1604
|
+
if (context.systemSessionCheck) {
|
|
1605
|
+
const decision = context.systemSessionCheck(isShellTool ? 'Bash' : toolName, normalizedInput);
|
|
1606
|
+
if (decision.allow)
|
|
1607
|
+
return { ok: true, applicable: true, decision: 'allow' };
|
|
1608
|
+
await recordPreToolUseDenial(decision.code, decision.reason);
|
|
1609
|
+
return { ok: true, applicable: true, decision: 'deny', reason: decision.reason };
|
|
1610
|
+
}
|
|
1536
1611
|
if ((this.chatModes.get(sessionKey) ?? this.currentMode) === 'fullaccess') {
|
|
1537
1612
|
if (this.isTrustedFullAccessSession(sessionKey))
|
|
1538
1613
|
return { ok: true, applicable: true, decision: 'allow' };
|
|
@@ -1623,6 +1698,19 @@ export class CodexRunner {
|
|
|
1623
1698
|
hasActiveStream(key) {
|
|
1624
1699
|
return this.activeStreams.has(key) || this.activeAbortControllers.has(key) || this.activeTurns.has(key);
|
|
1625
1700
|
}
|
|
1701
|
+
hasActiveOperation(sessionId, taskId) {
|
|
1702
|
+
const operations = this.queryOperations.get(sessionId);
|
|
1703
|
+
return !!operations && [...operations].some(operation => !taskId || operation.taskId === taskId);
|
|
1704
|
+
}
|
|
1705
|
+
cancelQueuedTurn(sessionId, identity) {
|
|
1706
|
+
const operation = [...(this.queryOperations.get(sessionId) ?? [])].find(candidate => !candidate.lockAcquired
|
|
1707
|
+
&& candidate.taskId === identity.taskId
|
|
1708
|
+
&& candidate.generation === identity.generation);
|
|
1709
|
+
if (!operation)
|
|
1710
|
+
return false;
|
|
1711
|
+
operation.controller.abort(Object.assign(new Error('Codex queued operation cancelled'), { name: 'AbortError' }));
|
|
1712
|
+
return true;
|
|
1713
|
+
}
|
|
1626
1714
|
threadOperationKey(threadId, sessionId) {
|
|
1627
1715
|
// Once a thread id is known it is the shared identity, including when
|
|
1628
1716
|
// multiple EvolCore sessions happen to address the same Codex thread.
|
|
@@ -1659,6 +1747,7 @@ export class CodexRunner {
|
|
|
1659
1747
|
merged.then(() => {
|
|
1660
1748
|
if (primary.tail === merged)
|
|
1661
1749
|
primary.tail = undefined;
|
|
1750
|
+
this.pruneThreadOperationLocks();
|
|
1662
1751
|
});
|
|
1663
1752
|
}
|
|
1664
1753
|
for (const alias of other.aliases) {
|
|
@@ -1694,7 +1783,8 @@ export class CodexRunner {
|
|
|
1694
1783
|
lock.aliases.delete(key);
|
|
1695
1784
|
}
|
|
1696
1785
|
}
|
|
1697
|
-
async acquireThreadOperation(key, operation) {
|
|
1786
|
+
async acquireThreadOperation(key, operation, signal) {
|
|
1787
|
+
signal?.throwIfAborted();
|
|
1698
1788
|
const lock = this.getThreadOperationLock(key);
|
|
1699
1789
|
const previous = lock.tail;
|
|
1700
1790
|
let releaseCurrent;
|
|
@@ -1702,12 +1792,8 @@ export class CodexRunner {
|
|
|
1702
1792
|
releaseCurrent = resolve;
|
|
1703
1793
|
});
|
|
1704
1794
|
lock.tail = current;
|
|
1705
|
-
if (previous) {
|
|
1706
|
-
logger.info(`[CodexRunner] queued ${operation} behind an active operation: ${key}`);
|
|
1707
|
-
await previous;
|
|
1708
|
-
}
|
|
1709
1795
|
let released = false;
|
|
1710
|
-
|
|
1796
|
+
const release = () => {
|
|
1711
1797
|
if (released)
|
|
1712
1798
|
return;
|
|
1713
1799
|
released = true;
|
|
@@ -1716,6 +1802,21 @@ export class CodexRunner {
|
|
|
1716
1802
|
releaseCurrent();
|
|
1717
1803
|
this.pruneThreadOperationLocks();
|
|
1718
1804
|
};
|
|
1805
|
+
try {
|
|
1806
|
+
if (previous) {
|
|
1807
|
+
logger.info(`[CodexRunner] queued ${operation} behind an active operation: ${key}`);
|
|
1808
|
+
await (signal ? waitForCodexOperation(previous, signal) : previous);
|
|
1809
|
+
}
|
|
1810
|
+
signal?.throwIfAborted();
|
|
1811
|
+
return release;
|
|
1812
|
+
}
|
|
1813
|
+
catch (error) {
|
|
1814
|
+
if (signal?.aborted && previous)
|
|
1815
|
+
void previous.then(release);
|
|
1816
|
+
else
|
|
1817
|
+
release();
|
|
1818
|
+
throw error;
|
|
1819
|
+
}
|
|
1719
1820
|
}
|
|
1720
1821
|
async *holdThreadOperation(stream, sessionId, release) {
|
|
1721
1822
|
try {
|
|
@@ -1747,11 +1848,29 @@ export class CodexRunner {
|
|
|
1747
1848
|
? this.cachedThreadForTopic(sessionId, topicAgentSessionId)
|
|
1748
1849
|
: initialAgentSessionId || this.activeSessions.get(sessionId);
|
|
1749
1850
|
const operationKey = this.threadOperationKey(modelOverride?.recovery?.agentSessionId || knownThreadId, sessionId);
|
|
1750
|
-
|
|
1751
|
-
|
|
1851
|
+
const operation = {
|
|
1852
|
+
controller: new AbortController(),
|
|
1853
|
+
taskId: modelOverride?.turn?.taskId ?? this.permissionContexts.get(sessionId)?.taskId,
|
|
1854
|
+
generation: modelOverride?.turn?.generation,
|
|
1855
|
+
lockAcquired: false,
|
|
1856
|
+
};
|
|
1857
|
+
const operations = this.queryOperations.get(sessionId) ?? new Set();
|
|
1858
|
+
operations.add(operation);
|
|
1859
|
+
this.queryOperations.set(sessionId, operations);
|
|
1860
|
+
let releaseLock;
|
|
1861
|
+
const release = () => {
|
|
1862
|
+
operations.delete(operation);
|
|
1863
|
+
if (this.queryOperations.get(sessionId) === operations && operations.size === 0) {
|
|
1864
|
+
this.queryOperations.delete(sessionId);
|
|
1865
|
+
}
|
|
1866
|
+
releaseLock?.();
|
|
1867
|
+
};
|
|
1752
1868
|
try {
|
|
1753
|
-
await this.assertInterruptSettled(sessionId, modelOverride?.recovery?.agentSessionId || knownThreadId);
|
|
1754
|
-
|
|
1869
|
+
await waitForCodexOperation(this.assertInterruptSettled(sessionId, modelOverride?.recovery?.agentSessionId || knownThreadId), operation.controller.signal);
|
|
1870
|
+
releaseLock = await this.acquireThreadOperation(operationKey, 'thread/resume+turn/start', operation.controller.signal);
|
|
1871
|
+
operation.lockAcquired = true;
|
|
1872
|
+
await waitForCodexOperation(this.assertInterruptSettled(sessionId, modelOverride?.recovery?.agentSessionId || knownThreadId), operation.controller.signal);
|
|
1873
|
+
const stream = await this.runQueryLocked(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv, operation.controller);
|
|
1755
1874
|
this.activeOperationReleases.set(sessionId, release);
|
|
1756
1875
|
return this.holdThreadOperation(stream, sessionId, release);
|
|
1757
1876
|
}
|
|
@@ -1760,7 +1879,7 @@ export class CodexRunner {
|
|
|
1760
1879
|
throw error;
|
|
1761
1880
|
}
|
|
1762
1881
|
}
|
|
1763
|
-
async runQueryLocked(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv) {
|
|
1882
|
+
async runQueryLocked(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv, operationController) {
|
|
1764
1883
|
// A daemon-managed task must never fall back to the app-server's
|
|
1765
1884
|
// process-wide TMPDIR if its session namespace disappeared.
|
|
1766
1885
|
getManagedTaskTempDir(runtimeEnv);
|
|
@@ -1789,7 +1908,8 @@ export class CodexRunner {
|
|
|
1789
1908
|
}
|
|
1790
1909
|
await this.assertInterruptSettled(sessionId, agentSessionId);
|
|
1791
1910
|
let resumingThread = !!agentSessionId;
|
|
1792
|
-
const
|
|
1911
|
+
const modelContextId = modelOverride?.model || this.modelContextId;
|
|
1912
|
+
const callModel = stripContextWindowSuffix(modelContextId);
|
|
1793
1913
|
const callEffort = modelOverride?.effortMode === 'model_default'
|
|
1794
1914
|
? undefined
|
|
1795
1915
|
: modelOverride?.effort ?? this.effort;
|
|
@@ -1901,6 +2021,7 @@ export class CodexRunner {
|
|
|
1901
2021
|
if (!resumingThread && this.ensureThreadCredential(threadId, threadSessionToken) !== threadSessionToken) {
|
|
1902
2022
|
throw new Error('Codex thread session credential could not be bound');
|
|
1903
2023
|
}
|
|
2024
|
+
this.permissionContexts.get(sessionId)?.bindSessionCredential?.(threadSessionToken, threadId);
|
|
1904
2025
|
const turn = modelOverride?.turn;
|
|
1905
2026
|
if (!resumingThread) {
|
|
1906
2027
|
// Keep a newly-created backend private to this runner until turn/start
|
|
@@ -1930,7 +2051,7 @@ export class CodexRunner {
|
|
|
1930
2051
|
}
|
|
1931
2052
|
this.threadProjectPaths.set(threadId, path.resolve(projectPath));
|
|
1932
2053
|
this.bindExternalToolBoundary(threadId, externalToolConfig);
|
|
1933
|
-
const controller = new AbortController();
|
|
2054
|
+
const controller = operationController ?? new AbortController();
|
|
1934
2055
|
this.activeAbortControllers.set(sessionId, controller);
|
|
1935
2056
|
const answer = modelOverride?.asyncQuestionAnswer;
|
|
1936
2057
|
if (answer && answer.providerThreadId !== threadId) {
|
|
@@ -1944,6 +2065,7 @@ export class CodexRunner {
|
|
|
1944
2065
|
sessionId,
|
|
1945
2066
|
projectPath,
|
|
1946
2067
|
model: callModel,
|
|
2068
|
+
modelContextId,
|
|
1947
2069
|
committed: resumingThread,
|
|
1948
2070
|
persistenceFinalized: resumingThread,
|
|
1949
2071
|
streamedAgentMessageIds: new Set(),
|
|
@@ -1957,6 +2079,11 @@ export class CodexRunner {
|
|
|
1957
2079
|
inputId: turn?.inputId,
|
|
1958
2080
|
inputAcceptedEmitted: false,
|
|
1959
2081
|
};
|
|
2082
|
+
let startupInterrupt;
|
|
2083
|
+
const interruptStartedTurn = (turnId) => {
|
|
2084
|
+
startupInterrupt ??= this.interruptAppServerTurn(threadId, turnId);
|
|
2085
|
+
return startupInterrupt;
|
|
2086
|
+
};
|
|
1960
2087
|
const unsubscribe = appServer.onNotification(notification => {
|
|
1961
2088
|
// 仅从 turn/started 锁定权威 turnId — resume 时会有上一轮 turn 的残留通知
|
|
1962
2089
|
// (如 thread/tokenUsage/updated)先于新 turn 到达,不能用它们 latch turnId
|
|
@@ -2000,8 +2127,8 @@ export class CodexRunner {
|
|
|
2000
2127
|
if (startedTurnId && !state.turnId) {
|
|
2001
2128
|
state.turnId = startedTurnId;
|
|
2002
2129
|
this.activeTurns.set(sessionId, { threadId, turnId: startedTurnId });
|
|
2003
|
-
if (this.consumePendingInterrupt(sessionId)) {
|
|
2004
|
-
|
|
2130
|
+
if (controller.signal.aborted || this.consumePendingInterrupt(sessionId)) {
|
|
2131
|
+
void interruptStartedTurn(startedTurnId);
|
|
2005
2132
|
this.activeTurns.delete(sessionId);
|
|
2006
2133
|
}
|
|
2007
2134
|
}
|
|
@@ -2018,8 +2145,9 @@ export class CodexRunner {
|
|
|
2018
2145
|
this.endAppServerQueueWhenSubagentsDone(queue, state);
|
|
2019
2146
|
}
|
|
2020
2147
|
});
|
|
2021
|
-
if (this.consumePendingInterrupt(sessionId)) {
|
|
2148
|
+
if (controller.signal.aborted || this.consumePendingInterrupt(sessionId)) {
|
|
2022
2149
|
controller.abort('User interrupt');
|
|
2150
|
+
queue.end();
|
|
2023
2151
|
this.activeAbortControllers.delete(sessionId);
|
|
2024
2152
|
this.activeStreams.delete(sessionId);
|
|
2025
2153
|
if (!resumingThread)
|
|
@@ -2086,8 +2214,10 @@ export class CodexRunner {
|
|
|
2086
2214
|
// the short settle window below.
|
|
2087
2215
|
await state.confirmProvisional?.(0);
|
|
2088
2216
|
}
|
|
2089
|
-
if (turnId && this.consumePendingInterrupt(sessionId)) {
|
|
2090
|
-
|
|
2217
|
+
if (turnId && (controller.signal.aborted || this.consumePendingInterrupt(sessionId))) {
|
|
2218
|
+
if (!state.turnCompleted && !['completed', 'failed', 'interrupted'].includes(turnResponse.turn?.status)) {
|
|
2219
|
+
await interruptStartedTurn(turnId);
|
|
2220
|
+
}
|
|
2091
2221
|
controller.abort('User interrupt');
|
|
2092
2222
|
this.activeAbortControllers.delete(sessionId);
|
|
2093
2223
|
this.activeStreams.delete(sessionId);
|
|
@@ -2116,14 +2246,21 @@ export class CodexRunner {
|
|
|
2116
2246
|
// ── Interrupt ──
|
|
2117
2247
|
async interrupt(sessionKey) {
|
|
2118
2248
|
this.permissionContexts.get(sessionKey)?.pauseController?.cancel();
|
|
2249
|
+
const operations = this.queryOperations.get(sessionKey);
|
|
2250
|
+
for (const operation of operations ?? []) {
|
|
2251
|
+
if (operation.lockAcquired) {
|
|
2252
|
+
operation.controller.abort(Object.assign(new Error('Codex operation interrupted'), { name: 'AbortError' }));
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2119
2255
|
const controller = this.activeAbortControllers.get(sessionKey);
|
|
2120
2256
|
const activeTurn = this.activeTurns.get(sessionKey);
|
|
2121
|
-
const hadActiveState = !!controller || !!activeTurn || this.activeStreams.has(sessionKey)
|
|
2257
|
+
const hadActiveState = !!controller || !!activeTurn || this.activeStreams.has(sessionKey)
|
|
2258
|
+
|| [...(operations ?? [])].some(operation => operation.lockAcquired);
|
|
2122
2259
|
const interruptTurn = activeTurn
|
|
2123
2260
|
? this.interruptAppServerTurn(activeTurn.threadId, activeTurn.turnId)
|
|
2124
2261
|
: Promise.resolve();
|
|
2125
2262
|
const interruptSubagents = this.interruptSubagentTurns(sessionKey);
|
|
2126
|
-
if (!activeTurn)
|
|
2263
|
+
if (!activeTurn && !operations?.size)
|
|
2127
2264
|
this.rememberPendingInterrupt(sessionKey);
|
|
2128
2265
|
if (controller)
|
|
2129
2266
|
controller.abort('User interrupt');
|
|
@@ -3228,7 +3365,7 @@ export class CodexRunner {
|
|
|
3228
3365
|
return await denyInfrastructure('ec_command_not_canonical', canonicalEcApproval?.parseFailure
|
|
3229
3366
|
? `EC command approval denied because canonical argv parsing failed (${canonicalEcApproval.parseFailure.issue})`
|
|
3230
3367
|
+ (canonicalEcApproval.parseFailure.dialect === 'powershell' && canonicalEcApproval.parseFailure.issue === 'invalid-quote'
|
|
3231
|
-
? '. PowerShell treats
|
|
3368
|
+
? '. PowerShell treats U+2018/U+2019/U+201A/U+201B as quote delimiters too. An EC inline text argument must be ASCII single-quoted; replace any character inside it that looks like a quote with U+02BC or U+FF07, or write the text to the session TMPDIR and pass it with --text-from-file.'
|
|
3232
3369
|
: '')
|
|
3233
3370
|
: 'EC command approval denied because no canonical argv could be derived', canonicalEcApproval?.parseFailure?.issue === 'invalid-quote' ? 'INVALID_QUOTE' : undefined);
|
|
3234
3371
|
}
|
|
@@ -3551,15 +3688,18 @@ export class CodexRunner {
|
|
|
3551
3688
|
...(trackedItem.appContext && typeof trackedItem.appContext === 'object' ? { appContext: trackedItem.appContext } : {}),
|
|
3552
3689
|
};
|
|
3553
3690
|
const policyResult = this.permissionContexts.get(sessionKey)?.policyHook?.(toolName, toolInput);
|
|
3691
|
+
const systemSessionCheck = this.permissionContexts.get(sessionKey)?.systemSessionCheck;
|
|
3554
3692
|
const workspacePath = this.resolvePermissionWorkspacePath(params);
|
|
3555
3693
|
const mode = normalizeExecutionPermissionMode(this.chatModes.get(sessionKey) ?? this.currentMode);
|
|
3556
3694
|
// MCP servers are inherited unless capability config explicitly disables
|
|
3557
3695
|
// them. Apps retain their separate request/bypass lifecycle policy.
|
|
3558
|
-
let allow =
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3696
|
+
let allow = systemSessionCheck
|
|
3697
|
+
? systemSessionCheck(toolName, toolInput).allow
|
|
3698
|
+
: !policyResult?.block
|
|
3699
|
+
&& !!workspacePath
|
|
3700
|
+
&& (isAppCall
|
|
3701
|
+
? mode === 'request' || mode === 'bypass'
|
|
3702
|
+
: mode !== 'readonly');
|
|
3563
3703
|
if (allow) {
|
|
3564
3704
|
try {
|
|
3565
3705
|
for (const question of questions)
|
|
@@ -3847,6 +3987,7 @@ export class CodexRunner {
|
|
|
3847
3987
|
// readonly decision. The map entry alone is stale if the directory was
|
|
3848
3988
|
// replaced between preflight and the approval callback.
|
|
3849
3989
|
managedTempDir: permCtx?.managedTempDir ?? this.getManagedTempDir(sessionKey),
|
|
3990
|
+
grantedReadRoots: permCtx?.grantedReadRoots,
|
|
3850
3991
|
allowReadonlySourceDiagnostics: permCtx?.allowReadonlySourceDiagnostics === true,
|
|
3851
3992
|
} : undefined;
|
|
3852
3993
|
if (toolName === 'Bash')
|
|
@@ -3943,6 +4084,13 @@ export class CodexRunner {
|
|
|
3943
4084
|
}
|
|
3944
4085
|
async resolvePermissionDecision(sessionKey, toolName, toolInput, summary, reason, workspacePath = process.cwd(), operationCwd = workspacePath, requestId, callId) {
|
|
3945
4086
|
const permissionContext = this.permissionContexts.get(sessionKey);
|
|
4087
|
+
// 系统会话:审批路径同样只看模块判定(FileChange / PermissionGrant 一律被模块拒绝)
|
|
4088
|
+
if (permissionContext?.systemSessionCheck) {
|
|
4089
|
+
const decision = permissionContext.systemSessionCheck(toolName, toolInput);
|
|
4090
|
+
return decision.allow
|
|
4091
|
+
? 'allow'
|
|
4092
|
+
: { decision: 'deny', decisionSource: 'policy', policyCode: decision.code, reason: decision.reason };
|
|
4093
|
+
}
|
|
3946
4094
|
const permissionPrompt = permissionContext?.sendPrompt ?? this.sendPromptFn;
|
|
3947
4095
|
const hasFilesystemPermissions = (toolName === 'Bash' || toolName === 'PermissionGrant')
|
|
3948
4096
|
&& this.hasFilesystemPermissionRequest(toolInput);
|
|
@@ -4345,15 +4493,37 @@ export class CodexRunner {
|
|
|
4345
4493
|
let input;
|
|
4346
4494
|
let result;
|
|
4347
4495
|
let isError = false;
|
|
4496
|
+
let executionFields = {};
|
|
4348
4497
|
switch (type) {
|
|
4349
|
-
case 'commandExecution':
|
|
4498
|
+
case 'commandExecution': {
|
|
4350
4499
|
name = 'Shell';
|
|
4351
4500
|
input = { command: item.command, cwd: item.cwd };
|
|
4352
4501
|
result = item.aggregatedOutput ?? item.output ?? '';
|
|
4353
4502
|
isError = item.exitCode != null
|
|
4354
4503
|
? Number(item.exitCode) !== 0
|
|
4355
4504
|
: item.status === 'failed' || item.status === 'declined';
|
|
4505
|
+
const exitCode = typeof item.exitCode === 'number' && Number.isInteger(item.exitCode)
|
|
4506
|
+
? item.exitCode
|
|
4507
|
+
: undefined;
|
|
4508
|
+
const itemError = item.error;
|
|
4509
|
+
const errorMessage = typeof itemError === 'string'
|
|
4510
|
+
? itemError
|
|
4511
|
+
: itemError && typeof itemError === 'object' && 'message' in itemError
|
|
4512
|
+
&& typeof itemError.message === 'string' ? itemError.message : undefined;
|
|
4513
|
+
executionFields = {
|
|
4514
|
+
...(exitCode !== undefined ? { exit_code: exitCode } : {}),
|
|
4515
|
+
...(item.aggregatedOutput != null ? { output_kind: 'combined' } : {}),
|
|
4516
|
+
...(typeof item.durationMs === 'number' ? { duration_ms: item.durationMs } : {}),
|
|
4517
|
+
...(isError ? {
|
|
4518
|
+
error: getToolResultErrorText({
|
|
4519
|
+
name, result, exitCode,
|
|
4520
|
+
error: errorMessage,
|
|
4521
|
+
reason: item.status === 'declined' ? 'Approval declined' : undefined,
|
|
4522
|
+
}),
|
|
4523
|
+
} : {}),
|
|
4524
|
+
};
|
|
4356
4525
|
break;
|
|
4526
|
+
}
|
|
4357
4527
|
case 'mcpToolCall':
|
|
4358
4528
|
name = `MCP:${item.server}/${item.tool}`;
|
|
4359
4529
|
input = item.arguments;
|
|
@@ -4414,6 +4584,7 @@ export class CodexRunner {
|
|
|
4414
4584
|
...(id ? { tool_use_id: id } : {}),
|
|
4415
4585
|
content: result,
|
|
4416
4586
|
is_error: isError,
|
|
4587
|
+
...executionFields,
|
|
4417
4588
|
},
|
|
4418
4589
|
];
|
|
4419
4590
|
}
|
|
@@ -4778,7 +4949,7 @@ export class CodexRunner {
|
|
|
4778
4949
|
lastReportedUsage: state.tokenUsage?.last,
|
|
4779
4950
|
})}`);
|
|
4780
4951
|
if (isRetryableError(new Error(turn.error.message))) {
|
|
4781
|
-
throw
|
|
4952
|
+
throw createCodexRetryMetadataError(turn.error.message, state);
|
|
4782
4953
|
}
|
|
4783
4954
|
yield { type: 'error', error: turn.error.message, errorType: toAgentErrorType(turn.error.message) };
|
|
4784
4955
|
}
|
|
@@ -4800,6 +4971,11 @@ export class CodexRunner {
|
|
|
4800
4971
|
// Codex owns retries within this turn. Throwing here closes our subscription
|
|
4801
4972
|
// before its recovery can finish; runQuery must not submit the input again.
|
|
4802
4973
|
if (params.willRetry === true) {
|
|
4974
|
+
const retryProgress = parseCodexRetryProgress(message);
|
|
4975
|
+
if (retryProgress) {
|
|
4976
|
+
state.modelRetryCount = Math.max(state.modelRetryCount ?? 0, retryProgress.count);
|
|
4977
|
+
state.modelRetryLimit = retryProgress.limit;
|
|
4978
|
+
}
|
|
4803
4979
|
yield {
|
|
4804
4980
|
type: 'notice', severity: 'warn', subtype: 'codex-model-retry',
|
|
4805
4981
|
message: isOutputTokenLimitError(new Error(message))
|
|
@@ -4808,8 +4984,9 @@ export class CodexRunner {
|
|
|
4808
4984
|
};
|
|
4809
4985
|
break;
|
|
4810
4986
|
}
|
|
4811
|
-
if (isRetryableError(new Error(message)))
|
|
4812
|
-
throw
|
|
4987
|
+
if (isRetryableError(new Error(message))) {
|
|
4988
|
+
throw createCodexRetryMetadataError(message, state);
|
|
4989
|
+
}
|
|
4813
4990
|
yield { type: 'error', error: message, errorType: toAgentErrorType(message) };
|
|
4814
4991
|
break;
|
|
4815
4992
|
}
|
|
@@ -5060,9 +5237,11 @@ export class CodexRunner {
|
|
|
5060
5237
|
type: 'tool_result',
|
|
5061
5238
|
name: 'Shell',
|
|
5062
5239
|
result: item.aggregatedOutput ?? '',
|
|
5240
|
+
...(item.aggregatedOutput != null ? { outputKind: 'combined' } : {}),
|
|
5063
5241
|
...(blockedFields ?? {
|
|
5064
5242
|
isError,
|
|
5065
5243
|
...codexItemErrorFields(item),
|
|
5244
|
+
...(itemErrorText ? { error: itemErrorText } : {}),
|
|
5066
5245
|
executed: true,
|
|
5067
5246
|
executionState: isError ? 'failed' : 'completed',
|
|
5068
5247
|
...(exitCode !== undefined ? { exitCode } : {}),
|
|
@@ -5298,15 +5477,29 @@ export class CodexRunner {
|
|
|
5298
5477
|
};
|
|
5299
5478
|
return Object.values(usage).some(value => value !== undefined) ? usage : undefined;
|
|
5300
5479
|
}
|
|
5301
|
-
mapCodexContextUsage(raw) {
|
|
5480
|
+
mapCodexContextUsage(raw, fallbackModel, runtimeWindow) {
|
|
5302
5481
|
if (!raw || typeof raw !== 'object')
|
|
5303
5482
|
return undefined;
|
|
5304
|
-
const totalTokens = this.pickNumber(raw.totalTokens, raw.total_tokens, raw.total);
|
|
5305
|
-
const
|
|
5306
|
-
const
|
|
5307
|
-
|
|
5483
|
+
const totalTokens = this.pickNumber(raw.totalTokens, raw.total_tokens, raw.total, raw.last?.totalTokens, raw.last?.total_tokens);
|
|
5484
|
+
const rawModel = typeof raw.model === 'string' && raw.model.trim() ? raw.model : undefined;
|
|
5485
|
+
const fallbackWindow = fallbackModel ? parseModelId(fallbackModel).contextWindow : undefined;
|
|
5486
|
+
const model = fallbackWindow !== undefined ? fallbackModel : rawModel ?? fallbackModel;
|
|
5487
|
+
if (totalTokens === undefined || totalTokens < 0 || !model)
|
|
5488
|
+
return undefined;
|
|
5489
|
+
const reportedMaxTokens = [runtimeWindow, raw.modelContextWindow, raw.model_context_window, raw.maxTokens, raw.max_tokens, raw.max]
|
|
5490
|
+
.find(value => typeof value === 'number' && Number.isFinite(value) && value > 0);
|
|
5491
|
+
const suffixWindow = parseModelId(model).contextWindow;
|
|
5492
|
+
const maxTokens = reportedMaxTokens ?? suffixWindow ?? findModelSpec(resolveRoot(), model)?.context_window;
|
|
5493
|
+
if (maxTokens === undefined || !Number.isFinite(maxTokens) || maxTokens <= 0)
|
|
5308
5494
|
return undefined;
|
|
5309
|
-
const
|
|
5495
|
+
const nativeWindow = [runtimeWindow, raw.modelContextWindow, raw.model_context_window]
|
|
5496
|
+
.find(value => typeof value === 'number' && Number.isFinite(value) && value > 0);
|
|
5497
|
+
const legacyWindow = [raw.maxTokens, raw.max_tokens, raw.max]
|
|
5498
|
+
.find(value => typeof value === 'number' && Number.isFinite(value) && value > 0);
|
|
5499
|
+
const percentage = (nativeWindow === undefined && (legacyWindow !== undefined || suffixWindow !== undefined)
|
|
5500
|
+
? this.pickNumber(raw.percentage)
|
|
5501
|
+
: undefined)
|
|
5502
|
+
?? Math.round((totalTokens / maxTokens) * 100);
|
|
5310
5503
|
const effort = typeof raw.effort === 'string' ? raw.effort : undefined;
|
|
5311
5504
|
return { totalTokens, maxTokens, percentage, model, effort };
|
|
5312
5505
|
}
|
|
@@ -5314,7 +5507,12 @@ export class CodexRunner {
|
|
|
5314
5507
|
const status = turn.status || 'completed';
|
|
5315
5508
|
const outputLimit = status === 'failed' && isOutputTokenLimitError(new Error(turn.error?.message ?? ''));
|
|
5316
5509
|
const tokenUsage = this.mapCodexTokenUsage(state.tokenUsage?.last ?? turn.tokenUsage ?? turn.usage);
|
|
5317
|
-
const
|
|
5510
|
+
const terminalTokenUsage = turn.tokenUsage ?? turn.usage;
|
|
5511
|
+
const contextUsage = this.mapCodexContextUsage({
|
|
5512
|
+
...state.tokenUsage,
|
|
5513
|
+
...(terminalTokenUsage && typeof terminalTokenUsage === 'object' ? terminalTokenUsage : {}),
|
|
5514
|
+
...(turn.contextUsage ?? state.tokenUsage?.contextUsage ?? state.tokenUsage?.context),
|
|
5515
|
+
}, state.modelContextId ?? state.model, this.pickNumber(state.tokenUsage?.modelContextWindow, state.tokenUsage?.model_context_window, terminalTokenUsage?.modelContextWindow, terminalTokenUsage?.model_context_window));
|
|
5318
5516
|
const terminalReason = status === 'completed'
|
|
5319
5517
|
? undefined
|
|
5320
5518
|
: status === 'interrupted'
|