@canonmsg/claude-code-plugin 0.31.2 → 0.31.4

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.31.2",
4
+ "version": "0.31.4",
5
5
  "channels": [
6
6
  {
7
7
  "server": "canon-channel",
package/README.md CHANGED
@@ -63,6 +63,8 @@ canon-claude --cwd ~/dev --workspace-root ~/dev
63
63
 
64
64
  `--cwd` is the default workspace. Each `--workspace-root` value is an approved local root; the host discovers immediate child projects with common markers such as `.git`, `package.json`, `pyproject.toml`, `Cargo.toml`, or `go.mod` and publishes them as selectable projects during session creation. Use repeated `--workspace /path/to/project` entries to advertise specific projects outside those roots. Worktree mode creates a best-effort per-conversation git worktree under `~/.canon/conversation-worktrees`; shared-project mode runs directly in the selected directory.
65
65
 
66
+ Inbound Canon images are materialized with the Agent SDK's bounded 10 MiB default. The host sends supported images to the direct Anthropic transport as native base64 blocks only when each raw file is at most 7 MiB and the exact encoded content array plus prompt text fit the aggregate request budget with reserved framing headroom. Seven MiB is derived specifically from the direct transport's 10 MB encoded-image limit; it is not a portable default for Bedrock or Vertex. Materialized images omitted from native blocks remain available by local path. Images over the 10 MiB automatic-download limit instead receive a validated HTTPS link in the Claude prompt; materialized images never receive a duplicate link.
67
+
66
68
  Current Canon truth for Claude host mode:
67
69
 
68
70
  - model is live-editable
@@ -87,6 +89,11 @@ canon-claude --cwd /path/to/project --turn-verbosity quiet
87
89
  | `verbose` | Live streaming text plus the margin activity rows on the final, everywhere |
88
90
  | `quiet` | The thinking indicator and the answer, nothing in between, everywhere |
89
91
 
92
+ In verbose mode, authored text remains continual across tool use: text before a
93
+ tool stays as its own speech bubble, the tool appears in the activity margin,
94
+ and text after the result resumes in a new bubble. These are ephemeral streaming
95
+ updates; only the final durable `turn_complete` message is notification-eligible.
96
+
90
97
  Quiet drops the live `/streaming` narration and the final's `turnTrail` activity rows. It does **not** drop the thinking indicator (which now stays up for the turn's whole working phase rather than handing over to a bubble that never appears; while the turn is parked on an approval the clients suppress an agent's dots and the header line carries the state), the turn state, the answer — including every part of a long chunked one — failure notices, generated files, or approval and question cards and their receipts.
91
98
 
92
99
  This is an agent-developer setting. Canon never changes it, and it is deliberately not part of the per-conversation session config a user can edit. `canon-necromance` replays a stored launch command verbatim, so add the flag at registration time if you want a non-default value.
@@ -0,0 +1,22 @@
1
+ import type { SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
2
+ import { type AnthropicImageBudgetOptions, type MaterializedCanonAttachment } from '@canonmsg/agent-sdk';
3
+ import { renderCanonHostInboundContent, type CanonReplyContext } from '@canonmsg/core';
4
+ type ClaudeRenderableInboundMessage = Parameters<typeof renderCanonHostInboundContent>[0];
5
+ /**
6
+ * Claude-only rendering policy for Canon media. Core deliberately omits all
7
+ * URLs globally. This wrapper restores validated HTTPS links only for images
8
+ * that the bounded runtime did not materialize, while materialized images keep
9
+ * their local-path placeholder without a duplicate link.
10
+ */
11
+ export declare function renderClaudeInboundContent(message: ClaudeRenderableInboundMessage, materialized?: ReadonlyArray<MaterializedCanonAttachment>): string;
12
+ export declare function withClaudeReplyContextMediaReferences(replyContext: CanonReplyContext | null, materialized: ReadonlyArray<MaterializedCanonAttachment>): CanonReplyContext | null;
13
+ /**
14
+ * Build the Claude Code SDK's multimodal user content without allowing native
15
+ * image blocks to consume the whole request. The prompt text is preserved
16
+ * verbatim, including local paths for images omitted from native blocks.
17
+ */
18
+ export declare function buildCanonUserContent(input: {
19
+ promptText: string;
20
+ materialized: ReadonlyArray<MaterializedCanonAttachment>;
21
+ }, budget?: Omit<AnthropicImageBudgetOptions, 'promptText'>): Promise<string | SDKUserMessage['message']['content']>;
22
+ export {};
@@ -0,0 +1,72 @@
1
+ import { toAnthropicImageBlocksWithinBudget, } from '@canonmsg/agent-sdk';
2
+ import { renderCanonHostInboundContent, } from '@canonmsg/core';
3
+ const MAX_PROMPT_ATTACHMENT_URL_LENGTH = 8_192;
4
+ function validatedPromptAttachmentUrl(value) {
5
+ if (typeof value !== 'string'
6
+ || value.length === 0
7
+ || value.length > MAX_PROMPT_ATTACHMENT_URL_LENGTH
8
+ || /[\u0000-\u0020\u007f]/.test(value)) {
9
+ return null;
10
+ }
11
+ try {
12
+ const parsed = new URL(value);
13
+ if (parsed.protocol !== 'https:'
14
+ || parsed.hostname.length === 0
15
+ || parsed.username.length > 0
16
+ || parsed.password.length > 0) {
17
+ return null;
18
+ }
19
+ return parsed.href;
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ /**
26
+ * Claude-only rendering policy for Canon media. Core deliberately omits all
27
+ * URLs globally. This wrapper restores validated HTTPS links only for images
28
+ * that the bounded runtime did not materialize, while materialized images keep
29
+ * their local-path placeholder without a duplicate link.
30
+ */
31
+ export function renderClaudeInboundContent(message, materialized = []) {
32
+ const rendered = renderCanonHostInboundContent(message, materialized);
33
+ const materializedIndexes = new Set(materialized.map((attachment) => attachment.index));
34
+ const imageLinks = (message.attachments ?? []).flatMap((attachment, index) => {
35
+ if (attachment.kind !== 'image' || materializedIndexes.has(index))
36
+ return [];
37
+ const url = validatedPromptAttachmentUrl(attachment.url);
38
+ return url ? [`[Image ${index + 1} link: ${url}]`] : [];
39
+ });
40
+ return imageLinks.length > 0 ? `${rendered}\n${imageLinks.join('\n')}` : rendered;
41
+ }
42
+ export function withClaudeReplyContextMediaReferences(replyContext, materialized) {
43
+ if (!replyContext?.found)
44
+ return replyContext;
45
+ return {
46
+ ...replyContext,
47
+ body: renderClaudeInboundContent({
48
+ text: replyContext.text,
49
+ contentType: replyContext.contentType,
50
+ attachments: replyContext.attachments,
51
+ contactCard: replyContext.contactCard,
52
+ senderType: replyContext.senderType ?? undefined,
53
+ }, materialized),
54
+ };
55
+ }
56
+ /**
57
+ * Build the Claude Code SDK's multimodal user content without allowing native
58
+ * image blocks to consume the whole request. The prompt text is preserved
59
+ * verbatim, including local paths for images omitted from native blocks.
60
+ */
61
+ export async function buildCanonUserContent(input, budget) {
62
+ const imageBlocks = await toAnthropicImageBlocksWithinBudget(input.materialized, {
63
+ promptText: input.promptText,
64
+ ...budget,
65
+ });
66
+ if (imageBlocks.length === 0)
67
+ return input.promptText;
68
+ return [
69
+ { type: 'text', text: input.promptText },
70
+ ...imageBlocks,
71
+ ];
72
+ }
package/dist/host.js CHANGED
@@ -26,14 +26,15 @@ import { execFileSync } from 'node:child_process';
26
26
  import { existsSync } from 'node:fs';
27
27
  import { readFile } from 'node:fs/promises';
28
28
  import { query, } from '@anthropic-ai/claude-agent-sdk';
29
- import { isAnthropicImageAttachment, materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMessage, toAnthropicImageBlock, } from '@canonmsg/agent-sdk';
29
+ import { materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMessage, } from '@canonmsg/agent-sdk';
30
30
  import { captureTurnArtifactSnapshot, createTurnArtifactRouter, IDLE_TIMEOUT_MS, collectMissedInboundMessages, createReconnectRecoveryGate, STARTUP_RECOVERY_MAX_MESSAGES, STARTUP_RECOVERY_PAGE_SIZE, } from '@canonmsg/coding-agent-host';
31
- import { EFFORT_OPTIONS, CLAUDE_PERMISSION_MODE_OPTIONS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildCanonInboundFrameV1, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, normalizeRuntimeCommandDescriptors, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createTurnOutputController, createRuntimeStatePublisher, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, CanonClient, CanonStream, ControlChannelPoller, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, ApprovalManager, RuntimeRequestManager, ExecutionEnvironmentError, FINAL_MESSAGE_HANDOFF_MS, buildLocalRuntimeId, getActiveProfileLock, heartbeatLocalRuntimeEntry, normalizeTurnMetadata, parseTurnVerbosityConfig, prepareConversationEnvironment, resolveCanonAgent, verifyResolvedAgentEnvironment, decideAutoReply, initRTDBAuth, isChunkedSendMessageError, sendMessageWithRetry, sendMessageWithRetryChunked, loadHostSessionConfig, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, saveRuntimeSessionState, clearRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd, resolveTurnVerbosity, shouldTriggerAgentTurn, upsertLocalRuntimeEntry, } from '@canonmsg/core';
31
+ import { buildCanonUserContent, renderClaudeInboundContent, withClaudeReplyContextMediaReferences, } from './canon-user-content.js';
32
+ import { EFFORT_OPTIONS, CLAUDE_PERMISSION_MODE_OPTIONS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildCanonInboundFrameV1, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, normalizeRuntimeCommandDescriptors, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createTurnOutputController, createRuntimeStatePublisher, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, CanonClient, CanonStream, ControlChannelPoller, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, ApprovalManager, RuntimeRequestManager, ExecutionEnvironmentError, FINAL_MESSAGE_HANDOFF_MS, buildLocalRuntimeId, getActiveProfileLock, heartbeatLocalRuntimeEntry, normalizeTurnMetadata, parseTurnVerbosityConfig, prepareConversationEnvironment, resolveCanonAgent, verifyResolvedAgentEnvironment, decideAutoReply, initRTDBAuth, isChunkedSendMessageError, sendMessageWithRetry, sendMessageWithRetryChunked, loadHostSessionConfig, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, saveRuntimeSessionState, clearRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd, resolveTurnVerbosity, shouldTriggerAgentTurn, upsertLocalRuntimeEntry, } from '@canonmsg/core';
32
33
  import { runCli } from '@canonmsg/core';
33
34
  import { CANON_VERB_MCP_SERVER_NAME, createCanonVerbMcpServer } from '@canonmsg/agent-tools';
34
35
  import { synthesizeClaudeApprovalDiff } from './approval-diff.js';
35
36
  import { decideClaudeToolPermissionForMode, parseAllowedNonOwnerClaudeTools, } from './tool-policy.js';
36
- import { applyClaudeSessionControl, boundClaudeFinalMetadata, buildClaudeFinalChunkingOptions, buildClaudeFinalMessageId, buildClaudeFinalTurnMetadata, buildClaudePendingFinalDelivery, buildClaudeTurnFailureNotice, buildTruncatedFinalText, buildUndeliverableFinalNotice, canDrainClaudeQueuedInput, claudeFinalWillChunk, classifyFinalDeliveryFailure, claudeInputOwnsTurnSlot, createClaudeTurnActivityState, decideClaudeControlSignalAction, decideClaudeInboundDispatch, dispatchClaudeInput, 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';
37
+ import { applyClaudeSessionControl, 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';
37
38
  import { CLAUDE_SUPPORTED_DIALOG_KINDS, buildClaudeAskUserPermissionDenied, buildClaudeAskUserPermissionResult, createClaudeUserDialogCoordinator, parseClaudeAskUserDialog, parseClaudeAskUserToolInput, resolveClaudeUserDialogRequestId, } from './user-dialog.js';
38
39
  function parseRuntimeVisibilityPreset(value) {
39
40
  return value === 'normal' || value === 'minimal' || value === 'full' ? value : undefined;
@@ -781,43 +782,30 @@ function resolveClaudeTurnModes(participantContext) {
781
782
  };
782
783
  }
783
784
  function renderInboundContent(message, materialized) {
784
- return renderCanonHostInboundContent(message, materialized);
785
+ return renderClaudeInboundContent(message, materialized);
785
786
  }
786
787
  async function materializePromptReplyContext(input) {
787
788
  if (!input.replyContext?.found || !input.replyContext.attachments?.length) {
788
789
  return { replyContext: input.replyContext, materialized: [] };
789
790
  }
790
791
  try {
791
- return await materializeReplyContextMedia(input.replyContext, {
792
+ const result = await materializeReplyContextMedia(input.replyContext, {
792
793
  agentId: input.agentId,
793
794
  conversationId: input.conversationId,
794
795
  });
796
+ return {
797
+ ...result,
798
+ replyContext: withClaudeReplyContextMediaReferences(result.replyContext, result.materialized),
799
+ };
795
800
  }
796
801
  catch (error) {
797
802
  console.error(`${input.logPrefix} Failed to materialize replied-to media:`, error instanceof Error ? error.message : error);
798
- return { replyContext: input.replyContext, materialized: [] };
803
+ return {
804
+ replyContext: withClaudeReplyContextMediaReferences(input.replyContext, []),
805
+ materialized: [],
806
+ };
799
807
  }
800
808
  }
801
- /**
802
- * Build the multimodal `content` payload handed to the Claude Code SDK.
803
- *
804
- * Images are delivered as native Anthropic `image` blocks (base64) so the
805
- * vision model sees pixels, not a text reference to a URL. Non-image
806
- * attachments (audio, files) are referenced by their local path inside the
807
- * text prompt — see `renderCanonHostInboundContent` — so the agent can
808
- * Read them on demand.
809
- */
810
- async function buildCanonUserContent(input) {
811
- const imageAttachments = input.materialized.filter(isAnthropicImageAttachment);
812
- if (imageAttachments.length === 0) {
813
- return input.promptText;
814
- }
815
- const imageBlocks = await Promise.all(imageAttachments.map((attachment) => toAnthropicImageBlock(attachment)));
816
- return [
817
- { type: 'text', text: input.promptText },
818
- ...imageBlocks,
819
- ];
820
- }
821
809
  // ── Session factory ─────────────────────────────────────────────────
822
810
  function createSession(conversationId, environment, agentId, client, typingSignals, runtimeState, onSessionEnd, onRuntimeDescriptorUpdate, config, resumeSessionId) {
823
811
  const { cwd } = environment;
@@ -2078,7 +2066,17 @@ function createSession(conversationId, environment, agentId, client, typingSigna
2078
2066
  // being written.
2079
2067
  if (!isClaudeMainTurnMessage(msg.parent_tool_use_id))
2080
2068
  break;
2081
- if (event?.type === 'content_block_start' && event.content_block?.type === 'tool_use') {
2069
+ if (event?.type === 'message_start') {
2070
+ beginClaudeAssistantResponse(session.turnActivity);
2071
+ }
2072
+ else if (event?.type === 'message_stop') {
2073
+ endClaudeAssistantResponse(session.turnActivity);
2074
+ }
2075
+ else if (event?.type === 'content_block_start' && event.content_block?.type === 'tool_use') {
2076
+ // A tool is also a response boundary for speech. This fallback
2077
+ // keeps continual streaming correct on SDK streams that omit raw
2078
+ // message_start/message_stop events.
2079
+ endClaudeAssistantResponse(session.turnActivity);
2082
2080
  // Claimed per CALL, keyed by the SDK's tool_use id. The old id was
2083
2081
  // the turn id, so every call in a turn wrote over the same row.
2084
2082
  const commands = planClaudeToolCallStart(session.turnActivity, {
@@ -2116,8 +2114,10 @@ function createSession(conversationId, environment, agentId, client, typingSigna
2116
2114
  stopVisibleWorkSignal();
2117
2115
  }
2118
2116
  writeTurn();
2119
- const blockIndex = typeof event.index === 'number' ? event.index : 'latest';
2120
- onStreamDelta(event.delta.text, `text:${session.currentTurnId ?? conversationId}:${blockIndex}`);
2117
+ onStreamDelta(event.delta.text, claimClaudeTextSegmentId(session.turnActivity, {
2118
+ turnId: session.currentTurnId ?? conversationId,
2119
+ index: event.index,
2120
+ }));
2121
2121
  }
2122
2122
  break;
2123
2123
  }
package/dist/server.js CHANGED
@@ -10,9 +10,10 @@ import { setDefaultResultOrder } from 'node:dns';
10
10
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
11
11
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
12
12
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
13
- import { CanonClient, CanonStream, ApprovalManager, buildLocalRuntimeId, getActiveProfileLock, markLocalRuntimeStopped, loadProfiles, isProfileLocked, resolveCanonAgent, verifyResolvedAgentEnvironment, getActiveProfile, releaseLock, upsertLocalRuntimeEntry, initRTDBAuth, renderCanonHostInboundContent, } from '@canonmsg/core';
13
+ import { CanonClient, CanonStream, ApprovalManager, buildLocalRuntimeId, getActiveProfileLock, markLocalRuntimeStopped, loadProfiles, isProfileLocked, resolveCanonAgent, verifyResolvedAgentEnvironment, getActiveProfile, releaseLock, upsertLocalRuntimeEntry, initRTDBAuth, } from '@canonmsg/core';
14
14
  import { materializeMessageMedia, sendMediaFileMessage, } from '@canonmsg/agent-sdk';
15
15
  import { ApprovalHttpServer } from './approval-server.js';
16
+ import { renderClaudeInboundContent } from './canon-user-content.js';
16
17
  import { runCli } from '@canonmsg/core';
17
18
  import { parseReplyArgs, parseSendMessageArgs, parseSetTypingArgs, } from './mcp-args.js';
18
19
  import { canonVerbToolDefinitions, executeCanonVerbTool, isCanonToolVerb, stampSendToTurnComplete, } from '@canonmsg/agent-tools';
@@ -55,7 +56,7 @@ function getPrimaryAttachment(message) {
55
56
  return null;
56
57
  }
57
58
  function renderInboundContent(message, materialized) {
58
- return renderCanonHostInboundContent(message, materialized);
59
+ return renderClaudeInboundContent(message, materialized);
59
60
  }
60
61
  function toolArgumentError(error) {
61
62
  return {
@@ -356,6 +356,10 @@ export interface ClaudeTurnActivityState {
356
356
  blockIdByToolUseId: Map<string, string>;
357
357
  /** This turn's plan block, once TodoWrite has produced one. */
358
358
  planBlockId: string | null;
359
+ /** Monotonic identity for assistant responses inside this turn. */
360
+ assistantResponseCount: number;
361
+ /** The response whose text deltas are currently being streamed. */
362
+ activeAssistantResponseKey: string | null;
359
363
  }
360
364
  /**
361
365
  * Whether an SDK message describes the agent's OWN work rather than a
@@ -367,6 +371,22 @@ export interface ClaudeTurnActivityState {
367
371
  export declare function isClaudeMainTurnMessage(parentToolUseId: unknown): boolean;
368
372
  export declare function createClaudeTurnActivityState(): ClaudeTurnActivityState;
369
373
  export declare function resetClaudeTurnActivityState(state: ClaudeTurnActivityState): void;
374
+ /** Open a fresh assistant response, whose content-block indexes start at zero. */
375
+ export declare function beginClaudeAssistantResponse(state: ClaudeTurnActivityState): string;
376
+ /** Close the response so a later text delta cannot reuse its block identity. */
377
+ export declare function endClaudeAssistantResponse(state: ClaudeTurnActivityState): void;
378
+ /**
379
+ * Stable id for one text block inside one assistant response.
380
+ *
381
+ * Anthropic restarts `content_block.index` for every assistant response. The
382
+ * response key is therefore required to distinguish text before a tool from
383
+ * text emitted after its result. Lazily opening a response keeps older SDK
384
+ * streams that omit `message_start` safe as well.
385
+ */
386
+ export declare function claimClaudeTextSegmentId(state: ClaudeTurnActivityState, input: {
387
+ turnId?: string | null;
388
+ index?: unknown;
389
+ }): string;
370
390
  export declare function claudeToolBlockId(toolUseId: string): string;
371
391
  export declare function claudePlanBlockId(turnId: string | null | undefined): string;
372
392
  /**
@@ -376,12 +376,42 @@ export function createClaudeTurnActivityState() {
376
376
  blockIdByIndex: new Map(),
377
377
  blockIdByToolUseId: new Map(),
378
378
  planBlockId: null,
379
+ assistantResponseCount: 0,
380
+ activeAssistantResponseKey: null,
379
381
  };
380
382
  }
381
383
  export function resetClaudeTurnActivityState(state) {
382
384
  state.blockIdByIndex.clear();
383
385
  state.blockIdByToolUseId.clear();
384
386
  state.planBlockId = null;
387
+ state.assistantResponseCount = 0;
388
+ state.activeAssistantResponseKey = null;
389
+ }
390
+ /** Open a fresh assistant response, whose content-block indexes start at zero. */
391
+ export function beginClaudeAssistantResponse(state) {
392
+ state.assistantResponseCount += 1;
393
+ state.activeAssistantResponseKey = `response-${state.assistantResponseCount}`;
394
+ return state.activeAssistantResponseKey;
395
+ }
396
+ /** Close the response so a later text delta cannot reuse its block identity. */
397
+ export function endClaudeAssistantResponse(state) {
398
+ state.activeAssistantResponseKey = null;
399
+ }
400
+ /**
401
+ * Stable id for one text block inside one assistant response.
402
+ *
403
+ * Anthropic restarts `content_block.index` for every assistant response. The
404
+ * response key is therefore required to distinguish text before a tool from
405
+ * text emitted after its result. Lazily opening a response keeps older SDK
406
+ * streams that omit `message_start` safe as well.
407
+ */
408
+ export function claimClaudeTextSegmentId(state, input) {
409
+ const responseKey = state.activeAssistantResponseKey
410
+ ?? beginClaudeAssistantResponse(state);
411
+ const index = typeof input.index === 'number' && Number.isInteger(input.index)
412
+ ? input.index
413
+ : 'latest';
414
+ return buildTrailBlockId('text', input.turnId ?? 'turn', responseKey, index);
385
415
  }
386
416
  export function claudeToolBlockId(toolUseId) {
387
417
  return buildTrailBlockId('tool', toolUseId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/claude-code-plugin",
3
- "version": "0.31.2",
3
+ "version": "0.31.4",
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,13 +30,13 @@
30
30
  "test": "vitest run"
31
31
  },
32
32
  "dependencies": {
33
- "@anthropic-ai/claude-agent-sdk": "0.3.220",
34
- "@canonmsg/agent-sdk": "^8.3.0",
35
- "@canonmsg/agent-tools": "^0.5.2",
33
+ "@anthropic-ai/claude-agent-sdk": "0.3.228",
34
+ "@canonmsg/agent-sdk": "^8.6.0",
35
+ "@canonmsg/agent-tools": "^0.5.3",
36
36
  "@canonmsg/coding-agent-host": "^0.5.0",
37
- "@canonmsg/core": "^10.3.1",
38
- "@canonmsg/rich-cards": "^0.10.0",
39
- "@modelcontextprotocol/sdk": "^1.29.0"
37
+ "@canonmsg/core": "^10.4.0",
38
+ "@canonmsg/rich-cards": "^0.10.2",
39
+ "@modelcontextprotocol/sdk": "^1.30.0"
40
40
  },
41
41
  "engines": {
42
42
  "node": ">=18.0.0"