@canonmsg/codex-plugin 0.19.1 → 0.20.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/README.md +2 -2
- package/dist/app-server-adapter.d.ts +57 -3
- package/dist/app-server-adapter.js +39 -5
- package/dist/host.js +61 -85
- package/dist/permission-mode.d.ts +1 -1
- package/dist/turn-activity.d.ts +10 -0
- package/dist/turn-activity.js +8 -0
- package/package.json +7 -7
- package/dist/adapter.d.ts +0 -96
- package/dist/adapter.js +0 -310
package/README.md
CHANGED
|
@@ -47,9 +47,9 @@ You do not need a git repo for host mode. The plugin passes `--skip-git-repo-che
|
|
|
47
47
|
- Interrupt by terminating the active Codex turn
|
|
48
48
|
- Tool/running status surfaced while Codex is working
|
|
49
49
|
|
|
50
|
-
##
|
|
50
|
+
## Transport
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
`canon-codex` drives Codex through the `codex app-server` JSON-RPC transport — the only supported transport. Canon routes native plan mode, runtime questions, approvals, tools, and live message deltas. A Codex CLI new enough to provide `codex app-server` is required.
|
|
53
53
|
|
|
54
54
|
Current Canon control truth for Codex host mode:
|
|
55
55
|
|
|
@@ -1,4 +1,57 @@
|
|
|
1
|
-
|
|
1
|
+
export type CodexSandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access';
|
|
2
|
+
export type CodexApprovalPolicy = 'untrusted' | 'on-request' | 'never';
|
|
3
|
+
export type CodexEvent = {
|
|
4
|
+
type: 'thread.started';
|
|
5
|
+
threadId: string;
|
|
6
|
+
} | {
|
|
7
|
+
type: 'turn.started';
|
|
8
|
+
} | {
|
|
9
|
+
type: 'message';
|
|
10
|
+
text: string;
|
|
11
|
+
delta?: string;
|
|
12
|
+
itemId?: string;
|
|
13
|
+
} | {
|
|
14
|
+
type: 'plan.updated';
|
|
15
|
+
text: string;
|
|
16
|
+
} | {
|
|
17
|
+
type: 'waiting';
|
|
18
|
+
reason: string;
|
|
19
|
+
} | {
|
|
20
|
+
type: 'command.started';
|
|
21
|
+
command: string;
|
|
22
|
+
itemId?: string;
|
|
23
|
+
} | {
|
|
24
|
+
type: 'command.completed';
|
|
25
|
+
command: string;
|
|
26
|
+
output: string;
|
|
27
|
+
exitCode: number | null;
|
|
28
|
+
itemId?: string;
|
|
29
|
+
} | {
|
|
30
|
+
type: 'turn.completed';
|
|
31
|
+
usage?: {
|
|
32
|
+
input_tokens?: number;
|
|
33
|
+
cached_input_tokens?: number;
|
|
34
|
+
output_tokens?: number;
|
|
35
|
+
};
|
|
36
|
+
} | {
|
|
37
|
+
type: 'skills.changed';
|
|
38
|
+
};
|
|
39
|
+
export interface CodexServerRequest {
|
|
40
|
+
id: string | number;
|
|
41
|
+
method: string;
|
|
42
|
+
params: Record<string, unknown>;
|
|
43
|
+
}
|
|
44
|
+
export interface CodexRunTurnOptions {
|
|
45
|
+
planMode?: boolean;
|
|
46
|
+
onServerRequest?: (request: CodexServerRequest) => Promise<unknown>;
|
|
47
|
+
}
|
|
48
|
+
export interface CodexTurnResult {
|
|
49
|
+
threadId: string | null;
|
|
50
|
+
finalMessage: string | null;
|
|
51
|
+
exitCode: number | null;
|
|
52
|
+
interrupted: boolean;
|
|
53
|
+
errorText: string | null;
|
|
54
|
+
}
|
|
2
55
|
export type JsonRecord = Record<string, unknown>;
|
|
3
56
|
export interface CodexSkillMetadata {
|
|
4
57
|
name: string;
|
|
@@ -14,7 +67,6 @@ export declare class CodexAppServerAdapter {
|
|
|
14
67
|
private model;
|
|
15
68
|
private reasoningEffort;
|
|
16
69
|
private readonly sandbox;
|
|
17
|
-
private readonly legacyApprovalPolicy;
|
|
18
70
|
private readonly addDirs;
|
|
19
71
|
private readonly configOverrides;
|
|
20
72
|
private readonly fullAuto;
|
|
@@ -39,6 +91,7 @@ export declare class CodexAppServerAdapter {
|
|
|
39
91
|
private skillsCache;
|
|
40
92
|
private messageTextByItem;
|
|
41
93
|
private planText;
|
|
94
|
+
private traceTurnEpochMs;
|
|
42
95
|
constructor(opts: {
|
|
43
96
|
cwd: string;
|
|
44
97
|
threadId?: string | null;
|
|
@@ -46,7 +99,6 @@ export declare class CodexAppServerAdapter {
|
|
|
46
99
|
model?: string | null;
|
|
47
100
|
reasoningEffort?: string | null;
|
|
48
101
|
sandbox?: CodexSandboxMode | null;
|
|
49
|
-
approvalPolicy?: CodexApprovalPolicy | null;
|
|
50
102
|
addDirs?: string[];
|
|
51
103
|
configOverrides?: string[];
|
|
52
104
|
fullAuto?: boolean;
|
|
@@ -96,4 +148,6 @@ export declare class CodexAppServerAdapter {
|
|
|
96
148
|
private clearActiveTurn;
|
|
97
149
|
private sendRequest;
|
|
98
150
|
private write;
|
|
151
|
+
private trace;
|
|
152
|
+
private traceLine;
|
|
99
153
|
}
|
|
@@ -6,7 +6,6 @@ export class CodexAppServerAdapter {
|
|
|
6
6
|
model;
|
|
7
7
|
reasoningEffort;
|
|
8
8
|
sandbox;
|
|
9
|
-
legacyApprovalPolicy;
|
|
10
9
|
addDirs;
|
|
11
10
|
configOverrides;
|
|
12
11
|
fullAuto;
|
|
@@ -31,6 +30,7 @@ export class CodexAppServerAdapter {
|
|
|
31
30
|
skillsCache = null;
|
|
32
31
|
messageTextByItem = new Map();
|
|
33
32
|
planText = '';
|
|
33
|
+
traceTurnEpochMs = null;
|
|
34
34
|
constructor(opts) {
|
|
35
35
|
this.cwd = opts.cwd;
|
|
36
36
|
this.threadId = opts.threadId ?? null;
|
|
@@ -38,7 +38,6 @@ export class CodexAppServerAdapter {
|
|
|
38
38
|
this.model = opts.model ?? null;
|
|
39
39
|
this.reasoningEffort = opts.reasoningEffort ?? null;
|
|
40
40
|
this.sandbox = opts.sandbox ?? null;
|
|
41
|
-
this.legacyApprovalPolicy = opts.approvalPolicy ?? null;
|
|
42
41
|
this.addDirs = opts.addDirs ?? [];
|
|
43
42
|
this.configOverrides = opts.configOverrides ?? [];
|
|
44
43
|
this.fullAuto = opts.fullAuto ?? false;
|
|
@@ -117,6 +116,8 @@ export class CodexAppServerAdapter {
|
|
|
117
116
|
this.interrupted = false;
|
|
118
117
|
this.messageTextByItem.clear();
|
|
119
118
|
this.planText = '';
|
|
119
|
+
this.traceTurnEpochMs = Date.now();
|
|
120
|
+
this.trace('turn/run begin');
|
|
120
121
|
try {
|
|
121
122
|
if (this.threadId && this.loadedThreadId !== this.threadId) {
|
|
122
123
|
const resumed = await this.sendRequest('thread/resume', {
|
|
@@ -158,9 +159,11 @@ export class CodexAppServerAdapter {
|
|
|
158
159
|
this.currentTurnReject = reject;
|
|
159
160
|
});
|
|
160
161
|
turnPromise.catch(() => { });
|
|
162
|
+
const turnInput = await this.buildTurnInput(prompt, imagePaths);
|
|
163
|
+
this.trace('turn/start sent');
|
|
161
164
|
const turnStarted = await this.sendRequest('turn/start', {
|
|
162
165
|
threadId: this.threadId,
|
|
163
|
-
input:
|
|
166
|
+
input: turnInput,
|
|
164
167
|
...(this.model ? { model: this.model } : {}),
|
|
165
168
|
...this.sandboxPolicyPayload(_extraAddDirs),
|
|
166
169
|
collaborationMode: {
|
|
@@ -172,6 +175,7 @@ export class CodexAppServerAdapter {
|
|
|
172
175
|
},
|
|
173
176
|
},
|
|
174
177
|
});
|
|
178
|
+
this.trace('turn/start ack');
|
|
175
179
|
const turn = turnStarted.turn;
|
|
176
180
|
if (this.currentTurnResolve) {
|
|
177
181
|
this.currentTurnId = readString(turn, 'id') ?? null;
|
|
@@ -187,7 +191,7 @@ export class CodexAppServerAdapter {
|
|
|
187
191
|
resolveApprovalPolicy() {
|
|
188
192
|
if (this.bypassApprovalsAndSandbox || this.fullAuto)
|
|
189
193
|
return 'never';
|
|
190
|
-
return
|
|
194
|
+
return null;
|
|
191
195
|
}
|
|
192
196
|
configPayload() {
|
|
193
197
|
const config = {};
|
|
@@ -322,6 +326,7 @@ export class CodexAppServerAdapter {
|
|
|
322
326
|
const message = parseJson(line);
|
|
323
327
|
if (!message)
|
|
324
328
|
return;
|
|
329
|
+
this.traceLine(line, message);
|
|
325
330
|
if ('id' in message && ('result' in message || 'error' in message) && !('method' in message)) {
|
|
326
331
|
const id = Number(message.id);
|
|
327
332
|
const pending = this.pending.get(id);
|
|
@@ -395,7 +400,7 @@ export class CodexAppServerAdapter {
|
|
|
395
400
|
this.messageTextByItem.set(itemId, next);
|
|
396
401
|
this.currentFinalMessage = this.joinedAgentMessageText() ?? this.currentFinalMessage;
|
|
397
402
|
if (next.trim())
|
|
398
|
-
this.currentOnEvent?.({ type: 'message', text: next, itemId });
|
|
403
|
+
this.currentOnEvent?.({ type: 'message', text: next, delta, itemId });
|
|
399
404
|
return;
|
|
400
405
|
}
|
|
401
406
|
if (method === 'turn/plan/updated') {
|
|
@@ -467,6 +472,7 @@ export class CodexAppServerAdapter {
|
|
|
467
472
|
return;
|
|
468
473
|
}
|
|
469
474
|
if (method === 'turn/completed') {
|
|
475
|
+
this.trace('turn/completed notification');
|
|
470
476
|
const turn = params.turn;
|
|
471
477
|
const status = turn?.status;
|
|
472
478
|
if (isRecord(status) && status.type === 'failed') {
|
|
@@ -530,6 +536,7 @@ export class CodexAppServerAdapter {
|
|
|
530
536
|
this.currentErrorText = null;
|
|
531
537
|
this.messageTextByItem.clear();
|
|
532
538
|
this.planText = '';
|
|
539
|
+
this.traceTurnEpochMs = null;
|
|
533
540
|
}
|
|
534
541
|
sendRequest(method, params) {
|
|
535
542
|
const id = this.requestSeq++;
|
|
@@ -549,6 +556,33 @@ export class CodexAppServerAdapter {
|
|
|
549
556
|
throw new Error('Codex app-server is not running');
|
|
550
557
|
this.child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
551
558
|
}
|
|
559
|
+
trace(message) {
|
|
560
|
+
if (!isCodexTraceEnabled())
|
|
561
|
+
return;
|
|
562
|
+
const elapsedMs = this.traceTurnEpochMs === null ? 0 : Date.now() - this.traceTurnEpochMs;
|
|
563
|
+
console.error(`[canon-codex-trace] +${elapsedMs}ms ${message}`);
|
|
564
|
+
}
|
|
565
|
+
traceLine(line, message) {
|
|
566
|
+
if (!isCodexTraceEnabled())
|
|
567
|
+
return;
|
|
568
|
+
const method = typeof message.method === 'string' ? message.method : null;
|
|
569
|
+
if (!method) {
|
|
570
|
+
const id = 'id' in message ? ` id=${String(message.id)}` : '';
|
|
571
|
+
this.trace(`line bytes=${line.length} response${id}`);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
if (method === 'item/agentMessage/delta') {
|
|
575
|
+
const params = isRecord(message.params) ? message.params : {};
|
|
576
|
+
const itemId = readString(params, 'itemId') ?? 'agent-message';
|
|
577
|
+
const delta = readRawString(params, 'delta') ?? '';
|
|
578
|
+
this.trace(`line bytes=${line.length} method=${method} itemId=${itemId} deltaLen=${delta.length}`);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
this.trace(`line bytes=${line.length} method=${method}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
function isCodexTraceEnabled() {
|
|
585
|
+
return process.env.CANON_CODEX_TRACE_EVENTS === '1';
|
|
552
586
|
}
|
|
553
587
|
function parseJson(line) {
|
|
554
588
|
try {
|
package/dist/host.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { setDefaultResultOrder } from 'node:dns';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import { spawnSync } from 'node:child_process';
|
|
5
4
|
import { dirname } from 'node:path';
|
|
6
5
|
import { parseArgs } from 'node:util';
|
|
7
6
|
import { getCodexImagePath, inferUploadMimeType, materializeMessageMedia, materializeReplyContextMedia, } from '@canonmsg/agent-sdk';
|
|
@@ -16,8 +15,7 @@ import { STARTUP_RECOVERY_MAX_MESSAGES, STARTUP_RECOVERY_PAGE_SIZE, captureTurnA
|
|
|
16
15
|
import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, buildPlanApprovalRequest, resolveQuestionAllowOther, buildCanonTurnContextV2, buildFirstPartyCodingRuntimeDescriptor, buildRuntimePresentationPolicy, buildCanonInboundFrameV1, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, EXECUTION_ENVIRONMENT_MODES, DEFAULT_RUNTIME_CAPABILITIES, normalizeTurnMetadata, parseRuntimeCardV1, shouldTriggerAgentTurn, renderCanonHostInboundContent, renderCodingHostInboundPrompt, } from '@canonmsg/core/contract';
|
|
17
16
|
import { ExecutionEnvironmentError, buildConfiguredWorkspaceOptionsWithRoots, buildLocalRuntimeId, buildPublicWorkspaceOptions, buildPublicWorkspaceRoots, heartbeatLocalRuntimeEntry, loadRuntimeSessionState, markLocalRuntimeStopped, prepareConversationEnvironment, releaseConversationEnvironment, resolveCanonAgent, saveRuntimeSessionState, upsertLocalRuntimeEntry, } from '@canonmsg/core/local';
|
|
18
17
|
import { readHostSessionConfig, resolveHostWorkspaceCwd, } from '@canonmsg/core/host';
|
|
19
|
-
import {
|
|
20
|
-
import { CodexAppServerAdapter } from './app-server-adapter.js';
|
|
18
|
+
import { CodexAppServerAdapter, } from './app-server-adapter.js';
|
|
21
19
|
import { CODEX_APP_DYNAMIC_TOOLS, deniedCodexAppToolResult, handleCodexAppToolCall, isCodexAppToolCall, } from './codex-app-tools.js';
|
|
22
20
|
import { mapCanonApprovalResultToCodexDecision, mapCodexAppServerApprovalRequest, } from './app-server-approval.js';
|
|
23
21
|
import { clearStoredThreadId, buildCodexThreadPolicyFingerprint, loadStoredThreadId, saveStoredThreadId, } from './session-store.js';
|
|
@@ -26,7 +24,7 @@ import { detectCodexCliVersion } from './codex-cli-version.js';
|
|
|
26
24
|
import { buildCodexModelGuardMessage, formatCodexTurnFailure, isRecoverableCodexThreadError, } from './error-format.js';
|
|
27
25
|
import { attachCodexControlNotifications } from './control-channel.js';
|
|
28
26
|
import { runCli } from './cli-entry.js';
|
|
29
|
-
import { beginCommandBlock, claimCommandBlock, createCommandBlockTracker, hasSpeechSegmentText,
|
|
27
|
+
import { beginCommandBlock, applyCodexMessageToStreamingOutput, claimCommandBlock, createCommandBlockTracker, hasSpeechSegmentText, } from './turn-activity.js';
|
|
30
28
|
const HELP = `canon-codex — run a local Codex agent host for Canon
|
|
31
29
|
|
|
32
30
|
USAGE
|
|
@@ -108,6 +106,35 @@ const CODEX_RUNTIME_CAPABILITIES = {
|
|
|
108
106
|
supportsQueue: true,
|
|
109
107
|
supportsNonFinalPermanentMessages: false,
|
|
110
108
|
};
|
|
109
|
+
function isCodexTraceEnabled() {
|
|
110
|
+
return process.env.CANON_CODEX_TRACE_EVENTS === '1';
|
|
111
|
+
}
|
|
112
|
+
function createCodexTracePort(port) {
|
|
113
|
+
if (!isCodexTraceEnabled())
|
|
114
|
+
return port;
|
|
115
|
+
return new Proxy(port, {
|
|
116
|
+
get(target, property, receiver) {
|
|
117
|
+
if (property !== 'publishStreaming') {
|
|
118
|
+
return Reflect.get(target, property, receiver);
|
|
119
|
+
}
|
|
120
|
+
return async (input) => {
|
|
121
|
+
const startedAt = Date.now();
|
|
122
|
+
const textLength = typeof input.text === 'string' ? input.text.length : 0;
|
|
123
|
+
console.error(`[canon-codex-trace] publishStreaming sent conversation=${input.conversationId} turn=${input.turnId ?? input.messageId ?? 'unknown'} textLen=${textLength}`);
|
|
124
|
+
try {
|
|
125
|
+
const result = await target.publishStreaming(input);
|
|
126
|
+
console.error(`[canon-codex-trace] publishStreaming ack conversation=${input.conversationId} ackMs=${Date.now() - startedAt}`);
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
131
|
+
console.error(`[canon-codex-trace] publishStreaming error conversation=${input.conversationId} ackMs=${Date.now() - startedAt} error=${message}`);
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
111
138
|
// This host process resolves and locks exactly one agent profile. The lock
|
|
112
139
|
// handle returned by resolveCanonAgent is held here so the top-level runCli
|
|
113
140
|
// error handler (outside main's scope) can release it on a failed start —
|
|
@@ -319,9 +346,7 @@ function resolveCodexEffectiveRuntimePolicy(input) {
|
|
|
319
346
|
}
|
|
320
347
|
const approvalOverride = mapCanonPermissionToCodex(permissionMode);
|
|
321
348
|
const defaultSandbox = (stringArg(input.args, 'sandbox') ?? null);
|
|
322
|
-
const defaultApprovalPolicy = (stringArg(input.args, 'ask-for-approval') ?? null);
|
|
323
349
|
const sandbox = approvalOverride ? approvalOverride.sandbox : defaultSandbox;
|
|
324
|
-
const approvalPolicy = approvalOverride ? null : defaultApprovalPolicy;
|
|
325
350
|
const fullAuto = approvalOverride ? approvalOverride.fullAuto : boolArg(input.args, 'full-auto');
|
|
326
351
|
const bypassApprovalsAndSandbox = approvalOverride
|
|
327
352
|
? approvalOverride.bypassApprovalsAndSandbox
|
|
@@ -331,7 +356,6 @@ function resolveCodexEffectiveRuntimePolicy(input) {
|
|
|
331
356
|
executionMode: input.environment.mode,
|
|
332
357
|
permissionMode: permissionMode ?? null,
|
|
333
358
|
sandbox,
|
|
334
|
-
approvalPolicy,
|
|
335
359
|
fullAuto,
|
|
336
360
|
bypassApprovalsAndSandbox,
|
|
337
361
|
});
|
|
@@ -339,7 +363,6 @@ function resolveCodexEffectiveRuntimePolicy(input) {
|
|
|
339
363
|
...(model ? { model } : {}),
|
|
340
364
|
...(permissionMode ? { permissionMode } : {}),
|
|
341
365
|
sandbox,
|
|
342
|
-
approvalPolicy,
|
|
343
366
|
fullAuto,
|
|
344
367
|
bypassApprovalsAndSandbox,
|
|
345
368
|
fingerprint,
|
|
@@ -397,17 +420,6 @@ function stringArgs(value) {
|
|
|
397
420
|
? value.filter((item) => typeof item === 'string' && item.trim().length > 0)
|
|
398
421
|
: undefined;
|
|
399
422
|
}
|
|
400
|
-
function supportsCodexAppServer(codexBin) {
|
|
401
|
-
if (process.env.CANON_CODEX_TRANSPORT === 'exec')
|
|
402
|
-
return false;
|
|
403
|
-
if (process.env.CANON_CODEX_TRANSPORT === 'app-server')
|
|
404
|
-
return true;
|
|
405
|
-
const result = spawnSync(codexBin, ['app-server', '--help'], {
|
|
406
|
-
encoding: 'utf8',
|
|
407
|
-
stdio: ['ignore', 'ignore', 'ignore'],
|
|
408
|
-
});
|
|
409
|
-
return result.status === 0;
|
|
410
|
-
}
|
|
411
423
|
function parsePlanCommand(content) {
|
|
412
424
|
const trimmed = content.trimStart();
|
|
413
425
|
if (!trimmed.startsWith('/plan'))
|
|
@@ -483,7 +495,6 @@ export async function main() {
|
|
|
483
495
|
model: { type: 'string' },
|
|
484
496
|
sandbox: { type: 'string' },
|
|
485
497
|
'ask-for-approval': { type: 'string' },
|
|
486
|
-
'codex-profile': { type: 'string' },
|
|
487
498
|
'add-dir': { type: 'string', multiple: true },
|
|
488
499
|
workspace: { type: 'string', multiple: true },
|
|
489
500
|
'workspace-root': { type: 'string', multiple: true },
|
|
@@ -516,14 +527,12 @@ export async function main() {
|
|
|
516
527
|
}
|
|
517
528
|
const codexBin = typeof args['codex-bin'] === 'string' ? args['codex-bin'] : 'codex';
|
|
518
529
|
const codexCliStatus = detectCodexCliVersion(codexBin);
|
|
519
|
-
const useAppServer = supportsCodexAppServer(codexBin);
|
|
520
530
|
if (codexCliStatus.version) {
|
|
521
531
|
console.error(`[canon-codex] Detected Codex CLI ${codexCliStatus.version} (${codexBin})`);
|
|
522
532
|
}
|
|
523
533
|
else {
|
|
524
534
|
console.error(`[canon-codex] Could not detect Codex CLI version for ${codexBin}: ${codexCliStatus.error ?? 'unknown result'}`);
|
|
525
535
|
}
|
|
526
|
-
console.error(`[canon-codex] Codex transport: ${useAppServer ? 'app-server' : 'exec --json'}`);
|
|
527
536
|
const { agentName: profileAgentName, profile, lockHandle, } = resolveCanonAgent({ logPrefix: '[canon-codex]', expectedClientType: 'codex' });
|
|
528
537
|
activeLockHandle = lockHandle ?? null;
|
|
529
538
|
console.error(`[canon-codex] Starting${profile ? ` (profile: ${profile})` : ''} in ${workingDir}`);
|
|
@@ -554,7 +563,7 @@ export async function main() {
|
|
|
554
563
|
});
|
|
555
564
|
// The BridgeClient's zod-derived result shapes mirror the core wire types
|
|
556
565
|
// structurally; the port pins the strong types the host machinery uses.
|
|
557
|
-
const port = bridge.client;
|
|
566
|
+
const port = createCodexTracePort(bridge.client);
|
|
558
567
|
bridge.client.onProtocolError((error) => {
|
|
559
568
|
console.error(`[canon-codex] Bridge protocol error: ${error.message}`);
|
|
560
569
|
});
|
|
@@ -718,35 +727,19 @@ export async function main() {
|
|
|
718
727
|
const initialEffort = config?.effort && CODEX_EFFORT_VALUES.has(config.effort)
|
|
719
728
|
? config.effort
|
|
720
729
|
: null;
|
|
721
|
-
const adapter =
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
dynamicTools: CODEX_APP_DYNAMIC_TOOLS,
|
|
735
|
-
})
|
|
736
|
-
: new CodexConversationAdapter({
|
|
737
|
-
cwd: sessionCwd,
|
|
738
|
-
threadId: storedThreadId,
|
|
739
|
-
codexBin,
|
|
740
|
-
model: policy.model ?? null,
|
|
741
|
-
reasoningEffort: initialEffort,
|
|
742
|
-
sandbox: policy.sandbox,
|
|
743
|
-
approvalPolicy: policy.approvalPolicy,
|
|
744
|
-
codexProfile: typeof args['codex-profile'] === 'string' ? args['codex-profile'] : null,
|
|
745
|
-
addDirs: args['add-dir'] ?? [],
|
|
746
|
-
configOverrides: args.config ?? [],
|
|
747
|
-
fullAuto: policy.fullAuto,
|
|
748
|
-
bypassApprovalsAndSandbox: policy.bypassApprovalsAndSandbox,
|
|
749
|
-
});
|
|
730
|
+
const adapter = new CodexAppServerAdapter({
|
|
731
|
+
cwd: sessionCwd,
|
|
732
|
+
threadId: storedThreadId,
|
|
733
|
+
codexBin,
|
|
734
|
+
model: policy.model ?? null,
|
|
735
|
+
reasoningEffort: initialEffort,
|
|
736
|
+
sandbox: policy.sandbox,
|
|
737
|
+
addDirs: args['add-dir'] ?? [],
|
|
738
|
+
configOverrides: args.config ?? [],
|
|
739
|
+
fullAuto: policy.fullAuto,
|
|
740
|
+
bypassApprovalsAndSandbox: policy.bypassApprovalsAndSandbox,
|
|
741
|
+
dynamicTools: CODEX_APP_DYNAMIC_TOOLS,
|
|
742
|
+
});
|
|
750
743
|
// eslint-disable-next-line prefer-const -- session must be declared before the module closures but assigned after them
|
|
751
744
|
let session;
|
|
752
745
|
// ── Per-session RTDB writers (generic skeleton from @canonmsg/agent-host) ──
|
|
@@ -976,7 +969,12 @@ export async function main() {
|
|
|
976
969
|
session.turnState = 'streaming';
|
|
977
970
|
writers.writeTurn();
|
|
978
971
|
writers.stopVisibleWorkSignal();
|
|
979
|
-
writers.streamingOutput
|
|
972
|
+
applyCodexMessageToStreamingOutput(writers.streamingOutput, {
|
|
973
|
+
turnId: session.currentTurnId,
|
|
974
|
+
itemId: event.itemId,
|
|
975
|
+
text: event.text,
|
|
976
|
+
delta: event.delta,
|
|
977
|
+
});
|
|
980
978
|
return;
|
|
981
979
|
}
|
|
982
980
|
if (event.type === 'plan.updated') {
|
|
@@ -1304,9 +1302,6 @@ export async function main() {
|
|
|
1304
1302
|
const params = request.params;
|
|
1305
1303
|
const expiresAt = Date.now() + 30 * 60_000;
|
|
1306
1304
|
if (request.method === 'item/tool/call' && isCodexAppToolCall(params)) {
|
|
1307
|
-
if (!(session.adapter instanceof CodexAppServerAdapter)) {
|
|
1308
|
-
return deniedCodexAppToolResult('codex_app tools require the Codex app-server transport.');
|
|
1309
|
-
}
|
|
1310
1305
|
if (!session.currentTurnCanUseCodexAppTools) {
|
|
1311
1306
|
return deniedCodexAppToolResult('Only the Canon owner can use codex_app tools.');
|
|
1312
1307
|
}
|
|
@@ -1537,11 +1532,9 @@ export async function main() {
|
|
|
1537
1532
|
const renderedContent = renderInboundContent(input.message, materialized);
|
|
1538
1533
|
const turnMetadata = normalizeTurnMetadata(input.message.metadata);
|
|
1539
1534
|
const requestedPlanMode = turnMetadata?.requestedTurnMode === 'plan';
|
|
1540
|
-
const planCommand =
|
|
1541
|
-
?
|
|
1542
|
-
|
|
1543
|
-
: parsePlanCommand(renderedContent)
|
|
1544
|
-
: { planMode: false, content: renderedContent };
|
|
1535
|
+
const planCommand = requestedPlanMode
|
|
1536
|
+
? { planMode: true, content: renderedContent }
|
|
1537
|
+
: parsePlanCommand(renderedContent);
|
|
1545
1538
|
const content = planCommand.content;
|
|
1546
1539
|
const hydrated = await loadHydratedInboundContext({
|
|
1547
1540
|
conversationId: input.conversationId,
|
|
@@ -1716,16 +1709,14 @@ export async function main() {
|
|
|
1716
1709
|
permissionModes: [...codexPermissionEnvelope.availablePermissionModes],
|
|
1717
1710
|
defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode,
|
|
1718
1711
|
presentation: runtimePresentation,
|
|
1719
|
-
supportsPlanMode:
|
|
1720
|
-
supportsCompact:
|
|
1721
|
-
supportsRichCards:
|
|
1712
|
+
supportsPlanMode: true,
|
|
1713
|
+
supportsCompact: true,
|
|
1714
|
+
supportsRichCards: true,
|
|
1722
1715
|
skills: codexSkills,
|
|
1723
1716
|
}),
|
|
1724
1717
|
});
|
|
1725
1718
|
let runtimeDescriptor = buildCurrentRuntimeDescriptor();
|
|
1726
1719
|
async function refreshCodexSkillInventory(forceReload = false) {
|
|
1727
|
-
if (!useAppServer)
|
|
1728
|
-
return;
|
|
1729
1720
|
const probe = new CodexAppServerAdapter({
|
|
1730
1721
|
cwd: workingDir,
|
|
1731
1722
|
codexBin,
|
|
@@ -1810,10 +1801,6 @@ export async function main() {
|
|
|
1810
1801
|
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Cannot compact context: no live Codex session`);
|
|
1811
1802
|
return;
|
|
1812
1803
|
}
|
|
1813
|
-
if (!(session.adapter instanceof CodexAppServerAdapter)) {
|
|
1814
|
-
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Cannot compact context: compact is only available for Codex app-server sessions`);
|
|
1815
|
-
return;
|
|
1816
|
-
}
|
|
1817
1804
|
try {
|
|
1818
1805
|
await session.adapter.compactThread();
|
|
1819
1806
|
console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Compact requested`);
|
|
@@ -1896,24 +1883,16 @@ export async function main() {
|
|
|
1896
1883
|
const payload = {
|
|
1897
1884
|
descriptor,
|
|
1898
1885
|
surfaceMode: 'host',
|
|
1899
|
-
// The exec --json transport cannot block on approvals — without a
|
|
1900
|
-
// strip-level warning a user can believe they have an approval
|
|
1901
|
-
// gate they do not have.
|
|
1902
|
-
...(useAppServer
|
|
1903
|
-
? {}
|
|
1904
|
-
: { warning: "Approvals can't block on this Codex CLI — update Codex to enable the app-server transport and approval gates." }),
|
|
1905
1886
|
statusItems: [
|
|
1906
1887
|
{
|
|
1907
1888
|
id: 'transport',
|
|
1908
1889
|
label: 'Transport',
|
|
1909
|
-
value:
|
|
1890
|
+
value: 'app-server',
|
|
1910
1891
|
},
|
|
1911
1892
|
{
|
|
1912
1893
|
id: 'streaming',
|
|
1913
1894
|
label: 'Live output',
|
|
1914
|
-
value:
|
|
1915
|
-
? 'Plans, questions, approvals, tools, and message deltas'
|
|
1916
|
-
: 'Thinking, tools, and completed-message previews',
|
|
1895
|
+
value: 'Plans, questions, approvals, tools, and message deltas',
|
|
1917
1896
|
},
|
|
1918
1897
|
{
|
|
1919
1898
|
id: 'codex-cli',
|
|
@@ -1924,8 +1903,7 @@ export async function main() {
|
|
|
1924
1903
|
{
|
|
1925
1904
|
id: 'nativeActions',
|
|
1926
1905
|
label: 'Native actions',
|
|
1927
|
-
value:
|
|
1928
|
-
...(useAppServer ? {} : { tone: 'warning' }),
|
|
1906
|
+
value: 'Enabled',
|
|
1929
1907
|
},
|
|
1930
1908
|
{
|
|
1931
1909
|
id: 'mediaOut',
|
|
@@ -1944,9 +1922,7 @@ export async function main() {
|
|
|
1944
1922
|
fallbackReason: resolveExecutionFallbackReason(session?.environment),
|
|
1945
1923
|
},
|
|
1946
1924
|
notes: [
|
|
1947
|
-
|
|
1948
|
-
? 'This Codex host uses the app-server transport, so Canon can route native plan mode, runtime questions, approvals, and live turn updates.'
|
|
1949
|
-
: 'This Codex host uses the current exec --json transport, so Canon can show thinking, tool activity, and completed assistant-message previews, but not native plan questions or structured approvals.',
|
|
1925
|
+
'This Codex host uses the app-server transport, so Canon can route native plan mode, runtime questions, approvals, and live turn updates.',
|
|
1950
1926
|
],
|
|
1951
1927
|
};
|
|
1952
1928
|
await port.publishRuntimeStatus({
|
package/dist/turn-activity.d.ts
CHANGED
|
@@ -24,6 +24,16 @@ export declare function createCommandBlockTracker(): CommandBlockTracker;
|
|
|
24
24
|
* only owns the codex id scheme.
|
|
25
25
|
*/
|
|
26
26
|
export declare function textSegmentBlockId(turnId: string | null | undefined, itemId?: string): string;
|
|
27
|
+
export interface CodexTextSegmentOutput {
|
|
28
|
+
appendTextSegmentDelta(id: string, delta: string): void;
|
|
29
|
+
replaceTextSegmentSnapshot(id: string, text: string): void;
|
|
30
|
+
}
|
|
31
|
+
export declare function applyCodexMessageToStreamingOutput(output: CodexTextSegmentOutput, input: {
|
|
32
|
+
turnId: string | null | undefined;
|
|
33
|
+
itemId?: string;
|
|
34
|
+
text: string;
|
|
35
|
+
delta?: string;
|
|
36
|
+
}): void;
|
|
27
37
|
export declare function beginCommandBlock(tracker: CommandBlockTracker, input: {
|
|
28
38
|
turnId: string | null | undefined;
|
|
29
39
|
command: string;
|
package/dist/turn-activity.js
CHANGED
|
@@ -27,6 +27,14 @@ function normalizeOptionalString(value) {
|
|
|
27
27
|
export function textSegmentBlockId(turnId, itemId) {
|
|
28
28
|
return `message:${turnId ?? 'turn'}:${normalizeOptionalString(itemId) ?? 'latest'}`;
|
|
29
29
|
}
|
|
30
|
+
export function applyCodexMessageToStreamingOutput(output, input) {
|
|
31
|
+
const segmentId = textSegmentBlockId(input.turnId, input.itemId);
|
|
32
|
+
if (input.delta !== undefined) {
|
|
33
|
+
output.appendTextSegmentDelta(segmentId, input.delta);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
output.replaceTextSegmentSnapshot(segmentId, input.text);
|
|
37
|
+
}
|
|
30
38
|
function nextCommandBlockId(tracker, turnId, itemId) {
|
|
31
39
|
const stableTurnId = turnId ?? 'turn';
|
|
32
40
|
if (itemId)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/codex-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Canon host integration for Codex CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -29,12 +29,12 @@
|
|
|
29
29
|
"prepack": "npm run build"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@canonmsg/agent-host": "^0.
|
|
33
|
-
"@canonmsg/agent-sdk": "^
|
|
34
|
-
"@canonmsg/backend-contracts": "^1.
|
|
35
|
-
"@canonmsg/bridge": "^0.
|
|
36
|
-
"@canonmsg/core": "^3.
|
|
37
|
-
"@canonmsg/framework": "^0.
|
|
32
|
+
"@canonmsg/agent-host": "^0.5.0",
|
|
33
|
+
"@canonmsg/agent-sdk": "^4.0.0",
|
|
34
|
+
"@canonmsg/backend-contracts": "^1.9.0",
|
|
35
|
+
"@canonmsg/bridge": "^0.3.0",
|
|
36
|
+
"@canonmsg/core": "^3.3.0",
|
|
37
|
+
"@canonmsg/framework": "^0.3.0"
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|
|
40
40
|
"node": ">=18.0.0"
|
package/dist/adapter.d.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
export type CodexSandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access';
|
|
2
|
-
export type CodexApprovalPolicy = 'untrusted' | 'on-request' | 'never';
|
|
3
|
-
export type CodexEvent = {
|
|
4
|
-
type: 'thread.started';
|
|
5
|
-
threadId: string;
|
|
6
|
-
} | {
|
|
7
|
-
type: 'turn.started';
|
|
8
|
-
} | {
|
|
9
|
-
type: 'message';
|
|
10
|
-
text: string;
|
|
11
|
-
itemId?: string;
|
|
12
|
-
} | {
|
|
13
|
-
type: 'plan.updated';
|
|
14
|
-
text: string;
|
|
15
|
-
} | {
|
|
16
|
-
type: 'waiting';
|
|
17
|
-
reason: string;
|
|
18
|
-
} | {
|
|
19
|
-
type: 'command.started';
|
|
20
|
-
command: string;
|
|
21
|
-
itemId?: string;
|
|
22
|
-
} | {
|
|
23
|
-
type: 'command.completed';
|
|
24
|
-
command: string;
|
|
25
|
-
output: string;
|
|
26
|
-
exitCode: number | null;
|
|
27
|
-
itemId?: string;
|
|
28
|
-
} | {
|
|
29
|
-
type: 'turn.completed';
|
|
30
|
-
usage?: {
|
|
31
|
-
input_tokens?: number;
|
|
32
|
-
cached_input_tokens?: number;
|
|
33
|
-
output_tokens?: number;
|
|
34
|
-
};
|
|
35
|
-
} | {
|
|
36
|
-
type: 'skills.changed';
|
|
37
|
-
};
|
|
38
|
-
export interface CodexServerRequest {
|
|
39
|
-
id: string | number;
|
|
40
|
-
method: string;
|
|
41
|
-
params: Record<string, unknown>;
|
|
42
|
-
}
|
|
43
|
-
export interface CodexRunTurnOptions {
|
|
44
|
-
planMode?: boolean;
|
|
45
|
-
onServerRequest?: (request: CodexServerRequest) => Promise<unknown>;
|
|
46
|
-
}
|
|
47
|
-
export interface CodexTurnResult {
|
|
48
|
-
threadId: string | null;
|
|
49
|
-
finalMessage: string | null;
|
|
50
|
-
exitCode: number | null;
|
|
51
|
-
interrupted: boolean;
|
|
52
|
-
errorText: string | null;
|
|
53
|
-
}
|
|
54
|
-
export declare class CodexConversationAdapter {
|
|
55
|
-
private readonly cwd;
|
|
56
|
-
private readonly codexBin;
|
|
57
|
-
private model;
|
|
58
|
-
private reasoningEffort;
|
|
59
|
-
private readonly sandbox;
|
|
60
|
-
private readonly legacyApprovalPolicy;
|
|
61
|
-
private readonly codexProfile;
|
|
62
|
-
private readonly addDirs;
|
|
63
|
-
private readonly configOverrides;
|
|
64
|
-
private readonly fullAuto;
|
|
65
|
-
private readonly bypassApprovalsAndSandbox;
|
|
66
|
-
private child;
|
|
67
|
-
private threadId;
|
|
68
|
-
private interruptTimer;
|
|
69
|
-
private interrupted;
|
|
70
|
-
constructor(opts: {
|
|
71
|
-
cwd: string;
|
|
72
|
-
threadId?: string | null;
|
|
73
|
-
codexBin?: string;
|
|
74
|
-
model?: string | null;
|
|
75
|
-
reasoningEffort?: string | null;
|
|
76
|
-
sandbox?: CodexSandboxMode | null;
|
|
77
|
-
approvalPolicy?: CodexApprovalPolicy | null;
|
|
78
|
-
codexProfile?: string | null;
|
|
79
|
-
addDirs?: string[];
|
|
80
|
-
configOverrides?: string[];
|
|
81
|
-
fullAuto?: boolean;
|
|
82
|
-
bypassApprovalsAndSandbox?: boolean;
|
|
83
|
-
});
|
|
84
|
-
getThreadId(): string | null;
|
|
85
|
-
clearThreadId(): void;
|
|
86
|
-
setModel(model: string | null): void;
|
|
87
|
-
/** Sets GPT reasoning effort, applied on the next turn via `-c model_reasoning_effort`. */
|
|
88
|
-
setReasoningEffort(effort: string | null): void;
|
|
89
|
-
isRunning(): boolean;
|
|
90
|
-
interrupt(): Promise<void>;
|
|
91
|
-
runTurn(prompt: string, onEvent: (event: CodexEvent) => void, onLog?: (line: string) => void, imagePaths?: readonly string[], extraAddDirs?: readonly string[], _options?: CodexRunTurnOptions): Promise<CodexTurnResult>;
|
|
92
|
-
private buildAddDirs;
|
|
93
|
-
private buildArgs;
|
|
94
|
-
private canResumeWithCurrentPolicy;
|
|
95
|
-
private clearActiveProcess;
|
|
96
|
-
}
|
package/dist/adapter.js
DELETED
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import { createInterface } from 'node:readline';
|
|
3
|
-
import { isRecoverableCodexThreadError } from './error-format.js';
|
|
4
|
-
export class CodexConversationAdapter {
|
|
5
|
-
cwd;
|
|
6
|
-
codexBin;
|
|
7
|
-
model;
|
|
8
|
-
reasoningEffort;
|
|
9
|
-
sandbox;
|
|
10
|
-
legacyApprovalPolicy;
|
|
11
|
-
codexProfile;
|
|
12
|
-
addDirs;
|
|
13
|
-
configOverrides;
|
|
14
|
-
fullAuto;
|
|
15
|
-
bypassApprovalsAndSandbox;
|
|
16
|
-
child = null;
|
|
17
|
-
threadId;
|
|
18
|
-
interruptTimer = null;
|
|
19
|
-
interrupted = false;
|
|
20
|
-
constructor(opts) {
|
|
21
|
-
this.cwd = opts.cwd;
|
|
22
|
-
this.threadId = opts.threadId ?? null;
|
|
23
|
-
this.codexBin = opts.codexBin ?? 'codex';
|
|
24
|
-
this.model = opts.model ?? null;
|
|
25
|
-
this.reasoningEffort = opts.reasoningEffort ?? null;
|
|
26
|
-
this.sandbox = opts.sandbox ?? null;
|
|
27
|
-
this.legacyApprovalPolicy = opts.approvalPolicy ?? null;
|
|
28
|
-
this.codexProfile = opts.codexProfile ?? null;
|
|
29
|
-
this.addDirs = opts.addDirs ?? [];
|
|
30
|
-
this.configOverrides = opts.configOverrides ?? [];
|
|
31
|
-
this.fullAuto = opts.fullAuto ?? false;
|
|
32
|
-
this.bypassApprovalsAndSandbox = opts.bypassApprovalsAndSandbox ?? false;
|
|
33
|
-
}
|
|
34
|
-
getThreadId() {
|
|
35
|
-
return this.threadId;
|
|
36
|
-
}
|
|
37
|
-
clearThreadId() {
|
|
38
|
-
this.threadId = null;
|
|
39
|
-
}
|
|
40
|
-
setModel(model) {
|
|
41
|
-
this.model = model;
|
|
42
|
-
}
|
|
43
|
-
/** Sets GPT reasoning effort, applied on the next turn via `-c model_reasoning_effort`. */
|
|
44
|
-
setReasoningEffort(effort) {
|
|
45
|
-
this.reasoningEffort = effort && effort.trim() ? effort.trim() : null;
|
|
46
|
-
}
|
|
47
|
-
isRunning() {
|
|
48
|
-
return this.child !== null;
|
|
49
|
-
}
|
|
50
|
-
async interrupt() {
|
|
51
|
-
if (!this.child)
|
|
52
|
-
return;
|
|
53
|
-
this.interrupted = true;
|
|
54
|
-
this.child.kill('SIGINT');
|
|
55
|
-
this.interruptTimer = setTimeout(() => {
|
|
56
|
-
if (this.child)
|
|
57
|
-
this.child.kill('SIGKILL');
|
|
58
|
-
}, 5_000);
|
|
59
|
-
}
|
|
60
|
-
async runTurn(prompt, onEvent, onLog, imagePaths = [], extraAddDirs = [], _options = {}) {
|
|
61
|
-
if (this.child) {
|
|
62
|
-
throw new Error('A Codex turn is already in progress for this conversation');
|
|
63
|
-
}
|
|
64
|
-
const args = this.buildArgs(prompt, imagePaths, extraAddDirs);
|
|
65
|
-
const child = spawn(this.codexBin, args, {
|
|
66
|
-
cwd: this.cwd,
|
|
67
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
68
|
-
});
|
|
69
|
-
this.child = child;
|
|
70
|
-
this.interrupted = false;
|
|
71
|
-
let latestMessage = null;
|
|
72
|
-
let lastErrorText = null;
|
|
73
|
-
const stdout = createInterface({ input: child.stdout });
|
|
74
|
-
const stderr = createInterface({ input: child.stderr });
|
|
75
|
-
stdout.on('line', (line) => {
|
|
76
|
-
const event = parseEventLine(line);
|
|
77
|
-
if (!event)
|
|
78
|
-
return;
|
|
79
|
-
switch (event.type) {
|
|
80
|
-
case 'thread.started':
|
|
81
|
-
this.threadId = event.thread_id;
|
|
82
|
-
onEvent({ type: 'thread.started', threadId: event.thread_id });
|
|
83
|
-
break;
|
|
84
|
-
case 'turn.started':
|
|
85
|
-
onEvent({ type: 'turn.started' });
|
|
86
|
-
break;
|
|
87
|
-
case 'item.started':
|
|
88
|
-
if (event.item?.type === 'command_execution') {
|
|
89
|
-
onEvent({
|
|
90
|
-
type: 'command.started',
|
|
91
|
-
command: String(event.item.command ?? ''),
|
|
92
|
-
...(typeof event.item.id === 'string' && event.item.id.trim()
|
|
93
|
-
? { itemId: event.item.id.trim() }
|
|
94
|
-
: {}),
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
break;
|
|
98
|
-
case 'item.completed':
|
|
99
|
-
if (event.item?.type === 'agent_message') {
|
|
100
|
-
latestMessage = normalizeMessageText(event.item.text);
|
|
101
|
-
if (latestMessage) {
|
|
102
|
-
onEvent({
|
|
103
|
-
type: 'message',
|
|
104
|
-
text: latestMessage,
|
|
105
|
-
...(typeof event.item.id === 'string' && event.item.id.trim()
|
|
106
|
-
? { itemId: event.item.id.trim() }
|
|
107
|
-
: {}),
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
else if (event.item?.type === 'command_execution') {
|
|
112
|
-
onEvent({
|
|
113
|
-
type: 'command.completed',
|
|
114
|
-
command: String(event.item.command ?? ''),
|
|
115
|
-
output: String(event.item.aggregated_output ?? ''),
|
|
116
|
-
exitCode: typeof event.item.exit_code === 'number' ? event.item.exit_code : null,
|
|
117
|
-
...(typeof event.item.id === 'string' && event.item.id.trim()
|
|
118
|
-
? { itemId: event.item.id.trim() }
|
|
119
|
-
: {}),
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
break;
|
|
123
|
-
case 'turn.completed':
|
|
124
|
-
onEvent({ type: 'turn.completed', usage: event.usage });
|
|
125
|
-
break;
|
|
126
|
-
case 'error':
|
|
127
|
-
lastErrorText = normalizeErrorText(event.message) ?? lastErrorText;
|
|
128
|
-
break;
|
|
129
|
-
case 'turn.failed':
|
|
130
|
-
lastErrorText = normalizeErrorText(event.error?.message ?? event.message) ?? lastErrorText;
|
|
131
|
-
break;
|
|
132
|
-
default:
|
|
133
|
-
break;
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
stderr.on('line', (line) => {
|
|
137
|
-
const trimmed = line.trim();
|
|
138
|
-
if (!trimmed)
|
|
139
|
-
return;
|
|
140
|
-
if (isIgnorableCodexLog(trimmed))
|
|
141
|
-
return;
|
|
142
|
-
lastErrorText = trimmed;
|
|
143
|
-
if (isRecoverableCodexThreadError(trimmed))
|
|
144
|
-
return;
|
|
145
|
-
onLog?.(trimmed);
|
|
146
|
-
});
|
|
147
|
-
return await new Promise((resolve, reject) => {
|
|
148
|
-
child.on('error', (error) => {
|
|
149
|
-
this.clearActiveProcess();
|
|
150
|
-
reject(error);
|
|
151
|
-
});
|
|
152
|
-
child.on('close', (code) => {
|
|
153
|
-
stdout.close();
|
|
154
|
-
stderr.close();
|
|
155
|
-
const interrupted = this.interrupted || code === 130;
|
|
156
|
-
const result = {
|
|
157
|
-
threadId: this.threadId,
|
|
158
|
-
finalMessage: latestMessage,
|
|
159
|
-
exitCode: code,
|
|
160
|
-
interrupted,
|
|
161
|
-
errorText: interrupted ? null : lastErrorText,
|
|
162
|
-
};
|
|
163
|
-
this.clearActiveProcess();
|
|
164
|
-
resolve(result);
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
buildAddDirs(extraAddDirs = []) {
|
|
169
|
-
const seen = new Set();
|
|
170
|
-
const dirs = [];
|
|
171
|
-
for (const value of [...this.addDirs, ...extraAddDirs]) {
|
|
172
|
-
const trimmed = value.trim();
|
|
173
|
-
if (!trimmed || seen.has(trimmed))
|
|
174
|
-
continue;
|
|
175
|
-
seen.add(trimmed);
|
|
176
|
-
dirs.push(value);
|
|
177
|
-
}
|
|
178
|
-
return dirs;
|
|
179
|
-
}
|
|
180
|
-
buildArgs(prompt, imagePaths = [], extraAddDirs = []) {
|
|
181
|
-
if (this.threadId && this.canResumeWithCurrentPolicy()) {
|
|
182
|
-
const args = ['exec', 'resume', '--json', '--skip-git-repo-check'];
|
|
183
|
-
if (this.model) {
|
|
184
|
-
args.push('-m', this.model);
|
|
185
|
-
}
|
|
186
|
-
if (this.codexProfile) {
|
|
187
|
-
args.push('-p', this.codexProfile);
|
|
188
|
-
}
|
|
189
|
-
for (const configOverride of this.configOverrides) {
|
|
190
|
-
args.push('-c', configOverride);
|
|
191
|
-
}
|
|
192
|
-
if (this.reasoningEffort) {
|
|
193
|
-
args.push('-c', `model_reasoning_effort="${this.reasoningEffort}"`);
|
|
194
|
-
}
|
|
195
|
-
for (const addDir of this.buildAddDirs(extraAddDirs)) {
|
|
196
|
-
args.push('--add-dir', addDir);
|
|
197
|
-
}
|
|
198
|
-
if (this.fullAuto) {
|
|
199
|
-
args.push('--full-auto');
|
|
200
|
-
}
|
|
201
|
-
if (this.bypassApprovalsAndSandbox) {
|
|
202
|
-
args.push('--dangerously-bypass-approvals-and-sandbox');
|
|
203
|
-
}
|
|
204
|
-
for (const imagePath of imagePaths) {
|
|
205
|
-
args.push('-i', imagePath);
|
|
206
|
-
}
|
|
207
|
-
if (imagePaths.length > 0) {
|
|
208
|
-
args.push('--');
|
|
209
|
-
}
|
|
210
|
-
args.push(this.threadId, prompt);
|
|
211
|
-
return args;
|
|
212
|
-
}
|
|
213
|
-
if (this.threadId) {
|
|
214
|
-
this.threadId = null;
|
|
215
|
-
}
|
|
216
|
-
const args = ['exec', '--json', '--color', 'never', '-C', this.cwd, '--skip-git-repo-check'];
|
|
217
|
-
const execMode = resolveExecMode({
|
|
218
|
-
sandbox: this.sandbox,
|
|
219
|
-
fullAuto: this.fullAuto,
|
|
220
|
-
bypassApprovalsAndSandbox: this.bypassApprovalsAndSandbox,
|
|
221
|
-
});
|
|
222
|
-
if (this.model) {
|
|
223
|
-
args.push('-m', this.model);
|
|
224
|
-
}
|
|
225
|
-
if (this.sandbox) {
|
|
226
|
-
args.push('-s', this.sandbox);
|
|
227
|
-
}
|
|
228
|
-
if (this.codexProfile) {
|
|
229
|
-
args.push('-p', this.codexProfile);
|
|
230
|
-
}
|
|
231
|
-
for (const addDir of this.buildAddDirs(extraAddDirs)) {
|
|
232
|
-
args.push('--add-dir', addDir);
|
|
233
|
-
}
|
|
234
|
-
for (const configOverride of this.configOverrides) {
|
|
235
|
-
args.push('-c', configOverride);
|
|
236
|
-
}
|
|
237
|
-
if (this.reasoningEffort) {
|
|
238
|
-
args.push('-c', `model_reasoning_effort="${this.reasoningEffort}"`);
|
|
239
|
-
}
|
|
240
|
-
if (execMode.fullAuto) {
|
|
241
|
-
args.push('--full-auto');
|
|
242
|
-
}
|
|
243
|
-
if (execMode.bypassApprovalsAndSandbox) {
|
|
244
|
-
args.push('--dangerously-bypass-approvals-and-sandbox');
|
|
245
|
-
}
|
|
246
|
-
for (const imagePath of imagePaths) {
|
|
247
|
-
args.push('-i', imagePath);
|
|
248
|
-
}
|
|
249
|
-
if (imagePaths.length > 0) {
|
|
250
|
-
args.push('--');
|
|
251
|
-
}
|
|
252
|
-
args.push(prompt);
|
|
253
|
-
return args;
|
|
254
|
-
}
|
|
255
|
-
canResumeWithCurrentPolicy() {
|
|
256
|
-
if (this.bypassApprovalsAndSandbox || this.fullAuto) {
|
|
257
|
-
return true;
|
|
258
|
-
}
|
|
259
|
-
return this.sandbox === null;
|
|
260
|
-
}
|
|
261
|
-
clearActiveProcess() {
|
|
262
|
-
if (this.interruptTimer) {
|
|
263
|
-
clearTimeout(this.interruptTimer);
|
|
264
|
-
this.interruptTimer = null;
|
|
265
|
-
}
|
|
266
|
-
this.child = null;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
function parseEventLine(line) {
|
|
270
|
-
const trimmed = line.trim();
|
|
271
|
-
if (!trimmed.startsWith('{'))
|
|
272
|
-
return null;
|
|
273
|
-
try {
|
|
274
|
-
return JSON.parse(trimmed);
|
|
275
|
-
}
|
|
276
|
-
catch {
|
|
277
|
-
return null;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
function normalizeMessageText(value) {
|
|
281
|
-
if (typeof value !== 'string')
|
|
282
|
-
return null;
|
|
283
|
-
const text = value.trim();
|
|
284
|
-
return text ? text : null;
|
|
285
|
-
}
|
|
286
|
-
function normalizeErrorText(value) {
|
|
287
|
-
if (typeof value !== 'string')
|
|
288
|
-
return null;
|
|
289
|
-
const text = value.trim();
|
|
290
|
-
return text ? text : null;
|
|
291
|
-
}
|
|
292
|
-
function resolveExecMode(input) {
|
|
293
|
-
if (input.bypassApprovalsAndSandbox) {
|
|
294
|
-
return { fullAuto: false, bypassApprovalsAndSandbox: true };
|
|
295
|
-
}
|
|
296
|
-
if (input.fullAuto) {
|
|
297
|
-
return { fullAuto: true, bypassApprovalsAndSandbox: false };
|
|
298
|
-
}
|
|
299
|
-
return { fullAuto: false, bypassApprovalsAndSandbox: false };
|
|
300
|
-
}
|
|
301
|
-
function isIgnorableCodexLog(line) {
|
|
302
|
-
return [
|
|
303
|
-
'Reading additional input from stdin...',
|
|
304
|
-
'ignoring interface.defaultPrompt',
|
|
305
|
-
'state db discrepancy during find_thread_path_by_id_str_in_subdir',
|
|
306
|
-
'failed to open state db',
|
|
307
|
-
'failed to initialize state runtime',
|
|
308
|
-
'Failed to delete shell snapshot',
|
|
309
|
-
].some((pattern) => line.includes(pattern));
|
|
310
|
-
}
|