@canonmsg/claude-code-plugin 0.12.8 → 0.13.0

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,11 +26,11 @@ 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, rtdbWrite, loadHostSessionConfig, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, saveRuntimeSessionState, clearRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
29
+ import { CLAUDE_PERMISSION_MODE_OPTIONS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, 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';
33
- import { buildClaudeFinalMessageId, createClaudeInputEnvelope, formatClaudeControlError, mergeClaudeModelOptions, shouldDeliverClaudeFinal, } from './session-state.js';
33
+ import { buildClaudeFinalMessageId, createClaudeInputEnvelope, formatClaudeControlError, mergeClaudeModelOptions, resetClaudeCompletedTurnState, shouldDeliverClaudeFinal, } from './session-state.js';
34
34
  const HELP = `canon-claude — run a local Claude Code agent host for Canon
35
35
 
36
36
  USAGE
@@ -154,26 +154,9 @@ function buildClaudeRuntimeDescriptor(input) {
154
154
  availability: ['always'],
155
155
  dispatch: { kind: 'open_details', target: 'effort' },
156
156
  },
157
- {
158
- id: 'stop',
159
- label: 'Stop',
160
- description: 'Interrupt the current Claude turn.',
161
- aliases: ['stop'],
162
- category: 'turn',
163
- placements: ['composer_slash', 'command_palette'],
164
- availability: ['busy'],
165
- dispatch: { kind: 'signal', signal: 'interrupt' },
166
- },
167
- {
168
- id: 'stop-and-clear-queue',
169
- label: 'Stop & clear queue',
170
- description: 'Interrupt the current Claude turn and drop queued Canon messages.',
171
- aliases: ['stop-clear', 'clear-queue'],
172
- category: 'turn',
173
- placements: ['composer_slash', 'command_palette', 'session_strip'],
174
- availability: ['busy_with_queue'],
175
- dispatch: { kind: 'signal', signal: 'stop_and_drop' },
176
- },
157
+ RUNTIME_STOP_ACTION,
158
+ RUNTIME_STOP_AND_DROP_ACTION,
159
+ RUNTIME_NEW_SESSION_ACTION,
177
160
  ],
178
161
  });
179
162
  }
@@ -822,15 +805,9 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
822
805
  if (session.closed)
823
806
  return;
824
807
  clearFinalDeliveryTimer();
825
- session.currentTurnId = null;
826
- session.currentTurnOpenedAt = null;
827
- session.lastAcceptedIntent = null;
828
- session.pendingFinalText = null;
829
- session.pendingFinalDelivery = null;
830
- session.activeInput = null;
831
- session.toolInProgress = false;
832
- session.turnState = 'idle';
808
+ resetClaudeCompletedTurnState(session);
833
809
  stopVisibleWorkSignal();
810
+ writeState();
834
811
  writeTurn();
835
812
  drainPendingInput();
836
813
  }
@@ -1570,19 +1547,20 @@ export async function main() {
1570
1547
  isOwner: input.isOwner,
1571
1548
  });
1572
1549
  }
1573
- function closeSession(conversationId) {
1550
+ function closeSession(conversationId, options = {}) {
1574
1551
  const session = sessions.get(conversationId);
1575
1552
  if (!session)
1576
1553
  return;
1554
+ const saveResume = options.saveResume !== false;
1577
1555
  session.closed = true;
1578
1556
  if (session.typingKeepaliveTimer) {
1579
1557
  clearInterval(session.typingKeepaliveTimer);
1580
1558
  session.typingKeepaliveTimer = null;
1581
1559
  }
1582
1560
  releaseConversationEnvironment(session.environment);
1583
- // Save SDK session ID for future resume
1584
- if (session.sdkSessionId) {
1585
- savedSessionIds.set(buildConversationEnvironmentKey(conversationId, session.environment.baseCwd), session.sdkSessionId);
1561
+ const resumeKey = buildConversationEnvironmentKey(conversationId, session.environment.baseCwd);
1562
+ if (saveResume && session.sdkSessionId) {
1563
+ savedSessionIds.set(resumeKey, session.sdkSessionId);
1586
1564
  saveRuntimeSessionState(runtimeId, {
1587
1565
  conversationId,
1588
1566
  baseCwd: session.environment.baseCwd,
@@ -1590,6 +1568,27 @@ export async function main() {
1590
1568
  claudeSessionId: session.sdkSessionId,
1591
1569
  });
1592
1570
  }
1571
+ else {
1572
+ const existingState = loadRuntimeSessionState(runtimeId, {
1573
+ conversationId,
1574
+ baseCwd: session.environment.baseCwd,
1575
+ executionMode: session.environment.mode,
1576
+ });
1577
+ savedSessionIds.delete(resumeKey);
1578
+ clearRuntimeSessionState(runtimeId, {
1579
+ conversationId,
1580
+ baseCwd: session.environment.baseCwd,
1581
+ executionMode: session.environment.mode,
1582
+ });
1583
+ if (existingState?.lastInboundMessageId) {
1584
+ saveRuntimeSessionState(runtimeId, {
1585
+ conversationId,
1586
+ baseCwd: session.environment.baseCwd,
1587
+ executionMode: session.environment.mode,
1588
+ lastInboundMessageId: existingState.lastInboundMessageId,
1589
+ });
1590
+ }
1591
+ }
1593
1592
  if (session.streamingTimer)
1594
1593
  clearTimeout(session.streamingTimer);
1595
1594
  session.streamingText = '';
@@ -1610,7 +1609,22 @@ export async function main() {
1610
1609
  }
1611
1610
  catch { }
1612
1611
  sessions.delete(conversationId);
1613
- console.error(`[canon-host] [${conversationId.slice(0, 8)}] Session suspended (sessionId saved for resume)`);
1612
+ console.error(`[canon-host] [${conversationId.slice(0, 8)}] Session ${options.reason ?? (saveResume ? 'suspended (sessionId saved for resume)' : 'cleared')}`);
1613
+ }
1614
+ async function resetRuntimeSession(session) {
1615
+ const conversationId = session.conversationId;
1616
+ const droppedInputs = session.pendingInputs.splice(0);
1617
+ await markSessionPendingInputsRejected(conversationId, droppedInputs);
1618
+ if (session.activeInput) {
1619
+ session.interruptedTurnKeys.add(session.activeInput.turnKey);
1620
+ }
1621
+ session.pendingFinalText = null;
1622
+ session.pendingFinalDelivery = null;
1623
+ session.turnState = 'interrupted';
1624
+ if (session.state.state === 'running') {
1625
+ await session.query.interrupt().catch(() => { });
1626
+ }
1627
+ closeSession(conversationId, { saveResume: false, reason: 'reset for new Canon session' });
1614
1628
  }
1615
1629
  async function markSessionPendingInputsRejected(conversationId, inputs) {
1616
1630
  await Promise.all(inputs.map((input) => {
@@ -2054,10 +2068,17 @@ export async function main() {
2054
2068
  if (sig && typeof sig === 'object') {
2055
2069
  const s = sig;
2056
2070
  const ts = s.updatedAt ?? 0;
2057
- if ((s.type === 'interrupt' || s.type === 'stop_and_drop') && ts > (lastSeenSignal.get(convoId) ?? 0)) {
2071
+ if ((s.type === 'interrupt' || s.type === 'stop_and_drop' || s.type === 'new_session')
2072
+ && ts > (lastSeenSignal.get(convoId) ?? 0)) {
2058
2073
  lastSeenSignal.set(convoId, ts);
2059
2074
  const session = sessions.get(convoId);
2060
2075
  if (session && !session.closed) {
2076
+ if (s.type === 'new_session') {
2077
+ console.error(`[canon-host] [${convoId.slice(0, 8)}] new_session signal`);
2078
+ await resetRuntimeSession(session);
2079
+ await rtdbWrite(`/control/${convoId}/${agentId}/signal`, null).catch(() => { });
2080
+ continue;
2081
+ }
2061
2082
  const isActive = session.state.state === 'running';
2062
2083
  if (!isActive && (s.type !== 'stop_and_drop' || session.pendingInputs.length === 0)) {
2063
2084
  await rtdbWrite(`/control/${convoId}/${agentId}/signal`, null).catch(() => { });
@@ -1,5 +1,5 @@
1
1
  import type { SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
2
- import type { DeliveryIntent, ModelOption } from '@canonmsg/core';
2
+ import type { DeliveryIntent, ModelOption, TurnLifecycleState } from '@canonmsg/core';
3
3
  export type ClaudeInputKind = 'seed' | 'canon';
4
4
  export interface ClaudeInputEnvelope {
5
5
  kind: ClaudeInputKind;
@@ -19,6 +19,19 @@ export interface ClaudePendingFinalDelivery {
19
19
  text: string;
20
20
  messageId: string;
21
21
  }
22
+ export interface ClaudeCompletedTurnState {
23
+ state: {
24
+ state?: 'idle' | 'running' | 'requires_action';
25
+ };
26
+ pendingFinalText: string | null;
27
+ pendingFinalDelivery: ClaudePendingFinalDelivery | null;
28
+ activeInput: ClaudeInputEnvelope | null;
29
+ toolInProgress: boolean;
30
+ turnState: TurnLifecycleState;
31
+ currentTurnId: string | null;
32
+ currentTurnOpenedAt: number | null;
33
+ lastAcceptedIntent: DeliveryIntent | null;
34
+ }
22
35
  export declare function createClaudeInputEnvelope(input: {
23
36
  kind: ClaudeInputKind;
24
37
  msg: SDKUserMessage;
@@ -36,6 +49,7 @@ export declare function shouldDeliverClaudeFinal(input: {
36
49
  finalizedTurnKeys: ReadonlySet<string>;
37
50
  interruptedTurnKeys: ReadonlySet<string>;
38
51
  }): boolean;
52
+ export declare function resetClaudeCompletedTurnState(session: ClaudeCompletedTurnState): void;
39
53
  export declare function mergeClaudeModelOptions(input: {
40
54
  previous: ReadonlyArray<ModelOption>;
41
55
  next: ReadonlyArray<ModelOption>;
@@ -29,6 +29,17 @@ export function shouldDeliverClaudeFinal(input) {
29
29
  return false;
30
30
  return true;
31
31
  }
32
+ export function resetClaudeCompletedTurnState(session) {
33
+ session.state.state = 'idle';
34
+ session.currentTurnId = null;
35
+ session.currentTurnOpenedAt = null;
36
+ session.lastAcceptedIntent = null;
37
+ session.pendingFinalText = null;
38
+ session.pendingFinalDelivery = null;
39
+ session.activeInput = null;
40
+ session.toolInProgress = false;
41
+ session.turnState = 'idle';
42
+ }
32
43
  export function mergeClaudeModelOptions(input) {
33
44
  const byValue = new Map();
34
45
  for (const option of input.previous) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/claude-code-plugin",
3
- "version": "0.12.8",
3
+ "version": "0.13.0",
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": "^1.1.4",
35
- "@canonmsg/core": "^0.15.4",
34
+ "@canonmsg/agent-sdk": "^1.2.0",
35
+ "@canonmsg/core": "^0.16.0",
36
36
  "@modelcontextprotocol/sdk": "^1.12.0"
37
37
  },
38
38
  "engines": {