@gmickel/gno 1.40.0 → 1.42.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 (62) hide show
  1. package/README.md +1 -0
  2. package/assets/skill/SKILL.md +22 -1
  3. package/assets/skill/cli-reference.md +48 -0
  4. package/assets/skill/mcp-reference.md +31 -0
  5. package/browser-extension/artifacts/{gno-browser-clipper-v1.40.0.zip → gno-browser-clipper-v1.42.0.zip} +0 -0
  6. package/browser-extension/artifacts/gno-browser-clipper-v1.42.0.zip.sha256 +1 -0
  7. package/browser-extension/dist/manifest.json +1 -1
  8. package/package.json +3 -2
  9. package/spec/cli.md +146 -7
  10. package/spec/db/schema.sql +17 -0
  11. package/spec/mcp.md +350 -6
  12. package/spec/output-schemas/memory-recall.schema.json +159 -0
  13. package/spec/output-schemas/memory-remember.schema.json +164 -0
  14. package/spec/output-schemas/status.schema.json +269 -54
  15. package/src/cli/commands/daemon.ts +1 -0
  16. package/src/cli/commands/mcp.ts +3 -1
  17. package/src/cli/commands/memory.ts +491 -0
  18. package/src/cli/commands/status.ts +23 -4
  19. package/src/cli/options.ts +4 -0
  20. package/src/cli/program.ts +155 -0
  21. package/src/config/types.ts +10 -0
  22. package/src/core/audit-provenance.ts +91 -0
  23. package/src/core/audit-workspace.ts +17 -0
  24. package/src/core/connector-verifier.ts +2 -4
  25. package/src/core/memory-diagnostics.ts +144 -0
  26. package/src/core/memory-fence.ts +239 -0
  27. package/src/core/memory-recall.ts +269 -0
  28. package/src/core/memory-record.ts +435 -0
  29. package/src/core/memory-remember.ts +425 -0
  30. package/src/core/memory-types.ts +211 -0
  31. package/src/core/memory.ts +87 -0
  32. package/src/ingestion/sync.ts +17 -0
  33. package/src/mcp/AGENTS.md +7 -1
  34. package/src/mcp/CLAUDE.md +7 -1
  35. package/src/mcp/context.ts +37 -7
  36. package/src/mcp/http-egress.ts +2 -0
  37. package/src/mcp/http-modern.ts +214 -0
  38. package/src/mcp/http-security.ts +5 -0
  39. package/src/mcp/http-session.ts +4 -3
  40. package/src/mcp/http-transport.ts +81 -12
  41. package/src/mcp/resources/index.ts +3 -6
  42. package/src/mcp/server.ts +18 -16
  43. package/src/mcp/stdio-serving.ts +45 -0
  44. package/src/mcp/tool-descriptions-core.ts +56 -0
  45. package/src/mcp/tool-profile.ts +112 -0
  46. package/src/mcp/tools/index.ts +286 -126
  47. package/src/mcp/tools/memory-recall.ts +122 -0
  48. package/src/mcp/tools/memory-remember.ts +177 -0
  49. package/src/mcp/tools/memory-shared.ts +86 -0
  50. package/src/pipeline/search.ts +2 -0
  51. package/src/pipeline/types.ts +8 -0
  52. package/src/sdk/client.ts +94 -1
  53. package/src/sdk/index.ts +13 -0
  54. package/src/sdk/types.ts +28 -0
  55. package/src/serve/routes/api.ts +167 -0
  56. package/src/serve/routes/mcp.ts +1 -0
  57. package/src/serve/server.ts +27 -0
  58. package/src/store/migrations/027-memory-scopes.ts +37 -0
  59. package/src/store/migrations/index.ts +2 -0
  60. package/src/store/sqlite/adapter.ts +127 -3
  61. package/src/store/types.ts +54 -0
  62. package/browser-extension/artifacts/gno-browser-clipper-v1.40.0.zip.sha256 +0 -1
@@ -38,6 +38,7 @@ import type { StartJobError } from "../jobs";
38
38
  import type { ResidentStatus } from "../status-model";
39
39
  import type { CollectionWatchService } from "../watch-service";
40
40
 
41
+ import { getIndexDbPath } from "../../app/constants";
41
42
  import { buildVerifiedAsk } from "../../app/verified-ask";
42
43
  import { modelsPull } from "../../cli/commands/models/pull";
43
44
  import {
@@ -80,6 +81,13 @@ import {
80
81
  planCreateFolder,
81
82
  planDuplicateRefactor,
82
83
  } from "../../core/file-refactors";
84
+ import {
85
+ MemoryError,
86
+ type MemoryErrorCode,
87
+ MemoryService,
88
+ type RecallInput,
89
+ type RememberInput,
90
+ } from "../../core/memory";
83
91
  import {
84
92
  hasContentMutation,
85
93
  recordContentMutation,
@@ -117,6 +125,7 @@ import {
117
125
  validateTag,
118
126
  } from "../../core/tags";
119
127
  import { validateRelPath } from "../../core/validation";
128
+ import { writeLeasePath } from "../../core/write-lease";
120
129
  import {
121
130
  defaultSyncService,
122
131
  type SyncResult,
@@ -534,6 +543,12 @@ export interface CreateEditableCopyRequestBody {
534
543
  uri?: string;
535
544
  }
536
545
 
546
+ /** POST /api/memory/remember body: the shared core contract, verbatim. */
547
+ export interface MemoryRememberRequestBody extends RememberInput {}
548
+
549
+ /** POST /api/memory/recall body: the shared core contract, verbatim. */
550
+ export interface MemoryRecallRequestBody extends RecallInput {}
551
+
537
552
  export interface PublishExportRequestBody {
538
553
  encryptionPassphrase?: string;
539
554
  slug?: string;
@@ -3673,6 +3688,158 @@ export async function handleCreateCapture(
3673
3688
  }
3674
3689
  }
3675
3690
 
3691
+ // ─────────────────────────────────────────────────────────────────────────────
3692
+ // Memory (remember / recall)
3693
+ // ─────────────────────────────────────────────────────────────────────────────
3694
+
3695
+ const HTTP_CREATED = 201;
3696
+ const HTTP_NOT_FOUND = 404;
3697
+ const HTTP_CONFLICT = 409;
3698
+ const HTTP_INTERNAL = 500;
3699
+
3700
+ /** HTTP status per stable memory error code; the code itself is the wire code. */
3701
+ const MEMORY_ERROR_STATUS: Readonly<Record<MemoryErrorCode, number>> = {
3702
+ MEMORY_TEXT_REQUIRED: 400,
3703
+ MEMORY_TEXT_TOO_LARGE: 400,
3704
+ MEMORY_QUERY_REQUIRED: 400,
3705
+ MEMORY_BUDGET_INVALID: 400,
3706
+ MEMORY_COLLECTION_REQUIRED: 400,
3707
+ MEMORY_COLLECTION_NOT_FOUND: HTTP_NOT_FOUND,
3708
+ MEMORY_COLLECTION_UNMANAGED: 400,
3709
+ MEMORY_SCOPES_REQUIRED: 400,
3710
+ MEMORY_SCOPES_INVALID: 400,
3711
+ MEMORY_IDENTITY_REQUIRED: 400,
3712
+ MEMORY_DECISION_INVALID: 400,
3713
+ MEMORY_PREDECESSOR_REQUIRED: 400,
3714
+ MEMORY_PREDECESSOR_NOT_FOUND: HTTP_NOT_FOUND,
3715
+ MEMORY_PREDECESSOR_HASH_MISMATCH: HTTP_CONFLICT,
3716
+ MEMORY_SUPERSEDE_CONFLICT: HTTP_CONFLICT,
3717
+ MEMORY_FENCED_REPLAY: 400,
3718
+ MEMORY_FENCED_DERIVED: 400,
3719
+ MEMORY_WRITE_LEASE_BUSY: HTTP_CONFLICT,
3720
+ MEMORY_SYNC_FAILED: HTTP_INTERNAL,
3721
+ MEMORY_SUPERSEDE_PROJECTION_FAILED: HTTP_INTERNAL,
3722
+ MEMORY_QUERY_FAILED: HTTP_INTERNAL,
3723
+ };
3724
+
3725
+ export interface MemoryRouteDeps {
3726
+ /** Shared `.mcp-write.lock` path; defaults to the resident index's lease. */
3727
+ lockPath?: string;
3728
+ lockWaitMs?: number;
3729
+ }
3730
+
3731
+ function memoryErrorResponse(error: unknown, fallback: string): Response {
3732
+ if (error instanceof MemoryError) {
3733
+ return errorResponse(
3734
+ error.code,
3735
+ error.message,
3736
+ MEMORY_ERROR_STATUS[error.code]
3737
+ );
3738
+ }
3739
+ return errorResponse(
3740
+ "RUNTIME",
3741
+ `${fallback}: ${error instanceof Error ? error.message : String(error)}`,
3742
+ HTTP_INTERNAL
3743
+ );
3744
+ }
3745
+
3746
+ async function readMemoryBody(
3747
+ req: Request
3748
+ ): Promise<
3749
+ | { ok: true; body: Record<string, unknown> }
3750
+ | { ok: false; response: Response }
3751
+ > {
3752
+ let body: unknown;
3753
+ try {
3754
+ body = await req.json();
3755
+ } catch {
3756
+ return {
3757
+ ok: false,
3758
+ response: errorResponse("VALIDATION", "Invalid JSON body"),
3759
+ };
3760
+ }
3761
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
3762
+ return {
3763
+ ok: false,
3764
+ response: errorResponse(
3765
+ "VALIDATION",
3766
+ "Request body must be a JSON object"
3767
+ ),
3768
+ };
3769
+ }
3770
+ return { ok: true, body: body as Record<string, unknown> };
3771
+ }
3772
+
3773
+ /**
3774
+ * The service owns the shared write lease; this adapter never takes it.
3775
+ * Semantic matching/retrieval is enabled only when the resident context has
3776
+ * an embedding port (and a vector index for recall); otherwise the service
3777
+ * reports lexical-only mode in the result.
3778
+ */
3779
+ function createMemoryService(
3780
+ ctxHolder: ContextHolder,
3781
+ store: SqliteAdapter,
3782
+ deps: MemoryRouteDeps
3783
+ ): MemoryService {
3784
+ const ctx = ctxHolder.current;
3785
+ return new MemoryService({
3786
+ store,
3787
+ config: ctx.config,
3788
+ collections: ctx.config.collections,
3789
+ lockPath: deps.lockPath ?? writeLeasePath(getIndexDbPath(ctx.indexName)),
3790
+ lockWaitMs: deps.lockWaitMs,
3791
+ embedPort: ctx.embedPort,
3792
+ vectorIndex: ctx.vectorIndex,
3793
+ });
3794
+ }
3795
+
3796
+ /**
3797
+ * POST /api/memory/remember
3798
+ * Store a fact (or propose candidates) in a memory-managed collection.
3799
+ * Returns 201 when a record was written, 200 otherwise.
3800
+ */
3801
+ export async function handleMemoryRemember(
3802
+ ctxHolder: ContextHolder,
3803
+ store: SqliteAdapter,
3804
+ req: Request,
3805
+ deps: MemoryRouteDeps = {}
3806
+ ): Promise<Response> {
3807
+ const parsed = await readMemoryBody(req);
3808
+ if (!parsed.ok) return parsed.response;
3809
+ try {
3810
+ const result = await createMemoryService(ctxHolder, store, deps).remember(
3811
+ parsed.body as unknown as MemoryRememberRequestBody
3812
+ );
3813
+ const wrote = result.outcome === "added" || result.outcome === "superseded";
3814
+ if (wrote) ctxHolder.markContentMutation?.();
3815
+ return jsonResponse(result, wrote ? HTTP_CREATED : 200);
3816
+ } catch (error) {
3817
+ return memoryErrorResponse(error, "Failed to remember");
3818
+ }
3819
+ }
3820
+
3821
+ /**
3822
+ * POST /api/memory/recall
3823
+ * Budgeted, cited recall of current facts in the caller's explicit scopes.
3824
+ */
3825
+ export async function handleMemoryRecall(
3826
+ ctxHolder: ContextHolder,
3827
+ store: SqliteAdapter,
3828
+ req: Request,
3829
+ deps: MemoryRouteDeps = {}
3830
+ ): Promise<Response> {
3831
+ const parsed = await readMemoryBody(req);
3832
+ if (!parsed.ok) return parsed.response;
3833
+ try {
3834
+ const result = await createMemoryService(ctxHolder, store, deps).recall(
3835
+ parsed.body as unknown as MemoryRecallRequestBody
3836
+ );
3837
+ return jsonResponse(result);
3838
+ } catch (error) {
3839
+ return memoryErrorResponse(error, "Failed to recall");
3840
+ }
3841
+ }
3842
+
3676
3843
  /**
3677
3844
  * POST /api/docs
3678
3845
  * Create a new document in a collection.
@@ -30,6 +30,7 @@ export async function createMcpHttpGateway(
30
30
  config: ResolvedHttpGatewayConfig
31
31
  ): Promise<McpHttpGateway> {
32
32
  runtime.mcpContext.enableWrite = config.enableWrite;
33
+ runtime.mcpContext.toolProfile = config.toolProfile;
33
34
  const transport = new HttpMcpTransport(runtime, {
34
35
  enableWrite: config.enableWrite,
35
36
  idleTimeoutMs: config.limits.sessionIdleTimeoutMs,
@@ -59,6 +59,8 @@ import {
59
59
  handleHealth,
60
60
  handleImportPreview,
61
61
  handleInstallConnector,
62
+ handleMemoryRecall,
63
+ handleMemoryRemember,
62
64
  handleMoveDoc,
63
65
  handleNotePresets,
64
66
  handleJob,
@@ -403,6 +405,7 @@ export async function startServer(
403
405
  allowedHosts: options.allowedHosts,
404
406
  allowedOrigins: options.allowedOrigins,
405
407
  enableWrite: options.enableWrite,
408
+ toolProfile: options.toolProfile,
406
409
  });
407
410
  if (!isHttpGatewayLoopbackBind(gatewayConfig.host)) {
408
411
  await runtime.dispose();
@@ -828,6 +831,30 @@ export async function startServer(
828
831
  );
829
832
  },
830
833
  },
834
+ "/api/memory/remember": {
835
+ POST: async (req: Request) => {
836
+ if (!isRequestAllowed(req, port)) {
837
+ return withSecurityHeaders(forbiddenResponse(), isDev);
838
+ }
839
+ return withSecurityHeaders(
840
+ await handleMemoryRemember(ctxHolder, store, req),
841
+ isDev
842
+ );
843
+ },
844
+ },
845
+ "/api/memory/recall": {
846
+ POST: async (req: Request) => {
847
+ if (!isRequestAllowed(req, port)) {
848
+ return withSecurityHeaders(forbiddenResponse(), isDev);
849
+ }
850
+ return withSecurityHeaders(
851
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
852
+ handleMemoryRecall(ctxHolder, store, req)
853
+ ),
854
+ isDev
855
+ );
856
+ },
857
+ },
831
858
  "/api/docs": {
832
859
  GET: async (req: Request) => {
833
860
  const url = new URL(req.url);
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Migration: indexed memory scopes for managed memory records.
3
+ *
4
+ * Scopes are filterable inside retrieval queries (never post-hoc over a
5
+ * bounded candidate window), so they live in their own indexed table.
6
+ *
7
+ * @module src/store/migrations/027-memory-scopes
8
+ */
9
+
10
+ import type { Database } from "bun:sqlite";
11
+
12
+ import type { Migration } from "./runner";
13
+
14
+ export const migration: Migration = {
15
+ version: 27,
16
+ name: "memory_scopes",
17
+
18
+ up(db: Database): void {
19
+ db.exec(`
20
+ CREATE TABLE IF NOT EXISTS doc_memory_scopes (
21
+ document_id INTEGER NOT NULL,
22
+ scope TEXT NOT NULL,
23
+ PRIMARY KEY (document_id, scope),
24
+ FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE
25
+ )
26
+ `);
27
+ db.exec(`
28
+ CREATE INDEX IF NOT EXISTS idx_doc_memory_scopes_scope
29
+ ON doc_memory_scopes(scope, document_id)
30
+ `);
31
+ },
32
+
33
+ down(db: Database): void {
34
+ db.exec("DROP INDEX IF EXISTS idx_doc_memory_scopes_scope");
35
+ db.exec("DROP TABLE IF EXISTS doc_memory_scopes");
36
+ },
37
+ };
@@ -40,6 +40,7 @@ import { migration as m023 } from "./023-collection-egress-policy";
40
40
  import { migration as m024 } from "./024-egress-derived-lineage";
41
41
  import { migration as m025 } from "./025-collection-egress-policy-revision";
42
42
  import { migration as m026 } from "./026-file-refactor-recovery-journal";
43
+ import { migration as m027 } from "./027-memory-scopes";
43
44
 
44
45
  /** All migrations in order */
45
46
  export const migrations = [
@@ -69,4 +70,5 @@ export const migrations = [
69
70
  m024,
70
71
  m025,
71
72
  m026,
73
+ m027,
72
74
  ];
@@ -75,6 +75,8 @@ import type {
75
75
  IndexStatus,
76
76
  IngestErrorInput,
77
77
  IngestErrorRow,
78
+ MemoryEligibleDocument,
79
+ MemoryEligibleDocumentsOptions,
78
80
  MigrationResult,
79
81
  RetrievalTraceAppendResult,
80
82
  RetrievalTraceBundle,
@@ -324,6 +326,13 @@ type FtsQueryBuildResult =
324
326
  | { ok: true; query: string }
325
327
  | { ok: false; error: string };
326
328
 
329
+ /**
330
+ * SQL fragment excluding documents superseded by an active document via the
331
+ * typed `supersedes` edge. `docIdExpr` names the candidate document id column.
332
+ */
333
+ const SUPERSEDED_EXCLUSION_SQL = (docIdExpr: string): string =>
334
+ `AND NOT EXISTS (SELECT 1 FROM doc_edges se JOIN documents sd ON sd.id = se.src_doc_id AND sd.active = 1 WHERE se.dst_doc_id = ${docIdExpr} AND se.edge_type = 'supersedes')`;
335
+
327
336
  /**
328
337
  * Narrow lexical grammar for BM25/FTS queries.
329
338
  *
@@ -333,7 +342,10 @@ type FtsQueryBuildResult =
333
342
  * - negation with at least one positive term
334
343
  * - hyphenated compounds handled intentionally
335
344
  */
336
- function buildFts5Query(query: string): FtsQueryBuildResult {
345
+ function buildFts5Query(
346
+ query: string,
347
+ options: { anyTerm?: boolean } = {}
348
+ ): FtsQueryBuildResult {
337
349
  const trimmed = query.trim();
338
350
  if (!trimmed) {
339
351
  return { ok: false, error: "Search query cannot be empty" };
@@ -454,7 +466,9 @@ function buildFts5Query(query: string): FtsQueryBuildResult {
454
466
  };
455
467
  }
456
468
 
457
- let ftsQuery = positive.join(" AND ");
469
+ let ftsQuery = options.anyTerm
470
+ ? `(${positive.join(" OR ")})`
471
+ : positive.join(" AND ");
458
472
  for (const negation of negative) {
459
473
  ftsQuery = `${ftsQuery} NOT ${negation}`;
460
474
  }
@@ -2635,7 +2649,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
2635
2649
  try {
2636
2650
  const db = this.ensureOpen();
2637
2651
  const limit = options.limit ?? 20;
2638
- const builtQuery = buildFts5Query(query);
2652
+ const builtQuery = buildFts5Query(query, { anyTerm: options.anyTerm });
2639
2653
  if (!builtQuery.ok) {
2640
2654
  return err("INVALID_INPUT", builtQuery.error);
2641
2655
  }
@@ -2683,6 +2697,21 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
2683
2697
  params.push(`%${options.author.toLowerCase()}%`);
2684
2698
  }
2685
2699
 
2700
+ // Scope and supersession filters run inside the candidate subquery so
2701
+ // they narrow the corpus before the FTS LIMIT (never a post-filter).
2702
+ const innerConditions: string[] = [];
2703
+ const innerParams: string[] = [];
2704
+ if (options.memoryScopesAny && options.memoryScopesAny.length > 0) {
2705
+ const placeholders = options.memoryScopesAny.map(() => "?").join(",");
2706
+ innerConditions.push(
2707
+ `AND EXISTS (SELECT 1 FROM doc_memory_scopes ms WHERE ms.document_id = documents.id AND ms.scope IN (${placeholders}))`
2708
+ );
2709
+ innerParams.push(...options.memoryScopesAny);
2710
+ }
2711
+ if (options.excludeSuperseded) {
2712
+ innerConditions.push(SUPERSEDED_EXCLUSION_SQL("documents.id"));
2713
+ }
2714
+
2686
2715
  const hasOuterFilters = tagConditions.length > 0;
2687
2716
  const ftsLimit = hasOuterFilters ? limit * 10 : limit;
2688
2717
  params.push(limit);
@@ -2711,6 +2740,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
2711
2740
  ? "AND (COALESCE(NULLIF(record_source_path, ''), rel_path) = ? OR substr(COALESCE(NULLIF(record_source_path, ''), rel_path), 1, length(?) + 1) = ? || '/')"
2712
2741
  : ""
2713
2742
  }
2743
+ ${innerConditions.join("\n ")}
2714
2744
  )
2715
2745
  ORDER BY score
2716
2746
  LIMIT ?
@@ -2789,6 +2819,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
2789
2819
  options.relPathPrefix,
2790
2820
  ]
2791
2821
  : []),
2822
+ ...innerParams,
2792
2823
  ftsLimit,
2793
2824
  ...params,
2794
2825
  ];
@@ -3076,6 +3107,99 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
3076
3107
  }
3077
3108
  }
3078
3109
 
3110
+ async setDocMemoryScopes(
3111
+ documentId: number,
3112
+ scopes: string[]
3113
+ ): Promise<StoreResult<void>> {
3114
+ try {
3115
+ const db = this.ensureOpen();
3116
+ const transaction = db.transaction(() => {
3117
+ db.run("DELETE FROM doc_memory_scopes WHERE document_id = ?", [
3118
+ documentId,
3119
+ ]);
3120
+ if (scopes.length > 0) {
3121
+ const stmt = db.prepare(
3122
+ "INSERT OR IGNORE INTO doc_memory_scopes (document_id, scope) VALUES (?, ?)"
3123
+ );
3124
+ for (const scope of scopes) {
3125
+ stmt.run(documentId, scope);
3126
+ }
3127
+ }
3128
+ });
3129
+ transaction();
3130
+ return ok(undefined);
3131
+ } catch (cause) {
3132
+ return err(
3133
+ "QUERY_FAILED",
3134
+ cause instanceof Error ? cause.message : "Failed to set memory scopes",
3135
+ cause
3136
+ );
3137
+ }
3138
+ }
3139
+
3140
+ async getDocMemoryScopes(documentId: number): Promise<StoreResult<string[]>> {
3141
+ try {
3142
+ const db = this.ensureOpen();
3143
+ const rows = db
3144
+ .query<{ scope: string }, [number]>(
3145
+ "SELECT scope FROM doc_memory_scopes WHERE document_id = ? ORDER BY scope"
3146
+ )
3147
+ .all(documentId);
3148
+ return ok(rows.map((row) => row.scope));
3149
+ } catch (cause) {
3150
+ return err(
3151
+ "QUERY_FAILED",
3152
+ cause instanceof Error ? cause.message : "Failed to read memory scopes",
3153
+ cause
3154
+ );
3155
+ }
3156
+ }
3157
+
3158
+ async listMemoryEligibleDocuments(
3159
+ options: MemoryEligibleDocumentsOptions
3160
+ ): Promise<StoreResult<MemoryEligibleDocument[]>> {
3161
+ try {
3162
+ const db = this.ensureOpen();
3163
+ if (options.scopes.length === 0) {
3164
+ return ok([]);
3165
+ }
3166
+ const placeholders = options.scopes.map(() => "?").join(",");
3167
+ const rows = db
3168
+ .query<
3169
+ { id: number; docid: string; uri: string; mirror_hash: string },
3170
+ string[]
3171
+ >(
3172
+ `
3173
+ SELECT d.id, d.docid, d.uri, d.mirror_hash
3174
+ FROM documents d
3175
+ WHERE d.active = 1
3176
+ AND d.collection = ?
3177
+ AND d.mirror_hash IS NOT NULL
3178
+ AND EXISTS (SELECT 1 FROM doc_memory_scopes ms WHERE ms.document_id = d.id AND ms.scope IN (${placeholders}))
3179
+ ${options.excludeSuperseded ? SUPERSEDED_EXCLUSION_SQL("d.id") : ""}
3180
+ ORDER BY d.id
3181
+ `
3182
+ )
3183
+ .all(options.collection, ...options.scopes);
3184
+ return ok(
3185
+ rows.map((row) => ({
3186
+ id: row.id,
3187
+ docid: row.docid,
3188
+ uri: row.uri,
3189
+ mirrorHash: row.mirror_hash,
3190
+ }))
3191
+ );
3192
+ } catch (cause) {
3193
+ return err(
3194
+ "QUERY_FAILED",
3195
+ cause instanceof Error
3196
+ ? cause.message
3197
+ : "Failed to list memory-eligible documents",
3198
+ cause
3199
+ );
3200
+ }
3201
+ }
3202
+
3079
3203
  /**
3080
3204
  * Get all tags for a document.
3081
3205
  */
@@ -643,6 +643,35 @@ export interface FtsSearchOptions {
643
643
  categories?: string[];
644
644
  /** Filter by author field (case-insensitive contains) */
645
645
  author?: string;
646
+ /**
647
+ * Managed-memory scope filter: keep only documents carrying at least one of
648
+ * these normalized scopes. Applied inside the FTS candidate subquery, before
649
+ * any LIMIT, so an out-of-scope document never occupies the window.
650
+ */
651
+ memoryScopesAny?: string[];
652
+ /**
653
+ * Exclude documents that an active document supersedes (typed edge
654
+ * `supersedes` pointing at them). Applied inside the candidate subquery.
655
+ */
656
+ excludeSuperseded?: boolean;
657
+ /** Match documents containing ANY positive term instead of ALL of them. */
658
+ anyTerm?: boolean;
659
+ }
660
+
661
+ /** Managed-memory eligibility query (unbounded, executed in one SQL query). */
662
+ export interface MemoryEligibleDocumentsOptions {
663
+ collection: string;
664
+ /** Normalized scopes; any-intersection semantics. */
665
+ scopes: string[];
666
+ excludeSuperseded?: boolean;
667
+ }
668
+
669
+ /** Minimal identity of a memory record eligible for managed recall. */
670
+ export interface MemoryEligibleDocument {
671
+ id: number;
672
+ docid: string;
673
+ uri: string;
674
+ mirrorHash: string;
646
675
  }
647
676
 
648
677
  /** Single FTS search result */
@@ -2022,6 +2051,31 @@ export interface StorePort {
2022
2051
  */
2023
2052
  getTagsForDoc(documentId: number): Promise<StoreResult<TagRow[]>>;
2024
2053
 
2054
+ // ─────────────────────────────────────────────────────────────────────────
2055
+ // Memory scopes (managed memory records)
2056
+ // ─────────────────────────────────────────────────────────────────────────
2057
+
2058
+ /**
2059
+ * Replace the indexed scope set of one document. An empty list clears it,
2060
+ * which removes the document from managed recall.
2061
+ */
2062
+ setDocMemoryScopes(
2063
+ documentId: number,
2064
+ scopes: string[]
2065
+ ): Promise<StoreResult<void>>;
2066
+
2067
+ /** Indexed scopes for one document (sorted). */
2068
+ getDocMemoryScopes(documentId: number): Promise<StoreResult<string[]>>;
2069
+
2070
+ /**
2071
+ * Every active document in the collection carrying at least one requested
2072
+ * scope, optionally excluding superseded records. One unbounded query, so
2073
+ * the result is the exact eligible set rather than a candidate window.
2074
+ */
2075
+ listMemoryEligibleDocuments(
2076
+ options: MemoryEligibleDocumentsOptions
2077
+ ): Promise<StoreResult<MemoryEligibleDocument[]>>;
2078
+
2025
2079
  /**
2026
2080
  * Get tags for multiple documents in a single query.
2027
2081
  * Returns a map of documentId -> TagRow[].
@@ -1 +0,0 @@
1
- 1d6a17618bddf4425527e94961459fa2a414d06f5123d43ffebadfeca8a3a89b gno-browser-clipper-v1.40.0.zip