@indigoai-us/hq-cli 5.294.0 → 5.295.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 +25 -0
- package/dist/command-catalog.generated.d.ts +12 -9
- package/dist/command-catalog.generated.js +15 -12
- package/dist/commands/auth.d.ts +1 -0
- package/dist/commands/auth.js +76 -1
- package/dist/commands/doctor.js +0 -74
- package/dist/commands/lanes-message.js +23 -5
- package/dist/commands/lanes-resume.js +0 -3
- package/dist/commands/lanes-shared.d.ts +1 -2
- package/dist/commands/lanes.d.ts +1 -9
- package/dist/commands/lanes.js +177 -172
- package/dist/fast-scaffold.d.ts +0 -5
- package/dist/fast-scaffold.js +0 -10
- package/dist/index.js +1 -10
- package/dist/lib/core-utils/sentry-report.d.ts +0 -19
- package/dist/lib/core-utils/sentry-report.js +8 -61
- package/dist/lib/doctor/checks/claude-wiring.js +9 -0
- package/dist/lib/lanes/flag-keys.d.ts +2 -0
- package/dist/lib/lanes/flag-keys.js +2 -0
- package/dist/lib/lanes/manager-status-gate.d.ts +0 -17
- package/dist/lib/lanes/manager-status-gate.js +2 -43
- package/dist/lib/lanes/outpost-dispatch.d.ts +2 -0
- package/dist/lib/lanes/outpost-dispatch.js +7 -1
- package/dist/lib/lanes/outpost-hire-cache.d.ts +0 -1
- package/dist/lib/lanes/outpost-hire-cache.js +0 -1
- package/dist/lib/lanes/outpost-snapshot-memo.d.ts +6 -0
- package/dist/lib/lanes/outpost-snapshot-memo.js +14 -0
- package/dist/lib/lanes/reconcile.d.ts +2 -3
- package/dist/lib/lanes/reconcile.js +44 -38
- package/dist/lib/lanes/spawn.d.ts +0 -2
- package/dist/lib/lanes/spawn.js +18 -5
- package/dist/lib/lanes/store.js +22 -0
- package/dist/lib/lanes/types.d.ts +18 -1
- package/dist/lib/lanes/types.js +1 -1
- package/dist/lib/lanes/watcher.d.ts +4 -3
- package/dist/lib/lanes/watcher.js +102 -34
- package/dist/lib/mesh/live/daemon/run.js +18 -1
- package/dist/utils/cognito-session.d.ts +6 -0
- package/dist/utils/cognito-session.js +9 -0
- package/dist/utils/self-update.js +31 -0
- package/dist/utils/version-gate.js +22 -3
- package/package.json +1 -1
- package/dist/fast-lanes-list-route.d.ts +0 -8
- package/dist/fast-lanes-list-route.js +0 -32
- package/dist/fast-lanes-list.d.ts +0 -3
- package/dist/fast-lanes-list.js +0 -224
- package/dist/lib/doctor/client-health-sentry-flag.d.ts +0 -23
- package/dist/lib/doctor/client-health-sentry-flag.js +0 -99
- package/dist/lib/doctor/client-health-sentry.d.ts +0 -71
- package/dist/lib/doctor/client-health-sentry.js +0 -286
- package/dist/lib/lanes/outpost-snapshot-cache.d.ts +0 -26
- package/dist/lib/lanes/outpost-snapshot-cache.js +0 -215
- package/dist/lib/lanes/outpost-snapshot-refresh.d.ts +0 -21
- package/dist/lib/lanes/outpost-snapshot-refresh.js +0 -329
package/dist/fast-scaffold.js
CHANGED
|
@@ -272,16 +272,6 @@ export function parseSecretsEnvFastRequest(argv) {
|
|
|
272
272
|
}
|
|
273
273
|
return hasOnly ? { options, scope } : null;
|
|
274
274
|
}
|
|
275
|
-
/**
|
|
276
|
-
* The cache-first list path only handles the bare JSON request. `--fresh`,
|
|
277
|
-
* filters, help, and unknown options stay with Commander and the full CLI.
|
|
278
|
-
*/
|
|
279
|
-
export function isFastCachedLanesListRequest(argv) {
|
|
280
|
-
return argv.length === 5 &&
|
|
281
|
-
argv[2] === "lanes" &&
|
|
282
|
-
argv[3] === "list" &&
|
|
283
|
-
argv[4] === "--json";
|
|
284
|
-
}
|
|
285
275
|
/**
|
|
286
276
|
* Parse `hq core [--hq-root <path>] <name> [args...]` when `<name>` is a
|
|
287
277
|
* 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 {
|
|
9
|
+
import { 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,15 +61,6 @@ 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
|
-
}
|
|
73
64
|
else if (isFastCoreRequest(process.argv)) {
|
|
74
65
|
// Hot relocated plumbing (`hq core hq-session`, `hq core checkpoint-stop-gate`)
|
|
75
66
|
// dispatches straight to the bundled-script runner, so it never pays for the
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import * as Sentry from "@sentry/node";
|
|
19
19
|
import type { CaptureContext, SeverityLevel } from "@sentry/node";
|
|
20
|
-
import type { ClientHealthSentryEvent } from "../doctor/client-health-sentry.js";
|
|
21
20
|
export declare const DEFAULT_SENTRY_REPORT_TIMEOUT_MS = 750;
|
|
22
21
|
export declare const MAX_SENTRY_REPORT_TIMEOUT_MS = 5000;
|
|
23
22
|
export declare const MAX_SENTRY_REPORT_PAYLOAD_BYTES = 2048;
|
|
@@ -96,17 +95,6 @@ export type SentryReportResult = {
|
|
|
96
95
|
event?: SentryReportEvent;
|
|
97
96
|
droppedMetadataKeys?: number;
|
|
98
97
|
};
|
|
99
|
-
export interface ClientHealthSentryScope {
|
|
100
|
-
clear(): void;
|
|
101
|
-
setLevel(level: SeverityLevel): void;
|
|
102
|
-
setFingerprint(fingerprint: string[]): void;
|
|
103
|
-
setTags(tags: Record<string, string>): void;
|
|
104
|
-
setUser(user: {
|
|
105
|
-
id: string;
|
|
106
|
-
ip_address: null;
|
|
107
|
-
}): void;
|
|
108
|
-
}
|
|
109
|
-
export type ClientHealthSentryScopeRunner = (callback: (scope: ClientHealthSentryScope) => void) => void;
|
|
110
98
|
/**
|
|
111
99
|
* Read no more than the accepted event size. Returning early keeps a malformed
|
|
112
100
|
* pipe from making a watchdog retain an unbounded hook payload in memory.
|
|
@@ -129,11 +117,4 @@ export declare function parseSentryReportEvent(stdin: string): SentryReportResul
|
|
|
129
117
|
* transport errors cannot echo event data back to a hook.
|
|
130
118
|
*/
|
|
131
119
|
export declare function reportSentryEvent(stdin: string, options?: SentryReportOptions, dependencies?: SentryReportDependencies): Promise<SentryReportResult>;
|
|
132
|
-
/** Send the client-health projection with a clean Sentry scope and no metadata payload. */
|
|
133
|
-
export declare function reportClientHealthEvents(events: readonly ClientHealthSentryEvent[], options?: {
|
|
134
|
-
timeoutMs?: number;
|
|
135
|
-
scopeRunner?: ClientHealthSentryScopeRunner;
|
|
136
|
-
}, dependencies?: SentryReportDependencies): Promise<{
|
|
137
|
-
code: 0 | 1;
|
|
138
|
-
}>;
|
|
139
120
|
//# sourceMappingURL=sentry-report.d.ts.map
|
|
@@ -577,6 +577,14 @@ function hookTimeoutDebugContextTags(context) {
|
|
|
577
577
|
}
|
|
578
578
|
function safeSlowChild(metadata) {
|
|
579
579
|
const value = metadata.slow_child;
|
|
580
|
+
const slowChildMs = metadata.slow_child_ms;
|
|
581
|
+
const hasMeasuredChild = typeof slowChildMs === "number" && Number.isFinite(slowChildMs) && slowChildMs >= 0;
|
|
582
|
+
if (metadata.hook_event === "SessionStart" &&
|
|
583
|
+
(metadata.bash_env_set === true || metadata.bash_env_set === "set") &&
|
|
584
|
+
!hasMeasuredChild &&
|
|
585
|
+
(typeof value !== "string" || value.length === 0 || value === "other")) {
|
|
586
|
+
return "entry-bash-startup";
|
|
587
|
+
}
|
|
580
588
|
if (typeof value !== "string" || !SAFE_HOOK_SCRIPT.test(value))
|
|
581
589
|
return "other";
|
|
582
590
|
return value;
|
|
@@ -1006,65 +1014,4 @@ export async function reportSentryEvent(stdin, options = {}, dependencies = defa
|
|
|
1006
1014
|
dependencies.stderr.write("Sentry report failed.\n");
|
|
1007
1015
|
return invalidInput();
|
|
1008
1016
|
}
|
|
1009
|
-
function isSafeClientHealthEvent(event) {
|
|
1010
|
-
const tags = event.tags;
|
|
1011
|
-
const checkClass = tags.check_class;
|
|
1012
|
-
const reasonCode = tags.reason_code;
|
|
1013
|
-
const classPattern = /^sync\.(?:update\.core|journal\.(?:personal|personal_vault|company)|manifest\.(?:personal|personal_vault|company))$|^other$/;
|
|
1014
|
-
const reasonPattern = /^(?:none|core-version-unavailable|core-update-available|no-local-tree|never-synced|invalid-last-sync|stale-threshold|manifest-unavailable|manifest-state-unavailable|no-manifest-scopes|never-uploaded|invalid-upload-time|unresolved-company-uid)$/;
|
|
1015
|
-
const versionPattern = /^(?:unknown|\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?(?:\+[A-Za-z0-9.-]+)?)$/;
|
|
1016
|
-
const safeTagKeys = [
|
|
1017
|
-
"check_class", "reason_code", "fix_outcome", "status", "failing_check_count",
|
|
1018
|
-
"cli_version", "core_version", "sync_version", "platform", "arch",
|
|
1019
|
-
];
|
|
1020
|
-
return (event.level === "warning" &&
|
|
1021
|
-
classPattern.test(checkClass) &&
|
|
1022
|
-
reasonPattern.test(reasonCode) &&
|
|
1023
|
-
event.message === `Client health: ${checkClass} unresolved after hq doctor --fix` &&
|
|
1024
|
-
event.fingerprint.length === 3 &&
|
|
1025
|
-
event.fingerprint[0] === "client-health" &&
|
|
1026
|
-
event.fingerprint[1] === checkClass &&
|
|
1027
|
-
event.fingerprint[2] === reasonCode &&
|
|
1028
|
-
(tags.fix_outcome === "not_auto_fixable" || tags.fix_outcome === "fix_failed" || tags.fix_outcome === "fix_timed_out") &&
|
|
1029
|
-
(tags.status === "FAIL" || tags.status === "WARN") &&
|
|
1030
|
-
/^\d{1,3}$/.test(tags.failing_check_count) &&
|
|
1031
|
-
safeTagKeys.every((key) => Object.hasOwn(tags, key)) &&
|
|
1032
|
-
Object.keys(tags).length === safeTagKeys.length &&
|
|
1033
|
-
[tags.cli_version, tags.core_version, tags.sync_version].every((value) => versionPattern.test(value)) &&
|
|
1034
|
-
/^[a-z0-9]+$/.test(tags.platform) &&
|
|
1035
|
-
/^[a-z0-9]+$/.test(tags.arch) &&
|
|
1036
|
-
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(event.user.id) &&
|
|
1037
|
-
event.user.ip_address === null);
|
|
1038
|
-
}
|
|
1039
|
-
/** Send the client-health projection with a clean Sentry scope and no metadata payload. */
|
|
1040
|
-
export async function reportClientHealthEvents(events, options = {}, dependencies = defaultDependencies) {
|
|
1041
|
-
const timeoutMs = resolveTimeout(options.timeoutMs);
|
|
1042
|
-
if (timeoutMs === null ||
|
|
1043
|
-
events.length === 0 ||
|
|
1044
|
-
events.length > 250 ||
|
|
1045
|
-
events.some((event) => !isSafeClientHealthEvent(event))) {
|
|
1046
|
-
dependencies.stderr.write("Invalid client health Sentry event.\n");
|
|
1047
|
-
return { code: 1 };
|
|
1048
|
-
}
|
|
1049
|
-
const withScope = options.scopeRunner ?? ((callback) => Sentry.withScope((scope) => callback(scope)));
|
|
1050
|
-
try {
|
|
1051
|
-
for (const event of events) {
|
|
1052
|
-
withScope((scope) => {
|
|
1053
|
-
scope.clear();
|
|
1054
|
-
scope.setLevel(event.level);
|
|
1055
|
-
scope.setFingerprint([...event.fingerprint]);
|
|
1056
|
-
scope.setTags({ sentry_event_type: "client_health", ...event.tags });
|
|
1057
|
-
scope.setUser({ id: event.user.id, ip_address: null });
|
|
1058
|
-
dependencies.sentry.captureMessage(event.message);
|
|
1059
|
-
});
|
|
1060
|
-
}
|
|
1061
|
-
if (await flushWithin(dependencies.sentry, timeoutMs))
|
|
1062
|
-
return { code: 0 };
|
|
1063
|
-
}
|
|
1064
|
-
catch {
|
|
1065
|
-
// Keep the event and transport details out of hook stderr.
|
|
1066
|
-
}
|
|
1067
|
-
dependencies.stderr.write("Client health Sentry report failed.\n");
|
|
1068
|
-
return { code: 1 };
|
|
1069
|
-
}
|
|
1070
1017
|
//# sourceMappingURL=sentry-report.js.map
|
|
@@ -460,6 +460,15 @@ function requiredFromFirstCommand(first) {
|
|
|
460
460
|
k += 1;
|
|
461
461
|
if (k >= first.length)
|
|
462
462
|
return { required, gatedHookId };
|
|
463
|
+
// SessionStart can use env to set BASH_ENV before Bash itself starts.
|
|
464
|
+
// Walk through that launcher and its assignments just like the shell scanner.
|
|
465
|
+
if (baseName(first[k]) === "env") {
|
|
466
|
+
k += 1;
|
|
467
|
+
while (k < first.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(first[k]))
|
|
468
|
+
k += 1;
|
|
469
|
+
if (k >= first.length || first[k].startsWith("-"))
|
|
470
|
+
return { required, gatedHookId };
|
|
471
|
+
}
|
|
463
472
|
if (INTERPRETERS.has(baseName(first[k]))) {
|
|
464
473
|
k += 1;
|
|
465
474
|
while (k < first.length && first[k].startsWith("-")) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
/** hq-flags keys used by lane list and placement behavior. */
|
|
2
2
|
export declare const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
|
|
3
|
+
/** Enables one automatic continuation after a missing-envelope repair. */
|
|
4
|
+
export declare const LANE_ENVELOPE_REPAIR_CONTINUE_FLAG = "lanes.envelope-repair-continue";
|
|
3
5
|
//# sourceMappingURL=flag-keys.d.ts.map
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
/** hq-flags keys used by lane list and placement behavior. */
|
|
2
2
|
export const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
|
|
3
|
+
/** Enables one automatic continuation after a missing-envelope repair. */
|
|
4
|
+
export const LANE_ENVELOPE_REPAIR_CONTINUE_FLAG = "lanes.envelope-repair-continue";
|
|
3
5
|
//# sourceMappingURL=flag-keys.js.map
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { readLanesRegistryFlag } from "./registry-flag.js";
|
|
2
1
|
import type { LaneRecord, ManagerStatusGate } from "./types.js";
|
|
3
|
-
export declare const MANAGER_STATUS_GATE_FLAG = "lanes.manager-status-gate";
|
|
4
2
|
export interface ManagerStatusTarget {
|
|
5
3
|
manager: boolean;
|
|
6
4
|
identified_by?: "story_id" | "worker_profile" | "juniors";
|
|
@@ -11,23 +9,8 @@ export interface StatusSnapshot {
|
|
|
11
9
|
mtime: number | null;
|
|
12
10
|
updatedAt: string | null;
|
|
13
11
|
}
|
|
14
|
-
export interface ManagerStatusGateResolution {
|
|
15
|
-
enabled: boolean;
|
|
16
|
-
target?: ManagerStatusTarget;
|
|
17
|
-
error?: string;
|
|
18
|
-
}
|
|
19
12
|
/** Resolve manager identity and the single declared status file, if present. */
|
|
20
13
|
export declare function resolveManagerStatusTarget(root: string, lane: LaneRecord): ManagerStatusTarget;
|
|
21
|
-
export declare function resolveManagerStatusGateForLane(root: string, lane: LaneRecord, options?: {
|
|
22
|
-
env?: NodeJS.ProcessEnv;
|
|
23
|
-
readFlag?: typeof readLanesRegistryFlag;
|
|
24
|
-
onError?: (error: unknown) => void;
|
|
25
|
-
}): Promise<ManagerStatusGateResolution>;
|
|
26
|
-
export declare function managerStatusGateEnabledForLane(root: string, lane: LaneRecord, options?: {
|
|
27
|
-
env?: NodeJS.ProcessEnv;
|
|
28
|
-
readFlag?: typeof readLanesRegistryFlag;
|
|
29
|
-
onError?: (error: unknown) => void;
|
|
30
|
-
}): Promise<boolean>;
|
|
31
14
|
export declare function readStatusSnapshot(filePath: string): StatusSnapshot;
|
|
32
15
|
export declare function managerStatusGateHeartbeat(root: string, lane: LaneRecord, turnId: string, turnStartedAt: string): ManagerStatusGate | undefined;
|
|
33
16
|
export declare function managerStatusWasWritten(gate: ManagerStatusGate, snapshot: StatusSnapshot): boolean;
|
|
@@ -2,9 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { checkCompanyPath } from "./checked-company-path.js";
|
|
4
4
|
import { LanesError } from "./errors.js";
|
|
5
|
-
import {
|
|
6
|
-
import { readLanesRegistryFlag } from "./registry-flag.js";
|
|
7
|
-
export const MANAGER_STATUS_GATE_FLAG = "lanes.manager-status-gate";
|
|
5
|
+
import { resolveLaneCompany } from "./lane-company.js";
|
|
8
6
|
function isObject(value) {
|
|
9
7
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
10
8
|
}
|
|
@@ -49,14 +47,6 @@ function matchesLane(rowLane, laneId) {
|
|
|
49
47
|
const bareLaneId = laneId.split("_", 1)[0] ?? "";
|
|
50
48
|
return row === laneId || row === bareLaneId;
|
|
51
49
|
}
|
|
52
|
-
function managerStatusGateErrorCode(err) {
|
|
53
|
-
return err instanceof LanesError ? err.error : "manager_status_gate_unavailable";
|
|
54
|
-
}
|
|
55
|
-
function logManagerStatusGateFailure(lane, err) {
|
|
56
|
-
const code = managerStatusGateErrorCode(err);
|
|
57
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
58
|
-
process.stderr.write(`hq lanes: manager status gate disabled for ${lane.lane_id}: ${code}: ${message}\n`);
|
|
59
|
-
}
|
|
60
50
|
/** Resolve manager identity and the single declared status file, if present. */
|
|
61
51
|
export function resolveManagerStatusTarget(root, lane) {
|
|
62
52
|
const directIdentity = lane.story_id === "MANAGER"
|
|
@@ -93,38 +83,6 @@ export function resolveManagerStatusTarget(root, lane) {
|
|
|
93
83
|
statusPath,
|
|
94
84
|
};
|
|
95
85
|
}
|
|
96
|
-
export async function resolveManagerStatusGateForLane(root, lane, options = {}) {
|
|
97
|
-
const reportError = options.onError ?? ((err) => logManagerStatusGateFailure(lane, err));
|
|
98
|
-
let enabled;
|
|
99
|
-
try {
|
|
100
|
-
const resolution = resolveLaneCompany(root, lane);
|
|
101
|
-
const company = resolution.company ?? undefined;
|
|
102
|
-
enabled = await (options.readFlag ?? readLanesRegistryFlag)(MANAGER_STATUS_GATE_FLAG, {
|
|
103
|
-
company,
|
|
104
|
-
...(company ? {
|
|
105
|
-
companyUidSource: laneCompanyUidSource(resolution.source),
|
|
106
|
-
} : {}),
|
|
107
|
-
env: options.env,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
catch (err) {
|
|
111
|
-
reportError(err);
|
|
112
|
-
return { enabled: false, error: managerStatusGateErrorCode(err) };
|
|
113
|
-
}
|
|
114
|
-
if (!enabled)
|
|
115
|
-
return { enabled: false };
|
|
116
|
-
try {
|
|
117
|
-
const target = resolveManagerStatusTarget(root, lane);
|
|
118
|
-
return { enabled: target.manager && target.statusPath !== undefined, target };
|
|
119
|
-
}
|
|
120
|
-
catch (err) {
|
|
121
|
-
reportError(err);
|
|
122
|
-
return { enabled: false, error: managerStatusGateErrorCode(err) };
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
export async function managerStatusGateEnabledForLane(root, lane, options = {}) {
|
|
126
|
-
return (await resolveManagerStatusGateForLane(root, lane, options)).enabled;
|
|
127
|
-
}
|
|
128
86
|
export function readStatusSnapshot(filePath) {
|
|
129
87
|
let stat;
|
|
130
88
|
try {
|
|
@@ -153,6 +111,7 @@ export function readStatusSnapshot(filePath) {
|
|
|
153
111
|
return { mtime: stat.mtimeMs, updatedAt };
|
|
154
112
|
}
|
|
155
113
|
export function managerStatusGateHeartbeat(root, lane, turnId, turnStartedAt) {
|
|
114
|
+
resolveLaneCompany(root, lane);
|
|
156
115
|
const target = resolveManagerStatusTarget(root, lane);
|
|
157
116
|
if (!target.manager || target.statusPath === undefined)
|
|
158
117
|
return undefined;
|
|
@@ -190,7 +190,13 @@ export function buildOutpostLaneCommand(input) {
|
|
|
190
190
|
args.push("--tail", input.tail);
|
|
191
191
|
if (input.action === "logs" && input.json === true)
|
|
192
192
|
args.push("--json");
|
|
193
|
-
|
|
193
|
+
const callerLaneId = input.callerLaneId?.trim();
|
|
194
|
+
const callerSessionId = input.callerSessionId?.trim();
|
|
195
|
+
const callerEnv = [
|
|
196
|
+
callerLaneId ? `HQ_LANE_ID=${callerLaneId}` : undefined,
|
|
197
|
+
callerSessionId ? `HQ_SESSION_ID=${callerSessionId}` : undefined,
|
|
198
|
+
].filter((value) => value !== undefined);
|
|
199
|
+
return withRemoteHqDir(joinCommandParts(callerEnv.length > 0 ? ["env", ...callerEnv, ...args] : args));
|
|
194
200
|
}
|
|
195
201
|
/** True when a remote CLI exited after rejecting the forwarded effort flag. */
|
|
196
202
|
export function outpostRejectedEffortOption(result) {
|
|
@@ -2,7 +2,6 @@ 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";
|
|
6
5
|
const CACHE_RELATIVE_PATH = path.join("workspace", "lanes-runs", ".cache", "outpost-hire-summaries.json");
|
|
7
6
|
export function outpostHireSummaryCachePath(hqRoot) {
|
|
8
7
|
return path.join(hqRoot, CACHE_RELATIVE_PATH);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Deduplicates identical remote snapshot queries within one command invocation. */
|
|
2
|
+
export declare class OutpostSnapshotInvocationMemo {
|
|
3
|
+
private readonly requests;
|
|
4
|
+
getOrCreate<T>(outpostId: string, query: unknown, fetch: () => Promise<T>): Promise<T>;
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=outpost-snapshot-memo.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Deduplicates identical remote snapshot queries within one command invocation. */
|
|
2
|
+
export class OutpostSnapshotInvocationMemo {
|
|
3
|
+
requests = new Map();
|
|
4
|
+
getOrCreate(outpostId, query, fetch) {
|
|
5
|
+
const key = JSON.stringify([outpostId, query]);
|
|
6
|
+
const existing = this.requests.get(key);
|
|
7
|
+
if (existing)
|
|
8
|
+
return existing;
|
|
9
|
+
const request = fetch();
|
|
10
|
+
this.requests.set(key, request);
|
|
11
|
+
return request;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=outpost-snapshot-memo.js.map
|
|
@@ -11,7 +11,7 @@ import { type ThrottleDeps, type CpuBusyObservation } from "./throttle.js";
|
|
|
11
11
|
import { readLanesRegistryFlag } from "./registry-flag.js";
|
|
12
12
|
import { type RunawayProcessInfo } from "./runaway-tool-guard.js";
|
|
13
13
|
import { type KeepAliveBlockedReason } from "./keep-alive.js";
|
|
14
|
-
import type { LaneCompanyRef, LaneRecord, LivenessState, QueuedReason, QuestionRecord, SeniorRef } from "./types.js";
|
|
14
|
+
import type { LaneCompanyRef, LaneAccountSelection, LaneRecord, LivenessState, QueuedReason, QuestionRecord, SeniorRef } from "./types.js";
|
|
15
15
|
import { type LaneMesh } from "./mesh-events.js";
|
|
16
16
|
/** Spec §9: unanswered at the current hop for 60 minutes → one hop. */
|
|
17
17
|
export declare const QUESTION_AGE_MS: number;
|
|
@@ -86,8 +86,6 @@ export interface ReconcileDeps {
|
|
|
86
86
|
skipAdmissionWithoutCpuSample?: boolean;
|
|
87
87
|
/** Flag reader seam used to assert rollout reads happen before admission locks. */
|
|
88
88
|
readRegistryFlag?: typeof readLanesRegistryFlag;
|
|
89
|
-
/** Pre-read manager gate decisions, scoped to each lane for this pass. */
|
|
90
|
-
managerStatusGateByLane?: ReadonlyMap<string, boolean>;
|
|
91
89
|
/** Active-company fallback for questions raised from an unscoped lane. */
|
|
92
90
|
resolveQuestionActiveCompany?: () => Promise<LaneCompanyRef>;
|
|
93
91
|
/** Resume queued messages after this pass wins a local dead-worker CAS. */
|
|
@@ -102,6 +100,7 @@ export interface ReconcileDeps {
|
|
|
102
100
|
prepareKeepAliveClaudeResume?: (lane: LaneRecord) => Promise<{
|
|
103
101
|
lane: LaneRecord;
|
|
104
102
|
selectionFailure?: string;
|
|
103
|
+
accountSelection?: LaneAccountSelection;
|
|
105
104
|
}>;
|
|
106
105
|
/** Process-table snapshot seam for the opt-in runaway-search guard. */
|
|
107
106
|
runawayToolProcessLister?: () => readonly RunawayProcessInfo[];
|
|
@@ -32,7 +32,6 @@ import { answeredQuestionEntries, questionRecordRefusalCode, raiseEnvelopeQuesti
|
|
|
32
32
|
import { SILENCE_MS, appendWatcherEvent, classifyLaneExit, isPidAlive, missingExpectedEnvelopeFile, readLatestEnvelopeFile, readLatestEnvelopeRecord, watcherEventsPath, watcherOutLogPath, writeWatcherDecision, } from "./watcher.js";
|
|
33
33
|
import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshCompany, } from "./mesh-events.js";
|
|
34
34
|
import { activeLaneStories, laneStories, storyStatusForEnvelope } from "./stories.js";
|
|
35
|
-
import { resolveManagerStatusGateForLane } from "./manager-status-gate.js";
|
|
36
35
|
import { decideEnvelopeRepair, envelopeQuestionIds, routeProviderFailure } from "./watcher.js";
|
|
37
36
|
import { PROVIDER_REJECTED_REASON, providerRejectionDetails, readProviderRejection, } from "./provider-rejection.js";
|
|
38
37
|
/** Spec §9: unanswered at the current hop for 60 minutes → one hop. */
|
|
@@ -48,43 +47,11 @@ export const PICKUP_VALIDATION_MAX_AGE_MS = 15 * 60 * 1000;
|
|
|
48
47
|
export const PICKUP_VALIDATION_TIMEOUT_REASON = "pickup_validation_timeout";
|
|
49
48
|
export const RECONCILE_UNIT_NAME = "hq-lanes-reconcile.service";
|
|
50
49
|
export const RECONCILE_TIMER_NAME = "hq-lanes-reconcile.timer";
|
|
51
|
-
function spawnDepsForHost(deps
|
|
50
|
+
function spawnDepsForHost(deps) {
|
|
52
51
|
return {
|
|
53
52
|
...(deps.spawnLaneDeps ?? {}),
|
|
54
53
|
hostname: deps.host.hostname,
|
|
55
54
|
admissionHost: deps.host,
|
|
56
|
-
...(lane !== undefined && deps.managerStatusGateByLane?.has(lane.lane_id)
|
|
57
|
-
? { managerStatusGateEnabled: deps.managerStatusGateByLane.get(lane.lane_id) === true }
|
|
58
|
-
: {}),
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
async function prepareManagerStatusGateStarts(root, lanes, deps, summary) {
|
|
62
|
-
const enabledByLane = new Map();
|
|
63
|
-
let gateFailureRecorded = false;
|
|
64
|
-
if (!deps.dryRun) {
|
|
65
|
-
await Promise.all(lanes.filter((lane) => laneRunsHere(lane, deps.host)).map(async (lane) => {
|
|
66
|
-
const resolution = await resolveManagerStatusGateForLane(root, lane, {
|
|
67
|
-
env: deps.env,
|
|
68
|
-
readFlag: deps.readRegistryFlag,
|
|
69
|
-
onError: (err) => {
|
|
70
|
-
if (gateFailureRecorded)
|
|
71
|
-
return;
|
|
72
|
-
gateFailureRecorded = true;
|
|
73
|
-
const code = err instanceof LanesError ? err.error : "manager_status_gate_unavailable";
|
|
74
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
75
|
-
summary.skipped.push({
|
|
76
|
-
kind: "lane",
|
|
77
|
-
id: lane.lane_id,
|
|
78
|
-
why: `manager status gate disabled for this reconcile pass: ${code}: ${message}`,
|
|
79
|
-
});
|
|
80
|
-
},
|
|
81
|
-
});
|
|
82
|
-
enabledByLane.set(lane.lane_id, resolution.enabled);
|
|
83
|
-
}));
|
|
84
|
-
}
|
|
85
|
-
return {
|
|
86
|
-
...deps,
|
|
87
|
-
managerStatusGateByLane: enabledByLane,
|
|
88
55
|
};
|
|
89
56
|
}
|
|
90
57
|
function logSkip(skip) {
|
|
@@ -854,7 +821,7 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
854
821
|
if (deps.afterLoad)
|
|
855
822
|
await deps.afterLoad();
|
|
856
823
|
await runRunawayToolGuardPass(hqRoot, lanes, config, deps, now, summary);
|
|
857
|
-
const startDeps =
|
|
824
|
+
const startDeps = deps;
|
|
858
825
|
markOverdueLanes(hqRoot, lanes, summary, deps, now);
|
|
859
826
|
await heartbeatAndDeadHosts(hqRoot, lanes, summary, startDeps, now);
|
|
860
827
|
for (const question of questions) {
|
|
@@ -1229,7 +1196,7 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
|
|
|
1229
1196
|
classified.account) {
|
|
1230
1197
|
try {
|
|
1231
1198
|
const recovered = await retryLaneOnAlternateAccount(hqRoot, classified, accountRetryReason, "reconcile", {
|
|
1232
|
-
...spawnDepsForHost(deps
|
|
1199
|
+
...spawnDepsForHost(deps),
|
|
1233
1200
|
});
|
|
1234
1201
|
summary.dead_host.push({
|
|
1235
1202
|
id: lane.lane_id,
|
|
@@ -1512,7 +1479,7 @@ async function admitQueued(hqRoot, lanes, summary, deps, now) {
|
|
|
1512
1479
|
nowMs,
|
|
1513
1480
|
});
|
|
1514
1481
|
const spawnFn = deps.spawn ??
|
|
1515
|
-
((root, lane) => spawnLaneWorker(root, lane, spawnDepsForHost(deps
|
|
1482
|
+
((root, lane) => spawnLaneWorker(root, lane, spawnDepsForHost(deps)));
|
|
1516
1483
|
const running = lanes.filter((lane) => (lane.state === "running" ||
|
|
1517
1484
|
hasFreshAdmissionReservation(lane, now.getTime()) ||
|
|
1518
1485
|
hasActivePendingResume(lane, now.getTime())) &&
|
|
@@ -1699,6 +1666,21 @@ function recordKeepAliveEvent(hqRoot, laneId, summary, event) {
|
|
|
1699
1666
|
});
|
|
1700
1667
|
}
|
|
1701
1668
|
}
|
|
1669
|
+
function keepAliveAccountSelectionEvidence(selection) {
|
|
1670
|
+
if (selection === undefined)
|
|
1671
|
+
return undefined;
|
|
1672
|
+
return {
|
|
1673
|
+
account: selection.account,
|
|
1674
|
+
selected_at: selection.selected_at,
|
|
1675
|
+
reason: selection.reason,
|
|
1676
|
+
considered: selection.considered.map((row) => ({
|
|
1677
|
+
account: row.account,
|
|
1678
|
+
live_lanes: row.live_lanes,
|
|
1679
|
+
eligible: row.eligible,
|
|
1680
|
+
reason: row.ineligible_reason ?? null,
|
|
1681
|
+
})),
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1702
1684
|
function keepAliveSkipGuidance(laneId, reason) {
|
|
1703
1685
|
switch (reason) {
|
|
1704
1686
|
case "missing_opt_in_time":
|
|
@@ -1892,10 +1874,12 @@ async function resumeBlockedKeepAlive(hqRoot, lane, questions, summary, deps, no
|
|
|
1892
1874
|
return;
|
|
1893
1875
|
}
|
|
1894
1876
|
let current = latest;
|
|
1877
|
+
let accountSelection;
|
|
1895
1878
|
if (current.provider === "claude" && deps.prepareKeepAliveClaudeResume !== undefined) {
|
|
1896
1879
|
try {
|
|
1897
1880
|
const prepared = await deps.prepareKeepAliveClaudeResume(current);
|
|
1898
1881
|
current = prepared.lane;
|
|
1882
|
+
accountSelection = prepared.accountSelection;
|
|
1899
1883
|
}
|
|
1900
1884
|
catch (error) {
|
|
1901
1885
|
markDirty(summary, { kind: "lane", id: lane.lane_id, why: `blocked keep-alive account preparation failed: ${error instanceof Error ? error.message : String(error)}` });
|
|
@@ -1917,8 +1901,16 @@ async function resumeBlockedKeepAlive(hqRoot, lane, questions, summary, deps, no
|
|
|
1917
1901
|
const queued = casLaneIf(hqRoot, latest.lane_id, (latestRecord) => latestRecord.version === expectedVersion && latestRecord.state === "blocked" && latestRecord.last_envelope?.decision === "blocked" && latestRecord.last_envelope.at === terminalAt, (current) => ({
|
|
1918
1902
|
...result.lane,
|
|
1919
1903
|
version: current.version,
|
|
1904
|
+
pending_resume: result.lane.pending_resume === undefined
|
|
1905
|
+
? undefined
|
|
1906
|
+
: { ...result.lane.pending_resume, keep_alive: true },
|
|
1920
1907
|
keep_alive_blocked_resume_streak: priorResumes + 1,
|
|
1921
1908
|
keep_alive_resume_count: (current.keep_alive_resume_count ?? 0) + 1,
|
|
1909
|
+
keep_alive_history: appendCappedHistory(current.keep_alive_history ?? [], {
|
|
1910
|
+
at: now.toISOString(),
|
|
1911
|
+
resumed_from: "blocked",
|
|
1912
|
+
account: current.account ?? null,
|
|
1913
|
+
}),
|
|
1922
1914
|
keep_alive_last_resume_at: now.toISOString(),
|
|
1923
1915
|
status_gate_history: appendCappedHistory(current.status_gate_history ?? [], {
|
|
1924
1916
|
event: "keep_alive_blocked_resumed",
|
|
@@ -1935,6 +1927,9 @@ async function resumeBlockedKeepAlive(hqRoot, lane, questions, summary, deps, no
|
|
|
1935
1927
|
at: now.toISOString(),
|
|
1936
1928
|
attempt: (lane.keep_alive_resume_count ?? 0) + 1,
|
|
1937
1929
|
resumed_from: "blocked",
|
|
1930
|
+
...(accountSelection !== undefined
|
|
1931
|
+
? { account_selection: keepAliveAccountSelectionEvidence(accountSelection) }
|
|
1932
|
+
: {}),
|
|
1938
1933
|
});
|
|
1939
1934
|
}
|
|
1940
1935
|
}
|
|
@@ -2390,6 +2385,7 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
|
|
|
2390
2385
|
writeAnsweredQuestions(hqRoot, current, questions);
|
|
2391
2386
|
}
|
|
2392
2387
|
let accountSelectionFailure;
|
|
2388
|
+
let accountSelection;
|
|
2393
2389
|
if (current.provider === "claude" && deps.prepareKeepAliveClaudeResume !== undefined) {
|
|
2394
2390
|
const optInBeforePreparation = current.keep_alive_opted_in_at;
|
|
2395
2391
|
let preparedLane;
|
|
@@ -2397,6 +2393,7 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
|
|
|
2397
2393
|
const prepared = await deps.prepareKeepAliveClaudeResume(current);
|
|
2398
2394
|
preparedLane = prepared.lane;
|
|
2399
2395
|
accountSelectionFailure = prepared.selectionFailure;
|
|
2396
|
+
accountSelection = prepared.accountSelection;
|
|
2400
2397
|
}
|
|
2401
2398
|
catch (err) {
|
|
2402
2399
|
// Account balancing is best-effort for keep-alive. A failure to read
|
|
@@ -2514,12 +2511,18 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
|
|
|
2514
2511
|
pending_resume: {
|
|
2515
2512
|
session_id: currentSessionId,
|
|
2516
2513
|
trigger: carriesAnsweredQuestions ? "answered_questions" : "resume",
|
|
2514
|
+
keep_alive: true,
|
|
2517
2515
|
resumed_from: resumedFrom,
|
|
2518
2516
|
text: resumeText,
|
|
2519
2517
|
compact: true,
|
|
2520
2518
|
at: nowIso,
|
|
2521
2519
|
},
|
|
2522
2520
|
keep_alive_resume_count: attempt,
|
|
2521
|
+
keep_alive_history: appendCappedHistory(latestLane.keep_alive_history ?? [], {
|
|
2522
|
+
at: nowIso,
|
|
2523
|
+
resumed_from: resumedFrom,
|
|
2524
|
+
account: latestLane.account ?? null,
|
|
2525
|
+
}),
|
|
2523
2526
|
keep_alive_last_resume_at: nowIso,
|
|
2524
2527
|
keep_alive_next_eligible_at: nextEligibleAt,
|
|
2525
2528
|
keep_alive_exhausted_at: undefined,
|
|
@@ -2573,6 +2576,9 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
|
|
|
2573
2576
|
attempt,
|
|
2574
2577
|
next_eligible_at: nextEligibleAt,
|
|
2575
2578
|
resumed_from: resumedFrom,
|
|
2579
|
+
...(accountSelection !== undefined
|
|
2580
|
+
? { account_selection: keepAliveAccountSelectionEvidence(accountSelection) }
|
|
2581
|
+
: {}),
|
|
2576
2582
|
});
|
|
2577
2583
|
}
|
|
2578
2584
|
}
|
|
@@ -2604,7 +2610,7 @@ async function resumeAwaiting(hqRoot, lanes, questions, summary, deps, onlyLaneI
|
|
|
2604
2610
|
const throttle = await decideAdmission(hqRoot, config.throttle, throttleDeps, deps.cpuBusyFraction, deps.cpuObservation);
|
|
2605
2611
|
throttleDecision = throttle.decision;
|
|
2606
2612
|
}
|
|
2607
|
-
const spawnFn = deps.spawn ?? ((root, lane) => spawnLaneWorker(root, lane, spawnDepsForHost(deps
|
|
2613
|
+
const spawnFn = deps.spawn ?? ((root, lane) => spawnLaneWorker(root, lane, spawnDepsForHost(deps)));
|
|
2608
2614
|
const nowMs = deps.now().getTime();
|
|
2609
2615
|
const fallbackAnswerAt = new Date(nowMs).toISOString();
|
|
2610
2616
|
const answerAtByLane = new Map();
|
|
@@ -88,8 +88,6 @@ export interface SpawnLaneDeps {
|
|
|
88
88
|
runCompaction?: RunCompactionFn;
|
|
89
89
|
/** Finds a session transcript under an account home. Injected in tests. */
|
|
90
90
|
locateTranscript?: (home: string, sessionId: string) => string | null;
|
|
91
|
-
/** Pre-read by the async command/reconcile boundary before this sync spawn. */
|
|
92
|
-
managerStatusGateEnabled?: boolean;
|
|
93
91
|
}
|
|
94
92
|
export interface ProviderLaunch {
|
|
95
93
|
command: string;
|