@canonmsg/codex-plugin 0.18.4 → 0.18.6

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/dist/adapter.d.ts CHANGED
@@ -8,6 +8,7 @@ export type CodexEvent = {
8
8
  } | {
9
9
  type: 'message';
10
10
  text: string;
11
+ itemId?: string;
11
12
  } | {
12
13
  type: 'plan.updated';
13
14
  text: string;
package/dist/adapter.js CHANGED
@@ -99,7 +99,13 @@ export class CodexConversationAdapter {
99
99
  if (event.item?.type === 'agent_message') {
100
100
  latestMessage = normalizeMessageText(event.item.text);
101
101
  if (latestMessage) {
102
- onEvent({ type: 'message', text: 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
+ });
103
109
  }
104
110
  }
105
111
  else if (event.item?.type === 'command_execution') {
@@ -338,7 +338,7 @@ export class CodexAppServerAdapter {
338
338
  this.messageTextByItem.set(itemId, next);
339
339
  this.currentFinalMessage = next.trim() ? next : this.currentFinalMessage;
340
340
  if (next.trim())
341
- this.currentOnEvent?.({ type: 'message', text: next });
341
+ this.currentOnEvent?.({ type: 'message', text: next, itemId });
342
342
  return;
343
343
  }
344
344
  if (method === 'turn/plan/updated') {
@@ -372,10 +372,15 @@ export class CodexAppServerAdapter {
372
372
  if (method === 'item/completed') {
373
373
  const item = params.item;
374
374
  if (item?.type === 'agentMessage') {
375
+ const itemId = readString(item, 'id');
375
376
  const text = readString(item, 'text');
376
377
  if (text) {
377
378
  this.currentFinalMessage = text;
378
- this.currentOnEvent?.({ type: 'message', text });
379
+ this.currentOnEvent?.({
380
+ type: 'message',
381
+ text,
382
+ ...(itemId ? { itemId } : {}),
383
+ });
379
384
  }
380
385
  }
381
386
  else if (item?.type === 'commandExecution') {
package/dist/host.d.ts CHANGED
@@ -1,4 +1,20 @@
1
1
  #!/usr/bin/env node
2
+ interface HostSessionState {
3
+ lastError?: string;
4
+ model?: string;
5
+ permissionMode?: string;
6
+ effort?: string;
7
+ /** Tokens consumed by the latest completed turn; Codex reports no context window. */
8
+ contextUsage?: {
9
+ totalTokens: number;
10
+ };
11
+ state: 'idle' | 'running';
12
+ }
13
+ export declare function buildCodexInitialSessionState(input: {
14
+ model?: string;
15
+ permissionMode?: string;
16
+ effort?: string | null;
17
+ }): HostSessionState;
2
18
  /** GPT reasoning-effort levels, applied next turn via model_reasoning_effort. */
3
19
  export declare const CODEX_EFFORT_OPTIONS: readonly [{
4
20
  readonly value: "minimal";
@@ -17,3 +33,4 @@ export declare const CODEX_EFFORT_OPTIONS: readonly [{
17
33
  readonly label: "Extra high";
18
34
  }];
19
35
  export declare function main(): Promise<void>;
36
+ export {};
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, sendMediaFileMessage, } from '@canonmsg/agent-sdk';
8
- import { buildTurnArtifactContextLine, ensureTurnArtifactDir, flushTurnArtifacts, } from '@canonmsg/coding-agent-host';
9
- import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, buildPlanApprovalRequest, resolveQuestionAllowOther, 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, CanonApiError, sendMessageWithRetry, sendMessageWithRetryChunked, shouldTriggerAgentTurn, saveRuntimeSessionState, buildBoundedTurnTrail, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCanonTurnBriefPrompt, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
8
+ import { captureTurnArtifactSnapshot, collectTurnArtifacts, } from '@canonmsg/coding-agent-host';
9
+ import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, buildPlanApprovalRequest, resolveQuestionAllowOther, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, buildCanonInboundFrameV1, 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, CanonApiError, sendMessageWithRetry, sendMessageWithRetryChunked, shouldTriggerAgentTurn, saveRuntimeSessionState, buildBoundedTurnTrail, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
10
10
  import { decideAutoReply, } from './inbound-policy.js';
11
11
  import { CodexConversationAdapter, } from './adapter.js';
12
12
  import { CodexAppServerAdapter } from './app-server-adapter.js';
@@ -19,7 +19,7 @@ import { startCodexStreamInBackground } from './host-lifecycle.js';
19
19
  import { createCodexControlPoller } from './control-channel.js';
20
20
  import { runCli } from './cli-entry.js';
21
21
  import { collectMissedInboundMessages, STARTUP_RECOVERY_MAX_MESSAGES, STARTUP_RECOVERY_PAGE_SIZE, } from './startup-recovery.js';
22
- import { beginCommandBlock, claimCommandBlock, createCommandBlockTracker, } from './turn-activity.js';
22
+ import { applyTextSegmentBlock, beginCommandBlock, claimCommandBlock, createCommandBlockTracker, } from './turn-activity.js';
23
23
  const HELP = `canon-codex — run a local Codex agent host for Canon
24
24
 
25
25
  USAGE
@@ -53,6 +53,14 @@ Keep this terminal open while you want Canon to reach the agent. Closing it,
53
53
  logging out, rebooting, or sleeping long enough to stop the process takes the
54
54
  local agent offline until you revive it. Docs:
55
55
  https://canonmail.com/agents/integrations`;
56
+ export function buildCodexInitialSessionState(input) {
57
+ return {
58
+ model: input.model,
59
+ permissionMode: input.permissionMode,
60
+ ...(input.effort ? { effort: input.effort } : {}),
61
+ state: 'idle',
62
+ };
63
+ }
56
64
  const MAX_SESSIONS = 12;
57
65
  const IDLE_TIMEOUT_MS = 30 * 60 * 1000;
58
66
  const HEARTBEAT_MS = 30_000;
@@ -248,7 +256,7 @@ function resolveCodexEffectiveRuntimePolicy(input) {
248
256
  };
249
257
  }
250
258
  function buildCanonPrompt(input) {
251
- return renderCanonTurnBriefPrompt(buildCanonTurnContextV2({
259
+ return renderCodingHostInboundPrompt(buildCanonInboundFrameV1(buildCanonTurnContextV2({
252
260
  content: input.content,
253
261
  conversationId: input.conversationId,
254
262
  participantContext: input.participantContext,
@@ -257,9 +265,8 @@ function buildCanonPrompt(input) {
257
265
  activeSelfContextId: input.activeSelfContextId,
258
266
  provenance: input.provenance,
259
267
  replyContext: input.replyContext,
260
- sessionContextLines: input.sessionContextLines,
261
268
  message: input.message,
262
- }));
269
+ })));
263
270
  }
264
271
  function renderInboundContent(message, materialized) {
265
272
  return renderCanonHostInboundContent(message, materialized);
@@ -668,6 +675,18 @@ export async function main() {
668
675
  blocks: session.turnBlocks,
669
676
  }).catch(() => { });
670
677
  }
678
+ function upsertCodexTextSegment(session, event) {
679
+ const next = applyTextSegmentBlock({
680
+ turnLiveText: session.turnLiveText,
681
+ turnBlocks: session.turnBlocks,
682
+ }, {
683
+ turnId: session.currentTurnId,
684
+ itemId: event.itemId,
685
+ text: event.text,
686
+ });
687
+ session.turnLiveText = next.turnLiveText;
688
+ session.turnBlocks = next.turnBlocks;
689
+ }
671
690
  function upsertTurnBlock(session, block) {
672
691
  const now = Date.now();
673
692
  const index = session.turnBlocks.findIndex((existing) => existing.id === block.id);
@@ -867,11 +886,11 @@ export async function main() {
867
886
  adapter,
868
887
  queue: [],
869
888
  running: false,
870
- state: {
889
+ state: buildCodexInitialSessionState({
871
890
  model: policy.model,
872
891
  permissionMode: policy.permissionMode,
873
- state: 'idle',
874
- },
892
+ effort: initialEffort,
893
+ }),
875
894
  policyFingerprint: policy.fingerprint,
876
895
  turnState: 'idle',
877
896
  currentTurnId: null,
@@ -907,8 +926,17 @@ export async function main() {
907
926
  pendingSessionCreations.delete(conversationId);
908
927
  }
909
928
  }
910
- function enqueuePrompt(session, prompt, intent = 'queue', toFront = false, sourceMessageId, markAccepted = false, imagePaths = [], mediaAddDirs = [], planMode = false) {
911
- const nextPrompt = { prompt, intent, sourceMessageId, markAccepted, imagePaths, mediaAddDirs, planMode };
929
+ function enqueuePrompt(session, prompt, intent = 'queue', toFront = false, sourceMessageId, markAccepted = false, imagePaths = [], mediaAddDirs = [], planMode = false, artifactRoutingMode = 'disabled') {
930
+ const nextPrompt = {
931
+ prompt,
932
+ intent,
933
+ sourceMessageId,
934
+ markAccepted,
935
+ imagePaths,
936
+ mediaAddDirs,
937
+ planMode,
938
+ artifactRoutingMode,
939
+ };
912
940
  if (toFront) {
913
941
  session.queue.unshift(nextPrompt);
914
942
  }
@@ -919,6 +947,11 @@ export async function main() {
919
947
  writeTurn(session);
920
948
  void runNextTurn(session);
921
949
  }
950
+ function resolveArtifactRoutingMode(participantContext) {
951
+ return participantContext.conversationType === 'direct' && participantContext.isOwner
952
+ ? 'workspace-generated'
953
+ : 'disabled';
954
+ }
922
955
  async function waitForRuntimeInputResponse(input) {
923
956
  while (Date.now() < input.expiresAt) {
924
957
  const response = await client.consumeRuntimeInputResponse({
@@ -1291,43 +1324,50 @@ export async function main() {
1291
1324
  message: input.message,
1292
1325
  });
1293
1326
  if (session.running && deliveryIntent === 'interrupt') {
1294
- enqueuePrompt(session, prompt, deliveryIntent, true, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode);
1327
+ const artifactRoutingMode = resolveArtifactRoutingMode(participantContext);
1328
+ enqueuePrompt(session, prompt, deliveryIntent, true, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode, artifactRoutingMode);
1295
1329
  console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Interrupting current turn for explicit human send-now`);
1296
1330
  await session.adapter.interrupt().catch(() => { });
1297
1331
  clearStreaming(input.conversationId);
1298
1332
  typingSignals.clear(input.conversationId).catch(() => { });
1299
1333
  return;
1300
1334
  }
1301
- enqueuePrompt(session, prompt, deliveryIntent, false, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode);
1335
+ const artifactRoutingMode = resolveArtifactRoutingMode(participantContext);
1336
+ enqueuePrompt(session, prompt, deliveryIntent, false, input.message.id, shouldMarkAccepted, imagePaths, mediaAddDirs, planCommand.planMode, artifactRoutingMode);
1337
+ }
1338
+ function sendTurnArtifactFile(session, file) {
1339
+ return sendMediaFileMessage(client, session.conversationId, file.path, '', {
1340
+ ...(session.activeSelfContextId ? { selfContextId: session.activeSelfContextId } : {}),
1341
+ metadata: {
1342
+ ...(session.currentTurnId ? { turnId: session.currentTurnId } : {}),
1343
+ // Media is permanent conversation content (promoted like a final
1344
+ // reply) but must never re-trigger other agents — the host's own
1345
+ // final text message stays the only turn-complete trigger.
1346
+ turnSemantics: 'turn_complete',
1347
+ replyBehavior: 'suppress_auto_reply',
1348
+ },
1349
+ });
1302
1350
  }
1303
- async function routeSessionArtifacts(session, artifactDir) {
1304
- if (!artifactDir)
1351
+ async function routeWorkspaceGeneratedArtifacts(session, baseline) {
1352
+ if (!baseline)
1305
1353
  return;
1306
1354
  const logPrefix = `[canon-codex] [${session.conversationId.slice(0, 8)}]`;
1307
1355
  try {
1308
- const result = await flushTurnArtifacts({
1309
- artifactDir,
1310
- send: (file) => sendMediaFileMessage(client, session.conversationId, file.path, '', {
1311
- ...(session.activeSelfContextId ? { selfContextId: session.activeSelfContextId } : {}),
1312
- metadata: {
1313
- ...(session.currentTurnId ? { turnId: session.currentTurnId } : {}),
1314
- // Media is permanent conversation content (promoted like a final
1315
- // reply) but must never re-trigger other agents — the host's own
1316
- // final text message stays the only turn-complete trigger.
1317
- turnSemantics: 'turn_complete',
1318
- replyBehavior: 'suppress_auto_reply',
1319
- },
1320
- }),
1356
+ const result = await collectTurnArtifacts({
1357
+ cwd: session.cwd,
1358
+ baseline,
1321
1359
  });
1322
- for (const entry of result.sent) {
1323
- console.error(`${logPrefix} Routed artifact ${entry.file.fileName} (${entry.messageId})`
1324
- + (entry.removeFailed ? '; removal failed, so it may resend later' : ''));
1325
- }
1326
- for (const failure of result.failed) {
1327
- console.error(`${logPrefix} Artifact upload failed for ${failure.file.fileName}; left in place: ${failure.error}`);
1360
+ for (const file of result.files) {
1361
+ try {
1362
+ const { messageId } = await sendTurnArtifactFile(session, file);
1363
+ console.error(`${logPrefix} Routed generated artifact ${file.relativePath ?? file.fileName} (${messageId})`);
1364
+ }
1365
+ catch (error) {
1366
+ console.error(`${logPrefix} Artifact upload failed for ${file.relativePath ?? file.fileName}: ${error instanceof Error ? error.message : String(error)}`);
1367
+ }
1328
1368
  }
1329
1369
  for (const skipped of result.skipped) {
1330
- if (skipped.reason === 'too-large' || skipped.reason === 'file-cap' || skipped.reason === 'unsupported') {
1370
+ if (skipped.reason === 'too-large' || skipped.reason === 'file-cap' || skipped.reason === 'scan-limit') {
1331
1371
  console.error(`${logPrefix} Artifact skipped ${skipped.fileName} (${skipped.reason})`);
1332
1372
  }
1333
1373
  }
@@ -1361,19 +1401,26 @@ export async function main() {
1361
1401
  // Status-only seed: 'thinking' renders as a working filament row on the
1362
1402
  // clients; text here would be bubbled as speech (v4 register rule).
1363
1403
  writeCodexStreaming(session, '', 'thinking');
1364
- let artifactDir = null;
1404
+ let artifactBaseline = null;
1365
1405
  let artifactsRouted = false;
1366
1406
  const routeArtifactsOnce = async () => {
1367
1407
  if (artifactsRouted)
1368
1408
  return;
1369
1409
  artifactsRouted = true;
1370
- await routeSessionArtifacts(session, artifactDir);
1410
+ if (nextTurn.artifactRoutingMode === 'workspace-generated') {
1411
+ await routeWorkspaceGeneratedArtifacts(session, artifactBaseline);
1412
+ }
1371
1413
  };
1372
1414
  try {
1373
1415
  const turnId = session.currentTurnId ?? randomUUID();
1374
1416
  session.currentTurnId = turnId;
1375
- artifactDir = await ensureTurnArtifactDir(session.cwd, turnId);
1376
- const turnPrompt = `${nextTurn.prompt}\n\n${buildTurnArtifactContextLine(artifactDir)}`;
1417
+ let turnPrompt = nextTurn.prompt;
1418
+ if (nextTurn.artifactRoutingMode === 'workspace-generated') {
1419
+ artifactBaseline = await captureTurnArtifactSnapshot({ cwd: session.cwd }).catch((error) => {
1420
+ console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Artifact snapshot failed:`, error instanceof Error ? error.message : error);
1421
+ return null;
1422
+ });
1423
+ }
1377
1424
  const modelGuard = buildCodexModelGuardMessage(session.state.model, codexCliStatus);
1378
1425
  if (modelGuard) {
1379
1426
  throw new ExecutionEnvironmentError(modelGuard, modelGuard);
@@ -1395,7 +1442,8 @@ export async function main() {
1395
1442
  markTurnProgress(session);
1396
1443
  writeTurn(session);
1397
1444
  stopVisibleWorkSignal(session);
1398
- writeCodexStreaming(session, event.text, 'streaming');
1445
+ upsertCodexTextSegment(session, event);
1446
+ writeCodexStreaming(session, null, 'streaming');
1399
1447
  return;
1400
1448
  }
1401
1449
  if (event.type === 'plan.updated') {
@@ -1824,7 +1872,7 @@ export async function main() {
1824
1872
  {
1825
1873
  id: 'mediaOut',
1826
1874
  label: 'Media out',
1827
- value: 'Explicit turn artifact directory',
1875
+ value: 'Generated media artifacts',
1828
1876
  },
1829
1877
  ],
1830
1878
  execution: {
@@ -1,3 +1,4 @@
1
+ import type { TurnOutputBlock } from '@canonmsg/core';
1
2
  interface RunningCommandBlock {
2
3
  command: string;
3
4
  blockId: string;
@@ -7,7 +8,17 @@ export interface CommandBlockTracker {
7
8
  sequence: number;
8
9
  running: RunningCommandBlock[];
9
10
  }
11
+ export interface TextSegmentBlockState {
12
+ turnLiveText: string;
13
+ turnBlocks: TurnOutputBlock[];
14
+ }
10
15
  export declare function createCommandBlockTracker(): CommandBlockTracker;
16
+ export declare function applyTextSegmentBlock(state: TextSegmentBlockState, input: {
17
+ turnId: string | null | undefined;
18
+ itemId?: string;
19
+ text: string;
20
+ now?: number;
21
+ }): TextSegmentBlockState;
11
22
  export declare function beginCommandBlock(tracker: CommandBlockTracker, input: {
12
23
  turnId: string | null | undefined;
13
24
  command: string;
@@ -8,6 +8,49 @@ function normalizeOptionalString(value) {
8
8
  const normalized = value?.trim();
9
9
  return normalized ? normalized : undefined;
10
10
  }
11
+ function textBlockId(turnId, itemId) {
12
+ return `message:${turnId ?? 'turn'}:${normalizeOptionalString(itemId) ?? 'latest'}`;
13
+ }
14
+ function buildLiveText(blocks) {
15
+ return blocks
16
+ .filter((block) => block.kind === 'text' && block.text?.trim())
17
+ .sort((left, right) => {
18
+ if (left.sequence !== right.sequence)
19
+ return left.sequence - right.sequence;
20
+ return left.id.localeCompare(right.id);
21
+ })
22
+ .map((block) => block.text)
23
+ .join('\n\n');
24
+ }
25
+ export function applyTextSegmentBlock(state, input) {
26
+ const id = textBlockId(input.turnId, input.itemId);
27
+ const now = input.now ?? Date.now();
28
+ const index = state.turnBlocks.findIndex((block) => block.id === id);
29
+ const existing = index >= 0 ? state.turnBlocks[index] : null;
30
+ const next = {
31
+ ...(existing ?? {
32
+ sequence: state.turnBlocks.length + 1,
33
+ createdAt: now,
34
+ }),
35
+ id,
36
+ turnId: input.turnId ?? id,
37
+ kind: 'text',
38
+ status: existing?.status ?? 'running',
39
+ text: input.text,
40
+ updatedAt: now,
41
+ };
42
+ const turnBlocks = index >= 0
43
+ ? [
44
+ ...state.turnBlocks.slice(0, index),
45
+ next,
46
+ ...state.turnBlocks.slice(index + 1),
47
+ ]
48
+ : [...state.turnBlocks, next];
49
+ return {
50
+ turnBlocks,
51
+ turnLiveText: buildLiveText(turnBlocks) || state.turnLiveText,
52
+ };
53
+ }
11
54
  function nextCommandBlockId(tracker, turnId, itemId) {
12
55
  const stableTurnId = turnId ?? 'turn';
13
56
  if (itemId)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/codex-plugin",
3
- "version": "0.18.4",
3
+ "version": "0.18.6",
4
4
  "description": "Canon host integration for Codex CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -29,9 +29,9 @@
29
29
  "prepack": "npm run build"
30
30
  },
31
31
  "dependencies": {
32
- "@canonmsg/agent-sdk": "^3.2.3",
33
- "@canonmsg/coding-agent-host": "^0.2.0",
34
- "@canonmsg/core": "^2.6.0"
32
+ "@canonmsg/agent-sdk": "^3.3.1",
33
+ "@canonmsg/coding-agent-host": "^0.2.2",
34
+ "@canonmsg/core": "^2.8.1"
35
35
  },
36
36
  "engines": {
37
37
  "node": ">=18.0.0"