@canonmsg/codex-plugin 0.29.0 → 0.29.2

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.
Files changed (2) hide show
  1. package/dist/host.js +99 -103
  2. package/package.json +4 -4
package/dist/host.js CHANGED
@@ -6,7 +6,7 @@ import { dirname } from 'node:path';
6
6
  import { parseArgs } from 'node:util';
7
7
  import { getCodexImagePath, materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMessage, } from '@canonmsg/agent-sdk';
8
8
  import { buildTrailBlockId, buildUndeliverableFinalNotice, captureTurnArtifactSnapshot, createTurnArtifactRouter, IDLE_TIMEOUT_MS, PLAN_BLOCK_TITLE, resolveTurnArtifactRouting, collectMissedInboundMessages, createRecoveryCheckpointTracker, createReconnectRecoveryCoordinator, STARTUP_RECOVERY_PAGE_SIZE, } from '@canonmsg/coding-agent-host';
9
- import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, 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, decideAutoReply, initRTDBAuth, buildLocalRuntimeId, heartbeatLocalRuntimeEntry, markLocalRuntimeStopped, normalizeTurnMetadata, parseTurnVerbosityConfig, RuntimeRequestManager, prepareConversationEnvironment, releaseConversationEnvironment, resolveLocalRuntimeSessionState, resolveCanonAgent, verifyResolvedAgentEnvironment, CanonApiError, loadRuntimeSessionState, sendMessageWithRetry, sendMessageWithRetryChunked, saveRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveConfiguredWorkspaceCwd, isSilentTurnSuppressed, resolveSilentTurnDelivery, resolveTurnVerbosity, shouldTriggerAgentTurn, upsertLocalRuntimeEntry, } from '@canonmsg/core';
9
+ import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, resolveQuestionAllowOther, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, buildCanonInboundFrameV1, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createRuntimeStatePublisher, createRuntimeHeartbeat, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, getActiveProfileLock, decideAutoReply, initRTDBAuth, buildLocalRuntimeId, heartbeatLocalRuntimeEntry, markLocalRuntimeStopped, normalizeTurnMetadata, parseTurnVerbosityConfig, RuntimeRequestManager, prepareConversationEnvironment, releaseConversationEnvironment, resolveLocalRuntimeSessionState, resolveCanonAgent, verifyResolvedAgentEnvironment, CanonApiError, loadRuntimeSessionState, sendMessageWithRetry, sendMessageWithRetryChunked, saveRuntimeSessionState, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveConfiguredWorkspaceCwd, isSilentTurnSuppressed, resolveSilentTurnDelivery, resolveTurnVerbosity, shouldTriggerAgentTurn, upsertLocalRuntimeEntry, } from '@canonmsg/core';
10
10
  import { validateCard } from '@canonmsg/rich-cards';
11
11
  import { CodexConversationAdapter, } from './adapter.js';
12
12
  import { CodexAppServerAdapter, } from './app-server-adapter.js';
@@ -253,9 +253,6 @@ export function buildCodexTurnResponseRouting(input) {
253
253
  export function getCodexRequestingUserId(message) {
254
254
  return message.senderType === 'human' ? message.senderId : null;
255
255
  }
256
- async function publishAgentRuntime(agentId, runtime, rtdb) {
257
- await publishHostAgentRuntime(agentId, 'codex', runtime, rtdb);
258
- }
259
256
  export function resolveSessionExecutionMode(serviceAgentMode = false, defaultExecutionMode = 'worktree') {
260
257
  if (serviceAgentMode)
261
258
  return 'locked';
@@ -2022,7 +2019,7 @@ export async function main() {
2022
2019
  return;
2023
2020
  }
2024
2021
  if (event.type === 'skills.changed') {
2025
- void refreshCodexSkillInventory(true).then(() => publishRuntimeHeartbeat());
2022
+ void refreshCodexSkillInventory(true).then(() => runtimeHeartbeat.refresh());
2026
2023
  return;
2027
2024
  }
2028
2025
  if (event.type === 'settings.updated') {
@@ -2401,7 +2398,6 @@ export async function main() {
2401
2398
  acceptedInboundMessageIds.delete(oldest);
2402
2399
  }
2403
2400
  }
2404
- let streamConnected = false;
2405
2401
  const hostAvailableExecutionModes = serviceAgentMode
2406
2402
  ? ['locked']
2407
2403
  : [...EXECUTION_ENVIRONMENT_MODES];
@@ -2569,108 +2565,109 @@ export async function main() {
2569
2565
  console.error(`[canon-codex] [${(error.conversationId ?? 'unknown').slice(0, 8)}] Control ${error.key ?? 'poll'} handler failed:`, error.error instanceof Error ? error.error.message : error.error);
2570
2566
  },
2571
2567
  });
2572
- let publishRuntimeDetailsInFlight = false;
2573
- const publishRuntimeHeartbeat = async () => {
2568
+ const publishLocalHeartbeat = () => {
2574
2569
  heartbeatLocalRuntimeEntry(runtimeId, {
2575
2570
  agentId,
2576
2571
  agentName: profileAgentName,
2577
2572
  cwd: workingDir,
2578
2573
  baseCwd: workingDir,
2579
2574
  });
2580
- if (!streamConnected)
2575
+ };
2576
+ const refreshRuntimeDetails = async (signal) => {
2577
+ await refreshKnownConversationIds().catch((error) => {
2578
+ console.error('[canon-codex] Failed to refresh known conversations:', error);
2579
+ });
2580
+ if (signal.aborted)
2581
2581
  return;
2582
- await publishAgentRuntime(agentId, runtimeDescriptor, rtdb).catch((error) => {
2583
- console.error('[canon-codex] Failed to publish agent runtime:', error);
2582
+ await publishHostSessionSnapshots({
2583
+ conversationIds: Array.from(knownConversationIds),
2584
+ agentId,
2585
+ rtdb,
2586
+ clientType: 'codex',
2587
+ }).catch((error) => {
2588
+ console.error('[canon-codex] Failed to publish session snapshots:', error);
2584
2589
  });
2585
- if (publishRuntimeDetailsInFlight)
2590
+ if (signal.aborted)
2586
2591
  return;
2587
- publishRuntimeDetailsInFlight = true;
2588
- try {
2589
- await refreshKnownConversationIds().catch((error) => {
2590
- console.error('[canon-codex] Failed to refresh known conversations:', error);
2591
- });
2592
- await publishHostSessionSnapshots({
2593
- conversationIds: Array.from(knownConversationIds),
2594
- agentId,
2595
- rtdb,
2596
- clientType: 'codex',
2597
- }).catch((error) => {
2598
- console.error('[canon-codex] Failed to publish session snapshots:', error);
2599
- });
2600
- await Promise.all(Array.from(knownConversationIds).map(async (conversationId) => {
2601
- const session = sessions.get(conversationId);
2602
- const workspaceId = session
2603
- ? resolveWorkspaceIdForBaseCwd(session.environment.baseCwd)
2604
- : runtimeDescriptor.defaultWorkspaceId;
2605
- const workspace = workspaceOptions.find((option) => option.id === workspaceId) ?? null;
2606
- const descriptor = runtimeDescriptor.runtimeDescriptor;
2607
- if (!descriptor)
2608
- return;
2609
- const payload = {
2610
- descriptor,
2611
- surfaceMode: 'host',
2612
- // The exec --json transport cannot block on approvals — without a
2613
- // strip-level warning a user can believe they have an approval
2614
- // gate they do not have.
2615
- ...(useAppServer
2616
- ? {}
2617
- : { warning: "Approvals can't block on this Codex CLI — update Codex to enable the app-server transport and approval gates." }),
2618
- statusItems: [
2619
- {
2620
- id: 'transport',
2621
- label: 'Transport',
2622
- value: useAppServer ? 'app-server' : 'exec --json',
2623
- },
2624
- {
2625
- id: 'streaming',
2626
- label: 'Live output',
2627
- value: useAppServer
2628
- ? 'Plans, questions, approvals, tools, and message deltas'
2629
- : 'Thinking, tools, and completed-message previews',
2630
- },
2631
- {
2632
- id: 'codex-cli',
2633
- label: 'Codex CLI',
2634
- value: codexCliStatus.version ?? (codexCliStatus.raw ?? 'Version unknown'),
2635
- tone: codexCliStatus.version ? 'default' : 'warning',
2636
- },
2637
- {
2638
- id: 'nativeActions',
2639
- label: 'Native actions',
2640
- value: useAppServer ? 'Enabled' : 'Limited until app-server transport',
2641
- ...(useAppServer ? {} : { tone: 'warning' }),
2642
- },
2643
- {
2644
- id: 'mediaOut',
2645
- label: 'Media out',
2646
- value: 'Generated media artifacts',
2647
- },
2648
- ],
2649
- execution: {
2650
- resolvedWorkspaceLabel: workspace?.label ?? workspaceId ?? null,
2651
- resolvedCwd: session?.cwd ?? workspace?.cwd ?? workingDir,
2652
- workspaceRootId: workspace?.workspaceRootId ?? null,
2653
- workspaceRelativePath: workspace?.workspaceRelativePath ?? null,
2654
- executionMode: session?.environment.mode ?? null,
2655
- executionBranch: session?.environment.branch ?? null,
2656
- worktreePath: session?.environment.worktreePath ?? null,
2657
- fallbackReason: resolveExecutionFallbackReason(session?.environment),
2592
+ const results = await Promise.allSettled(Array.from(knownConversationIds).map(async (conversationId) => {
2593
+ const session = sessions.get(conversationId);
2594
+ const workspaceId = session
2595
+ ? resolveWorkspaceIdForBaseCwd(session.environment.baseCwd)
2596
+ : runtimeDescriptor.defaultWorkspaceId;
2597
+ const workspace = workspaceOptions.find((option) => option.id === workspaceId) ?? null;
2598
+ const descriptor = runtimeDescriptor.runtimeDescriptor;
2599
+ if (!descriptor)
2600
+ return;
2601
+ const payload = {
2602
+ descriptor,
2603
+ surfaceMode: 'host',
2604
+ // The exec --json transport cannot block on approvals — without a
2605
+ // strip-level warning a user can believe they have an approval
2606
+ // gate they do not have.
2607
+ ...(useAppServer
2608
+ ? {}
2609
+ : { warning: "Approvals can't block on this Codex CLI — update Codex to enable the app-server transport and approval gates." }),
2610
+ statusItems: [
2611
+ {
2612
+ id: 'transport',
2613
+ label: 'Transport',
2614
+ value: useAppServer ? 'app-server' : 'exec --json',
2658
2615
  },
2659
- notes: [
2660
- useAppServer
2661
- ? 'This Codex host uses the app-server transport, so Canon can route native plan mode, runtime questions, approvals, and live turn updates.'
2662
- : 'This Codex host uses the current exec --json transport, so Canon can show thinking, tool activity, and completed assistant-message previews, but not native plan questions or structured approvals.',
2663
- ],
2664
- };
2665
- await runtimeState.writeRuntimeInfo(conversationId, payload);
2666
- })).catch((error) => {
2667
- console.error('[canon-codex] Failed to publish runtime info:', error);
2668
- });
2669
- }
2670
- finally {
2671
- publishRuntimeDetailsInFlight = false;
2672
- }
2616
+ {
2617
+ id: 'streaming',
2618
+ label: 'Live output',
2619
+ value: useAppServer
2620
+ ? 'Plans, questions, approvals, tools, and message deltas'
2621
+ : 'Thinking, tools, and completed-message previews',
2622
+ },
2623
+ {
2624
+ id: 'codex-cli',
2625
+ label: 'Codex CLI',
2626
+ value: codexCliStatus.version ?? (codexCliStatus.raw ?? 'Version unknown'),
2627
+ tone: codexCliStatus.version ? 'default' : 'warning',
2628
+ },
2629
+ {
2630
+ id: 'nativeActions',
2631
+ label: 'Native actions',
2632
+ value: useAppServer ? 'Enabled' : 'Limited until app-server transport',
2633
+ ...(useAppServer ? {} : { tone: 'warning' }),
2634
+ },
2635
+ {
2636
+ id: 'mediaOut',
2637
+ label: 'Media out',
2638
+ value: 'Generated media artifacts',
2639
+ },
2640
+ ],
2641
+ execution: {
2642
+ resolvedWorkspaceLabel: workspace?.label ?? workspaceId ?? null,
2643
+ resolvedCwd: session?.cwd ?? workspace?.cwd ?? workingDir,
2644
+ workspaceRootId: workspace?.workspaceRootId ?? null,
2645
+ workspaceRelativePath: workspace?.workspaceRelativePath ?? null,
2646
+ executionMode: session?.environment.mode ?? null,
2647
+ executionBranch: session?.environment.branch ?? null,
2648
+ worktreePath: session?.environment.worktreePath ?? null,
2649
+ fallbackReason: resolveExecutionFallbackReason(session?.environment),
2650
+ },
2651
+ notes: [
2652
+ useAppServer
2653
+ ? 'This Codex host uses the app-server transport, so Canon can route native plan mode, runtime questions, approvals, and live turn updates.'
2654
+ : 'This Codex host uses the current exec --json transport, so Canon can show thinking, tool activity, and completed assistant-message previews, but not native plan questions or structured approvals.',
2655
+ ],
2656
+ };
2657
+ await runtimeState.writeRuntimeInfo(conversationId, payload);
2658
+ }));
2659
+ const failure = results.find((result) => result.status === 'rejected');
2660
+ if (failure?.status === 'rejected')
2661
+ throw failure.reason;
2673
2662
  };
2663
+ const runtimeHeartbeat = createRuntimeHeartbeat({
2664
+ publisher: runtimeState,
2665
+ getRuntime: () => runtimeDescriptor,
2666
+ refreshDetails: refreshRuntimeDetails,
2667
+ onError: (error, operation) => {
2668
+ console.error(`[canon-codex] Runtime ${operation} failed:`, error);
2669
+ },
2670
+ });
2674
2671
  let startupRecoveryComplete = false;
2675
2672
  async function recoverInboundMessageGaps() {
2676
2673
  // A reconnect can reveal conversations created while this host was
@@ -2796,14 +2793,13 @@ export async function main() {
2796
2793
  codexDynamicTools = filterCodexCommunicationTools(baseCodexDynamicTools, outboundPolicy);
2797
2794
  },
2798
2795
  onConnected: () => {
2799
- streamConnected = true;
2800
- void publishRuntimeHeartbeat();
2796
+ publishLocalHeartbeat();
2797
+ runtimeHeartbeat.connect();
2801
2798
  observeReconnectRecovery('reconnect', reconnectRecovery.onConnected());
2802
2799
  console.error('[canon-codex] SSE connected');
2803
2800
  },
2804
2801
  onDisconnected: () => {
2805
- streamConnected = false;
2806
- runtimeState.clearAgentRuntime().catch(() => { });
2802
+ void runtimeHeartbeat.disconnect();
2807
2803
  console.error('[canon-codex] SSE disconnected');
2808
2804
  },
2809
2805
  onReplayExpired: () => observeReconnectRecovery('replay-expired', reconnectRecovery.onReplayExpired()),
@@ -2840,7 +2836,7 @@ export async function main() {
2840
2836
  writeTurn(session);
2841
2837
  }
2842
2838
  }
2843
- void publishRuntimeHeartbeat();
2839
+ publishLocalHeartbeat();
2844
2840
  }, HEARTBEAT_MS);
2845
2841
  const idleCheck = setInterval(() => {
2846
2842
  const now = Date.now();
@@ -2864,7 +2860,7 @@ export async function main() {
2864
2860
  }
2865
2861
  runtimeRequests.dispose();
2866
2862
  stream.stop();
2867
- await runtimeState.clearAgentRuntime().catch(() => { });
2863
+ await runtimeHeartbeat.dispose();
2868
2864
  for (const session of [...sessions.values()]) {
2869
2865
  await session.adapter.interrupt().catch(() => { });
2870
2866
  closeSession(session.conversationId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/codex-plugin",
3
- "version": "0.29.0",
3
+ "version": "0.29.2",
4
4
  "description": "Canon host integration for Codex CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -29,10 +29,10 @@
29
29
  "prepack": "npm run build"
30
30
  },
31
31
  "dependencies": {
32
- "@canonmsg/agent-sdk": "^10.0.0",
33
- "@canonmsg/agent-tools": "^0.8.0",
32
+ "@canonmsg/agent-sdk": "^10.2.0",
33
+ "@canonmsg/agent-tools": "^0.9.0",
34
34
  "@canonmsg/coding-agent-host": "^0.7.0",
35
- "@canonmsg/core": "^12.0.0",
35
+ "@canonmsg/core": "^12.2.0",
36
36
  "@canonmsg/rich-cards": "^0.10.4"
37
37
  },
38
38
  "engines": {