@remnic/core 9.7.9 → 9.7.11

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 (135) hide show
  1. package/dist/access-admin-ops-surface.js +5 -5
  2. package/dist/access-boundary.d.ts +1 -1
  3. package/dist/access-boundary.js +6 -6
  4. package/dist/access-cli.js +36 -36
  5. package/dist/access-http.d.ts +2 -1
  6. package/dist/access-http.js +10 -10
  7. package/dist/access-identity-continuity-surface.js +5 -5
  8. package/dist/access-lcm-surface.js +5 -5
  9. package/dist/access-mcp.js +9 -9
  10. package/dist/access-observe-write-surface.js +5 -5
  11. package/dist/access-operations-batch.js +7 -7
  12. package/dist/access-operations.d.ts +4 -4
  13. package/dist/access-operations.js +8 -8
  14. package/dist/access-recall-surface.js +5 -5
  15. package/dist/access-schema.d.ts +64 -64
  16. package/dist/access-service.js +5 -5
  17. package/dist/access-surface-catalog.js +2 -0
  18. package/dist/access-surface-catalog.js.map +1 -1
  19. package/dist/briefing.js +3 -3
  20. package/dist/causal-consolidation.js +4 -4
  21. package/dist/{chunk-HHAA35A3.js → chunk-2VN4JAX7.js} +548 -161
  22. package/dist/chunk-2VN4JAX7.js.map +1 -0
  23. package/dist/{chunk-MLT5EG6K.js → chunk-3OQ4IZJN.js} +4 -4
  24. package/dist/{chunk-DLPXS7BE.js → chunk-5U65RIVI.js} +348 -49
  25. package/dist/chunk-5U65RIVI.js.map +1 -0
  26. package/dist/{chunk-QKVX4Y6D.js → chunk-673NPXE3.js} +2 -2
  27. package/dist/{chunk-POUD5BLV.js → chunk-6LM6PPDX.js} +4 -2
  28. package/dist/chunk-6LM6PPDX.js.map +1 -0
  29. package/dist/chunk-7XRUYC6G.js +1794 -0
  30. package/dist/chunk-7XRUYC6G.js.map +1 -0
  31. package/dist/{chunk-5CFHYQU3.js → chunk-DLWYAN6A.js} +2 -2
  32. package/dist/{chunk-TY43GBX2.js → chunk-EQF5763Z.js} +2 -2
  33. package/dist/{chunk-SKXBOZDF.js → chunk-GHVJASQ4.js} +3 -3
  34. package/dist/{chunk-YGAOLHKN.js → chunk-JYRFO4EH.js} +4 -4
  35. package/dist/{chunk-F3FC7SMO.js → chunk-JZM5QUTE.js} +2 -2
  36. package/dist/{chunk-PV7E2FGY.js → chunk-KXLRMM35.js} +6 -6
  37. package/dist/{chunk-PHXCPVSX.js → chunk-L4H6MQ7I.js} +2 -2
  38. package/dist/{chunk-FKCJH7KP.js → chunk-LQKITFGB.js} +2 -2
  39. package/dist/{chunk-27VEKQQA.js → chunk-MXRMNKSY.js} +3 -3
  40. package/dist/{chunk-EIKBKQ6L.js → chunk-QHT4W3RU.js} +127 -9
  41. package/dist/chunk-QHT4W3RU.js.map +1 -0
  42. package/dist/{chunk-CDGGQGUU.js → chunk-SBFDL2EQ.js} +2 -2
  43. package/dist/{chunk-I2THZUAF.js → chunk-SECMZACR.js} +2 -2
  44. package/dist/{chunk-JU3GHO6J.js → chunk-TGBJ2OMK.js} +2 -2
  45. package/dist/{chunk-DE6ENYLG.js → chunk-TXLGHDHS.js} +2 -2
  46. package/dist/{chunk-KGNFFCZJ.js → chunk-U4KHQTJ6.js} +2 -2
  47. package/dist/{chunk-Y5U35ZVS.js → chunk-UTDA43TB.js} +2 -2
  48. package/dist/{chunk-CK6RI52P.js → chunk-VCEH5UYS.js} +2 -2
  49. package/dist/{chunk-UXWGPODH.js → chunk-VRAD2FGO.js} +7 -7
  50. package/dist/{chunk-KKASNHGF.js → chunk-W6R43NLA.js} +19 -19
  51. package/dist/{chunk-UP25ZZCN.js → chunk-WUDBMLC6.js} +2 -2
  52. package/dist/{chunk-XG5CGR3K.js → chunk-XF4KOVDK.js} +5 -5
  53. package/dist/{chunk-KPXAJ77O.js → chunk-YLTG6BO4.js} +2 -2
  54. package/dist/cli.js +23 -23
  55. package/dist/compounding/engine.js +3 -3
  56. package/dist/connectors/codex-materialize-runner.js +3 -3
  57. package/dist/connectors/index.js +3 -3
  58. package/dist/entity-retrieval.js +3 -3
  59. package/dist/index.d.ts +3990 -4
  60. package/dist/index.js +90 -43
  61. package/dist/lcm/engine.js +3 -3
  62. package/dist/lcm/index.js +8 -8
  63. package/dist/maintenance/memory-governance.js +3 -3
  64. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  65. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  66. package/dist/namespaces/migrate.js +4 -4
  67. package/dist/namespaces/storage.js +3 -3
  68. package/dist/operator-toolkit.js +7 -7
  69. package/dist/orchestration/maintenance.js +5 -5
  70. package/dist/orchestrator.js +36 -36
  71. package/dist/retrieval-agents.js +2 -2
  72. package/dist/schemas.d.ts +74 -74
  73. package/dist/semantic-consolidation.js +4 -4
  74. package/dist/semantic-rule-promotion.js +3 -3
  75. package/dist/semantic-rule-verifier.js +3 -3
  76. package/dist/shared-context/manager.d.ts +8 -8
  77. package/dist/storage.js +2 -2
  78. package/dist/temporal-index-compat.d.ts +1 -0
  79. package/dist/temporal-index-compat.js +16 -0
  80. package/dist/temporal-index-compat.js.map +1 -0
  81. package/dist/temporal-index.d.ts +58 -7
  82. package/dist/temporal-index.js +23 -3
  83. package/dist/transfer/types.d.ts +66 -66
  84. package/dist/verified-recall.js +3 -3
  85. package/package.json +2 -2
  86. package/src/access-boundary.ts +2 -0
  87. package/src/access-http.ts +147 -6
  88. package/src/access-mcp-cancellation.test.ts +2 -2
  89. package/src/access-operations.ts +2 -0
  90. package/src/access-surface-catalog.ts +2 -0
  91. package/src/index.ts +53 -0
  92. package/src/orchestration/consolidation-run.ts +5 -5
  93. package/src/orchestration/contradiction-linking-coordinator.ts +2 -2
  94. package/src/orchestration/lifecycle-policy-coordinator.ts +2 -2
  95. package/src/orchestration/persistence-index.ts +6 -6
  96. package/src/orchestration/recall-search-pipeline.ts +36 -21
  97. package/src/orchestration/semantic-consolidation-coordinator.ts +2 -2
  98. package/src/orchestrator.ts +0 -4
  99. package/src/relay/mission-access.test.ts +379 -0
  100. package/src/relay/mission-access.ts +125 -0
  101. package/src/relay/mission-fixture.ts +301 -0
  102. package/src/relay/mission.test.ts +964 -0
  103. package/src/relay/mission.ts +1725 -0
  104. package/src/temporal-index-compat.ts +311 -0
  105. package/src/temporal-index.test.ts +273 -22
  106. package/src/temporal-index.ts +427 -178
  107. package/dist/chunk-DLPXS7BE.js.map +0 -1
  108. package/dist/chunk-EIKBKQ6L.js.map +0 -1
  109. package/dist/chunk-HHAA35A3.js.map +0 -1
  110. package/dist/chunk-POUD5BLV.js.map +0 -1
  111. package/dist/chunk-VCFADTFS.js +0 -467
  112. package/dist/chunk-VCFADTFS.js.map +0 -1
  113. /package/dist/{chunk-MLT5EG6K.js.map → chunk-3OQ4IZJN.js.map} +0 -0
  114. /package/dist/{chunk-QKVX4Y6D.js.map → chunk-673NPXE3.js.map} +0 -0
  115. /package/dist/{chunk-5CFHYQU3.js.map → chunk-DLWYAN6A.js.map} +0 -0
  116. /package/dist/{chunk-TY43GBX2.js.map → chunk-EQF5763Z.js.map} +0 -0
  117. /package/dist/{chunk-SKXBOZDF.js.map → chunk-GHVJASQ4.js.map} +0 -0
  118. /package/dist/{chunk-YGAOLHKN.js.map → chunk-JYRFO4EH.js.map} +0 -0
  119. /package/dist/{chunk-F3FC7SMO.js.map → chunk-JZM5QUTE.js.map} +0 -0
  120. /package/dist/{chunk-PV7E2FGY.js.map → chunk-KXLRMM35.js.map} +0 -0
  121. /package/dist/{chunk-PHXCPVSX.js.map → chunk-L4H6MQ7I.js.map} +0 -0
  122. /package/dist/{chunk-FKCJH7KP.js.map → chunk-LQKITFGB.js.map} +0 -0
  123. /package/dist/{chunk-27VEKQQA.js.map → chunk-MXRMNKSY.js.map} +0 -0
  124. /package/dist/{chunk-CDGGQGUU.js.map → chunk-SBFDL2EQ.js.map} +0 -0
  125. /package/dist/{chunk-I2THZUAF.js.map → chunk-SECMZACR.js.map} +0 -0
  126. /package/dist/{chunk-JU3GHO6J.js.map → chunk-TGBJ2OMK.js.map} +0 -0
  127. /package/dist/{chunk-DE6ENYLG.js.map → chunk-TXLGHDHS.js.map} +0 -0
  128. /package/dist/{chunk-KGNFFCZJ.js.map → chunk-U4KHQTJ6.js.map} +0 -0
  129. /package/dist/{chunk-Y5U35ZVS.js.map → chunk-UTDA43TB.js.map} +0 -0
  130. /package/dist/{chunk-CK6RI52P.js.map → chunk-VCEH5UYS.js.map} +0 -0
  131. /package/dist/{chunk-UXWGPODH.js.map → chunk-VRAD2FGO.js.map} +0 -0
  132. /package/dist/{chunk-KKASNHGF.js.map → chunk-W6R43NLA.js.map} +0 -0
  133. /package/dist/{chunk-UP25ZZCN.js.map → chunk-WUDBMLC6.js.map} +0 -0
  134. /package/dist/{chunk-XG5CGR3K.js.map → chunk-XF4KOVDK.js.map} +0 -0
  135. /package/dist/{chunk-KPXAJ77O.js.map → chunk-YLTG6BO4.js.map} +0 -0
@@ -53,6 +53,7 @@ import { handleChatMessage, handleChatEventsSSE } from "./chat/chat-http.js";
53
53
  import { cleanupExpiredChatSessions, enforceChatSessionNamespace } from "./chat/chat-session.js";
54
54
  import { isDefaultReviewNamespace, listPairs, readPair } from "./contradiction/contradiction-review.js";
55
55
  import { isValidResolutionVerb, executeResolution } from "./contradiction/resolution.js";
56
+ import { RelayMissionStoreError } from "./relay/mission.js";
56
57
 
57
58
  export interface AccessHttpReadinessState {
58
59
  ready: boolean;
@@ -339,6 +340,22 @@ function decodePeerIdSegment(raw: string): string {
339
340
  }
340
341
  }
341
342
 
343
+ function decodeRelayMissionIdSegment(raw: string): string {
344
+ try {
345
+ return decodeURIComponent(raw);
346
+ } catch {
347
+ throw new EngramAccessInputError(
348
+ "missionId path segment is not valid percent-encoded input",
349
+ );
350
+ }
351
+ }
352
+
353
+ function rejectBlankRelayNamespace(value: string | null | undefined): void {
354
+ if (value !== undefined && value !== null && value.trim().length === 0) {
355
+ throw new EngramAccessInputError("Relay namespace must not be blank when provided");
356
+ }
357
+ }
358
+
342
359
  function codingContextFromProjectTag(projectTag: string): {
343
360
  projectId: string;
344
361
  branch: string | null;
@@ -378,7 +395,7 @@ export class EngramAccessHttpServer {
378
395
  res: ServerResponse,
379
396
  ctx: { authorized: boolean },
380
397
  ) => Promise<boolean>;
381
- private readonly writeRequestTimestamps: number[] = [];
398
+ private readonly writeRequestSlots: Array<{ readonly recordedAt: number }> = [];
382
399
  private readonly writeRateLimitMaxRequests: number;
383
400
  private readonly writeRateLimitWindowMs: number;
384
401
  private readonly mcpServer: EngramMcpServer;
@@ -499,6 +516,20 @@ export class EngramAccessHttpServer {
499
516
  this.respondJson(res, 400, { error: err.message, code: "correction_contract_error" });
500
517
  return;
501
518
  }
519
+ if (err instanceof RelayMissionStoreError) {
520
+ const status = err.code === "idempotency_conflict"
521
+ ? 409
522
+ : err.code === "limit_exceeded"
523
+ ? 413
524
+ : err.code === "lock_unavailable"
525
+ ? 503
526
+ : 500;
527
+ this.respondJson(res, status, {
528
+ error: status >= 500 ? "relay_backend_unavailable" : err.message,
529
+ code: `relay_${err.code}`,
530
+ });
531
+ return;
532
+ }
502
533
  if (res.headersSent) {
503
534
  res.destroy(err as Error);
504
535
  return;
@@ -1670,6 +1701,103 @@ export class EngramAccessHttpServer {
1670
1701
  return;
1671
1702
  }
1672
1703
 
1704
+ // Remnic Relay mission receipts (issue #1966). These two routes share the
1705
+ // access-boundary schemas and namespace-resolved storage contract. The
1706
+ // browser receives an already-reduced snapshot; it never scans general
1707
+ // memories, recall logs, or production state.
1708
+ const relayEventMatch = /^\/engram\/v1\/relay\/missions\/([^/]+)\/events$/.exec(pathname);
1709
+ if (req.method === "POST" && relayEventMatch) {
1710
+ const missionId = decodeRelayMissionIdSegment(relayEventMatch[1] ?? "");
1711
+ const body = await this.readJsonBody(req);
1712
+ const unexpectedFields = Object.keys(body).filter(
1713
+ (key) => key !== "namespace" && key !== "event",
1714
+ );
1715
+ if (unexpectedFields.length > 0) {
1716
+ throw new EngramAccessInputError(
1717
+ `Relay append body contains unexpected field(s): ${unexpectedFields.sort().join(", ")}`,
1718
+ );
1719
+ }
1720
+ if (!Object.prototype.hasOwnProperty.call(body, "event")) {
1721
+ throw new EngramAccessInputError("Relay append body must contain an event object");
1722
+ }
1723
+ if (
1724
+ Object.prototype.hasOwnProperty.call(body, "namespace") &&
1725
+ body.namespace !== undefined &&
1726
+ body.namespace !== null &&
1727
+ typeof body.namespace !== "string"
1728
+ ) {
1729
+ throw new EngramAccessInputError("namespace must be a string when provided");
1730
+ }
1731
+ rejectBlankRelayNamespace(
1732
+ typeof body.namespace === "string" ? body.namespace : undefined,
1733
+ );
1734
+ const namespace = this.resolveNamespace(
1735
+ req,
1736
+ typeof body.namespace === "string" ? body.namespace : undefined,
1737
+ );
1738
+ const op = getOperation("relay_mission_append");
1739
+ if (!op) {
1740
+ throw new Error("access-boundary: operation not registered: relay_mission_append");
1741
+ }
1742
+ let releaseWriteQuota: (() => void) | undefined;
1743
+ try {
1744
+ const output = (await op.run(
1745
+ { missionId, namespace, event: body.event },
1746
+ {
1747
+ service: this.service,
1748
+ authenticatedPrincipal: this.resolveRequestPrincipal(req),
1749
+ hooks: {
1750
+ enforceWriteQuota: () => {
1751
+ releaseWriteQuota ??= this.reserveWriteRateLimitSlot();
1752
+ },
1753
+ },
1754
+ },
1755
+ )) as { result: { appended: boolean; replayed: boolean; event: unknown } };
1756
+ if (output.result.appended) {
1757
+ // The reservation is now the committed quota hit. Do not record a
1758
+ // second timestamp after the append returns.
1759
+ releaseWriteQuota = undefined;
1760
+ }
1761
+ this.respondJson(res, output.result.appended ? 201 : 200, output.result);
1762
+ } finally {
1763
+ // Replays never invoke the hook. If a future store path invokes it but
1764
+ // does not append, or persistence throws after reservation, return the
1765
+ // slot so a failed write cannot exhaust the rolling window.
1766
+ releaseWriteQuota?.();
1767
+ }
1768
+ return;
1769
+ }
1770
+
1771
+ const relayMissionMatch = /^\/engram\/v1\/relay\/missions\/([^/]+)$/.exec(pathname);
1772
+ if (req.method === "GET" && relayMissionMatch) {
1773
+ const missionId = decodeRelayMissionIdSegment(relayMissionMatch[1] ?? "");
1774
+ rejectBlankRelayNamespace(parsed.searchParams.get("namespace"));
1775
+ const namespace = this.resolveNamespace(
1776
+ req,
1777
+ parsed.searchParams.get("namespace") ?? undefined,
1778
+ );
1779
+ const op = getOperation("relay_mission_read");
1780
+ if (!op) {
1781
+ throw new Error("access-boundary: operation not registered: relay_mission_read");
1782
+ }
1783
+ const output = (await op.run(
1784
+ {
1785
+ missionId,
1786
+ namespace,
1787
+ since: parsed.searchParams.get("since") ?? undefined,
1788
+ until: parsed.searchParams.get("until") ?? undefined,
1789
+ limit: parsed.searchParams.get("limit") ?? undefined,
1790
+ },
1791
+ {
1792
+ service: this.service,
1793
+ authenticatedPrincipal: this.resolveRequestPrincipal(req),
1794
+ },
1795
+ )) as { result: unknown };
1796
+ res.setHeader("cache-control", "no-store");
1797
+ this.respondJson(res, 200, output.result);
1798
+ return;
1799
+ }
1800
+
1673
1801
  if (req.method === "POST" && pathname === "/engram/v1/memories") {
1674
1802
  // Migrated through the access boundary (issue #1525): the registry
1675
1803
  // entry owns schema validation, normalization, and service dispatch.
@@ -3711,18 +3839,31 @@ export class EngramAccessHttpServer {
3711
3839
  private ensureWriteRateLimitAvailable(): void {
3712
3840
  const now = Date.now();
3713
3841
  while (
3714
- this.writeRequestTimestamps.length > 0 &&
3715
- now - (this.writeRequestTimestamps[0] ?? 0) > this.writeRateLimitWindowMs
3842
+ this.writeRequestSlots.length > 0 &&
3843
+ now - (this.writeRequestSlots[0]?.recordedAt ?? 0) > this.writeRateLimitWindowMs
3716
3844
  ) {
3717
- this.writeRequestTimestamps.shift();
3845
+ this.writeRequestSlots.shift();
3718
3846
  }
3719
- if (this.writeRequestTimestamps.length >= this.writeRateLimitMaxRequests) {
3847
+ if (this.writeRequestSlots.length >= this.writeRateLimitMaxRequests) {
3720
3848
  throw new HttpError(429, "write_rate_limited", "write_rate_limited");
3721
3849
  }
3722
3850
  }
3723
3851
 
3724
3852
  private recordWriteRateLimitHit(): void {
3725
- this.writeRequestTimestamps.push(Date.now());
3853
+ this.writeRequestSlots.push({ recordedAt: Date.now() });
3854
+ }
3855
+
3856
+ private reserveWriteRateLimitSlot(): () => void {
3857
+ this.ensureWriteRateLimitAvailable();
3858
+ const slot = { recordedAt: Date.now() };
3859
+ this.writeRequestSlots.push(slot);
3860
+ let active = true;
3861
+ return () => {
3862
+ if (!active) return;
3863
+ active = false;
3864
+ const index = this.writeRequestSlots.indexOf(slot);
3865
+ if (index >= 0) this.writeRequestSlots.splice(index, 1);
3866
+ };
3726
3867
  }
3727
3868
 
3728
3869
  private shouldCountWriteRateLimit(response: { dryRun?: boolean; idempotencyReplay?: boolean }): boolean {
@@ -282,7 +282,7 @@ test("MCP write quota is recorded after commit even when the client disconnects
282
282
  });
283
283
  const serverHost = server as unknown as {
284
284
  mcpServer: EngramMcpServer;
285
- writeRequestTimestamps: number[];
285
+ writeRequestSlots: Array<{ readonly recordedAt: number }>;
286
286
  };
287
287
  const originalHandleRequest = serverHost.mcpServer.handleRequest.bind(serverHost.mcpServer);
288
288
  serverHost.mcpServer.handleRequest = async (request, options) => {
@@ -318,7 +318,7 @@ test("MCP write quota is recorded after commit even when the client disconnects
318
318
 
319
319
  await waitFor(operationSettled.promise);
320
320
  await new Promise<void>((resolve) => setImmediate(resolve));
321
- assert.equal(serverHost.writeRequestTimestamps.length, 1);
321
+ assert.equal(serverHost.writeRequestSlots.length, 1);
322
322
  assert.equal(responseStarted, false, "the disconnected client must not receive the committed write response");
323
323
  } finally {
324
324
  releaseOperation.resolve();
@@ -678,6 +678,8 @@ export const codegraphIngestTracesOperation = defineOperation<
678
678
  // Batch-migrated operations (issue #1525): registers all remaining
679
679
  // handlers through the boundary as a side effect of importing this module.
680
680
  import "./access-operations-batch.js";
681
+ // Remnic Relay mission evidence operations (issue #1966).
682
+ import "./relay/mission-access.js";
681
683
 
682
684
  export const REGISTERED_OPERATIONS = [
683
685
  memoryGetOperation.spec.name,
@@ -193,6 +193,8 @@ export const HTTP_ROUTES: readonly HttpRouteEntry[] = [
193
193
  { method: "POST", pathname: "/engram/v1/lcm/compaction/flush", operation: "lcm_compaction_flush" },
194
194
  { method: "POST", pathname: "/engram/v1/lcm/compaction/record", operation: "lcm_compaction_record" },
195
195
  { method: "GET", pathname: "/engram/v1/lcm/status", operation: "lcm_status" },
196
+ { method: "GET", pathname: "/engram/v1/relay/missions/:id", operation: "relay_mission_read" },
197
+ { method: "POST", pathname: "/engram/v1/relay/missions/:id/events", operation: "relay_mission_append" },
196
198
  // Correction Contract (issue #1580) — plan/apply/pending.
197
199
  { method: "POST", pathname: "/engram/v1/correction/plan", operation: "memory_correct_plan" },
198
200
  { method: "POST", pathname: "/engram/v1/correction/apply", operation: "memory_correct_apply" },
package/src/index.ts CHANGED
@@ -387,6 +387,59 @@ export {
387
387
  } from "./access-http.js";
388
388
  export { EngramMcpServer } from "./access-mcp.js";
389
389
 
390
+ // Remnic Relay — host-agnostic mission evidence and receipt contract.
391
+ export {
392
+ RELAY_MISSION_DEFAULT_EVENT_LIMIT,
393
+ RELAY_MISSION_MAX_EVENT_LIMIT,
394
+ RELAY_MISSION_MAX_EVENTS,
395
+ RELAY_MISSION_MAX_FILE_BYTES,
396
+ RELAY_MISSION_MAX_LINE_BYTES,
397
+ RELAY_MISSION_SCHEMA_VERSION,
398
+ RelayEvidenceRefSchema,
399
+ RelayMissionEventInputSchema,
400
+ RelayMissionEventSchema,
401
+ RelayMissionPayloadSchema,
402
+ RelayMissionReadOptionsSchema,
403
+ RelayMissionStore,
404
+ RelayMissionStoreError,
405
+ compareRelayEvents,
406
+ reduceRelayMission,
407
+ relayMissionReceiptDigest,
408
+ type RelayAgentOutputSnapshot,
409
+ type RelayAgentSnapshot,
410
+ type RelayConflictSnapshot,
411
+ type RelayCorrectionSnapshot,
412
+ type RelayDecisionSnapshot,
413
+ type RelayEvidenceRef,
414
+ type RelayMissionAppendResult,
415
+ type RelayMissionEvent,
416
+ type RelayMissionEventInput,
417
+ type RelayMissionPayload,
418
+ type RelayMissionReadOptions,
419
+ type RelayMissionSnapshot,
420
+ type RelayMissionStatus,
421
+ type RelayMissionStoreErrorCode,
422
+ type RelayMissionStoreOptions,
423
+ type RelayPropagationSnapshot,
424
+ type RelayRecallSnapshot,
425
+ type RelayTestSnapshot,
426
+ } from "./relay/mission.js";
427
+ export {
428
+ RELAY_DEMO_MISSION_ID,
429
+ RELAY_DEMO_NAMESPACE,
430
+ createRelayMissionFixture,
431
+ } from "./relay/mission-fixture.js";
432
+ export {
433
+ appendRelayMissionEvent,
434
+ readRelayMission,
435
+ relayMissionAppendOperation,
436
+ relayMissionReadOperation,
437
+ type RelayMissionAppendAccessInput,
438
+ type RelayMissionAppendAccessOutput,
439
+ type RelayMissionReadAccessInput,
440
+ type RelayMissionReadAccessOutput,
441
+ } from "./relay/mission-access.js";
442
+
390
443
  // agentAccessHttp.authToken SecretRef resolution (issue #757). Exposed so
391
444
  // host-specific bootstrap code (the OpenClaw plugin in `src/index.ts`, the
392
445
  // standalone server, the CLI) can resolve a SecretRef to a literal bearer
@@ -30,7 +30,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
30
30
  import { log } from "../logger.js";
31
31
  import { applyCommitmentLedgerLifecycle } from "../commitment-ledger.js";
32
32
  import { recordDreamsPhaseRun } from "../maintenance/dreams-ledger.js";
33
- import { clearIndexes, deindexMemory, indexMemoriesBatch, indexesExist } from "../temporal-index.js";
33
+ import { deindexMemoryAsync } from "../temporal-index.js";
34
34
  import { isActiveMemoryStatus } from "../memory-lifecycle-ledger-utils.js";
35
35
  import {
36
36
  resolveConsolidationCapabilities,
@@ -165,7 +165,7 @@ export class ConsolidationRunCoordinator {
165
165
  memoryItemMutated = true;
166
166
  await this.deps.embeddingFallback.removeFromIndex(item.existingId);
167
167
  if (toInvalidate?.path && toInvalidate.frontmatter?.created) {
168
- deindexMemory(
168
+ await deindexMemoryAsync(
169
169
  config.memoryDir,
170
170
  toInvalidate.path,
171
171
  toInvalidate.frontmatter.created,
@@ -216,7 +216,7 @@ export class ConsolidationRunCoordinator {
216
216
  toMergeInvalidate?.path &&
217
217
  toMergeInvalidate.frontmatter?.created
218
218
  ) {
219
- deindexMemory(
219
+ await deindexMemoryAsync(
220
220
  config.memoryDir,
221
221
  toMergeInvalidate.path,
222
222
  toMergeInvalidate.frontmatter.created,
@@ -293,7 +293,7 @@ export class ConsolidationRunCoordinator {
293
293
  log.info(`cleaned ${deletedCommitments.length} expired commitments`);
294
294
  if (resolveIndexingCapabilities(config).queryAwareIndexing) {
295
295
  for (const m of deletedCommitments) {
296
- deindexMemory(
296
+ await deindexMemoryAsync(
297
297
  config.memoryDir,
298
298
  m.path,
299
299
  m.frontmatter.created,
@@ -336,7 +336,7 @@ export class ConsolidationRunCoordinator {
336
336
  log.info(`cleaned ${deletedTTL.length} TTL-expired memories`);
337
337
  if (resolveIndexingCapabilities(config).queryAwareIndexing) {
338
338
  for (const m of deletedTTL) {
339
- deindexMemory(
339
+ await deindexMemoryAsync(
340
340
  config.memoryDir,
341
341
  m.path,
342
342
  m.frontmatter.created,
@@ -27,7 +27,7 @@ import type { StorageManager } from "../index.js";
27
27
  import type { ExtractionEngine } from "../extraction.js";
28
28
  import { log } from "../logger.js";
29
29
  import { resolveIndexingCapabilities } from "../capabilities.js";
30
- import { deindexMemory } from "../temporal-index.js";
30
+ import { deindexMemoryAsync } from "../temporal-index.js";
31
31
 
32
32
  /** Result type of {@link ContradictionLinkingCoordinator.checkForContradiction}. */
33
33
  export interface ContradictionResult {
@@ -253,7 +253,7 @@ export class ContradictionLinkingCoordinator {
253
253
  resolveIndexingCapabilities(config).queryAwareIndexing &&
254
254
  contradiction.supersededPath
255
255
  ) {
256
- deindexMemory(
256
+ await deindexMemoryAsync(
257
257
  config.memoryDir,
258
258
  contradiction.supersededPath,
259
259
  contradiction.supersededCreated,
@@ -34,7 +34,7 @@ import {
34
34
  resolveCreationMemoryCapabilities,
35
35
  } from "../capabilities.js";
36
36
  import { isActiveMemoryStatus } from "../memory-lifecycle-ledger-utils.js";
37
- import { deindexMemory } from "../temporal-index.js";
37
+ import { deindexMemoryAsync } from "../temporal-index.js";
38
38
  import { extractTopics } from "../topics.js";
39
39
  import { log } from "../logger.js";
40
40
  import path from "node:path";
@@ -345,7 +345,7 @@ export class LifecyclePolicyCoordinator {
345
345
  memory.path &&
346
346
  memory.frontmatter?.created
347
347
  ) {
348
- deindexMemory(
348
+ await deindexMemoryAsync(
349
349
  this.config.memoryDir,
350
350
  memory.path,
351
351
  memory.frontmatter.created,
@@ -23,7 +23,7 @@ import { ContentHashIndex, StorageManager } from "../index.js";
23
23
  import { log } from "../logger.js";
24
24
  import { isActiveMemoryStatus } from "../memory-lifecycle-ledger-utils.js";
25
25
  import { stripCitationForTemplate } from "../source-attribution.js";
26
- import { clearIndexes, indexMemoriesBatch, indexesExist } from "../temporal-index.js";
26
+ import { clearIndexesAsync, indexMemoriesBatchAsync, indexesExistAsync } from "../temporal-index.js";
27
27
  import { normalizeSupersessionKey } from "../temporal-supersession.js";
28
28
  import type { MemoryFile, MemoryFrontmatter, PluginConfig } from "../types.js";
29
29
  import {
@@ -392,7 +392,7 @@ export class PersistenceIndexCoordinator {
392
392
  return;
393
393
  // Check for missing indexes BEFORE the early-return so first-time enablement
394
394
  // can bootstrap the full corpus even when this extraction turn persisted nothing.
395
- const needsFullRebuild = !indexesExist(this.deps.config.memoryDir);
395
+ const needsFullRebuild = !(await indexesExistAsync(this.deps.config.memoryDir));
396
396
  if (!needsFullRebuild && persistedIds.length === 0) return;
397
397
  try {
398
398
  // Read the corpus once to avoid N separate full-corpus scans.
@@ -449,17 +449,17 @@ export class PersistenceIndexCoordinator {
449
449
  if (needsFullRebuild) {
450
450
  // Always write empty indexes on full rebuild — even when the active pool
451
451
  // is empty (e.g. store contains only archived/superseded entries).
452
- // This marks bootstrap completion so indexesExist() returns true and
452
+ // This marks bootstrap completion so indexesExistAsync() returns true and
453
453
  // subsequent extractions skip the full-corpus scan.
454
- clearIndexes(this.deps.config.memoryDir);
454
+ await clearIndexesAsync(this.deps.config.memoryDir);
455
455
  if (entries.length > 0) {
456
- indexMemoriesBatch(this.deps.config.memoryDir, entries);
456
+ await indexMemoriesBatchAsync(this.deps.config.memoryDir, entries);
457
457
  }
458
458
  log.info(
459
459
  `temporal-index: bootstrapped from ${entries.length} active memories`,
460
460
  );
461
461
  } else if (entries.length > 0) {
462
- indexMemoriesBatch(this.deps.config.memoryDir, entries);
462
+ await indexMemoriesBatchAsync(this.deps.config.memoryDir, entries);
463
463
  }
464
464
  } catch (err) {
465
465
  log.debug(`temporal-index update failed (non-fatal): ${err}`);
@@ -33,7 +33,7 @@ import { RerankCache, rerankLocalOrNoop } from "../rerank.js";
33
33
  import type { SearchBackend, SearchDegradation, SearchExecutionOptions, SearchQueryOptions } from "../search/port.js";
34
34
  import { SecureStoreLockedError } from "../secure-store/index.js";
35
35
  import { isPathInsideStorageRoot } from "../storage-paths.js";
36
- import { extractTagsFromPrompt, isTemporalQuery, queryByDateRangeAsync, queryByTagsAsync, recencyWindowFromPrompt, resolvePromptTagPrefilterAsync } from "../temporal-index.js";
36
+ import { extractTagsFromPrompt, isTemporalQuery, queryByDateRangeAsync, queryByTagsAsync, readIndexSnapshotAsync, recencyWindowFromPrompt, resolvePromptTagPrefilterAsync } from "../temporal-index.js";
37
37
  import { shouldFilterSupersededFromRecall } from "../temporal-supersession.js";
38
38
  import { isValidAsOf, isValidityExpiredNow } from "../temporal-validity.js";
39
39
  import type { TrustStageResultItem } from "../trust-score-stage.js";
@@ -305,18 +305,26 @@ export class RecallSearchPipelineCoordinator {
305
305
  const temporalFromDate = isTemporalQuery(prompt)
306
306
  ? recencyWindowFromPrompt(prompt, Date.now())
307
307
  : null;
308
- const [rawTemporal, tagSignals] = await Promise.all([
309
- temporalFromDate
310
- ? queryByDateRangeAsync(this.deps.config.memoryDir, temporalFromDate)
311
- : Promise.resolve<Set<string> | null>(null),
312
- resolvePromptTagPrefilterAsync(this.deps.config.memoryDir, prompt).catch(
313
- () => ({
314
- matchedTags: extractTagsFromPrompt(prompt),
315
- expandedTags: extractTagsFromPrompt(prompt),
316
- paths: null,
317
- }),
318
- ),
319
- ]);
308
+ // Read the temporal + tag prefilter indexes as ONE consistent snapshot
309
+ // (issue #1911, Codex Medium) so a concurrent async index mutation can't be
310
+ // observed half-applied (new temporal row, tag membership not yet written),
311
+ // which would drop the current memory from the tag prefilter.
312
+ const [rawTemporal, tagSignals] = await readIndexSnapshotAsync(
313
+ this.deps.config.memoryDir,
314
+ () =>
315
+ Promise.all([
316
+ temporalFromDate
317
+ ? queryByDateRangeAsync(this.deps.config.memoryDir, temporalFromDate)
318
+ : Promise.resolve<Set<string> | null>(null),
319
+ resolvePromptTagPrefilterAsync(this.deps.config.memoryDir, prompt).catch(
320
+ () => ({
321
+ matchedTags: extractTagsFromPrompt(prompt),
322
+ expandedTags: extractTagsFromPrompt(prompt),
323
+ paths: null,
324
+ }),
325
+ ),
326
+ ]),
327
+ );
320
328
 
321
329
  const temporalCandidates = this.deps.scopeQueryAwarePaths(
322
330
  rawTemporal,
@@ -1516,14 +1524,21 @@ export class RecallSearchPipelineCoordinator {
1516
1524
  promptTags = extractTagsFromPrompt(prompt);
1517
1525
  }
1518
1526
 
1519
- const [rawTemporal, rawTags] = await Promise.all([
1520
- temporalFromDate !== null
1521
- ? queryByDateRangeAsync(this.deps.config.memoryDir, temporalFromDate)
1522
- : Promise.resolve<Set<string> | null>(null),
1523
- promptTags.length > 0
1524
- ? queryByTagsAsync(this.deps.config.memoryDir, promptTags)
1525
- : Promise.resolve<Set<string> | null>(null),
1526
- ]);
1527
+ // Consistent temporal+tag snapshot (issue #1911, Codex Medium): read both
1528
+ // indexes under one op-lock so an in-flight async mutation is never observed
1529
+ // half-applied.
1530
+ const [rawTemporal, rawTags] = await readIndexSnapshotAsync(
1531
+ this.deps.config.memoryDir,
1532
+ () =>
1533
+ Promise.all([
1534
+ temporalFromDate !== null
1535
+ ? queryByDateRangeAsync(this.deps.config.memoryDir, temporalFromDate)
1536
+ : Promise.resolve<Set<string> | null>(null),
1537
+ promptTags.length > 0
1538
+ ? queryByTagsAsync(this.deps.config.memoryDir, promptTags)
1539
+ : Promise.resolve<Set<string> | null>(null),
1540
+ ]),
1541
+ );
1527
1542
 
1528
1543
  const queryIntent =
1529
1544
  resolveConversationContextCapabilities(this.deps.config).intentRouting && prompt
@@ -42,7 +42,7 @@ import {
42
42
  gatewayTaskChainOptions,
43
43
  } from "../fallback-llm.js";
44
44
  import { resolveIndexingCapabilities, resolveConsolidationCapabilities } from "../capabilities.js";
45
- import { deindexMemory } from "../temporal-index.js";
45
+ import { deindexMemoryAsync } from "../temporal-index.js";
46
46
  import { runPeerProfileReasoner } from "../peers/index.js";
47
47
  import type { StorageManager } from "../index.js";
48
48
  import type { LocalLlmClient } from "../local-llm.js";
@@ -342,7 +342,7 @@ export class SemanticConsolidationCoordinator {
342
342
  m.path &&
343
343
  m.frontmatter?.created
344
344
  ) {
345
- deindexMemory(
345
+ await deindexMemoryAsync(
346
346
  targetStorage.dir,
347
347
  m.path,
348
348
  m.frontmatter.created,
@@ -326,10 +326,6 @@ import {
326
326
  } from "./lifecycle.js";
327
327
  import { isActiveMemoryStatus } from "./memory-lifecycle-ledger-utils.js";
328
328
  import {
329
- indexMemoriesBatch,
330
- clearIndexes,
331
- indexesExist,
332
- deindexMemory,
333
329
  queryByDateRangeAsync,
334
330
  queryByTagsAsync,
335
331
  isTemporalQuery,