@plinth-music/cli 0.22.0 → 0.23.0

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/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.22.0",
7587
+ version: "0.23.0",
7588
7588
  description: "Plinth capstone sync daemon and CLI",
7589
7589
  type: "module",
7590
7590
  private: false,
@@ -22148,7 +22148,22 @@ 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({
@@ -23893,6 +23908,17 @@ function moveEntityFile(workspaceSlug, entityType, fromSlug, toSlug, beforeMove)
23893
23908
  throw err;
23894
23909
  }
23895
23910
  }
23911
+ function deleteMirrorFile(workspaceSlug, entityType, entitySlug) {
23912
+ const target = entityFilePath(workspaceSlug, entityType, entitySlug);
23913
+ try {
23914
+ unlinkSync(target);
23915
+ return true;
23916
+ } catch (err) {
23917
+ if (err?.code === "ENOENT")
23918
+ return false;
23919
+ throw err;
23920
+ }
23921
+ }
23896
23922
  var ARTIST_MEMORY_ANCHOR = "memory.md";
23897
23923
  function artistMemoryFilePath(workspaceSlug, entitySlug) {
23898
23924
  assertSlug("entity slug", entitySlug);
@@ -29609,6 +29635,13 @@ class ApiError extends Error {
29609
29635
  this.name = "ApiError";
29610
29636
  }
29611
29637
  }
29638
+ var PermanentNameRetiredSchema = exports_external.object({
29639
+ code: exports_external.literal("permanent_name_retired"),
29640
+ entity_kind: exports_external.enum(["document", "workspace_file"]),
29641
+ logical_key: exports_external.string().min(1),
29642
+ action: exports_external.literal("refresh_required"),
29643
+ error: exports_external.string().optional()
29644
+ });
29612
29645
  function parseRetryAfterMs(headerValue, now = () => Date.now()) {
29613
29646
  if (!headerValue)
29614
29647
  return;
@@ -29622,6 +29655,92 @@ function parseRetryAfterMs(headerValue, now = () => Date.now()) {
29622
29655
  const delta = dateMs - now();
29623
29656
  return delta > 0 ? delta : 0;
29624
29657
  }
29658
+ var OperationEnvelopeBase = exports_external.object({
29659
+ entity_kind: exports_external.enum(["document", "workspace_file"]),
29660
+ operation_id: exports_external.string().uuid()
29661
+ });
29662
+ var OperationBoundBase = OperationEnvelopeBase.extend({
29663
+ logical_key: exports_external.string().min(1),
29664
+ payload_digest: exports_external.string().regex(/^[0-9a-f]{64}$/)
29665
+ });
29666
+ var OperationResolutionSchema = exports_external.discriminatedUnion("status", [
29667
+ OperationBoundBase.extend({ status: exports_external.literal("pending") }),
29668
+ OperationBoundBase.extend({ status: exports_external.literal("finalized"), live: exports_external.unknown() }),
29669
+ OperationBoundBase.extend({ status: exports_external.literal("terminal_cleaned") }),
29670
+ OperationEnvelopeBase.extend({ status: exports_external.literal("retired_not_found") })
29671
+ ]);
29672
+ var CurrentNamespaceSchema = exports_external.object({
29673
+ entity_kind: exports_external.enum(["document", "workspace_file"]),
29674
+ logical_key: exports_external.string().min(1),
29675
+ latest_retirement: exports_external.object({
29676
+ purge_generation: exports_external.string().uuid(),
29677
+ retirement_sequence: exports_external.number().int().positive().max(Number.MAX_SAFE_INTEGER)
29678
+ }).nullable(),
29679
+ live_owner: exports_external.object({ id: exports_external.string().uuid() }).nullable()
29680
+ });
29681
+ async function resolvePermanentDeletionOperation(args) {
29682
+ const { baseUrl, pat, entityKind, operationId, fetchImpl = fetch } = args;
29683
+ const url2 = `${baseUrl.replace(/\/$/, "")}/api/sync/permanent-deletions/operations/resolve`;
29684
+ let res;
29685
+ try {
29686
+ res = await fetchImpl(url2, {
29687
+ method: "POST",
29688
+ headers: {
29689
+ Authorization: `Bearer ${pat}`,
29690
+ Accept: "application/json",
29691
+ "Content-Type": "application/json"
29692
+ },
29693
+ body: JSON.stringify({ entity_kind: entityKind, operation_id: operationId })
29694
+ });
29695
+ } catch (error52) {
29696
+ const detail = error52 instanceof Error ? error52.message : "network error";
29697
+ throw new ApiError(0, `Could not reach ${url2}: ${detail}`);
29698
+ }
29699
+ if (res.status === 401)
29700
+ throw new ApiError(401, "Token was rejected by the server.");
29701
+ if (res.status === 429) {
29702
+ throw new ApiError(429, "Rate limited by the server.", parseRetryAfterMs(res.headers.get("retry-after")));
29703
+ }
29704
+ if (!res.ok) {
29705
+ throw new ApiError(res.status, `resolve permanent deletion operation failed with HTTP ${res.status}`);
29706
+ }
29707
+ const parsed = OperationResolutionSchema.safeParse(await res.json());
29708
+ if (!parsed.success) {
29709
+ throw new ApiError(res.status, `operation resolution response did not match the expected shape: ${parsed.error.message}`);
29710
+ }
29711
+ return parsed.data;
29712
+ }
29713
+ async function getCurrentPermanentNamespace(args) {
29714
+ const { baseUrl, pat, entityKind, logicalKey, fetchImpl = fetch } = args;
29715
+ const params = new URLSearchParams({ entity_kind: entityKind, logical_key: logicalKey });
29716
+ const url2 = `${baseUrl.replace(/\/$/, "")}/api/sync/permanent-deletions/namespaces/current?${params}`;
29717
+ let res;
29718
+ try {
29719
+ res = await fetchImpl(url2, {
29720
+ method: "GET",
29721
+ headers: { Authorization: `Bearer ${pat}`, Accept: "application/json" }
29722
+ });
29723
+ } catch (error52) {
29724
+ const detail = error52 instanceof Error ? error52.message : "network error";
29725
+ throw new ApiError(0, `Could not reach ${url2}: ${detail}`);
29726
+ }
29727
+ if (res.status === 401)
29728
+ throw new ApiError(401, "Token was rejected by the server.");
29729
+ if (res.status === 429) {
29730
+ throw new ApiError(429, "Rate limited by the server.", parseRetryAfterMs(res.headers.get("retry-after")));
29731
+ }
29732
+ if (!res.ok) {
29733
+ throw new ApiError(res.status, `get current permanent namespace failed with HTTP ${res.status}`);
29734
+ }
29735
+ const parsed = CurrentNamespaceSchema.safeParse(await res.json());
29736
+ if (!parsed.success) {
29737
+ throw new ApiError(res.status, `current permanent namespace response did not match the expected shape: ${parsed.error.message}`);
29738
+ }
29739
+ if (parsed.data.entity_kind !== entityKind || parsed.data.logical_key !== logicalKey) {
29740
+ throw new ApiError(res.status, "current permanent namespace response identity mismatch");
29741
+ }
29742
+ return parsed.data;
29743
+ }
29625
29744
  async function whoami(baseUrl, pat, fetchImpl = fetch) {
29626
29745
  const url2 = `${baseUrl.replace(/\/$/, "")}/api/auth/cli/whoami`;
29627
29746
  let res;
@@ -29797,6 +29916,16 @@ async function patchEntity(args) {
29797
29916
  }
29798
29917
  if (res.status === 409) {
29799
29918
  const json2 = await res.json();
29919
+ const retiredParse = PermanentNameRetiredSchema.safeParse(json2);
29920
+ if (retiredParse.success) {
29921
+ return {
29922
+ ok: false,
29923
+ code: 409,
29924
+ reason: "permanent_name_retired",
29925
+ entityKind: retiredParse.data.entity_kind,
29926
+ logicalKey: retiredParse.data.logical_key
29927
+ };
29928
+ }
29800
29929
  const contextParse = ContextDocConflictResponseSchema.safeParse(json2);
29801
29930
  if (contextParse.success) {
29802
29931
  const existing = strategy.singleResponseSchema.safeParse(contextParse.data.existing);
@@ -29853,17 +29982,33 @@ async function patchEntity(args) {
29853
29982
  throw new ApiError(res.status, `patch ${strategy.type} failed with HTTP ${res.status}`);
29854
29983
  }
29855
29984
  async function createEntity(args) {
29856
- const { strategy, baseUrl, pat, payload, fetchImpl = fetch } = args;
29985
+ const {
29986
+ strategy,
29987
+ baseUrl,
29988
+ pat,
29989
+ payload,
29990
+ retiredGeneration,
29991
+ operationId,
29992
+ fetchImpl = fetch
29993
+ } = args;
29857
29994
  const url2 = `${baseUrl.replace(/\/$/, "")}/api/sync/${strategy.apiSegment}`;
29995
+ if (retiredGeneration === undefined !== (operationId === undefined)) {
29996
+ throw new Error("retired generation and create operation id must be supplied together");
29997
+ }
29998
+ const headers = {
29999
+ Authorization: `Bearer ${pat}`,
30000
+ Accept: "application/json",
30001
+ "Content-Type": "application/json"
30002
+ };
30003
+ if (retiredGeneration) {
30004
+ headers["X-Plinth-Retired-Generation"] = retiredGeneration;
30005
+ headers["X-Plinth-Create-Operation-Id"] = operationId;
30006
+ }
29858
30007
  let res;
29859
30008
  try {
29860
30009
  res = await fetchImpl(url2, {
29861
30010
  method: "POST",
29862
- headers: {
29863
- Authorization: `Bearer ${pat}`,
29864
- Accept: "application/json",
29865
- "Content-Type": "application/json"
29866
- },
30011
+ headers,
29867
30012
  body: JSON.stringify(payload)
29868
30013
  });
29869
30014
  } catch (err) {
@@ -29880,6 +30025,16 @@ async function createEntity(args) {
29880
30025
  }
29881
30026
  if (res.status === 409) {
29882
30027
  const json2 = await res.json();
30028
+ const retiredParse = PermanentNameRetiredSchema.safeParse(json2);
30029
+ if (retiredParse.success) {
30030
+ return {
30031
+ ok: false,
30032
+ code: 409,
30033
+ reason: "permanent_name_retired",
30034
+ entityKind: retiredParse.data.entity_kind,
30035
+ logicalKey: retiredParse.data.logical_key
30036
+ };
30037
+ }
29883
30038
  const contextParse = ContextDocConflictResponseSchema.safeParse(json2);
29884
30039
  if (contextParse.success) {
29885
30040
  const existing2 = strategy.singleResponseSchema.safeParse(contextParse.data.existing);
@@ -30023,7 +30178,19 @@ var FileTombstoneSchema = exports_external.object({
30023
30178
  id: exports_external.string().uuid(),
30024
30179
  path: exports_external.string().min(1),
30025
30180
  updated_at: exports_external.string().datetime({ offset: true }),
30026
- deleted_at: exports_external.string().datetime({ offset: true })
30181
+ deleted_at: exports_external.string().datetime({ offset: true }),
30182
+ permanent_delete: exports_external.literal(true).optional(),
30183
+ purge_generation: exports_external.string().uuid().optional(),
30184
+ retirement_sequence: exports_external.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional()
30185
+ }).superRefine((row, ctx) => {
30186
+ const present2 = [
30187
+ row.permanent_delete !== undefined,
30188
+ row.purge_generation !== undefined,
30189
+ row.retirement_sequence !== undefined
30190
+ ];
30191
+ if (present2.some(Boolean) && !present2.every(Boolean)) {
30192
+ ctx.addIssue({ code: "custom", message: "permanent tombstone fields must be present together" });
30193
+ }
30027
30194
  });
30028
30195
  var FileRowSchema = exports_external.union([FileLiveSchema, FileTombstoneSchema]);
30029
30196
  var FilesPageSchema = exports_external.object({
@@ -30162,6 +30329,8 @@ async function postFile(args) {
30162
30329
  contentHash,
30163
30330
  mimeType,
30164
30331
  expectedUpdatedAt,
30332
+ retiredGeneration,
30333
+ operationId,
30165
30334
  fetchImpl = fetch
30166
30335
  } = args;
30167
30336
  const url2 = `${baseUrl.replace(/\/$/, "")}/api/sync/files`;
@@ -30174,6 +30343,13 @@ async function postFile(args) {
30174
30343
  };
30175
30344
  if (expectedUpdatedAt)
30176
30345
  headers["X-Plinth-Expected-Updated-At"] = expectedUpdatedAt;
30346
+ if (retiredGeneration === undefined !== (operationId === undefined)) {
30347
+ throw new Error("retired generation and create operation id must be supplied together");
30348
+ }
30349
+ if (retiredGeneration) {
30350
+ headers["X-Plinth-Retired-Generation"] = retiredGeneration;
30351
+ headers["X-Plinth-Create-Operation-Id"] = operationId;
30352
+ }
30177
30353
  let res;
30178
30354
  try {
30179
30355
  res = await fetchImpl(url2, { method: "POST", headers, body: bytes });
@@ -30200,6 +30376,22 @@ async function postFile(args) {
30200
30376
  }
30201
30377
  if (res.status === 409) {
30202
30378
  const json2 = await res.json();
30379
+ const retired = exports_external.object({
30380
+ code: exports_external.literal("permanent_name_retired"),
30381
+ entity_kind: exports_external.literal("workspace_file"),
30382
+ logical_key: exports_external.string().min(1),
30383
+ action: exports_external.literal("refresh_required"),
30384
+ error: exports_external.string().optional()
30385
+ }).safeParse(json2);
30386
+ if (retired.success) {
30387
+ return {
30388
+ ok: false,
30389
+ code: 409,
30390
+ reason: "permanent_name_retired",
30391
+ entityKind: retired.data.entity_kind,
30392
+ logicalKey: retired.data.logical_key
30393
+ };
30394
+ }
30203
30395
  const parsed = PreconditionSchema.safeParse(json2);
30204
30396
  if (!parsed.success) {
30205
30397
  throw new ApiError(409, `post file 409 body did not match precondition_failed shape`);
@@ -34096,7 +34288,7 @@ var sessionCommand = defineCommand({
34096
34288
 
34097
34289
  // src/commands/start.ts
34098
34290
  var import_gray_matter14 = __toESM(require_gray_matter(), 1);
34099
- import { existsSync as existsSync9, readFileSync as readFileSync28, statSync as statSync18 } from "node:fs";
34291
+ import { existsSync as existsSync10, readFileSync as readFileSync30, statSync as statSync18 } from "node:fs";
34100
34292
 
34101
34293
  // src/lib/daemon-log.ts
34102
34294
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync9, renameSync as renameSync7, statSync as statSync4 } from "node:fs";
@@ -34340,7 +34532,7 @@ class DestructiveGate {
34340
34532
  }
34341
34533
 
34342
34534
  // src/lib/resolve-pending.ts
34343
- import { readFileSync as readFileSync19 } from "node:fs";
34535
+ import { readFileSync as readFileSync20 } from "node:fs";
34344
34536
 
34345
34537
  // src/lib/conflicts.ts
34346
34538
  import { mkdirSync as mkdirSync10, readFileSync as readFileSync15, readdirSync as readdirSync6, statSync as statSync6, writeFileSync as writeFileSync7 } from "node:fs";
@@ -34359,6 +34551,26 @@ function stashConflict(args) {
34359
34551
  writeFileSync7(target, args.contents, { mode: 384 });
34360
34552
  return target;
34361
34553
  }
34554
+ function stashEntityConflictOnce(args) {
34555
+ const dir = conflictsRoot(args.workspaceSlug);
34556
+ const head = `${args.entityType}.${args.entitySlug}.`;
34557
+ const tail = new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-\\d{3}Z\\.${args.side}\\.md$`);
34558
+ let names = [];
34559
+ try {
34560
+ names = readdirSync6(dir);
34561
+ } catch {
34562
+ names = [];
34563
+ }
34564
+ const newest = names.filter((name) => name.startsWith(head) && tail.test(name.slice(head.length))).sort().pop();
34565
+ if (newest !== undefined) {
34566
+ const path5 = join12(dir, newest);
34567
+ try {
34568
+ if (readFileSync15(path5, "utf8") === args.contents)
34569
+ return { path: path5, reused: true };
34570
+ } catch {}
34571
+ }
34572
+ return { path: stashConflict(args), reused: false };
34573
+ }
34362
34574
  function fileConflictPath(args) {
34363
34575
  const { workspaceSlug, relPath, timestamp, side } = args;
34364
34576
  const iso = timestamp.toISOString().replace(/[:.]/g, "-");
@@ -34448,12 +34660,646 @@ function listConflictStashes(workspaceSlug) {
34448
34660
  }
34449
34661
 
34450
34662
  // src/lib/files/push.ts
34451
- import { readFileSync as readFileSync16, statSync as statSync7 } from "node:fs";
34663
+ import { readFileSync as readFileSync17, statSync as statSync7 } from "node:fs";
34664
+
34665
+ // src/lib/permanent-deletions.ts
34666
+ import { createHash as createHash3, randomUUID } from "node:crypto";
34667
+ import {
34668
+ chmodSync,
34669
+ closeSync as closeSync4,
34670
+ constants as constants2,
34671
+ existsSync as existsSync4,
34672
+ fsyncSync as fsyncSync2,
34673
+ fstatSync as fstatSync2,
34674
+ lstatSync as lstatSync4,
34675
+ mkdirSync as mkdirSync11,
34676
+ openSync as openSync4,
34677
+ readFileSync as readFileSync16,
34678
+ renameSync as renameSync8,
34679
+ unlinkSync as unlinkSync8,
34680
+ writeSync as writeSync2
34681
+ } from "node:fs";
34682
+ import { dirname as dirname10, join as join13 } from "node:path";
34683
+ var PERMANENT_DELETE_KINDS = ["document", "workspace_file"];
34684
+ var UUID = exports_external.string().uuid();
34685
+ var SHA256 = exports_external.string().regex(/^[0-9a-f]{64}$/);
34686
+ function canonicalLogicalKey(kind, logicalKey) {
34687
+ return kind === "document" ? isValidWorkspaceSlug(logicalKey) : validateFilesRelPath(logicalKey) === logicalKey;
34688
+ }
34689
+ var PendingOperationSchema = exports_external.object({
34690
+ operation_id: UUID,
34691
+ payload_digest: SHA256,
34692
+ state: exports_external.enum(["pending", "transport_unknown", "finalized", "terminal_cleaned"]),
34693
+ row_id: UUID.optional(),
34694
+ updated_at: exports_external.string().datetime({ offset: true })
34695
+ }).superRefine((value, ctx) => {
34696
+ if (value.state === "finalized" && value.row_id === undefined) {
34697
+ ctx.addIssue({ code: "custom", message: "finalized operation requires row_id" });
34698
+ }
34699
+ if (value.state !== "finalized" && value.row_id !== undefined) {
34700
+ ctx.addIssue({ code: "custom", message: "row_id is valid only for finalized operations" });
34701
+ }
34702
+ });
34703
+ var RetirementRecordSchema = exports_external.object({
34704
+ kind: exports_external.enum(PERMANENT_DELETE_KINDS),
34705
+ logical_key: exports_external.string().min(1),
34706
+ row_id: UUID,
34707
+ purge_generation: UUID,
34708
+ retirement_sequence: exports_external.number().int().positive().max(Number.MAX_SAFE_INTEGER),
34709
+ retired_at: exports_external.string().datetime({ offset: true }),
34710
+ disposition: exports_external.enum(["pending", "complete"]),
34711
+ completed_at: exports_external.string().datetime({ offset: true }).optional(),
34712
+ stash_path: exports_external.string().min(1).optional(),
34713
+ stash_hash: SHA256.optional(),
34714
+ operation: PendingOperationSchema.optional()
34715
+ }).superRefine((value, ctx) => {
34716
+ if (!canonicalLogicalKey(value.kind, value.logical_key)) {
34717
+ ctx.addIssue({ code: "custom", message: "retirement logical key is not canonical" });
34718
+ }
34719
+ if (value.disposition === "complete" && value.completed_at === undefined) {
34720
+ ctx.addIssue({ code: "custom", message: "complete retirement requires completed_at" });
34721
+ }
34722
+ if (value.disposition === "pending" && value.completed_at !== undefined) {
34723
+ ctx.addIssue({ code: "custom", message: "pending retirement cannot have completed_at" });
34724
+ }
34725
+ if (value.stash_path === undefined !== (value.stash_hash === undefined)) {
34726
+ ctx.addIssue({ code: "custom", message: "stash_path and stash_hash must be written together" });
34727
+ }
34728
+ });
34729
+ var RefreshMarkerSchema = exports_external.object({
34730
+ kind: exports_external.enum(PERMANENT_DELETE_KINDS),
34731
+ logical_key: exports_external.string().min(1),
34732
+ required_generation: UUID.optional(),
34733
+ started_at: exports_external.string().datetime({ offset: true }),
34734
+ attempts: exports_external.number().int().nonnegative(),
34735
+ retry_after: exports_external.string().datetime({ offset: true }).optional(),
34736
+ state: exports_external.enum(["required", "retention_missing"])
34737
+ }).superRefine((value, ctx) => {
34738
+ if (!canonicalLogicalKey(value.kind, value.logical_key)) {
34739
+ ctx.addIssue({ code: "custom", message: "refresh logical key is not canonical" });
34740
+ }
34741
+ });
34742
+ var JournalBodySchema = exports_external.object({
34743
+ version: exports_external.literal(1),
34744
+ revision: exports_external.number().int().nonnegative(),
34745
+ workspace_slug: exports_external.string().min(1),
34746
+ records: exports_external.array(RetirementRecordSchema),
34747
+ refresh: exports_external.array(RefreshMarkerSchema)
34748
+ });
34749
+ var JournalFileSchema = JournalBodySchema.extend({ checksum: SHA256 });
34750
+
34751
+ class PermanentDeletionJournalError extends Error {
34752
+ stage;
34753
+ constructor(message, stage) {
34754
+ super(message);
34755
+ this.stage = stage;
34756
+ this.name = "PermanentDeletionJournalError";
34757
+ }
34758
+ }
34759
+ function permanentDeletionJournalPath(workspaceSlug) {
34760
+ if (!isValidWorkspaceSlug(workspaceSlug)) {
34761
+ throw new PermanentDeletionJournalError(`refusing unsafe workspace slug: ${JSON.stringify(workspaceSlug)}`, "validate");
34762
+ }
34763
+ return join13(plinthHome(), workspaceSlug, "permanent-deletions.json");
34764
+ }
34765
+ function recordKey2(kind, logicalKey) {
34766
+ return `${kind}\x00${logicalKey}`;
34767
+ }
34768
+ function canonical(value) {
34769
+ if (value === null || typeof value !== "object")
34770
+ return JSON.stringify(value);
34771
+ if (Array.isArray(value))
34772
+ return `[${value.map(canonical).join(",")}]`;
34773
+ const obj = value;
34774
+ return `{${Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}:${canonical(obj[key])}`).join(",")}}`;
34775
+ }
34776
+ function canonicalPayloadDigest(value) {
34777
+ return createHash3("sha256").update(canonical(value), "utf8").digest("hex");
34778
+ }
34779
+ function fileCreatePayloadDigest(contentHash, mimeType) {
34780
+ return canonicalPayloadDigest({ content_hash: contentHash, mime_type: mimeType });
34781
+ }
34782
+ function checksum(body) {
34783
+ return createHash3("sha256").update(canonical(body), "utf8").digest("hex");
34784
+ }
34785
+ function emptyJournal(workspaceSlug) {
34786
+ return { version: 1, revision: 0, workspace_slug: workspaceSlug, records: [], refresh: [] };
34787
+ }
34788
+ function validateUniqueness(journal) {
34789
+ const records = new Set;
34790
+ for (const record2 of journal.records) {
34791
+ const key = recordKey2(record2.kind, record2.logical_key);
34792
+ if (records.has(key)) {
34793
+ throw new PermanentDeletionJournalError(`permanent deletion journal contains duplicate record ${record2.kind}:${record2.logical_key}`, "validate");
34794
+ }
34795
+ records.add(key);
34796
+ }
34797
+ const refresh = new Set;
34798
+ for (const marker of journal.refresh) {
34799
+ const key = recordKey2(marker.kind, marker.logical_key);
34800
+ if (refresh.has(key)) {
34801
+ throw new PermanentDeletionJournalError(`permanent deletion journal contains duplicate refresh marker ${marker.kind}:${marker.logical_key}`, "validate");
34802
+ }
34803
+ refresh.add(key);
34804
+ }
34805
+ }
34806
+ function assertOwnerOnlyFile(path5) {
34807
+ const stat = lstatSync4(path5);
34808
+ if (!stat.isFile() || stat.isSymbolicLink()) {
34809
+ throw new PermanentDeletionJournalError(`${path5} is not a regular file`, "validate");
34810
+ }
34811
+ if ((stat.mode & 511) !== 384) {
34812
+ throw new PermanentDeletionJournalError(`${path5} must have mode 0600`, "validate");
34813
+ }
34814
+ const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
34815
+ if (uid !== undefined && stat.uid !== uid) {
34816
+ throw new PermanentDeletionJournalError(`${path5} is not owned by the current user`, "validate");
34817
+ }
34818
+ }
34819
+ function assertPrivateDirectory(path5) {
34820
+ const stat = lstatSync4(path5);
34821
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
34822
+ throw new PermanentDeletionJournalError(`${path5} is not a private directory`, "validate");
34823
+ }
34824
+ if ((stat.mode & 511) !== 448) {
34825
+ throw new PermanentDeletionJournalError(`${path5} must have mode 0700`, "validate");
34826
+ }
34827
+ const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
34828
+ if (uid !== undefined && stat.uid !== uid) {
34829
+ throw new PermanentDeletionJournalError(`${path5} is not owned by the current user`, "validate");
34830
+ }
34831
+ }
34832
+ function parseJournal(workspaceSlug, raw) {
34833
+ let json2;
34834
+ try {
34835
+ json2 = JSON.parse(raw);
34836
+ } catch (error52) {
34837
+ const detail = error52 instanceof Error ? error52.message : String(error52);
34838
+ throw new PermanentDeletionJournalError(`permanent deletion journal is not valid JSON: ${detail}`, "validate");
34839
+ }
34840
+ const parsed = JournalFileSchema.safeParse(json2);
34841
+ if (!parsed.success) {
34842
+ throw new PermanentDeletionJournalError(`permanent deletion journal does not match schema v1: ${parsed.error.message}`, "validate");
34843
+ }
34844
+ const { checksum: storedChecksum, ...body } = parsed.data;
34845
+ if (body.workspace_slug !== workspaceSlug) {
34846
+ throw new PermanentDeletionJournalError(`permanent deletion journal belongs to ${body.workspace_slug}, not ${workspaceSlug}`, "validate");
34847
+ }
34848
+ if (checksum(body) !== storedChecksum) {
34849
+ throw new PermanentDeletionJournalError("permanent deletion journal checksum mismatch", "validate");
34850
+ }
34851
+ validateUniqueness(body);
34852
+ return body;
34853
+ }
34854
+ function fsyncDirectory(path5) {
34855
+ const fd = openSync4(path5, constants2.O_RDONLY);
34856
+ try {
34857
+ fsyncSync2(fd);
34858
+ } finally {
34859
+ closeSync4(fd);
34860
+ }
34861
+ }
34862
+ function loadPermanentDeletionJournal(workspaceSlug) {
34863
+ const target = permanentDeletionJournalPath(workspaceSlug);
34864
+ if (!existsSync4(target))
34865
+ return emptyJournal(workspaceSlug);
34866
+ assertPrivateDirectory(dirname10(target));
34867
+ assertOwnerOnlyFile(target);
34868
+ const journal = parseJournal(workspaceSlug, readFileSync16(target, "utf8"));
34869
+ try {
34870
+ fsyncDirectory(dirname10(target));
34871
+ } catch (error52) {
34872
+ const detail = error52 instanceof Error ? error52.message : String(error52);
34873
+ throw new PermanentDeletionJournalError(`permanent deletion journal durability remains indeterminate: ${detail}`, "durability_indeterminate");
34874
+ }
34875
+ return journal;
34876
+ }
34877
+ function validatedCurrentRevision(workspaceSlug, expected) {
34878
+ const current = loadPermanentDeletionJournal(workspaceSlug);
34879
+ if (current.revision !== expected) {
34880
+ throw new PermanentDeletionJournalError(`permanent deletion journal revision changed from ${expected} to ${current.revision}`, "validate");
34881
+ }
34882
+ return current;
34883
+ }
34884
+ function writePermanentDeletionJournal(prior, next) {
34885
+ if (prior.workspace_slug !== next.workspace_slug) {
34886
+ throw new PermanentDeletionJournalError("journal workspace cannot change", "validate");
34887
+ }
34888
+ validatedCurrentRevision(prior.workspace_slug, prior.revision);
34889
+ const body = JournalBodySchema.parse({ ...next, revision: prior.revision + 1 });
34890
+ validateUniqueness(body);
34891
+ const target = permanentDeletionJournalPath(body.workspace_slug);
34892
+ const parent = dirname10(target);
34893
+ mkdirSync11(parent, { recursive: true, mode: 448 });
34894
+ chmodSync(parent, 448);
34895
+ assertPrivateDirectory(parent);
34896
+ if (existsSync4(target))
34897
+ assertOwnerOnlyFile(target);
34898
+ const tmp = join13(parent, `.permanent-deletions.${process.pid}.${randomUUID()}.tmp`);
34899
+ const wire = JSON.stringify({ ...body, checksum: checksum(body) }, null, 2) + `
34900
+ `;
34901
+ let fd;
34902
+ try {
34903
+ fd = openSync4(tmp, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | (constants2.O_NOFOLLOW ?? 0), 384);
34904
+ const stat = fstatSync2(fd);
34905
+ if (!stat.isFile() || (stat.mode & 511) !== 384) {
34906
+ throw new PermanentDeletionJournalError("journal temporary file is not owner-only", "prepare");
34907
+ }
34908
+ const bytes = Buffer.from(wire, "utf8");
34909
+ let offset = 0;
34910
+ while (offset < bytes.length)
34911
+ offset += writeSync2(fd, bytes, offset, bytes.length - offset);
34912
+ fsyncSync2(fd);
34913
+ closeSync4(fd);
34914
+ fd = undefined;
34915
+ } catch (error52) {
34916
+ if (fd !== undefined) {
34917
+ try {
34918
+ closeSync4(fd);
34919
+ } catch {}
34920
+ }
34921
+ try {
34922
+ unlinkSync8(tmp);
34923
+ } catch {}
34924
+ if (error52 instanceof PermanentDeletionJournalError)
34925
+ throw error52;
34926
+ const detail = error52 instanceof Error ? error52.message : String(error52);
34927
+ throw new PermanentDeletionJournalError(`could not prepare journal write: ${detail}`, "write");
34928
+ }
34929
+ try {
34930
+ renameSync8(tmp, target);
34931
+ } catch (error52) {
34932
+ let installed2 = false;
34933
+ try {
34934
+ assertOwnerOnlyFile(target);
34935
+ const candidate = parseJournal(body.workspace_slug, readFileSync16(target, "utf8"));
34936
+ installed2 = candidate.revision === body.revision && checksum(candidate) === checksum(body);
34937
+ } catch {
34938
+ installed2 = false;
34939
+ }
34940
+ try {
34941
+ unlinkSync8(tmp);
34942
+ } catch {}
34943
+ if (!installed2) {
34944
+ const detail = error52 instanceof Error ? error52.message : String(error52);
34945
+ throw new PermanentDeletionJournalError(`could not rename journal: ${detail}`, "rename");
34946
+ }
34947
+ }
34948
+ assertOwnerOnlyFile(target);
34949
+ const installed = parseJournal(body.workspace_slug, readFileSync16(target, "utf8"));
34950
+ if (installed.revision !== body.revision || checksum(installed) !== checksum(body)) {
34951
+ throw new PermanentDeletionJournalError("renamed journal revision did not validate", "durability_indeterminate");
34952
+ }
34953
+ try {
34954
+ fsyncDirectory(parent);
34955
+ } catch (firstError) {
34956
+ assertOwnerOnlyFile(target);
34957
+ const reopened = parseJournal(body.workspace_slug, readFileSync16(target, "utf8"));
34958
+ if (reopened.revision !== body.revision || checksum(reopened) !== checksum(body)) {
34959
+ throw new PermanentDeletionJournalError("journal changed after rename", "durability_indeterminate");
34960
+ }
34961
+ try {
34962
+ fsyncDirectory(parent);
34963
+ } catch (secondError) {
34964
+ const first = firstError instanceof Error ? firstError.message : String(firstError);
34965
+ const second = secondError instanceof Error ? secondError.message : String(secondError);
34966
+ throw new PermanentDeletionJournalError(`journal renamed but parent directory sync failed (${first}; retry: ${second})`, "durability_indeterminate");
34967
+ }
34968
+ }
34969
+ return body;
34970
+ }
34971
+ function updatePermanentDeletionJournal(workspaceSlug, update) {
34972
+ const prior = loadPermanentDeletionJournal(workspaceSlug);
34973
+ const next = update(prior);
34974
+ if (next === prior)
34975
+ return prior;
34976
+ return writePermanentDeletionJournal(prior, next);
34977
+ }
34978
+ function findRetirement(journal, kind, logicalKey) {
34979
+ return journal.records.find((record2) => record2.kind === kind && record2.logical_key === logicalKey);
34980
+ }
34981
+ function findRefreshMarker(journal, kind, logicalKey) {
34982
+ return journal.refresh.find((marker) => marker.kind === kind && marker.logical_key === logicalKey);
34983
+ }
34984
+ function replaceRecord(journal, record2) {
34985
+ return {
34986
+ ...journal,
34987
+ records: [
34988
+ ...journal.records.filter((item) => recordKey2(item.kind, item.logical_key) !== recordKey2(record2.kind, record2.logical_key)),
34989
+ record2
34990
+ ]
34991
+ };
34992
+ }
34993
+ function requireFeedRefresh(args) {
34994
+ const now = args.now ?? (() => new Date);
34995
+ return updatePermanentDeletionJournal(args.workspaceSlug, (journal) => {
34996
+ const existing = findRefreshMarker(journal, args.kind, args.logicalKey);
34997
+ if (existing)
34998
+ return journal;
34999
+ return {
35000
+ ...journal,
35001
+ refresh: [...journal.refresh, {
35002
+ kind: args.kind,
35003
+ logical_key: args.logicalKey,
35004
+ ...args.requiredGeneration ? { required_generation: args.requiredGeneration } : {},
35005
+ started_at: now().toISOString(),
35006
+ attempts: 0,
35007
+ state: "required"
35008
+ }]
35009
+ };
35010
+ });
35011
+ }
35012
+ function markRetentionMissing(args) {
35013
+ const now = args.now ?? (() => new Date);
35014
+ return updatePermanentDeletionJournal(args.workspaceSlug, (journal) => {
35015
+ const existing = findRefreshMarker(journal, args.kind, args.logicalKey);
35016
+ if (!existing)
35017
+ return journal;
35018
+ const attempts = existing.attempts + 1;
35019
+ const backoffMs = Math.min(3600000, 1000 * 2 ** Math.min(attempts, 12));
35020
+ return {
35021
+ ...journal,
35022
+ refresh: journal.refresh.map((marker) => marker === existing ? {
35023
+ ...marker,
35024
+ attempts,
35025
+ state: "retention_missing",
35026
+ retry_after: new Date(now().getTime() + backoffMs).toISOString()
35027
+ } : marker)
35028
+ };
35029
+ });
35030
+ }
35031
+ function clearFeedRefresh(args) {
35032
+ return updatePermanentDeletionJournal(args.workspaceSlug, (journal) => {
35033
+ const record2 = findRetirement(journal, args.kind, args.logicalKey);
35034
+ if (!record2 || record2.disposition !== "complete" || record2.purge_generation !== args.generation) {
35035
+ throw new PermanentDeletionJournalError("cannot clear refresh before matching disposition is complete", "validate");
35036
+ }
35037
+ const next = journal.refresh.filter((marker) => recordKey2(marker.kind, marker.logical_key) !== recordKey2(args.kind, args.logicalKey));
35038
+ return next.length === journal.refresh.length ? journal : { ...journal, refresh: next };
35039
+ });
35040
+ }
35041
+ function recordRetirementPending(args) {
35042
+ return updatePermanentDeletionJournal(args.workspaceSlug, (journal) => {
35043
+ const existing = findRetirement(journal, args.kind, args.logicalKey);
35044
+ if (existing && existing.retirement_sequence > args.retirementSequence)
35045
+ return journal;
35046
+ if (existing && existing.retirement_sequence === args.retirementSequence && existing.purge_generation !== args.purgeGeneration) {
35047
+ throw new PermanentDeletionJournalError("one retirement_sequence maps to multiple generations", "validate");
35048
+ }
35049
+ if (existing?.purge_generation === args.purgeGeneration)
35050
+ return journal;
35051
+ return replaceRecord(journal, {
35052
+ kind: args.kind,
35053
+ logical_key: args.logicalKey,
35054
+ row_id: args.rowId,
35055
+ purge_generation: args.purgeGeneration,
35056
+ retirement_sequence: args.retirementSequence,
35057
+ retired_at: args.retiredAt,
35058
+ disposition: "pending"
35059
+ });
35060
+ });
35061
+ }
35062
+ function completeRetirement(args) {
35063
+ if (args.stashPath === undefined !== (args.stashHash === undefined)) {
35064
+ throw new PermanentDeletionJournalError("stash path and hash must be supplied together", "validate");
35065
+ }
35066
+ const now = args.now ?? (() => new Date);
35067
+ return updatePermanentDeletionJournal(args.workspaceSlug, (journal) => {
35068
+ const existing = findRetirement(journal, args.kind, args.logicalKey);
35069
+ if (!existing || existing.purge_generation !== args.generation) {
35070
+ throw new PermanentDeletionJournalError("cannot complete an unknown retirement generation", "validate");
35071
+ }
35072
+ if (existing.disposition === "complete")
35073
+ return journal;
35074
+ return replaceRecord(journal, {
35075
+ ...existing,
35076
+ disposition: "complete",
35077
+ completed_at: now().toISOString(),
35078
+ ...args.stashPath ? { stash_path: args.stashPath, stash_hash: args.stashHash } : {}
35079
+ });
35080
+ });
35081
+ }
35082
+ function recordRetirementStash(args) {
35083
+ return updatePermanentDeletionJournal(args.workspaceSlug, (journal) => {
35084
+ const existing = findRetirement(journal, args.kind, args.logicalKey);
35085
+ if (!existing || existing.purge_generation !== args.generation) {
35086
+ throw new PermanentDeletionJournalError("cannot attach a stash to an unknown retirement", "validate");
35087
+ }
35088
+ if (existing.stash_path === args.stashPath && existing.stash_hash === args.stashHash) {
35089
+ return journal;
35090
+ }
35091
+ if (existing.stash_path && existing.stash_hash !== args.stashHash) {
35092
+ throw new PermanentDeletionJournalError("retirement stash changed after it was recorded", "validate");
35093
+ }
35094
+ return replaceRecord(journal, {
35095
+ ...existing,
35096
+ stash_path: args.stashPath,
35097
+ stash_hash: args.stashHash
35098
+ });
35099
+ });
35100
+ }
35101
+ function prepareGenerationCreate(args) {
35102
+ const now = args.now ?? (() => new Date);
35103
+ let decision = { kind: "ordinary" };
35104
+ updatePermanentDeletionJournal(args.workspaceSlug, (journal) => {
35105
+ if (findRefreshMarker(journal, args.kind, args.logicalKey)) {
35106
+ decision = { kind: "blocked", reason: "refresh_required" };
35107
+ return journal;
35108
+ }
35109
+ const retirement = findRetirement(journal, args.kind, args.logicalKey);
35110
+ if (!retirement) {
35111
+ decision = { kind: "ordinary" };
35112
+ return journal;
35113
+ }
35114
+ if (retirement.disposition !== "complete") {
35115
+ decision = { kind: "blocked", reason: "disposition_pending" };
35116
+ return journal;
35117
+ }
35118
+ const operation = retirement.operation;
35119
+ if (operation) {
35120
+ if (operation.payload_digest === args.payloadDigest && (operation.state === "pending" || operation.state === "transport_unknown")) {
35121
+ decision = {
35122
+ kind: "authorised",
35123
+ retiredGeneration: retirement.purge_generation,
35124
+ operationId: operation.operation_id,
35125
+ payloadDigest: operation.payload_digest
35126
+ };
35127
+ return journal;
35128
+ }
35129
+ if (operation.state !== "terminal_cleaned") {
35130
+ decision = { kind: "blocked", reason: "operation_unresolved" };
35131
+ return journal;
35132
+ }
35133
+ }
35134
+ const nextOperation = {
35135
+ operation_id: randomUUID(),
35136
+ payload_digest: args.payloadDigest,
35137
+ state: "pending",
35138
+ updated_at: now().toISOString()
35139
+ };
35140
+ decision = {
35141
+ kind: "authorised",
35142
+ retiredGeneration: retirement.purge_generation,
35143
+ operationId: nextOperation.operation_id,
35144
+ payloadDigest: nextOperation.payload_digest
35145
+ };
35146
+ return replaceRecord(journal, { ...retirement, operation: nextOperation });
35147
+ });
35148
+ return decision;
35149
+ }
35150
+ function updateCreateOperation(args) {
35151
+ const now = args.now ?? (() => new Date);
35152
+ return updatePermanentDeletionJournal(args.workspaceSlug, (journal) => {
35153
+ const retirement = findRetirement(journal, args.kind, args.logicalKey);
35154
+ if (!retirement?.operation || retirement.operation.operation_id !== args.operationId) {
35155
+ throw new PermanentDeletionJournalError("operation update does not match the journal", "validate");
35156
+ }
35157
+ const from = retirement.operation.state;
35158
+ const allowed = {
35159
+ pending: new Set(["pending", "transport_unknown", "finalized", "terminal_cleaned"]),
35160
+ transport_unknown: new Set(["transport_unknown", "pending", "finalized", "terminal_cleaned"]),
35161
+ finalized: new Set(["finalized", "terminal_cleaned"]),
35162
+ terminal_cleaned: new Set(["terminal_cleaned"])
35163
+ };
35164
+ if (!allowed[from].has(args.state)) {
35165
+ throw new PermanentDeletionJournalError(`invalid create operation transition ${from} -> ${args.state}`, "validate");
35166
+ }
35167
+ if (args.state === "finalized" && args.rowId === undefined) {
35168
+ throw new PermanentDeletionJournalError("finalized operation requires a row id", "validate");
35169
+ }
35170
+ return replaceRecord(journal, {
35171
+ ...retirement,
35172
+ operation: {
35173
+ operation_id: retirement.operation.operation_id,
35174
+ payload_digest: retirement.operation.payload_digest,
35175
+ state: args.state,
35176
+ ...args.state === "finalized" && args.rowId ? { row_id: args.rowId } : {},
35177
+ updated_at: now().toISOString()
35178
+ }
35179
+ });
35180
+ });
35181
+ }
35182
+
35183
+ // src/lib/permanent-mutex.ts
35184
+ import { AsyncLocalStorage } from "node:async_hooks";
35185
+
35186
+ // src/lib/per-id-mutex.ts
35187
+ class PerIdMutex {
35188
+ chains = new Map;
35189
+ async run(key, fn) {
35190
+ const prior = this.chains.get(key) ?? Promise.resolve();
35191
+ const next = prior.then(fn, fn);
35192
+ this.chains.set(key, next);
35193
+ const cleanup = () => {
35194
+ if (this.chains.get(key) === next) {
35195
+ this.chains.delete(key);
35196
+ }
35197
+ };
35198
+ next.then(cleanup, cleanup);
35199
+ return next;
35200
+ }
35201
+ size() {
35202
+ return this.chains.size;
35203
+ }
35204
+ }
35205
+
35206
+ // src/lib/permanent-mutex.ts
35207
+ var mutex = new PerIdMutex;
35208
+ var heldKeys = new AsyncLocalStorage;
35209
+ function permanentDeletionMutexKey(kind, logicalKey) {
35210
+ return `permanent:${kind}:${logicalKey}`;
35211
+ }
35212
+ function withPermanentDeletionLock(kind, logicalKey, run) {
35213
+ const key = permanentDeletionMutexKey(kind, logicalKey);
35214
+ const held = heldKeys.getStore();
35215
+ if (held?.has(key))
35216
+ return run();
35217
+ return mutex.run(key, () => heldKeys.run(new Set([...held ?? [], key]), run));
35218
+ }
35219
+
35220
+ // src/lib/permanent-create.ts
35221
+ async function prepareOrResolveGenerationCreate(args) {
35222
+ return withPermanentDeletionLock(args.kind, args.logicalKey, () => prepareOrResolveGenerationCreateLocked(args));
35223
+ }
35224
+ async function prepareOrResolveGenerationCreateLocked(args) {
35225
+ let decision = prepareGenerationCreate(args);
35226
+ if (decision.kind !== "blocked" || decision.reason !== "operation_unresolved") {
35227
+ return decision;
35228
+ }
35229
+ const journal = loadPermanentDeletionJournal(args.workspaceSlug);
35230
+ const retirement = findRetirement(journal, args.kind, args.logicalKey);
35231
+ const operation = retirement?.operation;
35232
+ if (!operation)
35233
+ return decision;
35234
+ const entityKind = args.kind;
35235
+ const resolved = await resolvePermanentDeletionOperation({
35236
+ baseUrl: args.baseUrl,
35237
+ pat: args.pat,
35238
+ entityKind,
35239
+ operationId: operation.operation_id,
35240
+ fetchImpl: args.fetchImpl
35241
+ });
35242
+ if (resolved.entity_kind !== entityKind || resolved.operation_id !== operation.operation_id) {
35243
+ throw new Error("operation resolution identity does not match the local journal");
35244
+ }
35245
+ if (resolved.status === "retired_not_found") {
35246
+ updateCreateOperation({
35247
+ workspaceSlug: args.workspaceSlug,
35248
+ kind: args.kind,
35249
+ logicalKey: args.logicalKey,
35250
+ operationId: operation.operation_id,
35251
+ state: "terminal_cleaned",
35252
+ now: args.now
35253
+ });
35254
+ return prepareGenerationCreate(args);
35255
+ }
35256
+ if (resolved.logical_key !== args.logicalKey || resolved.payload_digest !== operation.payload_digest) {
35257
+ throw new Error("operation resolution binding does not match the local journal");
35258
+ }
35259
+ if (resolved.status === "pending") {
35260
+ updateCreateOperation({
35261
+ workspaceSlug: args.workspaceSlug,
35262
+ kind: args.kind,
35263
+ logicalKey: args.logicalKey,
35264
+ operationId: operation.operation_id,
35265
+ state: "pending",
35266
+ now: args.now
35267
+ });
35268
+ return { kind: "blocked", reason: "operation_unresolved" };
35269
+ }
35270
+ if (resolved.status === "terminal_cleaned") {
35271
+ updateCreateOperation({
35272
+ workspaceSlug: args.workspaceSlug,
35273
+ kind: args.kind,
35274
+ logicalKey: args.logicalKey,
35275
+ operationId: operation.operation_id,
35276
+ state: "terminal_cleaned",
35277
+ now: args.now
35278
+ });
35279
+ return prepareGenerationCreate(args);
35280
+ }
35281
+ const id = exports_external.object({ id: exports_external.string().uuid() }).parse(resolved.live).id;
35282
+ updateCreateOperation({
35283
+ workspaceSlug: args.workspaceSlug,
35284
+ kind: args.kind,
35285
+ logicalKey: args.logicalKey,
35286
+ operationId: operation.operation_id,
35287
+ state: "finalized",
35288
+ rowId: id,
35289
+ now: args.now
35290
+ });
35291
+ return {
35292
+ kind: "finalized",
35293
+ live: resolved.live,
35294
+ operationId: operation.operation_id,
35295
+ payloadDigest: operation.payload_digest
35296
+ };
35297
+ }
34452
35298
 
34453
35299
  // src/lib/files/content-hash.ts
34454
- import { createHash as createHash3 } from "node:crypto";
35300
+ import { createHash as createHash4 } from "node:crypto";
34455
35301
  function sha256Bytes(bytes) {
34456
- return createHash3("sha256").update(bytes).digest("hex");
35302
+ return createHash4("sha256").update(bytes).digest("hex");
34457
35303
  }
34458
35304
 
34459
35305
  // src/lib/files/mime.ts
@@ -34552,6 +35398,9 @@ async function restoreFileFromCloud(args) {
34552
35398
  });
34553
35399
  }
34554
35400
  async function pushFilesUpsert(args) {
35401
+ return withPermanentDeletionLock("workspace_file", args.relPath, () => pushFilesUpsertLocked(args));
35402
+ }
35403
+ async function pushFilesUpsertLocked(args) {
34555
35404
  const { workspaceSlug, baseUrl, pat, absolutePath, relPath, state, prepared, deps } = args;
34556
35405
  const fetchImpl = deps?.fetchImpl;
34557
35406
  const now = deps?.now ?? (() => new Date);
@@ -34604,7 +35453,7 @@ async function pushFilesUpsert(args) {
34604
35453
  state: next
34605
35454
  };
34606
35455
  }
34607
- bytes = readFileSync16(absolutePath);
35456
+ bytes = readFileSync17(absolutePath);
34608
35457
  hash2 = sha256Bytes(bytes);
34609
35458
  inode = st.ino;
34610
35459
  }
@@ -34619,7 +35468,61 @@ async function pushFilesUpsert(args) {
34619
35468
  return { outcome: { kind: "noop", relPath }, state };
34620
35469
  }
34621
35470
  const mimeType = inferMime(relPath);
35471
+ const createProtocol = existing ? { kind: "ordinary" } : await prepareOrResolveGenerationCreate({
35472
+ workspaceSlug,
35473
+ baseUrl,
35474
+ pat,
35475
+ kind: "workspace_file",
35476
+ logicalKey: relPath,
35477
+ payloadDigest: fileCreatePayloadDigest(hash2, mimeType),
35478
+ now,
35479
+ fetchImpl
35480
+ });
35481
+ if (createProtocol.kind === "blocked") {
35482
+ output?.warn(`files/${relPath} is held by permanent-delete reconciliation (${createProtocol.reason}).`);
35483
+ return {
35484
+ outcome: { kind: "permanent_delete_blocked", relPath, reason: createProtocol.reason },
35485
+ state
35486
+ };
35487
+ }
35488
+ if (createProtocol.kind === "finalized") {
35489
+ const parsed = FileLiveSchema.safeParse(createProtocol.live);
35490
+ if (!parsed.success) {
35491
+ throw new ApiError(200, "finalized file create operation live projection did not match the file schema");
35492
+ }
35493
+ if (parsed.data.path !== relPath) {
35494
+ throw new ApiError(200, "finalized file create operation live projection had the wrong path");
35495
+ }
35496
+ const adoptedState = persistFiles(state, {
35497
+ id: parsed.data.id,
35498
+ path: relPath,
35499
+ content_hash: parsed.data.content_hash,
35500
+ size_bytes: parsed.data.size_bytes,
35501
+ mime_type: parsed.data.mime_type,
35502
+ updated_at: parsed.data.updated_at,
35503
+ inode
35504
+ });
35505
+ return pushFilesUpsert({
35506
+ ...args,
35507
+ state: adoptedState,
35508
+ prepared: { bytes, hash: hash2, size, inode }
35509
+ });
35510
+ }
34622
35511
  const acceptPush = (row, action) => {
35512
+ if (createProtocol.kind === "authorised" && row.path !== relPath) {
35513
+ throw new ApiError(200, "generation-bearing file create response had the wrong path");
35514
+ }
35515
+ if (createProtocol.kind === "authorised") {
35516
+ updateCreateOperation({
35517
+ workspaceSlug,
35518
+ kind: "workspace_file",
35519
+ logicalKey: relPath,
35520
+ operationId: createProtocol.operationId,
35521
+ state: "finalized",
35522
+ rowId: row.id,
35523
+ now
35524
+ });
35525
+ }
34623
35526
  output?.log(filePushLine({ path: relPath, id: row.id, action }));
34624
35527
  return {
34625
35528
  outcome: { kind: action, relPath, id: row.id },
@@ -34655,22 +35558,57 @@ async function pushFilesUpsert(args) {
34655
35558
  output?.log(fileFailedLine({ path: relPath, status: 400, reason: "hash_mismatch" }));
34656
35559
  return { outcome: { kind: "ignored", relPath, reason: "hash_mismatch" }, state };
34657
35560
  };
34658
- const res = await postFile({
34659
- baseUrl,
34660
- pat,
34661
- relPath,
34662
- bytes,
34663
- contentHash: hash2,
34664
- mimeType,
34665
- expectedUpdatedAt: existing?.updated_at ?? null,
34666
- fetchImpl
34667
- });
35561
+ let res;
35562
+ try {
35563
+ res = await postFile({
35564
+ baseUrl,
35565
+ pat,
35566
+ relPath,
35567
+ bytes,
35568
+ contentHash: hash2,
35569
+ mimeType,
35570
+ expectedUpdatedAt: existing?.updated_at ?? null,
35571
+ ...createProtocol.kind === "authorised" ? {
35572
+ retiredGeneration: createProtocol.retiredGeneration,
35573
+ operationId: createProtocol.operationId
35574
+ } : {},
35575
+ fetchImpl
35576
+ });
35577
+ } catch (error52) {
35578
+ if (createProtocol.kind === "authorised" && (!(error52 instanceof ApiError) || error52.status === 0 || error52.status >= 500 || error52.status >= 200 && error52.status < 300)) {
35579
+ updateCreateOperation({
35580
+ workspaceSlug,
35581
+ kind: "workspace_file",
35582
+ logicalKey: relPath,
35583
+ operationId: createProtocol.operationId,
35584
+ state: "transport_unknown",
35585
+ now
35586
+ });
35587
+ }
35588
+ throw error52;
35589
+ }
34668
35590
  if (res.ok)
34669
35591
  return acceptPush(res.row, existing ? "updated" : "created");
34670
35592
  if (res.code === 413)
34671
35593
  return rejectOverCap(res.maxBytes);
34672
35594
  if (res.code === 400)
34673
35595
  return rejectHashMismatch();
35596
+ if ("reason" in res && res.reason === "permanent_name_retired") {
35597
+ if (res.entityKind !== "workspace_file" || res.logicalKey !== relPath) {
35598
+ throw new ApiError(409, "permanent-name refusal identity did not match the file create");
35599
+ }
35600
+ requireFeedRefresh({
35601
+ workspaceSlug,
35602
+ kind: "workspace_file",
35603
+ logicalKey: res.logicalKey,
35604
+ now
35605
+ });
35606
+ output?.warn(`files/${res.logicalKey} was permanently deleted in Plinth; local bytes are held while the file feed is refreshed.`);
35607
+ return {
35608
+ outcome: { kind: "permanent_delete_blocked", relPath: res.logicalKey, reason: "refresh_required" },
35609
+ state
35610
+ };
35611
+ }
34674
35612
  let nextState = state;
34675
35613
  let cloudRow;
34676
35614
  if (existing) {
@@ -34692,6 +35630,25 @@ async function pushFilesUpsert(args) {
34692
35630
  return rejectOverCap(retry.maxBytes);
34693
35631
  if (retry.code === 400)
34694
35632
  return rejectHashMismatch();
35633
+ if ("reason" in retry && retry.reason === "permanent_name_retired") {
35634
+ if (retry.entityKind !== "workspace_file" || retry.logicalKey !== relPath) {
35635
+ throw new ApiError(409, "permanent-name refusal identity did not match the file retry");
35636
+ }
35637
+ requireFeedRefresh({
35638
+ workspaceSlug,
35639
+ kind: "workspace_file",
35640
+ logicalKey: retry.logicalKey,
35641
+ now
35642
+ });
35643
+ return {
35644
+ outcome: {
35645
+ kind: "permanent_delete_blocked",
35646
+ relPath: retry.logicalKey,
35647
+ reason: "refresh_required"
35648
+ },
35649
+ state
35650
+ };
35651
+ }
34695
35652
  cloudRow = undefined;
34696
35653
  }
34697
35654
  }
@@ -34757,29 +35714,9 @@ async function pushFilesDelete(args) {
34757
35714
  return { outcome: { kind: "conflict", relPath, stashPath: "" }, state: nextState };
34758
35715
  }
34759
35716
 
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
35717
  // src/lib/push.ts
34781
35718
  var import_gray_matter10 = __toESM(require_gray_matter(), 1);
34782
- import { existsSync as existsSync4, readFileSync as readFileSync18, statSync as statSync8 } from "node:fs";
35719
+ import { existsSync as existsSync5, readFileSync as readFileSync19, statSync as statSync8 } from "node:fs";
34783
35720
  import { basename as basename5, relative as relative5 } from "node:path";
34784
35721
 
34785
35722
  // src/lib/frontmatter-id.ts
@@ -34829,7 +35766,7 @@ function frontmatterIdWriteIsSound(next, id, expectedBody) {
34829
35766
 
34830
35767
  // src/lib/pull-write.ts
34831
35768
  var import_gray_matter9 = __toESM(require_gray_matter(), 1);
34832
- import { readFileSync as readFileSync17 } from "node:fs";
35769
+ import { readFileSync as readFileSync18 } from "node:fs";
34833
35770
  function computeMirrorHashes(strategy, content) {
34834
35771
  const parsed = import_gray_matter9.default(content);
34835
35772
  const fm = parsed.data ?? {};
@@ -34873,7 +35810,7 @@ function relocateForSlugChange(args) {
34873
35810
  }
34874
35811
  let raw = null;
34875
35812
  try {
34876
- raw = readFileSync17(fromPath, "utf8");
35813
+ raw = readFileSync18(fromPath, "utf8");
34877
35814
  } catch (err) {
34878
35815
  const code = err?.code;
34879
35816
  if (code === "ENOENT") {
@@ -34939,12 +35876,21 @@ function relocateForSlugChange(args) {
34939
35876
  }
34940
35877
  function writeMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, suppressor, pathSlug) {
34941
35878
  const targetSlug = pathSlug ?? doc2.slug;
35879
+ const content = renderMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, targetSlug);
35880
+ if (suppressor) {
35881
+ const target = entityFilePath(workspaceSlug, strategy.type, targetSlug);
35882
+ suppressor.registerWrite(target, computeMirrorHashes(strategy, content));
35883
+ }
35884
+ writeMirrorFile(workspaceSlug, strategy.type, targetSlug, content);
35885
+ return content;
35886
+ }
35887
+ function renderMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, targetSlug) {
34942
35888
  let content = strategy.serialise(doc2);
34943
35889
  if (strategy.preserveLocalBodyOnPull) {
34944
35890
  const target = entityFilePath(workspaceSlug, strategy.type, targetSlug);
34945
35891
  let existing = null;
34946
35892
  try {
34947
- existing = readFileSync17(target, "utf8");
35893
+ existing = readFileSync18(target, "utf8");
34948
35894
  } catch (err) {
34949
35895
  if (err?.code !== "ENOENT")
34950
35896
  throw err;
@@ -34958,11 +35904,6 @@ function writeMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, suppressor,
34958
35904
  `;
34959
35905
  }
34960
35906
  }
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
35907
  return content;
34967
35908
  }
34968
35909
 
@@ -35076,7 +36017,7 @@ function stampStateFromMirror(args) {
35076
36017
  const { strategy, workspaceSlug, doc: doc2, state, priorMemoryHash } = args;
35077
36018
  const targetSlug = args.pathSlug ?? doc2.slug;
35078
36019
  const mirrorPath = entityFilePath(workspaceSlug, strategy.type, targetSlug);
35079
- const mirrorRaw = readFileSync18(mirrorPath, "utf8");
36020
+ const mirrorRaw = readFileSync19(mirrorPath, "utf8");
35080
36021
  const mirrorParsed = import_gray_matter10.default(mirrorRaw);
35081
36022
  const mirrorFm = mirrorParsed.data ?? {};
35082
36023
  const mirrorLifted = splitUserFrontmatter(strategy, mirrorFm);
@@ -35196,7 +36137,7 @@ async function pushFile(args) {
35196
36137
  const identity = args.identity ?? "auto";
35197
36138
  const fetchImpl = deps?.fetchImpl;
35198
36139
  const now = deps?.now ?? (() => new Date);
35199
- const mutex = deps?.mutex ?? new PerIdMutex;
36140
+ const mutex2 = deps?.mutex ?? new PerIdMutex;
35200
36141
  const output = deps?.output;
35201
36142
  const suppressor = deps?.suppressor;
35202
36143
  const echo = deps?.echo;
@@ -35222,14 +36163,14 @@ async function pushFile(args) {
35222
36163
  absolutePath,
35223
36164
  state,
35224
36165
  unlinkContext,
35225
- mutex,
36166
+ mutex: mutex2,
35226
36167
  fetchImpl,
35227
36168
  output
35228
36169
  });
35229
36170
  }
35230
36171
  let raw;
35231
36172
  try {
35232
- raw = readFileSync18(absolutePath, "utf8");
36173
+ raw = readFileSync19(absolutePath, "utf8");
35233
36174
  } catch (err) {
35234
36175
  if (err?.code === "ENOENT") {
35235
36176
  return runPushDelete({
@@ -35242,7 +36183,7 @@ async function pushFile(args) {
35242
36183
  absolutePath,
35243
36184
  state,
35244
36185
  unlinkContext: null,
35245
- mutex,
36186
+ mutex: mutex2,
35246
36187
  fetchImpl,
35247
36188
  output
35248
36189
  });
@@ -35304,7 +36245,7 @@ async function pushFile(args) {
35304
36245
  raw,
35305
36246
  parsed,
35306
36247
  state,
35307
- mutex,
36248
+ mutex: mutex2,
35308
36249
  fetchImpl,
35309
36250
  now,
35310
36251
  output,
@@ -35325,7 +36266,7 @@ async function pushFile(args) {
35325
36266
  parsed,
35326
36267
  stateEntry,
35327
36268
  state,
35328
- mutex,
36269
+ mutex: mutex2,
35329
36270
  fetchImpl,
35330
36271
  now,
35331
36272
  output,
@@ -35579,7 +36520,7 @@ async function runPushUpsert(args) {
35579
36520
  if (liveEntry !== snapshotEntry) {
35580
36521
  let onDisk = null;
35581
36522
  try {
35582
- onDisk = readFileSync18(args.absolutePath, "utf8");
36523
+ onDisk = readFileSync19(args.absolutePath, "utf8");
35583
36524
  } catch {
35584
36525
  onDisk = null;
35585
36526
  }
@@ -35788,6 +36729,28 @@ async function runPushUpsert(args) {
35788
36729
  base: workingState
35789
36730
  };
35790
36731
  }
36732
+ if (patchResult.reason === "permanent_name_retired") {
36733
+ if (strategy.type !== "documents" || patchResult.entityKind !== "document" || patchResult.logicalKey !== args.stateEntry.slug) {
36734
+ throw new ApiError(409, "permanent-name refusal identity did not match the document update");
36735
+ }
36736
+ requireFeedRefresh({
36737
+ workspaceSlug: args.workspaceSlug,
36738
+ kind: "document",
36739
+ logicalKey: patchResult.logicalKey,
36740
+ now: args.now
36741
+ });
36742
+ args.output?.warn(`${strategy.type}/${patchResult.logicalKey} was permanently deleted in Plinth; local content is held while the document feed is refreshed.`);
36743
+ return {
36744
+ outcome: {
36745
+ kind: "permanent_delete_blocked",
36746
+ entityType: strategy.type,
36747
+ entitySlug: patchResult.logicalKey,
36748
+ reason: "refresh_required"
36749
+ },
36750
+ state: workingState,
36751
+ base: workingState
36752
+ };
36753
+ }
35791
36754
  if (patchResult.reason === "slug_conflict") {
35792
36755
  throw new Error(`patch ${strategy.type} returned slug_conflict for a body+frontmatter PATCH (no slug sent)`);
35793
36756
  }
@@ -36019,7 +36982,7 @@ function writeCreatedIdBack(args) {
36019
36982
  if (entityFilePath(args.workspaceSlug, args.strategy.type, args.slug) !== args.absolutePath) {
36020
36983
  return;
36021
36984
  }
36022
- const readBack = args.readBack ?? ((p) => readFileSync18(p, "utf8"));
36985
+ const readBack = args.readBack ?? ((p) => readFileSync19(p, "utf8"));
36023
36986
  if (readBack(args.absolutePath) !== args.raw)
36024
36987
  return;
36025
36988
  args.suppressor?.registerWrite(args.absolutePath, computeMirrorHashes(args.strategy, write.content));
@@ -36027,6 +36990,12 @@ function writeCreatedIdBack(args) {
36027
36990
  } catch {}
36028
36991
  }
36029
36992
  async function runPushCreate(args) {
36993
+ if (args.strategy.type === "documents") {
36994
+ return withPermanentDeletionLock("document", args.slug, () => runPushCreateLocked(args));
36995
+ }
36996
+ return runPushCreateLocked(args);
36997
+ }
36998
+ async function runPushCreateLocked(args) {
36030
36999
  const strategy = args.strategy;
36031
37000
  const key = `create:${strategy.type}:${args.slug}`;
36032
37001
  return args.mutex.run(key, async () => {
@@ -36064,15 +37033,108 @@ async function runPushCreate(args) {
36064
37033
  else
36065
37034
  payload.body = normalised;
36066
37035
  }
36067
- const createResult = await createEntity({
36068
- strategy,
37036
+ const createProtocol = strategy.type === "documents" ? await prepareOrResolveGenerationCreate({
37037
+ workspaceSlug: args.workspaceSlug,
36069
37038
  baseUrl: args.baseUrl,
36070
37039
  pat: args.pat,
36071
- payload,
37040
+ kind: "document",
37041
+ logicalKey: args.slug,
37042
+ payloadDigest: canonicalPayloadDigest(payload),
37043
+ now: args.now,
36072
37044
  fetchImpl: args.fetchImpl
36073
- });
37045
+ }) : { kind: "ordinary" };
37046
+ if (createProtocol.kind === "blocked") {
37047
+ args.output?.warn(`${strategy.type}/${args.slug} is held by permanent-delete reconciliation (${createProtocol.reason}).`);
37048
+ return {
37049
+ outcome: {
37050
+ kind: "permanent_delete_blocked",
37051
+ entityType: strategy.type,
37052
+ entitySlug: args.slug,
37053
+ reason: createProtocol.reason
37054
+ },
37055
+ state: args.state,
37056
+ base: args.state
37057
+ };
37058
+ }
37059
+ if (createProtocol.kind === "finalized") {
37060
+ const parsedLive = strategy.singleResponseSchema.safeParse(createProtocol.live);
37061
+ if (!parsedLive.success) {
37062
+ throw new ApiError(200, "finalized create operation live projection did not match the entity schema");
37063
+ }
37064
+ if (parsedLive.data.slug !== args.slug) {
37065
+ throw new ApiError(200, "finalized create operation live projection had the wrong slug");
37066
+ }
37067
+ const acknowledged = computeMirrorHashes(strategy, strategy.serialise(parsedLive.data));
37068
+ const adoptedEntry = {
37069
+ id: parsedLive.data.id,
37070
+ slug: parsedLive.data.slug,
37071
+ updated_at: parsedLive.data.updated_at,
37072
+ revision: parsedLive.data.revision,
37073
+ body_hash: acknowledged.bodyHash,
37074
+ frontmatter_hash: acknowledged.frontmatterHash,
37075
+ inode: safeInode2(args.absolutePath)
37076
+ };
37077
+ const adoptedState = persistEntry(args.state, strategy.type, adoptedEntry);
37078
+ return runPushUpsert({
37079
+ echo: args.echo,
37080
+ strategy,
37081
+ workspaceSlug: args.workspaceSlug,
37082
+ baseUrl: args.baseUrl,
37083
+ pat: args.pat,
37084
+ absolutePath: args.absolutePath,
37085
+ raw: args.raw,
37086
+ parsed: args.parsed,
37087
+ stateEntry: adoptedEntry,
37088
+ state: adoptedState,
37089
+ mutex: args.mutex,
37090
+ fetchImpl: args.fetchImpl,
37091
+ now: args.now,
37092
+ output: args.output,
37093
+ quarantinedKeys: args.quarantinedKeys
37094
+ });
37095
+ }
37096
+ let createResult;
37097
+ try {
37098
+ createResult = await createEntity({
37099
+ strategy,
37100
+ baseUrl: args.baseUrl,
37101
+ pat: args.pat,
37102
+ payload,
37103
+ ...createProtocol.kind === "authorised" ? {
37104
+ retiredGeneration: createProtocol.retiredGeneration,
37105
+ operationId: createProtocol.operationId
37106
+ } : {},
37107
+ fetchImpl: args.fetchImpl
37108
+ });
37109
+ } catch (error52) {
37110
+ if (createProtocol.kind === "authorised" && (!(error52 instanceof ApiError) || error52.status === 0 || error52.status >= 500 || error52.status >= 200 && error52.status < 300)) {
37111
+ updateCreateOperation({
37112
+ workspaceSlug: args.workspaceSlug,
37113
+ kind: "document",
37114
+ logicalKey: args.slug,
37115
+ operationId: createProtocol.operationId,
37116
+ state: "transport_unknown",
37117
+ now: args.now
37118
+ });
37119
+ }
37120
+ throw error52;
37121
+ }
36074
37122
  if (createResult.ok) {
36075
37123
  const doc2 = createResult.doc;
37124
+ if (createProtocol.kind === "authorised" && doc2.slug !== args.slug) {
37125
+ throw new ApiError(200, "generation-bearing create response had the wrong slug");
37126
+ }
37127
+ if (createProtocol.kind === "authorised") {
37128
+ updateCreateOperation({
37129
+ workspaceSlug: args.workspaceSlug,
37130
+ kind: "document",
37131
+ logicalKey: args.slug,
37132
+ operationId: createProtocol.operationId,
37133
+ state: "finalized",
37134
+ rowId: doc2.id,
37135
+ now: args.now
37136
+ });
37137
+ }
36076
37138
  const createFm = args.parsed.data ?? {};
36077
37139
  const createLifted = splitUserFrontmatter(strategy, createFm);
36078
37140
  const userFm = userFrontmatter(strategy, createLifted.frontmatter);
@@ -36130,6 +37192,27 @@ async function runPushCreate(args) {
36130
37192
  base: args.state
36131
37193
  };
36132
37194
  }
37195
+ if (createResult.reason === "permanent_name_retired") {
37196
+ if (strategy.type !== "documents" || createResult.entityKind !== "document" || createResult.logicalKey !== args.slug) {
37197
+ throw new ApiError(409, "permanent-name refusal identity did not match the document create");
37198
+ }
37199
+ requireFeedRefresh({
37200
+ workspaceSlug: args.workspaceSlug,
37201
+ kind: "document",
37202
+ logicalKey: createResult.logicalKey,
37203
+ now: args.now
37204
+ });
37205
+ return {
37206
+ outcome: {
37207
+ kind: "permanent_delete_blocked",
37208
+ entityType: strategy.type,
37209
+ entitySlug: createResult.logicalKey,
37210
+ reason: "refresh_required"
37211
+ },
37212
+ state: args.state,
37213
+ base: args.state
37214
+ };
37215
+ }
36133
37216
  if (createResult.reason === "context_doc_conflict") {
36134
37217
  return reconcileContextDocConflict({
36135
37218
  echo: args.echo,
@@ -36218,7 +37301,7 @@ async function runPushDelete(args) {
36218
37301
  return args.mutex.run(id, async () => {
36219
37302
  const snapshot = getEntry(args.state, strategy.type, id) ?? null;
36220
37303
  const live = snapshot ? committedEntryIfNewer(args.workspaceSlug, strategy.type, snapshot) ?? snapshot : null;
36221
- if (existsSync4(args.absolutePath)) {
37304
+ if (existsSync5(args.absolutePath)) {
36222
37305
  args.output?.log(entityDeleteSuppressedLine({
36223
37306
  entityType: strategy.type,
36224
37307
  slug,
@@ -36310,7 +37393,7 @@ async function pushRename(args) {
36310
37393
  } = args;
36311
37394
  const fetchImpl = deps?.fetchImpl;
36312
37395
  const now = deps?.now ?? (() => new Date);
36313
- const mutex = deps?.mutex ?? new PerIdMutex;
37396
+ const mutex2 = deps?.mutex ?? new PerIdMutex;
36314
37397
  const output = deps?.output;
36315
37398
  const echo = deps?.echo;
36316
37399
  const strategy = requireStrategy(entityType);
@@ -36321,7 +37404,7 @@ async function pushRename(args) {
36321
37404
  } catch {
36322
37405
  toSlug = basename5(toPath, ".md");
36323
37406
  }
36324
- return mutex.run(id, async () => {
37407
+ return mutex2.run(id, async () => {
36325
37408
  const snapshot = getEntry(state, entityType, id);
36326
37409
  const live = snapshot ? committedEntryIfNewer(workspaceSlug, entityType, snapshot) ?? snapshot : undefined;
36327
37410
  const effectiveExpected = live?.updated_at ?? expectedUpdatedAt;
@@ -36371,7 +37454,7 @@ async function pushRename(args) {
36371
37454
  };
36372
37455
  if (!updated.body_hash || !updated.frontmatter_hash) {
36373
37456
  try {
36374
- const mirrorRaw = readFileSync18(toPath, "utf8");
37457
+ const mirrorRaw = readFileSync19(toPath, "utf8");
36375
37458
  const mirrorParsed = import_gray_matter10.default(mirrorRaw);
36376
37459
  const renameFm = mirrorParsed.data ?? {};
36377
37460
  const renameLifted = splitUserFrontmatter(strategy, renameFm);
@@ -36408,7 +37491,7 @@ async function pushRename(args) {
36408
37491
  if (patchResult.reason === "slug_conflict") {
36409
37492
  let contents2 = "";
36410
37493
  try {
36411
- contents2 = readFileSync18(toPath, "utf8");
37494
+ contents2 = readFileSync19(toPath, "utf8");
36412
37495
  } catch {}
36413
37496
  const stashPath2 = stashConflict({
36414
37497
  workspaceSlug,
@@ -36456,7 +37539,7 @@ async function pushRename(args) {
36456
37539
  }
36457
37540
  let contents = "";
36458
37541
  try {
36459
- contents = readFileSync18(toPath, "utf8");
37542
+ contents = readFileSync19(toPath, "utf8");
36460
37543
  } catch {}
36461
37544
  const stashPath = stashConflict({
36462
37545
  workspaceSlug,
@@ -36493,14 +37576,14 @@ async function pushRename(args) {
36493
37576
 
36494
37577
  // src/lib/resolve-pending.ts
36495
37578
  async function drainResolvedBatches(deps) {
36496
- const mutex = deps.mutex ?? new PerIdMutex;
37579
+ const mutex2 = deps.mutex ?? new PerIdMutex;
36497
37580
  let applied = 0;
36498
37581
  let discarded = 0;
36499
37582
  for (const batch of listPendingBatches(deps.workspaceSlug)) {
36500
37583
  if (batch.status === "pending")
36501
37584
  continue;
36502
37585
  if (batch.status === "approved") {
36503
- await applyBatch(batch, deps, mutex);
37586
+ await applyBatch(batch, deps, mutex2);
36504
37587
  deps.output?.log(`[DESTRUCTIVE_APPLIED batch=${batch.batch_id} ops=${batch.ops.length}]`);
36505
37588
  applied++;
36506
37589
  } else {
@@ -36515,7 +37598,7 @@ async function drainResolvedBatches(deps) {
36515
37598
  function opLabel(op) {
36516
37599
  return op.target === "file" ? `files/${op.path}` : `${op.entityType}/${op.slug}`;
36517
37600
  }
36518
- async function applyBatch(batch, deps, mutex) {
37601
+ async function applyBatch(batch, deps, mutex2) {
36519
37602
  for (const op of batch.ops) {
36520
37603
  try {
36521
37604
  if (op.target === "file") {
@@ -36535,7 +37618,7 @@ async function applyBatch(batch, deps, mutex) {
36535
37618
  deps: {
36536
37619
  fetchImpl: deps.fetchImpl,
36537
37620
  now: deps.now,
36538
- mutex,
37621
+ mutex: mutex2,
36539
37622
  output: deps.output,
36540
37623
  echo: deps.echo
36541
37624
  }
@@ -36589,7 +37672,7 @@ async function discardBatchOps(batch, deps) {
36589
37672
  const path5 = opPath(deps.workspaceSlug, op);
36590
37673
  let local = null;
36591
37674
  try {
36592
- local = readFileSync19(path5, "utf8");
37675
+ local = readFileSync20(path5, "utf8");
36593
37676
  } catch {
36594
37677
  local = null;
36595
37678
  }
@@ -36628,7 +37711,7 @@ async function discardFileOp(op, deps) {
36628
37711
  const abs = filesSubtreePath(deps.workspaceSlug, op.path);
36629
37712
  let local = null;
36630
37713
  try {
36631
- local = readFileSync19(abs);
37714
+ local = readFileSync20(abs);
36632
37715
  } catch {
36633
37716
  local = null;
36634
37717
  }
@@ -37597,10 +38680,10 @@ class FileDestructiveGate {
37597
38680
  }
37598
38681
 
37599
38682
  // src/lib/files/reconcile.ts
37600
- import { readFileSync as readFileSync20, statSync as statSync10 } from "node:fs";
38683
+ import { readFileSync as readFileSync21, statSync as statSync10 } from "node:fs";
37601
38684
 
37602
38685
  // src/lib/unsynced.ts
37603
- import { join as join13, relative as relative8 } from "node:path";
38686
+ import { join as join14, relative as relative8 } from "node:path";
37604
38687
  var RECORDABLE_REASONS = new Set([
37605
38688
  "rate_limit",
37606
38689
  "server",
@@ -37645,7 +38728,7 @@ function recordFileUnsynced(args) {
37645
38728
  });
37646
38729
  }
37647
38730
  async function drainUnsyncedOps(args) {
37648
- const { workspaceSlug, baseUrl, pat, getState, applyState, fetchImpl, mutex, output, echo } = args;
38731
+ const { workspaceSlug, baseUrl, pat, getState, applyState, fetchImpl, mutex: mutex2, output, echo } = args;
37649
38732
  const now = args.now ?? (() => new Date);
37650
38733
  const ops = listUnsynced(getState());
37651
38734
  let attempted = 0;
@@ -37662,7 +38745,7 @@ async function drainUnsyncedOps(args) {
37662
38745
  attempted += 1;
37663
38746
  try {
37664
38747
  if (op.target === "entity") {
37665
- const absolutePath = join13(workspaceMirrorPath(workspaceSlug), op.path);
38748
+ const absolutePath = join14(workspaceMirrorPath(workspaceSlug), op.path);
37666
38749
  const classified = classifyMirrorPath({ workspaceSlug, absolutePath });
37667
38750
  if (!classified) {
37668
38751
  clear("entity", op.path);
@@ -37679,7 +38762,7 @@ async function drainUnsyncedOps(args) {
37679
38762
  state: base2,
37680
38763
  exists: op.op === "upsert",
37681
38764
  unlinkContext: null,
37682
- deps: { fetchImpl, now, mutex, output, echo }
38765
+ deps: { fetchImpl, now, mutex: mutex2, output, echo }
37683
38766
  });
37684
38767
  applyState(result.base ?? base2, result.state);
37685
38768
  if (result.outcome.kind === "ignored" && result.outcome.reason === DELETE_DECLINED_PATH_EXISTS) {
@@ -37707,7 +38790,7 @@ async function drainUnsyncedOps(args) {
37707
38790
  deps: { fetchImpl, now, output }
37708
38791
  });
37709
38792
  };
37710
- const result = mutex ? await mutex.run(`files:${op.path}`, doFile) : await doFile();
38793
+ const result = mutex2 ? await mutex2.run(`files:${op.path}`, doFile) : await doFile();
37711
38794
  applyState(base2, result.state);
37712
38795
  }
37713
38796
  clear(op.target, op.path);
@@ -38475,9 +39558,9 @@ class NodeFsHandler {
38475
39558
  if (this.fsw.closed) {
38476
39559
  return;
38477
39560
  }
38478
- const dirname11 = sp.dirname(file2);
39561
+ const dirname12 = sp.dirname(file2);
38479
39562
  const basename7 = sp.basename(file2);
38480
- const parent = this.fsw._getWatchedDir(dirname11);
39563
+ const parent = this.fsw._getWatchedDir(dirname12);
38481
39564
  let prevStats = stats;
38482
39565
  if (parent.has(basename7))
38483
39566
  return;
@@ -38504,7 +39587,7 @@ class NodeFsHandler {
38504
39587
  prevStats = newStats2;
38505
39588
  }
38506
39589
  } catch (error52) {
38507
- this.fsw._remove(dirname11, basename7);
39590
+ this.fsw._remove(dirname12, basename7);
38508
39591
  }
38509
39592
  } else if (parent.has(basename7)) {
38510
39593
  const at = newStats.atimeMs;
@@ -39631,7 +40714,7 @@ function readBlobForPush(args) {
39631
40714
  }
39632
40715
  let bytes;
39633
40716
  try {
39634
- bytes = readFileSync20(absolutePath);
40717
+ bytes = readFileSync21(absolutePath);
39635
40718
  } catch (err) {
39636
40719
  if (err?.code === "ENOENT")
39637
40720
  return { kind: "gone" };
@@ -39941,7 +41024,7 @@ function applyArtistMemoryOnPull(args) {
39941
41024
  const cloudMemory = live.memory;
39942
41025
  const generated = live.memory_generated === true;
39943
41026
  const local = readArtistMemoryFile(workspaceSlug, slug);
39944
- const refuse = (localContent, canonical) => {
41027
+ const refuse = (localContent, canonical2) => {
39945
41028
  const refused = refuseGeneratedMemoryEdit({
39946
41029
  state,
39947
41030
  workspaceSlug,
@@ -39951,7 +41034,7 @@ function applyArtistMemoryOnPull(args) {
39951
41034
  conflictEntity: ENTITY_TYPE,
39952
41035
  conflictSlug: conflictSlug(slug),
39953
41036
  localContent,
39954
- canonicalBody: canonical,
41037
+ canonicalBody: canonical2,
39955
41038
  writeCanonical: (body) => writeMemory(workspaceSlug, slug, body, suppressor),
39956
41039
  now,
39957
41040
  output: out
@@ -40055,7 +41138,7 @@ async function pushArtistMemory(args) {
40055
41138
  id: entry2.id,
40056
41139
  fetchImpl
40057
41140
  });
40058
- const canonical = row.memory ?? "";
41141
+ const canonical2 = row.memory ?? "";
40059
41142
  const refused = refuseGeneratedMemoryEdit({
40060
41143
  state: base2,
40061
41144
  workspaceSlug,
@@ -40065,7 +41148,7 @@ async function pushArtistMemory(args) {
40065
41148
  conflictEntity: ENTITY_TYPE,
40066
41149
  conflictSlug: conflictSlug(slug),
40067
41150
  localContent: local,
40068
- canonicalBody: canonical,
41151
+ canonicalBody: canonical2,
40069
41152
  writeCanonical: (body) => writeMemory(workspaceSlug, slug, body, suppressor),
40070
41153
  now,
40071
41154
  output
@@ -40074,7 +41157,7 @@ async function pushArtistMemory(args) {
40074
41157
  ...entry2,
40075
41158
  updated_at: row.updated_at,
40076
41159
  revision: row.revision,
40077
- memory_hash: stableHash(canonical),
41160
+ memory_hash: stableHash(canonical2),
40078
41161
  memory_generated: row.memory_generated
40079
41162
  };
40080
41163
  return {
@@ -40170,7 +41253,7 @@ function adoptCloudMemory(args) {
40170
41253
  return persistEntry2(state, updated);
40171
41254
  }
40172
41255
  async function reconcileArtistMemory(args) {
40173
- const { workspaceSlug, baseUrl, pat, fetchImpl, output, suppressor, mutex } = args;
41256
+ const { workspaceSlug, baseUrl, pat, fetchImpl, output, suppressor, mutex: mutex2 } = args;
40174
41257
  const now = args.now ?? (() => new Date);
40175
41258
  let state = args.state;
40176
41259
  let transitioned = 0;
@@ -40212,7 +41295,7 @@ async function reconcileArtistMemory(args) {
40212
41295
  now,
40213
41296
  output,
40214
41297
  suppressor,
40215
- mutex
41298
+ mutex: mutex2
40216
41299
  });
40217
41300
  state = result.state;
40218
41301
  if (result.outcome.kind === "pushed")
@@ -40692,10 +41775,10 @@ function anyUnknown(state) {
40692
41775
  }
40693
41776
 
40694
41777
  // src/lib/memory/write.ts
40695
- import { readFileSync as readFileSync21, statSync as statSync11 } from "node:fs";
41778
+ import { readFileSync as readFileSync22, statSync as statSync11 } from "node:fs";
40696
41779
 
40697
41780
  // src/lib/files/extracted-text.ts
40698
- import { createHash as createHash4 } from "node:crypto";
41781
+ import { createHash as createHash5 } from "node:crypto";
40699
41782
  var DERIVED_TEXT_VERSION = 2;
40700
41783
  function yamlString3(value) {
40701
41784
  return JSON.stringify(value);
@@ -40769,7 +41852,7 @@ ${doc2.text}
40769
41852
  `;
40770
41853
  }
40771
41854
  function extractedTextHash(rendered) {
40772
- return createHash4("sha256").update(rendered, "utf8").digest("hex");
41855
+ return createHash5("sha256").update(rendered, "utf8").digest("hex");
40773
41856
  }
40774
41857
 
40775
41858
  // src/lib/memory/splice.ts
@@ -40887,7 +41970,7 @@ function writeDocumentAnnotation(args) {
40887
41970
  }
40888
41971
  let current;
40889
41972
  try {
40890
- current = readFileSync21(target, "utf8");
41973
+ current = readFileSync22(target, "utf8");
40891
41974
  } catch {
40892
41975
  return { kind: "absent" };
40893
41976
  }
@@ -42619,7 +43702,7 @@ class PullSuppressor {
42619
43702
 
42620
43703
  // src/lib/discover-entities.ts
42621
43704
  var import_gray_matter12 = __toESM(require_gray_matter(), 1);
42622
- import { readFileSync as readFileSync22, statSync as statSync12 } from "node:fs";
43705
+ import { readFileSync as readFileSync23, statSync as statSync12 } from "node:fs";
42623
43706
  var DISCOVERY_WRITE_CAP_DEFAULT = 10;
42624
43707
  function discoveryWriteCap() {
42625
43708
  const raw = process.env.PLINTH_DISCOVERY_WRITE_CAP;
@@ -42718,7 +43801,7 @@ function planDiscoveryWrites(args) {
42718
43801
  counts.discovered++;
42719
43802
  let frontmatter;
42720
43803
  try {
42721
- frontmatter = import_gray_matter12.default(readFileSync22(absolutePath, "utf8")).data ?? {};
43804
+ frontmatter = import_gray_matter12.default(readFileSync23(absolutePath, "utf8")).data ?? {};
42722
43805
  } catch {
42723
43806
  counts.discovered--;
42724
43807
  continue;
@@ -42947,7 +44030,7 @@ function recordDiscoveryFailure(args) {
42947
44030
 
42948
44031
  // src/lib/frontmatter-id-backfill.ts
42949
44032
  var import_gray_matter13 = __toESM(require_gray_matter(), 1);
42950
- import { readFileSync as readFileSync23, statSync as statSync13 } from "node:fs";
44033
+ import { readFileSync as readFileSync24, statSync as statSync13 } from "node:fs";
42951
44034
  var FRONTMATTER_ID_BACKFILL_VERSION = 1;
42952
44035
  function backfillWorthReporting(r) {
42953
44036
  return r.written > 0 || r.failed > 0 || r.skipped.foreignId > 0 || r.skipped.unwritable > 0 || r.skipped.slugCollision > 0;
@@ -42990,7 +44073,7 @@ function safeInode3(path5) {
42990
44073
  function backfillFrontmatterIds(args) {
42991
44074
  const { workspaceSlug, state } = args;
42992
44075
  const compose = args.compose ?? withFrontmatterId;
42993
- const readRaw = args.readFile ?? ((p) => readFileSync23(p, "utf8"));
44076
+ const readRaw = args.readFile ?? ((p) => readFileSync24(p, "utf8"));
42994
44077
  const result = emptyResult();
42995
44078
  if (state.frontmatter_id_backfill_version === FRONTMATTER_ID_BACKFILL_VERSION) {
42996
44079
  return result;
@@ -43118,7 +44201,7 @@ function backfillFrontmatterIds(args) {
43118
44201
  }
43119
44202
 
43120
44203
  // src/lib/reconcile.ts
43121
- import { readFileSync as readFileSync24, statSync as statSync14 } from "node:fs";
44204
+ import { readFileSync as readFileSync25, statSync as statSync14 } from "node:fs";
43122
44205
  async function reconcileDivergedRows(args) {
43123
44206
  const { workspaceSlug, baseUrl, pat, fetchImpl, output, isFrozen } = args;
43124
44207
  const now = args.now ?? (() => new Date);
@@ -43230,7 +44313,7 @@ async function reconcileDivergedRows(args) {
43230
44313
  }
43231
44314
  let content;
43232
44315
  try {
43233
- content = readFileSync24(absolutePath, "utf8");
44316
+ content = readFileSync25(absolutePath, "utf8");
43234
44317
  } catch (err) {
43235
44318
  if (err?.code === "ENOENT") {
43236
44319
  state = clearEntityCloudRowDeleted(state, strategy.mirrorRelativePath(entry2.slug));
@@ -43331,7 +44414,7 @@ async function reconcileDivergedRows(args) {
43331
44414
  }
43332
44415
 
43333
44416
  // src/lib/workspace-rules/sync.ts
43334
- import { existsSync as existsSync5 } from "node:fs";
44417
+ import { existsSync as existsSync6 } from "node:fs";
43335
44418
  import { relative as relative12 } from "node:path";
43336
44419
 
43337
44420
  // src/lib/workspace-rules/api.ts
@@ -43480,9 +44563,9 @@ function migrateHouseStyleFile(workspaceSlug) {
43480
44563
  return false;
43481
44564
  throw err;
43482
44565
  }
43483
- if (existsSync5(target))
44566
+ if (existsSync6(target))
43484
44567
  return false;
43485
- if (!existsSync5(legacy))
44568
+ if (!existsSync6(legacy))
43486
44569
  return false;
43487
44570
  return renameContextFile(workspaceSlug, HOUSE_STYLE_LEGACY_FILENAME, WORKSPACE_RULES_FILENAME);
43488
44571
  }
@@ -44580,17 +45663,18 @@ function createThrottledFetch(baseFetch, opts = {}) {
44580
45663
 
44581
45664
  // src/commands/sync.ts
44582
45665
  import {
44583
- existsSync as existsSync8,
45666
+ existsSync as existsSync9,
45667
+ lstatSync as lstatSync6,
44584
45668
  readdirSync as readdirSync7,
44585
- readFileSync as readFileSync27,
45669
+ readFileSync as readFileSync29,
44586
45670
  statSync as statSync17,
44587
- unlinkSync as unlinkSync8
45671
+ unlinkSync as unlinkSync9
44588
45672
  } from "node:fs";
44589
- import { join as join17 } from "node:path";
45673
+ import { join as join18 } from "node:path";
44590
45674
 
44591
45675
  // src/lib/conflicts-view.ts
44592
- import { existsSync as existsSync6 } from "node:fs";
44593
- import { join as join16 } from "node:path";
45676
+ import { existsSync as existsSync7 } from "node:fs";
45677
+ import { join as join17 } from "node:path";
44594
45678
  var NO_GENERATED_MEMORY = {
44595
45679
  artistMemoryGenerated: () => false,
44596
45680
  workspaceMemoryGenerated: () => false,
@@ -44636,7 +45720,7 @@ function resolveConflictRows(workspaceSlug, stashes, generatedMemory = NO_GENERA
44636
45720
  if (validateFilesRelPath(rel) === null) {
44637
45721
  return { kind: "unsafe-path", stash };
44638
45722
  }
44639
- const abs = join16(root, "files", rel);
45723
+ const abs = join17(root, "files", rel);
44640
45724
  if (hasSymlinkedComponent(workspaceSlug, abs)) {
44641
45725
  return { kind: "unsafe-path", stash };
44642
45726
  }
@@ -44644,7 +45728,7 @@ function resolveConflictRows(workspaceSlug, stashes, generatedMemory = NO_GENERA
44644
45728
  kind: "restorable",
44645
45729
  stash,
44646
45730
  mirrorPath: abs,
44647
- mirrorPathExists: existsSync6(abs)
45731
+ mirrorPathExists: existsSync7(abs)
44648
45732
  };
44649
45733
  });
44650
45734
  }
@@ -44766,9 +45850,377 @@ function refreshConflictsView(workspaceSlug) {
44766
45850
  }
44767
45851
 
44768
45852
  // src/lib/files/pull.ts
44769
- import { existsSync as existsSync7, lstatSync as lstatSync4, readFileSync as readFileSync25, statSync as statSync15 } from "node:fs";
44770
- import { dirname as dirname12 } from "node:path";
45853
+ import { existsSync as existsSync8, lstatSync as lstatSync5, readFileSync as readFileSync27, statSync as statSync15 } from "node:fs";
45854
+ import { dirname as dirname13 } from "node:path";
45855
+
45856
+ // src/lib/permanent-disposition.ts
45857
+ import { createHash as createHash6 } from "node:crypto";
45858
+ import { readFileSync as readFileSync26 } from "node:fs";
45859
+ function sha256(value) {
45860
+ return createHash6("sha256").update(value).digest("hex");
45861
+ }
45862
+ function verifyStash(path5, expectedHash) {
45863
+ const actual = sha256(readFileSync26(path5));
45864
+ if (actual !== expectedHash) {
45865
+ throw new Error(`permanent deletion stash verification failed for ${path5}`);
45866
+ }
45867
+ }
45868
+ function disposePermanentDocument(args) {
45869
+ return withPermanentDeletionLock("document", args.tombstone.logicalKey, async () => disposePermanentDocumentLocked(args));
45870
+ }
45871
+ function disposePermanentDocumentLocked(args) {
45872
+ const now = args.now ?? (() => new Date);
45873
+ const t = args.tombstone;
45874
+ recordRetirementPending({
45875
+ workspaceSlug: args.workspaceSlug,
45876
+ kind: "document",
45877
+ logicalKey: t.logicalKey,
45878
+ rowId: t.id,
45879
+ purgeGeneration: t.purgeGeneration,
45880
+ retirementSequence: t.retirementSequence,
45881
+ retiredAt: t.deletedAt
45882
+ });
45883
+ let journal = loadPermanentDeletionJournal(args.workspaceSlug);
45884
+ let record2 = findRetirement(journal, "document", t.logicalKey);
45885
+ if (record2.purge_generation !== t.purgeGeneration) {
45886
+ return { state: args.state, deleted: false };
45887
+ }
45888
+ if (args.liveOwnerId === t.id) {
45889
+ throw new Error("permanent document namespace reports the retired row as live owner");
45890
+ }
45891
+ if (record2.disposition === "complete") {
45892
+ const stale = args.liveOwnerId === null ? getEntryBySlug(args.state, args.strategy.type, t.logicalKey) : getEntry(args.state, args.strategy.type, t.id);
45893
+ const state2 = stale ? removeEntry(args.state, args.strategy.type, stale.id).state : args.state;
45894
+ return { state: state2, deleted: false, stashPath: record2.stash_path };
45895
+ }
45896
+ const retiredEntry = getEntry(args.state, args.strategy.type, t.id);
45897
+ const stateOwner = getEntryBySlug(args.state, args.strategy.type, t.logicalKey);
45898
+ const protectedOwner = args.liveOwnerId !== null && args.liveOwnerId !== t.id;
45899
+ let state = args.state;
45900
+ let deleted = false;
45901
+ let stashPath = record2.stash_path;
45902
+ let stashHash = record2.stash_hash;
45903
+ if (!protectedOwner) {
45904
+ const target = entityFilePath(args.workspaceSlug, args.strategy.type, retiredEntry?.slug ?? t.logicalKey);
45905
+ let raw;
45906
+ try {
45907
+ raw = readFileSync26(target, "utf8");
45908
+ } catch (error52) {
45909
+ if (error52?.code !== "ENOENT")
45910
+ throw error52;
45911
+ }
45912
+ if (raw !== undefined && !stashPath) {
45913
+ let matchesAcknowledged = false;
45914
+ if (retiredEntry) {
45915
+ try {
45916
+ const hashes = computeMirrorHashes(args.strategy, raw);
45917
+ matchesAcknowledged = hashes.bodyHash === retiredEntry.body_hash && hashes.frontmatterHash === retiredEntry.frontmatter_hash;
45918
+ } catch {
45919
+ matchesAcknowledged = false;
45920
+ }
45921
+ }
45922
+ if (!matchesAcknowledged) {
45923
+ const stash = stashEntityConflictOnce({
45924
+ workspaceSlug: args.workspaceSlug,
45925
+ entityType: args.strategy.type,
45926
+ entitySlug: t.logicalKey,
45927
+ timestamp: now(),
45928
+ contents: raw,
45929
+ side: "local"
45930
+ });
45931
+ stashPath = stash.path;
45932
+ stashHash = sha256(raw);
45933
+ verifyStash(stash.path, stashHash);
45934
+ recordRetirementStash({
45935
+ workspaceSlug: args.workspaceSlug,
45936
+ kind: "document",
45937
+ logicalKey: t.logicalKey,
45938
+ generation: t.purgeGeneration,
45939
+ stashPath,
45940
+ stashHash
45941
+ });
45942
+ args.output?.warn(`${args.strategy.type}/${t.logicalKey}: permanently deleted; recoverable local content was moved to ${stashPath}.`);
45943
+ }
45944
+ }
45945
+ args.suppressor?.registerDelete(target);
45946
+ deleted = deleteMirrorFile(args.workspaceSlug, args.strategy.type, retiredEntry?.slug ?? t.logicalKey);
45947
+ }
45948
+ const staleEntry = args.liveOwnerId === null ? stateOwner : retiredEntry;
45949
+ if (staleEntry)
45950
+ state = removeEntry(state, args.strategy.type, staleEntry.id).state;
45951
+ completeRetirement({
45952
+ workspaceSlug: args.workspaceSlug,
45953
+ kind: "document",
45954
+ logicalKey: t.logicalKey,
45955
+ generation: t.purgeGeneration,
45956
+ ...stashPath && stashHash ? { stashPath, stashHash } : {},
45957
+ now
45958
+ });
45959
+ return { state, deleted, ...stashPath ? { stashPath } : {} };
45960
+ }
45961
+ function disposePermanentFile(args) {
45962
+ return withPermanentDeletionLock("workspace_file", args.tombstone.logicalKey, async () => disposePermanentFileLocked(args));
45963
+ }
45964
+ function disposePermanentFileLocked(args) {
45965
+ const now = args.now ?? (() => new Date);
45966
+ const t = args.tombstone;
45967
+ recordRetirementPending({
45968
+ workspaceSlug: args.workspaceSlug,
45969
+ kind: "workspace_file",
45970
+ logicalKey: t.logicalKey,
45971
+ rowId: t.id,
45972
+ purgeGeneration: t.purgeGeneration,
45973
+ retirementSequence: t.retirementSequence,
45974
+ retiredAt: t.deletedAt
45975
+ });
45976
+ const journal = loadPermanentDeletionJournal(args.workspaceSlug);
45977
+ const record2 = findRetirement(journal, "workspace_file", t.logicalKey);
45978
+ if (record2.purge_generation !== t.purgeGeneration) {
45979
+ return { state: args.state, deleted: false, textDeleted: false };
45980
+ }
45981
+ if (args.liveOwnerId === t.id) {
45982
+ throw new Error("permanent file namespace reports the retired row as live owner");
45983
+ }
45984
+ if (record2.disposition === "complete") {
45985
+ const stale = getFileEntry(args.state, t.logicalKey);
45986
+ const state2 = stale && (args.liveOwnerId === null || stale.id === t.id) ? removeFileEntry(args.state, t.logicalKey).state : args.state;
45987
+ return {
45988
+ state: state2,
45989
+ deleted: false,
45990
+ textDeleted: false,
45991
+ ...record2.stash_path ? { stashPath: record2.stash_path } : {}
45992
+ };
45993
+ }
45994
+ const existing = getFileEntry(args.state, t.logicalKey);
45995
+ const protectedOwner = args.liveOwnerId !== null && args.liveOwnerId !== t.id;
45996
+ let state = args.state;
45997
+ let deleted = false;
45998
+ let textDeleted = false;
45999
+ let stashPath = record2.stash_path;
46000
+ let stashHash = record2.stash_hash;
46001
+ if (!protectedOwner) {
46002
+ const target = filesSubtreePath(args.workspaceSlug, t.logicalKey);
46003
+ let bytes;
46004
+ try {
46005
+ bytes = readFileSync26(target);
46006
+ } catch (error52) {
46007
+ if (error52?.code !== "ENOENT")
46008
+ throw error52;
46009
+ }
46010
+ if (bytes !== undefined && !stashPath) {
46011
+ const matchesAcknowledged = existing !== undefined && sha256Bytes(bytes) === existing.content_hash;
46012
+ if (!matchesAcknowledged) {
46013
+ const stash = stashFileConflictOnce({
46014
+ workspaceSlug: args.workspaceSlug,
46015
+ relPath: t.logicalKey,
46016
+ timestamp: now(),
46017
+ bytes,
46018
+ side: "local"
46019
+ });
46020
+ stashPath = stash.path;
46021
+ stashHash = sha256(bytes);
46022
+ verifyStash(stash.path, stashHash);
46023
+ recordRetirementStash({
46024
+ workspaceSlug: args.workspaceSlug,
46025
+ kind: "workspace_file",
46026
+ logicalKey: t.logicalKey,
46027
+ generation: t.purgeGeneration,
46028
+ stashPath,
46029
+ stashHash
46030
+ });
46031
+ args.output?.warn(`files/${t.logicalKey}: permanently deleted; recoverable local bytes were moved to ${stashPath}.`);
46032
+ }
46033
+ }
46034
+ args.suppressor?.registerDelete(target);
46035
+ deleted = deleteFilesFile(args.workspaceSlug, t.logicalKey);
46036
+ textDeleted = deleteExtractedText(args.workspaceSlug, t.logicalKey);
46037
+ }
46038
+ if (existing && (args.liveOwnerId === null || existing.id === t.id)) {
46039
+ state = removeFileEntry(state, t.logicalKey).state;
46040
+ }
46041
+ completeRetirement({
46042
+ workspaceSlug: args.workspaceSlug,
46043
+ kind: "workspace_file",
46044
+ logicalKey: t.logicalKey,
46045
+ generation: t.purgeGeneration,
46046
+ ...stashPath && stashHash ? { stashPath, stashHash } : {},
46047
+ now
46048
+ });
46049
+ return { state, deleted, textDeleted, ...stashPath ? { stashPath } : {} };
46050
+ }
46051
+
46052
+ // src/lib/files/pull.ts
46053
+ function dueFileRefreshMarkers(workspaceSlug, now) {
46054
+ try {
46055
+ return loadPermanentDeletionJournal(workspaceSlug).refresh.filter((marker) => marker.kind === "workspace_file" && (marker.retry_after === undefined || new Date(marker.retry_after).getTime() <= now.getTime()));
46056
+ } catch {
46057
+ return null;
46058
+ }
46059
+ }
46060
+ async function replayPermanentFiles(args, markers) {
46061
+ const now = args.now ?? (() => new Date);
46062
+ const rows = [];
46063
+ let cursor = null;
46064
+ let resultingCursor = args.cursor;
46065
+ while (true) {
46066
+ const page = await syncFilesPage({
46067
+ baseUrl: args.baseUrl,
46068
+ pat: args.pat,
46069
+ cursor,
46070
+ fetchImpl: args.fetchImpl
46071
+ });
46072
+ rows.push(...page.rows);
46073
+ if (page.next_cursor === null)
46074
+ break;
46075
+ resultingCursor = page.next_cursor;
46076
+ cursor = page.next_cursor;
46077
+ }
46078
+ let state = args.state;
46079
+ let written = 0;
46080
+ let deleted = 0;
46081
+ let textWritten = 0;
46082
+ let textDeleted = 0;
46083
+ let textUnknown = 0;
46084
+ let pointerRows = 0;
46085
+ let symlinkRefused = 0;
46086
+ let stashFailed = 0;
46087
+ let completePass = true;
46088
+ let lastCommittedState = args.state;
46089
+ const completed = [];
46090
+ for (const marker of markers) {
46091
+ 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");
46092
+ candidates.sort((a, b) => b.retirement_sequence - a.retirement_sequence);
46093
+ const candidate = candidates[0];
46094
+ const ambiguousLatest = candidate !== undefined && candidates.some((row) => row.retirement_sequence === candidate.retirement_sequence && row.purge_generation !== candidate.purge_generation);
46095
+ const stateBefore = state;
46096
+ const outcome = await withPermanentDeletionLock("workspace_file", marker.logical_key, async () => {
46097
+ const namespace = await getCurrentPermanentNamespace({
46098
+ baseUrl: args.baseUrl,
46099
+ pat: args.pat,
46100
+ entityKind: "workspace_file",
46101
+ logicalKey: marker.logical_key,
46102
+ fetchImpl: args.fetchImpl
46103
+ });
46104
+ if (!candidate || ambiguousLatest || namespace.latest_retirement === null || namespace.latest_retirement.purge_generation !== candidate.purge_generation || namespace.latest_retirement.retirement_sequence !== candidate.retirement_sequence) {
46105
+ markRetentionMissing({
46106
+ workspaceSlug: args.workspaceSlug,
46107
+ kind: "workspace_file",
46108
+ logicalKey: marker.logical_key,
46109
+ now
46110
+ });
46111
+ 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.`);
46112
+ return { state: stateBefore, deleted: false, textDeleted: false, complete: false };
46113
+ }
46114
+ const liveOwnerId = namespace.live_owner?.id ?? null;
46115
+ const disposition = await disposePermanentFile({
46116
+ workspaceSlug: args.workspaceSlug,
46117
+ tombstone: {
46118
+ id: candidate.id,
46119
+ logicalKey: marker.logical_key,
46120
+ deletedAt: candidate.deleted_at,
46121
+ purgeGeneration: candidate.purge_generation,
46122
+ retirementSequence: candidate.retirement_sequence
46123
+ },
46124
+ liveOwnerId,
46125
+ state: stateBefore,
46126
+ now,
46127
+ output: args.output,
46128
+ suppressor: args.suppressor
46129
+ });
46130
+ const committedState = args.commit && disposition.state !== stateBefore ? args.commit(stateBefore, disposition.state) : disposition.state;
46131
+ const current = await getCurrentPermanentNamespace({
46132
+ baseUrl: args.baseUrl,
46133
+ pat: args.pat,
46134
+ entityKind: "workspace_file",
46135
+ logicalKey: marker.logical_key,
46136
+ fetchImpl: args.fetchImpl
46137
+ });
46138
+ const complete = current.latest_retirement?.purge_generation === candidate.purge_generation && current.latest_retirement?.retirement_sequence === candidate.retirement_sequence && (current.live_owner?.id ?? null) === liveOwnerId;
46139
+ return {
46140
+ state: committedState,
46141
+ deleted: disposition.deleted,
46142
+ textDeleted: disposition.textDeleted,
46143
+ complete
46144
+ };
46145
+ });
46146
+ state = outcome.state;
46147
+ if (args.commit && state !== stateBefore)
46148
+ lastCommittedState = state;
46149
+ if (outcome.deleted)
46150
+ deleted++;
46151
+ if (outcome.textDeleted)
46152
+ textDeleted++;
46153
+ if (outcome.complete && candidate) {
46154
+ completed.push({ logicalKey: marker.logical_key, generation: candidate.purge_generation });
46155
+ } else {
46156
+ completePass = false;
46157
+ }
46158
+ }
46159
+ for (const row of rows) {
46160
+ if (row.deleted_at !== null && row.permanent_delete === true)
46161
+ continue;
46162
+ const outcome = await applyFileRow({
46163
+ workspaceSlug: args.workspaceSlug,
46164
+ baseUrl: args.baseUrl,
46165
+ pat: args.pat,
46166
+ row,
46167
+ state,
46168
+ fetchImpl: args.fetchImpl,
46169
+ now,
46170
+ output: args.output,
46171
+ suppressor: args.suppressor,
46172
+ isFileFrozen: args.isFileFrozen,
46173
+ loggedTombstoneSkips: args.loggedTombstoneSkips,
46174
+ forceTextRewalk: args.forceTextRewalk,
46175
+ loggedStashNotices: args.loggedStashNotices
46176
+ });
46177
+ state = outcome.state;
46178
+ if (outcome.action === "written")
46179
+ written++;
46180
+ else if (outcome.action === "deleted")
46181
+ deleted++;
46182
+ textWritten += outcome.textWritten ?? 0;
46183
+ textDeleted += outcome.textDeleted ?? 0;
46184
+ if (outcome.textUnknown)
46185
+ textUnknown++;
46186
+ if (outcome.pointer)
46187
+ pointerRows++;
46188
+ if (outcome.symlinkRefused) {
46189
+ symlinkRefused++;
46190
+ completePass = false;
46191
+ }
46192
+ if (outcome.stashFailed || outcome.textFailed) {
46193
+ if (outcome.stashFailed)
46194
+ stashFailed++;
46195
+ completePass = false;
46196
+ }
46197
+ }
46198
+ if (args.commit && state !== lastCommittedState)
46199
+ state = args.commit(lastCommittedState, state);
46200
+ return {
46201
+ state,
46202
+ fetched: rows.length,
46203
+ written,
46204
+ deleted,
46205
+ textWritten,
46206
+ textDeleted,
46207
+ textUnknown,
46208
+ pointerRows,
46209
+ symlinkRefused,
46210
+ stashFailed,
46211
+ lastCursor: completePass ? resultingCursor : args.cursor,
46212
+ permanentRefreshes: completed,
46213
+ permanentReplayComplete: completePass
46214
+ };
46215
+ }
44771
46216
  async function pullFiles(args) {
46217
+ const dueMarkers = dueFileRefreshMarkers(args.workspaceSlug, (args.now ?? (() => new Date))());
46218
+ if (dueMarkers && dueMarkers.length > 0)
46219
+ return replayPermanentFiles(args, dueMarkers);
46220
+ const permanentJournalBlocked = dueMarkers === null;
46221
+ if (permanentJournalBlocked) {
46222
+ 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.`);
46223
+ }
44772
46224
  const { workspaceSlug, baseUrl, pat, fetchImpl } = args;
44773
46225
  let state = args.state;
44774
46226
  let fetched = 0;
@@ -44780,15 +46232,32 @@ async function pullFiles(args) {
44780
46232
  let pointerRows = 0;
44781
46233
  let symlinkRefused = 0;
44782
46234
  let stashFailed = 0;
46235
+ let permanentEncountered = false;
44783
46236
  let cursor = args.cursor;
44784
46237
  let lastCursor = args.cursor;
44785
- let cursorBlocked = false;
46238
+ let cursorBlocked = permanentJournalBlocked;
44786
46239
  while (true) {
44787
46240
  const page = await syncFilesPage({ baseUrl, pat, cursor, fetchImpl });
44788
46241
  const pageBase = state;
44789
46242
  let pageTextFailed = false;
44790
46243
  for (const row of page.rows) {
44791
46244
  fetched++;
46245
+ if (row.deleted_at !== null && row.permanent_delete === true && row.purge_generation !== undefined) {
46246
+ if (permanentJournalBlocked) {
46247
+ pageTextFailed = true;
46248
+ continue;
46249
+ }
46250
+ requireFeedRefresh({
46251
+ workspaceSlug,
46252
+ kind: "workspace_file",
46253
+ logicalKey: row.path,
46254
+ requiredGeneration: row.purge_generation,
46255
+ now: args.now
46256
+ });
46257
+ permanentEncountered = true;
46258
+ pageTextFailed = true;
46259
+ continue;
46260
+ }
44792
46261
  const outcome = await applyFileRow({
44793
46262
  workspaceSlug,
44794
46263
  baseUrl,
@@ -44839,6 +46308,15 @@ async function pullFiles(args) {
44839
46308
  lastCursor = page.next_cursor;
44840
46309
  cursor = page.next_cursor;
44841
46310
  }
46311
+ if (permanentEncountered) {
46312
+ const markers = dueFileRefreshMarkers(workspaceSlug, (args.now ?? (() => new Date))());
46313
+ if (!markers) {
46314
+ throw new Error("permanent deletion journal became unreadable during file replay");
46315
+ }
46316
+ if (markers.length > 0) {
46317
+ return replayPermanentFiles({ ...args, state }, markers);
46318
+ }
46319
+ }
44842
46320
  if (textUnknown > 0) {
44843
46321
  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
46322
  }
@@ -44977,7 +46455,7 @@ async function applyFileRow(args) {
44977
46455
  }
44978
46456
  let localBytes;
44979
46457
  try {
44980
- localBytes = readFileSync25(abs);
46458
+ localBytes = readFileSync27(abs);
44981
46459
  } catch {
44982
46460
  localBytes = undefined;
44983
46461
  }
@@ -45023,7 +46501,7 @@ async function applyFileRow(args) {
45023
46501
  output
45024
46502
  });
45025
46503
  }
45026
- if (existing && existing.content_hash === row.content_hash && existsSync7(abs)) {
46504
+ if (existing && existing.content_hash === row.content_hash && existsSync8(abs)) {
45027
46505
  if (!args.forceTextRewalk && textIsCurrent(existing, row, workspaceSlug, relPath)) {
45028
46506
  return { state, action: "noop", textUnknown };
45029
46507
  }
@@ -45153,7 +46631,7 @@ async function applyPointerRow(args) {
45153
46631
  if (textSignal === "absent") {
45154
46632
  args.output?.warn(`files/${relPath}: Plinth holds neither bytes nor text for this file — it has no local representation in the mirror.`);
45155
46633
  }
45156
- const bytesInMirror = existsSync7(filesSubtreePath(workspaceSlug, relPath));
46634
+ const bytesInMirror = existsSync8(filesSubtreePath(workspaceSlug, relPath));
45157
46635
  if (bytesInMirror) {
45158
46636
  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
46637
  }
@@ -45179,11 +46657,11 @@ async function applyPointerRow(args) {
45179
46657
  }
45180
46658
  function stashUnpushedLocalEdit(args) {
45181
46659
  const { workspaceSlug, relPath, abs, existing, output } = args;
45182
- if (hasSymlinkedComponent(workspaceSlug, dirname12(abs)))
46660
+ if (hasSymlinkedComponent(workspaceSlug, dirname13(abs)))
45183
46661
  return { reason: "symlink" };
45184
46662
  let st;
45185
46663
  try {
45186
- st = lstatSync4(abs);
46664
+ st = lstatSync5(abs);
45187
46665
  } catch (err) {
45188
46666
  if (err?.code === "ENOENT")
45189
46667
  return null;
@@ -45193,7 +46671,7 @@ function stashUnpushedLocalEdit(args) {
45193
46671
  return { reason: "not_a_file" };
45194
46672
  let localBytes;
45195
46673
  try {
45196
- localBytes = readFileSync25(abs);
46674
+ localBytes = readFileSync27(abs);
45197
46675
  } catch (err) {
45198
46676
  return { reason: "unreadable", code: errCode(err) };
45199
46677
  }
@@ -45230,7 +46708,7 @@ function textIsCurrent(existing, row, workspaceSlug, relPath) {
45230
46708
  if (signal === "unknown")
45231
46709
  return true;
45232
46710
  if (signal === "absent") {
45233
- return existing.text_hash === undefined && !existsSync7(extractedTextPath(workspaceSlug, relPath));
46711
+ return existing.text_hash === undefined && !existsSync8(extractedTextPath(workspaceSlug, relPath));
45234
46712
  }
45235
46713
  if (existing.text_hash === undefined)
45236
46714
  return false;
@@ -45241,7 +46719,7 @@ function textIsCurrent(existing, row, workspaceSlug, relPath) {
45241
46719
  }
45242
46720
 
45243
46721
  // src/lib/files/rejection-sync.ts
45244
- import { readFileSync as readFileSync26, statSync as statSync16 } from "node:fs";
46722
+ import { readFileSync as readFileSync28, statSync as statSync16 } from "node:fs";
45245
46723
 
45246
46724
  // src/lib/files/rejections-api.ts
45247
46725
  var RejectionsFeedSchema = exports_external.object({
@@ -45374,7 +46852,7 @@ function settledDisposition(args) {
45374
46852
  return null;
45375
46853
  let bytes;
45376
46854
  try {
45377
- bytes = readFileSync26(absolute2);
46855
+ bytes = readFileSync28(absolute2);
45378
46856
  } catch {
45379
46857
  return null;
45380
46858
  }
@@ -45451,7 +46929,7 @@ function classifyApiError(err) {
45451
46929
  return { kind: "transient", status: err.status };
45452
46930
  }
45453
46931
  function hasExistingContent(workspacePath) {
45454
- if (!existsSync8(workspacePath))
46932
+ if (!existsSync9(workspacePath))
45455
46933
  return false;
45456
46934
  try {
45457
46935
  return readdirSync7(workspacePath, { withFileTypes: true }).some((e) => {
@@ -45460,7 +46938,7 @@ function hasExistingContent(workspacePath) {
45460
46938
  if (!e.isDirectory())
45461
46939
  return true;
45462
46940
  try {
45463
- return readdirSync7(join17(workspacePath, e.name)).length > 0;
46941
+ return readdirSync7(join18(workspacePath, e.name)).length > 0;
45464
46942
  } catch {
45465
46943
  return true;
45466
46944
  }
@@ -45490,7 +46968,7 @@ function unlinkIfProvablyOurs(workspaceSlug, strategy, entry2, suppressor) {
45490
46968
  }
45491
46969
  let raw;
45492
46970
  try {
45493
- raw = readFileSync27(target, "utf8");
46971
+ raw = readFileSync29(target, "utf8");
45494
46972
  } catch (err) {
45495
46973
  if (err?.code === "ENOENT") {
45496
46974
  return { didUnlink: false };
@@ -45508,7 +46986,7 @@ function unlinkIfProvablyOurs(workspaceSlug, strategy, entry2, suppressor) {
45508
46986
  }
45509
46987
  try {
45510
46988
  suppressor?.registerDelete(target);
45511
- unlinkSync8(target);
46989
+ unlinkSync9(target);
45512
46990
  return { didUnlink: true };
45513
46991
  } catch (err) {
45514
46992
  if (err?.code === "ENOENT") {
@@ -45517,6 +46995,118 @@ function unlinkIfProvablyOurs(workspaceSlug, strategy, entry2, suppressor) {
45517
46995
  throw err;
45518
46996
  }
45519
46997
  }
46998
+ function dueDocumentRefreshMarkers(workspaceSlug, now) {
46999
+ try {
47000
+ return loadPermanentDeletionJournal(workspaceSlug).refresh.filter((marker) => marker.kind === "document" && (marker.retry_after === undefined || new Date(marker.retry_after).getTime() <= now.getTime()));
47001
+ } catch {
47002
+ return null;
47003
+ }
47004
+ }
47005
+ async function replayPermanentDocuments(args) {
47006
+ const rows = [];
47007
+ let cursor = null;
47008
+ let resultingCursor = args.savedCursor;
47009
+ while (true) {
47010
+ const page = await syncEntity({
47011
+ strategy: args.strategy,
47012
+ baseUrl: args.baseUrl,
47013
+ pat: args.pat,
47014
+ cursor,
47015
+ fetchImpl: args.fetchImpl
47016
+ });
47017
+ rows.push(...page.rows);
47018
+ if (page.next_cursor === null)
47019
+ break;
47020
+ resultingCursor = page.next_cursor;
47021
+ cursor = page.next_cursor;
47022
+ }
47023
+ let state = args.state;
47024
+ const completed = [];
47025
+ let completePass = true;
47026
+ for (const marker of args.markers) {
47027
+ 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");
47028
+ candidates.sort((a, b) => b.retirement_sequence - a.retirement_sequence);
47029
+ const candidate = candidates[0];
47030
+ const ambiguousLatest = candidate !== undefined && candidates.some((row) => row.retirement_sequence === candidate.retirement_sequence && row.purge_generation !== candidate.purge_generation);
47031
+ const stateBefore = state;
47032
+ const outcome = await withPermanentDeletionLock("document", marker.logical_key, async () => {
47033
+ const namespace = await getCurrentPermanentNamespace({
47034
+ baseUrl: args.baseUrl,
47035
+ pat: args.pat,
47036
+ entityKind: "document",
47037
+ logicalKey: marker.logical_key,
47038
+ fetchImpl: args.fetchImpl
47039
+ });
47040
+ if (!candidate || ambiguousLatest || namespace.latest_retirement === null || namespace.latest_retirement.purge_generation !== candidate.purge_generation || namespace.latest_retirement.retirement_sequence !== candidate.retirement_sequence) {
47041
+ markRetentionMissing({
47042
+ workspaceSlug: args.workspaceSlug,
47043
+ kind: "document",
47044
+ logicalKey: marker.logical_key,
47045
+ now: args.now
47046
+ });
47047
+ 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.`);
47048
+ return { state: stateBefore, complete: false };
47049
+ }
47050
+ const liveOwnerId = namespace.live_owner?.id ?? null;
47051
+ const disposed = await disposePermanentDocument({
47052
+ workspaceSlug: args.workspaceSlug,
47053
+ strategy: args.strategy,
47054
+ tombstone: {
47055
+ id: candidate.id,
47056
+ logicalKey: marker.logical_key,
47057
+ deletedAt: candidate.deleted_at,
47058
+ purgeGeneration: candidate.purge_generation,
47059
+ retirementSequence: candidate.retirement_sequence
47060
+ },
47061
+ liveOwnerId,
47062
+ state: stateBefore,
47063
+ now: args.now,
47064
+ output: args.output,
47065
+ suppressor: args.suppressor
47066
+ });
47067
+ const committedState = args.commit && disposed.state !== stateBefore ? args.commit(stateBefore, disposed.state) : disposed.state;
47068
+ const current = await getCurrentPermanentNamespace({
47069
+ baseUrl: args.baseUrl,
47070
+ pat: args.pat,
47071
+ entityKind: "document",
47072
+ logicalKey: marker.logical_key,
47073
+ fetchImpl: args.fetchImpl
47074
+ });
47075
+ const complete = current.latest_retirement?.purge_generation === candidate.purge_generation && current.latest_retirement?.retirement_sequence === candidate.retirement_sequence && (current.live_owner?.id ?? null) === liveOwnerId;
47076
+ return { state: committedState, complete };
47077
+ });
47078
+ state = outcome.state;
47079
+ if (outcome.complete && candidate) {
47080
+ completed.push({ logicalKey: marker.logical_key, generation: candidate.purge_generation });
47081
+ } else {
47082
+ completePass = false;
47083
+ }
47084
+ }
47085
+ const counters = {
47086
+ fetched: rows.length,
47087
+ written: 0,
47088
+ deleted: 0,
47089
+ moved: 0,
47090
+ refused: 0
47091
+ };
47092
+ for (const row of rows) {
47093
+ if (row.deleted_at !== null && row.permanent_delete === true)
47094
+ continue;
47095
+ const outcome = applyRow(args.workspaceSlug, args.strategy, row, state, args.output, args.suppressor, args.now, args.isFrozen, args.loggedTombstoneSkips);
47096
+ state = outcome.state;
47097
+ if (outcome.action === "written")
47098
+ counters.written++;
47099
+ else if (outcome.action === "deleted")
47100
+ counters.deleted++;
47101
+ else if (outcome.action === "moved")
47102
+ counters.moved = (counters.moved ?? 0) + 1;
47103
+ else if (outcome.action === "slug_refused") {
47104
+ counters.refused = (counters.refused ?? 0) + 1;
47105
+ completePass = false;
47106
+ }
47107
+ }
47108
+ return { state, counters, cursor: resultingCursor, completed, completePass };
47109
+ }
45520
47110
  async function runSync(workspaceSlug, deps = {}) {
45521
47111
  const out = new Output({
45522
47112
  sink: deps.sink,
@@ -45640,9 +47230,55 @@ async function runSync(workspaceSlug, deps = {}) {
45640
47230
  for (const entityType of allEntityTypes()) {
45641
47231
  const strategy = requireStrategy(entityType);
45642
47232
  const startCursor = entry2.sync_cursors?.[entityType] ?? null;
47233
+ if (entityType === "documents") {
47234
+ const markers = dueDocumentRefreshMarkers(slug, now());
47235
+ if (markers && markers.length > 0) {
47236
+ const replay = await replayPermanentDocuments({
47237
+ workspaceSlug: slug,
47238
+ baseUrl: entry2.api_base_url,
47239
+ pat,
47240
+ state,
47241
+ savedCursor: startCursor,
47242
+ strategy,
47243
+ markers,
47244
+ fetchImpl,
47245
+ output: out,
47246
+ suppressor: deps.suppressor,
47247
+ now,
47248
+ isFrozen: deps.isFrozen,
47249
+ loggedTombstoneSkips: deps.loggedTombstoneSkips,
47250
+ commit: (before, after) => {
47251
+ const next = commitState(before, after);
47252
+ committed = next;
47253
+ return next;
47254
+ }
47255
+ });
47256
+ state = replay.state;
47257
+ commitPending();
47258
+ counters[entityType] = replay.counters;
47259
+ if (replay.completePass) {
47260
+ updateSyncCursor(slug, entityType, replay.cursor);
47261
+ for (const item of replay.completed) {
47262
+ clearFeedRefresh({
47263
+ workspaceSlug: slug,
47264
+ kind: "document",
47265
+ logicalKey: item.logicalKey,
47266
+ generation: item.generation
47267
+ });
47268
+ }
47269
+ }
47270
+ continue;
47271
+ }
47272
+ }
45643
47273
  let cursor = startCursor;
45644
47274
  let lastAdvancedCursor = startCursor;
45645
47275
  let cursorBlocked = false;
47276
+ let permanentEncountered = false;
47277
+ const permanentJournalBlocked = entityType === "documents" && dueDocumentRefreshMarkers(slug, now()) === null;
47278
+ if (permanentJournalBlocked) {
47279
+ cursorBlocked = true;
47280
+ 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.`);
47281
+ }
45646
47282
  const ctr = {
45647
47283
  fetched: 0,
45648
47284
  written: 0,
@@ -45660,6 +47296,22 @@ async function runSync(workspaceSlug, deps = {}) {
45660
47296
  });
45661
47297
  for (const row of page.rows) {
45662
47298
  ctr.fetched++;
47299
+ if (entityType === "documents" && row.deleted_at !== null && row.permanent_delete === true && row.purge_generation !== undefined) {
47300
+ if (permanentJournalBlocked) {
47301
+ cursorBlocked = true;
47302
+ continue;
47303
+ }
47304
+ requireFeedRefresh({
47305
+ workspaceSlug: slug,
47306
+ kind: "document",
47307
+ logicalKey: row.slug,
47308
+ requiredGeneration: row.purge_generation,
47309
+ now
47310
+ });
47311
+ permanentEncountered = true;
47312
+ cursorBlocked = true;
47313
+ continue;
47314
+ }
45663
47315
  let outcome;
45664
47316
  try {
45665
47317
  outcome = applyRow(slug, strategy, row, state, out, deps.suppressor, now, deps.isFrozen, deps.loggedTombstoneSkips);
@@ -45698,6 +47350,46 @@ async function runSync(workspaceSlug, deps = {}) {
45698
47350
  cursor = page.next_cursor;
45699
47351
  }
45700
47352
  counters[entityType] = ctr;
47353
+ if (permanentEncountered && entityType === "documents") {
47354
+ const markers = dueDocumentRefreshMarkers(slug, now());
47355
+ if (!markers || markers.length === 0)
47356
+ continue;
47357
+ const replay = await replayPermanentDocuments({
47358
+ workspaceSlug: slug,
47359
+ baseUrl: entry2.api_base_url,
47360
+ pat,
47361
+ state,
47362
+ savedCursor: startCursor,
47363
+ strategy,
47364
+ markers,
47365
+ fetchImpl,
47366
+ output: out,
47367
+ suppressor: deps.suppressor,
47368
+ now,
47369
+ isFrozen: deps.isFrozen,
47370
+ loggedTombstoneSkips: deps.loggedTombstoneSkips,
47371
+ commit: (before, after) => {
47372
+ const next = commitState(before, after);
47373
+ committed = next;
47374
+ return next;
47375
+ }
47376
+ });
47377
+ state = replay.state;
47378
+ commitPending();
47379
+ counters[entityType] = replay.counters;
47380
+ if (replay.completePass) {
47381
+ updateSyncCursor(slug, entityType, replay.cursor);
47382
+ for (const item of replay.completed) {
47383
+ clearFeedRefresh({
47384
+ workspaceSlug: slug,
47385
+ kind: "document",
47386
+ logicalKey: item.logicalKey,
47387
+ generation: item.generation
47388
+ });
47389
+ }
47390
+ }
47391
+ continue;
47392
+ }
45701
47393
  if (lastAdvancedCursor !== startCursor) {
45702
47394
  updateSyncCursor(slug, entityType, lastAdvancedCursor);
45703
47395
  }
@@ -45738,6 +47430,16 @@ async function runSync(workspaceSlug, deps = {}) {
45738
47430
  if (filesResult.lastCursor !== filesStartCursor) {
45739
47431
  updateSyncCursor(slug, "files", filesResult.lastCursor);
45740
47432
  }
47433
+ if (filesResult.permanentReplayComplete) {
47434
+ for (const item of filesResult.permanentRefreshes ?? []) {
47435
+ clearFeedRefresh({
47436
+ workspaceSlug: slug,
47437
+ kind: "workspace_file",
47438
+ logicalKey: item.logicalKey,
47439
+ generation: item.generation
47440
+ });
47441
+ }
47442
+ }
45741
47443
  const rulesResult = await pullWorkspaceRules({
45742
47444
  workspaceSlug: slug,
45743
47445
  baseUrl: entry2.api_base_url,
@@ -45969,7 +47671,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
45969
47671
  if (!entry2) {
45970
47672
  let present2 = false;
45971
47673
  try {
45972
- present2 = existsSync8(entityFilePath(workspaceSlug, strategy.type, row.slug));
47674
+ present2 = existsSync9(entityFilePath(workspaceSlug, strategy.type, row.slug));
45973
47675
  } catch (err) {
45974
47676
  if (!(err instanceof MirrorPathError))
45975
47677
  throw err;
@@ -46048,7 +47750,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
46048
47750
  const writeSlug = relocation.writeSlug;
46049
47751
  if (existingEntry && existingEntry.updated_at === live.updated_at && relocation.kind === "not_needed") {
46050
47752
  const path5 = entityFilePath(workspaceSlug, strategy.type, live.slug);
46051
- if (existsSync8(path5)) {
47753
+ if (existsSync9(path5)) {
46052
47754
  if (strategy.type === "artists") {
46053
47755
  const artistLive = live;
46054
47756
  state = applyArtistMemoryOnPull({
@@ -46075,7 +47777,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
46075
47777
  if (!suppressor && existingEntry && existingEntry.updated_at !== live.updated_at && relocation.kind !== "moved") {
46076
47778
  let localRaw = null;
46077
47779
  try {
46078
- localRaw = readFileSync27(entityFilePath(workspaceSlug, strategy.type, writeSlug), "utf8");
47780
+ localRaw = readFileSync29(entityFilePath(workspaceSlug, strategy.type, writeSlug), "utf8");
46079
47781
  } catch {
46080
47782
  localRaw = null;
46081
47783
  }
@@ -46095,6 +47797,38 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
46095
47797
  }
46096
47798
  }
46097
47799
  }
47800
+ if (!suppressor && !existingEntry) {
47801
+ const target = entityFilePath(workspaceSlug, strategy.type, writeSlug);
47802
+ let localRaw = null;
47803
+ try {
47804
+ localRaw = lstatSync6(target).isFile() ? readFileSync29(target, "utf8") : null;
47805
+ } catch {
47806
+ localRaw = null;
47807
+ }
47808
+ if (localRaw !== null) {
47809
+ const hashStrategy = strategy;
47810
+ const incomingHashes = computeMirrorHashes(hashStrategy, renderMirrorWithBodyPreserve(workspaceSlug, strategy, live, writeSlug));
47811
+ let diverged = true;
47812
+ try {
47813
+ const localHashes = computeMirrorHashes(hashStrategy, localRaw);
47814
+ diverged = localHashes.bodyHash !== incomingHashes.bodyHash || localHashes.frontmatterHash !== incomingHashes.frontmatterHash;
47815
+ } catch {
47816
+ diverged = true;
47817
+ }
47818
+ if (diverged) {
47819
+ const stash = stashEntityConflictOnce({
47820
+ workspaceSlug,
47821
+ entityType: strategy.type,
47822
+ entitySlug: live.slug,
47823
+ timestamp: now(),
47824
+ contents: localRaw,
47825
+ side: "local"
47826
+ });
47827
+ const where = stash.reused ? `already stashed at` : `stashed it to`;
47828
+ 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).`);
47829
+ }
47830
+ }
47831
+ }
46098
47832
  const content = writeMirrorWithBodyPreserve(workspaceSlug, strategy, live, suppressor, writeSlug);
46099
47833
  const hashes = computeMirrorHashes(strategy, content);
46100
47834
  let inode;
@@ -46233,7 +47967,7 @@ function backfillStateInodes(workspaceSlug, state, statFn = (p) => statSync18(p)
46233
47967
  return { state, changed: false };
46234
47968
  return { state: { ...state, entries: nextEntries }, changed: true };
46235
47969
  }
46236
- function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync28(p, "utf8")) {
47970
+ function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync30(p, "utf8")) {
46237
47971
  let changed = false;
46238
47972
  const nextEntries = {};
46239
47973
  for (const [entityType, bucket] of Object.entries(state.entries)) {
@@ -46264,7 +47998,7 @@ function makeWatchHandler(deps) {
46264
47998
  baseUrl,
46265
47999
  pat,
46266
48000
  spinner,
46267
- mutex,
48001
+ mutex: mutex2,
46268
48002
  getState,
46269
48003
  applyState,
46270
48004
  fetchImpl,
@@ -46308,7 +48042,7 @@ function makeWatchHandler(deps) {
46308
48042
  output?.log(fileDeleteSuppressedLine({ path: relPath, reason: "pull_echo" }));
46309
48043
  return;
46310
48044
  }
46311
- if (existsSync9(absolutePath)) {
48045
+ if (existsSync10(absolutePath)) {
46312
48046
  output?.log(fileDeleteSuppressedLine({ path: relPath, reason: "path_exists" }));
46313
48047
  return;
46314
48048
  }
@@ -46320,7 +48054,7 @@ function makeWatchHandler(deps) {
46320
48054
  return;
46321
48055
  }
46322
48056
  }
46323
- await mutex.run(`files:${relPath}`, async () => {
48057
+ await mutex2.run(`files:${relPath}`, async () => {
46324
48058
  const base2 = getState();
46325
48059
  const result = await pushFilesDelete({
46326
48060
  workspaceSlug,
@@ -46374,7 +48108,7 @@ function makeWatchHandler(deps) {
46374
48108
  spinner.update(`Watching ${workspaceSlug} · ${watchingSummary(getState())}`);
46375
48109
  return;
46376
48110
  }
46377
- const bytes = readFileSync28(absolutePath);
48111
+ const bytes = readFileSync30(absolutePath);
46378
48112
  const hash2 = sha256Bytes(bytes);
46379
48113
  if (suppressor?.shouldSuppressUpsert(absolutePath, {
46380
48114
  bodyHash: hash2,
@@ -46391,7 +48125,7 @@ function makeWatchHandler(deps) {
46391
48125
  return;
46392
48126
  }
46393
48127
  }
46394
- await mutex.run(`files:${relPath}`, async () => {
48128
+ await mutex2.run(`files:${relPath}`, async () => {
46395
48129
  const base2 = getState();
46396
48130
  const result = await pushFilesUpsert({
46397
48131
  workspaceSlug,
@@ -46438,7 +48172,7 @@ function makeWatchHandler(deps) {
46438
48172
  if (suppressor) {
46439
48173
  let raw = null;
46440
48174
  try {
46441
- raw = readFileSync28(absolutePath, "utf8");
48175
+ raw = readFileSync30(absolutePath, "utf8");
46442
48176
  } catch {
46443
48177
  raw = null;
46444
48178
  }
@@ -46450,7 +48184,7 @@ function makeWatchHandler(deps) {
46450
48184
  }
46451
48185
  }
46452
48186
  try {
46453
- await mutex.run(WORKSPACE_RULES_MUTEX_KEY, async () => {
48187
+ await mutex2.run(WORKSPACE_RULES_MUTEX_KEY, async () => {
46454
48188
  const base2 = getState();
46455
48189
  const result = await pushWorkspaceRules({
46456
48190
  workspaceSlug,
@@ -46477,7 +48211,7 @@ function makeWatchHandler(deps) {
46477
48211
  if (suppressor) {
46478
48212
  let raw = null;
46479
48213
  try {
46480
- raw = readFileSync28(absolutePath, "utf8");
48214
+ raw = readFileSync30(absolutePath, "utf8");
46481
48215
  } catch {
46482
48216
  raw = null;
46483
48217
  }
@@ -46489,7 +48223,7 @@ function makeWatchHandler(deps) {
46489
48223
  }
46490
48224
  }
46491
48225
  try {
46492
- await mutex.run(WORKSPACE_MEMORY_MUTEX_KEY, async () => {
48226
+ await mutex2.run(WORKSPACE_MEMORY_MUTEX_KEY, async () => {
46493
48227
  const base2 = getState();
46494
48228
  const result = await pushWorkspaceMemory({
46495
48229
  workspaceSlug,
@@ -46516,7 +48250,7 @@ function makeWatchHandler(deps) {
46516
48250
  if (suppressor) {
46517
48251
  let raw = null;
46518
48252
  try {
46519
- raw = readFileSync28(absolutePath, "utf8");
48253
+ raw = readFileSync30(absolutePath, "utf8");
46520
48254
  } catch {
46521
48255
  raw = null;
46522
48256
  }
@@ -46528,7 +48262,7 @@ function makeWatchHandler(deps) {
46528
48262
  }
46529
48263
  }
46530
48264
  try {
46531
- await mutex.run(USER_MEMORY_MUTEX_KEY, async () => {
48265
+ await mutex2.run(USER_MEMORY_MUTEX_KEY, async () => {
46532
48266
  const base2 = getState();
46533
48267
  const result = await pushUserMemory({
46534
48268
  workspaceSlug,
@@ -46555,7 +48289,7 @@ function makeWatchHandler(deps) {
46555
48289
  if (suppressor) {
46556
48290
  let raw = null;
46557
48291
  try {
46558
- raw = readFileSync28(absolutePath, "utf8");
48292
+ raw = readFileSync30(absolutePath, "utf8");
46559
48293
  } catch {
46560
48294
  raw = null;
46561
48295
  }
@@ -46567,7 +48301,7 @@ function makeWatchHandler(deps) {
46567
48301
  }
46568
48302
  }
46569
48303
  try {
46570
- await mutex.run(VOICE_MUTEX_KEY, async () => {
48304
+ await mutex2.run(VOICE_MUTEX_KEY, async () => {
46571
48305
  const base2 = getState();
46572
48306
  const result = await pushVoice({
46573
48307
  workspaceSlug,
@@ -46594,7 +48328,7 @@ function makeWatchHandler(deps) {
46594
48328
  if (suppressor) {
46595
48329
  let raw = null;
46596
48330
  try {
46597
- raw = readFileSync28(absolutePath, "utf8");
48331
+ raw = readFileSync30(absolutePath, "utf8");
46598
48332
  } catch {
46599
48333
  raw = null;
46600
48334
  }
@@ -46617,7 +48351,7 @@ function makeWatchHandler(deps) {
46617
48351
  now,
46618
48352
  output,
46619
48353
  suppressor,
46620
- mutex
48354
+ mutex: mutex2
46621
48355
  });
46622
48356
  applyState(base2, result.state);
46623
48357
  reportArtistMemoryOutcome(spinner, workspaceSlug, result.state, result.outcome);
@@ -46640,7 +48374,7 @@ function makeWatchHandler(deps) {
46640
48374
  const strategy = getStrategy(classifiedFrom.entityType);
46641
48375
  let toRaw = null;
46642
48376
  try {
46643
- toRaw = readFileSync28(event.toPath, "utf8");
48377
+ toRaw = readFileSync30(event.toPath, "utf8");
46644
48378
  } catch {
46645
48379
  toRaw = null;
46646
48380
  }
@@ -46667,7 +48401,7 @@ function makeWatchHandler(deps) {
46667
48401
  deps: {
46668
48402
  fetchImpl,
46669
48403
  now,
46670
- mutex,
48404
+ mutex: mutex2,
46671
48405
  output,
46672
48406
  echo: suppressor ? { watcher: "live", suppressor } : undefined
46673
48407
  }
@@ -46709,7 +48443,7 @@ function makeWatchHandler(deps) {
46709
48443
  if (event.kind === "upsert") {
46710
48444
  try {
46711
48445
  ownEcho = suppressor?.shouldSuppressUpsert(absolutePath, {
46712
- bodyHash: stableHash(readFileSync28(absolutePath, "utf8")),
48446
+ bodyHash: stableHash(readFileSync30(absolutePath, "utf8")),
46713
48447
  frontmatterHash: ""
46714
48448
  }) === true;
46715
48449
  } catch {
@@ -46764,7 +48498,7 @@ function makeWatchHandler(deps) {
46764
48498
  if (strategy) {
46765
48499
  let raw = null;
46766
48500
  try {
46767
- raw = readFileSync28(absolutePath, "utf8");
48501
+ raw = readFileSync30(absolutePath, "utf8");
46768
48502
  } catch {
46769
48503
  raw = null;
46770
48504
  }
@@ -46774,7 +48508,7 @@ function makeWatchHandler(deps) {
46774
48508
  }
46775
48509
  }
46776
48510
  }
46777
- if (event.kind === "unlink" && existsSync9(absolutePath)) {
48511
+ if (event.kind === "unlink" && existsSync10(absolutePath)) {
46778
48512
  output?.log(entityDeleteSuppressedLine({
46779
48513
  entityType: classified.entityType,
46780
48514
  slug: classified.entitySlug,
@@ -46813,7 +48547,7 @@ function makeWatchHandler(deps) {
46813
48547
  deps: {
46814
48548
  fetchImpl,
46815
48549
  now,
46816
- mutex,
48550
+ mutex: mutex2,
46817
48551
  output,
46818
48552
  suppressor,
46819
48553
  echo: suppressor ? { watcher: "live", suppressor } : undefined
@@ -47235,7 +48969,7 @@ async function runStart(workspaceSlug, deps = {}) {
47235
48969
  }
47236
48970
  const spinner = (deps.spinnerFactory ?? (() => createSpinner({ noColor: deps.noColor })))();
47237
48971
  spinner.start(`Watching ${slug} · ${watchingSummary(state)}`);
47238
- const mutex = new PerIdMutex;
48972
+ const mutex2 = new PerIdMutex;
47239
48973
  const bootDrain = await drainResolvedBatches({
47240
48974
  echo: { watcher: "none" },
47241
48975
  workspaceSlug: slug,
@@ -47247,7 +48981,7 @@ async function runStart(workspaceSlug, deps = {}) {
47247
48981
  },
47248
48982
  fetchImpl,
47249
48983
  now: deps.now,
47250
- mutex,
48984
+ mutex: mutex2,
47251
48985
  output: out
47252
48986
  });
47253
48987
  if (bootDrain.applied > 0 || bootDrain.discarded > 0) {
@@ -47265,7 +48999,7 @@ async function runStart(workspaceSlug, deps = {}) {
47265
48999
  },
47266
49000
  fetchImpl,
47267
49001
  now: deps.now,
47268
- mutex,
49002
+ mutex: mutex2,
47269
49003
  output: out
47270
49004
  });
47271
49005
  if (bootUnsynced.attempted > 0) {
@@ -47317,7 +49051,7 @@ async function runStart(workspaceSlug, deps = {}) {
47317
49051
  applyState: (base2, next) => {
47318
49052
  state = rebaseState(state, base2, next);
47319
49053
  },
47320
- mutex,
49054
+ mutex: mutex2,
47321
49055
  fetchImpl,
47322
49056
  now: deps.now,
47323
49057
  output: out,
@@ -47381,7 +49115,7 @@ async function runStart(workspaceSlug, deps = {}) {
47381
49115
  applyState: (base2, next) => {
47382
49116
  state = rebaseState(state, base2, next);
47383
49117
  },
47384
- mutex,
49118
+ mutex: mutex2,
47385
49119
  fetchImpl,
47386
49120
  now: deps.now,
47387
49121
  output: out,
@@ -47433,7 +49167,7 @@ async function runStart(workspaceSlug, deps = {}) {
47433
49167
  baseUrl: entry2.api_base_url,
47434
49168
  pat,
47435
49169
  spinner,
47436
- mutex,
49170
+ mutex: mutex2,
47437
49171
  getState: () => state,
47438
49172
  applyState: (base2, next) => {
47439
49173
  state = rebaseState(state, base2, next);
@@ -47497,7 +49231,7 @@ async function runStart(workspaceSlug, deps = {}) {
47497
49231
  workspaceSlug: slug,
47498
49232
  baseUrl: entry2.api_base_url,
47499
49233
  pat,
47500
- mutex,
49234
+ mutex: mutex2,
47501
49235
  commitState,
47502
49236
  getState: () => state,
47503
49237
  setState: setDaemonState,
@@ -47525,7 +49259,7 @@ async function runStart(workspaceSlug, deps = {}) {
47525
49259
  },
47526
49260
  fetchImpl,
47527
49261
  now: deps.now,
47528
- mutex,
49262
+ mutex: mutex2,
47529
49263
  output: out
47530
49264
  });
47531
49265
  if (drained.applied > 0 || drained.discarded > 0) {
@@ -47543,7 +49277,7 @@ async function runStart(workspaceSlug, deps = {}) {
47543
49277
  },
47544
49278
  fetchImpl,
47545
49279
  now: deps.now,
47546
- mutex,
49280
+ mutex: mutex2,
47547
49281
  output: out
47548
49282
  });
47549
49283
  if (redriven.resolved > 0) {
@@ -47555,7 +49289,7 @@ async function runStart(workspaceSlug, deps = {}) {
47555
49289
  spinner.update(`Watching ${slug} · ${watchingSummary(state)}`);
47556
49290
  }
47557
49291
  let rulesBase = state;
47558
- const rulesRecon = await mutex.run(WORKSPACE_RULES_MUTEX_KEY, () => {
49292
+ const rulesRecon = await mutex2.run(WORKSPACE_RULES_MUTEX_KEY, () => {
47559
49293
  rulesBase = state;
47560
49294
  return reconcileWorkspaceRules({
47561
49295
  workspaceSlug: slug,
@@ -47571,7 +49305,7 @@ async function runStart(workspaceSlug, deps = {}) {
47571
49305
  state = rebaseState(state, rulesBase, rulesRecon.state);
47572
49306
  reportSingletonReconcile(spinner, slug, state, WORKSPACE_RULES_FILENAME, rulesRecon.outcome);
47573
49307
  let memoryBase = state;
47574
- const memoryRecon = await mutex.run(WORKSPACE_MEMORY_MUTEX_KEY, () => {
49308
+ const memoryRecon = await mutex2.run(WORKSPACE_MEMORY_MUTEX_KEY, () => {
47575
49309
  memoryBase = state;
47576
49310
  return reconcileWorkspaceMemory({
47577
49311
  workspaceSlug: slug,
@@ -47587,7 +49321,7 @@ async function runStart(workspaceSlug, deps = {}) {
47587
49321
  state = rebaseState(state, memoryBase, memoryRecon.state);
47588
49322
  reportSingletonReconcile(spinner, slug, state, WORKSPACE_MEMORY_FILENAME, memoryRecon.outcome);
47589
49323
  let userMemoryBase = state;
47590
- const userMemoryRecon = await mutex.run(USER_MEMORY_MUTEX_KEY, () => {
49324
+ const userMemoryRecon = await mutex2.run(USER_MEMORY_MUTEX_KEY, () => {
47591
49325
  userMemoryBase = state;
47592
49326
  return reconcileUserMemory({
47593
49327
  workspaceSlug: slug,
@@ -47604,7 +49338,7 @@ async function runStart(workspaceSlug, deps = {}) {
47604
49338
  reportSingletonReconcile(spinner, slug, state, USER_MEMORY_FILENAME, userMemoryRecon.outcome);
47605
49339
  try {
47606
49340
  let voiceBase = state;
47607
- const voiceRecon = await mutex.run(VOICE_MUTEX_KEY, () => {
49341
+ const voiceRecon = await mutex2.run(VOICE_MUTEX_KEY, () => {
47608
49342
  voiceBase = state;
47609
49343
  return reconcileVoice({
47610
49344
  workspaceSlug: slug,
@@ -47635,7 +49369,7 @@ async function runStart(workspaceSlug, deps = {}) {
47635
49369
  now: deps.now,
47636
49370
  output: out,
47637
49371
  suppressor,
47638
- mutex
49372
+ mutex: mutex2
47639
49373
  });
47640
49374
  state = rebaseState(state, memBase, memRecon.state);
47641
49375
  if (memRecon.pushed > 0) {
@@ -48291,12 +50025,12 @@ function assessContextWalls(workspaceSlug, deps = {}) {
48291
50025
 
48292
50026
  // src/lib/build-stamp.ts
48293
50027
  import { execFileSync as execFileSync4 } from "node:child_process";
48294
- import { existsSync as existsSync10, mkdirSync as mkdirSync11, readFileSync as readFileSync29, writeFileSync as writeFileSync8 } from "node:fs";
48295
- import { dirname as dirname13, join as join18 } from "node:path";
50028
+ import { existsSync as existsSync11, mkdirSync as mkdirSync12, readFileSync as readFileSync31, writeFileSync as writeFileSync8 } from "node:fs";
50029
+ import { dirname as dirname14, join as join19 } from "node:path";
48296
50030
  import { fileURLToPath } from "node:url";
48297
50031
  var BUILD_STAMP_FILENAME = "build-stamp.json";
48298
50032
  function stampPath(distDir) {
48299
- return join18(distDir, BUILD_STAMP_FILENAME);
50033
+ return join19(distDir, BUILD_STAMP_FILENAME);
48300
50034
  }
48301
50035
  function readHead(repoDir) {
48302
50036
  try {
@@ -48315,7 +50049,7 @@ function readHead(repoDir) {
48315
50049
  }
48316
50050
  function readBuildStamp(distDir) {
48317
50051
  try {
48318
- const parsed = JSON.parse(readFileSync29(stampPath(distDir), "utf8"));
50052
+ const parsed = JSON.parse(readFileSync31(stampPath(distDir), "utf8"));
48319
50053
  if (parsed && typeof parsed.sha === "string" && parsed.sha.length > 0 && typeof parsed.dirty === "boolean" && typeof parsed.built_at === "string") {
48320
50054
  return { sha: parsed.sha, dirty: parsed.dirty, built_at: parsed.built_at };
48321
50055
  }
@@ -48327,9 +50061,9 @@ function readBuildStamp(distDir) {
48327
50061
  function findRepoRoot(fromDir) {
48328
50062
  let dir = fromDir;
48329
50063
  for (;; ) {
48330
- if (existsSync10(join18(dir, ".git")))
50064
+ if (existsSync11(join19(dir, ".git")))
48331
50065
  return dir;
48332
- const parent = dirname13(dir);
50066
+ const parent = dirname14(dir);
48333
50067
  if (parent === dir)
48334
50068
  return null;
48335
50069
  dir = parent;
@@ -48353,7 +50087,7 @@ function checkBuildStaleness(deps = {}) {
48353
50087
  const metaUrl = deps.metaUrl ?? import.meta.url;
48354
50088
  let distDir;
48355
50089
  try {
48356
- distDir = dirname13(fileURLToPath(metaUrl));
50090
+ distDir = dirname14(fileURLToPath(metaUrl));
48357
50091
  } catch {
48358
50092
  return { kind: "unknown" };
48359
50093
  }
@@ -48376,12 +50110,12 @@ function checkBuildStaleness(deps = {}) {
48376
50110
  }
48377
50111
 
48378
50112
  // src/lib/memory-links.ts
48379
- import { readFileSync as readFileSync30 } from "node:fs";
50113
+ import { readFileSync as readFileSync32 } from "node:fs";
48380
50114
  import { relative as relative15, sep as sep8 } from "node:path";
48381
50115
  var MEMORY_FILE_RE = /^(workspace-memory\.md|user-memory\.md|artists\/[^/]+\/memory\.md|memory\/[^/]+\.md|memory\/private\/[^/]+\.md)$/i;
48382
50116
  function scanMemoryLinks(deps) {
48383
50117
  const listFiles = deps.listFiles ?? ((r) => walkSubtreeFiles(r, isVcsInternal));
48384
- const readFile = deps.readFile ?? ((p) => readFileSync30(p, "utf8"));
50118
+ const readFile = deps.readFile ?? ((p) => readFileSync32(p, "utf8"));
48385
50119
  let root;
48386
50120
  let absPaths;
48387
50121
  try {
@@ -48425,7 +50159,7 @@ function scanMemoryLinks(deps) {
48425
50159
  init_output();
48426
50160
 
48427
50161
  // src/lib/pending-writes.ts
48428
- import { readFileSync as readFileSync31, statSync as statSync19 } from "node:fs";
50162
+ import { readFileSync as readFileSync33, statSync as statSync19 } from "node:fs";
48429
50163
  import { relative as relative16 } from "node:path";
48430
50164
  var import_gray_matter15 = __toESM(require_gray_matter(), 1);
48431
50165
  var EMPTY3 = { writes: [], count: 0 };
@@ -48473,7 +50207,7 @@ function scanPendingWrites(args) {
48473
50207
  continue;
48474
50208
  let hash2;
48475
50209
  try {
48476
- hash2 = sha256Bytes(readFileSync31(absolutePath));
50210
+ hash2 = sha256Bytes(readFileSync33(absolutePath));
48477
50211
  } catch {
48478
50212
  continue;
48479
50213
  }
@@ -48504,7 +50238,7 @@ function scanPendingWrites(args) {
48504
50238
  continue;
48505
50239
  let content;
48506
50240
  try {
48507
- content = readFileSync31(absolutePath, "utf8");
50241
+ content = readFileSync33(absolutePath, "utf8");
48508
50242
  } catch {
48509
50243
  continue;
48510
50244
  }
@@ -48834,6 +50568,33 @@ async function runStatus(args, deps = {}) {
48834
50568
  }
48835
50569
  }
48836
50570
  out.info("");
50571
+ let deletionJournal = null;
50572
+ let deletionJournalError = null;
50573
+ try {
50574
+ deletionJournal = loadPermanentDeletionJournal(slug);
50575
+ } catch (error52) {
50576
+ deletionJournalError = error52 instanceof Error ? error52.message : String(error52);
50577
+ }
50578
+ if (deletionJournalError) {
50579
+ out.info(row("Deletion", "BLOCKED — permanent deletion journal needs recovery."));
50580
+ out.info(cont(`${permanentDeletionJournalPath(slug)}: ${deletionJournalError}`));
50581
+ } else {
50582
+ const pending2 = deletionJournal.records.filter((record2) => record2.disposition === "pending");
50583
+ const refresh = deletionJournal.refresh;
50584
+ const operations = deletionJournal.records.filter((record2) => record2.operation?.state === "pending" || record2.operation?.state === "transport_unknown");
50585
+ if (pending2.length === 0 && refresh.length === 0 && operations.length === 0) {
50586
+ out.info(row("Deletion", "No permanent deletion reconciliation pending."));
50587
+ } else {
50588
+ 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.`));
50589
+ for (const marker of refresh.slice(0, 5)) {
50590
+ out.info(cont(`${marker.kind}:${marker.logical_key} — ${marker.state}` + (marker.retry_after ? `, retry after ${marker.retry_after}` : "")));
50591
+ }
50592
+ for (const record2 of operations.slice(0, 5)) {
50593
+ out.info(cont(`${record2.kind}:${record2.logical_key} — create ${record2.operation.state}`));
50594
+ }
50595
+ }
50596
+ }
50597
+ out.info("");
48837
50598
  const held = listPendingBatches(slug).filter((b) => b.status === "pending");
48838
50599
  if (held.length === 0) {
48839
50600
  out.info(row("Pending", "No destructive batches held."));
@@ -49388,7 +51149,7 @@ var versionCommand = defineCommand({
49388
51149
  });
49389
51150
 
49390
51151
  // src/commands/voice-gate.ts
49391
- import { readFileSync as readFileSync34 } from "node:fs";
51152
+ import { readFileSync as readFileSync36 } from "node:fs";
49392
51153
 
49393
51154
  // src/lib/voice-gate/api.ts
49394
51155
  var VoiceGateSurfaces = [
@@ -49490,15 +51251,15 @@ async function callVoiceGate(req) {
49490
51251
  }
49491
51252
 
49492
51253
  // src/lib/voice-gate/attempts.ts
49493
- import { createHash as createHash5 } from "node:crypto";
51254
+ import { createHash as createHash7 } from "node:crypto";
49494
51255
  import {
49495
- mkdirSync as mkdirSync12,
49496
- readFileSync as readFileSync32,
49497
- renameSync as renameSync8,
49498
- unlinkSync as unlinkSync9,
51256
+ mkdirSync as mkdirSync13,
51257
+ readFileSync as readFileSync34,
51258
+ renameSync as renameSync9,
51259
+ unlinkSync as unlinkSync10,
49499
51260
  writeFileSync as writeFileSync9
49500
51261
  } from "node:fs";
49501
- import { join as join19 } from "node:path";
51262
+ import { join as join20 } from "node:path";
49502
51263
  var MAX_REGENS = 2;
49503
51264
  var ENTRY_TTL_MS = 24 * 60 * 60 * 1000;
49504
51265
  var AttemptEntrySchema = exports_external.object({
@@ -49511,20 +51272,20 @@ var AttemptsFileSchema = exports_external.object({
49511
51272
  entries: exports_external.record(exports_external.string(), AttemptEntrySchema)
49512
51273
  });
49513
51274
  function attemptsDir(workspaceSlug) {
49514
- return join19(plinthHome(), "_voice_gate", workspaceSlug);
51275
+ return join20(plinthHome(), "_voice_gate", workspaceSlug);
49515
51276
  }
49516
51277
  function attemptsPath(workspaceSlug) {
49517
- return join19(attemptsDir(workspaceSlug), "attempts.json");
51278
+ return join20(attemptsDir(workspaceSlug), "attempts.json");
49518
51279
  }
49519
51280
  function attemptKey(identity) {
49520
- return createHash5("sha256").update(identity, "utf8").digest("hex").slice(0, 32);
51281
+ return createHash7("sha256").update(identity, "utf8").digest("hex").slice(0, 32);
49521
51282
  }
49522
51283
  function emptyFile() {
49523
51284
  return { version: 1, entries: {} };
49524
51285
  }
49525
51286
  function load2(workspaceSlug) {
49526
51287
  try {
49527
- const raw = readFileSync32(attemptsPath(workspaceSlug), "utf8");
51288
+ const raw = readFileSync34(attemptsPath(workspaceSlug), "utf8");
49528
51289
  const parsed = AttemptsFileSchema.safeParse(JSON.parse(raw));
49529
51290
  return parsed.success ? parsed.data : emptyFile();
49530
51291
  } catch {
@@ -49544,14 +51305,14 @@ function save(workspaceSlug, file2) {
49544
51305
  const dir = attemptsDir(workspaceSlug);
49545
51306
  const target = attemptsPath(workspaceSlug);
49546
51307
  const tmp = `${target}.${process.pid}.${tmpCounter2++}.tmp`;
49547
- mkdirSync12(dir, { recursive: true, mode: 448 });
51308
+ mkdirSync13(dir, { recursive: true, mode: 448 });
49548
51309
  writeFileSync9(tmp, `${JSON.stringify(file2, null, 2)}
49549
51310
  `, { mode: 384 });
49550
51311
  try {
49551
- renameSync8(tmp, target);
51312
+ renameSync9(tmp, target);
49552
51313
  } catch (err) {
49553
51314
  try {
49554
- unlinkSync9(tmp);
51315
+ unlinkSync10(tmp);
49555
51316
  } catch {}
49556
51317
  throw err;
49557
51318
  }
@@ -49687,7 +51448,7 @@ function denyUngated(detail) {
49687
51448
  }
49688
51449
 
49689
51450
  // src/lib/voice-gate/payload.ts
49690
- import { readFileSync as readFileSync33, statSync as statSync21 } from "node:fs";
51451
+ import { readFileSync as readFileSync35, statSync as statSync21 } from "node:fs";
49691
51452
  import { homedir as homedir2 } from "node:os";
49692
51453
  import { isAbsolute as isAbsolute4, resolve as resolve6 } from "node:path";
49693
51454
  var MAX_BODY_BYTES = 256 * 1024;
@@ -49848,7 +51609,7 @@ function defaultReadBodyFile(path5) {
49848
51609
  if (stat4.size > MAX_BODY_BYTES) {
49849
51610
  throw new Error(`draft body is ${stat4.size} bytes, over the ${MAX_BODY_BYTES}-byte gate limit`);
49850
51611
  }
49851
- return readFileSync33(path5, "utf8");
51612
+ return readFileSync35(path5, "utf8");
49852
51613
  }
49853
51614
  function parseRecipients(...raw) {
49854
51615
  const all = raw.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0);
@@ -50208,7 +51969,7 @@ async function runVoiceGateDirect(args, deps = {}) {
50208
51969
  let text;
50209
51970
  if (args.file) {
50210
51971
  try {
50211
- text = readFileSync34(args.file, "utf8");
51972
+ text = readFileSync36(args.file, "utf8");
50212
51973
  } catch (err) {
50213
51974
  const msg = err instanceof Error ? err.message : "unknown error";
50214
51975
  out.error(`Could not read ${args.file}: ${msg}`);