@canonmsg/claude-code-plugin 0.32.0 → 0.34.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/.claude-plugin/plugin.json +1 -1
- package/dist/agent-context-state.d.ts +15 -0
- package/dist/agent-context-state.js +35 -0
- package/dist/canon-user-content.d.ts +3 -3
- package/dist/canon-user-content.js +4 -4
- package/dist/communication-policy.d.ts +2 -0
- package/dist/communication-policy.js +4 -0
- package/dist/host.d.ts +2 -2
- package/dist/host.js +128 -259
- package/dist/inbound-reply-authority.d.ts +42 -0
- package/dist/inbound-reply-authority.js +107 -0
- package/dist/mcp-args.d.ts +3 -0
- package/dist/mcp-args.js +12 -0
- package/dist/register.js +2 -1
- package/dist/server.js +253 -144
- package/dist/session-state.d.ts +6 -46
- package/dist/session-state.js +1 -74
- package/dist/tool-policy.js +2 -7
- package/package.json +6 -6
package/dist/host.js
CHANGED
|
@@ -29,13 +29,14 @@ import { query, } from '@anthropic-ai/claude-agent-sdk';
|
|
|
29
29
|
import { materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMessage, } from '@canonmsg/agent-sdk';
|
|
30
30
|
import { captureTurnArtifactSnapshot, createTurnArtifactRouter, IDLE_TIMEOUT_MS, collectMissedInboundMessages, createRecoveryCheckpointTracker, createReconnectRecoveryCoordinator, STARTUP_RECOVERY_PAGE_SIZE, } from '@canonmsg/coding-agent-host';
|
|
31
31
|
import { buildCanonUserContent, renderClaudeInboundContent, withClaudeReplyContextMediaReferences, } from './canon-user-content.js';
|
|
32
|
-
import { EFFORT_OPTIONS, CLAUDE_PERMISSION_MODE_OPTIONS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildCanonInboundFrameV1, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, normalizeRuntimeCommandDescriptors, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createTurnOutputController, createRuntimeStatePublisher, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, CanonClient, CanonStream, ControlChannelPoller, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, ApprovalManager, RuntimeRequestManager,
|
|
32
|
+
import { EFFORT_OPTIONS, CLAUDE_PERMISSION_MODE_OPTIONS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildCanonInboundFrameV1, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, normalizeRuntimeCommandDescriptors, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createTurnOutputController, createRuntimeStatePublisher, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, CanonClient, CanonStream, ControlChannelPoller, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, ApprovalManager, RuntimeRequestManager, FINAL_MESSAGE_HANDOFF_MS, buildLocalRuntimeId, getActiveProfileLock, heartbeatLocalRuntimeEntry, normalizeTurnMetadata, parseTurnVerbosityConfig, prepareConversationEnvironment, resolveCanonAgent, verifyResolvedAgentEnvironment, decideAutoReply, initRTDBAuth, isChunkedSendMessageError, sendMessageWithRetry, sendMessageWithRetryChunked, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, resolveLocalRuntimeSessionState, saveRuntimeSessionState, clearRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCodingHostInboundPrompt, resolveConfiguredWorkspaceCwd, resolveTurnVerbosity, shouldTriggerAgentTurn, upsertLocalRuntimeEntry, } from '@canonmsg/core';
|
|
33
33
|
import { runCli } from '@canonmsg/core';
|
|
34
|
-
import { CANON_VERB_MCP_SERVER_NAME, createCanonVerbMcpServer
|
|
34
|
+
import { CANON_VERB_MCP_SERVER_NAME, createCanonVerbMcpServer } from '@canonmsg/agent-tools';
|
|
35
35
|
import { synthesizeClaudeApprovalDiff } from './approval-diff.js';
|
|
36
36
|
import { decideClaudeToolPermissionForMode, parseAllowedNonOwnerClaudeTools, } from './tool-policy.js';
|
|
37
|
-
import {
|
|
37
|
+
import { boundClaudeFinalMetadata, buildClaudeFinalChunkingOptions, buildClaudeFinalMessageId, buildClaudeFinalTurnMetadata, buildClaudePendingFinalDelivery, buildClaudeTurnFailureNotice, buildTruncatedFinalText, buildUndeliverableFinalNotice, canDrainClaudeQueuedInput, beginClaudeAssistantResponse, claimClaudeTextSegmentId, claudeFinalWillChunk, classifyFinalDeliveryFailure, claudeInputOwnsTurnSlot, createClaudeTurnActivityState, decideClaudeControlSignalAction, decideClaudeInboundDispatch, dispatchClaudeInput, endClaudeAssistantResponse, isClaudeTurnSlotReserved, readClaudeFinalDeliveryResume, releaseClaudeTurnSlot, runClaudeExhaustedFinalDelivery, shouldApplyClaudeEchoedSessionState, shouldReleaseClaudeTurnSlot, isClaudeMainTurnMessage, openClaudeTurn, planClaudeAssistantTrail, planClaudeStreamingWrite, planClaudeToolCallStart, planClaudeToolProgress, planClaudeToolResults, claudeFinalTurnTrail, publishedClaudeTurnState, rememberDispatchedClaudeInput, resetClaudeTurnActivityState, shouldOpenClaudeTurnOnRunning, shouldStopTypingDotsOnStreamedText, isOpenClaudeTurnState, takeClaudeResultOwner, takeClaudeToolBlockIdByIndex, claudeModelInfoToOption, claudeOriginForCanonSender, composeClaudeTurnFinal, describeUndeliveredClaudeFinal, confirmClaudeInterrupt, createClaudeInputEnvelope, deriveClaudeSupplementalModelProbes, formatClaudeCliVersion, formatClaudeControlError, isClaudeCustomModelOption, isSupportedClaudeCliVersion, mergeClaudeDiscoveredModelOptions, parseClaudeCliVersion, resolveClaudeTurnResponseRouting, resolveClaudeModelOptions, resetClaudeCompletedTurnState, shouldDeliverClaudeFinal, shouldRouteClaudeTurnArtifacts, MINIMUM_CLAUDE_CLI_VERSION, } from './session-state.js';
|
|
38
38
|
import { CLAUDE_SUPPORTED_DIALOG_KINDS, buildClaudeAskUserPermissionDenied, buildClaudeAskUserPermissionResult, createClaudeUserDialogCoordinator, parseClaudeAskUserDialog, parseClaudeAskUserToolInput, resolveClaudeUserDialogRequestId, } from './user-dialog.js';
|
|
39
|
+
import { isProactiveCommunicationEnabled } from './communication-policy.js';
|
|
39
40
|
function parseRuntimeVisibilityPreset(value) {
|
|
40
41
|
return value === 'normal' || value === 'minimal' || value === 'full' ? value : undefined;
|
|
41
42
|
}
|
|
@@ -53,6 +54,8 @@ COMMON FLAGS
|
|
|
53
54
|
--cwd <path> Project directory to run Claude Code from
|
|
54
55
|
--workspace <path> Additional project to expose in Canon
|
|
55
56
|
--workspace-root <path> Discover projects under an approved root
|
|
57
|
+
--default-execution-mode <worktree|locked>
|
|
58
|
+
Default for new Canon conversations
|
|
56
59
|
--runtime-visibility <normal|minimal|full>
|
|
57
60
|
Detail visibility preset
|
|
58
61
|
--show-runtime-detail <field>
|
|
@@ -86,12 +89,10 @@ let workspaceRoots = [];
|
|
|
86
89
|
let workspaceRootMetadata = [];
|
|
87
90
|
let runtimeModels = [];
|
|
88
91
|
const CLAUDE_METADATA_TTL_MS = 5 * 60 * 1000;
|
|
92
|
+
const LOCAL_CONFIGURATION_REQUIRED_MESSAGE = 'This agent needs local runtime configuration before it can start this conversation. '
|
|
93
|
+
+ 'Its operator must configure it and retry.';
|
|
89
94
|
const allowedNonOwnerClaudeTools = parseAllowedNonOwnerClaudeTools(process.env.CANON_CLAUDE_NON_OWNER_ALLOWED_TOOLS);
|
|
90
|
-
/**
|
|
91
|
-
* Agent-developer setting, resolved once at startup. Deliberately NOT read from
|
|
92
|
-
* `/session-config`: that path is the USER's per-conversation control plane,
|
|
93
|
-
* and owner ruling 5 puts turn verbosity outside user control.
|
|
94
|
-
*/
|
|
95
|
+
/** Turn verbosity is local agent-developer configuration, resolved at startup. */
|
|
95
96
|
let configuredTurnVerbosity = null;
|
|
96
97
|
/**
|
|
97
98
|
* `--turn-verbosity` beats `CANON_TURN_VERBOSITY`; `null` means "unset, use the
|
|
@@ -271,6 +272,7 @@ function buildClaudeRuntimeDescriptor(input) {
|
|
|
271
272
|
workspaces: input.workspaces,
|
|
272
273
|
workspaceRoots: input.workspaceRoots,
|
|
273
274
|
executionModes: input.executionModes,
|
|
275
|
+
defaultExecutionMode: input.defaultExecutionMode,
|
|
274
276
|
permissionModes: CLAUDE_PERMISSION_MODE_OPTIONS,
|
|
275
277
|
defaultPermissionMode: 'default',
|
|
276
278
|
effortOptions: [...EFFORT_OPTIONS],
|
|
@@ -303,11 +305,7 @@ function buildClaudeRuntimeDescriptor(input) {
|
|
|
303
305
|
activation: { kind: 'control', controlId: 'permissionMode', value: 'plan' },
|
|
304
306
|
},
|
|
305
307
|
],
|
|
306
|
-
admissionActions:
|
|
307
|
-
...HOST_ADMISSION_ACTIONS_DISABLED,
|
|
308
|
-
requestContact: true,
|
|
309
|
-
reachOut: true,
|
|
310
|
-
},
|
|
308
|
+
admissionActions: HOST_ADMISSION_ACTIONS_DISABLED,
|
|
311
309
|
presentation: input.presentation,
|
|
312
310
|
commands,
|
|
313
311
|
});
|
|
@@ -419,32 +417,20 @@ function toModelOptions(models) {
|
|
|
419
417
|
async function publishAgentRuntime(agentId, runtime, rtdb) {
|
|
420
418
|
await publishHostAgentRuntime(agentId, 'claude-code', runtime, rtdb);
|
|
421
419
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
conversationId,
|
|
425
|
-
agentId,
|
|
426
|
-
rtdb,
|
|
427
|
-
extraStringFields: ['permissionMode', 'effort'],
|
|
428
|
-
retryMissingMs: 3_000,
|
|
429
|
-
});
|
|
430
|
-
return {
|
|
431
|
-
...config,
|
|
432
|
-
permissionMode: config?.permissionMode
|
|
433
|
-
? parseClaudePermissionMode(config.permissionMode) ?? undefined
|
|
434
|
-
: undefined,
|
|
435
|
-
effort: normalizeClaudeEffortLevel(config?.effort),
|
|
436
|
-
ultracode: parseClaudeUltracodeMode(config?.runtimeControlValues?.ultracode),
|
|
437
|
-
};
|
|
420
|
+
export function resolveSessionExecutionMode(defaultExecutionMode = 'worktree') {
|
|
421
|
+
return defaultExecutionMode;
|
|
438
422
|
}
|
|
439
|
-
function
|
|
440
|
-
if (
|
|
441
|
-
return
|
|
442
|
-
|
|
423
|
+
function resolveConfiguredDefaultExecutionMode(value) {
|
|
424
|
+
if (value == null || value === '')
|
|
425
|
+
return 'worktree';
|
|
426
|
+
if (value === 'worktree' || value === 'locked')
|
|
427
|
+
return value;
|
|
428
|
+
throw new Error('--default-execution-mode must be worktree or locked');
|
|
443
429
|
}
|
|
444
|
-
function resolveWorkspaceCwd(
|
|
445
|
-
return
|
|
430
|
+
function resolveWorkspaceCwd() {
|
|
431
|
+
return resolveConfiguredWorkspaceCwd({
|
|
446
432
|
workspaceOptions,
|
|
447
|
-
|
|
433
|
+
workspaceId: workspaceOptions[0]?.id,
|
|
448
434
|
defaultCwd: workingDir,
|
|
449
435
|
});
|
|
450
436
|
}
|
|
@@ -757,16 +743,6 @@ const CLAUDE_RUNTIME_CAPABILITIES = {
|
|
|
757
743
|
*/
|
|
758
744
|
const NO_REPLY_VERB = 'no_reply';
|
|
759
745
|
export const NO_REPLY_TOOL_NAME = `mcp__${CANON_VERB_MCP_SERVER_NAME}__${NO_REPLY_VERB}`;
|
|
760
|
-
export const REACH_OUT_TOOL_NAME = `mcp__${CANON_VERB_MCP_SERVER_NAME}__send_to`;
|
|
761
|
-
function ownerBoundRenderOptions(isOwner) {
|
|
762
|
-
const admissionActions = isOwner
|
|
763
|
-
? { ...HOST_ADMISSION_ACTIONS_DISABLED, requestContact: true, reachOut: true }
|
|
764
|
-
: HOST_ADMISSION_ACTIONS_DISABLED;
|
|
765
|
-
return {
|
|
766
|
-
admissionActions,
|
|
767
|
-
...(admissionActions.reachOut ? { reachOutToolName: REACH_OUT_TOOL_NAME } : {}),
|
|
768
|
-
};
|
|
769
|
-
}
|
|
770
746
|
export function buildCanonPrompt(input) {
|
|
771
747
|
return renderCodingHostInboundPrompt(buildCanonInboundFrameV1(buildCanonTurnContextV2({
|
|
772
748
|
content: input.content,
|
|
@@ -799,8 +775,8 @@ function resolveClaudeTurnModes(participantContext) {
|
|
|
799
775
|
}),
|
|
800
776
|
};
|
|
801
777
|
}
|
|
802
|
-
function renderInboundContent(message, materialized
|
|
803
|
-
return renderClaudeInboundContent(message, materialized
|
|
778
|
+
function renderInboundContent(message, materialized) {
|
|
779
|
+
return renderClaudeInboundContent(message, materialized);
|
|
804
780
|
}
|
|
805
781
|
async function materializePromptReplyContext(input) {
|
|
806
782
|
if (!input.replyContext?.found || !input.replyContext.attachments?.length) {
|
|
@@ -813,27 +789,17 @@ async function materializePromptReplyContext(input) {
|
|
|
813
789
|
});
|
|
814
790
|
return {
|
|
815
791
|
...result,
|
|
816
|
-
replyContext: withClaudeReplyContextMediaReferences(result.replyContext, result.materialized
|
|
792
|
+
replyContext: withClaudeReplyContextMediaReferences(result.replyContext, result.materialized),
|
|
817
793
|
};
|
|
818
794
|
}
|
|
819
795
|
catch (error) {
|
|
820
796
|
console.error(`${input.logPrefix} Failed to materialize replied-to media:`, error instanceof Error ? error.message : error);
|
|
821
797
|
return {
|
|
822
|
-
replyContext: withClaudeReplyContextMediaReferences(input.replyContext, []
|
|
798
|
+
replyContext: withClaudeReplyContextMediaReferences(input.replyContext, []),
|
|
823
799
|
materialized: [],
|
|
824
800
|
};
|
|
825
801
|
}
|
|
826
802
|
}
|
|
827
|
-
function ownerBoundReplyContactTarget(replyContext) {
|
|
828
|
-
const card = replyContext?.found ? replyContext.contactCard : undefined;
|
|
829
|
-
if (!card?.userId)
|
|
830
|
-
return undefined;
|
|
831
|
-
return {
|
|
832
|
-
targetUserId: card.userId,
|
|
833
|
-
...(card.canonContactId ? { canonContactId: card.canonContactId } : {}),
|
|
834
|
-
sourceCardMessageId: replyContext.messageId,
|
|
835
|
-
};
|
|
836
|
-
}
|
|
837
803
|
// ── Session factory ─────────────────────────────────────────────────
|
|
838
804
|
function createSession(conversationId, environment, agentId, client, typingSignals, runtimeState, onSessionEnd, onRuntimeDescriptorUpdate, config, resumeSessionId) {
|
|
839
805
|
const { cwd } = environment;
|
|
@@ -852,7 +818,7 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
852
818
|
messageQueue.push(input);
|
|
853
819
|
}
|
|
854
820
|
};
|
|
855
|
-
let enqueueInboundMessage = (msg, intent = 'queue', sourceMessageId, markAccepted = false, isOwnerTurn = false, requestingUserId = null, turnModes = {}
|
|
821
|
+
let enqueueInboundMessage = (msg, intent = 'queue', sourceMessageId, markAccepted = false, isOwnerTurn = false, requestingUserId = null, turnModes = {}) => {
|
|
856
822
|
sendInput(createClaudeInputEnvelope({
|
|
857
823
|
kind: 'canon',
|
|
858
824
|
msg,
|
|
@@ -861,7 +827,6 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
861
827
|
markAccepted,
|
|
862
828
|
isOwnerTurn,
|
|
863
829
|
requestingUserId,
|
|
864
|
-
replyContactTarget,
|
|
865
830
|
...turnModes,
|
|
866
831
|
}));
|
|
867
832
|
};
|
|
@@ -1008,24 +973,7 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1008
973
|
const turnKey = session.activeInput?.turnKey;
|
|
1009
974
|
if (turnKey)
|
|
1010
975
|
session.silencedTurnKeys.add(turnKey);
|
|
1011
|
-
}, {
|
|
1012
|
-
ownerBoundCommunication: {
|
|
1013
|
-
getContext: () => {
|
|
1014
|
-
const input = session.activeInput;
|
|
1015
|
-
if (!input?.isOwnerTurn || !input.sourceMessageId)
|
|
1016
|
-
return null;
|
|
1017
|
-
return {
|
|
1018
|
-
isOwnerTurn: true,
|
|
1019
|
-
conversationId,
|
|
1020
|
-
sourceMessageId: input.sourceMessageId,
|
|
1021
|
-
...(session.currentTurnId ? { turnId: session.currentTurnId } : {}),
|
|
1022
|
-
...(input.replyContactTarget
|
|
1023
|
-
? { replyContactTarget: input.replyContactTarget }
|
|
1024
|
-
: {}),
|
|
1025
|
-
};
|
|
1026
|
-
},
|
|
1027
|
-
},
|
|
1028
|
-
}),
|
|
976
|
+
}, { communication: config.communicationEnabled === true }),
|
|
1029
977
|
},
|
|
1030
978
|
},
|
|
1031
979
|
...(claudeCliPath ? { pathToClaudeCodeExecutable: claudeCliPath } : {}),
|
|
@@ -1214,7 +1162,7 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1214
1162
|
environment,
|
|
1215
1163
|
query: q,
|
|
1216
1164
|
sendInput,
|
|
1217
|
-
enqueueInbound: (msg, intent = 'queue', sourceMessageId, markAccepted = false, isOwnerTurn = false, requestingUserId = null, turnModes = {}
|
|
1165
|
+
enqueueInbound: (msg, intent = 'queue', sourceMessageId, markAccepted = false, isOwnerTurn = false, requestingUserId = null, turnModes = {}) => enqueueInboundMessage(msg, intent, sourceMessageId, markAccepted, isOwnerTurn, requestingUserId, turnModes),
|
|
1218
1166
|
state: {
|
|
1219
1167
|
model: undefined,
|
|
1220
1168
|
permissionMode: config.permissionMode,
|
|
@@ -1491,6 +1439,7 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1491
1439
|
deliveryIntent: session.lastAcceptedIntent,
|
|
1492
1440
|
suppressAutoReply: input.suppressAutoReply,
|
|
1493
1441
|
}),
|
|
1442
|
+
...(input.turn.replyAuthority ? { replyAuthority: input.turn.replyAuthority } : {}),
|
|
1494
1443
|
});
|
|
1495
1444
|
console.error(`[canon-host] [${conversationId.slice(0, 8)}] `
|
|
1496
1445
|
+ (degraded.length === input.finalText.length
|
|
@@ -1558,6 +1507,7 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1558
1507
|
messageId: finalMessageId,
|
|
1559
1508
|
...(selfContextId ? { selfContextId } : {}),
|
|
1560
1509
|
metadata,
|
|
1510
|
+
...(deliverTurn.replyAuthority ? { replyAuthority: deliverTurn.replyAuthority } : {}),
|
|
1561
1511
|
}, {}, buildClaudeFinalChunkingOptions(resume));
|
|
1562
1512
|
markFinalTurnDelivered(deliverTurn);
|
|
1563
1513
|
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Sent final reply (${finalText.length} chars`
|
|
@@ -1620,6 +1570,9 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1620
1570
|
}
|
|
1621
1571
|
function sendTurnArtifactFile(file) {
|
|
1622
1572
|
return sendMediaFileMessage(client, conversationId, file.path, '', {
|
|
1573
|
+
...(session.activeInput?.replyAuthority
|
|
1574
|
+
? { replyAuthority: session.activeInput.replyAuthority }
|
|
1575
|
+
: {}),
|
|
1623
1576
|
...(session.activeSelfContextId ? { selfContextId: session.activeSelfContextId } : {}),
|
|
1624
1577
|
metadata: buildArtifactMediaMetadata(),
|
|
1625
1578
|
});
|
|
@@ -1698,51 +1651,8 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1698
1651
|
}, FINAL_DELIVERY_RETRY_MS);
|
|
1699
1652
|
}
|
|
1700
1653
|
function writeState() {
|
|
1701
|
-
const runtimeControlErrors = Object.keys(session.runtimeControlErrors).length > 0
|
|
1702
|
-
? session.runtimeControlErrors
|
|
1703
|
-
: null;
|
|
1704
|
-
const controlState = {};
|
|
1705
|
-
const appliedAt = Date.now();
|
|
1706
|
-
if (session.state.model !== undefined) {
|
|
1707
|
-
controlState.model = { value: session.state.model, source: 'applied', appliedAt };
|
|
1708
|
-
}
|
|
1709
|
-
else if (config.model !== undefined) {
|
|
1710
|
-
controlState.model = { value: config.model, source: 'requested' };
|
|
1711
|
-
}
|
|
1712
|
-
if (session.state.permissionMode !== undefined) {
|
|
1713
|
-
controlState.permissionMode = { value: session.state.permissionMode, source: 'applied', appliedAt };
|
|
1714
|
-
}
|
|
1715
|
-
else if (config.permissionMode !== undefined) {
|
|
1716
|
-
controlState.permissionMode = { value: config.permissionMode, source: 'requested' };
|
|
1717
|
-
}
|
|
1718
|
-
if (session.state.effort !== undefined) {
|
|
1719
|
-
controlState.effort = { value: session.state.effort, source: 'applied', appliedAt };
|
|
1720
|
-
}
|
|
1721
|
-
else if (config.effort !== undefined) {
|
|
1722
|
-
controlState.effort = { value: config.effort, source: 'requested' };
|
|
1723
|
-
}
|
|
1724
|
-
if (session.state.ultracode !== undefined) {
|
|
1725
|
-
controlState.ultracode = { value: session.state.ultracode, source: 'applied', appliedAt };
|
|
1726
|
-
}
|
|
1727
|
-
else if (config.ultracode !== undefined) {
|
|
1728
|
-
controlState.ultracode = { value: config.ultracode, source: 'requested' };
|
|
1729
|
-
}
|
|
1730
1654
|
runtimeState.writeSessionState(conversationId, {
|
|
1731
|
-
model: session.state.model,
|
|
1732
|
-
permissionMode: session.state.permissionMode,
|
|
1733
|
-
effort: session.state.effort,
|
|
1734
|
-
...(session.state.ultracode ? { runtimeControlValues: { ultracode: session.state.ultracode } } : {}),
|
|
1735
|
-
controlState,
|
|
1736
|
-
runtimeControlErrors,
|
|
1737
1655
|
contextUsage: session.state.contextUsage,
|
|
1738
|
-
...(session.availableModels.length ? { availableModels: session.availableModels } : {}),
|
|
1739
|
-
cwd,
|
|
1740
|
-
executionMode: session.environment.mode,
|
|
1741
|
-
...(session.environment.branch ? { executionBranch: session.environment.branch } : {}),
|
|
1742
|
-
...(session.environment.worktreePath ? { worktreePath: session.environment.worktreePath } : {}),
|
|
1743
|
-
...(resolveExecutionFallbackReason(session.environment)
|
|
1744
|
-
? { executionFallbackReason: resolveExecutionFallbackReason(session.environment) ?? undefined }
|
|
1745
|
-
: {}),
|
|
1746
1656
|
hostMode: true,
|
|
1747
1657
|
isActive: true,
|
|
1748
1658
|
}).catch(() => { });
|
|
@@ -1968,7 +1878,7 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1968
1878
|
function hasInterruptibleSdkInput() {
|
|
1969
1879
|
return session.activeInput !== null && session.dispatchingInput === null;
|
|
1970
1880
|
}
|
|
1971
|
-
enqueueInboundMessage = (msg, intent = 'queue', sourceMessageId = null, markAccepted = false, isOwnerTurn = false, requestingUserId = null, turnModes = {}
|
|
1881
|
+
enqueueInboundMessage = (msg, intent = 'queue', sourceMessageId = null, markAccepted = false, isOwnerTurn = false, requestingUserId = null, turnModes = {}) => {
|
|
1972
1882
|
session.lastActivity = Date.now();
|
|
1973
1883
|
const input = createClaudeInputEnvelope({
|
|
1974
1884
|
kind: 'canon',
|
|
@@ -1978,7 +1888,6 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
1978
1888
|
markAccepted,
|
|
1979
1889
|
isOwnerTurn,
|
|
1980
1890
|
requestingUserId,
|
|
1981
|
-
replyContactTarget,
|
|
1982
1891
|
...turnModes,
|
|
1983
1892
|
});
|
|
1984
1893
|
const decision = decideClaudeInboundDispatch({
|
|
@@ -2430,11 +2339,13 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
2430
2339
|
availableModels: modelList,
|
|
2431
2340
|
availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
2432
2341
|
availableExecutionModes: config.availableExecutionModes,
|
|
2342
|
+
defaultExecutionMode: config.defaultExecutionMode,
|
|
2433
2343
|
runtimeDescriptor: buildClaudeRuntimeDescriptor({
|
|
2434
2344
|
models: modelList,
|
|
2435
2345
|
workspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
2436
2346
|
workspaceRoots: workspaceRootMetadata,
|
|
2437
2347
|
executionModes: config.availableExecutionModes,
|
|
2348
|
+
defaultExecutionMode: config.defaultExecutionMode,
|
|
2438
2349
|
presentation: config.runtimePresentation,
|
|
2439
2350
|
}),
|
|
2440
2351
|
});
|
|
@@ -2464,6 +2375,7 @@ export async function main() {
|
|
|
2464
2375
|
cwd: { type: 'string' },
|
|
2465
2376
|
workspace: { type: 'string', multiple: true },
|
|
2466
2377
|
'workspace-root': { type: 'string', multiple: true },
|
|
2378
|
+
'default-execution-mode': { type: 'string' },
|
|
2467
2379
|
'runtime-visibility': { type: 'string' },
|
|
2468
2380
|
'show-runtime-detail': { type: 'string', multiple: true },
|
|
2469
2381
|
'hide-runtime-detail': { type: 'string', multiple: true },
|
|
@@ -2476,6 +2388,7 @@ export async function main() {
|
|
|
2476
2388
|
env: process.env.CANON_TURN_VERBOSITY,
|
|
2477
2389
|
onWarning: (message) => console.error(`[canon-host] ${message}`),
|
|
2478
2390
|
});
|
|
2391
|
+
const defaultExecutionMode = resolveConfiguredDefaultExecutionMode(args['default-execution-mode']);
|
|
2479
2392
|
workingDir = (typeof args.cwd === 'string' ? args.cwd : null) || process.cwd();
|
|
2480
2393
|
const configuredWorkspaces = (args.workspace ?? []).filter((value) => typeof value === 'string');
|
|
2481
2394
|
const configuredWorkspaceRoots = (args['workspace-root'] ?? []).filter((value) => typeof value === 'string');
|
|
@@ -2516,11 +2429,13 @@ export async function main() {
|
|
|
2516
2429
|
let agentId;
|
|
2517
2430
|
let ownerId = null;
|
|
2518
2431
|
let ownerName = null;
|
|
2432
|
+
let communicationEnabled = false;
|
|
2519
2433
|
try {
|
|
2520
2434
|
const ctx = await client.getAgentMe();
|
|
2521
2435
|
agentId = ctx.agentId;
|
|
2522
2436
|
ownerId = ctx.ownerId;
|
|
2523
2437
|
ownerName = ctx.ownerName;
|
|
2438
|
+
communicationEnabled = isProactiveCommunicationEnabled(ctx.outboundPolicy);
|
|
2524
2439
|
console.error(`[canon-host] Connected as ${ctx.displayName || agentId}`);
|
|
2525
2440
|
}
|
|
2526
2441
|
catch {
|
|
@@ -2567,15 +2482,6 @@ export async function main() {
|
|
|
2567
2482
|
hostMode: true,
|
|
2568
2483
|
rtdb,
|
|
2569
2484
|
});
|
|
2570
|
-
const recoveryCheckpointTrackers = new Map();
|
|
2571
|
-
function recoveryCheckpointsFor(conversationId) {
|
|
2572
|
-
let tracker = recoveryCheckpointTrackers.get(conversationId);
|
|
2573
|
-
if (!tracker) {
|
|
2574
|
-
tracker = createClaudeRecoveryCheckpointTracker((messageId) => persistConversationRecoveryCursor(conversationId, messageId));
|
|
2575
|
-
recoveryCheckpointTrackers.set(conversationId, tracker);
|
|
2576
|
-
}
|
|
2577
|
-
return tracker;
|
|
2578
|
-
}
|
|
2579
2485
|
let streamConnected = false;
|
|
2580
2486
|
const hostAvailableExecutionModes = [
|
|
2581
2487
|
...EXECUTION_ENVIRONMENT_MODES,
|
|
@@ -2587,6 +2493,7 @@ export async function main() {
|
|
|
2587
2493
|
hide: stringArgs(args['hide-runtime-detail']),
|
|
2588
2494
|
});
|
|
2589
2495
|
let runtimeDescriptor = {
|
|
2496
|
+
defaultExecutionMode,
|
|
2590
2497
|
defaultWorkspaceId: workspaceOptions[0]?.id,
|
|
2591
2498
|
defaultPermissionMode: 'default',
|
|
2592
2499
|
availablePermissionModes: [...CLAUDE_PERMISSION_MODE_OPTIONS],
|
|
@@ -2597,6 +2504,7 @@ export async function main() {
|
|
|
2597
2504
|
workspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
2598
2505
|
workspaceRoots: workspaceRootMetadata,
|
|
2599
2506
|
executionModes: hostAvailableExecutionModes,
|
|
2507
|
+
defaultExecutionMode,
|
|
2600
2508
|
presentation: runtimePresentation,
|
|
2601
2509
|
}),
|
|
2602
2510
|
};
|
|
@@ -2617,25 +2525,6 @@ export async function main() {
|
|
|
2617
2525
|
agentId,
|
|
2618
2526
|
rtdb,
|
|
2619
2527
|
clientType: 'claude-code',
|
|
2620
|
-
runtime: runtimeDescriptor,
|
|
2621
|
-
workspaceOptions,
|
|
2622
|
-
defaultCwd: workingDir,
|
|
2623
|
-
extraSessionConfigFields: ['permissionMode', 'effort'],
|
|
2624
|
-
liveSessionConfigByConversation: new Map(Array.from(sessions.values()).map((session) => {
|
|
2625
|
-
const workspaceId = resolveWorkspaceIdForBaseCwd(session.environment.baseCwd);
|
|
2626
|
-
return [
|
|
2627
|
-
session.conversationId,
|
|
2628
|
-
{
|
|
2629
|
-
...(session.state.model ? { model: session.state.model } : {}),
|
|
2630
|
-
...(session.state.permissionMode ? { permissionMode: session.state.permissionMode } : {}),
|
|
2631
|
-
...(session.state.effort ? { effort: session.state.effort } : {}),
|
|
2632
|
-
...(session.state.ultracode ? { runtimeControlValues: { ultracode: session.state.ultracode } } : {}),
|
|
2633
|
-
...(workspaceId ? { workspaceId } : {}),
|
|
2634
|
-
executionMode: session.environment.mode,
|
|
2635
|
-
executionBranch: session.environment.branch ?? null,
|
|
2636
|
-
},
|
|
2637
|
-
];
|
|
2638
|
-
})),
|
|
2639
2528
|
}).catch((error) => {
|
|
2640
2529
|
console.error('[canon-host] Failed to publish session snapshots:', error);
|
|
2641
2530
|
});
|
|
@@ -2675,6 +2564,7 @@ export async function main() {
|
|
|
2675
2564
|
workspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
2676
2565
|
workspaceRoots: workspaceRootMetadata,
|
|
2677
2566
|
executionModes: hostAvailableExecutionModes,
|
|
2567
|
+
defaultExecutionMode,
|
|
2678
2568
|
presentation: runtimePresentation,
|
|
2679
2569
|
}),
|
|
2680
2570
|
};
|
|
@@ -2709,6 +2599,7 @@ export async function main() {
|
|
|
2709
2599
|
try {
|
|
2710
2600
|
runtimeModels = await detectRuntimeModels(workingDir);
|
|
2711
2601
|
runtimeDescriptor = {
|
|
2602
|
+
defaultExecutionMode,
|
|
2712
2603
|
defaultWorkspaceId: workspaceOptions[0]?.id,
|
|
2713
2604
|
defaultModel: runtimeModels[0]?.value,
|
|
2714
2605
|
defaultPermissionMode: 'default',
|
|
@@ -2721,12 +2612,14 @@ export async function main() {
|
|
|
2721
2612
|
workspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
2722
2613
|
workspaceRoots: workspaceRootMetadata,
|
|
2723
2614
|
executionModes: hostAvailableExecutionModes,
|
|
2615
|
+
defaultExecutionMode,
|
|
2724
2616
|
presentation: runtimePresentation,
|
|
2725
2617
|
}),
|
|
2726
2618
|
};
|
|
2727
2619
|
}
|
|
2728
2620
|
catch {
|
|
2729
2621
|
runtimeDescriptor = {
|
|
2622
|
+
defaultExecutionMode,
|
|
2730
2623
|
defaultWorkspaceId: workspaceOptions[0]?.id,
|
|
2731
2624
|
defaultPermissionMode: 'default',
|
|
2732
2625
|
availablePermissionModes: [...CLAUDE_PERMISSION_MODE_OPTIONS],
|
|
@@ -2737,6 +2630,7 @@ export async function main() {
|
|
|
2737
2630
|
workspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
2738
2631
|
workspaceRoots: workspaceRootMetadata,
|
|
2739
2632
|
executionModes: hostAvailableExecutionModes,
|
|
2633
|
+
defaultExecutionMode,
|
|
2740
2634
|
presentation: runtimePresentation,
|
|
2741
2635
|
}),
|
|
2742
2636
|
};
|
|
@@ -2759,14 +2653,23 @@ export async function main() {
|
|
|
2759
2653
|
}
|
|
2760
2654
|
// ── Session manager ──
|
|
2761
2655
|
const sessions = new Map();
|
|
2656
|
+
const recoveryCheckpointTrackers = new Map();
|
|
2657
|
+
function recoveryCheckpointsFor(conversationId) {
|
|
2658
|
+
let tracker = recoveryCheckpointTrackers.get(conversationId);
|
|
2659
|
+
if (!tracker) {
|
|
2660
|
+
tracker = createClaudeRecoveryCheckpointTracker((messageId) => persistConversationRecoveryCursor(conversationId, messageId));
|
|
2661
|
+
recoveryCheckpointTrackers.set(conversationId, tracker);
|
|
2662
|
+
}
|
|
2663
|
+
return tracker;
|
|
2664
|
+
}
|
|
2762
2665
|
const approvalManager = ownerId ? new ApprovalManager(client, agentId, ownerId) : null;
|
|
2763
2666
|
// Interactive runtime input (AskUserQuestion) works with or without an owner,
|
|
2764
2667
|
// so its poll engine is always present. Create + poll fold onto the built-in
|
|
2765
2668
|
// input descriptor via `request()` (see the AskUserQuestion canUseTool branch).
|
|
2766
2669
|
const runtimeInputManager = new RuntimeRequestManager(client, { agentId, ownerId: ownerId ?? '' });
|
|
2767
2670
|
const pendingSessionCreations = new Map();
|
|
2768
|
-
// Shared /control channel poller (claude host profile):
|
|
2769
|
-
//
|
|
2671
|
+
// Shared /control channel poller (claude host profile): signal keys,
|
|
2672
|
+
// sequential conversations, active/idle cadence + jitter sampled at
|
|
2770
2673
|
// cycle start, eager baselines at session creation, and nodes left in place
|
|
2771
2674
|
// when a handler throws (consumeOnError defaults to false for both keys).
|
|
2772
2675
|
const controlPoller = new ControlChannelPoller({
|
|
@@ -2788,7 +2691,6 @@ export async function main() {
|
|
|
2788
2691
|
pollOnStart: true,
|
|
2789
2692
|
conversationConcurrency: 'sequential',
|
|
2790
2693
|
handlers: {
|
|
2791
|
-
session: { handle: applyRuntimeSessionControl },
|
|
2792
2694
|
signal: { handle: handleRuntimeControlSignal },
|
|
2793
2695
|
},
|
|
2794
2696
|
onError: ({ scope, key, conversationId, error }) => {
|
|
@@ -2887,7 +2789,6 @@ export async function main() {
|
|
|
2887
2789
|
ownerName,
|
|
2888
2790
|
membershipChange: pendingMembershipChanges.get(input.conversationId) ?? null,
|
|
2889
2791
|
groupContextMode: getGroupContextMode(input.conversationId, conversation),
|
|
2890
|
-
renderOptions: input.renderOptions,
|
|
2891
2792
|
});
|
|
2892
2793
|
}
|
|
2893
2794
|
function closeSession(conversationId, options = {}) {
|
|
@@ -3024,22 +2925,47 @@ export async function main() {
|
|
|
3024
2925
|
evictOldestIdle();
|
|
3025
2926
|
}
|
|
3026
2927
|
const creation = (async () => {
|
|
3027
|
-
const
|
|
3028
|
-
const
|
|
3029
|
-
|
|
3030
|
-
const environment = prepareConversationEnvironment({
|
|
2928
|
+
const sessionExecutionMode = resolveSessionExecutionMode(defaultExecutionMode);
|
|
2929
|
+
const workspaceCwd = resolveWorkspaceCwd();
|
|
2930
|
+
let environment = prepareConversationEnvironment({
|
|
3031
2931
|
agentId,
|
|
3032
2932
|
conversationId,
|
|
3033
2933
|
workspaceCwd,
|
|
3034
2934
|
allowWorktrees: sessionExecutionMode === 'worktree',
|
|
3035
2935
|
});
|
|
3036
2936
|
try {
|
|
3037
|
-
const
|
|
3038
|
-
const persisted = loadRuntimeSessionState(runtimeId, {
|
|
2937
|
+
const persistedMapping = resolveLocalRuntimeSessionState(runtimeId, {
|
|
3039
2938
|
conversationId,
|
|
3040
2939
|
baseCwd: environment.baseCwd,
|
|
3041
2940
|
executionMode: environment.mode,
|
|
2941
|
+
resumeField: 'claudeSessionId',
|
|
2942
|
+
configuredBaseCwds: workspaceOptions.map((workspace) => workspace.cwd),
|
|
2943
|
+
availableExecutionModes: hostAvailableExecutionModes,
|
|
3042
2944
|
});
|
|
2945
|
+
if (persistedMapping.status === 'configuration_required') {
|
|
2946
|
+
throw new ExecutionEnvironmentError(persistedMapping.message, LOCAL_CONFIGURATION_REQUIRED_MESSAGE);
|
|
2947
|
+
}
|
|
2948
|
+
if (persistedMapping.status === 'adopted'
|
|
2949
|
+
&& (persistedMapping.state.baseCwd !== environment.baseCwd
|
|
2950
|
+
|| persistedMapping.state.executionMode !== environment.mode)) {
|
|
2951
|
+
const restoredEnvironment = prepareConversationEnvironment({
|
|
2952
|
+
agentId,
|
|
2953
|
+
conversationId,
|
|
2954
|
+
workspaceCwd: persistedMapping.state.baseCwd,
|
|
2955
|
+
allowWorktrees: persistedMapping.state.executionMode === 'worktree',
|
|
2956
|
+
});
|
|
2957
|
+
if (restoredEnvironment.mode !== persistedMapping.state.executionMode) {
|
|
2958
|
+
releaseConversationEnvironment(restoredEnvironment);
|
|
2959
|
+
throw new ExecutionEnvironmentError(`Conversation ${conversationId} requires local execution mode ${persistedMapping.state.executionMode}, but workspace ${persistedMapping.state.baseCwd} can only be opened in ${restoredEnvironment.mode} mode.`, LOCAL_CONFIGURATION_REQUIRED_MESSAGE);
|
|
2960
|
+
}
|
|
2961
|
+
releaseConversationEnvironment(environment);
|
|
2962
|
+
environment = restoredEnvironment;
|
|
2963
|
+
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Restoring saved local runtime → ${environment.mode} (${environment.baseCwd})`);
|
|
2964
|
+
}
|
|
2965
|
+
const resumeKey = buildConversationEnvironmentKey(conversationId, environment.baseCwd);
|
|
2966
|
+
const persisted = persistedMapping.status === 'new'
|
|
2967
|
+
? null
|
|
2968
|
+
: persistedMapping.state;
|
|
3043
2969
|
const resumeId = savedSessionIds.get(resumeKey) ?? persisted?.claudeSessionId;
|
|
3044
2970
|
const session = createSession(conversationId, environment, agentId, client, typingSignals, runtimeState, (convoId, sdkSessionId) => {
|
|
3045
2971
|
// Called when session processing loop ends — save session ID for resume
|
|
@@ -3080,18 +3006,20 @@ export async function main() {
|
|
|
3080
3006
|
runtimeDescriptor = descriptor;
|
|
3081
3007
|
void publishRuntimeHeartbeat();
|
|
3082
3008
|
}, {
|
|
3083
|
-
model:
|
|
3084
|
-
permissionMode:
|
|
3085
|
-
effort:
|
|
3086
|
-
ultracode:
|
|
3009
|
+
model: runtimeModels[0]?.value,
|
|
3010
|
+
permissionMode: 'default',
|
|
3011
|
+
effort: undefined,
|
|
3012
|
+
ultracode: undefined,
|
|
3087
3013
|
availableModels: runtimeModels,
|
|
3088
3014
|
availableExecutionModes: hostAvailableExecutionModes,
|
|
3015
|
+
defaultExecutionMode,
|
|
3089
3016
|
runtimePresentation,
|
|
3090
3017
|
approvalManager,
|
|
3091
3018
|
runtimeInputManager,
|
|
3092
3019
|
canRequestApproval: () => canRequestCanonApproval(conversationId),
|
|
3093
3020
|
runtimeId,
|
|
3094
3021
|
ownerId,
|
|
3022
|
+
communicationEnabled,
|
|
3095
3023
|
onInputCompleted: (sourceMessageId) => {
|
|
3096
3024
|
recoveryCheckpointsFor(conversationId).settle(sourceMessageId);
|
|
3097
3025
|
settleInboundMessageId(sourceMessageId, true);
|
|
@@ -3100,6 +3028,10 @@ export async function main() {
|
|
|
3100
3028
|
}, resumeId);
|
|
3101
3029
|
sessions.set(conversationId, session);
|
|
3102
3030
|
await controlPoller.baseline([conversationId]);
|
|
3031
|
+
await runtimeState.patchAgentSessionSnapshot(conversationId, {
|
|
3032
|
+
configurationStatus: 'ready',
|
|
3033
|
+
lastError: null,
|
|
3034
|
+
});
|
|
3103
3035
|
return session;
|
|
3104
3036
|
}
|
|
3105
3037
|
catch (error) {
|
|
@@ -3151,15 +3083,13 @@ export async function main() {
|
|
|
3151
3083
|
}
|
|
3152
3084
|
const sender = m.senderName || m.senderId;
|
|
3153
3085
|
const isOwner = m.isOwner ?? (ownerId != null && m.senderId === ownerId);
|
|
3154
|
-
const
|
|
3155
|
-
const content = renderInboundContent(m, materialized, renderOptions);
|
|
3086
|
+
const content = renderInboundContent(m, materialized);
|
|
3156
3087
|
const hydrated = await loadHydratedInboundContext({
|
|
3157
3088
|
conversationId: input.conversationId,
|
|
3158
3089
|
message: m,
|
|
3159
3090
|
senderName: sender,
|
|
3160
3091
|
isOwner,
|
|
3161
3092
|
hydratedPage: input.hydratedPage,
|
|
3162
|
-
renderOptions,
|
|
3163
3093
|
});
|
|
3164
3094
|
const behavior = input.hydratedPage?.behavior ?? hydrated.behavior;
|
|
3165
3095
|
const activeSelfContextId = hydrated.activeSelfContextId;
|
|
@@ -3169,7 +3099,6 @@ export async function main() {
|
|
|
3169
3099
|
agentId,
|
|
3170
3100
|
conversationId: input.conversationId,
|
|
3171
3101
|
logPrefix: `[canon-host] [${input.conversationId.slice(0, 8)}]`,
|
|
3172
|
-
renderOptions,
|
|
3173
3102
|
});
|
|
3174
3103
|
const replyContext = replyMedia.replyContext;
|
|
3175
3104
|
const participantContext = hydrated.participantContext;
|
|
@@ -3188,13 +3117,17 @@ export async function main() {
|
|
|
3188
3117
|
}
|
|
3189
3118
|
catch (error) {
|
|
3190
3119
|
const message = error instanceof Error ? error.message : String(error);
|
|
3191
|
-
const userMessage = error instanceof ExecutionEnvironmentError ? error.userMessage : message;
|
|
3192
3120
|
console.error(`[canon-host] [${input.conversationId.slice(0, 8)}] Failed to create recovered session: ${message}`);
|
|
3121
|
+
await runtimeState.patchAgentSessionSnapshot(input.conversationId, {
|
|
3122
|
+
configurationStatus: 'configuration_required',
|
|
3123
|
+
lastError: LOCAL_CONFIGURATION_REQUIRED_MESSAGE,
|
|
3124
|
+
}).catch(() => { });
|
|
3193
3125
|
await markQueuedMessageAcceptedForConversation(input.conversationId, m.id ?? null, shouldMarkAccepted);
|
|
3194
|
-
await sendMessageWithRetry(client, input.conversationId,
|
|
3126
|
+
await sendMessageWithRetry(client, input.conversationId, LOCAL_CONFIGURATION_REQUIRED_MESSAGE, {
|
|
3195
3127
|
messageId: `claude-start-failed-${m.id}`,
|
|
3196
3128
|
...(activeSelfContextId ? { selfContextId: activeSelfContextId } : {}),
|
|
3197
3129
|
metadata: {
|
|
3130
|
+
runtimeStatus: 'configuration_required',
|
|
3198
3131
|
turnSemantics: 'turn_complete',
|
|
3199
3132
|
replyBehavior: 'suppress_auto_reply',
|
|
3200
3133
|
},
|
|
@@ -3202,7 +3135,7 @@ export async function main() {
|
|
|
3202
3135
|
return 'handled';
|
|
3203
3136
|
}
|
|
3204
3137
|
session.activeSelfContextId = activeSelfContextId;
|
|
3205
|
-
const
|
|
3138
|
+
const promptText = buildCanonPrompt({
|
|
3206
3139
|
content,
|
|
3207
3140
|
conversationId: input.conversationId,
|
|
3208
3141
|
participantContext,
|
|
@@ -3213,12 +3146,6 @@ export async function main() {
|
|
|
3213
3146
|
replyContext,
|
|
3214
3147
|
message: m,
|
|
3215
3148
|
});
|
|
3216
|
-
const lifecycleContext = isOwner
|
|
3217
|
-
? formatPendingContactLifecycleContext(takeLocalRuntimeContactLifecycleEvents(runtimeId, input.conversationId))
|
|
3218
|
-
: null;
|
|
3219
|
-
const promptText = lifecycleContext
|
|
3220
|
-
? `${basePromptText}\n\n${lifecycleContext}`
|
|
3221
|
-
: basePromptText;
|
|
3222
3149
|
const messageContent = await buildCanonUserContent({
|
|
3223
3150
|
promptText,
|
|
3224
3151
|
materialized: [...replyMedia.materialized, ...materialized],
|
|
@@ -3236,7 +3163,7 @@ export async function main() {
|
|
|
3236
3163
|
senderId: m.senderId,
|
|
3237
3164
|
senderName: m.senderName,
|
|
3238
3165
|
}),
|
|
3239
|
-
}, deliveryIntent, m.id ?? null, shouldMarkAccepted, isOwner, m.senderType === 'human' ? m.senderId : null, turnModes
|
|
3166
|
+
}, deliveryIntent, m.id ?? null, shouldMarkAccepted, isOwner, m.senderType === 'human' ? m.senderId : null, turnModes);
|
|
3240
3167
|
return 'queued';
|
|
3241
3168
|
}
|
|
3242
3169
|
const acceptedInboundMessageIds = new Set();
|
|
@@ -3316,27 +3243,6 @@ export async function main() {
|
|
|
3316
3243
|
const conversationsDiscoveredWhileOffline = startupRecoveryComplete
|
|
3317
3244
|
? new Set([...knownConversationIds].filter((id) => !knownBeforeRefresh.has(id)))
|
|
3318
3245
|
: new Set();
|
|
3319
|
-
try {
|
|
3320
|
-
let cursor = readLocalRuntimeEntry(runtimeId)?.contactLifecycleCursor ?? null;
|
|
3321
|
-
for (let pageNumber = 0; pageNumber < 10; pageNumber += 1) {
|
|
3322
|
-
const page = await client.listContactRequestLifecyclePage({
|
|
3323
|
-
cursor,
|
|
3324
|
-
limit: 100,
|
|
3325
|
-
});
|
|
3326
|
-
await reconcileContactLifecycleEvents({
|
|
3327
|
-
requests: page.requests,
|
|
3328
|
-
requesterId: agentId,
|
|
3329
|
-
record: (request) => recordLocalRuntimeContactLifecycleEvent(runtimeId, request),
|
|
3330
|
-
});
|
|
3331
|
-
cursor = page.nextCursor;
|
|
3332
|
-
saveLocalRuntimeContactLifecycleCursor(runtimeId, cursor);
|
|
3333
|
-
if (!page.hasMore)
|
|
3334
|
-
break;
|
|
3335
|
-
}
|
|
3336
|
-
}
|
|
3337
|
-
catch (error) {
|
|
3338
|
-
console.error('[canon-host] Contact lifecycle recovery failed:', error instanceof Error ? error.message : error);
|
|
3339
|
-
}
|
|
3340
3246
|
for (const conversationId of knownConversationIds) {
|
|
3341
3247
|
const recoveryCheckpoints = recoveryCheckpointsFor(conversationId);
|
|
3342
3248
|
const recoveryBatch = recoveryCheckpoints.reserveBatch();
|
|
@@ -3436,8 +3342,7 @@ export async function main() {
|
|
|
3436
3342
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to materialize media:`, error instanceof Error ? error.message : error);
|
|
3437
3343
|
}
|
|
3438
3344
|
}
|
|
3439
|
-
const
|
|
3440
|
-
const content = renderInboundContent(m, materialized, renderOptions);
|
|
3345
|
+
const content = renderInboundContent(m, materialized);
|
|
3441
3346
|
const hydrated = await loadHydratedInboundContext({
|
|
3442
3347
|
conversationId: convoId,
|
|
3443
3348
|
message: m,
|
|
@@ -3446,7 +3351,6 @@ export async function main() {
|
|
|
3446
3351
|
activeSelfContextId: payload.activeSelfContextId,
|
|
3447
3352
|
selfContexts: payload.selfContexts,
|
|
3448
3353
|
provenance: payload.provenance,
|
|
3449
|
-
renderOptions,
|
|
3450
3354
|
});
|
|
3451
3355
|
const behavior = payload.behavior ?? hydrated.behavior;
|
|
3452
3356
|
const activeSelfContextId = hydrated.activeSelfContextId;
|
|
@@ -3456,7 +3360,6 @@ export async function main() {
|
|
|
3456
3360
|
agentId,
|
|
3457
3361
|
conversationId: convoId,
|
|
3458
3362
|
logPrefix: `[canon-host] [${convoId.slice(0, 8)}]`,
|
|
3459
|
-
renderOptions,
|
|
3460
3363
|
});
|
|
3461
3364
|
const replyContext = replyMedia.replyContext;
|
|
3462
3365
|
const participantContext = hydrated.participantContext;
|
|
@@ -3481,21 +3384,27 @@ export async function main() {
|
|
|
3481
3384
|
}
|
|
3482
3385
|
catch (error) {
|
|
3483
3386
|
const message = error instanceof Error ? error.message : String(error);
|
|
3484
|
-
const userMessage = error instanceof ExecutionEnvironmentError ? error.userMessage : message;
|
|
3485
3387
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to create session: ${message}`);
|
|
3388
|
+
await runtimeState.patchAgentSessionSnapshot(convoId, {
|
|
3389
|
+
configurationStatus: 'configuration_required',
|
|
3390
|
+
lastError: LOCAL_CONFIGURATION_REQUIRED_MESSAGE,
|
|
3391
|
+
}).catch(() => { });
|
|
3486
3392
|
await markQueuedMessageAcceptedForConversation(convoId, m.id ?? null, shouldMarkAccepted);
|
|
3487
|
-
await sendMessageWithRetry(client, convoId,
|
|
3393
|
+
await sendMessageWithRetry(client, convoId, LOCAL_CONFIGURATION_REQUIRED_MESSAGE, {
|
|
3488
3394
|
messageId: `claude-start-failed-${m.id}`,
|
|
3489
3395
|
...(activeSelfContextId ? { selfContextId: activeSelfContextId } : {}),
|
|
3490
3396
|
metadata: {
|
|
3397
|
+
turnId: `claude-start:${m.id}`,
|
|
3398
|
+
runtimeStatus: 'configuration_required',
|
|
3491
3399
|
turnSemantics: 'turn_complete',
|
|
3492
3400
|
replyBehavior: 'suppress_auto_reply',
|
|
3493
3401
|
},
|
|
3402
|
+
...(payload.replyAuthority ? { replyAuthority: payload.replyAuthority } : {}),
|
|
3494
3403
|
}).catch(() => { });
|
|
3495
3404
|
return false;
|
|
3496
3405
|
}
|
|
3497
3406
|
session.activeSelfContextId = activeSelfContextId;
|
|
3498
|
-
const
|
|
3407
|
+
const promptText = buildCanonPrompt({
|
|
3499
3408
|
content,
|
|
3500
3409
|
conversationId: convoId,
|
|
3501
3410
|
participantContext,
|
|
@@ -3506,17 +3415,14 @@ export async function main() {
|
|
|
3506
3415
|
replyContext,
|
|
3507
3416
|
message: m,
|
|
3508
3417
|
});
|
|
3509
|
-
const lifecycleContext = isOwner
|
|
3510
|
-
? formatPendingContactLifecycleContext(takeLocalRuntimeContactLifecycleEvents(runtimeId, convoId))
|
|
3511
|
-
: null;
|
|
3512
|
-
const promptText = lifecycleContext
|
|
3513
|
-
? `${basePromptText}\n\n${lifecycleContext}`
|
|
3514
|
-
: basePromptText;
|
|
3515
3418
|
const messageContent = await buildCanonUserContent({
|
|
3516
3419
|
promptText,
|
|
3517
3420
|
materialized: [...replyMedia.materialized, ...materialized],
|
|
3518
3421
|
});
|
|
3519
|
-
const turnModes =
|
|
3422
|
+
const turnModes = {
|
|
3423
|
+
...resolveClaudeTurnModes(participantContext),
|
|
3424
|
+
replyAuthority: payload.replyAuthority ?? null,
|
|
3425
|
+
};
|
|
3520
3426
|
session.enqueueInbound({
|
|
3521
3427
|
type: 'user',
|
|
3522
3428
|
message: {
|
|
@@ -3529,7 +3435,7 @@ export async function main() {
|
|
|
3529
3435
|
senderId: m.senderId,
|
|
3530
3436
|
senderName: m.senderName,
|
|
3531
3437
|
}),
|
|
3532
|
-
}, deliveryIntent, m.id ?? null, shouldMarkAccepted, isOwner, m.senderType === 'human' ? m.senderId : null, turnModes
|
|
3438
|
+
}, deliveryIntent, m.id ?? null, shouldMarkAccepted, isOwner, m.senderType === 'human' ? m.senderId : null, turnModes);
|
|
3533
3439
|
return true;
|
|
3534
3440
|
})().then((queued) => {
|
|
3535
3441
|
if (!queued) {
|
|
@@ -3547,24 +3453,20 @@ export async function main() {
|
|
|
3547
3453
|
onConversationUpdated: (payload) => {
|
|
3548
3454
|
handleConversationUpdated(payload);
|
|
3549
3455
|
},
|
|
3550
|
-
onContactRequestUpdated: (payload) => {
|
|
3551
|
-
if (payload.requesterId !== agentId || !payload.sourceConversationId)
|
|
3552
|
-
return;
|
|
3553
|
-
recordLocalRuntimeContactLifecycleEvent(runtimeId, payload);
|
|
3554
|
-
},
|
|
3555
3456
|
onReplayExpired: () => {
|
|
3556
3457
|
void reconnectRecovery.onReplayExpired().catch((error) => {
|
|
3557
|
-
console.error('[canon-host] Replay-
|
|
3458
|
+
console.error('[canon-host] Replay-expiry recovery failed:', error instanceof Error ? error.message : error);
|
|
3558
3459
|
});
|
|
3559
3460
|
},
|
|
3560
3461
|
onConnected: () => {
|
|
3561
3462
|
streamConnected = true;
|
|
3562
3463
|
void publishRuntimeHeartbeat();
|
|
3563
3464
|
const recovery = reconnectRecovery.onConnected();
|
|
3564
|
-
if (recovery)
|
|
3465
|
+
if (recovery) {
|
|
3565
3466
|
void recovery.catch((error) => {
|
|
3566
|
-
console.error('[canon-host] Reconnect recovery failed:', error);
|
|
3467
|
+
console.error('[canon-host] Reconnect recovery failed:', error instanceof Error ? error.message : error);
|
|
3567
3468
|
});
|
|
3469
|
+
}
|
|
3568
3470
|
console.error('[canon-host] SSE connected');
|
|
3569
3471
|
},
|
|
3570
3472
|
onDisconnected: () => {
|
|
@@ -3578,39 +3480,6 @@ export async function main() {
|
|
|
3578
3480
|
stream.start().catch((err) => {
|
|
3579
3481
|
console.error('[canon-host] SSE start error:', err instanceof Error ? err.message : err);
|
|
3580
3482
|
});
|
|
3581
|
-
// ── Control signal watcher ──
|
|
3582
|
-
// Session control signals (model, mode, effort). A missing/closed session
|
|
3583
|
-
// leaves the node in place; lastSeen still advances, so a stale control is
|
|
3584
|
-
// never replayed once a session exists.
|
|
3585
|
-
async function applyRuntimeSessionControl(event) {
|
|
3586
|
-
const convoId = event.conversationId;
|
|
3587
|
-
const session = sessions.get(convoId);
|
|
3588
|
-
if (!session || session.closed)
|
|
3589
|
-
return { consume: false };
|
|
3590
|
-
const ctrl = event.control;
|
|
3591
|
-
console.error(`[canon-host] [${convoId.slice(0, 8)}] Control:`, ctrl);
|
|
3592
|
-
await applyClaudeSessionControl({
|
|
3593
|
-
model: ctrl.model,
|
|
3594
|
-
permissionMode: ctrl.permissionMode,
|
|
3595
|
-
effort: ctrl.effort,
|
|
3596
|
-
ultracode: ctrl.runtimeControlValues?.ultracode,
|
|
3597
|
-
}, {
|
|
3598
|
-
state: session.state,
|
|
3599
|
-
parsePermissionMode: parseClaudePermissionMode,
|
|
3600
|
-
normalizeEffort: normalizeClaudeEffortLevel,
|
|
3601
|
-
parseUltracode: parseClaudeUltracodeMode,
|
|
3602
|
-
setModel: (model) => session.query.setModel(model),
|
|
3603
|
-
setPermissionMode: (mode) => session.query.setPermissionMode(mode),
|
|
3604
|
-
applyEffort: (level) => applyClaudeEffortLevel(session.query, level),
|
|
3605
|
-
applyUltracode: (enabled) => applyClaudeUltracode(session.query, enabled),
|
|
3606
|
-
setRuntimeControlError: (controlId, value, error) => session.setRuntimeControlError(controlId, value, error),
|
|
3607
|
-
clearRuntimeControlError: (controlId) => session.clearRuntimeControlError(controlId),
|
|
3608
|
-
// Publish the app-subscribed snapshot immediately so the control-pending
|
|
3609
|
-
// gate clears now instead of at the next 30s heartbeat (lag fix).
|
|
3610
|
-
publishSnapshot: () => { void publishSessionSnapshots([convoId]); },
|
|
3611
|
-
});
|
|
3612
|
-
session.writeState();
|
|
3613
|
-
}
|
|
3614
3483
|
// Stop/new-session signals remain pending until the Claude SDK confirms the
|
|
3615
3484
|
// interrupt. `defer` deliberately makes the poller revisit the same node.
|
|
3616
3485
|
async function handleRuntimeControlSignal(event) {
|