@canonmsg/claude-code-plugin 0.21.2 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/approval-diff.d.ts +1 -1
- package/dist/approval-diff.js +1 -1
- package/dist/approval-server.d.ts +1 -1
- package/dist/cli-entry.d.ts +2 -2
- package/dist/cli-entry.js +1 -1
- package/dist/host.d.ts +1 -1
- package/dist/host.js +1051 -698
- package/dist/inbound-policy.d.ts +22 -0
- package/dist/inbound-policy.js +46 -0
- package/dist/mcp-args.d.ts +1 -1
- package/dist/register.js +25 -73
- package/dist/server.js +11 -23
- package/dist/session-state.d.ts +6 -55
- package/dist/session-state.js +17 -199
- package/dist/startup-recovery.d.ts +46 -0
- package/dist/startup-recovery.js +59 -0
- package/dist/streaming-buffer.d.ts +16 -0
- package/dist/streaming-buffer.js +52 -0
- package/dist/tool-policy.d.ts +1 -2
- package/package.json +8 -10
- package/dist/bridge-bin.d.ts +0 -14
- package/dist/bridge-bin.js +0 -27
- package/dist/session-defaults.d.ts +0 -35
- package/dist/session-defaults.js +0 -124
- package/dist/subagent-activity.d.ts +0 -11
- package/dist/subagent-activity.js +0 -51
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type CanonGroupContext, type CanonGroupContextMode, type CanonConversation, type ResolvedAgentBehaviorPolicy } from '@canonmsg/core';
|
|
2
|
+
export interface InboundParticipantContext {
|
|
3
|
+
conversationType: CanonConversation['type'] | 'unknown';
|
|
4
|
+
memberCount: number | null;
|
|
5
|
+
senderType: 'human' | 'ai_agent';
|
|
6
|
+
senderName: string;
|
|
7
|
+
isOwner: boolean;
|
|
8
|
+
mentionedAgent: boolean;
|
|
9
|
+
groupContext?: CanonGroupContext;
|
|
10
|
+
groupContextMode?: CanonGroupContextMode;
|
|
11
|
+
recentSenderTypes: Array<'human' | 'ai_agent'>;
|
|
12
|
+
recentHumanCount: number;
|
|
13
|
+
recentAgentCount: number;
|
|
14
|
+
consecutiveAgentTurns: number;
|
|
15
|
+
currentAgentStreakStartedByHuman: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface AutoReplyDecision {
|
|
18
|
+
allow: boolean;
|
|
19
|
+
reason: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function buildInboundContextLines(context: InboundParticipantContext): string[];
|
|
22
|
+
export declare function decideAutoReply(context: InboundParticipantContext, behavior?: ResolvedAgentBehaviorPolicy | null): AutoReplyDecision;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { buildCompactGroupContextLines, evaluateParticipationPolicy, resolveAgentBehaviorPolicy, } from '@canonmsg/core';
|
|
2
|
+
function formatRecentSenders(senderTypes) {
|
|
3
|
+
if (senderTypes.length === 0)
|
|
4
|
+
return 'none';
|
|
5
|
+
return senderTypes.map((senderType) => (senderType === 'ai_agent' ? 'agent' : 'human')).join(' -> ');
|
|
6
|
+
}
|
|
7
|
+
export function buildInboundContextLines(context) {
|
|
8
|
+
const conversationTypeLabel = context.conversationType === 'unknown'
|
|
9
|
+
? 'unknown'
|
|
10
|
+
: `${context.conversationType}${context.memberCount ? ` (${context.memberCount} members)` : ''}`;
|
|
11
|
+
const senderRole = context.isOwner
|
|
12
|
+
? 'The latest sender is the verified human owner of this Canon agent.'
|
|
13
|
+
: context.senderType === 'ai_agent'
|
|
14
|
+
? 'The latest sender is another AI agent in Canon.'
|
|
15
|
+
: 'The latest sender is a human Canon participant.';
|
|
16
|
+
return [
|
|
17
|
+
senderRole,
|
|
18
|
+
`Latest sender name: ${context.senderName}`,
|
|
19
|
+
`Latest sender type: ${context.senderType}`,
|
|
20
|
+
`Conversation type: ${conversationTypeLabel}`,
|
|
21
|
+
...(context.groupContext && context.groupContextMode
|
|
22
|
+
? buildCompactGroupContextLines(context.groupContext, context.groupContextMode)
|
|
23
|
+
: []),
|
|
24
|
+
`Directly addressed to this agent: ${context.mentionedAgent ? 'yes' : 'no'}`,
|
|
25
|
+
`Recent sender pattern: ${formatRecentSenders(context.recentSenderTypes)}`,
|
|
26
|
+
`Recent human messages: ${context.recentHumanCount}`,
|
|
27
|
+
`Recent agent messages: ${context.recentAgentCount}`,
|
|
28
|
+
`Consecutive recent agent turns: ${context.consecutiveAgentTurns}`,
|
|
29
|
+
`Current agent streak started after a human message: ${context.currentAgentStreakStartedByHuman ? 'yes' : 'no'}`,
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
export function decideAutoReply(context, behavior) {
|
|
33
|
+
const decision = evaluateParticipationPolicy(behavior ?? resolveAgentBehaviorPolicy(), {
|
|
34
|
+
conversationType: context.conversationType,
|
|
35
|
+
senderType: context.senderType,
|
|
36
|
+
isOwner: context.isOwner,
|
|
37
|
+
mentionedAgent: context.mentionedAgent,
|
|
38
|
+
recentHumanCount: context.recentHumanCount,
|
|
39
|
+
consecutiveAgentTurns: context.consecutiveAgentTurns,
|
|
40
|
+
currentAgentStreakStartedByHuman: context.currentAgentStreakStartedByHuman,
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
allow: decision.allow,
|
|
44
|
+
reason: decision.reason,
|
|
45
|
+
};
|
|
46
|
+
}
|
package/dist/mcp-args.d.ts
CHANGED
package/dist/register.js
CHANGED
|
@@ -9,47 +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 {
|
|
13
|
-
import { clearPendingRegistration, getOrCreatePendingRegistration, updatePendingRegistration, upsertAgentProfile, AGENTS_PATH, } from '@canonmsg/core/local';
|
|
14
|
-
import { resolvePackagedBridgeBin } from './bridge-bin.js';
|
|
15
|
-
import { connectBridge } from '@canonmsg/framework';
|
|
12
|
+
import { ackRegistrationApproval, clearPendingRegistration, getOrCreatePendingRegistration, registerAndWaitForApproval, updatePendingRegistration, upsertAgentProfile, AGENTS_PATH, } from '@canonmsg/core';
|
|
16
13
|
import { readFileSync } from 'node:fs';
|
|
17
14
|
import { parseArgs } from 'node:util';
|
|
18
15
|
import { runCli } from './cli-entry.js';
|
|
19
|
-
/**
|
|
20
|
-
* Registration rides the bridge (Phase 6, same shape as the Hermes port): a
|
|
21
|
-
* short-lived PRE-IDENTITY registration daemon (offline stub +
|
|
22
|
-
* CANON_BRIDGE_REGISTRATION_BASE_URL, managed-child so it dies with this
|
|
23
|
-
* process) serves the three unauthenticated §B10 routes over JSON-RPC.
|
|
24
|
-
*/
|
|
25
|
-
async function openRegistrationBridge(baseUrl) {
|
|
26
|
-
return connectBridge({
|
|
27
|
-
profile: 'registration',
|
|
28
|
-
mode: 'managed-child',
|
|
29
|
-
...(resolvePackagedBridgeBin() ? { binPath: resolvePackagedBridgeBin() } : {}),
|
|
30
|
-
env: {
|
|
31
|
-
...process.env,
|
|
32
|
-
CANON_BRIDGE_AGENT_ID: 'canon-registration',
|
|
33
|
-
CANON_BRIDGE_REGISTRATION_BASE_URL: baseUrl,
|
|
34
|
-
},
|
|
35
|
-
hello: { clientType: 'claude-code', wantFamilies: [] },
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
function bridgeRegistrationTransport(bridge) {
|
|
39
|
-
return {
|
|
40
|
-
register: (_baseUrl, body) => bridge.client.call('register', body),
|
|
41
|
-
checkStatus: (_baseUrl, requestId, pollToken) => bridge.client.call('getRegistrationStatus', {
|
|
42
|
-
requestId,
|
|
43
|
-
...(pollToken ? { pollToken } : {}),
|
|
44
|
-
}),
|
|
45
|
-
ackRegistrationStatus: async (_baseUrl, requestId, pollToken) => {
|
|
46
|
-
await bridge.client.call('ackRegistrationStatus', {
|
|
47
|
-
requestId,
|
|
48
|
-
...(pollToken ? { pollToken } : {}),
|
|
49
|
-
});
|
|
50
|
-
},
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
16
|
const HELP = `canon-register — register or reconnect a Claude Code agent in Canon
|
|
54
17
|
|
|
55
18
|
USAGE
|
|
@@ -99,39 +62,29 @@ export async function main() {
|
|
|
99
62
|
}
|
|
100
63
|
console.log(`Registering agent "${values.name}" (profile: ${profileName})...`);
|
|
101
64
|
const pending = getOrCreatePendingRegistration(profileName, 'claude-code');
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
},
|
|
126
|
-
onPollUpdate: () => {
|
|
127
|
-
process.stdout.write('.');
|
|
128
|
-
},
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
catch (error) {
|
|
132
|
-
bridge.client.close();
|
|
133
|
-
throw error;
|
|
134
|
-
}
|
|
65
|
+
const result = await registerAndWaitForApproval({
|
|
66
|
+
name: values.name,
|
|
67
|
+
description: values.description,
|
|
68
|
+
ownerPhone: values.phone,
|
|
69
|
+
developerInfo: 'Claude Code plugin',
|
|
70
|
+
clientType: 'claude-code',
|
|
71
|
+
baseUrl: values['base-url'],
|
|
72
|
+
requestedAgentId: existingAgentId,
|
|
73
|
+
localRegistrationId: pending.localRegistrationId,
|
|
74
|
+
}, {
|
|
75
|
+
onSubmitted: (requestId, pollToken) => {
|
|
76
|
+
updatePendingRegistration(profileName, {
|
|
77
|
+
requestId,
|
|
78
|
+
pollToken,
|
|
79
|
+
clientType: 'claude-code',
|
|
80
|
+
});
|
|
81
|
+
console.log(`Registration submitted (request ID: ${requestId}).`);
|
|
82
|
+
console.log('Waiting for approval in Canon app...');
|
|
83
|
+
},
|
|
84
|
+
onPollUpdate: () => {
|
|
85
|
+
process.stdout.write('.');
|
|
86
|
+
},
|
|
87
|
+
});
|
|
135
88
|
console.log(''); // newline after dots
|
|
136
89
|
switch (result.status) {
|
|
137
90
|
case 'approved': {
|
|
@@ -148,7 +101,7 @@ export async function main() {
|
|
|
148
101
|
...(typeof values['base-url'] === 'string' ? { baseUrl: values['base-url'] } : {}),
|
|
149
102
|
});
|
|
150
103
|
if (result.requestId) {
|
|
151
|
-
await
|
|
104
|
+
await ackRegistrationApproval(values['base-url'], result.requestId, result.pollToken);
|
|
152
105
|
}
|
|
153
106
|
clearPendingRegistration(profileName);
|
|
154
107
|
console.log(`Approved! Agent: ${result.agentName} (${result.agentId})`);
|
|
@@ -168,7 +121,6 @@ export async function main() {
|
|
|
168
121
|
process.exit(1);
|
|
169
122
|
break;
|
|
170
123
|
}
|
|
171
|
-
bridge.client.close();
|
|
172
124
|
}
|
|
173
125
|
runCli(import.meta.url, main, (error) => {
|
|
174
126
|
console.error('[canon-register] Fatal error:', error);
|
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, buildLocalRuntimeId, markLocalRuntimeStopped, loadProfiles, isProfileLocked, resolveCanonAgent, releaseLock, upsertLocalRuntimeEntry, initRTDBAuth, renderCanonHostInboundContent, } from '@canonmsg/core';
|
|
13
|
+
import { CanonClient, CanonStream, ApprovalManager, buildLocalRuntimeId, getActiveProfileLock, markLocalRuntimeStopped, loadProfiles, isProfileLocked, resolveCanonAgent, getActiveProfile, releaseLock, upsertLocalRuntimeEntry, initRTDBAuth, writeSessionState, clearSessionState, renderCanonHostInboundContent, } from '@canonmsg/core';
|
|
14
14
|
import { materializeMessageMedia, sendMediaFileMessage, } from '@canonmsg/agent-sdk';
|
|
15
15
|
import { ApprovalHttpServer } from './approval-server.js';
|
|
16
16
|
import { runCli } from './cli-entry.js';
|
|
@@ -34,16 +34,6 @@ const server = new Server({ name: 'canon-channel', version: '0.1.0' }, {
|
|
|
34
34
|
},
|
|
35
35
|
});
|
|
36
36
|
let client = null;
|
|
37
|
-
// Scoped RTDB handle for this channel server's single agent. Held alongside
|
|
38
|
-
// the other per-process singletons (client, stream, ...) so both startChannel
|
|
39
|
-
// and the shutdown handler in main() write session state as this agent — not
|
|
40
|
-
// through any module-global default.
|
|
41
|
-
let rtdb = null;
|
|
42
|
-
// The one agent profile this server resolved+locked, and its lock handle.
|
|
43
|
-
// Held here (not via a core module-global) so the list_agents tool handler and
|
|
44
|
-
// the shutdown/error paths — all outside startChannel's scope — can read them.
|
|
45
|
-
let activeProfile = null;
|
|
46
|
-
let activeLockHandle = null;
|
|
47
37
|
let stream = null;
|
|
48
38
|
let agentContext = null;
|
|
49
39
|
let approvalManager = null;
|
|
@@ -396,7 +386,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
396
386
|
profile: name,
|
|
397
387
|
agentName: p.agentName,
|
|
398
388
|
agentId: p.agentId,
|
|
399
|
-
status:
|
|
389
|
+
status: getActiveProfile() === name ? 'active (this session)' : lock.locked ? `in use (PID ${lock.pid})` : 'available',
|
|
400
390
|
};
|
|
401
391
|
});
|
|
402
392
|
if (entries.length === 0) {
|
|
@@ -557,8 +547,6 @@ async function startChannel() {
|
|
|
557
547
|
profile = resolved.profile;
|
|
558
548
|
baseUrl = resolved.baseUrl;
|
|
559
549
|
profileAgentName = resolved.agentName;
|
|
560
|
-
activeProfile = resolved.profile;
|
|
561
|
-
activeLockHandle = resolved.lockHandle ?? null;
|
|
562
550
|
}
|
|
563
551
|
catch (err) {
|
|
564
552
|
console.error(err instanceof Error ? err.message : err);
|
|
@@ -589,7 +577,7 @@ async function startChannel() {
|
|
|
589
577
|
}
|
|
590
578
|
}
|
|
591
579
|
// Initialize RTDB auth for session state reporting
|
|
592
|
-
|
|
580
|
+
initRTDBAuth(client);
|
|
593
581
|
const runtimeId = buildLocalRuntimeId({
|
|
594
582
|
runtime: 'claude-code',
|
|
595
583
|
profile,
|
|
@@ -624,7 +612,7 @@ async function startChannel() {
|
|
|
624
612
|
// Write initial session state
|
|
625
613
|
const initialState = { cwd: process.cwd(), isActive: true };
|
|
626
614
|
for (const convoId of conversationCache.keys()) {
|
|
627
|
-
|
|
615
|
+
writeSessionState(convoId, agentId, initialState).catch((err) => console.error(`[canon] Failed to write initial state for ${convoId}:`, err));
|
|
628
616
|
}
|
|
629
617
|
// Start SSE stream
|
|
630
618
|
stream = new CanonStream({
|
|
@@ -677,10 +665,9 @@ export async function main() {
|
|
|
677
665
|
// Graceful shutdown — release agent lock and clear session state
|
|
678
666
|
const shutdown = () => {
|
|
679
667
|
// Mark all conversations as inactive
|
|
680
|
-
if (agentContext
|
|
681
|
-
const scopedRtdb = rtdb;
|
|
668
|
+
if (agentContext) {
|
|
682
669
|
for (const convoId of conversationCache.keys()) {
|
|
683
|
-
|
|
670
|
+
clearSessionState(convoId, agentContext.agentId).catch(() => { });
|
|
684
671
|
}
|
|
685
672
|
}
|
|
686
673
|
approvalManager?.dispose();
|
|
@@ -688,14 +675,15 @@ export async function main() {
|
|
|
688
675
|
stream?.stop();
|
|
689
676
|
const runtimeId = buildLocalRuntimeId({
|
|
690
677
|
runtime: 'claude-code',
|
|
691
|
-
profile:
|
|
678
|
+
profile: getActiveProfile(),
|
|
692
679
|
cwd: process.cwd(),
|
|
693
680
|
launchCommand: ['canon-channel-server'],
|
|
694
681
|
});
|
|
695
682
|
markLocalRuntimeStopped(runtimeId);
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
683
|
+
const ap = getActiveProfile();
|
|
684
|
+
if (ap)
|
|
685
|
+
releaseLock(ap);
|
|
686
|
+
getActiveProfileLock()?.release();
|
|
699
687
|
process.exit(0);
|
|
700
688
|
};
|
|
701
689
|
process.on('SIGINT', shutdown);
|
package/dist/session-state.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { DeliveryIntent, ModelOption, TurnLifecycleState } from '@canonmsg/core
|
|
3
|
-
import type { TurnArtifactSnapshot } from '@canonmsg/agent-host';
|
|
4
|
-
import { type ClaudeEffortLevel } from './session-defaults.js';
|
|
5
|
-
export type { ClaudeEffortLevel } from './session-defaults.js';
|
|
1
|
+
import type { SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import type { DeliveryIntent, ModelOption, TurnLifecycleState } from '@canonmsg/core';
|
|
3
|
+
import type { TurnArtifactSnapshot } from '@canonmsg/coding-agent-host';
|
|
6
4
|
export type ClaudeInputKind = 'seed' | 'canon';
|
|
7
5
|
export type ClaudeArtifactRoutingMode = 'workspace-generated' | 'disabled';
|
|
8
6
|
export interface ClaudeInputEnvelope {
|
|
@@ -21,47 +19,6 @@ export interface ClaudeRuntimeControlError {
|
|
|
21
19
|
message: string;
|
|
22
20
|
updatedAt: number;
|
|
23
21
|
}
|
|
24
|
-
export type ClaudePermissionMode = Parameters<Query['setPermissionMode']>[0];
|
|
25
|
-
export interface ClaudeSessionControl {
|
|
26
|
-
model?: string;
|
|
27
|
-
permissionMode?: string;
|
|
28
|
-
effort?: string;
|
|
29
|
-
runtimeControlValues?: Record<string, string>;
|
|
30
|
-
updatedAt?: number;
|
|
31
|
-
updatedBy?: string;
|
|
32
|
-
}
|
|
33
|
-
export interface ClaudeHostSessionState {
|
|
34
|
-
model?: string;
|
|
35
|
-
permissionMode?: string;
|
|
36
|
-
effort?: string;
|
|
37
|
-
fallbackModel?: string;
|
|
38
|
-
runtimeControlValues?: Record<string, string>;
|
|
39
|
-
state?: 'idle' | 'running' | 'requires_action';
|
|
40
|
-
contextUsage?: {
|
|
41
|
-
percentage: number;
|
|
42
|
-
totalTokens: number;
|
|
43
|
-
maxTokens: number;
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
export declare function parseClaudePermissionMode(value: unknown): ClaudePermissionMode | null;
|
|
47
|
-
export declare function normalizeClaudeEffortLevel(value: unknown): ClaudeEffortLevel | undefined;
|
|
48
|
-
export declare function buildClaudeInitialSessionState(input: {
|
|
49
|
-
model?: string;
|
|
50
|
-
permissionMode?: string;
|
|
51
|
-
effort?: string;
|
|
52
|
-
fallbackModel?: string;
|
|
53
|
-
runtimeControlValues?: Record<string, string>;
|
|
54
|
-
}): ClaudeHostSessionState;
|
|
55
|
-
export interface ClaudeSessionControlTarget {
|
|
56
|
-
state: ClaudeHostSessionState;
|
|
57
|
-
controlApplySeq: Map<string, number>;
|
|
58
|
-
closed: boolean;
|
|
59
|
-
query: Pick<Query, 'setModel' | 'setPermissionMode' | 'applyFlagSettings'>;
|
|
60
|
-
writeState: () => void;
|
|
61
|
-
setRuntimeControlError: (controlId: string, value: string, error: unknown) => void;
|
|
62
|
-
clearRuntimeControlError: (controlId: string) => void;
|
|
63
|
-
}
|
|
64
|
-
export declare function applyClaudeSessionControl(target: ClaudeSessionControlTarget, ctrl: ClaudeSessionControl): void;
|
|
65
22
|
export interface ClaudePendingFinalDelivery {
|
|
66
23
|
turnKey: string;
|
|
67
24
|
text: string;
|
|
@@ -103,15 +60,9 @@ export declare function shouldDeliverClaudeFinal(input: {
|
|
|
103
60
|
interruptedTurnKeys: ReadonlySet<string>;
|
|
104
61
|
}): boolean;
|
|
105
62
|
export declare function resetClaudeCompletedTurnState(session: ClaudeCompletedTurnState): void;
|
|
106
|
-
export
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
description?: string;
|
|
110
|
-
}
|
|
111
|
-
export declare function claudeModelInfoToOption(model: ClaudeModelInfoLike): ModelOption;
|
|
112
|
-
export declare function isClaudeCustomModelOption(option: ModelOption | null | undefined): boolean;
|
|
113
|
-
export declare function resolveClaudeModelOptions(input: {
|
|
114
|
-
discovered: ReadonlyArray<ModelOption>;
|
|
63
|
+
export declare function mergeClaudeModelOptions(input: {
|
|
64
|
+
previous: ReadonlyArray<ModelOption>;
|
|
65
|
+
next: ReadonlyArray<ModelOption>;
|
|
115
66
|
selectedModel?: string | null;
|
|
116
67
|
}): ModelOption[];
|
|
117
68
|
export declare function formatClaudeControlError(error: unknown): string;
|
package/dist/session-state.js
CHANGED
|
@@ -1,173 +1,4 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import { CLAUDE_EFFORT_LEVELS, claudeThinkingToFlagValue, parseClaudeThinkingMode, parseClaudeUltracodeMode, } from './session-defaults.js';
|
|
3
|
-
const CLAUDE_PERMISSION_MODE_VALUES = new Set([
|
|
4
|
-
'acceptEdits',
|
|
5
|
-
'auto',
|
|
6
|
-
'bypassPermissions',
|
|
7
|
-
'default',
|
|
8
|
-
'dontAsk',
|
|
9
|
-
'plan',
|
|
10
|
-
]);
|
|
11
|
-
const CLAUDE_EFFORT_VALUES = new Set(CLAUDE_EFFORT_LEVELS);
|
|
12
|
-
export function parseClaudePermissionMode(value) {
|
|
13
|
-
return typeof value === 'string' && CLAUDE_PERMISSION_MODE_VALUES.has(value)
|
|
14
|
-
? value
|
|
15
|
-
: null;
|
|
16
|
-
}
|
|
17
|
-
export function normalizeClaudeEffortLevel(value) {
|
|
18
|
-
return typeof value === 'string' && CLAUDE_EFFORT_VALUES.has(value)
|
|
19
|
-
? value
|
|
20
|
-
: undefined;
|
|
21
|
-
}
|
|
22
|
-
export function buildClaudeInitialSessionState(input) {
|
|
23
|
-
return {
|
|
24
|
-
...(input.model ? { model: input.model } : {}),
|
|
25
|
-
...(input.permissionMode ? { permissionMode: input.permissionMode } : {}),
|
|
26
|
-
...(input.effort ? { effort: input.effort } : {}),
|
|
27
|
-
...(input.fallbackModel ? { fallbackModel: input.fallbackModel } : {}),
|
|
28
|
-
...(input.runtimeControlValues ? { runtimeControlValues: input.runtimeControlValues } : {}),
|
|
29
|
-
state: 'idle',
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
function nextControlApplySeq(target, controlId) {
|
|
33
|
-
const nextSeq = (target.controlApplySeq.get(controlId) ?? 0) + 1;
|
|
34
|
-
target.controlApplySeq.set(controlId, nextSeq);
|
|
35
|
-
return nextSeq;
|
|
36
|
-
}
|
|
37
|
-
function applyOptimisticControl(input) {
|
|
38
|
-
const seq = nextControlApplySeq(input.target, input.controlId);
|
|
39
|
-
const applyPromise = (async () => input.apply())();
|
|
40
|
-
void applyPromise
|
|
41
|
-
.catch((error) => {
|
|
42
|
-
if (input.target.closed || input.target.controlApplySeq.get(input.controlId) !== seq) {
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
input.revert();
|
|
46
|
-
input.target.setRuntimeControlError(input.controlId, input.value, error);
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
function setRuntimeControlValue(state, controlId, value) {
|
|
50
|
-
state.runtimeControlValues = {
|
|
51
|
-
...(state.runtimeControlValues ?? {}),
|
|
52
|
-
[controlId]: value,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
export function applyClaudeSessionControl(target, ctrl) {
|
|
56
|
-
const nextModel = typeof ctrl.model === 'string' && ctrl.model.trim() ? ctrl.model.trim() : null;
|
|
57
|
-
if (ctrl.model !== undefined && !nextModel) {
|
|
58
|
-
target.setRuntimeControlError('model', String(ctrl.model), new Error('Unsupported Claude model.'));
|
|
59
|
-
}
|
|
60
|
-
else if (nextModel && nextModel !== target.state.model) {
|
|
61
|
-
const previous = target.state.model;
|
|
62
|
-
target.state.model = nextModel;
|
|
63
|
-
target.clearRuntimeControlError('model');
|
|
64
|
-
applyOptimisticControl({
|
|
65
|
-
target,
|
|
66
|
-
controlId: 'model',
|
|
67
|
-
value: nextModel,
|
|
68
|
-
apply: () => target.query.setModel(nextModel),
|
|
69
|
-
revert: () => {
|
|
70
|
-
if (previous === undefined)
|
|
71
|
-
delete target.state.model;
|
|
72
|
-
else
|
|
73
|
-
target.state.model = previous;
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
const nextPermissionMode = parseClaudePermissionMode(ctrl.permissionMode);
|
|
78
|
-
if (ctrl.permissionMode !== undefined && !nextPermissionMode) {
|
|
79
|
-
target.setRuntimeControlError('permissionMode', ctrl.permissionMode, new Error(`Unsupported Claude permission mode: ${ctrl.permissionMode}`));
|
|
80
|
-
}
|
|
81
|
-
else if (nextPermissionMode && nextPermissionMode !== target.state.permissionMode) {
|
|
82
|
-
// Claude Code applies permission-mode changes asynchronously. We publish
|
|
83
|
-
// the requested value immediately so Canon does not show a stale "requested"
|
|
84
|
-
// state while the SDK catches up, then revert if the current apply fails.
|
|
85
|
-
const previous = target.state.permissionMode;
|
|
86
|
-
target.state.permissionMode = nextPermissionMode;
|
|
87
|
-
target.clearRuntimeControlError('permissionMode');
|
|
88
|
-
applyOptimisticControl({
|
|
89
|
-
target,
|
|
90
|
-
controlId: 'permissionMode',
|
|
91
|
-
value: nextPermissionMode,
|
|
92
|
-
apply: () => target.query.setPermissionMode(nextPermissionMode),
|
|
93
|
-
revert: () => {
|
|
94
|
-
if (previous === undefined)
|
|
95
|
-
delete target.state.permissionMode;
|
|
96
|
-
else
|
|
97
|
-
target.state.permissionMode = previous;
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
const nextEffort = normalizeClaudeEffortLevel(ctrl.effort);
|
|
102
|
-
if (nextEffort && nextEffort !== target.state.effort) {
|
|
103
|
-
const previous = target.state.effort;
|
|
104
|
-
target.state.effort = nextEffort;
|
|
105
|
-
target.clearRuntimeControlError('effort');
|
|
106
|
-
applyOptimisticControl({
|
|
107
|
-
target,
|
|
108
|
-
controlId: 'effort',
|
|
109
|
-
value: nextEffort,
|
|
110
|
-
apply: () => target.query.applyFlagSettings({ effortLevel: nextEffort }),
|
|
111
|
-
revert: () => {
|
|
112
|
-
if (previous === undefined)
|
|
113
|
-
delete target.state.effort;
|
|
114
|
-
else
|
|
115
|
-
target.state.effort = previous;
|
|
116
|
-
},
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
else if (ctrl.effort !== undefined && !nextEffort) {
|
|
120
|
-
target.setRuntimeControlError('effort', String(ctrl.effort), new Error('Unsupported Claude thinking level.'));
|
|
121
|
-
}
|
|
122
|
-
const runtimeControlValues = ctrl.runtimeControlValues ?? {};
|
|
123
|
-
if ('thinking' in runtimeControlValues) {
|
|
124
|
-
const nextThinking = parseClaudeThinkingMode(runtimeControlValues.thinking);
|
|
125
|
-
if (!nextThinking) {
|
|
126
|
-
target.setRuntimeControlError('thinking', String(runtimeControlValues.thinking), new Error('Unsupported Claude thinking mode.'));
|
|
127
|
-
}
|
|
128
|
-
else if (nextThinking !== target.state.runtimeControlValues?.thinking) {
|
|
129
|
-
const previous = target.state.runtimeControlValues?.thinking;
|
|
130
|
-
setRuntimeControlValue(target.state, 'thinking', nextThinking);
|
|
131
|
-
target.clearRuntimeControlError('thinking');
|
|
132
|
-
applyOptimisticControl({
|
|
133
|
-
target,
|
|
134
|
-
controlId: 'thinking',
|
|
135
|
-
value: nextThinking,
|
|
136
|
-
apply: () => target.query.applyFlagSettings({ alwaysThinkingEnabled: claudeThinkingToFlagValue(nextThinking) }),
|
|
137
|
-
revert: () => {
|
|
138
|
-
if (previous === undefined)
|
|
139
|
-
delete target.state.runtimeControlValues?.thinking;
|
|
140
|
-
else
|
|
141
|
-
setRuntimeControlValue(target.state, 'thinking', previous);
|
|
142
|
-
},
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
if ('ultracode' in runtimeControlValues) {
|
|
147
|
-
const nextUltracode = parseClaudeUltracodeMode(runtimeControlValues.ultracode);
|
|
148
|
-
if (!nextUltracode) {
|
|
149
|
-
target.setRuntimeControlError('ultracode', String(runtimeControlValues.ultracode), new Error('Unsupported Claude ultracode mode.'));
|
|
150
|
-
}
|
|
151
|
-
else if (nextUltracode !== target.state.runtimeControlValues?.ultracode) {
|
|
152
|
-
const previous = target.state.runtimeControlValues?.ultracode;
|
|
153
|
-
setRuntimeControlValue(target.state, 'ultracode', nextUltracode);
|
|
154
|
-
target.clearRuntimeControlError('ultracode');
|
|
155
|
-
applyOptimisticControl({
|
|
156
|
-
target,
|
|
157
|
-
controlId: 'ultracode',
|
|
158
|
-
value: nextUltracode,
|
|
159
|
-
apply: () => target.query.applyFlagSettings({ ultracode: nextUltracode === 'on' }),
|
|
160
|
-
revert: () => {
|
|
161
|
-
if (previous === undefined)
|
|
162
|
-
delete target.state.runtimeControlValues?.ultracode;
|
|
163
|
-
else
|
|
164
|
-
setRuntimeControlValue(target.state, 'ultracode', previous);
|
|
165
|
-
},
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
target.writeState();
|
|
170
|
-
}
|
|
171
2
|
export function createClaudeInputEnvelope(input) {
|
|
172
3
|
const sourceMessageId = input.sourceMessageId ?? null;
|
|
173
4
|
const fallbackId = randomUUID();
|
|
@@ -215,45 +46,32 @@ export function resetClaudeCompletedTurnState(session) {
|
|
|
215
46
|
session.toolInProgress = false;
|
|
216
47
|
session.turnState = 'idle';
|
|
217
48
|
}
|
|
218
|
-
function
|
|
219
|
-
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
220
|
-
}
|
|
221
|
-
function displayLabelWithClaudeVersion(input) {
|
|
222
|
-
const label = input.displayName.trim() || input.value;
|
|
223
|
-
const description = normalizeModelDescription(input.description);
|
|
224
|
-
if (!description || label.includes('(') || /\d/.test(label))
|
|
225
|
-
return label;
|
|
226
|
-
const [summary] = description.split('·');
|
|
227
|
-
const versionedLabel = summary?.trim();
|
|
228
|
-
return versionedLabel
|
|
229
|
-
&& versionedLabel.toLowerCase().startsWith(label.toLowerCase())
|
|
230
|
-
&& /\d/.test(versionedLabel)
|
|
231
|
-
? versionedLabel
|
|
232
|
-
: label;
|
|
233
|
-
}
|
|
234
|
-
export function claudeModelInfoToOption(model) {
|
|
235
|
-
return {
|
|
236
|
-
value: model.value,
|
|
237
|
-
label: displayLabelWithClaudeVersion(model),
|
|
238
|
-
...(normalizeModelDescription(model.description) ? { description: normalizeModelDescription(model.description) } : {}),
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
export function isClaudeCustomModelOption(option) {
|
|
242
|
-
return option?.description?.trim().toLowerCase() === 'custom model';
|
|
243
|
-
}
|
|
244
|
-
export function resolveClaudeModelOptions(input) {
|
|
49
|
+
export function mergeClaudeModelOptions(input) {
|
|
245
50
|
const byValue = new Map();
|
|
246
|
-
for (const option of input.
|
|
51
|
+
for (const option of input.previous) {
|
|
52
|
+
byValue.set(option.value, { ...option });
|
|
53
|
+
}
|
|
54
|
+
for (const option of input.next) {
|
|
247
55
|
byValue.set(option.value, { ...option });
|
|
248
56
|
}
|
|
249
57
|
if (input.selectedModel && !byValue.has(input.selectedModel)) {
|
|
250
58
|
byValue.set(input.selectedModel, {
|
|
251
59
|
value: input.selectedModel,
|
|
252
60
|
label: input.selectedModel,
|
|
253
|
-
description: 'Selected for this Canon session but not returned by Claude model
|
|
61
|
+
description: 'Selected for this Canon session but not returned by the latest Claude model probe.',
|
|
254
62
|
});
|
|
255
63
|
}
|
|
256
|
-
|
|
64
|
+
const nextValues = new Set(input.next.map((option) => option.value));
|
|
65
|
+
return [...byValue.values()].map((option) => {
|
|
66
|
+
if (nextValues.has(option.value))
|
|
67
|
+
return option;
|
|
68
|
+
return {
|
|
69
|
+
...option,
|
|
70
|
+
description: option.description
|
|
71
|
+
? `${option.description} Previously advertised; not returned by the latest Claude model probe.`
|
|
72
|
+
: 'Previously advertised; not returned by the latest Claude model probe.',
|
|
73
|
+
};
|
|
74
|
+
});
|
|
257
75
|
}
|
|
258
76
|
export function formatClaudeControlError(error) {
|
|
259
77
|
if (error instanceof Error && error.message.trim()) {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Startup recovery for inbound messages missed while the host was offline.
|
|
3
|
+
*
|
|
4
|
+
* The host persists a `lastInboundMessageId` cursor per conversation. On
|
|
5
|
+
* startup we paginate `getMessagesPage` (newest-first pages, older pages via
|
|
6
|
+
* its `before` message-id parameter) until the cursor is found or a hard
|
|
7
|
+
* per-conversation bound is hit, then replay everything after the cursor.
|
|
8
|
+
*
|
|
9
|
+
* This module is intentionally identical in packages/claude-code-plugin and
|
|
10
|
+
* packages/codex-plugin — keep both copies in sync (future consolidation
|
|
11
|
+
* candidate).
|
|
12
|
+
*/
|
|
13
|
+
export declare const STARTUP_RECOVERY_PAGE_SIZE = 25;
|
|
14
|
+
export declare const STARTUP_RECOVERY_MAX_MESSAGES = 500;
|
|
15
|
+
export interface StartupRecoveryMessage {
|
|
16
|
+
id: string;
|
|
17
|
+
senderId: string;
|
|
18
|
+
createdAt?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface StartupRecoveryPage {
|
|
21
|
+
messages: StartupRecoveryMessage[];
|
|
22
|
+
}
|
|
23
|
+
export type StartupRecoveryMode =
|
|
24
|
+
/** Cursor found — `messages` is everything strictly after it. */
|
|
25
|
+
'after-cursor'
|
|
26
|
+
/** Cursor present but not found within the bound — `messages` is the bounded recent window. */
|
|
27
|
+
| 'truncated-window'
|
|
28
|
+
/**
|
|
29
|
+
* No usable cursor (fresh runtime file, or the cursor message no longer
|
|
30
|
+
* exists in history) — only the newest inbound message is recovered, since
|
|
31
|
+
* a full-history replay could fire mass duplicate turns.
|
|
32
|
+
*/
|
|
33
|
+
| 'latest-only';
|
|
34
|
+
export interface StartupRecoveryResult<TPage extends StartupRecoveryPage> {
|
|
35
|
+
mode: StartupRecoveryMode;
|
|
36
|
+
/** Missed inbound messages (own messages excluded), oldest first. */
|
|
37
|
+
messages: TPage['messages'];
|
|
38
|
+
/** First page fetched — reusable as hydration context for recovered turns. */
|
|
39
|
+
newestPage: TPage;
|
|
40
|
+
}
|
|
41
|
+
export declare function collectMissedInboundMessages<TPage extends StartupRecoveryPage>(input: {
|
|
42
|
+
fetchPage: (before?: string) => Promise<TPage>;
|
|
43
|
+
cursor: string | null | undefined;
|
|
44
|
+
agentId: string;
|
|
45
|
+
maxMessages?: number;
|
|
46
|
+
}): Promise<StartupRecoveryResult<TPage>>;
|