@indigoai-us/hq-cli 5.281.0 → 5.283.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 +37 -0
- package/assets/scaffold/.claude/hooks/validate-policy-frontmatter.sh +485 -0
- package/assets/scaffold/core/scripts/eval-trigger.sh +193 -0
- package/assets/scaffold/core/scripts/policy-age-report.sh +156 -0
- package/assets/scaffold/core/scripts/policy-retire.sh +199 -0
- package/assets/scaffold/core/scripts/read-policy-frontmatter.sh +32 -0
- package/dist/command-catalog.generated.d.ts +67 -0
- package/dist/command-catalog.generated.js +83 -0
- package/dist/commands/core-knowledge-prefs.d.ts +10 -0
- package/dist/commands/core-knowledge-prefs.js +125 -0
- package/dist/commands/core-native-policy-flag.d.ts +9 -0
- package/dist/commands/core-native-policy-flag.js +12 -0
- package/dist/commands/core-ontology-native.d.ts +0 -1
- package/dist/commands/core-ontology-native.js +2 -30
- package/dist/commands/core-ontology.d.ts +4 -0
- package/dist/commands/core-ontology.js +8 -3
- package/dist/commands/core-policy-frontmatter.d.ts +8 -0
- package/dist/commands/core-policy-frontmatter.js +52 -0
- package/dist/commands/core-policy.d.ts +15 -0
- package/dist/commands/core-policy.js +982 -0
- package/dist/commands/core.js +14 -1
- package/dist/commands/lanes-message.js +1 -0
- package/dist/commands/lanes-move.js +2 -0
- package/dist/commands/lanes-resume.js +1 -0
- package/dist/commands/lanes-shared.d.ts +10 -1
- package/dist/commands/lanes-shared.js +2 -1
- package/dist/commands/lanes.d.ts +8 -0
- package/dist/commands/lanes.js +324 -93
- package/dist/fast-lanes-list-route.d.ts +8 -0
- package/dist/fast-lanes-list-route.js +30 -0
- package/dist/fast-lanes-list.d.ts +3 -0
- package/dist/fast-lanes-list.js +252 -0
- package/dist/fast-scaffold.d.ts +5 -0
- package/dist/fast-scaffold.js +10 -0
- package/dist/index.js +10 -1
- package/dist/lib/checkpoint-ontology/merge-copies.d.ts +24 -1
- package/dist/lib/checkpoint-ontology/merge-copies.js +70 -1
- package/dist/lib/lanes/flag-keys.d.ts +3 -0
- package/dist/lib/lanes/flag-keys.js +3 -0
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/keep-alive.js +1 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +1 -1
- package/dist/lib/lanes/outpost-dispatch.js +4 -2
- package/dist/lib/lanes/outpost-hire-cache.d.ts +3 -0
- package/dist/lib/lanes/outpost-hire-cache.js +11 -3
- package/dist/lib/lanes/outpost-snapshot-cache.d.ts +26 -0
- package/dist/lib/lanes/outpost-snapshot-cache.js +215 -0
- package/dist/lib/lanes/reconcile.d.ts +7 -1
- package/dist/lib/lanes/reconcile.js +88 -16
- package/dist/lib/lanes/spawn.js +5 -0
- package/dist/lib/lanes/store.d.ts +4 -0
- package/dist/lib/lanes/store.js +66 -0
- package/dist/lib/lanes/throttle.d.ts +29 -2
- package/dist/lib/lanes/throttle.js +145 -5
- package/dist/lib/lanes/types.d.ts +8 -1
- package/dist/lib/lanes/types.js +2 -1
- package/dist/lib/lanes/watcher.js +1 -0
- package/dist/lib/policy-validator.d.ts +2 -0
- package/dist/lib/policy-validator.js +136 -0
- package/dist/native-fast-route.d.ts +2 -0
- package/dist/native-fast-route.js +6 -2
- package/dist/utils/contribution-table.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface FastLanesListRouteDeps {
|
|
2
|
+
flagEnabled: (flagKey: string, env: NodeJS.ProcessEnv) => Promise<boolean>;
|
|
3
|
+
runFastList: () => Promise<void>;
|
|
4
|
+
runPlacementList: () => Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
/** Returns false unless this exact list form is safely enabled in the local flag snapshot. */
|
|
7
|
+
export declare function runFastLanesListRequest(argv: readonly string[], env?: NodeJS.ProcessEnv, deps?: FastLanesListRouteDeps): Promise<boolean>;
|
|
8
|
+
//# sourceMappingURL=fast-lanes-list-route.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { isFastCachedLanesListRequest } from "./fast-scaffold.js";
|
|
2
|
+
import { fastFlagEnabled } from "./native-fast-route.js";
|
|
3
|
+
import { LANE_OUTPOST_SNAPSHOT_CACHE_FLAG } from "./lib/lanes/outpost-hire-cache.js";
|
|
4
|
+
import { LANE_OUTPOST_LOAD_BALANCING_FLAG } from "./lib/lanes/flag-keys.js";
|
|
5
|
+
/** Returns false unless this exact list form is safely enabled in the local flag snapshot. */
|
|
6
|
+
export async function runFastLanesListRequest(argv, env = process.env, deps = {
|
|
7
|
+
flagEnabled: fastFlagEnabled,
|
|
8
|
+
runFastList: async () => {
|
|
9
|
+
const { runFastCachedLanesList } = await import("./fast-lanes-list.js");
|
|
10
|
+
await runFastCachedLanesList();
|
|
11
|
+
},
|
|
12
|
+
runPlacementList: async () => {
|
|
13
|
+
const { runFastCachedLanesListFromEntrypoint } = await import("./commands/lanes.js");
|
|
14
|
+
await runFastCachedLanesListFromEntrypoint();
|
|
15
|
+
},
|
|
16
|
+
}) {
|
|
17
|
+
if (!isFastCachedLanesListRequest(argv))
|
|
18
|
+
return false;
|
|
19
|
+
if (env.HQ_INTERNAL_LANES_REFRESH_REMOTE_SNAPSHOT === "1")
|
|
20
|
+
return false;
|
|
21
|
+
if (!await deps.flagEnabled(LANE_OUTPOST_SNAPSHOT_CACHE_FLAG, env))
|
|
22
|
+
return false;
|
|
23
|
+
const lanePlacementEnabled = await deps.flagEnabled(LANE_OUTPOST_LOAD_BALANCING_FLAG, env);
|
|
24
|
+
if (lanePlacementEnabled)
|
|
25
|
+
await deps.runPlacementList();
|
|
26
|
+
else
|
|
27
|
+
await deps.runFastList();
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=fast-lanes-list-route.js.map
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { compareLaneQueueOrder, laneRunsOnHost } from "./lib/lanes/admission.js";
|
|
6
|
+
import { readOutpostIdentity, resolveAdmissionHost } from "./lib/lanes/admission-host.js";
|
|
7
|
+
import { LaneSchemaError, isLanesError } from "./lib/lanes/errors.js";
|
|
8
|
+
import { OUTPOST_HIRE_SUMMARY_CACHE_TTL_MS, outpostHireSummaryCacheKey, readOutpostHireSummaryCacheIncludingStale, } from "./lib/lanes/outpost-hire-cache.js";
|
|
9
|
+
import { resolveLanesHqRoot } from "./lib/lanes/hq-root.js";
|
|
10
|
+
import { laneStories } from "./lib/lanes/stories.js";
|
|
11
|
+
import { readAllLanes } from "./lib/lanes/store.js";
|
|
12
|
+
import { isTerminalLaneState } from "./lib/lanes/types.js";
|
|
13
|
+
const LANES_RUNS_REL = path.join("workspace", "lanes-runs");
|
|
14
|
+
function withoutProviderHome(value) {
|
|
15
|
+
if (Array.isArray(value))
|
|
16
|
+
return value.map(withoutProviderHome);
|
|
17
|
+
if (value === null || typeof value !== "object")
|
|
18
|
+
return value;
|
|
19
|
+
const safe = {};
|
|
20
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
21
|
+
if (key === "provider_home_at_spawn")
|
|
22
|
+
continue;
|
|
23
|
+
safe[key] = withoutProviderHome(nested);
|
|
24
|
+
}
|
|
25
|
+
return safe;
|
|
26
|
+
}
|
|
27
|
+
function printJson(value) {
|
|
28
|
+
console.log(JSON.stringify(withoutProviderHome(value), null, 2));
|
|
29
|
+
}
|
|
30
|
+
function errorMessage(err) {
|
|
31
|
+
return err instanceof Error ? err.message : String(err);
|
|
32
|
+
}
|
|
33
|
+
function failJson(err) {
|
|
34
|
+
if (isLanesError(err)) {
|
|
35
|
+
if (err.error === "hire_grant_exhausted") {
|
|
36
|
+
process.stderr.write(`hq lanes: ${err.message}\n`);
|
|
37
|
+
}
|
|
38
|
+
printJson({ ...err.toJSON(), message: err.message });
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
const message = errorMessage(err);
|
|
42
|
+
process.stderr.write(`hq lanes: ${message}${err instanceof Error && err.stack ? `\n${err.stack}` : ""}\n`);
|
|
43
|
+
printJson({ error: "internal", message });
|
|
44
|
+
}
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
}
|
|
47
|
+
function laneHireFields(lane, aliveChildrenByParent) {
|
|
48
|
+
const aliveChildCount = aliveChildrenByParent.get(lane.lane_id) ?? 0;
|
|
49
|
+
const hireGrant = lane.hire_grant ?? 0;
|
|
50
|
+
return {
|
|
51
|
+
hire_grant: hireGrant,
|
|
52
|
+
hire_alive_children: aliveChildCount,
|
|
53
|
+
hire: `${aliveChildCount}/${hireGrant}`,
|
|
54
|
+
hire_summary: `hire ${aliveChildCount}/${hireGrant}`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function isOverdue(hqRoot, laneId, now) {
|
|
58
|
+
const filePath = path.join(hqRoot, LANES_RUNS_REL, laneId, "timeout.json");
|
|
59
|
+
let text;
|
|
60
|
+
try {
|
|
61
|
+
text = fs.readFileSync(filePath, "utf8");
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
if (err.code === "ENOENT")
|
|
65
|
+
return false;
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
let raw;
|
|
69
|
+
try {
|
|
70
|
+
raw = JSON.parse(text);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
throw new LaneSchemaError(`Invalid JSON in ${filePath}: ${errorMessage(err)}`, { path: filePath });
|
|
74
|
+
}
|
|
75
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
76
|
+
return false;
|
|
77
|
+
const deadline = raw.deadline_at;
|
|
78
|
+
if (typeof deadline !== "string")
|
|
79
|
+
return false;
|
|
80
|
+
const at = Date.parse(deadline);
|
|
81
|
+
if (!Number.isFinite(at))
|
|
82
|
+
return false;
|
|
83
|
+
return now.getTime() > at;
|
|
84
|
+
}
|
|
85
|
+
function startOutpostLaneSnapshotRefresh(root, env) {
|
|
86
|
+
let entry;
|
|
87
|
+
try {
|
|
88
|
+
const currentEntry = process.argv[1];
|
|
89
|
+
if (!currentEntry)
|
|
90
|
+
throw new Error("process.argv[1] is empty");
|
|
91
|
+
entry = fs.realpathSync(currentEntry);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
process.stderr.write(`lanes snapshot refresh could not resolve the running CLI entry: ${errorMessage(err)}\n`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const child = spawn(process.execPath, [...process.execArgv, entry, "lanes", "list"], {
|
|
99
|
+
cwd: root,
|
|
100
|
+
detached: true,
|
|
101
|
+
stdio: ["ignore", "ignore", "inherit"],
|
|
102
|
+
env: { ...env, HQ_ROOT: root, HQ_NO_UPDATE_CHECK: "1", HQ_INTERNAL_LANES_REFRESH_REMOTE_SNAPSHOT: "1" },
|
|
103
|
+
});
|
|
104
|
+
child.once("error", (err) => {
|
|
105
|
+
process.stderr.write(`lanes snapshot refresh could not start: ${errorMessage(err)}\n`);
|
|
106
|
+
});
|
|
107
|
+
child.unref();
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
process.stderr.write(`lanes snapshot refresh could not start: ${errorMessage(err)}\n`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function isOwnerLane(lane, host) {
|
|
114
|
+
return lane.host.kind === "outpost" &&
|
|
115
|
+
!isTerminalLaneState(lane.state) &&
|
|
116
|
+
!laneRunsOnHost(lane, host);
|
|
117
|
+
}
|
|
118
|
+
async function buildLaneList(hqRoot, sourceLanes, host, now, env) {
|
|
119
|
+
const selectedLanes = sourceLanes.sort(compareLaneQueueOrder);
|
|
120
|
+
const aliveChildrenByParent = new Map();
|
|
121
|
+
for (const child of selectedLanes) {
|
|
122
|
+
if (child.senior.kind !== "lane" || !child.senior.id || isTerminalLaneState(child.state))
|
|
123
|
+
continue;
|
|
124
|
+
aliveChildrenByParent.set(child.senior.id, (aliveChildrenByParent.get(child.senior.id) ?? 0) + 1);
|
|
125
|
+
}
|
|
126
|
+
const ownerLanes = selectedLanes.filter((lane) => isOwnerLane(lane, host));
|
|
127
|
+
const hireByLane = new Map();
|
|
128
|
+
const remoteSnapshotMetadata = new Map();
|
|
129
|
+
let remoteLanesLoading = false;
|
|
130
|
+
for (const lane of selectedLanes) {
|
|
131
|
+
if (!isOwnerLane(lane, host)) {
|
|
132
|
+
hireByLane.set(lane.lane_id, {
|
|
133
|
+
...laneHireFields(lane, aliveChildrenByParent),
|
|
134
|
+
hire_source: "local",
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (ownerLanes.length > 0) {
|
|
139
|
+
const nowMs = now.getTime();
|
|
140
|
+
const cached = readOutpostHireSummaryCacheIncludingStale(hqRoot, nowMs);
|
|
141
|
+
let refreshNeeded = false;
|
|
142
|
+
for (const lane of ownerLanes) {
|
|
143
|
+
if (lane.host.kind !== "outpost")
|
|
144
|
+
continue;
|
|
145
|
+
const summary = cached.get(outpostHireSummaryCacheKey(lane.host.id, lane.lane_id));
|
|
146
|
+
if (!summary) {
|
|
147
|
+
hireByLane.set(lane.lane_id, {
|
|
148
|
+
...laneHireFields(lane, aliveChildrenByParent),
|
|
149
|
+
hire_source: "local",
|
|
150
|
+
});
|
|
151
|
+
remoteSnapshotMetadata.set(lane.lane_id, { ageSeconds: null, stale: true, loading: true });
|
|
152
|
+
refreshNeeded = true;
|
|
153
|
+
remoteLanesLoading = true;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const ageMs = Math.max(0, nowMs - Date.parse(summary.cached_at));
|
|
157
|
+
const stale = ageMs >= OUTPOST_HIRE_SUMMARY_CACHE_TTL_MS;
|
|
158
|
+
hireByLane.set(lane.lane_id, {
|
|
159
|
+
hire_grant: summary.hire_grant,
|
|
160
|
+
hire_alive_children: summary.hire_alive_children,
|
|
161
|
+
hire: summary.hire,
|
|
162
|
+
hire_summary: summary.hire_summary,
|
|
163
|
+
hire_source: "owner",
|
|
164
|
+
});
|
|
165
|
+
remoteSnapshotMetadata.set(lane.lane_id, {
|
|
166
|
+
ageSeconds: Math.floor(ageMs / 1_000),
|
|
167
|
+
stale,
|
|
168
|
+
loading: stale,
|
|
169
|
+
});
|
|
170
|
+
if (stale) {
|
|
171
|
+
refreshNeeded = true;
|
|
172
|
+
remoteLanesLoading = true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (refreshNeeded)
|
|
176
|
+
startOutpostLaneSnapshotRefresh(hqRoot, env);
|
|
177
|
+
}
|
|
178
|
+
const sessions = selectedLanes.filter((lane) => lane.state === "running" && lane.senior.kind === "session" && Boolean(lane.senior.id));
|
|
179
|
+
let sessionLiveness;
|
|
180
|
+
let livenessDeps;
|
|
181
|
+
if (sessions.length > 0) {
|
|
182
|
+
const liveness = await import("./lib/lanes/liveness.js");
|
|
183
|
+
sessionLiveness = liveness.sessionLiveness;
|
|
184
|
+
const base = liveness.defaultLivenessDeps();
|
|
185
|
+
livenessDeps = {
|
|
186
|
+
...base,
|
|
187
|
+
now: () => now.getTime(),
|
|
188
|
+
homedir: () => os.homedir(),
|
|
189
|
+
env,
|
|
190
|
+
existsSync: (file) => fs.existsSync(file),
|
|
191
|
+
readFileSync: (file, encoding) => fs.readFileSync(file, encoding),
|
|
192
|
+
hostname: () => os.hostname(),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const livenessBySession = new Map();
|
|
196
|
+
const lanes = selectedLanes.map((lane) => {
|
|
197
|
+
const stories = laneStories(lane);
|
|
198
|
+
const remoteQueued = lane.state === "queued" && lane.host.kind === "outpost" && !laneRunsOnHost(lane, host);
|
|
199
|
+
let seniorLost = false;
|
|
200
|
+
if (lane.state === "running" && lane.senior.kind === "session" && lane.senior.id && sessionLiveness && livenessDeps) {
|
|
201
|
+
const sessionId = lane.senior.id;
|
|
202
|
+
const cached = livenessBySession.get(sessionId);
|
|
203
|
+
if (cached !== undefined)
|
|
204
|
+
seniorLost = cached;
|
|
205
|
+
else {
|
|
206
|
+
const state = sessionLiveness(hqRoot, sessionId, {
|
|
207
|
+
nowMs: now.getTime(),
|
|
208
|
+
deps: livenessDeps,
|
|
209
|
+
});
|
|
210
|
+
seniorLost = state === null || state === "ended" || state === "lost";
|
|
211
|
+
livenessBySession.set(sessionId, seniorLost);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
...lane,
|
|
216
|
+
...hireByLane.get(lane.lane_id),
|
|
217
|
+
stories,
|
|
218
|
+
story_ids: stories.map((story) => story.story_id),
|
|
219
|
+
no_story_reason: lane.no_story_reason ?? null,
|
|
220
|
+
...(remoteQueued ? { state: "remote", remote_state: lane.state, queued_reason: undefined } : {}),
|
|
221
|
+
...(remoteSnapshotMetadata.has(lane.lane_id)
|
|
222
|
+
? {
|
|
223
|
+
remote_snapshot_age_s: remoteSnapshotMetadata.get(lane.lane_id).ageSeconds,
|
|
224
|
+
remote_snapshot_stale: remoteSnapshotMetadata.get(lane.lane_id).stale,
|
|
225
|
+
...(remoteSnapshotMetadata.get(lane.lane_id).loading ? { remote_snapshot_loading: true } : {}),
|
|
226
|
+
}
|
|
227
|
+
: {}),
|
|
228
|
+
overdue: Boolean(lane.overdue) || isOverdue(hqRoot, lane.lane_id, now),
|
|
229
|
+
stalled: Boolean(lane.stalled),
|
|
230
|
+
senior_lost: seniorLost,
|
|
231
|
+
};
|
|
232
|
+
});
|
|
233
|
+
return { lanes, ...(remoteLanesLoading ? { remote_lanes_loading: true } : {}) };
|
|
234
|
+
}
|
|
235
|
+
/** Lightweight implementation for the exact cache-first `lanes list --json` entrypoint. */
|
|
236
|
+
export async function runFastCachedLanesList() {
|
|
237
|
+
try {
|
|
238
|
+
const env = process.env;
|
|
239
|
+
const hqRoot = resolveLanesHqRoot({ env, cwd: process.cwd() });
|
|
240
|
+
const now = new Date();
|
|
241
|
+
const host = resolveAdmissionHost({
|
|
242
|
+
hostname: () => os.hostname(),
|
|
243
|
+
readOutpostIdentity: () => readOutpostIdentity(),
|
|
244
|
+
});
|
|
245
|
+
const lanes = await buildLaneList(hqRoot, readAllLanes(hqRoot), host, now, env);
|
|
246
|
+
printJson({ ok: true, action: "list", ...lanes });
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
failJson(err);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
//# sourceMappingURL=fast-lanes-list.js.map
|
package/dist/fast-scaffold.d.ts
CHANGED
|
@@ -53,6 +53,11 @@ export type ParsedSecretsEnvFastRequest = {
|
|
|
53
53
|
* the full CLI, which remains the source of truth for help and errors.
|
|
54
54
|
*/
|
|
55
55
|
export declare function parseSecretsEnvFastRequest(argv: readonly string[]): ParsedSecretsEnvFastRequest | null;
|
|
56
|
+
/**
|
|
57
|
+
* The cache-first list path only handles the bare JSON request. `--fresh`,
|
|
58
|
+
* filters, help, and unknown options stay with Commander and the full CLI.
|
|
59
|
+
*/
|
|
60
|
+
export declare function isFastCachedLanesListRequest(argv: readonly string[]): boolean;
|
|
56
61
|
/**
|
|
57
62
|
* Parse `hq core [--hq-root <path>] <name> [args...]` when `<name>` is a
|
|
58
63
|
* fast-path script. Returns null for anything else so the caller defers to the
|
package/dist/fast-scaffold.js
CHANGED
|
@@ -112,6 +112,16 @@ export function parseSecretsEnvFastRequest(argv) {
|
|
|
112
112
|
}
|
|
113
113
|
return hasOnly ? { options, scope } : null;
|
|
114
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* The cache-first list path only handles the bare JSON request. `--fresh`,
|
|
117
|
+
* filters, help, and unknown options stay with Commander and the full CLI.
|
|
118
|
+
*/
|
|
119
|
+
export function isFastCachedLanesListRequest(argv) {
|
|
120
|
+
return argv.length === 5 &&
|
|
121
|
+
argv[2] === "lanes" &&
|
|
122
|
+
argv[3] === "list" &&
|
|
123
|
+
argv[4] === "--json";
|
|
124
|
+
}
|
|
115
125
|
/**
|
|
116
126
|
* Parse `hq core [--hq-root <path>] <name> [args...]` when `<name>` is a
|
|
117
127
|
* fast-path script. Returns null for anything else so the caller defers to the
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import "./node-network-compat.js";
|
|
|
6
6
|
import { isVersionRequest, wantsLabelledVersionReport } from "./version-request.js";
|
|
7
7
|
// Dependency-light: a pure parser + table, no command graph. Safe to load on
|
|
8
8
|
// every path (including --version) without reintroducing the heavy startup.
|
|
9
|
-
import { isFastCoreRequest, parseFastCore, parseSecretsEnvFastRequest, } from "./fast-scaffold.js";
|
|
9
|
+
import { isFastCachedLanesListRequest, isFastCoreRequest, parseFastCore, parseSecretsEnvFastRequest, } from "./fast-scaffold.js";
|
|
10
10
|
// Also dependency-light: registers a process-level rejection boundary; its only
|
|
11
11
|
// heavy import (main.js) is lazy and reached only when a rejection fires.
|
|
12
12
|
import { installProcessRejectionBoundary } from "./unhandled-rejection-boundary.js";
|
|
@@ -61,6 +61,15 @@ else if (parseSecretsEnvFastRequest(process.argv)) {
|
|
|
61
61
|
});
|
|
62
62
|
});
|
|
63
63
|
}
|
|
64
|
+
else if (isFastCachedLanesListRequest(process.argv)) {
|
|
65
|
+
void import("./fast-lanes-list-route.js").then(async ({ runFastLanesListRequest }) => {
|
|
66
|
+
const handled = await runFastLanesListRequest(process.argv);
|
|
67
|
+
if (!handled) {
|
|
68
|
+
const { runCli } = await import("./main.js");
|
|
69
|
+
await runCli();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
64
73
|
else if (isFastCoreRequest(process.argv)) {
|
|
65
74
|
// Hot relocated plumbing (`hq core hq-session`, `hq core checkpoint-stop-gate`)
|
|
66
75
|
// dispatches straight to the bundled-script runner, so it never pays for the
|
|
@@ -27,11 +27,23 @@
|
|
|
27
27
|
*
|
|
28
28
|
* A signal whose `entity_refs` names a copy holds that copy: signals are
|
|
29
29
|
* server-owned, so the ref cannot be re-pointed from here.
|
|
30
|
+
*
|
|
31
|
+
* With `pairKind: "variants"` (flag `checkpoint.ontology.merge-variants`) the
|
|
32
|
+
* pairs are two-file duplicate groups whose canonical names differ only in
|
|
33
|
+
* case, spacing or punctuation; the survivor is the spaced name, then the
|
|
34
|
+
* higher `signal_count`, then path order. The copy's canonical name becomes
|
|
35
|
+
* an alias of the survivor. Merge and delete work as for numbered copies.
|
|
30
36
|
*/
|
|
31
37
|
import { type OntologyCompany } from "./company.js";
|
|
32
38
|
import { type EntityIndex } from "./tree.js";
|
|
33
39
|
export declare const MERGE_COPIES_FLAG = "checkpoint.ontology.merge-copies";
|
|
34
|
-
|
|
40
|
+
/**
|
|
41
|
+
* `merge-copies --variants`: merge two-file duplicate groups whose canonical
|
|
42
|
+
* names differ only in spacing, case or punctuation (CO-65).
|
|
43
|
+
*/
|
|
44
|
+
export declare const MERGE_VARIANTS_FLAG = "checkpoint.ontology.merge-variants";
|
|
45
|
+
export type CopySkipReason = "no_original" | "original_invalid" | "identity_differs" | "not_in_duplicate_group" | "group_size" | "type_differs" | "names_differ" | "numbered_copy";
|
|
46
|
+
export type PairKind = "numbered" | "variants";
|
|
35
47
|
export interface CopySkip {
|
|
36
48
|
copy: string;
|
|
37
49
|
reason: CopySkipReason;
|
|
@@ -66,6 +78,7 @@ export interface CopyPlan extends CopyPair {
|
|
|
66
78
|
export interface MergeCopiesReport {
|
|
67
79
|
company: string;
|
|
68
80
|
mode: "dry_run" | "merge" | "delete";
|
|
81
|
+
pair_kind: PairKind;
|
|
69
82
|
duplicate_groups_before: number;
|
|
70
83
|
duplicate_groups_after: number;
|
|
71
84
|
copies_found: number;
|
|
@@ -97,6 +110,8 @@ export interface MergeCopiesOptions {
|
|
|
97
110
|
hqRoot: string;
|
|
98
111
|
company: OntologyCompany;
|
|
99
112
|
mode: "dry_run" | "merge" | "delete";
|
|
113
|
+
/** Which pairs to merge; default numbered copies. */
|
|
114
|
+
pairKind?: PairKind;
|
|
100
115
|
/** Handle at most this many copies (sorted by path). */
|
|
101
116
|
limit?: number;
|
|
102
117
|
now: Date;
|
|
@@ -107,6 +122,14 @@ export declare function findCopyPairs(index: EntityIndex): {
|
|
|
107
122
|
pairs: CopyPair[];
|
|
108
123
|
skipped: CopySkip[];
|
|
109
124
|
};
|
|
125
|
+
/**
|
|
126
|
+
* Two-file duplicate groups whose canonical names differ only in case, spacing
|
|
127
|
+
* or punctuation. Numbered copies are left to `findCopyPairs`. Read-only.
|
|
128
|
+
*/
|
|
129
|
+
export declare function findVariantPairs(index: EntityIndex): {
|
|
130
|
+
pairs: CopyPair[];
|
|
131
|
+
skipped: CopySkip[];
|
|
132
|
+
};
|
|
110
133
|
/**
|
|
111
134
|
* Plan, merge or delete numbered copies for one company. The dry run writes
|
|
112
135
|
* nothing; merge writes entity files only; delete removes vault keys and
|
|
@@ -27,6 +27,12 @@
|
|
|
27
27
|
*
|
|
28
28
|
* A signal whose `entity_refs` names a copy holds that copy: signals are
|
|
29
29
|
* server-owned, so the ref cannot be re-pointed from here.
|
|
30
|
+
*
|
|
31
|
+
* With `pairKind: "variants"` (flag `checkpoint.ontology.merge-variants`) the
|
|
32
|
+
* pairs are two-file duplicate groups whose canonical names differ only in
|
|
33
|
+
* case, spacing or punctuation; the survivor is the spaced name, then the
|
|
34
|
+
* higher `signal_count`, then path order. The copy's canonical name becomes
|
|
35
|
+
* an alias of the survivor. Merge and delete work as for numbered copies.
|
|
30
36
|
*/
|
|
31
37
|
import * as fs from "node:fs";
|
|
32
38
|
import * as path from "node:path";
|
|
@@ -38,6 +44,11 @@ import { isWritableEntityType, normalizeEntityName } from "./identity.js";
|
|
|
38
44
|
import { findDuplicateGroups, readBackRefs, recordOutbox } from "./multi-writer.js";
|
|
39
45
|
import { buildEntityIndex, listSignalFiles, readSignal } from "./tree.js";
|
|
40
46
|
export const MERGE_COPIES_FLAG = "checkpoint.ontology.merge-copies";
|
|
47
|
+
/**
|
|
48
|
+
* `merge-copies --variants`: merge two-file duplicate groups whose canonical
|
|
49
|
+
* names differ only in spacing, case or punctuation (CO-65).
|
|
50
|
+
*/
|
|
51
|
+
export const MERGE_VARIANTS_FLAG = "checkpoint.ontology.merge-variants";
|
|
41
52
|
/** `<slug> <n>.md`, the name a file manager gives a second copy. */
|
|
42
53
|
const COPY_NAME = /^(.+) (\d+)\.md$/;
|
|
43
54
|
/** A relationship list item with a link, as the gardener and checkpoint write them. */
|
|
@@ -133,6 +144,62 @@ export function findCopyPairs(index) {
|
|
|
133
144
|
}
|
|
134
145
|
return { pairs, skipped };
|
|
135
146
|
}
|
|
147
|
+
/** The canonical name with case, spacing and punctuation removed. */
|
|
148
|
+
function compactName(name, type) {
|
|
149
|
+
return normalizeEntityName(name, type).replace(/[^\p{L}\p{N}]/gu, "");
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Two-file duplicate groups whose canonical names differ only in case, spacing
|
|
153
|
+
* or punctuation. Numbered copies are left to `findCopyPairs`. Read-only.
|
|
154
|
+
*/
|
|
155
|
+
export function findVariantPairs(index) {
|
|
156
|
+
const pairs = [];
|
|
157
|
+
const skipped = [];
|
|
158
|
+
for (const group of findDuplicateGroups(index)) {
|
|
159
|
+
const paths = [...group.paths].sort((a, b) => a.localeCompare(b));
|
|
160
|
+
const numbered = paths.filter((rel) => COPY_NAME.test(path.posix.basename(rel)));
|
|
161
|
+
if (numbered.length > 0) {
|
|
162
|
+
for (const rel of numbered)
|
|
163
|
+
skipped.push({ copy: rel, reason: "numbered_copy" });
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (paths.length !== 2) {
|
|
167
|
+
skipped.push({ copy: paths[0], reason: "group_size" });
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const left = index.byRel.get(paths[0]);
|
|
171
|
+
const right = index.byRel.get(paths[1]);
|
|
172
|
+
if (!left || !right) {
|
|
173
|
+
skipped.push({ copy: paths[0], reason: "no_original" });
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (left.type !== right.type) {
|
|
177
|
+
skipped.push({ copy: paths[0], reason: "type_differs" });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const leftName = compactName(left.canonical_name, left.type);
|
|
181
|
+
const rightName = compactName(right.canonical_name, right.type);
|
|
182
|
+
if (leftName === "" || rightName === "" || leftName !== rightName) {
|
|
183
|
+
skipped.push({ copy: paths[0], reason: "names_differ" });
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const leftSpaced = left.canonical_name.includes(" ");
|
|
187
|
+
const rightSpaced = right.canonical_name.includes(" ");
|
|
188
|
+
let originalRel = paths[0];
|
|
189
|
+
let copyRel = paths[1];
|
|
190
|
+
if (leftSpaced !== rightSpaced) {
|
|
191
|
+
originalRel = leftSpaced ? paths[0] : paths[1];
|
|
192
|
+
copyRel = leftSpaced ? paths[1] : paths[0];
|
|
193
|
+
}
|
|
194
|
+
else if (left.signal_count !== right.signal_count) {
|
|
195
|
+
originalRel = left.signal_count > right.signal_count ? paths[0] : paths[1];
|
|
196
|
+
copyRel = originalRel === paths[0] ? paths[1] : paths[0];
|
|
197
|
+
}
|
|
198
|
+
pairs.push({ type: left.type, copy: copyRel, original: originalRel });
|
|
199
|
+
}
|
|
200
|
+
pairs.sort((a, b) => a.copy.localeCompare(b.copy));
|
|
201
|
+
return { pairs, skipped };
|
|
202
|
+
}
|
|
136
203
|
/** Resolve a relative link in `fromRel` to a company-relative path, or null. */
|
|
137
204
|
function resolveLink(fromRel, href) {
|
|
138
205
|
if (/^[a-z][a-z0-9+.-]*:/i.test(href) || href.startsWith("#") || href.startsWith("/"))
|
|
@@ -431,13 +498,15 @@ function writeEntity(hqRoot, company, rel, content, now) {
|
|
|
431
498
|
export async function mergeCopies(options) {
|
|
432
499
|
const { hqRoot, company, mode, now } = options;
|
|
433
500
|
const index = buildEntityIndex(company.dir);
|
|
434
|
-
const
|
|
501
|
+
const pairKind = options.pairKind ?? "numbered";
|
|
502
|
+
const found = pairKind === "variants" ? findVariantPairs(index) : findCopyPairs(index);
|
|
435
503
|
const pairs = options.limit !== undefined ? found.pairs.slice(0, Math.max(0, options.limit)) : found.pairs;
|
|
436
504
|
const before = scanLinks(company).broken;
|
|
437
505
|
const { plans, merges, finals, notReady } = plan(company, index, pairs, found.pairs, now);
|
|
438
506
|
const report = {
|
|
439
507
|
company: company.slug,
|
|
440
508
|
mode,
|
|
509
|
+
pair_kind: pairKind,
|
|
441
510
|
duplicate_groups_before: findDuplicateGroups(index).length,
|
|
442
511
|
duplicate_groups_after: 0,
|
|
443
512
|
copies_found: found.pairs.length,
|
|
@@ -20,7 +20,7 @@ export { LANE_STORY_LEDGER_FLAG, buildLaneStatusSnapshot, formatLaneStatusHuman,
|
|
|
20
20
|
export type { LaneStatusSnapshot, ProjectStoryLedger, StoryLedgerWriteDeps } from "./story-ledger.js";
|
|
21
21
|
export { buildOutpostPickupCommand, parseOutpostPickupPayload, } from "./outpost-dispatch.js";
|
|
22
22
|
export type { OutpostDispatchResult, OutpostPickupPayload, } from "./outpost-dispatch.js";
|
|
23
|
-
export { LANES_STORE_REL, LANE_RECORDS_DIR, PENDING_INDEX_DIR, QUESTION_RECORDS_DIR, appendCappedHistory, appendLaneAttribution, appendLaneSeniorHistory, appendQuestionRouting, createLane, createQuestion, isConflictSidecarName, laneRecordPath, lanesStoreRoot, listLaneIds, listQuestionIds, mergeLaneRecords, mergeQuestionRecords, mintLaneId, mintQuestionId, parseLaneRecord, parseQuestionRecord, pendingMarkerPath, questionRecordPath, readLane, readQuestion, resolveStoreHqRoot, sanitizeHostComponent, tryReadLane, tryReadQuestion, retryUpdateLane, retryUpdateQuestion, syncLaneRecordFromOwner, updateLane, updateQuestion, withLaneHireLocks, } from "./store.js";
|
|
23
|
+
export { LANES_STORE_REL, LANE_RECORDS_DIR, PENDING_INDEX_DIR, QUESTION_RECORDS_DIR, appendCappedHistory, appendLaneAttribution, appendLaneSeniorHistory, appendQuestionRouting, createLane, createQuestion, isConflictSidecarName, laneRecordPath, lanesStoreRoot, listLaneIds, listQuestionIds, mergeLaneRecords, mergeQuestionRecords, mintLaneId, mintQuestionId, parseLaneRecord, parseQuestionRecord, pendingMarkerPath, questionRecordPath, readAllLanes, readLane, readQuestion, resolveStoreHqRoot, sanitizeHostComponent, tryReadLane, tryReadQuestion, retryUpdateLane, retryUpdateQuestion, syncLaneRecordFromOwner, updateLane, updateQuestion, withLaneHireLocks, } from "./store.js";
|
|
24
24
|
export type { StoreDeps } from "./store.js";
|
|
25
25
|
export { DEFAULT_LANE_WATCH_INTERVAL_MS, MAX_LANE_WATCH_INTERVAL_MS, MAX_WATCHED_LANES, MAX_WATCH_OUTPUT_LINES, runLaneWatch, } from "./watch.js";
|
|
26
26
|
export type { LaneWatchDeps, LaneWatchResult } from "./watch.js";
|
package/dist/lib/lanes/index.js
CHANGED
|
@@ -12,7 +12,7 @@ export { DEFAULT_ACCOUNT_HOMES, DEFAULT_AUTO_COMPACT_TOKENS, DEFAULT_COMPACT_AFT
|
|
|
12
12
|
export { resolveLanesHqRoot } from "./hq-root.js";
|
|
13
13
|
export { LANE_STORY_LEDGER_FLAG, buildLaneStatusSnapshot, formatLaneStatusHuman, laneStoryLedgerEnabled, resolveProjectStoryLedger, syncLaneToProjectStoryLedger, validateProjectStories, } from "./story-ledger.js";
|
|
14
14
|
export { buildOutpostPickupCommand, parseOutpostPickupPayload, } from "./outpost-dispatch.js";
|
|
15
|
-
export { LANES_STORE_REL, LANE_RECORDS_DIR, PENDING_INDEX_DIR, QUESTION_RECORDS_DIR, appendCappedHistory, appendLaneAttribution, appendLaneSeniorHistory, appendQuestionRouting, createLane, createQuestion, isConflictSidecarName, laneRecordPath, lanesStoreRoot, listLaneIds, listQuestionIds, mergeLaneRecords, mergeQuestionRecords, mintLaneId, mintQuestionId, parseLaneRecord, parseQuestionRecord, pendingMarkerPath, questionRecordPath, readLane, readQuestion, resolveStoreHqRoot, sanitizeHostComponent, tryReadLane, tryReadQuestion, retryUpdateLane, retryUpdateQuestion, syncLaneRecordFromOwner, updateLane, updateQuestion, withLaneHireLocks, } from "./store.js";
|
|
15
|
+
export { LANES_STORE_REL, LANE_RECORDS_DIR, PENDING_INDEX_DIR, QUESTION_RECORDS_DIR, appendCappedHistory, appendLaneAttribution, appendLaneSeniorHistory, appendQuestionRouting, createLane, createQuestion, isConflictSidecarName, laneRecordPath, lanesStoreRoot, listLaneIds, listQuestionIds, mergeLaneRecords, mergeQuestionRecords, mintLaneId, mintQuestionId, parseLaneRecord, parseQuestionRecord, pendingMarkerPath, questionRecordPath, readAllLanes, readLane, readQuestion, resolveStoreHqRoot, sanitizeHostComponent, tryReadLane, tryReadQuestion, retryUpdateLane, retryUpdateQuestion, syncLaneRecordFromOwner, updateLane, updateQuestion, withLaneHireLocks, } from "./store.js";
|
|
16
16
|
export { DEFAULT_LANE_WATCH_INTERVAL_MS, MAX_LANE_WATCH_INTERVAL_MS, MAX_WATCHED_LANES, MAX_WATCH_OUTPUT_LINES, runLaneWatch, } from "./watch.js";
|
|
17
17
|
export { MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_TASK_OUTPUT_MAX_BYTES, MONITOR_TRANSCRIPT_MAX_BYTES, MONITOR_UNCOVERED_EXIT_CODE, coveredLaneIds, isMonitorTaskLive, monitorReminderText, parseCanonicalWatchCommand, parseMonitorTranscript, readLiveMonitorEvidence, } from "./monitor.js";
|
|
18
18
|
export { IMDS_HOST, IMDS_INSTANCE_ID_URL, IMDS_TIMEOUT_MS, IMDS_TOKEN_URL, defaultReadImdsInstanceId, resolveMachineIdentity, } from "./machine-identity.js";
|
|
@@ -86,7 +86,7 @@ export declare function buildOutpostLaneListCommand(input?: {
|
|
|
86
86
|
hqBinary?: string;
|
|
87
87
|
laneIds?: readonly string[];
|
|
88
88
|
active?: boolean;
|
|
89
|
-
hierarchy?: string;
|
|
89
|
+
hierarchy?: string | readonly string[];
|
|
90
90
|
snapshot?: boolean;
|
|
91
91
|
}): string;
|
|
92
92
|
/** Read questions on one Outpost, scoped to a company when supplied. */
|
|
@@ -77,8 +77,10 @@ export function buildOutpostLaneListCommand(input = {}) {
|
|
|
77
77
|
args.push("--lane", laneId);
|
|
78
78
|
if (input.active)
|
|
79
79
|
args.push("--active");
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
for (const laneId of typeof input.hierarchy === "string"
|
|
81
|
+
? [input.hierarchy]
|
|
82
|
+
: input.hierarchy ?? [])
|
|
83
|
+
args.push("--hierarchy", laneId);
|
|
82
84
|
if (input.snapshot)
|
|
83
85
|
args.push("--snapshot");
|
|
84
86
|
return withRemoteHqDir(joinCommandParts(args));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export declare const OUTPOST_HIRE_SUMMARY_CACHE_TTL_MS = 60000;
|
|
2
|
+
export declare const LANE_OUTPOST_SNAPSHOT_CACHE_FLAG = "lanes.outpost-snapshot-cache";
|
|
2
3
|
export interface OutpostHireSummary {
|
|
3
4
|
hire_grant: number;
|
|
4
5
|
hire_alive_children: number;
|
|
@@ -13,6 +14,8 @@ export interface OutpostHireSummaryCacheEntry extends OutpostHireSummary {
|
|
|
13
14
|
export declare function outpostHireSummaryCachePath(hqRoot: string): string;
|
|
14
15
|
export declare function outpostHireSummaryCacheKey(outpostId: string, laneId: string): string;
|
|
15
16
|
export declare function readOutpostHireSummaryCache(hqRoot: string, nowMs: number): Map<string, OutpostHireSummaryCacheEntry>;
|
|
17
|
+
/** Reads the last validated summaries even after their freshness window expires. */
|
|
18
|
+
export declare function readOutpostHireSummaryCacheIncludingStale(hqRoot: string, nowMs: number): Map<string, OutpostHireSummaryCacheEntry>;
|
|
16
19
|
export declare function writeOutpostHireSummaryCache(hqRoot: string, updates: readonly (OutpostHireSummary & {
|
|
17
20
|
outpost_id: string;
|
|
18
21
|
lane_id: string;
|
|
@@ -2,6 +2,7 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
export const OUTPOST_HIRE_SUMMARY_CACHE_TTL_MS = 60_000;
|
|
5
|
+
export const LANE_OUTPOST_SNAPSHOT_CACHE_FLAG = "lanes.outpost-snapshot-cache";
|
|
5
6
|
const CACHE_RELATIVE_PATH = path.join("workspace", "lanes-runs", ".cache", "outpost-hire-summaries.json");
|
|
6
7
|
export function outpostHireSummaryCachePath(hqRoot) {
|
|
7
8
|
return path.join(hqRoot, CACHE_RELATIVE_PATH);
|
|
@@ -10,6 +11,13 @@ export function outpostHireSummaryCacheKey(outpostId, laneId) {
|
|
|
10
11
|
return JSON.stringify([outpostId, laneId]);
|
|
11
12
|
}
|
|
12
13
|
export function readOutpostHireSummaryCache(hqRoot, nowMs) {
|
|
14
|
+
return readCache(hqRoot, nowMs, false);
|
|
15
|
+
}
|
|
16
|
+
/** Reads the last validated summaries even after their freshness window expires. */
|
|
17
|
+
export function readOutpostHireSummaryCacheIncludingStale(hqRoot, nowMs) {
|
|
18
|
+
return readCache(hqRoot, nowMs, true);
|
|
19
|
+
}
|
|
20
|
+
function readCache(hqRoot, nowMs, includeStale) {
|
|
13
21
|
const filePath = outpostHireSummaryCachePath(hqRoot);
|
|
14
22
|
let contents;
|
|
15
23
|
try {
|
|
@@ -36,7 +44,7 @@ export function readOutpostHireSummaryCache(hqRoot, nowMs) {
|
|
|
36
44
|
const entries = new Map();
|
|
37
45
|
const duplicates = new Set();
|
|
38
46
|
for (const value of parsed) {
|
|
39
|
-
const entry = parseCacheEntry(value, nowMs);
|
|
47
|
+
const entry = parseCacheEntry(value, nowMs, includeStale);
|
|
40
48
|
if (!entry)
|
|
41
49
|
continue;
|
|
42
50
|
const key = outpostHireSummaryCacheKey(entry.outpost_id, entry.lane_id);
|
|
@@ -94,7 +102,7 @@ export function writeOutpostHireSummaryCache(hqRoot, updates, nowMs, existingEnt
|
|
|
94
102
|
console.warn(`Could not write Outpost hire summary cache at ${filePath}: ${errorText(err)}`);
|
|
95
103
|
}
|
|
96
104
|
}
|
|
97
|
-
function parseCacheEntry(value, nowMs) {
|
|
105
|
+
function parseCacheEntry(value, nowMs, includeStale = false) {
|
|
98
106
|
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
99
107
|
return undefined;
|
|
100
108
|
const entry = value;
|
|
@@ -120,7 +128,7 @@ function parseCacheEntry(value, nowMs) {
|
|
|
120
128
|
const cachedAt = Date.parse(entry.cached_at);
|
|
121
129
|
if (!Number.isFinite(cachedAt) ||
|
|
122
130
|
cachedAt > nowMs ||
|
|
123
|
-
nowMs - cachedAt > OUTPOST_HIRE_SUMMARY_CACHE_TTL_MS) {
|
|
131
|
+
(!includeStale && nowMs - cachedAt > OUTPOST_HIRE_SUMMARY_CACHE_TTL_MS)) {
|
|
124
132
|
return undefined;
|
|
125
133
|
}
|
|
126
134
|
const hire = `${entry.hire_alive_children}/${entry.hire_grant}`;
|