@canonmsg/claude-code-plugin 0.12.1 → 0.12.2

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,8 +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, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildPlanApprovalRequest, buildQuestionRequest, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, EXECUTION_ENVIRONMENT_MODES, CanonClient, CanonStream, clearTurnState, 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, writeRuntimeInfo, writeSessionState, clearSessionState, writeTurnState, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, saveRuntimeSessionState, clearRuntimeSessionState, upsertLocalRuntimeEntry, } from '@canonmsg/core';
30
- import { buildCanonHostPrompt, buildHydratedInboundContext, createConversationMetadataLoader, loadHostSessionConfig, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, } from './host-runtime.js';
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';
31
30
  import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
32
31
  import { createStreamingBuffer } from './streaming-buffer.js';
33
32
  import { runCli } from './cli-entry.js';
@@ -47,76 +46,16 @@ let runtimeMetadata = {
47
46
  };
48
47
  let runtimeMetadataRefresh = null;
49
48
  function buildClaudeRuntimeDescriptor(input) {
50
- return {
51
- coreControls: [
52
- {
53
- id: 'model',
54
- label: 'Model',
55
- options: input.models,
56
- defaultValue: input.models[0]?.value ?? null,
57
- availability: 'setup_and_live',
58
- liveBehavior: 'immediate',
59
- selectionPolicy: 'inherit',
60
- },
61
- {
62
- id: 'workspace',
63
- label: 'Project',
64
- options: input.workspaces.map((workspace) => ({
65
- value: workspace.id,
66
- label: workspace.label,
67
- ...(workspace.description ? { description: workspace.description } : {}),
68
- ...(workspace.workspaceRootId ? { workspaceRootId: workspace.workspaceRootId } : {}),
69
- ...(workspace.workspaceRelativePath ? { workspaceRelativePath: workspace.workspaceRelativePath } : {}),
70
- ...(workspace.source ? { source: workspace.source } : {}),
71
- })),
72
- defaultValue: input.workspaces[0]?.id ?? null,
73
- availability: 'setup',
74
- liveBehavior: 'none',
75
- selectionPolicy: 'inherit',
76
- description: input.workspaceRoots?.length
77
- ? 'Choose one of the projects discovered inside the approved local roots for this host.'
78
- : 'Choose one of the local projects advertised by this host.',
79
- },
80
- {
81
- id: 'executionMode',
82
- label: 'Execution mode',
83
- options: input.executionModes.map((mode) => ({
84
- value: mode,
85
- label: mode === 'worktree' ? 'Isolated worktree' : 'Use shared project',
86
- description: mode === 'worktree'
87
- ? 'Creates or reuses a per-conversation git worktree under ~/.canon/conversation-worktrees when the selected project is a git repo.'
88
- : 'Runs directly in the selected project folder. Changes happen there.',
89
- })),
90
- defaultValue: null,
91
- availability: 'setup',
92
- liveBehavior: 'none',
93
- selectionPolicy: 'required_explicit',
94
- },
95
- ],
96
- runtimeControls: [
97
- {
98
- id: 'permissionMode',
99
- label: 'Permission mode',
100
- options: [...CLAUDE_PERMISSION_MODE_OPTIONS],
101
- defaultValue: 'default',
102
- availability: 'setup_and_live',
103
- liveBehavior: 'immediate',
104
- selectionPolicy: 'inherit',
105
- },
106
- {
107
- id: 'effort',
108
- label: 'Thinking level',
109
- options: [
110
- { value: 'low', label: 'Low' },
111
- { value: 'medium', label: 'Medium' },
112
- { value: 'high', label: 'High' },
113
- ],
114
- defaultValue: 'medium',
115
- availability: 'setup_and_live',
116
- liveBehavior: 'immediate',
117
- selectionPolicy: 'inherit',
118
- },
119
- ],
49
+ return buildFirstPartyCodingRuntimeDescriptor({
50
+ clientType: 'claude-code',
51
+ models: input.models,
52
+ workspaces: input.workspaces,
53
+ workspaceRoots: input.workspaceRoots,
54
+ executionModes: input.executionModes,
55
+ permissionModes: CLAUDE_PERMISSION_MODE_OPTIONS,
56
+ defaultPermissionMode: 'default',
57
+ streamingTextMode: 'delta',
58
+ admissionActions: HOST_ADMISSION_ACTIONS_DISABLED,
120
59
  actions: [
121
60
  {
122
61
  id: 'plan',
@@ -204,11 +143,7 @@ function buildClaudeRuntimeDescriptor(input) {
204
143
  dispatch: { kind: 'signal', signal: 'interrupt' },
205
144
  },
206
145
  ],
207
- workspaceRoots: input.workspaceRoots,
208
- supportsInterrupt: true,
209
- streamingTextMode: 'delta',
210
- admissionActions: HOST_ADMISSION_ACTIONS_DISABLED,
211
- };
146
+ });
212
147
  }
213
148
  function normalizeClaudeEffortLevel(value) {
214
149
  return value === 'low' || value === 'medium' || value === 'high'
@@ -550,7 +485,7 @@ async function buildCanonUserContent(input) {
550
485
  ];
551
486
  }
552
487
  // ── Session factory ─────────────────────────────────────────────────
553
- function createSession(conversationId, environment, agentId, client, onSessionEnd, onRuntimeDescriptorUpdate, config, resumeSessionId) {
488
+ function createSession(conversationId, environment, agentId, client, runtimeState, onSessionEnd, onRuntimeDescriptorUpdate, config, resumeSessionId) {
554
489
  const { cwd } = environment;
555
490
  // Per-session input stream with message queue
556
491
  let resolveInput = null;
@@ -757,10 +692,9 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
757
692
  session.streamingTimer = timer;
758
693
  },
759
694
  onWrite: (text) => {
760
- rtdbWrite(`/streaming/${conversationId}/${agentId}`, {
695
+ runtimeState.writeStreaming(conversationId, {
761
696
  text,
762
697
  status: 'streaming',
763
- updatedAt: { '.sv': 'timestamp' },
764
698
  }).catch(() => { });
765
699
  },
766
700
  });
@@ -773,7 +707,7 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
773
707
  }
774
708
  function clearStreaming() {
775
709
  streamingBuffer.clear();
776
- rtdbWrite(`/streaming/${conversationId}/${agentId}`, null).catch(() => { });
710
+ runtimeState.clearStreaming(conversationId).catch(() => { });
777
711
  }
778
712
  function clearIdleResetTimer() {
779
713
  if (!session.idleResetTimer)
@@ -862,7 +796,7 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
862
796
  }, FINAL_DELIVERY_RETRY_MS);
863
797
  }
864
798
  function writeState() {
865
- writeSessionState(conversationId, agentId, {
799
+ runtimeState.writeSessionState(conversationId, {
866
800
  ...session.state,
867
801
  ...(session.availableModels.length ? { availableModels: session.availableModels } : {}),
868
802
  cwd,
@@ -877,7 +811,7 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
877
811
  }).catch(() => { });
878
812
  }
879
813
  function writeTurn() {
880
- writeTurnState(conversationId, agentId, {
814
+ runtimeState.writeTurnState(conversationId, {
881
815
  turnId: session.currentTurnId,
882
816
  state: session.turnState,
883
817
  queueDepth: session.pendingInputs.length,
@@ -1138,7 +1072,7 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
1138
1072
  executionModes: config.availableExecutionModes,
1139
1073
  }),
1140
1074
  });
1141
- writeSessionState(conversationId, agentId, {
1075
+ runtimeState.writeSessionState(conversationId, {
1142
1076
  ...session.state,
1143
1077
  availableModels: modelList,
1144
1078
  cwd,
@@ -1244,6 +1178,11 @@ export async function main() {
1244
1178
  lastStartedAt: new Date().toISOString(),
1245
1179
  lastHeartbeatAt: new Date().toISOString(),
1246
1180
  });
1181
+ const runtimeState = createRuntimeStatePublisher({
1182
+ agentId,
1183
+ clientType: 'claude-code',
1184
+ hostMode: true,
1185
+ });
1247
1186
  let streamConnected = false;
1248
1187
  const hostAvailableExecutionModes = [
1249
1188
  ...EXECUTION_ENVIRONMENT_MODES,
@@ -1294,6 +1233,7 @@ export async function main() {
1294
1233
  runtime: runtimeDescriptor,
1295
1234
  workspaceOptions,
1296
1235
  defaultCwd: workingDir,
1236
+ extraSessionConfigFields: ['permissionMode', 'effort'],
1297
1237
  liveSessionConfigByConversation: new Map(Array.from(sessions.values()).map((session) => {
1298
1238
  const workspaceId = resolveWorkspaceIdForBaseCwd(session.environment.baseCwd);
1299
1239
  return [
@@ -1334,7 +1274,7 @@ export async function main() {
1334
1274
  worktreePath: session?.environment.worktreePath ?? null,
1335
1275
  fallbackReason: resolveExecutionFallbackReason(session?.environment),
1336
1276
  });
1337
- await writeRuntimeInfo(conversationId, agentId, payload);
1277
+ await runtimeState.writeRuntimeInfo(conversationId, payload);
1338
1278
  })).catch((error) => {
1339
1279
  console.error('[canon-host] Failed to publish runtime info:', error);
1340
1280
  });
@@ -1446,7 +1386,7 @@ export async function main() {
1446
1386
  if (session.streamingTimer)
1447
1387
  clearTimeout(session.streamingTimer);
1448
1388
  session.streamingText = '';
1449
- rtdbWrite(`/streaming/${conversationId}/${agentId}`, null).catch(() => { });
1389
+ runtimeState.clearStreaming(conversationId).catch(() => { });
1450
1390
  if (session.idleResetTimer) {
1451
1391
  clearTimeout(session.idleResetTimer);
1452
1392
  session.idleResetTimer = null;
@@ -1455,8 +1395,8 @@ export async function main() {
1455
1395
  clearTimeout(session.finalDeliveryTimer);
1456
1396
  session.finalDeliveryTimer = null;
1457
1397
  }
1458
- clearSessionState(conversationId, agentId).catch(() => { });
1459
- clearTurnState(conversationId, agentId).catch(() => { });
1398
+ runtimeState.clearSessionState(conversationId).catch(() => { });
1399
+ runtimeState.clearTurnState(conversationId).catch(() => { });
1460
1400
  client.setTyping(conversationId, false).catch(() => { });
1461
1401
  try {
1462
1402
  session.query.close();
@@ -1510,7 +1450,7 @@ export async function main() {
1510
1450
  executionMode: environment.mode,
1511
1451
  });
1512
1452
  const resumeId = savedSessionIds.get(resumeKey) ?? persisted?.claudeSessionId;
1513
- const session = createSession(conversationId, environment, agentId, client, (convoId, sdkSessionId) => {
1453
+ const session = createSession(conversationId, environment, agentId, client, runtimeState, (convoId, sdkSessionId) => {
1514
1454
  // Called when session processing loop ends — save session ID for resume
1515
1455
  const existing = sessions.get(convoId);
1516
1456
  if (existing) {
@@ -1533,8 +1473,8 @@ export async function main() {
1533
1473
  }
1534
1474
  }
1535
1475
  sessions.delete(convoId);
1536
- clearSessionState(convoId, agentId).catch(() => { });
1537
- clearTurnState(convoId, agentId).catch(() => { });
1476
+ runtimeState.clearSessionState(convoId).catch(() => { });
1477
+ runtimeState.clearTurnState(convoId).catch(() => { });
1538
1478
  }, (descriptor) => {
1539
1479
  runtimeDescriptor = descriptor;
1540
1480
  void publishRuntimeHeartbeat();
@@ -1794,7 +1734,7 @@ export async function main() {
1794
1734
  },
1795
1735
  onDisconnected: () => {
1796
1736
  streamConnected = false;
1797
- rtdbWrite(`/agent-runtime/${agentId}`, null).catch(() => { });
1737
+ runtimeState.clearAgentRuntime().catch(() => { });
1798
1738
  console.error('[canon-host] SSE disconnected');
1799
1739
  },
1800
1740
  onError: (err) => console.error(`[canon-host] SSE error: ${err.message}`),
@@ -1852,7 +1792,7 @@ export async function main() {
1852
1792
  console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to set effort:`, err);
1853
1793
  }
1854
1794
  }
1855
- writeSessionState(convoId, agentId, {
1795
+ runtimeState.writeSessionState(convoId, {
1856
1796
  ...session.state,
1857
1797
  ...(session.availableModels.length ? { availableModels: session.availableModels } : {}),
1858
1798
  cwd: session.cwd,
@@ -1880,7 +1820,7 @@ export async function main() {
1880
1820
  console.error(`[canon-host] [${convoId.slice(0, 8)}] Interrupt signal`);
1881
1821
  try {
1882
1822
  session.turnState = 'interrupted';
1883
- writeTurnState(convoId, agentId, {
1823
+ runtimeState.writeTurnState(convoId, {
1884
1824
  turnId: session.currentTurnId,
1885
1825
  state: 'interrupted',
1886
1826
  queueDepth: session.pendingInputs.length,
@@ -1896,7 +1836,7 @@ export async function main() {
1896
1836
  session.streamingTimer = null;
1897
1837
  }
1898
1838
  session.streamingText = '';
1899
- rtdbWrite(`/streaming/${convoId}/${agentId}`, null).catch(() => { });
1839
+ runtimeState.clearStreaming(convoId).catch(() => { });
1900
1840
  client.setTyping(convoId, false).catch(() => { });
1901
1841
  }
1902
1842
  catch (err) {
@@ -1917,7 +1857,7 @@ export async function main() {
1917
1857
  // ── Heartbeat (per-session) ──
1918
1858
  const heartbeat = setInterval(() => {
1919
1859
  for (const session of sessions.values()) {
1920
- writeSessionState(session.conversationId, agentId, {
1860
+ runtimeState.writeSessionState(session.conversationId, {
1921
1861
  ...session.state,
1922
1862
  ...(session.availableModels.length ? { availableModels: session.availableModels } : {}),
1923
1863
  cwd: session.cwd,
@@ -1927,7 +1867,7 @@ export async function main() {
1927
1867
  hostMode: true,
1928
1868
  isActive: true,
1929
1869
  }).catch(() => { });
1930
- writeTurnState(session.conversationId, agentId, {
1870
+ runtimeState.writeTurnState(session.conversationId, {
1931
1871
  turnId: session.currentTurnId,
1932
1872
  state: session.turnState,
1933
1873
  queueDepth: session.pendingInputs.length,
@@ -1962,7 +1902,7 @@ export async function main() {
1962
1902
  clearInterval(heartbeat);
1963
1903
  clearInterval(idleCheck);
1964
1904
  stream.stop();
1965
- await rtdbWrite(`/agent-runtime/${agentId}`, null).catch(() => { });
1905
+ await runtimeState.clearAgentRuntime().catch(() => { });
1966
1906
  for (const convoId of [...sessions.keys()]) {
1967
1907
  closeSession(convoId);
1968
1908
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/claude-code-plugin",
3
- "version": "0.12.1",
3
+ "version": "0.12.2",
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.1",
35
- "@canonmsg/core": "^0.13.0",
34
+ "@canonmsg/agent-sdk": "^0.10.2",
35
+ "@canonmsg/core": "^0.14.0",
36
36
  "@modelcontextprotocol/sdk": "^1.12.0"
37
37
  },
38
38
  "engines": {
@@ -1,134 +0,0 @@
1
- /**
2
- * Host-runtime helpers, inlined from @canonmsg/core.
3
- *
4
- * These helpers glue Canon host wrappers (Codex, Claude Code) to the Canon
5
- * RTDB session/runtime surface. They used to live in
6
- * `@canonmsg/core/src/host-runtime/` but were moved into each plugin in
7
- * media-parity PR C so core does not have to carry host-specific concerns.
8
- *
9
- * Keep this file in lockstep with the equivalent file in
10
- * `packages/codex-plugin/src/host-runtime.ts`. If you change the
11
- * behavior here, update that copy too and adjust the shared golden
12
- * fixture test (`packages/claude-code-plugin/src/host-runtime.test.ts`).
13
- */
14
- import { type AgentClientType, type AgentRuntime, type CanonClient, type CanonConversation, type CanonMessage, type CanonMessagesPage, type MessageCreatedPayload, type ResolvedAgentBehaviorPolicy, type SessionWorkspaceConfig } from '@canonmsg/core';
15
- export interface HostInboundParticipantContext {
16
- conversationType: CanonConversation['type'] | 'unknown';
17
- memberCount: number | null;
18
- senderType: 'human' | 'ai_agent';
19
- senderName: string;
20
- isOwner: boolean;
21
- mentionedAgent: boolean;
22
- recentSenderTypes: Array<'human' | 'ai_agent'>;
23
- recentHumanCount: number;
24
- recentAgentCount: number;
25
- consecutiveAgentTurns: number;
26
- currentAgentStreakStartedByHuman: boolean;
27
- }
28
- type HostInboundMessage = {
29
- text?: string | null;
30
- contentType?: CanonMessage['contentType'] | null;
31
- attachments?: CanonMessage['attachments'];
32
- senderType?: CanonMessage['senderType'];
33
- mentions?: string[] | null;
34
- contactCard?: CanonMessage['contactCard'];
35
- };
36
- interface HostWorkspaceResolverOption {
37
- id: string;
38
- cwd: string;
39
- }
40
- export declare const HOST_ADMISSION_ACTION_CAPABILITIES: Readonly<{
41
- canStartDirectConversation: false;
42
- canSendContactRequest: false;
43
- canApprovePendingContactRequests: false;
44
- canRejectPendingContactRequests: false;
45
- }>;
46
- export declare function buildCanonHostPrompt(input: {
47
- hostLabel: string;
48
- content: string;
49
- conversationId: string;
50
- participantContext: HostInboundParticipantContext;
51
- behavior?: ResolvedAgentBehaviorPolicy | null;
52
- workSession?: MessageCreatedPayload['message']['workSession'];
53
- workSessions?: MessageCreatedPayload['workSessions'];
54
- buildInboundContextLines: (context: HostInboundParticipantContext) => string[];
55
- }): string;
56
- /**
57
- * Render the **text portion** of an inbound Canon message. Images and other
58
- * binary attachments are referenced by short placeholders — their actual
59
- * bytes are delivered to the host as native vision/media inputs (Anthropic
60
- * image blocks, Codex `-i` file args). URLs are intentionally *not* inlined,
61
- * since the harness never needs to refetch and earlier `[Image: <url>]`
62
- * inlining caused vision models to see a string about an image instead of
63
- * the image itself.
64
- *
65
- * `materialized` may be passed so non-image attachments can reference a
66
- * local path the agent can Read. Without it we fall back to an unadorned
67
- * placeholder; the vision path still works because image blocks carry the
68
- * bytes directly.
69
- */
70
- export declare function renderCanonHostInboundContent(message: HostInboundMessage, materialized?: ReadonlyArray<{
71
- kind: 'image' | 'audio' | 'file';
72
- path: string;
73
- fileName?: string;
74
- durationMs?: number;
75
- index: number;
76
- }>): string;
77
- export declare function buildHydratedInboundContext(input: {
78
- agentId: string;
79
- conversation: CanonConversation | null;
80
- page?: CanonMessagesPage | null;
81
- message: HostInboundMessage;
82
- senderName: string;
83
- isOwner: boolean;
84
- }): {
85
- participantContext: HostInboundParticipantContext;
86
- behavior?: ResolvedAgentBehaviorPolicy | null;
87
- workSessions: NonNullable<MessageCreatedPayload['workSessions']>;
88
- hydratedFromPage: boolean;
89
- };
90
- export declare function publishHostAgentRuntime(agentId: string, clientType: AgentClientType, runtime: AgentRuntime): Promise<void>;
91
- export declare function publishHostSessionSnapshots(input: {
92
- conversationIds: string[];
93
- agentId: string;
94
- clientType: AgentClientType;
95
- runtime: AgentRuntime;
96
- workspaceOptions: HostWorkspaceResolverOption[];
97
- defaultCwd: string;
98
- liveSessionConfigByConversation?: ReadonlyMap<string, {
99
- model?: string;
100
- permissionMode?: string;
101
- effort?: string;
102
- runtimeControlValues?: Record<string, string>;
103
- workspaceId?: string;
104
- executionMode?: SessionWorkspaceConfig['executionMode'];
105
- executionBranch?: string | null;
106
- }>;
107
- }): Promise<void>;
108
- export declare function readHostSessionConfig<TExtra extends string = never>(raw: unknown, extraStringFields?: readonly TExtra[]): (SessionWorkspaceConfig & Partial<Record<TExtra, string>> & {
109
- runtimeControlValues?: Record<string, string>;
110
- }) | null;
111
- export declare function loadHostSessionConfig<TExtra extends string = never>(input: {
112
- conversationId: string;
113
- agentId: string;
114
- extraStringFields?: readonly TExtra[];
115
- }): Promise<(SessionWorkspaceConfig & Partial<Record<TExtra, string>> & {
116
- runtimeControlValues?: Record<string, string>;
117
- }) | null>;
118
- export declare function resolveHostWorkspaceCwd(input: {
119
- workspaceOptions: HostWorkspaceResolverOption[];
120
- config: {
121
- workspaceId?: string;
122
- retiredWorkspaceConfig?: boolean;
123
- } | null;
124
- defaultCwd: string;
125
- }): string;
126
- export declare function createConversationMetadataLoader(input: {
127
- client: CanonClient;
128
- conversationCache: Map<string, CanonConversation>;
129
- cacheTtlMs?: number;
130
- }): {
131
- refreshConversationCache(force?: boolean): Promise<void>;
132
- getConversationMeta(conversationId: string): Promise<CanonConversation | null>;
133
- };
134
- export {};
@@ -1,264 +0,0 @@
1
- /**
2
- * Host-runtime helpers, inlined from @canonmsg/core.
3
- *
4
- * These helpers glue Canon host wrappers (Codex, Claude Code) to the Canon
5
- * RTDB session/runtime surface. They used to live in
6
- * `@canonmsg/core/src/host-runtime/` but were moved into each plugin in
7
- * media-parity PR C so core does not have to carry host-specific concerns.
8
- *
9
- * Keep this file in lockstep with the equivalent file in
10
- * `packages/codex-plugin/src/host-runtime.ts`. If you change the
11
- * behavior here, update that copy too and adjust the shared golden
12
- * fixture test (`packages/claude-code-plugin/src/host-runtime.test.ts`).
13
- */
14
- import { buildAgentSessionSnapshot, buildConversationWorktreeSpec, buildBehaviorPolicyLines, buildParticipationHistorySnapshot, buildWorkSessionsPromptLines, mergeWorkSessionContexts, normalizeOptionalString, patchAgentSessionSnapshot, readSessionWorkspaceConfig, resolveConfiguredWorkspaceCwd, rtdbRead, rtdbWrite, } from '@canonmsg/core';
15
- export const HOST_ADMISSION_ACTION_CAPABILITIES = Object.freeze({
16
- canStartDirectConversation: false,
17
- canSendContactRequest: false,
18
- canApprovePendingContactRequests: false,
19
- canRejectPendingContactRequests: false,
20
- });
21
- export function buildCanonHostPrompt(input) {
22
- const resolvedWorkSessions = mergeWorkSessionContexts(input.workSession, input.workSessions);
23
- return [
24
- `You are connected to Canon messaging through a ${input.hostLabel} host wrapper.`,
25
- 'Only the last assistant message from this turn will be delivered as the permanent Canon reply.',
26
- 'Short intermediate assistant messages may be shown as ephemeral status while you work.',
27
- ...input.buildInboundContextLines(input.participantContext),
28
- ...buildBehaviorPolicyLines(input.behavior),
29
- ...buildWorkSessionsPromptLines(resolvedWorkSessions),
30
- 'Canon participants may be humans or AI agents.',
31
- 'Honor the Canon behavior policy above when deciding how proactively to participate.',
32
- ...(resolvedWorkSessions.length > 0
33
- ? ['Honor the Canon work-session context above within its stated disclosure limits.']
34
- : []),
35
- `Conversation ID: ${input.conversationId}`,
36
- '',
37
- 'New Canon message:',
38
- input.content,
39
- ].join('\n');
40
- }
41
- /**
42
- * Render the **text portion** of an inbound Canon message. Images and other
43
- * binary attachments are referenced by short placeholders — their actual
44
- * bytes are delivered to the host as native vision/media inputs (Anthropic
45
- * image blocks, Codex `-i` file args). URLs are intentionally *not* inlined,
46
- * since the harness never needs to refetch and earlier `[Image: <url>]`
47
- * inlining caused vision models to see a string about an image instead of
48
- * the image itself.
49
- *
50
- * `materialized` may be passed so non-image attachments can reference a
51
- * local path the agent can Read. Without it we fall back to an unadorned
52
- * placeholder; the vision path still works because image blocks carry the
53
- * bytes directly.
54
- */
55
- export function renderCanonHostInboundContent(message, materialized) {
56
- const body = message.text || '';
57
- const placeholders = [];
58
- const attachments = message.attachments ?? [];
59
- for (let i = 0; i < attachments.length; i += 1) {
60
- const att = attachments[i];
61
- const mat = materialized?.find((m) => m.index === i) ?? null;
62
- placeholders.push(describeAttachment(att, mat));
63
- }
64
- if (message.contentType === 'contact_card' && message.contactCard) {
65
- placeholders.push(describeContactCard(message.contactCard));
66
- }
67
- const rendered = [...placeholders, body].filter(Boolean).join('\n');
68
- return rendered || '[Empty message]';
69
- }
70
- function describeContactCard(card) {
71
- const parts = [`${card.userType} · userId: ${card.userId}`];
72
- if (card.ownerName)
73
- parts.push(`owner: ${card.ownerName}`);
74
- if (card.about)
75
- parts.push(`about: ${card.about}`);
76
- const identity = `📇 Contact card: "${card.displayName}" (${parts.join(' · ')}).`;
77
- const missingCapabilities = [
78
- !HOST_ADMISSION_ACTION_CAPABILITIES.canStartDirectConversation
79
- ? 'start a direct conversation'
80
- : null,
81
- !HOST_ADMISSION_ACTION_CAPABILITIES.canSendContactRequest
82
- ? 'send a contact request'
83
- : null,
84
- !HOST_ADMISSION_ACTION_CAPABILITIES.canApprovePendingContactRequests
85
- ? 'approve pending requests'
86
- : null,
87
- !HOST_ADMISSION_ACTION_CAPABILITIES.canRejectPendingContactRequests
88
- ? 'reject pending requests'
89
- : null,
90
- ].filter(Boolean).join(', ');
91
- const hint = `This host can inspect the card, but Canon admission actions are missing here. Missing capabilities: ${missingCapabilities}. Use another Canon surface for userId ${card.userId}.`;
92
- return `${identity}\n${hint}`;
93
- }
94
- function describeAttachment(attachment, materialized) {
95
- if (attachment.kind === 'image') {
96
- return '[Image attached]';
97
- }
98
- if (attachment.kind === 'audio') {
99
- const durationMs = materialized?.durationMs ?? attachment.durationMs;
100
- const duration = durationMs ? ` (${Math.round(durationMs / 1000)}s)` : '';
101
- const ref = materialized?.path ? ` ${materialized.path}` : '';
102
- return `[Voice message${duration}${ref}]`;
103
- }
104
- // file
105
- const label = materialized?.fileName ?? attachment.fileName ?? 'File';
106
- const ref = materialized?.path ? ` ${materialized.path}` : '';
107
- return `[File: ${label}${ref}]`;
108
- }
109
- export function buildHydratedInboundContext(input) {
110
- const history = buildParticipationHistorySnapshot(input.page?.messages ?? [], input.agentId);
111
- return {
112
- participantContext: {
113
- conversationType: input.conversation?.type ?? 'unknown',
114
- memberCount: input.conversation?.memberIds?.length ?? null,
115
- senderType: input.message.senderType ?? 'human',
116
- senderName: input.senderName,
117
- isOwner: input.isOwner,
118
- mentionedAgent: Array.isArray(input.message.mentions) && input.message.mentions.includes(input.agentId),
119
- recentSenderTypes: history.recentSenderTypes,
120
- recentHumanCount: history.recentHumanCount,
121
- recentAgentCount: history.recentAgentCount,
122
- consecutiveAgentTurns: history.consecutiveAgentTurns,
123
- currentAgentStreakStartedByHuman: history.currentAgentStreakStartedByHuman,
124
- },
125
- behavior: input.page?.behavior ?? input.conversation?.behavior,
126
- workSessions: input.page?.workSessions ?? [],
127
- hydratedFromPage: input.page != null,
128
- };
129
- }
130
- export async function publishHostAgentRuntime(agentId, clientType, runtime) {
131
- await rtdbWrite(`/agent-runtime/${agentId}`, {
132
- clientType,
133
- hostMode: true,
134
- ...runtime,
135
- updatedAt: { '.sv': 'timestamp' },
136
- });
137
- }
138
- export async function publishHostSessionSnapshots(input) {
139
- if (input.conversationIds.length === 0) {
140
- return;
141
- }
142
- await Promise.all(input.conversationIds.map(async (conversationId) => {
143
- const persistedConfig = await loadHostSessionConfig({
144
- conversationId,
145
- agentId: input.agentId,
146
- extraStringFields: ['permissionMode', 'effort'],
147
- });
148
- const liveConfig = input.liveSessionConfigByConversation?.get(conversationId) ?? null;
149
- const mergedConfig = {
150
- ...(persistedConfig ?? {}),
151
- ...(liveConfig ?? {}),
152
- };
153
- const snapshot = buildAgentSessionSnapshot({
154
- conversationId,
155
- agentId: input.agentId,
156
- runtime: {
157
- ...input.runtime,
158
- clientType: input.clientType,
159
- hostMode: true,
160
- },
161
- sessionConfig: {
162
- ...(mergedConfig.model ? { model: mergedConfig.model } : {}),
163
- ...(mergedConfig.permissionMode ? { permissionMode: mergedConfig.permissionMode } : {}),
164
- ...(mergedConfig.effort ? { effort: mergedConfig.effort } : {}),
165
- ...(mergedConfig.runtimeControlValues
166
- ? { runtimeControlValues: mergedConfig.runtimeControlValues }
167
- : {}),
168
- ...(mergedConfig.workspaceId ? { workspaceId: mergedConfig.workspaceId } : {}),
169
- ...(mergedConfig.executionMode ? { executionMode: mergedConfig.executionMode } : {}),
170
- },
171
- lastHeartbeatAt: undefined,
172
- });
173
- let executionBranch = liveConfig?.executionBranch ?? null;
174
- if (!executionBranch && snapshot.executionMode === 'worktree' && snapshot.workspaceId) {
175
- const workspace = input.workspaceOptions.find((option) => option.id === snapshot.workspaceId);
176
- if (workspace) {
177
- executionBranch = buildConversationWorktreeSpec({
178
- agentId: input.agentId,
179
- conversationId,
180
- workspaceCwd: workspace.cwd,
181
- }).branch;
182
- }
183
- }
184
- return patchAgentSessionSnapshot(conversationId, input.agentId, {
185
- clientType: input.clientType,
186
- hostMode: true,
187
- model: snapshot.model ?? null,
188
- permissionMode: snapshot.permissionMode ?? null,
189
- effort: snapshot.effort ?? null,
190
- runtimeControlValues: snapshot.runtimeControlValues ?? null,
191
- workspaceId: snapshot.workspaceId ?? null,
192
- executionMode: snapshot.executionMode ?? null,
193
- executionBranch,
194
- modelOptions: snapshot.modelOptions,
195
- permissionModeOptions: snapshot.permissionModeOptions,
196
- workspaceOptions: snapshot.workspaceOptions,
197
- availableExecutionModes: snapshot.availableExecutionModes,
198
- lastHeartbeatAt: { '.sv': 'timestamp' },
199
- });
200
- }));
201
- }
202
- export function readHostSessionConfig(raw, extraStringFields = []) {
203
- const baseConfig = readSessionWorkspaceConfig(raw);
204
- if (!raw || typeof raw !== 'object') {
205
- return baseConfig;
206
- }
207
- const data = raw;
208
- const extraConfig = Object.fromEntries(extraStringFields.flatMap((field) => {
209
- const value = normalizeOptionalString(data[field]);
210
- return value ? [[field, value]] : [];
211
- }));
212
- const runtimeControlValues = Object.fromEntries(Object.entries(data.runtimeControlValues && typeof data.runtimeControlValues === 'object'
213
- ? data.runtimeControlValues
214
- : {}).flatMap(([key, value]) => {
215
- const normalizedValue = normalizeOptionalString(value);
216
- return normalizedValue ? [[key, normalizedValue]] : [];
217
- }));
218
- return {
219
- ...(baseConfig ?? {}),
220
- ...extraConfig,
221
- ...(Object.keys(runtimeControlValues).length > 0 ? { runtimeControlValues } : {}),
222
- };
223
- }
224
- export async function loadHostSessionConfig(input) {
225
- const raw = await rtdbRead(`/session-config/${input.conversationId}/${input.agentId}`);
226
- return readHostSessionConfig(raw, input.extraStringFields);
227
- }
228
- export function resolveHostWorkspaceCwd(input) {
229
- return resolveConfiguredWorkspaceCwd(input);
230
- }
231
- export function createConversationMetadataLoader(input) {
232
- const cacheTtlMs = input.cacheTtlMs ?? 10_000;
233
- let conversationCacheLoadedAt = 0;
234
- async function refreshConversationCache(force = false) {
235
- if (!force
236
- && input.conversationCache.size > 0
237
- && Date.now() - conversationCacheLoadedAt < cacheTtlMs) {
238
- return;
239
- }
240
- const conversations = await input.client.getConversations();
241
- input.conversationCache.clear();
242
- for (const conversation of conversations) {
243
- input.conversationCache.set(conversation.id, conversation);
244
- }
245
- conversationCacheLoadedAt = Date.now();
246
- }
247
- async function getConversationMeta(conversationId) {
248
- try {
249
- await refreshConversationCache();
250
- const cached = input.conversationCache.get(conversationId);
251
- if (cached)
252
- return cached;
253
- await refreshConversationCache(true);
254
- return input.conversationCache.get(conversationId) ?? null;
255
- }
256
- catch {
257
- return input.conversationCache.get(conversationId) ?? null;
258
- }
259
- }
260
- return {
261
- refreshConversationCache,
262
- getConversationMeta,
263
- };
264
- }