@equationalapplications/core-llm-wiki 7.11.2 → 7.13.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/README.md +22 -0
- package/dist/{chunk-522SCN2E.mjs → chunk-PCRYIUV5.mjs} +2299 -1408
- package/dist/chunk-PCRYIUV5.mjs.map +1 -0
- package/dist/index.d.mts +10 -3
- package/dist/index.d.ts +10 -3
- package/dist/index.js +5336 -4465
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +645 -669
- package/dist/index.mjs.map +1 -1
- package/dist/{testing-B0UtFCq1.d.mts → testing-CqsXv2_l.d.mts} +195 -23
- package/dist/{testing-B0UtFCq1.d.ts → testing-CqsXv2_l.d.ts} +195 -23
- package/dist/testing.d.mts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +494 -35
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +1 -1
- package/package.json +3 -3
- package/dist/chunk-522SCN2E.mjs.map +0 -1
package/dist/testing.js
CHANGED
|
@@ -38,6 +38,8 @@ var SQLITE_RESULT_CODE_NAMES = {
|
|
|
38
38
|
// prefix and we normalize it back to the symbolic name so callers can branch on
|
|
39
39
|
// the same string the rest of the ecosystem uses.
|
|
40
40
|
2067: "SQLITE_CONSTRAINT_UNIQUE",
|
|
41
|
+
// 267 = SQLITE_CORRUPT_VTAB — a corrupt FTS5 shadow table (ftsCorruptionClass).
|
|
42
|
+
267: "SQLITE_CORRUPT_VTAB",
|
|
41
43
|
100: "SQLITE_ROW",
|
|
42
44
|
101: "SQLITE_DONE"
|
|
43
45
|
};
|
|
@@ -157,6 +159,19 @@ var WikiSupersedeError = class _WikiSupersedeError extends Error {
|
|
|
157
159
|
Object.setPrototypeOf(this, _WikiSupersedeError.prototype);
|
|
158
160
|
}
|
|
159
161
|
};
|
|
162
|
+
var WikiSearchUnavailableError = class extends Error {
|
|
163
|
+
constructor(message = "Keyword search is temporarily unavailable", cause) {
|
|
164
|
+
super(message, cause !== void 0 ? { cause } : void 0);
|
|
165
|
+
this.name = "WikiSearchUnavailableError";
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
function isWikiSearchUnavailableError(err) {
|
|
169
|
+
try {
|
|
170
|
+
return err instanceof WikiSearchUnavailableError;
|
|
171
|
+
} catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
160
175
|
|
|
161
176
|
// src/utils/pure.ts
|
|
162
177
|
var MAX_REPAIR_CANDIDATES = 5;
|
|
@@ -655,7 +670,7 @@ function taskRejectionReason(raw) {
|
|
|
655
670
|
}
|
|
656
671
|
function normalizeSourceRef(value) {
|
|
657
672
|
if (typeof value !== "string") return null;
|
|
658
|
-
const cleaned = value.replace(/[^A-Za-z0-9._\- ]/g, "").trim().slice(0, 255);
|
|
673
|
+
const cleaned = value.replace(/[^A-Za-z0-9._\- ]/g, "").trim().slice(0, 255).trimEnd();
|
|
659
674
|
return cleaned.length > 0 ? cleaned : null;
|
|
660
675
|
}
|
|
661
676
|
function normalizeSourceHash(value) {
|
|
@@ -708,7 +723,7 @@ var MESSAGE = {
|
|
|
708
723
|
embedding_failed: "A fact could not be embedded.",
|
|
709
724
|
hook_failed: "A host hook threw or rejected.",
|
|
710
725
|
background_job_failed: "A background maintenance job failed.",
|
|
711
|
-
heal_skipped: "Heal skipped a candidate fact.",
|
|
726
|
+
heal_skipped: "Heal skipped a candidate fact, or the whole LLM pass when keyword search is unavailable.",
|
|
712
727
|
grounding_missing: "A fact carried no qualifying evidence and was stored as a draft.",
|
|
713
728
|
grounding_failed: "A fact carried evidence not found in its source and was stored as a draft.",
|
|
714
729
|
classification_low_confidence: "A classifier answer was below the confidence threshold and was not applied.",
|
|
@@ -1394,7 +1409,7 @@ var ImportExportService = class {
|
|
|
1394
1409
|
if (merge) {
|
|
1395
1410
|
await this.searchService.syncEntries(entityId, [...upsertedFactIds]);
|
|
1396
1411
|
} else {
|
|
1397
|
-
await this.searchService.sync(entityId);
|
|
1412
|
+
await this.searchService.sync(entityId, { ids: [.../* @__PURE__ */ new Set([...upsertedFactIds, ...softDeletedFactIds])] });
|
|
1398
1413
|
}
|
|
1399
1414
|
for (const fact of bundle.facts) {
|
|
1400
1415
|
if (!fact.deleted_at && upsertedFactIds.has(fact.id) && !factsWithPreservedBlob.has(fact.id)) {
|
|
@@ -2327,7 +2342,8 @@ var IngestionService = class {
|
|
|
2327
2342
|
*
|
|
2328
2343
|
* @returns Counts: nodesWritten (validated nodes persisted), edgesWritten
|
|
2329
2344
|
* (manifest-valid edges persisted), superseded (prior facts soft-deleted
|
|
2330
|
-
* plus prior source-ref edges hard-deleted)
|
|
2345
|
+
* plus prior source-ref edges hard-deleted), and supersededFactIds (the
|
|
2346
|
+
* ids of those soft-deleted facts, for the caller's post-commit index sync).
|
|
2331
2347
|
*
|
|
2332
2348
|
* Node-ownership pre-flight (spec §3, REQ-GRAPH-01): the first statement
|
|
2333
2349
|
* is `assertGraphNodeOwnership(entityId, params.nodes, tx)`, which runs
|
|
@@ -2340,6 +2356,9 @@ var IngestionService = class {
|
|
|
2340
2356
|
* ordering and counts so success counts match the pre-change contract.
|
|
2341
2357
|
*/
|
|
2342
2358
|
async upsertGraphCore(entityId, params, tx, opts) {
|
|
2359
|
+
const sourceRef = normalizeSourceRef(params.sourceRef);
|
|
2360
|
+
if (!sourceRef) throw new Error("Invalid sourceRef");
|
|
2361
|
+
params = { ...params, sourceRef };
|
|
2343
2362
|
await this.assertGraphNodeOwnership(entityId, params.nodes, tx);
|
|
2344
2363
|
const now = Date.now();
|
|
2345
2364
|
const ontologyState = await this.ontologyService?.getEffectiveState(entityId, tx) ?? { mode: "off", manifest: { node_types: [], edge_types: [] } };
|
|
@@ -2452,7 +2471,8 @@ var IngestionService = class {
|
|
|
2452
2471
|
return {
|
|
2453
2472
|
nodesWritten: wikiFacts.length,
|
|
2454
2473
|
edgesWritten: edgesInsertedCount,
|
|
2455
|
-
superseded: deletedFactIds.length + deletedEdgeCount
|
|
2474
|
+
superseded: deletedFactIds.length + deletedEdgeCount,
|
|
2475
|
+
supersededFactIds: deletedFactIds
|
|
2456
2476
|
};
|
|
2457
2477
|
}
|
|
2458
2478
|
/**
|
|
@@ -2850,6 +2870,363 @@ async function runBatched(args) {
|
|
|
2850
2870
|
return { results, skipped, batches, unattempted, ...budgetStop ? { budgetStop } : {} };
|
|
2851
2871
|
}
|
|
2852
2872
|
|
|
2873
|
+
// src/db/schema.ts
|
|
2874
|
+
async function createTemporalIndexesIfColumnsExist(db, prefix) {
|
|
2875
|
+
const cols = await db.getAllAsync(`PRAGMA table_info(${prefix}entries)`);
|
|
2876
|
+
const names = new Set(cols.map((c) => c.name));
|
|
2877
|
+
if (names.has("superseded_by")) {
|
|
2878
|
+
await db.execAsync(
|
|
2879
|
+
`CREATE INDEX IF NOT EXISTS ${prefix}entries_superseded_idx ON ${prefix}entries(entity_id, superseded_by) WHERE superseded_by IS NOT NULL;`
|
|
2880
|
+
);
|
|
2881
|
+
}
|
|
2882
|
+
if (names.has("valid_from") && names.has("valid_to")) {
|
|
2883
|
+
await db.execAsync(
|
|
2884
|
+
`CREATE INDEX IF NOT EXISTS ${prefix}entries_temporal_idx ON ${prefix}entries(entity_id) WHERE valid_from IS NOT NULL OR valid_to IS NOT NULL;`
|
|
2885
|
+
);
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
// src/services/search/fts5Sql.ts
|
|
2890
|
+
var FTS5_STATE_KEY = "fts5_index_state";
|
|
2891
|
+
var FTS5_REBUILD_WM_KEY = "fts5_rebuild_wm";
|
|
2892
|
+
var FTS5_REBUILD_SNAPSHOT_KEY = "fts5_rebuild_snapshot";
|
|
2893
|
+
var FTS5_SWEEP_CURSOR_KEY = "fts5_orphan_sweep_cursor";
|
|
2894
|
+
function fts5PendingIndexesDdl(p) {
|
|
2895
|
+
return `
|
|
2896
|
+
CREATE INDEX IF NOT EXISTS ${p}fts_pending_id ON ${p}fts_pending(id);
|
|
2897
|
+
CREATE INDEX IF NOT EXISTS ${p}fts_pending_entity ON ${p}fts_pending(entity_id);`;
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2900
|
+
// src/db/migrations.ts
|
|
2901
|
+
var MIGRATIONS = [
|
|
2902
|
+
{
|
|
2903
|
+
version: 1,
|
|
2904
|
+
description: "Rebuild FTS5 with porter unicode61 tokenizer (superseded by v2)",
|
|
2905
|
+
run: async (_db, _prefix) => {
|
|
2906
|
+
}
|
|
2907
|
+
},
|
|
2908
|
+
{
|
|
2909
|
+
version: 2,
|
|
2910
|
+
description: "Remove FTS5; add embedding column for semantic retrieval",
|
|
2911
|
+
run: async (db, prefix) => {
|
|
2912
|
+
await db.withTransactionAsync(async (tx) => {
|
|
2913
|
+
await tx.execAsync(`
|
|
2914
|
+
DROP TRIGGER IF EXISTS ${prefix}entries_ai;
|
|
2915
|
+
DROP TRIGGER IF EXISTS ${prefix}entries_ad;
|
|
2916
|
+
DROP TRIGGER IF EXISTS ${prefix}entries_au;
|
|
2917
|
+
DROP TABLE IF EXISTS ${prefix}entries_fts;
|
|
2918
|
+
`);
|
|
2919
|
+
});
|
|
2920
|
+
const cols = await db.getAllAsync(
|
|
2921
|
+
`PRAGMA table_info(${prefix}entries)`
|
|
2922
|
+
);
|
|
2923
|
+
if (!cols.some((c) => c.name === "embedding")) {
|
|
2924
|
+
await db.execAsync(`ALTER TABLE ${prefix}entries ADD COLUMN embedding TEXT`);
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
},
|
|
2928
|
+
{
|
|
2929
|
+
version: 3,
|
|
2930
|
+
description: "Add embedding_blob BLOB column for Float32Array vector storage",
|
|
2931
|
+
run: async (db, prefix) => {
|
|
2932
|
+
const cols = await db.getAllAsync(
|
|
2933
|
+
`PRAGMA table_info(${prefix}entries)`
|
|
2934
|
+
);
|
|
2935
|
+
if (!cols.some((c) => c.name === "embedding_blob")) {
|
|
2936
|
+
await db.execAsync(
|
|
2937
|
+
`ALTER TABLE ${prefix}entries ADD COLUMN embedding_blob BLOB`
|
|
2938
|
+
);
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
},
|
|
2942
|
+
{
|
|
2943
|
+
version: 4,
|
|
2944
|
+
description: "Create outbox table for change data capture",
|
|
2945
|
+
run: async (db, prefix) => {
|
|
2946
|
+
await db.execAsync(`
|
|
2947
|
+
CREATE TABLE IF NOT EXISTS ${prefix}outbox (
|
|
2948
|
+
id TEXT PRIMARY KEY,
|
|
2949
|
+
entity_id TEXT NOT NULL,
|
|
2950
|
+
table_name TEXT NOT NULL,
|
|
2951
|
+
record_id TEXT NOT NULL,
|
|
2952
|
+
operation TEXT NOT NULL,
|
|
2953
|
+
payload TEXT NOT NULL,
|
|
2954
|
+
created_at INTEGER NOT NULL
|
|
2955
|
+
);
|
|
2956
|
+
CREATE INDEX IF NOT EXISTS ${prefix}outbox_entity_id_created_at
|
|
2957
|
+
ON ${prefix}outbox (entity_id, created_at);
|
|
2958
|
+
`);
|
|
2959
|
+
}
|
|
2960
|
+
},
|
|
2961
|
+
{
|
|
2962
|
+
version: 5,
|
|
2963
|
+
description: "Add okf_type to entries/tasks for OKF type fidelity; create edges table for OKF graph import",
|
|
2964
|
+
run: async (db, prefix) => {
|
|
2965
|
+
for (const table of ["entries", "tasks"]) {
|
|
2966
|
+
const cols = await db.getAllAsync(
|
|
2967
|
+
`PRAGMA table_info(${prefix}${table})`
|
|
2968
|
+
);
|
|
2969
|
+
if (!cols.some((c) => c.name === "okf_type")) {
|
|
2970
|
+
await db.execAsync(`ALTER TABLE ${prefix}${table} ADD COLUMN okf_type TEXT`);
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
await db.execAsync(`
|
|
2974
|
+
CREATE TABLE IF NOT EXISTS ${prefix}edges (
|
|
2975
|
+
id TEXT PRIMARY KEY,
|
|
2976
|
+
entity_id TEXT NOT NULL,
|
|
2977
|
+
source_id TEXT NOT NULL,
|
|
2978
|
+
target_id TEXT NOT NULL,
|
|
2979
|
+
edge_type TEXT NOT NULL,
|
|
2980
|
+
created_at INTEGER NOT NULL,
|
|
2981
|
+
UNIQUE(entity_id, source_id, target_id, edge_type)
|
|
2982
|
+
);
|
|
2983
|
+
CREATE INDEX IF NOT EXISTS ${prefix}edges_entity_idx ON ${prefix}edges (entity_id);
|
|
2984
|
+
`);
|
|
2985
|
+
}
|
|
2986
|
+
},
|
|
2987
|
+
{
|
|
2988
|
+
version: 6,
|
|
2989
|
+
description: "Add entity_manifests table for per-entity ontology state",
|
|
2990
|
+
run: async (db, prefix) => {
|
|
2991
|
+
await db.execAsync(`
|
|
2992
|
+
CREATE TABLE IF NOT EXISTS ${prefix}entity_manifests (
|
|
2993
|
+
entity_id TEXT PRIMARY KEY,
|
|
2994
|
+
mode TEXT NOT NULL DEFAULT 'off',
|
|
2995
|
+
manifest_json TEXT NOT NULL DEFAULT '{"node_types":[],"edge_types":[]}',
|
|
2996
|
+
updated_at INTEGER NOT NULL
|
|
2997
|
+
);
|
|
2998
|
+
`);
|
|
2999
|
+
}
|
|
3000
|
+
},
|
|
3001
|
+
{
|
|
3002
|
+
version: 7,
|
|
3003
|
+
description: "Add ontology_checked_at to entries for ontology backfill recheck cooldown",
|
|
3004
|
+
run: async (db, prefix) => {
|
|
3005
|
+
const cols = await db.getAllAsync(
|
|
3006
|
+
`PRAGMA table_info(${prefix}entries)`
|
|
3007
|
+
);
|
|
3008
|
+
if (!cols.some((c) => c.name === "ontology_checked_at")) {
|
|
3009
|
+
await db.execAsync(
|
|
3010
|
+
`ALTER TABLE ${prefix}entries ADD COLUMN ontology_checked_at INTEGER`
|
|
3011
|
+
);
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
},
|
|
3015
|
+
{
|
|
3016
|
+
version: 8,
|
|
3017
|
+
description: "Add heal_checked_at to entries for heal recheck cooldown",
|
|
3018
|
+
run: async (db, prefix) => {
|
|
3019
|
+
const cols = await db.getAllAsync(
|
|
3020
|
+
`PRAGMA table_info(${prefix}entries)`
|
|
3021
|
+
);
|
|
3022
|
+
if (!cols.some((c) => c.name === "heal_checked_at")) {
|
|
3023
|
+
await db.execAsync(
|
|
3024
|
+
`ALTER TABLE ${prefix}entries ADD COLUMN heal_checked_at INTEGER`
|
|
3025
|
+
);
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
},
|
|
3029
|
+
{
|
|
3030
|
+
version: 9,
|
|
3031
|
+
description: "add_source_ref_index",
|
|
3032
|
+
run: async (db, prefix) => {
|
|
3033
|
+
const duplicates = await db.getAllAsync(
|
|
3034
|
+
`SELECT entity_id, source_hash, COUNT(DISTINCT source_ref) AS n_refs
|
|
3035
|
+
FROM ${prefix}entries
|
|
3036
|
+
WHERE deleted_at IS NULL AND source_hash IS NOT NULL
|
|
3037
|
+
GROUP BY entity_id, source_hash
|
|
3038
|
+
HAVING COUNT(DISTINCT source_ref) > 1`
|
|
3039
|
+
);
|
|
3040
|
+
if (duplicates.length > 0) {
|
|
3041
|
+
const sample = duplicates.slice(0, 5).map((d) => `(entity_id=${d.entity_id}, source_hash=${d.source_hash.slice(0, 12)}\u2026, n_refs=${d.n_refs})`).join(", ");
|
|
3042
|
+
throw new Error(
|
|
3043
|
+
`Migration v9 (add_source_ref_index) failed: existing live rows have multiple sourceRefs sharing a hash. Found ${duplicates.length} duplicate (entity_id, source_hash) groups. First ${Math.min(5, duplicates.length)}: ${sample}. Resolve each by calling forget({ sourceRef: <loser> }) for the offending sourceRef, then re-run setup.`
|
|
3044
|
+
);
|
|
3045
|
+
}
|
|
3046
|
+
await db.execAsync(
|
|
3047
|
+
`CREATE TABLE IF NOT EXISTS ${prefix}source_ref_index (
|
|
3048
|
+
id TEXT PRIMARY KEY,
|
|
3049
|
+
entity_id TEXT NOT NULL,
|
|
3050
|
+
source_hash TEXT NOT NULL,
|
|
3051
|
+
source_ref TEXT NOT NULL,
|
|
3052
|
+
created_at INTEGER NOT NULL,
|
|
3053
|
+
deleted_at INTEGER
|
|
3054
|
+
);
|
|
3055
|
+
CREATE UNIQUE INDEX IF NOT EXISTS ${prefix}idx_source_ref_hash
|
|
3056
|
+
ON ${prefix}source_ref_index (entity_id, source_hash)
|
|
3057
|
+
WHERE deleted_at IS NULL;`
|
|
3058
|
+
);
|
|
3059
|
+
await db.execAsync(
|
|
3060
|
+
`INSERT OR IGNORE INTO ${prefix}source_ref_index (id, entity_id, source_hash, source_ref, created_at, deleted_at)
|
|
3061
|
+
SELECT
|
|
3062
|
+
'sri:' || entity_id || ':' || source_hash,
|
|
3063
|
+
entity_id,
|
|
3064
|
+
source_hash,
|
|
3065
|
+
source_ref,
|
|
3066
|
+
updated_at,
|
|
3067
|
+
NULL
|
|
3068
|
+
FROM (
|
|
3069
|
+
SELECT
|
|
3070
|
+
entity_id, source_hash, source_ref, updated_at,
|
|
3071
|
+
ROW_NUMBER() OVER (
|
|
3072
|
+
PARTITION BY entity_id, source_hash
|
|
3073
|
+
ORDER BY updated_at ASC, id ASC
|
|
3074
|
+
) AS rn
|
|
3075
|
+
FROM ${prefix}entries
|
|
3076
|
+
WHERE deleted_at IS NULL
|
|
3077
|
+
AND source_hash IS NOT NULL
|
|
3078
|
+
AND source_ref IS NOT NULL
|
|
3079
|
+
) ranked
|
|
3080
|
+
WHERE rn = 1;`
|
|
3081
|
+
);
|
|
3082
|
+
}
|
|
3083
|
+
},
|
|
3084
|
+
{
|
|
3085
|
+
version: 10,
|
|
3086
|
+
description: "OKF v0.2: add lifecycle_status, stale_after, generated_by, last_verified_*, okf_sources, okf_verified, okf_usage_window to entries and tasks",
|
|
3087
|
+
run: async (db, prefix) => {
|
|
3088
|
+
for (const table of ["entries", "tasks"]) {
|
|
3089
|
+
const cols = await db.getAllAsync(
|
|
3090
|
+
`PRAGMA table_info(${prefix}${table})`
|
|
3091
|
+
);
|
|
3092
|
+
const existing = new Set(cols.map((c) => c.name));
|
|
3093
|
+
const adds = [
|
|
3094
|
+
["lifecycle_status", `ALTER TABLE ${prefix}${table} ADD COLUMN lifecycle_status TEXT NOT NULL DEFAULT 'stable'`],
|
|
3095
|
+
["stale_after", `ALTER TABLE ${prefix}${table} ADD COLUMN stale_after INTEGER`],
|
|
3096
|
+
["generated_by", `ALTER TABLE ${prefix}${table} ADD COLUMN generated_by TEXT`],
|
|
3097
|
+
["last_verified_at", `ALTER TABLE ${prefix}${table} ADD COLUMN last_verified_at INTEGER`],
|
|
3098
|
+
["last_verified_by", `ALTER TABLE ${prefix}${table} ADD COLUMN last_verified_by TEXT`],
|
|
3099
|
+
["okf_sources", `ALTER TABLE ${prefix}${table} ADD COLUMN okf_sources TEXT`],
|
|
3100
|
+
["okf_verified", `ALTER TABLE ${prefix}${table} ADD COLUMN okf_verified TEXT`],
|
|
3101
|
+
["okf_usage_window", `ALTER TABLE ${prefix}${table} ADD COLUMN okf_usage_window TEXT`]
|
|
3102
|
+
];
|
|
3103
|
+
for (const [name, sql] of adds) {
|
|
3104
|
+
if (!existing.has(name)) await db.execAsync(sql);
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
await db.execAsync(`
|
|
3108
|
+
CREATE INDEX IF NOT EXISTS ${prefix}entries_lifecycle_status_idx ON ${prefix}entries(lifecycle_status);
|
|
3109
|
+
CREATE INDEX IF NOT EXISTS ${prefix}entries_stale_after_idx ON ${prefix}entries(stale_after);
|
|
3110
|
+
CREATE INDEX IF NOT EXISTS ${prefix}entries_last_verified_at_idx ON ${prefix}entries(last_verified_at);
|
|
3111
|
+
CREATE INDEX IF NOT EXISTS ${prefix}tasks_lifecycle_status_idx ON ${prefix}tasks(lifecycle_status);
|
|
3112
|
+
CREATE INDEX IF NOT EXISTS ${prefix}tasks_stale_after_idx ON ${prefix}tasks(stale_after);
|
|
3113
|
+
CREATE INDEX IF NOT EXISTS ${prefix}tasks_last_verified_at_idx ON ${prefix}tasks(last_verified_at);
|
|
3114
|
+
`);
|
|
3115
|
+
}
|
|
3116
|
+
},
|
|
3117
|
+
{
|
|
3118
|
+
version: 11,
|
|
3119
|
+
description: "Add embedding_failed_at, embedding_failure_kind, embedding_attempts to entries for embed retry state",
|
|
3120
|
+
run: async (db, prefix) => {
|
|
3121
|
+
const cols = await db.getAllAsync(
|
|
3122
|
+
`PRAGMA table_info(${prefix}entries)`
|
|
3123
|
+
);
|
|
3124
|
+
const existing = new Set(cols.map((c) => c.name));
|
|
3125
|
+
const adds = [
|
|
3126
|
+
["embedding_failed_at", `ALTER TABLE ${prefix}entries ADD COLUMN embedding_failed_at INTEGER`],
|
|
3127
|
+
["embedding_failure_kind", `ALTER TABLE ${prefix}entries ADD COLUMN embedding_failure_kind TEXT`],
|
|
3128
|
+
["embedding_attempts", `ALTER TABLE ${prefix}entries ADD COLUMN embedding_attempts INTEGER NOT NULL DEFAULT 0`]
|
|
3129
|
+
];
|
|
3130
|
+
for (const [name, sql] of adds) {
|
|
3131
|
+
if (!existing.has(name)) await db.execAsync(sql);
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
},
|
|
3135
|
+
{
|
|
3136
|
+
version: 12,
|
|
3137
|
+
description: "Replace edges_entity_idx with composite edges(entity_id, id) for keyset paging",
|
|
3138
|
+
run: async (db, prefix) => {
|
|
3139
|
+
await db.execAsync(`
|
|
3140
|
+
CREATE INDEX IF NOT EXISTS ${prefix}edges_entity_id_idx ON ${prefix}edges(entity_id, id);
|
|
3141
|
+
DROP INDEX IF EXISTS ${prefix}edges_entity_idx;
|
|
3142
|
+
`);
|
|
3143
|
+
}
|
|
3144
|
+
},
|
|
3145
|
+
{
|
|
3146
|
+
version: 13,
|
|
3147
|
+
description: "Temporal facts: valid_from/valid_to/superseded_by/superseded_at on entries, occurred_at on events, librarian watermark on checkpoints",
|
|
3148
|
+
run: async (db, prefix) => {
|
|
3149
|
+
const adds = [
|
|
3150
|
+
["entries", "valid_from", "INTEGER"],
|
|
3151
|
+
["entries", "valid_to", "INTEGER"],
|
|
3152
|
+
["entries", "superseded_by", "TEXT"],
|
|
3153
|
+
["entries", "superseded_at", "INTEGER"],
|
|
3154
|
+
["events", "occurred_at", "INTEGER"],
|
|
3155
|
+
["checkpoints", "librarian_watermark_at", "INTEGER"],
|
|
3156
|
+
["checkpoints", "librarian_watermark_id", "TEXT"]
|
|
3157
|
+
];
|
|
3158
|
+
const existingByTable = /* @__PURE__ */ new Map();
|
|
3159
|
+
for (const [table, column, type] of adds) {
|
|
3160
|
+
let existing = existingByTable.get(table);
|
|
3161
|
+
if (!existing) {
|
|
3162
|
+
const cols = await db.getAllAsync(`PRAGMA table_info(${prefix}${table})`);
|
|
3163
|
+
existing = new Set(cols.map((c) => c.name));
|
|
3164
|
+
existingByTable.set(table, existing);
|
|
3165
|
+
}
|
|
3166
|
+
if (!existing.has(column)) {
|
|
3167
|
+
await db.execAsync(`ALTER TABLE ${prefix}${table} ADD COLUMN ${column} ${type}`);
|
|
3168
|
+
existing.add(column);
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
await createTemporalIndexesIfColumnsExist(db, prefix);
|
|
3172
|
+
}
|
|
3173
|
+
},
|
|
3174
|
+
{
|
|
3175
|
+
version: 14,
|
|
3176
|
+
description: "Partial index for the every-open legacy source_type probe",
|
|
3177
|
+
run: async (db, prefix) => {
|
|
3178
|
+
await createLegacySourceTypeIndex(db, prefix);
|
|
3179
|
+
}
|
|
3180
|
+
},
|
|
3181
|
+
{
|
|
3182
|
+
version: 15,
|
|
3183
|
+
description: "fts_pending(id) and fts_pending(entity_id) indexes for the ids-first and entity-targeted drains",
|
|
3184
|
+
run: async (db, prefix) => {
|
|
3185
|
+
const row = await db.getFirstAsync(
|
|
3186
|
+
`SELECT count(*) AS n FROM sqlite_master WHERE type = 'table' AND name = ?`,
|
|
3187
|
+
[`${prefix}fts_pending`]
|
|
3188
|
+
);
|
|
3189
|
+
if (row?.n) await db.execAsync(fts5PendingIndexesDdl(prefix));
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
];
|
|
3193
|
+
async function createLegacySourceTypeIndex(db, prefix) {
|
|
3194
|
+
await db.execAsync(
|
|
3195
|
+
`CREATE INDEX IF NOT EXISTS ${prefix}entries_legacy_source_type_idx ON ${prefix}entries(source_type) WHERE source_type IN ('user_document', 'agent_inferred')`
|
|
3196
|
+
);
|
|
3197
|
+
}
|
|
3198
|
+
for (let i = 1; i < MIGRATIONS.length; i++) {
|
|
3199
|
+
if (MIGRATIONS[i].version <= MIGRATIONS[i - 1].version) {
|
|
3200
|
+
throw new Error(
|
|
3201
|
+
`migrations.ts: MIGRATIONS must be in strictly ascending version order. Found version ${MIGRATIONS[i].version} after ${MIGRATIONS[i - 1].version} at index ${i}.`
|
|
3202
|
+
);
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
MIGRATIONS.length > 0 ? MIGRATIONS[MIGRATIONS.length - 1].version : 0;
|
|
3206
|
+
|
|
3207
|
+
// src/db/sourceRefNormalize.ts
|
|
3208
|
+
var ENTRIES_WM_KEY = "normalize_v14_entries_wm";
|
|
3209
|
+
var SRI_WM_KEY = "normalize_v14_sri_wm";
|
|
3210
|
+
async function resetNormalizeWatermarks(meta) {
|
|
3211
|
+
await meta.deleteMeta(ENTRIES_WM_KEY);
|
|
3212
|
+
await meta.deleteMeta(SRI_WM_KEY);
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
// src/services/search/fts5Progress.ts
|
|
3216
|
+
async function writeRebuildStart(tx, prefix, meta) {
|
|
3217
|
+
const e = await tx.getFirstAsync(`SELECT coalesce(max(rowid), 0) AS hi FROM ${prefix}entries`);
|
|
3218
|
+
const m = await tx.getFirstAsync(`SELECT coalesce(max(fts_rowid), 0) AS hi FROM ${prefix}fts_map`);
|
|
3219
|
+
await meta.setMeta(FTS5_REBUILD_WM_KEY, JSON.stringify({ rowid: 0, id: null }), tx);
|
|
3220
|
+
await meta.setMeta(FTS5_REBUILD_SNAPSHOT_KEY, JSON.stringify({ entries: e?.hi ?? 0, map: m?.hi ?? 0 }), tx);
|
|
3221
|
+
await meta.setMeta(FTS5_SWEEP_CURSOR_KEY, "0", tx);
|
|
3222
|
+
}
|
|
3223
|
+
async function restartRebuildAfterVacuum(db, prefix, meta) {
|
|
3224
|
+
await db.withTransactionAsync(async (tx) => {
|
|
3225
|
+
if (await meta.getMeta(FTS5_STATE_KEY, tx) !== "rebuilding") return;
|
|
3226
|
+
await writeRebuildStart(tx, prefix, meta);
|
|
3227
|
+
});
|
|
3228
|
+
}
|
|
3229
|
+
|
|
2853
3230
|
// src/services/librarian/constants.ts
|
|
2854
3231
|
var FUZZY_THRESHOLD = 0.5;
|
|
2855
3232
|
var MIN_TOKENS_TO_QUALIFY = 3;
|
|
@@ -3147,6 +3524,22 @@ function classifyCandidate(candidate, neighbours, cfg, mode) {
|
|
|
3147
3524
|
if (score < cfg.novelThreshold) return { kind: "add" };
|
|
3148
3525
|
return { kind: "ambiguous" };
|
|
3149
3526
|
}
|
|
3527
|
+
async function loadStoredVectors(deps, entityId, nonLive) {
|
|
3528
|
+
const stored = [];
|
|
3529
|
+
for (const row of await deps.entryRepo.findWithEmbeddingsByEntityIds([entityId])) {
|
|
3530
|
+
if (nonLive.has(row.id)) continue;
|
|
3531
|
+
const vec = parseEmbedding(row.embedding_blob, row.embedding);
|
|
3532
|
+
if (vec) stored.push({ id: row.id, vec });
|
|
3533
|
+
}
|
|
3534
|
+
return stored;
|
|
3535
|
+
}
|
|
3536
|
+
async function gateNeedsKeyword(deps, entityId) {
|
|
3537
|
+
if (!await deps.entryRepo.hasLiveEntry(entityId)) return false;
|
|
3538
|
+
if (typeof deps.options.llmProvider.embed !== "function") return true;
|
|
3539
|
+
const nonLive = await deps.entryRepo.findNonLiveIdsByEntityIds([entityId], "current", Date.now());
|
|
3540
|
+
const stored = await loadStoredVectors(deps, entityId, nonLive);
|
|
3541
|
+
return stored.length === 0 || stored.some((s) => s.vec.length !== stored[0].vec.length);
|
|
3542
|
+
}
|
|
3150
3543
|
async function gateCandidates(deps, ctx, candidates, cfg) {
|
|
3151
3544
|
const { entityId, trigger } = ctx;
|
|
3152
3545
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3162,14 +3555,8 @@ async function gateCandidates(deps, ctx, candidates, cfg) {
|
|
|
3162
3555
|
}
|
|
3163
3556
|
const nonLive = await deps.entryRepo.findNonLiveIdsByEntityIds([entityId], "current", Date.now());
|
|
3164
3557
|
const canEmbed = typeof deps.options.llmProvider.embed === "function";
|
|
3165
|
-
const stored = [];
|
|
3166
|
-
|
|
3167
|
-
for (const row of await deps.entryRepo.findWithEmbeddingsByEntityIds([entityId])) {
|
|
3168
|
-
if (nonLive.has(row.id)) continue;
|
|
3169
|
-
const vec = parseEmbedding(row.embedding_blob, row.embedding);
|
|
3170
|
-
if (vec) stored.push({ id: row.id, vec });
|
|
3171
|
-
}
|
|
3172
|
-
}
|
|
3558
|
+
const stored = canEmbed && unique.length > 0 ? await loadStoredVectors(deps, entityId, nonLive) : [];
|
|
3559
|
+
const hasFacts = unique.length > 0 && await deps.entryRepo.hasLiveEntry(entityId);
|
|
3173
3560
|
const gated = [];
|
|
3174
3561
|
for (const c of unique) {
|
|
3175
3562
|
let ranked = null;
|
|
@@ -3190,7 +3577,7 @@ async function gateCandidates(deps, ctx, candidates, cfg) {
|
|
|
3190
3577
|
}
|
|
3191
3578
|
}
|
|
3192
3579
|
if (!ranked) {
|
|
3193
|
-
ranked = (await deps.searchService.searchKeyword(`${c.fact.title} ${c.fact.body}`, [entityId], cfg.k + nonLive.size)).filter((h) => !nonLive.has(h.id)).slice(0, cfg.k).map((h) => ({ id: h.id, score: null }));
|
|
3580
|
+
ranked = hasFacts ? (await deps.searchService.searchKeyword(`${c.fact.title} ${c.fact.body}`, [entityId], cfg.k + nonLive.size)).filter((h) => !nonLive.has(h.id)).slice(0, cfg.k).map((h) => ({ id: h.id, score: null })) : [];
|
|
3194
3581
|
}
|
|
3195
3582
|
const facts = ranked.length > 0 ? await deps.entryRepo.findByIds(ranked.map((r) => r.id), [entityId]) : [];
|
|
3196
3583
|
const byId = new Map(facts.map((f) => [f.id, f]));
|
|
@@ -3541,6 +3928,9 @@ var runOpsLibrarianPass = async (deps, ctx) => {
|
|
|
3541
3928
|
const sel = await selectBatch(deps, entityId, wm, ctx.meter);
|
|
3542
3929
|
if ("budgetStop" in sel) return { processedThrough: null, factsWritten: 0, budgetStop: sel.budgetStop };
|
|
3543
3930
|
if (sel.batch.length === 0) return { processedThrough: null, factsWritten: 0 };
|
|
3931
|
+
if (!deps.searchService.isKeywordAvailable() && await gateNeedsKeyword(deps, entityId)) {
|
|
3932
|
+
throw new WikiSearchUnavailableError("librarian: keyword search unavailable and this pass may need it; retry the pass");
|
|
3933
|
+
}
|
|
3544
3934
|
let ex;
|
|
3545
3935
|
try {
|
|
3546
3936
|
ex = await runExtract(deps, ctx, sel.batch);
|
|
@@ -3770,7 +4160,9 @@ var MaintenanceService = class {
|
|
|
3770
4160
|
}
|
|
3771
4161
|
}
|
|
3772
4162
|
if (vacuum) {
|
|
4163
|
+
await resetNormalizeWatermarks(this.metadataRepo);
|
|
3773
4164
|
await this.metadataRepo.vacuum();
|
|
4165
|
+
await restartRebuildAfterVacuum(this.db, this.prefix, this.metadataRepo);
|
|
3774
4166
|
}
|
|
3775
4167
|
await this.searchService.syncEntries(entityId, syncedIds);
|
|
3776
4168
|
return { entries: deletedEntries, tasks: deletedTasks, events: deletedEvents };
|
|
@@ -4069,7 +4461,8 @@ var MaintenanceService = class {
|
|
|
4069
4461
|
*
|
|
4070
4462
|
* Bounded: at most `batchSize` candidates per pass (#67). Loop on
|
|
4071
4463
|
* `result.remaining > 0` for convergence — see {@link HealResult.remaining}
|
|
4072
|
-
* for what convergence means here
|
|
4464
|
+
* for what convergence means here, and for the no-progress flags
|
|
4465
|
+
* (`searchUnavailable`, `budgetStop`) a loop must also stop on.
|
|
4073
4466
|
*/
|
|
4074
4467
|
async doRunHeal(entityId, options) {
|
|
4075
4468
|
const promptOverride = options?.promptOverride;
|
|
@@ -4115,7 +4508,11 @@ var MaintenanceService = class {
|
|
|
4115
4508
|
}
|
|
4116
4509
|
}
|
|
4117
4510
|
const healCandidates = await this.entryRepo.findHealCandidatesByEntityId(entityId, batchSize, recheckCutoff);
|
|
4118
|
-
|
|
4511
|
+
const keywordUnavailable = healCandidates.length > 0 && !this.searchService.isKeywordAvailable();
|
|
4512
|
+
if (healCandidates.length === 0 || keywordUnavailable) {
|
|
4513
|
+
if (keywordUnavailable) {
|
|
4514
|
+
emitDiagnostic(this.options, { ...diagBase, code: "heal_skipped", detail: { reason: "keyword_search_unavailable", count: healCandidates.length } });
|
|
4515
|
+
}
|
|
4119
4516
|
await this.searchService.syncEntries(entityId, Array.from(/* @__PURE__ */ new Set([...orphanedIds, ...staleDowngradedIds])));
|
|
4120
4517
|
this.searchService.evictCache(entityId);
|
|
4121
4518
|
const counts2 = await this.entryRepo.countHealCandidatesByEntityId(entityId, recheckCutoff);
|
|
@@ -4127,7 +4524,10 @@ var MaintenanceService = class {
|
|
|
4127
4524
|
skipped: [],
|
|
4128
4525
|
degraded: [],
|
|
4129
4526
|
remaining: counts2.eligible,
|
|
4130
|
-
deferred: counts2.deferred
|
|
4527
|
+
deferred: counts2.deferred,
|
|
4528
|
+
// remaining stays > 0 so maybeRunHeal holds the checkpoint back and
|
|
4529
|
+
// retries after the outage; the flag lets a host loop stop instead.
|
|
4530
|
+
...keywordUnavailable ? { searchUnavailable: true } : {}
|
|
4131
4531
|
};
|
|
4132
4532
|
}
|
|
4133
4533
|
const allTasks = await this.taskRepo.findAllPending([entityId], HEAL_MAX_TASKS);
|
|
@@ -4931,7 +5331,12 @@ var RetrievalService = class {
|
|
|
4931
5331
|
candidateRows = this._withoutIds(await this.entryRepo.findWithEmbeddingsByEntityIds(scoredEntityIds), excludedIds);
|
|
4932
5332
|
}
|
|
4933
5333
|
if (weight !== void 0 && weight < 1) {
|
|
4934
|
-
|
|
5334
|
+
try {
|
|
5335
|
+
miniSearchScores = await this.searchService.getKeywordScores(trimmedQuery, scoredEntityIds);
|
|
5336
|
+
} catch (err) {
|
|
5337
|
+
if (!isWikiSearchUnavailableError(err)) throw err;
|
|
5338
|
+
miniSearchScores = /* @__PURE__ */ new Map();
|
|
5339
|
+
}
|
|
4935
5340
|
}
|
|
4936
5341
|
}
|
|
4937
5342
|
if (candidateRows === null) {
|
|
@@ -5433,6 +5838,28 @@ function toIndexDoc(row) {
|
|
|
5433
5838
|
};
|
|
5434
5839
|
}
|
|
5435
5840
|
|
|
5841
|
+
// src/services/search/UnavailableIndexStrategy.ts
|
|
5842
|
+
var UnavailableIndexStrategy = class {
|
|
5843
|
+
constructor(cause) {
|
|
5844
|
+
this.cause = cause;
|
|
5845
|
+
}
|
|
5846
|
+
async search(_query, options) {
|
|
5847
|
+
if (options.entityIds.length === 0) return [];
|
|
5848
|
+
throw new WikiSearchUnavailableError(void 0, this.cause);
|
|
5849
|
+
}
|
|
5850
|
+
async replace(_entityId, _ids, _documents) {
|
|
5851
|
+
}
|
|
5852
|
+
async replaceEntity(_entityId, _documents) {
|
|
5853
|
+
}
|
|
5854
|
+
async replaceAll(_documents) {
|
|
5855
|
+
}
|
|
5856
|
+
hasIndexedEntity(_entityId) {
|
|
5857
|
+
return false;
|
|
5858
|
+
}
|
|
5859
|
+
async drain(_opts) {
|
|
5860
|
+
}
|
|
5861
|
+
};
|
|
5862
|
+
|
|
5436
5863
|
// src/services/SearchService.ts
|
|
5437
5864
|
var _SearchService = class _SearchService {
|
|
5438
5865
|
constructor(entryRepo, indexStrategy) {
|
|
@@ -5471,24 +5898,35 @@ var _SearchService = class _SearchService {
|
|
|
5471
5898
|
setIndexStrategy(strategy) {
|
|
5472
5899
|
this.indexStrategy = strategy;
|
|
5473
5900
|
}
|
|
5901
|
+
/** False while the installed strategy is the degraded UnavailableIndexStrategy (spec #280 Change 4). */
|
|
5902
|
+
isKeywordAvailable() {
|
|
5903
|
+
return !(this.indexStrategy instanceof UnavailableIndexStrategy);
|
|
5904
|
+
}
|
|
5905
|
+
/** True when the installed strategy drains a durable ledger (FTS5, or the degraded stand-in). */
|
|
5906
|
+
hasLedger() {
|
|
5907
|
+
return this.indexStrategy.drain !== void 0;
|
|
5908
|
+
}
|
|
5474
5909
|
/**
|
|
5475
5910
|
* One serialized drain turn for strategies with a durable ledger. Like
|
|
5476
5911
|
* sync(), it never rejects: the keyword index is rebuildable, and a failed
|
|
5477
|
-
* drain leaves the ledger rows in place for the next turn.
|
|
5912
|
+
* drain leaves the ledger rows in place for the next turn. Resolves false
|
|
5913
|
+
* when the drain failed.
|
|
5478
5914
|
*/
|
|
5479
|
-
drainTurn(entityId) {
|
|
5915
|
+
drainTurn(entityId, opts) {
|
|
5480
5916
|
const work = this.syncChain.then(async () => {
|
|
5481
5917
|
try {
|
|
5482
5918
|
try {
|
|
5483
|
-
await this.indexStrategy.drain();
|
|
5919
|
+
await this.indexStrategy.drain(opts);
|
|
5484
5920
|
} finally {
|
|
5485
5921
|
this.evictCache(entityId);
|
|
5486
5922
|
}
|
|
5923
|
+
return true;
|
|
5487
5924
|
} catch (err) {
|
|
5488
5925
|
console.warn(`[WikiMemory] search index drain failed for ${entityId ?? "*"}:`, err);
|
|
5926
|
+
return false;
|
|
5489
5927
|
}
|
|
5490
5928
|
});
|
|
5491
|
-
this.syncChain = work;
|
|
5929
|
+
this.syncChain = work.then(() => void 0);
|
|
5492
5930
|
return work;
|
|
5493
5931
|
}
|
|
5494
5932
|
/**
|
|
@@ -5498,9 +5936,18 @@ var _SearchService = class _SearchService {
|
|
|
5498
5936
|
* Rebuilds are serialized per instance and never reject: the keyword index
|
|
5499
5937
|
* is a rebuildable cache over SQLite, so degraded keyword search is the
|
|
5500
5938
|
* correct failure mode and killing the host process is not.
|
|
5939
|
+
*
|
|
5940
|
+
* With a ledger strategy, `sync()` runs one full quantum bounded by
|
|
5941
|
+
* `opts.deadline`; `sync(entityId)` is a write turn (ledger only).
|
|
5942
|
+
* `opts.ids` are the caller's touched ids: a ledger strategy drains them
|
|
5943
|
+
* first, to completion; MiniSearch ignores them and rebuilds the entity in
|
|
5944
|
+
* one scan.
|
|
5501
5945
|
*/
|
|
5502
|
-
async sync(entityId) {
|
|
5503
|
-
if (this.indexStrategy.drain)
|
|
5946
|
+
async sync(entityId, opts) {
|
|
5947
|
+
if (this.indexStrategy.drain) {
|
|
5948
|
+
await this.drainTurn(entityId, entityId === void 0 ? { mode: "full", deadline: opts?.deadline, ids: opts?.ids } : { mode: "ledger", entityId, ids: opts?.ids });
|
|
5949
|
+
return;
|
|
5950
|
+
}
|
|
5504
5951
|
const work = this.syncChain.then(async () => {
|
|
5505
5952
|
try {
|
|
5506
5953
|
try {
|
|
@@ -5541,13 +5988,17 @@ var _SearchService = class _SearchService {
|
|
|
5541
5988
|
* — including on an empty `ids` list, which is a no-op only for an entity
|
|
5542
5989
|
* the index already tracks, and even then one that waits for rebuilds
|
|
5543
5990
|
* already queued on the chain.
|
|
5991
|
+
*
|
|
5992
|
+
* Resolves false only when a ledger drain failed: the caller's ids were not
|
|
5993
|
+
* applied (their ledger rows remain), so a caller holding them retries.
|
|
5544
5994
|
*/
|
|
5545
5995
|
async syncEntries(entityId, ids) {
|
|
5546
|
-
if (this.indexStrategy.drain) return this.drainTurn(entityId);
|
|
5996
|
+
if (this.indexStrategy.drain) return this.drainTurn(entityId, { mode: "ledger", entityId, ids: [...new Set(ids)] });
|
|
5547
5997
|
const uniqueIds = [...new Set(ids)];
|
|
5548
5998
|
const indexed = this.indexStrategy.hasIndexedEntity?.(entityId);
|
|
5549
5999
|
if (uniqueIds.length === 0 && !this.staleEntities.has(entityId) && indexed === true) {
|
|
5550
|
-
|
|
6000
|
+
await this.syncChain;
|
|
6001
|
+
return true;
|
|
5551
6002
|
}
|
|
5552
6003
|
const work = this.syncChain.then(async () => {
|
|
5553
6004
|
try {
|
|
@@ -5576,7 +6027,8 @@ var _SearchService = class _SearchService {
|
|
|
5576
6027
|
}
|
|
5577
6028
|
});
|
|
5578
6029
|
this.syncChain = work;
|
|
5579
|
-
|
|
6030
|
+
await work;
|
|
6031
|
+
return true;
|
|
5580
6032
|
}
|
|
5581
6033
|
/**
|
|
5582
6034
|
* Forces the entity's next syncEntries to rebuild it in full. For writes core
|
|
@@ -5592,11 +6044,13 @@ var _SearchService = class _SearchService {
|
|
|
5592
6044
|
* so only those entities are rebuilt. Each rebuild still takes its own turn
|
|
5593
6045
|
* on the sync chain, and like syncEntries this never rejects. With nothing
|
|
5594
6046
|
* stale it resolves at once, without waiting on work already on the chain.
|
|
6047
|
+
* Resolves false only when a ledger drain failed (see syncEntries).
|
|
5595
6048
|
*/
|
|
5596
|
-
async syncStale() {
|
|
5597
|
-
if (this.indexStrategy.drain) return this.drainTurn();
|
|
6049
|
+
async syncStale(opts) {
|
|
6050
|
+
if (this.indexStrategy.drain) return this.drainTurn(void 0, { mode: "full", ids: opts?.ids });
|
|
5598
6051
|
const ids = [...this.staleEntities];
|
|
5599
6052
|
await Promise.all(ids.map((id) => this.syncEntries(id, [])));
|
|
6053
|
+
return true;
|
|
5600
6054
|
}
|
|
5601
6055
|
/**
|
|
5602
6056
|
* Clears the parsed vector cache. Useful for mid-loop flush guarantees
|
|
@@ -6185,6 +6639,7 @@ var WriteService = class {
|
|
|
6185
6639
|
this.jobManager = jobManager;
|
|
6186
6640
|
this.maintenanceService = maintenanceService;
|
|
6187
6641
|
this.inflight = /* @__PURE__ */ new Set();
|
|
6642
|
+
this.warnedSearchUnavailable = false;
|
|
6188
6643
|
}
|
|
6189
6644
|
track(p) {
|
|
6190
6645
|
this.inflight.add(p);
|
|
@@ -6281,8 +6736,7 @@ var WriteService = class {
|
|
|
6281
6736
|
this.jobManager.acquireLock("librarian", entityId);
|
|
6282
6737
|
const stage = { job: "librarian" };
|
|
6283
6738
|
const job = this.runLibrarianThenMaybeHeal(entityId, librarianCount, prevMemoryCheckpoint, stage).catch((err) => {
|
|
6284
|
-
|
|
6285
|
-
this.reportBackgroundFailure(entityId, stage.job);
|
|
6739
|
+
this.reportBackgroundFailure(entityId, stage.job, err);
|
|
6286
6740
|
}).finally(() => {
|
|
6287
6741
|
this.jobManager.releaseLock("librarian", entityId);
|
|
6288
6742
|
});
|
|
@@ -6293,8 +6747,7 @@ var WriteService = class {
|
|
|
6293
6747
|
}
|
|
6294
6748
|
} else if (!this.jobManager.isBlocked("librarian", entityId)) {
|
|
6295
6749
|
const healJob = this.maybeRunHeal(entityId, eventCount).catch((err) => {
|
|
6296
|
-
|
|
6297
|
-
this.reportBackgroundFailure(entityId, "heal");
|
|
6750
|
+
this.reportBackgroundFailure(entityId, "heal", err);
|
|
6298
6751
|
});
|
|
6299
6752
|
this.track(healJob);
|
|
6300
6753
|
}
|
|
@@ -6332,13 +6785,19 @@ var WriteService = class {
|
|
|
6332
6785
|
}
|
|
6333
6786
|
}
|
|
6334
6787
|
}
|
|
6335
|
-
reportBackgroundFailure(entityId, job) {
|
|
6788
|
+
reportBackgroundFailure(entityId, job, err) {
|
|
6789
|
+
const searchUnavailable = isWikiSearchUnavailableError(err);
|
|
6790
|
+
if (!searchUnavailable) console.error(err);
|
|
6791
|
+
else if (!this.warnedSearchUnavailable) {
|
|
6792
|
+
this.warnedSearchUnavailable = true;
|
|
6793
|
+
console.warn("[WikiMemory] Auto librarian/heal pass skipped: keyword search is unavailable. Work is held, not lost, until it recovers (a no-arg syncSearchIndex() retries the init); later skips report only via onDiagnostic.");
|
|
6794
|
+
}
|
|
6336
6795
|
emitDiagnostic(this.options, {
|
|
6337
6796
|
code: "background_job_failed",
|
|
6338
6797
|
operation: job,
|
|
6339
6798
|
trigger: "auto",
|
|
6340
6799
|
entityId,
|
|
6341
|
-
detail: { reason: "unhandled_rejection" }
|
|
6800
|
+
detail: { reason: searchUnavailable ? "keyword_search_unavailable" : "unhandled_rejection" }
|
|
6342
6801
|
});
|
|
6343
6802
|
}
|
|
6344
6803
|
};
|