@canonmsg/claude-code-plugin 0.30.0 → 0.31.1
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 +59 -50
- package/dist/session-state.d.ts +53 -1
- package/dist/session-state.js +62 -2
- package/package.json +5 -5
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;
|
|
@@ -981,6 +981,9 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
981
981
|
conversationId,
|
|
982
982
|
...(session.currentTurnId ? { turnId: session.currentTurnId } : {}),
|
|
983
983
|
...(routing.responseUserId ? { responseUserId: routing.responseUserId } : {}),
|
|
984
|
+
...(session.activeInput?.sourceMessageId
|
|
985
|
+
? { sourceMessageId: session.activeInput.sourceMessageId }
|
|
986
|
+
: {}),
|
|
984
987
|
};
|
|
985
988
|
}, (verb) => {
|
|
986
989
|
if (verb !== 'no_reply')
|
|
@@ -1600,37 +1603,32 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1600
1603
|
metadata: buildArtifactMediaMetadata(),
|
|
1601
1604
|
});
|
|
1602
1605
|
}
|
|
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
|
-
}
|
|
1606
|
+
/**
|
|
1607
|
+
* The one path from a completed turn to posting its workspace artifacts.
|
|
1608
|
+
*
|
|
1609
|
+
* The router owns the collect-and-post loop, so no completion branch can
|
|
1610
|
+
* reach the workspace around the gate. It is built per turn rather than per
|
|
1611
|
+
* session because its once-only latch is a TURN's latch — Codex reaches
|
|
1612
|
+
* completion through five branches and needs it; this host calls it once,
|
|
1613
|
+
* and gets the same guarantee for free.
|
|
1614
|
+
*
|
|
1615
|
+
* `finalText` is the model's own reply, which decides whether the turn's
|
|
1616
|
+
* silence actually suppresses anything — the same reading
|
|
1617
|
+
* `composeClaudeTurnFinal` makes for the text.
|
|
1618
|
+
*/
|
|
1619
|
+
function routeArtifactsForInput(input, finalText) {
|
|
1620
|
+
return createTurnArtifactRouter({
|
|
1621
|
+
decide: () => shouldRouteClaudeTurnArtifacts({
|
|
1622
|
+
turn: input,
|
|
1623
|
+
interruptedTurnKeys: session.interruptedTurnKeys,
|
|
1624
|
+
silencedTurnKeys: session.silencedTurnKeys,
|
|
1625
|
+
finalText,
|
|
1626
|
+
}),
|
|
1627
|
+
baseline: () => input.artifactBaseline,
|
|
1628
|
+
cwd: () => session.cwd,
|
|
1629
|
+
send: (file) => sendTurnArtifactFile(file),
|
|
1630
|
+
log: (line) => console.error(`[canon-host] [${conversationId.slice(0, 8)}] ${line}`),
|
|
1631
|
+
}).route();
|
|
1634
1632
|
}
|
|
1635
1633
|
function scheduleFinalDeliveryRetry(pending = session.pendingFinalDelivery) {
|
|
1636
1634
|
if (!pending || session.closed || session.pendingFinalDelivery?.turnKey !== pending.turnKey)
|
|
@@ -2218,11 +2216,6 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
2218
2216
|
// working, at their existing accuracy.
|
|
2219
2217
|
const completedInput = takeClaudeResultOwner(dispatchedInputs, msg.user_message_uuid) ?? session.activeInput;
|
|
2220
2218
|
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
2219
|
const resultText = typeof msg.result === 'string'
|
|
2227
2220
|
? msg.result.trim()
|
|
2228
2221
|
: '';
|
|
@@ -2237,25 +2230,42 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
2237
2230
|
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Turn failed (${msg.subtype}): `
|
|
2238
2231
|
+ (errorList.join(' | ') || 'no error detail'));
|
|
2239
2232
|
}
|
|
2240
|
-
const
|
|
2233
|
+
const silenced = Boolean(completedInput?.turnKey && session.silencedTurnKeys.has(completedInput.turnKey));
|
|
2234
|
+
// Composed before the artifacts are routed, because the artifact
|
|
2235
|
+
// gate weighs silence against this same model text — a turn cannot
|
|
2236
|
+
// speak and withhold its files, or the reverse. Composing is pure;
|
|
2237
|
+
// the artifacts still POST first, ahead of the reply they belong to.
|
|
2238
|
+
const turnFinal = composeClaudeTurnFinal({
|
|
2241
2239
|
resultText,
|
|
2242
2240
|
streamedText: session.pendingFinalText,
|
|
2243
2241
|
failureNotice,
|
|
2242
|
+
silenced,
|
|
2244
2243
|
});
|
|
2245
|
-
const
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
const suppressFinal = silenced && !failureNotice;
|
|
2251
|
-
const delivery = resolveSilentTurnDelivery({ silenced: suppressFinal, finalText });
|
|
2252
|
-
const shouldDeliverFinal = delivery === 'deliver'
|
|
2244
|
+
const finalText = turnFinal.finalText;
|
|
2245
|
+
if (completedInput?.kind === 'canon') {
|
|
2246
|
+
await routeArtifactsForInput(completedInput, turnFinal.modelText);
|
|
2247
|
+
}
|
|
2248
|
+
const shouldDeliverFinal = finalText
|
|
2253
2249
|
? shouldDeliverClaudeFinal({
|
|
2254
2250
|
turn: completedInput,
|
|
2255
2251
|
finalizedTurnKeys: session.finalizedTurnKeys,
|
|
2256
2252
|
interruptedTurnKeys: session.interruptedTurnKeys,
|
|
2257
2253
|
})
|
|
2258
2254
|
: false;
|
|
2255
|
+
// Silence drops the model's own words without sending them. Say so:
|
|
2256
|
+
// a suppressed final is invisible in the conversation, and the
|
|
2257
|
+
// failure notice that may follow it is Canon's line, not the
|
|
2258
|
+
// model's, so nothing else would record what was swallowed.
|
|
2259
|
+
if (turnFinal.speechSuppressed && turnFinal.modelText) {
|
|
2260
|
+
console.error(`[canon-host] [${conversationId.slice(0, 8)}] `
|
|
2261
|
+
+ `Final not delivered (${turnFinal.modelText.length} chars): `
|
|
2262
|
+
+ describeUndeliveredClaudeFinal({
|
|
2263
|
+
turn: completedInput,
|
|
2264
|
+
finalizedTurnKeys: session.finalizedTurnKeys,
|
|
2265
|
+
interruptedTurnKeys: session.interruptedTurnKeys,
|
|
2266
|
+
silencedTurnKeys: session.silencedTurnKeys,
|
|
2267
|
+
}));
|
|
2268
|
+
}
|
|
2259
2269
|
// Gated finals are dropped without sending. That is correct for the
|
|
2260
2270
|
// seed's own turn, but it is also how a misattributed Canon reply
|
|
2261
2271
|
// disappears — silently, which is why this class of bug went unseen.
|
|
@@ -2267,7 +2277,6 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
2267
2277
|
turn: completedInput,
|
|
2268
2278
|
finalizedTurnKeys: session.finalizedTurnKeys,
|
|
2269
2279
|
interruptedTurnKeys: session.interruptedTurnKeys,
|
|
2270
|
-
silencedTurnKeys: suppressFinal ? session.silencedTurnKeys : undefined,
|
|
2271
2280
|
}));
|
|
2272
2281
|
}
|
|
2273
2282
|
const finalDelivered = finalText && shouldDeliverFinal
|
|
@@ -2298,7 +2307,7 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
2298
2307
|
if (shouldDeliverFinal && finalText) {
|
|
2299
2308
|
scheduleFinalHandoffReset();
|
|
2300
2309
|
}
|
|
2301
|
-
else if (
|
|
2310
|
+
else if (turnFinal.speechSuppressed) {
|
|
2302
2311
|
// Deliberate silence: blank the node (text '' AND an explicit
|
|
2303
2312
|
// empty blocks array) before deleting it, or onStreamingCleared
|
|
2304
2313
|
// 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.1",
|
|
4
4
|
"description": "Canon channel plugin for Claude Code — messaging where AI agents are first-class citizens",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@anthropic-ai/claude-agent-sdk": "0.3.220",
|
|
34
|
-
"@canonmsg/agent-sdk": "^8.
|
|
35
|
-
"@canonmsg/agent-tools": "^0.
|
|
36
|
-
"@canonmsg/coding-agent-host": "^0.
|
|
37
|
-
"@canonmsg/core": "^10.
|
|
34
|
+
"@canonmsg/agent-sdk": "^8.3.0",
|
|
35
|
+
"@canonmsg/agent-tools": "^0.5.0",
|
|
36
|
+
"@canonmsg/coding-agent-host": "^0.5.0",
|
|
37
|
+
"@canonmsg/core": "^10.2.0",
|
|
38
38
|
"@canonmsg/rich-cards": "^0.9.1",
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
40
40
|
},
|