@canonmsg/claude-code-plugin 0.12.3 → 0.12.5
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/cli-entry.d.ts +2 -2
- package/dist/cli-entry.js +1 -26
- package/dist/host.js +290 -101
- package/dist/register.js +24 -0
- package/dist/server.js +14 -0
- package/dist/session-state.d.ts +44 -0
- package/dist/session-state.js +67 -0
- package/dist/setup.js +14 -0
- package/package.json +3 -3
package/dist/cli-entry.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
1
|
+
export { handleCliMetadataRequest, isDirectExecution, readCliPackageVersion, runCli, } from '@canonmsg/core';
|
|
2
|
+
export type { CliMetadata, RunCliMetadataOptions, } from '@canonmsg/core';
|
package/dist/cli-entry.js
CHANGED
|
@@ -1,26 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { resolve } from 'node:path';
|
|
3
|
-
import { pathToFileURL } from 'node:url';
|
|
4
|
-
export function isDirectExecution(moduleUrl) {
|
|
5
|
-
const entry = process.argv[1];
|
|
6
|
-
if (!entry)
|
|
7
|
-
return false;
|
|
8
|
-
const abs = resolve(entry);
|
|
9
|
-
let real = abs;
|
|
10
|
-
try {
|
|
11
|
-
real = realpathSync(abs);
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
// argv[1] may not exist on disk in rare harnesses; fall back to abs
|
|
15
|
-
}
|
|
16
|
-
return (pathToFileURL(real).href === moduleUrl ||
|
|
17
|
-
pathToFileURL(abs).href === moduleUrl);
|
|
18
|
-
}
|
|
19
|
-
export function runCli(moduleUrl, main, onError) {
|
|
20
|
-
if (!isDirectExecution(moduleUrl)) {
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
Promise.resolve()
|
|
24
|
-
.then(() => main())
|
|
25
|
-
.catch(onError);
|
|
26
|
-
}
|
|
1
|
+
export { handleCliMetadataRequest, isDirectExecution, readCliPackageVersion, runCli, } from '@canonmsg/core';
|
package/dist/host.js
CHANGED
|
@@ -26,10 +26,32 @@ import { execFileSync } from 'node:child_process';
|
|
|
26
26
|
import { existsSync } from 'node:fs';
|
|
27
27
|
import { query, } from '@anthropic-ai/claude-agent-sdk';
|
|
28
28
|
import { isAnthropicImageAttachment, materializeMessageMedia, toAnthropicImageBlock, } from '@canonmsg/agent-sdk';
|
|
29
|
-
import { CLAUDE_PERMISSION_MODE_OPTIONS, buildCanonHostPrompt, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, buildPlanApprovalRequest, buildQuestionRequest, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, createConversationMetadataLoader, createRuntimeStatePublisher, EXECUTION_ENVIRONMENT_MODES, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, ExecutionEnvironmentError, FINAL_MESSAGE_HANDOFF_MS, buildLocalRuntimeId, getActiveProfileLock, heartbeatLocalRuntimeEntry, normalizeTurnMetadata, prepareConversationEnvironment, resolveCanonAgent, initRTDBAuth, rtdbRead, loadHostSessionConfig, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, saveRuntimeSessionState, clearRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
|
|
29
|
+
import { CLAUDE_PERMISSION_MODE_OPTIONS, buildCanonHostPrompt, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, buildPlanApprovalRequest, buildQuestionRequest, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, createConversationMetadataLoader, createRuntimeStatePublisher, EXECUTION_ENVIRONMENT_MODES, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, ExecutionEnvironmentError, FINAL_MESSAGE_HANDOFF_MS, buildLocalRuntimeId, getActiveProfileLock, heartbeatLocalRuntimeEntry, normalizeTurnMetadata, prepareConversationEnvironment, resolveCanonAgent, initRTDBAuth, rtdbRead, rtdbWrite, loadHostSessionConfig, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, saveRuntimeSessionState, clearRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
|
|
30
30
|
import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
|
|
31
31
|
import { createStreamingBuffer } from './streaming-buffer.js';
|
|
32
32
|
import { runCli } from './cli-entry.js';
|
|
33
|
+
import { buildClaudeFinalMessageId, createClaudeInputEnvelope, formatClaudeControlError, mergeClaudeModelOptions, shouldDeliverClaudeFinal, } from './session-state.js';
|
|
34
|
+
const HELP = `canon-claude — run a local Claude Code agent host for Canon
|
|
35
|
+
|
|
36
|
+
USAGE
|
|
37
|
+
canon-claude [flags]
|
|
38
|
+
|
|
39
|
+
COMMON FLAGS
|
|
40
|
+
--cwd <path> Project directory to run Claude Code from
|
|
41
|
+
--workspace <path> Additional project to expose in Canon
|
|
42
|
+
--workspace-root <path> Discover projects under an approved root
|
|
43
|
+
--help, -h Show this help
|
|
44
|
+
--version, -V Show package version
|
|
45
|
+
|
|
46
|
+
AUTH
|
|
47
|
+
CANON_AGENT=<profile> canon-claude --cwd /path/to/project
|
|
48
|
+
CANON_API_KEY=agk_live_... canon-claude --cwd /path/to/project
|
|
49
|
+
|
|
50
|
+
EXAMPLES
|
|
51
|
+
canon-claude --cwd ~/dev/canon
|
|
52
|
+
canon-claude --cwd ~/dev/canon --workspace-root ~/dev
|
|
53
|
+
|
|
54
|
+
Keep this terminal open while you want Canon to reach the agent.`;
|
|
33
55
|
let workingDir = process.cwd();
|
|
34
56
|
let workspaceOptions = [];
|
|
35
57
|
let workspaceRoots = [];
|
|
@@ -142,6 +164,16 @@ function buildClaudeRuntimeDescriptor(input) {
|
|
|
142
164
|
availability: ['busy'],
|
|
143
165
|
dispatch: { kind: 'signal', signal: 'interrupt' },
|
|
144
166
|
},
|
|
167
|
+
{
|
|
168
|
+
id: 'stop-and-clear-queue',
|
|
169
|
+
label: 'Stop & clear queue',
|
|
170
|
+
description: 'Interrupt the current Claude turn and drop queued Canon messages.',
|
|
171
|
+
aliases: ['stop-clear', 'clear-queue'],
|
|
172
|
+
category: 'turn',
|
|
173
|
+
placements: ['composer_slash', 'command_palette', 'session_strip'],
|
|
174
|
+
availability: ['busy_with_queue'],
|
|
175
|
+
dispatch: { kind: 'signal', signal: 'stop_and_drop' },
|
|
176
|
+
},
|
|
145
177
|
],
|
|
146
178
|
});
|
|
147
179
|
}
|
|
@@ -450,17 +482,45 @@ const CLAUDE_RUNTIME_CAPABILITIES = {
|
|
|
450
482
|
...DEFAULT_RUNTIME_CAPABILITIES,
|
|
451
483
|
supportsInterrupt: true,
|
|
452
484
|
supportsQueue: true,
|
|
453
|
-
supportsInterleave:
|
|
485
|
+
supportsInterleave: false,
|
|
454
486
|
supportsRequiresAction: true,
|
|
455
487
|
supportsNonFinalPermanentMessages: false,
|
|
456
488
|
};
|
|
457
489
|
function buildCanonPrompt(input) {
|
|
490
|
+
const { session, ...promptInput } = input;
|
|
458
491
|
return buildCanonHostPrompt({
|
|
459
492
|
hostLabel: 'Claude Code',
|
|
460
493
|
buildInboundContextLines,
|
|
461
|
-
|
|
494
|
+
sessionContextLines: session ? buildClaudeSessionContextLines(session) : undefined,
|
|
495
|
+
...promptInput,
|
|
462
496
|
});
|
|
463
497
|
}
|
|
498
|
+
function optionLabel(value, options) {
|
|
499
|
+
if (!value)
|
|
500
|
+
return null;
|
|
501
|
+
const label = options.find((option) => option.value === value)?.label ?? value;
|
|
502
|
+
return label === value ? value : `${label} (${value})`;
|
|
503
|
+
}
|
|
504
|
+
function buildClaudeSessionContextLines(session) {
|
|
505
|
+
const workspace = workspaceOptions.find((option) => option.cwd === session.environment.baseCwd);
|
|
506
|
+
const permissionMode = optionLabel(session.state.permissionMode, CLAUDE_PERMISSION_MODE_OPTIONS);
|
|
507
|
+
const effort = optionLabel(session.state.effort, [
|
|
508
|
+
{ value: 'low', label: 'Low' },
|
|
509
|
+
{ value: 'medium', label: 'Medium' },
|
|
510
|
+
{ value: 'high', label: 'High' },
|
|
511
|
+
]);
|
|
512
|
+
const model = optionLabel(session.state.model, session.availableModels);
|
|
513
|
+
return [
|
|
514
|
+
model ? `Selected model: ${model}` : null,
|
|
515
|
+
permissionMode ? `Permission mode: ${permissionMode}` : null,
|
|
516
|
+
effort ? `Thinking level: ${effort}` : null,
|
|
517
|
+
workspace ? `Project: ${workspace.label} (${workspace.id})` : null,
|
|
518
|
+
`Execution mode: ${session.environment.mode}`,
|
|
519
|
+
`Working directory: ${session.cwd}`,
|
|
520
|
+
session.environment.branch ? `Git branch: ${session.environment.branch}` : null,
|
|
521
|
+
session.environment.worktreePath ? `Worktree: ${session.environment.worktreePath}` : null,
|
|
522
|
+
].filter((line) => Boolean(line));
|
|
523
|
+
}
|
|
464
524
|
function renderInboundContent(message, materialized) {
|
|
465
525
|
return renderCanonHostInboundContent(message, materialized);
|
|
466
526
|
}
|
|
@@ -491,19 +551,25 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
491
551
|
let resolveInput = null;
|
|
492
552
|
let inputDone = false;
|
|
493
553
|
const messageQueue = [];
|
|
494
|
-
const sendInput = (
|
|
554
|
+
const sendInput = (input) => {
|
|
495
555
|
if (resolveInput) {
|
|
496
556
|
const r = resolveInput;
|
|
497
557
|
resolveInput = null;
|
|
498
|
-
r(
|
|
558
|
+
r(input);
|
|
499
559
|
}
|
|
500
560
|
else {
|
|
501
561
|
// Queue if iterator isn't waiting yet (e.g., during init)
|
|
502
|
-
messageQueue.push(
|
|
562
|
+
messageQueue.push(input);
|
|
503
563
|
}
|
|
504
564
|
};
|
|
505
|
-
let enqueueInboundMessage = (msg,
|
|
506
|
-
sendInput(
|
|
565
|
+
let enqueueInboundMessage = (msg, intent = 'queue', sourceMessageId, markAccepted = false) => {
|
|
566
|
+
sendInput(createClaudeInputEnvelope({
|
|
567
|
+
kind: 'canon',
|
|
568
|
+
msg,
|
|
569
|
+
intent,
|
|
570
|
+
sourceMessageId,
|
|
571
|
+
markAccepted,
|
|
572
|
+
}));
|
|
507
573
|
};
|
|
508
574
|
const inputStream = {
|
|
509
575
|
[Symbol.asyncIterator]() {
|
|
@@ -513,10 +579,15 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
513
579
|
return Promise.resolve({ done: true, value: undefined });
|
|
514
580
|
// Drain queued messages first (sent before iterator was ready)
|
|
515
581
|
if (messageQueue.length > 0) {
|
|
516
|
-
|
|
582
|
+
const next = messageQueue.shift();
|
|
583
|
+
session.activeInput = next;
|
|
584
|
+
return Promise.resolve({ done: false, value: next.msg });
|
|
517
585
|
}
|
|
518
586
|
return new Promise((resolve) => {
|
|
519
|
-
resolveInput = (
|
|
587
|
+
resolveInput = (input) => {
|
|
588
|
+
session.activeInput = input;
|
|
589
|
+
resolve({ done: false, value: input.msg });
|
|
590
|
+
};
|
|
520
591
|
});
|
|
521
592
|
},
|
|
522
593
|
return() {
|
|
@@ -659,6 +730,7 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
659
730
|
state: {
|
|
660
731
|
model: config.model,
|
|
661
732
|
permissionMode: config.permissionMode,
|
|
733
|
+
effort: config.effort,
|
|
662
734
|
state: 'idle',
|
|
663
735
|
},
|
|
664
736
|
availableModels: config.availableModels,
|
|
@@ -668,8 +740,12 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
668
740
|
finalDeliveryTimer: null,
|
|
669
741
|
seedResponseHandled: !!resumeSessionId, // Resumed sessions don't need seed handling
|
|
670
742
|
pendingInputs: [],
|
|
743
|
+
activeInput: null,
|
|
744
|
+
finalizedTurnKeys: new Set(),
|
|
745
|
+
interruptedTurnKeys: new Set(),
|
|
671
746
|
pendingFinalText: null,
|
|
672
|
-
|
|
747
|
+
pendingFinalDelivery: null,
|
|
748
|
+
runtimeControlErrors: {},
|
|
673
749
|
toolInProgress: false,
|
|
674
750
|
turnState: 'idle',
|
|
675
751
|
currentTurnId: null,
|
|
@@ -680,6 +756,10 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
680
756
|
closed: false,
|
|
681
757
|
sdkSessionId: resumeSessionId || null,
|
|
682
758
|
pendingReply: null,
|
|
759
|
+
writeState: () => writeState(),
|
|
760
|
+
writeTurn: () => writeTurn(),
|
|
761
|
+
setRuntimeControlError: (controlId, value, error) => setRuntimeControlError(controlId, value, error),
|
|
762
|
+
clearRuntimeControlError: (controlId) => clearRuntimeControlError(controlId),
|
|
683
763
|
};
|
|
684
764
|
const streamingBuffer = createStreamingBuffer({
|
|
685
765
|
throttleMs: STREAMING_THROTTLE_MS,
|
|
@@ -729,7 +809,8 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
729
809
|
session.currentTurnOpenedAt = null;
|
|
730
810
|
session.lastAcceptedIntent = null;
|
|
731
811
|
session.pendingFinalText = null;
|
|
732
|
-
session.
|
|
812
|
+
session.pendingFinalDelivery = null;
|
|
813
|
+
session.activeInput = null;
|
|
733
814
|
session.toolInProgress = false;
|
|
734
815
|
session.turnState = 'idle';
|
|
735
816
|
stopVisibleWorkSignal();
|
|
@@ -745,10 +826,18 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
745
826
|
resetTurnToIdle();
|
|
746
827
|
}, FINAL_MESSAGE_HANDOFF_MS);
|
|
747
828
|
}
|
|
748
|
-
function markPendingFinalDelivery(finalText) {
|
|
829
|
+
function markPendingFinalDelivery(finalText, turn) {
|
|
749
830
|
clearIdleResetTimer();
|
|
750
831
|
session.pendingFinalText = finalText;
|
|
751
|
-
session.
|
|
832
|
+
session.pendingFinalDelivery = {
|
|
833
|
+
turnKey: turn.turnKey,
|
|
834
|
+
text: finalText,
|
|
835
|
+
messageId: buildClaudeFinalMessageId({
|
|
836
|
+
agentId,
|
|
837
|
+
conversationId,
|
|
838
|
+
turnKey: turn.turnKey,
|
|
839
|
+
}),
|
|
840
|
+
};
|
|
752
841
|
session.state.state = 'running';
|
|
753
842
|
session.turnState = 'streaming';
|
|
754
843
|
stopVisibleWorkSignal();
|
|
@@ -757,49 +846,75 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
757
846
|
writeTurn();
|
|
758
847
|
client.setTyping(conversationId, true, 'typing').catch(() => { });
|
|
759
848
|
}
|
|
760
|
-
async function deliverFinalReply(finalText) {
|
|
849
|
+
async function deliverFinalReply(finalText, turn = session.activeInput) {
|
|
850
|
+
if (!shouldDeliverClaudeFinal({
|
|
851
|
+
turn,
|
|
852
|
+
finalizedTurnKeys: session.finalizedTurnKeys,
|
|
853
|
+
interruptedTurnKeys: session.interruptedTurnKeys,
|
|
854
|
+
})) {
|
|
855
|
+
return true;
|
|
856
|
+
}
|
|
857
|
+
const deliverTurn = turn;
|
|
858
|
+
if (!deliverTurn)
|
|
859
|
+
return true;
|
|
860
|
+
const finalMessageId = buildClaudeFinalMessageId({
|
|
861
|
+
agentId,
|
|
862
|
+
conversationId,
|
|
863
|
+
turnKey: deliverTurn.turnKey,
|
|
864
|
+
});
|
|
761
865
|
try {
|
|
762
866
|
await client.sendMessage(conversationId, finalText, {
|
|
867
|
+
messageId: finalMessageId,
|
|
763
868
|
metadata: {
|
|
764
869
|
turnId: session.currentTurnId,
|
|
870
|
+
turnKey: deliverTurn.turnKey,
|
|
871
|
+
sourceMessageId: deliverTurn.sourceMessageId ?? undefined,
|
|
765
872
|
turnSemantics: 'turn_complete',
|
|
766
873
|
turnComplete: true,
|
|
767
874
|
deliveryIntent: session.lastAcceptedIntent ?? undefined,
|
|
768
875
|
},
|
|
769
876
|
});
|
|
877
|
+
session.finalizedTurnKeys.add(deliverTurn.turnKey);
|
|
770
878
|
session.state.state = 'idle';
|
|
771
|
-
session.
|
|
879
|
+
session.pendingFinalDelivery = null;
|
|
772
880
|
clearFinalDeliveryTimer();
|
|
773
881
|
writeState();
|
|
774
882
|
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Sent final reply (${finalText.length} chars)`);
|
|
775
883
|
return true;
|
|
776
884
|
}
|
|
777
885
|
catch (err) {
|
|
778
|
-
markPendingFinalDelivery(finalText);
|
|
886
|
+
markPendingFinalDelivery(finalText, deliverTurn);
|
|
779
887
|
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Failed to send final reply:`, err);
|
|
780
888
|
return false;
|
|
781
889
|
}
|
|
782
890
|
}
|
|
783
|
-
function scheduleFinalDeliveryRetry(
|
|
784
|
-
if (session.closed || session.
|
|
891
|
+
function scheduleFinalDeliveryRetry(pending = session.pendingFinalDelivery) {
|
|
892
|
+
if (!pending || session.closed || session.pendingFinalDelivery?.turnKey !== pending.turnKey)
|
|
785
893
|
return;
|
|
786
894
|
clearFinalDeliveryTimer();
|
|
787
895
|
session.finalDeliveryTimer = setTimeout(() => {
|
|
788
896
|
session.finalDeliveryTimer = null;
|
|
789
|
-
|
|
897
|
+
const retryTurn = session.activeInput?.turnKey === pending.turnKey
|
|
898
|
+
? session.activeInput
|
|
899
|
+
: null;
|
|
900
|
+
void deliverFinalReply(pending.text, retryTurn).then((sent) => {
|
|
790
901
|
if (sent) {
|
|
791
902
|
scheduleFinalHandoffReset();
|
|
792
903
|
return;
|
|
793
904
|
}
|
|
794
|
-
scheduleFinalDeliveryRetry(
|
|
905
|
+
scheduleFinalDeliveryRetry();
|
|
795
906
|
});
|
|
796
907
|
}, FINAL_DELIVERY_RETRY_MS);
|
|
797
908
|
}
|
|
798
909
|
function writeState() {
|
|
910
|
+
const runtimeControlErrors = Object.keys(session.runtimeControlErrors).length > 0
|
|
911
|
+
? session.runtimeControlErrors
|
|
912
|
+
: null;
|
|
799
913
|
runtimeState.writeSessionState(conversationId, {
|
|
800
914
|
model: session.state.model,
|
|
801
915
|
permissionMode: session.state.permissionMode,
|
|
802
916
|
effort: session.state.effort,
|
|
917
|
+
runtimeControlErrors,
|
|
803
918
|
contextUsage: session.state.contextUsage,
|
|
804
919
|
...(session.availableModels.length ? { availableModels: session.availableModels } : {}),
|
|
805
920
|
cwd,
|
|
@@ -813,6 +928,25 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
813
928
|
isActive: true,
|
|
814
929
|
}).catch(() => { });
|
|
815
930
|
}
|
|
931
|
+
function setRuntimeControlError(controlId, value, error) {
|
|
932
|
+
session.runtimeControlErrors = {
|
|
933
|
+
...session.runtimeControlErrors,
|
|
934
|
+
[controlId]: {
|
|
935
|
+
value,
|
|
936
|
+
message: formatClaudeControlError(error),
|
|
937
|
+
updatedAt: Date.now(),
|
|
938
|
+
},
|
|
939
|
+
};
|
|
940
|
+
writeState();
|
|
941
|
+
}
|
|
942
|
+
function clearRuntimeControlError(controlId) {
|
|
943
|
+
if (!(controlId in session.runtimeControlErrors))
|
|
944
|
+
return;
|
|
945
|
+
const next = { ...session.runtimeControlErrors };
|
|
946
|
+
delete next[controlId];
|
|
947
|
+
session.runtimeControlErrors = next;
|
|
948
|
+
writeState();
|
|
949
|
+
}
|
|
816
950
|
function writeTurn() {
|
|
817
951
|
runtimeState.writeTurnState(conversationId, {
|
|
818
952
|
turnId: session.currentTurnId,
|
|
@@ -860,22 +994,32 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
860
994
|
session.lastAcceptedIntent = next.intent;
|
|
861
995
|
writeTurn();
|
|
862
996
|
void markQueuedMessageAccepted(next.sourceMessageId, next.markAccepted)
|
|
863
|
-
.finally(() => sendInput(next
|
|
997
|
+
.finally(() => sendInput(next));
|
|
864
998
|
}
|
|
865
999
|
enqueueInboundMessage = (msg, intent = 'queue', sourceMessageId = null, markAccepted = false) => {
|
|
866
1000
|
session.lastActivity = Date.now();
|
|
867
|
-
|
|
868
|
-
|
|
1001
|
+
const input = createClaudeInputEnvelope({
|
|
1002
|
+
kind: 'canon',
|
|
1003
|
+
msg,
|
|
1004
|
+
intent,
|
|
1005
|
+
sourceMessageId,
|
|
1006
|
+
markAccepted,
|
|
1007
|
+
});
|
|
1008
|
+
if (session.pendingFinalDelivery) {
|
|
1009
|
+
session.pendingInputs.push(input);
|
|
869
1010
|
writeTurn();
|
|
870
1011
|
return;
|
|
871
1012
|
}
|
|
872
1013
|
if (session.state.state === 'running' && intent !== 'interrupt') {
|
|
873
|
-
session.pendingInputs.push(
|
|
1014
|
+
session.pendingInputs.push(input);
|
|
874
1015
|
writeTurn();
|
|
875
1016
|
return;
|
|
876
1017
|
}
|
|
877
1018
|
if (session.state.state === 'running' && intent === 'interrupt') {
|
|
878
|
-
session.
|
|
1019
|
+
if (session.activeInput) {
|
|
1020
|
+
session.interruptedTurnKeys.add(session.activeInput.turnKey);
|
|
1021
|
+
}
|
|
1022
|
+
session.pendingInputs.unshift(input);
|
|
879
1023
|
session.turnState = 'interrupted';
|
|
880
1024
|
stopVisibleWorkSignal();
|
|
881
1025
|
writeTurn();
|
|
@@ -885,7 +1029,7 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
885
1029
|
session.lastAcceptedIntent = intent;
|
|
886
1030
|
writeTurn();
|
|
887
1031
|
void markQueuedMessageAccepted(sourceMessageId, markAccepted)
|
|
888
|
-
.finally(() => sendInput(
|
|
1032
|
+
.finally(() => sendInput(input));
|
|
889
1033
|
};
|
|
890
1034
|
writeTurn();
|
|
891
1035
|
// ── Process SDK output messages ──
|
|
@@ -981,12 +1125,22 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
981
1125
|
const resultSessionId = msg.session_id;
|
|
982
1126
|
if (resultSessionId)
|
|
983
1127
|
session.sdkSessionId = resultSessionId;
|
|
1128
|
+
const completedInput = session.activeInput;
|
|
984
1129
|
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Turn complete (${msg.subtype})`);
|
|
985
1130
|
const resultText = typeof msg.result === 'string'
|
|
986
1131
|
? msg.result.trim()
|
|
987
1132
|
: '';
|
|
988
1133
|
const finalText = resultText || session.pendingFinalText?.trim() || null;
|
|
989
|
-
const
|
|
1134
|
+
const shouldDeliverFinal = finalText
|
|
1135
|
+
? shouldDeliverClaudeFinal({
|
|
1136
|
+
turn: completedInput,
|
|
1137
|
+
finalizedTurnKeys: session.finalizedTurnKeys,
|
|
1138
|
+
interruptedTurnKeys: session.interruptedTurnKeys,
|
|
1139
|
+
})
|
|
1140
|
+
: false;
|
|
1141
|
+
const finalDelivered = finalText && shouldDeliverFinal
|
|
1142
|
+
? await deliverFinalReply(finalText, completedInput)
|
|
1143
|
+
: true;
|
|
990
1144
|
try {
|
|
991
1145
|
const usage = await q.getContextUsage();
|
|
992
1146
|
session.state.contextUsage = {
|
|
@@ -1001,10 +1155,19 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
1001
1155
|
// Non-fatal
|
|
1002
1156
|
}
|
|
1003
1157
|
if (finalDelivered) {
|
|
1004
|
-
|
|
1158
|
+
if (completedInput?.turnKey === session.activeInput?.turnKey) {
|
|
1159
|
+
session.activeInput = null;
|
|
1160
|
+
}
|
|
1161
|
+
session.pendingFinalText = null;
|
|
1162
|
+
if (shouldDeliverFinal && finalText) {
|
|
1163
|
+
scheduleFinalHandoffReset();
|
|
1164
|
+
}
|
|
1165
|
+
else {
|
|
1166
|
+
resetTurnToIdle();
|
|
1167
|
+
}
|
|
1005
1168
|
}
|
|
1006
1169
|
else if (finalText) {
|
|
1007
|
-
scheduleFinalDeliveryRetry(
|
|
1170
|
+
scheduleFinalDeliveryRetry();
|
|
1008
1171
|
}
|
|
1009
1172
|
break;
|
|
1010
1173
|
}
|
|
@@ -1046,19 +1209,26 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
1046
1209
|
}
|
|
1047
1210
|
if (!resumeSessionId) {
|
|
1048
1211
|
// New session — send seed message to activate streaming input mode
|
|
1049
|
-
sendInput({
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1212
|
+
sendInput(createClaudeInputEnvelope({
|
|
1213
|
+
kind: 'seed',
|
|
1214
|
+
msg: {
|
|
1215
|
+
type: 'user',
|
|
1216
|
+
message: {
|
|
1217
|
+
role: 'user',
|
|
1218
|
+
content: 'You are connected to Canon messaging. Follow-up messages in this session come from Canon participants, and your text responses will be sent back to the conversation. You have full Claude Code capabilities (file read/write, bash, etc).',
|
|
1219
|
+
},
|
|
1220
|
+
parent_tool_use_id: null,
|
|
1054
1221
|
},
|
|
1055
|
-
|
|
1056
|
-
});
|
|
1222
|
+
}));
|
|
1057
1223
|
}
|
|
1058
1224
|
// Refresh the canonical runtime descriptor after Claude reports supported models.
|
|
1059
1225
|
try {
|
|
1060
1226
|
const models = await q.supportedModels();
|
|
1061
|
-
const modelList =
|
|
1227
|
+
const modelList = mergeClaudeModelOptions({
|
|
1228
|
+
previous: runtimeModels,
|
|
1229
|
+
next: toModelOptions(models),
|
|
1230
|
+
selectedModel: session.state.model,
|
|
1231
|
+
});
|
|
1062
1232
|
session.availableModels = modelList;
|
|
1063
1233
|
runtimeModels = modelList;
|
|
1064
1234
|
onRuntimeDescriptorUpdate({
|
|
@@ -1075,21 +1245,7 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
1075
1245
|
executionModes: config.availableExecutionModes,
|
|
1076
1246
|
}),
|
|
1077
1247
|
});
|
|
1078
|
-
|
|
1079
|
-
model: session.state.model,
|
|
1080
|
-
permissionMode: session.state.permissionMode,
|
|
1081
|
-
effort: session.state.effort,
|
|
1082
|
-
contextUsage: session.state.contextUsage,
|
|
1083
|
-
cwd,
|
|
1084
|
-
executionMode: session.environment.mode,
|
|
1085
|
-
...(session.environment.branch ? { executionBranch: session.environment.branch } : {}),
|
|
1086
|
-
...(session.environment.worktreePath ? { worktreePath: session.environment.worktreePath } : {}),
|
|
1087
|
-
...(resolveExecutionFallbackReason(session.environment)
|
|
1088
|
-
? { executionFallbackReason: resolveExecutionFallbackReason(session.environment) ?? undefined }
|
|
1089
|
-
: {}),
|
|
1090
|
-
hostMode: true,
|
|
1091
|
-
isActive: true,
|
|
1092
|
-
}).catch(() => { });
|
|
1248
|
+
writeState();
|
|
1093
1249
|
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Models: ${models.map((m) => m.displayName).join(', ')}`);
|
|
1094
1250
|
}
|
|
1095
1251
|
catch {
|
|
@@ -1322,6 +1478,10 @@ export async function main() {
|
|
|
1322
1478
|
for (const conversation of convos) {
|
|
1323
1479
|
knownConversationIds.add(conversation.id);
|
|
1324
1480
|
conversationCache.set(conversation.id, conversation);
|
|
1481
|
+
runtimeState.clearStreaming(conversation.id).catch(() => { });
|
|
1482
|
+
runtimeState.clearSessionState(conversation.id).catch(() => { });
|
|
1483
|
+
runtimeState.clearTurnState(conversation.id).catch(() => { });
|
|
1484
|
+
client.setTyping(conversation.id, false).catch(() => { });
|
|
1325
1485
|
}
|
|
1326
1486
|
console.error(`[canon-host] ${convos.length} conversations`);
|
|
1327
1487
|
}
|
|
@@ -1331,6 +1491,9 @@ export async function main() {
|
|
|
1331
1491
|
// ── Session manager ──
|
|
1332
1492
|
const sessions = new Map();
|
|
1333
1493
|
const pendingSessionCreations = new Map();
|
|
1494
|
+
let controlStopped = false;
|
|
1495
|
+
const lastSeenControl = new Map();
|
|
1496
|
+
const lastSeenSignal = new Map();
|
|
1334
1497
|
/** Saved SDK session IDs for resume after idle eviction, keyed by conversation + workspace */
|
|
1335
1498
|
const savedSessionIds = new Map();
|
|
1336
1499
|
const { getConversationMeta } = createConversationMetadataLoader({
|
|
@@ -1408,6 +1571,13 @@ export async function main() {
|
|
|
1408
1571
|
sessions.delete(conversationId);
|
|
1409
1572
|
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Session suspended (sessionId saved for resume)`);
|
|
1410
1573
|
}
|
|
1574
|
+
async function markSessionPendingInputsRejected(conversationId, inputs) {
|
|
1575
|
+
await Promise.all(inputs.map((input) => {
|
|
1576
|
+
if (!input.markAccepted || !input.sourceMessageId)
|
|
1577
|
+
return Promise.resolve();
|
|
1578
|
+
return client.updateMessageDisposition(conversationId, input.sourceMessageId, 'rejected').catch(() => { });
|
|
1579
|
+
}));
|
|
1580
|
+
}
|
|
1411
1581
|
function evictOldestIdle() {
|
|
1412
1582
|
let oldest = null;
|
|
1413
1583
|
for (const s of sessions.values()) {
|
|
@@ -1489,6 +1659,10 @@ export async function main() {
|
|
|
1489
1659
|
availableExecutionModes: hostAvailableExecutionModes,
|
|
1490
1660
|
}, resumeId);
|
|
1491
1661
|
sessions.set(conversationId, session);
|
|
1662
|
+
await Promise.all([
|
|
1663
|
+
baselineControlSignal(conversationId),
|
|
1664
|
+
baselineSessionControl(conversationId),
|
|
1665
|
+
]);
|
|
1492
1666
|
return session;
|
|
1493
1667
|
}
|
|
1494
1668
|
catch (error) {
|
|
@@ -1504,6 +1678,28 @@ export async function main() {
|
|
|
1504
1678
|
pendingSessionCreations.delete(conversationId);
|
|
1505
1679
|
}
|
|
1506
1680
|
}
|
|
1681
|
+
async function baselineControlSignal(conversationId) {
|
|
1682
|
+
if (lastSeenSignal.has(conversationId))
|
|
1683
|
+
return;
|
|
1684
|
+
const raw = await rtdbRead(`/control/${conversationId}/${agentId}/signal`).catch(() => null);
|
|
1685
|
+
if (!raw || typeof raw !== 'object')
|
|
1686
|
+
return;
|
|
1687
|
+
const timestamp = Number(raw.updatedAt ?? 0);
|
|
1688
|
+
if (timestamp > 0) {
|
|
1689
|
+
lastSeenSignal.set(conversationId, timestamp);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
async function baselineSessionControl(conversationId) {
|
|
1693
|
+
if (lastSeenControl.has(conversationId))
|
|
1694
|
+
return;
|
|
1695
|
+
const raw = await rtdbRead(`/control/${conversationId}/${agentId}/session`).catch(() => null);
|
|
1696
|
+
if (!raw || typeof raw !== 'object')
|
|
1697
|
+
return;
|
|
1698
|
+
const timestamp = Number(raw.updatedAt ?? 0);
|
|
1699
|
+
if (timestamp > 0) {
|
|
1700
|
+
lastSeenControl.set(conversationId, timestamp);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1507
1703
|
async function enqueueRecoveredInboundMessage(input) {
|
|
1508
1704
|
const m = input.message;
|
|
1509
1705
|
if (m.senderId === agentId)
|
|
@@ -1563,6 +1759,7 @@ export async function main() {
|
|
|
1563
1759
|
behavior,
|
|
1564
1760
|
workSession: m.workSession,
|
|
1565
1761
|
workSessions,
|
|
1762
|
+
session,
|
|
1566
1763
|
});
|
|
1567
1764
|
const messageContent = await buildCanonUserContent({ promptText, materialized });
|
|
1568
1765
|
session.enqueueInbound({
|
|
@@ -1711,6 +1908,7 @@ export async function main() {
|
|
|
1711
1908
|
behavior,
|
|
1712
1909
|
workSession: m.workSession,
|
|
1713
1910
|
workSessions,
|
|
1911
|
+
session,
|
|
1714
1912
|
});
|
|
1715
1913
|
const messageContent = await buildCanonUserContent({ promptText, materialized });
|
|
1716
1914
|
session.enqueueInbound({
|
|
@@ -1747,9 +1945,6 @@ export async function main() {
|
|
|
1747
1945
|
console.error('[canon-host] SSE start error:', err instanceof Error ? err.message : err);
|
|
1748
1946
|
});
|
|
1749
1947
|
// ── Control signal watcher ──
|
|
1750
|
-
let controlStopped = false;
|
|
1751
|
-
const lastSeenControl = new Map();
|
|
1752
|
-
const lastSeenSignal = new Map();
|
|
1753
1948
|
const pollControl = async () => {
|
|
1754
1949
|
while (!controlStopped) {
|
|
1755
1950
|
for (const convoId of [...sessions.keys()]) {
|
|
@@ -1768,9 +1963,11 @@ export async function main() {
|
|
|
1768
1963
|
try {
|
|
1769
1964
|
await session.query.setModel(ctrl.model);
|
|
1770
1965
|
session.state.model = ctrl.model;
|
|
1966
|
+
session.clearRuntimeControlError('model');
|
|
1771
1967
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Model → ${ctrl.model}`);
|
|
1772
1968
|
}
|
|
1773
1969
|
catch (err) {
|
|
1970
|
+
session.setRuntimeControlError('model', ctrl.model, err);
|
|
1774
1971
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to set model:`, err);
|
|
1775
1972
|
}
|
|
1776
1973
|
}
|
|
@@ -1778,9 +1975,11 @@ export async function main() {
|
|
|
1778
1975
|
try {
|
|
1779
1976
|
await session.query.setPermissionMode(ctrl.permissionMode);
|
|
1780
1977
|
session.state.permissionMode = ctrl.permissionMode;
|
|
1978
|
+
session.clearRuntimeControlError('permissionMode');
|
|
1781
1979
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Mode → ${ctrl.permissionMode}`);
|
|
1782
1980
|
}
|
|
1783
1981
|
catch (err) {
|
|
1982
|
+
session.setRuntimeControlError('permissionMode', ctrl.permissionMode, err);
|
|
1784
1983
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to set mode:`, err);
|
|
1785
1984
|
}
|
|
1786
1985
|
}
|
|
@@ -1789,41 +1988,46 @@ export async function main() {
|
|
|
1789
1988
|
try {
|
|
1790
1989
|
await session.query.applyFlagSettings({ effortLevel: nextEffort });
|
|
1791
1990
|
session.state.effort = nextEffort;
|
|
1991
|
+
session.clearRuntimeControlError('effort');
|
|
1792
1992
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Effort → ${nextEffort}`);
|
|
1793
1993
|
}
|
|
1794
1994
|
catch (err) {
|
|
1995
|
+
session.setRuntimeControlError('effort', nextEffort, err);
|
|
1795
1996
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to set effort:`, err);
|
|
1796
1997
|
}
|
|
1797
1998
|
}
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
cwd: session.cwd,
|
|
1804
|
-
executionMode: session.environment.mode,
|
|
1805
|
-
...(session.environment.branch ? { executionBranch: session.environment.branch } : {}),
|
|
1806
|
-
...(session.environment.worktreePath ? { worktreePath: session.environment.worktreePath } : {}),
|
|
1807
|
-
...(resolveExecutionFallbackReason(session.environment)
|
|
1808
|
-
? { executionFallbackReason: resolveExecutionFallbackReason(session.environment) ?? undefined }
|
|
1809
|
-
: {}),
|
|
1810
|
-
hostMode: true,
|
|
1811
|
-
isActive: true,
|
|
1812
|
-
}).catch(() => { });
|
|
1999
|
+
else if (ctrl.effort && !nextEffort) {
|
|
2000
|
+
session.setRuntimeControlError('effort', String(ctrl.effort), new Error('Unsupported Claude thinking level.'));
|
|
2001
|
+
}
|
|
2002
|
+
session.writeState();
|
|
2003
|
+
await rtdbWrite(`/control/${convoId}/${agentId}/session`, null).catch(() => { });
|
|
1813
2004
|
}
|
|
1814
2005
|
}
|
|
1815
2006
|
}
|
|
1816
|
-
// Interrupt signal
|
|
2007
|
+
// Interrupt/stop-clear signal
|
|
1817
2008
|
const sig = await rtdbRead(`/control/${convoId}/${agentId}/signal`);
|
|
1818
2009
|
if (sig && typeof sig === 'object') {
|
|
1819
2010
|
const s = sig;
|
|
1820
2011
|
const ts = s.updatedAt ?? 0;
|
|
1821
|
-
if (s.type === 'interrupt' && ts > (lastSeenSignal.get(convoId) ?? 0)) {
|
|
2012
|
+
if ((s.type === 'interrupt' || s.type === 'stop_and_drop') && ts > (lastSeenSignal.get(convoId) ?? 0)) {
|
|
1822
2013
|
lastSeenSignal.set(convoId, ts);
|
|
1823
2014
|
const session = sessions.get(convoId);
|
|
1824
2015
|
if (session && !session.closed) {
|
|
1825
|
-
|
|
2016
|
+
const isActive = session.state.state === 'running';
|
|
2017
|
+
if (!isActive && (s.type !== 'stop_and_drop' || session.pendingInputs.length === 0)) {
|
|
2018
|
+
await rtdbWrite(`/control/${convoId}/${agentId}/signal`, null).catch(() => { });
|
|
2019
|
+
continue;
|
|
2020
|
+
}
|
|
2021
|
+
console.error(`[canon-host] [${convoId.slice(0, 8)}] ${s.type} signal`);
|
|
1826
2022
|
try {
|
|
2023
|
+
if (s.type === 'stop_and_drop') {
|
|
2024
|
+
const droppedInputs = session.pendingInputs.splice(0);
|
|
2025
|
+
await markSessionPendingInputsRejected(convoId, droppedInputs);
|
|
2026
|
+
}
|
|
2027
|
+
if (session.activeInput) {
|
|
2028
|
+
session.interruptedTurnKeys.add(session.activeInput.turnKey);
|
|
2029
|
+
}
|
|
2030
|
+
session.pendingFinalText = null;
|
|
1827
2031
|
session.turnState = 'interrupted';
|
|
1828
2032
|
runtimeState.writeTurnState(convoId, {
|
|
1829
2033
|
turnId: session.currentTurnId,
|
|
@@ -1835,7 +2039,9 @@ export async function main() {
|
|
|
1835
2039
|
...(session.currentTurnOpenedAt ? { openedAt: session.currentTurnOpenedAt } : {}),
|
|
1836
2040
|
completedAt: { '.sv': 'timestamp' },
|
|
1837
2041
|
}).catch(() => { });
|
|
1838
|
-
|
|
2042
|
+
if (isActive) {
|
|
2043
|
+
await session.query.interrupt();
|
|
2044
|
+
}
|
|
1839
2045
|
if (session.streamingTimer) {
|
|
1840
2046
|
clearTimeout(session.streamingTimer);
|
|
1841
2047
|
session.streamingTimer = null;
|
|
@@ -1843,9 +2049,10 @@ export async function main() {
|
|
|
1843
2049
|
session.streamingText = '';
|
|
1844
2050
|
runtimeState.clearStreaming(convoId).catch(() => { });
|
|
1845
2051
|
client.setTyping(convoId, false).catch(() => { });
|
|
2052
|
+
await rtdbWrite(`/control/${convoId}/${agentId}/signal`, null).catch(() => { });
|
|
1846
2053
|
}
|
|
1847
2054
|
catch (err) {
|
|
1848
|
-
console.error(`[canon-host] [${convoId.slice(0, 8)}]
|
|
2055
|
+
console.error(`[canon-host] [${convoId.slice(0, 8)}] ${s.type} failed:`, err);
|
|
1849
2056
|
}
|
|
1850
2057
|
}
|
|
1851
2058
|
}
|
|
@@ -1862,30 +2069,8 @@ export async function main() {
|
|
|
1862
2069
|
// ── Heartbeat (per-session) ──
|
|
1863
2070
|
const heartbeat = setInterval(() => {
|
|
1864
2071
|
for (const session of sessions.values()) {
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
permissionMode: session.state.permissionMode,
|
|
1868
|
-
effort: session.state.effort,
|
|
1869
|
-
contextUsage: session.state.contextUsage,
|
|
1870
|
-
cwd: session.cwd,
|
|
1871
|
-
executionMode: session.environment.mode,
|
|
1872
|
-
...(session.environment.branch ? { executionBranch: session.environment.branch } : {}),
|
|
1873
|
-
...(session.environment.worktreePath ? { worktreePath: session.environment.worktreePath } : {}),
|
|
1874
|
-
hostMode: true,
|
|
1875
|
-
isActive: true,
|
|
1876
|
-
}).catch(() => { });
|
|
1877
|
-
runtimeState.writeTurnState(session.conversationId, {
|
|
1878
|
-
turnId: session.currentTurnId,
|
|
1879
|
-
state: session.turnState,
|
|
1880
|
-
queueDepth: session.pendingInputs.length,
|
|
1881
|
-
currentSpeakerId: agentId,
|
|
1882
|
-
lastAcceptedIntent: session.lastAcceptedIntent,
|
|
1883
|
-
capabilities: CLAUDE_RUNTIME_CAPABILITIES,
|
|
1884
|
-
...(session.currentTurnOpenedAt ? { openedAt: session.currentTurnOpenedAt } : {}),
|
|
1885
|
-
...(session.turnState === 'idle' || session.turnState === 'completed' || session.turnState === 'interrupted'
|
|
1886
|
-
? { completedAt: { '.sv': 'timestamp' } }
|
|
1887
|
-
: {}),
|
|
1888
|
-
}).catch(() => { });
|
|
2072
|
+
session.writeState();
|
|
2073
|
+
session.writeTurn();
|
|
1889
2074
|
}
|
|
1890
2075
|
void publishRuntimeHeartbeat();
|
|
1891
2076
|
}, HEARTBEAT_MS);
|
|
@@ -1926,7 +2111,11 @@ export async function main() {
|
|
|
1926
2111
|
}
|
|
1927
2112
|
// ── Entry ───────────────────────────────────────────────────────────
|
|
1928
2113
|
runCli(import.meta.url, main, (err) => {
|
|
1929
|
-
|
|
2114
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2115
|
+
console.error(`[canon-host] ${message}`);
|
|
1930
2116
|
getActiveProfileLock()?.release();
|
|
1931
2117
|
process.exit(1);
|
|
2118
|
+
}, {
|
|
2119
|
+
name: 'canon-claude',
|
|
2120
|
+
help: HELP,
|
|
1932
2121
|
});
|
package/dist/register.js
CHANGED
|
@@ -13,6 +13,27 @@ import { ackRegistrationApproval, clearPendingRegistration, getOrCreatePendingRe
|
|
|
13
13
|
import { readFileSync } from 'node:fs';
|
|
14
14
|
import { parseArgs } from 'node:util';
|
|
15
15
|
import { runCli } from './cli-entry.js';
|
|
16
|
+
const HELP = `canon-register — register or reconnect a Claude Code agent in Canon
|
|
17
|
+
|
|
18
|
+
USAGE
|
|
19
|
+
canon-register --name <name> --description <text> --phone <e164> [flags]
|
|
20
|
+
|
|
21
|
+
REQUIRED
|
|
22
|
+
--name <name> Agent display name shown in Canon
|
|
23
|
+
--description <text> Short profile description
|
|
24
|
+
--phone <e164> Owner phone number, for example +15551234567
|
|
25
|
+
|
|
26
|
+
FLAGS
|
|
27
|
+
--profile <name> Local profile name in ~/.canon/agents.json
|
|
28
|
+
--base-url <url> Canon API base URL override
|
|
29
|
+
--help, -h Show this help
|
|
30
|
+
--version, -V Show package version
|
|
31
|
+
|
|
32
|
+
EXAMPLES
|
|
33
|
+
canon-register --name "My Claude" --description "Claude Code agent" --phone "+15551234567"
|
|
34
|
+
canon-register --name "Frontend" --description "React work" --phone "+15551234567" --profile frontend
|
|
35
|
+
|
|
36
|
+
After approval, start it with CANON_AGENT=<profile> canon-claude --cwd /path/to/project.`;
|
|
16
37
|
export async function main() {
|
|
17
38
|
setDefaultResultOrder('ipv4first');
|
|
18
39
|
const { values } = parseArgs({
|
|
@@ -102,4 +123,7 @@ export async function main() {
|
|
|
102
123
|
runCli(import.meta.url, main, (error) => {
|
|
103
124
|
console.error('[canon-register] Fatal error:', error);
|
|
104
125
|
process.exit(1);
|
|
126
|
+
}, {
|
|
127
|
+
name: 'canon-register',
|
|
128
|
+
help: HELP,
|
|
105
129
|
});
|
package/dist/server.js
CHANGED
|
@@ -13,6 +13,17 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextpro
|
|
|
13
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
|
+
const HELP = `canon-channel-server — Claude Code MCP channel server for Canon
|
|
17
|
+
|
|
18
|
+
USAGE
|
|
19
|
+
canon-channel-server
|
|
20
|
+
|
|
21
|
+
FLAGS
|
|
22
|
+
--help, -h Show this help
|
|
23
|
+
--version, -V Show package version
|
|
24
|
+
|
|
25
|
+
This command is normally launched by Claude Code as an MCP stdio server. For
|
|
26
|
+
phone-controlled local sessions, use canon-claude instead.`;
|
|
16
27
|
// ── MCP server ─────────────────────────────────────────────────────────
|
|
17
28
|
const server = new Server({ name: 'canon-channel', version: '0.1.0' }, {
|
|
18
29
|
capabilities: {
|
|
@@ -672,4 +683,7 @@ export async function main() {
|
|
|
672
683
|
runCli(import.meta.url, main, (err) => {
|
|
673
684
|
console.error('[canon] Fatal error:', err);
|
|
674
685
|
process.exit(1);
|
|
686
|
+
}, {
|
|
687
|
+
name: 'canon-channel-server',
|
|
688
|
+
help: HELP,
|
|
675
689
|
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import type { DeliveryIntent, ModelOption } from '@canonmsg/core';
|
|
3
|
+
export type ClaudeInputKind = 'seed' | 'canon';
|
|
4
|
+
export interface ClaudeInputEnvelope {
|
|
5
|
+
kind: ClaudeInputKind;
|
|
6
|
+
msg: SDKUserMessage;
|
|
7
|
+
intent: DeliveryIntent;
|
|
8
|
+
sourceMessageId: string | null;
|
|
9
|
+
markAccepted: boolean;
|
|
10
|
+
turnKey: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ClaudeRuntimeControlError {
|
|
13
|
+
value: string;
|
|
14
|
+
message: string;
|
|
15
|
+
updatedAt: number;
|
|
16
|
+
}
|
|
17
|
+
export interface ClaudePendingFinalDelivery {
|
|
18
|
+
turnKey: string;
|
|
19
|
+
text: string;
|
|
20
|
+
messageId: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function createClaudeInputEnvelope(input: {
|
|
23
|
+
kind: ClaudeInputKind;
|
|
24
|
+
msg: SDKUserMessage;
|
|
25
|
+
intent?: DeliveryIntent;
|
|
26
|
+
sourceMessageId?: string | null;
|
|
27
|
+
markAccepted?: boolean;
|
|
28
|
+
}): ClaudeInputEnvelope;
|
|
29
|
+
export declare function buildClaudeFinalMessageId(input: {
|
|
30
|
+
agentId: string;
|
|
31
|
+
conversationId: string;
|
|
32
|
+
turnKey: string;
|
|
33
|
+
}): string;
|
|
34
|
+
export declare function shouldDeliverClaudeFinal(input: {
|
|
35
|
+
turn: ClaudeInputEnvelope | null;
|
|
36
|
+
finalizedTurnKeys: ReadonlySet<string>;
|
|
37
|
+
interruptedTurnKeys: ReadonlySet<string>;
|
|
38
|
+
}): boolean;
|
|
39
|
+
export declare function mergeClaudeModelOptions(input: {
|
|
40
|
+
previous: ReadonlyArray<ModelOption>;
|
|
41
|
+
next: ReadonlyArray<ModelOption>;
|
|
42
|
+
selectedModel?: string | null;
|
|
43
|
+
}): ModelOption[];
|
|
44
|
+
export declare function formatClaudeControlError(error: unknown): string;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
export function createClaudeInputEnvelope(input) {
|
|
3
|
+
const sourceMessageId = input.sourceMessageId ?? null;
|
|
4
|
+
const fallbackId = randomUUID();
|
|
5
|
+
return {
|
|
6
|
+
kind: input.kind,
|
|
7
|
+
msg: input.msg,
|
|
8
|
+
intent: input.intent ?? 'queue',
|
|
9
|
+
sourceMessageId,
|
|
10
|
+
markAccepted: Boolean(input.markAccepted),
|
|
11
|
+
turnKey: input.kind === 'canon' && sourceMessageId
|
|
12
|
+
? `canon:${sourceMessageId}`
|
|
13
|
+
: `${input.kind}:${fallbackId}`,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function buildClaudeFinalMessageId(input) {
|
|
17
|
+
const digest = createHash('sha256')
|
|
18
|
+
.update(`${input.agentId}\n${input.conversationId}\n${input.turnKey}`)
|
|
19
|
+
.digest('hex')
|
|
20
|
+
.slice(0, 32);
|
|
21
|
+
return `claude-final-${digest}`;
|
|
22
|
+
}
|
|
23
|
+
export function shouldDeliverClaudeFinal(input) {
|
|
24
|
+
if (!input.turn || input.turn.kind !== 'canon')
|
|
25
|
+
return false;
|
|
26
|
+
if (input.finalizedTurnKeys.has(input.turn.turnKey))
|
|
27
|
+
return false;
|
|
28
|
+
if (input.interruptedTurnKeys.has(input.turn.turnKey))
|
|
29
|
+
return false;
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
export function mergeClaudeModelOptions(input) {
|
|
33
|
+
const byValue = new Map();
|
|
34
|
+
for (const option of input.previous) {
|
|
35
|
+
byValue.set(option.value, { ...option });
|
|
36
|
+
}
|
|
37
|
+
for (const option of input.next) {
|
|
38
|
+
byValue.set(option.value, { ...option });
|
|
39
|
+
}
|
|
40
|
+
if (input.selectedModel && !byValue.has(input.selectedModel)) {
|
|
41
|
+
byValue.set(input.selectedModel, {
|
|
42
|
+
value: input.selectedModel,
|
|
43
|
+
label: input.selectedModel,
|
|
44
|
+
description: 'Selected for this Canon session but not returned by the latest Claude model probe.',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const nextValues = new Set(input.next.map((option) => option.value));
|
|
48
|
+
return [...byValue.values()].map((option) => {
|
|
49
|
+
if (nextValues.has(option.value))
|
|
50
|
+
return option;
|
|
51
|
+
return {
|
|
52
|
+
...option,
|
|
53
|
+
description: option.description
|
|
54
|
+
? `${option.description} Previously advertised; not returned by the latest Claude model probe.`
|
|
55
|
+
: 'Previously advertised; not returned by the latest Claude model probe.',
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export function formatClaudeControlError(error) {
|
|
60
|
+
if (error instanceof Error && error.message.trim()) {
|
|
61
|
+
return error.message.trim();
|
|
62
|
+
}
|
|
63
|
+
if (typeof error === 'string' && error.trim()) {
|
|
64
|
+
return error.trim();
|
|
65
|
+
}
|
|
66
|
+
return 'Claude Code did not apply that control.';
|
|
67
|
+
}
|
package/dist/setup.js
CHANGED
|
@@ -10,6 +10,17 @@ import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
|
|
|
10
10
|
import { join, dirname } from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
import { runCli } from './cli-entry.js';
|
|
13
|
+
const HELP = `canon-setup — install Claude Code Canon skills and print MCP setup
|
|
14
|
+
|
|
15
|
+
USAGE
|
|
16
|
+
canon-setup
|
|
17
|
+
|
|
18
|
+
FLAGS
|
|
19
|
+
--help, -h Show this help
|
|
20
|
+
--version, -V Show package version
|
|
21
|
+
|
|
22
|
+
This command writes Canon Claude Code skill files under ~/.claude/skills and
|
|
23
|
+
prints the MCP server configuration for Claude Code channel mode.`;
|
|
13
24
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
25
|
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
15
26
|
const skillsDir = join(home, '.claude', 'skills');
|
|
@@ -70,4 +81,7 @@ export function main() {
|
|
|
70
81
|
runCli(import.meta.url, main, (error) => {
|
|
71
82
|
console.error('[canon-setup] Fatal error:', error);
|
|
72
83
|
process.exit(1);
|
|
84
|
+
}, {
|
|
85
|
+
name: 'canon-setup',
|
|
86
|
+
help: HELP,
|
|
73
87
|
});
|
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.5",
|
|
4
4
|
"description": "Canon channel plugin for Claude Code — messaging where AI agents are first-class citizens",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@anthropic-ai/claude-agent-sdk": "^0.2.101",
|
|
34
|
-
"@canonmsg/agent-sdk": "^1.
|
|
35
|
-
"@canonmsg/core": "^0.15.
|
|
34
|
+
"@canonmsg/agent-sdk": "^1.1.0",
|
|
35
|
+
"@canonmsg/core": "^0.15.1",
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.12.0"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|