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.
- package/CHANGELOG.md +97 -0
- package/STABILITY.md +25 -8
- package/dist/api.d.ts +6 -0
- package/dist/api.js +12 -1
- package/dist/assets/prompts/consolidate-pair-check.md +2 -0
- package/dist/assets/prompts/consolidate-pair-same-age.md +1 -0
- package/dist/assets/prompts/consolidate-pair-terms.md +6 -0
- package/dist/assets/prompts/consolidate-pair.md +4 -12
- package/dist/assets/prompts/memory-infer-system.md +1 -1
- package/dist/assets/prompts/memory-infer-user.md +3 -2
- package/dist/assets/ui/app.js +59 -2
- package/dist/assets/ui/asset.js +71 -6
- package/dist/assets/ui/bundles.js +80 -3
- package/dist/assets/ui/diff-view.js +1 -1
- package/dist/assets/ui/editor.js +103 -0
- package/dist/assets/ui/index.html +2 -0
- package/dist/assets/ui/lib.js +51 -1
- package/dist/assets/ui/markdown.js +86 -19
- package/dist/assets/ui/search.js +25 -5
- package/dist/assets/ui/styles.css +70 -0
- package/dist/commands/health/improve-metrics.js +11 -1
- package/dist/commands/health/md-report.js +3 -3
- package/dist/commands/health.js +1 -2
- package/dist/commands/improve/consolidate/pair-pass.js +59 -31
- package/dist/commands/improve/extract.js +18 -12
- package/dist/commands/improve/loop-stages.js +2 -2
- package/dist/commands/improve/memory/memory-belief.js +2 -2
- package/dist/commands/proposal/repository.js +44 -13
- package/dist/commands/read/curate.js +78 -19
- package/dist/commands/read/knowledge.js +109 -77
- package/dist/commands/read/search-cli.js +25 -11
- package/dist/commands/read/search.js +3 -3
- package/dist/commands/read/show.js +47 -1
- package/dist/commands/sources/bundle-cli.js +4 -6
- package/dist/commands/sources/installed-stashes.js +10 -19
- package/dist/commands/ui/api.js +95 -5
- package/dist/commands/ui/document-source.js +51 -0
- package/dist/commands/ui/git-read.js +18 -2
- package/dist/commands/ui/proposal-actions.js +5 -3
- package/dist/commands/ui/server.js +25 -13
- package/dist/commands/ui/ui-cli.js +3 -3
- package/dist/commands/ui/workspace-actions.js +264 -0
- package/dist/commands/workflow-cli.js +3 -1
- package/dist/core/adapter/adapters/akm-adapter.js +1 -0
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +3 -5
- package/dist/core/adapter/adapters/okf-adapter.js +3 -16
- package/dist/core/asset/akm-markdown.js +19 -3
- package/dist/core/asset/markdown-link-targets.js +43 -0
- package/dist/core/asset/markdown.js +14 -6
- package/dist/core/config/config.js +6 -4
- package/dist/core/non-task-input.js +67 -3
- package/dist/core/write-source.js +2 -2
- package/dist/indexer/index-writer-lock.js +5 -1
- package/dist/indexer/passes/memory-inference.js +28 -6
- package/dist/indexer/passes/metadata.js +17 -0
- package/dist/indexer/scan/doc-to-entry.js +1 -0
- package/dist/indexer/search/db-search.js +16 -7
- package/dist/indexer/search/search-hit-enrichers.js +3 -3
- package/dist/llm/memory-infer.js +23 -14
- package/dist/output/shapes/curate.js +3 -1
- package/dist/output/shapes/helpers.js +6 -0
- package/dist/output/text/show-format.js +7 -0
- package/dist/scripts/akm-migrate-node.js +1820 -329
- package/dist/scripts/akm-migrate.js +1820 -329
- package/dist/storage/repositories/index-entries-repository.js +16 -0
- package/dist/storage/repositories/index-fts-repository.js +28 -6
- package/dist/storage/repositories/index-vec-repository.js +19 -3
- package/dist/workflows/exec/environment.js +4 -8
- package/dist/workflows/parser.js +13 -4
- package/docs/architecture/0.10-process-rules.md +9 -7
- package/docs/architecture/architecture.md +4 -0
- package/docs/architecture/internals/storage-locations.md +1 -1
- package/docs/architecture/persisted-data-compat.md +1 -1
- package/docs/guides/author-workflows.md +78 -28
- package/docs/guides/bundles.md +16 -2
- package/docs/reference/api.md +19 -1
- package/docs/reference/cli.md +90 -18
- package/docs/reference/env-and-secrets.md +11 -0
- package/docs/reference/memory.md +7 -6
- package/docs/reference/ui-api.md +84 -8
- package/docs/reference/workflow-schema.md +63 -32
- package/package.json +2 -1
|
@@ -5,15 +5,13 @@
|
|
|
5
5
|
* The consolidate pair pass (0.9.17-alpha.9 plan §5.2, brief §A): runs
|
|
6
6
|
* alongside the existing promote pass inside `akmConsolidate`, over new and
|
|
7
7
|
* changed memory-tier assets. Judges each near-duplicate/superseding pair
|
|
8
|
-
* with
|
|
9
|
-
* for
|
|
10
|
-
* never writes belief edges
|
|
11
|
-
* triage (`drain.ts`).
|
|
8
|
+
* with complete claim comparisons and mints a reviewed `retire` proposal
|
|
9
|
+
* for derived `duplicate` / `subsumed` outcomes — never merges,
|
|
10
|
+
* never writes belief edges. A staged retirement can be accepted unattended
|
|
11
|
+
* by triage (`drain.ts`); an unstaged one stays pending.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* supersedes) is 20/22 = 0.91 precision against the owner. `T_PAIR` is 0.93,
|
|
16
|
-
* not the initial 0.90, because the 0.90–0.93 band alone graded 3/4 (R1).
|
|
13
|
+
* Pair-selection calibration (owner grades, 2026-09-28): `T_PAIR` stays
|
|
14
|
+
* 0.93, not the initial 0.90, because the 0.90–0.93 band alone graded 3/4 (R1).
|
|
17
15
|
*
|
|
18
16
|
* Initiator eligibility and dating (post-review, alpha.9): "created" is the
|
|
19
17
|
* asset's git first-add time (one `git log` per run, {@link loadGitFirstAddedMap}),
|
|
@@ -30,6 +28,8 @@ import fs from "node:fs";
|
|
|
30
28
|
import path from "node:path";
|
|
31
29
|
import consolidatePairPrompt from "../../../assets/prompts/consolidate-pair.md" with { type: "text" };
|
|
32
30
|
import consolidatePairCheckPrompt from "../../../assets/prompts/consolidate-pair-check.md" with { type: "text" };
|
|
31
|
+
import consolidatePairSameAge from "../../../assets/prompts/consolidate-pair-same-age.md" with { type: "text" };
|
|
32
|
+
import consolidatePairTerms from "../../../assets/prompts/consolidate-pair-terms.md" with { type: "text" };
|
|
33
33
|
import { parseFrontmatter } from "../../../core/asset/frontmatter.js";
|
|
34
34
|
import { conceptIdFromTypeName } from "../../../core/asset/resolve-ref.js";
|
|
35
35
|
import { asNonEmptyString } from "../../../core/common.js";
|
|
@@ -37,6 +37,7 @@ import { concurrentMap } from "../../../core/concurrent.js";
|
|
|
37
37
|
import { parseEmbeddedJsonResponse } from "../../../core/parse.js";
|
|
38
38
|
import { DERIVED_SUFFIX } from "../../../core/recognition-util.js";
|
|
39
39
|
import { warnOnce } from "../../../core/warn.js";
|
|
40
|
+
import { sameScope, scopeFromFrontmatter } from "../../../indexer/passes/metadata.js";
|
|
40
41
|
import { runnerLlmConnection } from "../../../integrations/agent/runner.js";
|
|
41
42
|
import { assertRunnerCredentials } from "../../../integrations/agent/runner-dispatch.js";
|
|
42
43
|
import { runGit } from "../../../sources/providers/git-install.js";
|
|
@@ -67,28 +68,22 @@ export const BACKFILL_FLOOR = 0.95;
|
|
|
67
68
|
export const NEW_MATERIAL_DAYS = 7;
|
|
68
69
|
/** Pairs judged per run, highest cosine first (plan §7's nightly cost budget). */
|
|
69
70
|
export const MAX_PAIRS_PER_RUN = 300;
|
|
70
|
-
/** Body characters sent to the judge per side: enough for nearly every note, so a retirement is judged on the whole text. */
|
|
71
|
-
const PAIR_BODY_TRUNCATE_CHARS = 12_000;
|
|
72
71
|
const MS_PER_DAY = 86_400_000;
|
|
73
|
-
const RELATION_LABELS = ["duplicate", "subsumed", "supersedes", "contradicts", "overlap", "unrelated"];
|
|
74
72
|
const RETIRE_LABELS = new Set(["duplicate", "subsumed", "supersedes"]);
|
|
75
|
-
/** The
|
|
73
|
+
/** The labels represented by the existing retirement metadata. */
|
|
76
74
|
function isRetireLabel(label) {
|
|
77
75
|
return RETIRE_LABELS.has(label);
|
|
78
76
|
}
|
|
79
77
|
/** Exported, with {@link buildPairUserPrompt}, so a replay can drive the exact judge call. */
|
|
80
78
|
export const PAIR_JUDGE_JSON_SCHEMA = {
|
|
81
79
|
type: "object",
|
|
82
|
-
required: ["
|
|
80
|
+
required: ["reason", "onlyInA", "onlyInB", "confidence"],
|
|
83
81
|
additionalProperties: false,
|
|
84
82
|
properties: {
|
|
83
|
+
reason: { type: "string", maxLength: 400 },
|
|
85
84
|
onlyInA: { type: "array", items: { type: "string", maxLength: 200 }, maxItems: 20 },
|
|
86
85
|
onlyInB: { type: "array", items: { type: "string", maxLength: 200 }, maxItems: 20 },
|
|
87
|
-
relation: { type: "string", enum: [...RELATION_LABELS] },
|
|
88
|
-
redundant: { type: ["string", "null"], enum: ["A", "B", null] },
|
|
89
|
-
stale: { type: ["string", "null"], enum: ["A", null] },
|
|
90
86
|
confidence: { type: "number", minimum: 0, maximum: 1 },
|
|
91
|
-
reason: { type: "string", maxLength: 400 },
|
|
92
87
|
},
|
|
93
88
|
};
|
|
94
89
|
export const PAIR_CHECK_JSON_SCHEMA = {
|
|
@@ -97,17 +92,11 @@ export const PAIR_CHECK_JSON_SCHEMA = {
|
|
|
97
92
|
additionalProperties: false,
|
|
98
93
|
properties: { missing: { type: "array", items: { type: "string", maxLength: 200 }, maxItems: 20 } },
|
|
99
94
|
};
|
|
100
|
-
/**
|
|
95
|
+
/** Validate both claim lists, then derive the outcome instead of asking the model to duplicate that decision. */
|
|
101
96
|
export function parsePairJudgeResponse(raw) {
|
|
102
97
|
const parsed = parseEmbeddedJsonResponse(raw);
|
|
103
98
|
if (!parsed)
|
|
104
99
|
return undefined;
|
|
105
|
-
if (typeof parsed.relation !== "string" || !RELATION_LABELS.includes(parsed.relation)) {
|
|
106
|
-
return undefined;
|
|
107
|
-
}
|
|
108
|
-
const redundant = parsed.redundant;
|
|
109
|
-
if (redundant !== "A" && redundant !== "B" && redundant !== null)
|
|
110
|
-
return undefined;
|
|
111
100
|
if (typeof parsed.confidence !== "number" || !Number.isFinite(parsed.confidence))
|
|
112
101
|
return undefined;
|
|
113
102
|
const confidence = Math.max(0, Math.min(1, parsed.confidence));
|
|
@@ -116,7 +105,13 @@ export function parsePairJudgeResponse(raw) {
|
|
|
116
105
|
const onlyInB = claimList(parsed.onlyInB);
|
|
117
106
|
if (!onlyInA || !onlyInB)
|
|
118
107
|
return undefined;
|
|
119
|
-
|
|
108
|
+
const relation = onlyInA.length === 0 && onlyInB.length === 0
|
|
109
|
+
? "duplicate"
|
|
110
|
+
: onlyInA.length === 0 || onlyInB.length === 0
|
|
111
|
+
? "subsumed"
|
|
112
|
+
: "overlap";
|
|
113
|
+
const redundant = onlyInA.length === 0 ? "A" : onlyInB.length === 0 ? "B" : null;
|
|
114
|
+
return { onlyInA, onlyInB, relation, redundant, confidence, reason };
|
|
120
115
|
}
|
|
121
116
|
function claimList(value) {
|
|
122
117
|
if (!Array.isArray(value) || !value.every((v) => typeof v === "string"))
|
|
@@ -383,6 +378,10 @@ export function selectCandidates(db, initiators, bundleId, unvectored) {
|
|
|
383
378
|
// rest of the backlog (no row, not recently git-added) needs the higher
|
|
384
379
|
// BACKFILL_FLOOR.
|
|
385
380
|
const floor = initiator.backlog && !initiator.newMaterial ? BACKFILL_FLOOR : T_PAIR;
|
|
381
|
+
const initiatorSide = loadSide(initiator, undefined, "");
|
|
382
|
+
if (!initiatorSide)
|
|
383
|
+
continue;
|
|
384
|
+
const scope = scopeFromFrontmatter(initiatorSide.frontmatter);
|
|
386
385
|
let kept = 0;
|
|
387
386
|
for (const hit of getNeighborsByEntryId(db, initiator.entryId, PAIR_NEIGHBOR_FETCH_K)) {
|
|
388
387
|
if (kept >= PAIR_NEIGHBOR_K)
|
|
@@ -411,6 +410,9 @@ export function selectCandidates(db, initiators, bundleId, unvectored) {
|
|
|
411
410
|
const cosine = 1 - (hit.distance * hit.distance) / 2;
|
|
412
411
|
if (cosine < floor)
|
|
413
412
|
continue;
|
|
413
|
+
const otherSide = loadSide(other, undefined, "");
|
|
414
|
+
if (!otherSide || !sameScope(scope, scopeFromFrontmatter(otherSide.frontmatter)))
|
|
415
|
+
continue;
|
|
414
416
|
const key = pairKey(initiator.ref, other.ref);
|
|
415
417
|
if (seenPairs.has(key))
|
|
416
418
|
continue;
|
|
@@ -460,11 +462,22 @@ function sideSection(label, side) {
|
|
|
460
462
|
`Description: ${asNonEmptyString(side.frontmatter.description) ?? "(none)"}`,
|
|
461
463
|
"Content:",
|
|
462
464
|
"```",
|
|
463
|
-
stripFrontmatterBody(side.raw)
|
|
465
|
+
stripFrontmatterBody(side.raw),
|
|
464
466
|
"```",
|
|
465
467
|
"",
|
|
466
468
|
];
|
|
467
469
|
}
|
|
470
|
+
/** Lexical attention hints only: equal token sets do not prove equal claims. */
|
|
471
|
+
function exclusiveTerms(side, other) {
|
|
472
|
+
const tokens = (note) => new Set(`${asNonEmptyString(note.frontmatter.description) ?? ""}\n${stripFrontmatterBody(note.raw)}`
|
|
473
|
+
.split(/\s+/)
|
|
474
|
+
.filter(Boolean));
|
|
475
|
+
const otherTerms = tokens(other);
|
|
476
|
+
return JSON.stringify([...tokens(side)].filter((term) => !otherTerms.has(term)));
|
|
477
|
+
}
|
|
478
|
+
function termHints(lines) {
|
|
479
|
+
return consolidatePairTerms.replace("{{DIFFERENCES}}", lines.join("\n"));
|
|
480
|
+
}
|
|
468
481
|
/** Orders two loaded sides by date (older/newer) for the "A (older)"/"B (newer)" labelling the prompt requires. */
|
|
469
482
|
function orderByAge(x, y) {
|
|
470
483
|
const xMs = Date.parse(x.createdIso);
|
|
@@ -480,6 +493,11 @@ export function buildPairUserPrompt(older, newer) {
|
|
|
480
493
|
return [
|
|
481
494
|
...sideSection(tied ? "A (same age as B)" : "A (older)", older),
|
|
482
495
|
...sideSection(tied ? "B (same age as A)" : "B (newer)", newer),
|
|
496
|
+
termHints([
|
|
497
|
+
`Tokens only in A: ${exclusiveTerms(older, newer)}`,
|
|
498
|
+
`Tokens only in B: ${exclusiveTerms(newer, older)}`,
|
|
499
|
+
]),
|
|
500
|
+
...(tied ? [consolidatePairSameAge] : []),
|
|
483
501
|
].join("\n");
|
|
484
502
|
}
|
|
485
503
|
/** The tombstone-vocabulary reason a judge label maps to (`supersedes` -> `superseded`; the rest unchanged). */
|
|
@@ -489,8 +507,7 @@ export function tombstoneReason(label) {
|
|
|
489
507
|
/**
|
|
490
508
|
* The calibrated outcome table (owner grades, replacing plan §5.2's
|
|
491
509
|
* "shorter body" rule): `duplicate`/`supersedes` keep the newer copy;
|
|
492
|
-
* `subsumed` keeps the side the
|
|
493
|
-
* if that pointer is missing or invalid). A side is retired only when the
|
|
510
|
+
* `subsumed` keeps the side opposite the derived `redundant` pointer. A side is retired only when the
|
|
494
511
|
* judge listed nothing that it alone holds.
|
|
495
512
|
*/
|
|
496
513
|
export function decideRetirement(label, redundant, older, newer, only) {
|
|
@@ -512,7 +529,7 @@ function checkSection(label, side) {
|
|
|
512
529
|
`Description: ${asNonEmptyString(side.frontmatter.description) ?? "(none)"}`,
|
|
513
530
|
"Content:",
|
|
514
531
|
"```",
|
|
515
|
-
stripFrontmatterBody(side.raw)
|
|
532
|
+
stripFrontmatterBody(side.raw),
|
|
516
533
|
"```",
|
|
517
534
|
"",
|
|
518
535
|
].join("\n");
|
|
@@ -528,7 +545,7 @@ async function confirmNothingLost(ctx, retired, successor) {
|
|
|
528
545
|
feature: "memory_consolidation",
|
|
529
546
|
runner: ctx.llmRunner,
|
|
530
547
|
system: consolidatePairCheckPrompt,
|
|
531
|
-
prompt: `${checkSection("X (to delete)", retired)}\n${checkSection("Y (kept)", successor)}`,
|
|
548
|
+
prompt: `${checkSection("X (to delete)", retired)}\n${checkSection("Y (kept)", successor)}\n${termHints([`Tokens only in X: ${exclusiveTerms(retired, successor)}`])}${Date.parse(retired.createdIso) === Date.parse(successor.createdIso) ? `\n${consolidatePairSameAge}` : ""}`,
|
|
532
549
|
gate: { config: ctx.config, enabled: true },
|
|
533
550
|
request: {
|
|
534
551
|
responseSchema: PAIR_CHECK_JSON_SCHEMA,
|
|
@@ -554,6 +571,13 @@ async function judgeOne(ctx, candidate) {
|
|
|
554
571
|
const otherSide = loadSide(candidate.other, ctx.gitFirstAdded, ctx.stashDir);
|
|
555
572
|
if (!initiatorSide || !otherSide)
|
|
556
573
|
return { failed: false }; // unreadable since selection — skip, not a judge failure
|
|
574
|
+
const judgedScope = scopeFromFrontmatter(initiatorSide.frontmatter);
|
|
575
|
+
if (!sameScope(judgedScope, scopeFromFrontmatter(otherSide.frontmatter)))
|
|
576
|
+
return { failed: false };
|
|
577
|
+
const scopesStillFresh = () => [initiatorSide, otherSide].every((side) => {
|
|
578
|
+
const live = loadSide(side.asset, ctx.gitFirstAdded, ctx.stashDir);
|
|
579
|
+
return live !== undefined && sameScope(judgedScope, scopeFromFrontmatter(live.frontmatter));
|
|
580
|
+
});
|
|
557
581
|
// Item 0 / S1: this exact pair (same two refs, same two content hashes,
|
|
558
582
|
// EITHER orientation) was already judged and rejected OR reverted. Checked
|
|
559
583
|
// HERE, before the judge call, not after — the judge (not yet run) is what
|
|
@@ -651,7 +675,10 @@ async function judgeOne(ctx, candidate) {
|
|
|
651
675
|
ledgerAccess: { proposalsCtx: ctx.opts.proposalsCtx },
|
|
652
676
|
search: ctx.continuitySearch,
|
|
653
677
|
});
|
|
678
|
+
if (!scopesStillFresh())
|
|
679
|
+
return { failed: true };
|
|
654
680
|
const retirement = {
|
|
681
|
+
judgedScope,
|
|
655
682
|
retiredRef: retired.asset.ref,
|
|
656
683
|
successorRef: successor.asset.ref,
|
|
657
684
|
cosine: candidate.cosine,
|
|
@@ -691,7 +718,8 @@ async function judgeOne(ctx, candidate) {
|
|
|
691
718
|
// reviewed pairs (2026-10-04). Anything else waits for a person.
|
|
692
719
|
if ((verdict.relation !== "duplicate" || verdict.onlyInA.length + verdict.onlyInB.length === 0) &&
|
|
693
720
|
!continuityRisk &&
|
|
694
|
-
(await confirmNothingLost(ctx, retired, successor))
|
|
721
|
+
(await confirmNothingLost(ctx, retired, successor)) &&
|
|
722
|
+
scopesStillFresh()) {
|
|
695
723
|
recordGateDecision(ctx.stashDir, proposal.id, {
|
|
696
724
|
outcome: "staged",
|
|
697
725
|
reason: verdict.relation,
|
|
@@ -189,17 +189,27 @@ export function parseSinceArg(value, now = Date.now()) {
|
|
|
189
189
|
throw new UsageError(`--since value "${value}" could not be parsed (expected ISO timestamp or duration like 24h / 7d / 30m)`, "INVALID_FLAG_VALUE");
|
|
190
190
|
}
|
|
191
191
|
/**
|
|
192
|
-
*
|
|
193
|
-
*
|
|
192
|
+
* Assemble candidate metadata once in the authoritative Markdown that acceptance
|
|
193
|
+
* publishes. The payload projection is for proposal readers, not another write path.
|
|
194
194
|
*/
|
|
195
195
|
function buildCandidateProposal(candidate, sourceRef, sessionAssetRef) {
|
|
196
196
|
const ref = deriveExtractCandidateRef(candidate, sourceRef);
|
|
197
197
|
// Complete a description the model cut mid-sentence (no-op for valid ones).
|
|
198
198
|
const description = repairTruncatedDescription(candidate.description, candidate.body);
|
|
199
|
-
const fm = {
|
|
200
|
-
|
|
199
|
+
const fm = {
|
|
200
|
+
description,
|
|
201
|
+
evidence: candidate.evidence,
|
|
202
|
+
...(sessionAssetRef ? { xrefs: [sessionAssetRef], evidenceSources: [sessionAssetRef] } : {}),
|
|
203
|
+
};
|
|
204
|
+
// With no published session asset, keep the known native session identity in
|
|
205
|
+
// the existing structured provenance channel, never invent an AKM asset ref.
|
|
206
|
+
// The identity, not the log's absolute path, so published notes carry no local paths.
|
|
207
|
+
if (!sessionAssetRef && sourceRef.filePath) {
|
|
208
|
+
fm.provenance = { sources: [{ resource: `${sourceRef.harness}:${sourceRef.sessionId}` }] };
|
|
209
|
+
}
|
|
210
|
+
if (candidate.when_to_use)
|
|
201
211
|
fm.when_to_use = candidate.when_to_use;
|
|
202
|
-
return { ref, content: assembleAsset(fm, candidate.body),
|
|
212
|
+
return { ref, content: assembleAsset(fm, candidate.body), frontmatter: fm };
|
|
203
213
|
}
|
|
204
214
|
function canonicalSegment(value) {
|
|
205
215
|
return value
|
|
@@ -496,13 +506,9 @@ async function processSession(run, sessionRef, gate) {
|
|
|
496
506
|
attemptedRefs: [built.ref],
|
|
497
507
|
payload: {
|
|
498
508
|
content: built.content,
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
confidence: candidate.confidence,
|
|
503
|
-
...(sessionAsset.sessionAssetRef ? { xrefs: [sessionAsset.sessionAssetRef] } : {}),
|
|
504
|
-
evidence: candidate.evidence,
|
|
505
|
-
},
|
|
509
|
+
// Model confidence stays on the proposal for reviewers; it is not published,
|
|
510
|
+
// where promotion scoring would read it as a signal.
|
|
511
|
+
frontmatter: { ...built.frontmatter, confidence: candidate.confidence },
|
|
506
512
|
},
|
|
507
513
|
});
|
|
508
514
|
proposalIds.push(proposal.id);
|
|
@@ -473,13 +473,13 @@ export async function runMemoryInferenceMaintenancePass(ctx, dbCell, memoryRefsF
|
|
|
473
473
|
reEnrich: false,
|
|
474
474
|
onProgress: (event) => {
|
|
475
475
|
const current = event.currentRef ? ` ${event.currentRef}` : "";
|
|
476
|
-
info(`[improve] memory inference ${event.processed}/${event.total}${current} (written ${event.writtenFacts}, skipped ${event.skippedNoFacts})`);
|
|
476
|
+
info(`[improve] memory inference ${event.processed}/${event.total}${current} (written ${event.writtenFacts}, no change ${event.noChange}, skipped ${event.skippedNoFacts})`);
|
|
477
477
|
},
|
|
478
478
|
})));
|
|
479
479
|
const memoryInference = pass.result;
|
|
480
480
|
if (!memoryInference)
|
|
481
481
|
return { durationMs: pass.durationMs, warnings: pass.warnings };
|
|
482
|
-
info(`[improve] memory inference complete (${memoryInference.writtenFacts} facts written from ${memoryInference.splitParents} parents)`);
|
|
482
|
+
info(`[improve] memory inference complete (${memoryInference.writtenFacts} facts written from ${memoryInference.splitParents} parents, ${memoryInference.noChange} no change)`);
|
|
483
483
|
return {
|
|
484
484
|
memoryInference,
|
|
485
485
|
durationMs: pass.durationMs,
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* present edge AND demotion is a no-op; an edge without its demotion is
|
|
8
8
|
* repaired) and never weakens a stronger demotion — severity is
|
|
9
9
|
* superseded > contradicted > archived. (alpha.4 removed belief weights from
|
|
10
|
-
* ranking:
|
|
11
|
-
*
|
|
10
|
+
* ranking: beliefState never changes rank. Search/curate filter current
|
|
11
|
+
* by default; --belief all|historical explicitly includes older context.)
|
|
12
12
|
* The SCC resolver in memory-improve.ts is a state-transition writer (it
|
|
13
13
|
* replaces and clears edges) and deliberately does not use this (#885).
|
|
14
14
|
*/
|
|
@@ -33,9 +33,10 @@ import { assertAkmAssetWrite, commitAcceptedPaths, commitWriteTargetBoundary, pr
|
|
|
33
33
|
import { withAssetMutationLease } from "../../indexer/index-writer-lock.js";
|
|
34
34
|
import { indexWrittenAssets } from "../../indexer/index-written-assets.js";
|
|
35
35
|
import { deriveInstallations } from "../../indexer/installations.js";
|
|
36
|
+
import { sameScope, scopeFromFrontmatter } from "../../indexer/passes/metadata.js";
|
|
36
37
|
import { resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
37
38
|
import { insertEventOnce } from "../../storage/repositories/events-repository.js";
|
|
38
|
-
import { CONSOLIDATE_LEDGER_SOURCE, forgetImproveLedgerDecision, recordImproveLedger, recordImproveLedgerDecision, reopenImproveLedgerDecision, } from "../../storage/repositories/improve-ledger-repository.js";
|
|
39
|
+
import { CONSOLIDATE_LEDGER_SOURCE, forgetImproveLedgerDecision, PAIR_PASS_LEDGER_SOURCE, recordImproveLedger, recordImproveLedgerDecision, reopenImproveLedgerDecision, } from "../../storage/repositories/improve-ledger-repository.js";
|
|
39
40
|
import { getStateProposal, listStateProposalIdsByPrefix, listStateProposals, upsertProposal, } from "../../storage/repositories/proposals-repository.js";
|
|
40
41
|
import { openSqliteReadSnapshot } from "../../storage/sqlite-read-snapshot.js";
|
|
41
42
|
import { pkgVersion } from "../../version.js";
|
|
@@ -528,6 +529,14 @@ export function archiveProposal(stashDir, id, status, reason, ctx, gateDecision)
|
|
|
528
529
|
at: decidedAt,
|
|
529
530
|
...(reason !== undefined ? { detail: reason } : {}),
|
|
530
531
|
});
|
|
532
|
+
// The pair pass settles initiators under bare refs, independently of the
|
|
533
|
+
// proposal's durable ref. A stale decision must release both inputs for
|
|
534
|
+
// reassessment through that same ledger, rather than hold an old body forever.
|
|
535
|
+
if (isRetireProposal(updated) && gateDecision?.reason === STALE_TARGET_GATE_REASON) {
|
|
536
|
+
for (const ref of retireHeldRefs(updated)) {
|
|
537
|
+
recordImproveLedger(db, { stashDir, ref, source: PAIR_PASS_LEDGER_SOURCE, outcome: "failed", at: decidedAt });
|
|
538
|
+
}
|
|
539
|
+
}
|
|
531
540
|
return updated;
|
|
532
541
|
}));
|
|
533
542
|
}
|
|
@@ -1007,6 +1016,14 @@ export function preflightProposalPromotion(config, proposal, options = {}, ctx)
|
|
|
1007
1016
|
// best-effort; the freshness check is the real staleness gate
|
|
1008
1017
|
}
|
|
1009
1018
|
}
|
|
1019
|
+
if (prepared.promotionSource) {
|
|
1020
|
+
const sourcePath = resolveAssetFilePathSafe(target.source, parseRefInput(prepared.promotionSource));
|
|
1021
|
+
if (sourcePath &&
|
|
1022
|
+
fs.existsSync(sourcePath) &&
|
|
1023
|
+
!sameScope(scopeFromFrontmatter(parseFrontmatter(fs.readFileSync(sourcePath, "utf8")).data), scopeFromFrontmatter(parseFrontmatter(stampedContent).data))) {
|
|
1024
|
+
throw new UsageError(`Promotion ${proposal.id} has incompatible source/output scope — reject and reassess.`, "INVALID_FLAG_VALUE");
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1010
1027
|
const lintBlockers = promotionLintBlockers(stampedContent, assetPath, target.source.path, ref.type, config);
|
|
1011
1028
|
if (lintBlockers.length > 0) {
|
|
1012
1029
|
const summary = lintBlockers.map((finding) => `[${finding.issue}] ${finding.detail}`).join("; ");
|
|
@@ -1127,7 +1144,12 @@ function retirePromotionSource(mutationTarget, accepted, paths) {
|
|
|
1127
1144
|
warn(`[proposal] O1: ${accepted.id} has no recorded source hash (minted by an older release) — leaving its source ${accepted.promotionSource} unarchived.`);
|
|
1128
1145
|
return;
|
|
1129
1146
|
}
|
|
1130
|
-
const
|
|
1147
|
+
const sourceContent = fs.readFileSync(sourcePath, "utf8");
|
|
1148
|
+
if (!sameScope(scopeFromFrontmatter(parseFrontmatter(sourceContent).data), scopeFromFrontmatter(parseFrontmatter(proposalContent(accepted)).data))) {
|
|
1149
|
+
warn(`[proposal] source scope changed for ${accepted.id} — leaving ${accepted.promotionSource} unarchived.`);
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
const currentHash = contentHash(sourceContent, "body");
|
|
1131
1153
|
if (currentHash !== accepted.promotionSourceHash) {
|
|
1132
1154
|
warn(`[proposal] O1: source ${accepted.promotionSource} for ${accepted.id} changed since the promotion was queued — leaving it unarchived.`);
|
|
1133
1155
|
return;
|
|
@@ -1141,7 +1163,8 @@ function retirePromotionSource(mutationTarget, accepted, paths) {
|
|
|
1141
1163
|
const record = archiveCleanupCandidate(mutationTarget.source.path, candidate, sourcePath);
|
|
1142
1164
|
paths.push(sourcePath, path.join(mutationTarget.source.path, record.archivedPath), path.join(mutationTarget.source.path, record.auditPath));
|
|
1143
1165
|
const twin = derivedTwinPath(sourcePath, sourceRef.type);
|
|
1144
|
-
if (twin
|
|
1166
|
+
if (twin &&
|
|
1167
|
+
sameScope(scopeFromFrontmatter(parseFrontmatter(fs.readFileSync(twin, "utf8")).data), scopeFromFrontmatter(parseFrontmatter(sourceContent).data))) {
|
|
1145
1168
|
const twinRecord = archiveCleanupCandidate(mutationTarget.source.path, candidate, twin);
|
|
1146
1169
|
paths.push(twin, path.join(mutationTarget.source.path, twinRecord.archivedPath), path.join(mutationTarget.source.path, twinRecord.auditPath));
|
|
1147
1170
|
}
|
|
@@ -1325,7 +1348,7 @@ function persistRetireAcceptance(stashDir, proposal, info, ctx) {
|
|
|
1325
1348
|
}
|
|
1326
1349
|
/**
|
|
1327
1350
|
* B2: throws a stale-retire `UsageError` unless the successor still exists
|
|
1328
|
-
* and both sides' recorded body hashes
|
|
1351
|
+
* and both sides' recorded body hashes and judged scopes still match — the
|
|
1329
1352
|
* durable half of the chain guard. Used for a fresh accept, and (4b, third
|
|
1330
1353
|
* review round) to re-check a resumed accept whose intent was recorded but
|
|
1331
1354
|
* nothing has moved yet: a separate proposal accepted in between (e.g. this
|
|
@@ -1335,6 +1358,15 @@ function persistRetireAcceptance(stashDir, proposal, info, ctx) {
|
|
|
1335
1358
|
function assertRetirementStillFresh(proposalId, proposalRef, retirement, targetSource, retiredCurrentBytes) {
|
|
1336
1359
|
const successorPath = resolveAssetFilePathSafe(targetSource, parseRefInput(retirement.successorRef));
|
|
1337
1360
|
const successorBytes = successorPath && fs.existsSync(successorPath) ? fs.readFileSync(successorPath) : undefined;
|
|
1361
|
+
const judgedScope = retirement.judgedScope;
|
|
1362
|
+
if (!judgedScope || typeof judgedScope !== "object" || Array.isArray(judgedScope)) {
|
|
1363
|
+
throw new UsageError(`Retire proposal ${proposalId} is stale — judged scope cannot be established; reject it and reassess the pair before acceptance.`, "INVALID_FLAG_VALUE");
|
|
1364
|
+
}
|
|
1365
|
+
if (!sameScope(scopeFromFrontmatter(parseFrontmatter(retiredCurrentBytes.toString("utf8")).data), judgedScope) ||
|
|
1366
|
+
(successorBytes &&
|
|
1367
|
+
!sameScope(scopeFromFrontmatter(parseFrontmatter(successorBytes.toString("utf8")).data), judgedScope))) {
|
|
1368
|
+
throw new UsageError(`Retire proposal ${proposalId} is stale — scope changed since judging; reject and reassess.`, "INVALID_FLAG_VALUE");
|
|
1369
|
+
}
|
|
1338
1370
|
const retiredFresh = contentHash(retiredCurrentBytes, "body") === retirement.retiredContentHash;
|
|
1339
1371
|
const successorFresh = successorBytes !== undefined && contentHash(successorBytes, "body") === retirement.successorContentHash;
|
|
1340
1372
|
if (!successorBytes || !retiredFresh || !successorFresh) {
|
|
@@ -1416,14 +1448,10 @@ async function retireProposalWithLease(stashDir, config, proposal, options, ctx)
|
|
|
1416
1448
|
}
|
|
1417
1449
|
else {
|
|
1418
1450
|
assertAkmAssetWrite(target.source);
|
|
1419
|
-
//
|
|
1420
|
-
//
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
// though nothing here changed. Once something has moved, it is too late
|
|
1424
|
-
// to cleanly refuse — Phase 2 below already tolerates a partial move.
|
|
1425
|
-
if (working.retirement && fs.existsSync(intent.assetPath)) {
|
|
1426
|
-
assertRetirementStillFresh(proposal.id, proposal.ref, working.retirement, target.source, fs.readFileSync(intent.assetPath));
|
|
1451
|
+
// Revalidate the survivor even after a partial archive. The recorded original
|
|
1452
|
+
// is the judged retired side when its file has already moved.
|
|
1453
|
+
if (working.retirement) {
|
|
1454
|
+
assertRetirementStillFresh(proposal.id, proposal.ref, working.retirement, target.source, fs.existsSync(intent.assetPath) ? fs.readFileSync(intent.assetPath) : Buffer.from(intent.backupContent));
|
|
1427
1455
|
}
|
|
1428
1456
|
}
|
|
1429
1457
|
// Phase 2: move, idempotently — a resumed call skips whichever file a
|
|
@@ -1449,7 +1477,10 @@ async function retireProposalWithLease(stashDir, config, proposal, options, ctx)
|
|
|
1449
1477
|
// tombstone scan above (`alreadyDone`) already finds one archived earlier,
|
|
1450
1478
|
// so storing it was redundant persisted state.
|
|
1451
1479
|
const twinPath = derivedTwinPath(intent.assetPath, ref.type);
|
|
1452
|
-
if (twinPath &&
|
|
1480
|
+
if (twinPath &&
|
|
1481
|
+
!alreadyDone.has(path.resolve(twinPath)) &&
|
|
1482
|
+
fs.existsSync(twinPath) &&
|
|
1483
|
+
sameScope(scopeFromFrontmatter(parseFrontmatter(fs.readFileSync(twinPath, "utf8")).data), scopeFromFrontmatter(parseFrontmatter(intent.backupContent).data))) {
|
|
1453
1484
|
const twinRecord = archiveCleanupCandidate(mutationTarget.source.path, candidate, twinPath);
|
|
1454
1485
|
archiveDirs.push(path.dirname(twinRecord.auditPath));
|
|
1455
1486
|
paths.push(twinPath, path.join(mutationTarget.source.path, twinRecord.archivedPath), path.join(mutationTarget.source.path, twinRecord.auditPath));
|
|
@@ -24,7 +24,7 @@ import { appendEvent } from "../../core/events.js";
|
|
|
24
24
|
import { nonTaskInput } from "../../core/non-task-input.js";
|
|
25
25
|
import { redactCredentialPatterns } from "../../core/redaction.js";
|
|
26
26
|
import { withStateDbTelemetry } from "../../core/state-db.js";
|
|
27
|
-
import { searchHitContent } from "../../indexer/search/db-search.js";
|
|
27
|
+
import { matchBeliefFilter, searchHitContent } from "../../indexer/search/db-search.js";
|
|
28
28
|
import { copySearchHitAttribution, getSearchHitAttribution, usageEventAttributionMetadata, } from "../../indexer/search/search-attribution.js";
|
|
29
29
|
import { insertUsageEvent } from "../../indexer/usage/usage-events.js";
|
|
30
30
|
import { estimateTokenCount } from "../../llm/embedders/remote.js";
|
|
@@ -32,9 +32,11 @@ import { isLlmFeatureEnabled, tryLlmFeature } from "../../llm/feature-gate.js";
|
|
|
32
32
|
import { rerankDocuments } from "../../llm/rerank-client.js";
|
|
33
33
|
import { truncateDescription } from "../../output/shapes/helpers.js";
|
|
34
34
|
import { TELEMETRY_BUSY_TIMEOUT_MS, withIndexDb } from "../../storage/repositories/index-db.js";
|
|
35
|
-
import { findEntryIdByRef, getItemRefById } from "../../storage/repositories/index-entries-repository.js";
|
|
36
|
-
import { akmSearch, parseSearchSource } from "./search.js";
|
|
35
|
+
import { findEntryIdByRef, getEntryIdByFilePath, getItemRefById, } from "../../storage/repositories/index-entries-repository.js";
|
|
36
|
+
import { akmSearch, normalizeScopeFilters, parseBeliefFilterMode, parseSearchSource } from "./search.js";
|
|
37
37
|
import { akmShowUnified } from "./show.js";
|
|
38
|
+
// Keep the effective policy with the result through packing without changing JSON.
|
|
39
|
+
const recallPolicies = new WeakMap();
|
|
38
40
|
const DEFAULT_CURATE_LIMIT = 4;
|
|
39
41
|
const MAX_CURATE_SUPPORT_REFS = 2;
|
|
40
42
|
/** Fused candidates the reranker reorders when `search.curateRerank.topN` is unset. */
|
|
@@ -103,13 +105,19 @@ export async function akmCurate(options) {
|
|
|
103
105
|
if (!trimmedQuery) {
|
|
104
106
|
throw new UsageError('A curation query is required. Usage: akm curate "<task or prompt>" [--type <type>] [--limit <n>]', "MISSING_REQUIRED_ARGUMENT");
|
|
105
107
|
}
|
|
108
|
+
const recall = {
|
|
109
|
+
type: options.type,
|
|
110
|
+
source: options.source ?? "local",
|
|
111
|
+
filters: normalizeScopeFilters(options.filters),
|
|
112
|
+
belief: parseBeliefFilterMode(options.belief ?? "current"),
|
|
113
|
+
};
|
|
106
114
|
const nonTask = nonTaskInput(trimmedQuery);
|
|
107
115
|
if (nonTask) {
|
|
108
116
|
const abstained = {
|
|
109
117
|
query: options.query,
|
|
110
118
|
summary: `Curate abstained: the input is ${nonTask}, not a task.`,
|
|
111
119
|
items: [],
|
|
112
|
-
tip: 'Nothing was selected on purpose. To curate for it, pass the task itself: akm curate "<what you are trying to do>".',
|
|
120
|
+
tip: 'Nothing was selected on purpose. To curate for it, pass the task itself: akm curate "<what you are trying to do>". For a literal asset or word lookup, use akm search "<literal words>".',
|
|
113
121
|
};
|
|
114
122
|
if (!options.skipLogging) {
|
|
115
123
|
logCurateEvent(options.query, abstained, options.eventSource, options.attributionProjection);
|
|
@@ -125,15 +133,18 @@ export async function akmCurate(options) {
|
|
|
125
133
|
// An enabled reranker reorders the top fused candidates, not just the final `limit`.
|
|
126
134
|
limit: Math.max(limit, rerankTopN(loadConfig())),
|
|
127
135
|
source,
|
|
136
|
+
filters: recall.filters,
|
|
137
|
+
belief: recall.belief,
|
|
128
138
|
skipLogging: true,
|
|
129
139
|
}));
|
|
130
|
-
const result = await curateSearchResults(options.query, searchResponse, limit, options.type, options.eventSource);
|
|
140
|
+
const result = await curateSearchResults(options.query, searchResponse, limit, options.type, options.eventSource, options.searchResponse ? undefined : recall);
|
|
141
|
+
recallPolicies.set(result, recall);
|
|
131
142
|
if (!options.skipLogging) {
|
|
132
143
|
logCurateEvent(options.query, result, options.eventSource, options.attributionProjection);
|
|
133
144
|
}
|
|
134
145
|
return result;
|
|
135
146
|
}
|
|
136
|
-
export async function curateSearchResults(query, result, limit, selectedType, eventSource) {
|
|
147
|
+
export async function curateSearchResults(query, result, limit, selectedType, eventSource, recall) {
|
|
137
148
|
const allStashHits = result.hits.filter((hit) => hit.type !== "registry");
|
|
138
149
|
const registryHits = result.registryHits ?? [];
|
|
139
150
|
// F3/R-018: `--type` NARROWS the candidate pool. The caller's search
|
|
@@ -145,9 +156,18 @@ export async function curateSearchResults(query, result, limit, selectedType, ev
|
|
|
145
156
|
// F4/R-019: respect `--limit` for registry fill instead of hard-capping it
|
|
146
157
|
// at a bare literal 2 — the remaining slots after stash hits ARE the cap.
|
|
147
158
|
const selectedRegistryHits = selectedStashHits.length >= limit ? [] : registryHits.slice(0, limit - selectedStashHits.length);
|
|
148
|
-
const selectedRefs = new Set(selectedStashHits.
|
|
159
|
+
const selectedRefs = new Set(selectedStashHits.flatMap((hit) => {
|
|
160
|
+
if (!recall)
|
|
161
|
+
return [hit.ref];
|
|
162
|
+
try {
|
|
163
|
+
return [hit.ref, evidenceRef(hit)];
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return [hit.ref]; // no longer indexed: enrichCuratedStashHit drops it
|
|
167
|
+
}
|
|
168
|
+
}));
|
|
149
169
|
const items = [
|
|
150
|
-
...(await Promise.all(selectedStashHits.map((hit) => enrichCuratedStashHit(query, hit, selectedRefs, eventSource)))),
|
|
170
|
+
...(await Promise.all(selectedStashHits.map((hit) => enrichCuratedStashHit(query, hit, selectedRefs, eventSource, recall)))).filter((item) => item !== undefined),
|
|
151
171
|
...selectedRegistryHits.map((hit) => buildCuratedRegistryItem(query, hit)),
|
|
152
172
|
].slice(0, limit);
|
|
153
173
|
return {
|
|
@@ -181,15 +201,24 @@ export async function packCuratedHits(result, budgetTokens) {
|
|
|
181
201
|
for (const item of stashItems) {
|
|
182
202
|
let shown;
|
|
183
203
|
try {
|
|
184
|
-
shown = await akmShowUnified({
|
|
204
|
+
shown = await akmShowUnified({
|
|
205
|
+
ref: evidenceRef(item),
|
|
206
|
+
skipLogging: true,
|
|
207
|
+
recall: recallPolicies.get(result) ?? { belief: "current" },
|
|
208
|
+
});
|
|
185
209
|
}
|
|
186
210
|
catch {
|
|
187
211
|
continue;
|
|
188
212
|
}
|
|
213
|
+
const historical = matchBeliefFilter(shown.beliefState, "historical");
|
|
214
|
+
const context = {
|
|
215
|
+
...(shown.beliefState ? { beliefState: shown.beliefState } : {}),
|
|
216
|
+
...(historical ? { reason: "Historical context; not current supporting authority." } : {}),
|
|
217
|
+
};
|
|
189
218
|
const content = shown.content ?? shown.template ?? shown.prompt ?? "";
|
|
190
219
|
const tokens = estimateTokenCount(content);
|
|
191
220
|
if (used + tokens <= budgetTokens) {
|
|
192
|
-
packed.push({ ref: item.ref, tokens, content });
|
|
221
|
+
packed.push({ ref: item.ref, tokens, content, ...context });
|
|
193
222
|
used += tokens;
|
|
194
223
|
continue;
|
|
195
224
|
}
|
|
@@ -197,7 +226,7 @@ export async function packCuratedHits(result, budgetTokens) {
|
|
|
197
226
|
const remaining = budgetTokens - used;
|
|
198
227
|
if (remaining > 0) {
|
|
199
228
|
const truncated = content.slice(0, remaining * 4);
|
|
200
|
-
packed.push({ ref: item.ref, tokens: estimateTokenCount(truncated), content: truncated });
|
|
229
|
+
packed.push({ ref: item.ref, tokens: estimateTokenCount(truncated), content: truncated, ...context });
|
|
201
230
|
used += estimateTokenCount(truncated);
|
|
202
231
|
}
|
|
203
232
|
}
|
|
@@ -205,22 +234,38 @@ export async function packCuratedHits(result, budgetTokens) {
|
|
|
205
234
|
}
|
|
206
235
|
return { query: result.query, budget: budgetTokens, tokens: used, items: packed };
|
|
207
236
|
}
|
|
208
|
-
|
|
237
|
+
function evidenceRef(item) {
|
|
238
|
+
return withIndexDb((db) => {
|
|
239
|
+
const id = getEntryIdByFilePath(db, item.path);
|
|
240
|
+
const ref = id === undefined ? undefined : getItemRefById(db, id);
|
|
241
|
+
if (!ref) {
|
|
242
|
+
if (item.path)
|
|
243
|
+
throw new UsageError(`Indexed asset no longer available: ${item.ref}`);
|
|
244
|
+
return item.ref;
|
|
245
|
+
}
|
|
246
|
+
const fragment = parseBundleRef(item.ref).fragment;
|
|
247
|
+
return fragment ? `${ref}#${fragment}` : ref;
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
async function enrichCuratedStashHit(query, hit, selectedRefs, eventSource, recall) {
|
|
209
251
|
let shown;
|
|
210
252
|
try {
|
|
211
|
-
shown = await akmShowUnified({ ref: hit.ref, eventSource, skipLogging: true });
|
|
253
|
+
shown = await akmShowUnified({ ref: recall ? evidenceRef(hit) : hit.ref, eventSource, skipLogging: true, recall });
|
|
212
254
|
}
|
|
213
255
|
catch {
|
|
256
|
+
if (recall)
|
|
257
|
+
return undefined;
|
|
214
258
|
shown = undefined;
|
|
215
259
|
}
|
|
260
|
+
const ref = shown?.selectedRef ?? shown?.ref ?? hit.ref;
|
|
216
261
|
const description = shown?.description ?? hit.description;
|
|
217
262
|
const preview = buildCuratedPreview(shown, hit);
|
|
218
|
-
const supportRefs = buildCurateSupportRefs(shown?.links, selectedRefs,
|
|
263
|
+
const supportRefs = await buildCurateSupportRefs(shown?.links, selectedRefs, ref, recall);
|
|
219
264
|
const item = {
|
|
220
265
|
source: "local",
|
|
221
266
|
type: shown?.type ?? hit.type,
|
|
222
267
|
name: shown?.name ?? hit.name,
|
|
223
|
-
ref
|
|
268
|
+
ref,
|
|
224
269
|
path: shown?.path ?? hit.path,
|
|
225
270
|
editable: shown?.editable ?? hit.editable ?? false,
|
|
226
271
|
...((shown?.editable ?? hit.editable ?? false) === false
|
|
@@ -232,8 +277,9 @@ async function enrichCuratedStashHit(query, hit, selectedRefs, eventSource) {
|
|
|
232
277
|
...(shown?.parameters?.length ? { parameters: shown.parameters } : {}),
|
|
233
278
|
...(shown?.run ? { run: shown.run } : {}),
|
|
234
279
|
...(supportRefs.length > 0 ? { supportRefs } : {}),
|
|
235
|
-
followUp: `akm show ${
|
|
236
|
-
|
|
280
|
+
followUp: `akm show ${ref}`,
|
|
281
|
+
...(shown?.beliefState ? { beliefState: shown.beliefState } : {}),
|
|
282
|
+
reason: `${matchBeliefFilter(shown?.beliefState, "historical") ? "Historical context. " : ""}${buildCuratedReason(query, shown?.type ?? hit.type)}`,
|
|
237
283
|
...(hit.score !== undefined ? { score: hit.score } : {}),
|
|
238
284
|
};
|
|
239
285
|
copySearchHitAttribution(hit, item, item.description);
|
|
@@ -333,7 +379,7 @@ function rerankDocumentTexts(hits) {
|
|
|
333
379
|
* it, in the order `akm show` lists them. The LLM entity graph's `related`
|
|
334
380
|
* list no longer feeds them.
|
|
335
381
|
*/
|
|
336
|
-
function buildCurateSupportRefs(links, selectedRefs, ownerRef) {
|
|
382
|
+
async function buildCurateSupportRefs(links, selectedRefs, ownerRef, recall) {
|
|
337
383
|
const supportRefs = [];
|
|
338
384
|
for (const [part, direction] of [
|
|
339
385
|
["outgoing", "from"],
|
|
@@ -343,11 +389,24 @@ function buildCurateSupportRefs(links, selectedRefs, ownerRef) {
|
|
|
343
389
|
for (const ref of group.refs) {
|
|
344
390
|
if (ref === ownerRef || selectedRefs.has(ref) || supportRefs.some((existing) => existing.ref === ref))
|
|
345
391
|
continue;
|
|
392
|
+
let shown;
|
|
393
|
+
if (recall) {
|
|
394
|
+
try {
|
|
395
|
+
// A support ref is a link, not a hit: `--type` narrows the hits only.
|
|
396
|
+
const { type: _type, ...linkRecall } = recall;
|
|
397
|
+
shown = await akmShowUnified({ ref, recall: linkRecall, skipLogging: true });
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const historical = matchBeliefFilter(shown?.beliefState, "historical");
|
|
346
404
|
const type = supportRefType(ref);
|
|
347
405
|
supportRefs.push({
|
|
348
406
|
ref,
|
|
349
407
|
...(type ? { type } : {}),
|
|
350
|
-
|
|
408
|
+
...(shown?.beliefState ? { beliefState: shown.beliefState } : {}),
|
|
409
|
+
reason: `${historical ? "Historical context; " : ""}Declared link (${kind}) ${direction} this asset.`,
|
|
351
410
|
});
|
|
352
411
|
if (supportRefs.length >= MAX_CURATE_SUPPORT_REFS)
|
|
353
412
|
return supportRefs;
|