@canonmsg/claude-code-plugin 0.12.1 → 0.12.3

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,8 +796,11 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
862
796
  }, FINAL_DELIVERY_RETRY_MS);
863
797
  }
864
798
  function writeState() {
865
- writeSessionState(conversationId, agentId, {
866
- ...session.state,
799
+ runtimeState.writeSessionState(conversationId, {
800
+ model: session.state.model,
801
+ permissionMode: session.state.permissionMode,
802
+ effort: session.state.effort,
803
+ contextUsage: session.state.contextUsage,
867
804
  ...(session.availableModels.length ? { availableModels: session.availableModels } : {}),
868
805
  cwd,
869
806
  executionMode: session.environment.mode,
@@ -877,7 +814,7 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
877
814
  }).catch(() => { });
878
815
  }
879
816
  function writeTurn() {
880
- writeTurnState(conversationId, agentId, {
817
+ runtimeState.writeTurnState(conversationId, {
881
818
  turnId: session.currentTurnId,
882
819
  state: session.turnState,
883
820
  queueDepth: session.pendingInputs.length,
@@ -1118,7 +1055,7 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
1118
1055
  parent_tool_use_id: null,
1119
1056
  });
1120
1057
  }
1121
- // Write available models to session state
1058
+ // Refresh the canonical runtime descriptor after Claude reports supported models.
1122
1059
  try {
1123
1060
  const models = await q.supportedModels();
1124
1061
  const modelList = toModelOptions(models);
@@ -1138,9 +1075,11 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
1138
1075
  executionModes: config.availableExecutionModes,
1139
1076
  }),
1140
1077
  });
1141
- writeSessionState(conversationId, agentId, {
1142
- ...session.state,
1143
- availableModels: modelList,
1078
+ runtimeState.writeSessionState(conversationId, {
1079
+ model: session.state.model,
1080
+ permissionMode: session.state.permissionMode,
1081
+ effort: session.state.effort,
1082
+ contextUsage: session.state.contextUsage,
1144
1083
  cwd,
1145
1084
  executionMode: session.environment.mode,
1146
1085
  ...(session.environment.branch ? { executionBranch: session.environment.branch } : {}),
@@ -1244,6 +1183,11 @@ export async function main() {
1244
1183
  lastStartedAt: new Date().toISOString(),
1245
1184
  lastHeartbeatAt: new Date().toISOString(),
1246
1185
  });
1186
+ const runtimeState = createRuntimeStatePublisher({
1187
+ agentId,
1188
+ clientType: 'claude-code',
1189
+ hostMode: true,
1190
+ });
1247
1191
  let streamConnected = false;
1248
1192
  const hostAvailableExecutionModes = [
1249
1193
  ...EXECUTION_ENVIRONMENT_MODES,
@@ -1294,6 +1238,7 @@ export async function main() {
1294
1238
  runtime: runtimeDescriptor,
1295
1239
  workspaceOptions,
1296
1240
  defaultCwd: workingDir,
1241
+ extraSessionConfigFields: ['permissionMode', 'effort'],
1297
1242
  liveSessionConfigByConversation: new Map(Array.from(sessions.values()).map((session) => {
1298
1243
  const workspaceId = resolveWorkspaceIdForBaseCwd(session.environment.baseCwd);
1299
1244
  return [
@@ -1317,13 +1262,11 @@ export async function main() {
1317
1262
  ? resolveWorkspaceIdForBaseCwd(session.environment.baseCwd)
1318
1263
  : runtimeDescriptor.defaultWorkspaceId;
1319
1264
  const workspace = workspaceOptions.find((option) => option.id === workspaceId) ?? null;
1265
+ const descriptor = runtimeDescriptor.runtimeDescriptor;
1266
+ if (!descriptor)
1267
+ return;
1320
1268
  const payload = buildClaudeRuntimeInfo({
1321
- descriptor: runtimeDescriptor.runtimeDescriptor ?? buildClaudeRuntimeDescriptor({
1322
- models: runtimeModels,
1323
- workspaces: buildPublicWorkspaceOptions(workspaceOptions),
1324
- workspaceRoots: workspaceRootMetadata,
1325
- executionModes: hostAvailableExecutionModes,
1326
- }),
1269
+ descriptor,
1327
1270
  metadata,
1328
1271
  workspaceLabel: workspace?.label ?? workspaceId ?? null,
1329
1272
  resolvedCwd: session?.cwd ?? workspace?.cwd ?? workingDir,
@@ -1334,7 +1277,7 @@ export async function main() {
1334
1277
  worktreePath: session?.environment.worktreePath ?? null,
1335
1278
  fallbackReason: resolveExecutionFallbackReason(session?.environment),
1336
1279
  });
1337
- await writeRuntimeInfo(conversationId, agentId, payload);
1280
+ await runtimeState.writeRuntimeInfo(conversationId, payload);
1338
1281
  })).catch((error) => {
1339
1282
  console.error('[canon-host] Failed to publish runtime info:', error);
1340
1283
  });
@@ -1446,7 +1389,7 @@ export async function main() {
1446
1389
  if (session.streamingTimer)
1447
1390
  clearTimeout(session.streamingTimer);
1448
1391
  session.streamingText = '';
1449
- rtdbWrite(`/streaming/${conversationId}/${agentId}`, null).catch(() => { });
1392
+ runtimeState.clearStreaming(conversationId).catch(() => { });
1450
1393
  if (session.idleResetTimer) {
1451
1394
  clearTimeout(session.idleResetTimer);
1452
1395
  session.idleResetTimer = null;
@@ -1455,8 +1398,8 @@ export async function main() {
1455
1398
  clearTimeout(session.finalDeliveryTimer);
1456
1399
  session.finalDeliveryTimer = null;
1457
1400
  }
1458
- clearSessionState(conversationId, agentId).catch(() => { });
1459
- clearTurnState(conversationId, agentId).catch(() => { });
1401
+ runtimeState.clearSessionState(conversationId).catch(() => { });
1402
+ runtimeState.clearTurnState(conversationId).catch(() => { });
1460
1403
  client.setTyping(conversationId, false).catch(() => { });
1461
1404
  try {
1462
1405
  session.query.close();
@@ -1510,7 +1453,7 @@ export async function main() {
1510
1453
  executionMode: environment.mode,
1511
1454
  });
1512
1455
  const resumeId = savedSessionIds.get(resumeKey) ?? persisted?.claudeSessionId;
1513
- const session = createSession(conversationId, environment, agentId, client, (convoId, sdkSessionId) => {
1456
+ const session = createSession(conversationId, environment, agentId, client, runtimeState, (convoId, sdkSessionId) => {
1514
1457
  // Called when session processing loop ends — save session ID for resume
1515
1458
  const existing = sessions.get(convoId);
1516
1459
  if (existing) {
@@ -1533,8 +1476,8 @@ export async function main() {
1533
1476
  }
1534
1477
  }
1535
1478
  sessions.delete(convoId);
1536
- clearSessionState(convoId, agentId).catch(() => { });
1537
- clearTurnState(convoId, agentId).catch(() => { });
1479
+ runtimeState.clearSessionState(convoId).catch(() => { });
1480
+ runtimeState.clearTurnState(convoId).catch(() => { });
1538
1481
  }, (descriptor) => {
1539
1482
  runtimeDescriptor = descriptor;
1540
1483
  void publishRuntimeHeartbeat();
@@ -1794,7 +1737,7 @@ export async function main() {
1794
1737
  },
1795
1738
  onDisconnected: () => {
1796
1739
  streamConnected = false;
1797
- rtdbWrite(`/agent-runtime/${agentId}`, null).catch(() => { });
1740
+ runtimeState.clearAgentRuntime().catch(() => { });
1798
1741
  console.error('[canon-host] SSE disconnected');
1799
1742
  },
1800
1743
  onError: (err) => console.error(`[canon-host] SSE error: ${err.message}`),
@@ -1852,9 +1795,11 @@ export async function main() {
1852
1795
  console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to set effort:`, err);
1853
1796
  }
1854
1797
  }
1855
- writeSessionState(convoId, agentId, {
1856
- ...session.state,
1857
- ...(session.availableModels.length ? { availableModels: session.availableModels } : {}),
1798
+ runtimeState.writeSessionState(convoId, {
1799
+ model: session.state.model,
1800
+ permissionMode: session.state.permissionMode,
1801
+ effort: session.state.effort,
1802
+ contextUsage: session.state.contextUsage,
1858
1803
  cwd: session.cwd,
1859
1804
  executionMode: session.environment.mode,
1860
1805
  ...(session.environment.branch ? { executionBranch: session.environment.branch } : {}),
@@ -1880,7 +1825,7 @@ export async function main() {
1880
1825
  console.error(`[canon-host] [${convoId.slice(0, 8)}] Interrupt signal`);
1881
1826
  try {
1882
1827
  session.turnState = 'interrupted';
1883
- writeTurnState(convoId, agentId, {
1828
+ runtimeState.writeTurnState(convoId, {
1884
1829
  turnId: session.currentTurnId,
1885
1830
  state: 'interrupted',
1886
1831
  queueDepth: session.pendingInputs.length,
@@ -1896,7 +1841,7 @@ export async function main() {
1896
1841
  session.streamingTimer = null;
1897
1842
  }
1898
1843
  session.streamingText = '';
1899
- rtdbWrite(`/streaming/${convoId}/${agentId}`, null).catch(() => { });
1844
+ runtimeState.clearStreaming(convoId).catch(() => { });
1900
1845
  client.setTyping(convoId, false).catch(() => { });
1901
1846
  }
1902
1847
  catch (err) {
@@ -1917,9 +1862,11 @@ export async function main() {
1917
1862
  // ── Heartbeat (per-session) ──
1918
1863
  const heartbeat = setInterval(() => {
1919
1864
  for (const session of sessions.values()) {
1920
- writeSessionState(session.conversationId, agentId, {
1921
- ...session.state,
1922
- ...(session.availableModels.length ? { availableModels: session.availableModels } : {}),
1865
+ runtimeState.writeSessionState(session.conversationId, {
1866
+ model: session.state.model,
1867
+ permissionMode: session.state.permissionMode,
1868
+ effort: session.state.effort,
1869
+ contextUsage: session.state.contextUsage,
1923
1870
  cwd: session.cwd,
1924
1871
  executionMode: session.environment.mode,
1925
1872
  ...(session.environment.branch ? { executionBranch: session.environment.branch } : {}),
@@ -1927,7 +1874,7 @@ export async function main() {
1927
1874
  hostMode: true,
1928
1875
  isActive: true,
1929
1876
  }).catch(() => { });
1930
- writeTurnState(session.conversationId, agentId, {
1877
+ runtimeState.writeTurnState(session.conversationId, {
1931
1878
  turnId: session.currentTurnId,
1932
1879
  state: session.turnState,
1933
1880
  queueDepth: session.pendingInputs.length,
@@ -1962,7 +1909,7 @@ export async function main() {
1962
1909
  clearInterval(heartbeat);
1963
1910
  clearInterval(idleCheck);
1964
1911
  stream.stop();
1965
- await rtdbWrite(`/agent-runtime/${agentId}`, null).catch(() => { });
1912
+ await runtimeState.clearAgentRuntime().catch(() => { });
1966
1913
  for (const convoId of [...sessions.keys()]) {
1967
1914
  closeSession(convoId);
1968
1915
  }
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.3",
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": "^1.0.0",
35
+ "@canonmsg/core": "^0.15.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
- }