claude-mycelium 2.0.0 → 2.1.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/.agent-meta/_inhibitors.ndjson +1287 -0
- package/.agent-meta/_quarantine.json +45 -0
- package/.agent-meta/config.json +9 -0
- package/.claude/memory.db +0 -0
- package/.claude/settings.local.json +4 -1
- package/README.md +81 -235
- package/SECURITY.md +145 -0
- package/dist/agent/worker.d.ts +8 -0
- package/dist/agent/worker.d.ts.map +1 -0
- package/dist/agent/worker.js +97 -0
- package/dist/agent/worker.js.map +1 -0
- package/dist/bin.d.ts +7 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +11 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli/cost.d.ts +10 -0
- package/dist/cli/cost.d.ts.map +1 -0
- package/dist/cli/cost.js +163 -0
- package/dist/cli/cost.js.map +1 -0
- package/dist/cli/gc.d.ts +10 -0
- package/dist/cli/gc.d.ts.map +1 -0
- package/dist/cli/gc.js +108 -0
- package/dist/cli/gc.js.map +1 -0
- package/dist/cli/gradients.d.ts +10 -0
- package/dist/cli/gradients.d.ts.map +1 -0
- package/dist/cli/gradients.js +69 -0
- package/dist/cli/gradients.js.map +1 -0
- package/dist/cli/index.d.ts +17 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +72 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +11 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +97 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/status.d.ts +10 -0
- package/dist/cli/status.d.ts.map +1 -0
- package/dist/cli/status.js +191 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/coordination/file-locks.d.ts +42 -0
- package/dist/coordination/file-locks.d.ts.map +1 -0
- package/dist/coordination/file-locks.js +269 -0
- package/dist/coordination/file-locks.js.map +1 -0
- package/dist/coordination/index.d.ts +4 -0
- package/dist/coordination/index.d.ts.map +1 -1
- package/dist/coordination/index.js +4 -0
- package/dist/coordination/index.js.map +1 -1
- package/dist/coordination/inhibitors.d.ts +84 -0
- package/dist/coordination/inhibitors.d.ts.map +1 -0
- package/dist/coordination/inhibitors.js +290 -0
- package/dist/coordination/inhibitors.js.map +1 -0
- package/dist/coordination/process-manager.d.ts +73 -0
- package/dist/coordination/process-manager.d.ts.map +1 -0
- package/dist/coordination/process-manager.js +144 -0
- package/dist/coordination/process-manager.js.map +1 -0
- package/dist/core/agent-executor.d.ts.map +1 -1
- package/dist/core/agent-executor.js +28 -10
- package/dist/core/agent-executor.js.map +1 -1
- package/dist/core/change-applier.d.ts +29 -5
- package/dist/core/change-applier.d.ts.map +1 -1
- package/dist/core/change-applier.js +254 -24
- package/dist/core/change-applier.js.map +1 -1
- package/dist/core/signals/churn.d.ts.map +1 -1
- package/dist/core/signals/churn.js +6 -4
- package/dist/core/signals/churn.js.map +1 -1
- package/dist/core/signals/debt.d.ts.map +1 -1
- package/dist/core/signals/debt.js +4 -3
- package/dist/core/signals/debt.js.map +1 -1
- package/dist/cost/cost-tracker.d.ts.map +1 -1
- package/dist/cost/cost-tracker.js +2 -0
- package/dist/cost/cost-tracker.js.map +1 -1
- package/dist/gc/index.d.ts +17 -0
- package/dist/gc/index.d.ts.map +1 -0
- package/dist/gc/index.js +17 -0
- package/dist/gc/index.js.map +1 -0
- package/dist/gc/runner.d.ts +39 -0
- package/dist/gc/runner.d.ts.map +1 -0
- package/dist/gc/runner.js +277 -0
- package/dist/gc/runner.js.map +1 -0
- package/dist/gc/trace-compactor.d.ts +31 -0
- package/dist/gc/trace-compactor.d.ts.map +1 -0
- package/dist/gc/trace-compactor.js +162 -0
- package/dist/gc/trace-compactor.js.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/prompts/index.d.ts +2 -1
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js.map +1 -1
- package/dist/quarantine/explorer.d.ts +65 -0
- package/dist/quarantine/explorer.d.ts.map +1 -0
- package/dist/quarantine/explorer.js +175 -0
- package/dist/quarantine/explorer.js.map +1 -0
- package/dist/quarantine/index.d.ts +7 -0
- package/dist/quarantine/index.d.ts.map +1 -0
- package/dist/quarantine/index.js +7 -0
- package/dist/quarantine/index.js.map +1 -0
- package/dist/quarantine/manager.d.ts +75 -0
- package/dist/quarantine/manager.d.ts.map +1 -0
- package/dist/quarantine/manager.js +275 -0
- package/dist/quarantine/manager.js.map +1 -0
- package/dist/task/acceptance.d.ts +29 -0
- package/dist/task/acceptance.d.ts.map +1 -0
- package/dist/task/acceptance.js +228 -0
- package/dist/task/acceptance.js.map +1 -0
- package/dist/task/executor.d.ts +30 -0
- package/dist/task/executor.d.ts.map +1 -0
- package/dist/task/executor.js +429 -0
- package/dist/task/executor.js.map +1 -0
- package/dist/task/index.d.ts +12 -0
- package/dist/task/index.d.ts.map +1 -0
- package/dist/task/index.js +12 -0
- package/dist/task/index.js.map +1 -0
- package/dist/task/planner.d.ts +21 -0
- package/dist/task/planner.d.ts.map +1 -0
- package/dist/task/planner.js +253 -0
- package/dist/task/planner.js.map +1 -0
- package/dist/task/storage.d.ts +46 -0
- package/dist/task/storage.d.ts.map +1 -0
- package/dist/task/storage.js +266 -0
- package/dist/task/storage.js.map +1 -0
- package/dist/trace/trace-event.d.ts +2 -18
- package/dist/trace/trace-event.d.ts.map +1 -1
- package/dist/trace/trace-event.js +6 -6
- package/dist/trace/trace-event.js.map +1 -1
- package/dist/utils/file-utils.d.ts.map +1 -1
- package/dist/utils/file-utils.js +54 -15
- package/dist/utils/file-utils.js.map +1 -1
- package/docs/PHASE5_IMPLEMENTATION.md +237 -0
- package/docs/PHASES-3-7-COMPLETE.md +177 -0
- package/docs/PHASE_4_COMPLETE.md +135 -0
- package/docs/PHASE_7_DELIVERABLES.md +295 -0
- package/docs/PHASE_7_IMPLEMENTATION.md +306 -0
- package/docs/PHASE_7_SUMMARY.txt +195 -0
- package/docs/RELEASE-NOTES-v2.1.md +213 -0
- package/docs/ROADMAP.md +64 -57
- package/docs/SECURITY-AUDIT.md +387 -0
- package/docs/SNAPSHOT.md +59 -32
- package/docs/implementation/phase3-summary.md +220 -0
- package/package.json +19 -11
- package/src/agent/worker.ts +111 -0
- package/src/bin.ts +13 -0
- package/src/cli/cost.ts +210 -0
- package/src/cli/gc.ts +138 -0
- package/src/cli/gradients.ts +95 -0
- package/src/cli/index.ts +79 -0
- package/src/cli/init.ts +139 -0
- package/src/cli/status.ts +218 -0
- package/src/coordination/file-locks.ts +300 -0
- package/src/coordination/index.ts +4 -0
- package/src/coordination/inhibitors.ts +345 -0
- package/src/coordination/process-manager.ts +199 -0
- package/src/core/agent-executor.ts +20 -4
- package/src/core/signals/churn.ts +8 -5
- package/src/core/signals/debt.ts +4 -3
- package/src/cost/cost-tracker.ts +2 -0
- package/src/gc/index.ts +17 -0
- package/src/gc/runner.ts +314 -0
- package/src/gc/trace-compactor.ts +187 -0
- package/src/index.ts +7 -1
- package/src/prompts/index.ts +2 -1
- package/src/quarantine/explorer.ts +234 -0
- package/src/quarantine/index.ts +7 -0
- package/src/quarantine/manager.ts +336 -0
- package/src/task/acceptance.ts +267 -0
- package/src/task/executor.ts +538 -0
- package/src/task/index.ts +38 -0
- package/src/task/planner.ts +294 -0
- package/src/task/storage.ts +332 -0
- package/src/trace/trace-event.ts +7 -26
- package/src/utils/file-utils.ts +61 -15
- package/tests/cli/gc.test.ts +206 -0
- package/tests/cli/init.test.ts +181 -0
- package/tests/cli/status.test.ts +282 -0
- package/tests/coordination/file-locks.test.ts +196 -0
- package/tests/coordination/inhibitors.test.ts +459 -0
- package/tests/coordination/integration.test.ts +195 -0
- package/tests/coordination/process-manager.test.ts +165 -0
- package/tests/gc/trace-compactor.test.ts +245 -0
- package/tests/integration/phase-7.test.ts +145 -0
- package/tests/quarantine/explorer.test.ts +381 -0
- package/tests/quarantine/manager.test.ts +399 -0
- package/tests/security/command-injection.test.ts +88 -0
- package/tests/security/path-traversal.test.ts +103 -0
- package/tests/task/acceptance.test.ts +411 -0
- package/tests/task/executor.test.ts +421 -0
- package/tests/task/planner.test.ts +359 -0
- package/tsconfig.json +2 -2
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status CLI Command
|
|
3
|
+
*
|
|
4
|
+
* Display system overview
|
|
5
|
+
* Show agent count, recent activity
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import * as fs from 'fs/promises';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import { fileExists } from '../utils/file-utils.js';
|
|
13
|
+
import { loadConfig } from '../utils/config.js';
|
|
14
|
+
import type { TraceEvent } from '../types/index.js';
|
|
15
|
+
|
|
16
|
+
const command = new Command()
|
|
17
|
+
.name('status')
|
|
18
|
+
.description('Show system status and recent activity')
|
|
19
|
+
.option('--json', 'Output as JSON')
|
|
20
|
+
.action(async (options: any) => {
|
|
21
|
+
try {
|
|
22
|
+
const status = await getSystemStatus();
|
|
23
|
+
|
|
24
|
+
if (options.json) {
|
|
25
|
+
console.log(JSON.stringify(status, null, 2));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Colorized output
|
|
30
|
+
console.log('');
|
|
31
|
+
console.log(chalk.bold('Claude Mycelium Status'));
|
|
32
|
+
console.log(chalk.dim('─'.repeat(60)));
|
|
33
|
+
|
|
34
|
+
console.log(chalk.bold('Configuration:'));
|
|
35
|
+
console.log(
|
|
36
|
+
` Config version: ${status.config.version}`
|
|
37
|
+
);
|
|
38
|
+
console.log(
|
|
39
|
+
` Created: ${new Date(status.config.created_at).toLocaleDateString()}`
|
|
40
|
+
);
|
|
41
|
+
console.log(
|
|
42
|
+
` Error provider: ${status.config.error_provider}`
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
console.log(chalk.bold('Statistics:'));
|
|
46
|
+
console.log(` Total spawns: ${status.spawn_count}`);
|
|
47
|
+
console.log(` Last GC at spawn: ${status.last_gc_at_spawn}`);
|
|
48
|
+
console.log(
|
|
49
|
+
` Spawns since GC: ${status.spawn_count - status.last_gc_at_spawn}`
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
console.log(chalk.bold('Storage:'));
|
|
53
|
+
console.log(` Trace events: ${status.trace_event_count}`);
|
|
54
|
+
console.log(` Quarantined files: ${status.quarantine_count}`);
|
|
55
|
+
console.log(` Inhibitors: ${status.inhibitor_count}`);
|
|
56
|
+
console.log(
|
|
57
|
+
` Errors logged: ${status.error_count}`
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
if (status.recent_activity.length > 0) {
|
|
61
|
+
console.log(chalk.bold('Recent Activity (last 5):'));
|
|
62
|
+
for (const activity of status.recent_activity.slice(0, 5)) {
|
|
63
|
+
const timeAgo = getTimeAgo(new Date(activity.timestamp));
|
|
64
|
+
const modeColor = getModeColor(activity.mode);
|
|
65
|
+
console.log(
|
|
66
|
+
` ${chalk[modeColor](activity.mode.padEnd(18))} ${activity.file.substring(0, 35).padEnd(35)} ${timeAgo.padStart(10)}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log(chalk.dim('─'.repeat(60)));
|
|
72
|
+
console.log('');
|
|
73
|
+
} catch (error) {
|
|
74
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
75
|
+
console.error(chalk.red(`Error: ${message}`));
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get system status
|
|
82
|
+
*/
|
|
83
|
+
async function getSystemStatus() {
|
|
84
|
+
const config = loadConfig();
|
|
85
|
+
let traceEventCount = 0;
|
|
86
|
+
let quarantineCount = 0;
|
|
87
|
+
let inhibitorCount = 0;
|
|
88
|
+
let errorCount = 0;
|
|
89
|
+
const recentActivity: Array<{ timestamp: string; mode: string; file: string }> = [];
|
|
90
|
+
|
|
91
|
+
// Count trace events
|
|
92
|
+
const tracesDir = '.agent-meta/traces';
|
|
93
|
+
if (fileExists(tracesDir)) {
|
|
94
|
+
try {
|
|
95
|
+
const entries = await fs.readdir(tracesDir);
|
|
96
|
+
const traceFiles = entries.filter(f => f.endsWith('.ndjson'));
|
|
97
|
+
|
|
98
|
+
for (const file of traceFiles) {
|
|
99
|
+
const filePath = path.join(tracesDir, file);
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
103
|
+
const lines = content.split('\n').filter(Boolean);
|
|
104
|
+
traceEventCount += lines.length;
|
|
105
|
+
|
|
106
|
+
// Collect recent activity
|
|
107
|
+
for (const line of lines) {
|
|
108
|
+
try {
|
|
109
|
+
const event = JSON.parse(line) as TraceEvent;
|
|
110
|
+
recentActivity.push({
|
|
111
|
+
timestamp: event.timestamp,
|
|
112
|
+
mode: event.mode,
|
|
113
|
+
file: event.file_path,
|
|
114
|
+
});
|
|
115
|
+
} catch {
|
|
116
|
+
// Skip malformed lines
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
// File read error
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
// Directory read error
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Count quarantine entries
|
|
129
|
+
const quarantinePath = '.agent-meta/_quarantine.json';
|
|
130
|
+
if (fileExists(quarantinePath)) {
|
|
131
|
+
try {
|
|
132
|
+
const content = await fs.readFile(quarantinePath, 'utf-8');
|
|
133
|
+
const quarantine = JSON.parse(content);
|
|
134
|
+
quarantineCount = (quarantine.entries || []).length;
|
|
135
|
+
} catch {
|
|
136
|
+
// File read error
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Count inhibitors
|
|
141
|
+
const inhibitorsPath = '.agent-meta/_inhibitors.ndjson';
|
|
142
|
+
if (fileExists(inhibitorsPath)) {
|
|
143
|
+
try {
|
|
144
|
+
const content = await fs.readFile(inhibitorsPath, 'utf-8');
|
|
145
|
+
const lines = content.split('\n').filter(Boolean);
|
|
146
|
+
inhibitorCount = lines.length;
|
|
147
|
+
} catch {
|
|
148
|
+
// File read error
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Count errors
|
|
153
|
+
const errorsPath = '.agent-meta/_errors.json';
|
|
154
|
+
if (fileExists(errorsPath)) {
|
|
155
|
+
try {
|
|
156
|
+
const content = await fs.readFile(errorsPath, 'utf-8');
|
|
157
|
+
const errors = JSON.parse(content);
|
|
158
|
+
errorCount = (errors.errors || []).length;
|
|
159
|
+
} catch {
|
|
160
|
+
// File read error
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Sort recent activity by timestamp descending
|
|
165
|
+
recentActivity.sort(
|
|
166
|
+
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
config,
|
|
171
|
+
spawn_count: config.spawn_count,
|
|
172
|
+
last_gc_at_spawn: config.last_gc_at_spawn,
|
|
173
|
+
trace_event_count: traceEventCount,
|
|
174
|
+
quarantine_count: quarantineCount,
|
|
175
|
+
inhibitor_count: inhibitorCount,
|
|
176
|
+
error_count: errorCount,
|
|
177
|
+
recent_activity: recentActivity.slice(0, 10),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get human-readable time ago
|
|
183
|
+
*/
|
|
184
|
+
function getTimeAgo(date: Date): string {
|
|
185
|
+
const now = new Date();
|
|
186
|
+
const diff = now.getTime() - date.getTime();
|
|
187
|
+
const seconds = Math.floor(diff / 1000);
|
|
188
|
+
const minutes = Math.floor(seconds / 60);
|
|
189
|
+
const hours = Math.floor(minutes / 60);
|
|
190
|
+
const days = Math.floor(hours / 24);
|
|
191
|
+
|
|
192
|
+
if (days > 0) return `${days}d ago`;
|
|
193
|
+
if (hours > 0) return `${hours}h ago`;
|
|
194
|
+
if (minutes > 0) return `${minutes}m ago`;
|
|
195
|
+
return `${seconds}s ago`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get color for mode
|
|
200
|
+
*/
|
|
201
|
+
function getModeColor(mode: string): 'red' | 'yellow' | 'green' | 'blue' | 'magenta' | 'cyan' {
|
|
202
|
+
switch (mode) {
|
|
203
|
+
case 'error_reducer':
|
|
204
|
+
return 'red';
|
|
205
|
+
case 'complexity_reducer':
|
|
206
|
+
return 'yellow';
|
|
207
|
+
case 'debt_payer':
|
|
208
|
+
return 'magenta';
|
|
209
|
+
case 'stabilizer':
|
|
210
|
+
return 'green';
|
|
211
|
+
case 'explorer':
|
|
212
|
+
return 'cyan';
|
|
213
|
+
default:
|
|
214
|
+
return 'blue';
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export default command;
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-based locking for multi-agent coordination
|
|
3
|
+
* Uses atomic O_CREAT|O_EXCL flags via fs.open() for race-free lock acquisition
|
|
4
|
+
*
|
|
5
|
+
* Reference: ADR-004, second-spec §4.1
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'fs/promises';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import type { LockFile, Mode } from '../types/index.js';
|
|
11
|
+
|
|
12
|
+
const LOCK_DIR = '.agent-meta/locks';
|
|
13
|
+
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Acquire an exclusive lock on a file
|
|
17
|
+
* Uses atomic O_CREAT|O_EXCL to prevent races
|
|
18
|
+
*
|
|
19
|
+
* @param filePath - File to lock (relative path)
|
|
20
|
+
* @param agentId - Agent requesting the lock
|
|
21
|
+
* @param mode - Mode of operation
|
|
22
|
+
* @param taskId - Optional task ID
|
|
23
|
+
* @returns true if lock acquired, false if already locked
|
|
24
|
+
*/
|
|
25
|
+
export async function acquireLock(
|
|
26
|
+
filePath: string,
|
|
27
|
+
agentId: string,
|
|
28
|
+
mode: Mode,
|
|
29
|
+
taskId?: string
|
|
30
|
+
): Promise<boolean> {
|
|
31
|
+
await ensureLockDir();
|
|
32
|
+
|
|
33
|
+
const lockPath = getLockPath(filePath);
|
|
34
|
+
const now = new Date();
|
|
35
|
+
const expiresAt = new Date(now.getTime() + DEFAULT_TIMEOUT_MS);
|
|
36
|
+
|
|
37
|
+
const lockData: LockFile = {
|
|
38
|
+
agent_id: agentId,
|
|
39
|
+
file: filePath,
|
|
40
|
+
mode,
|
|
41
|
+
acquired_at: now.toISOString(),
|
|
42
|
+
expires_at: expiresAt.toISOString(),
|
|
43
|
+
pid: process.pid,
|
|
44
|
+
task_id: taskId,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// Check for expired lock first and attempt takeover
|
|
49
|
+
const takenOver = await tryTakeoverExpiredLock(lockPath, lockData);
|
|
50
|
+
if (takenOver) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Atomic creation: wx = O_CREAT|O_EXCL
|
|
55
|
+
// Will fail if file already exists
|
|
56
|
+
const fd = await fs.open(lockPath, 'wx');
|
|
57
|
+
await fd.writeFile(JSON.stringify(lockData, null, 2));
|
|
58
|
+
await fd.close();
|
|
59
|
+
|
|
60
|
+
return true;
|
|
61
|
+
} catch (error: any) {
|
|
62
|
+
if (error.code === 'EEXIST') {
|
|
63
|
+
// Lock already exists, check if we can take it over
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Release a lock
|
|
72
|
+
*
|
|
73
|
+
* @param filePath - File to unlock
|
|
74
|
+
* @returns true if lock released, false if not locked by this process
|
|
75
|
+
*/
|
|
76
|
+
export async function releaseLock(filePath: string): Promise<boolean> {
|
|
77
|
+
const lockPath = getLockPath(filePath);
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// Verify we own the lock before releasing
|
|
81
|
+
const content = await fs.readFile(lockPath, 'utf-8');
|
|
82
|
+
const lock: LockFile = JSON.parse(content);
|
|
83
|
+
|
|
84
|
+
if (lock.pid !== process.pid) {
|
|
85
|
+
// Not our lock
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await fs.unlink(lockPath);
|
|
90
|
+
return true;
|
|
91
|
+
} catch (error: any) {
|
|
92
|
+
if (error.code === 'ENOENT') {
|
|
93
|
+
// Lock doesn't exist
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if a file is locked
|
|
102
|
+
*
|
|
103
|
+
* @param filePath - File to check
|
|
104
|
+
* @returns LockFile if locked and valid, null otherwise
|
|
105
|
+
*/
|
|
106
|
+
export async function checkLock(filePath: string): Promise<LockFile | null> {
|
|
107
|
+
const lockPath = getLockPath(filePath);
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const content = await fs.readFile(lockPath, 'utf-8');
|
|
111
|
+
const lock: LockFile = JSON.parse(content);
|
|
112
|
+
|
|
113
|
+
// Check if lock is expired
|
|
114
|
+
if (isExpired(lock)) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Check if process is alive
|
|
119
|
+
if (!isProcessAlive(lock.pid)) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return lock;
|
|
124
|
+
} catch (error: any) {
|
|
125
|
+
if (error.code === 'ENOENT') {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Try to take over an expired or dead process lock
|
|
134
|
+
*
|
|
135
|
+
* @param lockPath - Path to lock file
|
|
136
|
+
* @param newLock - New lock data
|
|
137
|
+
* @returns true if takeover successful
|
|
138
|
+
*/
|
|
139
|
+
async function tryTakeoverExpiredLock(
|
|
140
|
+
lockPath: string,
|
|
141
|
+
newLock: LockFile
|
|
142
|
+
): Promise<boolean> {
|
|
143
|
+
try {
|
|
144
|
+
const content = await fs.readFile(lockPath, 'utf-8');
|
|
145
|
+
const oldLock: LockFile = JSON.parse(content);
|
|
146
|
+
|
|
147
|
+
// Check if lock is expired
|
|
148
|
+
if (isExpired(oldLock)) {
|
|
149
|
+
await fs.unlink(lockPath);
|
|
150
|
+
const fd = await fs.open(lockPath, 'wx');
|
|
151
|
+
await fd.writeFile(JSON.stringify(newLock, null, 2));
|
|
152
|
+
await fd.close();
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Check if process is dead
|
|
157
|
+
if (!isProcessAlive(oldLock.pid)) {
|
|
158
|
+
await fs.unlink(lockPath);
|
|
159
|
+
const fd = await fs.open(lockPath, 'wx');
|
|
160
|
+
await fd.writeFile(JSON.stringify(newLock, null, 2));
|
|
161
|
+
await fd.close();
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return false;
|
|
166
|
+
} catch (error: any) {
|
|
167
|
+
if (error.code === 'ENOENT') {
|
|
168
|
+
// Lock doesn't exist, normal acquisition can proceed
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Check if a lock has expired (5-minute timeout)
|
|
177
|
+
*/
|
|
178
|
+
function isExpired(lock: LockFile): boolean {
|
|
179
|
+
const expiresAt = new Date(lock.expires_at);
|
|
180
|
+
return Date.now() > expiresAt.getTime();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Check if a process is alive
|
|
185
|
+
* Uses process.kill(pid, 0) which doesn't actually kill but checks existence
|
|
186
|
+
*/
|
|
187
|
+
function isProcessAlive(pid: number): boolean {
|
|
188
|
+
try {
|
|
189
|
+
// Signal 0 checks if process exists without killing it
|
|
190
|
+
process.kill(pid, 0);
|
|
191
|
+
return true;
|
|
192
|
+
} catch (error: any) {
|
|
193
|
+
if (error.code === 'ESRCH') {
|
|
194
|
+
// Process not found
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
if (error.code === 'EPERM') {
|
|
198
|
+
// Process exists but we don't have permission to signal it
|
|
199
|
+
// This means it's alive
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get the lock file path for a given file
|
|
208
|
+
*/
|
|
209
|
+
function getLockPath(filePath: string): string {
|
|
210
|
+
// Sanitize file path to create a unique lock filename
|
|
211
|
+
const sanitized = filePath.replace(/[^a-zA-Z0-9]/g, '_');
|
|
212
|
+
return path.join(LOCK_DIR, `${sanitized}.lock`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Ensure lock directory exists
|
|
217
|
+
*/
|
|
218
|
+
async function ensureLockDir(): Promise<void> {
|
|
219
|
+
try {
|
|
220
|
+
await fs.mkdir(LOCK_DIR, { recursive: true });
|
|
221
|
+
} catch (error: any) {
|
|
222
|
+
if (error.code !== 'EEXIST') {
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* List all active locks
|
|
230
|
+
*/
|
|
231
|
+
export async function listLocks(): Promise<LockFile[]> {
|
|
232
|
+
try {
|
|
233
|
+
await ensureLockDir();
|
|
234
|
+
const files = await fs.readdir(LOCK_DIR);
|
|
235
|
+
const locks: LockFile[] = [];
|
|
236
|
+
|
|
237
|
+
for (const file of files) {
|
|
238
|
+
if (!file.endsWith('.lock')) continue;
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const lockPath = path.join(LOCK_DIR, file);
|
|
242
|
+
const content = await fs.readFile(lockPath, 'utf-8');
|
|
243
|
+
const lock: LockFile = JSON.parse(content);
|
|
244
|
+
|
|
245
|
+
// Only include valid locks
|
|
246
|
+
if (!isExpired(lock) && isProcessAlive(lock.pid)) {
|
|
247
|
+
locks.push(lock);
|
|
248
|
+
}
|
|
249
|
+
} catch {
|
|
250
|
+
// Skip invalid lock files
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return locks;
|
|
256
|
+
} catch (error: any) {
|
|
257
|
+
if (error.code === 'ENOENT') {
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Clean up expired and dead process locks
|
|
266
|
+
* Returns the number of locks cleaned up
|
|
267
|
+
*/
|
|
268
|
+
export async function cleanupStaleLocks(): Promise<number> {
|
|
269
|
+
try {
|
|
270
|
+
await ensureLockDir();
|
|
271
|
+
const files = await fs.readdir(LOCK_DIR);
|
|
272
|
+
let cleaned = 0;
|
|
273
|
+
|
|
274
|
+
for (const file of files) {
|
|
275
|
+
if (!file.endsWith('.lock')) continue;
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
const lockPath = path.join(LOCK_DIR, file);
|
|
279
|
+
const content = await fs.readFile(lockPath, 'utf-8');
|
|
280
|
+
const lock: LockFile = JSON.parse(content);
|
|
281
|
+
|
|
282
|
+
// Remove if expired or process is dead
|
|
283
|
+
if (isExpired(lock) || !isProcessAlive(lock.pid)) {
|
|
284
|
+
await fs.unlink(lockPath);
|
|
285
|
+
cleaned++;
|
|
286
|
+
}
|
|
287
|
+
} catch {
|
|
288
|
+
// Skip files we can't process
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return cleaned;
|
|
294
|
+
} catch (error: any) {
|
|
295
|
+
if (error.code === 'ENOENT') {
|
|
296
|
+
return 0;
|
|
297
|
+
}
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* - Gradient caching
|
|
6
6
|
* - File locks (Phase 3)
|
|
7
7
|
* - Process management (Phase 3)
|
|
8
|
+
* - Inhibitor signals (Phase 4)
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
export * from './gradient-cache.js';
|
|
12
|
+
export * from './file-locks.js';
|
|
13
|
+
export * from './process-manager.js';
|
|
14
|
+
export * from './inhibitors.js';
|