@eddacraft/anvil-runtime 0.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/LICENSE +14 -0
- package/dist/cache/cache-key.d.ts +45 -0
- package/dist/cache/cache-key.d.ts.map +1 -0
- package/dist/cache/cache-key.js +135 -0
- package/dist/cache/index.d.ts +27 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +38 -0
- package/dist/cache/providers/file-cache.d.ts +63 -0
- package/dist/cache/providers/file-cache.d.ts.map +1 -0
- package/dist/cache/providers/file-cache.js +369 -0
- package/dist/cache/providers/memory-cache.d.ts +52 -0
- package/dist/cache/providers/memory-cache.d.ts.map +1 -0
- package/dist/cache/providers/memory-cache.js +197 -0
- package/dist/cache/providers/null-cache.d.ts +26 -0
- package/dist/cache/providers/null-cache.d.ts.map +1 -0
- package/dist/cache/providers/null-cache.js +50 -0
- package/dist/cache/types.d.ts +114 -0
- package/dist/cache/types.d.ts.map +1 -0
- package/dist/cache/types.js +4 -0
- package/dist/concurrency/agent.d.ts +137 -0
- package/dist/concurrency/agent.d.ts.map +1 -0
- package/dist/concurrency/agent.js +440 -0
- package/dist/concurrency/atomic.d.ts +93 -0
- package/dist/concurrency/atomic.d.ts.map +1 -0
- package/dist/concurrency/atomic.js +281 -0
- package/dist/concurrency/git-agent.d.ts +114 -0
- package/dist/concurrency/git-agent.d.ts.map +1 -0
- package/dist/concurrency/git-agent.js +313 -0
- package/dist/concurrency/index.d.ts +95 -0
- package/dist/concurrency/index.d.ts.map +1 -0
- package/dist/concurrency/index.js +127 -0
- package/dist/concurrency/lock-manager.d.ts +170 -0
- package/dist/concurrency/lock-manager.d.ts.map +1 -0
- package/dist/concurrency/lock-manager.js +525 -0
- package/dist/concurrency/queue-manager.d.ts +166 -0
- package/dist/concurrency/queue-manager.d.ts.map +1 -0
- package/dist/concurrency/queue-manager.js +442 -0
- package/dist/concurrency/types.d.ts +382 -0
- package/dist/concurrency/types.d.ts.map +1 -0
- package/dist/concurrency/types.js +204 -0
- package/dist/export/constraint-collector.d.ts +175 -0
- package/dist/export/constraint-collector.d.ts.map +1 -0
- package/dist/export/constraint-collector.js +203 -0
- package/dist/export/formatters/llms-txt-formatter.d.ts +89 -0
- package/dist/export/formatters/llms-txt-formatter.d.ts.map +1 -0
- package/dist/export/formatters/llms-txt-formatter.js +249 -0
- package/dist/export/formatters/mcp-resource-formatter.d.ts +186 -0
- package/dist/export/formatters/mcp-resource-formatter.d.ts.map +1 -0
- package/dist/export/formatters/mcp-resource-formatter.js +139 -0
- package/dist/export/formatters/prompt-formatter.d.ts +83 -0
- package/dist/export/formatters/prompt-formatter.d.ts.map +1 -0
- package/dist/export/formatters/prompt-formatter.js +256 -0
- package/dist/export/index.d.ts +10 -0
- package/dist/export/index.d.ts.map +1 -0
- package/dist/export/index.js +9 -0
- package/dist/gate/check.interface.d.ts +15 -0
- package/dist/gate/check.interface.d.ts.map +1 -0
- package/dist/gate/check.interface.js +18 -0
- package/dist/gate/checks/antipattern.check.d.ts +27 -0
- package/dist/gate/checks/antipattern.check.d.ts.map +1 -0
- package/dist/gate/checks/antipattern.check.js +140 -0
- package/dist/gate/checks/architecture/circular-detector.d.ts +33 -0
- package/dist/gate/checks/architecture/circular-detector.d.ts.map +1 -0
- package/dist/gate/checks/architecture/circular-detector.js +71 -0
- package/dist/gate/checks/architecture/dependency-analyzer.d.ts +81 -0
- package/dist/gate/checks/architecture/dependency-analyzer.d.ts.map +1 -0
- package/dist/gate/checks/architecture/dependency-analyzer.js +136 -0
- package/dist/gate/checks/architecture/layer-validator.d.ts +75 -0
- package/dist/gate/checks/architecture/layer-validator.d.ts.map +1 -0
- package/dist/gate/checks/architecture/layer-validator.js +193 -0
- package/dist/gate/checks/architecture.check.d.ts +56 -0
- package/dist/gate/checks/architecture.check.d.ts.map +1 -0
- package/dist/gate/checks/architecture.check.js +394 -0
- package/dist/gate/checks/command-safety.check.d.ts +12 -0
- package/dist/gate/checks/command-safety.check.d.ts.map +1 -0
- package/dist/gate/checks/command-safety.check.js +230 -0
- package/dist/gate/checks/coverage.check.d.ts +9 -0
- package/dist/gate/checks/coverage.check.d.ts.map +1 -0
- package/dist/gate/checks/coverage.check.js +81 -0
- package/dist/gate/checks/dependency.check.d.ts +17 -0
- package/dist/gate/checks/dependency.check.d.ts.map +1 -0
- package/dist/gate/checks/dependency.check.js +342 -0
- package/dist/gate/checks/eslint.check.d.ts +14 -0
- package/dist/gate/checks/eslint.check.d.ts.map +1 -0
- package/dist/gate/checks/eslint.check.js +79 -0
- package/dist/gate/checks/policy.check.d.ts +78 -0
- package/dist/gate/checks/policy.check.d.ts.map +1 -0
- package/dist/gate/checks/policy.check.js +457 -0
- package/dist/gate/checks/secret/entropy-detector.d.ts +44 -0
- package/dist/gate/checks/secret/entropy-detector.d.ts.map +1 -0
- package/dist/gate/checks/secret/entropy-detector.js +76 -0
- package/dist/gate/checks/secret/git-scanner.d.ts +36 -0
- package/dist/gate/checks/secret/git-scanner.d.ts.map +1 -0
- package/dist/gate/checks/secret/git-scanner.js +90 -0
- package/dist/gate/checks/secret/secret-patterns.d.ts +42 -0
- package/dist/gate/checks/secret/secret-patterns.d.ts.map +1 -0
- package/dist/gate/checks/secret/secret-patterns.js +137 -0
- package/dist/gate/checks/secret.check.d.ts +56 -0
- package/dist/gate/checks/secret.check.d.ts.map +1 -0
- package/dist/gate/checks/secret.check.js +245 -0
- package/dist/gate/config/command-safety-config.d.ts +5 -0
- package/dist/gate/config/command-safety-config.d.ts.map +1 -0
- package/dist/gate/config/command-safety-config.js +69 -0
- package/dist/gate/config/index.d.ts +2 -0
- package/dist/gate/config/index.d.ts.map +1 -0
- package/dist/gate/config/index.js +1 -0
- package/dist/gate/formatters/command-safety-formatter.d.ts +10 -0
- package/dist/gate/formatters/command-safety-formatter.d.ts.map +1 -0
- package/dist/gate/formatters/command-safety-formatter.js +64 -0
- package/dist/gate/formatters/index.d.ts +2 -0
- package/dist/gate/formatters/index.d.ts.map +1 -0
- package/dist/gate/formatters/index.js +1 -0
- package/dist/gate/gate-config.d.ts +44 -0
- package/dist/gate/gate-config.d.ts.map +1 -0
- package/dist/gate/gate-config.js +334 -0
- package/dist/gate/gate-runner.d.ts +160 -0
- package/dist/gate/gate-runner.d.ts.map +1 -0
- package/dist/gate/gate-runner.js +531 -0
- package/dist/gate/index.d.ts +20 -0
- package/dist/gate/index.d.ts.map +1 -0
- package/dist/gate/index.js +14 -0
- package/dist/gate/parsers/command-parser.d.ts +18 -0
- package/dist/gate/parsers/command-parser.d.ts.map +1 -0
- package/dist/gate/parsers/command-parser.js +363 -0
- package/dist/gate/parsers/index.d.ts +2 -0
- package/dist/gate/parsers/index.d.ts.map +1 -0
- package/dist/gate/parsers/index.js +1 -0
- package/dist/gate/policy/index.d.ts +12 -0
- package/dist/gate/policy/index.d.ts.map +1 -0
- package/dist/gate/policy/index.js +10 -0
- package/dist/gate/rules/default-filesystem-rules.d.ts +3 -0
- package/dist/gate/rules/default-filesystem-rules.d.ts.map +1 -0
- package/dist/gate/rules/default-filesystem-rules.js +201 -0
- package/dist/gate/rules/default-git-rules.d.ts +3 -0
- package/dist/gate/rules/default-git-rules.d.ts.map +1 -0
- package/dist/gate/rules/default-git-rules.js +192 -0
- package/dist/gate/rules/index.d.ts +5 -0
- package/dist/gate/rules/index.d.ts.map +1 -0
- package/dist/gate/rules/index.js +3 -0
- package/dist/gate/rules/rule-matcher.d.ts +27 -0
- package/dist/gate/rules/rule-matcher.d.ts.map +1 -0
- package/dist/gate/rules/rule-matcher.js +228 -0
- package/dist/gate/rules/types.d.ts +250 -0
- package/dist/gate/rules/types.d.ts.map +1 -0
- package/dist/gate/rules/types.js +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/types/gate.types.d.ts +42 -0
- package/dist/types/gate.types.d.ts.map +1 -0
- package/dist/types/gate.types.js +94 -0
- package/dist/watch/debouncer.d.ts +90 -0
- package/dist/watch/debouncer.d.ts.map +1 -0
- package/dist/watch/debouncer.js +135 -0
- package/dist/watch/file-watcher.d.ts +73 -0
- package/dist/watch/file-watcher.d.ts.map +1 -0
- package/dist/watch/file-watcher.js +121 -0
- package/dist/watch/git-status.d.ts +98 -0
- package/dist/watch/git-status.d.ts.map +1 -0
- package/dist/watch/git-status.js +266 -0
- package/dist/watch/index.d.ts +16 -0
- package/dist/watch/index.d.ts.map +1 -0
- package/dist/watch/index.js +15 -0
- package/dist/watch/orchestrator.d.ts +113 -0
- package/dist/watch/orchestrator.d.ts.map +1 -0
- package/dist/watch/orchestrator.js +409 -0
- package/dist/watch/types.d.ts +190 -0
- package/dist/watch/types.d.ts.map +1 -0
- package/dist/watch/types.js +76 -0
- package/package.json +60 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lock Manager
|
|
3
|
+
*
|
|
4
|
+
* Provides distributed locking for multi-agent coordination using file-based locks.
|
|
5
|
+
* Supports automatic stale lock detection and recovery.
|
|
6
|
+
*/
|
|
7
|
+
import { type LockRecord, type LockType, type LockAcquisitionResult, type LockReleaseResult, type AgentInfo, type ConcurrencyConfig } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Options for LockManager
|
|
10
|
+
*/
|
|
11
|
+
export interface LockManagerOptions {
|
|
12
|
+
/** Workspace root directory */
|
|
13
|
+
workspaceRoot: string;
|
|
14
|
+
/** Concurrency configuration */
|
|
15
|
+
config?: Partial<ConcurrencyConfig>;
|
|
16
|
+
/** Agent info for this lock manager */
|
|
17
|
+
agentInfo?: AgentInfo;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Options for lock acquisition
|
|
21
|
+
*/
|
|
22
|
+
export interface AcquireLockOptions {
|
|
23
|
+
/** Lock type */
|
|
24
|
+
type: LockType;
|
|
25
|
+
/** Resource being locked */
|
|
26
|
+
resource: string;
|
|
27
|
+
/** Reason for lock */
|
|
28
|
+
reason?: string;
|
|
29
|
+
/** Custom timeout (overrides config) */
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
/** Wait for lock (block until acquired or timeout) */
|
|
32
|
+
wait?: boolean;
|
|
33
|
+
/** Wait timeout in ms (default: 30s) */
|
|
34
|
+
waitTimeoutMs?: number;
|
|
35
|
+
/** Retry interval in ms (default: 100ms) */
|
|
36
|
+
retryIntervalMs?: number;
|
|
37
|
+
/** Allow acquiring from stale agents */
|
|
38
|
+
acquireFromStale?: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Lock Manager
|
|
42
|
+
*
|
|
43
|
+
* Manages distributed file-based locks for multi-agent coordination.
|
|
44
|
+
*/
|
|
45
|
+
export declare class LockManager {
|
|
46
|
+
private readonly workspaceRoot;
|
|
47
|
+
private readonly config;
|
|
48
|
+
private readonly agent;
|
|
49
|
+
private readonly lockDir;
|
|
50
|
+
private readonly heldLocks;
|
|
51
|
+
private readonly renewalTimers;
|
|
52
|
+
constructor(options: LockManagerOptions);
|
|
53
|
+
/**
|
|
54
|
+
* Get the agent ID
|
|
55
|
+
*/
|
|
56
|
+
getAgentId(): string;
|
|
57
|
+
/**
|
|
58
|
+
* Acquire a lock
|
|
59
|
+
*/
|
|
60
|
+
acquire(options: AcquireLockOptions): Promise<LockAcquisitionResult>;
|
|
61
|
+
/**
|
|
62
|
+
* Acquire lock with wait/retry
|
|
63
|
+
*/
|
|
64
|
+
private acquireWithWait;
|
|
65
|
+
/**
|
|
66
|
+
* Release a lock
|
|
67
|
+
*/
|
|
68
|
+
release(type: LockType, resource: string): Promise<LockReleaseResult>;
|
|
69
|
+
/**
|
|
70
|
+
* Release all locks held by this manager
|
|
71
|
+
*/
|
|
72
|
+
releaseAll(): Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* Check if a lock is held
|
|
75
|
+
*/
|
|
76
|
+
isLocked(type: LockType, resource: string): Promise<boolean>;
|
|
77
|
+
/**
|
|
78
|
+
* Get lock info
|
|
79
|
+
*/
|
|
80
|
+
getLockInfo(type: LockType, resource: string): Promise<LockRecord | null>;
|
|
81
|
+
/**
|
|
82
|
+
* Check if we hold a lock
|
|
83
|
+
*/
|
|
84
|
+
holdsLock(type: LockType, resource: string): boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Get all locks held by this manager
|
|
87
|
+
*/
|
|
88
|
+
getHeldLocks(): LockRecord[];
|
|
89
|
+
/**
|
|
90
|
+
* Start auto-renewal for a lock
|
|
91
|
+
*/
|
|
92
|
+
startAutoRenewal(type: LockType, resource: string, intervalMs?: number): void;
|
|
93
|
+
/**
|
|
94
|
+
* Stop auto-renewal for a lock
|
|
95
|
+
*/
|
|
96
|
+
stopRenewal(lockKey: string): void;
|
|
97
|
+
/**
|
|
98
|
+
* Stop all auto-renewals
|
|
99
|
+
*/
|
|
100
|
+
stopAllRenewals(): void;
|
|
101
|
+
/**
|
|
102
|
+
* Cleanup expired locks in the lock directory
|
|
103
|
+
*/
|
|
104
|
+
cleanupExpiredLocks(): Promise<number>;
|
|
105
|
+
/**
|
|
106
|
+
* Create a new lock
|
|
107
|
+
*/
|
|
108
|
+
private createLock;
|
|
109
|
+
/**
|
|
110
|
+
* Renew an existing lock
|
|
111
|
+
*/
|
|
112
|
+
private renewLock;
|
|
113
|
+
/**
|
|
114
|
+
* Force acquire a lock (taking over from expired/stale holder)
|
|
115
|
+
*/
|
|
116
|
+
private forceAcquire;
|
|
117
|
+
/**
|
|
118
|
+
* Check if lock holder is stale
|
|
119
|
+
*/
|
|
120
|
+
private isHolderStale;
|
|
121
|
+
/**
|
|
122
|
+
* Read lock file
|
|
123
|
+
*/
|
|
124
|
+
private readLock;
|
|
125
|
+
/**
|
|
126
|
+
* Get lock key from type and resource
|
|
127
|
+
*/
|
|
128
|
+
private getLockKey;
|
|
129
|
+
/**
|
|
130
|
+
* Get lock file path
|
|
131
|
+
*/
|
|
132
|
+
private getLockPath;
|
|
133
|
+
/**
|
|
134
|
+
* Ensure lock directory exists
|
|
135
|
+
*/
|
|
136
|
+
private ensureLockDir;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Create a lock manager
|
|
140
|
+
*/
|
|
141
|
+
export declare function createLockManager(options: LockManagerOptions): LockManager;
|
|
142
|
+
/**
|
|
143
|
+
* Execute a function while holding a lock
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```typescript
|
|
147
|
+
* const result = await withLock(
|
|
148
|
+
* lockManager,
|
|
149
|
+
* { type: 'action', resource: 'gate' },
|
|
150
|
+
* async () => {
|
|
151
|
+
* // Perform work that requires exclusive access
|
|
152
|
+
* return await runGates();
|
|
153
|
+
* }
|
|
154
|
+
* );
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
export declare function withLock<T>(manager: LockManager, options: Omit<AcquireLockOptions, 'wait'>, fn: () => Promise<T>): Promise<T>;
|
|
158
|
+
/**
|
|
159
|
+
* Try to execute a function while holding a lock (non-blocking)
|
|
160
|
+
*
|
|
161
|
+
* Returns null if lock couldn't be acquired.
|
|
162
|
+
*/
|
|
163
|
+
export declare function tryWithLock<T>(manager: LockManager, options: Omit<AcquireLockOptions, 'wait'>, fn: () => Promise<T>): Promise<{
|
|
164
|
+
success: true;
|
|
165
|
+
result: T;
|
|
166
|
+
} | {
|
|
167
|
+
success: false;
|
|
168
|
+
heldBy?: LockAcquisitionResult['heldBy'];
|
|
169
|
+
}>;
|
|
170
|
+
//# sourceMappingURL=lock-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock-manager.d.ts","sourceRoot":"","sources":["../../src/concurrency/lock-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lock Manager
|
|
3
|
+
*
|
|
4
|
+
* Provides distributed locking for multi-agent coordination using file-based locks.
|
|
5
|
+
* Supports automatic stale lock detection and recovery.
|
|
6
|
+
*/
|
|
7
|
+
import { promises as fs } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
import { LockFileSchema, getDefaultConcurrencyConfig, } from './types.js';
|
|
11
|
+
import { atomicWriteJson, readJsonSafe, unlinkSafe, sleepWithJitter, tryAcquireFileLock, } from './atomic.js';
|
|
12
|
+
import { createAgentInfo } from './agent.js';
|
|
13
|
+
import { createDebugger } from '@eddacraft/anvil-core';
|
|
14
|
+
const debug = createDebugger('lock');
|
|
15
|
+
/**
|
|
16
|
+
* Lock Manager
|
|
17
|
+
*
|
|
18
|
+
* Manages distributed file-based locks for multi-agent coordination.
|
|
19
|
+
*/
|
|
20
|
+
export class LockManager {
|
|
21
|
+
workspaceRoot;
|
|
22
|
+
config;
|
|
23
|
+
agent;
|
|
24
|
+
lockDir;
|
|
25
|
+
// Track locks held by this manager instance
|
|
26
|
+
heldLocks = new Map();
|
|
27
|
+
// Renewal timers
|
|
28
|
+
renewalTimers = new Map();
|
|
29
|
+
constructor(options) {
|
|
30
|
+
this.workspaceRoot = options.workspaceRoot;
|
|
31
|
+
this.config = {
|
|
32
|
+
...getDefaultConcurrencyConfig(),
|
|
33
|
+
...options.config,
|
|
34
|
+
};
|
|
35
|
+
this.agent = options.agentInfo ?? createAgentInfo();
|
|
36
|
+
this.lockDir = join(this.workspaceRoot, this.config.lockDir);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Get the agent ID
|
|
40
|
+
*/
|
|
41
|
+
getAgentId() {
|
|
42
|
+
return this.agent.id;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Acquire a lock
|
|
46
|
+
*/
|
|
47
|
+
async acquire(options) {
|
|
48
|
+
const { type, resource, reason, timeoutMs = this.config.lockTimeoutMs, wait = false, waitTimeoutMs = 30000, retryIntervalMs = 100, acquireFromStale = this.config.autoAcquireFromStale, } = options;
|
|
49
|
+
const lockKey = this.getLockKey(type, resource);
|
|
50
|
+
const lockPath = this.getLockPath(lockKey);
|
|
51
|
+
// If waiting, use retry loop
|
|
52
|
+
if (wait) {
|
|
53
|
+
return this.acquireWithWait(options, waitTimeoutMs, retryIntervalMs);
|
|
54
|
+
}
|
|
55
|
+
await this.ensureLockDir();
|
|
56
|
+
// Check for existing lock
|
|
57
|
+
const existingLock = await this.readLock(lockPath);
|
|
58
|
+
if (existingLock) {
|
|
59
|
+
// Check if it's our lock
|
|
60
|
+
if (existingLock.lock.agentId === this.agent.id) {
|
|
61
|
+
// Renew our existing lock
|
|
62
|
+
return this.renewLock(lockPath, existingLock, reason);
|
|
63
|
+
}
|
|
64
|
+
// Check if lock is expired
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
const expiresAt = new Date(existingLock.lock.expiresAt).getTime();
|
|
67
|
+
if (now > expiresAt) {
|
|
68
|
+
debug(`Lock expired, taking over: ${lockKey}`);
|
|
69
|
+
return this.forceAcquire(lockPath, type, resource, reason, timeoutMs, existingLock);
|
|
70
|
+
}
|
|
71
|
+
// Check if holder is stale
|
|
72
|
+
if (acquireFromStale && (await this.isHolderStale(existingLock.lock))) {
|
|
73
|
+
debug(`Lock holder is stale, taking over: ${lockKey}`);
|
|
74
|
+
return this.forceAcquire(lockPath, type, resource, reason, timeoutMs, existingLock);
|
|
75
|
+
}
|
|
76
|
+
// Lock is held by another active agent
|
|
77
|
+
return {
|
|
78
|
+
acquired: false,
|
|
79
|
+
error: `Lock held by ${existingLock.lock.agentId}`,
|
|
80
|
+
heldBy: {
|
|
81
|
+
agentId: existingLock.lock.agentId,
|
|
82
|
+
agentType: existingLock.lock.agentType,
|
|
83
|
+
acquiredAt: existingLock.lock.acquiredAt,
|
|
84
|
+
expiresAt: existingLock.lock.expiresAt,
|
|
85
|
+
pid: existingLock.lock.pid,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
// No existing lock, acquire it
|
|
90
|
+
return this.createLock(lockPath, type, resource, reason, timeoutMs);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Acquire lock with wait/retry
|
|
94
|
+
*/
|
|
95
|
+
async acquireWithWait(options, waitTimeoutMs, retryIntervalMs) {
|
|
96
|
+
const startTime = Date.now();
|
|
97
|
+
while (Date.now() - startTime < waitTimeoutMs) {
|
|
98
|
+
const result = await this.acquire({ ...options, wait: false });
|
|
99
|
+
if (result.acquired) {
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
// Wait with jitter before retrying
|
|
103
|
+
await sleepWithJitter(retryIntervalMs);
|
|
104
|
+
}
|
|
105
|
+
// Timeout
|
|
106
|
+
const lockKey = this.getLockKey(options.type, options.resource);
|
|
107
|
+
const existingLock = await this.readLock(this.getLockPath(lockKey));
|
|
108
|
+
return {
|
|
109
|
+
acquired: false,
|
|
110
|
+
error: `Lock acquisition timed out after ${waitTimeoutMs}ms`,
|
|
111
|
+
heldBy: existingLock
|
|
112
|
+
? {
|
|
113
|
+
agentId: existingLock.lock.agentId,
|
|
114
|
+
agentType: existingLock.lock.agentType,
|
|
115
|
+
acquiredAt: existingLock.lock.acquiredAt,
|
|
116
|
+
expiresAt: existingLock.lock.expiresAt,
|
|
117
|
+
pid: existingLock.lock.pid,
|
|
118
|
+
}
|
|
119
|
+
: undefined,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Release a lock
|
|
124
|
+
*/
|
|
125
|
+
async release(type, resource) {
|
|
126
|
+
const lockKey = this.getLockKey(type, resource);
|
|
127
|
+
const lockPath = this.getLockPath(lockKey);
|
|
128
|
+
// Stop renewal timer
|
|
129
|
+
this.stopRenewal(lockKey);
|
|
130
|
+
const existingLock = await this.readLock(lockPath);
|
|
131
|
+
if (!existingLock) {
|
|
132
|
+
this.heldLocks.delete(lockKey);
|
|
133
|
+
return { released: true };
|
|
134
|
+
}
|
|
135
|
+
// Check if we own the lock
|
|
136
|
+
if (existingLock.lock.agentId !== this.agent.id) {
|
|
137
|
+
return {
|
|
138
|
+
released: false,
|
|
139
|
+
error: `Lock held by different agent: ${existingLock.lock.agentId}`,
|
|
140
|
+
wasHeldByOther: true,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
// Delete the lock file
|
|
144
|
+
await unlinkSafe(lockPath);
|
|
145
|
+
this.heldLocks.delete(lockKey);
|
|
146
|
+
debug(`Lock released: ${lockKey}`);
|
|
147
|
+
return { released: true };
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Release all locks held by this manager
|
|
151
|
+
*/
|
|
152
|
+
async releaseAll() {
|
|
153
|
+
for (const [, lock] of this.heldLocks) {
|
|
154
|
+
await this.release(lock.type, lock.resource);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Check if a lock is held
|
|
159
|
+
*/
|
|
160
|
+
async isLocked(type, resource) {
|
|
161
|
+
const lockKey = this.getLockKey(type, resource);
|
|
162
|
+
const lockPath = this.getLockPath(lockKey);
|
|
163
|
+
const existingLock = await this.readLock(lockPath);
|
|
164
|
+
if (!existingLock) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
// Check expiration
|
|
168
|
+
const now = Date.now();
|
|
169
|
+
const expiresAt = new Date(existingLock.lock.expiresAt).getTime();
|
|
170
|
+
return now <= expiresAt;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Get lock info
|
|
174
|
+
*/
|
|
175
|
+
async getLockInfo(type, resource) {
|
|
176
|
+
const lockKey = this.getLockKey(type, resource);
|
|
177
|
+
const lockPath = this.getLockPath(lockKey);
|
|
178
|
+
const existingLock = await this.readLock(lockPath);
|
|
179
|
+
return existingLock?.lock ?? null;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Check if we hold a lock
|
|
183
|
+
*/
|
|
184
|
+
holdsLock(type, resource) {
|
|
185
|
+
const lockKey = this.getLockKey(type, resource);
|
|
186
|
+
return this.heldLocks.has(lockKey);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Get all locks held by this manager
|
|
190
|
+
*/
|
|
191
|
+
getHeldLocks() {
|
|
192
|
+
return Array.from(this.heldLocks.values());
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Start auto-renewal for a lock
|
|
196
|
+
*/
|
|
197
|
+
startAutoRenewal(type, resource, intervalMs) {
|
|
198
|
+
const lockKey = this.getLockKey(type, resource);
|
|
199
|
+
const interval = intervalMs ?? Math.floor(this.config.lockTimeoutMs / 3);
|
|
200
|
+
if (this.renewalTimers.has(lockKey)) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const timer = setInterval(async () => {
|
|
204
|
+
try {
|
|
205
|
+
const lockPath = this.getLockPath(lockKey);
|
|
206
|
+
const existingLock = await this.readLock(lockPath);
|
|
207
|
+
if (existingLock && existingLock.lock.agentId === this.agent.id) {
|
|
208
|
+
await this.renewLock(lockPath, existingLock);
|
|
209
|
+
debug(`Lock auto-renewed: ${lockKey}`);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
// We lost the lock
|
|
213
|
+
this.stopRenewal(lockKey);
|
|
214
|
+
this.heldLocks.delete(lockKey);
|
|
215
|
+
debug(`Lost lock, stopping renewal: ${lockKey}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
debug(`Auto-renewal failed for ${lockKey}:`, error);
|
|
220
|
+
}
|
|
221
|
+
}, interval);
|
|
222
|
+
// Don't block process exit
|
|
223
|
+
timer.unref();
|
|
224
|
+
this.renewalTimers.set(lockKey, timer);
|
|
225
|
+
debug(`Auto-renewal started for ${lockKey} (interval=${interval}ms)`);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Stop auto-renewal for a lock
|
|
229
|
+
*/
|
|
230
|
+
stopRenewal(lockKey) {
|
|
231
|
+
const timer = this.renewalTimers.get(lockKey);
|
|
232
|
+
if (timer) {
|
|
233
|
+
clearInterval(timer);
|
|
234
|
+
this.renewalTimers.delete(lockKey);
|
|
235
|
+
debug(`Auto-renewal stopped for ${lockKey}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Stop all auto-renewals
|
|
240
|
+
*/
|
|
241
|
+
stopAllRenewals() {
|
|
242
|
+
for (const [lockKey, timer] of this.renewalTimers) {
|
|
243
|
+
clearInterval(timer);
|
|
244
|
+
debug(`Auto-renewal stopped for ${lockKey}`);
|
|
245
|
+
}
|
|
246
|
+
this.renewalTimers.clear();
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Cleanup expired locks in the lock directory
|
|
250
|
+
*/
|
|
251
|
+
async cleanupExpiredLocks() {
|
|
252
|
+
let cleaned = 0;
|
|
253
|
+
try {
|
|
254
|
+
const files = await fs.readdir(this.lockDir);
|
|
255
|
+
for (const file of files) {
|
|
256
|
+
if (!file.endsWith('.lock'))
|
|
257
|
+
continue;
|
|
258
|
+
const lockPath = join(this.lockDir, file);
|
|
259
|
+
const lock = await this.readLock(lockPath);
|
|
260
|
+
if (!lock)
|
|
261
|
+
continue;
|
|
262
|
+
const now = Date.now();
|
|
263
|
+
const expiresAt = new Date(lock.lock.expiresAt).getTime();
|
|
264
|
+
if (now > expiresAt) {
|
|
265
|
+
await unlinkSafe(lockPath);
|
|
266
|
+
cleaned++;
|
|
267
|
+
debug(`Cleaned up expired lock: ${file}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
if (error.code !== 'ENOENT') {
|
|
273
|
+
debug('Cleanup failed:', error);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return cleaned;
|
|
277
|
+
}
|
|
278
|
+
// ============================================================================
|
|
279
|
+
// Private Methods
|
|
280
|
+
// ============================================================================
|
|
281
|
+
/**
|
|
282
|
+
* Create a new lock
|
|
283
|
+
*/
|
|
284
|
+
async createLock(lockPath, type, resource, reason, timeoutMs) {
|
|
285
|
+
// Use O_EXCL sentinel to prevent two agents both creating the lock
|
|
286
|
+
const sentinelPath = `${lockPath}.creating`;
|
|
287
|
+
const sentinel = await tryAcquireFileLock(sentinelPath, this.agent.id);
|
|
288
|
+
if (!sentinel) {
|
|
289
|
+
// Another agent is creating the lock right now — read what they wrote
|
|
290
|
+
const existingLock = await this.readLock(lockPath);
|
|
291
|
+
if (existingLock) {
|
|
292
|
+
return {
|
|
293
|
+
acquired: false,
|
|
294
|
+
error: `Lock acquired by another agent: ${existingLock.lock.agentId}`,
|
|
295
|
+
heldBy: {
|
|
296
|
+
agentId: existingLock.lock.agentId,
|
|
297
|
+
agentType: existingLock.lock.agentType,
|
|
298
|
+
acquiredAt: existingLock.lock.acquiredAt,
|
|
299
|
+
expiresAt: existingLock.lock.expiresAt,
|
|
300
|
+
pid: existingLock.lock.pid,
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
acquired: false,
|
|
306
|
+
error: 'Lock creation in progress by another agent',
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
// Re-check after acquiring sentinel (another agent may have finished first)
|
|
311
|
+
const raceCheck = await this.readLock(lockPath);
|
|
312
|
+
if (raceCheck && raceCheck.lock.agentId !== this.agent.id) {
|
|
313
|
+
return {
|
|
314
|
+
acquired: false,
|
|
315
|
+
error: `Lock acquired by another agent: ${raceCheck.lock.agentId}`,
|
|
316
|
+
heldBy: {
|
|
317
|
+
agentId: raceCheck.lock.agentId,
|
|
318
|
+
agentType: raceCheck.lock.agentType,
|
|
319
|
+
acquiredAt: raceCheck.lock.acquiredAt,
|
|
320
|
+
expiresAt: raceCheck.lock.expiresAt,
|
|
321
|
+
pid: raceCheck.lock.pid,
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
const now = new Date();
|
|
326
|
+
const expiresAt = new Date(now.getTime() + timeoutMs);
|
|
327
|
+
const lock = {
|
|
328
|
+
type,
|
|
329
|
+
resource,
|
|
330
|
+
agentId: this.agent.id,
|
|
331
|
+
agentType: this.agent.type,
|
|
332
|
+
pid: this.agent.pid,
|
|
333
|
+
acquiredAt: now.toISOString(),
|
|
334
|
+
expiresAt: expiresAt.toISOString(),
|
|
335
|
+
reason,
|
|
336
|
+
renewCount: 0,
|
|
337
|
+
};
|
|
338
|
+
const lockFile = {
|
|
339
|
+
version: '1.0.0',
|
|
340
|
+
lock,
|
|
341
|
+
history: [],
|
|
342
|
+
};
|
|
343
|
+
await atomicWriteJson(lockPath, lockFile);
|
|
344
|
+
const lockKey = this.getLockKey(type, resource);
|
|
345
|
+
this.heldLocks.set(lockKey, lock);
|
|
346
|
+
debug(`Lock acquired: ${lockKey}`);
|
|
347
|
+
return {
|
|
348
|
+
acquired: true,
|
|
349
|
+
lock,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
finally {
|
|
353
|
+
await sentinel.release();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Renew an existing lock
|
|
358
|
+
*/
|
|
359
|
+
async renewLock(lockPath, existingLock, reason) {
|
|
360
|
+
const now = new Date();
|
|
361
|
+
const expiresAt = new Date(now.getTime() + this.config.lockTimeoutMs);
|
|
362
|
+
const lock = {
|
|
363
|
+
...existingLock.lock,
|
|
364
|
+
expiresAt: expiresAt.toISOString(),
|
|
365
|
+
renewCount: existingLock.lock.renewCount + 1,
|
|
366
|
+
reason: reason ?? existingLock.lock.reason,
|
|
367
|
+
};
|
|
368
|
+
const lockFile = {
|
|
369
|
+
...existingLock,
|
|
370
|
+
lock,
|
|
371
|
+
};
|
|
372
|
+
await atomicWriteJson(lockPath, lockFile);
|
|
373
|
+
const lockKey = this.getLockKey(lock.type, lock.resource);
|
|
374
|
+
this.heldLocks.set(lockKey, lock);
|
|
375
|
+
debug(`Lock renewed: ${lockKey} (count=${lock.renewCount})`);
|
|
376
|
+
return {
|
|
377
|
+
acquired: true,
|
|
378
|
+
lock,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Force acquire a lock (taking over from expired/stale holder)
|
|
383
|
+
*/
|
|
384
|
+
async forceAcquire(lockPath, type, resource, reason, timeoutMs, existingLock) {
|
|
385
|
+
const now = new Date();
|
|
386
|
+
const expiresAt = new Date(now.getTime() + timeoutMs);
|
|
387
|
+
const lock = {
|
|
388
|
+
type,
|
|
389
|
+
resource,
|
|
390
|
+
agentId: this.agent.id,
|
|
391
|
+
agentType: this.agent.type,
|
|
392
|
+
pid: this.agent.pid,
|
|
393
|
+
acquiredAt: now.toISOString(),
|
|
394
|
+
expiresAt: expiresAt.toISOString(),
|
|
395
|
+
reason,
|
|
396
|
+
renewCount: 0,
|
|
397
|
+
};
|
|
398
|
+
const lockFile = {
|
|
399
|
+
version: '1.0.0',
|
|
400
|
+
lock,
|
|
401
|
+
history: [
|
|
402
|
+
...existingLock.history,
|
|
403
|
+
{
|
|
404
|
+
agentId: existingLock.lock.agentId,
|
|
405
|
+
acquiredAt: existingLock.lock.acquiredAt,
|
|
406
|
+
releasedAt: now.toISOString(),
|
|
407
|
+
reason: 'Force-released (expired/stale)',
|
|
408
|
+
},
|
|
409
|
+
],
|
|
410
|
+
};
|
|
411
|
+
await atomicWriteJson(lockPath, lockFile);
|
|
412
|
+
const lockKey = this.getLockKey(type, resource);
|
|
413
|
+
this.heldLocks.set(lockKey, lock);
|
|
414
|
+
debug(`Lock force-acquired: ${lockKey}`);
|
|
415
|
+
return {
|
|
416
|
+
acquired: true,
|
|
417
|
+
lock,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Check if lock holder is stale
|
|
422
|
+
*/
|
|
423
|
+
async isHolderStale(lock) {
|
|
424
|
+
// Check if process is still running
|
|
425
|
+
if (lock.pid) {
|
|
426
|
+
try {
|
|
427
|
+
process.kill(lock.pid, 0);
|
|
428
|
+
return false; // Process exists
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
return true; // Process doesn't exist
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
// Can't determine - assume not stale
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Read lock file
|
|
439
|
+
*/
|
|
440
|
+
async readLock(lockPath) {
|
|
441
|
+
const data = await readJsonSafe(lockPath);
|
|
442
|
+
if (!data)
|
|
443
|
+
return null;
|
|
444
|
+
const result = LockFileSchema.safeParse(data);
|
|
445
|
+
if (!result.success) {
|
|
446
|
+
debug('Invalid lock file schema:', result.error);
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
return result.data;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Get lock key from type and resource
|
|
453
|
+
*/
|
|
454
|
+
getLockKey(type, resource) {
|
|
455
|
+
return `${type}:${resource}`;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Get lock file path
|
|
459
|
+
*/
|
|
460
|
+
getLockPath(lockKey) {
|
|
461
|
+
// Hash the key for safe filename
|
|
462
|
+
const hash = createHash('sha256').update(lockKey).digest('hex').slice(0, 16);
|
|
463
|
+
return join(this.lockDir, `${hash}.lock`);
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Ensure lock directory exists
|
|
467
|
+
*/
|
|
468
|
+
async ensureLockDir() {
|
|
469
|
+
await fs.mkdir(this.lockDir, { recursive: true });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Create a lock manager
|
|
474
|
+
*/
|
|
475
|
+
export function createLockManager(options) {
|
|
476
|
+
return new LockManager(options);
|
|
477
|
+
}
|
|
478
|
+
// ============================================================================
|
|
479
|
+
// Convenience Functions
|
|
480
|
+
// ============================================================================
|
|
481
|
+
/**
|
|
482
|
+
* Execute a function while holding a lock
|
|
483
|
+
*
|
|
484
|
+
* @example
|
|
485
|
+
* ```typescript
|
|
486
|
+
* const result = await withLock(
|
|
487
|
+
* lockManager,
|
|
488
|
+
* { type: 'action', resource: 'gate' },
|
|
489
|
+
* async () => {
|
|
490
|
+
* // Perform work that requires exclusive access
|
|
491
|
+
* return await runGates();
|
|
492
|
+
* }
|
|
493
|
+
* );
|
|
494
|
+
* ```
|
|
495
|
+
*/
|
|
496
|
+
export async function withLock(manager, options, fn) {
|
|
497
|
+
const result = await manager.acquire({ ...options, wait: true });
|
|
498
|
+
if (!result.acquired) {
|
|
499
|
+
throw new Error(`Failed to acquire lock: ${result.error}`);
|
|
500
|
+
}
|
|
501
|
+
try {
|
|
502
|
+
return await fn();
|
|
503
|
+
}
|
|
504
|
+
finally {
|
|
505
|
+
await manager.release(options.type, options.resource);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Try to execute a function while holding a lock (non-blocking)
|
|
510
|
+
*
|
|
511
|
+
* Returns null if lock couldn't be acquired.
|
|
512
|
+
*/
|
|
513
|
+
export async function tryWithLock(manager, options, fn) {
|
|
514
|
+
const result = await manager.acquire({ ...options, wait: false });
|
|
515
|
+
if (!result.acquired) {
|
|
516
|
+
return { success: false, heldBy: result.heldBy };
|
|
517
|
+
}
|
|
518
|
+
try {
|
|
519
|
+
const fnResult = await fn();
|
|
520
|
+
return { success: true, result: fnResult };
|
|
521
|
+
}
|
|
522
|
+
finally {
|
|
523
|
+
await manager.release(options.type, options.resource);
|
|
524
|
+
}
|
|
525
|
+
}
|