@canonmsg/claude-code-plugin 0.31.4 → 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/.claude-plugin/plugin.json +1 -1
- package/dist/canon-user-content.d.ts +3 -3
- package/dist/canon-user-content.js +4 -4
- package/dist/host.d.ts +3 -0
- package/dist/host.js +178 -53
- package/dist/register.js +1 -0
- package/dist/server.js +6 -2
- package/dist/session-state.d.ts +4 -0
- package/dist/session-state.js +3 -0
- package/package.json +5 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
|
|
2
2
|
import { type AnthropicImageBudgetOptions, type MaterializedCanonAttachment } from '@canonmsg/agent-sdk';
|
|
3
|
-
import { renderCanonHostInboundContent, type CanonReplyContext } from '@canonmsg/core';
|
|
3
|
+
import { renderCanonHostInboundContent, type CanonReplyContext, type HostInboundContentRenderOptions } from '@canonmsg/core';
|
|
4
4
|
type ClaudeRenderableInboundMessage = Parameters<typeof renderCanonHostInboundContent>[0];
|
|
5
5
|
/**
|
|
6
6
|
* Claude-only rendering policy for Canon media. Core deliberately omits all
|
|
@@ -8,8 +8,8 @@ type ClaudeRenderableInboundMessage = Parameters<typeof renderCanonHostInboundCo
|
|
|
8
8
|
* that the bounded runtime did not materialize, while materialized images keep
|
|
9
9
|
* their local-path placeholder without a duplicate link.
|
|
10
10
|
*/
|
|
11
|
-
export declare function renderClaudeInboundContent(message: ClaudeRenderableInboundMessage, materialized?: ReadonlyArray<MaterializedCanonAttachment
|
|
12
|
-
export declare function withClaudeReplyContextMediaReferences(replyContext: CanonReplyContext | null, materialized: ReadonlyArray<MaterializedCanonAttachment
|
|
11
|
+
export declare function renderClaudeInboundContent(message: ClaudeRenderableInboundMessage, materialized?: ReadonlyArray<MaterializedCanonAttachment>, renderOptions?: HostInboundContentRenderOptions): string;
|
|
12
|
+
export declare function withClaudeReplyContextMediaReferences(replyContext: CanonReplyContext | null, materialized: ReadonlyArray<MaterializedCanonAttachment>, renderOptions?: HostInboundContentRenderOptions): CanonReplyContext | null;
|
|
13
13
|
/**
|
|
14
14
|
* Build the Claude Code SDK's multimodal user content without allowing native
|
|
15
15
|
* image blocks to consume the whole request. The prompt text is preserved
|
|
@@ -28,8 +28,8 @@ function validatedPromptAttachmentUrl(value) {
|
|
|
28
28
|
* that the bounded runtime did not materialize, while materialized images keep
|
|
29
29
|
* their local-path placeholder without a duplicate link.
|
|
30
30
|
*/
|
|
31
|
-
export function renderClaudeInboundContent(message, materialized = []) {
|
|
32
|
-
const rendered = renderCanonHostInboundContent(message, materialized);
|
|
31
|
+
export function renderClaudeInboundContent(message, materialized = [], renderOptions = {}) {
|
|
32
|
+
const rendered = renderCanonHostInboundContent(message, materialized, renderOptions);
|
|
33
33
|
const materializedIndexes = new Set(materialized.map((attachment) => attachment.index));
|
|
34
34
|
const imageLinks = (message.attachments ?? []).flatMap((attachment, index) => {
|
|
35
35
|
if (attachment.kind !== 'image' || materializedIndexes.has(index))
|
|
@@ -39,7 +39,7 @@ export function renderClaudeInboundContent(message, materialized = []) {
|
|
|
39
39
|
});
|
|
40
40
|
return imageLinks.length > 0 ? `${rendered}\n${imageLinks.join('\n')}` : rendered;
|
|
41
41
|
}
|
|
42
|
-
export function withClaudeReplyContextMediaReferences(replyContext, materialized) {
|
|
42
|
+
export function withClaudeReplyContextMediaReferences(replyContext, materialized, renderOptions = {}) {
|
|
43
43
|
if (!replyContext?.found)
|
|
44
44
|
return replyContext;
|
|
45
45
|
return {
|
|
@@ -50,7 +50,7 @@ export function withClaudeReplyContextMediaReferences(replyContext, materialized
|
|
|
50
50
|
attachments: replyContext.attachments,
|
|
51
51
|
contactCard: replyContext.contactCard,
|
|
52
52
|
senderType: replyContext.senderType ?? undefined,
|
|
53
|
-
}, materialized),
|
|
53
|
+
}, materialized, renderOptions),
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
/**
|
package/dist/host.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { type PermissionResult, type SDKControlReloadPluginsResponse } from '@anthropic-ai/claude-agent-sdk';
|
|
3
|
+
import { type RecoveryCheckpointTracker } from '@canonmsg/coding-agent-host';
|
|
3
4
|
import { type CanonRuntimeCommandDescriptor, type HostInboundParticipantContext as InboundParticipantContext, type CanonReplyContext, type MessageCreatedPayload, type ResolvedAgentBehaviorPolicy, type TurnVerbosityConfig } from '@canonmsg/core';
|
|
4
5
|
/**
|
|
5
6
|
* `--turn-verbosity` beats `CANON_TURN_VERBOSITY`; `null` means "unset, use the
|
|
@@ -24,7 +25,9 @@ export declare function buildClaudePlanAllowResult(input: Record<string, unknown
|
|
|
24
25
|
* through. Canon-injected aliases keep precedence over native names.
|
|
25
26
|
*/
|
|
26
27
|
export declare function buildNativeSlashCommandDescriptors(native: SDKControlReloadPluginsResponse['commands'], reservedAliases: ReadonlySet<string>): CanonRuntimeCommandDescriptor[];
|
|
28
|
+
export declare function createClaudeRecoveryCheckpointTracker(persist: (messageId: string) => boolean): RecoveryCheckpointTracker;
|
|
27
29
|
export declare const NO_REPLY_TOOL_NAME: string;
|
|
30
|
+
export declare const REACH_OUT_TOOL_NAME = "mcp__canon__send_to";
|
|
28
31
|
export declare function buildCanonPrompt(input: {
|
|
29
32
|
content: string;
|
|
30
33
|
conversationId: string;
|
package/dist/host.js
CHANGED
|
@@ -27,11 +27,11 @@ import { existsSync } from 'node:fs';
|
|
|
27
27
|
import { readFile } from 'node:fs/promises';
|
|
28
28
|
import { query, } from '@anthropic-ai/claude-agent-sdk';
|
|
29
29
|
import { materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMessage, } from '@canonmsg/agent-sdk';
|
|
30
|
-
import { captureTurnArtifactSnapshot, createTurnArtifactRouter, IDLE_TIMEOUT_MS, collectMissedInboundMessages,
|
|
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
|
-
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
|
+
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, formatPendingContactLifecycleContext, getActiveProfileLock, heartbeatLocalRuntimeEntry, normalizeTurnMetadata, parseTurnVerbosityConfig, prepareConversationEnvironment, resolveCanonAgent, verifyResolvedAgentEnvironment, decideAutoReply, initRTDBAuth, isChunkedSendMessageError, sendMessageWithRetry, sendMessageWithRetryChunked, loadHostSessionConfig, loadRuntimeSessionState, markLocalRuntimeStopped, readLocalRuntimeEntry, reconcileContactLifecycleEvents, recordLocalRuntimeContactLifecycleEvent, saveLocalRuntimeContactLifecycleCursor, releaseConversationEnvironment, saveRuntimeSessionState, clearRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd, resolveTurnVerbosity, shouldTriggerAgentTurn, takeLocalRuntimeContactLifecycleEvents, upsertLocalRuntimeEntry, } from '@canonmsg/core';
|
|
33
33
|
import { runCli } from '@canonmsg/core';
|
|
34
|
-
import { CANON_VERB_MCP_SERVER_NAME, createCanonVerbMcpServer } from '@canonmsg/agent-tools';
|
|
34
|
+
import { CANON_VERB_MCP_SERVER_NAME, createCanonVerbMcpServer, } from '@canonmsg/agent-tools';
|
|
35
35
|
import { synthesizeClaudeApprovalDiff } from './approval-diff.js';
|
|
36
36
|
import { decideClaudeToolPermissionForMode, parseAllowedNonOwnerClaudeTools, } from './tool-policy.js';
|
|
37
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';
|
|
@@ -303,7 +303,11 @@ function buildClaudeRuntimeDescriptor(input) {
|
|
|
303
303
|
activation: { kind: 'control', controlId: 'permissionMode', value: 'plan' },
|
|
304
304
|
},
|
|
305
305
|
],
|
|
306
|
-
admissionActions:
|
|
306
|
+
admissionActions: {
|
|
307
|
+
...HOST_ADMISSION_ACTIONS_DISABLED,
|
|
308
|
+
requestContact: true,
|
|
309
|
+
reachOut: true,
|
|
310
|
+
},
|
|
307
311
|
presentation: input.presentation,
|
|
308
312
|
commands,
|
|
309
313
|
});
|
|
@@ -421,6 +425,7 @@ async function loadSessionConfig(conversationId, agentId, rtdb) {
|
|
|
421
425
|
agentId,
|
|
422
426
|
rtdb,
|
|
423
427
|
extraStringFields: ['permissionMode', 'effort'],
|
|
428
|
+
retryMissingMs: 3_000,
|
|
424
429
|
});
|
|
425
430
|
return {
|
|
426
431
|
...config,
|
|
@@ -728,6 +733,9 @@ const FINAL_DELIVERY_RETRY_MS = 30_000;
|
|
|
728
733
|
// without delaying anything that was going to fail anyway.
|
|
729
734
|
const MAX_FINAL_DELIVERY_RETRIES = 2;
|
|
730
735
|
const CLAUDE_RECOVERY_CURSOR_WORKSPACE_ID = 'claude-recovery-cursor-v1';
|
|
736
|
+
export function createClaudeRecoveryCheckpointTracker(persist) {
|
|
737
|
+
return createRecoveryCheckpointTracker(persist);
|
|
738
|
+
}
|
|
731
739
|
const CLAUDE_RUNTIME_CAPABILITIES = {
|
|
732
740
|
...DEFAULT_RUNTIME_CAPABILITIES,
|
|
733
741
|
supportsInterrupt: true,
|
|
@@ -749,6 +757,16 @@ const CLAUDE_RUNTIME_CAPABILITIES = {
|
|
|
749
757
|
*/
|
|
750
758
|
const NO_REPLY_VERB = 'no_reply';
|
|
751
759
|
export const NO_REPLY_TOOL_NAME = `mcp__${CANON_VERB_MCP_SERVER_NAME}__${NO_REPLY_VERB}`;
|
|
760
|
+
export const REACH_OUT_TOOL_NAME = `mcp__${CANON_VERB_MCP_SERVER_NAME}__send_to`;
|
|
761
|
+
function ownerBoundRenderOptions(isOwner) {
|
|
762
|
+
const admissionActions = isOwner
|
|
763
|
+
? { ...HOST_ADMISSION_ACTIONS_DISABLED, requestContact: true, reachOut: true }
|
|
764
|
+
: HOST_ADMISSION_ACTIONS_DISABLED;
|
|
765
|
+
return {
|
|
766
|
+
admissionActions,
|
|
767
|
+
...(admissionActions.reachOut ? { reachOutToolName: REACH_OUT_TOOL_NAME } : {}),
|
|
768
|
+
};
|
|
769
|
+
}
|
|
752
770
|
export function buildCanonPrompt(input) {
|
|
753
771
|
return renderCodingHostInboundPrompt(buildCanonInboundFrameV1(buildCanonTurnContextV2({
|
|
754
772
|
content: input.content,
|
|
@@ -781,8 +799,8 @@ function resolveClaudeTurnModes(participantContext) {
|
|
|
781
799
|
}),
|
|
782
800
|
};
|
|
783
801
|
}
|
|
784
|
-
function renderInboundContent(message, materialized) {
|
|
785
|
-
return renderClaudeInboundContent(message, materialized);
|
|
802
|
+
function renderInboundContent(message, materialized, renderOptions = {}) {
|
|
803
|
+
return renderClaudeInboundContent(message, materialized, renderOptions);
|
|
786
804
|
}
|
|
787
805
|
async function materializePromptReplyContext(input) {
|
|
788
806
|
if (!input.replyContext?.found || !input.replyContext.attachments?.length) {
|
|
@@ -795,17 +813,27 @@ async function materializePromptReplyContext(input) {
|
|
|
795
813
|
});
|
|
796
814
|
return {
|
|
797
815
|
...result,
|
|
798
|
-
replyContext: withClaudeReplyContextMediaReferences(result.replyContext, result.materialized),
|
|
816
|
+
replyContext: withClaudeReplyContextMediaReferences(result.replyContext, result.materialized, input.renderOptions),
|
|
799
817
|
};
|
|
800
818
|
}
|
|
801
819
|
catch (error) {
|
|
802
820
|
console.error(`${input.logPrefix} Failed to materialize replied-to media:`, error instanceof Error ? error.message : error);
|
|
803
821
|
return {
|
|
804
|
-
replyContext: withClaudeReplyContextMediaReferences(input.replyContext, []),
|
|
822
|
+
replyContext: withClaudeReplyContextMediaReferences(input.replyContext, [], input.renderOptions),
|
|
805
823
|
materialized: [],
|
|
806
824
|
};
|
|
807
825
|
}
|
|
808
826
|
}
|
|
827
|
+
function ownerBoundReplyContactTarget(replyContext) {
|
|
828
|
+
const card = replyContext?.found ? replyContext.contactCard : undefined;
|
|
829
|
+
if (!card?.userId)
|
|
830
|
+
return undefined;
|
|
831
|
+
return {
|
|
832
|
+
targetUserId: card.userId,
|
|
833
|
+
...(card.canonContactId ? { canonContactId: card.canonContactId } : {}),
|
|
834
|
+
sourceCardMessageId: replyContext.messageId,
|
|
835
|
+
};
|
|
836
|
+
}
|
|
809
837
|
// ── Session factory ─────────────────────────────────────────────────
|
|
810
838
|
function createSession(conversationId, environment, agentId, client, typingSignals, runtimeState, onSessionEnd, onRuntimeDescriptorUpdate, config, resumeSessionId) {
|
|
811
839
|
const { cwd } = environment;
|
|
@@ -824,7 +852,7 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
824
852
|
messageQueue.push(input);
|
|
825
853
|
}
|
|
826
854
|
};
|
|
827
|
-
let enqueueInboundMessage = (msg, intent = 'queue', sourceMessageId, markAccepted = false, isOwnerTurn = false, requestingUserId = null, turnModes = {}) => {
|
|
855
|
+
let enqueueInboundMessage = (msg, intent = 'queue', sourceMessageId, markAccepted = false, isOwnerTurn = false, requestingUserId = null, turnModes = {}, replyContactTarget) => {
|
|
828
856
|
sendInput(createClaudeInputEnvelope({
|
|
829
857
|
kind: 'canon',
|
|
830
858
|
msg,
|
|
@@ -833,6 +861,7 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
833
861
|
markAccepted,
|
|
834
862
|
isOwnerTurn,
|
|
835
863
|
requestingUserId,
|
|
864
|
+
replyContactTarget,
|
|
836
865
|
...turnModes,
|
|
837
866
|
}));
|
|
838
867
|
};
|
|
@@ -979,6 +1008,23 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
979
1008
|
const turnKey = session.activeInput?.turnKey;
|
|
980
1009
|
if (turnKey)
|
|
981
1010
|
session.silencedTurnKeys.add(turnKey);
|
|
1011
|
+
}, {
|
|
1012
|
+
ownerBoundCommunication: {
|
|
1013
|
+
getContext: () => {
|
|
1014
|
+
const input = session.activeInput;
|
|
1015
|
+
if (!input?.isOwnerTurn || !input.sourceMessageId)
|
|
1016
|
+
return null;
|
|
1017
|
+
return {
|
|
1018
|
+
isOwnerTurn: true,
|
|
1019
|
+
conversationId,
|
|
1020
|
+
sourceMessageId: input.sourceMessageId,
|
|
1021
|
+
...(session.currentTurnId ? { turnId: session.currentTurnId } : {}),
|
|
1022
|
+
...(input.replyContactTarget
|
|
1023
|
+
? { replyContactTarget: input.replyContactTarget }
|
|
1024
|
+
: {}),
|
|
1025
|
+
};
|
|
1026
|
+
},
|
|
1027
|
+
},
|
|
982
1028
|
}),
|
|
983
1029
|
},
|
|
984
1030
|
},
|
|
@@ -1168,7 +1214,7 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1168
1214
|
environment,
|
|
1169
1215
|
query: q,
|
|
1170
1216
|
sendInput,
|
|
1171
|
-
enqueueInbound: (msg, intent = 'queue', sourceMessageId, markAccepted = false, isOwnerTurn = false, requestingUserId = null, turnModes = {}) => enqueueInboundMessage(msg, intent, sourceMessageId, markAccepted, isOwnerTurn, requestingUserId, turnModes),
|
|
1217
|
+
enqueueInbound: (msg, intent = 'queue', sourceMessageId, markAccepted = false, isOwnerTurn = false, requestingUserId = null, turnModes = {}, replyContactTarget) => enqueueInboundMessage(msg, intent, sourceMessageId, markAccepted, isOwnerTurn, requestingUserId, turnModes, replyContactTarget),
|
|
1172
1218
|
state: {
|
|
1173
1219
|
model: undefined,
|
|
1174
1220
|
permissionMode: config.permissionMode,
|
|
@@ -1260,19 +1306,6 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1260
1306
|
function markInputCompleted(input) {
|
|
1261
1307
|
if (input?.kind !== 'canon' || !input.sourceMessageId)
|
|
1262
1308
|
return;
|
|
1263
|
-
if (config.runtimeId) {
|
|
1264
|
-
try {
|
|
1265
|
-
saveRuntimeSessionState(config.runtimeId, {
|
|
1266
|
-
conversationId,
|
|
1267
|
-
baseCwd: config.recoveryBaseCwd ?? environment.baseCwd,
|
|
1268
|
-
workspaceId: CLAUDE_RECOVERY_CURSOR_WORKSPACE_ID,
|
|
1269
|
-
lastInboundMessageId: input.sourceMessageId,
|
|
1270
|
-
});
|
|
1271
|
-
}
|
|
1272
|
-
catch (error) {
|
|
1273
|
-
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Failed to persist inbound recovery cursor:`, error instanceof Error ? error.message : error);
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
1309
|
config.onInputCompleted?.(input.sourceMessageId);
|
|
1277
1310
|
}
|
|
1278
1311
|
/**
|
|
@@ -1935,7 +1968,7 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1935
1968
|
function hasInterruptibleSdkInput() {
|
|
1936
1969
|
return session.activeInput !== null && session.dispatchingInput === null;
|
|
1937
1970
|
}
|
|
1938
|
-
enqueueInboundMessage = (msg, intent = 'queue', sourceMessageId = null, markAccepted = false, isOwnerTurn = false, requestingUserId = null, turnModes = {}) => {
|
|
1971
|
+
enqueueInboundMessage = (msg, intent = 'queue', sourceMessageId = null, markAccepted = false, isOwnerTurn = false, requestingUserId = null, turnModes = {}, replyContactTarget) => {
|
|
1939
1972
|
session.lastActivity = Date.now();
|
|
1940
1973
|
const input = createClaudeInputEnvelope({
|
|
1941
1974
|
kind: 'canon',
|
|
@@ -1945,6 +1978,7 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1945
1978
|
markAccepted,
|
|
1946
1979
|
isOwnerTurn,
|
|
1947
1980
|
requestingUserId,
|
|
1981
|
+
replyContactTarget,
|
|
1948
1982
|
...turnModes,
|
|
1949
1983
|
});
|
|
1950
1984
|
const decision = decideClaudeInboundDispatch({
|
|
@@ -2533,6 +2567,15 @@ export async function main() {
|
|
|
2533
2567
|
hostMode: true,
|
|
2534
2568
|
rtdb,
|
|
2535
2569
|
});
|
|
2570
|
+
const recoveryCheckpointTrackers = new Map();
|
|
2571
|
+
function recoveryCheckpointsFor(conversationId) {
|
|
2572
|
+
let tracker = recoveryCheckpointTrackers.get(conversationId);
|
|
2573
|
+
if (!tracker) {
|
|
2574
|
+
tracker = createClaudeRecoveryCheckpointTracker((messageId) => persistConversationRecoveryCursor(conversationId, messageId));
|
|
2575
|
+
recoveryCheckpointTrackers.set(conversationId, tracker);
|
|
2576
|
+
}
|
|
2577
|
+
return tracker;
|
|
2578
|
+
}
|
|
2536
2579
|
let streamConnected = false;
|
|
2537
2580
|
const hostAvailableExecutionModes = [
|
|
2538
2581
|
...EXECUTION_ENVIRONMENT_MODES,
|
|
@@ -2844,6 +2887,7 @@ export async function main() {
|
|
|
2844
2887
|
ownerName,
|
|
2845
2888
|
membershipChange: pendingMembershipChanges.get(input.conversationId) ?? null,
|
|
2846
2889
|
groupContextMode: getGroupContextMode(input.conversationId, conversation),
|
|
2890
|
+
renderOptions: input.renderOptions,
|
|
2847
2891
|
});
|
|
2848
2892
|
}
|
|
2849
2893
|
function closeSession(conversationId, options = {}) {
|
|
@@ -2944,10 +2988,12 @@ export async function main() {
|
|
|
2944
2988
|
const session = sessions.get(conversationId);
|
|
2945
2989
|
if (!session || session.pendingInputs.length === 0)
|
|
2946
2990
|
return;
|
|
2947
|
-
const
|
|
2948
|
-
|
|
2949
|
-
if (session.pendingInputs.length === before)
|
|
2991
|
+
const removed = session.pendingInputs.filter((input) => input.sourceMessageId === sourceMessageId);
|
|
2992
|
+
if (removed.length === 0)
|
|
2950
2993
|
return;
|
|
2994
|
+
session.pendingInputs = session.pendingInputs.filter((input) => input.sourceMessageId !== sourceMessageId);
|
|
2995
|
+
for (const input of removed)
|
|
2996
|
+
session.markInputCompleted(input);
|
|
2951
2997
|
session.writeTurn();
|
|
2952
2998
|
}
|
|
2953
2999
|
function evictOldestIdle() {
|
|
@@ -3045,9 +3091,9 @@ export async function main() {
|
|
|
3045
3091
|
runtimeInputManager,
|
|
3046
3092
|
canRequestApproval: () => canRequestCanonApproval(conversationId),
|
|
3047
3093
|
runtimeId,
|
|
3048
|
-
recoveryBaseCwd: workingDir,
|
|
3049
3094
|
ownerId,
|
|
3050
3095
|
onInputCompleted: (sourceMessageId) => {
|
|
3096
|
+
recoveryCheckpointsFor(conversationId).settle(sourceMessageId);
|
|
3051
3097
|
settleInboundMessageId(sourceMessageId, true);
|
|
3052
3098
|
},
|
|
3053
3099
|
publishSessionSnapshots: (ids) => { void publishSessionSnapshots(ids); },
|
|
@@ -3105,13 +3151,15 @@ export async function main() {
|
|
|
3105
3151
|
}
|
|
3106
3152
|
const sender = m.senderName || m.senderId;
|
|
3107
3153
|
const isOwner = m.isOwner ?? (ownerId != null && m.senderId === ownerId);
|
|
3108
|
-
const
|
|
3154
|
+
const renderOptions = ownerBoundRenderOptions(isOwner);
|
|
3155
|
+
const content = renderInboundContent(m, materialized, renderOptions);
|
|
3109
3156
|
const hydrated = await loadHydratedInboundContext({
|
|
3110
3157
|
conversationId: input.conversationId,
|
|
3111
3158
|
message: m,
|
|
3112
3159
|
senderName: sender,
|
|
3113
3160
|
isOwner,
|
|
3114
3161
|
hydratedPage: input.hydratedPage,
|
|
3162
|
+
renderOptions,
|
|
3115
3163
|
});
|
|
3116
3164
|
const behavior = input.hydratedPage?.behavior ?? hydrated.behavior;
|
|
3117
3165
|
const activeSelfContextId = hydrated.activeSelfContextId;
|
|
@@ -3121,6 +3169,7 @@ export async function main() {
|
|
|
3121
3169
|
agentId,
|
|
3122
3170
|
conversationId: input.conversationId,
|
|
3123
3171
|
logPrefix: `[canon-host] [${input.conversationId.slice(0, 8)}]`,
|
|
3172
|
+
renderOptions,
|
|
3124
3173
|
});
|
|
3125
3174
|
const replyContext = replyMedia.replyContext;
|
|
3126
3175
|
const participantContext = hydrated.participantContext;
|
|
@@ -3153,7 +3202,7 @@ export async function main() {
|
|
|
3153
3202
|
return 'handled';
|
|
3154
3203
|
}
|
|
3155
3204
|
session.activeSelfContextId = activeSelfContextId;
|
|
3156
|
-
const
|
|
3205
|
+
const basePromptText = buildCanonPrompt({
|
|
3157
3206
|
content,
|
|
3158
3207
|
conversationId: input.conversationId,
|
|
3159
3208
|
participantContext,
|
|
@@ -3164,6 +3213,12 @@ export async function main() {
|
|
|
3164
3213
|
replyContext,
|
|
3165
3214
|
message: m,
|
|
3166
3215
|
});
|
|
3216
|
+
const lifecycleContext = isOwner
|
|
3217
|
+
? formatPendingContactLifecycleContext(takeLocalRuntimeContactLifecycleEvents(runtimeId, input.conversationId))
|
|
3218
|
+
: null;
|
|
3219
|
+
const promptText = lifecycleContext
|
|
3220
|
+
? `${basePromptText}\n\n${lifecycleContext}`
|
|
3221
|
+
: basePromptText;
|
|
3167
3222
|
const messageContent = await buildCanonUserContent({
|
|
3168
3223
|
promptText,
|
|
3169
3224
|
materialized: [...replyMedia.materialized, ...materialized],
|
|
@@ -3181,7 +3236,7 @@ export async function main() {
|
|
|
3181
3236
|
senderId: m.senderId,
|
|
3182
3237
|
senderName: m.senderName,
|
|
3183
3238
|
}),
|
|
3184
|
-
}, deliveryIntent, m.id ?? null, shouldMarkAccepted, isOwner, m.senderType === 'human' ? m.senderId : null, turnModes);
|
|
3239
|
+
}, deliveryIntent, m.id ?? null, shouldMarkAccepted, isOwner, m.senderType === 'human' ? m.senderId : null, turnModes, ownerBoundReplyContactTarget(replyContext));
|
|
3185
3240
|
return 'queued';
|
|
3186
3241
|
}
|
|
3187
3242
|
const acceptedInboundMessageIds = new Set();
|
|
@@ -3239,16 +3294,65 @@ export async function main() {
|
|
|
3239
3294
|
workspaceId: CLAUDE_RECOVERY_CURSOR_WORKSPACE_ID,
|
|
3240
3295
|
})?.lastInboundMessageId ?? null;
|
|
3241
3296
|
}
|
|
3297
|
+
function persistConversationRecoveryCursor(conversationId, messageId) {
|
|
3298
|
+
try {
|
|
3299
|
+
saveRuntimeSessionState(runtimeId, {
|
|
3300
|
+
conversationId,
|
|
3301
|
+
baseCwd: workingDir,
|
|
3302
|
+
workspaceId: CLAUDE_RECOVERY_CURSOR_WORKSPACE_ID,
|
|
3303
|
+
lastInboundMessageId: messageId,
|
|
3304
|
+
});
|
|
3305
|
+
return true;
|
|
3306
|
+
}
|
|
3307
|
+
catch (error) {
|
|
3308
|
+
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Failed to persist inbound recovery cursor:`, error instanceof Error ? error.message : error);
|
|
3309
|
+
return false;
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
let startupRecoveryComplete = false;
|
|
3242
3313
|
async function performMissedInboundRecovery() {
|
|
3314
|
+
const knownBeforeRefresh = new Set(knownConversationIds);
|
|
3315
|
+
await refreshKnownConversationIds(true);
|
|
3316
|
+
const conversationsDiscoveredWhileOffline = startupRecoveryComplete
|
|
3317
|
+
? new Set([...knownConversationIds].filter((id) => !knownBeforeRefresh.has(id)))
|
|
3318
|
+
: new Set();
|
|
3319
|
+
try {
|
|
3320
|
+
let cursor = readLocalRuntimeEntry(runtimeId)?.contactLifecycleCursor ?? null;
|
|
3321
|
+
for (let pageNumber = 0; pageNumber < 10; pageNumber += 1) {
|
|
3322
|
+
const page = await client.listContactRequestLifecyclePage({
|
|
3323
|
+
cursor,
|
|
3324
|
+
limit: 100,
|
|
3325
|
+
});
|
|
3326
|
+
await reconcileContactLifecycleEvents({
|
|
3327
|
+
requests: page.requests,
|
|
3328
|
+
requesterId: agentId,
|
|
3329
|
+
record: (request) => recordLocalRuntimeContactLifecycleEvent(runtimeId, request),
|
|
3330
|
+
});
|
|
3331
|
+
cursor = page.nextCursor;
|
|
3332
|
+
saveLocalRuntimeContactLifecycleCursor(runtimeId, cursor);
|
|
3333
|
+
if (!page.hasMore)
|
|
3334
|
+
break;
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
catch (error) {
|
|
3338
|
+
console.error('[canon-host] Contact lifecycle recovery failed:', error instanceof Error ? error.message : error);
|
|
3339
|
+
}
|
|
3243
3340
|
for (const conversationId of knownConversationIds) {
|
|
3341
|
+
const recoveryCheckpoints = recoveryCheckpointsFor(conversationId);
|
|
3342
|
+
const recoveryBatch = recoveryCheckpoints.reserveBatch();
|
|
3244
3343
|
try {
|
|
3245
3344
|
const recovered = await collectMissedInboundMessages({
|
|
3246
3345
|
fetchPage: (before) => client.getMessagesPage(conversationId, STARTUP_RECOVERY_PAGE_SIZE, before),
|
|
3247
3346
|
cursor: persistedConversationCursor(conversationId),
|
|
3248
3347
|
agentId,
|
|
3348
|
+
requireContiguousCursor: true,
|
|
3349
|
+
noCursorMode: conversationsDiscoveredWhileOffline.has(conversationId)
|
|
3350
|
+
? 'bounded-window'
|
|
3351
|
+
: 'latest-only',
|
|
3249
3352
|
});
|
|
3250
|
-
|
|
3251
|
-
|
|
3353
|
+
recoveryBatch.commit(recovered.messages.map((message) => message.id), recovered.mode === 'incomplete-gap' ? recovered.recoveryCursor : null);
|
|
3354
|
+
if (recovered.mode === 'incomplete-gap') {
|
|
3355
|
+
console.error(`[canon-host] [${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'}`);
|
|
3252
3356
|
}
|
|
3253
3357
|
for (const message of recovered.messages) {
|
|
3254
3358
|
if (!claimInboundMessageId(message.id))
|
|
@@ -3268,8 +3372,10 @@ export async function main() {
|
|
|
3268
3372
|
}) === 'queued';
|
|
3269
3373
|
}
|
|
3270
3374
|
}
|
|
3271
|
-
if (!queued)
|
|
3375
|
+
if (!queued) {
|
|
3376
|
+
recoveryCheckpoints.settle(message.id);
|
|
3272
3377
|
settleInboundMessageId(message.id, true);
|
|
3378
|
+
}
|
|
3273
3379
|
}
|
|
3274
3380
|
catch (error) {
|
|
3275
3381
|
settleInboundMessageId(message.id, false);
|
|
@@ -3281,21 +3387,14 @@ export async function main() {
|
|
|
3281
3387
|
}
|
|
3282
3388
|
}
|
|
3283
3389
|
catch (error) {
|
|
3390
|
+
recoveryBatch.cancel();
|
|
3284
3391
|
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Startup recovery failed:`, error instanceof Error ? error.message : error);
|
|
3285
3392
|
}
|
|
3286
3393
|
}
|
|
3394
|
+
startupRecoveryComplete = true;
|
|
3287
3395
|
}
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
if (recoveryInFlight)
|
|
3291
|
-
return recoveryInFlight;
|
|
3292
|
-
recoveryInFlight = performMissedInboundRecovery().finally(() => {
|
|
3293
|
-
recoveryInFlight = null;
|
|
3294
|
-
});
|
|
3295
|
-
return recoveryInFlight;
|
|
3296
|
-
}
|
|
3297
|
-
const shouldRecoverAfterStreamConnect = createReconnectRecoveryGate();
|
|
3298
|
-
await recoverMissedInboundMessages();
|
|
3396
|
+
const reconnectRecovery = createReconnectRecoveryCoordinator(performMissedInboundRecovery);
|
|
3397
|
+
await reconnectRecovery.recoverNow();
|
|
3299
3398
|
// ── Connect SSE stream for inbound Canon messages ──
|
|
3300
3399
|
const stream = new CanonStream({
|
|
3301
3400
|
apiKey,
|
|
@@ -3306,11 +3405,13 @@ export async function main() {
|
|
|
3306
3405
|
const m = payload.message;
|
|
3307
3406
|
if (m.senderId === agentId)
|
|
3308
3407
|
return;
|
|
3408
|
+
const convoId = payload.conversationId;
|
|
3309
3409
|
if (!claimInboundMessageId(m.id))
|
|
3310
3410
|
return;
|
|
3311
|
-
|
|
3411
|
+
recoveryCheckpointsFor(convoId).track(m.id);
|
|
3312
3412
|
knownConversationIds.add(convoId);
|
|
3313
3413
|
if (handleRuntimeReplyMessage(convoId, m)) {
|
|
3414
|
+
recoveryCheckpointsFor(convoId).settle(m.id);
|
|
3314
3415
|
settleInboundMessageId(m.id, true);
|
|
3315
3416
|
return;
|
|
3316
3417
|
}
|
|
@@ -3318,6 +3419,7 @@ export async function main() {
|
|
|
3318
3419
|
const isOwner = m.isOwner ?? (ownerId != null && m.senderId === ownerId);
|
|
3319
3420
|
if (payload.turnDispatch && payload.turnDispatch.kind !== 'run_turn') {
|
|
3320
3421
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Ignoring server-dispatched observe-only message: ${payload.turnDispatch.reason}`);
|
|
3422
|
+
recoveryCheckpointsFor(convoId).settle(m.id);
|
|
3321
3423
|
settleInboundMessageId(m.id, true);
|
|
3322
3424
|
return;
|
|
3323
3425
|
}
|
|
@@ -3334,7 +3436,8 @@ export async function main() {
|
|
|
3334
3436
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to materialize media:`, error instanceof Error ? error.message : error);
|
|
3335
3437
|
}
|
|
3336
3438
|
}
|
|
3337
|
-
const
|
|
3439
|
+
const renderOptions = ownerBoundRenderOptions(isOwner);
|
|
3440
|
+
const content = renderInboundContent(m, materialized, renderOptions);
|
|
3338
3441
|
const hydrated = await loadHydratedInboundContext({
|
|
3339
3442
|
conversationId: convoId,
|
|
3340
3443
|
message: m,
|
|
@@ -3343,6 +3446,7 @@ export async function main() {
|
|
|
3343
3446
|
activeSelfContextId: payload.activeSelfContextId,
|
|
3344
3447
|
selfContexts: payload.selfContexts,
|
|
3345
3448
|
provenance: payload.provenance,
|
|
3449
|
+
renderOptions,
|
|
3346
3450
|
});
|
|
3347
3451
|
const behavior = payload.behavior ?? hydrated.behavior;
|
|
3348
3452
|
const activeSelfContextId = hydrated.activeSelfContextId;
|
|
@@ -3352,6 +3456,7 @@ export async function main() {
|
|
|
3352
3456
|
agentId,
|
|
3353
3457
|
conversationId: convoId,
|
|
3354
3458
|
logPrefix: `[canon-host] [${convoId.slice(0, 8)}]`,
|
|
3459
|
+
renderOptions,
|
|
3355
3460
|
});
|
|
3356
3461
|
const replyContext = replyMedia.replyContext;
|
|
3357
3462
|
const participantContext = hydrated.participantContext;
|
|
@@ -3390,7 +3495,7 @@ export async function main() {
|
|
|
3390
3495
|
return false;
|
|
3391
3496
|
}
|
|
3392
3497
|
session.activeSelfContextId = activeSelfContextId;
|
|
3393
|
-
const
|
|
3498
|
+
const basePromptText = buildCanonPrompt({
|
|
3394
3499
|
content,
|
|
3395
3500
|
conversationId: convoId,
|
|
3396
3501
|
participantContext,
|
|
@@ -3401,6 +3506,12 @@ export async function main() {
|
|
|
3401
3506
|
replyContext,
|
|
3402
3507
|
message: m,
|
|
3403
3508
|
});
|
|
3509
|
+
const lifecycleContext = isOwner
|
|
3510
|
+
? formatPendingContactLifecycleContext(takeLocalRuntimeContactLifecycleEvents(runtimeId, convoId))
|
|
3511
|
+
: null;
|
|
3512
|
+
const promptText = lifecycleContext
|
|
3513
|
+
? `${basePromptText}\n\n${lifecycleContext}`
|
|
3514
|
+
: basePromptText;
|
|
3404
3515
|
const messageContent = await buildCanonUserContent({
|
|
3405
3516
|
promptText,
|
|
3406
3517
|
materialized: [...replyMedia.materialized, ...materialized],
|
|
@@ -3418,11 +3529,13 @@ export async function main() {
|
|
|
3418
3529
|
senderId: m.senderId,
|
|
3419
3530
|
senderName: m.senderName,
|
|
3420
3531
|
}),
|
|
3421
|
-
}, deliveryIntent, m.id ?? null, shouldMarkAccepted, isOwner, m.senderType === 'human' ? m.senderId : null, turnModes);
|
|
3532
|
+
}, deliveryIntent, m.id ?? null, shouldMarkAccepted, isOwner, m.senderType === 'human' ? m.senderId : null, turnModes, ownerBoundReplyContactTarget(replyContext));
|
|
3422
3533
|
return true;
|
|
3423
3534
|
})().then((queued) => {
|
|
3424
|
-
if (!queued)
|
|
3535
|
+
if (!queued) {
|
|
3536
|
+
recoveryCheckpointsFor(convoId).settle(m.id);
|
|
3425
3537
|
settleInboundMessageId(m.id, true);
|
|
3538
|
+
}
|
|
3426
3539
|
}).catch((error) => {
|
|
3427
3540
|
settleInboundMessageId(m.id, false);
|
|
3428
3541
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to process inbound message:`, error instanceof Error ? error.message : error);
|
|
@@ -3434,12 +3547,24 @@ export async function main() {
|
|
|
3434
3547
|
onConversationUpdated: (payload) => {
|
|
3435
3548
|
handleConversationUpdated(payload);
|
|
3436
3549
|
},
|
|
3550
|
+
onContactRequestUpdated: (payload) => {
|
|
3551
|
+
if (payload.requesterId !== agentId || !payload.sourceConversationId)
|
|
3552
|
+
return;
|
|
3553
|
+
recordLocalRuntimeContactLifecycleEvent(runtimeId, payload);
|
|
3554
|
+
},
|
|
3555
|
+
onReplayExpired: () => {
|
|
3556
|
+
void reconnectRecovery.onReplayExpired().catch((error) => {
|
|
3557
|
+
console.error('[canon-host] Replay-expired recovery failed:', error);
|
|
3558
|
+
});
|
|
3559
|
+
},
|
|
3437
3560
|
onConnected: () => {
|
|
3438
3561
|
streamConnected = true;
|
|
3439
3562
|
void publishRuntimeHeartbeat();
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3563
|
+
const recovery = reconnectRecovery.onConnected();
|
|
3564
|
+
if (recovery)
|
|
3565
|
+
void recovery.catch((error) => {
|
|
3566
|
+
console.error('[canon-host] Reconnect recovery failed:', error);
|
|
3567
|
+
});
|
|
3443
3568
|
console.error('[canon-host] SSE connected');
|
|
3444
3569
|
},
|
|
3445
3570
|
onDisconnected: () => {
|
package/dist/register.js
CHANGED
|
@@ -37,6 +37,7 @@ After approval, start it with CANON_AGENT=<profile> canon-claude --cwd /path/to/
|
|
|
37
37
|
const OPTIONS = {
|
|
38
38
|
moduleUrl: import.meta.url,
|
|
39
39
|
clientType: 'claude-code',
|
|
40
|
+
sessionSetupPolicy: 'runtime_descriptor_required',
|
|
40
41
|
cliName: 'canon-register',
|
|
41
42
|
hostBinName: 'canon-claude',
|
|
42
43
|
developerInfo: 'Claude Code plugin',
|
package/dist/server.js
CHANGED
|
@@ -16,7 +16,8 @@ import { ApprovalHttpServer } from './approval-server.js';
|
|
|
16
16
|
import { renderClaudeInboundContent } from './canon-user-content.js';
|
|
17
17
|
import { runCli } from '@canonmsg/core';
|
|
18
18
|
import { parseReplyArgs, parseSendMessageArgs, parseSetTypingArgs, } from './mcp-args.js';
|
|
19
|
-
import { canonVerbToolDefinitions, executeCanonVerbTool, isCanonToolVerb, stampSendToTurnComplete, } from '@canonmsg/agent-tools';
|
|
19
|
+
import { OWNER_BOUND_CANON_COMMUNICATION_VERBS, canonVerbToolDefinitions, executeCanonVerbTool, isCanonToolVerb, stampSendToTurnComplete, } from '@canonmsg/agent-tools';
|
|
20
|
+
const OWNER_BOUND_COMMUNICATION_VERBS = new Set(OWNER_BOUND_CANON_COMMUNICATION_VERBS);
|
|
20
21
|
const HELP = `canon-channel-server — Claude Code MCP channel server for Canon
|
|
21
22
|
|
|
22
23
|
USAGE
|
|
@@ -174,7 +175,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
174
175
|
// Canonical verbs — projections of canon.verbs.v1 (@canonmsg/agent-tools).
|
|
175
176
|
// Channel mode owns no turn, so `no_reply` is an ack-only no-op here: the
|
|
176
177
|
// standalone session has no final delivery for it to suppress.
|
|
177
|
-
...canonVerbToolDefinitions(),
|
|
178
|
+
...canonVerbToolDefinitions().filter((definition) => !OWNER_BOUND_COMMUNICATION_VERBS.has(definition.name)),
|
|
178
179
|
],
|
|
179
180
|
}));
|
|
180
181
|
// ── Tool handlers ──────────────────────────────────────────────────────
|
|
@@ -314,6 +315,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
314
315
|
}
|
|
315
316
|
default: {
|
|
316
317
|
const name = request.params.name;
|
|
318
|
+
if (OWNER_BOUND_COMMUNICATION_VERBS.has(name)) {
|
|
319
|
+
return toolArgumentError(`${name} requires an owner-authored Canon turn and is unavailable in standalone channel mode.`);
|
|
320
|
+
}
|
|
317
321
|
if (isCanonToolVerb(name)) {
|
|
318
322
|
const rawArgs = args && typeof args === 'object' ? { ...args } : {};
|
|
319
323
|
const verbArgs = name === 'send_to' ? stampSendToTurnComplete(rawArgs) : rawArgs;
|
package/dist/session-state.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PermissionMode, SDKMessageOrigin, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
|
|
2
2
|
import type { DeliveryIntent, ModelOption, RuntimeStreamingPayload, TurnLifecycleState, TurnOutputBlock, TurnOutputSnapshot, TurnVerbosity } from '@canonmsg/core';
|
|
3
|
+
import type { OwnerBoundCanonContactTarget } from '@canonmsg/agent-tools';
|
|
3
4
|
import type { TurnArtifactRoutingDecision, TurnArtifactSnapshot } from '@canonmsg/coding-agent-host';
|
|
4
5
|
export type ClaudeInputKind = 'seed' | 'canon';
|
|
5
6
|
export type ClaudeArtifactRoutingMode = 'workspace-generated' | 'disabled';
|
|
@@ -23,6 +24,8 @@ export interface ClaudeInputEnvelope {
|
|
|
23
24
|
turnKey: string;
|
|
24
25
|
isOwnerTurn: boolean;
|
|
25
26
|
requestingUserId: string | null;
|
|
27
|
+
/** Trusted contact-card target resolved from the owner message's reply. */
|
|
28
|
+
replyContactTarget?: OwnerBoundCanonContactTarget;
|
|
26
29
|
}
|
|
27
30
|
/**
|
|
28
31
|
* The per-turn modes an inbound message resolves from its participant context,
|
|
@@ -102,6 +105,7 @@ export declare function createClaudeInputEnvelope(input: {
|
|
|
102
105
|
artifactBaseline?: TurnArtifactSnapshot | null;
|
|
103
106
|
isOwnerTurn?: boolean;
|
|
104
107
|
requestingUserId?: string | null;
|
|
108
|
+
replyContactTarget?: OwnerBoundCanonContactTarget;
|
|
105
109
|
}): ClaudeInputEnvelope;
|
|
106
110
|
export declare function resolveClaudeTurnResponseRouting(turn: Pick<ClaudeInputEnvelope, 'kind' | 'isOwnerTurn' | 'requestingUserId'> | null | undefined, ownerId: string | null | undefined, ownerOnly?: boolean): {
|
|
107
111
|
isOwnerTurn: boolean;
|
package/dist/session-state.js
CHANGED
|
@@ -28,6 +28,9 @@ export function createClaudeInputEnvelope(input) {
|
|
|
28
28
|
requestingUserId: input.kind === 'canon' && input.requestingUserId
|
|
29
29
|
? input.requestingUserId
|
|
30
30
|
: null,
|
|
31
|
+
...(input.kind === 'canon' && input.replyContactTarget
|
|
32
|
+
? { replyContactTarget: input.replyContactTarget }
|
|
33
|
+
: {}),
|
|
31
34
|
turnKey: input.kind === 'canon' && sourceMessageId
|
|
32
35
|
? `canon:${sourceMessageId}`
|
|
33
36
|
: `${input.kind}:${fallbackId}`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/claude-code-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.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": "^8.
|
|
35
|
-
"@canonmsg/agent-tools": "^0.
|
|
36
|
-
"@canonmsg/coding-agent-host": "^0.
|
|
37
|
-
"@canonmsg/core": "^10.
|
|
34
|
+
"@canonmsg/agent-sdk": "^8.9.0",
|
|
35
|
+
"@canonmsg/agent-tools": "^0.6.0",
|
|
36
|
+
"@canonmsg/coding-agent-host": "^0.6.0",
|
|
37
|
+
"@canonmsg/core": "^10.7.0",
|
|
38
38
|
"@canonmsg/rich-cards": "^0.10.2",
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.30.0"
|
|
40
40
|
},
|