@canonmsg/claude-code-plugin 0.12.0 → 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/README.md +11 -1
- package/dist/host.js +223 -107
- package/dist/register.js +23 -19
- package/dist/server.js +37 -3
- package/package.json +3 -3
- package/dist/host-runtime.d.ts +0 -134
- package/dist/host-runtime.js +0 -266
package/README.md
CHANGED
|
@@ -15,7 +15,15 @@ canon-register --name "My Claude" --description "My Claude Code agent" --phone "
|
|
|
15
15
|
canon-claude
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
Send a message to your agent in the Canon app. Claude Code responds with full capabilities (file read/write, bash, etc.).
|
|
18
|
+
Send a message to your agent in the Canon app. Claude Code responds with full capabilities (file read/write, bash, etc.). Keep the `canon-claude` terminal open; closing it takes this local agent offline.
|
|
19
|
+
|
|
20
|
+
After a restart or closed terminal, use the shared local manager to list and revive all recorded local agents:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g @canonmsg/local-agents
|
|
24
|
+
canon-necromance
|
|
25
|
+
canon-necromance revive frontend
|
|
26
|
+
```
|
|
19
27
|
|
|
20
28
|
## Features
|
|
21
29
|
|
|
@@ -60,6 +68,8 @@ CANON_AGENT=frontend canon-claude --cwd ~/projects/frontend
|
|
|
60
68
|
|
|
61
69
|
For a lighter integration without session controls, run Canon as a channel plugin inside your own Claude Code session. See [full setup guide](../../docs/claude-code-setup.md#channel-mode-alternative).
|
|
62
70
|
|
|
71
|
+
Channel mode is embedded inside Claude Code and is shown as non-revivable by `canon-necromance`. Use `canon-claude` host mode when you want phone-controlled runtime sessions.
|
|
72
|
+
|
|
63
73
|
## Development
|
|
64
74
|
|
|
65
75
|
```bash
|
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,
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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,10 +143,7 @@ function buildClaudeRuntimeDescriptor(input) {
|
|
|
204
143
|
dispatch: { kind: 'signal', signal: 'interrupt' },
|
|
205
144
|
},
|
|
206
145
|
],
|
|
207
|
-
|
|
208
|
-
supportsInterrupt: true,
|
|
209
|
-
streamingTextMode: 'delta',
|
|
210
|
-
};
|
|
146
|
+
});
|
|
211
147
|
}
|
|
212
148
|
function normalizeClaudeEffortLevel(value) {
|
|
213
149
|
return value === 'low' || value === 'medium' || value === 'high'
|
|
@@ -549,7 +485,7 @@ async function buildCanonUserContent(input) {
|
|
|
549
485
|
];
|
|
550
486
|
}
|
|
551
487
|
// ── Session factory ─────────────────────────────────────────────────
|
|
552
|
-
function createSession(conversationId, environment, agentId, client, onSessionEnd, onRuntimeDescriptorUpdate, config, resumeSessionId) {
|
|
488
|
+
function createSession(conversationId, environment, agentId, client, runtimeState, onSessionEnd, onRuntimeDescriptorUpdate, config, resumeSessionId) {
|
|
553
489
|
const { cwd } = environment;
|
|
554
490
|
// Per-session input stream with message queue
|
|
555
491
|
let resolveInput = null;
|
|
@@ -756,10 +692,9 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
|
|
|
756
692
|
session.streamingTimer = timer;
|
|
757
693
|
},
|
|
758
694
|
onWrite: (text) => {
|
|
759
|
-
|
|
695
|
+
runtimeState.writeStreaming(conversationId, {
|
|
760
696
|
text,
|
|
761
697
|
status: 'streaming',
|
|
762
|
-
updatedAt: { '.sv': 'timestamp' },
|
|
763
698
|
}).catch(() => { });
|
|
764
699
|
},
|
|
765
700
|
});
|
|
@@ -772,7 +707,7 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
|
|
|
772
707
|
}
|
|
773
708
|
function clearStreaming() {
|
|
774
709
|
streamingBuffer.clear();
|
|
775
|
-
|
|
710
|
+
runtimeState.clearStreaming(conversationId).catch(() => { });
|
|
776
711
|
}
|
|
777
712
|
function clearIdleResetTimer() {
|
|
778
713
|
if (!session.idleResetTimer)
|
|
@@ -861,7 +796,7 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
|
|
|
861
796
|
}, FINAL_DELIVERY_RETRY_MS);
|
|
862
797
|
}
|
|
863
798
|
function writeState() {
|
|
864
|
-
writeSessionState(conversationId,
|
|
799
|
+
runtimeState.writeSessionState(conversationId, {
|
|
865
800
|
...session.state,
|
|
866
801
|
...(session.availableModels.length ? { availableModels: session.availableModels } : {}),
|
|
867
802
|
cwd,
|
|
@@ -876,7 +811,7 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
|
|
|
876
811
|
}).catch(() => { });
|
|
877
812
|
}
|
|
878
813
|
function writeTurn() {
|
|
879
|
-
writeTurnState(conversationId,
|
|
814
|
+
runtimeState.writeTurnState(conversationId, {
|
|
880
815
|
turnId: session.currentTurnId,
|
|
881
816
|
state: session.turnState,
|
|
882
817
|
queueDepth: session.pendingInputs.length,
|
|
@@ -1137,7 +1072,7 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
|
|
|
1137
1072
|
executionModes: config.availableExecutionModes,
|
|
1138
1073
|
}),
|
|
1139
1074
|
});
|
|
1140
|
-
writeSessionState(conversationId,
|
|
1075
|
+
runtimeState.writeSessionState(conversationId, {
|
|
1141
1076
|
...session.state,
|
|
1142
1077
|
availableModels: modelList,
|
|
1143
1078
|
cwd,
|
|
@@ -1158,7 +1093,13 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
|
|
|
1158
1093
|
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Session ${resumeSessionId ? 'resumed' : 'started'} `
|
|
1159
1094
|
+ `(mode: ${environment.mode}, cwd: ${cwd})`);
|
|
1160
1095
|
};
|
|
1161
|
-
init().catch((err) =>
|
|
1096
|
+
init().catch((err) => {
|
|
1097
|
+
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Init failed:`, err);
|
|
1098
|
+
if (resumeSessionId) {
|
|
1099
|
+
session.sdkSessionId = null;
|
|
1100
|
+
onSessionEnd(conversationId, null);
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1162
1103
|
return session;
|
|
1163
1104
|
}
|
|
1164
1105
|
// ── Main ────────────────────────────────────────────────────────────
|
|
@@ -1186,10 +1127,10 @@ export async function main() {
|
|
|
1186
1127
|
for (const warning of workspaceDiscovery.warnings) {
|
|
1187
1128
|
console.error(`[canon-host] ${warning}`);
|
|
1188
1129
|
}
|
|
1189
|
-
const { apiKey, agentId: profileAgentId, profile } = resolveCanonAgent({ logPrefix: '[canon-host]' });
|
|
1130
|
+
const { apiKey, agentId: profileAgentId, agentName: profileAgentName, profile, baseUrl, lockHandle, } = resolveCanonAgent({ logPrefix: '[canon-host]', expectedClientType: 'claude-code' });
|
|
1190
1131
|
console.error(`[canon-host] Starting${profile ? ` (profile: ${profile})` : ''} in ${workingDir}`);
|
|
1191
1132
|
// ── Canon API client + RTDB auth ──
|
|
1192
|
-
const client = new CanonClient(apiKey);
|
|
1133
|
+
const client = new CanonClient(apiKey, baseUrl);
|
|
1193
1134
|
initRTDBAuth(client);
|
|
1194
1135
|
let agentId;
|
|
1195
1136
|
let ownerId = null;
|
|
@@ -1209,6 +1150,39 @@ export async function main() {
|
|
|
1209
1150
|
}
|
|
1210
1151
|
console.error(`[canon-host] Authenticated as ${agentId}`);
|
|
1211
1152
|
}
|
|
1153
|
+
const launchArgs = [...process.argv.slice(2)];
|
|
1154
|
+
if (!launchArgs.some((arg) => arg === '--cwd' || arg.startsWith('--cwd='))) {
|
|
1155
|
+
launchArgs.push('--cwd', workingDir);
|
|
1156
|
+
}
|
|
1157
|
+
const runtimeId = buildLocalRuntimeId({
|
|
1158
|
+
runtime: 'claude-code',
|
|
1159
|
+
profile,
|
|
1160
|
+
cwd: workingDir,
|
|
1161
|
+
launchCommand: ['canon-claude', ...launchArgs],
|
|
1162
|
+
});
|
|
1163
|
+
upsertLocalRuntimeEntry({
|
|
1164
|
+
id: runtimeId,
|
|
1165
|
+
runtime: 'claude-code',
|
|
1166
|
+
profile,
|
|
1167
|
+
agentId,
|
|
1168
|
+
agentName: profileAgentName,
|
|
1169
|
+
cwd: workingDir,
|
|
1170
|
+
baseCwd: workingDir,
|
|
1171
|
+
workspaceRoots: workspaceRoots.map((root) => root.cwd),
|
|
1172
|
+
workspaces: workspaceOptions.map((workspace) => workspace.cwd),
|
|
1173
|
+
launchCommand: ['canon-claude', ...launchArgs],
|
|
1174
|
+
pid: process.pid,
|
|
1175
|
+
status: profile ? 'running' : 'manual',
|
|
1176
|
+
reviveCapability: profile ? 'revivable' : 'manual',
|
|
1177
|
+
surfaceMode: 'host',
|
|
1178
|
+
lastStartedAt: new Date().toISOString(),
|
|
1179
|
+
lastHeartbeatAt: new Date().toISOString(),
|
|
1180
|
+
});
|
|
1181
|
+
const runtimeState = createRuntimeStatePublisher({
|
|
1182
|
+
agentId,
|
|
1183
|
+
clientType: 'claude-code',
|
|
1184
|
+
hostMode: true,
|
|
1185
|
+
});
|
|
1212
1186
|
let streamConnected = false;
|
|
1213
1187
|
const hostAvailableExecutionModes = [
|
|
1214
1188
|
...EXECUTION_ENVIRONMENT_MODES,
|
|
@@ -1230,6 +1204,12 @@ export async function main() {
|
|
|
1230
1204
|
const knownConversationIds = new Set();
|
|
1231
1205
|
let lastKnownConversationRefreshAt = 0;
|
|
1232
1206
|
const publishRuntimeHeartbeat = async () => {
|
|
1207
|
+
heartbeatLocalRuntimeEntry(runtimeId, {
|
|
1208
|
+
agentId,
|
|
1209
|
+
agentName: profileAgentName,
|
|
1210
|
+
cwd: workingDir,
|
|
1211
|
+
baseCwd: workingDir,
|
|
1212
|
+
});
|
|
1233
1213
|
if (!streamConnected)
|
|
1234
1214
|
return;
|
|
1235
1215
|
await refreshKnownConversationIds().catch((error) => {
|
|
@@ -1253,6 +1233,7 @@ export async function main() {
|
|
|
1253
1233
|
runtime: runtimeDescriptor,
|
|
1254
1234
|
workspaceOptions,
|
|
1255
1235
|
defaultCwd: workingDir,
|
|
1236
|
+
extraSessionConfigFields: ['permissionMode', 'effort'],
|
|
1256
1237
|
liveSessionConfigByConversation: new Map(Array.from(sessions.values()).map((session) => {
|
|
1257
1238
|
const workspaceId = resolveWorkspaceIdForBaseCwd(session.environment.baseCwd);
|
|
1258
1239
|
return [
|
|
@@ -1293,7 +1274,7 @@ export async function main() {
|
|
|
1293
1274
|
worktreePath: session?.environment.worktreePath ?? null,
|
|
1294
1275
|
fallbackReason: resolveExecutionFallbackReason(session?.environment),
|
|
1295
1276
|
});
|
|
1296
|
-
await writeRuntimeInfo(conversationId,
|
|
1277
|
+
await runtimeState.writeRuntimeInfo(conversationId, payload);
|
|
1297
1278
|
})).catch((error) => {
|
|
1298
1279
|
console.error('[canon-host] Failed to publish runtime info:', error);
|
|
1299
1280
|
});
|
|
@@ -1395,11 +1376,17 @@ export async function main() {
|
|
|
1395
1376
|
// Save SDK session ID for future resume
|
|
1396
1377
|
if (session.sdkSessionId) {
|
|
1397
1378
|
savedSessionIds.set(buildConversationEnvironmentKey(conversationId, session.environment.baseCwd), session.sdkSessionId);
|
|
1379
|
+
saveRuntimeSessionState(runtimeId, {
|
|
1380
|
+
conversationId,
|
|
1381
|
+
baseCwd: session.environment.baseCwd,
|
|
1382
|
+
executionMode: session.environment.mode,
|
|
1383
|
+
claudeSessionId: session.sdkSessionId,
|
|
1384
|
+
});
|
|
1398
1385
|
}
|
|
1399
1386
|
if (session.streamingTimer)
|
|
1400
1387
|
clearTimeout(session.streamingTimer);
|
|
1401
1388
|
session.streamingText = '';
|
|
1402
|
-
|
|
1389
|
+
runtimeState.clearStreaming(conversationId).catch(() => { });
|
|
1403
1390
|
if (session.idleResetTimer) {
|
|
1404
1391
|
clearTimeout(session.idleResetTimer);
|
|
1405
1392
|
session.idleResetTimer = null;
|
|
@@ -1408,8 +1395,8 @@ export async function main() {
|
|
|
1408
1395
|
clearTimeout(session.finalDeliveryTimer);
|
|
1409
1396
|
session.finalDeliveryTimer = null;
|
|
1410
1397
|
}
|
|
1411
|
-
clearSessionState(conversationId
|
|
1412
|
-
clearTurnState(conversationId
|
|
1398
|
+
runtimeState.clearSessionState(conversationId).catch(() => { });
|
|
1399
|
+
runtimeState.clearTurnState(conversationId).catch(() => { });
|
|
1413
1400
|
client.setTyping(conversationId, false).catch(() => { });
|
|
1414
1401
|
try {
|
|
1415
1402
|
session.query.close();
|
|
@@ -1457,19 +1444,37 @@ export async function main() {
|
|
|
1457
1444
|
});
|
|
1458
1445
|
try {
|
|
1459
1446
|
const resumeKey = buildConversationEnvironmentKey(conversationId, environment.baseCwd);
|
|
1460
|
-
const
|
|
1461
|
-
|
|
1447
|
+
const persisted = loadRuntimeSessionState(runtimeId, {
|
|
1448
|
+
conversationId,
|
|
1449
|
+
baseCwd: environment.baseCwd,
|
|
1450
|
+
executionMode: environment.mode,
|
|
1451
|
+
});
|
|
1452
|
+
const resumeId = savedSessionIds.get(resumeKey) ?? persisted?.claudeSessionId;
|
|
1453
|
+
const session = createSession(conversationId, environment, agentId, client, runtimeState, (convoId, sdkSessionId) => {
|
|
1462
1454
|
// Called when session processing loop ends — save session ID for resume
|
|
1463
1455
|
const existing = sessions.get(convoId);
|
|
1464
1456
|
if (existing) {
|
|
1465
1457
|
releaseConversationEnvironment(existing.environment);
|
|
1466
1458
|
if (sdkSessionId) {
|
|
1467
1459
|
savedSessionIds.set(buildConversationEnvironmentKey(convoId, existing.environment.baseCwd), sdkSessionId);
|
|
1460
|
+
saveRuntimeSessionState(runtimeId, {
|
|
1461
|
+
conversationId: convoId,
|
|
1462
|
+
baseCwd: existing.environment.baseCwd,
|
|
1463
|
+
executionMode: existing.environment.mode,
|
|
1464
|
+
claudeSessionId: sdkSessionId,
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
else {
|
|
1468
|
+
clearRuntimeSessionState(runtimeId, {
|
|
1469
|
+
conversationId: convoId,
|
|
1470
|
+
baseCwd: existing.environment.baseCwd,
|
|
1471
|
+
executionMode: existing.environment.mode,
|
|
1472
|
+
});
|
|
1468
1473
|
}
|
|
1469
1474
|
}
|
|
1470
1475
|
sessions.delete(convoId);
|
|
1471
|
-
clearSessionState(convoId
|
|
1472
|
-
clearTurnState(convoId
|
|
1476
|
+
runtimeState.clearSessionState(convoId).catch(() => { });
|
|
1477
|
+
runtimeState.clearTurnState(convoId).catch(() => { });
|
|
1473
1478
|
}, (descriptor) => {
|
|
1474
1479
|
runtimeDescriptor = descriptor;
|
|
1475
1480
|
void publishRuntimeHeartbeat();
|
|
@@ -1496,6 +1501,112 @@ export async function main() {
|
|
|
1496
1501
|
pendingSessionCreations.delete(conversationId);
|
|
1497
1502
|
}
|
|
1498
1503
|
}
|
|
1504
|
+
async function enqueueRecoveredInboundMessage(input) {
|
|
1505
|
+
const m = input.message;
|
|
1506
|
+
if (m.senderId === agentId)
|
|
1507
|
+
return;
|
|
1508
|
+
let materialized = [];
|
|
1509
|
+
if (m.id) {
|
|
1510
|
+
try {
|
|
1511
|
+
materialized = await materializeMessageMedia({
|
|
1512
|
+
id: m.id,
|
|
1513
|
+
attachments: m.attachments ?? [],
|
|
1514
|
+
}, { agentId, conversationId: input.conversationId });
|
|
1515
|
+
}
|
|
1516
|
+
catch (error) {
|
|
1517
|
+
console.error(`[canon-host] [${input.conversationId.slice(0, 8)}] Failed to materialize recovered media:`, error instanceof Error ? error.message : error);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
const sender = m.senderName || m.senderId;
|
|
1521
|
+
const isOwner = m.isOwner ?? (ownerId != null && m.senderId === ownerId);
|
|
1522
|
+
const content = renderInboundContent(m, materialized);
|
|
1523
|
+
const hydrated = await loadHydratedInboundContext({
|
|
1524
|
+
conversationId: input.conversationId,
|
|
1525
|
+
message: m,
|
|
1526
|
+
senderName: sender,
|
|
1527
|
+
isOwner,
|
|
1528
|
+
});
|
|
1529
|
+
const behavior = input.hydratedPage?.behavior ?? hydrated.behavior;
|
|
1530
|
+
const workSessions = input.hydratedPage?.workSessions ?? hydrated.workSessions;
|
|
1531
|
+
const participantContext = hydrated.participantContext;
|
|
1532
|
+
const autoReply = decideAutoReply(participantContext, behavior);
|
|
1533
|
+
if (!autoReply.allow) {
|
|
1534
|
+
console.error(`[canon-host] [${input.conversationId.slice(0, 8)}] Suppressed recovered auto-reply: ${autoReply.reason}`);
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
let session;
|
|
1538
|
+
try {
|
|
1539
|
+
session = await getOrCreateSession(input.conversationId);
|
|
1540
|
+
}
|
|
1541
|
+
catch (error) {
|
|
1542
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1543
|
+
const userMessage = error instanceof ExecutionEnvironmentError ? error.userMessage : message;
|
|
1544
|
+
console.error(`[canon-host] [${input.conversationId.slice(0, 8)}] Failed to create recovered session: ${message}`);
|
|
1545
|
+
await client.sendMessage(input.conversationId, `I couldn't start a coding session for this workspace: ${userMessage}`, {
|
|
1546
|
+
metadata: {
|
|
1547
|
+
turnSemantics: 'turn_complete',
|
|
1548
|
+
turnComplete: true,
|
|
1549
|
+
},
|
|
1550
|
+
}).catch(() => { });
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
const turnMetadata = normalizeTurnMetadata(m.metadata);
|
|
1554
|
+
const deliveryIntent = turnMetadata?.deliveryIntent ?? 'queue';
|
|
1555
|
+
const shouldMarkAccepted = turnMetadata?.inboundDisposition === 'queued';
|
|
1556
|
+
const promptText = buildCanonPrompt({
|
|
1557
|
+
content,
|
|
1558
|
+
conversationId: input.conversationId,
|
|
1559
|
+
participantContext,
|
|
1560
|
+
behavior,
|
|
1561
|
+
workSession: m.workSession,
|
|
1562
|
+
workSessions,
|
|
1563
|
+
});
|
|
1564
|
+
const messageContent = await buildCanonUserContent({ promptText, materialized });
|
|
1565
|
+
session.enqueueInbound({
|
|
1566
|
+
type: 'user',
|
|
1567
|
+
message: {
|
|
1568
|
+
role: 'user',
|
|
1569
|
+
content: messageContent,
|
|
1570
|
+
},
|
|
1571
|
+
parent_tool_use_id: null,
|
|
1572
|
+
}, deliveryIntent, m.id ?? null, shouldMarkAccepted);
|
|
1573
|
+
if (m.id) {
|
|
1574
|
+
saveRuntimeSessionState(runtimeId, {
|
|
1575
|
+
conversationId: input.conversationId,
|
|
1576
|
+
baseCwd: workingDir,
|
|
1577
|
+
lastInboundMessageId: m.id,
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
try {
|
|
1582
|
+
for (const conversation of conversationCache.values()) {
|
|
1583
|
+
const cursor = loadRuntimeSessionState(runtimeId, {
|
|
1584
|
+
conversationId: conversation.id,
|
|
1585
|
+
baseCwd: workingDir,
|
|
1586
|
+
})?.lastInboundMessageId;
|
|
1587
|
+
const page = await client.getMessagesPage(conversation.id, 25);
|
|
1588
|
+
const inboundMessages = page.messages
|
|
1589
|
+
.filter((message) => message.senderId !== agentId)
|
|
1590
|
+
.sort((a, b) => String(a.createdAt ?? '').localeCompare(String(b.createdAt ?? '')));
|
|
1591
|
+
const cursorIndex = cursor
|
|
1592
|
+
? inboundMessages.findIndex((message) => message.id === cursor)
|
|
1593
|
+
: -1;
|
|
1594
|
+
const messagesToRecover = cursorIndex >= 0
|
|
1595
|
+
? inboundMessages.slice(cursorIndex + 1)
|
|
1596
|
+
: inboundMessages.slice(-1);
|
|
1597
|
+
for (const message of messagesToRecover) {
|
|
1598
|
+
console.error(`[canon-host] [${conversation.id.slice(0, 8)}] Recovering inbound message on startup`);
|
|
1599
|
+
await enqueueRecoveredInboundMessage({
|
|
1600
|
+
conversationId: conversation.id,
|
|
1601
|
+
message: message,
|
|
1602
|
+
hydratedPage: page,
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
catch (error) {
|
|
1608
|
+
console.error('[canon-host] Failed startup recovery:', error instanceof Error ? error.message : error);
|
|
1609
|
+
}
|
|
1499
1610
|
// ── Connect SSE stream for inbound Canon messages ──
|
|
1500
1611
|
const stream = new CanonStream({
|
|
1501
1612
|
apiKey,
|
|
@@ -1607,6 +1718,13 @@ export async function main() {
|
|
|
1607
1718
|
},
|
|
1608
1719
|
parent_tool_use_id: null,
|
|
1609
1720
|
}, deliveryIntent, m.id ?? null, shouldMarkAccepted);
|
|
1721
|
+
if (m.id) {
|
|
1722
|
+
saveRuntimeSessionState(runtimeId, {
|
|
1723
|
+
conversationId: convoId,
|
|
1724
|
+
baseCwd: workingDir,
|
|
1725
|
+
lastInboundMessageId: m.id,
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1610
1728
|
})();
|
|
1611
1729
|
},
|
|
1612
1730
|
onConnected: () => {
|
|
@@ -1616,7 +1734,7 @@ export async function main() {
|
|
|
1616
1734
|
},
|
|
1617
1735
|
onDisconnected: () => {
|
|
1618
1736
|
streamConnected = false;
|
|
1619
|
-
|
|
1737
|
+
runtimeState.clearAgentRuntime().catch(() => { });
|
|
1620
1738
|
console.error('[canon-host] SSE disconnected');
|
|
1621
1739
|
},
|
|
1622
1740
|
onError: (err) => console.error(`[canon-host] SSE error: ${err.message}`),
|
|
@@ -1674,7 +1792,7 @@ export async function main() {
|
|
|
1674
1792
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to set effort:`, err);
|
|
1675
1793
|
}
|
|
1676
1794
|
}
|
|
1677
|
-
writeSessionState(convoId,
|
|
1795
|
+
runtimeState.writeSessionState(convoId, {
|
|
1678
1796
|
...session.state,
|
|
1679
1797
|
...(session.availableModels.length ? { availableModels: session.availableModels } : {}),
|
|
1680
1798
|
cwd: session.cwd,
|
|
@@ -1702,7 +1820,7 @@ export async function main() {
|
|
|
1702
1820
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Interrupt signal`);
|
|
1703
1821
|
try {
|
|
1704
1822
|
session.turnState = 'interrupted';
|
|
1705
|
-
writeTurnState(convoId,
|
|
1823
|
+
runtimeState.writeTurnState(convoId, {
|
|
1706
1824
|
turnId: session.currentTurnId,
|
|
1707
1825
|
state: 'interrupted',
|
|
1708
1826
|
queueDepth: session.pendingInputs.length,
|
|
@@ -1718,7 +1836,7 @@ export async function main() {
|
|
|
1718
1836
|
session.streamingTimer = null;
|
|
1719
1837
|
}
|
|
1720
1838
|
session.streamingText = '';
|
|
1721
|
-
|
|
1839
|
+
runtimeState.clearStreaming(convoId).catch(() => { });
|
|
1722
1840
|
client.setTyping(convoId, false).catch(() => { });
|
|
1723
1841
|
}
|
|
1724
1842
|
catch (err) {
|
|
@@ -1739,7 +1857,7 @@ export async function main() {
|
|
|
1739
1857
|
// ── Heartbeat (per-session) ──
|
|
1740
1858
|
const heartbeat = setInterval(() => {
|
|
1741
1859
|
for (const session of sessions.values()) {
|
|
1742
|
-
writeSessionState(session.conversationId,
|
|
1860
|
+
runtimeState.writeSessionState(session.conversationId, {
|
|
1743
1861
|
...session.state,
|
|
1744
1862
|
...(session.availableModels.length ? { availableModels: session.availableModels } : {}),
|
|
1745
1863
|
cwd: session.cwd,
|
|
@@ -1749,7 +1867,7 @@ export async function main() {
|
|
|
1749
1867
|
hostMode: true,
|
|
1750
1868
|
isActive: true,
|
|
1751
1869
|
}).catch(() => { });
|
|
1752
|
-
writeTurnState(session.conversationId,
|
|
1870
|
+
runtimeState.writeTurnState(session.conversationId, {
|
|
1753
1871
|
turnId: session.currentTurnId,
|
|
1754
1872
|
state: session.turnState,
|
|
1755
1873
|
queueDepth: session.pendingInputs.length,
|
|
@@ -1784,17 +1902,17 @@ export async function main() {
|
|
|
1784
1902
|
clearInterval(heartbeat);
|
|
1785
1903
|
clearInterval(idleCheck);
|
|
1786
1904
|
stream.stop();
|
|
1787
|
-
await
|
|
1905
|
+
await runtimeState.clearAgentRuntime().catch(() => { });
|
|
1788
1906
|
for (const convoId of [...sessions.keys()]) {
|
|
1789
1907
|
closeSession(convoId);
|
|
1790
1908
|
}
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
releaseLock(ap);
|
|
1909
|
+
markLocalRuntimeStopped(runtimeId);
|
|
1910
|
+
(lockHandle ?? getActiveProfileLock())?.release();
|
|
1794
1911
|
process.exit(0);
|
|
1795
1912
|
};
|
|
1796
1913
|
process.on('SIGINT', shutdown);
|
|
1797
1914
|
process.on('SIGTERM', shutdown);
|
|
1915
|
+
process.on('SIGHUP', shutdown);
|
|
1798
1916
|
console.error('[canon-host] Ready — sessions created on demand');
|
|
1799
1917
|
// Keep alive
|
|
1800
1918
|
await new Promise(() => { });
|
|
@@ -1802,8 +1920,6 @@ export async function main() {
|
|
|
1802
1920
|
// ── Entry ───────────────────────────────────────────────────────────
|
|
1803
1921
|
runCli(import.meta.url, main, (err) => {
|
|
1804
1922
|
console.error('[canon-host] Fatal error:', err);
|
|
1805
|
-
|
|
1806
|
-
if (ap)
|
|
1807
|
-
releaseLock(ap);
|
|
1923
|
+
getActiveProfileLock()?.release();
|
|
1808
1924
|
process.exit(1);
|
|
1809
1925
|
});
|
package/dist/register.js
CHANGED
|
@@ -9,14 +9,10 @@ import { setDefaultResultOrder } from 'node:dns';
|
|
|
9
9
|
*
|
|
10
10
|
* On approval, saves the agent to ~/.canon/agents.json under the profile name.
|
|
11
11
|
*/
|
|
12
|
-
import { registerAndWaitForApproval } from '@canonmsg/core';
|
|
13
|
-
import { readFileSync
|
|
14
|
-
import { join } from 'node:path';
|
|
15
|
-
import { homedir } from 'node:os';
|
|
12
|
+
import { ackRegistrationApproval, clearPendingRegistration, getOrCreatePendingRegistration, registerAndWaitForApproval, updatePendingRegistration, upsertAgentProfile, AGENTS_PATH, } from '@canonmsg/core';
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
16
14
|
import { parseArgs } from 'node:util';
|
|
17
15
|
import { runCli } from './cli-entry.js';
|
|
18
|
-
const CANON_DIR = join(homedir(), '.canon');
|
|
19
|
-
const AGENTS_PATH = join(CANON_DIR, 'agents.json');
|
|
20
16
|
export async function main() {
|
|
21
17
|
setDefaultResultOrder('ipv4first');
|
|
22
18
|
const { values } = parseArgs({
|
|
@@ -44,6 +40,7 @@ export async function main() {
|
|
|
44
40
|
// File doesn't exist yet
|
|
45
41
|
}
|
|
46
42
|
console.log(`Registering agent "${values.name}" (profile: ${profileName})...`);
|
|
43
|
+
const pending = getOrCreatePendingRegistration(profileName, 'claude-code');
|
|
47
44
|
const result = await registerAndWaitForApproval({
|
|
48
45
|
name: values.name,
|
|
49
46
|
description: values.description,
|
|
@@ -52,8 +49,14 @@ export async function main() {
|
|
|
52
49
|
clientType: 'claude-code',
|
|
53
50
|
baseUrl: values['base-url'],
|
|
54
51
|
requestedAgentId: existingAgentId,
|
|
52
|
+
localRegistrationId: pending.localRegistrationId,
|
|
55
53
|
}, {
|
|
56
|
-
onSubmitted: (requestId) => {
|
|
54
|
+
onSubmitted: (requestId, pollToken) => {
|
|
55
|
+
updatePendingRegistration(profileName, {
|
|
56
|
+
requestId,
|
|
57
|
+
pollToken,
|
|
58
|
+
clientType: 'claude-code',
|
|
59
|
+
});
|
|
57
60
|
console.log(`Registration submitted (request ID: ${requestId}).`);
|
|
58
61
|
console.log('Waiting for approval in Canon app...');
|
|
59
62
|
},
|
|
@@ -64,25 +67,26 @@ export async function main() {
|
|
|
64
67
|
console.log(''); // newline after dots
|
|
65
68
|
switch (result.status) {
|
|
66
69
|
case 'approved': {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
profiles = JSON.parse(readFileSync(AGENTS_PATH, 'utf-8'));
|
|
70
|
+
if (!result.apiKey || !result.agentId || !result.agentName) {
|
|
71
|
+
console.error('Approval completed but Canon did not return a usable API key. Run this command again to resume key pickup.');
|
|
72
|
+
process.exit(1);
|
|
72
73
|
}
|
|
73
|
-
|
|
74
|
-
// File doesn't exist yet
|
|
75
|
-
}
|
|
76
|
-
profiles[profileName] = {
|
|
74
|
+
upsertAgentProfile(profileName, {
|
|
77
75
|
apiKey: result.apiKey,
|
|
78
76
|
agentId: result.agentId,
|
|
79
77
|
agentName: result.agentName,
|
|
80
78
|
registeredAt: new Date().toISOString(),
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
clientType: 'claude-code',
|
|
80
|
+
...(typeof values['base-url'] === 'string' ? { baseUrl: values['base-url'] } : {}),
|
|
81
|
+
});
|
|
82
|
+
if (result.requestId) {
|
|
83
|
+
await ackRegistrationApproval(values['base-url'], result.requestId, result.pollToken);
|
|
84
|
+
}
|
|
85
|
+
clearPendingRegistration(profileName);
|
|
83
86
|
console.log(`Approved! Agent: ${result.agentName} (${result.agentId})`);
|
|
84
87
|
console.log(`Saved as profile "${profileName}" in ~/.canon/agents.json`);
|
|
85
|
-
console.log('
|
|
88
|
+
console.log('Start it with: CANON_AGENT=' + profileName + ' canon-claude --cwd /path/to/project');
|
|
89
|
+
console.log('Keep that terminal open. Closing it takes this local agent offline.');
|
|
86
90
|
break;
|
|
87
91
|
}
|
|
88
92
|
case 'rejected':
|
package/dist/server.js
CHANGED
|
@@ -10,7 +10,7 @@ import { setDefaultResultOrder } from 'node:dns';
|
|
|
10
10
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
11
11
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
12
12
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
13
|
-
import { CanonClient, CanonStream, ApprovalManager, loadProfiles, isProfileLocked, resolveCanonAgent, getActiveProfile, releaseLock, initRTDBAuth, writeSessionState, clearSessionState, } from '@canonmsg/core';
|
|
13
|
+
import { CanonClient, CanonStream, ApprovalManager, buildLocalRuntimeId, getActiveProfileLock, markLocalRuntimeStopped, loadProfiles, isProfileLocked, resolveCanonAgent, getActiveProfile, releaseLock, upsertLocalRuntimeEntry, initRTDBAuth, writeSessionState, clearSessionState, } from '@canonmsg/core';
|
|
14
14
|
import { ApprovalHttpServer } from './approval-server.js';
|
|
15
15
|
import { runCli } from './cli-entry.js';
|
|
16
16
|
// ── MCP server ─────────────────────────────────────────────────────────
|
|
@@ -525,10 +525,14 @@ async function handleInboundMessage(payload) {
|
|
|
525
525
|
async function startChannel() {
|
|
526
526
|
let apiKey;
|
|
527
527
|
let profile;
|
|
528
|
+
let baseUrl;
|
|
529
|
+
let profileAgentName;
|
|
528
530
|
try {
|
|
529
|
-
const resolved = resolveCanonAgent({ logPrefix: '[canon]' });
|
|
531
|
+
const resolved = resolveCanonAgent({ logPrefix: '[canon]', expectedClientType: 'claude-code' });
|
|
530
532
|
apiKey = resolved.apiKey;
|
|
531
533
|
profile = resolved.profile;
|
|
534
|
+
baseUrl = resolved.baseUrl;
|
|
535
|
+
profileAgentName = resolved.agentName;
|
|
532
536
|
}
|
|
533
537
|
catch (err) {
|
|
534
538
|
console.error(err instanceof Error ? err.message : err);
|
|
@@ -537,7 +541,7 @@ async function startChannel() {
|
|
|
537
541
|
if (profile) {
|
|
538
542
|
console.error(`[canon] Using agent profile: ${profile}`);
|
|
539
543
|
}
|
|
540
|
-
client = new CanonClient(apiKey);
|
|
544
|
+
client = new CanonClient(apiKey, baseUrl);
|
|
541
545
|
// Get agent identity — try /agents/me first, fall back to /agents/auth-token
|
|
542
546
|
let agentId;
|
|
543
547
|
try {
|
|
@@ -560,6 +564,27 @@ async function startChannel() {
|
|
|
560
564
|
}
|
|
561
565
|
// Initialize RTDB auth for session state reporting
|
|
562
566
|
initRTDBAuth(client);
|
|
567
|
+
const runtimeId = buildLocalRuntimeId({
|
|
568
|
+
runtime: 'claude-code',
|
|
569
|
+
profile,
|
|
570
|
+
cwd: process.cwd(),
|
|
571
|
+
launchCommand: ['canon-channel-server'],
|
|
572
|
+
});
|
|
573
|
+
upsertLocalRuntimeEntry({
|
|
574
|
+
id: runtimeId,
|
|
575
|
+
runtime: 'claude-code',
|
|
576
|
+
profile,
|
|
577
|
+
agentId,
|
|
578
|
+
agentName: agentContext?.displayName ?? profileAgentName,
|
|
579
|
+
cwd: process.cwd(),
|
|
580
|
+
launchCommand: ['canon-channel-server'],
|
|
581
|
+
pid: process.pid,
|
|
582
|
+
status: 'embedded',
|
|
583
|
+
reviveCapability: 'embedded',
|
|
584
|
+
surfaceMode: 'channel',
|
|
585
|
+
lastStartedAt: new Date().toISOString(),
|
|
586
|
+
lastHeartbeatAt: new Date().toISOString(),
|
|
587
|
+
});
|
|
563
588
|
// Populate conversation cache
|
|
564
589
|
try {
|
|
565
590
|
const convos = await client.getConversations();
|
|
@@ -627,13 +652,22 @@ export async function main() {
|
|
|
627
652
|
approvalManager?.dispose();
|
|
628
653
|
approvalServer?.stop().catch(() => { });
|
|
629
654
|
stream?.stop();
|
|
655
|
+
const runtimeId = buildLocalRuntimeId({
|
|
656
|
+
runtime: 'claude-code',
|
|
657
|
+
profile: getActiveProfile(),
|
|
658
|
+
cwd: process.cwd(),
|
|
659
|
+
launchCommand: ['canon-channel-server'],
|
|
660
|
+
});
|
|
661
|
+
markLocalRuntimeStopped(runtimeId);
|
|
630
662
|
const ap = getActiveProfile();
|
|
631
663
|
if (ap)
|
|
632
664
|
releaseLock(ap);
|
|
665
|
+
getActiveProfileLock()?.release();
|
|
633
666
|
process.exit(0);
|
|
634
667
|
};
|
|
635
668
|
process.on('SIGINT', shutdown);
|
|
636
669
|
process.on('SIGTERM', shutdown);
|
|
670
|
+
process.on('SIGHUP', shutdown);
|
|
637
671
|
}
|
|
638
672
|
runCli(import.meta.url, main, (err) => {
|
|
639
673
|
console.error('[canon] Fatal error:', err);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/claude-code-plugin",
|
|
3
|
-
"version": "0.12.
|
|
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.
|
|
35
|
-
"@canonmsg/core": "^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": {
|
package/dist/host-runtime.d.ts
DELETED
|
@@ -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 {};
|
package/dist/host-runtime.js
DELETED
|
@@ -1,266 +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.accessLevel)
|
|
73
|
-
parts.push(`access: ${card.accessLevel}`);
|
|
74
|
-
if (card.ownerName)
|
|
75
|
-
parts.push(`owner: ${card.ownerName}`);
|
|
76
|
-
if (card.about)
|
|
77
|
-
parts.push(`about: ${card.about}`);
|
|
78
|
-
const identity = `📇 Contact card: "${card.displayName}" (${parts.join(' · ')}).`;
|
|
79
|
-
const missingCapabilities = [
|
|
80
|
-
!HOST_ADMISSION_ACTION_CAPABILITIES.canStartDirectConversation
|
|
81
|
-
? 'start a direct conversation'
|
|
82
|
-
: null,
|
|
83
|
-
!HOST_ADMISSION_ACTION_CAPABILITIES.canSendContactRequest
|
|
84
|
-
? 'send a contact request'
|
|
85
|
-
: null,
|
|
86
|
-
!HOST_ADMISSION_ACTION_CAPABILITIES.canApprovePendingContactRequests
|
|
87
|
-
? 'approve pending requests'
|
|
88
|
-
: null,
|
|
89
|
-
!HOST_ADMISSION_ACTION_CAPABILITIES.canRejectPendingContactRequests
|
|
90
|
-
? 'reject pending requests'
|
|
91
|
-
: null,
|
|
92
|
-
].filter(Boolean).join(', ');
|
|
93
|
-
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}.`;
|
|
94
|
-
return `${identity}\n${hint}`;
|
|
95
|
-
}
|
|
96
|
-
function describeAttachment(attachment, materialized) {
|
|
97
|
-
if (attachment.kind === 'image') {
|
|
98
|
-
return '[Image attached]';
|
|
99
|
-
}
|
|
100
|
-
if (attachment.kind === 'audio') {
|
|
101
|
-
const durationMs = materialized?.durationMs ?? attachment.durationMs;
|
|
102
|
-
const duration = durationMs ? ` (${Math.round(durationMs / 1000)}s)` : '';
|
|
103
|
-
const ref = materialized?.path ? ` ${materialized.path}` : '';
|
|
104
|
-
return `[Voice message${duration}${ref}]`;
|
|
105
|
-
}
|
|
106
|
-
// file
|
|
107
|
-
const label = materialized?.fileName ?? attachment.fileName ?? 'File';
|
|
108
|
-
const ref = materialized?.path ? ` ${materialized.path}` : '';
|
|
109
|
-
return `[File: ${label}${ref}]`;
|
|
110
|
-
}
|
|
111
|
-
export function buildHydratedInboundContext(input) {
|
|
112
|
-
const history = buildParticipationHistorySnapshot(input.page?.messages ?? [], input.agentId);
|
|
113
|
-
return {
|
|
114
|
-
participantContext: {
|
|
115
|
-
conversationType: input.conversation?.type ?? 'unknown',
|
|
116
|
-
memberCount: input.conversation?.memberIds?.length ?? null,
|
|
117
|
-
senderType: input.message.senderType ?? 'human',
|
|
118
|
-
senderName: input.senderName,
|
|
119
|
-
isOwner: input.isOwner,
|
|
120
|
-
mentionedAgent: Array.isArray(input.message.mentions) && input.message.mentions.includes(input.agentId),
|
|
121
|
-
recentSenderTypes: history.recentSenderTypes,
|
|
122
|
-
recentHumanCount: history.recentHumanCount,
|
|
123
|
-
recentAgentCount: history.recentAgentCount,
|
|
124
|
-
consecutiveAgentTurns: history.consecutiveAgentTurns,
|
|
125
|
-
currentAgentStreakStartedByHuman: history.currentAgentStreakStartedByHuman,
|
|
126
|
-
},
|
|
127
|
-
behavior: input.page?.behavior ?? input.conversation?.behavior,
|
|
128
|
-
workSessions: input.page?.workSessions ?? [],
|
|
129
|
-
hydratedFromPage: input.page != null,
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
export async function publishHostAgentRuntime(agentId, clientType, runtime) {
|
|
133
|
-
await rtdbWrite(`/agent-runtime/${agentId}`, {
|
|
134
|
-
clientType,
|
|
135
|
-
hostMode: true,
|
|
136
|
-
...runtime,
|
|
137
|
-
updatedAt: { '.sv': 'timestamp' },
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
export async function publishHostSessionSnapshots(input) {
|
|
141
|
-
if (input.conversationIds.length === 0) {
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
await Promise.all(input.conversationIds.map(async (conversationId) => {
|
|
145
|
-
const persistedConfig = await loadHostSessionConfig({
|
|
146
|
-
conversationId,
|
|
147
|
-
agentId: input.agentId,
|
|
148
|
-
extraStringFields: ['permissionMode', 'effort'],
|
|
149
|
-
});
|
|
150
|
-
const liveConfig = input.liveSessionConfigByConversation?.get(conversationId) ?? null;
|
|
151
|
-
const mergedConfig = {
|
|
152
|
-
...(persistedConfig ?? {}),
|
|
153
|
-
...(liveConfig ?? {}),
|
|
154
|
-
};
|
|
155
|
-
const snapshot = buildAgentSessionSnapshot({
|
|
156
|
-
conversationId,
|
|
157
|
-
agentId: input.agentId,
|
|
158
|
-
runtime: {
|
|
159
|
-
...input.runtime,
|
|
160
|
-
clientType: input.clientType,
|
|
161
|
-
hostMode: true,
|
|
162
|
-
},
|
|
163
|
-
sessionConfig: {
|
|
164
|
-
...(mergedConfig.model ? { model: mergedConfig.model } : {}),
|
|
165
|
-
...(mergedConfig.permissionMode ? { permissionMode: mergedConfig.permissionMode } : {}),
|
|
166
|
-
...(mergedConfig.effort ? { effort: mergedConfig.effort } : {}),
|
|
167
|
-
...(mergedConfig.runtimeControlValues
|
|
168
|
-
? { runtimeControlValues: mergedConfig.runtimeControlValues }
|
|
169
|
-
: {}),
|
|
170
|
-
...(mergedConfig.workspaceId ? { workspaceId: mergedConfig.workspaceId } : {}),
|
|
171
|
-
...(mergedConfig.executionMode ? { executionMode: mergedConfig.executionMode } : {}),
|
|
172
|
-
},
|
|
173
|
-
lastHeartbeatAt: undefined,
|
|
174
|
-
});
|
|
175
|
-
let executionBranch = liveConfig?.executionBranch ?? null;
|
|
176
|
-
if (!executionBranch && snapshot.executionMode === 'worktree' && snapshot.workspaceId) {
|
|
177
|
-
const workspace = input.workspaceOptions.find((option) => option.id === snapshot.workspaceId);
|
|
178
|
-
if (workspace) {
|
|
179
|
-
executionBranch = buildConversationWorktreeSpec({
|
|
180
|
-
agentId: input.agentId,
|
|
181
|
-
conversationId,
|
|
182
|
-
workspaceCwd: workspace.cwd,
|
|
183
|
-
}).branch;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
return patchAgentSessionSnapshot(conversationId, input.agentId, {
|
|
187
|
-
clientType: input.clientType,
|
|
188
|
-
hostMode: true,
|
|
189
|
-
model: snapshot.model ?? null,
|
|
190
|
-
permissionMode: snapshot.permissionMode ?? null,
|
|
191
|
-
effort: snapshot.effort ?? null,
|
|
192
|
-
runtimeControlValues: snapshot.runtimeControlValues ?? null,
|
|
193
|
-
workspaceId: snapshot.workspaceId ?? null,
|
|
194
|
-
executionMode: snapshot.executionMode ?? null,
|
|
195
|
-
executionBranch,
|
|
196
|
-
modelOptions: snapshot.modelOptions,
|
|
197
|
-
permissionModeOptions: snapshot.permissionModeOptions,
|
|
198
|
-
workspaceOptions: snapshot.workspaceOptions,
|
|
199
|
-
availableExecutionModes: snapshot.availableExecutionModes,
|
|
200
|
-
lastHeartbeatAt: { '.sv': 'timestamp' },
|
|
201
|
-
});
|
|
202
|
-
}));
|
|
203
|
-
}
|
|
204
|
-
export function readHostSessionConfig(raw, extraStringFields = []) {
|
|
205
|
-
const baseConfig = readSessionWorkspaceConfig(raw);
|
|
206
|
-
if (!raw || typeof raw !== 'object') {
|
|
207
|
-
return baseConfig;
|
|
208
|
-
}
|
|
209
|
-
const data = raw;
|
|
210
|
-
const extraConfig = Object.fromEntries(extraStringFields.flatMap((field) => {
|
|
211
|
-
const value = normalizeOptionalString(data[field]);
|
|
212
|
-
return value ? [[field, value]] : [];
|
|
213
|
-
}));
|
|
214
|
-
const runtimeControlValues = Object.fromEntries(Object.entries(data.runtimeControlValues && typeof data.runtimeControlValues === 'object'
|
|
215
|
-
? data.runtimeControlValues
|
|
216
|
-
: {}).flatMap(([key, value]) => {
|
|
217
|
-
const normalizedValue = normalizeOptionalString(value);
|
|
218
|
-
return normalizedValue ? [[key, normalizedValue]] : [];
|
|
219
|
-
}));
|
|
220
|
-
return {
|
|
221
|
-
...(baseConfig ?? {}),
|
|
222
|
-
...extraConfig,
|
|
223
|
-
...(Object.keys(runtimeControlValues).length > 0 ? { runtimeControlValues } : {}),
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
export async function loadHostSessionConfig(input) {
|
|
227
|
-
const raw = await rtdbRead(`/session-config/${input.conversationId}/${input.agentId}`);
|
|
228
|
-
return readHostSessionConfig(raw, input.extraStringFields);
|
|
229
|
-
}
|
|
230
|
-
export function resolveHostWorkspaceCwd(input) {
|
|
231
|
-
return resolveConfiguredWorkspaceCwd(input);
|
|
232
|
-
}
|
|
233
|
-
export function createConversationMetadataLoader(input) {
|
|
234
|
-
const cacheTtlMs = input.cacheTtlMs ?? 10_000;
|
|
235
|
-
let conversationCacheLoadedAt = 0;
|
|
236
|
-
async function refreshConversationCache(force = false) {
|
|
237
|
-
if (!force
|
|
238
|
-
&& input.conversationCache.size > 0
|
|
239
|
-
&& Date.now() - conversationCacheLoadedAt < cacheTtlMs) {
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
const conversations = await input.client.getConversations();
|
|
243
|
-
input.conversationCache.clear();
|
|
244
|
-
for (const conversation of conversations) {
|
|
245
|
-
input.conversationCache.set(conversation.id, conversation);
|
|
246
|
-
}
|
|
247
|
-
conversationCacheLoadedAt = Date.now();
|
|
248
|
-
}
|
|
249
|
-
async function getConversationMeta(conversationId) {
|
|
250
|
-
try {
|
|
251
|
-
await refreshConversationCache();
|
|
252
|
-
const cached = input.conversationCache.get(conversationId);
|
|
253
|
-
if (cached)
|
|
254
|
-
return cached;
|
|
255
|
-
await refreshConversationCache(true);
|
|
256
|
-
return input.conversationCache.get(conversationId) ?? null;
|
|
257
|
-
}
|
|
258
|
-
catch {
|
|
259
|
-
return input.conversationCache.get(conversationId) ?? null;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
return {
|
|
263
|
-
refreshConversationCache,
|
|
264
|
-
getConversationMeta,
|
|
265
|
-
};
|
|
266
|
-
}
|