@canonmsg/codex-plugin 0.13.2 → 0.14.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
@@ -80,11 +80,13 @@ Worktree mode is project isolation, not an operating-system sandbox. The Codex C
80
80
  Useful flags:
81
81
 
82
82
  ```bash
83
- canon-codex --cwd /path/to/project --model gpt-5.4 --full-auto
83
+ canon-codex --cwd /path/to/project --model gpt-5.5 --full-auto
84
84
  ```
85
85
 
86
86
  Codex also supports `--add-dir /extra/path` for additional writable directories passed through to `codex exec`. Canon does not yet render those extra directories as workspace choices.
87
87
 
88
+ `gpt-5.5` requires a recent local Codex CLI. Older CLIs fail early with an upgrade message; upgrade Codex or choose a model supported by your installed CLI.
89
+
88
90
  Recent Codex CLI releases no longer accept `--ask-for-approval` with `codex exec`. If you previously launched Canon with `--sandbox workspace-write --ask-for-approval never`, switch to `--full-auto`.
89
91
 
90
92
  Do not start Canon with `--sandbox danger-full-access` as an unlabeled default. Use `--dangerously-bypass-approvals-and-sandbox` only when you intentionally want Canon to advertise the owner-only Bypass policy.
package/dist/adapter.d.ts CHANGED
@@ -17,11 +17,13 @@ export type CodexEvent = {
17
17
  } | {
18
18
  type: 'command.started';
19
19
  command: string;
20
+ itemId?: string;
20
21
  } | {
21
22
  type: 'command.completed';
22
23
  command: string;
23
24
  output: string;
24
25
  exitCode: number | null;
26
+ itemId?: string;
25
27
  } | {
26
28
  type: 'turn.completed';
27
29
  usage?: {
package/dist/adapter.js CHANGED
@@ -83,6 +83,9 @@ export class CodexConversationAdapter {
83
83
  onEvent({
84
84
  type: 'command.started',
85
85
  command: String(event.item.command ?? ''),
86
+ ...(typeof event.item.id === 'string' && event.item.id.trim()
87
+ ? { itemId: event.item.id.trim() }
88
+ : {}),
86
89
  });
87
90
  }
88
91
  break;
@@ -99,6 +102,9 @@ export class CodexConversationAdapter {
99
102
  command: String(event.item.command ?? ''),
100
103
  output: String(event.item.aggregated_output ?? ''),
101
104
  exitCode: typeof event.item.exit_code === 'number' ? event.item.exit_code : null,
105
+ ...(typeof event.item.id === 'string' && event.item.id.trim()
106
+ ? { itemId: event.item.id.trim() }
107
+ : {}),
102
108
  });
103
109
  }
104
110
  break;
@@ -311,10 +311,10 @@ export class CodexAppServerAdapter {
311
311
  }
312
312
  if (method === 'item/agentMessage/delta') {
313
313
  const itemId = readString(params, 'itemId') ?? 'agent-message';
314
- const delta = readString(params, 'delta') ?? '';
314
+ const delta = readRawString(params, 'delta') ?? '';
315
315
  const next = `${this.messageTextByItem.get(itemId) ?? ''}${delta}`;
316
316
  this.messageTextByItem.set(itemId, next);
317
- this.currentFinalMessage = next.trim() || this.currentFinalMessage;
317
+ this.currentFinalMessage = next.trim() ? next : this.currentFinalMessage;
318
318
  if (next.trim())
319
319
  this.currentOnEvent?.({ type: 'message', text: next });
320
320
  return;
@@ -327,9 +327,9 @@ export class CodexAppServerAdapter {
327
327
  return;
328
328
  }
329
329
  if (method === 'item/plan/delta') {
330
- const delta = readString(params, 'delta') ?? '';
330
+ const delta = readRawString(params, 'delta') ?? '';
331
331
  this.planText = `${this.planText}${delta}`;
332
- this.currentFinalMessage = this.planText.trim() || this.currentFinalMessage;
332
+ this.currentFinalMessage = this.planText.trim() ? this.planText : this.currentFinalMessage;
333
333
  if (this.planText.trim())
334
334
  this.currentOnEvent?.({ type: 'plan.updated', text: this.planText });
335
335
  return;
@@ -337,8 +337,14 @@ export class CodexAppServerAdapter {
337
337
  if (method === 'item/started') {
338
338
  const item = params.item;
339
339
  const summary = summarizeItem(item);
340
- if (summary)
341
- this.currentOnEvent?.({ type: 'command.started', command: summary });
340
+ if (summary) {
341
+ const itemId = readString(item, 'id');
342
+ this.currentOnEvent?.({
343
+ type: 'command.started',
344
+ command: summary,
345
+ ...(itemId ? { itemId } : {}),
346
+ });
347
+ }
342
348
  return;
343
349
  }
344
350
  if (method === 'item/completed') {
@@ -351,13 +357,28 @@ export class CodexAppServerAdapter {
351
357
  }
352
358
  }
353
359
  else if (item?.type === 'commandExecution') {
360
+ const itemId = readString(item, 'id');
354
361
  this.currentOnEvent?.({
355
362
  type: 'command.completed',
356
363
  command: readString(item, 'command') ?? 'Command',
357
364
  output: readString(item, 'aggregatedOutput') ?? '',
358
365
  exitCode: typeof item.exitCode === 'number' ? item.exitCode : null,
366
+ ...(itemId ? { itemId } : {}),
359
367
  });
360
368
  }
369
+ else {
370
+ const summary = summarizeItem(item);
371
+ if (summary) {
372
+ const itemId = readString(item, 'id');
373
+ this.currentOnEvent?.({
374
+ type: 'command.completed',
375
+ command: summary,
376
+ output: '',
377
+ exitCode: null,
378
+ ...(itemId ? { itemId } : {}),
379
+ });
380
+ }
381
+ }
361
382
  return;
362
383
  }
363
384
  if (method === 'turn/completed') {
@@ -432,6 +453,10 @@ function readString(record, key) {
432
453
  const value = record?.[key];
433
454
  return typeof value === 'string' && value.trim() ? value.trim() : undefined;
434
455
  }
456
+ function readRawString(record, key) {
457
+ const value = record?.[key];
458
+ return typeof value === 'string' ? value : undefined;
459
+ }
435
460
  function stringifyPreview(value) {
436
461
  if (typeof value === 'string')
437
462
  return value;
package/dist/host.js CHANGED
@@ -5,8 +5,8 @@ import { spawnSync } from 'node:child_process';
5
5
  import { dirname } from 'node:path';
6
6
  import { parseArgs } from 'node:util';
7
7
  import { getCodexImagePath, materializeMessageMedia, materializeReplyContextMedia, } from '@canonmsg/agent-sdk';
8
- import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, buildPlanApprovalRequest, buildCanonHostPrompt, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createRuntimeStatePublisher, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, getActiveProfileLock, initRTDBAuth, buildLocalRuntimeId, heartbeatLocalRuntimeEntry, loadRuntimeSessionState, markLocalRuntimeStopped, normalizeTurnMetadata, parseRuntimeCardV1, prepareConversationEnvironment, loadHostSessionConfig, releaseConversationEnvironment, resolveCanonAgent, rtdbRead, rtdbWrite, sendMessageWithRetry, shouldTriggerAgentTurn, saveRuntimeSessionState, buildBoundedTurnTrail, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
9
- import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
8
+ import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, buildPlanApprovalRequest, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createRuntimeStatePublisher, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, getActiveProfileLock, initRTDBAuth, buildLocalRuntimeId, heartbeatLocalRuntimeEntry, loadRuntimeSessionState, markLocalRuntimeStopped, normalizeTurnMetadata, parseRuntimeCardV1, prepareConversationEnvironment, loadHostSessionConfig, releaseConversationEnvironment, resolveCanonAgent, rtdbRead, rtdbWrite, CanonApiError, sendMessageWithRetry, sendMessageWithRetryChunked, shouldTriggerAgentTurn, saveRuntimeSessionState, buildBoundedTurnTrail, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCanonTurnBriefPrompt, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
9
+ import { decideAutoReply, } from './inbound-policy.js';
10
10
  import { CodexConversationAdapter, } from './adapter.js';
11
11
  import { CodexAppServerAdapter } from './app-server-adapter.js';
12
12
  import { mapCanonApprovalResultToCodexDecision, mapCodexAppServerApprovalRequest, } from './app-server-approval.js';
@@ -16,6 +16,7 @@ import { detectCodexCliVersion } from './codex-cli-version.js';
16
16
  import { buildCodexModelGuardMessage, formatCodexTurnFailure, isRecoverableCodexThreadError, } from './error-format.js';
17
17
  import { startCodexStreamInBackground } from './host-lifecycle.js';
18
18
  import { runCli } from './cli-entry.js';
19
+ import { beginCommandBlock, claimCommandBlock, createCommandBlockTracker, } from './turn-activity.js';
19
20
  const HELP = `canon-codex — run a local Codex agent host for Canon
20
21
 
21
22
  USAGE
@@ -227,11 +228,17 @@ function resolveCodexEffectiveRuntimePolicy(input) {
227
228
  };
228
229
  }
229
230
  function buildCanonPrompt(input) {
230
- return buildCanonHostPrompt({
231
- hostLabel: 'Codex',
232
- buildInboundContextLines,
233
- ...input,
234
- });
231
+ return renderCanonTurnBriefPrompt(buildCanonTurnContextV2({
232
+ content: input.content,
233
+ conversationId: input.conversationId,
234
+ participantContext: input.participantContext,
235
+ behavior: input.behavior,
236
+ selfContexts: input.selfContexts,
237
+ activeSelfContextId: input.activeSelfContextId,
238
+ provenance: input.provenance,
239
+ replyContext: input.replyContext,
240
+ message: input.message,
241
+ }));
235
242
  }
236
243
  function renderInboundContent(message, materialized) {
237
244
  return renderCanonHostInboundContent(message, materialized);
@@ -844,6 +851,7 @@ export async function main() {
844
851
  closed: false,
845
852
  turnLiveText: '',
846
853
  turnBlocks: [],
854
+ turnCommandBlocks: createCommandBlockTracker(),
847
855
  };
848
856
  sessions.set(conversationId, session);
849
857
  await Promise.all([
@@ -1214,7 +1222,7 @@ export async function main() {
1214
1222
  const userMessage = error instanceof ExecutionEnvironmentError ? error.userMessage : message;
1215
1223
  console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Failed to create session: ${message}`);
1216
1224
  await markQueuedMessageAccepted(input.conversationId, input.message.id, shouldMarkAccepted);
1217
- await sendMessageWithRetry(client, input.conversationId, `I couldn't start a coding session for this workspace: ${userMessage}`, {
1225
+ await sendMessageWithRetryChunked(client, input.conversationId, `I couldn't start a coding session for this workspace: ${userMessage}`, {
1218
1226
  messageId: `codex-start-failed-${input.message.id}`,
1219
1227
  ...(activeSelfContextId ? { selfContextId: activeSelfContextId } : {}),
1220
1228
  metadata: {
@@ -1231,7 +1239,10 @@ export async function main() {
1231
1239
  participantContext,
1232
1240
  behavior,
1233
1241
  selfContexts,
1242
+ activeSelfContextId,
1243
+ provenance: hydrated.provenance,
1234
1244
  replyContext,
1245
+ message: input.message,
1235
1246
  });
1236
1247
  if (session.running && deliveryIntent === 'interrupt') {
1237
1248
  enqueuePrompt(session, prompt, deliveryIntent, true, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode);
@@ -1255,6 +1266,7 @@ export async function main() {
1255
1266
  session.currentTurnId = randomUUID();
1256
1267
  session.turnLiveText = '';
1257
1268
  session.turnBlocks = [];
1269
+ session.turnCommandBlocks = createCommandBlockTracker();
1258
1270
  session.currentTurnOpenedAt = Date.now();
1259
1271
  session.currentTurnUpdatedAt = session.currentTurnOpenedAt;
1260
1272
  session.lastAcceptedIntent = nextTurn.intent;
@@ -1318,8 +1330,13 @@ export async function main() {
1318
1330
  markTurnProgress(session);
1319
1331
  writeTurn(session);
1320
1332
  startVisibleWorkSignal(session);
1333
+ const blockId = beginCommandBlock(session.turnCommandBlocks, {
1334
+ turnId: session.currentTurnId,
1335
+ command: event.command,
1336
+ itemId: event.itemId,
1337
+ });
1321
1338
  upsertTurnBlock(session, {
1322
- id: `command:${session.currentTurnId}`,
1339
+ id: blockId,
1323
1340
  kind: 'tool',
1324
1341
  status: 'running',
1325
1342
  title: summarizeCommand(event.command),
@@ -1329,7 +1346,12 @@ export async function main() {
1329
1346
  return;
1330
1347
  }
1331
1348
  if (event.type === 'command.completed') {
1332
- completeTurnBlock(session, `command:${session.currentTurnId}`, 'Command completed');
1349
+ const blockId = claimCommandBlock(session.turnCommandBlocks, {
1350
+ turnId: session.currentTurnId,
1351
+ command: event.command,
1352
+ itemId: event.itemId,
1353
+ });
1354
+ completeTurnBlock(session, blockId, 'Command completed');
1333
1355
  if (session.turnState === 'tool') {
1334
1356
  session.turnState = 'thinking';
1335
1357
  markTurnProgress(session);
@@ -1390,7 +1412,7 @@ export async function main() {
1390
1412
  clearStoredThread();
1391
1413
  }
1392
1414
  const turnTrail = buildFinalTurnTrail(session);
1393
- await sendMessageWithRetry(client, session.conversationId, result.finalMessage, {
1415
+ await sendMessageWithRetryChunked(client, session.conversationId, result.finalMessage, {
1394
1416
  messageId: buildCodexMessageId(session, 'final'),
1395
1417
  ...(session.activeSelfContextId
1396
1418
  ? { selfContextId: session.activeSelfContextId }
@@ -1413,7 +1435,7 @@ export async function main() {
1413
1435
  console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Turn exited ${result.exitCode}: ${result.errorText}`);
1414
1436
  }
1415
1437
  const turnTrail = buildFinalTurnTrail(session);
1416
- await sendMessageWithRetry(client, session.conversationId, userVisibleError, {
1438
+ await sendMessageWithRetryChunked(client, session.conversationId, userVisibleError, {
1417
1439
  messageId: buildCodexMessageId(session, 'error'),
1418
1440
  ...(session.activeSelfContextId
1419
1441
  ? { selfContextId: session.activeSelfContextId }
@@ -1442,10 +1464,12 @@ export async function main() {
1442
1464
  catch (error) {
1443
1465
  const message = error instanceof ExecutionEnvironmentError
1444
1466
  ? error.userMessage
1445
- : `The Codex host failed to start a turn: ${error instanceof Error ? error.message : String(error)}`;
1467
+ : error instanceof CanonApiError
1468
+ ? `The Codex host completed the turn, but Canon could not deliver the reply: ${error.message}`
1469
+ : `The Codex host failed during the turn: ${error instanceof Error ? error.message : String(error)}`;
1446
1470
  session.state.lastError = message;
1447
1471
  writeState(session);
1448
- await sendMessageWithRetry(client, session.conversationId, message, {
1472
+ await sendMessageWithRetryChunked(client, session.conversationId, message, {
1449
1473
  messageId: buildCodexMessageId(session, 'failure'),
1450
1474
  ...(session.activeSelfContextId
1451
1475
  ? { selfContextId: session.activeSelfContextId }
@@ -0,0 +1,21 @@
1
+ interface RunningCommandBlock {
2
+ command: string;
3
+ blockId: string;
4
+ itemId?: string;
5
+ }
6
+ export interface CommandBlockTracker {
7
+ sequence: number;
8
+ running: RunningCommandBlock[];
9
+ }
10
+ export declare function createCommandBlockTracker(): CommandBlockTracker;
11
+ export declare function beginCommandBlock(tracker: CommandBlockTracker, input: {
12
+ turnId: string | null | undefined;
13
+ command: string;
14
+ itemId?: string;
15
+ }): string;
16
+ export declare function claimCommandBlock(tracker: CommandBlockTracker, input: {
17
+ turnId: string | null | undefined;
18
+ command: string;
19
+ itemId?: string;
20
+ }): string;
21
+ export {};
@@ -0,0 +1,47 @@
1
+ export function createCommandBlockTracker() {
2
+ return {
3
+ sequence: 0,
4
+ running: [],
5
+ };
6
+ }
7
+ function normalizeOptionalString(value) {
8
+ const normalized = value?.trim();
9
+ return normalized ? normalized : undefined;
10
+ }
11
+ function nextCommandBlockId(tracker, turnId, itemId) {
12
+ const stableTurnId = turnId ?? 'turn';
13
+ if (itemId)
14
+ return `command:${stableTurnId}:${itemId}`;
15
+ tracker.sequence += 1;
16
+ return `command:${stableTurnId}:${tracker.sequence}`;
17
+ }
18
+ export function beginCommandBlock(tracker, input) {
19
+ const itemId = normalizeOptionalString(input.itemId);
20
+ if (itemId) {
21
+ const existing = tracker.running.find((block) => block.itemId === itemId);
22
+ if (existing)
23
+ return existing.blockId;
24
+ }
25
+ const blockId = nextCommandBlockId(tracker, input.turnId, itemId);
26
+ tracker.running.push({
27
+ command: input.command.trim(),
28
+ blockId,
29
+ ...(itemId ? { itemId } : {}),
30
+ });
31
+ return blockId;
32
+ }
33
+ export function claimCommandBlock(tracker, input) {
34
+ const itemId = normalizeOptionalString(input.itemId);
35
+ const command = input.command.trim();
36
+ const matchingIndex = itemId
37
+ ? tracker.running.findIndex((block) => block.itemId === itemId)
38
+ : tracker.running.findIndex((block) => !block.itemId && block.command === command);
39
+ const fallbackIndex = matchingIndex >= 0
40
+ ? matchingIndex
41
+ : tracker.running.findIndex((block) => !block.itemId);
42
+ if (fallbackIndex >= 0) {
43
+ const [block] = tracker.running.splice(fallbackIndex, 1);
44
+ return block.blockId;
45
+ }
46
+ return nextCommandBlockId(tracker, input.turnId, itemId);
47
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/codex-plugin",
3
- "version": "0.13.2",
3
+ "version": "0.14.0",
4
4
  "description": "Canon host integration for Codex CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -29,8 +29,8 @@
29
29
  "prepack": "npm run build"
30
30
  },
31
31
  "dependencies": {
32
- "@canonmsg/agent-sdk": "^2.3.0",
33
- "@canonmsg/core": "^1.6.0"
32
+ "@canonmsg/agent-sdk": "^3.0.0",
33
+ "@canonmsg/core": "^2.0.0"
34
34
  },
35
35
  "engines": {
36
36
  "node": ">=18.0.0"
@@ -54,7 +54,7 @@
54
54
  "devDependencies": {
55
55
  "@types/node": "^22.0.0",
56
56
  "typescript": "~5.7.0",
57
- "vitest": "^3.0.0"
57
+ "vitest": "^4.1.8"
58
58
  },
59
59
  "license": "MIT"
60
60
  }