@gmickel/gno 1.40.0 → 1.41.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 (46) hide show
  1. package/README.md +1 -0
  2. package/assets/skill/SKILL.md +17 -0
  3. package/assets/skill/cli-reference.md +48 -0
  4. package/assets/skill/mcp-reference.md +24 -0
  5. package/browser-extension/artifacts/{gno-browser-clipper-v1.40.0.zip → gno-browser-clipper-v1.41.0.zip} +0 -0
  6. package/browser-extension/artifacts/gno-browser-clipper-v1.41.0.zip.sha256 +1 -0
  7. package/browser-extension/dist/manifest.json +1 -1
  8. package/package.json +1 -1
  9. package/spec/cli.md +146 -7
  10. package/spec/db/schema.sql +17 -0
  11. package/spec/mcp.md +194 -0
  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/memory.ts +491 -0
  16. package/src/cli/commands/status.ts +23 -4
  17. package/src/cli/options.ts +4 -0
  18. package/src/cli/program.ts +127 -0
  19. package/src/config/types.ts +7 -0
  20. package/src/core/audit-provenance.ts +91 -0
  21. package/src/core/audit-workspace.ts +17 -0
  22. package/src/core/memory-diagnostics.ts +144 -0
  23. package/src/core/memory-fence.ts +239 -0
  24. package/src/core/memory-recall.ts +269 -0
  25. package/src/core/memory-record.ts +435 -0
  26. package/src/core/memory-remember.ts +425 -0
  27. package/src/core/memory-types.ts +211 -0
  28. package/src/core/memory.ts +87 -0
  29. package/src/ingestion/sync.ts +17 -0
  30. package/src/mcp/http-egress.ts +2 -0
  31. package/src/mcp/tools/index.ts +43 -0
  32. package/src/mcp/tools/memory-recall.ts +122 -0
  33. package/src/mcp/tools/memory-remember.ts +177 -0
  34. package/src/mcp/tools/memory-shared.ts +80 -0
  35. package/src/pipeline/search.ts +2 -0
  36. package/src/pipeline/types.ts +8 -0
  37. package/src/sdk/client.ts +94 -1
  38. package/src/sdk/index.ts +13 -0
  39. package/src/sdk/types.ts +28 -0
  40. package/src/serve/routes/api.ts +167 -0
  41. package/src/serve/server.ts +26 -0
  42. package/src/store/migrations/027-memory-scopes.ts +37 -0
  43. package/src/store/migrations/index.ts +2 -0
  44. package/src/store/sqlite/adapter.ts +127 -3
  45. package/src/store/types.ts +54 -0
  46. package/browser-extension/artifacts/gno-browser-clipper-v1.40.0.zip.sha256 +0 -1
@@ -354,6 +354,7 @@ export function createProgram(): Command {
354
354
  wireSearchCommands(program);
355
355
  wireOnboardingCommands(program);
356
356
  wireCaptureCommand(program);
357
+ wireMemoryCommands(program);
357
358
  wireManagementCommands(program);
358
359
  wireTraceCommands(program);
359
360
  wirePublishCommand(program);
@@ -1888,6 +1889,132 @@ function wireCaptureCommand(program: Command): void {
1888
1889
  );
1889
1890
  }
1890
1891
 
1892
+ // ─────────────────────────────────────────────────────────────────────────────
1893
+ // Memory Commands (remember, recall)
1894
+ // ─────────────────────────────────────────────────────────────────────────────
1895
+
1896
+ function wireMemoryCommands(program: Command): void {
1897
+ program
1898
+ .command("remember <text>")
1899
+ .description("Store a fact in a memory-managed collection")
1900
+ .option("-c, --collection <name>", "memory-managed target collection")
1901
+ .option(
1902
+ "--scope <scope>",
1903
+ "explicit scope (repeatable, required; no implicit global scope)",
1904
+ collectRepeatableValue,
1905
+ []
1906
+ )
1907
+ .option("--decision <decision>", "add or supersede (omit for candidates)")
1908
+ .option("--add", "shorthand for --decision add")
1909
+ .option(
1910
+ "--supersede <uri>",
1911
+ "shorthand for --decision supersede --predecessor <uri>"
1912
+ )
1913
+ .option("--predecessor <uri>", "predecessor gno:// URI for supersede")
1914
+ .option(
1915
+ "--predecessor-hash <hash>",
1916
+ "predecessor contentHash from recall (required for supersede)"
1917
+ )
1918
+ .option(
1919
+ "--receipt <path>",
1920
+ "recall receipt JSON to fence against (recall --json output)"
1921
+ )
1922
+ .option(
1923
+ "--derived-from <uri>",
1924
+ "declared origin of the fact (repeatable; gno:// origins are fenced)",
1925
+ collectRepeatableValue,
1926
+ []
1927
+ )
1928
+ .option("--source <text>", "free-form source evidence")
1929
+ .option(
1930
+ "--caller <id>",
1931
+ "caller identity (default: $GNO_MEMORY_CALLER or cli:<user>)"
1932
+ )
1933
+ .option(
1934
+ "--session <id>",
1935
+ "session identity (default: $GNO_MEMORY_SESSION or ppid:<pid>)"
1936
+ )
1937
+ .option("--json", "JSON output")
1938
+ .action(async (text: string, cmdOpts: Record<string, unknown>) => {
1939
+ const format = getFormat(cmdOpts);
1940
+ const globals = getGlobals();
1941
+ const { formatRememberResult, remember } =
1942
+ await import("./commands/memory");
1943
+ const result = await remember({
1944
+ configPath: globals.config,
1945
+ indexName: globals.index,
1946
+ text,
1947
+ collection: cmdOpts.collection as string | undefined,
1948
+ scopes: cmdOpts.scope as string[],
1949
+ decision: cmdOpts.decision as string | undefined,
1950
+ add: Boolean(cmdOpts.add),
1951
+ supersede: cmdOpts.supersede as string | undefined,
1952
+ predecessor: cmdOpts.predecessor as string | undefined,
1953
+ predecessorHash: cmdOpts.predecessorHash as string | undefined,
1954
+ receipt: cmdOpts.receipt as string | undefined,
1955
+ derivedFrom: cmdOpts.derivedFrom as string[],
1956
+ source: cmdOpts.source as string | undefined,
1957
+ caller: cmdOpts.caller as string | undefined,
1958
+ session: cmdOpts.session as string | undefined,
1959
+ });
1960
+ await writeOutput(
1961
+ formatRememberResult(result, {
1962
+ json: format === "json",
1963
+ quiet: globals.quiet,
1964
+ }),
1965
+ format
1966
+ );
1967
+ });
1968
+
1969
+ program
1970
+ .command("recall <query>")
1971
+ .description("Recall current facts from a memory-managed collection")
1972
+ .option("-c, --collection <name>", "memory-managed collection")
1973
+ .option(
1974
+ "--scope <scope>",
1975
+ "explicit scope (repeatable, required; no implicit global scope)",
1976
+ collectRepeatableValue,
1977
+ []
1978
+ )
1979
+ .option("--max-facts <n>", "budget override: max facts (default 8)")
1980
+ .option(
1981
+ "--max-tokens <n>",
1982
+ "budget override: max payload tokens (default 512)"
1983
+ )
1984
+ .option(
1985
+ "--caller <id>",
1986
+ "caller identity (default: $GNO_MEMORY_CALLER or cli:<user>)"
1987
+ )
1988
+ .option(
1989
+ "--session <id>",
1990
+ "session identity (default: $GNO_MEMORY_SESSION or ppid:<pid>)"
1991
+ )
1992
+ .option("--json", "JSON output")
1993
+ .action(async (query: string, cmdOpts: Record<string, unknown>) => {
1994
+ const format = getFormat(cmdOpts);
1995
+ const globals = getGlobals();
1996
+ const { formatRecallResult, recall } = await import("./commands/memory");
1997
+ const result = await recall({
1998
+ configPath: globals.config,
1999
+ indexName: globals.index,
2000
+ query,
2001
+ collection: cmdOpts.collection as string | undefined,
2002
+ scopes: cmdOpts.scope as string[],
2003
+ maxFacts: cmdOpts.maxFacts as number | undefined,
2004
+ maxTokens: cmdOpts.maxTokens as number | undefined,
2005
+ caller: cmdOpts.caller as string | undefined,
2006
+ session: cmdOpts.session as string | undefined,
2007
+ });
2008
+ await writeOutput(
2009
+ formatRecallResult(result, {
2010
+ json: format === "json",
2011
+ quiet: globals.quiet,
2012
+ }),
2013
+ format
2014
+ );
2015
+ });
2016
+ }
2017
+
1891
2018
  // ─────────────────────────────────────────────────────────────────────────────
1892
2019
  // Retrieval Commands (get, multi-get, ls)
1893
2020
  // ─────────────────────────────────────────────────────────────────────────────
@@ -163,6 +163,13 @@ export const CollectionSchema = z.object({
163
163
  */
164
164
  sourceAvailability: SourceAvailabilitySchema.optional(),
165
165
 
166
+ /**
167
+ * Declares the collection as a GNO-managed memory substrate: `remember`
168
+ * writes fact files here and refuses every collection without the flag.
169
+ * Omitted means false; ordinary retrieval is unaffected either way.
170
+ */
171
+ memoryManaged: z.boolean().optional(),
172
+
166
173
  /** Optional per-collection model overrides */
167
174
  models: z
168
175
  .object({
@@ -5,6 +5,7 @@ import type { CaptureSource } from "./capture";
5
5
 
6
6
  import { compareAuditFindingDrafts } from "./audit";
7
7
  import { validateDeclaredCaptureProvenance } from "./capture";
8
+ import { diagnoseMemoryContent } from "./memory-diagnostics";
8
9
  import {
9
10
  hasDeclaredRecordProvenance,
10
11
  validateDeclaredRecordProvenance,
@@ -20,6 +21,11 @@ export interface AuditProvenanceDocument {
20
21
  captureSourceSupported?: boolean;
21
22
  captureSource?: Partial<CaptureSource>;
22
23
  captureSourceDeclared: boolean;
24
+ /**
25
+ * Present only for documents in a memory-managed collection: the file
26
+ * content the memory-record validator runs against (null when unreadable).
27
+ */
28
+ memory?: { content: string | null };
23
29
  record: {
24
30
  recordKey?: string | null;
25
31
  recordSourceLocator?: string | null;
@@ -52,6 +58,90 @@ const issueFinding = (input: {
52
58
  ],
53
59
  });
54
60
 
61
+ const memoryFinding = (
62
+ document: AuditProvenanceDocument,
63
+ code: string,
64
+ message: string
65
+ ): AuditFindingDraft => ({
66
+ subject: document.uri,
67
+ location: code,
68
+ severity: "warning",
69
+ message: `memory record excluded from recall: ${message}`,
70
+ evidence: [
71
+ {
72
+ kind: "memory-record-contract",
73
+ summary: code,
74
+ uri: document.uri,
75
+ path: document.relPath,
76
+ },
77
+ ],
78
+ guidance: [
79
+ "Repair the memory frontmatter (memory.recordId/scopes/caller/session/createdAt/contentHash) or re-create the fact with gno remember",
80
+ ],
81
+ });
82
+
83
+ /**
84
+ * Managed memory files are audited against the memory-record contract; each
85
+ * malformed file yields one finding per diagnostic code. Ordinary retrieval
86
+ * still sees such files; managed recall does not.
87
+ */
88
+ export const evaluateMemoryRecordAudit = (
89
+ documents: readonly AuditProvenanceDocument[],
90
+ options: { truncated?: boolean } = {}
91
+ ): AuditRuleContribution => {
92
+ const findings: AuditFindingDraft[] = [];
93
+ let managedDocuments = 0;
94
+ let unreadable = 0;
95
+ for (const document of documents) {
96
+ if (document.memory === undefined) continue;
97
+ managedDocuments += 1;
98
+ if (document.memory.content === null) {
99
+ unreadable += 1;
100
+ continue;
101
+ }
102
+ const diagnostics = diagnoseMemoryContent(document.memory.content) ?? [];
103
+ for (const diagnostic of diagnostics) {
104
+ findings.push(
105
+ memoryFinding(document, diagnostic.code, diagnostic.message)
106
+ );
107
+ }
108
+ }
109
+ findings.sort(compareAuditFindingDrafts);
110
+ const truncated = options.truncated === true;
111
+ return {
112
+ ruleId: "provenance.memory-record",
113
+ category: "provenance",
114
+ status: truncated
115
+ ? "inconclusive"
116
+ : unreadable > 0
117
+ ? "unavailable"
118
+ : findings.length > 0
119
+ ? "fail"
120
+ : managedDocuments === 0
121
+ ? "skip"
122
+ : "pass",
123
+ message: truncated
124
+ ? "Audit selection was truncated; memory record contract not fully evaluated"
125
+ : unreadable > 0
126
+ ? `${unreadable} memory-managed source(s) could not be read`
127
+ : findings.length > 0
128
+ ? `${findings.length} memory record contract violation(s) across ${managedDocuments} managed document(s)`
129
+ : managedDocuments === 0
130
+ ? "No memory-managed collections in scope"
131
+ : `${managedDocuments} managed memory record(s) satisfy the contract`,
132
+ findings: findings.slice(0, PROVENANCE_AUDIT_MAX_FINDINGS_PER_RULE),
133
+ findingCount: findings.length,
134
+ examinedCount: managedDocuments,
135
+ skipReason: truncated
136
+ ? "snapshot_truncated"
137
+ : unreadable > 0
138
+ ? "source_unavailable"
139
+ : managedDocuments === 0
140
+ ? "no_memory_managed_collections"
141
+ : null,
142
+ };
143
+ };
144
+
55
145
  /** Missing provenance is completeness evidence, never a truth judgment. */
56
146
  export const evaluateProvenanceAudit = (
57
147
  documents: readonly AuditProvenanceDocument[],
@@ -150,5 +240,6 @@ export const evaluateProvenanceAudit = (
150
240
  recordFindings,
151
241
  declaredRecordDocuments
152
242
  ),
243
+ evaluateMemoryRecordAudit(documents, options),
153
244
  ];
154
245
  };
@@ -189,6 +189,7 @@ export const groupAuditPhysicalSources = (
189
189
  const observeDocument = async (
190
190
  document: DocumentRow,
191
191
  roots: ReadonlyMap<string, string>,
192
+ managedCollections: ReadonlySet<string>,
192
193
  readFrontmatter: boolean,
193
194
  inspectFreshness: boolean,
194
195
  signal?: AbortSignal
@@ -196,6 +197,8 @@ const observeDocument = async (
196
197
  const markdownSource = MARKDOWN_SOURCE_EXTENSIONS.has(
197
198
  document.sourceExt.toLowerCase()
198
199
  );
200
+ const memoryManaged = managedCollections.has(document.collection);
201
+ const memoryUnreadable = memoryManaged ? { content: null } : undefined;
199
202
  const root = roots.get(document.collection);
200
203
  if (!root) {
201
204
  return {
@@ -206,6 +209,7 @@ const observeDocument = async (
206
209
  sourceState: "unreadable",
207
210
  captureSourceSupported: markdownSource,
208
211
  captureSourceDeclared: false,
212
+ memory: memoryUnreadable,
209
213
  record: document,
210
214
  },
211
215
  freshness: {
@@ -238,6 +242,7 @@ const observeDocument = async (
238
242
  sourceState: "missing",
239
243
  captureSourceSupported: markdownSource,
240
244
  captureSourceDeclared: false,
245
+ memory: memoryUnreadable,
241
246
  record: document,
242
247
  },
243
248
  freshness: {
@@ -286,6 +291,11 @@ const observeDocument = async (
286
291
  captureSource: incompleteFrontmatter
287
292
  ? undefined
288
293
  : extractCaptureSourceFromFrontmatter(frontmatter),
294
+ // Memory facts are small single files, so the bounded frontmatter
295
+ // read covers the whole record the validator needs.
296
+ memory: memoryManaged
297
+ ? { content: incompleteFrontmatter ? null : frontmatter }
298
+ : undefined,
289
299
  record: document,
290
300
  },
291
301
  freshness: {
@@ -316,6 +326,7 @@ const observeDocument = async (
316
326
  sourceState: "unreadable",
317
327
  captureSourceSupported: markdownSource,
318
328
  captureSourceDeclared: false,
329
+ memory: memoryUnreadable,
319
330
  record: document,
320
331
  },
321
332
  freshness: {
@@ -382,11 +393,17 @@ const loadWorkspaceSnapshot = async (
382
393
  const needsSourceObservation =
383
394
  options.categories.includes("provenance") ||
384
395
  options.categories.includes("freshness");
396
+ const managedCollections = new Set(
397
+ options.collections
398
+ .filter((collection) => collection.memoryManaged === true)
399
+ .map((collection) => collection.name)
400
+ );
385
401
  const observed = needsSourceObservation
386
402
  ? await mapConcurrent(selected.documents, (document) =>
387
403
  observeDocument(
388
404
  document,
389
405
  roots,
406
+ managedCollections,
390
407
  options.categories.includes("provenance"),
391
408
  options.categories.includes("freshness"),
392
409
  options.signal
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Malformed-memory diagnostics projected through status and audit.
3
+ *
4
+ * A memory-managed collection may contain hand-edited files that no longer
5
+ * satisfy the record contract. Ingestion drops their scope rows (so managed
6
+ * recall never returns them) while ordinary retrieval still sees the file.
7
+ * This module names those files and their diagnostic codes.
8
+ *
9
+ * @module src/core/memory-diagnostics
10
+ */
11
+
12
+ import type { Collection } from "../config/types";
13
+ import type { StorePort } from "../store/types";
14
+ import type { MemoryDiagnostic } from "./memory-record";
15
+
16
+ import { validateMemoryRecord } from "./memory-record";
17
+
18
+ /** Bounded list of malformed files per collection in status output. */
19
+ export const MEMORY_STATUS_MAX_MALFORMED = 20;
20
+
21
+ export interface MalformedMemoryRecord {
22
+ uri: string;
23
+ relPath: string;
24
+ codes: string[];
25
+ diagnostics: MemoryDiagnostic[];
26
+ }
27
+
28
+ export interface MemoryCollectionStatus {
29
+ collection: string;
30
+ /** Active documents indexed in the collection. */
31
+ documents: number;
32
+ /** Documents that satisfy the record contract (recallable). */
33
+ records: number;
34
+ malformed: number;
35
+ malformedRecords: MalformedMemoryRecord[];
36
+ /** True when more malformed files exist than were listed. */
37
+ truncated: boolean;
38
+ }
39
+
40
+ export interface MemoryStatus {
41
+ managedCollections: number;
42
+ records: number;
43
+ malformed: number;
44
+ collections: MemoryCollectionStatus[];
45
+ }
46
+
47
+ export const memoryManagedCollections = (
48
+ collections: readonly Collection[]
49
+ ): Collection[] =>
50
+ collections.filter((collection) => collection.memoryManaged === true);
51
+
52
+ /** Validate one indexed file as a memory record (null when unreadable). */
53
+ export const diagnoseMemoryContent = (
54
+ content: string
55
+ ): MemoryDiagnostic[] | null => {
56
+ const validation = validateMemoryRecord(content);
57
+ return validation.ok ? [] : validation.diagnostics;
58
+ };
59
+
60
+ /**
61
+ * Scan every active document of each memory-managed collection through the
62
+ * validator. Read-only; bounded by the collection's own document count.
63
+ */
64
+ export async function buildMemoryStatus(
65
+ store: Pick<StorePort, "listDocuments" | "getContent">,
66
+ collections: readonly Collection[],
67
+ options: { maxListed?: number } = {}
68
+ ): Promise<MemoryStatus> {
69
+ const maxListed = options.maxListed ?? MEMORY_STATUS_MAX_MALFORMED;
70
+ const statuses: MemoryCollectionStatus[] = [];
71
+ for (const collection of memoryManagedCollections(collections)) {
72
+ const listed = await store.listDocuments(collection.name);
73
+ const documents = listed.ok
74
+ ? listed.value.filter((document) => document.active)
75
+ : [];
76
+ let records = 0;
77
+ const malformedRecords: MalformedMemoryRecord[] = [];
78
+ let malformed = 0;
79
+ for (const document of documents) {
80
+ if (!document.mirrorHash) continue;
81
+ const content = await store.getContent(document.mirrorHash);
82
+ if (!content.ok || content.value === null) continue;
83
+ const diagnostics = diagnoseMemoryContent(content.value);
84
+ if (diagnostics === null) continue;
85
+ if (diagnostics.length === 0) {
86
+ records += 1;
87
+ continue;
88
+ }
89
+ malformed += 1;
90
+ if (malformedRecords.length < maxListed) {
91
+ malformedRecords.push({
92
+ uri: document.uri,
93
+ relPath: document.relPath,
94
+ codes: [...new Set(diagnostics.map((item) => item.code))],
95
+ diagnostics,
96
+ });
97
+ }
98
+ }
99
+ malformedRecords.sort((left, right) =>
100
+ left.uri < right.uri ? -1 : left.uri > right.uri ? 1 : 0
101
+ );
102
+ statuses.push({
103
+ collection: collection.name,
104
+ documents: documents.length,
105
+ records,
106
+ malformed,
107
+ malformedRecords,
108
+ truncated: malformed > malformedRecords.length,
109
+ });
110
+ }
111
+ return {
112
+ managedCollections: statuses.length,
113
+ records: statuses.reduce((sum, item) => sum + item.records, 0),
114
+ malformed: statuses.reduce((sum, item) => sum + item.malformed, 0),
115
+ collections: statuses,
116
+ };
117
+ }
118
+
119
+ /** Terminal lines for the `gno status` memory section. */
120
+ export function formatMemoryStatusLines(status: MemoryStatus): string[] {
121
+ if (status.managedCollections === 0) {
122
+ return ["Memory: no memory-managed collections"];
123
+ }
124
+ const lines = [
125
+ `Memory: ${status.records} records, ${status.malformed} malformed across ${status.managedCollections} managed collection${status.managedCollections === 1 ? "" : "s"}`,
126
+ ];
127
+ for (const collection of status.collections) {
128
+ lines.push(
129
+ ` ${collection.collection}: ${collection.records} records` +
130
+ (collection.malformed > 0
131
+ ? `, ${collection.malformed} malformed (excluded from recall)`
132
+ : "")
133
+ );
134
+ for (const record of collection.malformedRecords) {
135
+ lines.push(` ${record.relPath}: ${record.codes.join(", ")}`);
136
+ }
137
+ if (collection.truncated) {
138
+ lines.push(
139
+ ` ... ${collection.malformed - collection.malformedRecords.length} more (run: gno audit --category provenance)`
140
+ );
141
+ }
142
+ }
143
+ return lines;
144
+ }