@remnic/core 9.3.621 → 9.3.623

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 (164) hide show
  1. package/dist/access-cli.js +28 -28
  2. package/dist/access-http.js +11 -11
  3. package/dist/access-mcp.js +10 -10
  4. package/dist/access-service.js +9 -9
  5. package/dist/briefing.js +6 -6
  6. package/dist/buffer-surprise.js +3 -3
  7. package/dist/calibration.js +2 -2
  8. package/dist/causal-consolidation.js +10 -10
  9. package/dist/{chunk-GLPBYIXN.js → chunk-2L54V4ZO.js} +3 -3
  10. package/dist/{chunk-PP2JH3GP.js → chunk-2UFQYU5F.js} +2 -2
  11. package/dist/{chunk-XAZOWLW4.js → chunk-3VONWEQB.js} +3 -3
  12. package/dist/{chunk-BF7ZRHH2.js → chunk-66SLUXKM.js} +2 -2
  13. package/dist/{chunk-3HPAPHUK.js → chunk-6KYMPV2O.js} +12 -11
  14. package/dist/chunk-6KYMPV2O.js.map +1 -0
  15. package/dist/{chunk-S53OYO3F.js → chunk-7VFZTJ7K.js} +2 -2
  16. package/dist/{chunk-4RR6ROTB.js → chunk-AGNBY3VG.js} +2 -2
  17. package/dist/{chunk-YEEAADCI.js → chunk-AYHXQR53.js} +2 -2
  18. package/dist/{chunk-IEUU7O4F.js → chunk-BNW5NJJH.js} +2 -2
  19. package/dist/{chunk-6GMPIJAZ.js → chunk-C3IW2F5Z.js} +2 -2
  20. package/dist/{chunk-4EWRLK3C.js → chunk-C4PZTWTG.js} +16 -16
  21. package/dist/{chunk-QVO4YOB7.js → chunk-D2B22JDF.js} +2 -2
  22. package/dist/{chunk-HA5SI4GK.js → chunk-FMGWXIES.js} +4 -4
  23. package/dist/{chunk-B6SU7YSE.js → chunk-GLWW3EJQ.js} +5 -5
  24. package/dist/{chunk-5BTCT236.js → chunk-GYTVOLNX.js} +2 -2
  25. package/dist/{chunk-IMA6GU4Y.js → chunk-H3PHZLMF.js} +3 -3
  26. package/dist/chunk-H3PHZLMF.js.map +1 -0
  27. package/dist/{chunk-TIPYPLLQ.js → chunk-I6UCUHLK.js} +4 -4
  28. package/dist/{chunk-2I2MDQIB.js → chunk-I74SUMNI.js} +2 -2
  29. package/dist/chunk-I74SUMNI.js.map +1 -0
  30. package/dist/{chunk-4H5ZJHEN.js → chunk-J6A3CX5N.js} +8 -3
  31. package/dist/{chunk-4H5ZJHEN.js.map → chunk-J6A3CX5N.js.map} +1 -1
  32. package/dist/{chunk-DEVUWMME.js → chunk-KGIGRNR6.js} +2 -2
  33. package/dist/{chunk-F4QTFIB4.js → chunk-KQFQ3IS5.js} +6 -6
  34. package/dist/{chunk-QSVPYQPG.js → chunk-LDXUBPMO.js} +2 -2
  35. package/dist/chunk-LDXUBPMO.js.map +1 -0
  36. package/dist/{chunk-JFEKNTX7.js → chunk-LN4YGHTM.js} +6 -2
  37. package/dist/chunk-LN4YGHTM.js.map +1 -0
  38. package/dist/{chunk-7XYTQGCC.js → chunk-MAV46GWQ.js} +2 -2
  39. package/dist/{chunk-KILOTVIF.js → chunk-MB5RSUW6.js} +2 -2
  40. package/dist/{chunk-WB3LYXC5.js → chunk-MON3LMO7.js} +3 -3
  41. package/dist/{chunk-APRRL26Q.js → chunk-O4UNM6OR.js} +2 -2
  42. package/dist/{chunk-AZDOWD2L.js → chunk-OZXVGYGZ.js} +2 -2
  43. package/dist/{chunk-WCYKT2DE.js → chunk-P4BC54KI.js} +23 -14
  44. package/dist/chunk-P4BC54KI.js.map +1 -0
  45. package/dist/{chunk-7MLB4NCL.js → chunk-PJGB7XRR.js} +6 -6
  46. package/dist/chunk-PJGB7XRR.js.map +1 -0
  47. package/dist/{chunk-DEPRLVLK.js → chunk-QFQQFX2H.js} +3 -3
  48. package/dist/{chunk-DEPRLVLK.js.map → chunk-QFQQFX2H.js.map} +1 -1
  49. package/dist/{chunk-QPD426WT.js → chunk-R3OQGYOU.js} +2 -2
  50. package/dist/{chunk-UZB5KHKX.js → chunk-RGMVMVMF.js} +2 -2
  51. package/dist/chunk-RGMVMVMF.js.map +1 -0
  52. package/dist/{chunk-O3U5BPUP.js → chunk-RKW6QR7W.js} +23 -19
  53. package/dist/chunk-RKW6QR7W.js.map +1 -0
  54. package/dist/{chunk-C6C7XVKG.js → chunk-UGEBPVNI.js} +3 -3
  55. package/dist/{chunk-4WMCPJWX.js → chunk-UQ7RN5HK.js} +22 -13
  56. package/dist/chunk-UQ7RN5HK.js.map +1 -0
  57. package/dist/{chunk-XQNPGNKK.js → chunk-W3BKVM64.js} +2 -2
  58. package/dist/{chunk-K5O2QY6T.js → chunk-YTWNKQ2G.js} +2 -2
  59. package/dist/chunk-YTWNKQ2G.js.map +1 -0
  60. package/dist/{chunk-2SGJY2UY.js → chunk-Z3CCEP6F.js} +3 -3
  61. package/dist/{chunk-THTIZJZA.js → chunk-ZJSZNTEI.js} +4 -4
  62. package/dist/{chunk-CIOMS6DI.js → chunk-ZZPIJPPD.js} +2 -2
  63. package/dist/chunking.js +1 -1
  64. package/dist/cli.js +23 -23
  65. package/dist/compounding/engine.js +6 -6
  66. package/dist/connectors/codex-materialize-runner.js +7 -7
  67. package/dist/connectors/codex-materialize.js +1 -1
  68. package/dist/connectors/index.js +7 -7
  69. package/dist/contradiction/index.js +2 -2
  70. package/dist/{contradiction-scan-GD7KUFWS.js → contradiction-scan-AZTGFMPY.js} +3 -3
  71. package/dist/entity-retrieval.js +6 -6
  72. package/dist/explicit-capture.js +1 -1
  73. package/dist/extraction-judge.js +3 -3
  74. package/dist/extraction.js +3 -3
  75. package/dist/fallback-llm.js +2 -2
  76. package/dist/identity-continuity.js +1 -1
  77. package/dist/index.js +45 -42
  78. package/dist/index.js.map +1 -1
  79. package/dist/json-extract.js +1 -1
  80. package/dist/lcm/engine.js +3 -3
  81. package/dist/lcm/index.js +3 -3
  82. package/dist/lcm/schema.js +2 -2
  83. package/dist/maintenance/memory-governance.js +6 -6
  84. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +6 -6
  85. package/dist/maintenance/rebuild-memory-projection.js +7 -7
  86. package/dist/memory-projection-store.js +2 -2
  87. package/dist/namespaces/migrate.js +7 -7
  88. package/dist/namespaces/storage.js +6 -6
  89. package/dist/operator-toolkit.js +9 -9
  90. package/dist/orchestrator.js +25 -25
  91. package/dist/peers/index.js +1 -1
  92. package/dist/recall-planner-llm.js +2 -2
  93. package/dist/runtime/better-sqlite.d.ts +2 -1
  94. package/dist/runtime/better-sqlite.js +3 -1
  95. package/dist/schemas.d.ts +22 -22
  96. package/dist/semantic-chunking.js +2 -2
  97. package/dist/semantic-consolidation.js +8 -8
  98. package/dist/semantic-rule-promotion.js +6 -6
  99. package/dist/semantic-rule-verifier.js +6 -6
  100. package/dist/source-attribution.js +1 -1
  101. package/dist/storage.js +5 -5
  102. package/dist/summarizer.js +3 -3
  103. package/dist/temporal-supersession.js +1 -1
  104. package/dist/transfer/export-sqlite.js +2 -2
  105. package/dist/transfer/import-sqlite.js +2 -2
  106. package/dist/transfer/types.d.ts +12 -12
  107. package/dist/verified-recall.js +6 -6
  108. package/package.json +1 -1
  109. package/src/chunking.ts +38 -23
  110. package/src/coding/review-context.ts +7 -1
  111. package/src/connectors/codex-materialize.ts +6 -1
  112. package/src/explicit-capture.ts +7 -2
  113. package/src/identity-continuity.ts +7 -1
  114. package/src/json-extract.ts +4 -1
  115. package/src/orchestrator.ts +5 -1
  116. package/src/peers/profile-reasoner.ts +4 -1
  117. package/src/runtime/better-sqlite.test.ts +29 -0
  118. package/src/runtime/better-sqlite.ts +30 -8
  119. package/src/semantic-chunking.ts +32 -16
  120. package/src/semantic-consolidation.ts +4 -1
  121. package/src/source-attribution.test.ts +21 -0
  122. package/src/source-attribution.ts +17 -2
  123. package/src/storage.ts +11 -2
  124. package/src/temporal-supersession.ts +4 -1
  125. package/dist/chunk-2I2MDQIB.js.map +0 -1
  126. package/dist/chunk-3HPAPHUK.js.map +0 -1
  127. package/dist/chunk-4WMCPJWX.js.map +0 -1
  128. package/dist/chunk-7MLB4NCL.js.map +0 -1
  129. package/dist/chunk-IMA6GU4Y.js.map +0 -1
  130. package/dist/chunk-JFEKNTX7.js.map +0 -1
  131. package/dist/chunk-K5O2QY6T.js.map +0 -1
  132. package/dist/chunk-O3U5BPUP.js.map +0 -1
  133. package/dist/chunk-QSVPYQPG.js.map +0 -1
  134. package/dist/chunk-UZB5KHKX.js.map +0 -1
  135. package/dist/chunk-WCYKT2DE.js.map +0 -1
  136. /package/dist/{chunk-GLPBYIXN.js.map → chunk-2L54V4ZO.js.map} +0 -0
  137. /package/dist/{chunk-PP2JH3GP.js.map → chunk-2UFQYU5F.js.map} +0 -0
  138. /package/dist/{chunk-XAZOWLW4.js.map → chunk-3VONWEQB.js.map} +0 -0
  139. /package/dist/{chunk-BF7ZRHH2.js.map → chunk-66SLUXKM.js.map} +0 -0
  140. /package/dist/{chunk-S53OYO3F.js.map → chunk-7VFZTJ7K.js.map} +0 -0
  141. /package/dist/{chunk-4RR6ROTB.js.map → chunk-AGNBY3VG.js.map} +0 -0
  142. /package/dist/{chunk-YEEAADCI.js.map → chunk-AYHXQR53.js.map} +0 -0
  143. /package/dist/{chunk-IEUU7O4F.js.map → chunk-BNW5NJJH.js.map} +0 -0
  144. /package/dist/{chunk-6GMPIJAZ.js.map → chunk-C3IW2F5Z.js.map} +0 -0
  145. /package/dist/{chunk-4EWRLK3C.js.map → chunk-C4PZTWTG.js.map} +0 -0
  146. /package/dist/{chunk-QVO4YOB7.js.map → chunk-D2B22JDF.js.map} +0 -0
  147. /package/dist/{chunk-HA5SI4GK.js.map → chunk-FMGWXIES.js.map} +0 -0
  148. /package/dist/{chunk-B6SU7YSE.js.map → chunk-GLWW3EJQ.js.map} +0 -0
  149. /package/dist/{chunk-5BTCT236.js.map → chunk-GYTVOLNX.js.map} +0 -0
  150. /package/dist/{chunk-TIPYPLLQ.js.map → chunk-I6UCUHLK.js.map} +0 -0
  151. /package/dist/{chunk-DEVUWMME.js.map → chunk-KGIGRNR6.js.map} +0 -0
  152. /package/dist/{chunk-F4QTFIB4.js.map → chunk-KQFQ3IS5.js.map} +0 -0
  153. /package/dist/{chunk-7XYTQGCC.js.map → chunk-MAV46GWQ.js.map} +0 -0
  154. /package/dist/{chunk-KILOTVIF.js.map → chunk-MB5RSUW6.js.map} +0 -0
  155. /package/dist/{chunk-WB3LYXC5.js.map → chunk-MON3LMO7.js.map} +0 -0
  156. /package/dist/{chunk-APRRL26Q.js.map → chunk-O4UNM6OR.js.map} +0 -0
  157. /package/dist/{chunk-AZDOWD2L.js.map → chunk-OZXVGYGZ.js.map} +0 -0
  158. /package/dist/{chunk-QPD426WT.js.map → chunk-R3OQGYOU.js.map} +0 -0
  159. /package/dist/{chunk-C6C7XVKG.js.map → chunk-UGEBPVNI.js.map} +0 -0
  160. /package/dist/{chunk-XQNPGNKK.js.map → chunk-W3BKVM64.js.map} +0 -0
  161. /package/dist/{chunk-2SGJY2UY.js.map → chunk-Z3CCEP6F.js.map} +0 -0
  162. /package/dist/{chunk-THTIZJZA.js.map → chunk-ZJSZNTEI.js.map} +0 -0
  163. /package/dist/{chunk-CIOMS6DI.js.map → chunk-ZZPIJPPD.js.map} +0 -0
  164. /package/dist/{contradiction-scan-GD7KUFWS.js.map → contradiction-scan-AZTGFMPY.js.map} +0 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/source-attribution.ts"],"sourcesContent":["/**\n * Inline Source Attribution Protocol (issue #369)\n *\n * Extracted facts carry provenance inline inside the fact body, so the\n * citation survives hostile memory text, copy/paste, and LLM quoting. This\n * complements — never replaces — the YAML frontmatter provenance stored on\n * disk.\n *\n * Default format (matches issue #369 proposal):\n *\n * The foo service uses Redis for rate limiting. [Source: agent=planner, session=abc123, ts=2026-04-10T14:25:07Z]\n *\n * Key properties:\n * - Inline (part of the body, not metadata).\n * - Compact (typically <80 chars of overhead per fact).\n * - Machine-parseable by a single regex.\n * - Opt-in via `inlineSourceAttributionEnabled` config flag (default off\n * for backwards compatibility with existing downstream consumers).\n * - Legacy facts without a citation remain fully readable.\n *\n * The format template is configurable via `inlineSourceAttributionFormat`\n * with supported placeholders:\n *\n * {agent} — principal / agent identifier\n * {session} — full session key (colon-delimited)\n * {sessionId} — short stable session id (trailing component)\n * {ts} — extraction timestamp (ISO 8601)\n * {date} — extraction date (YYYY-MM-DD)\n *\n * Any privacy-sensitive identifiers should be normalized before being passed\n * to `formatCitation` — the helper treats them as opaque strings.\n */\n\n/** Default citation format template (matches issue #369). */\nexport const DEFAULT_CITATION_FORMAT =\n \"[Source: agent={agent}, session={sessionId}, ts={ts}]\";\n\n/** Sentinel value used when a provenance field is missing. */\nexport const CITATION_UNKNOWN = \"unknown\";\n\nexport interface CitationContext {\n /** Principal / agent identifier (e.g. resolved via resolvePrincipal). */\n agent?: string;\n /** Full session key (e.g. \"agent:planner:main\"). */\n session?: string;\n /**\n * Opaque short session id. Derived from the trailing component of the\n * session key when not provided explicitly. Use this for compact formats\n * that do not need the full colon-delimited session key.\n */\n sessionId?: string;\n /** Extraction timestamp as an ISO 8601 string. */\n ts?: string;\n}\n\nexport interface ParsedCitation {\n /** Agent identifier parsed from the citation (never crashes on malformed input). */\n agent?: string;\n /** Session identifier parsed from the citation. */\n session?: string;\n /** Extraction timestamp parsed from the citation. */\n ts?: string;\n /** The full matched citation substring. */\n raw: string;\n}\n\n/**\n * Regex that matches the default `[Source: agent=X, session=Y, ts=Z]` shape\n * as well as human-edited variants (extra whitespace, reordered fields,\n * subset of fields). Matches non-greedily so it can be anchored anywhere in\n * the text. Kept as a getter factory so callers do not share regex state.\n */\nfunction defaultCitationMatcher(): RegExp {\n return /\\[Source:\\s*([^\\]\\n]+?)\\]/gi;\n}\n\n/**\n * Derive a short session id from a full session key.\n * Falls back to the raw session string if no colon is present.\n */\nexport function deriveSessionId(session: string | undefined): string | undefined {\n if (!session) return undefined;\n const trimmed = session.trim();\n if (trimmed.length === 0) return undefined;\n const parts = trimmed.split(\":\").filter((p) => p.length > 0);\n if (parts.length === 0) return trimmed;\n return parts[parts.length - 1];\n}\n\n/**\n * Format an inline citation tag using the provided template.\n *\n * Missing context fields fall back to {@link CITATION_UNKNOWN} — the caller\n * should always get a non-empty, parseable tag.\n *\n * Uses a single-pass substitution so that values which themselves contain\n * placeholder syntax (e.g. an agent literally named `\"{ts}\"`) cannot be\n * re-interpreted by subsequent replacement steps. Each placeholder slot\n * receives exactly one lookup and the substituted value is treated as\n * terminal text, not template source.\n */\nexport function formatCitation(\n ctx: CitationContext,\n template: string = DEFAULT_CITATION_FORMAT,\n): string {\n const session = ctx.session ?? \"\";\n const sessionId = ctx.sessionId ?? deriveSessionId(session) ?? CITATION_UNKNOWN;\n const ts = ctx.ts ?? CITATION_UNKNOWN;\n const agent = ctx.agent && ctx.agent.trim().length > 0 ? ctx.agent : CITATION_UNKNOWN;\n const date = ts && ts !== CITATION_UNKNOWN ? ts.slice(0, 10) : CITATION_UNKNOWN;\n const sessionForTemplate = session.trim().length > 0 ? session : CITATION_UNKNOWN;\n\n // Map from recognised placeholder names to their resolved value. Unknown\n // placeholder names are left intact (returning the original `{name}`).\n const values: Record<string, string> = {\n agent,\n session: sessionForTemplate,\n sessionId,\n ts,\n date,\n };\n\n // Single-pass scan: replace every recognised `{name}` in one sweep so that\n // substituted values cannot themselves be treated as template source on a\n // subsequent pass. The replacer-function form also guarantees that `$` /\n // `$&` / `$1` sequences inside values are emitted literally.\n return template.replace(/\\{([a-zA-Z_][\\w]*)\\}/g, (match, name: string) => {\n return Object.prototype.hasOwnProperty.call(values, name)\n ? values[name]!\n : match;\n });\n}\n\n/**\n * Returns true if the text already carries at least one citation marker.\n * Safe to call on any string — never throws.\n */\nexport function hasCitation(text: string): boolean {\n if (typeof text !== \"string\" || text.length === 0) return false;\n return defaultCitationMatcher().test(text);\n}\n\n/**\n * Escape a string for use as a regex literal.\n */\nfunction escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Escape a single character for use INSIDE a character class `[...]`.\n * Special chars inside character classes: `]`, `\\`, `^`, `-`.\n */\nfunction escapeRegExpCharClass(ch: string): string {\n if (ch === \"]\") return \"\\\\]\";\n if (ch === \"\\\\\") return \"\\\\\\\\\";\n if (ch === \"^\") return \"\\\\^\";\n if (ch === \"-\") return \"\\\\-\";\n // Other regex meta chars are NOT special inside [...] but escaping them is safe.\n return escapeRegExp(ch);\n}\n\n/**\n * Build a per-placeholder token pattern that excludes newlines, whitespace,\n * and any punctuation characters used as inner separators in the template.\n *\n * This prevents a placeholder span from consuming separator bytes and\n * matching strings that cross separator boundaries in user-supplied content.\n *\n * @param nonWordSepChars - Set of non-word (non-alphanumeric, non-`_`) chars\n * extracted from the inner separator literals of the template.\n */\nfunction buildTokenPattern(nonWordSepChars: Set<string>): string {\n // Always exclude newlines and whitespace.\n const base = \"\\\\n\\\\s\";\n if (nonWordSepChars.size === 0) {\n // No inner separator punctuation — the placeholder spans the full space\n // between prefix and suffix. Fall back to a generous no-newline match.\n return `[^\\\\n]+?`;\n }\n const escaped = [...nonWordSepChars].map(escapeRegExpCharClass).join(\"\");\n return `[^${base}${escaped}]+?`;\n}\n\n/** Regex that matches a `{placeholder}` token inside a template string. */\nconst PLACEHOLDER_REGEX = /\\{[a-zA-Z_][\\w]*\\}/g;\n\n/**\n * Build a regex that matches a citation produced by the given template.\n *\n * The approach depends on the shape of the template:\n *\n * - **Normal case (non-empty literal prefix or suffix).** Anchor the match\n * on the outer literal frame and reconstruct the interior as\n * `interToken + sep + interToken + sep + ... + sep + lastToken`.\n * All **intermediate** per-placeholder tokens exclude the combined set of\n * non-word separator characters used between any two adjacent placeholders,\n * preventing a value from consuming a separator and crossing placeholder\n * boundaries. The **last** token is only required to avoid newlines because\n * it is terminated by the literal suffix anchor — this lets placeholder\n * values that legitimately contain a separator character be recognised (e.g.\n * an ISO-8601 timestamp `2026-04-10T14:25:07Z` in `[src:{agent}:{ts}]`\n * where `:` is the inter-placeholder separator). A template like\n * `[src:{agent}/{sessionId}@{date}]` emits\n * `\\[src:[^\\n\\s/@]+?\\/[^\\n\\s/@]+?@[^\\n]+?\\]` so that `[src:foo/bar]`\n * is NOT matched (wrong separator count), `[src:foo/bar/extra@2026]`\n * is NOT matched (intermediate token crosses a `/` boundary), and\n * `[src:planner/main@2026-04-10]` IS matched correctly.\n *\n * - **Placeholder-bounded with whitespace separator.** Both prefix and\n * suffix are empty and the separator literal(s) between placeholders\n * contain at least one whitespace character (e.g. `{source}: {content}`,\n * `{agent} {sessionId}`). A whitespace-containing separator produces\n * output that is visually indistinguishable from ordinary prose, so the\n * safe strategy is to require a **hard bracket/paren/angle delimiter** on\n * both sides of the reconstructed match. Prose almost never places\n * `[...]` / `(...)` / `<...>` around a phrase, so this yields clean\n * false-positive rejection.\n *\n * - **Placeholder-bounded with compact (non-whitespace) separator.** Both\n * prefix and suffix are empty and the separator literal(s) contain NO\n * whitespace (e.g. `{agent}:{sessionId}`, `{agent}/{sessionId}`).\n * `formatCitation` emits a compact token like `planner:main` with no\n * surrounding delimiters, so the bracket strategy cannot detect it.\n * Instead, the pattern requires that the entire token is bordered by\n * whitespace or a bracket/paren/angle on each side:\n *\n * `(?:(?<=[\\[\\(\\<])|(?<!\\S))[\\w.-]+<sep>[\\w.-]+(?:(?=[\\]\\)\\>])|(?!\\S))`\n *\n * This accepts `planner:main` when it appears standalone or inside a\n * bracket-wrapped token, and rejects `host:80` embedded inside a URL like\n * `http://host:80` because `host` is immediately preceded by `/`\n * (non-whitespace, non-bracket).\n *\n * - **All-placeholder case (no literals between placeholders either).** No\n * reliable regex can be built — a template like `{agent}{sessionId}`\n * contains no anchor characters. Returns `null`; {@link\n * hasCitationForTemplate} treats this as \"cannot detect\" and returns\n * false, falling back on explicit sentinel/format detection only for the\n * default `[Source: ...]` shape.\n *\n * Returns `null` when the template has no placeholders (fully-literal\n * citation, handled by the string-equality fast path in {@link\n * hasCitationForTemplate}) **or** when the template is entirely placeholder-\n * only with no literal content whatsoever.\n */\nfunction templateMatcher(template: string): RegExp | null {\n // Split around all {placeholder} tokens.\n const parts = template.split(PLACEHOLDER_REGEX);\n if (parts.length <= 1) return null;\n\n const prefix = parts[0] ?? \"\";\n const suffix = parts[parts.length - 1] ?? \"\";\n\n // Normal case: at least one literal frame on the outside.\n // Tighten the per-placeholder token so it cannot consume separator\n // characters and match strings that cross separator boundaries\n // (Finding 3 — Uru3).\n if (prefix.length > 0 || suffix.length > 0) {\n const escapedPrefix = escapeRegExp(prefix);\n const escapedSuffix = escapeRegExp(suffix);\n\n // Inner parts: literal separators that sit between adjacent placeholders.\n // For `[src:{agent}/{sessionId}@{date}]`, parts = [\"[src:\", \"/\", \"@\", \"]\"]\n // so innerParts = [\"/\", \"@\"].\n const innerParts = parts.slice(1, -1);\n\n // Collect only the non-word (punctuation/symbol) characters from each\n // inner separator so alphabetic separator text (unlikely but valid) does\n // not exclude letters from the per-placeholder token pattern.\n const nonWordSepChars = new Set<string>();\n for (const sep of innerParts) {\n for (const ch of sep) {\n if (!/\\w/.test(ch)) {\n nonWordSepChars.add(ch);\n }\n }\n }\n\n // All intermediate tokens (every placeholder except the last) use the\n // combined exclusion so they cannot cross placeholder boundaries.\n //\n // The LAST token is different: it is terminated by the literal suffix anchor\n // (e.g. `\\]`), so it does not need to exclude inner-separator characters.\n // Dropping that restriction lets placeholder values that legitimately contain\n // a separator character (e.g. an ISO-8601 timestamp `2026-04-10T14:25:07Z`\n // in template `[src:{agent}:{ts}]`) be recognised correctly instead of\n // producing false-negative misses that trigger duplicate citation injection.\n //\n // Only the LAST token is relaxed. Intermediate tokens keep the combined\n // exclusion so that cross-boundary false positives are still rejected\n // (e.g. `[src:foo/bar/extra@2026-04-11]` for `[src:{a}/{b}@{c}]`).\n const interToken = buildTokenPattern(nonWordSepChars);\n // Last token: terminated by suffix anchor — exclude only newlines.\n const lastToken = buildTokenPattern(new Set<string>());\n\n // Reconstruct the interior: interToken sep interToken sep ... sep lastToken\n // (or just lastToken when there are no inner separators at all).\n const middle =\n innerParts.length === 0\n ? lastToken\n : interToken +\n innerParts\n .slice(0, -1)\n .map((sep) => escapeRegExp(sep) + interToken)\n .join(\"\") +\n escapeRegExp(innerParts[innerParts.length - 1]!) +\n lastToken;\n\n const pattern = escapedPrefix + middle + escapedSuffix;\n return new RegExp(pattern, \"i\");\n }\n\n // Placeholder-bounded case: prefix and suffix are both empty.\n const middleLiterals = parts.slice(1, -1);\n const hasNonEmptyMiddle = middleLiterals.some((p) => p.length > 0);\n if (!hasNonEmptyMiddle) {\n // All-placeholder template with no literal content. Impossible to anchor\n // reliably without sentinel markers; signal the caller.\n return null;\n }\n\n // Identifier token: one or more word chars, dots, dashes, or colons.\n // Colons are included to allow timestamp values like \"10:30\" or session\n // keys like \"agent:planner:main\" inside compact placeholder-bounded\n // templates. URL-like fragments (`http://host:80`) are still rejected\n // because the lead anchor requires whitespace or a bracket immediately\n // before the first id-token group (`http` is preceded by `/`).\n const idToken = \"[\\\\w.:-]+\";\n const body =\n idToken +\n middleLiterals.map((lit) => escapeRegExp(lit) + idToken).join(\"\");\n\n const separatorText = middleLiterals.join(\"\");\n if (/\\s/.test(separatorText)) {\n // Separator contains whitespace: the emitted citation looks like ordinary\n // prose (e.g. `planner main`). Require a hard bracket/paren/angle\n // delimiter on both sides to prevent false matches on English text.\n const opener = \"[\\\\[\\\\(\\\\<]\";\n const closer = \"[\\\\]\\\\)\\\\>]\";\n return new RegExp(opener + body + closer, \"i\");\n }\n\n // Separator is compact (no whitespace): `formatCitation` emits a token like\n // `planner:main` without surrounding delimiters. The challenge is that the\n // same token shape also matches ordinary hyphenated or slashed prose words\n // (e.g. `long-term`, `docs/setup`), causing `hasCitationForTemplate` to\n // return true on uncited fact bodies and silently suppress citation injection\n // from `attachCitation`.\n //\n // Fix (Finding 1): tighten the trail anchor so a bare compact token is only\n // accepted when it sits at the very end of the string (possibly followed by\n // optional trailing whitespace or a newline). Since `attachCitation` always\n // appends the citation at the trimmed end of the fact body, a real citation\n // token will always appear at the tail. Prose like `\"long-term solution\"`\n // has `long-term` in the middle of the string (followed by ` solution`), so\n // the end-of-string anchor rejects it — no false positive, no silent drop.\n //\n // The lead anchor still accepts either a bracket opener or a whitespace\n // boundary (or start of string), so `\"Fact. planner:main\"` and standalone\n // `\"planner:main\"` are both detected after the first attachment pass.\n //\n // Bracket-wrapped form (e.g. `[planner:main]`) is also accepted via the\n // opener/closer pair — bracket still takes precedence over end-of-string.\n //\n // Example — why `http://host:80` does NOT match:\n // Trying to match `host:80`: the char before `h` is `/` (non-whitespace,\n // non-bracket), so `(?<=[\\[\\(\\<])` and `(?<!\\S)` both fail ⟹ no match.\n // Trying to match `http:...`: after `http:` the next chars are `//` which\n // are not `[\\w.-]+`, so the second id-token group fails ⟹ no match.\n const leadAnchor = \"(?:(?<=[\\\\[\\\\(\\\\<])|(?<!\\\\S))\";\n // Trail: either a bracket closer (for `[token]` shape) or end-of-string\n // optionally preceded by whitespace. The `(?!\\S)` is deliberately removed\n // so that a compact token in the MIDDLE of a sentence does not match.\n const trailAnchor = \"(?:(?=[\\\\]\\\\)\\\\>])|(?=\\\\s*$))\";\n return new RegExp(leadAnchor + body + trailAnchor, \"i\");\n}\n\n/**\n * Returns true if `text` already carries a citation produced by `template`\n * **or** by the default `[Source: ...]` format (for facts that were tagged\n * before a config change).\n *\n * Use this instead of {@link hasCitation} whenever the caller has access to\n * the configured `inlineSourceAttributionFormat`.\n *\n * All-placeholder templates such as `{agent}{sessionId}` have no literal\n * content to anchor on and therefore cannot be reliably detected without\n * dedicated sentinel markers. In that case the function returns `false` —\n * callers that need idempotent dedup for such templates should either adopt\n * a template with literal delimiters (recommended) or rely on the default\n * `[Source: ...]` marker detection which is always available via\n * {@link hasCitation}.\n */\nexport function hasCitationForTemplate(text: string, template: string): boolean {\n if (typeof text !== \"string\" || text.length === 0) return false;\n // Always accept the default format as a fallback so facts tagged before a\n // configuration change are not double-tagged on reprocessing.\n if (hasCitation(text)) return true;\n // If the configured template matches the default, we're done.\n //\n // Known limitation (Thread 2 — Codex P2): this fast path exits without\n // checking whether the content carries a citation from a DIFFERENT custom\n // template that was active before the config was changed back to the default.\n // Such a fact would be detected by `hasCitation` above only if the prior\n // custom template happened to match the default `[Source: ...]` pattern.\n // In practice, template changes mid-stream are rare, and the false-negative\n // (missing an old custom citation) produces a benign duplicate citation rather\n // than data loss. A full fix would require storing the citation template used\n // at write time in the frontmatter and checking that here.\n if (template === DEFAULT_CITATION_FORMAT) return false;\n\n // Fully-literal template (no placeholders): exact inclusion check.\n if (!PLACEHOLDER_REGEX.test(template)) {\n // Reset lastIndex because PLACEHOLDER_REGEX is declared with /g.\n PLACEHOLDER_REGEX.lastIndex = 0;\n return text.includes(template);\n }\n // Reset lastIndex after the .test() probe above.\n PLACEHOLDER_REGEX.lastIndex = 0;\n\n const matcher = templateMatcher(template);\n if (!matcher) {\n // All-placeholder template: cannot build a reliable matcher. See the\n // docstring — callers should not rely on dedup for this shape.\n return false;\n }\n return matcher.test(text);\n}\n\n/**\n * Attach an inline citation to fact text.\n *\n * If the text already has a citation — either the default `[Source: ...]`\n * marker or one produced by the configured template — it is returned unchanged.\n * Existing provenance is respected and never overwritten. Otherwise the\n * citation is appended to the trimmed text with a single space separator,\n * which keeps the marker visually adjacent to the fact body.\n */\nexport function attachCitation(\n text: string,\n ctx: CitationContext,\n template: string = DEFAULT_CITATION_FORMAT,\n): string {\n if (typeof text !== \"string\") return text as unknown as string;\n if (hasCitationForTemplate(text, template)) return text;\n const trimmedEnd = text.replace(/\\s+$/u, \"\");\n if (trimmedEnd.length === 0) return text;\n const citation = formatCitation(ctx, template);\n // Preserve any trailing newline that callers rely on for markdown rendering.\n const trailing = text.slice(trimmedEnd.length);\n return `${trimmedEnd} ${citation}${trailing}`;\n}\n\n/**\n * Parse a single inline citation from a piece of text. Returns the first\n * citation encountered or `null` when none is present. Malformed citations\n * do not throw — fields that cannot be parsed simply remain `undefined`.\n */\nexport function parseCitation(text: string): ParsedCitation | null {\n if (typeof text !== \"string\" || text.length === 0) return null;\n const matcher = defaultCitationMatcher();\n const match = matcher.exec(text);\n if (!match) return null;\n\n const body = match[1] ?? \"\";\n const raw = match[0] ?? \"\";\n const parsed: ParsedCitation = { raw };\n\n const fields = body\n .split(\",\")\n .map((segment) => segment.trim())\n .filter((segment) => segment.length > 0);\n\n for (const field of fields) {\n const eqIdx = field.indexOf(\"=\");\n if (eqIdx <= 0) continue;\n const key = field.slice(0, eqIdx).trim().toLowerCase();\n const value = field.slice(eqIdx + 1).trim();\n if (value.length === 0) continue;\n switch (key) {\n case \"agent\":\n parsed.agent = value;\n break;\n case \"session\":\n case \"sessionid\":\n parsed.session = value;\n break;\n case \"ts\":\n case \"timestamp\":\n parsed.ts = value;\n break;\n default:\n // Unknown fields are ignored defensively so human edits never crash.\n break;\n }\n }\n\n return parsed;\n}\n\n/**\n * Parse every citation embedded in the text. Always returns an array; empty\n * when none are present.\n */\nexport function parseAllCitations(text: string): ParsedCitation[] {\n if (typeof text !== \"string\" || text.length === 0) return [];\n const matcher = defaultCitationMatcher();\n const results: ParsedCitation[] = [];\n let match: RegExpExecArray | null;\n while ((match = matcher.exec(text)) !== null) {\n const parsed = parseCitation(match[0]);\n if (parsed) results.push(parsed);\n }\n return results;\n}\n\n/**\n * Remove all inline citations from a piece of text.\n *\n * Callers that want the raw fact body (for dedup hashing, display, or\n * comparison) should use this helper instead of hand-rolled regexes so the\n * whole codebase agrees on the citation syntax.\n *\n * Finding 2 fix: when the input contains no citation marker, the input is\n * returned byte-for-byte unchanged. When a citation IS removed, whitespace\n * normalization is applied only at each join seam (the single space between\n * the preceding text and where the citation was), rather than across the\n * entire string. This preserves markdown hard-break spacing, aligned text,\n * and code-like snippets in fact bodies that happen to carry a citation.\n *\n * Implementation: each citation match is replaced by its \"seam fix\" — the\n * content before the match has its trailing whitespace trimmed and then a\n * single space is appended if any text remains, collapsing only the gap\n * left by the removed marker. Whitespace elsewhere in the body is untouched.\n */\nexport function stripCitation(text: string): string {\n if (typeof text !== \"string\" || text.length === 0) return text;\n // Early exit: no citation marker present — return the input unchanged so\n // that callers never lose formatting fidelity on uncited strings.\n if (!hasCitation(text)) return text;\n\n // Walk through all citations and slice them out one by one so that we can\n // normalise ONLY the whitespace at each seam rather than the entire string.\n const matcher = defaultCitationMatcher();\n let result = \"\";\n let lastIndex = 0;\n\n let match: RegExpExecArray | null;\n while ((match = matcher.exec(text)) !== null) {\n // Text before this citation. Trim trailing spaces/tabs at the seam only.\n const before = text.slice(lastIndex, match.index).replace(/[ \\t]+$/, \"\");\n result += before;\n lastIndex = match.index + match[0].length;\n }\n\n // Append any trailing text after the last citation. Trim leading\n // spaces/tabs and trailing whitespace at the join seam.\n const after = text.slice(lastIndex).replace(/^[ \\t]+/, \"\");\n if (after.length > 0) {\n if (result.length > 0) result += \" \";\n result += after;\n }\n\n return result.trimEnd();\n}\n\n/**\n * Strip an inline citation from text using a specific template regex.\n *\n * This is the template-aware counterpart to {@link stripCitation}. When the\n * caller holds the configured `inlineSourceAttributionFormat`, use this\n * function to strip citations produced by that template — including custom\n * templates that differ from the default `[Source: ...]` pattern.\n *\n * Behaviour:\n * - If the text has a **default-format** citation, delegates to\n * {@link stripCitation} (always safe).\n * - If the text has a **custom-template** citation detected by\n * `hasCitationForTemplate`, builds the template regex and removes every\n * occurrence (citations are appended at the end of the fact body by\n * {@link attachCitation}).\n * - All-placeholder templates (no literal prefix/suffix/separator) cannot\n * produce a reliable matcher. `hasCitationForTemplate` already returns\n * `false` for such templates, so this function never attempts to strip an\n * undetectable citation. The text is returned unchanged when no citation\n * is detected.\n * - If no citation is detected for the given template, returns the text\n * unchanged.\n *\n * @returns The stripped text (or the original text when no citation is found).\n */\nexport function stripCitationForTemplate(\n text: string,\n template: string,\n): string {\n if (typeof text !== \"string\" || text.length === 0) return text;\n\n // Fast path: default-format citation — delegate to the existing stripper.\n if (hasCitation(text)) return stripCitation(text);\n\n // No default citation; check whether the custom template produced one.\n // hasCitationForTemplate returns false for all-placeholder templates (no\n // reliable matcher), so those pass through unchanged below.\n if (!hasCitationForTemplate(text, template)) return text;\n\n // Build the template matcher. hasCitationForTemplate already returned true,\n // which means templateMatcher produced a non-null result. The null branch\n // here is a defensive fallback only — delegate to stripCitation.\n const matcher = templateMatcher(template);\n if (!matcher) return stripCitation(text);\n\n // The matcher regex was built without the global flag; add it for exec loop.\n const globalMatcher = new RegExp(\n matcher.source,\n matcher.flags.includes(\"g\") ? matcher.flags : matcher.flags + \"g\",\n );\n let result = \"\";\n let lastIndex = 0;\n let match: RegExpExecArray | null;\n\n while ((match = globalMatcher.exec(text)) !== null) {\n const matchEnd = match.index + match[0].length;\n const enclosure = enclosingDelimiterRange(text, match.index, matchEnd);\n const removalStart = enclosure?.start ?? match.index;\n const removalEnd = enclosure?.end ?? matchEnd;\n const before = text.slice(lastIndex, removalStart).replace(/[ \\t]+$/, \"\");\n result += before;\n lastIndex = removalEnd;\n // Guard against zero-width matches causing an infinite loop.\n if (match[0].length === 0) {\n globalMatcher.lastIndex++;\n }\n }\n\n const after = text.slice(lastIndex).replace(/^[ \\t]+/, \"\");\n if (after.length > 0) {\n if (result.length > 0) result += \" \";\n result += after;\n }\n\n return result.trimEnd();\n}\n\nfunction enclosingDelimiterRange(\n text: string,\n start: number,\n end: number,\n): { start: number; end: number } | undefined {\n if (start <= 0 || end >= text.length) return undefined;\n const opener = text[start - 1];\n const closer = text[end];\n if (\n (opener === \"[\" && closer === \"]\") ||\n (opener === \"(\" && closer === \")\") ||\n (opener === \"<\" && closer === \">\")\n ) {\n return { start: start - 1, end: end + 1 };\n }\n return undefined;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/source-attribution.ts"],"sourcesContent":["/**\n * Inline Source Attribution Protocol (issue #369)\n *\n * Extracted facts carry provenance inline inside the fact body, so the\n * citation survives hostile memory text, copy/paste, and LLM quoting. This\n * complements — never replaces — the YAML frontmatter provenance stored on\n * disk.\n *\n * Default format (matches issue #369 proposal):\n *\n * The foo service uses Redis for rate limiting. [Source: agent=planner, session=abc123, ts=2026-04-10T14:25:07Z]\n *\n * Key properties:\n * - Inline (part of the body, not metadata).\n * - Compact (typically <80 chars of overhead per fact).\n * - Machine-parseable by a single regex.\n * - Opt-in via `inlineSourceAttributionEnabled` config flag (default off\n * for backwards compatibility with existing downstream consumers).\n * - Legacy facts without a citation remain fully readable.\n *\n * The format template is configurable via `inlineSourceAttributionFormat`\n * with supported placeholders:\n *\n * {agent} — principal / agent identifier\n * {session} — full session key (colon-delimited)\n * {sessionId} — short stable session id (trailing component)\n * {ts} — extraction timestamp (ISO 8601)\n * {date} — extraction date (YYYY-MM-DD)\n *\n * Any privacy-sensitive identifiers should be normalized before being passed\n * to `formatCitation` — the helper treats them as opaque strings.\n */\n\n/** Default citation format template (matches issue #369). */\nexport const DEFAULT_CITATION_FORMAT =\n \"[Source: agent={agent}, session={sessionId}, ts={ts}]\";\n\n/** Sentinel value used when a provenance field is missing. */\nexport const CITATION_UNKNOWN = \"unknown\";\n\nexport interface CitationContext {\n /** Principal / agent identifier (e.g. resolved via resolvePrincipal). */\n agent?: string;\n /** Full session key (e.g. \"agent:planner:main\"). */\n session?: string;\n /**\n * Opaque short session id. Derived from the trailing component of the\n * session key when not provided explicitly. Use this for compact formats\n * that do not need the full colon-delimited session key.\n */\n sessionId?: string;\n /** Extraction timestamp as an ISO 8601 string. */\n ts?: string;\n}\n\nexport interface ParsedCitation {\n /** Agent identifier parsed from the citation (never crashes on malformed input). */\n agent?: string;\n /** Session identifier parsed from the citation. */\n session?: string;\n /** Extraction timestamp parsed from the citation. */\n ts?: string;\n /** The full matched citation substring. */\n raw: string;\n}\n\n/**\n * Regex that matches the default `[Source: agent=X, session=Y, ts=Z]` shape\n * as well as human-edited variants (extra whitespace, reordered fields,\n * subset of fields). Matches non-greedily so it can be anchored anywhere in\n * the text. Kept as a getter factory so callers do not share regex state.\n */\nfunction defaultCitationMatcher(): RegExp {\n // Bounded repetition {1,1024} instead of + so the match cannot backtrack\n // polynomially over hostile memory text (CodeQL js/polynomial-redos). A real\n // citation is far shorter than 1024 chars, so this is behavior-preserving for\n // any genuine [Source: …] block; only pathological/oversized input is excluded.\n return /\\[Source:([^\\]\\n]{1,1024})\\]/gi;\n}\n\n// Linear trailing-whitespace trim. Replaces text.replace(/\\s+$/u, \"\"), whose\n// anchored quantifier backtracks polynomially on long inputs (CodeQL\n// js/polynomial-redos). Matches the exact \\s (with u flag) semantics one char\n// at a time, so the trailing-content preservation logic in attachCitation is\n// unaffected.\nfunction trimTrailingWhitespace(text: string): string {\n let end = text.length;\n while (end > 0 && /\\s/u.test(text[end - 1]!)) end--;\n return text.slice(0, end);\n}\n\n/**\n * Derive a short session id from a full session key.\n * Falls back to the raw session string if no colon is present.\n */\nexport function deriveSessionId(session: string | undefined): string | undefined {\n if (!session) return undefined;\n const trimmed = session.trim();\n if (trimmed.length === 0) return undefined;\n const parts = trimmed.split(\":\").filter((p) => p.length > 0);\n if (parts.length === 0) return trimmed;\n return parts[parts.length - 1];\n}\n\n/**\n * Format an inline citation tag using the provided template.\n *\n * Missing context fields fall back to {@link CITATION_UNKNOWN} — the caller\n * should always get a non-empty, parseable tag.\n *\n * Uses a single-pass substitution so that values which themselves contain\n * placeholder syntax (e.g. an agent literally named `\"{ts}\"`) cannot be\n * re-interpreted by subsequent replacement steps. Each placeholder slot\n * receives exactly one lookup and the substituted value is treated as\n * terminal text, not template source.\n */\nexport function formatCitation(\n ctx: CitationContext,\n template: string = DEFAULT_CITATION_FORMAT,\n): string {\n const session = ctx.session ?? \"\";\n const sessionId = ctx.sessionId ?? deriveSessionId(session) ?? CITATION_UNKNOWN;\n const ts = ctx.ts ?? CITATION_UNKNOWN;\n const agent = ctx.agent && ctx.agent.trim().length > 0 ? ctx.agent : CITATION_UNKNOWN;\n const date = ts && ts !== CITATION_UNKNOWN ? ts.slice(0, 10) : CITATION_UNKNOWN;\n const sessionForTemplate = session.trim().length > 0 ? session : CITATION_UNKNOWN;\n\n // Map from recognised placeholder names to their resolved value. Unknown\n // placeholder names are left intact (returning the original `{name}`).\n const values: Record<string, string> = {\n agent,\n session: sessionForTemplate,\n sessionId,\n ts,\n date,\n };\n\n // Single-pass scan: replace every recognised `{name}` in one sweep so that\n // substituted values cannot themselves be treated as template source on a\n // subsequent pass. The replacer-function form also guarantees that `$` /\n // `$&` / `$1` sequences inside values are emitted literally.\n return template.replace(/\\{([a-zA-Z_][\\w]*)\\}/g, (match, name: string) => {\n return Object.prototype.hasOwnProperty.call(values, name)\n ? values[name]!\n : match;\n });\n}\n\n/**\n * Returns true if the text already carries at least one citation marker.\n * Safe to call on any string — never throws.\n */\nexport function hasCitation(text: string): boolean {\n if (typeof text !== \"string\" || text.length === 0) return false;\n return defaultCitationMatcher().test(text);\n}\n\n/**\n * Escape a string for use as a regex literal.\n */\nfunction escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Escape a single character for use INSIDE a character class `[...]`.\n * Special chars inside character classes: `]`, `\\`, `^`, `-`.\n */\nfunction escapeRegExpCharClass(ch: string): string {\n if (ch === \"]\") return \"\\\\]\";\n if (ch === \"\\\\\") return \"\\\\\\\\\";\n if (ch === \"^\") return \"\\\\^\";\n if (ch === \"-\") return \"\\\\-\";\n // Other regex meta chars are NOT special inside [...] but escaping them is safe.\n return escapeRegExp(ch);\n}\n\n/**\n * Build a per-placeholder token pattern that excludes newlines, whitespace,\n * and any punctuation characters used as inner separators in the template.\n *\n * This prevents a placeholder span from consuming separator bytes and\n * matching strings that cross separator boundaries in user-supplied content.\n *\n * @param nonWordSepChars - Set of non-word (non-alphanumeric, non-`_`) chars\n * extracted from the inner separator literals of the template.\n */\nfunction buildTokenPattern(nonWordSepChars: Set<string>): string {\n // Always exclude newlines and whitespace.\n const base = \"\\\\n\\\\s\";\n if (nonWordSepChars.size === 0) {\n // No inner separator punctuation — the placeholder spans the full space\n // between prefix and suffix. Fall back to a generous no-newline match.\n return `[^\\\\n]+?`;\n }\n const escaped = [...nonWordSepChars].map(escapeRegExpCharClass).join(\"\");\n return `[^${base}${escaped}]+?`;\n}\n\n/** Regex that matches a `{placeholder}` token inside a template string. */\nconst PLACEHOLDER_REGEX = /\\{[a-zA-Z_][\\w]*\\}/g;\n\n/**\n * Build a regex that matches a citation produced by the given template.\n *\n * The approach depends on the shape of the template:\n *\n * - **Normal case (non-empty literal prefix or suffix).** Anchor the match\n * on the outer literal frame and reconstruct the interior as\n * `interToken + sep + interToken + sep + ... + sep + lastToken`.\n * All **intermediate** per-placeholder tokens exclude the combined set of\n * non-word separator characters used between any two adjacent placeholders,\n * preventing a value from consuming a separator and crossing placeholder\n * boundaries. The **last** token is only required to avoid newlines because\n * it is terminated by the literal suffix anchor — this lets placeholder\n * values that legitimately contain a separator character be recognised (e.g.\n * an ISO-8601 timestamp `2026-04-10T14:25:07Z` in `[src:{agent}:{ts}]`\n * where `:` is the inter-placeholder separator). A template like\n * `[src:{agent}/{sessionId}@{date}]` emits\n * `\\[src:[^\\n\\s/@]+?\\/[^\\n\\s/@]+?@[^\\n]+?\\]` so that `[src:foo/bar]`\n * is NOT matched (wrong separator count), `[src:foo/bar/extra@2026]`\n * is NOT matched (intermediate token crosses a `/` boundary), and\n * `[src:planner/main@2026-04-10]` IS matched correctly.\n *\n * - **Placeholder-bounded with whitespace separator.** Both prefix and\n * suffix are empty and the separator literal(s) between placeholders\n * contain at least one whitespace character (e.g. `{source}: {content}`,\n * `{agent} {sessionId}`). A whitespace-containing separator produces\n * output that is visually indistinguishable from ordinary prose, so the\n * safe strategy is to require a **hard bracket/paren/angle delimiter** on\n * both sides of the reconstructed match. Prose almost never places\n * `[...]` / `(...)` / `<...>` around a phrase, so this yields clean\n * false-positive rejection.\n *\n * - **Placeholder-bounded with compact (non-whitespace) separator.** Both\n * prefix and suffix are empty and the separator literal(s) contain NO\n * whitespace (e.g. `{agent}:{sessionId}`, `{agent}/{sessionId}`).\n * `formatCitation` emits a compact token like `planner:main` with no\n * surrounding delimiters, so the bracket strategy cannot detect it.\n * Instead, the pattern requires that the entire token is bordered by\n * whitespace or a bracket/paren/angle on each side:\n *\n * `(?:(?<=[\\[\\(\\<])|(?<!\\S))[\\w.-]+<sep>[\\w.-]+(?:(?=[\\]\\)\\>])|(?!\\S))`\n *\n * This accepts `planner:main` when it appears standalone or inside a\n * bracket-wrapped token, and rejects `host:80` embedded inside a URL like\n * `http://host:80` because `host` is immediately preceded by `/`\n * (non-whitespace, non-bracket).\n *\n * - **All-placeholder case (no literals between placeholders either).** No\n * reliable regex can be built — a template like `{agent}{sessionId}`\n * contains no anchor characters. Returns `null`; {@link\n * hasCitationForTemplate} treats this as \"cannot detect\" and returns\n * false, falling back on explicit sentinel/format detection only for the\n * default `[Source: ...]` shape.\n *\n * Returns `null` when the template has no placeholders (fully-literal\n * citation, handled by the string-equality fast path in {@link\n * hasCitationForTemplate}) **or** when the template is entirely placeholder-\n * only with no literal content whatsoever.\n */\nfunction templateMatcher(template: string): RegExp | null {\n // Split around all {placeholder} tokens.\n const parts = template.split(PLACEHOLDER_REGEX);\n if (parts.length <= 1) return null;\n\n const prefix = parts[0] ?? \"\";\n const suffix = parts[parts.length - 1] ?? \"\";\n\n // Normal case: at least one literal frame on the outside.\n // Tighten the per-placeholder token so it cannot consume separator\n // characters and match strings that cross separator boundaries\n // (Finding 3 — Uru3).\n if (prefix.length > 0 || suffix.length > 0) {\n const escapedPrefix = escapeRegExp(prefix);\n const escapedSuffix = escapeRegExp(suffix);\n\n // Inner parts: literal separators that sit between adjacent placeholders.\n // For `[src:{agent}/{sessionId}@{date}]`, parts = [\"[src:\", \"/\", \"@\", \"]\"]\n // so innerParts = [\"/\", \"@\"].\n const innerParts = parts.slice(1, -1);\n\n // Collect only the non-word (punctuation/symbol) characters from each\n // inner separator so alphabetic separator text (unlikely but valid) does\n // not exclude letters from the per-placeholder token pattern.\n const nonWordSepChars = new Set<string>();\n for (const sep of innerParts) {\n for (const ch of sep) {\n if (!/\\w/.test(ch)) {\n nonWordSepChars.add(ch);\n }\n }\n }\n\n // All intermediate tokens (every placeholder except the last) use the\n // combined exclusion so they cannot cross placeholder boundaries.\n //\n // The LAST token is different: it is terminated by the literal suffix anchor\n // (e.g. `\\]`), so it does not need to exclude inner-separator characters.\n // Dropping that restriction lets placeholder values that legitimately contain\n // a separator character (e.g. an ISO-8601 timestamp `2026-04-10T14:25:07Z`\n // in template `[src:{agent}:{ts}]`) be recognised correctly instead of\n // producing false-negative misses that trigger duplicate citation injection.\n //\n // Only the LAST token is relaxed. Intermediate tokens keep the combined\n // exclusion so that cross-boundary false positives are still rejected\n // (e.g. `[src:foo/bar/extra@2026-04-11]` for `[src:{a}/{b}@{c}]`).\n const interToken = buildTokenPattern(nonWordSepChars);\n // Last token: terminated by suffix anchor — exclude only newlines.\n const lastToken = buildTokenPattern(new Set<string>());\n\n // Reconstruct the interior: interToken sep interToken sep ... sep lastToken\n // (or just lastToken when there are no inner separators at all).\n const middle =\n innerParts.length === 0\n ? lastToken\n : interToken +\n innerParts\n .slice(0, -1)\n .map((sep) => escapeRegExp(sep) + interToken)\n .join(\"\") +\n escapeRegExp(innerParts[innerParts.length - 1]!) +\n lastToken;\n\n const pattern = escapedPrefix + middle + escapedSuffix;\n return new RegExp(pattern, \"i\");\n }\n\n // Placeholder-bounded case: prefix and suffix are both empty.\n const middleLiterals = parts.slice(1, -1);\n const hasNonEmptyMiddle = middleLiterals.some((p) => p.length > 0);\n if (!hasNonEmptyMiddle) {\n // All-placeholder template with no literal content. Impossible to anchor\n // reliably without sentinel markers; signal the caller.\n return null;\n }\n\n // Identifier token: one or more word chars, dots, dashes, or colons.\n // Colons are included to allow timestamp values like \"10:30\" or session\n // keys like \"agent:planner:main\" inside compact placeholder-bounded\n // templates. URL-like fragments (`http://host:80`) are still rejected\n // because the lead anchor requires whitespace or a bracket immediately\n // before the first id-token group (`http` is preceded by `/`).\n const idToken = \"[\\\\w.:-]+\";\n const body =\n idToken +\n middleLiterals.map((lit) => escapeRegExp(lit) + idToken).join(\"\");\n\n const separatorText = middleLiterals.join(\"\");\n if (/\\s/.test(separatorText)) {\n // Separator contains whitespace: the emitted citation looks like ordinary\n // prose (e.g. `planner main`). Require a hard bracket/paren/angle\n // delimiter on both sides to prevent false matches on English text.\n const opener = \"[\\\\[\\\\(\\\\<]\";\n const closer = \"[\\\\]\\\\)\\\\>]\";\n return new RegExp(opener + body + closer, \"i\");\n }\n\n // Separator is compact (no whitespace): `formatCitation` emits a token like\n // `planner:main` without surrounding delimiters. The challenge is that the\n // same token shape also matches ordinary hyphenated or slashed prose words\n // (e.g. `long-term`, `docs/setup`), causing `hasCitationForTemplate` to\n // return true on uncited fact bodies and silently suppress citation injection\n // from `attachCitation`.\n //\n // Fix (Finding 1): tighten the trail anchor so a bare compact token is only\n // accepted when it sits at the very end of the string (possibly followed by\n // optional trailing whitespace or a newline). Since `attachCitation` always\n // appends the citation at the trimmed end of the fact body, a real citation\n // token will always appear at the tail. Prose like `\"long-term solution\"`\n // has `long-term` in the middle of the string (followed by ` solution`), so\n // the end-of-string anchor rejects it — no false positive, no silent drop.\n //\n // The lead anchor still accepts either a bracket opener or a whitespace\n // boundary (or start of string), so `\"Fact. planner:main\"` and standalone\n // `\"planner:main\"` are both detected after the first attachment pass.\n //\n // Bracket-wrapped form (e.g. `[planner:main]`) is also accepted via the\n // opener/closer pair — bracket still takes precedence over end-of-string.\n //\n // Example — why `http://host:80` does NOT match:\n // Trying to match `host:80`: the char before `h` is `/` (non-whitespace,\n // non-bracket), so `(?<=[\\[\\(\\<])` and `(?<!\\S)` both fail ⟹ no match.\n // Trying to match `http:...`: after `http:` the next chars are `//` which\n // are not `[\\w.-]+`, so the second id-token group fails ⟹ no match.\n const leadAnchor = \"(?:(?<=[\\\\[\\\\(\\\\<])|(?<!\\\\S))\";\n // Trail: either a bracket closer (for `[token]` shape) or end-of-string\n // optionally preceded by whitespace. The `(?!\\S)` is deliberately removed\n // so that a compact token in the MIDDLE of a sentence does not match.\n const trailAnchor = \"(?:(?=[\\\\]\\\\)\\\\>])|(?=\\\\s*$))\";\n return new RegExp(leadAnchor + body + trailAnchor, \"i\");\n}\n\n/**\n * Returns true if `text` already carries a citation produced by `template`\n * **or** by the default `[Source: ...]` format (for facts that were tagged\n * before a config change).\n *\n * Use this instead of {@link hasCitation} whenever the caller has access to\n * the configured `inlineSourceAttributionFormat`.\n *\n * All-placeholder templates such as `{agent}{sessionId}` have no literal\n * content to anchor on and therefore cannot be reliably detected without\n * dedicated sentinel markers. In that case the function returns `false` —\n * callers that need idempotent dedup for such templates should either adopt\n * a template with literal delimiters (recommended) or rely on the default\n * `[Source: ...]` marker detection which is always available via\n * {@link hasCitation}.\n */\nexport function hasCitationForTemplate(text: string, template: string): boolean {\n if (typeof text !== \"string\" || text.length === 0) return false;\n // Always accept the default format as a fallback so facts tagged before a\n // configuration change are not double-tagged on reprocessing.\n if (hasCitation(text)) return true;\n // If the configured template matches the default, we're done.\n //\n // Known limitation (Thread 2 — Codex P2): this fast path exits without\n // checking whether the content carries a citation from a DIFFERENT custom\n // template that was active before the config was changed back to the default.\n // Such a fact would be detected by `hasCitation` above only if the prior\n // custom template happened to match the default `[Source: ...]` pattern.\n // In practice, template changes mid-stream are rare, and the false-negative\n // (missing an old custom citation) produces a benign duplicate citation rather\n // than data loss. A full fix would require storing the citation template used\n // at write time in the frontmatter and checking that here.\n if (template === DEFAULT_CITATION_FORMAT) return false;\n\n // Fully-literal template (no placeholders): exact inclusion check.\n if (!PLACEHOLDER_REGEX.test(template)) {\n // Reset lastIndex because PLACEHOLDER_REGEX is declared with /g.\n PLACEHOLDER_REGEX.lastIndex = 0;\n return text.includes(template);\n }\n // Reset lastIndex after the .test() probe above.\n PLACEHOLDER_REGEX.lastIndex = 0;\n\n const matcher = templateMatcher(template);\n if (!matcher) {\n // All-placeholder template: cannot build a reliable matcher. See the\n // docstring — callers should not rely on dedup for this shape.\n return false;\n }\n return matcher.test(text);\n}\n\n/**\n * Attach an inline citation to fact text.\n *\n * If the text already has a citation — either the default `[Source: ...]`\n * marker or one produced by the configured template — it is returned unchanged.\n * Existing provenance is respected and never overwritten. Otherwise the\n * citation is appended to the trimmed text with a single space separator,\n * which keeps the marker visually adjacent to the fact body.\n */\nexport function attachCitation(\n text: string,\n ctx: CitationContext,\n template: string = DEFAULT_CITATION_FORMAT,\n): string {\n if (typeof text !== \"string\") return text as unknown as string;\n if (hasCitationForTemplate(text, template)) return text;\n const trimmedEnd = trimTrailingWhitespace(text);\n if (trimmedEnd.length === 0) return text;\n const citation = formatCitation(ctx, template);\n // Preserve any trailing newline that callers rely on for markdown rendering.\n const trailing = text.slice(trimmedEnd.length);\n return `${trimmedEnd} ${citation}${trailing}`;\n}\n\n/**\n * Parse a single inline citation from a piece of text. Returns the first\n * citation encountered or `null` when none is present. Malformed citations\n * do not throw — fields that cannot be parsed simply remain `undefined`.\n */\nexport function parseCitation(text: string): ParsedCitation | null {\n if (typeof text !== \"string\" || text.length === 0) return null;\n const matcher = defaultCitationMatcher();\n const match = matcher.exec(text);\n if (!match) return null;\n\n const body = match[1] ?? \"\";\n const raw = match[0] ?? \"\";\n const parsed: ParsedCitation = { raw };\n\n const fields = body\n .split(\",\")\n .map((segment) => segment.trim())\n .filter((segment) => segment.length > 0);\n\n for (const field of fields) {\n const eqIdx = field.indexOf(\"=\");\n if (eqIdx <= 0) continue;\n const key = field.slice(0, eqIdx).trim().toLowerCase();\n const value = field.slice(eqIdx + 1).trim();\n if (value.length === 0) continue;\n switch (key) {\n case \"agent\":\n parsed.agent = value;\n break;\n case \"session\":\n case \"sessionid\":\n parsed.session = value;\n break;\n case \"ts\":\n case \"timestamp\":\n parsed.ts = value;\n break;\n default:\n // Unknown fields are ignored defensively so human edits never crash.\n break;\n }\n }\n\n return parsed;\n}\n\n/**\n * Parse every citation embedded in the text. Always returns an array; empty\n * when none are present.\n */\nexport function parseAllCitations(text: string): ParsedCitation[] {\n if (typeof text !== \"string\" || text.length === 0) return [];\n const matcher = defaultCitationMatcher();\n const results: ParsedCitation[] = [];\n let match: RegExpExecArray | null;\n while ((match = matcher.exec(text)) !== null) {\n const parsed = parseCitation(match[0]);\n if (parsed) results.push(parsed);\n }\n return results;\n}\n\n/**\n * Remove all inline citations from a piece of text.\n *\n * Callers that want the raw fact body (for dedup hashing, display, or\n * comparison) should use this helper instead of hand-rolled regexes so the\n * whole codebase agrees on the citation syntax.\n *\n * Finding 2 fix: when the input contains no citation marker, the input is\n * returned byte-for-byte unchanged. When a citation IS removed, whitespace\n * normalization is applied only at each join seam (the single space between\n * the preceding text and where the citation was), rather than across the\n * entire string. This preserves markdown hard-break spacing, aligned text,\n * and code-like snippets in fact bodies that happen to carry a citation.\n *\n * Implementation: each citation match is replaced by its \"seam fix\" — the\n * content before the match has its trailing whitespace trimmed and then a\n * single space is appended if any text remains, collapsing only the gap\n * left by the removed marker. Whitespace elsewhere in the body is untouched.\n */\nexport function stripCitation(text: string): string {\n if (typeof text !== \"string\" || text.length === 0) return text;\n // Early exit: no citation marker present — return the input unchanged so\n // that callers never lose formatting fidelity on uncited strings.\n if (!hasCitation(text)) return text;\n\n // Walk through all citations and slice them out one by one so that we can\n // normalise ONLY the whitespace at each seam rather than the entire string.\n const matcher = defaultCitationMatcher();\n let result = \"\";\n let lastIndex = 0;\n\n let match: RegExpExecArray | null;\n while ((match = matcher.exec(text)) !== null) {\n // Text before this citation. Trim trailing spaces/tabs at the seam only.\n const before = text.slice(lastIndex, match.index).replace(/[ \\t]+$/, \"\");\n result += before;\n lastIndex = match.index + match[0].length;\n }\n\n // Append any trailing text after the last citation. Trim leading\n // spaces/tabs and trailing whitespace at the join seam.\n const after = text.slice(lastIndex).replace(/^[ \\t]+/, \"\");\n if (after.length > 0) {\n if (result.length > 0) result += \" \";\n result += after;\n }\n\n return result.trimEnd();\n}\n\n/**\n * Strip an inline citation from text using a specific template regex.\n *\n * This is the template-aware counterpart to {@link stripCitation}. When the\n * caller holds the configured `inlineSourceAttributionFormat`, use this\n * function to strip citations produced by that template — including custom\n * templates that differ from the default `[Source: ...]` pattern.\n *\n * Behaviour:\n * - If the text has a **default-format** citation, delegates to\n * {@link stripCitation} (always safe).\n * - If the text has a **custom-template** citation detected by\n * `hasCitationForTemplate`, builds the template regex and removes every\n * occurrence (citations are appended at the end of the fact body by\n * {@link attachCitation}).\n * - All-placeholder templates (no literal prefix/suffix/separator) cannot\n * produce a reliable matcher. `hasCitationForTemplate` already returns\n * `false` for such templates, so this function never attempts to strip an\n * undetectable citation. The text is returned unchanged when no citation\n * is detected.\n * - If no citation is detected for the given template, returns the text\n * unchanged.\n *\n * @returns The stripped text (or the original text when no citation is found).\n */\nexport function stripCitationForTemplate(\n text: string,\n template: string,\n): string {\n if (typeof text !== \"string\" || text.length === 0) return text;\n\n // Fast path: default-format citation — delegate to the existing stripper.\n if (hasCitation(text)) return stripCitation(text);\n\n // No default citation; check whether the custom template produced one.\n // hasCitationForTemplate returns false for all-placeholder templates (no\n // reliable matcher), so those pass through unchanged below.\n if (!hasCitationForTemplate(text, template)) return text;\n\n // Build the template matcher. hasCitationForTemplate already returned true,\n // which means templateMatcher produced a non-null result. The null branch\n // here is a defensive fallback only — delegate to stripCitation.\n const matcher = templateMatcher(template);\n if (!matcher) return stripCitation(text);\n\n // The matcher regex was built without the global flag; add it for exec loop.\n const globalMatcher = new RegExp(\n matcher.source,\n matcher.flags.includes(\"g\") ? matcher.flags : matcher.flags + \"g\",\n );\n let result = \"\";\n let lastIndex = 0;\n let match: RegExpExecArray | null;\n\n while ((match = globalMatcher.exec(text)) !== null) {\n const matchEnd = match.index + match[0].length;\n const enclosure = enclosingDelimiterRange(text, match.index, matchEnd);\n const removalStart = enclosure?.start ?? match.index;\n const removalEnd = enclosure?.end ?? matchEnd;\n const before = text.slice(lastIndex, removalStart).replace(/[ \\t]+$/, \"\");\n result += before;\n lastIndex = removalEnd;\n // Guard against zero-width matches causing an infinite loop.\n if (match[0].length === 0) {\n globalMatcher.lastIndex++;\n }\n }\n\n const after = text.slice(lastIndex).replace(/^[ \\t]+/, \"\");\n if (after.length > 0) {\n if (result.length > 0) result += \" \";\n result += after;\n }\n\n return result.trimEnd();\n}\n\nfunction enclosingDelimiterRange(\n text: string,\n start: number,\n end: number,\n): { start: number; end: number } | undefined {\n if (start <= 0 || end >= text.length) return undefined;\n const opener = text[start - 1];\n const closer = text[end];\n if (\n (opener === \"[\" && closer === \"]\") ||\n (opener === \"(\" && closer === \")\") ||\n (opener === \"<\" && closer === \">\")\n ) {\n return { start: start - 1, end: end + 1 };\n }\n return undefined;\n}\n"],"mappings":"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","names":[]}
@@ -12,7 +12,7 @@ import {
12
12
  } from "./chunk-L2EXJQJP.js";
13
13
  import {
14
14
  extractJsonCandidates
15
- } from "./chunk-UZB5KHKX.js";
15
+ } from "./chunk-RGMVMVMF.js";
16
16
  import {
17
17
  callCodexCliFallback
18
18
  } from "./chunk-RK6F44Y6.js";
@@ -692,4 +692,4 @@ export {
692
692
  fallbackLlmRuntimeContextFromConfig,
693
693
  FallbackLlmClient
694
694
  };
695
- //# sourceMappingURL=chunk-DEVUWMME.js.map
695
+ //# sourceMappingURL=chunk-KGIGRNR6.js.map
@@ -27,7 +27,7 @@ import {
27
27
  listMemoryGovernanceRuns,
28
28
  readMemoryGovernanceRunArtifact,
29
29
  runMemoryGovernance
30
- } from "./chunk-5BTCT236.js";
30
+ } from "./chunk-GYTVOLNX.js";
31
31
  import {
32
32
  clusterByKey,
33
33
  combineNamespaces,
@@ -73,7 +73,7 @@ import {
73
73
  persistExplicitCapture,
74
74
  queueExplicitCaptureForReview,
75
75
  validateExplicitCaptureInput
76
- } from "./chunk-IMA6GU4Y.js";
76
+ } from "./chunk-H3PHZLMF.js";
77
77
  import {
78
78
  CrossNamespaceBudget
79
79
  } from "./chunk-ODPLEWB6.js";
@@ -82,10 +82,10 @@ import {
82
82
  buildBriefing,
83
83
  parseBriefingFocus,
84
84
  parseBriefingWindow
85
- } from "./chunk-AZDOWD2L.js";
85
+ } from "./chunk-OZXVGYGZ.js";
86
86
  import {
87
87
  parseEntityFile
88
- } from "./chunk-7MLB4NCL.js";
88
+ } from "./chunk-PJGB7XRR.js";
89
89
  import {
90
90
  DEFAULT_RECALL_DISCLOSURE,
91
91
  isRecallDisclosure
@@ -100,7 +100,7 @@ import {
100
100
  } from "./chunk-SCU65EZI.js";
101
101
  import {
102
102
  getMemoryProjectionPath
103
- } from "./chunk-KILOTVIF.js";
103
+ } from "./chunk-MB5RSUW6.js";
104
104
  import {
105
105
  defaultCapsulesDir
106
106
  } from "./chunk-ZY2MNJR6.js";
@@ -4349,4 +4349,4 @@ export {
4349
4349
  shapeMemorySummary,
4350
4350
  EngramAccessService
4351
4351
  };
4352
- //# sourceMappingURL=chunk-F4QTFIB4.js.map
4352
+ //# sourceMappingURL=chunk-KQFQ3IS5.js.map
@@ -151,7 +151,7 @@ function splitLoopMarkdown(raw) {
151
151
  const sections = [];
152
152
  let current = null;
153
153
  for (const line of lines) {
154
- const sectionMatch = line.match(/^##\s+(.+?)\s*$/);
154
+ const sectionMatch = line.match(/^##\s(.+)$/);
155
155
  if (sectionMatch) {
156
156
  if (current) sections.push({ title: current.title, body: current.body.trimEnd() });
157
157
  current = { title: sectionMatch[1].trim(), body: "" };
@@ -265,4 +265,4 @@ export {
265
265
  upsertContinuityLoopInMarkdown,
266
266
  reviewContinuityLoopInMarkdown
267
267
  };
268
- //# sourceMappingURL=chunk-QSVPYQPG.js.map
268
+ //# sourceMappingURL=chunk-LDXUBPMO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/identity-continuity.ts"],"sourcesContent":["import type {\n ContinuityIncidentCloseInput,\n ContinuityIncidentOpenInput,\n ContinuityImprovementLoop,\n ContinuityLoopCadence,\n ContinuityLoopReviewInput,\n ContinuityLoopStatus,\n ContinuityLoopUpsertInput,\n ContinuityIncidentRecord,\n ContinuityIncidentState,\n} from \"./types.js\";\n\nfunction parseFrontmatterValue(raw: string): unknown {\n try {\n return JSON.parse(raw);\n } catch {\n return raw;\n }\n}\n\nfunction parseFrontmatter(raw: string): Record<string, unknown> {\n const parsed: Record<string, unknown> = {};\n for (const line of raw.split(\"\\n\")) {\n const idx = line.indexOf(\":\");\n if (idx <= 0) continue;\n const key = line.slice(0, idx).trim();\n const value = line.slice(idx + 1).trim();\n parsed[key] = parseFrontmatterValue(value);\n }\n return parsed;\n}\n\nfunction emitSection(lines: string[], title: string, value?: string): void {\n if (!value || value.trim().length === 0) return;\n lines.push(`## ${title}`, \"\", value.trim(), \"\");\n}\n\nfunction parseSection(body: string, title: string): string | undefined {\n const escaped = title.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const re = new RegExp(`## ${escaped}\\\\n\\\\n([\\\\s\\\\S]*?)(?=\\\\n## |$)`);\n const match = body.match(re);\n if (!match) return undefined;\n const value = match[1].trim();\n return value.length > 0 ? value : undefined;\n}\n\nexport function serializeContinuityIncident(incident: ContinuityIncidentRecord): string {\n const lines = [\n \"---\",\n `id: ${JSON.stringify(incident.id)}`,\n `state: ${JSON.stringify(incident.state)}`,\n `openedAt: ${JSON.stringify(incident.openedAt)}`,\n `updatedAt: ${JSON.stringify(incident.updatedAt)}`,\n ];\n if (incident.closedAt) lines.push(`closedAt: ${JSON.stringify(incident.closedAt)}`);\n if (incident.triggerWindow) lines.push(`triggerWindow: ${JSON.stringify(incident.triggerWindow)}`);\n lines.push(\"---\", \"\");\n\n emitSection(lines, \"Symptom\", incident.symptom);\n emitSection(lines, \"Suspected Cause\", incident.suspectedCause);\n emitSection(lines, \"Fix Applied\", incident.fixApplied);\n emitSection(lines, \"Verification Result\", incident.verificationResult);\n emitSection(lines, \"Preventive Rule\", incident.preventiveRule);\n\n return lines.join(\"\\n\").trimEnd() + \"\\n\";\n}\n\nexport function parseContinuityIncident(raw: string): ContinuityIncidentRecord | null {\n const match = raw.match(/^---\\n([\\s\\S]*?)\\n---\\n?([\\s\\S]*)$/);\n if (!match) return null;\n const frontmatter = parseFrontmatter(match[1]);\n const body = match[2] ?? \"\";\n\n const id = typeof frontmatter.id === \"string\" ? frontmatter.id : \"\";\n const stateRaw = frontmatter.state;\n const state: ContinuityIncidentState = stateRaw === \"closed\" ? \"closed\" : \"open\";\n const openedAt = typeof frontmatter.openedAt === \"string\" ? frontmatter.openedAt : \"\";\n const updatedAt = typeof frontmatter.updatedAt === \"string\" ? frontmatter.updatedAt : openedAt;\n const symptom = parseSection(body, \"Symptom\");\n\n if (!id || !openedAt || !updatedAt || !symptom) return null;\n\n return {\n id,\n state,\n openedAt,\n updatedAt,\n triggerWindow: typeof frontmatter.triggerWindow === \"string\" ? frontmatter.triggerWindow : undefined,\n symptom,\n suspectedCause: parseSection(body, \"Suspected Cause\"),\n fixApplied: parseSection(body, \"Fix Applied\"),\n verificationResult: parseSection(body, \"Verification Result\"),\n preventiveRule: parseSection(body, \"Preventive Rule\"),\n closedAt: typeof frontmatter.closedAt === \"string\" ? frontmatter.closedAt : undefined,\n };\n}\n\nexport function createContinuityIncidentRecord(\n id: string,\n input: ContinuityIncidentOpenInput,\n nowIso: string,\n): ContinuityIncidentRecord {\n return {\n id,\n state: \"open\",\n openedAt: nowIso,\n updatedAt: nowIso,\n triggerWindow: input.triggerWindow?.trim() || undefined,\n symptom: input.symptom.trim(),\n suspectedCause: input.suspectedCause?.trim() || undefined,\n };\n}\n\nexport function closeContinuityIncidentRecord(\n incident: ContinuityIncidentRecord,\n closure: ContinuityIncidentCloseInput,\n nowIso: string,\n): ContinuityIncidentRecord {\n return {\n ...incident,\n state: \"closed\",\n updatedAt: nowIso,\n closedAt: nowIso,\n fixApplied: closure.fixApplied.trim(),\n verificationResult: closure.verificationResult.trim(),\n preventiveRule: closure.preventiveRule?.trim() || incident.preventiveRule,\n };\n}\n\nconst LOOP_HEADER = \"# Continuity Improvement Loops\";\nconst LOOP_CADENCES = new Set<ContinuityLoopCadence>([\"daily\", \"weekly\", \"monthly\", \"quarterly\"]);\nconst LOOP_STATUSES = new Set<ContinuityLoopStatus>([\"active\", \"paused\", \"retired\"]);\nconst STALE_LAST_REVIEWED_FALLBACK = \"1970-01-01T00:00:00.000Z\";\n\nfunction normalizeLoopField(value: string | undefined): string | undefined {\n if (typeof value !== \"string\") return undefined;\n const trimmed = value.trim();\n if (trimmed.length === 0) return undefined;\n return trimmed.replace(/\\s+/g, \" \");\n}\n\nfunction isValidIso(value: string): boolean {\n const ts = Date.parse(value);\n return Number.isFinite(ts);\n}\n\nfunction normalizeContinuityLoop(\n input: ContinuityLoopUpsertInput | ContinuityImprovementLoop,\n nowIso: string,\n): ContinuityImprovementLoop | null {\n const id = normalizeLoopField(input.id);\n const cadence = normalizeLoopField(input.cadence) as ContinuityLoopCadence | undefined;\n const status = normalizeLoopField(input.status) as ContinuityLoopStatus | undefined;\n const purpose = normalizeLoopField(input.purpose);\n const killCondition = normalizeLoopField(input.killCondition);\n const notes = normalizeLoopField(input.notes);\n const lastReviewedRaw =\n \"lastReviewed\" in input && typeof input.lastReviewed === \"string\" ? input.lastReviewed : undefined;\n const lastReviewed = normalizeLoopField(lastReviewedRaw) ?? nowIso;\n\n if (!id || !cadence || !status || !purpose || !killCondition) return null;\n if (!LOOP_CADENCES.has(cadence)) return null;\n if (!LOOP_STATUSES.has(status)) return null;\n if (!isValidIso(lastReviewed)) return null;\n\n return {\n id,\n cadence,\n purpose,\n status,\n killCondition,\n lastReviewed,\n notes,\n };\n}\n\nfunction serializeContinuityLoopSection(loop: ContinuityImprovementLoop): string {\n const lines = [\n `## ${loop.id}`,\n `cadence: ${loop.cadence}`,\n `purpose: ${loop.purpose}`,\n `status: ${loop.status}`,\n `killCondition: ${loop.killCondition}`,\n `lastReviewed: ${loop.lastReviewed}`,\n ];\n if (loop.notes) lines.push(`notes: ${loop.notes}`);\n return lines.join(\"\\n\");\n}\n\ntype MarkdownSection = {\n title: string;\n body: string;\n};\n\nfunction splitLoopMarkdown(raw: string | null): { header: string; sections: MarkdownSection[] } {\n const text = (raw ?? \"\").replace(/\\r/g, \"\");\n const lines = text.split(\"\\n\");\n const headerLines: string[] = [];\n const sections: MarkdownSection[] = [];\n let current: MarkdownSection | null = null;\n\n for (const line of lines) {\n // /^##\\s(.+)$/ + the trim below is exactly equivalent to the original\n // /^##\\s+(.+?)\\s*$/ (same match/no-match and same trimmed title across all\n // inputs, including \"## \" → no-match and \"## \" → empty title) but has no\n // adjacent overlapping quantifiers, so it cannot backtrack polynomially\n // (CodeQL js/polynomial-redos). \\s matches a single fixed-width char and\n // .+ runs greedily to the line end — no \\s+/.* overlap.\n const sectionMatch = line.match(/^##\\s(.+)$/);\n if (sectionMatch) {\n if (current) sections.push({ title: current.title, body: current.body.trimEnd() });\n current = { title: sectionMatch[1].trim(), body: \"\" };\n continue;\n }\n if (!current) {\n headerLines.push(line);\n continue;\n }\n current.body += current.body.length > 0 ? `\\n${line}` : line;\n }\n if (current) sections.push({ title: current.title, body: current.body.trimEnd() });\n\n const headerRaw = headerLines.join(\"\\n\").trim();\n const header = headerRaw.length > 0 ? headerRaw : LOOP_HEADER;\n return { header, sections };\n}\n\nfunction parseLoopFromSection(section: MarkdownSection, nowIso: string): ContinuityImprovementLoop | null {\n const fields: Record<string, string> = {};\n for (const line of section.body.split(\"\\n\")) {\n const kv = line.match(/^([A-Za-z][A-Za-z0-9_]*):\\s*(.+?)\\s*$/);\n if (!kv) continue;\n fields[kv[1]] = kv[2];\n }\n const parsedLastReviewed = normalizeLoopField(fields.lastReviewed);\n const safeLastReviewed =\n parsedLastReviewed && isValidIso(parsedLastReviewed) ? parsedLastReviewed : STALE_LAST_REVIEWED_FALLBACK;\n return normalizeContinuityLoop(\n {\n id: section.title,\n cadence: (fields.cadence ?? \"\") as ContinuityLoopCadence,\n purpose: fields.purpose ?? \"\",\n status: (fields.status ?? \"\") as ContinuityLoopStatus,\n killCondition: fields.killCondition ?? \"\",\n lastReviewed: safeLastReviewed,\n notes: fields.notes,\n },\n nowIso,\n );\n}\n\nfunction joinLoopMarkdown(header: string, sections: MarkdownSection[]): string {\n const lines: string[] = [header.trim(), \"\"];\n for (const section of sections) {\n lines.push(`## ${section.title}`);\n if (section.body.trim().length > 0) {\n lines.push(section.body.trimEnd());\n }\n lines.push(\"\");\n }\n return lines.join(\"\\n\").replace(/\\n{3,}/g, \"\\n\\n\").trimEnd() + \"\\n\";\n}\n\nexport function parseContinuityImprovementLoops(raw: string): ContinuityImprovementLoop[] {\n const parsed = splitLoopMarkdown(raw);\n const nowIso = new Date().toISOString();\n return parsed.sections\n .map((section) => parseLoopFromSection(section, nowIso))\n .filter((loop): loop is ContinuityImprovementLoop => loop !== null);\n}\n\nexport function upsertContinuityLoopInMarkdown(\n raw: string | null,\n input: ContinuityLoopUpsertInput,\n nowIso: string,\n): { markdown: string; loop: ContinuityImprovementLoop } {\n const normalized = normalizeContinuityLoop(input, nowIso);\n if (!normalized) {\n throw new Error(\"Invalid continuity loop input\");\n }\n\n const parsed = splitLoopMarkdown(raw);\n let replaced = false;\n const nextSections = parsed.sections.map((section) => {\n if (normalizeLoopField(section.title) !== normalized.id) return section;\n replaced = true;\n return { title: normalized.id, body: serializeContinuityLoopSection(normalized).split(\"\\n\").slice(1).join(\"\\n\") };\n });\n\n if (!replaced) {\n nextSections.push({\n title: normalized.id,\n body: serializeContinuityLoopSection(normalized).split(\"\\n\").slice(1).join(\"\\n\"),\n });\n }\n\n return { markdown: joinLoopMarkdown(parsed.header, nextSections), loop: normalized };\n}\n\nexport function reviewContinuityLoopInMarkdown(\n raw: string | null,\n id: string,\n input: ContinuityLoopReviewInput,\n nowIso: string,\n): { markdown: string; loop: ContinuityImprovementLoop | null } {\n const parsed = splitLoopMarkdown(raw);\n const normalizedId = normalizeLoopField(id);\n if (!normalizedId) {\n return { markdown: joinLoopMarkdown(parsed.header, parsed.sections), loop: null };\n }\n let updatedLoop: ContinuityImprovementLoop | null = null;\n const nextSections = parsed.sections.map((section) => {\n if (normalizeLoopField(section.title) !== normalizedId) return section;\n const existing = parseLoopFromSection(section, nowIso);\n if (!existing) return section;\n const reviewed = applyContinuityLoopReview(existing, input, nowIso);\n updatedLoop = reviewed;\n return { title: reviewed.id, body: serializeContinuityLoopSection(reviewed).split(\"\\n\").slice(1).join(\"\\n\") };\n });\n\n return { markdown: joinLoopMarkdown(parsed.header, nextSections), loop: updatedLoop };\n}\n\nfunction applyContinuityLoopReview(\n existing: ContinuityImprovementLoop,\n input: ContinuityLoopReviewInput,\n nowIso: string,\n): ContinuityImprovementLoop {\n const nextStatus = normalizeLoopField(input.status) as ContinuityLoopStatus | undefined;\n const nextNotes = normalizeLoopField(input.notes);\n const reviewedAt = normalizeLoopField(input.reviewedAt) ?? nowIso;\n\n return {\n ...existing,\n status: nextStatus && LOOP_STATUSES.has(nextStatus) ? nextStatus : existing.status,\n notes: nextNotes ?? existing.notes,\n lastReviewed: isValidIso(reviewedAt) ? reviewedAt : nowIso,\n };\n}\n"],"mappings":"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","names":[]}
@@ -268,7 +268,11 @@ function renderMemorySummary(ctx) {
268
268
  }
269
269
  lines.push("");
270
270
  }
271
- const full = lines.join("\n").replace(/\n+$/u, "\n");
271
+ const joined = lines.join("\n");
272
+ let joinedEnd = joined.length;
273
+ while (joinedEnd > 0 && joined[joinedEnd - 1] === "\n") joinedEnd--;
274
+ const full = joinedEnd === joined.length ? joined : `${joined.slice(0, joinedEnd)}
275
+ `;
272
276
  return truncateToTokenBudget(full, ctx.maxTokens);
273
277
  }
274
278
  function renderMemoryMd(ctx) {
@@ -690,4 +694,4 @@ export {
690
694
  truncateToTokenBudget,
691
695
  describeMemoriesDir
692
696
  };
693
- //# sourceMappingURL=chunk-JFEKNTX7.js.map
697
+ //# sourceMappingURL=chunk-LN4YGHTM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/connectors/codex-materialize.ts"],"sourcesContent":["/**\n * codex-materialize.ts — Codex CLI native memory artifact materialization (#378)\n *\n * Periodically writes Remnic memories into the file layout that Codex CLI's\n * phase-2 consolidation reads directly under `<codex_home>/memories/`:\n *\n * memory_summary.md — always-loaded at session start (tight budget)\n * MEMORY.md — searchable handbook (task-group schema)\n * raw_memories.md — mechanical merge of raw memories, latest first\n * rollout_summaries/<slug>.md — per-session recaps\n *\n * Codex's own read path is agnostic to which producer wrote these files — it\n * tags reads by `memory_md` / `memory_summary` / `raw_memories` /\n * `rollout_summaries` / `skills`. By materializing Remnic content into this\n * exact layout we let Codex pick up Remnic memories without a single MCP call.\n *\n * Safety invariants\n * ─────────────────\n * - **Atomic writes.** Every file is rendered under `.remnic-tmp/` and then\n * `rename()`d into place so Codex never observes a half-written file.\n * - **Sentinel-based opt-in.** If `<codex_home>/memories/.remnic-managed` is\n * missing, we SKIP materialization entirely and log a warning. This honors\n * user hand-edits to the directory — a user who manually curated their\n * Codex memory layout will never have those edits overwritten.\n * - **Schema validation.** `MEMORY.md` content is validated against the\n * task-group schema before write. Invalid content throws and nothing is\n * written.\n * - **Idempotent no-ops.** A content hash is written into the sentinel. If\n * the re-rendered hash matches the previous run, we skip writes entirely.\n * - **Token budget.** `memory_summary.md` is truncated to fit under the\n * configured token budget (whitespace-tokenized approximation), leaving\n * headroom under Codex's 5000-token summary cap.\n *\n * Privacy\n * ───────\n * This module does not persist any user content outside `<codex_home>/memories`\n * — it only mirrors the memories that Remnic already wrote. It does not log\n * memory content to stdout; it logs file names, counts, and hashes.\n */\n\nimport {\n createHash,\n} from \"node:crypto\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { log } from \"../logger.js\";\nimport { readEnvVar, resolveHomeDir } from \"../runtime/env.js\";\nimport type { MemoryFile } from \"../types.js\";\nimport { expandTildePath } from \"../utils/path.js\";\n\n// ─── Types ─────────────────────────────────────────────────────────────────\n\n/**\n * Input for {@link materializeForNamespace}. Prefer passing pre-loaded\n * `memories` so this module stays I/O-agnostic and trivially testable.\n */\nexport interface MaterializeOptions {\n /** Pre-loaded Remnic memories for this namespace (required). */\n memories: MemoryFile[];\n /** Override `<codex_home>`. Defaults to `$CODEX_HOME` or `~/.codex`. */\n codexHome?: string;\n /** Maximum whitespace-tokenized size of memory_summary.md. Default 4500. */\n maxSummaryTokens?: number;\n /** Maximum age of rollout_summaries/*.md in days. Default 30. */\n rolloutRetentionDays?: number;\n /** Per-session rollout summaries to render. */\n rolloutSummaries?: RolloutSummaryInput[];\n /** Current time, injected for deterministic tests. */\n now?: Date;\n /** Optional logger override for tests. */\n logger?: { info: (msg: string) => void; warn: (msg: string) => void; debug?: (msg: string) => void };\n}\n\n/** Input describing one Codex rollout summary file. */\nexport interface RolloutSummaryInput {\n /** Stable slug for the file (becomes `<slug>.md`). */\n slug: string;\n /** Working directory used during the rollout. */\n cwd?: string;\n /** Path to the raw Codex rollout log, if known. */\n rolloutPath?: string;\n /** ISO-8601 timestamp of the last update. */\n updatedAt?: string;\n /** Opaque thread / session id. */\n threadId?: string;\n /** Markdown body for the recap. */\n body: string;\n /** Freeform keywords / search hints. */\n keywords?: string[];\n}\n\n/** Result of a materialization run. */\nexport interface MaterializeResult {\n /** Namespace that was materialized. */\n namespace: string;\n /** `<codex_home>/memories` path this run targeted. */\n memoriesDir: string;\n /** Was anything actually written (vs. skipped / idempotent no-op)? */\n wrote: boolean;\n /** True if the sentinel was missing and we skipped with a warning. */\n skippedNoSentinel: boolean;\n /** True if the hash matched the previous run and we short-circuited. */\n skippedIdempotent: boolean;\n /** Files that were written this run (relative to `memoriesDir`). */\n filesWritten: string[];\n /** Content hash computed for this run. */\n contentHash: string;\n}\n\n/** On-disk shape of the `.remnic-managed` sentinel. */\ninterface SentinelFile {\n version: number;\n namespace: string;\n updated_at: string;\n content_hash: string;\n}\n\n// ─── Constants ─────────────────────────────────────────────────────────────\n\n/** Bump when the on-disk layout or semantics change. */\nexport const MATERIALIZE_VERSION = 1;\n\n/** Sentinel file name at the root of the materialized memories dir. */\nexport const SENTINEL_FILE = \".remnic-managed\";\n\n/** Scratch directory used for atomic renames. */\nexport const TMP_DIR = \".remnic-tmp\";\n\n/** File names we own. Anything else in the directory is considered user-managed. */\nconst OWNED_FILES = new Set<string>([\n \"memory_summary.md\",\n \"MEMORY.md\",\n \"raw_memories.md\",\n]);\n\n/** Sub-directory for per-session rollout recaps. */\nconst ROLLOUT_SUBDIR = \"rollout_summaries\";\n\n// ─── Public entry points ───────────────────────────────────────────────────\n\n/**\n * Materialize a Remnic namespace into Codex's native memory layout.\n *\n * Returns a {@link MaterializeResult} describing what happened. Callers\n * should treat \"skipped\" as success — the sentinel / idempotent cases are\n * expected and intentional.\n *\n * @throws if `MEMORY.md` fails schema validation (we do not write garbage).\n */\nexport function materializeForNamespace(\n namespace: string,\n options: MaterializeOptions,\n): MaterializeResult {\n const logger = options.logger ?? {\n info: (msg) => log.info(`[codex-materialize] ${msg}`),\n warn: (msg) => log.warn(`[codex-materialize] ${msg}`),\n debug: (msg) => log.debug(`[codex-materialize] ${msg}`),\n };\n const memoriesDir = resolveCodexMemoriesDir(options.codexHome);\n const now = options.now ?? new Date();\n // Honor `0` as \"no summary budget\" — parseConfig already clamps to non-\n // negative integers, so any provided number is meaningful. Only fall back\n // to the default when the caller did not provide the option at all.\n const maxSummaryTokens =\n typeof options.maxSummaryTokens === \"number\" && options.maxSummaryTokens >= 0\n ? options.maxSummaryTokens\n : 4500;\n const rolloutRetentionDays =\n typeof options.rolloutRetentionDays === \"number\" && options.rolloutRetentionDays >= 0\n ? options.rolloutRetentionDays\n : 30;\n\n // ── Sentinel check ─────────────────────────────────────────────────────\n // We deliberately do NOT `mkdirSync(memoriesDir)` before reading the\n // sentinel: creating `~/.codex/memories/` for every user (including ones\n // who never use Codex) would make Remnic's post-consolidation hook leave\n // empty opt-in directories behind on disk. Instead we only check whether\n // the sentinel already exists — if the parent dir doesn't exist, the\n // sentinel can't exist either and we fall straight through to the skip\n // path without touching the filesystem. The `mkdirSync` for `memoriesDir`\n // happens later, only once we know we're actually going to write.\n const sentinelPath = path.join(memoriesDir, SENTINEL_FILE);\n const existingSentinel = readSentinel(sentinelPath);\n if (!existingSentinel) {\n // Log at `debug` when the entire memories dir doesn't exist — that's\n // the common \"user never opted in\" case and should not be noisy.\n // Keep the `warn` level only when the dir exists but lacks a sentinel,\n // which is the \"user hand-curated layout, don't overwrite\" case that\n // genuinely warrants attention.\n if (fs.existsSync(memoriesDir)) {\n logger.warn(\n `sentinel ${SENTINEL_FILE} missing in ${memoriesDir}; skipping materialization to preserve hand-edits`,\n );\n } else {\n logger.debug?.(\n `skipping materialization — ${memoriesDir} does not exist (user not opted in)`,\n );\n }\n return {\n namespace,\n memoriesDir,\n wrote: false,\n skippedNoSentinel: true,\n skippedIdempotent: false,\n filesWritten: [],\n contentHash: \"\",\n };\n }\n\n // Now that we know the user has opted in (sentinel exists), it's safe to\n // ensure the memories dir is present. In practice this is almost always a\n // no-op because the sentinel read above already succeeded, but a defensive\n // mkdirSync protects against a race where the dir was removed between the\n // sentinel read and the first write.\n fs.mkdirSync(memoriesDir, { recursive: true });\n\n // ── Render ─────────────────────────────────────────────────────────────\n const memories = [...options.memories];\n // Track whether the caller actually supplied a rollout set. `undefined`\n // means \"don't touch rollout_summaries/\"; an empty array is still\n // authoritative and means \"we own this dir and it should be empty\".\n const rolloutsSupplied = options.rolloutSummaries !== undefined;\n const rolloutSummaries = options.rolloutSummaries ?? [];\n\n // Prune-before-render: MEMORY.md and memory_summary.md both embed rollout\n // filenames in their body, so they must only ever see the *retained* set.\n // Running pruneRollouts after the renderers (as an earlier revision did)\n // caused MEMORY.md to list `rollout_summaries/<slug>.md` paths for rollouts\n // that were then pruned and never written — a broken link pointing at a\n // ghost file. See review feedback on PR #392.\n const retainedRollouts = pruneRollouts(rolloutSummaries, rolloutRetentionDays, now);\n\n // Deduplicate on sanitized filename. Two different slugs (\"Session 1\" and\n // \"session!!!1\") can sanitize to the same output (\"session-1\"), which would\n // otherwise make the first entry's tmp file get overwritten and cause the\n // later rename step to crash with ENOENT. For each collision slot we keep\n // the entry with the newest `updatedAt` so an unsorted input (or a caller\n // that accidentally appends older recaps after newer ones) can't have an\n // older recap clobber a newer one.\n // We do this at the retained-input level (not just at the written-file\n // level) so MEMORY.md's \"rollout_summary_files\" section lists each slot\n // exactly once and matches what actually gets written to disk.\n const dedupedRollouts: RolloutSummaryInput[] = [];\n const seenNames = new Map<string, number>();\n const parseTs = (value: string | undefined): number => {\n if (!value) return Number.NEGATIVE_INFINITY;\n const parsed = Date.parse(value);\n return Number.isFinite(parsed) ? parsed : Number.NEGATIVE_INFINITY;\n };\n for (const r of retainedRollouts) {\n const name = `${sanitizeSlug(r.slug)}.md`;\n const existingIdx = seenNames.get(name);\n if (existingIdx === undefined) {\n seenNames.set(name, dedupedRollouts.length);\n dedupedRollouts.push(r);\n continue;\n }\n // Newest-wins: only replace the existing entry if the incoming one has a\n // strictly newer timestamp. Ties keep the earlier entry (stable for\n // unsorted inputs) because overwriting on ties would flip rendering output\n // for benign call-order changes.\n const existing = dedupedRollouts[existingIdx];\n if (parseTs(r.updatedAt) > parseTs(existing.updatedAt)) {\n dedupedRollouts[existingIdx] = r;\n }\n }\n\n const memorySummary = renderMemorySummary({\n namespace,\n memories,\n rolloutSummaries: dedupedRollouts,\n maxTokens: maxSummaryTokens,\n });\n\n const memoryMd = renderMemoryMd({\n namespace,\n memories,\n rolloutSummaries: dedupedRollouts,\n });\n\n // Fail fast on schema issues — do not write garbage.\n const validation = validateMemoryMd(memoryMd);\n if (!validation.valid) {\n const reason = validation.errors.join(\"; \");\n logger.warn(`MEMORY.md failed schema validation: ${reason}`);\n throw new Error(`codex-materialize: MEMORY.md schema validation failed: ${reason}`);\n }\n\n const rawMemories = renderRawMemories({ memories });\n\n const rolloutFiles = dedupedRollouts.map((r) => ({\n name: `${sanitizeSlug(r.slug)}.md`,\n body: renderRolloutSummary(r),\n }));\n const destRolloutsDir = path.join(memoriesDir, ROLLOUT_SUBDIR);\n const retainedRolloutNames = new Set(rolloutFiles.map((r) => r.name));\n\n // ── Idempotence check ──────────────────────────────────────────────────\n const hash = computeContentHash({\n namespace,\n memorySummary,\n memoryMd,\n rawMemories,\n rolloutFiles,\n });\n\n if (existingSentinel.content_hash === hash) {\n // Idempotence early-return is only safe when the managed files we would\n // have written are still on disk. If a user or external process deleted\n // `MEMORY.md` / `memory_summary.md` / `raw_memories.md` (or any retained\n // rollout file) while the sentinel's hash stayed the same, we must fall\n // through and rewrite — otherwise Codex would be stuck with missing\n // artifacts until a memory-content change happens to flip the hash.\n const requiredFiles = [\n path.join(memoriesDir, \"memory_summary.md\"),\n path.join(memoriesDir, \"MEMORY.md\"),\n path.join(memoriesDir, \"raw_memories.md\"),\n ...rolloutFiles.map((r) => path.join(memoriesDir, ROLLOUT_SUBDIR, r.name)),\n ];\n const allPresent = requiredFiles.every((f) => fs.existsSync(f));\n const rolloutsClean =\n !rolloutsSupplied ||\n rolloutDirectoryMatchesRetainedSet(\n ensureSafeRolloutsDir(memoriesDir, destRolloutsDir),\n retainedRolloutNames,\n );\n if (allPresent && rolloutsClean) {\n logger.debug?.(`no-op materialization for namespace=${namespace} (hash unchanged)`);\n return {\n namespace,\n memoriesDir,\n wrote: false,\n skippedNoSentinel: false,\n skippedIdempotent: true,\n filesWritten: [],\n contentHash: hash,\n };\n }\n logger.debug?.(\n `hash unchanged for namespace=${namespace} but managed files need refresh — forcing rewrite`,\n );\n }\n\n // ── Atomic writes ──────────────────────────────────────────────────────\n // Use a unique, per-run staging sub-directory so two overlapping runs\n // (e.g. a session-end trigger overlapping with a consolidation post-hook)\n // can't stomp each other's tmp files mid-rename. The old \"fixed TMP_DIR,\n // wipe-on-entry\" layout meant run B would delete run A's staging area out\n // from under it, causing ENOENT on A's rename loop. Per-run uniqueness\n // turns the shared dir into an insulated workspace. See review feedback on\n // PR #392.\n const runTag = `${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n const tmpDir = path.join(memoriesDir, `${TMP_DIR}-${runTag}`);\n // Opportunistic GC for stale scratch dirs left behind by a previous\n // crashed run. We only remove entries whose mtime is older than the\n // stale-threshold below — that way we never delete another in-flight\n // run's staging area out from under it. The threshold is deliberately\n // generous (1h) because a healthy materialize completes in milliseconds\n // and there's no legitimate reason for a live staging dir to be older.\n //\n // NB: we compare against `Date.now()` (wall-clock), not against the\n // injected `options.now`. Tests and deterministic replays commonly\n // inject a non-current timestamp, but file mtimes on disk are always\n // wall-clock, so mixing the two would either false-positive delete\n // fresh dirs (test-time in the past) or false-negative skip stale ones\n // (test-time in the future).\n const TMP_STALE_MS = 60 * 60 * 1000;\n const wallClockMs = Date.now();\n try {\n for (const entry of fs.readdirSync(memoriesDir, { withFileTypes: true })) {\n if (!entry.isDirectory()) continue;\n if (!entry.name.startsWith(TMP_DIR)) continue;\n const stalePath = path.join(memoriesDir, entry.name);\n try {\n const stat = fs.statSync(stalePath);\n if (wallClockMs - stat.mtimeMs < TMP_STALE_MS) continue;\n fs.rmSync(stalePath, { recursive: true, force: true });\n } catch {\n // ignore — another concurrent run may own it, or we lack perms\n }\n }\n } catch {\n // ignore — dir may not exist yet\n }\n fs.mkdirSync(tmpDir, { recursive: true });\n fs.mkdirSync(path.join(tmpDir, ROLLOUT_SUBDIR), { recursive: true });\n\n const filesWritten: string[] = [];\n\n fs.writeFileSync(path.join(tmpDir, \"memory_summary.md\"), memorySummary);\n filesWritten.push(\"memory_summary.md\");\n\n fs.writeFileSync(path.join(tmpDir, \"MEMORY.md\"), memoryMd);\n filesWritten.push(\"MEMORY.md\");\n\n fs.writeFileSync(path.join(tmpDir, \"raw_memories.md\"), rawMemories);\n filesWritten.push(\"raw_memories.md\");\n\n for (const rollout of rolloutFiles) {\n fs.writeFileSync(path.join(tmpDir, ROLLOUT_SUBDIR, rollout.name), rollout.body);\n filesWritten.push(path.join(ROLLOUT_SUBDIR, rollout.name));\n }\n\n // Rename into place. Atomic per-file is sufficient — Codex reads each file\n // independently and tolerates an inconsistent in-between snapshot across\n // files for the duration of the rename loop (milliseconds).\n for (const rel of [\"memory_summary.md\", \"MEMORY.md\", \"raw_memories.md\"]) {\n const src = path.join(tmpDir, rel);\n const dest = path.join(memoriesDir, rel);\n fs.renameSync(src, dest);\n }\n\n const safeDestRolloutsDir = ensureSafeRolloutsDir(memoriesDir, destRolloutsDir);\n // Only garbage-collect rollout files when the caller actually supplied a\n // `rolloutSummaries` array — otherwise we'd wipe legitimately\n // user/Codex-created recap files on every session-end run, since those\n // calls typically omit the rollout set entirely. When rollouts were\n // supplied (even as an empty array — meaning \"we own this dir and it\n // should be empty\"), we clear the stale files we previously owned.\n if (rolloutsSupplied) {\n let existingRollouts: fs.Dirent[];\n try {\n existingRollouts = fs.readdirSync(safeDestRolloutsDir, { withFileTypes: true });\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") throw err;\n existingRollouts = [];\n }\n for (const entry of existingRollouts) {\n if (!entry.isFile()) continue;\n if (!entry.name.endsWith(\".md\")) continue;\n if (retainedRolloutNames.has(entry.name)) continue;\n try {\n fs.unlinkSync(path.join(safeDestRolloutsDir, entry.name));\n } catch (err) {\n throw new Error(\n `codex-materialize: failed to prune stale rollout summary ${entry.name}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n }\n\n for (const rollout of rolloutFiles) {\n const src = path.join(tmpDir, ROLLOUT_SUBDIR, rollout.name);\n const dest = path.join(safeDestRolloutsDir, rollout.name);\n fs.renameSync(src, dest);\n }\n\n // Update sentinel last so a crash leaves hash mismatched → next run rewrites.\n const sentinel: SentinelFile = {\n version: MATERIALIZE_VERSION,\n namespace,\n updated_at: now.toISOString(),\n content_hash: hash,\n };\n writeSentinelAtomically(sentinelPath, sentinel);\n\n try {\n fs.rmSync(tmpDir, { recursive: true, force: true });\n } catch {\n // ignore\n }\n\n logger.info(\n `materialized namespace=${namespace} files=${filesWritten.length} hash=${hash.slice(0, 12)}`,\n );\n\n return {\n namespace,\n memoriesDir,\n wrote: true,\n skippedNoSentinel: false,\n skippedIdempotent: false,\n filesWritten,\n contentHash: hash,\n };\n}\n\n/**\n * Create (or refresh) the `.remnic-managed` sentinel. Callers must do this\n * explicitly the first time they want Remnic to start managing a directory —\n * we never write it implicitly, because its presence is the user's opt-in.\n */\nexport function ensureSentinel(memoriesDir: string, namespace: string, now: Date = new Date()): void {\n fs.mkdirSync(memoriesDir, { recursive: true });\n const sentinelPath = path.join(memoriesDir, SENTINEL_FILE);\n if (fs.existsSync(sentinelPath)) {\n readSentinel(sentinelPath);\n return;\n }\n const sentinel: SentinelFile = {\n version: MATERIALIZE_VERSION,\n namespace,\n updated_at: now.toISOString(),\n content_hash: \"\",\n };\n writeSentinelAtomically(sentinelPath, sentinel);\n}\n\n// ─── Rendering ─────────────────────────────────────────────────────────────\n\ninterface RenderContext {\n namespace: string;\n memories: MemoryFile[];\n rolloutSummaries: RolloutSummaryInput[];\n // Historically this interface exposed a `now: Date` field, but neither\n // `renderMemoryMd` nor `renderMemorySummary` ever read it (rendered output\n // is purely a function of `namespace`, `memories`, and `rolloutSummaries`).\n // The field was flagged as dead weight in PR #392 review and removed.\n // If a future renderer needs a timestamp, re-add it here and update both\n // call sites and the schema test.\n}\n\ninterface SummaryRenderContext extends RenderContext {\n maxTokens: number;\n}\n\n/**\n * Render `memory_summary.md` — the always-loaded file.\n * Budget-capped at `maxTokens` whitespace tokens.\n */\nexport function renderMemorySummary(ctx: SummaryRenderContext): string {\n const lines: string[] = [];\n lines.push(\"# Memory Summary\");\n lines.push(\"\");\n lines.push(`_namespace: ${ctx.namespace}_`);\n lines.push(`_source: remnic_`);\n lines.push(\"\");\n\n const highValue = selectSummaryMemories(ctx.memories, 12);\n if (highValue.length > 0) {\n lines.push(\"## Top memories\");\n lines.push(\"\");\n for (const mem of highValue) {\n lines.push(`- ${oneLineSummary(mem)}`);\n }\n lines.push(\"\");\n }\n\n if (ctx.rolloutSummaries.length > 0) {\n lines.push(\"## Recent rollouts\");\n lines.push(\"\");\n const sorted = [...ctx.rolloutSummaries]\n .sort((a, b) => (b.updatedAt ?? \"\").localeCompare(a.updatedAt ?? \"\"))\n .slice(0, 5);\n for (const r of sorted) {\n const when = r.updatedAt ? ` (${r.updatedAt})` : \"\";\n lines.push(`- ${r.slug}${when}`);\n }\n lines.push(\"\");\n }\n\n // Collapse trailing newlines to one without the anchored /\\n+$/ quantifier,\n // which backtracks polynomially (CodeQL js/polynomial-redos).\n const joined = lines.join(\"\\n\");\n let joinedEnd = joined.length;\n while (joinedEnd > 0 && joined[joinedEnd - 1] === \"\\n\") joinedEnd--;\n const full = joinedEnd === joined.length ? joined : `${joined.slice(0, joinedEnd)}\\n`;\n return truncateToTokenBudget(full, ctx.maxTokens);\n}\n\n/**\n * Render `MEMORY.md` — the searchable handbook in Codex's task-group schema.\n */\nexport function renderMemoryMd(ctx: RenderContext): string {\n const lines: string[] = [];\n lines.push(`# Task Group: ${ctx.namespace}`);\n lines.push(`scope: ${ctx.namespace}`);\n lines.push(`applies_to: cwd=*; reuse_rule=namespace-match`);\n lines.push(\"\");\n\n // One \"task\" per top-level topic cluster. For the first cut we group by\n // memory category so the schema validator always sees at least one task.\n const byCategory = groupMemoriesByCategory(ctx.memories);\n let taskIndex = 1;\n if (byCategory.size === 0) {\n lines.push(`## Task ${taskIndex}: baseline — no memories yet`);\n lines.push(\"\");\n lines.push(\"### rollout_summary_files\");\n for (const r of ctx.rolloutSummaries) {\n lines.push(\n `- rollout_summaries/${sanitizeSlug(r.slug)}.md (cwd=${r.cwd ?? \"*\"}, rollout_path=${r.rolloutPath ?? \"\"}, updated_at=${r.updatedAt ?? \"\"}, thread_id=${r.threadId ?? \"\"})`,\n );\n }\n if (ctx.rolloutSummaries.length === 0) {\n lines.push(\"- (none)\");\n }\n lines.push(\"\");\n lines.push(\"### keywords\");\n lines.push(`- ${ctx.namespace}`);\n lines.push(\"\");\n taskIndex += 1;\n } else {\n for (const [category, mems] of byCategory) {\n lines.push(`## Task ${taskIndex}: ${category} memories, outcome=surface-to-codex`);\n lines.push(\"\");\n lines.push(\"### rollout_summary_files\");\n const relevantRollouts = ctx.rolloutSummaries.slice(0, 5);\n if (relevantRollouts.length === 0) {\n lines.push(\"- (none)\");\n } else {\n for (const r of relevantRollouts) {\n lines.push(\n `- rollout_summaries/${sanitizeSlug(r.slug)}.md (cwd=${r.cwd ?? \"*\"}, rollout_path=${r.rolloutPath ?? \"\"}, updated_at=${r.updatedAt ?? \"\"}, thread_id=${r.threadId ?? \"\"})`,\n );\n }\n }\n lines.push(\"\");\n lines.push(\"### keywords\");\n const keywords = collectKeywords(mems, category, ctx.namespace);\n lines.push(`- ${keywords.join(\", \")}`);\n lines.push(\"\");\n taskIndex += 1;\n }\n }\n\n lines.push(\"## User preferences\");\n const prefs = pickCategory(ctx.memories, [\"preference\"]);\n if (prefs.length === 0) {\n lines.push(\"- (none recorded)\");\n } else {\n for (const pref of prefs.slice(0, 20)) {\n lines.push(`- ${oneLineSummary(pref)}`);\n }\n }\n lines.push(\"\");\n\n lines.push(\"## Reusable knowledge\");\n const knowledge = pickCategory(ctx.memories, [\"fact\", \"decision\", \"principle\", \"rule\", \"skill\"]);\n if (knowledge.length === 0) {\n lines.push(\"- (none recorded)\");\n } else {\n for (const mem of knowledge.slice(0, 30)) {\n lines.push(`- ${oneLineSummary(mem)}`);\n }\n }\n lines.push(\"\");\n\n lines.push(\"## Failures and how to do differently\");\n const corrections = pickCategory(ctx.memories, [\"correction\"]);\n if (corrections.length === 0) {\n lines.push(\"- (none recorded)\");\n } else {\n for (const mem of corrections.slice(0, 20)) {\n lines.push(`- ${oneLineSummary(mem)}`);\n }\n }\n lines.push(\"\");\n\n return lines.join(\"\\n\");\n}\n\n/** Render `raw_memories.md` — mechanical dump, latest first. */\nexport function renderRawMemories(ctx: { memories: MemoryFile[] }): string {\n const sorted = [...ctx.memories].sort((a, b) => {\n const aUpdated = a.frontmatter.updated ?? a.frontmatter.created ?? \"\";\n const bUpdated = b.frontmatter.updated ?? b.frontmatter.created ?? \"\";\n return bUpdated.localeCompare(aUpdated);\n });\n\n const lines: string[] = [\"# Raw Memories\", \"\", \"_source: remnic — latest first_\", \"\"];\n for (const mem of sorted) {\n const fm = mem.frontmatter;\n const id = fm.id ?? \"unknown\";\n const category = fm.category ?? \"unknown\";\n const updated = fm.updated ?? fm.created ?? \"\";\n lines.push(`## ${id} (${category}, updated=${updated})`);\n lines.push(\"\");\n lines.push(mem.content.trim());\n lines.push(\"\");\n }\n return lines.join(\"\\n\");\n}\n\n/** Render a single rollout summary file. */\nexport function renderRolloutSummary(input: RolloutSummaryInput): string {\n const lines: string[] = [];\n lines.push(`# Rollout Summary: ${input.slug}`);\n lines.push(\"\");\n const meta: string[] = [];\n if (input.cwd) meta.push(`cwd=${input.cwd}`);\n if (input.rolloutPath) meta.push(`rollout_path=${input.rolloutPath}`);\n if (input.updatedAt) meta.push(`updated_at=${input.updatedAt}`);\n if (input.threadId) meta.push(`thread_id=${input.threadId}`);\n if (meta.length > 0) {\n lines.push(`_${meta.join(\"; \")}_`);\n lines.push(\"\");\n }\n if (input.keywords && input.keywords.length > 0) {\n lines.push(`**keywords:** ${input.keywords.join(\", \")}`);\n lines.push(\"\");\n }\n lines.push(input.body.trim());\n lines.push(\"\");\n return lines.join(\"\\n\");\n}\n\n// ─── Schema validation ─────────────────────────────────────────────────────\n\nexport interface MemoryMdValidation {\n valid: boolean;\n errors: string[];\n}\n\n/**\n * Validate that a rendered `MEMORY.md` matches Codex's task-group schema.\n * We enforce the minimum set of structural requirements called out in #378:\n *\n * - one `# Task Group:` header\n * - `scope:` and `applies_to:` lines directly beneath it\n * - at least one `## Task N:` section\n * - each task section has `### rollout_summary_files` and `### keywords`\n * - `## User preferences`, `## Reusable knowledge`,\n * `## Failures and how to do differently` sections all present\n */\nexport function validateMemoryMd(content: string): MemoryMdValidation {\n const errors: string[] = [];\n const lines = content.split(/\\r?\\n/u);\n\n const taskGroupIndex = lines.findIndex((l) => /^#\\s+Task Group:\\s+\\S+/u.test(l));\n if (taskGroupIndex === -1) {\n errors.push(\"missing `# Task Group:` header\");\n } else {\n const tail = lines.slice(taskGroupIndex + 1, taskGroupIndex + 5);\n if (!tail.some((l) => /^scope:\\s*\\S+/u.test(l))) {\n errors.push(\"missing `scope:` line under Task Group header\");\n }\n if (!tail.some((l) => /^applies_to:\\s*\\S+/u.test(l))) {\n errors.push(\"missing `applies_to:` line under Task Group header\");\n }\n }\n\n const taskHeaders = lines.filter((l) => /^##\\s+Task\\s+\\d+:/u.test(l));\n if (taskHeaders.length === 0) {\n errors.push(\"at least one `## Task N:` section is required\");\n }\n\n // For every task section, make sure we have rollout_summary_files + keywords\n // before the next `##` header at the same level.\n const sectionRegex = /^##\\s+/u;\n for (let i = 0; i < lines.length; i++) {\n if (!/^##\\s+Task\\s+\\d+:/u.test(lines[i])) continue;\n let hasRollout = false;\n let hasKeywords = false;\n for (let j = i + 1; j < lines.length; j++) {\n if (sectionRegex.test(lines[j])) break;\n if (/^###\\s+rollout_summary_files\\s*$/u.test(lines[j])) hasRollout = true;\n if (/^###\\s+keywords\\s*$/u.test(lines[j])) hasKeywords = true;\n }\n if (!hasRollout) errors.push(`task block at line ${i + 1} missing \\`### rollout_summary_files\\``);\n if (!hasKeywords) errors.push(`task block at line ${i + 1} missing \\`### keywords\\``);\n }\n\n const requiredSections = [\n /^##\\s+User preferences\\s*$/u,\n /^##\\s+Reusable knowledge\\s*$/u,\n /^##\\s+Failures and how to do differently\\s*$/u,\n ];\n for (const re of requiredSections) {\n if (!lines.some((l) => re.test(l))) {\n errors.push(`missing required section: ${re.source}`);\n }\n }\n\n return { valid: errors.length === 0, errors };\n}\n\n// ─── Helpers ───────────────────────────────────────────────────────────────\n\nfunction resolveCodexHome(override?: string): string {\n if (override && override.trim().length > 0) {\n return path.resolve(expandTildePath(override.trim()));\n }\n const fromEnv = readEnvVar(\"CODEX_HOME\");\n if (fromEnv && fromEnv.trim().length > 0) {\n return path.resolve(expandTildePath(fromEnv.trim()));\n }\n return path.resolve(resolveHomeDir(), \".codex\");\n}\n\nexport function resolveCodexMemoriesDir(codexHome?: string): string {\n return path.join(resolveCodexHome(codexHome), \"memories\");\n}\n\nexport function hasCodexMaterializeSentinel(codexHome?: string): boolean {\n return readSentinel(path.join(resolveCodexMemoriesDir(codexHome), SENTINEL_FILE)) !== null;\n}\n\nfunction rolloutDirectoryMatchesRetainedSet(\n rolloutsDir: string,\n retainedNames: Set<string>,\n): boolean {\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(rolloutsDir, { withFileTypes: true });\n } catch {\n return retainedNames.size === 0;\n }\n for (const entry of entries) {\n if (!entry.isFile()) continue;\n if (!entry.name.endsWith(\".md\")) continue;\n if (!retainedNames.has(entry.name)) return false;\n }\n return true;\n}\n\nfunction isPathInside(parent: string, child: string): boolean {\n const relative = path.relative(parent, child);\n return relative === \"\" || (!relative.startsWith(\"..\") && !path.isAbsolute(relative));\n}\n\nfunction ensureSafeRolloutsDir(memoriesDir: string, rolloutsDir: string): string {\n const memoriesReal = fs.realpathSync(memoriesDir);\n\n try {\n const stat = fs.lstatSync(rolloutsDir);\n if (stat.isSymbolicLink()) {\n throw new Error(\"is a symbolic link\");\n }\n if (!stat.isDirectory()) {\n throw new Error(\"is not a directory\");\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n throw new Error(\n `codex-materialize: unsafe ${ROLLOUT_SUBDIR} directory at ${rolloutsDir}: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n fs.mkdirSync(rolloutsDir, { recursive: true });\n }\n\n const rolloutsReal = fs.realpathSync(rolloutsDir);\n if (!isPathInside(memoriesReal, rolloutsReal)) {\n throw new Error(\n `codex-materialize: unsafe ${ROLLOUT_SUBDIR} directory at ${rolloutsDir}: resolves outside ${memoriesDir}`,\n );\n }\n\n return rolloutsDir;\n}\n\nfunction readSentinel(sentinelPath: string): SentinelFile | null {\n if (!fs.existsSync(sentinelPath)) return null;\n try {\n const raw = fs.readFileSync(sentinelPath, \"utf-8\");\n const parsed = JSON.parse(raw) as Partial<SentinelFile>;\n if (\n typeof parsed !== \"object\" ||\n parsed === null ||\n typeof parsed.version !== \"number\" ||\n typeof parsed.namespace !== \"string\" ||\n typeof parsed.updated_at !== \"string\" ||\n typeof parsed.content_hash !== \"string\"\n ) {\n throw new Error(\"invalid sentinel schema\");\n }\n return {\n version: parsed.version,\n namespace: parsed.namespace,\n updated_at: parsed.updated_at,\n content_hash: parsed.content_hash,\n };\n } catch (err) {\n throw new Error(\n `codex-materialize: corrupt ${SENTINEL_FILE} sentinel at ${sentinelPath}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n}\n\nfunction writeSentinelAtomically(\n sentinelPath: string,\n sentinel: SentinelFile,\n): void {\n const tmpPath = `${sentinelPath}.${process.pid}.${Date.now()}.${Math.random()\n .toString(16)\n .slice(2)}.tmp`;\n try {\n fs.writeFileSync(tmpPath, `${JSON.stringify(sentinel, null, 2)}\\n`, {\n flag: \"wx\",\n });\n fs.renameSync(tmpPath, sentinelPath);\n } catch (err) {\n try {\n fs.rmSync(tmpPath, { force: true });\n } catch {\n // ignore cleanup failures; preserve the original write/rename error\n }\n throw err;\n }\n}\n\nfunction selectSummaryMemories(memories: MemoryFile[], limit: number): MemoryFile[] {\n const scored = memories\n .filter((m) => !m.frontmatter.status || m.frontmatter.status === \"active\")\n .map((m) => {\n const confidence = typeof m.frontmatter.confidence === \"number\" ? m.frontmatter.confidence : 0;\n const importance =\n typeof m.frontmatter.importance === \"object\" &&\n m.frontmatter.importance !== null &&\n typeof (m.frontmatter.importance as { score?: number }).score === \"number\"\n ? ((m.frontmatter.importance as { score: number }).score ?? 0)\n : 0;\n const updated = m.frontmatter.updated ?? m.frontmatter.created ?? \"\";\n return { memory: m, score: importance * 2 + confidence, updated };\n });\n\n scored.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score;\n return b.updated.localeCompare(a.updated);\n });\n\n return scored.slice(0, limit).map((s) => s.memory);\n}\n\nfunction oneLineSummary(memory: MemoryFile): string {\n const raw = memory.content.replace(/\\s+/gu, \" \").trim();\n if (raw.length <= 160) return raw;\n return `${raw.slice(0, 157)}...`;\n}\n\nfunction groupMemoriesByCategory(memories: MemoryFile[]): Map<string, MemoryFile[]> {\n const map = new Map<string, MemoryFile[]>();\n for (const memory of memories) {\n if (memory.frontmatter.status && memory.frontmatter.status !== \"active\") continue;\n const category = memory.frontmatter.category ?? \"unknown\";\n const list = map.get(category) ?? [];\n list.push(memory);\n map.set(category, list);\n }\n return map;\n}\n\nfunction pickCategory(memories: MemoryFile[], categories: string[]): MemoryFile[] {\n const allowed = new Set(categories);\n return memories.filter(\n (m) =>\n (!m.frontmatter.status || m.frontmatter.status === \"active\") &&\n allowed.has(m.frontmatter.category ?? \"\"),\n );\n}\n\nfunction collectKeywords(memories: MemoryFile[], category: string, namespace: string): string[] {\n const keywords = new Set<string>();\n keywords.add(category);\n keywords.add(namespace);\n for (const mem of memories.slice(0, 10)) {\n for (const tag of mem.frontmatter.tags ?? []) {\n if (typeof tag === \"string\" && tag.trim().length > 0) keywords.add(tag.trim());\n }\n }\n return [...keywords].slice(0, 16);\n}\n\nfunction pruneRollouts(\n rollouts: RolloutSummaryInput[],\n retentionDays: number,\n now: Date,\n): RolloutSummaryInput[] {\n // Negative retention → \"infinite retention\" escape hatch. `parseConfig`\n // clamps the knob to >= 0, so in practice only callers passing a negative\n // value intentionally get the all-pass behavior.\n if (retentionDays < 0) return rollouts;\n // retentionDays === 0 → cutoff is exactly `now`, which prunes every\n // rollout whose `updatedAt` is in the past (i.e. all of them in practice).\n // This matches the documented semantics of \"retain for 0 days\".\n const cutoffMs = now.getTime() - retentionDays * 24 * 60 * 60 * 1000;\n return rollouts.filter((r) => {\n if (!r.updatedAt) return true;\n const t = Date.parse(r.updatedAt);\n if (!Number.isFinite(t)) return true;\n return t >= cutoffMs;\n });\n}\n\nfunction sanitizeSlug(slug: string): string {\n const sanitized = slug\n .toLowerCase()\n .replace(/[^a-z0-9._-]+/gu, \"-\")\n .slice(0, 96);\n const trimmed = trimHyphenEdges(sanitized);\n return trimmed || \"rollout\";\n}\n\nfunction trimHyphenEdges(value: string): string {\n let start = 0;\n let end = value.length;\n while (start < end && value[start] === \"-\") start += 1;\n while (end > start && value[end - 1] === \"-\") end -= 1;\n return value.slice(start, end);\n}\n\n/**\n * Whitespace-tokenized approximation used by the budget check. Matches the\n * simple heuristic Codex's usage.rs reporting uses for the \"5000 token\"\n * memory_summary cap.\n */\nexport function approximateTokenCount(text: string): number {\n const trimmed = text.trim();\n if (trimmed.length === 0) return 0;\n return trimmed.split(/\\s+/u).length;\n}\n\n/**\n * Truncate `text` so it fits under `maxTokens` whitespace tokens. We drop\n * trailing lines until we're under the budget and then append an ellipsis\n * marker so downstream readers can see that truncation happened.\n */\nexport function truncateToTokenBudget(text: string, maxTokens: number): string {\n if (maxTokens <= 0) return \"\";\n if (approximateTokenCount(text) <= maxTokens) return text;\n\n // Reserve headroom for the truncation marker so the line-preserving path\n // can actually fit the marker without flipping to the hard-cut fallback.\n // Both markers are counted with the same whitespace heuristic the budget\n // check uses, so the arithmetic stays consistent.\n const lineMarker = \"_[truncated for summary budget]_\";\n const tailMarker = \"[truncated]\";\n const lineMarkerTokens = approximateTokenCount(lineMarker);\n const tailMarkerTokens = approximateTokenCount(tailMarker);\n\n const lines = text.split(/\\r?\\n/u);\n const lineBudget = Math.max(0, maxTokens - lineMarkerTokens);\n while (lines.length > 0 && approximateTokenCount(lines.join(\"\\n\")) > lineBudget) {\n lines.pop();\n }\n lines.push(lineMarker);\n let result = lines.join(\"\\n\");\n\n // If a single huge line still blows the budget, hard-cut tokens. Reserve\n // space for the tail marker's own token count so the final string stays\n // within maxTokens rather than sneaking over by a few tokens.\n if (approximateTokenCount(result) > maxTokens) {\n const tokens = result.split(/\\s+/u);\n const keep = Math.max(0, maxTokens - tailMarkerTokens);\n result = keep > 0 ? `${tokens.slice(0, keep).join(\" \")} ${tailMarker}` : tailMarker;\n }\n return result;\n}\n\nfunction computeContentHash(input: {\n namespace: string;\n memorySummary: string;\n memoryMd: string;\n rawMemories: string;\n rolloutFiles: Array<{ name: string; body: string }>;\n}): string {\n const hash = createHash(\"sha256\");\n hash.update(`v${MATERIALIZE_VERSION}\\n`);\n hash.update(`namespace=${input.namespace}\\n`);\n hash.update(\"---memory_summary---\\n\");\n hash.update(input.memorySummary);\n hash.update(\"\\n---memory_md---\\n\");\n hash.update(input.memoryMd);\n hash.update(\"\\n---raw_memories---\\n\");\n hash.update(input.rawMemories);\n const sortedRollouts = [...input.rolloutFiles].sort((a, b) => a.name.localeCompare(b.name));\n for (const r of sortedRollouts) {\n hash.update(`\\n---rollout:${r.name}---\\n`);\n hash.update(r.body);\n }\n return hash.digest(\"hex\");\n}\n\n// ─── Stat helper for tests / debugging ─────────────────────────────────────\n\n/**\n * Return basic stats about a materialized memories dir. Useful for tests and\n * debug CLI output. Returns `null` if the dir does not exist.\n */\nexport function describeMemoriesDir(memoriesDir: string): {\n exists: boolean;\n hasSentinel: boolean;\n files: string[];\n sentinel: SentinelFile | null;\n} | null {\n if (!fs.existsSync(memoriesDir)) return null;\n const sentinelPath = path.join(memoriesDir, SENTINEL_FILE);\n const sentinel = readSentinel(sentinelPath);\n const files: string[] = [];\n for (const entry of fs.readdirSync(memoriesDir, { withFileTypes: true })) {\n if (entry.isFile() && OWNED_FILES.has(entry.name)) files.push(entry.name);\n }\n const rolloutsDir = path.join(memoriesDir, ROLLOUT_SUBDIR);\n if (fs.existsSync(rolloutsDir)) {\n try {\n for (const entry of fs.readdirSync(rolloutsDir, { withFileTypes: true })) {\n if (entry.isFile() && entry.name.endsWith(\".md\")) {\n files.push(path.join(ROLLOUT_SUBDIR, entry.name));\n }\n }\n } catch {\n // ignore\n }\n }\n return {\n exists: true,\n hasSentinel: sentinel !== null,\n files: files.sort(),\n sentinel,\n };\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  openBetterSqlite3
3
- } from "./chunk-3HPAPHUK.js";
3
+ } from "./chunk-6KYMPV2O.js";
4
4
  import {
5
5
  log
6
6
  } from "./chunk-2ODBA7MQ.js";
@@ -131,4 +131,4 @@ export {
131
131
  ensureLcmStateDir,
132
132
  applyLcmSchema
133
133
  };
134
- //# sourceMappingURL=chunk-7XYTQGCC.js.map
134
+ //# sourceMappingURL=chunk-MAV46GWQ.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  openBetterSqlite3
3
- } from "./chunk-3HPAPHUK.js";
3
+ } from "./chunk-6KYMPV2O.js";
4
4
 
5
5
  // src/memory-projection-store.ts
6
6
  import path from "path";
@@ -725,4 +725,4 @@ export {
725
725
  readProjectedLatestReviewQueue,
726
726
  readProjectedGovernanceRecord
727
727
  };
728
- //# sourceMappingURL=chunk-KILOTVIF.js.map
728
+ //# sourceMappingURL=chunk-MB5RSUW6.js.map
@@ -31,14 +31,14 @@ import {
31
31
  FallbackLlmClient,
32
32
  fallbackLlmRuntimeContextFromConfig,
33
33
  gatewayTaskChainOptions
34
- } from "./chunk-DEVUWMME.js";
34
+ } from "./chunk-KGIGRNR6.js";
35
35
  import {
36
36
  buildChatCompletionTokenLimit,
37
37
  shouldAssumeOpenAiChatCompletions
38
38
  } from "./chunk-L2EXJQJP.js";
39
39
  import {
40
40
  extractJsonCandidates
41
- } from "./chunk-UZB5KHKX.js";
41
+ } from "./chunk-RGMVMVMF.js";
42
42
  import {
43
43
  applyWorkExtractionBoundary
44
44
  } from "./chunk-EI6V5UXY.js";
@@ -2299,4 +2299,4 @@ ${memoryList}` }
2299
2299
  export {
2300
2300
  ExtractionEngine
2301
2301
  };
2302
- //# sourceMappingURL=chunk-WB3LYXC5.js.map
2302
+ //# sourceMappingURL=chunk-MON3LMO7.js.map
@@ -6,7 +6,7 @@ import {
6
6
  } from "./chunk-SFQ6QNL7.js";
7
7
  import {
8
8
  StorageManager
9
- } from "./chunk-7MLB4NCL.js";
9
+ } from "./chunk-PJGB7XRR.js";
10
10
  import {
11
11
  buildLifecycleEventsForMemory,
12
12
  sortMemoryLifecycleEvents
@@ -74,4 +74,4 @@ export {
74
74
  backupExistingLedger,
75
75
  rebuildMemoryLifecycleLedger
76
76
  };
77
- //# sourceMappingURL=chunk-APRRL26Q.js.map
77
+ //# sourceMappingURL=chunk-O4UNM6OR.js.map
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  StorageManager,
3
3
  normalizeEntityName
4
- } from "./chunk-7MLB4NCL.js";
4
+ } from "./chunk-PJGB7XRR.js";
5
5
  import {
6
6
  readEnvVar,
7
7
  resolveHomeDir
@@ -824,4 +824,4 @@ export {
824
824
  resolveBriefingSaveDir,
825
825
  briefingFilename
826
826
  };
827
- //# sourceMappingURL=chunk-AZDOWD2L.js.map
827
+ //# sourceMappingURL=chunk-OZXVGYGZ.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  chunkContent
3
- } from "./chunk-4WMCPJWX.js";
3
+ } from "./chunk-UQ7RN5HK.js";
4
4
 
5
5
  // src/semantic-chunking.ts
6
6
  var DEFAULT_SEMANTIC_CHUNKING_CONFIG = {
@@ -74,20 +74,29 @@ function findLocalMinima(series, threshold) {
74
74
  }
75
75
  function splitSentences(text) {
76
76
  const sentences = [];
77
- const sentenceRegex = /[^.!?]*[.!?]+(?:\s+|$)/g;
78
- let match;
79
- let lastIndex = 0;
80
- while ((match = sentenceRegex.exec(text)) !== null) {
81
- sentences.push(match[0].trim());
82
- lastIndex = sentenceRegex.lastIndex;
83
- }
84
- if (lastIndex < text.length) {
85
- const remaining = text.slice(lastIndex).trim();
86
- if (remaining) {
87
- sentences.push(remaining);
77
+ let start = 0;
78
+ for (let i = 0; i < text.length; i++) {
79
+ const ch = text[i];
80
+ if (ch !== "." && ch !== "!" && ch !== "?") continue;
81
+ let end = i;
82
+ while (end + 1 < text.length) {
83
+ const n = text[end + 1];
84
+ if (n !== "." && n !== "!" && n !== "?") break;
85
+ end++;
88
86
  }
87
+ const after = text[end + 1];
88
+ if (after === void 0 || /\s/.test(after)) {
89
+ const sentence = text.slice(start, end + 1).trim();
90
+ if (sentence.length > 0) sentences.push(sentence);
91
+ start = end + 1;
92
+ }
93
+ i = end;
94
+ }
95
+ if (start < text.length) {
96
+ const remaining = text.slice(start).trim();
97
+ if (remaining.length > 0) sentences.push(remaining);
89
98
  }
90
- return sentences.filter((s) => s.length > 0);
99
+ return sentences;
91
100
  }
92
101
  function estimateTokens(text) {
93
102
  return Math.ceil(text.length / 4);
@@ -337,4 +346,4 @@ export {
337
346
  findLocalMinima,
338
347
  semanticChunkContent
339
348
  };
340
- //# sourceMappingURL=chunk-WCYKT2DE.js.map
349
+ //# sourceMappingURL=chunk-P4BC54KI.js.map