@canonmsg/claude-code-plugin 0.31.4 → 0.33.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.d.ts +6 -1
- package/dist/host.js +102 -42
- package/dist/server.js +11 -3
- package/dist/tool-policy.js +2 -6
- package/package.json +6 -6
package/dist/host.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { type PermissionResult, type SDKControlReloadPluginsResponse } from '@anthropic-ai/claude-agent-sdk';
|
|
3
|
-
import { type
|
|
3
|
+
import { type RecoveryCheckpointTracker } from '@canonmsg/coding-agent-host';
|
|
4
|
+
import { type CanonRuntimeCommandDescriptor, type ExecutionEnvironmentMode, type HostInboundParticipantContext as InboundParticipantContext, type CanonReplyContext, type MessageCreatedPayload, type ResolvedAgentBehaviorPolicy, type TurnVerbosityConfig } from '@canonmsg/core';
|
|
4
5
|
/**
|
|
5
6
|
* `--turn-verbosity` beats `CANON_TURN_VERBOSITY`; `null` means "unset, use the
|
|
6
7
|
* per-conversation-type default".
|
|
@@ -24,6 +25,10 @@ export declare function buildClaudePlanAllowResult(input: Record<string, unknown
|
|
|
24
25
|
* through. Canon-injected aliases keep precedence over native names.
|
|
25
26
|
*/
|
|
26
27
|
export declare function buildNativeSlashCommandDescriptors(native: SDKControlReloadPluginsResponse['commands'], reservedAliases: ReadonlySet<string>): CanonRuntimeCommandDescriptor[];
|
|
28
|
+
export declare function resolveSessionExecutionMode(config: {
|
|
29
|
+
executionMode?: ExecutionEnvironmentMode;
|
|
30
|
+
} | null | undefined, defaultExecutionMode?: ExecutionEnvironmentMode): ExecutionEnvironmentMode;
|
|
31
|
+
export declare function createClaudeRecoveryCheckpointTracker(persist: (messageId: string) => boolean): RecoveryCheckpointTracker;
|
|
27
32
|
export declare const NO_REPLY_TOOL_NAME: string;
|
|
28
33
|
export declare function buildCanonPrompt(input: {
|
|
29
34
|
content: string;
|
package/dist/host.js
CHANGED
|
@@ -27,7 +27,7 @@ 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 { materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMessage, } from '@canonmsg/agent-sdk';
|
|
30
|
-
import { captureTurnArtifactSnapshot, createTurnArtifactRouter, IDLE_TIMEOUT_MS, collectMissedInboundMessages,
|
|
30
|
+
import { captureTurnArtifactSnapshot, createTurnArtifactRouter, IDLE_TIMEOUT_MS, collectMissedInboundMessages, createRecoveryCheckpointTracker, createReconnectRecoveryCoordinator, STARTUP_RECOVERY_PAGE_SIZE, } from '@canonmsg/coding-agent-host';
|
|
31
31
|
import { buildCanonUserContent, renderClaudeInboundContent, withClaudeReplyContextMediaReferences, } from './canon-user-content.js';
|
|
32
32
|
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, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd, resolveTurnVerbosity, shouldTriggerAgentTurn, upsertLocalRuntimeEntry, } from '@canonmsg/core';
|
|
33
33
|
import { runCli } from '@canonmsg/core';
|
|
@@ -53,6 +53,8 @@ COMMON FLAGS
|
|
|
53
53
|
--cwd <path> Project directory to run Claude Code from
|
|
54
54
|
--workspace <path> Additional project to expose in Canon
|
|
55
55
|
--workspace-root <path> Discover projects under an approved root
|
|
56
|
+
--default-execution-mode <worktree|locked>
|
|
57
|
+
Default for new Canon conversations
|
|
56
58
|
--runtime-visibility <normal|minimal|full>
|
|
57
59
|
Detail visibility preset
|
|
58
60
|
--show-runtime-detail <field>
|
|
@@ -271,6 +273,7 @@ function buildClaudeRuntimeDescriptor(input) {
|
|
|
271
273
|
workspaces: input.workspaces,
|
|
272
274
|
workspaceRoots: input.workspaceRoots,
|
|
273
275
|
executionModes: input.executionModes,
|
|
276
|
+
defaultExecutionMode: input.defaultExecutionMode,
|
|
274
277
|
permissionModes: CLAUDE_PERMISSION_MODE_OPTIONS,
|
|
275
278
|
defaultPermissionMode: 'default',
|
|
276
279
|
effortOptions: [...EFFORT_OPTIONS],
|
|
@@ -431,10 +434,15 @@ async function loadSessionConfig(conversationId, agentId, rtdb) {
|
|
|
431
434
|
ultracode: parseClaudeUltracodeMode(config?.runtimeControlValues?.ultracode),
|
|
432
435
|
};
|
|
433
436
|
}
|
|
434
|
-
function resolveSessionExecutionMode(config) {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
437
|
+
export function resolveSessionExecutionMode(config, defaultExecutionMode = 'worktree') {
|
|
438
|
+
return config?.executionMode ?? defaultExecutionMode;
|
|
439
|
+
}
|
|
440
|
+
function resolveConfiguredDefaultExecutionMode(value) {
|
|
441
|
+
if (value == null || value === '')
|
|
442
|
+
return 'worktree';
|
|
443
|
+
if (value === 'worktree' || value === 'locked')
|
|
444
|
+
return value;
|
|
445
|
+
throw new Error('--default-execution-mode must be worktree or locked');
|
|
438
446
|
}
|
|
439
447
|
function resolveWorkspaceCwd(config) {
|
|
440
448
|
return resolveHostWorkspaceCwd({
|
|
@@ -728,6 +736,9 @@ const FINAL_DELIVERY_RETRY_MS = 30_000;
|
|
|
728
736
|
// without delaying anything that was going to fail anyway.
|
|
729
737
|
const MAX_FINAL_DELIVERY_RETRIES = 2;
|
|
730
738
|
const CLAUDE_RECOVERY_CURSOR_WORKSPACE_ID = 'claude-recovery-cursor-v1';
|
|
739
|
+
export function createClaudeRecoveryCheckpointTracker(persist) {
|
|
740
|
+
return createRecoveryCheckpointTracker(persist);
|
|
741
|
+
}
|
|
731
742
|
const CLAUDE_RUNTIME_CAPABILITIES = {
|
|
732
743
|
...DEFAULT_RUNTIME_CAPABILITIES,
|
|
733
744
|
supportsInterrupt: true,
|
|
@@ -979,7 +990,7 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
979
990
|
const turnKey = session.activeInput?.turnKey;
|
|
980
991
|
if (turnKey)
|
|
981
992
|
session.silencedTurnKeys.add(turnKey);
|
|
982
|
-
}),
|
|
993
|
+
}, { communication: config.communicationEnabled !== false }),
|
|
983
994
|
},
|
|
984
995
|
},
|
|
985
996
|
...(claudeCliPath ? { pathToClaudeCodeExecutable: claudeCliPath } : {}),
|
|
@@ -1260,19 +1271,6 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1260
1271
|
function markInputCompleted(input) {
|
|
1261
1272
|
if (input?.kind !== 'canon' || !input.sourceMessageId)
|
|
1262
1273
|
return;
|
|
1263
|
-
if (config.runtimeId) {
|
|
1264
|
-
try {
|
|
1265
|
-
saveRuntimeSessionState(config.runtimeId, {
|
|
1266
|
-
conversationId,
|
|
1267
|
-
baseCwd: config.recoveryBaseCwd ?? environment.baseCwd,
|
|
1268
|
-
workspaceId: CLAUDE_RECOVERY_CURSOR_WORKSPACE_ID,
|
|
1269
|
-
lastInboundMessageId: input.sourceMessageId,
|
|
1270
|
-
});
|
|
1271
|
-
}
|
|
1272
|
-
catch (error) {
|
|
1273
|
-
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Failed to persist inbound recovery cursor:`, error instanceof Error ? error.message : error);
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
1274
|
config.onInputCompleted?.(input.sourceMessageId);
|
|
1277
1275
|
}
|
|
1278
1276
|
/**
|
|
@@ -2396,11 +2394,13 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
2396
2394
|
availableModels: modelList,
|
|
2397
2395
|
availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
2398
2396
|
availableExecutionModes: config.availableExecutionModes,
|
|
2397
|
+
defaultExecutionMode: config.defaultExecutionMode,
|
|
2399
2398
|
runtimeDescriptor: buildClaudeRuntimeDescriptor({
|
|
2400
2399
|
models: modelList,
|
|
2401
2400
|
workspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
2402
2401
|
workspaceRoots: workspaceRootMetadata,
|
|
2403
2402
|
executionModes: config.availableExecutionModes,
|
|
2403
|
+
defaultExecutionMode: config.defaultExecutionMode,
|
|
2404
2404
|
presentation: config.runtimePresentation,
|
|
2405
2405
|
}),
|
|
2406
2406
|
});
|
|
@@ -2430,6 +2430,7 @@ export async function main() {
|
|
|
2430
2430
|
cwd: { type: 'string' },
|
|
2431
2431
|
workspace: { type: 'string', multiple: true },
|
|
2432
2432
|
'workspace-root': { type: 'string', multiple: true },
|
|
2433
|
+
'default-execution-mode': { type: 'string' },
|
|
2433
2434
|
'runtime-visibility': { type: 'string' },
|
|
2434
2435
|
'show-runtime-detail': { type: 'string', multiple: true },
|
|
2435
2436
|
'hide-runtime-detail': { type: 'string', multiple: true },
|
|
@@ -2442,6 +2443,7 @@ export async function main() {
|
|
|
2442
2443
|
env: process.env.CANON_TURN_VERBOSITY,
|
|
2443
2444
|
onWarning: (message) => console.error(`[canon-host] ${message}`),
|
|
2444
2445
|
});
|
|
2446
|
+
const defaultExecutionMode = resolveConfiguredDefaultExecutionMode(args['default-execution-mode']);
|
|
2445
2447
|
workingDir = (typeof args.cwd === 'string' ? args.cwd : null) || process.cwd();
|
|
2446
2448
|
const configuredWorkspaces = (args.workspace ?? []).filter((value) => typeof value === 'string');
|
|
2447
2449
|
const configuredWorkspaceRoots = (args['workspace-root'] ?? []).filter((value) => typeof value === 'string');
|
|
@@ -2482,11 +2484,13 @@ export async function main() {
|
|
|
2482
2484
|
let agentId;
|
|
2483
2485
|
let ownerId = null;
|
|
2484
2486
|
let ownerName = null;
|
|
2487
|
+
let communicationEnabled = true;
|
|
2485
2488
|
try {
|
|
2486
2489
|
const ctx = await client.getAgentMe();
|
|
2487
2490
|
agentId = ctx.agentId;
|
|
2488
2491
|
ownerId = ctx.ownerId;
|
|
2489
2492
|
ownerName = ctx.ownerName;
|
|
2493
|
+
communicationEnabled = ctx.outboundPolicy !== 'closed';
|
|
2490
2494
|
console.error(`[canon-host] Connected as ${ctx.displayName || agentId}`);
|
|
2491
2495
|
}
|
|
2492
2496
|
catch {
|
|
@@ -2544,6 +2548,7 @@ export async function main() {
|
|
|
2544
2548
|
hide: stringArgs(args['hide-runtime-detail']),
|
|
2545
2549
|
});
|
|
2546
2550
|
let runtimeDescriptor = {
|
|
2551
|
+
defaultExecutionMode,
|
|
2547
2552
|
defaultWorkspaceId: workspaceOptions[0]?.id,
|
|
2548
2553
|
defaultPermissionMode: 'default',
|
|
2549
2554
|
availablePermissionModes: [...CLAUDE_PERMISSION_MODE_OPTIONS],
|
|
@@ -2554,6 +2559,7 @@ export async function main() {
|
|
|
2554
2559
|
workspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
2555
2560
|
workspaceRoots: workspaceRootMetadata,
|
|
2556
2561
|
executionModes: hostAvailableExecutionModes,
|
|
2562
|
+
defaultExecutionMode,
|
|
2557
2563
|
presentation: runtimePresentation,
|
|
2558
2564
|
}),
|
|
2559
2565
|
};
|
|
@@ -2632,6 +2638,7 @@ export async function main() {
|
|
|
2632
2638
|
workspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
2633
2639
|
workspaceRoots: workspaceRootMetadata,
|
|
2634
2640
|
executionModes: hostAvailableExecutionModes,
|
|
2641
|
+
defaultExecutionMode,
|
|
2635
2642
|
presentation: runtimePresentation,
|
|
2636
2643
|
}),
|
|
2637
2644
|
};
|
|
@@ -2666,6 +2673,7 @@ export async function main() {
|
|
|
2666
2673
|
try {
|
|
2667
2674
|
runtimeModels = await detectRuntimeModels(workingDir);
|
|
2668
2675
|
runtimeDescriptor = {
|
|
2676
|
+
defaultExecutionMode,
|
|
2669
2677
|
defaultWorkspaceId: workspaceOptions[0]?.id,
|
|
2670
2678
|
defaultModel: runtimeModels[0]?.value,
|
|
2671
2679
|
defaultPermissionMode: 'default',
|
|
@@ -2678,12 +2686,14 @@ export async function main() {
|
|
|
2678
2686
|
workspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
2679
2687
|
workspaceRoots: workspaceRootMetadata,
|
|
2680
2688
|
executionModes: hostAvailableExecutionModes,
|
|
2689
|
+
defaultExecutionMode,
|
|
2681
2690
|
presentation: runtimePresentation,
|
|
2682
2691
|
}),
|
|
2683
2692
|
};
|
|
2684
2693
|
}
|
|
2685
2694
|
catch {
|
|
2686
2695
|
runtimeDescriptor = {
|
|
2696
|
+
defaultExecutionMode,
|
|
2687
2697
|
defaultWorkspaceId: workspaceOptions[0]?.id,
|
|
2688
2698
|
defaultPermissionMode: 'default',
|
|
2689
2699
|
availablePermissionModes: [...CLAUDE_PERMISSION_MODE_OPTIONS],
|
|
@@ -2694,6 +2704,7 @@ export async function main() {
|
|
|
2694
2704
|
workspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
2695
2705
|
workspaceRoots: workspaceRootMetadata,
|
|
2696
2706
|
executionModes: hostAvailableExecutionModes,
|
|
2707
|
+
defaultExecutionMode,
|
|
2697
2708
|
presentation: runtimePresentation,
|
|
2698
2709
|
}),
|
|
2699
2710
|
};
|
|
@@ -2716,6 +2727,15 @@ export async function main() {
|
|
|
2716
2727
|
}
|
|
2717
2728
|
// ── Session manager ──
|
|
2718
2729
|
const sessions = new Map();
|
|
2730
|
+
const recoveryCheckpointTrackers = new Map();
|
|
2731
|
+
function recoveryCheckpointsFor(conversationId) {
|
|
2732
|
+
let tracker = recoveryCheckpointTrackers.get(conversationId);
|
|
2733
|
+
if (!tracker) {
|
|
2734
|
+
tracker = createClaudeRecoveryCheckpointTracker((messageId) => persistConversationRecoveryCursor(conversationId, messageId));
|
|
2735
|
+
recoveryCheckpointTrackers.set(conversationId, tracker);
|
|
2736
|
+
}
|
|
2737
|
+
return tracker;
|
|
2738
|
+
}
|
|
2719
2739
|
const approvalManager = ownerId ? new ApprovalManager(client, agentId, ownerId) : null;
|
|
2720
2740
|
// Interactive runtime input (AskUserQuestion) works with or without an owner,
|
|
2721
2741
|
// so its poll engine is always present. Create + poll fold onto the built-in
|
|
@@ -2944,10 +2964,12 @@ export async function main() {
|
|
|
2944
2964
|
const session = sessions.get(conversationId);
|
|
2945
2965
|
if (!session || session.pendingInputs.length === 0)
|
|
2946
2966
|
return;
|
|
2947
|
-
const
|
|
2948
|
-
|
|
2949
|
-
if (session.pendingInputs.length === before)
|
|
2967
|
+
const removed = session.pendingInputs.filter((input) => input.sourceMessageId === sourceMessageId);
|
|
2968
|
+
if (removed.length === 0)
|
|
2950
2969
|
return;
|
|
2970
|
+
session.pendingInputs = session.pendingInputs.filter((input) => input.sourceMessageId !== sourceMessageId);
|
|
2971
|
+
for (const input of removed)
|
|
2972
|
+
session.markInputCompleted(input);
|
|
2951
2973
|
session.writeTurn();
|
|
2952
2974
|
}
|
|
2953
2975
|
function evictOldestIdle() {
|
|
@@ -2979,7 +3001,7 @@ export async function main() {
|
|
|
2979
3001
|
}
|
|
2980
3002
|
const creation = (async () => {
|
|
2981
3003
|
const config = await loadSessionConfig(conversationId, agentId, rtdb);
|
|
2982
|
-
const sessionExecutionMode = resolveSessionExecutionMode(config);
|
|
3004
|
+
const sessionExecutionMode = resolveSessionExecutionMode(config, defaultExecutionMode);
|
|
2983
3005
|
const workspaceCwd = resolveWorkspaceCwd(config);
|
|
2984
3006
|
const environment = prepareConversationEnvironment({
|
|
2985
3007
|
agentId,
|
|
@@ -3040,14 +3062,16 @@ export async function main() {
|
|
|
3040
3062
|
ultracode: config?.ultracode,
|
|
3041
3063
|
availableModels: runtimeModels,
|
|
3042
3064
|
availableExecutionModes: hostAvailableExecutionModes,
|
|
3065
|
+
defaultExecutionMode,
|
|
3043
3066
|
runtimePresentation,
|
|
3044
3067
|
approvalManager,
|
|
3045
3068
|
runtimeInputManager,
|
|
3046
3069
|
canRequestApproval: () => canRequestCanonApproval(conversationId),
|
|
3047
3070
|
runtimeId,
|
|
3048
|
-
recoveryBaseCwd: workingDir,
|
|
3049
3071
|
ownerId,
|
|
3072
|
+
communicationEnabled,
|
|
3050
3073
|
onInputCompleted: (sourceMessageId) => {
|
|
3074
|
+
recoveryCheckpointsFor(conversationId).settle(sourceMessageId);
|
|
3051
3075
|
settleInboundMessageId(sourceMessageId, true);
|
|
3052
3076
|
},
|
|
3053
3077
|
publishSessionSnapshots: (ids) => { void publishSessionSnapshots(ids); },
|
|
@@ -3239,16 +3263,44 @@ export async function main() {
|
|
|
3239
3263
|
workspaceId: CLAUDE_RECOVERY_CURSOR_WORKSPACE_ID,
|
|
3240
3264
|
})?.lastInboundMessageId ?? null;
|
|
3241
3265
|
}
|
|
3266
|
+
function persistConversationRecoveryCursor(conversationId, messageId) {
|
|
3267
|
+
try {
|
|
3268
|
+
saveRuntimeSessionState(runtimeId, {
|
|
3269
|
+
conversationId,
|
|
3270
|
+
baseCwd: workingDir,
|
|
3271
|
+
workspaceId: CLAUDE_RECOVERY_CURSOR_WORKSPACE_ID,
|
|
3272
|
+
lastInboundMessageId: messageId,
|
|
3273
|
+
});
|
|
3274
|
+
return true;
|
|
3275
|
+
}
|
|
3276
|
+
catch (error) {
|
|
3277
|
+
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Failed to persist inbound recovery cursor:`, error instanceof Error ? error.message : error);
|
|
3278
|
+
return false;
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
let startupRecoveryComplete = false;
|
|
3242
3282
|
async function performMissedInboundRecovery() {
|
|
3283
|
+
const knownBeforeRefresh = new Set(knownConversationIds);
|
|
3284
|
+
await refreshKnownConversationIds(true);
|
|
3285
|
+
const conversationsDiscoveredWhileOffline = startupRecoveryComplete
|
|
3286
|
+
? new Set([...knownConversationIds].filter((id) => !knownBeforeRefresh.has(id)))
|
|
3287
|
+
: new Set();
|
|
3243
3288
|
for (const conversationId of knownConversationIds) {
|
|
3289
|
+
const recoveryCheckpoints = recoveryCheckpointsFor(conversationId);
|
|
3290
|
+
const recoveryBatch = recoveryCheckpoints.reserveBatch();
|
|
3244
3291
|
try {
|
|
3245
3292
|
const recovered = await collectMissedInboundMessages({
|
|
3246
3293
|
fetchPage: (before) => client.getMessagesPage(conversationId, STARTUP_RECOVERY_PAGE_SIZE, before),
|
|
3247
3294
|
cursor: persistedConversationCursor(conversationId),
|
|
3248
3295
|
agentId,
|
|
3296
|
+
requireContiguousCursor: true,
|
|
3297
|
+
noCursorMode: conversationsDiscoveredWhileOffline.has(conversationId)
|
|
3298
|
+
? 'bounded-window'
|
|
3299
|
+
: 'latest-only',
|
|
3249
3300
|
});
|
|
3250
|
-
|
|
3251
|
-
|
|
3301
|
+
recoveryBatch.commit(recovered.messages.map((message) => message.id), recovered.mode === 'incomplete-gap' ? recovered.recoveryCursor : null);
|
|
3302
|
+
if (recovered.mode === 'incomplete-gap') {
|
|
3303
|
+
console.error(`[canon-host] [${conversationId.slice(0, 8)}] recovery_incomplete_gap: cursor is missing; replaying ${recovered.messages.length} bounded inbound message(s) before advancing to ${recovered.recoveryCursor ?? 'no cursor'}`);
|
|
3252
3304
|
}
|
|
3253
3305
|
for (const message of recovered.messages) {
|
|
3254
3306
|
if (!claimInboundMessageId(message.id))
|
|
@@ -3268,8 +3320,10 @@ export async function main() {
|
|
|
3268
3320
|
}) === 'queued';
|
|
3269
3321
|
}
|
|
3270
3322
|
}
|
|
3271
|
-
if (!queued)
|
|
3323
|
+
if (!queued) {
|
|
3324
|
+
recoveryCheckpoints.settle(message.id);
|
|
3272
3325
|
settleInboundMessageId(message.id, true);
|
|
3326
|
+
}
|
|
3273
3327
|
}
|
|
3274
3328
|
catch (error) {
|
|
3275
3329
|
settleInboundMessageId(message.id, false);
|
|
@@ -3281,21 +3335,14 @@ export async function main() {
|
|
|
3281
3335
|
}
|
|
3282
3336
|
}
|
|
3283
3337
|
catch (error) {
|
|
3338
|
+
recoveryBatch.cancel();
|
|
3284
3339
|
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Startup recovery failed:`, error instanceof Error ? error.message : error);
|
|
3285
3340
|
}
|
|
3286
3341
|
}
|
|
3342
|
+
startupRecoveryComplete = true;
|
|
3287
3343
|
}
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
if (recoveryInFlight)
|
|
3291
|
-
return recoveryInFlight;
|
|
3292
|
-
recoveryInFlight = performMissedInboundRecovery().finally(() => {
|
|
3293
|
-
recoveryInFlight = null;
|
|
3294
|
-
});
|
|
3295
|
-
return recoveryInFlight;
|
|
3296
|
-
}
|
|
3297
|
-
const shouldRecoverAfterStreamConnect = createReconnectRecoveryGate();
|
|
3298
|
-
await recoverMissedInboundMessages();
|
|
3344
|
+
const reconnectRecovery = createReconnectRecoveryCoordinator(performMissedInboundRecovery);
|
|
3345
|
+
await reconnectRecovery.recoverNow();
|
|
3299
3346
|
// ── Connect SSE stream for inbound Canon messages ──
|
|
3300
3347
|
const stream = new CanonStream({
|
|
3301
3348
|
apiKey,
|
|
@@ -3306,11 +3353,13 @@ export async function main() {
|
|
|
3306
3353
|
const m = payload.message;
|
|
3307
3354
|
if (m.senderId === agentId)
|
|
3308
3355
|
return;
|
|
3356
|
+
const convoId = payload.conversationId;
|
|
3309
3357
|
if (!claimInboundMessageId(m.id))
|
|
3310
3358
|
return;
|
|
3311
|
-
|
|
3359
|
+
recoveryCheckpointsFor(convoId).track(m.id);
|
|
3312
3360
|
knownConversationIds.add(convoId);
|
|
3313
3361
|
if (handleRuntimeReplyMessage(convoId, m)) {
|
|
3362
|
+
recoveryCheckpointsFor(convoId).settle(m.id);
|
|
3314
3363
|
settleInboundMessageId(m.id, true);
|
|
3315
3364
|
return;
|
|
3316
3365
|
}
|
|
@@ -3318,6 +3367,7 @@ export async function main() {
|
|
|
3318
3367
|
const isOwner = m.isOwner ?? (ownerId != null && m.senderId === ownerId);
|
|
3319
3368
|
if (payload.turnDispatch && payload.turnDispatch.kind !== 'run_turn') {
|
|
3320
3369
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Ignoring server-dispatched observe-only message: ${payload.turnDispatch.reason}`);
|
|
3370
|
+
recoveryCheckpointsFor(convoId).settle(m.id);
|
|
3321
3371
|
settleInboundMessageId(m.id, true);
|
|
3322
3372
|
return;
|
|
3323
3373
|
}
|
|
@@ -3421,8 +3471,10 @@ export async function main() {
|
|
|
3421
3471
|
}, deliveryIntent, m.id ?? null, shouldMarkAccepted, isOwner, m.senderType === 'human' ? m.senderId : null, turnModes);
|
|
3422
3472
|
return true;
|
|
3423
3473
|
})().then((queued) => {
|
|
3424
|
-
if (!queued)
|
|
3474
|
+
if (!queued) {
|
|
3475
|
+
recoveryCheckpointsFor(convoId).settle(m.id);
|
|
3425
3476
|
settleInboundMessageId(m.id, true);
|
|
3477
|
+
}
|
|
3426
3478
|
}).catch((error) => {
|
|
3427
3479
|
settleInboundMessageId(m.id, false);
|
|
3428
3480
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to process inbound message:`, error instanceof Error ? error.message : error);
|
|
@@ -3434,11 +3486,19 @@ export async function main() {
|
|
|
3434
3486
|
onConversationUpdated: (payload) => {
|
|
3435
3487
|
handleConversationUpdated(payload);
|
|
3436
3488
|
},
|
|
3489
|
+
onReplayExpired: () => {
|
|
3490
|
+
void reconnectRecovery.onReplayExpired().catch((error) => {
|
|
3491
|
+
console.error('[canon-host] Replay-expiry recovery failed:', error instanceof Error ? error.message : error);
|
|
3492
|
+
});
|
|
3493
|
+
},
|
|
3437
3494
|
onConnected: () => {
|
|
3438
3495
|
streamConnected = true;
|
|
3439
3496
|
void publishRuntimeHeartbeat();
|
|
3440
|
-
|
|
3441
|
-
|
|
3497
|
+
const recovery = reconnectRecovery.onConnected();
|
|
3498
|
+
if (recovery) {
|
|
3499
|
+
void recovery.catch((error) => {
|
|
3500
|
+
console.error('[canon-host] Reconnect recovery failed:', error instanceof Error ? error.message : error);
|
|
3501
|
+
});
|
|
3442
3502
|
}
|
|
3443
3503
|
console.error('[canon-host] SSE connected');
|
|
3444
3504
|
},
|
package/dist/server.js
CHANGED
|
@@ -16,7 +16,7 @@ import { ApprovalHttpServer } from './approval-server.js';
|
|
|
16
16
|
import { renderClaudeInboundContent } from './canon-user-content.js';
|
|
17
17
|
import { runCli } from '@canonmsg/core';
|
|
18
18
|
import { parseReplyArgs, parseSendMessageArgs, parseSetTypingArgs, } from './mcp-args.js';
|
|
19
|
-
import { canonVerbToolDefinitions, executeCanonVerbTool, isCanonToolVerb,
|
|
19
|
+
import { canonVerbToolDefinitions, createCanonCommunicationBinding, executeCanonVerbTool, isCanonToolVerb, } from '@canonmsg/agent-tools';
|
|
20
20
|
const HELP = `canon-channel-server — Claude Code MCP channel server for Canon
|
|
21
21
|
|
|
22
22
|
USAGE
|
|
@@ -45,6 +45,7 @@ const conversationCache = new Map();
|
|
|
45
45
|
const TURN_COMPLETE_METADATA = {
|
|
46
46
|
turnSemantics: 'turn_complete',
|
|
47
47
|
};
|
|
48
|
+
const communicationBinding = createCanonCommunicationBinding();
|
|
48
49
|
/** Last owner-visible conversation to route approval cards into. */
|
|
49
50
|
let approvalConversationId = null;
|
|
50
51
|
/** Whether the message that most recently triggered work came from the owner. */
|
|
@@ -175,6 +176,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
175
176
|
// Channel mode owns no turn, so `no_reply` is an ack-only no-op here: the
|
|
176
177
|
// standalone session has no final delivery for it to suppress.
|
|
177
178
|
...canonVerbToolDefinitions(),
|
|
179
|
+
...(agentContext?.outboundPolicy === 'closed' ? [] : communicationBinding.tools),
|
|
178
180
|
],
|
|
179
181
|
}));
|
|
180
182
|
// ── Tool handlers ──────────────────────────────────────────────────────
|
|
@@ -314,10 +316,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
314
316
|
}
|
|
315
317
|
default: {
|
|
316
318
|
const name = request.params.name;
|
|
319
|
+
if (communicationBinding.isToolName(name)) {
|
|
320
|
+
const result = await communicationBinding.execute(client, name, args);
|
|
321
|
+
return {
|
|
322
|
+
content: result.content.map((item) => ({ type: 'text', text: item.text })),
|
|
323
|
+
...(result.isError ? { isError: true } : {}),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
317
326
|
if (isCanonToolVerb(name)) {
|
|
318
327
|
const rawArgs = args && typeof args === 'object' ? { ...args } : {};
|
|
319
|
-
const
|
|
320
|
-
const verbResult = await executeCanonVerbTool(client, name, verbArgs);
|
|
328
|
+
const verbResult = await executeCanonVerbTool(client, name, rawArgs);
|
|
321
329
|
return {
|
|
322
330
|
content: verbResult.content.map((item) => ({ type: 'text', text: item.text })),
|
|
323
331
|
...(verbResult.isError ? { isError: true } : {}),
|
package/dist/tool-policy.js
CHANGED
|
@@ -37,13 +37,9 @@ function normalizeToolName(toolName) {
|
|
|
37
37
|
function isCanonOutboundTool(normalizedToolName) {
|
|
38
38
|
return normalizedToolName.endsWith('__reply')
|
|
39
39
|
|| normalizedToolName.endsWith('__send_message')
|
|
40
|
-
|| normalizedToolName.endsWith('__send_contextual_message')
|
|
41
|
-
|| normalizedToolName.endsWith('__send_to')
|
|
42
40
|
|| normalizedToolName.endsWith('__share_contact')
|
|
43
41
|
|| normalizedToolName === 'reply'
|
|
44
42
|
|| normalizedToolName === 'send_message'
|
|
45
|
-
|| normalizedToolName === 'send_contextual_message'
|
|
46
|
-
|| normalizedToolName === 'send_to'
|
|
47
43
|
|| normalizedToolName === 'share_contact';
|
|
48
44
|
}
|
|
49
45
|
/**
|
|
@@ -79,7 +75,7 @@ const CANON_INTERACTION_LIFECYCLE_VERBS = new Set([
|
|
|
79
75
|
* Verbs a NON-owner conversation member may trigger: same-conversation HITL
|
|
80
76
|
* and display surfaces, whose responder routing and owner-only escalation
|
|
81
77
|
* (secret/sudo, session rules) are enforced server-side. Cross-conversation
|
|
82
|
-
* verbs (
|
|
78
|
+
* cross-conversation verbs (share_contact) and the reads (contacts, conversation
|
|
83
79
|
* lists — they expose the agent's other relationships to this conversation)
|
|
84
80
|
* stay owner-only until the provenance trigger-token primitive lands.
|
|
85
81
|
*/
|
|
@@ -257,7 +253,7 @@ export async function decideClaudeToolPermissionForMode(input) {
|
|
|
257
253
|
}
|
|
258
254
|
// HITL + read verbs are allowed without an approval card: the HITL verbs
|
|
259
255
|
// themselves render the human-facing card (recursion otherwise), and the
|
|
260
|
-
// reads are side-effect free. Outbound verbs (
|
|
256
|
+
// reads are side-effect free. Outbound verbs (share_contact)
|
|
261
257
|
// fall through to the mode policy — in native-approval mode they gate
|
|
262
258
|
// like any other side effect; in default mode owner turns allow.
|
|
263
259
|
if (CANON_HITL_OR_READ_VERBS.has(canonVerb) && !input.ruleForcedAsk) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/claude-code-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.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",
|
|
@@ -31,11 +31,11 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@anthropic-ai/claude-agent-sdk": "0.3.228",
|
|
34
|
-
"@canonmsg/agent-sdk": "^
|
|
35
|
-
"@canonmsg/agent-tools": "^0.
|
|
36
|
-
"@canonmsg/coding-agent-host": "^0.
|
|
37
|
-
"@canonmsg/core": "^
|
|
38
|
-
"@canonmsg/rich-cards": "^0.10.
|
|
34
|
+
"@canonmsg/agent-sdk": "^9.0.0",
|
|
35
|
+
"@canonmsg/agent-tools": "^0.7.0",
|
|
36
|
+
"@canonmsg/coding-agent-host": "^0.7.0",
|
|
37
|
+
"@canonmsg/core": "^11.0.0",
|
|
38
|
+
"@canonmsg/rich-cards": "^0.10.3",
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.30.0"
|
|
40
40
|
},
|
|
41
41
|
"engines": {
|