@canonmsg/codex-plugin 0.13.1 → 0.13.3

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,7 +5,7 @@ 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';
8
+ import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, buildPlanApprovalRequest, buildCanonHostPrompt, buildCanonTurnEnvelopeV1, 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, renderCanonTurnEnvelopePrompt, resolveCanonHostPromptMode, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
9
9
  import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
10
10
  import { CodexConversationAdapter, } from './adapter.js';
11
11
  import { CodexAppServerAdapter } from './app-server-adapter.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
@@ -49,6 +50,7 @@ Keep this terminal open while you want Canon to reach the agent. Closing it,
49
50
  logging out, rebooting, or sleeping long enough to stop the process takes the
50
51
  local agent offline until you revive it. Docs:
51
52
  https://canonmail.com/agents/integrations`;
53
+ const HOST_PROMPT_MODE = resolveCanonHostPromptMode(process.env.CANON_HOST_PROMPT_MODE);
52
54
  const MAX_SESSIONS = 12;
53
55
  const IDLE_TIMEOUT_MS = 30 * 60 * 1000;
54
56
  const HEARTBEAT_MS = 30_000;
@@ -125,6 +127,7 @@ function buildCodexRuntimeDescriptor(input) {
125
127
  result: 'action_or_values',
126
128
  maxTimeoutMs: 30 * 60_000,
127
129
  blockKinds: ['summary', 'metricGrid', 'chart', 'table', 'list', 'callout', 'actions'],
130
+ actionFieldTypes: ['text', 'textarea', 'select', 'multiSelect', 'boolean'],
128
131
  native: true,
129
132
  },
130
133
  },
@@ -226,6 +229,20 @@ function resolveCodexEffectiveRuntimePolicy(input) {
226
229
  };
227
230
  }
228
231
  function buildCanonPrompt(input) {
232
+ if (HOST_PROMPT_MODE === 'envelope') {
233
+ return renderCanonTurnEnvelopePrompt(buildCanonTurnEnvelopeV1({
234
+ content: input.content,
235
+ conversationId: input.conversationId,
236
+ participantContext: input.participantContext,
237
+ behavior: input.behavior,
238
+ selfContexts: input.selfContexts,
239
+ activeSelfContextId: input.activeSelfContextId,
240
+ provenance: input.provenance,
241
+ replyContext: input.replyContext,
242
+ message: input.message,
243
+ hydratedFromPage: input.hydratedFromPage,
244
+ }));
245
+ }
229
246
  return buildCanonHostPrompt({
230
247
  hostLabel: 'Codex',
231
248
  buildInboundContextLines,
@@ -843,6 +860,7 @@ export async function main() {
843
860
  closed: false,
844
861
  turnLiveText: '',
845
862
  turnBlocks: [],
863
+ turnCommandBlocks: createCommandBlockTracker(),
846
864
  };
847
865
  sessions.set(conversationId, session);
848
866
  await Promise.all([
@@ -1213,7 +1231,7 @@ export async function main() {
1213
1231
  const userMessage = error instanceof ExecutionEnvironmentError ? error.userMessage : message;
1214
1232
  console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Failed to create session: ${message}`);
1215
1233
  await markQueuedMessageAccepted(input.conversationId, input.message.id, shouldMarkAccepted);
1216
- await sendMessageWithRetry(client, input.conversationId, `I couldn't start a coding session for this workspace: ${userMessage}`, {
1234
+ await sendMessageWithRetryChunked(client, input.conversationId, `I couldn't start a coding session for this workspace: ${userMessage}`, {
1217
1235
  messageId: `codex-start-failed-${input.message.id}`,
1218
1236
  ...(activeSelfContextId ? { selfContextId: activeSelfContextId } : {}),
1219
1237
  metadata: {
@@ -1230,7 +1248,11 @@ export async function main() {
1230
1248
  participantContext,
1231
1249
  behavior,
1232
1250
  selfContexts,
1251
+ activeSelfContextId,
1252
+ provenance: hydrated.provenance,
1233
1253
  replyContext,
1254
+ message: input.message,
1255
+ hydratedFromPage: hydrated.hydratedFromPage,
1234
1256
  });
1235
1257
  if (session.running && deliveryIntent === 'interrupt') {
1236
1258
  enqueuePrompt(session, prompt, deliveryIntent, true, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode);
@@ -1254,6 +1276,7 @@ export async function main() {
1254
1276
  session.currentTurnId = randomUUID();
1255
1277
  session.turnLiveText = '';
1256
1278
  session.turnBlocks = [];
1279
+ session.turnCommandBlocks = createCommandBlockTracker();
1257
1280
  session.currentTurnOpenedAt = Date.now();
1258
1281
  session.currentTurnUpdatedAt = session.currentTurnOpenedAt;
1259
1282
  session.lastAcceptedIntent = nextTurn.intent;
@@ -1317,8 +1340,13 @@ export async function main() {
1317
1340
  markTurnProgress(session);
1318
1341
  writeTurn(session);
1319
1342
  startVisibleWorkSignal(session);
1343
+ const blockId = beginCommandBlock(session.turnCommandBlocks, {
1344
+ turnId: session.currentTurnId,
1345
+ command: event.command,
1346
+ itemId: event.itemId,
1347
+ });
1320
1348
  upsertTurnBlock(session, {
1321
- id: `command:${session.currentTurnId}`,
1349
+ id: blockId,
1322
1350
  kind: 'tool',
1323
1351
  status: 'running',
1324
1352
  title: summarizeCommand(event.command),
@@ -1328,7 +1356,12 @@ export async function main() {
1328
1356
  return;
1329
1357
  }
1330
1358
  if (event.type === 'command.completed') {
1331
- completeTurnBlock(session, `command:${session.currentTurnId}`, 'Command completed');
1359
+ const blockId = claimCommandBlock(session.turnCommandBlocks, {
1360
+ turnId: session.currentTurnId,
1361
+ command: event.command,
1362
+ itemId: event.itemId,
1363
+ });
1364
+ completeTurnBlock(session, blockId, 'Command completed');
1332
1365
  if (session.turnState === 'tool') {
1333
1366
  session.turnState = 'thinking';
1334
1367
  markTurnProgress(session);
@@ -1389,7 +1422,7 @@ export async function main() {
1389
1422
  clearStoredThread();
1390
1423
  }
1391
1424
  const turnTrail = buildFinalTurnTrail(session);
1392
- await sendMessageWithRetry(client, session.conversationId, result.finalMessage, {
1425
+ await sendMessageWithRetryChunked(client, session.conversationId, result.finalMessage, {
1393
1426
  messageId: buildCodexMessageId(session, 'final'),
1394
1427
  ...(session.activeSelfContextId
1395
1428
  ? { selfContextId: session.activeSelfContextId }
@@ -1412,7 +1445,7 @@ export async function main() {
1412
1445
  console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Turn exited ${result.exitCode}: ${result.errorText}`);
1413
1446
  }
1414
1447
  const turnTrail = buildFinalTurnTrail(session);
1415
- await sendMessageWithRetry(client, session.conversationId, userVisibleError, {
1448
+ await sendMessageWithRetryChunked(client, session.conversationId, userVisibleError, {
1416
1449
  messageId: buildCodexMessageId(session, 'error'),
1417
1450
  ...(session.activeSelfContextId
1418
1451
  ? { selfContextId: session.activeSelfContextId }
@@ -1441,10 +1474,12 @@ export async function main() {
1441
1474
  catch (error) {
1442
1475
  const message = error instanceof ExecutionEnvironmentError
1443
1476
  ? error.userMessage
1444
- : `The Codex host failed to start a turn: ${error instanceof Error ? error.message : String(error)}`;
1477
+ : error instanceof CanonApiError
1478
+ ? `The Codex host completed the turn, but Canon could not deliver the reply: ${error.message}`
1479
+ : `The Codex host failed during the turn: ${error instanceof Error ? error.message : String(error)}`;
1445
1480
  session.state.lastError = message;
1446
1481
  writeState(session);
1447
- await sendMessageWithRetry(client, session.conversationId, message, {
1482
+ await sendMessageWithRetryChunked(client, session.conversationId, message, {
1448
1483
  messageId: buildCodexMessageId(session, 'failure'),
1449
1484
  ...(session.activeSelfContextId
1450
1485
  ? { 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.1",
3
+ "version": "0.13.3",
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.1.1",
33
- "@canonmsg/core": "^1.3.0"
32
+ "@canonmsg/agent-sdk": "^2.4.0",
33
+ "@canonmsg/core": "^1.7.0"
34
34
  },
35
35
  "engines": {
36
36
  "node": ">=18.0.0"