@indigoai-us/hq-cli 5.114.0 → 5.115.1
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 +45 -0
- package/dist/command-catalog.generated.d.ts +28 -0
- package/dist/command-catalog.generated.js +37 -0
- package/dist/commands/bot-companies.d.ts +40 -0
- package/dist/commands/bot-companies.js +64 -0
- package/dist/commands/bot.d.ts +33 -1
- package/dist/commands/bot.js +181 -8
- package/dist/commands/onboard-bot-membership.d.ts +32 -0
- package/dist/commands/onboard-bot-membership.js +40 -0
- package/dist/commands/onboard.js +10 -0
- package/dist/lib/bot/api.d.ts +11 -0
- package/dist/lib/bot/api.js +8 -1
- package/dist/lib/bot/config.d.ts +35 -0
- package/dist/lib/bot/config.js +69 -2
- package/dist/lib/bot/prompt.d.ts +13 -1
- package/dist/lib/bot/prompt.js +37 -12
- package/dist/lib/bot/run.d.ts +30 -3
- package/dist/lib/bot/run.js +137 -14
- package/dist/lib/bot/runtime-sign-in.d.ts +24 -0
- package/dist/lib/bot/runtime-sign-in.js +31 -0
- package/dist/lib/bot/status.d.ts +6 -0
- package/package.json +1 -1
package/dist/lib/bot/run.js
CHANGED
|
@@ -26,6 +26,11 @@
|
|
|
26
26
|
* started (error, crash, owner stop, restart) posts one "did not finish"
|
|
27
27
|
* message and is NOT run again — it may already have done outward things.
|
|
28
28
|
* inflight.json marks the turn in progress so a restart can say so.
|
|
29
|
+
* - a turn that fails because the coding tool's sign-in no longer works
|
|
30
|
+
* (runtime-sign-in.ts) is not a failure of the message: the bot says once
|
|
31
|
+
* per conversation that it needs a sign-in, keeps the message un-acked,
|
|
32
|
+
* holds new turns, and tries again every SIGN_IN_RETRY_MS (and at once
|
|
33
|
+
* after a restart). A turn that works clears it.
|
|
29
34
|
* - failures before the model starts (missing binary) retry with backoff;
|
|
30
35
|
* 5 model failures in 10 minutes → state `failed` and a clean exit (launchd
|
|
31
36
|
* does not respawn a clean exit). Owner stops never count.
|
|
@@ -39,7 +44,7 @@ import * as fs from "node:fs";
|
|
|
39
44
|
import * as path from "node:path";
|
|
40
45
|
import { fullJitterDelayMs } from "../mesh/live/backoff.js";
|
|
41
46
|
import { acquirePidLock, defaultPidLockDeps, releasePidLock } from "../mesh/live/daemon/pid-lock.js";
|
|
42
|
-
import { patchBotConfig, readBotConfig } from "./config.js";
|
|
47
|
+
import { effectiveBotCompanies, effectiveBotKind, patchBotConfig, readBotConfig, SETUP_BOT_WORKER_ID } from "./config.js";
|
|
43
48
|
import { ensureBotSessionMeta } from "./company-bind.js";
|
|
44
49
|
import { readBotCredsIdentity } from "./creds.js";
|
|
45
50
|
import { isProcessed, markProcessed, readInboxState } from "./inbox-state.js";
|
|
@@ -50,6 +55,8 @@ import { buildSystemPrompt, introDmText, nonOwnerRefusalText, readPromptSources
|
|
|
50
55
|
import { FALLBACK_CLAUDE_PERMISSION_MODE, isClaudeBypassDisabled } from "./runtime/claude.js";
|
|
51
56
|
import { runRuntimeTurn, RuntimeError } from "./runtime/index.js";
|
|
52
57
|
import { isDmItem, resolveReplyTarget } from "./room-policy.js";
|
|
58
|
+
import { isRuntimeSignInFailure, SIGN_IN_RETRY_MS, signInNeededText } from "./runtime-sign-in.js";
|
|
59
|
+
import { HQ_BOT_AGENT_UID_ENV } from "../../commands/onboard-bot-membership.js";
|
|
53
60
|
import { clearBotSession, readBotSession, writeBotSession } from "./session.js";
|
|
54
61
|
import { defaultBotStatus, patchBotStatus, readBotStatus } from "./status.js";
|
|
55
62
|
export const INBOX_POLL_INTERVAL_MS = 2_000;
|
|
@@ -64,8 +71,38 @@ export const RECENT_MESSAGE_MAX_CHARS = 400;
|
|
|
64
71
|
/** Model turns a bot runs at once (each on a different conversation). */
|
|
65
72
|
export const MAX_PARALLEL_TURNS = 2;
|
|
66
73
|
export const KICKOFF_MESSAGE_ID = "kickoff";
|
|
74
|
+
/**
|
|
75
|
+
* The environment a model turn's `hq` commands run in.
|
|
76
|
+
*
|
|
77
|
+
* The bot process carries its own machine identity in HQ_MACHINE_CREDS_FILE /
|
|
78
|
+
* HQ_MACHINE_TOKEN_STATE_DIR so its inbox, heartbeat and posts are the bot's.
|
|
79
|
+
* What a turn inherits depends on the bot's kind:
|
|
80
|
+
* - A PERSONAL bot acts as its owner on every turn (DMs, the kickoff, rooms):
|
|
81
|
+
* everything the owner asks for — a company, a bot, an invite, a share —
|
|
82
|
+
* belongs to the owner's account, and the cloud rightly refuses a bot that
|
|
83
|
+
* tries to own it ("No person entity found"). Stripping the marker makes
|
|
84
|
+
* every `hq` command in the turn use the owner's sign-in, the same one the
|
|
85
|
+
* desktop app shares, so no command needs a special case. The bot's id
|
|
86
|
+
* (never a credential) rides along in HQ_BOT_AGENT_UID so what the owner
|
|
87
|
+
* creates in the turn — a company — can add the bot as a member afterwards.
|
|
88
|
+
* - A COMPANY bot acts as itself on every turn, like a cloud agent: the
|
|
89
|
+
* machine identity stays, and the cloud evaluates it through its own
|
|
90
|
+
* company memberships. It never acts as the owner, in DMs or in rooms.
|
|
91
|
+
* Pure so the rule is unit-testable.
|
|
92
|
+
*/
|
|
93
|
+
export function turnEnv(base, kind, agentUid) {
|
|
94
|
+
const env = { ...base };
|
|
95
|
+
delete env[HQ_BOT_AGENT_UID_ENV];
|
|
96
|
+
if (kind === "personal") {
|
|
97
|
+
delete env.HQ_MACHINE_CREDS_FILE;
|
|
98
|
+
delete env.HQ_MACHINE_TOKEN_STATE_DIR;
|
|
99
|
+
if (agentUid)
|
|
100
|
+
env[HQ_BOT_AGENT_UID_ENV] = agentUid;
|
|
101
|
+
}
|
|
102
|
+
return env;
|
|
103
|
+
}
|
|
67
104
|
/** The core worker HQ's setup bot runs. */
|
|
68
|
-
export const SETUP_WORKER_ID =
|
|
105
|
+
export const SETUP_WORKER_ID = SETUP_BOT_WORKER_ID;
|
|
69
106
|
/** The one message a person gets when a turn on their message did not finish. */
|
|
70
107
|
export function didNotFinishText(reason, progressPosted) {
|
|
71
108
|
const r = reason.trim().replace(/[.\s]+$/, "");
|
|
@@ -148,6 +185,9 @@ export async function runBot(deps) {
|
|
|
148
185
|
const turnCwd = config.hqRoot;
|
|
149
186
|
// HQ's setup bot is a guided conversation: no "working on it" filler between steps.
|
|
150
187
|
const isSetupBot = config.workerId === SETUP_WORKER_ID;
|
|
188
|
+
// Personal: every turn acts as the owner. Company: every turn acts as the bot.
|
|
189
|
+
const kind = effectiveBotKind(config);
|
|
190
|
+
const companies = effectiveBotCompanies(config);
|
|
151
191
|
const lockDeps = defaultPidLockDeps({
|
|
152
192
|
pid,
|
|
153
193
|
...(deps.isProcessAlive ? { isProcessAlive: deps.isProcessAlive } : {}),
|
|
@@ -193,14 +233,15 @@ export async function runBot(deps) {
|
|
|
193
233
|
await finish("stopped", 0, "local bot held for cloud promotion");
|
|
194
234
|
return { stop: async () => { }, done, status: () => null, idle: () => true };
|
|
195
235
|
}
|
|
196
|
-
|
|
197
|
-
|
|
236
|
+
// A restart is the owner's "try again" after signing in: start clean.
|
|
237
|
+
setStatus({ ...fallback(), pid, state: "starting", canDrainForPromotion: false, runtimeSignIn: null });
|
|
238
|
+
log("info", `starting ${config.name} (${config.runtime}, ${kind} bot${companies.length ? ` of ${companies.join(", ")}` : ""}) as ${config.agentUid} for ${config.ownerUid}`);
|
|
198
239
|
try {
|
|
199
240
|
const record = await api.getAgent(config.agentUid);
|
|
200
241
|
// Promotion keeps the identity and owner. Ownership alone must not let
|
|
201
242
|
// an old local installation start consuming the cloud bot's inbox.
|
|
202
|
-
if (record.uid !== config.agentUid || record.computeMode !== "local" || record.botKind !==
|
|
203
|
-
await finish("failed", 0, `identity ${config.agentUid} is not a
|
|
243
|
+
if (record.uid !== config.agentUid || record.computeMode !== "local" || record.botKind !== kind) {
|
|
244
|
+
await finish("failed", 0, `identity ${config.agentUid} is not a ${kind} local bot; this installation cannot run it`);
|
|
204
245
|
return { stop: async () => { }, done, status: () => null, idle: () => true };
|
|
205
246
|
}
|
|
206
247
|
const owner = record.ownerUid ?? null;
|
|
@@ -224,6 +265,8 @@ export async function runBot(deps) {
|
|
|
224
265
|
workerSource: config.workerSource,
|
|
225
266
|
workerId: config.workerId,
|
|
226
267
|
companySlug: config.companySlug,
|
|
268
|
+
kind,
|
|
269
|
+
companies,
|
|
227
270
|
memoryDir: config.memoryDir,
|
|
228
271
|
sources: readPromptSources(config.hqRoot, config.workerDir, undefined, {
|
|
229
272
|
memoryDir: config.memoryDir,
|
|
@@ -249,7 +292,11 @@ export async function runBot(deps) {
|
|
|
249
292
|
// the intro, so introSentAt (persisted before the turn) guarantees it never
|
|
250
293
|
// runs again on a restart.
|
|
251
294
|
let kickoffAfterIntro = null;
|
|
252
|
-
if (
|
|
295
|
+
if (config.introSentAt && config.kickoffPendingSignIn && config.kickoff?.trim()) {
|
|
296
|
+
// The kickoff was held for a sign-in on an earlier run; this start retries it.
|
|
297
|
+
kickoffAfterIntro = {};
|
|
298
|
+
}
|
|
299
|
+
else if (!config.introSentAt) {
|
|
253
300
|
try {
|
|
254
301
|
const sent = await api.sendDm({ toPersonUid: config.ownerUid, body: introDmText(config.name, config.runtime, config.intro) });
|
|
255
302
|
patchBotConfig(dir, { introSentAt: now().toISOString() });
|
|
@@ -264,6 +311,41 @@ export async function runBot(deps) {
|
|
|
264
311
|
// ── Heartbeat loop ─────────────────────────────────────────────────────────
|
|
265
312
|
const failures = new FailureWindow(FAILURE_WINDOW_MS, FAILURE_LIMIT);
|
|
266
313
|
let modelHealth = "ok";
|
|
314
|
+
// ── Coding tool sign-in ────────────────────────────────────────────────────
|
|
315
|
+
const signInRetryMs = Math.max(0, deps.signInRetryMs ?? SIGN_IN_RETRY_MS);
|
|
316
|
+
let signInIssue = null;
|
|
317
|
+
let nextSignInRetryAt = 0;
|
|
318
|
+
/** Conversations already told this time that the bot needs a sign-in. */
|
|
319
|
+
const toldAboutSignIn = new Set();
|
|
320
|
+
const markSignInBroken = (excerpt) => {
|
|
321
|
+
nextSignInRetryAt = now().getTime() + signInRetryMs;
|
|
322
|
+
modelHealth = "degraded";
|
|
323
|
+
if (signInIssue)
|
|
324
|
+
return;
|
|
325
|
+
signInIssue = { state: "expired", runtime: config.runtime, since: now().toISOString() };
|
|
326
|
+
setStatus({ runtimeSignIn: signInIssue, lastError: excerpt.slice(0, 400) });
|
|
327
|
+
log("warn", `${runtime.id} needs to sign in again on this computer; holding turns and retrying every ${Math.round(signInRetryMs / 1000)}s`);
|
|
328
|
+
};
|
|
329
|
+
const markSignInWorks = () => {
|
|
330
|
+
if (!signInIssue)
|
|
331
|
+
return;
|
|
332
|
+
signInIssue = null;
|
|
333
|
+
toldAboutSignIn.clear();
|
|
334
|
+
setStatus({ runtimeSignIn: null });
|
|
335
|
+
log("info", `${runtime.id} sign-in works again; resuming`);
|
|
336
|
+
};
|
|
337
|
+
/** Say once per conversation that the bot cannot work until its tool signs in. */
|
|
338
|
+
const tellSignInNeeded = async (conversation, post, kickoff = false) => {
|
|
339
|
+
if (toldAboutSignIn.has(conversation))
|
|
340
|
+
return;
|
|
341
|
+
try {
|
|
342
|
+
await post(signInNeededText(config.runtime, kickoff));
|
|
343
|
+
toldAboutSignIn.add(conversation);
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
log("warn", `could not say that ${runtime.id} needs a sign-in: ${err instanceof Error ? err.message : String(err)}`);
|
|
347
|
+
}
|
|
348
|
+
};
|
|
267
349
|
const heartbeatLoop = (async () => {
|
|
268
350
|
while (!stopping && state === "running") {
|
|
269
351
|
try {
|
|
@@ -314,20 +396,22 @@ export async function runBot(deps) {
|
|
|
314
396
|
// resets). Claude gets the full hook set via its payload session id;
|
|
315
397
|
// Codex/Grok only see HQ_SESSION_ID.
|
|
316
398
|
const sid = session?.sessionId ?? randomUUID();
|
|
317
|
-
|
|
399
|
+
const env = turnEnv(process.env, kind, config.agentUid);
|
|
400
|
+
let bind = { env };
|
|
318
401
|
if (config.companySlug) {
|
|
319
402
|
try {
|
|
320
403
|
ensureBotSessionMeta(config.hqRoot, sid, config.companySlug, undefined, now);
|
|
321
|
-
bind = { newSessionId: sid, env: { ...
|
|
404
|
+
bind = { newSessionId: sid, env: { ...env, HQ_SESSION_ID: sid } };
|
|
322
405
|
}
|
|
323
406
|
catch (err) {
|
|
324
407
|
log("warn", `company bind failed for ${config.companySlug}: ${err instanceof Error ? err.message : String(err)}`);
|
|
325
408
|
}
|
|
326
409
|
}
|
|
327
|
-
// Only the owner's own DM session (DMs and the
|
|
328
|
-
// keep their [#channel …] header first, and a
|
|
329
|
-
// by other people, who must not be handed the
|
|
330
|
-
|
|
410
|
+
// Only a personal bot, and only in the owner's own DM session (DMs and the
|
|
411
|
+
// kickoff). Room prompts must keep their [#channel …] header first, and a
|
|
412
|
+
// room turn can be triggered by other people, who must not be handed the
|
|
413
|
+
// owner's companies. A company bot acts as itself and never gets it.
|
|
414
|
+
if (deps.ownerContext && sessionScope === "dm" && kind === "personal") {
|
|
331
415
|
try {
|
|
332
416
|
const owner = await deps.ownerContext();
|
|
333
417
|
if (owner.status === "unavailable") {
|
|
@@ -384,6 +468,7 @@ export async function runBot(deps) {
|
|
|
384
468
|
}
|
|
385
469
|
}
|
|
386
470
|
modelHealth = "ok";
|
|
471
|
+
markSignInWorks();
|
|
387
472
|
log("info", `model turn done in ${Math.round((now().getTime() - tTurnStart) / 1000)}s (${Math.round((tTurnStart - tReceived) / 1000)}s before it)`);
|
|
388
473
|
if (turn.sessionId) {
|
|
389
474
|
writeBotSession(dir, {
|
|
@@ -543,11 +628,23 @@ export async function runBot(deps) {
|
|
|
543
628
|
return;
|
|
544
629
|
}
|
|
545
630
|
clearInflight(dir, KICKOFF_MESSAGE_ID);
|
|
631
|
+
if (readBotConfig(dir)?.kickoffPendingSignIn)
|
|
632
|
+
patchBotConfig(dir, { kickoffPendingSignIn: false });
|
|
546
633
|
const st = readStatus();
|
|
547
634
|
setStatus({ repliesSent: (st?.repliesSent ?? 0) + 1, lastActivityAt: now().toISOString() });
|
|
548
635
|
log("info", `kickoff reply sent (${outcome.reply.length} chars)`);
|
|
549
636
|
return;
|
|
550
637
|
}
|
|
638
|
+
if (!outcome.aborted && poster.postedCount === 0 && isRuntimeSignInFailure(outcome.excerpt)) {
|
|
639
|
+
// Nothing ran: hold the kickoff until the tool can sign in, then run it.
|
|
640
|
+
clearInflight(dir, KICKOFF_MESSAGE_ID);
|
|
641
|
+
patchBotConfig(dir, { kickoffPendingSignIn: true });
|
|
642
|
+
markSignInBroken(outcome.excerpt);
|
|
643
|
+
await tellSignInNeeded("dm", (body) => api.sendDm({ toPersonUid: peerUid, body }), true);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (readBotConfig(dir)?.kickoffPendingSignIn)
|
|
647
|
+
patchBotConfig(dir, { kickoffPendingSignIn: false });
|
|
551
648
|
let count = 0;
|
|
552
649
|
if (!outcome.aborted)
|
|
553
650
|
count = recordTurnFailure(outcome.error, "dm").count;
|
|
@@ -589,7 +686,7 @@ export async function runBot(deps) {
|
|
|
589
686
|
// 404 on the server by design), then ack so it never comes back.
|
|
590
687
|
try {
|
|
591
688
|
if (sender)
|
|
592
|
-
await api.sendDm({ toPersonUid: sender, body: nonOwnerRefusalText(config.name) });
|
|
689
|
+
await api.sendDm({ toPersonUid: sender, body: nonOwnerRefusalText(config.name, kind) });
|
|
593
690
|
}
|
|
594
691
|
catch {
|
|
595
692
|
/* refusal delivery is best-effort */
|
|
@@ -687,6 +784,17 @@ export async function runBot(deps) {
|
|
|
687
784
|
return;
|
|
688
785
|
}
|
|
689
786
|
await clearThinking();
|
|
787
|
+
if (!outcome.aborted && poster.postedCount === 0 && isRuntimeSignInFailure(outcome.excerpt)) {
|
|
788
|
+
// The tool refused before doing anything: keep the message for when it
|
|
789
|
+
// can sign in again, and say so once in this conversation.
|
|
790
|
+
clearInflight(dir, id);
|
|
791
|
+
attempts.delete(id);
|
|
792
|
+
markSignInBroken(outcome.excerpt);
|
|
793
|
+
const conversation = markerTarget.kind === "dm" ? "dm" : `room:${markerTarget.channelId}:${item.rootEventId ?? ""}`;
|
|
794
|
+
await tellSignInNeeded(conversation, send);
|
|
795
|
+
log("warn", `kept ${id} until ${runtime.id} can sign in again`);
|
|
796
|
+
return; // un-acked: answered once the sign-in works
|
|
797
|
+
}
|
|
690
798
|
if (!outcome.started) {
|
|
691
799
|
// The model never ran (e.g. the binary is missing): nothing happened
|
|
692
800
|
// outside this Mac, so retrying is safe.
|
|
@@ -751,6 +859,9 @@ export async function runBot(deps) {
|
|
|
751
859
|
for (const [lane, queue] of lanes) {
|
|
752
860
|
if (busyLanes.size >= maxParallelTurns || !canStart())
|
|
753
861
|
return;
|
|
862
|
+
// While the sign-in is broken, one message at a time tries it, on a timer.
|
|
863
|
+
if (signInIssue && (busyLanes.size > 0 || now().getTime() < nextSignInRetryAt))
|
|
864
|
+
return;
|
|
754
865
|
if (busyLanes.has(lane))
|
|
755
866
|
continue;
|
|
756
867
|
const item = queue.shift();
|
|
@@ -759,6 +870,8 @@ export async function runBot(deps) {
|
|
|
759
870
|
if (!item)
|
|
760
871
|
continue;
|
|
761
872
|
busyLanes.add(lane);
|
|
873
|
+
if (signInIssue)
|
|
874
|
+
nextSignInRetryAt = now().getTime() + signInRetryMs;
|
|
762
875
|
const task = handleItem(item)
|
|
763
876
|
.catch((err) => {
|
|
764
877
|
log("warn", `inbox item ${item.messageId} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -797,6 +910,16 @@ export async function runBot(deps) {
|
|
|
797
910
|
while (!stopping && state === "running") {
|
|
798
911
|
if (hasPromotionHold(dir))
|
|
799
912
|
break;
|
|
913
|
+
// A kickoff held for a sign-in retries on the same timer as messages.
|
|
914
|
+
if (busyLanes.size === 0 &&
|
|
915
|
+
(!signInIssue || now().getTime() >= nextSignInRetryAt) &&
|
|
916
|
+
readBotConfig(dir)?.kickoffPendingSignIn) {
|
|
917
|
+
if (signInIssue)
|
|
918
|
+
nextSignInRetryAt = now().getTime() + signInRetryMs;
|
|
919
|
+
await runKickoff().catch((err) => {
|
|
920
|
+
log("error", `kickoff retry crashed: ${err instanceof Error ? err.message : String(err)}`);
|
|
921
|
+
});
|
|
922
|
+
}
|
|
800
923
|
try {
|
|
801
924
|
const items = await api.pullInbox(config.agentUid);
|
|
802
925
|
setStatus({ lastInboxPollAt: now().toISOString() });
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bot's coding tool (Claude Code, Codex, Grok) is installed but its login
|
|
3
|
+
* no longer works: never signed in, expired, or revoked. The vendor CLI can
|
|
4
|
+
* still report itself as logged in (a saved but dead token), so the only
|
|
5
|
+
* reliable evidence is a turn that fails this way.
|
|
6
|
+
*
|
|
7
|
+
* Such a turn fails before the model does anything, so the message is safe to
|
|
8
|
+
* answer later. The bot says once that it needs a sign-in, keeps the message,
|
|
9
|
+
* and tries again on its own (and at once after `hq bot restart`).
|
|
10
|
+
*/
|
|
11
|
+
import type { BotRuntimeId } from "./config.js";
|
|
12
|
+
/** Time between the bot's own retries while its sign-in is broken. */
|
|
13
|
+
export declare const SIGN_IN_RETRY_MS = 60000;
|
|
14
|
+
export declare function isRuntimeSignInFailure(text: string | undefined | null): boolean;
|
|
15
|
+
export declare function runtimeDisplayName(runtime: BotRuntimeId | string): string;
|
|
16
|
+
/** Reported in status.json and `hq bot list --json` while the sign-in is broken. */
|
|
17
|
+
export interface RuntimeSignInIssue {
|
|
18
|
+
state: "expired";
|
|
19
|
+
runtime: BotRuntimeId;
|
|
20
|
+
since: string;
|
|
21
|
+
}
|
|
22
|
+
/** The one message a conversation gets when the bot cannot work until its tool signs in again. */
|
|
23
|
+
export declare function signInNeededText(runtime: BotRuntimeId | string, kickoff?: boolean): string;
|
|
24
|
+
//# sourceMappingURL=runtime-sign-in.d.ts.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bot's coding tool (Claude Code, Codex, Grok) is installed but its login
|
|
3
|
+
* no longer works: never signed in, expired, or revoked. The vendor CLI can
|
|
4
|
+
* still report itself as logged in (a saved but dead token), so the only
|
|
5
|
+
* reliable evidence is a turn that fails this way.
|
|
6
|
+
*
|
|
7
|
+
* Such a turn fails before the model does anything, so the message is safe to
|
|
8
|
+
* answer later. The bot says once that it needs a sign-in, keeps the message,
|
|
9
|
+
* and tries again on its own (and at once after `hq bot restart`).
|
|
10
|
+
*/
|
|
11
|
+
/** Time between the bot's own retries while its sign-in is broken. */
|
|
12
|
+
export const SIGN_IN_RETRY_MS = 60_000;
|
|
13
|
+
const SIGN_IN_FAILURE = /failed to authenticate|authentication[_ ]failed|not logged in|not signed in|please (?:run \/login|log ?in|sign ?in)|(?:oauth|access|refresh) token (?:has been |was )?(?:expired|revoked|invalid)|oauth session expired|invalid api key|\b401\b|unauthori[sz]ed|login (?:required|expired)|sign-?in (?:required|expired)/i;
|
|
14
|
+
/** Usage and rate limits are a different problem; a new sign-in does not fix them. */
|
|
15
|
+
const NOT_SIGN_IN = /usage[_ ]limit|rate[_ ]limit|quota|too many requests|credits/i;
|
|
16
|
+
export function isRuntimeSignInFailure(text) {
|
|
17
|
+
const t = (text ?? "").trim();
|
|
18
|
+
if (!t)
|
|
19
|
+
return false;
|
|
20
|
+
return SIGN_IN_FAILURE.test(t) && !NOT_SIGN_IN.test(t);
|
|
21
|
+
}
|
|
22
|
+
export function runtimeDisplayName(runtime) {
|
|
23
|
+
return runtime === "claude" ? "Claude Code" : runtime === "codex" ? "Codex" : runtime === "grok" ? "Grok" : runtime;
|
|
24
|
+
}
|
|
25
|
+
/** The one message a conversation gets when the bot cannot work until its tool signs in again. */
|
|
26
|
+
export function signInNeededText(runtime, kickoff = false) {
|
|
27
|
+
const tool = runtimeDisplayName(runtime);
|
|
28
|
+
return (`I can't work right now: ${tool} needs you to sign in again on this computer. ` +
|
|
29
|
+
`Use "Sign in to ${tool}" in HQ, and I'll ${kickoff ? "get started" : "answer this"} as soon as it works again — no need to send it twice.`);
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=runtime-sign-in.js.map
|
package/dist/lib/bot/status.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* status.json — atomic, metadata-only runtime status (local-bots US-003).
|
|
3
3
|
*/
|
|
4
4
|
import type { BotRuntimeId } from "./config.js";
|
|
5
|
+
import type { RuntimeSignInIssue } from "./runtime-sign-in.js";
|
|
5
6
|
export type BotState = "starting" | "running" | "stopped" | "failed";
|
|
6
7
|
export interface BotStatusFile {
|
|
7
8
|
v: 1;
|
|
@@ -29,6 +30,11 @@ export interface BotStatusFile {
|
|
|
29
30
|
* Persisted so later turns skip the failed attempt.
|
|
30
31
|
*/
|
|
31
32
|
claudePermissionMode?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Set while the coding tool's sign-in is broken (a turn failed for that
|
|
35
|
+
* reason); cleared by the next turn that works. `null` = known to work.
|
|
36
|
+
*/
|
|
37
|
+
runtimeSignIn?: RuntimeSignInIssue | null;
|
|
32
38
|
updatedAt: string;
|
|
33
39
|
}
|
|
34
40
|
export declare function defaultBotStatus(pid: number, runtime: BotRuntimeId, now?: () => Date): BotStatusFile;
|