@canonmsg/codex-plugin 0.20.0 → 0.21.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 +4 -4
- package/dist/adapter.d.ts +96 -0
- package/dist/adapter.js +310 -0
- package/dist/app-server-adapter.d.ts +3 -64
- package/dist/app-server-adapter.js +7 -54
- package/dist/app-server-approval.d.ts +1 -1
- package/dist/app-server-approval.js +1 -1
- package/dist/cli-entry.d.ts +2 -2
- package/dist/cli-entry.js +1 -1
- package/dist/control-channel.d.ts +46 -26
- package/dist/control-channel.js +50 -37
- package/dist/host-lifecycle.d.ts +4 -0
- package/dist/host-lifecycle.js +3 -0
- package/dist/host.d.ts +1 -1
- package/dist/host.js +1012 -944
- package/dist/inbound-policy.d.ts +22 -0
- package/dist/inbound-policy.js +46 -0
- package/dist/permission-mode.d.ts +1 -1
- package/dist/register.js +25 -73
- package/dist/session-store.d.ts +1 -1
- package/dist/session-store.js +1 -1
- package/dist/startup-recovery.d.ts +46 -0
- package/dist/startup-recovery.js +59 -0
- package/dist/turn-activity.d.ts +8 -23
- package/dist/turn-activity.js +40 -24
- package/package.json +7 -10
- package/dist/bridge-bin.d.ts +0 -14
- package/dist/bridge-bin.js +0 -27
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
|
+
## Current limitation
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
The stable `codex exec --json` surface exposes thinking state, tool activity, and completed assistant-message previews, but not token-by-token text deltas. v1 therefore publishes live progress and assistant-message snapshots without claiming true token streaming.
|
|
53
53
|
|
|
54
54
|
Current Canon control truth for Codex host mode:
|
|
55
55
|
|
|
@@ -108,7 +108,7 @@ ps aux | rg canon-codex
|
|
|
108
108
|
If you installed the package only inside this repo and not globally, run the built host directly:
|
|
109
109
|
|
|
110
110
|
```bash
|
|
111
|
-
node
|
|
111
|
+
node packages/codex-plugin/dist/host.js --cwd /path/to/project --full-auto
|
|
112
112
|
```
|
|
113
113
|
|
|
114
114
|
If `canon-codex` starts but cannot find the `codex` binary, either fix your `PATH` or launch with an explicit binary path:
|
|
@@ -149,7 +149,7 @@ CANON_AGENT=frontend canon-codex --cwd ~/projects/frontend
|
|
|
149
149
|
## Development
|
|
150
150
|
|
|
151
151
|
```bash
|
|
152
|
-
cd
|
|
152
|
+
cd packages/codex-plugin
|
|
153
153
|
npm install
|
|
154
154
|
npm run build
|
|
155
155
|
```
|
|
@@ -0,0 +1,96 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
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
|
+
}
|
|
@@ -1,57 +1,4 @@
|
|
|
1
|
-
|
|
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
|
-
}
|
|
1
|
+
import type { CodexApprovalPolicy, CodexEvent, CodexRunTurnOptions, CodexSandboxMode, CodexTurnResult } from './adapter.js';
|
|
55
2
|
export type JsonRecord = Record<string, unknown>;
|
|
56
3
|
export interface CodexSkillMetadata {
|
|
57
4
|
name: string;
|
|
@@ -67,6 +14,7 @@ export declare class CodexAppServerAdapter {
|
|
|
67
14
|
private model;
|
|
68
15
|
private reasoningEffort;
|
|
69
16
|
private readonly sandbox;
|
|
17
|
+
private readonly legacyApprovalPolicy;
|
|
70
18
|
private readonly addDirs;
|
|
71
19
|
private readonly configOverrides;
|
|
72
20
|
private readonly fullAuto;
|
|
@@ -91,7 +39,6 @@ export declare class CodexAppServerAdapter {
|
|
|
91
39
|
private skillsCache;
|
|
92
40
|
private messageTextByItem;
|
|
93
41
|
private planText;
|
|
94
|
-
private traceTurnEpochMs;
|
|
95
42
|
constructor(opts: {
|
|
96
43
|
cwd: string;
|
|
97
44
|
threadId?: string | null;
|
|
@@ -99,6 +46,7 @@ export declare class CodexAppServerAdapter {
|
|
|
99
46
|
model?: string | null;
|
|
100
47
|
reasoningEffort?: string | null;
|
|
101
48
|
sandbox?: CodexSandboxMode | null;
|
|
49
|
+
approvalPolicy?: CodexApprovalPolicy | null;
|
|
102
50
|
addDirs?: string[];
|
|
103
51
|
configOverrides?: string[];
|
|
104
52
|
fullAuto?: boolean;
|
|
@@ -136,18 +84,9 @@ export declare class CodexAppServerAdapter {
|
|
|
136
84
|
private handleLine;
|
|
137
85
|
private handleServerRequest;
|
|
138
86
|
private handleNotification;
|
|
139
|
-
/**
|
|
140
|
-
* The turn's final message is the JOIN of every agentMessage item, in
|
|
141
|
-
* insertion order — not just the last item. The streamed turn trail folds
|
|
142
|
-
* all items (one text segment per item), so the final text must match the
|
|
143
|
-
* streamed/folded text or the app renders the turn twice.
|
|
144
|
-
*/
|
|
145
|
-
private joinedAgentMessageText;
|
|
146
87
|
private isCurrentThreadNotification;
|
|
147
88
|
private resolveCurrentTurn;
|
|
148
89
|
private clearActiveTurn;
|
|
149
90
|
private sendRequest;
|
|
150
91
|
private write;
|
|
151
|
-
private trace;
|
|
152
|
-
private traceLine;
|
|
153
92
|
}
|
|
@@ -6,6 +6,7 @@ export class CodexAppServerAdapter {
|
|
|
6
6
|
model;
|
|
7
7
|
reasoningEffort;
|
|
8
8
|
sandbox;
|
|
9
|
+
legacyApprovalPolicy;
|
|
9
10
|
addDirs;
|
|
10
11
|
configOverrides;
|
|
11
12
|
fullAuto;
|
|
@@ -30,7 +31,6 @@ export class CodexAppServerAdapter {
|
|
|
30
31
|
skillsCache = null;
|
|
31
32
|
messageTextByItem = new Map();
|
|
32
33
|
planText = '';
|
|
33
|
-
traceTurnEpochMs = null;
|
|
34
34
|
constructor(opts) {
|
|
35
35
|
this.cwd = opts.cwd;
|
|
36
36
|
this.threadId = opts.threadId ?? null;
|
|
@@ -38,6 +38,7 @@ 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;
|
|
41
42
|
this.addDirs = opts.addDirs ?? [];
|
|
42
43
|
this.configOverrides = opts.configOverrides ?? [];
|
|
43
44
|
this.fullAuto = opts.fullAuto ?? false;
|
|
@@ -116,8 +117,6 @@ export class CodexAppServerAdapter {
|
|
|
116
117
|
this.interrupted = false;
|
|
117
118
|
this.messageTextByItem.clear();
|
|
118
119
|
this.planText = '';
|
|
119
|
-
this.traceTurnEpochMs = Date.now();
|
|
120
|
-
this.trace('turn/run begin');
|
|
121
120
|
try {
|
|
122
121
|
if (this.threadId && this.loadedThreadId !== this.threadId) {
|
|
123
122
|
const resumed = await this.sendRequest('thread/resume', {
|
|
@@ -159,11 +158,9 @@ export class CodexAppServerAdapter {
|
|
|
159
158
|
this.currentTurnReject = reject;
|
|
160
159
|
});
|
|
161
160
|
turnPromise.catch(() => { });
|
|
162
|
-
const turnInput = await this.buildTurnInput(prompt, imagePaths);
|
|
163
|
-
this.trace('turn/start sent');
|
|
164
161
|
const turnStarted = await this.sendRequest('turn/start', {
|
|
165
162
|
threadId: this.threadId,
|
|
166
|
-
input:
|
|
163
|
+
input: await this.buildTurnInput(prompt, imagePaths),
|
|
167
164
|
...(this.model ? { model: this.model } : {}),
|
|
168
165
|
...this.sandboxPolicyPayload(_extraAddDirs),
|
|
169
166
|
collaborationMode: {
|
|
@@ -175,7 +172,6 @@ export class CodexAppServerAdapter {
|
|
|
175
172
|
},
|
|
176
173
|
},
|
|
177
174
|
});
|
|
178
|
-
this.trace('turn/start ack');
|
|
179
175
|
const turn = turnStarted.turn;
|
|
180
176
|
if (this.currentTurnResolve) {
|
|
181
177
|
this.currentTurnId = readString(turn, 'id') ?? null;
|
|
@@ -191,7 +187,7 @@ export class CodexAppServerAdapter {
|
|
|
191
187
|
resolveApprovalPolicy() {
|
|
192
188
|
if (this.bypassApprovalsAndSandbox || this.fullAuto)
|
|
193
189
|
return 'never';
|
|
194
|
-
return
|
|
190
|
+
return this.legacyApprovalPolicy;
|
|
195
191
|
}
|
|
196
192
|
configPayload() {
|
|
197
193
|
const config = {};
|
|
@@ -326,7 +322,6 @@ export class CodexAppServerAdapter {
|
|
|
326
322
|
const message = parseJson(line);
|
|
327
323
|
if (!message)
|
|
328
324
|
return;
|
|
329
|
-
this.traceLine(line, message);
|
|
330
325
|
if ('id' in message && ('result' in message || 'error' in message) && !('method' in message)) {
|
|
331
326
|
const id = Number(message.id);
|
|
332
327
|
const pending = this.pending.get(id);
|
|
@@ -398,9 +393,9 @@ export class CodexAppServerAdapter {
|
|
|
398
393
|
const delta = readRawString(params, 'delta') ?? '';
|
|
399
394
|
const next = `${this.messageTextByItem.get(itemId) ?? ''}${delta}`;
|
|
400
395
|
this.messageTextByItem.set(itemId, next);
|
|
401
|
-
this.currentFinalMessage =
|
|
396
|
+
this.currentFinalMessage = next.trim() ? next : this.currentFinalMessage;
|
|
402
397
|
if (next.trim())
|
|
403
|
-
this.currentOnEvent?.({ type: 'message', text: next,
|
|
398
|
+
this.currentOnEvent?.({ type: 'message', text: next, itemId });
|
|
404
399
|
return;
|
|
405
400
|
}
|
|
406
401
|
if (method === 'turn/plan/updated') {
|
|
@@ -437,8 +432,7 @@ export class CodexAppServerAdapter {
|
|
|
437
432
|
const itemId = readString(item, 'id');
|
|
438
433
|
const text = readString(item, 'text');
|
|
439
434
|
if (text) {
|
|
440
|
-
this.
|
|
441
|
-
this.currentFinalMessage = this.joinedAgentMessageText() ?? text;
|
|
435
|
+
this.currentFinalMessage = text;
|
|
442
436
|
this.currentOnEvent?.({
|
|
443
437
|
type: 'message',
|
|
444
438
|
text,
|
|
@@ -472,7 +466,6 @@ export class CodexAppServerAdapter {
|
|
|
472
466
|
return;
|
|
473
467
|
}
|
|
474
468
|
if (method === 'turn/completed') {
|
|
475
|
-
this.trace('turn/completed notification');
|
|
476
469
|
const turn = params.turn;
|
|
477
470
|
const status = turn?.status;
|
|
478
471
|
if (isRecord(status) && status.type === 'failed') {
|
|
@@ -493,18 +486,6 @@ export class CodexAppServerAdapter {
|
|
|
493
486
|
this.currentErrorText = stringifyPreview(params);
|
|
494
487
|
}
|
|
495
488
|
}
|
|
496
|
-
/**
|
|
497
|
-
* The turn's final message is the JOIN of every agentMessage item, in
|
|
498
|
-
* insertion order — not just the last item. The streamed turn trail folds
|
|
499
|
-
* all items (one text segment per item), so the final text must match the
|
|
500
|
-
* streamed/folded text or the app renders the turn twice.
|
|
501
|
-
*/
|
|
502
|
-
joinedAgentMessageText() {
|
|
503
|
-
const joined = [...this.messageTextByItem.values()]
|
|
504
|
-
.filter((text) => text.trim())
|
|
505
|
-
.join('\n\n');
|
|
506
|
-
return joined.trim() ? joined : null;
|
|
507
|
-
}
|
|
508
489
|
isCurrentThreadNotification(params) {
|
|
509
490
|
const threadId = readNotificationThreadId(params);
|
|
510
491
|
if (threadId && this.threadId && threadId !== this.threadId)
|
|
@@ -536,7 +517,6 @@ export class CodexAppServerAdapter {
|
|
|
536
517
|
this.currentErrorText = null;
|
|
537
518
|
this.messageTextByItem.clear();
|
|
538
519
|
this.planText = '';
|
|
539
|
-
this.traceTurnEpochMs = null;
|
|
540
520
|
}
|
|
541
521
|
sendRequest(method, params) {
|
|
542
522
|
const id = this.requestSeq++;
|
|
@@ -556,33 +536,6 @@ export class CodexAppServerAdapter {
|
|
|
556
536
|
throw new Error('Codex app-server is not running');
|
|
557
537
|
this.child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
558
538
|
}
|
|
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';
|
|
586
539
|
}
|
|
587
540
|
function parseJson(line) {
|
|
588
541
|
try {
|