@canonmsg/claude-code-plugin 0.14.15 → 0.14.16

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 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, materializeReplyContextMedia, toAnthropicImageBlock, } from '@canonmsg/agent-sdk';
29
- import { CLAUDE_EFFORT_OPTIONS, CLAUDE_PERMISSION_MODE_OPTIONS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildCanonHostPrompt, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPlanApprovalRequest, buildQuestionRequest, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createTurnOutputController, createRuntimeStatePublisher, EXECUTION_ENVIRONMENT_MODES, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, ApprovalManager, 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';
29
+ import { CLAUDE_EFFORT_OPTIONS, CLAUDE_PERMISSION_MODE_OPTIONS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildCanonHostPrompt, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPlanApprovalRequest, buildQuestionRequest, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createTurnOutputController, createRuntimeStatePublisher, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, ApprovalManager, ExecutionEnvironmentError, FINAL_MESSAGE_HANDOFF_MS, buildLocalRuntimeId, getActiveProfileLock, heartbeatLocalRuntimeEntry, normalizeTurnMetadata, prepareConversationEnvironment, resolveCanonAgent, initRTDBAuth, rtdbRead, rtdbWrite, sendMessageWithRetry, 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 { runCli } from './cli-entry.js';
32
32
  import { decideClaudeToolPermissionForMode, parseAllowedNonOwnerClaudeTools, } from './tool-policy.js';
@@ -522,7 +522,10 @@ const STREAMING_THROTTLE_MS = 250;
522
522
  const HEARTBEAT_MS = 30_000;
523
523
  const IDLE_CHECK_MS = 60_000;
524
524
  const CONTROL_POLL_MS = 2000;
525
- const FINAL_DELIVERY_RETRY_MS = 2_000;
525
+ const IDLE_CONTROL_POLL_MS = 10_000;
526
+ const CONTROL_POLL_JITTER_MS = 1_000;
527
+ const FINAL_DELIVERY_RETRY_MS = 30_000;
528
+ const MAX_FINAL_DELIVERY_RETRIES = 1;
526
529
  const CLAUDE_RUNTIME_CAPABILITIES = {
527
530
  ...DEFAULT_RUNTIME_CAPABILITIES,
528
531
  supportsInterrupt: true,
@@ -532,6 +535,10 @@ const CLAUDE_RUNTIME_CAPABILITIES = {
532
535
  supportsRequiresAction: true,
533
536
  supportsNonFinalPermanentMessages: false,
534
537
  };
538
+ function controlPollDelayMs(hasActiveWork) {
539
+ const base = hasActiveWork ? CONTROL_POLL_MS : IDLE_CONTROL_POLL_MS;
540
+ return base + Math.floor(Math.random() * CONTROL_POLL_JITTER_MS);
541
+ }
535
542
  function buildCanonPrompt(input) {
536
543
  const { session, ...promptInput } = input;
537
544
  return buildCanonHostPrompt({
@@ -602,7 +609,7 @@ async function buildCanonUserContent(input) {
602
609
  ];
603
610
  }
604
611
  // ── Session factory ─────────────────────────────────────────────────
605
- function createSession(conversationId, environment, agentId, client, runtimeState, onSessionEnd, onRuntimeDescriptorUpdate, config, resumeSessionId) {
612
+ function createSession(conversationId, environment, agentId, client, typingSignals, runtimeState, onSessionEnd, onRuntimeDescriptorUpdate, config, resumeSessionId) {
606
613
  const { cwd } = environment;
607
614
  const cardResponseUserId = config.ownerId ?? undefined;
608
615
  const cardResponseUser = cardResponseUserId ? { responseUserId: cardResponseUserId } : undefined;
@@ -704,7 +711,8 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
704
711
  const questionId = randomUUID();
705
712
  const questions = input.questions ?? [];
706
713
  const questionCard = buildQuestionRequest(questionId, questions, cardResponseUser);
707
- await client.sendMessage(conversationId, questionCard.text, {
714
+ await sendMessageWithRetry(client, conversationId, questionCard.text, {
715
+ messageId: `claude-question-${questionId}`,
708
716
  ...(session.activeSelfContextId
709
717
  ? { selfContextId: session.activeSelfContextId }
710
718
  : {}),
@@ -754,7 +762,8 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
754
762
  summary: 'Claude has prepared a plan and is asking permission to continue.',
755
763
  ...(cardResponseUser ?? {}),
756
764
  });
757
- await client.sendMessage(conversationId, planApproval.text, {
765
+ await sendMessageWithRetry(client, conversationId, planApproval.text, {
766
+ messageId: `claude-plan-${planId}`,
758
767
  ...(session.activeSelfContextId
759
768
  ? { selfContextId: session.activeSelfContextId }
760
769
  : {}),
@@ -912,17 +921,21 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
912
921
  clearIdleResetTimer();
913
922
  session.idleResetTimer = setTimeout(() => {
914
923
  session.idleResetTimer = null;
915
- client.setTyping(conversationId, false).catch(() => { });
924
+ typingSignals.clear(conversationId).catch(() => { });
916
925
  clearStreaming();
917
926
  resetTurnToIdle();
918
927
  }, FINAL_MESSAGE_HANDOFF_MS);
919
928
  }
920
929
  function markPendingFinalDelivery(finalText, turn) {
921
930
  clearIdleResetTimer();
931
+ const retryCount = session.pendingFinalDelivery?.turnKey === turn.turnKey
932
+ ? session.pendingFinalDelivery.retryCount
933
+ : 0;
922
934
  session.pendingFinalText = finalText;
923
935
  session.pendingFinalDelivery = {
924
936
  turnKey: turn.turnKey,
925
937
  text: finalText,
938
+ retryCount,
926
939
  messageId: buildClaudeFinalMessageId({
927
940
  agentId,
928
941
  conversationId,
@@ -935,7 +948,7 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
935
948
  streamingOutput.replaceSnapshot(finalText, 'streaming').catch(() => { });
936
949
  writeState();
937
950
  writeTurn();
938
- client.setTyping(conversationId, true, 'typing').catch(() => { });
951
+ typingSignals.start(conversationId, 'typing').catch(() => { });
939
952
  }
940
953
  async function deliverFinalReply(finalText, turn = session.activeInput) {
941
954
  if (!shouldDeliverClaudeFinal({
@@ -954,7 +967,7 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
954
967
  turnKey: deliverTurn.turnKey,
955
968
  });
956
969
  try {
957
- await client.sendMessage(conversationId, finalText, {
970
+ await sendMessageWithRetry(client, conversationId, finalText, {
958
971
  messageId: finalMessageId,
959
972
  ...(session.activeSelfContextId
960
973
  ? { selfContextId: session.activeSelfContextId }
@@ -987,6 +1000,14 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
987
1000
  function scheduleFinalDeliveryRetry(pending = session.pendingFinalDelivery) {
988
1001
  if (!pending || session.closed || session.pendingFinalDelivery?.turnKey !== pending.turnKey)
989
1002
  return;
1003
+ if (pending.retryCount >= MAX_FINAL_DELIVERY_RETRIES) {
1004
+ console.error(`[canon-host] [${conversationId.slice(0, 8)}] Final reply delivery exhausted after bounded retries`);
1005
+ session.pendingFinalDelivery = null;
1006
+ resetTurnToIdle();
1007
+ return;
1008
+ }
1009
+ pending.retryCount += 1;
1010
+ session.pendingFinalDelivery = pending;
990
1011
  clearFinalDeliveryTimer();
991
1012
  session.finalDeliveryTimer = setTimeout(() => {
992
1013
  session.finalDeliveryTimer = null;
@@ -1091,19 +1112,17 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
1091
1112
  return;
1092
1113
  if (session.turnState !== 'thinking' && session.turnState !== 'tool')
1093
1114
  return;
1094
- client.setTyping(conversationId, true, 'thinking').catch(() => { });
1115
+ typingSignals.start(conversationId, 'thinking').catch(() => { });
1095
1116
  }
1096
1117
  function startVisibleWorkSignal() {
1097
1118
  refreshVisibleWorkSignal();
1098
- if (session.typingKeepaliveTimer)
1099
- return;
1100
- session.typingKeepaliveTimer = setInterval(refreshVisibleWorkSignal, 3500);
1101
1119
  }
1102
1120
  function stopVisibleWorkSignal() {
1103
1121
  if (session.typingKeepaliveTimer) {
1104
1122
  clearInterval(session.typingKeepaliveTimer);
1105
1123
  session.typingKeepaliveTimer = null;
1106
1124
  }
1125
+ typingSignals.clear(conversationId).catch(() => { });
1107
1126
  }
1108
1127
  async function markQueuedMessageAccepted(sourceMessageId, markAccepted) {
1109
1128
  if (!markAccepted || !sourceMessageId)
@@ -1149,7 +1168,6 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
1149
1168
  session.turnState = 'interrupted';
1150
1169
  stopVisibleWorkSignal();
1151
1170
  clearStreaming().catch(() => { });
1152
- client.setTyping(conversationId, false).catch(() => { });
1153
1171
  writeTurn();
1154
1172
  void session.query.interrupt().catch(() => { });
1155
1173
  return;
@@ -1189,7 +1207,6 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
1189
1207
  const event = msg.event;
1190
1208
  if (isActiveTurnInterrupted()) {
1191
1209
  stopVisibleWorkSignal();
1192
- client.setTyping(conversationId, false).catch(() => { });
1193
1210
  break;
1194
1211
  }
1195
1212
  if (event?.type === 'content_block_start' && event.content_block?.type === 'tool_use') {
@@ -1219,7 +1236,6 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
1219
1236
  session.toolInProgress = false;
1220
1237
  session.turnState = 'streaming';
1221
1238
  stopVisibleWorkSignal();
1222
- client.setTyping(conversationId, false).catch(() => { });
1223
1239
  writeTurn();
1224
1240
  onStreamDelta(event.delta.text);
1225
1241
  }
@@ -1244,7 +1260,6 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
1244
1260
  }
1245
1261
  else if (state === 'idle') {
1246
1262
  stopVisibleWorkSignal();
1247
- client.setTyping(conversationId, false).catch(() => { });
1248
1263
  }
1249
1264
  else if (state === 'requires_action') {
1250
1265
  clearIdleResetTimer();
@@ -1255,7 +1270,6 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
1255
1270
  session.toolInProgress = false;
1256
1271
  session.turnState = 'waiting_input';
1257
1272
  stopVisibleWorkSignal();
1258
- client.setTyping(conversationId, false).catch(() => { });
1259
1273
  }
1260
1274
  writeState();
1261
1275
  writeTurn();
@@ -1439,6 +1453,11 @@ export async function main() {
1439
1453
  // ── Canon API client + RTDB auth ──
1440
1454
  const client = new CanonClient(apiKey, baseUrl);
1441
1455
  initRTDBAuth(client);
1456
+ const typingSignals = createTypingStatusPublisher({
1457
+ setTyping: (conversationId, typing, status) => status
1458
+ ? client.setTyping(conversationId, typing, status)
1459
+ : client.setTyping(conversationId, typing),
1460
+ });
1442
1461
  async function markQueuedMessageAcceptedForConversation(conversationId, sourceMessageId, markAccepted) {
1443
1462
  if (!markAccepted || !sourceMessageId)
1444
1463
  return;
@@ -1645,7 +1664,7 @@ export async function main() {
1645
1664
  runtimeState.clearStreaming(conversation.id).catch(() => { });
1646
1665
  runtimeState.clearSessionState(conversation.id).catch(() => { });
1647
1666
  runtimeState.clearTurnState(conversation.id).catch(() => { });
1648
- client.setTyping(conversation.id, false).catch(() => { });
1667
+ typingSignals.clear(conversation.id).catch(() => { });
1649
1668
  }
1650
1669
  console.error(`[canon-host] ${convos.length} conversations`);
1651
1670
  }
@@ -1803,7 +1822,8 @@ export async function main() {
1803
1822
  }
1804
1823
  runtimeState.clearSessionState(conversationId).catch(() => { });
1805
1824
  runtimeState.clearTurnState(conversationId).catch(() => { });
1806
- client.setTyping(conversationId, false).catch(() => { });
1825
+ typingSignals.clear(conversationId).catch(() => { });
1826
+ typingSignals.dispose(conversationId);
1807
1827
  try {
1808
1828
  session.query.close();
1809
1829
  }
@@ -1822,7 +1842,7 @@ export async function main() {
1822
1842
  session.pendingFinalDelivery = null;
1823
1843
  session.turnState = 'interrupted';
1824
1844
  session.clearStreaming().catch(() => { });
1825
- client.setTyping(conversationId, false).catch(() => { });
1845
+ typingSignals.clear(conversationId).catch(() => { });
1826
1846
  if (session.state.state === 'running') {
1827
1847
  await session.query.interrupt().catch(() => { });
1828
1848
  }
@@ -1890,7 +1910,7 @@ export async function main() {
1890
1910
  executionMode: environment.mode,
1891
1911
  });
1892
1912
  const resumeId = savedSessionIds.get(resumeKey) ?? persisted?.claudeSessionId;
1893
- const session = createSession(conversationId, environment, agentId, client, runtimeState, (convoId, sdkSessionId) => {
1913
+ const session = createSession(conversationId, environment, agentId, client, typingSignals, runtimeState, (convoId, sdkSessionId) => {
1894
1914
  // Called when session processing loop ends — save session ID for resume
1895
1915
  const existing = sessions.get(convoId);
1896
1916
  if (existing) {
@@ -1913,6 +1933,8 @@ export async function main() {
1913
1933
  }
1914
1934
  }
1915
1935
  existing?.clearStreaming().catch(() => { });
1936
+ typingSignals.clear(convoId).catch(() => { });
1937
+ typingSignals.dispose(convoId);
1916
1938
  sessions.delete(convoId);
1917
1939
  runtimeState.clearSessionState(convoId).catch(() => { });
1918
1940
  runtimeState.clearTurnState(convoId).catch(() => { });
@@ -2028,7 +2050,8 @@ export async function main() {
2028
2050
  const userMessage = error instanceof ExecutionEnvironmentError ? error.userMessage : message;
2029
2051
  console.error(`[canon-host] [${input.conversationId.slice(0, 8)}] Failed to create recovered session: ${message}`);
2030
2052
  await markQueuedMessageAcceptedForConversation(input.conversationId, m.id ?? null, shouldMarkAccepted);
2031
- await client.sendMessage(input.conversationId, `I couldn't start a coding session for this workspace: ${userMessage}`, {
2053
+ await sendMessageWithRetry(client, input.conversationId, `I couldn't start a coding session for this workspace: ${userMessage}`, {
2054
+ messageId: `claude-start-failed-${m.id}`,
2032
2055
  ...(activeSelfContextId ? { selfContextId: activeSelfContextId } : {}),
2033
2056
  metadata: {
2034
2057
  turnSemantics: 'turn_complete',
@@ -2205,7 +2228,8 @@ export async function main() {
2205
2228
  const userMessage = error instanceof ExecutionEnvironmentError ? error.userMessage : message;
2206
2229
  console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to create session: ${message}`);
2207
2230
  await markQueuedMessageAcceptedForConversation(convoId, m.id ?? null, shouldMarkAccepted);
2208
- await client.sendMessage(convoId, `I couldn't start a coding session for this workspace: ${userMessage}`, {
2231
+ await sendMessageWithRetry(client, convoId, `I couldn't start a coding session for this workspace: ${userMessage}`, {
2232
+ messageId: `claude-start-failed-${m.id}`,
2209
2233
  ...(activeSelfContextId ? { selfContextId: activeSelfContextId } : {}),
2210
2234
  metadata: {
2211
2235
  turnSemantics: 'turn_complete',
@@ -2270,6 +2294,11 @@ export async function main() {
2270
2294
  // ── Control signal watcher ──
2271
2295
  const pollControl = async () => {
2272
2296
  while (!controlStopped) {
2297
+ const hadActiveWork = [...sessions.values()].some((session) => !session.closed
2298
+ && (session.state.state === 'running'
2299
+ || session.state.state === 'requires_action'
2300
+ || session.pendingInputs.length > 0
2301
+ || Boolean(session.pendingFinalDelivery)));
2273
2302
  for (const convoId of [...sessions.keys()]) {
2274
2303
  try {
2275
2304
  // Session control signals (model, mode)
@@ -2365,7 +2394,7 @@ export async function main() {
2365
2394
  session.pendingFinalText = null;
2366
2395
  session.turnState = 'interrupted';
2367
2396
  session.clearStreaming().catch(() => { });
2368
- client.setTyping(convoId, false).catch(() => { });
2397
+ typingSignals.clear(convoId).catch(() => { });
2369
2398
  runtimeState.writeTurnState(convoId, {
2370
2399
  turnId: session.currentTurnId,
2371
2400
  state: 'interrupted',
@@ -2392,7 +2421,7 @@ export async function main() {
2392
2421
  // Transient RTDB errors — ignore and retry next cycle
2393
2422
  }
2394
2423
  }
2395
- await new Promise((r) => setTimeout(r, CONTROL_POLL_MS));
2424
+ await new Promise((r) => setTimeout(r, controlPollDelayMs(hadActiveWork)));
2396
2425
  }
2397
2426
  };
2398
2427
  pollControl().catch((err) => console.error('[canon-host] Control poll crashed:', err));
@@ -19,6 +19,7 @@ export interface ClaudePendingFinalDelivery {
19
19
  turnKey: string;
20
20
  text: string;
21
21
  messageId: string;
22
+ retryCount: number;
22
23
  }
23
24
  export interface ClaudeCompletedTurnState {
24
25
  state: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/claude-code-plugin",
3
- "version": "0.14.15",
3
+ "version": "0.14.16",
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.141",
34
- "@canonmsg/agent-sdk": "^2.0.0",
35
- "@canonmsg/core": "^1.1.0",
34
+ "@canonmsg/agent-sdk": "^2.1.1",
35
+ "@canonmsg/core": "^1.3.0",
36
36
  "@modelcontextprotocol/sdk": "^1.29.0"
37
37
  },
38
38
  "engines": {