akm-cli 0.9.0-rc.3 → 0.9.0-rc.5
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 +12 -4
- package/README.md +7 -8
- package/SECURITY.md +5 -3
- package/dist/akm +44 -33
- package/dist/akm-migrate-storage +44 -33
- package/dist/assets/backends/schtasks-template.xml +2 -1
- package/dist/assets/help/help-improve.md +3 -0
- package/dist/cli.js +25 -10
- package/dist/commands/health/html-report.js +23 -2
- package/dist/commands/health/improve-metrics.js +45 -6
- package/dist/commands/health/md-report.js +10 -1
- package/dist/commands/health/windows.js +1 -1
- package/dist/commands/health.js +1 -1
- package/dist/commands/improve/distill/promote-memory.js +6 -2
- package/dist/commands/improve/distill/quality-gate.js +94 -31
- package/dist/commands/improve/distill.js +7 -2
- package/dist/commands/improve/extract.js +5 -2
- package/dist/commands/improve/improve-auto-accept.js +2 -2
- package/dist/commands/improve/improve.js +158 -176
- package/dist/commands/improve/locks.js +22 -94
- package/dist/commands/improve/loop-stages.js +15 -10
- package/dist/commands/improve/preparation.js +21 -17
- package/dist/commands/improve/proactive-maintenance.js +4 -6
- package/dist/commands/improve/reflect.js +40 -67
- package/dist/commands/tasks/default-tasks.js +5 -5
- package/dist/commands/tasks/tasks-cli.js +7 -3
- package/dist/commands/tasks/tasks.js +261 -68
- package/dist/core/extra-params.js +1 -0
- package/dist/core/improve-result.js +87 -7
- package/dist/core/maintenance-barrier.js +16 -0
- package/dist/core/state-db.js +1 -0
- package/dist/core/write-source.js +1 -0
- package/dist/indexer/ensure-index.js +6 -4
- package/dist/llm/client.js +7 -5
- package/dist/schemas/akm-task.json +2 -2
- package/dist/schemas/akm-workflow.json +2 -2
- package/dist/scripts/migrate-storage.js +23 -4
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +91 -9
- package/dist/storage/repositories/task-history-repository.js +30 -3
- package/dist/tasks/backends/cron.js +71 -38
- package/dist/tasks/backends/exec-utils.js +55 -3
- package/dist/tasks/backends/launchd.js +241 -50
- package/dist/tasks/backends/schtasks.js +434 -59
- package/dist/tasks/command-executable.js +93 -0
- package/dist/tasks/parser.js +142 -6
- package/dist/tasks/resolve-akm-bin.js +19 -4
- package/dist/tasks/runner.js +258 -93
- package/dist/tasks/schedule.js +108 -19
- package/dist/tasks/scheduler-invocation.js +86 -0
- package/dist/tasks/task-id.js +37 -0
- package/docs/migration/release-notes/0.9.0.md +4 -2
- package/docs/migration/v0.8-to-v0.9.md +64 -9
- package/package.json +2 -3
- package/schemas/akm-task.json +2 -2
- package/schemas/akm-workflow.json +2 -2
|
@@ -6,37 +6,22 @@ import path from "node:path";
|
|
|
6
6
|
import { ConfigError } from "../../core/errors.js";
|
|
7
7
|
import { appendEvent } from "../../core/events.js";
|
|
8
8
|
import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync, } from "../../core/file-lock.js";
|
|
9
|
-
import { withMaintenanceStartBarrier } from "../../core/maintenance-barrier.js";
|
|
9
|
+
import { tryWithMaintenanceStartBarrier, withMaintenanceStartBarrier } from "../../core/maintenance-barrier.js";
|
|
10
10
|
import { warn } from "../../core/warn.js";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
// alongside daily reflect+distill).
|
|
15
|
-
//
|
|
16
|
-
// consolidate.lock — protects consolidate + memoryInference (both write index.db)
|
|
17
|
-
// reflect-distill.lock — protects reflect + distill (both write state.db proposals)
|
|
18
|
-
// triage.lock — protects triage (writes proposal promotions)
|
|
19
|
-
//
|
|
20
|
-
// Stale timeouts are per-lock, tuned to the expected runtime of the protected
|
|
21
|
-
// processes: consolidate is disk-bound (1h), reflect+distill is GPU-bound (2h),
|
|
22
|
-
// triage is fast (30min).
|
|
23
|
-
export const PROCESS_LOCK_DEFS = {
|
|
24
|
-
consolidate: { fileName: "consolidate.lock", staleAfterMs: 60 * 60 * 1000 },
|
|
25
|
-
reflectDistill: { fileName: "reflect-distill.lock", staleAfterMs: 2 * 60 * 60 * 1000 },
|
|
26
|
-
triage: { fileName: "triage.lock", staleAfterMs: 30 * 60 * 1000 },
|
|
27
|
-
};
|
|
28
|
-
const heldProcessLocks = new Set();
|
|
29
|
-
export function resetHeldProcessLocks() {
|
|
30
|
-
heldProcessLocks.clear();
|
|
11
|
+
export const MIN_IMPROVE_LOCK_STALE_MS = 4 * 60 * 60 * 1000;
|
|
12
|
+
export function improveLockPath(lockBaseDir) {
|
|
13
|
+
return path.join(lockBaseDir, "improve.lock");
|
|
31
14
|
}
|
|
32
|
-
export function
|
|
33
|
-
return path.join(lockBaseDir, PROCESS_LOCK_DEFS[lockName].fileName);
|
|
34
|
-
}
|
|
35
|
-
export function tryAcquireProcessLock(lockPath, staleAfterMs, skipIfLocked, lockLabel) {
|
|
15
|
+
export function tryAcquireImproveLock(lockPath, staleAfterMs, skipIfLocked) {
|
|
36
16
|
let recoveryEvent;
|
|
37
|
-
const
|
|
17
|
+
const acquire = () => tryAcquireImproveLockUnlocked(lockPath, staleAfterMs, skipIfLocked, (event) => {
|
|
38
18
|
recoveryEvent = event;
|
|
39
|
-
})
|
|
19
|
+
});
|
|
20
|
+
const result = skipIfLocked ? tryWithMaintenanceStartBarrier(acquire) : withMaintenanceStartBarrier(acquire);
|
|
21
|
+
if (!result) {
|
|
22
|
+
warn("[improve] maintenance barrier held; skipping (--skip-if-locked)");
|
|
23
|
+
return { state: "skipped" };
|
|
24
|
+
}
|
|
40
25
|
if (recoveryEvent) {
|
|
41
26
|
try {
|
|
42
27
|
appendEvent(recoveryEvent);
|
|
@@ -47,12 +32,11 @@ export function tryAcquireProcessLock(lockPath, staleAfterMs, skipIfLocked, lock
|
|
|
47
32
|
}
|
|
48
33
|
return result;
|
|
49
34
|
}
|
|
50
|
-
function
|
|
35
|
+
function tryAcquireImproveLockUnlocked(lockPath, staleAfterMs, skipIfLocked, onRecovered) {
|
|
51
36
|
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
52
37
|
const lockPayload = () => createLockPayload({ startedAt: new Date().toISOString() });
|
|
53
38
|
let ownership = tryAcquireLockSync(lockPath, lockPayload());
|
|
54
39
|
if (ownership) {
|
|
55
|
-
heldProcessLocks.add(ownership);
|
|
56
40
|
return { state: "acquired", ownership };
|
|
57
41
|
}
|
|
58
42
|
const probe = probeLock(lockPath, { staleAfterMs });
|
|
@@ -64,7 +48,6 @@ function tryAcquireProcessLockUnlocked(lockPath, staleAfterMs, skipIfLocked, loc
|
|
|
64
48
|
if (probe.state === "absent") {
|
|
65
49
|
ownership = tryAcquireLockSync(lockPath, lockPayload());
|
|
66
50
|
if (ownership) {
|
|
67
|
-
heldProcessLocks.add(ownership);
|
|
68
51
|
return { state: "acquired", ownership };
|
|
69
52
|
}
|
|
70
53
|
// Re-grabbed by another racer in the window — fall through and treat as held.
|
|
@@ -83,15 +66,15 @@ function tryAcquireProcessLockUnlocked(lockPath, staleAfterMs, skipIfLocked, loc
|
|
|
83
66
|
if (probe.state === "stale") {
|
|
84
67
|
if (!reclaimStaleLock(lockPath, probe)) {
|
|
85
68
|
if (skipIfLocked) {
|
|
86
|
-
warn(
|
|
69
|
+
warn("[improve] lock changed ownership during stale recovery; skipping (--skip-if-locked)");
|
|
87
70
|
return { state: "skipped" };
|
|
88
71
|
}
|
|
89
|
-
throw new ConfigError(`akm improve
|
|
72
|
+
throw new ConfigError(`akm improve is already running. Delete ${lockPath} to force.`, "INVALID_CONFIG_FILE");
|
|
90
73
|
}
|
|
91
74
|
onRecovered({
|
|
92
75
|
eventType: "improve_lock_recovered",
|
|
93
76
|
metadata: {
|
|
94
|
-
lockName:
|
|
77
|
+
lockName: "improve",
|
|
95
78
|
stalePid: lock?.pid ?? null,
|
|
96
79
|
lockedAt: lock?.startedAt ?? null,
|
|
97
80
|
recoveredAt: new Date().toISOString(),
|
|
@@ -101,75 +84,20 @@ function tryAcquireProcessLockUnlocked(lockPath, staleAfterMs, skipIfLocked, loc
|
|
|
101
84
|
});
|
|
102
85
|
ownership = tryAcquireLockSync(lockPath, lockPayload());
|
|
103
86
|
if (ownership) {
|
|
104
|
-
heldProcessLocks.add(ownership);
|
|
105
87
|
return { state: "acquired", ownership };
|
|
106
88
|
}
|
|
107
89
|
if (skipIfLocked) {
|
|
108
|
-
warn(
|
|
90
|
+
warn("[improve] lock acquired by another run during stale recovery; skipping (--skip-if-locked)");
|
|
109
91
|
return { state: "skipped" };
|
|
110
92
|
}
|
|
111
|
-
throw new ConfigError(`akm improve
|
|
93
|
+
throw new ConfigError(`akm improve is already running. Delete ${lockPath} to force.`, "INVALID_CONFIG_FILE");
|
|
112
94
|
}
|
|
113
95
|
if (skipIfLocked) {
|
|
114
|
-
warn(`[improve]
|
|
96
|
+
warn(`[improve] another improve run holds the lock (PID ${lock?.pid}, started ${lock?.startedAt}); skipping (--skip-if-locked)`);
|
|
115
97
|
return { state: "skipped" };
|
|
116
98
|
}
|
|
117
|
-
throw new ConfigError(`akm improve
|
|
118
|
-
}
|
|
119
|
-
export function releaseProcessLock(ownership) {
|
|
120
|
-
if (!heldProcessLocks.has(ownership))
|
|
121
|
-
return;
|
|
122
|
-
try {
|
|
123
|
-
releaseLock(ownership);
|
|
124
|
-
}
|
|
125
|
-
finally {
|
|
126
|
-
heldProcessLocks.delete(ownership);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
export function releaseAllProcessLocks() {
|
|
130
|
-
for (const ownership of heldProcessLocks) {
|
|
131
|
-
try {
|
|
132
|
-
releaseLock(ownership);
|
|
133
|
-
}
|
|
134
|
-
catch {
|
|
135
|
-
// ignore
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
heldProcessLocks.clear();
|
|
99
|
+
throw new ConfigError(`akm improve is already running (PID ${lock?.pid}, started ${lock?.startedAt}). Delete ${lockPath} to force.`, "INVALID_CONFIG_FILE");
|
|
139
100
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
* backstop (signal handler / budget watchdog paths that skip the normal finally).
|
|
143
|
-
* Uses exact ownership handles so a lock legitimately re-acquired after stale
|
|
144
|
-
* recovery is never deleted. Does NOT clear the Set (the process is exiting).
|
|
145
|
-
*/
|
|
146
|
-
export function releaseHeldLocksIfOwned() {
|
|
147
|
-
for (const ownership of heldProcessLocks) {
|
|
148
|
-
releaseLock(ownership);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* RAII for the "best-effort stage" lock pattern: acquire the lock if available,
|
|
153
|
-
* run `body` REGARDLESS of acquisition, and release the lock in a `finally` iff
|
|
154
|
-
* we acquired it (on both the normal and the throw path). This makes
|
|
155
|
-
* release-on-throw LOCAL instead of relying on a distant outer catch.
|
|
156
|
-
*
|
|
157
|
-
* Behaviour matches the hand-rolled `acquired = tryAcquire(...) === "acquired";
|
|
158
|
-
* …run stage…; if (acquired) release` idiom it replaces:
|
|
159
|
-
* - When the lock is held and `skipIfLocked` is set, `tryAcquireProcessLock`
|
|
160
|
-
* returns `state: "skipped"` → the stage still runs (unlocked), nothing to release.
|
|
161
|
-
* - When the lock is held and `skipIfLocked` is NOT set, `tryAcquireProcessLock`
|
|
162
|
-
* throws (propagated here before `body` runs; nothing acquired, nothing released).
|
|
163
|
-
* - The process-exit backstop (`releaseHeldLocksIfOwned`) still covers a
|
|
164
|
-
* `process.exit` that skips this `finally`.
|
|
165
|
-
*/
|
|
166
|
-
export async function withOptionalProcessLock(opts, body) {
|
|
167
|
-
const acquisition = tryAcquireProcessLock(opts.lockPath, opts.staleAfterMs, opts.skipIfLocked, opts.label);
|
|
168
|
-
try {
|
|
169
|
-
return await body();
|
|
170
|
-
}
|
|
171
|
-
finally {
|
|
172
|
-
if (acquisition.state === "acquired")
|
|
173
|
-
releaseProcessLock(acquisition.ownership);
|
|
174
|
-
}
|
|
101
|
+
export function releaseImproveLock(ownership) {
|
|
102
|
+
releaseLock(ownership);
|
|
175
103
|
}
|
|
@@ -42,7 +42,7 @@ import { durableImproveRef } from "./source-identity.js";
|
|
|
42
42
|
// ── improve loop / post-loop / maintenance stages ───────────────────
|
|
43
43
|
// The cycle stages run by akmImprove, extracted from improve.ts.
|
|
44
44
|
export async function runImproveLoopStage(args) {
|
|
45
|
-
const { scope, options, primaryStashDir, reflectFn, distillFn, loopRefs, actions, signalBearingSet, distillCooledRefs, distillOnlyRefs, recentErrors, rejectedProposalsByRef, utilityMap, startMs, budgetMs, eventsCtx, improveProfile, resolvedPlan, } = args;
|
|
45
|
+
const { scope, options, primaryStashDir, reflectFn, distillFn, loopRefs, actions, signalBearingSet, distillCooledRefs, distillOnlyRefs, recentErrors, rejectedProposalsByRef, utilityMap, startMs, budgetMs, eventsCtx, improveProfile, resolvedPlan, budgetSignal, } = args;
|
|
46
46
|
// O-1 (#364): compute remaining budget at call time so each sub-call
|
|
47
47
|
// receives only its fair share of the wall-clock budget.
|
|
48
48
|
const remainingBudgetMs = () => Math.max(0, budgetMs - (Date.now() - startMs));
|
|
@@ -136,6 +136,7 @@ export async function runImproveLoopStage(args) {
|
|
|
136
136
|
stashDir: primaryStashDir,
|
|
137
137
|
config: options.config ?? loadConfig(),
|
|
138
138
|
eventsCtx,
|
|
139
|
+
targetSelector: options.target,
|
|
139
140
|
stateDbPath: eventsCtx?.dbPath,
|
|
140
141
|
// candidateCount drives the exploration budget. loopRefs is the per-phase
|
|
141
142
|
// set for reflect/distill; pass it so exploration budget is proportional.
|
|
@@ -147,6 +148,7 @@ export async function runImproveLoopStage(args) {
|
|
|
147
148
|
stashDir: primaryStashDir,
|
|
148
149
|
config: options.config ?? loadConfig(),
|
|
149
150
|
eventsCtx,
|
|
151
|
+
targetSelector: options.target,
|
|
150
152
|
stateDbPath: eventsCtx?.dbPath,
|
|
151
153
|
candidateCount: loopRefs.length,
|
|
152
154
|
});
|
|
@@ -211,7 +213,7 @@ export async function runImproveLoopStage(args) {
|
|
|
211
213
|
const reflectProfileRunner = resolvedPlan.processes.reflect.runner;
|
|
212
214
|
const reflectCallArgs = {
|
|
213
215
|
ref: planned.ref,
|
|
214
|
-
...(options.
|
|
216
|
+
...(options.sourceName ? { sourceName: options.sourceName } : {}),
|
|
215
217
|
...(options.legacyBareState ? { legacyBareState: true } : {}),
|
|
216
218
|
task: options.task,
|
|
217
219
|
// Active strategy supplies non-engine process tuning.
|
|
@@ -224,6 +226,7 @@ export async function runImproveLoopStage(args) {
|
|
|
224
226
|
// (default off when unset), so the running strategy decides.
|
|
225
227
|
lowValueFilter: improveProfile.processes?.reflect?.lowValueFilter?.enabled === true,
|
|
226
228
|
...(reflectBudgetMs > 0 ? { timeoutMs: reflectBudgetMs } : {}),
|
|
229
|
+
signal: budgetSignal,
|
|
227
230
|
runner: reflectProfileRunner ?? null,
|
|
228
231
|
// Attribution: carry the eligibility lane so reflect stamps it on
|
|
229
232
|
// the reflect_invoked event and the persisted proposal.
|
|
@@ -350,7 +353,7 @@ export async function runImproveLoopStage(args) {
|
|
|
350
353
|
// the asset changed; reset the counter so the dampener lifts.
|
|
351
354
|
if (isNoChange && eventsCtx?.db) {
|
|
352
355
|
try {
|
|
353
|
-
recordNoOp(eventsCtx.db, durableImproveRef(planned.ref, options.
|
|
356
|
+
recordNoOp(eventsCtx.db, durableImproveRef(planned.ref, options.sourceName));
|
|
354
357
|
}
|
|
355
358
|
catch {
|
|
356
359
|
// best-effort: plasticity counter failure never blocks the run
|
|
@@ -358,7 +361,7 @@ export async function runImproveLoopStage(args) {
|
|
|
358
361
|
}
|
|
359
362
|
else if (reflectResult.ok && eventsCtx?.db) {
|
|
360
363
|
try {
|
|
361
|
-
resetConsecutiveNoOps(eventsCtx.db, durableImproveRef(planned.ref, options.
|
|
364
|
+
resetConsecutiveNoOps(eventsCtx.db, durableImproveRef(planned.ref, options.sourceName));
|
|
362
365
|
}
|
|
363
366
|
catch {
|
|
364
367
|
// best-effort
|
|
@@ -485,7 +488,7 @@ export async function runImproveLoopStage(args) {
|
|
|
485
488
|
}
|
|
486
489
|
const distillResult = await withLlmStage("distill", () => distillFn({
|
|
487
490
|
ref: planned.ref,
|
|
488
|
-
...(options.
|
|
491
|
+
...(options.sourceName ? { sourceName: options.sourceName } : {}),
|
|
489
492
|
...(options.legacyBareState ? { legacyBareState: true } : {}),
|
|
490
493
|
...(parsedPlannedRef.type === "memory" ? { proposalKind: "auto" } : {}),
|
|
491
494
|
...(primaryStashDir ? { stashDir: primaryStashDir } : {}),
|
|
@@ -495,6 +498,7 @@ export async function runImproveLoopStage(args) {
|
|
|
495
498
|
llmConfig: resolvedPlan.processes.distill.runner
|
|
496
499
|
? materializeLlmRunnerConnection(resolvedPlan.processes.distill.runner)
|
|
497
500
|
: null,
|
|
501
|
+
signal: budgetSignal,
|
|
498
502
|
// Attribution: carry the eligibility lane so distill stamps it on the
|
|
499
503
|
// distill_invoked event and the persisted proposal.
|
|
500
504
|
...(planned.eligibilitySource ? { eligibilitySource: planned.eligibilitySource } : {}),
|
|
@@ -517,10 +521,10 @@ export async function runImproveLoopStage(args) {
|
|
|
517
521
|
if (eventsCtx?.db) {
|
|
518
522
|
try {
|
|
519
523
|
if (distillResult.outcome === "quality_rejected" || distillResult.outcome === "skipped") {
|
|
520
|
-
recordNoOp(eventsCtx.db, durableImproveRef(planned.ref, options.
|
|
524
|
+
recordNoOp(eventsCtx.db, durableImproveRef(planned.ref, options.sourceName));
|
|
521
525
|
}
|
|
522
526
|
else if (distillResult.outcome === "queued") {
|
|
523
|
-
resetConsecutiveNoOps(eventsCtx.db, durableImproveRef(planned.ref, options.
|
|
527
|
+
resetConsecutiveNoOps(eventsCtx.db, durableImproveRef(planned.ref, options.sourceName));
|
|
524
528
|
}
|
|
525
529
|
}
|
|
526
530
|
catch {
|
|
@@ -652,8 +656,7 @@ export async function runImprovePostLoopStage(args) {
|
|
|
652
656
|
}
|
|
653
657
|
}
|
|
654
658
|
// #609 — recombine / synthesize pass. Whole-corpus cross-episodic
|
|
655
|
-
// generalization. Runs in the post-loop stage under
|
|
656
|
-
// reads the consolidated corpus and writes proposals). Opt-in: gated on the
|
|
659
|
+
// generalization. Runs in the post-loop stage under the whole-run lock. Opt-in: gated on the
|
|
657
660
|
// `recombine` process being enabled, whole-stash / type scope (never `ref`),
|
|
658
661
|
// and not a dry run. Mirrors the proactiveMaintenance opt-in wiring.
|
|
659
662
|
let recombination;
|
|
@@ -773,6 +776,8 @@ export async function runImproveMaintenancePasses(args) {
|
|
|
773
776
|
const { options, primaryStashDir, memoryRefsForInference, allWarnings, reindexFn, consolidationRan, budgetSignal, eventsCtx, improveProfile, resolvedPlan, } = args;
|
|
774
777
|
if (!primaryStashDir)
|
|
775
778
|
return { memoryInferenceDurationMs: 0, graphExtractionDurationMs: 0 };
|
|
779
|
+
if (budgetSignal?.aborted)
|
|
780
|
+
return { memoryInferenceDurationMs: 0, graphExtractionDurationMs: 0 };
|
|
776
781
|
const config = options.config ?? loadConfig();
|
|
777
782
|
const sources = resolveSourceEntries(options.stashDir, config);
|
|
778
783
|
const memoryInferenceFn = options.memoryInferenceFn ?? runMemoryInferencePass;
|
|
@@ -799,7 +804,7 @@ export async function runImproveMaintenancePasses(args) {
|
|
|
799
804
|
db = undefined;
|
|
800
805
|
}
|
|
801
806
|
try {
|
|
802
|
-
await reindexFn({ stashDir });
|
|
807
|
+
await reindexFn({ stashDir, signal: budgetSignal });
|
|
803
808
|
}
|
|
804
809
|
finally {
|
|
805
810
|
db = openIndexDb();
|
|
@@ -283,6 +283,7 @@ export async function runConsolidationPass(args) {
|
|
|
283
283
|
stashDir: primaryStashDir,
|
|
284
284
|
config: consolidationConfig,
|
|
285
285
|
eventsCtx,
|
|
286
|
+
targetSelector: options.target,
|
|
286
287
|
stateDbPath: eventsCtx?.dbPath,
|
|
287
288
|
}, { minimumThreshold: 95 });
|
|
288
289
|
if (consolidateDisabledByProfile) {
|
|
@@ -413,7 +414,7 @@ export async function runConsolidationPass(args) {
|
|
|
413
414
|
* consolidation pass and accumulated here.
|
|
414
415
|
*/
|
|
415
416
|
async function runSessionExtractPass(args) {
|
|
416
|
-
const { options, primaryStashDir, improveProfile, resolvedPlan, eventsCtx, seedGateAccepted, seedGateFailed } = args;
|
|
417
|
+
const { options, primaryStashDir, improveProfile, resolvedPlan, eventsCtx, budgetSignal, seedGateAccepted, seedGateFailed, } = args;
|
|
417
418
|
const warnings = [];
|
|
418
419
|
// Phase 0.4 — session-extract pass.
|
|
419
420
|
//
|
|
@@ -446,6 +447,7 @@ async function runSessionExtractPass(args) {
|
|
|
446
447
|
stashDir: primaryStashDir,
|
|
447
448
|
config: extractConfig,
|
|
448
449
|
eventsCtx,
|
|
450
|
+
targetSelector: options.target,
|
|
449
451
|
stateDbPath: eventsCtx?.dbPath,
|
|
450
452
|
});
|
|
451
453
|
// #554 minNewSessions gate: skip the entire extract pass (ensureIndex was
|
|
@@ -525,6 +527,7 @@ async function runSessionExtractPass(args) {
|
|
|
525
527
|
config: extractConfig,
|
|
526
528
|
resolvedPlan: extractPlan,
|
|
527
529
|
dryRun: options.dryRun ?? false,
|
|
530
|
+
signal: budgetSignal,
|
|
528
531
|
...(options.extractHarnesses ? { harnesses: options.extractHarnesses } : {}),
|
|
529
532
|
// C2: pin extract's skip-tracking state.db open to the boundary path.
|
|
530
533
|
...(eventsCtx?.dbPath ? { stateDbPath: eventsCtx.dbPath } : {}),
|
|
@@ -699,6 +702,7 @@ export async function runImprovePreparationStage(args) {
|
|
|
699
702
|
improveProfile,
|
|
700
703
|
resolvedPlan,
|
|
701
704
|
eventsCtx,
|
|
705
|
+
budgetSignal,
|
|
702
706
|
seedGateAccepted: consolidationPass.gateAutoAcceptedCount,
|
|
703
707
|
seedGateFailed: consolidationPass.gateAutoAcceptFailedCount,
|
|
704
708
|
});
|
|
@@ -746,7 +750,7 @@ export async function runImprovePreparationStage(args) {
|
|
|
746
750
|
}
|
|
747
751
|
if ((appliedCleanup.archived.length > 0 || appliedCleanup.beliefStateTransitions.length > 0) && primaryStashDir) {
|
|
748
752
|
try {
|
|
749
|
-
await reindexFn({ stashDir: primaryStashDir });
|
|
753
|
+
await reindexFn({ stashDir: primaryStashDir, signal: budgetSignal });
|
|
750
754
|
}
|
|
751
755
|
catch (err) {
|
|
752
756
|
cleanupWarnings.push(`reindex after cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -826,9 +830,9 @@ export async function runImprovePreparationStage(args) {
|
|
|
826
830
|
// Per-ref queries would be N+1 and the planner is already the hottest path
|
|
827
831
|
// in `akm improve`.
|
|
828
832
|
const candidateRefs = postCleanupRefs.filter((r) => !validationFailureRefs.has(r.ref)).map((r) => r.ref);
|
|
829
|
-
const latestFeedbackTs = buildLatestFeedbackTsMap(candidateRefs, feedbackSinceCutoff, options.
|
|
830
|
-
const lastReflectProposalTs = buildLatestProposalTsMap(candidateRefs, "reflect", options.
|
|
831
|
-
const lastDistillProposalTs = buildLatestProposalTsMap(candidateRefs, "distill", options.
|
|
833
|
+
const latestFeedbackTs = buildLatestFeedbackTsMap(candidateRefs, feedbackSinceCutoff, options.sourceName, options.legacyBareState);
|
|
834
|
+
const lastReflectProposalTs = buildLatestProposalTsMap(candidateRefs, "reflect", options.sourceName, options.legacyBareState);
|
|
835
|
+
const lastDistillProposalTs = buildLatestProposalTsMap(candidateRefs, "distill", options.sourceName, options.legacyBareState);
|
|
832
836
|
// Refs the distill signal-delta gate rejected at planning time. The main
|
|
833
837
|
// loop reads this to skip distill for these refs without re-checking
|
|
834
838
|
// eligibility per iteration.
|
|
@@ -965,7 +969,7 @@ export async function runImprovePreparationStage(args) {
|
|
|
965
969
|
{
|
|
966
970
|
const feedbackCandidateRefs = [...processableRefs, ...noFeedbackPool].map((r) => r.ref);
|
|
967
971
|
const feedbackCandidateSet = new Set(feedbackCandidateRefs);
|
|
968
|
-
const feedbackRefByDurableKey = new Map(feedbackCandidateRefs.flatMap((ref) => improveStateReadRefs(ref, options.
|
|
972
|
+
const feedbackRefByDurableKey = new Map(feedbackCandidateRefs.flatMap((ref) => improveStateReadRefs(ref, options.sourceName, options.legacyBareState).map((key) => [key, ref])));
|
|
969
973
|
if (feedbackCandidateSet.size > 0) {
|
|
970
974
|
// Fetch ALL feedback events in one query (no ref filter, no since filter =
|
|
971
975
|
// single full table scan). Filtering per-ref in memory avoids N sequential
|
|
@@ -1036,9 +1040,9 @@ export async function runImprovePreparationStage(args) {
|
|
|
1036
1040
|
// Fix (WS-1 blocker 3): union the feedback pool into the lookup.
|
|
1037
1041
|
const allCandidateRefs = [...new Set([...signalFiltered, ...noFeedbackCandidates].map((r) => r.ref))];
|
|
1038
1042
|
retrievalCounts = getRetrievalCounts(dbForRetrieval, allCandidateRefs, {
|
|
1039
|
-
sourceName: options.
|
|
1043
|
+
sourceName: options.sourceName,
|
|
1040
1044
|
stashDir: primaryStashDir,
|
|
1041
|
-
includeLegacyBare: options.legacyBareState
|
|
1045
|
+
includeLegacyBare: options.legacyBareState,
|
|
1042
1046
|
});
|
|
1043
1047
|
lastUseMsForProactive = getLastUseMsByRef(dbForRetrieval, noFeedbackCandidates.map((r) => r.ref), primaryStashDir);
|
|
1044
1048
|
// High-retrieval signal-delta (simplified rule, 0.8.0): a no-feedback
|
|
@@ -1178,7 +1182,7 @@ export async function runImprovePreparationStage(args) {
|
|
|
1178
1182
|
// found later in the scan lost its slot to an earlier lower-scoring one.
|
|
1179
1183
|
const qualifying = [];
|
|
1180
1184
|
for (const r of candidates) {
|
|
1181
|
-
const row = readAssetSalienceForImproveRef(dbForHighSalience, r.ref, options.
|
|
1185
|
+
const row = readAssetSalienceForImproveRef(dbForHighSalience, r.ref, options.sourceName, options.legacyBareState);
|
|
1182
1186
|
if (row &&
|
|
1183
1187
|
isContentEncodingRow(row, parseAssetRef(r.ref).type) &&
|
|
1184
1188
|
row.encoding_salience >= salienceThreshold &&
|
|
@@ -1457,7 +1461,7 @@ export async function runImprovePreparationStage(args) {
|
|
|
1457
1461
|
withStateDb((dbForStoredEncoding) => {
|
|
1458
1462
|
for (const r of mergedRefs) {
|
|
1459
1463
|
const type = r.ref.includes(":") ? r.ref.slice(0, r.ref.indexOf(":")) : "";
|
|
1460
|
-
const row = readAssetSalienceForImproveRef(dbForStoredEncoding, r.ref, options.
|
|
1464
|
+
const row = readAssetSalienceForImproveRef(dbForStoredEncoding, r.ref, options.sourceName, options.legacyBareState);
|
|
1461
1465
|
if (row && isContentEncodingRow(row, type)) {
|
|
1462
1466
|
storedEncodingByRef.set(r.ref, row.encoding_salience);
|
|
1463
1467
|
}
|
|
@@ -1551,10 +1555,10 @@ export async function runImprovePreparationStage(args) {
|
|
|
1551
1555
|
//
|
|
1552
1556
|
// Load ALL existing rows so rank positions are stash-relative, not pool-relative.
|
|
1553
1557
|
const allStoredScores = getAllRankScores(stateDb);
|
|
1554
|
-
const existingAllScores = options.
|
|
1558
|
+
const existingAllScores = options.sourceName
|
|
1555
1559
|
? new Map([...allStoredScores].flatMap(([ref, score]) => {
|
|
1556
1560
|
const parsed = parseAssetRef(ref);
|
|
1557
|
-
return parsed.origin === options.
|
|
1561
|
+
return parsed.origin === options.sourceName || (options.legacyBareState && !parsed.origin)
|
|
1558
1562
|
? [[bareImproveRef(ref), score]]
|
|
1559
1563
|
: [];
|
|
1560
1564
|
}))
|
|
@@ -1655,7 +1659,7 @@ export async function runImprovePreparationStage(args) {
|
|
|
1655
1659
|
}, eventsCtx);
|
|
1656
1660
|
}
|
|
1657
1661
|
for (const [ref, vector] of salienceMap) {
|
|
1658
|
-
upsertAssetSalience(stateDb, durableImproveRef(ref, options.
|
|
1662
|
+
upsertAssetSalience(stateDb, durableImproveRef(ref, options.sourceName), vector, nowForSalience);
|
|
1659
1663
|
}
|
|
1660
1664
|
}, { path: eventsCtx?.dbPath });
|
|
1661
1665
|
}
|
|
@@ -1742,10 +1746,10 @@ export async function runImprovePreparationStage(args) {
|
|
|
1742
1746
|
withStateDb((replayDb) => {
|
|
1743
1747
|
const alreadyInPool = new Set(mergedRefs.map((r) => r.ref));
|
|
1744
1748
|
const storedRankScores = getAllRankScores(replayDb);
|
|
1745
|
-
const allRankScores = options.
|
|
1749
|
+
const allRankScores = options.sourceName
|
|
1746
1750
|
? new Map([...storedRankScores].flatMap(([ref, score]) => {
|
|
1747
1751
|
const parsed = parseAssetRef(ref);
|
|
1748
|
-
return parsed.origin === options.
|
|
1752
|
+
return parsed.origin === options.sourceName || (options.legacyBareState && !parsed.origin)
|
|
1749
1753
|
? [[bareImproveRef(ref), score]]
|
|
1750
1754
|
: [];
|
|
1751
1755
|
}))
|
|
@@ -1759,7 +1763,7 @@ export async function runImprovePreparationStage(args) {
|
|
|
1759
1763
|
for (const [ref, rankScore] of allRankScores) {
|
|
1760
1764
|
if (alreadyInPool.has(ref))
|
|
1761
1765
|
continue;
|
|
1762
|
-
const noOps = readConsecutiveNoOpsForImproveRef(replayDb, ref, options.
|
|
1766
|
+
const noOps = readConsecutiveNoOpsForImproveRef(replayDb, ref, options.sourceName, options.legacyBareState);
|
|
1763
1767
|
if (noOps >= SALIENCE_NO_OP_DAMPEN_THRESHOLD) {
|
|
1764
1768
|
convergedSkipped++;
|
|
1765
1769
|
continue;
|
|
@@ -1836,7 +1840,7 @@ export async function runImprovePreparationStage(args) {
|
|
|
1836
1840
|
const ownsNoOpDb = !eventsCtx?.db;
|
|
1837
1841
|
try {
|
|
1838
1842
|
for (const r of mergedRefs) {
|
|
1839
|
-
noOpMap.set(r.ref, readConsecutiveNoOpsForImproveRef(noOpDb, r.ref, options.
|
|
1843
|
+
noOpMap.set(r.ref, readConsecutiveNoOpsForImproveRef(noOpDb, r.ref, options.sourceName, options.legacyBareState));
|
|
1840
1844
|
}
|
|
1841
1845
|
}
|
|
1842
1846
|
finally {
|
|
@@ -86,13 +86,11 @@ export function selectProactiveMaintenanceRefs(params) {
|
|
|
86
86
|
return { selected, dueTotal, neverReflected, scored };
|
|
87
87
|
}
|
|
88
88
|
/**
|
|
89
|
-
*
|
|
89
|
+
* Pre-execution re-filter for proactive refs.
|
|
90
90
|
*
|
|
91
|
-
* Called
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* non-due (staleDays ≤ dueDays) since planning time are dropped before
|
|
95
|
-
* execution, closing the SELECT-time cooldown race.
|
|
91
|
+
* Called under the whole-run lock with freshly-read timestamp maps. Refs that
|
|
92
|
+
* became non-due before this run acquired the lock are dropped before execution,
|
|
93
|
+
* closing the SELECT-time cooldown race.
|
|
96
94
|
*
|
|
97
95
|
* The logic mirrors the DUE gate in `selectProactiveMaintenanceRefs` — an
|
|
98
96
|
* asset is due only when never touched OR last touched more than `dueDays`
|
|
@@ -45,7 +45,8 @@ import { chatCompletion } from "../../llm/client.js";
|
|
|
45
45
|
import { baseFailureFields, enoentHintMessage, isEnoentFailure } from "../agent/agent-support.js";
|
|
46
46
|
import { createProposal, isProposalSkipped, listProposals, } from "../proposal/repository.js";
|
|
47
47
|
import { checkReflectSize, isValidDescription } from "../proposal/validators/proposal-quality-validators.js";
|
|
48
|
-
import { deriveLessonRef
|
|
48
|
+
import { deriveLessonRef } from "./distill.js";
|
|
49
|
+
import { runReflectQualityJudge } from "./distill/quality-gate.js";
|
|
49
50
|
import { findAssetFilePath } from "./eligibility.js";
|
|
50
51
|
import { classifyReflectChange } from "./reflect-noise.js";
|
|
51
52
|
import { bareImproveRef, durableImproveRef } from "./source-identity.js";
|
|
@@ -663,8 +664,12 @@ export async function runReflectViaLlm(opts) {
|
|
|
663
664
|
try {
|
|
664
665
|
const stdout = await (opts.chat ?? chatCompletion)(opts.connection, messages, {
|
|
665
666
|
...(Object.hasOwn(opts, "timeoutMs") ? { timeoutMs: opts.timeoutMs } : {}),
|
|
667
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
666
668
|
...(opts.responseSchema !== undefined ? { responseSchema: opts.responseSchema } : {}),
|
|
667
669
|
...(opts.maxTokens !== undefined ? { maxTokens: opts.maxTokens } : {}),
|
|
670
|
+
// Reflect requires a machine-readable payload. Visible chain-of-thought
|
|
671
|
+
// can consume the output cap before the model reaches the JSON object.
|
|
672
|
+
enableThinking: false,
|
|
668
673
|
});
|
|
669
674
|
return {
|
|
670
675
|
ok: true,
|
|
@@ -931,6 +936,7 @@ export async function akmReflect(options = {}) {
|
|
|
931
936
|
prompt,
|
|
932
937
|
connection: spec.connection,
|
|
933
938
|
...(Object.hasOwn(opts, "timeoutMs") ? { timeoutMs: opts.timeoutMs } : {}),
|
|
939
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
934
940
|
priorDraft,
|
|
935
941
|
iteration: iter,
|
|
936
942
|
responseSchema: REFLECT_JSON_SCHEMA,
|
|
@@ -1100,72 +1106,7 @@ export async function akmReflect(options = {}) {
|
|
|
1100
1106
|
// caught downstream by createProposal; allow it to surface naturally.
|
|
1101
1107
|
}
|
|
1102
1108
|
}
|
|
1103
|
-
// 7.
|
|
1104
|
-
// Mirrors the lesson quality gate on distill proposals. The gate uses
|
|
1105
|
-
// `runLessonQualityJudge` from distill.ts and is gated behind either
|
|
1106
|
-
// `processes.reflect.qualityGate.enabled` or
|
|
1107
|
-
// `processes.distill.qualityGate.enabled` on the selected strategy.
|
|
1108
|
-
// Fail-CLOSED (07 P0-2): a judge error / no-LLM /
|
|
1109
|
-
// parse failure rejects the proposal rather than passing it through.
|
|
1110
|
-
// G-Eval (arXiv:2303.16634) — quality judgment before admission.
|
|
1111
|
-
const runtimeConfig = options.config ??
|
|
1112
|
-
(() => {
|
|
1113
|
-
try {
|
|
1114
|
-
return loadConfig();
|
|
1115
|
-
}
|
|
1116
|
-
catch {
|
|
1117
|
-
return undefined;
|
|
1118
|
-
}
|
|
1119
|
-
})();
|
|
1120
|
-
const chatFn = options.chat ?? chatCompletion;
|
|
1121
|
-
const qualityGateEnabled = (activeStrategy?.processes?.reflect?.qualityGate?.enabled ?? false) ||
|
|
1122
|
-
(activeStrategy?.processes?.distill?.qualityGate?.enabled ?? true);
|
|
1123
|
-
if (qualityGateEnabled && runtimeConfig) {
|
|
1124
|
-
const assetContent = (() => {
|
|
1125
|
-
if (!options.ref)
|
|
1126
|
-
return null;
|
|
1127
|
-
try {
|
|
1128
|
-
const refParsed = parseAssetRef(options.ref);
|
|
1129
|
-
const candidates = [
|
|
1130
|
-
path.join(stash, `${refParsed.type}s`, `${refParsed.name}.md`),
|
|
1131
|
-
path.join(stash, `${refParsed.type}s`, refParsed.name, "index.md"),
|
|
1132
|
-
];
|
|
1133
|
-
for (const p of candidates) {
|
|
1134
|
-
if (fs.existsSync(p))
|
|
1135
|
-
return fs.readFileSync(p, "utf8");
|
|
1136
|
-
}
|
|
1137
|
-
return null;
|
|
1138
|
-
}
|
|
1139
|
-
catch {
|
|
1140
|
-
return null;
|
|
1141
|
-
}
|
|
1142
|
-
})();
|
|
1143
|
-
const judgeResult = await runLessonQualityJudge(runtimeConfig, payload.content, assetContent ?? "", chatFn, undefined, runnerIsLlm(runnerSpec) ? materializeLlmRunnerConnection(runnerSpec) : undefined);
|
|
1144
|
-
if (!judgeResult.pass) {
|
|
1145
|
-
// Quality gate rejected the proposal — surface as parse_error so the
|
|
1146
|
-
// improve orchestrator can log it and move on without crashing.
|
|
1147
|
-
appendEvent({
|
|
1148
|
-
eventType: "reflect_completed",
|
|
1149
|
-
ref: payload.ref,
|
|
1150
|
-
metadata: {
|
|
1151
|
-
source: "reflect",
|
|
1152
|
-
qualityRejected: true,
|
|
1153
|
-
qualityScore: judgeResult.score,
|
|
1154
|
-
qualityReason: judgeResult.reason,
|
|
1155
|
-
},
|
|
1156
|
-
});
|
|
1157
|
-
return {
|
|
1158
|
-
schemaVersion: 2,
|
|
1159
|
-
ok: false,
|
|
1160
|
-
reason: "parse_error",
|
|
1161
|
-
error: `Reflect proposal quality gate rejected: score=${judgeResult.score}, reason="${judgeResult.reason}"`,
|
|
1162
|
-
...(options.ref ? { ref: options.ref } : {}),
|
|
1163
|
-
engine: engineName,
|
|
1164
|
-
exitCode: result.exitCode,
|
|
1165
|
-
};
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
// 7b. Reflect content-preservation rails:
|
|
1109
|
+
// 7. Reflect content-preservation rails:
|
|
1169
1110
|
// - Restore source frontmatter so reflect can never strip indexable
|
|
1170
1111
|
// fields (`description`, `when_to_use`, `tags`, ...).
|
|
1171
1112
|
// - Reset protected identity fields (`name`, `ref`, `id`, `slug`,
|
|
@@ -1241,6 +1182,38 @@ export async function akmReflect(options = {}) {
|
|
|
1241
1182
|
};
|
|
1242
1183
|
}
|
|
1243
1184
|
}
|
|
1185
|
+
// 7c. Judge the exact sanitized content that can be persisted. Fail closed
|
|
1186
|
+
// on cancellation, transport failure, malformed output, or an invalid score.
|
|
1187
|
+
const qualityGateEnabled = (activeStrategy?.processes?.reflect?.qualityGate?.enabled ?? false) ||
|
|
1188
|
+
(activeStrategy?.processes?.distill?.qualityGate?.enabled ?? true);
|
|
1189
|
+
if (qualityGateEnabled) {
|
|
1190
|
+
const judgeResult = await runReflectQualityJudge(config, payload.content, assetContent ?? "", feedback, options.chat ?? chatCompletion, {
|
|
1191
|
+
...(runnerIsLlm(runnerSpec) ? { llmConfig: materializeLlmRunnerConnection(runnerSpec) } : {}),
|
|
1192
|
+
...(Object.hasOwn(options, "timeoutMs") ? { timeoutMs: options.timeoutMs } : {}),
|
|
1193
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
1194
|
+
});
|
|
1195
|
+
if (!judgeResult.pass) {
|
|
1196
|
+
appendEvent({
|
|
1197
|
+
eventType: "reflect_completed",
|
|
1198
|
+
ref: payload.ref,
|
|
1199
|
+
metadata: {
|
|
1200
|
+
source: "reflect",
|
|
1201
|
+
qualityRejected: true,
|
|
1202
|
+
qualityScore: judgeResult.score,
|
|
1203
|
+
qualityReason: judgeResult.reason,
|
|
1204
|
+
},
|
|
1205
|
+
});
|
|
1206
|
+
return {
|
|
1207
|
+
schemaVersion: 2,
|
|
1208
|
+
ok: false,
|
|
1209
|
+
reason: "parse_error",
|
|
1210
|
+
error: `Reflect proposal quality gate rejected: score=${judgeResult.score}, reason="${judgeResult.reason}"`,
|
|
1211
|
+
...(options.ref ? { ref: options.ref } : {}),
|
|
1212
|
+
engine: engineName,
|
|
1213
|
+
exitCode: result.exitCode,
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1244
1217
|
// 8. Create the proposal. The proposal queue is the ONLY thing reflect
|
|
1245
1218
|
// writes — promotion to a real asset is gated by `akm proposal accept`.
|
|
1246
1219
|
//
|
|
@@ -67,12 +67,12 @@ export const DEFAULT_IMPROVE_TASKS = [
|
|
|
67
67
|
},
|
|
68
68
|
];
|
|
69
69
|
/**
|
|
70
|
-
* A schedule for the manual catch-up task. The scheduler requires a
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
70
|
+
* A schedule for the manual catch-up task. The scheduler requires a portable
|
|
71
|
+
* expression even when the task is registered disabled, so we give it a
|
|
72
|
+
* nominal daily cadence and leave it disabled. The documented entry point is
|
|
73
|
+
* `akm tasks run akm-improve-catchup`.
|
|
74
74
|
*/
|
|
75
|
-
const MANUAL_TASK_NOMINAL_SCHEDULE = "0 4
|
|
75
|
+
const MANUAL_TASK_NOMINAL_SCHEDULE = "0 4 * * *";
|
|
76
76
|
const DEFAULT_DEPS = {
|
|
77
77
|
list: akmTasksList,
|
|
78
78
|
add: akmTasksAdd,
|
|
@@ -136,11 +136,15 @@ const tasksRunCommand = defineCommand({
|
|
|
136
136
|
name: "run",
|
|
137
137
|
description: "Execute a task now (this is what cron / launchd / schtasks invoke at the scheduled time)",
|
|
138
138
|
},
|
|
139
|
-
args: {
|
|
139
|
+
args: {
|
|
140
|
+
id: { type: "positional", description: "Task id", required: true },
|
|
141
|
+
scheduled: { type: "boolean", description: "Internal marker for scheduler-generated runs", default: false },
|
|
142
|
+
},
|
|
140
143
|
async run({ args }) {
|
|
141
144
|
await runWithJsonErrors(async () => {
|
|
142
|
-
const
|
|
143
|
-
|
|
145
|
+
const envelope = await akmTasksRun(args.id, {
|
|
146
|
+
scheduled: args.scheduled === true,
|
|
147
|
+
});
|
|
144
148
|
output("tasks-run", envelope);
|
|
145
149
|
if (envelope.exitCode !== 0)
|
|
146
150
|
process.exit(envelope.exitCode);
|