nlm-memory 0.12.0 → 0.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.
Files changed (125) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/classify-parity.js +1 -1
  3. package/dist/cli/classify-parity.js.map +1 -1
  4. package/dist/cli/nlm.js +314 -55
  5. package/dist/cli/nlm.js.map +1 -1
  6. package/dist/core/classifier/prompt.d.ts +70 -0
  7. package/dist/core/classifier/prompt.js +71 -2
  8. package/dist/core/classifier/prompt.js.map +1 -1
  9. package/dist/core/exemplars/extract-exemplar.d.ts +36 -0
  10. package/dist/core/exemplars/extract-exemplar.js +177 -0
  11. package/dist/core/exemplars/extract-exemplar.js.map +1 -0
  12. package/dist/core/exemplars/ingest-exemplar.d.ts +30 -0
  13. package/dist/core/exemplars/ingest-exemplar.js +71 -0
  14. package/dist/core/exemplars/ingest-exemplar.js.map +1 -0
  15. package/dist/core/exemplars/recall-code.d.ts +25 -0
  16. package/dist/core/exemplars/recall-code.js +49 -0
  17. package/dist/core/exemplars/recall-code.js.map +1 -0
  18. package/dist/core/facts/backfill-facts.d.ts +4 -2
  19. package/dist/core/facts/backfill-facts.js +18 -27
  20. package/dist/core/facts/backfill-facts.js.map +1 -1
  21. package/dist/core/hook/strip-injected-context.d.ts +24 -0
  22. package/dist/core/hook/strip-injected-context.js +73 -0
  23. package/dist/core/hook/strip-injected-context.js.map +1 -0
  24. package/dist/core/ingest/ingest-session.d.ts +4 -2
  25. package/dist/core/ingest/ingest-session.js +12 -4
  26. package/dist/core/ingest/ingest-session.js.map +1 -1
  27. package/dist/core/providers/provider-registry.d.ts +31 -11
  28. package/dist/core/providers/provider-registry.js +27 -13
  29. package/dist/core/providers/provider-registry.js.map +1 -1
  30. package/dist/core/recall-facts/fact-recall-service.js +75 -28
  31. package/dist/core/recall-facts/fact-recall-service.js.map +1 -1
  32. package/dist/core/scheduler/scheduler.d.ts +4 -2
  33. package/dist/core/scheduler/scheduler.js +26 -15
  34. package/dist/core/scheduler/scheduler.js.map +1 -1
  35. package/dist/core/sources/source-registry.d.ts +36 -12
  36. package/dist/core/sources/source-registry.js +59 -44
  37. package/dist/core/sources/source-registry.js.map +1 -1
  38. package/dist/core/storage/backup-rotation.d.ts +38 -0
  39. package/dist/core/storage/backup-rotation.js +82 -0
  40. package/dist/core/storage/backup-rotation.js.map +1 -0
  41. package/dist/core/storage/null-code-exemplar-store.d.ts +21 -0
  42. package/dist/core/storage/null-code-exemplar-store.js +19 -0
  43. package/dist/core/storage/null-code-exemplar-store.js.map +1 -0
  44. package/dist/core/storage/pg-fact-store.d.ts +2 -0
  45. package/dist/core/storage/pg-fact-store.js +34 -3
  46. package/dist/core/storage/pg-fact-store.js.map +1 -1
  47. package/dist/core/storage/pg-session-store.d.ts +14 -4
  48. package/dist/core/storage/pg-session-store.js +123 -2
  49. package/dist/core/storage/pg-session-store.js.map +1 -1
  50. package/dist/core/storage/pg-storage.d.ts +11 -2
  51. package/dist/core/storage/pg-storage.js +14 -2
  52. package/dist/core/storage/pg-storage.js.map +1 -1
  53. package/dist/core/storage/pg-tx-context.d.ts +2 -0
  54. package/dist/core/storage/pg-tx-context.js +12 -0
  55. package/dist/core/storage/pg-tx-context.js.map +1 -1
  56. package/dist/core/storage/sqlite-code-exemplar-store.d.ts +40 -0
  57. package/dist/core/storage/sqlite-code-exemplar-store.js +247 -0
  58. package/dist/core/storage/sqlite-code-exemplar-store.js.map +1 -0
  59. package/dist/core/storage/sqlite-fact-store.d.ts +2 -0
  60. package/dist/core/storage/sqlite-fact-store.js +30 -2
  61. package/dist/core/storage/sqlite-fact-store.js.map +1 -1
  62. package/dist/core/storage/sqlite-session-store.d.ts +21 -0
  63. package/dist/core/storage/sqlite-session-store.js +23 -0
  64. package/dist/core/storage/sqlite-session-store.js.map +1 -1
  65. package/dist/core/storage/sqlite-storage.d.ts +6 -0
  66. package/dist/core/storage/sqlite-storage.js +14 -2
  67. package/dist/core/storage/sqlite-storage.js.map +1 -1
  68. package/dist/core/storage/supersedence-log.d.ts +8 -0
  69. package/dist/core/storage/supersedence-log.js +19 -0
  70. package/dist/core/storage/supersedence-log.js.map +1 -1
  71. package/dist/hook/prompt-recall-hook.d.ts +5 -4
  72. package/dist/hook/prompt-recall-hook.js +12 -7
  73. package/dist/hook/prompt-recall-hook.js.map +1 -1
  74. package/dist/http/app.d.ts +7 -1
  75. package/dist/http/app.js +226 -83
  76. package/dist/http/app.js.map +1 -1
  77. package/dist/install/codex.d.ts +29 -1
  78. package/dist/install/codex.js +117 -15
  79. package/dist/install/codex.js.map +1 -1
  80. package/dist/install/cursor.d.ts +4 -4
  81. package/dist/install/cursor.js +7 -7
  82. package/dist/install/cursor.js.map +1 -1
  83. package/dist/install/health.d.ts +56 -0
  84. package/dist/install/health.js +133 -0
  85. package/dist/install/health.js.map +1 -0
  86. package/dist/install/setup.js +1 -1
  87. package/dist/install/setup.js.map +1 -1
  88. package/dist/install/windsurf.d.ts +4 -4
  89. package/dist/install/windsurf.js +7 -7
  90. package/dist/install/windsurf.js.map +1 -1
  91. package/dist/llm/classifier-box.d.ts +4 -0
  92. package/dist/llm/classifier-box.js +9 -2
  93. package/dist/llm/classifier-box.js.map +1 -1
  94. package/dist/llm/ollama-client.d.ts +19 -1
  95. package/dist/llm/ollama-client.js +14 -6
  96. package/dist/llm/ollama-client.js.map +1 -1
  97. package/dist/llm/ollama-code-embedder.d.ts +28 -0
  98. package/dist/llm/ollama-code-embedder.js +73 -0
  99. package/dist/llm/ollama-code-embedder.js.map +1 -0
  100. package/dist/mcp/server.d.ts +10 -0
  101. package/dist/mcp/server.js +132 -1
  102. package/dist/mcp/server.js.map +1 -1
  103. package/dist/ports/code-embedder.d.ts +27 -0
  104. package/dist/ports/code-embedder.js +15 -0
  105. package/dist/ports/code-embedder.js.map +1 -0
  106. package/dist/ports/code-exemplar-store.d.ts +45 -0
  107. package/dist/ports/code-exemplar-store.js +11 -0
  108. package/dist/ports/code-exemplar-store.js.map +1 -0
  109. package/dist/ports/fact-store.d.ts +0 -0
  110. package/dist/ports/fact-store.js.map +1 -1
  111. package/dist/ports/storage.d.ts +8 -0
  112. package/dist/shared/types.d.ts +35 -0
  113. package/dist/ui/assets/{index-Fr5AMEPj.js → index-CVo7KKw2.js} +1 -1
  114. package/dist/ui/assets/{index-C74N8APQ.css → index-DCuQrzJH.css} +1 -1
  115. package/dist/ui/index.html +2 -2
  116. package/migrations/021_code_exemplars.sql +51 -0
  117. package/migrations/022_facts_retired_at.sql +17 -0
  118. package/migrations/pg/021_code_exemplars.sql +45 -0
  119. package/migrations/pg/022_facts_retired_at.sql +12 -0
  120. package/nlm/index.js +6 -1
  121. package/package.json +3 -2
  122. package/plugin/.codex-plugin/plugin.json +2 -2
  123. package/plugin/hooks/hooks.json +13 -2
  124. package/plugin/scripts/prompt-recall-hook.mjs +7 -1
  125. package/plugin/scripts/session-start-hook.mjs +275 -0
package/dist/http/app.js CHANGED
@@ -45,8 +45,12 @@ import { SourceRegistry, PgSourceRegistry, } from "../core/sources/source-regist
45
45
  import { ProviderRegistry, PgProviderRegistry, } from "../core/providers/provider-registry.js";
46
46
  import { listModels } from "../core/providers/provider-models.js";
47
47
  import { ingestSession, deriveSessionId } from "../core/ingest/ingest-session.js";
48
- import { listActions, undoAction, writeAction, writeActionsBatch, } from "../core/actions/actions-log.js";
48
+ import { listActions, listActionsPg, undoAction, undoActionPg, writeAction, writeActionPg, writeActionsBatch, writeActionsBatchPg, } from "../core/actions/actions-log.js";
49
+ import { PgSessionStore } from "../core/storage/pg-session-store.js";
49
50
  import { normalizeSignal } from "../core/signals/ingest-signal.js";
51
+ import { normalizeExemplar } from "../core/exemplars/ingest-exemplar.js";
52
+ import { extractExemplar } from "../core/exemplars/extract-exemplar.js";
53
+ import { recallCode } from "../core/exemplars/recall-code.js";
50
54
  import { buildFailureModeBlock } from "../core/signals/failure-mode-recall.js";
51
55
  import { aggregateFailureModes } from "../core/signals/aggregate.js";
52
56
  const MIME_TYPES = {
@@ -73,6 +77,81 @@ const DATA_STAT_TABLES = [
73
77
  "sources",
74
78
  "providers",
75
79
  ];
80
+ function sqliteDataStats(db, dbPath) {
81
+ const countOf = (table) => {
82
+ try {
83
+ const row = db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get();
84
+ return row?.n ?? 0;
85
+ }
86
+ catch {
87
+ return 0;
88
+ }
89
+ };
90
+ const tables = DATA_STAT_TABLES.map((name) => ({ name, rows: countOf(name) }));
91
+ const migrations = db
92
+ .prepare("SELECT version, name, applied_at FROM schema_migrations ORDER BY version")
93
+ .all();
94
+ const runtimes = db
95
+ .prepare("SELECT runtime, COUNT(*) AS n FROM sessions GROUP BY runtime ORDER BY n DESC")
96
+ .all();
97
+ let dbBytes = 0;
98
+ let dbPresent = false;
99
+ try {
100
+ dbBytes = statSync(dbPath).size;
101
+ dbPresent = true;
102
+ }
103
+ catch { /* file absent */ }
104
+ for (const sidecar of [`${dbPath}-wal`, `${dbPath}-shm`]) {
105
+ try {
106
+ dbBytes += statSync(sidecar).size;
107
+ }
108
+ catch { /* no sidecar */ }
109
+ }
110
+ return {
111
+ dbPath,
112
+ dbBytes,
113
+ dbPresent,
114
+ schemaVersion: migrations.length > 0 ? migrations[migrations.length - 1].version : null,
115
+ migrations,
116
+ tables,
117
+ runtimes,
118
+ };
119
+ }
120
+ async function pgDataStats(pool) {
121
+ const countOf = async (table) => {
122
+ try {
123
+ const r = await pool.query(`SELECT COUNT(*) AS n FROM ${table}`);
124
+ return Number(r.rows[0]?.n ?? 0);
125
+ }
126
+ catch {
127
+ return 0;
128
+ }
129
+ };
130
+ const tables = await Promise.all(DATA_STAT_TABLES.map(async (name) => ({ name, rows: await countOf(name) })));
131
+ const migrationsResult = await pool.query("SELECT version, name, applied_at FROM schema_migrations ORDER BY version");
132
+ const migrations = migrationsResult.rows.map((r) => ({
133
+ version: r.version,
134
+ name: r.name,
135
+ applied_at: String(r.applied_at),
136
+ }));
137
+ const runtimesResult = await pool.query("SELECT runtime, COUNT(*) AS n FROM sessions GROUP BY runtime ORDER BY n DESC");
138
+ const runtimes = runtimesResult.rows.map((r) => ({ runtime: r.runtime, n: Number(r.n) }));
139
+ let dbBytes = 0;
140
+ try {
141
+ const sizeResult = await pool.query("SELECT pg_database_size(current_database()) AS bytes");
142
+ dbBytes = Number(sizeResult.rows[0]?.bytes ?? 0);
143
+ }
144
+ catch { /* size unavailable */ }
145
+ return {
146
+ dbPath: "postgresql",
147
+ dbBytes,
148
+ dbPresent: true,
149
+ schemaVersion: migrations.length > 0 ? migrations[migrations.length - 1].version : null,
150
+ migrations,
151
+ tables,
152
+ runtimes,
153
+ };
154
+ }
76
155
  function parseLimit(raw, fallback, max) {
77
156
  if (raw === undefined)
78
157
  return fallback;
@@ -787,50 +866,14 @@ function registerDatasetRoute(app, deps) {
787
866
  // ── Data management ─────────────────────────────────────────────
788
867
  // Storage stats, live-safe backup snapshot, and staged restore.
789
868
  function registerDataManagementRoutes(app, deps) {
790
- app.get("/api/data/stats", (c) => {
869
+ app.get("/api/data/stats", async (c) => {
791
870
  if (!deps.liveStore || !deps.dbPath) {
792
871
  return c.json({ error: "data stats require liveStore + dbPath" }, 503);
793
872
  }
794
- // TODO(#215a): replace rawDb() with port methods; cast until then
795
- const db = deps.liveStore.rawDb();
796
- const countOf = (table) => {
797
- try {
798
- const row = db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get();
799
- return row?.n ?? 0;
800
- }
801
- catch {
802
- return 0;
803
- }
804
- };
805
- const tables = DATA_STAT_TABLES.map((name) => ({ name, rows: countOf(name) }));
806
- const migrations = db
807
- .prepare("SELECT version, name, applied_at FROM schema_migrations ORDER BY version")
808
- .all();
809
- const runtimes = db
810
- .prepare("SELECT runtime, COUNT(*) AS n FROM sessions GROUP BY runtime ORDER BY n DESC")
811
- .all();
812
- let dbBytes = 0;
813
- let dbPresent = false;
814
- try {
815
- dbBytes = statSync(deps.dbPath).size;
816
- dbPresent = true;
817
- }
818
- catch { /* file absent */ }
819
- for (const sidecar of [`${deps.dbPath}-wal`, `${deps.dbPath}-shm`]) {
820
- try {
821
- dbBytes += statSync(sidecar).size;
822
- }
823
- catch { /* no sidecar */ }
824
- }
825
- return c.json({
826
- dbPath: deps.dbPath,
827
- dbBytes,
828
- dbPresent,
829
- schemaVersion: migrations.length > 0 ? migrations[migrations.length - 1].version : null,
830
- migrations,
831
- tables,
832
- runtimes,
833
- });
873
+ const stats = deps.liveStore instanceof PgSessionStore
874
+ ? await pgDataStats(deps.liveStore.pool)
875
+ : sqliteDataStats(deps.liveStore.rawDb(), deps.dbPath);
876
+ return c.json(stats);
834
877
  });
835
878
  app.get("/api/data/backup", (c) => {
836
879
  const adminToken = process.env["NLM_MCP_TOKEN"];
@@ -846,9 +889,16 @@ function registerDataManagementRoutes(app, deps) {
846
889
  if (!deps.liveStore || !deps.dbPath) {
847
890
  return c.json({ error: "backup requires liveStore + dbPath" }, 503);
848
891
  }
892
+ // VACUUM INTO is a SQLite-only mechanism. PostgreSQL backups are delegated
893
+ // to the database layer (pg_dump / managed-PG snapshots / WAL archiving),
894
+ // not produced in-process by the daemon.
895
+ if (deps.liveStore instanceof PgSessionStore) {
896
+ return c.json({
897
+ error: "daemon backup is SQLite-only; for PostgreSQL use pg_dump or your managed-PG snapshot facility",
898
+ }, 501);
899
+ }
849
900
  const scratch = snapshotScratchPath(deps.dbPath);
850
901
  try {
851
- // TODO(#215a): replace rawDb() with port methods; cast until then
852
902
  vacuumSnapshot(deps.liveStore.rawDb(), scratch);
853
903
  const bytes = readFileSync(scratch);
854
904
  const stamp = new Date().toISOString().slice(0, 10);
@@ -876,6 +926,13 @@ function registerDataManagementRoutes(app, deps) {
876
926
  }
877
927
  if (!deps.dbPath)
878
928
  return c.json({ error: "restore requires dbPath" }, 503);
929
+ // Staged restore swaps the SQLite file on restart — SQLite-only. PG restores
930
+ // go through pg_restore / managed-PG point-in-time recovery.
931
+ if (deps.liveStore instanceof PgSessionStore) {
932
+ return c.json({
933
+ error: "daemon restore is SQLite-only; for PostgreSQL use pg_restore or your managed-PG recovery facility",
934
+ }, 501);
935
+ }
879
936
  const form = await c.req.parseBody().catch(() => null);
880
937
  const file = form?.["file"];
881
938
  if (!(file instanceof File)) {
@@ -913,8 +970,10 @@ function registerActionRoutes(app, deps) {
913
970
  const parsed = parseActionInput(body);
914
971
  if (!parsed)
915
972
  return c.json({ error: "invalid action payload" }, 400);
916
- // TODO(#215a): replace rawDb() with port methods; cast until then
917
- const id = writeAction(deps.liveStore.rawDb(), parsed);
973
+ const store = deps.liveStore;
974
+ const id = store instanceof PgSessionStore
975
+ ? await writeActionPg(store.pool, parsed)
976
+ : writeAction(store.rawDb(), parsed);
918
977
  return c.json({ id, timestamp: new Date().toISOString() });
919
978
  });
920
979
  app.post("/api/action/batch", async (c) => {
@@ -928,39 +987,46 @@ function registerActionRoutes(app, deps) {
928
987
  .filter((x) => x !== null);
929
988
  if (inputs.length === 0)
930
989
  return c.json({ accepted: 0, ids: [] });
931
- // TODO(#215a): replace rawDb() with port methods; cast until then
932
- const ids = writeActionsBatch(deps.liveStore.rawDb(), inputs);
990
+ const store = deps.liveStore;
991
+ const ids = store instanceof PgSessionStore
992
+ ? await writeActionsBatchPg(store.pool, inputs)
993
+ : writeActionsBatch(store.rawDb(), inputs);
933
994
  return c.json({ accepted: ids.length, ids });
934
995
  });
935
- app.post("/api/action/:id/undo", (c) => {
996
+ app.post("/api/action/:id/undo", async (c) => {
936
997
  if (!deps.liveStore)
937
998
  return c.json({ error: "actions require liveStore" }, 503);
938
- // TODO(#215a): replace rawDb() with port methods; cast until then
939
- const result = undoAction(deps.liveStore.rawDb(), c.req.param("id"));
999
+ const store = deps.liveStore;
1000
+ const result = store instanceof PgSessionStore
1001
+ ? await undoActionPg(store.pool, c.req.param("id"))
1002
+ : undoAction(store.rawDb(), c.req.param("id"));
940
1003
  if (!result)
941
1004
  return c.json({ error: "action not found or already undone" }, 404);
942
1005
  return c.json({ id: result.undoId, timestamp: new Date().toISOString() });
943
1006
  });
944
- app.get("/api/actions", (c) => {
1007
+ app.get("/api/actions", async (c) => {
945
1008
  if (!deps.liveStore)
946
1009
  return c.json({ actions: [] });
947
1010
  const limitRaw = c.req.query("limit");
948
1011
  const subjectId = c.req.query("subject_id");
949
1012
  const kind = c.req.query("kind");
950
1013
  const limit = limitRaw ? Math.max(1, Math.min(500, Number.parseInt(limitRaw, 10))) : 100;
951
- // TODO(#215a): replace rawDb() with port methods; cast until then
952
- const rows = listActions(deps.liveStore.rawDb(), {
1014
+ const opts = {
953
1015
  limit,
954
1016
  ...(subjectId ? { subjectId } : {}),
955
1017
  ...(kind ? { kind } : {}),
956
- });
1018
+ };
1019
+ const store = deps.liveStore;
1020
+ const rows = store instanceof PgSessionStore
1021
+ ? await listActionsPg(store.pool, opts)
1022
+ : listActions(store.rawDb(), opts);
957
1023
  return c.json({ actions: rows });
958
1024
  });
959
1025
  }
960
1026
  function registerClassifierRoutes(app, deps) {
961
1027
  app.get("/api/classifier/info", (c) => {
962
1028
  const provider = deps.classifier?.provider ?? "ollama";
963
- const model = deps.classifier?.model ?? "qwen3:4b-instruct-2507-q4_K_M";
1029
+ const model = deps.classifier?.model ?? "qwen3.5:4b";
964
1030
  return c.json({
965
1031
  provider,
966
1032
  model,
@@ -971,7 +1037,7 @@ function registerClassifierRoutes(app, deps) {
971
1037
  },
972
1038
  default_models: {
973
1039
  deepseek: ["deepseek-v4-flash", "deepseek-v4-pro", "deepseek-chat"],
974
- ollama: ["qwen3:4b-instruct-2507-q4_K_M", "qwen2.5:3b-instruct", "llama3.2:3b", "mistral:7b"],
1040
+ ollama: ["qwen3.5:4b", "qwen3:4b-instruct-2507-q4_K_M", "qwen2.5:3b-instruct", "llama3.2:3b", "mistral:7b"],
975
1041
  },
976
1042
  embedder: deps.embedderInfo ?? { provider: "ollama", model: "nomic-embed-text", dims: 768 },
977
1043
  });
@@ -999,10 +1065,10 @@ function registerClassifierRoutes(app, deps) {
999
1065
  // Each row = one transcript origin the daemon scans. UI uses these
1000
1066
  // endpoints to surface existing sources + let users add custom ones.
1001
1067
  function registerSourceRoutes(app, deps) {
1002
- app.get("/api/sources", (c) => {
1068
+ app.get("/api/sources", async (c) => {
1003
1069
  if (!deps.sources)
1004
1070
  return c.json({ sources: [] });
1005
- return c.json({ sources: deps.sources.list() });
1071
+ return c.json({ sources: await deps.sources.list() });
1006
1072
  });
1007
1073
  app.post("/api/sources", async (c) => {
1008
1074
  if (!deps.sources)
@@ -1011,11 +1077,10 @@ function registerSourceRoutes(app, deps) {
1011
1077
  const parsed = parseSourceInsert(body);
1012
1078
  if (!parsed)
1013
1079
  return c.json({ error: "invalid source payload" }, 400);
1014
- // TODO(#215a): PgSourceRegistry port; cast until then
1015
- if (deps.sources.getByName(parsed.name)) {
1080
+ if (await deps.sources.getByName(parsed.name)) {
1016
1081
  return c.json({ error: `source named '${parsed.name}' already exists` }, 409);
1017
1082
  }
1018
- return c.json(deps.sources.insert(parsed), 201);
1083
+ return c.json(await deps.sources.insert(parsed), 201);
1019
1084
  });
1020
1085
  app.patch("/api/sources/:id", async (c) => {
1021
1086
  if (!deps.sources)
@@ -1027,30 +1092,29 @@ function registerSourceRoutes(app, deps) {
1027
1092
  const patch = parseSourceUpdate(body);
1028
1093
  if (!patch)
1029
1094
  return c.json({ error: "invalid patch payload" }, 400);
1030
- const updated = deps.sources.update(id, patch);
1095
+ const updated = await deps.sources.update(id, patch);
1031
1096
  if (!updated)
1032
1097
  return c.json({ error: `source ${id} not found` }, 404);
1033
1098
  return c.json(updated);
1034
1099
  });
1035
- app.delete("/api/sources/:id", (c) => {
1100
+ app.delete("/api/sources/:id", async (c) => {
1036
1101
  if (!deps.sources)
1037
1102
  return c.json({ error: "sources registry unavailable" }, 503);
1038
1103
  const id = Number.parseInt(c.req.param("id"), 10);
1039
1104
  if (!Number.isFinite(id))
1040
1105
  return c.json({ error: "invalid id" }, 400);
1041
- const ok = deps.sources.delete(id);
1106
+ const ok = await deps.sources.delete(id);
1042
1107
  if (!ok)
1043
1108
  return c.json({ error: `source ${id} not found` }, 404);
1044
1109
  return c.json({ deleted: id });
1045
1110
  });
1046
- app.post("/api/sources/:id/regenerate-token", (c) => {
1111
+ app.post("/api/sources/:id/regenerate-token", async (c) => {
1047
1112
  if (!deps.sources)
1048
1113
  return c.json({ error: "sources registry unavailable" }, 503);
1049
1114
  const id = Number.parseInt(c.req.param("id"), 10);
1050
1115
  if (!Number.isFinite(id))
1051
1116
  return c.json({ error: "invalid id" }, 400);
1052
- // TODO(#215a): PgSourceRegistry port; cast until then
1053
- const token = deps.sources.regenerateToken(id);
1117
+ const token = await deps.sources.regenerateToken(id);
1054
1118
  if (!token)
1055
1119
  return c.json({ error: "regenerate-token only applies to webhook sources" }, 400);
1056
1120
  return c.json({ token });
@@ -1067,8 +1131,7 @@ function registerIngestRoute(app, deps) {
1067
1131
  const match = /^Bearer\s+(\S+)$/i.exec(auth);
1068
1132
  if (!match || !match[1])
1069
1133
  return c.json({ error: "missing or malformed bearer token" }, 401);
1070
- // TODO(#215a): PgSourceRegistry port; cast until then
1071
- const source = deps.sources.findByToken(match[1]);
1134
+ const source = await deps.sources.findByToken(match[1]);
1072
1135
  if (!source || source.kind !== "webhook")
1073
1136
  return c.json({ error: "invalid token" }, 401);
1074
1137
  if (!source.enabled)
@@ -1103,10 +1166,10 @@ function registerIngestRoute(app, deps) {
1103
1166
  // Each row = one LLM endpoint. Keys are redacted on every response
1104
1167
  // (rows carry hasApiKey:boolean instead).
1105
1168
  function registerProviderRoutes(app, deps) {
1106
- app.get("/api/providers", (c) => {
1169
+ app.get("/api/providers", async (c) => {
1107
1170
  if (!deps.providers)
1108
1171
  return c.json({ providers: [] });
1109
- return c.json({ providers: deps.providers.list() });
1172
+ return c.json({ providers: await deps.providers.list() });
1110
1173
  });
1111
1174
  app.post("/api/providers", async (c) => {
1112
1175
  if (!deps.providers)
@@ -1115,11 +1178,10 @@ function registerProviderRoutes(app, deps) {
1115
1178
  const parsed = parseProviderInsert(body);
1116
1179
  if (!parsed)
1117
1180
  return c.json({ error: "invalid provider payload" }, 400);
1118
- // TODO(#215a): PgProviderRegistry port; cast until then
1119
- if (deps.providers.getByName(parsed.name)) {
1181
+ if (await deps.providers.getByName(parsed.name)) {
1120
1182
  return c.json({ error: `provider named '${parsed.name}' already exists` }, 409);
1121
1183
  }
1122
- return c.json(deps.providers.insert(parsed), 201);
1184
+ return c.json(await deps.providers.insert(parsed), 201);
1123
1185
  });
1124
1186
  app.patch("/api/providers/:id", async (c) => {
1125
1187
  if (!deps.providers)
@@ -1131,18 +1193,18 @@ function registerProviderRoutes(app, deps) {
1131
1193
  const patch = parseProviderUpdate(body);
1132
1194
  if (!patch)
1133
1195
  return c.json({ error: "invalid patch payload" }, 400);
1134
- const updated = deps.providers.update(id, patch);
1196
+ const updated = await deps.providers.update(id, patch);
1135
1197
  if (!updated)
1136
1198
  return c.json({ error: `provider ${id} not found` }, 404);
1137
1199
  return c.json(updated);
1138
1200
  });
1139
- app.delete("/api/providers/:id", (c) => {
1201
+ app.delete("/api/providers/:id", async (c) => {
1140
1202
  if (!deps.providers)
1141
1203
  return c.json({ error: "providers registry unavailable" }, 503);
1142
1204
  const id = Number.parseInt(c.req.param("id"), 10);
1143
1205
  if (!Number.isFinite(id))
1144
1206
  return c.json({ error: "invalid id" }, 400);
1145
- const ok = deps.providers.delete(id);
1207
+ const ok = await deps.providers.delete(id);
1146
1208
  if (!ok)
1147
1209
  return c.json({ error: `provider ${id} not found` }, 404);
1148
1210
  return c.json({ deleted: id });
@@ -1153,11 +1215,10 @@ function registerProviderRoutes(app, deps) {
1153
1215
  const id = Number.parseInt(c.req.param("id"), 10);
1154
1216
  if (!Number.isFinite(id))
1155
1217
  return c.json({ error: "invalid id" }, 400);
1156
- // TODO(#215a): PgProviderRegistry port; cast until then
1157
- const provider = deps.providers.get(id);
1218
+ const provider = await deps.providers.get(id);
1158
1219
  if (!provider)
1159
1220
  return c.json({ error: `provider ${id} not found` }, 404);
1160
- const key = deps.providers.getSecret(id);
1221
+ const key = await deps.providers.getSecret(id);
1161
1222
  try {
1162
1223
  const models = await listModels(provider, { apiKey: key });
1163
1224
  return c.json({ models });
@@ -1173,11 +1234,10 @@ function registerProviderRoutes(app, deps) {
1173
1234
  const id = Number.parseInt(c.req.param("id"), 10);
1174
1235
  if (!Number.isFinite(id))
1175
1236
  return c.json({ error: "invalid id" }, 400);
1176
- // TODO(#215a): PgProviderRegistry port; cast until then
1177
- const provider = deps.providers.get(id);
1237
+ const provider = await deps.providers.get(id);
1178
1238
  if (!provider)
1179
1239
  return c.json({ error: `provider ${id} not found` }, 404);
1180
- const key = deps.providers.getSecret(id);
1240
+ const key = await deps.providers.getSecret(id);
1181
1241
  const startedAt = Date.now();
1182
1242
  try {
1183
1243
  const models = await listModels(provider, { apiKey: key });
@@ -1412,6 +1472,28 @@ function registerSignalRoutes(app, deps) {
1412
1472
  catch {
1413
1473
  return c.json({ error: "signal insert failed" }, 500);
1414
1474
  }
1475
+ // Best-effort code-exemplar capture: a code-bearing signal (git_sha or
1476
+ // detail.code) becomes a retrievable exemplar. Gated by the flag and
1477
+ // never allowed to fail the signal write — extractExemplar returns null
1478
+ // when there's no code, and embedding is fire-and-forget.
1479
+ if (process.env["NLM_CODE_EXEMPLARS_ENABLED"] === "1" &&
1480
+ deps.exemplarStore &&
1481
+ deps.installScope !== undefined) {
1482
+ const exemplarStore = deps.exemplarStore;
1483
+ try {
1484
+ const exemplar = extractExemplar(signal, { installScope: deps.installScope });
1485
+ if (exemplar) {
1486
+ const { id, skipped } = await exemplarStore.insert(exemplar);
1487
+ if (!skipped && deps.codeEmbedder) {
1488
+ void deps.codeEmbedder
1489
+ .embed(exemplar.taskContext + "\n" + exemplar.code, "document")
1490
+ .then((r) => exemplarStore.upsertEmbedding(id, r.vector))
1491
+ .catch(() => { });
1492
+ }
1493
+ }
1494
+ }
1495
+ catch { /* capture is best-effort; never blocks signal ingest */ }
1496
+ }
1415
1497
  return c.json({ id: signal.id, status: "accepted" }, 202);
1416
1498
  });
1417
1499
  app.get("/api/signals/failure-modes", async (c) => {
@@ -1439,6 +1521,67 @@ function registerSignalRoutes(app, deps) {
1439
1521
  const modes = aggregateFailureModes(rows);
1440
1522
  return c.json({ days, total: rows.length, modes });
1441
1523
  });
1524
+ // ── Code exemplar routes (NLM_CODE_EXEMPLARS_ENABLED=1) ───────────────────
1525
+ app.post("/api/exemplar", async (c) => {
1526
+ if (!deps.exemplarStore || deps.installScope === undefined) {
1527
+ return c.json({ error: "exemplar store not wired in this deployment" }, 503);
1528
+ }
1529
+ if (process.env["NLM_CODE_EXEMPLARS_ENABLED"] !== "1") {
1530
+ return c.json({ error: "code exemplars disabled" }, 403);
1531
+ }
1532
+ let body;
1533
+ try {
1534
+ body = await c.req.json();
1535
+ }
1536
+ catch {
1537
+ return c.json({ error: "body must be JSON" }, 400);
1538
+ }
1539
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
1540
+ return c.json({ error: "body must be a JSON object" }, 400);
1541
+ }
1542
+ let inp;
1543
+ try {
1544
+ inp = normalizeExemplar({ ...body, installScope: deps.installScope });
1545
+ }
1546
+ catch (e) {
1547
+ return c.json({ error: e instanceof Error ? e.message : "invalid exemplar" }, 400);
1548
+ }
1549
+ const { id, skipped } = await deps.exemplarStore.insert(inp);
1550
+ if (!skipped && deps.codeEmbedder) {
1551
+ // Best-effort embedding: fire-and-forget, never blocks the response.
1552
+ deps.codeEmbedder.embed(inp.taskContext + "\n" + inp.code, "document")
1553
+ .then((r) => deps.exemplarStore.upsertEmbedding(id, r.vector))
1554
+ .catch(() => { });
1555
+ }
1556
+ return c.json({ id, skipped, status: "accepted" }, 202);
1557
+ });
1558
+ app.get("/api/recall-code", async (c) => {
1559
+ if (!deps.exemplarStore || deps.installScope === undefined) {
1560
+ return c.json({ error: "exemplar store not wired in this deployment" }, 503);
1561
+ }
1562
+ if (process.env["NLM_CODE_EXEMPLARS_ENABLED"] !== "1") {
1563
+ return c.json({ error: "code exemplars disabled" }, 403);
1564
+ }
1565
+ const query = c.req.query("q");
1566
+ if (!query)
1567
+ return c.json({ error: "q (query) is required" }, 400);
1568
+ const kStr = c.req.query("k") ?? "5";
1569
+ const k = Math.max(1, Math.min(20, Number.parseInt(kStr, 10) || 5));
1570
+ const repo = c.req.query("repo");
1571
+ const lang = c.req.query("lang");
1572
+ const model = c.req.query("model");
1573
+ const includeNegatives = c.req.query("negatives") !== "0";
1574
+ const result = await recallCode({
1575
+ query,
1576
+ installScope: deps.installScope,
1577
+ ...(repo ? { repo } : {}),
1578
+ ...(lang ? { lang } : {}),
1579
+ ...(model ? { model } : {}),
1580
+ includeNegatives,
1581
+ k,
1582
+ }, deps.exemplarStore, deps.codeEmbedder ?? null, null);
1583
+ return c.json(result);
1584
+ });
1442
1585
  }
1443
1586
  function mountSpa(app, dist) {
1444
1587
  const indexHtml = join(dist, "index.html");