@junghanacs/entwurf 0.12.8 → 0.12.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/AGENTS.md +134 -248
  2. package/BASELINE.md +1 -1
  3. package/CHANGELOG.md +39 -1
  4. package/DELIVERY.md +5 -4
  5. package/README.md +148 -23
  6. package/VERIFY.md +3 -2
  7. package/demo/README.md +2 -2
  8. package/demo/demo.sh +7 -1
  9. package/docs/setup-clean-host.md +24 -7
  10. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +168 -81
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +12 -12
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +5 -3
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +26 -9
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +10 -6
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +16 -18
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +13 -13
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +31 -41
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +8 -9
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +32 -25
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +4 -4
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +5 -4
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +14 -13
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +11 -3
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +15 -4
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +500 -54
  26. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js +8 -3
  27. package/mcp/entwurf-bridge/dist/scripts/agy-imprint.js +14 -2
  28. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +155 -29
  29. package/mcp/entwurf-bridge/src/index.ts +200 -94
  30. package/package.json +11 -9
  31. package/pi-extensions/entwurf-control.ts +81 -39
  32. package/pi-extensions/lib/acp/models.ts +12 -12
  33. package/pi-extensions/lib/acp/overlay.ts +5 -3
  34. package/pi-extensions/lib/entwurf-core.ts +26 -9
  35. package/pi-extensions/lib/entwurf-deliverability.ts +10 -6
  36. package/pi-extensions/lib/entwurf-fact-provider.ts +24 -19
  37. package/pi-extensions/lib/entwurf-facts.ts +13 -13
  38. package/pi-extensions/lib/entwurf-resume-args.ts +40 -46
  39. package/pi-extensions/lib/entwurf-self-address.ts +8 -9
  40. package/pi-extensions/lib/entwurf-v2-contract-schema.ts +7 -2
  41. package/pi-extensions/lib/entwurf-v2-contract.ts +32 -25
  42. package/pi-extensions/lib/entwurf-v2-decider.ts +8 -8
  43. package/pi-extensions/lib/entwurf-v2-lock.ts +5 -4
  44. package/pi-extensions/lib/entwurf-v2-production.ts +22 -13
  45. package/pi-extensions/lib/entwurf-v2-spawn-production.ts +11 -3
  46. package/pi-extensions/lib/meta-sender-identity.ts +15 -5
  47. package/pi-extensions/lib/meta-session.ts +543 -58
  48. package/pi-extensions/meta-bridge-hook.ts +8 -2
  49. package/run.sh +78 -67
  50. package/scripts/agy-bridge-config.py +249 -49
  51. package/scripts/agy-bridge.sh +59 -14
  52. package/scripts/agy-imprint.ts +15 -1
  53. package/scripts/check-acp-carrier-augment.ts +34 -2
  54. package/scripts/check-acp-sdk-surface.ts +22 -11
  55. package/scripts/check-acp-session-reuse.ts +1 -1
  56. package/scripts/check-acp-session-store.ts +3 -3
  57. package/scripts/check-agy-permission-matrix.py +655 -0
  58. package/scripts/check-agy-sender-identity.ts +83 -0
  59. package/scripts/check-entwurf-control-rpc.ts +2 -2
  60. package/scripts/check-entwurf-deliverability.ts +9 -6
  61. package/scripts/check-entwurf-fact-provider.ts +16 -8
  62. package/scripts/check-entwurf-facts.ts +13 -13
  63. package/scripts/check-entwurf-resume-args.ts +25 -63
  64. package/scripts/check-entwurf-self-address.ts +187 -4
  65. package/scripts/check-entwurf-session-identity.ts +7 -6
  66. package/scripts/check-entwurf-v2-contract.ts +2 -2
  67. package/scripts/check-entwurf-v2-production.ts +9 -7
  68. package/scripts/check-entwurf-v2-spawn-production.ts +8 -4
  69. package/scripts/check-entwurf-v2-surface.ts +327 -14
  70. package/scripts/check-fresh-cut-gate.sh +305 -4
  71. package/scripts/check-gate-qualification.ts +785 -0
  72. package/scripts/check-meta-identity-consumers.ts +501 -1
  73. package/scripts/check-meta-listing.ts +91 -9
  74. package/scripts/check-meta-receiver-marker.ts +54 -0
  75. package/scripts/check-model-lock.ts +1 -1
  76. package/scripts/check-shell-quote.ts +2 -1
  77. package/scripts/lib/mutation-qualify.ts +794 -0
  78. package/scripts/meta-bridge-fresh-cut.ts +164 -28
  79. package/scripts/mutants/acp-augment.json +30 -0
  80. package/scripts/mutants/agy-permission.json +144 -0
  81. package/scripts/mutants/meta-identity.json +17 -0
  82. package/scripts/mutants/self-address.json +59 -0
  83. package/scripts/mutants/v2-surface.json +87 -0
  84. package/scripts/pi_settings_io.py +65 -0
  85. package/scripts/register-pi-package.py +183 -37
  86. package/scripts/register-pi-provider.py +68 -10
  87. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  88. package/scripts/smoke-acp-socket-citizen-live.ts +2 -2
  89. package/scripts/smoke-acp-v2-send-live.ts +33 -18
  90. package/scripts/smoke-agy-install-state.sh +205 -20
  91. package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -2
  92. package/scripts/smoke-meta-install-state.sh +1 -1
  93. package/scripts/smoke-pi-attach.ts +7 -2
  94. package/scripts/smoke-user-scope-citizen.sh +177 -0
  95. package/pi-extensions/lib/entwurf-mailbox-guard.ts +0 -100
  96. package/scripts/check-entwurf-mailbox-guard.ts +0 -262
@@ -1,12 +1,12 @@
1
1
  /**
2
- * meta-session — 1.0.0 garden-native meta-bridge, step 2: the RECORD AUTHORITY.
2
+ * meta-session — the shared V3 garden-citizen RECORD AUTHORITY.
3
3
  *
4
- * Backend-agnostic garden layer (#30). A *meta-session* is the bib card for a
5
- * native backend session (Claude Code / Antigravity / Codex) that has NO pi
6
- * JSONL of its own: an opaque pointer record that makes the native session a
7
- * garden citizen addressable + wakeable by a garden id WITHOUT pretending pi
8
- * owns its transcript (Hard Rule #8: reference the backend transcript, never
9
- * hydrate or replay it).
4
+ * This module entered through the native meta-bridge (#30), then became the one
5
+ * identity store for every addressable citizen, including `backend:"pi"` (#50).
6
+ * A record is a backend-owned-session bib card: it binds the backend's native id
7
+ * and transcript pointer to a garden id without taking over that backend's runtime,
8
+ * auth, or transcript. The module name is history; the live identity schema is not
9
+ * a native-only or pi-exception axis.
10
10
  *
11
11
  * Two layers, clearly sectioned:
12
12
  * 1. RECORD functions + types (mint / serialize / parse / certifyActiveStore /
@@ -58,7 +58,7 @@ import { generateSessionId, SESSION_ID_RE } from "./session-id.js";
58
58
  // ---------------------------------------------------------------------------
59
59
  // Errors
60
60
  // ---------------------------------------------------------------------------
61
- /** A meta-record is malformed, or an input violates the record contract. */
61
+ /** A garden-citizen record is malformed, or an input violates the record contract. */
62
62
  export class MetaRecordError extends Error {
63
63
  constructor(message) {
64
64
  super(message);
@@ -106,8 +106,9 @@ export function requireNonEmptyString(value, field) {
106
106
  }
107
107
  /** Validate the 3-backend NATIVE bridge axis (sender/receiver markers, capability
108
108
  * drift guard). Not a record-schema validator: identity records take
109
- * `requireCitizenBackend` (which admits `pi`); markers stay native-3 because a pi
110
- * session's sender identity is env-authored, never marker-authored. */
109
+ * `requireCitizenBackend` (which admits `pi`). Markers stay native-3 because the
110
+ * pi adapter carries its record-established garden id into children via env rather
111
+ * than using the native-hook pid marker rail. */
111
112
  export function requireBackend(value) {
112
113
  if (typeof value !== "string" || !META_BACKENDS.includes(value)) {
113
114
  throw new MetaRecordError(`meta-record "backend" must be one of ${META_BACKENDS.join(" | ")} (got ${describe(value)}).`);
@@ -164,7 +165,7 @@ function isoNow(now) {
164
165
  // ---------------------------------------------------------------------------
165
166
  /** The one live identity schema number. */
166
167
  export const META_SCHEMA_VERSION_V3 = 3;
167
- /** The 4 record-citizen backends: the three native backends + `pi` itself. */
168
+ /** Every backend admitted by the one V3 record-citizen schema. */
168
169
  export const META_CITIZEN_BACKENDS = ["claude-code", "antigravity", "codex", "pi"];
169
170
  export function requireCitizenBackend(value) {
170
171
  if (typeof value !== "string" || !META_CITIZEN_BACKENDS.includes(value)) {
@@ -195,6 +196,60 @@ export const FRESH_CUT_COMMAND_INSTALLED = "entwurf meta-bridge-fresh-cut";
195
196
  * (never replaces) the `FRESH_CUT_COMMAND` substring the gates assert.
196
197
  */
197
198
  export const FRESH_CUT_PRESCRIPTION = `\`${FRESH_CUT_COMMAND}\` (from an installed package: \`${FRESH_CUT_COMMAND_INSTALLED}\`)`;
199
+ /**
200
+ * THE fresh-cut EXIT CONTRACT (#54) — the store doctor's `0/1/2/3` has a sibling here.
201
+ *
202
+ * The cut used to answer `1` for three world-states that are not each other, and the
203
+ * documented chain `fresh-cut && setup` therefore stopped on all of them alike. What
204
+ * distinguishes them is not severity, it is **what already moved**:
205
+ *
206
+ * - `COMPLETE` (0) — the generation is archived (or there was none), the fresh v3
207
+ * store is open, and every dead/refuted marker and socket is gone. `&& setup` is
208
+ * exactly right here.
209
+ * - `NO_MOVE` (1) — the cut REFUSED before touching anything: a live or unprovable
210
+ * surface, an occupied archive destination, a surface that could not be read or
211
+ * planned, or a first rename that failed. The host is what it was; fix the named
212
+ * cause and re-run the same command. Nothing downstream may proceed, because the
213
+ * store the install refused is still there.
214
+ * - `USAGE` (2) — the operator asked for something this verb does not have. Same
215
+ * number the `-h` path and the store doctor use, so a wrapper never has to learn a
216
+ * second convention.
217
+ * - `HALF_CUT` (3) — a failure arrived AFTER at least one archive move, so the cut
218
+ * transition is incomplete and the fresh generation is not confirmed open. This
219
+ * state wants inspection before anything else runs; a re-run can finish the cut
220
+ * under its own stamp.
221
+ * - `CLEANUP_INCOMPLETE` (4) — the cut IS done: the generation moved, the fresh
222
+ * generation is open, install and citizen birth are no longer blocked by the
223
+ * store. What survived is disposable process state (marker/socket residue) that
224
+ * could not be unlinked. A runbook may run `setup` here but may not call the host
225
+ * clean. Re-running fresh-cut is safe only before a new citizen is born; after
226
+ * that, repair the named residue manually rather than archiving the new generation.
227
+ *
228
+ * Still fail-closed: only `COMPLETE` is zero. #54 asked for the states to become
229
+ * distinguishable, not for cleanup failure to become success.
230
+ *
231
+ * Frozen numbers. A caller — a shell runbook, CI, another agent — reads these, so
232
+ * renumbering is a breaking change to every chain that branches on them.
233
+ */
234
+ export const FRESH_CUT_EXIT = {
235
+ COMPLETE: 0,
236
+ NO_MOVE: 1,
237
+ USAGE: 2,
238
+ HALF_CUT: 3,
239
+ CLEANUP_INCOMPLETE: 4,
240
+ };
241
+ /**
242
+ * The mid-cut verdict, as a function rather than a branch, so the distinction #54 is
243
+ * about can be EXECUTED by a gate instead of grepped for.
244
+ *
245
+ * A rename that fails is not one event: if nothing had moved yet it is a refusal that
246
+ * happens to arrive late ({@link FRESH_CUT_EXIT.NO_MOVE} — the host is untouched), and
247
+ * if something had, it is a genuine {@link FRESH_CUT_EXIT.HALF_CUT}. The old code said
248
+ * both of those in prose and neither of them in its exit status.
249
+ */
250
+ export function midCutExit(archivedCount) {
251
+ return archivedCount === 0 ? FRESH_CUT_EXIT.NO_MOVE : FRESH_CUT_EXIT.HALF_CUT;
252
+ }
198
253
  /** The uniform "this record is not a live-generation v3 record" error, naming the fresh-cut fix. */
199
254
  function nonV3RecordMessage(version) {
200
255
  return (`meta-record "schemaVersion" must be ${META_SCHEMA_VERSION_V3} (got ${describe(version)}). ` +
@@ -353,15 +408,11 @@ export function parseMetaIdentity(json) {
353
408
  // const for the three existing backends (the drift guard) and COVERS exactly
354
409
  // META_CITIZEN_BACKENDS (pi included).
355
410
  //
356
- // pi's wakeMode = direct-inject (NOT self-fetch): pi's live wake path is the
357
- // entwurf-control socket `pi.sendMessage(... triggerTurn ...)` injects the
358
- // body straight into the model-visible turn, which is direct-inject by the
359
- // WakeMode definition (the last-1cm: who puts the body in front of the model).
360
- // self-fetch is Claude's mailbox path (the model must call its inbox-read MCP).
361
- // pi's dormant→resume→mailbox path is self-fetch-shaped, so pi is really
362
- // BIMODAL; a single wakeMode field cannot express both. Splitting it
363
- // (mailboxWakeMode vs controlSocketWakeMode) is out of 3C scope — for now the
364
- // single field reports pi's primary live capability (direct-inject) honestly.
411
+ // `wakeMode` describes the last centimetre, not citizen rank. The control-socket
412
+ // adapter injects the body into a pi turn, so backend `pi` is `direct-inject`;
413
+ // Claude's mailbox is `self-fetch`; native-push adapters are direct injection.
414
+ // Dormant spawn-bg resume is a separate transport/ownership decision and does not
415
+ // turn the pi record into a mailbox citizen.
365
416
  // ---------------------------------------------------------------------------
366
417
  /** Bump only on a breaking capability-registry shape change; the parser refuses other versions. */
367
418
  export const CAPABILITY_SCHEMA_VERSION = 1;
@@ -594,27 +645,106 @@ export function certifyActiveStore(entries, readRecord) {
594
645
  * the doctor and the install preflight call an unreadable host clean (2026-07-25
595
646
  * fresh-eyes review; `existsSync` returns false for a directory it merely cannot search,
596
647
  * which is the same laundering {@link inspectRecordEntry} refuses on the targeted path).
648
+ *
649
+ * Split out of {@link certifyActiveStoreDir} so that EVERY store-wide scan in this repo
650
+ * — the certification, the `entwurf_peers` listing, the rival scan — gets its entries
651
+ * from one function that carries the ENTRY KIND. A binding that does its own
652
+ * `readdir()` gets names only, so the next thing it does is `readFileSync`, and rule 1
653
+ * ("a symlink is refused, never followed — its bytes live where this store has no
654
+ * ownership") quietly stops holding on that surface. That is not hypothetical: both
655
+ * `entwurf_peers` bindings hand-rolled exactly that readdir, and the #52 duplicate pass
656
+ * then let a symlink pointing at foreign bytes quarantine a healthy regular record.
657
+ * The rule has to be structural, not remembered at each call site — the same lesson
658
+ * `pi_settings_io` learned about the settings writers.
597
659
  */
598
- export function certifyActiveStoreDir(dir) {
660
+ export function readActiveStoreEntries(dir) {
599
661
  const resolved = path.resolve(expandTilde(dir));
600
662
  let dirents;
601
663
  try {
602
664
  dirents = fs.readdirSync(resolved, { withFileTypes: true });
603
665
  }
604
666
  catch (err) {
605
- if (err.code === "ENOENT") {
606
- return { dir: resolved, scanned: 0, records: [], defects: [] };
607
- }
667
+ if (err.code === "ENOENT")
668
+ return [];
608
669
  throw new MetaRecordError(`cannot read the meta-record store ${resolved}: ${err instanceof Error ? err.message : String(err)}. ` +
609
670
  "That is a failure to inspect the store, not an empty store — refusing to certify a store this " +
610
671
  "process cannot read.");
611
672
  }
612
- const entries = dirents
673
+ return dirents
613
674
  .sort((a, b) => a.name.localeCompare(b.name))
614
675
  .map((entry) => ({ filename: entry.name, regularFile: entry.isFile() }));
676
+ }
677
+ /** The errno {@link readStoreRecordFile} raises for an entry whose OPEN succeeded but
678
+ * whose file description is not a regular file — a directory, a fifo, a device. There is
679
+ * no operating-system errno for "you opened the wrong KIND of thing", so this is a
680
+ * synthesized one; every real errno is passed through untouched. */
681
+ export const NOT_REGULAR_ENTRY_CODE = "ENTWURF_ENOTREG";
682
+ const O_NOFOLLOW_FLAG = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
683
+ const O_NONBLOCK_FLAG = typeof fs.constants.O_NONBLOCK === "number" ? fs.constants.O_NONBLOCK : 0;
684
+ /**
685
+ * Read a record's bytes with the entry KIND decided on the very file description the
686
+ * read will use.
687
+ *
688
+ * `lstat`-then-`readFileSync(path)` classifies one entry and reads another. Between
689
+ * those two syscalls the final path component can be replaced by a symlink, and the read
690
+ * then follows it into bytes the store does not own — rule 1 laundered by a race rather
691
+ * than by a missing check (2026-07-26 cross-review). A path looked up twice cannot hold
692
+ * the rule; one open file description can:
693
+ *
694
+ * `O_NOFOLLOW` — a symlink at the FINAL component fails the OPEN with `ELOOP`, so
695
+ * foreign bytes are refused before one of them is read. Intermediate
696
+ * components resolve as they always have: the shape of the store
697
+ * DIRECTORY is a separate question, and this seam does not widen into
698
+ * it.
699
+ * `O_NONBLOCK` — opening a fifo `O_RDONLY` BLOCKS until some writer appears, so a
700
+ * named pipe dropped in the store would hang every scan on a host where
701
+ * nobody is writing. Classify-then-open never had that window; this flag
702
+ * is what buys it back. It is a no-op for regular files, and it does not
703
+ * decide anything — the fstat below is what refuses the fifo.
704
+ * `fstat(fd)` — the kind of THIS description, not of a name. A directory opens fine on
705
+ * Linux, so without it the refusal would arrive later as a stranger
706
+ * errno from the read.
707
+ *
708
+ * Errno is deliberately NOT flattened here. The callers separate raced-away (`ENOENT`)
709
+ * from unreadable (`EACCES`) from refused (`ELOOP`), and a wrapper that turned all three
710
+ * into one `MetaRecordError` would silently kill the rival scan's raced-away skip — the
711
+ * loudest possible regression, since every concurrent birth would then refuse dispatch.
712
+ * Shaping a message is the job of the caller that knows what the answer is FOR.
713
+ */
714
+ export function readStoreRecordFile(file) {
715
+ if (O_NOFOLLOW_FLAG === 0) {
716
+ throw new MetaRecordError(`cannot read meta-record ${path.basename(file)} safely: this platform exposes no O_NOFOLLOW, so a record ` +
717
+ "read cannot refuse a symlink swapped in at the final component. entwurf certifies Linux only.");
718
+ }
719
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | O_NOFOLLOW_FLAG | O_NONBLOCK_FLAG);
720
+ try {
721
+ if (!fs.fstatSync(fd).isFile()) {
722
+ const err = new Error(`meta-record ${path.basename(file)} is not a regular file (directory/fifo/device) — a record's bytes ` +
723
+ "must live in the store itself, so this entry is never read.");
724
+ err.code = NOT_REGULAR_ENTRY_CODE;
725
+ throw err;
726
+ }
727
+ return fs.readFileSync(fd, "utf8");
728
+ }
729
+ finally {
730
+ fs.closeSync(fd);
731
+ }
732
+ }
733
+ /** The reader every store-WIDE scan pairs with {@link readActiveStoreEntries}. The
734
+ * caller's entry kind decides the POLICY — whether a non-regular entry is a defect
735
+ * (certification), a diagnostic (listing) or a non-candidate (the rival scan) — because
736
+ * only the caller knows that. What the caller cannot know is whether the entry is still
737
+ * that kind at the instant of the read, so {@link readStoreRecordFile} re-decides it on
738
+ * the fd and refuses there. Snapshot kind for the verdict, fd kind for the bytes. */
739
+ export function makeStoreRecordReader(dir) {
740
+ const resolved = path.resolve(expandTilde(dir));
741
+ return (filename) => readStoreRecordFile(path.join(resolved, filename));
742
+ }
743
+ export function certifyActiveStoreDir(dir) {
744
+ const resolved = path.resolve(expandTilde(dir));
615
745
  return {
616
746
  dir: resolved,
617
- ...certifyActiveStore(entries, (filename) => fs.readFileSync(path.join(resolved, filename), "utf8")),
747
+ ...certifyActiveStore(readActiveStoreEntries(resolved), makeStoreRecordReader(resolved)),
618
748
  };
619
749
  }
620
750
  /**
@@ -639,6 +769,25 @@ export function activeStoreRefusal(cert, shown = 3) {
639
769
  .join("\n ");
640
770
  return (`${headline}\n ${head}` + (cert.defects.length > shown ? `\n … and ${cert.defects.length - shown} more` : ""));
641
771
  }
772
+ /**
773
+ * Group already-parsed identities by the id that must be unique across a store.
774
+ * Insertion-ordered (Map), so every consumer reports rivals in the order it read them.
775
+ *
776
+ * Keeps the listing's duplicate pass deterministic and separate from its quarantine
777
+ * loop. Certification and the addressable read apply the same `nativeSessionId` equality
778
+ * rule at different granularities, but do not call this listing-specific grouping helper.
779
+ */
780
+ function groupByNativeSessionId(identities) {
781
+ const byNative = new Map();
782
+ for (const identity of identities) {
783
+ const seen = byNative.get(identity.nativeSessionId);
784
+ if (seen)
785
+ seen.push(identity);
786
+ else
787
+ byNative.set(identity.nativeSessionId, [identity]);
788
+ }
789
+ return byNative;
790
+ }
642
791
  /**
643
792
  * Scan every meta-record in a store into identities + explicit read errors.
644
793
  * Pure over injected (entries, readRecord) so gates drive it without IO; the
@@ -653,15 +802,37 @@ export function activeStoreRefusal(cert, shown = 3) {
653
802
  * is impossible: the filename IS `<gardenId>.meta.json`, so the filesystem
654
803
  * already enforces uniqueness — only body/filename drift can split authority.
655
804
  *
805
+ * It takes {@link ActiveStoreEntry} — filename PLUS kind — for the same reason the
806
+ * certification does: rule 1 says a symlinked record is refused and NEVER FOLLOWED, and
807
+ * a scan handed bare names cannot obey that, because the only thing it can do next is
808
+ * read the path. Both `entwurf_peers` bindings used to hand-roll a name-only readdir,
809
+ * so this surface read foreign bytes through a symlink while the doctor refused the
810
+ * same entry — one store, two contracts again. A non-regular entry is now a diagnostic
811
+ * whose bytes are never touched.
812
+ *
813
+ * Store-wide UNIQUENESS of `nativeSessionId` is enforced here as well (#52) — free,
814
+ * because this function already reads the whole store. See the loop below for why both
815
+ * rivals become errors rather than one becoming a winner.
816
+ *
656
817
  * mode "collect" (default) returns partial results; "strict" throws if ANY
657
818
  * record was unreadable (doctor / gate callers wanting all-or-nothing).
658
819
  */
659
820
  export function listAllMetaIdentities(entries, readRecord, opts = {}) {
660
821
  const identities = [];
661
822
  const errors = [];
662
- for (const filename of entries) {
823
+ for (const entry of entries) {
824
+ const filename = entry.filename;
663
825
  if (!filename.endsWith(".meta.json"))
664
826
  continue;
827
+ if (!entry.regularFile) {
828
+ errors.push({
829
+ filename,
830
+ message: "not a regular file (symlink/directory/special) — a record's bytes must live in the store " +
831
+ "itself, so this entry is never followed and cannot name a citizen. Inspect and remove it by " +
832
+ `hand, or archive the generation with ${FRESH_CUT_PRESCRIPTION}.`,
833
+ });
834
+ continue;
835
+ }
665
836
  let identity;
666
837
  try {
667
838
  identity = parseMetaIdentity(readRecord(filename));
@@ -682,6 +853,34 @@ export function listAllMetaIdentities(entries, readRecord, opts = {}) {
682
853
  }
683
854
  identities.push(identity);
684
855
  }
856
+ // Rule 4 of {@link certifyActiveStore}, enforced HERE too (#52). This scan already
857
+ // holds every record in its hand, so uniqueness costs NOTHING to check — and a
858
+ // facts surface that reports two records claiming one `nativeSessionId` as two
859
+ // healthy citizens is describing a store the doctor calls uncertifiable as clean.
860
+ // That is the sharpest form of the gap: not a missing guard, a WRONG FACT.
861
+ //
862
+ // Both rivals leave `identities` and become errors, never one winner: the store
863
+ // genuinely cannot say which record owns that session, so picking either would
864
+ // mint the authority the certification refuses to mint. Everyone else keeps
865
+ // listing — the same rule an unreadable record follows, because one broken pair
866
+ // must not blind `entwurf_peers` (the 0.10 lesson).
867
+ for (const [nativeSessionId, holders] of groupByNativeSessionId(identities)) {
868
+ if (holders.length < 2)
869
+ continue;
870
+ const files = holders.map((h) => metaRecordFilename(h));
871
+ for (const holder of holders) {
872
+ const index = identities.indexOf(holder);
873
+ if (index >= 0)
874
+ identities.splice(index, 1);
875
+ errors.push({
876
+ filename: metaRecordFilename(holder),
877
+ message: `duplicate nativeSessionId ${JSON.stringify(nativeSessionId)} — also claimed by ` +
878
+ `${files.filter((f) => f !== metaRecordFilename(holder)).join(", ")}. The native→garden mapping ` +
879
+ `must be unique; this store cannot say which record owns that session, so NEITHER is listed as ` +
880
+ `a citizen. Archive the generation with ${FRESH_CUT_PRESCRIPTION}.`,
881
+ });
882
+ }
883
+ }
685
884
  if (opts.mode === "strict" && errors.length > 0) {
686
885
  throw new MetaRecordError(`listAllMetaIdentities(strict): ${errors.length} unreadable meta-record(s): ${errors
687
886
  .map((e) => `${e.filename} (${e.message})`)
@@ -912,6 +1111,80 @@ export function startKeyScheme(key) {
912
1111
  return "ps";
913
1112
  return null;
914
1113
  }
1114
+ /**
1115
+ * CAN this pid own a native session at all? — the question one layer ABOVE
1116
+ * {@link classifyMarkerOwner}, and the one no marker consumer used to ask.
1117
+ *
1118
+ * `classifyMarkerOwner` answers "is the process this marker NAMES still the one
1119
+ * running", and for pid 1 the honest answer is YES: init is up for as long as the
1120
+ * host is, and its start-key does not change while it runs. So an `ownerPid: 1`
1121
+ * marker classifies `live` for the whole boot, and — this is the operational
1122
+ * point — THE ACTION THE REFUSAL PRESCRIBES CANNOT CHANGE THAT. Quiescing every
1123
+ * session leaves init running under the same start-key, so the operator does the
1124
+ * one thing they were told to do and the cut refuses again. (Deleting the marker
1125
+ * removes the CLAIM; it never refutes the verdict — which is exactly why the
1126
+ * affected host's only way out was a hand `rm`.) A reboot is not a dependable
1127
+ * remedy either: pid 1's key is `linux:<starttime in ticks since boot>` and init
1128
+ * starts within a few ticks, so the recorded value can simply come up again —
1129
+ * there is no contract in either direction, and this is not evidence that it must.
1130
+ * What was MEASURED (#53 A, second Linux host, 2026-07-25) is that the cut stayed
1131
+ * blocked until the file was deleted by hand, while `0.12.8` names that same cut
1132
+ * as the one repair for a pre-v3 store: the documented upgrade path had no in-band
1133
+ * exit. The pure rule is not wrong there; the marker's CLAIM is.
1134
+ *
1135
+ * WHERE SUCH A MARKER COMES FROM — stated at the size of the evidence. After this
1136
+ * fix no writer in THIS tree can mint one, so on a current install it is legacy or
1137
+ * corrupt residue. Legacy has more than one source: the retired shell-form Claude
1138
+ * hook (wrapper shell exits first → hook REPARENTED to init → reads `ppid = 1`),
1139
+ * and the agy imprint, which asked only `> 0` until #53 A and could mint the same
1140
+ * shape through the same reparenting. Corrupt is a real class too — this predicate
1141
+ * also refutes non-integer and unsafe-integer pids, which no writer here has ever
1142
+ * produced, so a foreign or damaged marker is the only way they appear. The ONE
1143
+ * file actually observed was a shell-form Claude hook reparented to init. None of
1144
+ * these is a zombie: a zombie is reaped, its pid is freed, the start-key stops
1145
+ * matching, and the marker resolves itself.
1146
+ *
1147
+ * A pid ≤ 1 is refuted BY CONSTRUCTION, which is why this predicate is shared by
1148
+ * both writers and every reader instead of living at one call site: 0 and
1149
+ * negatives address process GROUPS rather than a process (the rule
1150
+ * {@link probePidExistence} already holds), and on the axis this repo certifies no
1151
+ * native session is owned by init.
1152
+ *
1153
+ * THAT LAST CLAUSE IS A POLICY, NOT A LAW OF PROCESSES. The certified axis is a
1154
+ * Linux desktop/workstation host, where init is the service manager and every
1155
+ * native harness is a descendant of a login session. A container that runs the
1156
+ * harness AS pid 1 is a real shape in the world, and there `ownerPid: 1` would name
1157
+ * a genuine owner — so such a host is simply UNSUPPORTED here and fails CLOSED: the
1158
+ * writers refuse to mint the marker, so the session keeps its meta-record but never
1159
+ * claims reply-addressability, which costs a capability instead of granting a false
1160
+ * identity. Do not read this predicate as "pid 1 can never own anything"; read it as
1161
+ * "this repo does not certify a host where it does". Widening the axis means new
1162
+ * evidence and a new contract, not a quiet loosening of the bound.
1163
+ *
1164
+ * Writers refuse to mint it, readers refuse to honor it, and the cut treats it as
1165
+ * clearable residue rather than an owner claim — a proof of INVALIDITY, which is
1166
+ * stronger than the proof of death the cut already acts on.
1167
+ */
1168
+ export function isPlausibleOwnerPid(pid) {
1169
+ return Number.isSafeInteger(pid) && pid > 1;
1170
+ }
1171
+ /**
1172
+ * The write-side half of {@link isPlausibleOwnerPid}. A marker naming an
1173
+ * impossible owner is not a degraded marker, it is a lie that outlives every
1174
+ * process that could refute it — so minting one THROWS rather than warns, and no
1175
+ * future writer can reintroduce #53 A by forgetting the predicate at its own call
1176
+ * site (both current writers still ask it first, and fail closed in their own
1177
+ * words).
1178
+ */
1179
+ function requireOwnerPid(ownerPid, kind) {
1180
+ if (!isPlausibleOwnerPid(ownerPid)) {
1181
+ throw new Error(`refusing to write a ${kind} marker for owner pid ${ownerPid}: a pid <= 1 cannot own a native session ` +
1182
+ "(pid 1 is init — a reparented orphan is not an owner). Quiescing the sessions, which is what " +
1183
+ "meta-bridge-fresh-cut asks for when it refuses, would not refute such a marker, so it would keep " +
1184
+ "blocking the cut on this host (#53 A).");
1185
+ }
1186
+ return ownerPid;
1187
+ }
915
1188
  /**
916
1189
  * THE rule for "is the process this marker names still the one running?" — the one
917
1190
  * place a `dead` verdict may be produced, so no caller re-derives it from
@@ -983,15 +1256,18 @@ export function metaSenderMarkerPath(backend, ownerPid, sendersDir = defaultMeta
983
1256
  export function writeMetaSenderMarker(opts) {
984
1257
  const backend = requireBackend(opts.backend);
985
1258
  const gardenId = requireGardenId(opts.gardenId);
986
- const file = metaSenderMarkerPath(backend, opts.ownerPid, opts.sendersDir ?? defaultMetaSendersDir());
1259
+ // Refused BEFORE the path is built: an impossible owner must not even leave a
1260
+ // backend directory behind, let alone a marker keyed to its pid.
1261
+ const ownerPid = requireOwnerPid(opts.ownerPid, "sender");
1262
+ const file = metaSenderMarkerPath(backend, ownerPid, opts.sendersDir ?? defaultMetaSendersDir());
987
1263
  fs.mkdirSync(path.dirname(file), { recursive: true });
988
1264
  const marker = {
989
1265
  backend,
990
1266
  gardenId,
991
1267
  nativeSessionId: requireNonEmptyString(opts.nativeSessionId, "nativeSessionId"),
992
1268
  cwd: requireNonEmptyString(opts.cwd, "cwd"),
993
- ownerPid: opts.ownerPid,
994
- ownerStartKey: processStartKey(opts.ownerPid),
1269
+ ownerPid,
1270
+ ownerStartKey: processStartKey(ownerPid),
995
1271
  updatedAt: isoNow(opts.now ?? new Date()),
996
1272
  };
997
1273
  const tmp = `${file}.${crypto.randomBytes(4).toString("hex")}.tmp`;
@@ -1023,13 +1299,19 @@ export function readMetaSenderMarker(opts) {
1023
1299
  ownerStartKey: requireNonEmptyString(raw.ownerStartKey, "ownerStartKey"),
1024
1300
  updatedAt: requireNonEmptyString(raw.updatedAt, "updatedAt"),
1025
1301
  };
1302
+ // Plausibility comes FIRST and is never opt-out: it is a property of the CLAIM,
1303
+ // not of the owner's current state, so `verifyOwner: false` (inspection) does not
1304
+ // reach past it either. An `ownerPid: 1` marker is a reparented orphan's residue
1305
+ // and would otherwise keep granting a dead citizen's sender identity for as long
1306
+ // as the host is up — the pid-reuse guard cannot catch it, because init IS still
1307
+ // the same process (#53 A). This also subsumes the old `Number.isInteger` check.
1308
+ if (!isPlausibleOwnerPid(marker.ownerPid))
1309
+ return null;
1026
1310
  // pid-reuse guard (unless explicitly disabled): the owner pid must STILL be
1027
1311
  // the same process that wrote the marker. A bare pid is reused; pid+startKey
1028
1312
  // is boot-unique, so a stale marker from a dead session fails here instead of
1029
1313
  // granting a wrong-identity send.
1030
1314
  if (opts.verifyOwner !== false) {
1031
- if (!Number.isInteger(marker.ownerPid))
1032
- return null;
1033
1315
  const liveKey = processStartKey(marker.ownerPid);
1034
1316
  if (!liveKey || liveKey !== marker.ownerStartKey)
1035
1317
  return null;
@@ -1062,14 +1344,15 @@ export function metaReceiverMarkerPath(gardenId, receiversDir = defaultMetaRecei
1062
1344
  export function writeMetaReceiverMarker(opts) {
1063
1345
  const gardenId = requireGardenId(opts.gardenId);
1064
1346
  const backend = requireBackend(opts.backend);
1347
+ const ownerPid = requireOwnerPid(opts.ownerPid, "receiver");
1065
1348
  const file = metaReceiverMarkerPath(gardenId, opts.receiversDir ?? defaultMetaReceiversDir());
1066
1349
  fs.mkdirSync(path.dirname(file), { recursive: true });
1067
1350
  const marker = {
1068
1351
  gardenId,
1069
1352
  backend,
1070
1353
  nativeSessionId: requireNonEmptyString(opts.nativeSessionId, "nativeSessionId"),
1071
- ownerPid: opts.ownerPid,
1072
- ownerStartKey: processStartKey(opts.ownerPid),
1354
+ ownerPid,
1355
+ ownerStartKey: processStartKey(ownerPid),
1073
1356
  ownerKind: requireNonEmptyString(opts.ownerKind ?? "claude-code-cli", "ownerKind"),
1074
1357
  armProvenance: requireArmProvenance(opts.armProvenance),
1075
1358
  updatedAt: isoNow(opts.now ?? new Date()),
@@ -1106,9 +1389,12 @@ export function readMetaReceiverMarker(opts) {
1106
1389
  armProvenance: requireArmProvenance(raw.armProvenance),
1107
1390
  updatedAt: requireNonEmptyString(raw.updatedAt, "updatedAt"),
1108
1391
  };
1392
+ // Same rule as the sender marker, and for the same reason one layer over: an
1393
+ // impossible owner is not a live one, so a refuted marker must never read back
1394
+ // as an ACTIVE RECEIVER and pull the mailbox rail into delivering to a void.
1395
+ if (!isPlausibleOwnerPid(marker.ownerPid))
1396
+ return null;
1109
1397
  if (opts.verifyOwner !== false) {
1110
- if (!Number.isInteger(marker.ownerPid))
1111
- return null;
1112
1398
  const liveKey = processStartKey(marker.ownerPid);
1113
1399
  if (!liveKey || liveKey !== marker.ownerStartKey)
1114
1400
  return null;
@@ -1202,11 +1488,32 @@ function inspectRecordEntry(file) {
1202
1488
  catch (err) {
1203
1489
  if (err.code === "ENOENT")
1204
1490
  return "absent";
1205
- throw new MetaRecordError(`cannot inspect meta-record ${path.basename(file)} under ${path.dirname(file)}: ` +
1206
- `${err instanceof Error ? err.message : String(err)}. That is an inspection failure, not an absent ` +
1207
- `citizen — refusing to report "no record" from a store this process cannot read.`);
1491
+ throw recordInspectionFailure(file, err);
1208
1492
  }
1209
1493
  }
1494
+ export function classifyRecordReadFailure(code) {
1495
+ if (code === "ENOENT")
1496
+ return "absent";
1497
+ if (code === "ELOOP" || code === "ENXIO" || code === NOT_REGULAR_ENTRY_CODE)
1498
+ return "irregular";
1499
+ return "unreadable";
1500
+ }
1501
+ /** The three refusals a targeted read can reach from EITHER layer — the settled
1502
+ * classification or a raced read. One text each, so a race and a quiet store say the same
1503
+ * sentence to the operator. */
1504
+ function recordInspectionFailure(file, err) {
1505
+ return new MetaRecordError(`cannot inspect meta-record ${path.basename(file)} under ${path.dirname(file)}: ` +
1506
+ `${err instanceof Error ? err.message : String(err)}. That is an inspection failure, not an absent ` +
1507
+ `citizen — refusing to report "no record" from a store this process cannot read.`);
1508
+ }
1509
+ function absentRecordRefusal(id, file) {
1510
+ return new MetaRecordError(`no meta-record for garden id "${id}" under ${path.dirname(file)} — not a garden citizen, cannot deliver.`);
1511
+ }
1512
+ function irregularRecordRefusal(id) {
1513
+ return new MetaRecordError(`meta-record ${id}.meta.json is not a regular file (symlink/directory/special) — a record's bytes must ` +
1514
+ `live in the store itself, so this entry is never followed and cannot address a citizen. ` +
1515
+ `Inspect and remove it by hand, or archive the generation with ${FRESH_CUT_PRESCRIPTION}.`);
1516
+ }
1210
1517
  /**
1211
1518
  * The identity read-by-gardenId. Read the file, body is SSOT, fail-fast on
1212
1519
  * body/filename gardenId drift; V3-only via parseMetaIdentity (an unreadable
@@ -1216,22 +1523,48 @@ function inspectRecordEntry(file) {
1216
1523
  export function readMetaIdentityByGardenId(gardenId, sessionsDir = defaultMetaSessionsDir()) {
1217
1524
  const id = requireGardenId(gardenId);
1218
1525
  const file = recordFileFor(sessionsDir, id);
1526
+ // TWO LAYERS, AND THEY ARE NOT THE SAME RULE TWICE.
1527
+ //
1528
+ // Layer 1 — POLICY, on a settled store. Rule 1 of {@link certifyActiveStore}, enforced
1529
+ // HERE too, because this is the one place a live dispatch reads a record. Certifying
1530
+ // the store on WRITE while this read followed the link made the contract true only
1531
+ // where nobody was being addressed: the doctor refused a symlinked entry that v2
1532
+ // dispatch, `entwurf_self` and the sender-marker trust all resolved happily, from bytes
1533
+ // the store does not own (2026-07-25 fresh-eyes review). It classifies WITHOUT OPENING,
1534
+ // which is the point — a socket, a device, a mode-000 directory each earn the
1535
+ // certification's own sentence instead of whatever errno an `open` would have tripped
1536
+ // over (ENXIO, EACCES), and nothing special is opened to find out what it is. Deleting
1537
+ // this layer in favour of the fd alone looked like removing a duplicate enforcement
1538
+ // point and was actually a regression on all three shapes (2026-07-26 cross-review,
1539
+ // round 3 — found by GPT, reproduced here).
1540
+ //
1541
+ // Layer 2 — the RACE, on the bytes actually returned. A verdict about a name stops
1542
+ // being true the moment something replaces what the name points at, so the read
1543
+ // re-decides on its own fd. Its errno verdicts collapse onto layer 1's sentences via
1544
+ // {@link classifyRecordReadFailure}, which is pure so that the branches stay provable:
1545
+ // with layer 1 in front of it, no settled store can reach them.
1219
1546
  const kind = inspectRecordEntry(file);
1220
- if (kind === "absent") {
1221
- throw new MetaRecordError(`no meta-record for garden id "${id}" under ${path.dirname(file)} — not a garden citizen, cannot deliver.`);
1222
- }
1223
- // Rule 1 of {@link certifyActiveStore}, enforced HERE too — the one place a live
1224
- // dispatch reads a record. Certifying the store on WRITE while this read followed the
1225
- // link made the contract true only where nobody was being addressed: the doctor
1226
- // refused a symlinked entry that v2 dispatch, `entwurf_self` and the sender-marker
1227
- // trust all resolved happily, from bytes the store does not own (2026-07-25
1228
- // fresh-eyes review). One contract means both directions hold it.
1229
- if (kind === "irregular") {
1230
- throw new MetaRecordError(`meta-record ${id}.meta.json is not a regular file (symlink/directory/special) — a record's bytes must ` +
1231
- `live in the store itself, so this entry is never followed and cannot address a citizen. ` +
1232
- `Inspect and remove it by hand, or archive the generation with ${FRESH_CUT_PRESCRIPTION}.`);
1233
- }
1234
- const identity = parseMetaIdentity(fs.readFileSync(file, "utf8"));
1547
+ if (kind === "absent")
1548
+ throw absentRecordRefusal(id, file);
1549
+ if (kind === "irregular")
1550
+ throw irregularRecordRefusal(id);
1551
+ let raw;
1552
+ try {
1553
+ raw = readStoreRecordFile(file);
1554
+ }
1555
+ catch (err) {
1556
+ if (err instanceof MetaRecordError)
1557
+ throw err;
1558
+ switch (classifyRecordReadFailure(err.code)) {
1559
+ case "absent":
1560
+ throw absentRecordRefusal(id, file);
1561
+ case "irregular":
1562
+ throw irregularRecordRefusal(id);
1563
+ default:
1564
+ throw recordInspectionFailure(file, err);
1565
+ }
1566
+ }
1567
+ const identity = parseMetaIdentity(raw);
1235
1568
  if (identity.gardenId !== id) {
1236
1569
  throw new MetaRecordError(`meta-record body/filename drift: ${id}.meta.json contains gardenId "${identity.gardenId}". ` +
1237
1570
  `The body is the authority; this file is corrupt and a garden-id lookup can never reach it. ` +
@@ -1258,6 +1591,119 @@ export function metaRecordExistsByGardenId(gardenId, sessionsDir = defaultMetaSe
1258
1591
  const id = requireGardenId(gardenId);
1259
1592
  return inspectRecordEntry(recordFileFor(sessionsDir, id)) !== "absent";
1260
1593
  }
1594
+ /**
1595
+ * Pure: which OTHER records in this store claim `identity`'s `nativeSessionId`.
1596
+ * Returns their filenames, sorted; empty means `identity` holds it alone.
1597
+ *
1598
+ * A RIVAL IS A RECORD THAT COULD BE ADDRESSED INSTEAD. That is the whole test, and it
1599
+ * is narrower than "a file whose bytes mention the same id" — three neighbour shapes
1600
+ * are therefore NOT candidates, and skipping them is a claim about reachability, not
1601
+ * leniency:
1602
+ *
1603
+ * - NON-REGULAR (symlink/dir/special): rule 1 of {@link certifyActiveStore} says such
1604
+ * an entry is refused and never followed, because its bytes live where this store
1605
+ * has no ownership. So it is skipped WITHOUT BEING READ — following it to see
1606
+ * whether it "counts" would break the rule in the act of enforcing it, and let a
1607
+ * planted symlink to foreign bytes quarantine a healthy citizen.
1608
+ * - BODY/FILENAME DRIFT: a record whose body names a different garden id is
1609
+ * unreachable by garden-id lookup from either name, so it can never be dispatched
1610
+ * to and cannot compete for an address.
1611
+ * - UNPARSEABLE by the live schema: same reason — no read path can reach it.
1612
+ *
1613
+ * All three are real certification defects, and the certification and the listing both
1614
+ * say so. What they must not do is blind a healthy citizen, which is the 0.10 "corrupt
1615
+ * blocks registration forever" mistake wearing a new hat.
1616
+ *
1617
+ * A CANDIDATE WE COULD NOT READ IS A DIFFERENT ANSWER AND THROWS. A regular
1618
+ * `.meta.json` this process cannot read might be a genuine duplicate; skipping it would
1619
+ * report "holds it alone" from a scan that never asked, which is exactly the vacuous
1620
+ * pass the store-level readdir guard refuses one level up. ENOENT is the one exception
1621
+ * and the one this repo already recognises everywhere (`inspectRecordEntry`, the cut's
1622
+ * socket walk, `clearFiles`): a file that vanished between the readdir and the read is
1623
+ * not in the store, so it holds nothing.
1624
+ *
1625
+ * The identity's OWN file is excluded by filename, not by identity equality: the caller
1626
+ * has already proven body and filename agree, and a rival is by definition a different
1627
+ * file.
1628
+ */
1629
+ export function nativeSessionIdRivals(identity, entries, readRecord) {
1630
+ const own = metaRecordFilename(identity);
1631
+ const rivals = [];
1632
+ for (const entry of entries) {
1633
+ const filename = entry.filename;
1634
+ if (!filename.endsWith(".meta.json") || filename === own)
1635
+ continue;
1636
+ if (!entry.regularFile)
1637
+ continue; // never followed — see rule 1 above
1638
+ let raw;
1639
+ try {
1640
+ raw = readRecord(filename);
1641
+ }
1642
+ catch (err) {
1643
+ if (err.code === "ENOENT")
1644
+ continue; // raced away — holds nothing
1645
+ throw new MetaRecordError(`cannot read meta-record ${filename} while proving that ${own} holds nativeSessionId ` +
1646
+ `${JSON.stringify(identity.nativeSessionId)} alone: ` +
1647
+ `${err instanceof Error ? err.message : String(err)}. That record may be a duplicate, so this ` +
1648
+ `is an unanswered question, not a clean scan — refusing to dispatch at an address this process ` +
1649
+ `cannot certify.`);
1650
+ }
1651
+ let other;
1652
+ try {
1653
+ other = parseMetaIdentity(raw);
1654
+ }
1655
+ catch {
1656
+ continue; // unreachable by the live schema — not an addressable rival
1657
+ }
1658
+ if (metaRecordFilename(other) !== filename)
1659
+ continue; // drifted — unreachable by garden id
1660
+ if (other.nativeSessionId === identity.nativeSessionId)
1661
+ rivals.push(filename);
1662
+ }
1663
+ return rivals.sort();
1664
+ }
1665
+ /**
1666
+ * The read a DISPATCH does (#52) — the targeted read PLUS the store-wide half of the
1667
+ * contract that a targeted read cannot see on its own.
1668
+ *
1669
+ * WHY THIS IS A SECOND FUNCTION AND NOT A CHANGE TO {@link readMetaIdentityByGardenId}:
1670
+ * the cost is real and the README says so out loud — a call-relay does not re-scan the
1671
+ * whole store per message, and the mailbox poke, the sender-marker trust and
1672
+ * `entwurf_self` keep the per-entry half exactly as before. What separates the callers
1673
+ * is not how careful they are, it is what they DO with the answer: these two turn
1674
+ * `nativeSessionId` into an ADDRESS — a native-push injection into a live conversation,
1675
+ * a pi resume against a transcript — and each does it ONCE per dispatch, next to a
1676
+ * socket connect and a process spawn. One readdir there is nothing; the same readdir
1677
+ * per relayed message is the design the store deliberately does not have.
1678
+ *
1679
+ * The failure it prevents is not hypothetical corruption. `upsertMetaSession` certifies
1680
+ * and then writes, which is not a transaction: two concurrent births — two SessionStart
1681
+ * hooks, an `entwurf_register_native` racing an agy imprint — can both read one clean
1682
+ * snapshot and mint DIFFERENT garden ids for one native session. Nothing was ever
1683
+ * corrupted on such a host, and yet both ids would direct-inject the same conversation,
1684
+ * or resume one transcript twice under two per-garden-id locks.
1685
+ *
1686
+ * Fails LOUD, never soft: a duplicate is corruption of the address space, and QB1
1687
+ * reserves the soft `bad-target` answer for a record that is simply ABSENT.
1688
+ */
1689
+ export function readAddressableMetaIdentity(gardenId, sessionsDir = defaultMetaSessionsDir()) {
1690
+ const identity = readMetaIdentityByGardenId(gardenId, sessionsDir);
1691
+ const dir = path.resolve(expandTilde(sessionsDir));
1692
+ // Entries carry their KIND (readActiveStoreEntries), so the rival scan can refuse a
1693
+ // symlinked neighbour without reading it. A bare-name readdir here would have made
1694
+ // rule 1 unenforceable at exactly the surface that turns a record into an address.
1695
+ // A store that cannot be listed throws from there — answering "unique" from a scan
1696
+ // that never happened is the vacuous pass this whole check exists to refuse.
1697
+ const rivals = nativeSessionIdRivals(identity, readActiveStoreEntries(dir), makeStoreRecordReader(dir));
1698
+ if (rivals.length > 0) {
1699
+ throw new MetaRecordError(`meta-record ${metaRecordFilename(identity)} shares nativeSessionId ` +
1700
+ `${JSON.stringify(identity.nativeSessionId)} with ${rivals.join(", ")} — the native→garden mapping ` +
1701
+ `must be unique, so this store cannot say which record owns that session and dispatching at either ` +
1702
+ `garden id would reach the same native session twice. Archive the generation with ` +
1703
+ `${FRESH_CUT_PRESCRIPTION}.`);
1704
+ }
1705
+ return identity;
1706
+ }
1261
1707
  /**
1262
1708
  * Deliver a message body to a garden citizen's mailbox: validate the record
1263
1709
  * exists, write the `.msg` body FIRST, stamp `lastEnqueuedAt`, then poke