@abelxiaoxing/cadence 2.0.1 → 2.0.3

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.
@@ -3,7 +3,9 @@
3
3
 
4
4
  // src/service/cli.ts
5
5
  import { spawn } from "node:child_process";
6
+ import { existsSync, mkdirSync as mkdirSync4 } from "node:fs";
6
7
  import { request } from "node:http";
8
+ import { dirname as dirname8, join as join8, resolve as resolve5 } from "node:path";
7
9
  import { fileURLToPath } from "node:url";
8
10
 
9
11
  // src/task-types.ts
@@ -86,7 +88,10 @@ function safeChildEnv() {
86
88
  "TMPDIR",
87
89
  "LANG",
88
90
  "LC_ALL",
89
- "PATHEXT"
91
+ "PATHEXT",
92
+ "ProgramFiles",
93
+ "ProgramFiles(x86)",
94
+ "ProgramW6432"
90
95
  ])
91
96
  if (process.env[key] !== undefined)
92
97
  env[key] = process.env[key];
@@ -2039,12 +2044,6 @@ class CaptureBudgetLease {
2039
2044
  this.forget();
2040
2045
  }
2041
2046
  }
2042
- // src/service/coordination.ts
2043
- import { randomBytes, randomUUID as randomUUID3 } from "node:crypto";
2044
- import { opendirSync, realpathSync as realpathSync2 } from "node:fs";
2045
- import { hostname as hostname2 } from "node:os";
2046
- import { dirname as dirname3, isAbsolute as isAbsolute3, join as join3, relative as relative3, sep as sep3 } from "node:path";
2047
-
2048
2047
  // src/task-store.ts
2049
2048
  import { spawnSync } from "node:child_process";
2050
2049
  import { createHash, randomUUID } from "node:crypto";
@@ -2114,11 +2113,8 @@ function safeDirectory(path, create = false, inspectExisting) {
2114
2113
  at = join(at, part);
2115
2114
  let existing;
2116
2115
  try {
2117
- const st = lstatSync(at);
2118
- if (!st.isDirectory())
2119
- throw new Error("Unsafe directory");
2120
2116
  const checked = lstatSync(at);
2121
- if (checked.isSymbolicLink() || inspectExisting && !checked.isDirectory())
2117
+ if (!checked.isDirectory() || checked.isSymbolicLink())
2122
2118
  throw new Error("Unsafe directory");
2123
2119
  existing = checked;
2124
2120
  } catch (error) {
@@ -2132,7 +2128,8 @@ function safeDirectory(path, create = false, inspectExisting) {
2132
2128
  if (!object(mkdirError) || mkdirError.code !== "EEXIST")
2133
2129
  throw mkdirError;
2134
2130
  }
2135
- if (!lstatSync(at).isDirectory() || lstatSync(at).isSymbolicLink())
2131
+ const created = lstatSync(at);
2132
+ if (!created.isDirectory() || created.isSymbolicLink())
2136
2133
  throw new Error("Unsafe directory");
2137
2134
  }
2138
2135
  if (existing)
@@ -2140,13 +2137,29 @@ function safeDirectory(path, create = false, inspectExisting) {
2140
2137
  }
2141
2138
  return true;
2142
2139
  }
2143
- function readBounded(path, max) {
2140
+ var transientWindows = new Set(["EPERM", "EACCES", "EBUSY"]);
2141
+ function retryWindowsFileOperation(operation, recheck) {
2142
+ if (process.platform !== "win32")
2143
+ return operation();
2144
+ const deadline = performance.now() + 2000;
2145
+ for (let delay = 2;; delay = Math.min(delay * 2, 50)) {
2146
+ try {
2147
+ return operation();
2148
+ } catch (error) {
2149
+ if (!object(error) || !transientWindows.has(String(error.code)) || performance.now() >= deadline)
2150
+ throw error;
2151
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delay);
2152
+ recheck?.();
2153
+ }
2154
+ }
2155
+ }
2156
+ function readBounded(path, max, packaged = false) {
2144
2157
  if (!safeDirectory(dirname(path)))
2145
2158
  return;
2146
2159
  let fd;
2147
2160
  try {
2148
2161
  const st = lstatSync(path);
2149
- if (!st.isFile() || st.isSymbolicLink() || st.nlink !== 1 || st.size > max)
2162
+ if (!st.isFile() || st.isSymbolicLink() || !packaged && st.nlink !== 1 || st.size > max)
2150
2163
  throw new Error("Unsafe or oversized file");
2151
2164
  fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
2152
2165
  } catch (error) {
@@ -2156,7 +2169,7 @@ function readBounded(path, max) {
2156
2169
  }
2157
2170
  try {
2158
2171
  const st = fstatSync(fd);
2159
- if (!st.isFile() || st.nlink !== 1 || st.size > max)
2172
+ if (!st.isFile() || !packaged && st.nlink !== 1 || st.size > max)
2160
2173
  throw new Error("Unsafe or oversized file");
2161
2174
  const buffer = Buffer.alloc(max + 1);
2162
2175
  let size = 0;
@@ -2234,6 +2247,23 @@ function canonicalAmbientRoot(path) {
2234
2247
  return join(canonicalAmbientRoot(parent), full.slice(parent.length));
2235
2248
  }
2236
2249
  }
2250
+ function privateRootFor(canonicalRoot, stateHome) {
2251
+ return join(canonicalAmbientRoot(stateHome), "cadence-assistant-v1", createHash("sha256").update(canonicalRoot).digest("hex"));
2252
+ }
2253
+ function contains(root, path) {
2254
+ const within = relative(root, resolve(path));
2255
+ return !within || !within.startsWith(`..${sep}`) && within !== ".." && !isAbsolute(within);
2256
+ }
2257
+ function stateHomeInside(root, stateHome = defaultStateHome()) {
2258
+ const canonical = realpathSync(root);
2259
+ return contains(canonical, privateRootFor(canonical, stateHome));
2260
+ }
2261
+ var LANDING_WORKSPACE = "cadence";
2262
+ function effectiveWorkspace(requested, stateHome, home = homedir()) {
2263
+ if (!stateHomeInside(resolveWorkspace(requested).root, stateHome))
2264
+ return { workspace: requested, redirected: false };
2265
+ return { workspace: join(home, LANDING_WORKSPACE), redirected: true };
2266
+ }
2237
2267
 
2238
2268
  class TaskStore {
2239
2269
  root;
@@ -2242,9 +2272,8 @@ class TaskStore {
2242
2272
  constructor(root, stateHome = defaultStateHome()) {
2243
2273
  this.root = realpathSync(root);
2244
2274
  this.tasks = join(this.root, ".cadence", "assistant", "tasks");
2245
- this.privateRoot = join(canonicalAmbientRoot(stateHome), "cadence-assistant-v1", createHash("sha256").update(this.root).digest("hex"));
2246
- const within = relative(this.root, resolve(this.privateRoot));
2247
- if (!within || !within.startsWith(`..${sep}`) && within !== ".." && !isAbsolute(within))
2275
+ this.privateRoot = privateRootFor(this.root, stateHome);
2276
+ if (contains(this.root, this.privateRoot))
2248
2277
  throw new Error("Private state home must be outside the working copy");
2249
2278
  }
2250
2279
  get lockPath() {
@@ -2296,7 +2325,7 @@ class TaskStore {
2296
2325
  };
2297
2326
  let fd;
2298
2327
  try {
2299
- fd = openSync(this.lockPath, "wx", 384);
2328
+ fd = retryWindowsFileOperation(() => openSync(this.lockPath, "wx", 384));
2300
2329
  } catch {
2301
2330
  throw new Error("Checkout busy or unresolved lock; inspect and explicitly recover only a dead owner");
2302
2331
  }
@@ -2316,7 +2345,7 @@ class TaskStore {
2316
2345
  if (lock.pid !== process.pid || lock.host !== hostname())
2317
2346
  throw new Error("Only the owning process may release a lock");
2318
2347
  this.assertOwner(lock);
2319
- unlinkSync(this.lockPath);
2348
+ retryWindowsFileOperation(() => unlinkSync(this.lockPath), () => this.assertOwner(lock));
2320
2349
  }
2321
2350
  recover(expected, acknowledgeEscapedProcesses) {
2322
2351
  if (!acknowledgeEscapedProcesses || expected.host !== hostname())
@@ -2396,7 +2425,12 @@ class TaskStore {
2396
2425
  const latest = readBounded(join(dir, "task.json"), MAX_RECORD_BYTES);
2397
2426
  if ((latest ? contentIdentity(latest) : undefined) !== expectedIdentity)
2398
2427
  throw new Error("Task content changed; replacement preserved without overwrite");
2399
- renameSync(temporary, join(dir, "task.json"));
2428
+ retryWindowsFileOperation(() => renameSync(temporary, join(dir, "task.json")), () => {
2429
+ this.assertOwner(lock);
2430
+ const now = readBounded(join(dir, "task.json"), MAX_RECORD_BYTES);
2431
+ if ((now ? contentIdentity(now) : undefined) !== expectedIdentity)
2432
+ throw new Error("Task content changed; replacement preserved without overwrite");
2433
+ });
2400
2434
  return next;
2401
2435
  } finally {
2402
2436
  if (!held)
@@ -2436,6 +2470,326 @@ class TaskStore {
2436
2470
  }
2437
2471
  var TASK_CONFIG_SCHEMA = "cadence-assistant-checks/1";
2438
2472
 
2473
+ // src/service/autostart.ts
2474
+ import { spawnSync as spawnSync2 } from "node:child_process";
2475
+ import {
2476
+ lstatSync as lstatSync2,
2477
+ mkdirSync as mkdirSync2,
2478
+ readFileSync,
2479
+ realpathSync as realpathSync2,
2480
+ renameSync as renameSync2,
2481
+ rmSync,
2482
+ statSync,
2483
+ writeFileSync as writeFileSync2
2484
+ } from "node:fs";
2485
+ import { homedir as homedir2 } from "node:os";
2486
+ import { delimiter, dirname as dirname2, isAbsolute as isAbsolute2, join as join2, resolve as resolve2, sep as sep2 } from "node:path";
2487
+ var UNIT = "cadence.service";
2488
+ var MARKER = "# Generated by @abelxiaoxing/cadence";
2489
+ function hostSystem(root) {
2490
+ const run = (command, args) => {
2491
+ const r = spawnSync2(command, args, {
2492
+ encoding: "utf8",
2493
+ stdio: ["ignore", "pipe", "ignore"]
2494
+ });
2495
+ return { status: r.error ? null : r.status, stdout: r.stdout ?? "" };
2496
+ };
2497
+ return {
2498
+ platform: process.platform,
2499
+ env: process.env,
2500
+ home: homedir2(),
2501
+ node: process.execPath,
2502
+ root,
2503
+ unit: UNIT,
2504
+ systemctl: (args) => run("systemctl", ["--user", ...args]),
2505
+ linger: () => {
2506
+ const r = run("loginctl", [
2507
+ "show-user",
2508
+ String(process.getuid?.()),
2509
+ "--property=Linger",
2510
+ "--value"
2511
+ ]);
2512
+ return r.status === 0 ? r.stdout.trim() === "yes" : undefined;
2513
+ },
2514
+ enableLinger: (interactive) => {
2515
+ const r = interactive ? spawnSync2("loginctl", ["enable-linger"], { stdio: "inherit" }) : spawnSync2("busctl", [
2516
+ "--system",
2517
+ "--allow-interactive-authorization=no",
2518
+ "--timeout=15",
2519
+ "call",
2520
+ "org.freedesktop.login1",
2521
+ "/org/freedesktop/login1",
2522
+ "org.freedesktop.login1.Manager",
2523
+ "SetUserLinger",
2524
+ "ubb",
2525
+ String(process.getuid?.()),
2526
+ "true",
2527
+ "false"
2528
+ ], { stdio: "ignore", timeout: 20000 });
2529
+ return !r.error && r.status === 0;
2530
+ }
2531
+ };
2532
+ }
2533
+ function expandHome(value, home) {
2534
+ if (value === "~")
2535
+ return home;
2536
+ return value.startsWith("~/") ? join2(home, value.slice(2)) : value;
2537
+ }
2538
+ function agentDirectory(s) {
2539
+ const selected = s.env.PI_CODING_AGENT_DIR;
2540
+ return resolve2(selected ? expandHome(selected, s.home) : join2(s.home, ".pi", "agent"));
2541
+ }
2542
+ function piUserInstall(s) {
2543
+ try {
2544
+ const managed = join2(agentDirectory(s), "npm", "node_modules", "@abelxiaoxing", "cadence");
2545
+ return realpathSync2(managed) === realpathSync2(s.root);
2546
+ } catch {
2547
+ return false;
2548
+ }
2549
+ }
2550
+ function sdkPackages(root) {
2551
+ let start;
2552
+ try {
2553
+ start = realpathSync2(join2(root, "src"));
2554
+ } catch {
2555
+ return;
2556
+ }
2557
+ const found = [];
2558
+ for (const name of ["pi-coding-agent", "pi-ai"]) {
2559
+ let hit;
2560
+ for (let dir = start;!hit; dir = dirname2(dir)) {
2561
+ const candidate = join2(dir, "node_modules", "@earendil-works", name, "package.json");
2562
+ if (regularFile(candidate))
2563
+ hit = candidate;
2564
+ else if (dirname2(dir) === dir)
2565
+ return;
2566
+ }
2567
+ found.push(hit);
2568
+ }
2569
+ return found;
2570
+ }
2571
+ function unitPath(s) {
2572
+ const data = s.env.XDG_DATA_HOME;
2573
+ return join2(data && isAbsolute2(data) ? data : join2(s.home, ".local", "share"), "systemd", "user", s.unit);
2574
+ }
2575
+ function servicePath(value) {
2576
+ const kept = (value ?? "").split(delimiter).filter((entry) => isAbsolute2(entry) && !entry.endsWith(`${sep2}node_modules${sep2}.bin`) && !entry.includes(`${sep2}node-gyp-bin`));
2577
+ return kept.length ? [...new Set(kept)].join(delimiter) : "/usr/local/bin:/usr/bin:/bin";
2578
+ }
2579
+ function serviceEnvironment(s) {
2580
+ const out = [["PATH", servicePath(s.env.PATH)]];
2581
+ for (const key of ["LANG", "LC_ALL"]) {
2582
+ const value = s.env[key];
2583
+ if (value)
2584
+ out.push([key, value]);
2585
+ }
2586
+ const state = s.env.XDG_STATE_HOME;
2587
+ if (state && isAbsolute2(state))
2588
+ out.push(["XDG_STATE_HOME", state]);
2589
+ if (s.env.PI_CODING_AGENT_DIR)
2590
+ out.push(["PI_CODING_AGENT_DIR", agentDirectory(s)]);
2591
+ return out;
2592
+ }
2593
+ function checked(value) {
2594
+ if (/[\u0000-\u001f\u007f]/.test(value))
2595
+ throw new ContractError("invalid");
2596
+ return value;
2597
+ }
2598
+ var specifiers = (value) => checked(value).replaceAll("%", "%%");
2599
+ var quoted = (value) => `"${specifiers(value).replace(/["\\]/g, "\\$&")}"`;
2600
+ var argument = (value) => quoted(value).replaceAll("$", "$$$$");
2601
+ function buildUnit(u) {
2602
+ const command = (args) => [u.node, u.cli, ...args].map(argument).join(" ");
2603
+ return [
2604
+ `${MARKER} on Pi package install/update; upgrades rewrite this file.`,
2605
+ "# Override with `systemctl --user edit cadence`; opt out with `systemctl --user disable --now cadence` or `mask`.",
2606
+ "[Unit]",
2607
+ "Description=Cadence shared task service and WebUI",
2608
+ "Documentation=https://github.com/abelxiaoxing/cadence/blob/main/docs/service-management.md",
2609
+ `ConditionPathExists=${specifiers(u.cli)}`,
2610
+ `ConditionPathExists=${specifiers(join2(u.agentDir, "auth.json"))}`,
2611
+ ...u.sdk.map((path) => `ConditionPathExists=${specifiers(path)}`),
2612
+ "StartLimitIntervalSec=300",
2613
+ "StartLimitBurst=3",
2614
+ "",
2615
+ "[Service]",
2616
+ "Type=simple",
2617
+ `WorkingDirectory=${specifiers(u.workspace)}`,
2618
+ ...u.environment.map(([k, v]) => `Environment=${quoted(`${k}=${v}`)}`),
2619
+ `ExecStart=${command(["start", "--foreground", "--workspace", u.workspace])}`,
2620
+ `ExecStop=-${command(["stop", "--workspace", u.workspace])}`,
2621
+ "KillMode=mixed",
2622
+ "TimeoutStopSec=90",
2623
+ "Restart=on-failure",
2624
+ "RestartSec=10",
2625
+ "",
2626
+ "[Install]",
2627
+ "WantedBy=default.target",
2628
+ ""
2629
+ ].join(`
2630
+ `);
2631
+ }
2632
+ function readGenerated(path) {
2633
+ try {
2634
+ if (!lstatSync2(path).isFile())
2635
+ return null;
2636
+ const text2 = readFileSync(path, "utf8");
2637
+ return text2.startsWith(MARKER) ? text2 : null;
2638
+ } catch (error) {
2639
+ if (error.code === "ENOENT")
2640
+ return;
2641
+ throw error;
2642
+ }
2643
+ }
2644
+ function generatedWorkspace(text2) {
2645
+ return text2?.match(/^WorkingDirectory=(.+)$/m)?.[1].replaceAll("%%", "%");
2646
+ }
2647
+ function directory(path) {
2648
+ try {
2649
+ return isAbsolute2(path) && statSync(path).isDirectory();
2650
+ } catch {
2651
+ return false;
2652
+ }
2653
+ }
2654
+ function unitStateHome(s) {
2655
+ const state = s.env.XDG_STATE_HOME;
2656
+ return state && isAbsolute2(state) ? state : join2(s.home, ".local", "state");
2657
+ }
2658
+ function selectWorkspace(s, requested, previous) {
2659
+ let chosen;
2660
+ if (requested) {
2661
+ chosen = expandHome(requested, s.home);
2662
+ if (!directory(chosen))
2663
+ throw new ContractError("invalid");
2664
+ chosen = resolve2(chosen);
2665
+ } else {
2666
+ const kept = generatedWorkspace(previous);
2667
+ if (kept && directory(kept))
2668
+ chosen = kept;
2669
+ }
2670
+ const landing = chosen ? effectiveWorkspace(chosen, unitStateHome(s), s.home) : { workspace: join2(s.home, LANDING_WORKSPACE), redirected: false };
2671
+ if (landing.workspace !== chosen)
2672
+ mkdirSync2(landing.workspace, { recursive: true });
2673
+ return landing.redirected ? { workspace: landing.workspace, redirectedFrom: chosen } : { workspace: landing.workspace };
2674
+ }
2675
+ function write(path, content) {
2676
+ mkdirSync2(dirname2(path), { recursive: true });
2677
+ const temporary = `${path}.${process.pid}.tmp`;
2678
+ try {
2679
+ writeFileSync2(temporary, content, { mode: 420, flag: "wx" });
2680
+ renameSync2(temporary, path);
2681
+ } finally {
2682
+ rmSync(temporary, { force: true });
2683
+ }
2684
+ }
2685
+ function regularFile(path) {
2686
+ try {
2687
+ return statSync(path).isFile();
2688
+ } catch {
2689
+ return false;
2690
+ }
2691
+ }
2692
+ function bootStart(s, attempt, interactive) {
2693
+ const current = s.linger();
2694
+ if (current === undefined)
2695
+ return {};
2696
+ if (current)
2697
+ return { linger: "on" };
2698
+ if (!attempt)
2699
+ return { linger: "off" };
2700
+ return { linger: s.enableLinger(interactive) ? "enabled" : "refused" };
2701
+ }
2702
+ function configureAutostart(s, mode, requested, linger = false) {
2703
+ if (s.platform !== "linux")
2704
+ return { status: "skipped" };
2705
+ if (mode === "refresh" && (s.env.CADENCE_AUTOSTART === "0" || !piUserInstall(s)))
2706
+ return { status: "skipped" };
2707
+ if (s.systemctl(["show-environment"]).status !== 0)
2708
+ return { status: "unavailable" };
2709
+ const path = unitPath(s);
2710
+ const previous = readGenerated(path);
2711
+ if (previous === null)
2712
+ return { status: "foreign", unit: path };
2713
+ const sdk = sdkPackages(s.root);
2714
+ if (!sdk)
2715
+ return { status: "missing-pi-sdk" };
2716
+ const { workspace, redirectedFrom } = selectWorkspace(s, requested ?? s.env.CADENCE_WORKSPACE, previous);
2717
+ const agentDir = agentDirectory(s);
2718
+ const content = buildUnit({
2719
+ node: s.node,
2720
+ cli: join2(s.root, "src", "service-cli.mjs"),
2721
+ workspace,
2722
+ agentDir,
2723
+ sdk,
2724
+ environment: serviceEnvironment(s)
2725
+ });
2726
+ const enabled = s.systemctl(["is-enabled", s.unit]).stdout.trim();
2727
+ const active = s.systemctl(["is-active", "--quiet", s.unit]).status === 0;
2728
+ if (active)
2729
+ s.systemctl(["stop", s.unit]);
2730
+ write(path, content);
2731
+ s.systemctl(["daemon-reload"]);
2732
+ const result = {
2733
+ unit: path,
2734
+ workspace,
2735
+ ...redirectedFrom ? { redirectedFrom } : {}
2736
+ };
2737
+ if (enabled.startsWith("masked"))
2738
+ return { status: "masked", ...result };
2739
+ const wanted = mode === "install" || previous === undefined || enabled === "enabled";
2740
+ if (wanted && enabled !== "enabled" && s.systemctl(["enable", s.unit]).status !== 0)
2741
+ return { status: "failed", ...result };
2742
+ if (!wanted && !active)
2743
+ return { status: "disabled", ...result };
2744
+ const boot = () => wanted ? bootStart(s, mode === "install" ? linger : previous === undefined && s.env.CADENCE_LINGER !== "0", mode === "install") : {};
2745
+ if (!regularFile(join2(agentDir, "auth.json")))
2746
+ return { status: "waiting-for-pi-login", ...result, ...boot() };
2747
+ s.systemctl(["reset-failed", s.unit]);
2748
+ const started = s.systemctl(["start", s.unit]).status === 0;
2749
+ return { status: started ? "started" : "failed", ...result, ...boot() };
2750
+ }
2751
+ function removeAutostart(s) {
2752
+ if (s.platform !== "linux")
2753
+ return { status: "skipped" };
2754
+ const path = unitPath(s);
2755
+ const previous = readGenerated(path);
2756
+ if (previous === null)
2757
+ return { status: "foreign", unit: path };
2758
+ if (previous === undefined)
2759
+ return { status: "absent" };
2760
+ if (s.systemctl(["show-environment"]).status !== 0)
2761
+ return { status: "unavailable", unit: path };
2762
+ if (s.systemctl(["disable", "--now", s.unit]).status !== 0)
2763
+ return { status: "failed", unit: path };
2764
+ rmSync(path);
2765
+ s.systemctl(["daemon-reload"]);
2766
+ s.systemctl(["reset-failed", s.unit]);
2767
+ return { status: "removed", unit: path };
2768
+ }
2769
+ function autostartStatus(s) {
2770
+ if (s.platform !== "linux")
2771
+ return { status: "skipped" };
2772
+ const path = unitPath(s);
2773
+ const previous = readGenerated(path);
2774
+ if (previous === null)
2775
+ return { status: "foreign", unit: path };
2776
+ if (previous === undefined)
2777
+ return { status: "absent" };
2778
+ return {
2779
+ status: s.systemctl(["is-active", s.unit]).stdout.trim() || "unknown",
2780
+ enabled: s.systemctl(["is-enabled", s.unit]).stdout.trim() || "unknown",
2781
+ unit: path,
2782
+ workspace: generatedWorkspace(previous),
2783
+ ...bootStart(s, false, false)
2784
+ };
2785
+ }
2786
+
2787
+ // src/service/coordination.ts
2788
+ import { randomBytes, randomUUID as randomUUID3 } from "node:crypto";
2789
+ import { opendirSync, realpathSync as realpathSync3 } from "node:fs";
2790
+ import { hostname as hostname2 } from "node:os";
2791
+ import { dirname as dirname4, isAbsolute as isAbsolute4, join as join4, relative as relative3, sep as sep4 } from "node:path";
2792
+
2439
2793
  // src/service/private-files.ts
2440
2794
  import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
2441
2795
  import {
@@ -2443,13 +2797,15 @@ import {
2443
2797
  constants as constants2,
2444
2798
  fstatSync as fstatSync2,
2445
2799
  fsyncSync as fsyncSync2,
2446
- lstatSync as lstatSync2,
2800
+ lstatSync as lstatSync3,
2801
+ mkdirSync as mkdirSync3,
2447
2802
  openSync as openSync2,
2448
- renameSync as renameSync2,
2803
+ readSync as readSync2,
2804
+ renameSync as renameSync3,
2449
2805
  unlinkSync as unlinkSync2,
2450
- writeFileSync as writeFileSync2
2806
+ writeFileSync as writeFileSync3
2451
2807
  } from "node:fs";
2452
- import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
2808
+ import { dirname as dirname3, isAbsolute as isAbsolute3, join as join3, relative as relative2, resolve as resolve3, sep as sep3 } from "node:path";
2453
2809
  var METADATA_BYTES = 16 * 1024;
2454
2810
  var REGISTRY_BYTES = 1024 * 1024;
2455
2811
  var REGISTRY_ENTRIES = 128;
@@ -2459,8 +2815,8 @@ function refused() {
2459
2815
  function hash(bytes) {
2460
2816
  return createHash2("sha256").update(bytes).digest("hex");
2461
2817
  }
2462
- function privateMode(st) {
2463
- if (process.platform !== "win32" && ((st.mode & 63) !== 0 || process.getuid && st.uid !== process.getuid()))
2818
+ function privateMode(st, ownerOnly = true) {
2819
+ if (process.platform !== "win32" && (ownerOnly && (st.mode & 63) !== 0 || process.getuid && st.uid !== process.getuid()))
2464
2820
  refused();
2465
2821
  }
2466
2822
  function same(a, b) {
@@ -2469,42 +2825,114 @@ function same(a, b) {
2469
2825
  function missing(error) {
2470
2826
  return error instanceof Error && "code" in error && error.code === "ENOENT";
2471
2827
  }
2828
+ var job = 0;
2829
+ var advancing = false;
2830
+ function currentJob() {
2831
+ if (!advancing) {
2832
+ advancing = true;
2833
+ queueMicrotask(() => {
2834
+ job++;
2835
+ advancing = false;
2836
+ });
2837
+ }
2838
+ return job;
2839
+ }
2840
+
2841
+ class DirectoryWalks {
2842
+ job = currentJob();
2843
+ walked = new Map;
2844
+ open() {
2845
+ if (this.job === job)
2846
+ return true;
2847
+ this.walked.clear();
2848
+ return false;
2849
+ }
2850
+ remember(path, st) {
2851
+ if (this.open())
2852
+ this.walked.set(path, { dev: st.dev, ino: st.ino });
2853
+ }
2854
+ reuse(path, ownerOnly = true) {
2855
+ const walked = this.open() ? this.walked.get(path) : undefined;
2856
+ if (!walked)
2857
+ return false;
2858
+ let st;
2859
+ try {
2860
+ st = lstatSync3(path);
2861
+ } catch (error) {
2862
+ if (!missing(error))
2863
+ throw error;
2864
+ this.walked.delete(path);
2865
+ return false;
2866
+ }
2867
+ if (!st.isDirectory() || st.isSymbolicLink() || st.dev !== walked.dev || st.ino !== walked.ino) {
2868
+ this.walked.delete(path);
2869
+ return false;
2870
+ }
2871
+ privateMode(st, ownerOnly);
2872
+ return true;
2873
+ }
2874
+ }
2472
2875
 
2473
2876
  class PrivateFiles {
2474
2877
  namespace;
2475
- constructor(namespace) {
2878
+ ownerOnly;
2879
+ constructor(namespace, ownerOnly = true) {
2476
2880
  this.namespace = namespace;
2881
+ this.ownerOnly = ownerOnly;
2477
2882
  }
2478
- directory(path, create = false) {
2883
+ directory(path, create = false, walks) {
2479
2884
  const tail = relative2(this.namespace, path);
2480
- if (isAbsolute2(tail) || tail === ".." || tail.startsWith(`..${sep2}`))
2885
+ if (isAbsolute3(tail) || tail === ".." || tail.startsWith(`..${sep3}`))
2481
2886
  refused();
2482
2887
  if (!create) {
2483
- const namespace = resolve2(this.namespace);
2484
- let inside = false;
2485
- return safeDirectory(path, false, (at2, st) => {
2888
+ const full = resolve3(path);
2889
+ if (walks?.reuse(full, this.ownerOnly))
2890
+ return true;
2891
+ const namespace = resolve3(this.namespace);
2892
+ let inside = false, last;
2893
+ const found = safeDirectory(path, false, (at2, st) => {
2486
2894
  if (at2 === namespace)
2487
2895
  inside = true;
2488
2896
  if (inside)
2489
- privateMode(st);
2897
+ privateMode(st, this.ownerOnly);
2898
+ last = st;
2490
2899
  });
2900
+ if (found && last)
2901
+ walks?.remember(full, last);
2902
+ return found;
2491
2903
  }
2904
+ if (!safeDirectory(this.namespace, true))
2905
+ return false;
2906
+ privateMode(lstatSync3(this.namespace), this.ownerOnly);
2492
2907
  let at = this.namespace;
2493
- for (const part of ["", ...tail.split(sep2).filter(Boolean)]) {
2494
- if (part)
2495
- at = join2(at, part);
2496
- if (!safeDirectory(at, create))
2497
- return false;
2498
- privateMode(lstatSync2(at));
2908
+ for (const part of tail.split(sep3).filter(Boolean)) {
2909
+ at = join3(at, part);
2910
+ let st;
2911
+ try {
2912
+ st = lstatSync3(at);
2913
+ } catch (error) {
2914
+ if (!missing(error))
2915
+ throw error;
2916
+ try {
2917
+ mkdirSync3(at, { mode: 448 });
2918
+ } catch (mkdirError) {
2919
+ if (!(mkdirError instanceof Error && ("code" in mkdirError) && mkdirError.code === "EEXIST"))
2920
+ throw mkdirError;
2921
+ }
2922
+ st = lstatSync3(at);
2923
+ }
2924
+ if (!st.isDirectory() || st.isSymbolicLink())
2925
+ throw new Error("Unsafe directory");
2926
+ privateMode(st, this.ownerOnly);
2499
2927
  }
2500
2928
  return true;
2501
2929
  }
2502
- read(path, maximum = METADATA_BYTES) {
2503
- if (!this.directory(dirname2(path)))
2930
+ read(path, maximum = METADATA_BYTES, walks) {
2931
+ if (!this.directory(dirname3(path), false, walks))
2504
2932
  return;
2505
2933
  let before;
2506
2934
  try {
2507
- before = lstatSync2(path);
2935
+ before = lstatSync3(path);
2508
2936
  } catch (error) {
2509
2937
  if (missing(error))
2510
2938
  return;
@@ -2512,37 +2940,45 @@ class PrivateFiles {
2512
2940
  }
2513
2941
  if (!before.isFile() || before.nlink !== 1 || before.size > maximum)
2514
2942
  refused();
2515
- privateMode(before);
2943
+ privateMode(before, this.ownerOnly);
2516
2944
  const fd = openSync2(path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
2517
2945
  try {
2518
2946
  const opened = fstatSync2(fd);
2519
2947
  if (!same(before, opened))
2520
2948
  refused();
2521
- const bytes = readBounded(path, opened.size);
2522
- if (!bytes || bytes.length !== opened.size || !same(opened, fstatSync2(fd)) || !same(opened, lstatSync2(path)))
2949
+ const buffer = Buffer.alloc(opened.size + 1);
2950
+ let size = 0;
2951
+ while (size < buffer.length) {
2952
+ const n = readSync2(fd, buffer, size, buffer.length - size, null);
2953
+ if (!n)
2954
+ break;
2955
+ size += n;
2956
+ }
2957
+ const bytes = buffer.subarray(0, size);
2958
+ if (size !== opened.size || !same(opened, fstatSync2(fd)) || !same(opened, lstatSync3(path)))
2523
2959
  refused();
2524
2960
  return { bytes, identity: hash(bytes), stamp: stamp(opened) };
2525
2961
  } finally {
2526
2962
  closeSync2(fd);
2527
2963
  }
2528
2964
  }
2529
- assert(path, expected, maximum = METADATA_BYTES) {
2530
- const current = this.read(path, maximum);
2965
+ assert(path, expected, maximum = METADATA_BYTES, walks) {
2966
+ const current = this.read(path, maximum, walks);
2531
2967
  if (current?.identity !== expected?.identity || current?.stamp !== expected?.stamp)
2532
2968
  throw new ContractError("conflict");
2533
2969
  }
2534
2970
  create(path, bytes, maximum = METADATA_BYTES) {
2535
2971
  if (Buffer.byteLength(bytes) > maximum)
2536
2972
  throw new ContractError("oversize");
2537
- this.directory(dirname2(path), true);
2538
- const fd = openSync2(path, "wx", 384);
2973
+ this.directory(dirname3(path), true);
2974
+ const fd = retryWindowsFileOperation(() => openSync2(path, "wx", 384));
2539
2975
  try {
2540
- writeFileSync2(fd, bytes);
2976
+ writeFileSync3(fd, bytes);
2541
2977
  fsyncSync2(fd);
2542
2978
  } finally {
2543
2979
  closeSync2(fd);
2544
2980
  }
2545
- this.syncDirectory(dirname2(path));
2981
+ this.syncDirectory(dirname3(path));
2546
2982
  const result = this.read(path, maximum);
2547
2983
  if (!result || result.identity !== hash(bytes))
2548
2984
  refused();
@@ -2550,11 +2986,11 @@ class PrivateFiles {
2550
2986
  }
2551
2987
  replace(path, expected, bytes, maximum = METADATA_BYTES) {
2552
2988
  this.assert(path, expected, maximum);
2553
- const temporary = join2(dirname2(path), `pending-${randomUUID2()}.json`);
2989
+ const temporary = join3(dirname3(path), `pending-${randomUUID2()}.json`);
2554
2990
  this.create(temporary, bytes, maximum);
2555
2991
  this.assert(path, expected, maximum);
2556
- renameSync2(temporary, path);
2557
- this.syncDirectory(dirname2(path));
2992
+ retryWindowsFileOperation(() => renameSync3(temporary, path), () => this.assert(path, expected, maximum));
2993
+ this.syncDirectory(dirname3(path));
2558
2994
  const result = this.read(path, maximum);
2559
2995
  if (!result || result.identity !== hash(bytes))
2560
2996
  refused();
@@ -2562,8 +2998,8 @@ class PrivateFiles {
2562
2998
  }
2563
2999
  remove(path, expected, maximum = METADATA_BYTES) {
2564
3000
  this.assert(path, expected, maximum);
2565
- unlinkSync2(path);
2566
- this.syncDirectory(dirname2(path));
3001
+ retryWindowsFileOperation(() => unlinkSync2(path), () => this.assert(path, expected, maximum));
3002
+ this.syncDirectory(dirname3(path));
2567
3003
  }
2568
3004
  syncDirectory(path) {
2569
3005
  if (process.platform === "win32")
@@ -2617,7 +3053,7 @@ var registrationKeys = [
2617
3053
  var reservationKeys = registrationKeys.filter((k) => !["schema", "phase", "checkoutToken"].includes(k));
2618
3054
  function pathValue(value) {
2619
3055
  const path = text(value, 4096, true);
2620
- if (!isAbsolute3(path))
3056
+ if (!isAbsolute4(path))
2621
3057
  refused();
2622
3058
  return path;
2623
3059
  }
@@ -2674,7 +3110,7 @@ function decodeRegistration(value) {
2674
3110
  function rootsOverlap(a, b) {
2675
3111
  const within = (x, y) => {
2676
3112
  const r = relative3(x, y);
2677
- return r === "" || r !== ".." && !r.startsWith(`..${sep3}`) && !isAbsolute3(r);
3113
+ return r === "" || r !== ".." && !r.startsWith(`..${sep4}`) && !isAbsolute4(r);
2678
3114
  };
2679
3115
  return within(a, b) || within(b, a);
2680
3116
  }
@@ -2726,17 +3162,17 @@ class ServiceCoordination {
2726
3162
  stateHome;
2727
3163
  constructor(workspace, stateHome) {
2728
3164
  this.store = new TaskStore(resolveWorkspace(workspace).root, stateHome);
2729
- const namespace = dirname3(this.store.privateRoot);
3165
+ const namespace = dirname4(this.store.privateRoot);
2730
3166
  this.files = new PrivateFiles(namespace);
2731
- this.stateHome = dirname3(namespace);
2732
- this.directory = join3(this.store.privateRoot, "service-v1");
2733
- this.registryDirectory = join3(namespace, "service-registry-v1");
3167
+ this.stateHome = dirname4(namespace);
3168
+ this.directory = join4(this.store.privateRoot, "service-v1");
3169
+ this.registryDirectory = join4(namespace, "service-registry-v1");
2734
3170
  }
2735
3171
  path(name) {
2736
- return join3(this.directory, name);
3172
+ return join4(this.directory, name);
2737
3173
  }
2738
3174
  registryPath(name) {
2739
- return join3(this.registryDirectory, name);
3175
+ return join4(this.registryDirectory, name);
2740
3176
  }
2741
3177
  registry() {
2742
3178
  if (!this.files.directory(this.registryDirectory))
@@ -2756,7 +3192,7 @@ class ServiceCoordination {
2756
3192
  const o = closed(json(snapshot, REGISTRY_BYTES), ["schema", "entries"]);
2757
3193
  const entries = list(o.entries, REGISTRY_ENTRIES, decodeReservation);
2758
3194
  for (const [i, row] of entries.entries()) {
2759
- if (row.stateHome !== this.stateHome || realpathSync2(row.root) !== row.root)
3195
+ if (row.stateHome !== this.stateHome || realpathSync3(row.root) !== row.root)
2760
3196
  refused();
2761
3197
  if (entries.slice(0, i).some((other) => rootsOverlap(row.root, other.root) || row.endpoint === other.endpoint))
2762
3198
  refused();
@@ -2770,8 +3206,8 @@ class ServiceCoordination {
2770
3206
  }
2771
3207
  };
2772
3208
  }
2773
- assertNamespace() {
2774
- if (!this.files.directory(this.directory))
3209
+ assertNamespace(walks) {
3210
+ if (!this.files.directory(this.directory, false, walks))
2775
3211
  refused();
2776
3212
  names(this.directory, [
2777
3213
  "format.json",
@@ -2784,14 +3220,15 @@ class ServiceCoordination {
2784
3220
  "host-config.json",
2785
3221
  "history-v2"
2786
3222
  ]);
2787
- this.files.directory(join3(this.directory, "history-v2"));
3223
+ this.files.directory(join4(this.directory, "history-v2"), false, walks);
2788
3224
  }
2789
3225
  selected() {
2790
- if (!this.files.directory(this.directory))
3226
+ const walks = new DirectoryWalks;
3227
+ if (!this.files.directory(this.directory, false, walks))
2791
3228
  return;
2792
- this.assertNamespace();
2793
- const manifest = this.files.read(this.path("format.json"));
2794
- const config = this.files.read(this.path("config.json"));
3229
+ this.assertNamespace(walks);
3230
+ const manifest = this.files.read(this.path("format.json"), undefined, walks);
3231
+ const config = this.files.read(this.path("config.json"), undefined, walks);
2795
3232
  if (!manifest || !config)
2796
3233
  refused();
2797
3234
  const f = closed(json(manifest), [
@@ -2805,13 +3242,13 @@ class ServiceCoordination {
2805
3242
  literal(f.stateHome, this.stateHome);
2806
3243
  literal(f.configIdentity, config.identity);
2807
3244
  const c = decodeOwnerConfig(json(config));
2808
- const hostConfig = this.files.read(this.path("host-config.json"));
3245
+ const hostConfig = this.files.read(this.path("host-config.json"), undefined, walks);
2809
3246
  if (hostConfig && hostConfig.identity !== c.policyIdentity)
2810
3247
  refused();
2811
- const registration = this.files.read(this.path("registration.json"));
2812
- const owner = this.files.read(this.path("owner.lock"));
2813
- const capability = this.files.read(this.path("local-capability.json"));
2814
- const checkout = this.files.read(this.store.lockPath, 8192);
3248
+ const registration = this.files.read(this.path("registration.json"), undefined, walks);
3249
+ const owner = this.files.read(this.path("owner.lock"), undefined, walks);
3250
+ const capability = this.files.read(this.path("local-capability.json"), undefined, walks);
3251
+ const checkout = this.files.read(this.store.lockPath, 8192, walks);
2815
3252
  const value = registration ? decodeRegistration(json(registration)) : undefined;
2816
3253
  if (value && (value.root !== this.store.root || value.stateHome !== this.stateHome || value.configIdentity !== config.identity || !endpointMatches(c.endpoint, value.endpoint)))
2817
3254
  refused();
@@ -3158,8 +3595,8 @@ class OwnerLease {
3158
3595
  assert() {
3159
3596
  if (this.released || this.selected.value.pid !== process.pid || this.selected.value.host !== hostname2())
3160
3597
  throw new ContractError("conflict");
3161
- const c = this.coordination;
3162
- c.assertNamespace();
3598
+ const c = this.coordination, walks = new DirectoryWalks;
3599
+ c.assertNamespace(walks);
3163
3600
  for (const [name, snapshot] of [
3164
3601
  ["config.json", this.selected.config],
3165
3602
  ["host-config.json", this.selected.hostConfig],
@@ -3168,8 +3605,8 @@ class OwnerLease {
3168
3605
  ["owner.lock", this.selected.owner],
3169
3606
  ["local-capability.json", this.selected.capability]
3170
3607
  ])
3171
- c.files.assert(c.path(name), snapshot);
3172
- c.files.assert(c.store.lockPath, this.selected.checkout, 8192);
3608
+ c.files.assert(c.path(name), snapshot, undefined, walks);
3609
+ c.files.assert(c.store.lockPath, this.selected.checkout, 8192, walks);
3173
3610
  c.store.assertOwner(this.checkout);
3174
3611
  }
3175
3612
  phase(phase) {
@@ -3208,8 +3645,8 @@ import { isatty } from "node:tty";
3208
3645
 
3209
3646
  // src/history/recovery.ts
3210
3647
  import { createHash as createHash5, randomUUID as randomUUID5 } from "node:crypto";
3211
- import { lstatSync as lstatSync5, opendirSync as opendirSync3 } from "node:fs";
3212
- import { join as join5, relative as relative4 } from "node:path";
3648
+ import { lstatSync as lstatSync6, opendirSync as opendirSync3 } from "node:fs";
3649
+ import { join as join6, relative as relative4 } from "node:path";
3213
3650
 
3214
3651
  // src/service/recovery.ts
3215
3652
  async function inspectRecovery(coordination, history) {
@@ -3263,13 +3700,13 @@ import {
3263
3700
  constants as constants4,
3264
3701
  fstatSync as fstatSync4,
3265
3702
  fsyncSync as fsyncSync3,
3266
- lstatSync as lstatSync4,
3703
+ lstatSync as lstatSync5,
3267
3704
  opendirSync as opendirSync2,
3268
3705
  openSync as openSync4,
3269
3706
  statfsSync
3270
3707
  } from "node:fs";
3271
3708
  import { open } from "node:fs/promises";
3272
- import { dirname as dirname5, join as join4 } from "node:path";
3709
+ import { dirname as dirname6, join as join5 } from "node:path";
3273
3710
 
3274
3711
  // src/history/admission.ts
3275
3712
  function decodeRunAdmission(value) {
@@ -3293,19 +3730,19 @@ import {
3293
3730
  closeSync as closeSync3,
3294
3731
  constants as constants3,
3295
3732
  fstatSync as fstatSync3,
3296
- lstatSync as lstatSync3,
3733
+ lstatSync as lstatSync4,
3297
3734
  openSync as openSync3,
3298
- readSync as readSync2
3735
+ readSync as readSync3
3299
3736
  } from "node:fs";
3300
- import { dirname as dirname4 } from "node:path";
3737
+ import { dirname as dirname5 } from "node:path";
3301
3738
  function bad() {
3302
3739
  throw new ContractError("corrupt-selected");
3303
3740
  }
3304
3741
  var stamp2 = (s) => [s.dev, s.ino, s.size, s.mode, s.uid, s.nlink, s.ctimeMs, s.mtimeMs].join(":");
3305
- function selected(files, path, read) {
3306
- if (!files.files.directory(dirname4(path)))
3742
+ function selected(files, path, read, walks) {
3743
+ if (!files.files.directory(dirname5(path), false, walks))
3307
3744
  bad();
3308
- const before = lstatSync3(path);
3745
+ const before = lstatSync4(path);
3309
3746
  if (!before.isFile() || before.nlink !== 1 || process.platform !== "win32" && (before.mode & 63 || process.getuid && before.uid !== process.getuid()))
3310
3747
  bad();
3311
3748
  const fd = openSync3(path, constants3.O_RDONLY | (constants3.O_NOFOLLOW ?? 0));
@@ -3313,7 +3750,7 @@ function selected(files, path, read) {
3313
3750
  if (stamp2(fstatSync3(fd)) !== stamp2(before))
3314
3751
  bad();
3315
3752
  const result = read(fd, before.size, stamp2(before));
3316
- if (stamp2(fstatSync3(fd)) !== stamp2(before) || stamp2(lstatSync3(path)) !== stamp2(before))
3753
+ if (stamp2(fstatSync3(fd)) !== stamp2(before) || stamp2(lstatSync4(path)) !== stamp2(before))
3317
3754
  bad();
3318
3755
  return result;
3319
3756
  } finally {
@@ -3764,13 +4201,24 @@ function decodeJournalText(line) {
3764
4201
 
3765
4202
  // src/history/files.ts
3766
4203
  class HistoryReadBudget {
4204
+ clock;
3767
4205
  started = performance.now();
3768
4206
  bytes = 0;
4207
+ walks = new DirectoryWalks;
4208
+ constructor(clock = true) {
4209
+ this.clock = clock;
4210
+ }
4211
+ static authority() {
4212
+ return new HistoryReadBudget(false);
4213
+ }
3769
4214
  reserve(bytes) {
3770
- if (!Number.isSafeInteger(bytes) || bytes < 0 || this.bytes + bytes > LIMITS.searchBytes || performance.now() - this.started >= LIMITS.searchMs)
4215
+ if (!Number.isSafeInteger(bytes) || bytes < 0 || this.bytes + bytes > LIMITS.searchBytes || this.elapsed)
3771
4216
  throw new ContractError("receipt-inconclusive");
3772
4217
  this.bytes += bytes;
3773
4218
  }
4219
+ get elapsed() {
4220
+ return this.clock && performance.now() - this.started >= LIMITS.searchMs;
4221
+ }
3774
4222
  }
3775
4223
  function corrupt() {
3776
4224
  throw new ContractError("corrupt-selected");
@@ -3789,14 +4237,14 @@ class HistoryFiles {
3789
4237
  this.coordination = coordination;
3790
4238
  this.format = format;
3791
4239
  this.formatSnapshot = formatSnapshot;
3792
- this.directory = join4(coordination.directory, "history-v2");
4240
+ this.directory = join5(coordination.directory, "history-v2");
3793
4241
  this.files = coordination.files;
3794
4242
  }
3795
4243
  static open(coordination) {
3796
- const dir = join4(coordination.directory, "history-v2");
4244
+ const dir = join5(coordination.directory, "history-v2");
3797
4245
  if (!coordination.files.directory(dir))
3798
4246
  return;
3799
- const file = coordination.files.read(join4(dir, "format.json"));
4247
+ const file = coordination.files.read(join5(dir, "format.json"));
3800
4248
  if (!file)
3801
4249
  corrupt();
3802
4250
  const format = decodeFormat(json(file));
@@ -3812,7 +4260,7 @@ class HistoryFiles {
3812
4260
  existing.assertWritable();
3813
4261
  return existing;
3814
4262
  }
3815
- const dir = join4(c.directory, "history-v2");
4263
+ const dir = join5(c.directory, "history-v2");
3816
4264
  const format = {
3817
4265
  schema: FORMAT,
3818
4266
  storeId: randomUUID4(),
@@ -3822,19 +4270,19 @@ class HistoryFiles {
3822
4270
  c.files.directory(dir, true);
3823
4271
  for (const name of ["commands", "ledger", "runs", "indexes"]) {
3824
4272
  lease.assert();
3825
- c.files.directory(join4(dir, name), true);
4273
+ c.files.directory(join5(dir, name), true);
3826
4274
  }
3827
4275
  lease.assert();
3828
- const keyPath = join4(dir, "read-proof-key.json");
4276
+ const keyPath = join5(dir, "read-proof-key.json");
3829
4277
  c.files.create(keyPath, encoded({
3830
4278
  schema: "cadence-history-read-key/1",
3831
4279
  storeId: format.storeId,
3832
4280
  key: randomBytes2(32).toString("base64url")
3833
4281
  }));
3834
4282
  lease.assert();
3835
- const snapshot = c.files.create(join4(dir, "format.json"), encoded(format));
4283
+ const snapshot = c.files.create(join5(dir, "format.json"), encoded(format));
3836
4284
  const result = new HistoryFiles(c, format, snapshot);
3837
- result.syncParents(join4(dir, "format.json"));
4285
+ result.syncParents(join5(dir, "format.json"));
3838
4286
  result.assertWritable();
3839
4287
  return result;
3840
4288
  }
@@ -3869,7 +4317,7 @@ class HistoryFiles {
3869
4317
  throw new ContractError("quarantine");
3870
4318
  }
3871
4319
  assertFormat() {
3872
- this.files.assert(join4(this.directory, "format.json"), this.formatSnapshot);
4320
+ this.files.assert(join5(this.directory, "format.json"), this.formatSnapshot);
3873
4321
  }
3874
4322
  readProofMaterial(budget) {
3875
4323
  if (this.format.schema !== FORMAT)
@@ -3878,7 +4326,7 @@ class HistoryFiles {
3878
4326
  this.assertReadFormat(budget);
3879
4327
  else
3880
4328
  this.assertFormat();
3881
- const selected2 = this.readBudgeted(join4(this.directory, "read-proof-key.json"), 512, budget);
4329
+ const selected2 = this.readBudgeted(join5(this.directory, "read-proof-key.json"), 512, budget);
3882
4330
  if (!selected2 || this.proofSnapshot && (selected2.identity !== this.proofSnapshot.identity || selected2.stamp !== this.proofSnapshot.stamp))
3883
4331
  corrupt();
3884
4332
  try {
@@ -3906,17 +4354,17 @@ class HistoryFiles {
3906
4354
  return;
3907
4355
  }
3908
4356
  this.assertFormat();
3909
- assertIncarnation(this, join4(this.directory, "read-proof-key.json"), this.proofSnapshot.stamp);
4357
+ assertIncarnation(this, join5(this.directory, "read-proof-key.json"), this.proofSnapshot.stamp);
3910
4358
  }
3911
4359
  segmentPath(segment) {
3912
- return join4(this.directory, "ledger", `${String(segment).padStart(12, "0")}.jsonl`);
4360
+ return join5(this.directory, "ledger", `${String(segment).padStart(12, "0")}.jsonl`);
3913
4361
  }
3914
4362
  artifactPath(ref) {
3915
- return join4(this.directory, "runs", ref.runId, "artifacts", ref.artifactId, `${ref.part}.part`);
4363
+ return join5(this.directory, "runs", ref.runId, "artifacts", ref.artifactId, `${ref.part}.part`);
3916
4364
  }
3917
4365
  admissionPath(runId) {
3918
4366
  decodeId(runId);
3919
- return join4(this.directory, "runs", runId, "admission.json");
4367
+ return join5(this.directory, "runs", runId, "admission.json");
3920
4368
  }
3921
4369
  writeAdmission(lease, input) {
3922
4370
  lease.assert();
@@ -3930,19 +4378,19 @@ class HistoryFiles {
3930
4378
  return this.files.read(path, maximum);
3931
4379
  let size;
3932
4380
  try {
3933
- size = lstatSync4(path).size;
4381
+ size = lstatSync5(path).size;
3934
4382
  } catch {
3935
- return this.files.read(path, 0);
4383
+ return this.files.read(path, 0, budget.walks);
3936
4384
  }
3937
4385
  if (size > maximum)
3938
4386
  corrupt();
3939
4387
  budget.reserve(size);
3940
- return this.files.read(path, size);
4388
+ return this.files.read(path, size, budget.walks);
3941
4389
  }
3942
4390
  readCommand(commandId, budget) {
3943
4391
  decodeId(commandId);
3944
4392
  this.assertReadFormat(budget);
3945
- return this.readBudgeted(join4(this.directory, "commands", commandId, "intent.json"), RECORD_BYTES, budget);
4393
+ return this.readBudgeted(join5(this.directory, "commands", commandId, "intent.json"), RECORD_BYTES, budget);
3946
4394
  }
3947
4395
  readAdmission(runId, identity, budget) {
3948
4396
  return this.readAdmissionIdentity(runId, hash(canonicalJson(identity)), budget);
@@ -3963,7 +4411,7 @@ class HistoryFiles {
3963
4411
  syncParents(file) {
3964
4412
  if (process.platform === "win32")
3965
4413
  return;
3966
- for (let path = dirname5(file);; path = dirname5(path)) {
4414
+ for (let path = dirname6(file);; path = dirname6(path)) {
3967
4415
  if (!this.files.directory(path))
3968
4416
  corrupt();
3969
4417
  const fd = openSync4(path, constants4.O_RDONLY | constants4.O_DIRECTORY);
@@ -3974,13 +4422,13 @@ class HistoryFiles {
3974
4422
  }
3975
4423
  if (path === this.coordination.directory)
3976
4424
  break;
3977
- if (path === dirname5(path))
4425
+ if (path === dirname6(path))
3978
4426
  corrupt();
3979
4427
  }
3980
4428
  }
3981
4429
  assertReadFormat(budget) {
3982
4430
  budget.reserve(this.formatSnapshot.bytes.length);
3983
- this.files.assert(join4(this.directory, "format.json"), this.formatSnapshot, this.formatSnapshot.bytes.length);
4431
+ this.files.assert(join5(this.directory, "format.json"), this.formatSnapshot, this.formatSnapshot.bytes.length);
3984
4432
  }
3985
4433
  assertPublished(segment, end, seq) {
3986
4434
  const fence = publicationFences.get(this.directory);
@@ -4018,7 +4466,7 @@ class HistoryFiles {
4018
4466
  const dir = opendirSync2(path);
4019
4467
  try {
4020
4468
  for (let e = dir.readSync();e; e = dir.readSync()) {
4021
- const p = join4(path, e.name), st = lstatSync4(p);
4469
+ const p = join5(path, e.name), st = lstatSync5(p);
4022
4470
  if (st.isDirectory())
4023
4471
  bytes += walk(p, depth + 1);
4024
4472
  else {
@@ -4053,7 +4501,7 @@ class HistoryFiles {
4053
4501
  state.bytes = 0;
4054
4502
  state.identity = null;
4055
4503
  let count = 0;
4056
- const ledger = join4(this.directory, "ledger");
4504
+ const ledger = join5(this.directory, "ledger");
4057
4505
  if (!this.files.directory(ledger))
4058
4506
  corrupt();
4059
4507
  const dir = opendirSync2(ledger);
@@ -4218,7 +4666,7 @@ class HistoryFiles {
4218
4666
  let committed;
4219
4667
  const fd = await open(path, constants4.O_WRONLY | constants4.O_APPEND | (constants4.O_NOFOLLOW ?? 0));
4220
4668
  const assertOriginal = (bytes) => {
4221
- const original = fstatSync4(fd.fd), selected2 = lstatSync4(path);
4669
+ const original = fstatSync4(fd.fd), selected2 = lstatSync5(path);
4222
4670
  if (original.dev !== selected2.dev || original.ino !== selected2.ino || original.nlink !== 1)
4223
4671
  corrupt();
4224
4672
  const snapshot = this.files.read(path, LIMITS.segmentBytes);
@@ -4335,7 +4783,7 @@ class HistoryFiles {
4335
4783
  try {
4336
4784
  lease.assert();
4337
4785
  this.files.assert(path, before, LIMITS.segmentBytes);
4338
- const opened = await fd.stat(), selected2 = lstatSync4(path);
4786
+ const opened = await fd.stat(), selected2 = lstatSync5(path);
4339
4787
  if (opened.ino !== selected2.ino || opened.dev !== selected2.dev || opened.nlink !== 1)
4340
4788
  corrupt();
4341
4789
  beforeEffect?.();
@@ -4370,7 +4818,7 @@ class HistoryFiles {
4370
4818
  checkpoint(lease, state) {
4371
4819
  lease.assert();
4372
4820
  this.assertWritable();
4373
- const path = join4(this.directory, "checkpoint.json"), old = this.files.read(path);
4821
+ const path = join5(this.directory, "checkpoint.json"), old = this.files.read(path);
4374
4822
  const body = encoded({
4375
4823
  schema: "cadence-history-checkpoint/2",
4376
4824
  storeId: this.format.storeId,
@@ -4783,8 +5231,8 @@ function digestHistory(files, exclude) {
4783
5231
  const walk = (path, depth) => {
4784
5232
  if (depth > 8 || !files.files.directory(path))
4785
5233
  throw new ContractError("corrupt-selected");
4786
- const directory = lstatSync5(path);
4787
- digest.update(`incarnation:${directory.dev}:${directory.ino}
5234
+ const directory2 = lstatSync6(path);
5235
+ digest.update(`incarnation:${directory2.dev}:${directory2.ino}
4788
5236
  `);
4789
5237
  const dir = opendirSync3(path), names2 = [];
4790
5238
  try {
@@ -4797,11 +5245,11 @@ function digestHistory(files, exclude) {
4797
5245
  dir.closeSync();
4798
5246
  }
4799
5247
  for (const name of names2.sort()) {
4800
- const selected2 = join5(path, name);
5248
+ const selected2 = join6(path, name);
4801
5249
  if (selected2 === exclude)
4802
5250
  continue;
4803
5251
  const rel = relative4(files.directory, selected2);
4804
- if (lstatSync5(selected2).isDirectory()) {
5252
+ if (lstatSync6(selected2).isDirectory()) {
4805
5253
  digest.update(`directory:${Buffer.byteLength(rel)}:${rel}
4806
5254
  `);
4807
5255
  walk(selected2, depth + 1);
@@ -5015,7 +5463,7 @@ async function confirm(expected) {
5015
5463
  `resolve ${expected.epoch}`
5016
5464
  ]
5017
5465
  ];
5018
- await new Promise((resolve3, reject) => {
5466
+ await new Promise((resolve4, reject) => {
5019
5467
  let index = 0, line = "", done = false;
5020
5468
  const raw = process.stdin.isRaw;
5021
5469
  const finish = (ok) => {
@@ -5031,7 +5479,7 @@ async function confirm(expected) {
5031
5479
  process.stdout.write(`\x1B[?2004l
5032
5480
  `);
5033
5481
  if (ok)
5034
- resolve3();
5482
+ resolve4();
5035
5483
  else
5036
5484
  reject(new ContractError("forbidden"));
5037
5485
  };
@@ -5107,8 +5555,8 @@ async function localRecovery(c, operation) {
5107
5555
  }
5108
5556
 
5109
5557
  // src/service/runtime.ts
5110
- import { homedir as homedir2 } from "node:os";
5111
- import { dirname as dirname6, isAbsolute as isAbsolute4, join as join6, resolve as resolve3 } from "node:path";
5558
+ import { homedir as homedir3 } from "node:os";
5559
+ import { dirname as dirname7, isAbsolute as isAbsolute5, join as join7, resolve as resolve4 } from "node:path";
5112
5560
 
5113
5561
  // src/service/config.ts
5114
5562
  import { isIP } from "node:net";
@@ -5162,7 +5610,7 @@ function localOrigin(policy, actualPort) {
5162
5610
  var STARTUP_INPUT_BYTES = 16 * 1024;
5163
5611
  function absolute(value) {
5164
5612
  const result = text(value, 4096, true);
5165
- if (!isAbsolute4(result))
5613
+ if (!isAbsolute5(result))
5166
5614
  throw new ContractError("invalid");
5167
5615
  return result;
5168
5616
  }
@@ -5242,7 +5690,7 @@ function selectStart(options, ambient) {
5242
5690
  throw new ContractError("invalid");
5243
5691
  let fromFile;
5244
5692
  if (options.config) {
5245
- const path = resolve3(options.config), f = new PrivateFiles(dirname6(path));
5693
+ const path = resolve4(options.config), f = new PrivateFiles(dirname7(path));
5246
5694
  const bytes = f.read(path);
5247
5695
  if (!bytes)
5248
5696
  throw new ContractError("corrupt-selected");
@@ -5257,11 +5705,11 @@ function selectStart(options, ambient) {
5257
5705
  port: options.port ?? 17423,
5258
5706
  proxy: null
5259
5707
  },
5260
- agentDir: resolve3(options.agentDir ?? ambient.PI_CODING_AGENT_DIR ?? join6(ambient.HOME ?? ambient.USERPROFILE ?? homedir2(), ".pi", "agent")),
5708
+ agentDir: resolve4(options.agentDir ?? ambient.PI_CODING_AGENT_DIR ?? join7(ambient.HOME ?? ambient.USERPROFILE ?? homedir3(), ".pi", "agent")),
5261
5709
  tokenSource: options.tokenFile ? "file" : env ? "environment" : "generated",
5262
- tokenFile: options.tokenFile ? resolve3(options.tokenFile) : null
5710
+ tokenFile: options.tokenFile ? resolve4(options.tokenFile) : null
5263
5711
  });
5264
- if (fromFile && encoded(fromFile) !== encoded(selected2) || options.port !== undefined && options.port !== selected2.http.port || options.host !== undefined && options.host !== selected2.http.host || options.agentDir !== undefined && resolve3(options.agentDir) !== selected2.agentDir || options.tokenFile !== undefined && resolve3(options.tokenFile) !== selected2.tokenFile || env && selected2.tokenSource !== "environment" || selected2.tokenSource === "environment" && !env || resolveWorkspace(selected2.cwd).root !== workspace.root)
5712
+ if (fromFile && encoded(fromFile) !== encoded(selected2) || options.port !== undefined && options.port !== selected2.http.port || options.host !== undefined && options.host !== selected2.http.host || options.agentDir !== undefined && resolve4(options.agentDir) !== selected2.agentDir || options.tokenFile !== undefined && resolve4(options.tokenFile) !== selected2.tokenFile || env && selected2.tokenSource !== "environment" || selected2.tokenSource === "environment" && !env || resolveWorkspace(selected2.cwd).root !== workspace.root)
5265
5713
  throw new ContractError("conflict");
5266
5714
  return {
5267
5715
  workspace: workspace.root,
@@ -5293,21 +5741,37 @@ function portValue(value) {
5293
5741
  throw new ContractError("invalid");
5294
5742
  return integer(Number(value), 0, 65535);
5295
5743
  }
5744
+ var STARTUP_STAGES = [
5745
+ "package",
5746
+ "pi-agent-directory",
5747
+ "pi-auth",
5748
+ "pi-models",
5749
+ "pi-runtime",
5750
+ "service"
5751
+ ];
5752
+ function decodeStartupReport(value) {
5753
+ const o = closed(value, ["status", "error", "stage"]);
5754
+ return {
5755
+ status: oneOf(o.status, ["refused", "quarantined"]),
5756
+ error: oneOf(o.error, ERROR_CODES),
5757
+ stage: oneOf(o.stage, STARTUP_STAGES)
5758
+ };
5759
+ }
5296
5760
 
5297
5761
  // src/service/cli.ts
5298
5762
  function nativeCall(c, operation, reveal = false) {
5299
5763
  const access = c.localAccess();
5300
5764
  const url = new URL(`${access.endpoint}/local/v1/admin/${operation}`);
5301
- const write = !["status", "diagnose"].includes(operation);
5302
- const body = write ? JSON.stringify(reveal ? { reveal: true } : {}) : undefined;
5303
- return new Promise((resolve4, reject) => {
5765
+ const write2 = !["status", "diagnose"].includes(operation);
5766
+ const body = write2 ? JSON.stringify(reveal ? { reveal: true } : {}) : undefined;
5767
+ return new Promise((resolve6, reject) => {
5304
5768
  let timer;
5305
5769
  const fail = () => {
5306
5770
  clearTimeout(timer);
5307
5771
  reject(new ContractError("unavailable"));
5308
5772
  };
5309
5773
  const req = request(url, {
5310
- method: write ? "POST" : "GET",
5774
+ method: write2 ? "POST" : "GET",
5311
5775
  agent: false,
5312
5776
  headers: {
5313
5777
  "X-Cadence-Local": access.capability,
@@ -5341,7 +5805,7 @@ function nativeCall(c, operation, reveal = false) {
5341
5805
  throw new ContractError("stale");
5342
5806
  }
5343
5807
  clearTimeout(timer);
5344
- resolve4(reply.value);
5808
+ resolve6(reply.value);
5345
5809
  } catch {
5346
5810
  fail();
5347
5811
  }
@@ -5356,7 +5820,7 @@ function nativeCall(c, operation, reveal = false) {
5356
5820
  req.end(body);
5357
5821
  });
5358
5822
  }
5359
- var pause = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
5823
+ var pause = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
5360
5824
  async function serviceStatus(c, diagnose = false) {
5361
5825
  const observed = c.inspect();
5362
5826
  if (observed.status === "absent" || observed.status === "stopped")
@@ -5393,7 +5857,38 @@ async function stopService(c) {
5393
5857
  return { status: "unconfirmed", epoch };
5394
5858
  }
5395
5859
  function failedStatus(result) {
5396
- return !!(result && typeof result === "object" && ("status" in result) && ["unconfirmed", "unavailable", "quarantined"].includes(String(result.status)));
5860
+ return !!(result && typeof result === "object" && ("status" in result) && ["unconfirmed", "unavailable", "quarantined", "refused"].includes(String(result.status)));
5861
+ }
5862
+ function startupGuidance(result, input) {
5863
+ let report;
5864
+ try {
5865
+ report = decodeStartupReport(result);
5866
+ } catch {
5867
+ return "";
5868
+ }
5869
+ const agent = input.config.agentDir;
5870
+ const inspect = `Inspect: cadence-service diagnose|recover inspect --workspace ${JSON.stringify(input.workspace)}
5871
+ `;
5872
+ switch (report.stage) {
5873
+ case "package":
5874
+ return `Cadence refused its installed package files (web/dist or agents); reinstall the package rather than editing it.
5875
+ `;
5876
+ case "pi-agent-directory":
5877
+ return `Pi's agent directory ${agent} must be an existing real directory (not a symlink) owned by you.
5878
+ `;
5879
+ case "pi-auth":
5880
+ return `${join8(agent, "auth.json")} must be your own single-link regular file holding valid Pi credentials; log in to Pi first.
5881
+ `;
5882
+ case "pi-models":
5883
+ return `${join8(agent, "models.json")} must be your own single-link regular file using only fields Cadence's pinned Pi SDK supports; a field only newer Pi understands is refused.
5884
+ `;
5885
+ case "pi-runtime":
5886
+ return `Loading Pi's SDK model runtime failed after the owner registered (optional SDK packages missing, or sources the SDK refuses); it stays quarantined.
5887
+ ${inspect}`;
5888
+ default:
5889
+ return report.status === "quarantined" ? `The owner registered, then refused; it stays quarantined.
5890
+ ${inspect}` : "";
5891
+ }
5397
5892
  }
5398
5893
  async function startService(options, foreground, hostUrl) {
5399
5894
  const network = networkSnapshot(process.env);
@@ -5420,18 +5915,18 @@ async function startService(options, foreground, hostUrl) {
5420
5915
  stdio: ["pipe", "pipe", "ignore"]
5421
5916
  });
5422
5917
  let completed = false;
5423
- const closure = new Promise((resolve4) => child.once("close", (code, signal) => {
5918
+ const closure = new Promise((resolve6) => child.once("close", (code, signal) => {
5424
5919
  completed = true;
5425
5920
  if (foreground && (code !== 0 || signal !== null))
5426
5921
  process.exitCode = 1;
5427
- resolve4();
5922
+ resolve6();
5428
5923
  }));
5429
- const startup = new Promise((resolve4) => {
5924
+ const startup = new Promise((resolve6) => {
5430
5925
  let bytes = "", resolved = false;
5431
5926
  const finish = (value) => {
5432
5927
  if (!resolved) {
5433
5928
  resolved = true;
5434
- resolve4(value);
5929
+ resolve6(value);
5435
5930
  }
5436
5931
  };
5437
5932
  child.on("error", () => finish({ status: "unconfirmed" }));
@@ -5447,7 +5942,12 @@ async function startService(options, foreground, hostUrl) {
5447
5942
  `))
5448
5943
  return;
5449
5944
  try {
5450
- const ready = closed(parseWireJson(bytes, 4096), ["status", "epoch"]);
5945
+ const line = parseWireJson(bytes, 4096);
5946
+ if (line && typeof line === "object" && "status" in line && line.status !== "ready") {
5947
+ finish(decodeStartupReport(line));
5948
+ return;
5949
+ }
5950
+ const ready = closed(line, ["status", "epoch"]);
5451
5951
  if (ready.status !== "ready")
5452
5952
  throw new ContractError("unavailable");
5453
5953
  const epoch = decodeId(ready.epoch);
@@ -5466,11 +5966,14 @@ async function startService(options, foreground, hostUrl) {
5466
5966
  let timer;
5467
5967
  const result = await Promise.race([
5468
5968
  startup,
5469
- new Promise((resolve4) => {
5470
- timer = setTimeout(() => resolve4({ status: "unconfirmed" }), LIMITS.startWaitMs);
5969
+ new Promise((resolve6) => {
5970
+ timer = setTimeout(() => resolve6({ status: "unconfirmed" }), LIMITS.startWaitMs);
5471
5971
  })
5472
5972
  ]);
5473
5973
  clearTimeout(timer);
5974
+ const guidance = startupGuidance(result, input);
5975
+ if (guidance)
5976
+ process.stderr.write(guidance);
5474
5977
  child.stdout.destroy();
5475
5978
  child.stdin.destroy();
5476
5979
  if (!foreground)
@@ -5485,6 +5988,127 @@ async function startService(options, foreground, hostUrl) {
5485
5988
  }
5486
5989
  return result;
5487
5990
  }
5991
+ async function observeStarted(workspace) {
5992
+ const deadline = performance.now() + LIMITS.startWaitMs + 2000;
5993
+ let service = "unconfirmed";
5994
+ while (performance.now() < deadline) {
5995
+ try {
5996
+ const c = new ServiceCoordination(workspace);
5997
+ const observed = await serviceStatus(c);
5998
+ if (observed && typeof observed === "object" && "status" in observed)
5999
+ service = String(observed.status);
6000
+ if (service === "alive")
6001
+ return { service, endpoint: c.localAccess().endpoint };
6002
+ if (service === "quarantined")
6003
+ break;
6004
+ } catch {}
6005
+ await pause(250);
6006
+ }
6007
+ return { service };
6008
+ }
6009
+ function autostartGuidance(s, result) {
6010
+ const name = s.unit.replace(/\.service$/, "");
6011
+ const cli = JSON.stringify(join8(s.root, "src", "service-cli.mjs"));
6012
+ const workspace = JSON.stringify(result.workspace ?? "");
6013
+ const manage = `Manage: systemctl --user status|restart|stop ${name}; logs: journalctl --user -u ${name}; opt out: systemctl --user disable --now ${name}`;
6014
+ const note = result.redirectedFrom ? `Cadence workspace: ${result.workspace} (${result.redirectedFrom} contains Cadence's private state)
6015
+ ` : "";
6016
+ return note + autostartStatusGuidance(s, result, name, cli, workspace, manage);
6017
+ }
6018
+ function lingerGuidance(linger) {
6019
+ switch (linger) {
6020
+ case "enabled":
6021
+ return `Linger enabled: Cadence starts at boot and keeps running after logout (undo: loginctl disable-linger).
6022
+ `;
6023
+ case "off":
6024
+ return `Linger is off: Cadence starts at login and stops at logout. Start it at boot: loginctl enable-linger
6025
+ `;
6026
+ case "refused":
6027
+ return `Linger needs authorization: Cadence starts at login and stops at logout. Start it at boot: loginctl enable-linger (may ask for your password)
6028
+ `;
6029
+ default:
6030
+ return "";
6031
+ }
6032
+ }
6033
+ function autostartStatusGuidance(s, result, name, cli, workspace, manage) {
6034
+ switch (result.status) {
6035
+ case "started": {
6036
+ const linger = lingerGuidance(result.linger);
6037
+ if (result.service !== "alive")
6038
+ return `${s.unit} started for ${result.workspace}, but the owner reports ${result.service}.
6039
+ Inspect: node ${cli} diagnose --workspace ${workspace}
6040
+ ${manage}
6041
+ ${linger}`;
6042
+ return `Cadence WebUI: ${result.endpoint} (${s.unit}, workspace ${result.workspace})
6043
+ Enter with your private link: node ${cli} token-link --workspace ${workspace} --reveal
6044
+ ${manage}
6045
+ ${linger}`;
6046
+ }
6047
+ case "waiting-for-pi-login":
6048
+ return `${s.unit} is enabled for ${result.workspace}; it starts once Pi has ${join8(agentDirectory(s), "auth.json")}.
6049
+ Log in to Pi, then: systemctl --user start ${name}
6050
+ ${lingerGuidance(result.linger)}`;
6051
+ case "disabled":
6052
+ return `${s.unit} refreshed for ${result.workspace}; left disabled as configured.
6053
+ `;
6054
+ case "masked":
6055
+ return `${s.unit} is masked; Cadence did not enable or start it.
6056
+ `;
6057
+ case "missing-pi-sdk":
6058
+ return `Cadence did not register or refresh ${s.unit}: the Pi SDK packages pi-coding-agent and pi-ai (scope @earendil-works) do not resolve from ${s.root}, for example after installing without optional dependencies; an owner without them would quarantine.
6059
+ `;
6060
+ case "foreign":
6061
+ return `${result.unit} was not generated by Cadence; left unchanged.
6062
+ `;
6063
+ case "unavailable":
6064
+ return `No systemd user manager; start Cadence explicitly: node ${cli} start --workspace /absolute/project
6065
+ `;
6066
+ case "failed":
6067
+ return `Could not enable or start ${s.unit}; see systemctl --user status ${name} and journalctl --user -u ${name}.
6068
+ ${lingerGuidance(result.linger)}`;
6069
+ default:
6070
+ return "";
6071
+ }
6072
+ }
6073
+ async function autostartMain(argv, hostUrl) {
6074
+ const action = argv.shift();
6075
+ let requested;
6076
+ let linger = false;
6077
+ while (action === "install" && argv.length) {
6078
+ if (argv[0] === "--workspace" && argv[1] && requested === undefined)
6079
+ requested = resolve5(argv.splice(0, 2)[1]);
6080
+ else if (argv[0] === "--linger" && !linger) {
6081
+ argv.shift();
6082
+ linger = true;
6083
+ } else
6084
+ break;
6085
+ }
6086
+ if (argv.length || !["install", "refresh", "remove", "status"].includes(action ?? ""))
6087
+ throw new ContractError("invalid");
6088
+ const s = hostSystem(dirname8(dirname8(fileURLToPath(hostUrl))));
6089
+ let result;
6090
+ if (action === "remove")
6091
+ result = removeAutostart(s);
6092
+ else if (action === "status")
6093
+ result = autostartStatus(s);
6094
+ else {
6095
+ try {
6096
+ result = configureAutostart(s, action === "install" ? "install" : "refresh", requested, linger);
6097
+ } catch (error) {
6098
+ if (error instanceof ContractError && error.code === "invalid")
6099
+ process.stderr.write(`Cadence autostart needs an existing absolute workspace directory (--workspace or CADENCE_WORKSPACE).
6100
+ `);
6101
+ throw error;
6102
+ }
6103
+ if (result.status === "started" && result.workspace)
6104
+ Object.assign(result, await observeStarted(result.workspace));
6105
+ process.stderr.write(autostartGuidance(s, result));
6106
+ }
6107
+ process.stdout.write(`${JSON.stringify(result)}
6108
+ `);
6109
+ if (["failed", "foreign", "unavailable"].includes(result.status))
6110
+ process.exitCode = 1;
6111
+ }
5488
6112
  async function managementMain(argv, hostUrl) {
5489
6113
  try {
5490
6114
  if (!process.versions.node || process.versions.bun)
@@ -5492,12 +6116,17 @@ async function managementMain(argv, hostUrl) {
5492
6116
  const command = argv.shift();
5493
6117
  if (command === "--help" && !argv.length) {
5494
6118
  process.stdout.write(`cadence-service start|status|diagnose|stop|token-link --reveal|token-rotate|recover inspect|recover resolve [--workspace PATH] [--state-home PATH]
5495
- Explicit start only; default detached or --foreground. Start accepts --config, --agent-dir, --host, --port, --token-file.
6119
+ cadence-service autostart install [--workspace PATH] [--linger]|refresh|remove|status
6120
+ Start is explicit, or the Linux systemd user unit that Pi's user-scope install registers (CADENCE_AUTOSTART=0, disable or mask opts out); its first registration also enables linger for boot start when logind allows it without a password (CADENCE_LINGER=0 opts out), and --linger asks for it explicitly. Default detached or --foreground. Start accepts --config, --agent-dir, --host, --port, --token-file.
5496
6121
  Return/quit detaches clients; only explicit Cancel/Stop requests stop. Unconfirmed/quarantined is not stopped.
5497
6122
  Recovery resolve requires this local native terminal, exact unchanged dead-owner resources and separate process/credential/storage acknowledgments. No --yes, network recovery, killing, repair or cleanup.
5498
6123
  `);
5499
6124
  return;
5500
6125
  }
6126
+ if (command === "autostart") {
6127
+ await autostartMain(argv, hostUrl);
6128
+ return;
6129
+ }
5501
6130
  let recovery;
5502
6131
  if (![
5503
6132
  "start",
@@ -5555,10 +6184,23 @@ Recovery resolve requires this local native terminal, exact unchanged dead-owner
5555
6184
  else
5556
6185
  throw new ContractError("invalid");
5557
6186
  }
6187
+ const landing = effectiveWorkspace(options.workspace, options.stateHome);
6188
+ if (landing.redirected) {
6189
+ if (seen.has("--workspace"))
6190
+ process.stderr.write(`Cadence workspace: ${landing.workspace} (${options.workspace} contains Cadence's private state)
6191
+ `);
6192
+ options.workspace = landing.workspace;
6193
+ if (command === "start")
6194
+ mkdirSync4(landing.workspace, { recursive: true });
6195
+ }
5558
6196
  let result;
5559
6197
  if (command === "start")
5560
6198
  result = await startService(options, foreground, hostUrl);
5561
- else {
6199
+ else if (landing.redirected && !existsSync(landing.workspace)) {
6200
+ if (command !== "status" && command !== "diagnose")
6201
+ throw new ContractError("unavailable");
6202
+ result = { status: "absent" };
6203
+ } else {
5562
6204
  const c = new ServiceCoordination(options.workspace, options.stateHome);
5563
6205
  if (command === "recover" && recovery)
5564
6206
  result = await localRecovery(c, recovery);
@@ -5576,6 +6218,8 @@ Recovery resolve requires this local native terminal, exact unchanged dead-owner
5576
6218
  else
5577
6219
  throw new ContractError("invalid");
5578
6220
  }
6221
+ if (landing.redirected && result && typeof result === "object")
6222
+ result = { ...result, workspace: landing.workspace };
5579
6223
  if (result !== null)
5580
6224
  process.stdout.write(`${JSON.stringify(result)}
5581
6225
  `);