amalgm 0.1.264 → 0.1.265

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.265",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -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
  });
@@ -438,8 +443,14 @@ function createRegistration(options) {
438
443
  * same territory), so the only missing fact is the repo's atomic
439
444
  * Card+Checkpoint-backed identity birth.
440
445
  */
441
- function registerRepositoryInsideCoveredGround(ground, { identityAt = null, suppressJournal = false } = {}) {
446
+ function registerRepositoryInsideCoveredGround(ground, {
447
+ identityAt = null,
448
+ suppressJournal = false,
449
+ preparedRepoAt = null,
450
+ watcherAlreadyHeld = false,
451
+ } = {}) {
442
452
  let root = observer.listRoots().find((candidate) => candidate.path === ground) || null;
453
+ const rootAlreadyExisted = root !== null;
443
454
  if (!root) root = observer.enrollRoot(ground, { scan: false, watch: false });
444
455
  if (root.kind !== 'repo') {
445
456
  throw new Error(`expected Git repository ground at ${ground}, found ${root.kind}`);
@@ -462,8 +473,12 @@ function createRegistration(options) {
462
473
  return { rootPath: ground, record };
463
474
  }
464
475
 
476
+ let watcherHeld = watcherAlreadyHeld;
465
477
  try {
466
- requireWatchBirth(root);
478
+ if (!watcherHeld) {
479
+ requireWatchBirth(root);
480
+ watcherHeld = true;
481
+ }
467
482
  assertWitnessed({ observer, registry, memory });
468
483
  const register = () => registry.transaction(() => {
469
484
  registerTree({
@@ -475,6 +490,7 @@ function createRegistration(options) {
475
490
  repoStates,
476
491
  repoStateAt,
477
492
  identityAt,
493
+ preparedRepoAt,
478
494
  });
479
495
  assertWatchedThroughCommit(root.path);
480
496
  });
@@ -483,8 +499,12 @@ function createRegistration(options) {
483
499
  } catch (error) {
484
500
  // A root that existed only because the outer watcher discovered its
485
501
  // 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.
502
+ // do not remove it (the parent still owns the OS watcher). Release a
503
+ // held doorbell before returning the refusal; otherwise a failed
504
+ // registration would silently turn a healthy observer territory into
505
+ // a permanently mute one. A root this call itself enrolled has no
506
+ // durable identity and can safely retire in the outer caller.
507
+ if (watcherHeld && rootAlreadyExisted) observer.start(root.rootId);
488
508
  throw error;
489
509
  }
490
510
 
@@ -500,6 +520,155 @@ function createRegistration(options) {
500
520
  return { rootPath: root.path, record: rootRecordOf(root.rootId) };
501
521
  }
502
522
 
523
+ /**
524
+ * Plan all repository cargo under one selected root before its identity
525
+ * transaction begins. This work can be slow (a Card contains a Git bundle
526
+ * and its Checkpoint may contain real work), but it creates no registry
527
+ * fact. Its watcher is already held by the caller, so every ding remains
528
+ * available to Start's catch-up once the plan commits.
529
+ *
530
+ * Every nested repo has its own identity map and own state parcel. The
531
+ * outer map names the nested repo boundary; the nested parcel names its
532
+ * children. That is the same travel law used later by Send, merely used
533
+ * here for the first state.
534
+ */
535
+ async function prepareRepositoryPlans(root, identityAt) {
536
+ if (typeof captureRepoAsync !== 'function') return null;
537
+ const plans = new Map(); // absolute repo path -> { state, assignment }
538
+
539
+ const assertState = (repoPath, state) => {
540
+ if (state?.busy) {
541
+ throw new Error(`cannot register ${repoPath}: Git is unsettled (${state.busy})`);
542
+ }
543
+ if (!state?.stateId || !state.cardId || !state.checkpointId) {
544
+ throw new Error(`cannot register ${repoPath}: Git did not produce a Card + Checkpoint state`);
545
+ }
546
+ return state;
547
+ };
548
+
549
+ const planRepo = async (repoPath, rootRelativePath) => {
550
+ const assignment = await collectWorktreeIdentityAsync(repoPath, {
551
+ shouldEnroll: observer.shouldEnroll,
552
+ identityFor: (repoRelativePath) => identityAt?.(
553
+ rootRelativePath ? `${rootRelativePath}/${repoRelativePath}` : repoRelativePath,
554
+ )?.uuid || null,
555
+ });
556
+ const state = assertState(repoPath, await captureRepoAsync(repoPath, {
557
+ identity: transportIdentity(assignment),
558
+ }));
559
+ plans.set(repoPath, { state, assignment });
560
+ for (const [entryRelativePath, entry] of assignment) {
561
+ if (entry.type !== 'repo.git') continue;
562
+ await planRepo(
563
+ path.join(repoPath, entryRelativePath),
564
+ rootRelativePath ? `${rootRelativePath}/${entryRelativePath}` : entryRelativePath,
565
+ );
566
+ }
567
+ };
568
+
569
+ // A direct repo is its own initial state. An ordinary folder can still
570
+ // contain repo boundaries, and those must never fall back to a blocking
571
+ // main-thread capture just because the user selected their parent.
572
+ if (root.kind === 'repo') {
573
+ await planRepo(root.path, '');
574
+ return (repoPath) => plans.get(repoPath) || null;
575
+ }
576
+
577
+ let visited = 0;
578
+ const yieldIfNeeded = async () => {
579
+ visited += 1;
580
+ if (visited % 256 !== 0) return;
581
+ await new Promise((resolve) => setImmediate(resolve));
582
+ };
583
+ const discover = async (dirPath, relativePath) => {
584
+ let entries;
585
+ try {
586
+ entries = fs.readdirSync(dirPath, { withFileTypes: true })
587
+ .sort((a, b) => a.name.localeCompare(b.name));
588
+ } catch (error) {
589
+ if (error.code === 'EACCES' || error.code === 'EPERM') return;
590
+ throw error;
591
+ }
592
+ for (const entry of entries) {
593
+ await yieldIfNeeded();
594
+ const entryPath = path.join(dirPath, entry.name);
595
+ const entryRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
596
+ if (!observer.shouldEnroll(entryPath)) continue;
597
+ let stat;
598
+ try {
599
+ stat = fs.lstatSync(entryPath);
600
+ } catch {
601
+ continue;
602
+ }
603
+ if (!stat.isDirectory()) continue;
604
+ if (gitDirTruth(entryPath) === 'present') await planRepo(entryPath, entryRelativePath);
605
+ else await discover(entryPath, entryRelativePath);
606
+ }
607
+ };
608
+ await discover(root.path, '');
609
+ return (repoPath) => plans.get(repoPath) || null;
610
+ }
611
+
612
+ /**
613
+ * The public operation remains synchronous in its result: it returns only
614
+ * after the watcher, complete identity tree, state parcel, and journal all
615
+ * agree. Its preparation is asynchronous solely so large immutable cargo
616
+ * cannot freeze the HTTP runtime that owns the watcher proof.
617
+ */
618
+ async function addAsync(targetPath, options = {}) {
619
+ const identityAt = options.identityAt ?? null;
620
+ const suppressJournal = options.suppressJournal === true;
621
+ if (identityAt !== null && typeof identityAt !== 'function') {
622
+ throw new Error('addAsync identityAt must be a function when supplied');
623
+ }
624
+ if (typeof captureRepoAsync !== 'function') return add(targetPath, options);
625
+
626
+ let address;
627
+ let ground;
628
+ try {
629
+ const parent = fs.realpathSync(path.dirname(targetPath));
630
+ address = path.join(parent, path.basename(targetPath));
631
+ ground = fs.lstatSync(address).isSymbolicLink() ? address : fs.realpathSync(address);
632
+ } catch (error) {
633
+ if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') throw error;
634
+ throw refusal(`refusing to add ${targetPath}: there is no ground at that address`);
635
+ }
636
+
637
+ // Links intentionally retain link semantics even if their target is a
638
+ // repo. That is the same address law as `add`, not a hidden realpath.
639
+ const rootKind = fs.lstatSync(ground).isDirectory() && gitDirTruth(ground) === 'present'
640
+ ? 'repo'
641
+ : null;
642
+ if (rootKind !== 'repo') return add(targetPath, options);
643
+
644
+ let root = observer.listRoots().find((candidate) => candidate.path === ground) || null;
645
+ const rootAlreadyExisted = root !== null;
646
+ if (!root) root = observer.enrollRoot(ground, { scan: false, watch: false });
647
+ if (root.kind !== 'repo') return add(targetPath, options);
648
+ const bound = memory.boundTo(root.rootId);
649
+ if (bound !== undefined) return add(targetPath, options);
650
+
651
+ let watcherHeld = false;
652
+ try {
653
+ requireWatchBirth(root);
654
+ watcherHeld = true;
655
+ const preparedRepoAt = await prepareRepositoryPlans(root, identityAt);
656
+ return registerRepositoryInsideCoveredGround(ground, {
657
+ identityAt,
658
+ suppressJournal,
659
+ preparedRepoAt,
660
+ watcherAlreadyHeld: true,
661
+ });
662
+ } catch (error) {
663
+ // No identity was committed before preparation. A new root is only
664
+ // this failed request's temporary witness; an existing discovered root
665
+ // stays covered, but its held doorbell must be released.
666
+ if (!rootAlreadyExisted) observer.removeRoot(root.rootId);
667
+ else if (watcherHeld) observer.start(root.rootId);
668
+ throw error;
669
+ }
670
+ }
671
+
503
672
  /** Real ground -> the UUID of the entity that owns it, across trees. */
504
673
  function resolveGround(real) {
505
674
  const home = treeRootCovering(real);
@@ -746,6 +915,30 @@ function createRegistration(options) {
746
915
  return normalized.map(({ targetPath, identityAt }) => add(targetPath, { identityAt }));
747
916
  }
748
917
 
918
+ /** Same declared batch contract as `addMany`, while allowing a large Git
919
+ * cargo plan to run off the runtime event loop. Entries remain ordered and
920
+ * individually durable: this adds no second transaction model. */
921
+ async function addManyAsync(entries, { suppressJournal = false } = {}) {
922
+ if (!Array.isArray(entries) || entries.length === 0) {
923
+ throw new Error('addManyAsync requires one or more target paths');
924
+ }
925
+ const normalized = entries.map((entry) => {
926
+ if (typeof entry === 'string') return { targetPath: entry, identityAt: null };
927
+ if (!entry || typeof entry !== 'object' || typeof entry.targetPath !== 'string') {
928
+ throw new Error('addManyAsync entries must be paths or { targetPath, identityAt }');
929
+ }
930
+ if (entry.identityAt !== undefined && entry.identityAt !== null && typeof entry.identityAt !== 'function') {
931
+ throw new Error('addManyAsync identityAt must be a function when supplied');
932
+ }
933
+ return { targetPath: entry.targetPath, identityAt: entry.identityAt ?? null };
934
+ });
935
+ const results = [];
936
+ for (const { targetPath, identityAt } of normalized) {
937
+ results.push(await addAsync(targetPath, { identityAt, suppressJournal }));
938
+ }
939
+ return results;
940
+ }
941
+
749
942
  /** Reconcile one already-watched tree after the platform has materialized
750
943
  * a newly declared exact default inside it. This is deliberately not part
751
944
  * of ordinary re-registration: the caller must have already named the
@@ -783,6 +976,13 @@ function createRegistration(options) {
783
976
  return clean;
784
977
  }
785
978
 
979
+ // Reopen every persisted observer before the first reconciliation. An
980
+ // identity database survives a runtime restart; its Watch proof must too.
981
+ // `start()` opens physical owners and restores covered roots through the
982
+ // same coverage law as a new registration, then its census catches up on
983
+ // dings that occurred while this process was absent.
984
+ observer.start();
985
+
786
986
  // Startup is the same act as running — the one convergence pass,
787
987
  // nothing less. The projection must not wait for an event: a
788
988
  // rendering whose deletion was detected before a crash is equal
@@ -795,7 +995,9 @@ function createRegistration(options) {
795
995
  // observer bindings remain private to this registration composition.
796
996
  registry,
797
997
  add,
998
+ addAsync,
798
999
  addMany,
1000
+ addManyAsync,
799
1001
  reconcileDeclared,
800
1002
 
801
1003
  /**
@@ -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;
@@ -102,13 +113,17 @@ function createRepoFollowers({ observer, registry, repoStates, recordForRoot, on
102
113
  // cargo captures (withBundle) need content and read everything;
103
114
  // capture() enforces that side of the contract itself.
104
115
  const previous = withBundle ? null : repoStates.get(record.uuid);
105
- const captured = await worker.capture(repoPath, {
116
+ const options = {
106
117
  withBundle,
107
118
  suspects,
108
119
  previousUntracked: previous?.checkpoint?.untracked?.map(
109
120
  ({ path: rel, mode, sha256 }) => ({ path: rel, mode, sha256 }),
110
121
  ) ?? null,
111
- });
122
+ };
123
+ if (withBundle && captureRepoAsync) {
124
+ return captureRepoAsync(repoPath, { entityUuid: record.uuid, ...options });
125
+ }
126
+ const captured = await worker.capture(repoPath, options);
112
127
  return withBundle && !captured?.busy
113
128
  ? prepareRepo(captured, { entityUuid: record.uuid })
114
129
  : captured;
@@ -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
  },
@@ -91,6 +91,56 @@ function collectWorktreeIdentity(repoPath, { shouldEnroll, identityFor }) {
91
91
  return assignment;
92
92
  }
93
93
 
94
+ /**
95
+ * The asynchronous twin of the identity walk. It has exactly the same
96
+ * shape rules as `collectWorktreeIdentity`, but returns to Node between
97
+ * bounded batches. A large initial repo registration may take time to
98
+ * inspect, yet its held watcher and the runtime's health endpoint must stay
99
+ * alive while that inspection is in flight. This is planning only: no
100
+ * identity row exists until `registerTree` commits the finished plan.
101
+ */
102
+ async function collectWorktreeIdentityAsync(repoPath, { shouldEnroll, identityFor, yieldEvery = 256 }) {
103
+ const assignment = new Map(); // repo-relative path -> { uuid, type }
104
+ let visited = 0;
105
+ const yieldIfNeeded = async () => {
106
+ visited += 1;
107
+ if (visited % yieldEvery !== 0) return;
108
+ await new Promise((resolve) => setImmediate(resolve));
109
+ };
110
+ const walk = async (dirPath, relativePath) => {
111
+ let entries;
112
+ try {
113
+ entries = fs.readdirSync(dirPath, { withFileTypes: true })
114
+ .sort((a, b) => a.name.localeCompare(b.name));
115
+ } catch (error) {
116
+ if (error.code === 'EACCES' || error.code === 'EPERM') return;
117
+ throw error;
118
+ }
119
+ for (const entry of entries) {
120
+ await yieldIfNeeded();
121
+ const entryPath = path.join(dirPath, entry.name);
122
+ const entryRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
123
+ if (!shouldEnroll(entryPath)) continue;
124
+ let stat;
125
+ try {
126
+ stat = fs.lstatSync(entryPath);
127
+ } catch {
128
+ continue; // raced away between readdir and look; the create-walk decides
129
+ }
130
+ let type;
131
+ if (stat.isSymbolicLink()) type = 'link';
132
+ else if (stat.isDirectory()) type = isRepository(entryPath) ? 'repo.git' : 'folder';
133
+ else if (stat.isFile()) type = 'file.text';
134
+ else continue;
135
+ const uuid = identityFor?.(entryRelativePath) || crypto.randomUUID();
136
+ assignment.set(entryRelativePath, { uuid, type });
137
+ if (type === 'folder') await walk(entryPath, entryRelativePath);
138
+ }
139
+ };
140
+ await walk(repoPath, '');
141
+ return assignment;
142
+ }
143
+
94
144
  function transportIdentity(assignment) {
95
145
  return [...assignment].map(([entryPath, { uuid, type }]) => ({ path: entryPath, uuid, type }));
96
146
  }
@@ -100,7 +150,17 @@ function transportIdentity(assignment) {
100
150
  * directory ground. The caller supplies a root that is already known to the
101
151
  * observer but is not yet watched or scanned.
102
152
  */
103
- function registerTree({ root, registry, memory, shouldEnroll, captureRepo, repoStates, identityAt = null, repoStateAt = null }) {
153
+ function registerTree({
154
+ root,
155
+ registry,
156
+ memory,
157
+ shouldEnroll,
158
+ captureRepo,
159
+ repoStates,
160
+ identityAt = null,
161
+ repoStateAt = null,
162
+ preparedRepoAt = null,
163
+ }) {
104
164
  if (!root || !registry || !memory || typeof shouldEnroll !== 'function'
105
165
  || typeof captureRepo !== 'function' || !repoStates) {
106
166
  throw new Error('registerTree requires { root, registry, memory, shouldEnroll, captureRepo, repoStates }');
@@ -109,6 +169,9 @@ function registerTree({ root, registry, memory, shouldEnroll, captureRepo, repoS
109
169
  if (identityAt !== null && typeof identityAt !== 'function') {
110
170
  throw new Error('registerTree identityAt must be a function when supplied');
111
171
  }
172
+ if (preparedRepoAt !== null && typeof preparedRepoAt !== 'function') {
173
+ throw new Error('registerTree preparedRepoAt must be a function when supplied');
174
+ }
112
175
 
113
176
  function seeded(relativePath, { type, parentUUID, name, root: isRoot }) {
114
177
  const identity = identityAt?.(relativePath) || null;
@@ -151,6 +214,8 @@ function registerTree({ root, registry, memory, shouldEnroll, captureRepo, repoS
151
214
  const saved = repoStates.get(identity.uuid);
152
215
  if (saved?.stateId === identity.payloadVersion) return { state: saved, assignment: null };
153
216
  }
217
+ const prepared = preparedRepoAt?.(repoPath) || null;
218
+ if (prepared) return prepared;
154
219
  const assignment = collectWorktreeIdentity(repoPath, {
155
220
  shouldEnroll,
156
221
  identityFor: (repoRelativePath) => identityAt?.(
@@ -296,4 +361,4 @@ function registerDirectory(dirPath, parentUUID, {
296
361
  }
297
362
  }
298
363
 
299
- module.exports = { registerTree };
364
+ module.exports = { collectWorktreeIdentityAsync, registerTree, transportIdentity };
@@ -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
  }
@@ -657,6 +657,28 @@ test('adding a repository registers its allowed tree and saves its initial Repoc
657
657
  assert.equal(tree.root().uuid, after.uuid);
658
658
  });
659
659
 
660
+ test('a production repository register builds its immutable Git cargo off the runtime thread', async () => {
661
+ const service = makeService('repo-cargo-worker');
662
+ const repo = makeGround('repo-cargo-worker');
663
+ execFileSync('git', ['init', '-q'], { cwd: repo });
664
+ fs.writeFileSync(path.join(repo, 'history.md'), '# committed history\n');
665
+ execFileSync('git', ['add', '.'], { cwd: repo });
666
+ execFileSync('git', [
667
+ '-c', 'user.name=Amalgm Test',
668
+ '-c', 'user.email=amalgm-test@example.invalid',
669
+ 'commit', '-qm', 'initial history',
670
+ ], { cwd: repo });
671
+
672
+ const { record } = await service.register(repo);
673
+ const state = service.registration.repoState(record.uuid);
674
+ assert.equal(record.type, 'repo.git');
675
+ assert.ok(state?.transportVersion, 'the saved state names one immutable transport object');
676
+ assert.equal(state.card.bundle, null,
677
+ 'identity.db holds only bundle-free state; the worker left history in immutable content');
678
+ assert.ok(service.entityContent.bytes({ contentHash: state.transportVersion }).length > 0,
679
+ 'the worker finished the transport before Register committed the repo head');
680
+ });
681
+
660
682
  test('an edge-reached repository gets the same complete registration as a directly added repo', async () => {
661
683
  const service = makeService('edge-repo-card');
662
684
  const holder = makeGround('edge-repo-holder');
@@ -959,6 +981,25 @@ test('identity survives a real close-and-reopen over the same directory', async
959
981
  assert.equal(after, before, 'the tree keeps its permanent UUID across processes');
960
982
  });
961
983
 
984
+ test('a restarted runtime reopens the persisted watcher before it reports the tree healthy', async () => {
985
+ const dir = path.join(tempRoot, 'state-reopen-watch');
986
+ const ground = makeGround('reopen-watch');
987
+ fs.writeFileSync(path.join(ground, 'notes.md'), 'persisted ground\n');
988
+
989
+ const first = createRegistrationService({ dir });
990
+ const registered = await first.register(ground);
991
+ first.close();
992
+
993
+ const reopened = createRegistrationService({ dir });
994
+ services.push(reopened);
995
+ const root = reopened.observer.status().find((entry) => entry.path === ground);
996
+ assert.ok(root, 'the original observer root survived the restart');
997
+ assert.equal(root.watching, true, 'the restarted runtime has a live Watch rail');
998
+ assert.equal(root.degraded, false);
999
+ assert.equal(reopened.registration.tree(ground).root().uuid, registered.record.uuid,
1000
+ 'the watcher reopened over the same permanent identity');
1001
+ });
1002
+
962
1003
  // --- the policies are the real ones ---------------------------------------
963
1004
 
964
1005
  test("amalgm's enrollment policy is in force, and refuses loudly", async () => {