@amalgm/shell 0.1.88 → 0.1.90
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 +16 -2
- package/dist/detection/notebook-basis.d.ts +23 -0
- package/dist/detection/notebook-basis.js +23 -0
- package/dist/detection/notebook-basis.js.map +1 -0
- package/dist/detection/runtime.js +31 -5
- package/dist/detection/runtime.js.map +1 -1
- package/dist/user-ground-host.js +157 -59
- package/dist/user-ground-host.js.map +1 -1
- package/package.json +2 -2
package/dist/user-ground-host.js
CHANGED
|
@@ -15,6 +15,7 @@ import { ContentCacheDownload, captureContentFile, hashContentFile, sealCaptured
|
|
|
15
15
|
import { decodeContentWireBytes, encodeContentWireBytes } from "./content-wire-codec.js";
|
|
16
16
|
import { exactTextReplay, planGroundDetection, reconcileGroundUUIDs, } from "./detection/portable.js";
|
|
17
17
|
import { AcceptedInboxResultIndex } from "./detection/accepted-result.js";
|
|
18
|
+
import { sameNotebookBasisRow } from "./detection/notebook-basis.js";
|
|
18
19
|
import { NamedDetectRuntime } from "./detection/runtime.js";
|
|
19
20
|
import { WORKSPACE_UUID as UUID, createFilesRegisterPorts, ensureWorkspaceBinding, ensureWorkspaceReference, pathExists, pathWithin, referenceWorkspaceId, selectKnownRegistrationId, workspaceBindingDir, } from "./files-register-host.js";
|
|
20
21
|
import { applyRepositoryFiles, captureRepository, inspectRepositoryTransportFile, hasGitMarker, readCachedRepositoryIdentity, } from "./git-repository-host.js";
|
|
@@ -227,6 +228,7 @@ export class UserGroundHost {
|
|
|
227
228
|
findKnownWorkspaceId: ({ absolutePath, deviceNumber, inode }) => findKnownEntityId(this.databasePath(identity), absolutePath, deviceNumber, inode),
|
|
228
229
|
});
|
|
229
230
|
const add = {
|
|
231
|
+
sha256Hex,
|
|
230
232
|
lookupRegistry: async () => {
|
|
231
233
|
const started = performance.now();
|
|
232
234
|
this.stage({
|
|
@@ -526,12 +528,16 @@ export class UserGroundHost {
|
|
|
526
528
|
this.stage({
|
|
527
529
|
primitive: "register", stage: "entity-sqlite-commit", status: "started", workspaceId,
|
|
528
530
|
});
|
|
529
|
-
commitDetectedState(this.databasePath(identity), {
|
|
531
|
+
const committed = commitDetectedState(this.databasePath(identity), {
|
|
532
|
+
notebookBasis: scanned.notebookBasis,
|
|
530
533
|
acceptedMaterializedRows: scanned.acceptedMaterializedRows,
|
|
531
534
|
materializedRemovals: scanned.materializedRemovals,
|
|
532
535
|
acceptedNotebookRows: scanned.acceptedNotebookRows,
|
|
533
536
|
notebookRemovals: scanned.notebookRemovals,
|
|
534
537
|
});
|
|
538
|
+
if (!committed) {
|
|
539
|
+
throw new Error("verified ground advanced while registration was observing; retry");
|
|
540
|
+
}
|
|
535
541
|
this.stage({
|
|
536
542
|
primitive: "register",
|
|
537
543
|
stage: "entity-sqlite-commit",
|
|
@@ -1577,6 +1583,7 @@ export class UserGroundHost {
|
|
|
1577
1583
|
return;
|
|
1578
1584
|
}
|
|
1579
1585
|
let detection;
|
|
1586
|
+
let notebookBasis;
|
|
1580
1587
|
let acceptedMaterializedRows;
|
|
1581
1588
|
let materializedRemovals;
|
|
1582
1589
|
let acceptedNotebookRows;
|
|
@@ -1592,6 +1599,7 @@ export class UserGroundHost {
|
|
|
1592
1599
|
coreScan,
|
|
1593
1600
|
});
|
|
1594
1601
|
detection = scanned.detection;
|
|
1602
|
+
notebookBasis = scanned.notebookBasis;
|
|
1595
1603
|
acceptedMaterializedRows = scanned.acceptedMaterializedRows;
|
|
1596
1604
|
materializedRemovals = scanned.materializedRemovals;
|
|
1597
1605
|
acceptedNotebookRows = scanned.acceptedNotebookRows;
|
|
@@ -1615,13 +1623,17 @@ export class UserGroundHost {
|
|
|
1615
1623
|
}))
|
|
1616
1624
|
: []);
|
|
1617
1625
|
const recordCommitStarted = performance.now();
|
|
1618
|
-
commitDetectedState(this.databasePath(identity), {
|
|
1626
|
+
const committed = commitDetectedState(this.databasePath(identity), {
|
|
1627
|
+
notebookBasis,
|
|
1619
1628
|
acceptedMaterializedRows,
|
|
1620
1629
|
materializedRemovals,
|
|
1621
1630
|
acceptedNotebookRows,
|
|
1622
1631
|
notebookRemovals,
|
|
1623
1632
|
proposals: detectedRecords,
|
|
1624
1633
|
});
|
|
1634
|
+
if (!committed) {
|
|
1635
|
+
throw pipelineStageError("detection", new Error("the verified notebook advanced while Detect was observing; retrying fresh truth"));
|
|
1636
|
+
}
|
|
1625
1637
|
const recordCommitDuration = performance.now() - recordCommitStarted;
|
|
1626
1638
|
if (detectedRecords.length === 0) {
|
|
1627
1639
|
this.stage({
|
|
@@ -1666,7 +1678,8 @@ export class UserGroundHost {
|
|
|
1666
1678
|
}
|
|
1667
1679
|
}
|
|
1668
1680
|
async publishPendingSnapshotsBeforeLookup(identity, resourceId) {
|
|
1669
|
-
const pending = readSnapshotPublications(this.databasePath(identity))
|
|
1681
|
+
const pending = readSnapshotPublications(this.databasePath(identity))
|
|
1682
|
+
.filter((entry) => entry.status === "pending");
|
|
1670
1683
|
if (pending.length === 0)
|
|
1671
1684
|
return;
|
|
1672
1685
|
const first = pending[0];
|
|
@@ -1680,12 +1693,36 @@ export class UserGroundHost {
|
|
|
1680
1693
|
checksum: "",
|
|
1681
1694
|
records: snapshotFromRecords(JSON.parse(first.snapshotJson).records || []).records,
|
|
1682
1695
|
};
|
|
1683
|
-
|
|
1696
|
+
try {
|
|
1697
|
+
await this.publishPendingSnapshots(identity);
|
|
1698
|
+
}
|
|
1699
|
+
catch (error) {
|
|
1700
|
+
// A conclusive refusal settles the intent as rejected: the lookup that
|
|
1701
|
+
// follows fetches authoritative state and a later register supersedes
|
|
1702
|
+
// the settled row, so a refused publication never wedges startup. An
|
|
1703
|
+
// unknown outcome must keep blocking — the mutation may have landed.
|
|
1704
|
+
if (!conclusivelyRefused(error))
|
|
1705
|
+
throw error;
|
|
1706
|
+
this.cloudState = null;
|
|
1707
|
+
}
|
|
1684
1708
|
}
|
|
1685
1709
|
/** Register is the deliberate cold graph-publication boundary. Ordinary
|
|
1686
1710
|
* Watch detection stops at a durable exact record and never rebuilds this
|
|
1687
1711
|
* resource snapshot. */
|
|
1688
1712
|
async publishMaterializedSnapshot(identity, publicationRootIds, trace) {
|
|
1713
|
+
// Any durable pending intent resolves before a new snapshot derives: an
|
|
1714
|
+
// acknowledged publication advances the head through its original
|
|
1715
|
+
// mutation id (the authority answers a known mutation id idempotently),
|
|
1716
|
+
// and a conclusive refusal marks the row rejected so the snapshot
|
|
1717
|
+
// derived below may supersede it. A transport failure keeps the row
|
|
1718
|
+
// pending — its outcome is unknown, so nothing may replace it.
|
|
1719
|
+
try {
|
|
1720
|
+
await this.publishPendingSnapshots(identity, trace);
|
|
1721
|
+
}
|
|
1722
|
+
catch (error) {
|
|
1723
|
+
if (!conclusivelyRefused(error))
|
|
1724
|
+
throw error;
|
|
1725
|
+
}
|
|
1689
1726
|
const state = this.cloudState;
|
|
1690
1727
|
if (!state)
|
|
1691
1728
|
throw new Error("cloud state is unavailable for graph publication");
|
|
@@ -1693,7 +1730,7 @@ export class UserGroundHost {
|
|
|
1693
1730
|
.map(portableRecord), readGroundRowsForRoots(this.databasePath(identity), "detection_notebook", publicationRootIds)
|
|
1694
1731
|
.map(portableRecord));
|
|
1695
1732
|
const merged = mergeMaterializedRoots(state.records, localRecords);
|
|
1696
|
-
const traveling = travelingRecords(merged);
|
|
1733
|
+
const traveling = travelingRecords(merged, sha256Hex);
|
|
1697
1734
|
const impossibleGitLeaf = traveling.find((record) => (record.type === "file.text" || record.type === "file.binary" || record.type === "link")
|
|
1698
1735
|
&& record.payloadVersion?.startsWith("git:"));
|
|
1699
1736
|
if (impossibleGitLeaf) {
|
|
@@ -1705,16 +1742,6 @@ export class UserGroundHost {
|
|
|
1705
1742
|
const checksum = sha256Hex(snapshotJson);
|
|
1706
1743
|
if (checksum === state.checksum)
|
|
1707
1744
|
return;
|
|
1708
|
-
const pending = readSnapshotPublications(this.databasePath(identity));
|
|
1709
|
-
const identical = pending.find((entry) => entry.resourceId === state.resourceId
|
|
1710
|
-
&& entry.snapshotChecksum === checksum);
|
|
1711
|
-
if (identical) {
|
|
1712
|
-
await this.publishPendingSnapshots(identity, trace);
|
|
1713
|
-
return;
|
|
1714
|
-
}
|
|
1715
|
-
if (pending.length > 0) {
|
|
1716
|
-
throw new Error("a different graph snapshot is already pending publication");
|
|
1717
|
-
}
|
|
1718
1745
|
const localIds = new Set(localRecords.map((record) => record.uuid));
|
|
1719
1746
|
const replacementRootIds = localRecords
|
|
1720
1747
|
.filter((record) => record.parentUUID === null)
|
|
@@ -1736,7 +1763,7 @@ export class UserGroundHost {
|
|
|
1736
1763
|
const artifact = artifactForRecord(record);
|
|
1737
1764
|
return artifact !== null && !publishedArtifacts.has(artifact.contentHash);
|
|
1738
1765
|
});
|
|
1739
|
-
|
|
1766
|
+
supersedeSnapshotJournal(this.databasePath(identity), {
|
|
1740
1767
|
mutationId: randomUUID(),
|
|
1741
1768
|
resourceId: state.resourceId,
|
|
1742
1769
|
authorityEpoch: state.authorityEpoch,
|
|
@@ -1746,11 +1773,17 @@ export class UserGroundHost {
|
|
|
1746
1773
|
uploadRecords,
|
|
1747
1774
|
replacementRootIds: replacement.rootIds,
|
|
1748
1775
|
replacementRecords: replacement.records,
|
|
1776
|
+
status: "pending",
|
|
1749
1777
|
});
|
|
1750
1778
|
await this.publishPendingSnapshots(identity, trace);
|
|
1751
1779
|
}
|
|
1752
1780
|
async publishPendingSnapshots(identity, trace) {
|
|
1753
1781
|
for (const pending of readSnapshotPublications(this.databasePath(identity))) {
|
|
1782
|
+
// A rejected row is a settled fact, not an intent: the authority
|
|
1783
|
+
// conclusively refused this exact mutation, and only a newer complete
|
|
1784
|
+
// snapshot superseding it can publish this resource again.
|
|
1785
|
+
if (pending.status === "rejected")
|
|
1786
|
+
continue;
|
|
1754
1787
|
const snapshot = snapshotFromRecords(JSON.parse(pending.snapshotJson).records || []);
|
|
1755
1788
|
const replacement = rootReplacementFromRecords(pending.replacementRootIds, pending.replacementRecords);
|
|
1756
1789
|
try {
|
|
@@ -1796,6 +1829,13 @@ export class UserGroundHost {
|
|
|
1796
1829
|
durationMs: performance.now() - graphCommitStarted,
|
|
1797
1830
|
});
|
|
1798
1831
|
}
|
|
1832
|
+
// A non-retryable error frame is the authority's answer: this exact
|
|
1833
|
+
// mutation is refused forever. Settle the row so it is never
|
|
1834
|
+
// replayed and a newer snapshot may supersede it. Every other
|
|
1835
|
+
// failure leaves the outcome unknown and the row pending.
|
|
1836
|
+
if (error instanceof WireRequestError && error.retryable === false) {
|
|
1837
|
+
markSnapshotJournalRejected(this.databasePath(identity), pending.mutationId);
|
|
1838
|
+
}
|
|
1799
1839
|
throw pipelineStageError("cloud graph commit", error);
|
|
1800
1840
|
}
|
|
1801
1841
|
if (trace?.journey === "register") {
|
|
@@ -2006,7 +2046,8 @@ function initializeDatabase(file) {
|
|
|
2006
2046
|
mutation_id TEXT NOT NULL UNIQUE,
|
|
2007
2047
|
resource_id TEXT NOT NULL,
|
|
2008
2048
|
publication_json TEXT NOT NULL,
|
|
2009
|
-
created_at INTEGER NOT NULL
|
|
2049
|
+
created_at INTEGER NOT NULL,
|
|
2050
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'rejected'))
|
|
2010
2051
|
);
|
|
2011
2052
|
CREATE TABLE IF NOT EXISTS workspace_add_intents (
|
|
2012
2053
|
workspace_uuid TEXT PRIMARY KEY,
|
|
@@ -2041,6 +2082,10 @@ function initializeDatabase(file) {
|
|
|
2041
2082
|
if (!inboxColumns.has("resolved_at")) {
|
|
2042
2083
|
database.exec("ALTER TABLE record_inbox ADD COLUMN resolved_at INTEGER");
|
|
2043
2084
|
}
|
|
2085
|
+
const publicationColumns = new Set(database.prepare("PRAGMA table_info(snapshot_publications)").all().map(({ name }) => name));
|
|
2086
|
+
if (!publicationColumns.has("status")) {
|
|
2087
|
+
database.exec("ALTER TABLE snapshot_publications ADD COLUMN status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'rejected'))");
|
|
2088
|
+
}
|
|
2044
2089
|
database.exec("DROP INDEX IF EXISTS record_inbox_delivery");
|
|
2045
2090
|
database.exec("DROP TABLE IF EXISTS mutation_journal");
|
|
2046
2091
|
migratedDatabases.add(file);
|
|
@@ -2414,7 +2459,7 @@ function readSnapshotPublications(file) {
|
|
|
2414
2459
|
try {
|
|
2415
2460
|
return database.prepare(`
|
|
2416
2461
|
SELECT mutation_id AS mutationId, resource_id AS resourceId,
|
|
2417
|
-
publication_json AS publicationJson
|
|
2462
|
+
publication_json AS publicationJson, status
|
|
2418
2463
|
FROM snapshot_publications ORDER BY sequence
|
|
2419
2464
|
`).all().map((row) => {
|
|
2420
2465
|
const publication = JSON.parse(row.publicationJson);
|
|
@@ -2435,6 +2480,7 @@ function readSnapshotPublications(file) {
|
|
|
2435
2480
|
uploadRecords: snapshotFromRecords(publication.uploadRecords).records,
|
|
2436
2481
|
replacementRootIds: publication.replacementRootIds.map(String),
|
|
2437
2482
|
replacementRecords: snapshotFromRecords(publication.replacementRecords).records,
|
|
2483
|
+
status: row.status === "rejected" ? "rejected" : "pending",
|
|
2438
2484
|
};
|
|
2439
2485
|
});
|
|
2440
2486
|
}
|
|
@@ -2461,7 +2507,25 @@ function hasRecordsBeyondSnapshot(file) {
|
|
|
2461
2507
|
function commitDetectedState(file, input) {
|
|
2462
2508
|
const database = initializeDatabase(file);
|
|
2463
2509
|
try {
|
|
2464
|
-
database.transaction(() => {
|
|
2510
|
+
return database.transaction(() => {
|
|
2511
|
+
const basisRows = input.notebookBasis ?? [];
|
|
2512
|
+
const currentByUuid = new Map();
|
|
2513
|
+
for (let offset = 0; offset < basisRows.length; offset += 500) {
|
|
2514
|
+
const batch = basisRows.slice(offset, offset + 500);
|
|
2515
|
+
const placeholders = batch.map(() => "?").join(", ");
|
|
2516
|
+
const rows = database.prepare(`
|
|
2517
|
+
SELECT ${GROUND_ROW_PROJECTION} FROM detection_notebook
|
|
2518
|
+
WHERE uuid IN (${placeholders})
|
|
2519
|
+
`).all(...batch.map(({ uuid }) => uuid));
|
|
2520
|
+
for (const row of rows)
|
|
2521
|
+
currentByUuid.set(row.uuid, row);
|
|
2522
|
+
}
|
|
2523
|
+
for (const basis of basisRows) {
|
|
2524
|
+
const current = currentByUuid.get(basis.uuid);
|
|
2525
|
+
if (basis.row ? !sameNotebookBasisRow(basis.row, current) : current !== undefined) {
|
|
2526
|
+
return false;
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2465
2529
|
const acceptedInbox = new AcceptedInboxResultIndex(database);
|
|
2466
2530
|
const append = database.prepare(`
|
|
2467
2531
|
INSERT INTO record_outbox(
|
|
@@ -2503,33 +2567,75 @@ function commitDetectedState(file, input) {
|
|
|
2503
2567
|
for (const row of input.acceptedNotebookRows) {
|
|
2504
2568
|
runGroundUpsert(database, "detection_notebook", upsertNotebook, row);
|
|
2505
2569
|
}
|
|
2570
|
+
return true;
|
|
2506
2571
|
})();
|
|
2507
2572
|
}
|
|
2508
2573
|
finally {
|
|
2509
2574
|
database.close();
|
|
2510
2575
|
}
|
|
2511
2576
|
}
|
|
2512
|
-
|
|
2577
|
+
/** Journal the newest snapshot publication for its resource. A journal row
|
|
2578
|
+
* has exactly two meanings: `pending` — the submit outcome is unknown, so
|
|
2579
|
+
* the same mutation id must retry until the authority answers (the mutation
|
|
2580
|
+
* may already be accepted with only the acknowledgement lost); `rejected` —
|
|
2581
|
+
* the authority conclusively refused it, so it is never replayed. Only a
|
|
2582
|
+
* rejected row may be superseded — last write wins — and supersession and
|
|
2583
|
+
* insertion share one transaction, so a crash leaves exactly one durable
|
|
2584
|
+
* intent. Content uploads are content-addressed and idempotent, so a
|
|
2585
|
+
* superseded row loses no uploaded work. */
|
|
2586
|
+
function supersedeSnapshotJournal(file, entry) {
|
|
2513
2587
|
const database = initializeDatabase(file);
|
|
2514
2588
|
try {
|
|
2515
|
-
database.
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2589
|
+
database.transaction(() => {
|
|
2590
|
+
const unresolved = database.prepare(`
|
|
2591
|
+
SELECT 1 AS present FROM snapshot_publications
|
|
2592
|
+
WHERE resource_id = ? AND status = 'pending' LIMIT 1
|
|
2593
|
+
`).get(entry.resourceId);
|
|
2594
|
+
if (unresolved) {
|
|
2595
|
+
throw new Error("a pending publication with an unknown outcome must resolve before supersession");
|
|
2596
|
+
}
|
|
2597
|
+
database.prepare("DELETE FROM snapshot_publications WHERE resource_id = ? AND status = 'rejected'")
|
|
2598
|
+
.run(entry.resourceId);
|
|
2599
|
+
database.prepare(`
|
|
2600
|
+
INSERT INTO snapshot_publications(
|
|
2601
|
+
mutation_id, resource_id, publication_json, created_at
|
|
2602
|
+
) VALUES (?, ?, ?, ?)
|
|
2603
|
+
`).run(entry.mutationId, entry.resourceId, stableJson({
|
|
2604
|
+
authorityEpoch: entry.authorityEpoch,
|
|
2605
|
+
baseVersion: entry.baseVersion,
|
|
2606
|
+
snapshotChecksum: entry.snapshotChecksum,
|
|
2607
|
+
snapshotJson: entry.snapshotJson,
|
|
2608
|
+
uploadRecords: entry.uploadRecords,
|
|
2609
|
+
replacementRootIds: entry.replacementRootIds,
|
|
2610
|
+
replacementRecords: entry.replacementRecords,
|
|
2611
|
+
}), Date.now());
|
|
2612
|
+
})();
|
|
2528
2613
|
}
|
|
2529
2614
|
finally {
|
|
2530
2615
|
database.close();
|
|
2531
2616
|
}
|
|
2532
2617
|
}
|
|
2618
|
+
/** Settle a journal row as conclusively refused. It is never replayed again;
|
|
2619
|
+
* a newer complete snapshot supersedes it on the next publication. */
|
|
2620
|
+
function markSnapshotJournalRejected(file, mutationId) {
|
|
2621
|
+
const database = initializeDatabase(file);
|
|
2622
|
+
try {
|
|
2623
|
+
database.prepare("UPDATE snapshot_publications SET status = 'rejected' WHERE mutation_id = ?")
|
|
2624
|
+
.run(mutationId);
|
|
2625
|
+
}
|
|
2626
|
+
finally {
|
|
2627
|
+
database.close();
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
/** True when the authority itself answered that this exact mutation is
|
|
2631
|
+
* refused (a non-retryable error frame), as opposed to a transport or
|
|
2632
|
+
* pipeline failure whose outcome is unknown. Stage wrappers carry the wire
|
|
2633
|
+
* error as their cause. */
|
|
2634
|
+
function conclusivelyRefused(error) {
|
|
2635
|
+
if (error instanceof WireRequestError)
|
|
2636
|
+
return error.retryable === false;
|
|
2637
|
+
return error instanceof Error && conclusivelyRefused(error.cause);
|
|
2638
|
+
}
|
|
2533
2639
|
function deleteJournalEntry(file, mutationId) {
|
|
2534
2640
|
const database = initializeDatabase(file);
|
|
2535
2641
|
try {
|
|
@@ -2605,28 +2711,6 @@ function capturedGroundRow(resourceId, rootUUID, row) {
|
|
|
2605
2711
|
contentVerifiedAtMs: row.contentVerifiedAtMs,
|
|
2606
2712
|
};
|
|
2607
2713
|
}
|
|
2608
|
-
function sameGroundRow(left, right) {
|
|
2609
|
-
return right !== undefined
|
|
2610
|
-
&& left.resourceId === right.resourceId
|
|
2611
|
-
&& left.rootUUID === right.rootUUID
|
|
2612
|
-
&& left.uuid === right.uuid
|
|
2613
|
-
&& left.type === right.type
|
|
2614
|
-
&& left.parentUUID === right.parentUUID
|
|
2615
|
-
&& left.name === right.name
|
|
2616
|
-
&& left.status === right.status
|
|
2617
|
-
&& left.version === right.version
|
|
2618
|
-
&& left.payloadVersion === right.payloadVersion
|
|
2619
|
-
&& left.transportVersion === right.transportVersion
|
|
2620
|
-
&& left.relativePath === right.relativePath
|
|
2621
|
-
&& left.absolutePath === right.absolutePath
|
|
2622
|
-
&& left.deviceNumber === right.deviceNumber
|
|
2623
|
-
&& left.inode === right.inode
|
|
2624
|
-
&& left.byteSize === right.byteSize
|
|
2625
|
-
&& left.modifiedTimeMs === right.modifiedTimeMs
|
|
2626
|
-
&& left.changedTimeMs === right.changedTimeMs
|
|
2627
|
-
&& left.filesystemMode === right.filesystemMode
|
|
2628
|
-
&& left.contentVerifiedAtMs === right.contentVerifiedAtMs;
|
|
2629
|
-
}
|
|
2630
2714
|
function groundRowValues(row) {
|
|
2631
2715
|
return [
|
|
2632
2716
|
row.uuid, row.resourceId, row.rootUUID, row.type, row.parentUUID, row.name, row.status,
|
|
@@ -2975,7 +3059,8 @@ async function reconcileGround(input) {
|
|
|
2975
3059
|
? readMaterializedRowsWithoutNotebook(database, selectedRoots ?? undefined)
|
|
2976
3060
|
: [];
|
|
2977
3061
|
const existing = suspicions ? [...notebook, ...unverifiedIdentity] : materialized;
|
|
2978
|
-
const
|
|
3062
|
+
const observedNotebookByUuid = new Map(notebook.map((row) => [row.uuid, row]));
|
|
3063
|
+
const notebookByUuid = new Map(observedNotebookByUuid);
|
|
2979
3064
|
const materializedBaseline = suspicions
|
|
2980
3065
|
? existing.filter((row) => materializedUUIDs.has(row.uuid))
|
|
2981
3066
|
: materialized;
|
|
@@ -3034,11 +3119,11 @@ async function reconcileGround(input) {
|
|
|
3034
3119
|
}
|
|
3035
3120
|
for (const row of result.rows) {
|
|
3036
3121
|
const captured = capturedGroundRow(parameters.resourceId, parameters.rootUUID, row);
|
|
3037
|
-
if (!
|
|
3122
|
+
if (!sameNotebookBasisRow(captured, materializedByUuid.get(captured.uuid))) {
|
|
3038
3123
|
acceptedMaterializedRows.push(captured);
|
|
3039
3124
|
}
|
|
3040
3125
|
if (result.transferable) {
|
|
3041
|
-
if (
|
|
3126
|
+
if (sameNotebookBasisRow(captured, notebookByUuid.get(captured.uuid)))
|
|
3042
3127
|
continue;
|
|
3043
3128
|
acceptedNotebookRows.push(captured);
|
|
3044
3129
|
notebookByUuid.set(captured.uuid, captured);
|
|
@@ -3122,8 +3207,21 @@ async function reconcileGround(input) {
|
|
|
3122
3207
|
suspicion: observationFor(rootPath),
|
|
3123
3208
|
});
|
|
3124
3209
|
}
|
|
3210
|
+
const changedUUIDs = new Set([
|
|
3211
|
+
...acceptedMaterializedRows.map((row) => row.uuid),
|
|
3212
|
+
...materializedRemovals,
|
|
3213
|
+
...acceptedNotebookRows.map((row) => row.uuid),
|
|
3214
|
+
...notebookRemovals,
|
|
3215
|
+
...detection.flatMap((plan) => plan.kind === "ready"
|
|
3216
|
+
? plan.proposals.map((proposal) => proposal.entityId)
|
|
3217
|
+
: []),
|
|
3218
|
+
]);
|
|
3125
3219
|
return {
|
|
3126
3220
|
detection,
|
|
3221
|
+
notebookBasis: [...changedUUIDs].sort().map((uuid) => ({
|
|
3222
|
+
uuid,
|
|
3223
|
+
row: observedNotebookByUuid.get(uuid) ?? null,
|
|
3224
|
+
})),
|
|
3127
3225
|
acceptedMaterializedRows,
|
|
3128
3226
|
materializedRemovals: [...materializedRemovals],
|
|
3129
3227
|
acceptedNotebookRows,
|
|
@@ -4386,7 +4484,7 @@ async function installCloudWorkspace(input) {
|
|
|
4386
4484
|
}
|
|
4387
4485
|
const expectedRecords = pending?.records ?? records;
|
|
4388
4486
|
const localRecords = existing.map(portableRecord);
|
|
4389
|
-
if (!sameRecords(travelingRecords(localRecords), expectedRecords)) {
|
|
4487
|
+
if (!sameRecords(travelingRecords(localRecords, sha256Hex), expectedRecords)) {
|
|
4390
4488
|
throw new Error(`local workspace ${workspace.uuid} differs from its cloud graph`);
|
|
4391
4489
|
}
|
|
4392
4490
|
if (pending && root.absolutePath !== pending.destinationPath) {
|