@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/CHANGELOG.md +55 -0
- package/README.md +1 -1
- package/dist/build-stamp.json +2 -2
- package/dist/cli.js +2622 -307
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -7584,7 +7584,7 @@ function _getBuiltinFlags(long, short, userNames, userAliases) {
|
|
|
7584
7584
|
// package.json
|
|
7585
7585
|
var package_default = {
|
|
7586
7586
|
name: "@plinth-music/cli",
|
|
7587
|
-
version: "0.
|
|
7587
|
+
version: "0.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 (!
|
|
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 &&
|
|
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 = "
|
|
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
|
-
|
|
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
|
|
33705
|
-
import { existsSync as existsSync9, readFileSync as
|
|
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
|
|
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
|
-
|
|
33972
|
-
|
|
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
|
|
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 =
|
|
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
|
|
34283
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
36404
|
-
|
|
36405
|
-
|
|
36406
|
-
|
|
36407
|
-
|
|
36408
|
-
|
|
36409
|
-
|
|
36410
|
-
|
|
36411
|
-
|
|
36412
|
-
|
|
36413
|
-
|
|
36414
|
-
|
|
36415
|
-
|
|
36416
|
-
|
|
36417
|
-
|
|
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
|
-
|
|
36420
|
-
|
|
36421
|
-
|
|
36422
|
-
|
|
36423
|
-
|
|
36424
|
-
|
|
36425
|
-
|
|
36426
|
-
|
|
36427
|
-
|
|
36428
|
-
|
|
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: ${
|
|
36434
|
-
lines.push(`name: ${
|
|
36435
|
-
lines.push(`description: ${
|
|
36436
|
-
lines.push(`type: ${
|
|
36437
|
-
lines.push(`status: ${
|
|
36438
|
-
lines.push(`source: ${
|
|
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: ${
|
|
37215
|
+
lines.push(`generality: ${yamlString2(fact.generality)}`);
|
|
36441
37216
|
if (fact.artist_id)
|
|
36442
|
-
lines.push(`artist_id: ${
|
|
37217
|
+
lines.push(`artist_id: ${yamlString2(fact.artist_id)}`);
|
|
36443
37218
|
if (fact.artist_slug)
|
|
36444
|
-
lines.push(`artist: ${
|
|
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
|
|
36498
|
-
|
|
36499
|
-
|
|
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
|
|
36563
|
-
|
|
36564
|
-
|
|
36565
|
-
|
|
36566
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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 ${
|
|
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 ${
|
|
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
|
|
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" :
|
|
40021
|
-
`artist_name: ${row.artist_name === null ? "null" :
|
|
40022
|
-
`calendar_name: ${
|
|
40023
|
-
`generated_at: ${
|
|
40024
|
-
`window_start: ${
|
|
40025
|
-
`window_end: ${
|
|
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
|
|
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: ${
|
|
40107
|
-
`window_start: ${
|
|
40108
|
-
`window_end: ${
|
|
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 ${
|
|
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
|
|
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: ${
|
|
40205
|
-
`last_refresh_error: ${
|
|
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 ${
|
|
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
|
|
40400
|
-
import { readFileSync as
|
|
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
|
|
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 =
|
|
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 =
|
|
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 = { ...
|
|
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, ...
|
|
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
|
|
40728
|
-
import { readFileSync as
|
|
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
|
|
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) =>
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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(/\/$/, "")}${
|
|
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(/\/$/, "")}${
|
|
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
|
|
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(/\/$/, "")}${
|
|
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(/\/$/, "")}${
|
|
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
|
|
42364
|
-
statSync as
|
|
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
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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) =>
|
|
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) =>
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 =
|
|
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:
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
46014
|
-
import { dirname as
|
|
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(
|
|
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 =
|
|
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 =
|
|
48356
|
+
distDir = dirname13(fileURLToPath(metaUrl));
|
|
46076
48357
|
} catch {
|
|
46077
48358
|
return { kind: "unknown" };
|
|
46078
48359
|
}
|
|
46079
|
-
const
|
|
46080
|
-
if (!
|
|
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,
|
|
48366
|
+
const head = probe2(repoDir, stamp2.sha);
|
|
46086
48367
|
if (!head)
|
|
46087
48368
|
return { kind: "unknown" };
|
|
46088
|
-
if (head.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 && !
|
|
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
|
|
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) =>
|
|
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
|
|
48428
|
+
import { readFileSync as readFileSync31, statSync as statSync19 } from "node:fs";
|
|
46148
48429
|
import { relative as relative16 } from "node:path";
|
|
46149
|
-
var
|
|
46150
|
-
var
|
|
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
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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}`);
|