@canonmsg/claude-code-plugin 0.36.2 → 0.36.4
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 +85 -88
- package/dist/session-state.d.ts +5 -1
- package/dist/session-state.js +17 -3
- package/package.json +11 -6
package/dist/host.js
CHANGED
|
@@ -30,7 +30,7 @@ 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, 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,
|
|
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, 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';
|
|
@@ -1643,16 +1643,9 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1643
1643
|
.updateMessageDisposition(conversationId, input.sourceMessageId, 'rejected')
|
|
1644
1644
|
.catch(() => { });
|
|
1645
1645
|
}
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
const entry = await engine.getInbound(id);
|
|
1650
|
-
if (entry && ['observed', 'offered', 'deferred'].includes(entry.state)) {
|
|
1651
|
-
await engine.setInboundState(id, 'deferred', 'native-preflight');
|
|
1652
|
-
config.onInputDeferred?.(input.sourceMessageId);
|
|
1653
|
-
}
|
|
1654
|
-
}).catch((error) => console.error('[canon-host] Failed to defer native input:', error));
|
|
1655
|
-
}
|
|
1646
|
+
void input.inboundHandoff?.release('native-preflight').catch((error) => {
|
|
1647
|
+
console.error('[canon-host] Failed to release native input:', error);
|
|
1648
|
+
});
|
|
1656
1649
|
// resetTurnToIdle releases the slot, tears the published turn down (typing
|
|
1657
1650
|
// signal, seeded /streaming, turn state) and drains the queue. On a closed
|
|
1658
1651
|
// session it is a no-op, which is correct: the teardown owns that path.
|
|
@@ -1662,17 +1655,26 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1662
1655
|
return dispatchClaudeInput(session, input, {
|
|
1663
1656
|
openTurn,
|
|
1664
1657
|
markAccepted: (dispatched) => markQueuedMessageAccepted(dispatched.sourceMessageId, dispatched.markAccepted),
|
|
1665
|
-
prepareArtifacts:
|
|
1666
|
-
|
|
1667
|
-
if (input.kind
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
if (!await engine.claimInbound(id)) {
|
|
1671
|
-
throw new Error('Canon input already has an execution owner.');
|
|
1672
|
-
}
|
|
1658
|
+
prepareArtifacts: prepareArtifactsForInput,
|
|
1659
|
+
send: async (input) => {
|
|
1660
|
+
if (input.kind !== 'canon' || !input.sourceMessageId) {
|
|
1661
|
+
sendInput(input);
|
|
1662
|
+
return;
|
|
1673
1663
|
}
|
|
1664
|
+
const engine = await client.getEndpoint();
|
|
1665
|
+
input.inboundHandoff ??= await engine.acquireInbound(`message:${conversationId}:${input.sourceMessageId}`) ?? undefined;
|
|
1666
|
+
if (!input.inboundHandoff)
|
|
1667
|
+
throw new Error('Canon input already has an execution owner.');
|
|
1668
|
+
if (session.closed)
|
|
1669
|
+
await input.inboundHandoff.release('session-closed');
|
|
1670
|
+
const started = await engine.startInbound(input.inboundHandoff, () => {
|
|
1671
|
+
if (session.dispatchingInput === input)
|
|
1672
|
+
session.dispatchingInput = null;
|
|
1673
|
+
sendInput(input);
|
|
1674
|
+
});
|
|
1675
|
+
if (started.status !== 'submitted')
|
|
1676
|
+
throw new Error(`Canon input ${started.status} before native dispatch.`);
|
|
1674
1677
|
},
|
|
1675
|
-
send: sendInput,
|
|
1676
1678
|
abandon: abandonDispatchedInput,
|
|
1677
1679
|
});
|
|
1678
1680
|
}
|
|
@@ -1802,12 +1804,12 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1802
1804
|
catch { }
|
|
1803
1805
|
// A native failure cannot prove whether dispatched work ran. Release local
|
|
1804
1806
|
// in-flight claims without completing endpoint receipts or replaying work.
|
|
1805
|
-
const
|
|
1807
|
+
const unfinished = new Set([
|
|
1806
1808
|
session.activeInput, session.dispatchingInput,
|
|
1807
1809
|
...session.pendingInputs, ...dispatchedInputs.values(),
|
|
1808
|
-
]
|
|
1809
|
-
for (const
|
|
1810
|
-
|
|
1810
|
+
]);
|
|
1811
|
+
for (const input of unfinished)
|
|
1812
|
+
void input?.inboundHandoff?.release('session-ended').catch(console.error);
|
|
1811
1813
|
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Session ended`);
|
|
1812
1814
|
onSessionEnd(conversationId, session.sdkSessionId);
|
|
1813
1815
|
}
|
|
@@ -2688,6 +2690,9 @@ export async function main() {
|
|
|
2688
2690
|
return;
|
|
2689
2691
|
const saveResume = options.saveResume !== false;
|
|
2690
2692
|
session.closed = true;
|
|
2693
|
+
for (const input of [session.dispatchingInput, ...session.pendingInputs]) {
|
|
2694
|
+
void input?.inboundHandoff?.release('session-closed').catch(console.error);
|
|
2695
|
+
}
|
|
2691
2696
|
if (session.typingKeepaliveTimer) {
|
|
2692
2697
|
clearInterval(session.typingKeepaliveTimer);
|
|
2693
2698
|
session.typingKeepaliveTimer = null;
|
|
@@ -2733,6 +2738,11 @@ export async function main() {
|
|
|
2733
2738
|
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Session ${options.reason ?? (saveResume ? 'suspended (sessionId saved for resume)' : 'cleared')}`);
|
|
2734
2739
|
}
|
|
2735
2740
|
async function resetRuntimeSession(session) {
|
|
2741
|
+
const dispatching = session.dispatchingInput;
|
|
2742
|
+
void dispatching?.inboundHandoff?.cancel('canceled-before-execution').then((canceled) => {
|
|
2743
|
+
if (canceled)
|
|
2744
|
+
session.markInputCompleted(dispatching);
|
|
2745
|
+
}).catch(console.error);
|
|
2736
2746
|
const conversationId = session.conversationId;
|
|
2737
2747
|
// `dispatchingInput` excludes the dispatch window: the slot is reserved but
|
|
2738
2748
|
// the SDK has nothing in flight, so there is nothing to interrupt. The
|
|
@@ -2757,8 +2767,10 @@ export async function main() {
|
|
|
2757
2767
|
const droppedInputs = session.pendingInputs.splice(0);
|
|
2758
2768
|
await markSessionPendingInputsRejected(conversationId, droppedInputs);
|
|
2759
2769
|
session.markInputCompleted(session.activeInput);
|
|
2760
|
-
for (const input of droppedInputs)
|
|
2761
|
-
|
|
2770
|
+
for (const input of droppedInputs) {
|
|
2771
|
+
if (!input.inboundHandoff || input.inboundHandoff.phase === 'settled')
|
|
2772
|
+
session.markInputCompleted(input);
|
|
2773
|
+
}
|
|
2762
2774
|
if (session.activeInput) {
|
|
2763
2775
|
session.interruptedTurnKeys.add(session.activeInput.turnKey);
|
|
2764
2776
|
}
|
|
@@ -2770,13 +2782,14 @@ export async function main() {
|
|
|
2770
2782
|
closeSession(conversationId, { saveResume: false, reason: 'reset for new Canon session' });
|
|
2771
2783
|
}
|
|
2772
2784
|
async function markSessionPendingInputsRejected(conversationId, inputs) {
|
|
2773
|
-
await Promise.all(inputs.map((input) => {
|
|
2785
|
+
await Promise.all(inputs.map(async (input) => {
|
|
2786
|
+
await input.inboundHandoff?.cancel('canceled-before-execution');
|
|
2774
2787
|
if (!input.markAccepted || !input.sourceMessageId)
|
|
2775
2788
|
return Promise.resolve();
|
|
2776
2789
|
return client.updateMessageDisposition(conversationId, input.sourceMessageId, 'rejected').catch(() => { });
|
|
2777
2790
|
}));
|
|
2778
2791
|
}
|
|
2779
|
-
function removeQueuedInput(conversationId, sourceMessageId) {
|
|
2792
|
+
async function removeQueuedInput(conversationId, sourceMessageId) {
|
|
2780
2793
|
const session = sessions.get(conversationId);
|
|
2781
2794
|
if (!session || session.pendingInputs.length === 0)
|
|
2782
2795
|
return;
|
|
@@ -2784,8 +2797,10 @@ export async function main() {
|
|
|
2784
2797
|
if (removed.length === 0)
|
|
2785
2798
|
return;
|
|
2786
2799
|
session.pendingInputs = session.pendingInputs.filter((input) => input.sourceMessageId !== sourceMessageId);
|
|
2787
|
-
|
|
2788
|
-
|
|
2800
|
+
await Promise.all(removed.map(async (input) => {
|
|
2801
|
+
if (!input.inboundHandoff || await input.inboundHandoff.cancel('message-deleted'))
|
|
2802
|
+
session.markInputCompleted(input);
|
|
2803
|
+
}));
|
|
2789
2804
|
session.writeTurn();
|
|
2790
2805
|
}
|
|
2791
2806
|
function evictOldestIdle() {
|
|
@@ -2886,12 +2901,6 @@ export async function main() {
|
|
|
2886
2901
|
}
|
|
2887
2902
|
}
|
|
2888
2903
|
existing?.clearStreaming().catch(() => { });
|
|
2889
|
-
if (existing) {
|
|
2890
|
-
settleInboundMessageId(existing.activeInput?.sourceMessageId, false);
|
|
2891
|
-
for (const input of existing.pendingInputs) {
|
|
2892
|
-
settleInboundMessageId(input.sourceMessageId, false);
|
|
2893
|
-
}
|
|
2894
|
-
}
|
|
2895
2904
|
typingSignals.clear(convoId).catch(() => { });
|
|
2896
2905
|
typingSignals.dispose(convoId);
|
|
2897
2906
|
sessions.delete(convoId);
|
|
@@ -2922,14 +2931,13 @@ export async function main() {
|
|
|
2922
2931
|
return roster ? buildCompactGroupContextLines(roster, 'initial').join('\n') : '';
|
|
2923
2932
|
},
|
|
2924
2933
|
currentArtifactAudience: () => ({ memberIds: conversationCache.get(conversationId)?.memberIds, agentId, ownerId }),
|
|
2925
|
-
onInputDeferred: (sourceMessageId) => settleInboundMessageId(sourceMessageId, false),
|
|
2926
2934
|
onInputCompleted: (sourceMessageId) => {
|
|
2927
|
-
recoveryCheckpointsFor(conversationId).settle(sourceMessageId);
|
|
2928
|
-
settleInboundMessageId(sourceMessageId, true);
|
|
2929
2935
|
const id = `message:${conversationId}:${sourceMessageId}`;
|
|
2930
|
-
void endpoint.getInbound(id).then((entry) =>
|
|
2931
|
-
|
|
2932
|
-
|
|
2936
|
+
void endpoint.getInbound(id).then(async (entry) => {
|
|
2937
|
+
if (entry)
|
|
2938
|
+
await endpoint.setInboundState(id, 'settled', 'native-completed');
|
|
2939
|
+
recoveryCheckpointsFor(conversationId).settle(sourceMessageId);
|
|
2940
|
+
}).catch((error) => console.error('[canon-host] Failed to settle native input:', error));
|
|
2933
2941
|
},
|
|
2934
2942
|
publishSessionSnapshots: (ids) => { void publishSessionSnapshots(ids); },
|
|
2935
2943
|
}, resumeId);
|
|
@@ -2954,30 +2962,6 @@ export async function main() {
|
|
|
2954
2962
|
pendingSessionCreations.delete(conversationId);
|
|
2955
2963
|
}
|
|
2956
2964
|
}
|
|
2957
|
-
const acceptedInboundMessageIds = new Set();
|
|
2958
|
-
const inFlightInboundMessageIds = new Set();
|
|
2959
|
-
function claimInboundMessageId(messageId) {
|
|
2960
|
-
if (!messageId)
|
|
2961
|
-
return true;
|
|
2962
|
-
if (acceptedInboundMessageIds.has(messageId) || inFlightInboundMessageIds.has(messageId))
|
|
2963
|
-
return false;
|
|
2964
|
-
inFlightInboundMessageIds.add(messageId);
|
|
2965
|
-
return true;
|
|
2966
|
-
}
|
|
2967
|
-
function settleInboundMessageId(messageId, accepted) {
|
|
2968
|
-
if (!messageId)
|
|
2969
|
-
return;
|
|
2970
|
-
inFlightInboundMessageIds.delete(messageId);
|
|
2971
|
-
if (!accepted)
|
|
2972
|
-
return;
|
|
2973
|
-
acceptedInboundMessageIds.add(messageId);
|
|
2974
|
-
while (acceptedInboundMessageIds.size > 2_048) {
|
|
2975
|
-
const oldest = acceptedInboundMessageIds.values().next().value;
|
|
2976
|
-
if (!oldest)
|
|
2977
|
-
break;
|
|
2978
|
-
acceptedInboundMessageIds.delete(oldest);
|
|
2979
|
-
}
|
|
2980
|
-
}
|
|
2981
2965
|
function handleRuntimeReplyMessage(conversationId, message) {
|
|
2982
2966
|
if (message.metadata?.type === 'approval_reply' || (approvalManager?.pendingCount ?? 0) > 0) {
|
|
2983
2967
|
const consumed = approvalManager?.handleMessage(conversationId, {
|
|
@@ -3002,13 +2986,6 @@ export async function main() {
|
|
|
3002
2986
|
}
|
|
3003
2987
|
return consumed;
|
|
3004
2988
|
}
|
|
3005
|
-
function persistedConversationCursor(conversationId) {
|
|
3006
|
-
return loadRuntimeSessionState(runtimeId, {
|
|
3007
|
-
conversationId,
|
|
3008
|
-
baseCwd: workingDir,
|
|
3009
|
-
workspaceId: CLAUDE_RECOVERY_CURSOR_WORKSPACE_ID,
|
|
3010
|
-
})?.lastInboundMessageId ?? null;
|
|
3011
|
-
}
|
|
3012
2989
|
function persistConversationRecoveryCursor(conversationId, messageId) {
|
|
3013
2990
|
try {
|
|
3014
2991
|
saveRuntimeSessionState(runtimeId, {
|
|
@@ -3039,23 +3016,24 @@ export async function main() {
|
|
|
3039
3016
|
return;
|
|
3040
3017
|
}
|
|
3041
3018
|
const convoId = payload.conversationId;
|
|
3042
|
-
|
|
3019
|
+
const handoff = await endpoint.acquireInbound(event.id);
|
|
3020
|
+
if (!handoff)
|
|
3043
3021
|
return;
|
|
3044
3022
|
recoveryCheckpointsFor(convoId).track(m.id);
|
|
3045
3023
|
knownConversationIds.add(convoId);
|
|
3046
3024
|
if (handleRuntimeReplyMessage(convoId, m)) {
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3025
|
+
if (await handoff.settle('not-dispatched')) {
|
|
3026
|
+
recoveryCheckpointsFor(convoId).settle(m.id);
|
|
3027
|
+
}
|
|
3050
3028
|
return;
|
|
3051
3029
|
}
|
|
3052
3030
|
const sender = m.senderName || m.senderId;
|
|
3053
3031
|
const isOwner = m.isOwner ?? (ownerId != null && m.senderId === ownerId);
|
|
3054
3032
|
if (payload.turnDispatch && payload.turnDispatch.kind !== 'run_turn') {
|
|
3055
3033
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Ignoring server-dispatched observe-only message: ${payload.turnDispatch.reason}`);
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3034
|
+
if (await handoff.settle('not-dispatched')) {
|
|
3035
|
+
recoveryCheckpointsFor(convoId).settle(m.id);
|
|
3036
|
+
}
|
|
3059
3037
|
return;
|
|
3060
3038
|
}
|
|
3061
3039
|
await (async () => {
|
|
@@ -3155,6 +3133,7 @@ export async function main() {
|
|
|
3155
3133
|
materialized: [...replyMedia.materialized, ...materialized],
|
|
3156
3134
|
});
|
|
3157
3135
|
const turnModes = {
|
|
3136
|
+
inboundHandoff: handoff,
|
|
3158
3137
|
...resolveClaudeTurnModes(participantContext),
|
|
3159
3138
|
commandContext: commandInput.commandContext,
|
|
3160
3139
|
replyAuthority: payload.replyAuthority ?? null,
|
|
@@ -3175,12 +3154,12 @@ export async function main() {
|
|
|
3175
3154
|
return true;
|
|
3176
3155
|
})().then(async (queued) => {
|
|
3177
3156
|
if (!queued) {
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3157
|
+
if (await handoff.settle('not-dispatched')) {
|
|
3158
|
+
recoveryCheckpointsFor(convoId).settle(m.id);
|
|
3159
|
+
}
|
|
3181
3160
|
}
|
|
3182
|
-
}).catch((error) => {
|
|
3183
|
-
|
|
3161
|
+
}).catch(async (error) => {
|
|
3162
|
+
await handoff.release('queue-failed');
|
|
3184
3163
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to process inbound message:`, error instanceof Error ? error.message : error);
|
|
3185
3164
|
throw error;
|
|
3186
3165
|
});
|
|
@@ -3197,7 +3176,7 @@ export async function main() {
|
|
|
3197
3176
|
data: payload });
|
|
3198
3177
|
},
|
|
3199
3178
|
onMessageDeleted: (payload) => {
|
|
3200
|
-
removeQueuedInput(payload.conversationId, payload.messageId);
|
|
3179
|
+
void removeQueuedInput(payload.conversationId, payload.messageId).catch(console.error);
|
|
3201
3180
|
},
|
|
3202
3181
|
onConversationUpdated: (payload) => {
|
|
3203
3182
|
handleConversationUpdated(payload);
|
|
@@ -3255,6 +3234,22 @@ export async function main() {
|
|
|
3255
3234
|
queueDepth: session.pendingInputs.length,
|
|
3256
3235
|
});
|
|
3257
3236
|
if (action === 'defer') {
|
|
3237
|
+
const dispatching = session.dispatchingInput;
|
|
3238
|
+
if (dispatching?.inboundHandoff) {
|
|
3239
|
+
// Stop owns the pre-submit window too. Register cancellation before
|
|
3240
|
+
// any queued-disposition write; the shared claim fence prevents send.
|
|
3241
|
+
const canceled = dispatching.inboundHandoff.cancel('canceled-before-execution');
|
|
3242
|
+
const dropped = event.type === 'stop_and_drop' ? session.pendingInputs.splice(0) : [];
|
|
3243
|
+
await markSessionPendingInputsRejected(convoId, dropped);
|
|
3244
|
+
for (const input of dropped) {
|
|
3245
|
+
if (!input.inboundHandoff || input.inboundHandoff.phase === 'settled')
|
|
3246
|
+
session.markInputCompleted(input);
|
|
3247
|
+
}
|
|
3248
|
+
if (await canceled) {
|
|
3249
|
+
session.markInputCompleted(dispatching);
|
|
3250
|
+
return;
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3258
3253
|
// Reserved but not yet dispatched: there is nothing in the SDK to stop
|
|
3259
3254
|
// YET. Consuming the signal here would make a Stop pressed in the
|
|
3260
3255
|
// accept/artifact window do nothing while the turn it was aimed at ran to
|
|
@@ -3286,8 +3281,10 @@ export async function main() {
|
|
|
3286
3281
|
await markSessionPendingInputsRejected(convoId, droppedInputs);
|
|
3287
3282
|
}
|
|
3288
3283
|
session.markInputCompleted(session.activeInput);
|
|
3289
|
-
for (const input of droppedInputs)
|
|
3290
|
-
|
|
3284
|
+
for (const input of droppedInputs) {
|
|
3285
|
+
if (!input.inboundHandoff || input.inboundHandoff.phase === 'settled')
|
|
3286
|
+
session.markInputCompleted(input);
|
|
3287
|
+
}
|
|
3291
3288
|
if (session.activeInput) {
|
|
3292
3289
|
session.interruptedTurnKeys.add(session.activeInput.turnKey);
|
|
3293
3290
|
}
|
package/dist/session-state.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
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
|
+
import type { EndpointInboundHandoff } from '@canonmsg/core';
|
|
3
4
|
import type { AgentReplyAuthorityV1 } from '@canonmsg/backend-contracts';
|
|
4
5
|
import type { TurnArtifactRoutingDecision, TurnArtifactSnapshot } from '@canonmsg/coding-agent-host';
|
|
5
6
|
export type ClaudeInputKind = 'seed' | 'canon';
|
|
6
7
|
export type ClaudeArtifactRoutingMode = 'workspace-generated' | 'disabled';
|
|
7
8
|
export interface ClaudeInputEnvelope {
|
|
9
|
+
inboundHandoff?: EndpointInboundHandoff;
|
|
8
10
|
commandContext?: string;
|
|
9
11
|
kind: ClaudeInputKind;
|
|
10
12
|
/** Correlates this envelope with its turn result's user-message UUIDs. */
|
|
@@ -35,6 +37,7 @@ export interface ClaudeInputEnvelope {
|
|
|
35
37
|
* grouped so they travel together from the enqueue site onto the envelope.
|
|
36
38
|
*/
|
|
37
39
|
export interface ClaudeTurnModes {
|
|
40
|
+
inboundHandoff?: EndpointInboundHandoff;
|
|
38
41
|
commandContext?: string;
|
|
39
42
|
artifactRoutingMode?: ClaudeArtifactRoutingMode;
|
|
40
43
|
turnVerbosity?: TurnVerbosity;
|
|
@@ -100,6 +103,7 @@ export interface ClaudeCompletedTurnState {
|
|
|
100
103
|
turnActivity?: ClaudeTurnActivityState;
|
|
101
104
|
}
|
|
102
105
|
export declare function createClaudeInputEnvelope(input: {
|
|
106
|
+
inboundHandoff?: EndpointInboundHandoff;
|
|
103
107
|
commandContext?: string;
|
|
104
108
|
kind: ClaudeInputKind;
|
|
105
109
|
msg: SDKUserMessage;
|
|
@@ -315,7 +319,7 @@ export interface ClaudeDispatchDeps {
|
|
|
315
319
|
/** Capture the artifact baseline — a workspace walk. */
|
|
316
320
|
prepareArtifacts: (input: ClaudeInputEnvelope) => Promise<void>;
|
|
317
321
|
/** Hand the input to the SDK. */
|
|
318
|
-
send: (input: ClaudeInputEnvelope) => void
|
|
322
|
+
send: (input: ClaudeInputEnvelope) => void | Promise<void>;
|
|
319
323
|
/** Settle an input that will never be answered, and retire its turn. */
|
|
320
324
|
abandon: (input: ClaudeInputEnvelope, reason: ClaudeDispatchAbandonReason, error?: unknown) => void;
|
|
321
325
|
}
|
package/dist/session-state.js
CHANGED
|
@@ -12,6 +12,7 @@ export function createClaudeInputEnvelope(input) {
|
|
|
12
12
|
return {
|
|
13
13
|
...(input.commandContext ? { commandContext: input.commandContext } : {}),
|
|
14
14
|
kind: input.kind,
|
|
15
|
+
...(input.inboundHandoff ? { inboundHandoff: input.inboundHandoff } : {}),
|
|
15
16
|
messageUuid,
|
|
16
17
|
msg: { ...input.msg, uuid: messageUuid },
|
|
17
18
|
intent: input.intent ?? 'queue',
|
|
@@ -331,17 +332,30 @@ export async function dispatchClaudeInput(session, input, deps) {
|
|
|
331
332
|
catch (error) {
|
|
332
333
|
failure = { error };
|
|
333
334
|
}
|
|
334
|
-
if (session.dispatchingInput === input)
|
|
335
|
-
session.dispatchingInput = null;
|
|
336
335
|
if (failure) {
|
|
336
|
+
if (session.dispatchingInput === input)
|
|
337
|
+
session.dispatchingInput = null;
|
|
337
338
|
deps.abandon(input, 'error', failure.error);
|
|
338
339
|
return;
|
|
339
340
|
}
|
|
340
341
|
if (session.closed) {
|
|
342
|
+
if (session.dispatchingInput === input)
|
|
343
|
+
session.dispatchingInput = null;
|
|
341
344
|
deps.abandon(input, 'closed');
|
|
342
345
|
return;
|
|
343
346
|
}
|
|
344
|
-
|
|
347
|
+
try {
|
|
348
|
+
await deps.send(input);
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
if (session.dispatchingInput === input)
|
|
352
|
+
session.dispatchingInput = null;
|
|
353
|
+
deps.abandon(input, 'error', error);
|
|
354
|
+
}
|
|
355
|
+
finally {
|
|
356
|
+
if (session.dispatchingInput === input)
|
|
357
|
+
session.dispatchingInput = null;
|
|
358
|
+
}
|
|
345
359
|
}
|
|
346
360
|
// ── Turn lifecycle: opening a turn before the SDK says anything ──────
|
|
347
361
|
//
|
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.4",
|
|
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,11 +31,11 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@anthropic-ai/claude-agent-sdk": "0.3.278",
|
|
34
|
-
"@canonmsg/agent-sdk": "^11.0.
|
|
35
|
-
"@canonmsg/agent-tools": "^0.11.
|
|
36
|
-
"@canonmsg/coding-agent-host": "^0.
|
|
37
|
-
"@canonmsg/core": "^13.0.
|
|
38
|
-
"@canonmsg/rich-cards": "^0.10.
|
|
34
|
+
"@canonmsg/agent-sdk": "^11.0.3",
|
|
35
|
+
"@canonmsg/agent-tools": "^0.11.1",
|
|
36
|
+
"@canonmsg/coding-agent-host": "^0.10.1",
|
|
37
|
+
"@canonmsg/core": "^13.0.6",
|
|
38
|
+
"@canonmsg/rich-cards": "^0.10.7",
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.30.0"
|
|
40
40
|
},
|
|
41
41
|
"engines": {
|
|
@@ -50,6 +50,11 @@
|
|
|
50
50
|
"messaging"
|
|
51
51
|
],
|
|
52
52
|
"homepage": "https://canonmail.com/agents/integrations#claude-code",
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "https://github.com/HeyBobChan/canon.git",
|
|
56
|
+
"directory": "packages/claude-code-plugin"
|
|
57
|
+
},
|
|
53
58
|
"publishConfig": {
|
|
54
59
|
"access": "public"
|
|
55
60
|
},
|