@plinth-music/cli 0.21.1 → 0.22.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.21.1",
7587
+ version: "0.22.0",
7588
7588
  description: "Plinth capstone sync daemon and CLI",
7589
7589
  type: "module",
7590
7590
  private: false,
@@ -22479,6 +22479,204 @@ function formatValueToken(value) {
22479
22479
  return s.length > 40 ? s.slice(0, 40) : s;
22480
22480
  }
22481
22481
 
22482
+ // src/lib/memory/annotate.ts
22483
+ var MEMORY_ANNOTATION_KEYS = [
22484
+ "memory_state",
22485
+ "memory_checked_at",
22486
+ "memory_retryable",
22487
+ "memory_state_revision",
22488
+ "memory_invalidated_at",
22489
+ "memory_refresh_attempts",
22490
+ "memory_last_refresh_failed_at",
22491
+ "memory_stale_premises",
22492
+ "memory_stale_premises_withheld",
22493
+ "memory_corrections",
22494
+ "memory_corrections_count",
22495
+ "memory_corrections_coverage"
22496
+ ];
22497
+ function yamlString(value) {
22498
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/[\u0000-\u001f\u007f]/g, (c) => `\\x${c.charCodeAt(0).toString(16).padStart(2, "0")}`);
22499
+ return `"${escaped}"`;
22500
+ }
22501
+ function listItem(pairs, indent = " ") {
22502
+ return pairs.map(([k, v], i) => i === 0 ? `${indent}- ${k}: ${v}` : `${indent} ${k}: ${v}`);
22503
+ }
22504
+ function successorLines(outcome, successor) {
22505
+ const out = [];
22506
+ if (outcome)
22507
+ out.push(["trail", yamlString(outcome)]);
22508
+ if (successor?.slug) {
22509
+ out.push(["replaced_by", yamlString(successor.slug)]);
22510
+ if (successor.description) {
22511
+ out.push(["replaced_by_says", yamlString(successor.description)]);
22512
+ }
22513
+ } else if (outcome === "superseded" || outcome === "withheld") {
22514
+ out.push(["replaced_by", yamlString("withheld - not readable by you")]);
22515
+ }
22516
+ return out;
22517
+ }
22518
+ function correctionLines(items) {
22519
+ if (items.length === 0)
22520
+ return [];
22521
+ const lines = [];
22522
+ const sorted = [...items].sort((a, b) => a.evidence_id < b.evidence_id ? -1 : a.evidence_id > b.evidence_id ? 1 : 0);
22523
+ for (const item of sorted) {
22524
+ const pairs = [["fact", yamlString(item.slug)]];
22525
+ pairs.push(["status", yamlString(item.status)]);
22526
+ if (item.evidential_status && item.evidential_status !== "undisputed") {
22527
+ pairs.push(["dispute", yamlString(item.evidential_status)]);
22528
+ }
22529
+ if (item.heading_or_span)
22530
+ pairs.push(["where", yamlString(item.heading_or_span)]);
22531
+ if (item.redacted === true) {
22532
+ pairs.push(["says", yamlString("withheld")]);
22533
+ } else if (item.excerpt) {
22534
+ pairs.push(["says", yamlString(item.excerpt)]);
22535
+ }
22536
+ pairs.push(...successorLines(item.trail_outcome, item.successor));
22537
+ lines.push(...listItem(pairs));
22538
+ }
22539
+ return lines;
22540
+ }
22541
+ function premiseLines(items) {
22542
+ if (items.length === 0)
22543
+ return [];
22544
+ const lines = [];
22545
+ const sorted = [...items].sort((a, b) => a.fact_id < b.fact_id ? -1 : a.fact_id > b.fact_id ? 1 : 0);
22546
+ for (const item of sorted) {
22547
+ const pairs = [["fact", yamlString(item.slug)]];
22548
+ pairs.push(["status", yamlString(item.status)]);
22549
+ if (item.evidential_status && item.evidential_status !== "undisputed") {
22550
+ pairs.push(["dispute", yamlString(item.evidential_status)]);
22551
+ }
22552
+ pairs.push(...successorLines(item.trail_outcome, item.successor));
22553
+ lines.push(...listItem(pairs));
22554
+ }
22555
+ return lines;
22556
+ }
22557
+ function renderAnnotationLines(a) {
22558
+ const lines = [];
22559
+ if (a.kind === "document") {
22560
+ lines.push(`memory_state: ${yamlString(a.state)}`);
22561
+ }
22562
+ lines.push(`memory_checked_at: ${a.checkedAt === null ? "null" : yamlString(a.checkedAt)}`);
22563
+ if (a.retryable)
22564
+ lines.push("memory_retryable: true");
22565
+ if (a.stateRevision !== null) {
22566
+ lines.push(`memory_state_revision: ${a.stateRevision}`);
22567
+ }
22568
+ if (a.invalidatedAt) {
22569
+ lines.push(`memory_invalidated_at: ${yamlString(a.invalidatedAt)}`);
22570
+ }
22571
+ if (a.refreshAttempts !== null && a.refreshAttempts > 0) {
22572
+ lines.push(`memory_refresh_attempts: ${a.refreshAttempts}`);
22573
+ }
22574
+ if (a.lastRefreshFailedAt) {
22575
+ lines.push(`memory_last_refresh_failed_at: ${yamlString(a.lastRefreshFailedAt)}`);
22576
+ }
22577
+ const premises = premiseLines(a.stalePremises);
22578
+ if (premises.length > 0) {
22579
+ lines.push("memory_stale_premises:");
22580
+ lines.push(...premises);
22581
+ }
22582
+ if (a.withheldPremises > 0) {
22583
+ lines.push(`memory_stale_premises_withheld: ${a.withheldPremises}`);
22584
+ }
22585
+ if (a.corrections === null) {
22586
+ lines.push("memory_corrections: null");
22587
+ } else {
22588
+ lines.push(`memory_corrections_coverage: ${yamlString(a.coverage ?? "recorded_evidence")}`);
22589
+ lines.push(`memory_corrections_count: ${a.corrections.length}`);
22590
+ if (a.corrections.length === 0) {
22591
+ lines.push("memory_corrections: []");
22592
+ } else {
22593
+ lines.push("memory_corrections:");
22594
+ lines.push(...correctionLines(a.corrections));
22595
+ }
22596
+ }
22597
+ return lines;
22598
+ }
22599
+ function classifyServedState(word) {
22600
+ switch (word) {
22601
+ case "untracked":
22602
+ case "current":
22603
+ case "invalidated":
22604
+ case "refresh_failed":
22605
+ case "not_found":
22606
+ return word;
22607
+ default:
22608
+ return "unknown";
22609
+ }
22610
+ }
22611
+ function annotationForDocument(args) {
22612
+ const { projection, corrections, checkedAt } = args;
22613
+ if (!projection) {
22614
+ return {
22615
+ kind: "document",
22616
+ state: "unknown",
22617
+ checkedAt,
22618
+ retryable: false,
22619
+ stateRevision: null,
22620
+ invalidatedAt: null,
22621
+ lastRefreshFailedAt: null,
22622
+ refreshAttempts: null,
22623
+ stalePremises: [],
22624
+ withheldPremises: 0,
22625
+ corrections: corrections ? corrections.items : null,
22626
+ coverage: corrections ? corrections.coverage : null
22627
+ };
22628
+ }
22629
+ const state = classifyServedState(projection.state);
22630
+ const dependencies = projection.dependencies_not_current ?? [];
22631
+ return {
22632
+ kind: "document",
22633
+ state,
22634
+ checkedAt,
22635
+ retryable: projection.retryable ?? state === "refresh_failed",
22636
+ stateRevision: projection.state_revision ?? null,
22637
+ invalidatedAt: projection.invalidated_at ?? null,
22638
+ lastRefreshFailedAt: projection.last_refresh_failed_at ?? null,
22639
+ refreshAttempts: projection.refresh_attempts ?? null,
22640
+ stalePremises: dependencies.filter((d) => !("withheld" in d)),
22641
+ withheldPremises: dependencies.filter((d) => ("withheld" in d)).length,
22642
+ corrections: corrections ? corrections.items : null,
22643
+ coverage: corrections ? corrections.coverage : null
22644
+ };
22645
+ }
22646
+ function annotationForWorkspaceFile(args) {
22647
+ const { corrections, checkedAt } = args;
22648
+ return {
22649
+ kind: "workspace_file",
22650
+ state: corrections ? "current" : "unknown",
22651
+ checkedAt,
22652
+ retryable: false,
22653
+ stateRevision: null,
22654
+ invalidatedAt: null,
22655
+ lastRefreshFailedAt: null,
22656
+ refreshAttempts: null,
22657
+ stalePremises: [],
22658
+ withheldPremises: 0,
22659
+ corrections: corrections ? corrections.items : null,
22660
+ coverage: corrections ? corrections.coverage : null
22661
+ };
22662
+ }
22663
+ function unknownAnnotation(priorCheckedAt, kind = "document") {
22664
+ return {
22665
+ kind,
22666
+ state: "unknown",
22667
+ checkedAt: priorCheckedAt,
22668
+ retryable: false,
22669
+ stateRevision: null,
22670
+ invalidatedAt: null,
22671
+ lastRefreshFailedAt: null,
22672
+ refreshAttempts: null,
22673
+ stalePremises: [],
22674
+ withheldPremises: 0,
22675
+ corrections: null,
22676
+ coverage: null
22677
+ };
22678
+ }
22679
+
22482
22680
  // src/lib/entities/documents.ts
22483
22681
  var DocumentLiveSchema = exports_external.object({
22484
22682
  ...BASE_ROW,
@@ -22514,7 +22712,8 @@ var SERVER_AUTHORITATIVE_KEYS2 = new Set([
22514
22712
  "last_edited_at",
22515
22713
  "pinned",
22516
22714
  "artist_id",
22517
- "project_id"
22715
+ "project_id",
22716
+ ...MEMORY_ANNOTATION_KEYS
22518
22717
  ]);
22519
22718
  var WIRE_EDITABLE_TOP_LEVEL_KEYS2 = new Set([
22520
22719
  "category",
@@ -23755,8 +23954,11 @@ var MEMORY_FACTS_DIR = "memory";
23755
23954
  var MEMORY_FACTS_PRIVATE_SEGMENT = "private";
23756
23955
  var MEMORY_FACT_SLUG_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
23757
23956
  var MEMORY_FACT_SLUG_MAX = 64;
23957
+ function isWritableFactSlug(slug) {
23958
+ return MEMORY_FACT_SLUG_RE.test(slug) && slug.length <= MEMORY_FACT_SLUG_MAX;
23959
+ }
23758
23960
  function assertFactSlug(slug) {
23759
- if (!MEMORY_FACT_SLUG_RE.test(slug) || slug.length > MEMORY_FACT_SLUG_MAX) {
23961
+ if (!isWritableFactSlug(slug)) {
23760
23962
  throw new MirrorPathError(`Invalid memory fact slug: ${JSON.stringify(slug)}`);
23761
23963
  }
23762
23964
  }
@@ -23764,6 +23966,46 @@ function memoryFactRelPath(pile, slug) {
23764
23966
  assertFactSlug(slug);
23765
23967
  return pile === "private" ? `${MEMORY_FACTS_DIR}/${MEMORY_FACTS_PRIVATE_SEGMENT}/${slug}.md` : `${MEMORY_FACTS_DIR}/${slug}.md`;
23766
23968
  }
23969
+ function memoryFactLinkTarget(pile, slug) {
23970
+ return memoryFactRelPath(pile, slug).slice(0, -".md".length);
23971
+ }
23972
+ var MEMORY_FRESHNESS_BASENAME = "_freshness.md";
23973
+ function memoryFreshnessRelPath() {
23974
+ return `${MEMORY_FACTS_DIR}/${MEMORY_FRESHNESS_BASENAME}`;
23975
+ }
23976
+ function memoryFreshnessFilePath(workspaceSlug) {
23977
+ const ws = workspaceMirrorPath(workspaceSlug);
23978
+ const target = resolve(join2(ws, MEMORY_FACTS_DIR, MEMORY_FRESHNESS_BASENAME));
23979
+ const prefix = join2(ws, MEMORY_FACTS_DIR) + sep;
23980
+ if (!target.startsWith(prefix)) {
23981
+ throw new MirrorPathError("Resolved freshness marker path escapes the memory subtree");
23982
+ }
23983
+ assertNoSymlinkedComponent(ws, target);
23984
+ return target;
23985
+ }
23986
+ function writeMemoryFreshnessFile(workspaceSlug, content) {
23987
+ const target = memoryFreshnessFilePath(workspaceSlug);
23988
+ const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
23989
+ mkdirSync(dirname(target), { recursive: true, mode: 448 });
23990
+ writeFileSync(tmp, content, { mode: 384 });
23991
+ try {
23992
+ renameSync(tmp, target);
23993
+ } catch (err) {
23994
+ try {
23995
+ unlinkSync(tmp);
23996
+ } catch {}
23997
+ throw err;
23998
+ }
23999
+ }
24000
+ function readMemoryFreshnessFile(workspaceSlug) {
24001
+ try {
24002
+ return readFileSync(memoryFreshnessFilePath(workspaceSlug), "utf8");
24003
+ } catch (err) {
24004
+ if (err?.code === "ENOENT")
24005
+ return null;
24006
+ throw err;
24007
+ }
24008
+ }
23767
24009
  function parseMemoryFactRelPath(rel) {
23768
24010
  const posix = rel.split(sep).join("/");
23769
24011
  if (!posix.startsWith(`${MEMORY_FACTS_DIR}/`) || !posix.endsWith(".md")) {
@@ -25931,6 +26173,25 @@ import {
25931
26173
  writeFileSync as writeFileSync5
25932
26174
  } from "node:fs";
25933
26175
  import { dirname as dirname4, join as join6 } from "node:path";
26176
+
26177
+ // src/lib/hash.ts
26178
+ import { createHash } from "node:crypto";
26179
+ function stableHash(value) {
26180
+ return createHash("sha256").update(stableStringify(value)).digest("hex");
26181
+ }
26182
+ function stableStringify(value) {
26183
+ if (value === null || typeof value !== "object") {
26184
+ return JSON.stringify(value);
26185
+ }
26186
+ if (Array.isArray(value)) {
26187
+ return "[" + value.map((v) => stableStringify(v)).join(",") + "]";
26188
+ }
26189
+ const obj = value;
26190
+ const keys = Object.keys(obj).sort();
26191
+ return "{" + keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",") + "}";
26192
+ }
26193
+
26194
+ // src/lib/state.ts
25934
26195
  var StateEntrySchema = exports_external.object({
25935
26196
  id: exports_external.string().uuid(),
25936
26197
  slug: exports_external.string().min(1),
@@ -25978,9 +26239,61 @@ var MemoryFactEntrySchema = exports_external.object({
25978
26239
  id: exports_external.string().min(1),
25979
26240
  body_hash: exports_external.string().min(1)
25980
26241
  });
26242
+ var FeedWatermarkStateSchema = exports_external.object({
26243
+ snapshot: exports_external.string().min(1),
26244
+ taken_at: exports_external.string()
26245
+ });
26246
+ var FACT_FRESHNESS = ["current", "behind", "unknown"];
26247
+ var FACT_PULL_REASONS = [
26248
+ "behind",
26249
+ "floor_due",
26250
+ "watcher_edit",
26251
+ "incomplete_apply",
26252
+ "bad_watermark"
26253
+ ];
25981
26254
  var MemoryFactsStateSchema = exports_external.object({
25982
26255
  fetched_at: exports_external.string().datetime({ offset: true }),
25983
- facts: exports_external.record(exports_external.string(), MemoryFactEntrySchema).default({})
26256
+ facts: exports_external.record(exports_external.string(), MemoryFactEntrySchema).default({}),
26257
+ watermark: FeedWatermarkStateSchema.nullish(),
26258
+ freshness: exports_external.enum(FACT_FRESHNESS).optional(),
26259
+ freshness_checked_at: exports_external.string().datetime({ offset: true }).nullish(),
26260
+ freshness_reason: exports_external.string().nullish(),
26261
+ last_full_pull_at: exports_external.string().datetime({ offset: true }).nullish(),
26262
+ pull_epoch: exports_external.number().int().nonnegative().optional(),
26263
+ pull_pending: exports_external.partialRecord(exports_external.enum(FACT_PULL_REASONS), exports_external.number().int()).optional(),
26264
+ freshness_cooldown_until: exports_external.string().datetime({ offset: true }).nullish(),
26265
+ freshness_failures: exports_external.number().int().nonnegative().optional()
26266
+ });
26267
+ var MEMORY_SOURCE_STATES = [
26268
+ "untracked",
26269
+ "current",
26270
+ "invalidated",
26271
+ "refresh_failed",
26272
+ "not_found",
26273
+ "unknown"
26274
+ ];
26275
+ var MemorySourceStateSchema = exports_external.enum(MEMORY_SOURCE_STATES);
26276
+ var MemorySourceEntrySchema = exports_external.object({
26277
+ id: exports_external.string().min(1),
26278
+ state: MemorySourceStateSchema,
26279
+ state_revision: exports_external.number().nullable(),
26280
+ retryable: exports_external.boolean(),
26281
+ corrections_hash: exports_external.string().nullable(),
26282
+ corrections_count: exports_external.number().int().nonnegative().nullable(),
26283
+ checked_at: exports_external.string().nullable(),
26284
+ rendered: exports_external.string()
26285
+ });
26286
+ var MemorySourcesStateSchema = exports_external.object({
26287
+ polled_at: exports_external.string().datetime({ offset: true }).optional(),
26288
+ ask_failed_at: exports_external.string().datetime({ offset: true }).nullish(),
26289
+ write_interrupted: exports_external.boolean().nullish(),
26290
+ unwritten: exports_external.record(exports_external.string(), exports_external.object({
26291
+ path: exports_external.string().min(1),
26292
+ message: exports_external.string().min(1),
26293
+ recorded_at: exports_external.string().datetime({ offset: true })
26294
+ })).optional(),
26295
+ documents: exports_external.record(exports_external.string(), MemorySourceEntrySchema).default({}),
26296
+ workspace_files: exports_external.record(exports_external.string(), MemorySourceEntrySchema).default({})
25984
26297
  });
25985
26298
  var UnsyncedOpSchema = exports_external.object({
25986
26299
  target: exports_external.enum(["entity", "file"]),
@@ -26041,7 +26354,8 @@ var WorkspaceStateV2Schema = exports_external.object({
26041
26354
  team_rejected: TeamRejectedSchema.optional(),
26042
26355
  size_anchor: SizeAnchorSchema.optional(),
26043
26356
  last_trim_at: exports_external.string().min(1).optional(),
26044
- memory_facts: MemoryFactsStateSchema.optional()
26357
+ memory_facts: MemoryFactsStateSchema.optional(),
26358
+ memory_sources: MemorySourcesStateSchema.optional()
26045
26359
  });
26046
26360
  var WorkspaceStateV1Schema = exports_external.object({
26047
26361
  version: exports_external.literal(1),
@@ -26260,7 +26574,8 @@ function mergeWorkspaceState(fresh, before, after, wantCorrected = true) {
26260
26574
  team_rejected: mergeScalar(fresh.team_rejected, before.team_rejected, after.team_rejected),
26261
26575
  size_anchor: mergeAnchor(fresh.size_anchor, before.size_anchor, after.size_anchor),
26262
26576
  last_trim_at: mergeScalar(fresh.last_trim_at, before.last_trim_at, after.last_trim_at),
26263
- memory_facts: mergeScalar(fresh.memory_facts, before.memory_facts, after.memory_facts)
26577
+ memory_facts: mergeScalar(fresh.memory_facts, before.memory_facts, after.memory_facts),
26578
+ memory_sources: mergeScalar(fresh.memory_sources, before.memory_sources, after.memory_sources)
26264
26579
  };
26265
26580
  if (!wantCorrected)
26266
26581
  return { merged, corrected: after };
@@ -26556,13 +26871,62 @@ function getMemoryFactEntries(state) {
26556
26871
  function getMemoryFactEntry(state, relPath) {
26557
26872
  return state.memory_facts?.facts[relPath];
26558
26873
  }
26874
+ function raiseFactPullReason(state, reason, nowIso) {
26875
+ const entry = state.memory_facts ?? { fetched_at: nowIso, facts: {} };
26876
+ const pending = entry.pull_pending ?? {};
26877
+ const epoch = (entry.pull_epoch ?? 0) + 1;
26878
+ return {
26879
+ ...state,
26880
+ memory_facts: {
26881
+ ...entry,
26882
+ pull_epoch: epoch,
26883
+ pull_pending: { ...pending, [reason]: epoch }
26884
+ }
26885
+ };
26886
+ }
26559
26887
  function setMemoryFactsState(state, entry) {
26560
26888
  const existing = state.memory_facts;
26561
- if (existing && sameFactRegistry(existing.facts, entry.facts)) {
26889
+ if (existing && sameFactsEntry(existing, entry)) {
26562
26890
  return { state, changed: false };
26563
26891
  }
26564
26892
  return { state: { ...state, memory_facts: entry }, changed: true };
26565
26893
  }
26894
+ function sameFactsEntry(a, b) {
26895
+ if (!sameFactRegistry(a.facts, b.facts))
26896
+ return false;
26897
+ if ((a.watermark?.snapshot ?? null) !== (b.watermark?.snapshot ?? null))
26898
+ return false;
26899
+ if ((a.watermark?.taken_at ?? null) !== (b.watermark?.taken_at ?? null))
26900
+ return false;
26901
+ if ((a.freshness ?? null) !== (b.freshness ?? null))
26902
+ return false;
26903
+ if ((a.freshness_checked_at ?? null) !== (b.freshness_checked_at ?? null))
26904
+ return false;
26905
+ if ((a.freshness_reason ?? null) !== (b.freshness_reason ?? null))
26906
+ return false;
26907
+ if ((a.last_full_pull_at ?? null) !== (b.last_full_pull_at ?? null))
26908
+ return false;
26909
+ if ((a.pull_epoch ?? 0) !== (b.pull_epoch ?? 0))
26910
+ return false;
26911
+ if ((a.freshness_cooldown_until ?? null) !== (b.freshness_cooldown_until ?? null)) {
26912
+ return false;
26913
+ }
26914
+ if ((a.freshness_failures ?? 0) !== (b.freshness_failures ?? 0))
26915
+ return false;
26916
+ return samePullPending(a.pull_pending, b.pull_pending);
26917
+ }
26918
+ function samePullPending(a, b) {
26919
+ const ak = Object.keys(a ?? {});
26920
+ const bk = Object.keys(b ?? {});
26921
+ if (ak.length !== bk.length)
26922
+ return false;
26923
+ for (const key of ak) {
26924
+ const reason = key;
26925
+ if (a?.[reason] !== b?.[reason])
26926
+ return false;
26927
+ }
26928
+ return true;
26929
+ }
26566
26930
  function sameFactRegistry(a, b) {
26567
26931
  const aKeys = Object.keys(a);
26568
26932
  if (aKeys.length !== Object.keys(b).length)
@@ -26632,6 +26996,16 @@ function setLastTrimAt(state, at) {
26632
26996
  return { state, changed: false };
26633
26997
  return { state: { ...state, last_trim_at: at }, changed: true };
26634
26998
  }
26999
+ function getMemorySourceEntries(state, arm) {
27000
+ return state.memory_sources?.[arm] ?? {};
27001
+ }
27002
+ function setMemorySourcesState(state, entry) {
27003
+ const existing = state.memory_sources;
27004
+ if (existing !== undefined && stableHash(existing) === stableHash(entry)) {
27005
+ return { state, changed: false };
27006
+ }
27007
+ return { state: { ...state, memory_sources: entry }, changed: true };
27008
+ }
26635
27009
 
26636
27010
  // src/lib/memory-facts/stores.ts
26637
27011
  var BLOB_MEMORY_STORES = Object.freeze({
@@ -27174,25 +27548,8 @@ function injectUserBlock(generated, preserved) {
27174
27548
  return generated.slice(0, afterBegin) + preserved + generated.slice(endIdx);
27175
27549
  }
27176
27550
 
27177
- // src/lib/hash.ts
27178
- import { createHash } from "node:crypto";
27179
- function stableHash(value) {
27180
- return createHash("sha256").update(stableStringify(value)).digest("hex");
27181
- }
27182
- function stableStringify(value) {
27183
- if (value === null || typeof value !== "object") {
27184
- return JSON.stringify(value);
27185
- }
27186
- if (Array.isArray(value)) {
27187
- return "[" + value.map((v) => stableStringify(v)).join(",") + "]";
27188
- }
27189
- const obj = value;
27190
- const keys = Object.keys(obj).sort();
27191
- return "{" + keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",") + "}";
27192
- }
27193
-
27194
27551
  // src/lib/context/schema-version.ts
27195
- var TEMPLATE_VERSION = "5";
27552
+ var TEMPLATE_VERSION = "6";
27196
27553
  var HEADER_RE = /<!--\s*plinth-schema-version:\s*([a-f0-9]+)\s*-->/;
27197
27554
  function computeSchemaVersion(inputs) {
27198
27555
  return stableHash(inputs).slice(0, 12);
@@ -29974,7 +30331,16 @@ function fileOverCapLine(f) {
29974
30331
  return `[FILE_OVER_CAP path=${f.path} size=${f.size} max=${f.max} max_from=${f.source}]`;
29975
30332
  }
29976
30333
  function fileConflictLine(f) {
29977
- return `[FILE_CONFLICT path=${f.path} stash=${f.stash}]`;
30334
+ const viaPart = f.via ? ` via=${f.via}` : "";
30335
+ const reusedPart = f.reused ? " reused=true" : "";
30336
+ return `[FILE_CONFLICT path=${f.path} stash=${f.stash}${viaPart}${reusedPart}]`;
30337
+ }
30338
+ function filePushSkippedLine(f) {
30339
+ return `[FILE_PUSH_SKIPPED path=${f.path} op=${f.op} reason=${f.reason}]`;
30340
+ }
30341
+ function fileStashFailedLine(f) {
30342
+ const codePart = f.code ? ` code=${f.code}` : "";
30343
+ return `[FILE_STASH_FAILED path=${f.path} reason=${f.reason}${codePart}]`;
29978
30344
  }
29979
30345
  function fileRejectionSyncFailedLine(f) {
29980
30346
  const parts = [`[FILE_REJECTION_SYNC_FAILED verb=${f.verb}`];
@@ -30364,7 +30730,31 @@ function clearMemoryGeneratedRefusal(state, workspaceSlug, absolutePath) {
30364
30730
  return commitState(state, { ...state, rejected: next });
30365
30731
  }
30366
30732
 
30733
+ // src/lib/context/house-style.ts
30734
+ var HOUSE_STYLE = [
30735
+ "## House style: documents you write in `files/` or `documents/` (the workspace block below overrides it)",
30736
+ "- Title as an H1. In `files/` name the file after it; in `documents/` keep the lowercase-hyphen filename (`tour-brief.md`) or it never syncs.",
30737
+ "- Frontmatter: `artist`, `project`, `date`, `status`, as labels. A new `documents/` file links only through `artist_id:` / `project_id:`, the `id:` of the artist's or project's file.",
30738
+ "- A one-line summary directly under the title.",
30739
+ "- Money in tables, with the currency and thousands separators.",
30740
+ "- Dates in ISO form with the day name, e.g. `2026-09-21 (Monday)`.",
30741
+ "- Warnings and open questions as `>` callouts, not inline.",
30742
+ "- Plain headings: no emoji, no decorative rules."
30743
+ ];
30744
+
30367
30745
  // src/lib/context/workspace-claude-md.ts
30746
+ var FILES_SEED_FOLDERS = [
30747
+ "Deals",
30748
+ "Finance",
30749
+ "Live",
30750
+ "Releases",
30751
+ "Marketing",
30752
+ "Meetings"
30753
+ ];
30754
+ function seedFolderList() {
30755
+ const head = FILES_SEED_FOLDERS.slice(0, -1).join(", ");
30756
+ return `${head} or ${FILES_SEED_FOLDERS[FILES_SEED_FOLDERS.length - 1]}`;
30757
+ }
30368
30758
  var EXTRACTED_TEXT_DIR = extractedTextRelPath("x").replace(/x\.md$/, "");
30369
30759
  function rosterBlock(roster, activeProjectCountBySlug) {
30370
30760
  if (roster.length === 0)
@@ -30415,6 +30805,8 @@ function buildWorkspaceClaudeMd(input) {
30415
30805
  `- \`artists/<slug>/\` one folder per artist, the working dir for artist-scoped sessions · \`threads/<slug>/_thread.md\` one folder per thread${emptyNote(folders, ["artists", "threads"])}`,
30416
30806
  `- \`projects/\` \`tasks/\` \`documents/\` \`meetings/\` workspace-root flat entity files (one \`.md\` per entity, NOT nested under \`artists/\`, so one artist's rows are found by matching \`artist_id:\` in their frontmatter against that artist's own \`id:\` in \`artists/<slug>/_artist.md\`, never by matching the artist's name: a name match returned 16 of one artist's 46 projects and read as "no such project")${emptyNote(folders, ["projects", "tasks", "documents", "meetings"])}`,
30417
30807
  `- \`files/\` free-form document store: PDFs, executed agreements, spreadsheets, anything that is a FILE rather than an entity. Mirrored byte-for-byte both ways, folders and all${emptyNote(folders, [FILES_SUBTREE_DIR])}`,
30808
+ `- Where a file goes in \`files/\`: a project's files in \`files/<Artist>/<Project>/\`, both names as Plinth shows them (name or slug, any case). That folder path is what links the file to the artist and the project when it syncs; frontmatter links nothing. If the artist or project is renamed in Plinth, new files under the old folder name may no longer link to it (files already there keep their links): file new work under the new name and leave existing folders where they are`,
30809
+ `- An artist's files that belong to no single project go in \`files/<Artist>/\` under ${seedFolderList()} (a meeting's own notes stay on the Plinth meeting), or a folder two or more artists already have; start a new folder only when none fits. Files for no artist, an artist-less project's included, go in a top-level folder of \`files/\` named for the work, not an artist's name or slug; nothing links them`,
30418
30810
  `- \`${extractedTextRelPath("<path>")}\` where \`<path>\` is the file's path under \`files/\`: the text Plinth extracted from that file, so a PDF's contents can be searched. Not every file has one - text files are already their own text, and extraction can fail. Where it produced junk the file exists and says so instead of carrying it, so read what is there before trusting a silence. Derived: edits here are overwritten and never reach Plinth`,
30419
30811
  `- \`.claude/commands/\` local skills, incl. generated \`${START_COMMAND_INVOCATION}\` + \`${CLOSE_COMMAND_INVOCATION}\` + \`${ONBOARD_COMMAND_INVOCATION}\` · at the mirror root: \`workspace-memory.md\` \`workspace-rules.md\` (workspace-wide) + \`user-memory.md\` (yours), synced to Plinth · \`voice.md\` (yours: how you write, read by the agent, edited only by you) · \`CLAUDE.local.md\` private, gitignored`,
30420
30812
  ``,
@@ -30441,6 +30833,8 @@ function buildWorkspaceClaudeMd(input) {
30441
30833
  `## Skills: ${skillCount} discoverable location${skillCount === 1 ? "" : "s"}`,
30442
30834
  ...skillLines,
30443
30835
  ``,
30836
+ ...HOUSE_STYLE,
30837
+ ``,
30444
30838
  USER_BLOCK_BEGIN,
30445
30839
  USER_BLOCK_PLACEHOLDER,
30446
30840
  USER_BLOCK_END
@@ -33701,8 +34095,8 @@ var sessionCommand = defineCommand({
33701
34095
  });
33702
34096
 
33703
34097
  // src/commands/start.ts
33704
- var import_gray_matter13 = __toESM(require_gray_matter(), 1);
33705
- import { existsSync as existsSync9, readFileSync as readFileSync26, statSync as statSync17 } from "node:fs";
34098
+ var import_gray_matter14 = __toESM(require_gray_matter(), 1);
34099
+ import { existsSync as existsSync9, readFileSync as readFileSync28, statSync as statSync18 } from "node:fs";
33706
34100
 
33707
34101
  // src/lib/daemon-log.ts
33708
34102
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync9, renameSync as renameSync7, statSync as statSync4 } from "node:fs";
@@ -33946,10 +34340,10 @@ class DestructiveGate {
33946
34340
  }
33947
34341
 
33948
34342
  // src/lib/resolve-pending.ts
33949
- import { readFileSync as readFileSync18 } from "node:fs";
34343
+ import { readFileSync as readFileSync19 } from "node:fs";
33950
34344
 
33951
34345
  // src/lib/conflicts.ts
33952
- import { mkdirSync as mkdirSync10, readdirSync as readdirSync6, statSync as statSync6, writeFileSync as writeFileSync7 } from "node:fs";
34346
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync15, readdirSync as readdirSync6, statSync as statSync6, writeFileSync as writeFileSync7 } from "node:fs";
33953
34347
  import { dirname as dirname9, join as join12 } from "node:path";
33954
34348
  function conflictsRoot(workspaceSlug) {
33955
34349
  return join12(plinthHome(), "_conflicts", workspaceSlug);
@@ -33968,8 +34362,10 @@ function stashConflict(args) {
33968
34362
  function fileConflictPath(args) {
33969
34363
  const { workspaceSlug, relPath, timestamp, side } = args;
33970
34364
  const iso = timestamp.toISOString().replace(/[:.]/g, "-");
33971
- const safe = relPath.replace(/[/\\]/g, "__");
33972
- return join12(conflictsRoot(workspaceSlug), `files.${safe}.${iso}.${side}`);
34365
+ return join12(conflictsRoot(workspaceSlug), `${fileConflictStem(relPath)}.${iso}.${side}`);
34366
+ }
34367
+ function fileConflictStem(relPath) {
34368
+ return `files.${relPath.replace(/[/\\]/g, "__")}`;
33973
34369
  }
33974
34370
  function stashFileConflict(args) {
33975
34371
  const target = fileConflictPath(args);
@@ -33977,6 +34373,26 @@ function stashFileConflict(args) {
33977
34373
  writeFileSync7(target, args.bytes, { mode: 384 });
33978
34374
  return target;
33979
34375
  }
34376
+ function stashFileConflictOnce(args) {
34377
+ const dir = conflictsRoot(args.workspaceSlug);
34378
+ const head = `${fileConflictStem(args.relPath)}.`;
34379
+ const tail = new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-\\d{3}Z\\.${args.side}$`);
34380
+ let names = [];
34381
+ try {
34382
+ names = readdirSync6(dir);
34383
+ } catch {
34384
+ names = [];
34385
+ }
34386
+ const newest = names.filter((n) => n.startsWith(head) && tail.test(n.slice(head.length))).sort().pop();
34387
+ if (newest !== undefined) {
34388
+ const path5 = join12(dir, newest);
34389
+ try {
34390
+ if (Buffer.compare(args.bytes, readFileSync15(path5)) === 0)
34391
+ return { path: path5, reused: true };
34392
+ } catch {}
34393
+ }
34394
+ return { path: stashFileConflict(args), reused: false };
34395
+ }
33980
34396
  var STAMP_RE2 = /\.(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z)(?:\.(local|cloud))?(?:\.md)?$/;
33981
34397
  function parseStamp(stamp) {
33982
34398
  const iso = stamp.replace(/T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/, "T$1:$2:$3.$4Z");
@@ -34032,7 +34448,7 @@ function listConflictStashes(workspaceSlug) {
34032
34448
  }
34033
34449
 
34034
34450
  // src/lib/files/push.ts
34035
- import { readFileSync as readFileSync15, statSync as statSync7 } from "node:fs";
34451
+ import { readFileSync as readFileSync16, statSync as statSync7 } from "node:fs";
34036
34452
 
34037
34453
  // src/lib/files/content-hash.ts
34038
34454
  import { createHash as createHash3 } from "node:crypto";
@@ -34188,7 +34604,7 @@ async function pushFilesUpsert(args) {
34188
34604
  state: next
34189
34605
  };
34190
34606
  }
34191
- bytes = readFileSync15(absolutePath);
34607
+ bytes = readFileSync16(absolutePath);
34192
34608
  hash2 = sha256Bytes(bytes);
34193
34609
  inode = st.ino;
34194
34610
  }
@@ -34279,8 +34695,14 @@ async function pushFilesUpsert(args) {
34279
34695
  cloudRow = undefined;
34280
34696
  }
34281
34697
  }
34282
- const stashPath = stashFileConflict({ workspaceSlug, relPath, timestamp: now(), bytes, side: "local" });
34283
- output?.log(fileConflictLine({ path: relPath, stash: stashPath }));
34698
+ const { path: stashPath, reused } = stashFileConflictOnce({
34699
+ workspaceSlug,
34700
+ relPath,
34701
+ timestamp: now(),
34702
+ bytes,
34703
+ side: "local"
34704
+ });
34705
+ output?.log(fileConflictLine({ path: relPath, stash: stashPath, reused }));
34284
34706
  if (existing) {
34285
34707
  nextState = await restoreFileFromCloud({
34286
34708
  workspaceSlug,
@@ -34357,7 +34779,7 @@ class PerIdMutex {
34357
34779
 
34358
34780
  // src/lib/push.ts
34359
34781
  var import_gray_matter10 = __toESM(require_gray_matter(), 1);
34360
- import { existsSync as existsSync4, readFileSync as readFileSync17, statSync as statSync8 } from "node:fs";
34782
+ import { existsSync as existsSync4, readFileSync as readFileSync18, statSync as statSync8 } from "node:fs";
34361
34783
  import { basename as basename5, relative as relative5 } from "node:path";
34362
34784
 
34363
34785
  // src/lib/frontmatter-id.ts
@@ -34407,7 +34829,7 @@ function frontmatterIdWriteIsSound(next, id, expectedBody) {
34407
34829
 
34408
34830
  // src/lib/pull-write.ts
34409
34831
  var import_gray_matter9 = __toESM(require_gray_matter(), 1);
34410
- import { readFileSync as readFileSync16 } from "node:fs";
34832
+ import { readFileSync as readFileSync17 } from "node:fs";
34411
34833
  function computeMirrorHashes(strategy, content) {
34412
34834
  const parsed = import_gray_matter9.default(content);
34413
34835
  const fm = parsed.data ?? {};
@@ -34451,7 +34873,7 @@ function relocateForSlugChange(args) {
34451
34873
  }
34452
34874
  let raw = null;
34453
34875
  try {
34454
- raw = readFileSync16(fromPath, "utf8");
34876
+ raw = readFileSync17(fromPath, "utf8");
34455
34877
  } catch (err) {
34456
34878
  const code = err?.code;
34457
34879
  if (code === "ENOENT") {
@@ -34522,7 +34944,7 @@ function writeMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, suppressor,
34522
34944
  const target = entityFilePath(workspaceSlug, strategy.type, targetSlug);
34523
34945
  let existing = null;
34524
34946
  try {
34525
- existing = readFileSync16(target, "utf8");
34947
+ existing = readFileSync17(target, "utf8");
34526
34948
  } catch (err) {
34527
34949
  if (err?.code !== "ENOENT")
34528
34950
  throw err;
@@ -34654,7 +35076,7 @@ function stampStateFromMirror(args) {
34654
35076
  const { strategy, workspaceSlug, doc: doc2, state, priorMemoryHash } = args;
34655
35077
  const targetSlug = args.pathSlug ?? doc2.slug;
34656
35078
  const mirrorPath = entityFilePath(workspaceSlug, strategy.type, targetSlug);
34657
- const mirrorRaw = readFileSync17(mirrorPath, "utf8");
35079
+ const mirrorRaw = readFileSync18(mirrorPath, "utf8");
34658
35080
  const mirrorParsed = import_gray_matter10.default(mirrorRaw);
34659
35081
  const mirrorFm = mirrorParsed.data ?? {};
34660
35082
  const mirrorLifted = splitUserFrontmatter(strategy, mirrorFm);
@@ -34807,7 +35229,7 @@ async function pushFile(args) {
34807
35229
  }
34808
35230
  let raw;
34809
35231
  try {
34810
- raw = readFileSync17(absolutePath, "utf8");
35232
+ raw = readFileSync18(absolutePath, "utf8");
34811
35233
  } catch (err) {
34812
35234
  if (err?.code === "ENOENT") {
34813
35235
  return runPushDelete({
@@ -35157,7 +35579,7 @@ async function runPushUpsert(args) {
35157
35579
  if (liveEntry !== snapshotEntry) {
35158
35580
  let onDisk = null;
35159
35581
  try {
35160
- onDisk = readFileSync17(args.absolutePath, "utf8");
35582
+ onDisk = readFileSync18(args.absolutePath, "utf8");
35161
35583
  } catch {
35162
35584
  onDisk = null;
35163
35585
  }
@@ -35597,7 +36019,7 @@ function writeCreatedIdBack(args) {
35597
36019
  if (entityFilePath(args.workspaceSlug, args.strategy.type, args.slug) !== args.absolutePath) {
35598
36020
  return;
35599
36021
  }
35600
- const readBack = args.readBack ?? ((p) => readFileSync17(p, "utf8"));
36022
+ const readBack = args.readBack ?? ((p) => readFileSync18(p, "utf8"));
35601
36023
  if (readBack(args.absolutePath) !== args.raw)
35602
36024
  return;
35603
36025
  args.suppressor?.registerWrite(args.absolutePath, computeMirrorHashes(args.strategy, write.content));
@@ -35949,7 +36371,7 @@ async function pushRename(args) {
35949
36371
  };
35950
36372
  if (!updated.body_hash || !updated.frontmatter_hash) {
35951
36373
  try {
35952
- const mirrorRaw = readFileSync17(toPath, "utf8");
36374
+ const mirrorRaw = readFileSync18(toPath, "utf8");
35953
36375
  const mirrorParsed = import_gray_matter10.default(mirrorRaw);
35954
36376
  const renameFm = mirrorParsed.data ?? {};
35955
36377
  const renameLifted = splitUserFrontmatter(strategy, renameFm);
@@ -35986,7 +36408,7 @@ async function pushRename(args) {
35986
36408
  if (patchResult.reason === "slug_conflict") {
35987
36409
  let contents2 = "";
35988
36410
  try {
35989
- contents2 = readFileSync17(toPath, "utf8");
36411
+ contents2 = readFileSync18(toPath, "utf8");
35990
36412
  } catch {}
35991
36413
  const stashPath2 = stashConflict({
35992
36414
  workspaceSlug,
@@ -36034,7 +36456,7 @@ async function pushRename(args) {
36034
36456
  }
36035
36457
  let contents = "";
36036
36458
  try {
36037
- contents = readFileSync17(toPath, "utf8");
36459
+ contents = readFileSync18(toPath, "utf8");
36038
36460
  } catch {}
36039
36461
  const stashPath = stashConflict({
36040
36462
  workspaceSlug,
@@ -36167,7 +36589,7 @@ async function discardBatchOps(batch, deps) {
36167
36589
  const path5 = opPath(deps.workspaceSlug, op);
36168
36590
  let local = null;
36169
36591
  try {
36170
- local = readFileSync18(path5, "utf8");
36592
+ local = readFileSync19(path5, "utf8");
36171
36593
  } catch {
36172
36594
  local = null;
36173
36595
  }
@@ -36206,7 +36628,7 @@ async function discardFileOp(op, deps) {
36206
36628
  const abs = filesSubtreePath(deps.workspaceSlug, op.path);
36207
36629
  let local = null;
36208
36630
  try {
36209
- local = readFileSync18(abs);
36631
+ local = readFileSync19(abs);
36210
36632
  } catch {
36211
36633
  local = null;
36212
36634
  }
@@ -36276,20 +36698,29 @@ var FactCommonSchema = exports_external.object({
36276
36698
  body_md: exports_external.string(),
36277
36699
  fact_type: exports_external.string(),
36278
36700
  status: exports_external.string(),
36279
- superseded_by: exports_external.string().nullish(),
36280
36701
  generality: exports_external.string().nullish(),
36281
36702
  source: exports_external.string().nullish(),
36282
36703
  created_at: exports_external.string().nullish(),
36283
- updated_at: exports_external.string().nullish()
36704
+ updated_at: exports_external.string().nullish(),
36705
+ evidential_status: exports_external.string().nullish(),
36706
+ assertion_revision: exports_external.number().nullish(),
36707
+ support_revision: exports_external.number().nullish(),
36708
+ trail: exports_external.string().nullish(),
36709
+ current_fact_id: exports_external.string().nullish()
36284
36710
  });
36285
36711
  var WorkspaceFactWireSchema = FactCommonSchema.extend({
36286
36712
  artist_id: exports_external.string().nullish(),
36287
36713
  revision: exports_external.number().nullish()
36288
36714
  });
36289
36715
  var UserFactWireSchema = FactCommonSchema;
36716
+ var FeedWatermarkSchema = exports_external.object({
36717
+ snapshot: exports_external.string().min(1),
36718
+ taken_at: exports_external.string()
36719
+ });
36290
36720
  var MemoryFactsFeedSchema = exports_external.object({
36291
36721
  workspace_facts: exports_external.array(WorkspaceFactWireSchema),
36292
- user_facts: exports_external.array(UserFactWireSchema)
36722
+ user_facts: exports_external.array(UserFactWireSchema),
36723
+ watermark: FeedWatermarkSchema.nullish()
36293
36724
  });
36294
36725
  var ROUTE = "/api/sync/memory-facts";
36295
36726
  function baseHeaders2(pat) {
@@ -36322,6 +36753,272 @@ async function getMemoryFacts(args) {
36322
36753
  return parsed.data;
36323
36754
  }
36324
36755
 
36756
+ // src/lib/memory-facts/freshness-state.ts
36757
+ var FULL_PULL_FLOOR_MS = 3600000;
36758
+ var FAILURE_BACKOFF_MS = [30000, 60000, 120000, 300000];
36759
+ function failureBackoffMs(consecutiveFailures) {
36760
+ const i = Math.min(Math.max(consecutiveFailures, 1), FAILURE_BACKOFF_MS.length) - 1;
36761
+ return FAILURE_BACKOFF_MS[i];
36762
+ }
36763
+ function emptyFactsEntry(nowIso) {
36764
+ return { fetched_at: nowIso, facts: {} };
36765
+ }
36766
+ function raisePullReason(entry2, reason) {
36767
+ const pending = entry2.pull_pending ?? {};
36768
+ const epoch = (entry2.pull_epoch ?? 0) + 1;
36769
+ return { ...entry2, pull_epoch: epoch, pull_pending: { ...pending, [reason]: epoch } };
36770
+ }
36771
+ var COPY_SUSPECT_REASONS = ["incomplete_apply", "watcher_edit"];
36772
+ function copySuspectReason(entry2) {
36773
+ const pending = entry2.pull_pending ?? {};
36774
+ for (const reason of COPY_SUSPECT_REASONS) {
36775
+ if (pending[reason] !== undefined)
36776
+ return reason;
36777
+ }
36778
+ return null;
36779
+ }
36780
+ function pendingReasons(entry2) {
36781
+ const pending = entry2.pull_pending ?? {};
36782
+ return Object.keys(pending).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
36783
+ }
36784
+ function clearAppliedReasons(entry2, upToEpoch) {
36785
+ const pending = entry2.pull_pending ?? {};
36786
+ const kept = {};
36787
+ for (const key of Object.keys(pending)) {
36788
+ const raised = pending[key];
36789
+ if (raised !== undefined && raised > upToEpoch)
36790
+ kept[key] = raised;
36791
+ }
36792
+ return { ...entry2, pull_pending: kept };
36793
+ }
36794
+ function floorDue(entry2, now, floorMs = FULL_PULL_FLOOR_MS) {
36795
+ const last = entry2.last_full_pull_at;
36796
+ if (!last)
36797
+ return true;
36798
+ const at = Date.parse(last);
36799
+ if (!Number.isFinite(at))
36800
+ return true;
36801
+ return now.getTime() - at >= floorMs;
36802
+ }
36803
+ function mayAsk(entry2, now) {
36804
+ if (!entry2.watermark?.snapshot)
36805
+ return false;
36806
+ const until = entry2.freshness_cooldown_until;
36807
+ if (!until)
36808
+ return true;
36809
+ const at = Date.parse(until);
36810
+ if (!Number.isFinite(at))
36811
+ return true;
36812
+ return now.getTime() >= at;
36813
+ }
36814
+ function stamp(before, next, nowIso) {
36815
+ const moved = before.freshness !== next.freshness || (before.freshness_reason ?? null) !== (next.freshness_reason ?? null) || (before.watermark?.snapshot ?? null) !== (next.watermark?.snapshot ?? null);
36816
+ return moved ? { ...next, freshness_checked_at: nowIso } : { ...next, freshness_checked_at: before.freshness_checked_at ?? nowIso };
36817
+ }
36818
+ function applyVerdict(entry2, verdict, now) {
36819
+ const nowIso = now.toISOString();
36820
+ const served = {
36821
+ ...entry2,
36822
+ freshness_failures: 0,
36823
+ freshness_cooldown_until: null
36824
+ };
36825
+ switch (verdict.kind) {
36826
+ case "answer": {
36827
+ if (verdict.behind) {
36828
+ return raisePullReason(stamp(entry2, { ...served, freshness: "behind", freshness_reason: "behind" }, nowIso), "behind");
36829
+ }
36830
+ const suspect = copySuspectReason(entry2);
36831
+ if (suspect !== null) {
36832
+ return stamp(entry2, {
36833
+ ...served,
36834
+ freshness: "unknown",
36835
+ freshness_reason: suspect,
36836
+ watermark: entry2.watermark ?? null
36837
+ }, nowIso);
36838
+ }
36839
+ const movedOn = entry2.freshness !== "current" || !entry2.watermark;
36840
+ return stamp(entry2, {
36841
+ ...served,
36842
+ freshness: "current",
36843
+ freshness_reason: null,
36844
+ watermark: movedOn ? verdict.watermark ?? entry2.watermark ?? null : entry2.watermark ?? null
36845
+ }, nowIso);
36846
+ }
36847
+ case "bad_watermark": {
36848
+ return raisePullReason(stamp(entry2, {
36849
+ ...served,
36850
+ freshness: "unknown",
36851
+ freshness_reason: "bad_watermark",
36852
+ watermark: null
36853
+ }, nowIso), "bad_watermark");
36854
+ }
36855
+ case "unavailable": {
36856
+ const failures = (entry2.freshness_failures ?? 0) + 1;
36857
+ return {
36858
+ ...stamp(entry2, { ...entry2, freshness: "unknown", freshness_reason: "watermark_unavailable" }, nowIso),
36859
+ freshness_failures: failures,
36860
+ freshness_cooldown_until: new Date(now.getTime() + failureBackoffMs(failures)).toISOString()
36861
+ };
36862
+ }
36863
+ case "rate_limited": {
36864
+ const waitMs = Math.max(verdict.retryAfterMs ?? 0, failureBackoffMs(1));
36865
+ return {
36866
+ ...stamp(entry2, { ...entry2, freshness: "unknown", freshness_reason: "rate_limited" }, nowIso),
36867
+ freshness_cooldown_until: new Date(now.getTime() + waitMs).toISOString()
36868
+ };
36869
+ }
36870
+ case "failed": {
36871
+ const failures = (entry2.freshness_failures ?? 0) + 1;
36872
+ return {
36873
+ ...stamp(entry2, { ...entry2, freshness: "unknown", freshness_reason: "check_failed" }, nowIso),
36874
+ freshness_failures: failures,
36875
+ freshness_cooldown_until: new Date(now.getTime() + failureBackoffMs(failures)).toISOString()
36876
+ };
36877
+ }
36878
+ }
36879
+ }
36880
+ function withdrawClaim(entry2) {
36881
+ if (entry2.freshness === "unknown" && entry2.freshness_reason === "poll_failed") {
36882
+ return entry2;
36883
+ }
36884
+ return { ...entry2, freshness: "unknown", freshness_reason: "poll_failed" };
36885
+ }
36886
+ function pullDecision(entry2, verdict) {
36887
+ const reasons = pendingReasons(entry2);
36888
+ const servedNotBehind = verdict !== null && verdict.kind === "answer" && !verdict.behind;
36889
+ const pull = !(servedNotBehind && reasons.length === 0);
36890
+ return { pull, reasons, entry: entry2, epochAtFetch: entry2.pull_epoch ?? 0 };
36891
+ }
36892
+ function bankPull(args) {
36893
+ const { entry: entry2, complete, watermark, epochAtFetch, now } = args;
36894
+ const nowIso = now.toISOString();
36895
+ if (!complete) {
36896
+ return raisePullReason({ ...entry2, freshness: "unknown", freshness_reason: "incomplete_apply" }, "incomplete_apply");
36897
+ }
36898
+ const cleared = clearAppliedReasons(entry2, epochAtFetch);
36899
+ const survived = copySuspectReason(cleared);
36900
+ if (!watermark) {
36901
+ return stamp(entry2, {
36902
+ ...cleared,
36903
+ fetched_at: nowIso,
36904
+ last_full_pull_at: nowIso,
36905
+ watermark: null,
36906
+ freshness: "unknown",
36907
+ freshness_reason: "no_watermark"
36908
+ }, nowIso);
36909
+ }
36910
+ if (survived !== null) {
36911
+ return stamp(entry2, {
36912
+ ...cleared,
36913
+ fetched_at: nowIso,
36914
+ last_full_pull_at: nowIso,
36915
+ watermark: entry2.watermark ?? null,
36916
+ freshness: "unknown",
36917
+ freshness_reason: survived
36918
+ }, nowIso);
36919
+ }
36920
+ return stamp(entry2, {
36921
+ ...cleared,
36922
+ fetched_at: nowIso,
36923
+ last_full_pull_at: nowIso,
36924
+ watermark,
36925
+ freshness: "current",
36926
+ freshness_reason: null
36927
+ }, nowIso);
36928
+ }
36929
+
36930
+ // src/lib/memory-facts/marker.ts
36931
+ function verdictLines(entry2) {
36932
+ const freshness = entry2.freshness ?? "unknown";
36933
+ if (freshness === "current") {
36934
+ return [
36935
+ "Plinth confirmed this machine's copy of the memory facts was up to date.",
36936
+ "",
36937
+ "That was confirmed at the time below, by asking the server whether anything",
36938
+ "had changed since this copy was taken. It is not a promise about right now:",
36939
+ "if the daemon has stopped, nothing has been checked since."
36940
+ ];
36941
+ }
36942
+ if (freshness === "behind") {
36943
+ return [
36944
+ "The server says this copy is BEHIND: something you can read has changed",
36945
+ "since it was taken.",
36946
+ "",
36947
+ "The daemon downloads the whole fact list to catch up, normally within",
36948
+ "seconds. Until it does, treat the facts in this folder as slightly old -",
36949
+ "they are not wrong, they are just not the latest."
36950
+ ];
36951
+ }
36952
+ return [
36953
+ "Whether this copy is up to date is UNKNOWN. This is not the same as",
36954
+ "'unchanged': nobody has been able to check.",
36955
+ "",
36956
+ unknownBecause(entry2.freshness_reason ?? null),
36957
+ "",
36958
+ "Run `plinth status` to see whether the daemon is running."
36959
+ ];
36960
+ }
36961
+ function unknownBecause(reason) {
36962
+ switch (reason) {
36963
+ case "poll_failed":
36964
+ return "The last attempt to reach Plinth did not get through - usually no network.";
36965
+ case "rate_limited":
36966
+ return "Plinth asked this machine to slow down, so the check is waiting its turn.";
36967
+ case "watermark_unavailable":
36968
+ return "This Plinth server cannot answer freshness checks yet.";
36969
+ case "bad_watermark":
36970
+ return "The marker this machine was checking against is no longer one the server recognises, so it will download the full list to get a new one.";
36971
+ case "no_watermark":
36972
+ return "The last download carried no freshness marker, so there is nothing to compare against.";
36973
+ case "incomplete_apply":
36974
+ return "The last download did not fully land, so this copy is not the one the freshness marker describes.";
36975
+ case "check_failed":
36976
+ return "The freshness check did not complete.";
36977
+ default:
36978
+ return "No freshness check has succeeded yet on this machine.";
36979
+ }
36980
+ }
36981
+ function renderFreshnessMarker(entry2) {
36982
+ const freshness = entry2.freshness ?? "unknown";
36983
+ const lines = [
36984
+ "<!-- GENERATED by Plinth. Edits here are not saved: the next sync rewrites",
36985
+ ` this file. It is not a memory fact and nothing reads it back. This`,
36986
+ ` file is ${memoryFreshnessRelPath()}. -->`,
36987
+ "",
36988
+ "# Memory facts: is this copy current?",
36989
+ "",
36990
+ ...verdictLines(entry2),
36991
+ "",
36992
+ "```",
36993
+ `freshness: ${freshness}`
36994
+ ];
36995
+ if (entry2.freshness_checked_at) {
36996
+ lines.push(`answer established: ${entry2.freshness_checked_at}`);
36997
+ }
36998
+ if (entry2.last_full_pull_at && entry2.freshness_reason !== "no_watermark") {
36999
+ lines.push(`full fact list last downloaded: ${entry2.last_full_pull_at}`);
37000
+ }
37001
+ if (freshness !== "current" && entry2.freshness_reason) {
37002
+ lines.push(`reason: ${entry2.freshness_reason}`);
37003
+ }
37004
+ lines.push("```");
37005
+ return lines.join(`
37006
+ `).replace(/\n*$/, "") + `
37007
+ `;
37008
+ }
37009
+ function writeFreshnessMarkerIfChanged(args) {
37010
+ const rendered = renderFreshnessMarker(args.entry);
37011
+ const onDisk = readMemoryFreshnessFile(args.workspaceSlug);
37012
+ if (onDisk !== null && stableHash(onDisk) === stableHash(rendered))
37013
+ return false;
37014
+ args.suppressor?.registerWrite(memoryFreshnessFilePath(args.workspaceSlug), {
37015
+ bodyHash: stableHash(rendered),
37016
+ frontmatterHash: ""
37017
+ });
37018
+ writeMemoryFreshnessFile(args.workspaceSlug, rendered);
37019
+ return true;
37020
+ }
37021
+
36325
37022
  // src/lib/memory-facts/refusal.ts
36326
37023
  var MEMORY_IS_GENERATED_CODE = "memory_is_generated";
36327
37024
  var GeneratedRefusalSchema = exports_external.object({
@@ -36392,59 +37089,134 @@ function refuseGeneratedMemoryEdit(args) {
36392
37089
  }
36393
37090
 
36394
37091
  // src/lib/memory-facts/render.ts
36395
- function yamlString(value) {
37092
+ function yamlString2(value) {
36396
37093
  const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/[\u0000-\u001f\u007f]/g, (c) => `\\x${c.charCodeAt(0).toString(16).padStart(2, "0")}`);
36397
37094
  return `"${escaped}"`;
36398
37095
  }
36399
37096
  var SOURCE_UNKNOWN = "unknown";
36400
- function toRenderableWorkspaceFact(row, artistSlugById) {
37097
+ var UNKNOWN = "unknown";
37098
+ var EVIDENTIAL_STATUSES = new Set(["undisputed", "disputed", "resolved"]);
37099
+ var TRAIL_OUTCOMES = new Set([
37100
+ "current",
37101
+ "superseded",
37102
+ "retired",
37103
+ "dangling",
37104
+ "cycle",
37105
+ "withheld",
37106
+ "not_found"
37107
+ ]);
37108
+ function resolveTrail(row, slugById) {
37109
+ const outcome = row.trail ?? null;
37110
+ const rawSuccessor = row.current_fact_id ?? null;
37111
+ const successor = rawSuccessor !== null && rawSuccessor.trim() !== "" ? rawSuccessor : null;
37112
+ if (outcome === null)
37113
+ return { trail: UNKNOWN, consistent: true };
37114
+ if (!TRAIL_OUTCOMES.has(outcome))
37115
+ return { trail: UNKNOWN, consistent: true };
37116
+ if (outcome === "superseded") {
37117
+ if (successor === null) {
37118
+ return { trail: UNKNOWN, consistent: false };
37119
+ }
37120
+ const slug = slugById(successor);
37121
+ if (slug === undefined) {
37122
+ return { trail: UNKNOWN, consistent: false };
37123
+ }
37124
+ return {
37125
+ trail: "superseded",
37126
+ current_fact_id: successor,
37127
+ current_slug: slug,
37128
+ consistent: true
37129
+ };
37130
+ }
37131
+ if (successor !== null)
37132
+ return { trail: UNKNOWN, consistent: false };
37133
+ return { trail: outcome, consistent: true };
37134
+ }
37135
+ function evidentialStatus(raw) {
37136
+ return raw && EVIDENTIAL_STATUSES.has(raw) ? raw : UNKNOWN;
37137
+ }
37138
+ function revision(raw) {
37139
+ return typeof raw === "number" && Number.isFinite(raw) ? String(raw) : UNKNOWN;
37140
+ }
37141
+ function factSlugIndex(rows) {
37142
+ const byId = new Map;
37143
+ for (const row of rows) {
37144
+ if (isWritableFactSlug(row.slug))
37145
+ byId.set(row.id, row.slug);
37146
+ }
37147
+ return (id) => byId.get(id);
37148
+ }
37149
+ function toRenderableWorkspaceFact(row, artistSlugById, factSlugById) {
36401
37150
  const artistId = row.artist_id ?? null;
37151
+ const trail = resolveTrail(row, factSlugById);
36402
37152
  return {
36403
- pile: "shared",
36404
- id: row.id,
36405
- slug: row.slug,
36406
- description: row.description,
36407
- fact_type: row.fact_type,
36408
- status: row.status,
36409
- source: row.source,
36410
- superseded_by: row.superseded_by,
36411
- generality: row.generality,
36412
- body_md: row.body_md,
36413
- artist_id: artistId,
36414
- artist_slug: artistId ? artistSlugById(artistId) : undefined
36415
- };
36416
- }
36417
- function toRenderableUserFact(row) {
37153
+ consistent: trail.consistent,
37154
+ fact: {
37155
+ pile: "shared",
37156
+ id: row.id,
37157
+ slug: row.slug,
37158
+ description: row.description,
37159
+ fact_type: row.fact_type,
37160
+ status: row.status,
37161
+ source: row.source,
37162
+ generality: row.generality,
37163
+ body_md: row.body_md,
37164
+ artist_id: artistId,
37165
+ artist_slug: artistId ? artistSlugById(artistId) : undefined,
37166
+ evidential_status: evidentialStatus(row.evidential_status),
37167
+ trail: trail.trail,
37168
+ current_fact_id: trail.current_fact_id,
37169
+ current_slug: trail.current_slug,
37170
+ assertion_revision: revision(row.assertion_revision),
37171
+ support_revision: revision(row.support_revision)
37172
+ }
37173
+ };
37174
+ }
37175
+ function toRenderableUserFact(row, factSlugById) {
37176
+ const trail = resolveTrail(row, factSlugById);
36418
37177
  return {
36419
- pile: "private",
36420
- id: row.id,
36421
- slug: row.slug,
36422
- description: row.description,
36423
- fact_type: row.fact_type,
36424
- status: row.status,
36425
- source: row.source,
36426
- superseded_by: row.superseded_by,
36427
- generality: row.generality,
36428
- body_md: row.body_md
37178
+ consistent: trail.consistent,
37179
+ fact: {
37180
+ pile: "private",
37181
+ id: row.id,
37182
+ slug: row.slug,
37183
+ description: row.description,
37184
+ fact_type: row.fact_type,
37185
+ status: row.status,
37186
+ source: row.source,
37187
+ generality: row.generality,
37188
+ body_md: row.body_md,
37189
+ evidential_status: evidentialStatus(row.evidential_status),
37190
+ trail: trail.trail,
37191
+ current_fact_id: trail.current_fact_id,
37192
+ current_slug: trail.current_slug,
37193
+ assertion_revision: revision(row.assertion_revision),
37194
+ support_revision: revision(row.support_revision)
37195
+ }
36429
37196
  };
36430
37197
  }
36431
37198
  function renderFactFile(fact) {
36432
37199
  const lines = ["---"];
36433
- lines.push(`id: ${yamlString(fact.id)}`);
36434
- lines.push(`name: ${yamlString(fact.slug)}`);
36435
- lines.push(`description: ${yamlString(fact.description)}`);
36436
- lines.push(`type: ${yamlString(fact.fact_type)}`);
36437
- lines.push(`status: ${yamlString(fact.status)}`);
36438
- lines.push(`source: ${yamlString(fact.source ?? SOURCE_UNKNOWN)}`);
37200
+ lines.push(`id: ${yamlString2(fact.id)}`);
37201
+ lines.push(`name: ${yamlString2(fact.slug)}`);
37202
+ lines.push(`description: ${yamlString2(fact.description)}`);
37203
+ lines.push(`type: ${yamlString2(fact.fact_type)}`);
37204
+ lines.push(`status: ${yamlString2(fact.status)}`);
37205
+ lines.push(`source: ${yamlString2(fact.source ?? SOURCE_UNKNOWN)}`);
37206
+ lines.push(`evidential_status: ${yamlString2(fact.evidential_status)}`);
37207
+ lines.push(`trail: ${yamlString2(fact.trail)}`);
37208
+ if (fact.current_fact_id && fact.current_slug) {
37209
+ lines.push(`current_fact_id: ${yamlString2(fact.current_fact_id)}`);
37210
+ lines.push(`current: ${yamlString2(`[[${memoryFactLinkTarget(fact.pile, fact.current_slug)}]]`)}`);
37211
+ }
37212
+ lines.push(`assertion_revision: ${yamlString2(fact.assertion_revision)}`);
37213
+ lines.push(`support_revision: ${yamlString2(fact.support_revision)}`);
36439
37214
  if (fact.generality)
36440
- lines.push(`generality: ${yamlString(fact.generality)}`);
37215
+ lines.push(`generality: ${yamlString2(fact.generality)}`);
36441
37216
  if (fact.artist_id)
36442
- lines.push(`artist_id: ${yamlString(fact.artist_id)}`);
37217
+ lines.push(`artist_id: ${yamlString2(fact.artist_id)}`);
36443
37218
  if (fact.artist_slug)
36444
- lines.push(`artist: ${yamlString(fact.artist_slug)}`);
36445
- if (fact.superseded_by) {
36446
- lines.push(`superseded_by: ${yamlString(fact.superseded_by)}`);
36447
- }
37219
+ lines.push(`artist: ${yamlString2(fact.artist_slug)}`);
36448
37220
  lines.push("---");
36449
37221
  lines.push("");
36450
37222
  lines.push(...headerComment(fact));
@@ -36464,6 +37236,22 @@ function headerComment(fact) {
36464
37236
  ` ${factToolsFor(fact.pile)}`,
36465
37237
  ` with id ${fact.id}. This file is ${rel}.`
36466
37238
  ];
37239
+ if (fact.trail === "superseded" && fact.current_slug) {
37240
+ out.push(` SUPERSEDED: the current version of this fact is`, ` ${memoryFactLinkTarget(fact.pile, fact.current_slug)}.`);
37241
+ } else if (fact.trail === "withheld") {
37242
+ out.push(` SUPERSEDED: this fact was replaced, and the replacement is not one`, ` you can read. Nothing here names it.`);
37243
+ } else if (fact.trail === "dangling") {
37244
+ out.push(` SUPERSEDED: the fact that replaced this one is no longer served, so`, ` there is no current version to follow.`);
37245
+ } else if (fact.trail === "cycle") {
37246
+ out.push(` The correction trail for this fact loops back on itself, so no`, ` current version could be resolved.`);
37247
+ } else if (fact.trail === UNKNOWN) {
37248
+ out.push(` Its correction trail is UNKNOWN to this mirror - not "unchanged".`, ` Ask Plinth directly if it matters which version is current.`);
37249
+ }
37250
+ if (fact.evidential_status === "disputed") {
37251
+ out.push(` DISPUTED: this fact is contested. It is still the current one.`);
37252
+ } else if (fact.evidential_status === UNKNOWN) {
37253
+ out.push(` Whether it is disputed is UNKNOWN to this mirror, never assume not.`);
37254
+ }
36467
37255
  if (fact.status !== "active") {
36468
37256
  out.push(` STATUS ${fact.status.toUpperCase()}: this fact is no longer current and is not in any index.`);
36469
37257
  }
@@ -36480,24 +37268,45 @@ function artistSlugLookup(state) {
36480
37268
  return (id) => byId.get(id);
36481
37269
  }
36482
37270
  async function syncMemoryFacts(args) {
36483
- const { workspaceSlug, baseUrl, pat, fetchImpl, output, suppressor } = args;
37271
+ const { workspaceSlug, baseUrl, pat, fetchImpl, output, suppressor, gate } = args;
36484
37272
  const now = args.now ?? (() => new Date);
36485
37273
  let state = args.state;
37274
+ const decision = gate ? await gate.decide(now()) : pullDecision(state.memory_facts ?? emptyFactsEntry(now().toISOString()), null);
37275
+ if (!decision.pull) {
37276
+ return { state, written: 0, deleted: 0, refused: 0, facts: 0, pulled: false };
37277
+ }
36486
37278
  let feed;
36487
37279
  try {
36488
37280
  feed = await getMemoryFacts({ baseUrl, pat, fetchImpl });
36489
37281
  } catch (err) {
36490
37282
  if (err instanceof ApiError && err.status === 404) {
36491
37283
  output?.log(`[MEMORY_FACTS ws=${workspaceSlug} skipped reason=route_absent]`);
36492
- return { state, written: 0, deleted: 0, refused: 0, facts: 0 };
37284
+ return { state, written: 0, deleted: 0, refused: 0, facts: 0, pulled: false };
36493
37285
  }
36494
37286
  throw err;
36495
37287
  }
36496
37288
  const slugFor = artistSlugLookup(state);
36497
- const facts = [
36498
- ...feed.workspace_facts.map((row) => toRenderableWorkspaceFact(row, slugFor)),
36499
- ...feed.user_facts.map(toRenderableUserFact)
36500
- ];
37289
+ const sharedIndex = factSlugIndex(feed.workspace_facts);
37290
+ const privateIndex = factSlugIndex(feed.user_facts);
37291
+ let complete = true;
37292
+ const inconsistent = [];
37293
+ const facts = [];
37294
+ for (const row of feed.workspace_facts) {
37295
+ const { fact, consistent } = toRenderableWorkspaceFact(row, slugFor, sharedIndex);
37296
+ if (!consistent)
37297
+ inconsistent.push(fact.slug);
37298
+ facts.push(fact);
37299
+ }
37300
+ for (const row of feed.user_facts) {
37301
+ const { fact, consistent } = toRenderableUserFact(row, privateIndex);
37302
+ if (!consistent)
37303
+ inconsistent.push(fact.slug);
37304
+ facts.push(fact);
37305
+ }
37306
+ if (inconsistent.length > 0) {
37307
+ complete = false;
37308
+ output?.log(`[MEMORY_FACTS ws=${workspaceSlug} inconsistent_trail freshness=unknown ` + `facts=${inconsistent.slice(0, 10).join(",")}` + `${inconsistent.length > 10 ? `+${inconsistent.length - 10}` : ""}]`);
37309
+ }
36501
37310
  const acked = getMemoryFactEntries(state);
36502
37311
  const registry3 = {};
36503
37312
  const live = new Set;
@@ -36505,14 +37314,16 @@ async function syncMemoryFacts(args) {
36505
37314
  let refused = 0;
36506
37315
  for (const fact of facts) {
36507
37316
  let relPath;
37317
+ let rendered;
36508
37318
  try {
36509
37319
  relPath = memoryFactRelPath(fact.pile, fact.slug);
37320
+ rendered = renderFactFile(fact);
36510
37321
  } catch {
36511
37322
  output?.warn(`Memory fact "${fact.slug}" has a slug this mirror cannot write - skipped.`);
37323
+ complete = false;
36512
37324
  continue;
36513
37325
  }
36514
37326
  live.add(relPath);
36515
- const rendered = renderFactFile(fact);
36516
37327
  const renderedHash = stableHash(rendered);
36517
37328
  const onDisk = readMemoryFactFile(workspaceSlug, fact.pile, fact.slug);
36518
37329
  const ackedHash = acked[relPath]?.body_hash;
@@ -36559,15 +37370,46 @@ async function syncMemoryFacts(args) {
36559
37370
  deleted++;
36560
37371
  state = clearMemoryGeneratedRefusal(state, workspaceSlug, abs);
36561
37372
  }
36562
- const { state: next, changed } = setMemoryFactsState(state, {
36563
- fetched_at: now().toISOString(),
36564
- facts: registry3
36565
- });
36566
- state = changed ? commitState(state, next) : state;
37373
+ const bankArgs = {
37374
+ registry: registry3,
37375
+ complete,
37376
+ watermark: feed.watermark ?? null,
37377
+ epochAtFetch: decision.epochAtFetch,
37378
+ now: now()
37379
+ };
37380
+ let bankedFreshness;
37381
+ if (gate) {
37382
+ gate.bank(bankArgs);
37383
+ bankedFreshness = "banked";
37384
+ } else {
37385
+ const banked = bankPull({
37386
+ entry: { ...state.memory_facts ?? decision.entry, facts: registry3 },
37387
+ complete,
37388
+ watermark: bankArgs.watermark,
37389
+ epochAtFetch: decision.epochAtFetch,
37390
+ now: bankArgs.now
37391
+ });
37392
+ const { state: next, changed } = setMemoryFactsState(state, banked);
37393
+ state = changed ? commitState(state, next) : state;
37394
+ writeMarker(args, banked);
37395
+ bankedFreshness = banked.freshness ?? "unknown";
37396
+ }
36567
37397
  if (written > 0 || deleted > 0 || refused > 0) {
36568
- output?.log(`[MEMORY_FACTS ws=${workspaceSlug} facts=${facts.length} written=${written} deleted=${deleted} refused=${refused}]`);
37398
+ output?.log(`[MEMORY_FACTS ws=${workspaceSlug} facts=${facts.length} written=${written} deleted=${deleted} refused=${refused} freshness=${bankedFreshness}]`);
37399
+ }
37400
+ return { state, written, deleted, refused, facts: facts.length, pulled: true };
37401
+ }
37402
+ function writeMarker(args, entry2) {
37403
+ try {
37404
+ writeFreshnessMarkerIfChanged({
37405
+ workspaceSlug: args.workspaceSlug,
37406
+ entry: entry2,
37407
+ suppressor: args.suppressor
37408
+ });
37409
+ } catch (err) {
37410
+ const msg = err instanceof Error ? err.message : String(err);
37411
+ args.output?.log(`[MEMORY_FACTS marker_unwritten reason=${msg}]`);
36569
37412
  }
36570
- return { state, written, deleted, refused, facts: facts.length };
36571
37413
  }
36572
37414
  function writeFact(args, pile, slug, content) {
36573
37415
  const abs = memoryFactFilePath(args.workspaceSlug, pile, slug);
@@ -36595,8 +37437,14 @@ function classifyMemoryFactEvent(args) {
36595
37437
  return { kind: "ignore" };
36596
37438
  if (args.ownEcho)
36597
37439
  return { kind: "echo" };
37440
+ if (args.kind === "unlink") {
37441
+ return args.path.fact !== null && args.known ? { kind: "deleted", relPath: args.path.relPath, pile: args.path.fact.pile } : { kind: "ignore" };
37442
+ }
36598
37443
  if (args.kind !== "upsert")
36599
37444
  return { kind: "ignore" };
37445
+ if (args.path.relPath === memoryFreshnessRelPath()) {
37446
+ return { kind: "marker", relPath: args.path.relPath };
37447
+ }
36600
37448
  if (args.path.fact !== null && args.known) {
36601
37449
  return { kind: "generated", relPath: args.path.relPath, pile: args.path.fact.pile };
36602
37450
  }
@@ -36606,6 +37454,12 @@ function memoryFactEventWarning(v) {
36606
37454
  if (v.kind === "generated") {
36607
37455
  return `${v.relPath} is generated from a memory fact - your edit will be replaced ` + `on the next sync and kept in your conflicts folder. Change the fact with ` + `${factToolsFor(v.pile)}.`;
36608
37456
  }
37457
+ if (v.kind === "deleted") {
37458
+ return `${v.relPath} is generated from a memory fact - deleting the file does not ` + `delete the fact, so it will be written again on the next sync. Remove the ` + `fact itself with ${factToolsFor(v.pile)}.`;
37459
+ }
37460
+ if (v.kind === "marker") {
37461
+ return `${v.relPath} is generated - it reports whether this machine's copy of ` + `your memory facts is up to date. Your edit will be replaced on the next ` + `sync and is not kept.`;
37462
+ }
36609
37463
  if (v.kind === "unowned") {
36610
37464
  return `${v.relPath} is not synced: \`memory/\` holds files Plinth generates from ` + `memory facts. Put your own notes in \`files/\` instead.`;
36611
37465
  }
@@ -36743,7 +37597,7 @@ class FileDestructiveGate {
36743
37597
  }
36744
37598
 
36745
37599
  // src/lib/files/reconcile.ts
36746
- import { readFileSync as readFileSync19, statSync as statSync10 } from "node:fs";
37600
+ import { readFileSync as readFileSync20, statSync as statSync10 } from "node:fs";
36747
37601
 
36748
37602
  // src/lib/unsynced.ts
36749
37603
  import { join as join13, relative as relative8 } from "node:path";
@@ -38777,7 +39631,7 @@ function readBlobForPush(args) {
38777
39631
  }
38778
39632
  let bytes;
38779
39633
  try {
38780
- bytes = readFileSync19(absolutePath);
39634
+ bytes = readFileSync20(absolutePath);
38781
39635
  } catch (err) {
38782
39636
  if (err?.code === "ENOENT")
38783
39637
  return { kind: "gone" };
@@ -39377,6 +40231,1353 @@ async function reconcileArtistMemory(args) {
39377
40231
  // src/commands/start.ts
39378
40232
  init_output();
39379
40233
 
40234
+ // src/lib/memory/revalidate-api.ts
40235
+ var ROUTE2 = "/api/sync/memory/revalidate";
40236
+ var REVALIDATE_MAX_IDS_PER_ARM = 500;
40237
+ var REVALIDATE_TIMEOUT_MS = 20000;
40238
+ var SuccessorSchema = exports_external.object({
40239
+ id: exports_external.string(),
40240
+ slug: exports_external.string(),
40241
+ description: exports_external.string().nullish(),
40242
+ body_md: exports_external.string().nullish(),
40243
+ assertion_revision: exports_external.number().nullish(),
40244
+ support_revision: exports_external.number().nullish()
40245
+ });
40246
+ var WithheldSchema = exports_external.object({ withheld: exports_external.literal(true) });
40247
+ var StaleDependencySchema = exports_external.object({
40248
+ store: exports_external.string(),
40249
+ fact_id: exports_external.string(),
40250
+ slug: exports_external.string(),
40251
+ status: exports_external.string(),
40252
+ evidential_status: exports_external.string().nullish(),
40253
+ retired: exports_external.boolean().nullish(),
40254
+ recorded_assertion_revision: exports_external.number().nullish(),
40255
+ assertion_revision: exports_external.number().nullish(),
40256
+ recorded_support_revision: exports_external.number().nullish(),
40257
+ support_revision: exports_external.number().nullish(),
40258
+ trail_outcome: exports_external.string().nullish(),
40259
+ successor: SuccessorSchema.nullish()
40260
+ });
40261
+ var ProjectionSchema = exports_external.object({
40262
+ projection_kind: exports_external.string().nullish(),
40263
+ projection_id: exports_external.string().nullish(),
40264
+ state: exports_external.string(),
40265
+ state_revision: exports_external.number().nullish(),
40266
+ retryable: exports_external.boolean().nullish(),
40267
+ invalidated_at: exports_external.string().nullish(),
40268
+ invalidated_by: exports_external.union([WithheldSchema, exports_external.object({ store: exports_external.string(), fact_id: exports_external.string() })]).nullish(),
40269
+ refreshed_at: exports_external.string().nullish(),
40270
+ refresh_attempts: exports_external.number().nullish(),
40271
+ last_refresh_failed_at: exports_external.string().nullish(),
40272
+ dependencies_not_current: exports_external.array(exports_external.union([WithheldSchema, StaleDependencySchema])).nullish()
40273
+ });
40274
+ var CorrectionSchema = exports_external.object({
40275
+ store: exports_external.string(),
40276
+ fact_id: exports_external.string(),
40277
+ slug: exports_external.string(),
40278
+ status: exports_external.string(),
40279
+ evidential_status: exports_external.string().nullish(),
40280
+ retired: exports_external.boolean().nullish(),
40281
+ evidence_id: exports_external.string(),
40282
+ heading_or_span: exports_external.string().nullish(),
40283
+ excerpt: exports_external.string().nullish(),
40284
+ redacted: exports_external.boolean().nullish(),
40285
+ trail_outcome: exports_external.string().nullish(),
40286
+ successor: SuccessorSchema.nullish()
40287
+ });
40288
+ var CorrectionsFieldSchema = exports_external.object({
40289
+ coverage: exports_external.string(),
40290
+ items: exports_external.array(CorrectionSchema)
40291
+ });
40292
+ var RevalidatedDocumentSchema = exports_external.object({
40293
+ id: exports_external.string(),
40294
+ projection: ProjectionSchema.nullish(),
40295
+ corrections: CorrectionsFieldSchema.nullish()
40296
+ });
40297
+ var RevalidatedWorkspaceFileSchema = exports_external.object({
40298
+ id: exports_external.string(),
40299
+ corrections: CorrectionsFieldSchema.nullish()
40300
+ });
40301
+ var RevalidateEnvelopeSchema = exports_external.object({
40302
+ documents: exports_external.array(exports_external.unknown()).nullish(),
40303
+ workspace_files: exports_external.array(exports_external.unknown()).nullish(),
40304
+ unavailable: exports_external.array(exports_external.string()).nullish()
40305
+ });
40306
+ function canonicaliseIds(ids) {
40307
+ const seen = new Set;
40308
+ const out = [];
40309
+ for (const raw of ids) {
40310
+ const id = raw.toLowerCase();
40311
+ if (seen.has(id))
40312
+ continue;
40313
+ seen.add(id);
40314
+ out.push(id);
40315
+ }
40316
+ return out;
40317
+ }
40318
+ function chunkRevalidateBatches(args) {
40319
+ const max = args.max ?? REVALIDATE_MAX_IDS_PER_ARM;
40320
+ const docs = canonicaliseIds(args.documents);
40321
+ const files = canonicaliseIds(args.workspaceFiles);
40322
+ const batches = [];
40323
+ const count = Math.max(Math.ceil(docs.length / max), Math.ceil(files.length / max));
40324
+ for (let i = 0;i < count; i++) {
40325
+ const documents = docs.slice(i * max, (i + 1) * max);
40326
+ const workspace_files = files.slice(i * max, (i + 1) * max);
40327
+ if (documents.length === 0 && workspace_files.length === 0)
40328
+ continue;
40329
+ batches.push({ documents, workspace_files });
40330
+ }
40331
+ return batches;
40332
+ }
40333
+ function classifyStatus(status) {
40334
+ if (status === 401 || status === 403)
40335
+ return "unauthorised";
40336
+ if (status === 429)
40337
+ return "rate_limited";
40338
+ if (status === 503)
40339
+ return "unavailable";
40340
+ if (status >= 400 && status < 500)
40341
+ return "bad_request";
40342
+ return "server_error";
40343
+ }
40344
+ async function revalidateMemorySources(args) {
40345
+ const { baseUrl, pat, fetchImpl = fetch } = args;
40346
+ const timeoutMs = args.timeoutMs ?? REVALIDATE_TIMEOUT_MS;
40347
+ const url2 = `${baseUrl.replace(/\/$/, "")}${ROUTE2}`;
40348
+ const documents = canonicaliseIds(args.documents);
40349
+ const workspace_files = canonicaliseIds(args.workspaceFiles);
40350
+ if (documents.length === 0 && workspace_files.length === 0) {
40351
+ return { ok: true, documents: [], workspace_files: [], unavailable: [] };
40352
+ }
40353
+ const payload = {};
40354
+ if (documents.length > 0)
40355
+ payload.documents = documents;
40356
+ if (workspace_files.length > 0)
40357
+ payload.workspace_files = workspace_files;
40358
+ const controller = new AbortController;
40359
+ const deadline = setTimeout(() => {
40360
+ controller.abort();
40361
+ }, timeoutMs);
40362
+ let res;
40363
+ try {
40364
+ res = await fetchImpl(url2, {
40365
+ method: "POST",
40366
+ headers: {
40367
+ Authorization: `Bearer ${pat}`,
40368
+ Accept: "application/json",
40369
+ "Content-Type": "application/json"
40370
+ },
40371
+ body: JSON.stringify(payload),
40372
+ signal: controller.signal
40373
+ });
40374
+ } catch {
40375
+ return { ok: false, reason: "offline", status: 0 };
40376
+ } finally {
40377
+ clearTimeout(deadline);
40378
+ }
40379
+ if (!res.ok) {
40380
+ const reason = classifyStatus(res.status);
40381
+ const retryAfterMs = res.status === 429 ? parseRetryAfterMs(res.headers.get("retry-after")) ?? await retryAfterFromBody(res) : undefined;
40382
+ return { ok: false, reason, status: res.status, retryAfterMs };
40383
+ }
40384
+ let body;
40385
+ try {
40386
+ body = await res.json();
40387
+ } catch {
40388
+ return { ok: false, reason: "malformed", status: res.status };
40389
+ }
40390
+ const parsed = RevalidateEnvelopeSchema.safeParse(body);
40391
+ if (!parsed.success) {
40392
+ return { ok: false, reason: "malformed", status: res.status };
40393
+ }
40394
+ const keepValid = (rows, schema) => {
40395
+ const out = [];
40396
+ for (const row of rows ?? []) {
40397
+ const one = schema.safeParse(row);
40398
+ if (one.success)
40399
+ out.push(one.data);
40400
+ }
40401
+ return out;
40402
+ };
40403
+ return {
40404
+ ok: true,
40405
+ documents: keepValid(parsed.data.documents, RevalidatedDocumentSchema),
40406
+ workspace_files: keepValid(parsed.data.workspace_files, RevalidatedWorkspaceFileSchema),
40407
+ unavailable: parsed.data.unavailable ?? []
40408
+ };
40409
+ }
40410
+ async function retryAfterFromBody(res) {
40411
+ try {
40412
+ const body = await res.json();
40413
+ const seconds = body?.retry_after_seconds;
40414
+ if (typeof seconds === "number" && Number.isFinite(seconds) && seconds >= 0) {
40415
+ return Math.round(seconds * 1000);
40416
+ }
40417
+ } catch {}
40418
+ return;
40419
+ }
40420
+
40421
+ // src/lib/memory/revalidate.ts
40422
+ function sourceAnswerChanged(before, after) {
40423
+ if (before === undefined)
40424
+ return true;
40425
+ if (before.state !== after.state)
40426
+ return true;
40427
+ if (before.state_revision !== after.state_revision)
40428
+ return true;
40429
+ if (before.corrections_hash !== after.corrections_hash)
40430
+ return true;
40431
+ return before.rendered !== after.rendered;
40432
+ }
40433
+ function correctionsDigest(items) {
40434
+ if (items === null)
40435
+ return null;
40436
+ const sorted = [...items].sort((a, b) => a.evidence_id < b.evidence_id ? -1 : a.evidence_id > b.evidence_id ? 1 : 0);
40437
+ return stableHash(sorted);
40438
+ }
40439
+ function toEntry(args) {
40440
+ const { id, annotation } = args;
40441
+ const lines = renderAnnotationLines(annotation);
40442
+ return {
40443
+ id,
40444
+ state: annotation.state,
40445
+ state_revision: annotation.stateRevision,
40446
+ retryable: annotation.retryable,
40447
+ corrections_hash: correctionsDigest(annotation.corrections),
40448
+ corrections_count: annotation.corrections?.length ?? null,
40449
+ checked_at: annotation.checkedAt,
40450
+ rendered: lines.join(`
40451
+ `)
40452
+ };
40453
+ }
40454
+ function withoutStamp(rendered) {
40455
+ return rendered.split(`
40456
+ `).filter((l) => !l.startsWith("memory_checked_at:")).join(`
40457
+ `);
40458
+ }
40459
+ function sameAnswer(prior, next) {
40460
+ if (prior === undefined)
40461
+ return false;
40462
+ return prior.state === next.state && prior.state_revision === next.state_revision && prior.corrections_hash === next.corrections_hash && withoutStamp(prior.rendered) === withoutStamp(next.rendered);
40463
+ }
40464
+ function answer(args) {
40465
+ let entry2 = toEntry({ id: args.id, annotation: args.annotation });
40466
+ if (args.prior !== undefined && sameAnswer(args.prior, entry2)) {
40467
+ entry2 = toEntry({
40468
+ id: args.id,
40469
+ annotation: { ...args.annotation, checkedAt: args.prior.checked_at }
40470
+ });
40471
+ }
40472
+ return {
40473
+ arm: args.arm,
40474
+ id: args.id,
40475
+ entry: entry2,
40476
+ lines: entry2.rendered === "" ? [] : entry2.rendered.split(`
40477
+ `)
40478
+ };
40479
+ }
40480
+ function decideFromOutcome(args) {
40481
+ const { outcome, now } = args;
40482
+ const checkedAt = now.toISOString();
40483
+ const answers = [];
40484
+ if (!outcome.ok) {
40485
+ for (const id of args.askedDocuments) {
40486
+ answers.push({
40487
+ ...answer({
40488
+ arm: "documents",
40489
+ id,
40490
+ annotation: unknownAnnotation(args.priorDocuments[id]?.checked_at ?? null),
40491
+ prior: args.priorDocuments[id]
40492
+ })
40493
+ });
40494
+ }
40495
+ for (const id of args.askedWorkspaceFiles) {
40496
+ answers.push({
40497
+ ...answer({
40498
+ arm: "workspace_files",
40499
+ id,
40500
+ annotation: unknownAnnotation(args.priorWorkspaceFiles[id]?.checked_at ?? null, "workspace_file"),
40501
+ prior: args.priorWorkspaceFiles[id]
40502
+ })
40503
+ });
40504
+ }
40505
+ return { answers, unavailable: [], served: false };
40506
+ }
40507
+ const docById = new Map(outcome.documents.map((d) => [d.id.toLowerCase(), d]));
40508
+ const fileById = new Map(outcome.workspace_files.map((f) => [f.id.toLowerCase(), f]));
40509
+ for (const id of args.askedDocuments) {
40510
+ const served = docById.get(id);
40511
+ if (served === undefined) {
40512
+ answers.push(answer({
40513
+ arm: "documents",
40514
+ id,
40515
+ annotation: unknownAnnotation(args.priorDocuments[id]?.checked_at ?? null),
40516
+ prior: args.priorDocuments[id]
40517
+ }));
40518
+ continue;
40519
+ }
40520
+ answers.push(answer({
40521
+ arm: "documents",
40522
+ id,
40523
+ annotation: annotationForDocument({
40524
+ projection: served.projection,
40525
+ corrections: served.corrections,
40526
+ checkedAt
40527
+ }),
40528
+ prior: args.priorDocuments[id]
40529
+ }));
40530
+ }
40531
+ for (const id of args.askedWorkspaceFiles) {
40532
+ const served = fileById.get(id);
40533
+ if (served === undefined) {
40534
+ answers.push(answer({
40535
+ arm: "workspace_files",
40536
+ id,
40537
+ annotation: unknownAnnotation(args.priorWorkspaceFiles[id]?.checked_at ?? null, "workspace_file"),
40538
+ prior: args.priorWorkspaceFiles[id]
40539
+ }));
40540
+ continue;
40541
+ }
40542
+ answers.push(answer({
40543
+ arm: "workspace_files",
40544
+ id,
40545
+ annotation: annotationForWorkspaceFile({
40546
+ corrections: served.corrections,
40547
+ checkedAt
40548
+ }),
40549
+ prior: args.priorWorkspaceFiles[id]
40550
+ }));
40551
+ }
40552
+ return { answers, unavailable: outcome.unavailable, served: true };
40553
+ }
40554
+ async function runRevalidate(args) {
40555
+ const now = args.now ?? (() => new Date);
40556
+ const batches = chunkRevalidateBatches({
40557
+ documents: args.documentIds,
40558
+ workspaceFiles: args.workspaceFileIds
40559
+ });
40560
+ const answers = [];
40561
+ const unavailable = new Set;
40562
+ let anyServed = false;
40563
+ for (const batch of batches) {
40564
+ const outcome = await revalidateMemorySources({
40565
+ baseUrl: args.baseUrl,
40566
+ pat: args.pat,
40567
+ documents: batch.documents,
40568
+ workspaceFiles: batch.workspace_files,
40569
+ fetchImpl: args.fetchImpl
40570
+ });
40571
+ const decided = decideFromOutcome({
40572
+ outcome,
40573
+ askedDocuments: batch.documents,
40574
+ askedWorkspaceFiles: batch.workspace_files,
40575
+ priorDocuments: args.priorDocuments,
40576
+ priorWorkspaceFiles: args.priorWorkspaceFiles,
40577
+ now: now()
40578
+ });
40579
+ answers.push(...decided.answers);
40580
+ for (const u of decided.unavailable)
40581
+ unavailable.add(u);
40582
+ if (decided.served)
40583
+ anyServed = true;
40584
+ if (!outcome.ok && outcome.reason === "rate_limited") {
40585
+ args.output?.log(`[MEMORY_REVALIDATE rate_limited retry_after_ms=${outcome.retryAfterMs ?? "unset"} remaining_batches=${batches.length - batches.indexOf(batch) - 1}]`);
40586
+ for (const later of batches.slice(batches.indexOf(batch) + 1)) {
40587
+ const rest = decideFromOutcome({
40588
+ outcome,
40589
+ askedDocuments: later.documents,
40590
+ askedWorkspaceFiles: later.workspace_files,
40591
+ priorDocuments: args.priorDocuments,
40592
+ priorWorkspaceFiles: args.priorWorkspaceFiles,
40593
+ now: now()
40594
+ });
40595
+ answers.push(...rest.answers);
40596
+ }
40597
+ break;
40598
+ }
40599
+ }
40600
+ const changed = answers.filter((a) => sourceAnswerChanged(a.arm === "documents" ? args.priorDocuments[a.id] : args.priorWorkspaceFiles[a.id], a.entry));
40601
+ return {
40602
+ answers,
40603
+ changed,
40604
+ unavailable: [...unavailable].sort(),
40605
+ whollyUnknown: !anyServed && answers.length > 0
40606
+ };
40607
+ }
40608
+ function degradeAllToUnknown(args) {
40609
+ const answers = [];
40610
+ const docIds = new Set([
40611
+ ...Object.keys(args.priorDocuments),
40612
+ ...args.heldDocumentIds ?? []
40613
+ ]);
40614
+ const fileIds = new Set([
40615
+ ...Object.keys(args.priorWorkspaceFiles),
40616
+ ...args.heldWorkspaceFileIds ?? []
40617
+ ]);
40618
+ for (const id of docIds) {
40619
+ answers.push(answer({
40620
+ arm: "documents",
40621
+ id,
40622
+ annotation: unknownAnnotation(args.priorDocuments[id]?.checked_at ?? null)
40623
+ }));
40624
+ }
40625
+ for (const id of fileIds) {
40626
+ answers.push(answer({
40627
+ arm: "workspace_files",
40628
+ id,
40629
+ annotation: unknownAnnotation(args.priorWorkspaceFiles[id]?.checked_at ?? null, "workspace_file")
40630
+ }));
40631
+ }
40632
+ const changed = answers.filter((a) => sourceAnswerChanged(a.arm === "documents" ? args.priorDocuments[a.id] : args.priorWorkspaceFiles[a.id], a.entry));
40633
+ return { answers, changed, unavailable: [], whollyUnknown: answers.length > 0 };
40634
+ }
40635
+ function applyDecision(args) {
40636
+ const documents = { ...args.prior?.documents ?? {} };
40637
+ const workspace_files = { ...args.prior?.workspace_files ?? {} };
40638
+ for (const a of args.decision.answers) {
40639
+ if (args.unwritten?.has(`${a.arm}:${a.id}`))
40640
+ continue;
40641
+ if (a.arm === "documents")
40642
+ documents[a.id] = a.entry;
40643
+ else
40644
+ workspace_files[a.id] = a.entry;
40645
+ }
40646
+ return {
40647
+ polled_at: args.polledAt,
40648
+ ask_failed_at: args.askFailedAt ?? null,
40649
+ write_interrupted: args.prior?.write_interrupted,
40650
+ unwritten: args.prior?.unwritten,
40651
+ documents,
40652
+ workspace_files
40653
+ };
40654
+ }
40655
+ function pruneUnheld(args) {
40656
+ const held = {
40657
+ documents: new Set(args.documentIds.map((i) => i.toLowerCase())),
40658
+ workspace_files: new Set(args.workspaceFileIds.map((i) => i.toLowerCase()))
40659
+ };
40660
+ const keep = (arm) => Object.fromEntries(Object.entries(args.state[arm]).filter(([id]) => held[arm].has(id)));
40661
+ return {
40662
+ polled_at: args.state.polled_at,
40663
+ ask_failed_at: args.state.ask_failed_at ?? null,
40664
+ write_interrupted: args.state.write_interrupted,
40665
+ unwritten: args.state.unwritten,
40666
+ documents: keep("documents"),
40667
+ workspace_files: keep("workspace_files")
40668
+ };
40669
+ }
40670
+ function shouldRevalidate(args) {
40671
+ if (args.anyUnknown) {
40672
+ if (args.askFailedAt === undefined || args.askFailedAt === null)
40673
+ return true;
40674
+ const failed = Date.parse(args.askFailedAt);
40675
+ if (Number.isNaN(failed))
40676
+ return true;
40677
+ return args.now.getTime() - failed >= (args.unknownRetryMs ?? UNKNOWN_RETRY_MS);
40678
+ }
40679
+ if (args.polledAt === undefined)
40680
+ return true;
40681
+ const last = Date.parse(args.polledAt);
40682
+ if (Number.isNaN(last))
40683
+ return true;
40684
+ return args.now.getTime() - last >= args.intervalMs;
40685
+ }
40686
+ var UNKNOWN_RETRY_MS = 60000;
40687
+ function anyUnknown(state) {
40688
+ if (state === undefined)
40689
+ return false;
40690
+ const isUnknown = (e) => e.state === "unknown";
40691
+ return Object.values(state.documents).some(isUnknown) || Object.values(state.workspace_files).some(isUnknown);
40692
+ }
40693
+
40694
+ // src/lib/memory/write.ts
40695
+ import { readFileSync as readFileSync21, statSync as statSync11 } from "node:fs";
40696
+
40697
+ // src/lib/files/extracted-text.ts
40698
+ import { createHash as createHash4 } from "node:crypto";
40699
+ var DERIVED_TEXT_VERSION = 2;
40700
+ function yamlString3(value) {
40701
+ return JSON.stringify(value);
40702
+ }
40703
+ var PARTIAL_WARNING = [
40704
+ "> ⚠️ **PARTIAL EXTRACTION.** Some spans of this document came out as text and",
40705
+ "> some did not. Which spans is NOT recorded, so this could be a clean document",
40706
+ "> with mangled party names, or clean prose that degrades part-way through.",
40707
+ "> Verify anything load-bearing against the source file."
40708
+ ].join(`
40709
+ `);
40710
+ var NO_LOCAL_COPY_NOTE = [
40711
+ "> ℹ️ **NO LOCAL COPY OF THIS FILE.** Plinth does not hold this file's bytes, so",
40712
+ "> nothing was written under `files/` for it and the text below is the only copy",
40713
+ "> in this mirror. The original lives wherever `source_uri` above points."
40714
+ ].join(`
40715
+ `);
40716
+ function garbledBody(relPath, bytesInMirror) {
40717
+ return [
40718
+ "> \uD83D\uDD34 **NOT TEXT — body withheld.**",
40719
+ ">",
40720
+ "> Extraction produced characters that are not words. The usual cause is a PDF",
40721
+ "> whose embedded fonts carry no ToUnicode map, so the glyph indices come out",
40722
+ "> where the letters should be: a page reading *“Dated this 10th day of",
40723
+ "> September”* extracts as `):-,:019`.",
40724
+ ">",
40725
+ "> The characters are NOT reproduced here on purpose. They read as text to a",
40726
+ "> reader and to a search index, and they say nothing — writing them into the",
40727
+ "> mirror would put a document into your corpus that appears readable and is",
40728
+ "> not. They are excluded from Plinth's own search index for the same reason.",
40729
+ ">",
40730
+ bytesInMirror ? `> **Read the original instead:** \`files/${relPath}\`` : "> **There is no local original to read** — Plinth holds no bytes for this file." + " Follow `source_uri` above to the copy that has them."
40731
+ ].join(`
40732
+ `);
40733
+ }
40734
+ function renderExtractedText(doc2) {
40735
+ const withheld = doc2.quality === "garbled";
40736
+ const frontmatter = [
40737
+ "---",
40738
+ "plinth_generated: true",
40739
+ `source_file: ${doc2.bytesInMirror ? yamlString3(`files/${doc2.relPath}`) : "null"}`,
40740
+ `source_uri: ${doc2.sourceUri === undefined ? "unknown" : doc2.sourceUri === null ? "null" : yamlString3(doc2.sourceUri)}`,
40741
+ `content_quality: ${doc2.quality ?? "unchecked"}`,
40742
+ `body_withheld: ${withheld}`,
40743
+ `extracted_chars: ${withheld ? 0 : doc2.text.length}`,
40744
+ "---",
40745
+ "",
40746
+ "<!-- Generated by Plinth from the source file above. This file is DERIVED:",
40747
+ " your edits here are overwritten on the next sync and are never uploaded.",
40748
+ " Edit the source file instead. -->",
40749
+ ""
40750
+ ].join(`
40751
+ `);
40752
+ if (withheld) {
40753
+ return `${frontmatter}${garbledBody(doc2.relPath, doc2.bytesInMirror)}
40754
+ `;
40755
+ }
40756
+ const notices = [
40757
+ ...doc2.quality === "partial" ? [PARTIAL_WARNING] : [],
40758
+ ...doc2.bytesInMirror ? [] : [NO_LOCAL_COPY_NOTE]
40759
+ ];
40760
+ if (notices.length > 0) {
40761
+ return `${frontmatter}${notices.join(`
40762
+
40763
+ `)}
40764
+
40765
+ ${doc2.text}
40766
+ `;
40767
+ }
40768
+ return `${frontmatter}${doc2.text}
40769
+ `;
40770
+ }
40771
+ function extractedTextHash(rendered) {
40772
+ return createHash4("sha256").update(rendered, "utf8").digest("hex");
40773
+ }
40774
+
40775
+ // src/lib/memory/splice.ts
40776
+ var import_gray_matter11 = __toESM(require_gray_matter(), 1);
40777
+ var OPEN_RE = /^---\r?\n/;
40778
+ function findFrontmatter(content) {
40779
+ const open2 = OPEN_RE.exec(content);
40780
+ if (!open2)
40781
+ return null;
40782
+ const start = open2[0].length;
40783
+ const closeRe = /\r?\n---[ \t]*(\r?\n|$)/;
40784
+ const rest = content.slice(start);
40785
+ const close = closeRe.exec(rest);
40786
+ if (!close)
40787
+ return null;
40788
+ return {
40789
+ start,
40790
+ end: start + close.index,
40791
+ text: rest.slice(0, close.index)
40792
+ };
40793
+ }
40794
+ function isAnnotationKeyLine(line) {
40795
+ for (const key of MEMORY_ANNOTATION_KEYS) {
40796
+ if (line.startsWith(`${key}:`))
40797
+ return true;
40798
+ }
40799
+ return false;
40800
+ }
40801
+ function isContinuation(line) {
40802
+ return /^[ \t]+\S/.test(line) || line.trimEnd() === "";
40803
+ }
40804
+ function stripAnnotation(lines) {
40805
+ const out = [];
40806
+ let dropping = false;
40807
+ let pendingBlanks = [];
40808
+ for (const line of lines) {
40809
+ if (isAnnotationKeyLine(line)) {
40810
+ dropping = true;
40811
+ pendingBlanks = [];
40812
+ continue;
40813
+ }
40814
+ if (dropping) {
40815
+ if (line.trimEnd() === "") {
40816
+ pendingBlanks.push(line);
40817
+ continue;
40818
+ }
40819
+ if (isContinuation(line)) {
40820
+ pendingBlanks = [];
40821
+ continue;
40822
+ }
40823
+ dropping = false;
40824
+ out.push(...pendingBlanks);
40825
+ pendingBlanks = [];
40826
+ }
40827
+ out.push(line);
40828
+ }
40829
+ return out;
40830
+ }
40831
+ function spliceAnnotation(content, lines) {
40832
+ const fm = findFrontmatter(content);
40833
+ if (fm === null)
40834
+ return { content, changed: false };
40835
+ const existing = fm.text.split(`
40836
+ `);
40837
+ const kept = stripAnnotation(existing);
40838
+ const memberBlanks = [];
40839
+ while (kept.length > 0 && kept[kept.length - 1].trimEnd() === "") {
40840
+ memberBlanks.unshift(kept.pop());
40841
+ }
40842
+ const eol = fm.text.includes(`\r
40843
+ `) || content.startsWith(`---\r
40844
+ `) ? `\r
40845
+ ` : `
40846
+ `;
40847
+ const withEol = (l) => eol === `\r
40848
+ ` && !l.endsWith("\r") ? `${l}\r` : l;
40849
+ const next = lines.length === 0 ? [...kept, ...memberBlanks] : [...kept, ...memberBlanks, ...lines.map((l) => withEol(l))];
40850
+ const nextText = next.join(`
40851
+ `);
40852
+ if (nextText === fm.text)
40853
+ return { content, changed: false };
40854
+ const out = content.slice(0, fm.start) + nextText + content.slice(fm.end);
40855
+ if (!parsesAsFrontmatter(out) || bodyAfterFrontmatter(out) !== bodyAfterFrontmatter(content)) {
40856
+ return { content, changed: false };
40857
+ }
40858
+ return { content: out, changed: out !== content };
40859
+ }
40860
+ function parsesAsFrontmatter(content) {
40861
+ try {
40862
+ import_gray_matter11.default(content, { cache: false });
40863
+ return true;
40864
+ } catch {
40865
+ return false;
40866
+ }
40867
+ }
40868
+ function bodyAfterFrontmatter(content) {
40869
+ const fm = findFrontmatter(content);
40870
+ if (fm === null)
40871
+ return content;
40872
+ const rest = content.slice(fm.end);
40873
+ const close = /^\r?\n---[ \t]*(\r?\n|$)/.exec(rest);
40874
+ return close ? rest.slice(close[0].length) : rest;
40875
+ }
40876
+
40877
+ // src/lib/memory/write.ts
40878
+ function writeDocumentAnnotation(args) {
40879
+ const strategy = getStrategy("documents");
40880
+ if (strategy === undefined)
40881
+ return { kind: "absent" };
40882
+ let target;
40883
+ try {
40884
+ target = entityFilePath(args.workspaceSlug, "documents", args.slug);
40885
+ } catch {
40886
+ return { kind: "absent" };
40887
+ }
40888
+ let current;
40889
+ try {
40890
+ current = readFileSync21(target, "utf8");
40891
+ } catch {
40892
+ return { kind: "absent" };
40893
+ }
40894
+ const before = computeMirrorHashes(strategy, current);
40895
+ if (before.bodyHash !== args.entry.body_hash || before.frontmatterHash !== args.entry.frontmatter_hash) {
40896
+ return { kind: "diverged", relPath: strategy.mirrorRelativePath(args.slug) };
40897
+ }
40898
+ const spliced = spliceAnnotation(current, args.lines);
40899
+ if (!spliced.changed)
40900
+ return { kind: "unchanged" };
40901
+ const after = computeMirrorHashes(strategy, spliced.content);
40902
+ args.suppressor?.registerWrite(target, {
40903
+ bodyHash: after.bodyHash,
40904
+ frontmatterHash: after.frontmatterHash
40905
+ });
40906
+ writeMirrorFile(args.workspaceSlug, "documents", args.slug, spliced.content);
40907
+ return { kind: "written" };
40908
+ }
40909
+ function inodeAfterWrite(workspaceSlug, entityType, slug) {
40910
+ try {
40911
+ return statSync11(entityFilePath(workspaceSlug, entityType, slug)).ino;
40912
+ } catch {
40913
+ return;
40914
+ }
40915
+ }
40916
+ function onDiskTextHash(workspaceSlug, relPath) {
40917
+ try {
40918
+ const current = readExtractedText(workspaceSlug, relPath);
40919
+ return current === null ? undefined : extractedTextHash(current);
40920
+ } catch {
40921
+ return;
40922
+ }
40923
+ }
40924
+ function writeFileTextAnnotation(args) {
40925
+ const current = readExtractedText(args.workspaceSlug, args.relPath);
40926
+ if (current === null)
40927
+ return { kind: "absent" };
40928
+ if (args.textHash !== undefined && extractedTextHash(current) !== args.textHash) {
40929
+ return { kind: "diverged", relPath: extractedTextRelPath(args.relPath) };
40930
+ }
40931
+ const spliced = spliceAnnotation(current, args.lines);
40932
+ if (!spliced.changed)
40933
+ return { kind: "unchanged" };
40934
+ writeExtractedText(args.workspaceSlug, args.relPath, spliced.content);
40935
+ return { kind: "written", textHash: extractedTextHash(spliced.content) };
40936
+ }
40937
+ function divergedNotice(relPath, state) {
40938
+ return `${relPath}: its memory context is now "${state}", but the file has local ` + `edits so the marker was not written into it. Your edits are untouched. ` + `Resolve the edit and the next sync writes the marker.`;
40939
+ }
40940
+ function unreadableNotice(relPath, why) {
40941
+ const reason = why.replace(/\s+/g, " ").trim().slice(0, 120);
40942
+ return `${relPath}: its memory context could not be written because the file's ` + `frontmatter could not be read (${reason}). The file is untouched. Fix the ` + `frontmatter and the next sync writes the marker.`;
40943
+ }
40944
+
40945
+ // src/lib/memory/lane.ts
40946
+ var DEFAULT_REVALIDATE_INTERVAL_MS = 300000;
40947
+ function revalidateIntervalMs() {
40948
+ const raw = process.env.PLINTH_REVALIDATE_INTERVAL_MS;
40949
+ if (raw !== undefined) {
40950
+ const n = Number(raw);
40951
+ if (Number.isFinite(n) && n > 0)
40952
+ return n;
40953
+ }
40954
+ return DEFAULT_REVALIDATE_INTERVAL_MS;
40955
+ }
40956
+ var EMPTY = { documents: {}, workspace_files: {} };
40957
+ function heldDocuments(state) {
40958
+ const entries = state.entries["documents"] ?? {};
40959
+ return Object.values(entries).map((e) => ({ id: e.id.toLowerCase(), slug: e.slug }));
40960
+ }
40961
+ function heldFileTexts(state) {
40962
+ const files = state.files ?? {};
40963
+ return Object.values(files).filter((f) => f.text_hash !== undefined).map((f) => ({
40964
+ id: f.id.toLowerCase(),
40965
+ relPath: f.path,
40966
+ textHash: f.text_hash
40967
+ }));
40968
+ }
40969
+ async function writeChanged(args) {
40970
+ const docs = new Map(heldDocuments(args.state).map((d) => [d.id, d.slug]));
40971
+ const texts = new Map(heldFileTexts(args.state).map((f) => [f.id, f]));
40972
+ let written = 0;
40973
+ let diverged = 0;
40974
+ const notices = [];
40975
+ const textHashes = new Map;
40976
+ const inodes = new Map;
40977
+ const unwritten = new Set;
40978
+ const report = [];
40979
+ const clear = [];
40980
+ for (const answer2 of args.changed) {
40981
+ if (args.abortIfStale?.() === true) {
40982
+ args.output?.log("[MEMORY_REVALIDATE stopped reason=poll_failed_mid_write]");
40983
+ return { written, diverged, notices, textHashes, inodes, unwritten, report, clear, aborted: true };
40984
+ }
40985
+ if (answer2.arm === "documents") {
40986
+ const slug = docs.get(answer2.id);
40987
+ if (slug === undefined) {
40988
+ unwritten.add(`documents:${answer2.id}`);
40989
+ continue;
40990
+ }
40991
+ const entryAt = (state) => Object.values(state.entries["documents"] ?? {}).find((e) => e.id.toLowerCase() === answer2.id);
40992
+ const entry2 = entryAt(args.state);
40993
+ if (entry2 === undefined) {
40994
+ unwritten.add(`documents:${answer2.id}`);
40995
+ continue;
40996
+ }
40997
+ let outcome2;
40998
+ try {
40999
+ const under = await args.mutex.run(entry2.id, () => {
41000
+ if (args.abortIfStale?.() === true)
41001
+ return Promise.resolve("stale");
41002
+ const fresh = args.getState !== undefined ? entryAt(args.getState()) : undefined;
41003
+ const res = writeDocumentAnnotation({
41004
+ workspaceSlug: args.workspaceSlug,
41005
+ slug,
41006
+ entry: fresh ?? entry2,
41007
+ lines: answer2.lines,
41008
+ suppressor: args.suppressor,
41009
+ output: args.output
41010
+ });
41011
+ if (res.kind === "written") {
41012
+ res.inode = inodeAfterWrite(args.workspaceSlug, "documents", slug);
41013
+ }
41014
+ if (res.kind !== "diverged" && res.kind !== "absent") {
41015
+ args.onWritten?.("documents", answer2.id);
41016
+ }
41017
+ return Promise.resolve(res);
41018
+ });
41019
+ if (under === "stale") {
41020
+ args.output?.log("[MEMORY_REVALIDATE stopped reason=poll_failed_in_lock]");
41021
+ return { written, diverged, notices, textHashes, inodes, unwritten, report, clear, aborted: true };
41022
+ }
41023
+ outcome2 = under;
41024
+ } catch (err) {
41025
+ unwritten.add(`documents:${answer2.id}`);
41026
+ const why = err instanceof Error ? err.message : String(err);
41027
+ args.output?.log(`memory: could not annotate ${slug} (${why})`);
41028
+ report.push({
41029
+ path: getStrategy("documents").mirrorRelativePath(slug),
41030
+ record: unreadableNotice(getStrategy("documents").mirrorRelativePath(slug), why)
41031
+ });
41032
+ continue;
41033
+ }
41034
+ if (outcome2.kind === "diverged" || outcome2.kind === "absent") {
41035
+ unwritten.add(`documents:${answer2.id}`);
41036
+ }
41037
+ const docPath = getStrategy("documents").mirrorRelativePath(slug);
41038
+ if (outcome2.kind === "diverged") {
41039
+ report.push({ path: docPath, record: divergedNotice(docPath, answer2.entry.state) });
41040
+ } else {
41041
+ clear.push(docPath);
41042
+ }
41043
+ if (outcome2.kind === "written") {
41044
+ written++;
41045
+ if (outcome2.inode !== undefined) {
41046
+ inodes.set(entry2.id, { slug, to: outcome2.inode });
41047
+ }
41048
+ } else if (outcome2.kind === "diverged") {
41049
+ diverged++;
41050
+ notices.push(divergedNotice(outcome2.relPath, answer2.entry.state));
41051
+ }
41052
+ continue;
41053
+ }
41054
+ const file2 = texts.get(answer2.id);
41055
+ if (file2 === undefined) {
41056
+ unwritten.add(`workspace_files:${answer2.id}`);
41057
+ continue;
41058
+ }
41059
+ let outcome;
41060
+ try {
41061
+ const under = await args.mutex.run(`files:${file2.relPath}`, () => {
41062
+ if (args.abortIfStale?.() === true)
41063
+ return Promise.resolve("stale");
41064
+ const res = writeFileTextAnnotation({
41065
+ workspaceSlug: args.workspaceSlug,
41066
+ relPath: file2.relPath,
41067
+ textHash: args.freshTextHashes?.get(answer2.id) ?? file2.textHash,
41068
+ lines: answer2.lines
41069
+ });
41070
+ if (res.kind !== "diverged" && res.kind !== "absent") {
41071
+ args.onWritten?.("workspace_files", answer2.id, res.textHash);
41072
+ }
41073
+ return Promise.resolve(res);
41074
+ });
41075
+ if (under === "stale") {
41076
+ args.output?.log("[MEMORY_REVALIDATE stopped reason=poll_failed_in_lock]");
41077
+ return { written, diverged, notices, textHashes, inodes, unwritten, report, clear, aborted: true };
41078
+ }
41079
+ outcome = under;
41080
+ } catch (err) {
41081
+ unwritten.add(`workspace_files:${answer2.id}`);
41082
+ const why = err instanceof Error ? err.message : String(err);
41083
+ args.output?.log(`memory: could not annotate ${file2.relPath} (${why})`);
41084
+ report.push({
41085
+ path: extractedTextRelPath(file2.relPath),
41086
+ record: unreadableNotice(extractedTextRelPath(file2.relPath), why)
41087
+ });
41088
+ continue;
41089
+ }
41090
+ if (outcome.kind === "diverged" || outcome.kind === "absent") {
41091
+ unwritten.add(`workspace_files:${answer2.id}`);
41092
+ }
41093
+ const textPath = extractedTextRelPath(file2.relPath);
41094
+ if (outcome.kind === "diverged") {
41095
+ report.push({ path: textPath, record: divergedNotice(textPath, answer2.entry.state) });
41096
+ } else {
41097
+ clear.push(textPath);
41098
+ }
41099
+ if (outcome.kind === "written") {
41100
+ written++;
41101
+ if (outcome.textHash !== undefined) {
41102
+ textHashes.set(file2.relPath, { to: outcome.textHash });
41103
+ }
41104
+ } else if (outcome.kind === "diverged") {
41105
+ diverged++;
41106
+ notices.push(divergedNotice(outcome.relPath, answer2.entry.state));
41107
+ }
41108
+ }
41109
+ if (args.abortIfStale?.() === true) {
41110
+ args.output?.log("[MEMORY_REVALIDATE stopped reason=poll_failed_after_write]");
41111
+ return { written, diverged, notices, textHashes, inodes, unwritten, report, clear, aborted: true };
41112
+ }
41113
+ return { written, diverged, notices, textHashes, inodes, unwritten, report, clear, aborted: false };
41114
+ }
41115
+ function commit(args) {
41116
+ const { state, changed } = setMemorySourcesState(args.state, args.next);
41117
+ if (!changed && args.state === args.base)
41118
+ return args.base;
41119
+ return args.commitState(args.base, state);
41120
+ }
41121
+ async function revalidateTick(args) {
41122
+ const now = args.now ?? (() => new Date);
41123
+ const held = args.state.memory_sources ?? EMPTY;
41124
+ const documents = heldDocuments(args.state);
41125
+ const fileTexts = heldFileTexts(args.state);
41126
+ if (documents.length === 0 && fileTexts.length === 0) {
41127
+ return {
41128
+ state: args.state,
41129
+ written: 0,
41130
+ diverged: 0,
41131
+ notices: [],
41132
+ unavailable: [],
41133
+ ran: false
41134
+ };
41135
+ }
41136
+ if (!shouldRevalidate({
41137
+ polledAt: held.polled_at,
41138
+ intervalMs: args.intervalMs ?? revalidateIntervalMs(),
41139
+ now: now(),
41140
+ anyUnknown: anyUnknown(held),
41141
+ askFailedAt: held.ask_failed_at
41142
+ })) {
41143
+ return {
41144
+ state: args.state,
41145
+ written: 0,
41146
+ diverged: 0,
41147
+ notices: [],
41148
+ unavailable: [],
41149
+ ran: false
41150
+ };
41151
+ }
41152
+ const decision = await runRevalidate({
41153
+ baseUrl: args.baseUrl,
41154
+ pat: args.pat,
41155
+ documentIds: documents.map((d) => d.id),
41156
+ workspaceFileIds: fileTexts.map((f) => f.id),
41157
+ priorDocuments: getMemorySourceEntries(args.state, "documents"),
41158
+ priorWorkspaceFiles: getMemorySourceEntries(args.state, "workspace_files"),
41159
+ fetchImpl: args.fetchImpl,
41160
+ now,
41161
+ output: args.output
41162
+ });
41163
+ if (args.abortIfStale?.() === true) {
41164
+ args.output?.log("[MEMORY_REVALIDATE dropped reason=poll_failed_in_flight]");
41165
+ return {
41166
+ state: args.state,
41167
+ written: 0,
41168
+ diverged: 0,
41169
+ notices: [],
41170
+ unavailable: [],
41171
+ ran: false
41172
+ };
41173
+ }
41174
+ return await settle({
41175
+ ...args,
41176
+ decision,
41177
+ now,
41178
+ clearInterrupted: true,
41179
+ documents,
41180
+ fileTexts,
41181
+ held,
41182
+ askFailedAt: decision.whollyUnknown ? now().toISOString() : null
41183
+ });
41184
+ }
41185
+ async function degradeTick(args) {
41186
+ const now = args.now ?? (() => new Date);
41187
+ const held = args.state.memory_sources ?? EMPTY;
41188
+ const interrupted = held.write_interrupted === true;
41189
+ const sweep = interrupted;
41190
+ const unbankedDocs = args.unbanked?.documents ?? [];
41191
+ const unbankedFiles = (args.unbanked?.workspaceFiles ?? []).map((f) => f.id);
41192
+ const freshTextHashes = new Map;
41193
+ for (const f of args.unbanked?.workspaceFiles ?? []) {
41194
+ if (f.textHash !== undefined)
41195
+ freshTextHashes.set(f.id, f.textHash);
41196
+ }
41197
+ const decision = degradeAllToUnknown({
41198
+ priorDocuments: held.documents,
41199
+ priorWorkspaceFiles: held.workspace_files,
41200
+ heldDocumentIds: sweep ? heldDocuments(args.state).map((d) => d.id) : unbankedDocs,
41201
+ heldWorkspaceFileIds: sweep ? heldFileTexts(args.state).map((f) => f.id) : unbankedFiles
41202
+ });
41203
+ if (decision.changed.length === 0 && !sweep) {
41204
+ return {
41205
+ state: args.state,
41206
+ written: 0,
41207
+ diverged: 0,
41208
+ notices: [],
41209
+ unavailable: [],
41210
+ ran: decision.answers.length > 0
41211
+ };
41212
+ }
41213
+ return await settle({
41214
+ ...args,
41215
+ decision,
41216
+ now,
41217
+ freshTextHashes,
41218
+ documents: heldDocuments(args.state),
41219
+ fileTexts: heldFileTexts(args.state),
41220
+ held,
41221
+ keepPolledAt: true,
41222
+ clearInterrupted: sweep
41223
+ });
41224
+ }
41225
+ function nextUnwritten(args) {
41226
+ const out = {};
41227
+ const gone = new Set(args.cleared);
41228
+ for (const [path5, rec] of Object.entries(args.prior ?? {})) {
41229
+ if (!args.reachable.has(path5) || gone.has(path5))
41230
+ continue;
41231
+ out[path5] = rec;
41232
+ }
41233
+ for (const r of args.reported) {
41234
+ const prior = out[r.path];
41235
+ out[r.path] = prior !== undefined && prior.message === r.record ? prior : { path: r.path, message: r.record, recorded_at: args.now().toISOString() };
41236
+ }
41237
+ if (Object.keys(out).length === 0)
41238
+ return args.prior === undefined ? undefined : {};
41239
+ return out;
41240
+ }
41241
+ async function settle(args) {
41242
+ const writes = await writeChanged({
41243
+ onWritten: args.onWritten,
41244
+ freshTextHashes: args.freshTextHashes,
41245
+ workspaceSlug: args.workspaceSlug,
41246
+ state: args.state,
41247
+ abortIfStale: args.abortIfStale,
41248
+ changed: args.decision.answers,
41249
+ mutex: args.mutex,
41250
+ suppressor: args.suppressor,
41251
+ output: args.output,
41252
+ getState: args.getState
41253
+ });
41254
+ const liveHeldNow = args.getState?.().memory_sources ?? args.held;
41255
+ let state = args.state;
41256
+ if (writes.textHashes.size > 0 || writes.inodes.size > 0) {
41257
+ const files = { ...state.files ?? {} };
41258
+ for (const [relPath, hash2] of writes.textHashes) {
41259
+ const existing = files[relPath];
41260
+ if (existing === undefined)
41261
+ continue;
41262
+ if (onDiskTextHash(args.workspaceSlug, relPath) !== hash2.to)
41263
+ continue;
41264
+ files[relPath] = { ...existing, text_hash: hash2.to };
41265
+ }
41266
+ const documents = { ...state.entries["documents"] ?? {} };
41267
+ for (const [id, ino] of writes.inodes) {
41268
+ const existing = documents[id];
41269
+ if (existing === undefined)
41270
+ continue;
41271
+ if (inodeAfterWrite(args.workspaceSlug, "documents", ino.slug) !== ino.to)
41272
+ continue;
41273
+ documents[id] = { ...existing, inode: ino.to };
41274
+ }
41275
+ state = {
41276
+ ...state,
41277
+ files,
41278
+ entries: { ...state.entries, documents }
41279
+ };
41280
+ }
41281
+ if (writes.aborted) {
41282
+ const abortedRecords = nextUnwritten({
41283
+ prior: liveHeldNow.unwritten,
41284
+ reachable: new Set([
41285
+ ...args.documents.map((d) => getStrategy("documents").mirrorRelativePath(d.slug)),
41286
+ ...args.fileTexts.map((f) => extractedTextRelPath(f.relPath))
41287
+ ]),
41288
+ cleared: writes.clear,
41289
+ reported: writes.report,
41290
+ now: args.now
41291
+ });
41292
+ return {
41293
+ state: commit({
41294
+ base: args.state,
41295
+ state,
41296
+ next: { ...liveHeldNow, write_interrupted: true, unwritten: abortedRecords },
41297
+ commitState: args.commitState
41298
+ }),
41299
+ written: writes.written,
41300
+ diverged: writes.diverged,
41301
+ notices: writes.notices,
41302
+ unavailable: args.decision.unavailable,
41303
+ ran: false
41304
+ };
41305
+ }
41306
+ const unwrittenRecords = nextUnwritten({
41307
+ prior: liveHeldNow.unwritten,
41308
+ reachable: new Set([
41309
+ ...args.documents.map((d) => getStrategy("documents").mirrorRelativePath(d.slug)),
41310
+ ...args.fileTexts.map((f) => extractedTextRelPath(f.relPath))
41311
+ ]),
41312
+ cleared: writes.clear,
41313
+ reported: writes.report,
41314
+ now: args.now
41315
+ });
41316
+ const applied = applyDecision({
41317
+ unwritten: writes.unwritten,
41318
+ askFailedAt: args.askFailedAt !== undefined ? args.askFailedAt : liveHeldNow.ask_failed_at,
41319
+ prior: liveHeldNow,
41320
+ decision: args.decision,
41321
+ polledAt: args.keepPolledAt ? liveHeldNow.polled_at ?? args.now().toISOString() : args.now().toISOString()
41322
+ });
41323
+ const pruned = pruneUnheld({
41324
+ state: {
41325
+ ...args.clearInterrupted === true ? { ...applied, write_interrupted: false } : applied,
41326
+ unwritten: unwrittenRecords
41327
+ },
41328
+ documentIds: args.documents.map((d) => d.id),
41329
+ workspaceFileIds: args.fileTexts.map((f) => f.id)
41330
+ });
41331
+ const committed = commit({
41332
+ base: args.state,
41333
+ state,
41334
+ next: pruned,
41335
+ commitState: args.commitState
41336
+ });
41337
+ if (writes.written > 0 || writes.diverged > 0 || args.decision.unavailable.length > 0) {
41338
+ args.output?.log(`[MEMORY_${args.keepPolledAt === true ? "DEGRADE" : "REVALIDATE"} ` + `ws=${args.workspaceSlug} sources=${args.decision.answers.length} ` + `written=${writes.written} diverged=${writes.diverged} ` + `unavailable=${args.decision.unavailable.join(",") || "none"}]`);
41339
+ }
41340
+ return {
41341
+ state: committed,
41342
+ written: writes.written,
41343
+ diverged: writes.diverged,
41344
+ notices: writes.notices,
41345
+ unavailable: args.decision.unavailable,
41346
+ ran: true
41347
+ };
41348
+ }
41349
+ function memoryLaneCallback(args) {
41350
+ let revalidating = false;
41351
+ let pollFailures = 0;
41352
+ const pending = new Set;
41353
+ let inFlight = null;
41354
+ const withdraw = async (stamped) => {
41355
+ const degradeBase = args.getState();
41356
+ try {
41357
+ const degraded = await degradeTick({
41358
+ workspaceSlug: args.workspaceSlug,
41359
+ state: degradeBase,
41360
+ mutex: args.mutex,
41361
+ commitState: args.commitState,
41362
+ now: args.now,
41363
+ suppressor: args.suppressor,
41364
+ output: args.output,
41365
+ getState: args.getState,
41366
+ unbanked: {
41367
+ documents: [...stamped?.documents ?? []],
41368
+ workspaceFiles: [...stamped?.workspaceFiles ?? []].map(([id, textHash]) => ({
41369
+ id,
41370
+ textHash
41371
+ }))
41372
+ }
41373
+ });
41374
+ args.setState(rebaseState(args.getState(), degradeBase, degraded.state));
41375
+ for (const notice of degraded.notices)
41376
+ args.warn(notice);
41377
+ } catch (err) {
41378
+ const msg = err instanceof Error ? err.message : String(err);
41379
+ args.output?.log(`[MEMORY_DEGRADE failed reason=${msg}]`);
41380
+ }
41381
+ };
41382
+ const run = async (ok) => {
41383
+ if (!ok) {
41384
+ pollFailures++;
41385
+ await withdraw(inFlight);
41386
+ return;
41387
+ }
41388
+ if (revalidating)
41389
+ return;
41390
+ revalidating = true;
41391
+ const written = {
41392
+ documents: new Set,
41393
+ workspaceFiles: new Map
41394
+ };
41395
+ inFlight = written;
41396
+ const failuresAtStart = pollFailures;
41397
+ try {
41398
+ const revalidateBase = args.getState();
41399
+ const result = await revalidateTick({
41400
+ workspaceSlug: args.workspaceSlug,
41401
+ baseUrl: args.baseUrl,
41402
+ pat: args.pat,
41403
+ state: revalidateBase,
41404
+ mutex: args.mutex,
41405
+ commitState: args.commitState,
41406
+ fetchImpl: args.fetchImpl,
41407
+ now: args.now,
41408
+ intervalMs: args.intervalMs,
41409
+ suppressor: args.suppressor,
41410
+ output: args.output,
41411
+ getState: args.getState,
41412
+ abortIfStale: () => pollFailures !== failuresAtStart,
41413
+ onWritten: (arm, id, textHash) => {
41414
+ if (arm === "documents")
41415
+ written.documents.add(id);
41416
+ else
41417
+ written.workspaceFiles.set(id, textHash);
41418
+ }
41419
+ });
41420
+ args.setState(rebaseState(args.getState(), revalidateBase, result.state));
41421
+ for (const notice of result.notices)
41422
+ args.warn(notice);
41423
+ } catch (err) {
41424
+ const msg = err instanceof Error ? err.message : String(err);
41425
+ args.output?.log(`[MEMORY_REVALIDATE failed reason=${msg}]`);
41426
+ } finally {
41427
+ revalidating = false;
41428
+ inFlight = null;
41429
+ }
41430
+ };
41431
+ const callback = (ok) => {
41432
+ const p = run(ok).finally(() => {
41433
+ pending.delete(p);
41434
+ });
41435
+ pending.add(p);
41436
+ };
41437
+ callback.settled = async () => {
41438
+ while (pending.size > 0)
41439
+ await Promise.all([...pending]);
41440
+ };
41441
+ return callback;
41442
+ }
41443
+
41444
+ // src/lib/memory-facts/freshness.ts
41445
+ var ROUTE3 = "/api/sync/memory-facts/freshness";
41446
+ var FRESHNESS_DEADLINE_MS = 20000;
41447
+ var FreshnessBodySchema = exports_external.object({
41448
+ behind: exports_external.boolean(),
41449
+ watermark: FeedWatermarkSchema.nullish()
41450
+ });
41451
+ var CodeBodySchema = exports_external.object({ code: exports_external.string().nullish() });
41452
+ async function checkFactFreshness(args) {
41453
+ const { baseUrl, pat, snapshot, fetchImpl = fetch } = args;
41454
+ const url2 = `${baseUrl.replace(/\/$/, "")}${ROUTE3}?watermark=${encodeURIComponent(snapshot)}`;
41455
+ let res;
41456
+ try {
41457
+ res = await fetchImpl(url2, {
41458
+ method: "GET",
41459
+ headers: { Authorization: `Bearer ${pat}`, Accept: "application/json" },
41460
+ signal: args.signal ? AbortSignal.any([args.signal, AbortSignal.timeout(FRESHNESS_DEADLINE_MS)]) : AbortSignal.timeout(FRESHNESS_DEADLINE_MS)
41461
+ });
41462
+ } catch (err) {
41463
+ const msg = err instanceof Error ? err.message : "network error";
41464
+ return { kind: "failed", message: msg };
41465
+ }
41466
+ if (res.status === 429) {
41467
+ let fromBody = null;
41468
+ try {
41469
+ const body2 = await res.json();
41470
+ const seconds = body2?.retry_after_seconds;
41471
+ if (typeof seconds === "number" && Number.isFinite(seconds) && seconds > 0) {
41472
+ fromBody = Math.round(seconds * 1000);
41473
+ }
41474
+ } catch {}
41475
+ return {
41476
+ kind: "rate_limited",
41477
+ retryAfterMs: fromBody ?? parseRetryAfterMs(res.headers.get("retry-after")) ?? null
41478
+ };
41479
+ }
41480
+ if (res.status === 400 || res.status === 503) {
41481
+ let code = null;
41482
+ try {
41483
+ const body2 = await res.json();
41484
+ code = CodeBodySchema.safeParse(body2).data?.code ?? null;
41485
+ } catch {}
41486
+ if (code === "bad_watermark")
41487
+ return { kind: "bad_watermark" };
41488
+ if (code === "watermark_unavailable")
41489
+ return { kind: "unavailable" };
41490
+ return {
41491
+ kind: "failed",
41492
+ message: `freshness check failed with HTTP ${res.status}`
41493
+ };
41494
+ }
41495
+ if (!res.ok) {
41496
+ return {
41497
+ kind: "failed",
41498
+ message: `freshness check failed with HTTP ${res.status}`
41499
+ };
41500
+ }
41501
+ let body;
41502
+ try {
41503
+ body = await res.json();
41504
+ } catch {
41505
+ return { kind: "failed", message: "freshness response was not JSON" };
41506
+ }
41507
+ const parsed = FreshnessBodySchema.safeParse(body);
41508
+ if (!parsed.success) {
41509
+ return {
41510
+ kind: "failed",
41511
+ message: "freshness response did not match the expected shape"
41512
+ };
41513
+ }
41514
+ return {
41515
+ kind: "answer",
41516
+ behind: parsed.data.behind,
41517
+ watermark: parsed.data.behind ? null : parsed.data.watermark ?? null
41518
+ };
41519
+ }
41520
+
41521
+ // src/lib/memory-facts/freshness-gate.ts
41522
+ function createFactFreshnessGate(args) {
41523
+ const now = args.now ?? (() => new Date);
41524
+ const mutate = (fn) => {
41525
+ const before = args.getState();
41526
+ const entry2 = before.memory_facts ?? emptyFactsEntry(now().toISOString());
41527
+ const next = fn(entry2);
41528
+ const { state, changed } = setMemoryFactsState(before, next);
41529
+ if (changed) {
41530
+ const committed = args.commitState(before, state);
41531
+ args.setState(rebaseState(args.getState(), before, committed));
41532
+ }
41533
+ try {
41534
+ writeFreshnessMarkerIfChanged({
41535
+ workspaceSlug: args.workspaceSlug,
41536
+ entry: next,
41537
+ suppressor: args.suppressor
41538
+ });
41539
+ } catch (err) {
41540
+ const msg = err instanceof Error ? err.message : String(err);
41541
+ args.output?.log(`[MEMORY_FACTS marker_unwritten reason=${msg}]`);
41542
+ }
41543
+ return next;
41544
+ };
41545
+ return {
41546
+ onPollFailed() {
41547
+ mutate(withdrawClaim);
41548
+ },
41549
+ async decide(at) {
41550
+ const withFloor = (e) => floorDue(e, at, args.floorMs) ? raisePullReason(e, "floor_due") : e;
41551
+ const working = withFloor(args.getState().memory_facts ?? emptyFactsEntry(at.toISOString()));
41552
+ if (!mayAsk(working, at)) {
41553
+ return pullDecision(mutate(withFloor), null);
41554
+ }
41555
+ let verdict;
41556
+ try {
41557
+ verdict = await checkFactFreshness({
41558
+ baseUrl: args.baseUrl,
41559
+ pat: args.pat,
41560
+ snapshot: working.watermark.snapshot,
41561
+ fetchImpl: args.fetchImpl
41562
+ });
41563
+ } catch (err) {
41564
+ const msg = err instanceof Error ? err.message : String(err);
41565
+ verdict = { kind: "failed", message: msg };
41566
+ }
41567
+ return pullDecision(mutate((live) => applyVerdict(withFloor(live), verdict, at)), verdict);
41568
+ },
41569
+ bank(a) {
41570
+ mutate((live) => bankPull({
41571
+ entry: { ...live, facts: a.registry },
41572
+ complete: a.complete,
41573
+ watermark: a.watermark,
41574
+ epochAtFetch: a.epochAtFetch,
41575
+ now: a.now
41576
+ }));
41577
+ }
41578
+ };
41579
+ }
41580
+
39380
41581
  // src/lib/pull-loop.ts
39381
41582
  var DEFAULT_PULL_INTERVAL_MS = 15000;
39382
41583
  var PULL_BACKOFF_CAP_MS = 300000;
@@ -39391,11 +41592,29 @@ function pullIntervalMs() {
39391
41592
  }
39392
41593
  return DEFAULT_PULL_INTERVAL_MS;
39393
41594
  }
41595
+ function composeAfterPollOutcome(...arms) {
41596
+ const composed = (ok) => {
41597
+ for (const arm of arms) {
41598
+ try {
41599
+ arm(ok);
41600
+ } catch {}
41601
+ }
41602
+ };
41603
+ composed.settled = async () => {
41604
+ for (const arm of arms) {
41605
+ try {
41606
+ await arm.settled?.();
41607
+ } catch {}
41608
+ }
41609
+ };
41610
+ return composed;
41611
+ }
39394
41612
 
39395
41613
  class PullLoop {
39396
41614
  runOnce;
39397
41615
  onApplied;
39398
41616
  afterPoll;
41617
+ afterPollOutcome;
39399
41618
  notice;
39400
41619
  loginHint;
39401
41620
  intervalMs;
@@ -39414,6 +41633,7 @@ class PullLoop {
39414
41633
  this.runOnce = deps.runOnce;
39415
41634
  this.onApplied = deps.onApplied;
39416
41635
  this.afterPoll = deps.afterPoll;
41636
+ this.afterPollOutcome = deps.afterPollOutcome;
39417
41637
  this.notice = deps.notice;
39418
41638
  this.loginHint = deps.loginHint;
39419
41639
  this.intervalMs = deps.intervalMs ?? pullIntervalMs();
@@ -39463,10 +41683,12 @@ class PullLoop {
39463
41683
  this.onApplied(result.applied);
39464
41684
  }
39465
41685
  this.afterPoll();
41686
+ this.afterPollOutcome?.(true);
39466
41687
  return this.intervalMs;
39467
41688
  }
39468
41689
  onFailure(failure) {
39469
41690
  const kind = failure?.kind ?? "transient";
41691
+ this.afterPollOutcome?.(false);
39470
41692
  if (kind === "auth") {
39471
41693
  if (!this.authAnnounced) {
39472
41694
  this.authAnnounced = true;
@@ -39862,7 +42084,7 @@ var OWNERSHIP_NOTE2 = [
39862
42084
  `);
39863
42085
  var DEFAULT_USER_LABEL = "the Plinth account this mirror syncs with";
39864
42086
  var FULL_BAND_DAYS = 14;
39865
- function yamlString2(value) {
42087
+ function yamlString4(value) {
39866
42088
  return JSON.stringify(value);
39867
42089
  }
39868
42090
  function oneLine2(value, maxLength = 200) {
@@ -39955,14 +42177,14 @@ function windowLine(timeMin, timeMax) {
39955
42177
  return `${formatDay(from, -1)} to ${formatDay(to, -1)}`;
39956
42178
  }
39957
42179
  function statusLine(args) {
39958
- const { status, eventCount, error: error52, truncated, stamp, userLabel } = args;
42180
+ const { status, eventCount, error: error52, truncated, stamp: stamp2, userLabel } = args;
39959
42181
  switch (status) {
39960
42182
  case "fetched": {
39961
42183
  if (eventCount === 0) {
39962
- return `**Read fine as of ${stamp}. Nothing in this window.** The diary really ` + `is clear across the dates above. This is a successful read of an empty ` + `calendar, not a failed read - if it had failed, this line would say so.`;
42184
+ return `**Read fine as of ${stamp2}. Nothing in this window.** The diary really ` + `is clear across the dates above. This is a successful read of an empty ` + `calendar, not a failed read - if it had failed, this line would say so.`;
39963
42185
  }
39964
42186
  const truncatedNote = truncated ? " **This list is INCOMPLETE**: Google had more entries than one page and the rest were not collected." : "";
39965
- return `**Read fine as of ${stamp}.** ${eventCount} ` + `${eventCount === 1 ? "entry" : "entries"} in the window.${truncatedNote}`;
42187
+ return `**Read fine as of ${stamp2}.** ${eventCount} ` + `${eventCount === 1 ? "entry" : "entries"} in the window.${truncatedNote}`;
39966
42188
  }
39967
42189
  case "no-token":
39968
42190
  return `**NOT READ - Google is not connected for ${userLabel}.** Nothing is listed ` + `below because Plinth could not look, NOT because the diary is clear. ` + `Connect Google in Plinth to fill this in, and until then treat this ` + `calendar as unknown rather than empty.`;
@@ -40010,19 +42232,19 @@ function renderArtistCalendar(args) {
40010
42232
  const { row, generatedAt, window: window2 } = args;
40011
42233
  const userLabel = args.userLabel ?? DEFAULT_USER_LABEL;
40012
42234
  const events = row.events ?? [];
40013
- const stamp = humanStamp(generatedAt);
42235
+ const stamp2 = humanStamp(generatedAt);
40014
42236
  const referenceYear = new Date(Date.parse(generatedAt) || 0).getUTCFullYear();
40015
42237
  const title = rowDisplayName(row);
40016
42238
  const header = [
40017
42239
  "---",
40018
42240
  "plinth_generated: true",
40019
42241
  "view: calendar",
40020
- `artist_slug: ${row.artist_slug === null ? "null" : yamlString2(row.artist_slug)}`,
40021
- `artist_name: ${row.artist_name === null ? "null" : yamlString2(row.artist_name)}`,
40022
- `calendar_name: ${yamlString2(row.calendar_name)}`,
40023
- `generated_at: ${yamlString2(generatedAt)}`,
40024
- `window_start: ${yamlString2(window2.time_min)}`,
40025
- `window_end: ${yamlString2(window2.time_max)}`,
42242
+ `artist_slug: ${row.artist_slug === null ? "null" : yamlString4(row.artist_slug)}`,
42243
+ `artist_name: ${row.artist_name === null ? "null" : yamlString4(row.artist_name)}`,
42244
+ `calendar_name: ${yamlString4(row.calendar_name)}`,
42245
+ `generated_at: ${yamlString4(generatedAt)}`,
42246
+ `window_start: ${yamlString4(window2.time_min)}`,
42247
+ `window_end: ${yamlString4(window2.time_max)}`,
40026
42248
  `status: ${row.status}`,
40027
42249
  "fetch_state: fresh",
40028
42250
  `entry_count: ${events.length}`,
@@ -40042,7 +42264,7 @@ function renderArtistCalendar(args) {
40042
42264
  eventCount: events.length,
40043
42265
  error: row.error,
40044
42266
  truncated: row.truncated,
40045
- stamp,
42267
+ stamp: stamp2,
40046
42268
  userLabel
40047
42269
  }),
40048
42270
  "",
@@ -40089,7 +42311,7 @@ function renderUpcoming(args) {
40089
42311
  mappedArtistCount,
40090
42312
  internalName
40091
42313
  } = args;
40092
- const stamp = humanStamp(generatedAt);
42314
+ const stamp2 = humanStamp(generatedAt);
40093
42315
  const referenceYear = new Date(Date.parse(generatedAt) || 0).getUTCFullYear();
40094
42316
  const fetched = rows.filter((r) => r.status === "fetched");
40095
42317
  const failed = rows.filter((r) => r.status !== "fetched");
@@ -40103,9 +42325,9 @@ function renderUpcoming(args) {
40103
42325
  "plinth_generated: true",
40104
42326
  "view: calendar",
40105
42327
  "scope: workspace",
40106
- `generated_at: ${yamlString2(generatedAt)}`,
40107
- `window_start: ${yamlString2(window2.time_min)}`,
40108
- `window_end: ${yamlString2(window2.time_max)}`,
42328
+ `generated_at: ${yamlString4(generatedAt)}`,
42329
+ `window_start: ${yamlString4(window2.time_min)}`,
42330
+ `window_end: ${yamlString4(window2.time_max)}`,
40109
42331
  `artist_count: ${artistCount}`,
40110
42332
  `mapped_artist_count: ${mappedArtistCount}`,
40111
42333
  `calendars_read: ${fetched.length}`,
@@ -40122,7 +42344,7 @@ function renderUpcoming(args) {
40122
42344
  `);
40123
42345
  const preamble = [
40124
42346
  "",
40125
- `**As of ${stamp}.** Window ${windowLine(window2.time_min, window2.time_max)}.`,
42347
+ `**As of ${stamp2}.** Window ${windowLine(window2.time_min, window2.time_max)}.`,
40126
42348
  "",
40127
42349
  failed.length === 0 ? `**All ${fetched.length} mapped ${fetched.length === 1 ? "calendar was" : "calendars were"} read.**` : `**${fetched.length} of ${rows.length} mapped ${rows.length === 1 ? "calendar" : "calendars"} read; ${failed.length} could NOT be read.**` + ` Anything on an unread calendar is missing from the lists below - unknown, not absent.`,
40128
42350
  "",
@@ -40196,13 +42418,13 @@ function renderStaleFile(existing, args) {
40196
42418
  const fmMatch = /^---\n([\s\S]*?)\n---\n/.exec(split.header);
40197
42419
  const carried = (fmMatch?.[1] ?? "").split(`
40198
42420
  `).filter((line) => !/^(fetch_state|last_refresh_attempt|last_refresh_error):/.test(line));
40199
- const stamp = previousGeneratedAt ? humanStamp(previousGeneratedAt) : "an earlier refresh";
42421
+ const stamp2 = previousGeneratedAt ? humanStamp(previousGeneratedAt) : "an earlier refresh";
40200
42422
  const header = [
40201
42423
  "---",
40202
42424
  ...carried,
40203
42425
  "fetch_state: stale",
40204
- `last_refresh_attempt: ${yamlString2(args.attemptedAt)}`,
40205
- `last_refresh_error: ${yamlString2(oneLine2(args.reason, 300))}`,
42426
+ `last_refresh_attempt: ${yamlString4(args.attemptedAt)}`,
42427
+ `last_refresh_error: ${yamlString4(oneLine2(args.reason, 300))}`,
40206
42428
  "---",
40207
42429
  "",
40208
42430
  OWNERSHIP_NOTE2,
@@ -40211,7 +42433,7 @@ function renderStaleFile(existing, args) {
40211
42433
  "",
40212
42434
  `**STALE - the last refresh FAILED and nothing below was updated.** Attempted ` + `${humanStamp(args.attemptedAt)} and it did not succeed: ` + `${oneLine2(args.reason, 300)}`,
40213
42435
  "",
40214
- `**What is below was last read at ${stamp}** and has not moved since. Anything ` + `changed in Google after that is not here, and something cancelled after that ` + `is still listed. Say the age out loud rather than briefing it as current.`,
42436
+ `**What is below was last read at ${stamp2}** and has not moved since. Anything ` + `changed in Google after that is not here, and something cancelled after that ` + `is still listed. Say the age out loud rather than briefing it as current.`,
40215
42437
  ""
40216
42438
  ].join(`
40217
42439
  `);
@@ -40396,8 +42618,8 @@ class PullSuppressor {
40396
42618
  }
40397
42619
 
40398
42620
  // src/lib/discover-entities.ts
40399
- var import_gray_matter11 = __toESM(require_gray_matter(), 1);
40400
- import { readFileSync as readFileSync20, statSync as statSync11 } from "node:fs";
42621
+ var import_gray_matter12 = __toESM(require_gray_matter(), 1);
42622
+ import { readFileSync as readFileSync22, statSync as statSync12 } from "node:fs";
40401
42623
  var DISCOVERY_WRITE_CAP_DEFAULT = 10;
40402
42624
  function discoveryWriteCap() {
40403
42625
  const raw = process.env.PLINTH_DISCOVERY_WRITE_CAP;
@@ -40431,7 +42653,7 @@ function classifyRenameDestination(args) {
40431
42653
  }
40432
42654
  return { kind: "rename" };
40433
42655
  }
40434
- var EMPTY = {
42656
+ var EMPTY2 = {
40435
42657
  discovered: 0,
40436
42658
  created: 0,
40437
42659
  collisions: 0,
@@ -40467,7 +42689,7 @@ function planDiscoveryWrites(args) {
40467
42689
  continue;
40468
42690
  let liveInode;
40469
42691
  try {
40470
- liveInode = statSync11(absolutePath).ino;
42692
+ liveInode = statSync12(absolutePath).ino;
40471
42693
  } catch {
40472
42694
  continue;
40473
42695
  }
@@ -40496,7 +42718,7 @@ function planDiscoveryWrites(args) {
40496
42718
  counts.discovered++;
40497
42719
  let frontmatter;
40498
42720
  try {
40499
- frontmatter = import_gray_matter11.default(readFileSync20(absolutePath, "utf8")).data ?? {};
42721
+ frontmatter = import_gray_matter12.default(readFileSync22(absolutePath, "utf8")).data ?? {};
40500
42722
  } catch {
40501
42723
  counts.discovered--;
40502
42724
  continue;
@@ -40517,7 +42739,7 @@ async function discoverUntrackedEntities(args) {
40517
42739
  const { workspaceSlug, baseUrl, pat, fetchImpl, output, isFrozen } = args;
40518
42740
  const now = args.now ?? (() => new Date);
40519
42741
  let state = args.state;
40520
- const counts = { ...EMPTY };
42742
+ const counts = { ...EMPTY2 };
40521
42743
  let mirrorRootPath;
40522
42744
  try {
40523
42745
  mirrorRootPath = workspaceMirrorPath(workspaceSlug);
@@ -40537,7 +42759,7 @@ async function discoverUntrackedEntities(args) {
40537
42759
  } catch (err) {
40538
42760
  const msg = err instanceof Error ? err.message : "unknown error";
40539
42761
  output?.warn(`Could not check this workspace for files added while Plinth was closed: ${msg} ` + `(everything already synced is unaffected).`);
40540
- return { state, ...EMPTY };
42762
+ return { state, ...EMPTY2 };
40541
42763
  }
40542
42764
  const cap = discoveryWriteCap();
40543
42765
  const creates = planned.filter((w) => w.kind === "create");
@@ -40724,8 +42946,8 @@ function recordDiscoveryFailure(args) {
40724
42946
  }
40725
42947
 
40726
42948
  // src/lib/frontmatter-id-backfill.ts
40727
- var import_gray_matter12 = __toESM(require_gray_matter(), 1);
40728
- import { readFileSync as readFileSync21, statSync as statSync12 } from "node:fs";
42949
+ var import_gray_matter13 = __toESM(require_gray_matter(), 1);
42950
+ import { readFileSync as readFileSync23, statSync as statSync13 } from "node:fs";
40729
42951
  var FRONTMATTER_ID_BACKFILL_VERSION = 1;
40730
42952
  function backfillWorthReporting(r) {
40731
42953
  return r.written > 0 || r.failed > 0 || r.skipped.foreignId > 0 || r.skipped.unwritable > 0 || r.skipped.slugCollision > 0;
@@ -40760,7 +42982,7 @@ function heldByAnyBatch(workspaceSlug) {
40760
42982
  }
40761
42983
  function safeInode3(path5) {
40762
42984
  try {
40763
- return statSync12(path5).ino;
42985
+ return statSync13(path5).ino;
40764
42986
  } catch {
40765
42987
  return;
40766
42988
  }
@@ -40768,7 +42990,7 @@ function safeInode3(path5) {
40768
42990
  function backfillFrontmatterIds(args) {
40769
42991
  const { workspaceSlug, state } = args;
40770
42992
  const compose = args.compose ?? withFrontmatterId;
40771
- const readRaw = args.readFile ?? ((p) => readFileSync21(p, "utf8"));
42993
+ const readRaw = args.readFile ?? ((p) => readFileSync23(p, "utf8"));
40772
42994
  const result = emptyResult();
40773
42995
  if (state.frontmatter_id_backfill_version === FRONTMATTER_ID_BACKFILL_VERSION) {
40774
42996
  return result;
@@ -40817,7 +43039,7 @@ function backfillFrontmatterIds(args) {
40817
43039
  let rawBody;
40818
43040
  let parsedData;
40819
43041
  try {
40820
- const parsed = import_gray_matter12.default(raw);
43042
+ const parsed = import_gray_matter13.default(raw);
40821
43043
  parsedData = parsed.data ?? {};
40822
43044
  existing = parsedData["id"];
40823
43045
  rawBody = parsed.content;
@@ -40896,7 +43118,7 @@ function backfillFrontmatterIds(args) {
40896
43118
  }
40897
43119
 
40898
43120
  // src/lib/reconcile.ts
40899
- import { readFileSync as readFileSync22, statSync as statSync13 } from "node:fs";
43121
+ import { readFileSync as readFileSync24, statSync as statSync14 } from "node:fs";
40900
43122
  async function reconcileDivergedRows(args) {
40901
43123
  const { workspaceSlug, baseUrl, pat, fetchImpl, output, isFrozen } = args;
40902
43124
  const now = args.now ?? (() => new Date);
@@ -40995,7 +43217,7 @@ async function reconcileDivergedRows(args) {
40995
43217
  }
40996
43218
  let liveInode;
40997
43219
  try {
40998
- liveInode = statSync13(absolutePath).ino;
43220
+ liveInode = statSync14(absolutePath).ino;
40999
43221
  } catch {
41000
43222
  liveInode = undefined;
41001
43223
  }
@@ -41008,7 +43230,7 @@ async function reconcileDivergedRows(args) {
41008
43230
  }
41009
43231
  let content;
41010
43232
  try {
41011
- content = readFileSync22(absolutePath, "utf8");
43233
+ content = readFileSync24(absolutePath, "utf8");
41012
43234
  } catch (err) {
41013
43235
  if (err?.code === "ENOENT") {
41014
43236
  state = clearEntityCloudRowDeleted(state, strategy.mirrorRelativePath(entry2.slug));
@@ -41128,10 +43350,10 @@ var PreconditionSchema2 = exports_external.object({
41128
43350
  function baseHeaders3(pat) {
41129
43351
  return { Authorization: `Bearer ${pat}`, Accept: "application/json" };
41130
43352
  }
41131
- var ROUTE2 = "/api/sync/workspace-rules";
43353
+ var ROUTE4 = "/api/sync/workspace-rules";
41132
43354
  async function getWorkspaceRules(args) {
41133
43355
  const { baseUrl, pat, fetchImpl = fetch } = args;
41134
- const url2 = `${baseUrl.replace(/\/$/, "")}${ROUTE2}`;
43356
+ const url2 = `${baseUrl.replace(/\/$/, "")}${ROUTE4}`;
41135
43357
  let res;
41136
43358
  try {
41137
43359
  res = await fetchImpl(url2, { method: "GET", headers: baseHeaders3(pat) });
@@ -41157,7 +43379,7 @@ async function getWorkspaceRules(args) {
41157
43379
  }
41158
43380
  async function putWorkspaceRules(args) {
41159
43381
  const { baseUrl, pat, bodyMd, expectedUpdatedAt, fetchImpl = fetch } = args;
41160
- const url2 = `${baseUrl.replace(/\/$/, "")}${ROUTE2}`;
43382
+ const url2 = `${baseUrl.replace(/\/$/, "")}${ROUTE4}`;
41161
43383
  const headers = {
41162
43384
  ...baseHeaders3(pat),
41163
43385
  "Content-Type": "application/json"
@@ -41415,10 +43637,10 @@ var PreconditionSchema3 = exports_external.object({
41415
43637
  function baseHeaders4(pat) {
41416
43638
  return { Authorization: `Bearer ${pat}`, Accept: "application/json" };
41417
43639
  }
41418
- var ROUTE3 = "/api/sync/workspace-memory";
43640
+ var ROUTE5 = "/api/sync/workspace-memory";
41419
43641
  async function getWorkspaceMemory(args) {
41420
43642
  const { baseUrl, pat, fetchImpl = fetch } = args;
41421
- const url2 = `${baseUrl.replace(/\/$/, "")}${ROUTE3}`;
43643
+ const url2 = `${baseUrl.replace(/\/$/, "")}${ROUTE5}`;
41422
43644
  let res;
41423
43645
  try {
41424
43646
  res = await fetchImpl(url2, { method: "GET", headers: baseHeaders4(pat) });
@@ -41444,7 +43666,7 @@ async function getWorkspaceMemory(args) {
41444
43666
  }
41445
43667
  async function putWorkspaceMemory(args) {
41446
43668
  const { baseUrl, pat, bodyMd, expectedUpdatedAt, fetchImpl = fetch } = args;
41447
- const url2 = `${baseUrl.replace(/\/$/, "")}${ROUTE3}`;
43669
+ const url2 = `${baseUrl.replace(/\/$/, "")}${ROUTE5}`;
41448
43670
  const headers = {
41449
43671
  ...baseHeaders4(pat),
41450
43672
  "Content-Type": "application/json"
@@ -42360,8 +44582,8 @@ function createThrottledFetch(baseFetch, opts = {}) {
42360
44582
  import {
42361
44583
  existsSync as existsSync8,
42362
44584
  readdirSync as readdirSync7,
42363
- readFileSync as readFileSync25,
42364
- statSync as statSync16,
44585
+ readFileSync as readFileSync27,
44586
+ statSync as statSync17,
42365
44587
  unlinkSync as unlinkSync8
42366
44588
  } from "node:fs";
42367
44589
  import { join as join17 } from "node:path";
@@ -42544,87 +44766,8 @@ function refreshConflictsView(workspaceSlug) {
42544
44766
  }
42545
44767
 
42546
44768
  // src/lib/files/pull.ts
42547
- import { existsSync as existsSync7, readFileSync as readFileSync23, statSync as statSync14 } from "node:fs";
42548
-
42549
- // src/lib/files/extracted-text.ts
42550
- import { createHash as createHash4 } from "node:crypto";
42551
- var DERIVED_TEXT_VERSION = 2;
42552
- function yamlString3(value) {
42553
- return JSON.stringify(value);
42554
- }
42555
- var PARTIAL_WARNING = [
42556
- "> ⚠️ **PARTIAL EXTRACTION.** Some spans of this document came out as text and",
42557
- "> some did not. Which spans is NOT recorded, so this could be a clean document",
42558
- "> with mangled party names, or clean prose that degrades part-way through.",
42559
- "> Verify anything load-bearing against the source file."
42560
- ].join(`
42561
- `);
42562
- var NO_LOCAL_COPY_NOTE = [
42563
- "> ℹ️ **NO LOCAL COPY OF THIS FILE.** Plinth does not hold this file's bytes, so",
42564
- "> nothing was written under `files/` for it and the text below is the only copy",
42565
- "> in this mirror. The original lives wherever `source_uri` above points."
42566
- ].join(`
42567
- `);
42568
- function garbledBody(relPath, bytesInMirror) {
42569
- return [
42570
- "> \uD83D\uDD34 **NOT TEXT — body withheld.**",
42571
- ">",
42572
- "> Extraction produced characters that are not words. The usual cause is a PDF",
42573
- "> whose embedded fonts carry no ToUnicode map, so the glyph indices come out",
42574
- "> where the letters should be: a page reading *“Dated this 10th day of",
42575
- "> September”* extracts as `):-,:019`.",
42576
- ">",
42577
- "> The characters are NOT reproduced here on purpose. They read as text to a",
42578
- "> reader and to a search index, and they say nothing — writing them into the",
42579
- "> mirror would put a document into your corpus that appears readable and is",
42580
- "> not. They are excluded from Plinth's own search index for the same reason.",
42581
- ">",
42582
- bytesInMirror ? `> **Read the original instead:** \`files/${relPath}\`` : "> **There is no local original to read** — Plinth holds no bytes for this file." + " Follow `source_uri` above to the copy that has them."
42583
- ].join(`
42584
- `);
42585
- }
42586
- function renderExtractedText(doc2) {
42587
- const withheld = doc2.quality === "garbled";
42588
- const frontmatter = [
42589
- "---",
42590
- "plinth_generated: true",
42591
- `source_file: ${doc2.bytesInMirror ? yamlString3(`files/${doc2.relPath}`) : "null"}`,
42592
- `source_uri: ${doc2.sourceUri === undefined ? "unknown" : doc2.sourceUri === null ? "null" : yamlString3(doc2.sourceUri)}`,
42593
- `content_quality: ${doc2.quality ?? "unchecked"}`,
42594
- `body_withheld: ${withheld}`,
42595
- `extracted_chars: ${withheld ? 0 : doc2.text.length}`,
42596
- "---",
42597
- "",
42598
- "<!-- Generated by Plinth from the source file above. This file is DERIVED:",
42599
- " your edits here are overwritten on the next sync and are never uploaded.",
42600
- " Edit the source file instead. -->",
42601
- ""
42602
- ].join(`
42603
- `);
42604
- if (withheld) {
42605
- return `${frontmatter}${garbledBody(doc2.relPath, doc2.bytesInMirror)}
42606
- `;
42607
- }
42608
- const notices = [
42609
- ...doc2.quality === "partial" ? [PARTIAL_WARNING] : [],
42610
- ...doc2.bytesInMirror ? [] : [NO_LOCAL_COPY_NOTE]
42611
- ];
42612
- if (notices.length > 0) {
42613
- return `${frontmatter}${notices.join(`
42614
-
42615
- `)}
42616
-
42617
- ${doc2.text}
42618
- `;
42619
- }
42620
- return `${frontmatter}${doc2.text}
42621
- `;
42622
- }
42623
- function extractedTextHash(rendered) {
42624
- return createHash4("sha256").update(rendered, "utf8").digest("hex");
42625
- }
42626
-
42627
- // 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";
42628
44771
  async function pullFiles(args) {
42629
44772
  const { workspaceSlug, baseUrl, pat, fetchImpl } = args;
42630
44773
  let state = args.state;
@@ -42636,6 +44779,7 @@ async function pullFiles(args) {
42636
44779
  let textUnknown = 0;
42637
44780
  let pointerRows = 0;
42638
44781
  let symlinkRefused = 0;
44782
+ let stashFailed = 0;
42639
44783
  let cursor = args.cursor;
42640
44784
  let lastCursor = args.cursor;
42641
44785
  let cursorBlocked = false;
@@ -42657,7 +44801,8 @@ async function pullFiles(args) {
42657
44801
  suppressor: args.suppressor,
42658
44802
  isFileFrozen: args.isFileFrozen,
42659
44803
  loggedTombstoneSkips: args.loggedTombstoneSkips,
42660
- forceTextRewalk: args.forceTextRewalk
44804
+ forceTextRewalk: args.forceTextRewalk,
44805
+ loggedStashNotices: args.loggedStashNotices
42661
44806
  });
42662
44807
  state = outcome.state;
42663
44808
  if (!outcome.symlinkRefused) {
@@ -42679,6 +44824,10 @@ async function pullFiles(args) {
42679
44824
  symlinkRefused++;
42680
44825
  pageTextFailed = true;
42681
44826
  }
44827
+ if (outcome.stashFailed) {
44828
+ stashFailed++;
44829
+ pageTextFailed = true;
44830
+ }
42682
44831
  }
42683
44832
  if (args.commit && state !== pageBase)
42684
44833
  state = args.commit(pageBase, state);
@@ -42699,6 +44848,9 @@ async function pullFiles(args) {
42699
44848
  if (symlinkRefused > 0) {
42700
44849
  args.output?.warn(`${symlinkRefused} file ${symlinkRefused === 1 ? "row was" : "rows were"} left alone because a symlink sits on the path under files/ — syncing ${symlinkRefused === 1 ? "it" : "them"} would read or write outside your Plinth folder. Run \`plinth status\` to see which. Replace the link with a real folder or file and the next sync picks ${symlinkRefused === 1 ? "it" : "them"} up.`);
42701
44850
  }
44851
+ if (stashFailed > 0) {
44852
+ args.output?.warn(`${stashFailed} file ${stashFailed === 1 ? "was" : "were"} not updated from Plinth because the local copy could not be kept first — see the FILE_STASH_FAILED ${stashFailed === 1 ? "line" : "lines"}. Nothing local was replaced; the next sync tries again.`);
44853
+ }
42702
44854
  return {
42703
44855
  state,
42704
44856
  fetched,
@@ -42709,6 +44861,7 @@ async function pullFiles(args) {
42709
44861
  textUnknown,
42710
44862
  pointerRows,
42711
44863
  symlinkRefused,
44864
+ stashFailed,
42712
44865
  lastCursor
42713
44866
  };
42714
44867
  }
@@ -42722,7 +44875,16 @@ async function applyExtractedText(args) {
42722
44875
  }
42723
44876
  }
42724
44877
  async function materialiseExtractedText(args) {
42725
- const { workspaceSlug, baseUrl, pat, row, relPath, bytesInMirror, fetchImpl, output } = args;
44878
+ const {
44879
+ workspaceSlug,
44880
+ baseUrl,
44881
+ pat,
44882
+ row,
44883
+ relPath,
44884
+ bytesInMirror,
44885
+ fetchImpl,
44886
+ output
44887
+ } = args;
42726
44888
  const signal = extractedTextSignal(row);
42727
44889
  if (signal === "absent") {
42728
44890
  const removed = deleteExtractedText(workspaceSlug, relPath);
@@ -42815,7 +44977,7 @@ async function applyFileRow(args) {
42815
44977
  }
42816
44978
  let localBytes;
42817
44979
  try {
42818
- localBytes = readFileSync23(abs);
44980
+ localBytes = readFileSync25(abs);
42819
44981
  } catch {
42820
44982
  localBytes = undefined;
42821
44983
  }
@@ -42910,20 +45072,30 @@ async function applyFileRow(args) {
42910
45072
  output
42911
45073
  });
42912
45074
  }
42913
- if (!suppressor && existing && existing.content_hash !== row.content_hash && existsSync7(abs)) {
42914
- try {
42915
- const localBytes = readFileSync23(abs);
42916
- if (sha256Bytes(localBytes) !== existing.content_hash) {
42917
- const stashPath = stashFileConflict({ workspaceSlug, relPath, timestamp: now(), bytes: localBytes, side: "local" });
42918
- output?.warn(`Conflict on files/${relPath} → stashed local edit to ${stashPath} (cloud-wins on boot).`);
42919
- }
42920
- } catch {}
42921
- }
42922
45075
  const bytes = await downloadBytes(got.row.download_url, fetchImpl);
42923
45076
  if (sha256Bytes(bytes) !== row.content_hash) {
42924
45077
  output?.warn(`Integrity check failed for files/${relPath} (hash mismatch) — skipped this pull.`);
42925
45078
  return { state, action: "skipped" };
42926
45079
  }
45080
+ const stashFailure = stashUnpushedLocalEdit({
45081
+ workspaceSlug,
45082
+ relPath,
45083
+ abs,
45084
+ existing,
45085
+ rowHash: row.content_hash,
45086
+ ambient: suppressor !== undefined,
45087
+ now,
45088
+ output,
45089
+ loggedStashNotices: args.loggedStashNotices
45090
+ });
45091
+ if (stashFailure) {
45092
+ const key = `failed:${relPath}:${stashFailure.reason}:${stashFailure.code ?? ""}`;
45093
+ if (!args.loggedStashNotices?.has(key)) {
45094
+ args.loggedStashNotices?.add(key);
45095
+ output?.log(fileStashFailedLine({ path: relPath, ...stashFailure }));
45096
+ }
45097
+ return { state, action: "skipped", stashFailed: true };
45098
+ }
42927
45099
  suppressor?.registerWrite(abs, { bodyHash: row.content_hash, frontmatterHash: "" });
42928
45100
  writeFilesBytes(workspaceSlug, relPath, bytes);
42929
45101
  let text;
@@ -42953,7 +45125,7 @@ async function applyFileRow(args) {
42953
45125
  }
42954
45126
  let inode;
42955
45127
  try {
42956
- inode = statSync14(abs).ino;
45128
+ inode = statSync15(abs).ino;
42957
45129
  } catch {
42958
45130
  inode = undefined;
42959
45131
  }
@@ -43005,6 +45177,54 @@ async function applyPointerRow(args) {
43005
45177
  pointer: true
43006
45178
  };
43007
45179
  }
45180
+ function stashUnpushedLocalEdit(args) {
45181
+ const { workspaceSlug, relPath, abs, existing, output } = args;
45182
+ if (hasSymlinkedComponent(workspaceSlug, dirname12(abs)))
45183
+ return { reason: "symlink" };
45184
+ let st;
45185
+ try {
45186
+ st = lstatSync4(abs);
45187
+ } catch (err) {
45188
+ if (err?.code === "ENOENT")
45189
+ return null;
45190
+ return { reason: "unreadable", code: errCode(err) };
45191
+ }
45192
+ if (!st.isFile())
45193
+ return { reason: "not_a_file" };
45194
+ let localBytes;
45195
+ try {
45196
+ localBytes = readFileSync25(abs);
45197
+ } catch (err) {
45198
+ return { reason: "unreadable", code: errCode(err) };
45199
+ }
45200
+ const localHash = sha256Bytes(localBytes);
45201
+ if (localHash === args.rowHash || localHash === existing?.content_hash)
45202
+ return null;
45203
+ let stashed;
45204
+ try {
45205
+ stashed = stashFileConflictOnce({
45206
+ workspaceSlug,
45207
+ relPath,
45208
+ timestamp: args.now(),
45209
+ bytes: localBytes,
45210
+ side: "local"
45211
+ });
45212
+ } catch (err) {
45213
+ return { reason: "stash_io", code: errCode(err) };
45214
+ }
45215
+ const key = `reused:${stashed.path}:${args.rowHash}`;
45216
+ if (stashed.reused && args.loggedStashNotices?.has(key))
45217
+ return null;
45218
+ if (stashed.reused)
45219
+ args.loggedStashNotices?.add(key);
45220
+ output?.log(fileConflictLine({ path: relPath, stash: stashed.path, via: "pull", reused: stashed.reused }));
45221
+ output?.warn(stashed.reused ? `Conflict on files/${relPath} → your local edit is already kept at ${stashed.path}.` : `Conflict on files/${relPath} → stashed local edit to ${stashed.path} (cloud-wins${args.ambient ? "" : " on boot"}).`);
45222
+ return null;
45223
+ }
45224
+ function errCode(err) {
45225
+ const code = err?.code;
45226
+ return typeof code === "string" ? code : undefined;
45227
+ }
43008
45228
  function textIsCurrent(existing, row, workspaceSlug, relPath) {
43009
45229
  const signal = extractedTextSignal(row);
43010
45230
  if (signal === "unknown")
@@ -43021,7 +45241,7 @@ function textIsCurrent(existing, row, workspaceSlug, relPath) {
43021
45241
  }
43022
45242
 
43023
45243
  // src/lib/files/rejection-sync.ts
43024
- import { readFileSync as readFileSync24, statSync as statSync15 } from "node:fs";
45244
+ import { readFileSync as readFileSync26, statSync as statSync16 } from "node:fs";
43025
45245
 
43026
45246
  // src/lib/files/rejections-api.ts
43027
45247
  var RejectionsFeedSchema = exports_external.object({
@@ -43141,7 +45361,7 @@ function settledDisposition(args) {
43141
45361
  }
43142
45362
  let size = null;
43143
45363
  try {
43144
- const st = statSync15(absolute2);
45364
+ const st = statSync16(absolute2);
43145
45365
  size = st.isFile() ? st.size : null;
43146
45366
  } catch {
43147
45367
  size = null;
@@ -43154,7 +45374,7 @@ function settledDisposition(args) {
43154
45374
  return null;
43155
45375
  let bytes;
43156
45376
  try {
43157
- bytes = readFileSync24(absolute2);
45377
+ bytes = readFileSync26(absolute2);
43158
45378
  } catch {
43159
45379
  return null;
43160
45380
  }
@@ -43270,7 +45490,7 @@ function unlinkIfProvablyOurs(workspaceSlug, strategy, entry2, suppressor) {
43270
45490
  }
43271
45491
  let raw;
43272
45492
  try {
43273
- raw = readFileSync25(target, "utf8");
45493
+ raw = readFileSync27(target, "utf8");
43274
45494
  } catch (err) {
43275
45495
  if (err?.code === "ENOENT") {
43276
45496
  return { didUnlink: false };
@@ -43499,6 +45719,7 @@ async function runSync(workspaceSlug, deps = {}) {
43499
45719
  suppressor: deps.suppressor,
43500
45720
  isFileFrozen: deps.isFileFrozen,
43501
45721
  loggedTombstoneSkips: deps.loggedTombstoneSkips,
45722
+ loggedStashNotices: deps.loggedStashNotices,
43502
45723
  commit: commitState,
43503
45724
  forceTextRewalk: needsDerivedTextRewalk
43504
45725
  });
@@ -43596,7 +45817,8 @@ async function runSync(workspaceSlug, deps = {}) {
43596
45817
  fetchImpl,
43597
45818
  now,
43598
45819
  output: out,
43599
- suppressor: deps.suppressor
45820
+ suppressor: deps.suppressor,
45821
+ gate: deps.factFreshnessGate
43600
45822
  });
43601
45823
  if (factsResult.state !== state) {
43602
45824
  state = factsResult.state;
@@ -43853,7 +46075,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
43853
46075
  if (!suppressor && existingEntry && existingEntry.updated_at !== live.updated_at && relocation.kind !== "moved") {
43854
46076
  let localRaw = null;
43855
46077
  try {
43856
- localRaw = readFileSync25(entityFilePath(workspaceSlug, strategy.type, writeSlug), "utf8");
46078
+ localRaw = readFileSync27(entityFilePath(workspaceSlug, strategy.type, writeSlug), "utf8");
43857
46079
  } catch {
43858
46080
  localRaw = null;
43859
46081
  }
@@ -43877,7 +46099,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
43877
46099
  const hashes = computeMirrorHashes(strategy, content);
43878
46100
  let inode;
43879
46101
  try {
43880
- inode = statSync16(entityFilePath(workspaceSlug, strategy.type, writeSlug)).ino;
46102
+ inode = statSync17(entityFilePath(workspaceSlug, strategy.type, writeSlug)).ino;
43881
46103
  } catch {
43882
46104
  inode = undefined;
43883
46105
  }
@@ -43978,7 +46200,7 @@ var syncCommand = defineCommand({
43978
46200
  // src/commands/start.ts
43979
46201
  var SHUTDOWN_DRAIN_TIMEOUT_MS = 5000;
43980
46202
  var PULL_HEARTBEAT_MS = 900000;
43981
- function backfillStateInodes(workspaceSlug, state, statFn = (p) => statSync17(p)) {
46203
+ function backfillStateInodes(workspaceSlug, state, statFn = (p) => statSync18(p)) {
43982
46204
  let changed = false;
43983
46205
  const nextEntries = {};
43984
46206
  for (const [entityType, bucket] of Object.entries(state.entries)) {
@@ -44011,7 +46233,7 @@ function backfillStateInodes(workspaceSlug, state, statFn = (p) => statSync17(p)
44011
46233
  return { state, changed: false };
44012
46234
  return { state: { ...state, entries: nextEntries }, changed: true };
44013
46235
  }
44014
- function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync26(p, "utf8")) {
46236
+ function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync28(p, "utf8")) {
44015
46237
  let changed = false;
44016
46238
  const nextEntries = {};
44017
46239
  for (const [entityType, bucket] of Object.entries(state.entries)) {
@@ -44023,7 +46245,7 @@ function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync2
44023
46245
  }
44024
46246
  try {
44025
46247
  const path5 = entityFilePath(workspaceSlug, entityType, entry2.slug);
44026
- const bodyLen = import_gray_matter13.default(readFn(path5)).content.length;
46248
+ const bodyLen = import_gray_matter14.default(readFn(path5)).content.length;
44027
46249
  nextBucket[id] = { ...entry2, body_len: bodyLen };
44028
46250
  changed = true;
44029
46251
  } catch {
@@ -44059,18 +46281,22 @@ function makeWatchHandler(deps) {
44059
46281
  applyState(base2, update(base2));
44060
46282
  };
44061
46283
  const handleFilesEvent = async (relPath, absolutePath, exists) => {
46284
+ const eventOp = exists ? "upsert" : "delete";
44062
46285
  try {
44063
- if (isFileFrozen?.(relPath))
46286
+ if (isFileFrozen?.(relPath)) {
46287
+ output?.log(filePushSkippedLine({ path: relPath, op: eventOp, reason: "frozen" }));
44064
46288
  return;
46289
+ }
44065
46290
  try {
44066
46291
  filesSubtreePath(workspaceSlug, relPath);
44067
46292
  } catch (err) {
44068
46293
  if (!(err instanceof MirrorSymlinkError))
44069
46294
  throw err;
46295
+ output?.log(filePushSkippedLine({ path: relPath, op: eventOp, reason: "symlink" }));
44070
46296
  applyFromCurrent((base2) => recordFileSymlinkRefused({
44071
46297
  state: base2,
44072
46298
  relPath,
44073
- op: exists ? "upsert" : "delete",
46299
+ op: eventOp,
44074
46300
  now: now ?? (() => new Date)
44075
46301
  }));
44076
46302
  spinner.warn(`files/${relPath} was not synced: a symlink on its path would read or write outside your Plinth folder. Run \`plinth status\`.`);
@@ -44089,6 +46315,7 @@ function makeWatchHandler(deps) {
44089
46315
  if (fileGate) {
44090
46316
  const op = classifyFileDestructive({ relPath, absolutePath, exists: false }, getState());
44091
46317
  if (op) {
46318
+ output?.log(filePushSkippedLine({ path: relPath, op: eventOp, reason: "held" }));
44092
46319
  fileGate.consider(op);
44093
46320
  return;
44094
46321
  }
@@ -44103,6 +46330,9 @@ function makeWatchHandler(deps) {
44103
46330
  state: base2,
44104
46331
  deps: { fetchImpl, now, output, suppressor }
44105
46332
  });
46333
+ if (result.outcome.kind === "ignored") {
46334
+ output?.log(filePushSkippedLine({ path: relPath, op: eventOp, reason: "untracked" }));
46335
+ }
44106
46336
  applyState(base2, clearReportedSafeFileRejected(result.state, relPath));
44107
46337
  reportFileOutcome(spinner, workspaceSlug, getState(), result.outcome);
44108
46338
  });
@@ -44110,15 +46340,18 @@ function makeWatchHandler(deps) {
44110
46340
  }
44111
46341
  let st;
44112
46342
  try {
44113
- st = statSync17(absolutePath);
46343
+ st = statSync18(absolutePath);
44114
46344
  } catch (err) {
44115
- if (err?.code === "ENOENT")
46345
+ if (err?.code === "ENOENT") {
46346
+ output?.log(filePushSkippedLine({ path: relPath, op: eventOp, reason: "vanished" }));
44116
46347
  return;
46348
+ }
44117
46349
  throw err;
44118
46350
  }
44119
46351
  if (st.size > MAX_WORKSPACE_FILE_BYTES) {
44120
46352
  const tracked = getFileEntry(getState(), relPath);
44121
46353
  if (tracked && tracked.size_bytes === st.size) {
46354
+ output?.log(filePushSkippedLine({ path: relPath, op: eventOp, reason: "over_cap_acked" }));
44122
46355
  spinner.update(`Watching ${workspaceSlug} · ${watchingSummary(getState())}`);
44123
46356
  return;
44124
46357
  }
@@ -44141,17 +46374,19 @@ function makeWatchHandler(deps) {
44141
46374
  spinner.update(`Watching ${workspaceSlug} · ${watchingSummary(getState())}`);
44142
46375
  return;
44143
46376
  }
44144
- const bytes = readFileSync26(absolutePath);
46377
+ const bytes = readFileSync28(absolutePath);
44145
46378
  const hash2 = sha256Bytes(bytes);
44146
46379
  if (suppressor?.shouldSuppressUpsert(absolutePath, {
44147
46380
  bodyHash: hash2,
44148
46381
  frontmatterHash: ""
44149
46382
  })) {
46383
+ output?.log(filePushSkippedLine({ path: relPath, op: eventOp, reason: "pull_echo" }));
44150
46384
  return;
44151
46385
  }
44152
46386
  if (fileGate) {
44153
46387
  const op = classifyFileDestructive({ relPath, absolutePath, exists: true, size: st.size, hash: hash2 }, getState());
44154
46388
  if (op) {
46389
+ output?.log(filePushSkippedLine({ path: relPath, op: eventOp, reason: "held" }));
44155
46390
  fileGate.consider(op);
44156
46391
  return;
44157
46392
  }
@@ -44168,6 +46403,9 @@ function makeWatchHandler(deps) {
44168
46403
  prepared: { bytes, hash: hash2, size: st.size, inode: st.ino },
44169
46404
  deps: { fetchImpl, now, output, suppressor }
44170
46405
  });
46406
+ if (result.outcome.kind === "noop") {
46407
+ output?.log(filePushSkippedLine({ path: relPath, op: eventOp, reason: "unchanged" }));
46408
+ }
44171
46409
  applyState(base2, result.outcome.kind === "over_cap" ? result.state : clearFileRejected(result.state, relPath));
44172
46410
  reportFileOutcome(spinner, workspaceSlug, getState(), result.outcome);
44173
46411
  });
@@ -44179,7 +46417,7 @@ function makeWatchHandler(deps) {
44179
46417
  applyFromCurrent((base2) => recordFileUnsynced({
44180
46418
  state: base2,
44181
46419
  relPath,
44182
- op: exists ? "upsert" : "delete",
46420
+ op: eventOp,
44183
46421
  err,
44184
46422
  now: now ?? (() => new Date)
44185
46423
  }));
@@ -44187,7 +46425,7 @@ function makeWatchHandler(deps) {
44187
46425
  applyFromCurrent((base2) => recordFileRejected({
44188
46426
  state: base2,
44189
46427
  relPath,
44190
- op: exists ? "upsert" : "delete",
46428
+ op: eventOp,
44191
46429
  err,
44192
46430
  now: now ?? (() => new Date)
44193
46431
  }));
@@ -44200,7 +46438,7 @@ function makeWatchHandler(deps) {
44200
46438
  if (suppressor) {
44201
46439
  let raw = null;
44202
46440
  try {
44203
- raw = readFileSync26(absolutePath, "utf8");
46441
+ raw = readFileSync28(absolutePath, "utf8");
44204
46442
  } catch {
44205
46443
  raw = null;
44206
46444
  }
@@ -44239,7 +46477,7 @@ function makeWatchHandler(deps) {
44239
46477
  if (suppressor) {
44240
46478
  let raw = null;
44241
46479
  try {
44242
- raw = readFileSync26(absolutePath, "utf8");
46480
+ raw = readFileSync28(absolutePath, "utf8");
44243
46481
  } catch {
44244
46482
  raw = null;
44245
46483
  }
@@ -44278,7 +46516,7 @@ function makeWatchHandler(deps) {
44278
46516
  if (suppressor) {
44279
46517
  let raw = null;
44280
46518
  try {
44281
- raw = readFileSync26(absolutePath, "utf8");
46519
+ raw = readFileSync28(absolutePath, "utf8");
44282
46520
  } catch {
44283
46521
  raw = null;
44284
46522
  }
@@ -44317,7 +46555,7 @@ function makeWatchHandler(deps) {
44317
46555
  if (suppressor) {
44318
46556
  let raw = null;
44319
46557
  try {
44320
- raw = readFileSync26(absolutePath, "utf8");
46558
+ raw = readFileSync28(absolutePath, "utf8");
44321
46559
  } catch {
44322
46560
  raw = null;
44323
46561
  }
@@ -44356,7 +46594,7 @@ function makeWatchHandler(deps) {
44356
46594
  if (suppressor) {
44357
46595
  let raw = null;
44358
46596
  try {
44359
- raw = readFileSync26(absolutePath, "utf8");
46597
+ raw = readFileSync28(absolutePath, "utf8");
44360
46598
  } catch {
44361
46599
  raw = null;
44362
46600
  }
@@ -44402,7 +46640,7 @@ function makeWatchHandler(deps) {
44402
46640
  const strategy = getStrategy(classifiedFrom.entityType);
44403
46641
  let toRaw = null;
44404
46642
  try {
44405
- toRaw = readFileSync26(event.toPath, "utf8");
46643
+ toRaw = readFileSync28(event.toPath, "utf8");
44406
46644
  } catch {
44407
46645
  toRaw = null;
44408
46646
  }
@@ -44471,19 +46709,26 @@ function makeWatchHandler(deps) {
44471
46709
  if (event.kind === "upsert") {
44472
46710
  try {
44473
46711
  ownEcho = suppressor?.shouldSuppressUpsert(absolutePath, {
44474
- bodyHash: stableHash(readFileSync26(absolutePath, "utf8")),
46712
+ bodyHash: stableHash(readFileSync28(absolutePath, "utf8")),
44475
46713
  frontmatterHash: ""
44476
46714
  }) === true;
44477
46715
  } catch {
44478
46716
  ownEcho = true;
44479
46717
  }
44480
46718
  }
44481
- const warning2 = memoryFactEventWarning(classifyMemoryFactEvent({
46719
+ const verdict = classifyMemoryFactEvent({
44482
46720
  path: factPath,
44483
46721
  kind: event.kind === "upsert" ? "upsert" : "unlink",
44484
46722
  ownEcho,
44485
46723
  known: getMemoryFactEntry(getState(), factPath.relPath) !== undefined
44486
- }));
46724
+ });
46725
+ if (verdict.kind === "generated" || verdict.kind === "deleted") {
46726
+ applyFromCurrent((base3) => {
46727
+ const raised = raiseFactPullReason(base3, "watcher_edit", (now ?? (() => new Date))().toISOString());
46728
+ return raised === base3 ? base3 : commitState(base3, raised);
46729
+ });
46730
+ }
46731
+ const warning2 = memoryFactEventWarning(verdict);
44487
46732
  if (warning2)
44488
46733
  spinner.warn(warning2);
44489
46734
  spinner.update(`Watching ${workspaceSlug} · ${watchingSummary(getState())}`);
@@ -44519,7 +46764,7 @@ function makeWatchHandler(deps) {
44519
46764
  if (strategy) {
44520
46765
  let raw = null;
44521
46766
  try {
44522
- raw = readFileSync26(absolutePath, "utf8");
46767
+ raw = readFileSync28(absolutePath, "utf8");
44523
46768
  } catch {
44524
46769
  raw = null;
44525
46770
  }
@@ -44964,6 +47209,7 @@ async function runStart(workspaceSlug, deps = {}) {
44964
47209
  out.warn(`Artist memory reconcile skipped: ${msg}`);
44965
47210
  }
44966
47211
  const loggedTombstoneSkips = new Set;
47212
+ const loggedStashNotices = new Set;
44967
47213
  const initial = await runSync(workspaceSlug, {
44968
47214
  fetchImpl,
44969
47215
  keychain,
@@ -44973,7 +47219,8 @@ async function runStart(workspaceSlug, deps = {}) {
44973
47219
  logSink: daemonLog,
44974
47220
  isFrozen,
44975
47221
  isFileFrozen,
44976
- loggedTombstoneSkips
47222
+ loggedTombstoneSkips,
47223
+ loggedStashNotices
44977
47224
  });
44978
47225
  if (initial.exitCode !== 0)
44979
47226
  return initial;
@@ -45045,6 +47292,21 @@ async function runStart(workspaceSlug, deps = {}) {
45045
47292
  return r ? { entityType: r.entityType, slug: r.entitySlug } : null;
45046
47293
  };
45047
47294
  const suppressor = new PullSuppressor;
47295
+ const setDaemonState = (next) => {
47296
+ state = next;
47297
+ };
47298
+ const factFreshnessGate = createFactFreshnessGate({
47299
+ workspaceSlug: slug,
47300
+ baseUrl: entry2.api_base_url,
47301
+ pat,
47302
+ getState: () => state,
47303
+ setState: setDaemonState,
47304
+ commitState,
47305
+ fetchImpl,
47306
+ now: deps.now,
47307
+ suppressor,
47308
+ output: daemonLog
47309
+ });
45048
47310
  const flushHeldOp = (op) => flushHeldEntityOp({
45049
47311
  echo: { watcher: "live", suppressor },
45050
47312
  workspaceSlug: slug,
@@ -45204,9 +47466,11 @@ async function runStart(workspaceSlug, deps = {}) {
45204
47466
  sink: quietSink,
45205
47467
  logSink: daemonLog,
45206
47468
  suppressor,
47469
+ factFreshnessGate,
45207
47470
  isFrozen,
45208
47471
  isFileFrozen,
45209
- loggedTombstoneSkips
47472
+ loggedTombstoneSkips,
47473
+ loggedStashNotices
45210
47474
  });
45211
47475
  if (result.exitCode === 0) {
45212
47476
  const applied = result.applied ?? 0;
@@ -45226,6 +47490,23 @@ async function runStart(workspaceSlug, deps = {}) {
45226
47490
  spinner.success(`↓ ${n} ${n === 1 ? "change" : "changes"} from cloud`);
45227
47491
  spinner.update(`Watching ${slug} · ${watchingSummary(state)}`);
45228
47492
  },
47493
+ afterPollOutcome: composeAfterPollOutcome((ok) => {
47494
+ if (!ok)
47495
+ factFreshnessGate.onPollFailed();
47496
+ }, memoryLaneCallback({
47497
+ workspaceSlug: slug,
47498
+ baseUrl: entry2.api_base_url,
47499
+ pat,
47500
+ mutex,
47501
+ commitState,
47502
+ getState: () => state,
47503
+ setState: setDaemonState,
47504
+ warn: (notice) => spinner.warn(notice),
47505
+ fetchImpl,
47506
+ now: deps.now,
47507
+ suppressor,
47508
+ output: daemonLog
47509
+ })),
45229
47510
  afterPoll: () => {
45230
47511
  (async () => {
45231
47512
  state = loadState(slug);
@@ -45707,7 +47988,7 @@ var startCommand = defineCommand({
45707
47988
  });
45708
47989
 
45709
47990
  // src/commands/status.ts
45710
- import { statSync as statSync19 } from "node:fs";
47991
+ import { statSync as statSync20 } from "node:fs";
45711
47992
 
45712
47993
  // src/lib/context/context-walls.ts
45713
47994
  function prepare(raw) {
@@ -46010,8 +48291,8 @@ function assessContextWalls(workspaceSlug, deps = {}) {
46010
48291
 
46011
48292
  // src/lib/build-stamp.ts
46012
48293
  import { execFileSync as execFileSync4 } from "node:child_process";
46013
- import { existsSync as existsSync10, mkdirSync as mkdirSync11, readFileSync as readFileSync27, writeFileSync as writeFileSync8 } from "node:fs";
46014
- import { dirname as dirname12, join as join18 } from "node:path";
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";
46015
48296
  import { fileURLToPath } from "node:url";
46016
48297
  var BUILD_STAMP_FILENAME = "build-stamp.json";
46017
48298
  function stampPath(distDir) {
@@ -46034,7 +48315,7 @@ function readHead(repoDir) {
46034
48315
  }
46035
48316
  function readBuildStamp(distDir) {
46036
48317
  try {
46037
- const parsed = JSON.parse(readFileSync27(stampPath(distDir), "utf8"));
48318
+ const parsed = JSON.parse(readFileSync29(stampPath(distDir), "utf8"));
46038
48319
  if (parsed && typeof parsed.sha === "string" && parsed.sha.length > 0 && typeof parsed.dirty === "boolean" && typeof parsed.built_at === "string") {
46039
48320
  return { sha: parsed.sha, dirty: parsed.dirty, built_at: parsed.built_at };
46040
48321
  }
@@ -46048,7 +48329,7 @@ function findRepoRoot(fromDir) {
46048
48329
  for (;; ) {
46049
48330
  if (existsSync10(join18(dir, ".git")))
46050
48331
  return dir;
46051
- const parent = dirname12(dir);
48332
+ const parent = dirname13(dir);
46052
48333
  if (parent === dir)
46053
48334
  return null;
46054
48335
  dir = parent;
@@ -46072,35 +48353,35 @@ function checkBuildStaleness(deps = {}) {
46072
48353
  const metaUrl = deps.metaUrl ?? import.meta.url;
46073
48354
  let distDir;
46074
48355
  try {
46075
- distDir = dirname12(fileURLToPath(metaUrl));
48356
+ distDir = dirname13(fileURLToPath(metaUrl));
46076
48357
  } catch {
46077
48358
  return { kind: "unknown" };
46078
48359
  }
46079
- const stamp = readStamp(distDir);
46080
- if (!stamp)
48360
+ const stamp2 = readStamp(distDir);
48361
+ if (!stamp2)
46081
48362
  return { kind: "unknown" };
46082
48363
  const repoDir = findRepo(distDir);
46083
48364
  if (!repoDir)
46084
48365
  return { kind: "unknown" };
46085
- const head = probe2(repoDir, stamp.sha);
48366
+ const head = probe2(repoDir, stamp2.sha);
46086
48367
  if (!head)
46087
48368
  return { kind: "unknown" };
46088
- if (head.sha !== stamp.sha) {
46089
- return { kind: "stale", reason: "head-moved", stamp, head };
48369
+ if (head.sha !== stamp2.sha) {
48370
+ return { kind: "stale", reason: "head-moved", stamp: stamp2, head };
46090
48371
  }
46091
- if (head.dirty && !stamp.dirty) {
46092
- return { kind: "stale", reason: "tree-dirty", stamp, head };
48372
+ if (head.dirty && !stamp2.dirty) {
48373
+ return { kind: "stale", reason: "tree-dirty", stamp: stamp2, head };
46093
48374
  }
46094
- return { kind: "fresh", stamp, head };
48375
+ return { kind: "fresh", stamp: stamp2, head };
46095
48376
  }
46096
48377
 
46097
48378
  // src/lib/memory-links.ts
46098
- import { readFileSync as readFileSync28 } from "node:fs";
48379
+ import { readFileSync as readFileSync30 } from "node:fs";
46099
48380
  import { relative as relative15, sep as sep8 } from "node:path";
46100
48381
  var MEMORY_FILE_RE = /^(workspace-memory\.md|user-memory\.md|artists\/[^/]+\/memory\.md|memory\/[^/]+\.md|memory\/private\/[^/]+\.md)$/i;
46101
48382
  function scanMemoryLinks(deps) {
46102
48383
  const listFiles = deps.listFiles ?? ((r) => walkSubtreeFiles(r, isVcsInternal));
46103
- const readFile = deps.readFile ?? ((p) => readFileSync28(p, "utf8"));
48384
+ const readFile = deps.readFile ?? ((p) => readFileSync30(p, "utf8"));
46104
48385
  let root;
46105
48386
  let absPaths;
46106
48387
  try {
@@ -46144,10 +48425,10 @@ function scanMemoryLinks(deps) {
46144
48425
  init_output();
46145
48426
 
46146
48427
  // src/lib/pending-writes.ts
46147
- import { readFileSync as readFileSync29, statSync as statSync18 } from "node:fs";
48428
+ import { readFileSync as readFileSync31, statSync as statSync19 } from "node:fs";
46148
48429
  import { relative as relative16 } from "node:path";
46149
- var import_gray_matter14 = __toESM(require_gray_matter(), 1);
46150
- var EMPTY2 = { writes: [], count: 0 };
48430
+ var import_gray_matter15 = __toESM(require_gray_matter(), 1);
48431
+ var EMPTY3 = { writes: [], count: 0 };
46151
48432
  function reportedElsewhere(state) {
46152
48433
  const entities = new Set;
46153
48434
  const files = new Set;
@@ -46165,7 +48446,7 @@ function scanPendingWrites(args) {
46165
48446
  try {
46166
48447
  mirrorRootPath = workspaceMirrorPath(workspaceSlug);
46167
48448
  } catch {
46168
- return EMPTY2;
48449
+ return EMPTY3;
46169
48450
  }
46170
48451
  const ignored = watcherIgnoreMatcher(mirrorRootPath);
46171
48452
  const reported = reportedElsewhere(state);
@@ -46184,7 +48465,7 @@ function scanPendingWrites(args) {
46184
48465
  const entry3 = getFileEntry(state, fileMatch.relPath);
46185
48466
  let size;
46186
48467
  try {
46187
- size = statSync18(absolutePath).size;
48468
+ size = statSync19(absolutePath).size;
46188
48469
  } catch {
46189
48470
  continue;
46190
48471
  }
@@ -46192,7 +48473,7 @@ function scanPendingWrites(args) {
46192
48473
  continue;
46193
48474
  let hash2;
46194
48475
  try {
46195
- hash2 = sha256Bytes(readFileSync29(absolutePath));
48476
+ hash2 = sha256Bytes(readFileSync31(absolutePath));
46196
48477
  } catch {
46197
48478
  continue;
46198
48479
  }
@@ -46213,7 +48494,7 @@ function scanPendingWrites(args) {
46213
48494
  continue;
46214
48495
  let liveInode;
46215
48496
  try {
46216
- liveInode = statSync18(absolutePath).ino;
48497
+ liveInode = statSync19(absolutePath).ino;
46217
48498
  } catch {
46218
48499
  liveInode = undefined;
46219
48500
  }
@@ -46223,14 +48504,14 @@ function scanPendingWrites(args) {
46223
48504
  continue;
46224
48505
  let content;
46225
48506
  try {
46226
- content = readFileSync29(absolutePath, "utf8");
48507
+ content = readFileSync31(absolutePath, "utf8");
46227
48508
  } catch {
46228
48509
  continue;
46229
48510
  }
46230
48511
  if (!entry2) {
46231
48512
  let frontmatter;
46232
48513
  try {
46233
- frontmatter = import_gray_matter14.default(content).data ?? {};
48514
+ frontmatter = import_gray_matter15.default(content).data ?? {};
46234
48515
  } catch {
46235
48516
  continue;
46236
48517
  }
@@ -46461,7 +48742,7 @@ function renderBuild(out, staleness) {
46461
48742
  }
46462
48743
  function lastActivity(now) {
46463
48744
  try {
46464
- const mtimeMs = statSync19(daemonLogPath()).mtimeMs;
48745
+ const mtimeMs = statSync20(daemonLogPath()).mtimeMs;
46465
48746
  return formatRelative(now.getTime() - mtimeMs);
46466
48747
  } catch {
46467
48748
  return null;
@@ -46617,7 +48898,32 @@ async function runStatus(args, deps = {}) {
46617
48898
  }
46618
48899
  }
46619
48900
  out.info("");
48901
+ const factFreshness = localState.memory_facts?.freshness;
48902
+ if (factFreshness !== undefined) {
48903
+ const at = localState.memory_facts?.freshness_checked_at;
48904
+ const stamp2 = at ? readStamp(at) : undefined;
48905
+ const asOf = stamp2 ? ` (as of ${stamp2})` : "";
48906
+ const daemonLive = daemon !== null || daemonHolder()?.role === "daemon";
48907
+ if (factFreshness === "current") {
48908
+ out.info(row("Memory facts", `up to date${asOf}.`));
48909
+ if (!daemonLive) {
48910
+ out.info(cont(`The daemon is not running, so nothing has been checked since` + `${stamp2 ? ` ${stamp2}` : ""}. Start it with \`plinth start\`.`));
48911
+ } else if (!stamp2) {
48912
+ out.info(cont(`When that was last confirmed is not recorded.`));
48913
+ }
48914
+ } else if (factFreshness === "behind") {
48915
+ out.info(row("Memory facts", `behind${asOf} - the daemon is downloading the latest.`));
48916
+ if (!daemonLive) {
48917
+ out.info(cont(`Start it with \`plinth start\` and the copy catches up.`));
48918
+ }
48919
+ } else {
48920
+ out.info(row("Memory facts", `not checked${asOf} - whether this copy is up to date is unknown.`));
48921
+ out.info(cont(daemonLive ? "The daemon keeps trying; the facts you have are still readable." : `The daemon is not running. Start it with \`plinth start\`.`));
48922
+ }
48923
+ out.info("");
48924
+ }
46620
48925
  const rejected = listRejected(localState);
48926
+ const freshness = Object.values(localState.memory_sources?.unwritten ?? {}).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
46621
48927
  const localRejectedPaths = new Set(rejected.filter((r) => r.target === "file").map((r) => r.path));
46622
48928
  const teamRejected = listTeamRejected(localState).filter((r) => !localRejectedPaths.has(r.path));
46623
48929
  const MAX_SHOWN = 10;
@@ -46636,6 +48942,15 @@ async function runStatus(args, deps = {}) {
46636
48942
  out.info(cont(`…and ${rejected.length - MAX_SHOWN} more (see ~/.plinth/daemon.log).`));
46637
48943
  }
46638
48944
  }
48945
+ if (freshness.length > 0) {
48946
+ out.info(row("Memory context", `${freshness.length} file${freshness.length === 1 ? "" : "s"} could not be marked with ${freshness.length === 1 ? "its" : "their"} current memory state. Your edits are untouched.`));
48947
+ for (const r of freshness.slice(0, MAX_SHOWN)) {
48948
+ out.info(cont(r.message));
48949
+ }
48950
+ if (freshness.length > MAX_SHOWN) {
48951
+ out.info(cont(`…and ${freshness.length - MAX_SHOWN} more (see ~/.plinth/daemon.log).`));
48952
+ }
48953
+ }
46639
48954
  if (teamRejected.length > 0) {
46640
48955
  out.info(cont(""));
46641
48956
  out.info(cont(teamRejectedHeadline(teamRejected.length, localState)));
@@ -47073,7 +49388,7 @@ var versionCommand = defineCommand({
47073
49388
  });
47074
49389
 
47075
49390
  // src/commands/voice-gate.ts
47076
- import { readFileSync as readFileSync32 } from "node:fs";
49391
+ import { readFileSync as readFileSync34 } from "node:fs";
47077
49392
 
47078
49393
  // src/lib/voice-gate/api.ts
47079
49394
  var VoiceGateSurfaces = [
@@ -47178,7 +49493,7 @@ async function callVoiceGate(req) {
47178
49493
  import { createHash as createHash5 } from "node:crypto";
47179
49494
  import {
47180
49495
  mkdirSync as mkdirSync12,
47181
- readFileSync as readFileSync30,
49496
+ readFileSync as readFileSync32,
47182
49497
  renameSync as renameSync8,
47183
49498
  unlinkSync as unlinkSync9,
47184
49499
  writeFileSync as writeFileSync9
@@ -47209,7 +49524,7 @@ function emptyFile() {
47209
49524
  }
47210
49525
  function load2(workspaceSlug) {
47211
49526
  try {
47212
- const raw = readFileSync30(attemptsPath(workspaceSlug), "utf8");
49527
+ const raw = readFileSync32(attemptsPath(workspaceSlug), "utf8");
47213
49528
  const parsed = AttemptsFileSchema.safeParse(JSON.parse(raw));
47214
49529
  return parsed.success ? parsed.data : emptyFile();
47215
49530
  } catch {
@@ -47372,7 +49687,7 @@ function denyUngated(detail) {
47372
49687
  }
47373
49688
 
47374
49689
  // src/lib/voice-gate/payload.ts
47375
- import { readFileSync as readFileSync31, statSync as statSync20 } from "node:fs";
49690
+ import { readFileSync as readFileSync33, statSync as statSync21 } from "node:fs";
47376
49691
  import { homedir as homedir2 } from "node:os";
47377
49692
  import { isAbsolute as isAbsolute4, resolve as resolve6 } from "node:path";
47378
49693
  var MAX_BODY_BYTES = 256 * 1024;
@@ -47529,11 +49844,11 @@ function isHelperInvocation(tokens) {
47529
49844
  return helperIndex === start + 1 && /(^|\/)python[0-9.]*$/.test(prev);
47530
49845
  }
47531
49846
  function defaultReadBodyFile(path5) {
47532
- const stat4 = statSync20(path5);
49847
+ const stat4 = statSync21(path5);
47533
49848
  if (stat4.size > MAX_BODY_BYTES) {
47534
49849
  throw new Error(`draft body is ${stat4.size} bytes, over the ${MAX_BODY_BYTES}-byte gate limit`);
47535
49850
  }
47536
- return readFileSync31(path5, "utf8");
49851
+ return readFileSync33(path5, "utf8");
47537
49852
  }
47538
49853
  function parseRecipients(...raw) {
47539
49854
  const all = raw.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0);
@@ -47893,7 +50208,7 @@ async function runVoiceGateDirect(args, deps = {}) {
47893
50208
  let text;
47894
50209
  if (args.file) {
47895
50210
  try {
47896
- text = readFileSync32(args.file, "utf8");
50211
+ text = readFileSync34(args.file, "utf8");
47897
50212
  } catch (err) {
47898
50213
  const msg = err instanceof Error ? err.message : "unknown error";
47899
50214
  out.error(`Could not read ${args.file}: ${msg}`);