@canonmsg/claude-code-plugin 0.12.3 → 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 +55 -10
- package/package.json +2 -2
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
|
};
|
|
@@ -1322,6 +1332,10 @@ export async function main() {
|
|
|
1322
1332
|
for (const conversation of convos) {
|
|
1323
1333
|
knownConversationIds.add(conversation.id);
|
|
1324
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(() => { });
|
|
1325
1339
|
}
|
|
1326
1340
|
console.error(`[canon-host] ${convos.length} conversations`);
|
|
1327
1341
|
}
|
|
@@ -1331,6 +1345,9 @@ export async function main() {
|
|
|
1331
1345
|
// ── Session manager ──
|
|
1332
1346
|
const sessions = new Map();
|
|
1333
1347
|
const pendingSessionCreations = new Map();
|
|
1348
|
+
let controlStopped = false;
|
|
1349
|
+
const lastSeenControl = new Map();
|
|
1350
|
+
const lastSeenSignal = new Map();
|
|
1334
1351
|
/** Saved SDK session IDs for resume after idle eviction, keyed by conversation + workspace */
|
|
1335
1352
|
const savedSessionIds = new Map();
|
|
1336
1353
|
const { getConversationMeta } = createConversationMetadataLoader({
|
|
@@ -1408,6 +1425,13 @@ export async function main() {
|
|
|
1408
1425
|
sessions.delete(conversationId);
|
|
1409
1426
|
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Session suspended (sessionId saved for resume)`);
|
|
1410
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
|
+
}
|
|
1411
1435
|
function evictOldestIdle() {
|
|
1412
1436
|
let oldest = null;
|
|
1413
1437
|
for (const s of sessions.values()) {
|
|
@@ -1489,6 +1513,7 @@ export async function main() {
|
|
|
1489
1513
|
availableExecutionModes: hostAvailableExecutionModes,
|
|
1490
1514
|
}, resumeId);
|
|
1491
1515
|
sessions.set(conversationId, session);
|
|
1516
|
+
await baselineControlSignal(conversationId);
|
|
1492
1517
|
return session;
|
|
1493
1518
|
}
|
|
1494
1519
|
catch (error) {
|
|
@@ -1504,6 +1529,17 @@ export async function main() {
|
|
|
1504
1529
|
pendingSessionCreations.delete(conversationId);
|
|
1505
1530
|
}
|
|
1506
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
|
+
}
|
|
1507
1543
|
async function enqueueRecoveredInboundMessage(input) {
|
|
1508
1544
|
const m = input.message;
|
|
1509
1545
|
if (m.senderId === agentId)
|
|
@@ -1747,9 +1783,6 @@ export async function main() {
|
|
|
1747
1783
|
console.error('[canon-host] SSE start error:', err instanceof Error ? err.message : err);
|
|
1748
1784
|
});
|
|
1749
1785
|
// ── Control signal watcher ──
|
|
1750
|
-
let controlStopped = false;
|
|
1751
|
-
const lastSeenControl = new Map();
|
|
1752
|
-
const lastSeenSignal = new Map();
|
|
1753
1786
|
const pollControl = async () => {
|
|
1754
1787
|
while (!controlStopped) {
|
|
1755
1788
|
for (const convoId of [...sessions.keys()]) {
|
|
@@ -1813,17 +1846,26 @@ export async function main() {
|
|
|
1813
1846
|
}
|
|
1814
1847
|
}
|
|
1815
1848
|
}
|
|
1816
|
-
// Interrupt signal
|
|
1849
|
+
// Interrupt/stop-clear signal
|
|
1817
1850
|
const sig = await rtdbRead(`/control/${convoId}/${agentId}/signal`);
|
|
1818
1851
|
if (sig && typeof sig === 'object') {
|
|
1819
1852
|
const s = sig;
|
|
1820
1853
|
const ts = s.updatedAt ?? 0;
|
|
1821
|
-
if (s.type === 'interrupt' && ts > (lastSeenSignal.get(convoId) ?? 0)) {
|
|
1854
|
+
if ((s.type === 'interrupt' || s.type === 'stop_and_drop') && ts > (lastSeenSignal.get(convoId) ?? 0)) {
|
|
1822
1855
|
lastSeenSignal.set(convoId, ts);
|
|
1823
1856
|
const session = sessions.get(convoId);
|
|
1824
1857
|
if (session && !session.closed) {
|
|
1825
|
-
|
|
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`);
|
|
1826
1864
|
try {
|
|
1865
|
+
if (s.type === 'stop_and_drop') {
|
|
1866
|
+
const droppedInputs = session.pendingInputs.splice(0);
|
|
1867
|
+
await markSessionPendingInputsRejected(convoId, droppedInputs);
|
|
1868
|
+
}
|
|
1827
1869
|
session.turnState = 'interrupted';
|
|
1828
1870
|
runtimeState.writeTurnState(convoId, {
|
|
1829
1871
|
turnId: session.currentTurnId,
|
|
@@ -1835,7 +1877,9 @@ export async function main() {
|
|
|
1835
1877
|
...(session.currentTurnOpenedAt ? { openedAt: session.currentTurnOpenedAt } : {}),
|
|
1836
1878
|
completedAt: { '.sv': 'timestamp' },
|
|
1837
1879
|
}).catch(() => { });
|
|
1838
|
-
|
|
1880
|
+
if (isActive) {
|
|
1881
|
+
await session.query.interrupt();
|
|
1882
|
+
}
|
|
1839
1883
|
if (session.streamingTimer) {
|
|
1840
1884
|
clearTimeout(session.streamingTimer);
|
|
1841
1885
|
session.streamingTimer = null;
|
|
@@ -1843,9 +1887,10 @@ export async function main() {
|
|
|
1843
1887
|
session.streamingText = '';
|
|
1844
1888
|
runtimeState.clearStreaming(convoId).catch(() => { });
|
|
1845
1889
|
client.setTyping(convoId, false).catch(() => { });
|
|
1890
|
+
await rtdbWrite(`/control/${convoId}/${agentId}/signal`, null).catch(() => { });
|
|
1846
1891
|
}
|
|
1847
1892
|
catch (err) {
|
|
1848
|
-
console.error(`[canon-host] [${convoId.slice(0, 8)}]
|
|
1893
|
+
console.error(`[canon-host] [${convoId.slice(0, 8)}] ${s.type} failed:`, err);
|
|
1849
1894
|
}
|
|
1850
1895
|
}
|
|
1851
1896
|
}
|
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,7 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@anthropic-ai/claude-agent-sdk": "^0.2.101",
|
|
34
|
-
"@canonmsg/agent-sdk": "^1.
|
|
34
|
+
"@canonmsg/agent-sdk": "^1.1.0",
|
|
35
35
|
"@canonmsg/core": "^0.15.0",
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.12.0"
|
|
37
37
|
},
|