@canonmsg/claude-code-plugin 0.35.0 → 0.36.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/host.js +192 -339
- package/dist/server.js +2 -3
- package/package.json +7 -7
package/dist/host.js
CHANGED
|
@@ -27,10 +27,10 @@ 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,
|
|
30
|
+
import { captureTurnArtifactSnapshot, createTurnArtifactRouter, IDLE_TIMEOUT_MS, createRecoveryCheckpointTracker, createReconnectRecoveryCoordinator, } 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, 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,
|
|
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, isPendingCanonOperation, sendMessageWithRetry, sendMessageWithRetryChunked, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, resolveLocalRuntimeSessionState, saveRuntimeSessionState, clearRuntimeSessionState, publishHostSessionSnapshots, renderCodingHostInboundPrompt, resolveConfiguredWorkspaceCwd, resolveTurnVerbosity, 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';
|
|
@@ -1332,7 +1332,7 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1332
1332
|
const deliveredMessageIds = chunkedFailure
|
|
1333
1333
|
? chunkedFailure.deliveredMessageIds
|
|
1334
1334
|
: resume.deliveredMessageIds;
|
|
1335
|
-
const failure = classifyFinalDeliveryFailure(cause);
|
|
1335
|
+
const failure = isPendingCanonOperation(cause) ? 'retryable' : classifyFinalDeliveryFailure(cause);
|
|
1336
1336
|
if (failure === 'permanent') {
|
|
1337
1337
|
// Re-sending is pointless — the same request fails the same way. Surface
|
|
1338
1338
|
// what can be surfaced and finish the turn instead of burning the retry
|
|
@@ -1635,7 +1635,16 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1635
1635
|
.updateMessageDisposition(conversationId, input.sourceMessageId, 'rejected')
|
|
1636
1636
|
.catch(() => { });
|
|
1637
1637
|
}
|
|
1638
|
-
|
|
1638
|
+
if (input.kind === 'canon' && input.sourceMessageId) {
|
|
1639
|
+
const id = `message:${conversationId}:${input.sourceMessageId}`;
|
|
1640
|
+
void client.getEndpoint().then(async (engine) => {
|
|
1641
|
+
const entry = await engine.getInbound(id);
|
|
1642
|
+
if (entry && ['observed', 'offered', 'deferred'].includes(entry.state)) {
|
|
1643
|
+
await engine.setInboundState(id, 'deferred', 'native-preflight');
|
|
1644
|
+
config.onInputDeferred?.(input.sourceMessageId);
|
|
1645
|
+
}
|
|
1646
|
+
}).catch((error) => console.error('[canon-host] Failed to defer native input:', error));
|
|
1647
|
+
}
|
|
1639
1648
|
// resetTurnToIdle releases the slot, tears the published turn down (typing
|
|
1640
1649
|
// signal, seeded /streaming, turn state) and drains the queue. On a closed
|
|
1641
1650
|
// session it is a no-op, which is correct: the teardown owns that path.
|
|
@@ -1645,7 +1654,16 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1645
1654
|
return dispatchClaudeInput(session, input, {
|
|
1646
1655
|
openTurn,
|
|
1647
1656
|
markAccepted: (dispatched) => markQueuedMessageAccepted(dispatched.sourceMessageId, dispatched.markAccepted),
|
|
1648
|
-
prepareArtifacts:
|
|
1657
|
+
prepareArtifacts: async (input) => {
|
|
1658
|
+
await prepareArtifactsForInput(input);
|
|
1659
|
+
if (input.kind === 'canon' && input.sourceMessageId) {
|
|
1660
|
+
const engine = await client.getEndpoint();
|
|
1661
|
+
const id = `message:${conversationId}:${input.sourceMessageId}`;
|
|
1662
|
+
if (!await engine.claimInbound(id)) {
|
|
1663
|
+
throw new Error('Canon input already has an execution owner.');
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
},
|
|
1649
1667
|
send: sendInput,
|
|
1650
1668
|
abandon: abandonDispatchedInput,
|
|
1651
1669
|
});
|
|
@@ -2230,7 +2248,8 @@ export async function main() {
|
|
|
2230
2248
|
throw error;
|
|
2231
2249
|
}
|
|
2232
2250
|
// ── Canon API client + RTDB auth ──
|
|
2233
|
-
const client = new CanonClient(apiKey, baseUrl);
|
|
2251
|
+
const client = new CanonClient(apiKey, baseUrl, { environmentId: resolvedAgent.environmentId, streamUrl });
|
|
2252
|
+
const endpoint = await client.getEndpoint();
|
|
2234
2253
|
const rtdb = initRTDBAuth(client, { rtdbUrl, firebaseApiKey });
|
|
2235
2254
|
const typingSignals = createTypingStatusPublisher({
|
|
2236
2255
|
setTyping: (conversationId, typing, status) => status
|
|
@@ -2554,6 +2573,8 @@ export async function main() {
|
|
|
2554
2573
|
...payload.changes,
|
|
2555
2574
|
memberIds,
|
|
2556
2575
|
});
|
|
2576
|
+
void client.rememberConversation(conversationCache.get(payload.conversationId))
|
|
2577
|
+
.catch((error) => console.error('[canon] Failed to persist conversation update:', error));
|
|
2557
2578
|
}
|
|
2558
2579
|
if (membershipChange) {
|
|
2559
2580
|
pendingMembershipChanges.set(payload.conversationId, membershipChange);
|
|
@@ -2846,9 +2867,14 @@ export async function main() {
|
|
|
2846
2867
|
return roster ? buildCompactGroupContextLines(roster, 'initial').join('\n') : '';
|
|
2847
2868
|
},
|
|
2848
2869
|
currentArtifactAudience: () => ({ memberIds: conversationCache.get(conversationId)?.memberIds, agentId, ownerId }),
|
|
2870
|
+
onInputDeferred: (sourceMessageId) => settleInboundMessageId(sourceMessageId, false),
|
|
2849
2871
|
onInputCompleted: (sourceMessageId) => {
|
|
2850
2872
|
recoveryCheckpointsFor(conversationId).settle(sourceMessageId);
|
|
2851
2873
|
settleInboundMessageId(sourceMessageId, true);
|
|
2874
|
+
const id = `message:${conversationId}:${sourceMessageId}`;
|
|
2875
|
+
void endpoint.getInbound(id).then((entry) => entry
|
|
2876
|
+
? endpoint.setInboundState(id, 'settled', 'native-completed') : undefined)
|
|
2877
|
+
.catch((error) => console.error('[canon-host] Failed to settle native input:', error));
|
|
2852
2878
|
},
|
|
2853
2879
|
publishSessionSnapshots: (ids) => { void publishSessionSnapshots(ids); },
|
|
2854
2880
|
}, resumeId);
|
|
@@ -2873,134 +2899,6 @@ export async function main() {
|
|
|
2873
2899
|
pendingSessionCreations.delete(conversationId);
|
|
2874
2900
|
}
|
|
2875
2901
|
}
|
|
2876
|
-
async function enqueueRecoveredInboundMessage(input) {
|
|
2877
|
-
const m = {
|
|
2878
|
-
id: input.message.id,
|
|
2879
|
-
senderId: input.message.senderId,
|
|
2880
|
-
senderName: input.message.senderName,
|
|
2881
|
-
senderType: input.message.senderType,
|
|
2882
|
-
isOwner: input.message.isOwner,
|
|
2883
|
-
text: input.message.text ?? undefined,
|
|
2884
|
-
contentType: input.message.contentType,
|
|
2885
|
-
attachments: input.message.attachments,
|
|
2886
|
-
mentions: input.message.mentions,
|
|
2887
|
-
reactions: input.message.reactions,
|
|
2888
|
-
replyTo: input.message.replyTo ?? undefined,
|
|
2889
|
-
replyToPosition: input.message.replyToPosition ?? undefined,
|
|
2890
|
-
forwarded: input.message.forwarded,
|
|
2891
|
-
forwardedFrom: input.message.forwardedFrom,
|
|
2892
|
-
metadata: input.message.metadata,
|
|
2893
|
-
contactCard: input.message.contactCard,
|
|
2894
|
-
createdAt: input.message.createdAt,
|
|
2895
|
-
};
|
|
2896
|
-
if (m.senderId === agentId)
|
|
2897
|
-
return 'handled';
|
|
2898
|
-
let materialized = [];
|
|
2899
|
-
if (m.id) {
|
|
2900
|
-
try {
|
|
2901
|
-
materialized = await materializeMessageMedia({
|
|
2902
|
-
id: m.id,
|
|
2903
|
-
attachments: m.attachments ?? [],
|
|
2904
|
-
}, { agentId, conversationId: input.conversationId });
|
|
2905
|
-
}
|
|
2906
|
-
catch (error) {
|
|
2907
|
-
console.error(`[canon-host] [${input.conversationId.slice(0, 8)}] Failed to materialize recovered media:`, error instanceof Error ? error.message : error);
|
|
2908
|
-
}
|
|
2909
|
-
}
|
|
2910
|
-
const sender = m.senderName || m.senderId;
|
|
2911
|
-
const isOwner = m.isOwner ?? (ownerId != null && m.senderId === ownerId);
|
|
2912
|
-
const content = renderInboundContent(m, materialized);
|
|
2913
|
-
const hydrated = await loadHydratedInboundContext({
|
|
2914
|
-
conversationId: input.conversationId,
|
|
2915
|
-
message: m,
|
|
2916
|
-
senderName: sender,
|
|
2917
|
-
isOwner,
|
|
2918
|
-
hydratedPage: input.hydratedPage,
|
|
2919
|
-
});
|
|
2920
|
-
const behavior = input.hydratedPage?.behavior ?? hydrated.behavior;
|
|
2921
|
-
const activeSelfContextId = hydrated.activeSelfContextId;
|
|
2922
|
-
const selfContexts = hydrated.selfContexts;
|
|
2923
|
-
const replyMedia = await materializePromptReplyContext({
|
|
2924
|
-
replyContext: hydrated.replyContext,
|
|
2925
|
-
agentId,
|
|
2926
|
-
conversationId: input.conversationId,
|
|
2927
|
-
logPrefix: `[canon-host] [${input.conversationId.slice(0, 8)}]`,
|
|
2928
|
-
});
|
|
2929
|
-
const replyContext = replyMedia.replyContext;
|
|
2930
|
-
const participantContext = hydrated.participantContext;
|
|
2931
|
-
const autoReply = decideAutoReply(participantContext, behavior);
|
|
2932
|
-
if (!autoReply.allow) {
|
|
2933
|
-
console.error(`[canon-host] [${input.conversationId.slice(0, 8)}] Suppressed recovered auto-reply: ${autoReply.reason}`);
|
|
2934
|
-
return 'handled';
|
|
2935
|
-
}
|
|
2936
|
-
markGroupContextModeUsed(input.conversationId, participantContext.groupContextMode);
|
|
2937
|
-
const turnMetadata = normalizeTurnMetadata(m.metadata);
|
|
2938
|
-
const deliveryIntent = turnMetadata?.deliveryIntent ?? 'queue';
|
|
2939
|
-
const shouldMarkAccepted = turnMetadata?.inboundDisposition === 'queued';
|
|
2940
|
-
let session;
|
|
2941
|
-
try {
|
|
2942
|
-
session = await getOrCreateSession(input.conversationId);
|
|
2943
|
-
}
|
|
2944
|
-
catch (error) {
|
|
2945
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2946
|
-
console.error(`[canon-host] [${input.conversationId.slice(0, 8)}] Failed to create recovered session: ${message}`);
|
|
2947
|
-
await runtimeState.patchAgentSessionSnapshot(input.conversationId, {
|
|
2948
|
-
configurationStatus: 'configuration_required',
|
|
2949
|
-
lastError: LOCAL_CONFIGURATION_REQUIRED_MESSAGE,
|
|
2950
|
-
}).catch(() => { });
|
|
2951
|
-
await markQueuedMessageAcceptedForConversation(input.conversationId, m.id ?? null, shouldMarkAccepted);
|
|
2952
|
-
await sendMessageWithRetry(client, input.conversationId, LOCAL_CONFIGURATION_REQUIRED_MESSAGE, {
|
|
2953
|
-
messageId: `claude-start-failed-${m.id}`,
|
|
2954
|
-
...(activeSelfContextId ? { selfContextId: activeSelfContextId } : {}),
|
|
2955
|
-
metadata: {
|
|
2956
|
-
runtimeStatus: 'configuration_required',
|
|
2957
|
-
turnSemantics: 'turn_complete',
|
|
2958
|
-
replyBehavior: 'suppress_auto_reply',
|
|
2959
|
-
},
|
|
2960
|
-
}).catch(() => { });
|
|
2961
|
-
return 'handled';
|
|
2962
|
-
}
|
|
2963
|
-
session.activeSelfContextId = activeSelfContextId;
|
|
2964
|
-
const promptText = buildCanonPrompt({
|
|
2965
|
-
content,
|
|
2966
|
-
conversationId: input.conversationId,
|
|
2967
|
-
participantContext,
|
|
2968
|
-
behavior,
|
|
2969
|
-
selfContexts,
|
|
2970
|
-
activeSelfContextId,
|
|
2971
|
-
provenance: hydrated.provenance,
|
|
2972
|
-
replyContext,
|
|
2973
|
-
message: m,
|
|
2974
|
-
});
|
|
2975
|
-
const commandInput = buildClaudeNativeCommandInput({
|
|
2976
|
-
text: m.text,
|
|
2977
|
-
senderType: m.senderType,
|
|
2978
|
-
promptText,
|
|
2979
|
-
commands: runtimeMetadata.commands,
|
|
2980
|
-
});
|
|
2981
|
-
const messageContent = await buildCanonUserContent({
|
|
2982
|
-
promptText: commandInput.promptText,
|
|
2983
|
-
materialized: [...replyMedia.materialized, ...materialized],
|
|
2984
|
-
});
|
|
2985
|
-
const turnModes = {
|
|
2986
|
-
...resolveClaudeTurnModes(participantContext),
|
|
2987
|
-
commandContext: commandInput.commandContext,
|
|
2988
|
-
};
|
|
2989
|
-
session.enqueueInbound({
|
|
2990
|
-
type: 'user',
|
|
2991
|
-
message: {
|
|
2992
|
-
role: 'user',
|
|
2993
|
-
content: messageContent,
|
|
2994
|
-
},
|
|
2995
|
-
parent_tool_use_id: null,
|
|
2996
|
-
origin: claudeOriginForCanonSender({
|
|
2997
|
-
senderType: m.senderType,
|
|
2998
|
-
senderId: m.senderId,
|
|
2999
|
-
senderName: m.senderName,
|
|
3000
|
-
}),
|
|
3001
|
-
}, deliveryIntent, m.id ?? null, shouldMarkAccepted, isOwner, m.senderType === 'human' ? m.senderId : null, turnModes);
|
|
3002
|
-
return 'queued';
|
|
3003
|
-
}
|
|
3004
2902
|
const acceptedInboundMessageIds = new Set();
|
|
3005
2903
|
const inFlightInboundMessageIds = new Set();
|
|
3006
2904
|
function claimInboundMessageId(messageId) {
|
|
@@ -3071,223 +2969,177 @@ export async function main() {
|
|
|
3071
2969
|
return false;
|
|
3072
2970
|
}
|
|
3073
2971
|
}
|
|
3074
|
-
let startupRecoveryComplete = false;
|
|
3075
2972
|
async function performMissedInboundRecovery() {
|
|
3076
|
-
const knownBeforeRefresh = new Set(knownConversationIds);
|
|
3077
2973
|
await refreshKnownConversationIds(true);
|
|
3078
|
-
const conversationsDiscoveredWhileOffline = startupRecoveryComplete
|
|
3079
|
-
? new Set([...knownConversationIds].filter((id) => !knownBeforeRefresh.has(id)))
|
|
3080
|
-
: new Set();
|
|
3081
|
-
for (const conversationId of knownConversationIds) {
|
|
3082
|
-
const recoveryCheckpoints = recoveryCheckpointsFor(conversationId);
|
|
3083
|
-
const recoveryBatch = recoveryCheckpoints.reserveBatch();
|
|
3084
|
-
try {
|
|
3085
|
-
const recovered = await collectMissedInboundMessages({
|
|
3086
|
-
fetchPage: (before) => client.getMessagesPage(conversationId, STARTUP_RECOVERY_PAGE_SIZE, before),
|
|
3087
|
-
cursor: persistedConversationCursor(conversationId),
|
|
3088
|
-
agentId,
|
|
3089
|
-
requireContiguousCursor: true,
|
|
3090
|
-
noCursorMode: conversationsDiscoveredWhileOffline.has(conversationId)
|
|
3091
|
-
? 'bounded-window'
|
|
3092
|
-
: 'latest-only',
|
|
3093
|
-
});
|
|
3094
|
-
recoveryBatch.commit(recovered.messages.map((message) => message.id), recovered.mode === 'incomplete-gap' ? recovered.recoveryCursor : null);
|
|
3095
|
-
if (recovered.mode === 'incomplete-gap') {
|
|
3096
|
-
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'}`);
|
|
3097
|
-
}
|
|
3098
|
-
for (const message of recovered.messages) {
|
|
3099
|
-
if (!claimInboundMessageId(message.id))
|
|
3100
|
-
continue;
|
|
3101
|
-
try {
|
|
3102
|
-
let queued = false;
|
|
3103
|
-
if (!handleRuntimeReplyMessage(conversationId, message)) {
|
|
3104
|
-
const trigger = shouldTriggerAgentTurn({
|
|
3105
|
-
senderType: message.senderType,
|
|
3106
|
-
metadata: message.metadata,
|
|
3107
|
-
});
|
|
3108
|
-
if (trigger.allow) {
|
|
3109
|
-
queued = await enqueueRecoveredInboundMessage({
|
|
3110
|
-
conversationId,
|
|
3111
|
-
message,
|
|
3112
|
-
hydratedPage: recovered.newestPage,
|
|
3113
|
-
}) === 'queued';
|
|
3114
|
-
}
|
|
3115
|
-
}
|
|
3116
|
-
if (!queued) {
|
|
3117
|
-
recoveryCheckpoints.settle(message.id);
|
|
3118
|
-
settleInboundMessageId(message.id, true);
|
|
3119
|
-
}
|
|
3120
|
-
}
|
|
3121
|
-
catch (error) {
|
|
3122
|
-
settleInboundMessageId(message.id, false);
|
|
3123
|
-
throw error;
|
|
3124
|
-
}
|
|
3125
|
-
}
|
|
3126
|
-
if (recovered.messages.length > 0) {
|
|
3127
|
-
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Recovered ${recovered.messages.length} inbound message(s) (${recovered.mode})`);
|
|
3128
|
-
}
|
|
3129
|
-
}
|
|
3130
|
-
catch (error) {
|
|
3131
|
-
recoveryBatch.cancel();
|
|
3132
|
-
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Startup recovery failed:`, error instanceof Error ? error.message : error);
|
|
3133
|
-
}
|
|
3134
|
-
}
|
|
3135
|
-
startupRecoveryComplete = true;
|
|
3136
2974
|
}
|
|
3137
2975
|
const reconnectRecovery = createReconnectRecoveryCoordinator(performMissedInboundRecovery);
|
|
3138
2976
|
await reconnectRecovery.recoverNow();
|
|
3139
2977
|
// ── Connect SSE stream for inbound Canon messages ──
|
|
3140
|
-
const
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
materialized = await materializeMessageMedia({
|
|
3172
|
-
id: m.id,
|
|
3173
|
-
attachments: m.attachments ?? [],
|
|
3174
|
-
}, { agentId, conversationId: convoId });
|
|
3175
|
-
}
|
|
3176
|
-
catch (error) {
|
|
3177
|
-
console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to materialize media:`, error instanceof Error ? error.message : error);
|
|
3178
|
-
}
|
|
3179
|
-
}
|
|
3180
|
-
const content = renderInboundContent(m, materialized);
|
|
3181
|
-
const hydrated = await loadHydratedInboundContext({
|
|
3182
|
-
conversationId: convoId,
|
|
3183
|
-
message: m,
|
|
3184
|
-
senderName: sender,
|
|
3185
|
-
isOwner,
|
|
3186
|
-
activeSelfContextId: payload.activeSelfContextId,
|
|
3187
|
-
selfContexts: payload.selfContexts,
|
|
3188
|
-
provenance: payload.provenance,
|
|
3189
|
-
});
|
|
3190
|
-
const behavior = payload.behavior ?? hydrated.behavior;
|
|
3191
|
-
const activeSelfContextId = hydrated.activeSelfContextId;
|
|
3192
|
-
const selfContexts = hydrated.selfContexts;
|
|
3193
|
-
const replyMedia = await materializePromptReplyContext({
|
|
3194
|
-
replyContext: hydrated.replyContext,
|
|
3195
|
-
agentId,
|
|
3196
|
-
conversationId: convoId,
|
|
3197
|
-
logPrefix: `[canon-host] [${convoId.slice(0, 8)}]`,
|
|
3198
|
-
});
|
|
3199
|
-
const replyContext = replyMedia.replyContext;
|
|
3200
|
-
const participantContext = hydrated.participantContext;
|
|
3201
|
-
const autoReply = payload.turnDispatch?.kind === 'run_turn'
|
|
3202
|
-
? {
|
|
3203
|
-
allow: true,
|
|
3204
|
-
reason: payload.turnDispatch.reason || 'server dispatch allowed this turn',
|
|
3205
|
-
}
|
|
3206
|
-
: decideAutoReply(participantContext, behavior);
|
|
3207
|
-
if (!autoReply.allow) {
|
|
3208
|
-
console.error(`[canon-host] [${convoId.slice(0, 8)}] Suppressed auto-reply: ${autoReply.reason}`);
|
|
3209
|
-
return false;
|
|
3210
|
-
}
|
|
3211
|
-
markGroupContextModeUsed(convoId, participantContext.groupContextMode);
|
|
3212
|
-
console.error(`[canon-host] [${convoId.slice(0, 8)}] Message from ${sender}: "${content.slice(0, 80)}" (${autoReply.reason})`);
|
|
3213
|
-
const turnMetadata = normalizeTurnMetadata(m.metadata);
|
|
3214
|
-
const deliveryIntent = turnMetadata?.deliveryIntent ?? 'queue';
|
|
3215
|
-
const shouldMarkAccepted = turnMetadata?.inboundDisposition === 'queued';
|
|
3216
|
-
let session;
|
|
2978
|
+
const inbound = endpoint.acceptInbound({ kind: 'message.created',
|
|
2979
|
+
offer: async (event) => {
|
|
2980
|
+
const payload = event.data;
|
|
2981
|
+
const m = payload.message;
|
|
2982
|
+
if (m.senderId === agentId) {
|
|
2983
|
+
await endpoint.setInboundState(event.id, 'settled', 'own-message');
|
|
2984
|
+
return;
|
|
2985
|
+
}
|
|
2986
|
+
const convoId = payload.conversationId;
|
|
2987
|
+
if (!claimInboundMessageId(m.id))
|
|
2988
|
+
return;
|
|
2989
|
+
recoveryCheckpointsFor(convoId).track(m.id);
|
|
2990
|
+
knownConversationIds.add(convoId);
|
|
2991
|
+
if (handleRuntimeReplyMessage(convoId, m)) {
|
|
2992
|
+
recoveryCheckpointsFor(convoId).settle(m.id);
|
|
2993
|
+
settleInboundMessageId(m.id, true);
|
|
2994
|
+
await endpoint.setInboundState(`message:${convoId}:${m.id}`, 'settled', 'not-dispatched');
|
|
2995
|
+
return;
|
|
2996
|
+
}
|
|
2997
|
+
const sender = m.senderName || m.senderId;
|
|
2998
|
+
const isOwner = m.isOwner ?? (ownerId != null && m.senderId === ownerId);
|
|
2999
|
+
if (payload.turnDispatch && payload.turnDispatch.kind !== 'run_turn') {
|
|
3000
|
+
console.error(`[canon-host] [${convoId.slice(0, 8)}] Ignoring server-dispatched observe-only message: ${payload.turnDispatch.reason}`);
|
|
3001
|
+
recoveryCheckpointsFor(convoId).settle(m.id);
|
|
3002
|
+
settleInboundMessageId(m.id, true);
|
|
3003
|
+
await endpoint.setInboundState(`message:${convoId}:${m.id}`, 'settled', 'not-dispatched');
|
|
3004
|
+
return;
|
|
3005
|
+
}
|
|
3006
|
+
await (async () => {
|
|
3007
|
+
let materialized = [];
|
|
3008
|
+
if (m.id) {
|
|
3217
3009
|
try {
|
|
3218
|
-
|
|
3010
|
+
materialized = await materializeMessageMedia({
|
|
3011
|
+
id: m.id,
|
|
3012
|
+
attachments: m.attachments ?? [],
|
|
3013
|
+
}, { agentId, conversationId: convoId });
|
|
3219
3014
|
}
|
|
3220
3015
|
catch (error) {
|
|
3221
|
-
|
|
3222
|
-
console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to create session: ${message}`);
|
|
3223
|
-
await runtimeState.patchAgentSessionSnapshot(convoId, {
|
|
3224
|
-
configurationStatus: 'configuration_required',
|
|
3225
|
-
lastError: LOCAL_CONFIGURATION_REQUIRED_MESSAGE,
|
|
3226
|
-
}).catch(() => { });
|
|
3227
|
-
await markQueuedMessageAcceptedForConversation(convoId, m.id ?? null, shouldMarkAccepted);
|
|
3228
|
-
await sendMessageWithRetry(client, convoId, LOCAL_CONFIGURATION_REQUIRED_MESSAGE, {
|
|
3229
|
-
messageId: `claude-start-failed-${m.id}`,
|
|
3230
|
-
...(activeSelfContextId ? { selfContextId: activeSelfContextId } : {}),
|
|
3231
|
-
metadata: {
|
|
3232
|
-
turnId: `claude-start:${m.id}`,
|
|
3233
|
-
runtimeStatus: 'configuration_required',
|
|
3234
|
-
turnSemantics: 'turn_complete',
|
|
3235
|
-
replyBehavior: 'suppress_auto_reply',
|
|
3236
|
-
},
|
|
3237
|
-
...(payload.replyAuthority ? { replyAuthority: payload.replyAuthority } : {}),
|
|
3238
|
-
}).catch(() => { });
|
|
3239
|
-
return false;
|
|
3016
|
+
console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to materialize media:`, error instanceof Error ? error.message : error);
|
|
3240
3017
|
}
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
};
|
|
3268
|
-
session.enqueueInbound({
|
|
3269
|
-
type: 'user',
|
|
3270
|
-
message: {
|
|
3271
|
-
role: 'user',
|
|
3272
|
-
content: messageContent,
|
|
3273
|
-
},
|
|
3274
|
-
parent_tool_use_id: null,
|
|
3275
|
-
origin: claudeOriginForCanonSender({
|
|
3276
|
-
senderType: m.senderType,
|
|
3277
|
-
senderId: m.senderId,
|
|
3278
|
-
senderName: m.senderName,
|
|
3279
|
-
}),
|
|
3280
|
-
}, deliveryIntent, m.id ?? null, shouldMarkAccepted, isOwner, m.senderType === 'human' ? m.senderId : null, turnModes);
|
|
3281
|
-
return true;
|
|
3282
|
-
})().then((queued) => {
|
|
3283
|
-
if (!queued) {
|
|
3284
|
-
recoveryCheckpointsFor(convoId).settle(m.id);
|
|
3285
|
-
settleInboundMessageId(m.id, true);
|
|
3018
|
+
}
|
|
3019
|
+
const content = renderInboundContent(m, materialized);
|
|
3020
|
+
const hydrated = await loadHydratedInboundContext({
|
|
3021
|
+
conversationId: convoId,
|
|
3022
|
+
message: m,
|
|
3023
|
+
senderName: sender,
|
|
3024
|
+
isOwner,
|
|
3025
|
+
activeSelfContextId: payload.activeSelfContextId,
|
|
3026
|
+
selfContexts: payload.selfContexts,
|
|
3027
|
+
provenance: payload.provenance,
|
|
3028
|
+
});
|
|
3029
|
+
const behavior = payload.behavior ?? hydrated.behavior;
|
|
3030
|
+
const activeSelfContextId = hydrated.activeSelfContextId;
|
|
3031
|
+
const selfContexts = hydrated.selfContexts;
|
|
3032
|
+
const replyMedia = await materializePromptReplyContext({
|
|
3033
|
+
replyContext: hydrated.replyContext,
|
|
3034
|
+
agentId,
|
|
3035
|
+
conversationId: convoId,
|
|
3036
|
+
logPrefix: `[canon-host] [${convoId.slice(0, 8)}]`,
|
|
3037
|
+
});
|
|
3038
|
+
const replyContext = replyMedia.replyContext;
|
|
3039
|
+
const participantContext = hydrated.participantContext;
|
|
3040
|
+
const autoReply = payload.turnDispatch?.kind === 'run_turn'
|
|
3041
|
+
? {
|
|
3042
|
+
allow: true,
|
|
3043
|
+
reason: payload.turnDispatch.reason || 'server dispatch allowed this turn',
|
|
3286
3044
|
}
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
console.error(`[canon-host] [${convoId.slice(0, 8)}]
|
|
3045
|
+
: decideAutoReply(participantContext, behavior);
|
|
3046
|
+
if (!autoReply.allow) {
|
|
3047
|
+
console.error(`[canon-host] [${convoId.slice(0, 8)}] Suppressed auto-reply: ${autoReply.reason}`);
|
|
3048
|
+
return false;
|
|
3049
|
+
}
|
|
3050
|
+
markGroupContextModeUsed(convoId, participantContext.groupContextMode);
|
|
3051
|
+
console.error(`[canon-host] [${convoId.slice(0, 8)}] Message from ${sender}: "${content.slice(0, 80)}" (${autoReply.reason})`);
|
|
3052
|
+
const turnMetadata = normalizeTurnMetadata(m.metadata);
|
|
3053
|
+
const deliveryIntent = turnMetadata?.deliveryIntent ?? 'queue';
|
|
3054
|
+
const shouldMarkAccepted = turnMetadata?.inboundDisposition === 'queued';
|
|
3055
|
+
let session;
|
|
3056
|
+
try {
|
|
3057
|
+
session = await getOrCreateSession(convoId);
|
|
3058
|
+
}
|
|
3059
|
+
catch (error) {
|
|
3060
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3061
|
+
console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to create session: ${message}`);
|
|
3062
|
+
await runtimeState.patchAgentSessionSnapshot(convoId, {
|
|
3063
|
+
configurationStatus: 'configuration_required',
|
|
3064
|
+
lastError: LOCAL_CONFIGURATION_REQUIRED_MESSAGE,
|
|
3065
|
+
}).catch(() => { });
|
|
3066
|
+
await markQueuedMessageAcceptedForConversation(convoId, m.id ?? null, shouldMarkAccepted);
|
|
3067
|
+
await sendMessageWithRetry(client, convoId, LOCAL_CONFIGURATION_REQUIRED_MESSAGE, {
|
|
3068
|
+
messageId: `claude-start-failed-${m.id}`,
|
|
3069
|
+
...(activeSelfContextId ? { selfContextId: activeSelfContextId } : {}),
|
|
3070
|
+
metadata: {
|
|
3071
|
+
turnId: `claude-start:${m.id}`,
|
|
3072
|
+
runtimeStatus: 'configuration_required',
|
|
3073
|
+
turnSemantics: 'turn_complete',
|
|
3074
|
+
replyBehavior: 'suppress_auto_reply',
|
|
3075
|
+
},
|
|
3076
|
+
...(payload.replyAuthority ? { replyAuthority: payload.replyAuthority } : {}),
|
|
3077
|
+
}).catch(() => { });
|
|
3078
|
+
return false;
|
|
3079
|
+
}
|
|
3080
|
+
session.activeSelfContextId = activeSelfContextId;
|
|
3081
|
+
const promptText = buildCanonPrompt({
|
|
3082
|
+
content,
|
|
3083
|
+
conversationId: convoId,
|
|
3084
|
+
participantContext,
|
|
3085
|
+
behavior,
|
|
3086
|
+
selfContexts,
|
|
3087
|
+
activeSelfContextId,
|
|
3088
|
+
provenance: hydrated.provenance,
|
|
3089
|
+
replyContext,
|
|
3090
|
+
message: m,
|
|
3290
3091
|
});
|
|
3092
|
+
const commandInput = buildClaudeNativeCommandInput({
|
|
3093
|
+
text: m.text,
|
|
3094
|
+
senderType: m.senderType,
|
|
3095
|
+
promptText,
|
|
3096
|
+
commands: runtimeMetadata.commands,
|
|
3097
|
+
});
|
|
3098
|
+
const messageContent = await buildCanonUserContent({
|
|
3099
|
+
promptText: commandInput.promptText,
|
|
3100
|
+
materialized: [...replyMedia.materialized, ...materialized],
|
|
3101
|
+
});
|
|
3102
|
+
const turnModes = {
|
|
3103
|
+
...resolveClaudeTurnModes(participantContext),
|
|
3104
|
+
commandContext: commandInput.commandContext,
|
|
3105
|
+
replyAuthority: payload.replyAuthority ?? null,
|
|
3106
|
+
};
|
|
3107
|
+
session.enqueueInbound({
|
|
3108
|
+
type: 'user',
|
|
3109
|
+
message: {
|
|
3110
|
+
role: 'user',
|
|
3111
|
+
content: messageContent,
|
|
3112
|
+
},
|
|
3113
|
+
parent_tool_use_id: null,
|
|
3114
|
+
origin: claudeOriginForCanonSender({
|
|
3115
|
+
senderType: m.senderType,
|
|
3116
|
+
senderId: m.senderId,
|
|
3117
|
+
senderName: m.senderName,
|
|
3118
|
+
}),
|
|
3119
|
+
}, deliveryIntent, m.id ?? null, shouldMarkAccepted, isOwner, m.senderType === 'human' ? m.senderId : null, turnModes);
|
|
3120
|
+
return true;
|
|
3121
|
+
})().then(async (queued) => {
|
|
3122
|
+
if (!queued) {
|
|
3123
|
+
recoveryCheckpointsFor(convoId).settle(m.id);
|
|
3124
|
+
settleInboundMessageId(m.id, true);
|
|
3125
|
+
await endpoint.setInboundState(`message:${convoId}:${m.id}`, 'settled', 'not-dispatched');
|
|
3126
|
+
}
|
|
3127
|
+
}).catch((error) => {
|
|
3128
|
+
settleInboundMessageId(m.id, false);
|
|
3129
|
+
console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to process inbound message:`, error instanceof Error ? error.message : error);
|
|
3130
|
+
throw error;
|
|
3131
|
+
});
|
|
3132
|
+
},
|
|
3133
|
+
onError: (error) => console.error('[canon-host] Endpoint input deferred:', error), });
|
|
3134
|
+
inbound.start();
|
|
3135
|
+
const stream = new CanonStream({
|
|
3136
|
+
endpoint,
|
|
3137
|
+
agentId,
|
|
3138
|
+
handler: {
|
|
3139
|
+
onMessage: (payload) => {
|
|
3140
|
+
void inbound.receive({ id: `message:${payload.conversationId}:${payload.message.id}`,
|
|
3141
|
+
kind: 'message.created', conversationId: payload.conversationId, durable: true,
|
|
3142
|
+
data: payload });
|
|
3291
3143
|
},
|
|
3292
3144
|
onMessageDeleted: (payload) => {
|
|
3293
3145
|
removeQueuedInput(payload.conversationId, payload.messageId);
|
|
@@ -3436,6 +3288,7 @@ export async function main() {
|
|
|
3436
3288
|
clearInterval(heartbeat);
|
|
3437
3289
|
clearInterval(idleCheck);
|
|
3438
3290
|
stream.stop();
|
|
3291
|
+
await inbound.close();
|
|
3439
3292
|
approvalManager?.dispose();
|
|
3440
3293
|
runtimeInputManager.dispose();
|
|
3441
3294
|
await runtimeHeartbeat.dispose();
|
package/dist/server.js
CHANGED
|
@@ -578,7 +578,7 @@ async function startChannel() {
|
|
|
578
578
|
if (profile) {
|
|
579
579
|
console.error(`[canon] Using agent profile: ${profile}`);
|
|
580
580
|
}
|
|
581
|
-
client = new CanonClient(apiKey, baseUrl);
|
|
581
|
+
client = new CanonClient(apiKey, baseUrl, { environmentId: resolvedRuntime.environmentId, streamUrl: resolvedRuntime.streamUrl });
|
|
582
582
|
// Get agent identity — try /agents/me first, fall back to /agents/auth-token
|
|
583
583
|
let agentId;
|
|
584
584
|
try {
|
|
@@ -645,9 +645,8 @@ async function startChannel() {
|
|
|
645
645
|
}
|
|
646
646
|
// Start SSE stream
|
|
647
647
|
stream = new CanonStream({
|
|
648
|
-
|
|
648
|
+
endpoint: await client.getEndpoint(),
|
|
649
649
|
agentId,
|
|
650
|
-
streamUrl: resolvedRuntime.streamUrl,
|
|
651
650
|
handler: {
|
|
652
651
|
onMessage: handleInboundMessage,
|
|
653
652
|
onAgentContext: (ctx) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/claude-code-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.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,15 +31,15 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@anthropic-ai/claude-agent-sdk": "0.3.228",
|
|
34
|
-
"@canonmsg/agent-sdk": "^
|
|
35
|
-
"@canonmsg/agent-tools": "^0.
|
|
36
|
-
"@canonmsg/coding-agent-host": "^0.
|
|
37
|
-
"@canonmsg/core": "^
|
|
38
|
-
"@canonmsg/rich-cards": "^0.10.
|
|
34
|
+
"@canonmsg/agent-sdk": "^11.0.0",
|
|
35
|
+
"@canonmsg/agent-tools": "^0.11.0",
|
|
36
|
+
"@canonmsg/coding-agent-host": "^0.9.0",
|
|
37
|
+
"@canonmsg/core": "^13.0.0",
|
|
38
|
+
"@canonmsg/rich-cards": "^0.10.6",
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.30.0"
|
|
40
40
|
},
|
|
41
41
|
"engines": {
|
|
42
|
-
"node": ">=
|
|
42
|
+
"node": ">=22.22.3"
|
|
43
43
|
},
|
|
44
44
|
"keywords": [
|
|
45
45
|
"canon",
|