@canonmsg/core 0.12.0 → 0.14.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.
@@ -0,0 +1,256 @@
1
+ import { buildAgentSessionSnapshot } from './agent-session.js';
2
+ import { buildConversationWorktreeSpec, normalizeOptionalString, readSessionWorkspaceConfig, resolveConfiguredWorkspaceCwd, } from './execution-environment.js';
3
+ import { buildBehaviorPolicyLines, buildParticipationHistorySnapshot, } from './policy.js';
4
+ import { buildWorkSessionsPromptLines, mergeWorkSessionContexts, } from './work-session.js';
5
+ import { rtdbRead } from './rtdb-rest.js';
6
+ import { createRuntimeStatePublisher } from './runtime-state-publisher.js';
7
+ const HOST_INBOUND_CONTACT_CARD_ACTION_CAPABILITIES = Object.freeze({
8
+ canStartDirectConversation: false,
9
+ canSendContactRequest: false,
10
+ canApprovePendingContactRequests: false,
11
+ canRejectPendingContactRequests: false,
12
+ });
13
+ export function buildCanonHostPrompt(input) {
14
+ const resolvedWorkSessions = mergeWorkSessionContexts(input.workSession, input.workSessions);
15
+ return [
16
+ `You are connected to Canon messaging through a ${input.hostLabel} host wrapper.`,
17
+ 'Only the last assistant message from this turn will be delivered as the permanent Canon reply.',
18
+ 'Short intermediate assistant messages may be shown as ephemeral status while you work.',
19
+ ...input.buildInboundContextLines(input.participantContext),
20
+ ...buildBehaviorPolicyLines(input.behavior),
21
+ ...buildWorkSessionsPromptLines(resolvedWorkSessions),
22
+ 'Canon participants may be humans or AI agents.',
23
+ 'Honor the Canon behavior policy above when deciding how proactively to participate.',
24
+ ...(resolvedWorkSessions.length > 0
25
+ ? ['Honor the Canon work-session context above within its stated disclosure limits.']
26
+ : []),
27
+ `Conversation ID: ${input.conversationId}`,
28
+ '',
29
+ 'New Canon message:',
30
+ input.content,
31
+ ].join('\n');
32
+ }
33
+ /**
34
+ * Render the **text portion** of an inbound Canon message. Images are
35
+ * referenced by short placeholders — their actual bytes are delivered to the
36
+ * host as native vision/media inputs (Codex `-i <file>`, Anthropic image
37
+ * blocks). URLs are intentionally *not* inlined, since the harness never
38
+ * needs to refetch and earlier `[Image: <url>]` inlining caused vision
39
+ * models to see a string about an image instead of the image itself.
40
+ *
41
+ * `materialized` may be passed so non-image attachments can reference a
42
+ * local path the agent can Read. Without it we fall back to an unadorned
43
+ * placeholder; the vision path still works because image args carry the
44
+ * file path directly.
45
+ */
46
+ export function renderCanonHostInboundContent(message, materialized) {
47
+ const body = message.text || '';
48
+ const placeholders = [];
49
+ const attachments = message.attachments ?? [];
50
+ for (let i = 0; i < attachments.length; i += 1) {
51
+ const att = attachments[i];
52
+ const mat = materialized?.find((m) => m.index === i) ?? null;
53
+ placeholders.push(describeAttachment(att, mat));
54
+ }
55
+ if (message.contentType === 'contact_card' && message.contactCard) {
56
+ placeholders.push(describeContactCard(message.contactCard));
57
+ }
58
+ const rendered = [...placeholders, body].filter(Boolean).join('\n');
59
+ return rendered || '[Empty message]';
60
+ }
61
+ function describeContactCard(card) {
62
+ const parts = [`${card.userType} · userId: ${card.userId}`];
63
+ if (card.ownerName)
64
+ parts.push(`owner: ${card.ownerName}`);
65
+ if (card.about)
66
+ parts.push(`about: ${card.about}`);
67
+ const identity = `📇 Contact card: "${card.displayName}" (${parts.join(' · ')}).`;
68
+ const missingCapabilities = [
69
+ !HOST_INBOUND_CONTACT_CARD_ACTION_CAPABILITIES.canStartDirectConversation
70
+ ? 'start a direct conversation'
71
+ : null,
72
+ !HOST_INBOUND_CONTACT_CARD_ACTION_CAPABILITIES.canSendContactRequest
73
+ ? 'send a contact request'
74
+ : null,
75
+ !HOST_INBOUND_CONTACT_CARD_ACTION_CAPABILITIES.canApprovePendingContactRequests
76
+ ? 'approve pending requests'
77
+ : null,
78
+ !HOST_INBOUND_CONTACT_CARD_ACTION_CAPABILITIES.canRejectPendingContactRequests
79
+ ? 'reject pending requests'
80
+ : null,
81
+ ].filter(Boolean).join(', ');
82
+ const hint = `This host can inspect the card, but Canon admission actions are missing here. Missing capabilities: ${missingCapabilities}. Use another Canon surface for userId ${card.userId}.`;
83
+ return `${identity}\n${hint}`;
84
+ }
85
+ function describeAttachment(attachment, materialized) {
86
+ if (attachment.kind === 'image') {
87
+ return '[Image attached]';
88
+ }
89
+ if (attachment.kind === 'audio') {
90
+ const durationMs = materialized?.durationMs ?? attachment.durationMs;
91
+ const duration = durationMs ? ` (${Math.round(durationMs / 1000)}s)` : '';
92
+ const ref = materialized?.path ? ` ${materialized.path}` : '';
93
+ return `[Voice message${duration}${ref}]`;
94
+ }
95
+ // file
96
+ const label = materialized?.fileName ?? attachment.fileName ?? 'File';
97
+ const ref = materialized?.path ? ` ${materialized.path}` : '';
98
+ return `[File: ${label}${ref}]`;
99
+ }
100
+ export function buildHydratedInboundContext(input) {
101
+ const history = buildParticipationHistorySnapshot(input.page?.messages ?? [], input.agentId);
102
+ return {
103
+ participantContext: {
104
+ conversationType: input.conversation?.type ?? 'unknown',
105
+ memberCount: input.conversation?.memberIds?.length ?? null,
106
+ senderType: input.message.senderType ?? 'human',
107
+ senderName: input.senderName,
108
+ isOwner: input.isOwner,
109
+ mentionedAgent: Array.isArray(input.message.mentions) && input.message.mentions.includes(input.agentId),
110
+ recentSenderTypes: history.recentSenderTypes,
111
+ recentHumanCount: history.recentHumanCount,
112
+ recentAgentCount: history.recentAgentCount,
113
+ consecutiveAgentTurns: history.consecutiveAgentTurns,
114
+ currentAgentStreakStartedByHuman: history.currentAgentStreakStartedByHuman,
115
+ },
116
+ behavior: input.page?.behavior ?? input.conversation?.behavior,
117
+ workSessions: input.page?.workSessions ?? [],
118
+ hydratedFromPage: input.page != null,
119
+ };
120
+ }
121
+ export async function publishHostAgentRuntime(agentId, clientType, runtime) {
122
+ await createRuntimeStatePublisher({ agentId, clientType, hostMode: true })
123
+ .publishAgentRuntime(runtime);
124
+ }
125
+ export async function publishHostSessionSnapshots(input) {
126
+ if (input.conversationIds.length === 0) {
127
+ return;
128
+ }
129
+ const publisher = createRuntimeStatePublisher({
130
+ agentId: input.agentId,
131
+ clientType: input.clientType,
132
+ hostMode: true,
133
+ });
134
+ await Promise.all(input.conversationIds.map(async (conversationId) => {
135
+ const persistedConfig = await loadHostSessionConfig({
136
+ conversationId,
137
+ agentId: input.agentId,
138
+ extraStringFields: input.extraSessionConfigFields ?? ['permissionMode'],
139
+ });
140
+ const liveConfig = input.liveSessionConfigByConversation?.get(conversationId) ?? null;
141
+ const mergedConfig = {
142
+ ...(persistedConfig ?? {}),
143
+ ...(liveConfig ?? {}),
144
+ };
145
+ const snapshot = buildAgentSessionSnapshot({
146
+ conversationId,
147
+ agentId: input.agentId,
148
+ runtime: {
149
+ ...input.runtime,
150
+ clientType: input.clientType,
151
+ hostMode: true,
152
+ },
153
+ sessionConfig: {
154
+ ...(mergedConfig.model ? { model: mergedConfig.model } : {}),
155
+ ...(mergedConfig.permissionMode ? { permissionMode: mergedConfig.permissionMode } : {}),
156
+ ...(mergedConfig.effort ? { effort: mergedConfig.effort } : {}),
157
+ ...(mergedConfig.runtimeControlValues
158
+ ? { runtimeControlValues: mergedConfig.runtimeControlValues }
159
+ : {}),
160
+ ...(mergedConfig.workspaceId ? { workspaceId: mergedConfig.workspaceId } : {}),
161
+ ...(mergedConfig.executionMode ? { executionMode: mergedConfig.executionMode } : {}),
162
+ },
163
+ lastHeartbeatAt: undefined,
164
+ });
165
+ let executionBranch = liveConfig?.executionBranch ?? null;
166
+ if (!executionBranch && snapshot.executionMode === 'worktree' && snapshot.workspaceId) {
167
+ const workspace = input.workspaceOptions.find((option) => option.id === snapshot.workspaceId);
168
+ if (workspace) {
169
+ executionBranch = buildConversationWorktreeSpec({
170
+ agentId: input.agentId,
171
+ conversationId,
172
+ workspaceCwd: workspace.cwd,
173
+ }).branch;
174
+ }
175
+ }
176
+ return publisher.patchAgentSessionSnapshot(conversationId, {
177
+ clientType: input.clientType,
178
+ hostMode: true,
179
+ model: snapshot.model ?? null,
180
+ permissionMode: snapshot.permissionMode ?? null,
181
+ effort: snapshot.effort ?? null,
182
+ runtimeControlValues: snapshot.runtimeControlValues ?? null,
183
+ workspaceId: snapshot.workspaceId ?? null,
184
+ executionMode: snapshot.executionMode ?? null,
185
+ executionBranch,
186
+ modelOptions: snapshot.modelOptions,
187
+ permissionModeOptions: snapshot.permissionModeOptions,
188
+ workspaceOptions: snapshot.workspaceOptions,
189
+ availableExecutionModes: snapshot.availableExecutionModes,
190
+ lastHeartbeatAt: { '.sv': 'timestamp' },
191
+ });
192
+ }));
193
+ }
194
+ export function readHostSessionConfig(raw, extraStringFields = []) {
195
+ const baseConfig = readSessionWorkspaceConfig(raw);
196
+ if (!raw || typeof raw !== 'object') {
197
+ return baseConfig;
198
+ }
199
+ const data = raw;
200
+ const extraConfig = Object.fromEntries(extraStringFields.flatMap((field) => {
201
+ const value = normalizeOptionalString(data[field]);
202
+ return value ? [[field, value]] : [];
203
+ }));
204
+ const runtimeControlValues = Object.fromEntries(Object.entries(data.runtimeControlValues && typeof data.runtimeControlValues === 'object'
205
+ ? data.runtimeControlValues
206
+ : {}).flatMap(([key, value]) => {
207
+ const normalizedValue = normalizeOptionalString(value);
208
+ return normalizedValue ? [[key, normalizedValue]] : [];
209
+ }));
210
+ return {
211
+ ...(baseConfig ?? {}),
212
+ ...extraConfig,
213
+ ...(Object.keys(runtimeControlValues).length > 0 ? { runtimeControlValues } : {}),
214
+ };
215
+ }
216
+ export async function loadHostSessionConfig(input) {
217
+ const raw = await rtdbRead(`/session-config/${input.conversationId}/${input.agentId}`);
218
+ return readHostSessionConfig(raw, input.extraStringFields);
219
+ }
220
+ export function resolveHostWorkspaceCwd(input) {
221
+ return resolveConfiguredWorkspaceCwd(input);
222
+ }
223
+ export function createConversationMetadataLoader(input) {
224
+ const cacheTtlMs = input.cacheTtlMs ?? 10_000;
225
+ let conversationCacheLoadedAt = 0;
226
+ async function refreshConversationCache(force = false) {
227
+ if (!force
228
+ && input.conversationCache.size > 0
229
+ && Date.now() - conversationCacheLoadedAt < cacheTtlMs) {
230
+ return;
231
+ }
232
+ const conversations = await input.client.getConversations();
233
+ input.conversationCache.clear();
234
+ for (const conversation of conversations) {
235
+ input.conversationCache.set(conversation.id, conversation);
236
+ }
237
+ conversationCacheLoadedAt = Date.now();
238
+ }
239
+ async function getConversationMeta(conversationId) {
240
+ try {
241
+ await refreshConversationCache();
242
+ const cached = input.conversationCache.get(conversationId);
243
+ if (cached)
244
+ return cached;
245
+ await refreshConversationCache(true);
246
+ return input.conversationCache.get(conversationId) ?? null;
247
+ }
248
+ catch {
249
+ return input.conversationCache.get(conversationId) ?? null;
250
+ }
251
+ }
252
+ return {
253
+ refreshConversationCache,
254
+ getConversationMeta,
255
+ };
256
+ }
package/dist/index.d.ts CHANGED
@@ -6,13 +6,14 @@ export { buildConfiguredWorkspaceOptionsWithRoots, buildPublicWorkspaceRoots, bu
6
6
  export type { ConfiguredWorkspaceRoot, WorkspaceDiscoveryResult, } from './workspace-discovery.js';
7
7
  export { CanonClient, CanonApiError } from './client.js';
8
8
  export { buildAgentSessionSnapshot } from './agent-session.js';
9
+ export { CLAUDE_EFFORT_OPTIONS, EXECUTION_MODE_CONTROL_OPTIONS, buildFirstPartyCodingRuntimeDescriptor, buildRuntimeEffortControl, buildRuntimeExecutionModeControl, buildRuntimeExecutionModeOptions, buildRuntimeModelControl, buildRuntimePermissionModeControl, buildRuntimeWorkspaceControl, buildRuntimeWorkspaceControlOptions, } from './runtime-descriptor.js';
9
10
  export { CanonStream } from './stream.js';
10
11
  export type { StreamHandler } from './stream.js';
11
12
  export type { PolicyRole, ParticipationStyle, RepresentationMode, PermissionLevel, ConversationScope, AgentBehaviorSettings, Participant, Relationship, ContextOverlay, BehaviorProfile, AdmissionPolicy, RuntimeControlPolicy, ActionApprovalPolicy, ParticipationPolicy, WorkSession, ResolvedPolicy, ResolvedTurnEligibility, ResolvedAgentBehaviorPolicy, ParticipationHistoryMessage, ParticipationHistorySnapshot, ParticipationDecisionInput, ParticipationDecision, } from './policy.js';
12
13
  export { buildParticipationHistorySnapshot, buildParticipationHistorySnapshots, buildBehaviorPolicyLines, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, evaluateParticipationPolicy, getDefaultParticipationPolicy, resolveAgentBehaviorPolicy, } from './policy.js';
13
14
  export { DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, FINAL_MESSAGE_HANDOFF_MS, isTurnOpen, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
14
15
  export type { DeliveryIntent, TurnMessageSemantics, InboundDisposition, TurnLifecycleState, RuntimeCapabilities, HostAdmissionActionCapabilities, TurnState, TurnMetadata, TriggerDecision, } from './turn-protocol.js';
15
- export { registerAndWaitForApproval } from './registration.js';
16
+ export { ackRegistrationApproval, registerAndWaitForApproval, submitRegistrationRequest, waitForRegistrationApproval, } from './registration.js';
16
17
  export { ApprovalManager } from './approval-manager.js';
17
18
  export { generateApprovalId, buildApprovalRequest, buildApprovalReply, buildApprovalOutcome, parseTextApprovalReply, redactSecrets, } from './approval-format.js';
18
19
  export { DEFAULT_APPROVAL_CONFIG, } from './approval-types.js';
@@ -21,14 +22,20 @@ export { buildPlanApprovalReply, buildPlanApprovalRequest, buildQuestionReply, b
21
22
  export type { ClaudeQuestionMetadata, ClaudeQuestionReplyMetadata, PlanApprovalMetadata, PlanApprovalReplyMetadata, RuntimeQuestionDefinition, RuntimeQuestionOption, } from './runtime-cards.js';
22
23
  export { createStreamingHelper } from './streaming.js';
23
24
  export type { RTDBHandle, RTDBRef, ServerTimestamp, StreamingHelperOptions, StreamingNode } from './streaming.js';
24
- export { loadProfiles, isProfileLocked, acquireLock, releaseLock, isProcessAlive, CANON_DIR, AGENTS_PATH, LOCKS_DIR, } from './agent-profiles.js';
25
- export type { AgentProfile } from './agent-profiles.js';
26
- export { resolveCanonAgent, resolveCanonProfile, getActiveProfile } from './agent-resolver.js';
25
+ export { clearPendingRegistration, getOrCreatePendingRegistration, loadPendingRegistrations, loadProfiles, savePendingRegistrations, saveProfiles, updatePendingRegistration, upsertAgentProfile, isProfileLocked, acquireLock, releaseLock, isProcessAlive, CANON_DIR, AGENTS_PATH, LOCKS_DIR, } from './agent-profiles.js';
26
+ export type { AgentProfile, PendingRegistration, ProfileLockHandle } from './agent-profiles.js';
27
+ export { resolveCanonAgent, resolveCanonProfile, getActiveProfile, getActiveProfileLock } from './agent-resolver.js';
27
28
  export type { ResolvedAgent } from './agent-resolver.js';
29
+ export { RUNTIMES_DIR, buildLocalRuntimeId, clearRuntimeSessionState, describeProfileLock, heartbeatLocalRuntimeEntry, listLocalRuntimeEntries, loadRuntimeSessionState, markLocalRuntimeStopped, readLocalRuntimeEntry, removeLocalRuntimeEntry, saveRuntimeSessionState, upsertLocalRuntimeEntry, } from './local-runtime-catalog.js';
30
+ export type { LocalRuntimeCatalogEntry, LocalRuntimeKind, LocalRuntimeReviveCapability, LocalRuntimeSessionState, LocalRuntimeStatus, } from './local-runtime-catalog.js';
28
31
  export { buildConfiguredWorkspaceOptions, buildConversationEnvironmentKey, buildConversationWorktreeSpec, buildPublicWorkspaceOptions, buildWorkspaceOptionId, EXECUTION_ENVIRONMENT_MODES, isEnabledFlag, isExecutionEnvironmentMode, normalizeOptionalString, readSessionWorkspaceConfig, resolveConfiguredWorkspaceCwd, ExecutionEnvironmentError, prepareConversationEnvironment, releaseConversationEnvironment, } from './execution-environment.js';
29
32
  export type { ConfiguredWorkspaceOption, ExecutionEnvironmentMode, PreparedExecutionEnvironment, SessionWorkspaceConfig, } from './execution-environment.js';
30
33
  export { initRTDBAuth, rtdbWrite, rtdbRead, patchAgentSessionSnapshot, patchRuntimeInfo, writeRuntimeInfo, clearRuntimeInfo, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from './rtdb-rest.js';
31
- export type { AgentSessionSnapshotPatch, RuntimeInfoPayloadData, SessionStatePayload, TurnStatePayload, } from './rtdb-rest.js';
34
+ export type { AgentSessionSnapshotPatch, RTDBClientHandle, RuntimeInfoPayloadData, SessionStatePayload, TurnStatePayload, } from './rtdb-rest.js';
35
+ export { buildCanonHostPrompt, buildHydratedInboundContext, createConversationMetadataLoader, loadHostSessionConfig, publishHostAgentRuntime, publishHostSessionSnapshots, readHostSessionConfig, renderCanonHostInboundContent, resolveHostWorkspaceCwd, } from './host-runtime.js';
36
+ export type { HostInboundParticipantContext, } from './host-runtime.js';
37
+ export { createRuntimeStatePublisher, } from './runtime-state-publisher.js';
38
+ export type { RuntimeStatePublisher, RuntimeStatePublisherOptions, RuntimeStreamingPayload, } from './runtime-state-publisher.js';
32
39
  export { formatCanonMessageAsText } from './message-format.js';
33
40
  export { DEFAULT_BASE_URL, DEFAULT_STREAM_URL, DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
34
41
  export { resolveCanonBaseUrl } from './base-url.js';
package/dist/index.js CHANGED
@@ -5,13 +5,14 @@ export { buildConfiguredWorkspaceOptionsWithRoots, buildPublicWorkspaceRoots, bu
5
5
  // Client
6
6
  export { CanonClient, CanonApiError } from './client.js';
7
7
  export { buildAgentSessionSnapshot } from './agent-session.js';
8
+ export { CLAUDE_EFFORT_OPTIONS, EXECUTION_MODE_CONTROL_OPTIONS, buildFirstPartyCodingRuntimeDescriptor, buildRuntimeEffortControl, buildRuntimeExecutionModeControl, buildRuntimeExecutionModeOptions, buildRuntimeModelControl, buildRuntimePermissionModeControl, buildRuntimeWorkspaceControl, buildRuntimeWorkspaceControlOptions, } from './runtime-descriptor.js';
8
9
  // Stream
9
10
  export { CanonStream } from './stream.js';
10
11
  export { buildParticipationHistorySnapshot, buildParticipationHistorySnapshots, buildBehaviorPolicyLines, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, evaluateParticipationPolicy, getDefaultParticipationPolicy, resolveAgentBehaviorPolicy, } from './policy.js';
11
12
  // Turn protocol
12
13
  export { DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, FINAL_MESSAGE_HANDOFF_MS, isTurnOpen, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
13
14
  // Registration
14
- export { registerAndWaitForApproval } from './registration.js';
15
+ export { ackRegistrationApproval, registerAndWaitForApproval, submitRegistrationRequest, waitForRegistrationApproval, } from './registration.js';
15
16
  // Approval
16
17
  export { ApprovalManager } from './approval-manager.js';
17
18
  export { generateApprovalId, buildApprovalRequest, buildApprovalReply, buildApprovalOutcome, parseTextApprovalReply, redactSecrets, } from './approval-format.js';
@@ -20,13 +21,18 @@ export { buildPlanApprovalReply, buildPlanApprovalRequest, buildQuestionReply, b
20
21
  // Streaming (RTDB helpers)
21
22
  export { createStreamingHelper } from './streaming.js';
22
23
  // Agent profiles (loading, locking, resolution)
23
- export { loadProfiles, isProfileLocked, acquireLock, releaseLock, isProcessAlive, CANON_DIR, AGENTS_PATH, LOCKS_DIR, } from './agent-profiles.js';
24
+ export { clearPendingRegistration, getOrCreatePendingRegistration, loadPendingRegistrations, loadProfiles, savePendingRegistrations, saveProfiles, updatePendingRegistration, upsertAgentProfile, isProfileLocked, acquireLock, releaseLock, isProcessAlive, CANON_DIR, AGENTS_PATH, LOCKS_DIR, } from './agent-profiles.js';
24
25
  // Agent resolver
25
- export { resolveCanonAgent, resolveCanonProfile, getActiveProfile } from './agent-resolver.js';
26
+ export { resolveCanonAgent, resolveCanonProfile, getActiveProfile, getActiveProfileLock } from './agent-resolver.js';
27
+ // Local runtime catalog
28
+ export { RUNTIMES_DIR, buildLocalRuntimeId, clearRuntimeSessionState, describeProfileLock, heartbeatLocalRuntimeEntry, listLocalRuntimeEntries, loadRuntimeSessionState, markLocalRuntimeStopped, readLocalRuntimeEntry, removeLocalRuntimeEntry, saveRuntimeSessionState, upsertLocalRuntimeEntry, } from './local-runtime-catalog.js';
26
29
  // Execution environments for host-mode coding sessions
27
30
  export { buildConfiguredWorkspaceOptions, buildConversationEnvironmentKey, buildConversationWorktreeSpec, buildPublicWorkspaceOptions, buildWorkspaceOptionId, EXECUTION_ENVIRONMENT_MODES, isEnabledFlag, isExecutionEnvironmentMode, normalizeOptionalString, readSessionWorkspaceConfig, resolveConfiguredWorkspaceCwd, ExecutionEnvironmentError, prepareConversationEnvironment, releaseConversationEnvironment, } from './execution-environment.js';
28
31
  // RTDB REST helpers (token exchange, session state, generic read/write)
29
32
  export { initRTDBAuth, rtdbWrite, rtdbRead, patchAgentSessionSnapshot, patchRuntimeInfo, writeRuntimeInfo, clearRuntimeInfo, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from './rtdb-rest.js';
33
+ // Runtime host plumbing
34
+ export { buildCanonHostPrompt, buildHydratedInboundContext, createConversationMetadataLoader, loadHostSessionConfig, publishHostAgentRuntime, publishHostSessionSnapshots, readHostSessionConfig, renderCanonHostInboundContent, resolveHostWorkspaceCwd, } from './host-runtime.js';
35
+ export { createRuntimeStatePublisher, } from './runtime-state-publisher.js';
30
36
  // Message formatting (LLM-facing text projection)
31
37
  export { formatCanonMessageAsText } from './message-format.js';
32
38
  // Constants
@@ -0,0 +1,65 @@
1
+ import type { AgentClientType, ExecutionEnvironmentMode } from './types.js';
2
+ export type LocalRuntimeKind = AgentClientType | 'sdk';
3
+ export type LocalRuntimeStatus = 'running' | 'offline' | 'stale' | 'manual' | 'embedded';
4
+ export type LocalRuntimeReviveCapability = 'revivable' | 'manual' | 'embedded' | 'missing-profile';
5
+ export interface LocalRuntimeSessionState {
6
+ conversationId: string;
7
+ workspaceId?: string;
8
+ baseCwd: string;
9
+ executionMode?: ExecutionEnvironmentMode;
10
+ threadId?: string;
11
+ claudeSessionId?: string;
12
+ lastInboundMessageId?: string;
13
+ updatedAt: string;
14
+ }
15
+ export interface LocalRuntimeCatalogEntry {
16
+ id: string;
17
+ runtime: LocalRuntimeKind;
18
+ profile: string | null;
19
+ agentId?: string;
20
+ agentName?: string;
21
+ cwd: string;
22
+ baseCwd?: string;
23
+ workspaceRoots?: string[];
24
+ workspaces?: string[];
25
+ launchCommand: string[];
26
+ pid?: number;
27
+ status: LocalRuntimeStatus;
28
+ reviveCapability: LocalRuntimeReviveCapability;
29
+ surfaceMode?: 'host' | 'channel' | 'embedded';
30
+ lastStartedAt?: string;
31
+ lastHeartbeatAt?: string;
32
+ lastStoppedAt?: string;
33
+ lastAuthError?: string;
34
+ lastError?: string;
35
+ sessions?: Record<string, LocalRuntimeSessionState>;
36
+ }
37
+ export declare const RUNTIMES_DIR: string;
38
+ export declare function buildLocalRuntimeId(input: {
39
+ runtime: LocalRuntimeKind;
40
+ profile?: string | null;
41
+ cwd: string;
42
+ launchCommand?: string[];
43
+ }): string;
44
+ export declare function readLocalRuntimeEntry(id: string): LocalRuntimeCatalogEntry | null;
45
+ export declare function upsertLocalRuntimeEntry(entry: LocalRuntimeCatalogEntry): LocalRuntimeCatalogEntry;
46
+ export declare function heartbeatLocalRuntimeEntry(id: string, patch?: Partial<LocalRuntimeCatalogEntry>): LocalRuntimeCatalogEntry | null;
47
+ export declare function markLocalRuntimeStopped(id: string, patch?: Partial<LocalRuntimeCatalogEntry>): LocalRuntimeCatalogEntry | null;
48
+ export declare function removeLocalRuntimeEntry(id: string): void;
49
+ export declare function listLocalRuntimeEntries(options?: {
50
+ runtime?: LocalRuntimeKind;
51
+ }): LocalRuntimeCatalogEntry[];
52
+ export declare function loadRuntimeSessionState(runtimeId: string, input: {
53
+ conversationId: string;
54
+ baseCwd: string;
55
+ executionMode?: ExecutionEnvironmentMode;
56
+ workspaceId?: string;
57
+ }): LocalRuntimeSessionState | null;
58
+ export declare function saveRuntimeSessionState(runtimeId: string, input: Omit<LocalRuntimeSessionState, 'updatedAt'>): LocalRuntimeSessionState;
59
+ export declare function clearRuntimeSessionState(runtimeId: string, input: {
60
+ conversationId: string;
61
+ baseCwd?: string;
62
+ executionMode?: ExecutionEnvironmentMode;
63
+ workspaceId?: string;
64
+ }): void;
65
+ export declare function describeProfileLock(profile: string): string;
@@ -0,0 +1,200 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import { join, resolve } from 'node:path';
4
+ import { CANON_DIR, isProcessAlive, isProfileLocked, loadProfiles } from './agent-profiles.js';
5
+ export const RUNTIMES_DIR = join(CANON_DIR, 'runtimes');
6
+ const LEGACY_CODEX_SESSIONS_PATH = join(CANON_DIR, 'codex-sessions.json');
7
+ function shortHash(value) {
8
+ return createHash('sha256').update(value).digest('hex').slice(0, 16);
9
+ }
10
+ function safeRuntimeFileName(id) {
11
+ return `${shortHash(id)}.json`;
12
+ }
13
+ function runtimePath(id) {
14
+ return join(RUNTIMES_DIR, safeRuntimeFileName(id));
15
+ }
16
+ function writeJsonAtomic(path, value) {
17
+ mkdirSync(RUNTIMES_DIR, { recursive: true });
18
+ const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
19
+ writeFileSync(tmp, JSON.stringify(value, null, 2), { mode: 0o600 });
20
+ renameSync(tmp, path);
21
+ }
22
+ export function buildLocalRuntimeId(input) {
23
+ const profile = input.profile ?? 'manual';
24
+ return `${input.runtime}:${profile}:${shortHash(`${resolve(input.cwd)}:${(input.launchCommand ?? []).join('\0')}`)}`;
25
+ }
26
+ export function readLocalRuntimeEntry(id) {
27
+ try {
28
+ const parsed = JSON.parse(readFileSync(runtimePath(id), 'utf-8'));
29
+ return parsed.id === id ? parsed : null;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ export function upsertLocalRuntimeEntry(entry) {
36
+ const existing = readLocalRuntimeEntry(entry.id);
37
+ const next = {
38
+ ...existing,
39
+ ...entry,
40
+ sessions: {
41
+ ...(existing?.sessions ?? {}),
42
+ ...(entry.sessions ?? {}),
43
+ },
44
+ };
45
+ writeJsonAtomic(runtimePath(entry.id), next);
46
+ return next;
47
+ }
48
+ export function heartbeatLocalRuntimeEntry(id, patch = {}) {
49
+ const existing = readLocalRuntimeEntry(id);
50
+ if (!existing)
51
+ return null;
52
+ return upsertLocalRuntimeEntry({
53
+ ...existing,
54
+ ...patch,
55
+ id,
56
+ status: 'running',
57
+ pid: patch.pid ?? process.pid,
58
+ lastHeartbeatAt: new Date().toISOString(),
59
+ });
60
+ }
61
+ export function markLocalRuntimeStopped(id, patch = {}) {
62
+ const existing = readLocalRuntimeEntry(id);
63
+ if (!existing)
64
+ return null;
65
+ return upsertLocalRuntimeEntry({
66
+ ...existing,
67
+ ...patch,
68
+ id,
69
+ status: existing.reviveCapability === 'embedded' ? 'embedded' : 'offline',
70
+ pid: undefined,
71
+ lastStoppedAt: new Date().toISOString(),
72
+ });
73
+ }
74
+ export function removeLocalRuntimeEntry(id) {
75
+ try {
76
+ unlinkSync(runtimePath(id));
77
+ }
78
+ catch { }
79
+ }
80
+ function deriveRuntimeState(entry) {
81
+ let status = entry.status;
82
+ if (status === 'running' && entry.pid && !isProcessAlive(entry.pid)) {
83
+ status = 'stale';
84
+ }
85
+ const profiles = loadProfiles();
86
+ let reviveCapability = entry.reviveCapability;
87
+ if (entry.profile && !profiles[entry.profile]) {
88
+ reviveCapability = 'missing-profile';
89
+ }
90
+ return { ...entry, status, reviveCapability };
91
+ }
92
+ export function listLocalRuntimeEntries(options = {}) {
93
+ if (!existsSync(RUNTIMES_DIR))
94
+ return [];
95
+ const entries = [];
96
+ for (const name of readdirSync(RUNTIMES_DIR)) {
97
+ if (!name.endsWith('.json'))
98
+ continue;
99
+ try {
100
+ const entry = JSON.parse(readFileSync(join(RUNTIMES_DIR, name), 'utf-8'));
101
+ if (options.runtime && entry.runtime !== options.runtime)
102
+ continue;
103
+ entries.push(deriveRuntimeState(entry));
104
+ }
105
+ catch {
106
+ // Ignore corrupt breadcrumbs; they should not block the manager.
107
+ }
108
+ }
109
+ return entries.sort((a, b) => {
110
+ const at = a.lastHeartbeatAt ?? a.lastStartedAt ?? a.lastStoppedAt ?? '';
111
+ const bt = b.lastHeartbeatAt ?? b.lastStartedAt ?? b.lastStoppedAt ?? '';
112
+ return bt.localeCompare(at);
113
+ });
114
+ }
115
+ function sessionKey(input) {
116
+ return shortHash([
117
+ input.conversationId,
118
+ resolve(input.baseCwd),
119
+ input.executionMode ?? 'unknown',
120
+ input.workspaceId ?? '',
121
+ ].join('\0'));
122
+ }
123
+ export function loadRuntimeSessionState(runtimeId, input) {
124
+ migrateLegacyCodexSessions(runtimeId);
125
+ const entry = readLocalRuntimeEntry(runtimeId);
126
+ return entry?.sessions?.[sessionKey(input)] ?? null;
127
+ }
128
+ export function saveRuntimeSessionState(runtimeId, input) {
129
+ const entry = readLocalRuntimeEntry(runtimeId);
130
+ if (!entry) {
131
+ throw new Error(`Runtime catalog entry not found: ${runtimeId}`);
132
+ }
133
+ const key = sessionKey(input);
134
+ const existingState = entry.sessions?.[key];
135
+ const nextState = {
136
+ ...existingState,
137
+ ...input,
138
+ updatedAt: new Date().toISOString(),
139
+ };
140
+ upsertLocalRuntimeEntry({
141
+ ...entry,
142
+ sessions: {
143
+ ...(entry.sessions ?? {}),
144
+ [key]: nextState,
145
+ },
146
+ });
147
+ return nextState;
148
+ }
149
+ export function clearRuntimeSessionState(runtimeId, input) {
150
+ const entry = readLocalRuntimeEntry(runtimeId);
151
+ if (!entry?.sessions)
152
+ return;
153
+ const sessions = { ...entry.sessions };
154
+ if (input.baseCwd) {
155
+ delete sessions[sessionKey({
156
+ conversationId: input.conversationId,
157
+ baseCwd: input.baseCwd,
158
+ executionMode: input.executionMode,
159
+ workspaceId: input.workspaceId,
160
+ })];
161
+ }
162
+ else {
163
+ for (const [key, state] of Object.entries(sessions)) {
164
+ if (state.conversationId === input.conversationId)
165
+ delete sessions[key];
166
+ }
167
+ }
168
+ upsertLocalRuntimeEntry({ ...entry, sessions });
169
+ }
170
+ function migrateLegacyCodexSessions(runtimeId) {
171
+ if (!existsSync(LEGACY_CODEX_SESSIONS_PATH))
172
+ return;
173
+ const entry = readLocalRuntimeEntry(runtimeId);
174
+ if (!entry || entry.runtime !== 'codex')
175
+ return;
176
+ try {
177
+ const legacy = JSON.parse(readFileSync(LEGACY_CODEX_SESSIONS_PATH, 'utf-8'));
178
+ const agentSessions = entry.agentId ? legacy.agents?.[entry.agentId] : null;
179
+ if (!agentSessions)
180
+ return;
181
+ const sessions = { ...(entry.sessions ?? {}) };
182
+ for (const [conversationId, state] of Object.entries(agentSessions)) {
183
+ const baseCwd = state.cwd;
184
+ sessions[sessionKey({ conversationId, baseCwd })] = {
185
+ conversationId,
186
+ baseCwd,
187
+ threadId: state.threadId,
188
+ updatedAt: state.updatedAt,
189
+ };
190
+ }
191
+ upsertLocalRuntimeEntry({ ...entry, sessions });
192
+ }
193
+ catch {
194
+ return;
195
+ }
196
+ }
197
+ export function describeProfileLock(profile) {
198
+ const lock = isProfileLocked(profile);
199
+ return lock.locked ? `locked by PID ${lock.pid}` : 'available';
200
+ }
@@ -8,6 +8,14 @@ import type { RegistrationInput, RegistrationResult, RegistrationStatus } from '
8
8
  * 3. On approval, returns the API key
9
9
  */
10
10
  export declare function registerAndWaitForApproval(input: RegistrationInput, callbacks?: {
11
- onSubmitted?: (requestId: string) => void;
11
+ onSubmitted?: (requestId: string, pollToken?: string) => void;
12
12
  onPollUpdate?: (status: RegistrationStatus) => void;
13
13
  }): Promise<RegistrationResult>;
14
+ export declare function submitRegistrationRequest(input: RegistrationInput): Promise<{
15
+ requestId: string;
16
+ pollToken?: string;
17
+ }>;
18
+ export declare function waitForRegistrationApproval(baseUrl: string | undefined, requestId: string, pollToken?: string, callbacks?: {
19
+ onPollUpdate?: (status: RegistrationStatus) => void;
20
+ }): Promise<RegistrationResult>;
21
+ export declare function ackRegistrationApproval(baseUrl: string | undefined, requestId: string, pollToken?: string): Promise<void>;