@indigoai-us/hq-cli 5.264.0 → 5.266.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/dist/command-catalog.generated.d.ts +19 -0
- package/dist/command-catalog.generated.js +25 -0
- package/dist/commands/daemon.d.ts +3 -0
- package/dist/commands/daemon.js +11 -0
- package/dist/commands/lanes-kpis.d.ts +11 -0
- package/dist/commands/lanes-kpis.js +34 -0
- package/dist/commands/lanes-watch.js +2 -1
- package/dist/commands/lanes.js +12 -0
- package/dist/lib/daemon/handoff.d.ts +2 -0
- package/dist/lib/daemon/handoff.js +11 -2
- package/dist/lib/daemon/run.d.ts +19 -0
- package/dist/lib/daemon/run.js +23 -4
- package/dist/lib/daemon/supervisor.d.ts +5 -0
- package/dist/lib/daemon/supervisor.js +1 -0
- package/dist/lib/lanes/config.d.ts +2 -0
- package/dist/lib/lanes/config.js +4 -0
- package/dist/lib/lanes/keep-alive.d.ts +2 -2
- package/dist/lib/lanes/keep-alive.js +9 -6
- package/dist/lib/lanes/kr-freshness.d.ts +76 -0
- package/dist/lib/lanes/kr-freshness.js +412 -0
- package/dist/lib/lanes/lane-company.d.ts +9 -0
- package/dist/lib/lanes/lane-company.js +24 -0
- package/dist/lib/lanes/reconcile.js +57 -6
- package/dist/lib/lanes/types.d.ts +20 -1
- package/dist/lib/lanes/types.js +1 -1
- package/dist/lib/lanes/watcher.d.ts +2 -0
- package/dist/lib/lanes/watcher.js +70 -1
- package/package.json +1 -1
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { DEFAULT_KR_FRESHNESS_HOURS, loadLanesConfig } from "./config.js";
|
|
5
|
+
import { LanesError } from "./errors.js";
|
|
6
|
+
import { resolveManagerStatusTarget } from "./manager-status-gate.js";
|
|
7
|
+
import { readLanesRegistryFlag } from "./registry-flag.js";
|
|
8
|
+
import { assertNoSecretText } from "./text-scan.js";
|
|
9
|
+
export const KR_FRESHNESS_FLAG = "lanes.kr-freshness";
|
|
10
|
+
export const KR_FRESHNESS_DEFAULT_HOURS = DEFAULT_KR_FRESHNESS_HOURS;
|
|
11
|
+
export const KR_FRESHNESS_LEDGER_REL = path.join("workspace", "lanes", "kpi-ledger");
|
|
12
|
+
export const KR_FRESHNESS_LEDGER_SCHEMA = "kr-freshness-ledger.v1";
|
|
13
|
+
export const KR_FRESHNESS_HISTORY_LIMIT = 50;
|
|
14
|
+
const MAX_STATUS_BYTES = 1024 * 1024;
|
|
15
|
+
const MAX_LEDGER_BYTES = 1024 * 1024;
|
|
16
|
+
const MAX_KPIS = 1000;
|
|
17
|
+
const REMOVED_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
18
|
+
function isObject(value) {
|
|
19
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
20
|
+
}
|
|
21
|
+
function fail(code, message, details = {}) {
|
|
22
|
+
throw new LanesError(code, message, details);
|
|
23
|
+
}
|
|
24
|
+
function errorCode(error) {
|
|
25
|
+
return error instanceof LanesError ? error.error : "kr_freshness_unavailable";
|
|
26
|
+
}
|
|
27
|
+
function sha256(value) {
|
|
28
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
29
|
+
}
|
|
30
|
+
function displayName(name) {
|
|
31
|
+
const chars = Array.from(name);
|
|
32
|
+
return chars.length <= 120 ? name : `${chars.slice(0, 119).join("")}…`;
|
|
33
|
+
}
|
|
34
|
+
function safeLaneId(laneId) {
|
|
35
|
+
return /^[A-Za-z0-9._-]{1,200}$/.test(laneId) && laneId !== "." && laneId !== "..";
|
|
36
|
+
}
|
|
37
|
+
function ensureLedgerDirectory(root, create) {
|
|
38
|
+
let current = path.resolve(root);
|
|
39
|
+
const parts = [current];
|
|
40
|
+
for (const segment of ["workspace", "lanes", "kpi-ledger"]) {
|
|
41
|
+
current = path.join(current, segment);
|
|
42
|
+
parts.push(current);
|
|
43
|
+
}
|
|
44
|
+
for (const [index, directory] of parts.entries()) {
|
|
45
|
+
if (create && index > 0) {
|
|
46
|
+
try {
|
|
47
|
+
fs.mkdirSync(directory, { mode: 0o700 });
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (error.code !== "EEXIST") {
|
|
51
|
+
fail("kr_freshness_ledger_write_failed", `Cannot create KR freshness ledger directory: ${directory}`, { path: directory });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
let stat;
|
|
56
|
+
try {
|
|
57
|
+
stat = fs.lstatSync(directory);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (!create && error.code === "ENOENT")
|
|
61
|
+
return false;
|
|
62
|
+
fail("kr_freshness_ledger_unavailable", `Cannot inspect KR freshness ledger directory: ${directory}`, { path: directory });
|
|
63
|
+
}
|
|
64
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
65
|
+
fail("kr_freshness_ledger_invalid", `KR freshness ledger path must not be a symlink: ${directory}`, { path: directory });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
function ledgerFilePath(root, laneId) {
|
|
71
|
+
if (!safeLaneId(laneId))
|
|
72
|
+
fail("kr_freshness_lane_id_invalid", "Lane id cannot be used as a KR freshness ledger filename");
|
|
73
|
+
return path.join(root, KR_FRESHNESS_LEDGER_REL, `${laneId}.json`);
|
|
74
|
+
}
|
|
75
|
+
function validDate(value) {
|
|
76
|
+
return typeof value === "string" && Number.isFinite(Date.parse(value));
|
|
77
|
+
}
|
|
78
|
+
function validateLedger(value, laneId, filePath) {
|
|
79
|
+
if (!isObject(value) || value.schema_version !== KR_FRESHNESS_LEDGER_SCHEMA || value.lane_id !== laneId ||
|
|
80
|
+
!validDate(value.updated_at) || !isObject(value.kpis)) {
|
|
81
|
+
fail("kr_freshness_ledger_invalid", `KR freshness ledger has an invalid shape: ${filePath}`, { path: filePath });
|
|
82
|
+
}
|
|
83
|
+
const keys = Object.keys(value.kpis);
|
|
84
|
+
if (keys.length > MAX_KPIS)
|
|
85
|
+
fail("kr_freshness_ledger_invalid", `KR freshness ledger exceeds ${MAX_KPIS} entries`, { path: filePath });
|
|
86
|
+
const kpis = {};
|
|
87
|
+
for (const key of keys) {
|
|
88
|
+
const row = value.kpis[key];
|
|
89
|
+
if (!/^[a-f0-9]{64}$/.test(key) || !isObject(row) || typeof row.name !== "string" || row.name.trim() === "" ||
|
|
90
|
+
!(typeof row.value === "string" || (typeof row.value === "number" && Number.isFinite(row.value))) ||
|
|
91
|
+
!(row.entry_measured_at === null || typeof row.entry_measured_at === "string") ||
|
|
92
|
+
!validDate(row.first_seen_at) || !validDate(row.measured_at) || !validDate(row.value_changed_at) ||
|
|
93
|
+
(row.removed_at !== undefined && !validDate(row.removed_at))) {
|
|
94
|
+
fail("kr_freshness_ledger_invalid", `KR freshness ledger contains an invalid KPI record: ${filePath}`, { path: filePath });
|
|
95
|
+
}
|
|
96
|
+
assertNoSecretText("KR name", row.name);
|
|
97
|
+
if (typeof row.value === "string")
|
|
98
|
+
assertNoSecretText("KR value", row.value);
|
|
99
|
+
if (typeof row.entry_measured_at === "string")
|
|
100
|
+
assertNoSecretText("KR measured_at", row.entry_measured_at);
|
|
101
|
+
kpis[key] = {
|
|
102
|
+
name: row.name,
|
|
103
|
+
value: row.value,
|
|
104
|
+
entry_measured_at: row.entry_measured_at,
|
|
105
|
+
first_seen_at: row.first_seen_at,
|
|
106
|
+
measured_at: row.measured_at,
|
|
107
|
+
value_changed_at: row.value_changed_at,
|
|
108
|
+
...(typeof row.removed_at === "string" ? { removed_at: row.removed_at } : {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
schema_version: KR_FRESHNESS_LEDGER_SCHEMA,
|
|
113
|
+
lane_id: laneId,
|
|
114
|
+
updated_at: value.updated_at,
|
|
115
|
+
kpis,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export function readKrFreshnessStatus(statusPath) {
|
|
119
|
+
let stat;
|
|
120
|
+
try {
|
|
121
|
+
stat = fs.lstatSync(statusPath);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
fail("kr_freshness_status_unreadable", `Cannot inspect manager status for KR freshness: ${statusPath}`, {
|
|
125
|
+
path: statusPath,
|
|
126
|
+
cause: error.code ?? "unknown",
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_STATUS_BYTES) {
|
|
130
|
+
fail("kr_freshness_status_invalid", `Manager status is not a bounded regular file: ${statusPath}`, { path: statusPath });
|
|
131
|
+
}
|
|
132
|
+
let text;
|
|
133
|
+
try {
|
|
134
|
+
text = fs.readFileSync(statusPath, "utf8");
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
fail("kr_freshness_status_unreadable", `Cannot read manager status for KR freshness: ${statusPath}`, {
|
|
138
|
+
path: statusPath,
|
|
139
|
+
cause: error.code ?? "unknown",
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
if (Buffer.byteLength(text, "utf8") > MAX_STATUS_BYTES) {
|
|
143
|
+
fail("kr_freshness_status_invalid", `Manager status exceeds the KR freshness size limit: ${statusPath}`, { path: statusPath });
|
|
144
|
+
}
|
|
145
|
+
let parsed;
|
|
146
|
+
try {
|
|
147
|
+
parsed = JSON.parse(text);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
fail("kr_freshness_status_invalid", `Manager status contains invalid JSON: ${statusPath}`, { path: statusPath });
|
|
151
|
+
}
|
|
152
|
+
if (!isObject(parsed) || !Array.isArray(parsed.kpis)) {
|
|
153
|
+
fail("kr_freshness_status_invalid", `Manager status must contain a top-level kpis array: ${statusPath}`, { path: statusPath });
|
|
154
|
+
}
|
|
155
|
+
if (parsed.kpis.length > MAX_KPIS) {
|
|
156
|
+
fail("kr_freshness_status_invalid", `Manager status exceeds ${MAX_KPIS} KPIs: ${statusPath}`, { path: statusPath });
|
|
157
|
+
}
|
|
158
|
+
const found = new Map();
|
|
159
|
+
for (const [index, candidate] of parsed.kpis.entries()) {
|
|
160
|
+
if (!isObject(candidate) || typeof candidate.name !== "string" || candidate.name.trim() === "")
|
|
161
|
+
continue;
|
|
162
|
+
const fullName = candidate.name.trim();
|
|
163
|
+
if (Array.from(fullName).some((char) => {
|
|
164
|
+
const codePoint = char.codePointAt(0) ?? 0;
|
|
165
|
+
return codePoint <= 0x1f || codePoint === 0x7f;
|
|
166
|
+
})) {
|
|
167
|
+
fail("kr_freshness_status_invalid", `Manager KPI ${index + 1} name contains control characters: ${statusPath}`, { path: statusPath, index });
|
|
168
|
+
}
|
|
169
|
+
if (!(typeof candidate.value === "string" || (typeof candidate.value === "number" && Number.isFinite(candidate.value)))) {
|
|
170
|
+
fail("kr_freshness_status_invalid", `Manager KPI ${index + 1} needs a string or finite number value: ${statusPath}`, { path: statusPath, index });
|
|
171
|
+
}
|
|
172
|
+
if (candidate.measured_at !== undefined && typeof candidate.measured_at !== "string") {
|
|
173
|
+
fail("kr_freshness_status_invalid", `Manager KPI ${index + 1} measured_at must be a string when present: ${statusPath}`, { path: statusPath, index });
|
|
174
|
+
}
|
|
175
|
+
assertNoSecretText("KR name", fullName);
|
|
176
|
+
if (typeof candidate.value === "string")
|
|
177
|
+
assertNoSecretText("KR value", candidate.value);
|
|
178
|
+
if (typeof candidate.measured_at === "string")
|
|
179
|
+
assertNoSecretText("KR measured_at", candidate.measured_at);
|
|
180
|
+
const key = sha256(fullName);
|
|
181
|
+
if (found.has(key))
|
|
182
|
+
fail("kr_freshness_status_invalid", `Manager status contains duplicate KPI names: ${statusPath}`, { path: statusPath, index });
|
|
183
|
+
found.set(key, {
|
|
184
|
+
key,
|
|
185
|
+
fullName,
|
|
186
|
+
name: displayName(fullName),
|
|
187
|
+
value: candidate.value,
|
|
188
|
+
entryMeasuredAt: typeof candidate.measured_at === "string" ? candidate.measured_at : null,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return [...found.values()];
|
|
192
|
+
}
|
|
193
|
+
export function krFreshnessLedgerPath(root, laneId) {
|
|
194
|
+
return ledgerFilePath(root, laneId);
|
|
195
|
+
}
|
|
196
|
+
export function readKrFreshnessLedger(root, laneId) {
|
|
197
|
+
const filePath = ledgerFilePath(root, laneId);
|
|
198
|
+
if (!ensureLedgerDirectory(root, false))
|
|
199
|
+
return null;
|
|
200
|
+
let stat;
|
|
201
|
+
try {
|
|
202
|
+
stat = fs.lstatSync(filePath);
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (error.code === "ENOENT")
|
|
206
|
+
return null;
|
|
207
|
+
fail("kr_freshness_ledger_unreadable", `Cannot inspect KR freshness ledger: ${filePath}`, { path: filePath });
|
|
208
|
+
}
|
|
209
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_LEDGER_BYTES) {
|
|
210
|
+
fail("kr_freshness_ledger_invalid", `KR freshness ledger is not a bounded regular file: ${filePath}`, { path: filePath });
|
|
211
|
+
}
|
|
212
|
+
let parsed;
|
|
213
|
+
try {
|
|
214
|
+
parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
fail("kr_freshness_ledger_invalid", `KR freshness ledger contains invalid JSON: ${filePath}`, { path: filePath });
|
|
218
|
+
}
|
|
219
|
+
return validateLedger(parsed, laneId, filePath);
|
|
220
|
+
}
|
|
221
|
+
export function writeKrFreshnessLedger(root, ledger) {
|
|
222
|
+
const filePath = ledgerFilePath(root, ledger.lane_id);
|
|
223
|
+
const directory = path.dirname(filePath);
|
|
224
|
+
let tempPath;
|
|
225
|
+
try {
|
|
226
|
+
ensureLedgerDirectory(root, true);
|
|
227
|
+
const data = `${JSON.stringify(ledger, null, 2)}\n`;
|
|
228
|
+
if (Buffer.byteLength(data, "utf8") > MAX_LEDGER_BYTES) {
|
|
229
|
+
fail("kr_freshness_ledger_invalid", `KR freshness ledger exceeds ${MAX_LEDGER_BYTES} bytes`, { path: filePath });
|
|
230
|
+
}
|
|
231
|
+
tempPath = path.join(directory, `.${ledger.lane_id}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
|
|
232
|
+
fs.writeFileSync(tempPath, data, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
233
|
+
fs.renameSync(tempPath, filePath);
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
if (tempPath !== undefined) {
|
|
237
|
+
try {
|
|
238
|
+
fs.unlinkSync(tempPath);
|
|
239
|
+
}
|
|
240
|
+
catch { /* preserve the original write failure */ }
|
|
241
|
+
}
|
|
242
|
+
if (error instanceof LanesError)
|
|
243
|
+
throw error;
|
|
244
|
+
fail("kr_freshness_ledger_write_failed", `Cannot write KR freshness ledger: ${filePath}`, { path: filePath });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
export function updateKrFreshnessLedger(laneId, prior, statusKpis, now) {
|
|
248
|
+
const at = new Date(now).toISOString();
|
|
249
|
+
const next = {};
|
|
250
|
+
const seen = new Set();
|
|
251
|
+
for (const item of statusKpis) {
|
|
252
|
+
const { key, fullName } = item;
|
|
253
|
+
seen.add(key);
|
|
254
|
+
const old = prior?.kpis[key];
|
|
255
|
+
const changed = old === undefined || JSON.stringify(old.value) !== JSON.stringify(item.value);
|
|
256
|
+
const remeasured = old === undefined || old.removed_at !== undefined || old.entry_measured_at !== item.entryMeasuredAt;
|
|
257
|
+
next[key] = {
|
|
258
|
+
name: item.name,
|
|
259
|
+
value: item.value,
|
|
260
|
+
entry_measured_at: item.entryMeasuredAt,
|
|
261
|
+
first_seen_at: old?.first_seen_at ?? at,
|
|
262
|
+
measured_at: changed || remeasured ? at : old.measured_at,
|
|
263
|
+
value_changed_at: changed ? at : old.value_changed_at,
|
|
264
|
+
};
|
|
265
|
+
assertNoSecretText("KR name", fullName);
|
|
266
|
+
}
|
|
267
|
+
for (const [key, old] of Object.entries(prior?.kpis ?? {})) {
|
|
268
|
+
if (seen.has(key))
|
|
269
|
+
continue;
|
|
270
|
+
const removedAt = old.removed_at ?? at;
|
|
271
|
+
if (now - Date.parse(removedAt) >= REMOVED_RETENTION_MS)
|
|
272
|
+
continue;
|
|
273
|
+
next[key] = { ...old, removed_at: removedAt };
|
|
274
|
+
}
|
|
275
|
+
if (Object.keys(next).length > MAX_KPIS) {
|
|
276
|
+
fail("kr_freshness_ledger_invalid", `KR freshness ledger exceeds ${MAX_KPIS} retained entries`);
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
schema_version: KR_FRESHNESS_LEDGER_SCHEMA,
|
|
280
|
+
lane_id: laneId,
|
|
281
|
+
updated_at: at,
|
|
282
|
+
kpis: next,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
export function krFreshnessVerdict(kpi, now, windowHours) {
|
|
286
|
+
const cutoff = now - windowHours * 60 * 60 * 1000;
|
|
287
|
+
if (Date.parse(kpi.value_changed_at) >= cutoff)
|
|
288
|
+
return "moving";
|
|
289
|
+
if (Date.parse(kpi.measured_at) >= cutoff)
|
|
290
|
+
return "flat";
|
|
291
|
+
return "stalled";
|
|
292
|
+
}
|
|
293
|
+
export function krFreshnessFingerprint(keys) {
|
|
294
|
+
return sha256(JSON.stringify([...keys].sort()));
|
|
295
|
+
}
|
|
296
|
+
function viewsFromLedger(ledger, now, windowHours) {
|
|
297
|
+
if (!ledger)
|
|
298
|
+
return [];
|
|
299
|
+
return Object.entries(ledger.kpis)
|
|
300
|
+
.filter(([, kpi]) => kpi.removed_at === undefined)
|
|
301
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
302
|
+
.map(([, kpi]) => ({
|
|
303
|
+
name: kpi.name,
|
|
304
|
+
value: kpi.value,
|
|
305
|
+
verdict: krFreshnessVerdict(kpi, now, windowHours),
|
|
306
|
+
measured_at: kpi.measured_at,
|
|
307
|
+
value_changed_at: kpi.value_changed_at,
|
|
308
|
+
}));
|
|
309
|
+
}
|
|
310
|
+
function reportFailure(lane, error) {
|
|
311
|
+
process.stderr.write(`hq lanes: KR freshness disabled for ${lane.lane_id}: ${errorCode(error)}\n`);
|
|
312
|
+
}
|
|
313
|
+
function failedResolution(error, onError) {
|
|
314
|
+
onError(error);
|
|
315
|
+
return {
|
|
316
|
+
flagEnabled: false,
|
|
317
|
+
enabled: false,
|
|
318
|
+
windowHours: KR_FRESHNESS_DEFAULT_HOURS,
|
|
319
|
+
kpis: [],
|
|
320
|
+
stalledKeys: [],
|
|
321
|
+
error: errorCode(error),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
/** Read the flag before manager mapping, status, configuration or ledger files. */
|
|
325
|
+
export async function resolveKrFreshnessForLane(root, lane, options = {}) {
|
|
326
|
+
const onError = options.onError ?? ((error) => reportFailure(lane, error));
|
|
327
|
+
let flagEnabled;
|
|
328
|
+
try {
|
|
329
|
+
flagEnabled = await (options.readFlag ?? readLanesRegistryFlag)(KR_FRESHNESS_FLAG, {
|
|
330
|
+
company: lane.company ?? lane.mesh_company ?? undefined,
|
|
331
|
+
env: options.env,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
return failedResolution(error, onError);
|
|
336
|
+
}
|
|
337
|
+
if (!flagEnabled) {
|
|
338
|
+
return { flagEnabled: false, enabled: false, windowHours: KR_FRESHNESS_DEFAULT_HOURS, kpis: [], stalledKeys: [] };
|
|
339
|
+
}
|
|
340
|
+
try {
|
|
341
|
+
const target = resolveManagerStatusTarget(root, lane);
|
|
342
|
+
if (!target.manager || target.statusPath === undefined) {
|
|
343
|
+
return {
|
|
344
|
+
flagEnabled: true,
|
|
345
|
+
enabled: true,
|
|
346
|
+
target,
|
|
347
|
+
windowHours: KR_FRESHNESS_DEFAULT_HOURS,
|
|
348
|
+
kpis: [],
|
|
349
|
+
stalledKeys: [],
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
const windowHours = loadLanesConfig(root).kr_freshness_hours;
|
|
353
|
+
const statusKpis = readKrFreshnessStatus(target.statusPath);
|
|
354
|
+
const prior = readKrFreshnessLedger(root, lane.lane_id);
|
|
355
|
+
const ledger = updateKrFreshnessLedger(lane.lane_id, prior, statusKpis, options.now ?? Date.now());
|
|
356
|
+
writeKrFreshnessLedger(root, ledger);
|
|
357
|
+
const effectiveNow = options.now ?? Date.now();
|
|
358
|
+
return {
|
|
359
|
+
flagEnabled: true,
|
|
360
|
+
enabled: true,
|
|
361
|
+
target,
|
|
362
|
+
statusPath: target.statusPath,
|
|
363
|
+
windowHours,
|
|
364
|
+
kpis: viewsFromLedger(ledger, effectiveNow, windowHours),
|
|
365
|
+
stalledKeys: Object.entries(ledger.kpis)
|
|
366
|
+
.filter(([, kpi]) => kpi.removed_at === undefined && krFreshnessVerdict(kpi, effectiveNow, windowHours) === "stalled")
|
|
367
|
+
.map(([key]) => key)
|
|
368
|
+
.sort(),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
return failedResolution(error, onError);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/** Read a previously written ledger for `lanes show`; this never reads status. */
|
|
376
|
+
export async function resolveKrFreshnessForShow(root, lane, options = {}) {
|
|
377
|
+
const onError = options.onError ?? ((error) => reportFailure(lane, error));
|
|
378
|
+
let enabled;
|
|
379
|
+
try {
|
|
380
|
+
enabled = await (options.readFlag ?? readLanesRegistryFlag)(KR_FRESHNESS_FLAG, {
|
|
381
|
+
company: lane.company ?? lane.mesh_company ?? undefined,
|
|
382
|
+
env: options.env,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
onError(error);
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
if (!enabled)
|
|
390
|
+
return null;
|
|
391
|
+
try {
|
|
392
|
+
const windowHours = loadLanesConfig(root).kr_freshness_hours;
|
|
393
|
+
const ledger = readKrFreshnessLedger(root, lane.lane_id);
|
|
394
|
+
return { enabled: true, kpis: viewsFromLedger(ledger, options.now ?? Date.now(), windowHours) };
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
onError(error);
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/** Operator report path: the ledger remains readable while runtime enforcement is off. */
|
|
402
|
+
export function readKrFreshnessReport(root, laneId, now) {
|
|
403
|
+
const ledger = readKrFreshnessLedger(root, laneId);
|
|
404
|
+
const windowHours = loadLanesConfig(root).kr_freshness_hours;
|
|
405
|
+
return {
|
|
406
|
+
ledger_path: krFreshnessLedgerPath(root, laneId),
|
|
407
|
+
available: ledger !== null,
|
|
408
|
+
kr_freshness_hours: windowHours,
|
|
409
|
+
kpis: viewsFromLedger(ledger, now, windowHours),
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
//# sourceMappingURL=kr-freshness.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LaneCompanyRef, LaneRecord } from "./types.js";
|
|
2
|
+
export type LaneCompanySource = "lane" | "project" | "unresolved";
|
|
3
|
+
export interface LaneCompanyResolution {
|
|
4
|
+
company: LaneCompanyRef | null;
|
|
5
|
+
source: LaneCompanySource;
|
|
6
|
+
}
|
|
7
|
+
/** Resolve a lane's company without inferring ownership from its worker or caller. */
|
|
8
|
+
export declare function resolveLaneCompany(hqRoot: string, lane: Pick<LaneRecord, "lane_id" | "company" | "project_id">): LaneCompanyResolution;
|
|
9
|
+
//# sourceMappingURL=lane-company.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { resolveProjectQuestionCompany } from "./questions.js";
|
|
2
|
+
import { LanesError } from "./errors.js";
|
|
3
|
+
/** Resolve a lane's company without inferring ownership from its worker or caller. */
|
|
4
|
+
export function resolveLaneCompany(hqRoot, lane) {
|
|
5
|
+
const slug = typeof lane.company?.slug === "string" ? lane.company.slug.trim() : "";
|
|
6
|
+
const uid = typeof lane.company?.uid === "string" ? lane.company.uid.trim() : "";
|
|
7
|
+
if (slug && uid) {
|
|
8
|
+
return { company: { slug, uid }, source: "lane" };
|
|
9
|
+
}
|
|
10
|
+
let projectCompany;
|
|
11
|
+
try {
|
|
12
|
+
projectCompany = resolveProjectQuestionCompany(hqRoot, lane);
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
if (err instanceof LanesError && err.error === "question_company_unavailable") {
|
|
16
|
+
return { company: null, source: "unresolved" };
|
|
17
|
+
}
|
|
18
|
+
throw err;
|
|
19
|
+
}
|
|
20
|
+
return projectCompany
|
|
21
|
+
? { company: projectCompany, source: "project" }
|
|
22
|
+
: { company: null, source: "unresolved" };
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=lane-company.js.map
|
|
@@ -22,6 +22,7 @@ import { ANSWERED_QUESTIONS_FILE, armLaneWatcher, envelopeRepairMode, isPidRecor
|
|
|
22
22
|
import { appendCappedHistory, lanesStoreRoot, parseLaneRecord, parseQuestionRecord, readLane, readQuestion, updateLane, updateQuestion, } from "./store.js";
|
|
23
23
|
import { applyThrottleDecision, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled, readAvailableMemMb, readThrottleState, resolveCpuBusyFraction, throttleStatePath, writeThrottleState, } from "./throttle.js";
|
|
24
24
|
import { readLanesRegistryFlag } from "./registry-flag.js";
|
|
25
|
+
import { resolveLaneCompany } from "./lane-company.js";
|
|
25
26
|
import { LEAD_LANE_KEEP_ALIVE_FLAG, LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT, LEAD_LANE_KEEP_ALIVE_MAX_RESUMES, LEAD_LANE_KEEP_ALIVE_RESUME_TEXT, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, doneKeepAliveVerdict, hasPendingKeepAliveWork, keepAliveExclusion, loadRetiredLaneIds, leadLaneKeepAliveBackoffMs, undeliveredAnsweredQuestions, } from "./keep-alive.js";
|
|
26
27
|
import { providerSupportsSessionResume } from "./delivery.js";
|
|
27
28
|
import { applyDefaultQuotaHold, readQuotaSignal, recordAccountQuota } from "./quota.js";
|
|
@@ -851,8 +852,11 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
851
852
|
if (doneKeepAliveVerdict(lane, envelope, hasQueuedWork) !== "eligible")
|
|
852
853
|
continue;
|
|
853
854
|
}
|
|
854
|
-
const
|
|
855
|
-
|
|
855
|
+
const company = resolveLaneCompany(hqRoot, lane).company;
|
|
856
|
+
if (company === null)
|
|
857
|
+
continue;
|
|
858
|
+
const key = keepAliveCompanyKey(company);
|
|
859
|
+
keepAliveFlagCompanies.set(key, company);
|
|
856
860
|
}
|
|
857
861
|
const keepAliveFlagByCompany = new Map();
|
|
858
862
|
const supersessionLineageAwareByCompany = new Map();
|
|
@@ -1598,6 +1602,15 @@ function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCom
|
|
|
1598
1602
|
const nowMs = now.getTime();
|
|
1599
1603
|
const nowIso = now.toISOString();
|
|
1600
1604
|
const config = loadLanesConfig(hqRoot);
|
|
1605
|
+
const resolvedCompanyByLaneVersion = new Map();
|
|
1606
|
+
const companyForLane = (candidate) => {
|
|
1607
|
+
const key = `${candidate.lane_id}\u0000${candidate.version}`;
|
|
1608
|
+
if (resolvedCompanyByLaneVersion.has(key))
|
|
1609
|
+
return resolvedCompanyByLaneVersion.get(key) ?? null;
|
|
1610
|
+
const company = resolveLaneCompany(hqRoot, candidate).company;
|
|
1611
|
+
resolvedCompanyByLaneVersion.set(key, company);
|
|
1612
|
+
return company;
|
|
1613
|
+
};
|
|
1601
1614
|
const keepAliveEnabled = [...flagByCompany.values()].some((enabled) => enabled);
|
|
1602
1615
|
let retiredLaneIds = null;
|
|
1603
1616
|
if (keepAliveEnabled) {
|
|
@@ -1615,7 +1628,7 @@ function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCom
|
|
|
1615
1628
|
originalLane.state === "cancelled" ||
|
|
1616
1629
|
originalLane.state === "failed") {
|
|
1617
1630
|
if (originalLane.keep_alive === true &&
|
|
1618
|
-
flagByCompany.get(keepAliveCompanyKey(originalLane
|
|
1631
|
+
flagByCompany.get(keepAliveCompanyKey(companyForLane(originalLane))) === true)
|
|
1619
1632
|
recordKeepAliveSkip(hqRoot, originalLane, "terminal_state", summary, deps);
|
|
1620
1633
|
continue;
|
|
1621
1634
|
}
|
|
@@ -1641,8 +1654,28 @@ function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCom
|
|
|
1641
1654
|
continue;
|
|
1642
1655
|
}
|
|
1643
1656
|
}
|
|
1644
|
-
|
|
1657
|
+
const company = companyForLane(lane);
|
|
1658
|
+
if (company === null) {
|
|
1659
|
+
if (lane.keep_alive === true) {
|
|
1660
|
+
summary.skipped.push({
|
|
1661
|
+
kind: "lane",
|
|
1662
|
+
id: lane.lane_id,
|
|
1663
|
+
why: "keep_alive_company_unresolved",
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1645
1666
|
continue;
|
|
1667
|
+
}
|
|
1668
|
+
const companyKey = keepAliveCompanyKey(company);
|
|
1669
|
+
if (flagByCompany.get(companyKey) !== true) {
|
|
1670
|
+
if (lane.keep_alive === true) {
|
|
1671
|
+
summary.skipped.push({
|
|
1672
|
+
kind: "lane",
|
|
1673
|
+
id: lane.lane_id,
|
|
1674
|
+
why: "keep_alive_flag_off",
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
continue;
|
|
1678
|
+
}
|
|
1646
1679
|
// Persist a stable timestamp for legacy interrupted rows before a later
|
|
1647
1680
|
// keep-alive status write can advance their general updated_at clock. The
|
|
1648
1681
|
// watcher classification is the interruption time; updated_at is only an
|
|
@@ -1760,7 +1793,7 @@ function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCom
|
|
|
1760
1793
|
recordKeepAliveSkip(hqRoot, lane, "retired_list_invalid", summary, deps);
|
|
1761
1794
|
continue;
|
|
1762
1795
|
}
|
|
1763
|
-
const exclusion = keepAliveExclusion(lane, lanes, retiredLaneIds, supersessionLineageAwareByCompany.get(
|
|
1796
|
+
const exclusion = keepAliveExclusion(lane, lanes, retiredLaneIds, supersessionLineageAwareByCompany.get(companyKey) === true, companyForLane);
|
|
1764
1797
|
if (exclusion !== undefined) {
|
|
1765
1798
|
recordKeepAliveSkip(hqRoot, lane, exclusion, summary, deps);
|
|
1766
1799
|
continue;
|
|
@@ -1851,7 +1884,25 @@ function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCom
|
|
|
1851
1884
|
continue;
|
|
1852
1885
|
}
|
|
1853
1886
|
const currentLanes = loadLaneRecords(hqRoot, summary);
|
|
1854
|
-
const
|
|
1887
|
+
const currentCompany = companyForLane(current);
|
|
1888
|
+
if (currentCompany === null) {
|
|
1889
|
+
summary.skipped.push({
|
|
1890
|
+
kind: "lane",
|
|
1891
|
+
id: current.lane_id,
|
|
1892
|
+
why: "keep_alive_company_unresolved",
|
|
1893
|
+
});
|
|
1894
|
+
continue;
|
|
1895
|
+
}
|
|
1896
|
+
const currentCompanyKey = keepAliveCompanyKey(currentCompany);
|
|
1897
|
+
if (flagByCompany.get(currentCompanyKey) !== true) {
|
|
1898
|
+
summary.skipped.push({
|
|
1899
|
+
kind: "lane",
|
|
1900
|
+
id: current.lane_id,
|
|
1901
|
+
why: "keep_alive_flag_off",
|
|
1902
|
+
});
|
|
1903
|
+
continue;
|
|
1904
|
+
}
|
|
1905
|
+
const currentExclusion = keepAliveExclusion(current, currentLanes, retiredLaneIds, supersessionLineageAwareByCompany.get(currentCompanyKey) === true, companyForLane);
|
|
1855
1906
|
if (currentExclusion !== undefined) {
|
|
1856
1907
|
recordKeepAliveSkip(hqRoot, current, currentExclusion, summary, deps);
|
|
1857
1908
|
continue;
|
|
@@ -370,7 +370,22 @@ export interface BlockerFreshnessHistoryEntry {
|
|
|
370
370
|
resume_skipped_reason?: "open_question" | "blocked" | "miss_limit" | "no_provider_session";
|
|
371
371
|
delivery_failed?: true;
|
|
372
372
|
}
|
|
373
|
-
export
|
|
373
|
+
export interface KrFreshnessState {
|
|
374
|
+
/** Stable fingerprint of the last stalled KPI set, which is resumed once. */
|
|
375
|
+
stalled_fingerprint?: string;
|
|
376
|
+
}
|
|
377
|
+
export interface KrFreshnessHistoryEntry {
|
|
378
|
+
event: "kr_stalled";
|
|
379
|
+
turn_id: string;
|
|
380
|
+
path: string;
|
|
381
|
+
at: string;
|
|
382
|
+
names: string[];
|
|
383
|
+
count: number;
|
|
384
|
+
fingerprint: string;
|
|
385
|
+
resume_queued?: boolean;
|
|
386
|
+
resume_skipped_reason?: "open_question" | "blocked" | "miss_limit" | "no_provider_session";
|
|
387
|
+
}
|
|
388
|
+
export type LaneGateHistoryEntry = ManagerStatusGateHistoryEntry | BlockerFreshnessHistoryEntry | KrFreshnessHistoryEntry;
|
|
374
389
|
export interface LaneStory {
|
|
375
390
|
project_id: string;
|
|
376
391
|
story_id: string;
|
|
@@ -516,6 +531,8 @@ export interface LaneRecord {
|
|
|
516
531
|
status_gate_history?: LaneGateHistoryEntry[];
|
|
517
532
|
/** Last blocker-freshness state used to detect consecutive stale turns. */
|
|
518
533
|
blocker_freshness_state?: BlockerFreshnessState;
|
|
534
|
+
/** Last stalled KR set already offered one automatic resume. */
|
|
535
|
+
kr_freshness_state?: KrFreshnessState;
|
|
519
536
|
/** Last automatic compaction of a resumed session (compaction.ts). */
|
|
520
537
|
last_compaction?: LaneCompactionRecord;
|
|
521
538
|
/**
|
|
@@ -857,6 +874,8 @@ export interface LanesConfig {
|
|
|
857
874
|
session_question_timeout_secs: number;
|
|
858
875
|
/** Age in hours after which an open manager blocker must be re-verified. */
|
|
859
876
|
blocker_freshness_hours: number;
|
|
877
|
+
/** Age in hours after which a manager KPI is considered stalled. */
|
|
878
|
+
kr_freshness_hours: number;
|
|
860
879
|
}
|
|
861
880
|
export declare function parseMajorSchema(schemaVersion: string, expectedPrefix: string, expectedMajor: number): void;
|
|
862
881
|
export declare function parseLaneEnvelope(raw: unknown): LaneEnvelope;
|
package/dist/lib/lanes/types.js
CHANGED
|
@@ -228,7 +228,7 @@ export const LANE_FIELD_WRITERS = {
|
|
|
228
228
|
provider: ["creator"], account: ["creator", "spawn", "command", "watcher", "reconcile"], account_selection: ["creator", "command"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], failed_resume_history: ["command"], pending_resume: ["command", "spawn", "watcher", "reconcile"], placement_default: ["creator"], host: ["creator", "command"], origin_host: ["creator"], origin_host_source: ["creator"],
|
|
229
229
|
version: ["creator"], created_at: ["creator"], updated_at: ["creator"], attribution_history: ["command"],
|
|
230
230
|
last_envelope: ["watcher", "reconcile"], model: ["creator", "spawn", "command"], model_source: ["creator", "spawn", "command"], effort: ["creator", "spawn", "command"], effort_source: ["creator", "spawn", "command"], fast_mode: ["spawn"],
|
|
231
|
-
last_message_at: ["command"], status_gate: ["spawn", "watcher"], status_gate_history: ["watcher"], blocker_freshness_state: ["watcher"], last_compaction: ["spawn"], story_complete: ["watcher", "reconcile"], story_complete_reason: ["watcher", "reconcile"], story_status: ["watcher", "reconcile"],
|
|
231
|
+
last_message_at: ["command"], status_gate: ["spawn", "watcher"], status_gate_history: ["watcher"], blocker_freshness_state: ["watcher"], kr_freshness_state: ["watcher"], last_compaction: ["spawn"], story_complete: ["watcher", "reconcile"], story_complete_reason: ["watcher", "reconcile"], story_status: ["watcher", "reconcile"],
|
|
232
232
|
overdue: ["reconcile"], overdue_at: ["reconcile"], last_host_report_at: ["watcher", "spawn", "reconcile"], stalled: ["watcher"], stalled_at: ["watcher"], stalled_reason: ["watcher"],
|
|
233
233
|
admitted_reason: ["command", "reconcile"], terminal_reason: ["command", "watcher", "spawn", "reconcile"], stop_history: ["command"], interrupted_at: ["watcher", "reconcile"], terminal_details: ["command", "spawn", "watcher", "reconcile"],
|
|
234
234
|
quota_reset_at: ["watcher", "reconcile"], model_quota_marker: ["watcher", "reconcile"], outpost_command_id: ["command"],
|
|
@@ -233,6 +233,8 @@ export interface WatcherLoopDeps {
|
|
|
233
233
|
readManagerStatusGateFlag?: typeof readLanesRegistryFlag;
|
|
234
234
|
/** Test seam; production reads the company-scoped hq-flags row. */
|
|
235
235
|
readBlockerFreshnessFlag?: typeof readLanesRegistryFlag;
|
|
236
|
+
/** Test seam; production reads the company-scoped hq-flags row. */
|
|
237
|
+
readKrFreshnessFlag?: typeof readLanesRegistryFlag;
|
|
236
238
|
}
|
|
237
239
|
/**
|
|
238
240
|
* Wrap one mesh call. A failed emit is logged as `mesh_event_failed` with
|