@canonmsg/core 0.7.1 → 0.7.3

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.
@@ -1,8 +1,8 @@
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
6
  import { EXECUTION_ENVIRONMENT_MODES, isExecutionEnvironmentMode, } from './execution-environment-mode.js';
7
7
  export { EXECUTION_ENVIRONMENT_MODES, isExecutionEnvironmentMode };
8
8
  export class ExecutionEnvironmentError extends Error {
@@ -56,46 +56,19 @@ function detectRepoRoot(cwd) {
56
56
  const repoRoot = result.stdout.trim();
57
57
  return repoRoot ? resolve(repoRoot) : null;
58
58
  }
59
- function isRepoClean(repoRoot) {
60
- const result = runGit(repoRoot, ['status', '--porcelain']);
61
- return result.ok && result.stdout.trim() === '';
62
- }
63
59
  function branchExists(repoRoot, branch) {
64
60
  return runGit(repoRoot, ['show-ref', '--verify', '--quiet', `refs/heads/${branch}`]).ok;
65
61
  }
66
- function loadWorkspaceLock(lockPath) {
67
- try {
68
- return JSON.parse(readFileSync(lockPath, 'utf-8'));
69
- }
70
- catch {
71
- return null;
72
- }
73
- }
74
- function cleanupStaleWorkspaceLock(lockPath) {
75
- const record = loadWorkspaceLock(lockPath);
76
- if (!record) {
77
- try {
78
- unlinkSync(lockPath);
79
- return true;
80
- }
81
- catch {
82
- return false;
83
- }
84
- }
85
- if (isProcessAlive(record.pid))
86
- return false;
87
- try {
88
- unlinkSync(lockPath);
89
- return true;
90
- }
91
- catch {
92
- return false;
93
- }
94
- }
95
- 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) {
96
67
  mkdirSync(WORKSPACE_LOCKS_ROOT, { recursive: true });
97
68
  const workspaceCwd = resolve(input.workspaceCwd);
98
- 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`);
99
72
  const record = {
100
73
  pid: process.pid,
101
74
  agentId: input.agentId,
@@ -103,34 +76,13 @@ function acquireWorkspaceLock(input) {
103
76
  workspaceCwd,
104
77
  createdAt: new Date().toISOString(),
105
78
  };
106
- while (true) {
107
- try {
108
- writeFileSync(lockPath, JSON.stringify(record, null, 2), { flag: 'wx' });
109
- return lockPath;
110
- }
111
- catch (error) {
112
- const code = error.code;
113
- if (code !== 'EEXIST') {
114
- throw error;
115
- }
116
- const existing = loadWorkspaceLock(lockPath);
117
- if (!existing) {
118
- if (cleanupStaleWorkspaceLock(lockPath))
119
- continue;
120
- throw new Error(`Workspace lock file is unreadable: ${workspaceCwd}`);
121
- }
122
- if (!isProcessAlive(existing.pid)) {
123
- if (cleanupStaleWorkspaceLock(lockPath))
124
- continue;
125
- }
126
- if (existing.pid === process.pid
127
- && existing.agentId === input.agentId
128
- && existing.conversationId === input.conversationId) {
129
- return lockPath;
130
- }
131
- 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.');
132
- }
79
+ try {
80
+ writeFileSync(lockPath, JSON.stringify(record, null, 2));
81
+ }
82
+ catch {
83
+ // Best-effort; an unwritable breadcrumb must not block a session.
133
84
  }
85
+ return lockPath;
134
86
  }
135
87
  export function buildConversationEnvironmentKey(conversationId, workspaceCwd) {
136
88
  return `${conversationId}:${resolve(workspaceCwd)}`;
@@ -217,12 +169,12 @@ export function prepareConversationEnvironment(input) {
217
169
  cwd: baseCwd,
218
170
  baseCwd,
219
171
  mode: 'locked',
220
- lockPath: acquireWorkspaceLock({
172
+ lockPath: recordWorkspaceUsage({
221
173
  agentId: input.agentId,
222
174
  conversationId: input.conversationId,
223
175
  workspaceCwd: baseCwd,
224
176
  }),
225
- reason: 'Worktree isolation is disabled for this host',
177
+ reason: 'Sharing the base workspace (locked mode)',
226
178
  };
227
179
  }
228
180
  const repoRoot = detectRepoRoot(baseCwd);
@@ -242,7 +194,11 @@ export function prepareConversationEnvironment(input) {
242
194
  };
243
195
  }
244
196
  }
245
- 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.
246
202
  mkdirSync(dirname(spec.worktreePath), { recursive: true });
247
203
  const addArgs = branchExists(repoRoot, spec.branch)
248
204
  ? ['worktree', 'add', spec.worktreePath, spec.branch]
@@ -264,14 +220,14 @@ export function prepareConversationEnvironment(input) {
264
220
  cwd: baseCwd,
265
221
  baseCwd,
266
222
  mode: 'locked',
267
- lockPath: acquireWorkspaceLock({
223
+ lockPath: recordWorkspaceUsage({
268
224
  agentId: input.agentId,
269
225
  conversationId: input.conversationId,
270
226
  workspaceCwd: baseCwd,
271
227
  }),
272
228
  reason: repoRoot
273
- ? 'Base repository is dirty or worktree creation failed'
274
- : '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',
275
231
  };
276
232
  }
277
233
  export function releaseConversationEnvironment(environment) {
package/dist/types.d.ts CHANGED
@@ -17,6 +17,25 @@ export interface ForwardedFrom {
17
17
  sourceConversationId: string;
18
18
  messageId: string;
19
19
  }
20
+ /**
21
+ * Server-serialized contact-card payload. Emitted on messages with
22
+ * `contentType: 'contact_card'` so agents receive the referenced user's
23
+ * identity alongside the card. Agents use the referenced `userId` with the
24
+ * standard send-message path; if the target's inbound policy blocks cold
25
+ * contact, they first send a contact request and retry after approval.
26
+ */
27
+ export interface ContactCardPayload {
28
+ userId: string;
29
+ displayName: string;
30
+ avatarUrl: string | null;
31
+ userType: 'human' | 'ai_agent';
32
+ about?: string;
33
+ isActive?: boolean;
34
+ ownerId?: string;
35
+ ownerName?: string;
36
+ accessLevel?: 'open' | 'owner-only';
37
+ lifecycleState?: string;
38
+ }
20
39
  export interface CanonMessage {
21
40
  id: string;
22
41
  senderId: string;
@@ -33,6 +52,7 @@ export interface CanonMessage {
33
52
  forwardedFrom?: ForwardedFrom;
34
53
  workSession?: CanonWorkSessionContext | null;
35
54
  metadata?: Record<string, unknown>;
55
+ contactCard?: ContactCardPayload;
36
56
  status: 'sent' | 'read';
37
57
  deleted: boolean;
38
58
  createdAt: string;
@@ -122,6 +142,8 @@ export interface MessageCreatedPayload {
122
142
  workSession?: CanonWorkSessionContext | null;
123
143
  /** Structured metadata for rich UI (approval cards, etc.) */
124
144
  metadata?: Record<string, unknown>;
145
+ /** Populated when `contentType === 'contact_card'`. */
146
+ contactCard?: ContactCardPayload;
125
147
  };
126
148
  }
127
149
  export interface TypingPayload {
@@ -251,4 +273,5 @@ export interface RegistrationStatus {
251
273
  agentName: string;
252
274
  agentId?: string;
253
275
  apiKey?: string;
276
+ apiKeyDelivered?: boolean;
254
277
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/core",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
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",