@indigoai-us/hq-cli 5.345.190 → 5.345.193
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 +420 -0
- package/assets/bot-workers/setup/worker.yaml +91 -34
- package/dist/command-catalog.generated.d.ts +96 -19
- package/dist/command-catalog.generated.js +124 -21
- package/dist/commands/bot-jobs.d.ts +21 -18
- package/dist/commands/bot-jobs.js +127 -39
- package/dist/commands/bot.d.ts +7 -4
- package/dist/commands/bot.js +289 -140
- package/dist/commands/core-jobs-schedule.js +10 -0
- package/dist/commands/daemon.js +37 -0
- package/dist/commands/files.d.ts +41 -6
- package/dist/commands/files.js +108 -27
- package/dist/commands/pack-start-bot.d.ts +4 -6
- package/dist/commands/pack-start-bot.js +4 -8
- package/dist/commands/signals-run.d.ts +45 -0
- package/dist/commands/signals-run.js +175 -0
- package/dist/commands/signals.d.ts +1 -0
- package/dist/commands/signals.js +3 -0
- package/dist/commands/sources.d.ts +11 -0
- package/dist/commands/sources.js +137 -25
- package/dist/commands/staff/reset-test-account.d.ts +10 -0
- package/dist/commands/staff/reset-test-account.js +18 -2
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +31171 -52512
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/bot/api.d.ts +3 -2
- package/dist/lib/bot/config.d.ts +2 -0
- package/dist/lib/bot/fleet-cron.d.ts +121 -0
- package/dist/lib/bot/fleet-cron.js +319 -0
- package/dist/lib/bot/fleet-local.d.ts +113 -2
- package/dist/lib/bot/fleet-local.js +597 -35
- package/dist/lib/bot/fleet-owner-companies.d.ts +12 -0
- package/dist/lib/bot/fleet-owner-companies.js +30 -0
- package/dist/lib/bot/fleet-watch.d.ts +25 -0
- package/dist/lib/bot/fleet-watch.js +51 -0
- package/dist/lib/bot/index.d.ts +1 -1
- package/dist/lib/bot/index.js +1 -1
- package/dist/lib/bot/job-run.d.ts +5 -4
- package/dist/lib/bot/job-run.js +5 -4
- package/dist/lib/bot/jobs/hot-signal-sync.d.ts +57 -3
- package/dist/lib/bot/jobs/hot-signal-sync.js +163 -4
- package/dist/lib/bot/local-jobs.d.ts +13 -2
- package/dist/lib/bot/local-jobs.js +17 -4
- package/dist/lib/bot/progress.d.ts +14 -0
- package/dist/lib/bot/progress.js +47 -1
- package/dist/lib/bot/run.d.ts +55 -2
- package/dist/lib/bot/run.js +192 -10
- package/dist/lib/bot/setup-tool-offer.d.ts +1 -0
- package/dist/lib/bot/setup-tool-offer.js +16 -0
- package/dist/lib/bot/signals-engine-flag.d.ts +18 -0
- package/dist/lib/bot/signals-engine-flag.js +30 -0
- package/dist/lib/daemon/catalog.d.ts +4 -0
- package/dist/lib/daemon/catalog.js +29 -9
- package/dist/lib/daemon/legacy.d.ts +6 -0
- package/dist/lib/daemon/legacy.js +16 -10
- package/dist/lib/daemon/run.d.ts +1 -0
- package/dist/lib/daemon/run.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/flag-registry.d.ts +13 -1
- package/dist/lib/flag-registry.js +46 -0
- package/dist/lib/hot-channels/adapters/attio.d.ts +75 -2
- package/dist/lib/hot-channels/adapters/attio.js +367 -29
- package/dist/lib/hot-channels/adapters/gmail.d.ts +14 -0
- package/dist/lib/hot-channels/adapters/gmail.js +59 -2
- package/dist/lib/hot-channels/adapters/linear.d.ts +24 -0
- package/dist/lib/hot-channels/adapters/linear.js +71 -1
- package/dist/lib/hot-channels/adapters/meetings.js +4 -1
- package/dist/lib/hot-channels/adapters/slack.js +7 -3
- package/dist/lib/hot-channels/alert.js +1 -1
- package/dist/lib/hot-channels/contract.d.ts +11 -0
- package/dist/lib/hot-channels/contract.js +1 -0
- package/dist/lib/hot-channels/cursors.js +1 -1
- package/dist/lib/hot-channels/landing.d.ts +6 -2
- package/dist/lib/hot-channels/landing.js +13 -6
- package/dist/lib/hot-channels/ledger.js +2 -0
- package/dist/lib/hot-channels/loop.js +18 -4
- package/dist/lib/hot-channels/registry.js +9 -1
- package/dist/lib/hot-channels/run.d.ts +18 -0
- package/dist/lib/hot-channels/run.js +15 -4
- package/dist/lib/hot-channels/signals-pass.d.ts +1 -0
- package/dist/lib/hot-channels/signals-pass.js +7 -5
- package/dist/lib/hot-channels/sync-lock.js +22 -6
- package/dist/lib/mesh/live/daemon/install.d.ts +22 -0
- package/dist/lib/mesh/live/daemon/install.js +117 -30
- package/dist/lib/signals/adapters-catalog.d.ts +77 -0
- package/dist/lib/signals/adapters-catalog.js +194 -0
- package/dist/lib/signals/engine.d.ts +196 -0
- package/dist/lib/signals/engine.js +495 -0
- package/dist/lib/signals/failure-alert.d.ts +89 -0
- package/dist/lib/signals/failure-alert.js +175 -0
- package/dist/lib/signals/local-date.d.ts +3 -0
- package/dist/lib/signals/local-date.js +6 -0
- package/dist/lib/signals/projection.d.ts +42 -0
- package/dist/lib/signals/projection.js +67 -0
- package/dist/lib/signals/reply.d.ts +112 -0
- package/dist/lib/signals/reply.js +368 -0
- package/dist/lib/signals/run-lock.d.ts +36 -0
- package/dist/lib/signals/run-lock.js +148 -0
- package/dist/lib/signals/run-record.d.ts +24 -0
- package/dist/lib/signals/run-record.js +82 -0
- package/dist/lib/signals/sanitize.d.ts +105 -0
- package/dist/lib/signals/sanitize.js +439 -0
- package/dist/lib/signals/settings.d.ts +10 -0
- package/dist/lib/signals/settings.js +48 -0
- package/dist/lib/signals/steps/adapters.d.ts +14 -0
- package/dist/lib/signals/steps/adapters.js +156 -0
- package/dist/lib/signals/steps/entity-similarity.d.ts +10 -0
- package/dist/lib/signals/steps/entity-similarity.js +47 -0
- package/dist/lib/signals/steps/extract.d.ts +113 -0
- package/dist/lib/signals/steps/extract.js +765 -0
- package/dist/lib/signals/steps/garden.d.ts +24 -0
- package/dist/lib/signals/steps/garden.js +250 -0
- package/dist/lib/signals/steps/types.d.ts +112 -0
- package/dist/lib/signals/steps/types.js +4 -0
- package/dist/lib/signals/steps/upload.d.ts +43 -0
- package/dist/lib/signals/steps/upload.js +324 -0
- package/dist/lib/signals/write-guard.d.ts +63 -0
- package/dist/lib/signals/write-guard.js +287 -0
- package/dist/utils/resident-package-preload.js +16 -0
- package/package.json +9 -9
- package/dist/lib/bot/fleet-default-flag.d.ts +0 -31
- package/dist/lib/bot/fleet-default-flag.js +0 -127
package/dist/lib/bot/progress.js
CHANGED
|
@@ -37,6 +37,25 @@ export const PRE_TOOL_ASIDE_MAX_CHARS = 40;
|
|
|
37
37
|
* your agency docs now, then I'll look at the client folders.") and run past
|
|
38
38
|
* the limit; an hq-block is always content the desktop app must draw.
|
|
39
39
|
*/
|
|
40
|
+
/**
|
|
41
|
+
* A heads-up the model writes right before a long piece of work ("Give me
|
|
42
|
+
* about two minutes to check what's on this computer, then I'll ask my
|
|
43
|
+
* first question."). A bot that drops every pre-tool message
|
|
44
|
+
* (`dropPreToolMessages`) still lets ONE of these through per turn, so the
|
|
45
|
+
* person hears what is happening and how long it takes instead of watching
|
|
46
|
+
* a spinner. Short, one or two sentences, names a wait, no hq-block.
|
|
47
|
+
*/
|
|
48
|
+
export const WAIT_HEADS_UP_MAX_CHARS = 240;
|
|
49
|
+
export function isWaitHeadsUp(text) {
|
|
50
|
+
const t = text.trim();
|
|
51
|
+
// A question is not a heads-up: it would be held as an in-between question
|
|
52
|
+
// and dropped when the turn ends with a final answer.
|
|
53
|
+
if (!t || t.length > WAIT_HEADS_UP_MAX_CHARS || t.includes("```") || t.includes("?"))
|
|
54
|
+
return false;
|
|
55
|
+
// It has to name a wait. "I'll check the manifest first." is narration
|
|
56
|
+
// (the setup worker forbids it); "Give me about two minutes" is not.
|
|
57
|
+
return /\b(?:minutes?|seconds?|a moment|one moment|right back|be back)\b/i.test(t);
|
|
58
|
+
}
|
|
40
59
|
export function isPreToolAside(text) {
|
|
41
60
|
const t = text.trim();
|
|
42
61
|
return t.length < PRE_TOOL_ASIDE_MAX_CHARS && !t.includes("```hq-block");
|
|
@@ -61,6 +80,8 @@ export class ProgressPoster {
|
|
|
61
80
|
workingNoticeText;
|
|
62
81
|
buffer;
|
|
63
82
|
dropPreTool;
|
|
83
|
+
/** With dropPreToolMessages: one wait heads-up (isWaitHeadsUp) per turn is kept. */
|
|
84
|
+
headsUpKept = false;
|
|
64
85
|
onDropped;
|
|
65
86
|
/**
|
|
66
87
|
* Messages waiting to learn whether a tool call follows: every message when
|
|
@@ -124,6 +145,18 @@ export class ProgressPoster {
|
|
|
124
145
|
this.enqueueWith(this.workingNoticeText, this.sendProgress);
|
|
125
146
|
}, this.workingNoticeMs);
|
|
126
147
|
}
|
|
148
|
+
/** Arm the still-working line again after a kept heads-up (start() skips a turn that said something). */
|
|
149
|
+
rearmNoticeAfter(saidCount) {
|
|
150
|
+
if (this.buffer || this.finished || this.noticeTimer !== null || this.workingNoticeMs <= 0)
|
|
151
|
+
return;
|
|
152
|
+
this.noticeTimer = this.setTimer(() => {
|
|
153
|
+
this.noticeTimer = null;
|
|
154
|
+
if (this.finished || this.noticeSent || this.said.length !== saidCount)
|
|
155
|
+
return;
|
|
156
|
+
this.noticeSent = true;
|
|
157
|
+
this.enqueueWith(this.workingNoticeText, this.sendProgress);
|
|
158
|
+
}, this.workingNoticeMs);
|
|
159
|
+
}
|
|
127
160
|
/** One finished assistant message from the model. */
|
|
128
161
|
message(text) {
|
|
129
162
|
const t = text.trim();
|
|
@@ -145,8 +178,21 @@ export class ProgressPoster {
|
|
|
145
178
|
tool() {
|
|
146
179
|
if (this.finished)
|
|
147
180
|
return;
|
|
148
|
-
|
|
181
|
+
let dropped = this.candidates;
|
|
149
182
|
this.candidates = [];
|
|
183
|
+
// The last thing said before the tool call, when it tells the person
|
|
184
|
+
// what is about to happen and how long, is for them: post it.
|
|
185
|
+
if (this.dropPreTool && !this.headsUpKept) {
|
|
186
|
+
const last = dropped.at(-1);
|
|
187
|
+
if (last !== undefined && isWaitHeadsUp(last)) {
|
|
188
|
+
this.headsUpKept = true;
|
|
189
|
+
dropped = dropped.slice(0, -1);
|
|
190
|
+
this.accept(last);
|
|
191
|
+
// The heads-up named a wait; if the work then outlives that silence
|
|
192
|
+
// budget with nothing more said, the still-working line goes out too.
|
|
193
|
+
this.rearmNoticeAfter(this.said.length);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
150
196
|
for (const t of dropped) {
|
|
151
197
|
try {
|
|
152
198
|
this.onDropped(t);
|
package/dist/lib/bot/run.d.ts
CHANGED
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
* - first start only: the intro DM, then (when bot.json has `kickoff`) ONE
|
|
16
16
|
* model turn on that prompt as if the owner sent it, answered in the DM, so
|
|
17
17
|
* a guided bot starts working without waiting for the owner to type
|
|
18
|
-
* - job items (channel "job"): a run
|
|
19
|
-
*
|
|
18
|
+
* - job items (channel "job"): a scheduled run, fired by the bot's own loop
|
|
19
|
+
* from its jobs.json (lib/bot/local-jobs.ts) or, for a hosted agent, by
|
|
20
|
+
* the cloud. No system scheduler is used on this computer. The run is one model turn
|
|
20
21
|
* in its own session (one per jobId), reported in the owner's DM with a
|
|
21
22
|
* "scheduled job" header through the same split/recovery path as every other
|
|
22
23
|
* reply, and reported back to the cloud EXACTLY ONCE — nothing else clears
|
|
@@ -152,8 +153,20 @@ export interface BotRunDeps {
|
|
|
152
153
|
company: string;
|
|
153
154
|
catchUp: boolean;
|
|
154
155
|
now: () => Date;
|
|
156
|
+
/** When the run was due (the fired item's `firedAt`); the catch-up reply names it. */
|
|
157
|
+
scheduledFor?: string;
|
|
158
|
+
/** The job's zone, for the times in the reply. */
|
|
159
|
+
timeZone?: string;
|
|
155
160
|
/** The signals-failure alert to the owner, sent even when the job is quiet (US-012). */
|
|
156
161
|
sendAlert: (text: string) => Promise<void>;
|
|
162
|
+
/** This run is the bot's automatic retry of a failed run. */
|
|
163
|
+
retry?: boolean;
|
|
164
|
+
/**
|
|
165
|
+
* Schedules one automatic retry of this run at `at` (the run record's
|
|
166
|
+
* nextRetryAt), in this process. Returns true when it is scheduled. Only
|
|
167
|
+
* offered for a local job's scheduled run, never for a retry.
|
|
168
|
+
*/
|
|
169
|
+
scheduleRetry?: (at: string) => boolean;
|
|
157
170
|
}) => Promise<{
|
|
158
171
|
ok: boolean;
|
|
159
172
|
text: string;
|
|
@@ -315,6 +328,46 @@ export declare function preflightCreds(dir: string, config: BotConfig): string |
|
|
|
315
328
|
export declare function runBot(deps: BotRunDeps): Promise<BotHandle>;
|
|
316
329
|
/** True for a setup note the desktop app sent, not words the person typed. */
|
|
317
330
|
export declare function isAppSetupNote(text: string): boolean;
|
|
331
|
+
/**
|
|
332
|
+
* True for the ONE note the desktop app sends when its visual first run
|
|
333
|
+
* closes: the handoff that names the assistant (`name` in `done`). Older
|
|
334
|
+
* builds sent partial notes (an import, a company) with no `name`; those
|
|
335
|
+
* fold in quietly (`APP_SETUP_NOTE_TURN_NOTE`).
|
|
336
|
+
*/
|
|
337
|
+
export declare function isFirstRunHandoffNote(text: string): boolean;
|
|
338
|
+
/**
|
|
339
|
+
* Put ahead of the end-of-setup handoff note: it is the assistant's first
|
|
340
|
+
* turn, so it always answers, and in seconds: everything it needs is in the
|
|
341
|
+
* handoff, the Owner context block and the facts appended below it, so it
|
|
342
|
+
* runs no command and reads no file first. The shape of that first message
|
|
343
|
+
* is in the setup worker's instructions ("When the app hands you settled
|
|
344
|
+
* steps").
|
|
345
|
+
*/
|
|
346
|
+
export declare const FIRST_RUN_HANDOFF_TURN_NOTE: string;
|
|
347
|
+
export interface FirstRunHandoffFacts {
|
|
348
|
+
/** Company folders under companies/ on this computer (slugs). */
|
|
349
|
+
companyFolders: string[];
|
|
350
|
+
/** The bot's setup-progress.md, or null when there is none yet. */
|
|
351
|
+
progressNote: string | null;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* What the first-run turn would otherwise spend tool calls finding out: the
|
|
355
|
+
* company folders on this computer and the progress note. Read here, once,
|
|
356
|
+
* so the first reply needs no command. Never throws.
|
|
357
|
+
*/
|
|
358
|
+
export declare function firstRunHandoffFacts(hqRoot: string, memoryDir: string): FirstRunHandoffFacts;
|
|
359
|
+
/** The whole first-run turn note: the fixed lead, then the facts. */
|
|
360
|
+
export declare function firstRunHandoffTurnNote(facts: FirstRunHandoffFacts): string;
|
|
361
|
+
/** The setup bot's "still working" line: plain words, no dash, nothing about mechanics. */
|
|
362
|
+
export declare const SETUP_WORKING_NOTICE_TEXT = "Still working on it. I'll post here as soon as I'm done.";
|
|
363
|
+
/**
|
|
364
|
+
* The setup bot's silence budget before that line. Half the general one:
|
|
365
|
+
* the person is new and watching. The bot is told to say what it is about
|
|
366
|
+
* to do and how long first (a wait heads-up, kept by the poster); the line
|
|
367
|
+
* goes out when it said nothing, or when the work outlives the heads-up by
|
|
368
|
+
* this much with nothing more said.
|
|
369
|
+
*/
|
|
370
|
+
export declare const SETUP_WORKING_NOTICE_MS = 30000;
|
|
318
371
|
/** Put ahead of an app setup note: fold it in, and stay quiet unless it changes what to say. */
|
|
319
372
|
export declare const APP_SETUP_NOTE_TURN_NOTE: string;
|
|
320
373
|
//# sourceMappingURL=run.d.ts.map
|
package/dist/lib/bot/run.js
CHANGED
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
* - first start only: the intro DM, then (when bot.json has `kickoff`) ONE
|
|
16
16
|
* model turn on that prompt as if the owner sent it, answered in the DM, so
|
|
17
17
|
* a guided bot starts working without waiting for the owner to type
|
|
18
|
-
* - job items (channel "job"): a run
|
|
19
|
-
*
|
|
18
|
+
* - job items (channel "job"): a scheduled run, fired by the bot's own loop
|
|
19
|
+
* from its jobs.json (lib/bot/local-jobs.ts) or, for a hosted agent, by
|
|
20
|
+
* the cloud. No system scheduler is used on this computer. The run is one model turn
|
|
20
21
|
* in its own session (one per jobId), reported in the owner's DM with a
|
|
21
22
|
* "scheduled job" header through the same split/recovery path as every other
|
|
22
23
|
* reply, and reported back to the cloud EXACTLY ONCE — nothing else clears
|
|
@@ -84,6 +85,7 @@ import { resolveRoomMentionsForBot, roomPostHasMentionTokens, } from "./room-men
|
|
|
84
85
|
import { buildJobTurnPrompt, isCatchUpRun, isJobItem, jobReportHeader, jobRunOutput, machineTimeZone, planJobRuns, stripJobHeader, SUPERSEDED_DISPOSITION } from "./job-run.js";
|
|
85
86
|
import { botJobQuietOptionEnabled } from "./job-quiet-flag.js";
|
|
86
87
|
import { currentHotSignalSyncCompany, hotSignalSyncJobCompany, readActiveCompanySlug, removeStaleHotSignalSyncJobs } from "./jobs/hot-signal-sync.js";
|
|
88
|
+
import { isLocalJobMessage, isLocalJobRetryMessage, localJobRetryMessageId, readLocalJobs } from "./local-jobs.js";
|
|
87
89
|
import { clearPendingJobCompletion, isCompletionDue, isCompletionExpired, isCompletionForOtherAgent, readPendingJobCompletions, recordPendingJobCompletion } from "./job-completions.js";
|
|
88
90
|
import { SIGN_IN_CHECK_FRESH_MS, SIGN_IN_CHECK_MS, SIGN_IN_RETRY_MS, signInNeededText, signedOutExcerpt, } from "./runtime-sign-in.js";
|
|
89
91
|
import { checkRuntimeSignIn } from "./runtime-sign-in-check.js";
|
|
@@ -1374,13 +1376,18 @@ export async function runBot(deps) {
|
|
|
1374
1376
|
};
|
|
1375
1377
|
const poster = new ProgressPoster({
|
|
1376
1378
|
// The setup bot talks to someone new to HQ: its notes to itself never
|
|
1377
|
-
// reach them
|
|
1378
|
-
// desktop app shows that the bot is working.
|
|
1379
|
+
// reach them.
|
|
1379
1380
|
...(isSetupBot ? { dropPreToolMessages: true } : {}),
|
|
1380
1381
|
// Short asides before a tool call never reach the person either (isPreToolAside).
|
|
1381
1382
|
onDropped: (text) => log("debug", `dropped a pre-tool aside for ${messageId} (${text.length} chars): ${text}`),
|
|
1383
|
+
// The setup bot talks to someone new to HQ, so its "still working" line
|
|
1384
|
+
// is the plain one (no dash, no mechanics) and comes sooner. It used to
|
|
1385
|
+
// post none at all because the desktop app shows the bot working; a
|
|
1386
|
+
// first run on 2026-11-02 sat on that spinner for over three minutes
|
|
1387
|
+
// with nothing said, so the line is back for it too.
|
|
1388
|
+
...(isSetupBot ? { workingNoticeText: SETUP_WORKING_NOTICE_TEXT, workingNoticeMs: SETUP_WORKING_NOTICE_MS } : {}),
|
|
1382
1389
|
...deps.progress,
|
|
1383
|
-
...(args.workingNotice === false || silenceable
|
|
1390
|
+
...(args.workingNotice === false || silenceable ? { workingNoticeMs: 0 } : {}),
|
|
1384
1391
|
...(silenceable || args.suppressOutput ? { buffer: true } : {}),
|
|
1385
1392
|
sendProgress: makeSendFn(args.sendProgress),
|
|
1386
1393
|
sendToPerson: makeSendFn(args.sendToPerson),
|
|
@@ -1578,6 +1585,27 @@ export async function runBot(deps) {
|
|
|
1578
1585
|
const handledJobFires = new Map();
|
|
1579
1586
|
/** Runs an older fire of a job that a newer pending run supersedes. */
|
|
1580
1587
|
const supersededJobRuns = new Set();
|
|
1588
|
+
/**
|
|
1589
|
+
* The automatic retry of a failed signals job run, per jobId: one synthetic
|
|
1590
|
+
* local run, due at the run record's nextRetryAt. Kept in memory only, so a
|
|
1591
|
+
* stop or restart drops it (the next scheduled fire, or "Try now", runs
|
|
1592
|
+
* instead). The inbox loop enqueues it once it is due.
|
|
1593
|
+
*/
|
|
1594
|
+
const signalsRetries = new Map();
|
|
1595
|
+
const scheduleSignalsRetry = (item, jobId, at) => {
|
|
1596
|
+
const dueMs = Date.parse(at);
|
|
1597
|
+
if (!Number.isFinite(dueMs) || stopping || state !== "running")
|
|
1598
|
+
return false;
|
|
1599
|
+
const { requestId: _requestId, receivedAt: _receivedAt, ...rest } = item;
|
|
1600
|
+
signalsRetries.set(jobId, {
|
|
1601
|
+
...rest,
|
|
1602
|
+
messageId: localJobRetryMessageId(jobId, dueMs),
|
|
1603
|
+
firedAt: new Date(dueMs).toISOString(),
|
|
1604
|
+
staleAfter: new Date(dueMs + 10 * 60_000).toISOString(),
|
|
1605
|
+
});
|
|
1606
|
+
log("info", `signals job ${jobId}: the run failed; one automatic retry at ${new Date(dueMs).toISOString()}`);
|
|
1607
|
+
return true;
|
|
1608
|
+
};
|
|
1581
1609
|
/**
|
|
1582
1610
|
* Report one fired run as finished. This is the ONLY thing that clears the
|
|
1583
1611
|
* job's in-progress flag in the cloud, so a failure is logged loudly and the
|
|
@@ -1887,8 +1915,9 @@ export async function runBot(deps) {
|
|
|
1887
1915
|
tReceived,
|
|
1888
1916
|
sendProgress,
|
|
1889
1917
|
sendToPerson,
|
|
1890
|
-
//
|
|
1891
|
-
|
|
1918
|
+
// A kickoff turn that stays silent for a minute posts the "still
|
|
1919
|
+
// working" line like any other turn: the app's thinking row alone
|
|
1920
|
+
// left a first run looking stuck (2026-11-02).
|
|
1892
1921
|
});
|
|
1893
1922
|
await clearThinking();
|
|
1894
1923
|
if (!outcome.ok && outcome.error instanceof CompanyBindingError) {
|
|
@@ -2107,8 +2136,23 @@ export async function runBot(deps) {
|
|
|
2107
2136
|
log("info", `hot-signal-sync job ${jobId} run ${id} skipped: company ${syncCompany} no longer served (current: ${currentCompany ?? "none"})`);
|
|
2108
2137
|
return;
|
|
2109
2138
|
}
|
|
2139
|
+
// Times in the reply are in the job's own zone (jobs.json), not this machine's.
|
|
2140
|
+
const jobZone = readLocalJobs(dir).find((j) => j.jobId === jobId)?.timezone ?? timeZone;
|
|
2141
|
+
const isRetryRun = isLocalJobRetryMessage(id);
|
|
2110
2142
|
const result = deps.hotSignalSync
|
|
2111
|
-
? await deps.hotSignalSync({
|
|
2143
|
+
? await deps.hotSignalSync({
|
|
2144
|
+
company: syncCompany,
|
|
2145
|
+
catchUp,
|
|
2146
|
+
now,
|
|
2147
|
+
...(job.firedAt ? { scheduledFor: job.firedAt } : {}),
|
|
2148
|
+
timeZone: jobZone,
|
|
2149
|
+
sendAlert: (text) => sendToPerson(text),
|
|
2150
|
+
...(isRetryRun ? { retry: true } : {}),
|
|
2151
|
+
// One in-process retry per failed scheduled run of a local job.
|
|
2152
|
+
...(isLocalJobMessage(id) && !isRetryRun
|
|
2153
|
+
? { scheduleRetry: (at) => scheduleSignalsRetry(item, jobId, at) }
|
|
2154
|
+
: {}),
|
|
2155
|
+
})
|
|
2112
2156
|
: { ok: false, text: `This bot cannot run the hot-channel sync for ${syncCompany}.` };
|
|
2113
2157
|
markProcessed(dir, id, now().toISOString());
|
|
2114
2158
|
const status = result.ok ? "succeeded" : "failed";
|
|
@@ -2542,8 +2586,13 @@ export async function runBot(deps) {
|
|
|
2542
2586
|
// answering every note re-posted that question (three times in a row on
|
|
2543
2587
|
// 2026-10-12). Such a turn may stay quiet.
|
|
2544
2588
|
const appSetupNote = isSetupBot && ownerDm && isAppSetupNote(messageText);
|
|
2589
|
+
// The one note the app sends when its visual setup closes is different:
|
|
2590
|
+
// the assistant was created without a kickoff, so this note is its first
|
|
2591
|
+
// turn and the person is in the chat waiting for its first message. It
|
|
2592
|
+
// never stays quiet.
|
|
2593
|
+
const firstRunHandoff = appSetupNote && isFirstRunHandoffNote(messageText);
|
|
2545
2594
|
const ownerTurnText = decisionTurn.note ? `${decisionTurn.note}\n\n${ownerText}` : ownerText;
|
|
2546
|
-
const prompt = target.kind === "room" ? await buildRoomTurn(item, target, text) : target.peerUid === config.ownerUid ? (appSetupNote ? `${APP_SETUP_NOTE_TURN_NOTE}\n\n${ownerTurnText}` : ownerTurnText) :
|
|
2595
|
+
const prompt = target.kind === "room" ? await buildRoomTurn(item, target, text) : target.peerUid === config.ownerUid ? (firstRunHandoff ? `${firstRunHandoffTurnNote(firstRunHandoffFacts(config.hqRoot, resolveBotMemoryDir(config)))}\n\n${ownerTurnText}` : appSetupNote ? `${APP_SETUP_NOTE_TURN_NOTE}\n\n${ownerTurnText}` : ownerTurnText) :
|
|
2547
2596
|
`[HQ DM sender: ${target.peerUid}; owner: ${config.ownerUid}; sender is not the owner.]\n\n${text}`;
|
|
2548
2597
|
const markerTarget = target.kind === "dm" ? { kind: "dm", peerUid: target.peerUid } : { kind: "room", channelId: target.channelId };
|
|
2549
2598
|
const { sendProgress, sendToPerson, saved } = trackingSend(markerTarget, item.rootEventId);
|
|
@@ -2559,7 +2608,7 @@ export async function runBot(deps) {
|
|
|
2559
2608
|
tReceived,
|
|
2560
2609
|
sendProgress,
|
|
2561
2610
|
sendToPerson,
|
|
2562
|
-
silenceable: (target.kind === "room" && !target.mentioned) || appSetupNote,
|
|
2611
|
+
silenceable: (target.kind === "room" && !target.mentioned) || (appSetupNote && !firstRunHandoff),
|
|
2563
2612
|
...(readDirs.length ? { readDirs } : {}),
|
|
2564
2613
|
...(target.kind === "dm" ? { historyEventId: item.eventId ?? id } : {}),
|
|
2565
2614
|
...(target.kind === "room"
|
|
@@ -2814,6 +2863,27 @@ export async function runBot(deps) {
|
|
|
2814
2863
|
else
|
|
2815
2864
|
lanes.set(lane, [item]);
|
|
2816
2865
|
};
|
|
2866
|
+
/** Hands a due signals retry to its job lane, unless the job was removed meanwhile. */
|
|
2867
|
+
const enqueueDueSignalsRetries = () => {
|
|
2868
|
+
if (signalsRetries.size === 0)
|
|
2869
|
+
return;
|
|
2870
|
+
const t = now().getTime();
|
|
2871
|
+
let added = false;
|
|
2872
|
+
for (const [jobId, retryItem] of signalsRetries) {
|
|
2873
|
+
if (Date.parse(retryItem.firedAt ?? "") > t)
|
|
2874
|
+
continue;
|
|
2875
|
+
signalsRetries.delete(jobId);
|
|
2876
|
+
if (!readLocalJobs(dir).some((j) => j.jobId === jobId)) {
|
|
2877
|
+
log("info", `signals job ${jobId} was removed; its automatic retry is dropped`);
|
|
2878
|
+
continue;
|
|
2879
|
+
}
|
|
2880
|
+
log("info", `signals job ${jobId}: running the automatic retry`);
|
|
2881
|
+
enqueue(retryItem);
|
|
2882
|
+
added = true;
|
|
2883
|
+
}
|
|
2884
|
+
if (added)
|
|
2885
|
+
pump();
|
|
2886
|
+
};
|
|
2817
2887
|
const inboxLoop = (async () => {
|
|
2818
2888
|
// Turns cut off by the last restart are reported before anything new runs.
|
|
2819
2889
|
for (const leftover of leftovers) {
|
|
@@ -2838,6 +2908,8 @@ export async function runBot(deps) {
|
|
|
2838
2908
|
// the work), alongside the poll rather than in front of it. Also during
|
|
2839
2909
|
// a hold: a report owed to the cloud has nothing to do with the model.
|
|
2840
2910
|
kickCompletionRetries();
|
|
2911
|
+
// A failed signals run's automatic retry, once due (no model turn).
|
|
2912
|
+
enqueueDueSignalsRetries();
|
|
2841
2913
|
// Holding after repeated model failures: no inbox pulls until the
|
|
2842
2914
|
// cooldown is over (the update watcher above keeps running meanwhile).
|
|
2843
2915
|
if (modelHoldUntil > now().getTime()) {
|
|
@@ -2942,6 +3014,8 @@ export async function runBot(deps) {
|
|
|
2942
3014
|
// An owner stop or a promotion while an update restart drains wins: the
|
|
2943
3015
|
// bot then exits 0 and stays down instead of being restarted.
|
|
2944
3016
|
cliUpdatePending = null;
|
|
3017
|
+
// A pending signals retry is in-process only: a stop cancels it.
|
|
3018
|
+
signalsRetries.clear();
|
|
2945
3019
|
if (stopping) {
|
|
2946
3020
|
// Already stopping (an earlier stop, a startup signal, or an update
|
|
2947
3021
|
// restart): still wait for the loops and finish, so this call ends the
|
|
@@ -2993,6 +3067,114 @@ const APP_SETUP_NOTE_PREFIX = "Setup note from the HQ desktop app:";
|
|
|
2993
3067
|
export function isAppSetupNote(text) {
|
|
2994
3068
|
return text.trimStart().startsWith(APP_SETUP_NOTE_PREFIX);
|
|
2995
3069
|
}
|
|
3070
|
+
/**
|
|
3071
|
+
* True for the ONE note the desktop app sends when its visual first run
|
|
3072
|
+
* closes: the handoff that names the assistant (`name` in `done`). Older
|
|
3073
|
+
* builds sent partial notes (an import, a company) with no `name`; those
|
|
3074
|
+
* fold in quietly (`APP_SETUP_NOTE_TURN_NOTE`).
|
|
3075
|
+
*/
|
|
3076
|
+
export function isFirstRunHandoffNote(text) {
|
|
3077
|
+
if (!isAppSetupNote(text))
|
|
3078
|
+
return false;
|
|
3079
|
+
const m = /Handoff from the app:\s*(\{.*\})/.exec(text);
|
|
3080
|
+
if (!m)
|
|
3081
|
+
return false;
|
|
3082
|
+
// The JSON ends at the `}` that closes the object; the note's own
|
|
3083
|
+
// sentences follow it on the same line. Braces inside string values (a
|
|
3084
|
+
// company name, a report path) do not count.
|
|
3085
|
+
let depth = 0;
|
|
3086
|
+
let end = -1;
|
|
3087
|
+
let inString = false;
|
|
3088
|
+
for (let i = 0; i < m[1].length; i += 1) {
|
|
3089
|
+
const ch = m[1][i];
|
|
3090
|
+
if (inString) {
|
|
3091
|
+
if (ch === "\\")
|
|
3092
|
+
i += 1;
|
|
3093
|
+
else if (ch === '"')
|
|
3094
|
+
inString = false;
|
|
3095
|
+
continue;
|
|
3096
|
+
}
|
|
3097
|
+
if (ch === '"')
|
|
3098
|
+
inString = true;
|
|
3099
|
+
else if (ch === "{")
|
|
3100
|
+
depth += 1;
|
|
3101
|
+
else if (ch === "}") {
|
|
3102
|
+
depth -= 1;
|
|
3103
|
+
if (depth === 0) {
|
|
3104
|
+
end = i + 1;
|
|
3105
|
+
break;
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
if (end < 0)
|
|
3110
|
+
return false;
|
|
3111
|
+
try {
|
|
3112
|
+
const handoff = JSON.parse(m[1].slice(0, end));
|
|
3113
|
+
return handoff.from === "desktop-visual-first-run" && Array.isArray(handoff.done) && handoff.done.includes("name");
|
|
3114
|
+
}
|
|
3115
|
+
catch {
|
|
3116
|
+
return false;
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
/**
|
|
3120
|
+
* Put ahead of the end-of-setup handoff note: it is the assistant's first
|
|
3121
|
+
* turn, so it always answers, and in seconds: everything it needs is in the
|
|
3122
|
+
* handoff, the Owner context block and the facts appended below it, so it
|
|
3123
|
+
* runs no command and reads no file first. The shape of that first message
|
|
3124
|
+
* is in the setup worker's instructions ("When the app hands you settled
|
|
3125
|
+
* steps").
|
|
3126
|
+
*/
|
|
3127
|
+
export const FIRST_RUN_HANDOFF_TURN_NOTE = "[This message is the HQ desktop app's end-of-setup handoff, not words from the person. The person has just left the app's setup screens " +
|
|
3128
|
+
"and is in this chat now, waiting for your first message, so always reply; never answer with {\"action\":\"no_reply\"}. " +
|
|
3129
|
+
"Reply now, from this message alone: the handoff, the Owner context block above, and the facts from this HQ listed below. " +
|
|
3130
|
+
"Run no command and read no file before this reply. Do not install, repair or check tools; that comes after they answer, and you will say first how long it takes. " +
|
|
3131
|
+
"Write the first message your instructions describe for this handoff: who you are and what you can help with, " +
|
|
3132
|
+
"then exactly what setup is left (or that none is) and one question.";
|
|
3133
|
+
/** Longest progress note quoted into the first-run handoff turn. */
|
|
3134
|
+
const FIRST_RUN_NOTE_MAX_CHARS = 4_000;
|
|
3135
|
+
/**
|
|
3136
|
+
* What the first-run turn would otherwise spend tool calls finding out: the
|
|
3137
|
+
* company folders on this computer and the progress note. Read here, once,
|
|
3138
|
+
* so the first reply needs no command. Never throws.
|
|
3139
|
+
*/
|
|
3140
|
+
export function firstRunHandoffFacts(hqRoot, memoryDir) {
|
|
3141
|
+
let companyFolders;
|
|
3142
|
+
try {
|
|
3143
|
+
companyFolders = fs
|
|
3144
|
+
.readdirSync(path.join(hqRoot, "companies"), { withFileTypes: true })
|
|
3145
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith("."))
|
|
3146
|
+
.map((e) => e.name)
|
|
3147
|
+
.sort();
|
|
3148
|
+
}
|
|
3149
|
+
catch {
|
|
3150
|
+
companyFolders = [];
|
|
3151
|
+
}
|
|
3152
|
+
let progressNote;
|
|
3153
|
+
try {
|
|
3154
|
+
const note = fs.readFileSync(path.join(memoryDir, "setup-progress.md"), "utf8").trim();
|
|
3155
|
+
progressNote = note ? note.slice(0, FIRST_RUN_NOTE_MAX_CHARS) : null;
|
|
3156
|
+
}
|
|
3157
|
+
catch {
|
|
3158
|
+
progressNote = null;
|
|
3159
|
+
}
|
|
3160
|
+
return { companyFolders, progressNote };
|
|
3161
|
+
}
|
|
3162
|
+
/** The whole first-run turn note: the fixed lead, then the facts. */
|
|
3163
|
+
export function firstRunHandoffTurnNote(facts) {
|
|
3164
|
+
const folders = facts.companyFolders.length ? facts.companyFolders.join(", ") : "none yet";
|
|
3165
|
+
const note = facts.progressNote ? `\n---\n${facts.progressNote}\n---` : " none (this is a new setup).";
|
|
3166
|
+
return `${FIRST_RUN_HANDOFF_TURN_NOTE} Facts from this HQ: company folders on this computer: ${folders}. Your progress note:${note}]`;
|
|
3167
|
+
}
|
|
3168
|
+
/** The setup bot's "still working" line: plain words, no dash, nothing about mechanics. */
|
|
3169
|
+
export const SETUP_WORKING_NOTICE_TEXT = "Still working on it. I'll post here as soon as I'm done.";
|
|
3170
|
+
/**
|
|
3171
|
+
* The setup bot's silence budget before that line. Half the general one:
|
|
3172
|
+
* the person is new and watching. The bot is told to say what it is about
|
|
3173
|
+
* to do and how long first (a wait heads-up, kept by the poster); the line
|
|
3174
|
+
* goes out when it said nothing, or when the work outlives the heads-up by
|
|
3175
|
+
* this much with nothing more said.
|
|
3176
|
+
*/
|
|
3177
|
+
export const SETUP_WORKING_NOTICE_MS = 30_000;
|
|
2996
3178
|
/** Put ahead of an app setup note: fold it in, and stay quiet unless it changes what to say. */
|
|
2997
3179
|
export const APP_SETUP_NOTE_TURN_NOTE = "[This message is a setup note from the HQ desktop app, not words from the person. Record what it settles. " +
|
|
2998
3180
|
"If you already asked the person something that is still unanswered and this note does not change it, reply with exactly " +
|
|
@@ -47,4 +47,5 @@ export declare function setupOfferAnsweredInWordsNote(tool: CodingTool): string;
|
|
|
47
47
|
export type SetupToolOfferAnswer = "kept_here" | "wrote";
|
|
48
48
|
/** How the person answered a pending offer: the button, or anything else. */
|
|
49
49
|
export declare function setupToolOfferAnswer(text: string, attachments: number): SetupToolOfferAnswer;
|
|
50
|
+
export declare function setupToolOfferPendingSection(tool: CodingTool, kickoff: string): string;
|
|
50
51
|
//# sourceMappingURL=setup-tool-offer.d.ts.map
|
|
@@ -88,4 +88,20 @@ export function setupOfferAnsweredInWordsNote(tool) {
|
|
|
88
88
|
export function setupToolOfferAnswer(text, attachments) {
|
|
89
89
|
return text === KEEP_GOING_HERE_REPLY && attachments === 0 ? "kept_here" : "wrote";
|
|
90
90
|
}
|
|
91
|
+
export function setupToolOfferPendingSection(tool, kickoff) {
|
|
92
|
+
const app = CODING_TOOL_NAMES[tool];
|
|
93
|
+
return [
|
|
94
|
+
"## The app offer waiting for an answer",
|
|
95
|
+
"",
|
|
96
|
+
`Your first message, sent by the HQ app, offered to continue setup in the ${app} app, because the person uses that app a lot on this computer. ` +
|
|
97
|
+
`It showed a button that opens the ${app} app in their HQ folder and a "${KEEP_GOING_HERE_REPLY}" button. Setup has not started and no first task has run.`,
|
|
98
|
+
"",
|
|
99
|
+
`- If the person replies exactly "${KEEP_GOING_HERE_REPLY}", setup happens here. Do not mention the offer; carry out this first task now:`,
|
|
100
|
+
"",
|
|
101
|
+
kickoff.trim(),
|
|
102
|
+
"",
|
|
103
|
+
`- If they write anything else and want to continue in the ${app} app, tell them in plain words to click the button above to open it. ` +
|
|
104
|
+
"Otherwise begin setup here as your instructions say for a first message.",
|
|
105
|
+
].join("\n");
|
|
106
|
+
}
|
|
91
107
|
//# sourceMappingURL=setup-tool-offer.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default-off rollout gate for the signals engine in the bot's built-in
|
|
3
|
+
* hot-signal-sync job (hq-cli #2306, hot-signal-sync US-006).
|
|
4
|
+
*
|
|
5
|
+
* Off (the default): a fired job runs the hot-channel sync alone, as before
|
|
6
|
+
* #2306. On: the job runs the full signals engine (`runSignals`) and replies in
|
|
7
|
+
* the storyboarded format. Turn it on for a company in the flag registry, or on
|
|
8
|
+
* one computer with `HQ_FLAG_CLI_BOT_SIGNALS_ENGINE_JOB=true` in the bot's
|
|
9
|
+
* environment.
|
|
10
|
+
*/
|
|
11
|
+
export declare const BOT_SIGNALS_ENGINE_JOB_FLAG = "cli.bot-signals-engine-job";
|
|
12
|
+
export declare function botSignalsEngineJobEnabled(): Promise<boolean>;
|
|
13
|
+
/**
|
|
14
|
+
* The engine a fired hot-signal-sync job gets: `engine` when the flag is on,
|
|
15
|
+
* otherwise undefined, so `runHotSignalSyncJob` takes its sync-only path.
|
|
16
|
+
*/
|
|
17
|
+
export declare function signalsEngineForJob<T>(engine: T, enabled?: () => Promise<boolean>): Promise<T | undefined>;
|
|
18
|
+
//# sourceMappingURL=signals-engine-flag.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default-off rollout gate for the signals engine in the bot's built-in
|
|
3
|
+
* hot-signal-sync job (hq-cli #2306, hot-signal-sync US-006).
|
|
4
|
+
*
|
|
5
|
+
* Off (the default): a fired job runs the hot-channel sync alone, as before
|
|
6
|
+
* #2306. On: the job runs the full signals engine (`runSignals`) and replies in
|
|
7
|
+
* the storyboarded format. Turn it on for a company in the flag registry, or on
|
|
8
|
+
* one computer with `HQ_FLAG_CLI_BOT_SIGNALS_ENGINE_JOB=true` in the bot's
|
|
9
|
+
* environment.
|
|
10
|
+
*/
|
|
11
|
+
export const BOT_SIGNALS_ENGINE_JOB_FLAG = "cli.bot-signals-engine-job";
|
|
12
|
+
export async function botSignalsEngineJobEnabled() {
|
|
13
|
+
try {
|
|
14
|
+
const { resolveProcessFlagGate } = await import("../flag-registry.js");
|
|
15
|
+
return resolveProcessFlagGate(BOT_SIGNALS_ENGINE_JOB_FLAG, { fallback: false }, () => false);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
const errorClass = error instanceof Error ? error.name : "unknown";
|
|
19
|
+
console.warn(`hq: Could not resolve ${BOT_SIGNALS_ENGINE_JOB_FLAG} (${errorClass}); the bot's signals job runs the sync only.`);
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The engine a fired hot-signal-sync job gets: `engine` when the flag is on,
|
|
25
|
+
* otherwise undefined, so `runHotSignalSyncJob` takes its sync-only path.
|
|
26
|
+
*/
|
|
27
|
+
export async function signalsEngineForJob(engine, enabled = botSignalsEngineJobEnabled) {
|
|
28
|
+
return (await enabled()) ? engine : undefined;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=signals-engine-flag.js.map
|
|
@@ -92,6 +92,9 @@ export interface SyncCoreRunning {
|
|
|
92
92
|
personal: boolean;
|
|
93
93
|
companies: ReadonlySet<string>;
|
|
94
94
|
}
|
|
95
|
+
export declare const FLEET_BOTS_UPDATE_UNIT = "fleet-bots-update";
|
|
96
|
+
export declare const FLEET_BOTS_UPDATE_INTERVAL_SEC: number;
|
|
97
|
+
export declare const FLEET_BOTS_UPDATE_INITIAL_DELAY_SEC = 120;
|
|
95
98
|
export declare const SYNC_CORE_RECONCILE_UNIT = "sync-core";
|
|
96
99
|
export declare const SYNC_CORE_RECONCILE_INTERVAL_SEC = 600;
|
|
97
100
|
export declare const SYNC_CORE_MANIFEST_UNIT = "sync-core-manifest";
|
|
@@ -148,6 +151,7 @@ export interface PlanContext {
|
|
|
148
151
|
/** ~/.hq-agent/kit.json exists. */
|
|
149
152
|
kitConfigured: boolean;
|
|
150
153
|
bots: readonly string[];
|
|
154
|
+
fleetBots?: readonly string[];
|
|
151
155
|
/** Name of a pre-daemon unit that still runs this unit id, or null. */
|
|
152
156
|
legacyOwner: (unitId: string) => string | null;
|
|
153
157
|
/** This CLI is a global install the updater can replace (not a project dependency). */
|
|
@@ -92,6 +92,9 @@ export function enabledServices(machineType, overrides) {
|
|
|
92
92
|
};
|
|
93
93
|
return SERVICE_NAMES.filter((s) => wanted(s) || implied(s));
|
|
94
94
|
}
|
|
95
|
+
export const FLEET_BOTS_UPDATE_UNIT = "fleet-bots-update";
|
|
96
|
+
export const FLEET_BOTS_UPDATE_INTERVAL_SEC = 30 * 60;
|
|
97
|
+
export const FLEET_BOTS_UPDATE_INITIAL_DELAY_SEC = 120;
|
|
95
98
|
export const SYNC_CORE_RECONCILE_UNIT = "sync-core";
|
|
96
99
|
export const SYNC_CORE_RECONCILE_INTERVAL_SEC = 600;
|
|
97
100
|
export const SYNC_CORE_MANIFEST_UNIT = "sync-core-manifest";
|
|
@@ -322,17 +325,34 @@ function planService(service, ctx) {
|
|
|
322
325
|
];
|
|
323
326
|
}
|
|
324
327
|
case "bots": {
|
|
328
|
+
const fleetWatch = (ctx.fleetBots ?? []).length > 0
|
|
329
|
+
? [
|
|
330
|
+
base(service, {
|
|
331
|
+
id: FLEET_BOTS_UPDATE_UNIT,
|
|
332
|
+
description: "moves HQ Fleet bots to the current runtime release while they are idle",
|
|
333
|
+
kind: "scheduled",
|
|
334
|
+
intervalSec: FLEET_BOTS_UPDATE_INTERVAL_SEC,
|
|
335
|
+
initialDelaySec: FLEET_BOTS_UPDATE_INITIAL_DELAY_SEC,
|
|
336
|
+
command: { type: "hq", args: ["daemon", "service", "fleet-bots-update"] },
|
|
337
|
+
env: { HQ_NO_UPDATE_CHECK: "1" },
|
|
338
|
+
blocked: firstBlock([[!ctx.signedIn, NOT_SIGNED_IN]]),
|
|
339
|
+
}),
|
|
340
|
+
]
|
|
341
|
+
: [];
|
|
325
342
|
if (ctx.bots.length === 0) {
|
|
326
|
-
return [base(service, { blocked: { state: "waiting", reason: "no local bots" } })];
|
|
343
|
+
return fleetWatch.length > 0 ? fleetWatch : [base(service, { blocked: { state: "waiting", reason: "no local bots" } })];
|
|
327
344
|
}
|
|
328
|
-
return
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
345
|
+
return [
|
|
346
|
+
...ctx.bots.map((name) => base(service, {
|
|
347
|
+
id: `bots:${name}`,
|
|
348
|
+
description: `runs the local bot ${name}`,
|
|
349
|
+
restart: "on-failure",
|
|
350
|
+
command: { type: "hq", args: ["bot", "run", name] },
|
|
351
|
+
// A bot drains on SIGUSR2: running turns finish, then it exits.
|
|
352
|
+
drainSignal: "SIGUSR2",
|
|
353
|
+
})),
|
|
354
|
+
...fleetWatch,
|
|
355
|
+
];
|
|
336
356
|
}
|
|
337
357
|
case "session-host":
|
|
338
358
|
return [
|
|
@@ -32,6 +32,12 @@ export interface LegacyProbeDeps {
|
|
|
32
32
|
launchdJobLoaded: (label: string) => boolean;
|
|
33
33
|
/** Whether the task exists and runs as the current user. Another user's task is never taken over. */
|
|
34
34
|
windowsTaskOwner: (taskName: string) => WindowsTaskOwner;
|
|
35
|
+
/**
|
|
36
|
+
* Mesh task name `hq mesh daemon install` recorded for this user. It is a
|
|
37
|
+
* per-user name (`ai.getindigo.hq-mesh-daemon.<user>`) when the shared name
|
|
38
|
+
* belonged to another account.
|
|
39
|
+
*/
|
|
40
|
+
windowsRecordedMeshTask?: () => string;
|
|
35
41
|
}
|
|
36
42
|
export declare function describeLegacyUnit(unit: LegacyUnit): string;
|
|
37
43
|
export declare function findLegacyUnits(deps: LegacyProbeDeps): LegacyUnit[];
|
|
@@ -100,16 +100,22 @@ function linuxUnits(deps) {
|
|
|
100
100
|
return units;
|
|
101
101
|
}
|
|
102
102
|
function windowsUnits(deps) {
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
unit
|
|
112
|
-
|
|
103
|
+
const shared = "ai.getindigo.hq-mesh-daemon";
|
|
104
|
+
const recorded = deps.windowsRecordedMeshTask?.();
|
|
105
|
+
const tasks = recorded && recorded !== shared ? [recorded, shared] : [shared];
|
|
106
|
+
const units = [];
|
|
107
|
+
for (const task of tasks) {
|
|
108
|
+
const owner = deps.windowsTaskOwner(task);
|
|
109
|
+
if (owner === "none")
|
|
110
|
+
continue;
|
|
111
|
+
const unit = { unitId: "mesh", service: "mesh", kind: "schtasks", name: task, migratable: owner === "mine" };
|
|
112
|
+
if (owner === "other")
|
|
113
|
+
unit.notMineReason = "it belongs to another Windows user";
|
|
114
|
+
if (owner === "unreadable")
|
|
115
|
+
unit.notMineReason = "HQ could not read which Windows user it belongs to";
|
|
116
|
+
units.push(unit);
|
|
117
|
+
}
|
|
118
|
+
return units;
|
|
113
119
|
}
|
|
114
120
|
export function findLegacyUnits(deps) {
|
|
115
121
|
if (deps.platform === "darwin")
|