@plinth-music/cli 0.22.0 → 0.23.1
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/CHANGELOG.md +77 -0
- package/dist/build-stamp.json +2 -2
- package/dist/cli.js +2381 -248
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -7584,7 +7584,7 @@ function _getBuiltinFlags(long, short, userNames, userAliases) {
|
|
|
7584
7584
|
// package.json
|
|
7585
7585
|
var package_default = {
|
|
7586
7586
|
name: "@plinth-music/cli",
|
|
7587
|
-
version: "0.
|
|
7587
|
+
version: "0.23.1",
|
|
7588
7588
|
description: "Plinth capstone sync daemon and CLI",
|
|
7589
7589
|
type: "module",
|
|
7590
7590
|
private: false,
|
|
@@ -22148,12 +22148,28 @@ var BASE_ROW = {
|
|
|
22148
22148
|
};
|
|
22149
22149
|
var TombstoneSchema = exports_external.object({
|
|
22150
22150
|
...BASE_ROW,
|
|
22151
|
-
deleted_at: exports_external.string().datetime({ offset: true })
|
|
22151
|
+
deleted_at: exports_external.string().datetime({ offset: true }),
|
|
22152
|
+
permanent_delete: exports_external.literal(true).optional(),
|
|
22153
|
+
purge_generation: exports_external.string().uuid().optional(),
|
|
22154
|
+
retirement_sequence: exports_external.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional()
|
|
22155
|
+
}).superRefine((row, ctx) => {
|
|
22156
|
+
const present = [
|
|
22157
|
+
row.permanent_delete !== undefined,
|
|
22158
|
+
row.purge_generation !== undefined,
|
|
22159
|
+
row.retirement_sequence !== undefined
|
|
22160
|
+
];
|
|
22161
|
+
if (present.some(Boolean) && !present.every(Boolean)) {
|
|
22162
|
+
ctx.addIssue({
|
|
22163
|
+
code: "custom",
|
|
22164
|
+
message: "permanent tombstone fields must be present together"
|
|
22165
|
+
});
|
|
22166
|
+
}
|
|
22152
22167
|
});
|
|
22153
22168
|
function makePageResponseSchema(responseKey, liveSchema) {
|
|
22154
22169
|
return exports_external.object({
|
|
22155
22170
|
[responseKey]: exports_external.array(exports_external.union([liveSchema, TombstoneSchema])),
|
|
22156
|
-
next_cursor: exports_external.string().nullable()
|
|
22171
|
+
next_cursor: exports_external.string().nullable(),
|
|
22172
|
+
high_water: exports_external.string().nullable().optional()
|
|
22157
22173
|
});
|
|
22158
22174
|
}
|
|
22159
22175
|
function makeParsePageResponse(schema, responseKey) {
|
|
@@ -22161,7 +22177,8 @@ function makeParsePageResponse(schema, responseKey) {
|
|
|
22161
22177
|
const parsed = schema.parse(json2);
|
|
22162
22178
|
return {
|
|
22163
22179
|
rows: parsed[responseKey],
|
|
22164
|
-
next_cursor: parsed.next_cursor
|
|
22180
|
+
next_cursor: parsed.next_cursor,
|
|
22181
|
+
high_water: parsed.high_water ?? null
|
|
22165
22182
|
};
|
|
22166
22183
|
};
|
|
22167
22184
|
}
|
|
@@ -23893,6 +23910,17 @@ function moveEntityFile(workspaceSlug, entityType, fromSlug, toSlug, beforeMove)
|
|
|
23893
23910
|
throw err;
|
|
23894
23911
|
}
|
|
23895
23912
|
}
|
|
23913
|
+
function deleteMirrorFile(workspaceSlug, entityType, entitySlug) {
|
|
23914
|
+
const target = entityFilePath(workspaceSlug, entityType, entitySlug);
|
|
23915
|
+
try {
|
|
23916
|
+
unlinkSync(target);
|
|
23917
|
+
return true;
|
|
23918
|
+
} catch (err) {
|
|
23919
|
+
if (err?.code === "ENOENT")
|
|
23920
|
+
return false;
|
|
23921
|
+
throw err;
|
|
23922
|
+
}
|
|
23923
|
+
}
|
|
23896
23924
|
var ARTIST_MEMORY_ANCHOR = "memory.md";
|
|
23897
23925
|
function artistMemoryFilePath(workspaceSlug, entitySlug) {
|
|
23898
23926
|
assertSlug("entity slug", entitySlug);
|
|
@@ -25319,7 +25347,8 @@ var WorkspaceEntrySchema = exports_external.object({
|
|
|
25319
25347
|
last_synced_at: exports_external.string().datetime({ offset: true }).nullable(),
|
|
25320
25348
|
workspace_name: exports_external.string().min(1).optional(),
|
|
25321
25349
|
member: MemberPrincipalSchema.optional(),
|
|
25322
|
-
sync_cursors: SyncCursorsSchema.optional()
|
|
25350
|
+
sync_cursors: SyncCursorsSchema.optional(),
|
|
25351
|
+
sync_high_water: SyncCursorsSchema.optional()
|
|
25323
25352
|
});
|
|
25324
25353
|
var ConfigFileSchema = exports_external.object({
|
|
25325
25354
|
version: exports_external.literal(1),
|
|
@@ -25381,7 +25410,8 @@ function carryForwardSyncState(prior, entry) {
|
|
|
25381
25410
|
return {
|
|
25382
25411
|
...entry,
|
|
25383
25412
|
last_synced_at: prior.last_synced_at,
|
|
25384
|
-
...prior.sync_cursors !== undefined ? { sync_cursors: prior.sync_cursors } : {}
|
|
25413
|
+
...prior.sync_cursors !== undefined ? { sync_cursors: prior.sync_cursors } : {},
|
|
25414
|
+
...prior.sync_high_water !== undefined ? { sync_high_water: prior.sync_high_water } : {}
|
|
25385
25415
|
};
|
|
25386
25416
|
}
|
|
25387
25417
|
function upsertWorkspace(slug, entry, options2 = {}) {
|
|
@@ -25422,6 +25452,23 @@ function updateSyncCursor(slug, entityType, cursor) {
|
|
|
25422
25452
|
writeAtomic(next);
|
|
25423
25453
|
return next;
|
|
25424
25454
|
}
|
|
25455
|
+
function updateSyncHighWater(slug, entityType, mark) {
|
|
25456
|
+
const existing = loadConfig();
|
|
25457
|
+
if (!existing) {
|
|
25458
|
+
throw new Error(`No config. Run \`plinth login --workspace ${slug}\` first.`);
|
|
25459
|
+
}
|
|
25460
|
+
const entry = requireWorkspace(existing, slug);
|
|
25461
|
+
if ((entry.sync_high_water?.[entityType] ?? null) === mark)
|
|
25462
|
+
return;
|
|
25463
|
+
const nextEntry = {
|
|
25464
|
+
...entry,
|
|
25465
|
+
sync_high_water: { ...entry.sync_high_water, [entityType]: mark }
|
|
25466
|
+
};
|
|
25467
|
+
writeAtomic({
|
|
25468
|
+
...existing,
|
|
25469
|
+
workspaces: { ...existing.workspaces, [slug]: nextEntry }
|
|
25470
|
+
});
|
|
25471
|
+
}
|
|
25425
25472
|
function setMemberPrincipal(slug, member) {
|
|
25426
25473
|
const existing = loadConfig();
|
|
25427
25474
|
if (!existing)
|
|
@@ -26709,7 +26756,7 @@ function allFileEntries(state) {
|
|
|
26709
26756
|
return Object.values(state.files ?? {});
|
|
26710
26757
|
}
|
|
26711
26758
|
function fileEntriesEqual(a, b) {
|
|
26712
|
-
return a.id === b.id && a.path === b.path && a.content_hash === b.content_hash && a.size_bytes === b.size_bytes && a.mime_type === b.mime_type && a.updated_at === b.updated_at && a.inode === b.inode;
|
|
26759
|
+
return a.id === b.id && a.path === b.path && a.content_hash === b.content_hash && a.size_bytes === b.size_bytes && a.mime_type === b.mime_type && a.updated_at === b.updated_at && a.inode === b.inode && a.text_hash === b.text_hash;
|
|
26713
26760
|
}
|
|
26714
26761
|
function clearFrontmatterIdBackfillMarker(state) {
|
|
26715
26762
|
if (state.frontmatter_id_backfill_version === undefined) {
|
|
@@ -29609,6 +29656,13 @@ class ApiError extends Error {
|
|
|
29609
29656
|
this.name = "ApiError";
|
|
29610
29657
|
}
|
|
29611
29658
|
}
|
|
29659
|
+
var PermanentNameRetiredSchema = exports_external.object({
|
|
29660
|
+
code: exports_external.literal("permanent_name_retired"),
|
|
29661
|
+
entity_kind: exports_external.enum(["document", "workspace_file"]),
|
|
29662
|
+
logical_key: exports_external.string().min(1),
|
|
29663
|
+
action: exports_external.literal("refresh_required"),
|
|
29664
|
+
error: exports_external.string().optional()
|
|
29665
|
+
});
|
|
29612
29666
|
function parseRetryAfterMs(headerValue, now = () => Date.now()) {
|
|
29613
29667
|
if (!headerValue)
|
|
29614
29668
|
return;
|
|
@@ -29622,6 +29676,92 @@ function parseRetryAfterMs(headerValue, now = () => Date.now()) {
|
|
|
29622
29676
|
const delta = dateMs - now();
|
|
29623
29677
|
return delta > 0 ? delta : 0;
|
|
29624
29678
|
}
|
|
29679
|
+
var OperationEnvelopeBase = exports_external.object({
|
|
29680
|
+
entity_kind: exports_external.enum(["document", "workspace_file"]),
|
|
29681
|
+
operation_id: exports_external.string().uuid()
|
|
29682
|
+
});
|
|
29683
|
+
var OperationBoundBase = OperationEnvelopeBase.extend({
|
|
29684
|
+
logical_key: exports_external.string().min(1),
|
|
29685
|
+
payload_digest: exports_external.string().regex(/^[0-9a-f]{64}$/)
|
|
29686
|
+
});
|
|
29687
|
+
var OperationResolutionSchema = exports_external.discriminatedUnion("status", [
|
|
29688
|
+
OperationBoundBase.extend({ status: exports_external.literal("pending") }),
|
|
29689
|
+
OperationBoundBase.extend({ status: exports_external.literal("finalized"), live: exports_external.unknown() }),
|
|
29690
|
+
OperationBoundBase.extend({ status: exports_external.literal("terminal_cleaned") }),
|
|
29691
|
+
OperationEnvelopeBase.extend({ status: exports_external.literal("retired_not_found") })
|
|
29692
|
+
]);
|
|
29693
|
+
var CurrentNamespaceSchema = exports_external.object({
|
|
29694
|
+
entity_kind: exports_external.enum(["document", "workspace_file"]),
|
|
29695
|
+
logical_key: exports_external.string().min(1),
|
|
29696
|
+
latest_retirement: exports_external.object({
|
|
29697
|
+
purge_generation: exports_external.string().uuid(),
|
|
29698
|
+
retirement_sequence: exports_external.number().int().positive().max(Number.MAX_SAFE_INTEGER)
|
|
29699
|
+
}).nullable(),
|
|
29700
|
+
live_owner: exports_external.object({ id: exports_external.string().uuid() }).nullable()
|
|
29701
|
+
});
|
|
29702
|
+
async function resolvePermanentDeletionOperation(args) {
|
|
29703
|
+
const { baseUrl, pat, entityKind, operationId, fetchImpl = fetch } = args;
|
|
29704
|
+
const url2 = `${baseUrl.replace(/\/$/, "")}/api/sync/permanent-deletions/operations/resolve`;
|
|
29705
|
+
let res;
|
|
29706
|
+
try {
|
|
29707
|
+
res = await fetchImpl(url2, {
|
|
29708
|
+
method: "POST",
|
|
29709
|
+
headers: {
|
|
29710
|
+
Authorization: `Bearer ${pat}`,
|
|
29711
|
+
Accept: "application/json",
|
|
29712
|
+
"Content-Type": "application/json"
|
|
29713
|
+
},
|
|
29714
|
+
body: JSON.stringify({ entity_kind: entityKind, operation_id: operationId })
|
|
29715
|
+
});
|
|
29716
|
+
} catch (error52) {
|
|
29717
|
+
const detail = error52 instanceof Error ? error52.message : "network error";
|
|
29718
|
+
throw new ApiError(0, `Could not reach ${url2}: ${detail}`);
|
|
29719
|
+
}
|
|
29720
|
+
if (res.status === 401)
|
|
29721
|
+
throw new ApiError(401, "Token was rejected by the server.");
|
|
29722
|
+
if (res.status === 429) {
|
|
29723
|
+
throw new ApiError(429, "Rate limited by the server.", parseRetryAfterMs(res.headers.get("retry-after")));
|
|
29724
|
+
}
|
|
29725
|
+
if (!res.ok) {
|
|
29726
|
+
throw new ApiError(res.status, `resolve permanent deletion operation failed with HTTP ${res.status}`);
|
|
29727
|
+
}
|
|
29728
|
+
const parsed = OperationResolutionSchema.safeParse(await res.json());
|
|
29729
|
+
if (!parsed.success) {
|
|
29730
|
+
throw new ApiError(res.status, `operation resolution response did not match the expected shape: ${parsed.error.message}`);
|
|
29731
|
+
}
|
|
29732
|
+
return parsed.data;
|
|
29733
|
+
}
|
|
29734
|
+
async function getCurrentPermanentNamespace(args) {
|
|
29735
|
+
const { baseUrl, pat, entityKind, logicalKey, fetchImpl = fetch } = args;
|
|
29736
|
+
const params = new URLSearchParams({ entity_kind: entityKind, logical_key: logicalKey });
|
|
29737
|
+
const url2 = `${baseUrl.replace(/\/$/, "")}/api/sync/permanent-deletions/namespaces/current?${params}`;
|
|
29738
|
+
let res;
|
|
29739
|
+
try {
|
|
29740
|
+
res = await fetchImpl(url2, {
|
|
29741
|
+
method: "GET",
|
|
29742
|
+
headers: { Authorization: `Bearer ${pat}`, Accept: "application/json" }
|
|
29743
|
+
});
|
|
29744
|
+
} catch (error52) {
|
|
29745
|
+
const detail = error52 instanceof Error ? error52.message : "network error";
|
|
29746
|
+
throw new ApiError(0, `Could not reach ${url2}: ${detail}`);
|
|
29747
|
+
}
|
|
29748
|
+
if (res.status === 401)
|
|
29749
|
+
throw new ApiError(401, "Token was rejected by the server.");
|
|
29750
|
+
if (res.status === 429) {
|
|
29751
|
+
throw new ApiError(429, "Rate limited by the server.", parseRetryAfterMs(res.headers.get("retry-after")));
|
|
29752
|
+
}
|
|
29753
|
+
if (!res.ok) {
|
|
29754
|
+
throw new ApiError(res.status, `get current permanent namespace failed with HTTP ${res.status}`);
|
|
29755
|
+
}
|
|
29756
|
+
const parsed = CurrentNamespaceSchema.safeParse(await res.json());
|
|
29757
|
+
if (!parsed.success) {
|
|
29758
|
+
throw new ApiError(res.status, `current permanent namespace response did not match the expected shape: ${parsed.error.message}`);
|
|
29759
|
+
}
|
|
29760
|
+
if (parsed.data.entity_kind !== entityKind || parsed.data.logical_key !== logicalKey) {
|
|
29761
|
+
throw new ApiError(res.status, "current permanent namespace response identity mismatch");
|
|
29762
|
+
}
|
|
29763
|
+
return parsed.data;
|
|
29764
|
+
}
|
|
29625
29765
|
async function whoami(baseUrl, pat, fetchImpl = fetch) {
|
|
29626
29766
|
const url2 = `${baseUrl.replace(/\/$/, "")}/api/auth/cli/whoami`;
|
|
29627
29767
|
let res;
|
|
@@ -29797,6 +29937,16 @@ async function patchEntity(args) {
|
|
|
29797
29937
|
}
|
|
29798
29938
|
if (res.status === 409) {
|
|
29799
29939
|
const json2 = await res.json();
|
|
29940
|
+
const retiredParse = PermanentNameRetiredSchema.safeParse(json2);
|
|
29941
|
+
if (retiredParse.success) {
|
|
29942
|
+
return {
|
|
29943
|
+
ok: false,
|
|
29944
|
+
code: 409,
|
|
29945
|
+
reason: "permanent_name_retired",
|
|
29946
|
+
entityKind: retiredParse.data.entity_kind,
|
|
29947
|
+
logicalKey: retiredParse.data.logical_key
|
|
29948
|
+
};
|
|
29949
|
+
}
|
|
29800
29950
|
const contextParse = ContextDocConflictResponseSchema.safeParse(json2);
|
|
29801
29951
|
if (contextParse.success) {
|
|
29802
29952
|
const existing = strategy.singleResponseSchema.safeParse(contextParse.data.existing);
|
|
@@ -29853,17 +30003,33 @@ async function patchEntity(args) {
|
|
|
29853
30003
|
throw new ApiError(res.status, `patch ${strategy.type} failed with HTTP ${res.status}`);
|
|
29854
30004
|
}
|
|
29855
30005
|
async function createEntity(args) {
|
|
29856
|
-
const {
|
|
30006
|
+
const {
|
|
30007
|
+
strategy,
|
|
30008
|
+
baseUrl,
|
|
30009
|
+
pat,
|
|
30010
|
+
payload,
|
|
30011
|
+
retiredGeneration,
|
|
30012
|
+
operationId,
|
|
30013
|
+
fetchImpl = fetch
|
|
30014
|
+
} = args;
|
|
29857
30015
|
const url2 = `${baseUrl.replace(/\/$/, "")}/api/sync/${strategy.apiSegment}`;
|
|
30016
|
+
if (retiredGeneration === undefined !== (operationId === undefined)) {
|
|
30017
|
+
throw new Error("retired generation and create operation id must be supplied together");
|
|
30018
|
+
}
|
|
30019
|
+
const headers = {
|
|
30020
|
+
Authorization: `Bearer ${pat}`,
|
|
30021
|
+
Accept: "application/json",
|
|
30022
|
+
"Content-Type": "application/json"
|
|
30023
|
+
};
|
|
30024
|
+
if (retiredGeneration) {
|
|
30025
|
+
headers["X-Plinth-Retired-Generation"] = retiredGeneration;
|
|
30026
|
+
headers["X-Plinth-Create-Operation-Id"] = operationId;
|
|
30027
|
+
}
|
|
29858
30028
|
let res;
|
|
29859
30029
|
try {
|
|
29860
30030
|
res = await fetchImpl(url2, {
|
|
29861
30031
|
method: "POST",
|
|
29862
|
-
headers
|
|
29863
|
-
Authorization: `Bearer ${pat}`,
|
|
29864
|
-
Accept: "application/json",
|
|
29865
|
-
"Content-Type": "application/json"
|
|
29866
|
-
},
|
|
30032
|
+
headers,
|
|
29867
30033
|
body: JSON.stringify(payload)
|
|
29868
30034
|
});
|
|
29869
30035
|
} catch (err) {
|
|
@@ -29880,6 +30046,16 @@ async function createEntity(args) {
|
|
|
29880
30046
|
}
|
|
29881
30047
|
if (res.status === 409) {
|
|
29882
30048
|
const json2 = await res.json();
|
|
30049
|
+
const retiredParse = PermanentNameRetiredSchema.safeParse(json2);
|
|
30050
|
+
if (retiredParse.success) {
|
|
30051
|
+
return {
|
|
30052
|
+
ok: false,
|
|
30053
|
+
code: 409,
|
|
30054
|
+
reason: "permanent_name_retired",
|
|
30055
|
+
entityKind: retiredParse.data.entity_kind,
|
|
30056
|
+
logicalKey: retiredParse.data.logical_key
|
|
30057
|
+
};
|
|
30058
|
+
}
|
|
29883
30059
|
const contextParse = ContextDocConflictResponseSchema.safeParse(json2);
|
|
29884
30060
|
if (contextParse.success) {
|
|
29885
30061
|
const existing2 = strategy.singleResponseSchema.safeParse(contextParse.data.existing);
|
|
@@ -30023,12 +30199,25 @@ var FileTombstoneSchema = exports_external.object({
|
|
|
30023
30199
|
id: exports_external.string().uuid(),
|
|
30024
30200
|
path: exports_external.string().min(1),
|
|
30025
30201
|
updated_at: exports_external.string().datetime({ offset: true }),
|
|
30026
|
-
deleted_at: exports_external.string().datetime({ offset: true })
|
|
30202
|
+
deleted_at: exports_external.string().datetime({ offset: true }),
|
|
30203
|
+
permanent_delete: exports_external.literal(true).optional(),
|
|
30204
|
+
purge_generation: exports_external.string().uuid().optional(),
|
|
30205
|
+
retirement_sequence: exports_external.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional()
|
|
30206
|
+
}).superRefine((row, ctx) => {
|
|
30207
|
+
const present2 = [
|
|
30208
|
+
row.permanent_delete !== undefined,
|
|
30209
|
+
row.purge_generation !== undefined,
|
|
30210
|
+
row.retirement_sequence !== undefined
|
|
30211
|
+
];
|
|
30212
|
+
if (present2.some(Boolean) && !present2.every(Boolean)) {
|
|
30213
|
+
ctx.addIssue({ code: "custom", message: "permanent tombstone fields must be present together" });
|
|
30214
|
+
}
|
|
30027
30215
|
});
|
|
30028
30216
|
var FileRowSchema = exports_external.union([FileLiveSchema, FileTombstoneSchema]);
|
|
30029
30217
|
var FilesPageSchema = exports_external.object({
|
|
30030
30218
|
files: exports_external.array(FileRowSchema),
|
|
30031
|
-
next_cursor: exports_external.string().nullable()
|
|
30219
|
+
next_cursor: exports_external.string().nullable(),
|
|
30220
|
+
high_water: exports_external.string().nullable().optional()
|
|
30032
30221
|
});
|
|
30033
30222
|
var FileWithDownloadSchema = FileLiveSchema.extend({
|
|
30034
30223
|
download_url: exports_external.string().url().nullable()
|
|
@@ -30081,7 +30270,11 @@ async function syncFilesPage(args) {
|
|
|
30081
30270
|
if (!parsed.success) {
|
|
30082
30271
|
throw new ApiError(res.status, `sync files response did not match the expected shape: ${parsed.error.message}`);
|
|
30083
30272
|
}
|
|
30084
|
-
return {
|
|
30273
|
+
return {
|
|
30274
|
+
rows: parsed.data.files,
|
|
30275
|
+
next_cursor: parsed.data.next_cursor,
|
|
30276
|
+
high_water: parsed.data.high_water ?? null
|
|
30277
|
+
};
|
|
30085
30278
|
}
|
|
30086
30279
|
async function getFile(args) {
|
|
30087
30280
|
const { baseUrl, pat, id, fetchImpl = fetch } = args;
|
|
@@ -30162,6 +30355,8 @@ async function postFile(args) {
|
|
|
30162
30355
|
contentHash,
|
|
30163
30356
|
mimeType,
|
|
30164
30357
|
expectedUpdatedAt,
|
|
30358
|
+
retiredGeneration,
|
|
30359
|
+
operationId,
|
|
30165
30360
|
fetchImpl = fetch
|
|
30166
30361
|
} = args;
|
|
30167
30362
|
const url2 = `${baseUrl.replace(/\/$/, "")}/api/sync/files`;
|
|
@@ -30174,6 +30369,13 @@ async function postFile(args) {
|
|
|
30174
30369
|
};
|
|
30175
30370
|
if (expectedUpdatedAt)
|
|
30176
30371
|
headers["X-Plinth-Expected-Updated-At"] = expectedUpdatedAt;
|
|
30372
|
+
if (retiredGeneration === undefined !== (operationId === undefined)) {
|
|
30373
|
+
throw new Error("retired generation and create operation id must be supplied together");
|
|
30374
|
+
}
|
|
30375
|
+
if (retiredGeneration) {
|
|
30376
|
+
headers["X-Plinth-Retired-Generation"] = retiredGeneration;
|
|
30377
|
+
headers["X-Plinth-Create-Operation-Id"] = operationId;
|
|
30378
|
+
}
|
|
30177
30379
|
let res;
|
|
30178
30380
|
try {
|
|
30179
30381
|
res = await fetchImpl(url2, { method: "POST", headers, body: bytes });
|
|
@@ -30200,6 +30402,22 @@ async function postFile(args) {
|
|
|
30200
30402
|
}
|
|
30201
30403
|
if (res.status === 409) {
|
|
30202
30404
|
const json2 = await res.json();
|
|
30405
|
+
const retired = exports_external.object({
|
|
30406
|
+
code: exports_external.literal("permanent_name_retired"),
|
|
30407
|
+
entity_kind: exports_external.literal("workspace_file"),
|
|
30408
|
+
logical_key: exports_external.string().min(1),
|
|
30409
|
+
action: exports_external.literal("refresh_required"),
|
|
30410
|
+
error: exports_external.string().optional()
|
|
30411
|
+
}).safeParse(json2);
|
|
30412
|
+
if (retired.success) {
|
|
30413
|
+
return {
|
|
30414
|
+
ok: false,
|
|
30415
|
+
code: 409,
|
|
30416
|
+
reason: "permanent_name_retired",
|
|
30417
|
+
entityKind: retired.data.entity_kind,
|
|
30418
|
+
logicalKey: retired.data.logical_key
|
|
30419
|
+
};
|
|
30420
|
+
}
|
|
30203
30421
|
const parsed = PreconditionSchema.safeParse(json2);
|
|
30204
30422
|
if (!parsed.success) {
|
|
30205
30423
|
throw new ApiError(409, `post file 409 body did not match precondition_failed shape`);
|
|
@@ -30327,6 +30545,12 @@ function fileDeleteSuppressedLine(f) {
|
|
|
30327
30545
|
function entityDeleteSuppressedLine(f) {
|
|
30328
30546
|
return `[ENTITY_DELETE_SUPPRESSED entityType=${f.entityType} slug=${f.slug} reason=${f.reason}]`;
|
|
30329
30547
|
}
|
|
30548
|
+
function entityRestoreDeferredLine(f) {
|
|
30549
|
+
return `[ENTITY_RESTORE_DEFERRED entityType=${f.entityType} slug=${f.slug} wait_s=${f.waitSeconds}]`;
|
|
30550
|
+
}
|
|
30551
|
+
function fileRestoreDeferredLine(f) {
|
|
30552
|
+
return `[FILE_RESTORE_DEFERRED path=${f.path} wait_s=${f.waitSeconds}]`;
|
|
30553
|
+
}
|
|
30330
30554
|
function fileOverCapLine(f) {
|
|
30331
30555
|
return `[FILE_OVER_CAP path=${f.path} size=${f.size} max=${f.max} max_from=${f.source}]`;
|
|
30332
30556
|
}
|
|
@@ -34096,7 +34320,7 @@ var sessionCommand = defineCommand({
|
|
|
34096
34320
|
|
|
34097
34321
|
// src/commands/start.ts
|
|
34098
34322
|
var import_gray_matter14 = __toESM(require_gray_matter(), 1);
|
|
34099
|
-
import { existsSync as
|
|
34323
|
+
import { existsSync as existsSync10, readFileSync as readFileSync30, statSync as statSync18 } from "node:fs";
|
|
34100
34324
|
|
|
34101
34325
|
// src/lib/daemon-log.ts
|
|
34102
34326
|
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync9, renameSync as renameSync7, statSync as statSync4 } from "node:fs";
|
|
@@ -34340,7 +34564,7 @@ class DestructiveGate {
|
|
|
34340
34564
|
}
|
|
34341
34565
|
|
|
34342
34566
|
// src/lib/resolve-pending.ts
|
|
34343
|
-
import { readFileSync as
|
|
34567
|
+
import { readFileSync as readFileSync20 } from "node:fs";
|
|
34344
34568
|
|
|
34345
34569
|
// src/lib/conflicts.ts
|
|
34346
34570
|
import { mkdirSync as mkdirSync10, readFileSync as readFileSync15, readdirSync as readdirSync6, statSync as statSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
@@ -34359,6 +34583,26 @@ function stashConflict(args) {
|
|
|
34359
34583
|
writeFileSync7(target, args.contents, { mode: 384 });
|
|
34360
34584
|
return target;
|
|
34361
34585
|
}
|
|
34586
|
+
function stashEntityConflictOnce(args) {
|
|
34587
|
+
const dir = conflictsRoot(args.workspaceSlug);
|
|
34588
|
+
const head = `${args.entityType}.${args.entitySlug}.`;
|
|
34589
|
+
const tail = new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-\\d{3}Z\\.${args.side}\\.md$`);
|
|
34590
|
+
let names = [];
|
|
34591
|
+
try {
|
|
34592
|
+
names = readdirSync6(dir);
|
|
34593
|
+
} catch {
|
|
34594
|
+
names = [];
|
|
34595
|
+
}
|
|
34596
|
+
const newest = names.filter((name) => name.startsWith(head) && tail.test(name.slice(head.length))).sort().pop();
|
|
34597
|
+
if (newest !== undefined) {
|
|
34598
|
+
const path5 = join12(dir, newest);
|
|
34599
|
+
try {
|
|
34600
|
+
if (readFileSync15(path5, "utf8") === args.contents)
|
|
34601
|
+
return { path: path5, reused: true };
|
|
34602
|
+
} catch {}
|
|
34603
|
+
}
|
|
34604
|
+
return { path: stashConflict(args), reused: false };
|
|
34605
|
+
}
|
|
34362
34606
|
function fileConflictPath(args) {
|
|
34363
34607
|
const { workspaceSlug, relPath, timestamp, side } = args;
|
|
34364
34608
|
const iso = timestamp.toISOString().replace(/[:.]/g, "-");
|
|
@@ -34448,12 +34692,646 @@ function listConflictStashes(workspaceSlug) {
|
|
|
34448
34692
|
}
|
|
34449
34693
|
|
|
34450
34694
|
// src/lib/files/push.ts
|
|
34451
|
-
import { readFileSync as
|
|
34695
|
+
import { readFileSync as readFileSync17, statSync as statSync7 } from "node:fs";
|
|
34696
|
+
|
|
34697
|
+
// src/lib/permanent-deletions.ts
|
|
34698
|
+
import { createHash as createHash3, randomUUID } from "node:crypto";
|
|
34699
|
+
import {
|
|
34700
|
+
chmodSync,
|
|
34701
|
+
closeSync as closeSync4,
|
|
34702
|
+
constants as constants2,
|
|
34703
|
+
existsSync as existsSync4,
|
|
34704
|
+
fsyncSync as fsyncSync2,
|
|
34705
|
+
fstatSync as fstatSync2,
|
|
34706
|
+
lstatSync as lstatSync4,
|
|
34707
|
+
mkdirSync as mkdirSync11,
|
|
34708
|
+
openSync as openSync4,
|
|
34709
|
+
readFileSync as readFileSync16,
|
|
34710
|
+
renameSync as renameSync8,
|
|
34711
|
+
unlinkSync as unlinkSync8,
|
|
34712
|
+
writeSync as writeSync2
|
|
34713
|
+
} from "node:fs";
|
|
34714
|
+
import { dirname as dirname10, join as join13 } from "node:path";
|
|
34715
|
+
var PERMANENT_DELETE_KINDS = ["document", "workspace_file"];
|
|
34716
|
+
var UUID = exports_external.string().uuid();
|
|
34717
|
+
var SHA256 = exports_external.string().regex(/^[0-9a-f]{64}$/);
|
|
34718
|
+
function canonicalLogicalKey(kind, logicalKey) {
|
|
34719
|
+
return kind === "document" ? isValidWorkspaceSlug(logicalKey) : validateFilesRelPath(logicalKey) === logicalKey;
|
|
34720
|
+
}
|
|
34721
|
+
var PendingOperationSchema = exports_external.object({
|
|
34722
|
+
operation_id: UUID,
|
|
34723
|
+
payload_digest: SHA256,
|
|
34724
|
+
state: exports_external.enum(["pending", "transport_unknown", "finalized", "terminal_cleaned"]),
|
|
34725
|
+
row_id: UUID.optional(),
|
|
34726
|
+
updated_at: exports_external.string().datetime({ offset: true })
|
|
34727
|
+
}).superRefine((value, ctx) => {
|
|
34728
|
+
if (value.state === "finalized" && value.row_id === undefined) {
|
|
34729
|
+
ctx.addIssue({ code: "custom", message: "finalized operation requires row_id" });
|
|
34730
|
+
}
|
|
34731
|
+
if (value.state !== "finalized" && value.row_id !== undefined) {
|
|
34732
|
+
ctx.addIssue({ code: "custom", message: "row_id is valid only for finalized operations" });
|
|
34733
|
+
}
|
|
34734
|
+
});
|
|
34735
|
+
var RetirementRecordSchema = exports_external.object({
|
|
34736
|
+
kind: exports_external.enum(PERMANENT_DELETE_KINDS),
|
|
34737
|
+
logical_key: exports_external.string().min(1),
|
|
34738
|
+
row_id: UUID,
|
|
34739
|
+
purge_generation: UUID,
|
|
34740
|
+
retirement_sequence: exports_external.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
34741
|
+
retired_at: exports_external.string().datetime({ offset: true }),
|
|
34742
|
+
disposition: exports_external.enum(["pending", "complete"]),
|
|
34743
|
+
completed_at: exports_external.string().datetime({ offset: true }).optional(),
|
|
34744
|
+
stash_path: exports_external.string().min(1).optional(),
|
|
34745
|
+
stash_hash: SHA256.optional(),
|
|
34746
|
+
operation: PendingOperationSchema.optional()
|
|
34747
|
+
}).superRefine((value, ctx) => {
|
|
34748
|
+
if (!canonicalLogicalKey(value.kind, value.logical_key)) {
|
|
34749
|
+
ctx.addIssue({ code: "custom", message: "retirement logical key is not canonical" });
|
|
34750
|
+
}
|
|
34751
|
+
if (value.disposition === "complete" && value.completed_at === undefined) {
|
|
34752
|
+
ctx.addIssue({ code: "custom", message: "complete retirement requires completed_at" });
|
|
34753
|
+
}
|
|
34754
|
+
if (value.disposition === "pending" && value.completed_at !== undefined) {
|
|
34755
|
+
ctx.addIssue({ code: "custom", message: "pending retirement cannot have completed_at" });
|
|
34756
|
+
}
|
|
34757
|
+
if (value.stash_path === undefined !== (value.stash_hash === undefined)) {
|
|
34758
|
+
ctx.addIssue({ code: "custom", message: "stash_path and stash_hash must be written together" });
|
|
34759
|
+
}
|
|
34760
|
+
});
|
|
34761
|
+
var RefreshMarkerSchema = exports_external.object({
|
|
34762
|
+
kind: exports_external.enum(PERMANENT_DELETE_KINDS),
|
|
34763
|
+
logical_key: exports_external.string().min(1),
|
|
34764
|
+
required_generation: UUID.optional(),
|
|
34765
|
+
started_at: exports_external.string().datetime({ offset: true }),
|
|
34766
|
+
attempts: exports_external.number().int().nonnegative(),
|
|
34767
|
+
retry_after: exports_external.string().datetime({ offset: true }).optional(),
|
|
34768
|
+
state: exports_external.enum(["required", "retention_missing"])
|
|
34769
|
+
}).superRefine((value, ctx) => {
|
|
34770
|
+
if (!canonicalLogicalKey(value.kind, value.logical_key)) {
|
|
34771
|
+
ctx.addIssue({ code: "custom", message: "refresh logical key is not canonical" });
|
|
34772
|
+
}
|
|
34773
|
+
});
|
|
34774
|
+
var JournalBodySchema = exports_external.object({
|
|
34775
|
+
version: exports_external.literal(1),
|
|
34776
|
+
revision: exports_external.number().int().nonnegative(),
|
|
34777
|
+
workspace_slug: exports_external.string().min(1),
|
|
34778
|
+
records: exports_external.array(RetirementRecordSchema),
|
|
34779
|
+
refresh: exports_external.array(RefreshMarkerSchema)
|
|
34780
|
+
});
|
|
34781
|
+
var JournalFileSchema = JournalBodySchema.extend({ checksum: SHA256 });
|
|
34782
|
+
|
|
34783
|
+
class PermanentDeletionJournalError extends Error {
|
|
34784
|
+
stage;
|
|
34785
|
+
constructor(message, stage) {
|
|
34786
|
+
super(message);
|
|
34787
|
+
this.stage = stage;
|
|
34788
|
+
this.name = "PermanentDeletionJournalError";
|
|
34789
|
+
}
|
|
34790
|
+
}
|
|
34791
|
+
function permanentDeletionJournalPath(workspaceSlug) {
|
|
34792
|
+
if (!isValidWorkspaceSlug(workspaceSlug)) {
|
|
34793
|
+
throw new PermanentDeletionJournalError(`refusing unsafe workspace slug: ${JSON.stringify(workspaceSlug)}`, "validate");
|
|
34794
|
+
}
|
|
34795
|
+
return join13(plinthHome(), workspaceSlug, "permanent-deletions.json");
|
|
34796
|
+
}
|
|
34797
|
+
function recordKey2(kind, logicalKey) {
|
|
34798
|
+
return `${kind}\x00${logicalKey}`;
|
|
34799
|
+
}
|
|
34800
|
+
function canonical(value) {
|
|
34801
|
+
if (value === null || typeof value !== "object")
|
|
34802
|
+
return JSON.stringify(value);
|
|
34803
|
+
if (Array.isArray(value))
|
|
34804
|
+
return `[${value.map(canonical).join(",")}]`;
|
|
34805
|
+
const obj = value;
|
|
34806
|
+
return `{${Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}:${canonical(obj[key])}`).join(",")}}`;
|
|
34807
|
+
}
|
|
34808
|
+
function canonicalPayloadDigest(value) {
|
|
34809
|
+
return createHash3("sha256").update(canonical(value), "utf8").digest("hex");
|
|
34810
|
+
}
|
|
34811
|
+
function fileCreatePayloadDigest(contentHash, mimeType) {
|
|
34812
|
+
return canonicalPayloadDigest({ content_hash: contentHash, mime_type: mimeType });
|
|
34813
|
+
}
|
|
34814
|
+
function checksum(body) {
|
|
34815
|
+
return createHash3("sha256").update(canonical(body), "utf8").digest("hex");
|
|
34816
|
+
}
|
|
34817
|
+
function emptyJournal(workspaceSlug) {
|
|
34818
|
+
return { version: 1, revision: 0, workspace_slug: workspaceSlug, records: [], refresh: [] };
|
|
34819
|
+
}
|
|
34820
|
+
function validateUniqueness(journal) {
|
|
34821
|
+
const records = new Set;
|
|
34822
|
+
for (const record2 of journal.records) {
|
|
34823
|
+
const key = recordKey2(record2.kind, record2.logical_key);
|
|
34824
|
+
if (records.has(key)) {
|
|
34825
|
+
throw new PermanentDeletionJournalError(`permanent deletion journal contains duplicate record ${record2.kind}:${record2.logical_key}`, "validate");
|
|
34826
|
+
}
|
|
34827
|
+
records.add(key);
|
|
34828
|
+
}
|
|
34829
|
+
const refresh = new Set;
|
|
34830
|
+
for (const marker of journal.refresh) {
|
|
34831
|
+
const key = recordKey2(marker.kind, marker.logical_key);
|
|
34832
|
+
if (refresh.has(key)) {
|
|
34833
|
+
throw new PermanentDeletionJournalError(`permanent deletion journal contains duplicate refresh marker ${marker.kind}:${marker.logical_key}`, "validate");
|
|
34834
|
+
}
|
|
34835
|
+
refresh.add(key);
|
|
34836
|
+
}
|
|
34837
|
+
}
|
|
34838
|
+
function assertOwnerOnlyFile(path5) {
|
|
34839
|
+
const stat = lstatSync4(path5);
|
|
34840
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
34841
|
+
throw new PermanentDeletionJournalError(`${path5} is not a regular file`, "validate");
|
|
34842
|
+
}
|
|
34843
|
+
if ((stat.mode & 511) !== 384) {
|
|
34844
|
+
throw new PermanentDeletionJournalError(`${path5} must have mode 0600`, "validate");
|
|
34845
|
+
}
|
|
34846
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
34847
|
+
if (uid !== undefined && stat.uid !== uid) {
|
|
34848
|
+
throw new PermanentDeletionJournalError(`${path5} is not owned by the current user`, "validate");
|
|
34849
|
+
}
|
|
34850
|
+
}
|
|
34851
|
+
function assertPrivateDirectory(path5) {
|
|
34852
|
+
const stat = lstatSync4(path5);
|
|
34853
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
34854
|
+
throw new PermanentDeletionJournalError(`${path5} is not a private directory`, "validate");
|
|
34855
|
+
}
|
|
34856
|
+
if ((stat.mode & 511) !== 448) {
|
|
34857
|
+
throw new PermanentDeletionJournalError(`${path5} must have mode 0700`, "validate");
|
|
34858
|
+
}
|
|
34859
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
34860
|
+
if (uid !== undefined && stat.uid !== uid) {
|
|
34861
|
+
throw new PermanentDeletionJournalError(`${path5} is not owned by the current user`, "validate");
|
|
34862
|
+
}
|
|
34863
|
+
}
|
|
34864
|
+
function parseJournal(workspaceSlug, raw) {
|
|
34865
|
+
let json2;
|
|
34866
|
+
try {
|
|
34867
|
+
json2 = JSON.parse(raw);
|
|
34868
|
+
} catch (error52) {
|
|
34869
|
+
const detail = error52 instanceof Error ? error52.message : String(error52);
|
|
34870
|
+
throw new PermanentDeletionJournalError(`permanent deletion journal is not valid JSON: ${detail}`, "validate");
|
|
34871
|
+
}
|
|
34872
|
+
const parsed = JournalFileSchema.safeParse(json2);
|
|
34873
|
+
if (!parsed.success) {
|
|
34874
|
+
throw new PermanentDeletionJournalError(`permanent deletion journal does not match schema v1: ${parsed.error.message}`, "validate");
|
|
34875
|
+
}
|
|
34876
|
+
const { checksum: storedChecksum, ...body } = parsed.data;
|
|
34877
|
+
if (body.workspace_slug !== workspaceSlug) {
|
|
34878
|
+
throw new PermanentDeletionJournalError(`permanent deletion journal belongs to ${body.workspace_slug}, not ${workspaceSlug}`, "validate");
|
|
34879
|
+
}
|
|
34880
|
+
if (checksum(body) !== storedChecksum) {
|
|
34881
|
+
throw new PermanentDeletionJournalError("permanent deletion journal checksum mismatch", "validate");
|
|
34882
|
+
}
|
|
34883
|
+
validateUniqueness(body);
|
|
34884
|
+
return body;
|
|
34885
|
+
}
|
|
34886
|
+
function fsyncDirectory(path5) {
|
|
34887
|
+
const fd = openSync4(path5, constants2.O_RDONLY);
|
|
34888
|
+
try {
|
|
34889
|
+
fsyncSync2(fd);
|
|
34890
|
+
} finally {
|
|
34891
|
+
closeSync4(fd);
|
|
34892
|
+
}
|
|
34893
|
+
}
|
|
34894
|
+
function loadPermanentDeletionJournal(workspaceSlug) {
|
|
34895
|
+
const target = permanentDeletionJournalPath(workspaceSlug);
|
|
34896
|
+
if (!existsSync4(target))
|
|
34897
|
+
return emptyJournal(workspaceSlug);
|
|
34898
|
+
assertPrivateDirectory(dirname10(target));
|
|
34899
|
+
assertOwnerOnlyFile(target);
|
|
34900
|
+
const journal = parseJournal(workspaceSlug, readFileSync16(target, "utf8"));
|
|
34901
|
+
try {
|
|
34902
|
+
fsyncDirectory(dirname10(target));
|
|
34903
|
+
} catch (error52) {
|
|
34904
|
+
const detail = error52 instanceof Error ? error52.message : String(error52);
|
|
34905
|
+
throw new PermanentDeletionJournalError(`permanent deletion journal durability remains indeterminate: ${detail}`, "durability_indeterminate");
|
|
34906
|
+
}
|
|
34907
|
+
return journal;
|
|
34908
|
+
}
|
|
34909
|
+
function validatedCurrentRevision(workspaceSlug, expected) {
|
|
34910
|
+
const current = loadPermanentDeletionJournal(workspaceSlug);
|
|
34911
|
+
if (current.revision !== expected) {
|
|
34912
|
+
throw new PermanentDeletionJournalError(`permanent deletion journal revision changed from ${expected} to ${current.revision}`, "validate");
|
|
34913
|
+
}
|
|
34914
|
+
return current;
|
|
34915
|
+
}
|
|
34916
|
+
function writePermanentDeletionJournal(prior, next) {
|
|
34917
|
+
if (prior.workspace_slug !== next.workspace_slug) {
|
|
34918
|
+
throw new PermanentDeletionJournalError("journal workspace cannot change", "validate");
|
|
34919
|
+
}
|
|
34920
|
+
validatedCurrentRevision(prior.workspace_slug, prior.revision);
|
|
34921
|
+
const body = JournalBodySchema.parse({ ...next, revision: prior.revision + 1 });
|
|
34922
|
+
validateUniqueness(body);
|
|
34923
|
+
const target = permanentDeletionJournalPath(body.workspace_slug);
|
|
34924
|
+
const parent = dirname10(target);
|
|
34925
|
+
mkdirSync11(parent, { recursive: true, mode: 448 });
|
|
34926
|
+
chmodSync(parent, 448);
|
|
34927
|
+
assertPrivateDirectory(parent);
|
|
34928
|
+
if (existsSync4(target))
|
|
34929
|
+
assertOwnerOnlyFile(target);
|
|
34930
|
+
const tmp = join13(parent, `.permanent-deletions.${process.pid}.${randomUUID()}.tmp`);
|
|
34931
|
+
const wire = JSON.stringify({ ...body, checksum: checksum(body) }, null, 2) + `
|
|
34932
|
+
`;
|
|
34933
|
+
let fd;
|
|
34934
|
+
try {
|
|
34935
|
+
fd = openSync4(tmp, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | (constants2.O_NOFOLLOW ?? 0), 384);
|
|
34936
|
+
const stat = fstatSync2(fd);
|
|
34937
|
+
if (!stat.isFile() || (stat.mode & 511) !== 384) {
|
|
34938
|
+
throw new PermanentDeletionJournalError("journal temporary file is not owner-only", "prepare");
|
|
34939
|
+
}
|
|
34940
|
+
const bytes = Buffer.from(wire, "utf8");
|
|
34941
|
+
let offset = 0;
|
|
34942
|
+
while (offset < bytes.length)
|
|
34943
|
+
offset += writeSync2(fd, bytes, offset, bytes.length - offset);
|
|
34944
|
+
fsyncSync2(fd);
|
|
34945
|
+
closeSync4(fd);
|
|
34946
|
+
fd = undefined;
|
|
34947
|
+
} catch (error52) {
|
|
34948
|
+
if (fd !== undefined) {
|
|
34949
|
+
try {
|
|
34950
|
+
closeSync4(fd);
|
|
34951
|
+
} catch {}
|
|
34952
|
+
}
|
|
34953
|
+
try {
|
|
34954
|
+
unlinkSync8(tmp);
|
|
34955
|
+
} catch {}
|
|
34956
|
+
if (error52 instanceof PermanentDeletionJournalError)
|
|
34957
|
+
throw error52;
|
|
34958
|
+
const detail = error52 instanceof Error ? error52.message : String(error52);
|
|
34959
|
+
throw new PermanentDeletionJournalError(`could not prepare journal write: ${detail}`, "write");
|
|
34960
|
+
}
|
|
34961
|
+
try {
|
|
34962
|
+
renameSync8(tmp, target);
|
|
34963
|
+
} catch (error52) {
|
|
34964
|
+
let installed2 = false;
|
|
34965
|
+
try {
|
|
34966
|
+
assertOwnerOnlyFile(target);
|
|
34967
|
+
const candidate = parseJournal(body.workspace_slug, readFileSync16(target, "utf8"));
|
|
34968
|
+
installed2 = candidate.revision === body.revision && checksum(candidate) === checksum(body);
|
|
34969
|
+
} catch {
|
|
34970
|
+
installed2 = false;
|
|
34971
|
+
}
|
|
34972
|
+
try {
|
|
34973
|
+
unlinkSync8(tmp);
|
|
34974
|
+
} catch {}
|
|
34975
|
+
if (!installed2) {
|
|
34976
|
+
const detail = error52 instanceof Error ? error52.message : String(error52);
|
|
34977
|
+
throw new PermanentDeletionJournalError(`could not rename journal: ${detail}`, "rename");
|
|
34978
|
+
}
|
|
34979
|
+
}
|
|
34980
|
+
assertOwnerOnlyFile(target);
|
|
34981
|
+
const installed = parseJournal(body.workspace_slug, readFileSync16(target, "utf8"));
|
|
34982
|
+
if (installed.revision !== body.revision || checksum(installed) !== checksum(body)) {
|
|
34983
|
+
throw new PermanentDeletionJournalError("renamed journal revision did not validate", "durability_indeterminate");
|
|
34984
|
+
}
|
|
34985
|
+
try {
|
|
34986
|
+
fsyncDirectory(parent);
|
|
34987
|
+
} catch (firstError) {
|
|
34988
|
+
assertOwnerOnlyFile(target);
|
|
34989
|
+
const reopened = parseJournal(body.workspace_slug, readFileSync16(target, "utf8"));
|
|
34990
|
+
if (reopened.revision !== body.revision || checksum(reopened) !== checksum(body)) {
|
|
34991
|
+
throw new PermanentDeletionJournalError("journal changed after rename", "durability_indeterminate");
|
|
34992
|
+
}
|
|
34993
|
+
try {
|
|
34994
|
+
fsyncDirectory(parent);
|
|
34995
|
+
} catch (secondError) {
|
|
34996
|
+
const first = firstError instanceof Error ? firstError.message : String(firstError);
|
|
34997
|
+
const second = secondError instanceof Error ? secondError.message : String(secondError);
|
|
34998
|
+
throw new PermanentDeletionJournalError(`journal renamed but parent directory sync failed (${first}; retry: ${second})`, "durability_indeterminate");
|
|
34999
|
+
}
|
|
35000
|
+
}
|
|
35001
|
+
return body;
|
|
35002
|
+
}
|
|
35003
|
+
function updatePermanentDeletionJournal(workspaceSlug, update) {
|
|
35004
|
+
const prior = loadPermanentDeletionJournal(workspaceSlug);
|
|
35005
|
+
const next = update(prior);
|
|
35006
|
+
if (next === prior)
|
|
35007
|
+
return prior;
|
|
35008
|
+
return writePermanentDeletionJournal(prior, next);
|
|
35009
|
+
}
|
|
35010
|
+
function findRetirement(journal, kind, logicalKey) {
|
|
35011
|
+
return journal.records.find((record2) => record2.kind === kind && record2.logical_key === logicalKey);
|
|
35012
|
+
}
|
|
35013
|
+
function findRefreshMarker(journal, kind, logicalKey) {
|
|
35014
|
+
return journal.refresh.find((marker) => marker.kind === kind && marker.logical_key === logicalKey);
|
|
35015
|
+
}
|
|
35016
|
+
function replaceRecord(journal, record2) {
|
|
35017
|
+
return {
|
|
35018
|
+
...journal,
|
|
35019
|
+
records: [
|
|
35020
|
+
...journal.records.filter((item) => recordKey2(item.kind, item.logical_key) !== recordKey2(record2.kind, record2.logical_key)),
|
|
35021
|
+
record2
|
|
35022
|
+
]
|
|
35023
|
+
};
|
|
35024
|
+
}
|
|
35025
|
+
function requireFeedRefresh(args) {
|
|
35026
|
+
const now = args.now ?? (() => new Date);
|
|
35027
|
+
return updatePermanentDeletionJournal(args.workspaceSlug, (journal) => {
|
|
35028
|
+
const existing = findRefreshMarker(journal, args.kind, args.logicalKey);
|
|
35029
|
+
if (existing)
|
|
35030
|
+
return journal;
|
|
35031
|
+
return {
|
|
35032
|
+
...journal,
|
|
35033
|
+
refresh: [...journal.refresh, {
|
|
35034
|
+
kind: args.kind,
|
|
35035
|
+
logical_key: args.logicalKey,
|
|
35036
|
+
...args.requiredGeneration ? { required_generation: args.requiredGeneration } : {},
|
|
35037
|
+
started_at: now().toISOString(),
|
|
35038
|
+
attempts: 0,
|
|
35039
|
+
state: "required"
|
|
35040
|
+
}]
|
|
35041
|
+
};
|
|
35042
|
+
});
|
|
35043
|
+
}
|
|
35044
|
+
function markRetentionMissing(args) {
|
|
35045
|
+
const now = args.now ?? (() => new Date);
|
|
35046
|
+
return updatePermanentDeletionJournal(args.workspaceSlug, (journal) => {
|
|
35047
|
+
const existing = findRefreshMarker(journal, args.kind, args.logicalKey);
|
|
35048
|
+
if (!existing)
|
|
35049
|
+
return journal;
|
|
35050
|
+
const attempts = existing.attempts + 1;
|
|
35051
|
+
const backoffMs = Math.min(3600000, 1000 * 2 ** Math.min(attempts, 12));
|
|
35052
|
+
return {
|
|
35053
|
+
...journal,
|
|
35054
|
+
refresh: journal.refresh.map((marker) => marker === existing ? {
|
|
35055
|
+
...marker,
|
|
35056
|
+
attempts,
|
|
35057
|
+
state: "retention_missing",
|
|
35058
|
+
retry_after: new Date(now().getTime() + backoffMs).toISOString()
|
|
35059
|
+
} : marker)
|
|
35060
|
+
};
|
|
35061
|
+
});
|
|
35062
|
+
}
|
|
35063
|
+
function clearFeedRefresh(args) {
|
|
35064
|
+
return updatePermanentDeletionJournal(args.workspaceSlug, (journal) => {
|
|
35065
|
+
const record2 = findRetirement(journal, args.kind, args.logicalKey);
|
|
35066
|
+
if (!record2 || record2.disposition !== "complete" || record2.purge_generation !== args.generation) {
|
|
35067
|
+
throw new PermanentDeletionJournalError("cannot clear refresh before matching disposition is complete", "validate");
|
|
35068
|
+
}
|
|
35069
|
+
const next = journal.refresh.filter((marker) => recordKey2(marker.kind, marker.logical_key) !== recordKey2(args.kind, args.logicalKey));
|
|
35070
|
+
return next.length === journal.refresh.length ? journal : { ...journal, refresh: next };
|
|
35071
|
+
});
|
|
35072
|
+
}
|
|
35073
|
+
function recordRetirementPending(args) {
|
|
35074
|
+
return updatePermanentDeletionJournal(args.workspaceSlug, (journal) => {
|
|
35075
|
+
const existing = findRetirement(journal, args.kind, args.logicalKey);
|
|
35076
|
+
if (existing && existing.retirement_sequence > args.retirementSequence)
|
|
35077
|
+
return journal;
|
|
35078
|
+
if (existing && existing.retirement_sequence === args.retirementSequence && existing.purge_generation !== args.purgeGeneration) {
|
|
35079
|
+
throw new PermanentDeletionJournalError("one retirement_sequence maps to multiple generations", "validate");
|
|
35080
|
+
}
|
|
35081
|
+
if (existing?.purge_generation === args.purgeGeneration)
|
|
35082
|
+
return journal;
|
|
35083
|
+
return replaceRecord(journal, {
|
|
35084
|
+
kind: args.kind,
|
|
35085
|
+
logical_key: args.logicalKey,
|
|
35086
|
+
row_id: args.rowId,
|
|
35087
|
+
purge_generation: args.purgeGeneration,
|
|
35088
|
+
retirement_sequence: args.retirementSequence,
|
|
35089
|
+
retired_at: args.retiredAt,
|
|
35090
|
+
disposition: "pending"
|
|
35091
|
+
});
|
|
35092
|
+
});
|
|
35093
|
+
}
|
|
35094
|
+
function completeRetirement(args) {
|
|
35095
|
+
if (args.stashPath === undefined !== (args.stashHash === undefined)) {
|
|
35096
|
+
throw new PermanentDeletionJournalError("stash path and hash must be supplied together", "validate");
|
|
35097
|
+
}
|
|
35098
|
+
const now = args.now ?? (() => new Date);
|
|
35099
|
+
return updatePermanentDeletionJournal(args.workspaceSlug, (journal) => {
|
|
35100
|
+
const existing = findRetirement(journal, args.kind, args.logicalKey);
|
|
35101
|
+
if (!existing || existing.purge_generation !== args.generation) {
|
|
35102
|
+
throw new PermanentDeletionJournalError("cannot complete an unknown retirement generation", "validate");
|
|
35103
|
+
}
|
|
35104
|
+
if (existing.disposition === "complete")
|
|
35105
|
+
return journal;
|
|
35106
|
+
return replaceRecord(journal, {
|
|
35107
|
+
...existing,
|
|
35108
|
+
disposition: "complete",
|
|
35109
|
+
completed_at: now().toISOString(),
|
|
35110
|
+
...args.stashPath ? { stash_path: args.stashPath, stash_hash: args.stashHash } : {}
|
|
35111
|
+
});
|
|
35112
|
+
});
|
|
35113
|
+
}
|
|
35114
|
+
function recordRetirementStash(args) {
|
|
35115
|
+
return updatePermanentDeletionJournal(args.workspaceSlug, (journal) => {
|
|
35116
|
+
const existing = findRetirement(journal, args.kind, args.logicalKey);
|
|
35117
|
+
if (!existing || existing.purge_generation !== args.generation) {
|
|
35118
|
+
throw new PermanentDeletionJournalError("cannot attach a stash to an unknown retirement", "validate");
|
|
35119
|
+
}
|
|
35120
|
+
if (existing.stash_path === args.stashPath && existing.stash_hash === args.stashHash) {
|
|
35121
|
+
return journal;
|
|
35122
|
+
}
|
|
35123
|
+
if (existing.stash_path && existing.stash_hash !== args.stashHash) {
|
|
35124
|
+
throw new PermanentDeletionJournalError("retirement stash changed after it was recorded", "validate");
|
|
35125
|
+
}
|
|
35126
|
+
return replaceRecord(journal, {
|
|
35127
|
+
...existing,
|
|
35128
|
+
stash_path: args.stashPath,
|
|
35129
|
+
stash_hash: args.stashHash
|
|
35130
|
+
});
|
|
35131
|
+
});
|
|
35132
|
+
}
|
|
35133
|
+
function prepareGenerationCreate(args) {
|
|
35134
|
+
const now = args.now ?? (() => new Date);
|
|
35135
|
+
let decision = { kind: "ordinary" };
|
|
35136
|
+
updatePermanentDeletionJournal(args.workspaceSlug, (journal) => {
|
|
35137
|
+
if (findRefreshMarker(journal, args.kind, args.logicalKey)) {
|
|
35138
|
+
decision = { kind: "blocked", reason: "refresh_required" };
|
|
35139
|
+
return journal;
|
|
35140
|
+
}
|
|
35141
|
+
const retirement = findRetirement(journal, args.kind, args.logicalKey);
|
|
35142
|
+
if (!retirement) {
|
|
35143
|
+
decision = { kind: "ordinary" };
|
|
35144
|
+
return journal;
|
|
35145
|
+
}
|
|
35146
|
+
if (retirement.disposition !== "complete") {
|
|
35147
|
+
decision = { kind: "blocked", reason: "disposition_pending" };
|
|
35148
|
+
return journal;
|
|
35149
|
+
}
|
|
35150
|
+
const operation = retirement.operation;
|
|
35151
|
+
if (operation) {
|
|
35152
|
+
if (operation.payload_digest === args.payloadDigest && (operation.state === "pending" || operation.state === "transport_unknown")) {
|
|
35153
|
+
decision = {
|
|
35154
|
+
kind: "authorised",
|
|
35155
|
+
retiredGeneration: retirement.purge_generation,
|
|
35156
|
+
operationId: operation.operation_id,
|
|
35157
|
+
payloadDigest: operation.payload_digest
|
|
35158
|
+
};
|
|
35159
|
+
return journal;
|
|
35160
|
+
}
|
|
35161
|
+
if (operation.state !== "terminal_cleaned") {
|
|
35162
|
+
decision = { kind: "blocked", reason: "operation_unresolved" };
|
|
35163
|
+
return journal;
|
|
35164
|
+
}
|
|
35165
|
+
}
|
|
35166
|
+
const nextOperation = {
|
|
35167
|
+
operation_id: randomUUID(),
|
|
35168
|
+
payload_digest: args.payloadDigest,
|
|
35169
|
+
state: "pending",
|
|
35170
|
+
updated_at: now().toISOString()
|
|
35171
|
+
};
|
|
35172
|
+
decision = {
|
|
35173
|
+
kind: "authorised",
|
|
35174
|
+
retiredGeneration: retirement.purge_generation,
|
|
35175
|
+
operationId: nextOperation.operation_id,
|
|
35176
|
+
payloadDigest: nextOperation.payload_digest
|
|
35177
|
+
};
|
|
35178
|
+
return replaceRecord(journal, { ...retirement, operation: nextOperation });
|
|
35179
|
+
});
|
|
35180
|
+
return decision;
|
|
35181
|
+
}
|
|
35182
|
+
function updateCreateOperation(args) {
|
|
35183
|
+
const now = args.now ?? (() => new Date);
|
|
35184
|
+
return updatePermanentDeletionJournal(args.workspaceSlug, (journal) => {
|
|
35185
|
+
const retirement = findRetirement(journal, args.kind, args.logicalKey);
|
|
35186
|
+
if (!retirement?.operation || retirement.operation.operation_id !== args.operationId) {
|
|
35187
|
+
throw new PermanentDeletionJournalError("operation update does not match the journal", "validate");
|
|
35188
|
+
}
|
|
35189
|
+
const from = retirement.operation.state;
|
|
35190
|
+
const allowed = {
|
|
35191
|
+
pending: new Set(["pending", "transport_unknown", "finalized", "terminal_cleaned"]),
|
|
35192
|
+
transport_unknown: new Set(["transport_unknown", "pending", "finalized", "terminal_cleaned"]),
|
|
35193
|
+
finalized: new Set(["finalized", "terminal_cleaned"]),
|
|
35194
|
+
terminal_cleaned: new Set(["terminal_cleaned"])
|
|
35195
|
+
};
|
|
35196
|
+
if (!allowed[from].has(args.state)) {
|
|
35197
|
+
throw new PermanentDeletionJournalError(`invalid create operation transition ${from} -> ${args.state}`, "validate");
|
|
35198
|
+
}
|
|
35199
|
+
if (args.state === "finalized" && args.rowId === undefined) {
|
|
35200
|
+
throw new PermanentDeletionJournalError("finalized operation requires a row id", "validate");
|
|
35201
|
+
}
|
|
35202
|
+
return replaceRecord(journal, {
|
|
35203
|
+
...retirement,
|
|
35204
|
+
operation: {
|
|
35205
|
+
operation_id: retirement.operation.operation_id,
|
|
35206
|
+
payload_digest: retirement.operation.payload_digest,
|
|
35207
|
+
state: args.state,
|
|
35208
|
+
...args.state === "finalized" && args.rowId ? { row_id: args.rowId } : {},
|
|
35209
|
+
updated_at: now().toISOString()
|
|
35210
|
+
}
|
|
35211
|
+
});
|
|
35212
|
+
});
|
|
35213
|
+
}
|
|
35214
|
+
|
|
35215
|
+
// src/lib/permanent-mutex.ts
|
|
35216
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
35217
|
+
|
|
35218
|
+
// src/lib/per-id-mutex.ts
|
|
35219
|
+
class PerIdMutex {
|
|
35220
|
+
chains = new Map;
|
|
35221
|
+
async run(key, fn) {
|
|
35222
|
+
const prior = this.chains.get(key) ?? Promise.resolve();
|
|
35223
|
+
const next = prior.then(fn, fn);
|
|
35224
|
+
this.chains.set(key, next);
|
|
35225
|
+
const cleanup = () => {
|
|
35226
|
+
if (this.chains.get(key) === next) {
|
|
35227
|
+
this.chains.delete(key);
|
|
35228
|
+
}
|
|
35229
|
+
};
|
|
35230
|
+
next.then(cleanup, cleanup);
|
|
35231
|
+
return next;
|
|
35232
|
+
}
|
|
35233
|
+
size() {
|
|
35234
|
+
return this.chains.size;
|
|
35235
|
+
}
|
|
35236
|
+
}
|
|
35237
|
+
|
|
35238
|
+
// src/lib/permanent-mutex.ts
|
|
35239
|
+
var mutex = new PerIdMutex;
|
|
35240
|
+
var heldKeys = new AsyncLocalStorage;
|
|
35241
|
+
function permanentDeletionMutexKey(kind, logicalKey) {
|
|
35242
|
+
return `permanent:${kind}:${logicalKey}`;
|
|
35243
|
+
}
|
|
35244
|
+
function withPermanentDeletionLock(kind, logicalKey, run) {
|
|
35245
|
+
const key = permanentDeletionMutexKey(kind, logicalKey);
|
|
35246
|
+
const held = heldKeys.getStore();
|
|
35247
|
+
if (held?.has(key))
|
|
35248
|
+
return run();
|
|
35249
|
+
return mutex.run(key, () => heldKeys.run(new Set([...held ?? [], key]), run));
|
|
35250
|
+
}
|
|
35251
|
+
|
|
35252
|
+
// src/lib/permanent-create.ts
|
|
35253
|
+
async function prepareOrResolveGenerationCreate(args) {
|
|
35254
|
+
return withPermanentDeletionLock(args.kind, args.logicalKey, () => prepareOrResolveGenerationCreateLocked(args));
|
|
35255
|
+
}
|
|
35256
|
+
async function prepareOrResolveGenerationCreateLocked(args) {
|
|
35257
|
+
let decision = prepareGenerationCreate(args);
|
|
35258
|
+
if (decision.kind !== "blocked" || decision.reason !== "operation_unresolved") {
|
|
35259
|
+
return decision;
|
|
35260
|
+
}
|
|
35261
|
+
const journal = loadPermanentDeletionJournal(args.workspaceSlug);
|
|
35262
|
+
const retirement = findRetirement(journal, args.kind, args.logicalKey);
|
|
35263
|
+
const operation = retirement?.operation;
|
|
35264
|
+
if (!operation)
|
|
35265
|
+
return decision;
|
|
35266
|
+
const entityKind = args.kind;
|
|
35267
|
+
const resolved = await resolvePermanentDeletionOperation({
|
|
35268
|
+
baseUrl: args.baseUrl,
|
|
35269
|
+
pat: args.pat,
|
|
35270
|
+
entityKind,
|
|
35271
|
+
operationId: operation.operation_id,
|
|
35272
|
+
fetchImpl: args.fetchImpl
|
|
35273
|
+
});
|
|
35274
|
+
if (resolved.entity_kind !== entityKind || resolved.operation_id !== operation.operation_id) {
|
|
35275
|
+
throw new Error("operation resolution identity does not match the local journal");
|
|
35276
|
+
}
|
|
35277
|
+
if (resolved.status === "retired_not_found") {
|
|
35278
|
+
updateCreateOperation({
|
|
35279
|
+
workspaceSlug: args.workspaceSlug,
|
|
35280
|
+
kind: args.kind,
|
|
35281
|
+
logicalKey: args.logicalKey,
|
|
35282
|
+
operationId: operation.operation_id,
|
|
35283
|
+
state: "terminal_cleaned",
|
|
35284
|
+
now: args.now
|
|
35285
|
+
});
|
|
35286
|
+
return prepareGenerationCreate(args);
|
|
35287
|
+
}
|
|
35288
|
+
if (resolved.logical_key !== args.logicalKey || resolved.payload_digest !== operation.payload_digest) {
|
|
35289
|
+
throw new Error("operation resolution binding does not match the local journal");
|
|
35290
|
+
}
|
|
35291
|
+
if (resolved.status === "pending") {
|
|
35292
|
+
updateCreateOperation({
|
|
35293
|
+
workspaceSlug: args.workspaceSlug,
|
|
35294
|
+
kind: args.kind,
|
|
35295
|
+
logicalKey: args.logicalKey,
|
|
35296
|
+
operationId: operation.operation_id,
|
|
35297
|
+
state: "pending",
|
|
35298
|
+
now: args.now
|
|
35299
|
+
});
|
|
35300
|
+
return { kind: "blocked", reason: "operation_unresolved" };
|
|
35301
|
+
}
|
|
35302
|
+
if (resolved.status === "terminal_cleaned") {
|
|
35303
|
+
updateCreateOperation({
|
|
35304
|
+
workspaceSlug: args.workspaceSlug,
|
|
35305
|
+
kind: args.kind,
|
|
35306
|
+
logicalKey: args.logicalKey,
|
|
35307
|
+
operationId: operation.operation_id,
|
|
35308
|
+
state: "terminal_cleaned",
|
|
35309
|
+
now: args.now
|
|
35310
|
+
});
|
|
35311
|
+
return prepareGenerationCreate(args);
|
|
35312
|
+
}
|
|
35313
|
+
const id = exports_external.object({ id: exports_external.string().uuid() }).parse(resolved.live).id;
|
|
35314
|
+
updateCreateOperation({
|
|
35315
|
+
workspaceSlug: args.workspaceSlug,
|
|
35316
|
+
kind: args.kind,
|
|
35317
|
+
logicalKey: args.logicalKey,
|
|
35318
|
+
operationId: operation.operation_id,
|
|
35319
|
+
state: "finalized",
|
|
35320
|
+
rowId: id,
|
|
35321
|
+
now: args.now
|
|
35322
|
+
});
|
|
35323
|
+
return {
|
|
35324
|
+
kind: "finalized",
|
|
35325
|
+
live: resolved.live,
|
|
35326
|
+
operationId: operation.operation_id,
|
|
35327
|
+
payloadDigest: operation.payload_digest
|
|
35328
|
+
};
|
|
35329
|
+
}
|
|
34452
35330
|
|
|
34453
35331
|
// src/lib/files/content-hash.ts
|
|
34454
|
-
import { createHash as
|
|
35332
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
34455
35333
|
function sha256Bytes(bytes) {
|
|
34456
|
-
return
|
|
35334
|
+
return createHash4("sha256").update(bytes).digest("hex");
|
|
34457
35335
|
}
|
|
34458
35336
|
|
|
34459
35337
|
// src/lib/files/mime.ts
|
|
@@ -34552,6 +35430,9 @@ async function restoreFileFromCloud(args) {
|
|
|
34552
35430
|
});
|
|
34553
35431
|
}
|
|
34554
35432
|
async function pushFilesUpsert(args) {
|
|
35433
|
+
return withPermanentDeletionLock("workspace_file", args.relPath, () => pushFilesUpsertLocked(args));
|
|
35434
|
+
}
|
|
35435
|
+
async function pushFilesUpsertLocked(args) {
|
|
34555
35436
|
const { workspaceSlug, baseUrl, pat, absolutePath, relPath, state, prepared, deps } = args;
|
|
34556
35437
|
const fetchImpl = deps?.fetchImpl;
|
|
34557
35438
|
const now = deps?.now ?? (() => new Date);
|
|
@@ -34604,7 +35485,7 @@ async function pushFilesUpsert(args) {
|
|
|
34604
35485
|
state: next
|
|
34605
35486
|
};
|
|
34606
35487
|
}
|
|
34607
|
-
bytes =
|
|
35488
|
+
bytes = readFileSync17(absolutePath);
|
|
34608
35489
|
hash2 = sha256Bytes(bytes);
|
|
34609
35490
|
inode = st.ino;
|
|
34610
35491
|
}
|
|
@@ -34619,7 +35500,61 @@ async function pushFilesUpsert(args) {
|
|
|
34619
35500
|
return { outcome: { kind: "noop", relPath }, state };
|
|
34620
35501
|
}
|
|
34621
35502
|
const mimeType = inferMime(relPath);
|
|
35503
|
+
const createProtocol = existing ? { kind: "ordinary" } : await prepareOrResolveGenerationCreate({
|
|
35504
|
+
workspaceSlug,
|
|
35505
|
+
baseUrl,
|
|
35506
|
+
pat,
|
|
35507
|
+
kind: "workspace_file",
|
|
35508
|
+
logicalKey: relPath,
|
|
35509
|
+
payloadDigest: fileCreatePayloadDigest(hash2, mimeType),
|
|
35510
|
+
now,
|
|
35511
|
+
fetchImpl
|
|
35512
|
+
});
|
|
35513
|
+
if (createProtocol.kind === "blocked") {
|
|
35514
|
+
output?.warn(`files/${relPath} is held by permanent-delete reconciliation (${createProtocol.reason}).`);
|
|
35515
|
+
return {
|
|
35516
|
+
outcome: { kind: "permanent_delete_blocked", relPath, reason: createProtocol.reason },
|
|
35517
|
+
state
|
|
35518
|
+
};
|
|
35519
|
+
}
|
|
35520
|
+
if (createProtocol.kind === "finalized") {
|
|
35521
|
+
const parsed = FileLiveSchema.safeParse(createProtocol.live);
|
|
35522
|
+
if (!parsed.success) {
|
|
35523
|
+
throw new ApiError(200, "finalized file create operation live projection did not match the file schema");
|
|
35524
|
+
}
|
|
35525
|
+
if (parsed.data.path !== relPath) {
|
|
35526
|
+
throw new ApiError(200, "finalized file create operation live projection had the wrong path");
|
|
35527
|
+
}
|
|
35528
|
+
const adoptedState = persistFiles(state, {
|
|
35529
|
+
id: parsed.data.id,
|
|
35530
|
+
path: relPath,
|
|
35531
|
+
content_hash: parsed.data.content_hash,
|
|
35532
|
+
size_bytes: parsed.data.size_bytes,
|
|
35533
|
+
mime_type: parsed.data.mime_type,
|
|
35534
|
+
updated_at: parsed.data.updated_at,
|
|
35535
|
+
inode
|
|
35536
|
+
});
|
|
35537
|
+
return pushFilesUpsert({
|
|
35538
|
+
...args,
|
|
35539
|
+
state: adoptedState,
|
|
35540
|
+
prepared: { bytes, hash: hash2, size, inode }
|
|
35541
|
+
});
|
|
35542
|
+
}
|
|
34622
35543
|
const acceptPush = (row, action) => {
|
|
35544
|
+
if (createProtocol.kind === "authorised" && row.path !== relPath) {
|
|
35545
|
+
throw new ApiError(200, "generation-bearing file create response had the wrong path");
|
|
35546
|
+
}
|
|
35547
|
+
if (createProtocol.kind === "authorised") {
|
|
35548
|
+
updateCreateOperation({
|
|
35549
|
+
workspaceSlug,
|
|
35550
|
+
kind: "workspace_file",
|
|
35551
|
+
logicalKey: relPath,
|
|
35552
|
+
operationId: createProtocol.operationId,
|
|
35553
|
+
state: "finalized",
|
|
35554
|
+
rowId: row.id,
|
|
35555
|
+
now
|
|
35556
|
+
});
|
|
35557
|
+
}
|
|
34623
35558
|
output?.log(filePushLine({ path: relPath, id: row.id, action }));
|
|
34624
35559
|
return {
|
|
34625
35560
|
outcome: { kind: action, relPath, id: row.id },
|
|
@@ -34655,22 +35590,57 @@ async function pushFilesUpsert(args) {
|
|
|
34655
35590
|
output?.log(fileFailedLine({ path: relPath, status: 400, reason: "hash_mismatch" }));
|
|
34656
35591
|
return { outcome: { kind: "ignored", relPath, reason: "hash_mismatch" }, state };
|
|
34657
35592
|
};
|
|
34658
|
-
|
|
34659
|
-
|
|
34660
|
-
|
|
34661
|
-
|
|
34662
|
-
|
|
34663
|
-
|
|
34664
|
-
|
|
34665
|
-
|
|
34666
|
-
|
|
34667
|
-
|
|
35593
|
+
let res;
|
|
35594
|
+
try {
|
|
35595
|
+
res = await postFile({
|
|
35596
|
+
baseUrl,
|
|
35597
|
+
pat,
|
|
35598
|
+
relPath,
|
|
35599
|
+
bytes,
|
|
35600
|
+
contentHash: hash2,
|
|
35601
|
+
mimeType,
|
|
35602
|
+
expectedUpdatedAt: existing?.updated_at ?? null,
|
|
35603
|
+
...createProtocol.kind === "authorised" ? {
|
|
35604
|
+
retiredGeneration: createProtocol.retiredGeneration,
|
|
35605
|
+
operationId: createProtocol.operationId
|
|
35606
|
+
} : {},
|
|
35607
|
+
fetchImpl
|
|
35608
|
+
});
|
|
35609
|
+
} catch (error52) {
|
|
35610
|
+
if (createProtocol.kind === "authorised" && (!(error52 instanceof ApiError) || error52.status === 0 || error52.status >= 500 || error52.status >= 200 && error52.status < 300)) {
|
|
35611
|
+
updateCreateOperation({
|
|
35612
|
+
workspaceSlug,
|
|
35613
|
+
kind: "workspace_file",
|
|
35614
|
+
logicalKey: relPath,
|
|
35615
|
+
operationId: createProtocol.operationId,
|
|
35616
|
+
state: "transport_unknown",
|
|
35617
|
+
now
|
|
35618
|
+
});
|
|
35619
|
+
}
|
|
35620
|
+
throw error52;
|
|
35621
|
+
}
|
|
34668
35622
|
if (res.ok)
|
|
34669
35623
|
return acceptPush(res.row, existing ? "updated" : "created");
|
|
34670
35624
|
if (res.code === 413)
|
|
34671
35625
|
return rejectOverCap(res.maxBytes);
|
|
34672
35626
|
if (res.code === 400)
|
|
34673
35627
|
return rejectHashMismatch();
|
|
35628
|
+
if ("reason" in res && res.reason === "permanent_name_retired") {
|
|
35629
|
+
if (res.entityKind !== "workspace_file" || res.logicalKey !== relPath) {
|
|
35630
|
+
throw new ApiError(409, "permanent-name refusal identity did not match the file create");
|
|
35631
|
+
}
|
|
35632
|
+
requireFeedRefresh({
|
|
35633
|
+
workspaceSlug,
|
|
35634
|
+
kind: "workspace_file",
|
|
35635
|
+
logicalKey: res.logicalKey,
|
|
35636
|
+
now
|
|
35637
|
+
});
|
|
35638
|
+
output?.warn(`files/${res.logicalKey} was permanently deleted in Plinth; local bytes are held while the file feed is refreshed.`);
|
|
35639
|
+
return {
|
|
35640
|
+
outcome: { kind: "permanent_delete_blocked", relPath: res.logicalKey, reason: "refresh_required" },
|
|
35641
|
+
state
|
|
35642
|
+
};
|
|
35643
|
+
}
|
|
34674
35644
|
let nextState = state;
|
|
34675
35645
|
let cloudRow;
|
|
34676
35646
|
if (existing) {
|
|
@@ -34692,6 +35662,25 @@ async function pushFilesUpsert(args) {
|
|
|
34692
35662
|
return rejectOverCap(retry.maxBytes);
|
|
34693
35663
|
if (retry.code === 400)
|
|
34694
35664
|
return rejectHashMismatch();
|
|
35665
|
+
if ("reason" in retry && retry.reason === "permanent_name_retired") {
|
|
35666
|
+
if (retry.entityKind !== "workspace_file" || retry.logicalKey !== relPath) {
|
|
35667
|
+
throw new ApiError(409, "permanent-name refusal identity did not match the file retry");
|
|
35668
|
+
}
|
|
35669
|
+
requireFeedRefresh({
|
|
35670
|
+
workspaceSlug,
|
|
35671
|
+
kind: "workspace_file",
|
|
35672
|
+
logicalKey: retry.logicalKey,
|
|
35673
|
+
now
|
|
35674
|
+
});
|
|
35675
|
+
return {
|
|
35676
|
+
outcome: {
|
|
35677
|
+
kind: "permanent_delete_blocked",
|
|
35678
|
+
relPath: retry.logicalKey,
|
|
35679
|
+
reason: "refresh_required"
|
|
35680
|
+
},
|
|
35681
|
+
state
|
|
35682
|
+
};
|
|
35683
|
+
}
|
|
34695
35684
|
cloudRow = undefined;
|
|
34696
35685
|
}
|
|
34697
35686
|
}
|
|
@@ -34757,29 +35746,9 @@ async function pushFilesDelete(args) {
|
|
|
34757
35746
|
return { outcome: { kind: "conflict", relPath, stashPath: "" }, state: nextState };
|
|
34758
35747
|
}
|
|
34759
35748
|
|
|
34760
|
-
// src/lib/per-id-mutex.ts
|
|
34761
|
-
class PerIdMutex {
|
|
34762
|
-
chains = new Map;
|
|
34763
|
-
async run(key, fn) {
|
|
34764
|
-
const prior = this.chains.get(key) ?? Promise.resolve();
|
|
34765
|
-
const next = prior.then(fn, fn);
|
|
34766
|
-
this.chains.set(key, next);
|
|
34767
|
-
const cleanup = () => {
|
|
34768
|
-
if (this.chains.get(key) === next) {
|
|
34769
|
-
this.chains.delete(key);
|
|
34770
|
-
}
|
|
34771
|
-
};
|
|
34772
|
-
next.then(cleanup, cleanup);
|
|
34773
|
-
return next;
|
|
34774
|
-
}
|
|
34775
|
-
size() {
|
|
34776
|
-
return this.chains.size;
|
|
34777
|
-
}
|
|
34778
|
-
}
|
|
34779
|
-
|
|
34780
35749
|
// src/lib/push.ts
|
|
34781
35750
|
var import_gray_matter10 = __toESM(require_gray_matter(), 1);
|
|
34782
|
-
import { existsSync as
|
|
35751
|
+
import { existsSync as existsSync5, readFileSync as readFileSync19, statSync as statSync8 } from "node:fs";
|
|
34783
35752
|
import { basename as basename5, relative as relative5 } from "node:path";
|
|
34784
35753
|
|
|
34785
35754
|
// src/lib/frontmatter-id.ts
|
|
@@ -34829,7 +35798,7 @@ function frontmatterIdWriteIsSound(next, id, expectedBody) {
|
|
|
34829
35798
|
|
|
34830
35799
|
// src/lib/pull-write.ts
|
|
34831
35800
|
var import_gray_matter9 = __toESM(require_gray_matter(), 1);
|
|
34832
|
-
import { readFileSync as
|
|
35801
|
+
import { readFileSync as readFileSync18 } from "node:fs";
|
|
34833
35802
|
function computeMirrorHashes(strategy, content) {
|
|
34834
35803
|
const parsed = import_gray_matter9.default(content);
|
|
34835
35804
|
const fm = parsed.data ?? {};
|
|
@@ -34873,7 +35842,7 @@ function relocateForSlugChange(args) {
|
|
|
34873
35842
|
}
|
|
34874
35843
|
let raw = null;
|
|
34875
35844
|
try {
|
|
34876
|
-
raw =
|
|
35845
|
+
raw = readFileSync18(fromPath, "utf8");
|
|
34877
35846
|
} catch (err) {
|
|
34878
35847
|
const code = err?.code;
|
|
34879
35848
|
if (code === "ENOENT") {
|
|
@@ -34939,12 +35908,21 @@ function relocateForSlugChange(args) {
|
|
|
34939
35908
|
}
|
|
34940
35909
|
function writeMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, suppressor, pathSlug) {
|
|
34941
35910
|
const targetSlug = pathSlug ?? doc2.slug;
|
|
35911
|
+
const content = renderMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, targetSlug);
|
|
35912
|
+
if (suppressor) {
|
|
35913
|
+
const target = entityFilePath(workspaceSlug, strategy.type, targetSlug);
|
|
35914
|
+
suppressor.registerWrite(target, computeMirrorHashes(strategy, content));
|
|
35915
|
+
}
|
|
35916
|
+
writeMirrorFile(workspaceSlug, strategy.type, targetSlug, content);
|
|
35917
|
+
return content;
|
|
35918
|
+
}
|
|
35919
|
+
function renderMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, targetSlug) {
|
|
34942
35920
|
let content = strategy.serialise(doc2);
|
|
34943
35921
|
if (strategy.preserveLocalBodyOnPull) {
|
|
34944
35922
|
const target = entityFilePath(workspaceSlug, strategy.type, targetSlug);
|
|
34945
35923
|
let existing = null;
|
|
34946
35924
|
try {
|
|
34947
|
-
existing =
|
|
35925
|
+
existing = readFileSync18(target, "utf8");
|
|
34948
35926
|
} catch (err) {
|
|
34949
35927
|
if (err?.code !== "ENOENT")
|
|
34950
35928
|
throw err;
|
|
@@ -34958,11 +35936,6 @@ function writeMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, suppressor,
|
|
|
34958
35936
|
`;
|
|
34959
35937
|
}
|
|
34960
35938
|
}
|
|
34961
|
-
if (suppressor) {
|
|
34962
|
-
const target = entityFilePath(workspaceSlug, strategy.type, targetSlug);
|
|
34963
|
-
suppressor.registerWrite(target, computeMirrorHashes(strategy, content));
|
|
34964
|
-
}
|
|
34965
|
-
writeMirrorFile(workspaceSlug, strategy.type, targetSlug, content);
|
|
34966
35939
|
return content;
|
|
34967
35940
|
}
|
|
34968
35941
|
|
|
@@ -35076,7 +36049,7 @@ function stampStateFromMirror(args) {
|
|
|
35076
36049
|
const { strategy, workspaceSlug, doc: doc2, state, priorMemoryHash } = args;
|
|
35077
36050
|
const targetSlug = args.pathSlug ?? doc2.slug;
|
|
35078
36051
|
const mirrorPath = entityFilePath(workspaceSlug, strategy.type, targetSlug);
|
|
35079
|
-
const mirrorRaw =
|
|
36052
|
+
const mirrorRaw = readFileSync19(mirrorPath, "utf8");
|
|
35080
36053
|
const mirrorParsed = import_gray_matter10.default(mirrorRaw);
|
|
35081
36054
|
const mirrorFm = mirrorParsed.data ?? {};
|
|
35082
36055
|
const mirrorLifted = splitUserFrontmatter(strategy, mirrorFm);
|
|
@@ -35196,7 +36169,7 @@ async function pushFile(args) {
|
|
|
35196
36169
|
const identity = args.identity ?? "auto";
|
|
35197
36170
|
const fetchImpl = deps?.fetchImpl;
|
|
35198
36171
|
const now = deps?.now ?? (() => new Date);
|
|
35199
|
-
const
|
|
36172
|
+
const mutex2 = deps?.mutex ?? new PerIdMutex;
|
|
35200
36173
|
const output = deps?.output;
|
|
35201
36174
|
const suppressor = deps?.suppressor;
|
|
35202
36175
|
const echo = deps?.echo;
|
|
@@ -35222,14 +36195,14 @@ async function pushFile(args) {
|
|
|
35222
36195
|
absolutePath,
|
|
35223
36196
|
state,
|
|
35224
36197
|
unlinkContext,
|
|
35225
|
-
mutex,
|
|
36198
|
+
mutex: mutex2,
|
|
35226
36199
|
fetchImpl,
|
|
35227
36200
|
output
|
|
35228
36201
|
});
|
|
35229
36202
|
}
|
|
35230
36203
|
let raw;
|
|
35231
36204
|
try {
|
|
35232
|
-
raw =
|
|
36205
|
+
raw = readFileSync19(absolutePath, "utf8");
|
|
35233
36206
|
} catch (err) {
|
|
35234
36207
|
if (err?.code === "ENOENT") {
|
|
35235
36208
|
return runPushDelete({
|
|
@@ -35242,7 +36215,7 @@ async function pushFile(args) {
|
|
|
35242
36215
|
absolutePath,
|
|
35243
36216
|
state,
|
|
35244
36217
|
unlinkContext: null,
|
|
35245
|
-
mutex,
|
|
36218
|
+
mutex: mutex2,
|
|
35246
36219
|
fetchImpl,
|
|
35247
36220
|
output
|
|
35248
36221
|
});
|
|
@@ -35304,7 +36277,7 @@ async function pushFile(args) {
|
|
|
35304
36277
|
raw,
|
|
35305
36278
|
parsed,
|
|
35306
36279
|
state,
|
|
35307
|
-
mutex,
|
|
36280
|
+
mutex: mutex2,
|
|
35308
36281
|
fetchImpl,
|
|
35309
36282
|
now,
|
|
35310
36283
|
output,
|
|
@@ -35325,7 +36298,7 @@ async function pushFile(args) {
|
|
|
35325
36298
|
parsed,
|
|
35326
36299
|
stateEntry,
|
|
35327
36300
|
state,
|
|
35328
|
-
mutex,
|
|
36301
|
+
mutex: mutex2,
|
|
35329
36302
|
fetchImpl,
|
|
35330
36303
|
now,
|
|
35331
36304
|
output,
|
|
@@ -35579,7 +36552,7 @@ async function runPushUpsert(args) {
|
|
|
35579
36552
|
if (liveEntry !== snapshotEntry) {
|
|
35580
36553
|
let onDisk = null;
|
|
35581
36554
|
try {
|
|
35582
|
-
onDisk =
|
|
36555
|
+
onDisk = readFileSync19(args.absolutePath, "utf8");
|
|
35583
36556
|
} catch {
|
|
35584
36557
|
onDisk = null;
|
|
35585
36558
|
}
|
|
@@ -35788,6 +36761,28 @@ async function runPushUpsert(args) {
|
|
|
35788
36761
|
base: workingState
|
|
35789
36762
|
};
|
|
35790
36763
|
}
|
|
36764
|
+
if (patchResult.reason === "permanent_name_retired") {
|
|
36765
|
+
if (strategy.type !== "documents" || patchResult.entityKind !== "document" || patchResult.logicalKey !== args.stateEntry.slug) {
|
|
36766
|
+
throw new ApiError(409, "permanent-name refusal identity did not match the document update");
|
|
36767
|
+
}
|
|
36768
|
+
requireFeedRefresh({
|
|
36769
|
+
workspaceSlug: args.workspaceSlug,
|
|
36770
|
+
kind: "document",
|
|
36771
|
+
logicalKey: patchResult.logicalKey,
|
|
36772
|
+
now: args.now
|
|
36773
|
+
});
|
|
36774
|
+
args.output?.warn(`${strategy.type}/${patchResult.logicalKey} was permanently deleted in Plinth; local content is held while the document feed is refreshed.`);
|
|
36775
|
+
return {
|
|
36776
|
+
outcome: {
|
|
36777
|
+
kind: "permanent_delete_blocked",
|
|
36778
|
+
entityType: strategy.type,
|
|
36779
|
+
entitySlug: patchResult.logicalKey,
|
|
36780
|
+
reason: "refresh_required"
|
|
36781
|
+
},
|
|
36782
|
+
state: workingState,
|
|
36783
|
+
base: workingState
|
|
36784
|
+
};
|
|
36785
|
+
}
|
|
35791
36786
|
if (patchResult.reason === "slug_conflict") {
|
|
35792
36787
|
throw new Error(`patch ${strategy.type} returned slug_conflict for a body+frontmatter PATCH (no slug sent)`);
|
|
35793
36788
|
}
|
|
@@ -36019,7 +37014,7 @@ function writeCreatedIdBack(args) {
|
|
|
36019
37014
|
if (entityFilePath(args.workspaceSlug, args.strategy.type, args.slug) !== args.absolutePath) {
|
|
36020
37015
|
return;
|
|
36021
37016
|
}
|
|
36022
|
-
const readBack = args.readBack ?? ((p) =>
|
|
37017
|
+
const readBack = args.readBack ?? ((p) => readFileSync19(p, "utf8"));
|
|
36023
37018
|
if (readBack(args.absolutePath) !== args.raw)
|
|
36024
37019
|
return;
|
|
36025
37020
|
args.suppressor?.registerWrite(args.absolutePath, computeMirrorHashes(args.strategy, write.content));
|
|
@@ -36027,6 +37022,12 @@ function writeCreatedIdBack(args) {
|
|
|
36027
37022
|
} catch {}
|
|
36028
37023
|
}
|
|
36029
37024
|
async function runPushCreate(args) {
|
|
37025
|
+
if (args.strategy.type === "documents") {
|
|
37026
|
+
return withPermanentDeletionLock("document", args.slug, () => runPushCreateLocked(args));
|
|
37027
|
+
}
|
|
37028
|
+
return runPushCreateLocked(args);
|
|
37029
|
+
}
|
|
37030
|
+
async function runPushCreateLocked(args) {
|
|
36030
37031
|
const strategy = args.strategy;
|
|
36031
37032
|
const key = `create:${strategy.type}:${args.slug}`;
|
|
36032
37033
|
return args.mutex.run(key, async () => {
|
|
@@ -36064,15 +37065,108 @@ async function runPushCreate(args) {
|
|
|
36064
37065
|
else
|
|
36065
37066
|
payload.body = normalised;
|
|
36066
37067
|
}
|
|
36067
|
-
const
|
|
36068
|
-
|
|
37068
|
+
const createProtocol = strategy.type === "documents" ? await prepareOrResolveGenerationCreate({
|
|
37069
|
+
workspaceSlug: args.workspaceSlug,
|
|
36069
37070
|
baseUrl: args.baseUrl,
|
|
36070
37071
|
pat: args.pat,
|
|
36071
|
-
|
|
37072
|
+
kind: "document",
|
|
37073
|
+
logicalKey: args.slug,
|
|
37074
|
+
payloadDigest: canonicalPayloadDigest(payload),
|
|
37075
|
+
now: args.now,
|
|
36072
37076
|
fetchImpl: args.fetchImpl
|
|
36073
|
-
});
|
|
37077
|
+
}) : { kind: "ordinary" };
|
|
37078
|
+
if (createProtocol.kind === "blocked") {
|
|
37079
|
+
args.output?.warn(`${strategy.type}/${args.slug} is held by permanent-delete reconciliation (${createProtocol.reason}).`);
|
|
37080
|
+
return {
|
|
37081
|
+
outcome: {
|
|
37082
|
+
kind: "permanent_delete_blocked",
|
|
37083
|
+
entityType: strategy.type,
|
|
37084
|
+
entitySlug: args.slug,
|
|
37085
|
+
reason: createProtocol.reason
|
|
37086
|
+
},
|
|
37087
|
+
state: args.state,
|
|
37088
|
+
base: args.state
|
|
37089
|
+
};
|
|
37090
|
+
}
|
|
37091
|
+
if (createProtocol.kind === "finalized") {
|
|
37092
|
+
const parsedLive = strategy.singleResponseSchema.safeParse(createProtocol.live);
|
|
37093
|
+
if (!parsedLive.success) {
|
|
37094
|
+
throw new ApiError(200, "finalized create operation live projection did not match the entity schema");
|
|
37095
|
+
}
|
|
37096
|
+
if (parsedLive.data.slug !== args.slug) {
|
|
37097
|
+
throw new ApiError(200, "finalized create operation live projection had the wrong slug");
|
|
37098
|
+
}
|
|
37099
|
+
const acknowledged = computeMirrorHashes(strategy, strategy.serialise(parsedLive.data));
|
|
37100
|
+
const adoptedEntry = {
|
|
37101
|
+
id: parsedLive.data.id,
|
|
37102
|
+
slug: parsedLive.data.slug,
|
|
37103
|
+
updated_at: parsedLive.data.updated_at,
|
|
37104
|
+
revision: parsedLive.data.revision,
|
|
37105
|
+
body_hash: acknowledged.bodyHash,
|
|
37106
|
+
frontmatter_hash: acknowledged.frontmatterHash,
|
|
37107
|
+
inode: safeInode2(args.absolutePath)
|
|
37108
|
+
};
|
|
37109
|
+
const adoptedState = persistEntry(args.state, strategy.type, adoptedEntry);
|
|
37110
|
+
return runPushUpsert({
|
|
37111
|
+
echo: args.echo,
|
|
37112
|
+
strategy,
|
|
37113
|
+
workspaceSlug: args.workspaceSlug,
|
|
37114
|
+
baseUrl: args.baseUrl,
|
|
37115
|
+
pat: args.pat,
|
|
37116
|
+
absolutePath: args.absolutePath,
|
|
37117
|
+
raw: args.raw,
|
|
37118
|
+
parsed: args.parsed,
|
|
37119
|
+
stateEntry: adoptedEntry,
|
|
37120
|
+
state: adoptedState,
|
|
37121
|
+
mutex: args.mutex,
|
|
37122
|
+
fetchImpl: args.fetchImpl,
|
|
37123
|
+
now: args.now,
|
|
37124
|
+
output: args.output,
|
|
37125
|
+
quarantinedKeys: args.quarantinedKeys
|
|
37126
|
+
});
|
|
37127
|
+
}
|
|
37128
|
+
let createResult;
|
|
37129
|
+
try {
|
|
37130
|
+
createResult = await createEntity({
|
|
37131
|
+
strategy,
|
|
37132
|
+
baseUrl: args.baseUrl,
|
|
37133
|
+
pat: args.pat,
|
|
37134
|
+
payload,
|
|
37135
|
+
...createProtocol.kind === "authorised" ? {
|
|
37136
|
+
retiredGeneration: createProtocol.retiredGeneration,
|
|
37137
|
+
operationId: createProtocol.operationId
|
|
37138
|
+
} : {},
|
|
37139
|
+
fetchImpl: args.fetchImpl
|
|
37140
|
+
});
|
|
37141
|
+
} catch (error52) {
|
|
37142
|
+
if (createProtocol.kind === "authorised" && (!(error52 instanceof ApiError) || error52.status === 0 || error52.status >= 500 || error52.status >= 200 && error52.status < 300)) {
|
|
37143
|
+
updateCreateOperation({
|
|
37144
|
+
workspaceSlug: args.workspaceSlug,
|
|
37145
|
+
kind: "document",
|
|
37146
|
+
logicalKey: args.slug,
|
|
37147
|
+
operationId: createProtocol.operationId,
|
|
37148
|
+
state: "transport_unknown",
|
|
37149
|
+
now: args.now
|
|
37150
|
+
});
|
|
37151
|
+
}
|
|
37152
|
+
throw error52;
|
|
37153
|
+
}
|
|
36074
37154
|
if (createResult.ok) {
|
|
36075
37155
|
const doc2 = createResult.doc;
|
|
37156
|
+
if (createProtocol.kind === "authorised" && doc2.slug !== args.slug) {
|
|
37157
|
+
throw new ApiError(200, "generation-bearing create response had the wrong slug");
|
|
37158
|
+
}
|
|
37159
|
+
if (createProtocol.kind === "authorised") {
|
|
37160
|
+
updateCreateOperation({
|
|
37161
|
+
workspaceSlug: args.workspaceSlug,
|
|
37162
|
+
kind: "document",
|
|
37163
|
+
logicalKey: args.slug,
|
|
37164
|
+
operationId: createProtocol.operationId,
|
|
37165
|
+
state: "finalized",
|
|
37166
|
+
rowId: doc2.id,
|
|
37167
|
+
now: args.now
|
|
37168
|
+
});
|
|
37169
|
+
}
|
|
36076
37170
|
const createFm = args.parsed.data ?? {};
|
|
36077
37171
|
const createLifted = splitUserFrontmatter(strategy, createFm);
|
|
36078
37172
|
const userFm = userFrontmatter(strategy, createLifted.frontmatter);
|
|
@@ -36130,6 +37224,27 @@ async function runPushCreate(args) {
|
|
|
36130
37224
|
base: args.state
|
|
36131
37225
|
};
|
|
36132
37226
|
}
|
|
37227
|
+
if (createResult.reason === "permanent_name_retired") {
|
|
37228
|
+
if (strategy.type !== "documents" || createResult.entityKind !== "document" || createResult.logicalKey !== args.slug) {
|
|
37229
|
+
throw new ApiError(409, "permanent-name refusal identity did not match the document create");
|
|
37230
|
+
}
|
|
37231
|
+
requireFeedRefresh({
|
|
37232
|
+
workspaceSlug: args.workspaceSlug,
|
|
37233
|
+
kind: "document",
|
|
37234
|
+
logicalKey: createResult.logicalKey,
|
|
37235
|
+
now: args.now
|
|
37236
|
+
});
|
|
37237
|
+
return {
|
|
37238
|
+
outcome: {
|
|
37239
|
+
kind: "permanent_delete_blocked",
|
|
37240
|
+
entityType: strategy.type,
|
|
37241
|
+
entitySlug: createResult.logicalKey,
|
|
37242
|
+
reason: "refresh_required"
|
|
37243
|
+
},
|
|
37244
|
+
state: args.state,
|
|
37245
|
+
base: args.state
|
|
37246
|
+
};
|
|
37247
|
+
}
|
|
36133
37248
|
if (createResult.reason === "context_doc_conflict") {
|
|
36134
37249
|
return reconcileContextDocConflict({
|
|
36135
37250
|
echo: args.echo,
|
|
@@ -36218,7 +37333,7 @@ async function runPushDelete(args) {
|
|
|
36218
37333
|
return args.mutex.run(id, async () => {
|
|
36219
37334
|
const snapshot = getEntry(args.state, strategy.type, id) ?? null;
|
|
36220
37335
|
const live = snapshot ? committedEntryIfNewer(args.workspaceSlug, strategy.type, snapshot) ?? snapshot : null;
|
|
36221
|
-
if (
|
|
37336
|
+
if (existsSync5(args.absolutePath)) {
|
|
36222
37337
|
args.output?.log(entityDeleteSuppressedLine({
|
|
36223
37338
|
entityType: strategy.type,
|
|
36224
37339
|
slug,
|
|
@@ -36310,7 +37425,7 @@ async function pushRename(args) {
|
|
|
36310
37425
|
} = args;
|
|
36311
37426
|
const fetchImpl = deps?.fetchImpl;
|
|
36312
37427
|
const now = deps?.now ?? (() => new Date);
|
|
36313
|
-
const
|
|
37428
|
+
const mutex2 = deps?.mutex ?? new PerIdMutex;
|
|
36314
37429
|
const output = deps?.output;
|
|
36315
37430
|
const echo = deps?.echo;
|
|
36316
37431
|
const strategy = requireStrategy(entityType);
|
|
@@ -36321,7 +37436,7 @@ async function pushRename(args) {
|
|
|
36321
37436
|
} catch {
|
|
36322
37437
|
toSlug = basename5(toPath, ".md");
|
|
36323
37438
|
}
|
|
36324
|
-
return
|
|
37439
|
+
return mutex2.run(id, async () => {
|
|
36325
37440
|
const snapshot = getEntry(state, entityType, id);
|
|
36326
37441
|
const live = snapshot ? committedEntryIfNewer(workspaceSlug, entityType, snapshot) ?? snapshot : undefined;
|
|
36327
37442
|
const effectiveExpected = live?.updated_at ?? expectedUpdatedAt;
|
|
@@ -36371,7 +37486,7 @@ async function pushRename(args) {
|
|
|
36371
37486
|
};
|
|
36372
37487
|
if (!updated.body_hash || !updated.frontmatter_hash) {
|
|
36373
37488
|
try {
|
|
36374
|
-
const mirrorRaw =
|
|
37489
|
+
const mirrorRaw = readFileSync19(toPath, "utf8");
|
|
36375
37490
|
const mirrorParsed = import_gray_matter10.default(mirrorRaw);
|
|
36376
37491
|
const renameFm = mirrorParsed.data ?? {};
|
|
36377
37492
|
const renameLifted = splitUserFrontmatter(strategy, renameFm);
|
|
@@ -36408,7 +37523,7 @@ async function pushRename(args) {
|
|
|
36408
37523
|
if (patchResult.reason === "slug_conflict") {
|
|
36409
37524
|
let contents2 = "";
|
|
36410
37525
|
try {
|
|
36411
|
-
contents2 =
|
|
37526
|
+
contents2 = readFileSync19(toPath, "utf8");
|
|
36412
37527
|
} catch {}
|
|
36413
37528
|
const stashPath2 = stashConflict({
|
|
36414
37529
|
workspaceSlug,
|
|
@@ -36456,7 +37571,7 @@ async function pushRename(args) {
|
|
|
36456
37571
|
}
|
|
36457
37572
|
let contents = "";
|
|
36458
37573
|
try {
|
|
36459
|
-
contents =
|
|
37574
|
+
contents = readFileSync19(toPath, "utf8");
|
|
36460
37575
|
} catch {}
|
|
36461
37576
|
const stashPath = stashConflict({
|
|
36462
37577
|
workspaceSlug,
|
|
@@ -36493,14 +37608,14 @@ async function pushRename(args) {
|
|
|
36493
37608
|
|
|
36494
37609
|
// src/lib/resolve-pending.ts
|
|
36495
37610
|
async function drainResolvedBatches(deps) {
|
|
36496
|
-
const
|
|
37611
|
+
const mutex2 = deps.mutex ?? new PerIdMutex;
|
|
36497
37612
|
let applied = 0;
|
|
36498
37613
|
let discarded = 0;
|
|
36499
37614
|
for (const batch of listPendingBatches(deps.workspaceSlug)) {
|
|
36500
37615
|
if (batch.status === "pending")
|
|
36501
37616
|
continue;
|
|
36502
37617
|
if (batch.status === "approved") {
|
|
36503
|
-
await applyBatch(batch, deps,
|
|
37618
|
+
await applyBatch(batch, deps, mutex2);
|
|
36504
37619
|
deps.output?.log(`[DESTRUCTIVE_APPLIED batch=${batch.batch_id} ops=${batch.ops.length}]`);
|
|
36505
37620
|
applied++;
|
|
36506
37621
|
} else {
|
|
@@ -36515,7 +37630,7 @@ async function drainResolvedBatches(deps) {
|
|
|
36515
37630
|
function opLabel(op) {
|
|
36516
37631
|
return op.target === "file" ? `files/${op.path}` : `${op.entityType}/${op.slug}`;
|
|
36517
37632
|
}
|
|
36518
|
-
async function applyBatch(batch, deps,
|
|
37633
|
+
async function applyBatch(batch, deps, mutex2) {
|
|
36519
37634
|
for (const op of batch.ops) {
|
|
36520
37635
|
try {
|
|
36521
37636
|
if (op.target === "file") {
|
|
@@ -36535,7 +37650,7 @@ async function applyBatch(batch, deps, mutex) {
|
|
|
36535
37650
|
deps: {
|
|
36536
37651
|
fetchImpl: deps.fetchImpl,
|
|
36537
37652
|
now: deps.now,
|
|
36538
|
-
mutex,
|
|
37653
|
+
mutex: mutex2,
|
|
36539
37654
|
output: deps.output,
|
|
36540
37655
|
echo: deps.echo
|
|
36541
37656
|
}
|
|
@@ -36589,7 +37704,7 @@ async function discardBatchOps(batch, deps) {
|
|
|
36589
37704
|
const path5 = opPath(deps.workspaceSlug, op);
|
|
36590
37705
|
let local = null;
|
|
36591
37706
|
try {
|
|
36592
|
-
local =
|
|
37707
|
+
local = readFileSync20(path5, "utf8");
|
|
36593
37708
|
} catch {
|
|
36594
37709
|
local = null;
|
|
36595
37710
|
}
|
|
@@ -36628,7 +37743,7 @@ async function discardFileOp(op, deps) {
|
|
|
36628
37743
|
const abs = filesSubtreePath(deps.workspaceSlug, op.path);
|
|
36629
37744
|
let local = null;
|
|
36630
37745
|
try {
|
|
36631
|
-
local =
|
|
37746
|
+
local = readFileSync20(abs);
|
|
36632
37747
|
} catch {
|
|
36633
37748
|
local = null;
|
|
36634
37749
|
}
|
|
@@ -36971,7 +38086,7 @@ function unknownBecause(reason) {
|
|
|
36971
38086
|
case "no_watermark":
|
|
36972
38087
|
return "The last download carried no freshness marker, so there is nothing to compare against.";
|
|
36973
38088
|
case "incomplete_apply":
|
|
36974
|
-
return "The last download did not fully land, so this copy is not
|
|
38089
|
+
return "The last download either did not fully land or carried a correction trail this copy could not use, so this copy is not one the freshness marker can vouch for.";
|
|
36975
38090
|
case "check_failed":
|
|
36976
38091
|
return "The freshness check did not complete.";
|
|
36977
38092
|
default:
|
|
@@ -37128,6 +38243,9 @@ function resolveTrail(row, slugById) {
|
|
|
37128
38243
|
consistent: true
|
|
37129
38244
|
};
|
|
37130
38245
|
}
|
|
38246
|
+
if (outcome === "current" && successor === row.id) {
|
|
38247
|
+
return { trail: "current", consistent: true };
|
|
38248
|
+
}
|
|
37131
38249
|
if (successor !== null)
|
|
37132
38250
|
return { trail: UNKNOWN, consistent: false };
|
|
37133
38251
|
return { trail: outcome, consistent: true };
|
|
@@ -37597,10 +38715,10 @@ class FileDestructiveGate {
|
|
|
37597
38715
|
}
|
|
37598
38716
|
|
|
37599
38717
|
// src/lib/files/reconcile.ts
|
|
37600
|
-
import { readFileSync as
|
|
38718
|
+
import { readFileSync as readFileSync21, statSync as statSync10 } from "node:fs";
|
|
37601
38719
|
|
|
37602
38720
|
// src/lib/unsynced.ts
|
|
37603
|
-
import { join as
|
|
38721
|
+
import { join as join14, relative as relative8 } from "node:path";
|
|
37604
38722
|
var RECORDABLE_REASONS = new Set([
|
|
37605
38723
|
"rate_limit",
|
|
37606
38724
|
"server",
|
|
@@ -37645,7 +38763,7 @@ function recordFileUnsynced(args) {
|
|
|
37645
38763
|
});
|
|
37646
38764
|
}
|
|
37647
38765
|
async function drainUnsyncedOps(args) {
|
|
37648
|
-
const { workspaceSlug, baseUrl, pat, getState, applyState, fetchImpl, mutex, output, echo } = args;
|
|
38766
|
+
const { workspaceSlug, baseUrl, pat, getState, applyState, fetchImpl, mutex: mutex2, output, echo } = args;
|
|
37649
38767
|
const now = args.now ?? (() => new Date);
|
|
37650
38768
|
const ops = listUnsynced(getState());
|
|
37651
38769
|
let attempted = 0;
|
|
@@ -37662,7 +38780,7 @@ async function drainUnsyncedOps(args) {
|
|
|
37662
38780
|
attempted += 1;
|
|
37663
38781
|
try {
|
|
37664
38782
|
if (op.target === "entity") {
|
|
37665
|
-
const absolutePath =
|
|
38783
|
+
const absolutePath = join14(workspaceMirrorPath(workspaceSlug), op.path);
|
|
37666
38784
|
const classified = classifyMirrorPath({ workspaceSlug, absolutePath });
|
|
37667
38785
|
if (!classified) {
|
|
37668
38786
|
clear("entity", op.path);
|
|
@@ -37679,7 +38797,7 @@ async function drainUnsyncedOps(args) {
|
|
|
37679
38797
|
state: base2,
|
|
37680
38798
|
exists: op.op === "upsert",
|
|
37681
38799
|
unlinkContext: null,
|
|
37682
|
-
deps: { fetchImpl, now, mutex, output, echo }
|
|
38800
|
+
deps: { fetchImpl, now, mutex: mutex2, output, echo }
|
|
37683
38801
|
});
|
|
37684
38802
|
applyState(result.base ?? base2, result.state);
|
|
37685
38803
|
if (result.outcome.kind === "ignored" && result.outcome.reason === DELETE_DECLINED_PATH_EXISTS) {
|
|
@@ -37707,7 +38825,7 @@ async function drainUnsyncedOps(args) {
|
|
|
37707
38825
|
deps: { fetchImpl, now, output }
|
|
37708
38826
|
});
|
|
37709
38827
|
};
|
|
37710
|
-
const result =
|
|
38828
|
+
const result = mutex2 ? await mutex2.run(`files:${op.path}`, doFile) : await doFile();
|
|
37711
38829
|
applyState(base2, result.state);
|
|
37712
38830
|
}
|
|
37713
38831
|
clear(op.target, op.path);
|
|
@@ -38475,9 +39593,9 @@ class NodeFsHandler {
|
|
|
38475
39593
|
if (this.fsw.closed) {
|
|
38476
39594
|
return;
|
|
38477
39595
|
}
|
|
38478
|
-
const
|
|
39596
|
+
const dirname12 = sp.dirname(file2);
|
|
38479
39597
|
const basename7 = sp.basename(file2);
|
|
38480
|
-
const parent = this.fsw._getWatchedDir(
|
|
39598
|
+
const parent = this.fsw._getWatchedDir(dirname12);
|
|
38481
39599
|
let prevStats = stats;
|
|
38482
39600
|
if (parent.has(basename7))
|
|
38483
39601
|
return;
|
|
@@ -38504,7 +39622,7 @@ class NodeFsHandler {
|
|
|
38504
39622
|
prevStats = newStats2;
|
|
38505
39623
|
}
|
|
38506
39624
|
} catch (error52) {
|
|
38507
|
-
this.fsw._remove(
|
|
39625
|
+
this.fsw._remove(dirname12, basename7);
|
|
38508
39626
|
}
|
|
38509
39627
|
} else if (parent.has(basename7)) {
|
|
38510
39628
|
const at = newStats.atimeMs;
|
|
@@ -39592,7 +40710,12 @@ function startWatcher(opts, handler) {
|
|
|
39592
40710
|
});
|
|
39593
40711
|
watcher.on("add", (p) => classifier.onAdd(p));
|
|
39594
40712
|
watcher.on("change", (p) => classifier.onChange(p));
|
|
39595
|
-
watcher.on("unlink", (p) =>
|
|
40713
|
+
watcher.on("unlink", (p) => {
|
|
40714
|
+
try {
|
|
40715
|
+
opts.onUnlink?.(p);
|
|
40716
|
+
} catch {}
|
|
40717
|
+
classifier.onUnlink(p);
|
|
40718
|
+
});
|
|
39596
40719
|
return {
|
|
39597
40720
|
async stop() {
|
|
39598
40721
|
classifier.shutdown();
|
|
@@ -39631,7 +40754,7 @@ function readBlobForPush(args) {
|
|
|
39631
40754
|
}
|
|
39632
40755
|
let bytes;
|
|
39633
40756
|
try {
|
|
39634
|
-
bytes =
|
|
40757
|
+
bytes = readFileSync21(absolutePath);
|
|
39635
40758
|
} catch (err) {
|
|
39636
40759
|
if (err?.code === "ENOENT")
|
|
39637
40760
|
return { kind: "gone" };
|
|
@@ -39941,7 +41064,7 @@ function applyArtistMemoryOnPull(args) {
|
|
|
39941
41064
|
const cloudMemory = live.memory;
|
|
39942
41065
|
const generated = live.memory_generated === true;
|
|
39943
41066
|
const local = readArtistMemoryFile(workspaceSlug, slug);
|
|
39944
|
-
const refuse = (localContent,
|
|
41067
|
+
const refuse = (localContent, canonical2) => {
|
|
39945
41068
|
const refused = refuseGeneratedMemoryEdit({
|
|
39946
41069
|
state,
|
|
39947
41070
|
workspaceSlug,
|
|
@@ -39951,7 +41074,7 @@ function applyArtistMemoryOnPull(args) {
|
|
|
39951
41074
|
conflictEntity: ENTITY_TYPE,
|
|
39952
41075
|
conflictSlug: conflictSlug(slug),
|
|
39953
41076
|
localContent,
|
|
39954
|
-
canonicalBody:
|
|
41077
|
+
canonicalBody: canonical2,
|
|
39955
41078
|
writeCanonical: (body) => writeMemory(workspaceSlug, slug, body, suppressor),
|
|
39956
41079
|
now,
|
|
39957
41080
|
output: out
|
|
@@ -40055,7 +41178,7 @@ async function pushArtistMemory(args) {
|
|
|
40055
41178
|
id: entry2.id,
|
|
40056
41179
|
fetchImpl
|
|
40057
41180
|
});
|
|
40058
|
-
const
|
|
41181
|
+
const canonical2 = row.memory ?? "";
|
|
40059
41182
|
const refused = refuseGeneratedMemoryEdit({
|
|
40060
41183
|
state: base2,
|
|
40061
41184
|
workspaceSlug,
|
|
@@ -40065,7 +41188,7 @@ async function pushArtistMemory(args) {
|
|
|
40065
41188
|
conflictEntity: ENTITY_TYPE,
|
|
40066
41189
|
conflictSlug: conflictSlug(slug),
|
|
40067
41190
|
localContent: local,
|
|
40068
|
-
canonicalBody:
|
|
41191
|
+
canonicalBody: canonical2,
|
|
40069
41192
|
writeCanonical: (body) => writeMemory(workspaceSlug, slug, body, suppressor),
|
|
40070
41193
|
now,
|
|
40071
41194
|
output
|
|
@@ -40074,7 +41197,7 @@ async function pushArtistMemory(args) {
|
|
|
40074
41197
|
...entry2,
|
|
40075
41198
|
updated_at: row.updated_at,
|
|
40076
41199
|
revision: row.revision,
|
|
40077
|
-
memory_hash: stableHash(
|
|
41200
|
+
memory_hash: stableHash(canonical2),
|
|
40078
41201
|
memory_generated: row.memory_generated
|
|
40079
41202
|
};
|
|
40080
41203
|
return {
|
|
@@ -40170,7 +41293,7 @@ function adoptCloudMemory(args) {
|
|
|
40170
41293
|
return persistEntry2(state, updated);
|
|
40171
41294
|
}
|
|
40172
41295
|
async function reconcileArtistMemory(args) {
|
|
40173
|
-
const { workspaceSlug, baseUrl, pat, fetchImpl, output, suppressor, mutex } = args;
|
|
41296
|
+
const { workspaceSlug, baseUrl, pat, fetchImpl, output, suppressor, mutex: mutex2 } = args;
|
|
40174
41297
|
const now = args.now ?? (() => new Date);
|
|
40175
41298
|
let state = args.state;
|
|
40176
41299
|
let transitioned = 0;
|
|
@@ -40212,7 +41335,7 @@ async function reconcileArtistMemory(args) {
|
|
|
40212
41335
|
now,
|
|
40213
41336
|
output,
|
|
40214
41337
|
suppressor,
|
|
40215
|
-
mutex
|
|
41338
|
+
mutex: mutex2
|
|
40216
41339
|
});
|
|
40217
41340
|
state = result.state;
|
|
40218
41341
|
if (result.outcome.kind === "pushed")
|
|
@@ -40692,10 +41815,10 @@ function anyUnknown(state) {
|
|
|
40692
41815
|
}
|
|
40693
41816
|
|
|
40694
41817
|
// src/lib/memory/write.ts
|
|
40695
|
-
import { readFileSync as
|
|
41818
|
+
import { readFileSync as readFileSync22, statSync as statSync11 } from "node:fs";
|
|
40696
41819
|
|
|
40697
41820
|
// src/lib/files/extracted-text.ts
|
|
40698
|
-
import { createHash as
|
|
41821
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
40699
41822
|
var DERIVED_TEXT_VERSION = 2;
|
|
40700
41823
|
function yamlString3(value) {
|
|
40701
41824
|
return JSON.stringify(value);
|
|
@@ -40769,7 +41892,7 @@ ${doc2.text}
|
|
|
40769
41892
|
`;
|
|
40770
41893
|
}
|
|
40771
41894
|
function extractedTextHash(rendered) {
|
|
40772
|
-
return
|
|
41895
|
+
return createHash5("sha256").update(rendered, "utf8").digest("hex");
|
|
40773
41896
|
}
|
|
40774
41897
|
|
|
40775
41898
|
// src/lib/memory/splice.ts
|
|
@@ -40887,7 +42010,7 @@ function writeDocumentAnnotation(args) {
|
|
|
40887
42010
|
}
|
|
40888
42011
|
let current;
|
|
40889
42012
|
try {
|
|
40890
|
-
current =
|
|
42013
|
+
current = readFileSync22(target, "utf8");
|
|
40891
42014
|
} catch {
|
|
40892
42015
|
return { kind: "absent" };
|
|
40893
42016
|
}
|
|
@@ -41716,6 +42839,109 @@ class PullLoop {
|
|
|
41716
42839
|
}
|
|
41717
42840
|
}
|
|
41718
42841
|
|
|
42842
|
+
// src/lib/high-water.ts
|
|
42843
|
+
var HIGH_WATER_BASE_READ_MS = 60 * 60 * 1000;
|
|
42844
|
+
function baseReadIntervalMs(env2 = process.env) {
|
|
42845
|
+
const raw = Number(env2.PLINTH_HIGH_WATER_BASE_READ_MS);
|
|
42846
|
+
return Number.isFinite(raw) && raw > 0 ? raw : HIGH_WATER_BASE_READ_MS;
|
|
42847
|
+
}
|
|
42848
|
+
function provenMark(args) {
|
|
42849
|
+
if (!args.clean)
|
|
42850
|
+
return null;
|
|
42851
|
+
return args.finalHighWater ?? args.advancedTo;
|
|
42852
|
+
}
|
|
42853
|
+
function createBaseReadSchedule(bootMs, intervalMs = HIGH_WATER_BASE_READ_MS) {
|
|
42854
|
+
let lastBaseReadMs = bootMs;
|
|
42855
|
+
let restoresWaiting = false;
|
|
42856
|
+
let lastUsedHighWater = false;
|
|
42857
|
+
return {
|
|
42858
|
+
useHighWater(nowMs) {
|
|
42859
|
+
if (restoresWaiting)
|
|
42860
|
+
return lastUsedHighWater;
|
|
42861
|
+
return nowMs - lastBaseReadMs < intervalMs;
|
|
42862
|
+
},
|
|
42863
|
+
completed(nowMs, usedHighWater, ok, restoresDeferred = 0) {
|
|
42864
|
+
if (!ok)
|
|
42865
|
+
return;
|
|
42866
|
+
if (!usedHighWater)
|
|
42867
|
+
lastBaseReadMs = nowMs;
|
|
42868
|
+
restoresWaiting = restoresDeferred > 0;
|
|
42869
|
+
lastUsedHighWater = usedHighWater;
|
|
42870
|
+
}
|
|
42871
|
+
};
|
|
42872
|
+
}
|
|
42873
|
+
|
|
42874
|
+
// src/lib/restore-deferral.ts
|
|
42875
|
+
var RESTORE_DEFERRAL_MS = 5 * 60000;
|
|
42876
|
+
var UNEXPLAINED_GRACE_MS = 5000;
|
|
42877
|
+
function keyOf(absolutePath) {
|
|
42878
|
+
return absolutePath.normalize("NFC");
|
|
42879
|
+
}
|
|
42880
|
+
function sightingKey(key, version2) {
|
|
42881
|
+
return `${key}\x00${version2}`;
|
|
42882
|
+
}
|
|
42883
|
+
function forgetSightings(recent, key) {
|
|
42884
|
+
const prefix = `${key}\x00`;
|
|
42885
|
+
for (const k of recent.keys())
|
|
42886
|
+
if (k.startsWith(prefix))
|
|
42887
|
+
recent.delete(k);
|
|
42888
|
+
}
|
|
42889
|
+
function noteUnlink(recent, absolutePath, nowMs) {
|
|
42890
|
+
forgetExpired(recent, nowMs);
|
|
42891
|
+
const key = keyOf(absolutePath);
|
|
42892
|
+
forgetSightings(recent, key);
|
|
42893
|
+
recent.set(key, { kind: "unlink", at: nowMs, logged: false });
|
|
42894
|
+
}
|
|
42895
|
+
function deferRestore(recent, absolutePath, version2, nowMs) {
|
|
42896
|
+
if (!recent)
|
|
42897
|
+
return { defer: false, first: false };
|
|
42898
|
+
forgetExpired(recent, nowMs);
|
|
42899
|
+
const key = keyOf(absolutePath);
|
|
42900
|
+
const unlinked = recent.get(key);
|
|
42901
|
+
if (unlinked?.kind === "unlink") {
|
|
42902
|
+
if (unlinked.logged)
|
|
42903
|
+
return { defer: true, first: false };
|
|
42904
|
+
recent.set(key, { ...unlinked, logged: true });
|
|
42905
|
+
return { defer: true, first: true };
|
|
42906
|
+
}
|
|
42907
|
+
const sighting = sightingKey(key, version2);
|
|
42908
|
+
const seen = recent.get(sighting);
|
|
42909
|
+
if (seen === undefined) {
|
|
42910
|
+
recent.set(sighting, { kind: "missing", at: nowMs });
|
|
42911
|
+
return { defer: true, first: false };
|
|
42912
|
+
}
|
|
42913
|
+
if (nowMs - seen.at < UNEXPLAINED_GRACE_MS)
|
|
42914
|
+
return { defer: true, first: false };
|
|
42915
|
+
recent.delete(sighting);
|
|
42916
|
+
return { defer: false, first: false };
|
|
42917
|
+
}
|
|
42918
|
+
function presentAgain(recent, absolutePath) {
|
|
42919
|
+
if (recent)
|
|
42920
|
+
forgetSightings(recent, keyOf(absolutePath));
|
|
42921
|
+
}
|
|
42922
|
+
function forgetExpired(recent, nowMs) {
|
|
42923
|
+
for (const [key, seen] of recent) {
|
|
42924
|
+
if (seen.kind === "unlink" && nowMs - seen.at >= RESTORE_DEFERRAL_MS)
|
|
42925
|
+
recent.delete(key);
|
|
42926
|
+
}
|
|
42927
|
+
}
|
|
42928
|
+
|
|
42929
|
+
// src/lib/ambient-poll.ts
|
|
42930
|
+
function createAmbientPollWiring(opts) {
|
|
42931
|
+
const recentUnlinks = new Map;
|
|
42932
|
+
const baseReads = createBaseReadSchedule(opts.now(), opts.baseReadIntervalMs);
|
|
42933
|
+
return {
|
|
42934
|
+
onUnlink: (absolutePath) => noteUnlink(recentUnlinks, absolutePath, opts.now()),
|
|
42935
|
+
async poll(run) {
|
|
42936
|
+
const startMs = opts.now();
|
|
42937
|
+
const useHighWater = baseReads.useHighWater(startMs);
|
|
42938
|
+
const result = await run({ useHighWater, recentUnlinks });
|
|
42939
|
+
baseReads.completed(startMs, useHighWater, result.exitCode === 0, result.restoresDeferred ?? 0);
|
|
42940
|
+
return result;
|
|
42941
|
+
}
|
|
42942
|
+
};
|
|
42943
|
+
}
|
|
42944
|
+
|
|
41719
42945
|
// src/lib/calendar-loop.ts
|
|
41720
42946
|
var DEFAULT_CALENDAR_INTERVAL_MS = 900000;
|
|
41721
42947
|
var CALENDAR_BACKOFF_CAP_MS = 3600000;
|
|
@@ -41876,6 +43102,12 @@ var CALENDAR_STATUSES = [
|
|
|
41876
43102
|
"token-expired",
|
|
41877
43103
|
"google-error"
|
|
41878
43104
|
];
|
|
43105
|
+
var KNOWN_TRUNCATED_REASONS = ["page_limit", "time_limit"];
|
|
43106
|
+
var TruncatedReasonSchema = exports_external.string().nullish().transform((v) => {
|
|
43107
|
+
if (v === null || v === undefined)
|
|
43108
|
+
return null;
|
|
43109
|
+
return KNOWN_TRUNCATED_REASONS.includes(v) ? v : "other";
|
|
43110
|
+
});
|
|
41879
43111
|
var CalendarRowSchema = exports_external.object({
|
|
41880
43112
|
artist_id: exports_external.string().nullable(),
|
|
41881
43113
|
artist_slug: exports_external.string().nullable(),
|
|
@@ -41884,8 +43116,13 @@ var CalendarRowSchema = exports_external.object({
|
|
|
41884
43116
|
calendar_name: exports_external.string(),
|
|
41885
43117
|
status: exports_external.enum(CALENDAR_STATUSES),
|
|
41886
43118
|
events: exports_external.array(CalendarEventSchema).nullable(),
|
|
41887
|
-
error: exports_external.object({
|
|
41888
|
-
|
|
43119
|
+
error: exports_external.object({
|
|
43120
|
+
upstream_status: exports_external.number().nullable(),
|
|
43121
|
+
message: exports_external.string()
|
|
43122
|
+
}).nullish().transform((v) => v ?? null),
|
|
43123
|
+
truncated: exports_external.boolean().optional().default(false),
|
|
43124
|
+
truncated_reason: TruncatedReasonSchema,
|
|
43125
|
+
resume_from: exports_external.string().nullish().transform((v) => v ?? null)
|
|
41889
43126
|
});
|
|
41890
43127
|
var CalendarResponseSchema = exports_external.object({
|
|
41891
43128
|
generated_at: exports_external.string().min(1),
|
|
@@ -42073,6 +43310,62 @@ function readAsOf(text, today) {
|
|
|
42073
43310
|
return { parts, yearInferred: first.yearInferred };
|
|
42074
43311
|
}
|
|
42075
43312
|
|
|
43313
|
+
// src/lib/calendar/limits.ts
|
|
43314
|
+
var CALENDAR_RANGE_TOOL = "list_calendar_events";
|
|
43315
|
+
function spanPhrase(timeMin, timeMax) {
|
|
43316
|
+
const ms = Date.parse(timeMax) - Date.parse(timeMin);
|
|
43317
|
+
if (!Number.isFinite(ms) || ms <= 0)
|
|
43318
|
+
return null;
|
|
43319
|
+
const weeks = ms / (7 * DAY_MS);
|
|
43320
|
+
if (Number.isInteger(weeks))
|
|
43321
|
+
return `${weeks} ${weeks === 1 ? "week" : "weeks"}`;
|
|
43322
|
+
const days = ms / DAY_MS;
|
|
43323
|
+
if (Number.isInteger(days))
|
|
43324
|
+
return `${days} ${days === 1 ? "day" : "days"}`;
|
|
43325
|
+
return null;
|
|
43326
|
+
}
|
|
43327
|
+
function coverageLine(timeMin, timeMax) {
|
|
43328
|
+
const span = spanPhrase(timeMin, timeMax);
|
|
43329
|
+
return `**This file holds only the window above${span ? ` (${span})` : ""}.** ` + `When a question needs a date outside it, past or future, call the Plinth ` + `tool \`${CALENDAR_RANGE_TOOL}\` for just that range.`;
|
|
43330
|
+
}
|
|
43331
|
+
var TRUNCATION_CAUSE = {
|
|
43332
|
+
page_limit: "Google had more entries in this window than one refresh reads.",
|
|
43333
|
+
time_limit: "The read from Google ran out of time.",
|
|
43334
|
+
other: "The read stopped before the end of the window."
|
|
43335
|
+
};
|
|
43336
|
+
var TRUNCATION_SHORT = {
|
|
43337
|
+
page_limit: "stopped at the page limit",
|
|
43338
|
+
time_limit: "ran out of time",
|
|
43339
|
+
other: "stopped before the end of the window"
|
|
43340
|
+
};
|
|
43341
|
+
var OFFSET_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})$/i;
|
|
43342
|
+
function resumeRange(resumeFrom, window2) {
|
|
43343
|
+
if (resumeFrom === null || !OFFSET_TIMESTAMP_RE.test(resumeFrom))
|
|
43344
|
+
return null;
|
|
43345
|
+
if (!OFFSET_TIMESTAMP_RE.test(window2.time_min) || !OFFSET_TIMESTAMP_RE.test(window2.time_max)) {
|
|
43346
|
+
return null;
|
|
43347
|
+
}
|
|
43348
|
+
const at = Date.parse(resumeFrom);
|
|
43349
|
+
const min = Date.parse(window2.time_min);
|
|
43350
|
+
const max = Date.parse(window2.time_max);
|
|
43351
|
+
if (!(at > min && at < max))
|
|
43352
|
+
return null;
|
|
43353
|
+
return { from: new Date(at).toISOString(), to: new Date(max).toISOString() };
|
|
43354
|
+
}
|
|
43355
|
+
function incompleteLine(args) {
|
|
43356
|
+
const { stamp: stamp2, eventCount, reason, resumeFrom, window: window2 } = args;
|
|
43357
|
+
const kept = eventCount === 0 ? "No entries were collected before the read stopped, so this is NOT a clear diary." : `${eventCount} ${eventCount === 1 ? "entry was" : "entries were"} collected before the read stopped.`;
|
|
43358
|
+
const range = resumeRange(resumeFrom, window2);
|
|
43359
|
+
const pointer = range ? ` For the rest, call the Plinth tool \`${CALENDAR_RANGE_TOOL}\` from ` + `\`${range.from}\` to \`${range.to}\` (a few entries repeat).` : ` For the rest, call the Plinth tool \`${CALENDAR_RANGE_TOOL}\` for a ` + `narrower range inside the window.`;
|
|
43360
|
+
return `**Read as of ${stamp2}, but this list is INCOMPLETE.** ` + `${TRUNCATION_CAUSE[reason ?? "other"]} ${kept} ` + `The rest of the window is not included: unknown, not absent.${pointer}`;
|
|
43361
|
+
}
|
|
43362
|
+
function sentence(text) {
|
|
43363
|
+
return /[.!?\u2026]$/.test(text) ? text : `${text}.`;
|
|
43364
|
+
}
|
|
43365
|
+
function truncationShort(reason) {
|
|
43366
|
+
return TRUNCATION_SHORT[reason ?? "other"];
|
|
43367
|
+
}
|
|
43368
|
+
|
|
42076
43369
|
// src/lib/calendar/render.ts
|
|
42077
43370
|
var CALENDAR_BODY_MARKER = "<!-- PLINTH:CALENDAR-BODY -->";
|
|
42078
43371
|
var OWNERSHIP_NOTE2 = [
|
|
@@ -42176,37 +43469,61 @@ function windowLine(timeMin, timeMax) {
|
|
|
42176
43469
|
return `${timeMin} to ${timeMax}`;
|
|
42177
43470
|
return `${formatDay(from, -1)} to ${formatDay(to, -1)}`;
|
|
42178
43471
|
}
|
|
43472
|
+
function isRefusal(error52) {
|
|
43473
|
+
return error52.upstream_status !== null && error52.upstream_status >= 400;
|
|
43474
|
+
}
|
|
43475
|
+
function googleErrorLine(error52) {
|
|
43476
|
+
if (error52 !== null && !isRefusal(error52)) {
|
|
43477
|
+
return `**NOT READ - the read from Google did not complete.** ` + `${sentence(oneLine2(error52.message, 300))} The next refresh retries; until then ` + `nothing below is current, so treat this calendar as unknown rather than empty. ` + `For a date on it you need now, call the Plinth tool \`${CALENDAR_RANGE_TOOL}\` ` + `for a narrower range.`;
|
|
43478
|
+
}
|
|
43479
|
+
return `**NOT READ - Google refused the request` + `${error52 ? ` (HTTP ${error52.upstream_status})` : ""}.** ` + `${error52 ? sentence(oneLine2(error52.message, 300)) : "No detail was returned."} ` + `A calendar that was unshared or deleted in Google reads exactly like this. ` + `Nothing below is current; treat this calendar as unknown rather than empty.`;
|
|
43480
|
+
}
|
|
42179
43481
|
function statusLine(args) {
|
|
42180
43482
|
const { status, eventCount, error: error52, truncated, stamp: stamp2, userLabel } = args;
|
|
42181
43483
|
switch (status) {
|
|
42182
43484
|
case "fetched": {
|
|
43485
|
+
if (truncated) {
|
|
43486
|
+
return incompleteLine({
|
|
43487
|
+
stamp: stamp2,
|
|
43488
|
+
eventCount,
|
|
43489
|
+
reason: args.truncatedReason,
|
|
43490
|
+
resumeFrom: args.resumeFrom,
|
|
43491
|
+
window: args.window
|
|
43492
|
+
});
|
|
43493
|
+
}
|
|
42183
43494
|
if (eventCount === 0) {
|
|
42184
43495
|
return `**Read fine as of ${stamp2}. Nothing in this window.** The diary really ` + `is clear across the dates above. This is a successful read of an empty ` + `calendar, not a failed read - if it had failed, this line would say so.`;
|
|
42185
43496
|
}
|
|
42186
|
-
|
|
42187
|
-
return `**Read fine as of ${stamp2}.** ${eventCount} ` + `${eventCount === 1 ? "entry" : "entries"} in the window.${truncatedNote}`;
|
|
43497
|
+
return `**Read fine as of ${stamp2}.** ${eventCount} ` + `${eventCount === 1 ? "entry" : "entries"} in the window.`;
|
|
42188
43498
|
}
|
|
42189
43499
|
case "no-token":
|
|
42190
43500
|
return `**NOT READ - Google is not connected for ${userLabel}.** Nothing is listed ` + `below because Plinth could not look, NOT because the diary is clear. ` + `Connect Google in Plinth to fill this in, and until then treat this ` + `calendar as unknown rather than empty.`;
|
|
42191
43501
|
case "token-expired":
|
|
42192
43502
|
return `**NOT READ - the Google connection for ${userLabel} has expired.** Plinth ` + `still holds a connection but could not refresh it, so nothing below is ` + `current. Reconnect Google in Plinth. Treat this calendar as unknown ` + `rather than empty.`;
|
|
42193
43503
|
case "google-error":
|
|
42194
|
-
return
|
|
43504
|
+
return googleErrorLine(error52);
|
|
42195
43505
|
}
|
|
42196
43506
|
}
|
|
42197
43507
|
function shortStatus(row) {
|
|
42198
43508
|
switch (row.status) {
|
|
42199
43509
|
case "fetched": {
|
|
42200
43510
|
const n = row.events?.length ?? 0;
|
|
43511
|
+
const count = `${n} ${n === 1 ? "entry" : "entries"}`;
|
|
43512
|
+
if (row.truncated) {
|
|
43513
|
+
return `read, ${count}, INCOMPLETE (${truncationShort(row.truncated_reason)}); ` + `the rest of the window is unknown, not empty`;
|
|
43514
|
+
}
|
|
42201
43515
|
if (n === 0)
|
|
42202
43516
|
return "read fine, nothing in this window";
|
|
42203
|
-
return `read fine, ${
|
|
43517
|
+
return `read fine, ${count}`;
|
|
42204
43518
|
}
|
|
42205
43519
|
case "no-token":
|
|
42206
43520
|
return "NOT READ - Google is not connected; unknown, not empty";
|
|
42207
43521
|
case "token-expired":
|
|
42208
43522
|
return "NOT READ - the Google connection has expired; unknown, not empty";
|
|
42209
43523
|
case "google-error":
|
|
43524
|
+
if (row.error !== null && !isRefusal(row.error)) {
|
|
43525
|
+
return `NOT READ - the read from Google did not complete: ` + `${oneLine2(row.error.message, 120)}; the next refresh retries; unknown, not empty`;
|
|
43526
|
+
}
|
|
42210
43527
|
return `NOT READ - Google refused` + `${row.error ? ` (HTTP ${row.error.upstream_status}): ${oneLine2(row.error.message, 120)}` : ""}` + `; unknown, not empty`;
|
|
42211
43528
|
}
|
|
42212
43529
|
}
|
|
@@ -42264,21 +43581,25 @@ function renderArtistCalendar(args) {
|
|
|
42264
43581
|
eventCount: events.length,
|
|
42265
43582
|
error: row.error,
|
|
42266
43583
|
truncated: row.truncated,
|
|
43584
|
+
truncatedReason: row.truncated_reason,
|
|
43585
|
+
resumeFrom: row.resume_from,
|
|
43586
|
+
window: window2,
|
|
42267
43587
|
stamp: stamp2,
|
|
42268
43588
|
userLabel
|
|
42269
43589
|
}),
|
|
42270
43590
|
"",
|
|
42271
43591
|
`**Window:** ${windowLine(window2.time_min, window2.time_max)}.`,
|
|
42272
43592
|
"",
|
|
43593
|
+
...row.status === "fetched" ? [coverageLine(window2.time_min, window2.time_max), ""] : [],
|
|
42273
43594
|
"**Dates are read here and changed in Plinth's calendar view, by a person.** If something below needs moving, say what the change is and hand it over.",
|
|
42274
43595
|
"",
|
|
42275
|
-
renderBands(events, generatedAt, referenceYear, "")
|
|
43596
|
+
renderBands(events, generatedAt, referenceYear, "", row.truncated)
|
|
42276
43597
|
].join(`
|
|
42277
43598
|
`);
|
|
42278
43599
|
return `${header}${CALENDAR_BODY_MARKER}
|
|
42279
43600
|
${body}`;
|
|
42280
43601
|
}
|
|
42281
|
-
function renderBands(events, generatedAt, referenceYear, artistPrefix) {
|
|
43602
|
+
function renderBands(events, generatedAt, referenceYear, artistPrefix, truncated) {
|
|
42282
43603
|
if (events.length === 0)
|
|
42283
43604
|
return "";
|
|
42284
43605
|
const { full, rest } = splitBands(events, generatedAt);
|
|
@@ -42292,7 +43613,7 @@ function renderBands(events, generatedAt, referenceYear, artistPrefix) {
|
|
|
42292
43613
|
}
|
|
42293
43614
|
out.push("", "## Rest of the window", "");
|
|
42294
43615
|
if (rest.length === 0) {
|
|
42295
|
-
out.push("_Nothing further in the window._");
|
|
43616
|
+
out.push(truncated ? "_Nothing further was collected: the read stopped before the end of the window._" : "_Nothing further in the window._");
|
|
42296
43617
|
} else {
|
|
42297
43618
|
for (const e of rest)
|
|
42298
43619
|
out.push(compactLine(e, referenceYear, artistPrefix));
|
|
@@ -42301,6 +43622,9 @@ function renderBands(events, generatedAt, referenceYear, artistPrefix) {
|
|
|
42301
43622
|
return out.join(`
|
|
42302
43623
|
`);
|
|
42303
43624
|
}
|
|
43625
|
+
function lacksConnection(row) {
|
|
43626
|
+
return row.status === "no-token" || row.status === "token-expired";
|
|
43627
|
+
}
|
|
42304
43628
|
function renderUpcoming(args) {
|
|
42305
43629
|
const {
|
|
42306
43630
|
rows,
|
|
@@ -42316,6 +43640,7 @@ function renderUpcoming(args) {
|
|
|
42316
43640
|
const fetched = rows.filter((r) => r.status === "fetched");
|
|
42317
43641
|
const failed = rows.filter((r) => r.status !== "fetched");
|
|
42318
43642
|
const truncatedAnywhere = rows.some((r) => r.truncated);
|
|
43643
|
+
const cutShort = fetched.filter((r) => r.truncated).length;
|
|
42319
43644
|
const merged = fetched.flatMap((row) => (row.events ?? []).map((event) => ({ event, row }))).sort((a, b) => sortInstant(a.event) - sortInstant(b.event));
|
|
42320
43645
|
const { full, rest } = splitBands(merged.map((m) => m.event), generatedAt);
|
|
42321
43646
|
const fullSet = new Set(full);
|
|
@@ -42346,7 +43671,12 @@ function renderUpcoming(args) {
|
|
|
42346
43671
|
"",
|
|
42347
43672
|
`**As of ${stamp2}.** Window ${windowLine(window2.time_min, window2.time_max)}.`,
|
|
42348
43673
|
"",
|
|
43674
|
+
...rows.length > 0 && !rows.every(lacksConnection) ? [coverageLine(window2.time_min, window2.time_max), ""] : [],
|
|
42349
43675
|
failed.length === 0 ? `**All ${fetched.length} mapped ${fetched.length === 1 ? "calendar was" : "calendars were"} read.**` : `**${fetched.length} of ${rows.length} mapped ${rows.length === 1 ? "calendar" : "calendars"} read; ${failed.length} could NOT be read.**` + ` Anything on an unread calendar is missing from the lists below - unknown, not absent.`,
|
|
43676
|
+
...cutShort === 0 ? [] : [
|
|
43677
|
+
"",
|
|
43678
|
+
`**${cutShort} of the calendars read ${cutShort === 1 ? "was" : "were"} cut short** before the end ` + `of the window, so entries past the point where ${cutShort === 1 ? "it" : "each"} stopped are ` + `missing from the lists below - unknown, not absent. The status list says which.`
|
|
43679
|
+
],
|
|
42350
43680
|
"",
|
|
42351
43681
|
`${artistCount} ${artistCount === 1 ? "artist" : "artists"} in the roster; ` + `${mappedArtistCount} ${mappedArtistCount === 1 ? "has" : "have"} a calendar mapped in Plinth.`,
|
|
42352
43682
|
"",
|
|
@@ -42363,7 +43693,7 @@ function renderUpcoming(args) {
|
|
|
42363
43693
|
body.push(`## Next ${FULL_BAND_DAYS} days`, "", "_Links are in the per-artist file named beside each calendar above._", "");
|
|
42364
43694
|
const fullMerged = merged.filter((m) => fullSet.has(m.event));
|
|
42365
43695
|
if (fullMerged.length === 0) {
|
|
42366
|
-
body.push(fetched.length === 0 ? `_No calendar could be read, so nothing can be listed for the next ${FULL_BAND_DAYS} days._` : `_Nothing in the next ${FULL_BAND_DAYS} days on any calendar that was read._`);
|
|
43696
|
+
body.push(fetched.length === 0 ? `_No calendar could be read, so nothing can be listed for the next ${FULL_BAND_DAYS} days._` : cutShort > 0 ? `_Nothing collected for the next ${FULL_BAND_DAYS} days on any calendar that was read; the ones cut short may be missing entries._` : `_Nothing in the next ${FULL_BAND_DAYS} days on any calendar that was read._`);
|
|
42367
43697
|
} else {
|
|
42368
43698
|
for (const { event, row } of fullMerged) {
|
|
42369
43699
|
body.push(fullLine(event, referenceYear, `**${rowDisplayName(row)}**`, false));
|
|
@@ -42372,7 +43702,7 @@ function renderUpcoming(args) {
|
|
|
42372
43702
|
body.push("", "## Rest of the window", "");
|
|
42373
43703
|
const restMerged = merged.filter((m) => restSet.has(m.event));
|
|
42374
43704
|
if (restMerged.length === 0) {
|
|
42375
|
-
body.push(fetched.length === 0 ? "_No calendar could be read, so nothing can be listed._" : "_Nothing further in the window on any calendar that was read._");
|
|
43705
|
+
body.push(fetched.length === 0 ? "_No calendar could be read, so nothing can be listed._" : cutShort > 0 ? "_Nothing further was collected on any calendar that was read; the ones cut short may be missing entries past where they stopped._" : "_Nothing further in the window on any calendar that was read._");
|
|
42376
43706
|
} else {
|
|
42377
43707
|
for (const { event, row } of restMerged) {
|
|
42378
43708
|
body.push(compactLine(event, referenceYear, `**${rowDisplayName(row)}**`));
|
|
@@ -42619,7 +43949,7 @@ class PullSuppressor {
|
|
|
42619
43949
|
|
|
42620
43950
|
// src/lib/discover-entities.ts
|
|
42621
43951
|
var import_gray_matter12 = __toESM(require_gray_matter(), 1);
|
|
42622
|
-
import { readFileSync as
|
|
43952
|
+
import { readFileSync as readFileSync23, statSync as statSync12 } from "node:fs";
|
|
42623
43953
|
var DISCOVERY_WRITE_CAP_DEFAULT = 10;
|
|
42624
43954
|
function discoveryWriteCap() {
|
|
42625
43955
|
const raw = process.env.PLINTH_DISCOVERY_WRITE_CAP;
|
|
@@ -42718,7 +44048,7 @@ function planDiscoveryWrites(args) {
|
|
|
42718
44048
|
counts.discovered++;
|
|
42719
44049
|
let frontmatter;
|
|
42720
44050
|
try {
|
|
42721
|
-
frontmatter = import_gray_matter12.default(
|
|
44051
|
+
frontmatter = import_gray_matter12.default(readFileSync23(absolutePath, "utf8")).data ?? {};
|
|
42722
44052
|
} catch {
|
|
42723
44053
|
counts.discovered--;
|
|
42724
44054
|
continue;
|
|
@@ -42947,7 +44277,7 @@ function recordDiscoveryFailure(args) {
|
|
|
42947
44277
|
|
|
42948
44278
|
// src/lib/frontmatter-id-backfill.ts
|
|
42949
44279
|
var import_gray_matter13 = __toESM(require_gray_matter(), 1);
|
|
42950
|
-
import { readFileSync as
|
|
44280
|
+
import { readFileSync as readFileSync24, statSync as statSync13 } from "node:fs";
|
|
42951
44281
|
var FRONTMATTER_ID_BACKFILL_VERSION = 1;
|
|
42952
44282
|
function backfillWorthReporting(r) {
|
|
42953
44283
|
return r.written > 0 || r.failed > 0 || r.skipped.foreignId > 0 || r.skipped.unwritable > 0 || r.skipped.slugCollision > 0;
|
|
@@ -42990,7 +44320,7 @@ function safeInode3(path5) {
|
|
|
42990
44320
|
function backfillFrontmatterIds(args) {
|
|
42991
44321
|
const { workspaceSlug, state } = args;
|
|
42992
44322
|
const compose = args.compose ?? withFrontmatterId;
|
|
42993
|
-
const readRaw = args.readFile ?? ((p) =>
|
|
44323
|
+
const readRaw = args.readFile ?? ((p) => readFileSync24(p, "utf8"));
|
|
42994
44324
|
const result = emptyResult();
|
|
42995
44325
|
if (state.frontmatter_id_backfill_version === FRONTMATTER_ID_BACKFILL_VERSION) {
|
|
42996
44326
|
return result;
|
|
@@ -43118,7 +44448,7 @@ function backfillFrontmatterIds(args) {
|
|
|
43118
44448
|
}
|
|
43119
44449
|
|
|
43120
44450
|
// src/lib/reconcile.ts
|
|
43121
|
-
import { readFileSync as
|
|
44451
|
+
import { readFileSync as readFileSync25, statSync as statSync14 } from "node:fs";
|
|
43122
44452
|
async function reconcileDivergedRows(args) {
|
|
43123
44453
|
const { workspaceSlug, baseUrl, pat, fetchImpl, output, isFrozen } = args;
|
|
43124
44454
|
const now = args.now ?? (() => new Date);
|
|
@@ -43230,7 +44560,7 @@ async function reconcileDivergedRows(args) {
|
|
|
43230
44560
|
}
|
|
43231
44561
|
let content;
|
|
43232
44562
|
try {
|
|
43233
|
-
content =
|
|
44563
|
+
content = readFileSync25(absolutePath, "utf8");
|
|
43234
44564
|
} catch (err) {
|
|
43235
44565
|
if (err?.code === "ENOENT") {
|
|
43236
44566
|
state = clearEntityCloudRowDeleted(state, strategy.mirrorRelativePath(entry2.slug));
|
|
@@ -43331,7 +44661,7 @@ async function reconcileDivergedRows(args) {
|
|
|
43331
44661
|
}
|
|
43332
44662
|
|
|
43333
44663
|
// src/lib/workspace-rules/sync.ts
|
|
43334
|
-
import { existsSync as
|
|
44664
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
43335
44665
|
import { relative as relative12 } from "node:path";
|
|
43336
44666
|
|
|
43337
44667
|
// src/lib/workspace-rules/api.ts
|
|
@@ -43480,9 +44810,9 @@ function migrateHouseStyleFile(workspaceSlug) {
|
|
|
43480
44810
|
return false;
|
|
43481
44811
|
throw err;
|
|
43482
44812
|
}
|
|
43483
|
-
if (
|
|
44813
|
+
if (existsSync6(target))
|
|
43484
44814
|
return false;
|
|
43485
|
-
if (!
|
|
44815
|
+
if (!existsSync6(legacy))
|
|
43486
44816
|
return false;
|
|
43487
44817
|
return renameContextFile(workspaceSlug, HOUSE_STYLE_LEGACY_FILENAME, WORKSPACE_RULES_FILENAME);
|
|
43488
44818
|
}
|
|
@@ -44580,17 +45910,18 @@ function createThrottledFetch(baseFetch, opts = {}) {
|
|
|
44580
45910
|
|
|
44581
45911
|
// src/commands/sync.ts
|
|
44582
45912
|
import {
|
|
44583
|
-
existsSync as
|
|
45913
|
+
existsSync as existsSync9,
|
|
45914
|
+
lstatSync as lstatSync6,
|
|
44584
45915
|
readdirSync as readdirSync7,
|
|
44585
|
-
readFileSync as
|
|
45916
|
+
readFileSync as readFileSync29,
|
|
44586
45917
|
statSync as statSync17,
|
|
44587
|
-
unlinkSync as
|
|
45918
|
+
unlinkSync as unlinkSync9
|
|
44588
45919
|
} from "node:fs";
|
|
44589
|
-
import { join as
|
|
45920
|
+
import { join as join18 } from "node:path";
|
|
44590
45921
|
|
|
44591
45922
|
// src/lib/conflicts-view.ts
|
|
44592
|
-
import { existsSync as
|
|
44593
|
-
import { join as
|
|
45923
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
45924
|
+
import { join as join17 } from "node:path";
|
|
44594
45925
|
var NO_GENERATED_MEMORY = {
|
|
44595
45926
|
artistMemoryGenerated: () => false,
|
|
44596
45927
|
workspaceMemoryGenerated: () => false,
|
|
@@ -44636,7 +45967,7 @@ function resolveConflictRows(workspaceSlug, stashes, generatedMemory = NO_GENERA
|
|
|
44636
45967
|
if (validateFilesRelPath(rel) === null) {
|
|
44637
45968
|
return { kind: "unsafe-path", stash };
|
|
44638
45969
|
}
|
|
44639
|
-
const abs =
|
|
45970
|
+
const abs = join17(root, "files", rel);
|
|
44640
45971
|
if (hasSymlinkedComponent(workspaceSlug, abs)) {
|
|
44641
45972
|
return { kind: "unsafe-path", stash };
|
|
44642
45973
|
}
|
|
@@ -44644,7 +45975,7 @@ function resolveConflictRows(workspaceSlug, stashes, generatedMemory = NO_GENERA
|
|
|
44644
45975
|
kind: "restorable",
|
|
44645
45976
|
stash,
|
|
44646
45977
|
mirrorPath: abs,
|
|
44647
|
-
mirrorPathExists:
|
|
45978
|
+
mirrorPathExists: existsSync7(abs)
|
|
44648
45979
|
};
|
|
44649
45980
|
});
|
|
44650
45981
|
}
|
|
@@ -44766,9 +46097,385 @@ function refreshConflictsView(workspaceSlug) {
|
|
|
44766
46097
|
}
|
|
44767
46098
|
|
|
44768
46099
|
// src/lib/files/pull.ts
|
|
44769
|
-
import { existsSync as
|
|
44770
|
-
import { dirname as
|
|
46100
|
+
import { existsSync as existsSync8, lstatSync as lstatSync5, readFileSync as readFileSync27, statSync as statSync15 } from "node:fs";
|
|
46101
|
+
import { dirname as dirname13 } from "node:path";
|
|
46102
|
+
|
|
46103
|
+
// src/lib/permanent-disposition.ts
|
|
46104
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
46105
|
+
import { readFileSync as readFileSync26 } from "node:fs";
|
|
46106
|
+
function sha256(value) {
|
|
46107
|
+
return createHash6("sha256").update(value).digest("hex");
|
|
46108
|
+
}
|
|
46109
|
+
function verifyStash(path5, expectedHash) {
|
|
46110
|
+
const actual = sha256(readFileSync26(path5));
|
|
46111
|
+
if (actual !== expectedHash) {
|
|
46112
|
+
throw new Error(`permanent deletion stash verification failed for ${path5}`);
|
|
46113
|
+
}
|
|
46114
|
+
}
|
|
46115
|
+
function disposePermanentDocument(args) {
|
|
46116
|
+
return withPermanentDeletionLock("document", args.tombstone.logicalKey, async () => disposePermanentDocumentLocked(args));
|
|
46117
|
+
}
|
|
46118
|
+
function disposePermanentDocumentLocked(args) {
|
|
46119
|
+
const now = args.now ?? (() => new Date);
|
|
46120
|
+
const t = args.tombstone;
|
|
46121
|
+
recordRetirementPending({
|
|
46122
|
+
workspaceSlug: args.workspaceSlug,
|
|
46123
|
+
kind: "document",
|
|
46124
|
+
logicalKey: t.logicalKey,
|
|
46125
|
+
rowId: t.id,
|
|
46126
|
+
purgeGeneration: t.purgeGeneration,
|
|
46127
|
+
retirementSequence: t.retirementSequence,
|
|
46128
|
+
retiredAt: t.deletedAt
|
|
46129
|
+
});
|
|
46130
|
+
let journal = loadPermanentDeletionJournal(args.workspaceSlug);
|
|
46131
|
+
let record2 = findRetirement(journal, "document", t.logicalKey);
|
|
46132
|
+
if (record2.purge_generation !== t.purgeGeneration) {
|
|
46133
|
+
return { state: args.state, deleted: false };
|
|
46134
|
+
}
|
|
46135
|
+
if (args.liveOwnerId === t.id) {
|
|
46136
|
+
throw new Error("permanent document namespace reports the retired row as live owner");
|
|
46137
|
+
}
|
|
46138
|
+
if (record2.disposition === "complete") {
|
|
46139
|
+
const stale = args.liveOwnerId === null ? getEntryBySlug(args.state, args.strategy.type, t.logicalKey) : getEntry(args.state, args.strategy.type, t.id);
|
|
46140
|
+
const state2 = stale ? removeEntry(args.state, args.strategy.type, stale.id).state : args.state;
|
|
46141
|
+
return { state: state2, deleted: false, stashPath: record2.stash_path };
|
|
46142
|
+
}
|
|
46143
|
+
const retiredEntry = getEntry(args.state, args.strategy.type, t.id);
|
|
46144
|
+
const stateOwner = getEntryBySlug(args.state, args.strategy.type, t.logicalKey);
|
|
46145
|
+
const protectedOwner = args.liveOwnerId !== null && args.liveOwnerId !== t.id;
|
|
46146
|
+
let state = args.state;
|
|
46147
|
+
let deleted = false;
|
|
46148
|
+
let stashPath = record2.stash_path;
|
|
46149
|
+
let stashHash = record2.stash_hash;
|
|
46150
|
+
if (!protectedOwner) {
|
|
46151
|
+
const target = entityFilePath(args.workspaceSlug, args.strategy.type, retiredEntry?.slug ?? t.logicalKey);
|
|
46152
|
+
let raw;
|
|
46153
|
+
try {
|
|
46154
|
+
raw = readFileSync26(target, "utf8");
|
|
46155
|
+
} catch (error52) {
|
|
46156
|
+
if (error52?.code !== "ENOENT")
|
|
46157
|
+
throw error52;
|
|
46158
|
+
}
|
|
46159
|
+
if (raw !== undefined && !stashPath) {
|
|
46160
|
+
let matchesAcknowledged = false;
|
|
46161
|
+
if (retiredEntry) {
|
|
46162
|
+
try {
|
|
46163
|
+
const hashes = computeMirrorHashes(args.strategy, raw);
|
|
46164
|
+
matchesAcknowledged = hashes.bodyHash === retiredEntry.body_hash && hashes.frontmatterHash === retiredEntry.frontmatter_hash;
|
|
46165
|
+
} catch {
|
|
46166
|
+
matchesAcknowledged = false;
|
|
46167
|
+
}
|
|
46168
|
+
}
|
|
46169
|
+
if (!matchesAcknowledged) {
|
|
46170
|
+
const stash = stashEntityConflictOnce({
|
|
46171
|
+
workspaceSlug: args.workspaceSlug,
|
|
46172
|
+
entityType: args.strategy.type,
|
|
46173
|
+
entitySlug: t.logicalKey,
|
|
46174
|
+
timestamp: now(),
|
|
46175
|
+
contents: raw,
|
|
46176
|
+
side: "local"
|
|
46177
|
+
});
|
|
46178
|
+
stashPath = stash.path;
|
|
46179
|
+
stashHash = sha256(raw);
|
|
46180
|
+
verifyStash(stash.path, stashHash);
|
|
46181
|
+
recordRetirementStash({
|
|
46182
|
+
workspaceSlug: args.workspaceSlug,
|
|
46183
|
+
kind: "document",
|
|
46184
|
+
logicalKey: t.logicalKey,
|
|
46185
|
+
generation: t.purgeGeneration,
|
|
46186
|
+
stashPath,
|
|
46187
|
+
stashHash
|
|
46188
|
+
});
|
|
46189
|
+
args.output?.warn(`${args.strategy.type}/${t.logicalKey}: permanently deleted; recoverable local content was moved to ${stashPath}.`);
|
|
46190
|
+
}
|
|
46191
|
+
}
|
|
46192
|
+
args.suppressor?.registerDelete(target);
|
|
46193
|
+
deleted = deleteMirrorFile(args.workspaceSlug, args.strategy.type, retiredEntry?.slug ?? t.logicalKey);
|
|
46194
|
+
}
|
|
46195
|
+
const staleEntry = args.liveOwnerId === null ? stateOwner : retiredEntry;
|
|
46196
|
+
if (staleEntry)
|
|
46197
|
+
state = removeEntry(state, args.strategy.type, staleEntry.id).state;
|
|
46198
|
+
completeRetirement({
|
|
46199
|
+
workspaceSlug: args.workspaceSlug,
|
|
46200
|
+
kind: "document",
|
|
46201
|
+
logicalKey: t.logicalKey,
|
|
46202
|
+
generation: t.purgeGeneration,
|
|
46203
|
+
...stashPath && stashHash ? { stashPath, stashHash } : {},
|
|
46204
|
+
now
|
|
46205
|
+
});
|
|
46206
|
+
return { state, deleted, ...stashPath ? { stashPath } : {} };
|
|
46207
|
+
}
|
|
46208
|
+
function disposePermanentFile(args) {
|
|
46209
|
+
return withPermanentDeletionLock("workspace_file", args.tombstone.logicalKey, async () => disposePermanentFileLocked(args));
|
|
46210
|
+
}
|
|
46211
|
+
function disposePermanentFileLocked(args) {
|
|
46212
|
+
const now = args.now ?? (() => new Date);
|
|
46213
|
+
const t = args.tombstone;
|
|
46214
|
+
recordRetirementPending({
|
|
46215
|
+
workspaceSlug: args.workspaceSlug,
|
|
46216
|
+
kind: "workspace_file",
|
|
46217
|
+
logicalKey: t.logicalKey,
|
|
46218
|
+
rowId: t.id,
|
|
46219
|
+
purgeGeneration: t.purgeGeneration,
|
|
46220
|
+
retirementSequence: t.retirementSequence,
|
|
46221
|
+
retiredAt: t.deletedAt
|
|
46222
|
+
});
|
|
46223
|
+
const journal = loadPermanentDeletionJournal(args.workspaceSlug);
|
|
46224
|
+
const record2 = findRetirement(journal, "workspace_file", t.logicalKey);
|
|
46225
|
+
if (record2.purge_generation !== t.purgeGeneration) {
|
|
46226
|
+
return { state: args.state, deleted: false, textDeleted: false };
|
|
46227
|
+
}
|
|
46228
|
+
if (args.liveOwnerId === t.id) {
|
|
46229
|
+
throw new Error("permanent file namespace reports the retired row as live owner");
|
|
46230
|
+
}
|
|
46231
|
+
if (record2.disposition === "complete") {
|
|
46232
|
+
const stale = getFileEntry(args.state, t.logicalKey);
|
|
46233
|
+
const state2 = stale && (args.liveOwnerId === null || stale.id === t.id) ? removeFileEntry(args.state, t.logicalKey).state : args.state;
|
|
46234
|
+
return {
|
|
46235
|
+
state: state2,
|
|
46236
|
+
deleted: false,
|
|
46237
|
+
textDeleted: false,
|
|
46238
|
+
...record2.stash_path ? { stashPath: record2.stash_path } : {}
|
|
46239
|
+
};
|
|
46240
|
+
}
|
|
46241
|
+
const existing = getFileEntry(args.state, t.logicalKey);
|
|
46242
|
+
const protectedOwner = args.liveOwnerId !== null && args.liveOwnerId !== t.id;
|
|
46243
|
+
let state = args.state;
|
|
46244
|
+
let deleted = false;
|
|
46245
|
+
let textDeleted = false;
|
|
46246
|
+
let stashPath = record2.stash_path;
|
|
46247
|
+
let stashHash = record2.stash_hash;
|
|
46248
|
+
if (!protectedOwner) {
|
|
46249
|
+
const target = filesSubtreePath(args.workspaceSlug, t.logicalKey);
|
|
46250
|
+
let bytes;
|
|
46251
|
+
try {
|
|
46252
|
+
bytes = readFileSync26(target);
|
|
46253
|
+
} catch (error52) {
|
|
46254
|
+
if (error52?.code !== "ENOENT")
|
|
46255
|
+
throw error52;
|
|
46256
|
+
}
|
|
46257
|
+
if (bytes !== undefined && !stashPath) {
|
|
46258
|
+
const matchesAcknowledged = existing !== undefined && sha256Bytes(bytes) === existing.content_hash;
|
|
46259
|
+
if (!matchesAcknowledged) {
|
|
46260
|
+
const stash = stashFileConflictOnce({
|
|
46261
|
+
workspaceSlug: args.workspaceSlug,
|
|
46262
|
+
relPath: t.logicalKey,
|
|
46263
|
+
timestamp: now(),
|
|
46264
|
+
bytes,
|
|
46265
|
+
side: "local"
|
|
46266
|
+
});
|
|
46267
|
+
stashPath = stash.path;
|
|
46268
|
+
stashHash = sha256(bytes);
|
|
46269
|
+
verifyStash(stash.path, stashHash);
|
|
46270
|
+
recordRetirementStash({
|
|
46271
|
+
workspaceSlug: args.workspaceSlug,
|
|
46272
|
+
kind: "workspace_file",
|
|
46273
|
+
logicalKey: t.logicalKey,
|
|
46274
|
+
generation: t.purgeGeneration,
|
|
46275
|
+
stashPath,
|
|
46276
|
+
stashHash
|
|
46277
|
+
});
|
|
46278
|
+
args.output?.warn(`files/${t.logicalKey}: permanently deleted; recoverable local bytes were moved to ${stashPath}.`);
|
|
46279
|
+
}
|
|
46280
|
+
}
|
|
46281
|
+
args.suppressor?.registerDelete(target);
|
|
46282
|
+
deleted = deleteFilesFile(args.workspaceSlug, t.logicalKey);
|
|
46283
|
+
textDeleted = deleteExtractedText(args.workspaceSlug, t.logicalKey);
|
|
46284
|
+
}
|
|
46285
|
+
if (existing && (args.liveOwnerId === null || existing.id === t.id)) {
|
|
46286
|
+
state = removeFileEntry(state, t.logicalKey).state;
|
|
46287
|
+
}
|
|
46288
|
+
completeRetirement({
|
|
46289
|
+
workspaceSlug: args.workspaceSlug,
|
|
46290
|
+
kind: "workspace_file",
|
|
46291
|
+
logicalKey: t.logicalKey,
|
|
46292
|
+
generation: t.purgeGeneration,
|
|
46293
|
+
...stashPath && stashHash ? { stashPath, stashHash } : {},
|
|
46294
|
+
now
|
|
46295
|
+
});
|
|
46296
|
+
return { state, deleted, textDeleted, ...stashPath ? { stashPath } : {} };
|
|
46297
|
+
}
|
|
46298
|
+
|
|
46299
|
+
// src/lib/files/pull.ts
|
|
46300
|
+
function dueFileRefreshMarkers(workspaceSlug, now) {
|
|
46301
|
+
try {
|
|
46302
|
+
return loadPermanentDeletionJournal(workspaceSlug).refresh.filter((marker) => marker.kind === "workspace_file" && (marker.retry_after === undefined || new Date(marker.retry_after).getTime() <= now.getTime()));
|
|
46303
|
+
} catch {
|
|
46304
|
+
return null;
|
|
46305
|
+
}
|
|
46306
|
+
}
|
|
46307
|
+
async function replayPermanentFiles(args, markers) {
|
|
46308
|
+
const now = args.now ?? (() => new Date);
|
|
46309
|
+
const rows = [];
|
|
46310
|
+
let cursor = null;
|
|
46311
|
+
const savedCursor = args.baseCursor === undefined ? args.cursor : args.baseCursor;
|
|
46312
|
+
let resultingCursor = savedCursor;
|
|
46313
|
+
while (true) {
|
|
46314
|
+
const page = await syncFilesPage({
|
|
46315
|
+
baseUrl: args.baseUrl,
|
|
46316
|
+
pat: args.pat,
|
|
46317
|
+
cursor,
|
|
46318
|
+
fetchImpl: args.fetchImpl
|
|
46319
|
+
});
|
|
46320
|
+
rows.push(...page.rows);
|
|
46321
|
+
if (page.next_cursor === null)
|
|
46322
|
+
break;
|
|
46323
|
+
resultingCursor = page.next_cursor;
|
|
46324
|
+
cursor = page.next_cursor;
|
|
46325
|
+
}
|
|
46326
|
+
let state = args.state;
|
|
46327
|
+
let written = 0;
|
|
46328
|
+
let deleted = 0;
|
|
46329
|
+
let textWritten = 0;
|
|
46330
|
+
let textDeleted = 0;
|
|
46331
|
+
let textUnknown = 0;
|
|
46332
|
+
let pointerRows = 0;
|
|
46333
|
+
let symlinkRefused = 0;
|
|
46334
|
+
let stashFailed = 0;
|
|
46335
|
+
let restoresDeferred = 0;
|
|
46336
|
+
let completePass = true;
|
|
46337
|
+
let lastCommittedState = args.state;
|
|
46338
|
+
const completed = [];
|
|
46339
|
+
for (const marker of markers) {
|
|
46340
|
+
const candidates = rows.filter((row) => row.path === marker.logical_key && row.deleted_at !== null && row.permanent_delete === true && typeof row.purge_generation === "string" && typeof row.retirement_sequence === "number");
|
|
46341
|
+
candidates.sort((a, b) => b.retirement_sequence - a.retirement_sequence);
|
|
46342
|
+
const candidate = candidates[0];
|
|
46343
|
+
const ambiguousLatest = candidate !== undefined && candidates.some((row) => row.retirement_sequence === candidate.retirement_sequence && row.purge_generation !== candidate.purge_generation);
|
|
46344
|
+
const stateBefore = state;
|
|
46345
|
+
const outcome = await withPermanentDeletionLock("workspace_file", marker.logical_key, async () => {
|
|
46346
|
+
const namespace = await getCurrentPermanentNamespace({
|
|
46347
|
+
baseUrl: args.baseUrl,
|
|
46348
|
+
pat: args.pat,
|
|
46349
|
+
entityKind: "workspace_file",
|
|
46350
|
+
logicalKey: marker.logical_key,
|
|
46351
|
+
fetchImpl: args.fetchImpl
|
|
46352
|
+
});
|
|
46353
|
+
if (!candidate || ambiguousLatest || namespace.latest_retirement === null || namespace.latest_retirement.purge_generation !== candidate.purge_generation || namespace.latest_retirement.retirement_sequence !== candidate.retirement_sequence) {
|
|
46354
|
+
markRetentionMissing({
|
|
46355
|
+
workspaceSlug: args.workspaceSlug,
|
|
46356
|
+
kind: "workspace_file",
|
|
46357
|
+
logicalKey: marker.logical_key,
|
|
46358
|
+
now
|
|
46359
|
+
});
|
|
46360
|
+
args.output?.warn(`files/${marker.logical_key}: permanent-delete replay could not prove one unambiguous current retained tombstone; creates remain fenced and the replay will retry.`);
|
|
46361
|
+
return { state: stateBefore, deleted: false, textDeleted: false, complete: false };
|
|
46362
|
+
}
|
|
46363
|
+
const liveOwnerId = namespace.live_owner?.id ?? null;
|
|
46364
|
+
const disposition = await disposePermanentFile({
|
|
46365
|
+
workspaceSlug: args.workspaceSlug,
|
|
46366
|
+
tombstone: {
|
|
46367
|
+
id: candidate.id,
|
|
46368
|
+
logicalKey: marker.logical_key,
|
|
46369
|
+
deletedAt: candidate.deleted_at,
|
|
46370
|
+
purgeGeneration: candidate.purge_generation,
|
|
46371
|
+
retirementSequence: candidate.retirement_sequence
|
|
46372
|
+
},
|
|
46373
|
+
liveOwnerId,
|
|
46374
|
+
state: stateBefore,
|
|
46375
|
+
now,
|
|
46376
|
+
output: args.output,
|
|
46377
|
+
suppressor: args.suppressor
|
|
46378
|
+
});
|
|
46379
|
+
const committedState = args.commit && disposition.state !== stateBefore ? args.commit(stateBefore, disposition.state) : disposition.state;
|
|
46380
|
+
const current = await getCurrentPermanentNamespace({
|
|
46381
|
+
baseUrl: args.baseUrl,
|
|
46382
|
+
pat: args.pat,
|
|
46383
|
+
entityKind: "workspace_file",
|
|
46384
|
+
logicalKey: marker.logical_key,
|
|
46385
|
+
fetchImpl: args.fetchImpl
|
|
46386
|
+
});
|
|
46387
|
+
const complete = current.latest_retirement?.purge_generation === candidate.purge_generation && current.latest_retirement?.retirement_sequence === candidate.retirement_sequence && (current.live_owner?.id ?? null) === liveOwnerId;
|
|
46388
|
+
return {
|
|
46389
|
+
state: committedState,
|
|
46390
|
+
deleted: disposition.deleted,
|
|
46391
|
+
textDeleted: disposition.textDeleted,
|
|
46392
|
+
complete
|
|
46393
|
+
};
|
|
46394
|
+
});
|
|
46395
|
+
state = outcome.state;
|
|
46396
|
+
if (args.commit && state !== stateBefore)
|
|
46397
|
+
lastCommittedState = state;
|
|
46398
|
+
if (outcome.deleted)
|
|
46399
|
+
deleted++;
|
|
46400
|
+
if (outcome.textDeleted)
|
|
46401
|
+
textDeleted++;
|
|
46402
|
+
if (outcome.complete && candidate) {
|
|
46403
|
+
completed.push({ logicalKey: marker.logical_key, generation: candidate.purge_generation });
|
|
46404
|
+
} else {
|
|
46405
|
+
completePass = false;
|
|
46406
|
+
}
|
|
46407
|
+
}
|
|
46408
|
+
for (const row of rows) {
|
|
46409
|
+
if (row.deleted_at !== null && row.permanent_delete === true)
|
|
46410
|
+
continue;
|
|
46411
|
+
const outcome = await applyFileRow({
|
|
46412
|
+
workspaceSlug: args.workspaceSlug,
|
|
46413
|
+
baseUrl: args.baseUrl,
|
|
46414
|
+
pat: args.pat,
|
|
46415
|
+
row,
|
|
46416
|
+
state,
|
|
46417
|
+
fetchImpl: args.fetchImpl,
|
|
46418
|
+
now,
|
|
46419
|
+
output: args.output,
|
|
46420
|
+
suppressor: args.suppressor,
|
|
46421
|
+
isFileFrozen: args.isFileFrozen,
|
|
46422
|
+
loggedTombstoneSkips: args.loggedTombstoneSkips,
|
|
46423
|
+
forceTextRewalk: args.forceTextRewalk,
|
|
46424
|
+
loggedStashNotices: args.loggedStashNotices,
|
|
46425
|
+
recentUnlinks: args.recentUnlinks
|
|
46426
|
+
});
|
|
46427
|
+
state = outcome.state;
|
|
46428
|
+
if (outcome.action === "written")
|
|
46429
|
+
written++;
|
|
46430
|
+
else if (outcome.action === "deleted")
|
|
46431
|
+
deleted++;
|
|
46432
|
+
textWritten += outcome.textWritten ?? 0;
|
|
46433
|
+
textDeleted += outcome.textDeleted ?? 0;
|
|
46434
|
+
if (outcome.textUnknown)
|
|
46435
|
+
textUnknown++;
|
|
46436
|
+
if (outcome.pointer)
|
|
46437
|
+
pointerRows++;
|
|
46438
|
+
if (outcome.symlinkRefused) {
|
|
46439
|
+
symlinkRefused++;
|
|
46440
|
+
completePass = false;
|
|
46441
|
+
}
|
|
46442
|
+
if (outcome.stashFailed || outcome.textFailed || outcome.integrityFailed) {
|
|
46443
|
+
if (outcome.stashFailed)
|
|
46444
|
+
stashFailed++;
|
|
46445
|
+
completePass = false;
|
|
46446
|
+
}
|
|
46447
|
+
if (outcome.restoreDeferred) {
|
|
46448
|
+
restoresDeferred++;
|
|
46449
|
+
completePass = false;
|
|
46450
|
+
}
|
|
46451
|
+
}
|
|
46452
|
+
if (args.commit && state !== lastCommittedState)
|
|
46453
|
+
state = args.commit(lastCommittedState, state);
|
|
46454
|
+
return {
|
|
46455
|
+
state,
|
|
46456
|
+
fetched: rows.length,
|
|
46457
|
+
written,
|
|
46458
|
+
deleted,
|
|
46459
|
+
textWritten,
|
|
46460
|
+
textDeleted,
|
|
46461
|
+
textUnknown,
|
|
46462
|
+
pointerRows,
|
|
46463
|
+
symlinkRefused,
|
|
46464
|
+
stashFailed,
|
|
46465
|
+
lastCursor: completePass ? resultingCursor : savedCursor,
|
|
46466
|
+
restoresDeferred,
|
|
46467
|
+
permanentRefreshes: completed,
|
|
46468
|
+
permanentReplayComplete: completePass
|
|
46469
|
+
};
|
|
46470
|
+
}
|
|
44771
46471
|
async function pullFiles(args) {
|
|
46472
|
+
const dueMarkers = dueFileRefreshMarkers(args.workspaceSlug, (args.now ?? (() => new Date))());
|
|
46473
|
+
if (dueMarkers && dueMarkers.length > 0)
|
|
46474
|
+
return replayPermanentFiles(args, dueMarkers);
|
|
46475
|
+
const permanentJournalBlocked = dueMarkers === null;
|
|
46476
|
+
if (permanentJournalBlocked) {
|
|
46477
|
+
args.output?.warn(`files: permanent deletion journal is invalid or unreadable; ordinary rows may apply, but the files cursor and permanent dispositions are held. Run \`plinth status\` for the journal path.`);
|
|
46478
|
+
}
|
|
44772
46479
|
const { workspaceSlug, baseUrl, pat, fetchImpl } = args;
|
|
44773
46480
|
let state = args.state;
|
|
44774
46481
|
let fetched = 0;
|
|
@@ -44780,15 +46487,47 @@ async function pullFiles(args) {
|
|
|
44780
46487
|
let pointerRows = 0;
|
|
44781
46488
|
let symlinkRefused = 0;
|
|
44782
46489
|
let stashFailed = 0;
|
|
46490
|
+
let restoresDeferred = 0;
|
|
46491
|
+
let permanentEncountered = false;
|
|
46492
|
+
const baseCursor = args.baseCursor === undefined ? args.cursor : args.baseCursor;
|
|
44783
46493
|
let cursor = args.cursor;
|
|
44784
46494
|
let lastCursor = args.cursor;
|
|
44785
|
-
let
|
|
46495
|
+
let finalHighWater = null;
|
|
46496
|
+
let markRefused = false;
|
|
46497
|
+
let cursorBlocked = permanentJournalBlocked;
|
|
44786
46498
|
while (true) {
|
|
44787
|
-
|
|
46499
|
+
let page;
|
|
46500
|
+
try {
|
|
46501
|
+
page = await syncFilesPage({ baseUrl, pat, cursor, fetchImpl });
|
|
46502
|
+
} catch (err) {
|
|
46503
|
+
if (!markRefused && cursor === args.cursor && args.cursor !== baseCursor && err instanceof ApiError && err.status === 400) {
|
|
46504
|
+
markRefused = true;
|
|
46505
|
+
cursor = baseCursor;
|
|
46506
|
+
lastCursor = baseCursor;
|
|
46507
|
+
continue;
|
|
46508
|
+
}
|
|
46509
|
+
throw err;
|
|
46510
|
+
}
|
|
44788
46511
|
const pageBase = state;
|
|
44789
46512
|
let pageTextFailed = false;
|
|
44790
46513
|
for (const row of page.rows) {
|
|
44791
46514
|
fetched++;
|
|
46515
|
+
if (row.deleted_at !== null && row.permanent_delete === true && row.purge_generation !== undefined) {
|
|
46516
|
+
if (permanentJournalBlocked) {
|
|
46517
|
+
pageTextFailed = true;
|
|
46518
|
+
continue;
|
|
46519
|
+
}
|
|
46520
|
+
requireFeedRefresh({
|
|
46521
|
+
workspaceSlug,
|
|
46522
|
+
kind: "workspace_file",
|
|
46523
|
+
logicalKey: row.path,
|
|
46524
|
+
requiredGeneration: row.purge_generation,
|
|
46525
|
+
now: args.now
|
|
46526
|
+
});
|
|
46527
|
+
permanentEncountered = true;
|
|
46528
|
+
pageTextFailed = true;
|
|
46529
|
+
continue;
|
|
46530
|
+
}
|
|
44792
46531
|
const outcome = await applyFileRow({
|
|
44793
46532
|
workspaceSlug,
|
|
44794
46533
|
baseUrl,
|
|
@@ -44802,7 +46541,8 @@ async function pullFiles(args) {
|
|
|
44802
46541
|
isFileFrozen: args.isFileFrozen,
|
|
44803
46542
|
loggedTombstoneSkips: args.loggedTombstoneSkips,
|
|
44804
46543
|
forceTextRewalk: args.forceTextRewalk,
|
|
44805
|
-
loggedStashNotices: args.loggedStashNotices
|
|
46544
|
+
loggedStashNotices: args.loggedStashNotices,
|
|
46545
|
+
recentUnlinks: args.recentUnlinks
|
|
44806
46546
|
});
|
|
44807
46547
|
state = outcome.state;
|
|
44808
46548
|
if (!outcome.symlinkRefused) {
|
|
@@ -44828,17 +46568,34 @@ async function pullFiles(args) {
|
|
|
44828
46568
|
stashFailed++;
|
|
44829
46569
|
pageTextFailed = true;
|
|
44830
46570
|
}
|
|
46571
|
+
if (outcome.integrityFailed)
|
|
46572
|
+
pageTextFailed = true;
|
|
46573
|
+
if (outcome.restoreDeferred) {
|
|
46574
|
+
restoresDeferred++;
|
|
46575
|
+
pageTextFailed = true;
|
|
46576
|
+
}
|
|
44831
46577
|
}
|
|
44832
46578
|
if (args.commit && state !== pageBase)
|
|
44833
46579
|
state = args.commit(pageBase, state);
|
|
44834
46580
|
if (pageTextFailed)
|
|
44835
46581
|
cursorBlocked = true;
|
|
44836
|
-
if (page.next_cursor === null)
|
|
46582
|
+
if (page.next_cursor === null) {
|
|
46583
|
+
finalHighWater = page.high_water ?? null;
|
|
44837
46584
|
break;
|
|
46585
|
+
}
|
|
44838
46586
|
if (!cursorBlocked)
|
|
44839
46587
|
lastCursor = page.next_cursor;
|
|
44840
46588
|
cursor = page.next_cursor;
|
|
44841
46589
|
}
|
|
46590
|
+
if (permanentEncountered) {
|
|
46591
|
+
const markers = dueFileRefreshMarkers(workspaceSlug, (args.now ?? (() => new Date))());
|
|
46592
|
+
if (!markers) {
|
|
46593
|
+
throw new Error("permanent deletion journal became unreadable during file replay");
|
|
46594
|
+
}
|
|
46595
|
+
if (markers.length > 0) {
|
|
46596
|
+
return replayPermanentFiles({ ...args, state }, markers);
|
|
46597
|
+
}
|
|
46598
|
+
}
|
|
44842
46599
|
if (textUnknown > 0) {
|
|
44843
46600
|
args.output?.warn(`${textUnknown} file ${textUnknown === 1 ? "row" : "rows"} arrived without a readable \`has_extracted_text\` — this server predates the extracted-text contract, or the field was dropped from the payload. Derived text under views/extracted/ was left untouched rather than deleted, and is not being updated either.`);
|
|
44844
46601
|
}
|
|
@@ -44862,7 +46619,14 @@ async function pullFiles(args) {
|
|
|
44862
46619
|
pointerRows,
|
|
44863
46620
|
symlinkRefused,
|
|
44864
46621
|
stashFailed,
|
|
44865
|
-
lastCursor
|
|
46622
|
+
lastCursor,
|
|
46623
|
+
highWater: provenMark({
|
|
46624
|
+
clean: !cursorBlocked && textUnknown === 0,
|
|
46625
|
+
finalHighWater,
|
|
46626
|
+
advancedTo: lastCursor !== (markRefused ? baseCursor : args.cursor) ? lastCursor : null
|
|
46627
|
+
}),
|
|
46628
|
+
markRefused,
|
|
46629
|
+
restoresDeferred
|
|
44866
46630
|
};
|
|
44867
46631
|
}
|
|
44868
46632
|
async function applyExtractedText(args) {
|
|
@@ -44977,7 +46741,7 @@ async function applyFileRow(args) {
|
|
|
44977
46741
|
}
|
|
44978
46742
|
let localBytes;
|
|
44979
46743
|
try {
|
|
44980
|
-
localBytes =
|
|
46744
|
+
localBytes = readFileSync27(abs);
|
|
44981
46745
|
} catch {
|
|
44982
46746
|
localBytes = undefined;
|
|
44983
46747
|
}
|
|
@@ -45023,7 +46787,18 @@ async function applyFileRow(args) {
|
|
|
45023
46787
|
output
|
|
45024
46788
|
});
|
|
45025
46789
|
}
|
|
45026
|
-
if (existing && existing.content_hash === row.content_hash &&
|
|
46790
|
+
if (existing && existing.content_hash === row.content_hash && !existsSync8(abs)) {
|
|
46791
|
+
const verdict = deferRestore(args.recentUnlinks, abs, row.updated_at, now().getTime());
|
|
46792
|
+
if (verdict.defer) {
|
|
46793
|
+
if (verdict.first) {
|
|
46794
|
+
output?.log(fileRestoreDeferredLine({ path: relPath, waitSeconds: RESTORE_DEFERRAL_MS / 1000 }));
|
|
46795
|
+
}
|
|
46796
|
+
return { state, action: "noop", textUnknown, restoreDeferred: true };
|
|
46797
|
+
}
|
|
46798
|
+
} else {
|
|
46799
|
+
presentAgain(args.recentUnlinks, abs);
|
|
46800
|
+
}
|
|
46801
|
+
if (existing && existing.content_hash === row.content_hash && existsSync8(abs)) {
|
|
45027
46802
|
if (!args.forceTextRewalk && textIsCurrent(existing, row, workspaceSlug, relPath)) {
|
|
45028
46803
|
return { state, action: "noop", textUnknown };
|
|
45029
46804
|
}
|
|
@@ -45075,7 +46850,7 @@ async function applyFileRow(args) {
|
|
|
45075
46850
|
const bytes = await downloadBytes(got.row.download_url, fetchImpl);
|
|
45076
46851
|
if (sha256Bytes(bytes) !== row.content_hash) {
|
|
45077
46852
|
output?.warn(`Integrity check failed for files/${relPath} (hash mismatch) — skipped this pull.`);
|
|
45078
|
-
return { state, action: "skipped" };
|
|
46853
|
+
return { state, action: "skipped", integrityFailed: true };
|
|
45079
46854
|
}
|
|
45080
46855
|
const stashFailure = stashUnpushedLocalEdit({
|
|
45081
46856
|
workspaceSlug,
|
|
@@ -45153,7 +46928,7 @@ async function applyPointerRow(args) {
|
|
|
45153
46928
|
if (textSignal === "absent") {
|
|
45154
46929
|
args.output?.warn(`files/${relPath}: Plinth holds neither bytes nor text for this file — it has no local representation in the mirror.`);
|
|
45155
46930
|
}
|
|
45156
|
-
const bytesInMirror =
|
|
46931
|
+
const bytesInMirror = existsSync8(filesSubtreePath(workspaceSlug, relPath));
|
|
45157
46932
|
if (bytesInMirror) {
|
|
45158
46933
|
args.output?.warn(`files/${relPath}: Plinth holds no bytes for this file, but a local copy is present at that path — left exactly as it is. Plinth will not update it.`);
|
|
45159
46934
|
}
|
|
@@ -45179,11 +46954,11 @@ async function applyPointerRow(args) {
|
|
|
45179
46954
|
}
|
|
45180
46955
|
function stashUnpushedLocalEdit(args) {
|
|
45181
46956
|
const { workspaceSlug, relPath, abs, existing, output } = args;
|
|
45182
|
-
if (hasSymlinkedComponent(workspaceSlug,
|
|
46957
|
+
if (hasSymlinkedComponent(workspaceSlug, dirname13(abs)))
|
|
45183
46958
|
return { reason: "symlink" };
|
|
45184
46959
|
let st;
|
|
45185
46960
|
try {
|
|
45186
|
-
st =
|
|
46961
|
+
st = lstatSync5(abs);
|
|
45187
46962
|
} catch (err) {
|
|
45188
46963
|
if (err?.code === "ENOENT")
|
|
45189
46964
|
return null;
|
|
@@ -45193,7 +46968,7 @@ function stashUnpushedLocalEdit(args) {
|
|
|
45193
46968
|
return { reason: "not_a_file" };
|
|
45194
46969
|
let localBytes;
|
|
45195
46970
|
try {
|
|
45196
|
-
localBytes =
|
|
46971
|
+
localBytes = readFileSync27(abs);
|
|
45197
46972
|
} catch (err) {
|
|
45198
46973
|
return { reason: "unreadable", code: errCode(err) };
|
|
45199
46974
|
}
|
|
@@ -45230,10 +47005,12 @@ function textIsCurrent(existing, row, workspaceSlug, relPath) {
|
|
|
45230
47005
|
if (signal === "unknown")
|
|
45231
47006
|
return true;
|
|
45232
47007
|
if (signal === "absent") {
|
|
45233
|
-
return existing.text_hash === undefined && !
|
|
47008
|
+
return existing.text_hash === undefined && !existsSync8(extractedTextPath(workspaceSlug, relPath));
|
|
45234
47009
|
}
|
|
45235
47010
|
if (existing.text_hash === undefined)
|
|
45236
47011
|
return false;
|
|
47012
|
+
if (existing.updated_at !== row.updated_at)
|
|
47013
|
+
return false;
|
|
45237
47014
|
const onDisk = readExtractedText(workspaceSlug, relPath);
|
|
45238
47015
|
if (onDisk === null)
|
|
45239
47016
|
return false;
|
|
@@ -45241,7 +47018,7 @@ function textIsCurrent(existing, row, workspaceSlug, relPath) {
|
|
|
45241
47018
|
}
|
|
45242
47019
|
|
|
45243
47020
|
// src/lib/files/rejection-sync.ts
|
|
45244
|
-
import { readFileSync as
|
|
47021
|
+
import { readFileSync as readFileSync28, statSync as statSync16 } from "node:fs";
|
|
45245
47022
|
|
|
45246
47023
|
// src/lib/files/rejections-api.ts
|
|
45247
47024
|
var RejectionsFeedSchema = exports_external.object({
|
|
@@ -45374,7 +47151,7 @@ function settledDisposition(args) {
|
|
|
45374
47151
|
return null;
|
|
45375
47152
|
let bytes;
|
|
45376
47153
|
try {
|
|
45377
|
-
bytes =
|
|
47154
|
+
bytes = readFileSync28(absolute2);
|
|
45378
47155
|
} catch {
|
|
45379
47156
|
return null;
|
|
45380
47157
|
}
|
|
@@ -45451,7 +47228,7 @@ function classifyApiError(err) {
|
|
|
45451
47228
|
return { kind: "transient", status: err.status };
|
|
45452
47229
|
}
|
|
45453
47230
|
function hasExistingContent(workspacePath) {
|
|
45454
|
-
if (!
|
|
47231
|
+
if (!existsSync9(workspacePath))
|
|
45455
47232
|
return false;
|
|
45456
47233
|
try {
|
|
45457
47234
|
return readdirSync7(workspacePath, { withFileTypes: true }).some((e) => {
|
|
@@ -45460,7 +47237,7 @@ function hasExistingContent(workspacePath) {
|
|
|
45460
47237
|
if (!e.isDirectory())
|
|
45461
47238
|
return true;
|
|
45462
47239
|
try {
|
|
45463
|
-
return readdirSync7(
|
|
47240
|
+
return readdirSync7(join18(workspacePath, e.name)).length > 0;
|
|
45464
47241
|
} catch {
|
|
45465
47242
|
return true;
|
|
45466
47243
|
}
|
|
@@ -45490,7 +47267,7 @@ function unlinkIfProvablyOurs(workspaceSlug, strategy, entry2, suppressor) {
|
|
|
45490
47267
|
}
|
|
45491
47268
|
let raw;
|
|
45492
47269
|
try {
|
|
45493
|
-
raw =
|
|
47270
|
+
raw = readFileSync29(target, "utf8");
|
|
45494
47271
|
} catch (err) {
|
|
45495
47272
|
if (err?.code === "ENOENT") {
|
|
45496
47273
|
return { didUnlink: false };
|
|
@@ -45508,7 +47285,7 @@ function unlinkIfProvablyOurs(workspaceSlug, strategy, entry2, suppressor) {
|
|
|
45508
47285
|
}
|
|
45509
47286
|
try {
|
|
45510
47287
|
suppressor?.registerDelete(target);
|
|
45511
|
-
|
|
47288
|
+
unlinkSync9(target);
|
|
45512
47289
|
return { didUnlink: true };
|
|
45513
47290
|
} catch (err) {
|
|
45514
47291
|
if (err?.code === "ENOENT") {
|
|
@@ -45517,6 +47294,122 @@ function unlinkIfProvablyOurs(workspaceSlug, strategy, entry2, suppressor) {
|
|
|
45517
47294
|
throw err;
|
|
45518
47295
|
}
|
|
45519
47296
|
}
|
|
47297
|
+
function dueDocumentRefreshMarkers(workspaceSlug, now) {
|
|
47298
|
+
try {
|
|
47299
|
+
return loadPermanentDeletionJournal(workspaceSlug).refresh.filter((marker) => marker.kind === "document" && (marker.retry_after === undefined || new Date(marker.retry_after).getTime() <= now.getTime()));
|
|
47300
|
+
} catch {
|
|
47301
|
+
return null;
|
|
47302
|
+
}
|
|
47303
|
+
}
|
|
47304
|
+
async function replayPermanentDocuments(args) {
|
|
47305
|
+
const rows = [];
|
|
47306
|
+
let cursor = null;
|
|
47307
|
+
let resultingCursor = args.savedCursor;
|
|
47308
|
+
while (true) {
|
|
47309
|
+
const page = await syncEntity({
|
|
47310
|
+
strategy: args.strategy,
|
|
47311
|
+
baseUrl: args.baseUrl,
|
|
47312
|
+
pat: args.pat,
|
|
47313
|
+
cursor,
|
|
47314
|
+
fetchImpl: args.fetchImpl
|
|
47315
|
+
});
|
|
47316
|
+
rows.push(...page.rows);
|
|
47317
|
+
if (page.next_cursor === null)
|
|
47318
|
+
break;
|
|
47319
|
+
resultingCursor = page.next_cursor;
|
|
47320
|
+
cursor = page.next_cursor;
|
|
47321
|
+
}
|
|
47322
|
+
let state = args.state;
|
|
47323
|
+
const completed = [];
|
|
47324
|
+
let completePass = true;
|
|
47325
|
+
let restoresDeferred = 0;
|
|
47326
|
+
for (const marker of args.markers) {
|
|
47327
|
+
const candidates = rows.filter((row) => row.deleted_at !== null && row.slug === marker.logical_key && row.permanent_delete === true && typeof row.purge_generation === "string" && typeof row.retirement_sequence === "number");
|
|
47328
|
+
candidates.sort((a, b) => b.retirement_sequence - a.retirement_sequence);
|
|
47329
|
+
const candidate = candidates[0];
|
|
47330
|
+
const ambiguousLatest = candidate !== undefined && candidates.some((row) => row.retirement_sequence === candidate.retirement_sequence && row.purge_generation !== candidate.purge_generation);
|
|
47331
|
+
const stateBefore = state;
|
|
47332
|
+
const outcome = await withPermanentDeletionLock("document", marker.logical_key, async () => {
|
|
47333
|
+
const namespace = await getCurrentPermanentNamespace({
|
|
47334
|
+
baseUrl: args.baseUrl,
|
|
47335
|
+
pat: args.pat,
|
|
47336
|
+
entityKind: "document",
|
|
47337
|
+
logicalKey: marker.logical_key,
|
|
47338
|
+
fetchImpl: args.fetchImpl
|
|
47339
|
+
});
|
|
47340
|
+
if (!candidate || ambiguousLatest || namespace.latest_retirement === null || namespace.latest_retirement.purge_generation !== candidate.purge_generation || namespace.latest_retirement.retirement_sequence !== candidate.retirement_sequence) {
|
|
47341
|
+
markRetentionMissing({
|
|
47342
|
+
workspaceSlug: args.workspaceSlug,
|
|
47343
|
+
kind: "document",
|
|
47344
|
+
logicalKey: marker.logical_key,
|
|
47345
|
+
now: args.now
|
|
47346
|
+
});
|
|
47347
|
+
args.output.warn(`documents/${marker.logical_key}: permanent-delete replay could not prove one unambiguous current retained tombstone; creates remain fenced and the replay will retry.`);
|
|
47348
|
+
return { state: stateBefore, complete: false };
|
|
47349
|
+
}
|
|
47350
|
+
const liveOwnerId = namespace.live_owner?.id ?? null;
|
|
47351
|
+
const disposed = await disposePermanentDocument({
|
|
47352
|
+
workspaceSlug: args.workspaceSlug,
|
|
47353
|
+
strategy: args.strategy,
|
|
47354
|
+
tombstone: {
|
|
47355
|
+
id: candidate.id,
|
|
47356
|
+
logicalKey: marker.logical_key,
|
|
47357
|
+
deletedAt: candidate.deleted_at,
|
|
47358
|
+
purgeGeneration: candidate.purge_generation,
|
|
47359
|
+
retirementSequence: candidate.retirement_sequence
|
|
47360
|
+
},
|
|
47361
|
+
liveOwnerId,
|
|
47362
|
+
state: stateBefore,
|
|
47363
|
+
now: args.now,
|
|
47364
|
+
output: args.output,
|
|
47365
|
+
suppressor: args.suppressor
|
|
47366
|
+
});
|
|
47367
|
+
const committedState = args.commit && disposed.state !== stateBefore ? args.commit(stateBefore, disposed.state) : disposed.state;
|
|
47368
|
+
const current = await getCurrentPermanentNamespace({
|
|
47369
|
+
baseUrl: args.baseUrl,
|
|
47370
|
+
pat: args.pat,
|
|
47371
|
+
entityKind: "document",
|
|
47372
|
+
logicalKey: marker.logical_key,
|
|
47373
|
+
fetchImpl: args.fetchImpl
|
|
47374
|
+
});
|
|
47375
|
+
const complete = current.latest_retirement?.purge_generation === candidate.purge_generation && current.latest_retirement?.retirement_sequence === candidate.retirement_sequence && (current.live_owner?.id ?? null) === liveOwnerId;
|
|
47376
|
+
return { state: committedState, complete };
|
|
47377
|
+
});
|
|
47378
|
+
state = outcome.state;
|
|
47379
|
+
if (outcome.complete && candidate) {
|
|
47380
|
+
completed.push({ logicalKey: marker.logical_key, generation: candidate.purge_generation });
|
|
47381
|
+
} else {
|
|
47382
|
+
completePass = false;
|
|
47383
|
+
}
|
|
47384
|
+
}
|
|
47385
|
+
const counters = {
|
|
47386
|
+
fetched: rows.length,
|
|
47387
|
+
written: 0,
|
|
47388
|
+
deleted: 0,
|
|
47389
|
+
moved: 0,
|
|
47390
|
+
refused: 0
|
|
47391
|
+
};
|
|
47392
|
+
for (const row of rows) {
|
|
47393
|
+
if (row.deleted_at !== null && row.permanent_delete === true)
|
|
47394
|
+
continue;
|
|
47395
|
+
const outcome = applyRow(args.workspaceSlug, args.strategy, row, state, args.output, args.suppressor, args.now, args.isFrozen, args.loggedTombstoneSkips, args.recentUnlinks);
|
|
47396
|
+
state = outcome.state;
|
|
47397
|
+
if (outcome.action === "written")
|
|
47398
|
+
counters.written++;
|
|
47399
|
+
else if (outcome.action === "deleted")
|
|
47400
|
+
counters.deleted++;
|
|
47401
|
+
else if (outcome.action === "moved")
|
|
47402
|
+
counters.moved = (counters.moved ?? 0) + 1;
|
|
47403
|
+
else if (outcome.action === "slug_refused") {
|
|
47404
|
+
counters.refused = (counters.refused ?? 0) + 1;
|
|
47405
|
+
completePass = false;
|
|
47406
|
+
} else if (outcome.action === "restore_deferred") {
|
|
47407
|
+
restoresDeferred++;
|
|
47408
|
+
completePass = false;
|
|
47409
|
+
}
|
|
47410
|
+
}
|
|
47411
|
+
return { state, counters, cursor: resultingCursor, completed, completePass, restoresDeferred };
|
|
47412
|
+
}
|
|
45520
47413
|
async function runSync(workspaceSlug, deps = {}) {
|
|
45521
47414
|
const out = new Output({
|
|
45522
47415
|
sink: deps.sink,
|
|
@@ -45636,13 +47529,66 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
45636
47529
|
committed = state;
|
|
45637
47530
|
};
|
|
45638
47531
|
const counters = {};
|
|
47532
|
+
let restoresDeferred = 0;
|
|
45639
47533
|
try {
|
|
45640
47534
|
for (const entityType of allEntityTypes()) {
|
|
45641
47535
|
const strategy = requireStrategy(entityType);
|
|
45642
47536
|
const startCursor = entry2.sync_cursors?.[entityType] ?? null;
|
|
45643
|
-
|
|
45644
|
-
|
|
47537
|
+
if (entityType === "documents") {
|
|
47538
|
+
const markers = dueDocumentRefreshMarkers(slug, now());
|
|
47539
|
+
if (markers && markers.length > 0) {
|
|
47540
|
+
const replay = await replayPermanentDocuments({
|
|
47541
|
+
workspaceSlug: slug,
|
|
47542
|
+
baseUrl: entry2.api_base_url,
|
|
47543
|
+
pat,
|
|
47544
|
+
state,
|
|
47545
|
+
savedCursor: startCursor,
|
|
47546
|
+
strategy,
|
|
47547
|
+
markers,
|
|
47548
|
+
fetchImpl,
|
|
47549
|
+
output: out,
|
|
47550
|
+
suppressor: deps.suppressor,
|
|
47551
|
+
now,
|
|
47552
|
+
isFrozen: deps.isFrozen,
|
|
47553
|
+
loggedTombstoneSkips: deps.loggedTombstoneSkips,
|
|
47554
|
+
recentUnlinks: deps.recentUnlinks,
|
|
47555
|
+
commit: (before, after) => {
|
|
47556
|
+
const next = commitState(before, after);
|
|
47557
|
+
committed = next;
|
|
47558
|
+
return next;
|
|
47559
|
+
}
|
|
47560
|
+
});
|
|
47561
|
+
state = replay.state;
|
|
47562
|
+
commitPending();
|
|
47563
|
+
counters[entityType] = replay.counters;
|
|
47564
|
+
restoresDeferred += replay.restoresDeferred;
|
|
47565
|
+
if (replay.completePass) {
|
|
47566
|
+
updateSyncCursor(slug, entityType, replay.cursor);
|
|
47567
|
+
updateSyncHighWater(slug, entityType, null);
|
|
47568
|
+
for (const item of replay.completed) {
|
|
47569
|
+
clearFeedRefresh({
|
|
47570
|
+
workspaceSlug: slug,
|
|
47571
|
+
kind: "document",
|
|
47572
|
+
logicalKey: item.logicalKey,
|
|
47573
|
+
generation: item.generation
|
|
47574
|
+
});
|
|
47575
|
+
}
|
|
47576
|
+
}
|
|
47577
|
+
continue;
|
|
47578
|
+
}
|
|
47579
|
+
}
|
|
47580
|
+
const storedMark = entry2.sync_high_water?.[entityType] ?? null;
|
|
47581
|
+
let walkStart = deps.useHighWater === true && storedMark !== null ? storedMark : startCursor;
|
|
47582
|
+
let cursor = walkStart;
|
|
47583
|
+
let lastAdvancedCursor = walkStart;
|
|
47584
|
+
let finalHighWater = null;
|
|
45645
47585
|
let cursorBlocked = false;
|
|
47586
|
+
let permanentEncountered = false;
|
|
47587
|
+
const permanentJournalBlocked = entityType === "documents" && dueDocumentRefreshMarkers(slug, now()) === null;
|
|
47588
|
+
if (permanentJournalBlocked) {
|
|
47589
|
+
cursorBlocked = true;
|
|
47590
|
+
out.warn(`documents: permanent deletion journal is invalid or unreadable; ordinary rows may apply, but the document cursor and permanent dispositions are held. Run \`plinth status\` for the journal path.`);
|
|
47591
|
+
}
|
|
45646
47592
|
const ctr = {
|
|
45647
47593
|
fetched: 0,
|
|
45648
47594
|
written: 0,
|
|
@@ -45651,18 +47597,47 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
45651
47597
|
refused: 0
|
|
45652
47598
|
};
|
|
45653
47599
|
while (true) {
|
|
45654
|
-
|
|
45655
|
-
|
|
45656
|
-
|
|
45657
|
-
|
|
45658
|
-
|
|
45659
|
-
|
|
45660
|
-
|
|
47600
|
+
let page;
|
|
47601
|
+
try {
|
|
47602
|
+
page = await syncEntity({
|
|
47603
|
+
strategy,
|
|
47604
|
+
baseUrl: entry2.api_base_url,
|
|
47605
|
+
pat,
|
|
47606
|
+
cursor,
|
|
47607
|
+
fetchImpl
|
|
47608
|
+
});
|
|
47609
|
+
} catch (err) {
|
|
47610
|
+
if (cursor === walkStart && walkStart !== startCursor && err instanceof ApiError && err.status === 400) {
|
|
47611
|
+
updateSyncHighWater(slug, entityType, null);
|
|
47612
|
+
out.warn(`${entityType}: the stored high-water mark was refused; reading from the cursor instead.`);
|
|
47613
|
+
walkStart = startCursor;
|
|
47614
|
+
cursor = startCursor;
|
|
47615
|
+
lastAdvancedCursor = startCursor;
|
|
47616
|
+
continue;
|
|
47617
|
+
}
|
|
47618
|
+
throw err;
|
|
47619
|
+
}
|
|
45661
47620
|
for (const row of page.rows) {
|
|
45662
47621
|
ctr.fetched++;
|
|
47622
|
+
if (entityType === "documents" && row.deleted_at !== null && row.permanent_delete === true && row.purge_generation !== undefined) {
|
|
47623
|
+
if (permanentJournalBlocked) {
|
|
47624
|
+
cursorBlocked = true;
|
|
47625
|
+
continue;
|
|
47626
|
+
}
|
|
47627
|
+
requireFeedRefresh({
|
|
47628
|
+
workspaceSlug: slug,
|
|
47629
|
+
kind: "document",
|
|
47630
|
+
logicalKey: row.slug,
|
|
47631
|
+
requiredGeneration: row.purge_generation,
|
|
47632
|
+
now
|
|
47633
|
+
});
|
|
47634
|
+
permanentEncountered = true;
|
|
47635
|
+
cursorBlocked = true;
|
|
47636
|
+
continue;
|
|
47637
|
+
}
|
|
45663
47638
|
let outcome;
|
|
45664
47639
|
try {
|
|
45665
|
-
outcome = applyRow(slug, strategy, row, state, out, deps.suppressor, now, deps.isFrozen, deps.loggedTombstoneSkips);
|
|
47640
|
+
outcome = applyRow(slug, strategy, row, state, out, deps.suppressor, now, deps.isFrozen, deps.loggedTombstoneSkips, deps.recentUnlinks);
|
|
45666
47641
|
} catch (err) {
|
|
45667
47642
|
if (!(err instanceof MirrorSymlinkError))
|
|
45668
47643
|
throw err;
|
|
@@ -45685,25 +47660,82 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
45685
47660
|
ctr.deleted++;
|
|
45686
47661
|
else if (outcome.action === "moved")
|
|
45687
47662
|
ctr.moved = (ctr.moved ?? 0) + 1;
|
|
45688
|
-
else if (outcome.action === "
|
|
47663
|
+
else if (outcome.action === "restore_deferred") {
|
|
47664
|
+
cursorBlocked = true;
|
|
47665
|
+
restoresDeferred++;
|
|
47666
|
+
} else if (outcome.action === "slug_refused") {
|
|
45689
47667
|
ctr.refused = (ctr.refused ?? 0) + 1;
|
|
45690
47668
|
cursorBlocked = true;
|
|
45691
47669
|
}
|
|
45692
47670
|
}
|
|
45693
47671
|
commitPending();
|
|
45694
|
-
if (page.next_cursor === null)
|
|
47672
|
+
if (page.next_cursor === null) {
|
|
47673
|
+
finalHighWater = page.high_water ?? null;
|
|
45695
47674
|
break;
|
|
47675
|
+
}
|
|
45696
47676
|
if (!cursorBlocked)
|
|
45697
47677
|
lastAdvancedCursor = page.next_cursor;
|
|
45698
47678
|
cursor = page.next_cursor;
|
|
45699
47679
|
}
|
|
45700
47680
|
counters[entityType] = ctr;
|
|
45701
|
-
if (
|
|
47681
|
+
if (permanentEncountered && entityType === "documents") {
|
|
47682
|
+
const markers = dueDocumentRefreshMarkers(slug, now());
|
|
47683
|
+
if (!markers || markers.length === 0)
|
|
47684
|
+
continue;
|
|
47685
|
+
const replay = await replayPermanentDocuments({
|
|
47686
|
+
workspaceSlug: slug,
|
|
47687
|
+
baseUrl: entry2.api_base_url,
|
|
47688
|
+
pat,
|
|
47689
|
+
state,
|
|
47690
|
+
savedCursor: startCursor,
|
|
47691
|
+
strategy,
|
|
47692
|
+
markers,
|
|
47693
|
+
fetchImpl,
|
|
47694
|
+
output: out,
|
|
47695
|
+
suppressor: deps.suppressor,
|
|
47696
|
+
now,
|
|
47697
|
+
isFrozen: deps.isFrozen,
|
|
47698
|
+
loggedTombstoneSkips: deps.loggedTombstoneSkips,
|
|
47699
|
+
recentUnlinks: deps.recentUnlinks,
|
|
47700
|
+
commit: (before, after) => {
|
|
47701
|
+
const next = commitState(before, after);
|
|
47702
|
+
committed = next;
|
|
47703
|
+
return next;
|
|
47704
|
+
}
|
|
47705
|
+
});
|
|
47706
|
+
state = replay.state;
|
|
47707
|
+
commitPending();
|
|
47708
|
+
counters[entityType] = replay.counters;
|
|
47709
|
+
restoresDeferred += replay.restoresDeferred;
|
|
47710
|
+
if (replay.completePass) {
|
|
47711
|
+
updateSyncCursor(slug, entityType, replay.cursor);
|
|
47712
|
+
updateSyncHighWater(slug, entityType, null);
|
|
47713
|
+
for (const item of replay.completed) {
|
|
47714
|
+
clearFeedRefresh({
|
|
47715
|
+
workspaceSlug: slug,
|
|
47716
|
+
kind: "document",
|
|
47717
|
+
logicalKey: item.logicalKey,
|
|
47718
|
+
generation: item.generation
|
|
47719
|
+
});
|
|
47720
|
+
}
|
|
47721
|
+
}
|
|
47722
|
+
continue;
|
|
47723
|
+
}
|
|
47724
|
+
if (walkStart === startCursor && lastAdvancedCursor !== walkStart) {
|
|
45702
47725
|
updateSyncCursor(slug, entityType, lastAdvancedCursor);
|
|
45703
47726
|
}
|
|
47727
|
+
const mark = provenMark({
|
|
47728
|
+
clean: !cursorBlocked,
|
|
47729
|
+
finalHighWater,
|
|
47730
|
+
advancedTo: lastAdvancedCursor !== walkStart ? lastAdvancedCursor : null
|
|
47731
|
+
});
|
|
47732
|
+
if (mark !== null)
|
|
47733
|
+
updateSyncHighWater(slug, entityType, mark);
|
|
45704
47734
|
}
|
|
45705
47735
|
const needsDerivedTextRewalk = state.derived_text_version !== DERIVED_TEXT_VERSION;
|
|
45706
|
-
const
|
|
47736
|
+
const filesBaseCursor = needsDerivedTextRewalk ? null : entry2.sync_cursors?.["files"] ?? null;
|
|
47737
|
+
const filesMark = entry2.sync_high_water?.["files"] ?? null;
|
|
47738
|
+
const filesStartCursor = !needsDerivedTextRewalk && deps.useHighWater === true && filesMark !== null ? filesMark : filesBaseCursor;
|
|
45707
47739
|
if (needsDerivedTextRewalk) {
|
|
45708
47740
|
out.info("Re-reading the file index once to refresh extracted text (template changed).");
|
|
45709
47741
|
}
|
|
@@ -45713,6 +47745,8 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
45713
47745
|
pat,
|
|
45714
47746
|
state,
|
|
45715
47747
|
cursor: filesStartCursor,
|
|
47748
|
+
baseCursor: filesBaseCursor,
|
|
47749
|
+
recentUnlinks: deps.recentUnlinks,
|
|
45716
47750
|
fetchImpl,
|
|
45717
47751
|
now,
|
|
45718
47752
|
output: out,
|
|
@@ -45726,7 +47760,8 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
45726
47760
|
if (filesResult.state !== state) {
|
|
45727
47761
|
state = filesResult.state;
|
|
45728
47762
|
}
|
|
45729
|
-
|
|
47763
|
+
restoresDeferred += filesResult.restoresDeferred ?? 0;
|
|
47764
|
+
if (needsDerivedTextRewalk && filesResult.textUnknown === 0 && (filesResult.restoresDeferred ?? 0) === 0) {
|
|
45730
47765
|
state = { ...state, derived_text_version: DERIVED_TEXT_VERSION };
|
|
45731
47766
|
}
|
|
45732
47767
|
counters["files"] = {
|
|
@@ -45735,9 +47770,28 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
45735
47770
|
deleted: filesResult.deleted + filesResult.textDeleted
|
|
45736
47771
|
};
|
|
45737
47772
|
commitPending();
|
|
45738
|
-
|
|
47773
|
+
const filesWalkStart = filesResult.markRefused ? filesBaseCursor : filesStartCursor;
|
|
47774
|
+
const filesWalkedFromCursor = filesWalkStart === filesBaseCursor || filesResult.permanentReplayComplete === true;
|
|
47775
|
+
if (filesWalkedFromCursor && filesResult.lastCursor !== filesBaseCursor) {
|
|
45739
47776
|
updateSyncCursor(slug, "files", filesResult.lastCursor);
|
|
45740
47777
|
}
|
|
47778
|
+
if (filesResult.markRefused) {
|
|
47779
|
+
updateSyncHighWater(slug, "files", null);
|
|
47780
|
+
out.warn("files: the stored high-water mark was refused; reading from the cursor instead.");
|
|
47781
|
+
}
|
|
47782
|
+
if (filesResult.highWater)
|
|
47783
|
+
updateSyncHighWater(slug, "files", filesResult.highWater);
|
|
47784
|
+
if (filesResult.permanentReplayComplete) {
|
|
47785
|
+
updateSyncHighWater(slug, "files", null);
|
|
47786
|
+
for (const item of filesResult.permanentRefreshes ?? []) {
|
|
47787
|
+
clearFeedRefresh({
|
|
47788
|
+
workspaceSlug: slug,
|
|
47789
|
+
kind: "workspace_file",
|
|
47790
|
+
logicalKey: item.logicalKey,
|
|
47791
|
+
generation: item.generation
|
|
47792
|
+
});
|
|
47793
|
+
}
|
|
47794
|
+
}
|
|
45741
47795
|
const rulesResult = await pullWorkspaceRules({
|
|
45742
47796
|
workspaceSlug: slug,
|
|
45743
47797
|
baseUrl: entry2.api_base_url,
|
|
@@ -45916,7 +47970,7 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
45916
47970
|
const totalRefused = Object.values(counters).reduce((sum, c) => sum + (c.refused ?? 0), 0);
|
|
45917
47971
|
if (totalWritten === 0 && totalDeleted === 0 && totalMoved === 0 && totalRefused === 0) {
|
|
45918
47972
|
out.success("Already up to date.");
|
|
45919
|
-
return { exitCode: 0, applied: 0 };
|
|
47973
|
+
return { exitCode: 0, applied: 0, restoresDeferred };
|
|
45920
47974
|
}
|
|
45921
47975
|
const elapsed = ((now().getTime() - startedAt.getTime()) / 1000).toFixed(1);
|
|
45922
47976
|
for (const [type, c] of Object.entries(counters)) {
|
|
@@ -45934,9 +47988,9 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
45934
47988
|
out.info(` ${type}: ${parts.join(", ")}`);
|
|
45935
47989
|
}
|
|
45936
47990
|
out.success(`Done in ${elapsed}s.`);
|
|
45937
|
-
return { exitCode: 0, applied: totalWritten + totalMoved + totalDeleted };
|
|
47991
|
+
return { exitCode: 0, applied: totalWritten + totalMoved + totalDeleted, restoresDeferred };
|
|
45938
47992
|
}
|
|
45939
|
-
function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = () => new Date, isFrozen, loggedTombstoneSkips) {
|
|
47993
|
+
function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = () => new Date, isFrozen, loggedTombstoneSkips, recentUnlinks) {
|
|
45940
47994
|
if (isFrozen?.(strategy.type, row.id)) {
|
|
45941
47995
|
return { state, action: "skipped" };
|
|
45942
47996
|
}
|
|
@@ -45969,7 +48023,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
45969
48023
|
if (!entry2) {
|
|
45970
48024
|
let present2 = false;
|
|
45971
48025
|
try {
|
|
45972
|
-
present2 =
|
|
48026
|
+
present2 = existsSync9(entityFilePath(workspaceSlug, strategy.type, row.slug));
|
|
45973
48027
|
} catch (err) {
|
|
45974
48028
|
if (!(err instanceof MirrorPathError))
|
|
45975
48029
|
throw err;
|
|
@@ -46048,7 +48102,22 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
46048
48102
|
const writeSlug = relocation.writeSlug;
|
|
46049
48103
|
if (existingEntry && existingEntry.updated_at === live.updated_at && relocation.kind === "not_needed") {
|
|
46050
48104
|
const path5 = entityFilePath(workspaceSlug, strategy.type, live.slug);
|
|
46051
|
-
|
|
48105
|
+
const present2 = existsSync9(path5);
|
|
48106
|
+
if (!present2) {
|
|
48107
|
+
const verdict = deferRestore(recentUnlinks, path5, live.updated_at, now().getTime());
|
|
48108
|
+
if (verdict.defer) {
|
|
48109
|
+
if (verdict.first) {
|
|
48110
|
+
out.log(entityRestoreDeferredLine({
|
|
48111
|
+
entityType: strategy.type,
|
|
48112
|
+
slug: live.slug,
|
|
48113
|
+
waitSeconds: RESTORE_DEFERRAL_MS / 1000
|
|
48114
|
+
}));
|
|
48115
|
+
}
|
|
48116
|
+
return { state, action: "restore_deferred" };
|
|
48117
|
+
}
|
|
48118
|
+
}
|
|
48119
|
+
if (present2) {
|
|
48120
|
+
presentAgain(recentUnlinks, path5);
|
|
46052
48121
|
if (strategy.type === "artists") {
|
|
46053
48122
|
const artistLive = live;
|
|
46054
48123
|
state = applyArtistMemoryOnPull({
|
|
@@ -46075,7 +48144,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
46075
48144
|
if (!suppressor && existingEntry && existingEntry.updated_at !== live.updated_at && relocation.kind !== "moved") {
|
|
46076
48145
|
let localRaw = null;
|
|
46077
48146
|
try {
|
|
46078
|
-
localRaw =
|
|
48147
|
+
localRaw = readFileSync29(entityFilePath(workspaceSlug, strategy.type, writeSlug), "utf8");
|
|
46079
48148
|
} catch {
|
|
46080
48149
|
localRaw = null;
|
|
46081
48150
|
}
|
|
@@ -46095,6 +48164,38 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
46095
48164
|
}
|
|
46096
48165
|
}
|
|
46097
48166
|
}
|
|
48167
|
+
if (!suppressor && !existingEntry) {
|
|
48168
|
+
const target = entityFilePath(workspaceSlug, strategy.type, writeSlug);
|
|
48169
|
+
let localRaw = null;
|
|
48170
|
+
try {
|
|
48171
|
+
localRaw = lstatSync6(target).isFile() ? readFileSync29(target, "utf8") : null;
|
|
48172
|
+
} catch {
|
|
48173
|
+
localRaw = null;
|
|
48174
|
+
}
|
|
48175
|
+
if (localRaw !== null) {
|
|
48176
|
+
const hashStrategy = strategy;
|
|
48177
|
+
const incomingHashes = computeMirrorHashes(hashStrategy, renderMirrorWithBodyPreserve(workspaceSlug, strategy, live, writeSlug));
|
|
48178
|
+
let diverged = true;
|
|
48179
|
+
try {
|
|
48180
|
+
const localHashes = computeMirrorHashes(hashStrategy, localRaw);
|
|
48181
|
+
diverged = localHashes.bodyHash !== incomingHashes.bodyHash || localHashes.frontmatterHash !== incomingHashes.frontmatterHash;
|
|
48182
|
+
} catch {
|
|
48183
|
+
diverged = true;
|
|
48184
|
+
}
|
|
48185
|
+
if (diverged) {
|
|
48186
|
+
const stash = stashEntityConflictOnce({
|
|
48187
|
+
workspaceSlug,
|
|
48188
|
+
entityType: strategy.type,
|
|
48189
|
+
entitySlug: live.slug,
|
|
48190
|
+
timestamp: now(),
|
|
48191
|
+
contents: localRaw,
|
|
48192
|
+
side: "local"
|
|
48193
|
+
});
|
|
48194
|
+
const where = stash.reused ? `already stashed at` : `stashed it to`;
|
|
48195
|
+
out.warn(`Conflict on ${strategy.type}/${live.slug} → the local file there was not tracked for ` + `the incoming row and differed from it; ${where} ${stash.path} (cloud-wins on boot).`);
|
|
48196
|
+
}
|
|
48197
|
+
}
|
|
48198
|
+
}
|
|
46098
48199
|
const content = writeMirrorWithBodyPreserve(workspaceSlug, strategy, live, suppressor, writeSlug);
|
|
46099
48200
|
const hashes = computeMirrorHashes(strategy, content);
|
|
46100
48201
|
let inode;
|
|
@@ -46233,7 +48334,7 @@ function backfillStateInodes(workspaceSlug, state, statFn = (p) => statSync18(p)
|
|
|
46233
48334
|
return { state, changed: false };
|
|
46234
48335
|
return { state: { ...state, entries: nextEntries }, changed: true };
|
|
46235
48336
|
}
|
|
46236
|
-
function backfillBodyLengths(workspaceSlug, state, readFn = (p) =>
|
|
48337
|
+
function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync30(p, "utf8")) {
|
|
46237
48338
|
let changed = false;
|
|
46238
48339
|
const nextEntries = {};
|
|
46239
48340
|
for (const [entityType, bucket] of Object.entries(state.entries)) {
|
|
@@ -46264,7 +48365,7 @@ function makeWatchHandler(deps) {
|
|
|
46264
48365
|
baseUrl,
|
|
46265
48366
|
pat,
|
|
46266
48367
|
spinner,
|
|
46267
|
-
mutex,
|
|
48368
|
+
mutex: mutex2,
|
|
46268
48369
|
getState,
|
|
46269
48370
|
applyState,
|
|
46270
48371
|
fetchImpl,
|
|
@@ -46308,7 +48409,7 @@ function makeWatchHandler(deps) {
|
|
|
46308
48409
|
output?.log(fileDeleteSuppressedLine({ path: relPath, reason: "pull_echo" }));
|
|
46309
48410
|
return;
|
|
46310
48411
|
}
|
|
46311
|
-
if (
|
|
48412
|
+
if (existsSync10(absolutePath)) {
|
|
46312
48413
|
output?.log(fileDeleteSuppressedLine({ path: relPath, reason: "path_exists" }));
|
|
46313
48414
|
return;
|
|
46314
48415
|
}
|
|
@@ -46320,7 +48421,7 @@ function makeWatchHandler(deps) {
|
|
|
46320
48421
|
return;
|
|
46321
48422
|
}
|
|
46322
48423
|
}
|
|
46323
|
-
await
|
|
48424
|
+
await mutex2.run(`files:${relPath}`, async () => {
|
|
46324
48425
|
const base2 = getState();
|
|
46325
48426
|
const result = await pushFilesDelete({
|
|
46326
48427
|
workspaceSlug,
|
|
@@ -46374,7 +48475,7 @@ function makeWatchHandler(deps) {
|
|
|
46374
48475
|
spinner.update(`Watching ${workspaceSlug} · ${watchingSummary(getState())}`);
|
|
46375
48476
|
return;
|
|
46376
48477
|
}
|
|
46377
|
-
const bytes =
|
|
48478
|
+
const bytes = readFileSync30(absolutePath);
|
|
46378
48479
|
const hash2 = sha256Bytes(bytes);
|
|
46379
48480
|
if (suppressor?.shouldSuppressUpsert(absolutePath, {
|
|
46380
48481
|
bodyHash: hash2,
|
|
@@ -46391,7 +48492,7 @@ function makeWatchHandler(deps) {
|
|
|
46391
48492
|
return;
|
|
46392
48493
|
}
|
|
46393
48494
|
}
|
|
46394
|
-
await
|
|
48495
|
+
await mutex2.run(`files:${relPath}`, async () => {
|
|
46395
48496
|
const base2 = getState();
|
|
46396
48497
|
const result = await pushFilesUpsert({
|
|
46397
48498
|
workspaceSlug,
|
|
@@ -46438,7 +48539,7 @@ function makeWatchHandler(deps) {
|
|
|
46438
48539
|
if (suppressor) {
|
|
46439
48540
|
let raw = null;
|
|
46440
48541
|
try {
|
|
46441
|
-
raw =
|
|
48542
|
+
raw = readFileSync30(absolutePath, "utf8");
|
|
46442
48543
|
} catch {
|
|
46443
48544
|
raw = null;
|
|
46444
48545
|
}
|
|
@@ -46450,7 +48551,7 @@ function makeWatchHandler(deps) {
|
|
|
46450
48551
|
}
|
|
46451
48552
|
}
|
|
46452
48553
|
try {
|
|
46453
|
-
await
|
|
48554
|
+
await mutex2.run(WORKSPACE_RULES_MUTEX_KEY, async () => {
|
|
46454
48555
|
const base2 = getState();
|
|
46455
48556
|
const result = await pushWorkspaceRules({
|
|
46456
48557
|
workspaceSlug,
|
|
@@ -46477,7 +48578,7 @@ function makeWatchHandler(deps) {
|
|
|
46477
48578
|
if (suppressor) {
|
|
46478
48579
|
let raw = null;
|
|
46479
48580
|
try {
|
|
46480
|
-
raw =
|
|
48581
|
+
raw = readFileSync30(absolutePath, "utf8");
|
|
46481
48582
|
} catch {
|
|
46482
48583
|
raw = null;
|
|
46483
48584
|
}
|
|
@@ -46489,7 +48590,7 @@ function makeWatchHandler(deps) {
|
|
|
46489
48590
|
}
|
|
46490
48591
|
}
|
|
46491
48592
|
try {
|
|
46492
|
-
await
|
|
48593
|
+
await mutex2.run(WORKSPACE_MEMORY_MUTEX_KEY, async () => {
|
|
46493
48594
|
const base2 = getState();
|
|
46494
48595
|
const result = await pushWorkspaceMemory({
|
|
46495
48596
|
workspaceSlug,
|
|
@@ -46516,7 +48617,7 @@ function makeWatchHandler(deps) {
|
|
|
46516
48617
|
if (suppressor) {
|
|
46517
48618
|
let raw = null;
|
|
46518
48619
|
try {
|
|
46519
|
-
raw =
|
|
48620
|
+
raw = readFileSync30(absolutePath, "utf8");
|
|
46520
48621
|
} catch {
|
|
46521
48622
|
raw = null;
|
|
46522
48623
|
}
|
|
@@ -46528,7 +48629,7 @@ function makeWatchHandler(deps) {
|
|
|
46528
48629
|
}
|
|
46529
48630
|
}
|
|
46530
48631
|
try {
|
|
46531
|
-
await
|
|
48632
|
+
await mutex2.run(USER_MEMORY_MUTEX_KEY, async () => {
|
|
46532
48633
|
const base2 = getState();
|
|
46533
48634
|
const result = await pushUserMemory({
|
|
46534
48635
|
workspaceSlug,
|
|
@@ -46555,7 +48656,7 @@ function makeWatchHandler(deps) {
|
|
|
46555
48656
|
if (suppressor) {
|
|
46556
48657
|
let raw = null;
|
|
46557
48658
|
try {
|
|
46558
|
-
raw =
|
|
48659
|
+
raw = readFileSync30(absolutePath, "utf8");
|
|
46559
48660
|
} catch {
|
|
46560
48661
|
raw = null;
|
|
46561
48662
|
}
|
|
@@ -46567,7 +48668,7 @@ function makeWatchHandler(deps) {
|
|
|
46567
48668
|
}
|
|
46568
48669
|
}
|
|
46569
48670
|
try {
|
|
46570
|
-
await
|
|
48671
|
+
await mutex2.run(VOICE_MUTEX_KEY, async () => {
|
|
46571
48672
|
const base2 = getState();
|
|
46572
48673
|
const result = await pushVoice({
|
|
46573
48674
|
workspaceSlug,
|
|
@@ -46594,7 +48695,7 @@ function makeWatchHandler(deps) {
|
|
|
46594
48695
|
if (suppressor) {
|
|
46595
48696
|
let raw = null;
|
|
46596
48697
|
try {
|
|
46597
|
-
raw =
|
|
48698
|
+
raw = readFileSync30(absolutePath, "utf8");
|
|
46598
48699
|
} catch {
|
|
46599
48700
|
raw = null;
|
|
46600
48701
|
}
|
|
@@ -46617,7 +48718,7 @@ function makeWatchHandler(deps) {
|
|
|
46617
48718
|
now,
|
|
46618
48719
|
output,
|
|
46619
48720
|
suppressor,
|
|
46620
|
-
mutex
|
|
48721
|
+
mutex: mutex2
|
|
46621
48722
|
});
|
|
46622
48723
|
applyState(base2, result.state);
|
|
46623
48724
|
reportArtistMemoryOutcome(spinner, workspaceSlug, result.state, result.outcome);
|
|
@@ -46640,7 +48741,7 @@ function makeWatchHandler(deps) {
|
|
|
46640
48741
|
const strategy = getStrategy(classifiedFrom.entityType);
|
|
46641
48742
|
let toRaw = null;
|
|
46642
48743
|
try {
|
|
46643
|
-
toRaw =
|
|
48744
|
+
toRaw = readFileSync30(event.toPath, "utf8");
|
|
46644
48745
|
} catch {
|
|
46645
48746
|
toRaw = null;
|
|
46646
48747
|
}
|
|
@@ -46667,7 +48768,7 @@ function makeWatchHandler(deps) {
|
|
|
46667
48768
|
deps: {
|
|
46668
48769
|
fetchImpl,
|
|
46669
48770
|
now,
|
|
46670
|
-
mutex,
|
|
48771
|
+
mutex: mutex2,
|
|
46671
48772
|
output,
|
|
46672
48773
|
echo: suppressor ? { watcher: "live", suppressor } : undefined
|
|
46673
48774
|
}
|
|
@@ -46709,7 +48810,7 @@ function makeWatchHandler(deps) {
|
|
|
46709
48810
|
if (event.kind === "upsert") {
|
|
46710
48811
|
try {
|
|
46711
48812
|
ownEcho = suppressor?.shouldSuppressUpsert(absolutePath, {
|
|
46712
|
-
bodyHash: stableHash(
|
|
48813
|
+
bodyHash: stableHash(readFileSync30(absolutePath, "utf8")),
|
|
46713
48814
|
frontmatterHash: ""
|
|
46714
48815
|
}) === true;
|
|
46715
48816
|
} catch {
|
|
@@ -46764,7 +48865,7 @@ function makeWatchHandler(deps) {
|
|
|
46764
48865
|
if (strategy) {
|
|
46765
48866
|
let raw = null;
|
|
46766
48867
|
try {
|
|
46767
|
-
raw =
|
|
48868
|
+
raw = readFileSync30(absolutePath, "utf8");
|
|
46768
48869
|
} catch {
|
|
46769
48870
|
raw = null;
|
|
46770
48871
|
}
|
|
@@ -46774,7 +48875,7 @@ function makeWatchHandler(deps) {
|
|
|
46774
48875
|
}
|
|
46775
48876
|
}
|
|
46776
48877
|
}
|
|
46777
|
-
if (event.kind === "unlink" &&
|
|
48878
|
+
if (event.kind === "unlink" && existsSync10(absolutePath)) {
|
|
46778
48879
|
output?.log(entityDeleteSuppressedLine({
|
|
46779
48880
|
entityType: classified.entityType,
|
|
46780
48881
|
slug: classified.entitySlug,
|
|
@@ -46813,7 +48914,7 @@ function makeWatchHandler(deps) {
|
|
|
46813
48914
|
deps: {
|
|
46814
48915
|
fetchImpl,
|
|
46815
48916
|
now,
|
|
46816
|
-
mutex,
|
|
48917
|
+
mutex: mutex2,
|
|
46817
48918
|
output,
|
|
46818
48919
|
suppressor,
|
|
46819
48920
|
echo: suppressor ? { watcher: "live", suppressor } : undefined
|
|
@@ -47235,7 +49336,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47235
49336
|
}
|
|
47236
49337
|
const spinner = (deps.spinnerFactory ?? (() => createSpinner({ noColor: deps.noColor })))();
|
|
47237
49338
|
spinner.start(`Watching ${slug} · ${watchingSummary(state)}`);
|
|
47238
|
-
const
|
|
49339
|
+
const mutex2 = new PerIdMutex;
|
|
47239
49340
|
const bootDrain = await drainResolvedBatches({
|
|
47240
49341
|
echo: { watcher: "none" },
|
|
47241
49342
|
workspaceSlug: slug,
|
|
@@ -47247,7 +49348,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47247
49348
|
},
|
|
47248
49349
|
fetchImpl,
|
|
47249
49350
|
now: deps.now,
|
|
47250
|
-
mutex,
|
|
49351
|
+
mutex: mutex2,
|
|
47251
49352
|
output: out
|
|
47252
49353
|
});
|
|
47253
49354
|
if (bootDrain.applied > 0 || bootDrain.discarded > 0) {
|
|
@@ -47265,7 +49366,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47265
49366
|
},
|
|
47266
49367
|
fetchImpl,
|
|
47267
49368
|
now: deps.now,
|
|
47268
|
-
mutex,
|
|
49369
|
+
mutex: mutex2,
|
|
47269
49370
|
output: out
|
|
47270
49371
|
});
|
|
47271
49372
|
if (bootUnsynced.attempted > 0) {
|
|
@@ -47317,7 +49418,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47317
49418
|
applyState: (base2, next) => {
|
|
47318
49419
|
state = rebaseState(state, base2, next);
|
|
47319
49420
|
},
|
|
47320
|
-
mutex,
|
|
49421
|
+
mutex: mutex2,
|
|
47321
49422
|
fetchImpl,
|
|
47322
49423
|
now: deps.now,
|
|
47323
49424
|
output: out,
|
|
@@ -47381,7 +49482,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47381
49482
|
applyState: (base2, next) => {
|
|
47382
49483
|
state = rebaseState(state, base2, next);
|
|
47383
49484
|
},
|
|
47384
|
-
mutex,
|
|
49485
|
+
mutex: mutex2,
|
|
47385
49486
|
fetchImpl,
|
|
47386
49487
|
now: deps.now,
|
|
47387
49488
|
output: out,
|
|
@@ -47433,7 +49534,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47433
49534
|
baseUrl: entry2.api_base_url,
|
|
47434
49535
|
pat,
|
|
47435
49536
|
spinner,
|
|
47436
|
-
mutex,
|
|
49537
|
+
mutex: mutex2,
|
|
47437
49538
|
getState: () => state,
|
|
47438
49539
|
applyState: (base2, next) => {
|
|
47439
49540
|
state = rebaseState(state, base2, next);
|
|
@@ -47453,13 +49554,18 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47453
49554
|
return { exitCode: 0 };
|
|
47454
49555
|
}
|
|
47455
49556
|
writeDaemonPid(slug, process.pid, (deps.now ?? (() => new Date))());
|
|
47456
|
-
const
|
|
49557
|
+
const ambient = createAmbientPollWiring({
|
|
49558
|
+
now: () => (deps.now?.() ?? new Date).getTime(),
|
|
49559
|
+
baseReadIntervalMs: baseReadIntervalMs()
|
|
49560
|
+
});
|
|
49561
|
+
const watcher = startWatcher({ workspaceSlug: slug, getStateEntry, classifyPath, onUnlink: ambient.onUnlink }, handler);
|
|
47457
49562
|
const quietSink = discardSink();
|
|
47458
49563
|
let draining = false;
|
|
47459
49564
|
let lastHeartbeatMs = 0;
|
|
47460
49565
|
const pullLoop = new PullLoop({
|
|
47461
49566
|
runOnce: async () => {
|
|
47462
|
-
const result = await runSync(slug, {
|
|
49567
|
+
const result = await ambient.poll((sc2) => runSync(slug, {
|
|
49568
|
+
...sc2,
|
|
47463
49569
|
fetchImpl,
|
|
47464
49570
|
keychain,
|
|
47465
49571
|
now: deps.now,
|
|
@@ -47471,7 +49577,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47471
49577
|
isFileFrozen,
|
|
47472
49578
|
loggedTombstoneSkips,
|
|
47473
49579
|
loggedStashNotices
|
|
47474
|
-
});
|
|
49580
|
+
}));
|
|
47475
49581
|
if (result.exitCode === 0) {
|
|
47476
49582
|
const applied = result.applied ?? 0;
|
|
47477
49583
|
if (applied > 0) {
|
|
@@ -47497,7 +49603,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47497
49603
|
workspaceSlug: slug,
|
|
47498
49604
|
baseUrl: entry2.api_base_url,
|
|
47499
49605
|
pat,
|
|
47500
|
-
mutex,
|
|
49606
|
+
mutex: mutex2,
|
|
47501
49607
|
commitState,
|
|
47502
49608
|
getState: () => state,
|
|
47503
49609
|
setState: setDaemonState,
|
|
@@ -47525,7 +49631,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47525
49631
|
},
|
|
47526
49632
|
fetchImpl,
|
|
47527
49633
|
now: deps.now,
|
|
47528
|
-
mutex,
|
|
49634
|
+
mutex: mutex2,
|
|
47529
49635
|
output: out
|
|
47530
49636
|
});
|
|
47531
49637
|
if (drained.applied > 0 || drained.discarded > 0) {
|
|
@@ -47543,7 +49649,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47543
49649
|
},
|
|
47544
49650
|
fetchImpl,
|
|
47545
49651
|
now: deps.now,
|
|
47546
|
-
mutex,
|
|
49652
|
+
mutex: mutex2,
|
|
47547
49653
|
output: out
|
|
47548
49654
|
});
|
|
47549
49655
|
if (redriven.resolved > 0) {
|
|
@@ -47555,7 +49661,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47555
49661
|
spinner.update(`Watching ${slug} · ${watchingSummary(state)}`);
|
|
47556
49662
|
}
|
|
47557
49663
|
let rulesBase = state;
|
|
47558
|
-
const rulesRecon = await
|
|
49664
|
+
const rulesRecon = await mutex2.run(WORKSPACE_RULES_MUTEX_KEY, () => {
|
|
47559
49665
|
rulesBase = state;
|
|
47560
49666
|
return reconcileWorkspaceRules({
|
|
47561
49667
|
workspaceSlug: slug,
|
|
@@ -47571,7 +49677,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47571
49677
|
state = rebaseState(state, rulesBase, rulesRecon.state);
|
|
47572
49678
|
reportSingletonReconcile(spinner, slug, state, WORKSPACE_RULES_FILENAME, rulesRecon.outcome);
|
|
47573
49679
|
let memoryBase = state;
|
|
47574
|
-
const memoryRecon = await
|
|
49680
|
+
const memoryRecon = await mutex2.run(WORKSPACE_MEMORY_MUTEX_KEY, () => {
|
|
47575
49681
|
memoryBase = state;
|
|
47576
49682
|
return reconcileWorkspaceMemory({
|
|
47577
49683
|
workspaceSlug: slug,
|
|
@@ -47587,7 +49693,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47587
49693
|
state = rebaseState(state, memoryBase, memoryRecon.state);
|
|
47588
49694
|
reportSingletonReconcile(spinner, slug, state, WORKSPACE_MEMORY_FILENAME, memoryRecon.outcome);
|
|
47589
49695
|
let userMemoryBase = state;
|
|
47590
|
-
const userMemoryRecon = await
|
|
49696
|
+
const userMemoryRecon = await mutex2.run(USER_MEMORY_MUTEX_KEY, () => {
|
|
47591
49697
|
userMemoryBase = state;
|
|
47592
49698
|
return reconcileUserMemory({
|
|
47593
49699
|
workspaceSlug: slug,
|
|
@@ -47604,7 +49710,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47604
49710
|
reportSingletonReconcile(spinner, slug, state, USER_MEMORY_FILENAME, userMemoryRecon.outcome);
|
|
47605
49711
|
try {
|
|
47606
49712
|
let voiceBase = state;
|
|
47607
|
-
const voiceRecon = await
|
|
49713
|
+
const voiceRecon = await mutex2.run(VOICE_MUTEX_KEY, () => {
|
|
47608
49714
|
voiceBase = state;
|
|
47609
49715
|
return reconcileVoice({
|
|
47610
49716
|
workspaceSlug: slug,
|
|
@@ -47635,7 +49741,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
47635
49741
|
now: deps.now,
|
|
47636
49742
|
output: out,
|
|
47637
49743
|
suppressor,
|
|
47638
|
-
mutex
|
|
49744
|
+
mutex: mutex2
|
|
47639
49745
|
});
|
|
47640
49746
|
state = rebaseState(state, memBase, memRecon.state);
|
|
47641
49747
|
if (memRecon.pushed > 0) {
|
|
@@ -48291,12 +50397,12 @@ function assessContextWalls(workspaceSlug, deps = {}) {
|
|
|
48291
50397
|
|
|
48292
50398
|
// src/lib/build-stamp.ts
|
|
48293
50399
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
48294
|
-
import { existsSync as
|
|
48295
|
-
import { dirname as
|
|
50400
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync12, readFileSync as readFileSync31, writeFileSync as writeFileSync8 } from "node:fs";
|
|
50401
|
+
import { dirname as dirname14, join as join19 } from "node:path";
|
|
48296
50402
|
import { fileURLToPath } from "node:url";
|
|
48297
50403
|
var BUILD_STAMP_FILENAME = "build-stamp.json";
|
|
48298
50404
|
function stampPath(distDir) {
|
|
48299
|
-
return
|
|
50405
|
+
return join19(distDir, BUILD_STAMP_FILENAME);
|
|
48300
50406
|
}
|
|
48301
50407
|
function readHead(repoDir) {
|
|
48302
50408
|
try {
|
|
@@ -48315,7 +50421,7 @@ function readHead(repoDir) {
|
|
|
48315
50421
|
}
|
|
48316
50422
|
function readBuildStamp(distDir) {
|
|
48317
50423
|
try {
|
|
48318
|
-
const parsed = JSON.parse(
|
|
50424
|
+
const parsed = JSON.parse(readFileSync31(stampPath(distDir), "utf8"));
|
|
48319
50425
|
if (parsed && typeof parsed.sha === "string" && parsed.sha.length > 0 && typeof parsed.dirty === "boolean" && typeof parsed.built_at === "string") {
|
|
48320
50426
|
return { sha: parsed.sha, dirty: parsed.dirty, built_at: parsed.built_at };
|
|
48321
50427
|
}
|
|
@@ -48327,9 +50433,9 @@ function readBuildStamp(distDir) {
|
|
|
48327
50433
|
function findRepoRoot(fromDir) {
|
|
48328
50434
|
let dir = fromDir;
|
|
48329
50435
|
for (;; ) {
|
|
48330
|
-
if (
|
|
50436
|
+
if (existsSync11(join19(dir, ".git")))
|
|
48331
50437
|
return dir;
|
|
48332
|
-
const parent =
|
|
50438
|
+
const parent = dirname14(dir);
|
|
48333
50439
|
if (parent === dir)
|
|
48334
50440
|
return null;
|
|
48335
50441
|
dir = parent;
|
|
@@ -48353,7 +50459,7 @@ function checkBuildStaleness(deps = {}) {
|
|
|
48353
50459
|
const metaUrl = deps.metaUrl ?? import.meta.url;
|
|
48354
50460
|
let distDir;
|
|
48355
50461
|
try {
|
|
48356
|
-
distDir =
|
|
50462
|
+
distDir = dirname14(fileURLToPath(metaUrl));
|
|
48357
50463
|
} catch {
|
|
48358
50464
|
return { kind: "unknown" };
|
|
48359
50465
|
}
|
|
@@ -48376,12 +50482,12 @@ function checkBuildStaleness(deps = {}) {
|
|
|
48376
50482
|
}
|
|
48377
50483
|
|
|
48378
50484
|
// src/lib/memory-links.ts
|
|
48379
|
-
import { readFileSync as
|
|
50485
|
+
import { readFileSync as readFileSync32 } from "node:fs";
|
|
48380
50486
|
import { relative as relative15, sep as sep8 } from "node:path";
|
|
48381
50487
|
var MEMORY_FILE_RE = /^(workspace-memory\.md|user-memory\.md|artists\/[^/]+\/memory\.md|memory\/[^/]+\.md|memory\/private\/[^/]+\.md)$/i;
|
|
48382
50488
|
function scanMemoryLinks(deps) {
|
|
48383
50489
|
const listFiles = deps.listFiles ?? ((r) => walkSubtreeFiles(r, isVcsInternal));
|
|
48384
|
-
const readFile = deps.readFile ?? ((p) =>
|
|
50490
|
+
const readFile = deps.readFile ?? ((p) => readFileSync32(p, "utf8"));
|
|
48385
50491
|
let root;
|
|
48386
50492
|
let absPaths;
|
|
48387
50493
|
try {
|
|
@@ -48425,7 +50531,7 @@ function scanMemoryLinks(deps) {
|
|
|
48425
50531
|
init_output();
|
|
48426
50532
|
|
|
48427
50533
|
// src/lib/pending-writes.ts
|
|
48428
|
-
import { readFileSync as
|
|
50534
|
+
import { readFileSync as readFileSync33, statSync as statSync19 } from "node:fs";
|
|
48429
50535
|
import { relative as relative16 } from "node:path";
|
|
48430
50536
|
var import_gray_matter15 = __toESM(require_gray_matter(), 1);
|
|
48431
50537
|
var EMPTY3 = { writes: [], count: 0 };
|
|
@@ -48473,7 +50579,7 @@ function scanPendingWrites(args) {
|
|
|
48473
50579
|
continue;
|
|
48474
50580
|
let hash2;
|
|
48475
50581
|
try {
|
|
48476
|
-
hash2 = sha256Bytes(
|
|
50582
|
+
hash2 = sha256Bytes(readFileSync33(absolutePath));
|
|
48477
50583
|
} catch {
|
|
48478
50584
|
continue;
|
|
48479
50585
|
}
|
|
@@ -48504,7 +50610,7 @@ function scanPendingWrites(args) {
|
|
|
48504
50610
|
continue;
|
|
48505
50611
|
let content;
|
|
48506
50612
|
try {
|
|
48507
|
-
content =
|
|
50613
|
+
content = readFileSync33(absolutePath, "utf8");
|
|
48508
50614
|
} catch {
|
|
48509
50615
|
continue;
|
|
48510
50616
|
}
|
|
@@ -48834,6 +50940,33 @@ async function runStatus(args, deps = {}) {
|
|
|
48834
50940
|
}
|
|
48835
50941
|
}
|
|
48836
50942
|
out.info("");
|
|
50943
|
+
let deletionJournal = null;
|
|
50944
|
+
let deletionJournalError = null;
|
|
50945
|
+
try {
|
|
50946
|
+
deletionJournal = loadPermanentDeletionJournal(slug);
|
|
50947
|
+
} catch (error52) {
|
|
50948
|
+
deletionJournalError = error52 instanceof Error ? error52.message : String(error52);
|
|
50949
|
+
}
|
|
50950
|
+
if (deletionJournalError) {
|
|
50951
|
+
out.info(row("Deletion", "BLOCKED — permanent deletion journal needs recovery."));
|
|
50952
|
+
out.info(cont(`${permanentDeletionJournalPath(slug)}: ${deletionJournalError}`));
|
|
50953
|
+
} else {
|
|
50954
|
+
const pending2 = deletionJournal.records.filter((record2) => record2.disposition === "pending");
|
|
50955
|
+
const refresh = deletionJournal.refresh;
|
|
50956
|
+
const operations = deletionJournal.records.filter((record2) => record2.operation?.state === "pending" || record2.operation?.state === "transport_unknown");
|
|
50957
|
+
if (pending2.length === 0 && refresh.length === 0 && operations.length === 0) {
|
|
50958
|
+
out.info(row("Deletion", "No permanent deletion reconciliation pending."));
|
|
50959
|
+
} else {
|
|
50960
|
+
out.info(row("Deletion", `${pending2.length} disposition${pending2.length === 1 ? "" : "s"} pending; ` + `${refresh.length} feed refresh${refresh.length === 1 ? "" : "es"} required; ` + `${operations.length} create operation${operations.length === 1 ? "" : "s"} unresolved.`));
|
|
50961
|
+
for (const marker of refresh.slice(0, 5)) {
|
|
50962
|
+
out.info(cont(`${marker.kind}:${marker.logical_key} — ${marker.state}` + (marker.retry_after ? `, retry after ${marker.retry_after}` : "")));
|
|
50963
|
+
}
|
|
50964
|
+
for (const record2 of operations.slice(0, 5)) {
|
|
50965
|
+
out.info(cont(`${record2.kind}:${record2.logical_key} — create ${record2.operation.state}`));
|
|
50966
|
+
}
|
|
50967
|
+
}
|
|
50968
|
+
}
|
|
50969
|
+
out.info("");
|
|
48837
50970
|
const held = listPendingBatches(slug).filter((b) => b.status === "pending");
|
|
48838
50971
|
if (held.length === 0) {
|
|
48839
50972
|
out.info(row("Pending", "No destructive batches held."));
|
|
@@ -49388,7 +51521,7 @@ var versionCommand = defineCommand({
|
|
|
49388
51521
|
});
|
|
49389
51522
|
|
|
49390
51523
|
// src/commands/voice-gate.ts
|
|
49391
|
-
import { readFileSync as
|
|
51524
|
+
import { readFileSync as readFileSync36 } from "node:fs";
|
|
49392
51525
|
|
|
49393
51526
|
// src/lib/voice-gate/api.ts
|
|
49394
51527
|
var VoiceGateSurfaces = [
|
|
@@ -49490,15 +51623,15 @@ async function callVoiceGate(req) {
|
|
|
49490
51623
|
}
|
|
49491
51624
|
|
|
49492
51625
|
// src/lib/voice-gate/attempts.ts
|
|
49493
|
-
import { createHash as
|
|
51626
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
49494
51627
|
import {
|
|
49495
|
-
mkdirSync as
|
|
49496
|
-
readFileSync as
|
|
49497
|
-
renameSync as
|
|
49498
|
-
unlinkSync as
|
|
51628
|
+
mkdirSync as mkdirSync13,
|
|
51629
|
+
readFileSync as readFileSync34,
|
|
51630
|
+
renameSync as renameSync9,
|
|
51631
|
+
unlinkSync as unlinkSync10,
|
|
49499
51632
|
writeFileSync as writeFileSync9
|
|
49500
51633
|
} from "node:fs";
|
|
49501
|
-
import { join as
|
|
51634
|
+
import { join as join20 } from "node:path";
|
|
49502
51635
|
var MAX_REGENS = 2;
|
|
49503
51636
|
var ENTRY_TTL_MS = 24 * 60 * 60 * 1000;
|
|
49504
51637
|
var AttemptEntrySchema = exports_external.object({
|
|
@@ -49511,20 +51644,20 @@ var AttemptsFileSchema = exports_external.object({
|
|
|
49511
51644
|
entries: exports_external.record(exports_external.string(), AttemptEntrySchema)
|
|
49512
51645
|
});
|
|
49513
51646
|
function attemptsDir(workspaceSlug) {
|
|
49514
|
-
return
|
|
51647
|
+
return join20(plinthHome(), "_voice_gate", workspaceSlug);
|
|
49515
51648
|
}
|
|
49516
51649
|
function attemptsPath(workspaceSlug) {
|
|
49517
|
-
return
|
|
51650
|
+
return join20(attemptsDir(workspaceSlug), "attempts.json");
|
|
49518
51651
|
}
|
|
49519
51652
|
function attemptKey(identity) {
|
|
49520
|
-
return
|
|
51653
|
+
return createHash7("sha256").update(identity, "utf8").digest("hex").slice(0, 32);
|
|
49521
51654
|
}
|
|
49522
51655
|
function emptyFile() {
|
|
49523
51656
|
return { version: 1, entries: {} };
|
|
49524
51657
|
}
|
|
49525
51658
|
function load2(workspaceSlug) {
|
|
49526
51659
|
try {
|
|
49527
|
-
const raw =
|
|
51660
|
+
const raw = readFileSync34(attemptsPath(workspaceSlug), "utf8");
|
|
49528
51661
|
const parsed = AttemptsFileSchema.safeParse(JSON.parse(raw));
|
|
49529
51662
|
return parsed.success ? parsed.data : emptyFile();
|
|
49530
51663
|
} catch {
|
|
@@ -49544,14 +51677,14 @@ function save(workspaceSlug, file2) {
|
|
|
49544
51677
|
const dir = attemptsDir(workspaceSlug);
|
|
49545
51678
|
const target = attemptsPath(workspaceSlug);
|
|
49546
51679
|
const tmp = `${target}.${process.pid}.${tmpCounter2++}.tmp`;
|
|
49547
|
-
|
|
51680
|
+
mkdirSync13(dir, { recursive: true, mode: 448 });
|
|
49548
51681
|
writeFileSync9(tmp, `${JSON.stringify(file2, null, 2)}
|
|
49549
51682
|
`, { mode: 384 });
|
|
49550
51683
|
try {
|
|
49551
|
-
|
|
51684
|
+
renameSync9(tmp, target);
|
|
49552
51685
|
} catch (err) {
|
|
49553
51686
|
try {
|
|
49554
|
-
|
|
51687
|
+
unlinkSync10(tmp);
|
|
49555
51688
|
} catch {}
|
|
49556
51689
|
throw err;
|
|
49557
51690
|
}
|
|
@@ -49687,7 +51820,7 @@ function denyUngated(detail) {
|
|
|
49687
51820
|
}
|
|
49688
51821
|
|
|
49689
51822
|
// src/lib/voice-gate/payload.ts
|
|
49690
|
-
import { readFileSync as
|
|
51823
|
+
import { readFileSync as readFileSync35, statSync as statSync21 } from "node:fs";
|
|
49691
51824
|
import { homedir as homedir2 } from "node:os";
|
|
49692
51825
|
import { isAbsolute as isAbsolute4, resolve as resolve6 } from "node:path";
|
|
49693
51826
|
var MAX_BODY_BYTES = 256 * 1024;
|
|
@@ -49848,7 +51981,7 @@ function defaultReadBodyFile(path5) {
|
|
|
49848
51981
|
if (stat4.size > MAX_BODY_BYTES) {
|
|
49849
51982
|
throw new Error(`draft body is ${stat4.size} bytes, over the ${MAX_BODY_BYTES}-byte gate limit`);
|
|
49850
51983
|
}
|
|
49851
|
-
return
|
|
51984
|
+
return readFileSync35(path5, "utf8");
|
|
49852
51985
|
}
|
|
49853
51986
|
function parseRecipients(...raw) {
|
|
49854
51987
|
const all = raw.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0);
|
|
@@ -50208,7 +52341,7 @@ async function runVoiceGateDirect(args, deps = {}) {
|
|
|
50208
52341
|
let text;
|
|
50209
52342
|
if (args.file) {
|
|
50210
52343
|
try {
|
|
50211
|
-
text =
|
|
52344
|
+
text = readFileSync36(args.file, "utf8");
|
|
50212
52345
|
} catch (err) {
|
|
50213
52346
|
const msg = err instanceof Error ? err.message : "unknown error";
|
|
50214
52347
|
out.error(`Could not read ${args.file}: ${msg}`);
|