@indigoai-us/hq-cli 5.345.171 → 5.345.173
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 +34 -0
- package/dist/command-catalog.generated.d.ts +74 -2
- package/dist/command-catalog.generated.js +88 -2
- package/dist/command-registration-plan.d.ts +7 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/bot.js +46 -8
- package/dist/commands/cloud.js +9 -5
- package/dist/commands/daemon.js +16 -1
- package/dist/commands/files.d.ts +1 -0
- package/dist/commands/files.js +22 -4
- package/dist/commands/sync-core.d.ts +22 -0
- package/dist/commands/sync-core.js +126 -0
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +278 -224
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/bot/fleet-local.d.ts +16 -1
- package/dist/lib/bot/fleet-local.js +99 -48
- package/dist/lib/bot/fleet-python.d.ts +47 -0
- package/dist/lib/bot/fleet-python.js +218 -0
- package/dist/lib/bot/index.d.ts +1 -0
- package/dist/lib/bot/index.js +1 -0
- package/dist/lib/bot/prompt.d.ts +6 -1
- package/dist/lib/bot/prompt.js +8 -3
- package/dist/lib/daemon/catalog.d.ts +12 -4
- package/dist/lib/daemon/catalog.js +70 -27
- package/dist/lib/daemon/config.d.ts +0 -7
- package/dist/lib/daemon/config.js +0 -32
- package/dist/lib/daemon/run.d.ts +5 -1
- package/dist/lib/daemon/run.js +48 -11
- package/dist/lib/daemon/supervisor.js +3 -1
- package/dist/lib/sync-core/engine-home.d.ts +45 -0
- package/dist/lib/sync-core/engine-home.js +194 -0
- package/dist/lib/sync-core/flag.d.ts +17 -0
- package/dist/lib/sync-core/flag.js +47 -0
- package/dist/lib/sync-core/reconcile.d.ts +31 -0
- package/dist/lib/sync-core/reconcile.js +217 -0
- package/dist/lib/sync-core/release.d.ts +32 -0
- package/dist/lib/sync-core/release.js +142 -0
- package/dist/lib/sync-core/rollout-state.d.ts +28 -0
- package/dist/lib/sync-core/rollout-state.js +115 -0
- package/dist/lib/sync-core/skip.d.ts +7 -0
- package/dist/lib/sync-core/skip.js +30 -0
- package/dist/lib/vault-sync-switches.d.ts +5 -0
- package/dist/lib/vault-sync-switches.js +43 -0
- package/dist/utils/remote-list-not-converging.d.ts +21 -0
- package/dist/utils/remote-list-not-converging.js +40 -0
- package/package.json +8 -8
- package/dist/lib/daemon/sync-core-canary.d.ts +0 -12
- package/dist/lib/daemon/sync-core-canary.js +0 -61
|
@@ -9,10 +9,13 @@
|
|
|
9
9
|
* with no entrypoint yet stay in the catalog as `unavailable` so status shows
|
|
10
10
|
* them instead of silently dropping them.
|
|
11
11
|
*/
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { syncCoreUnitId } from "../sync-core/engine-home.js";
|
|
12
14
|
import { SYNC_RUNNER_OWNER, syncRunnerUnleasedBlockReason } from "./sync-runner.js";
|
|
13
15
|
import { vaultSkipEnv } from "../vault-sync-switches.js";
|
|
14
16
|
export const SERVICE_NAMES = [
|
|
15
17
|
"sync",
|
|
18
|
+
"sync-core",
|
|
16
19
|
"mesh",
|
|
17
20
|
"heartbeat",
|
|
18
21
|
"inbox",
|
|
@@ -31,6 +34,7 @@ export function isServiceName(value) {
|
|
|
31
34
|
}
|
|
32
35
|
export const SERVICE_DESCRIPTIONS = {
|
|
33
36
|
sync: "keeps company vaults in sync with the cloud",
|
|
37
|
+
"sync-core": "moves vaults to the new sync engine when their company's switch is on",
|
|
34
38
|
mesh: "Work Mesh Live presence",
|
|
35
39
|
heartbeat: "reports this machine's status to HQ",
|
|
36
40
|
inbox: "picks up the agent's incoming messages and tasks",
|
|
@@ -46,9 +50,10 @@ export const SERVICE_DESCRIPTIONS = {
|
|
|
46
50
|
};
|
|
47
51
|
/** The services each machine type runs without being asked. */
|
|
48
52
|
export const DEFAULT_SERVICES = {
|
|
49
|
-
desktop: ["sync", "mesh", "heartbeat", "bots", "lanes-reconcile", "search-index", "updater", "housekeeping"],
|
|
53
|
+
desktop: ["sync", "sync-core", "mesh", "heartbeat", "bots", "lanes-reconcile", "search-index", "updater", "housekeeping"],
|
|
50
54
|
"self-hosted": [
|
|
51
55
|
"sync",
|
|
56
|
+
"sync-core",
|
|
52
57
|
"mesh",
|
|
53
58
|
"heartbeat",
|
|
54
59
|
"bots",
|
|
@@ -79,6 +84,9 @@ export function enabledServices(machineType, overrides) {
|
|
|
79
84
|
const defaults = DEFAULT_SERVICES[machineType];
|
|
80
85
|
return SERVICE_NAMES.filter((s) => (defaults.includes(s) || overrides.enable.includes(s)) && !overrides.disable.includes(s));
|
|
81
86
|
}
|
|
87
|
+
export const SYNC_CORE_RECONCILE_UNIT = "sync-core";
|
|
88
|
+
export const SYNC_CORE_RECONCILE_INTERVAL_SEC = 600;
|
|
89
|
+
export const SYNC_CORE_LEGACY_SETTLE_MS = 10 * 60 * 1000;
|
|
82
90
|
function base(service, over = {}) {
|
|
83
91
|
return {
|
|
84
92
|
id: service,
|
|
@@ -123,11 +131,12 @@ function planService(service, ctx) {
|
|
|
123
131
|
const agentMachine = ctx.machineType === "agent-kit" || ctx.machineType === "agent-box";
|
|
124
132
|
switch (service) {
|
|
125
133
|
case "sync": {
|
|
126
|
-
const
|
|
134
|
+
const running = ctx.syncCoreRunning;
|
|
135
|
+
const vaultSync = running && (running.personal || running.companies.size > 0)
|
|
127
136
|
? {
|
|
128
137
|
allOff: ctx.vaultSync?.allOff === true,
|
|
129
|
-
personalOff: true,
|
|
130
|
-
offCompanies: ctx.vaultSync?.offCompanies ??
|
|
138
|
+
personalOff: ctx.vaultSync?.personalOff === true || running.personal,
|
|
139
|
+
offCompanies: new Set([...(ctx.vaultSync?.offCompanies ?? []), ...running.companies]),
|
|
131
140
|
}
|
|
132
141
|
: ctx.vaultSync;
|
|
133
142
|
let ownerBlock;
|
|
@@ -181,33 +190,67 @@ function planService(service, ctx) {
|
|
|
181
190
|
blocked: commonBlock,
|
|
182
191
|
})
|
|
183
192
|
: unavailable(service, "the hq-cloud sync runner is missing from this CLI install");
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
...(ctx.syncCoreCanary.vaultUid ? { HQ_SYNC_CANARY_VAULT: ctx.syncCoreCanary.vaultUid } : {}),
|
|
199
|
-
...(ctx.syncCoreCanary.canaryPath ? { HQ_SYNC_CANARY_PATH: ctx.syncCoreCanary.canaryPath } : {}),
|
|
200
|
-
...(ctx.syncCoreCanary.fullVault ? { HQ_SYNC_FULL_VAULT: "1" } : {}),
|
|
193
|
+
return [unit];
|
|
194
|
+
}
|
|
195
|
+
case "sync-core": {
|
|
196
|
+
if (ctx.platform !== "darwin" && ctx.platform !== "linux") {
|
|
197
|
+
return [unavailable(service, "the new sync engine runs on macOS and Linux only")];
|
|
198
|
+
}
|
|
199
|
+
const reconcile = base(service, {
|
|
200
|
+
id: SYNC_CORE_RECONCILE_UNIT,
|
|
201
|
+
kind: "scheduled",
|
|
202
|
+
intervalSec: SYNC_CORE_RECONCILE_INTERVAL_SEC,
|
|
203
|
+
initialDelaySec: 30,
|
|
204
|
+
command: {
|
|
205
|
+
type: "hq",
|
|
206
|
+
args: ["daemon", "service", "sync-core-reconcile", ...(ctx.hqRoot ? ["--hq-root", ctx.hqRoot] : [])],
|
|
201
207
|
},
|
|
202
|
-
|
|
208
|
+
env: { HQ_NO_UPDATE_CHECK: "1" },
|
|
203
209
|
blocked: firstBlock([
|
|
204
|
-
[
|
|
205
|
-
[
|
|
206
|
-
[ctx.vaultSync?.personalOff !== true, "turn the personal vault off in the current sync before starting the sync-core canary"],
|
|
207
|
-
[ctx.legacySyncReadyForCore !== true, "waiting for legacy sync to run its company-only plan"],
|
|
210
|
+
[!ctx.hqRoot, NO_HQ_ROOT],
|
|
211
|
+
[!ctx.signedIn, NOT_SIGNED_IN],
|
|
208
212
|
]),
|
|
209
213
|
});
|
|
210
|
-
|
|
214
|
+
const now = ctx.now ?? Date.now();
|
|
215
|
+
const engines = (ctx.syncCoreVaults ?? []).map((vault) => {
|
|
216
|
+
const full = vault.phase === "full";
|
|
217
|
+
const legacyOff = vault.kind === "personal"
|
|
218
|
+
? ctx.vaultSync?.personalOff === true
|
|
219
|
+
: ctx.vaultSync?.offCompanies.has(vault.slug ?? "") === true;
|
|
220
|
+
const offSince = vault.legacyOffSince ? Date.parse(vault.legacyOffSince) : Number.NaN;
|
|
221
|
+
const settled = Number.isFinite(offSince) && now - offSince >= SYNC_CORE_LEGACY_SETTLE_MS;
|
|
222
|
+
const legacyReady = ctx.legacySyncReadyForCore === true && (!ctx.desktopAppManagesSync || settled);
|
|
223
|
+
const what = vault.kind === "personal" ? "the personal vault" : `the ${vault.slug} vault`;
|
|
224
|
+
return base(service, {
|
|
225
|
+
id: syncCoreUnitId(vault.uid),
|
|
226
|
+
description: full
|
|
227
|
+
? `keeps ${what} in sync with the new sync engine (${vault.release})`
|
|
228
|
+
: `warms the new sync engine's index for ${what} (${vault.release}, read-only)`,
|
|
229
|
+
command: { type: "exec", file: vault.binaryPath, args: ["start", "--loop"] },
|
|
230
|
+
env: {
|
|
231
|
+
HOME: vault.engineHome,
|
|
232
|
+
HQ_STATE_DIR: path.join(ctx.home, ".hq"),
|
|
233
|
+
HQ_SYNC_ENGINE: "rust-test",
|
|
234
|
+
HQ_SYNC_MODE: full ? "full" : "readonly",
|
|
235
|
+
...(full ? { HQ_SYNC_FULL_VAULT: "1" } : {}),
|
|
236
|
+
HQ_SYNC_PROTOCOL: "1",
|
|
237
|
+
HQ_SYNC_CORE_RELEASE: vault.release,
|
|
238
|
+
},
|
|
239
|
+
heartbeatTimeoutMs: 180_000,
|
|
240
|
+
blocked: firstBlock([
|
|
241
|
+
[ctx.syncPaused, "sync is paused (run `hq daemon sync resume`)"],
|
|
242
|
+
[ctx.vaultSync?.allOff === true, "cloud sync is off for every vault on this computer (turn it back on in HQ Settings > Sync)"],
|
|
243
|
+
[!ctx.hqRoot, NO_HQ_ROOT],
|
|
244
|
+
[!ctx.signedIn, NOT_SIGNED_IN],
|
|
245
|
+
[!!vault.blockedReason, vault.blockedReason ?? ""],
|
|
246
|
+
[full && !legacyOff, `waiting for the current sync to be turned off for ${what}`],
|
|
247
|
+
[full && !legacyReady, ctx.legacySyncReadyForCore !== true
|
|
248
|
+
? "waiting for the current sync to run its plan without this vault"
|
|
249
|
+
: "waiting for the desktop app's sync to pick up the off-switch"],
|
|
250
|
+
]),
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
return [reconcile, ...engines];
|
|
211
254
|
}
|
|
212
255
|
case "mesh": {
|
|
213
256
|
const args = ["mesh", "daemon", "run", ...(ctx.hqRoot ? ["--hq-root", ctx.hqRoot] : [])];
|
|
@@ -18,19 +18,12 @@ export interface DaemonConfig {
|
|
|
18
18
|
instantSyncEnabled?: boolean;
|
|
19
19
|
/** Explicit desktop handoff; absent values preserve the legacy per-machine owner. */
|
|
20
20
|
syncOwner?: SyncOwner;
|
|
21
|
-
syncCoreCanary?: SyncCoreCanaryConfig;
|
|
22
|
-
syncCoreCanaryError?: string;
|
|
23
|
-
syncCoreCanaryInvalid?: unknown;
|
|
24
21
|
/** Services turned on here beyond the machine type's defaults. */
|
|
25
22
|
enable: string[];
|
|
26
23
|
/** Default services turned off here. */
|
|
27
24
|
disable: string[];
|
|
28
25
|
}
|
|
29
26
|
export type SyncOwner = "daemon" | "desktop-app";
|
|
30
|
-
export interface SyncCoreCanaryConfig {
|
|
31
|
-
binaryPath: string;
|
|
32
|
-
vaultUid: string;
|
|
33
|
-
}
|
|
34
27
|
export declare function effectiveSyncOwner(config: Pick<DaemonConfig, "syncOwner">, machineType: MachineType): SyncOwner;
|
|
35
28
|
export declare function readDaemonConfig(file: string): DaemonConfig;
|
|
36
29
|
export declare function writeDaemonConfig(file: string, config: DaemonConfig): void;
|
|
@@ -19,27 +19,6 @@ function emptyDaemonConfig() {
|
|
|
19
19
|
function stringList(value) {
|
|
20
20
|
return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
21
21
|
}
|
|
22
|
-
function syncCoreCanary(value, file) {
|
|
23
|
-
if (value === undefined)
|
|
24
|
-
return {};
|
|
25
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
26
|
-
return { error: `hq daemon config at ${file} has invalid syncCoreCanary`, invalid: value };
|
|
27
|
-
}
|
|
28
|
-
const candidate = value;
|
|
29
|
-
if (typeof candidate.binaryPath !== "string" || !path.isAbsolute(candidate.binaryPath)) {
|
|
30
|
-
return {
|
|
31
|
-
error: `hq daemon config at ${file} has invalid syncCoreCanary: binaryPath must be absolute`,
|
|
32
|
-
invalid: value,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
if (typeof candidate.vaultUid !== "string" || !/^prs_[A-Za-z0-9][A-Za-z0-9_-]*$/.test(candidate.vaultUid)) {
|
|
36
|
-
return {
|
|
37
|
-
error: `hq daemon config at ${file} has invalid syncCoreCanary: vaultUid must name one personal vault`,
|
|
38
|
-
invalid: value,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
return { config: { binaryPath: candidate.binaryPath, vaultUid: candidate.vaultUid } };
|
|
42
|
-
}
|
|
43
22
|
export function readDaemonConfig(file) {
|
|
44
23
|
let text;
|
|
45
24
|
try {
|
|
@@ -72,13 +51,6 @@ export function readDaemonConfig(file) {
|
|
|
72
51
|
config.machineType = obj.machineType;
|
|
73
52
|
if (obj.syncOwner === "daemon" || obj.syncOwner === "desktop-app")
|
|
74
53
|
config.syncOwner = obj.syncOwner;
|
|
75
|
-
const parsedCanary = syncCoreCanary(obj.syncCoreCanary, file);
|
|
76
|
-
if (parsedCanary.config)
|
|
77
|
-
config.syncCoreCanary = parsedCanary.config;
|
|
78
|
-
if (parsedCanary.error)
|
|
79
|
-
config.syncCoreCanaryError = parsedCanary.error;
|
|
80
|
-
if (parsedCanary.error)
|
|
81
|
-
config.syncCoreCanaryInvalid = parsedCanary.invalid;
|
|
82
54
|
if (typeof obj.hqRoot === "string" && obj.hqRoot.trim())
|
|
83
55
|
config.hqRoot = obj.hqRoot;
|
|
84
56
|
if (typeof obj.hqCloudVersion === "string" && validSemver(obj.hqCloudVersion) === obj.hqCloudVersion) {
|
|
@@ -101,10 +73,6 @@ export function writeDaemonConfig(file, config) {
|
|
|
101
73
|
ordered.instantSyncEnabled = config.instantSyncEnabled;
|
|
102
74
|
if (config.syncOwner)
|
|
103
75
|
ordered.syncOwner = config.syncOwner;
|
|
104
|
-
if (config.syncCoreCanary)
|
|
105
|
-
ordered.syncCoreCanary = config.syncCoreCanary;
|
|
106
|
-
else if (config.syncCoreCanaryError)
|
|
107
|
-
ordered.syncCoreCanary = config.syncCoreCanaryInvalid;
|
|
108
76
|
ordered.enable = config.enable;
|
|
109
77
|
ordered.disable = config.disable;
|
|
110
78
|
fs.writeFileSync(tmp, `${JSON.stringify(ordered, null, 2)}\n`, { mode: 0o600 });
|
package/dist/lib/daemon/run.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { type HostApp, type Supervisor } from "./supervisor.js";
|
|
|
14
14
|
export declare const LOG_ROTATE_BYTES: number;
|
|
15
15
|
/** Exit code for "restart me": non-zero so every service manager relaunches (Windows tasks only restart on failure). */
|
|
16
16
|
export declare const RESTART_EXIT_CODE = 75;
|
|
17
|
-
export declare function planUnitsWithSyncCoreInterlock(services: readonly ServiceName[], context: PlanContext, supervisor: Pick<Supervisor, "runningPlanKey"> | null | undefined): UnitPlan[];
|
|
17
|
+
export declare function planUnitsWithSyncCoreInterlock(services: readonly ServiceName[], context: PlanContext, supervisor: Pick<Supervisor, "runningPlanKey" | "state"> | null | undefined): UnitPlan[];
|
|
18
18
|
/** A bounded shutdown deadline that can account for sync push persistence. */
|
|
19
19
|
export declare function createShutdownCap(): {
|
|
20
20
|
promise: Promise<"timeout">;
|
|
@@ -94,6 +94,10 @@ export declare function daemonSyncUnitIsPresent(units: readonly {
|
|
|
94
94
|
export declare function resolveSyncRunnerPath(): string | null;
|
|
95
95
|
/** The hosted desktop app uses this env value when planning daemon services. */
|
|
96
96
|
export declare function daemonInstantSyncEnabled(env: NodeJS.ProcessEnv, configured?: boolean): boolean;
|
|
97
|
+
/**
|
|
98
|
+
* The bots the daemon runs itself. A bot on the HQ Fleet Runtime has its own
|
|
99
|
+
* background service, so it is not one of them.
|
|
100
|
+
*/
|
|
97
101
|
export declare function listLocalBots(root?: string): string[];
|
|
98
102
|
export declare function windowsTaskExists(taskName: string): boolean;
|
|
99
103
|
export declare function probeLegacyUnits(platform: NodeJS.Platform, home: string): LegacyUnit[];
|
package/dist/lib/daemon/run.js
CHANGED
|
@@ -18,11 +18,12 @@ import { anywhereRuntimeAllowed, readHqAnywherePersonSetting, stickyPersonSettin
|
|
|
18
18
|
import { currentPackageUseLeaseInstall, currentPackageUseUpdateRequested } from "../../utils/package-use-lease.js";
|
|
19
19
|
import { isLocalDependencyInstall, resolveRunningInstall } from "../../utils/version-gate.js";
|
|
20
20
|
import { agentKitPaths } from "../agent-kit/paths.js";
|
|
21
|
-
import { readBotConfig } from "../bot/config.js";
|
|
21
|
+
import { HQ_FLEET_RUNTIME_ENGINE_ID, readBotConfig } from "../bot/config.js";
|
|
22
22
|
import { botsRoot } from "../bot/paths.js";
|
|
23
23
|
import { CLAUDE_RUNNER_SCRIPT, CODEX_RUNNER_SCRIPT, enabledServices, planUnits } from "./catalog.js";
|
|
24
24
|
import { effectiveSyncOwner, readDaemonConfig } from "./config.js";
|
|
25
|
-
import {
|
|
25
|
+
import { inspectEngineHome } from "../sync-core/engine-home.js";
|
|
26
|
+
import { readSyncCoreRolloutState } from "../sync-core/rollout-state.js";
|
|
26
27
|
import { assertPersonalServiceSessionCapability, readRemoteControlPoolsConfig, remoteControlPoolSecretsEnv, } from "./remote-control-pools.js";
|
|
27
28
|
import { loadLanesConfig } from "../lanes/config.js";
|
|
28
29
|
import { daemonLanesReconcileHandler } from "../lanes/daemon-reconcile.js";
|
|
@@ -64,17 +65,48 @@ export const LOG_ROTATE_BYTES = 5 * 1024 * 1024;
|
|
|
64
65
|
export const RESTART_EXIT_CODE = 75;
|
|
65
66
|
const NON_SYNC_SHUTDOWN_CAP_MS = STOP_ESCALATION_MS + 5_000;
|
|
66
67
|
const SYNC_RUNNER_SHUTDOWN_CAP_MS = STOP_ESCALATION_MS + SYNC_RUNNER_PERSIST_GRACE_MS + 5_000;
|
|
68
|
+
function syncCoreVaultPlans(home, hqRoot, platform) {
|
|
69
|
+
const state = readSyncCoreRolloutState(home);
|
|
70
|
+
if (!state)
|
|
71
|
+
return [];
|
|
72
|
+
return state.vaults.map((vault) => inspectEngineHome(home, hqRoot, {
|
|
73
|
+
uid: vault.uid,
|
|
74
|
+
kind: vault.kind,
|
|
75
|
+
...(vault.slug ? { slug: vault.slug } : {}),
|
|
76
|
+
phase: vault.phase,
|
|
77
|
+
engineHome: vault.engineHome,
|
|
78
|
+
binaryPath: state.binaryPath,
|
|
79
|
+
release: state.release,
|
|
80
|
+
...(vault.legacyOffSince ? { legacyOffSince: vault.legacyOffSince } : {}),
|
|
81
|
+
}, platform));
|
|
82
|
+
}
|
|
83
|
+
const rememberedSyncCoreVaults = new Map();
|
|
67
84
|
export function planUnitsWithSyncCoreInterlock(services, context, supervisor) {
|
|
68
|
-
const
|
|
85
|
+
const companies = new Set();
|
|
86
|
+
let personal = false;
|
|
87
|
+
for (const vault of context.syncCoreVaults ?? []) {
|
|
88
|
+
rememberedSyncCoreVaults.set(vault.uid, { kind: vault.kind, slug: vault.slug, full: vault.phase === "full" });
|
|
89
|
+
}
|
|
90
|
+
for (const unit of supervisor?.state().units ?? []) {
|
|
91
|
+
if (!unit.id.startsWith("sync-core:") || supervisor?.runningPlanKey(unit.id) === undefined)
|
|
92
|
+
continue;
|
|
93
|
+
const vault = rememberedSyncCoreVaults.get(unit.id.slice("sync-core:".length));
|
|
94
|
+
if (!vault?.full)
|
|
95
|
+
continue;
|
|
96
|
+
if (vault.kind === "personal")
|
|
97
|
+
personal = true;
|
|
98
|
+
else if (vault.slug)
|
|
99
|
+
companies.add(vault.slug);
|
|
100
|
+
}
|
|
69
101
|
const transitionContext = {
|
|
70
102
|
...context,
|
|
71
|
-
|
|
103
|
+
syncCoreRunning: { personal, companies },
|
|
72
104
|
legacySyncReadyForCore: false,
|
|
73
105
|
};
|
|
74
106
|
const waiting = planUnits(services, transitionContext);
|
|
75
107
|
const legacy = waiting.find((unit) => unit.id === "sync");
|
|
76
108
|
const legacySyncReadyForCore = legacy !== undefined
|
|
77
|
-
&& supervisor?.runningPlanKey("sync") === planKey(legacy, CLI_VERSION);
|
|
109
|
+
&& (legacy.blocked !== undefined || supervisor?.runningPlanKey("sync") === planKey(legacy, CLI_VERSION));
|
|
78
110
|
return planUnits(services, { ...transitionContext, legacySyncReadyForCore });
|
|
79
111
|
}
|
|
80
112
|
/** A bounded shutdown deadline that can account for sync push persistence. */
|
|
@@ -260,12 +292,21 @@ export function resolveSyncRunnerPath() {
|
|
|
260
292
|
export function daemonInstantSyncEnabled(env, configured) {
|
|
261
293
|
return configured ?? env.HQ_DAEMON_INSTANT_SYNC !== "0";
|
|
262
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* The bots the daemon runs itself. A bot on the HQ Fleet Runtime has its own
|
|
297
|
+
* background service, so it is not one of them.
|
|
298
|
+
*/
|
|
263
299
|
export function listLocalBots(root = botsRoot()) {
|
|
264
300
|
if (!fs.existsSync(root))
|
|
265
301
|
return [];
|
|
266
302
|
return fs
|
|
267
303
|
.readdirSync(root, { withFileTypes: true })
|
|
268
|
-
.filter((d) =>
|
|
304
|
+
.filter((d) => {
|
|
305
|
+
if (!d.isDirectory())
|
|
306
|
+
return false;
|
|
307
|
+
const config = readBotConfig(path.join(root, d.name));
|
|
308
|
+
return config !== null && config.engine?.id !== HQ_FLEET_RUNTIME_ENGINE_ID;
|
|
309
|
+
})
|
|
269
310
|
.map((d) => d.name)
|
|
270
311
|
.sort();
|
|
271
312
|
}
|
|
@@ -978,11 +1019,7 @@ export async function runDaemon(opts) {
|
|
|
978
1019
|
return switches;
|
|
979
1020
|
})(),
|
|
980
1021
|
syncOwner,
|
|
981
|
-
|
|
982
|
-
? { blockedReason: config.syncCoreCanaryError }
|
|
983
|
-
: config.syncCoreCanary
|
|
984
|
-
? inspectSyncCoreCanary(home, hqRoot, config.syncCoreCanary, platform)
|
|
985
|
-
: undefined,
|
|
1022
|
+
syncCoreVaults: syncCoreVaultPlans(home, hqRoot, platform),
|
|
986
1023
|
desktopAppManagesSync: appManagesSync,
|
|
987
1024
|
syncRunnerProcesses,
|
|
988
1025
|
syncRunnerOwnerPid,
|
|
@@ -87,7 +87,7 @@ function keyHash(key) {
|
|
|
87
87
|
return createHash("sha256").update(key).digest("hex");
|
|
88
88
|
}
|
|
89
89
|
function recoverableHold(plan, hold) {
|
|
90
|
-
return plan.kind === "long-running" && (plan.service === "sync" || plan.service === "mesh")
|
|
90
|
+
return plan.kind === "long-running" && (plan.service === "sync" || plan.service === "sync-core" || plan.service === "mesh")
|
|
91
91
|
&& hold?.kind === "exhausted" && hold.cause === "transient";
|
|
92
92
|
}
|
|
93
93
|
function heldReason(unitId, hold) {
|
|
@@ -388,6 +388,8 @@ export function createSupervisor(deps) {
|
|
|
388
388
|
const cause = classifyExit(evidence);
|
|
389
389
|
if (cause.kind !== "permanent")
|
|
390
390
|
return false;
|
|
391
|
+
if (slot.plan.service === "sync-core" && cause.cause === "bad-config")
|
|
392
|
+
return false;
|
|
391
393
|
slot.failures += 1;
|
|
392
394
|
holdUnit(slot, "permanent", cause.cause, cause.detail);
|
|
393
395
|
return true;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type SyncCoreVaultKind = "personal" | "company";
|
|
2
|
+
export type SyncCoreVaultPhase = "warming" | "full";
|
|
3
|
+
export interface SyncCoreVault {
|
|
4
|
+
uid: string;
|
|
5
|
+
kind: SyncCoreVaultKind;
|
|
6
|
+
slug?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SyncCoreVaultPlan extends SyncCoreVault {
|
|
9
|
+
phase: SyncCoreVaultPhase;
|
|
10
|
+
engineHome: string;
|
|
11
|
+
binaryPath: string;
|
|
12
|
+
release: string;
|
|
13
|
+
legacyOffSince?: string;
|
|
14
|
+
blockedReason?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare const SYNC_CORE_REQUESTS_PER_DAY = 20000;
|
|
17
|
+
export declare function syncCoreEngineHome(home: string, vaultUid: string): string;
|
|
18
|
+
export declare function syncCoreUnitId(vaultUid: string): string;
|
|
19
|
+
export declare function isSyncCoreVaultUid(value: unknown): value is string;
|
|
20
|
+
export declare function engineConfigToml(vault: SyncCoreVault, hqRoot: string, phase: SyncCoreVaultPhase): string;
|
|
21
|
+
export interface WriteEngineHomeOptions {
|
|
22
|
+
home: string;
|
|
23
|
+
hqRoot: string;
|
|
24
|
+
vault: SyncCoreVault;
|
|
25
|
+
phase: SyncCoreVaultPhase;
|
|
26
|
+
tokenFile: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function writeEngineHome(opts: WriteEngineHomeOptions): {
|
|
29
|
+
engineHome: string;
|
|
30
|
+
changed: boolean;
|
|
31
|
+
};
|
|
32
|
+
export interface SyncCorePlanFigures {
|
|
33
|
+
at: string;
|
|
34
|
+
uploads: number;
|
|
35
|
+
downloads: number;
|
|
36
|
+
deletes: number;
|
|
37
|
+
conflicts: number;
|
|
38
|
+
remoteRows: number;
|
|
39
|
+
localFiles: number;
|
|
40
|
+
}
|
|
41
|
+
export declare function engineReadOnlyPlan(engineHome: string, vaultUid: string): SyncCorePlanFigures | null;
|
|
42
|
+
export declare function clearEngineStatus(home: string, vaultUid: string): void;
|
|
43
|
+
export declare function removeEngineOwnership(home: string, vaultUid: string): void;
|
|
44
|
+
export declare function inspectEngineHome(home: string, hqRoot: string | undefined, plan: SyncCoreVaultPlan, platform: NodeJS.Platform): SyncCoreVaultPlan;
|
|
45
|
+
//# sourceMappingURL=engine-home.d.ts.map
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { parse as parseToml } from "smol-toml";
|
|
4
|
+
export const SYNC_CORE_REQUESTS_PER_DAY = 20_000;
|
|
5
|
+
export function syncCoreEngineHome(home, vaultUid) {
|
|
6
|
+
return path.join(home, ".hq", "sync-core", "engines", vaultUid);
|
|
7
|
+
}
|
|
8
|
+
export function syncCoreUnitId(vaultUid) {
|
|
9
|
+
return `sync-core:${vaultUid}`;
|
|
10
|
+
}
|
|
11
|
+
export function isSyncCoreVaultUid(value) {
|
|
12
|
+
return typeof value === "string" && /^(prs|cmp)_[A-Za-z0-9][A-Za-z0-9_-]*$/.test(value);
|
|
13
|
+
}
|
|
14
|
+
export function engineConfigToml(vault, hqRoot, phase) {
|
|
15
|
+
const lines = [
|
|
16
|
+
"[sync]",
|
|
17
|
+
'engine = "rust-test"',
|
|
18
|
+
`mode = ${phase === "full" ? '"full"' : '"readonly"'}`,
|
|
19
|
+
...(phase === "full" ? ["full_vault = true"] : []),
|
|
20
|
+
`vaults = [${JSON.stringify(vault.uid)}]`,
|
|
21
|
+
`hq_root = ${JSON.stringify(hqRoot)}`,
|
|
22
|
+
`requests_per_day = ${SYNC_CORE_REQUESTS_PER_DAY}`,
|
|
23
|
+
];
|
|
24
|
+
if (vault.kind === "company") {
|
|
25
|
+
lines.push("[sync.roots]", `${vault.uid} = ${JSON.stringify(`companies/${vault.slug}`)}`);
|
|
26
|
+
}
|
|
27
|
+
return `${lines.join("\n")}\n`;
|
|
28
|
+
}
|
|
29
|
+
function replaceSymlink(link, target) {
|
|
30
|
+
try {
|
|
31
|
+
if (fs.lstatSync(link).isSymbolicLink() && fs.readlinkSync(link) === target)
|
|
32
|
+
return;
|
|
33
|
+
fs.rmSync(link, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error.code !== "ENOENT")
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
fs.symlinkSync(target, link);
|
|
40
|
+
}
|
|
41
|
+
function writeIfChanged(file, text) {
|
|
42
|
+
try {
|
|
43
|
+
if (fs.readFileSync(file, "utf8") === text)
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (error.code !== "ENOENT")
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
51
|
+
fs.writeFileSync(tmp, text, { mode: 0o600 });
|
|
52
|
+
fs.renameSync(tmp, file);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
export function writeEngineHome(opts) {
|
|
56
|
+
const engineHome = syncCoreEngineHome(opts.home, opts.vault.uid);
|
|
57
|
+
const hq = path.join(engineHome, ".hq");
|
|
58
|
+
const root = path.join(hq, "sync-core");
|
|
59
|
+
fs.mkdirSync(path.join(root, "owned"), { recursive: true, mode: 0o700 });
|
|
60
|
+
replaceSymlink(path.join(hq, "menubar.json"), path.join(opts.home, ".hq", "menubar.json"));
|
|
61
|
+
replaceSymlink(path.join(hq, "work-mesh"), path.join(opts.home, ".hq", "work-mesh"));
|
|
62
|
+
replaceSymlink(path.join(hq, "cognito-tokens.json"), opts.tokenFile);
|
|
63
|
+
const configChanged = writeIfChanged(path.join(root, "config.toml"), engineConfigToml(opts.vault, opts.hqRoot, opts.phase));
|
|
64
|
+
const ownedPath = path.join(root, "owned", `${opts.vault.uid}.json`);
|
|
65
|
+
let ownedChanged = false;
|
|
66
|
+
if (opts.phase === "full") {
|
|
67
|
+
ownedChanged = writeIfChanged(ownedPath, `${JSON.stringify({ vault: opts.vault.uid })}\n`);
|
|
68
|
+
}
|
|
69
|
+
else if (fs.existsSync(ownedPath)) {
|
|
70
|
+
fs.rmSync(ownedPath, { force: true });
|
|
71
|
+
ownedChanged = true;
|
|
72
|
+
}
|
|
73
|
+
return { engineHome, changed: configChanged || ownedChanged };
|
|
74
|
+
}
|
|
75
|
+
function count(value) {
|
|
76
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
|
|
77
|
+
}
|
|
78
|
+
export function engineReadOnlyPlan(engineHome, vaultUid) {
|
|
79
|
+
let status;
|
|
80
|
+
try {
|
|
81
|
+
status = record(JSON.parse(fs.readFileSync(path.join(engineHome, ".hq", "sync-core", "status.json"), "utf8")));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
if (record(status?.mode)?.value !== "readonly")
|
|
87
|
+
return null;
|
|
88
|
+
const lastPlan = record(status?.last_plan);
|
|
89
|
+
if (!lastPlan || typeof lastPlan.at_ms !== "number" || lastPlan.at_ms <= 0)
|
|
90
|
+
return null;
|
|
91
|
+
const vaults = Array.isArray(lastPlan.vaults) ? lastPlan.vaults : [];
|
|
92
|
+
const run = vaults.map(record).find((v) => v?.vault === vaultUid);
|
|
93
|
+
if (!run)
|
|
94
|
+
return null;
|
|
95
|
+
const errors = Array.isArray(lastPlan.errors) ? lastPlan.errors : [];
|
|
96
|
+
if (errors.some((e) => {
|
|
97
|
+
const vault = record(e)?.vault;
|
|
98
|
+
return vault === vaultUid || typeof vault !== "string";
|
|
99
|
+
}))
|
|
100
|
+
return null;
|
|
101
|
+
const ops = record(record(run.plan)?.ops_by_action) ?? {};
|
|
102
|
+
return {
|
|
103
|
+
at: new Date(lastPlan.at_ms).toISOString(),
|
|
104
|
+
uploads: count(ops.upload),
|
|
105
|
+
downloads: count(ops.download),
|
|
106
|
+
deletes: count(ops.delete_local) + count(ops.delete_remote),
|
|
107
|
+
conflicts: count(ops.conflict) + count(ops.merge),
|
|
108
|
+
remoteRows: count(record(run.remote)?.rows),
|
|
109
|
+
localFiles: count(record(run.local)?.files),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export function clearEngineStatus(home, vaultUid) {
|
|
113
|
+
fs.rmSync(path.join(syncCoreEngineHome(home, vaultUid), ".hq", "sync-core", "status.json"), { force: true });
|
|
114
|
+
}
|
|
115
|
+
export function removeEngineOwnership(home, vaultUid) {
|
|
116
|
+
fs.rmSync(path.join(syncCoreEngineHome(home, vaultUid), ".hq", "sync-core", "owned", `${vaultUid}.json`), { force: true });
|
|
117
|
+
}
|
|
118
|
+
function record(value) {
|
|
119
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
120
|
+
}
|
|
121
|
+
function blocked(plan, blockedReason) {
|
|
122
|
+
return { ...plan, blockedReason };
|
|
123
|
+
}
|
|
124
|
+
export function inspectEngineHome(home, hqRoot, plan, platform) {
|
|
125
|
+
if (platform !== "darwin" && platform !== "linux")
|
|
126
|
+
return blocked(plan, "the sync engine requires macOS or Linux");
|
|
127
|
+
try {
|
|
128
|
+
if (!fs.statSync(plan.binaryPath).isFile())
|
|
129
|
+
return blocked(plan, "the sync engine binary is missing");
|
|
130
|
+
fs.accessSync(plan.binaryPath, fs.constants.X_OK);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return blocked(plan, "the sync engine binary is missing or not executable");
|
|
134
|
+
}
|
|
135
|
+
if (plan.engineHome !== syncCoreEngineHome(home, plan.uid)) {
|
|
136
|
+
return blocked(plan, `the engine home for ${plan.uid} is not under this user's sync-core folder`);
|
|
137
|
+
}
|
|
138
|
+
const configPath = path.join(plan.engineHome, ".hq", "sync-core", "config.toml");
|
|
139
|
+
let sync;
|
|
140
|
+
try {
|
|
141
|
+
sync = record(record(parseToml(fs.readFileSync(configPath, "utf8")))?.sync);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return blocked(plan, `the engine config at ${configPath} is missing or invalid`);
|
|
145
|
+
}
|
|
146
|
+
if (!sync || sync.engine !== "rust-test")
|
|
147
|
+
return blocked(plan, "the engine config must select engine rust-test");
|
|
148
|
+
if (plan.phase === "full" && (sync.mode !== "full" || sync.full_vault !== true)) {
|
|
149
|
+
return blocked(plan, "the engine config must select engine rust-test in full-vault mode");
|
|
150
|
+
}
|
|
151
|
+
if (plan.phase === "warming" && sync.mode !== "readonly") {
|
|
152
|
+
return blocked(plan, "the engine config must select readonly mode while the index warms");
|
|
153
|
+
}
|
|
154
|
+
const vaults = sync.vaults;
|
|
155
|
+
if (!Array.isArray(vaults) || vaults.length !== 1 || vaults[0] !== plan.uid) {
|
|
156
|
+
return blocked(plan, `the engine config must name exactly ${plan.uid}`);
|
|
157
|
+
}
|
|
158
|
+
if (!hqRoot || typeof sync.hq_root !== "string" || path.resolve(sync.hq_root) !== path.resolve(hqRoot)) {
|
|
159
|
+
return blocked(plan, "the engine config must use the daemon's HQ folder");
|
|
160
|
+
}
|
|
161
|
+
if (plan.kind === "company") {
|
|
162
|
+
const roots = record(sync.roots);
|
|
163
|
+
if (!plan.slug || roots?.[plan.uid] !== `companies/${plan.slug}`) {
|
|
164
|
+
return blocked(plan, `the engine config must map ${plan.uid} to companies/${plan.slug ?? "<slug>"}`);
|
|
165
|
+
}
|
|
166
|
+
if (!fs.existsSync(path.join(hqRoot, "companies", plan.slug))) {
|
|
167
|
+
return blocked(plan, `companies/${plan.slug} does not exist under the HQ folder`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const ownedPath = path.join(plan.engineHome, ".hq", "sync-core", "owned", `${plan.uid}.json`);
|
|
171
|
+
if (plan.phase === "full") {
|
|
172
|
+
try {
|
|
173
|
+
const owned = record(JSON.parse(fs.readFileSync(ownedPath, "utf8")));
|
|
174
|
+
if (owned?.vault !== plan.uid)
|
|
175
|
+
throw new Error("mismatch");
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return blocked(plan, `the engine ownership record for ${plan.uid} is missing or invalid`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else if (fs.existsSync(ownedPath)) {
|
|
182
|
+
return blocked(plan, `the engine ownership record for ${plan.uid} must not exist while the index warms`);
|
|
183
|
+
}
|
|
184
|
+
for (const link of ["menubar.json", "work-mesh", "cognito-tokens.json"]) {
|
|
185
|
+
try {
|
|
186
|
+
fs.readlinkSync(path.join(plan.engineHome, ".hq", link));
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return blocked(plan, `the engine home for ${plan.uid} is missing its ${link} link`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return plan;
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=engine-home.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type FlagClientOptions, type FlagResolution } from "@indigoai-us/hq-flags-client";
|
|
2
|
+
export declare const SYNC_RUST_ENGINE_ROLLOUT_FLAG = "sync.rust-engine-rollout";
|
|
3
|
+
export interface RolloutFlagClient {
|
|
4
|
+
ready(): Promise<void>;
|
|
5
|
+
explain(flagKey: string, options?: {
|
|
6
|
+
fallback?: boolean;
|
|
7
|
+
}): FlagResolution;
|
|
8
|
+
close(): void;
|
|
9
|
+
}
|
|
10
|
+
export interface ReadCompanyRolloutFlagOptions {
|
|
11
|
+
env?: NodeJS.ProcessEnv;
|
|
12
|
+
createClient?: (options: FlagClientOptions) => RolloutFlagClient;
|
|
13
|
+
}
|
|
14
|
+
export declare class RolloutFlagReadError extends Error {
|
|
15
|
+
}
|
|
16
|
+
export declare function readCompanyRolloutFlag(companyUid: string, token: string, options?: ReadCompanyRolloutFlagOptions): Promise<boolean>;
|
|
17
|
+
//# sourceMappingURL=flag.d.ts.map
|