akm-cli 0.8.0-rc2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{.github/CHANGELOG.md → CHANGELOG.md} +191 -3
- package/README.md +22 -6
- package/SECURITY.md +93 -0
- package/dist/cli/config-migrate.js +144 -0
- package/dist/cli/config-validate.js +39 -0
- package/dist/cli/confirm.js +73 -0
- package/dist/cli/parse-args.js +93 -3
- package/dist/cli/shared.js +129 -0
- package/dist/cli.js +2141 -1268
- package/dist/commands/add-cli.js +279 -0
- package/dist/commands/agent-dispatch.js +20 -12
- package/dist/commands/agent-support.js +11 -5
- package/dist/commands/completions.js +3 -0
- package/dist/commands/config-cli.js +129 -517
- package/dist/commands/consolidate.js +1533 -144
- package/dist/commands/curate.js +44 -3
- package/dist/commands/db-cli.js +23 -0
- package/dist/commands/distill-promotion-policy.js +5 -3
- package/dist/commands/distill.js +906 -100
- package/dist/commands/env.js +213 -0
- package/dist/commands/eval-cases.js +3 -0
- package/dist/commands/events.js +3 -0
- package/dist/commands/extract-cli.js +127 -0
- package/dist/commands/extract-prompt.js +204 -0
- package/dist/commands/extract.js +477 -0
- package/dist/commands/feedback-cli.js +331 -0
- package/dist/commands/graph.js +260 -5
- package/dist/commands/health.js +977 -51
- package/dist/commands/help/help-accept.md +6 -3
- package/dist/commands/help/help-improve.md +36 -8
- package/dist/commands/help/help-proposals.md +7 -4
- package/dist/commands/help/help-reject.md +5 -2
- package/dist/commands/history.js +51 -16
- package/dist/commands/improve-auto-accept.js +97 -0
- package/dist/commands/improve-cli.js +236 -0
- package/dist/commands/improve-profiles.js +184 -0
- package/dist/commands/improve-result-file.js +167 -0
- package/dist/commands/improve.js +1725 -332
- package/dist/commands/info.js +3 -0
- package/dist/commands/init.js +49 -1
- package/dist/commands/installed-stashes.js +6 -23
- package/dist/commands/knowledge.js +3 -0
- package/dist/commands/lint/agent-linter.js +3 -0
- package/dist/commands/lint/base-linter.js +199 -5
- package/dist/commands/lint/command-linter.js +3 -0
- package/dist/commands/lint/default-linter.js +3 -0
- package/dist/commands/lint/env-key-rules.js +154 -0
- package/dist/commands/lint/index.js +92 -3
- package/dist/commands/lint/knowledge-linter.js +3 -0
- package/dist/commands/lint/markdown-insertion.js +343 -0
- package/dist/commands/lint/memory-linter.js +3 -0
- package/dist/commands/lint/registry.js +3 -0
- package/dist/commands/lint/skill-linter.js +3 -0
- package/dist/commands/lint/task-linter.js +15 -12
- package/dist/commands/lint/types.js +3 -0
- package/dist/commands/lint/workflow-linter.js +3 -0
- package/dist/commands/lint.js +3 -0
- package/dist/commands/migration-help.js +5 -2
- package/dist/commands/proposal-drain-policies.js +128 -0
- package/dist/commands/proposal-drain.js +477 -0
- package/dist/commands/proposal.js +60 -6
- package/dist/commands/propose.js +24 -19
- package/dist/commands/reflect.js +1004 -94
- package/dist/commands/registry-cli.js +150 -0
- package/dist/commands/registry-search.js +3 -0
- package/dist/commands/remember-cli.js +257 -0
- package/dist/commands/remember.js +15 -6
- package/dist/commands/schema-repair.js +88 -15
- package/dist/commands/search.js +99 -14
- package/dist/commands/secret.js +173 -0
- package/dist/commands/self-update.js +3 -0
- package/dist/commands/show.js +32 -13
- package/dist/commands/source-add.js +7 -35
- package/dist/commands/source-clone.js +3 -0
- package/dist/commands/source-manage.js +3 -0
- package/dist/commands/tasks.js +161 -95
- package/dist/commands/url-checker.js +3 -0
- package/dist/core/action-contributors.js +3 -0
- package/dist/core/asset-ref.js +13 -2
- package/dist/core/asset-registry.js +9 -2
- package/dist/core/asset-serialize.js +88 -0
- package/dist/core/asset-spec.js +61 -5
- package/dist/core/common.js +93 -5
- package/dist/core/concurrent.js +3 -0
- package/dist/core/config-io.js +347 -0
- package/dist/core/config-migration.js +622 -0
- package/dist/core/config-schema.js +558 -0
- package/dist/core/config-sources.js +108 -0
- package/dist/core/config-types.js +4 -0
- package/dist/core/config-walker.js +337 -0
- package/dist/core/config.js +366 -1077
- package/dist/core/errors.js +42 -20
- package/dist/core/events.js +31 -25
- package/dist/core/file-lock.js +104 -0
- package/dist/core/frontmatter.js +75 -10
- package/dist/core/lesson-lint.js +3 -0
- package/dist/core/markdown.js +3 -0
- package/dist/core/memory-belief.js +62 -0
- package/dist/core/memory-contradiction-detect.js +274 -0
- package/dist/core/memory-improve.js +142 -14
- package/dist/core/parse.js +3 -0
- package/dist/core/paths.js +218 -50
- package/dist/core/proposal-quality-validators.js +380 -0
- package/dist/core/proposal-validators.js +11 -3
- package/dist/core/proposals.js +464 -5
- package/dist/core/state-db.js +349 -56
- package/dist/core/text-truncation.js +107 -0
- package/dist/core/time.js +3 -0
- package/dist/core/tty.js +59 -0
- package/dist/core/warn.js +7 -2
- package/dist/core/write-source.js +12 -0
- package/dist/indexer/db-backup.js +391 -0
- package/dist/indexer/db-search.js +136 -28
- package/dist/indexer/db.js +661 -166
- package/dist/indexer/ensure-index.js +3 -0
- package/dist/indexer/file-context.js +3 -0
- package/dist/indexer/graph-boost.js +162 -40
- package/dist/indexer/graph-db.js +241 -51
- package/dist/indexer/graph-dedup.js +3 -7
- package/dist/indexer/graph-extraction.js +242 -149
- package/dist/indexer/index-context.js +3 -9
- package/dist/indexer/indexer.js +84 -14
- package/dist/indexer/llm-cache.js +24 -19
- package/dist/indexer/manifest.js +3 -0
- package/dist/indexer/matchers.js +184 -11
- package/dist/indexer/memory-inference.js +94 -50
- package/dist/indexer/metadata-contributors.js +3 -0
- package/dist/indexer/metadata.js +110 -50
- package/dist/indexer/path-resolver.js +3 -0
- package/dist/indexer/project-context.js +192 -0
- package/dist/indexer/ranking-contributors.js +134 -7
- package/dist/indexer/ranking.js +8 -1
- package/dist/indexer/search-fields.js +5 -9
- package/dist/indexer/search-hit-enrichers.js +91 -2
- package/dist/indexer/search-source.js +20 -1
- package/dist/indexer/semantic-status.js +4 -1
- package/dist/indexer/staleness-detect.js +447 -0
- package/dist/indexer/usage-events.js +12 -9
- package/dist/indexer/walker.js +3 -0
- package/dist/integrations/agent/builders.js +135 -0
- package/dist/integrations/agent/config.js +121 -401
- package/dist/integrations/agent/detect.js +3 -0
- package/dist/integrations/agent/index.js +6 -14
- package/dist/integrations/agent/model-aliases.js +55 -0
- package/dist/integrations/agent/profiles.js +3 -0
- package/dist/integrations/agent/prompts.js +137 -8
- package/dist/integrations/agent/runner.js +208 -0
- package/dist/integrations/agent/sdk-runner.js +8 -2
- package/dist/integrations/agent/spawn.js +54 -14
- package/dist/integrations/github.js +3 -0
- package/dist/integrations/lockfile.js +22 -51
- package/dist/integrations/session-logs/index.js +4 -0
- package/dist/integrations/session-logs/inline-refs.js +35 -0
- package/dist/integrations/session-logs/pre-filter.js +152 -0
- package/dist/integrations/session-logs/providers/claude-code.js +226 -0
- package/dist/integrations/session-logs/providers/opencode.js +231 -25
- package/dist/integrations/session-logs/types.js +3 -0
- package/dist/llm/call-ai.js +14 -26
- package/dist/llm/client.js +16 -2
- package/dist/llm/embedder.js +20 -29
- package/dist/llm/embedders/cache.js +3 -7
- package/dist/llm/embedders/local.js +42 -1
- package/dist/llm/embedders/remote.js +20 -8
- package/dist/llm/embedders/types.js +3 -7
- package/dist/llm/feature-gate.js +92 -56
- package/dist/llm/graph-extract.js +401 -30
- package/dist/llm/index-passes.js +44 -29
- package/dist/llm/memory-infer.js +30 -2
- package/dist/llm/metadata-enhance.js +3 -7
- package/dist/llm/prompts/extract-session.md +80 -0
- package/dist/llm/prompts/graph-extract-user-prompt.md +24 -1
- package/dist/output/cli-hints-full.md +60 -32
- package/dist/output/cli-hints-short.md +10 -7
- package/dist/output/cli-hints.js +5 -2
- package/dist/output/context.js +60 -8
- package/dist/output/renderers.js +170 -194
- package/dist/output/shapes/curate.js +56 -0
- package/dist/output/shapes/distill.js +10 -0
- package/dist/output/shapes/env-list.js +19 -0
- package/dist/output/shapes/events.js +11 -0
- package/dist/output/shapes/helpers.js +424 -0
- package/dist/output/shapes/history.js +7 -0
- package/dist/output/shapes/passthrough.js +105 -0
- package/dist/output/shapes/proposal-accept.js +7 -0
- package/dist/output/shapes/proposal-diff.js +7 -0
- package/dist/output/shapes/proposal-list.js +7 -0
- package/dist/output/shapes/proposal-producer.js +11 -0
- package/dist/output/shapes/proposal-reject.js +7 -0
- package/dist/output/shapes/proposal-show.js +7 -0
- package/dist/output/shapes/registry-search.js +6 -0
- package/dist/output/shapes/registry.js +30 -0
- package/dist/output/shapes/search.js +6 -0
- package/dist/output/shapes/secret-list.js +19 -0
- package/dist/output/shapes/show.js +6 -0
- package/dist/output/shapes/vault-list.js +19 -0
- package/dist/output/shapes.js +51 -549
- package/dist/output/text/add.js +6 -0
- package/dist/output/text/clone.js +6 -0
- package/dist/output/text/config.js +6 -0
- package/dist/output/text/curate.js +6 -0
- package/dist/output/text/distill.js +7 -0
- package/dist/output/text/enable-disable.js +7 -0
- package/dist/output/text/events.js +10 -0
- package/dist/output/text/feedback.js +6 -0
- package/dist/output/text/helpers.js +1059 -0
- package/dist/output/text/history.js +7 -0
- package/dist/output/text/import.js +6 -0
- package/dist/output/text/index.js +6 -0
- package/dist/output/text/info.js +6 -0
- package/dist/output/text/init.js +6 -0
- package/dist/output/text/list.js +6 -0
- package/dist/output/text/proposal-producer.js +8 -0
- package/dist/output/text/proposal.js +12 -0
- package/dist/output/text/registry-commands.js +11 -0
- package/dist/output/text/registry.js +30 -0
- package/dist/output/text/remember.js +6 -0
- package/dist/output/text/remove.js +6 -0
- package/dist/output/text/save.js +6 -0
- package/dist/output/text/search.js +6 -0
- package/dist/output/text/show.js +6 -0
- package/dist/output/text/update.js +6 -0
- package/dist/output/text/upgrade.js +6 -0
- package/dist/output/text/vault.js +16 -0
- package/dist/output/text/wiki.js +15 -0
- package/dist/output/text/workflow.js +14 -0
- package/dist/output/text.js +44 -1329
- package/dist/registry/build-index.js +3 -0
- package/dist/registry/create-provider-registry.js +3 -0
- package/dist/registry/factory.js +4 -1
- package/dist/registry/origin-resolve.js +3 -0
- package/dist/registry/providers/index.js +3 -0
- package/dist/registry/providers/skills-sh.js +11 -2
- package/dist/registry/providers/static-index.js +10 -1
- package/dist/registry/providers/types.js +3 -24
- package/dist/registry/resolve.js +11 -16
- package/dist/registry/types.js +3 -0
- package/dist/scripts/migrate-storage.js +17767 -0
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
- package/dist/scripts/migrations/v16-to-v17.js +141 -0
- package/dist/setup/detect.js +3 -0
- package/dist/setup/ripgrep-install.js +3 -0
- package/dist/setup/ripgrep-resolve.js +3 -0
- package/dist/setup/setup.js +306 -67
- package/dist/setup/steps.js +3 -15
- package/dist/sources/include.js +3 -0
- package/dist/sources/provider-factory.js +3 -11
- package/dist/sources/provider.js +3 -20
- package/dist/sources/providers/filesystem.js +19 -23
- package/dist/sources/providers/git.js +171 -21
- package/dist/sources/providers/index.js +3 -0
- package/dist/sources/providers/install-types.js +3 -13
- package/dist/sources/providers/npm.js +3 -4
- package/dist/sources/providers/provider-utils.js +3 -0
- package/dist/sources/providers/sync-from-ref.js +3 -11
- package/dist/sources/providers/tar-utils.js +3 -0
- package/dist/sources/providers/website.js +18 -22
- package/dist/sources/resolve.js +3 -0
- package/dist/sources/types.js +3 -0
- package/dist/sources/website-ingest.js +3 -0
- package/dist/tasks/backends/cron.js +3 -0
- package/dist/tasks/backends/exec-utils.js +3 -0
- package/dist/tasks/backends/index.js +3 -11
- package/dist/tasks/backends/launchd.js +3 -0
- package/dist/tasks/backends/schtasks.js +3 -0
- package/dist/tasks/parser.js +51 -38
- package/dist/tasks/resolveAkmBin.js +3 -0
- package/dist/tasks/runner.js +35 -9
- package/dist/tasks/schedule.js +20 -1
- package/dist/tasks/schema.js +5 -3
- package/dist/tasks/validator.js +6 -3
- package/dist/version.js +3 -0
- package/dist/wiki/wiki-templates.js +3 -0
- package/dist/wiki/wiki.js +3 -0
- package/dist/workflows/authoring.js +3 -0
- package/dist/workflows/cli.js +3 -0
- package/dist/workflows/db.js +140 -10
- package/dist/workflows/document-cache.js +3 -10
- package/dist/workflows/parser.js +3 -0
- package/dist/workflows/renderer.js +3 -0
- package/dist/workflows/runs.js +18 -1
- package/dist/workflows/schema.js +3 -0
- package/dist/workflows/scope-key.js +3 -0
- package/dist/workflows/validator.js +5 -9
- package/docs/README.md +7 -2
- package/docs/data-and-telemetry.md +225 -0
- package/docs/migration/release-notes/0.7.5.md +2 -2
- package/docs/migration/release-notes/0.8.0.md +57 -5
- package/docs/migration/v0.7-to-v0.8.md +1378 -0
- package/package.json +28 -11
- package/.github/LICENSE +0 -374
- package/dist/commands/install-audit.js +0 -385
- package/dist/commands/vault.js +0 -310
- package/dist/indexer/match-contributors.js +0 -141
- package/dist/integrations/agent/pipeline.js +0 -39
- package/dist/integrations/agent/runners.js +0 -31
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* LLM-based contradiction-detection pass for derived memories (M-1 / #367).
|
|
6
|
+
*
|
|
7
|
+
* Runs BEFORE `analyzeMemoryCleanup` to populate `contradictedBy` frontmatter
|
|
8
|
+
* edges so the existing `resolveFamilyContradictions` SCC resolver has real
|
|
9
|
+
* input to work on. Without this pass the SCC resolver operates on a nearly
|
|
10
|
+
* empty edge graph because no automated subsystem was previously generating
|
|
11
|
+
* contradiction edges — the elegant Tarjan implementation in memory-improve.ts
|
|
12
|
+
* had no input.
|
|
13
|
+
*
|
|
14
|
+
* # Algorithm
|
|
15
|
+
*
|
|
16
|
+
* 1. Collect all derived memories grouped by `parentRef` family.
|
|
17
|
+
* 2. For each family, enumerate candidate pairs (limited to MAX_FAMILY_SIZE).
|
|
18
|
+
* 3. For each pair, call the LLM to judge whether the two memories are in
|
|
19
|
+
* direct factual conflict.
|
|
20
|
+
* 4. For confirmed contradictions, write `contradictedBy` edges directly to
|
|
21
|
+
* the losing memory's frontmatter (same mechanism as `persistBeliefStateTransition`).
|
|
22
|
+
*
|
|
23
|
+
* # LLM Feature Gate
|
|
24
|
+
*
|
|
25
|
+
* The pass is gated behind `profiles.improve.default.processes.consolidate.contradictionDetection.enabled`.
|
|
26
|
+
* When the gate is disabled or no LLM is configured,
|
|
27
|
+
* the pass is a no-op and `analyzeMemoryCleanup` proceeds with only manually
|
|
28
|
+
* annotated edges.
|
|
29
|
+
*
|
|
30
|
+
* # References
|
|
31
|
+
*
|
|
32
|
+
* - Zep / Graphiti (arXiv:2501.13956): writes contradiction edges at detection time.
|
|
33
|
+
* - ATMS (de Kleer 1986): assumption-based truth maintenance via edge propagation.
|
|
34
|
+
* - mem0 contradiction probe (arXiv:2504.19413): pairwise LLM-judge pattern.
|
|
35
|
+
*/
|
|
36
|
+
import fs from "node:fs";
|
|
37
|
+
import path from "node:path";
|
|
38
|
+
import { chatCompletion, parseEmbeddedJsonResponse } from "../llm/client";
|
|
39
|
+
import { tryLlmFeature } from "../llm/feature-gate";
|
|
40
|
+
import { assembleAsset } from "./asset-serialize";
|
|
41
|
+
import { getDefaultLlmConfig } from "./config";
|
|
42
|
+
import { parseFrontmatter } from "./frontmatter";
|
|
43
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
44
|
+
/**
|
|
45
|
+
* Maximum family size for pairwise contradiction checking. Families larger
|
|
46
|
+
* than this are skipped to bound the LLM call count (O(n²) pairs).
|
|
47
|
+
*/
|
|
48
|
+
const MAX_FAMILY_SIZE = 8;
|
|
49
|
+
/**
|
|
50
|
+
* Maximum number of contradiction pairs to check per improve run, across all
|
|
51
|
+
* families. Prevents runaway LLM usage on stashes with many memories.
|
|
52
|
+
*/
|
|
53
|
+
const MAX_PAIRS_PER_RUN = 20;
|
|
54
|
+
/**
|
|
55
|
+
* Truncation limit for memory body content sent to the LLM judge.
|
|
56
|
+
* Keeps prompts compact while preserving the key factual claims.
|
|
57
|
+
*/
|
|
58
|
+
const BODY_TRUNCATION = 800;
|
|
59
|
+
// ── Prompt builder ────────────────────────────────────────────────────────────
|
|
60
|
+
function buildContradictionJudgePrompt(a, b) {
|
|
61
|
+
return [
|
|
62
|
+
"You are evaluating two derived memory entries to determine if they contain",
|
|
63
|
+
"directly contradictory factual claims about the same subject.",
|
|
64
|
+
"",
|
|
65
|
+
"Memory A:",
|
|
66
|
+
`Ref: ${a.ref}`,
|
|
67
|
+
`Description: ${a.description || "(none)"}`,
|
|
68
|
+
"Content:",
|
|
69
|
+
"```",
|
|
70
|
+
a.body.slice(0, BODY_TRUNCATION),
|
|
71
|
+
"```",
|
|
72
|
+
"",
|
|
73
|
+
"Memory B:",
|
|
74
|
+
`Ref: ${b.ref}`,
|
|
75
|
+
`Description: ${b.description || "(none)"}`,
|
|
76
|
+
"Content:",
|
|
77
|
+
"```",
|
|
78
|
+
b.body.slice(0, BODY_TRUNCATION),
|
|
79
|
+
"```",
|
|
80
|
+
"",
|
|
81
|
+
"Answer ONLY with valid JSON — no prose, no code fences:",
|
|
82
|
+
'{"contradicts": true|false, "reason": "<one sentence explaining why or why not>"}',
|
|
83
|
+
"",
|
|
84
|
+
"A contradiction means the memories make mutually exclusive factual claims about the",
|
|
85
|
+
"same topic (e.g. Memory A says 'always use VPN' while Memory B says 'VPN is optional').",
|
|
86
|
+
"If the memories are complementary, about different topics, or one supersedes the other",
|
|
87
|
+
"without direct conflict, return false.",
|
|
88
|
+
].join("\n");
|
|
89
|
+
}
|
|
90
|
+
// ── Filesystem helpers ────────────────────────────────────────────────────────
|
|
91
|
+
function* walkMarkdownFilesLocal(root) {
|
|
92
|
+
if (!fs.existsSync(root))
|
|
93
|
+
return;
|
|
94
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
95
|
+
const full = path.join(root, entry.name);
|
|
96
|
+
if (entry.isDirectory())
|
|
97
|
+
yield* walkMarkdownFilesLocal(full);
|
|
98
|
+
else if (entry.isFile() && entry.name.endsWith(".md"))
|
|
99
|
+
yield full;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function toMemoryRef(memoriesDir, filePath) {
|
|
103
|
+
const rel = path.relative(memoriesDir, filePath);
|
|
104
|
+
if (!rel || rel.startsWith(".."))
|
|
105
|
+
return undefined;
|
|
106
|
+
const name = rel.replace(/\\/g, "/").replace(/\.md$/i, "");
|
|
107
|
+
return `memory:${name}`;
|
|
108
|
+
}
|
|
109
|
+
function isDerivedMemory(filePath, frontmatter) {
|
|
110
|
+
// Name-based guard (M-2): the .derived suffix is structural and immutable.
|
|
111
|
+
const base = path.basename(filePath, ".md");
|
|
112
|
+
if (base.endsWith(".derived"))
|
|
113
|
+
return true;
|
|
114
|
+
// Frontmatter-based guard: inferred: true marks explicit child memories.
|
|
115
|
+
return frontmatter.inferred === true;
|
|
116
|
+
}
|
|
117
|
+
function resolveParentRef(filePath, frontmatter, memoriesRootDir) {
|
|
118
|
+
// Prefer the explicit source: frontmatter.
|
|
119
|
+
const source = frontmatter.source;
|
|
120
|
+
if (typeof source === "string" && source.startsWith("memory:"))
|
|
121
|
+
return source;
|
|
122
|
+
// Fall back to deriving parent from the file name (strip .derived suffix).
|
|
123
|
+
const base = path.basename(filePath, ".md");
|
|
124
|
+
if (base.endsWith(".derived")) {
|
|
125
|
+
const parentName = base.slice(0, -".derived".length);
|
|
126
|
+
// Use the stash memories root so nested paths (e.g. memories/nested/foo.derived.md)
|
|
127
|
+
// resolve to the correct relative ref (memory:nested/foo, not memory:foo).
|
|
128
|
+
const rootDir = memoriesRootDir ?? path.dirname(filePath);
|
|
129
|
+
const rel = path.relative(rootDir, path.join(path.dirname(filePath), parentName));
|
|
130
|
+
return `memory:${rel.replace(/\\/g, "/")}`;
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
// ── Edge writing ─────────────────────────────────────────────────────────────
|
|
135
|
+
/**
|
|
136
|
+
* Write a `contradictedBy` edge to the losing memory's frontmatter file.
|
|
137
|
+
* Preserves all existing frontmatter keys; only adds/updates `contradictedBy`
|
|
138
|
+
* and `beliefState: contradicted`.
|
|
139
|
+
*/
|
|
140
|
+
/** Returns true if the edge was newly written, false if it already existed. */
|
|
141
|
+
function writeContradictedByEdge(filePath, contradictedByRef) {
|
|
142
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
143
|
+
const parsed = parseFrontmatter(raw);
|
|
144
|
+
const existing = Array.isArray(parsed.data.contradictedBy) ? parsed.data.contradictedBy : [];
|
|
145
|
+
if (existing.includes(contradictedByRef))
|
|
146
|
+
return false; // Edge already written.
|
|
147
|
+
const updatedContradictedBy = [...new Set([...existing, contradictedByRef])].sort();
|
|
148
|
+
const nextFrontmatter = {
|
|
149
|
+
...parsed.data,
|
|
150
|
+
contradictedBy: updatedContradictedBy,
|
|
151
|
+
beliefState: "contradicted",
|
|
152
|
+
};
|
|
153
|
+
fs.writeFileSync(filePath, assembleAsset(nextFrontmatter, parsed.content), "utf8");
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
// ── Main entry point ──────────────────────────────────────────────────────────
|
|
157
|
+
/**
|
|
158
|
+
* Run the LLM-based contradiction-detection pass on derived memories in
|
|
159
|
+
* `<stashDir>/memories/`. Writes `contradictedBy` frontmatter edges for
|
|
160
|
+
* confirmed contradiction pairs so the subsequent `resolveFamilyContradictions`
|
|
161
|
+
* SCC pass has edges to work on.
|
|
162
|
+
*
|
|
163
|
+
* @param stashDir - Root stash directory.
|
|
164
|
+
* @param config - Loaded AKM config (used to access LLM settings).
|
|
165
|
+
* @param chat - Optional chat seam for testing (defaults to chatCompletion).
|
|
166
|
+
*/
|
|
167
|
+
export async function detectAndWriteContradictions(stashDir, config, chat = chatCompletion) {
|
|
168
|
+
const result = {
|
|
169
|
+
familiesExamined: 0,
|
|
170
|
+
pairsChecked: 0,
|
|
171
|
+
edgesWritten: 0,
|
|
172
|
+
warnings: [],
|
|
173
|
+
};
|
|
174
|
+
const contradictionLlm = getDefaultLlmConfig(config);
|
|
175
|
+
if (!contradictionLlm)
|
|
176
|
+
return result;
|
|
177
|
+
// Collect derived memories grouped by parent.
|
|
178
|
+
const memoriesDir = path.join(stashDir, "memories");
|
|
179
|
+
const byParent = new Map();
|
|
180
|
+
for (const filePath of walkMarkdownFilesLocal(memoriesDir)) {
|
|
181
|
+
let raw;
|
|
182
|
+
try {
|
|
183
|
+
raw = fs.readFileSync(filePath, "utf8");
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const parsed = parseFrontmatter(raw);
|
|
189
|
+
if (!isDerivedMemory(filePath, parsed.data))
|
|
190
|
+
continue;
|
|
191
|
+
const parentRef = resolveParentRef(filePath, parsed.data, memoriesDir);
|
|
192
|
+
if (!parentRef)
|
|
193
|
+
continue;
|
|
194
|
+
const ref = toMemoryRef(memoriesDir, filePath);
|
|
195
|
+
if (!ref)
|
|
196
|
+
continue;
|
|
197
|
+
const entry = {
|
|
198
|
+
filePath,
|
|
199
|
+
ref,
|
|
200
|
+
parentRef,
|
|
201
|
+
body: parsed.content.trim(),
|
|
202
|
+
description: typeof parsed.data.description === "string" ? parsed.data.description : "",
|
|
203
|
+
};
|
|
204
|
+
const family = byParent.get(parentRef) ?? [];
|
|
205
|
+
family.push(entry);
|
|
206
|
+
byParent.set(parentRef, family);
|
|
207
|
+
}
|
|
208
|
+
let totalPairsChecked = 0;
|
|
209
|
+
for (const [, family] of byParent) {
|
|
210
|
+
if (family.length < 2)
|
|
211
|
+
continue;
|
|
212
|
+
if (family.length > MAX_FAMILY_SIZE) {
|
|
213
|
+
result.warnings.push(`Skipping contradiction check for family of ${family.length} members (exceeds MAX_FAMILY_SIZE=${MAX_FAMILY_SIZE})`);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
result.familiesExamined++;
|
|
217
|
+
for (let i = 0; i < family.length - 1; i++) {
|
|
218
|
+
for (let j = i + 1; j < family.length; j++) {
|
|
219
|
+
if (totalPairsChecked >= MAX_PAIRS_PER_RUN)
|
|
220
|
+
break;
|
|
221
|
+
const a = family[i];
|
|
222
|
+
const b = family[j];
|
|
223
|
+
if (!a || !b)
|
|
224
|
+
continue;
|
|
225
|
+
// Skip pairs where edges already exist in BOTH directions (no new information).
|
|
226
|
+
const aRaw = fs.readFileSync(a.filePath, "utf8");
|
|
227
|
+
const aParsed = parseFrontmatter(aRaw);
|
|
228
|
+
const aCB = Array.isArray(aParsed.data.contradictedBy)
|
|
229
|
+
? aParsed.data.contradictedBy
|
|
230
|
+
: [];
|
|
231
|
+
const bRaw = fs.readFileSync(b.filePath, "utf8");
|
|
232
|
+
const bParsed = parseFrontmatter(bRaw);
|
|
233
|
+
const bCB = Array.isArray(bParsed.data.contradictedBy)
|
|
234
|
+
? bParsed.data.contradictedBy
|
|
235
|
+
: [];
|
|
236
|
+
if (aCB.includes(b.ref) && bCB.includes(a.ref))
|
|
237
|
+
continue;
|
|
238
|
+
const prompt = buildContradictionJudgePrompt(a, b);
|
|
239
|
+
const judgeResult = await tryLlmFeature("memory_contradiction_detection", config, async () => {
|
|
240
|
+
return chat(contradictionLlm, [
|
|
241
|
+
{ role: "system", content: "Return only valid JSON. No prose." },
|
|
242
|
+
{ role: "user", content: prompt },
|
|
243
|
+
]);
|
|
244
|
+
}, null);
|
|
245
|
+
totalPairsChecked++;
|
|
246
|
+
result.pairsChecked++;
|
|
247
|
+
if (!judgeResult)
|
|
248
|
+
continue; // Feature gate disabled or LLM call failed.
|
|
249
|
+
let parsed = null;
|
|
250
|
+
try {
|
|
251
|
+
parsed = parseEmbeddedJsonResponse(judgeResult);
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
result.warnings.push(`Could not parse contradiction judge response for pair ${a.ref} / ${b.ref}`);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (!parsed?.contradicts)
|
|
258
|
+
continue;
|
|
259
|
+
// Write contradiction edges: both members get contradictedBy pointing to each other.
|
|
260
|
+
try {
|
|
261
|
+
const wroteA = writeContradictedByEdge(a.filePath, b.ref);
|
|
262
|
+
const wroteB = writeContradictedByEdge(b.filePath, a.ref);
|
|
263
|
+
result.edgesWritten += (wroteA ? 1 : 0) + (wroteB ? 1 : 0);
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
result.warnings.push(`Failed to write contradiction edge ${a.ref} <-> ${b.ref}: ${err instanceof Error ? err.message : String(err)}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (totalPairsChecked >= MAX_PAIRS_PER_RUN)
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
import fs from "node:fs";
|
|
2
5
|
import path from "node:path";
|
|
3
|
-
import { stringify as yamlStringify } from "yaml";
|
|
4
6
|
import { makeAssetRef, parseAssetRef } from "./asset-ref";
|
|
7
|
+
import { assembleAsset } from "./asset-serialize";
|
|
5
8
|
import { firstString, groupBy, stringArray } from "./common";
|
|
6
9
|
import { parseFrontmatter } from "./frontmatter";
|
|
7
10
|
const DERIVED_SUFFIX = ".derived";
|
|
@@ -172,6 +175,34 @@ export function applyMemoryCleanup(stashDir, plan) {
|
|
|
172
175
|
warnings.push(formatApplyWarning("archive", candidate.ref, error));
|
|
173
176
|
}
|
|
174
177
|
}
|
|
178
|
+
// M-5 / #396: Resolve relative dates for flagged candidates.
|
|
179
|
+
// Anchor: use the file's `createdAt` frontmatter field, or fall back to the
|
|
180
|
+
// file's mtime. Graphiti arXiv:2501.13956, HeidelTime (Strötgen & Gertz 2010).
|
|
181
|
+
let relativeDatesResolved = 0;
|
|
182
|
+
for (const candidate of plan.relativeDateCandidates) {
|
|
183
|
+
try {
|
|
184
|
+
const raw = fs.readFileSync(candidate.filePath, "utf8");
|
|
185
|
+
const fm = parseFrontmatter(raw);
|
|
186
|
+
const createdAtStr = fm.data.createdAt;
|
|
187
|
+
let referenceDate;
|
|
188
|
+
if (createdAtStr) {
|
|
189
|
+
const parsed = new Date(createdAtStr);
|
|
190
|
+
referenceDate = Number.isNaN(parsed.getTime()) ? new Date(fs.statSync(candidate.filePath).mtimeMs) : parsed;
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
referenceDate = new Date(fs.statSync(candidate.filePath).mtimeMs);
|
|
194
|
+
}
|
|
195
|
+
const resolvedBody = resolveRelativeDates(fm.content ?? "", referenceDate);
|
|
196
|
+
if (resolvedBody === (fm.content ?? ""))
|
|
197
|
+
continue; // no change
|
|
198
|
+
const newContent = assembleAsset(fm.data, resolvedBody);
|
|
199
|
+
fs.writeFileSync(candidate.filePath, newContent, "utf8");
|
|
200
|
+
relativeDatesResolved++;
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
warnings.push(formatApplyWarning("relative-date-resolve", candidate.ref, error));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
175
206
|
archived.sort((a, b) => a.ref.localeCompare(b.ref));
|
|
176
207
|
appliedBeliefTransitions.sort(compareBeliefTransitions);
|
|
177
208
|
return {
|
|
@@ -179,6 +210,7 @@ export function applyMemoryCleanup(stashDir, plan) {
|
|
|
179
210
|
beliefStateTransitions: appliedBeliefTransitions,
|
|
180
211
|
...(transitionLogPath ? { transitionLogPath: path.relative(stashDir, transitionLogPath).replace(/\\/g, "/") } : {}),
|
|
181
212
|
...(transitionLogPath ? { transitionLogEntries: appliedBeliefTransitions.length } : {}),
|
|
213
|
+
...(relativeDatesResolved > 0 ? { relativeDatesResolved } : {}),
|
|
182
214
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
183
215
|
};
|
|
184
216
|
}
|
|
@@ -186,6 +218,52 @@ function formatApplyWarning(stage, ref, error) {
|
|
|
186
218
|
const detail = error instanceof Error ? error.message : String(error);
|
|
187
219
|
return `${stage} failed for ${ref}: ${detail}`;
|
|
188
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* M-5 / #396: Resolve relative date expressions to absolute ISO dates.
|
|
223
|
+
*
|
|
224
|
+
* Uses `referenceDate` as the anchor point (Graphiti arXiv:2501.13956,
|
|
225
|
+
* HeidelTime Strötgen & Gertz 2010 — document creation time as reference).
|
|
226
|
+
* Replaces patterns like "yesterday", "3 days ago", "last week" with their
|
|
227
|
+
* ISO 8601 date string (YYYY-MM-DD).
|
|
228
|
+
*
|
|
229
|
+
* Returns the rewritten string; returns the original if no matches.
|
|
230
|
+
*/
|
|
231
|
+
function resolveRelativeDates(text, referenceDate) {
|
|
232
|
+
const RELATIVE_DATE_RE = /\b(yesterday|last week|last month|last year|\d+ days? ago|\d+ weeks? ago|\d+ months? ago)\b/gi;
|
|
233
|
+
return text.replace(RELATIVE_DATE_RE, (match) => {
|
|
234
|
+
const lower = match.toLowerCase().trim();
|
|
235
|
+
const d = new Date(referenceDate);
|
|
236
|
+
if (lower === "yesterday") {
|
|
237
|
+
d.setDate(d.getDate() - 1);
|
|
238
|
+
}
|
|
239
|
+
else if (lower === "last week") {
|
|
240
|
+
d.setDate(d.getDate() - 7);
|
|
241
|
+
}
|
|
242
|
+
else if (lower === "last month") {
|
|
243
|
+
d.setMonth(d.getMonth() - 1);
|
|
244
|
+
}
|
|
245
|
+
else if (lower === "last year") {
|
|
246
|
+
d.setFullYear(d.getFullYear() - 1);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
const numMatch = lower.match(/^(\d+)\s+(day|week|month)s?\s+ago$/);
|
|
250
|
+
if (numMatch) {
|
|
251
|
+
const n = Number.parseInt(numMatch[1] ?? "0", 10);
|
|
252
|
+
const unit = numMatch[2] ?? "day";
|
|
253
|
+
if (unit === "day")
|
|
254
|
+
d.setDate(d.getDate() - n);
|
|
255
|
+
else if (unit === "week")
|
|
256
|
+
d.setDate(d.getDate() - n * 7);
|
|
257
|
+
else if (unit === "month")
|
|
258
|
+
d.setMonth(d.getMonth() - n);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
return match; // unrecognized pattern — leave as-is
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return d.toISOString().slice(0, 10); // YYYY-MM-DD
|
|
265
|
+
});
|
|
266
|
+
}
|
|
189
267
|
function resolveFamilyContradictions(family) {
|
|
190
268
|
if (family.length === 0)
|
|
191
269
|
return { contradictionCandidates: [], transitions: [] };
|
|
@@ -203,12 +281,25 @@ function resolveFamilyContradictions(family) {
|
|
|
203
281
|
return {
|
|
204
282
|
contradictionCandidates: [],
|
|
205
283
|
transitions: family
|
|
206
|
-
.filter((record) =>
|
|
284
|
+
.filter((record) => {
|
|
285
|
+
// `deprecated` is a frozen historical state — never refresh it to active.
|
|
286
|
+
// (`superseded` is intentionally still refreshable to preserve pre-Phase-1A behavior.)
|
|
287
|
+
if (isFrozenHistoricalBeliefState(record.beliefState))
|
|
288
|
+
return false;
|
|
289
|
+
// `active` and `asserted` are both "current/believed" states. Only emit a
|
|
290
|
+
// refresh if there is something to clear (contradictions / currentBeliefRefs)
|
|
291
|
+
// or the state is something else entirely (e.g. lingering `contradicted`).
|
|
292
|
+
if (isActiveLikeBeliefState(record.beliefState)) {
|
|
293
|
+
return record.contradictedBy.length > 0 || record.currentBeliefRefs.length > 0;
|
|
294
|
+
}
|
|
295
|
+
return true;
|
|
296
|
+
})
|
|
207
297
|
.map((record) => ({
|
|
208
298
|
ref: record.ref,
|
|
209
299
|
parentRef: record.parentRef,
|
|
210
300
|
fromState: record.beliefState,
|
|
211
|
-
|
|
301
|
+
// Preserve `asserted` authority; otherwise refresh to plain `active`.
|
|
302
|
+
toState: record.beliefState === "asserted" ? "asserted" : "active",
|
|
212
303
|
reason: "belief-refresh",
|
|
213
304
|
})),
|
|
214
305
|
};
|
|
@@ -289,14 +380,22 @@ function resolveFamilyContradictions(family) {
|
|
|
289
380
|
}
|
|
290
381
|
const componentRefs = [...components[componentIndex]].sort();
|
|
291
382
|
const peerCurrentRefs = componentRefs.filter((ref) => ref !== record.ref);
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
383
|
+
// `deprecated` is a frozen historical state — never refresh to active.
|
|
384
|
+
// (`superseded` is intentionally still refreshable to preserve pre-Phase-1A behavior.)
|
|
385
|
+
if (isFrozenHistoricalBeliefState(record.beliefState)) {
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
// For `active` / `asserted` records, only refresh when something changes.
|
|
389
|
+
// For everything else (e.g. lingering `contradicted`) always refresh.
|
|
390
|
+
const isActiveLike = isActiveLikeBeliefState(record.beliefState);
|
|
391
|
+
const needsRefresh = !isActiveLike || record.contradictedBy.length > 0 || !sameStringArray(record.currentBeliefRefs, peerCurrentRefs);
|
|
392
|
+
if (needsRefresh) {
|
|
295
393
|
transitions.push({
|
|
296
394
|
ref: record.ref,
|
|
297
395
|
parentRef: record.parentRef,
|
|
298
396
|
fromState: record.beliefState,
|
|
299
|
-
|
|
397
|
+
// Preserve `asserted` authority; otherwise refresh to plain `active`.
|
|
398
|
+
toState: record.beliefState === "asserted" ? "asserted" : "active",
|
|
300
399
|
reason: "belief-refresh",
|
|
301
400
|
...(peerCurrentRefs[0] ? { relatedRef: peerCurrentRefs[0], relatedRefs: peerCurrentRefs } : {}),
|
|
302
401
|
...(peerCurrentRefs.length > 0 ? { currentBeliefRefs: peerCurrentRefs } : {}),
|
|
@@ -364,7 +463,7 @@ function archiveCleanupCandidate(stashDir, candidate, filePath) {
|
|
|
364
463
|
const archiveRef = path.relative(stashDir, archivedPath).replace(/\\/g, "/");
|
|
365
464
|
const auditPath = path.join(archiveDir, "cleanup.md");
|
|
366
465
|
const auditRef = path.relative(stashDir, auditPath).replace(/\\/g, "/");
|
|
367
|
-
const
|
|
466
|
+
const auditAsset = assembleAsset({
|
|
368
467
|
schemaVersion: 1,
|
|
369
468
|
kind: "memory-cleanup-archive",
|
|
370
469
|
archivedAt,
|
|
@@ -376,8 +475,8 @@ function archiveCleanupCandidate(stashDir, candidate, filePath) {
|
|
|
376
475
|
...(candidate.survivorRef ? { survivorRef: candidate.survivorRef } : {}),
|
|
377
476
|
originalPath,
|
|
378
477
|
archivedPath: archiveRef,
|
|
379
|
-
})
|
|
380
|
-
fs.writeFileSync(auditPath,
|
|
478
|
+
}, "Archived derived memory for recoverable cleanup.\n");
|
|
479
|
+
fs.writeFileSync(auditPath, auditAsset, "utf8");
|
|
381
480
|
return {
|
|
382
481
|
ref: candidate.ref,
|
|
383
482
|
parentRef: candidate.parentRef,
|
|
@@ -412,9 +511,7 @@ function persistBeliefStateTransition(filePath, transition) {
|
|
|
412
511
|
nextFrontmatter.currentBeliefRefs = [...currentBeliefRefs];
|
|
413
512
|
else
|
|
414
513
|
delete nextFrontmatter.currentBeliefRefs;
|
|
415
|
-
|
|
416
|
-
const body = parsed.content.replace(/^\n+/, "");
|
|
417
|
-
fs.writeFileSync(filePath, `---\n${frontmatter}\n---\n\n${body}`, "utf8");
|
|
514
|
+
fs.writeFileSync(filePath, assembleAsset(nextFrontmatter, parsed.content), "utf8");
|
|
418
515
|
}
|
|
419
516
|
function appendBeliefStateTransitionLog(stashDir, transitions) {
|
|
420
517
|
const logDir = path.join(stashDir, ".akm", "memory-cleanup");
|
|
@@ -512,9 +609,40 @@ function collectDerivedMemories(stashDir, parentRefFilter) {
|
|
|
512
609
|
}
|
|
513
610
|
return records.sort(compareRecords);
|
|
514
611
|
}
|
|
612
|
+
/**
|
|
613
|
+
* `active` and `asserted` are both "currently believed" states. `asserted` carries
|
|
614
|
+
* stronger user-explicit authority (set by the hot-path `akm remember`) but for
|
|
615
|
+
* state-machine purposes (contradiction resolution, refresh logic) they are
|
|
616
|
+
* equivalent.
|
|
617
|
+
*/
|
|
618
|
+
function isActiveLikeBeliefState(state) {
|
|
619
|
+
return state === "active" || state === "asserted";
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* `deprecated` is a frozen historical state introduced in Phase 1A. Once
|
|
623
|
+
* recorded, the contradiction-resolution pass must not refresh it back to
|
|
624
|
+
* active. (`contradicted` is also historical but it *is* updated by the
|
|
625
|
+
* contradiction resolver, so it is treated separately.)
|
|
626
|
+
*
|
|
627
|
+
* Note: `superseded` is deliberately NOT included here. Pre-Phase-1A,
|
|
628
|
+
* `superseded` records were refreshed to `active` by the belief-refresh
|
|
629
|
+
* pass (the old guard was `record.beliefState !== "active"`). In practice
|
|
630
|
+
* most `superseded` records are pruned earlier via `supersededBy` metadata
|
|
631
|
+
* in `analyzeMemoryCleanup`, so they never reach belief refresh — but a
|
|
632
|
+
* record marked `beliefState: superseded` without `supersededBy` metadata
|
|
633
|
+
* was previously refreshable. Preserving that behavior here avoids a
|
|
634
|
+
* surprise regression; only `deprecated` is the new frozen state.
|
|
635
|
+
*/
|
|
636
|
+
function isFrozenHistoricalBeliefState(state) {
|
|
637
|
+
return state === "deprecated";
|
|
638
|
+
}
|
|
515
639
|
function resolveBeliefState(frontmatter) {
|
|
516
640
|
const explicit = firstString(frontmatter.beliefState);
|
|
517
|
-
if (explicit === "active" ||
|
|
641
|
+
if (explicit === "active" ||
|
|
642
|
+
explicit === "asserted" ||
|
|
643
|
+
explicit === "deprecated" ||
|
|
644
|
+
explicit === "superseded" ||
|
|
645
|
+
explicit === "contradicted") {
|
|
518
646
|
return explicit;
|
|
519
647
|
}
|
|
520
648
|
return "active";
|
package/dist/core/parse.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
/**
|
|
2
5
|
* Shared JSON parsing utilities for LLM and agent output.
|
|
3
6
|
*
|