@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.
- package/dist/execution-environment.js +26 -70
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import { existsSync, mkdirSync,
|
|
3
|
+
import { existsSync, mkdirSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
4
4
|
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
5
|
-
import { CANON_DIR
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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:
|
|
172
|
+
lockPath: recordWorkspaceUsage({
|
|
221
173
|
agentId: input.agentId,
|
|
222
174
|
conversationId: input.conversationId,
|
|
223
175
|
workspaceCwd: baseCwd,
|
|
224
176
|
}),
|
|
225
|
-
reason: '
|
|
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
|
|
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:
|
|
223
|
+
lockPath: recordWorkspaceUsage({
|
|
268
224
|
agentId: input.agentId,
|
|
269
225
|
conversationId: input.conversationId,
|
|
270
226
|
workspaceCwd: baseCwd,
|
|
271
227
|
}),
|
|
272
228
|
reason: repoRoot
|
|
273
|
-
? '
|
|
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) {
|