@indigoai-us/hq-cli 5.276.0 → 5.278.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 +99 -0
- package/assets/scaffold/core/scripts/hq-delegate-pickup.sh +99 -0
- package/assets/scaffold/core/scripts/hq-delegate-repo.sh +158 -0
- package/assets/scaffold/core/scripts/hq-delegate-resolve.sh +145 -0
- package/assets/scaffold/core/scripts/hq-delegate-secrets.sh +129 -0
- package/assets/scaffold/core/scripts/hq-delegate-transfer.sh +175 -0
- package/assets/scaffold/core/scripts/lib/secret-patterns.sh +58 -0
- package/dist/command-catalog.generated.d.ts +239 -1
- package/dist/command-catalog.generated.js +302 -1
- package/dist/commands/core-delegate.d.ts +6 -0
- package/dist/commands/core-delegate.js +987 -0
- package/dist/commands/core.js +10 -0
- package/dist/commands/daemon.d.ts +3 -0
- package/dist/commands/daemon.js +173 -1
- package/dist/commands/dm.d.ts +2 -0
- package/dist/commands/dm.js +11 -4
- package/dist/commands/lanes-accounts.js +44 -2
- package/dist/commands/lanes-message.js +69 -12
- package/dist/commands/lanes-resume.js +2 -0
- package/dist/commands/lanes-shared.d.ts +3 -0
- package/dist/commands/lanes-shared.js +12 -2
- package/dist/commands/lanes.d.ts +5 -1
- package/dist/commands/lanes.js +399 -47
- package/dist/commands/secrets.d.ts +16 -1
- package/dist/commands/secrets.js +77 -48
- package/dist/commands/sync-mode.d.ts +11 -0
- package/dist/commands/sync-mode.js +30 -0
- package/dist/fast-scaffold.d.ts +17 -0
- package/dist/fast-scaffold.js +91 -0
- package/dist/index.js +17 -1
- package/dist/lanes-drain-hook.js +154 -7
- package/dist/lib/agent-kit/run/inbox.js +3 -2
- package/dist/lib/checkpoint-ontology/apply.d.ts +6 -0
- package/dist/lib/checkpoint-ontology/apply.js +111 -3
- package/dist/lib/checkpoint-ontology/plan.d.ts +23 -0
- package/dist/lib/checkpoint-ontology/plan.js +23 -0
- package/dist/lib/checkpoint-ontology/tree.d.ts +2 -0
- package/dist/lib/checkpoint-ontology/tree.js +34 -0
- package/dist/lib/core-utils/jq-json-serializer.d.ts +5 -0
- package/dist/lib/core-utils/jq-json-serializer.js +10 -0
- package/dist/lib/core-utils/secret-patterns.d.ts +41 -0
- package/dist/lib/core-utils/secret-patterns.js +33 -0
- package/dist/lib/daemon/catalog.d.ts +18 -2
- package/dist/lib/daemon/catalog.js +18 -2
- package/dist/lib/daemon/config.d.ts +6 -0
- package/dist/lib/daemon/config.js +15 -2
- package/dist/lib/daemon/requests.d.ts +4 -0
- package/dist/lib/daemon/requests.js +2 -0
- package/dist/lib/daemon/run.js +94 -8
- package/dist/lib/daemon/supervisor.d.ts +8 -0
- package/dist/lib/daemon/supervisor.js +101 -1
- package/dist/lib/daemon/sync-runner.d.ts +58 -0
- package/dist/lib/daemon/sync-runner.js +277 -0
- package/dist/lib/doctor/__testing__/run-doctor.d.ts +8 -0
- package/dist/lib/doctor/__testing__/run-doctor.js +52 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/lanes/claude-balance.d.ts +6 -1
- package/dist/lib/lanes/claude-balance.js +46 -8
- package/dist/lib/lanes/config.d.ts +2 -1
- package/dist/lib/lanes/config.js +31 -0
- package/dist/lib/lanes/hire-grant-dispatch.d.ts +2 -0
- package/dist/lib/lanes/hire-grant-dispatch.js +41 -0
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.d.ts +3 -0
- package/dist/lib/lanes/keep-alive.js +3 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +4 -0
- package/dist/lib/lanes/outpost-dispatch.js +11 -2
- package/dist/lib/lanes/reconcile.d.ts +9 -0
- package/dist/lib/lanes/reconcile.js +127 -2
- package/dist/lib/lanes/runaway-tool-guard.d.ts +38 -0
- package/dist/lib/lanes/runaway-tool-guard.js +284 -0
- package/dist/lib/lanes/spawn.js +17 -10
- package/dist/lib/lanes/store.js +45 -1
- package/dist/lib/lanes/types.d.ts +37 -1
- package/dist/lib/lanes/types.js +2 -2
- package/dist/lib/lanes/watcher.d.ts +2 -0
- package/dist/lib/lanes/watcher.js +13 -2
- package/dist/secrets-env-fast-path.d.ts +5 -0
- package/dist/secrets-env-fast-path.js +16 -0
- package/dist/secrets-env-fast-route.d.ts +18 -0
- package/dist/secrets-env-fast-route.js +54 -0
- package/package.json +2 -2
|
@@ -3,6 +3,7 @@ import type { HostCapacityAccount, HostCapacitySnapshot } from "./host-capacity.
|
|
|
3
3
|
import type { LaneHost, LaneRecord, LanesConfig } from "./types.js";
|
|
4
4
|
export declare const BALANCE_LIVE_STATES: readonly ["running", "queued", "awaiting_input"];
|
|
5
5
|
export declare const BALANCE_MOVABLE_STATES: readonly ["interrupted", "done", "awaiting_input"];
|
|
6
|
+
export declare const CLAUDE_RECENT_USAGE_WINDOW_MS: number;
|
|
6
7
|
export interface BalanceHostSnapshot {
|
|
7
8
|
capacity: HostCapacitySnapshot;
|
|
8
9
|
lanes: readonly LaneRecord[];
|
|
@@ -39,11 +40,12 @@ export interface ClaudeAccountLoad {
|
|
|
39
40
|
live_lanes: number;
|
|
40
41
|
counts_complete: boolean;
|
|
41
42
|
usage: NonNullable<HostCapacityAccount["usage"]> | null;
|
|
43
|
+
recent_usage_at: string | null;
|
|
42
44
|
hosts: AccountHostLoad[];
|
|
43
45
|
}
|
|
44
46
|
export interface ClaudeAccountSelection {
|
|
45
47
|
account: string;
|
|
46
|
-
signal: "usage" | "lane_count" | "explicit";
|
|
48
|
+
signal: "usage" | "recent_usage" | "lane_count" | "explicit";
|
|
47
49
|
reason: "least_loaded" | "explicit";
|
|
48
50
|
target_host: LaneHost;
|
|
49
51
|
considered: ClaudeAccountLoad[];
|
|
@@ -72,6 +74,9 @@ export declare function selectClaudeAccount(input: {
|
|
|
72
74
|
model?: string;
|
|
73
75
|
/** Keep the recorded account eligible for its already-running CLI model. */
|
|
74
76
|
recordedModelAccount?: string;
|
|
77
|
+
/** Recent completed-run tiebreak; callers pass the default-off hq-flags result. */
|
|
78
|
+
recentUsageEnabled?: boolean;
|
|
79
|
+
now?: Date;
|
|
75
80
|
explicitAccount?: string;
|
|
76
81
|
unavailableHosts?: readonly BalanceHostGap[];
|
|
77
82
|
}): ClaudeAccountSelection;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { LanesError } from "./errors.js";
|
|
2
2
|
export const BALANCE_LIVE_STATES = ["running", "queued", "awaiting_input"];
|
|
3
3
|
export const BALANCE_MOVABLE_STATES = ["interrupted", "done", "awaiting_input"];
|
|
4
|
+
export const CLAUDE_RECENT_USAGE_WINDOW_MS = 5 * 60 * 60 * 1000;
|
|
4
5
|
function sameHost(a, b) {
|
|
5
6
|
return a.kind === b.kind && a.id === b.id;
|
|
6
7
|
}
|
|
@@ -74,10 +75,31 @@ function usageScore(usage) {
|
|
|
74
75
|
return null;
|
|
75
76
|
return Math.max(usage.five_hour_percent, usage.weekly_percent);
|
|
76
77
|
}
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
function recentUsageAt(hosts, account, now) {
|
|
79
|
+
if (hosts.some((host) => !host.laneListAvailable))
|
|
80
|
+
return null;
|
|
81
|
+
const nowMs = now.getTime();
|
|
82
|
+
const cutoff = nowMs - CLAUDE_RECENT_USAGE_WINDOW_MS;
|
|
83
|
+
let latestAt = null;
|
|
84
|
+
let latestMs = cutoff - 1;
|
|
85
|
+
for (const host of hosts) {
|
|
86
|
+
for (const lane of host.lanes) {
|
|
87
|
+
if (lane.provider !== "claude" || lane.account !== account ||
|
|
88
|
+
lane.last_run_usage === undefined || lane.last_run_usage_at === undefined)
|
|
89
|
+
continue;
|
|
90
|
+
const atMs = Date.parse(lane.last_run_usage_at);
|
|
91
|
+
if (!Number.isFinite(atMs) || atMs < cutoff || atMs > nowMs || atMs <= latestMs)
|
|
92
|
+
continue;
|
|
93
|
+
latestMs = atMs;
|
|
94
|
+
latestAt = lane.last_run_usage_at;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return latestAt;
|
|
98
|
+
}
|
|
99
|
+
function selectionSignal(loads, recentUsageEnabled, laneSnapshotsComplete) {
|
|
100
|
+
if (loads.length > 0 && loads.every((load) => usageScore(load.usage) !== null))
|
|
101
|
+
return "usage";
|
|
102
|
+
return recentUsageEnabled && laneSnapshotsComplete ? "recent_usage" : "lane_count";
|
|
81
103
|
}
|
|
82
104
|
function targetIneligibilityReason(accountId, globalEnabled, allowedAccounts, target) {
|
|
83
105
|
if (!globalEnabled)
|
|
@@ -120,6 +142,7 @@ function buildLoads(input) {
|
|
|
120
142
|
live_lanes: knownCounts.reduce((sum, count) => sum + count, 0),
|
|
121
143
|
counts_complete: hosts.every((host) => host.live_lanes !== null && host.count_source !== "capacity_partial"),
|
|
122
144
|
usage: input.target.capacity.providers.claude.accounts[account]?.usage ?? null,
|
|
145
|
+
recent_usage_at: recentUsageAt(input.hosts, account, input.now),
|
|
123
146
|
hosts,
|
|
124
147
|
};
|
|
125
148
|
});
|
|
@@ -133,7 +156,8 @@ export function selectClaudeAccount(input) {
|
|
|
133
156
|
unavailable_hosts: input.unavailableHosts ?? [],
|
|
134
157
|
});
|
|
135
158
|
}
|
|
136
|
-
const
|
|
159
|
+
const now = input.now ?? new Date();
|
|
160
|
+
const { loads, skipped } = buildLoads({ ...input, target, now });
|
|
137
161
|
if (input.explicitAccount !== undefined) {
|
|
138
162
|
const selected = loads.find((load) => load.account === input.explicitAccount);
|
|
139
163
|
if (!selected || !selected.eligible) {
|
|
@@ -173,14 +197,28 @@ export function selectClaudeAccount(input) {
|
|
|
173
197
|
unavailable_hosts: input.unavailableHosts ?? [],
|
|
174
198
|
});
|
|
175
199
|
}
|
|
176
|
-
const signal = selectionSignal(eligible);
|
|
200
|
+
const signal = selectionSignal(eligible, input.recentUsageEnabled === true, input.hosts.every((host) => host.laneListAvailable) && (input.unavailableHosts?.length ?? 0) === 0);
|
|
177
201
|
eligible.sort((a, b) => {
|
|
178
202
|
if (signal === "usage") {
|
|
179
203
|
const byUsage = usageScore(a.usage) - usageScore(b.usage);
|
|
180
204
|
if (byUsage !== 0)
|
|
181
205
|
return byUsage;
|
|
182
206
|
}
|
|
183
|
-
|
|
207
|
+
const byLiveLanes = a.live_lanes - b.live_lanes;
|
|
208
|
+
if (byLiveLanes !== 0)
|
|
209
|
+
return byLiveLanes;
|
|
210
|
+
if (signal === "recent_usage") {
|
|
211
|
+
if (a.recent_usage_at === null && b.recent_usage_at !== null)
|
|
212
|
+
return -1;
|
|
213
|
+
if (a.recent_usage_at !== null && b.recent_usage_at === null)
|
|
214
|
+
return 1;
|
|
215
|
+
if (a.recent_usage_at !== null && b.recent_usage_at !== null) {
|
|
216
|
+
const byRecentUsage = Date.parse(a.recent_usage_at) - Date.parse(b.recent_usage_at);
|
|
217
|
+
if (byRecentUsage !== 0)
|
|
218
|
+
return byRecentUsage;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return a.account.localeCompare(b.account);
|
|
184
222
|
});
|
|
185
223
|
return {
|
|
186
224
|
account: eligible[0].account,
|
|
@@ -196,7 +234,7 @@ export function accountLoadsForReport(input) {
|
|
|
196
234
|
const first = input.hosts[0];
|
|
197
235
|
if (!first)
|
|
198
236
|
return [];
|
|
199
|
-
return buildLoads({ hosts: input.hosts, target: first, config: input.config }).loads.map((load) => ({
|
|
237
|
+
return buildLoads({ hosts: input.hosts, target: first, config: input.config, now: new Date() }).loads.map((load) => ({
|
|
200
238
|
...load,
|
|
201
239
|
eligible: false,
|
|
202
240
|
ineligible_reason: undefined,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* that resolve to the same home are refused (identity-keyed-cap rule).
|
|
8
8
|
*/
|
|
9
9
|
import * as fs from "node:fs";
|
|
10
|
-
import { type AccountConfig, type LaneProvider, type LanesConfig, type LivenessWindows, type ProviderConfig, type TrackerChecksConfig } from "./types.js";
|
|
10
|
+
import { type AccountConfig, type LaneProvider, type LanesConfig, type RunawaySearchGuardConfig, type LivenessWindows, type ProviderConfig, type TrackerChecksConfig } from "./types.js";
|
|
11
11
|
export declare const LANES_CONFIG_REL: string;
|
|
12
12
|
export declare const DEFAULT_MAX_QUEUED = 3;
|
|
13
13
|
export declare const DEFAULT_CPU_BUSY_PAUSE = 0.85;
|
|
@@ -44,6 +44,7 @@ export declare const DEFAULT_BLOCKER_FRESHNESS_HOURS = 4;
|
|
|
44
44
|
/** Manager key results must be re-measured within six hours by default. */
|
|
45
45
|
export declare const DEFAULT_KR_FRESHNESS_HOURS = 6;
|
|
46
46
|
export declare const DEFAULT_TRACKER_CHECKS: TrackerChecksConfig;
|
|
47
|
+
export declare const DEFAULT_RUNAWAY_SEARCH_GUARD_CONFIG: RunawaySearchGuardConfig;
|
|
47
48
|
export declare const DEFAULT_LIVENESS_WINDOWS: LivenessWindows;
|
|
48
49
|
export declare const DEFAULT_PROVIDER_MAX_RUNNING: Record<LaneProvider, number>;
|
|
49
50
|
export declare const DEFAULT_ACCOUNT_HOMES: Record<LaneProvider, string>;
|
package/dist/lib/lanes/config.js
CHANGED
|
@@ -56,6 +56,10 @@ export const DEFAULT_TRACKER_CHECKS = {
|
|
|
56
56
|
kpi_value_max_age_ms: 24 * 60 * 60 * 1000,
|
|
57
57
|
directive_ack_timeout_ms: 30 * 60 * 1000,
|
|
58
58
|
};
|
|
59
|
+
export const DEFAULT_RUNAWAY_SEARCH_GUARD_CONFIG = {
|
|
60
|
+
max_age_seconds: 20,
|
|
61
|
+
find_maxdepth_exempt: 3,
|
|
62
|
+
};
|
|
59
63
|
export const DEFAULT_LIVENESS_WINDOWS = {
|
|
60
64
|
live_within_ms: DEFAULT_LIVE_WITHIN_MS,
|
|
61
65
|
turn_start_lost_ms: DEFAULT_TURN_START_LOST_MS,
|
|
@@ -252,6 +256,21 @@ function parsePreferredAccounts(provider, value, accounts) {
|
|
|
252
256
|
return [...preferred];
|
|
253
257
|
}
|
|
254
258
|
export function assertUniqueAccountHomes(provider, accounts) {
|
|
259
|
+
// An override must not let one account reuse another account's shared default home.
|
|
260
|
+
const declaredHomes = new Map();
|
|
261
|
+
for (const [id, account] of Object.entries(accounts)) {
|
|
262
|
+
const homes = new Set([
|
|
263
|
+
account.home,
|
|
264
|
+
...Object.values(account.hosts ?? {}).map(({ home }) => home),
|
|
265
|
+
]);
|
|
266
|
+
for (const home of homes) {
|
|
267
|
+
const prior = declaredHomes.get(home);
|
|
268
|
+
if (prior !== undefined && prior !== id) {
|
|
269
|
+
throw new LanesConfigError("duplicate_account_home", `providers.${provider} accounts ${JSON.stringify(prior)} and ${JSON.stringify(id)} declare the same home ${home}`, { provider, accounts: [prior, id], home });
|
|
270
|
+
}
|
|
271
|
+
declaredHomes.set(home, id);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
255
274
|
const hostnames = new Set(Object.values(accounts).flatMap((account) => Object.keys(account.hosts ?? {})));
|
|
256
275
|
const check = (hostname) => {
|
|
257
276
|
const seen = new Map();
|
|
@@ -402,6 +421,16 @@ function parseTrackerChecks(raw) {
|
|
|
402
421
|
directive_ack_timeout_ms: positiveInt(checks.directive_ack_timeout_ms, "tracker.checks.directive_ack_timeout_ms", DEFAULT_TRACKER_CHECKS.directive_ack_timeout_ms),
|
|
403
422
|
};
|
|
404
423
|
}
|
|
424
|
+
function parseRunawaySearchGuard(raw) {
|
|
425
|
+
const guard = raw === undefined ? {} : raw;
|
|
426
|
+
if (!isPlainObject(guard)) {
|
|
427
|
+
throw new LanesConfigError("invalid_config", "runaway_search_guard must be an object");
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
max_age_seconds: positiveInt(guard.max_age_seconds, "runaway_search_guard.max_age_seconds", DEFAULT_RUNAWAY_SEARCH_GUARD_CONFIG.max_age_seconds),
|
|
431
|
+
find_maxdepth_exempt: positiveInt(guard.find_maxdepth_exempt, "runaway_search_guard.find_maxdepth_exempt", DEFAULT_RUNAWAY_SEARCH_GUARD_CONFIG.find_maxdepth_exempt),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
405
434
|
export function documentedDefaultConfig(deps = defaultDeps()) {
|
|
406
435
|
const providers = {};
|
|
407
436
|
for (const provider of LANE_PROVIDERS) {
|
|
@@ -425,6 +454,7 @@ export function documentedDefaultConfig(deps = defaultDeps()) {
|
|
|
425
454
|
},
|
|
426
455
|
liveness: { ...DEFAULT_LIVENESS_WINDOWS },
|
|
427
456
|
tracker: { checks: { ...DEFAULT_TRACKER_CHECKS } },
|
|
457
|
+
runaway_search_guard: { ...DEFAULT_RUNAWAY_SEARCH_GUARD_CONFIG },
|
|
428
458
|
session_question_timeout_secs: DEFAULT_SESSION_QUESTION_TIMEOUT_SECS,
|
|
429
459
|
blocker_freshness_hours: DEFAULT_BLOCKER_FRESHNESS_HOURS,
|
|
430
460
|
kr_freshness_hours: DEFAULT_KR_FRESHNESS_HOURS,
|
|
@@ -469,6 +499,7 @@ export function parseLanesConfig(raw, deps = defaultDeps()) {
|
|
|
469
499
|
throttle: parseThrottle(raw.throttle),
|
|
470
500
|
liveness: parseLiveness(raw.liveness),
|
|
471
501
|
tracker: { checks: parseTrackerChecks(raw.tracker) },
|
|
502
|
+
runaway_search_guard: parseRunawaySearchGuard(raw.runaway_search_guard),
|
|
472
503
|
session_question_timeout_secs: positiveInt(raw.session_question_timeout_secs, "session_question_timeout_secs", DEFAULT_SESSION_QUESTION_TIMEOUT_SECS),
|
|
473
504
|
blocker_freshness_hours: positiveNumber(raw.blocker_freshness_hours, "blocker_freshness_hours", DEFAULT_BLOCKER_FRESHNESS_HOURS),
|
|
474
505
|
kr_freshness_hours: positiveNumber(raw.kr_freshness_hours, "kr_freshness_hours", DEFAULT_KR_FRESHNESS_HOURS),
|
|
@@ -18,6 +18,8 @@ export interface OutpostHireGrantContext {
|
|
|
18
18
|
superseded_message_caller_id?: string;
|
|
19
19
|
superseded_message_session_id?: string;
|
|
20
20
|
}
|
|
21
|
+
/** Select the owner records needed to reproduce the primary's grant hierarchy. */
|
|
22
|
+
export declare function selectOutpostHireGrantHierarchy(targetLaneId: string, lanes: readonly LaneRecord[]): LaneRecord[];
|
|
21
23
|
/** Encode only the owner-relevant hierarchy from the primary's complete store. */
|
|
22
24
|
export declare function createOutpostHireGrantContext(target: LaneRecord, lanes: readonly LaneRecord[], supersededMessageCallerId?: string, supersededMessageSessionId?: string): string;
|
|
23
25
|
/** Decode and validate an owner grant context, binding it to the target lane. */
|
|
@@ -15,6 +15,47 @@ function copySenior(senior) {
|
|
|
15
15
|
? { kind: senior.kind }
|
|
16
16
|
: { kind: senior.kind, id: senior.id };
|
|
17
17
|
}
|
|
18
|
+
// Terminal relatives are included so the owner's final state overwrites a stale
|
|
19
|
+
// primary copy; the context builder counts only alive children afterwards.
|
|
20
|
+
function directChildren(lanes, parentLaneId) {
|
|
21
|
+
return lanes.filter((lane) => lane.senior.kind === "lane" && lane.senior.id === parentLaneId);
|
|
22
|
+
}
|
|
23
|
+
/** Select the owner records needed to reproduce the primary's grant hierarchy. */
|
|
24
|
+
export function selectOutpostHireGrantHierarchy(targetLaneId, lanes) {
|
|
25
|
+
const current = lanes.find((lane) => lane.lane_id === targetLaneId);
|
|
26
|
+
if (!current)
|
|
27
|
+
return [];
|
|
28
|
+
const relevant = new Set([current.lane_id]);
|
|
29
|
+
if (current.senior.kind === "lane" && current.senior.id) {
|
|
30
|
+
relevant.add(current.senior.id);
|
|
31
|
+
for (const sibling of directChildren(lanes, current.senior.id)) {
|
|
32
|
+
relevant.add(sibling.lane_id);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
for (const child of directChildren(lanes, current.lane_id)) {
|
|
36
|
+
relevant.add(child.lane_id);
|
|
37
|
+
}
|
|
38
|
+
const companyUid = current.company?.uid;
|
|
39
|
+
const projectId = current.project_id;
|
|
40
|
+
if (companyUid && projectId) {
|
|
41
|
+
// A superseded-message caller must be checked against the same project and
|
|
42
|
+
// company rows that the complete hierarchy builder sees.
|
|
43
|
+
for (const candidate of lanes) {
|
|
44
|
+
if (candidate.company?.uid === companyUid && candidate.project_id === projectId) {
|
|
45
|
+
relevant.add(candidate.lane_id);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (current.senior.kind === "session" && companyUid) {
|
|
50
|
+
for (const candidate of lanes) {
|
|
51
|
+
if (candidate.company?.uid === companyUid && ((current.worker_profile !== undefined && candidate.worker_profile === current.worker_profile) ||
|
|
52
|
+
(projectId !== undefined && candidate.project_id === projectId && (candidate.hire_grant ?? 0) > 0))) {
|
|
53
|
+
relevant.add(candidate.lane_id);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return lanes.filter((lane) => relevant.has(lane.lane_id));
|
|
58
|
+
}
|
|
18
59
|
function snapshot(lane) {
|
|
19
60
|
return {
|
|
20
61
|
lane_id: lane.lane_id,
|
|
@@ -10,7 +10,7 @@ export type { AdmissionCapReason, AdmissionHost } from "./admission.js";
|
|
|
10
10
|
export { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "./host-capacity.js";
|
|
11
11
|
export type { HostCapacityAccount, HostCapacityProvider, HostCapacitySnapshot, HostPlacementChoice, } from "./host-capacity.js";
|
|
12
12
|
export { CONFIG_SCHEMA_VERSION, ENVELOPE_DECISIONS, ENVELOPE_SCHEMA_VERSION, HISTORY_CAP, HOST_KINDS, LANE_PROVIDERS, LANE_SCHEMA_VERSION, LANE_STORY_STATUSES, LANE_STATES, LIVENESS_STATES, QUESTION_BODY_STORES, QUESTION_SCHEMA_VERSION, QUESTION_STATUSES, QUEUED_REASONS, RAISED_VIA, ROUTING_REASONS, SENIOR_KINDS, STORY_STATUSES, TERMINAL_LANE_STATES, isEnvelopeDecision, isHostKind, isLaneProvider, isLaneState, isLivenessState, isQuestionStatus, isSeniorKind, isTerminalLaneState, parseLaneEnvelope, parseLaneProvider, parseLaneState, parseLivenessState, parseMajorSchema, parseQuestionStatus, parseSeniorKind, } from "./types.js";
|
|
13
|
-
export type { AccountConfig, AttributionHistoryEntry, EnvelopeDecision, EnvelopeQuestion, HostKind, LaneCompanyRef, LaneAccountSelection, LaneAccountSelectionConsidered, LaneAccountSelectionHostCount, LaneEnvelope, LaneHost, LaneProvider, LanePlacementPolicy, LaneRecord, LaneStory, LaneStoryStatus, PendingResume, AccountHistoryEntry, LaneState, LanesConfig, LastEnvelopeRef, LivenessState, LivenessWindows, ProviderConfig, QuestionAnswer, QuestionBodyRef, QuestionRecord, QuestionStatus, QueuedReason, RaisedVia, RoutingHistoryEntry, RoutingReason, SeniorHistoryEntry, SeniorKind, SeniorRef, WorkerProfileRepinHistoryEntry, StoryStatus, TerminalLaneState, ThrottleConfig, } from "./types.js";
|
|
13
|
+
export type { AccountConfig, AttributionHistoryEntry, EnvelopeDecision, EnvelopeQuestion, HostKind, LaneCompanyRef, LaneAccountSelection, LaneAccountSelectionConsidered, LaneAccountSelectionHostCount, LaneEnvelope, LaneHost, LaneProvider, LanePlacementPolicy, LaneRecord, LaneStory, LaneStoryStatus, PendingResume, AccountHistoryEntry, LaneState, LanesConfig, LastEnvelopeRef, LivenessState, LivenessWindows, ProviderConfig, QuestionAnswer, QuestionBodyRef, QuestionRecord, QuestionStatus, QueuedReason, RaisedVia, RoutingHistoryEntry, RoutingReason, SeniorHistoryEntry, SeniorKind, SeniorRef, WorkerProfileRepinHistoryEntry, LaneSettingsHistoryEntry, StoryStatus, TerminalLaneState, ThrottleConfig, } from "./types.js";
|
|
14
14
|
export { ACTIVE_LANE_STORY_STATUSES, activeLaneStories, laneStories, laneStory, storyKey, storyStatusForEnvelope, storyWarning, withCurrentStory, withLaneStories, } from "./stories.js";
|
|
15
15
|
export { DEFAULT_ACCOUNT_HOMES, DEFAULT_AUTO_COMPACT_TOKENS, DEFAULT_COMPACT_AFTER_MS, DEFAULT_CPU_BUSY_PAUSE, DEFAULT_CPU_BUSY_RESUME, DEFAULT_LIVE_WITHIN_MS, DEFAULT_LIVENESS_WINDOWS, DEFAULT_MAX_QUEUED, DEFAULT_MIN_AVAILABLE_MEM_MB, DEFAULT_PROVIDER_MAX_RUNNING, DEFAULT_SILENT_LOST_MS, DEFAULT_TURN_START_LOST_MS, LANES_CONFIG_REL, accountMaxRunning, canonicalizeHome, documentedDefaultConfig, expandHome, lanesConfigPath, loadLanesConfig, parseLanesConfig, providerCompactAfterMs, providerAutoCompactTokens, providerQuotaDefaultHoldMs, DEFAULT_QUOTA_HOLD_MS, writeLanesConfig, } from "./config.js";
|
|
16
16
|
export type { ConfigLoadDeps, ConfigWriteDeps } from "./config.js";
|
|
@@ -4,6 +4,9 @@ export declare const LEAD_LANE_KEEP_ALIVE_DEFAULT_ENABLED = false;
|
|
|
4
4
|
/** Missing or unreadable registry values retain the legacy supersession behavior. */
|
|
5
5
|
export declare const LANE_SUPERSESSION_LINEAGE_AWARE_FLAG = "lanes.supersession-lineage-aware";
|
|
6
6
|
export declare const LANE_SUPERSESSION_LINEAGE_AWARE_DEFAULT_ENABLED = false;
|
|
7
|
+
/** Missing or unreadable registry values keep rejecting superseded messages. */
|
|
8
|
+
export declare const LANE_RUNNING_SUPERSEDED_MESSAGE_DELIVERY_FLAG = "lanes.running-superseded-message-delivery";
|
|
9
|
+
export declare const LANE_RUNNING_SUPERSEDED_MESSAGE_DELIVERY_DEFAULT_ENABLED = false;
|
|
7
10
|
export declare const LEAD_LANE_KEEP_ALIVE_MAX_RESUMES = 24;
|
|
8
11
|
export declare const LEAD_LANE_KEEP_ALIVE_BASE_BACKOFF_MS: number;
|
|
9
12
|
export declare const LEAD_LANE_KEEP_ALIVE_MAX_BACKOFF_MS: number;
|
|
@@ -8,6 +8,9 @@ export const LEAD_LANE_KEEP_ALIVE_DEFAULT_ENABLED = false;
|
|
|
8
8
|
/** Missing or unreadable registry values retain the legacy supersession behavior. */
|
|
9
9
|
export const LANE_SUPERSESSION_LINEAGE_AWARE_FLAG = "lanes.supersession-lineage-aware";
|
|
10
10
|
export const LANE_SUPERSESSION_LINEAGE_AWARE_DEFAULT_ENABLED = false;
|
|
11
|
+
/** Missing or unreadable registry values keep rejecting superseded messages. */
|
|
12
|
+
export const LANE_RUNNING_SUPERSEDED_MESSAGE_DELIVERY_FLAG = "lanes.running-superseded-message-delivery";
|
|
13
|
+
export const LANE_RUNNING_SUPERSEDED_MESSAGE_DELIVERY_DEFAULT_ENABLED = false;
|
|
11
14
|
export const LEAD_LANE_KEEP_ALIVE_MAX_RESUMES = 24;
|
|
12
15
|
export const LEAD_LANE_KEEP_ALIVE_BASE_BACKOFF_MS = 5 * 60 * 1000;
|
|
13
16
|
export const LEAD_LANE_KEEP_ALIVE_MAX_BACKOFF_MS = 60 * 60 * 1000;
|
|
@@ -82,6 +82,10 @@ export declare function buildOutpostCapacityCommand(): string;
|
|
|
82
82
|
/** Read lane records owned by an Outpost, using its configured HQ binary when set. */
|
|
83
83
|
export declare function buildOutpostLaneListCommand(input?: {
|
|
84
84
|
hqBinary?: string;
|
|
85
|
+
laneIds?: readonly string[];
|
|
86
|
+
active?: boolean;
|
|
87
|
+
hierarchy?: string;
|
|
88
|
+
snapshot?: boolean;
|
|
85
89
|
}): string;
|
|
86
90
|
/** Read questions on one Outpost, scoped to a company when supplied. */
|
|
87
91
|
export declare function buildOutpostQuestionListCommand(input?: {
|
|
@@ -67,12 +67,21 @@ export function buildOutpostCapacityCommand() {
|
|
|
67
67
|
}
|
|
68
68
|
/** Read lane records owned by an Outpost, using its configured HQ binary when set. */
|
|
69
69
|
export function buildOutpostLaneListCommand(input = {}) {
|
|
70
|
-
|
|
70
|
+
const args = [
|
|
71
71
|
input.hqBinary?.trim() || "hq",
|
|
72
72
|
"lanes",
|
|
73
73
|
"list",
|
|
74
74
|
"--json",
|
|
75
|
-
]
|
|
75
|
+
];
|
|
76
|
+
for (const laneId of input.laneIds ?? [])
|
|
77
|
+
args.push("--lane", laneId);
|
|
78
|
+
if (input.active)
|
|
79
|
+
args.push("--active");
|
|
80
|
+
if (input.hierarchy !== undefined)
|
|
81
|
+
args.push("--hierarchy", input.hierarchy);
|
|
82
|
+
if (input.snapshot)
|
|
83
|
+
args.push("--snapshot");
|
|
84
|
+
return withRemoteHqDir(joinCommandParts(args));
|
|
76
85
|
}
|
|
77
86
|
/** Read questions on one Outpost, scoped to a company when supplied. */
|
|
78
87
|
export function buildOutpostQuestionListCommand(input = {}) {
|
|
@@ -9,6 +9,7 @@ import { type LivenessDeps } from "./liveness.js";
|
|
|
9
9
|
import { type EnvelopeRepairSpawnInput, type SpawnLaneDeps } from "./spawn.js";
|
|
10
10
|
import { type ThrottleDeps } from "./throttle.js";
|
|
11
11
|
import { readLanesRegistryFlag } from "./registry-flag.js";
|
|
12
|
+
import { type RunawayProcessInfo } from "./runaway-tool-guard.js";
|
|
12
13
|
import { type KeepAliveBlockedReason } from "./keep-alive.js";
|
|
13
14
|
import type { LaneCompanyRef, LaneRecord, LivenessState, QueuedReason, QuestionRecord, SeniorRef } from "./types.js";
|
|
14
15
|
import { type LaneMesh } from "./mesh-events.js";
|
|
@@ -91,6 +92,14 @@ export interface ReconcileDeps {
|
|
|
91
92
|
meshEmitTimeoutMs?: number;
|
|
92
93
|
/** Fresh keep-alive eligibility read; injected to exercise opt-in changes before queueing. */
|
|
93
94
|
readKeepAliveRecheckLane?: (hqRoot: string, laneId: string) => LaneRecord;
|
|
95
|
+
/** Process-table snapshot seam for the opt-in runaway-search guard. */
|
|
96
|
+
runawayToolProcessLister?: () => readonly RunawayProcessInfo[];
|
|
97
|
+
/** Test seam: prevent real process signals while exercising the guard. */
|
|
98
|
+
runawayToolProcessSignal?: (pid: number, signal: NodeJS.Signals) => void;
|
|
99
|
+
/** Test seam for interleaving a watcher signal with an append-only guard event. */
|
|
100
|
+
runawayToolEventAppend?: (target: string, line: string) => void;
|
|
101
|
+
/** Test seam for validating a candidate PID against its process-table starttime. */
|
|
102
|
+
runawayToolProcessIsCurrent?: (pid: number, expectedStarttime: number) => boolean;
|
|
94
103
|
/**
|
|
95
104
|
* Awaited after lane/question records and liveness are loaded, before any
|
|
96
105
|
* CAS write. Test seam so two in-process passes can share a snapshot.
|
|
@@ -18,16 +18,18 @@ import { AccountResolutionError, LanesError, isLaneVersionConflict } from "./err
|
|
|
18
18
|
import { loadWorkerProfile } from "./profile.js";
|
|
19
19
|
import { terminalChildResumeHireGrantError, withHireGrantAdmission, } from "./grants.js";
|
|
20
20
|
import { classifySessionLiveness, defaultLivenessDeps, loadLivenessIndex, readDaemonPresenceConnected, unindexedClaudeSessionLiveness, } from "./liveness.js";
|
|
21
|
-
import { ANSWERED_QUESTIONS_FILE, armLaneWatcher, envelopeRepairMode, isPidRecordAlive, laneRunDir, readPidRecord, readSpawnManifest, markSpawnFailure, spawnEnvelopeRepair, spawnLaneWorker, retryLaneOnAlternateAccount, } from "./spawn.js";
|
|
21
|
+
import { ANSWERED_QUESTIONS_FILE, armLaneWatcher, envelopeRepairMode, isPidRecordAlive, laneRunDir, readPidRecord, readProcStarttime, readSpawnManifest, markSpawnFailure, spawnEnvelopeRepair, spawnLaneWorker, retryLaneOnAlternateAccount, } from "./spawn.js";
|
|
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
25
|
import { resolveLaneCompany } from "./lane-company.js";
|
|
26
|
+
import { appendDropboxMessage } from "./dropbox.js";
|
|
27
|
+
import { DEFAULT_RUNAWAY_SEARCH_GUARD_CONFIG, listHostProcesses, runRunawayToolGuard, RUNAWAY_TOOL_GUARD_FLAG, } from "./runaway-tool-guard.js";
|
|
26
28
|
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";
|
|
27
29
|
import { providerSupportsSessionResume } from "./delivery.js";
|
|
28
30
|
import { applyDefaultQuotaHold, readQuotaSignal, recordAccountQuota } from "./quota.js";
|
|
29
31
|
import { answeredQuestionEntries, questionRecordRefusalCode, raiseEnvelopeQuestions, resolveQuestionCompanyWithFallback, } from "./questions.js";
|
|
30
|
-
import { SILENCE_MS, classifyLaneExit, isPidAlive, missingExpectedEnvelopeFile, readLatestEnvelopeFile, readLatestEnvelopeRecord, watcherEventsPath, watcherOutLogPath, writeWatcherDecision, } from "./watcher.js";
|
|
32
|
+
import { SILENCE_MS, appendWatcherEvent, classifyLaneExit, isPidAlive, missingExpectedEnvelopeFile, readLatestEnvelopeFile, readLatestEnvelopeRecord, watcherEventsPath, watcherOutLogPath, writeWatcherDecision, } from "./watcher.js";
|
|
31
33
|
import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshCompany, } from "./mesh-events.js";
|
|
32
34
|
import { laneStories, storyStatusForEnvelope } from "./stories.js";
|
|
33
35
|
import { resolveManagerStatusGateForLane } from "./manager-status-gate.js";
|
|
@@ -658,6 +660,128 @@ function classifyDeadLocalLane(hqRoot, laneId, now) {
|
|
|
658
660
|
lane,
|
|
659
661
|
};
|
|
660
662
|
}
|
|
663
|
+
function runawayGuardCompanyKey(company) {
|
|
664
|
+
return company
|
|
665
|
+
? `${company.uid ?? ""}\u0000${company.slug ?? ""}`
|
|
666
|
+
: "unresolved-company";
|
|
667
|
+
}
|
|
668
|
+
function reportRunawayGuardError(summary, stage, error) {
|
|
669
|
+
summary.ok = false;
|
|
670
|
+
summary.errors.push({
|
|
671
|
+
code: `runaway_tool_guard_${stage}_failed`,
|
|
672
|
+
message: `runaway search guard ${stage} failed; no additional search processes were stopped`,
|
|
673
|
+
});
|
|
674
|
+
process.stderr.write(`${JSON.stringify({
|
|
675
|
+
event: "lanes.runaway_tool_guard.failed",
|
|
676
|
+
stage,
|
|
677
|
+
error: error instanceof Error ? error.name : "UnknownError",
|
|
678
|
+
fallback: "continue_without_argv",
|
|
679
|
+
})}\n`);
|
|
680
|
+
}
|
|
681
|
+
async function runRunawayToolGuardPass(hqRoot, lanes, config, deps, now, summary) {
|
|
682
|
+
if (deps.dryRun)
|
|
683
|
+
return;
|
|
684
|
+
const localRunningLanes = lanes.filter((lane) => lane.state === "running" && laneRunsHere(lane, deps.host));
|
|
685
|
+
if (localRunningLanes.length === 0)
|
|
686
|
+
return;
|
|
687
|
+
const groups = new Map();
|
|
688
|
+
for (const lane of localRunningLanes) {
|
|
689
|
+
let worker;
|
|
690
|
+
try {
|
|
691
|
+
worker = readPidRecord(path.join(laneRunDir(hqRoot, lane.lane_id), "worker.pid"));
|
|
692
|
+
if (!worker ||
|
|
693
|
+
worker.starttime === undefined ||
|
|
694
|
+
!isPidRecordAlive(worker, deps.isAlive))
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
catch (error) {
|
|
698
|
+
reportRunawayGuardError(summary, "worker_pid_read", error);
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
let company;
|
|
702
|
+
try {
|
|
703
|
+
company = resolveLaneCompany(hqRoot, lane).company;
|
|
704
|
+
}
|
|
705
|
+
catch (error) {
|
|
706
|
+
reportRunawayGuardError(summary, "company_resolution", error);
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
const key = runawayGuardCompanyKey(company);
|
|
710
|
+
const group = groups.get(key) ?? { company, lanes: [] };
|
|
711
|
+
group.lanes.push({ laneId: lane.lane_id, agentPid: worker.pid, agentStarttime: worker.starttime });
|
|
712
|
+
groups.set(key, group);
|
|
713
|
+
}
|
|
714
|
+
if (groups.size === 0)
|
|
715
|
+
return;
|
|
716
|
+
const readRegistryFlag = deps.readRegistryFlag ?? readLanesRegistryFlag;
|
|
717
|
+
const enabledGroups = new Set();
|
|
718
|
+
const env = deps.env ?? process.env;
|
|
719
|
+
await Promise.all([...groups.entries()].map(async ([key, group]) => {
|
|
720
|
+
try {
|
|
721
|
+
const enabled = await readRegistryFlag(RUNAWAY_TOOL_GUARD_FLAG, {
|
|
722
|
+
company: group.company ?? undefined,
|
|
723
|
+
env,
|
|
724
|
+
});
|
|
725
|
+
if (enabled)
|
|
726
|
+
enabledGroups.add(key);
|
|
727
|
+
}
|
|
728
|
+
catch (error) {
|
|
729
|
+
reportRunawayGuardError(summary, "flag_read", error);
|
|
730
|
+
}
|
|
731
|
+
}));
|
|
732
|
+
if (enabledGroups.size === 0)
|
|
733
|
+
return;
|
|
734
|
+
const laneProcesses = [...groups.entries()]
|
|
735
|
+
.filter(([key]) => enabledGroups.has(key))
|
|
736
|
+
.flatMap(([, group]) => group.lanes);
|
|
737
|
+
if (laneProcesses.length === 0)
|
|
738
|
+
return;
|
|
739
|
+
let processes;
|
|
740
|
+
try {
|
|
741
|
+
processes = (deps.runawayToolProcessLister ?? listHostProcesses)();
|
|
742
|
+
}
|
|
743
|
+
catch (error) {
|
|
744
|
+
reportRunawayGuardError(summary, "process_table_read", error);
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
const runawayConfig = config.runaway_search_guard ?? DEFAULT_RUNAWAY_SEARCH_GUARD_CONFIG;
|
|
748
|
+
try {
|
|
749
|
+
runRunawayToolGuard({
|
|
750
|
+
hqRoot,
|
|
751
|
+
lanes: laneProcesses,
|
|
752
|
+
processes,
|
|
753
|
+
config: runawayConfig,
|
|
754
|
+
now,
|
|
755
|
+
currentUid: process.getuid?.() ?? -1,
|
|
756
|
+
isCurrentProcess: deps.runawayToolProcessIsCurrent ??
|
|
757
|
+
((pid, expectedStarttime) => readProcStarttime(pid) === expectedStarttime),
|
|
758
|
+
signal: deps.runawayToolProcessSignal ?? ((pid, signal) => process.kill(pid, signal)),
|
|
759
|
+
recordEvent: (laneId, event) => appendWatcherEvent(watcherEventsPath(hqRoot, laneId), event, deps.runawayToolEventAppend),
|
|
760
|
+
notifyLane: (laneId, text) => appendDropboxMessage(hqRoot, laneId, {
|
|
761
|
+
at: now.toISOString(),
|
|
762
|
+
message_id: `runaway-search-guard:${laneId}:${now.getTime()}`,
|
|
763
|
+
kind: "system_notice",
|
|
764
|
+
text,
|
|
765
|
+
}),
|
|
766
|
+
reportError: (stage, errorName) => {
|
|
767
|
+
summary.ok = false;
|
|
768
|
+
summary.errors.push({
|
|
769
|
+
code: `runaway_tool_guard_${stage}_failed`,
|
|
770
|
+
message: `runaway search guard could not ${stage} a stopped process (${errorName})`,
|
|
771
|
+
});
|
|
772
|
+
process.stderr.write(`${JSON.stringify({
|
|
773
|
+
event: "lanes.runaway_tool_guard.failed",
|
|
774
|
+
stage,
|
|
775
|
+
error: errorName,
|
|
776
|
+
fallback: "continue_without_argv",
|
|
777
|
+
})}\n`);
|
|
778
|
+
},
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
catch (error) {
|
|
782
|
+
reportRunawayGuardError(summary, "execution", error);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
661
785
|
/** Unlocked pass body. `runReconcilePass` takes the process lock around this. */
|
|
662
786
|
export async function runPassBody(hqRoot, deps) {
|
|
663
787
|
const now = deps.now();
|
|
@@ -728,6 +852,7 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
728
852
|
: QUESTION_AGE_MS;
|
|
729
853
|
if (deps.afterLoad)
|
|
730
854
|
await deps.afterLoad();
|
|
855
|
+
await runRunawayToolGuardPass(hqRoot, lanes, config, deps, now, summary);
|
|
731
856
|
const startDeps = await prepareManagerStatusGateStarts(hqRoot, lanes, deps, summary);
|
|
732
857
|
markOverdueLanes(hqRoot, lanes, summary, deps, now);
|
|
733
858
|
await heartbeatAndDeadHosts(hqRoot, lanes, summary, startDeps, now);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { RunawaySearchGuardConfig } from "./types.js";
|
|
2
|
+
export declare const RUNAWAY_TOOL_GUARD_FLAG = "lanes.runaway-tool-guard";
|
|
3
|
+
export interface RunawayProcessInfo {
|
|
4
|
+
pid: number;
|
|
5
|
+
ppid: number;
|
|
6
|
+
elapsedSeconds: number;
|
|
7
|
+
uid: number;
|
|
8
|
+
command: string;
|
|
9
|
+
argv: readonly string[];
|
|
10
|
+
cwd: string;
|
|
11
|
+
starttime: number | null;
|
|
12
|
+
}
|
|
13
|
+
export interface RunawayLaneProcess {
|
|
14
|
+
laneId: string;
|
|
15
|
+
agentPid: number;
|
|
16
|
+
agentStarttime: number;
|
|
17
|
+
}
|
|
18
|
+
export interface RunawayToolGuardInput {
|
|
19
|
+
hqRoot: string;
|
|
20
|
+
lanes: readonly RunawayLaneProcess[];
|
|
21
|
+
processes: readonly RunawayProcessInfo[];
|
|
22
|
+
config: RunawaySearchGuardConfig;
|
|
23
|
+
now: Date;
|
|
24
|
+
currentUid: number;
|
|
25
|
+
isCurrentProcess: (pid: number, expectedStarttime: number) => boolean;
|
|
26
|
+
signal: (pid: number, signal: NodeJS.Signals) => void;
|
|
27
|
+
recordEvent: (laneId: string, event: Record<string, unknown>) => void;
|
|
28
|
+
notifyLane: (laneId: string, text: string) => void;
|
|
29
|
+
reportError: (stage: "signal" | "record" | "notify", errorName: string) => void;
|
|
30
|
+
}
|
|
31
|
+
export interface RunawayToolGuardResult {
|
|
32
|
+
stopped: number;
|
|
33
|
+
}
|
|
34
|
+
export declare function runRunawayToolGuard(input: RunawayToolGuardInput): RunawayToolGuardResult;
|
|
35
|
+
/** Read the host process table once. argv and cwd are read only for search tools. */
|
|
36
|
+
export declare function listHostProcesses(): RunawayProcessInfo[];
|
|
37
|
+
export { DEFAULT_RUNAWAY_SEARCH_GUARD_CONFIG } from "./config.js";
|
|
38
|
+
//# sourceMappingURL=runaway-tool-guard.d.ts.map
|