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
|
@@ -30,9 +30,7 @@ import { akmDistill } from "./distill.js";
|
|
|
30
30
|
import { buildLatestProposalTsMap, collectEligibleRefs, collectEligibleRefsReadOnly, memoryCleanupParentRef, resolveImproveScope, shouldAnalyzeMemoryCleanup, } from "./eligibility.js";
|
|
31
31
|
import { countEvalCases } from "./eval-cases.js";
|
|
32
32
|
import { resolveImprovePlan } from "./improve-strategies.js";
|
|
33
|
-
|
|
34
|
-
// resetHeldProcessLocks is re-exported (the test seam imports it from here).
|
|
35
|
-
import { PROCESS_LOCK_DEFS, processLockPath, releaseAllProcessLocks, releaseHeldLocksIfOwned, releaseProcessLock, tryAcquireProcessLock, withOptionalProcessLock, } from "./locks.js";
|
|
33
|
+
import { improveLockPath, MIN_IMPROVE_LOCK_STALE_MS, releaseImproveLock, tryAcquireImproveLock } from "./locks.js";
|
|
36
34
|
// The cycle loop / post-loop / maintenance stages live in ./loop-stages.
|
|
37
35
|
import { runImproveLoopStage, runImprovePostLoopStage } from "./loop-stages.js";
|
|
38
36
|
import { detectAndWriteContradictions } from "./memory/memory-contradiction-detect.js";
|
|
@@ -43,7 +41,6 @@ import { DEFAULT_DUE_DAYS, filterProactiveDue } from "./proactive-maintenance.js
|
|
|
43
41
|
import { akmReflect } from "./reflect.js";
|
|
44
42
|
import { errMessage } from "./shared.js";
|
|
45
43
|
import { shouldReadLegacyBareImproveState } from "./source-identity.js";
|
|
46
|
-
export { resetHeldProcessLocks } from "./locks.js";
|
|
47
44
|
// Re-exported from ./loop-stages for test importers (improve-db-locking).
|
|
48
45
|
export { runImproveMaintenancePasses } from "./loop-stages.js";
|
|
49
46
|
// Re-exported from ./preparation so existing importers (tests, callers) resolve.
|
|
@@ -88,6 +85,15 @@ export function armBudgetWatchdog(budgetMs, controller, deps) {
|
|
|
88
85
|
};
|
|
89
86
|
}
|
|
90
87
|
export async function akmImprove(options = {}) {
|
|
88
|
+
const startMs = Date.now();
|
|
89
|
+
const budgetMs = options.timeoutMs ?? 2 * 60 * 60 * 1000;
|
|
90
|
+
const budgetAbortController = new AbortController();
|
|
91
|
+
Object.defineProperty(budgetAbortController.signal, "remainingBudgetMs", {
|
|
92
|
+
get: () => Math.max(0, budgetMs - (Date.now() - startMs)),
|
|
93
|
+
enumerable: false,
|
|
94
|
+
configurable: true,
|
|
95
|
+
});
|
|
96
|
+
let clearBudgetTimer = () => { };
|
|
91
97
|
const scope = resolveImproveScope(options.scope);
|
|
92
98
|
const reflectFn = options.reflectFn ?? akmReflect;
|
|
93
99
|
const distillFn = options.distillFn ?? akmDistill;
|
|
@@ -101,14 +107,7 @@ export async function akmImprove(options = {}) {
|
|
|
101
107
|
const runImprovePostLoopStageImpl = options.runImprovePostLoopStageFn ?? runImprovePostLoopStage;
|
|
102
108
|
// Resolve the improve profile for this run. Profile drives type filtering,
|
|
103
109
|
// process gating, and default autoAccept/limit values.
|
|
104
|
-
|
|
105
|
-
const writeTarget = options.writeTarget ??
|
|
106
|
-
(options.target
|
|
107
|
-
? resolveWriteTarget(_earlyConfig, options.target, { requireWritable: !options.dryRun })
|
|
108
|
-
: undefined);
|
|
109
|
-
if (writeTarget && options.target) {
|
|
110
|
-
_earlyConfig = { ..._earlyConfig, defaultWriteTarget: writeTarget.source.name };
|
|
111
|
-
}
|
|
110
|
+
const _earlyConfig = options.config ?? loadConfig();
|
|
112
111
|
const resolvedPlan = options.resolvedPlan ??
|
|
113
112
|
resolveImprovePlan(options.strategy, _earlyConfig, {
|
|
114
113
|
repairValidationFailures: options.repairValidationFailures,
|
|
@@ -116,6 +115,18 @@ export async function akmImprove(options = {}) {
|
|
|
116
115
|
const selectedStrategy = resolvedPlan.strategy;
|
|
117
116
|
const improveSensitiveValues = collectEngineCredentialValues(_earlyConfig);
|
|
118
117
|
const improveProfile = selectedStrategy.config;
|
|
118
|
+
const writeTarget = options.writeTarget ??
|
|
119
|
+
(options.target || _earlyConfig.defaultWriteTarget || !options.stashDir
|
|
120
|
+
? resolveWriteTarget(_earlyConfig, options.target, { requireWritable: !options.dryRun })
|
|
121
|
+
: {
|
|
122
|
+
source: { kind: "filesystem", name: "stash", path: options.stashDir },
|
|
123
|
+
config: {
|
|
124
|
+
type: "filesystem",
|
|
125
|
+
name: "stash",
|
|
126
|
+
path: options.stashDir,
|
|
127
|
+
writable: true,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
119
130
|
// Apply profile defaults — CLI flags take precedence over profile defaults.
|
|
120
131
|
// Rebuild options with effective values so all downstream stage functions
|
|
121
132
|
// automatically pick up the profile-driven defaults.
|
|
@@ -124,20 +135,17 @@ export async function akmImprove(options = {}) {
|
|
|
124
135
|
// Pin nested calls and quality gates to the same config snapshot as the
|
|
125
136
|
// invocation plan. They must never reload a changed config mid-run.
|
|
126
137
|
config: _earlyConfig,
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
},
|
|
137
|
-
}
|
|
138
|
-
: {}),
|
|
138
|
+
target: writeTarget.selector,
|
|
139
|
+
sourceName: writeTarget.source.name,
|
|
140
|
+
writeTarget,
|
|
141
|
+
stashDir: writeTarget.source.path,
|
|
142
|
+
consolidateOptions: {
|
|
143
|
+
...options.consolidateOptions,
|
|
144
|
+
target: writeTarget.selector,
|
|
145
|
+
writeTarget,
|
|
146
|
+
},
|
|
139
147
|
legacyBareState: options.legacyBareState ??
|
|
140
|
-
shouldReadLegacyBareImproveState(writeTarget
|
|
148
|
+
shouldReadLegacyBareImproveState(writeTarget.source.name, writeTarget.source.path, _earlyConfig),
|
|
141
149
|
autoAccept: options.autoAccept ?? improveProfile.autoAccept,
|
|
142
150
|
// Profile-level limit, then process-level reflect.limit as fallback.
|
|
143
151
|
// CLI --limit takes precedence over both.
|
|
@@ -154,7 +162,7 @@ export async function akmImprove(options = {}) {
|
|
|
154
162
|
primaryStashDir = undefined;
|
|
155
163
|
}
|
|
156
164
|
const syncRepoDir = writeTarget?.source.repoPath ?? primaryStashDir;
|
|
157
|
-
|
|
165
|
+
let initialGitPaths = new Set();
|
|
158
166
|
// C2 (#553/#554/#499): resolve the state.db path ONCE, synchronously, at the
|
|
159
167
|
// command boundary — before the first `await` below. Every state.db open in
|
|
160
168
|
// this run (`openStateDatabase`, every default-path `appendEvent`) is pinned
|
|
@@ -181,16 +189,11 @@ export async function akmImprove(options = {}) {
|
|
|
181
189
|
//
|
|
182
190
|
// The global tune call is intentionally removed here. See per-phase calls
|
|
183
191
|
// below (near each makeGateConfig / runAutoAcceptGate block).
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
// quick-shredder consolidate can run alongside daily reflect+distill.
|
|
187
|
-
//
|
|
188
|
-
// consolidate.lock — protects consolidate + memoryInference + graphExtraction (index.db writers)
|
|
189
|
-
// reflect-distill.lock — protects reflect + distill (state.db proposal writers)
|
|
190
|
-
// triage.lock — protects triage pre-pass (state.db proposal promotions)
|
|
191
|
-
//
|
|
192
|
-
// Lock base directory — same `.akm/` under the primary stash dir.
|
|
192
|
+
// One conservative run lock protects the complete mutation window, including
|
|
193
|
+
// triage, indexing, proposal work, maintenance, and final stash sync.
|
|
193
194
|
const lockBaseDir = primaryStashDir ? path.join(primaryStashDir, ".akm") : path.join(options.stashDir ?? ".", ".akm");
|
|
195
|
+
const resolvedLockPath = improveLockPath(lockBaseDir);
|
|
196
|
+
const lockStaleAfterMs = Math.max(MIN_IMPROVE_LOCK_STALE_MS, budgetMs + 10 * 60 * 1000);
|
|
194
197
|
const preEnsureCleanupWarnings = [];
|
|
195
198
|
// #616: assigned by runIndexAndCollect() (closure) so TS cannot prove definite
|
|
196
199
|
// assignment — seed with empty values; the first runIndexAndCollect() call
|
|
@@ -236,9 +239,11 @@ export async function akmImprove(options = {}) {
|
|
|
236
239
|
// best-effort; leave preEnsureEntryCount undefined
|
|
237
240
|
}
|
|
238
241
|
try {
|
|
239
|
-
await ensureIndexFn(primaryStashDir, { mode: "blocking" });
|
|
242
|
+
await ensureIndexFn(primaryStashDir, { mode: "blocking", signal: budgetAbortController.signal });
|
|
240
243
|
}
|
|
241
244
|
catch (err) {
|
|
245
|
+
if (budgetAbortController.signal.aborted)
|
|
246
|
+
throw err;
|
|
242
247
|
preEnsureCleanupWarnings.push(`ensureIndex failed: ${errMessage(err)}`);
|
|
243
248
|
}
|
|
244
249
|
// #339 loud-fail: if the index was empty pre-ensureIndex but is now
|
|
@@ -286,6 +291,8 @@ export async function akmImprove(options = {}) {
|
|
|
286
291
|
: null), { engine: resolvedPlan.processes.consolidate.runner?.engine, process: "consolidate" });
|
|
287
292
|
}
|
|
288
293
|
catch (err) {
|
|
294
|
+
if (budgetAbortController.signal.aborted)
|
|
295
|
+
throw err;
|
|
289
296
|
// Non-fatal: contradiction detection is a best-effort pass.
|
|
290
297
|
warn(`[improve] contradiction detection failed (non-fatal): ${errMessage(err)}`);
|
|
291
298
|
}
|
|
@@ -298,60 +305,74 @@ export async function akmImprove(options = {}) {
|
|
|
298
305
|
? "Improve folds memory cleanup into the same proposal queue: speculative promotions still go through reflect/distill proposals, while high-confidence redundant derived memories are moved into a recoverable cleanup archive instead of being left active in the stash."
|
|
299
306
|
: undefined;
|
|
300
307
|
};
|
|
301
|
-
|
|
302
|
-
// that handler (not every exit listener in the process). Declared in the scope
|
|
303
|
-
// shared by the try and its finally; assigned when the backstop is registered.
|
|
308
|
+
let improveLockOwnership;
|
|
304
309
|
let exitBackstop;
|
|
310
|
+
const releaseRunLock = () => {
|
|
311
|
+
const ownership = improveLockOwnership;
|
|
312
|
+
if (!ownership)
|
|
313
|
+
return;
|
|
314
|
+
improveLockOwnership = undefined;
|
|
315
|
+
try {
|
|
316
|
+
releaseImproveLock(ownership);
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
// Best-effort cleanup. Exact ownership prevents deleting a successor.
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
if (!options.dryRun) {
|
|
323
|
+
const remainingBudget = budgetAbortController.signal.remainingBudgetMs;
|
|
324
|
+
clearBudgetTimer = armBudgetWatchdog(Math.max(1, remainingBudget ?? budgetMs), budgetAbortController);
|
|
325
|
+
}
|
|
305
326
|
try {
|
|
306
|
-
// #607: Per-process lock acquisition. Each process acquires only the lock(s)
|
|
307
|
-
// it needs. The dry-run branch produces plannedRefs/memorySummary WITHOUT any
|
|
308
|
-
// locks (decision: dry-run never mutates the queue).
|
|
309
327
|
if (!options.dryRun) {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
328
|
+
const acquisition = tryAcquireImproveLock(resolvedLockPath, lockStaleAfterMs, options.skipIfLocked);
|
|
329
|
+
if (acquisition.state === "skipped") {
|
|
330
|
+
clearBudgetTimer();
|
|
331
|
+
return {
|
|
332
|
+
schemaVersion: 2,
|
|
333
|
+
ok: true,
|
|
334
|
+
strategy: selectedStrategy.name,
|
|
335
|
+
scope,
|
|
336
|
+
dryRun: false,
|
|
337
|
+
skipped: { reason: "lock-held" },
|
|
338
|
+
memorySummary: { eligible: 0, derived: 0 },
|
|
339
|
+
plannedRefs: [],
|
|
340
|
+
actions: [],
|
|
341
|
+
...(options.runId !== undefined ? { runId: options.runId } : {}),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
improveLockOwnership = acquisition.ownership;
|
|
345
|
+
exitBackstop = releaseRunLock;
|
|
346
|
+
process.on("exit", exitBackstop);
|
|
347
|
+
initialGitPaths =
|
|
348
|
+
syncRepoDir && isGitBackedStash(syncRepoDir) ? new Set(listGitChangedPaths(syncRepoDir)) : new Set();
|
|
349
|
+
// Drain the standing proposal backlog before indexing so fresh proposal
|
|
350
|
+
// generation sees promotions from this same serialized run.
|
|
320
351
|
if (primaryStashDir && resolvedPlan.processes.triage.enabled) {
|
|
321
352
|
if (scope.mode === "ref") {
|
|
322
353
|
warn("[improve] triage pre-pass skipped (single-ref scope never drains the whole queue)");
|
|
323
354
|
}
|
|
324
355
|
else {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
356
|
+
try {
|
|
357
|
+
const triageConfig = improveProfile.processes?.triage;
|
|
358
|
+
const policy = resolveDrainPolicy(triageConfig?.policy);
|
|
359
|
+
const applyMode = triageConfig?.applyMode ?? "queue";
|
|
360
|
+
const maxAccepts = triageConfig?.maxAcceptsPerRun ?? 25;
|
|
361
|
+
triageDrain = await drainProposalsFn({
|
|
362
|
+
stashDir: primaryStashDir,
|
|
363
|
+
...(options.target ? { target: options.target } : {}),
|
|
364
|
+
config: options.config,
|
|
365
|
+
policy,
|
|
366
|
+
applyMode,
|
|
367
|
+
maxAccepts,
|
|
368
|
+
dryRun: false,
|
|
369
|
+
excludeIds: new Set(),
|
|
370
|
+
...(triageConfig?.maxDiffLines !== undefined ? { maxDiffLines: triageConfig.maxDiffLines } : {}),
|
|
371
|
+
judgment: resolvedPlan.triageJudgment,
|
|
372
|
+
});
|
|
329
373
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const triageConfig = improveProfile.processes?.triage;
|
|
333
|
-
const policy = resolveDrainPolicy(triageConfig?.policy);
|
|
334
|
-
const applyMode = triageConfig?.applyMode ?? "queue";
|
|
335
|
-
const maxAccepts = triageConfig?.maxAcceptsPerRun ?? 25;
|
|
336
|
-
triageDrain = await drainProposalsFn({
|
|
337
|
-
stashDir: primaryStashDir,
|
|
338
|
-
...(options.target ? { target: options.target } : {}),
|
|
339
|
-
config: options.config,
|
|
340
|
-
policy,
|
|
341
|
-
applyMode,
|
|
342
|
-
maxAccepts,
|
|
343
|
-
dryRun: false,
|
|
344
|
-
excludeIds: new Set(),
|
|
345
|
-
...(triageConfig?.maxDiffLines !== undefined ? { maxDiffLines: triageConfig.maxDiffLines } : {}),
|
|
346
|
-
judgment: resolvedPlan.triageJudgment,
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
catch (err) {
|
|
350
|
-
warn(`[improve] triage pre-pass failed (non-fatal): ${errMessage(err)}`);
|
|
351
|
-
}
|
|
352
|
-
finally {
|
|
353
|
-
releaseProcessLock(triageResult.ownership);
|
|
354
|
-
}
|
|
374
|
+
catch (err) {
|
|
375
|
+
warn(`[improve] triage pre-pass failed (non-fatal): ${errMessage(err)}`);
|
|
355
376
|
}
|
|
356
377
|
}
|
|
357
378
|
}
|
|
@@ -373,39 +394,19 @@ export async function akmImprove(options = {}) {
|
|
|
373
394
|
plannedRefs,
|
|
374
395
|
...(strategyFilteredRefs.length > 0 ? { strategyFilteredRefs } : {}),
|
|
375
396
|
};
|
|
397
|
+
clearBudgetTimer();
|
|
376
398
|
return result;
|
|
377
399
|
}
|
|
378
400
|
}
|
|
379
401
|
catch (err) {
|
|
380
|
-
|
|
402
|
+
clearBudgetTimer();
|
|
403
|
+
if (exitBackstop) {
|
|
404
|
+
process.removeListener("exit", exitBackstop);
|
|
405
|
+
exitBackstop = undefined;
|
|
406
|
+
}
|
|
407
|
+
releaseRunLock();
|
|
381
408
|
throw err;
|
|
382
409
|
}
|
|
383
|
-
// #607: per-process locks are acquired/released around each stage below.
|
|
384
|
-
// The triage pre-pass already ran under triage.lock (released). The
|
|
385
|
-
// preparation stage runs under consolidate.lock, the loop stage under
|
|
386
|
-
// reflect-distill.lock, and the post-loop stage under consolidate.lock again.
|
|
387
|
-
// Each stage acquires its lock just before starting and releases in finally.
|
|
388
|
-
// best-effort `unlinkSync` is a no-op when no lock file exists.
|
|
389
|
-
const startMs = Date.now();
|
|
390
|
-
const budgetMs = options.timeoutMs ?? 2 * 60 * 60 * 1000; // default 2 hours
|
|
391
|
-
// O-1 (#364): Create a shared AbortController derived from startMs + budgetMs.
|
|
392
|
-
// Every async seam receives this signal so a hung sub-call cannot extend the
|
|
393
|
-
// run past the declared budget.
|
|
394
|
-
// References: Anthropic *Building Effective Agents* (2024); CoALA §5 (arXiv:2309.02427).
|
|
395
|
-
const budgetAbortController = new AbortController();
|
|
396
|
-
// Attach a live `remainingBudgetMs` getter to the signal so sub-callers
|
|
397
|
-
// (e.g. consolidate.ts cold-start budget estimation) can read the remaining
|
|
398
|
-
// wall-clock budget without needing an extra plumbing parameter. The property
|
|
399
|
-
// is computed at access time via a getter so it always reflects the actual
|
|
400
|
-
// elapsed time rather than a stale snapshot taken at arm time.
|
|
401
|
-
Object.defineProperty(budgetAbortController.signal, "remainingBudgetMs", {
|
|
402
|
-
get: () => Math.max(0, budgetMs - (Date.now() - startMs)),
|
|
403
|
-
enumerable: false,
|
|
404
|
-
configurable: true,
|
|
405
|
-
});
|
|
406
|
-
// Declared in the outer scope so the `finally` can clear the timer even if a
|
|
407
|
-
// throw occurs before/after it is armed. Defaults to a no-op until armed.
|
|
408
|
-
let clearBudgetTimer = () => { };
|
|
409
410
|
// I1: open a single state.db connection for the entire improve run so all
|
|
410
411
|
// appendEvent calls reuse one handle instead of open/migrate/close per call.
|
|
411
412
|
let eventsDb;
|
|
@@ -489,14 +490,6 @@ export async function akmImprove(options = {}) {
|
|
|
489
490
|
}
|
|
490
491
|
};
|
|
491
492
|
try {
|
|
492
|
-
// H7 (#566): arm the budget watchdog. `armBudgetWatchdog` captures both the
|
|
493
|
-
// budget timer and the hard-kill timer it schedules on exhaustion, returning
|
|
494
|
-
// a single dispose() that clears whichever are still pending. The `finally`
|
|
495
|
-
// calls dispose() via `clearBudgetTimer` (RAII), so a clean cooperative
|
|
496
|
-
// drain cancels the pending hard-kill before it can fire — the process then
|
|
497
|
-
// exits naturally instead of being force-`exit(0)`-ed mid-flush, which could
|
|
498
|
-
// truncate an in-flight log or `state.db` transaction.
|
|
499
|
-
clearBudgetTimer = armBudgetWatchdog(budgetMs, budgetAbortController);
|
|
500
493
|
try {
|
|
501
494
|
eventsDb = openStateDatabase(resolvedStateDbPath);
|
|
502
495
|
eventsCtx = { db: eventsDb };
|
|
@@ -533,9 +526,8 @@ export async function akmImprove(options = {}) {
|
|
|
533
526
|
// #616 — bounded multi-cycle phasing. The prep->loop->post-loop sequence is
|
|
534
527
|
// wrapped in an N-cycle loop. Each cycle re-runs ensureIndex +
|
|
535
528
|
// collectEligibleRefs (via runIndexAndCollect) so gate-accepted output of
|
|
536
|
-
// cycle N becomes selectable input to cycle N+1. The
|
|
537
|
-
//
|
|
538
|
-
// exactly as the single-pass path did. For maxCycles:1 the loop runs once and
|
|
529
|
+
// cycle N becomes selectable input to cycle N+1. The whole sequence runs
|
|
530
|
+
// under the invocation's single lock. For maxCycles:1 the loop runs once and
|
|
539
531
|
// every accumulator below collapses to the single-cycle value (sum-of-one,
|
|
540
532
|
// concat-of-one, last==only) => BYTE-IDENTICAL to pre-#616.
|
|
541
533
|
//
|
|
@@ -606,15 +598,7 @@ export async function akmImprove(options = {}) {
|
|
|
606
598
|
}, eventsCtx);
|
|
607
599
|
}
|
|
608
600
|
}
|
|
609
|
-
|
|
610
|
-
// ensureIndex, extract all write index.db). Released immediately after.
|
|
611
|
-
const consolidateLPath = processLockPath(lockBaseDir, "consolidate");
|
|
612
|
-
preparation = await withOptionalProcessLock({
|
|
613
|
-
lockPath: consolidateLPath,
|
|
614
|
-
staleAfterMs: PROCESS_LOCK_DEFS.consolidate.staleAfterMs,
|
|
615
|
-
skipIfLocked: options.skipIfLocked,
|
|
616
|
-
label: "consolidate",
|
|
617
|
-
}, () => runImprovePreparationStageImpl({
|
|
601
|
+
const runPreparation = () => runImprovePreparationStageImpl({
|
|
618
602
|
scope,
|
|
619
603
|
options,
|
|
620
604
|
plannedRefs,
|
|
@@ -630,7 +614,8 @@ export async function akmImprove(options = {}) {
|
|
|
630
614
|
resolvedPlan,
|
|
631
615
|
strategyName: selectedStrategy.name,
|
|
632
616
|
budgetSignal: budgetAbortController.signal,
|
|
633
|
-
})
|
|
617
|
+
});
|
|
618
|
+
preparation = await runPreparation();
|
|
634
619
|
prepGateCount += preparation.gateAutoAcceptedCount;
|
|
635
620
|
prepGateFailedCount += preparation.gateAutoAcceptFailedCount;
|
|
636
621
|
// D6: pre-load all proposal_rejected events from the last 30 days once,
|
|
@@ -644,26 +629,15 @@ export async function akmImprove(options = {}) {
|
|
|
644
629
|
rejectedProposalsByRef.set(e.ref, e);
|
|
645
630
|
}
|
|
646
631
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
const loopResult = await withOptionalProcessLock({
|
|
651
|
-
lockPath: reflectDistillLPath,
|
|
652
|
-
staleAfterMs: PROCESS_LOCK_DEFS.reflectDistill.staleAfterMs,
|
|
653
|
-
skipIfLocked: options.skipIfLocked,
|
|
654
|
-
label: "reflect-distill",
|
|
655
|
-
}, () => {
|
|
656
|
-
// Post-lock cooldown re-filter for proactive refs (#SELECT-TIME-LEAK).
|
|
657
|
-
// Planning built `lastReflectProposalTs` BEFORE acquiring this lock, so a
|
|
658
|
-
// concurrent run's `reflect_invoked` writes are invisible to it. Now that
|
|
659
|
-
// we hold the lock, re-read fresh timestamp maps for the proactive subset
|
|
660
|
-
// and drop any ref whose cooldown has been consumed by the concurrent run.
|
|
632
|
+
const runLoop = () => {
|
|
633
|
+
// Re-read cooldown timestamps immediately before execution so external
|
|
634
|
+
// proposal writes that occurred before this run acquired its lock are visible.
|
|
661
635
|
const proactiveLoopRefs = preparation.loopRefs.filter((r) => r.eligibilitySource === "proactive");
|
|
662
636
|
let postLockLoopRefs = preparation.loopRefs;
|
|
663
637
|
if (proactiveLoopRefs.length > 0) {
|
|
664
638
|
const proactiveRefStrs = proactiveLoopRefs.map((r) => r.ref);
|
|
665
|
-
const freshReflectTs = buildLatestProposalTsMap(proactiveRefStrs, "reflect", options.
|
|
666
|
-
const freshDistillTs = buildLatestProposalTsMap(proactiveRefStrs, "distill", options.
|
|
639
|
+
const freshReflectTs = buildLatestProposalTsMap(proactiveRefStrs, "reflect", options.sourceName, options.legacyBareState);
|
|
640
|
+
const freshDistillTs = buildLatestProposalTsMap(proactiveRefStrs, "distill", options.sourceName, options.legacyBareState);
|
|
667
641
|
const pmDueDays = improveProfile.processes?.proactiveMaintenance?.dueDays ?? DEFAULT_DUE_DAYS;
|
|
668
642
|
const stillDue = new Set(filterProactiveDue(proactiveLoopRefs, freshReflectTs, freshDistillTs, pmDueDays, Date.now()).map((r) => r.ref));
|
|
669
643
|
const dropped = proactiveLoopRefs.filter((r) => !stillDue.has(r.ref));
|
|
@@ -693,7 +667,8 @@ export async function akmImprove(options = {}) {
|
|
|
693
667
|
resolvedPlan,
|
|
694
668
|
budgetSignal: budgetAbortController.signal,
|
|
695
669
|
});
|
|
696
|
-
}
|
|
670
|
+
};
|
|
671
|
+
const loopResult = await runLoop();
|
|
697
672
|
const loopGateCountThisCycle = loopResult.gateAutoAcceptedCount;
|
|
698
673
|
reflectsWithErrorContext += loopResult.reflectsWithErrorContext;
|
|
699
674
|
loopGateCount += loopResult.gateAutoAcceptedCount;
|
|
@@ -702,33 +677,42 @@ export async function akmImprove(options = {}) {
|
|
|
702
677
|
// #551: consolidation now runs in the preparation stage (before extract);
|
|
703
678
|
// its result and run-flag are read from `preparation`, not the post-loop.
|
|
704
679
|
consolidation = preparation.consolidation;
|
|
705
|
-
//
|
|
706
|
-
//
|
|
707
|
-
const
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
680
|
+
// Do not start new post-loop work after the shared wall-clock budget. The
|
|
681
|
+
// result still finalizes normally, so scheduled budget exhaustion exits 0.
|
|
682
|
+
const emptyPostLoopResult = {
|
|
683
|
+
allWarnings: [],
|
|
684
|
+
gateAutoAcceptedCount: 0,
|
|
685
|
+
gateAutoAcceptFailedCount: 0,
|
|
686
|
+
memoryInferenceDurationMs: 0,
|
|
687
|
+
graphExtractionDurationMs: 0,
|
|
688
|
+
};
|
|
689
|
+
const remainingBudget = budgetAbortController.signal.remainingBudgetMs;
|
|
690
|
+
let postLoopResult;
|
|
691
|
+
if (budgetAbortController.signal.aborted || (remainingBudget !== undefined && remainingBudget <= 0)) {
|
|
692
|
+
info("[improve] post-loop maintenance skipped (wall-clock budget exhausted)");
|
|
693
|
+
postLoopResult = emptyPostLoopResult;
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
postLoopResult = await runImprovePostLoopStageImpl({
|
|
697
|
+
scope,
|
|
698
|
+
options,
|
|
699
|
+
primaryStashDir,
|
|
700
|
+
actionableRefs: preparation.actionableRefs,
|
|
701
|
+
appliedCleanup: preparation.appliedCleanup,
|
|
702
|
+
cleanupWarnings: preparation.cleanupWarnings,
|
|
703
|
+
memoryRefsForInference,
|
|
704
|
+
reindexFn,
|
|
705
|
+
eventsCtx,
|
|
706
|
+
budgetSignal: budgetAbortController.signal,
|
|
707
|
+
improveProfile,
|
|
708
|
+
resolvedPlan,
|
|
709
|
+
consolidationRan: preparation.consolidationRan,
|
|
710
|
+
// R5: floor violations from this run's consolidate pass + the
|
|
711
|
+
// auto-accepted volume so far (prep + loop gates) for churn detection.
|
|
712
|
+
consolidationMergeFloorViolations: preparation.consolidation.mergeFloorViolations ?? 0,
|
|
713
|
+
acceptedActions: preparation.gateAutoAcceptedCount + loopGateCountThisCycle,
|
|
714
|
+
});
|
|
715
|
+
}
|
|
732
716
|
const postLoopGateCountThisCycle = postLoopResult.gateAutoAcceptedCount;
|
|
733
717
|
// Last-wins point-in-time objects.
|
|
734
718
|
memoryInference = postLoopResult.memoryInference;
|
|
@@ -916,9 +900,6 @@ export async function akmImprove(options = {}) {
|
|
|
916
900
|
// O-1 (#364): Clear the budget abort timer so it does not keep the event
|
|
917
901
|
// loop alive after the run completes.
|
|
918
902
|
clearBudgetTimer();
|
|
919
|
-
// #607: release any per-process locks still held (backstop for error paths;
|
|
920
|
-
// the normal path already released each lock after its stage completed).
|
|
921
|
-
releaseAllProcessLocks();
|
|
922
903
|
// Drop ONLY our own process.exit backstop so it does not fire later (or
|
|
923
904
|
// accumulate across repeated in-process calls). Must NOT use
|
|
924
905
|
// removeAllListeners("exit") here: in the in-process model (tests and
|
|
@@ -928,6 +909,7 @@ export async function akmImprove(options = {}) {
|
|
|
928
909
|
process.removeListener("exit", exitBackstop);
|
|
929
910
|
exitBackstop = undefined;
|
|
930
911
|
}
|
|
912
|
+
releaseRunLock();
|
|
931
913
|
// I1: close the long-lived state.db connection opened at the top of the run.
|
|
932
914
|
try {
|
|
933
915
|
eventsDb?.close();
|