@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.
Files changed (2) hide show
  1. package/dist/host.js +76 -24
  2. 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: true,
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
- ...session.state,
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
- // Write available models to session state
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
- ...session.state,
1077
- availableModels: modelList,
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: runtimeDescriptor.runtimeDescriptor ?? buildClaudeRuntimeDescriptor({
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
- ...session.state,
1797
- ...(session.availableModels.length ? { availableModels: session.availableModels } : {}),
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
- 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`);
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
- await session.query.interrupt();
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)}] Interrupt failed:`, err);
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
- ...session.state,
1862
- ...(session.availableModels.length ? { availableModels: session.availableModels } : {}),
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.2",
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": "^0.10.2",
35
- "@canonmsg/core": "^0.14.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": {