@abelxiaoxing/cadence 2.0.0 → 2.0.2

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,297 @@ 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
+ };
2515
+ }
2516
+ function expandHome(value, home) {
2517
+ if (value === "~")
2518
+ return home;
2519
+ return value.startsWith("~/") ? join2(home, value.slice(2)) : value;
2520
+ }
2521
+ function agentDirectory(s) {
2522
+ const selected = s.env.PI_CODING_AGENT_DIR;
2523
+ return resolve2(selected ? expandHome(selected, s.home) : join2(s.home, ".pi", "agent"));
2524
+ }
2525
+ function piUserInstall(s) {
2526
+ try {
2527
+ const managed = join2(agentDirectory(s), "npm", "node_modules", "@abelxiaoxing", "cadence");
2528
+ return realpathSync2(managed) === realpathSync2(s.root);
2529
+ } catch {
2530
+ return false;
2531
+ }
2532
+ }
2533
+ function sdkPackages(root) {
2534
+ let start;
2535
+ try {
2536
+ start = realpathSync2(join2(root, "src"));
2537
+ } catch {
2538
+ return;
2539
+ }
2540
+ const found = [];
2541
+ for (const name of ["pi-coding-agent", "pi-ai"]) {
2542
+ let hit;
2543
+ for (let dir = start;!hit; dir = dirname2(dir)) {
2544
+ const candidate = join2(dir, "node_modules", "@earendil-works", name, "package.json");
2545
+ if (regularFile(candidate))
2546
+ hit = candidate;
2547
+ else if (dirname2(dir) === dir)
2548
+ return;
2549
+ }
2550
+ found.push(hit);
2551
+ }
2552
+ return found;
2553
+ }
2554
+ function unitPath(s) {
2555
+ const data = s.env.XDG_DATA_HOME;
2556
+ return join2(data && isAbsolute2(data) ? data : join2(s.home, ".local", "share"), "systemd", "user", s.unit);
2557
+ }
2558
+ function servicePath(value) {
2559
+ const kept = (value ?? "").split(delimiter).filter((entry) => isAbsolute2(entry) && !entry.endsWith(`${sep2}node_modules${sep2}.bin`) && !entry.includes(`${sep2}node-gyp-bin`));
2560
+ return kept.length ? [...new Set(kept)].join(delimiter) : "/usr/local/bin:/usr/bin:/bin";
2561
+ }
2562
+ function serviceEnvironment(s) {
2563
+ const out = [["PATH", servicePath(s.env.PATH)]];
2564
+ for (const key of ["LANG", "LC_ALL"]) {
2565
+ const value = s.env[key];
2566
+ if (value)
2567
+ out.push([key, value]);
2568
+ }
2569
+ const state = s.env.XDG_STATE_HOME;
2570
+ if (state && isAbsolute2(state))
2571
+ out.push(["XDG_STATE_HOME", state]);
2572
+ if (s.env.PI_CODING_AGENT_DIR)
2573
+ out.push(["PI_CODING_AGENT_DIR", agentDirectory(s)]);
2574
+ return out;
2575
+ }
2576
+ function checked(value) {
2577
+ if (/[\u0000-\u001f\u007f]/.test(value))
2578
+ throw new ContractError("invalid");
2579
+ return value;
2580
+ }
2581
+ var specifiers = (value) => checked(value).replaceAll("%", "%%");
2582
+ var quoted = (value) => `"${specifiers(value).replace(/["\\]/g, "\\$&")}"`;
2583
+ var argument = (value) => quoted(value).replaceAll("$", "$$$$");
2584
+ function buildUnit(u) {
2585
+ const command = (args) => [u.node, u.cli, ...args].map(argument).join(" ");
2586
+ return [
2587
+ `${MARKER} on Pi package install/update; upgrades rewrite this file.`,
2588
+ "# Override with `systemctl --user edit cadence`; opt out with `systemctl --user disable --now cadence` or `mask`.",
2589
+ "[Unit]",
2590
+ "Description=Cadence shared task service and WebUI",
2591
+ "Documentation=https://github.com/abelxiaoxing/cadence/blob/main/docs/service-management.md",
2592
+ `ConditionPathExists=${specifiers(u.cli)}`,
2593
+ `ConditionPathExists=${specifiers(join2(u.agentDir, "auth.json"))}`,
2594
+ ...u.sdk.map((path) => `ConditionPathExists=${specifiers(path)}`),
2595
+ "StartLimitIntervalSec=300",
2596
+ "StartLimitBurst=3",
2597
+ "",
2598
+ "[Service]",
2599
+ "Type=simple",
2600
+ `WorkingDirectory=${specifiers(u.workspace)}`,
2601
+ ...u.environment.map(([k, v]) => `Environment=${quoted(`${k}=${v}`)}`),
2602
+ `ExecStart=${command(["start", "--foreground", "--workspace", u.workspace])}`,
2603
+ `ExecStop=-${command(["stop", "--workspace", u.workspace])}`,
2604
+ "KillMode=mixed",
2605
+ "TimeoutStopSec=90",
2606
+ "Restart=on-failure",
2607
+ "RestartSec=10",
2608
+ "",
2609
+ "[Install]",
2610
+ "WantedBy=default.target",
2611
+ ""
2612
+ ].join(`
2613
+ `);
2614
+ }
2615
+ function readGenerated(path) {
2616
+ try {
2617
+ if (!lstatSync2(path).isFile())
2618
+ return null;
2619
+ const text2 = readFileSync(path, "utf8");
2620
+ return text2.startsWith(MARKER) ? text2 : null;
2621
+ } catch (error) {
2622
+ if (error.code === "ENOENT")
2623
+ return;
2624
+ throw error;
2625
+ }
2626
+ }
2627
+ function generatedWorkspace(text2) {
2628
+ return text2?.match(/^WorkingDirectory=(.+)$/m)?.[1].replaceAll("%%", "%");
2629
+ }
2630
+ function directory(path) {
2631
+ try {
2632
+ return isAbsolute2(path) && statSync(path).isDirectory();
2633
+ } catch {
2634
+ return false;
2635
+ }
2636
+ }
2637
+ function unitStateHome(s) {
2638
+ const state = s.env.XDG_STATE_HOME;
2639
+ return state && isAbsolute2(state) ? state : join2(s.home, ".local", "state");
2640
+ }
2641
+ function selectWorkspace(s, requested, previous) {
2642
+ let chosen;
2643
+ if (requested) {
2644
+ chosen = expandHome(requested, s.home);
2645
+ if (!directory(chosen))
2646
+ throw new ContractError("invalid");
2647
+ chosen = resolve2(chosen);
2648
+ } else {
2649
+ const kept = generatedWorkspace(previous);
2650
+ if (kept && directory(kept))
2651
+ chosen = kept;
2652
+ }
2653
+ const landing = chosen ? effectiveWorkspace(chosen, unitStateHome(s), s.home) : { workspace: join2(s.home, LANDING_WORKSPACE), redirected: false };
2654
+ if (landing.workspace !== chosen)
2655
+ mkdirSync2(landing.workspace, { recursive: true });
2656
+ return landing.redirected ? { workspace: landing.workspace, redirectedFrom: chosen } : { workspace: landing.workspace };
2657
+ }
2658
+ function write(path, content) {
2659
+ mkdirSync2(dirname2(path), { recursive: true });
2660
+ const temporary = `${path}.${process.pid}.tmp`;
2661
+ try {
2662
+ writeFileSync2(temporary, content, { mode: 420, flag: "wx" });
2663
+ renameSync2(temporary, path);
2664
+ } finally {
2665
+ rmSync(temporary, { force: true });
2666
+ }
2667
+ }
2668
+ function regularFile(path) {
2669
+ try {
2670
+ return statSync(path).isFile();
2671
+ } catch {
2672
+ return false;
2673
+ }
2674
+ }
2675
+ function configureAutostart(s, mode, requested) {
2676
+ if (s.platform !== "linux")
2677
+ return { status: "skipped" };
2678
+ if (mode === "refresh" && (s.env.CADENCE_AUTOSTART === "0" || !piUserInstall(s)))
2679
+ return { status: "skipped" };
2680
+ if (s.systemctl(["show-environment"]).status !== 0)
2681
+ return { status: "unavailable" };
2682
+ const path = unitPath(s);
2683
+ const previous = readGenerated(path);
2684
+ if (previous === null)
2685
+ return { status: "foreign", unit: path };
2686
+ const sdk = sdkPackages(s.root);
2687
+ if (!sdk)
2688
+ return { status: "missing-pi-sdk" };
2689
+ const { workspace, redirectedFrom } = selectWorkspace(s, requested ?? s.env.CADENCE_WORKSPACE, previous);
2690
+ const agentDir = agentDirectory(s);
2691
+ const content = buildUnit({
2692
+ node: s.node,
2693
+ cli: join2(s.root, "src", "service-cli.mjs"),
2694
+ workspace,
2695
+ agentDir,
2696
+ sdk,
2697
+ environment: serviceEnvironment(s)
2698
+ });
2699
+ const enabled = s.systemctl(["is-enabled", s.unit]).stdout.trim();
2700
+ const active = s.systemctl(["is-active", "--quiet", s.unit]).status === 0;
2701
+ if (active)
2702
+ s.systemctl(["stop", s.unit]);
2703
+ write(path, content);
2704
+ s.systemctl(["daemon-reload"]);
2705
+ const result = {
2706
+ unit: path,
2707
+ workspace,
2708
+ ...redirectedFrom ? { redirectedFrom } : {}
2709
+ };
2710
+ if (enabled.startsWith("masked"))
2711
+ return { status: "masked", ...result };
2712
+ const wanted = mode === "install" || previous === undefined || enabled === "enabled";
2713
+ if (wanted && enabled !== "enabled" && s.systemctl(["enable", s.unit]).status !== 0)
2714
+ return { status: "failed", ...result };
2715
+ if (!wanted && !active)
2716
+ return { status: "disabled", ...result };
2717
+ if (!regularFile(join2(agentDir, "auth.json")))
2718
+ return { status: "waiting-for-pi-login", ...result };
2719
+ s.systemctl(["reset-failed", s.unit]);
2720
+ const started = s.systemctl(["start", s.unit]).status === 0;
2721
+ return { status: started ? "started" : "failed", ...result };
2722
+ }
2723
+ function removeAutostart(s) {
2724
+ if (s.platform !== "linux")
2725
+ return { status: "skipped" };
2726
+ const path = unitPath(s);
2727
+ const previous = readGenerated(path);
2728
+ if (previous === null)
2729
+ return { status: "foreign", unit: path };
2730
+ if (previous === undefined)
2731
+ return { status: "absent" };
2732
+ if (s.systemctl(["show-environment"]).status !== 0)
2733
+ return { status: "unavailable", unit: path };
2734
+ if (s.systemctl(["disable", "--now", s.unit]).status !== 0)
2735
+ return { status: "failed", unit: path };
2736
+ rmSync(path);
2737
+ s.systemctl(["daemon-reload"]);
2738
+ s.systemctl(["reset-failed", s.unit]);
2739
+ return { status: "removed", unit: path };
2740
+ }
2741
+ function autostartStatus(s) {
2742
+ if (s.platform !== "linux")
2743
+ return { status: "skipped" };
2744
+ const path = unitPath(s);
2745
+ const previous = readGenerated(path);
2746
+ if (previous === null)
2747
+ return { status: "foreign", unit: path };
2748
+ if (previous === undefined)
2749
+ return { status: "absent" };
2750
+ return {
2751
+ status: s.systemctl(["is-active", s.unit]).stdout.trim() || "unknown",
2752
+ enabled: s.systemctl(["is-enabled", s.unit]).stdout.trim() || "unknown",
2753
+ unit: path,
2754
+ workspace: generatedWorkspace(previous)
2755
+ };
2756
+ }
2757
+
2758
+ // src/service/coordination.ts
2759
+ import { randomBytes, randomUUID as randomUUID3 } from "node:crypto";
2760
+ import { opendirSync, realpathSync as realpathSync3 } from "node:fs";
2761
+ import { hostname as hostname2 } from "node:os";
2762
+ import { dirname as dirname4, isAbsolute as isAbsolute4, join as join4, relative as relative3, sep as sep4 } from "node:path";
2763
+
2439
2764
  // src/service/private-files.ts
2440
2765
  import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
2441
2766
  import {
@@ -2443,13 +2768,15 @@ import {
2443
2768
  constants as constants2,
2444
2769
  fstatSync as fstatSync2,
2445
2770
  fsyncSync as fsyncSync2,
2446
- lstatSync as lstatSync2,
2771
+ lstatSync as lstatSync3,
2772
+ mkdirSync as mkdirSync3,
2447
2773
  openSync as openSync2,
2448
- renameSync as renameSync2,
2774
+ readSync as readSync2,
2775
+ renameSync as renameSync3,
2449
2776
  unlinkSync as unlinkSync2,
2450
- writeFileSync as writeFileSync2
2777
+ writeFileSync as writeFileSync3
2451
2778
  } 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";
2779
+ import { dirname as dirname3, isAbsolute as isAbsolute3, join as join3, relative as relative2, resolve as resolve3, sep as sep3 } from "node:path";
2453
2780
  var METADATA_BYTES = 16 * 1024;
2454
2781
  var REGISTRY_BYTES = 1024 * 1024;
2455
2782
  var REGISTRY_ENTRIES = 128;
@@ -2469,42 +2796,112 @@ function same(a, b) {
2469
2796
  function missing(error) {
2470
2797
  return error instanceof Error && "code" in error && error.code === "ENOENT";
2471
2798
  }
2799
+ var job = 0;
2800
+ var advancing = false;
2801
+ function currentJob() {
2802
+ if (!advancing) {
2803
+ advancing = true;
2804
+ queueMicrotask(() => {
2805
+ job++;
2806
+ advancing = false;
2807
+ });
2808
+ }
2809
+ return job;
2810
+ }
2811
+
2812
+ class DirectoryWalks {
2813
+ job = currentJob();
2814
+ walked = new Map;
2815
+ open() {
2816
+ if (this.job === job)
2817
+ return true;
2818
+ this.walked.clear();
2819
+ return false;
2820
+ }
2821
+ remember(path, st) {
2822
+ if (this.open())
2823
+ this.walked.set(path, { dev: st.dev, ino: st.ino });
2824
+ }
2825
+ reuse(path) {
2826
+ const walked = this.open() ? this.walked.get(path) : undefined;
2827
+ if (!walked)
2828
+ return false;
2829
+ let st;
2830
+ try {
2831
+ st = lstatSync3(path);
2832
+ } catch (error) {
2833
+ if (!missing(error))
2834
+ throw error;
2835
+ this.walked.delete(path);
2836
+ return false;
2837
+ }
2838
+ if (!st.isDirectory() || st.isSymbolicLink() || st.dev !== walked.dev || st.ino !== walked.ino) {
2839
+ this.walked.delete(path);
2840
+ return false;
2841
+ }
2842
+ privateMode(st);
2843
+ return true;
2844
+ }
2845
+ }
2472
2846
 
2473
2847
  class PrivateFiles {
2474
2848
  namespace;
2475
2849
  constructor(namespace) {
2476
2850
  this.namespace = namespace;
2477
2851
  }
2478
- directory(path, create = false) {
2852
+ directory(path, create = false, walks) {
2479
2853
  const tail = relative2(this.namespace, path);
2480
- if (isAbsolute2(tail) || tail === ".." || tail.startsWith(`..${sep2}`))
2854
+ if (isAbsolute3(tail) || tail === ".." || tail.startsWith(`..${sep3}`))
2481
2855
  refused();
2482
2856
  if (!create) {
2483
- const namespace = resolve2(this.namespace);
2484
- let inside = false;
2485
- return safeDirectory(path, false, (at2, st) => {
2857
+ const full = resolve3(path);
2858
+ if (walks?.reuse(full))
2859
+ return true;
2860
+ const namespace = resolve3(this.namespace);
2861
+ let inside = false, last;
2862
+ const found = safeDirectory(path, false, (at2, st) => {
2486
2863
  if (at2 === namespace)
2487
2864
  inside = true;
2488
2865
  if (inside)
2489
2866
  privateMode(st);
2867
+ last = st;
2490
2868
  });
2869
+ if (found && last)
2870
+ walks?.remember(full, last);
2871
+ return found;
2491
2872
  }
2873
+ if (!safeDirectory(this.namespace, true))
2874
+ return false;
2875
+ privateMode(lstatSync3(this.namespace));
2492
2876
  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));
2877
+ for (const part of tail.split(sep3).filter(Boolean)) {
2878
+ at = join3(at, part);
2879
+ let st;
2880
+ try {
2881
+ st = lstatSync3(at);
2882
+ } catch (error) {
2883
+ if (!missing(error))
2884
+ throw error;
2885
+ try {
2886
+ mkdirSync3(at, { mode: 448 });
2887
+ } catch (mkdirError) {
2888
+ if (!(mkdirError instanceof Error && ("code" in mkdirError) && mkdirError.code === "EEXIST"))
2889
+ throw mkdirError;
2890
+ }
2891
+ st = lstatSync3(at);
2892
+ }
2893
+ if (!st.isDirectory() || st.isSymbolicLink())
2894
+ throw new Error("Unsafe directory");
2895
+ privateMode(st);
2499
2896
  }
2500
2897
  return true;
2501
2898
  }
2502
- read(path, maximum = METADATA_BYTES) {
2503
- if (!this.directory(dirname2(path)))
2899
+ read(path, maximum = METADATA_BYTES, walks) {
2900
+ if (!this.directory(dirname3(path), false, walks))
2504
2901
  return;
2505
2902
  let before;
2506
2903
  try {
2507
- before = lstatSync2(path);
2904
+ before = lstatSync3(path);
2508
2905
  } catch (error) {
2509
2906
  if (missing(error))
2510
2907
  return;
@@ -2518,31 +2915,39 @@ class PrivateFiles {
2518
2915
  const opened = fstatSync2(fd);
2519
2916
  if (!same(before, opened))
2520
2917
  refused();
2521
- const bytes = readBounded(path, opened.size);
2522
- if (!bytes || bytes.length !== opened.size || !same(opened, fstatSync2(fd)) || !same(opened, lstatSync2(path)))
2918
+ const buffer = Buffer.alloc(opened.size + 1);
2919
+ let size = 0;
2920
+ while (size < buffer.length) {
2921
+ const n = readSync2(fd, buffer, size, buffer.length - size, null);
2922
+ if (!n)
2923
+ break;
2924
+ size += n;
2925
+ }
2926
+ const bytes = buffer.subarray(0, size);
2927
+ if (size !== opened.size || !same(opened, fstatSync2(fd)) || !same(opened, lstatSync3(path)))
2523
2928
  refused();
2524
2929
  return { bytes, identity: hash(bytes), stamp: stamp(opened) };
2525
2930
  } finally {
2526
2931
  closeSync2(fd);
2527
2932
  }
2528
2933
  }
2529
- assert(path, expected, maximum = METADATA_BYTES) {
2530
- const current = this.read(path, maximum);
2934
+ assert(path, expected, maximum = METADATA_BYTES, walks) {
2935
+ const current = this.read(path, maximum, walks);
2531
2936
  if (current?.identity !== expected?.identity || current?.stamp !== expected?.stamp)
2532
2937
  throw new ContractError("conflict");
2533
2938
  }
2534
2939
  create(path, bytes, maximum = METADATA_BYTES) {
2535
2940
  if (Buffer.byteLength(bytes) > maximum)
2536
2941
  throw new ContractError("oversize");
2537
- this.directory(dirname2(path), true);
2538
- const fd = openSync2(path, "wx", 384);
2942
+ this.directory(dirname3(path), true);
2943
+ const fd = retryWindowsFileOperation(() => openSync2(path, "wx", 384));
2539
2944
  try {
2540
- writeFileSync2(fd, bytes);
2945
+ writeFileSync3(fd, bytes);
2541
2946
  fsyncSync2(fd);
2542
2947
  } finally {
2543
2948
  closeSync2(fd);
2544
2949
  }
2545
- this.syncDirectory(dirname2(path));
2950
+ this.syncDirectory(dirname3(path));
2546
2951
  const result = this.read(path, maximum);
2547
2952
  if (!result || result.identity !== hash(bytes))
2548
2953
  refused();
@@ -2550,11 +2955,11 @@ class PrivateFiles {
2550
2955
  }
2551
2956
  replace(path, expected, bytes, maximum = METADATA_BYTES) {
2552
2957
  this.assert(path, expected, maximum);
2553
- const temporary = join2(dirname2(path), `pending-${randomUUID2()}.json`);
2958
+ const temporary = join3(dirname3(path), `pending-${randomUUID2()}.json`);
2554
2959
  this.create(temporary, bytes, maximum);
2555
2960
  this.assert(path, expected, maximum);
2556
- renameSync2(temporary, path);
2557
- this.syncDirectory(dirname2(path));
2961
+ retryWindowsFileOperation(() => renameSync3(temporary, path), () => this.assert(path, expected, maximum));
2962
+ this.syncDirectory(dirname3(path));
2558
2963
  const result = this.read(path, maximum);
2559
2964
  if (!result || result.identity !== hash(bytes))
2560
2965
  refused();
@@ -2562,8 +2967,8 @@ class PrivateFiles {
2562
2967
  }
2563
2968
  remove(path, expected, maximum = METADATA_BYTES) {
2564
2969
  this.assert(path, expected, maximum);
2565
- unlinkSync2(path);
2566
- this.syncDirectory(dirname2(path));
2970
+ retryWindowsFileOperation(() => unlinkSync2(path), () => this.assert(path, expected, maximum));
2971
+ this.syncDirectory(dirname3(path));
2567
2972
  }
2568
2973
  syncDirectory(path) {
2569
2974
  if (process.platform === "win32")
@@ -2617,7 +3022,7 @@ var registrationKeys = [
2617
3022
  var reservationKeys = registrationKeys.filter((k) => !["schema", "phase", "checkoutToken"].includes(k));
2618
3023
  function pathValue(value) {
2619
3024
  const path = text(value, 4096, true);
2620
- if (!isAbsolute3(path))
3025
+ if (!isAbsolute4(path))
2621
3026
  refused();
2622
3027
  return path;
2623
3028
  }
@@ -2674,7 +3079,7 @@ function decodeRegistration(value) {
2674
3079
  function rootsOverlap(a, b) {
2675
3080
  const within = (x, y) => {
2676
3081
  const r = relative3(x, y);
2677
- return r === "" || r !== ".." && !r.startsWith(`..${sep3}`) && !isAbsolute3(r);
3082
+ return r === "" || r !== ".." && !r.startsWith(`..${sep4}`) && !isAbsolute4(r);
2678
3083
  };
2679
3084
  return within(a, b) || within(b, a);
2680
3085
  }
@@ -2726,17 +3131,17 @@ class ServiceCoordination {
2726
3131
  stateHome;
2727
3132
  constructor(workspace, stateHome) {
2728
3133
  this.store = new TaskStore(resolveWorkspace(workspace).root, stateHome);
2729
- const namespace = dirname3(this.store.privateRoot);
3134
+ const namespace = dirname4(this.store.privateRoot);
2730
3135
  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");
3136
+ this.stateHome = dirname4(namespace);
3137
+ this.directory = join4(this.store.privateRoot, "service-v1");
3138
+ this.registryDirectory = join4(namespace, "service-registry-v1");
2734
3139
  }
2735
3140
  path(name) {
2736
- return join3(this.directory, name);
3141
+ return join4(this.directory, name);
2737
3142
  }
2738
3143
  registryPath(name) {
2739
- return join3(this.registryDirectory, name);
3144
+ return join4(this.registryDirectory, name);
2740
3145
  }
2741
3146
  registry() {
2742
3147
  if (!this.files.directory(this.registryDirectory))
@@ -2756,7 +3161,7 @@ class ServiceCoordination {
2756
3161
  const o = closed(json(snapshot, REGISTRY_BYTES), ["schema", "entries"]);
2757
3162
  const entries = list(o.entries, REGISTRY_ENTRIES, decodeReservation);
2758
3163
  for (const [i, row] of entries.entries()) {
2759
- if (row.stateHome !== this.stateHome || realpathSync2(row.root) !== row.root)
3164
+ if (row.stateHome !== this.stateHome || realpathSync3(row.root) !== row.root)
2760
3165
  refused();
2761
3166
  if (entries.slice(0, i).some((other) => rootsOverlap(row.root, other.root) || row.endpoint === other.endpoint))
2762
3167
  refused();
@@ -2770,8 +3175,8 @@ class ServiceCoordination {
2770
3175
  }
2771
3176
  };
2772
3177
  }
2773
- assertNamespace() {
2774
- if (!this.files.directory(this.directory))
3178
+ assertNamespace(walks) {
3179
+ if (!this.files.directory(this.directory, false, walks))
2775
3180
  refused();
2776
3181
  names(this.directory, [
2777
3182
  "format.json",
@@ -2784,14 +3189,15 @@ class ServiceCoordination {
2784
3189
  "host-config.json",
2785
3190
  "history-v2"
2786
3191
  ]);
2787
- this.files.directory(join3(this.directory, "history-v2"));
3192
+ this.files.directory(join4(this.directory, "history-v2"), false, walks);
2788
3193
  }
2789
3194
  selected() {
2790
- if (!this.files.directory(this.directory))
3195
+ const walks = new DirectoryWalks;
3196
+ if (!this.files.directory(this.directory, false, walks))
2791
3197
  return;
2792
- this.assertNamespace();
2793
- const manifest = this.files.read(this.path("format.json"));
2794
- const config = this.files.read(this.path("config.json"));
3198
+ this.assertNamespace(walks);
3199
+ const manifest = this.files.read(this.path("format.json"), undefined, walks);
3200
+ const config = this.files.read(this.path("config.json"), undefined, walks);
2795
3201
  if (!manifest || !config)
2796
3202
  refused();
2797
3203
  const f = closed(json(manifest), [
@@ -2805,13 +3211,13 @@ class ServiceCoordination {
2805
3211
  literal(f.stateHome, this.stateHome);
2806
3212
  literal(f.configIdentity, config.identity);
2807
3213
  const c = decodeOwnerConfig(json(config));
2808
- const hostConfig = this.files.read(this.path("host-config.json"));
3214
+ const hostConfig = this.files.read(this.path("host-config.json"), undefined, walks);
2809
3215
  if (hostConfig && hostConfig.identity !== c.policyIdentity)
2810
3216
  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);
3217
+ const registration = this.files.read(this.path("registration.json"), undefined, walks);
3218
+ const owner = this.files.read(this.path("owner.lock"), undefined, walks);
3219
+ const capability = this.files.read(this.path("local-capability.json"), undefined, walks);
3220
+ const checkout = this.files.read(this.store.lockPath, 8192, walks);
2815
3221
  const value = registration ? decodeRegistration(json(registration)) : undefined;
2816
3222
  if (value && (value.root !== this.store.root || value.stateHome !== this.stateHome || value.configIdentity !== config.identity || !endpointMatches(c.endpoint, value.endpoint)))
2817
3223
  refused();
@@ -3158,8 +3564,8 @@ class OwnerLease {
3158
3564
  assert() {
3159
3565
  if (this.released || this.selected.value.pid !== process.pid || this.selected.value.host !== hostname2())
3160
3566
  throw new ContractError("conflict");
3161
- const c = this.coordination;
3162
- c.assertNamespace();
3567
+ const c = this.coordination, walks = new DirectoryWalks;
3568
+ c.assertNamespace(walks);
3163
3569
  for (const [name, snapshot] of [
3164
3570
  ["config.json", this.selected.config],
3165
3571
  ["host-config.json", this.selected.hostConfig],
@@ -3168,8 +3574,8 @@ class OwnerLease {
3168
3574
  ["owner.lock", this.selected.owner],
3169
3575
  ["local-capability.json", this.selected.capability]
3170
3576
  ])
3171
- c.files.assert(c.path(name), snapshot);
3172
- c.files.assert(c.store.lockPath, this.selected.checkout, 8192);
3577
+ c.files.assert(c.path(name), snapshot, undefined, walks);
3578
+ c.files.assert(c.store.lockPath, this.selected.checkout, 8192, walks);
3173
3579
  c.store.assertOwner(this.checkout);
3174
3580
  }
3175
3581
  phase(phase) {
@@ -3208,8 +3614,8 @@ import { isatty } from "node:tty";
3208
3614
 
3209
3615
  // src/history/recovery.ts
3210
3616
  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";
3617
+ import { lstatSync as lstatSync6, opendirSync as opendirSync3 } from "node:fs";
3618
+ import { join as join6, relative as relative4 } from "node:path";
3213
3619
 
3214
3620
  // src/service/recovery.ts
3215
3621
  async function inspectRecovery(coordination, history) {
@@ -3263,13 +3669,13 @@ import {
3263
3669
  constants as constants4,
3264
3670
  fstatSync as fstatSync4,
3265
3671
  fsyncSync as fsyncSync3,
3266
- lstatSync as lstatSync4,
3672
+ lstatSync as lstatSync5,
3267
3673
  opendirSync as opendirSync2,
3268
3674
  openSync as openSync4,
3269
3675
  statfsSync
3270
3676
  } from "node:fs";
3271
3677
  import { open } from "node:fs/promises";
3272
- import { dirname as dirname5, join as join4 } from "node:path";
3678
+ import { dirname as dirname6, join as join5 } from "node:path";
3273
3679
 
3274
3680
  // src/history/admission.ts
3275
3681
  function decodeRunAdmission(value) {
@@ -3293,19 +3699,19 @@ import {
3293
3699
  closeSync as closeSync3,
3294
3700
  constants as constants3,
3295
3701
  fstatSync as fstatSync3,
3296
- lstatSync as lstatSync3,
3702
+ lstatSync as lstatSync4,
3297
3703
  openSync as openSync3,
3298
- readSync as readSync2
3704
+ readSync as readSync3
3299
3705
  } from "node:fs";
3300
- import { dirname as dirname4 } from "node:path";
3706
+ import { dirname as dirname5 } from "node:path";
3301
3707
  function bad() {
3302
3708
  throw new ContractError("corrupt-selected");
3303
3709
  }
3304
3710
  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)))
3711
+ function selected(files, path, read, walks) {
3712
+ if (!files.files.directory(dirname5(path), false, walks))
3307
3713
  bad();
3308
- const before = lstatSync3(path);
3714
+ const before = lstatSync4(path);
3309
3715
  if (!before.isFile() || before.nlink !== 1 || process.platform !== "win32" && (before.mode & 63 || process.getuid && before.uid !== process.getuid()))
3310
3716
  bad();
3311
3717
  const fd = openSync3(path, constants3.O_RDONLY | (constants3.O_NOFOLLOW ?? 0));
@@ -3313,7 +3719,7 @@ function selected(files, path, read) {
3313
3719
  if (stamp2(fstatSync3(fd)) !== stamp2(before))
3314
3720
  bad();
3315
3721
  const result = read(fd, before.size, stamp2(before));
3316
- if (stamp2(fstatSync3(fd)) !== stamp2(before) || stamp2(lstatSync3(path)) !== stamp2(before))
3722
+ if (stamp2(fstatSync3(fd)) !== stamp2(before) || stamp2(lstatSync4(path)) !== stamp2(before))
3317
3723
  bad();
3318
3724
  return result;
3319
3725
  } finally {
@@ -3764,13 +4170,24 @@ function decodeJournalText(line) {
3764
4170
 
3765
4171
  // src/history/files.ts
3766
4172
  class HistoryReadBudget {
4173
+ clock;
3767
4174
  started = performance.now();
3768
4175
  bytes = 0;
4176
+ walks = new DirectoryWalks;
4177
+ constructor(clock = true) {
4178
+ this.clock = clock;
4179
+ }
4180
+ static authority() {
4181
+ return new HistoryReadBudget(false);
4182
+ }
3769
4183
  reserve(bytes) {
3770
- if (!Number.isSafeInteger(bytes) || bytes < 0 || this.bytes + bytes > LIMITS.searchBytes || performance.now() - this.started >= LIMITS.searchMs)
4184
+ if (!Number.isSafeInteger(bytes) || bytes < 0 || this.bytes + bytes > LIMITS.searchBytes || this.elapsed)
3771
4185
  throw new ContractError("receipt-inconclusive");
3772
4186
  this.bytes += bytes;
3773
4187
  }
4188
+ get elapsed() {
4189
+ return this.clock && performance.now() - this.started >= LIMITS.searchMs;
4190
+ }
3774
4191
  }
3775
4192
  function corrupt() {
3776
4193
  throw new ContractError("corrupt-selected");
@@ -3789,14 +4206,14 @@ class HistoryFiles {
3789
4206
  this.coordination = coordination;
3790
4207
  this.format = format;
3791
4208
  this.formatSnapshot = formatSnapshot;
3792
- this.directory = join4(coordination.directory, "history-v2");
4209
+ this.directory = join5(coordination.directory, "history-v2");
3793
4210
  this.files = coordination.files;
3794
4211
  }
3795
4212
  static open(coordination) {
3796
- const dir = join4(coordination.directory, "history-v2");
4213
+ const dir = join5(coordination.directory, "history-v2");
3797
4214
  if (!coordination.files.directory(dir))
3798
4215
  return;
3799
- const file = coordination.files.read(join4(dir, "format.json"));
4216
+ const file = coordination.files.read(join5(dir, "format.json"));
3800
4217
  if (!file)
3801
4218
  corrupt();
3802
4219
  const format = decodeFormat(json(file));
@@ -3812,7 +4229,7 @@ class HistoryFiles {
3812
4229
  existing.assertWritable();
3813
4230
  return existing;
3814
4231
  }
3815
- const dir = join4(c.directory, "history-v2");
4232
+ const dir = join5(c.directory, "history-v2");
3816
4233
  const format = {
3817
4234
  schema: FORMAT,
3818
4235
  storeId: randomUUID4(),
@@ -3822,19 +4239,19 @@ class HistoryFiles {
3822
4239
  c.files.directory(dir, true);
3823
4240
  for (const name of ["commands", "ledger", "runs", "indexes"]) {
3824
4241
  lease.assert();
3825
- c.files.directory(join4(dir, name), true);
4242
+ c.files.directory(join5(dir, name), true);
3826
4243
  }
3827
4244
  lease.assert();
3828
- const keyPath = join4(dir, "read-proof-key.json");
4245
+ const keyPath = join5(dir, "read-proof-key.json");
3829
4246
  c.files.create(keyPath, encoded({
3830
4247
  schema: "cadence-history-read-key/1",
3831
4248
  storeId: format.storeId,
3832
4249
  key: randomBytes2(32).toString("base64url")
3833
4250
  }));
3834
4251
  lease.assert();
3835
- const snapshot = c.files.create(join4(dir, "format.json"), encoded(format));
4252
+ const snapshot = c.files.create(join5(dir, "format.json"), encoded(format));
3836
4253
  const result = new HistoryFiles(c, format, snapshot);
3837
- result.syncParents(join4(dir, "format.json"));
4254
+ result.syncParents(join5(dir, "format.json"));
3838
4255
  result.assertWritable();
3839
4256
  return result;
3840
4257
  }
@@ -3869,7 +4286,7 @@ class HistoryFiles {
3869
4286
  throw new ContractError("quarantine");
3870
4287
  }
3871
4288
  assertFormat() {
3872
- this.files.assert(join4(this.directory, "format.json"), this.formatSnapshot);
4289
+ this.files.assert(join5(this.directory, "format.json"), this.formatSnapshot);
3873
4290
  }
3874
4291
  readProofMaterial(budget) {
3875
4292
  if (this.format.schema !== FORMAT)
@@ -3878,7 +4295,7 @@ class HistoryFiles {
3878
4295
  this.assertReadFormat(budget);
3879
4296
  else
3880
4297
  this.assertFormat();
3881
- const selected2 = this.readBudgeted(join4(this.directory, "read-proof-key.json"), 512, budget);
4298
+ const selected2 = this.readBudgeted(join5(this.directory, "read-proof-key.json"), 512, budget);
3882
4299
  if (!selected2 || this.proofSnapshot && (selected2.identity !== this.proofSnapshot.identity || selected2.stamp !== this.proofSnapshot.stamp))
3883
4300
  corrupt();
3884
4301
  try {
@@ -3906,17 +4323,17 @@ class HistoryFiles {
3906
4323
  return;
3907
4324
  }
3908
4325
  this.assertFormat();
3909
- assertIncarnation(this, join4(this.directory, "read-proof-key.json"), this.proofSnapshot.stamp);
4326
+ assertIncarnation(this, join5(this.directory, "read-proof-key.json"), this.proofSnapshot.stamp);
3910
4327
  }
3911
4328
  segmentPath(segment) {
3912
- return join4(this.directory, "ledger", `${String(segment).padStart(12, "0")}.jsonl`);
4329
+ return join5(this.directory, "ledger", `${String(segment).padStart(12, "0")}.jsonl`);
3913
4330
  }
3914
4331
  artifactPath(ref) {
3915
- return join4(this.directory, "runs", ref.runId, "artifacts", ref.artifactId, `${ref.part}.part`);
4332
+ return join5(this.directory, "runs", ref.runId, "artifacts", ref.artifactId, `${ref.part}.part`);
3916
4333
  }
3917
4334
  admissionPath(runId) {
3918
4335
  decodeId(runId);
3919
- return join4(this.directory, "runs", runId, "admission.json");
4336
+ return join5(this.directory, "runs", runId, "admission.json");
3920
4337
  }
3921
4338
  writeAdmission(lease, input) {
3922
4339
  lease.assert();
@@ -3930,19 +4347,19 @@ class HistoryFiles {
3930
4347
  return this.files.read(path, maximum);
3931
4348
  let size;
3932
4349
  try {
3933
- size = lstatSync4(path).size;
4350
+ size = lstatSync5(path).size;
3934
4351
  } catch {
3935
- return this.files.read(path, 0);
4352
+ return this.files.read(path, 0, budget.walks);
3936
4353
  }
3937
4354
  if (size > maximum)
3938
4355
  corrupt();
3939
4356
  budget.reserve(size);
3940
- return this.files.read(path, size);
4357
+ return this.files.read(path, size, budget.walks);
3941
4358
  }
3942
4359
  readCommand(commandId, budget) {
3943
4360
  decodeId(commandId);
3944
4361
  this.assertReadFormat(budget);
3945
- return this.readBudgeted(join4(this.directory, "commands", commandId, "intent.json"), RECORD_BYTES, budget);
4362
+ return this.readBudgeted(join5(this.directory, "commands", commandId, "intent.json"), RECORD_BYTES, budget);
3946
4363
  }
3947
4364
  readAdmission(runId, identity, budget) {
3948
4365
  return this.readAdmissionIdentity(runId, hash(canonicalJson(identity)), budget);
@@ -3963,7 +4380,7 @@ class HistoryFiles {
3963
4380
  syncParents(file) {
3964
4381
  if (process.platform === "win32")
3965
4382
  return;
3966
- for (let path = dirname5(file);; path = dirname5(path)) {
4383
+ for (let path = dirname6(file);; path = dirname6(path)) {
3967
4384
  if (!this.files.directory(path))
3968
4385
  corrupt();
3969
4386
  const fd = openSync4(path, constants4.O_RDONLY | constants4.O_DIRECTORY);
@@ -3974,13 +4391,13 @@ class HistoryFiles {
3974
4391
  }
3975
4392
  if (path === this.coordination.directory)
3976
4393
  break;
3977
- if (path === dirname5(path))
4394
+ if (path === dirname6(path))
3978
4395
  corrupt();
3979
4396
  }
3980
4397
  }
3981
4398
  assertReadFormat(budget) {
3982
4399
  budget.reserve(this.formatSnapshot.bytes.length);
3983
- this.files.assert(join4(this.directory, "format.json"), this.formatSnapshot, this.formatSnapshot.bytes.length);
4400
+ this.files.assert(join5(this.directory, "format.json"), this.formatSnapshot, this.formatSnapshot.bytes.length);
3984
4401
  }
3985
4402
  assertPublished(segment, end, seq) {
3986
4403
  const fence = publicationFences.get(this.directory);
@@ -4018,7 +4435,7 @@ class HistoryFiles {
4018
4435
  const dir = opendirSync2(path);
4019
4436
  try {
4020
4437
  for (let e = dir.readSync();e; e = dir.readSync()) {
4021
- const p = join4(path, e.name), st = lstatSync4(p);
4438
+ const p = join5(path, e.name), st = lstatSync5(p);
4022
4439
  if (st.isDirectory())
4023
4440
  bytes += walk(p, depth + 1);
4024
4441
  else {
@@ -4053,7 +4470,7 @@ class HistoryFiles {
4053
4470
  state.bytes = 0;
4054
4471
  state.identity = null;
4055
4472
  let count = 0;
4056
- const ledger = join4(this.directory, "ledger");
4473
+ const ledger = join5(this.directory, "ledger");
4057
4474
  if (!this.files.directory(ledger))
4058
4475
  corrupt();
4059
4476
  const dir = opendirSync2(ledger);
@@ -4218,7 +4635,7 @@ class HistoryFiles {
4218
4635
  let committed;
4219
4636
  const fd = await open(path, constants4.O_WRONLY | constants4.O_APPEND | (constants4.O_NOFOLLOW ?? 0));
4220
4637
  const assertOriginal = (bytes) => {
4221
- const original = fstatSync4(fd.fd), selected2 = lstatSync4(path);
4638
+ const original = fstatSync4(fd.fd), selected2 = lstatSync5(path);
4222
4639
  if (original.dev !== selected2.dev || original.ino !== selected2.ino || original.nlink !== 1)
4223
4640
  corrupt();
4224
4641
  const snapshot = this.files.read(path, LIMITS.segmentBytes);
@@ -4335,7 +4752,7 @@ class HistoryFiles {
4335
4752
  try {
4336
4753
  lease.assert();
4337
4754
  this.files.assert(path, before, LIMITS.segmentBytes);
4338
- const opened = await fd.stat(), selected2 = lstatSync4(path);
4755
+ const opened = await fd.stat(), selected2 = lstatSync5(path);
4339
4756
  if (opened.ino !== selected2.ino || opened.dev !== selected2.dev || opened.nlink !== 1)
4340
4757
  corrupt();
4341
4758
  beforeEffect?.();
@@ -4370,7 +4787,7 @@ class HistoryFiles {
4370
4787
  checkpoint(lease, state) {
4371
4788
  lease.assert();
4372
4789
  this.assertWritable();
4373
- const path = join4(this.directory, "checkpoint.json"), old = this.files.read(path);
4790
+ const path = join5(this.directory, "checkpoint.json"), old = this.files.read(path);
4374
4791
  const body = encoded({
4375
4792
  schema: "cadence-history-checkpoint/2",
4376
4793
  storeId: this.format.storeId,
@@ -4783,8 +5200,8 @@ function digestHistory(files, exclude) {
4783
5200
  const walk = (path, depth) => {
4784
5201
  if (depth > 8 || !files.files.directory(path))
4785
5202
  throw new ContractError("corrupt-selected");
4786
- const directory = lstatSync5(path);
4787
- digest.update(`incarnation:${directory.dev}:${directory.ino}
5203
+ const directory2 = lstatSync6(path);
5204
+ digest.update(`incarnation:${directory2.dev}:${directory2.ino}
4788
5205
  `);
4789
5206
  const dir = opendirSync3(path), names2 = [];
4790
5207
  try {
@@ -4797,11 +5214,11 @@ function digestHistory(files, exclude) {
4797
5214
  dir.closeSync();
4798
5215
  }
4799
5216
  for (const name of names2.sort()) {
4800
- const selected2 = join5(path, name);
5217
+ const selected2 = join6(path, name);
4801
5218
  if (selected2 === exclude)
4802
5219
  continue;
4803
5220
  const rel = relative4(files.directory, selected2);
4804
- if (lstatSync5(selected2).isDirectory()) {
5221
+ if (lstatSync6(selected2).isDirectory()) {
4805
5222
  digest.update(`directory:${Buffer.byteLength(rel)}:${rel}
4806
5223
  `);
4807
5224
  walk(selected2, depth + 1);
@@ -5015,7 +5432,7 @@ async function confirm(expected) {
5015
5432
  `resolve ${expected.epoch}`
5016
5433
  ]
5017
5434
  ];
5018
- await new Promise((resolve3, reject) => {
5435
+ await new Promise((resolve4, reject) => {
5019
5436
  let index = 0, line = "", done = false;
5020
5437
  const raw = process.stdin.isRaw;
5021
5438
  const finish = (ok) => {
@@ -5031,7 +5448,7 @@ async function confirm(expected) {
5031
5448
  process.stdout.write(`\x1B[?2004l
5032
5449
  `);
5033
5450
  if (ok)
5034
- resolve3();
5451
+ resolve4();
5035
5452
  else
5036
5453
  reject(new ContractError("forbidden"));
5037
5454
  };
@@ -5107,8 +5524,8 @@ async function localRecovery(c, operation) {
5107
5524
  }
5108
5525
 
5109
5526
  // 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";
5527
+ import { homedir as homedir3 } from "node:os";
5528
+ import { dirname as dirname7, isAbsolute as isAbsolute5, join as join7, resolve as resolve4 } from "node:path";
5112
5529
 
5113
5530
  // src/service/config.ts
5114
5531
  import { isIP } from "node:net";
@@ -5162,7 +5579,7 @@ function localOrigin(policy, actualPort) {
5162
5579
  var STARTUP_INPUT_BYTES = 16 * 1024;
5163
5580
  function absolute(value) {
5164
5581
  const result = text(value, 4096, true);
5165
- if (!isAbsolute4(result))
5582
+ if (!isAbsolute5(result))
5166
5583
  throw new ContractError("invalid");
5167
5584
  return result;
5168
5585
  }
@@ -5242,7 +5659,7 @@ function selectStart(options, ambient) {
5242
5659
  throw new ContractError("invalid");
5243
5660
  let fromFile;
5244
5661
  if (options.config) {
5245
- const path = resolve3(options.config), f = new PrivateFiles(dirname6(path));
5662
+ const path = resolve4(options.config), f = new PrivateFiles(dirname7(path));
5246
5663
  const bytes = f.read(path);
5247
5664
  if (!bytes)
5248
5665
  throw new ContractError("corrupt-selected");
@@ -5257,11 +5674,11 @@ function selectStart(options, ambient) {
5257
5674
  port: options.port ?? 17423,
5258
5675
  proxy: null
5259
5676
  },
5260
- agentDir: resolve3(options.agentDir ?? ambient.PI_CODING_AGENT_DIR ?? join6(ambient.HOME ?? ambient.USERPROFILE ?? homedir2(), ".pi", "agent")),
5677
+ agentDir: resolve4(options.agentDir ?? ambient.PI_CODING_AGENT_DIR ?? join7(ambient.HOME ?? ambient.USERPROFILE ?? homedir3(), ".pi", "agent")),
5261
5678
  tokenSource: options.tokenFile ? "file" : env ? "environment" : "generated",
5262
- tokenFile: options.tokenFile ? resolve3(options.tokenFile) : null
5679
+ tokenFile: options.tokenFile ? resolve4(options.tokenFile) : null
5263
5680
  });
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)
5681
+ 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
5682
  throw new ContractError("conflict");
5266
5683
  return {
5267
5684
  workspace: workspace.root,
@@ -5293,21 +5710,37 @@ function portValue(value) {
5293
5710
  throw new ContractError("invalid");
5294
5711
  return integer(Number(value), 0, 65535);
5295
5712
  }
5713
+ var STARTUP_STAGES = [
5714
+ "package",
5715
+ "pi-agent-directory",
5716
+ "pi-auth",
5717
+ "pi-models",
5718
+ "pi-runtime",
5719
+ "service"
5720
+ ];
5721
+ function decodeStartupReport(value) {
5722
+ const o = closed(value, ["status", "error", "stage"]);
5723
+ return {
5724
+ status: oneOf(o.status, ["refused", "quarantined"]),
5725
+ error: oneOf(o.error, ERROR_CODES),
5726
+ stage: oneOf(o.stage, STARTUP_STAGES)
5727
+ };
5728
+ }
5296
5729
 
5297
5730
  // src/service/cli.ts
5298
5731
  function nativeCall(c, operation, reveal = false) {
5299
5732
  const access = c.localAccess();
5300
5733
  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) => {
5734
+ const write2 = !["status", "diagnose"].includes(operation);
5735
+ const body = write2 ? JSON.stringify(reveal ? { reveal: true } : {}) : undefined;
5736
+ return new Promise((resolve6, reject) => {
5304
5737
  let timer;
5305
5738
  const fail = () => {
5306
5739
  clearTimeout(timer);
5307
5740
  reject(new ContractError("unavailable"));
5308
5741
  };
5309
5742
  const req = request(url, {
5310
- method: write ? "POST" : "GET",
5743
+ method: write2 ? "POST" : "GET",
5311
5744
  agent: false,
5312
5745
  headers: {
5313
5746
  "X-Cadence-Local": access.capability,
@@ -5341,7 +5774,7 @@ function nativeCall(c, operation, reveal = false) {
5341
5774
  throw new ContractError("stale");
5342
5775
  }
5343
5776
  clearTimeout(timer);
5344
- resolve4(reply.value);
5777
+ resolve6(reply.value);
5345
5778
  } catch {
5346
5779
  fail();
5347
5780
  }
@@ -5356,7 +5789,7 @@ function nativeCall(c, operation, reveal = false) {
5356
5789
  req.end(body);
5357
5790
  });
5358
5791
  }
5359
- var pause = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
5792
+ var pause = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
5360
5793
  async function serviceStatus(c, diagnose = false) {
5361
5794
  const observed = c.inspect();
5362
5795
  if (observed.status === "absent" || observed.status === "stopped")
@@ -5393,7 +5826,38 @@ async function stopService(c) {
5393
5826
  return { status: "unconfirmed", epoch };
5394
5827
  }
5395
5828
  function failedStatus(result) {
5396
- return !!(result && typeof result === "object" && ("status" in result) && ["unconfirmed", "unavailable", "quarantined"].includes(String(result.status)));
5829
+ return !!(result && typeof result === "object" && ("status" in result) && ["unconfirmed", "unavailable", "quarantined", "refused"].includes(String(result.status)));
5830
+ }
5831
+ function startupGuidance(result, input) {
5832
+ let report;
5833
+ try {
5834
+ report = decodeStartupReport(result);
5835
+ } catch {
5836
+ return "";
5837
+ }
5838
+ const agent = input.config.agentDir;
5839
+ const inspect = `Inspect: cadence-service diagnose|recover inspect --workspace ${JSON.stringify(input.workspace)}
5840
+ `;
5841
+ switch (report.stage) {
5842
+ case "package":
5843
+ return `Cadence refused its installed package files (web/dist or agents); reinstall the package rather than editing it.
5844
+ `;
5845
+ case "pi-agent-directory":
5846
+ return `Pi's agent directory ${agent} must be an existing real directory owned by you with mode 0700 (chmod 700).
5847
+ `;
5848
+ case "pi-auth":
5849
+ return `${join8(agent, "auth.json")} must be your own single-link regular file with mode 0600 holding valid Pi credentials; log in to Pi first.
5850
+ `;
5851
+ case "pi-models":
5852
+ return `${join8(agent, "models.json")} must be your own single-link regular file with mode 0600, using only fields Cadence's pinned Pi SDK supports; a field only newer Pi understands is refused.
5853
+ `;
5854
+ case "pi-runtime":
5855
+ return `Loading Pi's SDK model runtime failed after the owner registered (optional SDK packages missing, or sources the SDK refuses); it stays quarantined.
5856
+ ${inspect}`;
5857
+ default:
5858
+ return report.status === "quarantined" ? `The owner registered, then refused; it stays quarantined.
5859
+ ${inspect}` : "";
5860
+ }
5397
5861
  }
5398
5862
  async function startService(options, foreground, hostUrl) {
5399
5863
  const network = networkSnapshot(process.env);
@@ -5420,18 +5884,18 @@ async function startService(options, foreground, hostUrl) {
5420
5884
  stdio: ["pipe", "pipe", "ignore"]
5421
5885
  });
5422
5886
  let completed = false;
5423
- const closure = new Promise((resolve4) => child.once("close", (code, signal) => {
5887
+ const closure = new Promise((resolve6) => child.once("close", (code, signal) => {
5424
5888
  completed = true;
5425
5889
  if (foreground && (code !== 0 || signal !== null))
5426
5890
  process.exitCode = 1;
5427
- resolve4();
5891
+ resolve6();
5428
5892
  }));
5429
- const startup = new Promise((resolve4) => {
5893
+ const startup = new Promise((resolve6) => {
5430
5894
  let bytes = "", resolved = false;
5431
5895
  const finish = (value) => {
5432
5896
  if (!resolved) {
5433
5897
  resolved = true;
5434
- resolve4(value);
5898
+ resolve6(value);
5435
5899
  }
5436
5900
  };
5437
5901
  child.on("error", () => finish({ status: "unconfirmed" }));
@@ -5447,7 +5911,12 @@ async function startService(options, foreground, hostUrl) {
5447
5911
  `))
5448
5912
  return;
5449
5913
  try {
5450
- const ready = closed(parseWireJson(bytes, 4096), ["status", "epoch"]);
5914
+ const line = parseWireJson(bytes, 4096);
5915
+ if (line && typeof line === "object" && "status" in line && line.status !== "ready") {
5916
+ finish(decodeStartupReport(line));
5917
+ return;
5918
+ }
5919
+ const ready = closed(line, ["status", "epoch"]);
5451
5920
  if (ready.status !== "ready")
5452
5921
  throw new ContractError("unavailable");
5453
5922
  const epoch = decodeId(ready.epoch);
@@ -5466,11 +5935,14 @@ async function startService(options, foreground, hostUrl) {
5466
5935
  let timer;
5467
5936
  const result = await Promise.race([
5468
5937
  startup,
5469
- new Promise((resolve4) => {
5470
- timer = setTimeout(() => resolve4({ status: "unconfirmed" }), LIMITS.startWaitMs);
5938
+ new Promise((resolve6) => {
5939
+ timer = setTimeout(() => resolve6({ status: "unconfirmed" }), LIMITS.startWaitMs);
5471
5940
  })
5472
5941
  ]);
5473
5942
  clearTimeout(timer);
5943
+ const guidance = startupGuidance(result, input);
5944
+ if (guidance)
5945
+ process.stderr.write(guidance);
5474
5946
  child.stdout.destroy();
5475
5947
  child.stdin.destroy();
5476
5948
  if (!foreground)
@@ -5485,6 +5957,104 @@ async function startService(options, foreground, hostUrl) {
5485
5957
  }
5486
5958
  return result;
5487
5959
  }
5960
+ async function observeStarted(workspace) {
5961
+ const deadline = performance.now() + LIMITS.startWaitMs + 2000;
5962
+ let service = "unconfirmed";
5963
+ while (performance.now() < deadline) {
5964
+ try {
5965
+ const c = new ServiceCoordination(workspace);
5966
+ const observed = await serviceStatus(c);
5967
+ if (observed && typeof observed === "object" && "status" in observed)
5968
+ service = String(observed.status);
5969
+ if (service === "alive")
5970
+ return { service, endpoint: c.localAccess().endpoint };
5971
+ if (service === "quarantined")
5972
+ break;
5973
+ } catch {}
5974
+ await pause(250);
5975
+ }
5976
+ return { service };
5977
+ }
5978
+ function autostartGuidance(s, result) {
5979
+ const name = s.unit.replace(/\.service$/, "");
5980
+ const cli = JSON.stringify(join8(s.root, "src", "service-cli.mjs"));
5981
+ const workspace = JSON.stringify(result.workspace ?? "");
5982
+ const manage = `Manage: systemctl --user status|restart|stop ${name}; logs: journalctl --user -u ${name}; opt out: systemctl --user disable --now ${name}`;
5983
+ const note = result.redirectedFrom ? `Cadence workspace: ${result.workspace} (${result.redirectedFrom} contains Cadence's private state)
5984
+ ` : "";
5985
+ return note + autostartStatusGuidance(s, result, name, cli, workspace, manage);
5986
+ }
5987
+ function autostartStatusGuidance(s, result, name, cli, workspace, manage) {
5988
+ switch (result.status) {
5989
+ case "started": {
5990
+ if (result.service !== "alive")
5991
+ return `${s.unit} started for ${result.workspace}, but the owner reports ${result.service}.
5992
+ Inspect: node ${cli} diagnose --workspace ${workspace}
5993
+ ${manage}
5994
+ `;
5995
+ const linger = s.linger() === false ? "Linger is off: Cadence starts at login and stops at logout. `loginctl enable-linger` keeps it running and starts it at boot.\n" : "";
5996
+ return `Cadence WebUI: ${result.endpoint} (${s.unit}, workspace ${result.workspace})
5997
+ Enter with your private link: node ${cli} token-link --workspace ${workspace} --reveal
5998
+ ${manage}
5999
+ ${linger}`;
6000
+ }
6001
+ case "waiting-for-pi-login":
6002
+ return `${s.unit} is enabled for ${result.workspace}; it starts once Pi has ${join8(agentDirectory(s), "auth.json")}.
6003
+ Log in to Pi, then: systemctl --user start ${name}
6004
+ `;
6005
+ case "disabled":
6006
+ return `${s.unit} refreshed for ${result.workspace}; left disabled as configured.
6007
+ `;
6008
+ case "masked":
6009
+ return `${s.unit} is masked; Cadence did not enable or start it.
6010
+ `;
6011
+ case "missing-pi-sdk":
6012
+ 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.
6013
+ `;
6014
+ case "foreign":
6015
+ return `${result.unit} was not generated by Cadence; left unchanged.
6016
+ `;
6017
+ case "unavailable":
6018
+ return `No systemd user manager; start Cadence explicitly: node ${cli} start --workspace /absolute/project
6019
+ `;
6020
+ case "failed":
6021
+ return `Could not enable or start ${s.unit}; see systemctl --user status ${name} and journalctl --user -u ${name}.
6022
+ `;
6023
+ default:
6024
+ return "";
6025
+ }
6026
+ }
6027
+ async function autostartMain(argv, hostUrl) {
6028
+ const action = argv.shift();
6029
+ let requested;
6030
+ if (action === "install" && argv[0] === "--workspace" && argv[1])
6031
+ requested = resolve5(argv.splice(0, 2)[1]);
6032
+ if (argv.length || !["install", "refresh", "remove", "status"].includes(action ?? ""))
6033
+ throw new ContractError("invalid");
6034
+ const s = hostSystem(dirname8(dirname8(fileURLToPath(hostUrl))));
6035
+ let result;
6036
+ if (action === "remove")
6037
+ result = removeAutostart(s);
6038
+ else if (action === "status")
6039
+ result = autostartStatus(s);
6040
+ else {
6041
+ try {
6042
+ result = configureAutostart(s, action === "install" ? "install" : "refresh", requested);
6043
+ } catch (error) {
6044
+ if (error instanceof ContractError && error.code === "invalid")
6045
+ process.stderr.write(`Cadence autostart needs an existing absolute workspace directory (--workspace or CADENCE_WORKSPACE).
6046
+ `);
6047
+ throw error;
6048
+ }
6049
+ if (result.status === "started" && result.workspace)
6050
+ Object.assign(result, await observeStarted(result.workspace));
6051
+ process.stderr.write(autostartGuidance(s, result));
6052
+ }
6053
+ process.stdout.write(`${JSON.stringify(result)}
6054
+ `);
6055
+ if (["failed", "foreign", "unavailable"].includes(result.status))
6056
+ process.exitCode = 1;
6057
+ }
5488
6058
  async function managementMain(argv, hostUrl) {
5489
6059
  try {
5490
6060
  if (!process.versions.node || process.versions.bun)
@@ -5492,12 +6062,17 @@ async function managementMain(argv, hostUrl) {
5492
6062
  const command = argv.shift();
5493
6063
  if (command === "--help" && !argv.length) {
5494
6064
  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.
6065
+ cadence-service autostart install [--workspace PATH]|refresh|remove|status
6066
+ Start is explicit, or the Linux systemd user unit that Pi's user-scope install registers (CADENCE_AUTOSTART=0, disable or mask opts out). Default detached or --foreground. Start accepts --config, --agent-dir, --host, --port, --token-file.
5496
6067
  Return/quit detaches clients; only explicit Cancel/Stop requests stop. Unconfirmed/quarantined is not stopped.
5497
6068
  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
6069
  `);
5499
6070
  return;
5500
6071
  }
6072
+ if (command === "autostart") {
6073
+ await autostartMain(argv, hostUrl);
6074
+ return;
6075
+ }
5501
6076
  let recovery;
5502
6077
  if (![
5503
6078
  "start",
@@ -5555,10 +6130,23 @@ Recovery resolve requires this local native terminal, exact unchanged dead-owner
5555
6130
  else
5556
6131
  throw new ContractError("invalid");
5557
6132
  }
6133
+ const landing = effectiveWorkspace(options.workspace, options.stateHome);
6134
+ if (landing.redirected) {
6135
+ if (seen.has("--workspace"))
6136
+ process.stderr.write(`Cadence workspace: ${landing.workspace} (${options.workspace} contains Cadence's private state)
6137
+ `);
6138
+ options.workspace = landing.workspace;
6139
+ if (command === "start")
6140
+ mkdirSync4(landing.workspace, { recursive: true });
6141
+ }
5558
6142
  let result;
5559
6143
  if (command === "start")
5560
6144
  result = await startService(options, foreground, hostUrl);
5561
- else {
6145
+ else if (landing.redirected && !existsSync(landing.workspace)) {
6146
+ if (command !== "status" && command !== "diagnose")
6147
+ throw new ContractError("unavailable");
6148
+ result = { status: "absent" };
6149
+ } else {
5562
6150
  const c = new ServiceCoordination(options.workspace, options.stateHome);
5563
6151
  if (command === "recover" && recovery)
5564
6152
  result = await localRecovery(c, recovery);
@@ -5576,6 +6164,8 @@ Recovery resolve requires this local native terminal, exact unchanged dead-owner
5576
6164
  else
5577
6165
  throw new ContractError("invalid");
5578
6166
  }
6167
+ if (landing.redirected && result && typeof result === "object")
6168
+ result = { ...result, workspace: landing.workspace };
5579
6169
  if (result !== null)
5580
6170
  process.stdout.write(`${JSON.stringify(result)}
5581
6171
  `);