amalgm 0.1.264 → 0.1.266

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.264",
3
+ "version": "0.1.266",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -94,7 +94,10 @@ never merges, never decides truth — it emits facts for the layers above.
94
94
  (catch-up for the window we were dead), watcher death (rebuild the
95
95
  doorbell, reconcile the gap once), and whenever a consumer asks — a sync
96
96
  session opening, a document opening, an app regaining focus are events
97
- too, and they belong to the layer above.
97
+ too, and they belong to the layer above. A freshly registered repo is the
98
+ one adapter-defined birth exception: its Card + Checkpoint is already the
99
+ complete first observation, so it arms Watch without scanning every child
100
+ again. Restart still performs the ordinary conservative catch-up scan.
98
101
  4. **Echo suppression is content, not bookkeeping.** A writer records its own
99
102
  write (`noteWrite`); a scan that finds the recorded hash finds nothing.
100
103
  5. **Identity has two axes, never mixed — and KIND decides which axis
@@ -1229,6 +1229,22 @@ function createObserver(options) {
1229
1229
  }
1230
1230
  scanFilesRoot(root, { allowRootRepository: true });
1231
1231
  },
1232
+ /**
1233
+ * Structural registration already knows its nested Git boundaries from
1234
+ * the same state parcel that names them. Refresh only that in-memory
1235
+ * forwarding map after it installs those observer roots; this is not a
1236
+ * filesystem census and it neither reads nor records worktree files.
1237
+ */
1238
+ refreshRepoTerritories(rootId) {
1239
+ const root = store.listRoots().find((row) => row.rootId === rootId);
1240
+ if (!root || root.kind !== 'repo') {
1241
+ throw new Error(`refreshRepoTerritories requires a live repo root: ${rootId}`);
1242
+ }
1243
+ const state = live.get(root.rootId);
1244
+ if (!state) return;
1245
+ state.territories = new Set(childRepoRoots(root)
1246
+ .map((child) => child.path.slice(root.path.length + 1)));
1247
+ },
1232
1248
  scanNow,
1233
1249
 
1234
1250
  // The referee (verify.js): re-hash every enrolled byte, report every
@@ -1313,7 +1329,7 @@ function createObserver(options) {
1313
1329
  return statusOf(root);
1314
1330
  },
1315
1331
 
1316
- start(rootId = null) {
1332
+ start(rootId = null, { census = true } = {}) {
1317
1333
  const firstStart = !started;
1318
1334
  if (firstStart) started = true;
1319
1335
  if (!firstStart && rootId === null) return;
@@ -1328,10 +1344,19 @@ function createObserver(options) {
1328
1344
  const state = attach(root, { watch: true });
1329
1345
  state.held = false;
1330
1346
  }
1331
- try {
1332
- for (const root of roots) scanNow(root.rootId);
1333
- } catch (error) {
1334
- console.warn('[Observer] initial scan failed:', error?.message || error);
1347
+ // A normal root needs its first observed census before its structural
1348
+ // registration can become evidence. A freshly registered Git root is
1349
+ // different: its already-committed Card + Checkpoint is that complete
1350
+ // initial observation. Re-walking its worktree here would duplicate
1351
+ // the adapter's state (and can block the runtime for a large repo).
1352
+ // `census: false` is therefore only the repo adapter's birth path;
1353
+ // later recoveries and runtime start retain the conservative census.
1354
+ if (census) {
1355
+ try {
1356
+ for (const root of roots) scanNow(root.rootId);
1357
+ } catch (error) {
1358
+ console.warn('[Observer] initial scan failed:', error?.message || error);
1359
+ }
1335
1360
  }
1336
1361
  },
1337
1362
 
@@ -67,7 +67,7 @@ const { capture } = require('../repocard');
67
67
  const { refusal } = require('./refusal');
68
68
  const { createRepoFollowers } = require('./repo-followers');
69
69
  const { createRepoStates } = require('./repo-states');
70
- const { registerTree } = require('./tree');
70
+ const { collectWorktreeIdentityAsync, registerTree, transportIdentity } = require('./tree');
71
71
  const { gitDirTruth } = require('../observer/scan');
72
72
 
73
73
  // Events that can change what the recorded edges reach: a new or
@@ -88,6 +88,10 @@ function createRegistration(options) {
88
88
 
89
89
  const opened = openIdentity();
90
90
  const captureRepo = opened.captureRepo || ((repoPath) => capture(repoPath, { withBundle: false }));
91
+ const captureRepoAsync = opened.captureRepoAsync ?? null;
92
+ if (captureRepoAsync !== null && typeof captureRepoAsync !== 'function') {
93
+ throw new Error('openIdentity.captureRepoAsync must be a function when supplied');
94
+ }
91
95
  const repoStateAt = opened.repoStateAt ?? null;
92
96
  if (repoStateAt !== null && typeof repoStateAt !== 'function') {
93
97
  throw new Error('openIdentity.repoStateAt must be a function when supplied');
@@ -111,6 +115,7 @@ function createRegistration(options) {
111
115
  registry,
112
116
  repoStates,
113
117
  prepareRepo: opened.prepareRepo ?? null,
118
+ captureRepoAsync,
114
119
  recordForRoot: repoRecordForRoot,
115
120
  onSettled: reconcileSettledRepo,
116
121
  });
@@ -167,13 +172,14 @@ function createRegistration(options) {
167
172
  for (const { scopeId } of memory.allBindings()) {
168
173
  if (!liveIds.has(scopeId)) memory.dropScope(scopeId);
169
174
  }
170
- for (const [rootId] of [...lenses]) {
171
- const row = observer.listRoots().find((member) => member.rootId === rootId);
172
- if (!row) {
173
- lenses.delete(rootId);
174
- continue;
175
- }
176
- const record = rootRecordOf(rootId);
175
+ // Retirement is decided from durable witness bindings, not from the
176
+ // in-memory lens map. A fresh runtime has no lenses yet, but it may have
177
+ // restarted halfway through retiring an adopted tree; letting that old
178
+ // observer root survive would make its next start look like a second
179
+ // tree. The binding is the evidence that survives the crash, so it is
180
+ // the only honest starting point.
181
+ for (const row of observer.listRoots()) {
182
+ const record = rootRecordOf(row.rootId);
177
183
  const nestedRepoScope = row.kind === 'repo' && record?.type === 'repo.git'
178
184
  && observer.listRoots().some((other) => other.rootId !== row.rootId
179
185
  && row.path.startsWith(`${other.path}/`));
@@ -185,15 +191,18 @@ function createRegistration(options) {
185
191
  // sweep above collects. The reverse order once minted a duplicate
186
192
  // tree: an unbound living root looks like fresh ground.
187
193
  const family = observer.listRoots()
188
- .filter((member) => member.rootId === rootId || member.path.startsWith(`${row.path}/`));
194
+ .filter((member) => member.rootId === row.rootId || member.path.startsWith(`${row.path}/`));
189
195
  for (const member of family) {
190
- if (member.rootId !== rootId) observer.removeRoot(member.rootId);
196
+ if (member.rootId !== row.rootId) observer.removeRoot(member.rootId);
191
197
  }
192
- observer.removeRoot(rootId);
193
- memory.dropScope(rootId);
194
- lenses.delete(rootId);
198
+ observer.removeRoot(row.rootId);
199
+ memory.dropScope(row.rootId);
200
+ lenses.delete(row.rootId);
195
201
  }
196
202
  }
203
+ for (const [rootId] of [...lenses]) {
204
+ if (!observer.listRoots().some((row) => row.rootId === rootId)) lenses.delete(rootId);
205
+ }
197
206
  for (const root of treeRoots()) {
198
207
  if (lenses.has(root.rootId)) continue;
199
208
  const lens = createEvidence({ observer, registry, memory, rootId: root.rootId, classify });
@@ -438,8 +447,14 @@ function createRegistration(options) {
438
447
  * same territory), so the only missing fact is the repo's atomic
439
448
  * Card+Checkpoint-backed identity birth.
440
449
  */
441
- function registerRepositoryInsideCoveredGround(ground, { identityAt = null, suppressJournal = false } = {}) {
450
+ function registerRepositoryInsideCoveredGround(ground, {
451
+ identityAt = null,
452
+ suppressJournal = false,
453
+ preparedRepoAt = null,
454
+ watcherAlreadyHeld = false,
455
+ } = {}) {
442
456
  let root = observer.listRoots().find((candidate) => candidate.path === ground) || null;
457
+ const rootAlreadyExisted = root !== null;
443
458
  if (!root) root = observer.enrollRoot(ground, { scan: false, watch: false });
444
459
  if (root.kind !== 'repo') {
445
460
  throw new Error(`expected Git repository ground at ${ground}, found ${root.kind}`);
@@ -462,8 +477,12 @@ function createRegistration(options) {
462
477
  return { rootPath: ground, record };
463
478
  }
464
479
 
480
+ let watcherHeld = watcherAlreadyHeld;
465
481
  try {
466
- requireWatchBirth(root);
482
+ if (!watcherHeld) {
483
+ requireWatchBirth(root);
484
+ watcherHeld = true;
485
+ }
467
486
  assertWitnessed({ observer, registry, memory });
468
487
  const register = () => registry.transaction(() => {
469
488
  registerTree({
@@ -475,6 +494,7 @@ function createRegistration(options) {
475
494
  repoStates,
476
495
  repoStateAt,
477
496
  identityAt,
497
+ preparedRepoAt,
478
498
  });
479
499
  assertWatchedThroughCommit(root.path);
480
500
  });
@@ -483,23 +503,225 @@ function createRegistration(options) {
483
503
  } catch (error) {
484
504
  // A root that existed only because the outer watcher discovered its
485
505
  // territory must remain coverage after a refused explicit admission;
486
- // do not remove it (the parent still owns the OS watcher). A root this
487
- // call itself enrolled has no durable identity and can safely retire.
506
+ // do not remove it (the parent still owns the OS watcher). Release a
507
+ // held doorbell before returning the refusal; otherwise a failed
508
+ // registration would silently turn a healthy observer territory into
509
+ // a permanently mute one. A root this call itself enrolled has no
510
+ // durable identity and can safely retire in the outer caller.
511
+ if (watcherHeld && rootAlreadyExisted) observer.start(root.rootId);
488
512
  throw error;
489
513
  }
490
514
 
491
- const startWatch = () => {
492
- observer.start(root.rootId);
493
- ensureTrees();
494
- reconcileInitialRepoWorktrees(root.path);
495
- flushIfDirty();
496
- requireLiveWatchers(root.path);
497
- };
515
+ const startWatch = () => startRepositoryFromBaseline(root);
498
516
  if (suppressJournal) registry.withoutJournal(startWatch);
499
517
  else startWatch();
500
518
  return { rootPath: root.path, record: rootRecordOf(root.rootId) };
501
519
  }
502
520
 
521
+ /**
522
+ * A repo's initial Card + Checkpoint is not preliminary data awaiting a
523
+ * generic filesystem census: it *is* the first complete state. Start its
524
+ * follower while the watcher is held, then release the watcher without
525
+ * scanning every worktree child a second time. The next raw ding reaches
526
+ * the follower and exactly one settled state authorizes the ordinary child
527
+ * reconciliation. This keeps Register synchronous without duplicating
528
+ * either its cargo capture or its initial Detect pass.
529
+ */
530
+ function startRepositoryFromBaseline(root) {
531
+ // Structural registration already knows every nested repo boundary from
532
+ // the same assignment that packed its state. Install those territories
533
+ // directly from that identity graph; discovering them again by walking
534
+ // files would reintroduce the duplicate initial census this adapter
535
+ // exists to avoid.
536
+ const territories = [];
537
+ for (const record of registry.activeByType('repo.git')) {
538
+ const repoPath = addressOf(record.uuid);
539
+ if (repoPath === null || (repoPath !== root.path && !repoPath.startsWith(`${root.path}/`))) continue;
540
+ let territory = observer.listRoots().find((candidate) => candidate.path === repoPath) ?? null;
541
+ if (!territory) territory = observer.enrollRoot(repoPath, { scan: false, watch: false });
542
+ if (territory.kind !== 'repo') {
543
+ throw new Error(`registered repository ${repoPath} is no longer Git ground before Watch began`);
544
+ }
545
+ memory.bind(territory.rootId, territory.rootId, record.uuid);
546
+ if (!lenses.has(territory.rootId)) {
547
+ // A repo baseline is already authoritative state. The lens exists so
548
+ // callers can address the tree, but does not reconcile absent
549
+ // observer child rows as if that state had never been captured.
550
+ lenses.set(territory.rootId, createEvidence({
551
+ observer, registry, memory, rootId: territory.rootId, classify,
552
+ }));
553
+ }
554
+ territories.push(territory);
555
+ }
556
+ territories.sort((left, right) => left.path.length - right.path.length);
557
+ // The outer raw watcher must forward a nested repo's first later ding
558
+ // to that nested follower. Its territory set is normally filled by a
559
+ // census; structural registration already supplied the same boundaries,
560
+ // so refresh the forwarding map without performing that census.
561
+ for (const territory of territories) observer.refreshRepoTerritories(territory.rootId);
562
+ repoFollowers.sync({ startOnBirth: false });
563
+ for (const territory of territories) observer.start(territory.rootId, { census: false });
564
+ requireLiveWatchers(root.path);
565
+
566
+ // Edge discovery is a distinct registration promise. It only reads link
567
+ // surfaces (repo roots answer that with a link census), never converts
568
+ // the just-packed worktree into generic per-file evidence. The newly
569
+ // reached roots then receive their usual identity/lens setup.
570
+ edgesDirty = true;
571
+ tryFollow();
572
+ ensureTrees();
573
+ repoFollowers.sync({ startOnBirth: false });
574
+ }
575
+
576
+ /**
577
+ * Plan all repository cargo under one selected root before its identity
578
+ * transaction begins. This work can be slow (a Card contains a Git bundle
579
+ * and its Checkpoint may contain real work), but it creates no registry
580
+ * fact. Its watcher is already held by the caller, so every ding remains
581
+ * available to Start's catch-up once the plan commits.
582
+ *
583
+ * Every nested repo has its own identity map and own state parcel. The
584
+ * outer map names the nested repo boundary; the nested parcel names its
585
+ * children. That is the same travel law used later by Send, merely used
586
+ * here for the first state.
587
+ */
588
+ async function prepareRepositoryPlans(root, identityAt) {
589
+ if (typeof captureRepoAsync !== 'function') return null;
590
+ const plans = new Map(); // absolute repo path -> { state, assignment }
591
+
592
+ const assertState = (repoPath, state) => {
593
+ if (state?.busy) {
594
+ throw new Error(`cannot register ${repoPath}: Git is unsettled (${state.busy})`);
595
+ }
596
+ if (!state?.stateId || !state.cardId || !state.checkpointId) {
597
+ throw new Error(`cannot register ${repoPath}: Git did not produce a Card + Checkpoint state`);
598
+ }
599
+ return state;
600
+ };
601
+
602
+ const planRepo = async (repoPath, rootRelativePath) => {
603
+ const assignment = await collectWorktreeIdentityAsync(repoPath, {
604
+ shouldEnroll: observer.shouldEnroll,
605
+ identityFor: (repoRelativePath) => identityAt?.(
606
+ rootRelativePath ? `${rootRelativePath}/${repoRelativePath}` : repoRelativePath,
607
+ )?.uuid || null,
608
+ });
609
+ const state = assertState(repoPath, await captureRepoAsync(repoPath, {
610
+ identity: transportIdentity(assignment),
611
+ }));
612
+ plans.set(repoPath, { state, assignment });
613
+ for (const [entryRelativePath, entry] of assignment) {
614
+ if (entry.type !== 'repo.git') continue;
615
+ await planRepo(
616
+ path.join(repoPath, entryRelativePath),
617
+ rootRelativePath ? `${rootRelativePath}/${entryRelativePath}` : entryRelativePath,
618
+ );
619
+ }
620
+ };
621
+
622
+ // A direct repo is its own initial state. An ordinary folder can still
623
+ // contain repo boundaries, and those must never fall back to a blocking
624
+ // main-thread capture just because the user selected their parent.
625
+ if (root.kind === 'repo') {
626
+ await planRepo(root.path, '');
627
+ return (repoPath) => plans.get(repoPath) || null;
628
+ }
629
+
630
+ let visited = 0;
631
+ const yieldIfNeeded = async () => {
632
+ visited += 1;
633
+ if (visited % 256 !== 0) return;
634
+ await new Promise((resolve) => setImmediate(resolve));
635
+ };
636
+ const discover = async (dirPath, relativePath) => {
637
+ let entries;
638
+ try {
639
+ entries = fs.readdirSync(dirPath, { withFileTypes: true })
640
+ .sort((a, b) => a.name.localeCompare(b.name));
641
+ } catch (error) {
642
+ if (error.code === 'EACCES' || error.code === 'EPERM') return;
643
+ throw error;
644
+ }
645
+ for (const entry of entries) {
646
+ await yieldIfNeeded();
647
+ const entryPath = path.join(dirPath, entry.name);
648
+ const entryRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
649
+ if (!observer.shouldEnroll(entryPath)) continue;
650
+ let stat;
651
+ try {
652
+ stat = fs.lstatSync(entryPath);
653
+ } catch {
654
+ continue;
655
+ }
656
+ if (!stat.isDirectory()) continue;
657
+ if (gitDirTruth(entryPath) === 'present') await planRepo(entryPath, entryRelativePath);
658
+ else await discover(entryPath, entryRelativePath);
659
+ }
660
+ };
661
+ await discover(root.path, '');
662
+ return (repoPath) => plans.get(repoPath) || null;
663
+ }
664
+
665
+ /**
666
+ * The public operation remains synchronous in its result: it returns only
667
+ * after the watcher, complete identity tree, state parcel, and journal all
668
+ * agree. Its preparation is asynchronous solely so large immutable cargo
669
+ * cannot freeze the HTTP runtime that owns the watcher proof.
670
+ */
671
+ async function addAsync(targetPath, options = {}) {
672
+ const identityAt = options.identityAt ?? null;
673
+ const suppressJournal = options.suppressJournal === true;
674
+ if (identityAt !== null && typeof identityAt !== 'function') {
675
+ throw new Error('addAsync identityAt must be a function when supplied');
676
+ }
677
+ if (typeof captureRepoAsync !== 'function') return add(targetPath, options);
678
+
679
+ let address;
680
+ let ground;
681
+ try {
682
+ const parent = fs.realpathSync(path.dirname(targetPath));
683
+ address = path.join(parent, path.basename(targetPath));
684
+ ground = fs.lstatSync(address).isSymbolicLink() ? address : fs.realpathSync(address);
685
+ } catch (error) {
686
+ if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') throw error;
687
+ throw refusal(`refusing to add ${targetPath}: there is no ground at that address`);
688
+ }
689
+
690
+ // Links intentionally retain link semantics even if their target is a
691
+ // repo. That is the same address law as `add`, not a hidden realpath.
692
+ const rootKind = fs.lstatSync(ground).isDirectory() && gitDirTruth(ground) === 'present'
693
+ ? 'repo'
694
+ : null;
695
+ if (rootKind !== 'repo') return add(targetPath, options);
696
+
697
+ let root = observer.listRoots().find((candidate) => candidate.path === ground) || null;
698
+ const rootAlreadyExisted = root !== null;
699
+ if (!root) root = observer.enrollRoot(ground, { scan: false, watch: false });
700
+ if (root.kind !== 'repo') return add(targetPath, options);
701
+ const bound = memory.boundTo(root.rootId);
702
+ if (bound !== undefined) return add(targetPath, options);
703
+
704
+ let watcherHeld = false;
705
+ try {
706
+ requireWatchBirth(root);
707
+ watcherHeld = true;
708
+ const preparedRepoAt = await prepareRepositoryPlans(root, identityAt);
709
+ return registerRepositoryInsideCoveredGround(ground, {
710
+ identityAt,
711
+ suppressJournal,
712
+ preparedRepoAt,
713
+ watcherAlreadyHeld: true,
714
+ });
715
+ } catch (error) {
716
+ // No identity was committed before preparation. A new root is only
717
+ // this failed request's temporary witness; an existing discovered root
718
+ // stays covered, but its held doorbell must be released.
719
+ if (!rootAlreadyExisted) observer.removeRoot(root.rootId);
720
+ else if (watcherHeld) observer.start(root.rootId);
721
+ throw error;
722
+ }
723
+ }
724
+
503
725
  /** Real ground -> the UUID of the entity that owns it, across trees. */
504
726
  function resolveGround(real) {
505
727
  const home = treeRootCovering(real);
@@ -707,6 +929,7 @@ function createRegistration(options) {
707
929
  // mutation. Keep the whole first Watch/Detect transition inside the
708
930
  // existing journal boundary used for the seeded structural bind.
709
931
  const startWatch = () => {
932
+ if (root.kind === 'repo') return startRepositoryFromBaseline(root);
710
933
  observer.start(root.rootId);
711
934
  ensureTrees();
712
935
  reconcileInitialRepoWorktrees(root.path);
@@ -746,6 +969,30 @@ function createRegistration(options) {
746
969
  return normalized.map(({ targetPath, identityAt }) => add(targetPath, { identityAt }));
747
970
  }
748
971
 
972
+ /** Same declared batch contract as `addMany`, while allowing a large Git
973
+ * cargo plan to run off the runtime event loop. Entries remain ordered and
974
+ * individually durable: this adds no second transaction model. */
975
+ async function addManyAsync(entries, { suppressJournal = false } = {}) {
976
+ if (!Array.isArray(entries) || entries.length === 0) {
977
+ throw new Error('addManyAsync requires one or more target paths');
978
+ }
979
+ const normalized = entries.map((entry) => {
980
+ if (typeof entry === 'string') return { targetPath: entry, identityAt: null };
981
+ if (!entry || typeof entry !== 'object' || typeof entry.targetPath !== 'string') {
982
+ throw new Error('addManyAsync entries must be paths or { targetPath, identityAt }');
983
+ }
984
+ if (entry.identityAt !== undefined && entry.identityAt !== null && typeof entry.identityAt !== 'function') {
985
+ throw new Error('addManyAsync identityAt must be a function when supplied');
986
+ }
987
+ return { targetPath: entry.targetPath, identityAt: entry.identityAt ?? null };
988
+ });
989
+ const results = [];
990
+ for (const { targetPath, identityAt } of normalized) {
991
+ results.push(await addAsync(targetPath, { identityAt, suppressJournal }));
992
+ }
993
+ return results;
994
+ }
995
+
749
996
  /** Reconcile one already-watched tree after the platform has materialized
750
997
  * a newly declared exact default inside it. This is deliberately not part
751
998
  * of ordinary re-registration: the caller must have already named the
@@ -783,6 +1030,13 @@ function createRegistration(options) {
783
1030
  return clean;
784
1031
  }
785
1032
 
1033
+ // Reopen every persisted observer before the first reconciliation. An
1034
+ // identity database survives a runtime restart; its Watch proof must too.
1035
+ // `start()` opens physical owners and restores covered roots through the
1036
+ // same coverage law as a new registration, then its census catches up on
1037
+ // dings that occurred while this process was absent.
1038
+ observer.start();
1039
+
786
1040
  // Startup is the same act as running — the one convergence pass,
787
1041
  // nothing less. The projection must not wait for an event: a
788
1042
  // rendering whose deletion was detected before a crash is equal
@@ -795,7 +1049,9 @@ function createRegistration(options) {
795
1049
  // observer bindings remain private to this registration composition.
796
1050
  registry,
797
1051
  add,
1052
+ addAsync,
798
1053
  addMany,
1054
+ addManyAsync,
799
1055
  reconcileDeclared,
800
1056
 
801
1057
  /**
@@ -25,12 +25,23 @@
25
25
 
26
26
  const { createFollower, createCaptureWorker } = require('../repocard');
27
27
 
28
- function createRepoFollowers({ observer, registry, repoStates, recordForRoot, onSettled, prepareRepo = null }) {
28
+ function createRepoFollowers({
29
+ observer,
30
+ registry,
31
+ repoStates,
32
+ recordForRoot,
33
+ onSettled,
34
+ prepareRepo = null,
35
+ captureRepoAsync = null,
36
+ }) {
29
37
  if (!observer || !registry || !repoStates || typeof recordForRoot !== 'function'
30
38
  || typeof onSettled !== 'function') {
31
39
  throw new Error('createRepoFollowers requires { observer, registry, repoStates, recordForRoot, onSettled }');
32
40
  }
33
41
 
42
+ // Production cloud-cargo captures are already complete in the service's
43
+ // dedicated worker (bundle -> transport -> immutable content head). The
44
+ // local/test fallback owns its lightweight capture worker here.
34
45
  const worker = createCaptureWorker();
35
46
  const followers = new Map(); // observer rootId -> { path, uuid, follower }
36
47
  let stopped = false;
@@ -65,7 +76,16 @@ function createRepoFollowers({ observer, registry, repoStates, recordForRoot, on
65
76
  * move, be adopted, or cease being a repository; in every case the old
66
77
  * follower is closed before a replacement can speak.
67
78
  */
68
- function sync() {
79
+ /**
80
+ * A freshly registered repo already has a Card + Checkpoint baseline and
81
+ * its watcher is still held. Its follower must exist before that hold is
82
+ * released, but it must not immediately spend a second pair of full Git
83
+ * captures just to rediscover the state it was born with. Any ding heard
84
+ * during the release rings this follower normally. A cold runtime restart
85
+ * keeps the default (`startOnBirth: true`) and independently proves the
86
+ * persisted baseline against disk.
87
+ */
88
+ function sync({ startOnBirth = true } = {}) {
69
89
  if (stopped) return;
70
90
  const roots = observer.listRoots().filter((root) => root.kind === 'repo');
71
91
  const live = new Set(roots.map((root) => root.rootId));
@@ -102,13 +122,17 @@ function createRepoFollowers({ observer, registry, repoStates, recordForRoot, on
102
122
  // cargo captures (withBundle) need content and read everything;
103
123
  // capture() enforces that side of the contract itself.
104
124
  const previous = withBundle ? null : repoStates.get(record.uuid);
105
- const captured = await worker.capture(repoPath, {
125
+ const options = {
106
126
  withBundle,
107
127
  suspects,
108
128
  previousUntracked: previous?.checkpoint?.untracked?.map(
109
129
  ({ path: rel, mode, sha256 }) => ({ path: rel, mode, sha256 }),
110
130
  ) ?? null,
111
- });
131
+ };
132
+ if (withBundle && captureRepoAsync) {
133
+ return captureRepoAsync(repoPath, { entityUuid: record.uuid, ...options });
134
+ }
135
+ const captured = await worker.capture(repoPath, options);
112
136
  return withBundle && !captured?.busy
113
137
  ? prepareRepo(captured, { entityUuid: record.uuid })
114
138
  : captured;
@@ -128,9 +152,10 @@ function createRepoFollowers({ observer, registry, repoStates, recordForRoot, on
128
152
  },
129
153
  });
130
154
  followers.set(root.rootId, { path: root.path, uuid: record.uuid, follower });
131
- // Birth captures the initial Card + Checkpoint. The follower owns its
132
- // retry policy; registration must not invent an alternate Git scanner.
133
- void follower.start();
155
+ // A restart catches up from disk; a fresh Register has already
156
+ // captured its exact baseline and will receive held doorbells when
157
+ // Watch releases, so it does not duplicate the expensive birth pair.
158
+ if (startOnBirth) void follower.start();
134
159
  }
135
160
  }
136
161
 
@@ -58,7 +58,7 @@ const { classifyFile } = require('./classify');
58
58
  const { createEntityCloud } = require('./entity-cloud');
59
59
  const { createEntityContent } = require('./entity-content');
60
60
  const { createRegistration } = require('./index');
61
- const { capture, apply: applyRepo } = require('../repocard');
61
+ const { capture, apply: applyRepo, createCaptureWorker } = require('../repocard');
62
62
  const { transportBytes, localState, unpack } = require('./repo-states');
63
63
  const { machineId, userId } = require('../workspace/identity');
64
64
 
@@ -99,6 +99,11 @@ function createRegistrationService({
99
99
  handle.pragma('synchronous = NORMAL');
100
100
  }
101
101
  const entityContent = createEntityContent({ dir });
102
+ // Git bundles and their encoded immutable transports can be hundreds of
103
+ // megabytes. Build and chunk them off the runtime thread so its local
104
+ // health endpoint and already-live watchers stay responsive throughout a
105
+ // synchronous Register operation.
106
+ const cargoCaptureWorker = createCaptureWorker();
102
107
  // createRegistration owns the schema migration. Give entity-cloud a stable
103
108
  // lookup facade now, then connect it to that one migrated state store once
104
109
  // the boundary exists; never open a second registry just to read Git state.
@@ -142,6 +147,27 @@ function createRegistrationService({
142
147
  return localState(captured, artifact.contentHash);
143
148
  }
144
149
 
150
+ async function captureRepoAsync(repoPath, {
151
+ identity = null,
152
+ entityUuid = null,
153
+ suspects = null,
154
+ previousUntracked = null,
155
+ } = {}) {
156
+ const transportIdentity = identity ?? (entityUuid ? worktreeIdentity(entityUuid) : null);
157
+ const captured = await cargoCaptureWorker.capture(repoPath, {
158
+ withBundle: true,
159
+ suspects,
160
+ previousUntracked,
161
+ cargo: { dir, identity: transportIdentity },
162
+ });
163
+ if (captured?.busy) return captured;
164
+ if (!captured?.stateId || !captured?.transportVersion) {
165
+ throw new Error(`repo cargo capture did not produce a state and immutable transport: ${repoPath}`);
166
+ }
167
+ repoTransportHeads.set(captured.stateId, captured.transportVersion);
168
+ return captured;
169
+ }
170
+
145
171
  // Entity-cloud needs only logical ancestry to choose a content plan. It
146
172
  // never owns a watcher or address; this facade is available after the
147
173
  // registration boundary is constructed below.
@@ -174,6 +200,7 @@ function createRegistrationService({
174
200
  sentinelPath: path.join(dir, 'identity.epoch'),
175
201
  journal: entityCloud.journal,
176
202
  captureRepo: (repoPath, options) => prepareRepo(capture(repoPath, { withBundle: true }), options),
203
+ captureRepoAsync,
177
204
  prepareRepo,
178
205
  repoStateAt: (uuid) => importedRepoStates.get(uuid) || null,
179
206
  }),
@@ -972,9 +999,9 @@ function createRegistrationService({
972
999
  // signed-out registration is allowed.
973
1000
  const cloudActive = entityCloud.replica()?.state === 'active';
974
1001
  if (!cloudActive || estimatedRegistrationRecords(targetPath) < ATOMIC_REGISTRATION_THRESHOLD) {
975
- return boundary.current.add(targetPath);
1002
+ return (await boundary.current.addManyAsync([targetPath]))[0];
976
1003
  }
977
- const result = boundary.current.registry.withoutJournal(() => boundary.current.add(targetPath));
1004
+ const result = (await boundary.current.addManyAsync([targetPath], { suppressJournal: true }))[0];
978
1005
  entityCloud.replaceSnapshot(boundary.current.registry.syncRecords());
979
1006
  return result;
980
1007
  },
@@ -988,13 +1015,13 @@ function createRegistrationService({
988
1015
  && targetPaths.some((targetPath) => (
989
1016
  estimatedRegistrationRecords(targetPath) >= ATOMIC_REGISTRATION_THRESHOLD
990
1017
  ));
991
- if (!needsAtomicReplacement) return boundary.current.addMany(targetPaths);
1018
+ if (!needsAtomicReplacement) return boundary.current.addManyAsync(targetPaths);
992
1019
  // The snapshot must describe whatever actually committed, even when a
993
1020
  // later path refuses (each path is durable alone): without journal
994
1021
  // rows, a committed-but-unsnapshotted registration would be local
995
1022
  // truth with zero cloud delivery intent — a ghost nothing repairs.
996
1023
  try {
997
- return boundary.current.registry.withoutJournal(() => boundary.current.addMany(targetPaths));
1024
+ return await boundary.current.addManyAsync(targetPaths, { suppressJournal: true });
998
1025
  } finally {
999
1026
  entityCloud.replaceSnapshot(boundary.current.registry.syncRecords());
1000
1027
  }
@@ -1049,6 +1076,7 @@ function createRegistrationService({
1049
1076
  closed = true;
1050
1077
  observer.stop(); // doorbells and their timers
1051
1078
  boundary.current.stop(); // and the pass they may already have scheduled
1079
+ void cargoCaptureWorker.close();
1052
1080
  evidenceDb.close();
1053
1081
  identityDb.close();
1054
1082
  },
@@ -23,7 +23,10 @@
23
23
  * although they have identical behavior today. A structural walk therefore
24
24
  * uses file.text as the content-free ordinary-file placeholder. The first
25
25
  * Detect pass immediately attests the actual bytes and corrects that
26
- * representation through the normal one-observation transition.
26
+ * representation through the first later settled repo reconciliation. The
27
+ * initial Card + Checkpoint is authoritative content state; repeating a
28
+ * generic per-file scan merely to refine this local physical label would
29
+ * duplicate that state and can freeze large registration.
27
30
  */
28
31
 
29
32
  const fs = require('fs');
@@ -91,6 +94,56 @@ function collectWorktreeIdentity(repoPath, { shouldEnroll, identityFor }) {
91
94
  return assignment;
92
95
  }
93
96
 
97
+ /**
98
+ * The asynchronous twin of the identity walk. It has exactly the same
99
+ * shape rules as `collectWorktreeIdentity`, but returns to Node between
100
+ * bounded batches. A large initial repo registration may take time to
101
+ * inspect, yet its held watcher and the runtime's health endpoint must stay
102
+ * alive while that inspection is in flight. This is planning only: no
103
+ * identity row exists until `registerTree` commits the finished plan.
104
+ */
105
+ async function collectWorktreeIdentityAsync(repoPath, { shouldEnroll, identityFor, yieldEvery = 256 }) {
106
+ const assignment = new Map(); // repo-relative path -> { uuid, type }
107
+ let visited = 0;
108
+ const yieldIfNeeded = async () => {
109
+ visited += 1;
110
+ if (visited % yieldEvery !== 0) return;
111
+ await new Promise((resolve) => setImmediate(resolve));
112
+ };
113
+ const walk = async (dirPath, relativePath) => {
114
+ let entries;
115
+ try {
116
+ entries = fs.readdirSync(dirPath, { withFileTypes: true })
117
+ .sort((a, b) => a.name.localeCompare(b.name));
118
+ } catch (error) {
119
+ if (error.code === 'EACCES' || error.code === 'EPERM') return;
120
+ throw error;
121
+ }
122
+ for (const entry of entries) {
123
+ await yieldIfNeeded();
124
+ const entryPath = path.join(dirPath, entry.name);
125
+ const entryRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
126
+ if (!shouldEnroll(entryPath)) continue;
127
+ let stat;
128
+ try {
129
+ stat = fs.lstatSync(entryPath);
130
+ } catch {
131
+ continue; // raced away between readdir and look; the create-walk decides
132
+ }
133
+ let type;
134
+ if (stat.isSymbolicLink()) type = 'link';
135
+ else if (stat.isDirectory()) type = isRepository(entryPath) ? 'repo.git' : 'folder';
136
+ else if (stat.isFile()) type = 'file.text';
137
+ else continue;
138
+ const uuid = identityFor?.(entryRelativePath) || crypto.randomUUID();
139
+ assignment.set(entryRelativePath, { uuid, type });
140
+ if (type === 'folder') await walk(entryPath, entryRelativePath);
141
+ }
142
+ };
143
+ await walk(repoPath, '');
144
+ return assignment;
145
+ }
146
+
94
147
  function transportIdentity(assignment) {
95
148
  return [...assignment].map(([entryPath, { uuid, type }]) => ({ path: entryPath, uuid, type }));
96
149
  }
@@ -100,7 +153,17 @@ function transportIdentity(assignment) {
100
153
  * directory ground. The caller supplies a root that is already known to the
101
154
  * observer but is not yet watched or scanned.
102
155
  */
103
- function registerTree({ root, registry, memory, shouldEnroll, captureRepo, repoStates, identityAt = null, repoStateAt = null }) {
156
+ function registerTree({
157
+ root,
158
+ registry,
159
+ memory,
160
+ shouldEnroll,
161
+ captureRepo,
162
+ repoStates,
163
+ identityAt = null,
164
+ repoStateAt = null,
165
+ preparedRepoAt = null,
166
+ }) {
104
167
  if (!root || !registry || !memory || typeof shouldEnroll !== 'function'
105
168
  || typeof captureRepo !== 'function' || !repoStates) {
106
169
  throw new Error('registerTree requires { root, registry, memory, shouldEnroll, captureRepo, repoStates }');
@@ -109,6 +172,9 @@ function registerTree({ root, registry, memory, shouldEnroll, captureRepo, repoS
109
172
  if (identityAt !== null && typeof identityAt !== 'function') {
110
173
  throw new Error('registerTree identityAt must be a function when supplied');
111
174
  }
175
+ if (preparedRepoAt !== null && typeof preparedRepoAt !== 'function') {
176
+ throw new Error('registerTree preparedRepoAt must be a function when supplied');
177
+ }
112
178
 
113
179
  function seeded(relativePath, { type, parentUUID, name, root: isRoot }) {
114
180
  const identity = identityAt?.(relativePath) || null;
@@ -151,6 +217,8 @@ function registerTree({ root, registry, memory, shouldEnroll, captureRepo, repoS
151
217
  const saved = repoStates.get(identity.uuid);
152
218
  if (saved?.stateId === identity.payloadVersion) return { state: saved, assignment: null };
153
219
  }
220
+ const prepared = preparedRepoAt?.(repoPath) || null;
221
+ if (prepared) return prepared;
154
222
  const assignment = collectWorktreeIdentity(repoPath, {
155
223
  shouldEnroll,
156
224
  identityFor: (repoRelativePath) => identityAt?.(
@@ -296,4 +364,4 @@ function registerDirectory(dirPath, parentUUID, {
296
364
  }
297
365
  }
298
366
 
299
- module.exports = { registerTree };
367
+ module.exports = { collectWorktreeIdentityAsync, registerTree, transportIdentity };
@@ -622,7 +622,20 @@ function createEvidence(options) {
622
622
  const parentUuid = ensureDir(path.dirname(territory) === '.' ? '' : path.dirname(territory), root);
623
623
  const name = path.basename(territory);
624
624
  const at = registry.activeAt(parentUuid, name);
625
- if (at?.type !== 'repo.git') continue;
625
+ if (at?.type === 'repo.git') continue;
626
+ const territoryRoot = observer.listRoots()
627
+ .find((row) => row.kind === 'repo' && row.path === path.join(groundPath, territory));
628
+ const arrivedRecord = arrivedTree(territoryRoot?.device, territoryRoot?.inode, REPO_TREE);
629
+ if (!arrivedRecord) continue;
630
+ // A Git territory is the repo-shaped form of the same adoption law
631
+ // directories use above. Its initial Card + Checkpoint already
632
+ // travels with the existing repo UUID, so the new observer root must
633
+ // adopt that identity — never mint or ignore a half-repository.
634
+ if (at) {
635
+ nowDivergent.set(territory, null); // two identity claims: decide neither
636
+ continue;
637
+ }
638
+ registry.move(arrivedRecord.uuid, { parentUUID: parentUuid, name });
626
639
  }
627
640
  // A lifted lens is the reverse transition on the same record.
628
641
  for (const { record, rel } of walkActive(root.uuid, '', [])) {
@@ -750,6 +750,13 @@ function createRegistry(options) {
750
750
  /** Full logical graph for cloud snapshots. Machine bindings and evidence
751
751
  * are intentionally absent. */
752
752
  syncRecords: () => store.allEntities().map(syncRecord),
753
+ /** Active records of one physical type. This is a query primitive for
754
+ * type adapters; it does not expose storage writes or weaken the single
755
+ * registry boundary. */
756
+ activeByType: (type) => {
757
+ if (!TYPES.has(type)) throw new Error(`unknown entity type ${JSON.stringify(type)}`);
758
+ return store.activeByType(type).map(record);
759
+ },
753
760
  /** Every registered tree's top record. */
754
761
  roots: () => store.activeRoots().map(record),
755
762
  /** Every active managed reference with the identity it targets. The
@@ -20,9 +20,31 @@ const { Worker, isMainThread, parentPort } = require('worker_threads');
20
20
 
21
21
  if (!isMainThread) {
22
22
  const { capture } = require('./capture');
23
+ const { createEntityContent } = require('../registration/entity-content');
24
+ const { transportBytes, localState } = require('../registration/repo-states');
25
+
26
+ /**
27
+ * A cloud-bound initial repo capture is deliberately completed here, not
28
+ * handed back as a giant Git bundle for the runtime thread to JSON-encode
29
+ * and chunk. The runtime's job is to remain the responsive authority for
30
+ * Watch + Register; this worker owns the expensive, immutable cargo build.
31
+ * It returns only the bundle-free state plus its content head.
32
+ */
33
+ function captureCargo(repoPath, options) {
34
+ const cargo = options?.cargo;
35
+ if (!cargo || typeof cargo.dir !== 'string') return capture(repoPath, options);
36
+ const snap = capture(repoPath, { ...options, withBundle: true });
37
+ if (snap?.busy) return snap;
38
+ const content = createEntityContent({ dir: cargo.dir });
39
+ const artifact = content.captureBytes({
40
+ bytes: transportBytes(snap, Array.isArray(cargo.identity) ? cargo.identity : null),
41
+ });
42
+ return localState(snap, artifact.contentHash);
43
+ }
44
+
23
45
  parentPort.on('message', ({ id, repoPath, options }) => {
24
46
  try {
25
- parentPort.postMessage({ id, snap: capture(repoPath, options) });
47
+ parentPort.postMessage({ id, snap: captureCargo(repoPath, options) });
26
48
  } catch (error) {
27
49
  parentPort.postMessage({ id, error: error?.message || String(error) });
28
50
  }
@@ -608,13 +608,17 @@ test('adding a repository registers its allowed tree and saves its initial Repoc
608
608
  assert.ok(initialState?.stateId, 'registration saved the initial Card + Checkpoint');
609
609
  assert.ok(initialState.checkpoint.untracked.some((file) => file.path === 'readme.md'),
610
610
  'the persisted baseline contains the actual working checkpoint, not only its hash');
611
+ const observerRoot = service.observer.listRoots().find((root) => root.path === repo);
612
+ assert.deepEqual(service.observer.filesForRoot(observerRoot.rootId), [],
613
+ 'repo birth does not perform a second per-file census after its Card + Checkpoint is saved');
611
614
  assert.equal(tree.root().version, registered.version,
612
615
  'add returned only after the repo entity pointed at its saved baseline');
613
616
  const readme = tree.registry.activeAt(registered.uuid, 'readme.md');
614
617
  const src = tree.registry.activeAt(registered.uuid, 'src');
615
618
  assert.equal(readme.type, 'file.text');
616
619
  assert.equal(src.type, 'folder');
617
- assert.equal(tree.registry.activeAt(src.uuid, 'blob.bin').type, 'file.binary');
620
+ assert.equal(tree.registry.activeAt(src.uuid, 'blob.bin').type, 'file.text',
621
+ 'repo birth records structural leaf shape; the Card + Checkpoint owns its actual bytes without a second read');
618
622
  assert.equal(tree.registry.activeAt(registered.uuid, 'node_modules'), null,
619
623
  'the same enrollment policy excludes dependency trees inside repos');
620
624
  const initial = tree.root();
@@ -657,6 +661,28 @@ test('adding a repository registers its allowed tree and saves its initial Repoc
657
661
  assert.equal(tree.root().uuid, after.uuid);
658
662
  });
659
663
 
664
+ test('a production repository register builds its immutable Git cargo off the runtime thread', async () => {
665
+ const service = makeService('repo-cargo-worker');
666
+ const repo = makeGround('repo-cargo-worker');
667
+ execFileSync('git', ['init', '-q'], { cwd: repo });
668
+ fs.writeFileSync(path.join(repo, 'history.md'), '# committed history\n');
669
+ execFileSync('git', ['add', '.'], { cwd: repo });
670
+ execFileSync('git', [
671
+ '-c', 'user.name=Amalgm Test',
672
+ '-c', 'user.email=amalgm-test@example.invalid',
673
+ 'commit', '-qm', 'initial history',
674
+ ], { cwd: repo });
675
+
676
+ const { record } = await service.register(repo);
677
+ const state = service.registration.repoState(record.uuid);
678
+ assert.equal(record.type, 'repo.git');
679
+ assert.ok(state?.transportVersion, 'the saved state names one immutable transport object');
680
+ assert.equal(state.card.bundle, null,
681
+ 'identity.db holds only bundle-free state; the worker left history in immutable content');
682
+ assert.ok(service.entityContent.bytes({ contentHash: state.transportVersion }).length > 0,
683
+ 'the worker finished the transport before Register committed the repo head');
684
+ });
685
+
660
686
  test('an edge-reached repository gets the same complete registration as a directly added repo', async () => {
661
687
  const service = makeService('edge-repo-card');
662
688
  const holder = makeGround('edge-repo-holder');
@@ -959,6 +985,25 @@ test('identity survives a real close-and-reopen over the same directory', async
959
985
  assert.equal(after, before, 'the tree keeps its permanent UUID across processes');
960
986
  });
961
987
 
988
+ test('a restarted runtime reopens the persisted watcher before it reports the tree healthy', async () => {
989
+ const dir = path.join(tempRoot, 'state-reopen-watch');
990
+ const ground = makeGround('reopen-watch');
991
+ fs.writeFileSync(path.join(ground, 'notes.md'), 'persisted ground\n');
992
+
993
+ const first = createRegistrationService({ dir });
994
+ const registered = await first.register(ground);
995
+ first.close();
996
+
997
+ const reopened = createRegistrationService({ dir });
998
+ services.push(reopened);
999
+ const root = reopened.observer.status().find((entry) => entry.path === ground);
1000
+ assert.ok(root, 'the original observer root survived the restart');
1001
+ assert.equal(root.watching, true, 'the restarted runtime has a live Watch rail');
1002
+ assert.equal(root.degraded, false);
1003
+ assert.equal(reopened.registration.tree(ground).root().uuid, registered.record.uuid,
1004
+ 'the watcher reopened over the same permanent identity');
1005
+ });
1006
+
962
1007
  // --- the policies are the real ones ---------------------------------------
963
1008
 
964
1009
  test("amalgm's enrollment policy is in force, and refuses loudly", async () => {