@canonmsg/claude-code-plugin 0.23.0 → 0.25.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/dist/host.js +147 -115
- package/dist/session-state.d.ts +42 -1
- package/dist/session-state.js +71 -0
- package/package.json +4 -4
package/dist/host.js
CHANGED
|
@@ -28,12 +28,12 @@ import { readFile } from 'node:fs/promises';
|
|
|
28
28
|
import { query, } from '@anthropic-ai/claude-agent-sdk';
|
|
29
29
|
import { isAnthropicImageAttachment, materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMessage, toAnthropicImageBlock, } from '@canonmsg/agent-sdk';
|
|
30
30
|
import { captureTurnArtifactSnapshot, collectTurnArtifacts, } from '@canonmsg/coding-agent-host';
|
|
31
|
-
import { CLAUDE_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,
|
|
31
|
+
import { CLAUDE_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, resolveQuestionAllowOther, 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, prepareConversationEnvironment, resolveCanonAgent, initRTDBAuth, sendMessageWithRetry, loadHostSessionConfig, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, saveRuntimeSessionState, clearRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
|
|
32
32
|
import { decideAutoReply, } from './inbound-policy.js';
|
|
33
33
|
import { runCli } from './cli-entry.js';
|
|
34
34
|
import { synthesizeClaudeApprovalDiff } from './approval-diff.js';
|
|
35
35
|
import { decideClaudeToolPermissionForMode, parseAllowedNonOwnerClaudeTools, } from './tool-policy.js';
|
|
36
|
-
import { buildClaudeFinalMessageId, createClaudeInputEnvelope, formatClaudeControlError, mergeClaudeModelOptions, resetClaudeCompletedTurnState, shouldDeliverClaudeFinal, } from './session-state.js';
|
|
36
|
+
import { applyClaudeSessionControl, buildClaudeFinalMessageId, createClaudeInputEnvelope, formatClaudeControlError, mergeClaudeModelOptions, resetClaudeCompletedTurnState, shouldDeliverClaudeFinal, } from './session-state.js';
|
|
37
37
|
function parseRuntimeVisibilityPreset(value) {
|
|
38
38
|
return value === 'normal' || value === 'minimal' || value === 'full' ? value : undefined;
|
|
39
39
|
}
|
|
@@ -195,6 +195,26 @@ function buildClaudeRuntimeDescriptor(input) {
|
|
|
195
195
|
controlId: 'effort',
|
|
196
196
|
},
|
|
197
197
|
},
|
|
198
|
+
{
|
|
199
|
+
id: 'ultracode-details',
|
|
200
|
+
label: 'Ultracode',
|
|
201
|
+
description: 'Toggle Claude ultracode using /ultracode.',
|
|
202
|
+
aliases: ['ultracode'],
|
|
203
|
+
category: 'runtime',
|
|
204
|
+
placements: ['composer_slash', 'command_palette'],
|
|
205
|
+
availability: ['always'],
|
|
206
|
+
args: [{
|
|
207
|
+
id: 'level',
|
|
208
|
+
label: 'Level',
|
|
209
|
+
kind: 'enum',
|
|
210
|
+
required: true,
|
|
211
|
+
choices: [...CLAUDE_ULTRACODE_OPTIONS],
|
|
212
|
+
}],
|
|
213
|
+
dispatch: {
|
|
214
|
+
kind: 'control',
|
|
215
|
+
controlId: 'ultracode',
|
|
216
|
+
},
|
|
217
|
+
},
|
|
198
218
|
{
|
|
199
219
|
...RUNTIME_NEW_SESSION_ACTION,
|
|
200
220
|
primitive: 'session.new',
|
|
@@ -212,6 +232,15 @@ function buildClaudeRuntimeDescriptor(input) {
|
|
|
212
232
|
permissionModes: CLAUDE_PERMISSION_MODE_OPTIONS,
|
|
213
233
|
defaultPermissionMode: 'default',
|
|
214
234
|
effortOptions: [...CLAUDE_EFFORT_OPTIONS],
|
|
235
|
+
extraRuntimeControls: [{
|
|
236
|
+
id: 'ultracode',
|
|
237
|
+
label: 'Ultracode',
|
|
238
|
+
options: [...CLAUDE_ULTRACODE_OPTIONS],
|
|
239
|
+
defaultValue: 'off',
|
|
240
|
+
availability: 'setup_and_live',
|
|
241
|
+
liveBehavior: 'immediate',
|
|
242
|
+
selectionPolicy: 'inherit',
|
|
243
|
+
}],
|
|
215
244
|
streamingTextMode: 'delta',
|
|
216
245
|
turnModes: [
|
|
217
246
|
{
|
|
@@ -257,6 +286,16 @@ function normalizeClaudeEffortLevel(value) {
|
|
|
257
286
|
async function applyClaudeEffortLevel(q, effort) {
|
|
258
287
|
await q.applyFlagSettings({ effortLevel: effort });
|
|
259
288
|
}
|
|
289
|
+
const CLAUDE_ULTRACODE_OPTIONS = [
|
|
290
|
+
{ value: 'on', label: 'On' },
|
|
291
|
+
{ value: 'off', label: 'Off' },
|
|
292
|
+
];
|
|
293
|
+
function parseClaudeUltracodeMode(value) {
|
|
294
|
+
return value === 'on' || value === 'off' ? value : undefined;
|
|
295
|
+
}
|
|
296
|
+
async function applyClaudeUltracode(q, enabled) {
|
|
297
|
+
await q.applyFlagSettings({ ultracode: enabled });
|
|
298
|
+
}
|
|
260
299
|
let cachedClaudeCliPath;
|
|
261
300
|
// The SDK bundles a Bun-compiled `claude` Mach-O binary next to
|
|
262
301
|
// claude-agent-sdk-darwin-arm64. On some macOS setups that exact binary gets
|
|
@@ -314,6 +353,7 @@ async function loadSessionConfig(conversationId, agentId) {
|
|
|
314
353
|
? parseClaudePermissionMode(config.permissionMode) ?? undefined
|
|
315
354
|
: undefined,
|
|
316
355
|
effort: normalizeClaudeEffortLevel(config?.effort),
|
|
356
|
+
ultracode: parseClaudeUltracodeMode(config?.runtimeControlValues?.ultracode),
|
|
317
357
|
};
|
|
318
358
|
}
|
|
319
359
|
function resolveSessionExecutionMode(config) {
|
|
@@ -729,7 +769,6 @@ async function buildCanonUserContent(input) {
|
|
|
729
769
|
function createSession(conversationId, environment, agentId, client, typingSignals, runtimeState, onSessionEnd, onRuntimeDescriptorUpdate, config, resumeSessionId) {
|
|
730
770
|
const { cwd } = environment;
|
|
731
771
|
const cardResponseUserId = config.ownerId ?? undefined;
|
|
732
|
-
const cardResponseUser = cardResponseUserId ? { responseUserId: cardResponseUserId } : undefined;
|
|
733
772
|
// Per-session input stream with message queue
|
|
734
773
|
let resolveInput = null;
|
|
735
774
|
let inputDone = false;
|
|
@@ -804,30 +843,6 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
804
843
|
};
|
|
805
844
|
});
|
|
806
845
|
}
|
|
807
|
-
async function waitForRuntimeInputResponse(input) {
|
|
808
|
-
session.lastActivity = Date.now();
|
|
809
|
-
while (Date.now() < input.expiresAt) {
|
|
810
|
-
const response = await client.consumeRuntimeInputResponse({
|
|
811
|
-
conversationId,
|
|
812
|
-
inputId: input.inputId,
|
|
813
|
-
}).catch(() => null);
|
|
814
|
-
if (response?.status === 'submitted') {
|
|
815
|
-
return { status: 'submitted', answers: response.answers, value: response.value };
|
|
816
|
-
}
|
|
817
|
-
if (response?.status === 'cancelled' || response?.status === 'timeout') {
|
|
818
|
-
return { status: response.status };
|
|
819
|
-
}
|
|
820
|
-
await sleep(1_000);
|
|
821
|
-
}
|
|
822
|
-
const response = await client.consumeRuntimeInputResponse({
|
|
823
|
-
conversationId,
|
|
824
|
-
inputId: input.inputId,
|
|
825
|
-
}).catch(() => null);
|
|
826
|
-
if (response?.status === 'submitted') {
|
|
827
|
-
return { status: 'submitted', answers: response.answers, value: response.value };
|
|
828
|
-
}
|
|
829
|
-
return { status: 'timeout' };
|
|
830
|
-
}
|
|
831
846
|
function buildArtifactMediaMetadata() {
|
|
832
847
|
return {
|
|
833
848
|
...(session.currentTurnId ? { turnId: session.currentTurnId } : {}),
|
|
@@ -863,18 +878,19 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
863
878
|
const questionId = randomUUID();
|
|
864
879
|
const questions = mapClaudeQuestions(input.questions);
|
|
865
880
|
const expiresAt = Date.now() + REPLY_TIMEOUT_MS;
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
881
|
+
// Create + poll fold onto the shared spine. `responderPolicy: 'infer'`
|
|
882
|
+
// keeps the claude host's responder rule byte-identical (explicit
|
|
883
|
+
// `cardResponseUserId` wins, else omit — never the manager's ownerId
|
|
884
|
+
// fallback), and `native` carries the claude-specific descriptor.
|
|
885
|
+
const reply = await config.runtimeInputManager.request('input', conversationId, {
|
|
869
886
|
kind: 'clarify',
|
|
870
|
-
expiresAt,
|
|
871
887
|
title: 'Claude needs input',
|
|
872
888
|
prompt: questions?.length
|
|
873
889
|
? 'Claude needs your input to continue.'
|
|
874
890
|
: 'Claude needs input to continue.',
|
|
875
891
|
...(questions ? { questions } : {}),
|
|
876
892
|
sensitive: Boolean(questions?.some((question) => question.isSecret)),
|
|
877
|
-
|
|
893
|
+
responseUserId: cardResponseUserId,
|
|
878
894
|
native: {
|
|
879
895
|
runtime: 'claude-code',
|
|
880
896
|
method: 'AskUserQuestion',
|
|
@@ -882,9 +898,15 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
882
898
|
turnId: session.currentTurnId ?? undefined,
|
|
883
899
|
},
|
|
884
900
|
turnId: session.currentTurnId ?? undefined,
|
|
901
|
+
}, {
|
|
902
|
+
requestId: questionId,
|
|
903
|
+
expiresAt,
|
|
904
|
+
responderPolicy: 'infer',
|
|
905
|
+
onCreated: () => {
|
|
906
|
+
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Runtime input question sent (${questionId.slice(0, 8)})`);
|
|
907
|
+
session.lastActivity = Date.now();
|
|
908
|
+
},
|
|
885
909
|
});
|
|
886
|
-
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Runtime input question sent (${questionId.slice(0, 8)})`);
|
|
887
|
-
const reply = await waitForRuntimeInputResponse({ inputId: questionId, expiresAt });
|
|
888
910
|
if (reply.status !== 'submitted') {
|
|
889
911
|
return { behavior: 'deny', message: 'No reply received (timed out)' };
|
|
890
912
|
}
|
|
@@ -930,22 +952,19 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
930
952
|
const planBody = typeof input.plan === 'string' && input.plan.trim()
|
|
931
953
|
? input.plan.trim()
|
|
932
954
|
: undefined;
|
|
933
|
-
|
|
955
|
+
// Route plan-CREATE through the unified spine (`/runtime-plan/request`):
|
|
956
|
+
// seeds the server-owned pending node/attention/state and authors the
|
|
957
|
+
// `plan_approval` card. Resolution is UNCHANGED — the gate still blocks
|
|
958
|
+
// on the server-authored `plan_approval_reply` below.
|
|
959
|
+
await client.createRuntimePlanRequest({
|
|
960
|
+
conversationId,
|
|
961
|
+
planId,
|
|
962
|
+
title: 'Plan ready for review.',
|
|
934
963
|
summary: 'Claude has prepared a plan and is asking permission to continue.',
|
|
935
964
|
...(planBody ? { body: planBody } : {}),
|
|
936
|
-
...(
|
|
965
|
+
...(cardResponseUserId ? { responseUserId: cardResponseUserId } : {}),
|
|
937
966
|
...(allowedPrompts.length > 0 ? { allowedPrompts } : {}),
|
|
938
|
-
|
|
939
|
-
await sendMessageWithRetry(client, conversationId, planApproval.text, {
|
|
940
|
-
messageId: `claude-plan-${planId}`,
|
|
941
|
-
...(session.activeSelfContextId
|
|
942
|
-
? { selfContextId: session.activeSelfContextId }
|
|
943
|
-
: {}),
|
|
944
|
-
metadata: {
|
|
945
|
-
...planApproval.metadata,
|
|
946
|
-
turnSemantics: 'control',
|
|
947
|
-
replyBehavior: 'suppress_auto_reply',
|
|
948
|
-
},
|
|
967
|
+
...(session.currentTurnId ? { turnId: session.currentTurnId } : {}),
|
|
949
968
|
});
|
|
950
969
|
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Plan approval sent (${planId.slice(0, 8)})`);
|
|
951
970
|
const reply = await waitForReply('plan_approval_reply', planId);
|
|
@@ -988,6 +1007,9 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
988
1007
|
if (session.state.permissionMode === 'plan') {
|
|
989
1008
|
session.state.permissionMode = 'default';
|
|
990
1009
|
writeState();
|
|
1010
|
+
// Converge the app immediately on the SDK-driven plan-mode exit
|
|
1011
|
+
// instead of waiting for the next 30s heartbeat (lag fix).
|
|
1012
|
+
config.publishSessionSnapshots?.([conversationId]);
|
|
991
1013
|
}
|
|
992
1014
|
return { behavior: 'allow' };
|
|
993
1015
|
}
|
|
@@ -1056,6 +1078,7 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1056
1078
|
model: undefined,
|
|
1057
1079
|
permissionMode: config.permissionMode,
|
|
1058
1080
|
effort: undefined,
|
|
1081
|
+
ultracode: undefined,
|
|
1059
1082
|
state: 'idle',
|
|
1060
1083
|
},
|
|
1061
1084
|
availableModels: config.availableModels,
|
|
@@ -1324,10 +1347,17 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1324
1347
|
else if (config.effort !== undefined) {
|
|
1325
1348
|
controlState.effort = { value: config.effort, source: 'requested' };
|
|
1326
1349
|
}
|
|
1350
|
+
if (session.state.ultracode !== undefined) {
|
|
1351
|
+
controlState.ultracode = { value: session.state.ultracode, source: 'applied', appliedAt };
|
|
1352
|
+
}
|
|
1353
|
+
else if (config.ultracode !== undefined) {
|
|
1354
|
+
controlState.ultracode = { value: config.ultracode, source: 'requested' };
|
|
1355
|
+
}
|
|
1327
1356
|
runtimeState.writeSessionState(conversationId, {
|
|
1328
1357
|
model: session.state.model,
|
|
1329
1358
|
permissionMode: session.state.permissionMode,
|
|
1330
1359
|
effort: session.state.effort,
|
|
1360
|
+
...(session.state.ultracode ? { runtimeControlValues: { ultracode: session.state.ultracode } } : {}),
|
|
1331
1361
|
controlState,
|
|
1332
1362
|
runtimeControlErrors,
|
|
1333
1363
|
contextUsage: session.state.contextUsage,
|
|
@@ -1666,6 +1696,15 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1666
1696
|
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Failed to set initial effort:`, err);
|
|
1667
1697
|
}
|
|
1668
1698
|
}
|
|
1699
|
+
if (config.ultracode) {
|
|
1700
|
+
try {
|
|
1701
|
+
await applyClaudeUltracode(q, config.ultracode === 'on');
|
|
1702
|
+
session.state.ultracode = config.ultracode;
|
|
1703
|
+
}
|
|
1704
|
+
catch (err) {
|
|
1705
|
+
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Failed to set initial ultracode:`, err);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1669
1708
|
if (!resumeSessionId) {
|
|
1670
1709
|
// New session — send seed message to activate streaming input mode
|
|
1671
1710
|
sendInput(createClaudeInputEnvelope({
|
|
@@ -1848,31 +1887,15 @@ export async function main() {
|
|
|
1848
1887
|
const promptedGroupContextConversationIds = new Set();
|
|
1849
1888
|
const pendingMembershipChanges = new Map();
|
|
1850
1889
|
let lastKnownConversationRefreshAt = 0;
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
});
|
|
1858
|
-
if (!streamConnected)
|
|
1890
|
+
// Publish the app-subscribed /agent-session snapshot for the given
|
|
1891
|
+
// conversations. Extracted from the heartbeat so control handlers can converge
|
|
1892
|
+
// the app immediately instead of waiting for the next 30s heartbeat
|
|
1893
|
+
// (session-control lag fix). Hoisted so it is reachable from earlier code.
|
|
1894
|
+
async function publishSessionSnapshots(conversationIds) {
|
|
1895
|
+
if (!streamConnected || conversationIds.length === 0)
|
|
1859
1896
|
return;
|
|
1860
|
-
await refreshKnownConversationIds().catch((error) => {
|
|
1861
|
-
console.error('[canon-host] Failed to refresh known conversations:', error);
|
|
1862
|
-
});
|
|
1863
|
-
const activeSessionQuery = sessions.values().next().value?.query ?? null;
|
|
1864
|
-
const metadata = await refreshClaudeRuntimeMetadata({
|
|
1865
|
-
cwd: workingDir,
|
|
1866
|
-
activeQuery: activeSessionQuery,
|
|
1867
|
-
}).catch((error) => {
|
|
1868
|
-
console.error('[canon-host] Failed to refresh Claude runtime metadata:', error);
|
|
1869
|
-
return runtimeMetadata;
|
|
1870
|
-
});
|
|
1871
|
-
await publishAgentRuntime(agentId, runtimeDescriptor).catch((error) => {
|
|
1872
|
-
console.error('[canon-host] Failed to publish agent runtime:', error);
|
|
1873
|
-
});
|
|
1874
1897
|
await publishHostSessionSnapshots({
|
|
1875
|
-
conversationIds
|
|
1898
|
+
conversationIds,
|
|
1876
1899
|
agentId,
|
|
1877
1900
|
clientType: 'claude-code',
|
|
1878
1901
|
runtime: runtimeDescriptor,
|
|
@@ -1887,6 +1910,7 @@ export async function main() {
|
|
|
1887
1910
|
...(session.state.model ? { model: session.state.model } : {}),
|
|
1888
1911
|
...(session.state.permissionMode ? { permissionMode: session.state.permissionMode } : {}),
|
|
1889
1912
|
...(session.state.effort ? { effort: session.state.effort } : {}),
|
|
1913
|
+
...(session.state.ultracode ? { runtimeControlValues: { ultracode: session.state.ultracode } } : {}),
|
|
1890
1914
|
...(workspaceId ? { workspaceId } : {}),
|
|
1891
1915
|
executionMode: session.environment.mode,
|
|
1892
1916
|
executionBranch: session.environment.branch ?? null,
|
|
@@ -1896,6 +1920,31 @@ export async function main() {
|
|
|
1896
1920
|
}).catch((error) => {
|
|
1897
1921
|
console.error('[canon-host] Failed to publish session snapshots:', error);
|
|
1898
1922
|
});
|
|
1923
|
+
}
|
|
1924
|
+
const publishRuntimeHeartbeat = async () => {
|
|
1925
|
+
heartbeatLocalRuntimeEntry(runtimeId, {
|
|
1926
|
+
agentId,
|
|
1927
|
+
agentName: profileAgentName,
|
|
1928
|
+
cwd: workingDir,
|
|
1929
|
+
baseCwd: workingDir,
|
|
1930
|
+
});
|
|
1931
|
+
if (!streamConnected)
|
|
1932
|
+
return;
|
|
1933
|
+
await refreshKnownConversationIds().catch((error) => {
|
|
1934
|
+
console.error('[canon-host] Failed to refresh known conversations:', error);
|
|
1935
|
+
});
|
|
1936
|
+
const activeSessionQuery = sessions.values().next().value?.query ?? null;
|
|
1937
|
+
const metadata = await refreshClaudeRuntimeMetadata({
|
|
1938
|
+
cwd: workingDir,
|
|
1939
|
+
activeQuery: activeSessionQuery,
|
|
1940
|
+
}).catch((error) => {
|
|
1941
|
+
console.error('[canon-host] Failed to refresh Claude runtime metadata:', error);
|
|
1942
|
+
return runtimeMetadata;
|
|
1943
|
+
});
|
|
1944
|
+
await publishAgentRuntime(agentId, runtimeDescriptor).catch((error) => {
|
|
1945
|
+
console.error('[canon-host] Failed to publish agent runtime:', error);
|
|
1946
|
+
});
|
|
1947
|
+
await publishSessionSnapshots(Array.from(knownConversationIds));
|
|
1899
1948
|
// Rebuild so the descriptor picks up native slash commands from the
|
|
1900
1949
|
// metadata refresh above — the initial descriptor is built before the
|
|
1901
1950
|
// command inventory has loaded.
|
|
@@ -1992,6 +2041,10 @@ export async function main() {
|
|
|
1992
2041
|
// ── Session manager ──
|
|
1993
2042
|
const sessions = new Map();
|
|
1994
2043
|
const approvalManager = ownerId ? new ApprovalManager(client, agentId, ownerId) : null;
|
|
2044
|
+
// Interactive runtime input (AskUserQuestion) works with or without an owner,
|
|
2045
|
+
// so its poll engine is always present. Create + poll fold onto the built-in
|
|
2046
|
+
// input descriptor via `request()` (see the AskUserQuestion canUseTool branch).
|
|
2047
|
+
const runtimeInputManager = new RuntimeRequestManager(client, { agentId, ownerId: ownerId ?? '' });
|
|
1995
2048
|
const pendingSessionCreations = new Map();
|
|
1996
2049
|
// Shared /control channel poller (claude host profile): session + signal
|
|
1997
2050
|
// keys, sequential conversations, active/idle cadence + jitter sampled at
|
|
@@ -2293,13 +2346,16 @@ export async function main() {
|
|
|
2293
2346
|
model: config?.model ?? runtimeModels[0]?.value,
|
|
2294
2347
|
permissionMode: config?.permissionMode ?? 'default',
|
|
2295
2348
|
effort: config?.effort,
|
|
2349
|
+
ultracode: config?.ultracode,
|
|
2296
2350
|
availableModels: runtimeModels,
|
|
2297
2351
|
availableExecutionModes: hostAvailableExecutionModes,
|
|
2298
2352
|
runtimePresentation,
|
|
2299
2353
|
approvalManager,
|
|
2354
|
+
runtimeInputManager,
|
|
2300
2355
|
canRequestApproval: () => canRequestCanonApproval(conversationId),
|
|
2301
2356
|
runtimeId,
|
|
2302
2357
|
ownerId,
|
|
2358
|
+
publishSessionSnapshots: (ids) => { void publishSessionSnapshots(ids); },
|
|
2303
2359
|
}, resumeId);
|
|
2304
2360
|
sessions.set(conversationId, session);
|
|
2305
2361
|
await controlPoller.baseline([conversationId]);
|
|
@@ -2609,51 +2665,26 @@ export async function main() {
|
|
|
2609
2665
|
return { consume: false };
|
|
2610
2666
|
const ctrl = event.control;
|
|
2611
2667
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Control:`, ctrl);
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
session.state.permissionMode = nextPermissionMode;
|
|
2633
|
-
session.clearRuntimeControlError('permissionMode');
|
|
2634
|
-
console.error(`[canon-host] [${convoId.slice(0, 8)}] Mode → ${nextPermissionMode}`);
|
|
2635
|
-
}
|
|
2636
|
-
catch (err) {
|
|
2637
|
-
session.setRuntimeControlError('permissionMode', nextPermissionMode, err);
|
|
2638
|
-
console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to set mode:`, err);
|
|
2639
|
-
}
|
|
2640
|
-
}
|
|
2641
|
-
const nextEffort = normalizeClaudeEffortLevel(ctrl.effort);
|
|
2642
|
-
if (nextEffort && nextEffort !== session.state.effort) {
|
|
2643
|
-
try {
|
|
2644
|
-
await applyClaudeEffortLevel(session.query, nextEffort);
|
|
2645
|
-
session.state.effort = nextEffort;
|
|
2646
|
-
session.clearRuntimeControlError('effort');
|
|
2647
|
-
console.error(`[canon-host] [${convoId.slice(0, 8)}] Effort → ${nextEffort}`);
|
|
2648
|
-
}
|
|
2649
|
-
catch (err) {
|
|
2650
|
-
session.setRuntimeControlError('effort', nextEffort, err);
|
|
2651
|
-
console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to set effort:`, err);
|
|
2652
|
-
}
|
|
2653
|
-
}
|
|
2654
|
-
else if (ctrl.effort && !nextEffort) {
|
|
2655
|
-
session.setRuntimeControlError('effort', String(ctrl.effort), new Error('Unsupported Claude thinking level.'));
|
|
2656
|
-
}
|
|
2668
|
+
await applyClaudeSessionControl({
|
|
2669
|
+
model: ctrl.model,
|
|
2670
|
+
permissionMode: ctrl.permissionMode,
|
|
2671
|
+
effort: ctrl.effort,
|
|
2672
|
+
ultracode: ctrl.runtimeControlValues?.ultracode,
|
|
2673
|
+
}, {
|
|
2674
|
+
state: session.state,
|
|
2675
|
+
parsePermissionMode: parseClaudePermissionMode,
|
|
2676
|
+
normalizeEffort: normalizeClaudeEffortLevel,
|
|
2677
|
+
parseUltracode: parseClaudeUltracodeMode,
|
|
2678
|
+
setModel: (model) => session.query.setModel(model),
|
|
2679
|
+
setPermissionMode: (mode) => session.query.setPermissionMode(mode),
|
|
2680
|
+
applyEffort: (level) => applyClaudeEffortLevel(session.query, level),
|
|
2681
|
+
applyUltracode: (enabled) => applyClaudeUltracode(session.query, enabled),
|
|
2682
|
+
setRuntimeControlError: (controlId, value, error) => session.setRuntimeControlError(controlId, value, error),
|
|
2683
|
+
clearRuntimeControlError: (controlId) => session.clearRuntimeControlError(controlId),
|
|
2684
|
+
// Publish the app-subscribed snapshot immediately so the control-pending
|
|
2685
|
+
// gate clears now instead of at the next 30s heartbeat (lag fix).
|
|
2686
|
+
publishSnapshot: () => { void publishSessionSnapshots([convoId]); },
|
|
2687
|
+
});
|
|
2657
2688
|
session.writeState();
|
|
2658
2689
|
}
|
|
2659
2690
|
// Interrupt/stop-clear signal. Throwing leaves the node in place
|
|
@@ -2731,6 +2762,7 @@ export async function main() {
|
|
|
2731
2762
|
clearInterval(idleCheck);
|
|
2732
2763
|
stream.stop();
|
|
2733
2764
|
approvalManager?.dispose();
|
|
2765
|
+
runtimeInputManager.dispose();
|
|
2734
2766
|
await runtimeState.clearAgentRuntime().catch(() => { });
|
|
2735
2767
|
for (const convoId of [...sessions.keys()]) {
|
|
2736
2768
|
closeSession(convoId);
|
package/dist/session-state.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
|
|
1
|
+
import type { PermissionMode, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
|
|
2
2
|
import type { DeliveryIntent, ModelOption, TurnLifecycleState } from '@canonmsg/core';
|
|
3
3
|
import type { TurnArtifactSnapshot } from '@canonmsg/coding-agent-host';
|
|
4
4
|
export type ClaudeInputKind = 'seed' | 'canon';
|
|
@@ -66,3 +66,44 @@ export declare function mergeClaudeModelOptions(input: {
|
|
|
66
66
|
selectedModel?: string | null;
|
|
67
67
|
}): ModelOption[];
|
|
68
68
|
export declare function formatClaudeControlError(error: unknown): string;
|
|
69
|
+
/** Requested session-control values from the control plane (raw, pre-validation). */
|
|
70
|
+
export interface ClaudeSessionControlInput {
|
|
71
|
+
model?: string;
|
|
72
|
+
permissionMode?: string;
|
|
73
|
+
effort?: string;
|
|
74
|
+
ultracode?: string;
|
|
75
|
+
}
|
|
76
|
+
/** Mutable mirror of the live session's control state — mutated in place on success. */
|
|
77
|
+
export interface ClaudeSessionControlState {
|
|
78
|
+
model?: string;
|
|
79
|
+
permissionMode?: string;
|
|
80
|
+
effort?: string;
|
|
81
|
+
ultracode?: string;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Injected validators + side effects so the apply logic stays host-agnostic and
|
|
85
|
+
* unit-testable. The host wires these to the live SDK query and its runtime
|
|
86
|
+
* control-error bookkeeping; tests inject fakes.
|
|
87
|
+
*/
|
|
88
|
+
export interface ClaudeSessionControlDeps {
|
|
89
|
+
state: ClaudeSessionControlState;
|
|
90
|
+
parsePermissionMode: (value: unknown) => PermissionMode | null;
|
|
91
|
+
normalizeEffort: (value: unknown) => string | undefined;
|
|
92
|
+
parseUltracode: (value: unknown) => string | undefined;
|
|
93
|
+
setModel: (model: string) => Promise<void>;
|
|
94
|
+
setPermissionMode: (mode: PermissionMode) => Promise<void>;
|
|
95
|
+
applyEffort: (level: string) => Promise<void>;
|
|
96
|
+
applyUltracode: (enabled: boolean) => Promise<void>;
|
|
97
|
+
setRuntimeControlError: (controlId: string, value: string, error: unknown) => void;
|
|
98
|
+
clearRuntimeControlError: (controlId: string) => void;
|
|
99
|
+
publishSnapshot: () => void;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Apply a session-control request to the live Claude session. Mirrors the host's
|
|
103
|
+
* control semantics (validation, only-if-different checks, error recording,
|
|
104
|
+
* clear-on-success). Whenever a live op is invoked — on success OR caught
|
|
105
|
+
* failure — publishSnapshot() is called exactly once so the app-subscribed
|
|
106
|
+
* /agent-session snapshot converges immediately instead of waiting for the next
|
|
107
|
+
* heartbeat. If nothing changed, publishSnapshot() is not called.
|
|
108
|
+
*/
|
|
109
|
+
export declare function applyClaudeSessionControl(control: ClaudeSessionControlInput, deps: ClaudeSessionControlDeps): Promise<void>;
|
package/dist/session-state.js
CHANGED
|
@@ -82,3 +82,74 @@ export function formatClaudeControlError(error) {
|
|
|
82
82
|
}
|
|
83
83
|
return 'Claude Code did not apply that control.';
|
|
84
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Apply a session-control request to the live Claude session. Mirrors the host's
|
|
87
|
+
* control semantics (validation, only-if-different checks, error recording,
|
|
88
|
+
* clear-on-success). Whenever a live op is invoked — on success OR caught
|
|
89
|
+
* failure — publishSnapshot() is called exactly once so the app-subscribed
|
|
90
|
+
* /agent-session snapshot converges immediately instead of waiting for the next
|
|
91
|
+
* heartbeat. If nothing changed, publishSnapshot() is not called.
|
|
92
|
+
*/
|
|
93
|
+
export async function applyClaudeSessionControl(control, deps) {
|
|
94
|
+
const { state } = deps;
|
|
95
|
+
let liveOpInvoked = false;
|
|
96
|
+
if (control.model && control.model !== state.model) {
|
|
97
|
+
liveOpInvoked = true;
|
|
98
|
+
try {
|
|
99
|
+
await deps.setModel(control.model);
|
|
100
|
+
state.model = control.model;
|
|
101
|
+
deps.clearRuntimeControlError('model');
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
deps.setRuntimeControlError('model', control.model, err);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const nextPermissionMode = deps.parsePermissionMode(control.permissionMode);
|
|
108
|
+
if (control.permissionMode && !nextPermissionMode) {
|
|
109
|
+
deps.setRuntimeControlError('permissionMode', control.permissionMode, new Error(`Unsupported Claude permission mode: ${control.permissionMode}`));
|
|
110
|
+
}
|
|
111
|
+
else if (nextPermissionMode && nextPermissionMode !== state.permissionMode) {
|
|
112
|
+
liveOpInvoked = true;
|
|
113
|
+
try {
|
|
114
|
+
await deps.setPermissionMode(nextPermissionMode);
|
|
115
|
+
state.permissionMode = nextPermissionMode;
|
|
116
|
+
deps.clearRuntimeControlError('permissionMode');
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
deps.setRuntimeControlError('permissionMode', nextPermissionMode, err);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const nextEffort = deps.normalizeEffort(control.effort);
|
|
123
|
+
if (nextEffort && nextEffort !== state.effort) {
|
|
124
|
+
liveOpInvoked = true;
|
|
125
|
+
try {
|
|
126
|
+
await deps.applyEffort(nextEffort);
|
|
127
|
+
state.effort = nextEffort;
|
|
128
|
+
deps.clearRuntimeControlError('effort');
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
deps.setRuntimeControlError('effort', nextEffort, err);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else if (control.effort && !nextEffort) {
|
|
135
|
+
deps.setRuntimeControlError('effort', String(control.effort), new Error('Unsupported Claude thinking level.'));
|
|
136
|
+
}
|
|
137
|
+
const nextUltracode = deps.parseUltracode(control.ultracode);
|
|
138
|
+
if (control.ultracode && !nextUltracode) {
|
|
139
|
+
deps.setRuntimeControlError('ultracode', String(control.ultracode), new Error('Unsupported Claude ultracode mode.'));
|
|
140
|
+
}
|
|
141
|
+
else if (nextUltracode && nextUltracode !== state.ultracode) {
|
|
142
|
+
liveOpInvoked = true;
|
|
143
|
+
try {
|
|
144
|
+
await deps.applyUltracode(nextUltracode === 'on');
|
|
145
|
+
state.ultracode = nextUltracode;
|
|
146
|
+
deps.clearRuntimeControlError('ultracode');
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
deps.setRuntimeControlError('ultracode', nextUltracode, err);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (liveOpInvoked) {
|
|
153
|
+
deps.publishSnapshot();
|
|
154
|
+
}
|
|
155
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/claude-code-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.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",
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
"test": "vitest run"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@anthropic-ai/claude-agent-sdk": "^0.
|
|
34
|
-
"@canonmsg/agent-sdk": "^5.
|
|
33
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.200",
|
|
34
|
+
"@canonmsg/agent-sdk": "^5.1.0",
|
|
35
35
|
"@canonmsg/coding-agent-host": "^0.2.2",
|
|
36
|
-
"@canonmsg/core": "^4.
|
|
36
|
+
"@canonmsg/core": "^4.2.0",
|
|
37
37
|
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|