@gmickel/gno 2.5.1 → 2.7.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 (160) hide show
  1. package/README.md +60 -5
  2. package/assets/skill/README.md +5 -1
  3. package/assets/skill/SKILL.md +80 -4
  4. package/assets/skill/cli-reference.md +132 -2
  5. package/assets/skill/examples.md +30 -0
  6. package/assets/skill/mcp-reference.md +54 -1
  7. package/assets/skill/recipes/capture-and-file.md +6 -0
  8. package/assets/skill/recipes/memory-file-decision.md +6 -0
  9. package/assets/skill/recipes/memory-supersede-fact.md +5 -0
  10. package/assets/skill/recipes/session-evidence-lookup.md +98 -0
  11. package/assets/spa-production.json.gz +0 -0
  12. package/browser-extension/artifacts/{gno-browser-clipper-v2.5.1.zip → gno-browser-clipper-v2.7.0.zip} +0 -0
  13. package/browser-extension/artifacts/gno-browser-clipper-v2.7.0.zip.sha256 +1 -0
  14. package/browser-extension/dist/manifest.json +1 -1
  15. package/package.json +2 -1
  16. package/spec/cli.md +449 -35
  17. package/spec/mcp.md +234 -4
  18. package/spec/output-schemas/ask.schema.json +1 -1
  19. package/spec/output-schemas/capture-receipt.schema.json +4 -1
  20. package/spec/output-schemas/doctor.schema.json +88 -0
  21. package/spec/output-schemas/error.schema.json +11 -2
  22. package/spec/output-schemas/get.schema.json +1 -1
  23. package/spec/output-schemas/mcp-capture-result.schema.json +4 -2
  24. package/spec/output-schemas/memory-remember.schema.json +10 -4
  25. package/spec/output-schemas/multi-get.schema.json +4 -1
  26. package/spec/output-schemas/peek.schema.json +2 -9
  27. package/spec/output-schemas/request-status.schema.json +113 -0
  28. package/spec/output-schemas/resident-status.schema.json +22 -0
  29. package/spec/output-schemas/search-result.schema.json +1 -1
  30. package/spec/output-schemas/search-results.schema.json +1 -1
  31. package/spec/output-schemas/sessions-automation-run.schema.json +46 -0
  32. package/spec/output-schemas/sessions-discovery.schema.json +38 -0
  33. package/spec/output-schemas/sessions-import-receipt.schema.json +156 -0
  34. package/spec/output-schemas/sessions-status.schema.json +432 -0
  35. package/spec/output-schemas/status.schema.json +98 -0
  36. package/src/cli/commands/ask.ts +14 -2
  37. package/src/cli/commands/capture.ts +55 -96
  38. package/src/cli/commands/daemon.ts +41 -0
  39. package/src/cli/commands/doctor.ts +54 -20
  40. package/src/cli/commands/embed.ts +41 -3
  41. package/src/cli/commands/ls.ts +3 -0
  42. package/src/cli/commands/memory.ts +12 -3
  43. package/src/cli/commands/query.ts +5 -0
  44. package/src/cli/commands/request-status.ts +59 -0
  45. package/src/cli/commands/reset.ts +39 -5
  46. package/src/cli/commands/sessions.ts +713 -0
  47. package/src/cli/commands/shared.ts +14 -1
  48. package/src/cli/commands/status.ts +63 -5
  49. package/src/cli/commands/vec.ts +54 -0
  50. package/src/cli/detach.ts +29 -1
  51. package/src/cli/errors.ts +13 -9
  52. package/src/cli/program.ts +441 -2
  53. package/src/cli/session-binding.ts +49 -0
  54. package/src/config/types.ts +8 -0
  55. package/src/core/capture-publish.ts +239 -0
  56. package/src/core/capture-sync.ts +12 -2
  57. package/src/core/host-paths.ts +31 -0
  58. package/src/core/memory-remember.ts +234 -122
  59. package/src/core/memory-types.ts +11 -0
  60. package/src/core/network-boundary-inventory.ts +8 -0
  61. package/src/core/request-receipts.ts +671 -0
  62. package/src/core/shutdown-budget.ts +6 -0
  63. package/src/core/vector-partition-status.ts +52 -0
  64. package/src/embed/backlog.ts +124 -18
  65. package/src/embed/fingerprint.ts +6 -3
  66. package/src/embed/retry.ts +66 -27
  67. package/src/embed/variant-backlog.ts +15 -10
  68. package/src/embed/variant-retry.ts +31 -22
  69. package/src/index.ts +30 -2
  70. package/src/llm/native-worker/dispatcher.ts +2 -0
  71. package/src/llm/native-worker/embedding-identity.ts +42 -0
  72. package/src/llm/native-worker/protocol.ts +1 -0
  73. package/src/llm/types.ts +3 -0
  74. package/src/mcp/context.ts +17 -0
  75. package/src/mcp/http-egress.ts +4 -0
  76. package/src/mcp/http-transport.ts +2 -0
  77. package/src/mcp/resources/index.ts +6 -5
  78. package/src/mcp/tool-descriptions-core.ts +1 -1
  79. package/src/mcp/tools/capture.ts +87 -85
  80. package/src/mcp/tools/index.ts +77 -4
  81. package/src/mcp/tools/memory-remember.ts +8 -1
  82. package/src/mcp/tools/memory-shared.ts +7 -1
  83. package/src/mcp/tools/request-status.ts +73 -0
  84. package/src/mcp/tools/sessions.ts +208 -0
  85. package/src/mcp/tools/status.ts +4 -0
  86. package/src/pipeline/hybrid.ts +37 -7
  87. package/src/pipeline/vsearch.ts +14 -2
  88. package/src/sdk/client.ts +180 -84
  89. package/src/sdk/index.ts +6 -0
  90. package/src/sdk/types.ts +54 -2
  91. package/src/serve/capture-service.ts +98 -32
  92. package/src/serve/config-sync.ts +3 -2
  93. package/src/serve/embed-scheduler.ts +133 -19
  94. package/src/serve/host-path-redaction.ts +79 -0
  95. package/src/serve/public/app.tsx +4 -1
  96. package/src/serve/public/components/CaptureModal.tsx +26 -8
  97. package/src/serve/public/components/sessions/AutomationPanel.tsx +800 -0
  98. package/src/serve/public/components/sessions/ImportReceipt.tsx +238 -0
  99. package/src/serve/public/components/sessions/SessionSearch.tsx +286 -0
  100. package/src/serve/public/components/sessions/SourcesPanel.tsx +541 -0
  101. package/src/serve/public/components/sessions/api.ts +40 -0
  102. package/src/serve/public/globals.built.css +1 -1
  103. package/src/serve/public/hooks/use-api.ts +26 -3
  104. package/src/serve/public/lib/request-intent.ts +77 -0
  105. package/src/serve/public/lib/snippet.tsx +52 -0
  106. package/src/serve/public/lib/workspace-actions.ts +12 -1
  107. package/src/serve/public/lib/workspace-tabs.ts +2 -0
  108. package/src/serve/public/pages/Dashboard.tsx +22 -9
  109. package/src/serve/public/pages/DocView.tsx +25 -6
  110. package/src/serve/public/pages/DocumentEditor.tsx +224 -104
  111. package/src/serve/public/pages/Search.tsx +1 -41
  112. package/src/serve/public/pages/Sessions.tsx +350 -0
  113. package/src/serve/resident-runtime.ts +69 -4
  114. package/src/serve/resident-status.ts +13 -1
  115. package/src/serve/routes/api.ts +476 -147
  116. package/src/serve/routes/sessions.ts +766 -0
  117. package/src/serve/security.ts +9 -0
  118. package/src/serve/server.ts +215 -10
  119. package/src/serve/session-automation.ts +146 -0
  120. package/src/serve/status-model.ts +16 -0
  121. package/src/serve/status.ts +2 -0
  122. package/src/serve/watch-reconciliation-shared.ts +3 -0
  123. package/src/serve/watch-service-events.ts +3 -2
  124. package/src/serve/watch-service-run-flush.ts +35 -2
  125. package/src/serve/watch-service.ts +5 -0
  126. package/src/sessions/archive.ts +348 -0
  127. package/src/sessions/automation-state.ts +444 -0
  128. package/src/sessions/automation-status.ts +239 -0
  129. package/src/sessions/automation.ts +1169 -0
  130. package/src/sessions/binding.ts +105 -0
  131. package/src/sessions/claude-hook.ts +240 -0
  132. package/src/sessions/config.ts +176 -0
  133. package/src/sessions/format.ts +191 -0
  134. package/src/sessions/import-child-env.ts +8 -0
  135. package/src/sessions/import-child.ts +152 -0
  136. package/src/sessions/parsers/claude-code.ts +259 -0
  137. package/src/sessions/parsers/codex.ts +303 -0
  138. package/src/sessions/parsers/hermes.ts +248 -0
  139. package/src/sessions/parsers/openclaw.ts +496 -0
  140. package/src/sessions/parsers/shared.ts +184 -0
  141. package/src/sessions/sanitize.ts +222 -0
  142. package/src/sessions/service.ts +1533 -0
  143. package/src/sessions/setup.ts +477 -0
  144. package/src/sessions/sources.ts +518 -0
  145. package/src/sessions/state.ts +118 -0
  146. package/src/sessions/types.ts +457 -0
  147. package/src/store/migrations/031-runtime-independent-vectors.ts +29 -0
  148. package/src/store/migrations/032-vector-runtime-callers.ts +17 -0
  149. package/src/store/migrations/index.ts +4 -0
  150. package/src/store/sqlite/adapter.ts +76 -16
  151. package/src/store/sqlite/scoped-index.ts +9 -0
  152. package/src/store/types.ts +11 -1
  153. package/src/store/vector/lazy.ts +46 -43
  154. package/src/store/vector/runtime-compat.ts +651 -0
  155. package/src/store/vector/sqlite-vec.ts +20 -2
  156. package/src/store/vector/status.ts +276 -35
  157. package/src/store/vector/types.ts +2 -0
  158. package/src/store/vector/variant-search.ts +71 -23
  159. package/src/store/vector/variants.ts +49 -14
  160. package/browser-extension/artifacts/gno-browser-clipper-v2.5.1.zip.sha256 +0 -1
@@ -24,6 +24,7 @@ import type {
24
24
  RememberResult,
25
25
  } from "./memory-types";
26
26
 
27
+ import { buildUri } from "../app/constants";
27
28
  import { defaultSyncService, withContentTypeRules } from "../ingestion";
28
29
  import { withWriteLock } from "./file-lock";
29
30
  import { atomicCreate } from "./file-ops";
@@ -56,6 +57,13 @@ import {
56
57
  MEMORY_SEMANTIC_LIKELY_THRESHOLD,
57
58
  MemoryError,
58
59
  } from "./memory-types";
60
+ import {
61
+ isLeaseBusy,
62
+ RequestReceiptError,
63
+ requestDigest,
64
+ runRequestedWrite,
65
+ validateRequestId,
66
+ } from "./request-receipts";
59
67
 
60
68
  export interface CandidateQuery {
61
69
  text: string;
@@ -249,6 +257,12 @@ async function supersedesEdgeProjected(
249
257
  // remember
250
258
  // ─────────────────────────────────────────────────────────────────────────────
251
259
 
260
+ interface RememberPlan {
261
+ frontmatter: MemoryRecordFrontmatter;
262
+ supersedes: string[];
263
+ relPath: string;
264
+ }
265
+
252
266
  export async function rememberFact(
253
267
  deps: MemoryServiceDeps,
254
268
  rawInput: RememberInput
@@ -270,14 +284,26 @@ export async function rememberFact(
270
284
  );
271
285
  }
272
286
  }
287
+ const requestId =
288
+ rawInput.requestId === undefined
289
+ ? undefined
290
+ : validateRequestId(rawInput.requestId);
291
+ if (requestId !== undefined && decision === undefined) {
292
+ throw new RequestReceiptError(
293
+ "REQUEST_ID_INVALID",
294
+ "requestId applies only to writes: pass decision add or supersede."
295
+ );
296
+ }
273
297
 
274
298
  const { candidates, matching } = await findMemoryCandidates(deps, {
275
299
  text,
276
300
  collection: collection.name,
277
301
  scopes,
278
302
  });
303
+ // With a request ID the exact-duplicate decision is made (and recorded)
304
+ // under the lease, so a later retry replays it instead of re-deciding.
279
305
  const exact = candidates.find((candidate) => candidate.match === "exact");
280
- if (exact && decision !== "supersede") {
306
+ if (exact && decision !== "supersede" && requestId === undefined) {
281
307
  const { similarity: _similarity, match: _match, ...record } = exact;
282
308
  return { outcome: "existing", record, matching };
283
309
  }
@@ -285,135 +311,222 @@ export async function rememberFact(
285
311
  return { outcome: "candidates", candidates, matching };
286
312
  }
287
313
 
288
- const createdAt = memoryNow(deps).toISOString();
289
314
  const source = rawInput.source?.trim() || undefined;
290
- const frontmatter: MemoryRecordFrontmatter = {
291
- recordId: buildMemoryRecordId({
292
- contentHash,
293
- createdAt,
315
+ const absPathOf = (plan: RememberPlan) => join(collection.path, plan.relPath);
316
+
317
+ /** Under the lease: decide against current state; never writes. */
318
+ const prepare = async (): Promise<
319
+ { plan: RememberPlan } | { result: RememberResult }
320
+ > => {
321
+ const supersedes: string[] = [];
322
+ if (decision === "supersede") {
323
+ supersedes.push(
324
+ await verifyPredecessor(
325
+ deps,
326
+ collection.name,
327
+ rawInput.predecessorUri as string,
328
+ rawInput.predecessorHash as string
329
+ )
330
+ );
331
+ } else {
332
+ // The pre-lease check raced with any concurrent writer; decide
333
+ // idempotency on the state visible under the lease.
334
+ const existing = await findExactCurrent(deps, {
335
+ text,
336
+ collection: collection.name,
337
+ scopes,
338
+ contentHash,
339
+ });
340
+ if (existing) {
341
+ return { result: { outcome: "existing", record: existing, matching } };
342
+ }
343
+ }
344
+ const createdAt = memoryNow(deps).toISOString();
345
+ const frontmatter: MemoryRecordFrontmatter = {
346
+ recordId: buildMemoryRecordId({
347
+ contentHash,
348
+ createdAt,
349
+ caller: identity.caller,
350
+ session: identity.session,
351
+ }),
352
+ scopes,
294
353
  caller: identity.caller,
295
354
  session: identity.session,
296
- }),
297
- scopes,
298
- caller: identity.caller,
299
- session: identity.session,
300
- createdAt,
301
- contentHash,
302
- ...(source ? { source } : {}),
355
+ createdAt,
356
+ contentHash,
357
+ ...(source ? { source } : {}),
358
+ };
359
+ return {
360
+ plan: {
361
+ frontmatter,
362
+ supersedes,
363
+ relPath: buildMemoryRecordRelPath(frontmatter),
364
+ },
365
+ };
303
366
  };
304
- const relPath = buildMemoryRecordRelPath(frontmatter);
305
- const absPath = join(collection.path, relPath);
306
- const lockWaitMs = memoryLockWaitMs(deps);
307
-
308
- let leased: RememberResult;
309
- try {
310
- leased = await withWriteLock(
311
- deps.lockPath,
312
- async () => {
313
- const supersedes: string[] = [];
314
- if (decision === "supersede") {
315
- supersedes.push(
316
- await verifyPredecessor(
317
- deps,
318
- collection.name,
319
- rawInput.predecessorUri as string,
320
- rawInput.predecessorHash as string
321
- )
322
- );
323
- } else {
324
- // The pre-lease check raced with any concurrent writer; decide
325
- // idempotency on the state visible under the lease.
326
- const existing = await findExactCurrent(deps, {
327
- text,
328
- collection: collection.name,
329
- scopes,
330
- contentHash,
331
- });
332
- if (existing) {
333
- return { outcome: "existing", record: existing, matching };
367
+ const serialize = (plan: RememberPlan): string =>
368
+ serializeMemoryRecord({
369
+ frontmatter: plan.frontmatter,
370
+ supersedes: plan.supersedes,
371
+ text,
372
+ });
373
+ const publish = async (plan: RememberPlan): Promise<void> => {
374
+ await mkdir(dirname(absPathOf(plan)), { recursive: true });
375
+ await atomicCreate(absPathOf(plan), serialize(plan));
376
+ };
377
+ /** Under the lease: lexical sync, projection check, result. */
378
+ const finish = async (plan: RememberPlan): Promise<RememberResult> => {
379
+ const { relPath, supersedes, frontmatter } = plan;
380
+ const absPath = absPathOf(plan);
381
+ // syncPaths (not syncFiles) so typed-edge projection errors surface.
382
+ const syncResult = await (deps.syncService ?? defaultSyncService).syncPaths(
383
+ collection,
384
+ store,
385
+ [relPath],
386
+ withContentTypeRules({ runUpdateCmd: false, gitPull: false }, config)
387
+ );
388
+ const fileResult = syncResult.files?.[0];
389
+ const doc = await store.getDocument(collection.name, relPath);
390
+ const sync: MemorySyncState =
391
+ fileResult?.status === "error" || !doc.ok || doc.value === null
392
+ ? {
393
+ status: "failed",
394
+ error:
395
+ fileResult?.errorMessage ??
396
+ fileResult?.errorCode ??
397
+ "memory record was written but is not retrievable yet",
334
398
  }
335
- }
336
- await mkdir(dirname(absPath), { recursive: true });
337
- await atomicCreate(
338
- absPath,
339
- serializeMemoryRecord({ frontmatter, supersedes, text })
399
+ : { status: "completed" };
400
+ if (sync.status === "failed") {
401
+ throw new MemoryError(
402
+ "MEMORY_SYNC_FAILED",
403
+ `Memory record written to ${buildUri(collection.name, relPath)} but lexical sync failed: ${sync.error}. Run gno update to retry indexing.`
404
+ );
405
+ }
406
+ const written = (doc as { value: DocumentRow }).value;
407
+ const projectionErrors = syncResult.errors
408
+ .map((error) => `${error.relPath}: ${error.message}`)
409
+ .join("; ");
410
+ if (decision === "supersede") {
411
+ // The write is only a supersession once the edge is projected;
412
+ // until then the predecessor still reads as current.
413
+ const projected =
414
+ projectionErrors.length === 0 &&
415
+ (await supersedesEdgeProjected(deps, written.id, supersedes));
416
+ if (!projected) {
417
+ throw new MemoryError(
418
+ "MEMORY_SUPERSEDE_PROJECTION_FAILED",
419
+ `Successor written to ${buildUri(collection.name, relPath)} but its supersedes edge did not project${projectionErrors ? ` (${projectionErrors})` : ""}; the predecessor still reads as current. Run gno update to retry the projection.`
340
420
  );
341
- // syncPaths (not syncFiles) so typed-edge projection errors surface.
342
- const syncResult = await (
343
- deps.syncService ?? defaultSyncService
344
- ).syncPaths(
345
- collection,
346
- store,
347
- [relPath],
348
- withContentTypeRules({ runUpdateCmd: false, gitPull: false }, config)
421
+ }
422
+ } else if (projectionErrors.length > 0) {
423
+ throw new MemoryError(
424
+ "MEMORY_SYNC_FAILED",
425
+ `Memory record written to ${buildUri(collection.name, relPath)} but typed-edge projection failed: ${projectionErrors}. Run gno update to retry indexing.`
426
+ );
427
+ }
428
+ const record: MemoryFact = {
429
+ uri: written.uri,
430
+ docid: written.docid,
431
+ recordId: frontmatter.recordId,
432
+ text,
433
+ scopes: frontmatter.scopes,
434
+ caller: frontmatter.caller,
435
+ session: frontmatter.session,
436
+ createdAt: frontmatter.createdAt,
437
+ contentHash,
438
+ supersedes,
439
+ ...(frontmatter.source ? { source: frontmatter.source } : {}),
440
+ };
441
+ return {
442
+ outcome: decision === "supersede" ? "superseded" : "added",
443
+ record,
444
+ absPath,
445
+ sync,
446
+ matching,
447
+ };
448
+ };
449
+
450
+ const lockWaitMs = memoryLockWaitMs(deps);
451
+ try {
452
+ if (requestId === undefined) {
453
+ return await withWriteLock(
454
+ deps.lockPath,
455
+ async () => {
456
+ const prepared = await prepare();
457
+ if ("result" in prepared) return prepared.result;
458
+ await publish(prepared.plan);
459
+ return finish(prepared.plan);
460
+ },
461
+ lockWaitMs
462
+ );
463
+ }
464
+ if (!deps.requests) {
465
+ throw new RequestReceiptError(
466
+ "REQUEST_LEDGER_UNAVAILABLE",
467
+ "This memory service has no request ledger; retry without requestId."
468
+ );
469
+ }
470
+ const outcome = await runRequestedWrite<RememberPlan, RememberResult>({
471
+ ledgerPath: deps.requests.ledgerPath,
472
+ namespace: deps.requests.namespace,
473
+ requestId,
474
+ operation: "remember",
475
+ // Caller/session are provenance, not intent: a retry may reconnect
476
+ // under a new session and must still match.
477
+ digest: requestDigest("remember", {
478
+ collection: collection.name,
479
+ text,
480
+ scopes,
481
+ decision,
482
+ predecessorUri: rawInput.predecessorUri?.trim(),
483
+ predecessorHash: rawInput.predecessorHash,
484
+ source,
485
+ }),
486
+ lockPath: deps.lockPath,
487
+ lockWaitMs,
488
+ checkpoint: deps.requests.checkpoint,
489
+ prepare: async () => {
490
+ const prepared = await prepare();
491
+ if ("result" in prepared) return prepared;
492
+ return { plan: prepared.plan, publish: () => publish(prepared.plan) };
493
+ },
494
+ inspect: async (plan) => {
495
+ const file = Bun.file(absPathOf(plan));
496
+ if (!(await file.exists())) return "absent";
497
+ if ((await file.text()) !== serialize(plan)) return "unexpected";
498
+ // Another successor may have superseded the predecessor while this
499
+ // one sat unprojected: finishing would leave two successors.
500
+ const [predecessorUri] = plan.supersedes;
501
+ if (!predecessorUri) return "published";
502
+ const predecessor = await store.getDocumentByUri(predecessorUri);
503
+ if (!predecessor.ok || !predecessor.value) return "unexpected";
504
+ const successors = await store.getEdgeBacklinksForDoc(
505
+ predecessor.value.id,
506
+ { edgeType: MEMORY_SUPERSEDES_EDGE }
349
507
  );
350
- const fileResult = syncResult.files?.[0];
351
- const doc = await store.getDocument(collection.name, relPath);
352
- const sync: MemorySyncState =
353
- fileResult?.status === "error" || !doc.ok || doc.value === null
354
- ? {
355
- status: "failed",
356
- error:
357
- fileResult?.errorMessage ??
358
- fileResult?.errorCode ??
359
- "memory record was written but is not retrievable yet",
360
- }
361
- : { status: "completed" };
362
- if (sync.status === "failed") {
363
- throw new MemoryError(
364
- "MEMORY_SYNC_FAILED",
365
- `Memory record written to ${absPath} but lexical sync failed: ${sync.error}. Run gno update to retry indexing.`
366
- );
367
- }
368
- const written = (doc as { value: DocumentRow }).value;
369
- const projectionErrors = syncResult.errors
370
- .map((error) => `${error.relPath}: ${error.message}`)
371
- .join("; ");
372
- if (decision === "supersede") {
373
- // The write is only a supersession once the edge is projected;
374
- // until then the predecessor still reads as current.
375
- const projected =
376
- projectionErrors.length === 0 &&
377
- (await supersedesEdgeProjected(deps, written.id, supersedes));
378
- if (!projected) {
379
- throw new MemoryError(
380
- "MEMORY_SUPERSEDE_PROJECTION_FAILED",
381
- `Successor written to ${absPath} but its supersedes edge did not project${projectionErrors ? ` (${projectionErrors})` : ""}; the predecessor still reads as current. Run gno update to retry the projection.`
382
- );
383
- }
384
- } else if (projectionErrors.length > 0) {
385
- throw new MemoryError(
386
- "MEMORY_SYNC_FAILED",
387
- `Memory record written to ${absPath} but typed-edge projection failed: ${projectionErrors}. Run gno update to retry indexing.`
388
- );
389
- }
390
- const record: MemoryFact = {
391
- uri: written.uri,
392
- docid: written.docid,
393
- recordId: frontmatter.recordId,
394
- text,
395
- scopes,
396
- caller: identity.caller,
397
- session: identity.session,
398
- createdAt,
399
- contentHash,
400
- supersedes,
401
- ...(source ? { source } : {}),
402
- };
403
- return {
404
- outcome: decision === "supersede" ? "superseded" : "added",
405
- record,
406
- absPath,
407
- sync,
408
- matching,
409
- };
508
+ const ownUri = `gno://${collection.name}/${plan.relPath}`;
509
+ return successors.ok &&
510
+ successors.value.every((edge) => edge.sourceUri === ownUri)
511
+ ? "published"
512
+ : "unexpected";
410
513
  },
411
- lockWaitMs
412
- );
514
+ finish,
515
+ resultRef: (result) =>
516
+ result.outcome === "candidates"
517
+ ? {}
518
+ : {
519
+ uri: result.record.uri,
520
+ docid: result.record.docid,
521
+ contentHash: result.record.contentHash,
522
+ },
523
+ });
524
+ return { ...outcome.result, request: outcome.request } as RememberResult;
413
525
  } catch (error) {
414
- if (error instanceof MemoryError) throw error;
415
- const message = error instanceof Error ? error.message : String(error);
416
- if (message.startsWith("LOCKED")) {
526
+ if (error instanceof MemoryError || error instanceof RequestReceiptError) {
527
+ throw error;
528
+ }
529
+ if (isLeaseBusy(error)) {
417
530
  throw new MemoryError(
418
531
  "MEMORY_WRITE_LEASE_BUSY",
419
532
  `Could not acquire the shared write lease at ${deps.lockPath} within ${lockWaitMs}ms: another write holds it. The memory service takes the lease itself; callers must not pre-hold it.`
@@ -421,5 +534,4 @@ export async function rememberFact(
421
534
  }
422
535
  throw error;
423
536
  }
424
- return leased;
425
537
  }
@@ -15,6 +15,7 @@ import type { EmbeddingPort } from "../llm/types";
15
15
  import type { StorePort } from "../store/types";
16
16
  import type { VectorIndexPort } from "../store/vector/types";
17
17
  import type { EgressLineage } from "./egress-provenance";
18
+ import type { RequestCheckpoint, RequestReceiptInfo } from "./request-receipts";
18
19
 
19
20
  // ─────────────────────────────────────────────────────────────────────────────
20
21
  // Binding defaults (tunable here, documented in docs/MEMORY.md)
@@ -106,6 +107,8 @@ export interface RememberInput extends MemoryIdentity {
106
107
  /** Declared origins; any gno:// origin is fenced. */
107
108
  derivedFrom?: string[];
108
109
  source?: string;
110
+ /** Opt-in retry identity for add/supersede; see docs/MEMORY.md. */
111
+ requestId?: string;
109
112
  }
110
113
 
111
114
  export interface MemoryFact {
@@ -149,6 +152,7 @@ export type RememberResult =
149
152
  outcome: "existing";
150
153
  record: MemoryFact;
151
154
  matching: MemoryMatchDiagnostics;
155
+ request?: RequestReceiptInfo;
152
156
  }
153
157
  | {
154
158
  outcome: "candidates";
@@ -161,6 +165,7 @@ export type RememberResult =
161
165
  absPath: string;
162
166
  sync: MemorySyncState;
163
167
  matching: MemoryMatchDiagnostics;
168
+ request?: RequestReceiptInfo;
164
169
  };
165
170
 
166
171
  export interface RecallInput extends MemoryIdentity {
@@ -208,4 +213,10 @@ export interface MemoryServiceDeps {
208
213
  /** Must surface typed-edge projection errors (`syncPaths`, not `syncFiles`). */
209
214
  syncService?: Pick<typeof defaultSyncService, "syncPaths">;
210
215
  now?: () => Date;
216
+ /** Request ledger + caller namespace; required to honour `requestId`. */
217
+ requests?: {
218
+ ledgerPath: string;
219
+ namespace: string;
220
+ checkpoint?: (stage: RequestCheckpoint) => Promise<void> | void;
221
+ };
211
222
  }
@@ -297,6 +297,14 @@ export const NETWORK_BOUNDARY_INVENTORY = [
297
297
  action: "serve",
298
298
  enforcement: "local_process_only",
299
299
  },
300
+ {
301
+ id: "session-import-process",
302
+ key: "src/sessions/import-child.ts::child_process#1",
303
+ path: "src/sessions/import-child.ts",
304
+ primitive: "child_process",
305
+ action: null,
306
+ enforcement: "local_process_only",
307
+ },
300
308
  {
301
309
  id: "http-mcp-tools",
302
310
  key: "logical::http-mcp-tools",