@canonmsg/codex-plugin 0.13.2 → 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;
@@ -227,6 +229,20 @@ function resolveCodexEffectiveRuntimePolicy(input) {
227
229
  };
228
230
  }
229
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
+ }
230
246
  return buildCanonHostPrompt({
231
247
  hostLabel: 'Codex',
232
248
  buildInboundContextLines,
@@ -844,6 +860,7 @@ export async function main() {
844
860
  closed: false,
845
861
  turnLiveText: '',
846
862
  turnBlocks: [],
863
+ turnCommandBlocks: createCommandBlockTracker(),
847
864
  };
848
865
  sessions.set(conversationId, session);
849
866
  await Promise.all([
@@ -1214,7 +1231,7 @@ export async function main() {
1214
1231
  const userMessage = error instanceof ExecutionEnvironmentError ? error.userMessage : message;
1215
1232
  console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Failed to create session: ${message}`);
1216
1233
  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}`, {
1234
+ await sendMessageWithRetryChunked(client, input.conversationId, `I couldn't start a coding session for this workspace: ${userMessage}`, {
1218
1235
  messageId: `codex-start-failed-${input.message.id}`,
1219
1236
  ...(activeSelfContextId ? { selfContextId: activeSelfContextId } : {}),
1220
1237
  metadata: {
@@ -1231,7 +1248,11 @@ export async function main() {
1231
1248
  participantContext,
1232
1249
  behavior,
1233
1250
  selfContexts,
1251
+ activeSelfContextId,
1252
+ provenance: hydrated.provenance,
1234
1253
  replyContext,
1254
+ message: input.message,
1255
+ hydratedFromPage: hydrated.hydratedFromPage,
1235
1256
  });
1236
1257
  if (session.running && deliveryIntent === 'interrupt') {
1237
1258
  enqueuePrompt(session, prompt, deliveryIntent, true, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode);
@@ -1255,6 +1276,7 @@ export async function main() {
1255
1276
  session.currentTurnId = randomUUID();
1256
1277
  session.turnLiveText = '';
1257
1278
  session.turnBlocks = [];
1279
+ session.turnCommandBlocks = createCommandBlockTracker();
1258
1280
  session.currentTurnOpenedAt = Date.now();
1259
1281
  session.currentTurnUpdatedAt = session.currentTurnOpenedAt;
1260
1282
  session.lastAcceptedIntent = nextTurn.intent;
@@ -1318,8 +1340,13 @@ export async function main() {
1318
1340
  markTurnProgress(session);
1319
1341
  writeTurn(session);
1320
1342
  startVisibleWorkSignal(session);
1343
+ const blockId = beginCommandBlock(session.turnCommandBlocks, {
1344
+ turnId: session.currentTurnId,
1345
+ command: event.command,
1346
+ itemId: event.itemId,
1347
+ });
1321
1348
  upsertTurnBlock(session, {
1322
- id: `command:${session.currentTurnId}`,
1349
+ id: blockId,
1323
1350
  kind: 'tool',
1324
1351
  status: 'running',
1325
1352
  title: summarizeCommand(event.command),
@@ -1329,7 +1356,12 @@ export async function main() {
1329
1356
  return;
1330
1357
  }
1331
1358
  if (event.type === 'command.completed') {
1332
- 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');
1333
1365
  if (session.turnState === 'tool') {
1334
1366
  session.turnState = 'thinking';
1335
1367
  markTurnProgress(session);
@@ -1390,7 +1422,7 @@ export async function main() {
1390
1422
  clearStoredThread();
1391
1423
  }
1392
1424
  const turnTrail = buildFinalTurnTrail(session);
1393
- await sendMessageWithRetry(client, session.conversationId, result.finalMessage, {
1425
+ await sendMessageWithRetryChunked(client, session.conversationId, result.finalMessage, {
1394
1426
  messageId: buildCodexMessageId(session, 'final'),
1395
1427
  ...(session.activeSelfContextId
1396
1428
  ? { selfContextId: session.activeSelfContextId }
@@ -1413,7 +1445,7 @@ export async function main() {
1413
1445
  console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Turn exited ${result.exitCode}: ${result.errorText}`);
1414
1446
  }
1415
1447
  const turnTrail = buildFinalTurnTrail(session);
1416
- await sendMessageWithRetry(client, session.conversationId, userVisibleError, {
1448
+ await sendMessageWithRetryChunked(client, session.conversationId, userVisibleError, {
1417
1449
  messageId: buildCodexMessageId(session, 'error'),
1418
1450
  ...(session.activeSelfContextId
1419
1451
  ? { selfContextId: session.activeSelfContextId }
@@ -1442,10 +1474,12 @@ export async function main() {
1442
1474
  catch (error) {
1443
1475
  const message = error instanceof ExecutionEnvironmentError
1444
1476
  ? error.userMessage
1445
- : `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)}`;
1446
1480
  session.state.lastError = message;
1447
1481
  writeState(session);
1448
- await sendMessageWithRetry(client, session.conversationId, message, {
1482
+ await sendMessageWithRetryChunked(client, session.conversationId, message, {
1449
1483
  messageId: buildCodexMessageId(session, 'failure'),
1450
1484
  ...(session.activeSelfContextId
1451
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.2",
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.3.0",
33
- "@canonmsg/core": "^1.6.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"