@canonmsg/codex-plugin 0.30.1 → 0.32.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/dist/host.d.ts CHANGED
@@ -247,6 +247,11 @@ export declare function shouldRouteCodexTurnArtifacts(input: {
247
247
  artifactRoutingMode: TurnArtifactRoutingMode | undefined;
248
248
  silenced: boolean;
249
249
  finalText: string | null | undefined;
250
+ currentAudience?: {
251
+ memberIds?: readonly string[];
252
+ agentId: string;
253
+ ownerId: string | null | undefined;
254
+ };
250
255
  }): TurnArtifactRoutingDecision;
251
256
  export declare function main(): Promise<void>;
252
257
  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 { 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, 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';
8
+ import { buildTrailBlockId, buildUndeliverableFinalNotice, captureTurnArtifactSnapshot, createTurnArtifactRouter, IDLE_TIMEOUT_MS, PLAN_BLOCK_TITLE, resolveTurnArtifactRouting, createRecoveryCheckpointTracker, createReconnectRecoveryCoordinator, } from '@canonmsg/coding-agent-host';
9
+ import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, resolveQuestionAllowOther, buildCanonTurnContextV2, buildCanonGroupContext, buildCompactGroupContextLines, 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, sendMessageWithRetry, isPendingCanonOperation, sendMessageWithRetryChunked, saveRuntimeSessionState, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveConfiguredWorkspaceCwd, isSilentTurnSuppressed, resolveSilentTurnDelivery, resolveTurnVerbosity, 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';
@@ -599,8 +599,12 @@ export function selectCodexNativeImagePaths(imagePaths, nativeVisionEnabled) {
599
599
  * does not currently carry into the catch without a race.
600
600
  */
601
601
  export function shouldRouteCodexTurnArtifacts(input) {
602
+ const audience = input.currentAudience;
603
+ const ownerPair = !audience || (audience.ownerId !== audience.agentId
604
+ && Boolean(audience.ownerId) && audience.memberIds?.length === 2
605
+ && audience.memberIds.includes(audience.agentId) && audience.memberIds.includes(audience.ownerId));
602
606
  return resolveTurnArtifactRouting({
603
- artifactRoutingMode: input.artifactRoutingMode,
607
+ artifactRoutingMode: ownerPair ? input.artifactRoutingMode : 'disabled',
604
608
  silenced: isSilentTurnSuppressed({ silenced: input.silenced, finalText: input.finalText }),
605
609
  });
606
610
  }
@@ -684,7 +688,8 @@ export async function main() {
684
688
  lockHandle?.release();
685
689
  throw error;
686
690
  }
687
- const client = new CanonClient(apiKey, baseUrl);
691
+ const client = new CanonClient(apiKey, baseUrl, { environmentId: resolvedAgent.environmentId, streamUrl });
692
+ const endpoint = await client.getEndpoint();
688
693
  const rtdb = initRTDBAuth(client, { rtdbUrl, firebaseApiKey });
689
694
  const typingSignals = createTypingStatusPublisher({
690
695
  setTyping: (conversationId, typing, status) => status
@@ -854,19 +859,23 @@ export async function main() {
854
859
  if (cached) {
855
860
  conversationCache.set(payload.conversationId, {
856
861
  ...cached,
862
+ ...payload.changes,
857
863
  memberIds,
858
864
  });
865
+ void client.rememberConversation(conversationCache.get(payload.conversationId))
866
+ .catch((error) => console.error('[canon] Failed to persist conversation update:', error));
859
867
  }
860
868
  if (membershipChange) {
861
869
  pendingMembershipChanges.set(payload.conversationId, membershipChange);
862
870
  }
863
871
  if (!memberIds.includes(agentId)) {
864
872
  knownConversationIds.delete(payload.conversationId);
865
- conversationCache.delete(payload.conversationId);
873
+ pendingMembershipChanges.delete(payload.conversationId);
874
+ closeSession(payload.conversationId);
866
875
  }
867
876
  }
868
877
  function getGroupContextMode(conversationId, conversation) {
869
- if (conversation?.type !== 'group')
878
+ if (!conversation)
870
879
  return undefined;
871
880
  if (pendingMembershipChanges.has(conversationId))
872
881
  return 'membership_change';
@@ -884,7 +893,7 @@ export async function main() {
884
893
  }
885
894
  async function loadHydratedInboundContext(input) {
886
895
  const [conversation, page] = await Promise.all([
887
- getConversationMeta(input.conversationId),
896
+ getConversationMeta(input.conversationId, { refreshIfMemberMissing: agentId }),
888
897
  input.hydratedPage
889
898
  ? Promise.resolve(input.hydratedPage)
890
899
  : client.getMessagesPage(input.conversationId, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT).catch(() => null),
@@ -893,6 +902,7 @@ export async function main() {
893
902
  agentId,
894
903
  conversationId: input.conversationId,
895
904
  conversation,
905
+ requireAgentMembership: true,
896
906
  page,
897
907
  activeSelfContextId: input.activeSelfContextId,
898
908
  selfContexts: input.selfContexts,
@@ -1204,6 +1214,10 @@ export async function main() {
1204
1214
  }
1205
1215
  }
1206
1216
  async function getOrCreateSession(conversationId) {
1217
+ const current = conversationCache.get(conversationId);
1218
+ if (current && !current.memberIds.includes(agentId)) {
1219
+ throw new Error('Agent is no longer a member of this conversation');
1220
+ }
1207
1221
  knownConversationIds.add(conversationId);
1208
1222
  const existing = sessions.get(conversationId);
1209
1223
  if (existing && !existing.closed) {
@@ -1776,6 +1790,7 @@ export async function main() {
1776
1790
  if (input.turnDispatch && input.turnDispatch.kind !== 'run_turn') {
1777
1791
  console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Suppressed server-dispatched turn: ${input.turnDispatch.reason}`);
1778
1792
  recoveryCheckpointsFor(input.conversationId).settle(input.message.id);
1793
+ await endpoint.setInboundState(`message:${input.conversationId}:${input.message.id}`, 'settled', 'not-dispatched');
1779
1794
  return;
1780
1795
  }
1781
1796
  if (input.message.metadata?.type === 'plan_approval_reply') {
@@ -1784,6 +1799,7 @@ export async function main() {
1784
1799
  // replies left by an older coding descriptor instead of turning them
1785
1800
  // into hidden plan/implementation prompts after an upgrade or restart.
1786
1801
  recoveryCheckpointsFor(input.conversationId).settle(input.message.id);
1802
+ await endpoint.setInboundState(`message:${input.conversationId}:${input.message.id}`, 'settled', 'not-dispatched');
1787
1803
  return;
1788
1804
  }
1789
1805
  const planId = readString(input.message.metadata, 'planId');
@@ -1792,6 +1808,7 @@ export async function main() {
1792
1808
  metadata: input.message.metadata,
1793
1809
  })) {
1794
1810
  recoveryCheckpointsFor(input.conversationId).settle(input.message.id);
1811
+ await endpoint.setInboundState(`message:${input.conversationId}:${input.message.id}`, 'settled', 'not-dispatched');
1795
1812
  return;
1796
1813
  }
1797
1814
  const session = await getOrCreateSession(input.conversationId);
@@ -1866,6 +1883,7 @@ export async function main() {
1866
1883
  if (!autoReply.allow) {
1867
1884
  console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Suppressed auto-reply: ${autoReply.reason}`);
1868
1885
  recoveryCheckpointsFor(input.conversationId).settle(input.message.id);
1886
+ await endpoint.setInboundState(`message:${input.conversationId}:${input.message.id}`, 'settled', 'not-dispatched');
1869
1887
  return;
1870
1888
  }
1871
1889
  console.error(`[canon-codex] [${input.conversationId.slice(0, 8)}] Message from ${input.senderName}: "${content.slice(0, 80)}" (${autoReply.reason})`);
@@ -1896,6 +1914,7 @@ export async function main() {
1896
1914
  ...(input.replyAuthority ? { replyAuthority: input.replyAuthority } : {}),
1897
1915
  }).catch(() => { });
1898
1916
  recoveryCheckpointsFor(input.conversationId).settle(input.message.id);
1917
+ await endpoint.setInboundState(`message:${input.conversationId}:${input.message.id}`, 'settled', 'not-dispatched');
1899
1918
  return;
1900
1919
  }
1901
1920
  session.activeSelfContextId = activeSelfContextId;
@@ -1930,8 +1949,9 @@ export async function main() {
1930
1949
  replyAuthority: input.replyAuthority ?? null,
1931
1950
  });
1932
1951
  }
1933
- function sendTurnArtifactFile(session, file) {
1952
+ function sendTurnArtifactFile(session, file, canPublish) {
1934
1953
  return sendMediaFileMessage(client, session.conversationId, file.path, '', {
1954
+ canPublish,
1935
1955
  ...(session.currentReplyAuthority ? { replyAuthority: session.currentReplyAuthority } : {}),
1936
1956
  ...(session.activeSelfContextId ? { selfContextId: session.activeSelfContextId } : {}),
1937
1957
  metadata: {
@@ -1951,6 +1971,8 @@ export async function main() {
1951
1971
  if (!nextTurn)
1952
1972
  return;
1953
1973
  session.running = true;
1974
+ const inboundId = nextTurn.sourceMessageId ? `message:${session.conversationId}:${nextTurn.sourceMessageId}` : null;
1975
+ let journaledInput = false;
1954
1976
  session.state.lastError = undefined;
1955
1977
  session.state.state = 'running';
1956
1978
  session.currentTurnId = randomUUID();
@@ -2001,22 +2023,29 @@ export async function main() {
2001
2023
  // the collect-and-post loop, so there is no un-gated path left to the
2002
2024
  // workspace: the failure branches still send Canon's notice, and none of
2003
2025
  // them can post the artifacts of a turn that chose silence.
2026
+ const decideArtifactRouting = () => shouldRouteCodexTurnArtifacts({
2027
+ artifactRoutingMode: nextTurn.artifactRoutingMode,
2028
+ silenced: session.currentTurnSilenced,
2029
+ finalText: turnFinalText,
2030
+ currentAudience: { memberIds: conversationCache.get(session.conversationId)?.memberIds, agentId, ownerId },
2031
+ });
2004
2032
  const artifactRouter = createTurnArtifactRouter({
2005
- decide: () => shouldRouteCodexTurnArtifacts({
2006
- artifactRoutingMode: nextTurn.artifactRoutingMode,
2007
- silenced: session.currentTurnSilenced,
2008
- finalText: turnFinalText,
2009
- }),
2033
+ decide: decideArtifactRouting,
2010
2034
  baseline: () => artifactBaseline,
2011
2035
  cwd: () => session.cwd,
2012
- send: (file) => sendTurnArtifactFile(session, file),
2036
+ send: (file) => sendTurnArtifactFile(session, file, () => decideArtifactRouting().route),
2013
2037
  log: (line) => console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] ${line}`),
2014
2038
  });
2015
2039
  const routeArtifactsOnce = artifactRouter.route;
2016
2040
  try {
2017
2041
  const turnId = session.currentTurnId ?? randomUUID();
2018
2042
  session.currentTurnId = turnId;
2019
- let turnPrompt = nextTurn.prompt;
2043
+ // Queueing does not freeze the audience: refresh at the native turn
2044
+ // boundary while retaining the same thread and its existing memory.
2045
+ const roster = buildCanonGroupContext({
2046
+ conversation: conversationCache.get(session.conversationId), messages: [], agentId, ownerId, ownerName,
2047
+ });
2048
+ let turnPrompt = [nextTurn.prompt, ...(roster ? buildCompactGroupContextLines(roster, 'initial') : [])].join('\n\n');
2020
2049
  if (nextTurn.artifactRoutingMode === 'workspace-generated') {
2021
2050
  artifactBaseline = await captureTurnArtifactSnapshot({ cwd: session.cwd }).catch((error) => {
2022
2051
  console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Artifact snapshot failed:`, error instanceof Error ? error.message : error);
@@ -2161,6 +2190,12 @@ export async function main() {
2161
2190
  skillInvocationText: nextTurn.skillInvocationText,
2162
2191
  onServerRequest: (request) => handleCodexServerRequest(session, request, nextTurn.requestingUserId ?? null, nextTurn.sourceMessageId ?? null),
2163
2192
  });
2193
+ // Canon-origin queues must retain a durable execution owner, including
2194
+ // legacy queues restored before endpoint migration. Native continuations
2195
+ // without a Canon source message remain a separate operator path.
2196
+ if (inboundId && !await endpoint.claimInbound(inboundId))
2197
+ return;
2198
+ journaledInput = !!inboundId;
2164
2199
  let result = await runTurnOnce();
2165
2200
  if (!result.interrupted
2166
2201
  && !result.finalMessage
@@ -2176,6 +2211,8 @@ export async function main() {
2176
2211
  session.currentTurnSilenced = false;
2177
2212
  result = await runTurnOnce();
2178
2213
  }
2214
+ if (journaledInput)
2215
+ await endpoint.setInboundState(inboundId, 'settled', 'native-completed');
2179
2216
  // Both the artifact gate and the final delivery weigh silence against
2180
2217
  // this text, and they must weigh the same one — set it before any
2181
2218
  // completion branch runs, including the ones that route first.
@@ -2340,6 +2377,11 @@ export async function main() {
2340
2377
  }
2341
2378
  }
2342
2379
  catch (error) {
2380
+ if (isPendingCanonOperation(error)) {
2381
+ session.state.lastError = 'Canon delivery remains pending reconciliation.';
2382
+ writeState(session);
2383
+ return;
2384
+ }
2343
2385
  const message = error instanceof ExecutionEnvironmentError
2344
2386
  ? error.userMessage
2345
2387
  : error instanceof CanonApiError
@@ -2634,75 +2676,10 @@ export async function main() {
2634
2676
  console.error(`[canon-codex] Runtime ${operation} failed:`, error);
2635
2677
  },
2636
2678
  });
2637
- let startupRecoveryComplete = false;
2638
2679
  async function recoverInboundMessageGaps() {
2639
- // A reconnect can reveal conversations created while this host was
2640
- // offline. Refresh before sweeping cursors; this is one REST reconciliation
2641
- // pass, not an automatic retry loop.
2642
- const knownBeforeRefresh = new Set(knownConversationIds);
2680
+ // Refresh discovery only. Timeline reconstruction is never authorization to
2681
+ // execute historical turns; admitted pending work lives in the endpoint inbox.
2643
2682
  await refreshKnownConversationIds(true);
2644
- const conversationsDiscoveredWhileOffline = startupRecoveryComplete
2645
- ? new Set([...knownConversationIds].filter((id) => !knownBeforeRefresh.has(id)))
2646
- : new Set();
2647
- for (const conversationId of knownConversationIds) {
2648
- const recoveryCheckpoints = recoveryCheckpointsFor(conversationId);
2649
- const recoveryBatch = recoveryCheckpoints.reserveBatch();
2650
- try {
2651
- const cursor = loadRuntimeSessionState(runtimeId, {
2652
- conversationId,
2653
- baseCwd: workingDir,
2654
- })?.lastInboundMessageId ?? null;
2655
- const recovered = await collectMissedInboundMessages({
2656
- fetchPage: (before) => client.getMessagesPage(conversationId, STARTUP_RECOVERY_PAGE_SIZE, before),
2657
- cursor,
2658
- agentId,
2659
- requireContiguousCursor: true,
2660
- noCursorMode: conversationsDiscoveredWhileOffline.has(conversationId)
2661
- ? 'bounded-window'
2662
- : 'latest-only',
2663
- });
2664
- recoveryBatch.commit(recovered.messages.map((message) => message.id), recovered.mode === 'incomplete-gap' ? recovered.recoveryCursor : null);
2665
- if (recovered.mode === 'incomplete-gap') {
2666
- console.error(`[canon-codex] [${conversationId.slice(0, 8)}] recovery_incomplete_gap: cursor is missing; replaying ${recovered.messages.length} bounded inbound message(s) before advancing to ${recovered.recoveryCursor ?? 'no cursor'}`);
2667
- }
2668
- for (const message of recovered.messages) {
2669
- const isPlanReply = isCodexPlanApprovalReply(message.metadata, serviceAgentMode);
2670
- if (!isPlanReply && !shouldTriggerAgentTurn({
2671
- senderType: message.senderType,
2672
- metadata: message.metadata,
2673
- }).allow) {
2674
- recoveryCheckpoints.settle(message.id);
2675
- continue;
2676
- }
2677
- if (!claimInboundMessageId(message.id))
2678
- continue;
2679
- try {
2680
- await enqueueInboundMessage({
2681
- conversationId,
2682
- message,
2683
- senderName: message.senderName || message.senderId,
2684
- isOwner: message.senderId === ownerId,
2685
- behavior: recovered.newestPage.behavior,
2686
- activeSelfContextId: recovered.newestPage.activeSelfContextIdByMessageId?.[message.id] ?? null,
2687
- selfContexts: recovered.newestPage.selfContexts,
2688
- hydratedPage: recovered.newestPage,
2689
- });
2690
- settleInboundMessageId(message.id, true);
2691
- }
2692
- catch (error) {
2693
- settleInboundMessageId(message.id, false);
2694
- throw error;
2695
- }
2696
- }
2697
- if (recovered.messages.length > 0) {
2698
- console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Recovered ${recovered.messages.length} inbound message(s) (${recovered.mode})`);
2699
- }
2700
- }
2701
- catch (error) {
2702
- recoveryBatch.cancel();
2703
- console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Recovery failed:`, error instanceof Error ? error.message : error);
2704
- }
2705
- }
2706
2683
  }
2707
2684
  const reconnectRecovery = createReconnectRecoveryCoordinator(recoverInboundMessageGaps);
2708
2685
  const observeReconnectRecovery = (reason, recovery) => {
@@ -2712,39 +2689,51 @@ export async function main() {
2712
2689
  console.error(`[canon-codex] ${reason} recovery failed:`, error instanceof Error ? error.message : error);
2713
2690
  });
2714
2691
  };
2692
+ const inbound = endpoint.acceptInbound({ kind: 'message.created',
2693
+ offer: async (event) => {
2694
+ const payload = event.data;
2695
+ const message = payload.message;
2696
+ if (message.senderId === agentId) {
2697
+ await endpoint.setInboundState(event.id, 'settled', 'own-message');
2698
+ return;
2699
+ }
2700
+ if (!claimInboundMessageId(message.id))
2701
+ return;
2702
+ recoveryCheckpointsFor(payload.conversationId).track(message.id);
2703
+ if (payload.turnDispatch && payload.turnDispatch.kind !== 'run_turn') {
2704
+ console.error(`[canon-codex] [${payload.conversationId.slice(0, 8)}] Ignoring server-dispatched observe-only message: ${payload.turnDispatch.reason}`);
2705
+ recoveryCheckpointsFor(payload.conversationId).settle(message.id);
2706
+ settleInboundMessageId(message.id, true);
2707
+ await endpoint.setInboundState(`message:${payload.conversationId}:${message.id}`, 'settled', 'observe-only');
2708
+ return;
2709
+ }
2710
+ await enqueueInboundMessage({
2711
+ conversationId: payload.conversationId,
2712
+ message,
2713
+ senderName: message.senderName || message.senderId,
2714
+ isOwner: message.isOwner ?? (ownerId != null && message.senderId === ownerId),
2715
+ behavior: payload.behavior,
2716
+ activeSelfContextId: payload.activeSelfContextId,
2717
+ selfContexts: payload.selfContexts,
2718
+ provenance: payload.provenance,
2719
+ turnDispatch: payload.turnDispatch,
2720
+ replyAuthority: payload.replyAuthority,
2721
+ }).then(() => settleInboundMessageId(message.id, true), (error) => {
2722
+ settleInboundMessageId(message.id, false);
2723
+ console.error(`[canon-codex] [${payload.conversationId.slice(0, 8)}] Failed to queue inbound message:`, error instanceof Error ? error.message : error);
2724
+ throw error;
2725
+ });
2726
+ },
2727
+ onError: (error) => console.error('[canon-codex] Endpoint input deferred:', error), });
2728
+ inbound.start();
2715
2729
  const stream = new CanonStream({
2716
- apiKey,
2730
+ endpoint,
2717
2731
  agentId,
2718
- streamUrl,
2719
2732
  handler: {
2720
2733
  onMessage: (payload) => {
2721
- const message = payload.message;
2722
- if (message.senderId === agentId)
2723
- return;
2724
- if (!claimInboundMessageId(message.id))
2725
- return;
2726
- recoveryCheckpointsFor(payload.conversationId).track(message.id);
2727
- if (payload.turnDispatch && payload.turnDispatch.kind !== 'run_turn') {
2728
- console.error(`[canon-codex] [${payload.conversationId.slice(0, 8)}] Ignoring server-dispatched observe-only message: ${payload.turnDispatch.reason}`);
2729
- recoveryCheckpointsFor(payload.conversationId).settle(message.id);
2730
- settleInboundMessageId(message.id, true);
2731
- return;
2732
- }
2733
- void enqueueInboundMessage({
2734
- conversationId: payload.conversationId,
2735
- message,
2736
- senderName: message.senderName || message.senderId,
2737
- isOwner: message.isOwner ?? (ownerId != null && message.senderId === ownerId),
2738
- behavior: payload.behavior,
2739
- activeSelfContextId: payload.activeSelfContextId,
2740
- selfContexts: payload.selfContexts,
2741
- provenance: payload.provenance,
2742
- turnDispatch: payload.turnDispatch,
2743
- replyAuthority: payload.replyAuthority,
2744
- }).then(() => settleInboundMessageId(message.id, true), (error) => {
2745
- settleInboundMessageId(message.id, false);
2746
- console.error(`[canon-codex] [${payload.conversationId.slice(0, 8)}] Failed to queue inbound message:`, error instanceof Error ? error.message : error);
2747
- });
2734
+ void inbound.receive({ id: `message:${payload.conversationId}:${payload.message.id}`,
2735
+ kind: 'message.created', conversationId: payload.conversationId, durable: true,
2736
+ data: payload });
2748
2737
  },
2749
2738
  onMessageDeleted: (payload) => {
2750
2739
  removeQueuedPrompt(payload.conversationId, payload.messageId);
@@ -2790,7 +2779,6 @@ export async function main() {
2790
2779
  await reconnectRecovery.recoverNow().catch((error) => {
2791
2780
  console.error('[canon-codex] Startup recovery failed:', error instanceof Error ? error.message : error);
2792
2781
  });
2793
- startupRecoveryComplete = true;
2794
2782
  startCodexStreamInBackground(stream, (error) => {
2795
2783
  console.error('[canon-codex] SSE start error:', error instanceof Error ? error.message : error);
2796
2784
  });
@@ -2826,6 +2814,7 @@ export async function main() {
2826
2814
  }
2827
2815
  runtimeRequests.dispose();
2828
2816
  stream.stop();
2817
+ await inbound.close();
2829
2818
  await runtimeHeartbeat.dispose();
2830
2819
  for (const session of [...sessions.values()]) {
2831
2820
  await session.adapter.interrupt().catch(() => { });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/codex-plugin",
3
- "version": "0.30.1",
3
+ "version": "0.32.0",
4
4
  "description": "Canon host integration for Codex CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -31,15 +31,15 @@
31
31
  "prepack": "npm run build"
32
32
  },
33
33
  "dependencies": {
34
- "@canonmsg/agent-sdk": "^10.3.0",
35
- "@canonmsg/agent-tools": "^0.9.1",
36
- "@canonmsg/coding-agent-host": "^0.7.1",
37
- "@canonmsg/core": "^12.4.0",
38
- "@canonmsg/rich-cards": "^0.10.5",
34
+ "@canonmsg/agent-sdk": "^11.0.0",
35
+ "@canonmsg/agent-tools": "^0.11.0",
36
+ "@canonmsg/coding-agent-host": "^0.9.0",
37
+ "@canonmsg/core": "^13.0.0",
38
+ "@canonmsg/rich-cards": "^0.10.6",
39
39
  "ws": "^8.21.3"
40
40
  },
41
41
  "engines": {
42
- "node": ">=18.0.0"
42
+ "node": ">=22.22.3"
43
43
  },
44
44
  "keywords": [
45
45
  "canon",