@canonmsg/core 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/browser.d.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  export { AGENT_CAPABILITIES, } from './types.js';
2
+ export { resolveCanonBaseUrl } from './base-url.js';
3
+ export { DEFAULT_BASE_URL, DEFAULT_STREAM_URL, DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
2
4
  export type { AgentCapabilities, AgentClientType, AgentRuntime, MediaAttachment, MediaAttachmentKind, ModelOption, SessionConfig, WorkspaceOption, } from './types.js';
5
+ export { EXECUTION_ENVIRONMENT_MODES, isExecutionEnvironmentMode, } from './execution-environment-mode.js';
6
+ export type { ExecutionEnvironmentMode } from './execution-environment-mode.js';
3
7
  export type { CanonResolvedWorkSession, CanonWorkSession, CanonWorkSessionContext, CanonWorkSessionConversationRole, CanonWorkSessionDisclosureMode, CanonWorkSessionParticipant, CanonWorkSessionStatus, CreateWorkSessionOptions, SendLinkedMessageOptions, SendLinkedMessageResult, UpdateWorkSessionConversationOptions, WorkSessionPromptRenderOptions, } from './work-session.js';
4
8
  export { buildWorkSessionPromptLines, buildWorkSessionsPromptLines, mergeWorkSessionContexts, } from './work-session.js';
5
9
  export type { AgentBehaviorSettings, ParticipationHistoryMessage, ParticipationHistorySnapshot, ParticipationStyle, ResolvedAgentBehaviorPolicy, } from './policy.js';
package/dist/browser.js CHANGED
@@ -1,4 +1,7 @@
1
1
  export { AGENT_CAPABILITIES, } from './types.js';
2
+ export { resolveCanonBaseUrl } from './base-url.js';
3
+ export { DEFAULT_BASE_URL, DEFAULT_STREAM_URL, DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
4
+ export { EXECUTION_ENVIRONMENT_MODES, isExecutionEnvironmentMode, } from './execution-environment-mode.js';
2
5
  export { buildWorkSessionPromptLines, buildWorkSessionsPromptLines, mergeWorkSessionContexts, } from './work-session.js';
3
6
  export { buildParticipationHistorySnapshot, buildParticipationHistorySnapshots, buildBehaviorPolicyLines, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, evaluateParticipationPolicy, getDefaultParticipationPolicy, resolveAgentBehaviorPolicy, } from './policy.js';
4
7
  export { DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, isTurnOpen, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Browser-safe helpers for Canon execution modes. Separated from
3
+ * `execution-environment.ts` (which pulls in Node-only modules) so both
4
+ * Node hosts and browser/Expo clients can import these primitives.
5
+ */
6
+ export type ExecutionEnvironmentMode = 'worktree' | 'locked';
7
+ export declare const EXECUTION_ENVIRONMENT_MODES: readonly ExecutionEnvironmentMode[];
8
+ export declare function isExecutionEnvironmentMode(value: unknown): value is ExecutionEnvironmentMode;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Browser-safe helpers for Canon execution modes. Separated from
3
+ * `execution-environment.ts` (which pulls in Node-only modules) so both
4
+ * Node hosts and browser/Expo clients can import these primitives.
5
+ */
6
+ export const EXECUTION_ENVIRONMENT_MODES = [
7
+ 'worktree',
8
+ 'locked',
9
+ ];
10
+ export function isExecutionEnvironmentMode(value) {
11
+ return value === 'worktree' || value === 'locked';
12
+ }
@@ -1,5 +1,7 @@
1
1
  import type { WorkspaceOption } from './types.js';
2
- export type ExecutionEnvironmentMode = 'worktree' | 'locked';
2
+ import { EXECUTION_ENVIRONMENT_MODES, isExecutionEnvironmentMode, type ExecutionEnvironmentMode } from './execution-environment-mode.js';
3
+ export { EXECUTION_ENVIRONMENT_MODES, isExecutionEnvironmentMode };
4
+ export type { ExecutionEnvironmentMode };
3
5
  export interface PreparedExecutionEnvironment {
4
6
  cwd: string;
5
7
  baseCwd: string;
@@ -24,6 +26,7 @@ export interface ConfiguredWorkspaceOption extends WorkspaceResolverOption {
24
26
  export interface SessionWorkspaceConfig {
25
27
  workspaceId?: string;
26
28
  model?: string;
29
+ executionMode?: ExecutionEnvironmentMode;
27
30
  retiredWorkspaceConfig?: boolean;
28
31
  }
29
32
  export declare function normalizeOptionalString(value: unknown): string | undefined;
@@ -56,4 +59,3 @@ export declare function prepareConversationEnvironment(input: {
56
59
  allowWorktrees?: boolean;
57
60
  }): PreparedExecutionEnvironment;
58
61
  export declare function releaseConversationEnvironment(environment: Pick<PreparedExecutionEnvironment, 'lockPath'>): void;
59
- export {};
@@ -1,8 +1,10 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import { createHash } from 'node:crypto';
3
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from 'node:fs';
3
+ import { existsSync, mkdirSync, unlinkSync, writeFileSync, } from 'node:fs';
4
4
  import { basename, dirname, join, relative, resolve } from 'node:path';
5
- import { CANON_DIR, isProcessAlive } from './agent-profiles.js';
5
+ import { CANON_DIR } from './agent-profiles.js';
6
+ import { EXECUTION_ENVIRONMENT_MODES, isExecutionEnvironmentMode, } from './execution-environment-mode.js';
7
+ export { EXECUTION_ENVIRONMENT_MODES, isExecutionEnvironmentMode };
6
8
  export class ExecutionEnvironmentError extends Error {
7
9
  userMessage;
8
10
  constructor(message, userMessage = message) {
@@ -54,46 +56,19 @@ function detectRepoRoot(cwd) {
54
56
  const repoRoot = result.stdout.trim();
55
57
  return repoRoot ? resolve(repoRoot) : null;
56
58
  }
57
- function isRepoClean(repoRoot) {
58
- const result = runGit(repoRoot, ['status', '--porcelain']);
59
- return result.ok && result.stdout.trim() === '';
60
- }
61
59
  function branchExists(repoRoot, branch) {
62
60
  return runGit(repoRoot, ['show-ref', '--verify', '--quiet', `refs/heads/${branch}`]).ok;
63
61
  }
64
- function loadWorkspaceLock(lockPath) {
65
- try {
66
- return JSON.parse(readFileSync(lockPath, 'utf-8'));
67
- }
68
- catch {
69
- return null;
70
- }
71
- }
72
- function cleanupStaleWorkspaceLock(lockPath) {
73
- const record = loadWorkspaceLock(lockPath);
74
- if (!record) {
75
- try {
76
- unlinkSync(lockPath);
77
- return true;
78
- }
79
- catch {
80
- return false;
81
- }
82
- }
83
- if (isProcessAlive(record.pid))
84
- return false;
85
- try {
86
- unlinkSync(lockPath);
87
- return true;
88
- }
89
- catch {
90
- return false;
91
- }
92
- }
93
- function acquireWorkspaceLock(input) {
62
+ // Advisory workspace-usage record. Multiple sessions may share a workspace;
63
+ // we write per-session files (not per-workspace) so nothing contends. Treat
64
+ // these as observability breadcrumbs, not exclusive locks — the name stays
65
+ // for backward compat with existing tooling / docs.
66
+ function recordWorkspaceUsage(input) {
94
67
  mkdirSync(WORKSPACE_LOCKS_ROOT, { recursive: true });
95
68
  const workspaceCwd = resolve(input.workspaceCwd);
96
- const lockPath = join(WORKSPACE_LOCKS_ROOT, `${shortHash(workspaceCwd)}.json`);
69
+ const workspaceHash = shortHash(workspaceCwd);
70
+ const sessionHash = shortHash(`${input.agentId}:${input.conversationId}`).slice(0, 8);
71
+ const lockPath = join(WORKSPACE_LOCKS_ROOT, `${workspaceHash}.${sessionHash}.json`);
97
72
  const record = {
98
73
  pid: process.pid,
99
74
  agentId: input.agentId,
@@ -101,34 +76,13 @@ function acquireWorkspaceLock(input) {
101
76
  workspaceCwd,
102
77
  createdAt: new Date().toISOString(),
103
78
  };
104
- while (true) {
105
- try {
106
- writeFileSync(lockPath, JSON.stringify(record, null, 2), { flag: 'wx' });
107
- return lockPath;
108
- }
109
- catch (error) {
110
- const code = error.code;
111
- if (code !== 'EEXIST') {
112
- throw error;
113
- }
114
- const existing = loadWorkspaceLock(lockPath);
115
- if (!existing) {
116
- if (cleanupStaleWorkspaceLock(lockPath))
117
- continue;
118
- throw new Error(`Workspace lock file is unreadable: ${workspaceCwd}`);
119
- }
120
- if (!isProcessAlive(existing.pid)) {
121
- if (cleanupStaleWorkspaceLock(lockPath))
122
- continue;
123
- }
124
- if (existing.pid === process.pid
125
- && existing.agentId === input.agentId
126
- && existing.conversationId === input.conversationId) {
127
- return lockPath;
128
- }
129
- throw new ExecutionEnvironmentError(`Workspace is already in use by Canon session ${existing.conversationId.slice(0, 8)} (PID ${existing.pid}).`, 'That workspace is already in use by another Canon coding session on this machine.');
130
- }
79
+ try {
80
+ writeFileSync(lockPath, JSON.stringify(record, null, 2));
131
81
  }
82
+ catch {
83
+ // Best-effort; an unwritable breadcrumb must not block a session.
84
+ }
85
+ return lockPath;
132
86
  }
133
87
  export function buildConversationEnvironmentKey(conversationId, workspaceCwd) {
134
88
  return `${conversationId}:${resolve(workspaceCwd)}`;
@@ -168,9 +122,14 @@ export function readSessionWorkspaceConfig(raw) {
168
122
  : undefined;
169
123
  const hasRetiredWorkspaceReference = (normalizeOptionalString(data.cwd) !== undefined
170
124
  || isRetiredWorkspaceId(rawWorkspaceId));
125
+ const rawExecutionMode = data.executionMode;
126
+ const executionMode = isExecutionEnvironmentMode(rawExecutionMode)
127
+ ? rawExecutionMode
128
+ : undefined;
171
129
  return {
172
130
  workspaceId: stableWorkspaceId,
173
131
  model: normalizeOptionalString(data.model),
132
+ ...(executionMode ? { executionMode } : {}),
174
133
  ...(!stableWorkspaceId && hasRetiredWorkspaceReference
175
134
  ? { retiredWorkspaceConfig: true }
176
135
  : {}),
@@ -210,12 +169,12 @@ export function prepareConversationEnvironment(input) {
210
169
  cwd: baseCwd,
211
170
  baseCwd,
212
171
  mode: 'locked',
213
- lockPath: acquireWorkspaceLock({
172
+ lockPath: recordWorkspaceUsage({
214
173
  agentId: input.agentId,
215
174
  conversationId: input.conversationId,
216
175
  workspaceCwd: baseCwd,
217
176
  }),
218
- reason: 'Worktree isolation is disabled for this host',
177
+ reason: 'Sharing the base workspace (locked mode)',
219
178
  };
220
179
  }
221
180
  const repoRoot = detectRepoRoot(baseCwd);
@@ -235,7 +194,11 @@ export function prepareConversationEnvironment(input) {
235
194
  };
236
195
  }
237
196
  }
238
- else if (isRepoClean(repoRoot)) {
197
+ else {
198
+ // `git worktree add` works fine even when the source worktree is dirty —
199
+ // uncommitted changes stay with the source tree, the new worktree is
200
+ // based on HEAD. Historically we required a clean repo and silently fell
201
+ // through to locked mode; that failure mode surprised users. Just try.
239
202
  mkdirSync(dirname(spec.worktreePath), { recursive: true });
240
203
  const addArgs = branchExists(repoRoot, spec.branch)
241
204
  ? ['worktree', 'add', spec.worktreePath, spec.branch]
@@ -257,14 +220,14 @@ export function prepareConversationEnvironment(input) {
257
220
  cwd: baseCwd,
258
221
  baseCwd,
259
222
  mode: 'locked',
260
- lockPath: acquireWorkspaceLock({
223
+ lockPath: recordWorkspaceUsage({
261
224
  agentId: input.agentId,
262
225
  conversationId: input.conversationId,
263
226
  workspaceCwd: baseCwd,
264
227
  }),
265
228
  reason: repoRoot
266
- ? 'Base repository is dirty or worktree creation failed'
267
- : 'Workspace is not a git repository',
229
+ ? 'Worktree creation failed; sharing the base workspace'
230
+ : 'Workspace is not a git repository; sharing the base workspace',
268
231
  };
269
232
  }
270
233
  export function releaseConversationEnvironment(environment) {
package/dist/index.d.ts CHANGED
@@ -20,7 +20,7 @@ export { loadProfiles, isProfileLocked, acquireLock, releaseLock, isProcessAlive
20
20
  export type { AgentProfile } from './agent-profiles.js';
21
21
  export { resolveCanonAgent, resolveCanonProfile, getActiveProfile } from './agent-resolver.js';
22
22
  export type { ResolvedAgent } from './agent-resolver.js';
23
- export { buildConfiguredWorkspaceOptions, buildConversationEnvironmentKey, buildConversationWorktreeSpec, buildPublicWorkspaceOptions, buildWorkspaceOptionId, isEnabledFlag, normalizeOptionalString, readSessionWorkspaceConfig, resolveConfiguredWorkspaceCwd, ExecutionEnvironmentError, prepareConversationEnvironment, releaseConversationEnvironment, } from './execution-environment.js';
23
+ export { buildConfiguredWorkspaceOptions, buildConversationEnvironmentKey, buildConversationWorktreeSpec, buildPublicWorkspaceOptions, buildWorkspaceOptionId, EXECUTION_ENVIRONMENT_MODES, isEnabledFlag, isExecutionEnvironmentMode, normalizeOptionalString, readSessionWorkspaceConfig, resolveConfiguredWorkspaceCwd, ExecutionEnvironmentError, prepareConversationEnvironment, releaseConversationEnvironment, } from './execution-environment.js';
24
24
  export type { ConfiguredWorkspaceOption, ExecutionEnvironmentMode, PreparedExecutionEnvironment, SessionWorkspaceConfig, } from './execution-environment.js';
25
25
  export { initRTDBAuth, rtdbWrite, rtdbRead, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from './rtdb-rest.js';
26
26
  export type { SessionStatePayload, TurnStatePayload } from './rtdb-rest.js';
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ export { loadProfiles, isProfileLocked, acquireLock, releaseLock, isProcessAlive
21
21
  // Agent resolver
22
22
  export { resolveCanonAgent, resolveCanonProfile, getActiveProfile } from './agent-resolver.js';
23
23
  // Execution environments for host-mode coding sessions
24
- export { buildConfiguredWorkspaceOptions, buildConversationEnvironmentKey, buildConversationWorktreeSpec, buildPublicWorkspaceOptions, buildWorkspaceOptionId, isEnabledFlag, normalizeOptionalString, readSessionWorkspaceConfig, resolveConfiguredWorkspaceCwd, ExecutionEnvironmentError, prepareConversationEnvironment, releaseConversationEnvironment, } from './execution-environment.js';
24
+ export { buildConfiguredWorkspaceOptions, buildConversationEnvironmentKey, buildConversationWorktreeSpec, buildPublicWorkspaceOptions, buildWorkspaceOptionId, EXECUTION_ENVIRONMENT_MODES, isEnabledFlag, isExecutionEnvironmentMode, normalizeOptionalString, readSessionWorkspaceConfig, resolveConfiguredWorkspaceCwd, ExecutionEnvironmentError, prepareConversationEnvironment, releaseConversationEnvironment, } from './execution-environment.js';
25
25
  // RTDB REST helpers (token exchange, session state, generic read/write)
26
26
  export { initRTDBAuth, rtdbWrite, rtdbRead, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from './rtdb-rest.js';
27
27
  // Constants
package/dist/types.d.ts CHANGED
@@ -1,5 +1,7 @@
1
+ import type { ExecutionEnvironmentMode } from './execution-environment-mode.js';
1
2
  import type { ResolvedAgentBehaviorPolicy } from './policy.js';
2
3
  import type { CanonWorkSessionContext } from './work-session.js';
4
+ export type { ExecutionEnvironmentMode };
3
5
  export type MediaAttachmentKind = 'image' | 'audio' | 'file';
4
6
  export interface MediaAttachment {
5
7
  kind: MediaAttachmentKind;
@@ -11,6 +13,10 @@ export interface MediaAttachment {
11
13
  height?: number;
12
14
  durationMs?: number;
13
15
  }
16
+ export interface ForwardedFrom {
17
+ sourceConversationId: string;
18
+ messageId: string;
19
+ }
14
20
  export interface CanonMessage {
15
21
  id: string;
16
22
  senderId: string;
@@ -19,13 +25,12 @@ export interface CanonMessage {
19
25
  isOwner: boolean;
20
26
  contentType: 'text' | 'image' | 'audio' | 'file' | 'contact_card';
21
27
  text: string | null;
22
- imageUrl: string | null;
23
- audioUrl: string | null;
24
- audioDurationMs: number | null;
25
- attachments?: MediaAttachment[];
28
+ attachments: MediaAttachment[];
26
29
  mentions: string[];
27
30
  replyTo: string | null;
28
31
  replyToPosition: number | null;
32
+ forwarded?: boolean;
33
+ forwardedFrom?: ForwardedFrom;
29
34
  workSession?: CanonWorkSessionContext | null;
30
35
  metadata?: Record<string, unknown>;
31
36
  status: 'sent' | 'read';
@@ -59,6 +64,12 @@ export type AgentClientType = 'claude-code' | 'openclaw' | 'codex' | 'generic';
59
64
  export interface AgentCapabilities {
60
65
  supportsModelSwitch: boolean;
61
66
  supportsPermissionMode: boolean;
67
+ /**
68
+ * Whether permissionMode can be changed mid-session. When undefined,
69
+ * defaults to `supportsPermissionMode` — keep the UI chip non-interactive
70
+ * for agents whose approval mode is locked at session creation.
71
+ */
72
+ supportsRuntimePermissionMode?: boolean;
62
73
  supportsEffort: boolean;
63
74
  supportsSessionState: boolean;
64
75
  supportsInterrupt: boolean;
@@ -101,12 +112,11 @@ export interface MessageCreatedPayload {
101
112
  isOwner?: boolean;
102
113
  text?: string;
103
114
  contentType?: 'text' | 'image' | 'audio' | 'file' | 'contact_card';
104
- imageUrl?: string;
105
- audioUrl?: string;
106
- audioDurationMs?: number;
107
115
  attachments?: MediaAttachment[];
108
116
  replyTo?: string;
109
117
  replyToPosition?: number;
118
+ forwarded?: boolean;
119
+ forwardedFrom?: ForwardedFrom;
110
120
  mentions?: string[];
111
121
  createdAt?: string;
112
122
  workSession?: CanonWorkSessionContext | null;
@@ -128,9 +138,6 @@ export interface SendMessageOptions {
128
138
  contentType?: 'text' | 'audio' | 'image' | 'file' | 'contact_card';
129
139
  replyTo?: string;
130
140
  replyToPosition?: number;
131
- audioUrl?: string;
132
- audioDurationMs?: number;
133
- imageUrl?: string;
134
141
  attachments?: MediaAttachment[];
135
142
  contactCardUserId?: string;
136
143
  mentions?: string[];
@@ -186,16 +193,39 @@ export interface SessionConfig {
186
193
  model?: string;
187
194
  permissionMode?: string;
188
195
  workspaceId?: string;
196
+ /**
197
+ * Explicitly selected execution mode. Sessions created before this field
198
+ * existed stay `undefined`; UIs must prompt for a value and plugin hosts
199
+ * fail-closed rather than inferring one.
200
+ */
201
+ executionMode?: ExecutionEnvironmentMode;
189
202
  availableModels?: ModelOption[];
190
203
  workspaceOptions?: WorkspaceOption[];
204
+ availableExecutionModes?: ExecutionEnvironmentMode[];
191
205
  updatedAt?: number;
192
206
  }
207
+ export interface PermissionModeOption {
208
+ value: string;
209
+ label: string;
210
+ }
193
211
  export interface AgentRuntime {
194
212
  clientType?: AgentClientType;
195
213
  hostMode?: boolean;
196
214
  defaultModel?: string;
197
215
  defaultPermissionMode?: string;
216
+ availablePermissionModes?: PermissionModeOption[];
198
217
  defaultWorkspaceId?: string;
218
+ /**
219
+ * Execution modes the host will accept. The runtime advertises this so the
220
+ * app can offer matching choices; it is NOT used to auto-populate missing
221
+ * session-config values.
222
+ */
223
+ availableExecutionModes?: ExecutionEnvironmentMode[];
224
+ /**
225
+ * Reference default surfaced to UI. Treated as advisory only — callers must
226
+ * still have the user confirm a selection before persisting.
227
+ */
228
+ defaultExecutionMode?: ExecutionEnvironmentMode;
199
229
  availableModels?: ModelOption[];
200
230
  availableWorkspaces?: WorkspaceOption[];
201
231
  updatedAt?: number;
package/dist/types.js CHANGED
@@ -12,6 +12,7 @@ export const AGENT_CAPABILITIES = {
12
12
  'claude-code': {
13
13
  supportsModelSwitch: true,
14
14
  supportsPermissionMode: true,
15
+ supportsRuntimePermissionMode: true,
15
16
  supportsEffort: true,
16
17
  supportsSessionState: true,
17
18
  supportsInterrupt: true,
@@ -20,7 +21,8 @@ export const AGENT_CAPABILITIES = {
20
21
  },
21
22
  'codex': {
22
23
  supportsModelSwitch: false,
23
- supportsPermissionMode: false,
24
+ supportsPermissionMode: true,
25
+ supportsRuntimePermissionMode: false,
24
26
  supportsEffort: false,
25
27
  supportsSessionState: true,
26
28
  supportsInterrupt: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/core",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Canon core — shared types, REST client, SSE stream, and registration for Canon messaging",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",