@indigoai-us/hq-cli 5.345.156 → 5.345.160
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 +255 -0
- package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +110 -2
- package/assets/scaffold/core/scripts/rebuild-projects-index.sh +101 -5
- package/assets/scaffold/core/scripts/worktree.sh +4 -0
- package/dist/command-catalog.generated.d.ts +535 -10
- package/dist/command-catalog.generated.js +678 -31
- package/dist/command-registration-plan.d.ts +18 -0
- package/dist/command-registration-plan.js +4 -0
- package/dist/commands/_patterns.d.ts +17 -0
- package/dist/commands/_patterns.js +22 -0
- package/dist/commands/access.d.ts +6 -0
- package/dist/commands/access.js +18 -1
- package/dist/commands/acl-lock.d.ts +41 -0
- package/dist/commands/acl-lock.js +28 -0
- package/dist/commands/agent-enroll.d.ts +2 -0
- package/dist/commands/agent-enroll.js +9 -4
- package/dist/commands/agent-kit.js +8 -6
- package/dist/commands/agents.js +5 -11
- package/dist/commands/billing-sponsor.d.ts +4 -4
- package/dist/commands/billing-sponsor.js +11 -11
- package/dist/commands/billing-usage.d.ts +22 -8
- package/dist/commands/billing-usage.js +57 -22
- package/dist/commands/billing.d.ts +7 -7
- package/dist/commands/billing.js +13 -13
- package/dist/commands/bot.js +7 -4
- package/dist/commands/channel-suggest.d.ts +56 -0
- package/dist/commands/channel-suggest.js +170 -0
- package/dist/commands/channels.js +7 -1
- package/dist/commands/cloud-provision.js +2 -2
- package/dist/commands/cloud.d.ts +45 -0
- package/dist/commands/cloud.js +157 -17
- package/dist/commands/company.js +4 -3
- package/dist/commands/core-checkpoint-native.d.ts +18 -0
- package/dist/commands/core-checkpoint-native.js +67 -2
- package/dist/commands/core-pipeline-driver-native.d.ts +12 -1
- package/dist/commands/core-pipeline-driver-native.js +131 -5
- package/dist/commands/core.js +12 -0
- package/dist/commands/crm.js +83 -6
- package/dist/commands/db-remote-deps.js +12 -1
- package/dist/commands/deploy-access.d.ts +33 -0
- package/dist/commands/deploy-access.js +322 -0
- package/dist/commands/dm-attach.d.ts +106 -0
- package/dist/commands/dm-attach.js +184 -0
- package/dist/commands/dm.d.ts +37 -0
- package/dist/commands/dm.js +172 -10
- package/dist/commands/feedback-show.d.ts +40 -0
- package/dist/commands/feedback-show.js +261 -0
- package/dist/commands/feedback.d.ts +21 -0
- package/dist/commands/feedback.js +152 -2
- package/dist/commands/files.d.ts +24 -0
- package/dist/commands/files.js +71 -8
- package/dist/commands/integrations-connect.js +7 -3
- package/dist/commands/lanes-accounts.js +16 -1
- package/dist/commands/lanes-loop.js +14 -4
- package/dist/commands/lanes-message.js +6 -4
- package/dist/commands/lanes-resume.js +36 -13
- package/dist/commands/lanes-shared.d.ts +5 -0
- package/dist/commands/lanes-shared.js +13 -3
- package/dist/commands/lanes-watch.js +1 -0
- package/dist/commands/lanes.d.ts +20 -1
- package/dist/commands/lanes.js +201 -51
- package/dist/commands/meetings.d.ts +4 -4
- package/dist/commands/meetings.js +17 -8
- package/dist/commands/mesh.js +149 -1
- package/dist/commands/monitor.d.ts +1 -0
- package/dist/commands/monitor.js +4 -1
- package/dist/commands/repo.d.ts +41 -0
- package/dist/commands/repo.js +331 -0
- package/dist/commands/scan.d.ts +3 -1
- package/dist/commands/scan.js +7 -3
- package/dist/commands/secrets.d.ts +21 -0
- package/dist/commands/secrets.js +114 -14
- package/dist/commands/signals.js +17 -5
- package/dist/commands/skill.js +6 -6
- package/dist/commands/staff/listings.d.ts +41 -0
- package/dist/commands/staff/listings.js +160 -0
- package/dist/commands/staff/reset-test-account.d.ts +3 -2
- package/dist/commands/staff/reset-test-account.js +4 -3
- package/dist/commands/whoami.js +1 -1
- package/dist/hook-engine-path.js +13 -2
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +3392 -1243
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/run/inbox.js +2 -1
- package/dist/lib/agent-kit/wake-secrets.d.ts +7 -0
- package/dist/lib/agent-kit/wake-secrets.js +32 -0
- package/dist/lib/agent-kit/wake.d.ts +4 -0
- package/dist/lib/agent-kit/wake.js +49 -3
- package/dist/lib/billing/plan-limit-refusals.d.ts +13 -3
- package/dist/lib/billing/plan-limit-refusals.js +26 -3
- package/dist/lib/billing/plan-limit-upgrade-url.d.ts +8 -0
- package/dist/lib/billing/plan-limit-upgrade-url.js +18 -0
- package/dist/lib/billing/plan-lock.d.ts +22 -6
- package/dist/lib/billing/plan-lock.js +58 -30
- package/dist/lib/billing/usage-cost.d.ts +24 -2
- package/dist/lib/billing/usage-cost.js +103 -3
- package/dist/lib/billing/usage-only.d.ts +37 -1
- package/dist/lib/billing/usage-only.js +89 -4
- package/dist/lib/billing/usage-pause.d.ts +27 -0
- package/dist/lib/billing/usage-pause.js +50 -0
- package/dist/lib/bot/inflight.d.ts +7 -1
- package/dist/lib/bot/inflight.js +10 -2
- package/dist/lib/bot/prompt.js +3 -0
- package/dist/lib/bot/run.js +72 -20
- package/dist/lib/bot/session.d.ts +14 -0
- package/dist/lib/bot/session.js +70 -0
- package/dist/lib/capacity/collectors/claude-oauth.d.ts +6 -0
- package/dist/lib/capacity/collectors/claude-oauth.js +12 -0
- package/dist/lib/capacity/lane-login.d.ts +6 -0
- package/dist/lib/capacity/lane-login.js +5 -0
- package/dist/lib/core-utils/worktree.js +2 -0
- package/dist/lib/daemon/exit-cause.d.ts +43 -0
- package/dist/lib/daemon/exit-cause.js +88 -0
- package/dist/lib/daemon/handoff.d.ts +2 -0
- package/dist/lib/daemon/handoff.js +6 -1
- package/dist/lib/daemon/hq-child-bootstrap.mjs +20 -3
- package/dist/lib/daemon/paths.d.ts +2 -0
- package/dist/lib/daemon/paths.js +1 -0
- package/dist/lib/daemon/run.js +13 -0
- package/dist/lib/daemon/supervisor.d.ts +54 -1
- package/dist/lib/daemon/supervisor.js +297 -21
- package/dist/lib/daemon/unit-holds.d.ts +22 -0
- package/dist/lib/daemon/unit-holds.js +86 -0
- package/dist/lib/db/control-plane.d.ts +5 -0
- package/dist/lib/db/control-plane.js +10 -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/hook-engine/resolve.d.ts +14 -0
- package/dist/lib/hook-engine/resolve.js +55 -1
- package/dist/lib/index-render/company-knowledge.js +1 -3
- package/dist/lib/index-render/projects.js +1 -3
- package/dist/lib/lanes/account-company.d.ts +28 -0
- package/dist/lib/lanes/account-company.js +53 -0
- package/dist/lib/lanes/accounts.d.ts +22 -3
- package/dist/lib/lanes/accounts.js +38 -2
- package/dist/lib/lanes/claude-balance.d.ts +6 -1
- package/dist/lib/lanes/claude-balance.js +6 -1
- package/dist/lib/lanes/claude-credential.d.ts +78 -0
- package/dist/lib/lanes/claude-credential.js +163 -0
- package/dist/lib/lanes/compaction.d.ts +2 -0
- package/dist/lib/lanes/compaction.js +9 -0
- package/dist/lib/lanes/config.js +6 -0
- package/dist/lib/lanes/envelope-repair-dispatch.js +2 -1
- package/dist/lib/lanes/hooks-install.js +2 -1
- package/dist/lib/lanes/host-capacity.d.ts +2 -0
- package/dist/lib/lanes/host-capacity.js +1 -0
- package/dist/lib/lanes/keep-alive.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.js +3 -1
- package/dist/lib/lanes/links.js +10 -4
- package/dist/lib/lanes/loop.js +16 -1
- package/dist/lib/lanes/node-binary.d.ts +17 -0
- package/dist/lib/lanes/node-binary.js +30 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
- package/dist/lib/lanes/outpost-dispatch.js +1 -0
- package/dist/lib/lanes/reconcile.js +4 -0
- package/dist/lib/lanes/remote-control-lane.js +2 -0
- package/dist/lib/lanes/spawn.d.ts +3 -0
- package/dist/lib/lanes/spawn.js +119 -9
- package/dist/lib/lanes/store.d.ts +6 -0
- package/dist/lib/lanes/store.js +107 -18
- package/dist/lib/lanes/types.d.ts +2 -0
- package/dist/lib/lanes/watch.js +28 -5
- package/dist/lib/lanes/watcher-main.js +4 -3
- package/dist/lib/lanes/watcher.d.ts +4 -0
- package/dist/lib/lanes/watcher.js +110 -21
- package/dist/lib/mesh/api.d.ts +32 -0
- package/dist/lib/mesh/api.js +75 -0
- package/dist/lib/plan-limit-nag.d.ts +0 -16
- package/dist/lib/plan-limit-nag.js +17 -18
- package/dist/lib/secret-scan/detect.js +9 -0
- package/dist/lib/secret-scan/repo-config.d.ts +41 -0
- package/dist/lib/secret-scan/repo-config.js +237 -0
- package/dist/lib/secret-scan/rules.d.ts +7 -1
- package/dist/lib/secret-scan/rules.js +3 -1
- package/dist/lib/secret-scan/staged.d.ts +5 -0
- package/dist/lib/secret-scan/staged.js +19 -2
- package/dist/lib/secret-scan/types.d.ts +2 -0
- package/dist/lib/sync/server-owned-local-only.d.ts +29 -0
- package/dist/lib/sync/server-owned-local-only.js +75 -0
- package/dist/main.d.ts +0 -2
- package/dist/main.js +19 -57
- package/dist/utils/package-use-lease.js +27 -9
- package/dist/utils/plan-gate-error.d.ts +30 -14
- package/dist/utils/plan-gate-error.js +60 -46
- package/dist/utils/team-upgrade.d.ts +19 -6
- package/dist/utils/team-upgrade.js +37 -39
- package/dist/utils/version-gate.d.ts +7 -0
- package/dist/utils/version-gate.js +66 -0
- package/package.json +8 -8
package/dist/lib/bot/run.js
CHANGED
|
@@ -85,7 +85,7 @@ import { SIGN_IN_RETRY_MS, signInNeededText } from "./runtime-sign-in.js";
|
|
|
85
85
|
import { classifyRuntimeError, classifyRuntimeFailure, runtimeDidNotFinishText, runtimeFailureReason, runtimeFailureText, runtimeRepairNotice, withRetrySuggestion, } from "./runtime-failure.js";
|
|
86
86
|
import { HQ_BOT_AGENT_UID_ENV } from "../../commands/onboard-bot-membership.js";
|
|
87
87
|
import { captureHeartbeatIdentityMismatch, identityMismatchMessage, isHeartbeatIdentityMismatch, } from "../heartbeat-identity.js";
|
|
88
|
-
import { clearBotSession, readBotSession, writeBotSession } from "./session.js";
|
|
88
|
+
import { clearBotSession, ensureBotCompanyScope, readBotSession, writeBotSession } from "./session.js";
|
|
89
89
|
import { defaultBotStatus, patchBotStatus, readBotStatus } from "./status.js";
|
|
90
90
|
import { CLI_VERSION } from "../../cli-version.js";
|
|
91
91
|
import { CLI_UPDATE_RESTART_EXIT_CODE } from "./cli-update.js";
|
|
@@ -309,6 +309,8 @@ function defaultSleep(ms, signal) {
|
|
|
309
309
|
* one did not: `delivered` is how many parts the person has, so a retry starts
|
|
310
310
|
* where it stopped instead of repeating what they already read.
|
|
311
311
|
*/
|
|
312
|
+
class CompanyBindingError extends Error {
|
|
313
|
+
}
|
|
312
314
|
class PartialPostError extends Error {
|
|
313
315
|
parts;
|
|
314
316
|
delivered;
|
|
@@ -453,6 +455,11 @@ export async function runBot(deps) {
|
|
|
453
455
|
// Personal: every turn acts as the owner. Company: every turn acts as the bot.
|
|
454
456
|
const kind = effectiveBotKind(config);
|
|
455
457
|
const companies = effectiveBotCompanies(config);
|
|
458
|
+
// A single membership is unambiguous; personal and multi-company bots stay unbound.
|
|
459
|
+
const companySlug = config.companySlug ?? (kind === "company" && companies.length === 1 ? companies[0] : undefined);
|
|
460
|
+
const companyScopeKey = kind === "company" || companySlug !== undefined
|
|
461
|
+
? JSON.stringify({ kind, companies: [...companies].sort(), companySlug: companySlug ?? null })
|
|
462
|
+
: undefined;
|
|
456
463
|
const lockDeps = defaultPidLockDeps({
|
|
457
464
|
pid,
|
|
458
465
|
...(deps.isProcessAlive ? { isProcessAlive: deps.isProcessAlive } : {}),
|
|
@@ -521,7 +528,7 @@ export async function runBot(deps) {
|
|
|
521
528
|
// A restart is the owner's "try again" after signing in: start clean.
|
|
522
529
|
setStatus({ ...fallback(), pid, state: "starting", canDrainForPromotion: false, runtimeSignIn: null, cliVersion });
|
|
523
530
|
log("info", `starting ${config.name} on hq-cli ${cliVersion} (${config.runtime}, ${kind} bot${companies.length ? ` of ${companies.join(", ")}` : ""}) as ${config.agentUid} for ${config.ownerUid}`);
|
|
524
|
-
log("info", `${runtime.id} binary: ${describeRuntimeBinary(runtime, turnEnv(process.env, kind, config.agentUid,
|
|
531
|
+
log("info", `${runtime.id} binary: ${describeRuntimeBinary(runtime, turnEnv(process.env, kind, config.agentUid, companySlug), turnCwd)}`);
|
|
525
532
|
deps.cliUpdate?.startup(log);
|
|
526
533
|
try {
|
|
527
534
|
const record = await api.getAgent(config.agentUid);
|
|
@@ -541,6 +548,15 @@ export async function runBot(deps) {
|
|
|
541
548
|
await finish("failed", 0, `could not verify bot identity: ${err instanceof Error ? err.message : String(err)}`);
|
|
542
549
|
return { stop: async () => { }, done, status: () => null, idle: () => true };
|
|
543
550
|
}
|
|
551
|
+
try {
|
|
552
|
+
const scope = ensureBotCompanyScope(dir, companyScopeKey);
|
|
553
|
+
if (scope.reset)
|
|
554
|
+
log("info", "company scope changed or was unverified; archived prior conversations before starting fresh");
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
await finish("failed", 0, "could not isolate previous company conversations; settle any pending work under the previous settings and check workspace permissions before restarting");
|
|
558
|
+
return { stop: async () => { }, done, status: () => null, idle: () => true };
|
|
559
|
+
}
|
|
544
560
|
// ── System prompt ──────────────────────────────────────────────────────────
|
|
545
561
|
// A pack worker's bot gets the pack's "Pack setup" section (and the HQ setup
|
|
546
562
|
// takeover when the pack asks for it and setup is unfinished).
|
|
@@ -563,14 +579,14 @@ export async function runBot(deps) {
|
|
|
563
579
|
workerDir: config.workerDir,
|
|
564
580
|
workerSource: config.workerSource,
|
|
565
581
|
workerId: config.workerId,
|
|
566
|
-
companySlug
|
|
582
|
+
companySlug,
|
|
567
583
|
kind,
|
|
568
584
|
companies,
|
|
569
585
|
memoryDir: config.memoryDir,
|
|
570
586
|
sources: readPromptSources(config.hqRoot, config.workerDir, undefined, {
|
|
571
587
|
memoryDir: config.memoryDir,
|
|
572
588
|
workerSource: config.workerSource,
|
|
573
|
-
companySlug
|
|
589
|
+
companySlug,
|
|
574
590
|
}),
|
|
575
591
|
});
|
|
576
592
|
const systemPromptFile = path.join(dir, "system-prompt.md");
|
|
@@ -878,8 +894,18 @@ export async function runBot(deps) {
|
|
|
878
894
|
* A DM with no session (or whose resume id is gone) gets the recent thread
|
|
879
895
|
* prepended so a one-word reply still has the question it answers.
|
|
880
896
|
*/
|
|
897
|
+
const bindingNotices = new Set();
|
|
881
898
|
const executeTurn = async (prompt, sessionScope, tReceived, stream = {}, historyEventId) => {
|
|
882
|
-
const
|
|
899
|
+
const fresh = readBotConfig(dir);
|
|
900
|
+
if (!fresh)
|
|
901
|
+
throw new CompanyBindingError(`I couldn't read my company settings. Please restart ${config.name} after restoring its configuration; this work is still pending.`);
|
|
902
|
+
const live = fresh;
|
|
903
|
+
const liveCompanies = effectiveBotCompanies(live);
|
|
904
|
+
const liveCompanySlug = live.companySlug ?? (effectiveBotKind(live) === "company" && liveCompanies.length === 1 ? liveCompanies[0] : undefined);
|
|
905
|
+
if (liveCompanySlug !== companySlug || effectiveBotKind(live) !== kind ||
|
|
906
|
+
JSON.stringify([...liveCompanies].sort()) !== JSON.stringify([...companies].sort())) {
|
|
907
|
+
throw new CompanyBindingError(`My company settings changed. Please restart ${config.name} to load them before I continue; this work is still pending.`);
|
|
908
|
+
}
|
|
883
909
|
const remembered = readStatus()?.claudePermissionMode;
|
|
884
910
|
const actsPurelyForOwner = sessionScope === "dm" || sessionScope.startsWith("job:");
|
|
885
911
|
const composePrompt = async (session) => {
|
|
@@ -920,22 +946,22 @@ export async function runBot(deps) {
|
|
|
920
946
|
return next;
|
|
921
947
|
};
|
|
922
948
|
const runAttempt = async (session, turnPrompt) => {
|
|
923
|
-
// Company bind (worker-sourced bots): HQ's company hooks key on
|
|
949
|
+
// Company bind (worker-sourced and single-company bots): HQ's company hooks key on
|
|
924
950
|
// workspace/sessions/<sid>/meta.yaml, so the session id is fixed up front
|
|
925
951
|
// and the meta file written before every turn (idempotent; covers session
|
|
926
952
|
// resets). Claude gets the full hook set via its payload session id;
|
|
927
953
|
// Codex/Grok only see HQ_SESSION_ID.
|
|
928
954
|
const sid = session?.sessionId ?? randomUUID();
|
|
929
|
-
const env = turnEnv(process.env, kind, config.agentUid,
|
|
955
|
+
const env = turnEnv(process.env, kind, config.agentUid, companySlug);
|
|
930
956
|
let bind = { env };
|
|
931
|
-
if (
|
|
957
|
+
if (companySlug) {
|
|
932
958
|
try {
|
|
933
|
-
ensureBotSessionMeta(config.hqRoot, sid,
|
|
934
|
-
bind = { newSessionId: sid, env: { ...env, HQ_SESSION_ID: sid } };
|
|
959
|
+
ensureBotSessionMeta(config.hqRoot, sid, liveCompanySlug, undefined, now);
|
|
935
960
|
}
|
|
936
|
-
catch
|
|
937
|
-
|
|
961
|
+
catch {
|
|
962
|
+
throw new CompanyBindingError(`I couldn't bind this session to ${companySlug}. Please check the bot's workspace permissions and retry; this work is still pending.`);
|
|
938
963
|
}
|
|
964
|
+
bind = { newSessionId: sid, env: { ...env, HQ_SESSION_ID: sid } };
|
|
939
965
|
}
|
|
940
966
|
const tTurnStart = now().getTime();
|
|
941
967
|
// Model and thinking level are read fresh each turn, so a change from the
|
|
@@ -997,6 +1023,7 @@ export async function runBot(deps) {
|
|
|
997
1023
|
v: 1,
|
|
998
1024
|
runtime: config.runtime,
|
|
999
1025
|
sessionId: turn.sessionId,
|
|
1026
|
+
...(companyScopeKey !== undefined ? { companyScopeKey } : {}),
|
|
1000
1027
|
turns: (session?.turns ?? 0) + 1,
|
|
1001
1028
|
updatedAt: now().toISOString(),
|
|
1002
1029
|
}, sessionScope);
|
|
@@ -1143,7 +1170,18 @@ export async function runBot(deps) {
|
|
|
1143
1170
|
await poster.abandon();
|
|
1144
1171
|
const excerpt = err instanceof RuntimeError ? err.stderrExcerpt : err instanceof Error ? err.message : String(err);
|
|
1145
1172
|
const aborted = (err instanceof RuntimeError && err.aborted) || controller.signal.aborted;
|
|
1146
|
-
|
|
1173
|
+
if (err instanceof CompanyBindingError) {
|
|
1174
|
+
setStatus({ lastError: err.message });
|
|
1175
|
+
log("warn", err.message);
|
|
1176
|
+
if (!bindingNotices.has(args.sessionScope)) {
|
|
1177
|
+
try {
|
|
1178
|
+
await args.sendToPerson(err.message);
|
|
1179
|
+
bindingNotices.add(args.sessionScope);
|
|
1180
|
+
}
|
|
1181
|
+
catch { /* Keep the notice retryable if delivery failed. */ }
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
const started = !(err instanceof CompanyBindingError) && (!(err instanceof RuntimeError) || err.started);
|
|
1147
1185
|
return { outcome: { ok: false, started, aborted, excerpt, error: err }, poster };
|
|
1148
1186
|
}
|
|
1149
1187
|
finally {
|
|
@@ -1180,7 +1218,7 @@ export async function runBot(deps) {
|
|
|
1180
1218
|
}
|
|
1181
1219
|
await sendToTarget(target, notice.text, rootEventId, "human");
|
|
1182
1220
|
};
|
|
1183
|
-
const postOne = async (target, body, rootEventId, audience) => {
|
|
1221
|
+
const postOne = async (target, body, rootEventId, audience, omitFromHistory = false) => {
|
|
1184
1222
|
const root = rootEventId ? { rootEventId } : {};
|
|
1185
1223
|
if (target.kind === "dm") {
|
|
1186
1224
|
const posted = await api.sendDm({
|
|
@@ -1189,7 +1227,7 @@ export async function runBot(deps) {
|
|
|
1189
1227
|
...root,
|
|
1190
1228
|
...(audience ? { audience } : {}),
|
|
1191
1229
|
});
|
|
1192
|
-
if (audience !== "agent") {
|
|
1230
|
+
if (audience !== "agent" && !omitFromHistory) {
|
|
1193
1231
|
const eventId = posted && typeof posted === "object" && "eventId" in posted && typeof posted.eventId === "string"
|
|
1194
1232
|
? posted.eventId
|
|
1195
1233
|
: undefined;
|
|
@@ -1235,10 +1273,10 @@ export async function runBot(deps) {
|
|
|
1235
1273
|
* PartialPostError naming how many parts got through, so the caller can save
|
|
1236
1274
|
* exactly the remainder.
|
|
1237
1275
|
*/
|
|
1238
|
-
const sendParts = async (target, parts, from, rootEventId, onDelivered, audience) => {
|
|
1276
|
+
const sendParts = async (target, parts, from, rootEventId, onDelivered, audience, omitFromHistory = false) => {
|
|
1239
1277
|
for (let i = Math.max(0, from); i < parts.length; i += 1) {
|
|
1240
1278
|
try {
|
|
1241
|
-
await postOne(target, parts[i], rootEventId, audience);
|
|
1279
|
+
await postOne(target, parts[i], rootEventId, audience, omitFromHistory);
|
|
1242
1280
|
}
|
|
1243
1281
|
catch (err) {
|
|
1244
1282
|
throw new PartialPostError(parts, i, err);
|
|
@@ -1459,7 +1497,7 @@ export async function runBot(deps) {
|
|
|
1459
1497
|
const parts = marker.replyParts ?? splitBody(recoveryBody(marker));
|
|
1460
1498
|
const from = marker.replyParts ? Math.min(Math.max(0, marker.replyPartsDelivered ?? 0), parts.length) : 0;
|
|
1461
1499
|
try {
|
|
1462
|
-
await sendParts(marker.target, parts, from, marker.rootEventId, (count) => patchInflight(dir, marker.messageId, { replyParts: parts, replyPartsDelivered: count }));
|
|
1500
|
+
await sendParts(marker.target, parts, from, marker.rootEventId, (count) => patchInflight(dir, marker.messageId, { replyParts: parts, replyPartsDelivered: count }), undefined, marker.omitFromHistory);
|
|
1463
1501
|
}
|
|
1464
1502
|
catch (err) {
|
|
1465
1503
|
const reason = err instanceof Error ? err.message : String(err);
|
|
@@ -1471,12 +1509,12 @@ export async function runBot(deps) {
|
|
|
1471
1509
|
// no part of this run reached the chat yet.
|
|
1472
1510
|
const headerOut = (marker.progressPosted ?? 0) > 0 || (err instanceof PartialPostError ? err.delivered : from) > 0;
|
|
1473
1511
|
const text = marker.jobHeader && !headerOut ? `${marker.jobHeader}${UNDELIVERABLE_JOB_REPORT_TEXT}` : UNDELIVERABLE_JOB_REPORT_TEXT;
|
|
1474
|
-
await postOne(marker.target, text, marker.rootEventId).catch(() => undefined);
|
|
1512
|
+
await postOne(marker.target, text, marker.rootEventId, undefined, marker.omitFromHistory).catch(() => undefined);
|
|
1475
1513
|
// Giving up on DELIVERY is not giving up on the RUN: report it.
|
|
1476
1514
|
await reportRecoveredJobRun(marker);
|
|
1477
1515
|
}
|
|
1478
1516
|
else {
|
|
1479
|
-
await postOne(marker.target, UNDELIVERABLE_REPLY_TEXT, marker.rootEventId).catch(() => undefined);
|
|
1517
|
+
await postOne(marker.target, UNDELIVERABLE_REPLY_TEXT, marker.rootEventId, undefined, marker.omitFromHistory).catch(() => undefined);
|
|
1480
1518
|
}
|
|
1481
1519
|
await closeMarker(marker, isKickoff);
|
|
1482
1520
|
return true;
|
|
@@ -1592,6 +1630,10 @@ export async function runBot(deps) {
|
|
|
1592
1630
|
workingNotice: false,
|
|
1593
1631
|
});
|
|
1594
1632
|
await clearThinking();
|
|
1633
|
+
if (!outcome.ok && outcome.error instanceof CompanyBindingError) {
|
|
1634
|
+
clearInflight(dir, KICKOFF_MESSAGE_ID);
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1595
1637
|
if (outcome.ok) {
|
|
1596
1638
|
// The kickoff never runs silenceable, so outcome.silent is never set here.
|
|
1597
1639
|
if (poster.postedCount === 0) {
|
|
@@ -1758,6 +1800,11 @@ export async function runBot(deps) {
|
|
|
1758
1800
|
workingNotice: false,
|
|
1759
1801
|
...(quietJob ? { suppressOutput: true } : {}),
|
|
1760
1802
|
});
|
|
1803
|
+
if (!outcome.ok && outcome.error instanceof CompanyBindingError) {
|
|
1804
|
+
clearInflight(dir, id);
|
|
1805
|
+
attempts.delete(id);
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1761
1808
|
if (outcome.ok) {
|
|
1762
1809
|
if (!quietJob && !reportDelivered(outcome.reply)) {
|
|
1763
1810
|
// The work finished but the REPORT did not fully reach the DM — even if
|
|
@@ -2076,6 +2123,11 @@ export async function runBot(deps) {
|
|
|
2076
2123
|
});
|
|
2077
2124
|
// The model saw the note once its turn ran. A turn that never started, or
|
|
2078
2125
|
// one held for a coding-tool sign-in, runs again and keeps the note for it.
|
|
2126
|
+
if (!outcome.ok && outcome.error instanceof CompanyBindingError) {
|
|
2127
|
+
clearInflight(dir, id);
|
|
2128
|
+
attempts.delete(id);
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2079
2131
|
if (setupNote && !toolOffer && (outcome.ok || (outcome.started && !isSignInFailure(outcome.excerpt)))) {
|
|
2080
2132
|
try {
|
|
2081
2133
|
patchSetupIntroState(dir, { notePending: false });
|
|
@@ -10,10 +10,24 @@ export interface BotSessionFile {
|
|
|
10
10
|
v: 1;
|
|
11
11
|
runtime: BotRuntimeId;
|
|
12
12
|
sessionId: string;
|
|
13
|
+
/** Company/membership fingerprint under which this transcript was created. */
|
|
14
|
+
companyScopeKey?: string;
|
|
13
15
|
turns: number;
|
|
14
16
|
updatedAt: string;
|
|
15
17
|
}
|
|
16
18
|
export declare function readBotSession(dir: string, runtime: BotRuntimeId, scope?: BotSessionScope): BotSessionFile | null;
|
|
17
19
|
export declare function writeBotSession(dir: string, session: BotSessionFile, scope?: BotSessionScope): void;
|
|
18
20
|
export declare function clearBotSession(dir: string, scope?: BotSessionScope): void;
|
|
21
|
+
/**
|
|
22
|
+
* Run once at startup, before reading or appending conversation state. A scope
|
|
23
|
+
* change must never resume a prior company's model session or replay its DM
|
|
24
|
+
* history. Preserve those files privately for recovery, then record the new
|
|
25
|
+
* scope. Filesystem failures propagate so callers cannot start a model turn.
|
|
26
|
+
* Legacy personal bots retain their existing history; company bots require
|
|
27
|
+
* positive scope evidence and archive legacy, unproven conversations once.
|
|
28
|
+
*/
|
|
29
|
+
export declare function ensureBotCompanyScope(dir: string, companyScopeKey: string | undefined): {
|
|
30
|
+
reset: boolean;
|
|
31
|
+
archiveDir?: string;
|
|
32
|
+
};
|
|
19
33
|
//# sourceMappingURL=session.d.ts.map
|
package/dist/lib/bot/session.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import { omitInflightRecoveryHistory, readInflightTurns } from "./inflight.js";
|
|
9
11
|
import { botSessionPath } from "./paths.js";
|
|
10
12
|
export function readBotSession(dir, runtime, scope = "dm") {
|
|
11
13
|
const p = botSessionPath(dir, scope);
|
|
@@ -36,4 +38,72 @@ export function clearBotSession(dir, scope = "dm") {
|
|
|
36
38
|
/* already gone */
|
|
37
39
|
}
|
|
38
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Run once at startup, before reading or appending conversation state. A scope
|
|
43
|
+
* change must never resume a prior company's model session or replay its DM
|
|
44
|
+
* history. Preserve those files privately for recovery, then record the new
|
|
45
|
+
* scope. Filesystem failures propagate so callers cannot start a model turn.
|
|
46
|
+
* Legacy personal bots retain their existing history; company bots require
|
|
47
|
+
* positive scope evidence and archive legacy, unproven conversations once.
|
|
48
|
+
*/
|
|
49
|
+
export function ensureBotCompanyScope(dir, companyScopeKey) {
|
|
50
|
+
const marker = path.join(dir, "company-scope.json");
|
|
51
|
+
const hasMarker = fs.existsSync(marker);
|
|
52
|
+
if (!hasMarker && companyScopeKey === undefined)
|
|
53
|
+
return { reset: false };
|
|
54
|
+
const expected = companyScopeKey ?? null;
|
|
55
|
+
let provenPreviousScope = false;
|
|
56
|
+
if (hasMarker) {
|
|
57
|
+
// Read errors fail closed. Invalid JSON is unproven provenance and resets.
|
|
58
|
+
const body = fs.readFileSync(marker, "utf8");
|
|
59
|
+
let parsed;
|
|
60
|
+
try {
|
|
61
|
+
parsed = JSON.parse(body);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
parsed = null;
|
|
65
|
+
}
|
|
66
|
+
provenPreviousScope = Boolean(parsed && typeof parsed === "object" &&
|
|
67
|
+
parsed.v === 1 &&
|
|
68
|
+
(typeof parsed.companyScopeKey === "string" ||
|
|
69
|
+
parsed.companyScopeKey === null));
|
|
70
|
+
if (parsed && typeof parsed === "object" &&
|
|
71
|
+
parsed.v === 1 &&
|
|
72
|
+
parsed.companyScopeKey === expected) {
|
|
73
|
+
return { reset: false };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// A known company switch must settle recovery under its previous scope.
|
|
77
|
+
// Legacy bots have no scope evidence: isolate their model context below,
|
|
78
|
+
// but leave delivery/job markers intact for the existing recovery path.
|
|
79
|
+
// Recovery only settles saved work; it never resumes the old model session.
|
|
80
|
+
// Match recovery's tolerant parser so corrupt markers do not brick startup.
|
|
81
|
+
const pending = readInflightTurns(dir, { strictIo: true });
|
|
82
|
+
if (provenPreviousScope && pending.length > 0) {
|
|
83
|
+
throw new Error("Bot company settings changed with pending work. Restore the previous company settings and settle pending work before switching companies.");
|
|
84
|
+
}
|
|
85
|
+
if (!provenPreviousScope)
|
|
86
|
+
omitInflightRecoveryHistory(dir, pending);
|
|
87
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
88
|
+
const names = fs.readdirSync(dir).filter((name) => /^(?:session|dm-history)(?:-[A-Za-z0-9_-]+)?\.json$/.test(name));
|
|
89
|
+
let archiveDir;
|
|
90
|
+
if (names.length) {
|
|
91
|
+
const archiveRoot = path.join(dir, "company-scope-archives");
|
|
92
|
+
fs.mkdirSync(archiveRoot, { recursive: true, mode: 0o700 });
|
|
93
|
+
archiveDir = path.join(archiveRoot, randomUUID());
|
|
94
|
+
fs.mkdirSync(archiveDir, { mode: 0o700 });
|
|
95
|
+
for (const name of names)
|
|
96
|
+
fs.renameSync(path.join(dir, name), path.join(archiveDir, name));
|
|
97
|
+
}
|
|
98
|
+
// Commit the new provenance only after every old conversation was isolated.
|
|
99
|
+
const tmp = path.join(dir, `.company-scope.${randomUUID()}.tmp`);
|
|
100
|
+
try {
|
|
101
|
+
fs.writeFileSync(tmp, `${JSON.stringify({ v: 1, companyScopeKey: expected })}\n`, { mode: 0o600 });
|
|
102
|
+
fs.renameSync(tmp, marker);
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
fs.rmSync(tmp, { force: true });
|
|
106
|
+
}
|
|
107
|
+
return { reset: names.length > 0, ...(archiveDir ? { archiveDir } : {}) };
|
|
108
|
+
}
|
|
39
109
|
//# sourceMappingURL=session.js.map
|
|
@@ -287,6 +287,12 @@ export declare function tokenFingerprint(token: string): string;
|
|
|
287
287
|
export declare function readClaudeRefreshLast(root: string, home: string): ClaudeRefreshLastEntry | null;
|
|
288
288
|
/** @deprecated prefer {@link revokedMarkerForCurrentCredential}; this is the pre-fingerprint shim. */
|
|
289
289
|
export declare function isAccountRevokedPersisted(root: string, accountId: string): boolean;
|
|
290
|
+
/**
|
|
291
|
+
* True when the capacity collector has recorded this refresh token as
|
|
292
|
+
* revoked (by token fingerprint, under any account). Lanes use it to tell a
|
|
293
|
+
* dead sign-in from an access token that Claude Code will renew at startup.
|
|
294
|
+
*/
|
|
295
|
+
export declare function isRefreshTokenRevoked(root: string, refreshToken: string): boolean;
|
|
290
296
|
/**
|
|
291
297
|
* Compare the persisted revoked marker (if any) against the refresh token the
|
|
292
298
|
* caller is about to use.
|
|
@@ -1556,6 +1556,18 @@ export function isAccountRevokedPersisted(root, accountId) {
|
|
|
1556
1556
|
const file = readRefreshLast(root);
|
|
1557
1557
|
return Object.prototype.hasOwnProperty.call(file.revokedAccounts, accountId);
|
|
1558
1558
|
}
|
|
1559
|
+
/**
|
|
1560
|
+
* True when the capacity collector has recorded this refresh token as
|
|
1561
|
+
* revoked (by token fingerprint, under any account). Lanes use it to tell a
|
|
1562
|
+
* dead sign-in from an access token that Claude Code will renew at startup.
|
|
1563
|
+
*/
|
|
1564
|
+
export function isRefreshTokenRevoked(root, refreshToken) {
|
|
1565
|
+
const file = readRefreshLast(root);
|
|
1566
|
+
const fp = tokenFingerprint(refreshToken);
|
|
1567
|
+
if (file.revokedTokens[fp] !== undefined)
|
|
1568
|
+
return true;
|
|
1569
|
+
return Object.values(file.revokedAccounts).some((entry) => entry.fingerprint === fp);
|
|
1570
|
+
}
|
|
1559
1571
|
/**
|
|
1560
1572
|
* Compare the persisted revoked marker (if any) against the refresh token the
|
|
1561
1573
|
* caller is about to use.
|
|
@@ -20,6 +20,12 @@ export interface LaneLoginInput {
|
|
|
20
20
|
* login confirms sign-in through this probe. Defaults to the lanes probe.
|
|
21
21
|
*/
|
|
22
22
|
probeProvider?: Ctx["probeProvider"];
|
|
23
|
+
/**
|
|
24
|
+
* Claude access-token expiry check for the post-login status. Defaults to
|
|
25
|
+
* the real Keychain/file read when the default probe is used; a caller that
|
|
26
|
+
* injects a probe passes its own reader or gets no expiry check.
|
|
27
|
+
*/
|
|
28
|
+
readClaudeCredential?: Ctx["readClaudeCredential"];
|
|
23
29
|
}
|
|
24
30
|
export interface LaneLoginResult {
|
|
25
31
|
id: string;
|
|
@@ -13,6 +13,7 @@ import { createLaneAccountsHandlers } from "../../commands/lanes-accounts.js";
|
|
|
13
13
|
import { effectiveAccountHome } from "../lanes/account-home.js";
|
|
14
14
|
import { canonicalizeHome, DEFAULT_ACCOUNT_HOMES, loadLanesConfig } from "../lanes/config.js";
|
|
15
15
|
import { resolveLanesHqRoot } from "../lanes/hq-root.js";
|
|
16
|
+
import { readLaneClaudeCredential } from "../lanes/claude-credential.js";
|
|
16
17
|
import { defaultProbe } from "../lanes/models.js";
|
|
17
18
|
import { parseLaneProvider } from "../lanes/types.js";
|
|
18
19
|
const ACCOUNT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
@@ -108,6 +109,10 @@ function loginCtx(input, hqRoot) {
|
|
|
108
109
|
existsSync: (target) => fs.existsSync(target),
|
|
109
110
|
spawnLogin: defaultHomeEnv(input.spawnLogin ?? defaultSpawnLogin, input.homedir ?? os.homedir),
|
|
110
111
|
probeProvider: defaultHomeProbe(input.probeProvider ?? defaultProbe, input.homedir ?? os.homedir),
|
|
112
|
+
// Always a real field: the Proxy below turns a missing one into a throwing
|
|
113
|
+
// function, which accountStatus would call and report as a failed check.
|
|
114
|
+
readClaudeCredential: input.readClaudeCredential ??
|
|
115
|
+
(input.probeProvider === undefined ? readLaneClaudeCredential : undefined),
|
|
111
116
|
};
|
|
112
117
|
return new Proxy(fields, {
|
|
113
118
|
get(target, prop) {
|
|
@@ -42,6 +42,8 @@ export function createWorktree(args = [], options = {}) {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
const die = (message) => { line(stderr, `ERROR: ${message}`); return 1; };
|
|
45
|
+
// Accept "--base origin/<branch>" as well as "--base <branch>"; fetch and ref lookups add origin/ themselves.
|
|
46
|
+
base = base.replace(/^origin\//, "");
|
|
45
47
|
if (!name)
|
|
46
48
|
return die("--name is required (kebab-case slug, e.g. 'fix-login-bug')");
|
|
47
49
|
if (name.includes("/") || name.includes(" "))
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classifies why a supervised long-running unit stopped, so the supervisor can
|
|
3
|
+
* tell a failure that a restart will not fix (permanent) from one that may
|
|
4
|
+
* clear on its own (transient).
|
|
5
|
+
*
|
|
6
|
+
* Permanent causes stop the restart loop and hold the unit until a manual
|
|
7
|
+
* `hq daemon restart <unit>`, a command change, or a CLI update. Anything not
|
|
8
|
+
* recognised is transient, so an unknown failure keeps the bounded retry.
|
|
9
|
+
*
|
|
10
|
+
* Output is never substring-matched. Only npm's own diagnostic line
|
|
11
|
+
* (`npm error code <CODE>` from npm 10+, `npm ERR! code <CODE>` from older
|
|
12
|
+
* npm) counts, and only for a code in PERMANENT_NPM_CODES. The Desktop app
|
|
13
|
+
* uses the same prefixes and list.
|
|
14
|
+
*/
|
|
15
|
+
export type PermanentExitCause = "missing-binary" | "not-executable" | "package-resolution" | "engine-mismatch" | "permission-denied" | "bad-config" | "auth-refused";
|
|
16
|
+
export type ExitCause = {
|
|
17
|
+
kind: "transient";
|
|
18
|
+
} | {
|
|
19
|
+
kind: "permanent";
|
|
20
|
+
cause: PermanentExitCause;
|
|
21
|
+
detail: string;
|
|
22
|
+
};
|
|
23
|
+
export interface ExitEvidence {
|
|
24
|
+
code: number | null;
|
|
25
|
+
signal: string | null;
|
|
26
|
+
/** The launch error, when the process could not be started. */
|
|
27
|
+
spawnError?: unknown;
|
|
28
|
+
/** Output the process wrote during this run only (not earlier runs). */
|
|
29
|
+
outputTail?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* npm error codes that a retry cannot fix without a change by a person.
|
|
33
|
+
* Network and cache codes (ECONNRESET, ETIMEDOUT, ENOTFOUND, EAI_AGAIN,
|
|
34
|
+
* ECONNREFUSED, EINTEGRITY, EPERM, EBUSY, ENOSPC) stay transient on purpose.
|
|
35
|
+
*/
|
|
36
|
+
export declare const PERMANENT_NPM_CODES: Readonly<Record<string, PermanentExitCause>>;
|
|
37
|
+
/** The first npm error code line naming a permanent code, as `{ code, line }`. */
|
|
38
|
+
export declare function permanentNpmCode(output: string): {
|
|
39
|
+
code: string;
|
|
40
|
+
cause: PermanentExitCause;
|
|
41
|
+
} | undefined;
|
|
42
|
+
export declare function classifyExit(evidence: ExitEvidence): ExitCause;
|
|
43
|
+
//# sourceMappingURL=exit-cause.d.ts.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classifies why a supervised long-running unit stopped, so the supervisor can
|
|
3
|
+
* tell a failure that a restart will not fix (permanent) from one that may
|
|
4
|
+
* clear on its own (transient).
|
|
5
|
+
*
|
|
6
|
+
* Permanent causes stop the restart loop and hold the unit until a manual
|
|
7
|
+
* `hq daemon restart <unit>`, a command change, or a CLI update. Anything not
|
|
8
|
+
* recognised is transient, so an unknown failure keeps the bounded retry.
|
|
9
|
+
*
|
|
10
|
+
* Output is never substring-matched. Only npm's own diagnostic line
|
|
11
|
+
* (`npm error code <CODE>` from npm 10+, `npm ERR! code <CODE>` from older
|
|
12
|
+
* npm) counts, and only for a code in PERMANENT_NPM_CODES. The Desktop app
|
|
13
|
+
* uses the same prefixes and list.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* npm error codes that a retry cannot fix without a change by a person.
|
|
17
|
+
* Network and cache codes (ECONNRESET, ETIMEDOUT, ENOTFOUND, EAI_AGAIN,
|
|
18
|
+
* ECONNREFUSED, EINTEGRITY, EPERM, EBUSY, ENOSPC) stay transient on purpose.
|
|
19
|
+
*/
|
|
20
|
+
export const PERMANENT_NPM_CODES = {
|
|
21
|
+
ETARGET: "package-resolution", // no version matches the requested range or tag
|
|
22
|
+
E404: "package-resolution", // package or version not in the registry
|
|
23
|
+
EINVALIDPACKAGENAME: "package-resolution",
|
|
24
|
+
EUNSUPPORTEDPROTOCOL: "package-resolution", // unsupported spec, e.g. a bad URL scheme
|
|
25
|
+
EBADENGINE: "engine-mismatch", // engine-strict and node does not satisfy engines
|
|
26
|
+
EACCES: "permission-denied", // cache or prefix not writable by this user
|
|
27
|
+
E401: "auth-refused",
|
|
28
|
+
E403: "auth-refused",
|
|
29
|
+
ENEEDAUTH: "auth-refused",
|
|
30
|
+
};
|
|
31
|
+
const NPM_CODE_PREFIXES = ["npm error code ", "npm ERR! code "];
|
|
32
|
+
/** sysexits.h EX_NOPERM / EX_CONFIG, and the shell's not-executable / not-found codes. */
|
|
33
|
+
const EXIT_CODE_CAUSES = {
|
|
34
|
+
77: { cause: "auth-refused", detail: "exit code 77 (permission or auth refused)" },
|
|
35
|
+
78: { cause: "bad-config", detail: "exit code 78 (configuration error)" },
|
|
36
|
+
126: { cause: "not-executable", detail: "exit code 126 (command not executable)" },
|
|
37
|
+
127: { cause: "missing-binary", detail: "exit code 127 (command not found)" },
|
|
38
|
+
};
|
|
39
|
+
/** Launch errno codes, read from the error's `code` field only. */
|
|
40
|
+
const SPAWN_ERROR_CAUSES = {
|
|
41
|
+
ENOENT: "missing-binary",
|
|
42
|
+
EACCES: "not-executable",
|
|
43
|
+
ENOEXEC: "not-executable",
|
|
44
|
+
};
|
|
45
|
+
function errorCode(err) {
|
|
46
|
+
if (err && typeof err === "object" && "code" in err && typeof err.code === "string") {
|
|
47
|
+
return err.code;
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
/** The first npm error code line naming a permanent code, as `{ code, line }`. */
|
|
52
|
+
export function permanentNpmCode(output) {
|
|
53
|
+
for (const raw of output.split(/\r?\n/)) {
|
|
54
|
+
const line = raw.trim();
|
|
55
|
+
for (const prefix of NPM_CODE_PREFIXES) {
|
|
56
|
+
if (!line.startsWith(prefix))
|
|
57
|
+
continue;
|
|
58
|
+
const code = line.slice(prefix.length).split(/\s+/)[0] ?? "";
|
|
59
|
+
const cause = Object.prototype.hasOwnProperty.call(PERMANENT_NPM_CODES, code) ? PERMANENT_NPM_CODES[code] : undefined;
|
|
60
|
+
if (cause)
|
|
61
|
+
return { code, cause };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
export function classifyExit(evidence) {
|
|
67
|
+
if (evidence.spawnError !== undefined) {
|
|
68
|
+
const code = errorCode(evidence.spawnError);
|
|
69
|
+
const cause = code ? SPAWN_ERROR_CAUSES[code] : undefined;
|
|
70
|
+
if (cause)
|
|
71
|
+
return { kind: "permanent", cause, detail: `could not start (${code})` };
|
|
72
|
+
}
|
|
73
|
+
// A process killed by a signal says nothing about its own configuration.
|
|
74
|
+
if (evidence.signal)
|
|
75
|
+
return { kind: "transient" };
|
|
76
|
+
if (evidence.code === 0)
|
|
77
|
+
return { kind: "transient" };
|
|
78
|
+
if (evidence.code !== null) {
|
|
79
|
+
const byCode = EXIT_CODE_CAUSES[evidence.code];
|
|
80
|
+
if (byCode)
|
|
81
|
+
return { kind: "permanent", ...byCode };
|
|
82
|
+
}
|
|
83
|
+
const npm = evidence.outputTail ? permanentNpmCode(evidence.outputTail) : undefined;
|
|
84
|
+
if (npm)
|
|
85
|
+
return { kind: "permanent", cause: npm.cause, detail: `npm error code ${npm.code}` };
|
|
86
|
+
return { kind: "transient" };
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=exit-cause.js.map
|
|
@@ -9,6 +9,8 @@ import { type MachineType } from "./machine-type.js";
|
|
|
9
9
|
import { type DaemonPaths } from "./paths.js";
|
|
10
10
|
/** A live daemon runs `unitId` (or held it after it was stopped from outside). */
|
|
11
11
|
export declare function daemonSupervises(paths: DaemonPaths, unitId: string, alive?: (pid: number) => boolean): boolean;
|
|
12
|
+
/** Current supervisor hold, including its explicit recovery command; never infer one from stale state. */
|
|
13
|
+
export declare function daemonHoldReason(paths: DaemonPaths, unitId: string, alive?: (pid: number) => boolean): string | null;
|
|
12
14
|
/**
|
|
13
15
|
* A live daemon runs the service `unitId` belongs to (`bots` for
|
|
14
16
|
* `bots:<name>`), so it will run this instance once its plan includes it. For
|
|
@@ -16,7 +16,7 @@ import { detectMachineType } from "./machine-type.js";
|
|
|
16
16
|
import { daemonPaths } from "./paths.js";
|
|
17
17
|
import { writeControlRequest } from "./requests.js";
|
|
18
18
|
/** Statuses where the daemon itself owns the unit's process. */
|
|
19
|
-
const OWNED = new Set(["running", "backoff", "exited", "idle", "stopped"]);
|
|
19
|
+
const OWNED = new Set(["running", "backoff", "exited", "idle", "stopped", "held"]);
|
|
20
20
|
const RECENT_HOSTED_DAEMON_MS = 5 * 60_000;
|
|
21
21
|
/** The updater's postinstall may run in the short gap before the desktop app relaunches its daemon. */
|
|
22
22
|
function recentlyHostedDaemon(paths) {
|
|
@@ -44,6 +44,11 @@ export function daemonSupervises(paths, unitId, alive = (pid) => isDaemonProcess
|
|
|
44
44
|
const unit = liveState(paths, alive)?.units?.find((u) => u.id === unitId);
|
|
45
45
|
return unit !== undefined && OWNED.has(unit.status);
|
|
46
46
|
}
|
|
47
|
+
/** Current supervisor hold, including its explicit recovery command; never infer one from stale state. */
|
|
48
|
+
export function daemonHoldReason(paths, unitId, alive = (pid) => isDaemonProcess(pid)) {
|
|
49
|
+
const unit = liveState(paths, alive)?.units?.find((u) => u.id === unitId);
|
|
50
|
+
return unit?.status === "held" ? unit.reason ?? `held; \`hq daemon restart ${unitId}\` retries` : null;
|
|
51
|
+
}
|
|
47
52
|
/**
|
|
48
53
|
* A live daemon runs the service `unitId` belongs to (`bots` for
|
|
49
54
|
* `bots:<name>`), so it will run this instance once its plan includes it. For
|
|
@@ -43,14 +43,31 @@ function updateRequestIsActive(requestPath) {
|
|
|
43
43
|
const pid = request.pid;
|
|
44
44
|
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0)
|
|
45
45
|
throw new Error("The HQ CLI update request is invalid");
|
|
46
|
+
// Same staleness rule as src/utils/package-use-lease.ts: a request written
|
|
47
|
+
// before the current boot (pid may be reused), or whose pid is gone (ESRCH)
|
|
48
|
+
// or owned by another user (EPERM), is not a live updater.
|
|
49
|
+
let stale = false;
|
|
46
50
|
try {
|
|
47
|
-
|
|
51
|
+
stale = fs.statSync(requestPath).mtimeMs < Date.now() - os.uptime() * 1000 - 5_000;
|
|
48
52
|
}
|
|
49
53
|
catch (error) {
|
|
50
|
-
if (error.code === "
|
|
51
|
-
fs.rmSync(requestPath, { force: true });
|
|
54
|
+
if (error.code === "ENOENT")
|
|
52
55
|
return false;
|
|
56
|
+
throw new Error("Unable to read the HQ CLI update request");
|
|
57
|
+
}
|
|
58
|
+
if (!stale) {
|
|
59
|
+
try {
|
|
60
|
+
process.kill(pid, 0);
|
|
53
61
|
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
const code = error.code;
|
|
64
|
+
if (code === "ESRCH" || code === "EPERM")
|
|
65
|
+
stale = true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (stale) {
|
|
69
|
+
fs.rmSync(requestPath, { force: true });
|
|
70
|
+
return false;
|
|
54
71
|
}
|
|
55
72
|
return true;
|
|
56
73
|
}
|
|
@@ -8,6 +8,8 @@ export interface DaemonPaths {
|
|
|
8
8
|
configPath: string;
|
|
9
9
|
remoteControlPoolsPath: string;
|
|
10
10
|
statePath: string;
|
|
11
|
+
/** Long-running units the supervisor holds down, with why (see unit-holds.ts). */
|
|
12
|
+
holdsPath: string;
|
|
11
13
|
lockPath: string;
|
|
12
14
|
logsDir: string;
|
|
13
15
|
requestsDir: string;
|
package/dist/lib/daemon/paths.js
CHANGED
|
@@ -14,6 +14,7 @@ export function daemonPaths(home = os.homedir(), env = process.env) {
|
|
|
14
14
|
configPath: path.join(root, "config.json"),
|
|
15
15
|
remoteControlPoolsPath: path.join(root, "remote-control-pools.json"),
|
|
16
16
|
statePath: path.join(root, "state.json"),
|
|
17
|
+
holdsPath: path.join(root, "holds.json"),
|
|
17
18
|
lockPath: path.join(root, "daemon.pid"),
|
|
18
19
|
logsDir,
|
|
19
20
|
requestsDir: path.join(root, "requests"),
|