@canonmsg/core 0.7.1 → 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.
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/core",
3
- "version": "0.7.1",
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",