@canonmsg/claude-code-plugin 0.34.5 → 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 +236 -350
- package/dist/server.js +2 -3
- package/dist/session-state.d.ts +5 -0
- package/dist/session-state.js +5 -1
- 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, 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';
|
|
@@ -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
|
|
@@ -1316,7 +1332,7 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1316
1332
|
const deliveredMessageIds = chunkedFailure
|
|
1317
1333
|
? chunkedFailure.deliveredMessageIds
|
|
1318
1334
|
: resume.deliveredMessageIds;
|
|
1319
|
-
const failure = classifyFinalDeliveryFailure(cause);
|
|
1335
|
+
const failure = isPendingCanonOperation(cause) ? 'retryable' : classifyFinalDeliveryFailure(cause);
|
|
1320
1336
|
if (failure === 'permanent') {
|
|
1321
1337
|
// Re-sending is pointless — the same request fails the same way. Surface
|
|
1322
1338
|
// what can be surfaced and finish the turn instead of burning the retry
|
|
@@ -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:
|
|
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
|
}
|
|
@@ -1616,7 +1635,16 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1616
1635
|
.updateMessageDisposition(conversationId, input.sourceMessageId, 'rejected')
|
|
1617
1636
|
.catch(() => { });
|
|
1618
1637
|
}
|
|
1619
|
-
|
|
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
|
+
}
|
|
1620
1648
|
// resetTurnToIdle releases the slot, tears the published turn down (typing
|
|
1621
1649
|
// signal, seeded /streaming, turn state) and drains the queue. On a closed
|
|
1622
1650
|
// session it is a no-op, which is correct: the teardown owns that path.
|
|
@@ -1626,7 +1654,16 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1626
1654
|
return dispatchClaudeInput(session, input, {
|
|
1627
1655
|
openTurn,
|
|
1628
1656
|
markAccepted: (dispatched) => markQueuedMessageAccepted(dispatched.sourceMessageId, dispatched.markAccepted),
|
|
1629
|
-
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
|
+
},
|
|
1630
1667
|
send: sendInput,
|
|
1631
1668
|
abandon: abandonDispatchedInput,
|
|
1632
1669
|
});
|
|
@@ -2211,7 +2248,8 @@ export async function main() {
|
|
|
2211
2248
|
throw error;
|
|
2212
2249
|
}
|
|
2213
2250
|
// ── Canon API client + RTDB auth ──
|
|
2214
|
-
const client = new CanonClient(apiKey, baseUrl);
|
|
2251
|
+
const client = new CanonClient(apiKey, baseUrl, { environmentId: resolvedAgent.environmentId, streamUrl });
|
|
2252
|
+
const endpoint = await client.getEndpoint();
|
|
2215
2253
|
const rtdb = initRTDBAuth(client, { rtdbUrl, firebaseApiKey });
|
|
2216
2254
|
const typingSignals = createTypingStatusPublisher({
|
|
2217
2255
|
setTyping: (conversationId, typing, status) => status
|
|
@@ -2532,19 +2570,23 @@ export async function main() {
|
|
|
2532
2570
|
if (cached) {
|
|
2533
2571
|
conversationCache.set(payload.conversationId, {
|
|
2534
2572
|
...cached,
|
|
2573
|
+
...payload.changes,
|
|
2535
2574
|
memberIds,
|
|
2536
2575
|
});
|
|
2576
|
+
void client.rememberConversation(conversationCache.get(payload.conversationId))
|
|
2577
|
+
.catch((error) => console.error('[canon] Failed to persist conversation update:', error));
|
|
2537
2578
|
}
|
|
2538
2579
|
if (membershipChange) {
|
|
2539
2580
|
pendingMembershipChanges.set(payload.conversationId, membershipChange);
|
|
2540
2581
|
}
|
|
2541
2582
|
if (!memberIds.includes(agentId)) {
|
|
2542
2583
|
knownConversationIds.delete(payload.conversationId);
|
|
2543
|
-
|
|
2584
|
+
pendingMembershipChanges.delete(payload.conversationId);
|
|
2585
|
+
closeSession(payload.conversationId);
|
|
2544
2586
|
}
|
|
2545
2587
|
}
|
|
2546
2588
|
function getGroupContextMode(conversationId, conversation) {
|
|
2547
|
-
if (conversation
|
|
2589
|
+
if (!conversation)
|
|
2548
2590
|
return undefined;
|
|
2549
2591
|
if (pendingMembershipChanges.has(conversationId))
|
|
2550
2592
|
return 'membership_change';
|
|
@@ -2562,7 +2604,7 @@ export async function main() {
|
|
|
2562
2604
|
}
|
|
2563
2605
|
async function loadHydratedInboundContext(input) {
|
|
2564
2606
|
const [conversation, page] = await Promise.all([
|
|
2565
|
-
getConversationMeta(input.conversationId),
|
|
2607
|
+
getConversationMeta(input.conversationId, { refreshIfMemberMissing: agentId }),
|
|
2566
2608
|
input.hydratedPage
|
|
2567
2609
|
? Promise.resolve(input.hydratedPage)
|
|
2568
2610
|
: client.getMessagesPage(input.conversationId, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT).catch(() => null),
|
|
@@ -2571,6 +2613,7 @@ export async function main() {
|
|
|
2571
2613
|
agentId,
|
|
2572
2614
|
conversationId: input.conversationId,
|
|
2573
2615
|
conversation,
|
|
2616
|
+
requireAgentMembership: true,
|
|
2574
2617
|
page,
|
|
2575
2618
|
activeSelfContextId: input.activeSelfContextId,
|
|
2576
2619
|
selfContexts: input.selfContexts,
|
|
@@ -2704,6 +2747,10 @@ export async function main() {
|
|
|
2704
2747
|
}
|
|
2705
2748
|
}
|
|
2706
2749
|
async function getOrCreateSession(conversationId) {
|
|
2750
|
+
const current = conversationCache.get(conversationId);
|
|
2751
|
+
if (current && !current.memberIds.includes(agentId)) {
|
|
2752
|
+
throw new Error('Agent is no longer a member of this conversation');
|
|
2753
|
+
}
|
|
2707
2754
|
knownConversationIds.add(conversationId);
|
|
2708
2755
|
const existing = sessions.get(conversationId);
|
|
2709
2756
|
if (existing && !existing.closed) {
|
|
@@ -2813,9 +2860,21 @@ export async function main() {
|
|
|
2813
2860
|
runtimeId,
|
|
2814
2861
|
ownerId,
|
|
2815
2862
|
communicationEnabled,
|
|
2863
|
+
currentParticipantContext: () => {
|
|
2864
|
+
const roster = buildCanonGroupContext({
|
|
2865
|
+
conversation: conversationCache.get(conversationId), messages: [], agentId, ownerId, ownerName,
|
|
2866
|
+
});
|
|
2867
|
+
return roster ? buildCompactGroupContextLines(roster, 'initial').join('\n') : '';
|
|
2868
|
+
},
|
|
2869
|
+
currentArtifactAudience: () => ({ memberIds: conversationCache.get(conversationId)?.memberIds, agentId, ownerId }),
|
|
2870
|
+
onInputDeferred: (sourceMessageId) => settleInboundMessageId(sourceMessageId, false),
|
|
2816
2871
|
onInputCompleted: (sourceMessageId) => {
|
|
2817
2872
|
recoveryCheckpointsFor(conversationId).settle(sourceMessageId);
|
|
2818
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));
|
|
2819
2878
|
},
|
|
2820
2879
|
publishSessionSnapshots: (ids) => { void publishSessionSnapshots(ids); },
|
|
2821
2880
|
}, resumeId);
|
|
@@ -2840,134 +2899,6 @@ export async function main() {
|
|
|
2840
2899
|
pendingSessionCreations.delete(conversationId);
|
|
2841
2900
|
}
|
|
2842
2901
|
}
|
|
2843
|
-
async function enqueueRecoveredInboundMessage(input) {
|
|
2844
|
-
const m = {
|
|
2845
|
-
id: input.message.id,
|
|
2846
|
-
senderId: input.message.senderId,
|
|
2847
|
-
senderName: input.message.senderName,
|
|
2848
|
-
senderType: input.message.senderType,
|
|
2849
|
-
isOwner: input.message.isOwner,
|
|
2850
|
-
text: input.message.text ?? undefined,
|
|
2851
|
-
contentType: input.message.contentType,
|
|
2852
|
-
attachments: input.message.attachments,
|
|
2853
|
-
mentions: input.message.mentions,
|
|
2854
|
-
reactions: input.message.reactions,
|
|
2855
|
-
replyTo: input.message.replyTo ?? undefined,
|
|
2856
|
-
replyToPosition: input.message.replyToPosition ?? undefined,
|
|
2857
|
-
forwarded: input.message.forwarded,
|
|
2858
|
-
forwardedFrom: input.message.forwardedFrom,
|
|
2859
|
-
metadata: input.message.metadata,
|
|
2860
|
-
contactCard: input.message.contactCard,
|
|
2861
|
-
createdAt: input.message.createdAt,
|
|
2862
|
-
};
|
|
2863
|
-
if (m.senderId === agentId)
|
|
2864
|
-
return 'handled';
|
|
2865
|
-
let materialized = [];
|
|
2866
|
-
if (m.id) {
|
|
2867
|
-
try {
|
|
2868
|
-
materialized = await materializeMessageMedia({
|
|
2869
|
-
id: m.id,
|
|
2870
|
-
attachments: m.attachments ?? [],
|
|
2871
|
-
}, { agentId, conversationId: input.conversationId });
|
|
2872
|
-
}
|
|
2873
|
-
catch (error) {
|
|
2874
|
-
console.error(`[canon-host] [${input.conversationId.slice(0, 8)}] Failed to materialize recovered media:`, error instanceof Error ? error.message : error);
|
|
2875
|
-
}
|
|
2876
|
-
}
|
|
2877
|
-
const sender = m.senderName || m.senderId;
|
|
2878
|
-
const isOwner = m.isOwner ?? (ownerId != null && m.senderId === ownerId);
|
|
2879
|
-
const content = renderInboundContent(m, materialized);
|
|
2880
|
-
const hydrated = await loadHydratedInboundContext({
|
|
2881
|
-
conversationId: input.conversationId,
|
|
2882
|
-
message: m,
|
|
2883
|
-
senderName: sender,
|
|
2884
|
-
isOwner,
|
|
2885
|
-
hydratedPage: input.hydratedPage,
|
|
2886
|
-
});
|
|
2887
|
-
const behavior = input.hydratedPage?.behavior ?? hydrated.behavior;
|
|
2888
|
-
const activeSelfContextId = hydrated.activeSelfContextId;
|
|
2889
|
-
const selfContexts = hydrated.selfContexts;
|
|
2890
|
-
const replyMedia = await materializePromptReplyContext({
|
|
2891
|
-
replyContext: hydrated.replyContext,
|
|
2892
|
-
agentId,
|
|
2893
|
-
conversationId: input.conversationId,
|
|
2894
|
-
logPrefix: `[canon-host] [${input.conversationId.slice(0, 8)}]`,
|
|
2895
|
-
});
|
|
2896
|
-
const replyContext = replyMedia.replyContext;
|
|
2897
|
-
const participantContext = hydrated.participantContext;
|
|
2898
|
-
const autoReply = decideAutoReply(participantContext, behavior);
|
|
2899
|
-
if (!autoReply.allow) {
|
|
2900
|
-
console.error(`[canon-host] [${input.conversationId.slice(0, 8)}] Suppressed recovered auto-reply: ${autoReply.reason}`);
|
|
2901
|
-
return 'handled';
|
|
2902
|
-
}
|
|
2903
|
-
markGroupContextModeUsed(input.conversationId, participantContext.groupContextMode);
|
|
2904
|
-
const turnMetadata = normalizeTurnMetadata(m.metadata);
|
|
2905
|
-
const deliveryIntent = turnMetadata?.deliveryIntent ?? 'queue';
|
|
2906
|
-
const shouldMarkAccepted = turnMetadata?.inboundDisposition === 'queued';
|
|
2907
|
-
let session;
|
|
2908
|
-
try {
|
|
2909
|
-
session = await getOrCreateSession(input.conversationId);
|
|
2910
|
-
}
|
|
2911
|
-
catch (error) {
|
|
2912
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2913
|
-
console.error(`[canon-host] [${input.conversationId.slice(0, 8)}] Failed to create recovered session: ${message}`);
|
|
2914
|
-
await runtimeState.patchAgentSessionSnapshot(input.conversationId, {
|
|
2915
|
-
configurationStatus: 'configuration_required',
|
|
2916
|
-
lastError: LOCAL_CONFIGURATION_REQUIRED_MESSAGE,
|
|
2917
|
-
}).catch(() => { });
|
|
2918
|
-
await markQueuedMessageAcceptedForConversation(input.conversationId, m.id ?? null, shouldMarkAccepted);
|
|
2919
|
-
await sendMessageWithRetry(client, input.conversationId, LOCAL_CONFIGURATION_REQUIRED_MESSAGE, {
|
|
2920
|
-
messageId: `claude-start-failed-${m.id}`,
|
|
2921
|
-
...(activeSelfContextId ? { selfContextId: activeSelfContextId } : {}),
|
|
2922
|
-
metadata: {
|
|
2923
|
-
runtimeStatus: 'configuration_required',
|
|
2924
|
-
turnSemantics: 'turn_complete',
|
|
2925
|
-
replyBehavior: 'suppress_auto_reply',
|
|
2926
|
-
},
|
|
2927
|
-
}).catch(() => { });
|
|
2928
|
-
return 'handled';
|
|
2929
|
-
}
|
|
2930
|
-
session.activeSelfContextId = activeSelfContextId;
|
|
2931
|
-
const promptText = buildCanonPrompt({
|
|
2932
|
-
content,
|
|
2933
|
-
conversationId: input.conversationId,
|
|
2934
|
-
participantContext,
|
|
2935
|
-
behavior,
|
|
2936
|
-
selfContexts,
|
|
2937
|
-
activeSelfContextId,
|
|
2938
|
-
provenance: hydrated.provenance,
|
|
2939
|
-
replyContext,
|
|
2940
|
-
message: m,
|
|
2941
|
-
});
|
|
2942
|
-
const commandInput = buildClaudeNativeCommandInput({
|
|
2943
|
-
text: m.text,
|
|
2944
|
-
senderType: m.senderType,
|
|
2945
|
-
promptText,
|
|
2946
|
-
commands: runtimeMetadata.commands,
|
|
2947
|
-
});
|
|
2948
|
-
const messageContent = await buildCanonUserContent({
|
|
2949
|
-
promptText: commandInput.promptText,
|
|
2950
|
-
materialized: [...replyMedia.materialized, ...materialized],
|
|
2951
|
-
});
|
|
2952
|
-
const turnModes = {
|
|
2953
|
-
...resolveClaudeTurnModes(participantContext),
|
|
2954
|
-
commandContext: commandInput.commandContext,
|
|
2955
|
-
};
|
|
2956
|
-
session.enqueueInbound({
|
|
2957
|
-
type: 'user',
|
|
2958
|
-
message: {
|
|
2959
|
-
role: 'user',
|
|
2960
|
-
content: messageContent,
|
|
2961
|
-
},
|
|
2962
|
-
parent_tool_use_id: null,
|
|
2963
|
-
origin: claudeOriginForCanonSender({
|
|
2964
|
-
senderType: m.senderType,
|
|
2965
|
-
senderId: m.senderId,
|
|
2966
|
-
senderName: m.senderName,
|
|
2967
|
-
}),
|
|
2968
|
-
}, deliveryIntent, m.id ?? null, shouldMarkAccepted, isOwner, m.senderType === 'human' ? m.senderId : null, turnModes);
|
|
2969
|
-
return 'queued';
|
|
2970
|
-
}
|
|
2971
2902
|
const acceptedInboundMessageIds = new Set();
|
|
2972
2903
|
const inFlightInboundMessageIds = new Set();
|
|
2973
2904
|
function claimInboundMessageId(messageId) {
|
|
@@ -3038,223 +2969,177 @@ export async function main() {
|
|
|
3038
2969
|
return false;
|
|
3039
2970
|
}
|
|
3040
2971
|
}
|
|
3041
|
-
let startupRecoveryComplete = false;
|
|
3042
2972
|
async function performMissedInboundRecovery() {
|
|
3043
|
-
const knownBeforeRefresh = new Set(knownConversationIds);
|
|
3044
2973
|
await refreshKnownConversationIds(true);
|
|
3045
|
-
const conversationsDiscoveredWhileOffline = startupRecoveryComplete
|
|
3046
|
-
? new Set([...knownConversationIds].filter((id) => !knownBeforeRefresh.has(id)))
|
|
3047
|
-
: new Set();
|
|
3048
|
-
for (const conversationId of knownConversationIds) {
|
|
3049
|
-
const recoveryCheckpoints = recoveryCheckpointsFor(conversationId);
|
|
3050
|
-
const recoveryBatch = recoveryCheckpoints.reserveBatch();
|
|
3051
|
-
try {
|
|
3052
|
-
const recovered = await collectMissedInboundMessages({
|
|
3053
|
-
fetchPage: (before) => client.getMessagesPage(conversationId, STARTUP_RECOVERY_PAGE_SIZE, before),
|
|
3054
|
-
cursor: persistedConversationCursor(conversationId),
|
|
3055
|
-
agentId,
|
|
3056
|
-
requireContiguousCursor: true,
|
|
3057
|
-
noCursorMode: conversationsDiscoveredWhileOffline.has(conversationId)
|
|
3058
|
-
? 'bounded-window'
|
|
3059
|
-
: 'latest-only',
|
|
3060
|
-
});
|
|
3061
|
-
recoveryBatch.commit(recovered.messages.map((message) => message.id), recovered.mode === 'incomplete-gap' ? recovered.recoveryCursor : null);
|
|
3062
|
-
if (recovered.mode === 'incomplete-gap') {
|
|
3063
|
-
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'}`);
|
|
3064
|
-
}
|
|
3065
|
-
for (const message of recovered.messages) {
|
|
3066
|
-
if (!claimInboundMessageId(message.id))
|
|
3067
|
-
continue;
|
|
3068
|
-
try {
|
|
3069
|
-
let queued = false;
|
|
3070
|
-
if (!handleRuntimeReplyMessage(conversationId, message)) {
|
|
3071
|
-
const trigger = shouldTriggerAgentTurn({
|
|
3072
|
-
senderType: message.senderType,
|
|
3073
|
-
metadata: message.metadata,
|
|
3074
|
-
});
|
|
3075
|
-
if (trigger.allow) {
|
|
3076
|
-
queued = await enqueueRecoveredInboundMessage({
|
|
3077
|
-
conversationId,
|
|
3078
|
-
message,
|
|
3079
|
-
hydratedPage: recovered.newestPage,
|
|
3080
|
-
}) === 'queued';
|
|
3081
|
-
}
|
|
3082
|
-
}
|
|
3083
|
-
if (!queued) {
|
|
3084
|
-
recoveryCheckpoints.settle(message.id);
|
|
3085
|
-
settleInboundMessageId(message.id, true);
|
|
3086
|
-
}
|
|
3087
|
-
}
|
|
3088
|
-
catch (error) {
|
|
3089
|
-
settleInboundMessageId(message.id, false);
|
|
3090
|
-
throw error;
|
|
3091
|
-
}
|
|
3092
|
-
}
|
|
3093
|
-
if (recovered.messages.length > 0) {
|
|
3094
|
-
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Recovered ${recovered.messages.length} inbound message(s) (${recovered.mode})`);
|
|
3095
|
-
}
|
|
3096
|
-
}
|
|
3097
|
-
catch (error) {
|
|
3098
|
-
recoveryBatch.cancel();
|
|
3099
|
-
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Startup recovery failed:`, error instanceof Error ? error.message : error);
|
|
3100
|
-
}
|
|
3101
|
-
}
|
|
3102
|
-
startupRecoveryComplete = true;
|
|
3103
2974
|
}
|
|
3104
2975
|
const reconnectRecovery = createReconnectRecoveryCoordinator(performMissedInboundRecovery);
|
|
3105
2976
|
await reconnectRecovery.recoverNow();
|
|
3106
2977
|
// ── Connect SSE stream for inbound Canon messages ──
|
|
3107
|
-
const
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
materialized = await materializeMessageMedia({
|
|
3139
|
-
id: m.id,
|
|
3140
|
-
attachments: m.attachments ?? [],
|
|
3141
|
-
}, { agentId, conversationId: convoId });
|
|
3142
|
-
}
|
|
3143
|
-
catch (error) {
|
|
3144
|
-
console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to materialize media:`, error instanceof Error ? error.message : error);
|
|
3145
|
-
}
|
|
3146
|
-
}
|
|
3147
|
-
const content = renderInboundContent(m, materialized);
|
|
3148
|
-
const hydrated = await loadHydratedInboundContext({
|
|
3149
|
-
conversationId: convoId,
|
|
3150
|
-
message: m,
|
|
3151
|
-
senderName: sender,
|
|
3152
|
-
isOwner,
|
|
3153
|
-
activeSelfContextId: payload.activeSelfContextId,
|
|
3154
|
-
selfContexts: payload.selfContexts,
|
|
3155
|
-
provenance: payload.provenance,
|
|
3156
|
-
});
|
|
3157
|
-
const behavior = payload.behavior ?? hydrated.behavior;
|
|
3158
|
-
const activeSelfContextId = hydrated.activeSelfContextId;
|
|
3159
|
-
const selfContexts = hydrated.selfContexts;
|
|
3160
|
-
const replyMedia = await materializePromptReplyContext({
|
|
3161
|
-
replyContext: hydrated.replyContext,
|
|
3162
|
-
agentId,
|
|
3163
|
-
conversationId: convoId,
|
|
3164
|
-
logPrefix: `[canon-host] [${convoId.slice(0, 8)}]`,
|
|
3165
|
-
});
|
|
3166
|
-
const replyContext = replyMedia.replyContext;
|
|
3167
|
-
const participantContext = hydrated.participantContext;
|
|
3168
|
-
const autoReply = payload.turnDispatch?.kind === 'run_turn'
|
|
3169
|
-
? {
|
|
3170
|
-
allow: true,
|
|
3171
|
-
reason: payload.turnDispatch.reason || 'server dispatch allowed this turn',
|
|
3172
|
-
}
|
|
3173
|
-
: decideAutoReply(participantContext, behavior);
|
|
3174
|
-
if (!autoReply.allow) {
|
|
3175
|
-
console.error(`[canon-host] [${convoId.slice(0, 8)}] Suppressed auto-reply: ${autoReply.reason}`);
|
|
3176
|
-
return false;
|
|
3177
|
-
}
|
|
3178
|
-
markGroupContextModeUsed(convoId, participantContext.groupContextMode);
|
|
3179
|
-
console.error(`[canon-host] [${convoId.slice(0, 8)}] Message from ${sender}: "${content.slice(0, 80)}" (${autoReply.reason})`);
|
|
3180
|
-
const turnMetadata = normalizeTurnMetadata(m.metadata);
|
|
3181
|
-
const deliveryIntent = turnMetadata?.deliveryIntent ?? 'queue';
|
|
3182
|
-
const shouldMarkAccepted = turnMetadata?.inboundDisposition === 'queued';
|
|
3183
|
-
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) {
|
|
3184
3009
|
try {
|
|
3185
|
-
|
|
3010
|
+
materialized = await materializeMessageMedia({
|
|
3011
|
+
id: m.id,
|
|
3012
|
+
attachments: m.attachments ?? [],
|
|
3013
|
+
}, { agentId, conversationId: convoId });
|
|
3186
3014
|
}
|
|
3187
3015
|
catch (error) {
|
|
3188
|
-
|
|
3189
|
-
console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to create session: ${message}`);
|
|
3190
|
-
await runtimeState.patchAgentSessionSnapshot(convoId, {
|
|
3191
|
-
configurationStatus: 'configuration_required',
|
|
3192
|
-
lastError: LOCAL_CONFIGURATION_REQUIRED_MESSAGE,
|
|
3193
|
-
}).catch(() => { });
|
|
3194
|
-
await markQueuedMessageAcceptedForConversation(convoId, m.id ?? null, shouldMarkAccepted);
|
|
3195
|
-
await sendMessageWithRetry(client, convoId, LOCAL_CONFIGURATION_REQUIRED_MESSAGE, {
|
|
3196
|
-
messageId: `claude-start-failed-${m.id}`,
|
|
3197
|
-
...(activeSelfContextId ? { selfContextId: activeSelfContextId } : {}),
|
|
3198
|
-
metadata: {
|
|
3199
|
-
turnId: `claude-start:${m.id}`,
|
|
3200
|
-
runtimeStatus: 'configuration_required',
|
|
3201
|
-
turnSemantics: 'turn_complete',
|
|
3202
|
-
replyBehavior: 'suppress_auto_reply',
|
|
3203
|
-
},
|
|
3204
|
-
...(payload.replyAuthority ? { replyAuthority: payload.replyAuthority } : {}),
|
|
3205
|
-
}).catch(() => { });
|
|
3206
|
-
return false;
|
|
3016
|
+
console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to materialize media:`, error instanceof Error ? error.message : error);
|
|
3207
3017
|
}
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
};
|
|
3235
|
-
session.enqueueInbound({
|
|
3236
|
-
type: 'user',
|
|
3237
|
-
message: {
|
|
3238
|
-
role: 'user',
|
|
3239
|
-
content: messageContent,
|
|
3240
|
-
},
|
|
3241
|
-
parent_tool_use_id: null,
|
|
3242
|
-
origin: claudeOriginForCanonSender({
|
|
3243
|
-
senderType: m.senderType,
|
|
3244
|
-
senderId: m.senderId,
|
|
3245
|
-
senderName: m.senderName,
|
|
3246
|
-
}),
|
|
3247
|
-
}, deliveryIntent, m.id ?? null, shouldMarkAccepted, isOwner, m.senderType === 'human' ? m.senderId : null, turnModes);
|
|
3248
|
-
return true;
|
|
3249
|
-
})().then((queued) => {
|
|
3250
|
-
if (!queued) {
|
|
3251
|
-
recoveryCheckpointsFor(convoId).settle(m.id);
|
|
3252
|
-
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',
|
|
3253
3044
|
}
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
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,
|
|
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],
|
|
3257
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 });
|
|
3258
3143
|
},
|
|
3259
3144
|
onMessageDeleted: (payload) => {
|
|
3260
3145
|
removeQueuedInput(payload.conversationId, payload.messageId);
|
|
@@ -3403,6 +3288,7 @@ export async function main() {
|
|
|
3403
3288
|
clearInterval(heartbeat);
|
|
3404
3289
|
clearInterval(idleCheck);
|
|
3405
3290
|
stream.stop();
|
|
3291
|
+
await inbound.close();
|
|
3406
3292
|
approvalManager?.dispose();
|
|
3407
3293
|
runtimeInputManager.dispose();
|
|
3408
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/dist/session-state.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/session-state.js
CHANGED
|
@@ -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.
|
|
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",
|