@canonmsg/claude-code-plugin 0.21.0 → 0.21.2
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 +92 -179
- package/dist/session-defaults.d.ts +1 -0
- package/dist/session-defaults.js +2 -1
- package/dist/session-state.d.ts +53 -4
- package/dist/session-state.js +199 -17
- package/package.json +3 -3
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,
|
|
40
|
-
import {
|
|
39
|
+
import { applyClaudeSessionControl, buildClaudeFinalMessageId, buildClaudeInitialSessionState, claudeModelInfoToOption, createClaudeInputEnvelope, formatClaudeControlError, isClaudeCustomModelOption, 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
|
|
@@ -351,11 +331,20 @@ function resolveClaudeCliPath() {
|
|
|
351
331
|
console.error('[canon-host] claude CLI not on PATH; using SDK-bundled binary');
|
|
352
332
|
return undefined;
|
|
353
333
|
}
|
|
334
|
+
const CLAUDE_SUPPLEMENTAL_MODEL_PROBES = ['opus'];
|
|
335
|
+
function mergeModelOptions(...groups) {
|
|
336
|
+
const byValue = new Map();
|
|
337
|
+
for (const group of groups) {
|
|
338
|
+
for (const option of group) {
|
|
339
|
+
if (!byValue.has(option.value)) {
|
|
340
|
+
byValue.set(option.value, option);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return [...byValue.values()];
|
|
345
|
+
}
|
|
354
346
|
function toModelOptions(models) {
|
|
355
|
-
return models.map(
|
|
356
|
-
value: model.value,
|
|
357
|
-
label: model.displayName,
|
|
358
|
-
}));
|
|
347
|
+
return models.map(claudeModelInfoToOption);
|
|
359
348
|
}
|
|
360
349
|
async function publishAgentRuntime(port, runtime) {
|
|
361
350
|
await port.publishHostAgentRuntime({
|
|
@@ -395,6 +384,21 @@ function resolveExecutionFallbackReason(environment) {
|
|
|
395
384
|
: environment.reason;
|
|
396
385
|
}
|
|
397
386
|
async function detectRuntimeModels(cwd) {
|
|
387
|
+
const discovered = await detectRuntimeModelsForSelection(cwd, 'default');
|
|
388
|
+
const supplemental = await Promise.all(CLAUDE_SUPPLEMENTAL_MODEL_PROBES.map(async (model) => {
|
|
389
|
+
try {
|
|
390
|
+
const options = await detectRuntimeModelsForSelection(cwd, model);
|
|
391
|
+
const option = options.find((entry) => entry.value === model);
|
|
392
|
+
return option && !isClaudeCustomModelOption(option) ? [option] : [];
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
console.error(`[canon-host] Supplemental Claude model probe failed for ${model}:`, error);
|
|
396
|
+
return [];
|
|
397
|
+
}
|
|
398
|
+
}));
|
|
399
|
+
return mergeModelOptions(discovered, ...supplemental);
|
|
400
|
+
}
|
|
401
|
+
async function detectRuntimeModelsForSelection(cwd, model) {
|
|
398
402
|
const emptyPrompt = {
|
|
399
403
|
async *[Symbol.asyncIterator]() { },
|
|
400
404
|
};
|
|
@@ -404,6 +408,7 @@ async function detectRuntimeModels(cwd) {
|
|
|
404
408
|
options: {
|
|
405
409
|
systemPrompt: { type: 'preset', preset: 'claude_code' },
|
|
406
410
|
tools: { type: 'preset', preset: 'claude_code' },
|
|
411
|
+
model,
|
|
407
412
|
permissionMode: 'default',
|
|
408
413
|
cwd,
|
|
409
414
|
settings: { forceLoginMethod: 'claudeai' },
|
|
@@ -431,6 +436,7 @@ async function inspectClaudeRuntimeMetadata(cwd, activeQuery) {
|
|
|
431
436
|
options: {
|
|
432
437
|
systemPrompt: { type: 'preset', preset: 'claude_code' },
|
|
433
438
|
tools: { type: 'preset', preset: 'claude_code' },
|
|
439
|
+
model: 'default',
|
|
434
440
|
permissionMode: 'default',
|
|
435
441
|
cwd,
|
|
436
442
|
settings: { forceLoginMethod: 'claudeai' },
|
|
@@ -871,6 +877,7 @@ function createSession(conversationId, environment, agentId, port, typingSignals
|
|
|
871
877
|
const queryOptions = {
|
|
872
878
|
systemPrompt: { type: 'preset', preset: 'claude_code' },
|
|
873
879
|
tools: { type: 'preset', preset: 'claude_code' },
|
|
880
|
+
model: config.model ?? 'default',
|
|
874
881
|
permissionMode: parseClaudePermissionMode(config.permissionMode) ?? 'default',
|
|
875
882
|
allowDangerouslySkipPermissions: true,
|
|
876
883
|
cwd,
|
|
@@ -1092,16 +1099,16 @@ function createSession(conversationId, environment, agentId, port, typingSignals
|
|
|
1092
1099
|
cwd,
|
|
1093
1100
|
environment,
|
|
1094
1101
|
query: q,
|
|
1102
|
+
controlApplySeq: new Map(),
|
|
1095
1103
|
sendInput,
|
|
1096
1104
|
enqueueInbound: (msg, intent = 'queue', sourceMessageId, markAccepted = false, isOwnerTurn = false, artifactRoutingMode = 'disabled') => enqueueInboundMessage(msg, intent, sourceMessageId, markAccepted, isOwnerTurn, artifactRoutingMode),
|
|
1097
|
-
state: {
|
|
1098
|
-
model:
|
|
1105
|
+
state: buildClaudeInitialSessionState({
|
|
1106
|
+
model: config.model,
|
|
1099
1107
|
permissionMode: config.permissionMode,
|
|
1100
|
-
effort:
|
|
1108
|
+
effort: config.effort,
|
|
1101
1109
|
fallbackModel: config.sessionSettings.fallbackModel,
|
|
1102
1110
|
runtimeControlValues: config.sessionSettings.runtimeControlValues,
|
|
1103
|
-
|
|
1104
|
-
},
|
|
1111
|
+
}),
|
|
1105
1112
|
availableModels: config.availableModels,
|
|
1106
1113
|
streamingText: '',
|
|
1107
1114
|
streamingTimer: null,
|
|
@@ -1140,12 +1147,7 @@ function createSession(conversationId, environment, agentId, port, typingSignals
|
|
|
1140
1147
|
capabilities: CLAUDE_RUNTIME_CAPABILITIES,
|
|
1141
1148
|
fallbackTurnId: `claude-${conversationId}`,
|
|
1142
1149
|
streamingThrottleMs: STREAMING_THROTTLE_MS,
|
|
1143
|
-
requestedControls: {
|
|
1144
|
-
model: config.model,
|
|
1145
|
-
permissionMode: config.permissionMode,
|
|
1146
|
-
effort: config.effort,
|
|
1147
|
-
runtimeControlValues: config.sessionSettings.runtimeControlValues,
|
|
1148
|
-
},
|
|
1150
|
+
requestedControls: {},
|
|
1149
1151
|
}, {
|
|
1150
1152
|
session: () => session,
|
|
1151
1153
|
port,
|
|
@@ -1161,6 +1163,9 @@ function createSession(conversationId, environment, agentId, port, typingSignals
|
|
|
1161
1163
|
formatControlError: formatClaudeControlError,
|
|
1162
1164
|
});
|
|
1163
1165
|
const { streamingOutput, flushStreaming, onStreamDelta, clearStreaming, getFinalTurnTrail, writeState, writeTurn, setRuntimeControlError, clearRuntimeControlError, refreshVisibleWorkSignal, startVisibleWorkSignal, stopVisibleWorkSignal, } = writers;
|
|
1166
|
+
// Initial values come from persisted session config and are effective for
|
|
1167
|
+
// this query. Publish immediately so controls do not linger as requested.
|
|
1168
|
+
writeState();
|
|
1164
1169
|
function isActiveTurnInterrupted() {
|
|
1165
1170
|
const turnKey = session.activeInput?.turnKey;
|
|
1166
1171
|
return Boolean(turnKey && session.interruptedTurnKeys.has(turnKey));
|
|
@@ -1485,24 +1490,6 @@ function createSession(conversationId, environment, agentId, port, typingSignals
|
|
|
1485
1490
|
const init = async () => {
|
|
1486
1491
|
processMessages().catch((err) => console.error(`[canon-host] [${conversationId.slice(0, 8)}] processMessages crashed:`, err));
|
|
1487
1492
|
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
1493
|
if (!resumeSessionId) {
|
|
1507
1494
|
// New session — send seed message to activate streaming input mode
|
|
1508
1495
|
sendInput(createClaudeInputEnvelope({
|
|
@@ -1517,16 +1504,22 @@ function createSession(conversationId, environment, agentId, port, typingSignals
|
|
|
1517
1504
|
},
|
|
1518
1505
|
}));
|
|
1519
1506
|
}
|
|
1520
|
-
// Refresh the canonical runtime descriptor
|
|
1507
|
+
// Refresh the canonical runtime descriptor from a neutral probe. The live
|
|
1508
|
+
// session may be initialized with a selected model, which can narrow the
|
|
1509
|
+
// SDK's cached model list.
|
|
1521
1510
|
try {
|
|
1522
|
-
const
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1511
|
+
const discoveredModels = await detectRuntimeModels(cwd).catch((error) => {
|
|
1512
|
+
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Model discovery failed:`, error);
|
|
1513
|
+
return runtimeModels;
|
|
1514
|
+
});
|
|
1515
|
+
if (discoveredModels.length > 0) {
|
|
1516
|
+
runtimeModels = discoveredModels;
|
|
1517
|
+
}
|
|
1518
|
+
const modelList = resolveClaudeModelOptions({
|
|
1519
|
+
discovered: runtimeModels,
|
|
1526
1520
|
selectedModel: session.state.model,
|
|
1527
1521
|
});
|
|
1528
1522
|
session.availableModels = modelList;
|
|
1529
|
-
runtimeModels = modelList;
|
|
1530
1523
|
onRuntimeDescriptorUpdate({
|
|
1531
1524
|
defaultWorkspaceId: workspaceOptions[0]?.id,
|
|
1532
1525
|
defaultModel: config.sessionSettings.model ?? modelList[0]?.value,
|
|
@@ -1544,7 +1537,7 @@ function createSession(conversationId, environment, agentId, port, typingSignals
|
|
|
1544
1537
|
}),
|
|
1545
1538
|
});
|
|
1546
1539
|
writeState();
|
|
1547
|
-
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Models: ${
|
|
1540
|
+
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Models: ${modelList.map((m) => m.label).join(', ')}`);
|
|
1548
1541
|
}
|
|
1549
1542
|
catch {
|
|
1550
1543
|
writeState();
|
|
@@ -1702,31 +1695,12 @@ export async function main() {
|
|
|
1702
1695
|
refreshFloorMs: HEARTBEAT_MS,
|
|
1703
1696
|
}, { port });
|
|
1704
1697
|
const { conversationCache, knownConversationIds, getConversationMeta, refreshKnownConversationIds, handleConversationUpdated, markGroupContextModeUsed, loadHydratedInboundContext, } = directory;
|
|
1705
|
-
const
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
agentName: profileAgentName,
|
|
1709
|
-
cwd: workingDir,
|
|
1710
|
-
baseCwd: workingDir,
|
|
1711
|
-
});
|
|
1712
|
-
if (!streamConnected)
|
|
1698
|
+
const publishSessionSnapshotsForConversations = async (conversationIds) => {
|
|
1699
|
+
const ids = Array.from(new Set(conversationIds));
|
|
1700
|
+
if (!streamConnected || ids.length === 0)
|
|
1713
1701
|
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
1702
|
await port.publishHostSessionSnapshots({
|
|
1729
|
-
conversationIds:
|
|
1703
|
+
conversationIds: ids,
|
|
1730
1704
|
clientType: 'claude-code',
|
|
1731
1705
|
runtime: runtimeDescriptor,
|
|
1732
1706
|
workspaceOptions: workspaceOptions.map(({ id, cwd }) => ({ id, cwd })),
|
|
@@ -1752,6 +1726,31 @@ export async function main() {
|
|
|
1752
1726
|
}).catch((error) => {
|
|
1753
1727
|
console.error('[canon-host] Failed to publish session snapshots:', error);
|
|
1754
1728
|
});
|
|
1729
|
+
};
|
|
1730
|
+
const publishRuntimeHeartbeat = async () => {
|
|
1731
|
+
heartbeatLocalRuntimeEntry(runtimeId, {
|
|
1732
|
+
agentId,
|
|
1733
|
+
agentName: profileAgentName,
|
|
1734
|
+
cwd: workingDir,
|
|
1735
|
+
baseCwd: workingDir,
|
|
1736
|
+
});
|
|
1737
|
+
if (!streamConnected)
|
|
1738
|
+
return;
|
|
1739
|
+
await refreshKnownConversationIds().catch((error) => {
|
|
1740
|
+
console.error('[canon-host] Failed to refresh known conversations:', error);
|
|
1741
|
+
});
|
|
1742
|
+
const activeSessionQuery = sessions.values().next().value?.query ?? null;
|
|
1743
|
+
const metadata = await refreshClaudeRuntimeMetadata({
|
|
1744
|
+
cwd: workingDir,
|
|
1745
|
+
activeQuery: activeSessionQuery,
|
|
1746
|
+
}).catch((error) => {
|
|
1747
|
+
console.error('[canon-host] Failed to refresh Claude runtime metadata:', error);
|
|
1748
|
+
return runtimeMetadata;
|
|
1749
|
+
});
|
|
1750
|
+
await publishAgentRuntime(port, runtimeDescriptor).catch((error) => {
|
|
1751
|
+
console.error('[canon-host] Failed to publish agent runtime:', error);
|
|
1752
|
+
});
|
|
1753
|
+
await publishSessionSnapshotsForConversations(knownConversationIds);
|
|
1755
1754
|
// Rebuild so the descriptor picks up native slash commands from the
|
|
1756
1755
|
// metadata refresh above — the initial descriptor is built before the
|
|
1757
1756
|
// command inventory has loaded.
|
|
@@ -2373,105 +2372,19 @@ export async function main() {
|
|
|
2373
2372
|
},
|
|
2374
2373
|
});
|
|
2375
2374
|
// ── Control signal watcher ──
|
|
2376
|
-
// Session control signals (model, mode, effort).
|
|
2377
|
-
//
|
|
2378
|
-
// never replayed once a session exists.
|
|
2375
|
+
// Session control signals (model, mode, effort). The bridge has already
|
|
2376
|
+
// consumed the control notification, so every path must publish an answer.
|
|
2379
2377
|
async function applyRuntimeSessionControl(event) {
|
|
2380
2378
|
const convoId = event.conversationId;
|
|
2381
2379
|
const session = sessions.get(convoId);
|
|
2382
|
-
if (!session || session.closed)
|
|
2383
|
-
|
|
2380
|
+
if (!session || session.closed) {
|
|
2381
|
+
console.error(`[canon-host] [${convoId.slice(0, 8)}] Control received without live session; publishing persisted snapshot`);
|
|
2382
|
+
void publishSessionSnapshotsForConversations([convoId]);
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2384
2385
|
const ctrl = event.control;
|
|
2385
2386
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Control:`, ctrl);
|
|
2386
|
-
|
|
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();
|
|
2387
|
+
applyClaudeSessionControl(session, ctrl);
|
|
2475
2388
|
}
|
|
2476
2389
|
// ── Heartbeat + idle cleanup + graceful shutdown (agent-host lifecycle) ──
|
|
2477
2390
|
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;
|
package/dist/session-defaults.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
const
|
|
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
|
}
|
package/dist/session-state.d.ts
CHANGED
|
@@ -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,15 @@ export declare function shouldDeliverClaudeFinal(input: {
|
|
|
60
103
|
interruptedTurnKeys: ReadonlySet<string>;
|
|
61
104
|
}): boolean;
|
|
62
105
|
export declare function resetClaudeCompletedTurnState(session: ClaudeCompletedTurnState): void;
|
|
63
|
-
export
|
|
64
|
-
|
|
65
|
-
|
|
106
|
+
export interface ClaudeModelInfoLike {
|
|
107
|
+
value: string;
|
|
108
|
+
displayName: string;
|
|
109
|
+
description?: string;
|
|
110
|
+
}
|
|
111
|
+
export declare function claudeModelInfoToOption(model: ClaudeModelInfoLike): ModelOption;
|
|
112
|
+
export declare function isClaudeCustomModelOption(option: ModelOption | null | undefined): boolean;
|
|
113
|
+
export declare function resolveClaudeModelOptions(input: {
|
|
114
|
+
discovered: ReadonlyArray<ModelOption>;
|
|
66
115
|
selectedModel?: string | null;
|
|
67
116
|
}): ModelOption[];
|
|
68
117
|
export declare function formatClaudeControlError(error: unknown): string;
|
package/dist/session-state.js
CHANGED
|
@@ -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,45 @@ export function resetClaudeCompletedTurnState(session) {
|
|
|
46
215
|
session.toolInProgress = false;
|
|
47
216
|
session.turnState = 'idle';
|
|
48
217
|
}
|
|
49
|
-
|
|
218
|
+
function normalizeModelDescription(value) {
|
|
219
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
220
|
+
}
|
|
221
|
+
function displayLabelWithClaudeVersion(input) {
|
|
222
|
+
const label = input.displayName.trim() || input.value;
|
|
223
|
+
const description = normalizeModelDescription(input.description);
|
|
224
|
+
if (!description || label.includes('(') || /\d/.test(label))
|
|
225
|
+
return label;
|
|
226
|
+
const [summary] = description.split('·');
|
|
227
|
+
const versionedLabel = summary?.trim();
|
|
228
|
+
return versionedLabel
|
|
229
|
+
&& versionedLabel.toLowerCase().startsWith(label.toLowerCase())
|
|
230
|
+
&& /\d/.test(versionedLabel)
|
|
231
|
+
? versionedLabel
|
|
232
|
+
: label;
|
|
233
|
+
}
|
|
234
|
+
export function claudeModelInfoToOption(model) {
|
|
235
|
+
return {
|
|
236
|
+
value: model.value,
|
|
237
|
+
label: displayLabelWithClaudeVersion(model),
|
|
238
|
+
...(normalizeModelDescription(model.description) ? { description: normalizeModelDescription(model.description) } : {}),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
export function isClaudeCustomModelOption(option) {
|
|
242
|
+
return option?.description?.trim().toLowerCase() === 'custom model';
|
|
243
|
+
}
|
|
244
|
+
export function resolveClaudeModelOptions(input) {
|
|
50
245
|
const byValue = new Map();
|
|
51
|
-
for (const option of input.
|
|
52
|
-
byValue.set(option.value, { ...option });
|
|
53
|
-
}
|
|
54
|
-
for (const option of input.next) {
|
|
246
|
+
for (const option of input.discovered) {
|
|
55
247
|
byValue.set(option.value, { ...option });
|
|
56
248
|
}
|
|
57
249
|
if (input.selectedModel && !byValue.has(input.selectedModel)) {
|
|
58
250
|
byValue.set(input.selectedModel, {
|
|
59
251
|
value: input.selectedModel,
|
|
60
252
|
label: input.selectedModel,
|
|
61
|
-
description: 'Selected for this Canon session but not returned by
|
|
253
|
+
description: 'Selected for this Canon session but not returned by Claude model discovery.',
|
|
62
254
|
});
|
|
63
255
|
}
|
|
64
|
-
|
|
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
|
-
});
|
|
256
|
+
return [...byValue.values()];
|
|
75
257
|
}
|
|
76
258
|
export function formatClaudeControlError(error) {
|
|
77
259
|
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.
|
|
3
|
+
"version": "0.21.2",
|
|
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,8 +33,8 @@
|
|
|
33
33
|
"@anthropic-ai/claude-agent-sdk": "^0.3.200",
|
|
34
34
|
"@canonmsg/agent-host": "^0.4.0",
|
|
35
35
|
"@canonmsg/agent-sdk": "^3.4.4",
|
|
36
|
-
"@canonmsg/bridge": "^0.2.
|
|
37
|
-
"@canonmsg/core": "^3.2.
|
|
36
|
+
"@canonmsg/bridge": "^0.2.3",
|
|
37
|
+
"@canonmsg/core": "^3.2.1",
|
|
38
38
|
"@canonmsg/framework": "^0.2.0",
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
40
40
|
},
|