@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,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue Manager
|
|
3
|
+
*
|
|
4
|
+
* Provides fair queuing for resources when multiple agents are waiting
|
|
5
|
+
* for the same lock. Supports priority-based scheduling and timeout handling.
|
|
6
|
+
*/
|
|
7
|
+
import { type QueueJoinResult, type QueueStatusResult, type LockType, type AgentInfo, type ConcurrencyConfig, type LockAcquisitionResult } from './types.js';
|
|
8
|
+
import { LockManager } from './lock-manager.js';
|
|
9
|
+
/**
|
|
10
|
+
* Options for QueueManager
|
|
11
|
+
*/
|
|
12
|
+
export interface QueueManagerOptions {
|
|
13
|
+
/** Workspace root directory */
|
|
14
|
+
workspaceRoot: string;
|
|
15
|
+
/** Concurrency configuration */
|
|
16
|
+
config?: Partial<ConcurrencyConfig>;
|
|
17
|
+
/** Agent info */
|
|
18
|
+
agentInfo?: AgentInfo;
|
|
19
|
+
/** Lock manager (optional, created if not provided) */
|
|
20
|
+
lockManager?: LockManager;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Options for joining a queue
|
|
24
|
+
*/
|
|
25
|
+
export interface QueueJoinOptions {
|
|
26
|
+
/** Lock type */
|
|
27
|
+
type: LockType;
|
|
28
|
+
/** Resource identifier */
|
|
29
|
+
resource: string;
|
|
30
|
+
/** Priority (lower = higher priority, default: 100) */
|
|
31
|
+
priority?: number;
|
|
32
|
+
/** Reason for waiting */
|
|
33
|
+
reason?: string;
|
|
34
|
+
/** Custom timeout (overrides config) */
|
|
35
|
+
timeoutMs?: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Options for waiting in queue
|
|
39
|
+
*/
|
|
40
|
+
export interface QueueWaitOptions extends QueueJoinOptions {
|
|
41
|
+
/** Maximum time to wait in queue (ms) */
|
|
42
|
+
maxWaitMs?: number;
|
|
43
|
+
/** Polling interval (ms) */
|
|
44
|
+
pollIntervalMs?: number;
|
|
45
|
+
/** Callback when position changes */
|
|
46
|
+
onPositionChange?: (position: number, total: number) => void;
|
|
47
|
+
/** Callback when lock is acquired */
|
|
48
|
+
onLockAcquired?: () => void;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Queue Manager
|
|
52
|
+
*
|
|
53
|
+
* Manages fair queuing for resources across multiple agents.
|
|
54
|
+
*/
|
|
55
|
+
export declare class QueueManager {
|
|
56
|
+
private readonly workspaceRoot;
|
|
57
|
+
private readonly config;
|
|
58
|
+
private readonly agent;
|
|
59
|
+
private readonly queueDir;
|
|
60
|
+
private readonly lockManager;
|
|
61
|
+
constructor(options: QueueManagerOptions);
|
|
62
|
+
/**
|
|
63
|
+
* Get the agent ID
|
|
64
|
+
*/
|
|
65
|
+
getAgentId(): string;
|
|
66
|
+
/**
|
|
67
|
+
* Join a queue for a resource
|
|
68
|
+
*/
|
|
69
|
+
join(options: QueueJoinOptions): Promise<QueueJoinResult>;
|
|
70
|
+
/**
|
|
71
|
+
* Leave a queue
|
|
72
|
+
*/
|
|
73
|
+
leave(type: LockType, resource: string): Promise<boolean>;
|
|
74
|
+
/**
|
|
75
|
+
* Get queue status
|
|
76
|
+
*/
|
|
77
|
+
getStatus(type: LockType, resource: string): Promise<QueueStatusResult>;
|
|
78
|
+
/**
|
|
79
|
+
* Check if it's our turn (we're first in queue)
|
|
80
|
+
*/
|
|
81
|
+
isOurTurn(type: LockType, resource: string): Promise<boolean>;
|
|
82
|
+
/**
|
|
83
|
+
* Wait in queue until lock is acquired
|
|
84
|
+
*
|
|
85
|
+
* This is the main coordination primitive - joins queue, waits for turn,
|
|
86
|
+
* then acquires lock.
|
|
87
|
+
*/
|
|
88
|
+
waitForLock(options: QueueWaitOptions): Promise<LockAcquisitionResult>;
|
|
89
|
+
/**
|
|
90
|
+
* Get all queues in the workspace
|
|
91
|
+
*/
|
|
92
|
+
getAllQueues(): Promise<Array<{
|
|
93
|
+
type: LockType;
|
|
94
|
+
resource: string;
|
|
95
|
+
entries: number;
|
|
96
|
+
}>>;
|
|
97
|
+
/**
|
|
98
|
+
* Cleanup all timed-out entries across all queues
|
|
99
|
+
*/
|
|
100
|
+
cleanupAllTimedOut(): Promise<number>;
|
|
101
|
+
/**
|
|
102
|
+
* Load queue from file
|
|
103
|
+
*/
|
|
104
|
+
private loadQueue;
|
|
105
|
+
/**
|
|
106
|
+
* Sort queue entries by priority then by queued time
|
|
107
|
+
*/
|
|
108
|
+
private sortQueue;
|
|
109
|
+
/**
|
|
110
|
+
* Cleanup timed-out entries
|
|
111
|
+
*/
|
|
112
|
+
private cleanupTimedOut;
|
|
113
|
+
/**
|
|
114
|
+
* Get queue file path
|
|
115
|
+
*/
|
|
116
|
+
private getQueuePath;
|
|
117
|
+
/**
|
|
118
|
+
* Ensure queue directory exists
|
|
119
|
+
*/
|
|
120
|
+
private ensureQueueDir;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Create a queue manager
|
|
124
|
+
*/
|
|
125
|
+
export declare function createQueueManager(options: QueueManagerOptions): QueueManager;
|
|
126
|
+
/**
|
|
127
|
+
* Coordinated execution with fair queuing
|
|
128
|
+
*
|
|
129
|
+
* This is the primary way to execute code that requires exclusive access
|
|
130
|
+
* to a resource in a multi-agent environment.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* const result = await coordinatedExecution(
|
|
135
|
+
* queueManager,
|
|
136
|
+
* { type: 'action', resource: 'gate', reason: 'Running quality gates' },
|
|
137
|
+
* async () => {
|
|
138
|
+
* return await runGates();
|
|
139
|
+
* }
|
|
140
|
+
* );
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
export declare function coordinatedExecution<T>(manager: QueueManager, options: QueueWaitOptions, fn: () => Promise<T>): Promise<T>;
|
|
144
|
+
/**
|
|
145
|
+
* Result of a concurrent group execution
|
|
146
|
+
*/
|
|
147
|
+
export interface ConcurrentGroupResult<T> {
|
|
148
|
+
/** Results from each execution */
|
|
149
|
+
results: Array<{
|
|
150
|
+
agentId: string;
|
|
151
|
+
result?: T;
|
|
152
|
+
error?: Error;
|
|
153
|
+
}>;
|
|
154
|
+
/** Total execution time */
|
|
155
|
+
totalTimeMs: number;
|
|
156
|
+
/** Number of successful executions */
|
|
157
|
+
successCount: number;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Execute function with coordination, allowing limited concurrency
|
|
161
|
+
*
|
|
162
|
+
* Useful when you want to allow some parallel execution but limit the
|
|
163
|
+
* total number of concurrent agents.
|
|
164
|
+
*/
|
|
165
|
+
export declare function withConcurrencyLimit<T>(manager: QueueManager, type: LockType, resource: string, maxConcurrent: number, fn: () => Promise<T>): Promise<T>;
|
|
166
|
+
//# sourceMappingURL=queue-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queue-manager.d.ts","sourceRoot":"","sources":["../../src/concurrency/queue-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue Manager
|
|
3
|
+
*
|
|
4
|
+
* Provides fair queuing for resources when multiple agents are waiting
|
|
5
|
+
* for the same lock. Supports priority-based scheduling and timeout handling.
|
|
6
|
+
*/
|
|
7
|
+
import { promises as fs } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
10
|
+
import { QueueFileSchema, getDefaultConcurrencyConfig, } from './types.js';
|
|
11
|
+
import { atomicWriteJson, readJsonSafe, unlinkSafe, sleepWithJitter, acquireFileLock, } from './atomic.js';
|
|
12
|
+
import { createAgentInfo } from './agent.js';
|
|
13
|
+
import { LockManager } from './lock-manager.js';
|
|
14
|
+
import { createDebugger } from '@eddacraft/anvil-core';
|
|
15
|
+
const debug = createDebugger('queue');
|
|
16
|
+
/**
|
|
17
|
+
* Queue Manager
|
|
18
|
+
*
|
|
19
|
+
* Manages fair queuing for resources across multiple agents.
|
|
20
|
+
*/
|
|
21
|
+
export class QueueManager {
|
|
22
|
+
workspaceRoot;
|
|
23
|
+
config;
|
|
24
|
+
agent;
|
|
25
|
+
queueDir;
|
|
26
|
+
lockManager;
|
|
27
|
+
constructor(options) {
|
|
28
|
+
this.workspaceRoot = options.workspaceRoot;
|
|
29
|
+
this.config = {
|
|
30
|
+
...getDefaultConcurrencyConfig(),
|
|
31
|
+
...options.config,
|
|
32
|
+
};
|
|
33
|
+
this.agent = options.agentInfo ?? createAgentInfo();
|
|
34
|
+
this.queueDir = join(this.workspaceRoot, this.config.queueDir);
|
|
35
|
+
this.lockManager =
|
|
36
|
+
options.lockManager ??
|
|
37
|
+
new LockManager({
|
|
38
|
+
workspaceRoot: options.workspaceRoot,
|
|
39
|
+
config: options.config,
|
|
40
|
+
agentInfo: this.agent,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get the agent ID
|
|
45
|
+
*/
|
|
46
|
+
getAgentId() {
|
|
47
|
+
return this.agent.id;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Join a queue for a resource
|
|
51
|
+
*/
|
|
52
|
+
async join(options) {
|
|
53
|
+
const { type, resource, priority = 100, reason, timeoutMs = this.config.queueTimeoutMs, } = options;
|
|
54
|
+
await this.ensureQueueDir();
|
|
55
|
+
const queuePath = this.getQueuePath(type, resource);
|
|
56
|
+
// Serialise access to the queue file to prevent lost updates
|
|
57
|
+
const lockPath = `${queuePath}.lock`;
|
|
58
|
+
const fileLock = await acquireFileLock(lockPath, {
|
|
59
|
+
timeout: Math.min(timeoutMs, 30_000),
|
|
60
|
+
retryInterval: 50,
|
|
61
|
+
});
|
|
62
|
+
if (!fileLock) {
|
|
63
|
+
throw new Error(`Timed out waiting for queue lock: ${type}:${resource}`);
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const queue = await this.loadQueue(queuePath, type, resource);
|
|
67
|
+
// Check if already in queue
|
|
68
|
+
const existingIndex = queue.entries.findIndex((e) => e.agentId === this.agent.id);
|
|
69
|
+
if (existingIndex !== -1) {
|
|
70
|
+
// Update existing entry
|
|
71
|
+
queue.entries[existingIndex] = {
|
|
72
|
+
...queue.entries[existingIndex],
|
|
73
|
+
priority,
|
|
74
|
+
reason,
|
|
75
|
+
timeoutAt: new Date(Date.now() + timeoutMs).toISOString(),
|
|
76
|
+
};
|
|
77
|
+
this.sortQueue(queue);
|
|
78
|
+
queue.updatedAt = new Date().toISOString();
|
|
79
|
+
await atomicWriteJson(queuePath, queue);
|
|
80
|
+
const newPosition = queue.entries.findIndex((e) => e.agentId === this.agent.id) + 1;
|
|
81
|
+
debug(`Queue entry updated: ${type}:${resource} position=${newPosition}`);
|
|
82
|
+
return {
|
|
83
|
+
entryId: queue.entries[existingIndex].id,
|
|
84
|
+
position: newPosition,
|
|
85
|
+
alreadyQueued: true,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// Check queue size limit
|
|
89
|
+
if (queue.entries.length >= this.config.maxQueueSize) {
|
|
90
|
+
throw new Error(`Queue is full (max ${this.config.maxQueueSize} entries)`);
|
|
91
|
+
}
|
|
92
|
+
// Create new entry
|
|
93
|
+
const entry = {
|
|
94
|
+
id: randomUUID(),
|
|
95
|
+
agentId: this.agent.id,
|
|
96
|
+
agentType: this.agent.type,
|
|
97
|
+
lockType: type,
|
|
98
|
+
resource,
|
|
99
|
+
queuedAt: new Date().toISOString(),
|
|
100
|
+
priority,
|
|
101
|
+
timeoutAt: new Date(Date.now() + timeoutMs).toISOString(),
|
|
102
|
+
reason,
|
|
103
|
+
};
|
|
104
|
+
queue.entries.push(entry);
|
|
105
|
+
this.sortQueue(queue);
|
|
106
|
+
queue.updatedAt = new Date().toISOString();
|
|
107
|
+
await atomicWriteJson(queuePath, queue);
|
|
108
|
+
const position = queue.entries.findIndex((e) => e.id === entry.id) + 1;
|
|
109
|
+
debug(`Joined queue: ${type}:${resource} position=${position}`);
|
|
110
|
+
return {
|
|
111
|
+
entryId: entry.id,
|
|
112
|
+
position,
|
|
113
|
+
alreadyQueued: false,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
await fileLock.release();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Leave a queue
|
|
122
|
+
*/
|
|
123
|
+
async leave(type, resource) {
|
|
124
|
+
const queuePath = this.getQueuePath(type, resource);
|
|
125
|
+
const queue = await this.loadQueue(queuePath, type, resource);
|
|
126
|
+
const index = queue.entries.findIndex((e) => e.agentId === this.agent.id);
|
|
127
|
+
if (index === -1) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
queue.entries.splice(index, 1);
|
|
131
|
+
queue.updatedAt = new Date().toISOString();
|
|
132
|
+
if (queue.entries.length === 0) {
|
|
133
|
+
await unlinkSafe(queuePath);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
await atomicWriteJson(queuePath, queue);
|
|
137
|
+
}
|
|
138
|
+
debug(`Left queue: ${type}:${resource}`);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Get queue status
|
|
143
|
+
*/
|
|
144
|
+
async getStatus(type, resource) {
|
|
145
|
+
const queuePath = this.getQueuePath(type, resource);
|
|
146
|
+
const queue = await this.loadQueue(queuePath, type, resource);
|
|
147
|
+
// Clean up timed-out entries
|
|
148
|
+
await this.cleanupTimedOut(queue, queuePath);
|
|
149
|
+
const yourEntry = queue.entries.find((e) => e.agentId === this.agent.id);
|
|
150
|
+
const yourPosition = yourEntry
|
|
151
|
+
? queue.entries.findIndex((e) => e.agentId === this.agent.id) + 1
|
|
152
|
+
: undefined;
|
|
153
|
+
// Get current lock holder
|
|
154
|
+
const lockInfo = await this.lockManager.getLockInfo(type, resource);
|
|
155
|
+
return {
|
|
156
|
+
totalEntries: queue.entries.length,
|
|
157
|
+
yourPosition,
|
|
158
|
+
yourEntry,
|
|
159
|
+
currentHolder: lockInfo
|
|
160
|
+
? {
|
|
161
|
+
agentId: lockInfo.agentId,
|
|
162
|
+
agentType: lockInfo.agentType,
|
|
163
|
+
acquiredAt: lockInfo.acquiredAt,
|
|
164
|
+
expiresAt: lockInfo.expiresAt,
|
|
165
|
+
}
|
|
166
|
+
: undefined,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Check if it's our turn (we're first in queue)
|
|
171
|
+
*/
|
|
172
|
+
async isOurTurn(type, resource) {
|
|
173
|
+
const status = await this.getStatus(type, resource);
|
|
174
|
+
return status.yourPosition === 1;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Wait in queue until lock is acquired
|
|
178
|
+
*
|
|
179
|
+
* This is the main coordination primitive - joins queue, waits for turn,
|
|
180
|
+
* then acquires lock.
|
|
181
|
+
*/
|
|
182
|
+
async waitForLock(options) {
|
|
183
|
+
const { type, resource, priority, reason, timeoutMs, maxWaitMs = this.config.queueTimeoutMs, pollIntervalMs = 500, onPositionChange, onLockAcquired, } = options;
|
|
184
|
+
const startTime = Date.now();
|
|
185
|
+
// First try to acquire lock directly (might be available)
|
|
186
|
+
const directResult = await this.lockManager.acquire({
|
|
187
|
+
type,
|
|
188
|
+
resource,
|
|
189
|
+
reason,
|
|
190
|
+
wait: false,
|
|
191
|
+
});
|
|
192
|
+
if (directResult.acquired) {
|
|
193
|
+
onLockAcquired?.();
|
|
194
|
+
return directResult;
|
|
195
|
+
}
|
|
196
|
+
// Join queue
|
|
197
|
+
const joinResult = await this.join({ type, resource, priority, reason, timeoutMs });
|
|
198
|
+
let lastPosition = joinResult.position;
|
|
199
|
+
debug(`Waiting in queue: ${type}:${resource} position=${lastPosition}`);
|
|
200
|
+
try {
|
|
201
|
+
while (Date.now() - startTime < maxWaitMs) {
|
|
202
|
+
// Check our position
|
|
203
|
+
const status = await this.getStatus(type, resource);
|
|
204
|
+
if (status.yourPosition !== lastPosition) {
|
|
205
|
+
lastPosition = status.yourPosition ?? lastPosition;
|
|
206
|
+
onPositionChange?.(lastPosition, status.totalEntries);
|
|
207
|
+
debug(`Queue position changed: ${type}:${resource} position=${lastPosition}`);
|
|
208
|
+
}
|
|
209
|
+
// If we're first, try to acquire lock
|
|
210
|
+
if (status.yourPosition === 1) {
|
|
211
|
+
const lockResult = await this.lockManager.acquire({
|
|
212
|
+
type,
|
|
213
|
+
resource,
|
|
214
|
+
reason,
|
|
215
|
+
wait: false,
|
|
216
|
+
});
|
|
217
|
+
if (lockResult.acquired) {
|
|
218
|
+
// Leave queue and return
|
|
219
|
+
await this.leave(type, resource);
|
|
220
|
+
onLockAcquired?.();
|
|
221
|
+
return lockResult;
|
|
222
|
+
}
|
|
223
|
+
// Lock still held, wait longer
|
|
224
|
+
}
|
|
225
|
+
// Wait before polling again
|
|
226
|
+
await sleepWithJitter(pollIntervalMs);
|
|
227
|
+
}
|
|
228
|
+
// Timeout
|
|
229
|
+
await this.leave(type, resource);
|
|
230
|
+
return {
|
|
231
|
+
acquired: false,
|
|
232
|
+
error: `Queue wait timed out after ${maxWaitMs}ms`,
|
|
233
|
+
queuePosition: lastPosition,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
// Cleanup on error
|
|
238
|
+
await this.leave(type, resource).catch(() => { });
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Get all queues in the workspace
|
|
244
|
+
*/
|
|
245
|
+
async getAllQueues() {
|
|
246
|
+
const result = [];
|
|
247
|
+
try {
|
|
248
|
+
const files = await fs.readdir(this.queueDir);
|
|
249
|
+
for (const file of files) {
|
|
250
|
+
if (!file.endsWith('.json'))
|
|
251
|
+
continue;
|
|
252
|
+
const queuePath = join(this.queueDir, file);
|
|
253
|
+
const data = await readJsonSafe(queuePath);
|
|
254
|
+
if (data) {
|
|
255
|
+
const parseResult = QueueFileSchema.safeParse(data);
|
|
256
|
+
if (parseResult.success) {
|
|
257
|
+
result.push({
|
|
258
|
+
type: parseResult.data.lockType,
|
|
259
|
+
resource: parseResult.data.resource,
|
|
260
|
+
entries: parseResult.data.entries.length,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
if (error.code !== 'ENOENT') {
|
|
268
|
+
debug('Failed to list queues:', error);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Cleanup all timed-out entries across all queues
|
|
275
|
+
*/
|
|
276
|
+
async cleanupAllTimedOut() {
|
|
277
|
+
let cleaned = 0;
|
|
278
|
+
try {
|
|
279
|
+
const files = await fs.readdir(this.queueDir);
|
|
280
|
+
for (const file of files) {
|
|
281
|
+
if (!file.endsWith('.json'))
|
|
282
|
+
continue;
|
|
283
|
+
const queuePath = join(this.queueDir, file);
|
|
284
|
+
const data = await readJsonSafe(queuePath);
|
|
285
|
+
if (data) {
|
|
286
|
+
const parseResult = QueueFileSchema.safeParse(data);
|
|
287
|
+
if (parseResult.success) {
|
|
288
|
+
cleaned += await this.cleanupTimedOut(parseResult.data, queuePath);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
if (error.code !== 'ENOENT') {
|
|
295
|
+
debug('Cleanup failed:', error);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return cleaned;
|
|
299
|
+
}
|
|
300
|
+
// ============================================================================
|
|
301
|
+
// Private Methods
|
|
302
|
+
// ============================================================================
|
|
303
|
+
/**
|
|
304
|
+
* Load queue from file
|
|
305
|
+
*/
|
|
306
|
+
async loadQueue(queuePath, type, resource) {
|
|
307
|
+
const data = await readJsonSafe(queuePath);
|
|
308
|
+
if (data) {
|
|
309
|
+
const result = QueueFileSchema.safeParse(data);
|
|
310
|
+
if (result.success) {
|
|
311
|
+
return result.data;
|
|
312
|
+
}
|
|
313
|
+
debug('Invalid queue file schema:', result.error);
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
version: '1.0.0',
|
|
317
|
+
resource,
|
|
318
|
+
lockType: type,
|
|
319
|
+
updatedAt: new Date().toISOString(),
|
|
320
|
+
entries: [],
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Sort queue entries by priority then by queued time
|
|
325
|
+
*/
|
|
326
|
+
sortQueue(queue) {
|
|
327
|
+
queue.entries.sort((a, b) => {
|
|
328
|
+
// Lower priority number = higher priority
|
|
329
|
+
if (a.priority !== b.priority) {
|
|
330
|
+
return a.priority - b.priority;
|
|
331
|
+
}
|
|
332
|
+
// Earlier queued = higher priority
|
|
333
|
+
return new Date(a.queuedAt).getTime() - new Date(b.queuedAt).getTime();
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Cleanup timed-out entries
|
|
338
|
+
*/
|
|
339
|
+
async cleanupTimedOut(queue, queuePath) {
|
|
340
|
+
const now = Date.now();
|
|
341
|
+
const before = queue.entries.length;
|
|
342
|
+
queue.entries = queue.entries.filter((entry) => {
|
|
343
|
+
const timeoutAt = new Date(entry.timeoutAt).getTime();
|
|
344
|
+
return now < timeoutAt;
|
|
345
|
+
});
|
|
346
|
+
const removed = before - queue.entries.length;
|
|
347
|
+
if (removed > 0) {
|
|
348
|
+
if (queue.entries.length === 0) {
|
|
349
|
+
await unlinkSafe(queuePath);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
queue.updatedAt = new Date().toISOString();
|
|
353
|
+
await atomicWriteJson(queuePath, queue);
|
|
354
|
+
}
|
|
355
|
+
debug(`Cleaned up ${removed} timed-out queue entries`);
|
|
356
|
+
}
|
|
357
|
+
return removed;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Get queue file path
|
|
361
|
+
*/
|
|
362
|
+
getQueuePath(type, resource) {
|
|
363
|
+
const key = `${type}:${resource}`;
|
|
364
|
+
const hash = createHash('sha256').update(key).digest('hex').slice(0, 16);
|
|
365
|
+
return join(this.queueDir, `${hash}.json`);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Ensure queue directory exists
|
|
369
|
+
*/
|
|
370
|
+
async ensureQueueDir() {
|
|
371
|
+
await fs.mkdir(this.queueDir, { recursive: true });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Create a queue manager
|
|
376
|
+
*/
|
|
377
|
+
export function createQueueManager(options) {
|
|
378
|
+
return new QueueManager(options);
|
|
379
|
+
}
|
|
380
|
+
// ============================================================================
|
|
381
|
+
// High-Level Coordination Primitives
|
|
382
|
+
// ============================================================================
|
|
383
|
+
/**
|
|
384
|
+
* Coordinated execution with fair queuing
|
|
385
|
+
*
|
|
386
|
+
* This is the primary way to execute code that requires exclusive access
|
|
387
|
+
* to a resource in a multi-agent environment.
|
|
388
|
+
*
|
|
389
|
+
* @example
|
|
390
|
+
* ```typescript
|
|
391
|
+
* const result = await coordinatedExecution(
|
|
392
|
+
* queueManager,
|
|
393
|
+
* { type: 'action', resource: 'gate', reason: 'Running quality gates' },
|
|
394
|
+
* async () => {
|
|
395
|
+
* return await runGates();
|
|
396
|
+
* }
|
|
397
|
+
* );
|
|
398
|
+
* ```
|
|
399
|
+
*/
|
|
400
|
+
export async function coordinatedExecution(manager, options, fn) {
|
|
401
|
+
const lockResult = await manager.waitForLock(options);
|
|
402
|
+
if (!lockResult.acquired) {
|
|
403
|
+
throw new Error(`Failed to acquire lock: ${lockResult.error}`);
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
return await fn();
|
|
407
|
+
}
|
|
408
|
+
finally {
|
|
409
|
+
await manager['lockManager'].release(options.type, options.resource);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Execute function with coordination, allowing limited concurrency
|
|
414
|
+
*
|
|
415
|
+
* Useful when you want to allow some parallel execution but limit the
|
|
416
|
+
* total number of concurrent agents.
|
|
417
|
+
*/
|
|
418
|
+
export async function withConcurrencyLimit(manager, type, resource, maxConcurrent, fn) {
|
|
419
|
+
// Use resource slots for concurrency limiting
|
|
420
|
+
for (let slot = 0; slot < maxConcurrent; slot++) {
|
|
421
|
+
const slotResource = `${resource}:slot-${slot}`;
|
|
422
|
+
const result = await manager.waitForLock({
|
|
423
|
+
type,
|
|
424
|
+
resource: slotResource,
|
|
425
|
+
maxWaitMs: 1000, // Quick check per slot
|
|
426
|
+
});
|
|
427
|
+
if (result.acquired) {
|
|
428
|
+
try {
|
|
429
|
+
return await fn();
|
|
430
|
+
}
|
|
431
|
+
finally {
|
|
432
|
+
await manager['lockManager'].release(type, slotResource);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
// All slots taken, wait for any slot
|
|
437
|
+
return coordinatedExecution(manager, {
|
|
438
|
+
type,
|
|
439
|
+
resource: `${resource}:slot-0`, // Wait for first slot
|
|
440
|
+
maxWaitMs: manager['config'].queueTimeoutMs,
|
|
441
|
+
}, fn);
|
|
442
|
+
}
|