@remnic/core 9.3.622 → 9.3.624
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.
- package/dist/access-cli.js +24 -24
- package/dist/access-http.js +9 -9
- package/dist/access-mcp.js +8 -8
- package/dist/access-service.js +7 -7
- package/dist/briefing.js +4 -4
- package/dist/buffer-surprise.js +3 -3
- package/dist/calibration.js +2 -2
- package/dist/causal-consolidation.js +8 -8
- package/dist/{chunk-UK727RHF.js → chunk-2L54V4ZO.js} +3 -3
- package/dist/{chunk-YPNGPHNZ.js → chunk-2UFQYU5F.js} +2 -2
- package/dist/{chunk-XAZOWLW4.js → chunk-3VONWEQB.js} +3 -3
- package/dist/{chunk-BF7ZRHH2.js → chunk-66SLUXKM.js} +2 -2
- package/dist/{chunk-VNR3K2R3.js → chunk-7TPH6UZL.js} +14 -14
- package/dist/{chunk-AVHPSLQ2.js → chunk-AYHXQR53.js} +2 -2
- package/dist/{chunk-LANHQ7EN.js → chunk-BNW5NJJH.js} +2 -2
- package/dist/{chunk-6GMPIJAZ.js → chunk-C3IW2F5Z.js} +2 -2
- package/dist/{chunk-4NS2ELXF.js → chunk-FG76RDVI.js} +8 -10
- package/dist/chunk-FG76RDVI.js.map +1 -0
- package/dist/{chunk-3GM7ZY6H.js → chunk-FMGWXIES.js} +4 -4
- package/dist/{chunk-LMZ7XQBB.js → chunk-GLWW3EJQ.js} +3 -3
- package/dist/{chunk-2GRRN7SZ.js → chunk-GYTVOLNX.js} +2 -2
- package/dist/{chunk-IMA6GU4Y.js → chunk-H3PHZLMF.js} +3 -3
- package/dist/chunk-H3PHZLMF.js.map +1 -0
- package/dist/{chunk-XQUIHXNI.js → chunk-I6UCUHLK.js} +4 -4
- package/dist/{chunk-2I2MDQIB.js → chunk-I74SUMNI.js} +2 -2
- package/dist/chunk-I74SUMNI.js.map +1 -0
- package/dist/{chunk-4H5ZJHEN.js → chunk-J6A3CX5N.js} +8 -3
- package/dist/{chunk-4H5ZJHEN.js.map → chunk-J6A3CX5N.js.map} +1 -1
- package/dist/{chunk-DEVUWMME.js → chunk-KGIGRNR6.js} +2 -2
- package/dist/{chunk-EOLCAPOU.js → chunk-KQFQ3IS5.js} +5 -5
- package/dist/{chunk-QSVPYQPG.js → chunk-LDXUBPMO.js} +2 -2
- package/dist/chunk-LDXUBPMO.js.map +1 -0
- package/dist/{chunk-JFEKNTX7.js → chunk-LN4YGHTM.js} +6 -2
- package/dist/chunk-LN4YGHTM.js.map +1 -0
- package/dist/{chunk-WB3LYXC5.js → chunk-MON3LMO7.js} +3 -3
- package/dist/{chunk-GA3PMY73.js → chunk-O4UNM6OR.js} +2 -2
- package/dist/{chunk-4G2RQTAE.js → chunk-OZXVGYGZ.js} +2 -2
- package/dist/{chunk-WCYKT2DE.js → chunk-P4BC54KI.js} +23 -14
- package/dist/chunk-P4BC54KI.js.map +1 -0
- package/dist/{chunk-DXBCNDVD.js → chunk-PJGB7XRR.js} +5 -5
- package/dist/chunk-PJGB7XRR.js.map +1 -0
- package/dist/{chunk-ZNCDQZIS.js → chunk-QFQQFX2H.js} +3 -3
- package/dist/{chunk-ZNCDQZIS.js.map → chunk-QFQQFX2H.js.map} +1 -1
- package/dist/{chunk-CCNZM5UM.js → chunk-R3OQGYOU.js} +2 -2
- package/dist/{chunk-UZB5KHKX.js → chunk-RGMVMVMF.js} +2 -2
- package/dist/chunk-RGMVMVMF.js.map +1 -0
- package/dist/{chunk-EDP57PFC.js → chunk-RKW6QR7W.js} +22 -18
- package/dist/chunk-RKW6QR7W.js.map +1 -0
- package/dist/{chunk-4MHHUPNH.js → chunk-UGEBPVNI.js} +3 -3
- package/dist/{chunk-4WMCPJWX.js → chunk-UQ7RN5HK.js} +22 -13
- package/dist/chunk-UQ7RN5HK.js.map +1 -0
- package/dist/{chunk-ZUNNG6PC.js → chunk-W3BKVM64.js} +2 -2
- package/dist/{chunk-K5O2QY6T.js → chunk-YTWNKQ2G.js} +2 -2
- package/dist/chunk-YTWNKQ2G.js.map +1 -0
- package/dist/{chunk-2SGJY2UY.js → chunk-Z3CCEP6F.js} +3 -3
- package/dist/{chunk-UCGCSZP2.js → chunk-ZZPIJPPD.js} +2 -2
- package/dist/chunking.js +1 -1
- package/dist/cli.js +19 -19
- package/dist/compounding/engine.js +4 -4
- package/dist/connectors/codex-materialize-runner.js +5 -5
- package/dist/connectors/codex-materialize.js +1 -1
- package/dist/connectors/index.js +5 -5
- package/dist/contradiction/index.js +2 -2
- package/dist/{contradiction-scan-GD7KUFWS.js → contradiction-scan-AZTGFMPY.js} +3 -3
- package/dist/entity-retrieval.js +4 -4
- package/dist/explicit-capture.js +1 -1
- package/dist/extraction-judge.js +3 -3
- package/dist/extraction.js +3 -3
- package/dist/fallback-llm.js +2 -2
- package/dist/identity-continuity.js +1 -1
- package/dist/index.js +39 -36
- package/dist/index.js.map +1 -1
- package/dist/json-extract.js +1 -1
- package/dist/maintenance/memory-governance.js +4 -4
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -4
- package/dist/maintenance/rebuild-memory-projection.js +5 -5
- package/dist/namespaces/migrate.js +5 -5
- package/dist/namespaces/storage.js +4 -4
- package/dist/operator-toolkit.js +7 -7
- package/dist/orchestrator.js +21 -21
- package/dist/peers/index.js +1 -1
- package/dist/recall-planner-llm.js +2 -2
- package/dist/semantic-chunking.js +2 -2
- package/dist/semantic-consolidation.js +6 -6
- package/dist/semantic-rule-promotion.js +4 -4
- package/dist/semantic-rule-verifier.js +4 -4
- package/dist/source-attribution.js +1 -1
- package/dist/storage.js +3 -3
- package/dist/summarizer.js +3 -3
- package/dist/temporal-supersession.js +1 -1
- package/dist/verified-recall.js +4 -4
- package/package.json +1 -1
- package/src/access-http.ts +15 -8
- package/src/chunking.ts +38 -23
- package/src/coding/review-context.ts +7 -1
- package/src/connectors/codex-materialize.ts +6 -1
- package/src/explicit-capture.ts +7 -2
- package/src/identity-continuity.ts +7 -1
- package/src/json-extract.ts +4 -1
- package/src/orchestrator.ts +5 -1
- package/src/peers/profile-reasoner.ts +4 -1
- package/src/semantic-chunking.ts +32 -16
- package/src/semantic-consolidation.ts +4 -1
- package/src/source-attribution.test.ts +21 -0
- package/src/source-attribution.ts +17 -2
- package/src/storage.ts +11 -2
- package/src/temporal-supersession.ts +4 -1
- package/dist/chunk-2I2MDQIB.js.map +0 -1
- package/dist/chunk-4NS2ELXF.js.map +0 -1
- package/dist/chunk-4WMCPJWX.js.map +0 -1
- package/dist/chunk-DXBCNDVD.js.map +0 -1
- package/dist/chunk-EDP57PFC.js.map +0 -1
- package/dist/chunk-IMA6GU4Y.js.map +0 -1
- package/dist/chunk-JFEKNTX7.js.map +0 -1
- package/dist/chunk-K5O2QY6T.js.map +0 -1
- package/dist/chunk-QSVPYQPG.js.map +0 -1
- package/dist/chunk-UZB5KHKX.js.map +0 -1
- package/dist/chunk-WCYKT2DE.js.map +0 -1
- /package/dist/{chunk-UK727RHF.js.map → chunk-2L54V4ZO.js.map} +0 -0
- /package/dist/{chunk-YPNGPHNZ.js.map → chunk-2UFQYU5F.js.map} +0 -0
- /package/dist/{chunk-XAZOWLW4.js.map → chunk-3VONWEQB.js.map} +0 -0
- /package/dist/{chunk-BF7ZRHH2.js.map → chunk-66SLUXKM.js.map} +0 -0
- /package/dist/{chunk-VNR3K2R3.js.map → chunk-7TPH6UZL.js.map} +0 -0
- /package/dist/{chunk-AVHPSLQ2.js.map → chunk-AYHXQR53.js.map} +0 -0
- /package/dist/{chunk-LANHQ7EN.js.map → chunk-BNW5NJJH.js.map} +0 -0
- /package/dist/{chunk-6GMPIJAZ.js.map → chunk-C3IW2F5Z.js.map} +0 -0
- /package/dist/{chunk-3GM7ZY6H.js.map → chunk-FMGWXIES.js.map} +0 -0
- /package/dist/{chunk-LMZ7XQBB.js.map → chunk-GLWW3EJQ.js.map} +0 -0
- /package/dist/{chunk-2GRRN7SZ.js.map → chunk-GYTVOLNX.js.map} +0 -0
- /package/dist/{chunk-XQUIHXNI.js.map → chunk-I6UCUHLK.js.map} +0 -0
- /package/dist/{chunk-DEVUWMME.js.map → chunk-KGIGRNR6.js.map} +0 -0
- /package/dist/{chunk-EOLCAPOU.js.map → chunk-KQFQ3IS5.js.map} +0 -0
- /package/dist/{chunk-WB3LYXC5.js.map → chunk-MON3LMO7.js.map} +0 -0
- /package/dist/{chunk-GA3PMY73.js.map → chunk-O4UNM6OR.js.map} +0 -0
- /package/dist/{chunk-4G2RQTAE.js.map → chunk-OZXVGYGZ.js.map} +0 -0
- /package/dist/{chunk-CCNZM5UM.js.map → chunk-R3OQGYOU.js.map} +0 -0
- /package/dist/{chunk-4MHHUPNH.js.map → chunk-UGEBPVNI.js.map} +0 -0
- /package/dist/{chunk-ZUNNG6PC.js.map → chunk-W3BKVM64.js.map} +0 -0
- /package/dist/{chunk-2SGJY2UY.js.map → chunk-Z3CCEP6F.js.map} +0 -0
- /package/dist/{chunk-UCGCSZP2.js.map → chunk-ZZPIJPPD.js.map} +0 -0
- /package/dist/{contradiction-scan-GD7KUFWS.js.map → contradiction-scan-AZTGFMPY.js.map} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/explicit-capture.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { Orchestrator } from \"./orchestrator.js\";\nimport { isSafeRouteNamespace } from \"./routing/engine.js\";\nimport { sanitizeMemoryContent } from \"./sanitize.js\";\nimport { ContentHashIndex } from \"./storage.js\";\nimport type { CaptureMode, MemoryCategory, MemoryLifecycleEvent, PluginConfig } from \"./types.js\";\n\nexport type ExplicitCaptureInput = {\n content: string;\n category?: string;\n confidence?: number;\n namespace?: string;\n tags?: string[];\n entityRef?: string;\n ttl?: string;\n sourceReason?: string;\n};\n\nexport type ValidExplicitCapture = {\n content: string;\n category: MemoryCategory;\n confidence: number;\n namespace?: string;\n tags: string[];\n entityRef?: string;\n expiresAt?: string;\n sourceReason?: string;\n /**\n * When true, `namespace` was already resolved AND authorized by the caller\n * (the access service's `resolveCodingScopedWriteNamespace`, which auth-checks\n * the base and derives a session-owned `project-*` overlay). The persist /\n * queue layer then routes to it directly instead of re-validating against the\n * static policy allow-list — which would otherwise reject legitimately-derived\n * dynamic project namespaces (#1434). Callers that do NOT pre-authorize the\n * namespace must leave this unset so the allow-list guard still applies.\n */\n namespacePreResolved?: boolean;\n};\n\nexport type ExplicitCaptureSource = \"memory_store\" | \"memory_capture\" | \"suggestion_submit\" | \"inline\";\ntype ExplicitCaptureValidationMode = \"legacy_tool\" | \"strict_explicit\";\n\nconst INLINE_NOTE_RE = /<memory_note>\\s*([\\s\\S]*?)\\s*<\\/memory_note>/gi;\nconst INLINE_NOTE_MARKUP_RE = /<memory_note>\\s*[\\s\\S]*?\\s*<\\/memory_note>/i;\nconst INLINE_ALLOWED_CATEGORIES = new Set<MemoryCategory>([\n \"fact\",\n \"preference\",\n \"correction\",\n \"entity\",\n \"decision\",\n \"relationship\",\n \"principle\",\n \"commitment\",\n \"moment\",\n \"skill\",\n \"rule\",\n \"procedure\",\n \"reasoning_trace\",\n]);\n\nconst SECRET_PATTERNS: RegExp[] = [\n /\\bsk-[A-Za-z0-9]{16,}\\b/,\n /\\bAKIA[0-9A-Z]{16}\\b/,\n /\\bBearer\\s+[A-Za-z0-9._-]{16,}\\b/i,\n /\\b(?:api[_-]?key|secret|token|password|passwd)\\s*[:=]\\s*[^\\s]{8,}\\b/i,\n /\\b(?:authorization)\\s*:\\s*[^\\s]{8,}\\b/i,\n];\nconst SECRET_REDACTION_PATTERNS: Array<{ pattern: RegExp; replacement: string }> = [\n { pattern: /\\bsk-[A-Za-z0-9]{16,}\\b/g, replacement: \"[redacted openai key]\" },\n { pattern: /\\bAKIA[0-9A-Z]{16}\\b/g, replacement: \"[redacted aws key]\" },\n { pattern: /\\bBearer\\s+[A-Za-z0-9._-]{16,}\\b/gi, replacement: \"Bearer [redacted token]\" },\n {\n pattern: /\\b(?:api[_-]?key|secret|token|password|passwd)\\s*[:=]\\s*[^\\s]{8,}\\b/gi,\n replacement: \"[redacted credential]\",\n },\n {\n pattern: /\\b(?:authorization)\\s*:\\s*[^\\s]{8,}\\b/gi,\n replacement: \"authorization: [redacted credential]\",\n },\n];\nconst EXPLICIT_CAPTURE_REVIEW_TAGS = [\"explicit-capture\", \"queued-review\"];\n\nfunction explicitCaptureActor(source: ExplicitCaptureSource): string {\n switch (source) {\n case \"inline\":\n return \"inline.memory_note\";\n case \"memory_store\":\n return \"tool.memory_store\";\n case \"suggestion_submit\":\n return \"tool.suggestion_submit\";\n default:\n return \"tool.memory_capture\";\n }\n}\n\nfunction asTrimmed(value: string | undefined): string | undefined {\n const trimmed = value?.trim();\n return trimmed && trimmed.length > 0 ? trimmed : undefined;\n}\n\nfunction normalizeCaptureContent(value: string): string {\n return value\n .toLowerCase()\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction redactSecrets(value: string): string {\n let redacted = value;\n for (const { pattern, replacement } of SECRET_REDACTION_PATTERNS) {\n redacted = redacted.replace(pattern, replacement);\n }\n return redacted;\n}\n\nfunction containsSecretLikeValue(value: string): boolean {\n return SECRET_PATTERNS.some((pattern) => pattern.test(value));\n}\n\nfunction assertNoSecretLikeMetadata(field: string, value: string | undefined): void {\n const trimmed = asTrimmed(value);\n if (trimmed && containsSecretLikeValue(trimmed)) {\n throw new Error(`${field} appears to contain a secret or credential`);\n }\n}\n\nfunction assertNoSecretLikeMetadataList(field: string, values: string[] | undefined): void {\n for (const value of values ?? []) {\n assertNoSecretLikeMetadata(field, value);\n }\n}\n\nfunction sanitizeReviewText(value: string | undefined, fallback: string): string {\n const redacted = redactSecrets(asTrimmed(value) ?? fallback);\n const sanitized = sanitizeMemoryContent(redacted);\n const safe = sanitized.text.trim();\n return safe.length > 0 ? safe : fallback;\n}\n\nfunction sanitizeReviewMetadata(value: string | undefined): string | undefined {\n const trimmed = asTrimmed(value);\n if (!trimmed) return undefined;\n return sanitizeReviewText(trimmed, \"[redacted]\");\n}\n\nfunction sanitizeReviewTags(tags: string[] | undefined): string[] {\n return Array.from(new Set((tags ?? [])\n .map((tag) => sanitizeReviewMetadata(tag))\n .filter((tag): tag is string => typeof tag === \"string\" && tag.length > 0)));\n}\n\nfunction normalizeExplicitCaptureError(error: unknown): string {\n if (error instanceof Error && error.message.trim().length > 0) return error.message.trim();\n const rendered = String(error).trim();\n return rendered.length > 0 ? rendered : \"explicit capture failed\";\n}\n\nfunction resolveExplicitCaptureReviewNamespace(\n orchestrator: Orchestrator,\n namespace: string | undefined,\n): string | undefined {\n const normalized = asTrimmed(namespace);\n if (!normalized) return undefined;\n return resolveExplicitCaptureNamespace(orchestrator, normalized);\n}\n\nfunction resolveExplicitCaptureNamespace(\n orchestrator: Orchestrator,\n namespace: string | undefined,\n): string | undefined {\n const normalized = asTrimmed(namespace);\n if (!normalized) return undefined;\n if (!orchestrator.config.namespacesEnabled) {\n if (normalized !== orchestrator.config.defaultNamespace) {\n throw new Error(`unsupported namespace: ${normalized}`);\n }\n return normalized;\n }\n const allowed = new Set([\n orchestrator.config.defaultNamespace,\n orchestrator.config.sharedNamespace,\n ...orchestrator.config.namespacePolicies.map((policy) => policy.name),\n ].map((value) => value.trim()).filter(Boolean));\n if (!allowed.has(normalized)) {\n throw new Error(`unsupported namespace: ${normalized}`);\n }\n return normalized;\n}\n\nfunction parseExplicitCaptureTtl(ttl: string | undefined): string | undefined {\n const raw = asTrimmed(ttl);\n if (!raw) return undefined;\n\n const absoluteMs = Date.parse(raw);\n if (Number.isFinite(absoluteMs)) {\n return new Date(absoluteMs).toISOString();\n }\n\n const relative = raw.match(/^(\\d+)\\s*([mhdw])$/i);\n if (!relative) {\n throw new Error(\"ttl must be an ISO-8601 timestamp or relative duration like 30m, 12h, 7d, or 2w\");\n }\n\n const amount = Number.parseInt(relative[1] ?? \"\", 10);\n const unit = (relative[2] ?? \"\").toLowerCase();\n if (!Number.isFinite(amount) || amount <= 0) {\n throw new Error(\"ttl duration must be a positive integer\");\n }\n\n const multiplier =\n unit === \"m\" ? 60_000\n : unit === \"h\" ? 60 * 60_000\n : unit === \"d\" ? 24 * 60 * 60_000\n : 7 * 24 * 60 * 60_000;\n return new Date(Date.now() + amount * multiplier).toISOString();\n}\n\nfunction parseInlineConfidence(value: string): number {\n const trimmed = value.trim();\n if (!/^[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[+-]?\\d+)?$/i.test(trimmed)) {\n return Number.NaN;\n }\n const parsed = Number(trimmed);\n return Number.isFinite(parsed) ? parsed : Number.NaN;\n}\n\nfunction parseInlineNote(block: string): ExplicitCaptureInput | null {\n const lines = block.replace(/\\r/g, \"\").split(\"\\n\");\n const note: Partial<ExplicitCaptureInput> = {};\n let idx = 0;\n\n while (idx < lines.length) {\n const rawLine = lines[idx] ?? \"\";\n const line = rawLine.trim();\n idx += 1;\n if (line.length === 0) continue;\n const colonIdx = line.indexOf(\":\");\n if (colonIdx < 0) continue;\n const key = line.slice(0, colonIdx).trim();\n const value = line.slice(colonIdx + 1).trim();\n\n if (key === \"content\" && value === \"|\") {\n const contentLines: string[] = [];\n while (idx < lines.length) {\n const next = lines[idx] ?? \"\";\n if (next.startsWith(\" \") || next.startsWith(\"\\t\")) {\n contentLines.push(next.replace(/^( |\\t)/, \"\"));\n idx += 1;\n continue;\n }\n if (next.trim().length === 0) {\n contentLines.push(\"\");\n idx += 1;\n continue;\n }\n break;\n }\n note.content = contentLines.join(\"\\n\").trim();\n continue;\n }\n\n switch (key) {\n case \"content\":\n note.content = value;\n break;\n case \"category\":\n note.category = value;\n break;\n case \"confidence\":\n note.confidence = parseInlineConfidence(value);\n break;\n case \"namespace\":\n note.namespace = value;\n break;\n case \"tags\":\n note.tags = value\n .split(\",\")\n .map((entry) => entry.trim())\n .filter(Boolean);\n break;\n case \"entityRef\":\n note.entityRef = value;\n break;\n case \"ttl\":\n note.ttl = value;\n break;\n case \"sourceReason\":\n note.sourceReason = value;\n break;\n default:\n break;\n }\n }\n\n return asTrimmed(note.content) ? (note as ExplicitCaptureInput) : null;\n}\n\nexport function parseInlineExplicitCaptureNotes(text: string): ExplicitCaptureInput[] {\n const notes: ExplicitCaptureInput[] = [];\n for (const match of text.matchAll(INLINE_NOTE_RE)) {\n const parsed = parseInlineNote(match[1] ?? \"\");\n if (parsed) notes.push(parsed);\n }\n return notes;\n}\n\nexport function hasInlineExplicitCaptureMarkup(text: string): boolean {\n return INLINE_NOTE_MARKUP_RE.test(text);\n}\n\nexport function stripInlineExplicitCaptureNotes(text: string): string {\n return text.replace(INLINE_NOTE_RE, \"\").trim();\n}\n\nexport function validateExplicitCaptureInput(\n input: ExplicitCaptureInput,\n mode: ExplicitCaptureValidationMode = \"strict_explicit\",\n): ValidExplicitCapture {\n const content = asTrimmed(input.content);\n if (!content) throw new Error(\"content is required\");\n if (mode === \"strict_explicit\") {\n if (content.length < 10) throw new Error(\"content must be at least 10 characters\");\n if (content.length > 4000) throw new Error(\"content must be 4000 characters or fewer\");\n }\n if (/<memory_note>/i.test(content) || /<\\/memory_note>/i.test(content)) {\n throw new Error(\"nested memory_note blocks are not allowed\");\n }\n\n const category = (asTrimmed(input.category) ?? \"fact\") as MemoryCategory;\n if (!INLINE_ALLOWED_CATEGORIES.has(category)) {\n throw new Error(`unsupported category: ${input.category ?? category}`);\n }\n\n const sanitized = sanitizeMemoryContent(content);\n if (!sanitized.clean) {\n throw new Error(\"content failed memory sanitization\");\n }\n for (const pattern of SECRET_PATTERNS) {\n if (pattern.test(content)) {\n throw new Error(\"content appears to contain a secret or credential\");\n }\n }\n assertNoSecretLikeMetadata(\"sourceReason\", input.sourceReason);\n assertNoSecretLikeMetadata(\"entityRef\", input.entityRef);\n assertNoSecretLikeMetadata(\"ttl\", input.ttl);\n assertNoSecretLikeMetadataList(\"tags\", input.tags);\n\n if (input.confidence !== undefined && !Number.isFinite(input.confidence)) {\n throw new Error(\"confidence must be a finite number\");\n }\n const confidence = input.confidence === undefined ? 0.95 : Number(input.confidence);\n if (confidence < 0 || confidence > 1) {\n throw new Error(\"confidence must be between 0 and 1\");\n }\n const requestedNamespace = asTrimmed(input.namespace);\n if (requestedNamespace && !isSafeRouteNamespace(requestedNamespace)) {\n throw new Error(`unsafe namespace: ${requestedNamespace}`);\n }\n const expiresAt = parseExplicitCaptureTtl(input.ttl);\n\n return {\n content,\n category,\n confidence,\n namespace: asTrimmed(input.namespace),\n tags: Array.from(new Set((input.tags ?? []).map((tag) => tag.trim()).filter(Boolean))),\n entityRef: asTrimmed(input.entityRef),\n expiresAt,\n sourceReason: asTrimmed(input.sourceReason),\n };\n}\n\nasync function findDuplicateExplicitCapture(\n orchestrator: Orchestrator,\n resolvedNamespace: string | undefined,\n candidate: ValidExplicitCapture,\n): Promise<string | null> {\n const storage = await orchestrator.getStorage(resolvedNamespace);\n if (\n candidate.category === \"fact\"\n && typeof (storage as { hasFactContentHash?: (content: string) => Promise<boolean> }).hasFactContentHash === \"function\"\n ) {\n try {\n const hasHash = await (storage as { hasFactContentHash: (content: string) => Promise<boolean> }).hasFactContentHash(\n candidate.content,\n );\n if (!hasHash) {\n const authoritative =\n typeof (storage as { isFactContentHashAuthoritative?: () => Promise<boolean> | boolean }).isFactContentHashAuthoritative\n === \"function\"\n ? await (storage as { isFactContentHashAuthoritative: () => Promise<boolean> | boolean })\n .isFactContentHashAuthoritative()\n : false;\n if (authoritative) return null;\n }\n } catch (err) {\n // Fail open: hash index is only an optimization, so fall back to the full corpus scan.\n void err;\n }\n }\n const existing = await storage.readAllMemories();\n const normalizedCandidate = normalizeCaptureContent(candidate.content);\n const match = existing.find((memory) => {\n const status = memory.frontmatter.status ?? \"active\";\n if (status !== \"active\") return false;\n if (memory.frontmatter.category !== candidate.category) return false;\n return normalizeCaptureContent(memory.content) === normalizedCandidate;\n });\n return match?.frontmatter.id ?? null;\n}\n\nexport async function persistExplicitCapture(\n orchestrator: Orchestrator,\n candidate: ValidExplicitCapture,\n source: ExplicitCaptureSource,\n): Promise<{ id: string; duplicateOf?: string }> {\n const resolvedNamespace = candidate.namespacePreResolved\n ? asTrimmed(candidate.namespace)\n : resolveExplicitCaptureNamespace(orchestrator, candidate.namespace);\n const duplicateOf = await findDuplicateExplicitCapture(orchestrator, resolvedNamespace, candidate);\n if (duplicateOf) {\n return { id: duplicateOf, duplicateOf };\n }\n\n const storage = await orchestrator.getStorage(resolvedNamespace);\n const id = await storage.writeMemory(candidate.category, candidate.content, {\n confidence: candidate.confidence,\n tags: candidate.tags,\n entityRef: candidate.entityRef,\n expiresAt: candidate.expiresAt,\n source: source === \"inline\" ? \"explicit-inline\" : \"explicit\",\n });\n\n const created = new Date().toISOString();\n const event: MemoryLifecycleEvent = {\n eventId: `mle-${randomUUID()}`,\n memoryId: id,\n eventType: \"explicit_capture_accepted\",\n timestamp: created,\n actor: explicitCaptureActor(source),\n reasonCode: candidate.sourceReason,\n ruleVersion: \"explicit-capture.v1\",\n };\n await storage.appendMemoryLifecycleEvents([event]);\n\n return { id };\n}\n\nfunction buildExplicitCaptureReviewContent(input: ExplicitCaptureInput, reason: string): string {\n const requestedContent = asTrimmed(input.content);\n const safeContent = sanitizeReviewText(requestedContent, \"[empty explicit capture]\");\n const safeCategory = sanitizeReviewMetadata(input.category);\n const safeNamespace = sanitizeReviewMetadata(input.namespace);\n const safeEntityRef = sanitizeReviewMetadata(input.entityRef);\n const safeTtl = sanitizeReviewMetadata(input.ttl);\n const safeSourceReason = sanitizeReviewMetadata(input.sourceReason);\n const safeTags = sanitizeReviewTags(input.tags);\n const lines = [\n \"Explicit capture queued for review.\",\n \"\",\n `Reason: ${reason}`,\n \"\",\n \"Submitted content:\",\n safeContent,\n ];\n const metadata = [\n safeCategory ? `Requested category: ${safeCategory}` : undefined,\n safeNamespace ? `Requested namespace: ${safeNamespace}` : undefined,\n safeEntityRef ? `Requested entityRef: ${safeEntityRef}` : undefined,\n safeTtl ? `Requested ttl: ${safeTtl}` : undefined,\n safeSourceReason ? `Requested sourceReason: ${safeSourceReason}` : undefined,\n safeTags.length > 0 ? `Requested tags: ${safeTags.join(\", \")}` : undefined,\n ].filter((entry): entry is string => typeof entry === \"string\" && entry.length > 0);\n if (metadata.length > 0) {\n lines.push(\"\", ...metadata);\n }\n return lines.join(\"\\n\");\n}\n\nasync function findQueuedExplicitCaptureDuplicate(\n orchestrator: Orchestrator,\n namespace: string | undefined,\n content: string,\n): Promise<string | null> {\n const storage = await orchestrator.getStorage(namespace);\n const existing = await storage.readAllMemories();\n const normalized = normalizeCaptureContent(content);\n const match = existing.find((memory) => {\n const status = memory.frontmatter.status ?? \"active\";\n if (status !== \"pending_review\") return false;\n if (!(memory.frontmatter.tags ?? []).includes(\"queued-review\")) return false;\n return normalizeCaptureContent(memory.content) === normalized;\n });\n return match?.frontmatter.id ?? null;\n}\n\nexport async function queueExplicitCaptureForReview(\n orchestrator: Orchestrator,\n input: ExplicitCaptureInput,\n source: ExplicitCaptureSource,\n error: unknown,\n): Promise<{ id: string; duplicateOf?: string }> {\n const reason = sanitizeReviewText(normalizeExplicitCaptureError(error), \"explicit capture failed\");\n const requestedNamespace = asTrimmed(input.namespace);\n // A caller-pre-authorized namespace (e.g. a session-owned project overlay\n // from the access service) routes directly; otherwise apply the static\n // policy allow-list guard (#1434).\n const queueNamespace = (input as { namespacePreResolved?: boolean }).namespacePreResolved\n ? requestedNamespace\n : resolveExplicitCaptureReviewNamespace(orchestrator, requestedNamespace);\n const content = buildExplicitCaptureReviewContent(input, reason);\n const duplicateOf = await findQueuedExplicitCaptureDuplicate(orchestrator, queueNamespace, content);\n if (duplicateOf) {\n return { id: duplicateOf, duplicateOf };\n }\n\n const requestedCategory = asTrimmed(input.category);\n const reviewCategory = requestedCategory && INLINE_ALLOWED_CATEGORIES.has(requestedCategory as MemoryCategory)\n ? requestedCategory as MemoryCategory\n : \"fact\";\n const requestedTags = sanitizeReviewTags(input.tags);\n const storage = await orchestrator.getStorage(queueNamespace);\n const id = await storage.writeMemory(reviewCategory, content, {\n confidence: 0.2,\n tags: Array.from(new Set([...EXPLICIT_CAPTURE_REVIEW_TAGS, ...requestedTags])),\n entityRef: sanitizeReviewMetadata(input.entityRef),\n source: source === \"inline\" ? \"explicit-inline-review\" : \"explicit-review\",\n });\n const created = await storage.getMemoryById(id);\n if (created) {\n await storage.writeMemoryFrontmatter(created, {\n status: \"pending_review\",\n updated: new Date().toISOString(),\n }, {\n actor: explicitCaptureActor(source),\n reasonCode: reason,\n ruleVersion: \"explicit-capture.v1\",\n });\n }\n const event: MemoryLifecycleEvent = {\n eventId: `mle-${randomUUID()}`,\n memoryId: id,\n eventType: \"explicit_capture_queued\",\n timestamp: new Date().toISOString(),\n actor: explicitCaptureActor(source),\n reasonCode: reason,\n ruleVersion: \"explicit-capture.v1\",\n };\n await storage.appendMemoryLifecycleEvents([event]);\n return { id };\n}\n\nexport function shouldSkipImplicitExtraction(cfg: Pick<PluginConfig, \"captureMode\">): boolean {\n return cfg.captureMode === \"explicit\";\n}\n\nexport function shouldProcessInlineExplicitCapture(cfg: Pick<PluginConfig, \"captureMode\">): boolean {\n return cfg.captureMode !== \"implicit\";\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
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 const full = lines.join(\"\\n\").replace(/\\n+$/u, \"\\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 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/temporal-supersession.ts"],"sourcesContent":["/**\n * Temporal Supersession (issue #375)\n *\n * When a new fact lands with `structuredAttributes` keyed on a known\n * `entityRef`, any prior fact whose supersession key collides with the new\n * fact's key is marked `status: \"superseded\"` and linked via\n * `supersededBy` / `supersededAt`. Recall filters those superseded memories\n * by default so agents see only the \"current\" value per entity attribute.\n *\n * The algorithm is intentionally O(N) over the memory corpus per write, but\n * skips cheaply when the new fact has no structuredAttributes. It reuses the\n * cached `readAllMemories()` path so cost is amortized with the rest of the\n * write pipeline.\n */\nimport type { MemoryFile, MemoryFrontmatter } from \"./types.js\";\nimport type { StorageManager } from \"./storage.js\";\nimport { log } from \"./logger.js\";\nimport { effectiveValidAt } from \"./temporal-validity.js\";\n\n/**\n * Shared normalization for supersession key components.\n *\n * Trims surrounding whitespace, lowercases, then collapses any run of\n * whitespace OR hyphens to a single hyphen, and strips any leading/trailing\n * hyphens that result. Both `computeSupersessionKey` and\n * `lookupAttributeByNormalizedKey` must use this so that keys produced at\n * write time and keys used at lookup time are identical regardless of how\n * the LLM encoded whitespace, hyphens, or casing (Finding B fix).\n *\n * Symmetry guarantee: `\"foo bar\"`, `\"foo-bar\"`, `\"foo - bar\"`, and\n * `\"foo bar\"` all canonicalize to `\"foo-bar\"`.\n *\n * Exported so external tests can verify the canonical form.\n */\nexport function normalizeSupersessionKey(raw: string): string {\n return raw\n .trim()\n .toLowerCase()\n .replace(/[\\s\\-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n}\n\n/**\n * Stable supersession key for an (entityRef, attributeName) pair.\n *\n * The algorithm is:\n * - normalize the entityRef (trim, lower-case, collapse whitespace)\n * - normalize the attributeName the same way\n * - join with `::`\n *\n * Exported so tests and tools can recompute it without depending on storage.\n */\nexport function computeSupersessionKey(\n entityRef: string | undefined,\n attributeName: string,\n): string | null {\n if (!entityRef || typeof entityRef !== \"string\") return null;\n if (!attributeName || typeof attributeName !== \"string\") return null;\n const entity = normalizeSupersessionKey(entityRef);\n const attr = normalizeSupersessionKey(attributeName);\n if (entity.length === 0 || attr.length === 0) return null;\n return `${entity}::${attr}`;\n}\n\n/**\n * Compute the full set of supersession keys for a fact with structured\n * attributes. Returns an empty array if no keys can be derived.\n */\nexport function supersessionKeysForFact(spec: {\n entityRef?: string;\n structuredAttributes?: Record<string, string>;\n}): string[] {\n if (!spec.entityRef) return [];\n if (!spec.structuredAttributes) return [];\n const keys: string[] = [];\n for (const attrName of Object.keys(spec.structuredAttributes)) {\n const key = computeSupersessionKey(spec.entityRef, attrName);\n if (key) keys.push(key);\n }\n return keys;\n}\n\n/**\n * Look up a structured-attribute value by a raw key, normalizing both sides\n * with `normalizeSupersessionKey` before comparing. This ensures that keys\n * written by the LLM with mixed case, surrounding whitespace, or internal\n * whitespace (e.g. `\"City\"`, `\" city \"`, `\"job title\"`, `\"job-title\"`)\n * are all matched against normalized keys produced by `computeSupersessionKey`\n * (Finding B fix — uses the same helper so both sides are identical).\n *\n * The storage format is NOT changed — we only normalize at lookup time.\n */\nexport function lookupAttributeByNormalizedKey(\n attributes: Record<string, unknown>,\n rawKey: string,\n): unknown {\n const normalizedTarget = normalizeSupersessionKey(rawKey);\n for (const [k, v] of Object.entries(attributes)) {\n if (normalizeSupersessionKey(k) === normalizedTarget) return v;\n }\n return undefined;\n}\n\n/**\n * Decide whether an existing memory should be superseded by a newly-written\n * memory that carries the supplied supersession key set.\n *\n * Only memories that:\n * - are currently `active`\n * - share an `entityRef` with the new fact\n * - share at least one supersession key with the new fact\n * - are older than the new fact\n * - have a conflicting value (different string) for the overlapping key\n * are eligible. This keeps supersession local to the attribute that actually\n * changed — if fact A sets `{city: Austin, tool: vim}` and fact B sets\n * `{city: NYC}`, only the city attribute is superseded, not the tool.\n */\nexport function shouldSupersedeExisting(args: {\n candidate: MemoryFrontmatter;\n newEntityRef: string;\n newAttributes: Record<string, string>;\n newCreatedAt: string;\n newMemoryId: string;\n}): { matchedKeys: string[] } | null {\n const { candidate, newEntityRef, newAttributes, newCreatedAt, newMemoryId } = args;\n\n if (candidate.id === newMemoryId) return null;\n if (candidate.status && candidate.status !== \"active\") return null;\n if (!candidate.entityRef) return null;\n if (!candidate.structuredAttributes) return null;\n\n // Reuse the shared `normalizeSupersessionKey` helper so this comparison\n // cannot drift from the canonical form used to build supersession keys\n // elsewhere in this file.\n const candidateEntityNorm = normalizeSupersessionKey(candidate.entityRef);\n const newEntityNorm = normalizeSupersessionKey(newEntityRef);\n if (candidateEntityNorm !== newEntityNorm) return null;\n\n // Must be older than the new fact's effective validity start — equal\n // timestamps are ignored to avoid races within the same millisecond. When\n // replay/import supplies source time, valid_at must drive ordering instead\n // of wall-clock persistence time.\n const candidateCreated = Date.parse(effectiveValidAt(candidate));\n const newCreated = Date.parse(newCreatedAt);\n if (!Number.isFinite(candidateCreated) || !Number.isFinite(newCreated)) return null;\n if (candidateCreated >= newCreated) return null;\n\n const matchedKeys: string[] = [];\n for (const [attrName, newValue] of Object.entries(newAttributes)) {\n // Use normalized key lookup so mixed-case or whitespace-padded keys\n // stored by the LLM are matched correctly (Finding 2 fix).\n const candidateValue = lookupAttributeByNormalizedKey(\n candidate.structuredAttributes,\n attrName,\n );\n if (candidateValue === undefined) continue;\n // Only supersede on conflicting values — identical values are a no-op.\n if (normalizeValue(String(candidateValue)) === normalizeValue(newValue)) continue;\n const key = computeSupersessionKey(newEntityRef, attrName);\n if (key) matchedKeys.push(key);\n }\n\n return matchedKeys.length > 0 ? { matchedKeys } : null;\n}\n\nfunction normalizeValue(v: string): string {\n return v.trim().toLowerCase();\n}\n\nasync function expireChildChunksForSupersededParent(args: {\n storage: StorageManager;\n allCandidates: MemoryFile[];\n parentId: string;\n newMemoryId: string;\n supersededAt: string;\n invalidAt?: string;\n}): Promise<void> {\n const processedChunkIds = new Set<string>();\n const chunks = args.allCandidates.filter(\n (candidate) => candidate.frontmatter.parentId === args.parentId,\n );\n\n for (const chunk of chunks) {\n const chunkKey = chunk.frontmatter.id ?? chunk.path;\n if (processedChunkIds.has(chunkKey)) continue;\n\n try {\n const freshChunk = await args.storage.readMemoryByPath(chunk.path);\n if (!freshChunk) continue;\n processedChunkIds.add(chunkKey);\n const freshStatus = freshChunk.frontmatter.status ?? \"active\";\n if (freshStatus !== \"active\" || freshChunk.frontmatter.supersededBy) continue;\n\n await args.storage.writeMemoryFrontmatter(\n freshChunk,\n {\n status: \"superseded\",\n supersededBy: args.newMemoryId,\n supersededAt: args.supersededAt,\n updated: args.supersededAt,\n ...(args.invalidAt && !freshChunk.frontmatter.invalid_at\n ? { invalid_at: args.invalidAt }\n : {}),\n },\n {\n actor: \"temporal-supersession\",\n reasonCode: \"structured-attribute-update-child-chunk\",\n relatedMemoryIds: [args.newMemoryId, args.parentId],\n },\n );\n } catch (err) {\n log.warn(\n `temporal-supersession: failed to expire child chunk ${chunk.frontmatter.id} for parent ${args.parentId}: ${err}`,\n );\n }\n }\n}\n\nexport interface TemporalSupersessionResult {\n supersededIds: string[];\n matchedKeys: string[];\n}\n\n/**\n * Scan existing memories and mark any that are superseded by the\n * just-written memory. Fails open on I/O errors — the new memory is already\n * on disk, and supersession is a best-effort hygiene step.\n */\nexport async function applyTemporalSupersession(args: {\n storage: StorageManager;\n newMemoryId: string;\n entityRef?: string;\n structuredAttributes?: Record<string, string>;\n createdAt: string;\n enabled: boolean;\n /**\n * When true, skip the persisted `frontmatter.created` lookup and use\n * `args.createdAt` directly as the ordering anchor. Set this on the\n * hash-dedup short-circuit path where `newMemoryId` points to an existing\n * OLD fact (no new file is written) and its persisted timestamp would be\n * stale relative to the incoming promotion event (PR #402 Finding Uyui).\n */\n useCallerTimestamp?: boolean;\n}): Promise<TemporalSupersessionResult> {\n const empty: TemporalSupersessionResult = { supersededIds: [], matchedKeys: [] };\n if (!args.enabled) return empty;\n if (!args.entityRef) return empty;\n if (!args.structuredAttributes) return empty;\n if (Object.keys(args.structuredAttributes).length === 0) return empty;\n\n const newKeys = supersessionKeysForFact({\n entityRef: args.entityRef,\n structuredAttributes: args.structuredAttributes,\n });\n if (newKeys.length === 0) return empty;\n\n let hotMemories: MemoryFile[];\n try {\n hotMemories = await args.storage.readAllMemories();\n } catch (err) {\n log.warn(`temporal-supersession: readAllMemories failed: ${err}`);\n return empty;\n }\n\n // Finding 1 fix: use the on-disk effective validity start of the\n // newly-written memory rather than a wall-clock timestamp sampled after\n // `writeMemory` returns. In concurrent writers the two can differ by enough\n // to cause wrong-direction supersession. If source replay/import provided\n // valid_at, it must drive ordering; otherwise created remains the legacy\n // fallback. If the memory is not yet visible in the cache (edge case during\n // fast concurrent writes) fall back to args.createdAt.\n //\n // PR #402 round-12 (Finding Uyui): on the hash-dedup early-return path the\n // caller supplies the OLD matching fact's id as `newMemoryId` (no new file is\n // written). That makes `newMemoryFile.frontmatter.created` an arbitrarily\n // old timestamp. When `args.useCallerTimestamp` is set the caller explicitly\n // opts out of the persisted-timestamp lookup so `args.createdAt` (the\n // incoming event time: source valid_at when present, otherwise wall-clock) is\n // used directly, keeping ordering correct regardless of how old the matching\n // fact is.\n const newMemoryFile = hotMemories.find((m) => m.frontmatter.id === args.newMemoryId);\n const persistedCreatedAt = args.useCallerTimestamp\n ? args.createdAt\n : (newMemoryFile ? effectiveValidAt(newMemoryFile.frontmatter) : args.createdAt);\n\n const supersededIds: string[] = [];\n const matchedKeys = new Set<string>();\n\n // Process hot then cold. Hot-then-cold ordering is safer because hot\n // writes are more frequent and the CAS re-read guards against double-writes.\n // A Set<string> of already-processed ids ensures that a memory visible in\n // both tiers (same logical memory with different filesystem paths during a\n // migration race) is processed at most once. Keying on `frontmatter.id`\n // is correct because the same logical memory has the same id regardless of\n // which tier's directory it currently lives in (PR #402 Finding 1 fix).\n // Fall back to path-based keying when id is absent (defensive).\n const processedIds = new Set<string>();\n\n // Finding UOGi fix (round-6): readAllColdMemories() performs a full uncached\n // recursive directory scan of cold/. After Finding UTsP broadened the scan\n // to cover the entire cold root (not just facts/+corrections/), the per-call\n // cost grows with the cold tree size.\n //\n // The fix is a TTL-based in-memory cache inside StorageManager\n // (readAllColdMemories caches its result for COLD_SCAN_CACHE_TTL_MS) that is\n // shared across consecutive supersession calls within the same write burst.\n // The cache is invalidated automatically on any hot→cold demotion (which\n // calls invalidateAllMemoriesCache, which also clears the cold cache) and\n // expires after the TTL as a safety net.\n //\n // This means back-to-back structured-attribute writes in the same burst\n // (e.g. batch extraction) pay the cold I/O cost at most once, not N times.\n // Correctness is preserved because the cache TTL ensures eventual consistency\n // and the invalidation hook covers the hot→cold path.\n\n let coldMemories: MemoryFile[];\n try {\n coldMemories = await args.storage.readAllColdMemories();\n } catch (err) {\n log.warn(`temporal-supersession: readAllColdMemories failed: ${err}`);\n coldMemories = [];\n }\n\n // Combine hot and cold memories into a single scan. New memory itself is\n // excluded inline. We do NOT skip cold scan when hot produced zero\n // supersessions — the P1 finding is precisely that stale cold facts leak\n // when hot has no hits.\n const allCandidates: MemoryFile[] = [...hotMemories, ...coldMemories];\n\n for (const memory of allCandidates) {\n if (memory.frontmatter.id === args.newMemoryId) continue;\n const dedupeKey = memory.frontmatter.id ?? memory.path;\n if (processedIds.has(dedupeKey)) continue;\n const snapshotStatus = memory.frontmatter.status ?? \"active\";\n if (snapshotStatus !== \"active\") {\n // A stale non-active snapshot entry must not suppress an active copy of\n // the same logical memory that appears later in another tier. This can\n // happen during hot/cold migration races where the hot snapshot is already\n // superseded but the cold copy is still active and should be evaluated.\n continue;\n }\n // NOTE: do NOT call processedIds.add(dedupeKey) here. We defer marking\n // the id as processed until AFTER the CAS re-read succeeds. If we mark\n // it here and the re-read fails (e.g. the hot entry has already been\n // migrated to cold storage), the same logical id that appears later in\n // the cold tier scan would be silently skipped, leaving a stale cold\n // fact unsuperseded. Deferring ensures that a failed primary-tier read\n // grants the alternate tier a chance to process the same id (PR #402\n // round-6 Finding 1 fix).\n\n const decision = shouldSupersedeExisting({\n candidate: memory.frontmatter,\n newEntityRef: args.entityRef,\n newAttributes: args.structuredAttributes,\n newCreatedAt: persistedCreatedAt,\n newMemoryId: args.newMemoryId,\n });\n if (!decision) {\n // No supersession decision — safe to mark as processed now so the\n // alternate tier doesn't re-evaluate an identical non-matching entry.\n processedIds.add(dedupeKey);\n continue;\n }\n\n try {\n // CAS-style re-read immediately before the write. `readAllMemories()`\n // is a snapshot — with concurrent writers, another run may have already\n // superseded this candidate since we loaded it. If we blindly trust the\n // snapshot we can clobber a newer `supersededBy` link with a stale one.\n //\n // File storage offers no true locking, so the best we can do is:\n // 1. re-read the exact file we're about to mutate\n // 2. verify status is still \"active\" and no `supersededBy` is set\n // 3. only then issue the write\n // If the re-read shows a newer concurrent writer beat us to it, skip.\n // This CAS pattern applies equally to hot and cold tier candidates.\n // Mark as processed AFTER confirming the candidate is readable so that\n // a migration-race read failure on the hot entry does not silently\n // prevent the cold entry from being evaluated (Finding 1, round 6).\n const fresh = await args.storage.readMemoryByPath(memory.path);\n if (!fresh) {\n log.debug(\n `[engram] temporal supersession skipped candidate ${memory.frontmatter.id}: no longer readable at ${memory.path} — leaving id available for alternate tier`,\n );\n // Do NOT add to processedIds — allow the cold-tier copy to be\n // evaluated in the next iteration of the same scan.\n continue;\n }\n // Candidate is readable — mark the id as processed now to prevent the\n // alternate tier from double-writing.\n processedIds.add(dedupeKey);\n const freshStatus = fresh.frontmatter.status ?? \"active\";\n if (freshStatus !== \"active\" || fresh.frontmatter.supersededBy) {\n log.debug(\n `[engram] temporal supersession skipped candidate ${memory.frontmatter.id}: already superseded by concurrent writer`,\n );\n continue;\n }\n\n // Finding 2 fix: the `supersededAt` / `updated` timestamps written to the\n // old fact must never run backwards relative to its own persisted\n // `created` timestamp. If the caller-supplied `args.createdAt` (which\n // represents \"when the new replacing fact was authored\") is earlier than\n // either the new fact's persisted `created` (T_new) or the old fact's\n // persisted `created` (T_old), we'd be writing a nonsensical\n // `supersededAt` that precedes the old memory's own creation. Clamp to\n // the monotonic maximum so time only moves forward.\n // This monotonic clamp is applied for both hot and cold tier writes.\n const oldCreatedMs = new Date(fresh.frontmatter.created).getTime();\n const newCreatedMs = new Date(persistedCreatedAt).getTime();\n const argCreatedMs = new Date(args.createdAt).getTime();\n const maxMs = Math.max(\n Number.isFinite(oldCreatedMs) ? oldCreatedMs : 0,\n Number.isFinite(newCreatedMs) ? newCreatedMs : 0,\n Number.isFinite(argCreatedMs) ? argCreatedMs : 0,\n );\n const supersededAt = new Date(maxMs).toISOString();\n\n // Issue #680 — explicit fact lifecycle. When the new fact\n // supersedes this one, set the predecessor's `invalid_at` to the\n // successor's effective valid_at. Skip when the predecessor\n // already carries an `invalid_at` so manual / earlier values\n // are preserved (idempotent).\n //\n // Codex P1 on PR #713: in the hash-dedup early-return path\n // (`useCallerTimestamp: true`), `newMemoryFile` is actually the\n // OLD matching fact — no new file was written — so its\n // `valid_at` is the predecessor's own old timestamp, not the\n // successor's effective time. Use `persistedCreatedAt`\n // directly in that path so the predecessor's invalid_at lines\n // up with the caller's wall-clock, not the matching fact's old\n // valid_at. The non-dedup path keeps the previous behavior\n // (prefer the new file's explicit valid_at, fall back to its\n // persisted created).\n let invalidAtPatch: string | undefined;\n if (!fresh.frontmatter.invalid_at) {\n if (args.useCallerTimestamp) {\n invalidAtPatch = persistedCreatedAt;\n } else {\n const newValidAt = newMemoryFile?.frontmatter.valid_at?.trim();\n invalidAtPatch =\n newValidAt && newValidAt.length > 0 ? newValidAt : persistedCreatedAt;\n }\n }\n const wrote = await args.storage.writeMemoryFrontmatter(\n fresh,\n {\n status: \"superseded\",\n supersededBy: args.newMemoryId,\n supersededAt,\n updated: supersededAt,\n ...(invalidAtPatch ? { invalid_at: invalidAtPatch } : {}),\n },\n {\n actor: \"temporal-supersession\",\n reasonCode: \"structured-attribute-update\",\n relatedMemoryIds: [args.newMemoryId],\n },\n );\n if (wrote) {\n supersededIds.push(memory.frontmatter.id);\n for (const key of decision.matchedKeys) matchedKeys.add(key);\n await expireChildChunksForSupersededParent({\n storage: args.storage,\n allCandidates,\n parentId: fresh.frontmatter.id,\n newMemoryId: args.newMemoryId,\n supersededAt,\n invalidAt: invalidAtPatch ?? fresh.frontmatter.invalid_at,\n });\n }\n } catch (err) {\n log.warn(\n `temporal-supersession: failed to mark ${memory.frontmatter.id} superseded: ${err}`,\n );\n }\n }\n\n if (supersededIds.length > 0) {\n log.debug(\n `temporal-supersession: marked ${supersededIds.length} memories superseded by ${args.newMemoryId} (keys=${Array.from(matchedKeys).join(\",\")})`,\n );\n }\n\n return { supersededIds, matchedKeys: Array.from(matchedKeys) };\n}\n\n/**\n * Recall-side filter: returns true when the candidate should be excluded\n * from recall because it has been temporally superseded. When\n * `includeInRecall` is true, this always returns false (the fact is kept),\n * matching the audit/history opt-in described in the config.\n */\nexport function shouldFilterSupersededFromRecall(\n frontmatter: MemoryFrontmatter,\n options: { enabled: boolean; includeInRecall: boolean },\n): boolean {\n if (!options.enabled) return false;\n if (options.includeInRecall) return false;\n return frontmatter.status === \"superseded\";\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
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 const sectionMatch = line.match(/^##\\s+(.+?)\\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":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/json-extract.ts"],"sourcesContent":["/**\n * Utilities for extracting JSON payloads from LLM outputs.\n *\n * We see common failure modes:\n * - \"Here's an example: {..}\\nHere's the real answer: {..}\" (multiple JSON blocks)\n * - fenced ```json blocks\n * - leading/trailing prose around JSON\n *\n * These helpers attempt multiple candidates and let callers validate with schemas.\n */\n\nexport function stripCodeFences(text: string): string {\n return text.replace(/```(?:json)?\\s*([\\s\\S]*?)```/gi, (_m, inner) => String(inner).trim());\n}\n\nexport function extractJsonCandidates(text: string): string[] {\n const trimmed = text.trim();\n const cleaned = stripCodeFences(trimmed);\n const candidates: string[] = [];\n\n if (cleaned.length > 0) candidates.push(cleaned);\n candidates.push(...scanBalancedJsonBlocks(cleaned));\n\n // Legacy regex fallback (single object)\n const objMatch = cleaned.match(/\\{[\\s\\S]*\\}/);\n if (objMatch) candidates.push(objMatch[0]);\n\n const seen = new Set<string>();\n return candidates\n .map((c) => c.trim())\n .filter((c) => c.length > 0)\n .filter((c) => {\n if (seen.has(c)) return false;\n seen.add(c);\n return true;\n });\n}\n\nfunction scanBalancedJsonBlocks(text: string): string[] {\n const out: string[] = [];\n const opens = new Set([\"{\", \"[\"]);\n const closes: Record<string, string> = { \"{\": \"}\", \"[\": \"]\" };\n\n for (let i = 0; i < text.length; i++) {\n const start = text[i];\n if (!opens.has(start)) continue;\n\n const expectedClose = closes[start];\n let depth = 0;\n let inString = false;\n let escape = false;\n\n for (let j = i; j < text.length; j++) {\n const ch = text[j];\n\n if (inString) {\n if (escape) {\n escape = false;\n } else if (ch === \"\\\\\") {\n escape = true;\n } else if (ch === \"\\\"\") {\n inString = false;\n }\n continue;\n }\n\n if (ch === \"\\\"\") {\n inString = true;\n continue;\n }\n\n if (ch === start) depth++;\n if (ch === expectedClose) depth--;\n\n if (depth === 0) {\n out.push(text.slice(i, j + 1).trim());\n i = j;\n break;\n }\n }\n }\n\n return out;\n}\n\n"],"mappings":";AAWO,SAAS,gBAAgB,MAAsB;AACpD,SAAO,KAAK,QAAQ,kCAAkC,CAAC,IAAI,UAAU,OAAO,KAAK,EAAE,KAAK,CAAC;AAC3F;AAEO,SAAS,sBAAsB,MAAwB;AAC5D,QAAM,UAAU,KAAK,KAAK;AAC1B,QAAM,UAAU,gBAAgB,OAAO;AACvC,QAAM,aAAuB,CAAC;AAE9B,MAAI,QAAQ,SAAS,EAAG,YAAW,KAAK,OAAO;AAC/C,aAAW,KAAK,GAAG,uBAAuB,OAAO,CAAC;AAGlD,QAAM,WAAW,QAAQ,MAAM,aAAa;AAC5C,MAAI,SAAU,YAAW,KAAK,SAAS,CAAC,CAAC;AAEzC,QAAM,OAAO,oBAAI,IAAY;AAC7B,SAAO,WACJ,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EACnB,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,EAC1B,OAAO,CAAC,MAAM;AACb,QAAI,KAAK,IAAI,CAAC,EAAG,QAAO;AACxB,SAAK,IAAI,CAAC;AACV,WAAO;AAAA,EACT,CAAC;AACL;AAEA,SAAS,uBAAuB,MAAwB;AACtD,QAAM,MAAgB,CAAC;AACvB,QAAM,QAAQ,oBAAI,IAAI,CAAC,KAAK,GAAG,CAAC;AAChC,QAAM,SAAiC,EAAE,KAAK,KAAK,KAAK,IAAI;AAE5D,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,QAAQ,KAAK,CAAC;AACpB,QAAI,CAAC,MAAM,IAAI,KAAK,EAAG;AAEvB,UAAM,gBAAgB,OAAO,KAAK;AAClC,QAAI,QAAQ;AACZ,QAAI,WAAW;AACf,QAAI,SAAS;AAEb,aAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,YAAM,KAAK,KAAK,CAAC;AAEjB,UAAI,UAAU;AACZ,YAAI,QAAQ;AACV,mBAAS;AAAA,QACX,WAAW,OAAO,MAAM;AACtB,mBAAS;AAAA,QACX,WAAW,OAAO,KAAM;AACtB,qBAAW;AAAA,QACb;AACA;AAAA,MACF;AAEA,UAAI,OAAO,KAAM;AACf,mBAAW;AACX;AAAA,MACF;AAEA,UAAI,OAAO,MAAO;AAClB,UAAI,OAAO,cAAe;AAE1B,UAAI,UAAU,GAAG;AACf,YAAI,KAAK,KAAK,MAAM,GAAG,IAAI,CAAC,EAAE,KAAK,CAAC;AACpC,YAAI;AACJ;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/semantic-chunking.ts"],"sourcesContent":["/**\n * Semantic Chunking with Smoothing-Based Topic Boundaries (Issue #368)\n *\n * An optional alternative to the recursive chunker in chunking.ts.\n * Uses sentence embeddings + cosine similarity + smoothing to detect\n * natural topic boundaries, producing more coherent chunks.\n */\n\nimport { chunkContent, type Chunk, type ChunkResult } from \"./chunking.js\";\n\n// ---------------------------------------------------------------------------\n// Configuration\n// ---------------------------------------------------------------------------\n\nexport interface SemanticChunkingConfig {\n /** Target tokens per chunk. Default: 200. */\n targetTokens: number;\n /** Minimum tokens for a segment before merging with neighbor. Default: 100. */\n minTokens: number;\n /** Maximum tokens for a segment before recursive splitting. Default: 400. */\n maxTokens: number;\n /** Window size for the moving-average smoothing filter. Default: 3. */\n smoothingWindowSize: number;\n /** How many standard deviations below the mean constitutes a boundary. Default: 1.0. */\n boundaryThresholdStdDevs: number;\n /** Batch size for embedding requests. Default: 32. */\n embeddingBatchSize: number;\n /** Fall back to recursive chunking when embeddings are unavailable. Default: true. */\n fallbackToRecursive: boolean;\n}\n\nexport const DEFAULT_SEMANTIC_CHUNKING_CONFIG: SemanticChunkingConfig = {\n targetTokens: 200,\n minTokens: 100,\n maxTokens: 400,\n smoothingWindowSize: 3,\n boundaryThresholdStdDevs: 1.0,\n embeddingBatchSize: 32,\n fallbackToRecursive: true,\n};\n\n// ---------------------------------------------------------------------------\n// Result types\n// ---------------------------------------------------------------------------\n\nexport interface SemanticChunk extends Chunk {\n /** Optional topic hint derived from position. */\n topicLabel?: string;\n /** Cosine similarity score at the trailing boundary of this chunk. */\n boundaryScore: number;\n}\n\nexport interface SemanticChunkResult {\n /** Whether content was split into multiple chunks. */\n chunked: boolean;\n /** The chunks produced. */\n chunks: SemanticChunk[];\n /** Sentence indices where topic splits occurred. */\n boundaries: number[];\n /** Which algorithm produced the result. */\n method: \"semantic\" | \"recursive-fallback\";\n}\n\n// ---------------------------------------------------------------------------\n// Embedding function signature\n// ---------------------------------------------------------------------------\n\n/** Caller-provided function that embeds an array of texts, returning vectors. */\nexport type EmbedFn = (texts: string[]) => Promise<number[][]>;\n\n// ---------------------------------------------------------------------------\n// Math utilities (exported for testing)\n// ---------------------------------------------------------------------------\n\n/**\n * Cosine similarity between two vectors.\n * Returns a value in [-1, 1]. Identical direction = 1, orthogonal = 0.\n *\n * NOTE: This duplicates cosineSimilarity in recall-mmr.ts and embedding-fallback.ts.\n * Consider extracting to a shared math utility in a future refactor.\n */\nexport function cosineSimilarity(a: number[], b: number[]): number {\n if (a.length !== b.length) {\n throw new Error(\n `cosineSimilarity: vector length mismatch (${a.length} vs ${b.length})`,\n );\n }\n if (a.length === 0) return 0;\n\n let dot = 0;\n let magA = 0;\n let magB = 0;\n for (let i = 0; i < a.length; i++) {\n dot += a[i] * b[i];\n magA += a[i] * a[i];\n magB += b[i] * b[i];\n }\n\n const denom = Math.sqrt(magA) * Math.sqrt(magB);\n if (denom === 0) return 0;\n return dot / denom;\n}\n\n/**\n * Arithmetic mean of a numeric series.\n */\nexport function mean(series: number[]): number {\n if (series.length === 0) return 0;\n let sum = 0;\n for (const v of series) sum += v;\n return sum / series.length;\n}\n\n/**\n * Population standard deviation of a numeric series.\n */\nexport function stddev(series: number[]): number {\n if (series.length === 0) return 0;\n const m = mean(series);\n let sumSq = 0;\n for (const v of series) {\n const d = v - m;\n sumSq += d * d;\n }\n return Math.sqrt(sumSq / series.length);\n}\n\n/**\n * Simple moving average over a 1D series.\n * The window is centered: for window size W, each output[i] averages\n * series[i - floor(W/2) .. i + floor(W/2)], clamped to bounds.\n *\n * Even window sizes are rounded up to the next odd value so the window\n * is symmetric around the center point (Finding 4, PR #420).\n */\nexport function movingAverage(series: number[], windowSize: number): number[] {\n if (series.length === 0) return [];\n if (windowSize < 1) windowSize = 1;\n // Round even values up to the next odd so the window is symmetric.\n if (windowSize % 2 === 0) windowSize = windowSize + 1;\n\n const halfW = Math.floor(windowSize / 2);\n const result: number[] = new Array(series.length);\n\n for (let i = 0; i < series.length; i++) {\n const lo = Math.max(0, i - halfW);\n const hi = Math.min(series.length - 1, i + halfW);\n let sum = 0;\n for (let j = lo; j <= hi; j++) sum += series[j];\n result[i] = sum / (hi - lo + 1);\n }\n return result;\n}\n\n/**\n * Find indices in the series that are local minima AND below the threshold.\n * A local minimum is a point lower than both its immediate neighbors\n * (or lower-or-equal at series boundaries).\n */\nexport function findLocalMinima(\n series: number[],\n threshold: number,\n): number[] {\n if (series.length <= 2) return [];\n\n const minima: number[] = [];\n for (let i = 1; i < series.length - 1; i++) {\n if (\n series[i] < series[i - 1] &&\n series[i] < series[i + 1] &&\n series[i] < threshold\n ) {\n minima.push(i);\n }\n }\n return minima;\n}\n\n// ---------------------------------------------------------------------------\n// Sentence tokenizer\n// ---------------------------------------------------------------------------\n\n/**\n * Split text into sentences at punctuation boundaries.\n * Preserves punctuation with the preceding sentence.\n */\nfunction splitSentences(text: string): string[] {\n const sentences: string[] = [];\n const sentenceRegex = /[^.!?]*[.!?]+(?:\\s+|$)/g;\n\n let match: RegExpExecArray | null;\n let lastIndex = 0;\n\n while ((match = sentenceRegex.exec(text)) !== null) {\n sentences.push(match[0].trim());\n lastIndex = sentenceRegex.lastIndex;\n }\n\n if (lastIndex < text.length) {\n const remaining = text.slice(lastIndex).trim();\n if (remaining) {\n sentences.push(remaining);\n }\n }\n\n return sentences.filter((s) => s.length > 0);\n}\n\n// ---------------------------------------------------------------------------\n// Token estimation\n// ---------------------------------------------------------------------------\n\n/** Rough token estimate: ~4 chars per token for English. */\nfunction estimateTokens(text: string): number {\n return Math.ceil(text.length / 4);\n}\n\n// ---------------------------------------------------------------------------\n// Core semantic chunking\n// ---------------------------------------------------------------------------\n\n/**\n * Batch-embed sentences using the provided embed function.\n * Respects the configured batch size.\n */\nasync function batchEmbed(\n sentences: string[],\n embedFn: EmbedFn,\n batchSize: number,\n): Promise<number[][]> {\n const allEmbeddings: number[][] = [];\n\n for (let i = 0; i < sentences.length; i += batchSize) {\n const batch = sentences.slice(i, i + batchSize);\n const batchResult = await embedFn(batch);\n for (const vec of batchResult) {\n allEmbeddings.push(vec);\n }\n }\n\n return allEmbeddings;\n}\n\nfunction findEmbeddingDimensionMismatch(\n embeddings: number[][],\n): { expected: number; actual: number; index: number } | null {\n if (embeddings.length <= 1) return null;\n const expected = embeddings[0].length;\n for (let i = 1; i < embeddings.length; i++) {\n const actual = embeddings[i].length;\n if (actual !== expected) {\n return { expected, actual, index: i };\n }\n }\n return null;\n}\n\n/**\n * Build segments from boundary indices.\n * boundaries are sentence indices at which splits occur (i.e., the split\n * happens AFTER the boundary index sentence).\n */\nfunction buildSegments(\n sentences: string[],\n boundaries: number[],\n): string[][] {\n const sorted = [...boundaries].sort((a, b) => a - b);\n const segments: string[][] = [];\n let start = 0;\n\n for (const b of sorted) {\n // Split after sentence at index b: segment is [start .. b]\n const splitPoint = b + 1;\n if (splitPoint > start && splitPoint <= sentences.length) {\n segments.push(sentences.slice(start, splitPoint));\n start = splitPoint;\n }\n }\n\n // Remaining sentences\n if (start < sentences.length) {\n segments.push(sentences.slice(start));\n }\n\n return segments;\n}\n\n/**\n * Merge short segments (below minTokens) with their neighbor.\n * Prefers merging forward; falls back to merging backward.\n */\nfunction mergeShortSegments(\n segments: string[][],\n minTokens: number,\n): string[][] {\n if (segments.length <= 1) return segments;\n\n const merged: string[][] = [];\n let buffer: string[] = [];\n\n for (let i = 0; i < segments.length; i++) {\n buffer = [...buffer, ...segments[i]];\n const tokenCount = estimateTokens(buffer.join(\" \"));\n\n if (tokenCount >= minTokens || i === segments.length - 1) {\n merged.push(buffer);\n buffer = [];\n }\n }\n\n // If the last merge left a dangling buffer, attach it to the last segment\n if (buffer.length > 0) {\n if (merged.length > 0) {\n merged[merged.length - 1] = [...merged[merged.length - 1], ...buffer];\n } else {\n merged.push(buffer);\n }\n }\n\n return merged;\n}\n\n/**\n * Split an oversized segment using recursive chunking.\n */\nfunction splitLongSegment(\n segment: string[],\n maxTokens: number,\n targetTokens: number,\n): SemanticChunk[] {\n const text = segment.join(\" \");\n // Cap targetTokens to maxTokens so recursive splitting never produces\n // segments larger than the configured maximum (Finding 2, PR #420).\n const cappedTarget = Math.min(targetTokens, maxTokens);\n const result: ChunkResult = chunkContent(text, {\n targetTokens: cappedTarget,\n minTokens: Math.min(cappedTarget, maxTokens),\n overlapSentences: 0,\n });\n\n return result.chunks.map((c) => ({\n content: c.content,\n index: c.index,\n tokenCount: c.tokenCount,\n boundaryScore: 0,\n }));\n}\n\n/**\n * Semantic chunking with smoothing-based topic boundary detection.\n *\n * @param content - Full text to chunk.\n * @param embedFn - Async function that embeds an array of texts.\n * @param config - Optional partial config overrides.\n * @returns SemanticChunkResult\n */\nexport async function semanticChunkContent(\n content: string,\n embedFn: EmbedFn,\n config?: Partial<SemanticChunkingConfig>,\n): Promise<SemanticChunkResult> {\n const cfg: SemanticChunkingConfig = {\n ...DEFAULT_SEMANTIC_CHUNKING_CONFIG,\n ...config,\n };\n\n // Guard against non-positive batch size which would cause an infinite loop\n const batchSize = Math.max(1, cfg.embeddingBatchSize);\n\n // --- Empty / trivially short input ---\n if (!content || content.trim().length === 0) {\n return {\n chunked: false,\n chunks: [],\n boundaries: [],\n method: \"semantic\",\n };\n }\n\n const sentences = splitSentences(content);\n\n if (sentences.length <= 1) {\n const tokenCount = estimateTokens(content);\n return {\n chunked: false,\n chunks: [\n {\n content: content.trim(),\n index: 0,\n tokenCount,\n boundaryScore: 1,\n },\n ],\n boundaries: [],\n method: \"semantic\",\n };\n }\n\n // If total tokens is short enough, return as single chunk\n const totalTokens = estimateTokens(content);\n if (totalTokens <= cfg.minTokens) {\n return {\n chunked: false,\n chunks: [\n {\n content: content.trim(),\n index: 0,\n tokenCount: totalTokens,\n boundaryScore: 1,\n },\n ],\n boundaries: [],\n method: \"semantic\",\n };\n }\n\n // --- Attempt embedding ---\n let embeddings: number[][];\n try {\n embeddings = await batchEmbed(sentences, embedFn, batchSize);\n } catch {\n // Embedding failed — fall back if configured\n if (cfg.fallbackToRecursive) {\n return buildRecursiveFallback(content, cfg);\n }\n throw new Error(\n \"Semantic chunking failed: embedding function threw and fallbackToRecursive is disabled\",\n );\n }\n\n if (embeddings.length !== sentences.length) {\n if (cfg.fallbackToRecursive) {\n return buildRecursiveFallback(content, cfg);\n }\n throw new Error(\n `Semantic chunking failed: expected ${sentences.length} embeddings but received ${embeddings.length}`,\n );\n }\n\n const dimensionMismatch = findEmbeddingDimensionMismatch(embeddings);\n if (dimensionMismatch) {\n if (cfg.fallbackToRecursive) {\n return buildRecursiveFallback(content, cfg);\n }\n throw new Error(\n `Semantic chunking failed: embedding vectors have mismatched dimensions ` +\n `(${dimensionMismatch.expected} vs ${dimensionMismatch.actual} at index ${dimensionMismatch.index})`,\n );\n }\n\n // --- Compute pairwise cosine similarity ---\n const similarities: number[] = [];\n for (let i = 0; i < sentences.length - 1; i++) {\n similarities.push(cosineSimilarity(embeddings[i], embeddings[i + 1]));\n }\n\n // If only one pair (2 sentences), nothing to smooth or split meaningfully.\n // However, if the combined content exceeds maxTokens, apply recursive splitting.\n if (similarities.length <= 1) {\n if (totalTokens > cfg.maxTokens) {\n return buildRecursiveFallback(content, cfg);\n }\n return {\n chunked: false,\n chunks: [\n {\n content: content.trim(),\n index: 0,\n tokenCount: totalTokens,\n boundaryScore: similarities.length === 1 ? similarities[0] : 1,\n },\n ],\n boundaries: [],\n method: \"semantic\",\n };\n }\n\n // --- Smooth the similarity series ---\n const smoothed = movingAverage(similarities, cfg.smoothingWindowSize);\n\n // --- Detect boundaries: local minima below (mean - k * stddev) ---\n const m = mean(smoothed);\n const s = stddev(smoothed);\n const threshold = m - cfg.boundaryThresholdStdDevs * s;\n const rawBoundaries = findLocalMinima(smoothed, threshold);\n\n // --- Build segments, merge short, split long ---\n let segments = buildSegments(sentences, rawBoundaries);\n segments = mergeShortSegments(segments, cfg.minTokens);\n\n // --- Convert segments to chunks, splitting oversized ones ---\n const chunks: SemanticChunk[] = [];\n const finalBoundaries: number[] = [];\n let sentenceOffset = 0;\n\n for (let segIdx = 0; segIdx < segments.length; segIdx++) {\n const segment = segments[segIdx];\n const segText = segment.join(\" \");\n const segTokens = estimateTokens(segText);\n\n if (segTokens > cfg.maxTokens) {\n // Recursive split for oversized segment\n const subChunks = splitLongSegment(segment, cfg.maxTokens, cfg.targetTokens);\n for (const sc of subChunks) {\n chunks.push({\n ...sc,\n index: chunks.length,\n });\n }\n } else {\n // Compute boundary score: the similarity at the trailing edge\n const trailingSentenceIdx = sentenceOffset + segment.length - 1;\n let bScore = 1;\n if (\n trailingSentenceIdx < similarities.length &&\n segIdx < segments.length - 1\n ) {\n bScore = smoothed[trailingSentenceIdx] ?? similarities[trailingSentenceIdx] ?? 1;\n }\n\n chunks.push({\n content: segText,\n index: chunks.length,\n tokenCount: segTokens,\n boundaryScore: bScore,\n });\n }\n\n // Record boundaries (all but the last segment produce a boundary)\n if (segIdx < segments.length - 1) {\n finalBoundaries.push(sentenceOffset + segment.length - 1);\n }\n sentenceOffset += segment.length;\n }\n\n return {\n chunked: chunks.length > 1,\n chunks,\n boundaries: finalBoundaries,\n method: \"semantic\",\n };\n}\n\n// ---------------------------------------------------------------------------\n// Recursive fallback helper\n// ---------------------------------------------------------------------------\n\nfunction buildRecursiveFallback(\n content: string,\n cfg: SemanticChunkingConfig,\n): SemanticChunkResult {\n // Cap targetTokens to maxTokens so the recursive fallback path honours the\n // same constraint as splitLongSegment (PR #439 post-merge cursor[bot] finding).\n const cappedTarget = Math.min(cfg.targetTokens, cfg.maxTokens);\n const result: ChunkResult = chunkContent(content, {\n targetTokens: cappedTarget,\n minTokens: Math.min(cfg.minTokens, cappedTarget),\n overlapSentences: 0,\n });\n\n return {\n chunked: result.chunked,\n chunks: result.chunks.map((c) => ({\n ...c,\n boundaryScore: 0,\n })),\n boundaries: [],\n method: \"recursive-fallback\",\n };\n}\n"],"mappings":"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","names":[]}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|