@indigoai-us/hq-cli 5.345.38 → 5.345.39
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 +8 -0
- package/dist/command-catalog.generated.d.ts +43 -0
- package/dist/command-catalog.generated.js +54 -0
- package/dist/commands/core-dm-bind-native.js +5 -4
- package/dist/commands/db-destroy.d.ts +15 -0
- package/dist/commands/db-destroy.js +143 -0
- package/dist/commands/db-dump.d.ts +9 -0
- package/dist/commands/db-dump.js +19 -11
- package/dist/commands/db.js +2 -0
- package/dist/commands/files-browse.js +1 -1
- package/dist/commands/lanes-message.js +14 -5
- package/dist/commands/lanes-resume.js +62 -14
- package/dist/commands/lanes.js +21 -4
- package/dist/commands/report.js +5 -1
- package/dist/lib/db/control-plane.d.ts +10 -0
- package/dist/lib/db/control-plane.js +18 -0
- package/dist/lib/deploy-api.js +2 -0
- package/dist/lib/doctor/fix/apply.d.ts +2 -0
- package/dist/lib/doctor/fix/apply.js +52 -1
- package/dist/lib/flag-registry.d.ts +1 -1
- package/dist/lib/flag-registry.js +20 -7
- package/dist/lib/lanes/accounts.js +24 -2
- package/dist/lib/lanes/delivery.d.ts +5 -2
- package/dist/lib/lanes/delivery.js +14 -5
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/reconcile.js +49 -5
- package/dist/lib/lanes/spawn.d.ts +7 -3
- package/dist/lib/lanes/spawn.js +97 -5
- package/dist/lib/lanes/store.js +54 -20
- package/dist/lib/lanes/story-ledger.d.ts +1 -1
- package/dist/lib/lanes/story-ledger.js +4 -4
- package/dist/lib/lanes/types.js +3 -3
- package/package.json +1 -1
package/dist/commands/lanes.js
CHANGED
|
@@ -63,7 +63,7 @@ import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, lan
|
|
|
63
63
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
64
64
|
import { listLaneReviews } from "../lib/lanes/reviews.js";
|
|
65
65
|
import { effectiveAccountHome } from "../lib/lanes/account-home.js";
|
|
66
|
-
import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, isFinishedResumableHireChild, childrenHoldingHireGrantCarve, laneHoldsHireGrantCarve, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, syncLaneToProjectStoryLedger, removeLaneFromProjectStoryLedger, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readAllLanes, readLane, sanitizeHostComponent, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, DEAD_HOST_MS, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, readWorkerRunResult, resumeCommandRefusal, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
|
|
66
|
+
import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, isFinishedResumableHireChild, childrenHoldingHireGrantCarve, laneHoldsHireGrantCarve, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, syncLaneToProjectStoryLedger, removeLaneFromProjectStoryLedger, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readAllLanes, readLane, sanitizeHostComponent, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, DEAD_HOST_MS, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, isFailedQuotaKeepAliveLane, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, readWorkerRunResult, resumeCommandRefusal, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
|
|
67
67
|
import { laneStories, storyWarning, } from "../lib/lanes/stories.js";
|
|
68
68
|
import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
|
|
69
69
|
import { laneCompanyUidSource, resolveLaneCompany } from "../lib/lanes/lane-company.js";
|
|
@@ -6015,6 +6015,19 @@ async function runLaneDoctor(ctx, pendingQuestionsForLane) {
|
|
|
6015
6015
|
const retiredEntries = loadRetiredLaneEntries(hqRoot);
|
|
6016
6016
|
const findings = [];
|
|
6017
6017
|
for (const lane of lanes) {
|
|
6018
|
+
if (lane.keep_alive === true &&
|
|
6019
|
+
lane.state === "failed" &&
|
|
6020
|
+
(lane.terminal_reason === "account_quota_exhausted" || lane.terminal_reason === "account_model_unavailable")) {
|
|
6021
|
+
findings.push({
|
|
6022
|
+
kind: "failed_keep_alive_lane",
|
|
6023
|
+
lane_id: lane.lane_id,
|
|
6024
|
+
senior: lane.senior,
|
|
6025
|
+
terminal_reason: lane.terminal_reason,
|
|
6026
|
+
...(isFailedQuotaKeepAliveLane(lane)
|
|
6027
|
+
? { suggested_command: `hq lanes resume ${lane.lane_id}` }
|
|
6028
|
+
: { suggested_action: "Inspect the quota reset details before authorizing a failed-lane resume." }),
|
|
6029
|
+
});
|
|
6030
|
+
}
|
|
6018
6031
|
if (lane.keep_alive !== true || isTerminalLaneState(lane.state))
|
|
6019
6032
|
continue;
|
|
6020
6033
|
const retiredEntry = retiredEntries.find((entry) => matchesRetiredLaneEntry(entry, lane.lane_id));
|
|
@@ -7206,6 +7219,11 @@ async function runReparentOwnerTarget(ctx, laneId, opts) {
|
|
|
7206
7219
|
changed_descendant_ids: result.changedDescendantIds,
|
|
7207
7220
|
});
|
|
7208
7221
|
}
|
|
7222
|
+
function assertLaneNotRunningForReparent(lane) {
|
|
7223
|
+
if (lane.state === "running") {
|
|
7224
|
+
throw new LanesError("lane_running", `Cannot re-parent running lane ${lane.lane_id}; wait until it stops first`, { lane_id: lane.lane_id, state: lane.state });
|
|
7225
|
+
}
|
|
7226
|
+
}
|
|
7209
7227
|
function runReparentLocked(ctx, laneId, opts, finishedChildrenDoNotCount = false) {
|
|
7210
7228
|
const hqRoot = ctx.hqRoot();
|
|
7211
7229
|
if (!opts.senior) {
|
|
@@ -7215,9 +7233,6 @@ function runReparentLocked(ctx, laneId, opts, finishedChildrenDoNotCount = false
|
|
|
7215
7233
|
assertSeniorExists(hqRoot, nextSenior, ctx);
|
|
7216
7234
|
const lane = readLane(hqRoot, laneId);
|
|
7217
7235
|
assertLaneRunsHereForOperatorMutation(ctx, lane, "reparent");
|
|
7218
|
-
if (isTerminalLaneState(lane.state)) {
|
|
7219
|
-
throw new LanesError("lane_terminal", `Cannot re-parent a lane in terminal state ${lane.state}`, { lane_id: laneId, state: lane.state });
|
|
7220
|
-
}
|
|
7221
7236
|
const seniorCompany = companyOfSenior(hqRoot, nextSenior, ctx);
|
|
7222
7237
|
if (lane.company === null && seniorCompany.binding === "company") {
|
|
7223
7238
|
const company = seniorCompany.slug ?? seniorCompany.uid ?? "<company>";
|
|
@@ -7257,6 +7272,7 @@ function runReparentLocked(ctx, laneId, opts, finishedChildrenDoNotCount = false
|
|
|
7257
7272
|
chain.push({ lane_id: lane.lane_id, depth: newDepth });
|
|
7258
7273
|
throw maxLaneDepthError(newDepth, chain, `Re-parent would put lane at depth ${newDepth}`, depthCap);
|
|
7259
7274
|
}
|
|
7275
|
+
assertLaneNotRunningForReparent(lane);
|
|
7260
7276
|
const pending = pendingQuestionsForLane(hqRoot, laneId);
|
|
7261
7277
|
const oldDepth = derivedLaneDepth(hqRoot, lane.senior, ctx);
|
|
7262
7278
|
if (newDepth > oldDepth && pending.length > 0) {
|
|
@@ -7295,6 +7311,7 @@ function runReparentLocked(ctx, laneId, opts, finishedChildrenDoNotCount = false
|
|
|
7295
7311
|
const at = ctx.now().toISOString();
|
|
7296
7312
|
const updated = casUpdateLane(hqRoot, laneId, (current) => {
|
|
7297
7313
|
assertLaneRunsHereForOperatorMutation(ctx, current, "reparent");
|
|
7314
|
+
assertLaneNotRunningForReparent(current);
|
|
7298
7315
|
const already = current.senior_history.some((entry) => entry.at === at && entry.reason === (opts.reason ?? "reparent"));
|
|
7299
7316
|
if (already && current.depth === newDepth)
|
|
7300
7317
|
return current;
|
package/dist/commands/report.js
CHANGED
|
@@ -10,6 +10,8 @@ import { listDeployApps } from "../lib/deploy-apps.js";
|
|
|
10
10
|
import { listReportTemplates } from "../lib/report-templates.js";
|
|
11
11
|
import { adoptReport, buildReportTarball, conventionReportDir, deployOrgContext, ReportPublishError, plainFailureReason, publishReport, } from "../lib/report-publish.js";
|
|
12
12
|
const REPORT_CHANNEL_STATE_FILE = "report-channels.json";
|
|
13
|
+
const REPORT_UPLOAD_TIMEOUT_MS = 120_000;
|
|
14
|
+
const REPORT_CLEANUP_TIMEOUT_MS = 30_000;
|
|
13
15
|
const MAX_REPORT_DISCOVERY_CHANNELS = 50;
|
|
14
16
|
const JOB_EXECUTION_DISABLED_MESSAGE = "Scheduled jobs are turned off for this company. A company owner can turn them back on.";
|
|
15
17
|
function reportPath(channelId, reportId) {
|
|
@@ -305,7 +307,7 @@ async function deployReportStatic(token, input) {
|
|
|
305
307
|
throw new ReportPublishError("hq-deploy did not create an upload.");
|
|
306
308
|
let upload;
|
|
307
309
|
try {
|
|
308
|
-
upload = await fetch(presignedUrl, { method: "PUT", headers: { "Content-Type": "application/gzip" }, body: tarball.bytes });
|
|
310
|
+
upload = await fetch(presignedUrl, { method: "PUT", headers: { "Content-Type": "application/gzip" }, body: tarball.bytes, signal: AbortSignal.timeout(REPORT_UPLOAD_TIMEOUT_MS) });
|
|
309
311
|
}
|
|
310
312
|
catch {
|
|
311
313
|
throw new ReportPublishError("Report upload failed.");
|
|
@@ -365,6 +367,7 @@ async function discardDraftAction(reportId, options) {
|
|
|
365
367
|
const res = await fetch(`${DEPLOY_API_URL.replace(/\/+$/, "")}/api/apps/${encodeURIComponent(report.draft.appId)}`, {
|
|
366
368
|
method: "DELETE",
|
|
367
369
|
headers: deployHeaders(token, false, org.headers),
|
|
370
|
+
signal: AbortSignal.timeout(REPORT_CLEANUP_TIMEOUT_MS),
|
|
368
371
|
});
|
|
369
372
|
if (!res.ok && res.status !== 404)
|
|
370
373
|
throw new ReportPublishError("Could not remove the draft deployment.");
|
|
@@ -487,6 +490,7 @@ async function decommissionAction(reportId, options) {
|
|
|
487
490
|
const res = await fetch(`${DEPLOY_API_URL.replace(/\/+$/, "")}/api/apps/${encodeURIComponent(id)}`, {
|
|
488
491
|
method: "DELETE",
|
|
489
492
|
headers: deployHeaders(token, false, org.headers),
|
|
493
|
+
signal: AbortSignal.timeout(REPORT_CLEANUP_TIMEOUT_MS),
|
|
490
494
|
});
|
|
491
495
|
if (res.status === 404)
|
|
492
496
|
leftover.push("The published page was already gone.");
|
|
@@ -51,6 +51,12 @@ export interface RemoteAppSession {
|
|
|
51
51
|
expiration?: string;
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
+
export interface RemoteDestroyAppResponse {
|
|
55
|
+
ok: boolean;
|
|
56
|
+
appId: string;
|
|
57
|
+
mode: "cluster" | "schema";
|
|
58
|
+
destroyedAt: string;
|
|
59
|
+
}
|
|
54
60
|
export interface ControlPlaneClientOptions {
|
|
55
61
|
baseUrl: string;
|
|
56
62
|
/** Bearer access token */
|
|
@@ -73,5 +79,9 @@ export declare class ControlPlaneDbClient {
|
|
|
73
79
|
companyUid: string;
|
|
74
80
|
appId: string;
|
|
75
81
|
}): Promise<RemoteAppSession>;
|
|
82
|
+
destroyApp(input: {
|
|
83
|
+
companyUid: string;
|
|
84
|
+
appId: string;
|
|
85
|
+
}): Promise<RemoteDestroyAppResponse>;
|
|
76
86
|
}
|
|
77
87
|
//# sourceMappingURL=control-plane.d.ts.map
|
|
@@ -109,6 +109,24 @@ export class ControlPlaneDbClient {
|
|
|
109
109
|
throw httpError("session", res.status, text);
|
|
110
110
|
return JSON.parse(text);
|
|
111
111
|
}
|
|
112
|
+
async destroyApp(input) {
|
|
113
|
+
const token = await this.getAccessToken();
|
|
114
|
+
const url = `${this.baseUrl}/v1/db/apps/${encodeURIComponent(input.appId)}/destroy`;
|
|
115
|
+
const res = await this.fetchImpl(url, {
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: {
|
|
118
|
+
Authorization: `Bearer ${token}`,
|
|
119
|
+
"Content-Type": "application/json",
|
|
120
|
+
},
|
|
121
|
+
body: JSON.stringify({ companyUid: input.companyUid }),
|
|
122
|
+
signal: AbortSignal.timeout(DB_PROVISION_TIMEOUT_MS),
|
|
123
|
+
});
|
|
124
|
+
const text = await res.text();
|
|
125
|
+
assertNoPostgresUrl("destroy", text);
|
|
126
|
+
if (!res.ok)
|
|
127
|
+
throw httpError("destroy", res.status, text);
|
|
128
|
+
return JSON.parse(text);
|
|
129
|
+
}
|
|
112
130
|
}
|
|
113
131
|
function httpError(label, status, text) {
|
|
114
132
|
let msg = `${label} failed (${status})`;
|
package/dist/lib/deploy-api.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Shared authenticated request path for the hq-deploy control plane. */
|
|
2
2
|
export const DEPLOY_API_URL = process.env.HQ_DEPLOY_API_URL?.trim() || "https://api.indigo-hq.com";
|
|
3
|
+
const DEPLOY_API_REQUEST_TIMEOUT_MS = 30_000;
|
|
3
4
|
export function deployHeaders(token, json = true, extra = {}) {
|
|
4
5
|
return {
|
|
5
6
|
Authorization: `Bearer ${token}`,
|
|
@@ -16,6 +17,7 @@ export async function deployApiRequest(token, route, method, body, extraHeaders
|
|
|
16
17
|
method,
|
|
17
18
|
headers: deployHeaders(token, body !== undefined, extraHeaders),
|
|
18
19
|
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
20
|
+
signal: AbortSignal.timeout(DEPLOY_API_REQUEST_TIMEOUT_MS),
|
|
19
21
|
});
|
|
20
22
|
}
|
|
21
23
|
//# sourceMappingURL=deploy-api.js.map
|
|
@@ -127,6 +127,8 @@ export interface AppliedFixBackup {
|
|
|
127
127
|
}
|
|
128
128
|
/** A newly unlocked safe repair can be picked up for at most this many passes. */
|
|
129
129
|
export declare const MAX_FIX_PASSES = 3;
|
|
130
|
+
/** Two 90s pulls leave 120s for startup and check overhead under the hook's 300s ceiling. */
|
|
131
|
+
export declare const MAX_REFRESH_SYNC_PLANS_PER_RUN = 2;
|
|
130
132
|
/** The outcome of an {@link applyFixes} run. */
|
|
131
133
|
export interface ApplyFixesResult {
|
|
132
134
|
/** 0 on success, decline, or a clean no-op; 1 when a dirty tree is refused. */
|
|
@@ -47,6 +47,43 @@ export const HOOK_CONFIG_DIRS = [".claude", ".codex", ".grok"];
|
|
|
47
47
|
export const REFRESH_SYNC_TIMEOUT_MS = 90_000;
|
|
48
48
|
/** A newly unlocked safe repair can be picked up for at most this many passes. */
|
|
49
49
|
export const MAX_FIX_PASSES = 3;
|
|
50
|
+
/** Two 90s pulls leave 120s for startup and check overhead under the hook's 300s ceiling. */
|
|
51
|
+
export const MAX_REFRESH_SYNC_PLANS_PER_RUN = 2;
|
|
52
|
+
const REFRESH_SYNC_PROGRESS_RELATIVE_PATH = path.join("workspace", "doctor", "refresh-sync-progress.json");
|
|
53
|
+
function readRefreshSyncProgress(hqRoot) {
|
|
54
|
+
let contents;
|
|
55
|
+
try {
|
|
56
|
+
contents = fs.readFileSync(path.join(hqRoot, REFRESH_SYNC_PROGRESS_RELATIVE_PATH), "utf8");
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (error.code === "ENOENT")
|
|
60
|
+
return null;
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
const parsed = JSON.parse(contents);
|
|
64
|
+
if (typeof parsed === "object" && parsed !== null &&
|
|
65
|
+
parsed.version === 1 &&
|
|
66
|
+
typeof parsed.lastAttemptedSlug === "string") {
|
|
67
|
+
return parsed;
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`Invalid refresh-sync progress cursor at ${REFRESH_SYNC_PROGRESS_RELATIVE_PATH}`);
|
|
70
|
+
}
|
|
71
|
+
function writeRefreshSyncProgress(hqRoot, slug) {
|
|
72
|
+
const progressPath = path.join(hqRoot, REFRESH_SYNC_PROGRESS_RELATIVE_PATH);
|
|
73
|
+
const directory = path.dirname(progressPath);
|
|
74
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
75
|
+
const temporaryPath = `${progressPath}.${process.pid}.tmp`;
|
|
76
|
+
fs.writeFileSync(temporaryPath, `${JSON.stringify({ version: 1, lastAttemptedSlug: slug })}\n`, { mode: 0o600 });
|
|
77
|
+
fs.renameSync(temporaryPath, progressPath);
|
|
78
|
+
}
|
|
79
|
+
function rotateRefreshSyncPlans(plans, afterSlug) {
|
|
80
|
+
const ordered = [...plans].sort((a, b) => a.target < b.target ? -1 : a.target > b.target ? 1 : 0);
|
|
81
|
+
if (!afterSlug || ordered.length < 2)
|
|
82
|
+
return ordered;
|
|
83
|
+
const next = ordered.findIndex((plan) => plan.target > afterSlug);
|
|
84
|
+
const start = next === -1 ? 0 : next;
|
|
85
|
+
return [...ordered.slice(start), ...ordered.slice(0, start)];
|
|
86
|
+
}
|
|
50
87
|
/**
|
|
51
88
|
* Apply every auto-fixable finding, honouring the dirty-tree refusal, the
|
|
52
89
|
* preview/confirmation gate, the pre-write backup, and the post-fix re-check.
|
|
@@ -87,6 +124,8 @@ export async function applyFixes(options) {
|
|
|
87
124
|
// Identity is deliberately the check id + target pair: an unsuccessful
|
|
88
125
|
// repair must not be applied repeatedly in one invocation.
|
|
89
126
|
let findings = await collect(hqRoot);
|
|
127
|
+
let lastAttemptedRefreshSyncSlug = readRefreshSyncProgress(hqRoot)?.lastAttemptedSlug ?? null;
|
|
128
|
+
let refreshSyncAppliedThisRun = 0;
|
|
90
129
|
const appliedKeys = new Set();
|
|
91
130
|
const planned = [];
|
|
92
131
|
const backups = [];
|
|
@@ -114,7 +153,11 @@ export async function applyFixes(options) {
|
|
|
114
153
|
keysThisPass.add(key);
|
|
115
154
|
plans.push(plan);
|
|
116
155
|
}
|
|
117
|
-
|
|
156
|
+
const refreshPlans = rotateRefreshSyncPlans(plans.filter((plan) => plan.fixClass === "refresh-sync"), lastAttemptedRefreshSyncSlug).slice(0, Math.max(0, MAX_REFRESH_SYNC_PLANS_PER_RUN - refreshSyncAppliedThisRun));
|
|
157
|
+
return [
|
|
158
|
+
...plans.filter((plan) => plan.fixClass !== "refresh-sync"),
|
|
159
|
+
...refreshPlans,
|
|
160
|
+
];
|
|
118
161
|
};
|
|
119
162
|
for (let pass = 1; pass <= MAX_FIX_PASSES; pass += 1) {
|
|
120
163
|
const plans = buildPlans(findings);
|
|
@@ -154,6 +197,14 @@ export async function applyFixes(options) {
|
|
|
154
197
|
// 5. Apply each plan once. Later findings with this check id + target are
|
|
155
198
|
// excluded even if this repair did not make its check pass.
|
|
156
199
|
for (const plan of plans) {
|
|
200
|
+
if (plan.fixClass === "refresh-sync") {
|
|
201
|
+
// Persist before starting the bounded pull. If this process is killed,
|
|
202
|
+
// the next invocation resumes after this company instead of starving
|
|
203
|
+
// every later journal behind it.
|
|
204
|
+
writeRefreshSyncProgress(hqRoot, plan.target);
|
|
205
|
+
lastAttemptedRefreshSyncSlug = plan.target;
|
|
206
|
+
refreshSyncAppliedThisRun += 1;
|
|
207
|
+
}
|
|
157
208
|
appliedKeys.add(JSON.stringify([plan.checkId, plan.target]));
|
|
158
209
|
planned.push({ plan, pass });
|
|
159
210
|
await plan.apply();
|
|
@@ -63,7 +63,7 @@ export declare function resolveFlagGate(reader: FlagReader | null | undefined, f
|
|
|
63
63
|
/** Resolve one explicit company's flag without relying on process-wide binding. */
|
|
64
64
|
export declare function resolveCompanyFlagGate(companyUid: string, idToken: string, flagKey: string, lookup: FlagLookupOptions, legacyFallback: () => boolean, dependencies?: CompanyFlagGateDependencies): Promise<boolean>;
|
|
65
65
|
/** Resolve a process gate against the shared snapshot reader, when configured. */
|
|
66
|
-
export declare function resolveProcessFlagGate(flagKey: string, lookup: FlagLookupOptions, legacyFallback: () => boolean): boolean;
|
|
66
|
+
export declare function resolveProcessFlagGate(flagKey: string, lookup: FlagLookupOptions, legacyFallback: () => boolean, companyUid?: string): boolean;
|
|
67
67
|
/**
|
|
68
68
|
* The registry snapshot held by this process, or null when none has arrived
|
|
69
69
|
* (no endpoint configured, offline, or still fetching). Callers that must
|
|
@@ -11,7 +11,7 @@ import { resolveFlagRegistryEndpoint, } from "./flag-registry-endpoint.js";
|
|
|
11
11
|
import { FLAG_REGISTRY_WORKER_SOURCE } from "./flag-registry-worker-source.generated.js";
|
|
12
12
|
import { resolveProcessFlagCompanyContext } from "./process-flag-company-context.js";
|
|
13
13
|
import { registerFlagRegistryRefreshWaiter } from "./flag-registry-refresh-wait.js";
|
|
14
|
-
import { flagSnapshotWriteFailureTags, writeFlagSnapshotFile } from "./flag-snapshot-file.js";
|
|
14
|
+
import { flagSnapshotWriteFailureTags, readFlagSnapshotFile, writeFlagSnapshotFile, } from "./flag-snapshot-file.js";
|
|
15
15
|
function captureFlagRegistryException(error, tags, sentry) {
|
|
16
16
|
if (!sentry)
|
|
17
17
|
return;
|
|
@@ -298,13 +298,26 @@ export async function resolveCompanyFlagGate(companyUid, idToken, flagKey, looku
|
|
|
298
298
|
}
|
|
299
299
|
}
|
|
300
300
|
/** Resolve a process gate against the shared snapshot reader, when configured. */
|
|
301
|
-
export function resolveProcessFlagGate(flagKey, lookup, legacyFallback) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
301
|
+
export function resolveProcessFlagGate(flagKey, lookup, legacyFallback, companyUid) {
|
|
302
|
+
const targetCompanyUid = companyUid ?? (processReader
|
|
303
|
+
? refreshCompanyUid
|
|
304
|
+
: resolveProcessFlagCompanyContext().companyUid);
|
|
305
|
+
const processSnapshotMatchesTarget = refreshCompanyUid === targetCompanyUid;
|
|
306
|
+
if (processReader && heldSnapshot && processSnapshotMatchesTarget) {
|
|
307
|
+
return resolveFlagGate(processReader, flagKey, lookup, legacyFallback);
|
|
308
|
+
}
|
|
309
|
+
// A command can reach a synchronous gate before the startup worker returns.
|
|
310
|
+
// Use the last validated, fresh company snapshot in that window, and when
|
|
311
|
+
// the process reader was initialized for a different company scope.
|
|
312
|
+
if (targetCompanyUid) {
|
|
313
|
+
const cached = readFlagSnapshotFile(targetCompanyUid);
|
|
314
|
+
if (cached) {
|
|
315
|
+
const snapshot = { version: 0, flags: cached.flags };
|
|
316
|
+
const reader = createProcessSnapshotReader([targetCompanyUid], () => snapshot);
|
|
317
|
+
return resolveFlagGate(reader, flagKey, lookup, legacyFallback);
|
|
318
|
+
}
|
|
306
319
|
}
|
|
307
|
-
return
|
|
320
|
+
return localOverrideOrFallback(flagKey, legacyFallback);
|
|
308
321
|
}
|
|
309
322
|
/**
|
|
310
323
|
* The registry snapshot held by this process, or null when none has arrived
|
|
@@ -233,9 +233,31 @@ export function selectBalancedAccount(provider, config, ctx = {}) {
|
|
|
233
233
|
})
|
|
234
234
|
: proven;
|
|
235
235
|
if (quota && runnable.length === 0) {
|
|
236
|
+
const blocked = proven.map((id) => ({
|
|
237
|
+
id,
|
|
238
|
+
block: quotaBlockFor(quota, provider, id, modelForAccount(provider, id, config, ctx), now),
|
|
239
|
+
}));
|
|
240
|
+
const resetTimes = blocked
|
|
241
|
+
.map(({ block }) => block.quota_reset_at)
|
|
242
|
+
.filter((value) => typeof value === "string" && Number.isFinite(Date.parse(value)))
|
|
243
|
+
.sort((a, b) => Date.parse(a) - Date.parse(b));
|
|
244
|
+
const earliest = blocked.find(({ block }) => block.reason === "account_quota_exhausted");
|
|
245
|
+
if (earliest !== undefined) {
|
|
246
|
+
const resetsByAccount = blocked.map(({ id, block }) => ({
|
|
247
|
+
account: id,
|
|
248
|
+
reason: block.reason,
|
|
249
|
+
quota_reset_at: block.quota_reset_at ?? null,
|
|
250
|
+
}));
|
|
251
|
+
throw new AccountResolutionError("account_quota_exhausted", `All logged-in accounts for ${provider} are quota-limited for the selected model`, {
|
|
252
|
+
provider,
|
|
253
|
+
account: earliest.id,
|
|
254
|
+
model: earliest.block.model ?? null,
|
|
255
|
+
quota_reset_at: resetTimes[0] ?? null,
|
|
256
|
+
account_reasons: resetsByAccount,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
236
259
|
const sample = proven[0];
|
|
237
|
-
|
|
238
|
-
throwIfQuotaBlocked(block, provider, sample);
|
|
260
|
+
throwIfQuotaBlocked(blocked[0].block, provider, sample);
|
|
239
261
|
}
|
|
240
262
|
const underCap = runnable.filter((id) => {
|
|
241
263
|
const cap = accountMaxRunning(providerCfg, id);
|
|
@@ -68,14 +68,17 @@ export declare const LANE_DELIVERY_MECHANISMS: readonly ["dropbox", "resume", "n
|
|
|
68
68
|
export type LaneDeliveryMechanism = (typeof LANE_DELIVERY_MECHANISMS)[number];
|
|
69
69
|
/** Additional presentation-only result used by `hq lanes show`. */
|
|
70
70
|
export type LaneShowDeliveryMechanism = LaneDeliveryMechanism | "questions";
|
|
71
|
-
export type DeliveryLane = Pick<LaneRecord, "state" | "provider" | "provider_session_id" | "terminal_reason" | "envelope_repair">;
|
|
71
|
+
export type DeliveryLane = Pick<LaneRecord, "state" | "provider" | "provider_session_id" | "terminal_reason" | "envelope_repair" | "keep_alive" | "quota_reset_at" | "terminal_details">;
|
|
72
|
+
export declare function isFailedQuotaKeepAliveLane(lane: DeliveryLane): boolean;
|
|
72
73
|
/**
|
|
73
74
|
* `dropbox`: a process is (or is about to be) alive to drain the inbox.
|
|
74
75
|
* `resume`: the process has exited, the provider can resume, the state allows
|
|
75
76
|
* it, and a session id was recorded from the worker's own result.
|
|
76
77
|
* `none`: nothing can deliver; the caller must refuse and write nothing.
|
|
77
78
|
*/
|
|
78
|
-
export declare function laneDeliveryMechanism(lane: DeliveryLane
|
|
79
|
+
export declare function laneDeliveryMechanism(lane: DeliveryLane, options?: {
|
|
80
|
+
forMessage?: boolean;
|
|
81
|
+
}): LaneDeliveryMechanism;
|
|
79
82
|
export declare function hasRecordedSessionId(lane: Pick<LaneRecord, "provider_session_id">): lane is Pick<LaneRecord, "provider_session_id"> & {
|
|
80
83
|
provider_session_id: string;
|
|
81
84
|
};
|
|
@@ -94,17 +94,25 @@ function isFailedEnvelopeRepairLane(lane) {
|
|
|
94
94
|
lane.envelope_repair?.attempted === true &&
|
|
95
95
|
lane.envelope_repair.succeeded === false);
|
|
96
96
|
}
|
|
97
|
+
export function isFailedQuotaKeepAliveLane(lane) {
|
|
98
|
+
const detailsReset = lane.terminal_details?.quota_reset_at;
|
|
99
|
+
const resetAt = lane.quota_reset_at ?? (typeof detailsReset === "string" ? detailsReset : undefined);
|
|
100
|
+
return lane.state === "failed" && lane.keep_alive === true &&
|
|
101
|
+
(lane.terminal_reason === "account_quota_exhausted" || lane.terminal_reason === "account_model_unavailable") &&
|
|
102
|
+
typeof resetAt === "string" && Number.isFinite(Date.parse(resetAt));
|
|
103
|
+
}
|
|
97
104
|
/**
|
|
98
105
|
* `dropbox`: a process is (or is about to be) alive to drain the inbox.
|
|
99
106
|
* `resume`: the process has exited, the provider can resume, the state allows
|
|
100
107
|
* it, and a session id was recorded from the worker's own result.
|
|
101
108
|
* `none`: nothing can deliver; the caller must refuse and write nothing.
|
|
102
109
|
*/
|
|
103
|
-
export function laneDeliveryMechanism(lane) {
|
|
110
|
+
export function laneDeliveryMechanism(lane, options = {}) {
|
|
104
111
|
if (lane.state === "queued" || lane.state === "running")
|
|
105
112
|
return "dropbox";
|
|
106
113
|
if (providerSupportsSessionResume(lane.provider) &&
|
|
107
|
-
(isResumableLaneState(lane.state) || lane.state === "awaiting_input" || isFailedEnvelopeRepairLane(lane)
|
|
114
|
+
(isResumableLaneState(lane.state) || lane.state === "awaiting_input" || isFailedEnvelopeRepairLane(lane) ||
|
|
115
|
+
(!options.forMessage && isFailedQuotaKeepAliveLane(lane))) &&
|
|
108
116
|
hasRecordedSessionId(lane)) {
|
|
109
117
|
return "resume";
|
|
110
118
|
}
|
|
@@ -350,6 +358,7 @@ export function resumeCommandRefusal(lane, options = {}) {
|
|
|
350
358
|
if (!isResumeCommandState(lane.state) &&
|
|
351
359
|
!(options.allowAwaitingInput === true && lane.state === "awaiting_input") &&
|
|
352
360
|
!isFailedEnvelopeRepairLane(lane) &&
|
|
361
|
+
!isFailedQuotaKeepAliveLane(lane) &&
|
|
353
362
|
!(options.allowFailed === true && lane.state === "failed")) {
|
|
354
363
|
return {
|
|
355
364
|
error: "lane_not_resumable",
|
|
@@ -393,14 +402,14 @@ export function planLaneLaunch(lane, trigger) {
|
|
|
393
402
|
}
|
|
394
403
|
return { mode: "fresh" };
|
|
395
404
|
case "message": {
|
|
396
|
-
if (laneDeliveryMechanism(lane) !== "resume" || !hasRecordedSessionId(lane)) {
|
|
405
|
+
if (laneDeliveryMechanism(lane, { forMessage: true }) !== "resume" || !hasRecordedSessionId(lane)) {
|
|
397
406
|
throw new LanesError("lane_not_accepting_messages", deliveryRefusalReason(lane) || `Lane is ${lane.state} and cannot be resumed`, { state: lane.state, provider: lane.provider });
|
|
398
407
|
}
|
|
399
408
|
return { mode: "resume", session_id: lane.provider_session_id };
|
|
400
409
|
}
|
|
401
410
|
case "resume": {
|
|
402
411
|
const refusal = resumeCommandRefusal(lane, {
|
|
403
|
-
allowFailed: trigger.fromFailed === true,
|
|
412
|
+
allowFailed: trigger.fromFailed === true || isFailedQuotaKeepAliveLane(lane),
|
|
404
413
|
allowAwaitingInput: trigger.allowAwaitingInput === true,
|
|
405
414
|
});
|
|
406
415
|
if (refusal !== null || !hasRecordedSessionId(lane)) {
|
|
@@ -429,7 +438,7 @@ export function claimableStatesFor(trigger, lane) {
|
|
|
429
438
|
? [...RESUMABLE_LANE_STATES, "awaiting_input"]
|
|
430
439
|
: RESUMABLE_LANE_STATES;
|
|
431
440
|
case "resume": {
|
|
432
|
-
const states = trigger.fromFailed === true || envelopeRepairFailed
|
|
441
|
+
const states = trigger.fromFailed === true || envelopeRepairFailed || (lane !== undefined && isFailedQuotaKeepAliveLane(lane))
|
|
433
442
|
? [...RESUME_COMMAND_STATES, "failed"]
|
|
434
443
|
: [...RESUME_COMMAND_STATES];
|
|
435
444
|
return lane?.state === "awaiting_input" ? [...states, "awaiting_input"] : states;
|
|
@@ -63,7 +63,7 @@ export type { RegistryPersona } from "./registry-persona.js";
|
|
|
63
63
|
export type { RegistryWorkerEntry, ResolvedLaneWorker, WorkerTenancy, WorkerYaml, } from "./registry.js";
|
|
64
64
|
export { bodyStoreForLane, envelopeQuestionId, parseQuestionBodyStore, questionBodyAbsPath, questionBodyRef, raiseEnvelopeQuestions, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "./questions.js";
|
|
65
65
|
export type { QuestionBody, RaiseLaneQuestionInput } from "./questions.js";
|
|
66
|
-
export { LANE_DELIVERY_MECHANISMS, LAUNCH_TRIGGERS, PROVIDER_SESSION_RESUME, PROVIDER_SESSION_PORTABLE, providerSessionPortable, sessionNotPortableMessage, parseGrokResultOutput, RESUMABLE_LANE_STATES, RESUME_COMMAND_STATES, SESSION_ID_ABSENT_REASONS, claimableStatesFor, deliveryRefusalReason, hasRecordedSessionId, isResumableLaneState, isResumeCommandState, laneDeliveryMechanism, parseClaudeResultOutput, parseCodexEventOutput, parseWorkerRunOutput, planLaneLaunch, providerSupportsSessionResume, readWorkerRunResult, resumeCommandRefusal, } from "./delivery.js";
|
|
66
|
+
export { LANE_DELIVERY_MECHANISMS, LAUNCH_TRIGGERS, PROVIDER_SESSION_RESUME, PROVIDER_SESSION_PORTABLE, providerSessionPortable, sessionNotPortableMessage, parseGrokResultOutput, RESUMABLE_LANE_STATES, RESUME_COMMAND_STATES, SESSION_ID_ABSENT_REASONS, claimableStatesFor, deliveryRefusalReason, hasRecordedSessionId, isFailedQuotaKeepAliveLane, isResumableLaneState, isResumeCommandState, laneDeliveryMechanism, parseClaudeResultOutput, parseCodexEventOutput, parseWorkerRunOutput, planLaneLaunch, providerSupportsSessionResume, readWorkerRunResult, resumeCommandRefusal, } from "./delivery.js";
|
|
67
67
|
export type { DeliveryLane, LaneDeliveryMechanism, LaneShowDeliveryMechanism, LaunchPlan, LaunchTrigger, LaunchTriggerKind, ResumableLaneState, ResumeCommandState, SessionIdAbsentReason, WorkerRunResult, } from "./delivery.js";
|
|
68
68
|
export { DROPBOX_DIR, INBOX_NAME, appendDropboxMessage, answerDropboxMessage, drainDropbox, dropboxDir, dropboxInboxPath, formatOperatorInstruction, hookAdditionalContextJson, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneIdForAnswerDelivery, operatorMessageRefusalReason, readToolActivity, recordToolActivity, runDrainHook, toolActivityPath, TOOL_ACTIVITY_NAME, } from "./dropbox.js";
|
|
69
69
|
export type { DrainDeps, DrainHookResult, DropboxMessage, ToolActivityRecord, } from "./dropbox.js";
|
package/dist/lib/lanes/index.js
CHANGED
|
@@ -34,7 +34,7 @@ export { WORKER_REGISTRY_REL, personalWorkerDir, personalWorkerExists, readWorke
|
|
|
34
34
|
export { ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, materializeWorkerProfile, personalWorkerProfileDir, REGISTRY_PERSONA_PROMPT_MAX_CHARS, workerProfileMarker, workerProfilePrompt, } from "./profile.js";
|
|
35
35
|
export { loadRegistryPersonaFromRecord, registryWorkerRecord, resolveRegistryPersona, } from "./registry-persona.js";
|
|
36
36
|
export { bodyStoreForLane, envelopeQuestionId, parseQuestionBodyStore, questionBodyAbsPath, questionBodyRef, raiseEnvelopeQuestions, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "./questions.js";
|
|
37
|
-
export { LANE_DELIVERY_MECHANISMS, LAUNCH_TRIGGERS, PROVIDER_SESSION_RESUME, PROVIDER_SESSION_PORTABLE, providerSessionPortable, sessionNotPortableMessage, parseGrokResultOutput, RESUMABLE_LANE_STATES, RESUME_COMMAND_STATES, SESSION_ID_ABSENT_REASONS, claimableStatesFor, deliveryRefusalReason, hasRecordedSessionId, isResumableLaneState, isResumeCommandState, laneDeliveryMechanism, parseClaudeResultOutput, parseCodexEventOutput, parseWorkerRunOutput, planLaneLaunch, providerSupportsSessionResume, readWorkerRunResult, resumeCommandRefusal, } from "./delivery.js";
|
|
37
|
+
export { LANE_DELIVERY_MECHANISMS, LAUNCH_TRIGGERS, PROVIDER_SESSION_RESUME, PROVIDER_SESSION_PORTABLE, providerSessionPortable, sessionNotPortableMessage, parseGrokResultOutput, RESUMABLE_LANE_STATES, RESUME_COMMAND_STATES, SESSION_ID_ABSENT_REASONS, claimableStatesFor, deliveryRefusalReason, hasRecordedSessionId, isFailedQuotaKeepAliveLane, isResumableLaneState, isResumeCommandState, laneDeliveryMechanism, parseClaudeResultOutput, parseCodexEventOutput, parseWorkerRunOutput, planLaneLaunch, providerSupportsSessionResume, readWorkerRunResult, resumeCommandRefusal, } from "./delivery.js";
|
|
38
38
|
export { DROPBOX_DIR, INBOX_NAME, appendDropboxMessage, answerDropboxMessage, drainDropbox, dropboxDir, dropboxInboxPath, formatOperatorInstruction, hookAdditionalContextJson, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneIdForAnswerDelivery, operatorMessageRefusalReason, readToolActivity, recordToolActivity, runDrainHook, toolActivityPath, TOOL_ACTIVITY_NAME, } from "./dropbox.js";
|
|
39
39
|
export { DRAIN_HOOK_BASENAME, DRAIN_HOOK_REGISTRATION_KINDS, DRAIN_HOOK_TIMEOUT_SECONDS, PROVIDER_REGISTRATION_KIND, drainHookCommand, drainHookRegistrationPath, drainHookShPath, installDrainHook, lanesHookAssetsDir, mergeClaudeSettingsHook, mergeCodexConfigHook, probeDrainHookRegistration, renderDrainHookJson, } from "./hooks-install.js";
|
|
40
40
|
export { EFFORT_SOURCES, PROVIDER_EFFORT_LEVELS, appendEffortFlag, defaultEffortForModel, parseCliEffort, parseConfigEffort, resolveLaneEffort, } from "./effort.js";
|
|
@@ -29,7 +29,7 @@ import { laneCompanyUidSource, resolveLaneCompany } from "./lane-company.js";
|
|
|
29
29
|
import { appendDropboxMessage } from "./dropbox.js";
|
|
30
30
|
import { DEFAULT_RUNAWAY_SEARCH_GUARD_CONFIG, listHostProcesses, runRunawayToolGuard, RUNAWAY_TOOL_GUARD_FLAG, } from "./runaway-tool-guard.js";
|
|
31
31
|
import { LEAD_LANE_KEEP_ALIVE_FLAG, LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_FLAG, LEAD_LANE_KEEP_ALIVE_IMMEDIATE_FIRST_RESUME_FLAG, LEAD_LANE_KEEP_ALIVE_FIRST_RESUME_SETTLE_MS, LEAD_LANE_KEEP_ALIVE_BLOCKED_GRACE_MS, LEAD_LANE_KEEP_ALIVE_BLOCKED_RESUME_TEXT, queueLaneTerminalResume, 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, dueKeepAliveNextCheckMs, hasPendingKeepAliveWork, keepAliveNextCheckAtMs, keepAliveExclusion, loadRetiredLaneIds, leadLaneKeepAliveBackoffMs, undeliveredAnsweredQuestions, } from "./keep-alive.js";
|
|
32
|
-
import { providerSupportsSessionResume } from "./delivery.js";
|
|
32
|
+
import { isFailedQuotaKeepAliveLane, providerSupportsSessionResume } from "./delivery.js";
|
|
33
33
|
import { isIsoDateTime } from "./types.js";
|
|
34
34
|
import { applyDefaultQuotaHold, clearAccountQuotaAfterSuccessfulTurnBestEffort, loadQuotaState, readQuotaSignal, recordAccountQuota, } from "./quota.js";
|
|
35
35
|
import { answeredQuestionEntries, questionRecordRefusalCode, raiseEnvelopeQuestions, resolveQuestionCompanyWithFallback, } from "./questions.js";
|
|
@@ -2586,7 +2586,7 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
|
|
|
2586
2586
|
dueAdmissionRan = true;
|
|
2587
2587
|
await admitDueCandidates(new Set(queuedDueCandidateIds));
|
|
2588
2588
|
};
|
|
2589
|
-
for (
|
|
2589
|
+
for (let originalLane of candidates) {
|
|
2590
2590
|
if (admitDueCandidates !== undefined && dueCandidateIds.size > 0 && !dueCandidateIds.has(originalLane.lane_id)) {
|
|
2591
2591
|
await admitQueuedDueCandidates();
|
|
2592
2592
|
}
|
|
@@ -2596,14 +2596,50 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
|
|
|
2596
2596
|
continue;
|
|
2597
2597
|
if (!laneRunsHere(originalLane, deps.host))
|
|
2598
2598
|
continue;
|
|
2599
|
+
const failedQuotaHold = isFailedQuotaKeepAliveLane(originalLane);
|
|
2599
2600
|
if (originalLane.state === "killed" ||
|
|
2600
2601
|
originalLane.state === "cancelled" ||
|
|
2601
|
-
originalLane.state === "failed") {
|
|
2602
|
+
(originalLane.state === "failed" && !failedQuotaHold)) {
|
|
2602
2603
|
if (originalLane.keep_alive === true &&
|
|
2603
2604
|
flagByCompany.get(keepAliveCompanyKey(companyForLane(originalLane))) === true)
|
|
2604
2605
|
recordKeepAliveSkip(hqRoot, originalLane, "terminal_state", summary, deps);
|
|
2605
2606
|
continue;
|
|
2606
2607
|
}
|
|
2608
|
+
if (failedQuotaHold) {
|
|
2609
|
+
const detailsReset = originalLane.terminal_details?.quota_reset_at;
|
|
2610
|
+
const resetAt = originalLane.quota_reset_at ??
|
|
2611
|
+
(typeof detailsReset === "string" ? detailsReset : undefined);
|
|
2612
|
+
const companyKey = keepAliveCompanyKey(companyForLane(originalLane));
|
|
2613
|
+
if (resetAt === undefined ||
|
|
2614
|
+
Date.parse(resetAt) > nowMs ||
|
|
2615
|
+
flagByCompany.get(companyKey) !== true)
|
|
2616
|
+
continue;
|
|
2617
|
+
const resetAtMs = Date.parse(resetAt);
|
|
2618
|
+
const priorInterruptedAtMs = Date.parse(originalLane.interrupted_at ?? "");
|
|
2619
|
+
const optedInAtMs = Date.parse(originalLane.keep_alive_opted_in_at ?? "");
|
|
2620
|
+
const recoveredInterruptedAt = new Date(Math.max(Number.isFinite(priorInterruptedAtMs) ? priorInterruptedAtMs : resetAtMs, Number.isFinite(optedInAtMs) ? optedInAtMs + 1 : resetAtMs)).toISOString();
|
|
2621
|
+
const recovered = casLaneIf(hqRoot, originalLane.lane_id, (current) => current.version === originalLane.version &&
|
|
2622
|
+
current.state === "failed" &&
|
|
2623
|
+
current.keep_alive === true &&
|
|
2624
|
+
current.terminal_reason === originalLane.terminal_reason &&
|
|
2625
|
+
(current.quota_reset_at ?? current.terminal_details?.quota_reset_at) === resetAt, (current) => ({
|
|
2626
|
+
...current,
|
|
2627
|
+
state: "interrupted",
|
|
2628
|
+
interrupted_at: recoveredInterruptedAt,
|
|
2629
|
+
quota_reset_at: resetAt,
|
|
2630
|
+
keep_alive_next_eligible_at: resetAt,
|
|
2631
|
+
keep_alive_resume_count: 0,
|
|
2632
|
+
keep_alive_last_resume_at: undefined,
|
|
2633
|
+
keep_alive_exhausted_at: undefined,
|
|
2634
|
+
queued_reason: undefined,
|
|
2635
|
+
queued_details: undefined,
|
|
2636
|
+
queued_at: undefined,
|
|
2637
|
+
admission_reserved_at: undefined,
|
|
2638
|
+
}), deps.dryRun);
|
|
2639
|
+
if (!recovered.changed)
|
|
2640
|
+
continue;
|
|
2641
|
+
originalLane = recovered.record;
|
|
2642
|
+
}
|
|
2607
2643
|
if (originalLane.state === "blocked" && originalLane.keep_alive === true) {
|
|
2608
2644
|
await resumeBlockedKeepAlive(hqRoot, originalLane, questions, summary, deps, now, flagByCompany, blockedResumeFlagByCompany, companyForLane, lanes, retiredLaneIds, supersessionLineageAwareByCompany);
|
|
2609
2645
|
continue;
|
|
@@ -2714,7 +2750,11 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
|
|
|
2714
2750
|
continue;
|
|
2715
2751
|
}
|
|
2716
2752
|
}
|
|
2717
|
-
|
|
2753
|
+
const quotaHold = lane.keep_alive === true &&
|
|
2754
|
+
(lane.terminal_reason === "account_quota_exhausted" || lane.terminal_reason === "account_model_unavailable") &&
|
|
2755
|
+
typeof lane.quota_reset_at === "string" &&
|
|
2756
|
+
Number.isFinite(Date.parse(lane.quota_reset_at));
|
|
2757
|
+
if (initialState === "interrupted" && lane.terminal_reason !== "exited_with_partial_output" && !quotaHold) {
|
|
2718
2758
|
recordKeepAliveSkip(hqRoot, lane, "terminal_state", summary, deps, lane.terminal_reason ?? "missing interruption reason");
|
|
2719
2759
|
continue;
|
|
2720
2760
|
}
|
|
@@ -2816,7 +2856,11 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
|
|
|
2816
2856
|
recordKeepAliveSkip(hqRoot, current, "not_opted_in", summary, deps);
|
|
2817
2857
|
continue;
|
|
2818
2858
|
}
|
|
2819
|
-
|
|
2859
|
+
const currentQuotaHold = current.keep_alive === true &&
|
|
2860
|
+
(current.terminal_reason === "account_quota_exhausted" || current.terminal_reason === "account_model_unavailable") &&
|
|
2861
|
+
typeof current.quota_reset_at === "string" &&
|
|
2862
|
+
Number.isFinite(Date.parse(current.quota_reset_at));
|
|
2863
|
+
if (initialState === "interrupted" && current.terminal_reason !== "exited_with_partial_output" && !currentQuotaHold) {
|
|
2820
2864
|
recordKeepAliveSkip(hqRoot, current, "terminal_state", summary, deps, current.terminal_reason ?? "missing interruption reason");
|
|
2821
2865
|
continue;
|
|
2822
2866
|
}
|
|
@@ -15,11 +15,11 @@ import { type LaunchTrigger } from "./delivery.js";
|
|
|
15
15
|
import { type MachineIdentity } from "./machine-identity.js";
|
|
16
16
|
import { type AdmissionHost } from "./admission.js";
|
|
17
17
|
import { type StatFs } from "./disk-guard.js";
|
|
18
|
-
import { type LaneCompanyRef, type LaneProvider, type LaneRecord, type AccountHistoryEntry } from "./types.js";
|
|
18
|
+
import { type LaneCompanyRef, type LaneProvider, type LaneRecord, type AccountHistoryEntry, type PendingResume } from "./types.js";
|
|
19
19
|
export declare const LANES_RUNS_REL: string;
|
|
20
20
|
/** Written by reconcile before a resume spawn; the worker prompt names this path. */
|
|
21
21
|
export declare const ANSWERED_QUESTIONS_FILE = "answered-questions.json";
|
|
22
|
-
export declare const SPAWN_TERMINAL_REASONS: readonly ["account_not_logged_in", "unknown_account", "account_disabled", "no_enabled_account", "spawn_failed", "not_detached", "watcher_not_armed", "model_unsupported", "account_quota_exhausted"];
|
|
22
|
+
export declare const SPAWN_TERMINAL_REASONS: readonly ["account_not_logged_in", "unknown_account", "account_disabled", "no_enabled_account", "spawn_failed", "not_detached", "watcher_not_armed", "model_unsupported", "account_quota_exhausted", "account_model_unavailable"];
|
|
23
23
|
export type SpawnTerminalReason = (typeof SPAWN_TERMINAL_REASONS)[number];
|
|
24
24
|
declare const PROVIDER_COMMAND: Record<LaneProvider, string>;
|
|
25
25
|
export declare function providerCommandOnPath(command: string, pathEnv: string | undefined): boolean;
|
|
@@ -244,7 +244,11 @@ export declare function workerEnv(base: NodeJS.ProcessEnv, resolved: {
|
|
|
244
244
|
export declare const WORKER_POLICY_HOOK_CAPABILITY = "# hq-capability: worker-policy-dir v1";
|
|
245
245
|
/** Grok does not run HQ hooks; Claude and Codex use the installed capability marker. */
|
|
246
246
|
export declare function workerPolicyDeliveryMode(hqRoot: string, provider: LaneProvider): "hook" | "prompt";
|
|
247
|
-
export declare function markSpawnFailure(hqRoot: string, laneId: string, reasonRaw: string, details: Record<string, unknown
|
|
247
|
+
export declare function markSpawnFailure(hqRoot: string, laneId: string, reasonRaw: string, details: Record<string, unknown>, options?: {
|
|
248
|
+
resumeTrigger?: "resume" | "message";
|
|
249
|
+
pendingResume?: PendingResume;
|
|
250
|
+
at?: string;
|
|
251
|
+
}): LaneRecord;
|
|
248
252
|
/**
|
|
249
253
|
* Spec §5.2 executing-side check. Compares this machine's identity to
|
|
250
254
|
* `origin_host` (never to `host.id`). Unresolvable identity is a cancel.
|