@indigoai-us/hq-cli 5.345.171 → 5.345.172

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/command-catalog.generated.d.ts +74 -2
  3. package/dist/command-catalog.generated.js +88 -2
  4. package/dist/command-registration-plan.d.ts +7 -0
  5. package/dist/command-registration-plan.js +1 -0
  6. package/dist/commands/bot.js +46 -8
  7. package/dist/commands/cloud.js +9 -5
  8. package/dist/commands/daemon.js +16 -1
  9. package/dist/commands/files.d.ts +1 -0
  10. package/dist/commands/files.js +22 -4
  11. package/dist/commands/sync-core.d.ts +22 -0
  12. package/dist/commands/sync-core.js +124 -0
  13. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +278 -224
  14. package/dist/hq-cloud-sync-runner/package.json +1 -1
  15. package/dist/lib/bot/fleet-local.d.ts +16 -1
  16. package/dist/lib/bot/fleet-local.js +99 -48
  17. package/dist/lib/bot/fleet-python.d.ts +47 -0
  18. package/dist/lib/bot/fleet-python.js +218 -0
  19. package/dist/lib/bot/index.d.ts +1 -0
  20. package/dist/lib/bot/index.js +1 -0
  21. package/dist/lib/bot/prompt.d.ts +6 -1
  22. package/dist/lib/bot/prompt.js +8 -3
  23. package/dist/lib/daemon/catalog.d.ts +12 -4
  24. package/dist/lib/daemon/catalog.js +70 -27
  25. package/dist/lib/daemon/config.d.ts +0 -7
  26. package/dist/lib/daemon/config.js +0 -32
  27. package/dist/lib/daemon/run.d.ts +5 -1
  28. package/dist/lib/daemon/run.js +48 -11
  29. package/dist/lib/daemon/supervisor.js +3 -1
  30. package/dist/lib/sync-core/engine-home.d.ts +44 -0
  31. package/dist/lib/sync-core/engine-home.js +193 -0
  32. package/dist/lib/sync-core/flag.d.ts +17 -0
  33. package/dist/lib/sync-core/flag.js +47 -0
  34. package/dist/lib/sync-core/reconcile.d.ts +31 -0
  35. package/dist/lib/sync-core/reconcile.js +196 -0
  36. package/dist/lib/sync-core/release.d.ts +32 -0
  37. package/dist/lib/sync-core/release.js +140 -0
  38. package/dist/lib/sync-core/rollout-state.d.ts +24 -0
  39. package/dist/lib/sync-core/rollout-state.js +94 -0
  40. package/dist/lib/vault-sync-switches.d.ts +5 -0
  41. package/dist/lib/vault-sync-switches.js +43 -0
  42. package/dist/utils/remote-list-not-converging.d.ts +21 -0
  43. package/dist/utils/remote-list-not-converging.js +40 -0
  44. package/package.json +8 -8
  45. package/dist/lib/daemon/sync-core-canary.d.ts +0 -12
  46. 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 vaultSync = ctx.syncCoreCanaryRunning
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 ?? new Set(),
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
- if (!ctx.syncCoreCanary)
185
- return [unit];
186
- const core = base(service, {
187
- id: "sync-core-canary",
188
- description: ctx.syncCoreCanary.fullVault
189
- ? "keeps the personal vault in sync with sync-core"
190
- : "keeps one personal-vault canary in sync with sync-core",
191
- command: ctx.syncCoreCanary.binaryPath
192
- ? { type: "exec", file: ctx.syncCoreCanary.binaryPath, args: ["start", "--loop"] }
193
- : null,
194
- env: {
195
- HQ_SYNC_ENGINE: "rust-test",
196
- HQ_SYNC_MODE: "full",
197
- HQ_SYNC_PROTOCOL: "1",
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
- heartbeatTimeoutMs: 180_000,
208
+ env: { HQ_NO_UPDATE_CHECK: "1" },
203
209
  blocked: firstBlock([
204
- [!!commonBlock, commonBlock?.reason ?? ""],
205
- [!!ctx.syncCoreCanary.blockedReason, ctx.syncCoreCanary.blockedReason ?? ""],
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
- return [unit, core];
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 });
@@ -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[];
@@ -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 { inspectSyncCoreCanary } from "./sync-core-canary.js";
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 syncCoreCanaryRunning = supervisor?.runningPlanKey("sync-core-canary") !== undefined;
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
- syncCoreCanaryRunning,
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) => d.isDirectory() && readBotConfig(path.join(root, d.name)) !== null)
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
- syncCoreCanary: config.syncCoreCanaryError
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,44 @@
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
+ remoteRows: number;
38
+ localFiles: number;
39
+ }
40
+ export declare function engineReadOnlyPlan(engineHome: string, vaultUid: string): SyncCorePlanFigures | null;
41
+ export declare function clearEngineStatus(home: string, vaultUid: string): void;
42
+ export declare function removeEngineOwnership(home: string, vaultUid: string): void;
43
+ export declare function inspectEngineHome(home: string, hqRoot: string | undefined, plan: SyncCoreVaultPlan, platform: NodeJS.Platform): SyncCoreVaultPlan;
44
+ //# sourceMappingURL=engine-home.d.ts.map
@@ -0,0 +1,193 @@
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
+ remoteRows: count(record(run.remote)?.rows),
108
+ localFiles: count(record(run.local)?.files),
109
+ };
110
+ }
111
+ export function clearEngineStatus(home, vaultUid) {
112
+ fs.rmSync(path.join(syncCoreEngineHome(home, vaultUid), ".hq", "sync-core", "status.json"), { force: true });
113
+ }
114
+ export function removeEngineOwnership(home, vaultUid) {
115
+ fs.rmSync(path.join(syncCoreEngineHome(home, vaultUid), ".hq", "sync-core", "owned", `${vaultUid}.json`), { force: true });
116
+ }
117
+ function record(value) {
118
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
119
+ }
120
+ function blocked(plan, blockedReason) {
121
+ return { ...plan, blockedReason };
122
+ }
123
+ export function inspectEngineHome(home, hqRoot, plan, platform) {
124
+ if (platform !== "darwin" && platform !== "linux")
125
+ return blocked(plan, "the sync engine requires macOS or Linux");
126
+ try {
127
+ if (!fs.statSync(plan.binaryPath).isFile())
128
+ return blocked(plan, "the sync engine binary is missing");
129
+ fs.accessSync(plan.binaryPath, fs.constants.X_OK);
130
+ }
131
+ catch {
132
+ return blocked(plan, "the sync engine binary is missing or not executable");
133
+ }
134
+ if (plan.engineHome !== syncCoreEngineHome(home, plan.uid)) {
135
+ return blocked(plan, `the engine home for ${plan.uid} is not under this user's sync-core folder`);
136
+ }
137
+ const configPath = path.join(plan.engineHome, ".hq", "sync-core", "config.toml");
138
+ let sync;
139
+ try {
140
+ sync = record(record(parseToml(fs.readFileSync(configPath, "utf8")))?.sync);
141
+ }
142
+ catch {
143
+ return blocked(plan, `the engine config at ${configPath} is missing or invalid`);
144
+ }
145
+ if (!sync || sync.engine !== "rust-test")
146
+ return blocked(plan, "the engine config must select engine rust-test");
147
+ if (plan.phase === "full" && (sync.mode !== "full" || sync.full_vault !== true)) {
148
+ return blocked(plan, "the engine config must select engine rust-test in full-vault mode");
149
+ }
150
+ if (plan.phase === "warming" && sync.mode !== "readonly") {
151
+ return blocked(plan, "the engine config must select readonly mode while the index warms");
152
+ }
153
+ const vaults = sync.vaults;
154
+ if (!Array.isArray(vaults) || vaults.length !== 1 || vaults[0] !== plan.uid) {
155
+ return blocked(plan, `the engine config must name exactly ${plan.uid}`);
156
+ }
157
+ if (!hqRoot || typeof sync.hq_root !== "string" || path.resolve(sync.hq_root) !== path.resolve(hqRoot)) {
158
+ return blocked(plan, "the engine config must use the daemon's HQ folder");
159
+ }
160
+ if (plan.kind === "company") {
161
+ const roots = record(sync.roots);
162
+ if (!plan.slug || roots?.[plan.uid] !== `companies/${plan.slug}`) {
163
+ return blocked(plan, `the engine config must map ${plan.uid} to companies/${plan.slug ?? "<slug>"}`);
164
+ }
165
+ if (!fs.existsSync(path.join(hqRoot, "companies", plan.slug))) {
166
+ return blocked(plan, `companies/${plan.slug} does not exist under the HQ folder`);
167
+ }
168
+ }
169
+ const ownedPath = path.join(plan.engineHome, ".hq", "sync-core", "owned", `${plan.uid}.json`);
170
+ if (plan.phase === "full") {
171
+ try {
172
+ const owned = record(JSON.parse(fs.readFileSync(ownedPath, "utf8")));
173
+ if (owned?.vault !== plan.uid)
174
+ throw new Error("mismatch");
175
+ }
176
+ catch {
177
+ return blocked(plan, `the engine ownership record for ${plan.uid} is missing or invalid`);
178
+ }
179
+ }
180
+ else if (fs.existsSync(ownedPath)) {
181
+ return blocked(plan, `the engine ownership record for ${plan.uid} must not exist while the index warms`);
182
+ }
183
+ for (const link of ["menubar.json", "work-mesh", "cognito-tokens.json"]) {
184
+ try {
185
+ fs.readlinkSync(path.join(plan.engineHome, ".hq", link));
186
+ }
187
+ catch {
188
+ return blocked(plan, `the engine home for ${plan.uid} is missing its ${link} link`);
189
+ }
190
+ }
191
+ return plan;
192
+ }
193
+ //# 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