@canonmsg/claude-code-plugin 0.30.0 → 0.31.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 +56 -50
- package/dist/session-state.d.ts +53 -1
- package/dist/session-state.js +62 -2
- package/package.json +3 -3
package/dist/host.js
CHANGED
|
@@ -27,13 +27,13 @@ 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 { isAnthropicImageAttachment, materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMessage, toAnthropicImageBlock, } from '@canonmsg/agent-sdk';
|
|
30
|
-
import { captureTurnArtifactSnapshot,
|
|
31
|
-
import { EFFORT_OPTIONS, CLAUDE_PERMISSION_MODE_OPTIONS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildCanonInboundFrameV1, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, normalizeRuntimeCommandDescriptors, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createTurnOutputController, createRuntimeStatePublisher, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, CanonClient, CanonStream, ControlChannelPoller, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, ApprovalManager, RuntimeRequestManager, ExecutionEnvironmentError, FINAL_MESSAGE_HANDOFF_MS, buildLocalRuntimeId, getActiveProfileLock, heartbeatLocalRuntimeEntry, normalizeTurnMetadata, parseTurnVerbosityConfig, prepareConversationEnvironment, resolveCanonAgent, verifyResolvedAgentEnvironment, decideAutoReply, initRTDBAuth, isChunkedSendMessageError, sendMessageWithRetry, sendMessageWithRetryChunked, loadHostSessionConfig, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, saveRuntimeSessionState, clearRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd,
|
|
30
|
+
import { captureTurnArtifactSnapshot, createTurnArtifactRouter, IDLE_TIMEOUT_MS, collectMissedInboundMessages, createReconnectRecoveryGate, STARTUP_RECOVERY_MAX_MESSAGES, STARTUP_RECOVERY_PAGE_SIZE, } from '@canonmsg/coding-agent-host';
|
|
31
|
+
import { EFFORT_OPTIONS, CLAUDE_PERMISSION_MODE_OPTIONS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildCanonInboundFrameV1, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, normalizeRuntimeCommandDescriptors, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createTurnOutputController, createRuntimeStatePublisher, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, CanonClient, CanonStream, ControlChannelPoller, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, ApprovalManager, RuntimeRequestManager, ExecutionEnvironmentError, FINAL_MESSAGE_HANDOFF_MS, buildLocalRuntimeId, getActiveProfileLock, heartbeatLocalRuntimeEntry, normalizeTurnMetadata, parseTurnVerbosityConfig, prepareConversationEnvironment, resolveCanonAgent, verifyResolvedAgentEnvironment, decideAutoReply, initRTDBAuth, isChunkedSendMessageError, sendMessageWithRetry, sendMessageWithRetryChunked, loadHostSessionConfig, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, saveRuntimeSessionState, clearRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd, resolveTurnVerbosity, shouldTriggerAgentTurn, upsertLocalRuntimeEntry, } from '@canonmsg/core';
|
|
32
32
|
import { runCli } from '@canonmsg/core';
|
|
33
33
|
import { CANON_VERB_MCP_SERVER_NAME, createCanonVerbMcpServer } from '@canonmsg/agent-tools';
|
|
34
34
|
import { synthesizeClaudeApprovalDiff } from './approval-diff.js';
|
|
35
35
|
import { decideClaudeToolPermissionForMode, parseAllowedNonOwnerClaudeTools, } from './tool-policy.js';
|
|
36
|
-
import { applyClaudeSessionControl, boundClaudeFinalMetadata, buildClaudeFinalChunkingOptions, buildClaudeFinalMessageId, buildClaudeFinalTurnMetadata, buildClaudePendingFinalDelivery, buildClaudeTurnFailureNotice, buildTruncatedFinalText, buildUndeliverableFinalNotice, canDrainClaudeQueuedInput, claudeFinalWillChunk, classifyFinalDeliveryFailure, claudeInputOwnsTurnSlot, createClaudeTurnActivityState, decideClaudeControlSignalAction, decideClaudeInboundDispatch, dispatchClaudeInput, isClaudeTurnSlotReserved, readClaudeFinalDeliveryResume, releaseClaudeTurnSlot, runClaudeExhaustedFinalDelivery, shouldApplyClaudeEchoedSessionState, shouldReleaseClaudeTurnSlot, isClaudeMainTurnMessage, openClaudeTurn, planClaudeAssistantTrail, planClaudeStreamingWrite, planClaudeToolCallStart, planClaudeToolProgress, planClaudeToolResults, claudeFinalTurnTrail, publishedClaudeTurnState, rememberDispatchedClaudeInput, resetClaudeTurnActivityState, shouldOpenClaudeTurnOnRunning, shouldStopTypingDotsOnStreamedText, isOpenClaudeTurnState, takeClaudeResultOwner, takeClaudeToolBlockIdByIndex, claudeModelInfoToOption, claudeOriginForCanonSender,
|
|
36
|
+
import { applyClaudeSessionControl, boundClaudeFinalMetadata, buildClaudeFinalChunkingOptions, buildClaudeFinalMessageId, buildClaudeFinalTurnMetadata, buildClaudePendingFinalDelivery, buildClaudeTurnFailureNotice, buildTruncatedFinalText, buildUndeliverableFinalNotice, canDrainClaudeQueuedInput, claudeFinalWillChunk, classifyFinalDeliveryFailure, claudeInputOwnsTurnSlot, createClaudeTurnActivityState, decideClaudeControlSignalAction, decideClaudeInboundDispatch, dispatchClaudeInput, 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';
|
|
37
37
|
import { CLAUDE_SUPPORTED_DIALOG_KINDS, buildClaudeAskUserPermissionDenied, buildClaudeAskUserPermissionResult, createClaudeUserDialogCoordinator, parseClaudeAskUserDialog, parseClaudeAskUserToolInput, resolveClaudeUserDialogRequestId, } from './user-dialog.js';
|
|
38
38
|
function parseRuntimeVisibilityPreset(value) {
|
|
39
39
|
return value === 'normal' || value === 'minimal' || value === 'full' ? value : undefined;
|
|
@@ -1600,37 +1600,32 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1600
1600
|
metadata: buildArtifactMediaMetadata(),
|
|
1601
1601
|
});
|
|
1602
1602
|
}
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
console.error(`[canon-host] [${conversationId.slice(0, 8)}]
|
|
1628
|
-
}
|
|
1629
|
-
}
|
|
1630
|
-
async function routeArtifactsForInput(input) {
|
|
1631
|
-
if (input.artifactRoutingMode === 'workspace-generated') {
|
|
1632
|
-
await routeWorkspaceGeneratedArtifacts(input);
|
|
1633
|
-
}
|
|
1603
|
+
/**
|
|
1604
|
+
* The one path from a completed turn to posting its workspace artifacts.
|
|
1605
|
+
*
|
|
1606
|
+
* The router owns the collect-and-post loop, so no completion branch can
|
|
1607
|
+
* reach the workspace around the gate. It is built per turn rather than per
|
|
1608
|
+
* session because its once-only latch is a TURN's latch — Codex reaches
|
|
1609
|
+
* completion through five branches and needs it; this host calls it once,
|
|
1610
|
+
* and gets the same guarantee for free.
|
|
1611
|
+
*
|
|
1612
|
+
* `finalText` is the model's own reply, which decides whether the turn's
|
|
1613
|
+
* silence actually suppresses anything — the same reading
|
|
1614
|
+
* `composeClaudeTurnFinal` makes for the text.
|
|
1615
|
+
*/
|
|
1616
|
+
function routeArtifactsForInput(input, finalText) {
|
|
1617
|
+
return createTurnArtifactRouter({
|
|
1618
|
+
decide: () => shouldRouteClaudeTurnArtifacts({
|
|
1619
|
+
turn: input,
|
|
1620
|
+
interruptedTurnKeys: session.interruptedTurnKeys,
|
|
1621
|
+
silencedTurnKeys: session.silencedTurnKeys,
|
|
1622
|
+
finalText,
|
|
1623
|
+
}),
|
|
1624
|
+
baseline: () => input.artifactBaseline,
|
|
1625
|
+
cwd: () => session.cwd,
|
|
1626
|
+
send: (file) => sendTurnArtifactFile(file),
|
|
1627
|
+
log: (line) => console.error(`[canon-host] [${conversationId.slice(0, 8)}] ${line}`),
|
|
1628
|
+
}).route();
|
|
1634
1629
|
}
|
|
1635
1630
|
function scheduleFinalDeliveryRetry(pending = session.pendingFinalDelivery) {
|
|
1636
1631
|
if (!pending || session.closed || session.pendingFinalDelivery?.turnKey !== pending.turnKey)
|
|
@@ -2218,11 +2213,6 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
2218
2213
|
// working, at their existing accuracy.
|
|
2219
2214
|
const completedInput = takeClaudeResultOwner(dispatchedInputs, msg.user_message_uuid) ?? session.activeInput;
|
|
2220
2215
|
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Turn complete (${msg.subtype})`);
|
|
2221
|
-
// Turn artifacts land before the final text reply.
|
|
2222
|
-
if (completedInput?.kind === 'canon'
|
|
2223
|
-
&& !session.interruptedTurnKeys.has(completedInput.turnKey)) {
|
|
2224
|
-
await routeArtifactsForInput(completedInput);
|
|
2225
|
-
}
|
|
2226
2216
|
const resultText = typeof msg.result === 'string'
|
|
2227
2217
|
? msg.result.trim()
|
|
2228
2218
|
: '';
|
|
@@ -2237,25 +2227,42 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
2237
2227
|
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Turn failed (${msg.subtype}): `
|
|
2238
2228
|
+ (errorList.join(' | ') || 'no error detail'));
|
|
2239
2229
|
}
|
|
2240
|
-
const
|
|
2230
|
+
const silenced = Boolean(completedInput?.turnKey && session.silencedTurnKeys.has(completedInput.turnKey));
|
|
2231
|
+
// Composed before the artifacts are routed, because the artifact
|
|
2232
|
+
// gate weighs silence against this same model text — a turn cannot
|
|
2233
|
+
// speak and withhold its files, or the reverse. Composing is pure;
|
|
2234
|
+
// the artifacts still POST first, ahead of the reply they belong to.
|
|
2235
|
+
const turnFinal = composeClaudeTurnFinal({
|
|
2241
2236
|
resultText,
|
|
2242
2237
|
streamedText: session.pendingFinalText,
|
|
2243
2238
|
failureNotice,
|
|
2239
|
+
silenced,
|
|
2244
2240
|
});
|
|
2245
|
-
const
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
const suppressFinal = silenced && !failureNotice;
|
|
2251
|
-
const delivery = resolveSilentTurnDelivery({ silenced: suppressFinal, finalText });
|
|
2252
|
-
const shouldDeliverFinal = delivery === 'deliver'
|
|
2241
|
+
const finalText = turnFinal.finalText;
|
|
2242
|
+
if (completedInput?.kind === 'canon') {
|
|
2243
|
+
await routeArtifactsForInput(completedInput, turnFinal.modelText);
|
|
2244
|
+
}
|
|
2245
|
+
const shouldDeliverFinal = finalText
|
|
2253
2246
|
? shouldDeliverClaudeFinal({
|
|
2254
2247
|
turn: completedInput,
|
|
2255
2248
|
finalizedTurnKeys: session.finalizedTurnKeys,
|
|
2256
2249
|
interruptedTurnKeys: session.interruptedTurnKeys,
|
|
2257
2250
|
})
|
|
2258
2251
|
: false;
|
|
2252
|
+
// Silence drops the model's own words without sending them. Say so:
|
|
2253
|
+
// a suppressed final is invisible in the conversation, and the
|
|
2254
|
+
// failure notice that may follow it is Canon's line, not the
|
|
2255
|
+
// model's, so nothing else would record what was swallowed.
|
|
2256
|
+
if (turnFinal.speechSuppressed && turnFinal.modelText) {
|
|
2257
|
+
console.error(`[canon-host] [${conversationId.slice(0, 8)}] `
|
|
2258
|
+
+ `Final not delivered (${turnFinal.modelText.length} chars): `
|
|
2259
|
+
+ describeUndeliveredClaudeFinal({
|
|
2260
|
+
turn: completedInput,
|
|
2261
|
+
finalizedTurnKeys: session.finalizedTurnKeys,
|
|
2262
|
+
interruptedTurnKeys: session.interruptedTurnKeys,
|
|
2263
|
+
silencedTurnKeys: session.silencedTurnKeys,
|
|
2264
|
+
}));
|
|
2265
|
+
}
|
|
2259
2266
|
// Gated finals are dropped without sending. That is correct for the
|
|
2260
2267
|
// seed's own turn, but it is also how a misattributed Canon reply
|
|
2261
2268
|
// disappears — silently, which is why this class of bug went unseen.
|
|
@@ -2267,7 +2274,6 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
2267
2274
|
turn: completedInput,
|
|
2268
2275
|
finalizedTurnKeys: session.finalizedTurnKeys,
|
|
2269
2276
|
interruptedTurnKeys: session.interruptedTurnKeys,
|
|
2270
|
-
silencedTurnKeys: suppressFinal ? session.silencedTurnKeys : undefined,
|
|
2271
2277
|
}));
|
|
2272
2278
|
}
|
|
2273
2279
|
const finalDelivered = finalText && shouldDeliverFinal
|
|
@@ -2298,7 +2304,7 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
2298
2304
|
if (shouldDeliverFinal && finalText) {
|
|
2299
2305
|
scheduleFinalHandoffReset();
|
|
2300
2306
|
}
|
|
2301
|
-
else if (
|
|
2307
|
+
else if (turnFinal.speechSuppressed) {
|
|
2302
2308
|
// Deliberate silence: blank the node (text '' AND an explicit
|
|
2303
2309
|
// empty blocks array) before deleting it, or onStreamingCleared
|
|
2304
2310
|
// salvages this turn's narration into a durable bubble. Teardown
|
package/dist/session-state.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PermissionMode, SDKMessageOrigin, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
|
|
2
2
|
import type { DeliveryIntent, ModelOption, RuntimeStreamingPayload, TurnLifecycleState, TurnOutputBlock, TurnOutputSnapshot, TurnVerbosity } from '@canonmsg/core';
|
|
3
|
-
import type { TurnArtifactSnapshot } from '@canonmsg/coding-agent-host';
|
|
3
|
+
import type { TurnArtifactRoutingDecision, TurnArtifactSnapshot } from '@canonmsg/coding-agent-host';
|
|
4
4
|
export type ClaudeInputKind = 'seed' | 'canon';
|
|
5
5
|
export type ClaudeArtifactRoutingMode = 'workspace-generated' | 'disabled';
|
|
6
6
|
export interface ClaudeInputEnvelope {
|
|
@@ -118,6 +118,23 @@ export declare function shouldDeliverClaudeFinal(input: {
|
|
|
118
118
|
finalizedTurnKeys: ReadonlySet<string>;
|
|
119
119
|
interruptedTurnKeys: ReadonlySet<string>;
|
|
120
120
|
}): boolean;
|
|
121
|
+
/**
|
|
122
|
+
* The artifact half of `shouldDeliverClaudeFinal`: workspace media is posted as
|
|
123
|
+
* a message, so the same turn-level gates apply to it. Silence is the one this
|
|
124
|
+
* host was missing — see `resolveTurnArtifactRouting` for the ruling.
|
|
125
|
+
*
|
|
126
|
+
* The only place silence is resolved for Claude artifacts. `finalText` is the
|
|
127
|
+
* model's own reply for the turn (never Canon's failure notice, which is a host
|
|
128
|
+
* diagnostic and goes out either way), weighed by `isSilentTurnSuppressed` —
|
|
129
|
+
* the same switch `composeClaudeTurnFinal` reads. One switch, so flipping
|
|
130
|
+
* `DEFAULT_SILENT_TURN_PRECEDENCE` moves a turn's text and its files together.
|
|
131
|
+
*/
|
|
132
|
+
export declare function shouldRouteClaudeTurnArtifacts(input: {
|
|
133
|
+
turn: Pick<ClaudeInputEnvelope, 'artifactRoutingMode' | 'turnKey'>;
|
|
134
|
+
interruptedTurnKeys: ReadonlySet<string>;
|
|
135
|
+
silencedTurnKeys: ReadonlySet<string>;
|
|
136
|
+
finalText: string | null | undefined;
|
|
137
|
+
}): TurnArtifactRoutingDecision;
|
|
121
138
|
export declare function rememberDispatchedClaudeInput(dispatched: Map<string, ClaudeInputEnvelope>, input: ClaudeInputEnvelope): void;
|
|
122
139
|
/**
|
|
123
140
|
* The envelope a turn result belongs to, matched by `user_message_uuid`.
|
|
@@ -497,6 +514,41 @@ export declare function composeClaudeFinalText(input: {
|
|
|
497
514
|
streamedText?: string | null;
|
|
498
515
|
failureNotice?: string | null;
|
|
499
516
|
}): string | null;
|
|
517
|
+
export interface ClaudeTurnFinal {
|
|
518
|
+
/** What actually goes to Canon, or null when the turn says nothing. */
|
|
519
|
+
finalText: string | null;
|
|
520
|
+
/** The model's own text for the turn, whether or not it is delivered. */
|
|
521
|
+
modelText: string | null;
|
|
522
|
+
/**
|
|
523
|
+
* Silence — not "no text" — is why the model's own words are being dropped.
|
|
524
|
+
* The artifact gate reads the same verdict, so a silenced turn withholds its
|
|
525
|
+
* files and its speech together.
|
|
526
|
+
*/
|
|
527
|
+
speechSuppressed: boolean;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* What a completed turn actually says, once `no_reply` is taken into account.
|
|
531
|
+
*
|
|
532
|
+
* Silence suppresses the MODEL's output and nothing else — its result text or,
|
|
533
|
+
* failing that, the text it streamed before going quiet. Canon's own "this turn
|
|
534
|
+
* broke" notice still goes out: suppressing it would leave the owner with a
|
|
535
|
+
* turn that failed and said nothing, which is exactly what the notice exists to
|
|
536
|
+
* prevent.
|
|
537
|
+
*
|
|
538
|
+
* The two used to be merged. `composeClaudeFinalText` appends the notice to
|
|
539
|
+
* whatever partial text exists, and the host then turned silence OFF entirely
|
|
540
|
+
* whenever a notice was present — so a turn that streamed "Here's the chart:",
|
|
541
|
+
* called `no_reply`, and then hit `error_max_turns` posted the model's line
|
|
542
|
+
* next to the notice while withholding the chart itself. Codex has never done
|
|
543
|
+
* that: its failure branches send `formatCodexTurnFailure(...)` alone and never
|
|
544
|
+
* `result.finalMessage`. This is the same contract on both hosts.
|
|
545
|
+
*/
|
|
546
|
+
export declare function composeClaudeTurnFinal(input: {
|
|
547
|
+
resultText?: string | null;
|
|
548
|
+
streamedText?: string | null;
|
|
549
|
+
failureNotice?: string | null;
|
|
550
|
+
silenced: boolean;
|
|
551
|
+
}): ClaudeTurnFinal;
|
|
500
552
|
export type ClaudeFinalDeliveryFailure = 'retry' | 'permanent';
|
|
501
553
|
/**
|
|
502
554
|
* How the host should react to a failed final-reply send.
|
package/dist/session-state.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { USAGE_LIMIT_ERROR_PREFIXES } from '@anthropic-ai/claude-agent-sdk';
|
|
3
|
-
import { buildBoundedTurnTrail, DEFAULT_CHUNKED_MESSAGE_TEXT_MAX_BYTES, isRetryableCanonDeliveryError, shouldPublishTurnTrail, utf8ByteLength, VERB_LIMITS, } from '@canonmsg/core';
|
|
4
|
-
import { boundTrailBlockMap, buildTrailBlockId, buildUndeliverableFinalNotice as buildHostUndeliverableFinalNotice, normalizePlanStepStatus, normalizeTrailKey, PLAN_BLOCK_TITLE, renderPlanSteps, truncateFailureDetail, } from '@canonmsg/coding-agent-host';
|
|
3
|
+
import { buildBoundedTurnTrail, DEFAULT_CHUNKED_MESSAGE_TEXT_MAX_BYTES, isRetryableCanonDeliveryError, isSilentTurnSuppressed, shouldPublishTurnTrail, utf8ByteLength, VERB_LIMITS, } from '@canonmsg/core';
|
|
4
|
+
import { boundTrailBlockMap, buildTrailBlockId, buildUndeliverableFinalNotice as buildHostUndeliverableFinalNotice, normalizePlanStepStatus, normalizeTrailKey, PLAN_BLOCK_TITLE, renderPlanSteps, resolveTurnArtifactRouting, truncateFailureDetail, } from '@canonmsg/coding-agent-host';
|
|
5
5
|
export function createClaudeInputEnvelope(input) {
|
|
6
6
|
const sourceMessageId = input.sourceMessageId ?? null;
|
|
7
7
|
const fallbackId = randomUUID();
|
|
@@ -60,6 +60,27 @@ export function shouldDeliverClaudeFinal(input) {
|
|
|
60
60
|
return false;
|
|
61
61
|
return true;
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* The artifact half of `shouldDeliverClaudeFinal`: workspace media is posted as
|
|
65
|
+
* a message, so the same turn-level gates apply to it. Silence is the one this
|
|
66
|
+
* host was missing — see `resolveTurnArtifactRouting` for the ruling.
|
|
67
|
+
*
|
|
68
|
+
* The only place silence is resolved for Claude artifacts. `finalText` is the
|
|
69
|
+
* model's own reply for the turn (never Canon's failure notice, which is a host
|
|
70
|
+
* diagnostic and goes out either way), weighed by `isSilentTurnSuppressed` —
|
|
71
|
+
* the same switch `composeClaudeTurnFinal` reads. One switch, so flipping
|
|
72
|
+
* `DEFAULT_SILENT_TURN_PRECEDENCE` moves a turn's text and its files together.
|
|
73
|
+
*/
|
|
74
|
+
export function shouldRouteClaudeTurnArtifacts(input) {
|
|
75
|
+
return resolveTurnArtifactRouting({
|
|
76
|
+
artifactRoutingMode: input.turn.artifactRoutingMode,
|
|
77
|
+
interrupted: input.interruptedTurnKeys.has(input.turn.turnKey),
|
|
78
|
+
silenced: isSilentTurnSuppressed({
|
|
79
|
+
silenced: input.silencedTurnKeys.has(input.turn.turnKey),
|
|
80
|
+
finalText: input.finalText,
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
63
84
|
/** Bounds the correlation map if the SDK ever stops reporting a uuid we sent. */
|
|
64
85
|
const MAX_TRACKED_DISPATCHED_INPUTS = 64;
|
|
65
86
|
export function rememberDispatchedClaudeInput(dispatched, input) {
|
|
@@ -777,6 +798,45 @@ export function composeClaudeFinalText(input) {
|
|
|
777
798
|
return primary;
|
|
778
799
|
return primary ? `${primary}\n\n${input.failureNotice}` : input.failureNotice;
|
|
779
800
|
}
|
|
801
|
+
/**
|
|
802
|
+
* What a completed turn actually says, once `no_reply` is taken into account.
|
|
803
|
+
*
|
|
804
|
+
* Silence suppresses the MODEL's output and nothing else — its result text or,
|
|
805
|
+
* failing that, the text it streamed before going quiet. Canon's own "this turn
|
|
806
|
+
* broke" notice still goes out: suppressing it would leave the owner with a
|
|
807
|
+
* turn that failed and said nothing, which is exactly what the notice exists to
|
|
808
|
+
* prevent.
|
|
809
|
+
*
|
|
810
|
+
* The two used to be merged. `composeClaudeFinalText` appends the notice to
|
|
811
|
+
* whatever partial text exists, and the host then turned silence OFF entirely
|
|
812
|
+
* whenever a notice was present — so a turn that streamed "Here's the chart:",
|
|
813
|
+
* called `no_reply`, and then hit `error_max_turns` posted the model's line
|
|
814
|
+
* next to the notice while withholding the chart itself. Codex has never done
|
|
815
|
+
* that: its failure branches send `formatCodexTurnFailure(...)` alone and never
|
|
816
|
+
* `result.finalMessage`. This is the same contract on both hosts.
|
|
817
|
+
*/
|
|
818
|
+
export function composeClaudeTurnFinal(input) {
|
|
819
|
+
const modelText = composeClaudeFinalText({
|
|
820
|
+
resultText: input.resultText,
|
|
821
|
+
streamedText: input.streamedText,
|
|
822
|
+
});
|
|
823
|
+
const speechSuppressed = isSilentTurnSuppressed({
|
|
824
|
+
silenced: input.silenced,
|
|
825
|
+
finalText: modelText,
|
|
826
|
+
});
|
|
827
|
+
if (speechSuppressed) {
|
|
828
|
+
return {
|
|
829
|
+
finalText: input.failureNotice?.trim() || null,
|
|
830
|
+
modelText,
|
|
831
|
+
speechSuppressed: true,
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
return {
|
|
835
|
+
finalText: composeClaudeFinalText(input),
|
|
836
|
+
modelText,
|
|
837
|
+
speechSuppressed: false,
|
|
838
|
+
};
|
|
839
|
+
}
|
|
780
840
|
/**
|
|
781
841
|
* How the host should react to a failed final-reply send.
|
|
782
842
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/claude-code-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.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",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"@anthropic-ai/claude-agent-sdk": "0.3.220",
|
|
34
34
|
"@canonmsg/agent-sdk": "^8.2.0",
|
|
35
35
|
"@canonmsg/agent-tools": "^0.4.0",
|
|
36
|
-
"@canonmsg/coding-agent-host": "^0.
|
|
37
|
-
"@canonmsg/core": "^10.
|
|
36
|
+
"@canonmsg/coding-agent-host": "^0.5.0",
|
|
37
|
+
"@canonmsg/core": "^10.1.0",
|
|
38
38
|
"@canonmsg/rich-cards": "^0.9.1",
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
40
40
|
},
|