akm-cli 0.10.261008-beta → 0.10.261010-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.
Files changed (82) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/STABILITY.md +25 -8
  3. package/dist/api.d.ts +6 -0
  4. package/dist/api.js +12 -1
  5. package/dist/assets/prompts/consolidate-pair-check.md +2 -0
  6. package/dist/assets/prompts/consolidate-pair-same-age.md +1 -0
  7. package/dist/assets/prompts/consolidate-pair-terms.md +6 -0
  8. package/dist/assets/prompts/consolidate-pair.md +4 -12
  9. package/dist/assets/prompts/memory-infer-system.md +1 -1
  10. package/dist/assets/prompts/memory-infer-user.md +3 -2
  11. package/dist/assets/ui/app.js +59 -2
  12. package/dist/assets/ui/asset.js +71 -6
  13. package/dist/assets/ui/bundles.js +80 -3
  14. package/dist/assets/ui/diff-view.js +1 -1
  15. package/dist/assets/ui/editor.js +103 -0
  16. package/dist/assets/ui/index.html +2 -0
  17. package/dist/assets/ui/lib.js +51 -1
  18. package/dist/assets/ui/markdown.js +86 -19
  19. package/dist/assets/ui/search.js +25 -5
  20. package/dist/assets/ui/styles.css +70 -0
  21. package/dist/commands/health/improve-metrics.js +11 -1
  22. package/dist/commands/health/md-report.js +3 -3
  23. package/dist/commands/health.js +1 -2
  24. package/dist/commands/improve/consolidate/pair-pass.js +59 -31
  25. package/dist/commands/improve/extract.js +18 -12
  26. package/dist/commands/improve/loop-stages.js +2 -2
  27. package/dist/commands/improve/memory/memory-belief.js +2 -2
  28. package/dist/commands/proposal/repository.js +44 -13
  29. package/dist/commands/read/curate.js +78 -19
  30. package/dist/commands/read/knowledge.js +109 -77
  31. package/dist/commands/read/search-cli.js +25 -11
  32. package/dist/commands/read/search.js +3 -3
  33. package/dist/commands/read/show.js +47 -1
  34. package/dist/commands/sources/bundle-cli.js +4 -6
  35. package/dist/commands/sources/installed-stashes.js +10 -19
  36. package/dist/commands/ui/api.js +95 -5
  37. package/dist/commands/ui/document-source.js +51 -0
  38. package/dist/commands/ui/git-read.js +18 -2
  39. package/dist/commands/ui/proposal-actions.js +5 -3
  40. package/dist/commands/ui/server.js +25 -13
  41. package/dist/commands/ui/ui-cli.js +3 -3
  42. package/dist/commands/ui/workspace-actions.js +264 -0
  43. package/dist/commands/workflow-cli.js +3 -1
  44. package/dist/core/adapter/adapters/akm-adapter.js +1 -0
  45. package/dist/core/adapter/adapters/llm-wiki-adapter.js +3 -5
  46. package/dist/core/adapter/adapters/okf-adapter.js +3 -16
  47. package/dist/core/asset/akm-markdown.js +19 -3
  48. package/dist/core/asset/markdown-link-targets.js +43 -0
  49. package/dist/core/asset/markdown.js +14 -6
  50. package/dist/core/config/config.js +6 -4
  51. package/dist/core/non-task-input.js +67 -3
  52. package/dist/core/write-source.js +2 -2
  53. package/dist/indexer/index-writer-lock.js +5 -1
  54. package/dist/indexer/passes/memory-inference.js +28 -6
  55. package/dist/indexer/passes/metadata.js +17 -0
  56. package/dist/indexer/scan/doc-to-entry.js +1 -0
  57. package/dist/indexer/search/db-search.js +16 -7
  58. package/dist/indexer/search/search-hit-enrichers.js +3 -3
  59. package/dist/llm/memory-infer.js +23 -14
  60. package/dist/output/shapes/curate.js +3 -1
  61. package/dist/output/shapes/helpers.js +6 -0
  62. package/dist/output/text/show-format.js +7 -0
  63. package/dist/scripts/akm-migrate-node.js +1820 -329
  64. package/dist/scripts/akm-migrate.js +1820 -329
  65. package/dist/storage/repositories/index-entries-repository.js +16 -0
  66. package/dist/storage/repositories/index-fts-repository.js +28 -6
  67. package/dist/storage/repositories/index-vec-repository.js +19 -3
  68. package/dist/workflows/exec/environment.js +4 -8
  69. package/dist/workflows/parser.js +13 -4
  70. package/docs/architecture/0.10-process-rules.md +9 -7
  71. package/docs/architecture/architecture.md +4 -0
  72. package/docs/architecture/internals/storage-locations.md +1 -1
  73. package/docs/architecture/persisted-data-compat.md +1 -1
  74. package/docs/guides/author-workflows.md +78 -28
  75. package/docs/guides/bundles.md +16 -2
  76. package/docs/reference/api.md +19 -1
  77. package/docs/reference/cli.md +90 -18
  78. package/docs/reference/env-and-secrets.md +11 -0
  79. package/docs/reference/memory.md +7 -6
  80. package/docs/reference/ui-api.md +84 -8
  81. package/docs/reference/workflow-schema.md +63 -32
  82. package/package.json +2 -1
@@ -64,6 +64,12 @@ const FM_INFERRED = "inferred";
64
64
  const FM_INFERENCE_PROCESSED = "inferenceProcessed";
65
65
  const FM_SOURCE = "source";
66
66
  const FM_CAPTURE_MODE = "captureMode";
67
+ /**
68
+ * Cache namespace for this pass. Bump it when the cached response semantics
69
+ * change (v3: a cached row may be an explicit no-change decision); rows under an
70
+ * older namespace are never read again, which is a disposable cache miss.
71
+ */
72
+ const MEMORY_INFERENCE_CACHE_VARIANT = "memory-inference-v3";
67
73
  function validateDerivedMemoryDraft(raw) {
68
74
  if (!raw || typeof raw !== "object")
69
75
  return undefined;
@@ -79,6 +85,10 @@ function validateDerivedMemoryDraft(raw) {
79
85
  ? { title, description, tags, searchHints, content }
80
86
  : undefined;
81
87
  }
88
+ /** Cache validator: a recognised no-change decision, a complete draft, or `undefined` (unusable → recompute). */
89
+ function validateMemoryInferDecision(raw) {
90
+ return memoryInfer.isMemoryInferNoChange(raw) ? memoryInfer.MEMORY_INFER_NO_CHANGE : validateDerivedMemoryDraft(raw);
91
+ }
82
92
  function planPendingMemoryRecord(record, ctx) {
83
93
  if (ctx.signal?.aborted)
84
94
  return { kind: "aborted", record };
@@ -86,11 +96,11 @@ function planPendingMemoryRecord(record, ctx) {
86
96
  return { kind: "existing-child", record };
87
97
  if (ctx.db && !ctx.reEnrich) {
88
98
  const cached = bestEffort(() => {
89
- const entry = getLlmCacheEntry(ctx.db, record.filePath, computeBodyHash(record.body), "memory-inference-v2");
90
- return entry ? validateDerivedMemoryDraft(JSON.parse(entry.resultJson)) : undefined;
99
+ const entry = getLlmCacheEntry(ctx.db, record.filePath, computeBodyHash(record.body), MEMORY_INFERENCE_CACHE_VARIANT);
100
+ return entry ? validateMemoryInferDecision(JSON.parse(entry.resultJson)) : undefined;
91
101
  }, "memory inference cache read corrupt — fall through to recompute");
92
102
  if (cached)
93
- return { kind: "cache-hit", record, derived: cached };
103
+ return { kind: "cache-hit", record, decision: cached };
94
104
  }
95
105
  return { kind: "model", record };
96
106
  }
@@ -132,11 +142,11 @@ async function inferPendingMemoryRecord(plan, ctx) {
132
142
  retryAttempts += 1;
133
143
  };
134
144
  const infer = () => compressMemoryToDerivedMemory(llmRunner, record.body, signal, featureConfig, (event) => warn(`[akm] LLM fallback for ${event.feature}: ${event.reason}`), inferTelemetry, onRetryAttempt, onNotices);
135
- let derived = plan.kind === "cache-hit" ? plan.derived : undefined;
145
+ let derived = plan.kind === "cache-hit" ? plan.decision : undefined;
136
146
  try {
137
147
  if (plan.kind === "model") {
138
148
  derived = db
139
- ? await withLlmCache(db, record.filePath, record.body, reEnrich ?? false, infer, validateDerivedMemoryDraft, undefined, "memory-inference-v2", {
149
+ ? await withLlmCache(db, record.filePath, record.body, reEnrich ?? false, infer, validateMemoryInferDecision, undefined, MEMORY_INFERENCE_CACHE_VARIANT, {
140
150
  onCacheHit: () => {
141
151
  fromCache = true;
142
152
  },
@@ -153,6 +163,12 @@ async function inferPendingMemoryRecord(plan, ctx) {
153
163
  }
154
164
  if (!derived)
155
165
  return { skipped: true, fromCache, retryAttempts };
166
+ // An explicit decision, not a failure. The parent is left exactly as it is: stamping it
167
+ // `inferenceProcessed` would hide every later edit, and the cached decision already
168
+ // stops the retry for as long as the effective input is unchanged.
169
+ if (memoryInfer.isMemoryInferNoChange(derived)) {
170
+ return { skipped: false, splitParent: false, written: 0, noChange: true, fromCache, retryAttempts };
171
+ }
156
172
  const writeOutcome = await writeDerivedMemory(record, derived);
157
173
  if (writeOutcome.written > 0) {
158
174
  markParentProcessed(record);
@@ -209,6 +225,7 @@ async function runMemoryInferencePassBody(ctx, provenance) {
209
225
  retryAttempts: 0,
210
226
  splitParents: 0,
211
227
  writtenFacts: 0,
228
+ noChange: 0,
212
229
  skippedNoFacts: 0,
213
230
  skippedChildExists: 0,
214
231
  skippedAborted: 0,
@@ -273,7 +290,7 @@ async function runMemoryInferencePassBody(ctx, provenance) {
273
290
  assertRunnerCredentials(llmRunner);
274
291
  let processed = 0;
275
292
  const total = pending.length;
276
- onProgress?.({ processed, total, writtenFacts: 0, skippedNoFacts: 0 });
293
+ onProgress?.({ processed, total, writtenFacts: 0, noChange: 0, skippedNoFacts: 0 });
277
294
  let configFailure;
278
295
  const perRecordResults = await concurrentMap(plans, (plan) => inferPendingMemoryRecord(plan, {
279
296
  config,
@@ -305,6 +322,7 @@ async function runMemoryInferencePassBody(ctx, provenance) {
305
322
  processed,
306
323
  total,
307
324
  writtenFacts: result.writtenFacts,
325
+ noChange: result.noChange,
308
326
  skippedNoFacts: result.skippedNoFacts,
309
327
  currentRef: pending[i]?.ref,
310
328
  });
@@ -321,6 +339,9 @@ async function runMemoryInferencePassBody(ctx, provenance) {
321
339
  // Intentionally NOT marked processed — a transient LLM failure should
322
340
  // be retried on the next index run.
323
341
  }
342
+ else if ("noChange" in res && res.noChange) {
343
+ result.noChange += 1;
344
+ }
324
345
  else if (res.splitParent) {
325
346
  result.splitParents += 1;
326
347
  result.writtenFacts += res.written;
@@ -349,6 +370,7 @@ async function runMemoryInferencePassBody(ctx, provenance) {
349
370
  processed,
350
371
  total,
351
372
  writtenFacts: result.writtenFacts,
373
+ noChange: result.noChange,
352
374
  skippedNoFacts: result.skippedNoFacts,
353
375
  currentRef: pending[i]?.ref,
354
376
  });
@@ -165,6 +165,8 @@ export function validateStashEntry(entry) {
165
165
  const evidenceSources = normalizeNonEmptyStringList(e.evidenceSources);
166
166
  if (evidenceSources)
167
167
  result.evidenceSources = evidenceSources;
168
+ if (typeof e.evidence === "string" && e.evidence.trim().length > 0)
169
+ result.evidence = e.evidence;
168
170
  if (typeof e.derivedFrom === "string" && e.derivedFrom.trim().length > 0) {
169
171
  result.derivedFrom = e.derivedFrom.trim();
170
172
  }
@@ -242,6 +244,19 @@ export function applyScopeFrontmatter(entry, fmData) {
242
244
  if (scope)
243
245
  entry.scope = scope;
244
246
  }
247
+ /** The same normalized tuple the index projects, including legacy nested scope. */
248
+ export function scopeFromFrontmatter(fm) {
249
+ const entry = { type: "memory", name: "scope" };
250
+ if (typeof fm.scope === "object" && fm.scope !== null && !Array.isArray(fm.scope)) {
251
+ entry.scope = normalizeScopeObject(fm.scope);
252
+ }
253
+ applyScopeFrontmatter(entry, fm);
254
+ return entry.scope ?? {};
255
+ }
256
+ /** Exact maintenance equality: absence is a value, never a wildcard. */
257
+ export function sameScope(a, b) {
258
+ return SCOPE_KEYS.every((key) => a[key] === b[key]);
259
+ }
245
260
  function normalizeIntent(value) {
246
261
  if (typeof value !== "object" || value === null || Array.isArray(value))
247
262
  return undefined;
@@ -367,6 +382,8 @@ export function applyCuratedFrontmatter(entry, fmData) {
367
382
  const evidenceSources = normalizeNonEmptyStringList(fmData.evidenceSources);
368
383
  if (evidenceSources)
369
384
  entry.evidenceSources = evidenceSources;
385
+ if (typeof fmData.evidence === "string" && fmData.evidence.trim().length > 0)
386
+ entry.evidence = fmData.evidence;
370
387
  // Phase 5A / Advantage D5: capture parent ref for derived memories.
371
388
  // Memory-inference writes `source: "memories/<parent>"` and `inferred: true`
372
389
  // (and a derived child name suffix `.derived`). We mirror that source ref into
@@ -138,6 +138,7 @@ export function indexDocumentToStashEntry(doc) {
138
138
  entry.wikiRole = dj.wikiRole;
139
139
  assignStringList(entry, "sources", dj.sources);
140
140
  assignStringList(entry, "evidenceSources", dj.evidenceSources);
141
+ assignString(entry, "evidence", dj.evidence);
141
142
  assignStringList(entry, "uses", dj.uses);
142
143
  // D2 (#730): OKF v0.2 provenance promoteProposal stamps onto AKM-native
143
144
  // writes, carried via DOCUMENT_JSON_CARRIED_FIELDS (akm-adapter.ts) —
@@ -22,7 +22,7 @@ import { embed } from "../../llm/embedder.js";
22
22
  import { applyEmbeddingTemplate, resolveEmbeddingProfile } from "../../llm/embedders/profile.js";
23
23
  import { normalizeEmbeddingEndpoint } from "../../llm/embedders/remote.js";
24
24
  import { assertIndexPathReadable, closeDatabase, openExistingDatabase, openReadonlyExistingDatabase, } from "../../storage/repositories/index-connection.js";
25
- import { getAllEntries, getBaseBeliefStatesForDerivedTwins, getEntryById, getEntryCount, getEntryRefsAndTypes, } from "../../storage/repositories/index-entries-repository.js";
25
+ import { getAllEntries, getBaseBeliefStatesForDerivedTwins, getEntriesByIds, getEntryById, getEntryCount, getEntryRefsAndTypes, } from "../../storage/repositories/index-entries-repository.js";
26
26
  import { searchFts } from "../../storage/repositories/index-fts-repository.js";
27
27
  import { getMeta } from "../../storage/repositories/index-meta-repository.js";
28
28
  import { getEmbeddingCount, searchVec } from "../../storage/repositories/index-vec-repository.js";
@@ -221,8 +221,14 @@ async function searchDatabase(db, input) {
221
221
  const typeFilter = searchType === "any" ? undefined : searchType;
222
222
  const startedAt = Date.now();
223
223
  // The query embedding request goes out first, so FTS runs while it is in flight.
224
- const vectorChannel = startVectorChannel(db, query, config);
225
- const lexical = searchFts(db, query, CHANNEL_DEPTH, typeFilter, defaultExcludes);
224
+ const filterCandidates = (ids) => {
225
+ const entries = getEntriesByIds(db, ids)
226
+ .filter(hasIndexedProvenance)
227
+ .filter((row) => (typeFilter ? row.entry.type === typeFilter : !defaultExcludes.includes(row.entry.type)));
228
+ return new Set(applyEntryFilters(entries, filterOptions).map((row) => row.id));
229
+ };
230
+ const vectorChannel = startVectorChannel(db, query, config, filterCandidates);
231
+ const lexical = searchFts(db, query, CHANNEL_DEPTH, typeFilter, defaultExcludes, filterCandidates);
226
232
  const vector = await vectorChannel;
227
233
  const embedMs = Date.now() - startedAt;
228
234
  const tRank0 = Date.now();
@@ -244,6 +250,7 @@ async function searchDatabase(db, input) {
244
250
  sources,
245
251
  config,
246
252
  db,
253
+ eligible: (row) => applyEntryFilters([row], filterOptions).length > 0,
247
254
  })));
248
255
  const mode = vector.warning ? "fts-fallback" : vector.neighbors ? "semantic" : "keyword";
249
256
  return { embedMs, rankMs, hits, mode, semanticWarning: vector.warning, feedbackWarning: feedback.warning };
@@ -371,6 +378,7 @@ async function enumerateEntries(opts) {
371
378
  sources,
372
379
  config,
373
380
  db,
381
+ eligible: (row) => applyEntryFilters([row], opts).length > 0,
374
382
  })));
375
383
  }
376
384
  /**
@@ -378,7 +386,7 @@ async function enumerateEntries(opts) {
378
386
  * source-narrowing → scope → proposed-quality → derived-twin belief
379
387
  * inheritance → belief filter.
380
388
  */
381
- function applyEntryFilters(items, opts) {
389
+ export function applyEntryFilters(items, opts) {
382
390
  const { filters } = opts;
383
391
  // Source filter: when the caller narrowed `sources` via `--from <name>`,
384
392
  // drop entries whose filePath does not live under any requested source. The
@@ -428,7 +436,7 @@ function inheritDerivedTwinBeliefStates(db, items) {
428
436
  }
429
437
  }
430
438
  }
431
- function matchBeliefFilter(beliefState, filter) {
439
+ export function matchBeliefFilter(beliefState, filter) {
432
440
  if (filter === "all")
433
441
  return true;
434
442
  // 03: the belief filter applies to ANY flagged entry, not just memories, so
@@ -452,7 +460,7 @@ function matchBeliefFilter(beliefState, filter) {
452
460
  * query overlaps it. A slow or failing embedder degrades the search to keyword
453
461
  * ranking with a warning after `embedding.queryTimeoutMs`.
454
462
  */
455
- function startVectorChannel(db, query, config) {
463
+ function startVectorChannel(db, query, config, filterCandidates) {
456
464
  if (config.semanticSearchMode === "off")
457
465
  return Promise.resolve({ neighbors: null });
458
466
  // A real-time completeness fact, not a cached verdict: skip the round trip
@@ -472,7 +480,7 @@ function startVectorChannel(db, query, config) {
472
480
  });
473
481
  const queryText = applyEmbeddingTemplate(resolveEmbeddingProfile(config.embedding).queryTemplate, query);
474
482
  return Promise.race([embed(queryText, config.embedding, controller.signal), timeout])
475
- .then((vector) => ({ neighbors: searchVec(db, vector, CHANNEL_DEPTH) }))
483
+ .then((vector) => ({ neighbors: searchVec(db, vector, CHANNEL_DEPTH, undefined, filterCandidates) }))
476
484
  .catch((error) => ({ neighbors: null, warning: buildVectorFallbackWarning(config, error) }))
477
485
  .finally(() => clearTimeout(timer));
478
486
  }
@@ -574,6 +582,7 @@ export async function buildDbHit(input) {
574
582
  stashDir: entryStashDir,
575
583
  bundleId: input.bundleId,
576
584
  db: input.db,
585
+ eligible: input.eligible,
577
586
  });
578
587
  return hit;
579
588
  }
@@ -17,7 +17,7 @@ export async function enrichSearchHit(hit, ctx) {
17
17
  renderer?.enrichSearchHit?.(hit, ctx.stashDir);
18
18
  }
19
19
  if (ctx.type === "memory" && ctx.db)
20
- surfaceDerivedMemory(hit, ctx.db, ctx.bundleId);
20
+ surfaceDerivedMemory(hit, ctx.db, ctx.bundleId, ctx.eligible);
21
21
  }
22
22
  /**
23
23
  * Phase 5A / Advantage D5 — when a parent memory has a `.derived` child
@@ -28,7 +28,7 @@ export async function enrichSearchHit(hit, ctx) {
28
28
  * The parent ref is preserved on the hit — only the surface text is
29
29
  * swapped, so links and provenance still point at the canonical parent.
30
30
  */
31
- function surfaceDerivedMemory(hit, db, bundleId) {
31
+ function surfaceDerivedMemory(hit, db, bundleId, eligible) {
32
32
  // Never recurse: a `.derived` hit is itself the child surface; leaving
33
33
  // it untouched also avoids `<parent>.derived.derived` chains.
34
34
  if (hit.name.toLowerCase().endsWith(".derived"))
@@ -38,7 +38,7 @@ function surfaceDerivedMemory(hit, db, bundleId) {
38
38
  // carries. INTERNAL lookup key into `getDerivedForParent`: the `derived_from`
39
39
  // column stores this same conceptId grammar.
40
40
  const derived = getDerivedForParent(db, `memories/${hit.name}`, bundleId);
41
- if (!derived)
41
+ if (!derived || (eligible && !eligible(derived)))
42
42
  return;
43
43
  // Swap description / tags from the derived child (SourceSearchHit carries no
44
44
  // `searchHints`).
@@ -34,35 +34,40 @@ const PROMPT_PLACEHOLDERS = new Set([
34
34
  "search phrase 2",
35
35
  "2-3 sentence compressed body preserving key facts verbatim",
36
36
  ]);
37
+ export const MEMORY_INFER_NO_CHANGE = Object.freeze({ noChange: true });
38
+ export function isMemoryInferNoChange(value) {
39
+ return typeof value === "object" && value !== null && value.noChange === true;
40
+ }
37
41
  /**
38
42
  * Strict JSON Schema for the derived-memory payload. Sent as `response_format`
39
43
  * unless the engine sets `supportsJsonSchema: false`; the client drops it once
40
44
  * for a provider that rejects it.
41
45
  *
42
- * Extends the responseSchema lift (PR 1, asset-writers-investigation §5) to
43
- * the memory-inference path. Mirrors the validation gate below
44
- * (title/description/content + non-empty tags/searchHints) so a
45
- * schema-compliant response is guaranteed to pass the downstream check
46
- * — no more "incomplete derived memory payload from LLM; skipping memory"
47
- * for shape-only failures.
46
+ * Every property is required (a strict provider refuses otherwise), so the
47
+ * explicit no-change decision is `noChange: true` with the other fields empty.
48
+ * That also means the schema cannot demand non-empty fields: the parser below
49
+ * is the gate that requires a complete payload whenever `noChange` is false.
48
50
  */
49
51
  export const DERIVED_MEMORY_JSON_SCHEMA = {
50
52
  type: "object",
51
53
  properties: {
52
- title: { type: "string", minLength: 1 },
53
- description: { type: "string", minLength: 1 },
54
- content: { type: "string", minLength: 1 },
55
- tags: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 8 },
56
- searchHints: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 6 },
54
+ noChange: { type: "boolean" },
55
+ title: { type: "string" },
56
+ description: { type: "string" },
57
+ content: { type: "string" },
58
+ tags: { type: "array", items: { type: "string" }, maxItems: 8 },
59
+ searchHints: { type: "array", items: { type: "string" }, maxItems: 6 },
57
60
  },
58
- required: ["title", "description", "content", "tags", "searchHints"],
61
+ required: ["noChange", "title", "description", "content", "tags", "searchHints"],
59
62
  additionalProperties: false,
60
63
  };
61
64
  /**
62
65
  * Compress a single memory body into one derived memory via the configured LLM.
63
66
  *
64
- * Returns `undefined` on any failure (timeout, invalid JSON, empty response).
65
- * Errors are logged via `warn()` but never thrown — a failed split for one memory
67
+ * Returns the draft, or {@link MEMORY_INFER_NO_CHANGE} when the model explicitly
68
+ * decides the memory needs no derived memory. Returns `undefined` on any failure
69
+ * (timeout, invalid JSON, empty or incomplete response, HTML, abort, disabled
70
+ * pass), which is never the same thing as no-change. Errors are logged via `warn()` but never thrown — a failed split for one memory
66
71
  * must not abort the rest of the index pass.
67
72
  *
68
73
  * Routes through `callStructured({ feature: "memory_inference", ... })` so the
@@ -105,6 +110,10 @@ export async function compressMemoryToDerivedMemory(llmRunner, body, signal, akm
105
110
  warn("memory inference: invalid JSON response from LLM; skipping memory.");
106
111
  return undefined;
107
112
  }
113
+ // Only a literal boolean `true` is a decision; anything else falls through
114
+ // to the complete-payload checks, so a malformed reply is never restraint.
115
+ if (parsed.noChange === true)
116
+ return MEMORY_INFER_NO_CHANGE;
108
117
  const title = typeof parsed.title === "string" ? parsed.title.trim() : "";
109
118
  const description = typeof parsed.description === "string" ? parsed.description.trim() : "";
110
119
  const content = typeof parsed.content === "string" ? parsed.content.trim() : "";
@@ -5,7 +5,7 @@ import { capDescription, NORMAL_DESCRIPTION_LIMIT, pickFields } from "./helpers.
5
5
  // Curation is a small, high-signal top-N. Even at `brief` we keep `followUp`
6
6
  // (the actionable `akm show <ref>` command) and `reason` (why this asset was
7
7
  // selected) — these are the point of curate, unlike a bulk search listing.
8
- const BRIEF_FIELDS = ["source", "type", "name", "ref", "id", "supportRefs", "followUp", "reason"];
8
+ const BRIEF_FIELDS = ["source", "type", "name", "ref", "id", "supportRefs", "beliefState", "followUp", "reason"];
9
9
  const NORMAL_FIELDS = [
10
10
  "source",
11
11
  "type",
@@ -21,6 +21,7 @@ const NORMAL_FIELDS = [
21
21
  "parameters",
22
22
  "run",
23
23
  "supportRefs",
24
+ "beliefState",
24
25
  "followUp",
25
26
  "reason",
26
27
  "score",
@@ -37,6 +38,7 @@ const AGENT_FIELDS = [
37
38
  "id",
38
39
  "description",
39
40
  "supportRefs",
41
+ "beliefState",
40
42
  "followUp",
41
43
  "reason",
42
44
  "score",
@@ -386,6 +386,9 @@ export function shapeShowOutput(result, detail, shape = "human") {
386
386
  "steps",
387
387
  "keys",
388
388
  "links",
389
+ "evidence",
390
+ "evidenceSources",
391
+ "provenance",
389
392
  ...FRAGMENT_PROVENANCE_FIELDS,
390
393
  ...FRAGMENT_CONTEXT_FIELDS,
391
394
  ]);
@@ -418,6 +421,9 @@ export function shapeShowOutput(result, detail, shape = "human") {
418
421
  "activeRun",
419
422
  "keys",
420
423
  "links",
424
+ "evidence",
425
+ "evidenceSources",
426
+ "provenance",
421
427
  ...FRAGMENT_PROVENANCE_FIELDS,
422
428
  ...FRAGMENT_CONTEXT_FIELDS,
423
429
  // ref, path, and editable are always projected — at every --detail level,
@@ -20,6 +20,13 @@ export function formatShowPlain(r, detail) {
20
20
  }
21
21
  if (r.description)
22
22
  lines.push(`description: ${String(r.description)}`);
23
+ // Evidence is captured text: keep it on one line so it cannot forge other fields.
24
+ if (r.evidence)
25
+ lines.push(`evidence: ${String(r.evidence).replace(/\s+/g, " ")}`);
26
+ if (Array.isArray(r.evidenceSources))
27
+ lines.push(`evidenceSources: ${r.evidenceSources.join(", ")}`);
28
+ if (r.evidence && r.provenance)
29
+ lines.push(`provenance: ${JSON.stringify(r.provenance)}`);
23
30
  if (r.workflowTitle)
24
31
  lines.push(`workflowTitle: ${String(r.workflowTitle)}`);
25
32
  if (r.agent)