@coreplane/switchboard 1.220.0 → 1.221.0

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.
@@ -84,6 +84,7 @@ import { BASH_TIMEOUT_MAX_MS, BASH_TIMEOUT_MS, clampBashTimeout } from "../../sr
84
84
  import { selectBindingsToPurge } from "../../src/execution/bindingPurge.js";
85
85
  import { busyAfterKillReason, planForceDetach } from "../../src/execution/residentDetach.js";
86
86
  import { parseReadonly, planReadonlyAttach } from "../../src/execution/residentReadonly.js";
87
+ import { decideWorktree, parseReuse, type WorktreeFacts } from "../../src/execution/residentReuse.js";
87
88
  import {
88
89
  depCacheScript,
89
90
  mutableCachePaths,
@@ -1029,6 +1030,10 @@ interface AttachOk {
1029
1030
  reconciled: boolean;
1030
1031
  /** True when a dirty/stale/missing worktree was wiped and recreated. */
1031
1032
  recreated: boolean;
1033
+ /** Which container the tree is in: the kernel's boot id of this resident's
1034
+ * VM (docs/reference/specs/harness-pi.md item 8), so the run's row can name
1035
+ * the container its pi runs in. Absent when the kernel does not say. */
1036
+ container?: string;
1032
1037
  deps: ThreadDepsMechanism;
1033
1038
  /** `ok` — credential file written; `unavailable` — writable attach but no
1034
1039
  * token (see credentialsError); `none` — read-only attach, deliberately no
@@ -1371,6 +1376,16 @@ class StepError extends Error {
1371
1376
  }
1372
1377
  }
1373
1378
 
1379
+ /** A reusing attach (item 66) found no tree it can keep: gone, unreadable, or
1380
+ * built for the other mode. Nothing on disk was touched; the attach answers
1381
+ * 409 `needs: "recreate"` and the caller decides what a run without its
1382
+ * workspace does. */
1383
+ class ReuseRefusedError extends Error {
1384
+ constructor(readonly why: string) {
1385
+ super(`reuse-refused: ${why}`);
1386
+ }
1387
+ }
1388
+
1374
1389
  /** Thrown after the resident has ALREADY been transitioned to `down` (reason
1375
1390
  * persisted); signals callers the cycle is over without re-flipping. */
1376
1391
  class ResidentDownError extends Error {
@@ -1576,6 +1591,7 @@ export class ResidentDO extends Sandbox<Env> {
1576
1591
  private readonly hydrationMemoTtlMs = 60_000;
1577
1592
  private gitSetupDone = false;
1578
1593
  private stageDirsReady = new Set<string>();
1594
+ private containerIdMemo: string | undefined = undefined;
1579
1595
 
1580
1596
  /** The incarnation: one isolate paired with one container runtime
1581
1597
  * (docs/reference/specs/resident-repos.md item 22). Minted when the object
@@ -1604,6 +1620,7 @@ export class ResidentDO extends Sandbox<Env> {
1604
1620
  this.hydratedVerdictAt = 0;
1605
1621
  this.gitSetupDone = false;
1606
1622
  this.stageDirsReady.clear();
1623
+ this.containerIdMemo = undefined;
1607
1624
  this.depsStoreDirReady = false;
1608
1625
  this.depsInstallSlots = null;
1609
1626
  }
@@ -4306,6 +4323,7 @@ export class ResidentDO extends Sandbox<Env> {
4306
4323
  refHint: string | null,
4307
4324
  readonly = false,
4308
4325
  wantSha: string | null = null,
4326
+ reuse = false,
4309
4327
  record?: ResidentRecord,
4310
4328
  traceparent?: string,
4311
4329
  ): Promise<AttachOk | ThreadErr> {
@@ -4314,7 +4332,7 @@ export class ResidentDO extends Sandbox<Env> {
4314
4332
  const t0 = systemClock();
4315
4333
  const trace = createStepTrace(t0);
4316
4334
  const res = await this.stepTrace.run(trace, () =>
4317
- this.attachThreadTraced(threadKey, refHint, readonly, wantSha, record, t0),
4335
+ this.attachThreadTraced(threadKey, refHint, readonly, wantSha, reuse, record, t0),
4318
4336
  );
4319
4337
  // The same steps as the resident's own `resident.attach` root (item 22).
4320
4338
  emitStepRoot("resident.attach", t0, trace.steps(), traceparent, "error" in res ? refusalOutcome(res) : "ok");
@@ -4327,6 +4345,7 @@ export class ResidentDO extends Sandbox<Env> {
4327
4345
  refHint: string | null,
4328
4346
  readonly: boolean,
4329
4347
  wantSha: string | null,
4348
+ reuse: boolean,
4330
4349
  record: ResidentRecord | undefined,
4331
4350
  t0: number,
4332
4351
  ): Promise<AttachOk | ThreadErr> {
@@ -4346,7 +4365,7 @@ export class ResidentDO extends Sandbox<Env> {
4346
4365
  // container under it (and isIdle never parks the cycle mid-attach).
4347
4366
  this.attachesInFlight++;
4348
4367
  try {
4349
- return await this.attachThreadBody(threadKey, refHint, readonly, wantSha, resourceId, t0, record);
4368
+ return await this.attachThreadBody(threadKey, refHint, readonly, wantSha, reuse, resourceId, t0, record);
4350
4369
  } finally {
4351
4370
  this.attachesInFlight--;
4352
4371
  }
@@ -4376,6 +4395,8 @@ export class ResidentDO extends Sandbox<Env> {
4376
4395
  refHint: string | null,
4377
4396
  readonly: boolean,
4378
4397
  wantSha: string | null,
4398
+ /** A resumed run's attach (item 66): keep the thread's tree as it stands, or refuse. */
4399
+ reuse: boolean,
4379
4400
  resourceId: string,
4380
4401
  t0: number,
4381
4402
  recordFromRoute?: ResidentRecord,
@@ -4439,6 +4460,7 @@ export class ResidentDO extends Sandbox<Env> {
4439
4460
  threadKey,
4440
4461
  refHint,
4441
4462
  wantSha,
4463
+ reuse,
4442
4464
  slug,
4443
4465
  t0,
4444
4466
  facts,
@@ -4460,6 +4482,7 @@ export class ResidentDO extends Sandbox<Env> {
4460
4482
  threadKey: string;
4461
4483
  refHint: string | null;
4462
4484
  wantSha: string | null;
4485
+ reuse: boolean;
4463
4486
  slug: string;
4464
4487
  t0: number;
4465
4488
  facts: RepoFacts;
@@ -4468,7 +4491,7 @@ export class ResidentDO extends Sandbox<Env> {
4468
4491
  mode: ReturnType<typeof planReadonlyAttach>;
4469
4492
  rollback: () => Promise<void>;
4470
4493
  }): Promise<AttachOk | ThreadErr> {
4471
- const { threadKey, refHint, wantSha, slug, t0, facts, record, binding, mode, rollback } = input;
4494
+ const { threadKey, refHint, wantSha, reuse, slug, t0, facts, record, binding, mode, rollback } = input;
4472
4495
 
4473
4496
  // Command-level token mint — before the lock so mint latency
4474
4497
  // never holds the mutex, and failure never blocks the attach.
@@ -4545,11 +4568,17 @@ export class ResidentDO extends Sandbox<Env> {
4545
4568
  const threadLockKey = await this.lockfileKey(sha);
4546
4569
  const recreated = await this.ensureThreadWorktree(binding, sha, mode.originUrl, mode.modeSwitch, {
4547
4570
  detached: target.kind === "sha",
4571
+ reuse,
4548
4572
  });
4549
4573
  return { sha, threadLockKey, recreated };
4550
4574
  }, ATTACH_MUTEX_WAIT_MS);
4551
4575
  } catch (err) {
4552
4576
  await rollback();
4577
+ // A reusing attach found no tree it can keep (item 66): nothing was
4578
+ // wiped, and the caller (not this resident) decides what a run
4579
+ // without its workspace does.
4580
+ if (err instanceof ReuseRefusedError)
4581
+ return { error: `reuse-refused: ${err.why}`, status: 409, needs: "recreate" };
4553
4582
  if (err instanceof MirrorBusyError) {
4554
4583
  const s = await this.getStatus();
4555
4584
  return { error: errMsg(err), status: 503, state: s.state, reason: "mirror-busy" };
@@ -4617,6 +4646,9 @@ export class ResidentDO extends Sandbox<Env> {
4617
4646
  // step counts it — the admission's free-space term is a live `df`, and the
4618
4647
  // per-part projection it reads from the sample moves only with a cycle.
4619
4648
 
4649
+ // Which container the tree is in, for the run's row (harness-pi item 8):
4650
+ // memoized per incarnation, so this is a read after the first attach.
4651
+ const container = await this.containerIdentity();
4620
4652
  return {
4621
4653
  workspace: binding.worktreePath,
4622
4654
  ref: binding.ref,
@@ -4624,6 +4656,7 @@ export class ResidentDO extends Sandbox<Env> {
4624
4656
  user: binding.user,
4625
4657
  reconciled: deps.reconciled,
4626
4658
  recreated: locked.value.recreated,
4659
+ ...(container !== undefined ? { container } : {}),
4627
4660
  deps: deps.deps,
4628
4661
  credentials,
4629
4662
  ...(credentialsError ? { credentialsError } : {}),
@@ -4634,9 +4667,14 @@ export class ResidentDO extends Sandbox<Env> {
4634
4667
  };
4635
4668
  }
4636
4669
 
4637
- /** Ensure the worktree exists, wiping dirty/stale trees. Returns
4638
- * true when the tree was (re)created. MUST be called holding the mirror
4639
- * mutex — the clone reads the mirror.
4670
+ /** Ensure the worktree exists. A provisioning attach wipes dirty/stale
4671
+ * trees; a reusing attach (`opts.reuse`, item 66) keeps a readable tree
4672
+ * exactly as it stands (the dirt and the HEAD are the resumed run's own
4673
+ * work) and throws `ReuseRefusedError` for one it cannot keep, touching
4674
+ * nothing. The decision is the pure `decideWorktree`; this method measures
4675
+ * the facts and runs the verdict. Returns true when the tree was
4676
+ * (re)created. MUST be called holding the mirror mutex: the clone reads
4677
+ * the mirror.
4640
4678
  *
4641
4679
  * "Dirty" is tracked-file dirt (`status --porcelain -uno`): untracked
4642
4680
  * scratch files are the thread's own state and survive re-attach.
@@ -4658,8 +4696,9 @@ export class ResidentDO extends Sandbox<Env> {
4658
4696
  modeSwitch: boolean,
4659
4697
  /** `detached`: the bound ref is gone from the mirror and `sha` is the
4660
4698
  * expected commit it still holds (item 51) — the tree is checked out at
4661
- * that commit, detached, instead of at a branch. */
4662
- opts: { detached: boolean } = { detached: false },
4699
+ * that commit, detached, instead of at a branch. `reuse`: a resumed
4700
+ * run's attach (item 66): keep the tree as it stands, or refuse. */
4701
+ opts: { detached: boolean; reuse: boolean } = { detached: false, reuse: false },
4663
4702
  ): Promise<boolean> {
4664
4703
  const wt = binding.worktreePath;
4665
4704
  const threadDir = parentDir(wt);
@@ -4667,32 +4706,38 @@ export class ResidentDO extends Sandbox<Env> {
4667
4706
  // 700 + thread-user ownership: other thread users cannot traverse in.
4668
4707
  await this.runOk(["install", "-d", "-m", "700", "-o", binding.user, "-g", binding.user, threadDir], "thread-dir");
4669
4708
 
4670
- // A tree built for the other mode (item 50) is wiped before any other
4671
- // check: a mirror-origin, credential-less tree must never serve a
4672
- // writable run, and a GitHub-origin tree must never serve a read-only one.
4673
- let recreate = modeSwitch;
4674
- const exists = !recreate && (await this.run(["test", "-d", `${wt}/.git`])).exitCode === 0;
4675
- if (exists) {
4709
+ // What is on disk, measured as the thread user. A tree built for the
4710
+ // other mode (item 50) is decided before any probe: a mirror-origin,
4711
+ // credential-less tree must never serve a writable run, and a
4712
+ // GitHub-origin tree must never serve a read-only one. The ancestry probe
4713
+ // is a provisioning attach's alone: a reusing attach never judges the HEAD.
4714
+ const facts: WorktreeFacts = { exists: false };
4715
+ if (!modeSwitch && (await this.run(["test", "-d", `${wt}/.git`])).exitCode === 0) {
4716
+ facts.exists = true;
4676
4717
  const status = await this.threadRun(binding.user, wt, "git status --porcelain -uno", DEFAULT_EXEC_TIMEOUT_MS);
4677
4718
  const head = await this.threadRun(binding.user, wt, "git rev-parse HEAD", DEFAULT_EXEC_TIMEOUT_MS);
4678
4719
  if (status.exitCode !== 0 || head.exitCode !== 0) {
4679
- recreate = true; // unreadable/corrupt (or owned by a previous pool user)
4680
- } else if (status.stdout.trim() !== "") {
4681
- recreate = true; // dirty tracked files
4720
+ // unreadable/corrupt (or owned by a previous pool user)
4721
+ facts.readable = false;
4722
+ facts.detail = (status.exitCode !== 0 ? status.stderr : head.stderr).trim().split("\n")[0]?.slice(0, 200) ?? "";
4682
4723
  } else {
4683
- const headSha = head.stdout.trim();
4684
- if (headSha !== sha) {
4724
+ facts.readable = true;
4725
+ facts.dirty = status.stdout.trim() !== "";
4726
+ facts.head = head.stdout.trim();
4727
+ if (!opts.reuse && !facts.dirty && facts.head !== sha) {
4685
4728
  const anc = await this.threadRun(
4686
4729
  binding.user,
4687
4730
  wt,
4688
4731
  `git merge-base --is-ancestor ${sha} HEAD`,
4689
4732
  DEFAULT_EXEC_TIMEOUT_MS,
4690
4733
  );
4691
- if (anc.exitCode !== 0) recreate = true; // stale: behind/diverged from the mirror tip
4734
+ facts.descendsFromTip = anc.exitCode === 0;
4692
4735
  }
4693
4736
  }
4694
- if (!recreate) return false;
4695
4737
  }
4738
+ const decision = decideWorktree({ reuse: opts.reuse, modeSwitch, sha, worktreePath: wt, facts });
4739
+ if (decision.kind === "refuse") throw new ReuseRefusedError(decision.why);
4740
+ if (decision.kind === "reuse") return false;
4696
4741
 
4697
4742
  await this.runOk(["rm", "-rf", wt], "worktree-clean");
4698
4743
  if (opts.detached) {
@@ -4727,6 +4772,24 @@ export class ResidentDO extends Sandbox<Env> {
4727
4772
  return true;
4728
4773
  }
4729
4774
 
4775
+ /** Which container this is: the kernel's boot id, one per VM boot and
4776
+ * world-readable, so the run's pool user reads the same word from inside
4777
+ * (docs/reference/specs/harness-pi.md item 8). Memoized per incarnation (a
4778
+ * replaced runtime clears it with the other memos). Undefined when the
4779
+ * kernel does not say or the command could not run: then the answer names
4780
+ * no container, and nothing is judged by it. */
4781
+ private async containerIdentity(): Promise<string | undefined> {
4782
+ if (this.containerIdMemo !== undefined) return this.containerIdMemo;
4783
+ try {
4784
+ const word = (await this.runOk(["cat", "/proc/sys/kernel/random/boot_id"], "boot-id")).trim();
4785
+ if (!/^[A-Za-z0-9-]{1,64}$/.test(word)) return undefined;
4786
+ this.containerIdMemo = word;
4787
+ return word;
4788
+ } catch {
4789
+ return undefined;
4790
+ }
4791
+ }
4792
+
4730
4793
  /** Read-only attach (item 50): make sure the tree carries no credential file
4731
4794
  * and no credential helper — idempotent, run on every read-only attach. */
4732
4795
  private async scrubThreadCredentials(binding: ThreadBinding): Promise<void> {
@@ -7475,13 +7538,15 @@ async function handleAttach(env: Env, body: Record<string, unknown>, traceparent
7475
7538
  if ("error" in readonly) return json({ error: readonly.error }, 400);
7476
7539
  const want = parseWantSha(body.sha);
7477
7540
  if ("error" in want) return json({ error: want.error }, 400);
7541
+ const reuse = parseReuse(body.reuse);
7542
+ if ("error" in reuse) return json({ error: reuse.error }, 400);
7478
7543
  // Post-validation, the answer streams like /exec (item 59): heartbeat
7479
7544
  // whitespace then ONE JSON document over HTTP 200, so an attach that waits
7480
7545
  // on a deps install (minutes) cannot lose the connection the way a plain
7481
7546
  // response does (`fetch failed` a few minutes in). A refusal
7482
7547
  // carries its `status` in the body; `ResidentExecutor.attach` reads it there.
7483
7548
  return streamHeartbeatJson(
7484
- ctx.stub.attachThread(ctx.threadKey, refHint, readonly.readonly, want.sha, ctx.record, traceparent),
7549
+ ctx.stub.attachThread(ctx.threadKey, refHint, readonly.readonly, want.sha, reuse.reuse, ctx.record, traceparent),
7485
7550
  (result) => result,
7486
7551
  (err) => ({ error: errMsg(err), status: 500 }),
7487
7552
  );
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.220.0",
3
+ "version": "1.221.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "switchboard",
9
- "version": "1.220.0",
9
+ "version": "1.221.0",
10
10
  "license": "Apache-2.0",
11
11
  "workspaces": [
12
12
  "web",
@@ -16710,7 +16710,7 @@
16710
16710
  },
16711
16711
  "packages/switchboard": {
16712
16712
  "name": "@coreplane/switchboard",
16713
- "version": "1.220.0",
16713
+ "version": "1.221.0",
16714
16714
  "license": "Apache-2.0",
16715
16715
  "dependencies": {
16716
16716
  "@anthropic-ai/sdk": "^0.124.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.220.0",
3
+ "version": "1.221.0",
4
4
  "private": true,
5
5
  "description": "Mention it in Slack and an agent reviews the PR, ships the fix, or answers the question — on the model you choose, with its tools running where you decide.",
6
6
  "license": "Apache-2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.220.0",
3
- "commit": "cf86e5df3d3256037bf4925a9e13de0b61483df3",
4
- "builtAt": "2026-09-15T02:39:37.781Z"
2
+ "version": "1.221.0",
3
+ "commit": "7b1634ab29d92dcf5cab3f670193bc627e98e21d",
4
+ "builtAt": "2026-09-15T14:37:20.916Z"
5
5
  }
@@ -622,6 +622,19 @@ export function parseExitPrefix(output: string): { failed: boolean; exitCode?: n
622
622
  return /^\d+$/.test(m[1]) ? { failed: true, exitCode: Number(m[1]) } : { failed: true };
623
623
  }
624
624
 
625
+ /** A tool that could not do what it was asked answers the model with a text
626
+ * that opens `error:` (`attach_file`, the `submit_*` tools, the run tools —
627
+ * each declares `failsInText` on its `RunnableTool`) instead of throwing, so
628
+ * the model can read the reason and go on. The record must call that result
629
+ * what the model reads it as: `ok:false`. Both loops (the native runner and
630
+ * the pi bridge) derive `ok` for such a tool through this one reader; bash
631
+ * keeps `parseExitPrefix`; a tool relaying content it did not write is never
632
+ * read this way. Ordinary output that merely contains the word later on is a
633
+ * success. */
634
+ export function toolTextFailed(output: string): boolean {
635
+ return /^\s*error:/i.test(stripAnsi(output));
636
+ }
637
+
625
638
  /** Upper bound on a `tool_result.output` — large enough for a test run or a
626
639
  * diff to read in full on the run page, small enough that a run of ordinary
627
640
  * length fits the registry's backlog whole (the backlog is bounded by count AND
@@ -0,0 +1,98 @@
1
+ /** The worktree decision of the resident Worker's attach
2
+ * (deploy/cloudflare-resident/worker.ts `ensureThreadWorktree`), kept pure
3
+ * and dependency-free so it is unit-testable from src/ and imported by the
4
+ * resident Worker like residentReadonly and residentHead: the tested code IS
5
+ * the shipped code.
6
+ *
7
+ * Background: an attach treated the thread's tree as disposable: dirty
8
+ * tracked files or a HEAD off the mirror's tip meant `rm -rf` and a fresh
9
+ * clone. That is the right discipline for a fresh run, whose tree is a
10
+ * leftover of some earlier run in the thread. It is the wrong discipline for
11
+ * a run that comes back after a bot restart: the dirty tree IS that run's
12
+ * work, and the wipe either destroyed it under a still-living process or
13
+ * raced that process's own writes and failed the attach outright.
14
+ *
15
+ * Shape: the caller says whether this attach REUSES (a resumed run) or
16
+ * PROVISIONS (a fresh one). A reusing attach keeps a readable tree exactly
17
+ * as it stands, dirt and stale HEAD included, and refuses by name a tree it
18
+ * cannot keep (gone, unreadable, built for the other mode) without touching
19
+ * it. A provisioning attach keeps the dirty/stale discipline byte for byte. */
20
+
21
+ export type ParsedReuse = { reuse: boolean } | { error: string };
22
+
23
+ /** `/attach` body field `reuse`: absent → false (the body every bot always
24
+ * sent, a fresh attach); a boolean → itself; anything else → a 400-shaped error. */
25
+ export function parseReuse(value: unknown): ParsedReuse {
26
+ if (value === undefined) return { reuse: false };
27
+ if (typeof value === "boolean") return { reuse: value };
28
+ return { error: "reuse must be a boolean when present" };
29
+ }
30
+
31
+ /** What the attach measured about the tree on disk, as the thread user. Each
32
+ * probe past `exists` is measured only when the one before it allowed; an
33
+ * unmeasured fact is undefined. */
34
+ export interface WorktreeFacts {
35
+ /** `<worktree>/.git` is a directory. */
36
+ exists: boolean;
37
+ /** `git status --porcelain -uno` and `git rev-parse HEAD` both succeeded. */
38
+ readable?: boolean;
39
+ /** The failing probe's first error line, when one failed. */
40
+ detail?: string;
41
+ /** Tracked files differ from HEAD (untracked scratch files never count). */
42
+ dirty?: boolean;
43
+ /** The tree's HEAD. */
44
+ head?: string;
45
+ /** Whether the target commit is an ancestor of HEAD, measured by a
46
+ * provisioning attach whose HEAD is not the target; never by a reusing one. */
47
+ descendsFromTip?: boolean;
48
+ }
49
+
50
+ export type WorktreeDecision =
51
+ /** Keep the tree exactly as it stands. */
52
+ | { kind: "reuse" }
53
+ /** Wipe it and clone afresh (a provisioning attach only). */
54
+ | { kind: "recreate"; why: "mode-switch" | "missing" | "unreadable" | "dirty" | "stale" }
55
+ /** A reusing attach cannot keep this tree, and must not replace it. */
56
+ | { kind: "refuse"; why: string };
57
+
58
+ export function decideWorktree(input: {
59
+ /** True for a resumed run's attach: keep the tree, never wipe it. */
60
+ reuse: boolean;
61
+ /** The tree was built for the other mode (read-only against writable). */
62
+ modeSwitch: boolean;
63
+ /** The commit a provisioning attach checks out: the ref's tip, or the expected head. */
64
+ sha: string;
65
+ worktreePath: string;
66
+ facts: WorktreeFacts;
67
+ }): WorktreeDecision {
68
+ const { reuse, modeSwitch, sha, worktreePath, facts } = input;
69
+ if (modeSwitch) {
70
+ return reuse
71
+ ? {
72
+ kind: "refuse",
73
+ why: `the worktree at ${worktreePath} was built for the other mode (read-only against writable)`,
74
+ }
75
+ : { kind: "recreate", why: "mode-switch" };
76
+ }
77
+ if (!facts.exists) {
78
+ return reuse
79
+ ? {
80
+ kind: "refuse",
81
+ why: `no worktree at ${worktreePath}: the container disk was recycled or the tree was evicted since the run attached`,
82
+ }
83
+ : { kind: "recreate", why: "missing" };
84
+ }
85
+ if (facts.readable !== true) {
86
+ return reuse
87
+ ? {
88
+ kind: "refuse",
89
+ why: `the worktree at ${worktreePath} cannot be read (${facts.detail ?? "git probe failed"})`,
90
+ }
91
+ : { kind: "recreate", why: "unreadable" };
92
+ }
93
+ // A reusing attach judges nothing past readability: the dirt and the HEAD are the run's own state.
94
+ if (reuse) return { kind: "reuse" };
95
+ if (facts.dirty) return { kind: "recreate", why: "dirty" };
96
+ if (facts.head !== sha && facts.descendsFromTip !== true) return { kind: "recreate", why: "stale" };
97
+ return { kind: "reuse" };
98
+ }
@@ -60,6 +60,7 @@ export const RESIDENT_STEP_LABELS = {
60
60
  "ops-dir": "preparing a directory",
61
61
  "stage-dir": "preparing a directory",
62
62
  stat: "checking a file",
63
+ "boot-id": "reading the container's identity",
63
64
  touch: "touching a marker",
64
65
  nproc: "counting CPUs",
65
66
  mutex_wait: "waiting for the workspace",
@@ -28,7 +28,7 @@
28
28
  "src": "../node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-vietnamese-wght-normal.woff2"
29
29
  },
30
30
  "../node_modules/@pierre/diffs/dist/index.js": {
31
- "file": "assets/dist-DQ1U73LF.js",
31
+ "file": "assets/dist-C3C34ZMu.js",
32
32
  "name": "dist",
33
33
  "src": "../node_modules/@pierre/diffs/dist/index.js",
34
34
  "isDynamicEntry": true,
@@ -2562,16 +2562,16 @@
2562
2562
  "_seed-DeKSHrt2.js"
2563
2563
  ]
2564
2564
  },
2565
- "_StatusDot-CfBonGcV.js": {
2566
- "file": "assets/StatusDot-CfBonGcV.js",
2565
+ "_StatusDot-W2x3d782.js": {
2566
+ "file": "assets/StatusDot-W2x3d782.js",
2567
2567
  "name": "StatusDot",
2568
2568
  "imports": [
2569
2569
  "_seed-DeKSHrt2.js",
2570
- "_Tooltip-CBD36E3W.js"
2570
+ "_Tooltip-BLRVgG37.js"
2571
2571
  ]
2572
2572
  },
2573
- "_Tooltip-CBD36E3W.js": {
2574
- "file": "assets/Tooltip-CBD36E3W.js",
2573
+ "_Tooltip-BLRVgG37.js": {
2574
+ "file": "assets/Tooltip-BLRVgG37.js",
2575
2575
  "name": "Tooltip",
2576
2576
  "imports": [
2577
2577
  "_AppShell-lYVcj-k7.js",
@@ -2789,7 +2789,7 @@
2789
2789
  ]
2790
2790
  },
2791
2791
  "src/main.ts": {
2792
- "file": "assets/main-CdLnG-yl.js",
2792
+ "file": "assets/main-B9dN-Wec.js",
2793
2793
  "name": "main",
2794
2794
  "src": "src/main.ts",
2795
2795
  "isEntry": true,
@@ -2841,19 +2841,19 @@
2841
2841
  ]
2842
2842
  },
2843
2843
  "src/pages/ResidentDetailPage.vue": {
2844
- "file": "assets/ResidentDetailPage-DvCt96Dk.js",
2844
+ "file": "assets/ResidentDetailPage-iqyj3Kpt.js",
2845
2845
  "name": "ResidentDetailPage",
2846
2846
  "src": "src/pages/ResidentDetailPage.vue",
2847
2847
  "isDynamicEntry": true,
2848
2848
  "imports": [
2849
2849
  "_AppShell-lYVcj-k7.js",
2850
2850
  "_seed-DeKSHrt2.js",
2851
- "_StatusDot-CfBonGcV.js",
2851
+ "_StatusDot-W2x3d782.js",
2852
2852
  "_residentDiskBudget-BMBKlYRH.js"
2853
2853
  ]
2854
2854
  },
2855
2855
  "src/pages/ResidentsIndexPage.vue": {
2856
- "file": "assets/ResidentsIndexPage-BZaYja1-.js",
2856
+ "file": "assets/ResidentsIndexPage-BiECMcX5.js",
2857
2857
  "name": "ResidentsIndexPage",
2858
2858
  "src": "src/pages/ResidentsIndexPage.vue",
2859
2859
  "isDynamicEntry": true,
@@ -2861,20 +2861,20 @@
2861
2861
  "_AppShell-lYVcj-k7.js",
2862
2862
  "_seed-DeKSHrt2.js",
2863
2863
  "_wallClock-61NpVkY6.js",
2864
- "_StatusDot-CfBonGcV.js",
2864
+ "_StatusDot-W2x3d782.js",
2865
2865
  "_residentDiskBudget-BMBKlYRH.js",
2866
2866
  "_indexFormat-B-pd4r7Y.js"
2867
2867
  ]
2868
2868
  },
2869
2869
  "src/pages/RunRoutePage.vue": {
2870
- "file": "assets/RunRoutePage-DI4vzCUu.js",
2870
+ "file": "assets/RunRoutePage-YCWYYZkj.js",
2871
2871
  "name": "RunRoutePage",
2872
2872
  "src": "src/pages/RunRoutePage.vue",
2873
2873
  "isDynamicEntry": true,
2874
2874
  "imports": [
2875
2875
  "_AppShell-lYVcj-k7.js",
2876
2876
  "_seed-DeKSHrt2.js",
2877
- "_Tooltip-CBD36E3W.js",
2877
+ "_Tooltip-BLRVgG37.js",
2878
2878
  "src/main.ts",
2879
2879
  "_wallClock-61NpVkY6.js",
2880
2880
  "_NotFoundPage-BzWS4Ca1.js",
@@ -2890,31 +2890,31 @@
2890
2890
  ]
2891
2891
  },
2892
2892
  "src/pages/RunsIndexPage.vue": {
2893
- "file": "assets/RunsIndexPage-Ch9uP4p4.js",
2893
+ "file": "assets/RunsIndexPage-BX6231bA.js",
2894
2894
  "name": "RunsIndexPage",
2895
2895
  "src": "src/pages/RunsIndexPage.vue",
2896
2896
  "isDynamicEntry": true,
2897
2897
  "imports": [
2898
2898
  "_AppShell-lYVcj-k7.js",
2899
2899
  "_seed-DeKSHrt2.js",
2900
- "_Tooltip-CBD36E3W.js",
2900
+ "_Tooltip-BLRVgG37.js",
2901
2901
  "_wallClock-61NpVkY6.js",
2902
2902
  "_RunsTabs-DEICQ4FY.js",
2903
- "_StatusDot-CfBonGcV.js",
2903
+ "_StatusDot-W2x3d782.js",
2904
2904
  "_indexFormat-B-pd4r7Y.js",
2905
2905
  "_favicon-CWPcvWvp.js",
2906
2906
  "_localIso-L06jV29p.js"
2907
2907
  ]
2908
2908
  },
2909
2909
  "src/pages/ScheduledPage.vue": {
2910
- "file": "assets/ScheduledPage-DAkAHMCH.js",
2910
+ "file": "assets/ScheduledPage-ibRdZaDS.js",
2911
2911
  "name": "ScheduledPage",
2912
2912
  "src": "src/pages/ScheduledPage.vue",
2913
2913
  "isDynamicEntry": true,
2914
2914
  "imports": [
2915
2915
  "_AppShell-lYVcj-k7.js",
2916
2916
  "_seed-DeKSHrt2.js",
2917
- "_Tooltip-CBD36E3W.js",
2917
+ "_Tooltip-BLRVgG37.js",
2918
2918
  "_wallClock-61NpVkY6.js",
2919
2919
  "_RunsTabs-DEICQ4FY.js",
2920
2920
  "_indexFormat-B-pd4r7Y.js",
@@ -1 +1 @@
1
- import{t as e}from"./AppShell-lYVcj-k7.js";import{Lr as t,Xt as n,_n as r,cn as i,gn as a,ir as o,ln as ee,on as s,r as c,sn as l,un as u,ur as d,vi as f,wi as p,yn as te}from"./seed-DeKSHrt2.js";import{t as m}from"./StatusDot-CfBonGcV.js";import{a as ne,c as re,d as ie,f as h,i as g,l as ae,n as oe,o as se,r as ce,s as _,t as le,u as ue}from"./residentDiskBudget-BMBKlYRH.js";var de={class:`mt-4 first:mt-0`},fe={class:`w-full border-collapse font-mono text-[0.8125rem]`},pe={class:`border-t border-muted`},me={class:`px-2 py-1 align-top`},v={class:`ml-1.5 font-medium`},y={class:`border-t border-muted`},b={class:`break-all px-2 py-1 align-top`},x={key:0},S={key:1,class:`text-dimmed`},C={class:`border-t border-muted`},w={class:`break-all px-2 py-1 align-top`},T={key:0},E={key:1,class:`text-dimmed`},D={class:`border-t border-muted`},O={class:`break-all px-2 py-1 align-top`},k={key:0},A={key:1,class:`text-dimmed`},j={class:`border-t border-muted`},M={class:`break-all px-2 py-1 align-top`},N={key:0},P={key:1,class:`text-dimmed`},F={class:`border-t border-muted`},I={class:`break-all px-2 py-1 align-top`},L={key:1,class:`text-dimmed`},R={class:`mt-4`},z={class:`w-full border-collapse font-mono text-[0.8125rem]`},he={class:`border-t border-muted`},ge={class:`break-all px-2 py-1 align-top`},_e=[`href`],ve={key:1},ye={class:`border-t border-muted`},be={class:`break-all px-2 py-1 align-top`},xe={class:`border-t border-muted`},Se={class:`break-all px-2 py-1 align-top`},Ce=[`href`],we={key:1},Te={key:2,class:`text-dimmed`},Ee={class:`border-t border-muted`},De={class:`break-all px-2 py-1 align-top`},Oe={key:0},ke={key:1,class:`text-dimmed`},Ae={class:`border-t border-muted`},je={class:`break-all px-2 py-1 align-top`},Me={key:0},Ne={key:1,class:`text-dimmed`},Pe={class:`border-t border-muted`},Fe={class:`break-all px-2 py-1 align-top`},Ie={key:0},Le={key:1,class:`text-dimmed`},Re={class:`mt-4`},ze={class:`w-full border-collapse font-mono text-[0.8125rem]`},Be={class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},Ve={class:`break-all px-2 py-1 align-top`},He={class:`w-full border-collapse font-mono text-[0.8125rem]`},Ue={class:`w-32 break-all px-2 py-1 align-top text-muted sm:w-96 sm:break-words`},We={class:`px-2 py-1 align-top`},Ge={key:1,class:`text-xs text-muted`},Ke={class:`mt-4`},qe={key:0,class:`w-full border-collapse font-mono text-[0.8125rem]`},Je={class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},Ye={class:`break-all px-2 py-1 align-top`},Xe={key:0},B={key:1,class:`text-dimmed`},Ze={key:1,class:`text-xs text-muted`},Qe={class:`mt-4`},$e={class:`mb-1 text-xs text-muted`},et={class:`overflow-x-auto`},tt={class:`w-full border-collapse font-mono text-[0.8125rem]`},nt={class:`break-all px-2 py-1 align-top`},rt={key:0,class:`text-xs text-muted`},it={class:`break-all px-2 py-1 align-top`},at=[`href`],ot={key:1},st={key:2,class:`text-dimmed`},ct={class:`break-all px-2 py-1 align-top`},lt={key:0},ut={key:1,class:`text-dimmed`},dt={key:2},ft={key:3,class:`text-dimmed`},pt={class:`break-all px-2 py-1 align-top`},mt={key:0},ht={key:1,class:`text-dimmed`},gt={key:2},_t={key:3,class:`text-dimmed`},vt={key:1,class:`text-xs text-muted`},yt={class:`mt-4`},bt={class:`w-full border-collapse font-mono text-[0.8125rem]`},xt={class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},St={class:`px-2 py-1 align-top`},Ct={class:`mt-4`},wt={key:0,class:`w-full border-collapse font-mono text-[0.8125rem]`},Tt={class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},Et={class:`break-all px-2 py-1 align-top`},Dt={class:`text-xs text-muted`},Ot={key:1,class:`text-xs text-muted`},kt={class:`mt-4`},At={class:`w-full border-collapse font-mono text-[0.8125rem]`},jt={class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},Mt={class:`break-all px-2 py-1 align-top`},Nt={key:0},Pt={key:1,class:`text-dimmed`},Ft={class:`mt-4 text-xs text-muted`},It={class:`rounded bg-elevated px-1 py-0.5`},Lt={class:`rounded bg-elevated px-1 py-0.5`},Rt={class:`rounded bg-elevated px-1 py-0.5`},V=te({__name:`ResidentDetailPage`,setup(te){let V=c(`resident`),H=s(()=>V?.record??{}),U=s(()=>ae(H.value)),W=s(()=>ue(H.value)||(V?.slug??``)),G=s(()=>h(U.value.sha)),K=s(()=>se.test(W.value)?`https://github.com/${W.value}`:void 0),q=s(()=>K.value&&/^[0-9a-f]{7,40}$/.test(G.value)?`${K.value}/commit/${G.value}`:void 0),J=s(()=>{let e=U.value.snapshot;return e&&typeof e==`object`?_(e):void 0}),zt=s(()=>_(U.value.schedules)),Y=s(()=>{let e=_(H.value.commands),t=_(H.value.effects);return Object.keys(e).sort().map(n=>({name:n,command:h(e[n]),effect:h(t[n])||`readonly`}))}),X=s(()=>{let e=U.value.threads;return(Array.isArray(e)?e.map(_):[]).map(e=>{let t=h(e.sha);return{threadKey:h(e.threadKey)||`?`,ref:h(e.ref)||`?`,sha:t?t.slice(0,8):``,commitHref:K.value&&/^[0-9a-f]{7,40}$/.test(t)?`${K.value}/commit/${t}`:void 0,user:h(e.user),deps:h(e.deps),boundAt:h(e.boundAt),lastAttachAt:h(e.lastAttachAt),evicted:e.evicted===!0,evictedAt:h(e.evictedAt),evictedWhy:h(e.evictedWhy)}}).sort((e,t)=>t.lastAttachAt.localeCompare(e.lastAttachAt))}),Z=s(()=>X.value.filter(e=>!e.evicted).length),Q=s(()=>{let e=U.value.lastRestore;return e?JSON.stringify(e):``}),$=s(()=>re(H.value)),Bt=s(()=>typeof H.value.diskBudgetMb==`number`?H.value.diskBudgetMb:void 0),Vt=s(()=>{let e=$.value;if(!e)return[];let t=le(e),{freeKiB:n,capped:r,capacityKiB:i}=oe(e,Bt.value),a=n-t.totalKiB,o=t=>{let n=ne(e.parts,t);return n===null?`? (checkout not measured)`:n===0?`?`:`${Math.max(0,Math.floor(a/n))} more (${g(n)} each)`};return[[`used / total`,ce(e)],[`free`,`${g(n)}${r?` under the ${g(i)} diskBudgetMb cap`:``}`],[`reserve`,`${g(t.totalKiB)} (snapshot staging ${g(t.stagingKiB)} + floor ${g(t.floorKiB)})`],[`headroom`,`${g(Math.max(0,a))} — room for ${o(`hardlink`)} hardlinked trees, ${o(`reconcile`)} lockfile-diverged (reconciling)`],[`measured`,e.at||`—`]]}),Ht=s(()=>{let e=$.value;if(!e)return[];let t=[[`mirror`,g(e.parts.mirror)],[`checkout deps (node_modules)`,g(e.parts.deps)],[`checkout (history + tree + build)`,g(e.parts.checkout)]];for(let[n,r]of Object.entries(e.parts.threads).sort((e,t)=>t[1]-e[1]))t.push([`thread ${n}`,g(r)]);for(let[n,r]of Object.entries(e.parts.homes).sort((e,t)=>t[1]-e[1]))r>=1024&&t.push([`home ${n}`,g(r)]);return t.push([`other (image, /tmp, …)`,g(e.parts.other)]),t});return(s,c)=>(o(),i(e,{title:W.value||`Resident`,nav:`residents`},{leading:t(()=>[...c[0]||=[l(`a`,{class:`back text-sm text-muted no-underline hover:text-primary`,href:`/residents`},`← All residents`,-1)]]),default:t(()=>[l(`section`,de,[c[8]||=l(`h2`,{class:`mb-1.5 font-mono text-xs font-medium uppercase tracking-wider text-muted`},`Lifecycle`,-1),l(`table`,fe,[l(`tbody`,null,[l(`tr`,pe,[c[1]||=l(`td`,{class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},`state`,-1),l(`td`,me,[r(m,{tone:f(ie)(U.value.state),label:U.value.state},null,8,[`tone`,`label`]),l(`span`,v,p(U.value.state),1)])]),l(`tr`,y,[c[2]||=l(`td`,{class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},`reason`,-1),l(`td`,b,[U.value.reason?(o(),u(`span`,x,p(U.value.reason),1)):(o(),u(`span`,S,`—`))])]),l(`tr`,C,[c[3]||=l(`td`,{class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},`last refresh error`,-1),l(`td`,w,[f(h)(U.value.lastRefreshError)?(o(),u(`span`,T,p(f(h)(U.value.lastRefreshError)),1)):(o(),u(`span`,E,`—`))])]),l(`tr`,D,[c[4]||=l(`td`,{class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},`last restore`,-1),l(`td`,O,[Q.value?(o(),u(`span`,k,p(Q.value),1)):(o(),u(`span`,A,`—`))])]),l(`tr`,j,[c[5]||=l(`td`,{class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},`state updated`,-1),l(`td`,M,[f(h)(U.value.updatedAt)?(o(),u(`span`,N,p(f(h)(U.value.updatedAt)),1)):(o(),u(`span`,P,`—`))])]),l(`tr`,F,[c[7]||=l(`td`,{class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},`idle since`,-1),l(`td`,I,[f(h)(U.value.idleSince)?(o(),u(n,{key:0},[a(p(f(h)(U.value.idleSince))+` `,1),c[6]||=l(`span`,{class:`text-xs text-muted`},`(refresh parked; container may sleep)`,-1)],64)):(o(),u(`span`,L,`awake`))])])])])]),l(`section`,R,[c[15]||=l(`h2`,{class:`mb-1.5 font-mono text-xs font-medium uppercase tracking-wider text-muted`},`Pinned facts`,-1),l(`table`,z,[l(`tbody`,null,[l(`tr`,he,[c[9]||=l(`td`,{class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},`repository`,-1),l(`td`,ge,[K.value?(o(),u(`a`,{key:0,class:`text-primary`,href:K.value},p(K.value),9,_e)):(o(),u(`span`,ve,p(f(h)(H.value.resource)||`—`),1))])]),l(`tr`,ye,[c[10]||=l(`td`,{class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},`default ref`,-1),l(`td`,be,p(f(h)(U.value.defaultRef)||f(h)(H.value.defaultRef)||`—`),1)]),l(`tr`,xe,[c[11]||=l(`td`,{class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},`pinned sha`,-1),l(`td`,Se,[q.value?(o(),u(`a`,{key:0,class:`text-primary`,href:q.value},p(G.value),9,Ce)):G.value?(o(),u(`span`,we,p(G.value),1)):(o(),u(`span`,Te,`—`))])]),l(`tr`,Ee,[c[12]||=l(`td`,{class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},`lockfile hash`,-1),l(`td`,De,[f(h)(U.value.lockfileHash)?(o(),u(`span`,Oe,p(f(h)(U.value.lockfileHash)),1)):(o(),u(`span`,ke,`—`))])]),l(`tr`,Ae,[c[13]||=l(`td`,{class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},`provisioned`,-1),l(`td`,je,[f(h)(U.value.provisionedAt)?(o(),u(`span`,Me,p(f(h)(U.value.provisionedAt)),1)):(o(),u(`span`,Ne,`—`))])]),l(`tr`,Pe,[c[14]||=l(`td`,{class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},`last refresh`,-1),l(`td`,Fe,[f(h)(U.value.lastRefreshAt)?(o(),u(`span`,Ie,p(f(h)(U.value.lastRefreshAt)),1)):(o(),u(`span`,Le,`—`))])])])])]),l(`section`,Re,[c[17]||=l(`h2`,{class:`mb-1.5 font-mono text-xs font-medium uppercase tracking-wider text-muted`},`Disk`,-1),$.value?(o(),u(n,{key:0},[l(`table`,ze,[l(`tbody`,null,[(o(!0),u(n,null,d(Vt.value,([e,t])=>(o(),u(`tr`,{key:e,class:`border-t border-muted`},[l(`td`,Be,p(e),1),l(`td`,Ve,p(t),1)]))),128))])]),c[16]||=l(`p`,{class:`mb-1 mt-2 text-xs text-muted`},` components (one du, hardlinks counted once — a thread tree shows only what it does not share with the checkout) `,-1),l(`table`,He,[l(`tbody`,null,[(o(!0),u(n,null,d(Ht.value,([e,t])=>(o(),u(`tr`,{key:e,class:`border-t border-muted`},[l(`td`,Ue,p(e),1),l(`td`,We,p(t),1)]))),128))])])],64)):(o(),u(`p`,Ge,` not measured yet — the first refresh cycle or attach of this container measures it `))]),l(`section`,Ke,[c[18]||=l(`h2`,{class:`mb-1.5 font-mono text-xs font-medium uppercase tracking-wider text-muted`},`Snapshot stamp`,-1),J.value?(o(),u(`table`,qe,[l(`tbody`,null,[(o(),u(n,null,d([`ref`,`sha`,`lockfileHash`,`createdAt`,`mirrorBackupId`,`checkoutBackupId`],e=>l(`tr`,{key:e,class:`border-t border-muted`},[l(`td`,Je,p(e),1),l(`td`,Ye,[f(h)(J.value[e])?(o(),u(`span`,Xe,p(f(h)(J.value[e])),1)):(o(),u(`span`,B,`—`))])])),64))])])):(o(),u(`p`,Ze,`no snapshot recorded`))]),l(`section`,Qe,[c[22]||=l(`h2`,{class:`mb-1.5 font-mono text-xs font-medium uppercase tracking-wider text-muted`},`Thread worktrees`,-1),X.value.length>0?(o(),u(n,{key:0},[l(`p`,$e,p(Z.value)+` live · `+p(X.value.length-Z.value)+` evicted`,1),l(`div`,et,[l(`table`,tt,[l(`tbody`,null,[c[21]||=l(`tr`,{class:`text-xs text-muted`},[l(`td`,{class:`px-2 py-1`},`thread`),l(`td`,{class:`px-2 py-1`},`ref · sha`),l(`td`,{class:`px-2 py-1`},`user · deps`),l(`td`,{class:`px-2 py-1`},`bound · last attach`)],-1),(o(!0),u(n,null,d(X.value,e=>(o(),u(`tr`,{key:e.threadKey,class:`border-t border-muted`},[l(`td`,nt,[e.evicted?(o(),u(`span`,rt,[a(`evicted `+p(e.evictedAt||`—`),1),e.evictedWhy?(o(),u(n,{key:0},[a(` · `+p(e.evictedWhy),1)],64)):ee(``,!0)])):(o(),i(m,{key:1,tone:`green`,label:`live`,tip:`live`})),a(` `+p(e.threadKey),1)]),l(`td`,it,[a(p(e.ref)+` · `,1),e.commitHref?(o(),u(`a`,{key:0,class:`text-primary`,href:e.commitHref},p(e.sha),9,at)):e.sha?(o(),u(`span`,ot,p(e.sha),1)):(o(),u(`span`,st,`—`))]),l(`td`,ct,[e.user?(o(),u(`span`,lt,p(e.user),1)):(o(),u(`span`,ut,`—`)),c[19]||=a(` · `,-1),e.deps?(o(),u(`span`,dt,p(e.deps),1)):(o(),u(`span`,ft,`—`))]),l(`td`,pt,[e.boundAt?(o(),u(`span`,mt,p(e.boundAt),1)):(o(),u(`span`,ht,`—`)),c[20]||=a(` · `,-1),e.lastAttachAt?(o(),u(`span`,gt,p(e.lastAttachAt),1)):(o(),u(`span`,_t,`—`))])]))),128))])])])],64)):(o(),u(`p`,vt,`no thread worktrees`))]),l(`section`,yt,[c[23]||=l(`h2`,{class:`mb-1.5 font-mono text-xs font-medium uppercase tracking-wider text-muted`},`Pending schedules`,-1),l(`table`,bt,[l(`tbody`,null,[(o(),u(n,null,d([[`refresh`,`refresh`],[`provision run`,`provisionRun`],[`provision deadline`,`provisionDeadline`]],([e,t])=>l(`tr`,{key:t,class:`border-t border-muted`},[l(`td`,xt,p(e),1),l(`td`,St,p(f(h)(zt.value[t]??0)||`0`),1)])),64))])])]),l(`section`,Ct,[c[25]||=l(`h2`,{class:`mb-1.5 font-mono text-xs font-medium uppercase tracking-wider text-muted`},`Command table`,-1),Y.value.length>0?(o(),u(`table`,wt,[l(`tbody`,null,[(o(!0),u(n,null,d(Y.value,e=>(o(),u(`tr`,{key:e.name,class:`border-t border-muted`},[l(`td`,Tt,p(e.name),1),l(`td`,Et,[l(`code`,null,p(e.command),1),c[24]||=a(),l(`span`,Dt,p(e.effect),1)])]))),128))])])):(o(),u(`p`,Ot,`no command table`))]),l(`section`,kt,[c[26]||=l(`h2`,{class:`mb-1.5 font-mono text-xs font-medium uppercase tracking-wider text-muted`},`Registry settings`,-1),l(`table`,At,[l(`tbody`,null,[(o(!0),u(n,null,d([[`onboarded`,f(h)(H.value.onboardedAt)],[`record updated`,f(h)(H.value.updatedAt)],[`provisioning timeout (ms)`,f(h)(H.value.provisioningTimeoutMs)],[`disk budget (MB)`,f(h)(H.value.diskBudgetMb)],[`worktree TTL (days)`,f(h)(H.value.worktreeTtlDays??7)]],([e,t])=>(o(),u(`tr`,{key:e,class:`border-t border-muted`},[l(`td`,jt,p(e),1),l(`td`,Mt,[t?(o(),u(`span`,Nt,p(t),1)):(o(),u(`span`,Pt,`—`))])]))),128))])])]),l(`p`,Ft,[c[27]||=a(` Manage from chat: `,-1),l(`code`,It,`repo rebuild `+p(W.value),1),c[28]||=a(` · `,-1),l(`code`,Lt,`repo reconfigure `+p(W.value)+` …`,1),c[29]||=a(` · `,-1),l(`code`,Rt,`repo offboard `+p(W.value)+` --dry-run`,1)])]),_:1},8,[`title`]))}});export{V as default};
1
+ import{t as e}from"./AppShell-lYVcj-k7.js";import{Lr as t,Xt as n,_n as r,cn as i,gn as a,ir as o,ln as ee,on as s,r as c,sn as l,un as u,ur as d,vi as f,wi as p,yn as te}from"./seed-DeKSHrt2.js";import{t as m}from"./StatusDot-W2x3d782.js";import{a as ne,c as re,d as ie,f as h,i as g,l as ae,n as oe,o as se,r as ce,s as _,t as le,u as ue}from"./residentDiskBudget-BMBKlYRH.js";var de={class:`mt-4 first:mt-0`},fe={class:`w-full border-collapse font-mono text-[0.8125rem]`},pe={class:`border-t border-muted`},me={class:`px-2 py-1 align-top`},v={class:`ml-1.5 font-medium`},y={class:`border-t border-muted`},b={class:`break-all px-2 py-1 align-top`},x={key:0},S={key:1,class:`text-dimmed`},C={class:`border-t border-muted`},w={class:`break-all px-2 py-1 align-top`},T={key:0},E={key:1,class:`text-dimmed`},D={class:`border-t border-muted`},O={class:`break-all px-2 py-1 align-top`},k={key:0},A={key:1,class:`text-dimmed`},j={class:`border-t border-muted`},M={class:`break-all px-2 py-1 align-top`},N={key:0},P={key:1,class:`text-dimmed`},F={class:`border-t border-muted`},I={class:`break-all px-2 py-1 align-top`},L={key:1,class:`text-dimmed`},R={class:`mt-4`},z={class:`w-full border-collapse font-mono text-[0.8125rem]`},he={class:`border-t border-muted`},ge={class:`break-all px-2 py-1 align-top`},_e=[`href`],ve={key:1},ye={class:`border-t border-muted`},be={class:`break-all px-2 py-1 align-top`},xe={class:`border-t border-muted`},Se={class:`break-all px-2 py-1 align-top`},Ce=[`href`],we={key:1},Te={key:2,class:`text-dimmed`},Ee={class:`border-t border-muted`},De={class:`break-all px-2 py-1 align-top`},Oe={key:0},ke={key:1,class:`text-dimmed`},Ae={class:`border-t border-muted`},je={class:`break-all px-2 py-1 align-top`},Me={key:0},Ne={key:1,class:`text-dimmed`},Pe={class:`border-t border-muted`},Fe={class:`break-all px-2 py-1 align-top`},Ie={key:0},Le={key:1,class:`text-dimmed`},Re={class:`mt-4`},ze={class:`w-full border-collapse font-mono text-[0.8125rem]`},Be={class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},Ve={class:`break-all px-2 py-1 align-top`},He={class:`w-full border-collapse font-mono text-[0.8125rem]`},Ue={class:`w-32 break-all px-2 py-1 align-top text-muted sm:w-96 sm:break-words`},We={class:`px-2 py-1 align-top`},Ge={key:1,class:`text-xs text-muted`},Ke={class:`mt-4`},qe={key:0,class:`w-full border-collapse font-mono text-[0.8125rem]`},Je={class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},Ye={class:`break-all px-2 py-1 align-top`},Xe={key:0},B={key:1,class:`text-dimmed`},Ze={key:1,class:`text-xs text-muted`},Qe={class:`mt-4`},$e={class:`mb-1 text-xs text-muted`},et={class:`overflow-x-auto`},tt={class:`w-full border-collapse font-mono text-[0.8125rem]`},nt={class:`break-all px-2 py-1 align-top`},rt={key:0,class:`text-xs text-muted`},it={class:`break-all px-2 py-1 align-top`},at=[`href`],ot={key:1},st={key:2,class:`text-dimmed`},ct={class:`break-all px-2 py-1 align-top`},lt={key:0},ut={key:1,class:`text-dimmed`},dt={key:2},ft={key:3,class:`text-dimmed`},pt={class:`break-all px-2 py-1 align-top`},mt={key:0},ht={key:1,class:`text-dimmed`},gt={key:2},_t={key:3,class:`text-dimmed`},vt={key:1,class:`text-xs text-muted`},yt={class:`mt-4`},bt={class:`w-full border-collapse font-mono text-[0.8125rem]`},xt={class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},St={class:`px-2 py-1 align-top`},Ct={class:`mt-4`},wt={key:0,class:`w-full border-collapse font-mono text-[0.8125rem]`},Tt={class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},Et={class:`break-all px-2 py-1 align-top`},Dt={class:`text-xs text-muted`},Ot={key:1,class:`text-xs text-muted`},kt={class:`mt-4`},At={class:`w-full border-collapse font-mono text-[0.8125rem]`},jt={class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},Mt={class:`break-all px-2 py-1 align-top`},Nt={key:0},Pt={key:1,class:`text-dimmed`},Ft={class:`mt-4 text-xs text-muted`},It={class:`rounded bg-elevated px-1 py-0.5`},Lt={class:`rounded bg-elevated px-1 py-0.5`},Rt={class:`rounded bg-elevated px-1 py-0.5`},V=te({__name:`ResidentDetailPage`,setup(te){let V=c(`resident`),H=s(()=>V?.record??{}),U=s(()=>ae(H.value)),W=s(()=>ue(H.value)||(V?.slug??``)),G=s(()=>h(U.value.sha)),K=s(()=>se.test(W.value)?`https://github.com/${W.value}`:void 0),q=s(()=>K.value&&/^[0-9a-f]{7,40}$/.test(G.value)?`${K.value}/commit/${G.value}`:void 0),J=s(()=>{let e=U.value.snapshot;return e&&typeof e==`object`?_(e):void 0}),zt=s(()=>_(U.value.schedules)),Y=s(()=>{let e=_(H.value.commands),t=_(H.value.effects);return Object.keys(e).sort().map(n=>({name:n,command:h(e[n]),effect:h(t[n])||`readonly`}))}),X=s(()=>{let e=U.value.threads;return(Array.isArray(e)?e.map(_):[]).map(e=>{let t=h(e.sha);return{threadKey:h(e.threadKey)||`?`,ref:h(e.ref)||`?`,sha:t?t.slice(0,8):``,commitHref:K.value&&/^[0-9a-f]{7,40}$/.test(t)?`${K.value}/commit/${t}`:void 0,user:h(e.user),deps:h(e.deps),boundAt:h(e.boundAt),lastAttachAt:h(e.lastAttachAt),evicted:e.evicted===!0,evictedAt:h(e.evictedAt),evictedWhy:h(e.evictedWhy)}}).sort((e,t)=>t.lastAttachAt.localeCompare(e.lastAttachAt))}),Z=s(()=>X.value.filter(e=>!e.evicted).length),Q=s(()=>{let e=U.value.lastRestore;return e?JSON.stringify(e):``}),$=s(()=>re(H.value)),Bt=s(()=>typeof H.value.diskBudgetMb==`number`?H.value.diskBudgetMb:void 0),Vt=s(()=>{let e=$.value;if(!e)return[];let t=le(e),{freeKiB:n,capped:r,capacityKiB:i}=oe(e,Bt.value),a=n-t.totalKiB,o=t=>{let n=ne(e.parts,t);return n===null?`? (checkout not measured)`:n===0?`?`:`${Math.max(0,Math.floor(a/n))} more (${g(n)} each)`};return[[`used / total`,ce(e)],[`free`,`${g(n)}${r?` under the ${g(i)} diskBudgetMb cap`:``}`],[`reserve`,`${g(t.totalKiB)} (snapshot staging ${g(t.stagingKiB)} + floor ${g(t.floorKiB)})`],[`headroom`,`${g(Math.max(0,a))} — room for ${o(`hardlink`)} hardlinked trees, ${o(`reconcile`)} lockfile-diverged (reconciling)`],[`measured`,e.at||`—`]]}),Ht=s(()=>{let e=$.value;if(!e)return[];let t=[[`mirror`,g(e.parts.mirror)],[`checkout deps (node_modules)`,g(e.parts.deps)],[`checkout (history + tree + build)`,g(e.parts.checkout)]];for(let[n,r]of Object.entries(e.parts.threads).sort((e,t)=>t[1]-e[1]))t.push([`thread ${n}`,g(r)]);for(let[n,r]of Object.entries(e.parts.homes).sort((e,t)=>t[1]-e[1]))r>=1024&&t.push([`home ${n}`,g(r)]);return t.push([`other (image, /tmp, …)`,g(e.parts.other)]),t});return(s,c)=>(o(),i(e,{title:W.value||`Resident`,nav:`residents`},{leading:t(()=>[...c[0]||=[l(`a`,{class:`back text-sm text-muted no-underline hover:text-primary`,href:`/residents`},`← All residents`,-1)]]),default:t(()=>[l(`section`,de,[c[8]||=l(`h2`,{class:`mb-1.5 font-mono text-xs font-medium uppercase tracking-wider text-muted`},`Lifecycle`,-1),l(`table`,fe,[l(`tbody`,null,[l(`tr`,pe,[c[1]||=l(`td`,{class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},`state`,-1),l(`td`,me,[r(m,{tone:f(ie)(U.value.state),label:U.value.state},null,8,[`tone`,`label`]),l(`span`,v,p(U.value.state),1)])]),l(`tr`,y,[c[2]||=l(`td`,{class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},`reason`,-1),l(`td`,b,[U.value.reason?(o(),u(`span`,x,p(U.value.reason),1)):(o(),u(`span`,S,`—`))])]),l(`tr`,C,[c[3]||=l(`td`,{class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},`last refresh error`,-1),l(`td`,w,[f(h)(U.value.lastRefreshError)?(o(),u(`span`,T,p(f(h)(U.value.lastRefreshError)),1)):(o(),u(`span`,E,`—`))])]),l(`tr`,D,[c[4]||=l(`td`,{class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},`last restore`,-1),l(`td`,O,[Q.value?(o(),u(`span`,k,p(Q.value),1)):(o(),u(`span`,A,`—`))])]),l(`tr`,j,[c[5]||=l(`td`,{class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},`state updated`,-1),l(`td`,M,[f(h)(U.value.updatedAt)?(o(),u(`span`,N,p(f(h)(U.value.updatedAt)),1)):(o(),u(`span`,P,`—`))])]),l(`tr`,F,[c[7]||=l(`td`,{class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},`idle since`,-1),l(`td`,I,[f(h)(U.value.idleSince)?(o(),u(n,{key:0},[a(p(f(h)(U.value.idleSince))+` `,1),c[6]||=l(`span`,{class:`text-xs text-muted`},`(refresh parked; container may sleep)`,-1)],64)):(o(),u(`span`,L,`awake`))])])])])]),l(`section`,R,[c[15]||=l(`h2`,{class:`mb-1.5 font-mono text-xs font-medium uppercase tracking-wider text-muted`},`Pinned facts`,-1),l(`table`,z,[l(`tbody`,null,[l(`tr`,he,[c[9]||=l(`td`,{class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},`repository`,-1),l(`td`,ge,[K.value?(o(),u(`a`,{key:0,class:`text-primary`,href:K.value},p(K.value),9,_e)):(o(),u(`span`,ve,p(f(h)(H.value.resource)||`—`),1))])]),l(`tr`,ye,[c[10]||=l(`td`,{class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},`default ref`,-1),l(`td`,be,p(f(h)(U.value.defaultRef)||f(h)(H.value.defaultRef)||`—`),1)]),l(`tr`,xe,[c[11]||=l(`td`,{class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},`pinned sha`,-1),l(`td`,Se,[q.value?(o(),u(`a`,{key:0,class:`text-primary`,href:q.value},p(G.value),9,Ce)):G.value?(o(),u(`span`,we,p(G.value),1)):(o(),u(`span`,Te,`—`))])]),l(`tr`,Ee,[c[12]||=l(`td`,{class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},`lockfile hash`,-1),l(`td`,De,[f(h)(U.value.lockfileHash)?(o(),u(`span`,Oe,p(f(h)(U.value.lockfileHash)),1)):(o(),u(`span`,ke,`—`))])]),l(`tr`,Ae,[c[13]||=l(`td`,{class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},`provisioned`,-1),l(`td`,je,[f(h)(U.value.provisionedAt)?(o(),u(`span`,Me,p(f(h)(U.value.provisionedAt)),1)):(o(),u(`span`,Ne,`—`))])]),l(`tr`,Pe,[c[14]||=l(`td`,{class:`w-32 px-2 py-1 align-top text-muted sm:w-48 sm:whitespace-nowrap`},`last refresh`,-1),l(`td`,Fe,[f(h)(U.value.lastRefreshAt)?(o(),u(`span`,Ie,p(f(h)(U.value.lastRefreshAt)),1)):(o(),u(`span`,Le,`—`))])])])])]),l(`section`,Re,[c[17]||=l(`h2`,{class:`mb-1.5 font-mono text-xs font-medium uppercase tracking-wider text-muted`},`Disk`,-1),$.value?(o(),u(n,{key:0},[l(`table`,ze,[l(`tbody`,null,[(o(!0),u(n,null,d(Vt.value,([e,t])=>(o(),u(`tr`,{key:e,class:`border-t border-muted`},[l(`td`,Be,p(e),1),l(`td`,Ve,p(t),1)]))),128))])]),c[16]||=l(`p`,{class:`mb-1 mt-2 text-xs text-muted`},` components (one du, hardlinks counted once — a thread tree shows only what it does not share with the checkout) `,-1),l(`table`,He,[l(`tbody`,null,[(o(!0),u(n,null,d(Ht.value,([e,t])=>(o(),u(`tr`,{key:e,class:`border-t border-muted`},[l(`td`,Ue,p(e),1),l(`td`,We,p(t),1)]))),128))])])],64)):(o(),u(`p`,Ge,` not measured yet — the first refresh cycle or attach of this container measures it `))]),l(`section`,Ke,[c[18]||=l(`h2`,{class:`mb-1.5 font-mono text-xs font-medium uppercase tracking-wider text-muted`},`Snapshot stamp`,-1),J.value?(o(),u(`table`,qe,[l(`tbody`,null,[(o(),u(n,null,d([`ref`,`sha`,`lockfileHash`,`createdAt`,`mirrorBackupId`,`checkoutBackupId`],e=>l(`tr`,{key:e,class:`border-t border-muted`},[l(`td`,Je,p(e),1),l(`td`,Ye,[f(h)(J.value[e])?(o(),u(`span`,Xe,p(f(h)(J.value[e])),1)):(o(),u(`span`,B,`—`))])])),64))])])):(o(),u(`p`,Ze,`no snapshot recorded`))]),l(`section`,Qe,[c[22]||=l(`h2`,{class:`mb-1.5 font-mono text-xs font-medium uppercase tracking-wider text-muted`},`Thread worktrees`,-1),X.value.length>0?(o(),u(n,{key:0},[l(`p`,$e,p(Z.value)+` live · `+p(X.value.length-Z.value)+` evicted`,1),l(`div`,et,[l(`table`,tt,[l(`tbody`,null,[c[21]||=l(`tr`,{class:`text-xs text-muted`},[l(`td`,{class:`px-2 py-1`},`thread`),l(`td`,{class:`px-2 py-1`},`ref · sha`),l(`td`,{class:`px-2 py-1`},`user · deps`),l(`td`,{class:`px-2 py-1`},`bound · last attach`)],-1),(o(!0),u(n,null,d(X.value,e=>(o(),u(`tr`,{key:e.threadKey,class:`border-t border-muted`},[l(`td`,nt,[e.evicted?(o(),u(`span`,rt,[a(`evicted `+p(e.evictedAt||`—`),1),e.evictedWhy?(o(),u(n,{key:0},[a(` · `+p(e.evictedWhy),1)],64)):ee(``,!0)])):(o(),i(m,{key:1,tone:`green`,label:`live`,tip:`live`})),a(` `+p(e.threadKey),1)]),l(`td`,it,[a(p(e.ref)+` · `,1),e.commitHref?(o(),u(`a`,{key:0,class:`text-primary`,href:e.commitHref},p(e.sha),9,at)):e.sha?(o(),u(`span`,ot,p(e.sha),1)):(o(),u(`span`,st,`—`))]),l(`td`,ct,[e.user?(o(),u(`span`,lt,p(e.user),1)):(o(),u(`span`,ut,`—`)),c[19]||=a(` · `,-1),e.deps?(o(),u(`span`,dt,p(e.deps),1)):(o(),u(`span`,ft,`—`))]),l(`td`,pt,[e.boundAt?(o(),u(`span`,mt,p(e.boundAt),1)):(o(),u(`span`,ht,`—`)),c[20]||=a(` · `,-1),e.lastAttachAt?(o(),u(`span`,gt,p(e.lastAttachAt),1)):(o(),u(`span`,_t,`—`))])]))),128))])])])],64)):(o(),u(`p`,vt,`no thread worktrees`))]),l(`section`,yt,[c[23]||=l(`h2`,{class:`mb-1.5 font-mono text-xs font-medium uppercase tracking-wider text-muted`},`Pending schedules`,-1),l(`table`,bt,[l(`tbody`,null,[(o(),u(n,null,d([[`refresh`,`refresh`],[`provision run`,`provisionRun`],[`provision deadline`,`provisionDeadline`]],([e,t])=>l(`tr`,{key:t,class:`border-t border-muted`},[l(`td`,xt,p(e),1),l(`td`,St,p(f(h)(zt.value[t]??0)||`0`),1)])),64))])])]),l(`section`,Ct,[c[25]||=l(`h2`,{class:`mb-1.5 font-mono text-xs font-medium uppercase tracking-wider text-muted`},`Command table`,-1),Y.value.length>0?(o(),u(`table`,wt,[l(`tbody`,null,[(o(!0),u(n,null,d(Y.value,e=>(o(),u(`tr`,{key:e.name,class:`border-t border-muted`},[l(`td`,Tt,p(e.name),1),l(`td`,Et,[l(`code`,null,p(e.command),1),c[24]||=a(),l(`span`,Dt,p(e.effect),1)])]))),128))])])):(o(),u(`p`,Ot,`no command table`))]),l(`section`,kt,[c[26]||=l(`h2`,{class:`mb-1.5 font-mono text-xs font-medium uppercase tracking-wider text-muted`},`Registry settings`,-1),l(`table`,At,[l(`tbody`,null,[(o(!0),u(n,null,d([[`onboarded`,f(h)(H.value.onboardedAt)],[`record updated`,f(h)(H.value.updatedAt)],[`provisioning timeout (ms)`,f(h)(H.value.provisioningTimeoutMs)],[`disk budget (MB)`,f(h)(H.value.diskBudgetMb)],[`worktree TTL (days)`,f(h)(H.value.worktreeTtlDays??7)]],([e,t])=>(o(),u(`tr`,{key:e,class:`border-t border-muted`},[l(`td`,jt,p(e),1),l(`td`,Mt,[t?(o(),u(`span`,Nt,p(t),1)):(o(),u(`span`,Pt,`—`))])]))),128))])])]),l(`p`,Ft,[c[27]||=a(` Manage from chat: `,-1),l(`code`,It,`repo rebuild `+p(W.value),1),c[28]||=a(` · `,-1),l(`code`,Lt,`repo reconfigure `+p(W.value)+` …`,1),c[29]||=a(` · `,-1),l(`code`,Rt,`repo offboard `+p(W.value)+` --dry-run`,1)])]),_:1},8,[`title`]))}});export{V as default};