@canonmsg/claude-code-plugin 0.25.3 → 0.25.5
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 +31 -54
- package/dist/startup-recovery.d.ts +5 -0
- package/dist/startup-recovery.js +12 -0
- package/package.json +2 -2
package/dist/host.js
CHANGED
|
@@ -35,7 +35,7 @@ import { synthesizeClaudeApprovalDiff } from './approval-diff.js';
|
|
|
35
35
|
import { decideClaudeToolPermissionForMode, parseAllowedNonOwnerClaudeTools, } from './tool-policy.js';
|
|
36
36
|
import { applyClaudeSessionControl, buildClaudeFinalMessageId, claudeModelInfoToOption, confirmClaudeInterrupt, createClaudeInputEnvelope, formatClaudeControlError, isClaudeCustomModelOption, mergeClaudeDiscoveredModelOptions, resolveClaudeTurnResponseRouting, resolveClaudeModelOptions, resetClaudeCompletedTurnState, shouldDeliverClaudeFinal, } from './session-state.js';
|
|
37
37
|
import { CLAUDE_SUPPORTED_DIALOG_KINDS, buildClaudeAskUserPermissionDenied, buildClaudeAskUserPermissionResult, createClaudeUserDialogCoordinator, parseClaudeAskUserDialog, parseClaudeAskUserToolInput, resolveClaudeUserDialogRequestId, } from './user-dialog.js';
|
|
38
|
-
import { collectMissedInboundMessages, STARTUP_RECOVERY_MAX_MESSAGES, STARTUP_RECOVERY_PAGE_SIZE, } from './startup-recovery.js';
|
|
38
|
+
import { collectMissedInboundMessages, createReconnectRecoveryGate, STARTUP_RECOVERY_MAX_MESSAGES, STARTUP_RECOVERY_PAGE_SIZE, } from './startup-recovery.js';
|
|
39
39
|
function parseRuntimeVisibilityPreset(value) {
|
|
40
40
|
return value === 'normal' || value === 'minimal' || value === 'full' ? value : undefined;
|
|
41
41
|
}
|
|
@@ -841,25 +841,6 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
841
841
|
}
|
|
842
842
|
});
|
|
843
843
|
}
|
|
844
|
-
function waitForReply(type, requestId, responseUserId) {
|
|
845
|
-
// Keep session alive while waiting for user input
|
|
846
|
-
session.lastActivity = Date.now();
|
|
847
|
-
return new Promise((resolve, reject) => {
|
|
848
|
-
const timer = setTimeout(() => {
|
|
849
|
-
session.pendingReply = null;
|
|
850
|
-
reject(new Error('Timed out waiting for user reply'));
|
|
851
|
-
}, REPLY_TIMEOUT_MS);
|
|
852
|
-
session.pendingReply = {
|
|
853
|
-
type,
|
|
854
|
-
requestId,
|
|
855
|
-
responseUserId,
|
|
856
|
-
resolve: (metadata) => {
|
|
857
|
-
clearTimeout(timer);
|
|
858
|
-
resolve(metadata);
|
|
859
|
-
},
|
|
860
|
-
};
|
|
861
|
-
});
|
|
862
|
-
}
|
|
863
844
|
function buildArtifactMediaMetadata() {
|
|
864
845
|
return {
|
|
865
846
|
...(session.currentTurnId ? { turnId: session.currentTurnId } : {}),
|
|
@@ -939,24 +920,24 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
939
920
|
const planBody = typeof input.plan === 'string' && input.plan.trim()
|
|
940
921
|
? input.plan.trim()
|
|
941
922
|
: undefined;
|
|
942
|
-
|
|
943
|
-
// seeds the server-owned pending node/attention/state and authors the
|
|
944
|
-
// `plan_approval` card. Resolution is UNCHANGED — the gate still blocks
|
|
945
|
-
// on the server-authored `plan_approval_reply` below.
|
|
946
|
-
await client.createRuntimePlanRequest({
|
|
947
|
-
conversationId,
|
|
948
|
-
planId,
|
|
923
|
+
const reply = await config.runtimeInputManager.request('plan', conversationId, {
|
|
949
924
|
title: 'Plan ready for review.',
|
|
950
925
|
summary: 'Claude has prepared a plan and is asking permission to continue.',
|
|
951
926
|
...(planBody ? { body: planBody } : {}),
|
|
952
927
|
responseUserId: turnResponseRouting.responseUserId,
|
|
953
928
|
...(allowedPrompts.length > 0 ? { allowedPrompts } : {}),
|
|
954
929
|
...(session.currentTurnId ? { turnId: session.currentTurnId } : {}),
|
|
930
|
+
}, {
|
|
931
|
+
requestId: planId,
|
|
955
932
|
expiresAt: Date.now() + REPLY_TIMEOUT_MS,
|
|
933
|
+
responderPolicy: 'infer',
|
|
934
|
+
signal: options.signal,
|
|
935
|
+
onCreated: () => {
|
|
936
|
+
session.lastActivity = Date.now();
|
|
937
|
+
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Plan approval sent (${planId.slice(0, 8)})`);
|
|
938
|
+
},
|
|
956
939
|
});
|
|
957
|
-
|
|
958
|
-
const reply = await waitForReply('plan_approval_reply', planId, turnResponseRouting.responseUserId);
|
|
959
|
-
if (reply.decision === 'approve') {
|
|
940
|
+
if (reply.status === 'approve') {
|
|
960
941
|
// Honor opt-in selective grants: when the card sent an explicit
|
|
961
942
|
// granted subset, pre-approve those tools as session rules so they
|
|
962
943
|
// do not re-prompt. Owner-turn only; absent grants preserve the
|
|
@@ -1000,13 +981,21 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1000
981
|
}
|
|
1001
982
|
return { behavior: 'allow' };
|
|
1002
983
|
}
|
|
1003
|
-
if (reply.
|
|
984
|
+
if (reply.status === 'reject') {
|
|
1004
985
|
return {
|
|
1005
986
|
behavior: 'deny',
|
|
1006
987
|
message: reply.feedback?.trim()
|
|
1007
988
|
|| 'Plan declined — keep planning and wait for guidance before proceeding.',
|
|
1008
989
|
};
|
|
1009
990
|
}
|
|
991
|
+
if (reply.status === 'cancelled' || reply.status === 'timeout') {
|
|
992
|
+
return {
|
|
993
|
+
behavior: 'deny',
|
|
994
|
+
message: reply.status === 'timeout'
|
|
995
|
+
? 'No reply received (timed out)'
|
|
996
|
+
: 'The plan review was cancelled.',
|
|
997
|
+
};
|
|
998
|
+
}
|
|
1010
999
|
return {
|
|
1011
1000
|
behavior: 'deny',
|
|
1012
1001
|
message: reply.feedback?.trim() || 'User requested changes to the plan.',
|
|
@@ -1093,7 +1082,6 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1093
1082
|
controlInterruptPending: false,
|
|
1094
1083
|
closed: false,
|
|
1095
1084
|
sdkSessionId: resumeSessionId || null,
|
|
1096
|
-
pendingReply: null,
|
|
1097
1085
|
writeState: () => writeState(),
|
|
1098
1086
|
writeTurn: () => writeTurn(),
|
|
1099
1087
|
clearStreaming: () => clearStreaming(),
|
|
@@ -2559,28 +2547,14 @@ export async function main() {
|
|
|
2559
2547
|
return true;
|
|
2560
2548
|
}
|
|
2561
2549
|
}
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Ignoring stale ${message.metadata.type} with no pending request`);
|
|
2569
|
-
return true;
|
|
2570
|
-
}
|
|
2571
|
-
if (message.senderId !== pendingReply.responseUserId) {
|
|
2572
|
-
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Ignoring ${message.metadata.type} from unexpected responder ${message.senderId}`);
|
|
2573
|
-
return true;
|
|
2574
|
-
}
|
|
2575
|
-
if (pendingReply.type !== message.metadata.type || pendingReply.requestId !== replyRequestId) {
|
|
2576
|
-
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Ignoring mismatched ${message.metadata.type} (${String(replyRequestId ?? 'missing-id')})`);
|
|
2577
|
-
return true;
|
|
2550
|
+
const consumed = runtimeInputManager.handleMessage(conversationId, {
|
|
2551
|
+
senderId: message.senderId,
|
|
2552
|
+
metadata: message.metadata,
|
|
2553
|
+
});
|
|
2554
|
+
if (consumed) {
|
|
2555
|
+
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Runtime response receipt woke canonical polling`);
|
|
2578
2556
|
}
|
|
2579
|
-
|
|
2580
|
-
existingSession.lastActivity = Date.now();
|
|
2581
|
-
pendingReply.resolve(message.metadata);
|
|
2582
|
-
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Reply received (${message.metadata.type}:${pendingReply.requestId.slice(0, 8)})`);
|
|
2583
|
-
return true;
|
|
2557
|
+
return consumed;
|
|
2584
2558
|
}
|
|
2585
2559
|
function persistedConversationCursors(conversationId) {
|
|
2586
2560
|
const canonicalCursor = loadRuntimeSessionState(runtimeId, {
|
|
@@ -2663,6 +2637,7 @@ export async function main() {
|
|
|
2663
2637
|
});
|
|
2664
2638
|
return recoveryInFlight;
|
|
2665
2639
|
}
|
|
2640
|
+
const shouldRecoverAfterStreamConnect = createReconnectRecoveryGate();
|
|
2666
2641
|
await recoverMissedInboundMessages();
|
|
2667
2642
|
// ── Connect SSE stream for inbound Canon messages ──
|
|
2668
2643
|
const stream = new CanonStream({
|
|
@@ -2799,7 +2774,9 @@ export async function main() {
|
|
|
2799
2774
|
onConnected: () => {
|
|
2800
2775
|
streamConnected = true;
|
|
2801
2776
|
void publishRuntimeHeartbeat();
|
|
2802
|
-
|
|
2777
|
+
if (shouldRecoverAfterStreamConnect()) {
|
|
2778
|
+
void recoverMissedInboundMessages();
|
|
2779
|
+
}
|
|
2803
2780
|
console.error('[canon-host] SSE connected');
|
|
2804
2781
|
},
|
|
2805
2782
|
onDisconnected: () => {
|
|
@@ -40,6 +40,11 @@ export interface StartupRecoveryResult<TPage extends StartupRecoveryPage> {
|
|
|
40
40
|
/** First page fetched — reusable as hydration context for recovered turns. */
|
|
41
41
|
newestPage: TPage;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Startup recovery runs before the stream starts. The first SSE connection
|
|
45
|
+
* therefore needs no second pass, while later reconnects still need catch-up.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createReconnectRecoveryGate(): () => boolean;
|
|
43
48
|
export declare function collectMissedInboundMessages<TPage extends StartupRecoveryPage>(input: {
|
|
44
49
|
fetchPage: (before?: string) => Promise<TPage>;
|
|
45
50
|
cursor: string | null | undefined;
|
package/dist/startup-recovery.js
CHANGED
|
@@ -11,6 +11,18 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export const STARTUP_RECOVERY_PAGE_SIZE = 25;
|
|
13
13
|
export const STARTUP_RECOVERY_MAX_MESSAGES = 500;
|
|
14
|
+
/**
|
|
15
|
+
* Startup recovery runs before the stream starts. The first SSE connection
|
|
16
|
+
* therefore needs no second pass, while later reconnects still need catch-up.
|
|
17
|
+
*/
|
|
18
|
+
export function createReconnectRecoveryGate() {
|
|
19
|
+
let hasConnected = false;
|
|
20
|
+
return () => {
|
|
21
|
+
const shouldRecover = hasConnected;
|
|
22
|
+
hasConnected = true;
|
|
23
|
+
return shouldRecover;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
14
26
|
export async function collectMissedInboundMessages(input) {
|
|
15
27
|
const maxMessages = input.maxMessages ?? STARTUP_RECOVERY_MAX_MESSAGES;
|
|
16
28
|
const newestPage = await input.fetchPage();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/claude-code-plugin",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.5",
|
|
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,7 +33,7 @@
|
|
|
33
33
|
"@anthropic-ai/claude-agent-sdk": "0.3.204",
|
|
34
34
|
"@canonmsg/agent-sdk": "^5.1.1",
|
|
35
35
|
"@canonmsg/coding-agent-host": "^0.2.2",
|
|
36
|
-
"@canonmsg/core": "^4.2.
|
|
36
|
+
"@canonmsg/core": "^4.2.3",
|
|
37
37
|
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|