@canonmsg/claude-code-plugin 0.21.0 → 0.21.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/dist/host.js CHANGED
@@ -36,8 +36,8 @@ import { ApprovalManager, readHostSessionConfig, resolveHostWorkspaceCwd, } from
36
36
  import { runCli } from './cli-entry.js';
37
37
  import { synthesizeClaudeApprovalDiff } from './approval-diff.js';
38
38
  import { decideClaudeToolPermissionForMode, parseAllowedNonOwnerClaudeTools, } from './tool-policy.js';
39
- import { buildClaudeFinalMessageId, createClaudeInputEnvelope, formatClaudeControlError, mergeClaudeModelOptions, resetClaudeCompletedTurnState, shouldDeliverClaudeFinal, } from './session-state.js';
40
- import { claudeThinkingToFlagValue, claudeThinkingToConfig, parseClaudeSessionDefaults, parseClaudeThinkingMode, parseClaudeUltracodeMode, resolveEffectiveSessionSettings, } from './session-defaults.js';
39
+ import { applyClaudeSessionControl, buildClaudeFinalMessageId, buildClaudeInitialSessionState, createClaudeInputEnvelope, formatClaudeControlError, normalizeClaudeEffortLevel, parseClaudePermissionMode, resolveClaudeModelOptions, resetClaudeCompletedTurnState, shouldDeliverClaudeFinal, } from './session-state.js';
40
+ import { claudeThinkingToConfig, parseClaudeSessionDefaults, resolveEffectiveSessionSettings, } from './session-defaults.js';
41
41
  import { mapClaudeSubagentActivity } from './subagent-activity.js';
42
42
  function parseRuntimeVisibilityPreset(value) {
43
43
  return value === 'normal' || value === 'minimal' || value === 'full' ? value : undefined;
@@ -295,26 +295,6 @@ function buildClaudeRuntimeDescriptor(input) {
295
295
  commands,
296
296
  });
297
297
  }
298
- const CLAUDE_EFFORT_VALUES = new Set(CLAUDE_EFFORT_OPTIONS.map((option) => option.value));
299
- function parseClaudePermissionMode(value) {
300
- if (value === 'acceptEdits'
301
- || value === 'auto'
302
- || value === 'bypassPermissions'
303
- || value === 'default'
304
- || value === 'dontAsk'
305
- || value === 'plan') {
306
- return value;
307
- }
308
- return null;
309
- }
310
- function normalizeClaudeEffortLevel(value) {
311
- return typeof value === 'string' && CLAUDE_EFFORT_VALUES.has(value)
312
- ? value
313
- : undefined;
314
- }
315
- async function applyClaudeEffortLevel(q, effort) {
316
- await q.applyFlagSettings({ effortLevel: effort });
317
- }
318
298
  let cachedClaudeCliPath;
319
299
  // The SDK bundles a Bun-compiled `claude` Mach-O binary next to
320
300
  // claude-agent-sdk-darwin-arm64. On some macOS setups that exact binary gets
@@ -404,6 +384,7 @@ async function detectRuntimeModels(cwd) {
404
384
  options: {
405
385
  systemPrompt: { type: 'preset', preset: 'claude_code' },
406
386
  tools: { type: 'preset', preset: 'claude_code' },
387
+ model: 'default',
407
388
  permissionMode: 'default',
408
389
  cwd,
409
390
  settings: { forceLoginMethod: 'claudeai' },
@@ -431,6 +412,7 @@ async function inspectClaudeRuntimeMetadata(cwd, activeQuery) {
431
412
  options: {
432
413
  systemPrompt: { type: 'preset', preset: 'claude_code' },
433
414
  tools: { type: 'preset', preset: 'claude_code' },
415
+ model: 'default',
434
416
  permissionMode: 'default',
435
417
  cwd,
436
418
  settings: { forceLoginMethod: 'claudeai' },
@@ -871,6 +853,7 @@ function createSession(conversationId, environment, agentId, port, typingSignals
871
853
  const queryOptions = {
872
854
  systemPrompt: { type: 'preset', preset: 'claude_code' },
873
855
  tools: { type: 'preset', preset: 'claude_code' },
856
+ model: config.model ?? 'default',
874
857
  permissionMode: parseClaudePermissionMode(config.permissionMode) ?? 'default',
875
858
  allowDangerouslySkipPermissions: true,
876
859
  cwd,
@@ -1092,16 +1075,16 @@ function createSession(conversationId, environment, agentId, port, typingSignals
1092
1075
  cwd,
1093
1076
  environment,
1094
1077
  query: q,
1078
+ controlApplySeq: new Map(),
1095
1079
  sendInput,
1096
1080
  enqueueInbound: (msg, intent = 'queue', sourceMessageId, markAccepted = false, isOwnerTurn = false, artifactRoutingMode = 'disabled') => enqueueInboundMessage(msg, intent, sourceMessageId, markAccepted, isOwnerTurn, artifactRoutingMode),
1097
- state: {
1098
- model: undefined,
1081
+ state: buildClaudeInitialSessionState({
1082
+ model: config.model,
1099
1083
  permissionMode: config.permissionMode,
1100
- effort: undefined,
1084
+ effort: config.effort,
1101
1085
  fallbackModel: config.sessionSettings.fallbackModel,
1102
1086
  runtimeControlValues: config.sessionSettings.runtimeControlValues,
1103
- state: 'idle',
1104
- },
1087
+ }),
1105
1088
  availableModels: config.availableModels,
1106
1089
  streamingText: '',
1107
1090
  streamingTimer: null,
@@ -1140,12 +1123,7 @@ function createSession(conversationId, environment, agentId, port, typingSignals
1140
1123
  capabilities: CLAUDE_RUNTIME_CAPABILITIES,
1141
1124
  fallbackTurnId: `claude-${conversationId}`,
1142
1125
  streamingThrottleMs: STREAMING_THROTTLE_MS,
1143
- requestedControls: {
1144
- model: config.model,
1145
- permissionMode: config.permissionMode,
1146
- effort: config.effort,
1147
- runtimeControlValues: config.sessionSettings.runtimeControlValues,
1148
- },
1126
+ requestedControls: {},
1149
1127
  }, {
1150
1128
  session: () => session,
1151
1129
  port,
@@ -1161,6 +1139,9 @@ function createSession(conversationId, environment, agentId, port, typingSignals
1161
1139
  formatControlError: formatClaudeControlError,
1162
1140
  });
1163
1141
  const { streamingOutput, flushStreaming, onStreamDelta, clearStreaming, getFinalTurnTrail, writeState, writeTurn, setRuntimeControlError, clearRuntimeControlError, refreshVisibleWorkSignal, startVisibleWorkSignal, stopVisibleWorkSignal, } = writers;
1142
+ // Initial values come from persisted session config and are effective for
1143
+ // this query. Publish immediately so controls do not linger as requested.
1144
+ writeState();
1164
1145
  function isActiveTurnInterrupted() {
1165
1146
  const turnKey = session.activeInput?.turnKey;
1166
1147
  return Boolean(turnKey && session.interruptedTurnKeys.has(turnKey));
@@ -1485,24 +1466,6 @@ function createSession(conversationId, environment, agentId, port, typingSignals
1485
1466
  const init = async () => {
1486
1467
  processMessages().catch((err) => console.error(`[canon-host] [${conversationId.slice(0, 8)}] processMessages crashed:`, err));
1487
1468
  await q.initializationResult();
1488
- if (config.model) {
1489
- try {
1490
- await q.setModel(config.model);
1491
- session.state.model = config.model;
1492
- }
1493
- catch (err) {
1494
- console.error(`[canon-host] [${conversationId.slice(0, 8)}] Failed to set initial model:`, err);
1495
- }
1496
- }
1497
- if (config.effort) {
1498
- try {
1499
- await applyClaudeEffortLevel(q, config.effort);
1500
- session.state.effort = config.effort;
1501
- }
1502
- catch (err) {
1503
- console.error(`[canon-host] [${conversationId.slice(0, 8)}] Failed to set initial effort:`, err);
1504
- }
1505
- }
1506
1469
  if (!resumeSessionId) {
1507
1470
  // New session — send seed message to activate streaming input mode
1508
1471
  sendInput(createClaudeInputEnvelope({
@@ -1517,16 +1480,22 @@ function createSession(conversationId, environment, agentId, port, typingSignals
1517
1480
  },
1518
1481
  }));
1519
1482
  }
1520
- // Refresh the canonical runtime descriptor after Claude reports supported models.
1483
+ // Refresh the canonical runtime descriptor from a neutral probe. The live
1484
+ // session may be initialized with a selected model, which can narrow the
1485
+ // SDK's cached model list.
1521
1486
  try {
1522
- const models = await q.supportedModels();
1523
- const modelList = mergeClaudeModelOptions({
1524
- previous: runtimeModels,
1525
- next: toModelOptions(models),
1487
+ const discoveredModels = await detectRuntimeModels(cwd).catch((error) => {
1488
+ console.error(`[canon-host] [${conversationId.slice(0, 8)}] Model discovery failed:`, error);
1489
+ return runtimeModels;
1490
+ });
1491
+ if (discoveredModels.length > 0) {
1492
+ runtimeModels = discoveredModels;
1493
+ }
1494
+ const modelList = resolveClaudeModelOptions({
1495
+ discovered: runtimeModels,
1526
1496
  selectedModel: session.state.model,
1527
1497
  });
1528
1498
  session.availableModels = modelList;
1529
- runtimeModels = modelList;
1530
1499
  onRuntimeDescriptorUpdate({
1531
1500
  defaultWorkspaceId: workspaceOptions[0]?.id,
1532
1501
  defaultModel: config.sessionSettings.model ?? modelList[0]?.value,
@@ -1544,7 +1513,7 @@ function createSession(conversationId, environment, agentId, port, typingSignals
1544
1513
  }),
1545
1514
  });
1546
1515
  writeState();
1547
- console.error(`[canon-host] [${conversationId.slice(0, 8)}] Models: ${models.map((m) => m.displayName).join(', ')}`);
1516
+ console.error(`[canon-host] [${conversationId.slice(0, 8)}] Models: ${modelList.map((m) => m.label).join(', ')}`);
1548
1517
  }
1549
1518
  catch {
1550
1519
  writeState();
@@ -1702,31 +1671,12 @@ export async function main() {
1702
1671
  refreshFloorMs: HEARTBEAT_MS,
1703
1672
  }, { port });
1704
1673
  const { conversationCache, knownConversationIds, getConversationMeta, refreshKnownConversationIds, handleConversationUpdated, markGroupContextModeUsed, loadHydratedInboundContext, } = directory;
1705
- const publishRuntimeHeartbeat = async () => {
1706
- heartbeatLocalRuntimeEntry(runtimeId, {
1707
- agentId,
1708
- agentName: profileAgentName,
1709
- cwd: workingDir,
1710
- baseCwd: workingDir,
1711
- });
1712
- if (!streamConnected)
1674
+ const publishSessionSnapshotsForConversations = async (conversationIds) => {
1675
+ const ids = Array.from(new Set(conversationIds));
1676
+ if (!streamConnected || ids.length === 0)
1713
1677
  return;
1714
- await refreshKnownConversationIds().catch((error) => {
1715
- console.error('[canon-host] Failed to refresh known conversations:', error);
1716
- });
1717
- const activeSessionQuery = sessions.values().next().value?.query ?? null;
1718
- const metadata = await refreshClaudeRuntimeMetadata({
1719
- cwd: workingDir,
1720
- activeQuery: activeSessionQuery,
1721
- }).catch((error) => {
1722
- console.error('[canon-host] Failed to refresh Claude runtime metadata:', error);
1723
- return runtimeMetadata;
1724
- });
1725
- await publishAgentRuntime(port, runtimeDescriptor).catch((error) => {
1726
- console.error('[canon-host] Failed to publish agent runtime:', error);
1727
- });
1728
1678
  await port.publishHostSessionSnapshots({
1729
- conversationIds: Array.from(knownConversationIds),
1679
+ conversationIds: ids,
1730
1680
  clientType: 'claude-code',
1731
1681
  runtime: runtimeDescriptor,
1732
1682
  workspaceOptions: workspaceOptions.map(({ id, cwd }) => ({ id, cwd })),
@@ -1752,6 +1702,31 @@ export async function main() {
1752
1702
  }).catch((error) => {
1753
1703
  console.error('[canon-host] Failed to publish session snapshots:', error);
1754
1704
  });
1705
+ };
1706
+ const publishRuntimeHeartbeat = async () => {
1707
+ heartbeatLocalRuntimeEntry(runtimeId, {
1708
+ agentId,
1709
+ agentName: profileAgentName,
1710
+ cwd: workingDir,
1711
+ baseCwd: workingDir,
1712
+ });
1713
+ if (!streamConnected)
1714
+ return;
1715
+ await refreshKnownConversationIds().catch((error) => {
1716
+ console.error('[canon-host] Failed to refresh known conversations:', error);
1717
+ });
1718
+ const activeSessionQuery = sessions.values().next().value?.query ?? null;
1719
+ const metadata = await refreshClaudeRuntimeMetadata({
1720
+ cwd: workingDir,
1721
+ activeQuery: activeSessionQuery,
1722
+ }).catch((error) => {
1723
+ console.error('[canon-host] Failed to refresh Claude runtime metadata:', error);
1724
+ return runtimeMetadata;
1725
+ });
1726
+ await publishAgentRuntime(port, runtimeDescriptor).catch((error) => {
1727
+ console.error('[canon-host] Failed to publish agent runtime:', error);
1728
+ });
1729
+ await publishSessionSnapshotsForConversations(knownConversationIds);
1755
1730
  // Rebuild so the descriptor picks up native slash commands from the
1756
1731
  // metadata refresh above — the initial descriptor is built before the
1757
1732
  // command inventory has loaded.
@@ -2373,105 +2348,19 @@ export async function main() {
2373
2348
  },
2374
2349
  });
2375
2350
  // ── Control signal watcher ──
2376
- // Session control signals (model, mode, effort). A missing/closed session
2377
- // leaves the node in place; lastSeen still advances, so a stale control is
2378
- // never replayed once a session exists.
2351
+ // Session control signals (model, mode, effort). The bridge has already
2352
+ // consumed the control notification, so every path must publish an answer.
2379
2353
  async function applyRuntimeSessionControl(event) {
2380
2354
  const convoId = event.conversationId;
2381
2355
  const session = sessions.get(convoId);
2382
- if (!session || session.closed)
2383
- return { consume: false };
2356
+ if (!session || session.closed) {
2357
+ console.error(`[canon-host] [${convoId.slice(0, 8)}] Control received without live session; publishing persisted snapshot`);
2358
+ void publishSessionSnapshotsForConversations([convoId]);
2359
+ return;
2360
+ }
2384
2361
  const ctrl = event.control;
2385
2362
  console.error(`[canon-host] [${convoId.slice(0, 8)}] Control:`, ctrl);
2386
- if (ctrl.model && ctrl.model !== session.state.model) {
2387
- try {
2388
- await session.query.setModel(ctrl.model);
2389
- session.state.model = ctrl.model;
2390
- session.clearRuntimeControlError('model');
2391
- console.error(`[canon-host] [${convoId.slice(0, 8)}] Model → ${ctrl.model}`);
2392
- }
2393
- catch (err) {
2394
- session.setRuntimeControlError('model', ctrl.model, err);
2395
- console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to set model:`, err);
2396
- }
2397
- }
2398
- const nextPermissionMode = parseClaudePermissionMode(ctrl.permissionMode);
2399
- if (ctrl.permissionMode && !nextPermissionMode) {
2400
- session.setRuntimeControlError('permissionMode', ctrl.permissionMode, new Error(`Unsupported Claude permission mode: ${ctrl.permissionMode}`));
2401
- console.error(`[canon-host] [${convoId.slice(0, 8)}] Unsupported mode: ${ctrl.permissionMode}`);
2402
- }
2403
- else if (nextPermissionMode && nextPermissionMode !== session.state.permissionMode) {
2404
- try {
2405
- await session.query.setPermissionMode(nextPermissionMode);
2406
- session.state.permissionMode = nextPermissionMode;
2407
- session.clearRuntimeControlError('permissionMode');
2408
- console.error(`[canon-host] [${convoId.slice(0, 8)}] Mode → ${nextPermissionMode}`);
2409
- }
2410
- catch (err) {
2411
- session.setRuntimeControlError('permissionMode', nextPermissionMode, err);
2412
- console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to set mode:`, err);
2413
- }
2414
- }
2415
- const nextEffort = normalizeClaudeEffortLevel(ctrl.effort);
2416
- if (nextEffort && nextEffort !== session.state.effort) {
2417
- try {
2418
- await applyClaudeEffortLevel(session.query, nextEffort);
2419
- session.state.effort = nextEffort;
2420
- session.clearRuntimeControlError('effort');
2421
- console.error(`[canon-host] [${convoId.slice(0, 8)}] Effort → ${nextEffort}`);
2422
- }
2423
- catch (err) {
2424
- session.setRuntimeControlError('effort', nextEffort, err);
2425
- console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to set effort:`, err);
2426
- }
2427
- }
2428
- else if (ctrl.effort && !nextEffort) {
2429
- session.setRuntimeControlError('effort', String(ctrl.effort), new Error('Unsupported Claude thinking level.'));
2430
- }
2431
- const runtimeControlValues = ctrl.runtimeControlValues ?? {};
2432
- if ('thinking' in runtimeControlValues) {
2433
- const nextThinking = parseClaudeThinkingMode(runtimeControlValues.thinking);
2434
- if (!nextThinking) {
2435
- session.setRuntimeControlError('thinking', String(runtimeControlValues.thinking), new Error('Unsupported Claude thinking mode.'));
2436
- }
2437
- else if (nextThinking !== session.state.runtimeControlValues?.thinking) {
2438
- try {
2439
- await session.query.applyFlagSettings({ alwaysThinkingEnabled: claudeThinkingToFlagValue(nextThinking) });
2440
- session.state.runtimeControlValues = {
2441
- ...(session.state.runtimeControlValues ?? {}),
2442
- thinking: nextThinking,
2443
- };
2444
- session.clearRuntimeControlError('thinking');
2445
- console.error(`[canon-host] [${convoId.slice(0, 8)}] Thinking → ${nextThinking}`);
2446
- }
2447
- catch (err) {
2448
- session.setRuntimeControlError('thinking', nextThinking, err);
2449
- console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to set thinking:`, err);
2450
- }
2451
- }
2452
- }
2453
- if ('ultracode' in runtimeControlValues) {
2454
- const nextUltracode = parseClaudeUltracodeMode(runtimeControlValues.ultracode);
2455
- if (!nextUltracode) {
2456
- session.setRuntimeControlError('ultracode', String(runtimeControlValues.ultracode), new Error('Unsupported Claude ultracode mode.'));
2457
- }
2458
- else if (nextUltracode !== session.state.runtimeControlValues?.ultracode) {
2459
- try {
2460
- await session.query.applyFlagSettings({ ultracode: nextUltracode === 'on' });
2461
- session.state.runtimeControlValues = {
2462
- ...(session.state.runtimeControlValues ?? {}),
2463
- ultracode: nextUltracode,
2464
- };
2465
- session.clearRuntimeControlError('ultracode');
2466
- console.error(`[canon-host] [${convoId.slice(0, 8)}] Ultracode → ${nextUltracode}`);
2467
- }
2468
- catch (err) {
2469
- session.setRuntimeControlError('ultracode', nextUltracode, err);
2470
- console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to set ultracode:`, err);
2471
- }
2472
- }
2473
- }
2474
- session.writeState();
2363
+ applyClaudeSessionControl(session, ctrl);
2475
2364
  }
2476
2365
  // ── Heartbeat + idle cleanup + graceful shutdown (agent-host lifecycle) ──
2477
2366
  lifecycle.startTimers();
@@ -18,6 +18,7 @@ export interface ClaudeSessionControlValues {
18
18
  export interface ClaudeEffectiveSessionSettings extends ClaudeSessionDefaults {
19
19
  runtimeControlValues: Record<string, string>;
20
20
  }
21
+ export declare const CLAUDE_EFFORT_LEVELS: readonly ClaudeEffortLevel[];
21
22
  export declare function parseClaudeThinkingMode(value: unknown): ClaudeThinkingMode | undefined;
22
23
  export declare function parseClaudeUltracodeMode(value: unknown): ClaudeUltracodeMode | undefined;
23
24
  export declare function parseClaudeSessionDefaults(raw: unknown): ClaudeSessionDefaults;
@@ -1,4 +1,5 @@
1
- const CLAUDE_EFFORT_VALUES = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
1
+ export const CLAUDE_EFFORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'];
2
+ const CLAUDE_EFFORT_VALUES = new Set(CLAUDE_EFFORT_LEVELS);
2
3
  function optionalString(value) {
3
4
  return typeof value === 'string' && value.trim() ? value.trim() : undefined;
4
5
  }
@@ -1,6 +1,8 @@
1
- import type { SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
1
+ import type { Query, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
2
2
  import type { DeliveryIntent, ModelOption, TurnLifecycleState } from '@canonmsg/core/contract';
3
3
  import type { TurnArtifactSnapshot } from '@canonmsg/agent-host';
4
+ import { type ClaudeEffortLevel } from './session-defaults.js';
5
+ export type { ClaudeEffortLevel } from './session-defaults.js';
4
6
  export type ClaudeInputKind = 'seed' | 'canon';
5
7
  export type ClaudeArtifactRoutingMode = 'workspace-generated' | 'disabled';
6
8
  export interface ClaudeInputEnvelope {
@@ -19,6 +21,47 @@ export interface ClaudeRuntimeControlError {
19
21
  message: string;
20
22
  updatedAt: number;
21
23
  }
24
+ export type ClaudePermissionMode = Parameters<Query['setPermissionMode']>[0];
25
+ export interface ClaudeSessionControl {
26
+ model?: string;
27
+ permissionMode?: string;
28
+ effort?: string;
29
+ runtimeControlValues?: Record<string, string>;
30
+ updatedAt?: number;
31
+ updatedBy?: string;
32
+ }
33
+ export interface ClaudeHostSessionState {
34
+ model?: string;
35
+ permissionMode?: string;
36
+ effort?: string;
37
+ fallbackModel?: string;
38
+ runtimeControlValues?: Record<string, string>;
39
+ state?: 'idle' | 'running' | 'requires_action';
40
+ contextUsage?: {
41
+ percentage: number;
42
+ totalTokens: number;
43
+ maxTokens: number;
44
+ };
45
+ }
46
+ export declare function parseClaudePermissionMode(value: unknown): ClaudePermissionMode | null;
47
+ export declare function normalizeClaudeEffortLevel(value: unknown): ClaudeEffortLevel | undefined;
48
+ export declare function buildClaudeInitialSessionState(input: {
49
+ model?: string;
50
+ permissionMode?: string;
51
+ effort?: string;
52
+ fallbackModel?: string;
53
+ runtimeControlValues?: Record<string, string>;
54
+ }): ClaudeHostSessionState;
55
+ export interface ClaudeSessionControlTarget {
56
+ state: ClaudeHostSessionState;
57
+ controlApplySeq: Map<string, number>;
58
+ closed: boolean;
59
+ query: Pick<Query, 'setModel' | 'setPermissionMode' | 'applyFlagSettings'>;
60
+ writeState: () => void;
61
+ setRuntimeControlError: (controlId: string, value: string, error: unknown) => void;
62
+ clearRuntimeControlError: (controlId: string) => void;
63
+ }
64
+ export declare function applyClaudeSessionControl(target: ClaudeSessionControlTarget, ctrl: ClaudeSessionControl): void;
22
65
  export interface ClaudePendingFinalDelivery {
23
66
  turnKey: string;
24
67
  text: string;
@@ -60,9 +103,8 @@ export declare function shouldDeliverClaudeFinal(input: {
60
103
  interruptedTurnKeys: ReadonlySet<string>;
61
104
  }): boolean;
62
105
  export declare function resetClaudeCompletedTurnState(session: ClaudeCompletedTurnState): void;
63
- export declare function mergeClaudeModelOptions(input: {
64
- previous: ReadonlyArray<ModelOption>;
65
- next: ReadonlyArray<ModelOption>;
106
+ export declare function resolveClaudeModelOptions(input: {
107
+ discovered: ReadonlyArray<ModelOption>;
66
108
  selectedModel?: string | null;
67
109
  }): ModelOption[];
68
110
  export declare function formatClaudeControlError(error: unknown): string;
@@ -1,4 +1,173 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
+ import { CLAUDE_EFFORT_LEVELS, claudeThinkingToFlagValue, parseClaudeThinkingMode, parseClaudeUltracodeMode, } from './session-defaults.js';
3
+ const CLAUDE_PERMISSION_MODE_VALUES = new Set([
4
+ 'acceptEdits',
5
+ 'auto',
6
+ 'bypassPermissions',
7
+ 'default',
8
+ 'dontAsk',
9
+ 'plan',
10
+ ]);
11
+ const CLAUDE_EFFORT_VALUES = new Set(CLAUDE_EFFORT_LEVELS);
12
+ export function parseClaudePermissionMode(value) {
13
+ return typeof value === 'string' && CLAUDE_PERMISSION_MODE_VALUES.has(value)
14
+ ? value
15
+ : null;
16
+ }
17
+ export function normalizeClaudeEffortLevel(value) {
18
+ return typeof value === 'string' && CLAUDE_EFFORT_VALUES.has(value)
19
+ ? value
20
+ : undefined;
21
+ }
22
+ export function buildClaudeInitialSessionState(input) {
23
+ return {
24
+ ...(input.model ? { model: input.model } : {}),
25
+ ...(input.permissionMode ? { permissionMode: input.permissionMode } : {}),
26
+ ...(input.effort ? { effort: input.effort } : {}),
27
+ ...(input.fallbackModel ? { fallbackModel: input.fallbackModel } : {}),
28
+ ...(input.runtimeControlValues ? { runtimeControlValues: input.runtimeControlValues } : {}),
29
+ state: 'idle',
30
+ };
31
+ }
32
+ function nextControlApplySeq(target, controlId) {
33
+ const nextSeq = (target.controlApplySeq.get(controlId) ?? 0) + 1;
34
+ target.controlApplySeq.set(controlId, nextSeq);
35
+ return nextSeq;
36
+ }
37
+ function applyOptimisticControl(input) {
38
+ const seq = nextControlApplySeq(input.target, input.controlId);
39
+ const applyPromise = (async () => input.apply())();
40
+ void applyPromise
41
+ .catch((error) => {
42
+ if (input.target.closed || input.target.controlApplySeq.get(input.controlId) !== seq) {
43
+ return;
44
+ }
45
+ input.revert();
46
+ input.target.setRuntimeControlError(input.controlId, input.value, error);
47
+ });
48
+ }
49
+ function setRuntimeControlValue(state, controlId, value) {
50
+ state.runtimeControlValues = {
51
+ ...(state.runtimeControlValues ?? {}),
52
+ [controlId]: value,
53
+ };
54
+ }
55
+ export function applyClaudeSessionControl(target, ctrl) {
56
+ const nextModel = typeof ctrl.model === 'string' && ctrl.model.trim() ? ctrl.model.trim() : null;
57
+ if (ctrl.model !== undefined && !nextModel) {
58
+ target.setRuntimeControlError('model', String(ctrl.model), new Error('Unsupported Claude model.'));
59
+ }
60
+ else if (nextModel && nextModel !== target.state.model) {
61
+ const previous = target.state.model;
62
+ target.state.model = nextModel;
63
+ target.clearRuntimeControlError('model');
64
+ applyOptimisticControl({
65
+ target,
66
+ controlId: 'model',
67
+ value: nextModel,
68
+ apply: () => target.query.setModel(nextModel),
69
+ revert: () => {
70
+ if (previous === undefined)
71
+ delete target.state.model;
72
+ else
73
+ target.state.model = previous;
74
+ },
75
+ });
76
+ }
77
+ const nextPermissionMode = parseClaudePermissionMode(ctrl.permissionMode);
78
+ if (ctrl.permissionMode !== undefined && !nextPermissionMode) {
79
+ target.setRuntimeControlError('permissionMode', ctrl.permissionMode, new Error(`Unsupported Claude permission mode: ${ctrl.permissionMode}`));
80
+ }
81
+ else if (nextPermissionMode && nextPermissionMode !== target.state.permissionMode) {
82
+ // Claude Code applies permission-mode changes asynchronously. We publish
83
+ // the requested value immediately so Canon does not show a stale "requested"
84
+ // state while the SDK catches up, then revert if the current apply fails.
85
+ const previous = target.state.permissionMode;
86
+ target.state.permissionMode = nextPermissionMode;
87
+ target.clearRuntimeControlError('permissionMode');
88
+ applyOptimisticControl({
89
+ target,
90
+ controlId: 'permissionMode',
91
+ value: nextPermissionMode,
92
+ apply: () => target.query.setPermissionMode(nextPermissionMode),
93
+ revert: () => {
94
+ if (previous === undefined)
95
+ delete target.state.permissionMode;
96
+ else
97
+ target.state.permissionMode = previous;
98
+ },
99
+ });
100
+ }
101
+ const nextEffort = normalizeClaudeEffortLevel(ctrl.effort);
102
+ if (nextEffort && nextEffort !== target.state.effort) {
103
+ const previous = target.state.effort;
104
+ target.state.effort = nextEffort;
105
+ target.clearRuntimeControlError('effort');
106
+ applyOptimisticControl({
107
+ target,
108
+ controlId: 'effort',
109
+ value: nextEffort,
110
+ apply: () => target.query.applyFlagSettings({ effortLevel: nextEffort }),
111
+ revert: () => {
112
+ if (previous === undefined)
113
+ delete target.state.effort;
114
+ else
115
+ target.state.effort = previous;
116
+ },
117
+ });
118
+ }
119
+ else if (ctrl.effort !== undefined && !nextEffort) {
120
+ target.setRuntimeControlError('effort', String(ctrl.effort), new Error('Unsupported Claude thinking level.'));
121
+ }
122
+ const runtimeControlValues = ctrl.runtimeControlValues ?? {};
123
+ if ('thinking' in runtimeControlValues) {
124
+ const nextThinking = parseClaudeThinkingMode(runtimeControlValues.thinking);
125
+ if (!nextThinking) {
126
+ target.setRuntimeControlError('thinking', String(runtimeControlValues.thinking), new Error('Unsupported Claude thinking mode.'));
127
+ }
128
+ else if (nextThinking !== target.state.runtimeControlValues?.thinking) {
129
+ const previous = target.state.runtimeControlValues?.thinking;
130
+ setRuntimeControlValue(target.state, 'thinking', nextThinking);
131
+ target.clearRuntimeControlError('thinking');
132
+ applyOptimisticControl({
133
+ target,
134
+ controlId: 'thinking',
135
+ value: nextThinking,
136
+ apply: () => target.query.applyFlagSettings({ alwaysThinkingEnabled: claudeThinkingToFlagValue(nextThinking) }),
137
+ revert: () => {
138
+ if (previous === undefined)
139
+ delete target.state.runtimeControlValues?.thinking;
140
+ else
141
+ setRuntimeControlValue(target.state, 'thinking', previous);
142
+ },
143
+ });
144
+ }
145
+ }
146
+ if ('ultracode' in runtimeControlValues) {
147
+ const nextUltracode = parseClaudeUltracodeMode(runtimeControlValues.ultracode);
148
+ if (!nextUltracode) {
149
+ target.setRuntimeControlError('ultracode', String(runtimeControlValues.ultracode), new Error('Unsupported Claude ultracode mode.'));
150
+ }
151
+ else if (nextUltracode !== target.state.runtimeControlValues?.ultracode) {
152
+ const previous = target.state.runtimeControlValues?.ultracode;
153
+ setRuntimeControlValue(target.state, 'ultracode', nextUltracode);
154
+ target.clearRuntimeControlError('ultracode');
155
+ applyOptimisticControl({
156
+ target,
157
+ controlId: 'ultracode',
158
+ value: nextUltracode,
159
+ apply: () => target.query.applyFlagSettings({ ultracode: nextUltracode === 'on' }),
160
+ revert: () => {
161
+ if (previous === undefined)
162
+ delete target.state.runtimeControlValues?.ultracode;
163
+ else
164
+ setRuntimeControlValue(target.state, 'ultracode', previous);
165
+ },
166
+ });
167
+ }
168
+ }
169
+ target.writeState();
170
+ }
2
171
  export function createClaudeInputEnvelope(input) {
3
172
  const sourceMessageId = input.sourceMessageId ?? null;
4
173
  const fallbackId = randomUUID();
@@ -46,32 +215,19 @@ export function resetClaudeCompletedTurnState(session) {
46
215
  session.toolInProgress = false;
47
216
  session.turnState = 'idle';
48
217
  }
49
- export function mergeClaudeModelOptions(input) {
218
+ export function resolveClaudeModelOptions(input) {
50
219
  const byValue = new Map();
51
- for (const option of input.previous) {
52
- byValue.set(option.value, { ...option });
53
- }
54
- for (const option of input.next) {
220
+ for (const option of input.discovered) {
55
221
  byValue.set(option.value, { ...option });
56
222
  }
57
223
  if (input.selectedModel && !byValue.has(input.selectedModel)) {
58
224
  byValue.set(input.selectedModel, {
59
225
  value: input.selectedModel,
60
226
  label: input.selectedModel,
61
- description: 'Selected for this Canon session but not returned by the latest Claude model probe.',
227
+ description: 'Selected for this Canon session but not returned by Claude model discovery.',
62
228
  });
63
229
  }
64
- const nextValues = new Set(input.next.map((option) => option.value));
65
- return [...byValue.values()].map((option) => {
66
- if (nextValues.has(option.value))
67
- return option;
68
- return {
69
- ...option,
70
- description: option.description
71
- ? `${option.description} Previously advertised; not returned by the latest Claude model probe.`
72
- : 'Previously advertised; not returned by the latest Claude model probe.',
73
- };
74
- });
230
+ return [...byValue.values()];
75
231
  }
76
232
  export function formatClaudeControlError(error) {
77
233
  if (error instanceof Error && error.message.trim()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/claude-code-plugin",
3
- "version": "0.21.0",
3
+ "version": "0.21.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",