@canonmsg/claude-code-plugin 0.34.5 → 0.35.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.
@@ -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.34.5",
4
+ "version": "0.35.0",
5
5
  "channels": [
6
6
  {
7
7
  "server": "canon-channel",
package/dist/host.js CHANGED
@@ -30,7 +30,7 @@ import { materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMes
30
30
  import { captureTurnArtifactSnapshot, createTurnArtifactRouter, IDLE_TIMEOUT_MS, collectMissedInboundMessages, createRecoveryCheckpointTracker, createReconnectRecoveryCoordinator, STARTUP_RECOVERY_PAGE_SIZE, } from '@canonmsg/coding-agent-host';
31
31
  import { buildCanonUserContent, renderClaudeInboundContent, withClaudeReplyContextMediaReferences, } from './canon-user-content.js';
32
32
  import { buildClaudeNativeCommandInput, createClaudeCommandContextHook, getClaudeNativeCommandOutput, } from './native-command-input.js';
33
- 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, createRuntimeHeartbeat, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, CanonClient, CanonStream, ControlChannelPoller, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, ApprovalManager, RuntimeRequestManager, FINAL_MESSAGE_HANDOFF_MS, buildLocalRuntimeId, getActiveProfileLock, heartbeatLocalRuntimeEntry, normalizeTurnMetadata, parseTurnVerbosityConfig, prepareConversationEnvironment, resolveCanonAgent, verifyResolvedAgentEnvironment, decideAutoReply, initRTDBAuth, isChunkedSendMessageError, sendMessageWithRetry, sendMessageWithRetryChunked, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, resolveLocalRuntimeSessionState, saveRuntimeSessionState, clearRuntimeSessionState, publishHostSessionSnapshots, renderCodingHostInboundPrompt, resolveConfiguredWorkspaceCwd, resolveTurnVerbosity, shouldTriggerAgentTurn, upsertLocalRuntimeEntry, } from '@canonmsg/core';
33
+ import { EFFORT_OPTIONS, CLAUDE_PERMISSION_MODE_OPTIONS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildCanonInboundFrameV1, buildCanonGroupContext, buildCompactGroupContextLines, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, normalizeRuntimeCommandDescriptors, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createTurnOutputController, createRuntimeStatePublisher, createRuntimeHeartbeat, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, CanonClient, CanonStream, ControlChannelPoller, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, ApprovalManager, RuntimeRequestManager, FINAL_MESSAGE_HANDOFF_MS, buildLocalRuntimeId, getActiveProfileLock, heartbeatLocalRuntimeEntry, normalizeTurnMetadata, parseTurnVerbosityConfig, prepareConversationEnvironment, resolveCanonAgent, verifyResolvedAgentEnvironment, decideAutoReply, initRTDBAuth, isChunkedSendMessageError, sendMessageWithRetry, sendMessageWithRetryChunked, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, resolveLocalRuntimeSessionState, saveRuntimeSessionState, clearRuntimeSessionState, publishHostSessionSnapshots, renderCodingHostInboundPrompt, resolveConfiguredWorkspaceCwd, resolveTurnVerbosity, shouldTriggerAgentTurn, upsertLocalRuntimeEntry, } from '@canonmsg/core';
34
34
  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';
@@ -615,6 +615,22 @@ function createSession(conversationId, environment, agentId, client, typingSigna
615
615
  // Only Canon turns may own the active-input slot. Result ownership comes from
616
616
  // UUID correlation, while this slot drives live tool/dialog state.
617
617
  function bindActiveInput(input) {
618
+ if (claudeInputOwnsTurnSlot(input)) {
619
+ const roster = config.currentParticipantContext?.();
620
+ if (roster && input.commandContext) {
621
+ // Native slash-command arguments must stay byte-for-byte intact. Their
622
+ // context travels through UserPromptExpansion rather than the command.
623
+ input.commandContext = `${input.commandContext}\n\n${roster}`;
624
+ }
625
+ else if (roster) {
626
+ const content = input.msg.message.content;
627
+ input.msg = { ...input.msg, message: { ...input.msg.message,
628
+ content: typeof content === 'string'
629
+ ? `${content}\n\n${roster}`
630
+ : [...content, { type: 'text', text: roster }],
631
+ } };
632
+ }
633
+ }
618
634
  rememberDispatchedClaudeInput(dispatchedInputs, input);
619
635
  // The slot drives in-turn concerns (tool routing, dialogs, interrupts), so
620
636
  // it tracks the newest Canon turn. Result ownership is resolved by uuid, not
@@ -1359,8 +1375,9 @@ function createSession(conversationId, environment, agentId, client, typingSigna
1359
1375
  return false;
1360
1376
  }
1361
1377
  }
1362
- function sendTurnArtifactFile(file) {
1378
+ function sendTurnArtifactFile(file, canPublish) {
1363
1379
  return sendMediaFileMessage(client, conversationId, file.path, '', {
1380
+ canPublish,
1364
1381
  ...(session.activeInput?.replyAuthority
1365
1382
  ? { replyAuthority: session.activeInput.replyAuthority }
1366
1383
  : {}),
@@ -1382,16 +1399,18 @@ function createSession(conversationId, environment, agentId, client, typingSigna
1382
1399
  * `composeClaudeTurnFinal` makes for the text.
1383
1400
  */
1384
1401
  function routeArtifactsForInput(input, finalText) {
1402
+ const decideArtifactRouting = () => shouldRouteClaudeTurnArtifacts({
1403
+ turn: input,
1404
+ interruptedTurnKeys: session.interruptedTurnKeys,
1405
+ silencedTurnKeys: session.silencedTurnKeys,
1406
+ finalText,
1407
+ currentAudience: config.currentArtifactAudience?.() ?? { agentId, ownerId: config.ownerId },
1408
+ });
1385
1409
  return createTurnArtifactRouter({
1386
- decide: () => shouldRouteClaudeTurnArtifacts({
1387
- turn: input,
1388
- interruptedTurnKeys: session.interruptedTurnKeys,
1389
- silencedTurnKeys: session.silencedTurnKeys,
1390
- finalText,
1391
- }),
1410
+ decide: decideArtifactRouting,
1392
1411
  baseline: () => input.artifactBaseline,
1393
1412
  cwd: () => session.cwd,
1394
- send: (file) => sendTurnArtifactFile(file),
1413
+ send: (file) => sendTurnArtifactFile(file, () => decideArtifactRouting().route),
1395
1414
  log: (line) => console.error(`[canon-host] [${conversationId.slice(0, 8)}] ${line}`),
1396
1415
  }).route();
1397
1416
  }
@@ -2532,6 +2551,7 @@ export async function main() {
2532
2551
  if (cached) {
2533
2552
  conversationCache.set(payload.conversationId, {
2534
2553
  ...cached,
2554
+ ...payload.changes,
2535
2555
  memberIds,
2536
2556
  });
2537
2557
  }
@@ -2540,11 +2560,12 @@ export async function main() {
2540
2560
  }
2541
2561
  if (!memberIds.includes(agentId)) {
2542
2562
  knownConversationIds.delete(payload.conversationId);
2543
- conversationCache.delete(payload.conversationId);
2563
+ pendingMembershipChanges.delete(payload.conversationId);
2564
+ closeSession(payload.conversationId);
2544
2565
  }
2545
2566
  }
2546
2567
  function getGroupContextMode(conversationId, conversation) {
2547
- if (conversation?.type !== 'group')
2568
+ if (!conversation)
2548
2569
  return undefined;
2549
2570
  if (pendingMembershipChanges.has(conversationId))
2550
2571
  return 'membership_change';
@@ -2562,7 +2583,7 @@ export async function main() {
2562
2583
  }
2563
2584
  async function loadHydratedInboundContext(input) {
2564
2585
  const [conversation, page] = await Promise.all([
2565
- getConversationMeta(input.conversationId),
2586
+ getConversationMeta(input.conversationId, { refreshIfMemberMissing: agentId }),
2566
2587
  input.hydratedPage
2567
2588
  ? Promise.resolve(input.hydratedPage)
2568
2589
  : client.getMessagesPage(input.conversationId, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT).catch(() => null),
@@ -2571,6 +2592,7 @@ export async function main() {
2571
2592
  agentId,
2572
2593
  conversationId: input.conversationId,
2573
2594
  conversation,
2595
+ requireAgentMembership: true,
2574
2596
  page,
2575
2597
  activeSelfContextId: input.activeSelfContextId,
2576
2598
  selfContexts: input.selfContexts,
@@ -2704,6 +2726,10 @@ export async function main() {
2704
2726
  }
2705
2727
  }
2706
2728
  async function getOrCreateSession(conversationId) {
2729
+ const current = conversationCache.get(conversationId);
2730
+ if (current && !current.memberIds.includes(agentId)) {
2731
+ throw new Error('Agent is no longer a member of this conversation');
2732
+ }
2707
2733
  knownConversationIds.add(conversationId);
2708
2734
  const existing = sessions.get(conversationId);
2709
2735
  if (existing && !existing.closed) {
@@ -2813,6 +2839,13 @@ export async function main() {
2813
2839
  runtimeId,
2814
2840
  ownerId,
2815
2841
  communicationEnabled,
2842
+ currentParticipantContext: () => {
2843
+ const roster = buildCanonGroupContext({
2844
+ conversation: conversationCache.get(conversationId), messages: [], agentId, ownerId, ownerName,
2845
+ });
2846
+ return roster ? buildCompactGroupContextLines(roster, 'initial').join('\n') : '';
2847
+ },
2848
+ currentArtifactAudience: () => ({ memberIds: conversationCache.get(conversationId)?.memberIds, agentId, ownerId }),
2816
2849
  onInputCompleted: (sourceMessageId) => {
2817
2850
  recoveryCheckpointsFor(conversationId).settle(sourceMessageId);
2818
2851
  settleInboundMessageId(sourceMessageId, true);
@@ -142,6 +142,11 @@ export declare function shouldRouteClaudeTurnArtifacts(input: {
142
142
  interruptedTurnKeys: ReadonlySet<string>;
143
143
  silencedTurnKeys: ReadonlySet<string>;
144
144
  finalText: string | null | undefined;
145
+ currentAudience?: {
146
+ memberIds?: readonly string[];
147
+ agentId: string;
148
+ ownerId: string | null | undefined;
149
+ };
145
150
  }): TurnArtifactRoutingDecision;
146
151
  export declare function rememberDispatchedClaudeInput(dispatched: Map<string, ClaudeInputEnvelope>, input: ClaudeInputEnvelope): void;
147
152
  /**
@@ -74,8 +74,12 @@ export function shouldDeliverClaudeFinal(input) {
74
74
  * `DEFAULT_SILENT_TURN_PRECEDENCE` moves a turn's text and its files together.
75
75
  */
76
76
  export function shouldRouteClaudeTurnArtifacts(input) {
77
+ const audience = input.currentAudience;
78
+ const ownerPair = !audience || (audience.ownerId !== audience.agentId
79
+ && Boolean(audience.ownerId) && audience.memberIds?.length === 2
80
+ && audience.memberIds.includes(audience.agentId) && audience.memberIds.includes(audience.ownerId));
77
81
  return resolveTurnArtifactRouting({
78
- artifactRoutingMode: input.turn.artifactRoutingMode,
82
+ artifactRoutingMode: ownerPair ? input.turn.artifactRoutingMode : 'disabled',
79
83
  interrupted: input.interruptedTurnKeys.has(input.turn.turnKey),
80
84
  silenced: isSilentTurnSuppressed({
81
85
  silenced: input.silencedTurnKeys.has(input.turn.turnKey),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/claude-code-plugin",
3
- "version": "0.34.5",
3
+ "version": "0.35.0",
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",
@@ -31,10 +31,10 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@anthropic-ai/claude-agent-sdk": "0.3.228",
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",
34
+ "@canonmsg/agent-sdk": "^10.4.0",
35
+ "@canonmsg/agent-tools": "^0.10.0",
36
+ "@canonmsg/coding-agent-host": "^0.8.0",
37
+ "@canonmsg/core": "^12.5.0",
38
38
  "@canonmsg/rich-cards": "^0.10.5",
39
39
  "@modelcontextprotocol/sdk": "^1.30.0"
40
40
  },