@canonmsg/claude-code-plugin 0.36.0 → 0.36.2
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/README.md +1 -1
- package/dist/host.js +76 -21
- package/dist/session-state.d.ts +21 -10
- package/dist/session-state.js +47 -24
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Connect Claude Code to [Canon](https://canonmail.com/agents) — a messaging app
|
|
|
4
4
|
|
|
5
5
|
## Quick start
|
|
6
6
|
|
|
7
|
-
The package includes a compatible Claude Code runtime. If `claude` is installed on `PATH`, or selected with `CANON_CLAUDE_CLI_PATH`, use Claude Code 2.1.
|
|
7
|
+
The package includes a compatible Claude Code runtime. If `claude` is installed on `PATH`, or selected with `CANON_CLAUDE_CLI_PATH`, use Claude Code 2.1.278 or newer. The bundled Claude Agent SDK is pinned to 0.3.278; Canon relies on its explicit input/result attribution for merged messages and native errors. An external binary below that minimum is logged and skipped in favour of the bundled runtime.
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
# Install
|
package/dist/host.js
CHANGED
|
@@ -30,12 +30,12 @@ import { materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMes
|
|
|
30
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, isPendingCanonOperation, sendMessageWithRetry, sendMessageWithRetryChunked, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, resolveLocalRuntimeSessionState, saveRuntimeSessionState, clearRuntimeSessionState, publishHostSessionSnapshots, renderCodingHostInboundPrompt, resolveConfiguredWorkspaceCwd, resolveTurnVerbosity, upsertLocalRuntimeEntry, } from '@canonmsg/core';
|
|
33
|
+
import { EFFORT_OPTIONS, CLAUDE_PERMISSION_MODE_OPTIONS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildCanonInboundFrameV1, buildCanonGroupContext, buildCompactGroupContextLines, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, hydrateCanonReplyContext, 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';
|
|
37
37
|
import { decideClaudeToolPermissionForMode, parseAllowedNonOwnerClaudeTools, } from './tool-policy.js';
|
|
38
|
-
import { 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';
|
|
38
|
+
import { 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, claudeCompletedSourceMessageIds, shouldFailClaudeSessionForUnattributedResult, 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';
|
|
39
39
|
import { CLAUDE_SUPPORTED_DIALOG_KINDS, buildClaudeAskUserPermissionDenied, buildClaudeAskUserPermissionResult, createClaudeUserDialogCoordinator, parseClaudeAskUserDialog, parseClaudeAskUserToolInput, resolveClaudeUserDialogRequestId, } from './user-dialog.js';
|
|
40
40
|
import { isProactiveCommunicationEnabled } from './communication-policy.js';
|
|
41
41
|
function parseRuntimeVisibilityPreset(value) {
|
|
@@ -1059,9 +1059,9 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1059
1059
|
return streamingOutput.clear().catch(() => { });
|
|
1060
1060
|
}
|
|
1061
1061
|
function markInputCompleted(input) {
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1062
|
+
for (const sourceMessageId of claudeCompletedSourceMessageIds(input)) {
|
|
1063
|
+
config.onInputCompleted?.(sourceMessageId);
|
|
1064
|
+
}
|
|
1065
1065
|
}
|
|
1066
1066
|
/**
|
|
1067
1067
|
* The turn trail to attach to the final send. Empty on a quiet turn — see
|
|
@@ -1120,6 +1120,8 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1120
1120
|
drainPendingInput();
|
|
1121
1121
|
}
|
|
1122
1122
|
function scheduleFinalHandoffReset() {
|
|
1123
|
+
if (session.closed)
|
|
1124
|
+
return;
|
|
1123
1125
|
clearIdleResetTimer();
|
|
1124
1126
|
session.idleResetTimer = setTimeout(() => {
|
|
1125
1127
|
session.idleResetTimer = null;
|
|
@@ -1177,8 +1179,10 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1177
1179
|
session.finalizedTurnKeys.add(turn.turnKey);
|
|
1178
1180
|
markInputCompleted(turn);
|
|
1179
1181
|
releaseTurnSlotFor(turn);
|
|
1180
|
-
session.pendingFinalDelivery
|
|
1181
|
-
|
|
1182
|
+
if (session.pendingFinalDelivery?.turnKey === turn.turnKey) {
|
|
1183
|
+
session.pendingFinalDelivery = null;
|
|
1184
|
+
clearFinalDeliveryTimer();
|
|
1185
|
+
}
|
|
1182
1186
|
writeState();
|
|
1183
1187
|
}
|
|
1184
1188
|
/**
|
|
@@ -1461,6 +1465,8 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1461
1465
|
}, FINAL_DELIVERY_RETRY_MS);
|
|
1462
1466
|
}
|
|
1463
1467
|
function writeState() {
|
|
1468
|
+
if (session.closed)
|
|
1469
|
+
return;
|
|
1464
1470
|
runtimeState.writeSessionState(conversationId, {
|
|
1465
1471
|
contextUsage: session.state.contextUsage,
|
|
1466
1472
|
hostMode: true,
|
|
@@ -1487,6 +1493,8 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1487
1493
|
writeState();
|
|
1488
1494
|
}
|
|
1489
1495
|
function writeTurn() {
|
|
1496
|
+
if (session.closed)
|
|
1497
|
+
return;
|
|
1490
1498
|
const isOpenTurn = isOpenClaudeTurnState(session.turnState);
|
|
1491
1499
|
if (isOpenTurn) {
|
|
1492
1500
|
session.currentTurnUpdatedAt = Date.now();
|
|
@@ -1780,6 +1788,29 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1780
1788
|
applyTrailCommands(planClaudeToolResults(session.turnActivity, content));
|
|
1781
1789
|
}
|
|
1782
1790
|
// ── Process SDK output messages ──
|
|
1791
|
+
function finishSession() {
|
|
1792
|
+
if (session.closed)
|
|
1793
|
+
return;
|
|
1794
|
+
session.closed = true;
|
|
1795
|
+
inputDone = true;
|
|
1796
|
+
clearIdleResetTimer();
|
|
1797
|
+
clearFinalDeliveryTimer();
|
|
1798
|
+
stopVisibleWorkSignal();
|
|
1799
|
+
try {
|
|
1800
|
+
q.close();
|
|
1801
|
+
}
|
|
1802
|
+
catch { }
|
|
1803
|
+
// A native failure cannot prove whether dispatched work ran. Release local
|
|
1804
|
+
// in-flight claims without completing endpoint receipts or replaying work.
|
|
1805
|
+
const unfinishedIds = new Set([
|
|
1806
|
+
session.activeInput, session.dispatchingInput,
|
|
1807
|
+
...session.pendingInputs, ...dispatchedInputs.values(),
|
|
1808
|
+
].flatMap(claudeCompletedSourceMessageIds));
|
|
1809
|
+
for (const sourceMessageId of unfinishedIds)
|
|
1810
|
+
config.onInputDeferred?.(sourceMessageId);
|
|
1811
|
+
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Session ended`);
|
|
1812
|
+
onSessionEnd(conversationId, session.sdkSessionId);
|
|
1813
|
+
}
|
|
1783
1814
|
const processMessages = async () => {
|
|
1784
1815
|
try {
|
|
1785
1816
|
for await (const msg of q) {
|
|
@@ -1988,10 +2019,17 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1988
2019
|
const resultSessionId = msg.session_id;
|
|
1989
2020
|
if (resultSessionId)
|
|
1990
2021
|
session.sdkSessionId = resultSessionId;
|
|
1991
|
-
//
|
|
1992
|
-
//
|
|
1993
|
-
//
|
|
1994
|
-
const completedInput = takeClaudeResultOwner(dispatchedInputs, msg.user_message_uuid
|
|
2022
|
+
// A result may acknowledge several coalesced inputs, but never a
|
|
2023
|
+
// queued input absent from its UUID list. Native background/resume
|
|
2024
|
+
// results with no known UUID do not belong to the active Canon turn.
|
|
2025
|
+
const completedInput = takeClaudeResultOwner(dispatchedInputs, msg.user_message_uuid, msg.user_message_uuids);
|
|
2026
|
+
if (!completedInput) {
|
|
2027
|
+
if (shouldFailClaudeSessionForUnattributedResult(msg, Boolean(session.activeInput || session.dispatchingInput || session.pendingInputs.length))) {
|
|
2028
|
+
throw new Error('Claude returned an unattributed result; native session closed with unfinished inputs preserved.');
|
|
2029
|
+
}
|
|
2030
|
+
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Ignoring result without a known input UUID`);
|
|
2031
|
+
break;
|
|
2032
|
+
}
|
|
1995
2033
|
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Turn complete (${msg.subtype})`);
|
|
1996
2034
|
const resultText = typeof msg.result === 'string'
|
|
1997
2035
|
? msg.result.trim()
|
|
@@ -2014,7 +2052,8 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
2014
2052
|
// the artifacts still POST first, ahead of the reply they belong to.
|
|
2015
2053
|
const turnFinal = composeClaudeTurnFinal({
|
|
2016
2054
|
resultText,
|
|
2017
|
-
streamedText: session.
|
|
2055
|
+
streamedText: completedInput.turnKey === session.activeInput?.turnKey
|
|
2056
|
+
? session.pendingFinalText : null,
|
|
2018
2057
|
failureNotice,
|
|
2019
2058
|
silenced,
|
|
2020
2059
|
});
|
|
@@ -2077,6 +2116,14 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
2077
2116
|
if (!finalText || !shouldDeliverFinal) {
|
|
2078
2117
|
markInputCompleted(completedInput);
|
|
2079
2118
|
}
|
|
2119
|
+
// Sending the final and fetching usage both await. A newer turn
|
|
2120
|
+
// may now own the live text, pending delivery and handoff timer.
|
|
2121
|
+
if (!shouldReleaseClaudeTurnSlot({
|
|
2122
|
+
dispatching: session.dispatchingInput !== null,
|
|
2123
|
+
activeTurnKey: session.activeInput?.turnKey ?? null,
|
|
2124
|
+
completedTurnKey: completedInput.turnKey,
|
|
2125
|
+
}))
|
|
2126
|
+
break;
|
|
2080
2127
|
if (completedInput?.turnKey === session.activeInput?.turnKey) {
|
|
2081
2128
|
session.activeInput = null;
|
|
2082
2129
|
}
|
|
@@ -2118,16 +2165,14 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
2118
2165
|
catch (err) {
|
|
2119
2166
|
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Query stream error:`, err);
|
|
2120
2167
|
}
|
|
2121
|
-
|
|
2122
|
-
if (!session.closed) {
|
|
2123
|
-
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Session ended`);
|
|
2124
|
-
onSessionEnd(conversationId, session.sdkSessionId);
|
|
2125
|
-
}
|
|
2168
|
+
finishSession();
|
|
2126
2169
|
};
|
|
2127
2170
|
// Start processing loop and initialize session
|
|
2128
2171
|
const init = async () => {
|
|
2129
2172
|
processMessages().catch((err) => console.error(`[canon-host] [${conversationId.slice(0, 8)}] processMessages crashed:`, err));
|
|
2130
2173
|
await q.initializationResult();
|
|
2174
|
+
if (session.closed)
|
|
2175
|
+
return;
|
|
2131
2176
|
if (config.model) {
|
|
2132
2177
|
try {
|
|
2133
2178
|
await q.setModel(config.model);
|
|
@@ -2160,6 +2205,8 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
2160
2205
|
const models = await q.supportedModels();
|
|
2161
2206
|
const discoveredOptions = toModelOptions(models);
|
|
2162
2207
|
const supplemental = await detectSupplementalRuntimeModels(cwd, deriveClaudeSupplementalModelProbes(discoveredOptions));
|
|
2208
|
+
if (session.closed)
|
|
2209
|
+
return;
|
|
2163
2210
|
const modelList = resolveClaudeModelOptions({
|
|
2164
2211
|
discovered: mergeClaudeDiscoveredModelOptions(discoveredOptions, ...supplemental),
|
|
2165
2212
|
selectedModel: session.state.model,
|
|
@@ -2194,10 +2241,9 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
2194
2241
|
};
|
|
2195
2242
|
init().catch((err) => {
|
|
2196
2243
|
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Init failed:`, err);
|
|
2197
|
-
if (resumeSessionId)
|
|
2244
|
+
if (resumeSessionId)
|
|
2198
2245
|
session.sdkSessionId = null;
|
|
2199
|
-
|
|
2200
|
-
}
|
|
2246
|
+
finishSession();
|
|
2201
2247
|
});
|
|
2202
2248
|
return session;
|
|
2203
2249
|
}
|
|
@@ -2609,7 +2655,7 @@ export async function main() {
|
|
|
2609
2655
|
? Promise.resolve(input.hydratedPage)
|
|
2610
2656
|
: client.getMessagesPage(input.conversationId, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT).catch(() => null),
|
|
2611
2657
|
]);
|
|
2612
|
-
|
|
2658
|
+
const hydrated = buildHydratedInboundContext({
|
|
2613
2659
|
agentId,
|
|
2614
2660
|
conversationId: input.conversationId,
|
|
2615
2661
|
conversation,
|
|
@@ -2626,6 +2672,15 @@ export async function main() {
|
|
|
2626
2672
|
membershipChange: pendingMembershipChanges.get(input.conversationId) ?? null,
|
|
2627
2673
|
groupContextMode: getGroupContextMode(input.conversationId, conversation),
|
|
2628
2674
|
});
|
|
2675
|
+
return {
|
|
2676
|
+
...hydrated,
|
|
2677
|
+
replyContext: await hydrateCanonReplyContext({
|
|
2678
|
+
client,
|
|
2679
|
+
conversationId: input.conversationId,
|
|
2680
|
+
message: input.message,
|
|
2681
|
+
messages: page?.messages,
|
|
2682
|
+
}),
|
|
2683
|
+
};
|
|
2629
2684
|
}
|
|
2630
2685
|
function closeSession(conversationId, options = {}) {
|
|
2631
2686
|
const session = sessions.get(conversationId);
|
package/dist/session-state.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SDKMessageOrigin, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
|
|
1
|
+
import type { SDKMessageOrigin, SDKResultMessage, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
|
|
2
2
|
import type { DeliveryIntent, ModelOption, RuntimeStreamingPayload, TurnLifecycleState, TurnOutputBlock, TurnOutputSnapshot, TurnVerbosity } from '@canonmsg/core';
|
|
3
3
|
import type { AgentReplyAuthorityV1 } from '@canonmsg/backend-contracts';
|
|
4
4
|
import type { TurnArtifactRoutingDecision, TurnArtifactSnapshot } from '@canonmsg/coding-agent-host';
|
|
@@ -7,8 +7,10 @@ export type ClaudeArtifactRoutingMode = 'workspace-generated' | 'disabled';
|
|
|
7
7
|
export interface ClaudeInputEnvelope {
|
|
8
8
|
commandContext?: string;
|
|
9
9
|
kind: ClaudeInputKind;
|
|
10
|
-
/** Correlates this envelope with its turn result's
|
|
10
|
+
/** Correlates this envelope with its turn result's user-message UUIDs. */
|
|
11
11
|
messageUuid: string;
|
|
12
|
+
/** Other Canon inputs acknowledged by the same result; settle with its final. */
|
|
13
|
+
resultSourceMessageIds?: string[];
|
|
12
14
|
msg: SDKUserMessage;
|
|
13
15
|
intent: DeliveryIntent;
|
|
14
16
|
sourceMessageId: string | null;
|
|
@@ -150,15 +152,24 @@ export declare function shouldRouteClaudeTurnArtifacts(input: {
|
|
|
150
152
|
}): TurnArtifactRoutingDecision;
|
|
151
153
|
export declare function rememberDispatchedClaudeInput(dispatched: Map<string, ClaudeInputEnvelope>, input: ClaudeInputEnvelope): void;
|
|
152
154
|
/**
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
* The
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
|
|
159
|
-
|
|
155
|
+
* Match only the inputs the SDK explicitly acknowledges. Its plural list is in
|
|
156
|
+
* consumption order and can include messages folded in after the first reply.
|
|
157
|
+
* The last known Canon input owns the final; internal seeds cannot displace it.
|
|
158
|
+
* A singular UUID acknowledges only itself, never everything earlier in our map.
|
|
159
|
+
* Missing/unknown attribution must not fall back to an unrelated active turn.
|
|
160
|
+
*/
|
|
161
|
+
export declare function takeClaudeResultOwner(dispatched: Map<string, ClaudeInputEnvelope>, userMessageUuid: unknown, userMessageUuids?: unknown): ClaudeInputEnvelope | null;
|
|
162
|
+
/** Keep coalesced inputs pending until their shared final is delivered or retired. */
|
|
163
|
+
export declare function claudeCompletedSourceMessageIds(input: ClaudeInputEnvelope | null | undefined): string[];
|
|
164
|
+
/**
|
|
165
|
+
* A zeroed/session error may have no input UUID. Treat it as a session failure,
|
|
166
|
+
* never as successful completion of whichever Canon input happens to be active.
|
|
167
|
+
* An unattributed success while foreground work is outstanding is ambiguous too:
|
|
168
|
+
* waiting for an idle echo would wedge that input, because its slot rejects idle
|
|
169
|
+
* until completion. Close the native session and preserve the unsettled input.
|
|
170
|
+
* Explicit foreign/replayed UUIDs remain unrelated results and are ignored.
|
|
160
171
|
*/
|
|
161
|
-
export declare function
|
|
172
|
+
export declare function shouldFailClaudeSessionForUnattributedResult(result: Pick<SDKResultMessage, 'subtype' | 'is_error' | 'user_message_uuid' | 'user_message_uuids'>, hasForegroundWork: boolean): boolean;
|
|
162
173
|
/**
|
|
163
174
|
* Whether an envelope owns the session's active-turn slot. Only Canon turns do;
|
|
164
175
|
* any future internal envelope must remain invisible to user-facing turn state.
|
package/dist/session-state.js
CHANGED
|
@@ -99,28 +99,54 @@ export function rememberDispatchedClaudeInput(dispatched, input) {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* The
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* folded into that batch do not linger. Returns null when the result carries no
|
|
108
|
-
* usable uuid, leaving the caller to fall back.
|
|
102
|
+
* Match only the inputs the SDK explicitly acknowledges. Its plural list is in
|
|
103
|
+
* consumption order and can include messages folded in after the first reply.
|
|
104
|
+
* The last known Canon input owns the final; internal seeds cannot displace it.
|
|
105
|
+
* A singular UUID acknowledges only itself, never everything earlier in our map.
|
|
106
|
+
* Missing/unknown attribution must not fall back to an unrelated active turn.
|
|
109
107
|
*/
|
|
110
|
-
export function takeClaudeResultOwner(dispatched, userMessageUuid) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
dispatched.delete(key);
|
|
117
|
-
if (key === userMessageUuid) {
|
|
118
|
-
owner = envelope;
|
|
119
|
-
break;
|
|
120
|
-
}
|
|
108
|
+
export function takeClaudeResultOwner(dispatched, userMessageUuid, userMessageUuids) {
|
|
109
|
+
const uuids = Array.isArray(userMessageUuids)
|
|
110
|
+
? userMessageUuids.filter((uuid) => typeof uuid === 'string' && uuid.length > 0)
|
|
111
|
+
: [];
|
|
112
|
+
if (typeof userMessageUuid === 'string' && !uuids.includes(userMessageUuid)) {
|
|
113
|
+
uuids.push(userMessageUuid);
|
|
121
114
|
}
|
|
115
|
+
const inputs = [...new Set(uuids)].flatMap((uuid) => {
|
|
116
|
+
const input = dispatched.get(uuid);
|
|
117
|
+
return input ? [input] : [];
|
|
118
|
+
});
|
|
119
|
+
const owner = [...inputs].reverse().find((input) => input.kind === 'canon') ?? inputs.at(-1) ?? null;
|
|
120
|
+
if (!owner)
|
|
121
|
+
return null;
|
|
122
|
+
owner.resultSourceMessageIds = [...new Set(inputs.flatMap(claudeCompletedSourceMessageIds))];
|
|
123
|
+
for (const input of inputs)
|
|
124
|
+
dispatched.delete(input.messageUuid);
|
|
122
125
|
return owner;
|
|
123
126
|
}
|
|
127
|
+
/** Keep coalesced inputs pending until their shared final is delivered or retired. */
|
|
128
|
+
export function claudeCompletedSourceMessageIds(input) {
|
|
129
|
+
if (input?.kind !== 'canon')
|
|
130
|
+
return [];
|
|
131
|
+
return [...new Set([
|
|
132
|
+
...(input.sourceMessageId ? [input.sourceMessageId] : []),
|
|
133
|
+
...(input.resultSourceMessageIds ?? []),
|
|
134
|
+
])];
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* A zeroed/session error may have no input UUID. Treat it as a session failure,
|
|
138
|
+
* never as successful completion of whichever Canon input happens to be active.
|
|
139
|
+
* An unattributed success while foreground work is outstanding is ambiguous too:
|
|
140
|
+
* waiting for an idle echo would wedge that input, because its slot rejects idle
|
|
141
|
+
* until completion. Close the native session and preserve the unsettled input.
|
|
142
|
+
* Explicit foreign/replayed UUIDs remain unrelated results and are ignored.
|
|
143
|
+
*/
|
|
144
|
+
export function shouldFailClaudeSessionForUnattributedResult(result, hasForegroundWork) {
|
|
145
|
+
const hasUuid = (typeof result.user_message_uuid === 'string' && result.user_message_uuid.length > 0)
|
|
146
|
+
|| (Array.isArray(result.user_message_uuids)
|
|
147
|
+
&& result.user_message_uuids.some((uuid) => typeof uuid === 'string' && uuid.length > 0));
|
|
148
|
+
return !hasUuid && (hasForegroundWork || result.is_error || result.subtype !== 'success');
|
|
149
|
+
}
|
|
124
150
|
/**
|
|
125
151
|
* Whether an envelope owns the session's active-turn slot. Only Canon turns do;
|
|
126
152
|
* any future internal envelope must remain invisible to user-facing turn state.
|
|
@@ -1188,12 +1214,9 @@ export async function runClaudeExhaustedFinalDelivery(session, pending, deps) {
|
|
|
1188
1214
|
}
|
|
1189
1215
|
deps.retireTurn();
|
|
1190
1216
|
}
|
|
1191
|
-
//
|
|
1192
|
-
//
|
|
1193
|
-
|
|
1194
|
-
// shortens the picker instead of failing. Keep this at or above the CLI the
|
|
1195
|
-
// pinned SDK bundles, so the bundled fallback always satisfies it.
|
|
1196
|
-
export const MINIMUM_CLAUDE_CLI_VERSION = [2, 1, 220];
|
|
1217
|
+
// External CLIs must support the same input/result attribution contract as the
|
|
1218
|
+
// pinned SDK. Older binaries fall back to the bundled, audited runtime.
|
|
1219
|
+
export const MINIMUM_CLAUDE_CLI_VERSION = [2, 1, 278];
|
|
1197
1220
|
export function parseClaudeCliVersion(output) {
|
|
1198
1221
|
const match = output.trim().match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
1199
1222
|
return match ? match.slice(1, 4).map(Number) : null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/claude-code-plugin",
|
|
3
|
-
"version": "0.36.
|
|
3
|
+
"version": "0.36.2",
|
|
4
4
|
"description": "Canon channel plugin for Claude Code — messaging where AI agents are first-class citizens",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
"test": "vitest run"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@anthropic-ai/claude-agent-sdk": "0.3.
|
|
34
|
-
"@canonmsg/agent-sdk": "^11.0.
|
|
33
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.278",
|
|
34
|
+
"@canonmsg/agent-sdk": "^11.0.1",
|
|
35
35
|
"@canonmsg/agent-tools": "^0.11.0",
|
|
36
36
|
"@canonmsg/coding-agent-host": "^0.9.0",
|
|
37
|
-
"@canonmsg/core": "^13.0.
|
|
37
|
+
"@canonmsg/core": "^13.0.4",
|
|
38
38
|
"@canonmsg/rich-cards": "^0.10.6",
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.30.0"
|
|
40
40
|
},
|