ai-maestro 1.0.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.
- package/CHANGELOG.md +11 -0
- package/LICENSE +15 -0
- package/README.md +727 -0
- package/bin/maestro.mjs +246 -0
- package/package.json +29 -0
- package/src/checkpoint.mjs +102 -0
- package/src/codex-diagnostics.mjs +682 -0
- package/src/codex-home-inspect.mjs +252 -0
- package/src/codex-home.mjs +258 -0
- package/src/commands.mjs +809 -0
- package/src/config.mjs +11 -0
- package/src/debug.mjs +164 -0
- package/src/encoding-guard.mjs +125 -0
- package/src/engines/ai-router-engine.mjs +37 -0
- package/src/engines/codex-engine.mjs +21 -0
- package/src/engines/engine.mjs +21 -0
- package/src/engines/registry.mjs +29 -0
- package/src/files.mjs +65 -0
- package/src/format.mjs +132 -0
- package/src/lock.mjs +439 -0
- package/src/memory-log.mjs +18 -0
- package/src/memory.mjs +1 -0
- package/src/orchestration/attempt-chain-runtime.mjs +627 -0
- package/src/orchestration/budget-manager.mjs +121 -0
- package/src/orchestration/capability-registry.mjs +108 -0
- package/src/orchestration/consolidator.mjs +772 -0
- package/src/orchestration/delegation-executor.mjs +262 -0
- package/src/orchestration/delegation-manager.mjs +116 -0
- package/src/orchestration/deployment-intent.mjs +36 -0
- package/src/orchestration/engine-history.mjs +110 -0
- package/src/orchestration/engine-policy.mjs +73 -0
- package/src/orchestration/engine-selector.mjs +187 -0
- package/src/orchestration/execution-context.mjs +45 -0
- package/src/orchestration/failure-classifier.mjs +136 -0
- package/src/orchestration/failure-evidence.mjs +237 -0
- package/src/orchestration/fallback-chain-lock.mjs +217 -0
- package/src/orchestration/fallback-chain-trail.mjs +218 -0
- package/src/orchestration/fallback-executor.mjs +146 -0
- package/src/orchestration/fallback-graph.mjs +106 -0
- package/src/orchestration/fallback-recommendation.mjs +73 -0
- package/src/orchestration/file-conflict-detector.mjs +126 -0
- package/src/orchestration/host-validation.mjs +241 -0
- package/src/orchestration/orchestration-loop.mjs +1971 -0
- package/src/orchestration/orchestration-runtime.mjs +1019 -0
- package/src/orchestration/orchestration-scheduler.mjs +135 -0
- package/src/orchestration/orchestration-trail.mjs +192 -0
- package/src/orchestration/preflight.mjs +212 -0
- package/src/orchestration/provider-health.mjs +566 -0
- package/src/orchestration/provider-router.mjs +352 -0
- package/src/orchestration/rc-check-adapters.mjs +817 -0
- package/src/orchestration/rc-check.mjs +544 -0
- package/src/orchestration/rc-policy.mjs +200 -0
- package/src/orchestration/runtime-gate.mjs +146 -0
- package/src/orchestration/sector-managers.mjs +215 -0
- package/src/orchestration/self-hosting-canary.mjs +231 -0
- package/src/orchestration/self-hosting-readiness.mjs +244 -0
- package/src/orchestration/spec-planner.mjs +877 -0
- package/src/orchestration/subtask-planner.mjs +176 -0
- package/src/orchestration/task-classifier.mjs +241 -0
- package/src/orchestration/verifier.mjs +1608 -0
- package/src/orchestration-commands.mjs +279 -0
- package/src/planner.mjs +38 -0
- package/src/project.mjs +1 -0
- package/src/run-diagnostics.mjs +641 -0
- package/src/runner.mjs +243 -0
- package/src/safety.mjs +116 -0
- package/src/schema.mjs +371 -0
- package/src/session-commands.mjs +521 -0
- package/src/shell.mjs +93 -0
- package/src/smart-planner.mjs +249 -0
- package/src/task-commands.mjs +1182 -0
- package/src/tasks.mjs +134 -0
- package/src/ui/commands.mjs +76 -0
- package/src/ui/events.mjs +45 -0
- package/src/ui/public/app.js +600 -0
- package/src/ui/public/index.html +88 -0
- package/src/ui/public/style.css +460 -0
- package/src/ui/server.mjs +112 -0
- package/src/ui/state.mjs +504 -0
- package/src/usage.mjs +178 -0
- package/src/workspace-diff.mjs +228 -0
package/src/lock.mjs
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { CONFIG } from './config.mjs';
|
|
5
|
+
|
|
6
|
+
const lockPath = path.join(CONFIG.maestroPath, '.lock.json');
|
|
7
|
+
const taskLocksDir = path.join(CONFIG.maestroPath, 'locks', 'tasks');
|
|
8
|
+
const STALE_MS = 30 * 60 * 1000;
|
|
9
|
+
const READ_RETRY_ATTEMPTS = 30;
|
|
10
|
+
const READ_RETRY_DELAY_MS = 5;
|
|
11
|
+
const RENAME_RETRY_ATTEMPTS = 30;
|
|
12
|
+
const RENAME_RETRY_DELAY_MS = 5;
|
|
13
|
+
const ACQUIRE_RETRY_ATTEMPTS = 3;
|
|
14
|
+
const ownedTaskLockTokens = new Set();
|
|
15
|
+
let globalLockHandle = null;
|
|
16
|
+
|
|
17
|
+
export function getLockPath() {
|
|
18
|
+
return lockPath;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function delay(ms) {
|
|
22
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function ownerToken() {
|
|
26
|
+
return process.pid + ':' + crypto.randomUUID();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isPidAlive(pid) {
|
|
30
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
process.kill(pid, 0);
|
|
35
|
+
return true;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return error.code === 'EPERM';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isTaskLockHeldByActiveOwner(lock) {
|
|
42
|
+
if (!lock || !Number.isInteger(lock.pid) || lock.pid <= 0) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (lock.pid === process.pid && lock.ownerToken && !ownedTaskLockTokens.has(lock.ownerToken)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return isPidAlive(lock.pid);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function safeTaskKey(taskId) {
|
|
52
|
+
const value = String(taskId);
|
|
53
|
+
if (value.length === 0) {
|
|
54
|
+
throw new Error('taskId is required');
|
|
55
|
+
}
|
|
56
|
+
return crypto.createHash('sha256').update(value).digest('hex');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getTaskLockPath(taskId) {
|
|
60
|
+
return path.join(taskLocksDir, 'task-' + safeTaskKey(taskId) + '.lock.json');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function inspectTaskLock(taskId) {
|
|
64
|
+
const taskIdText = String(taskId);
|
|
65
|
+
const taskLockPath = getTaskLockPath(taskIdText);
|
|
66
|
+
let raw;
|
|
67
|
+
try {
|
|
68
|
+
raw = await fs.readFile(taskLockPath, 'utf-8');
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error.code === 'ENOENT') {
|
|
71
|
+
return {
|
|
72
|
+
taskId: taskIdText,
|
|
73
|
+
lockPath: taskLockPath,
|
|
74
|
+
exists: false,
|
|
75
|
+
lock: null,
|
|
76
|
+
lockCorrupted: false,
|
|
77
|
+
pidAlive: false,
|
|
78
|
+
heldByActive: false,
|
|
79
|
+
activeLock: null
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const lock = JSON.parse(raw);
|
|
87
|
+
const heldByActive = isTaskLockHeldByActiveOwner(lock);
|
|
88
|
+
return {
|
|
89
|
+
taskId: taskIdText,
|
|
90
|
+
lockPath: taskLockPath,
|
|
91
|
+
exists: true,
|
|
92
|
+
lock,
|
|
93
|
+
lockCorrupted: false,
|
|
94
|
+
pidAlive: isPidAlive(lock && lock.pid),
|
|
95
|
+
heldByActive,
|
|
96
|
+
activeLock: heldByActive ? safeActiveLockInfo(lock, taskIdText) : null
|
|
97
|
+
};
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (error instanceof SyntaxError) {
|
|
100
|
+
return {
|
|
101
|
+
taskId: taskIdText,
|
|
102
|
+
lockPath: taskLockPath,
|
|
103
|
+
exists: true,
|
|
104
|
+
lock: null,
|
|
105
|
+
lockCorrupted: true,
|
|
106
|
+
parseError: error.message,
|
|
107
|
+
rawBytes: Buffer.byteLength(raw, 'utf8'),
|
|
108
|
+
pidAlive: false,
|
|
109
|
+
heldByActive: false,
|
|
110
|
+
activeLock: null
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function readJsonFile(filePath) {
|
|
118
|
+
try {
|
|
119
|
+
return JSON.parse(await fs.readFile(filePath, 'utf-8'));
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (error.code === 'ENOENT' || error instanceof SyntaxError) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function readLockEventually(filePath) {
|
|
129
|
+
for (let attempt = 0; attempt < READ_RETRY_ATTEMPTS; attempt += 1) {
|
|
130
|
+
const lock = await readJsonFile(filePath);
|
|
131
|
+
if (lock) {
|
|
132
|
+
return lock;
|
|
133
|
+
}
|
|
134
|
+
await delay(READ_RETRY_DELAY_MS);
|
|
135
|
+
}
|
|
136
|
+
return readJsonFile(filePath);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function writeJsonExclusive(filePath, value) {
|
|
140
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
141
|
+
const handle = await fs.open(filePath, 'wx');
|
|
142
|
+
let completed = false;
|
|
143
|
+
try {
|
|
144
|
+
await handle.writeFile(JSON.stringify(value, null, 2));
|
|
145
|
+
completed = true;
|
|
146
|
+
} finally {
|
|
147
|
+
await handle.close();
|
|
148
|
+
if (!completed) {
|
|
149
|
+
await fs.rm(filePath, { force: true });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function renameWithTransientRetry(sourcePath, targetPath) {
|
|
155
|
+
for (let attempt = 0; attempt < RENAME_RETRY_ATTEMPTS; attempt += 1) {
|
|
156
|
+
try {
|
|
157
|
+
await fs.rename(sourcePath, targetPath);
|
|
158
|
+
return { ok: true };
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (error.code !== 'EPERM' && error.code !== 'EACCES') {
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
await delay(RENAME_RETRY_DELAY_MS);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return { ok: false };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function lockAgeMs(lock) {
|
|
170
|
+
const startedAt = Date.parse(lock && lock.startedAt);
|
|
171
|
+
return Number.isFinite(startedAt) ? Date.now() - startedAt : null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function safeActiveLockInfo(lock, taskId = null) {
|
|
175
|
+
const ageMs = lockAgeMs(lock);
|
|
176
|
+
const info = {
|
|
177
|
+
pid: Number.isInteger(lock && lock.pid) ? lock.pid : null,
|
|
178
|
+
command: typeof (lock && lock.command) === 'string' ? lock.command : null,
|
|
179
|
+
startedAt: typeof (lock && lock.startedAt) === 'string' ? lock.startedAt : null
|
|
180
|
+
};
|
|
181
|
+
if (taskId !== null) {
|
|
182
|
+
info.taskId = String(taskId);
|
|
183
|
+
}
|
|
184
|
+
if (ageMs !== null && ageMs > STALE_MS) {
|
|
185
|
+
info.warning = 'active_lock_exceeds_stale_threshold';
|
|
186
|
+
}
|
|
187
|
+
return info;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function safeRecoveryLockInfo(recovery, taskId = null) {
|
|
191
|
+
const info = {
|
|
192
|
+
pid: Number.isInteger(recovery && recovery.pid) ? recovery.pid : null,
|
|
193
|
+
command: 'lock recovery in progress',
|
|
194
|
+
startedAt: typeof (recovery && recovery.startedAt) === 'string' ? recovery.startedAt : null,
|
|
195
|
+
reason: 'recovery_in_progress'
|
|
196
|
+
};
|
|
197
|
+
if (taskId !== null) {
|
|
198
|
+
info.taskId = String(taskId);
|
|
199
|
+
}
|
|
200
|
+
return info;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function rememberGlobalLock(lock) {
|
|
204
|
+
globalLockHandle = {
|
|
205
|
+
pid: lock.pid,
|
|
206
|
+
command: lock.command,
|
|
207
|
+
startedAt: lock.startedAt
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function isSameGlobalLock(left, right) {
|
|
212
|
+
return Boolean(left && right) &&
|
|
213
|
+
left.pid === right.pid &&
|
|
214
|
+
left.command === right.command &&
|
|
215
|
+
left.startedAt === right.startedAt;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function maestroLockedError(existing) {
|
|
219
|
+
const pid = existing && existing.pid ? existing.pid : 'unknown';
|
|
220
|
+
const command = existing && existing.command ? existing.command : 'unknown command';
|
|
221
|
+
const startedAt = existing && existing.startedAt ? existing.startedAt : 'unknown time';
|
|
222
|
+
const error = new Error(
|
|
223
|
+
'Another maestro process (PID ' + pid + ') is running "' + command +
|
|
224
|
+
'" since ' + startedAt + '. Wait for it to finish or delete .maestro/.lock.json if you are sure it is stale.'
|
|
225
|
+
);
|
|
226
|
+
error.code = 'MAESTRO_LOCKED';
|
|
227
|
+
return error;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function replaceLockWithRecoveryClaim(lockFilePath, nextLock, confirmOwner) {
|
|
231
|
+
const token = ownerToken();
|
|
232
|
+
const recoveryPath = lockFilePath + '.recovery';
|
|
233
|
+
const recoveryClaim = {
|
|
234
|
+
pid: process.pid,
|
|
235
|
+
startedAt: new Date().toISOString(),
|
|
236
|
+
ownerToken: token
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
await writeJsonExclusive(recoveryPath, recoveryClaim);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (error.code !== 'EEXIST') {
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
return { acquiredClaim: false };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let tempPath = null;
|
|
249
|
+
try {
|
|
250
|
+
const current = await readLockEventually(lockFilePath);
|
|
251
|
+
if (current && isTaskLockHeldByActiveOwner(current)) {
|
|
252
|
+
return { acquiredClaim: true, replaced: false, activeLock: current };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
tempPath = lockFilePath + '.tmp-' + process.pid + '-' + token.replace(/[^A-Za-z0-9_-]/g, '');
|
|
256
|
+
await writeJsonExclusive(tempPath, nextLock);
|
|
257
|
+
const renamed = await renameWithTransientRetry(tempPath, lockFilePath);
|
|
258
|
+
if (!renamed.ok) {
|
|
259
|
+
const activeLock = await readLockEventually(lockFilePath);
|
|
260
|
+
return { acquiredClaim: true, replaced: false, activeLock };
|
|
261
|
+
}
|
|
262
|
+
tempPath = null;
|
|
263
|
+
|
|
264
|
+
const finalLock = await readLockEventually(lockFilePath);
|
|
265
|
+
if (!confirmOwner(finalLock)) {
|
|
266
|
+
return { acquiredClaim: true, replaced: false, activeLock: finalLock };
|
|
267
|
+
}
|
|
268
|
+
return { acquiredClaim: true, replaced: true, finalLock };
|
|
269
|
+
} finally {
|
|
270
|
+
if (tempPath) {
|
|
271
|
+
await fs.rm(tempPath, { force: true });
|
|
272
|
+
}
|
|
273
|
+
const currentClaim = await readJsonFile(recoveryPath);
|
|
274
|
+
if (currentClaim && currentClaim.ownerToken === token && currentClaim.pid === process.pid) {
|
|
275
|
+
await fs.rm(recoveryPath, { force: true });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function waitForRecovery(lockFilePath, taskId) {
|
|
281
|
+
const recoveryPath = lockFilePath + '.recovery';
|
|
282
|
+
for (let attempt = 0; attempt < READ_RETRY_ATTEMPTS; attempt += 1) {
|
|
283
|
+
const recovery = await readJsonFile(recoveryPath);
|
|
284
|
+
if (!recovery) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
const current = await readJsonFile(lockFilePath);
|
|
288
|
+
if (current && isTaskLockHeldByActiveOwner(current)) {
|
|
289
|
+
return { outcome: 'held_by_active', activeLock: safeActiveLockInfo(current, taskId) };
|
|
290
|
+
}
|
|
291
|
+
if (recovery.pid && !isPidAlive(recovery.pid)) {
|
|
292
|
+
await fs.rm(recoveryPath, { force: true });
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
await delay(READ_RETRY_DELAY_MS);
|
|
296
|
+
}
|
|
297
|
+
const current = await readLockEventually(lockFilePath);
|
|
298
|
+
if (current && isTaskLockHeldByActiveOwner(current)) {
|
|
299
|
+
return { outcome: 'held_by_active', activeLock: safeActiveLockInfo(current, taskId) };
|
|
300
|
+
}
|
|
301
|
+
const recovery = await readJsonFile(recoveryPath);
|
|
302
|
+
if (recovery && recovery.pid && isPidAlive(recovery.pid)) {
|
|
303
|
+
return { outcome: 'held_by_active', activeLock: safeRecoveryLockInfo(recovery, taskId) };
|
|
304
|
+
}
|
|
305
|
+
if (recovery && recovery.pid && !isPidAlive(recovery.pid)) {
|
|
306
|
+
await fs.rm(recoveryPath, { force: true });
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function acquireGlobalWithRecovery(command) {
|
|
312
|
+
await fs.mkdir(CONFIG.maestroPath, { recursive: true });
|
|
313
|
+
const lock = { pid: process.pid, command, startedAt: new Date().toISOString() };
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
await writeJsonExclusive(lockPath, lock);
|
|
317
|
+
rememberGlobalLock(lock);
|
|
318
|
+
return lock;
|
|
319
|
+
} catch (error) {
|
|
320
|
+
if (error.code !== 'EEXIST') {
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const existing = await readLockEventually(lockPath);
|
|
326
|
+
if (existing && isPidAlive(existing.pid)) {
|
|
327
|
+
throw maestroLockedError(existing);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
for (let attempt = 0; attempt < ACQUIRE_RETRY_ATTEMPTS; attempt += 1) {
|
|
331
|
+
const recovery = await replaceLockWithRecoveryClaim(
|
|
332
|
+
lockPath,
|
|
333
|
+
lock,
|
|
334
|
+
finalLock => finalLock && finalLock.pid === process.pid && finalLock.startedAt === lock.startedAt && finalLock.command === command
|
|
335
|
+
);
|
|
336
|
+
if (recovery.replaced) {
|
|
337
|
+
rememberGlobalLock(lock);
|
|
338
|
+
return lock;
|
|
339
|
+
}
|
|
340
|
+
if (recovery.activeLock && isPidAlive(recovery.activeLock.pid)) {
|
|
341
|
+
throw maestroLockedError(recovery.activeLock);
|
|
342
|
+
}
|
|
343
|
+
const waited = await waitForRecovery(lockPath, null);
|
|
344
|
+
if (waited && waited.activeLock) {
|
|
345
|
+
throw maestroLockedError(waited.activeLock);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
throw maestroLockedError(await readLockEventually(lockPath));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export async function acquireLock(command) {
|
|
352
|
+
return acquireGlobalWithRecovery(command);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export async function releaseLock() {
|
|
356
|
+
if (!globalLockHandle) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const existing = await readJsonFile(lockPath);
|
|
360
|
+
if (isSameGlobalLock(existing, globalLockHandle)) {
|
|
361
|
+
await fs.rm(lockPath, { force: true });
|
|
362
|
+
globalLockHandle = null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export async function acquireOrRecover(taskId, options = {}) {
|
|
367
|
+
const taskIdText = String(taskId);
|
|
368
|
+
const taskLockPath = getTaskLockPath(taskIdText);
|
|
369
|
+
const token = ownerToken();
|
|
370
|
+
const command = options.command ?? ('task ' + taskIdText);
|
|
371
|
+
const lock = {
|
|
372
|
+
taskId: taskIdText,
|
|
373
|
+
pid: process.pid,
|
|
374
|
+
command,
|
|
375
|
+
startedAt: new Date().toISOString(),
|
|
376
|
+
ownerToken: token
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
await writeJsonExclusive(taskLockPath, lock);
|
|
381
|
+
ownedTaskLockTokens.add(token);
|
|
382
|
+
return {
|
|
383
|
+
outcome: 'acquired',
|
|
384
|
+
lockHandle: { taskId: taskIdText, lockPath: taskLockPath, pid: process.pid, ownerToken: token }
|
|
385
|
+
};
|
|
386
|
+
} catch (error) {
|
|
387
|
+
if (error.code !== 'EEXIST') {
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const existing = await readLockEventually(taskLockPath);
|
|
393
|
+
if (existing && isTaskLockHeldByActiveOwner(existing)) {
|
|
394
|
+
return { outcome: 'held_by_active', activeLock: safeActiveLockInfo(existing || {}, taskIdText) };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
for (let attempt = 0; attempt < ACQUIRE_RETRY_ATTEMPTS; attempt += 1) {
|
|
398
|
+
const recovery = await replaceLockWithRecoveryClaim(
|
|
399
|
+
taskLockPath,
|
|
400
|
+
lock,
|
|
401
|
+
finalLock => finalLock && finalLock.ownerToken === token && finalLock.pid === process.pid
|
|
402
|
+
);
|
|
403
|
+
if (recovery.replaced) {
|
|
404
|
+
ownedTaskLockTokens.add(token);
|
|
405
|
+
return {
|
|
406
|
+
outcome: 'recovered_and_acquired',
|
|
407
|
+
lockHandle: { taskId: taskIdText, lockPath: taskLockPath, pid: process.pid, ownerToken: token }
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
if (recovery.activeLock && isTaskLockHeldByActiveOwner(recovery.activeLock)) {
|
|
411
|
+
return { outcome: 'held_by_active', activeLock: safeActiveLockInfo(recovery.activeLock, taskIdText) };
|
|
412
|
+
}
|
|
413
|
+
const waited = await waitForRecovery(taskLockPath, taskIdText);
|
|
414
|
+
if (waited) {
|
|
415
|
+
return waited;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const current = await readLockEventually(taskLockPath);
|
|
419
|
+
return { outcome: 'held_by_active', activeLock: safeActiveLockInfo(current || {}, taskIdText) };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export async function releaseTaskLock(lockHandle) {
|
|
423
|
+
if (!lockHandle || lockHandle.ownerToken == null || lockHandle.taskId == null) {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
const taskLockPath = getTaskLockPath(lockHandle.taskId);
|
|
427
|
+
const existing = await readJsonFile(taskLockPath);
|
|
428
|
+
if (
|
|
429
|
+
existing &&
|
|
430
|
+
existing.pid === lockHandle.pid &&
|
|
431
|
+
existing.pid === process.pid &&
|
|
432
|
+
existing.ownerToken === lockHandle.ownerToken
|
|
433
|
+
) {
|
|
434
|
+
await fs.rm(taskLockPath, { force: true });
|
|
435
|
+
ownedTaskLockTokens.delete(lockHandle.ownerToken);
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { CONFIG } from './config.mjs';
|
|
3
|
+
import { pathExists } from './files.mjs';
|
|
4
|
+
import { execFile } from './shell.mjs';
|
|
5
|
+
|
|
6
|
+
export async function logMemory(message) {
|
|
7
|
+
const cliPath = path.join(CONFIG.memoryPath, 'tools', 'cli.mjs');
|
|
8
|
+
if (await pathExists(cliPath)) {
|
|
9
|
+
await execFile('node', [cliPath, 'log', message, '--agent', 'maestro-cli']);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function memoryDecision(message) {
|
|
14
|
+
const cliPath = path.join(CONFIG.memoryPath, 'tools', 'cli.mjs');
|
|
15
|
+
if (await pathExists(cliPath)) {
|
|
16
|
+
await execFile('node', [cliPath, 'decision', message, '--agent', 'maestro-cli']);
|
|
17
|
+
}
|
|
18
|
+
}
|
package/src/memory.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
console.log('Memory module');
|