sentinelayer-cli 0.8.0 → 0.8.1
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/README.md +13 -0
- package/package.json +4 -4
- package/src/agents/ai-governance/index.js +12 -0
- package/src/agents/ai-governance/tools/base.js +171 -0
- package/src/agents/ai-governance/tools/eval-regression.js +47 -0
- package/src/agents/ai-governance/tools/hitl-audit.js +81 -0
- package/src/agents/ai-governance/tools/index.js +52 -0
- package/src/agents/ai-governance/tools/prompt-drift.js +42 -0
- package/src/agents/ai-governance/tools/provenance-check.js +69 -0
- package/src/agents/backend/index.js +12 -0
- package/src/agents/backend/tools/base.js +189 -0
- package/src/agents/backend/tools/circuit-breaker-check.js +123 -0
- package/src/agents/backend/tools/idempotency-audit.js +105 -0
- package/src/agents/backend/tools/index.js +87 -0
- package/src/agents/backend/tools/retry-audit.js +132 -0
- package/src/agents/backend/tools/timeout-audit.js +144 -0
- package/src/agents/code-quality/index.js +12 -0
- package/src/agents/code-quality/tools/base.js +159 -0
- package/src/agents/code-quality/tools/complexity-measure.js +197 -0
- package/src/agents/code-quality/tools/coupling-analysis.js +81 -0
- package/src/agents/code-quality/tools/cycle-detect.js +49 -0
- package/src/agents/code-quality/tools/dep-graph.js +196 -0
- package/src/agents/code-quality/tools/index.js +89 -0
- package/src/agents/data-layer/index.js +12 -0
- package/src/agents/data-layer/tools/base.js +181 -0
- package/src/agents/data-layer/tools/index-audit.js +165 -0
- package/src/agents/data-layer/tools/index.js +83 -0
- package/src/agents/data-layer/tools/migration-scan.js +135 -0
- package/src/agents/data-layer/tools/query-explain.js +120 -0
- package/src/agents/data-layer/tools/tenancy-scan.js +166 -0
- package/src/agents/documentation/index.js +12 -0
- package/src/agents/documentation/tools/api-diff.js +91 -0
- package/src/agents/documentation/tools/base.js +151 -0
- package/src/agents/documentation/tools/dead-link-check.js +58 -0
- package/src/agents/documentation/tools/docstring-coverage.js +78 -0
- package/src/agents/documentation/tools/index.js +52 -0
- package/src/agents/documentation/tools/readme-freshness.js +61 -0
- package/src/agents/envelope/fix-cycle.js +45 -0
- package/src/agents/envelope/index.js +31 -0
- package/src/agents/envelope/loop.js +150 -0
- package/src/agents/envelope/pulse.js +18 -0
- package/src/agents/envelope/stream.js +40 -0
- package/src/agents/infrastructure/index.js +12 -0
- package/src/agents/infrastructure/tools/base.js +171 -0
- package/src/agents/infrastructure/tools/checkov-run.js +32 -0
- package/src/agents/infrastructure/tools/drift-detect.js +59 -0
- package/src/agents/infrastructure/tools/iam-least-priv-check.js +78 -0
- package/src/agents/infrastructure/tools/index.js +52 -0
- package/src/agents/infrastructure/tools/tflint-run.js +31 -0
- package/src/agents/jules/loop.js +7 -4
- package/src/agents/jules/swarm/sub-agent.js +5 -1
- package/src/agents/jules/tools/auth-audit.js +10 -1
- package/src/agents/mode.js +113 -0
- package/src/agents/observability/index.js +12 -0
- package/src/agents/observability/tools/alert-audit.js +39 -0
- package/src/agents/observability/tools/base.js +181 -0
- package/src/agents/observability/tools/dashboard-gap.js +42 -0
- package/src/agents/observability/tools/index.js +54 -0
- package/src/agents/observability/tools/log-schema-check.js +74 -0
- package/src/agents/observability/tools/span-coverage.js +74 -0
- package/src/agents/persona-visuals.js +38 -0
- package/src/agents/release/index.js +12 -0
- package/src/agents/release/tools/base.js +181 -0
- package/src/agents/release/tools/changelog-diff.js +86 -0
- package/src/agents/release/tools/feature-flag-audit.js +126 -0
- package/src/agents/release/tools/index.js +61 -0
- package/src/agents/release/tools/rollback-verify.js +129 -0
- package/src/agents/release/tools/semver-check.js +109 -0
- package/src/agents/reliability/index.js +12 -0
- package/src/agents/reliability/tools/backpressure-check.js +129 -0
- package/src/agents/reliability/tools/base.js +181 -0
- package/src/agents/reliability/tools/chaos-probe.js +109 -0
- package/src/agents/reliability/tools/graceful-degradation-check.js +114 -0
- package/src/agents/reliability/tools/health-check-audit.js +111 -0
- package/src/agents/reliability/tools/index.js +87 -0
- package/src/agents/run-persona.js +109 -0
- package/src/agents/security/index.js +12 -0
- package/src/agents/security/tools/authz-audit.js +134 -0
- package/src/agents/security/tools/base.js +190 -0
- package/src/agents/security/tools/crypto-review.js +175 -0
- package/src/agents/security/tools/index.js +97 -0
- package/src/agents/security/tools/sast-scan.js +175 -0
- package/src/agents/security/tools/secrets-scan.js +216 -0
- package/src/agents/supply-chain/index.js +12 -0
- package/src/agents/supply-chain/tools/attestation-check.js +42 -0
- package/src/agents/supply-chain/tools/base.js +151 -0
- package/src/agents/supply-chain/tools/index.js +52 -0
- package/src/agents/supply-chain/tools/lockfile-integrity.js +73 -0
- package/src/agents/supply-chain/tools/package-verify.js +56 -0
- package/src/agents/supply-chain/tools/sbom-diff.js +34 -0
- package/src/agents/testing/index.js +12 -0
- package/src/agents/testing/tools/base.js +202 -0
- package/src/agents/testing/tools/coverage-gap.js +144 -0
- package/src/agents/testing/tools/flake-detect.js +125 -0
- package/src/agents/testing/tools/index.js +85 -0
- package/src/agents/testing/tools/mutation-test.js +143 -0
- package/src/agents/testing/tools/snapshot-diff.js +103 -0
- package/src/auth/gate.js +65 -37
- package/src/cli.js +1 -1
- package/src/commands/chat.js +3 -10
- package/src/commands/legacy-args.js +10 -0
- package/src/commands/omargate.js +36 -2
- package/src/commands/persona.js +46 -1
- package/src/commands/scan.js +3 -10
- package/src/commands/session.js +654 -6
- package/src/commands/spec.js +3 -10
- package/src/coord/events-log.js +141 -0
- package/src/coord/handshake.js +719 -0
- package/src/coord/index.js +35 -0
- package/src/coord/paths.js +84 -0
- package/src/coord/priority.js +62 -0
- package/src/coord/tarjan.js +157 -0
- package/src/cost/tokenizer.js +160 -0
- package/src/cost/tracker.js +61 -0
- package/src/daemon/artifact-lineage.js +362 -0
- package/src/daemon/assignment-ledger.js +117 -0
- package/src/daemon/ast-drift.js +496 -0
- package/src/daemon/ingest-refresh.js +69 -2
- package/src/ingest/engine.js +15 -0
- package/src/ingest/ownership.js +380 -0
- package/src/legacy-cli.js +68 -1
- package/src/orchestrator/kai-chen.js +126 -0
- package/src/review/ai-review.js +3 -10
- package/src/review/compliance-pack.js +389 -0
- package/src/review/investor-dd-config.js +54 -0
- package/src/review/investor-dd-file-loop.js +303 -0
- package/src/review/investor-dd-file-router.js +406 -0
- package/src/review/investor-dd-html-report.js +233 -0
- package/src/review/investor-dd-notification.js +120 -0
- package/src/review/investor-dd-orchestrator.js +405 -0
- package/src/review/investor-dd-persona-runner.js +275 -0
- package/src/review/live-validator.js +253 -0
- package/src/review/omargate-orchestrator.js +90 -2
- package/src/review/persona-prompts.js +244 -56
- package/src/review/reconciliation-rules.js +329 -0
- package/src/review/reproducibility-chain.js +136 -0
- package/src/review/scan-modes.js +102 -3
- package/src/session/agent-registry.js +7 -0
- package/src/session/analytics.js +479 -0
- package/src/session/daemon.js +609 -14
- package/src/session/file-locks.js +666 -0
- package/src/session/paths.js +4 -0
- package/src/session/recap.js +567 -0
- package/src/session/redact.js +82 -0
- package/src/session/runtime-bridge.js +24 -1
- package/src/session/scoring.js +406 -0
- package/src/session/setup-guides.js +304 -0
- package/src/session/store.js +318 -2
- package/src/session/stream.js +9 -1
- package/src/session/sync.js +753 -0
- package/src/session/tasks.js +1054 -0
- package/src/session/templates.js +188 -0
- package/src/swarm/runtime.js +1 -8
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
import fsp from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
6
|
+
|
|
7
|
+
import { createAgentEvent } from "../events/schema.js";
|
|
8
|
+
import { resolveSessionPaths } from "./paths.js";
|
|
9
|
+
import { appendToStream } from "./stream.js";
|
|
10
|
+
|
|
11
|
+
const FILE_LOCK_SCHEMA_VERSION = "1.0.0";
|
|
12
|
+
const DEFAULT_FILE_LOCK_TTL_SECONDS = 300;
|
|
13
|
+
const DEFAULT_LOCK_TIMEOUT_MS = 10_000;
|
|
14
|
+
const DEFAULT_LOCK_STALE_MS = 30_000;
|
|
15
|
+
const DEFAULT_LOCK_POLL_MS = 25;
|
|
16
|
+
const SENTI_AGENT_ID = "senti";
|
|
17
|
+
|
|
18
|
+
function normalizeString(value) {
|
|
19
|
+
return String(value || "").trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeIsoTimestamp(value, fallbackIso = new Date().toISOString()) {
|
|
23
|
+
const normalized = normalizeString(value);
|
|
24
|
+
if (!normalized) {
|
|
25
|
+
return fallbackIso;
|
|
26
|
+
}
|
|
27
|
+
const epoch = Date.parse(normalized);
|
|
28
|
+
if (!Number.isFinite(epoch)) {
|
|
29
|
+
return fallbackIso;
|
|
30
|
+
}
|
|
31
|
+
return new Date(epoch).toISOString();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizePositiveInteger(value, fallbackValue) {
|
|
35
|
+
if (value === undefined || value === null || normalizeString(value) === "") {
|
|
36
|
+
return fallbackValue;
|
|
37
|
+
}
|
|
38
|
+
const normalized = Number(value);
|
|
39
|
+
if (!Number.isFinite(normalized) || normalized <= 0) {
|
|
40
|
+
throw new Error("Value must be a positive integer.");
|
|
41
|
+
}
|
|
42
|
+
return Math.floor(normalized);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function toIsoAfterSeconds(nowIso, seconds) {
|
|
46
|
+
const nowEpoch = Date.parse(normalizeIsoTimestamp(nowIso, nowIso));
|
|
47
|
+
return new Date(nowEpoch + Math.max(1, Math.floor(Number(seconds) || 0)) * 1000).toISOString();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseEpoch(value, fallbackIso = new Date().toISOString()) {
|
|
51
|
+
return Date.parse(normalizeIsoTimestamp(value, fallbackIso)) || 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function formatSince(fromIso, nowIso = new Date().toISOString()) {
|
|
55
|
+
const nowEpoch = parseEpoch(nowIso, nowIso);
|
|
56
|
+
const fromEpoch = parseEpoch(fromIso, nowIso);
|
|
57
|
+
const deltaMs = Math.max(0, nowEpoch - fromEpoch);
|
|
58
|
+
const deltaSeconds = Math.max(0, Math.floor(deltaMs / 1000));
|
|
59
|
+
if (deltaSeconds < 60) {
|
|
60
|
+
return `${deltaSeconds}s ago`;
|
|
61
|
+
}
|
|
62
|
+
const deltaMinutes = Math.floor(deltaSeconds / 60);
|
|
63
|
+
if (deltaMinutes < 60) {
|
|
64
|
+
return `${deltaMinutes}m ago`;
|
|
65
|
+
}
|
|
66
|
+
const deltaHours = Math.floor(deltaMinutes / 60);
|
|
67
|
+
if (deltaHours < 24) {
|
|
68
|
+
return `${deltaHours}h ago`;
|
|
69
|
+
}
|
|
70
|
+
const deltaDays = Math.floor(deltaHours / 24);
|
|
71
|
+
return `${deltaDays}d ago`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeFilePath(filePath, { targetPath = process.cwd() } = {}) {
|
|
75
|
+
const raw = normalizeString(filePath);
|
|
76
|
+
if (!raw) {
|
|
77
|
+
throw new Error("filePath is required.");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let normalized = raw;
|
|
81
|
+
if (path.isAbsolute(raw)) {
|
|
82
|
+
normalized = path.relative(path.resolve(String(targetPath || ".")), path.resolve(raw));
|
|
83
|
+
}
|
|
84
|
+
normalized = normalizeString(normalized).replace(/\\/g, "/");
|
|
85
|
+
normalized = normalized.replace(/^\.\/+/, "");
|
|
86
|
+
if (!normalized || normalized === ".") {
|
|
87
|
+
throw new Error("filePath is required.");
|
|
88
|
+
}
|
|
89
|
+
return normalized;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function normalizeAgentId(agentId) {
|
|
93
|
+
const normalized = normalizeString(agentId).toLowerCase();
|
|
94
|
+
if (!normalized) {
|
|
95
|
+
throw new Error("agentId is required.");
|
|
96
|
+
}
|
|
97
|
+
return normalized;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isLockExpired(lockRecord, nowIso = new Date().toISOString()) {
|
|
101
|
+
const nowEpoch = parseEpoch(nowIso, nowIso);
|
|
102
|
+
const expiresAtEpoch = parseEpoch(lockRecord?.expiresAt, nowIso);
|
|
103
|
+
if (!Number.isFinite(nowEpoch) || !Number.isFinite(expiresAtEpoch)) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
return nowEpoch >= expiresAtEpoch;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function normalizeLockRecord(filePath, raw = {}, { nowIso = new Date().toISOString() } = {}) {
|
|
110
|
+
const normalizedFile = normalizeString(filePath);
|
|
111
|
+
if (!normalizedFile) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const agentId = normalizeString(raw.agentId).toLowerCase();
|
|
115
|
+
if (!agentId) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
const ttlSeconds = normalizePositiveInteger(raw.ttlSeconds, DEFAULT_FILE_LOCK_TTL_SECONDS);
|
|
119
|
+
const lockedAt = normalizeIsoTimestamp(raw.lockedAt, nowIso);
|
|
120
|
+
const expiresAt = normalizeIsoTimestamp(raw.expiresAt, toIsoAfterSeconds(lockedAt, ttlSeconds));
|
|
121
|
+
return {
|
|
122
|
+
file: normalizedFile,
|
|
123
|
+
agentId,
|
|
124
|
+
intent: normalizeString(raw.intent),
|
|
125
|
+
lockedAt,
|
|
126
|
+
expiresAt,
|
|
127
|
+
ttlSeconds,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function normalizeRegistry(raw = {}, { sessionId, nowIso = new Date().toISOString() } = {}) {
|
|
132
|
+
const source = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
133
|
+
const inputLocks = source.locks && typeof source.locks === "object" ? source.locks : {};
|
|
134
|
+
const locks = {};
|
|
135
|
+
for (const [filePath, value] of Object.entries(inputLocks)) {
|
|
136
|
+
const normalizedFilePath = normalizeString(filePath);
|
|
137
|
+
if (!normalizedFilePath) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const record = normalizeLockRecord(normalizedFilePath, value, { nowIso });
|
|
141
|
+
if (!record) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
locks[normalizedFilePath] = record;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
schemaVersion: FILE_LOCK_SCHEMA_VERSION,
|
|
149
|
+
sessionId: normalizeString(source.sessionId) || normalizeString(sessionId),
|
|
150
|
+
updatedAt: normalizeIsoTimestamp(source.updatedAt, nowIso),
|
|
151
|
+
locks,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function readJsonFile(filePath, { allowMissing = true } = {}) {
|
|
156
|
+
try {
|
|
157
|
+
const raw = await fsp.readFile(filePath, "utf-8");
|
|
158
|
+
return JSON.parse(raw);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (allowMissing && error && typeof error === "object" && error.code === "ENOENT") {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function writeJsonFile(filePath, payload) {
|
|
168
|
+
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
169
|
+
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
170
|
+
await fsp.writeFile(tmpPath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
|
|
171
|
+
await fsp.rename(tmpPath, filePath);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function ensureSessionExists(paths) {
|
|
175
|
+
try {
|
|
176
|
+
await fsp.access(paths.metadataPath);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (error && typeof error === "object" && error.code === "ENOENT") {
|
|
179
|
+
throw new Error(`Session '${paths.sessionId}' was not found.`);
|
|
180
|
+
}
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function acquireLock(lockPath, {
|
|
186
|
+
timeoutMs = DEFAULT_LOCK_TIMEOUT_MS,
|
|
187
|
+
staleMs = DEFAULT_LOCK_STALE_MS,
|
|
188
|
+
pollMs = DEFAULT_LOCK_POLL_MS,
|
|
189
|
+
} = {}) {
|
|
190
|
+
const start = Date.now();
|
|
191
|
+
while (true) {
|
|
192
|
+
try {
|
|
193
|
+
await fsp.mkdir(lockPath);
|
|
194
|
+
return;
|
|
195
|
+
} catch (error) {
|
|
196
|
+
const code = error && typeof error === "object" ? error.code : "";
|
|
197
|
+
if (!(code === "EEXIST" || code === "EPERM" || code === "EACCES")) {
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const stat = await fsp.stat(lockPath);
|
|
203
|
+
const ageMs = Date.now() - Number(stat.mtimeMs || 0);
|
|
204
|
+
if (Number.isFinite(ageMs) && ageMs > staleMs) {
|
|
205
|
+
await fsp.rm(lockPath, { recursive: true, force: true });
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
} catch {
|
|
209
|
+
// Continue waiting.
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (Date.now() - start >= timeoutMs) {
|
|
213
|
+
throw new Error("Timed out waiting for session file lock registry.");
|
|
214
|
+
}
|
|
215
|
+
await sleep(pollMs);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function releaseLock(lockPath) {
|
|
221
|
+
await fsp.rm(lockPath, { recursive: true, force: true }).catch(() => {});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function pruneExpiredLocks(registry, { nowIso = new Date().toISOString() } = {}) {
|
|
225
|
+
const expired = [];
|
|
226
|
+
for (const [filePath, lockRecord] of Object.entries(registry.locks || {})) {
|
|
227
|
+
if (!isLockExpired(lockRecord, nowIso)) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
expired.push({
|
|
231
|
+
...lockRecord,
|
|
232
|
+
file: filePath,
|
|
233
|
+
expiredAt: normalizeIsoTimestamp(nowIso, new Date().toISOString()),
|
|
234
|
+
});
|
|
235
|
+
delete registry.locks[filePath];
|
|
236
|
+
}
|
|
237
|
+
return expired;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function presentLock(lockRecord, { nowIso = new Date().toISOString() } = {}) {
|
|
241
|
+
if (!lockRecord) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
file: normalizeString(lockRecord.file),
|
|
246
|
+
agentId: normalizeString(lockRecord.agentId),
|
|
247
|
+
intent: normalizeString(lockRecord.intent),
|
|
248
|
+
lockedAt: normalizeIsoTimestamp(lockRecord.lockedAt, nowIso),
|
|
249
|
+
expiresAt: normalizeIsoTimestamp(lockRecord.expiresAt, nowIso),
|
|
250
|
+
ttlSeconds: normalizePositiveInteger(lockRecord.ttlSeconds, DEFAULT_FILE_LOCK_TTL_SECONDS),
|
|
251
|
+
since: formatSince(lockRecord.lockedAt, nowIso),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function appendLockEvent(
|
|
256
|
+
sessionId,
|
|
257
|
+
event,
|
|
258
|
+
agentId,
|
|
259
|
+
payload,
|
|
260
|
+
{
|
|
261
|
+
targetPath = process.cwd(),
|
|
262
|
+
nowIso = new Date().toISOString(),
|
|
263
|
+
} = {}
|
|
264
|
+
) {
|
|
265
|
+
return appendToStream(
|
|
266
|
+
sessionId,
|
|
267
|
+
createAgentEvent({
|
|
268
|
+
event,
|
|
269
|
+
agentId: normalizeAgentId(agentId),
|
|
270
|
+
sessionId,
|
|
271
|
+
ts: normalizeIsoTimestamp(nowIso, new Date().toISOString()),
|
|
272
|
+
payload,
|
|
273
|
+
}),
|
|
274
|
+
{
|
|
275
|
+
targetPath,
|
|
276
|
+
}
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function emitExpiredLockEvents(
|
|
281
|
+
sessionId,
|
|
282
|
+
expiredLocks = [],
|
|
283
|
+
{
|
|
284
|
+
targetPath = process.cwd(),
|
|
285
|
+
nowIso = new Date().toISOString(),
|
|
286
|
+
actorAgentId = SENTI_AGENT_ID,
|
|
287
|
+
} = {}
|
|
288
|
+
) {
|
|
289
|
+
const events = [];
|
|
290
|
+
for (const lockRecord of expiredLocks) {
|
|
291
|
+
const payload = {
|
|
292
|
+
file: normalizeString(lockRecord.file),
|
|
293
|
+
heldBy: normalizeString(lockRecord.agentId),
|
|
294
|
+
intent: normalizeString(lockRecord.intent),
|
|
295
|
+
lockedAt: normalizeIsoTimestamp(lockRecord.lockedAt, nowIso),
|
|
296
|
+
expiresAt: normalizeIsoTimestamp(lockRecord.expiresAt, nowIso),
|
|
297
|
+
expiredAt: normalizeIsoTimestamp(nowIso, new Date().toISOString()),
|
|
298
|
+
};
|
|
299
|
+
const event = await appendLockEvent(
|
|
300
|
+
sessionId,
|
|
301
|
+
"file_lock_expired",
|
|
302
|
+
actorAgentId,
|
|
303
|
+
payload,
|
|
304
|
+
{
|
|
305
|
+
targetPath,
|
|
306
|
+
nowIso,
|
|
307
|
+
}
|
|
308
|
+
);
|
|
309
|
+
events.push(event);
|
|
310
|
+
}
|
|
311
|
+
return events;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function mutateRegistry(
|
|
315
|
+
sessionId,
|
|
316
|
+
{
|
|
317
|
+
targetPath = process.cwd(),
|
|
318
|
+
nowIso = new Date().toISOString(),
|
|
319
|
+
emitExpiredEvents = true,
|
|
320
|
+
expiredEventAgentId = SENTI_AGENT_ID,
|
|
321
|
+
} = {},
|
|
322
|
+
mutator = async () => ({})
|
|
323
|
+
) {
|
|
324
|
+
const paths = resolveSessionPaths(sessionId, { targetPath });
|
|
325
|
+
await ensureSessionExists(paths);
|
|
326
|
+
const normalizedNow = normalizeIsoTimestamp(nowIso, new Date().toISOString());
|
|
327
|
+
|
|
328
|
+
await acquireLock(paths.fileLocksLockPath);
|
|
329
|
+
let result = null;
|
|
330
|
+
let expiredLocks = [];
|
|
331
|
+
try {
|
|
332
|
+
const rawRegistry = await readJsonFile(paths.fileLocksPath, { allowMissing: true });
|
|
333
|
+
const registry = normalizeRegistry(rawRegistry || {}, {
|
|
334
|
+
sessionId: paths.sessionId,
|
|
335
|
+
nowIso: normalizedNow,
|
|
336
|
+
});
|
|
337
|
+
expiredLocks = pruneExpiredLocks(registry, {
|
|
338
|
+
nowIso: normalizedNow,
|
|
339
|
+
});
|
|
340
|
+
result = await mutator(registry, {
|
|
341
|
+
nowIso: normalizedNow,
|
|
342
|
+
paths,
|
|
343
|
+
});
|
|
344
|
+
registry.updatedAt = normalizedNow;
|
|
345
|
+
await writeJsonFile(paths.fileLocksPath, registry);
|
|
346
|
+
} finally {
|
|
347
|
+
await releaseLock(paths.fileLocksLockPath);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const expiredEvents = emitExpiredEvents
|
|
351
|
+
? await emitExpiredLockEvents(sessionId, expiredLocks, {
|
|
352
|
+
targetPath,
|
|
353
|
+
nowIso: normalizedNow,
|
|
354
|
+
actorAgentId: expiredEventAgentId,
|
|
355
|
+
})
|
|
356
|
+
: [];
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
result,
|
|
360
|
+
expiredLocks,
|
|
361
|
+
expiredEvents,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export async function lockFile(
|
|
366
|
+
sessionId,
|
|
367
|
+
agentId,
|
|
368
|
+
filePath,
|
|
369
|
+
{
|
|
370
|
+
intent = "",
|
|
371
|
+
ttlSeconds = DEFAULT_FILE_LOCK_TTL_SECONDS,
|
|
372
|
+
targetPath = process.cwd(),
|
|
373
|
+
nowIso = new Date().toISOString(),
|
|
374
|
+
} = {}
|
|
375
|
+
) {
|
|
376
|
+
const normalizedAgentId = normalizeAgentId(agentId);
|
|
377
|
+
const normalizedFilePath = normalizeFilePath(filePath, { targetPath });
|
|
378
|
+
const normalizedIntent = normalizeString(intent);
|
|
379
|
+
const normalizedTtlSeconds = normalizePositiveInteger(ttlSeconds, DEFAULT_FILE_LOCK_TTL_SECONDS);
|
|
380
|
+
const normalizedNow = normalizeIsoTimestamp(nowIso, new Date().toISOString());
|
|
381
|
+
|
|
382
|
+
const mutation = await mutateRegistry(
|
|
383
|
+
sessionId,
|
|
384
|
+
{
|
|
385
|
+
targetPath,
|
|
386
|
+
nowIso: normalizedNow,
|
|
387
|
+
emitExpiredEvents: true,
|
|
388
|
+
expiredEventAgentId: SENTI_AGENT_ID,
|
|
389
|
+
},
|
|
390
|
+
async (registry) => {
|
|
391
|
+
const existing = registry.locks[normalizedFilePath] || null;
|
|
392
|
+
if (existing && existing.agentId !== normalizedAgentId) {
|
|
393
|
+
return {
|
|
394
|
+
locked: false,
|
|
395
|
+
file: normalizedFilePath,
|
|
396
|
+
heldBy: normalizeString(existing.agentId),
|
|
397
|
+
since: formatSince(existing.lockedAt, normalizedNow),
|
|
398
|
+
lock: presentLock({ ...existing, file: normalizedFilePath }, { nowIso: normalizedNow }),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const lockedAt = normalizedNow;
|
|
403
|
+
const expiresAt = toIsoAfterSeconds(lockedAt, normalizedTtlSeconds);
|
|
404
|
+
const lockRecord = {
|
|
405
|
+
file: normalizedFilePath,
|
|
406
|
+
agentId: normalizedAgentId,
|
|
407
|
+
intent: normalizedIntent,
|
|
408
|
+
lockedAt,
|
|
409
|
+
expiresAt,
|
|
410
|
+
ttlSeconds: normalizedTtlSeconds,
|
|
411
|
+
// Forensic holder metadata. Not used for correctness (TTL-based
|
|
412
|
+
// reclaim handles liveness), but lets operators see which process
|
|
413
|
+
// on which host took a lock when debugging stale-lock incidents.
|
|
414
|
+
holderPid: process.pid,
|
|
415
|
+
holderHostname: os.hostname(),
|
|
416
|
+
};
|
|
417
|
+
registry.locks[normalizedFilePath] = lockRecord;
|
|
418
|
+
return {
|
|
419
|
+
locked: true,
|
|
420
|
+
file: normalizedFilePath,
|
|
421
|
+
lock: presentLock(lockRecord, { nowIso: normalizedNow }),
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
if (mutation.result?.locked) {
|
|
427
|
+
const event = await appendLockEvent(
|
|
428
|
+
sessionId,
|
|
429
|
+
"file_lock",
|
|
430
|
+
normalizedAgentId,
|
|
431
|
+
{
|
|
432
|
+
file: normalizedFilePath,
|
|
433
|
+
intent: normalizedIntent,
|
|
434
|
+
ttlSeconds: normalizedTtlSeconds,
|
|
435
|
+
expiresAt: mutation.result.lock?.expiresAt || toIsoAfterSeconds(normalizedNow, normalizedTtlSeconds),
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
targetPath,
|
|
439
|
+
nowIso: normalizedNow,
|
|
440
|
+
}
|
|
441
|
+
);
|
|
442
|
+
return {
|
|
443
|
+
...mutation.result,
|
|
444
|
+
event,
|
|
445
|
+
expiredEvents: mutation.expiredEvents,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
...mutation.result,
|
|
451
|
+
expiredEvents: mutation.expiredEvents,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export async function unlockFile(
|
|
456
|
+
sessionId,
|
|
457
|
+
agentId,
|
|
458
|
+
filePath,
|
|
459
|
+
{
|
|
460
|
+
reason = "manual_release",
|
|
461
|
+
force = false,
|
|
462
|
+
targetPath = process.cwd(),
|
|
463
|
+
nowIso = new Date().toISOString(),
|
|
464
|
+
actorAgentId = null,
|
|
465
|
+
} = {}
|
|
466
|
+
) {
|
|
467
|
+
const normalizedAgentId = normalizeAgentId(agentId);
|
|
468
|
+
const normalizedFilePath = normalizeFilePath(filePath, { targetPath });
|
|
469
|
+
const normalizedNow = normalizeIsoTimestamp(nowIso, new Date().toISOString());
|
|
470
|
+
const normalizedReason = normalizeString(reason) || "manual_release";
|
|
471
|
+
|
|
472
|
+
const mutation = await mutateRegistry(
|
|
473
|
+
sessionId,
|
|
474
|
+
{
|
|
475
|
+
targetPath,
|
|
476
|
+
nowIso: normalizedNow,
|
|
477
|
+
emitExpiredEvents: true,
|
|
478
|
+
expiredEventAgentId: SENTI_AGENT_ID,
|
|
479
|
+
},
|
|
480
|
+
async (registry) => {
|
|
481
|
+
const existing = registry.locks[normalizedFilePath] || null;
|
|
482
|
+
if (!existing) {
|
|
483
|
+
return {
|
|
484
|
+
unlocked: false,
|
|
485
|
+
file: normalizedFilePath,
|
|
486
|
+
reason: "not_locked",
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
if (!force && normalizeString(existing.agentId) !== normalizedAgentId) {
|
|
490
|
+
return {
|
|
491
|
+
unlocked: false,
|
|
492
|
+
file: normalizedFilePath,
|
|
493
|
+
reason: "held_by_other_agent",
|
|
494
|
+
heldBy: normalizeString(existing.agentId),
|
|
495
|
+
since: formatSince(existing.lockedAt, normalizedNow),
|
|
496
|
+
lock: presentLock({ ...existing, file: normalizedFilePath }, { nowIso: normalizedNow }),
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
delete registry.locks[normalizedFilePath];
|
|
501
|
+
return {
|
|
502
|
+
unlocked: true,
|
|
503
|
+
file: normalizedFilePath,
|
|
504
|
+
lock: presentLock({ ...existing, file: normalizedFilePath }, { nowIso: normalizedNow }),
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
if (!mutation.result?.unlocked) {
|
|
510
|
+
return {
|
|
511
|
+
...mutation.result,
|
|
512
|
+
expiredEvents: mutation.expiredEvents,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const emittedBy = normalizeAgentId(actorAgentId || (force ? SENTI_AGENT_ID : normalizedAgentId));
|
|
517
|
+
const event = await appendLockEvent(
|
|
518
|
+
sessionId,
|
|
519
|
+
"file_unlock",
|
|
520
|
+
emittedBy,
|
|
521
|
+
{
|
|
522
|
+
file: normalizedFilePath,
|
|
523
|
+
heldBy: normalizeString(mutation.result.lock?.agentId) || normalizedAgentId,
|
|
524
|
+
intent: normalizeString(mutation.result.lock?.intent),
|
|
525
|
+
reason: normalizedReason,
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
targetPath,
|
|
529
|
+
nowIso: normalizedNow,
|
|
530
|
+
}
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
return {
|
|
534
|
+
...mutation.result,
|
|
535
|
+
event,
|
|
536
|
+
expiredEvents: mutation.expiredEvents,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export async function checkFileLock(
|
|
541
|
+
sessionId,
|
|
542
|
+
filePath,
|
|
543
|
+
{
|
|
544
|
+
targetPath = process.cwd(),
|
|
545
|
+
nowIso = new Date().toISOString(),
|
|
546
|
+
emitExpiredEvents = true,
|
|
547
|
+
} = {}
|
|
548
|
+
) {
|
|
549
|
+
const normalizedFilePath = normalizeFilePath(filePath, { targetPath });
|
|
550
|
+
const normalizedNow = normalizeIsoTimestamp(nowIso, new Date().toISOString());
|
|
551
|
+
|
|
552
|
+
const mutation = await mutateRegistry(
|
|
553
|
+
sessionId,
|
|
554
|
+
{
|
|
555
|
+
targetPath,
|
|
556
|
+
nowIso: normalizedNow,
|
|
557
|
+
emitExpiredEvents,
|
|
558
|
+
expiredEventAgentId: SENTI_AGENT_ID,
|
|
559
|
+
},
|
|
560
|
+
async (registry) => {
|
|
561
|
+
const existing = registry.locks[normalizedFilePath] || null;
|
|
562
|
+
if (!existing) {
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
return presentLock({ ...existing, file: normalizedFilePath }, { nowIso: normalizedNow });
|
|
566
|
+
}
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
return mutation.result;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
export async function listFileLocks(
|
|
573
|
+
sessionId,
|
|
574
|
+
{
|
|
575
|
+
targetPath = process.cwd(),
|
|
576
|
+
nowIso = new Date().toISOString(),
|
|
577
|
+
emitExpiredEvents = true,
|
|
578
|
+
} = {}
|
|
579
|
+
) {
|
|
580
|
+
const normalizedNow = normalizeIsoTimestamp(nowIso, new Date().toISOString());
|
|
581
|
+
|
|
582
|
+
const mutation = await mutateRegistry(
|
|
583
|
+
sessionId,
|
|
584
|
+
{
|
|
585
|
+
targetPath,
|
|
586
|
+
nowIso: normalizedNow,
|
|
587
|
+
emitExpiredEvents,
|
|
588
|
+
expiredEventAgentId: SENTI_AGENT_ID,
|
|
589
|
+
},
|
|
590
|
+
async (registry) =>
|
|
591
|
+
Object.entries(registry.locks || {})
|
|
592
|
+
.map(([file, lockRecord]) => presentLock({ ...lockRecord, file }, { nowIso: normalizedNow }))
|
|
593
|
+
.filter(Boolean)
|
|
594
|
+
.sort((left, right) => parseEpoch(left.lockedAt, normalizedNow) - parseEpoch(right.lockedAt, normalizedNow))
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
return mutation.result;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
export async function releaseFileLocksForAgent(
|
|
601
|
+
sessionId,
|
|
602
|
+
agentId,
|
|
603
|
+
{
|
|
604
|
+
reason = "agent_killed",
|
|
605
|
+
targetPath = process.cwd(),
|
|
606
|
+
nowIso = new Date().toISOString(),
|
|
607
|
+
actorAgentId = SENTI_AGENT_ID,
|
|
608
|
+
} = {}
|
|
609
|
+
) {
|
|
610
|
+
const normalizedAgentId = normalizeAgentId(agentId);
|
|
611
|
+
const normalizedNow = normalizeIsoTimestamp(nowIso, new Date().toISOString());
|
|
612
|
+
const normalizedReason = normalizeString(reason) || "agent_killed";
|
|
613
|
+
|
|
614
|
+
const mutation = await mutateRegistry(
|
|
615
|
+
sessionId,
|
|
616
|
+
{
|
|
617
|
+
targetPath,
|
|
618
|
+
nowIso: normalizedNow,
|
|
619
|
+
emitExpiredEvents: true,
|
|
620
|
+
expiredEventAgentId: normalizeAgentId(actorAgentId || SENTI_AGENT_ID),
|
|
621
|
+
},
|
|
622
|
+
async (registry) => {
|
|
623
|
+
const released = [];
|
|
624
|
+
for (const [filePath, lockRecord] of Object.entries(registry.locks || {})) {
|
|
625
|
+
if (normalizeString(lockRecord.agentId) !== normalizedAgentId) {
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
released.push(presentLock({ ...lockRecord, file: filePath }, { nowIso: normalizedNow }));
|
|
629
|
+
delete registry.locks[filePath];
|
|
630
|
+
}
|
|
631
|
+
return released;
|
|
632
|
+
}
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
const events = [];
|
|
636
|
+
const actor = normalizeAgentId(actorAgentId || SENTI_AGENT_ID);
|
|
637
|
+
for (const lockRecord of mutation.result || []) {
|
|
638
|
+
const event = await appendLockEvent(
|
|
639
|
+
sessionId,
|
|
640
|
+
"file_unlock",
|
|
641
|
+
actor,
|
|
642
|
+
{
|
|
643
|
+
file: normalizeString(lockRecord.file),
|
|
644
|
+
heldBy: normalizedAgentId,
|
|
645
|
+
intent: normalizeString(lockRecord.intent),
|
|
646
|
+
reason: normalizedReason,
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
targetPath,
|
|
650
|
+
nowIso: normalizedNow,
|
|
651
|
+
}
|
|
652
|
+
);
|
|
653
|
+
events.push(event);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return {
|
|
657
|
+
releasedCount: events.length,
|
|
658
|
+
released: mutation.result || [],
|
|
659
|
+
events,
|
|
660
|
+
expiredEvents: mutation.expiredEvents,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
export {
|
|
665
|
+
DEFAULT_FILE_LOCK_TTL_SECONDS,
|
|
666
|
+
};
|
package/src/session/paths.js
CHANGED
|
@@ -26,6 +26,10 @@ export function resolveSessionPaths(sessionId, { targetPath = process.cwd() } =
|
|
|
26
26
|
streamPath: path.join(sessionDir, "stream.ndjson"),
|
|
27
27
|
rotatedStreamPath: path.join(sessionDir, "stream.1.ndjson"),
|
|
28
28
|
lockPath: path.join(sessionDir, ".stream.lock"),
|
|
29
|
+
fileLocksPath: path.join(sessionDir, "file-locks.json"),
|
|
30
|
+
fileLocksLockPath: path.join(sessionDir, ".file-locks.lock"),
|
|
31
|
+
tasksPath: path.join(sessionDir, "tasks.json"),
|
|
32
|
+
tasksLockPath: path.join(sessionDir, ".tasks.lock"),
|
|
29
33
|
agentsDir: path.join(sessionDir, "agents"),
|
|
30
34
|
runtimeRunsDir: path.join(sessionDir, "runtime-runs"),
|
|
31
35
|
sentiDir: path.join(sessionDir, "senti"),
|