@indigoai-us/hq-cli 5.345.41 → 5.345.43
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 +12 -0
- package/dist/command-catalog.generated.d.ts +7 -3
- package/dist/command-catalog.generated.js +8 -3
- package/dist/commands/db-remote-deps.js +3 -0
- package/dist/commands/db-usage.js +11 -4
- package/dist/commands/install-global.d.ts +8 -1
- package/dist/commands/install-global.js +128 -25
- package/dist/commands/lanes-message.js +1 -0
- package/dist/commands/lanes-resume.js +3 -0
- package/dist/commands/lanes.js +203 -44
- package/dist/lib/anywhere-person-setting.d.ts +2 -0
- package/dist/lib/anywhere-person-setting.js +2 -1
- package/dist/lib/anywhere-runtime-fast-flag.js +8 -2
- package/dist/lib/daemon/anywhere-listener.d.ts +16 -0
- package/dist/lib/daemon/anywhere-listener.js +79 -0
- package/dist/lib/daemon/run.js +30 -23
- package/dist/lib/db/control-plane.d.ts +46 -0
- package/dist/lib/db/control-plane.js +14 -0
- package/dist/lib/db/usage-format.d.ts +4 -1
- package/dist/lib/db/usage-format.js +66 -0
- package/dist/lib/doctor/checks/anywhere.d.ts +10 -0
- package/dist/lib/doctor/checks/anywhere.js +45 -17
- package/dist/lib/flag-registry.d.ts +4 -0
- package/dist/lib/flag-registry.js +43 -2
- package/dist/lib/flag-snapshot-file.d.ts +17 -0
- package/dist/lib/flag-snapshot-file.js +153 -37
- package/dist/lib/lanes/flag-keys.d.ts +2 -0
- package/dist/lib/lanes/flag-keys.js +2 -0
- package/dist/lib/lanes/types.d.ts +1 -1
- package/dist/utils/resident-package-preload.js +5 -1
- package/package.json +1 -1
package/dist/commands/lanes.js
CHANGED
|
@@ -37,7 +37,7 @@ import { laneDepthCap } from "../lib/lanes/depth-cap.js";
|
|
|
37
37
|
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
38
38
|
import { runResidentCommandWithPackageLease } from "../utils/resident-package-preload.js";
|
|
39
39
|
import { readLanesFlagLocalOverride, readLanesRegistryFlag, readLanesRegistryFlagFailOff } from "../lib/lanes/registry-flag.js";
|
|
40
|
-
import { LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_LIST_SKIP_REMOTE_REFRESH_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG, LANE_REUSE_REFRESHES_PROFILE_FLAG, } from "../lib/lanes/flag-keys.js";
|
|
40
|
+
import { LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_LIST_SKIP_REMOTE_REFRESH_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG, LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG, LANE_REUSE_REFRESHES_PROFILE_FLAG, } from "../lib/lanes/flag-keys.js";
|
|
41
41
|
import { configuredCodexRolloutHomes, retainCodexRollouts } from "../lib/lanes/disk-guard.js";
|
|
42
42
|
import { isControllerAliasOutpostId } from "../lib/lanes/admission-host.js";
|
|
43
43
|
import { runLeadAutopilotPass } from "../lib/lanes/autopilot.js";
|
|
@@ -4221,12 +4221,99 @@ function authorizeFailedResume(ctx, lane, encodedContext) {
|
|
|
4221
4221
|
}
|
|
4222
4222
|
return context.actor;
|
|
4223
4223
|
}
|
|
4224
|
-
const callerAuthorization =
|
|
4224
|
+
const callerAuthorization = failedResumeCallerAuthorization(ctx, lane);
|
|
4225
4225
|
if (!callerAuthorization.authorized) {
|
|
4226
|
-
throw new LanesError("failed_resume_unauthorized", appendCallerSeniorReason("Only the lane's senior may resume a failed lane with --from-failed", callerAuthorization), { lane_id: lane.lane_id, reason: callerAuthorization.reason });
|
|
4226
|
+
throw new LanesError("failed_resume_unauthorized", appendCallerSeniorReason("Only an ancestor in the lane's senior history or its company manager may resume a failed lane with --from-failed", callerAuthorization), { lane_id: lane.lane_id, reason: callerAuthorization.reason });
|
|
4227
4227
|
}
|
|
4228
4228
|
return resumeProfileRepinActor(ctx);
|
|
4229
4229
|
}
|
|
4230
|
+
function failedResumeCallerAuthorization(ctx, lane) {
|
|
4231
|
+
const direct = callerSeniorAuthorization(ctx, lane.senior);
|
|
4232
|
+
if (direct.authorized)
|
|
4233
|
+
return direct;
|
|
4234
|
+
const hqRoot = ctx.hqRoot();
|
|
4235
|
+
const targetCompany = resolveLaneCompany(hqRoot, lane).company;
|
|
4236
|
+
const visited = new Set();
|
|
4237
|
+
const pending = [
|
|
4238
|
+
lane.senior,
|
|
4239
|
+
...lane.senior_history.flatMap((entry) => [entry.from, entry.to]),
|
|
4240
|
+
];
|
|
4241
|
+
while (pending.length > 0) {
|
|
4242
|
+
const ref = pending.pop();
|
|
4243
|
+
const key = seniorKey(ref);
|
|
4244
|
+
if (visited.has(key))
|
|
4245
|
+
continue;
|
|
4246
|
+
visited.add(key);
|
|
4247
|
+
if (ref.kind === "user") {
|
|
4248
|
+
if (targetCompany === null && callerSeniorAuthorization(ctx, ref).authorized) {
|
|
4249
|
+
return { authorized: true };
|
|
4250
|
+
}
|
|
4251
|
+
continue;
|
|
4252
|
+
}
|
|
4253
|
+
if (!ref.id)
|
|
4254
|
+
continue;
|
|
4255
|
+
if (ref.kind === "lane") {
|
|
4256
|
+
const ancestor = tryReadLane(hqRoot, ref.id);
|
|
4257
|
+
if (!ancestor)
|
|
4258
|
+
continue;
|
|
4259
|
+
const ancestorCompany = resolveLaneCompany(hqRoot, ancestor).company;
|
|
4260
|
+
if (!sameLaneCompany(targetCompany, ancestorCompany))
|
|
4261
|
+
continue;
|
|
4262
|
+
if (callerSeniorAuthorization(ctx, ref).authorized)
|
|
4263
|
+
return { authorized: true };
|
|
4264
|
+
pending.push(ancestor.senior, ...ancestor.senior_history.flatMap((entry) => [entry.from, entry.to]));
|
|
4265
|
+
continue;
|
|
4266
|
+
}
|
|
4267
|
+
if (ref.kind === "session") {
|
|
4268
|
+
const loaded = loadSessionMeta(hqRoot, ref.id, ctx);
|
|
4269
|
+
if (loaded.status !== "ok")
|
|
4270
|
+
continue;
|
|
4271
|
+
const sessionCompany = companyBindingFromSessionMeta(loaded.doc);
|
|
4272
|
+
const sessionCompanyRef = sessionCompany.binding === "company"
|
|
4273
|
+
? { slug: sessionCompany.slug ?? "", uid: sessionCompany.uid ?? "" }
|
|
4274
|
+
: null;
|
|
4275
|
+
if (targetCompany === null) {
|
|
4276
|
+
if (sessionCompany.binding !== "none")
|
|
4277
|
+
continue;
|
|
4278
|
+
}
|
|
4279
|
+
else if (sessionCompany.binding !== "company" ||
|
|
4280
|
+
!sameLaneCompany(targetCompany, sessionCompanyRef)) {
|
|
4281
|
+
continue;
|
|
4282
|
+
}
|
|
4283
|
+
if (callerSeniorAuthorization(ctx, ref).authorized)
|
|
4284
|
+
return { authorized: true };
|
|
4285
|
+
const value = sessionSeniorValue(loaded.doc);
|
|
4286
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
4287
|
+
try {
|
|
4288
|
+
pending.push(parseSeniorRef(value));
|
|
4289
|
+
}
|
|
4290
|
+
catch {
|
|
4291
|
+
continue;
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4294
|
+
else if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
4295
|
+
const senior = value;
|
|
4296
|
+
if (senior.kind === "user")
|
|
4297
|
+
continue;
|
|
4298
|
+
if ((senior.kind === "lane" || senior.kind === "session") &&
|
|
4299
|
+
typeof senior.id === "string" && senior.id.trim() !== "") {
|
|
4300
|
+
pending.push({ kind: senior.kind, id: senior.id });
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
4305
|
+
const callerLaneId = ctx.env.HQ_LANE_ID?.trim();
|
|
4306
|
+
const callerLane = callerLaneId ? tryReadLane(hqRoot, callerLaneId) : null;
|
|
4307
|
+
if (callerLane && targetCompany !== null) {
|
|
4308
|
+
const callerCompany = resolveLaneCompany(hqRoot, callerLane).company;
|
|
4309
|
+
if (callerCompany !== null &&
|
|
4310
|
+
sameLaneCompany(targetCompany, callerCompany) &&
|
|
4311
|
+
resolveManagerStatusTarget(hqRoot, callerLane).manager) {
|
|
4312
|
+
return { authorized: true };
|
|
4313
|
+
}
|
|
4314
|
+
}
|
|
4315
|
+
return { authorized: false, reason: "caller is not an ancestor lane or session, nor the lane's same-company manager" };
|
|
4316
|
+
}
|
|
4230
4317
|
function workerProfileHashMismatch(lane, profile, callerAuthorization) {
|
|
4231
4318
|
const message = `Worker profile ${JSON.stringify(profile.id)} on Outpost ${lane.host.id} does not match the hash recorded by the lane`;
|
|
4232
4319
|
return new LanesError("worker_profile_hash_mismatch", callerAuthorization && !callerAuthorization.authorized
|
|
@@ -6836,7 +6923,7 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
6836
6923
|
account: target,
|
|
6837
6924
|
...(input.accountSource !== undefined ? { account_source: input.accountSource } : {}),
|
|
6838
6925
|
...(input.accountSelection !== undefined ? { account_selection: input.accountSelection } : {}),
|
|
6839
|
-
admission_accounts: undefined,
|
|
6926
|
+
admission_accounts: input.preserveAdmissionAccounts ? current.admission_accounts : undefined,
|
|
6840
6927
|
provider_session_id: sessionId,
|
|
6841
6928
|
provider_session_id_absent: undefined,
|
|
6842
6929
|
...(input.cliModel !== undefined
|
|
@@ -6866,6 +6953,9 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
6866
6953
|
...(input.refreshProfileSettings === true ? { refreshProfileSettings: true } : {}),
|
|
6867
6954
|
at,
|
|
6868
6955
|
});
|
|
6956
|
+
if (input.preserveAdmissionAccounts && current.admission_accounts !== undefined) {
|
|
6957
|
+
next.admission_accounts = [...current.admission_accounts];
|
|
6958
|
+
}
|
|
6869
6959
|
if (input.deferTurnStart) {
|
|
6870
6960
|
next.state = current.state;
|
|
6871
6961
|
}
|
|
@@ -6989,53 +7079,112 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, opti
|
|
|
6989
7079
|
});
|
|
6990
7080
|
}
|
|
6991
7081
|
catch (err) {
|
|
6992
|
-
|
|
6993
|
-
err
|
|
6994
|
-
Array.isArray(err.details.account_reasons)
|
|
6995
|
-
? err.details.account_reasons.filter((reason) => reason !== null && typeof reason === "object" && !Array.isArray(reason))
|
|
6996
|
-
: [];
|
|
7082
|
+
if (!(err instanceof LanesError) || err.error !== "claude_account_balance_no_eligible_account")
|
|
7083
|
+
throw err;
|
|
6997
7084
|
const unavailablePinReasons = new Set(["disabled", "home_missing", "not_logged_in", "quota_held"]);
|
|
6998
|
-
|
|
6999
|
-
profilePin.every((account) => accountReasons.some((reason) => reason.account === account && typeof reason.reason === "string" && unavailablePinReasons.has(reason.reason)));
|
|
7000
|
-
if (!everyProfileAccountUnavailable || lane.account === undefined ||
|
|
7001
|
-
(lane.admission_accounts !== undefined && !lane.admission_accounts.includes(lane.account)))
|
|
7085
|
+
if (profilePin === undefined || profilePin.length === 0)
|
|
7002
7086
|
throw err;
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7087
|
+
// Admission scope can hide the profile accounts from the first selection's
|
|
7088
|
+
// diagnostics. Re-evaluate only the pin so exhaustion means an unavailable
|
|
7089
|
+
// account, not merely an empty intersection with admission_accounts.
|
|
7090
|
+
let pinnedAccountReasons;
|
|
7091
|
+
try {
|
|
7092
|
+
selectClaudeAccount({ ...selectionInput, allowedAccounts: profilePin });
|
|
7093
|
+
throw err;
|
|
7094
|
+
}
|
|
7095
|
+
catch (pinErr) {
|
|
7096
|
+
if (pinErr === err)
|
|
7097
|
+
throw pinErr;
|
|
7098
|
+
if (!(pinErr instanceof LanesError) || pinErr.error !== "claude_account_balance_no_eligible_account")
|
|
7099
|
+
throw err;
|
|
7100
|
+
pinnedAccountReasons = Array.isArray(pinErr.details.account_reasons)
|
|
7101
|
+
? pinErr.details.account_reasons.filter((reason) => reason !== null && typeof reason === "object" && !Array.isArray(reason))
|
|
7102
|
+
: [];
|
|
7103
|
+
}
|
|
7104
|
+
pinnedAccountReasons = pinnedAccountReasons.filter((reason) => typeof reason.account === "string" && profilePin.includes(reason.account));
|
|
7105
|
+
const everyProfileAccountUnavailable = profilePin.every((account) => pinnedAccountReasons.some((reason) => reason.account === account && typeof reason.reason === "string" && unavailablePinReasons.has(reason.reason)));
|
|
7106
|
+
if (!everyProfileAccountUnavailable)
|
|
7107
|
+
throw err;
|
|
7108
|
+
// Refreshing profile settings is allowed to change the account pin. Keep
|
|
7109
|
+
// that operation on its existing selection path instead of applying this
|
|
7110
|
+
// fallback against a profile snapshot that is being refreshed.
|
|
7111
|
+
if (options.refreshProfileSettings === true)
|
|
7112
|
+
throw err;
|
|
7113
|
+
let fallbackSelection;
|
|
7114
|
+
if (lane.account !== undefined && lane.admission_accounts?.includes(lane.account) !== false) {
|
|
7115
|
+
try {
|
|
7116
|
+
fallbackSelection = selectClaudeAccount({ ...selectionInput, allowedAccounts: [lane.account] });
|
|
7117
|
+
}
|
|
7118
|
+
catch (recordedErr) {
|
|
7119
|
+
if (!(recordedErr instanceof LanesError) || recordedErr.error !== "claude_account_balance_no_eligible_account") {
|
|
7120
|
+
throw recordedErr;
|
|
7121
|
+
}
|
|
7122
|
+
}
|
|
7123
|
+
}
|
|
7124
|
+
if (fallbackSelection === undefined) {
|
|
7125
|
+
const resolution = resolveLaneCompany(hqRoot, lane);
|
|
7126
|
+
const company = resolution.company ?? undefined;
|
|
7127
|
+
const exhaustedPinFallbackEnabled = await readLanesRegistryFlagFailOff(LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG, {
|
|
7128
|
+
company,
|
|
7129
|
+
...(company ? { companyUidSource: laneCompanyUidSource(resolution.source) } : {}),
|
|
7130
|
+
env: ctx.env,
|
|
7131
|
+
}, readLanesRegistryFlag);
|
|
7132
|
+
if (!exhaustedPinFallbackEnabled)
|
|
7133
|
+
throw err;
|
|
7134
|
+
try {
|
|
7135
|
+
fallbackSelection = selectClaudeAccount({
|
|
7136
|
+
...selectionInput,
|
|
7137
|
+
...(lane.admission_accounts !== undefined ? { allowedAccounts: lane.admission_accounts } : {}),
|
|
7138
|
+
});
|
|
7139
|
+
}
|
|
7140
|
+
catch (balanceErr) {
|
|
7141
|
+
if (!(balanceErr instanceof LanesError) || balanceErr.error !== "claude_account_balance_no_eligible_account") {
|
|
7142
|
+
throw balanceErr;
|
|
7143
|
+
}
|
|
7144
|
+
const balancerAccountReasons = Array.isArray(balanceErr.details.account_reasons)
|
|
7145
|
+
? balanceErr.details.account_reasons.filter((reason) => reason !== null && typeof reason === "object" && !Array.isArray(reason) &&
|
|
7146
|
+
reason.reason !== "not_allowed_by_lane")
|
|
7147
|
+
: [];
|
|
7148
|
+
throw new LanesError(balanceErr.error, balanceErr.message, {
|
|
7149
|
+
...balanceErr.details,
|
|
7150
|
+
account_reasons: balancerAccountReasons,
|
|
7151
|
+
profile_pin_reasons: pinnedAccountReasons,
|
|
7152
|
+
balancer_account_reasons: balancerAccountReasons,
|
|
7153
|
+
});
|
|
7154
|
+
}
|
|
7155
|
+
}
|
|
7007
7156
|
selected = {
|
|
7008
|
-
...
|
|
7009
|
-
considered:
|
|
7010
|
-
const pinnedReason =
|
|
7011
|
-
? accountReasons.find((reason) => reason.account === entry.account)?.reason
|
|
7012
|
-
: undefined;
|
|
7157
|
+
...fallbackSelection,
|
|
7158
|
+
considered: fallbackSelection.considered.map((entry) => {
|
|
7159
|
+
const pinnedReason = pinnedAccountReasons.find((reason) => reason.account === entry.account)?.reason;
|
|
7013
7160
|
return typeof pinnedReason === "string"
|
|
7014
7161
|
? { ...entry, eligible: false, ineligible_reason: pinnedReason }
|
|
7015
7162
|
: entry;
|
|
7016
7163
|
}),
|
|
7017
7164
|
skipped_accounts: [
|
|
7018
|
-
...
|
|
7165
|
+
...fallbackSelection.skipped_accounts.filter((entry) => !profilePin.includes(entry.account)),
|
|
7019
7166
|
...profilePin.map((account) => ({
|
|
7020
7167
|
account,
|
|
7021
7168
|
host: lane.host,
|
|
7022
|
-
reason: String(
|
|
7169
|
+
reason: String(pinnedAccountReasons.find((entry) => entry.account === account)?.reason),
|
|
7023
7170
|
})),
|
|
7024
7171
|
],
|
|
7025
7172
|
};
|
|
7026
7173
|
profilePinUnavailable = true;
|
|
7027
7174
|
}
|
|
7028
|
-
const historyReason =
|
|
7029
|
-
? "
|
|
7030
|
-
:
|
|
7031
|
-
? "
|
|
7032
|
-
:
|
|
7033
|
-
? "
|
|
7034
|
-
: currentLogin
|
|
7035
|
-
? "
|
|
7036
|
-
:
|
|
7037
|
-
? "
|
|
7038
|
-
: "
|
|
7175
|
+
const historyReason = profilePinUnavailable
|
|
7176
|
+
? "profile_pin_exhausted"
|
|
7177
|
+
: lane.account === undefined
|
|
7178
|
+
? "load_balance"
|
|
7179
|
+
: currentConfig?.enabled !== true
|
|
7180
|
+
? "account_unavailable"
|
|
7181
|
+
: currentLogin === false
|
|
7182
|
+
? "recorded_account_logged_out"
|
|
7183
|
+
: currentLogin !== true
|
|
7184
|
+
? "account_unavailable"
|
|
7185
|
+
: quotaBlockFor(loadQuotaState(hqRoot), "claude", lane.account, model, ctx.now()).blocked
|
|
7186
|
+
? "usage_limit"
|
|
7187
|
+
: "load_balance";
|
|
7039
7188
|
return {
|
|
7040
7189
|
selection: {
|
|
7041
7190
|
...selected,
|
|
@@ -7224,19 +7373,26 @@ function assertLaneNotRunningForReparent(lane) {
|
|
|
7224
7373
|
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
7374
|
}
|
|
7226
7375
|
}
|
|
7227
|
-
function runReparentLocked(ctx, laneId, opts, finishedChildrenDoNotCount = false) {
|
|
7376
|
+
async function runReparentLocked(ctx, laneId, opts, finishedChildrenDoNotCount = false) {
|
|
7228
7377
|
const hqRoot = ctx.hqRoot();
|
|
7229
7378
|
if (!opts.senior) {
|
|
7230
7379
|
throw new LanesError("missing_senior", "--senior is required", {});
|
|
7231
7380
|
}
|
|
7232
7381
|
const nextSenior = parseSeniorRef(opts.senior);
|
|
7233
7382
|
assertSeniorExists(hqRoot, nextSenior, ctx);
|
|
7234
|
-
|
|
7383
|
+
let lane = readLane(hqRoot, laneId);
|
|
7235
7384
|
assertLaneRunsHereForOperatorMutation(ctx, lane, "reparent");
|
|
7385
|
+
assertLaneNotRunningForReparent(lane);
|
|
7236
7386
|
const seniorCompany = companyOfSenior(hqRoot, nextSenior, ctx);
|
|
7237
7387
|
if (lane.company === null && seniorCompany.binding === "company") {
|
|
7238
|
-
const company = seniorCompany.slug ?? seniorCompany.uid
|
|
7239
|
-
|
|
7388
|
+
const company = seniorCompany.slug ?? (seniorCompany.uid
|
|
7389
|
+
? canonicalCompanySlugForUid(hqRoot, seniorCompany.uid, nextSenior)
|
|
7390
|
+
: undefined);
|
|
7391
|
+
if (!company) {
|
|
7392
|
+
throw new LanesError("lane_company_missing", `Lane ${laneId} has no company attribution and the destination senior's company cannot be resolved`, { lane_id: laneId, senior: nextSenior, senior_company: serializeSeniorCompany(seniorCompany) });
|
|
7393
|
+
}
|
|
7394
|
+
await repairCompanyOnOwner(ctx, laneId, { company }, false);
|
|
7395
|
+
lane = readLane(hqRoot, laneId);
|
|
7240
7396
|
}
|
|
7241
7397
|
assertSameTenant(lane.company, nextSenior, seniorCompany, { lane_id: laneId });
|
|
7242
7398
|
if (nextSenior.kind === "lane" && seniorKey(lane.senior) !== seniorKey(nextSenior)) {
|
|
@@ -7372,7 +7528,7 @@ async function runRepairCompany(ctx, laneId, opts) {
|
|
|
7372
7528
|
}
|
|
7373
7529
|
await repairCompanyOnOwner(ctx, laneId, opts);
|
|
7374
7530
|
}
|
|
7375
|
-
async function repairCompanyOnOwner(ctx, laneId, opts) {
|
|
7531
|
+
async function repairCompanyOnOwner(ctx, laneId, opts, emitResult = true) {
|
|
7376
7532
|
const hqRoot = ctx.hqRoot();
|
|
7377
7533
|
const companySlug = opts.company?.trim();
|
|
7378
7534
|
if (!companySlug) {
|
|
@@ -7450,7 +7606,9 @@ async function repairCompanyOnOwner(ctx, laneId, opts) {
|
|
|
7450
7606
|
const repairedLane = readLane(hqRoot, laneId);
|
|
7451
7607
|
syncLaneToProjectStoryLedger(hqRoot, repairedLane, migratedQuestionRefs);
|
|
7452
7608
|
removeLaneFromProjectStoryLedger(hqRoot, initial);
|
|
7453
|
-
|
|
7609
|
+
if (emitResult) {
|
|
7610
|
+
printJson({ ok: true, action: "repair-company", lane_id: laneId, lane: repairedLane, migrated_pending_questions: migratedQuestionRefs.length });
|
|
7611
|
+
}
|
|
7454
7612
|
}
|
|
7455
7613
|
async function runRepairCompanyOwnerTarget(ctx, laneId, opts) {
|
|
7456
7614
|
if (!ctx.isSsmOwnerDispatch()) {
|
|
@@ -8476,7 +8634,8 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
|
|
|
8476
8634
|
const moved = await prepareAccountMove(ctx, recoveredLane, {
|
|
8477
8635
|
account: selection.account,
|
|
8478
8636
|
cliModel: pendingModel,
|
|
8479
|
-
accountHistoryReason: "keep_alive_resume",
|
|
8637
|
+
accountHistoryReason: automatic.historyReason === "profile_pin_exhausted" ? automatic.historyReason : "keep_alive_resume",
|
|
8638
|
+
...(automatic.historyReason === "profile_pin_exhausted" ? { preserveAdmissionAccounts: true } : {}),
|
|
8480
8639
|
...(pendingModel === undefined && automatic.preservedModel !== undefined
|
|
8481
8640
|
? { preservedModel: automatic.preservedModel }
|
|
8482
8641
|
: {}),
|
|
@@ -9185,10 +9344,10 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9185
9344
|
}));
|
|
9186
9345
|
lanes
|
|
9187
9346
|
.command("resume")
|
|
9188
|
-
.description("Resume an interrupted, done or blocked lane, or an awaiting_input lane with no answerable question; recover failed lanes with
|
|
9347
|
+
.description("Resume an interrupted, done or blocked lane, or an awaiting_input lane with no answerable question; recover failed lanes with --from-failed as a senior ancestor or same-company manager")
|
|
9189
9348
|
.argument("<lane-id>", "Lane id")
|
|
9190
9349
|
.option("--text <msg>", "Operator message for the resumed turn")
|
|
9191
|
-
.option("--from-failed", "Explicitly resume a failed lane as
|
|
9350
|
+
.option("--from-failed", "Explicitly resume a failed lane as an ancestor or same-company manager")
|
|
9192
9351
|
.option("--reason <text>", "Required explanation for resuming from failed")
|
|
9193
9352
|
.option("--project <slug>", "Re-point project attribution; a changed story is named in the resumed prompt")
|
|
9194
9353
|
.option("--story <id>", "Re-point story attribution; a changed story is named in the resumed prompt")
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export interface ReadHqAnywherePersonSettingOptions {
|
|
2
2
|
readSetting?: () => Promise<boolean | null>;
|
|
3
|
+
readFlag?: (env: NodeJS.ProcessEnv) => boolean;
|
|
4
|
+
refreshFlag?: () => Promise<void>;
|
|
3
5
|
getPersonToken?: () => Promise<string>;
|
|
4
6
|
fetchImpl?: typeof fetch;
|
|
5
7
|
apiBaseUrl?: string;
|
|
@@ -33,7 +33,8 @@ export function evaluateAnywhereRuntime(flagEnabled, setting) {
|
|
|
33
33
|
}
|
|
34
34
|
/** The operator flag always wins; the person preference is fetched only while it is on. */
|
|
35
35
|
export async function anywhereRuntimeAllowed(env = process.env, options = {}) {
|
|
36
|
-
|
|
36
|
+
await options.refreshFlag?.();
|
|
37
|
+
if (!(options.readFlag ?? anywhereRuntimeEnabledFromSnapshot)(env))
|
|
37
38
|
return false;
|
|
38
39
|
const setting = options.readSetting ? await options.readSetting() : await readHqAnywherePersonSetting({ ...options, env });
|
|
39
40
|
return evaluateAnywhereRuntime(true, setting).enabled;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readLocalOverride, resolveFlag } from "@indigoai-us/hq-flags-client";
|
|
2
2
|
import { resolveProcessFlagCompanyContext } from "./process-flag-company-context.js";
|
|
3
|
-
import { readFlagSnapshotFile } from "./flag-snapshot-file.js";
|
|
3
|
+
import { readFlagSnapshotFile, readPersonFlagSnapshotFile } from "./flag-snapshot-file.js";
|
|
4
4
|
import { HQ_ANYWHERE_RUNTIME_FLAG } from "./anywhere-runtime-flag-key.js";
|
|
5
5
|
/** Resolve the Anywhere gate from hq-flags without loading the CLI's large worker bundle. */
|
|
6
6
|
export function anywhereRuntimeEnabledFromSnapshot(env = process.env) {
|
|
@@ -8,10 +8,16 @@ export function anywhereRuntimeEnabledFromSnapshot(env = process.env) {
|
|
|
8
8
|
if (local !== null)
|
|
9
9
|
return local;
|
|
10
10
|
const company = resolveProcessFlagCompanyContext(env);
|
|
11
|
-
const
|
|
11
|
+
const hasCompanyContext = Boolean(company.companyUid || company.companySlug);
|
|
12
|
+
const persisted = company.companyUid
|
|
13
|
+
? readFlagSnapshotFile(company.companyUid, { env })
|
|
14
|
+
: hasCompanyContext
|
|
15
|
+
? null
|
|
16
|
+
: readPersonFlagSnapshotFile({ env });
|
|
12
17
|
const snapshot = persisted
|
|
13
18
|
? { version: 0, flags: persisted.flags }
|
|
14
19
|
: null;
|
|
20
|
+
// HQ Anywhere authorizes a capability, so a missing or stale snapshot fails closed.
|
|
15
21
|
return resolveFlag({
|
|
16
22
|
flagKey: HQ_ANYWHERE_RUNTIME_FLAG,
|
|
17
23
|
env,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { HqdServer } from "./server.js";
|
|
2
|
+
export interface HqdRuntimeListenerOptions {
|
|
3
|
+
isEnabled: () => Promise<boolean>;
|
|
4
|
+
startServer: () => Promise<HqdServer>;
|
|
5
|
+
onServerChange?: (server: HqdServer | null) => void;
|
|
6
|
+
intervalMs?: number;
|
|
7
|
+
schedule?: (callback: () => void, delayMs: number) => () => void;
|
|
8
|
+
log?: (error: unknown) => void;
|
|
9
|
+
}
|
|
10
|
+
export interface HqdRuntimeListener {
|
|
11
|
+
refresh(): Promise<void>;
|
|
12
|
+
close(): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
/** Keep the local hqd listener aligned with the live flag and person setting. */
|
|
15
|
+
export declare function startHqdRuntimeListener(options: HqdRuntimeListenerOptions): Promise<HqdRuntimeListener>;
|
|
16
|
+
//# sourceMappingURL=anywhere-listener.d.ts.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** Keep the local hqd listener aligned with the live flag and person setting. */
|
|
2
|
+
export async function startHqdRuntimeListener(options) {
|
|
3
|
+
const intervalMs = options.intervalMs ?? 60_000;
|
|
4
|
+
const log = options.log ?? (() => { });
|
|
5
|
+
const schedule = options.schedule ?? ((callback, delayMs) => {
|
|
6
|
+
const timer = setInterval(callback, delayMs);
|
|
7
|
+
timer.unref();
|
|
8
|
+
return () => clearInterval(timer);
|
|
9
|
+
});
|
|
10
|
+
let server = null;
|
|
11
|
+
let closed = false;
|
|
12
|
+
let inFlight = null;
|
|
13
|
+
const refresh = () => {
|
|
14
|
+
if (closed)
|
|
15
|
+
return Promise.resolve();
|
|
16
|
+
if (inFlight)
|
|
17
|
+
return inFlight;
|
|
18
|
+
inFlight = (async () => {
|
|
19
|
+
let enabled = false;
|
|
20
|
+
try {
|
|
21
|
+
enabled = await options.isEnabled();
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
log(error);
|
|
25
|
+
}
|
|
26
|
+
if (closed || enabled === (server !== null))
|
|
27
|
+
return;
|
|
28
|
+
if (enabled) {
|
|
29
|
+
try {
|
|
30
|
+
const opened = await options.startServer();
|
|
31
|
+
if (closed) {
|
|
32
|
+
await opened.close();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
server = opened;
|
|
36
|
+
options.onServerChange?.(server);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
log(error);
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const current = server;
|
|
44
|
+
server = null;
|
|
45
|
+
options.onServerChange?.(null);
|
|
46
|
+
if (current) {
|
|
47
|
+
try {
|
|
48
|
+
await current.close();
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
log(error);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
})().finally(() => {
|
|
55
|
+
inFlight = null;
|
|
56
|
+
});
|
|
57
|
+
return inFlight;
|
|
58
|
+
};
|
|
59
|
+
await refresh();
|
|
60
|
+
if (closed)
|
|
61
|
+
return { refresh, close: async () => { } };
|
|
62
|
+
const cancel = schedule(() => { void refresh(); }, intervalMs);
|
|
63
|
+
return {
|
|
64
|
+
refresh,
|
|
65
|
+
async close() {
|
|
66
|
+
if (closed)
|
|
67
|
+
return;
|
|
68
|
+
closed = true;
|
|
69
|
+
cancel();
|
|
70
|
+
await inFlight;
|
|
71
|
+
const current = server;
|
|
72
|
+
server = null;
|
|
73
|
+
options.onServerChange?.(null);
|
|
74
|
+
if (current)
|
|
75
|
+
await current.close();
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=anywhere-listener.js.map
|
package/dist/lib/daemon/run.js
CHANGED
|
@@ -35,6 +35,7 @@ import { detectMachineType } from "./machine-type.js";
|
|
|
35
35
|
import { appendPrivate, daemonPaths, ensurePrivateDir, openPrivateAppend } from "./paths.js";
|
|
36
36
|
import { hqdSocketPath } from "./protocol.js";
|
|
37
37
|
import { startHqdServer } from "./server.js";
|
|
38
|
+
import { startHqdRuntimeListener } from "./anywhere-listener.js";
|
|
38
39
|
import { heartbeatFile, startHeartbeat } from "./heartbeat.js";
|
|
39
40
|
import { createProtocolTail, ensureProtocolFifo, openProtocolWriter } from "./protocol-tail.js";
|
|
40
41
|
import { takeControlRequests } from "./requests.js";
|
|
@@ -835,14 +836,19 @@ export async function runDaemon(opts) {
|
|
|
835
836
|
return on;
|
|
836
837
|
};
|
|
837
838
|
// hqd: the session/company socket every runtime talks to (hq-anywhere US-008).
|
|
838
|
-
let hqd = null;
|
|
839
839
|
let hqdShutdown = null;
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
840
|
+
let stopHeartbeat = () => { };
|
|
841
|
+
const hqdListener = await startHqdRuntimeListener({
|
|
842
|
+
isEnabled: async () => shouldStartHqd(await anywhereRuntimeAllowed(env, {
|
|
843
|
+
refreshFlag: async () => {
|
|
844
|
+
const { refreshFlagRegistrySnapshot } = await import("../flag-registry.js");
|
|
845
|
+
await refreshFlagRegistrySnapshot(5_000);
|
|
846
|
+
},
|
|
847
|
+
onError: (name) => log("warn", `hqd person setting read failed (${name}); runtime remains off`),
|
|
848
|
+
})),
|
|
849
|
+
startServer: async () => {
|
|
844
850
|
const startConfig = readDaemonConfig(paths.configPath);
|
|
845
|
-
|
|
851
|
+
return startHqdServer({
|
|
846
852
|
socketPath: hqdSocketPath(home, env),
|
|
847
853
|
registryPath: registryFile(registryDir(home, env)),
|
|
848
854
|
hqRoot: startConfig.hqRoot ?? (env.HQ_ROOT?.trim() || undefined),
|
|
@@ -850,24 +856,25 @@ export async function runDaemon(opts) {
|
|
|
850
856
|
onShutdown: () => hqdShutdown?.(),
|
|
851
857
|
reportError: reportHqdCrash,
|
|
852
858
|
});
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
}
|
|
867
|
-
:
|
|
859
|
+
},
|
|
860
|
+
onServerChange: (server) => {
|
|
861
|
+
stopHeartbeat();
|
|
862
|
+
stopHeartbeat = () => { };
|
|
863
|
+
if (!server)
|
|
864
|
+
return;
|
|
865
|
+
// US-012: 60s presence heartbeat with version and session count.
|
|
866
|
+
stopHeartbeat = startHeartbeat({
|
|
867
|
+
version: CLI_VERSION,
|
|
868
|
+
sessionCount: () => server.sessions.size,
|
|
869
|
+
file: heartbeatFile(home, env),
|
|
870
|
+
log,
|
|
871
|
+
});
|
|
872
|
+
},
|
|
873
|
+
intervalMs: 60_000,
|
|
874
|
+
log: (error) => log("error", `hqd socket listener update failed: ${error.message ?? String(error)}`),
|
|
875
|
+
});
|
|
868
876
|
const closeHqd = () => {
|
|
869
|
-
|
|
870
|
-
void hqd?.close().catch((err) => log("warn", `hqd socket did not close cleanly: ${err.message}`));
|
|
877
|
+
void hqdListener.close().catch((err) => log("warn", `hqd socket did not close cleanly: ${err.message}`));
|
|
871
878
|
};
|
|
872
879
|
return await new Promise((resolve) => {
|
|
873
880
|
let stopping = false;
|
|
@@ -24,6 +24,38 @@ export interface RemoteStatusResponse {
|
|
|
24
24
|
secretRef: string;
|
|
25
25
|
} | null;
|
|
26
26
|
}
|
|
27
|
+
export type RemoteAppLimitState = "ok" | "warn" | "overage" | "locked";
|
|
28
|
+
/** One app that owns its cluster. Cents are whole US cents; `estimate` is null for an exempt company. */
|
|
29
|
+
export interface RemoteAppUsage {
|
|
30
|
+
appId: string;
|
|
31
|
+
period: string;
|
|
32
|
+
measuredAt: string | null;
|
|
33
|
+
ended: boolean;
|
|
34
|
+
state: RemoteAppLimitState;
|
|
35
|
+
locked: boolean;
|
|
36
|
+
exempt: boolean;
|
|
37
|
+
dpu: {
|
|
38
|
+
used: number;
|
|
39
|
+
included: number;
|
|
40
|
+
overage: number;
|
|
41
|
+
ceiling: number;
|
|
42
|
+
pct: number;
|
|
43
|
+
};
|
|
44
|
+
storage: {
|
|
45
|
+
usedBytes: number;
|
|
46
|
+
billedBytes: number;
|
|
47
|
+
includedBytes: number;
|
|
48
|
+
overageGbMonth: number;
|
|
49
|
+
ceilingBytes: number;
|
|
50
|
+
pct: number;
|
|
51
|
+
};
|
|
52
|
+
estimate: {
|
|
53
|
+
baseCents: number;
|
|
54
|
+
dpuOverageCents: number;
|
|
55
|
+
storageOverageCents: number;
|
|
56
|
+
totalCents: number;
|
|
57
|
+
} | null;
|
|
58
|
+
}
|
|
27
59
|
export interface RemoteUsageResponse {
|
|
28
60
|
companyUid: string;
|
|
29
61
|
dpuUsed: number;
|
|
@@ -34,7 +66,20 @@ export interface RemoteUsageResponse {
|
|
|
34
66
|
pctStorage: number;
|
|
35
67
|
period: string;
|
|
36
68
|
measuredAt: string | null;
|
|
69
|
+
/** False when every app owns its own cluster and there is no shared company database. */
|
|
70
|
+
companyCluster?: boolean;
|
|
71
|
+
apps?: RemoteAppUsage[];
|
|
37
72
|
}
|
|
73
|
+
/** Answer for one app: its own numbers, or the company numbers when it shares the company cluster. */
|
|
74
|
+
export type RemoteAppUsageResponse = (RemoteAppUsage & {
|
|
75
|
+
companyUid: string;
|
|
76
|
+
mode: "app-cluster";
|
|
77
|
+
status?: string;
|
|
78
|
+
}) | (RemoteUsageResponse & {
|
|
79
|
+
appId: string;
|
|
80
|
+
mode: "company-cluster";
|
|
81
|
+
exempt?: boolean;
|
|
82
|
+
});
|
|
38
83
|
/**
|
|
39
84
|
* Scoped temporary AWS credentials for one app's database role. The control
|
|
40
85
|
* plane would issue these from `POST /v1/db/apps/{appId}/session`.
|
|
@@ -74,6 +119,7 @@ export declare class ControlPlaneDbClient {
|
|
|
74
119
|
region?: string;
|
|
75
120
|
}): Promise<RemoteProvisionResponse>;
|
|
76
121
|
status(companyUid: string): Promise<RemoteStatusResponse>;
|
|
122
|
+
appUsage(companyUid: string, appId: string): Promise<RemoteAppUsageResponse>;
|
|
77
123
|
usage(companyUid: string): Promise<RemoteUsageResponse>;
|
|
78
124
|
appSession(input: {
|
|
79
125
|
companyUid: string;
|
|
@@ -78,6 +78,20 @@ export class ControlPlaneDbClient {
|
|
|
78
78
|
}
|
|
79
79
|
return JSON.parse(text);
|
|
80
80
|
}
|
|
81
|
+
async appUsage(companyUid, appId) {
|
|
82
|
+
const token = await this.getAccessToken();
|
|
83
|
+
const query = `companyUid=${encodeURIComponent(companyUid)}&appId=${encodeURIComponent(appId)}`;
|
|
84
|
+
const res = await this.fetchImpl(`${this.baseUrl}/v1/db/usage?${query}`, {
|
|
85
|
+
method: "GET",
|
|
86
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
87
|
+
signal: AbortSignal.timeout(DB_STATUS_TIMEOUT_MS),
|
|
88
|
+
});
|
|
89
|
+
const text = await res.text();
|
|
90
|
+
assertNoPostgresUrl("usage", text);
|
|
91
|
+
if (!res.ok)
|
|
92
|
+
throw httpError("usage", res.status, text);
|
|
93
|
+
return JSON.parse(text);
|
|
94
|
+
}
|
|
81
95
|
async usage(companyUid) {
|
|
82
96
|
const token = await this.getAccessToken();
|
|
83
97
|
const url = `${this.baseUrl}/v1/db/usage?companyUid=${encodeURIComponent(companyUid)}`;
|