akm-cli 0.10.261010-beta → 0.10.261012-beta
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/CHANGELOG.md +101 -0
- package/dist/assets/prompts/distill-lesson-system.md +18 -5
- package/dist/assets/prompts/memory-infer-user.md +3 -1
- package/dist/commands/health/checks.js +10 -6
- package/dist/commands/improve/distill-promotion-policy.js +5 -4
- package/dist/commands/improve/extract.js +92 -21
- package/dist/commands/improve/improve.js +16 -2
- package/dist/commands/improve/loop-stages.js +2 -2
- package/dist/commands/improve/session-asset.js +2 -2
- package/dist/commands/improve/stage.js +3 -1
- package/dist/commands/read/remember-cli.js +1 -1
- package/dist/commands/read/show.js +4 -1
- package/dist/commands/remember.js +12 -7
- package/dist/core/adapter/adapters/akm-metadata.js +5 -14
- package/dist/core/asset/frontmatter.js +10 -1
- package/dist/core/subprocess.js +19 -7
- package/dist/indexer/passes/memory-inference.js +194 -19
- package/dist/indexer/search/db-search.js +2 -0
- package/dist/indexer/search/search-attribution.js +8 -3
- package/dist/indexer/search/search-hit-enrichers.js +16 -14
- package/dist/llm/memory-infer.js +3 -1
- package/dist/output/renderers.js +4 -4
- package/dist/output/text/show-format.js +1 -1
- package/dist/scripts/akm-migrate-node.js +7 -10
- package/dist/scripts/akm-migrate.js +7 -10
- package/docs/architecture/0.10-process-rules.md +12 -8
- package/docs/architecture/improvement.md +23 -0
- package/docs/architecture/internals/storage-locations.md +5 -3
- package/docs/architecture/persisted-data-compat.md +1 -0
- package/docs/reference/cli.md +1 -1
- package/docs/reference/data-and-telemetry.md +2 -1
- package/docs/reference/memory.md +30 -5
- package/package.json +1 -1
|
@@ -30414,7 +30414,7 @@ function formatShowPlain(r, detail) {
|
|
|
30414
30414
|
lines.push(`evidence: ${String(r.evidence).replace(/\s+/g, " ")}`);
|
|
30415
30415
|
if (Array.isArray(r.evidenceSources))
|
|
30416
30416
|
lines.push(`evidenceSources: ${r.evidenceSources.join(", ")}`);
|
|
30417
|
-
if (r.
|
|
30417
|
+
if (r.provenance)
|
|
30418
30418
|
lines.push(`provenance: ${JSON.stringify(r.provenance)}`);
|
|
30419
30419
|
if (r.workflowTitle)
|
|
30420
30420
|
lines.push(`workflowTitle: ${String(r.workflowTitle)}`);
|
|
@@ -40136,6 +40136,9 @@ function parseFrontmatter(raw) {
|
|
|
40136
40136
|
bodyStartLine: parsedBlock.bodyStartLine
|
|
40137
40137
|
};
|
|
40138
40138
|
}
|
|
40139
|
+
function readFrontmatterDate(fm, key) {
|
|
40140
|
+
return asNonEmptyString(fm[key]) ?? asNonEmptyString(fm[`${key}_at`]);
|
|
40141
|
+
}
|
|
40139
40142
|
function normalizeYamlValues(value) {
|
|
40140
40143
|
if (value instanceof Date) {
|
|
40141
40144
|
const y = value.getUTCFullYear();
|
|
@@ -45358,15 +45361,9 @@ function foldRecognizedMetadata(rendererName, file) {
|
|
|
45358
45361
|
const source = nonEmptyString2(fm.source);
|
|
45359
45362
|
if (source)
|
|
45360
45363
|
hints.add(source);
|
|
45361
|
-
const
|
|
45362
|
-
if (
|
|
45363
|
-
hints.add(`
|
|
45364
|
-
} else {
|
|
45365
|
-
try {
|
|
45366
|
-
const isoDate = file.stat().mtime.toISOString().slice(0, 10);
|
|
45367
|
-
hints.add(`observed_at:${isoDate}`);
|
|
45368
|
-
} catch {}
|
|
45369
|
-
}
|
|
45364
|
+
const observed = readFrontmatterDate(fm, "observed");
|
|
45365
|
+
if (observed)
|
|
45366
|
+
hints.add(`observed:${observed}`);
|
|
45370
45367
|
const expires = nonEmptyString2(fm.expires);
|
|
45371
45368
|
if (expires)
|
|
45372
45369
|
hints.add(`expires:${expires}`);
|
|
@@ -446,7 +446,7 @@ All paths are relative to the `rules-010` worktree. Citation form is `path:line`
|
|
|
446
446
|
|
|
447
447
|
- **Lesson vs knowledge kind (`proposalKind`)** — The loop passes `"auto"` for memories. A reinforced memory can graduate to a knowledge proposal with no generation call (see the promotion rule below). Otherwise it falls through to the lesson path. A direct request for `knowledge` uses the knowledge system prompt and schema, but the promotion assessment runs first for it too, so a promotable memory takes the no-generation path. *Why:* not stated in code/docs. (`src/commands/improve/loop-stages.ts:338`; `src/commands/improve/distill.ts:99,467-469,527-536`)
|
|
448
448
|
|
|
449
|
-
- **Memory → knowledge promotion score** — Promotion needs a weighted score of at least 3.8. Per positive feedback event (max 4) +0.8; two or more positives +0.65; no positive −0.9; exactly one positive −0.7; each negative −2.0; `quality: curated` +0.55; `confidence` ≥0.95 / ≥0.85 / ≥0.75 scales a 0.7 weight by 1 / 0.7 / 0.35; `source` +0.4; `observed_at` +0.4; description +0.2; tags +0.15; body of 8 or more words +0.15; tentative words (maybe, might, tbd, todo, investigate…) −1.1. Hard blockers are `.derived`, an empty body, conflict/contradiction/superseded/obsolete/retracted markers, `quality: proposed`, `subjective: true`, an `expires` field, and conflict markers in feedback. Promotion counts positive events, bare or not, over all time (`distill.ts:1288-1297`), but the loop refuses to invoke distill when every in-window signal is a bare positive (`loop-stages.ts:323-326`), so a memory with four bare positives (score 3.85, over the 3.8 bar) never promotes through the loop; the guard's rationale ("the writer has nothing to distil") does not apply to promotion, which makes no writer call. *Why:* "weights were chosen by a grid search over a labelled corpus; they are a plain constant now". (`src/commands/improve/distill-promotion-policy.ts:125-155,226-300,308-324`)
|
|
449
|
+
- **Memory → knowledge promotion score** — Promotion needs a weighted score of at least 3.8. Per positive feedback event (max 4) +0.8; two or more positives +0.65; no positive −0.9; exactly one positive −0.7; each negative −2.0; `quality: curated` +0.55; `confidence` ≥0.95 / ≥0.85 / ≥0.75 scales a 0.7 weight by 1 / 0.7 / 0.35; `source` +0.4; `observed` (or the older `observed_at`) +0.4; description +0.2; tags +0.15; body of 8 or more words +0.15; tentative words (maybe, might, tbd, todo, investigate…) −1.1. Hard blockers are `.derived`, an empty body, conflict/contradiction/superseded/obsolete/retracted markers, `quality: proposed`, `subjective: true`, an `expires` field, and conflict markers in feedback. Promotion counts positive events, bare or not, over all time (`distill.ts:1288-1297`), but the loop refuses to invoke distill when every in-window signal is a bare positive (`loop-stages.ts:323-326`), so a memory with four bare positives (score 3.85, over the 3.8 bar) never promotes through the loop; the guard's rationale ("the writer has nothing to distil") does not apply to promotion, which makes no writer call. *Why:* "weights were chosen by a grid search over a labelled corpus; they are a plain constant now". (`src/commands/improve/distill-promotion-policy.ts:125-155,226-300,308-324`)
|
|
450
450
|
|
|
451
451
|
- **Promotion destination merge** — Promotion writes to `knowledge/<leaf>` (the memory's last name segment). Only a file inside the same stash counts as the existing destination (#1000). With a runner, the model picks `ADD`, `UPDATE` or `NOOP` over the existing text (3000 chars each); `NOOP` returns `skipped` with `conflict_noop`. With no runner, the existing text is appended for the reviewer. Promotions go through the same judge, with the plain lesson rubric and no `extending` text, whose GROUNDING requires every statement to be in the source memory or its feedback; the existing knowledge doc's lines are in neither, so an ADD/UPDATE merge is expected to score 2 or less and be `quality_rejected` for 30 days, and the merge path yields a proposal only with the gate off. *Why:* the promotion is filed in `run.stash`, so only that bundle's doc is its destination. (`src/commands/improve/distill.ts:731-737,1086-1185`; `stage.ts:382`; `src/commands/improve/distill-promotion-policy.ts:102-111`; #1000, D-1/#369)
|
|
452
452
|
|
|
@@ -497,7 +497,7 @@ All paths are relative to the `rules-010` worktree. Citation form is `path:line`
|
|
|
497
497
|
|
|
498
498
|
- **Dedupe: unchanged sessions are skipped with zero LLM calls** — A session whose raw event stream hash (role plus text, excluding titles, timestamps and inline refs) equals the stored `content_hash` is skipped as `already_extracted`. Only `--force` overrides it, even for `--session-id`. A session whose stored hash is null is retried. *Why:* "the skip authority for already extracted". (`src/commands/improve/extract.ts:363-370,434-444`; `src/storage/repositories/extract-sessions-repository.ts:210-217`)
|
|
499
499
|
|
|
500
|
-
- **Hash is not recorded when the failure is transient** — `llm_unavailable`, `triaged_out` and `malformed_model_output` store a null hash so the session is retried later. `already_extracted` and `locked_concurrent` are not rewritten. `read_failed` and `malformed_model_output` are recorded with outcome `failed` (`read_failed` also ends with a null hash); an `exception` outcome is accounted but never persisted (`account`, not `accountAndRecord`), so `akm health` never sees those failures. *Why:* "one that failed for a transient reason keeps a null hash so it is retried". (`src/commands/improve/extract.ts:
|
|
500
|
+
- **Hash is not recorded when the failure is transient** — `llm_unavailable`, `triaged_out` and `malformed_model_output` store a null hash so the session is retried later. So does a session with a proposal write that failed for any reason other than the validator's refusal (state.db busy, I/O, a config problem; `proposal_write_failed`, or a mixed `candidates_queued` run with `failedCount > failedRejectedCount`): the next run re-extracts it, the candidates that persisted come back blocked by their `proposed` ledger window (7 days) or as accepted from this session (below), and only the failed ones are written again (#1212). A write the validator refused (`UsageError` code `INVALID_PROPOSAL`, counted in `failedRejectedCount`) fails the same way on the same content, so a session whose only failed writes are refusals records its hash and is extracted again only when its content changes; the outcome is still `proposal_write_failed` when nothing persisted, so `akm health` still warns on an all-failed window (#1224). `already_extracted` and `locked_concurrent` are not rewritten. `read_failed` and `malformed_model_output` are recorded with outcome `failed` (`read_failed` also ends with a null hash); an `exception` outcome is accounted but never persisted (`account`, not `accountAndRecord`), so `akm health` never sees those failures. *Why:* "one that failed for a transient reason, or had a proposal write fail for any reason but the validator's refusal, keeps a null hash so it is retried"; retrying a refusal cost a model call on every run with no chance of a different write. No extract write had been refused on the owner's machine when this was decided (zero `proposal_creation_rejected` events with `source: extract` since 2026-09-12), because the extract contract already enforces what the mint validators check for these types; the rule covers drift between the two, and adds no cap or counter. (`src/commands/improve/extract.ts:766-772,953-992`; `src/commands/proposal/repository.ts:364-372,475-512`)
|
|
501
501
|
|
|
502
502
|
- **`triaged_out` sessions are re-read every run** — A `triaged_out` outcome stores a null hash, so `shouldSkipAlreadyExtractedSession` returns false and `countNewExtractCandidates` counts the row as new on every run. With `default`'s `triage.enabled: true`, triaged-out sessions perpetually satisfy any `minNewSessions` value once set. *Why:* not stated. (`extract.ts:921,1297`, `extract-sessions-repository.ts:215`)
|
|
503
503
|
- **Pre-LLM gates: size floor and optional triage** — Sessions whose raw text is under `minContentChars` (default 10; 0 disables) are `too_short`. Optional heuristic triage (`processes.extract.triage`; off unless enabled, `minScore` default 2) adds up to 2 for learning-marker words, 1.5 for tool density, 1.5 for edits/commits, and 1 for the substantive share, and skips sessions scoring below the minimum (`triaged_out`). *Why:* "only truly empty sessions are safe to skip" at the size floor. (`src/commands/improve/extract.ts:83-87,372-395,451-459`)
|
|
@@ -512,9 +512,9 @@ All paths are relative to the `rules-010` worktree. Citation form is `path:line`
|
|
|
512
512
|
|
|
513
513
|
- **Where proposals go and their refs** — Each candidate is minted as a proposal with `source: "extract"` and no judge verdict, so it is not stamped (extract never writes assets itself). Refs: `memories/<project>/<slug>`, `lessons/<project>/<slug>` or `knowledge/<slug>`, where `<project>` is the last path segment of the session's project hint. Frontmatter carries `description`, `when_to_use` (lessons), `confidence`, `evidence`, and `xrefs` to the session asset when one was written. *Why:* the queue is the only way to a live asset. (`src/commands/improve/extract.ts:319-349,703-719`)
|
|
514
514
|
|
|
515
|
-
- **Candidate dedupe through the ledger** — A candidate whose ref has a live `extract`-source ledger window (pending for 7 days, or recently rejected for 7 days) is skipped with a warning and not queued again. `--dry-run`
|
|
515
|
+
- **Candidate dedupe through the ledger** — A candidate whose ref has a live `extract`-source ledger window (pending for 7 days, or recently rejected for 7 days) is skipped with a warning and not queued again. `--dry-run` lists the eligible refs in `sessions[].previewRefs` and queues nothing; `proposals` and `sessions[].proposalIds` hold only persisted proposals. *Why:* so the same insight is not queued repeatedly. (`src/commands/improve/extract.ts:696-716`; `src/storage/repositories/improve-ledger-repository.ts:78,82,142-163`; `src/commands/proposal/repository.ts:499-507`)
|
|
516
516
|
|
|
517
|
-
- **An accepted extract proposal leaves no ledger window** — Once accepted, an `extract` proposal's ledger row has no clock, so the same ref extracted from another session is minted again as an `update` proposal against the now-live asset, with no content-diff noise gate at mint. Dedupe is by the model-chosen ref, not by insight. *Why:* not
|
|
517
|
+
- **An accepted extract proposal leaves no ledger window, except for its own session** — Once accepted, an `extract` proposal's ledger row has no clock, so the same ref extracted from another session is minted again as an `update` proposal against the now-live asset, with no content-diff noise gate at mint. Dedupe is by the model-chosen ref, not by insight. The session that produced the accepted proposal does not propose that ref again: when a candidate's row is `accepted` and its proposal names this session (the session asset in `xrefs`/`evidenceSources`, or `<harness>:<sessionId>` in `provenance.sources`, as `buildCandidateProposal` writes them), the candidate is counted in `blockedCount` and skipped with the warning `skipped: accepted from this session as proposal <id>`. This holds for a retry after a failed write (#1212), a re-extraction after the session grew, and `--force`. *Why:* a re-extraction is a fresh model call, so its text is close to the accepted text but rarely byte-identical, and a content-equality check at mint would seldom fire; the session that already produced the accepted insight has nothing new to say about the same ref, while another session may (#1224). On the owner's machine, re-extracted sessions queued 1,123 extract proposals for a ref the same session had already had accepted: 1,114 before the content-hash skip (#602) landed on 2026-06-21, 9 through 2026-07-04, none since. The #1212 retry is a new way back to that duplicate. A proposal written before these marks existed (legacy `sources: session:…` frontmatter) is not recognized. (`src/commands/improve/extract.ts:701-748`; `src/storage/repositories/improve-ledger-repository.ts:159-161`; `src/commands/proposal/repository.ts:427,435`)
|
|
518
518
|
- **Session asset (#561)** — By default (`indexSessions` on), a session at or above `minSessionDuration` (default 5 min; 0 disables; a missing timestamp counts as long enough) is also written as `sessions/<harness>/<id>.md`: a 2–4 sentence summary plus key topics, with `log_path` and an `access` hint to read the raw log. It is fail-open: no summary means no asset. It costs a second LLM call per processed session. *Why:* make sessions searchable by recall; the write is additive and uses no queue. (`src/commands/improve/session-asset.ts:5-10,114-120,155-193,210-236`; `src/commands/improve/extract.ts:83-85,612-632`; `src/core/config/schema/improve-processes.ts:201-211`) The summary call is pre-filtered and fenced against injection (fixed in this PR, #12), but its output is still written with `fs.writeFileSync` straight into the stash (not through the proposal queue or `writeAssetToSource`) as the asset's description and body, then indexed, so a summary the model was talked into is not reviewed. The owner confirmed on 2026-10-11 that writing it straight to the stash is fine, so there is no change. (`session-asset.ts:75-97,226`, `extract.ts:679`)
|
|
519
519
|
|
|
520
520
|
- **Per-run limit** — `maxSessionsPerRun` (default 25; 0 disables the cap) caps NEW sessions sent to the model. The rest are deferred to a later run with a warning. A session whose lock is lost at execution, or that is skipped by the under-lock re-gate, refills its slot; a session skipped at planning does not. `--session-id`, `--force` and a `--since` run are uncapped. *Why:* "the rest wait for later runs". (`src/commands/improve/extract.ts:87-88,490-529,818-831,1184-1188`)
|
|
@@ -541,6 +541,8 @@ All paths are relative to the `rules-010` worktree. Citation form is `path:line`
|
|
|
541
541
|
|
|
542
542
|
- **What is written: one derived memory per parent** — The LLM compresses the memory body into one derived memory: `title`, a one-sentence `description`, `content` (2–3 sentences), `tags` (3–8 in the prompt; schema max 8) and `searchHints` (3–6; schema max 6; the parser requires at least one of each unless the reply is a no-change). It keeps names, versions, paths and config keys verbatim. The whole body is sent, because the 4000-char `MAX_BODY_CHARS` truncation was removed (otherwise the derived memory "looked complete" while the model saw a fraction). The temperature is the engine's or process's configured one, else 0.1. *Why:* compress a memory into a higher-signal memory for later retrieval. (`src/llm/memory-infer.ts:77-88,123-139`; `src/assets/prompts/memory-infer-user.md`; CHANGELOG 0.9.2x ("Memory inference uses the configured temperature") and "Content and memory bodies are no longer silently truncated")
|
|
543
543
|
|
|
544
|
+
- **Request input layout** — After the prompt's `Source input:` line, the request carries a `Source context:` line with the selected existing fields as pretty-printed JSON, a blank line, a `Body:` line and the raw trimmed body. The body is not JSON-escaped. The prompt says everything after the first line that is exactly `Body:` is the body, so a body that repeats either marker cannot pose as context; pretty-printed JSON never has a line that is exactly `Body:`. An empty or whitespace-only body makes the whole input `""`, and no model attempt follows. *Why:* escaped Markdown on one line is a needless readability tax for small local models (PR #1222 review). (`src/indexer/passes/memory-inference.ts:731-763`; `src/assets/prompts/memory-infer-user.md`)
|
|
545
|
+
|
|
544
546
|
- **Reply validation** — A reply with `noChange: true` is the no-change result (see "No change is a successful result"). Otherwise the reply is skipped (returns nothing and warns) if it is invalid JSON, if any of title, description, content, tags or searchHints is missing or empty, or if it is a prompt placeholder echoed back ("short title string", "tag1", etc.). Tags are trimmed and capped at 8, hints at 6. *Why:* "never return field descriptions or placeholder values". (`src/llm/memory-infer.ts:36-44,140-177`)
|
|
545
547
|
|
|
546
548
|
- **Derived file shape and naming** — The child is `memories/<parent>.derived.md` with frontmatter `inferred: true`, `captureMode: background`, `source: memories/<parent>`, `derivedFrom`, `description`, `tags`, `searchHints`, `title` and `updated`. The body is `# Title` plus the content. It is written through `writeAssetToSource`. The child carries no `createdAt`, so memory cleanup anchors relative dates on its file mtime. *Why:* `.derived` is a provenance marker on the same identity. (`src/indexer/passes/memory-inference.ts:659-717`; `.derived` inputs are skipped by `eligibility.ts:283-285`, `distill-promotion-policy.ts:129-131` and `memory/derived-ref.ts:46-47`; #882 in CHANGELOG)
|
|
@@ -551,17 +553,19 @@ All paths are relative to the `rules-010` worktree. Citation form is `path:line`
|
|
|
551
553
|
|
|
552
554
|
- **Child-exists pre-check (#588, #550)** — Before any LLM or cache call, if `<parent>.derived.md` already exists on disk, the parent is marked processed and no attempt is used (`skippedChildExists`). A child that appears mid-flight, or a write throw, also counts there. A genuine write failure leaves the parent unmarked for retry. *Why:* #588: about 55% of the inference budget was spent rediscovering children that already existed. #550: parents were re-queued every run (about 37 wasted LLM calls per run on one stash). (`src/indexer/passes/memory-inference.ts:204-210,265-285,343-358,663-700`; CHANGELOG #588/#550)
|
|
553
555
|
|
|
554
|
-
- **Caching** — Results are cached in the index DB keyed by the parent's file path,
|
|
556
|
+
- **Caching** — Results are cached in the index DB keyed by the parent's file path, the hash of its effective input ([selected source context](../reference/memory.md#derived-memories-as-retrieval-shortcuts) plus the raw body, in the request layout above), and variant `memory-inference-v5`. That one string is also what the request sends and what the `minPendingCount` gate looks up. Older variants are disposable misses. Unchanged effective input gets a validated cache hit with no LLM call (`cacheHits`); relevant metadata edits invalidate it, while unrelated metadata and normalized-equivalent formatting do not. The cached value is a derived memory or `{ "noChange": true }`; an unrecognised or corrupt value falls through to recompute. `reEnrich` bypasses it (the improve step passes `false`). The pending-count gate and pass use the same cache lookup. *Why:* avoid stale decisions and skip re-processing unchanged input; cache hits are tracked separately so the yield rate does not drift as the cache warms. (`src/indexer/passes/memory-inference.ts`; `src/indexer/db/llm-cache.ts`)
|
|
555
557
|
|
|
556
558
|
- **Retries** — The LLM client retries once on a retryable transient failure, after a jittered backoff of 200–800 ms. The retry is skipped if 90% of the timeout was already used. The retry counts as `retryAttempts`, not a failure. *Why:* "a single bounded retry for transient failures". (`src/llm/client.ts:144-182,300-330`; `src/indexer/passes/memory-inference.ts:289-291`)
|
|
557
559
|
|
|
558
|
-
- **No change is a successful result (#1208)** — The model may reply `noChange: true` (the other fields empty) when a derived memory would add nothing: a tool-count or checkpoint note, or a note already short and specific. Only a literal boolean `true` counts. The pass counts it in `noChange` (fresh or cached, also in `cacheHits` when cached), writes no child, and leaves the parent untouched
|
|
560
|
+
- **No change is a successful result (#1208)** — The model may reply `noChange: true` (the other fields empty) when a derived memory would add nothing: a tool-count or checkpoint note, or a note already short and specific. Only a literal boolean `true` counts. The pass counts it in `noChange` (fresh or cached, also in `cacheHits` when cached), writes no child, and leaves the parent untouched. It is not stamped `inferenceProcessed`, so a body or relevant source-context edit can reopen it under the caching rules above. `noChange` never feeds `skippedNoFacts`, `htmlErrorCount` or `unaccounted`. Invalid or incomplete JSON, HTML, timeout, abort and transport errors stay `undefined`, are never cached, and are retried; a disabled pass makes no decision. Without a database the decision is reported but not remembered. The count is `improve` result `memoryInference.noChange` and health `memoryInference.noChange`; both are per-run, include cached decisions, and are not cumulative. The `minPendingCount` gate excludes parents with a cached no-change decision for their current effective input; a pass made only of cached decisions is not admitted. Health `yieldRate` is unchanged (`written / freshAttempts`) and measures child yield, which restraint lowers by design. *Why:* the prompt demanded a child for every note, so an unusable reply was a failure that cost a model call on every run. (`src/llm/memory-infer.ts`; `src/indexer/passes/memory-inference.ts`)
|
|
559
561
|
|
|
560
562
|
- **Skips leave the parent pending** — If the LLM returns nothing usable (timeout, invalid, incomplete or placeholder reply), `skippedNoFacts` counts it and the parent is NOT marked processed, so it is retried on the next run. An HTML reply is counted in `htmlErrorCount`. The per-call failure never throws. An abort signal skips unstarted parents (`skippedAborted`). Credentials are checked up front only if at least one parent will dispatch to the model, so cache-only batches work offline. *Why:* "a transient LLM failure should be retried on the next index run". (`src/indexer/passes/memory-inference.ts:341,516-519,464-465`; `src/llm/memory-infer.ts:178-188`)
|
|
561
563
|
|
|
562
564
|
- **Concurrency and enablement layers** — The pass runs with the engine connection's `concurrency` or 1. The live gates are autonomy, then `processes.memoryInference.enabled`, then `minPendingCount`, then a resolvable runner; any off makes it a no-op. `index.memory.enabled` has no effect in 0.10: the only caller passes a runner, which forces it on, so there is no standalone `akm index` path. Existing derived children are never deleted. *Why:* "A pass runs iff both layers allow it." (`src/indexer/passes/memory-inference.ts:22-30,425-435,472-492`; `src/llm/feature-gate.ts:54`)
|
|
563
565
|
|
|
564
|
-
- **
|
|
566
|
+
- **Scope sync for existing children (#1209)** — Each run, before any model work, the pass compares each existing child (`<parent>.derived.md` with `inferred: true` and `derivedFrom` naming that parent) with its parent's normalized scope (`scopeFromFrontmatter`, legacy nested `scope:` included). If they differ, it rewrites only the child's scope frontmatter. It sets the flat `scope_*` keys, removes stale ones and any nested `scope:`, and keeps every other byte, body included. It makes no model call and reuses the pass's single walk of `memories/`. Each child is re-read and re-checked before the write, which is atomic and journaled like the parent stamp. A matching child gets no write. A file without `inferred: true` or a matching `derivedFrom`, a child with no parent file, and frontmatter that cannot be edited in place (a warning) are left alone. A default bundle with `writable: false` is skipped. The count is `scopeSynced` on the pass result (the improve `memoryInference` action), not in health. Sync runs only when the pass runs, so the gates in "Concurrency and enablement layers" and `minPendingCount` also defer it. *Why:* a parent is stamped `inferenceProcessed` once its child exists, so it is never revisited. Children written before scope was copied stayed unscoped, and later parent scope edits never reached the child. Under a scope filter the child was filtered out and the parent lost `expandTo`. #1209 rules out regenerating or deleting children, but allows a metadata-only rewrite. (`src/indexer/passes/memory-inference.ts:515-522,653-729,925-965`)
|
|
567
|
+
|
|
568
|
+
- **Post-pass indexing** — After inference, the improve run indexes exactly the files it wrote (`writtenPaths`, children plus stamped parents plus scope-synced children) with `indexWrittenAssets` instead of a full reindex. An index failure is only a warning. *Why:* writtenPaths comes from the run-scoped write-provenance journal, so it cannot drift from what hit disk. (`src/commands/improve/loop-stages.ts:538-554`; `src/indexer/passes/memory-inference.ts:134-142`)
|
|
565
569
|
|
|
566
570
|
## Memory cleanup
|
|
567
571
|
|
|
@@ -689,7 +693,7 @@ Paths are relative to the rules-010 checkout. `repository.ts` is `src/commands/p
|
|
|
689
693
|
- **Remember: hot capture** — `akm remember` always writes a memory with `captureMode: hot` and `beliefState: asserted`. If you pass no description, it synthesizes one deterministically from the body: whole sentences up to the description cap, with a leading heading skipped and no LLM call. *Why:* #834 — hot-path memories had no description or tags and the indexer covers only frontmatter and headings, so they were retrievable only by their filename. The ranking rationale ("user-supplied memories outrank otherwise-equal background-derived ones") is stale: no ranking code reads `captureMode`, and since decision 5 (changed in this PR) neither does consolidate. (`src/commands/read/remember-cli.ts:210-246,310-326`; `src/commands/remember.ts:184-214`)
|
|
690
694
|
- **Remember: validation and tags** — `--name` must be flat (use `--path` for a subdirectory). `--tag`, `--expires`, `--source` or `--description` require at least one tag, so `--description` alone refuses the write while the zero-flag path writes with no tags and a synthesized description; the suggested remedies are unreliable (`--auto` derives only `code` from a fenced block, `--enrich` is fail-soft to no tags), which works against #834's goal of findable hot memories. `--show-similar` runs a search that logs events under the ambient event source (`remember-cli.ts:36-50`). `--auto` or `--enrich` alone, scope alone and `--xref` alone do not require tags. `--expires` is a shorthand duration resolved to an ISO date (`30d`, `12h`, `5m` minutes, `3M` months). Each `--xref` must resolve in the write target or a configured source, and an unresolvable one aborts with nothing written. (`remember-cli.ts:36-50,163-208,261-308`; `src/commands/remember.ts:75-87`)
|
|
691
695
|
- **Remember: `--supersedes`** — The ref must resolve; an unresolvable or self-referencing one aborts with no partial write. The new memory gets an xref to the old asset and the old asset is demoted (`beliefState: superseded` plus `supersededBy`, a metadata-only edit), so ranking prefers the correction. A ref outside the write target still writes the correction but skips the demotion. `--force` is needed to overwrite an existing name. (`remember-cli.ts:82-86,108-112,175-189`)
|
|
692
|
-
- **Remember: `--enrich`** — The LLM call has a 10 s timeout and is fail-soft: a provider, timeout or parse failure warns and writes the memory without enrichment. An invalid config is a hard pre-write error. Heuristic `--auto` derives `code`, `subjective`, source and `
|
|
696
|
+
- **Remember: `--enrich`** — The LLM call has a 10 s timeout and is fail-soft: a provider, timeout or parse failure warns and writes the memory without enrichment. An invalid config is a hard pre-write error. Heuristic `--auto` derives `code`, `subjective`, source and `observed` from the body with no network. The result gets no protection from consolidation: a `captureMode: hot` memory is retired, promoted and archived like any other (changed in this PR, decision 5). *Why:* the write path must not block on a misbehaving endpoint. (`src/commands/remember.ts:230-253,297-306,381-385`)
|
|
693
697
|
|
|
694
698
|
### Code and doc mismatches
|
|
695
699
|
- The task mentions "Merge/delete operations" and a "confidence cutoff for staging". Neither exists in this build. The promote pass is promote-only (see the first rule). Staging has no confidence threshold (see the gate-staging rule). A leftover health-check message mentions a "confidence threshold" (`src/commands/health/checks.ts:1533`), which is legacy wording.
|
|
@@ -469,6 +469,29 @@ by that toggle. Session indexing writes are additive
|
|
|
469
469
|
(`sessions/**`), which is why they are one of the writes left deliberately
|
|
470
470
|
ungated by the autonomy gate above.
|
|
471
471
|
|
|
472
|
+
`--dry-run` keeps eligible candidate refs in `sessions[].previewRefs`. It leaves
|
|
473
|
+
proposal rows, seen-session tracking, the improve ledger and assets unchanged;
|
|
474
|
+
an explicitly labeled preview event may still be recorded. `proposals`,
|
|
475
|
+
`sessions[].proposalIds`, `candidatesCreated` and the `--auto` result's
|
|
476
|
+
`totalProposals` count only successfully persisted proposals, so they are empty
|
|
477
|
+
or zero in a preview. `sessions[].candidateCount` counts parsed model candidates;
|
|
478
|
+
`blockedCount` and `failedCount` distinguish ledger holds from failed writes.
|
|
479
|
+
|
|
480
|
+
The existing `extract_invoked` event records `dryRun`, `candidateCount`,
|
|
481
|
+
`previewCount`, `proposalCount`, `blockedCount` and `failedCount`.
|
|
482
|
+
`candidates_queued` requires at least one successful write; zero-write runs use
|
|
483
|
+
`candidates_blocked` or `proposal_write_failed`. A dry run with eligible refs
|
|
484
|
+
uses `candidates_previewed`; an empty model result remains `no_candidates`.
|
|
485
|
+
A session with a failed write other than a validator refusal keeps a null
|
|
486
|
+
content hash in its seen-session row, so the next run re-extracts it:
|
|
487
|
+
candidates that persisted are held by their `proposed` ledger window, or as
|
|
488
|
+
accepted from this session, and only the failed ones are written again. A
|
|
489
|
+
refusal recurs on the same content, so it alone records the hash (#1224).
|
|
490
|
+
Unavailable or malformed model output retains its distinct skip/failure result.
|
|
491
|
+
Older events without `dryRun` have unknown preview status; their historical
|
|
492
|
+
`candidates_queued` label cannot establish persistence. Proposal metrics use
|
|
493
|
+
actual proposal rows, and health uses live seen-session outcomes.
|
|
494
|
+
|
|
472
495
|
## See also
|
|
473
496
|
|
|
474
497
|
- [Improve the Library](../guides/improve-the-library.md) — the user-facing
|
|
@@ -275,14 +275,16 @@ Incremental indexing cache. Directory skipped if hash + mtime unchanged.
|
|
|
275
275
|
| Column | Type | Notes |
|
|
276
276
|
|---|---|---|
|
|
277
277
|
| `asset_ref` | TEXT NOT NULL | Absolute file path |
|
|
278
|
-
| `cache_variant` | TEXT NOT NULL | Extractor/cache fingerprint. Memory inference uses a version-tagged variant (`memory-inference-
|
|
279
|
-
| `body_hash` | TEXT NOT NULL | SHA-256 hex digest of
|
|
278
|
+
| `cache_variant` | TEXT NOT NULL | Extractor/cache fingerprint. Memory inference uses a version-tagged variant (`memory-inference-v5`). |
|
|
279
|
+
| `body_hash` | TEXT NOT NULL | SHA-256 hex digest of the cache input; memory inference hashes body plus selected source context. |
|
|
280
280
|
| `result_json` | TEXT NOT NULL | Serialized LLM enrichment result |
|
|
281
281
|
| `updated_at` | INTEGER NOT NULL | Unix ms timestamp |
|
|
282
282
|
|
|
283
283
|
Primary key: `(asset_ref, cache_variant)`.
|
|
284
284
|
|
|
285
|
-
Cache miss on
|
|
285
|
+
Cache miss on effective-input change or cache-variant change. See the
|
|
286
|
+
[memory-inference caching rules](../0.10-process-rules.md#memory-inference).
|
|
287
|
+
Stale rows removed by
|
|
286
288
|
`clearStaleCacheEntries()`. The cache can also be bypassed by internal forced
|
|
287
289
|
re-enrichment callers.
|
|
288
290
|
|
|
@@ -44,6 +44,7 @@ and named once, round-trips through ordinary writes, and is dropped by
|
|
|
44
44
|
| Native scheduler rows (crontab, launchd plists, Task Scheduler) | `src/tasks/backends/cron.ts`, `launchd.ts`, `schtasks.ts`; the invocation grammar in `src/tasks/scheduler-invocation.ts` | The row's own invocation shape: current rows set their environment inline (a cron `VAR=value` prefix, a plist `EnvironmentVariables` entry, a PowerShell `$env:` assignment) — always `AKM_BUNDLE_DIR`, the syncing shell's working stash — and carry no `--scheduler-context`; 0.9.0 – 0.9.17-alpha.6 rows carry `--scheduler-context <descriptor>` holding the same values; older rows carry neither. A cron command over 1,000 bytes runs an akm wrapper script (`sh <log dir>/.akm-cron-wrapper-…sh`) that holds the same line | Every shape is listed and parsed (`parseScheduledInvocationArgv`; a spilled cron row from its wrapper script): a row inside akm's own `# akm:task` markers from before `--scheduler-context` (#881), and a `--scheduler-context` row, which keeps firing (the CLI applies its descriptor, below) until the next plain `akm task sync` rewrites it in place — an update that keeps the row's launcher and schedule, never an add or remove. The primary bundle proves a row by the `AKM_BUNDLE_DIR` it carries, inline or in its descriptor (#846); any other bundle owns rows by name. Rows that still carry the pre-0.9.17 workflow execution-evidence marker (`# akm:workflow-evidence` in the crontab, an `akm-workflow-evidence` plist comment or PowerShell literal) are listed like any other and rewritten once without it. Plain `akm task sync` keeps an installed row's own launcher (`installOptionsFor`, `src/tasks/scheduler-sync.ts`) and recomputes its environment; `akm task sync --rebind` repoints the launcher | A row inside akm's markers, `com.akm.task.` label namespace, or `\akm\` task folder whose argv this binary cannot parse (for example one a newer akm wrote) is not listed: sync leaves it alone unless an enabled source renders a row with the same native id, which then replaces it — the row is derived state and its source is the record. A crontab whose akm markers are malformed is refused unmodified (`parseBlocks`, `cron.ts`) | Upgrade rehearsal gate (`tests/integration/upgrade-rehearsal/`: the previous release's `--scheduler-context` rows fire under the candidate, then `task sync --dry-run` shows them only as updates); `tests/tasks-sync.test.ts` (`--scheduler-context` rows and spilled rows are updates, never adds) |
|
|
45
45
|
| Scheduler-context descriptor (`$DATA/tasks/context/<sha256>.json`, written by 0.9.0 – 0.9.17-alpha.6) | No longer written: a row carries its own environment (#987). Read by `src/tasks/scheduler-invocation.ts` (`readLegacySchedulerContext`) | `version: 1`; the environment key set | When a row that names one fires, the CLI reads the file as plain JSON and applies its `AKM_*_DIR` keys and, in one written before 0.9.17, `PATH`; nothing else about the file (name, mode, owner, link) is checked. Sync reads its `AKM_BUNDLE_DIR` to attribute the row. A plain `akm task sync` that reconciles the row rewrites it without the argument; a row a sync leaves as it is (its source failed, a `--bundle` sync did not cover it) keeps naming the file | A key this binary does not know is ignored. A file that is gone or is not JSON fails the run with `INVALID_CONFIG_FILE` naming `akm task sync`; sync cannot attribute such a row to the primary bundle and leaves it in place, and `akm task prune` reports it as `invalid-context` | `tests/tasks-scheduler-invocation.test.ts`; `tests/integration/scheduler-context-launcher.test.ts`; upgrade rehearsal gate (`tests/integration/upgrade-rehearsal/`) fires the previous release's rows under the candidate before and after its plain `task sync` |
|
|
46
46
|
| Proposal `metadata_json` | `src/storage/repositories/proposals-repository.ts` | No explicit version field; presence/absence of the `changes` key | Rows from before the `changes` key existed (~89% of archived rows on real installs) are treated as a known legacy gap — decoded with an empty change list rather than thrown (`proposals-repository.ts:47-56`). 0.9.17-alpha.9 adds a consolidate pair-pass `retire` proposal's `changes[0].op: "delete"` (no `after`, already a valid `FileChangeOp`) and its `retirement` / `retiredArchive` / `promotionSource` / `promotionSourceHash` metadata keys, plus the retirement continuity check's `retirement.continuityRisk` (rule R3, item 1) — a person can accept a proposal carrying it by id even though it is excluded from bulk accept; an older reader's `validatePresentMetadata` does not enumerate these keys, so it neither inspects nor rejects them — the row decodes with them silently absent, same as any other field an older release predates. 0.9.19 adds `reviewHistory` (the rejections `akm proposal reopen` undid, oldest first, each with its gate verdict) the same way: a proposal reopened by 0.9.19 and read by an older release is an ordinary pending proposal with its history absent, and that release drops the key if it rewrites the row. `retirement.judgedScope` (Unreleased, #1197) optionally records the shared normalized memory scope tuple; `{}` means known unscoped and absence means unknown. Older rows still decode unchanged; acceptance refuses an unknown tuple and asks for rejection/reassessment, rather than inferring judgment from live files. Staged stale decisions use the existing stale-target rejection/ledger retry path. Older readers carry this nested object through but do not enforce it, so do not downgrade before reviewing pending retirements. No format-version bump or database migration is needed for this additive metadata. The quality judge's evidence, `scores` and `judgeReason`, is added inside the existing `gateDecision` object (Unreleased): an older reader's `validatePresentMetadata` checks only the gate fields it knows and carries the object through whole, so the evidence survives a rewrite by an older release unread. An older release also counts a pending proposal's age (retention expiry, `--older-than`) from `createdAt`, so after a downgrade a reopened proposal can expire at the next `akm improve` run (a retire proposal never expires), or be swept by a scheduled `--older-than` bulk action or drain | 0.9.17-alpha.8 and earlier predate the retire proposal shape entirely — they were never taught to read a delete-primary change with no `after`. `akm proposal show` / `diff` / `drain` exit 70 on one, and drain's own nightly pre-pass failing on the first pending retire proposal it meets stops that run's auto-promotion for the whole stash, not just that one proposal. **Downgrade gate:** every pending retire proposal must be accepted or rejected (`akm proposal accept` / `reject`) before downgrading to 0.9.17-alpha.8 or earlier — once decided it is archived, out of the pending set `show`/`diff`/`drain` iterate, and no longer trips this. **Gap** otherwise (no code-level guard; this is operator guidance only). Downgrading also revives the starvation the second review round's B1 fixed forward-only: 0.9.17-alpha.8's `retrieval-scope.ts` counts a `consolidate-pair` ledger row as processed, so the pair pass's own nightly attempts crowd new material back out of every other improve lane's scope, the same way they did before B1 — measured on the real bundle, two alpha.9 nights left only 341 of 2,183 new-only assets still in scope once read under alpha.8. **Gap** here too (the ledger `source` string carries no version of its own; operator guidance only — do not downgrade across a pair-pass run without expecting this). | `tests/proposal-repository-pure.test.ts` — unit only; the rehearsal builds no proposal |
|
|
47
|
+
| Extraction events and `extract_sessions_seen` outcomes | `src/commands/improve/extract.ts`; `src/storage/repositories/extract-sessions-repository.ts` | Existing event envelope / state.db layout; no format bump | #1212 adds `dryRun`, `previewCount`, `blockedCount`, and `failedCount` to event metadata, and `blockedCount` and `failedCount` to seen-session metadata. #1224 adds `failedRejectedCount` to seen-session metadata only: the writes among `failedCount` that the proposal validator refused (`INVALID_PROPOSAL`). A seen row with a failed write other than a refusal stores a null `content_hash`, so the session is retried; a row whose failed writes were all refusals records its hash. An absent `failedRejectedCount` reads as 0. `blockedCount` also counts candidates skipped because this session's proposal for the ref was already accepted. `candidateCount` counts parsed model candidates; `proposalCount` counts successful proposal creation. Preview events use `candidates_previewed`; zero-write runs use `candidates_blocked` or `proposal_write_failed`. Preview runs never write seen-session rows. Legacy metadata is read unchanged: missing `dryRun` means unknown preview status, and historical `candidates_queued` is not proof of persistence. No historical rewrite is performed | Older readers pass through metadata and outcome text. Older health checks may report an all-write-failed run as active instead of warning. Proposal totals still come from proposal rows. No database migration or queue/content rewrite is needed | `tests/integration/extract-command.test.ts` (accounting replay, legacy records); `tests/integration/health/health-session-extraction-ledger.test.ts` |
|
|
47
48
|
| `improve_ledger` rows | `src/storage/repositories/improve-ledger-repository.ts` | No version field of its own; the `content_hash` column (migration `029-improve-ledger-content-hash`) | A consolidate promotion row with no `content_hash` is held by its clock alone (`next_eligible_at`). 0.9.19 records `content_hash`, and no `next_eligible_at`, on a consolidate promotion once it is accepted or rejected, and holds the memory until its body hash differs (`isContentDrivenRow`); a promotion decided by an older release has no hash and keeps its old window (an accepted one none, a rejected one 7 days) | An older release reads a row by `next_eligible_at` alone and ignores the hash, so after a downgrade a memory whose promotion 0.9.19 decided is offered to the promote pass again at once, a rejected one sooner than the 7 days the older release would have held it; the hash stays on the row until the older release records its next attempt on that memory, which clears it. **Gap** (no code-level guard; operator guidance only) | `tests/integration/storage/improve-ledger-repository.test.ts` |
|
|
48
49
|
| `task_history` metadata | `src/storage/repositories/task-history-repository.ts` | `metadataVersion` (currently `2`) | An absent `metadataVersion` is treated as a legacy row (58% of rows written by prior releases) and decoded best-effort; unknown fields are dropped harmlessly rather than round-tripped (`task-history-repository.ts:61-69`) | A `metadataVersion` newer than this binary's `2` is decoded best-effort as version 2 with a warning, rather than rejected (`task-history-repository.ts:85-89`) | Upgrade rehearsal gate (`tests/integration/upgrade-rehearsal/`) |
|
|
49
50
|
| Lock payloads | `src/core/file-lock.ts` | No version field; payload is a bare numeric pid string, or a JSON object carrying `pid` and optionally `launcherPid` (`extractLockIdentity`, `file-lock.ts`) | Any payload shape parses permissively: extra fields are ignored, a missing or unparseable `launcherPid` is simply absent, and a payload with no parseable `pid` probes as `stale` (`invalid_pid`, `probeLock`) and is reclaimed rather than rejected | Same permissive parse; a payload from a newer akm that adds fields is read the same way | `tests/integration/file-lock.test.ts` (describe "probeLock: launcherPid (#956)") — not the rehearsal, which does not contend locks |
|
package/docs/reference/cli.md
CHANGED
|
@@ -1766,7 +1766,7 @@ akm remember "VPN required for staging deploys" \
|
|
|
1766
1766
|
--expires 90d \
|
|
1767
1767
|
--source "skills/deploy"
|
|
1768
1768
|
|
|
1769
|
-
# Opt-in heuristic tagging — derives `code`, `source`, `
|
|
1769
|
+
# Opt-in heuristic tagging — derives `code`, `source`, `observed`, `subjective`:
|
|
1770
1770
|
akm remember "Found this snippet: \`curl -fsSL ... | bash\`" --tag ops --auto
|
|
1771
1771
|
|
|
1772
1772
|
# Opt-in LLM enrichment (requires configured LLM endpoint; fails soft):
|
|
@@ -281,7 +281,8 @@ version marker are historical/unattributed. Attributed rows use
|
|
|
281
281
|
- `memoryInference`: `direct` when the emitted ref is an inferred child, or
|
|
282
282
|
`surface` when derived description/tags were actually present in the emitted
|
|
283
283
|
search or selected curate output. Brief output and internally replaced
|
|
284
|
-
descriptions are controls
|
|
284
|
+
descriptions are controls. Tag-only fallback is attributed in full search;
|
|
285
|
+
normal/agent search and curate omit tags and remain controls in that case.
|
|
285
286
|
- `graphExtraction`: written only by releases that boosted search with the
|
|
286
287
|
graph — the graph contribution applied to the hit, plus `bodyHash` and
|
|
287
288
|
`extractionRunId` when available. Current releases do not rank by the graph
|
package/docs/reference/memory.md
CHANGED
|
@@ -27,7 +27,7 @@ that the indexer records:
|
|
|
27
27
|
| --- | --- |
|
|
28
28
|
| `tags` | Free-form categorization |
|
|
29
29
|
| `source` | Where the memory came from (also used as the derived-memory parent backref — see below) |
|
|
30
|
-
| `
|
|
30
|
+
| `observed` | When the observation happened. Older memories may carry `observed_at`; it is still read, and `observed` wins when both are present. `akm` writes only `observed`. |
|
|
31
31
|
| `expires` | When the memory should stop being considered current |
|
|
32
32
|
| `subjective` | Marks the memory as opinion/preference rather than fact |
|
|
33
33
|
| `description` | Short human-readable summary |
|
|
@@ -72,10 +72,35 @@ parent/child link in the `derived_from` column. This provenance backref is a
|
|
|
72
72
|
sanctioned internal channel that carries a bare parent name, not a public
|
|
73
73
|
[ref](refs.md) — agents never construct it.
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
description and
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
Inference receives the body with selected existing source context: title,
|
|
76
|
+
description, evidence and references, known source dates, and normalized scope.
|
|
77
|
+
The request carries that context as JSON after a `Source context:` line, then
|
|
78
|
+
the raw body after a `Body:` line, as written and not JSON-escaped. Everything
|
|
79
|
+
after the first `Body:` line is treated as the body, so a body cannot pose as
|
|
80
|
+
context. A memory with an empty body is not sent. The prompt asks the model to preserve qualifications, uncertainty, actors, and the distinction between
|
|
81
|
+
requests, references, attempts, and demonstrated outcomes. The caller copies
|
|
82
|
+
the parent's normalized user/agent/run/channel scope to the child; the model
|
|
83
|
+
cannot author that scope. Source evidence remains on the parent via the backref.
|
|
84
|
+
Existing children are kept and never regenerated. On each inference run, an
|
|
85
|
+
existing child (`<parent>.derived.md` with `inferred: true` and `derivedFrom`
|
|
86
|
+
naming its parent) whose scope differs from its parent's has only its scope
|
|
87
|
+
frontmatter rewritten to match. This covers children written before scope was
|
|
88
|
+
copied and later parent scope edits, so the child stays reachable under the
|
|
89
|
+
parent's scope filter. The flat `scope_*` keys are set; stale ones and a legacy
|
|
90
|
+
nested `scope:` are removed. The body and other metadata are kept byte for
|
|
91
|
+
byte. No model call is made, a matching child is not written, and a read-only
|
|
92
|
+
default bundle is skipped. The run reports the count as `scopeSynced`. A cached no-change decision is reconsidered when
|
|
93
|
+
the body or supplied source context changes; unrelated metadata does not
|
|
94
|
+
invalidate it.
|
|
95
|
+
|
|
96
|
+
Search hits for the parent memory preserve a meaningful authored description
|
|
97
|
+
and tags that add information beyond filename and directory tokens. The derived
|
|
98
|
+
child's metadata fills missing or whitespace-only fields independently; a known
|
|
99
|
+
filename-generated description or tags consisting only of generated path tokens
|
|
100
|
+
also permit fallback. An authored tag repeating a path token counts as generated.
|
|
101
|
+
An `expandTo: memories/<derived>` field on the hit points at the richer
|
|
102
|
+
derived ref, qualified with the parent's bundle when needed. The parent ref
|
|
103
|
+
itself is preserved on the hit, so existing
|
|
79
104
|
automation keeps working — agents that want the deeper summary follow
|
|
80
105
|
`expandTo`.
|
|
81
106
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.261012-beta",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cursor.",
|
|
6
6
|
"keywords": [
|