@amalgm/shell 0.1.54 → 0.1.56
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/PURPOSE.md +47 -3
- package/dist/detection/runtime.js +98 -31
- package/dist/detection/runtime.js.map +1 -1
- package/dist/entity-apply-host.d.ts +28 -0
- package/dist/entity-apply-host.js +661 -0
- package/dist/entity-apply-host.js.map +1 -0
- package/dist/entity-apply-store.d.ts +42 -0
- package/dist/entity-apply-store.js +367 -0
- package/dist/entity-apply-store.js.map +1 -0
- package/dist/git-registration-host.d.ts +8 -1
- package/dist/git-registration-host.js +21 -0
- package/dist/git-registration-host.js.map +1 -1
- package/dist/git-repository-host.js +16 -4
- package/dist/git-repository-host.js.map +1 -1
- package/dist/materialized-graph.js +27 -4
- package/dist/materialized-graph.js.map +1 -1
- package/dist/user-ground-host.d.ts +6 -0
- package/dist/user-ground-host.js +249 -84
- package/dist/user-ground-host.js.map +1 -1
- package/package.json +2 -2
package/dist/user-ground-host.js
CHANGED
|
@@ -3,9 +3,10 @@ import { existsSync, lstatSync, realpathSync, readFileSync, readdirSync, readlin
|
|
|
3
3
|
import { open } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { buildUserHomeManifest, buildUserManifest, liveMachineStateDir, scopedAmalgmDir, shippedUserHomeDeclaration, } from "@amalgm/core/identity";
|
|
6
|
-
import { CHUNK_BYTES, CONTENT_CONTRACT, ENTITY_CLOUD_CONTRACT, ENTITY_CLOUD_SCHEMA_VERSION, artifactForRecord, canonicalVersion, checkContentManifest, classifyDirectory, classifyFile, classifyRegisteredRoot, createLocalEntityRecord, createEntityRecordAuthorityPort, convergeUserGround, createUserGroundEnrollmentPolicy, isRepositoryMetadataEntry, download as downloadArtifact, encodeEntityRecord, EntityRecordRail, membershipHash, parseSnapshot, pathIsSuspect, privateEntityResourceId, sameRecords, snapshotFromRecords, stableJson, travelingRecords, upload as uploadArtifact, userGroundRecords, } from "@amalgm/live";
|
|
6
|
+
import { CHUNK_BYTES, CONTENT_CONTRACT, ENTITY_CLOUD_CONTRACT, ENTITY_CLOUD_SCHEMA_VERSION, artifactForRecord, canonicalVersion, checkContentManifest, classifyDirectory, classifyFile, classifyRegisteredRoot, createLocalEntityRecord, createEntityRecordAuthorityPort, convergeUserGround, createUserGroundEnrollmentPolicy, isRepositoryMetadataEntry, indexRepositoryTerritory, download as downloadArtifact, encodeEntityRecord, EntityApplyRail, EntityRecordRail, membershipHash, parseSnapshot, pathIsSuspect, privateEntityResourceId, sameRecords, snapshotFromRecords, stableJson, travelingRecords, upload as uploadArtifact, userGroundRecords, } from "@amalgm/live";
|
|
7
7
|
import Database from "better-sqlite3";
|
|
8
8
|
import { atomicCopy, atomicWrite, ensurePrivateDir } from "./filesystem.js";
|
|
9
|
+
import { EntityApplyHost } from "./entity-apply-host.js";
|
|
9
10
|
import { EntityRecordSqliteStore } from "./entity-record-store.js";
|
|
10
11
|
import { ContentCacheDownload, captureContentFile, hashContentFile, sealCapturedArtifact, } from "./content-cache-host.js";
|
|
11
12
|
import { decodeContentWireBytes, encodeContentWireBytes } from "./content-wire-codec.js";
|
|
@@ -13,7 +14,7 @@ import { exactTextReplay, planGroundDetection, reconcileGroundUUIDs, } from "./d
|
|
|
13
14
|
import { NamedDetectRuntime } from "./detection/runtime.js";
|
|
14
15
|
import { WORKSPACE_UUID as UUID, createFilesRegisterPorts, ensureWorkspaceBinding, ensureWorkspaceReference, pathExists, pathWithin, referenceWorkspaceId, selectKnownRegistrationId, workspaceBindingDir, } from "./files-register-host.js";
|
|
15
16
|
import { applyRepositoryFiles, captureRepository, inspectRepositoryTransportFile, hasGitMarker, } from "./git-repository-host.js";
|
|
16
|
-
import { inspectGitRegistration, } from "./git-registration-host.js";
|
|
17
|
+
import { canonicalizeGitIdentity, inspectGitRegistration, sameGitIdentity, } from "./git-registration-host.js";
|
|
17
18
|
import { projectMaterializedGraph } from "./materialized-graph.js";
|
|
18
19
|
import { NodeWatchHost, } from "./watching/index.js";
|
|
19
20
|
import { WireClient, WireRequestError } from "./wire-client.js";
|
|
@@ -44,9 +45,12 @@ export class UserGroundHost {
|
|
|
44
45
|
syncing = null;
|
|
45
46
|
recordRail = null;
|
|
46
47
|
recordStore = null;
|
|
48
|
+
applyRail = null;
|
|
49
|
+
applyHost = null;
|
|
47
50
|
namedDetect = null;
|
|
48
51
|
coldOperation = false;
|
|
49
52
|
closing = false;
|
|
53
|
+
groundEffectTail = Promise.resolve();
|
|
50
54
|
port;
|
|
51
55
|
constructor(options) {
|
|
52
56
|
this.options = options;
|
|
@@ -88,6 +92,7 @@ export class UserGroundHost {
|
|
|
88
92
|
watching: true,
|
|
89
93
|
};
|
|
90
94
|
await this.startRecordRail(this.activeIdentity);
|
|
95
|
+
await this.startApplyRail(this.activeIdentity);
|
|
91
96
|
this.converged = true;
|
|
92
97
|
return evidence;
|
|
93
98
|
},
|
|
@@ -148,26 +153,38 @@ export class UserGroundHost {
|
|
|
148
153
|
return cloud;
|
|
149
154
|
},
|
|
150
155
|
materializeWorkspace: async (input) => {
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
stage
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
156
|
+
const release = await this.acquireGroundEffect();
|
|
157
|
+
try {
|
|
158
|
+
const installed = await installCloudWorkspace({
|
|
159
|
+
identity,
|
|
160
|
+
userRoot,
|
|
161
|
+
database: this.databasePath(identity),
|
|
162
|
+
cacheDir: this.cacheDir(identity),
|
|
163
|
+
bindingDir,
|
|
164
|
+
resourceId: privateEntityResourceId(identity.userId, sha256Hex),
|
|
165
|
+
workspace: input.workspace,
|
|
166
|
+
reference: input.reference,
|
|
167
|
+
references: input.references,
|
|
168
|
+
records: input.records,
|
|
169
|
+
destinationParent: input.destinationParent,
|
|
170
|
+
cloudHead: input.cloudHead,
|
|
171
|
+
readContent: (artifact) => this.downloadContent(privateEntityResourceId(identity.userId, sha256Hex), this.cacheDir(identity), artifact),
|
|
172
|
+
onStage: (stage) => this.options.onWorkspaceAddStage?.({
|
|
173
|
+
workspaceId: input.workspace.uuid,
|
|
174
|
+
stage,
|
|
175
|
+
}),
|
|
176
|
+
});
|
|
177
|
+
// Add changes both disk and the exact SQLite projection. Open the
|
|
178
|
+
// selected root's singular Watch coverage before Detect can observe
|
|
179
|
+
// either side of that transaction; the public cover port below
|
|
180
|
+
// independently proves the same coverage before clearing intent.
|
|
181
|
+
this.ensureWatchers(identity, installed.alreadyMaterialized ? [] : [input.workspace.uuid]);
|
|
182
|
+
assertHealthyWatch(this.watchHost.evidence(), input.workspace.uuid);
|
|
183
|
+
return installed;
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
release();
|
|
187
|
+
}
|
|
171
188
|
},
|
|
172
189
|
coverWorkspace: async ({ workspaceId, baseline }) => {
|
|
173
190
|
const pending = readWorkspaceAddIntent(this.databasePath(identity), workspaceId);
|
|
@@ -189,37 +206,43 @@ export class UserGroundHost {
|
|
|
189
206
|
const userRoot = this.userRoot(identity);
|
|
190
207
|
const bindingDir = workspaceBindingDir(userRoot, identity.deviceId);
|
|
191
208
|
for (const intent of readWorkspaceAddIntents(database)) {
|
|
192
|
-
const
|
|
209
|
+
const release = await this.acquireGroundEffect();
|
|
193
210
|
const visibleAcrossBlindInterval = pathExists(intent.destinationPath);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
211
|
+
try {
|
|
212
|
+
const selection = workspaceAddSelection(intent);
|
|
213
|
+
const stagedAcrossBlindInterval = pathExists(intent.stagingPath);
|
|
214
|
+
if (stagedAcrossBlindInterval && readRows(database).some((row) => row.resourceId === intent.resourceId && row.rootUUID === intent.workspaceId)) {
|
|
215
|
+
// Rows prove that staging was verified before the crash, not that its
|
|
216
|
+
// bytes remained unchanged while no watcher existed. Return this
|
|
217
|
+
// private install to the intent-only stage and reproduce it from the
|
|
218
|
+
// verified immutable cache before making it visible.
|
|
219
|
+
deleteRootRows(database, intent.resourceId, intent.workspaceId);
|
|
220
|
+
}
|
|
221
|
+
await installCloudWorkspace({
|
|
222
|
+
identity,
|
|
223
|
+
userRoot,
|
|
224
|
+
database,
|
|
225
|
+
cacheDir: this.cacheDir(identity),
|
|
226
|
+
bindingDir,
|
|
227
|
+
resourceId: intent.resourceId,
|
|
228
|
+
workspace: selection.workspace,
|
|
229
|
+
reference: selection.reference,
|
|
230
|
+
references: intent.references,
|
|
231
|
+
records: intent.records,
|
|
232
|
+
destinationParent: dirname(intent.destinationPath),
|
|
233
|
+
cloudHead: intent.cloudHead,
|
|
234
|
+
readContent: (artifact) => this.downloadContent(intent.resourceId, this.cacheDir(identity), artifact),
|
|
235
|
+
onStage: (stage) => this.options.onWorkspaceAddStage?.({
|
|
236
|
+
workspaceId: intent.workspaceId,
|
|
237
|
+
stage,
|
|
238
|
+
}),
|
|
239
|
+
});
|
|
240
|
+
this.ensureWatchers(identity, visibleAcrossBlindInterval ? [] : [intent.workspaceId]);
|
|
241
|
+
assertHealthyWatch(this.watchHost.evidence(), intent.workspaceId);
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
release();
|
|
201
245
|
}
|
|
202
|
-
await installCloudWorkspace({
|
|
203
|
-
identity,
|
|
204
|
-
userRoot,
|
|
205
|
-
database,
|
|
206
|
-
cacheDir: this.cacheDir(identity),
|
|
207
|
-
bindingDir,
|
|
208
|
-
resourceId: intent.resourceId,
|
|
209
|
-
workspace: selection.workspace,
|
|
210
|
-
reference: selection.reference,
|
|
211
|
-
references: intent.references,
|
|
212
|
-
records: intent.records,
|
|
213
|
-
destinationParent: dirname(intent.destinationPath),
|
|
214
|
-
cloudHead: intent.cloudHead,
|
|
215
|
-
readContent: (artifact) => this.downloadContent(intent.resourceId, this.cacheDir(identity), artifact),
|
|
216
|
-
onStage: (stage) => this.options.onWorkspaceAddStage?.({
|
|
217
|
-
workspaceId: intent.workspaceId,
|
|
218
|
-
stage,
|
|
219
|
-
}),
|
|
220
|
-
});
|
|
221
|
-
this.ensureWatchers(identity, visibleAcrossBlindInterval ? [] : [intent.workspaceId]);
|
|
222
|
-
assertHealthyWatch(this.watchHost.evidence(), intent.workspaceId);
|
|
223
246
|
if (visibleAcrossBlindInterval) {
|
|
224
247
|
// Ground that was visible while the runtime was absent may have been
|
|
225
248
|
// edited after its verified reveal. It is an ordinary startup blind
|
|
@@ -258,8 +281,12 @@ export class UserGroundHost {
|
|
|
258
281
|
throw stageError("Watch coverage", error);
|
|
259
282
|
}
|
|
260
283
|
let rows = readRows(this.databasePath(identity));
|
|
261
|
-
|
|
262
|
-
&&
|
|
284
|
+
const hasRegisteredBoundary = (records) => records.some((record) => record.uuid === workspaceId && record.status === "active")
|
|
285
|
+
&& records.some((record) => record.type === "reference"
|
|
286
|
+
&& record.status === "active"
|
|
287
|
+
&& record.payloadVersion === workspaceId);
|
|
288
|
+
if (hasRegisteredBoundary(rows.map(portableRecord))
|
|
289
|
+
&& hasRegisteredBoundary(this.cloudState.records)) {
|
|
263
290
|
this.finishColdOperation();
|
|
264
291
|
return {
|
|
265
292
|
registered: true,
|
|
@@ -271,11 +298,13 @@ export class UserGroundHost {
|
|
|
271
298
|
// Register is a cold declaration, not a filesystem edit. It performs one
|
|
272
299
|
// complete reconciliation and publishes one graph head; it must not mint
|
|
273
300
|
// a parallel stream of Detect records for the same initial contents.
|
|
274
|
-
|
|
275
|
-
this.watchDirty = false;
|
|
301
|
+
let releaseGround = null;
|
|
276
302
|
try {
|
|
277
303
|
if (this.syncing)
|
|
278
304
|
await this.syncing;
|
|
305
|
+
releaseGround = await this.acquireGroundEffect();
|
|
306
|
+
const observations = this.watchHost.observations();
|
|
307
|
+
this.watchDirty = false;
|
|
279
308
|
const scanned = await reconcileGround({
|
|
280
309
|
identity,
|
|
281
310
|
userRoot: this.userRoot(identity),
|
|
@@ -306,6 +335,7 @@ export class UserGroundHost {
|
|
|
306
335
|
throw stageError("entity registration or cloud publication", error);
|
|
307
336
|
}
|
|
308
337
|
finally {
|
|
338
|
+
releaseGround?.();
|
|
309
339
|
this.finishColdOperation();
|
|
310
340
|
}
|
|
311
341
|
try {
|
|
@@ -317,11 +347,11 @@ export class UserGroundHost {
|
|
|
317
347
|
throw stageError("Watch coverage", error);
|
|
318
348
|
}
|
|
319
349
|
rows = readRows(this.databasePath(identity));
|
|
320
|
-
if (!rows.
|
|
321
|
-
throw stageError("entity registration", new Error(`workspace ${workspaceId}
|
|
350
|
+
if (!hasRegisteredBoundary(rows.map(portableRecord))) {
|
|
351
|
+
throw stageError("entity registration", new Error(`workspace ${workspaceId} and its durable reference were not recorded`));
|
|
322
352
|
}
|
|
323
|
-
if (!this.cloudState.records
|
|
324
|
-
throw stageError("cloud graph commit", new Error(`workspace ${workspaceId} is absent from the committed head`));
|
|
353
|
+
if (!hasRegisteredBoundary(this.cloudState.records)) {
|
|
354
|
+
throw stageError("cloud graph commit", new Error(`workspace ${workspaceId} or its durable reference is absent from the committed head`));
|
|
325
355
|
}
|
|
326
356
|
return {
|
|
327
357
|
registered: true,
|
|
@@ -357,6 +387,7 @@ export class UserGroundHost {
|
|
|
357
387
|
}
|
|
358
388
|
} while (!this.watchHost.settled(cutoff));
|
|
359
389
|
await this.recordRail?.flush();
|
|
390
|
+
await this.applyRail?.flush();
|
|
360
391
|
if (!this.closing)
|
|
361
392
|
this.ensureWatchers(identity);
|
|
362
393
|
}
|
|
@@ -410,6 +441,7 @@ export class UserGroundHost {
|
|
|
410
441
|
},
|
|
411
442
|
random: Math.random,
|
|
412
443
|
},
|
|
444
|
+
onInboxCommitted: () => this.applyRail?.recordsAvailable(),
|
|
413
445
|
});
|
|
414
446
|
this.recordStore = store;
|
|
415
447
|
this.recordRail = rail;
|
|
@@ -426,6 +458,62 @@ export class UserGroundHost {
|
|
|
426
458
|
throw pipelineStageError("entity record subscribe", error);
|
|
427
459
|
}
|
|
428
460
|
}
|
|
461
|
+
async startApplyRail(identity) {
|
|
462
|
+
if (!this.cloudState)
|
|
463
|
+
throw new Error("entity Apply rail requires cloud authority");
|
|
464
|
+
if (this.applyRail || this.applyHost)
|
|
465
|
+
throw new Error("entity Apply rail already started");
|
|
466
|
+
let host;
|
|
467
|
+
host = new EntityApplyHost({
|
|
468
|
+
databaseFile: this.databasePath(identity),
|
|
469
|
+
userRoot: this.userRoot(identity),
|
|
470
|
+
cacheDir: this.cacheDir(identity),
|
|
471
|
+
bindingDir: workspaceBindingDir(this.userRoot(identity), identity.deviceId),
|
|
472
|
+
readContent: (artifact) => this.downloadContent(this.cloudState.resourceId, this.cacheDir(identity), artifact),
|
|
473
|
+
captureLocal: async (intent, plan, position) => {
|
|
474
|
+
// Apply never suppresses Watch. This explicit whole-root suspicion is
|
|
475
|
+
// the synchronous proof boundary used only when Apply observed a
|
|
476
|
+
// save racing its reveal and must make that save durable first.
|
|
477
|
+
this.watchHost.suspectAll();
|
|
478
|
+
this.watchDirty = true;
|
|
479
|
+
await this.syncNow();
|
|
480
|
+
if (!host.store.hasLocalWorkAfter(intent.authorityId, intent.entityId, intent.throughSequence)) {
|
|
481
|
+
throw new Error(`Apply ${position} capture for ${plan.target.type} ${plan.target.name} (${intent.entityId}) produced no durable local record`);
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
acquireGround: () => this.acquireGroundEffect(),
|
|
485
|
+
});
|
|
486
|
+
const rail = new EntityApplyRail({
|
|
487
|
+
store: host.store,
|
|
488
|
+
effects: host.effects,
|
|
489
|
+
schedule: {
|
|
490
|
+
schedule(delayMs, callback) {
|
|
491
|
+
const timer = setTimeout(callback, delayMs);
|
|
492
|
+
timer.unref();
|
|
493
|
+
return { cancel: () => clearTimeout(timer) };
|
|
494
|
+
},
|
|
495
|
+
random: Math.random,
|
|
496
|
+
},
|
|
497
|
+
sha256Hex,
|
|
498
|
+
now: Date.now,
|
|
499
|
+
// Path cones and repository territories are wider than one UUID. Until
|
|
500
|
+
// the host exposes those locks, one serial machine-effect owner is the
|
|
501
|
+
// only honest concurrency setting.
|
|
502
|
+
concurrency: 1,
|
|
503
|
+
});
|
|
504
|
+
this.applyHost = host;
|
|
505
|
+
this.applyRail = rail;
|
|
506
|
+
try {
|
|
507
|
+
await rail.start();
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
await rail.stop();
|
|
511
|
+
host.close();
|
|
512
|
+
this.applyRail = null;
|
|
513
|
+
this.applyHost = null;
|
|
514
|
+
throw pipelineStageError("entity Apply resume", error);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
429
517
|
async activateRuntimeTunnel(gatewayPort, runtimeToken) {
|
|
430
518
|
// Files convergence and Watch/Detect own ground currency. Advertising an
|
|
431
519
|
// HTTP tunnel must never trigger or wait for a second filesystem pass.
|
|
@@ -444,8 +532,10 @@ export class UserGroundHost {
|
|
|
444
532
|
this.watchHost.close({ catchUp: false });
|
|
445
533
|
this.namedDetect?.close();
|
|
446
534
|
this.namedDetect = null;
|
|
535
|
+
const applyStopped = this.applyRail?.stop();
|
|
447
536
|
const recordStopped = this.recordRail?.stop();
|
|
448
537
|
await this.wire.close();
|
|
538
|
+
await applyStopped;
|
|
449
539
|
await recordStopped;
|
|
450
540
|
// An upload already inside its retry helper may have reopened the wire
|
|
451
541
|
// after the first close. The stopped rail prevents new queue work; this
|
|
@@ -454,8 +544,18 @@ export class UserGroundHost {
|
|
|
454
544
|
this.recordStore?.close();
|
|
455
545
|
this.recordStore = null;
|
|
456
546
|
this.recordRail = null;
|
|
547
|
+
this.applyHost?.close();
|
|
548
|
+
this.applyHost = null;
|
|
549
|
+
this.applyRail = null;
|
|
457
550
|
return;
|
|
458
551
|
}
|
|
552
|
+
await this.recordRail?.flush();
|
|
553
|
+
await this.applyRail?.flush();
|
|
554
|
+
const applyStopped = this.applyRail?.stop();
|
|
555
|
+
await applyStopped;
|
|
556
|
+
this.applyHost?.close();
|
|
557
|
+
this.applyHost = null;
|
|
558
|
+
this.applyRail = null;
|
|
459
559
|
// Command shutdown is the last catch-up boundary. A filesystem callback
|
|
460
560
|
// may still be queued, so watcher silence cannot be used as evidence yet.
|
|
461
561
|
this.watchHost.suspectAll();
|
|
@@ -798,6 +898,9 @@ export class UserGroundHost {
|
|
|
798
898
|
rootId: workspaceId,
|
|
799
899
|
directory,
|
|
800
900
|
materialized: true,
|
|
901
|
+
// Every materialized root has one reserved machine-control subtree.
|
|
902
|
+
// Apply stages and retains recovery bytes here; Watch never enrolls it.
|
|
903
|
+
excludedSubtrees: [join(directory, ".amalgm")],
|
|
801
904
|
});
|
|
802
905
|
}
|
|
803
906
|
const health = this.watchHost.reconcile([...wanted.values()], {
|
|
@@ -856,7 +959,24 @@ export class UserGroundHost {
|
|
|
856
959
|
this.scheduleRescan();
|
|
857
960
|
}
|
|
858
961
|
}
|
|
962
|
+
async acquireGroundEffect() {
|
|
963
|
+
let release;
|
|
964
|
+
const held = new Promise((resolve) => { release = resolve; });
|
|
965
|
+
const preceding = this.groundEffectTail;
|
|
966
|
+
this.groundEffectTail = preceding.then(() => held);
|
|
967
|
+
await preceding;
|
|
968
|
+
return release;
|
|
969
|
+
}
|
|
859
970
|
async syncLocalChanges(identity) {
|
|
971
|
+
const release = await this.acquireGroundEffect();
|
|
972
|
+
try {
|
|
973
|
+
await this.syncLocalChangesUnlocked(identity);
|
|
974
|
+
}
|
|
975
|
+
finally {
|
|
976
|
+
release();
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
async syncLocalChangesUnlocked(identity) {
|
|
860
980
|
if (!this.cloudState)
|
|
861
981
|
throw new Error("cloud state is unavailable for user-ground Watch");
|
|
862
982
|
const observations = this.watchHost.observations();
|
|
@@ -885,6 +1005,7 @@ export class UserGroundHost {
|
|
|
885
1005
|
});
|
|
886
1006
|
}
|
|
887
1007
|
this.watchHost.settle(observations);
|
|
1008
|
+
this.applyRail?.localStateChanged();
|
|
888
1009
|
this.recordRail?.localRecordsAvailable();
|
|
889
1010
|
return;
|
|
890
1011
|
}
|
|
@@ -935,6 +1056,7 @@ export class UserGroundHost {
|
|
|
935
1056
|
});
|
|
936
1057
|
this.namedDetect?.refreshEnrollmentPolicy();
|
|
937
1058
|
this.watchHost.settle(observations);
|
|
1059
|
+
this.applyRail?.localStateChanged();
|
|
938
1060
|
this.recordRail?.localRecordsAvailable();
|
|
939
1061
|
}
|
|
940
1062
|
async publishPendingSnapshotsBeforeLookup(identity, resourceId) {
|
|
@@ -1482,14 +1604,22 @@ function commitDetectedState(file, input) {
|
|
|
1482
1604
|
for (const uuid of input.materializedRemovals ?? [])
|
|
1483
1605
|
removeMaterialized.run(uuid);
|
|
1484
1606
|
const upsertMaterialized = prepareGroundUpsert(database, "entities");
|
|
1607
|
+
// A valid graph may move or re-root several UUIDs through one another's
|
|
1608
|
+
// old address slots. Release every changed UUID's old slot first, then
|
|
1609
|
+
// install the complete new projection in this same transaction; row
|
|
1610
|
+
// iteration order must never decide whether an atomic graph is valid.
|
|
1485
1611
|
for (const row of input.acceptedMaterializedRows ?? []) {
|
|
1486
|
-
|
|
1612
|
+
removeMaterialized.run(row.uuid);
|
|
1613
|
+
}
|
|
1614
|
+
for (const row of input.acceptedMaterializedRows ?? []) {
|
|
1615
|
+
runGroundUpsert(database, "entities", upsertMaterialized, row);
|
|
1487
1616
|
}
|
|
1488
1617
|
for (const uuid of input.notebookRemovals)
|
|
1489
1618
|
remove.run(uuid);
|
|
1490
1619
|
const upsertNotebook = prepareGroundUpsert(database, "detection_notebook");
|
|
1491
|
-
for (const row of input.acceptedNotebookRows)
|
|
1492
|
-
upsertNotebook
|
|
1620
|
+
for (const row of input.acceptedNotebookRows) {
|
|
1621
|
+
runGroundUpsert(database, "detection_notebook", upsertNotebook, row);
|
|
1622
|
+
}
|
|
1493
1623
|
})();
|
|
1494
1624
|
}
|
|
1495
1625
|
finally {
|
|
@@ -1617,6 +1747,19 @@ function prepareGroundUpsert(database, table) {
|
|
|
1617
1747
|
content_verified_at_ms = excluded.content_verified_at_ms
|
|
1618
1748
|
`);
|
|
1619
1749
|
}
|
|
1750
|
+
function runGroundUpsert(database, table, statement, row) {
|
|
1751
|
+
try {
|
|
1752
|
+
statement.run(...groundRowValues(row));
|
|
1753
|
+
}
|
|
1754
|
+
catch (error) {
|
|
1755
|
+
const occupant = database.prepare(`
|
|
1756
|
+
SELECT uuid, absolute_path AS absolutePath FROM ${table}
|
|
1757
|
+
WHERE resource_id = ? AND root_uuid = ? AND relative_path = ?
|
|
1758
|
+
`).get(row.resourceId, row.rootUUID, row.relativePath);
|
|
1759
|
+
throw new Error(`${table} projection could not place ${row.uuid} at ${row.rootUUID}:${row.relativePath}; `
|
|
1760
|
+
+ `occupied by ${occupant?.uuid ?? "no retained row"} (${occupant?.absolutePath ?? "unknown"})`, { cause: error });
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1620
1763
|
function persistRows(file, identity, resourceId, rootUUID, rows, previousRows = [], options = {}) {
|
|
1621
1764
|
const database = initializeDatabase(file);
|
|
1622
1765
|
try {
|
|
@@ -1631,6 +1774,7 @@ function persistRows(file, identity, resourceId, rootUUID, rows, previousRows =
|
|
|
1631
1774
|
const upsert = prepareGroundUpsert(database, "entities");
|
|
1632
1775
|
const upsertNotebook = prepareGroundUpsert(database, "detection_notebook");
|
|
1633
1776
|
const remove = database.prepare("DELETE FROM entities WHERE uuid = ? AND resource_id = ? AND root_uuid = ?");
|
|
1777
|
+
const releaseChangedSlot = database.prepare("DELETE FROM entities WHERE uuid = ?");
|
|
1634
1778
|
const removeNotebook = database.prepare("DELETE FROM detection_notebook WHERE uuid = ? AND resource_id = ? AND root_uuid = ?");
|
|
1635
1779
|
const previousByUuid = new Map(previousRows.map((row) => [row.uuid, row]));
|
|
1636
1780
|
const materializedIds = new Set(database.prepare("SELECT uuid FROM entities").all()
|
|
@@ -1671,10 +1815,13 @@ function persistRows(file, identity, resourceId, rootUUID, rows, previousRows =
|
|
|
1671
1815
|
}
|
|
1672
1816
|
}
|
|
1673
1817
|
for (const row of changed)
|
|
1674
|
-
|
|
1818
|
+
releaseChangedSlot.run(row.record.uuid);
|
|
1819
|
+
for (const row of changed) {
|
|
1820
|
+
runGroundUpsert(database, "entities", upsert, capturedGroundRow(resourceId, rootUUID, row));
|
|
1821
|
+
}
|
|
1675
1822
|
if (options.updateNotebook !== false) {
|
|
1676
1823
|
for (const row of notebookChanged) {
|
|
1677
|
-
upsertNotebook
|
|
1824
|
+
runGroundUpsert(database, "detection_notebook", upsertNotebook, capturedGroundRow(resourceId, rootUUID, row));
|
|
1678
1825
|
}
|
|
1679
1826
|
}
|
|
1680
1827
|
});
|
|
@@ -2087,6 +2234,11 @@ async function reconcileRoot(input) {
|
|
|
2087
2234
|
const children = readdirSync(directory, { withFileTypes: true })
|
|
2088
2235
|
.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
2089
2236
|
for (const child of children) {
|
|
2237
|
+
// Every materialization boundary reserves its root .amalgm/ directory
|
|
2238
|
+
// for machine intents, staging, and recovery. This is structural host
|
|
2239
|
+
// state, never enrollment policy and never an entity.
|
|
2240
|
+
if (base === "" && child.name === ".amalgm")
|
|
2241
|
+
continue;
|
|
2090
2242
|
if (isRepositoryMetadataEntry(parentType, child.name))
|
|
2091
2243
|
continue;
|
|
2092
2244
|
const relativePath = base ? `${base}/${child.name}` : child.name;
|
|
@@ -2294,20 +2446,20 @@ async function reconcileRoot(input) {
|
|
|
2294
2446
|
// children first so one capture pass is already the fixed point.
|
|
2295
2447
|
const repositories = entries.filter((entry) => entry.type === "repo.git")
|
|
2296
2448
|
.sort((left, right) => right.relativePath.length - left.relativePath.length);
|
|
2449
|
+
const territory = indexRepositoryTerritory(entries);
|
|
2450
|
+
const previousEntries = [...existingByPath.values()];
|
|
2451
|
+
const previousTerritory = indexRepositoryTerritory(previousEntries);
|
|
2297
2452
|
const replayByUuid = new Map();
|
|
2298
|
-
const properDescendantOf = (candidate, ancestor) => ancestor === "" ? candidate !== "" : candidate.startsWith(`${ancestor}/`);
|
|
2299
|
-
const repositoryOwner = (entry) => repositories
|
|
2300
|
-
.filter((repository) => repository.uuid !== entry.uuid
|
|
2301
|
-
&& properDescendantOf(entry.relativePath, repository.relativePath))
|
|
2302
|
-
.sort((left, right) => right.relativePath.length - left.relativePath.length)[0] ?? null;
|
|
2303
2453
|
try {
|
|
2304
2454
|
for (const repository of repositories) {
|
|
2305
|
-
const
|
|
2306
|
-
|
|
2455
|
+
const ownedEntries = territory.ownedBy(repository.uuid);
|
|
2456
|
+
const repositoryPath = (entry) => repository.relativePath
|
|
2457
|
+
? entry.relativePath.slice(repository.relativePath.length + 1)
|
|
2458
|
+
: entry.relativePath;
|
|
2459
|
+
const ownedByPath = new Map(ownedEntries.map((entry) => [repositoryPath(entry), entry]));
|
|
2460
|
+
let repoIdentity = ownedEntries
|
|
2307
2461
|
.map((entry) => ({
|
|
2308
|
-
path:
|
|
2309
|
-
? entry.relativePath.slice(repository.relativePath.length + 1)
|
|
2310
|
-
: entry.relativePath,
|
|
2462
|
+
path: repositoryPath(entry),
|
|
2311
2463
|
uuid: entry.uuid,
|
|
2312
2464
|
type: entry.type,
|
|
2313
2465
|
payloadVersion: entry.payloadVersion,
|
|
@@ -2345,15 +2497,8 @@ async function reconcileRoot(input) {
|
|
|
2345
2497
|
}
|
|
2346
2498
|
}
|
|
2347
2499
|
}
|
|
2348
|
-
const previousRepositories = [...existingByPath.values()]
|
|
2349
|
-
.filter((row) => row.type === "repo.git");
|
|
2350
|
-
const previousOwner = (row) => previousRepositories
|
|
2351
|
-
.filter((candidate) => candidate.uuid !== row.uuid
|
|
2352
|
-
&& properDescendantOf(row.relativePath, candidate.relativePath))
|
|
2353
|
-
.sort((left, right) => right.relativePath.length - left.relativePath.length)[0] ?? null;
|
|
2354
2500
|
const previousIdentity = prior && priorRow
|
|
2355
|
-
?
|
|
2356
|
-
.filter((row) => previousOwner(row)?.uuid === priorRow.uuid)
|
|
2501
|
+
? previousTerritory.ownedBy(priorRow.uuid)
|
|
2357
2502
|
.map((row) => ({
|
|
2358
2503
|
path: priorRow.relativePath
|
|
2359
2504
|
? row.relativePath.slice(priorRow.relativePath.length + 1)
|
|
@@ -2365,7 +2510,27 @@ async function reconcileRoot(input) {
|
|
|
2365
2510
|
.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0)
|
|
2366
2511
|
: null;
|
|
2367
2512
|
evidence && (evidence.repositoryCaptures += 1);
|
|
2368
|
-
|
|
2513
|
+
let captured = null;
|
|
2514
|
+
let coherent = false;
|
|
2515
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
2516
|
+
captured = captureRepository(repository.absolutePath, repoIdentity, prior, previousIdentity);
|
|
2517
|
+
const after = canonicalizeGitIdentity(repoIdentity, inspectGitRegistration(repository.absolutePath));
|
|
2518
|
+
if (sameGitIdentity(after, repoIdentity)) {
|
|
2519
|
+
coherent = true;
|
|
2520
|
+
break;
|
|
2521
|
+
}
|
|
2522
|
+
repoIdentity = after;
|
|
2523
|
+
}
|
|
2524
|
+
if (!coherent || !captured) {
|
|
2525
|
+
throw new Error(`repository did not settle during capture: ${repository.absolutePath}`);
|
|
2526
|
+
}
|
|
2527
|
+
for (const identityEntry of repoIdentity) {
|
|
2528
|
+
const entry = ownedByPath.get(identityEntry.path);
|
|
2529
|
+
if (!entry)
|
|
2530
|
+
continue;
|
|
2531
|
+
entry.type = identityEntry.type;
|
|
2532
|
+
entry.payloadVersion = identityEntry.payloadVersion ?? null;
|
|
2533
|
+
}
|
|
2369
2534
|
repository.payloadVersion = captured.stateId;
|
|
2370
2535
|
repository.transportVersion = captured.transportVersion;
|
|
2371
2536
|
if (captured.bytes) {
|
|
@@ -2450,7 +2615,7 @@ async function reconcileRoot(input) {
|
|
|
2450
2615
|
const detected = rows.map((row) => {
|
|
2451
2616
|
const base = beforeByUuid.get(row.record.uuid) ?? null;
|
|
2452
2617
|
const entry = entryByUuid.get(row.record.uuid);
|
|
2453
|
-
if (row.record.type !== "repo.git" && entry &&
|
|
2618
|
+
if (row.record.type !== "repo.git" && entry && territory.ownerOf(entry)) {
|
|
2454
2619
|
return {
|
|
2455
2620
|
record: row.record,
|
|
2456
2621
|
relativePath: row.relativePath,
|