@canonmsg/claude-code-plugin 0.12.2 → 0.12.4
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 +76 -24
- package/package.json +3 -3
package/dist/host.js
CHANGED
|
@@ -26,7 +26,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
26
26
|
import { existsSync } from 'node:fs';
|
|
27
27
|
import { query, } from '@anthropic-ai/claude-agent-sdk';
|
|
28
28
|
import { isAnthropicImageAttachment, materializeMessageMedia, toAnthropicImageBlock, } from '@canonmsg/agent-sdk';
|
|
29
|
-
import { CLAUDE_PERMISSION_MODE_OPTIONS, buildCanonHostPrompt, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, buildPlanApprovalRequest, buildQuestionRequest, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, createConversationMetadataLoader, createRuntimeStatePublisher, EXECUTION_ENVIRONMENT_MODES, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, ExecutionEnvironmentError, FINAL_MESSAGE_HANDOFF_MS, buildLocalRuntimeId, getActiveProfileLock, heartbeatLocalRuntimeEntry, normalizeTurnMetadata, prepareConversationEnvironment, resolveCanonAgent, initRTDBAuth, rtdbRead, loadHostSessionConfig, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, saveRuntimeSessionState, clearRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
|
|
29
|
+
import { CLAUDE_PERMISSION_MODE_OPTIONS, buildCanonHostPrompt, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, buildPlanApprovalRequest, buildQuestionRequest, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, createConversationMetadataLoader, createRuntimeStatePublisher, EXECUTION_ENVIRONMENT_MODES, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, ExecutionEnvironmentError, FINAL_MESSAGE_HANDOFF_MS, buildLocalRuntimeId, getActiveProfileLock, heartbeatLocalRuntimeEntry, normalizeTurnMetadata, prepareConversationEnvironment, resolveCanonAgent, initRTDBAuth, rtdbRead, rtdbWrite, loadHostSessionConfig, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, saveRuntimeSessionState, clearRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
|
|
30
30
|
import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
|
|
31
31
|
import { createStreamingBuffer } from './streaming-buffer.js';
|
|
32
32
|
import { runCli } from './cli-entry.js';
|
|
@@ -142,6 +142,16 @@ function buildClaudeRuntimeDescriptor(input) {
|
|
|
142
142
|
availability: ['busy'],
|
|
143
143
|
dispatch: { kind: 'signal', signal: 'interrupt' },
|
|
144
144
|
},
|
|
145
|
+
{
|
|
146
|
+
id: 'stop-and-clear-queue',
|
|
147
|
+
label: 'Stop & clear queue',
|
|
148
|
+
description: 'Interrupt the current Claude turn and drop queued Canon messages.',
|
|
149
|
+
aliases: ['stop-clear', 'clear-queue'],
|
|
150
|
+
category: 'turn',
|
|
151
|
+
placements: ['composer_slash', 'command_palette', 'session_strip'],
|
|
152
|
+
availability: ['busy_with_queue'],
|
|
153
|
+
dispatch: { kind: 'signal', signal: 'stop_and_drop' },
|
|
154
|
+
},
|
|
145
155
|
],
|
|
146
156
|
});
|
|
147
157
|
}
|
|
@@ -450,7 +460,7 @@ const CLAUDE_RUNTIME_CAPABILITIES = {
|
|
|
450
460
|
...DEFAULT_RUNTIME_CAPABILITIES,
|
|
451
461
|
supportsInterrupt: true,
|
|
452
462
|
supportsQueue: true,
|
|
453
|
-
supportsInterleave:
|
|
463
|
+
supportsInterleave: false,
|
|
454
464
|
supportsRequiresAction: true,
|
|
455
465
|
supportsNonFinalPermanentMessages: false,
|
|
456
466
|
};
|
|
@@ -797,7 +807,10 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
797
807
|
}
|
|
798
808
|
function writeState() {
|
|
799
809
|
runtimeState.writeSessionState(conversationId, {
|
|
800
|
-
|
|
810
|
+
model: session.state.model,
|
|
811
|
+
permissionMode: session.state.permissionMode,
|
|
812
|
+
effort: session.state.effort,
|
|
813
|
+
contextUsage: session.state.contextUsage,
|
|
801
814
|
...(session.availableModels.length ? { availableModels: session.availableModels } : {}),
|
|
802
815
|
cwd,
|
|
803
816
|
executionMode: session.environment.mode,
|
|
@@ -1052,7 +1065,7 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
1052
1065
|
parent_tool_use_id: null,
|
|
1053
1066
|
});
|
|
1054
1067
|
}
|
|
1055
|
-
//
|
|
1068
|
+
// Refresh the canonical runtime descriptor after Claude reports supported models.
|
|
1056
1069
|
try {
|
|
1057
1070
|
const models = await q.supportedModels();
|
|
1058
1071
|
const modelList = toModelOptions(models);
|
|
@@ -1073,8 +1086,10 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
1073
1086
|
}),
|
|
1074
1087
|
});
|
|
1075
1088
|
runtimeState.writeSessionState(conversationId, {
|
|
1076
|
-
|
|
1077
|
-
|
|
1089
|
+
model: session.state.model,
|
|
1090
|
+
permissionMode: session.state.permissionMode,
|
|
1091
|
+
effort: session.state.effort,
|
|
1092
|
+
contextUsage: session.state.contextUsage,
|
|
1078
1093
|
cwd,
|
|
1079
1094
|
executionMode: session.environment.mode,
|
|
1080
1095
|
...(session.environment.branch ? { executionBranch: session.environment.branch } : {}),
|
|
@@ -1257,13 +1272,11 @@ export async function main() {
|
|
|
1257
1272
|
? resolveWorkspaceIdForBaseCwd(session.environment.baseCwd)
|
|
1258
1273
|
: runtimeDescriptor.defaultWorkspaceId;
|
|
1259
1274
|
const workspace = workspaceOptions.find((option) => option.id === workspaceId) ?? null;
|
|
1275
|
+
const descriptor = runtimeDescriptor.runtimeDescriptor;
|
|
1276
|
+
if (!descriptor)
|
|
1277
|
+
return;
|
|
1260
1278
|
const payload = buildClaudeRuntimeInfo({
|
|
1261
|
-
descriptor
|
|
1262
|
-
models: runtimeModels,
|
|
1263
|
-
workspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
1264
|
-
workspaceRoots: workspaceRootMetadata,
|
|
1265
|
-
executionModes: hostAvailableExecutionModes,
|
|
1266
|
-
}),
|
|
1279
|
+
descriptor,
|
|
1267
1280
|
metadata,
|
|
1268
1281
|
workspaceLabel: workspace?.label ?? workspaceId ?? null,
|
|
1269
1282
|
resolvedCwd: session?.cwd ?? workspace?.cwd ?? workingDir,
|
|
@@ -1319,6 +1332,10 @@ export async function main() {
|
|
|
1319
1332
|
for (const conversation of convos) {
|
|
1320
1333
|
knownConversationIds.add(conversation.id);
|
|
1321
1334
|
conversationCache.set(conversation.id, conversation);
|
|
1335
|
+
runtimeState.clearStreaming(conversation.id).catch(() => { });
|
|
1336
|
+
runtimeState.clearSessionState(conversation.id).catch(() => { });
|
|
1337
|
+
runtimeState.clearTurnState(conversation.id).catch(() => { });
|
|
1338
|
+
client.setTyping(conversation.id, false).catch(() => { });
|
|
1322
1339
|
}
|
|
1323
1340
|
console.error(`[canon-host] ${convos.length} conversations`);
|
|
1324
1341
|
}
|
|
@@ -1328,6 +1345,9 @@ export async function main() {
|
|
|
1328
1345
|
// ── Session manager ──
|
|
1329
1346
|
const sessions = new Map();
|
|
1330
1347
|
const pendingSessionCreations = new Map();
|
|
1348
|
+
let controlStopped = false;
|
|
1349
|
+
const lastSeenControl = new Map();
|
|
1350
|
+
const lastSeenSignal = new Map();
|
|
1331
1351
|
/** Saved SDK session IDs for resume after idle eviction, keyed by conversation + workspace */
|
|
1332
1352
|
const savedSessionIds = new Map();
|
|
1333
1353
|
const { getConversationMeta } = createConversationMetadataLoader({
|
|
@@ -1405,6 +1425,13 @@ export async function main() {
|
|
|
1405
1425
|
sessions.delete(conversationId);
|
|
1406
1426
|
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Session suspended (sessionId saved for resume)`);
|
|
1407
1427
|
}
|
|
1428
|
+
async function markSessionPendingInputsRejected(conversationId, inputs) {
|
|
1429
|
+
await Promise.all(inputs.map((input) => {
|
|
1430
|
+
if (!input.markAccepted || !input.sourceMessageId)
|
|
1431
|
+
return Promise.resolve();
|
|
1432
|
+
return client.updateMessageDisposition(conversationId, input.sourceMessageId, 'rejected').catch(() => { });
|
|
1433
|
+
}));
|
|
1434
|
+
}
|
|
1408
1435
|
function evictOldestIdle() {
|
|
1409
1436
|
let oldest = null;
|
|
1410
1437
|
for (const s of sessions.values()) {
|
|
@@ -1486,6 +1513,7 @@ export async function main() {
|
|
|
1486
1513
|
availableExecutionModes: hostAvailableExecutionModes,
|
|
1487
1514
|
}, resumeId);
|
|
1488
1515
|
sessions.set(conversationId, session);
|
|
1516
|
+
await baselineControlSignal(conversationId);
|
|
1489
1517
|
return session;
|
|
1490
1518
|
}
|
|
1491
1519
|
catch (error) {
|
|
@@ -1501,6 +1529,17 @@ export async function main() {
|
|
|
1501
1529
|
pendingSessionCreations.delete(conversationId);
|
|
1502
1530
|
}
|
|
1503
1531
|
}
|
|
1532
|
+
async function baselineControlSignal(conversationId) {
|
|
1533
|
+
if (lastSeenSignal.has(conversationId))
|
|
1534
|
+
return;
|
|
1535
|
+
const raw = await rtdbRead(`/control/${conversationId}/${agentId}/signal`).catch(() => null);
|
|
1536
|
+
if (!raw || typeof raw !== 'object')
|
|
1537
|
+
return;
|
|
1538
|
+
const timestamp = Number(raw.updatedAt ?? 0);
|
|
1539
|
+
if (timestamp > 0) {
|
|
1540
|
+
lastSeenSignal.set(conversationId, timestamp);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1504
1543
|
async function enqueueRecoveredInboundMessage(input) {
|
|
1505
1544
|
const m = input.message;
|
|
1506
1545
|
if (m.senderId === agentId)
|
|
@@ -1744,9 +1783,6 @@ export async function main() {
|
|
|
1744
1783
|
console.error('[canon-host] SSE start error:', err instanceof Error ? err.message : err);
|
|
1745
1784
|
});
|
|
1746
1785
|
// ── Control signal watcher ──
|
|
1747
|
-
let controlStopped = false;
|
|
1748
|
-
const lastSeenControl = new Map();
|
|
1749
|
-
const lastSeenSignal = new Map();
|
|
1750
1786
|
const pollControl = async () => {
|
|
1751
1787
|
while (!controlStopped) {
|
|
1752
1788
|
for (const convoId of [...sessions.keys()]) {
|
|
@@ -1793,8 +1829,10 @@ export async function main() {
|
|
|
1793
1829
|
}
|
|
1794
1830
|
}
|
|
1795
1831
|
runtimeState.writeSessionState(convoId, {
|
|
1796
|
-
|
|
1797
|
-
|
|
1832
|
+
model: session.state.model,
|
|
1833
|
+
permissionMode: session.state.permissionMode,
|
|
1834
|
+
effort: session.state.effort,
|
|
1835
|
+
contextUsage: session.state.contextUsage,
|
|
1798
1836
|
cwd: session.cwd,
|
|
1799
1837
|
executionMode: session.environment.mode,
|
|
1800
1838
|
...(session.environment.branch ? { executionBranch: session.environment.branch } : {}),
|
|
@@ -1808,17 +1846,26 @@ export async function main() {
|
|
|
1808
1846
|
}
|
|
1809
1847
|
}
|
|
1810
1848
|
}
|
|
1811
|
-
// Interrupt signal
|
|
1849
|
+
// Interrupt/stop-clear signal
|
|
1812
1850
|
const sig = await rtdbRead(`/control/${convoId}/${agentId}/signal`);
|
|
1813
1851
|
if (sig && typeof sig === 'object') {
|
|
1814
1852
|
const s = sig;
|
|
1815
1853
|
const ts = s.updatedAt ?? 0;
|
|
1816
|
-
if (s.type === 'interrupt' && ts > (lastSeenSignal.get(convoId) ?? 0)) {
|
|
1854
|
+
if ((s.type === 'interrupt' || s.type === 'stop_and_drop') && ts > (lastSeenSignal.get(convoId) ?? 0)) {
|
|
1817
1855
|
lastSeenSignal.set(convoId, ts);
|
|
1818
1856
|
const session = sessions.get(convoId);
|
|
1819
1857
|
if (session && !session.closed) {
|
|
1820
|
-
|
|
1858
|
+
const isActive = session.state.state === 'running';
|
|
1859
|
+
if (!isActive && (s.type !== 'stop_and_drop' || session.pendingInputs.length === 0)) {
|
|
1860
|
+
await rtdbWrite(`/control/${convoId}/${agentId}/signal`, null).catch(() => { });
|
|
1861
|
+
continue;
|
|
1862
|
+
}
|
|
1863
|
+
console.error(`[canon-host] [${convoId.slice(0, 8)}] ${s.type} signal`);
|
|
1821
1864
|
try {
|
|
1865
|
+
if (s.type === 'stop_and_drop') {
|
|
1866
|
+
const droppedInputs = session.pendingInputs.splice(0);
|
|
1867
|
+
await markSessionPendingInputsRejected(convoId, droppedInputs);
|
|
1868
|
+
}
|
|
1822
1869
|
session.turnState = 'interrupted';
|
|
1823
1870
|
runtimeState.writeTurnState(convoId, {
|
|
1824
1871
|
turnId: session.currentTurnId,
|
|
@@ -1830,7 +1877,9 @@ export async function main() {
|
|
|
1830
1877
|
...(session.currentTurnOpenedAt ? { openedAt: session.currentTurnOpenedAt } : {}),
|
|
1831
1878
|
completedAt: { '.sv': 'timestamp' },
|
|
1832
1879
|
}).catch(() => { });
|
|
1833
|
-
|
|
1880
|
+
if (isActive) {
|
|
1881
|
+
await session.query.interrupt();
|
|
1882
|
+
}
|
|
1834
1883
|
if (session.streamingTimer) {
|
|
1835
1884
|
clearTimeout(session.streamingTimer);
|
|
1836
1885
|
session.streamingTimer = null;
|
|
@@ -1838,9 +1887,10 @@ export async function main() {
|
|
|
1838
1887
|
session.streamingText = '';
|
|
1839
1888
|
runtimeState.clearStreaming(convoId).catch(() => { });
|
|
1840
1889
|
client.setTyping(convoId, false).catch(() => { });
|
|
1890
|
+
await rtdbWrite(`/control/${convoId}/${agentId}/signal`, null).catch(() => { });
|
|
1841
1891
|
}
|
|
1842
1892
|
catch (err) {
|
|
1843
|
-
console.error(`[canon-host] [${convoId.slice(0, 8)}]
|
|
1893
|
+
console.error(`[canon-host] [${convoId.slice(0, 8)}] ${s.type} failed:`, err);
|
|
1844
1894
|
}
|
|
1845
1895
|
}
|
|
1846
1896
|
}
|
|
@@ -1858,8 +1908,10 @@ export async function main() {
|
|
|
1858
1908
|
const heartbeat = setInterval(() => {
|
|
1859
1909
|
for (const session of sessions.values()) {
|
|
1860
1910
|
runtimeState.writeSessionState(session.conversationId, {
|
|
1861
|
-
|
|
1862
|
-
|
|
1911
|
+
model: session.state.model,
|
|
1912
|
+
permissionMode: session.state.permissionMode,
|
|
1913
|
+
effort: session.state.effort,
|
|
1914
|
+
contextUsage: session.state.contextUsage,
|
|
1863
1915
|
cwd: session.cwd,
|
|
1864
1916
|
executionMode: session.environment.mode,
|
|
1865
1917
|
...(session.environment.branch ? { executionBranch: session.environment.branch } : {}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/claude-code-plugin",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.4",
|
|
4
4
|
"description": "Canon channel plugin for Claude Code — messaging where AI agents are first-class citizens",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@anthropic-ai/claude-agent-sdk": "^0.2.101",
|
|
34
|
-
"@canonmsg/agent-sdk": "^
|
|
35
|
-
"@canonmsg/core": "^0.
|
|
34
|
+
"@canonmsg/agent-sdk": "^1.1.0",
|
|
35
|
+
"@canonmsg/core": "^0.15.0",
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.12.0"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|