@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.
Files changed (2) hide show
  1. package/dist/host.js +55 -10
  2. 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: true,
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
- console.error(`[canon-host] [${convoId.slice(0, 8)}] Interrupt signal`);
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
- await session.query.interrupt();
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)}] Interrupt failed:`, err);
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",
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.0.0",
34
+ "@canonmsg/agent-sdk": "^1.1.0",
35
35
  "@canonmsg/core": "^0.15.0",
36
36
  "@modelcontextprotocol/sdk": "^1.12.0"
37
37
  },