@canonmsg/claude-code-plugin 0.36.1 → 0.36.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.
@@ -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.36.1",
4
+ "version": "0.36.2",
5
5
  "channels": [
6
6
  {
7
7
  "server": "canon-channel",
package/README.md CHANGED
@@ -4,7 +4,7 @@ Connect Claude Code to [Canon](https://canonmail.com/agents) — a messaging app
4
4
 
5
5
  ## Quick start
6
6
 
7
- The package includes a compatible Claude Code runtime. If `claude` is installed on `PATH`, or selected with `CANON_CLAUDE_CLI_PATH`, use Claude Code 2.1.220 or newer — the model picker lists whatever the CLI reports, so an older binary hides newer model families. Either source below that minimum is logged and skipped in favour of the bundled runtime, so a stale install costs model options rather than taking the host down.
7
+ The package includes a compatible Claude Code runtime. If `claude` is installed on `PATH`, or selected with `CANON_CLAUDE_CLI_PATH`, use Claude Code 2.1.278 or newer. The bundled Claude Agent SDK is pinned to 0.3.278; Canon relies on its explicit input/result attribution for merged messages and native errors. An external binary below that minimum is logged and skipped in favour of the bundled runtime.
8
8
 
9
9
  ```bash
10
10
  # Install
package/dist/host.js CHANGED
@@ -35,7 +35,7 @@ import { runCli } from '@canonmsg/core';
35
35
  import { CANON_VERB_MCP_SERVER_NAME, createCanonVerbMcpServer } from '@canonmsg/agent-tools';
36
36
  import { synthesizeClaudeApprovalDiff } from './approval-diff.js';
37
37
  import { decideClaudeToolPermissionForMode, parseAllowedNonOwnerClaudeTools, } from './tool-policy.js';
38
- import { boundClaudeFinalMetadata, buildClaudeFinalChunkingOptions, buildClaudeFinalMessageId, buildClaudeFinalTurnMetadata, buildClaudePendingFinalDelivery, buildClaudeTurnFailureNotice, buildTruncatedFinalText, buildUndeliverableFinalNotice, canDrainClaudeQueuedInput, beginClaudeAssistantResponse, claimClaudeTextSegmentId, claudeFinalWillChunk, classifyFinalDeliveryFailure, claudeInputOwnsTurnSlot, createClaudeTurnActivityState, decideClaudeControlSignalAction, decideClaudeInboundDispatch, dispatchClaudeInput, endClaudeAssistantResponse, isClaudeTurnSlotReserved, readClaudeFinalDeliveryResume, releaseClaudeTurnSlot, runClaudeExhaustedFinalDelivery, shouldApplyClaudeEchoedSessionState, shouldReleaseClaudeTurnSlot, isClaudeMainTurnMessage, openClaudeTurn, planClaudeAssistantTrail, planClaudeStreamingWrite, planClaudeToolCallStart, planClaudeToolProgress, planClaudeToolResults, claudeFinalTurnTrail, publishedClaudeTurnState, rememberDispatchedClaudeInput, resetClaudeTurnActivityState, shouldOpenClaudeTurnOnRunning, shouldStopTypingDotsOnStreamedText, isOpenClaudeTurnState, takeClaudeResultOwner, takeClaudeToolBlockIdByIndex, claudeModelInfoToOption, claudeOriginForCanonSender, composeClaudeTurnFinal, describeUndeliveredClaudeFinal, confirmClaudeInterrupt, createClaudeInputEnvelope, deriveClaudeSupplementalModelProbes, formatClaudeCliVersion, formatClaudeControlError, isClaudeCustomModelOption, isSupportedClaudeCliVersion, mergeClaudeDiscoveredModelOptions, parseClaudeCliVersion, resolveClaudeTurnResponseRouting, resolveClaudeModelOptions, resetClaudeCompletedTurnState, shouldDeliverClaudeFinal, shouldRouteClaudeTurnArtifacts, MINIMUM_CLAUDE_CLI_VERSION, } from './session-state.js';
38
+ import { boundClaudeFinalMetadata, buildClaudeFinalChunkingOptions, buildClaudeFinalMessageId, buildClaudeFinalTurnMetadata, buildClaudePendingFinalDelivery, buildClaudeTurnFailureNotice, buildTruncatedFinalText, buildUndeliverableFinalNotice, canDrainClaudeQueuedInput, beginClaudeAssistantResponse, claimClaudeTextSegmentId, claudeFinalWillChunk, classifyFinalDeliveryFailure, claudeInputOwnsTurnSlot, createClaudeTurnActivityState, decideClaudeControlSignalAction, decideClaudeInboundDispatch, dispatchClaudeInput, endClaudeAssistantResponse, isClaudeTurnSlotReserved, readClaudeFinalDeliveryResume, releaseClaudeTurnSlot, runClaudeExhaustedFinalDelivery, shouldApplyClaudeEchoedSessionState, shouldReleaseClaudeTurnSlot, isClaudeMainTurnMessage, openClaudeTurn, planClaudeAssistantTrail, planClaudeStreamingWrite, planClaudeToolCallStart, planClaudeToolProgress, planClaudeToolResults, claudeFinalTurnTrail, publishedClaudeTurnState, rememberDispatchedClaudeInput, resetClaudeTurnActivityState, shouldOpenClaudeTurnOnRunning, shouldStopTypingDotsOnStreamedText, isOpenClaudeTurnState, takeClaudeResultOwner, claudeCompletedSourceMessageIds, shouldFailClaudeSessionForUnattributedResult, takeClaudeToolBlockIdByIndex, claudeModelInfoToOption, claudeOriginForCanonSender, composeClaudeTurnFinal, describeUndeliveredClaudeFinal, confirmClaudeInterrupt, createClaudeInputEnvelope, deriveClaudeSupplementalModelProbes, formatClaudeCliVersion, formatClaudeControlError, isClaudeCustomModelOption, isSupportedClaudeCliVersion, mergeClaudeDiscoveredModelOptions, parseClaudeCliVersion, resolveClaudeTurnResponseRouting, resolveClaudeModelOptions, resetClaudeCompletedTurnState, shouldDeliverClaudeFinal, shouldRouteClaudeTurnArtifacts, MINIMUM_CLAUDE_CLI_VERSION, } from './session-state.js';
39
39
  import { CLAUDE_SUPPORTED_DIALOG_KINDS, buildClaudeAskUserPermissionDenied, buildClaudeAskUserPermissionResult, createClaudeUserDialogCoordinator, parseClaudeAskUserDialog, parseClaudeAskUserToolInput, resolveClaudeUserDialogRequestId, } from './user-dialog.js';
40
40
  import { isProactiveCommunicationEnabled } from './communication-policy.js';
41
41
  function parseRuntimeVisibilityPreset(value) {
@@ -1059,9 +1059,9 @@ function createSession(conversationId, environment, agentId, client, typingSigna
1059
1059
  return streamingOutput.clear().catch(() => { });
1060
1060
  }
1061
1061
  function markInputCompleted(input) {
1062
- if (input?.kind !== 'canon' || !input.sourceMessageId)
1063
- return;
1064
- config.onInputCompleted?.(input.sourceMessageId);
1062
+ for (const sourceMessageId of claudeCompletedSourceMessageIds(input)) {
1063
+ config.onInputCompleted?.(sourceMessageId);
1064
+ }
1065
1065
  }
1066
1066
  /**
1067
1067
  * The turn trail to attach to the final send. Empty on a quiet turn — see
@@ -1120,6 +1120,8 @@ function createSession(conversationId, environment, agentId, client, typingSigna
1120
1120
  drainPendingInput();
1121
1121
  }
1122
1122
  function scheduleFinalHandoffReset() {
1123
+ if (session.closed)
1124
+ return;
1123
1125
  clearIdleResetTimer();
1124
1126
  session.idleResetTimer = setTimeout(() => {
1125
1127
  session.idleResetTimer = null;
@@ -1177,8 +1179,10 @@ function createSession(conversationId, environment, agentId, client, typingSigna
1177
1179
  session.finalizedTurnKeys.add(turn.turnKey);
1178
1180
  markInputCompleted(turn);
1179
1181
  releaseTurnSlotFor(turn);
1180
- session.pendingFinalDelivery = null;
1181
- clearFinalDeliveryTimer();
1182
+ if (session.pendingFinalDelivery?.turnKey === turn.turnKey) {
1183
+ session.pendingFinalDelivery = null;
1184
+ clearFinalDeliveryTimer();
1185
+ }
1182
1186
  writeState();
1183
1187
  }
1184
1188
  /**
@@ -1461,6 +1465,8 @@ function createSession(conversationId, environment, agentId, client, typingSigna
1461
1465
  }, FINAL_DELIVERY_RETRY_MS);
1462
1466
  }
1463
1467
  function writeState() {
1468
+ if (session.closed)
1469
+ return;
1464
1470
  runtimeState.writeSessionState(conversationId, {
1465
1471
  contextUsage: session.state.contextUsage,
1466
1472
  hostMode: true,
@@ -1487,6 +1493,8 @@ function createSession(conversationId, environment, agentId, client, typingSigna
1487
1493
  writeState();
1488
1494
  }
1489
1495
  function writeTurn() {
1496
+ if (session.closed)
1497
+ return;
1490
1498
  const isOpenTurn = isOpenClaudeTurnState(session.turnState);
1491
1499
  if (isOpenTurn) {
1492
1500
  session.currentTurnUpdatedAt = Date.now();
@@ -1780,6 +1788,29 @@ function createSession(conversationId, environment, agentId, client, typingSigna
1780
1788
  applyTrailCommands(planClaudeToolResults(session.turnActivity, content));
1781
1789
  }
1782
1790
  // ── Process SDK output messages ──
1791
+ function finishSession() {
1792
+ if (session.closed)
1793
+ return;
1794
+ session.closed = true;
1795
+ inputDone = true;
1796
+ clearIdleResetTimer();
1797
+ clearFinalDeliveryTimer();
1798
+ stopVisibleWorkSignal();
1799
+ try {
1800
+ q.close();
1801
+ }
1802
+ catch { }
1803
+ // A native failure cannot prove whether dispatched work ran. Release local
1804
+ // in-flight claims without completing endpoint receipts or replaying work.
1805
+ const unfinishedIds = new Set([
1806
+ session.activeInput, session.dispatchingInput,
1807
+ ...session.pendingInputs, ...dispatchedInputs.values(),
1808
+ ].flatMap(claudeCompletedSourceMessageIds));
1809
+ for (const sourceMessageId of unfinishedIds)
1810
+ config.onInputDeferred?.(sourceMessageId);
1811
+ console.error(`[canon-host] [${conversationId.slice(0, 8)}] Session ended`);
1812
+ onSessionEnd(conversationId, session.sdkSessionId);
1813
+ }
1783
1814
  const processMessages = async () => {
1784
1815
  try {
1785
1816
  for await (const msg of q) {
@@ -1988,10 +2019,17 @@ function createSession(conversationId, environment, agentId, client, typingSigna
1988
2019
  const resultSessionId = msg.session_id;
1989
2020
  if (resultSessionId)
1990
2021
  session.sdkSessionId = resultSessionId;
1991
- // Match the result to the envelope that produced it. Falling back to
1992
- // the slot only when the SDK reports no uuid keeps older runtimes
1993
- // working, at their existing accuracy.
1994
- const completedInput = takeClaudeResultOwner(dispatchedInputs, msg.user_message_uuid) ?? session.activeInput;
2022
+ // A result may acknowledge several coalesced inputs, but never a
2023
+ // queued input absent from its UUID list. Native background/resume
2024
+ // results with no known UUID do not belong to the active Canon turn.
2025
+ const completedInput = takeClaudeResultOwner(dispatchedInputs, msg.user_message_uuid, msg.user_message_uuids);
2026
+ if (!completedInput) {
2027
+ if (shouldFailClaudeSessionForUnattributedResult(msg, Boolean(session.activeInput || session.dispatchingInput || session.pendingInputs.length))) {
2028
+ throw new Error('Claude returned an unattributed result; native session closed with unfinished inputs preserved.');
2029
+ }
2030
+ console.error(`[canon-host] [${conversationId.slice(0, 8)}] Ignoring result without a known input UUID`);
2031
+ break;
2032
+ }
1995
2033
  console.error(`[canon-host] [${conversationId.slice(0, 8)}] Turn complete (${msg.subtype})`);
1996
2034
  const resultText = typeof msg.result === 'string'
1997
2035
  ? msg.result.trim()
@@ -2014,7 +2052,8 @@ function createSession(conversationId, environment, agentId, client, typingSigna
2014
2052
  // the artifacts still POST first, ahead of the reply they belong to.
2015
2053
  const turnFinal = composeClaudeTurnFinal({
2016
2054
  resultText,
2017
- streamedText: session.pendingFinalText,
2055
+ streamedText: completedInput.turnKey === session.activeInput?.turnKey
2056
+ ? session.pendingFinalText : null,
2018
2057
  failureNotice,
2019
2058
  silenced,
2020
2059
  });
@@ -2077,6 +2116,14 @@ function createSession(conversationId, environment, agentId, client, typingSigna
2077
2116
  if (!finalText || !shouldDeliverFinal) {
2078
2117
  markInputCompleted(completedInput);
2079
2118
  }
2119
+ // Sending the final and fetching usage both await. A newer turn
2120
+ // may now own the live text, pending delivery and handoff timer.
2121
+ if (!shouldReleaseClaudeTurnSlot({
2122
+ dispatching: session.dispatchingInput !== null,
2123
+ activeTurnKey: session.activeInput?.turnKey ?? null,
2124
+ completedTurnKey: completedInput.turnKey,
2125
+ }))
2126
+ break;
2080
2127
  if (completedInput?.turnKey === session.activeInput?.turnKey) {
2081
2128
  session.activeInput = null;
2082
2129
  }
@@ -2118,16 +2165,14 @@ function createSession(conversationId, environment, agentId, client, typingSigna
2118
2165
  catch (err) {
2119
2166
  console.error(`[canon-host] [${conversationId.slice(0, 8)}] Query stream error:`, err);
2120
2167
  }
2121
- // Only call onSessionEnd if not already closed (avoids double cleanup from closeSession)
2122
- if (!session.closed) {
2123
- console.error(`[canon-host] [${conversationId.slice(0, 8)}] Session ended`);
2124
- onSessionEnd(conversationId, session.sdkSessionId);
2125
- }
2168
+ finishSession();
2126
2169
  };
2127
2170
  // Start processing loop and initialize session
2128
2171
  const init = async () => {
2129
2172
  processMessages().catch((err) => console.error(`[canon-host] [${conversationId.slice(0, 8)}] processMessages crashed:`, err));
2130
2173
  await q.initializationResult();
2174
+ if (session.closed)
2175
+ return;
2131
2176
  if (config.model) {
2132
2177
  try {
2133
2178
  await q.setModel(config.model);
@@ -2160,6 +2205,8 @@ function createSession(conversationId, environment, agentId, client, typingSigna
2160
2205
  const models = await q.supportedModels();
2161
2206
  const discoveredOptions = toModelOptions(models);
2162
2207
  const supplemental = await detectSupplementalRuntimeModels(cwd, deriveClaudeSupplementalModelProbes(discoveredOptions));
2208
+ if (session.closed)
2209
+ return;
2163
2210
  const modelList = resolveClaudeModelOptions({
2164
2211
  discovered: mergeClaudeDiscoveredModelOptions(discoveredOptions, ...supplemental),
2165
2212
  selectedModel: session.state.model,
@@ -2194,10 +2241,9 @@ function createSession(conversationId, environment, agentId, client, typingSigna
2194
2241
  };
2195
2242
  init().catch((err) => {
2196
2243
  console.error(`[canon-host] [${conversationId.slice(0, 8)}] Init failed:`, err);
2197
- if (resumeSessionId) {
2244
+ if (resumeSessionId)
2198
2245
  session.sdkSessionId = null;
2199
- onSessionEnd(conversationId, null);
2200
- }
2246
+ finishSession();
2201
2247
  });
2202
2248
  return session;
2203
2249
  }
@@ -1,4 +1,4 @@
1
- import type { SDKMessageOrigin, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
1
+ import type { SDKMessageOrigin, SDKResultMessage, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
2
2
  import type { DeliveryIntent, ModelOption, RuntimeStreamingPayload, TurnLifecycleState, TurnOutputBlock, TurnOutputSnapshot, TurnVerbosity } from '@canonmsg/core';
3
3
  import type { AgentReplyAuthorityV1 } from '@canonmsg/backend-contracts';
4
4
  import type { TurnArtifactRoutingDecision, TurnArtifactSnapshot } from '@canonmsg/coding-agent-host';
@@ -7,8 +7,10 @@ export type ClaudeArtifactRoutingMode = 'workspace-generated' | 'disabled';
7
7
  export interface ClaudeInputEnvelope {
8
8
  commandContext?: string;
9
9
  kind: ClaudeInputKind;
10
- /** Correlates this envelope with its turn result's `user_message_uuid`. */
10
+ /** Correlates this envelope with its turn result's user-message UUIDs. */
11
11
  messageUuid: string;
12
+ /** Other Canon inputs acknowledged by the same result; settle with its final. */
13
+ resultSourceMessageIds?: string[];
12
14
  msg: SDKUserMessage;
13
15
  intent: DeliveryIntent;
14
16
  sourceMessageId: string | null;
@@ -150,15 +152,24 @@ export declare function shouldRouteClaudeTurnArtifacts(input: {
150
152
  }): TurnArtifactRoutingDecision;
151
153
  export declare function rememberDispatchedClaudeInput(dispatched: Map<string, ClaudeInputEnvelope>, input: ClaudeInputEnvelope): void;
152
154
  /**
153
- * The envelope a turn result belongs to, matched by `user_message_uuid`.
154
- *
155
- * The SDK coalesces queued inputs — three rapid messages can produce two turns —
156
- * and reports the LAST message of a coalesced batch, which is the correct owner
157
- * of the reply. Entries up to and including the match are consumed, so messages
158
- * folded into that batch do not linger. Returns null when the result carries no
159
- * usable uuid, leaving the caller to fall back.
155
+ * Match only the inputs the SDK explicitly acknowledges. Its plural list is in
156
+ * consumption order and can include messages folded in after the first reply.
157
+ * The last known Canon input owns the final; internal seeds cannot displace it.
158
+ * A singular UUID acknowledges only itself, never everything earlier in our map.
159
+ * Missing/unknown attribution must not fall back to an unrelated active turn.
160
+ */
161
+ export declare function takeClaudeResultOwner(dispatched: Map<string, ClaudeInputEnvelope>, userMessageUuid: unknown, userMessageUuids?: unknown): ClaudeInputEnvelope | null;
162
+ /** Keep coalesced inputs pending until their shared final is delivered or retired. */
163
+ export declare function claudeCompletedSourceMessageIds(input: ClaudeInputEnvelope | null | undefined): string[];
164
+ /**
165
+ * A zeroed/session error may have no input UUID. Treat it as a session failure,
166
+ * never as successful completion of whichever Canon input happens to be active.
167
+ * An unattributed success while foreground work is outstanding is ambiguous too:
168
+ * waiting for an idle echo would wedge that input, because its slot rejects idle
169
+ * until completion. Close the native session and preserve the unsettled input.
170
+ * Explicit foreign/replayed UUIDs remain unrelated results and are ignored.
160
171
  */
161
- export declare function takeClaudeResultOwner(dispatched: Map<string, ClaudeInputEnvelope>, userMessageUuid: unknown): ClaudeInputEnvelope | null;
172
+ export declare function shouldFailClaudeSessionForUnattributedResult(result: Pick<SDKResultMessage, 'subtype' | 'is_error' | 'user_message_uuid' | 'user_message_uuids'>, hasForegroundWork: boolean): boolean;
162
173
  /**
163
174
  * Whether an envelope owns the session's active-turn slot. Only Canon turns do;
164
175
  * any future internal envelope must remain invisible to user-facing turn state.
@@ -99,28 +99,54 @@ export function rememberDispatchedClaudeInput(dispatched, input) {
99
99
  }
100
100
  }
101
101
  /**
102
- * The envelope a turn result belongs to, matched by `user_message_uuid`.
103
- *
104
- * The SDK coalesces queued inputs — three rapid messages can produce two turns —
105
- * and reports the LAST message of a coalesced batch, which is the correct owner
106
- * of the reply. Entries up to and including the match are consumed, so messages
107
- * folded into that batch do not linger. Returns null when the result carries no
108
- * usable uuid, leaving the caller to fall back.
102
+ * Match only the inputs the SDK explicitly acknowledges. Its plural list is in
103
+ * consumption order and can include messages folded in after the first reply.
104
+ * The last known Canon input owns the final; internal seeds cannot displace it.
105
+ * A singular UUID acknowledges only itself, never everything earlier in our map.
106
+ * Missing/unknown attribution must not fall back to an unrelated active turn.
109
107
  */
110
- export function takeClaudeResultOwner(dispatched, userMessageUuid) {
111
- if (typeof userMessageUuid !== 'string' || !dispatched.has(userMessageUuid)) {
112
- return null;
113
- }
114
- let owner = null;
115
- for (const [key, envelope] of dispatched) {
116
- dispatched.delete(key);
117
- if (key === userMessageUuid) {
118
- owner = envelope;
119
- break;
120
- }
108
+ export function takeClaudeResultOwner(dispatched, userMessageUuid, userMessageUuids) {
109
+ const uuids = Array.isArray(userMessageUuids)
110
+ ? userMessageUuids.filter((uuid) => typeof uuid === 'string' && uuid.length > 0)
111
+ : [];
112
+ if (typeof userMessageUuid === 'string' && !uuids.includes(userMessageUuid)) {
113
+ uuids.push(userMessageUuid);
121
114
  }
115
+ const inputs = [...new Set(uuids)].flatMap((uuid) => {
116
+ const input = dispatched.get(uuid);
117
+ return input ? [input] : [];
118
+ });
119
+ const owner = [...inputs].reverse().find((input) => input.kind === 'canon') ?? inputs.at(-1) ?? null;
120
+ if (!owner)
121
+ return null;
122
+ owner.resultSourceMessageIds = [...new Set(inputs.flatMap(claudeCompletedSourceMessageIds))];
123
+ for (const input of inputs)
124
+ dispatched.delete(input.messageUuid);
122
125
  return owner;
123
126
  }
127
+ /** Keep coalesced inputs pending until their shared final is delivered or retired. */
128
+ export function claudeCompletedSourceMessageIds(input) {
129
+ if (input?.kind !== 'canon')
130
+ return [];
131
+ return [...new Set([
132
+ ...(input.sourceMessageId ? [input.sourceMessageId] : []),
133
+ ...(input.resultSourceMessageIds ?? []),
134
+ ])];
135
+ }
136
+ /**
137
+ * A zeroed/session error may have no input UUID. Treat it as a session failure,
138
+ * never as successful completion of whichever Canon input happens to be active.
139
+ * An unattributed success while foreground work is outstanding is ambiguous too:
140
+ * waiting for an idle echo would wedge that input, because its slot rejects idle
141
+ * until completion. Close the native session and preserve the unsettled input.
142
+ * Explicit foreign/replayed UUIDs remain unrelated results and are ignored.
143
+ */
144
+ export function shouldFailClaudeSessionForUnattributedResult(result, hasForegroundWork) {
145
+ const hasUuid = (typeof result.user_message_uuid === 'string' && result.user_message_uuid.length > 0)
146
+ || (Array.isArray(result.user_message_uuids)
147
+ && result.user_message_uuids.some((uuid) => typeof uuid === 'string' && uuid.length > 0));
148
+ return !hasUuid && (hasForegroundWork || result.is_error || result.subtype !== 'success');
149
+ }
124
150
  /**
125
151
  * Whether an envelope owns the session's active-turn slot. Only Canon turns do;
126
152
  * any future internal envelope must remain invisible to user-facing turn state.
@@ -1188,12 +1214,9 @@ export async function runClaudeExhaustedFinalDelivery(session, pending, deps) {
1188
1214
  }
1189
1215
  deps.retireTurn();
1190
1216
  }
1191
- // Model options are discovery-driven: the host publishes whatever the Claude
1192
- // Code CLI it spawns reports. A CLI older than this predates whole model
1193
- // families (2.1.220 is the first to know Opus 5), so an old binary silently
1194
- // shortens the picker instead of failing. Keep this at or above the CLI the
1195
- // pinned SDK bundles, so the bundled fallback always satisfies it.
1196
- export const MINIMUM_CLAUDE_CLI_VERSION = [2, 1, 220];
1217
+ // External CLIs must support the same input/result attribution contract as the
1218
+ // pinned SDK. Older binaries fall back to the bundled, audited runtime.
1219
+ export const MINIMUM_CLAUDE_CLI_VERSION = [2, 1, 278];
1197
1220
  export function parseClaudeCliVersion(output) {
1198
1221
  const match = output.trim().match(/^(\d+)\.(\d+)\.(\d+)/);
1199
1222
  return match ? match.slice(1, 4).map(Number) : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/claude-code-plugin",
3
- "version": "0.36.1",
3
+ "version": "0.36.2",
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",
@@ -30,7 +30,7 @@
30
30
  "test": "vitest run"
31
31
  },
32
32
  "dependencies": {
33
- "@anthropic-ai/claude-agent-sdk": "0.3.228",
33
+ "@anthropic-ai/claude-agent-sdk": "0.3.278",
34
34
  "@canonmsg/agent-sdk": "^11.0.1",
35
35
  "@canonmsg/agent-tools": "^0.11.0",
36
36
  "@canonmsg/coding-agent-host": "^0.9.0",