akm-cli 0.9.15-beta.3 → 0.9.15-beta.4
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 +57 -3
- package/dist/commands/health/checks.js +2 -7
- package/dist/commands/improve/improve-cli.js +107 -6
- package/dist/commands/improve/improve.js +47 -3
- package/dist/commands/improve/reflect.js +159 -64
- package/dist/core/errors.js +1 -0
- package/dist/core/maintenance-barrier.js +35 -6
- package/dist/llm/client.js +18 -0
- package/dist/llm/usage-persist.js +7 -1
- package/dist/scripts/akm-migrate-node.js +17 -5
- package/dist/scripts/akm-migrate.js +17 -5
- package/docs/migration/release-notes/0.9.15.md +65 -0
- package/docs/reference/cli.md +33 -1
- package/docs/reference/configuration.md +34 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
|
|
7
|
-
## [0.9.15-beta.
|
|
7
|
+
## [0.9.15-beta.4] - 2026-09-10
|
|
8
8
|
|
|
9
9
|
### Added
|
|
10
10
|
|
|
@@ -250,8 +250,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
250
250
|
`embedding.contextLength` is Ollama's `num_ctx` only now — it used to also
|
|
251
251
|
silently set the per-request token budget (`embedding.maxTokens`), so
|
|
252
252
|
setting it for the server's context window changed request batching too.
|
|
253
|
-
The request budget is `embedding.maxTokens` (default
|
|
254
|
-
carries about
|
|
253
|
+
The request budget is `embedding.maxTokens` (default 6000, see #954
|
|
254
|
+
below), so a request carries about 11 documents alongside the new
|
|
255
|
+
per-document cap by default.
|
|
255
256
|
- **`akm index` reports where its embedding credential came from, before the
|
|
256
257
|
first provider request (#953).** A field report suspected a gateway was
|
|
257
258
|
receiving unauthenticated embedding requests despite `embedding.apiKey`
|
|
@@ -279,6 +280,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
279
280
|
it differs from the running CLI, pointing at `akm task sync` as the
|
|
280
281
|
remedy. `unknown` when not probed, no task is installed, or the recorded
|
|
281
282
|
binary cannot be executed.
|
|
283
|
+
- **`akm improve <ref> --show-prompt` prints the composed reflect prompt for
|
|
284
|
+
one asset and exits (#952).** A beta.3 field round confirmed the #952 prompt
|
|
285
|
+
fix by reading source, but no live `akm improve` completed across three
|
|
286
|
+
attempts, leaving no cheap way to see the prompt in practice. `--show-prompt`
|
|
287
|
+
reuses every read-only step `akm improve`'s live reflect step already
|
|
288
|
+
performs (source resolution, runner selection, feedback/schema-hint/
|
|
289
|
+
related-lesson/rejected-proposal gathering) and stops before the dispatch
|
|
290
|
+
lease reflect would otherwise acquire — no lock, index write, or engine call,
|
|
291
|
+
same as `--dry-run`. Requires a fully-qualified asset ref as the scope; JSON/
|
|
292
|
+
yaml output carries the prompt as a `prompt` field, text output prints it
|
|
293
|
+
directly so the #952 framing (feedback shown as an unverified report, and
|
|
294
|
+
the instruction never to emit the truncation marker or out-of-asset content)
|
|
295
|
+
can be checked by eye.
|
|
282
296
|
|
|
283
297
|
### Changed
|
|
284
298
|
|
|
@@ -610,6 +624,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
610
624
|
precedent (#948) for state.db; the original driver text survives as
|
|
611
625
|
`cause`. `--skip-if-locked` is unaffected — it already skips gracefully
|
|
612
626
|
before ever attempting the write.
|
|
627
|
+
- **A concurrent plain `akm index` (no `--skip-if-locked`) could still exit
|
|
628
|
+
78 instead of 75, a 2026-09-10 field re-test found (#956).** Two `akm
|
|
629
|
+
index` runs colliding on the short internal barrier that registers the
|
|
630
|
+
opt-in rebuild lock (shared with every other akm lock/lease) threw
|
|
631
|
+
`ConfigError("INVALID_CONFIG_FILE")` — a config-error exit that tells a
|
|
632
|
+
supervisor to stop retrying, when this is ordinary contention between two
|
|
633
|
+
legitimate runs. The barrier is meant to be held only milliseconds, so it
|
|
634
|
+
now retries briefly (a bounded, jittered backoff) before giving up, letting
|
|
635
|
+
an ordinary collision succeed instead of erroring at all; if it is still
|
|
636
|
+
busy after that, it raises `TransientError` with a dedicated
|
|
637
|
+
`MAINTENANCE_BARRIER_BUSY` code (exit 75) instead of the config error. The
|
|
638
|
+
rebuild lock itself is unaffected and still never blocks (#872).
|
|
613
639
|
- **The fingerprint-rename canary embeds the exact text the stored vector was
|
|
614
640
|
generated from (#955).** `sampleEmbeddedEntriesForCanary` handed the canary
|
|
615
641
|
the entry's raw `search_text`, while the main embedding pass caps it to
|
|
@@ -621,6 +647,34 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
621
647
|
purge and rebuild on a same-model rename. The canary now caps each sampled
|
|
622
648
|
entry's search text the same way, through the same `capEmbeddingText`
|
|
623
649
|
helper, before requesting its vector.
|
|
650
|
+
- **`akm improve --require-engines` now probes reachability instead of only
|
|
651
|
+
checking config/credentials, and a dead engine can no longer hang a run
|
|
652
|
+
past its timeout or a signal (#957).** Field, beta.3: engines pointed at a
|
|
653
|
+
dead endpoint, then `akm improve --require-engines` ran ~4 minutes with
|
|
654
|
+
zero output, ignoring an external `timeout 30` (SIGTERM) and akm's own
|
|
655
|
+
`--timeout-ms` — the documented exit-78 "required engines unavailable"
|
|
656
|
+
path could never be observed, because `--require-engines` only checked
|
|
657
|
+
that an engine was configured and credentialed, never whether it actually
|
|
658
|
+
answered. It now also runs the same bounded reachability probe `akm
|
|
659
|
+
health`'s `default-llm-engine`/`configured-engines` checks already use
|
|
660
|
+
(one `/models` request per distinct endpoint), before any lock, log, or
|
|
661
|
+
index side effect, and aborts at exit 78 naming the unreachable engine and
|
|
662
|
+
endpoint. Separately, a live run now prints one default-level line if it
|
|
663
|
+
has waited more than a few seconds on its first engine response, so a
|
|
664
|
+
scheduled run's log is never silently empty while an engine is slow or
|
|
665
|
+
dead — `--timeout-ms` and an engine's own configured timeout already
|
|
666
|
+
aborted the in-flight request correctly (confirmed by this investigation,
|
|
667
|
+
not changed), and SIGTERM/SIGINT already ended the process within its
|
|
668
|
+
documented grace period.
|
|
669
|
+
- **`akm improve --show-prompt` now includes `avoidPatterns` when a live
|
|
670
|
+
improve loop has set them (#952).** The preview built its own second copy
|
|
671
|
+
of reflect's prompt-source gathering and `ReflectPromptInput` assembly,
|
|
672
|
+
which had already drifted from the real dispatch path: it never read
|
|
673
|
+
`avoidPatterns` (recent-error context from earlier assets in the same
|
|
674
|
+
run), so the preview was not always the prompt a live iteration would
|
|
675
|
+
actually send. Both the preview and the real dispatch path (`akmReflect`,
|
|
676
|
+
`runReflectRefineIterations`) now gather sources and assemble the prompt
|
|
677
|
+
input through the same two shared helpers, so this cannot drift again.
|
|
624
678
|
|
|
625
679
|
## [0.9.14] - 2026-09-04
|
|
626
680
|
|
|
@@ -12,6 +12,7 @@ import { withEngineFallback } from "../../integrations/agent/engine-fallback.js"
|
|
|
12
12
|
import { isLlmCredentialAvailable, resolveEngine, } from "../../integrations/agent/engine-resolution.js";
|
|
13
13
|
import { executionEngineDefinitionsFromConfig } from "../../integrations/agent/execution-definitions.js";
|
|
14
14
|
import { loadModelMap, mergeModelMapLayers, parseModelMapLayer, readInstalledModelMapText, resolveModelMapAlias, userModelMapPath, } from "../../integrations/agent/model-map.js";
|
|
15
|
+
import { probeEndpointOnce } from "../../llm/client.js";
|
|
15
16
|
import { listKeys } from "../env/env.js";
|
|
16
17
|
import { resolveImprovePlan } from "../improve/improve-strategies.js";
|
|
17
18
|
import { ENGINE_LAST_USED_LOOKBACK_DAYS } from "./engine-usage.js";
|
|
@@ -20,13 +21,7 @@ import { ACTIVE_RUN_WARN_MS, TASK_FAIL_RATE_WARN, } from "./types.js";
|
|
|
20
21
|
function probeConnectionReachable(connection, deps, cache) {
|
|
21
22
|
if (!deps.probeReachable)
|
|
22
23
|
return Promise.resolve(undefined);
|
|
23
|
-
|
|
24
|
-
let pending = cache.get(key);
|
|
25
|
-
if (!pending) {
|
|
26
|
-
pending = deps.probeReachable(connection);
|
|
27
|
-
cache.set(key, pending);
|
|
28
|
-
}
|
|
29
|
-
return pending;
|
|
24
|
+
return probeEndpointOnce(connection, cache, deps.probeReachable);
|
|
30
25
|
}
|
|
31
26
|
function reachabilityEvidence(reach) {
|
|
32
27
|
return reach
|
|
@@ -15,12 +15,16 @@ import { redactSensitiveText } from "../../core/redaction.js";
|
|
|
15
15
|
import { clearLogFile, setLogFile, warn } from "../../core/warn.js";
|
|
16
16
|
import { resolveWriteTarget } from "../../core/write-source.js";
|
|
17
17
|
import { collectEngineCredentialValues } from "../../integrations/agent/engine-resolution.js";
|
|
18
|
-
import {
|
|
18
|
+
import { probeEndpointOnce, probeLlmEndpoint } from "../../llm/client.js";
|
|
19
|
+
import { getOutputMode } from "../../output/context.js";
|
|
20
|
+
import { deliverRendered } from "../../output/html-render.js";
|
|
21
|
+
import { akmImprove, resolveImproveReadSource } from "./improve.js";
|
|
19
22
|
import { runImproveReportQuery } from "./improve-report.js";
|
|
20
23
|
import { buildImproveRunId, recordImproveRunResult, recordTerminatedImproveRun, } from "./improve-result-file.js";
|
|
21
24
|
import { runImproveSession } from "./improve-session.js";
|
|
22
|
-
import { resolveImprovePlan } from "./improve-strategies.js";
|
|
25
|
+
import { resolveImprovePlan, } from "./improve-strategies.js";
|
|
23
26
|
import { formatUsageReportTable } from "./improve-usage-report.js";
|
|
27
|
+
import { renderReflectPromptPreview } from "./reflect.js";
|
|
24
28
|
let akmImproveForRun = akmImprove;
|
|
25
29
|
/** Swap the CLI's improve work implementation in deterministic subprocess tests. */
|
|
26
30
|
export function _setAkmImproveForTests(fake) {
|
|
@@ -93,6 +97,84 @@ function assertRequiredEnginesAvailable(plan) {
|
|
|
93
97
|
const lines = plan.engineUnavailable.map((item) => ` - ${item.process} (${item.configKey}): ${item.reason}`);
|
|
94
98
|
throw new ConfigError(`--require-engines: ${plan.engineUnavailable.length} improve process${plan.engineUnavailable.length === 1 ? "" : "es"} cannot run because ${plan.engineUnavailable.length === 1 ? "its" : "their"} engine is unavailable:\n${lines.join("\n")}`, "LLM_NOT_CONFIGURED");
|
|
95
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Every distinct `kind: "llm"` connection the active strategy's plan would
|
|
102
|
+
* actually dispatch against — the main per-process runners plus triage's own
|
|
103
|
+
* judgment engine, which is resolved separately (#957).
|
|
104
|
+
*/
|
|
105
|
+
function collectRequiredEngineTargets(plan) {
|
|
106
|
+
const targets = [];
|
|
107
|
+
for (const [processName, process] of Object.entries(plan.processes)) {
|
|
108
|
+
if (process.runner) {
|
|
109
|
+
targets.push({ process: processName, engine: process.runner.engine, connection: process.runner.connection });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (plan.triageJudgment?.kind === "llm") {
|
|
113
|
+
targets.push({
|
|
114
|
+
process: "triage.judgment",
|
|
115
|
+
engine: plan.triageJudgment.engine,
|
|
116
|
+
connection: plan.triageJudgment.connection,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return targets;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* `--require-engines` field re-test (#957): the static check above only
|
|
123
|
+
* proves an engine is configured and credentialed — it cannot see a dead
|
|
124
|
+
* endpoint. A field run against an unreachable engine sat silent for
|
|
125
|
+
* minutes instead of hitting the documented exit-78 path. Reuse the SAME
|
|
126
|
+
* bounded reachability probe `akm health`'s `default-llm-engine` /
|
|
127
|
+
* `configured-engines` checks already run (`probeLlmEndpoint`, a single
|
|
128
|
+
* `/models` GET bounded by its own default timeout) once per distinct
|
|
129
|
+
* endpoint (via the shared `probeEndpointOnce` memoization health/checks.ts
|
|
130
|
+
* also uses), so a dead engine is caught here instead of during dispatch.
|
|
131
|
+
*/
|
|
132
|
+
async function assertRequiredEnginesReachable(plan, probeReachable = probeLlmEndpoint) {
|
|
133
|
+
const targets = collectRequiredEngineTargets(plan);
|
|
134
|
+
if (targets.length === 0)
|
|
135
|
+
return;
|
|
136
|
+
const probesByEndpoint = new Map();
|
|
137
|
+
const probed = await Promise.all(targets.map(async (target) => ({
|
|
138
|
+
...target,
|
|
139
|
+
reach: await probeEndpointOnce(target.connection, probesByEndpoint, probeReachable),
|
|
140
|
+
})));
|
|
141
|
+
const unreachable = probed.filter((item) => !item.reach.reachable);
|
|
142
|
+
if (unreachable.length === 0)
|
|
143
|
+
return;
|
|
144
|
+
const lines = unreachable.map((item) => ` - ${item.process} (engine "${item.engine}", ${item.connection.endpoint}): ${item.reach.error ?? "did not respond"}`);
|
|
145
|
+
throw new ConfigError(`--require-engines: ${unreachable.length} improve process${unreachable.length === 1 ? "" : "es"} cannot run because ${unreachable.length === 1 ? "its" : "their"} engine endpoint is not reachable:\n${lines.join("\n")}`, "LLM_NOT_CONFIGURED");
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* `--show-prompt` (#952): render the composed reflect prompt for one asset ref
|
|
149
|
+
* and exit, before any lock, log, index write, or engine dispatch — the field
|
|
150
|
+
* had no cheap way to confirm the #952 prompt fix (unverified-feedback framing,
|
|
151
|
+
* no-truncation-marker instruction) without running a full improve cycle.
|
|
152
|
+
* Reuses `renderReflectPromptPreview` (reflect.ts), which stops before the
|
|
153
|
+
* dispatch lease reflect would otherwise acquire, so this never calls an engine.
|
|
154
|
+
*/
|
|
155
|
+
async function runShowPromptCli(refArg, parsedRef, taskArg, targetArg, resolvedPlan) {
|
|
156
|
+
const readSource = resolveImproveReadSource(resolvedPlan.config, parsedRef, targetArg);
|
|
157
|
+
const preview = await renderReflectPromptPreview({
|
|
158
|
+
ref: refArg,
|
|
159
|
+
...(taskArg ? { task: taskArg } : {}),
|
|
160
|
+
improveProfile: resolvedPlan.strategy.config,
|
|
161
|
+
config: resolvedPlan.config,
|
|
162
|
+
stashDir: readSource.source.path,
|
|
163
|
+
});
|
|
164
|
+
const outputMode = getOutputMode();
|
|
165
|
+
if (outputMode.format === "text") {
|
|
166
|
+
deliverRendered(preview.prompt, outputMode.outputPath);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
output("improve", {
|
|
170
|
+
schemaVersion: 2,
|
|
171
|
+
ok: true,
|
|
172
|
+
ref: preview.ref,
|
|
173
|
+
engine: preview.engine,
|
|
174
|
+
engineKind: preview.engineKind,
|
|
175
|
+
prompt: preview.prompt,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
96
178
|
/**
|
|
97
179
|
* `akm improve report` (#944): a scope value that dispatches to the per-run
|
|
98
180
|
* LLM usage/routing report instead of a real improve run — "report" is not,
|
|
@@ -172,7 +254,12 @@ export const improveCommand = defineCommand({
|
|
|
172
254
|
},
|
|
173
255
|
"require-engines": {
|
|
174
256
|
type: "boolean",
|
|
175
|
-
description: "Abort before any indexing, lock, or log side effect (exit 78) if the active strategy would enable a process whose engine or credential cannot be resolved in this process's environment. Without this flag, improve degrades gracefully instead: it skips the affected processes and reports them in the result's skippedProcesses. Recommended alongside --skip-if-locked for scheduled runs.",
|
|
257
|
+
description: "Abort before any indexing, lock, or log side effect (exit 78) if the active strategy would enable a process whose engine or credential cannot be resolved in this process's environment, OR whose endpoint fails a bounded reachability probe (the same probe akm health runs). Without this flag, improve degrades gracefully instead: it skips the affected processes and reports them in the result's skippedProcesses. Recommended alongside --skip-if-locked for scheduled runs.",
|
|
258
|
+
default: false,
|
|
259
|
+
},
|
|
260
|
+
"show-prompt": {
|
|
261
|
+
type: "boolean",
|
|
262
|
+
description: "Print the composed reflect prompt for one asset ref and exit — no lock, index write, or engine dispatch (#952). Requires a fully-qualified asset ref as the scope positional (e.g. `akm improve lessons/my-lesson --show-prompt`). JSON/yaml format carries the prompt as a `prompt` field; text format prints it directly.",
|
|
176
263
|
default: false,
|
|
177
264
|
},
|
|
178
265
|
run: {
|
|
@@ -216,8 +303,10 @@ export const improveCommand = defineCommand({
|
|
|
216
303
|
const targetArg = getStringArg(args, "bundle");
|
|
217
304
|
const taskArg = getStringArg(args, "task");
|
|
218
305
|
// #947 — `--plan` is a zero-logic discoverability alias for `--dry-run`;
|
|
219
|
-
// it must never fork the computation, only set the same flag.
|
|
220
|
-
|
|
306
|
+
// it must never fork the computation, only set the same flag. #952 —
|
|
307
|
+
// `--show-prompt` implies the same read-only posture (it never reaches
|
|
308
|
+
// akmImprove at all, but keeps writeTarget/resolvedPlan unset the same way).
|
|
309
|
+
const dryRun = args["dry-run"] || args.plan || args["show-prompt"];
|
|
221
310
|
const limitRaw = parsePositiveIntFlag(args.limit ?? undefined);
|
|
222
311
|
const timeoutMs = parsePositiveIntFlag(args["timeout-ms"], "--timeout-ms");
|
|
223
312
|
const requireFeedbackSignal = args["require-feedback-signal"];
|
|
@@ -239,8 +328,20 @@ export const improveCommand = defineCommand({
|
|
|
239
328
|
// is disabled purely by an unreachable credential; a live run keeps
|
|
240
329
|
// throwing (allowAllDisabled unset).
|
|
241
330
|
const resolvedPlan = resolveImprovePlan(strategyArg, effectiveConfig, { allowAllDisabled: Boolean(dryRun) });
|
|
242
|
-
|
|
331
|
+
// #952 — same interception point as the `report` scope above: before any
|
|
332
|
+
// lock, log, or index side effect. Requires a single fully-qualified
|
|
333
|
+
// asset ref (not a type or whole-bundle scope).
|
|
334
|
+
if (args["show-prompt"]) {
|
|
335
|
+
if (!scopeArg || !scopeRef) {
|
|
336
|
+
throw new UsageError("`--show-prompt` requires a fully-qualified asset ref as the scope (e.g. `akm improve lessons/my-lesson --show-prompt`).", "INVALID_FLAG_VALUE");
|
|
337
|
+
}
|
|
338
|
+
await runShowPromptCli(scopeArg, scopeRef, taskArg, targetArg, resolvedPlan);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (args["require-engines"]) {
|
|
243
342
|
assertRequiredEnginesAvailable(resolvedPlan);
|
|
343
|
+
await assertRequiredEnginesReachable(resolvedPlan);
|
|
344
|
+
}
|
|
244
345
|
const selectedStrategyName = resolvedPlan.strategy.name;
|
|
245
346
|
const sensitiveValues = collectEngineCredentialValues(effectiveConfig);
|
|
246
347
|
// Only set the keys the user actually passed (citty leaves the flag
|
|
@@ -65,6 +65,19 @@ export function renderSyncCommitMessage(template, result, nowMs) {
|
|
|
65
65
|
};
|
|
66
66
|
return template.replace(/\{(\w+)\}/g, (match, key) => tokens[key] ?? match);
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* How long the improve loop waits for its FIRST engine response (success or
|
|
70
|
+
* error — any terminal record proves the run is not silent) before printing
|
|
71
|
+
* one default-level line. The timer is armed once the triage/index prepass
|
|
72
|
+
* finishes and the loop is about to start dispatching engine requests — not
|
|
73
|
+
* at run start — so it measures engine latency, not prepass time. Field
|
|
74
|
+
* re-test (#957): an engine pointed at a dead endpoint produced zero output
|
|
75
|
+
* for minutes, so a genuine hang looked identical to a normal-but-slow run.
|
|
76
|
+
* A few seconds is short enough that an operator watching a scheduled run's
|
|
77
|
+
* live log sees something promptly, long enough that an ordinary fast
|
|
78
|
+
* response never prints it.
|
|
79
|
+
*/
|
|
80
|
+
export const FIRST_ENGINE_RESPONSE_HEARTBEAT_MS = 5_000;
|
|
68
81
|
export function armBudgetWatchdog(budgetMs, controller, deps) {
|
|
69
82
|
const setTimeoutFn = deps?.setTimeoutFn ?? setTimeout;
|
|
70
83
|
const clearTimeoutFn = deps?.clearTimeoutFn ?? clearTimeout;
|
|
@@ -119,6 +132,12 @@ export async function akmImprove(options = {}) {
|
|
|
119
132
|
options = setup.options;
|
|
120
133
|
const { budgetMs, budgetAbortController, scope, selectedStrategy, syncRepoDir, resolvedStateDbPath, resolvedLockPath, } = setup;
|
|
121
134
|
let clearBudgetTimer = () => { };
|
|
135
|
+
let clearFirstResponseHeartbeat = () => { };
|
|
136
|
+
// #957: set by the usage sink's onRecord callback the moment any engine
|
|
137
|
+
// call terminates (success or error), including one issued by the prepass
|
|
138
|
+
// itself — makes arming the heartbeat below a no-op when the run is
|
|
139
|
+
// already known not to be silent.
|
|
140
|
+
let firstEngineResponseSeen = false;
|
|
122
141
|
let initialGitPaths = new Set();
|
|
123
142
|
const runJournal = createRunWriteJournal();
|
|
124
143
|
const preEnsureCleanupWarnings = [];
|
|
@@ -175,7 +194,10 @@ export async function akmImprove(options = {}) {
|
|
|
175
194
|
return buildLockSkippedResult(selectedStrategy.name, scope, options.runId);
|
|
176
195
|
}
|
|
177
196
|
improveLockOwnership = acquisition.ownership;
|
|
178
|
-
disposeLlmUsageSink = installLlmUsagePersistence(() => eventsCtx)
|
|
197
|
+
disposeLlmUsageSink = installLlmUsagePersistence(() => eventsCtx, () => {
|
|
198
|
+
firstEngineResponseSeen = true;
|
|
199
|
+
clearFirstResponseHeartbeat();
|
|
200
|
+
});
|
|
179
201
|
exitBackstop = releaseRunLock;
|
|
180
202
|
process.on("exit", exitBackstop);
|
|
181
203
|
initialGitPaths =
|
|
@@ -215,6 +237,7 @@ export async function akmImprove(options = {}) {
|
|
|
215
237
|
// If the live prepass fails, emit its summary and clear the owning sink
|
|
216
238
|
// before any run teardown. The disposer is idempotent with the main finalizer.
|
|
217
239
|
disposeLlmUsageSink();
|
|
240
|
+
clearFirstResponseHeartbeat();
|
|
218
241
|
clearBudgetTimer();
|
|
219
242
|
if (exitBackstop) {
|
|
220
243
|
process.removeListener("exit", exitBackstop);
|
|
@@ -233,6 +256,20 @@ export async function akmImprove(options = {}) {
|
|
|
233
256
|
// are all in hand. See buildImproveRunContext for exactly which
|
|
234
257
|
// already-resolved values back each field.
|
|
235
258
|
const ctx = buildImproveRunContext(setup, eventsCtx);
|
|
259
|
+
// #957: arm the heartbeat here, immediately before the improve loop
|
|
260
|
+
// starts dispatching engine requests — not at run start, where its timer
|
|
261
|
+
// would measure the triage/index prepass instead of engine latency. A
|
|
262
|
+
// no-op when the prepass already produced a terminal LLM record (the
|
|
263
|
+
// onRecord callback above already saw it). Cleared the moment any call
|
|
264
|
+
// terminates (success or error) — never rearmed, so this prints at most
|
|
265
|
+
// once per run.
|
|
266
|
+
if (!firstEngineResponseSeen) {
|
|
267
|
+
const firstResponseTimer = setTimeout(() => {
|
|
268
|
+
warn("[improve] Still waiting for the first engine response...");
|
|
269
|
+
}, FIRST_ENGINE_RESPONSE_HEARTBEAT_MS);
|
|
270
|
+
firstResponseTimer.unref?.();
|
|
271
|
+
clearFirstResponseHeartbeat = () => clearTimeout(firstResponseTimer);
|
|
272
|
+
}
|
|
236
273
|
const seq = await runImproveStageSequence({
|
|
237
274
|
run: setup,
|
|
238
275
|
strategyFilteredRefs,
|
|
@@ -292,6 +329,8 @@ export async function akmImprove(options = {}) {
|
|
|
292
329
|
// #576: clear the per-run LLM usage sink BEFORE closing `eventsDb` below, so
|
|
293
330
|
// no late sink invocation can write through a closed handle.
|
|
294
331
|
disposeLlmUsageSink();
|
|
332
|
+
// #957: never leave the first-response heartbeat timer pending past the run.
|
|
333
|
+
clearFirstResponseHeartbeat();
|
|
295
334
|
// O-1 (#364): Clear the budget abort timer so it does not keep the event
|
|
296
335
|
// loop alive after the run completes.
|
|
297
336
|
clearBudgetTimer();
|
|
@@ -345,8 +384,13 @@ function describeRunWrittenPaths(setup, writtenPaths) {
|
|
|
345
384
|
}
|
|
346
385
|
return [...described].sort();
|
|
347
386
|
}
|
|
348
|
-
/**
|
|
349
|
-
|
|
387
|
+
/**
|
|
388
|
+
* Resolve a dry-run inspection source without adapting it into a write target.
|
|
389
|
+
* Exported so `improve-cli.ts`'s `--show-prompt` (#952) can resolve the same
|
|
390
|
+
* read-only bundle a plain `--dry-run` would, without duplicating this
|
|
391
|
+
* selector/target/fallback precedence.
|
|
392
|
+
*/
|
|
393
|
+
export function resolveImproveReadSource(config, scopedRef, explicitTarget, fallbackStashDir) {
|
|
350
394
|
if (scopedRef?.origin && explicitTarget && scopedRef.origin !== explicitTarget) {
|
|
351
395
|
throw new UsageError(`Qualified ref bundle "${scopedRef.origin}" conflicts with --target "${explicitTarget}".`, "INVALID_FLAG_VALUE", `Drop --target or use --target ${scopedRef.origin}.`);
|
|
352
396
|
}
|
|
@@ -29,7 +29,7 @@ import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
|
29
29
|
import { conceptIdFromTypeName, parseRefInput } from "../../core/asset/resolve-ref.js";
|
|
30
30
|
import { DESCRIPTION_MAX_CHARS, requiresDescription } from "../../core/authoring-rules.js";
|
|
31
31
|
import { loadConfig } from "../../core/config/config.js";
|
|
32
|
-
import { ConfigError } from "../../core/errors.js";
|
|
32
|
+
import { ConfigError, UsageError } from "../../core/errors.js";
|
|
33
33
|
import { appendEvent, readEvents } from "../../core/events.js";
|
|
34
34
|
import { lintLessonContent } from "../../core/lesson-lint.js";
|
|
35
35
|
import { parseEmbeddedJsonResponse } from "../../core/parse.js";
|
|
@@ -1371,6 +1371,92 @@ async function resolveReflectSource(options, stash, emitReflectFailed) {
|
|
|
1371
1371
|
}
|
|
1372
1372
|
return { assetContent, parsedRef };
|
|
1373
1373
|
}
|
|
1374
|
+
/**
|
|
1375
|
+
* #952 — the flat REFLECT_CONTENT_CAP (12 000 chars) exists only to avoid
|
|
1376
|
+
* E2BIG when the prompt travels through CLI argv (agent/SDK runners). The
|
|
1377
|
+
* direct-LLM HTTP path never touches argv, so it can use the resolved
|
|
1378
|
+
* engine's own context window instead. The reserve for "the rest of the
|
|
1379
|
+
* prompt" is measured directly (not guessed): build the same prompt with
|
|
1380
|
+
* the content cap forced to zero and use its length as the overhead, so
|
|
1381
|
+
* feedback/standards/schema-hints/prior-draft size is accounted for
|
|
1382
|
+
* exactly, per this call. A reflect rewrite returns a body roughly the
|
|
1383
|
+
* size of the input, so the budget only spends HALF of the usable window
|
|
1384
|
+
* on input content and reserves the other half for the model's own
|
|
1385
|
+
* output — otherwise a full-context request leaves no room for a
|
|
1386
|
+
* response. Never drops below the flat floor.
|
|
1387
|
+
*
|
|
1388
|
+
* Shared by the real dispatch path ({@link runReflectRefineIterations}) and
|
|
1389
|
+
* `renderReflectPromptPreview`'s `--show-prompt` preview, so the preview
|
|
1390
|
+
* renders the exact prompt reflect would actually send for LLM runners
|
|
1391
|
+
* instead of always the flat-cap prompt.
|
|
1392
|
+
*/
|
|
1393
|
+
function computeReflectContentBudgetChars(promptInput, runnerSpec) {
|
|
1394
|
+
return runnerIsLlm(runnerSpec) && promptInput.assetContent?.trim()
|
|
1395
|
+
? Math.max(REFLECT_CONTENT_CAP, Math.floor(((runnerSpec.connection.contextLength ?? DEFAULT_CONTEXT_LENGTH_TOKENS) * CHARS_PER_TOKEN -
|
|
1396
|
+
buildReflectPrompt({ ...promptInput, contentBudgetChars: 0 }).prompt.length) /
|
|
1397
|
+
2))
|
|
1398
|
+
: undefined;
|
|
1399
|
+
}
|
|
1400
|
+
/**
|
|
1401
|
+
* #952 — gather every read-only prompt-input source {@link buildReflectPromptInput}
|
|
1402
|
+
* folds into a `ReflectPromptInput`: recent feedback, schema/lint hints, related
|
|
1403
|
+
* lessons, previously-rejected proposals, and stash standards context.
|
|
1404
|
+
*
|
|
1405
|
+
* Shared by the real dispatch path (`akmReflect`'s step 4, via
|
|
1406
|
+
* {@link runReflectRefineIterations}) and `renderReflectPromptPreview`'s
|
|
1407
|
+
* `--show-prompt` preview, so both gather from exactly one definition instead
|
|
1408
|
+
* of two copies that can drift out of agreement.
|
|
1409
|
+
*/
|
|
1410
|
+
async function gatherReflectPromptSources(options, stash, parsedRef, assetContent, assetCtx) {
|
|
1411
|
+
const feedback = readRecentFeedback(options.ref ? (options.itemRef ?? durableImproveRef(options.ref)) : undefined, options.eventsCtx);
|
|
1412
|
+
const schemaHints = buildSchemaHints(parsedRef?.type ?? "", assetContent);
|
|
1413
|
+
const relatedLessons = options.ref && parsedRef ? await readRelatedLessons(assetCtx, stash, options.ref, parsedRef, options.itemRef) : [];
|
|
1414
|
+
// Reflexion-style verbal-RL: inject rejected proposals so the agent avoids
|
|
1415
|
+
// reproducing proposals that have already been reviewed and refused.
|
|
1416
|
+
const rejectedProposals = readRejectedProposals(stash, options.ref, options.ctx);
|
|
1417
|
+
// Standards "rulebook" for this target — stash convention/meta facts; empty
|
|
1418
|
+
// when none fire.
|
|
1419
|
+
const standardsContext = resolveStandardsContext(options.ref, stash);
|
|
1420
|
+
return { feedback, schemaHints, relatedLessons, rejectedProposals, standardsContext };
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* #952 — assemble the `ReflectPromptInput` object literal reflect actually
|
|
1424
|
+
* sends, from gathered sources plus the per-call values (draft path, prior
|
|
1425
|
+
* draft). Shared by the real dispatch path ({@link runReflectRefineIterations})
|
|
1426
|
+
* and `renderReflectPromptPreview`'s `--show-prompt` preview — including
|
|
1427
|
+
* `avoidPatterns`, which the preview previously omitted even though a live
|
|
1428
|
+
* improve loop passes it (recent-error context, O-5 / #378).
|
|
1429
|
+
*/
|
|
1430
|
+
function buildReflectPromptInput(args) {
|
|
1431
|
+
const { options, parsedRef, assetContent, sources, runnerSpec, draftFilePath, priorDraft } = args;
|
|
1432
|
+
const { feedback, schemaHints, relatedLessons, rejectedProposals, standardsContext } = sources;
|
|
1433
|
+
const outputMode = runnerIsLlm(runnerSpec)
|
|
1434
|
+
? wantsJsonSchemaOutput(runnerSpec.connection)
|
|
1435
|
+
? "json_schema"
|
|
1436
|
+
: "framed_markdown"
|
|
1437
|
+
: undefined;
|
|
1438
|
+
return {
|
|
1439
|
+
...(options.ref ? { ref: options.ref } : {}),
|
|
1440
|
+
...(parsedRef?.type ? { type: parsedRef.type } : {}),
|
|
1441
|
+
...(parsedRef?.name ? { name: parsedRef.name } : {}),
|
|
1442
|
+
...(assetContent !== undefined ? { assetContent } : {}),
|
|
1443
|
+
...(feedback.length > 0 ? { feedback } : {}),
|
|
1444
|
+
...(schemaHints.length > 0 ? { schemaHints } : {}),
|
|
1445
|
+
...(relatedLessons.length > 0 ? { relatedLessons } : {}),
|
|
1446
|
+
...(options.task ? { task: options.task } : {}),
|
|
1447
|
+
...(standardsContext.trim() ? { standardsContext } : {}),
|
|
1448
|
+
...(options.avoidPatterns && options.avoidPatterns.length > 0 ? { avoidPatterns: options.avoidPatterns } : {}),
|
|
1449
|
+
...(rejectedProposals.length > 0 ? { rejectedProposals } : {}),
|
|
1450
|
+
// R-1: inject prior draft as self-critique target on iterations > 0
|
|
1451
|
+
...(priorDraft !== undefined ? { priorDraft } : {}),
|
|
1452
|
+
// Issue A (#reflect-pipeline file-write contract): when the runner can
|
|
1453
|
+
// touch the filesystem, instruct the agent to write the proposal body
|
|
1454
|
+
// to a tmp file instead of inlining it in JSON. Avoids parse failures
|
|
1455
|
+
// on long bodies (e.g. knowledge/systems/KOKORO_USAGE_GUIDE 8.4KB).
|
|
1456
|
+
...(draftFilePath ? { draftFilePath } : {}),
|
|
1457
|
+
...(outputMode ? { outputMode } : {}),
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1374
1460
|
/**
|
|
1375
1461
|
* Run the agent with the optional Self-Refine loop (R-1 / #372): up to
|
|
1376
1462
|
* `maxRefineIters` invocations, each injecting the prior draft as self-critique
|
|
@@ -1379,17 +1465,12 @@ async function resolveReflectSource(options, stash, emitReflectFailed) {
|
|
|
1379
1465
|
* result + last draft path. Extracted verbatim from `akmReflect`.
|
|
1380
1466
|
*/
|
|
1381
1467
|
async function runReflectRefineIterations(args) {
|
|
1382
|
-
const { options, parsedRef, assetContent,
|
|
1468
|
+
const { options, parsedRef, assetContent, sources, runnerSpec, lease, agentEnv, draftPathsToCleanup, onNotices } = args;
|
|
1383
1469
|
const maxRefineIters = Math.max(1, options.maxRefineIters ?? 1);
|
|
1384
1470
|
// Determine whether this dispatch can honour the file-write contract.
|
|
1385
1471
|
// Agent CLI + OpenCode SDK runners both have filesystem access; the direct
|
|
1386
1472
|
// LLM HTTP runner does NOT.
|
|
1387
1473
|
const canRunnerWriteFile = runnerSupportsFileWrite(runnerSpec);
|
|
1388
|
-
const outputMode = runnerIsLlm(runnerSpec)
|
|
1389
|
-
? wantsJsonSchemaOutput(runnerSpec.connection)
|
|
1390
|
-
? "json_schema"
|
|
1391
|
-
: "framed_markdown"
|
|
1392
|
-
: undefined;
|
|
1393
1474
|
// Initialized to a sentinel; always overwritten in the first loop iteration
|
|
1394
1475
|
// (maxRefineIters is clamped to >= 1 above).
|
|
1395
1476
|
let result = {};
|
|
@@ -1404,44 +1485,16 @@ async function runReflectRefineIterations(args) {
|
|
|
1404
1485
|
draftPathsToCleanup.push(iterDraftPath);
|
|
1405
1486
|
lastDraftPath = iterDraftPath;
|
|
1406
1487
|
}
|
|
1407
|
-
const promptInput = {
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
...(options.avoidPatterns && options.avoidPatterns.length > 0 ? { avoidPatterns: options.avoidPatterns } : {}),
|
|
1418
|
-
...(rejectedProposals.length > 0 ? { rejectedProposals } : {}),
|
|
1419
|
-
// R-1: inject prior draft as self-critique target on iterations > 0
|
|
1420
|
-
...(priorDraft !== undefined ? { priorDraft } : {}),
|
|
1421
|
-
// Issue A (#reflect-pipeline file-write contract): when the runner can
|
|
1422
|
-
// touch the filesystem, instruct the agent to write the proposal body
|
|
1423
|
-
// to a tmp file instead of inlining it in JSON. Avoids parse failures
|
|
1424
|
-
// on long bodies (e.g. knowledge/systems/KOKORO_USAGE_GUIDE 8.4KB).
|
|
1425
|
-
...(iterDraftPath ? { draftFilePath: iterDraftPath } : {}),
|
|
1426
|
-
...(outputMode ? { outputMode } : {}),
|
|
1427
|
-
};
|
|
1428
|
-
// #952 — the flat REFLECT_CONTENT_CAP (12 000 chars) exists only to avoid
|
|
1429
|
-
// E2BIG when the prompt travels through CLI argv (agent/SDK runners). The
|
|
1430
|
-
// direct-LLM HTTP path never touches argv, so it can use the resolved
|
|
1431
|
-
// engine's own context window instead. The reserve for "the rest of the
|
|
1432
|
-
// prompt" is measured directly (not guessed): build the same prompt with
|
|
1433
|
-
// the content cap forced to zero and use its length as the overhead, so
|
|
1434
|
-
// feedback/standards/schema-hints/prior-draft size is accounted for
|
|
1435
|
-
// exactly, per this call. A reflect rewrite returns a body roughly the
|
|
1436
|
-
// size of the input, so the budget only spends HALF of the usable window
|
|
1437
|
-
// on input content and reserves the other half for the model's own
|
|
1438
|
-
// output — otherwise a full-context request leaves no room for a
|
|
1439
|
-
// response. Never drops below the flat floor.
|
|
1440
|
-
const contentBudgetChars = runnerIsLlm(runnerSpec) && assetContent?.trim()
|
|
1441
|
-
? Math.max(REFLECT_CONTENT_CAP, Math.floor(((runnerSpec.connection.contextLength ?? DEFAULT_CONTEXT_LENGTH_TOKENS) * CHARS_PER_TOKEN -
|
|
1442
|
-
buildReflectPrompt({ ...promptInput, contentBudgetChars: 0 }).prompt.length) /
|
|
1443
|
-
2))
|
|
1444
|
-
: undefined;
|
|
1488
|
+
const promptInput = buildReflectPromptInput({
|
|
1489
|
+
options,
|
|
1490
|
+
parsedRef,
|
|
1491
|
+
assetContent,
|
|
1492
|
+
sources,
|
|
1493
|
+
runnerSpec,
|
|
1494
|
+
draftFilePath: iterDraftPath,
|
|
1495
|
+
priorDraft,
|
|
1496
|
+
});
|
|
1497
|
+
const contentBudgetChars = computeReflectContentBudgetChars(promptInput, runnerSpec);
|
|
1445
1498
|
const { prompt } = buildReflectPrompt({
|
|
1446
1499
|
...promptInput,
|
|
1447
1500
|
...(contentBudgetChars !== undefined ? { contentBudgetChars } : {}),
|
|
@@ -1459,10 +1512,10 @@ async function runReflectRefineIterations(args) {
|
|
|
1459
1512
|
...(options.signal ? { signal: options.signal } : {}),
|
|
1460
1513
|
priorDraft,
|
|
1461
1514
|
iteration: iter,
|
|
1462
|
-
...(outputMode === "json_schema"
|
|
1515
|
+
...(promptInput.outputMode === "json_schema"
|
|
1463
1516
|
? { responseSchema: options.ref ? REFLECT_JSON_SCHEMA : REFLECT_UNSCOPED_JSON_SCHEMA }
|
|
1464
1517
|
: {}),
|
|
1465
|
-
outputMode: outputMode ?? "framed_markdown",
|
|
1518
|
+
outputMode: promptInput.outputMode ?? "framed_markdown",
|
|
1466
1519
|
...(options.ref ? { targetRef: options.ref } : {}),
|
|
1467
1520
|
allowRepair: repairAttempts === 0,
|
|
1468
1521
|
...(options.chat ? { chat: options.chat } : {}),
|
|
@@ -1650,6 +1703,62 @@ function validateReflectPayloadRef(args) {
|
|
|
1650
1703
|
return undefined;
|
|
1651
1704
|
}
|
|
1652
1705
|
}
|
|
1706
|
+
/**
|
|
1707
|
+
* #952 — render the composed reflect prompt for exactly one asset with no
|
|
1708
|
+
* engine dispatch. Reuses every read-only step `akmReflect` performs before
|
|
1709
|
+
* {@link buildReflectPrompt} (source resolution, runner resolution, feedback /
|
|
1710
|
+
* schema-hint / related-lesson / rejected-proposal gathering) and stops right
|
|
1711
|
+
* there: no dispatch lease is acquired, no request is sent, and — because the
|
|
1712
|
+
* `emitReflectFailed` callback passed to {@link resolveReflectSource} here is
|
|
1713
|
+
* a no-op — no `reflect_invoked`/`reflect_completed` event is appended either.
|
|
1714
|
+
*
|
|
1715
|
+
* `akm improve <ref> --show-prompt` (`improve-cli.ts`) is the CLI surface: a
|
|
1716
|
+
* field operator uses it to see the exact prompt reflect would send, in
|
|
1717
|
+
* seconds, without running a full improve cycle or needing a reachable
|
|
1718
|
+
* engine.
|
|
1719
|
+
*/
|
|
1720
|
+
export async function renderReflectPromptPreview(options) {
|
|
1721
|
+
if (!options.ref) {
|
|
1722
|
+
throw new UsageError("renderReflectPromptPreview requires options.ref.", "INVALID_FLAG_VALUE");
|
|
1723
|
+
}
|
|
1724
|
+
const ref = options.ref;
|
|
1725
|
+
const stash = resolveRunStashDir(options.stashDir);
|
|
1726
|
+
const sourceResolved = await resolveReflectSource(options, stash, () => {
|
|
1727
|
+
// No event emitted: this is a read-only preview, not a real invocation.
|
|
1728
|
+
});
|
|
1729
|
+
if ("failure" in sourceResolved) {
|
|
1730
|
+
const { failure } = sourceResolved;
|
|
1731
|
+
throw new UsageError((!failure.ok && failure.error) || `Reflect cannot preview ref "${ref}".`, "INVALID_FLAG_VALUE");
|
|
1732
|
+
}
|
|
1733
|
+
const { assetContent, parsedRef } = sourceResolved;
|
|
1734
|
+
const { runnerSpec, engineName } = resolveReflectRunner(options);
|
|
1735
|
+
const ctx = buildReflectRunContext({ options, stash, config: options.config ?? loadConfig(), runnerSpec });
|
|
1736
|
+
const assetCtx = ctx.withFreshAssetMemo();
|
|
1737
|
+
const sources = await gatherReflectPromptSources(options, stash, parsedRef, assetContent, assetCtx);
|
|
1738
|
+
const canRunnerWriteFile = runnerSupportsFileWrite(runnerSpec);
|
|
1739
|
+
// Same tmp-path synthesis a real dispatch would use (Issue A) — never
|
|
1740
|
+
// written to, since this preview never runs the agent.
|
|
1741
|
+
const draftFilePath = canRunnerWriteFile ? synthesizeReflectDraftPath(ref) : undefined;
|
|
1742
|
+
const previewPromptInput = buildReflectPromptInput({
|
|
1743
|
+
options,
|
|
1744
|
+
parsedRef,
|
|
1745
|
+
assetContent,
|
|
1746
|
+
sources,
|
|
1747
|
+
runnerSpec,
|
|
1748
|
+
draftFilePath,
|
|
1749
|
+
priorDraft: undefined,
|
|
1750
|
+
});
|
|
1751
|
+
// #952 — mirror the real dispatch path's context-aware content budget (see
|
|
1752
|
+
// computeReflectContentBudgetChars) so the preview shows the exact prompt
|
|
1753
|
+
// reflect would send: an LLM engine with a large context window gets the
|
|
1754
|
+
// full asset with no truncation marker, not the flat 12 000-char cap.
|
|
1755
|
+
const contentBudgetChars = computeReflectContentBudgetChars(previewPromptInput, runnerSpec);
|
|
1756
|
+
const { prompt } = buildReflectPrompt({
|
|
1757
|
+
...previewPromptInput,
|
|
1758
|
+
...(contentBudgetChars !== undefined ? { contentBudgetChars } : {}),
|
|
1759
|
+
});
|
|
1760
|
+
return { ref, prompt, engine: engineName, engineKind: runnerSpec.kind };
|
|
1761
|
+
}
|
|
1653
1762
|
export async function akmReflect(options = {}) {
|
|
1654
1763
|
const stash = resolveRunStashDir(options.stashDir);
|
|
1655
1764
|
// Build lazy event emitters. The invocation row is committed only after the
|
|
@@ -1695,17 +1804,7 @@ export async function akmReflect(options = {}) {
|
|
|
1695
1804
|
// 4. Build the shared prompt inputs — feedback, hints, lessons, rejected
|
|
1696
1805
|
// proposals. These are stable across refinement iterations; only the
|
|
1697
1806
|
// `priorDraft` field changes per-iteration (R-1 / #372).
|
|
1698
|
-
const
|
|
1699
|
-
const schemaHints = buildSchemaHints(parsedRef?.type ?? "", assetContent);
|
|
1700
|
-
const relatedLessons = options.ref && parsedRef
|
|
1701
|
-
? await readRelatedLessons(assetCtx, stash, options.ref, parsedRef, options.itemRef)
|
|
1702
|
-
: [];
|
|
1703
|
-
// Reflexion-style verbal-RL: inject rejected proposals so the agent avoids
|
|
1704
|
-
// reproducing proposals that have already been reviewed and refused.
|
|
1705
|
-
const rejectedProposals = readRejectedProposals(stash, options.ref, options.ctx);
|
|
1706
|
-
// Standards "rulebook" for this target — stash convention/meta facts; empty
|
|
1707
|
-
// when none fire.
|
|
1708
|
-
const standardsContext = resolveStandardsContext(options.ref, stash);
|
|
1807
|
+
const sources = await gatherReflectPromptSources(options, stash, parsedRef, assetContent, assetCtx);
|
|
1709
1808
|
// 5. Spawn the agent — with the optional Self-Refine loop (R-1 / #372),
|
|
1710
1809
|
// extracted to {@link runReflectRefineIterations}.
|
|
1711
1810
|
const agentEnv = options.eventSource === "improve" ? { AKM_EVENT_SOURCE: "improve" } : {};
|
|
@@ -1725,11 +1824,7 @@ export async function akmReflect(options = {}) {
|
|
|
1725
1824
|
options,
|
|
1726
1825
|
parsedRef,
|
|
1727
1826
|
assetContent,
|
|
1728
|
-
|
|
1729
|
-
schemaHints,
|
|
1730
|
-
relatedLessons,
|
|
1731
|
-
rejectedProposals,
|
|
1732
|
-
standardsContext,
|
|
1827
|
+
sources,
|
|
1733
1828
|
runnerSpec,
|
|
1734
1829
|
lease: generationLease,
|
|
1735
1830
|
agentEnv,
|
|
@@ -1808,7 +1903,7 @@ export async function akmReflect(options = {}) {
|
|
|
1808
1903
|
qualityGateSkippedNoJudge,
|
|
1809
1904
|
qualityJudgeRunner,
|
|
1810
1905
|
qualityJudgeLease,
|
|
1811
|
-
feedback,
|
|
1906
|
+
feedback: sources.feedback,
|
|
1812
1907
|
stash,
|
|
1813
1908
|
emitReflectFailed,
|
|
1814
1909
|
onNotices: collectExecutionNotices,
|
package/dist/core/errors.js
CHANGED
|
@@ -83,6 +83,7 @@ const TRANSIENT_HINTS = {
|
|
|
83
83
|
RUN_LEASE_HELD: "Wait for the named engine invocation to finish or for the lease to expire, then retry. `akm workflow status <id>` shows the current lease.",
|
|
84
84
|
STATE_DB_CONTENDED: "Another akm process is writing state.db right now. Wait a few seconds and retry; commands that support --skip-if-locked can skip instead of failing.",
|
|
85
85
|
INDEX_DB_CONTENDED: "Another akm process is writing index.db; retry shortly, or pass --skip-if-locked on scheduled runs.",
|
|
86
|
+
MAINTENANCE_BARRIER_BUSY: "Another akm process is registering a lock or lease right now. Retry shortly, or pass --skip-if-locked on scheduled index/improve/workflow runs.",
|
|
86
87
|
};
|
|
87
88
|
/** Default hint for each NotFoundError code. */
|
|
88
89
|
const NOT_FOUND_HINTS = {
|
|
@@ -6,7 +6,8 @@ import { randomUUID } from "node:crypto";
|
|
|
6
6
|
import fs from "node:fs";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { sleepSync } from "../runtime.js";
|
|
9
|
-
import {
|
|
9
|
+
import { backoffDelay } from "./common.js";
|
|
10
|
+
import { ConfigError, TransientError } from "./errors.js";
|
|
10
11
|
import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync } from "./file-lock.js";
|
|
11
12
|
import { getMaintenanceBarrierPath } from "./paths.js";
|
|
12
13
|
const heldBarrierContext = new AsyncLocalStorage();
|
|
@@ -24,6 +25,26 @@ const heldBarrierContext = new AsyncLocalStorage();
|
|
|
24
25
|
* (`commands/improve/extract.ts`).
|
|
25
26
|
*/
|
|
26
27
|
const MAINTENANCE_BARRIER_STALE_AFTER_MS = 5 * 60 * 1000;
|
|
28
|
+
/**
|
|
29
|
+
* The barrier normally holds for one lock-file write — sub-millisecond on
|
|
30
|
+
* any real filesystem. Two akm processes racing to register a lock in the
|
|
31
|
+
* very same instant (e.g. two `akm index` runs a scheduler launched back to
|
|
32
|
+
* back) can still collide on it; retrying briefly resolves that ordinary
|
|
33
|
+
* case instead of failing a legitimate concurrent invocation outright
|
|
34
|
+
* (field follow-up to #956, G1). Bounded short so a genuinely wedged holder
|
|
35
|
+
* still surfaces the busy error promptly rather than making a losing
|
|
36
|
+
* process hang — comfortably above the barrier's normal hold time, well
|
|
37
|
+
* below a length that would make this feel like the blocking lock #872
|
|
38
|
+
* removed. Never applies to the rebuild lock itself, which stays
|
|
39
|
+
* non-blocking (#872).
|
|
40
|
+
*/
|
|
41
|
+
const MAINTENANCE_BARRIER_BUSY_RETRY_BOUND_MS = 1_500;
|
|
42
|
+
let busyRetryBoundMsForTests;
|
|
43
|
+
/** Test-only override for {@link MAINTENANCE_BARRIER_BUSY_RETRY_BOUND_MS}, so a unit test can exercise the
|
|
44
|
+
* exhausted-retry throw without a real ~1.5s wait. Restored via tests/_helpers/seams.ts's resetAllSeams(). */
|
|
45
|
+
export function _setMaintenanceBarrierBusyRetryBoundMsForTests(ms) {
|
|
46
|
+
busyRetryBoundMsForTests = ms;
|
|
47
|
+
}
|
|
27
48
|
/**
|
|
28
49
|
* Serialize the short critical section that creates each long-lived AKM lock,
|
|
29
50
|
* lease, or state activity. The operation keeps its own ownership record; this
|
|
@@ -44,11 +65,19 @@ export function tryAcquireMaintenanceBarrier() {
|
|
|
44
65
|
return undefined;
|
|
45
66
|
}
|
|
46
67
|
export function acquireMaintenanceBarrier() {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
68
|
+
const boundMs = busyRetryBoundMsForTests ?? MAINTENANCE_BARRIER_BUSY_RETRY_BOUND_MS;
|
|
69
|
+
const deadline = Date.now() + boundMs;
|
|
70
|
+
for (let attempt = 0;; attempt += 1) {
|
|
71
|
+
const release = tryAcquireMaintenanceBarrier();
|
|
72
|
+
if (release)
|
|
73
|
+
return release;
|
|
74
|
+
const remainingMs = deadline - Date.now();
|
|
75
|
+
if (remainingMs <= 0)
|
|
76
|
+
break;
|
|
77
|
+
sleepSync(Math.min(backoffDelay(attempt), remainingMs));
|
|
78
|
+
}
|
|
79
|
+
throw new TransientError(`AKM maintenance is in progress (barrier ${getMaintenanceBarrierPath()}); retry shortly. ` +
|
|
80
|
+
`A sentinel older than ${MAINTENANCE_BARRIER_STALE_AFTER_MS / 60_000} minute(s) is reclaimed automatically on the next attempt.`, "MAINTENANCE_BARRIER_BUSY");
|
|
52
81
|
}
|
|
53
82
|
export function withMaintenanceStartBarrier(run) {
|
|
54
83
|
if (heldBarrierContext.getStore()?.active)
|
package/dist/llm/client.js
CHANGED
|
@@ -472,3 +472,21 @@ export async function probeLlmEndpoint(config, timeoutMs = 3_000) {
|
|
|
472
472
|
return { reachable: false, error: err instanceof Error ? err.message : String(err) };
|
|
473
473
|
}
|
|
474
474
|
}
|
|
475
|
+
/**
|
|
476
|
+
* Endpoint-keyed probe memoization (#957): every caller that probes several
|
|
477
|
+
* engine connections in one pass (`akm health`'s engine checks, `akm
|
|
478
|
+
* improve --require-engines`) shares one in-flight probe per distinct
|
|
479
|
+
* endpoint (trailing slashes normalized) instead of firing a duplicate probe
|
|
480
|
+
* when two engines point at the same server. `cache` must be scoped to one
|
|
481
|
+
* invocation and never shared across calls — a stale "reachable" surviving
|
|
482
|
+
* past the run that produced it is the failure mode this exists to avoid.
|
|
483
|
+
*/
|
|
484
|
+
export function probeEndpointOnce(connection, cache, probe) {
|
|
485
|
+
const key = connection.endpoint.replace(/\/+$/, "");
|
|
486
|
+
let pending = cache.get(key);
|
|
487
|
+
if (!pending) {
|
|
488
|
+
pending = probe(connection);
|
|
489
|
+
cache.set(key, pending);
|
|
490
|
+
}
|
|
491
|
+
return pending;
|
|
492
|
+
}
|
|
@@ -67,12 +67,18 @@ function toEventMetadata(record) {
|
|
|
67
67
|
* opened for its other events. A getter is resolved for every append so a
|
|
68
68
|
* caller can replace its context binding without replacing this owning sink.
|
|
69
69
|
* When omitted, `appendEvent` falls back to its default open-insert-close path.
|
|
70
|
+
*
|
|
71
|
+
* `onRecord`, when supplied, runs synchronously for every terminal record
|
|
72
|
+
* BEFORE persistence — improve's first-engine-response heartbeat (#957) uses
|
|
73
|
+
* it to know the run is no longer silent, without this module taking on any
|
|
74
|
+
* dependency of its own on improve's lifecycle.
|
|
70
75
|
*/
|
|
71
|
-
export function installLlmUsagePersistence(ctx) {
|
|
76
|
+
export function installLlmUsagePersistence(ctx, onRecord) {
|
|
72
77
|
let expectedTerminalRecords = 0;
|
|
73
78
|
let disposed = false;
|
|
74
79
|
setLlmUsageSink((record) => {
|
|
75
80
|
expectedTerminalRecords += 1;
|
|
81
|
+
onRecord?.(record);
|
|
76
82
|
appendEvent({ eventType: LLM_USAGE_EVENT, metadata: toEventMetadata(record) }, typeof ctx === "function" ? ctx() : ctx);
|
|
77
83
|
});
|
|
78
84
|
return () => {
|
|
@@ -7071,7 +7071,8 @@ var init_errors = __esm(() => {
|
|
|
7071
7071
|
TRANSIENT_HINTS = {
|
|
7072
7072
|
RUN_LEASE_HELD: "Wait for the named engine invocation to finish or for the lease to expire, then retry. `akm workflow status <id>` shows the current lease.",
|
|
7073
7073
|
STATE_DB_CONTENDED: "Another akm process is writing state.db right now. Wait a few seconds and retry; commands that support --skip-if-locked can skip instead of failing.",
|
|
7074
|
-
INDEX_DB_CONTENDED: "Another akm process is writing index.db; retry shortly, or pass --skip-if-locked on scheduled runs."
|
|
7074
|
+
INDEX_DB_CONTENDED: "Another akm process is writing index.db; retry shortly, or pass --skip-if-locked on scheduled runs.",
|
|
7075
|
+
MAINTENANCE_BARRIER_BUSY: "Another akm process is registering a lock or lease right now. Retry shortly, or pass --skip-if-locked on scheduled index/improve/workflow runs."
|
|
7075
7076
|
};
|
|
7076
7077
|
NOT_FOUND_HINTS = {
|
|
7077
7078
|
ASSET_NOT_FOUND: "Run `akm search <query>` or `akm index` to refresh the index.",
|
|
@@ -77923,10 +77924,13 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
77923
77924
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
77924
77925
|
import fs7 from "node:fs";
|
|
77925
77926
|
import path12 from "node:path";
|
|
77927
|
+
init_common();
|
|
77926
77928
|
init_errors();
|
|
77927
77929
|
init_paths();
|
|
77928
77930
|
var heldBarrierContext = new AsyncLocalStorage;
|
|
77929
77931
|
var MAINTENANCE_BARRIER_STALE_AFTER_MS = 5 * 60 * 1000;
|
|
77932
|
+
var MAINTENANCE_BARRIER_BUSY_RETRY_BOUND_MS = 1500;
|
|
77933
|
+
var busyRetryBoundMsForTests;
|
|
77930
77934
|
function tryAcquireMaintenanceBarrier() {
|
|
77931
77935
|
const lockPath = getMaintenanceBarrierPath();
|
|
77932
77936
|
fs7.mkdirSync(path12.dirname(lockPath), { recursive: true });
|
|
@@ -77942,10 +77946,18 @@ function tryAcquireMaintenanceBarrier() {
|
|
|
77942
77946
|
return;
|
|
77943
77947
|
}
|
|
77944
77948
|
function acquireMaintenanceBarrier() {
|
|
77945
|
-
const
|
|
77946
|
-
|
|
77947
|
-
|
|
77948
|
-
|
|
77949
|
+
const boundMs = busyRetryBoundMsForTests ?? MAINTENANCE_BARRIER_BUSY_RETRY_BOUND_MS;
|
|
77950
|
+
const deadline = Date.now() + boundMs;
|
|
77951
|
+
for (let attempt = 0;; attempt += 1) {
|
|
77952
|
+
const release = tryAcquireMaintenanceBarrier();
|
|
77953
|
+
if (release)
|
|
77954
|
+
return release;
|
|
77955
|
+
const remainingMs = deadline - Date.now();
|
|
77956
|
+
if (remainingMs <= 0)
|
|
77957
|
+
break;
|
|
77958
|
+
sleepSync(Math.min(backoffDelay(attempt), remainingMs));
|
|
77959
|
+
}
|
|
77960
|
+
throw new TransientError(`AKM maintenance is in progress (barrier ${getMaintenanceBarrierPath()}); retry shortly. ` + `A sentinel older than ${MAINTENANCE_BARRIER_STALE_AFTER_MS / 60000} minute(s) is reclaimed automatically on the next attempt.`, "MAINTENANCE_BARRIER_BUSY");
|
|
77949
77961
|
}
|
|
77950
77962
|
function withMaintenanceStartBarrier(run) {
|
|
77951
77963
|
if (heldBarrierContext.getStore()?.active)
|
|
@@ -7070,7 +7070,8 @@ var init_errors = __esm(() => {
|
|
|
7070
7070
|
TRANSIENT_HINTS = {
|
|
7071
7071
|
RUN_LEASE_HELD: "Wait for the named engine invocation to finish or for the lease to expire, then retry. `akm workflow status <id>` shows the current lease.",
|
|
7072
7072
|
STATE_DB_CONTENDED: "Another akm process is writing state.db right now. Wait a few seconds and retry; commands that support --skip-if-locked can skip instead of failing.",
|
|
7073
|
-
INDEX_DB_CONTENDED: "Another akm process is writing index.db; retry shortly, or pass --skip-if-locked on scheduled runs."
|
|
7073
|
+
INDEX_DB_CONTENDED: "Another akm process is writing index.db; retry shortly, or pass --skip-if-locked on scheduled runs.",
|
|
7074
|
+
MAINTENANCE_BARRIER_BUSY: "Another akm process is registering a lock or lease right now. Retry shortly, or pass --skip-if-locked on scheduled index/improve/workflow runs."
|
|
7074
7075
|
};
|
|
7075
7076
|
NOT_FOUND_HINTS = {
|
|
7076
7077
|
ASSET_NOT_FOUND: "Run `akm search <query>` or `akm index` to refresh the index.",
|
|
@@ -77251,10 +77252,13 @@ import { AsyncLocalStorage } from "async_hooks";
|
|
|
77251
77252
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
77252
77253
|
import fs7 from "fs";
|
|
77253
77254
|
import path12 from "path";
|
|
77255
|
+
init_common();
|
|
77254
77256
|
init_errors();
|
|
77255
77257
|
init_paths();
|
|
77256
77258
|
var heldBarrierContext = new AsyncLocalStorage;
|
|
77257
77259
|
var MAINTENANCE_BARRIER_STALE_AFTER_MS = 5 * 60 * 1000;
|
|
77260
|
+
var MAINTENANCE_BARRIER_BUSY_RETRY_BOUND_MS = 1500;
|
|
77261
|
+
var busyRetryBoundMsForTests;
|
|
77258
77262
|
function tryAcquireMaintenanceBarrier() {
|
|
77259
77263
|
const lockPath = getMaintenanceBarrierPath();
|
|
77260
77264
|
fs7.mkdirSync(path12.dirname(lockPath), { recursive: true });
|
|
@@ -77270,10 +77274,18 @@ function tryAcquireMaintenanceBarrier() {
|
|
|
77270
77274
|
return;
|
|
77271
77275
|
}
|
|
77272
77276
|
function acquireMaintenanceBarrier() {
|
|
77273
|
-
const
|
|
77274
|
-
|
|
77275
|
-
|
|
77276
|
-
|
|
77277
|
+
const boundMs = busyRetryBoundMsForTests ?? MAINTENANCE_BARRIER_BUSY_RETRY_BOUND_MS;
|
|
77278
|
+
const deadline = Date.now() + boundMs;
|
|
77279
|
+
for (let attempt = 0;; attempt += 1) {
|
|
77280
|
+
const release = tryAcquireMaintenanceBarrier();
|
|
77281
|
+
if (release)
|
|
77282
|
+
return release;
|
|
77283
|
+
const remainingMs = deadline - Date.now();
|
|
77284
|
+
if (remainingMs <= 0)
|
|
77285
|
+
break;
|
|
77286
|
+
sleepSync(Math.min(backoffDelay(attempt), remainingMs));
|
|
77287
|
+
}
|
|
77288
|
+
throw new TransientError(`AKM maintenance is in progress (barrier ${getMaintenanceBarrierPath()}); retry shortly. ` + `A sentinel older than ${MAINTENANCE_BARRIER_STALE_AFTER_MS / 60000} minute(s) is reclaimed automatically on the next attempt.`, "MAINTENANCE_BARRIER_BUSY");
|
|
77277
77289
|
}
|
|
77278
77290
|
function withMaintenanceStartBarrier(run) {
|
|
77279
77291
|
if (heldBarrierContext.getStore()?.active)
|
|
@@ -8,6 +8,20 @@ now, retry shortly." If a script or scheduler wrapper special-cases exit 2 to
|
|
|
8
8
|
detect a held lease, switch it to exit 75, or read the JSON envelope's `code`
|
|
9
9
|
field instead.
|
|
10
10
|
|
|
11
|
+
A concurrent `akm index` now also exits 75 instead of exit 78 or exit 70. A
|
|
12
|
+
2026-09-10 field report found a second `akm index` (no `--skip-if-locked`)
|
|
13
|
+
colliding on the short internal barrier that registers the opt-in rebuild
|
|
14
|
+
lock could fail with `{"code":"INVALID_CONFIG_FILE"}` at exit 78 — a
|
|
15
|
+
config-error exit that told a supervisor to stop retrying ordinary
|
|
16
|
+
contention between two legitimate runs. That barrier now retries briefly
|
|
17
|
+
before giving up, and a busy barrier is reclassified as `TransientError`
|
|
18
|
+
code `MAINTENANCE_BARRIER_BUSY` (exit 75) instead. Contention on index.db
|
|
19
|
+
itself was already reclassified from a raw `database is locked` error (exit
|
|
20
|
+
70) to `TransientError` code `INDEX_DB_CONTENDED` (exit 75). Either way, the
|
|
21
|
+
fix for a scheduled or opportunistic index run is the same:
|
|
22
|
+
`akm index --skip-if-locked` steps aside (exit 0) instead of contending at
|
|
23
|
+
all.
|
|
24
|
+
|
|
11
25
|
The six shipped scheduled `improve` task templates now run with
|
|
12
26
|
`--require-engines`, which aborts (exit 78) before any index work when a
|
|
13
27
|
process's engine or credential cannot be resolved in the task's own
|
|
@@ -17,6 +31,23 @@ already-materialized task files are not rewritten. To get the same protection
|
|
|
17
31
|
on an existing scheduled task, add `--require-engines` to its `run:` command
|
|
18
32
|
yourself, then run `akm task sync`.
|
|
19
33
|
|
|
34
|
+
`--require-engines` now also runs a bounded reachability probe against each
|
|
35
|
+
distinct engine endpoint (the same probe `akm health` already uses), not
|
|
36
|
+
only a config/credential check. A field re-test found the flag let a run
|
|
37
|
+
through to a fully dead endpoint, which then sat silent for minutes making
|
|
38
|
+
no progress and no exit — the flag's exit-78 abort now catches that case up
|
|
39
|
+
front, naming the unreachable engine and endpoint, before any index work.
|
|
40
|
+
If a scheduled `--require-engines` run starts failing at exit 78 after
|
|
41
|
+
upgrading, check that the engine's endpoint actually answers — this is the
|
|
42
|
+
flag doing its documented job on a condition it previously missed, not a
|
|
43
|
+
new failure mode. Separately, `--timeout-ms` and an engine's own configured
|
|
44
|
+
timeout already aborted an in-flight request correctly, and SIGTERM/SIGINT
|
|
45
|
+
already ended a run within its documented grace period — both confirmed,
|
|
46
|
+
not changed, by this investigation. A live run now also prints one
|
|
47
|
+
default-level line if it waits more than a few seconds on its first engine
|
|
48
|
+
response, so a slow-but-alive run and a dead one are never indistinguishable
|
|
49
|
+
from silence alone.
|
|
50
|
+
|
|
20
51
|
`akm health --no-probe` now also skips the `cli-version` update check (a GitHub
|
|
21
52
|
release lookup), alongside the engine-reachability checks it already skipped.
|
|
22
53
|
An air-gapped or offline host's existing `--no-probe` habit now suppresses both
|
|
@@ -114,6 +145,40 @@ instead of ever failing a whole batch over one oversized document.
|
|
|
114
145
|
set `contextLength` specifically to control request batching (not your
|
|
115
146
|
Ollama server's context window), set `embedding.maxTokens` instead.
|
|
116
147
|
|
|
148
|
+
**Which token knob fixed the original 8k-context overflow.** A 0.9.15-beta
|
|
149
|
+
field report described documents estimated under the request budget that
|
|
150
|
+
still tokenized to 8.5k-12.4k real tokens against an 8192-token endpoint,
|
|
151
|
+
because the 4-chars-per-token estimator undercounts dense technical text.
|
|
152
|
+
`maxInputTokens`, `maxTokens`, and `contextLength` are easy to confuse, and
|
|
153
|
+
only one of them makes that overflow structurally unreachable:
|
|
154
|
+
|
|
155
|
+
- `embedding.maxInputTokens` (default `512`) is the per-DOCUMENT cap. It
|
|
156
|
+
truncates a document's embedded text to its head before the document is
|
|
157
|
+
ever counted toward a request, so no single document can contribute more
|
|
158
|
+
than 512 estimated tokens. This is the fix: it makes the original
|
|
159
|
+
single-document overflow structurally unreachable, independent of the
|
|
160
|
+
other two knobs.
|
|
161
|
+
- `embedding.maxTokens` (default `6000`) is the per-REQUEST budget — how
|
|
162
|
+
many already-capped documents fit in one HTTP request — plus a same-run
|
|
163
|
+
adaptive shrink on the first context-size rejection. It reduces how often
|
|
164
|
+
a request lands near an endpoint's real limit, but a request-level budget
|
|
165
|
+
alone cannot stop one oversized document from overflowing a request.
|
|
166
|
+
- `embedding.contextLength` sets Ollama's `num_ctx` only. It no longer feeds
|
|
167
|
+
the request token budget the way it used to (the two fields used to share
|
|
168
|
+
this one value), and it has no effect at all against a non-Ollama
|
|
169
|
+
endpoint.
|
|
170
|
+
|
|
171
|
+
The field's exact 0.9.15-beta config — `contextLength: 8192` and
|
|
172
|
+
`maxTokens: 8000` — produces no 400s on 0.9.15. `maxTokens` now defaults
|
|
173
|
+
lower anyway (6000), but that is not why the overflow stopped: every
|
|
174
|
+
document is truncated to `maxInputTokens` (512 tokens) before it is counted
|
|
175
|
+
toward any request, so the 8.5k-12.4k-token documents that used to overflow
|
|
176
|
+
an 8192-token endpoint can no longer reach the request budget in the first
|
|
177
|
+
place. Set `embedding.maxInputTokens` higher only if you need documents
|
|
178
|
+
longer than ~2000 characters embedded in full — for a corpus with such
|
|
179
|
+
documents, size `embedding.maxTokens` to still fit the worst case, or the
|
|
180
|
+
overflow risk returns.
|
|
181
|
+
|
|
117
182
|
`akm index --full` and an index-generation bump no longer re-embed
|
|
118
183
|
unchanged content: vectors about to be discarded are salvaged and handed
|
|
119
184
|
back to unchanged entries at the start of the next embedding pass instead
|
package/docs/reference/cli.md
CHANGED
|
@@ -285,6 +285,13 @@ long enough to exhaust the driver's retry window, the run now fails with
|
|
|
285
285
|
exit 75 (`TransientError`, code `INDEX_DB_CONTENDED`) instead of the raw
|
|
286
286
|
driver error at exit 70 — the same retry-shortly contract as
|
|
287
287
|
`STATE_DB_CONTENDED`, so a scheduler can branch on it instead of alerting.
|
|
288
|
+
The rebuild lock itself is registered through a brief internal barrier
|
|
289
|
+
(`getMaintenanceBarrierPath()`) shared with every other akm lock/lease; two
|
|
290
|
+
`akm index` runs launched close enough together to collide on that
|
|
291
|
+
registration step retry briefly and then, if it is still busy, also exit 75
|
|
292
|
+
(code `MAINTENANCE_BARRIER_BUSY`) rather than the config-error exit 78 a
|
|
293
|
+
2026-09-10 field report found — a busy registration barrier is ordinary
|
|
294
|
+
contention between two legitimate runs, never a broken config file.
|
|
288
295
|
`--skip-if-locked` changes that only for the invocation that passes it: if
|
|
289
296
|
the lock is already held by a live process, it skips gracefully (exit 0,
|
|
290
297
|
`{ ok: true, skipped: { reason: "lock-held", pid, launcherPid, startedAt } }`
|
|
@@ -2336,6 +2343,7 @@ akm improve --require-engines # for scheduled runs: abort (exit 78) ins
|
|
|
2336
2343
|
akm improve --no-sync # skip the end-of-run git commit entirely (default: on for git-backed bundles)
|
|
2337
2344
|
akm improve --sync --no-push # commit only, skip the push after it
|
|
2338
2345
|
akm improve --plan --strategy thorough # preview thorough's resolved engine/model routing; nothing is dispatched
|
|
2346
|
+
akm improve lessons/my-lesson --show-prompt --format text # print the composed reflect prompt for one asset, unwrapped; no lock/index/engine call
|
|
2339
2347
|
akm improve report # LLM usage/routing report for the most recent real run
|
|
2340
2348
|
akm improve report --run <id> # ...for one specific improve_runs id
|
|
2341
2349
|
akm improve report --since 7d # ...aggregated over every real run started in the last 7 days
|
|
@@ -2355,7 +2363,8 @@ akm improve report --since 7d # ...aggregated over every real run start
|
|
|
2355
2363
|
| `--strategy <name>` | Override the active improve strategy (a built-in or entry under `improve.strategies`) |
|
|
2356
2364
|
| `--json-to-stdout` | Also emit the full persisted JSON result on stdout for a live run. Without this flag, stdout stays empty. Dry-runs always emit their result and are never persisted. |
|
|
2357
2365
|
| `--skip-if-locked` | If another improve run already holds the lock, skip gracefully (exit 0) instead of failing with "already running" (exit 78). Use for high-frequency scheduled runs so they don't pile up failures while a longer run is in progress. |
|
|
2358
|
-
| `--require-engines` | Abort (exit 78, before any indexing, lock, or log side effect) if the active strategy would enable a process whose engine or credential cannot be resolved in this process's environment. Without this flag, improve degrades gracefully: it skips the affected processes and reports them in the result's `skippedProcesses`. Recommended alongside `--skip-if-locked` for scheduled runs, since the operator's own shell can pass config validation while a scheduler's stripped-down environment (see #953) cannot. |
|
|
2366
|
+
| `--require-engines` | Abort (exit 78, before any indexing, lock, or log side effect) if the active strategy would enable a process whose engine or credential cannot be resolved in this process's environment, OR whose endpoint fails a bounded reachability probe — the same probe `akm health`'s `default-llm-engine`/`configured-engines` checks run, once per distinct endpoint. Without this flag, improve degrades gracefully: it skips the affected processes and reports them in the result's `skippedProcesses`. Recommended alongside `--skip-if-locked` for scheduled runs, since the operator's own shell can pass config validation while a scheduler's stripped-down environment (see #953) cannot. |
|
|
2367
|
+
| `--show-prompt` | Print the composed reflect prompt (#952) for one asset and exit — before any lock, index write, or engine dispatch. Requires a fully-qualified asset ref as the scope (`akm improve lessons/my-lesson --show-prompt`); rejected with a type or whole-bundle scope. The default output format is JSON, which carries the prompt as a `prompt` field (escaped into one line) alongside the resolved `engine`/`engineKind`; pass `--format text` to print the prompt itself, unwrapped and readable by eye. |
|
|
2359
2368
|
| `--sync` / `--no-sync` | Commit (and optionally push) the git-backed primary bundle when the run finishes. Default: on for git-backed bundles (per profile config). |
|
|
2360
2369
|
| `--push` / `--no-push` | Push after the end-of-run sync commit when writable with a remote configured. `--no-push` commits only, skipping the push. Default: per profile config (`true`). `sync.push` stays outside the autonomy gate — this is a per-run opt-out, not a default change. |
|
|
2361
2370
|
|
|
@@ -2411,6 +2420,18 @@ on an unavailable credential either — even a strategy left with every process
|
|
|
2411
2420
|
disabled this way still returns its plan, with the affected processes in
|
|
2412
2421
|
`skippedProcesses`.
|
|
2413
2422
|
|
|
2423
|
+
`--timeout-ms` is a run-wide wall-clock budget: when it expires, the run
|
|
2424
|
+
cooperatively aborts any in-flight engine request (the same `AbortSignal`
|
|
2425
|
+
every LLM call already honors) instead of waiting out the engine's own,
|
|
2426
|
+
much longer, per-call timeout — the run then finishes and reports normally
|
|
2427
|
+
rather than hanging past its budget. SIGTERM/SIGINT/SIGHUP end a live run
|
|
2428
|
+
the same way, within a short bounded grace period, and the process exits
|
|
2429
|
+
with a stable per-signal code (`143`/`130`/`129`) rather than needing a
|
|
2430
|
+
`kill -9`. If a live run has waited more than a few seconds without any
|
|
2431
|
+
engine response at all, one default-level line ("Still waiting for the
|
|
2432
|
+
first engine response...") is printed so a scheduled run's log is never
|
|
2433
|
+
silently empty while an engine is slow or dead.
|
|
2434
|
+
|
|
2414
2435
|
For dry runs, `plannedRefs` is the effective post-limit work set, not every
|
|
2415
2436
|
ref in the requested scope. The `plan` object preserves both views: raw scope
|
|
2416
2437
|
size and per-gate removals, configured and effective caps, final ranked refs
|
|
@@ -2452,6 +2473,17 @@ no model or per-process notices. Neither `--dry-run` nor `--plan` probes
|
|
|
2452
2473
|
engine reachability over the network — pair with `akm health --probe` (or the
|
|
2453
2474
|
default probe-on behavior) to check whether a named engine actually answers.
|
|
2454
2475
|
|
|
2476
|
+
`--show-prompt` (#952) is the cheapest way to exercise reflect alone: it
|
|
2477
|
+
builds the exact prompt reflect would send for one asset — the same source
|
|
2478
|
+
resolution, runner selection, feedback/schema-hint/related-lesson/rejected-
|
|
2479
|
+
proposal gathering `akm improve`'s live reflect step uses — and prints it
|
|
2480
|
+
without acquiring a dispatch lease, so it never calls an engine. Add
|
|
2481
|
+
`--format text` (the default JSON/yaml envelope escapes the prompt into one
|
|
2482
|
+
line, which defeats a by-eye read) to confirm by eye that recent feedback is
|
|
2483
|
+
framed as an unverified report to investigate (never a fact to insert
|
|
2484
|
+
verbatim) and that the response contract tells the model never to emit the
|
|
2485
|
+
truncation marker or any content from outside the shown asset.
|
|
2486
|
+
|
|
2455
2487
|
When reinforced facts need promotion, `knowledge` is the higher-authority
|
|
2456
2488
|
destination than `memory`. The deterministic search ranking also prefers
|
|
2457
2489
|
`knowledge` over `memory` hits, including inferred `.derived` memories, when
|
|
@@ -411,6 +411,40 @@ apply when unset), for a remote endpoint (`src/llm/embedders/remote.ts`):
|
|
|
411
411
|
| `embedding.timeoutMs` | `120000` (120s) | Per-request wall timeout — see below. |
|
|
412
412
|
| `embedding.concurrency` | `1` loopback / `2` remote | In-flight request window — see below. |
|
|
413
413
|
|
|
414
|
+
**Which knob fixed the field's 8k-context overflow, worked examples.** A
|
|
415
|
+
0.9.15-beta field report described documents estimated under the request
|
|
416
|
+
budget that still tokenized to 8.5k-12.4k real tokens against an
|
|
417
|
+
8192-token endpoint, because the 4-chars-per-token estimator undercounts
|
|
418
|
+
dense technical text. Three knobs changed shape between beta and this
|
|
419
|
+
release; only one of them makes that overflow structurally unreachable:
|
|
420
|
+
|
|
421
|
+
- `embedding.maxInputTokens: 512` — per-DOCUMENT cap, applied before
|
|
422
|
+
batching. Example: a 6,000-character API reference page is truncated to
|
|
423
|
+
its first ~2,000 characters (512 estimated tokens) before it is ever
|
|
424
|
+
counted toward a request. This is the fix for the original overflow: no
|
|
425
|
+
single document can contribute more than 512 estimated tokens to a
|
|
426
|
+
request, no matter how `maxTokens` or `contextLength` are set.
|
|
427
|
+
- `embedding.maxTokens: 6000` — per-REQUEST budget: how many already-capped
|
|
428
|
+
documents' estimated tokens fit in one HTTP request. Example: with the
|
|
429
|
+
default 512-token document cap, a request packs about 11 documents before
|
|
430
|
+
this budget is reached and the request is sent; if the run's first
|
|
431
|
+
request is still rejected for exceeding the endpoint's real context
|
|
432
|
+
window, akm shrinks this budget to three quarters of its value (floored
|
|
433
|
+
at twice `maxInputTokens`) for every later request in the same run. A
|
|
434
|
+
request-level budget alone cannot stop one oversized document from
|
|
435
|
+
overflowing a request — only the per-document cap above does that.
|
|
436
|
+
- `embedding.contextLength: 8192` — Ollama's `num_ctx` only, forwarded
|
|
437
|
+
verbatim on a native `/api/embed` request. It has no effect on request or
|
|
438
|
+
document sizing, and no effect at all against a non-Ollama endpoint — see
|
|
439
|
+
below for why that used not to be true.
|
|
440
|
+
|
|
441
|
+
A field config of `contextLength: 8192` + `maxTokens: 8000` (the exact
|
|
442
|
+
0.9.15-beta values from the original report) produces no 400s on 0.9.15:
|
|
443
|
+
`maxInputTokens` (512, new this release) caps every document before it is
|
|
444
|
+
counted, so the original 8.5k-12.4k-token documents that overflowed the
|
|
445
|
+
8192-token endpoint can never reach the request budget in the first place —
|
|
446
|
+
independent of whatever `maxTokens` or `contextLength` are set to.
|
|
447
|
+
|
|
414
448
|
`embedding.timeoutMs` (positive integer, default `120000` — 120s) is the
|
|
415
449
|
budget for a request at the FULL token budget (`embedding.maxTokens`); a
|
|
416
450
|
local model server on a large, token-budget-bounded batch legitimately takes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.9.15-beta.
|
|
3
|
+
"version": "0.9.15-beta.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cursor.",
|
|
6
6
|
"keywords": [
|