@canonmsg/claude-code-plugin 0.25.3 → 0.25.5

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "Canon",
3
3
  "description": "Connect Claude Code to Canon — messaging where AI agents are first-class citizens",
4
- "version": "0.25.3",
4
+ "version": "0.25.5",
5
5
  "channels": [
6
6
  {
7
7
  "server": "canon-channel",
package/dist/host.js CHANGED
@@ -35,7 +35,7 @@ import { synthesizeClaudeApprovalDiff } from './approval-diff.js';
35
35
  import { decideClaudeToolPermissionForMode, parseAllowedNonOwnerClaudeTools, } from './tool-policy.js';
36
36
  import { applyClaudeSessionControl, buildClaudeFinalMessageId, claudeModelInfoToOption, confirmClaudeInterrupt, createClaudeInputEnvelope, formatClaudeControlError, isClaudeCustomModelOption, mergeClaudeDiscoveredModelOptions, resolveClaudeTurnResponseRouting, resolveClaudeModelOptions, resetClaudeCompletedTurnState, shouldDeliverClaudeFinal, } from './session-state.js';
37
37
  import { CLAUDE_SUPPORTED_DIALOG_KINDS, buildClaudeAskUserPermissionDenied, buildClaudeAskUserPermissionResult, createClaudeUserDialogCoordinator, parseClaudeAskUserDialog, parseClaudeAskUserToolInput, resolveClaudeUserDialogRequestId, } from './user-dialog.js';
38
- import { collectMissedInboundMessages, STARTUP_RECOVERY_MAX_MESSAGES, STARTUP_RECOVERY_PAGE_SIZE, } from './startup-recovery.js';
38
+ import { collectMissedInboundMessages, createReconnectRecoveryGate, STARTUP_RECOVERY_MAX_MESSAGES, STARTUP_RECOVERY_PAGE_SIZE, } from './startup-recovery.js';
39
39
  function parseRuntimeVisibilityPreset(value) {
40
40
  return value === 'normal' || value === 'minimal' || value === 'full' ? value : undefined;
41
41
  }
@@ -841,25 +841,6 @@ function createSession(conversationId, environment, agentId, client, typingSigna
841
841
  }
842
842
  });
843
843
  }
844
- function waitForReply(type, requestId, responseUserId) {
845
- // Keep session alive while waiting for user input
846
- session.lastActivity = Date.now();
847
- return new Promise((resolve, reject) => {
848
- const timer = setTimeout(() => {
849
- session.pendingReply = null;
850
- reject(new Error('Timed out waiting for user reply'));
851
- }, REPLY_TIMEOUT_MS);
852
- session.pendingReply = {
853
- type,
854
- requestId,
855
- responseUserId,
856
- resolve: (metadata) => {
857
- clearTimeout(timer);
858
- resolve(metadata);
859
- },
860
- };
861
- });
862
- }
863
844
  function buildArtifactMediaMetadata() {
864
845
  return {
865
846
  ...(session.currentTurnId ? { turnId: session.currentTurnId } : {}),
@@ -939,24 +920,24 @@ function createSession(conversationId, environment, agentId, client, typingSigna
939
920
  const planBody = typeof input.plan === 'string' && input.plan.trim()
940
921
  ? input.plan.trim()
941
922
  : undefined;
942
- // Route plan-CREATE through the unified spine (`/runtime-plan/request`):
943
- // seeds the server-owned pending node/attention/state and authors the
944
- // `plan_approval` card. Resolution is UNCHANGED — the gate still blocks
945
- // on the server-authored `plan_approval_reply` below.
946
- await client.createRuntimePlanRequest({
947
- conversationId,
948
- planId,
923
+ const reply = await config.runtimeInputManager.request('plan', conversationId, {
949
924
  title: 'Plan ready for review.',
950
925
  summary: 'Claude has prepared a plan and is asking permission to continue.',
951
926
  ...(planBody ? { body: planBody } : {}),
952
927
  responseUserId: turnResponseRouting.responseUserId,
953
928
  ...(allowedPrompts.length > 0 ? { allowedPrompts } : {}),
954
929
  ...(session.currentTurnId ? { turnId: session.currentTurnId } : {}),
930
+ }, {
931
+ requestId: planId,
955
932
  expiresAt: Date.now() + REPLY_TIMEOUT_MS,
933
+ responderPolicy: 'infer',
934
+ signal: options.signal,
935
+ onCreated: () => {
936
+ session.lastActivity = Date.now();
937
+ console.error(`[canon-host] [${conversationId.slice(0, 8)}] Plan approval sent (${planId.slice(0, 8)})`);
938
+ },
956
939
  });
957
- console.error(`[canon-host] [${conversationId.slice(0, 8)}] Plan approval sent (${planId.slice(0, 8)})`);
958
- const reply = await waitForReply('plan_approval_reply', planId, turnResponseRouting.responseUserId);
959
- if (reply.decision === 'approve') {
940
+ if (reply.status === 'approve') {
960
941
  // Honor opt-in selective grants: when the card sent an explicit
961
942
  // granted subset, pre-approve those tools as session rules so they
962
943
  // do not re-prompt. Owner-turn only; absent grants preserve the
@@ -1000,13 +981,21 @@ function createSession(conversationId, environment, agentId, client, typingSigna
1000
981
  }
1001
982
  return { behavior: 'allow' };
1002
983
  }
1003
- if (reply.decision === 'reject') {
984
+ if (reply.status === 'reject') {
1004
985
  return {
1005
986
  behavior: 'deny',
1006
987
  message: reply.feedback?.trim()
1007
988
  || 'Plan declined — keep planning and wait for guidance before proceeding.',
1008
989
  };
1009
990
  }
991
+ if (reply.status === 'cancelled' || reply.status === 'timeout') {
992
+ return {
993
+ behavior: 'deny',
994
+ message: reply.status === 'timeout'
995
+ ? 'No reply received (timed out)'
996
+ : 'The plan review was cancelled.',
997
+ };
998
+ }
1010
999
  return {
1011
1000
  behavior: 'deny',
1012
1001
  message: reply.feedback?.trim() || 'User requested changes to the plan.',
@@ -1093,7 +1082,6 @@ function createSession(conversationId, environment, agentId, client, typingSigna
1093
1082
  controlInterruptPending: false,
1094
1083
  closed: false,
1095
1084
  sdkSessionId: resumeSessionId || null,
1096
- pendingReply: null,
1097
1085
  writeState: () => writeState(),
1098
1086
  writeTurn: () => writeTurn(),
1099
1087
  clearStreaming: () => clearStreaming(),
@@ -2559,28 +2547,14 @@ export async function main() {
2559
2547
  return true;
2560
2548
  }
2561
2549
  }
2562
- if (message.metadata?.type !== 'plan_approval_reply')
2563
- return false;
2564
- const existingSession = sessions.get(conversationId);
2565
- const pendingReply = existingSession?.pendingReply;
2566
- const replyRequestId = message.metadata.planId;
2567
- if (!pendingReply) {
2568
- console.error(`[canon-host] [${conversationId.slice(0, 8)}] Ignoring stale ${message.metadata.type} with no pending request`);
2569
- return true;
2570
- }
2571
- if (message.senderId !== pendingReply.responseUserId) {
2572
- console.error(`[canon-host] [${conversationId.slice(0, 8)}] Ignoring ${message.metadata.type} from unexpected responder ${message.senderId}`);
2573
- return true;
2574
- }
2575
- if (pendingReply.type !== message.metadata.type || pendingReply.requestId !== replyRequestId) {
2576
- console.error(`[canon-host] [${conversationId.slice(0, 8)}] Ignoring mismatched ${message.metadata.type} (${String(replyRequestId ?? 'missing-id')})`);
2577
- return true;
2550
+ const consumed = runtimeInputManager.handleMessage(conversationId, {
2551
+ senderId: message.senderId,
2552
+ metadata: message.metadata,
2553
+ });
2554
+ if (consumed) {
2555
+ console.error(`[canon-host] [${conversationId.slice(0, 8)}] Runtime response receipt woke canonical polling`);
2578
2556
  }
2579
- existingSession.pendingReply = null;
2580
- existingSession.lastActivity = Date.now();
2581
- pendingReply.resolve(message.metadata);
2582
- console.error(`[canon-host] [${conversationId.slice(0, 8)}] Reply received (${message.metadata.type}:${pendingReply.requestId.slice(0, 8)})`);
2583
- return true;
2557
+ return consumed;
2584
2558
  }
2585
2559
  function persistedConversationCursors(conversationId) {
2586
2560
  const canonicalCursor = loadRuntimeSessionState(runtimeId, {
@@ -2663,6 +2637,7 @@ export async function main() {
2663
2637
  });
2664
2638
  return recoveryInFlight;
2665
2639
  }
2640
+ const shouldRecoverAfterStreamConnect = createReconnectRecoveryGate();
2666
2641
  await recoverMissedInboundMessages();
2667
2642
  // ── Connect SSE stream for inbound Canon messages ──
2668
2643
  const stream = new CanonStream({
@@ -2799,7 +2774,9 @@ export async function main() {
2799
2774
  onConnected: () => {
2800
2775
  streamConnected = true;
2801
2776
  void publishRuntimeHeartbeat();
2802
- void recoverMissedInboundMessages();
2777
+ if (shouldRecoverAfterStreamConnect()) {
2778
+ void recoverMissedInboundMessages();
2779
+ }
2803
2780
  console.error('[canon-host] SSE connected');
2804
2781
  },
2805
2782
  onDisconnected: () => {
@@ -40,6 +40,11 @@ export interface StartupRecoveryResult<TPage extends StartupRecoveryPage> {
40
40
  /** First page fetched — reusable as hydration context for recovered turns. */
41
41
  newestPage: TPage;
42
42
  }
43
+ /**
44
+ * Startup recovery runs before the stream starts. The first SSE connection
45
+ * therefore needs no second pass, while later reconnects still need catch-up.
46
+ */
47
+ export declare function createReconnectRecoveryGate(): () => boolean;
43
48
  export declare function collectMissedInboundMessages<TPage extends StartupRecoveryPage>(input: {
44
49
  fetchPage: (before?: string) => Promise<TPage>;
45
50
  cursor: string | null | undefined;
@@ -11,6 +11,18 @@
11
11
  */
12
12
  export const STARTUP_RECOVERY_PAGE_SIZE = 25;
13
13
  export const STARTUP_RECOVERY_MAX_MESSAGES = 500;
14
+ /**
15
+ * Startup recovery runs before the stream starts. The first SSE connection
16
+ * therefore needs no second pass, while later reconnects still need catch-up.
17
+ */
18
+ export function createReconnectRecoveryGate() {
19
+ let hasConnected = false;
20
+ return () => {
21
+ const shouldRecover = hasConnected;
22
+ hasConnected = true;
23
+ return shouldRecover;
24
+ };
25
+ }
14
26
  export async function collectMissedInboundMessages(input) {
15
27
  const maxMessages = input.maxMessages ?? STARTUP_RECOVERY_MAX_MESSAGES;
16
28
  const newestPage = await input.fetchPage();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/claude-code-plugin",
3
- "version": "0.25.3",
3
+ "version": "0.25.5",
4
4
  "description": "Canon channel plugin for Claude Code — messaging where AI agents are first-class citizens",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -33,7 +33,7 @@
33
33
  "@anthropic-ai/claude-agent-sdk": "0.3.204",
34
34
  "@canonmsg/agent-sdk": "^5.1.1",
35
35
  "@canonmsg/coding-agent-host": "^0.2.2",
36
- "@canonmsg/core": "^4.2.2",
36
+ "@canonmsg/core": "^4.2.3",
37
37
  "@modelcontextprotocol/sdk": "^1.29.0"
38
38
  },
39
39
  "engines": {