akm-cli 0.10.261009-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 +36 -0
- package/STABILITY.md +15 -4
- 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/improve/loop-stages.js +2 -2
- package/dist/commands/read/curate.js +1 -1
- package/dist/commands/read/knowledge.js +109 -77
- 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/asset/akm-markdown.js +19 -3
- 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/llm/memory-infer.js +23 -14
- package/dist/scripts/akm-migrate-node.js +18 -10
- package/dist/scripts/akm-migrate.js +18 -10
- package/dist/workflows/exec/environment.js +4 -8
- package/dist/workflows/parser.js +13 -4
- package/docs/architecture/0.10-process-rules.md +5 -3
- package/docs/architecture/internals/storage-locations.md +1 -1
- package/docs/guides/author-workflows.md +78 -28
- package/docs/guides/bundles.md +16 -2
- package/docs/reference/cli.md +39 -16
- package/docs/reference/env-and-secrets.md +11 -0
- package/docs/reference/ui-api.md +84 -8
- package/docs/reference/workflow-schema.md +63 -32
- package/package.json +1 -1
|
@@ -138,7 +138,11 @@ export async function withAssetMutationLease(purpose, run) {
|
|
|
138
138
|
return run();
|
|
139
139
|
const context = inherited ?? new Set();
|
|
140
140
|
const execute = async () => {
|
|
141
|
-
const lease = await acquireAssetMutationLease({
|
|
141
|
+
const lease = await acquireAssetMutationLease({
|
|
142
|
+
purpose,
|
|
143
|
+
// #956: say who is in the way instead of waiting silently, as the sync path does.
|
|
144
|
+
onWait: ({ waitedMs }) => warn(`waiting for ${describeAssetMutationHolder(lockPath)} — ${Math.round(waitedMs / 1000)}s`),
|
|
145
|
+
});
|
|
142
146
|
if (!lease)
|
|
143
147
|
throw new Error(`asset mutation lease unavailable for ${purpose}`);
|
|
144
148
|
context.add(lockPath);
|
|
@@ -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),
|
|
90
|
-
return entry ?
|
|
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,
|
|
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.
|
|
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,
|
|
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
|
});
|
package/dist/llm/memory-infer.js
CHANGED
|
@@ -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
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
65
|
-
*
|
|
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() : "";
|
|
@@ -28296,7 +28296,7 @@ function renderRunsDetailMd(runs) {
|
|
|
28296
28296
|
"refl_ok/fail/cd/skip",
|
|
28297
28297
|
"distill_q/llm-fail/judge/validator/cfg/skip",
|
|
28298
28298
|
"cons_proc/promo/merge/del",
|
|
28299
|
-
"mem_cons/written/skip",
|
|
28299
|
+
"mem_cons/written/nochange/skip",
|
|
28300
28300
|
"orphans",
|
|
28301
28301
|
"lint_f/fl",
|
|
28302
28302
|
"result_status"
|
|
@@ -28311,7 +28311,7 @@ function renderRunsDetailMd(runs) {
|
|
|
28311
28311
|
`${r.actions.reflect.ok}/${r.actions.reflect.failed}/${r.actions.reflect.cooldown}/${r.actions.reflect.skipped}`,
|
|
28312
28312
|
`${r.actions.distill.queued}/${r.actions.distill.llmFailed}/${r.actions.distill.judgeRejected}/${r.actions.distill.validatorRejected}/${r.actions.distill.configDisabled}/${r.actions.distill.skipped}`,
|
|
28313
28313
|
`${r.consolidation.processed}/${r.consolidation.promoted}/${r.consolidation.merged}/${r.consolidation.deleted}`,
|
|
28314
|
-
`${r.memoryInference.considered}/${r.memoryInference.written}/${r.memoryInference.skippedNoFacts}`,
|
|
28314
|
+
`${r.memoryInference.considered}/${r.memoryInference.written}/${r.memoryInference.noChange}/${r.memoryInference.skippedNoFacts}`,
|
|
28315
28315
|
String(r.orphansPurged),
|
|
28316
28316
|
`${r.lintFixed}/${r.lintFlagged}`,
|
|
28317
28317
|
r.resultStatus ?? "valid"
|
|
@@ -44342,14 +44342,18 @@ function parseMarkdownToc(content) {
|
|
|
44342
44342
|
const headings = [];
|
|
44343
44343
|
const parsed = parseFrontmatter(content);
|
|
44344
44344
|
const start = parsed.frontmatter ? parsed.bodyStartLine - 1 : 0;
|
|
44345
|
-
let
|
|
44345
|
+
let fence;
|
|
44346
44346
|
for (let i = start;i < lines.length; i++) {
|
|
44347
|
-
|
|
44348
|
-
|
|
44347
|
+
const [, run = "", rest = ""] = lines[i].match(/^ {0,3}(`{3,}|~{3,})(.*)$/) ?? [];
|
|
44348
|
+
if (fence) {
|
|
44349
|
+
if (run[0] === fence.char && run.length >= fence.length && !rest.trim())
|
|
44350
|
+
fence = undefined;
|
|
44349
44351
|
continue;
|
|
44350
44352
|
}
|
|
44351
|
-
if (
|
|
44353
|
+
if (run && !(run[0] === "`" && rest.includes("`"))) {
|
|
44354
|
+
fence = { char: run[0], length: run.length };
|
|
44352
44355
|
continue;
|
|
44356
|
+
}
|
|
44353
44357
|
const match = lines[i].match(/^(#{1,6})\s+(.+)$/);
|
|
44354
44358
|
if (match) {
|
|
44355
44359
|
headings.push({
|
|
@@ -44975,7 +44979,9 @@ function parseSteps(ctx, raw) {
|
|
|
44975
44979
|
return;
|
|
44976
44980
|
}
|
|
44977
44981
|
const label = typeof rawStep.id === "string" && rawStep.id !== "" ? `Step "${rawStep.id}"` : `Step ${index + 1}`;
|
|
44978
|
-
|
|
44982
|
+
const isMapStep = rawStep.map !== undefined;
|
|
44983
|
+
const unitKeyHome = isMapStep ? "map.unit:" : "unit:";
|
|
44984
|
+
checkUnknownKeys(ctx, rawStep, path27, STEP_KEYS, label, (key) => UNIT_KEYS.includes(key) ? ` (this belongs under "${unitKeyHome}")` : "");
|
|
44979
44985
|
let id = "";
|
|
44980
44986
|
if (typeof rawStep.id !== "string" || rawStep.id === "") {
|
|
44981
44987
|
ctx.err([...path27, "id"], `${label} requires a non-empty string "id".`);
|
|
@@ -44990,7 +44996,6 @@ function parseSteps(ctx, raw) {
|
|
|
44990
44996
|
seenIds.set(id, index);
|
|
44991
44997
|
}
|
|
44992
44998
|
}
|
|
44993
|
-
const isMapStep = rawStep.map !== undefined;
|
|
44994
44999
|
if (isMapStep && rawStep.unit !== undefined) {
|
|
44995
45000
|
ctx.err(path27, `${label} is a map step; the per-item dispatch-override bag belongs at "map.unit", not top-level "unit".`);
|
|
44996
45001
|
}
|
|
@@ -45002,6 +45007,9 @@ function parseSteps(ctx, raw) {
|
|
|
45002
45007
|
const step = { id, source: ctx.refAt(path27) };
|
|
45003
45008
|
if (unit)
|
|
45004
45009
|
step.unit = unit;
|
|
45010
|
+
if (output !== undefined && !isMapStep && unit?.output === undefined && unit?.workflow === undefined) {
|
|
45011
|
+
step.unit = { ...unit ?? { source: step.source }, output };
|
|
45012
|
+
}
|
|
45005
45013
|
if (map)
|
|
45006
45014
|
step.map = map;
|
|
45007
45015
|
if (inputs)
|
|
@@ -45448,10 +45456,10 @@ function checkReferenceSyntax(ctx, text, path27, label) {
|
|
|
45448
45456
|
if (!result.ok)
|
|
45449
45457
|
ctx.err(path27, `${label}: ${result.message}`);
|
|
45450
45458
|
}
|
|
45451
|
-
function checkUnknownKeys(ctx, obj, path27, allowed, label) {
|
|
45459
|
+
function checkUnknownKeys(ctx, obj, path27, allowed, label, hint = () => "") {
|
|
45452
45460
|
for (const key of Object.keys(obj)) {
|
|
45453
45461
|
if (!allowed.includes(key)) {
|
|
45454
|
-
ctx.err([...path27, key], `Unknown ${label} key "${key}". Allowed keys: ${allowed.join(", ")}.`);
|
|
45462
|
+
ctx.err([...path27, key], `Unknown ${label} key "${key}"${hint(key)}. Allowed keys: ${allowed.join(", ")}.`);
|
|
45455
45463
|
}
|
|
45456
45464
|
}
|
|
45457
45465
|
}
|
|
@@ -27624,7 +27624,7 @@ function renderRunsDetailMd(runs) {
|
|
|
27624
27624
|
"refl_ok/fail/cd/skip",
|
|
27625
27625
|
"distill_q/llm-fail/judge/validator/cfg/skip",
|
|
27626
27626
|
"cons_proc/promo/merge/del",
|
|
27627
|
-
"mem_cons/written/skip",
|
|
27627
|
+
"mem_cons/written/nochange/skip",
|
|
27628
27628
|
"orphans",
|
|
27629
27629
|
"lint_f/fl",
|
|
27630
27630
|
"result_status"
|
|
@@ -27639,7 +27639,7 @@ function renderRunsDetailMd(runs) {
|
|
|
27639
27639
|
`${r.actions.reflect.ok}/${r.actions.reflect.failed}/${r.actions.reflect.cooldown}/${r.actions.reflect.skipped}`,
|
|
27640
27640
|
`${r.actions.distill.queued}/${r.actions.distill.llmFailed}/${r.actions.distill.judgeRejected}/${r.actions.distill.validatorRejected}/${r.actions.distill.configDisabled}/${r.actions.distill.skipped}`,
|
|
27641
27641
|
`${r.consolidation.processed}/${r.consolidation.promoted}/${r.consolidation.merged}/${r.consolidation.deleted}`,
|
|
27642
|
-
`${r.memoryInference.considered}/${r.memoryInference.written}/${r.memoryInference.skippedNoFacts}`,
|
|
27642
|
+
`${r.memoryInference.considered}/${r.memoryInference.written}/${r.memoryInference.noChange}/${r.memoryInference.skippedNoFacts}`,
|
|
27643
27643
|
String(r.orphansPurged),
|
|
27644
27644
|
`${r.lintFixed}/${r.lintFlagged}`,
|
|
27645
27645
|
r.resultStatus ?? "valid"
|
|
@@ -43670,14 +43670,18 @@ function parseMarkdownToc(content) {
|
|
|
43670
43670
|
const headings = [];
|
|
43671
43671
|
const parsed = parseFrontmatter(content);
|
|
43672
43672
|
const start = parsed.frontmatter ? parsed.bodyStartLine - 1 : 0;
|
|
43673
|
-
let
|
|
43673
|
+
let fence;
|
|
43674
43674
|
for (let i = start;i < lines.length; i++) {
|
|
43675
|
-
|
|
43676
|
-
|
|
43675
|
+
const [, run = "", rest = ""] = lines[i].match(/^ {0,3}(`{3,}|~{3,})(.*)$/) ?? [];
|
|
43676
|
+
if (fence) {
|
|
43677
|
+
if (run[0] === fence.char && run.length >= fence.length && !rest.trim())
|
|
43678
|
+
fence = undefined;
|
|
43677
43679
|
continue;
|
|
43678
43680
|
}
|
|
43679
|
-
if (
|
|
43681
|
+
if (run && !(run[0] === "`" && rest.includes("`"))) {
|
|
43682
|
+
fence = { char: run[0], length: run.length };
|
|
43680
43683
|
continue;
|
|
43684
|
+
}
|
|
43681
43685
|
const match = lines[i].match(/^(#{1,6})\s+(.+)$/);
|
|
43682
43686
|
if (match) {
|
|
43683
43687
|
headings.push({
|
|
@@ -44303,7 +44307,9 @@ function parseSteps(ctx, raw) {
|
|
|
44303
44307
|
return;
|
|
44304
44308
|
}
|
|
44305
44309
|
const label = typeof rawStep.id === "string" && rawStep.id !== "" ? `Step "${rawStep.id}"` : `Step ${index + 1}`;
|
|
44306
|
-
|
|
44310
|
+
const isMapStep = rawStep.map !== undefined;
|
|
44311
|
+
const unitKeyHome = isMapStep ? "map.unit:" : "unit:";
|
|
44312
|
+
checkUnknownKeys(ctx, rawStep, path27, STEP_KEYS, label, (key) => UNIT_KEYS.includes(key) ? ` (this belongs under "${unitKeyHome}")` : "");
|
|
44307
44313
|
let id = "";
|
|
44308
44314
|
if (typeof rawStep.id !== "string" || rawStep.id === "") {
|
|
44309
44315
|
ctx.err([...path27, "id"], `${label} requires a non-empty string "id".`);
|
|
@@ -44318,7 +44324,6 @@ function parseSteps(ctx, raw) {
|
|
|
44318
44324
|
seenIds.set(id, index);
|
|
44319
44325
|
}
|
|
44320
44326
|
}
|
|
44321
|
-
const isMapStep = rawStep.map !== undefined;
|
|
44322
44327
|
if (isMapStep && rawStep.unit !== undefined) {
|
|
44323
44328
|
ctx.err(path27, `${label} is a map step; the per-item dispatch-override bag belongs at "map.unit", not top-level "unit".`);
|
|
44324
44329
|
}
|
|
@@ -44330,6 +44335,9 @@ function parseSteps(ctx, raw) {
|
|
|
44330
44335
|
const step = { id, source: ctx.refAt(path27) };
|
|
44331
44336
|
if (unit)
|
|
44332
44337
|
step.unit = unit;
|
|
44338
|
+
if (output !== undefined && !isMapStep && unit?.output === undefined && unit?.workflow === undefined) {
|
|
44339
|
+
step.unit = { ...unit ?? { source: step.source }, output };
|
|
44340
|
+
}
|
|
44333
44341
|
if (map)
|
|
44334
44342
|
step.map = map;
|
|
44335
44343
|
if (inputs)
|
|
@@ -44776,10 +44784,10 @@ function checkReferenceSyntax(ctx, text, path27, label) {
|
|
|
44776
44784
|
if (!result.ok)
|
|
44777
44785
|
ctx.err(path27, `${label}: ${result.message}`);
|
|
44778
44786
|
}
|
|
44779
|
-
function checkUnknownKeys(ctx, obj, path27, allowed, label) {
|
|
44787
|
+
function checkUnknownKeys(ctx, obj, path27, allowed, label, hint = () => "") {
|
|
44780
44788
|
for (const key of Object.keys(obj)) {
|
|
44781
44789
|
if (!allowed.includes(key)) {
|
|
44782
|
-
ctx.err([...path27, key], `Unknown ${label} key "${key}". Allowed keys: ${allowed.join(", ")}.`);
|
|
44790
|
+
ctx.err([...path27, key], `Unknown ${label} key "${key}"${hint(key)}. Allowed keys: ${allowed.join(", ")}.`);
|
|
44783
44791
|
}
|
|
44784
44792
|
}
|
|
44785
44793
|
}
|
|
@@ -26,15 +26,11 @@ export function materializeFrozenWorkflowEnvironment(descriptors, options = {})
|
|
|
26
26
|
sensitive.add(descriptor.value);
|
|
27
27
|
continue;
|
|
28
28
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (value)
|
|
34
|
-
sensitive.add(value);
|
|
35
|
-
}
|
|
29
|
+
// `pass_env` names reach the exec child through its allowlist
|
|
30
|
+
// (`execAllowlist`, exec-unit.ts), not as bindings. Their values are
|
|
31
|
+
// deliberately not redacted, so they are neither read nor marked sensitive here.
|
|
32
|
+
if (descriptor.kind === "pass-through")
|
|
36
33
|
continue;
|
|
37
|
-
}
|
|
38
34
|
const source = options.readEnvFile ? options.readEnvFile(descriptor) : readEnvFile(descriptor.owner);
|
|
39
35
|
const parsed = dotenv.parse(typeof source === "string" ? source : Buffer.from(source));
|
|
40
36
|
const keys = Object.keys(parsed).sort(compareCodePoints);
|
package/dist/workflows/parser.js
CHANGED
|
@@ -588,7 +588,9 @@ function parseSteps(ctx, raw) {
|
|
|
588
588
|
return;
|
|
589
589
|
}
|
|
590
590
|
const label = typeof rawStep.id === "string" && rawStep.id !== "" ? `Step "${rawStep.id}"` : `Step ${index + 1}`;
|
|
591
|
-
|
|
591
|
+
const isMapStep = rawStep.map !== undefined;
|
|
592
|
+
const unitKeyHome = isMapStep ? "map.unit:" : "unit:";
|
|
593
|
+
checkUnknownKeys(ctx, rawStep, path, STEP_KEYS, label, (key) => UNIT_KEYS.includes(key) ? ` (this belongs under "${unitKeyHome}")` : "");
|
|
592
594
|
let id = "";
|
|
593
595
|
if (typeof rawStep.id !== "string" || rawStep.id === "") {
|
|
594
596
|
ctx.err([...path, "id"], `${label} requires a non-empty string "id".`);
|
|
@@ -608,7 +610,6 @@ function parseSteps(ctx, raw) {
|
|
|
608
610
|
seenIds.set(id, index);
|
|
609
611
|
}
|
|
610
612
|
}
|
|
611
|
-
const isMapStep = rawStep.map !== undefined;
|
|
612
613
|
if (isMapStep && rawStep.unit !== undefined) {
|
|
613
614
|
ctx.err(path, `${label} is a map step; the per-item dispatch-override bag belongs at "map.unit", not top-level "unit".`);
|
|
614
615
|
}
|
|
@@ -620,6 +621,14 @@ function parseSteps(ctx, raw) {
|
|
|
620
621
|
const step = { id, source: ctx.refAt(path) };
|
|
621
622
|
if (unit)
|
|
622
623
|
step.unit = unit;
|
|
624
|
+
// A solo step's `output` is also its unit's contract. Only `unit.output`
|
|
625
|
+
// makes the dispatch ask for, parse and re-request JSON, so without this a
|
|
626
|
+
// step-level schema met a raw text reply and could never pass. A map
|
|
627
|
+
// step's `output` describes the collected/reduced value, and a child
|
|
628
|
+
// workflow unit's result is the child's own, so both keep the step-only meaning.
|
|
629
|
+
if (output !== undefined && !isMapStep && unit?.output === undefined && unit?.workflow === undefined) {
|
|
630
|
+
step.unit = { ...(unit ?? { source: step.source }), output };
|
|
631
|
+
}
|
|
623
632
|
if (map)
|
|
624
633
|
step.map = map;
|
|
625
634
|
if (inputs)
|
|
@@ -1149,10 +1158,10 @@ function checkReferenceSyntax(ctx, text, path, label) {
|
|
|
1149
1158
|
if (!result.ok)
|
|
1150
1159
|
ctx.err(path, `${label}: ${result.message}`);
|
|
1151
1160
|
}
|
|
1152
|
-
function checkUnknownKeys(ctx, obj, path, allowed, label) {
|
|
1161
|
+
function checkUnknownKeys(ctx, obj, path, allowed, label, hint = () => "") {
|
|
1153
1162
|
for (const key of Object.keys(obj)) {
|
|
1154
1163
|
if (!allowed.includes(key)) {
|
|
1155
|
-
ctx.err([...path, key], `Unknown ${label} key "${key}". Allowed keys: ${allowed.join(", ")}.`);
|
|
1164
|
+
ctx.err([...path, key], `Unknown ${label} key "${key}"${hint(key)}. Allowed keys: ${allowed.join(", ")}.`);
|
|
1156
1165
|
}
|
|
1157
1166
|
}
|
|
1158
1167
|
}
|
|
@@ -539,9 +539,9 @@ All paths are relative to the `rules-010` worktree. Citation form is `path:line`
|
|
|
539
539
|
|
|
540
540
|
- **Only the default akm bundle is written** — Read-only caches (git, npm, website) are untouched. The pass takes the source whose `isDefault` flag is true, never `sources[0]` (fixed in this PR, #8), in line with `AGENTS.md` ("Never infer the default/trusted bundle from array position"). With no default bundle it warns `memory inference: skipped — no default bundle among the sources…`, and with a default bundle that is not an `akm` adapter bundle it warns and names the path; either way it writes nothing. The write target is still hardcoded `name: "stash"`. *Why:* inferred children written to caches "would be clobbered by the next sync()"; a skipped pass should say so (the gate's "never silent" rule). (`src/indexer/passes/memory-inference.ts:440-455,660-675`, `autonomy-gate.ts:26`)
|
|
541
541
|
|
|
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
|
|
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
|
-
- **Reply validation** —
|
|
544
|
+
- **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
545
|
|
|
546
546
|
- **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)
|
|
547
547
|
|
|
@@ -551,10 +551,12 @@ All paths are relative to the `rules-010` worktree. Citation form is `path:line`
|
|
|
551
551
|
|
|
552
552
|
- **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
553
|
|
|
554
|
-
- **Caching** — Results are cached in the index DB keyed by the parent's file path, a body hash and variant `memory-inference-v2
|
|
554
|
+
- **Caching** — Results are cached in the index DB keyed by the parent's file path, a body hash and variant `memory-inference-v3` (bumped from v2 when a cached row could become an explicit no-change decision; older rows are never read, a disposable miss). An unchanged body gets a validated cache hit with no LLM call (`cacheHits`). The cached value is a derived memory or `{ "noChange": true }`; an unrecognised value falls through to recompute. `reEnrich` bypasses it (the improve step passes `false`). A corrupt cache entry falls through to recompute. *Why:* skip re-processing unchanged content; cache hits are tracked separately so the yield rate does not drift as the cache warms. (`src/indexer/passes/memory-inference.ts:210-221,304-332`; `src/indexer/db/llm-cache.ts:53-86`)
|
|
555
555
|
|
|
556
556
|
- **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
557
|
|
|
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: it is NOT stamped `inferenceProcessed`, because that flag is checked before the body hash and would hide a later edit. The decision is cached like any result, so an unchanged body makes no further call and does not mutate the parent; an edited body (the cache key is the body hash, the whole input the model sees) or `reEnrich` asks the model again. `noChange` never feeds `skippedNoFacts`, `htmlErrorCount` or `unaccounted`. Anything that is not a clean reply (invalid or incomplete JSON, HTML, timeout, abort, transport error, disabled pass) stays `undefined`, is never cached, and is retried. Without a database the decision is reported but not remembered. The count is `improve` result `memoryInference.noChange` and health `memoryInference.noChange`; 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
|
+
|
|
558
560
|
- **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`)
|
|
559
561
|
|
|
560
562
|
- **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`)
|
|
@@ -275,7 +275,7 @@ 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-
|
|
278
|
+
| `cache_variant` | TEXT NOT NULL | Extractor/cache fingerprint. Memory inference uses a version-tagged variant (`memory-inference-v3`). |
|
|
279
279
|
| `body_hash` | TEXT NOT NULL | SHA-256 hex digest of file body |
|
|
280
280
|
| `result_json` | TEXT NOT NULL | Serialized LLM enrichment result |
|
|
281
281
|
| `updated_at` | INTEGER NOT NULL | Unix ms timestamp |
|