@canonmsg/codex-plugin 0.20.0 → 0.22.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 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
- ## Transport
50
+ ## Current limitation
51
51
 
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.
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 adapters/codex-plugin/dist/host.js --cwd /path/to/project --full-auto
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 adapters/codex-plugin
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
+ }
@@ -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
- 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
- }
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: turnInput,
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 null;
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 = this.joinedAgentMessageText() ?? this.currentFinalMessage;
396
+ this.currentFinalMessage = next.trim() ? next : this.currentFinalMessage;
402
397
  if (next.trim())
403
- this.currentOnEvent?.({ type: 'message', text: next, delta, itemId });
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.messageTextByItem.set(itemId ?? 'agent-message', text);
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 {