akm-cli 0.9.0-beta.2 → 0.9.0-beta.26
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 +614 -0
- package/dist/assets/prompts/consolidate-system.md +23 -0
- package/dist/assets/prompts/contradiction-judge.md +33 -0
- package/dist/assets/prompts/distill-knowledge-system.md +22 -0
- package/dist/assets/prompts/distill-lesson-system.md +36 -0
- package/dist/assets/prompts/extract-session.md +5 -1
- package/dist/assets/prompts/graph-extract-system.md +1 -0
- package/dist/assets/prompts/memory-infer-system.md +1 -0
- package/dist/assets/prompts/memory-infer-user.md +5 -0
- package/dist/assets/prompts/metadata-enhance-system.md +1 -0
- package/dist/assets/prompts/procedural-system.md +44 -0
- package/dist/assets/prompts/recombine-system.md +40 -0
- package/dist/assets/prompts/staleness-detect-system.md +6 -0
- package/dist/assets/prompts/validate-summary-judge.md +1 -0
- package/dist/assets/templates/html/default.html +78 -0
- package/dist/assets/templates/html/health.html +730 -0
- package/dist/assets/templates/html/vendor/echarts.min.js +45 -0
- package/dist/cli/shared.js +21 -5
- package/dist/cli.js +47 -5
- package/dist/commands/agent/contribute-cli.js +16 -3
- package/dist/commands/feedback-cli.js +15 -6
- package/dist/commands/graph/graph.js +75 -71
- package/dist/commands/health/checks.js +48 -0
- package/dist/commands/health/html-report.js +790 -0
- package/dist/commands/health.js +478 -15
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/consolidate.js +634 -111
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill.js +145 -69
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +115 -1
- package/dist/commands/improve/extract-prompt.js +33 -2
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +280 -35
- package/dist/commands/improve/feedback-valence.js +54 -0
- package/dist/commands/improve/homeostatic.js +467 -0
- package/dist/commands/improve/improve-auto-accept.js +139 -6
- package/dist/commands/improve/improve-profiles.js +12 -0
- package/dist/commands/improve/improve.js +1851 -515
- package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
- package/dist/commands/improve/outcome-loop.js +256 -0
- package/dist/commands/improve/proactive-maintenance.js +87 -0
- package/dist/commands/improve/procedural.js +409 -0
- package/dist/commands/improve/recombine.js +488 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +51 -1
- package/dist/commands/improve/related-sessions.js +120 -0
- package/dist/commands/improve/salience.js +386 -0
- package/dist/commands/improve/triage.js +95 -0
- package/dist/commands/lint/agent-linter.js +19 -24
- package/dist/commands/lint/base-linter.js +173 -60
- package/dist/commands/lint/command-linter.js +19 -24
- package/dist/commands/lint/env-key-rules.js +34 -1
- package/dist/commands/lint/index.js +30 -13
- package/dist/commands/lint/memory-linter.js +1 -1
- package/dist/commands/lint/registry.js +5 -2
- package/dist/commands/lint/task-linter.js +3 -3
- package/dist/commands/lint/workflow-linter.js +26 -1
- package/dist/commands/proposal/drain.js +73 -6
- package/dist/commands/proposal/proposal-cli.js +22 -10
- package/dist/commands/proposal/proposal.js +17 -1
- package/dist/commands/proposal/validators/proposals.js +369 -329
- package/dist/commands/read/curate.js +294 -79
- package/dist/commands/read/search-cli.js +7 -0
- package/dist/commands/read/search.js +1 -0
- package/dist/commands/remember.js +6 -2
- package/dist/commands/sources/installed-stashes.js +5 -1
- package/dist/commands/sources/stash-cli.js +10 -2
- package/dist/core/asset/frontmatter.js +166 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/config/config-schema.js +241 -0
- package/dist/core/config/config.js +2 -2
- package/dist/core/logs-db.js +305 -0
- package/dist/core/paths.js +3 -0
- package/dist/core/state-db.js +706 -42
- package/dist/indexer/db/db.js +347 -38
- package/dist/indexer/db/graph-db.js +81 -86
- package/dist/indexer/ensure-index.js +152 -17
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/index-writer-lock.js +99 -0
- package/dist/indexer/indexer.js +114 -111
- package/dist/indexer/passes/memory-inference.js +71 -25
- package/dist/indexer/passes/staleness-detect.js +2 -5
- package/dist/indexer/search/db-search.js +15 -4
- package/dist/indexer/search/ranking.js +4 -0
- package/dist/integrations/harnesses/claude/session-log.js +27 -5
- package/dist/integrations/harnesses/opencode/session-log.js +9 -0
- package/dist/integrations/session-logs/index.js +16 -0
- package/dist/llm/client.js +38 -4
- package/dist/llm/embedder.js +27 -3
- package/dist/llm/embedders/local.js +66 -2
- package/dist/llm/graph-extract.js +2 -1
- package/dist/llm/memory-infer.js +4 -8
- package/dist/llm/metadata-enhance.js +9 -1
- package/dist/llm/usage-persist.js +77 -0
- package/dist/llm/usage-telemetry.js +103 -0
- package/dist/output/context.js +3 -2
- package/dist/output/html-render.js +73 -0
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/shapes/helpers.js +17 -1
- package/dist/output/text/helpers.js +78 -1
- package/dist/runtime.js +25 -1
- package/dist/scripts/migrate-storage.js +1194 -607
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +455 -270
- package/dist/sources/providers/tar-utils.js +16 -8
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/tasks/runner.js +99 -16
- package/dist/workflows/db.js +5 -2
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/data-and-telemetry.md +1 -0
- package/package.json +7 -5
|
@@ -133,24 +133,188 @@ export const ImproveProcessConfigSchema = z
|
|
|
133
133
|
// consolidation pass skips entirely (emits `pool_below_min_size`). 0 disables
|
|
134
134
|
// the guard. Only meaningful on the `consolidate` process. Default 500.
|
|
135
135
|
minPoolSize: z.number().int().min(0).optional(),
|
|
136
|
+
// Consolidate process: deterministic near-duplicate dedup pre-pass (#617).
|
|
137
|
+
// A cheap, no-LLM fast path that collapses obvious duplicates (`.derived`
|
|
138
|
+
// origin pairs + content twins) before the LLM consolidation. Default OFF
|
|
139
|
+
// — when absent the consolidate pass behaves byte-identically to today.
|
|
140
|
+
// `cosineThreshold` is a strict floor in [0, 1] (default 0.97) for the
|
|
141
|
+
// optional embedding-similarity match; exact normalized content-hash
|
|
142
|
+
// equality always collapses regardless of the threshold. Only meaningful
|
|
143
|
+
// on the `consolidate` process.
|
|
144
|
+
dedup: z
|
|
145
|
+
.object({
|
|
146
|
+
enabled: z.boolean().optional(),
|
|
147
|
+
cosineThreshold: z.number().min(0).max(1).optional(),
|
|
148
|
+
// WS-3a: maximum pool size for the O(n²) cosine-similarity twin compare.
|
|
149
|
+
// Only the first `cosineCandidateLimit` memories are cosine-compared;
|
|
150
|
+
// exact-hash matches still run over the full pool. Default 500. Raise
|
|
151
|
+
// with care — cost is O(n²).
|
|
152
|
+
cosineCandidateLimit: z.number().int().positive().optional(),
|
|
153
|
+
})
|
|
154
|
+
.strict()
|
|
155
|
+
.optional(),
|
|
156
|
+
// Consolidate process: judged-state cache (#581). When enabled, a memory
|
|
157
|
+
// whose current content hash equals its cached judged hash is SKIPPED from
|
|
158
|
+
// the LLM pool (judged-unchanged → no re-judge), letting one run sweep the
|
|
159
|
+
// whole corpus at O(changed/new) cost instead of narrowing to a recent
|
|
160
|
+
// time-window slice. Default OFF — when absent the consolidate pass behaves
|
|
161
|
+
// byte-identically to today (the incrementalSince path is unaffected). Only
|
|
162
|
+
// meaningful on the `consolidate` process.
|
|
163
|
+
judgedCache: z.object({ enabled: z.boolean().optional() }).strict().optional(),
|
|
136
164
|
qualityGate: z.object({ enabled: z.boolean().optional() }).strict().optional(),
|
|
137
165
|
contradictionDetection: z.object({ enabled: z.boolean().optional() }).strict().optional(),
|
|
138
166
|
// Extract process config (only meaningful for extract process)
|
|
139
167
|
defaultSince: z.string().min(1).optional(),
|
|
140
168
|
maxTotalChars: positiveInt.optional(),
|
|
169
|
+
// Extract process: minimum raw session size (pre-filter inputCount) below
|
|
170
|
+
// which the extract LLM call is skipped (#595/#596). 0 disables the gate.
|
|
171
|
+
// Absent = default 10 (skip only truly empty sessions). Only meaningful
|
|
172
|
+
// on the `extract` process.
|
|
173
|
+
minContentChars: z.number().int().min(0).optional(),
|
|
141
174
|
maxChunkSize: z.number().int().min(1).max(50).optional(),
|
|
175
|
+
// Consolidate process: narrow candidate pool to memories modified within
|
|
176
|
+
// this duration window plus their graph neighbours. Only meaningful on
|
|
177
|
+
// the `consolidate` process. Absent = full-pool sweep.
|
|
178
|
+
incrementalSince: z.string().optional(),
|
|
179
|
+
// Consolidate process: hard cap on memories processed per pass.
|
|
180
|
+
// Reflect/distill: max refs processed (same as profile-level `limit`).
|
|
181
|
+
limit: positiveInt.optional(),
|
|
182
|
+
// Consolidate process: graph neighbours per changed memory during
|
|
183
|
+
// incremental consolidation. Default 5. Only meaningful with incrementalSince.
|
|
184
|
+
neighborsPerChanged: z.number().int().min(1).optional(),
|
|
185
|
+
// Distill process: skip distill entirely when reflect produced zero planned refs.
|
|
186
|
+
requirePlannedRefs: z.boolean().optional(),
|
|
187
|
+
// proactiveMaintenance process (Layer 2): staleness gate + rotation cooldown
|
|
188
|
+
// in days (default 30). Only meaningful on `proactiveMaintenance`.
|
|
189
|
+
dueDays: z.number().int().min(0).optional(),
|
|
190
|
+
// proactiveMaintenance process: top-N bound per run (default 25). Alias for
|
|
191
|
+
// `limit`; `maxPerRun` wins when both are set.
|
|
192
|
+
maxPerRun: positiveInt.optional(),
|
|
193
|
+
// MemoryInference process: minimum pending memory count to run the pass.
|
|
194
|
+
minPendingCount: z.number().int().min(0).optional(),
|
|
142
195
|
// Extract process: minimum number of new (unseen, in-window) candidate
|
|
143
196
|
// sessions below which the extract pass skips entirely (emits an
|
|
144
197
|
// `improve_skipped` event with `reason: "below_min_new_sessions"`). 0
|
|
145
198
|
// disables the guard. Only meaningful on the `extract` process. Default 0
|
|
146
199
|
// (disabled) so existing behaviour is preserved; only opted-in profiles set it.
|
|
147
200
|
minNewSessions: z.number().int().min(0).optional(),
|
|
201
|
+
// Extract process: cap on NEW sessions processed (LLM-called) per run; the
|
|
202
|
+
// rest roll to the next run (still unseen). 0 disables. Absent = default 25.
|
|
203
|
+
maxSessionsPerRun: z.number().int().min(0).optional(),
|
|
148
204
|
// #561 — index agent sessions as a searchable `session` asset (extract
|
|
149
205
|
// process). Absent = on-when-an-LLM-is-available (fail-open when offline).
|
|
150
206
|
indexSessions: z.boolean().optional(),
|
|
151
207
|
// #561 — minimum session duration in minutes for session indexing. 0
|
|
152
208
|
// disables the gate. Absent = default 5. Only meaningful on `extract`.
|
|
153
209
|
minSessionDuration: z.number().min(0).optional(),
|
|
210
|
+
// Consolidate process: fallback p90 wall-clock time per consolidation chunk
|
|
211
|
+
// in seconds, used for cold-start budget estimation when no telemetry
|
|
212
|
+
// history exists. The actual p90 is derived from observed run durations
|
|
213
|
+
// once sufficient history accumulates; this value is only used on the very
|
|
214
|
+
// first run. Default 30 s. Only meaningful on the `consolidate` process.
|
|
215
|
+
p90ChunkSecondsDefault: z.number().finite().positive().optional(),
|
|
216
|
+
// WS-3b: Homeostatic demotion (step 0a). Before any LLM merge, demote
|
|
217
|
+
// retrievalSalience for stale/low-value assets so the merge pool is bounded
|
|
218
|
+
// and high-SNR. Demotion is state.db-only (file content untouched);
|
|
219
|
+
// re-promotable on re-retrieval. Default OFF. Only meaningful on the
|
|
220
|
+
// `consolidate` process.
|
|
221
|
+
homeostaticDemotion: z
|
|
222
|
+
.object({
|
|
223
|
+
enabled: z.boolean().optional(),
|
|
224
|
+
// Minimum days since last retrieval to consider an asset stale (default 30).
|
|
225
|
+
staleDays: z.number().int().min(0).optional(),
|
|
226
|
+
// Demotion factor: multiply retrievalSalience by this when stale (default 0.5).
|
|
227
|
+
demotionFactor: z.number().min(0).max(1).optional(),
|
|
228
|
+
})
|
|
229
|
+
.strict()
|
|
230
|
+
.optional(),
|
|
231
|
+
// WS-3b: Schema-similarity gate (step 0b). At intake, if a new candidate's
|
|
232
|
+
// body embedding is within epsilon of an existing derived-layer lesson/knowledge
|
|
233
|
+
// node, mark it schema-consistent and lower its priority. Default OFF.
|
|
234
|
+
// Only meaningful on the `consolidate` and `extract` processes.
|
|
235
|
+
schemaSimilarity: z
|
|
236
|
+
.object({
|
|
237
|
+
enabled: z.boolean().optional(),
|
|
238
|
+
// Epsilon: cosine similarity threshold above which a candidate is schema-consistent
|
|
239
|
+
// (default 0.85 — looser than dedup's 0.97 since we want to catch conceptual overlap).
|
|
240
|
+
epsilon: z.number().min(0).max(1).optional(),
|
|
241
|
+
// Multiplicative factor applied to candidate confidence when schema-consistent.
|
|
242
|
+
// Default 0.5 — halves the confidence so schema-consistent candidates are less likely
|
|
243
|
+
// to pass the quality gate and create redundant stash entries.
|
|
244
|
+
confidencePenalty: z.number().min(0).max(1).optional(),
|
|
245
|
+
})
|
|
246
|
+
.strict()
|
|
247
|
+
.optional(),
|
|
248
|
+
// WS-3b: Hot-probation intake buffer (step 0c, #604). New system-generated
|
|
249
|
+
// extractions enter captureMode: hot-probation and spend ONE consolidation
|
|
250
|
+
// cycle in probation. Dedup + quality second-pass runs before promotion.
|
|
251
|
+
// Default OFF. Only meaningful on the `extract` process.
|
|
252
|
+
hotProbation: z
|
|
253
|
+
.object({
|
|
254
|
+
enabled: z.boolean().optional(),
|
|
255
|
+
})
|
|
256
|
+
.strict()
|
|
257
|
+
.optional(),
|
|
258
|
+
// WS-3b: Anti-collapse guards (step 8). Prevents the consolidation pipeline
|
|
259
|
+
// from collapsing too aggressively and losing diversity.
|
|
260
|
+
// - maxGeneration: refuse to merge two assets both above this generation (default 2).
|
|
261
|
+
// - lexicalDiversityCheck: low n-gram diversity ⇒ raise merge threshold.
|
|
262
|
+
// - randomClusterFraction: occasional random (non-similar) cluster in pool (default 0.05).
|
|
263
|
+
// Default OFF. Only meaningful on the `consolidate` process.
|
|
264
|
+
antiCollapse: z
|
|
265
|
+
.object({
|
|
266
|
+
enabled: z.boolean().optional(),
|
|
267
|
+
maxGeneration: z.number().int().min(1).optional(),
|
|
268
|
+
lexicalDiversityCheck: z.boolean().optional(),
|
|
269
|
+
randomClusterFraction: z.number().min(0).max(1).optional(),
|
|
270
|
+
})
|
|
271
|
+
.strict()
|
|
272
|
+
.optional(),
|
|
273
|
+
// WS-3b: CLS (Complementary Learning System) interleaving (step 9).
|
|
274
|
+
// distill/memoryInference prompts include embedding-retrieved existing adjacent
|
|
275
|
+
// lessons/knowledge to prevent catastrophic interference with prior generalizations.
|
|
276
|
+
// Default OFF. Only meaningful on `distill` and `memoryInference` processes.
|
|
277
|
+
cls: z
|
|
278
|
+
.object({
|
|
279
|
+
enabled: z.boolean().optional(),
|
|
280
|
+
// Number of adjacent lessons/knowledge to include in prompts (default 3).
|
|
281
|
+
adjacentCount: z.number().int().min(1).optional(),
|
|
282
|
+
})
|
|
283
|
+
.strict()
|
|
284
|
+
.optional(),
|
|
285
|
+
// WS-3b: Distill→source fidelity check (step 10). After a distill proposal,
|
|
286
|
+
// check it against its cited source memories; a contradiction flag forces
|
|
287
|
+
// human review. Default OFF. Only meaningful on `distill` process.
|
|
288
|
+
fidelityCheck: z
|
|
289
|
+
.object({
|
|
290
|
+
enabled: z.boolean().optional(),
|
|
291
|
+
})
|
|
292
|
+
.strict()
|
|
293
|
+
.optional(),
|
|
294
|
+
// #609 — recombine process: minimum related-memory cluster size before an
|
|
295
|
+
// LLM generalization call. Default 3. Only meaningful on `recombine`.
|
|
296
|
+
minClusterSize: z.number().int().min(2).optional(),
|
|
297
|
+
// #609 — recombine process: hard cap on clusters processed per run (one
|
|
298
|
+
// bounded LLM call each). Default 5. Only meaningful on `recombine`.
|
|
299
|
+
maxClustersPerRun: positiveInt.optional(),
|
|
300
|
+
// #609 — recombine process: relatedness signal used to form clusters
|
|
301
|
+
// (tags | graph | both). Clustering is by relatedness, never embedding
|
|
302
|
+
// similarity. Default "tags". Only meaningful on `recombine`.
|
|
303
|
+
relatednessSource: z.enum(["tags", "graph", "both"]).optional(),
|
|
304
|
+
// #609 — recombine process: consecutive re-inductions required before a
|
|
305
|
+
// hypothesis is promoted to a lesson. Default 2. Only meaningful on
|
|
306
|
+
// `recombine`.
|
|
307
|
+
confirmThreshold: z.number().int().min(1).optional(),
|
|
308
|
+
// #615 — procedural process: minimum number of distinct assets sharing the
|
|
309
|
+
// same successful normalized ordered-action sequence before it is compiled
|
|
310
|
+
// into a workflow proposal. Default 3. Only meaningful on `procedural`.
|
|
311
|
+
minRecurrence: z.number().int().min(2).optional(),
|
|
312
|
+
// #615 — procedural process: hard cap on workflow proposals emitted per run
|
|
313
|
+
// (one bounded LLM call each). Default 3. Only meaningful on `procedural`.
|
|
314
|
+
maxProposalsPerRun: positiveInt.optional(),
|
|
315
|
+
// #615 — procedural process: asset type a compiled sequence is emitted as.
|
|
316
|
+
// Reserved; v1 always emits "workflow". Only meaningful on `procedural`.
|
|
317
|
+
emitAs: z.enum(["workflow", "skill"]).optional(),
|
|
154
318
|
// Triage process config (only meaningful for the `triage` process)
|
|
155
319
|
applyMode: z.enum(["queue", "promote"]).optional(),
|
|
156
320
|
policy: z.string().min(1).optional(),
|
|
@@ -176,6 +340,9 @@ const ImproveProfileProcessesSchema = z
|
|
|
176
340
|
graphExtraction: ImproveProcessConfigSchema.optional(),
|
|
177
341
|
validation: ImproveProcessConfigSchema.optional(),
|
|
178
342
|
triage: ImproveProcessConfigSchema.optional(),
|
|
343
|
+
proactiveMaintenance: ImproveProcessConfigSchema.optional(),
|
|
344
|
+
recombine: ImproveProcessConfigSchema.optional(),
|
|
345
|
+
procedural: ImproveProcessConfigSchema.optional(),
|
|
179
346
|
})
|
|
180
347
|
.passthrough()
|
|
181
348
|
.superRefine((val, ctx) => {
|
|
@@ -199,6 +366,9 @@ const ImproveProfileProcessesSchema = z
|
|
|
199
366
|
"validation",
|
|
200
367
|
"extract",
|
|
201
368
|
"triage",
|
|
369
|
+
"proactiveMaintenance",
|
|
370
|
+
"recombine",
|
|
371
|
+
"procedural",
|
|
202
372
|
]);
|
|
203
373
|
for (const k of Object.keys(raw)) {
|
|
204
374
|
if (!allowed.has(k)) {
|
|
@@ -216,6 +386,12 @@ export const ImproveProfileConfigSchema = z
|
|
|
216
386
|
processes: ImproveProfileProcessesSchema.optional(),
|
|
217
387
|
autoAccept: nonNegativeNumber.optional(),
|
|
218
388
|
limit: positiveInt.optional(),
|
|
389
|
+
// #614 — symmetric valence weighting in the eligibility sort. When true,
|
|
390
|
+
// the attention term becomes |valence| MAGNITUDE so BOTH strong positive
|
|
391
|
+
// and strong negative feedback drive attention (utility stays dominant) and
|
|
392
|
+
// strong-signed assets are routed to a fix/reinforce lane. DEFAULT OFF —
|
|
393
|
+
// false/absent preserves the legacy negative-only ranking byte-for-byte.
|
|
394
|
+
symmetricValence: z.boolean().optional(),
|
|
219
395
|
sync: z
|
|
220
396
|
.object({
|
|
221
397
|
enabled: z.boolean().optional(),
|
|
@@ -332,6 +508,7 @@ const SearchGraphBoostSchema = z
|
|
|
332
508
|
export const SearchConfigSchema = z
|
|
333
509
|
.object({
|
|
334
510
|
minScore: nonNegativeNumber.optional(),
|
|
511
|
+
defaultExcludeTypes: z.array(nonEmptyString).optional(),
|
|
335
512
|
curateRerank: z.object({ enabled: z.boolean().optional() }).strict().optional(),
|
|
336
513
|
graphBoost: SearchGraphBoostSchema.optional(),
|
|
337
514
|
})
|
|
@@ -350,10 +527,74 @@ const ImproveUtilityDecaySchema = z
|
|
|
350
527
|
feedbackStabilityBoost: z.number().finite().min(1).optional(),
|
|
351
528
|
})
|
|
352
529
|
.strict();
|
|
530
|
+
// #612 / WS-4 — auto-accept gate calibration + bounded, opt-in per-phase
|
|
531
|
+
// threshold auto-tune. DEFAULT OFF: when absent (or `autoTune: false`) no
|
|
532
|
+
// tuning occurs, so the gate behaves byte-identically to today.
|
|
533
|
+
// WS-4 adds: per-phase persistence (state.db) + auto-tune ceiling default 85.
|
|
534
|
+
const ImproveCalibrationSchema = z
|
|
535
|
+
.object({
|
|
536
|
+
/** Master switch for the bounded threshold auto-tune. Default false (parity). */
|
|
537
|
+
autoTune: z.boolean().optional(),
|
|
538
|
+
/** Lower bound (0-100) the tuned threshold may never drop below. */
|
|
539
|
+
minThreshold: z.number().int().min(0).max(100).optional(),
|
|
540
|
+
/**
|
|
541
|
+
* Upper bound (0-100) the tuned threshold may never rise above.
|
|
542
|
+
* WS-4 default: 85 (prevents gate converging to pure exploitation).
|
|
543
|
+
*/
|
|
544
|
+
maxThreshold: z.number().int().min(0).max(100).optional(),
|
|
545
|
+
/** Maximum adjustment magnitude (points) applied in one tune step. */
|
|
546
|
+
maxStep: positiveInt.optional(),
|
|
547
|
+
/** Minimum acted-on sample count required before any adjustment. */
|
|
548
|
+
minSamples: nonNegativeNumber.optional(),
|
|
549
|
+
/** Target realized accept rate in [0, 1]. Default 0.9. */
|
|
550
|
+
targetAcceptRate: z.number().finite().min(0).max(1).optional(),
|
|
551
|
+
})
|
|
552
|
+
.strict();
|
|
553
|
+
// WS-4 — exploration budget: a fixed fraction of proposals accepted per run
|
|
554
|
+
// regardless of confidence. DEFAULT OFF.
|
|
555
|
+
const ImproveExplorationSchema = z
|
|
556
|
+
.object({
|
|
557
|
+
/**
|
|
558
|
+
* Enable the exploration budget lane. Default false (parity).
|
|
559
|
+
* When true, a fraction of proposals are accepted regardless of confidence.
|
|
560
|
+
*/
|
|
561
|
+
enabled: z.boolean().optional(),
|
|
562
|
+
/**
|
|
563
|
+
* Fraction of proposals per run to accept as exploration [0, 1].
|
|
564
|
+
* Default 0.05 (5%). Clamped to [0, 1] at read time.
|
|
565
|
+
*/
|
|
566
|
+
budgetFraction: z.number().finite().min(0).max(1).optional(),
|
|
567
|
+
})
|
|
568
|
+
.strict();
|
|
569
|
+
const ImproveSalienceSchema = z
|
|
570
|
+
.object({
|
|
571
|
+
/**
|
|
572
|
+
* WS-2 Part-V gate: enable the outcome-weight term in the salience projection.
|
|
573
|
+
* Default false (parity — WS-1 weights w_e=0.30, w_r=0.70 until Part-V confirms
|
|
574
|
+
* no regression). Set to true after running scripts/akm-eval + health report.
|
|
575
|
+
*/
|
|
576
|
+
outcomeWeightEnabled: z.boolean().optional(),
|
|
577
|
+
/**
|
|
578
|
+
* Minimum encoding salience score [0, 1] for a zero-feedback asset to be
|
|
579
|
+
* admitted to the high-salience improve lane (#608).
|
|
580
|
+
* Default 0.75. Set to 1.0 to disable the lane entirely.
|
|
581
|
+
*/
|
|
582
|
+
salienceThreshold: z.number().min(0).max(1).optional(),
|
|
583
|
+
/**
|
|
584
|
+
* Per-run additive replay budget (#610). Up to this many top-salience refs are
|
|
585
|
+
* revisited even with no reactive signal and regardless of cooldown. Additive
|
|
586
|
+
* on top of --limit. Default 0 = no replay.
|
|
587
|
+
*/
|
|
588
|
+
replayBudget: z.number().int().min(0).optional(),
|
|
589
|
+
})
|
|
590
|
+
.strict();
|
|
353
591
|
export const ImproveConfigSchema = z
|
|
354
592
|
.object({
|
|
355
593
|
utilityDecay: ImproveUtilityDecaySchema.optional(),
|
|
356
594
|
eventRetentionDays: nonNegativeNumber.optional(),
|
|
595
|
+
calibration: ImproveCalibrationSchema.optional(),
|
|
596
|
+
exploration: ImproveExplorationSchema.optional(),
|
|
597
|
+
salience: ImproveSalienceSchema.optional(),
|
|
357
598
|
})
|
|
358
599
|
.strict();
|
|
359
600
|
// ── Index / per-pass ────────────────────────────────────────────────────────
|
|
@@ -249,8 +249,8 @@ function maybeAutoMigrateConfigFile(configPath, text) {
|
|
|
249
249
|
" to preview a dry-run diff: akm config migrate --dry-run --print-diff",
|
|
250
250
|
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
|
251
251
|
].join("\n");
|
|
252
|
-
process.stderr
|
|
253
|
-
process.stdout
|
|
252
|
+
process.stderr?.write?.(`${banner}\n`);
|
|
253
|
+
process.stdout?.write?.(`${banner}\n`);
|
|
254
254
|
}
|
|
255
255
|
catch (err) {
|
|
256
256
|
// #461: never return migrated bytes when disk write fails — that triggers
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* logs.db — Dedicated SQLite database for task/run log lines (#579).
|
|
6
|
+
*
|
|
7
|
+
* Replaces grep-the-flat-file consumption of `<cacheDir>/tasks/logs/<id>/<ts>.log`
|
|
8
|
+
* with structured, indexed rows: `{ts, task_id, run_id, stream, level, line}`.
|
|
9
|
+
* The strategic direction (stop scattering data across files/folders) means
|
|
10
|
+
* every NEW log consumer queries this database; the per-run text file written
|
|
11
|
+
* by the task runner is retained only as a transitional tail for humans —
|
|
12
|
+
* see docs/technical/logs-audit.md for the full producer audit.
|
|
13
|
+
*
|
|
14
|
+
* ## Why a separate database from state.db
|
|
15
|
+
*
|
|
16
|
+
* Log lines are high-volume, append-only, and freely purgeable; state.db rows
|
|
17
|
+
* (events, proposals, task_history) are durable records. Separating them keeps
|
|
18
|
+
* state.db small and lets log retention be aggressive without touching durable
|
|
19
|
+
* state. Cross-db queries (e.g. "failed task_history row → its log lines") use
|
|
20
|
+
* SQLite ATTACH — see {@link attachStateDatabase}.
|
|
21
|
+
*
|
|
22
|
+
* ## run_id
|
|
23
|
+
*
|
|
24
|
+
* state.db's `task_history` identifies a run by the unique pair
|
|
25
|
+
* `(task_id, started_at)` (see migration 002 in state-db.ts). logs.db encodes
|
|
26
|
+
* that pair as a single string — {@link buildTaskRunId} — so log rows can be
|
|
27
|
+
* joined back to their history row:
|
|
28
|
+
*
|
|
29
|
+
* l.run_id = th.task_id || '@' || th.started_at
|
|
30
|
+
*
|
|
31
|
+
* ## Schema evolution
|
|
32
|
+
*
|
|
33
|
+
* Same migration-safety contract as state.db: append-only `MIGRATIONS` applied
|
|
34
|
+
* through the shared runner in src/storage/engines/sqlite-migrations.ts.
|
|
35
|
+
*
|
|
36
|
+
* @module logs-db
|
|
37
|
+
*/
|
|
38
|
+
import fs from "node:fs";
|
|
39
|
+
import path from "node:path";
|
|
40
|
+
import { openDatabase } from "../storage/database.js";
|
|
41
|
+
import { runMigrations as runSqliteMigrations } from "../storage/engines/sqlite-migrations.js";
|
|
42
|
+
import { applyStandardPragmas } from "../storage/sqlite-pragmas.js";
|
|
43
|
+
import { getDataDir } from "./paths.js";
|
|
44
|
+
import { getStateDbPath } from "./state-db.js";
|
|
45
|
+
// ── Path helper ──────────────────────────────────────────────────────────────
|
|
46
|
+
/**
|
|
47
|
+
* Default path: `<dataDir>/logs.db` — alongside state.db so cooperating
|
|
48
|
+
* processes sharing a data root automatically share the same logs database
|
|
49
|
+
* (same `AKM_DATA_DIR` / XDG env-isolation as {@link getStateDbPath}).
|
|
50
|
+
*/
|
|
51
|
+
export function getLogsDbPath() {
|
|
52
|
+
return path.join(getDataDir(), "logs.db");
|
|
53
|
+
}
|
|
54
|
+
// ── Database open ────────────────────────────────────────────────────────────
|
|
55
|
+
/**
|
|
56
|
+
* Open (and initialise / migrate) the logs database.
|
|
57
|
+
*
|
|
58
|
+
* @param dbPath - Override the database file path (tests pass a tmpdir path).
|
|
59
|
+
*
|
|
60
|
+
* PRAGMA rationale:
|
|
61
|
+
*
|
|
62
|
+
* journal_mode = WAL
|
|
63
|
+
* Readers never block writers and vice-versa; crashes are safe (the WAL is
|
|
64
|
+
* replayed on next open). Required because the task runner writes log rows
|
|
65
|
+
* while `akm health` may be reading them.
|
|
66
|
+
*
|
|
67
|
+
* busy_timeout = 30000
|
|
68
|
+
* Log writes happen at the end of scheduled task runs, which can pile up
|
|
69
|
+
* (cron fan-out). 30 s of retry absorbs a slow concurrent writer instead of
|
|
70
|
+
* surfacing SQLITE_BUSY and dropping log lines.
|
|
71
|
+
*/
|
|
72
|
+
export function openLogsDatabase(dbPath) {
|
|
73
|
+
const resolvedPath = dbPath ?? getLogsDbPath();
|
|
74
|
+
const dir = path.dirname(resolvedPath);
|
|
75
|
+
if (!fs.existsSync(dir)) {
|
|
76
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
const db = openDatabase(resolvedPath);
|
|
79
|
+
// PRAGMAs must run before any DDL or DML. foreignKeys:false preserves this
|
|
80
|
+
// opener's historical behaviour — logs.db has never enforced foreign keys.
|
|
81
|
+
applyStandardPragmas(db, { dataDir: dir, foreignKeys: false });
|
|
82
|
+
runMigrations(db);
|
|
83
|
+
return db;
|
|
84
|
+
}
|
|
85
|
+
// ── Migrations ───────────────────────────────────────────────────────────────
|
|
86
|
+
/**
|
|
87
|
+
* All migrations in application order. APPEND only — never insert in the
|
|
88
|
+
* middle or reorder. Same contract as state.db's MIGRATIONS array.
|
|
89
|
+
*/
|
|
90
|
+
const MIGRATIONS = [
|
|
91
|
+
// ── Migration 001 — task_logs ───────────────────────────────────────────────
|
|
92
|
+
//
|
|
93
|
+
// One row per log line emitted by a task run.
|
|
94
|
+
//
|
|
95
|
+
// Indexed (query) columns:
|
|
96
|
+
// ts TEXT — ISO-8601 UTC; range queries ("logs in the last hour").
|
|
97
|
+
// task_id TEXT — task identifier; per-task log views.
|
|
98
|
+
// run_id TEXT — buildTaskRunId(task_id, started_at); per-run log views
|
|
99
|
+
// and the join key back to state.db task_history.
|
|
100
|
+
//
|
|
101
|
+
// Non-indexed columns:
|
|
102
|
+
// stream TEXT — 'stdout' | 'stderr'; which pipe the line came from.
|
|
103
|
+
// level TEXT — 'info' | 'warn' | 'error'; runner-assigned severity
|
|
104
|
+
// ('info' for captured stdout, 'error' for stderr and
|
|
105
|
+
// failure diagnostics).
|
|
106
|
+
// line TEXT — the log line itself (no trailing newline).
|
|
107
|
+
//
|
|
108
|
+
// ADD COLUMN extension points (future migrations):
|
|
109
|
+
// ALTER TABLE task_logs ADD COLUMN seq INTEGER DEFAULT NULL;
|
|
110
|
+
// ALTER TABLE task_logs ADD COLUMN source TEXT DEFAULT NULL;
|
|
111
|
+
//
|
|
112
|
+
// TTL: rows where ts < NOW() - retention can be deleted by purgeOldTaskLogs().
|
|
113
|
+
// No automatic deletion occurs here.
|
|
114
|
+
{
|
|
115
|
+
id: "001-task-logs",
|
|
116
|
+
up: `
|
|
117
|
+
CREATE TABLE IF NOT EXISTS task_logs (
|
|
118
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
119
|
+
ts TEXT NOT NULL,
|
|
120
|
+
task_id TEXT NOT NULL,
|
|
121
|
+
run_id TEXT NOT NULL,
|
|
122
|
+
stream TEXT NOT NULL DEFAULT 'stdout',
|
|
123
|
+
level TEXT NOT NULL DEFAULT 'info',
|
|
124
|
+
line TEXT NOT NULL
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
-- Query patterns:
|
|
128
|
+
-- SELECT … WHERE ts >= ? AND ts <= ? → idx_task_logs_ts (purge, windows)
|
|
129
|
+
-- SELECT … WHERE task_id = ? → idx_task_logs_task_id
|
|
130
|
+
-- SELECT … WHERE run_id = ? → idx_task_logs_run_id (per-run tail)
|
|
131
|
+
CREATE INDEX IF NOT EXISTS idx_task_logs_ts ON task_logs(ts);
|
|
132
|
+
CREATE INDEX IF NOT EXISTS idx_task_logs_task_id ON task_logs(task_id);
|
|
133
|
+
CREATE INDEX IF NOT EXISTS idx_task_logs_run_id ON task_logs(run_id);
|
|
134
|
+
`,
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
/**
|
|
138
|
+
* Apply every pending migration. Called automatically by
|
|
139
|
+
* {@link openLogsDatabase}; exported for the same test seams state-db exposes.
|
|
140
|
+
*/
|
|
141
|
+
export function runMigrations(db) {
|
|
142
|
+
runSqliteMigrations(db, MIGRATIONS);
|
|
143
|
+
}
|
|
144
|
+
// ── run_id ───────────────────────────────────────────────────────────────────
|
|
145
|
+
/**
|
|
146
|
+
* Encode a task run's identity — the unique `(task_id, started_at)` pair from
|
|
147
|
+
* state.db `task_history` — as a single run_id string.
|
|
148
|
+
*
|
|
149
|
+
* The format MUST stay in sync with the SQL expression
|
|
150
|
+
* `task_id || '@' || started_at` used by {@link queryFailedRunLogLines}.
|
|
151
|
+
*/
|
|
152
|
+
export function buildTaskRunId(taskId, startedAtIso) {
|
|
153
|
+
return `${taskId}@${startedAtIso}`;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Insert a batch of log lines for one task run in a single transaction.
|
|
157
|
+
* Returns the number of rows inserted. Lines are stored in array order
|
|
158
|
+
* (ascending rowid), so reading back `ORDER BY id` reproduces emission order.
|
|
159
|
+
*
|
|
160
|
+
* Errors propagate — the task runner wraps this in its own best-effort
|
|
161
|
+
* handling (mirroring `appendHistory`) so an unwritable logs.db never fails
|
|
162
|
+
* a task run.
|
|
163
|
+
*/
|
|
164
|
+
export function insertTaskLogLines(db, input) {
|
|
165
|
+
if (input.lines.length === 0)
|
|
166
|
+
return 0;
|
|
167
|
+
const stmt = db.prepare(`INSERT INTO task_logs (ts, task_id, run_id, stream, level, line)
|
|
168
|
+
VALUES (?, ?, ?, ?, ?, ?)`);
|
|
169
|
+
db.transaction(() => {
|
|
170
|
+
for (const entry of input.lines) {
|
|
171
|
+
stmt.run(input.ts, input.taskId, input.runId, entry.stream ?? "stdout", entry.level ?? "info", entry.line);
|
|
172
|
+
}
|
|
173
|
+
})();
|
|
174
|
+
return input.lines.length;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Read log lines matching the filter, in emission order (ascending id).
|
|
178
|
+
*
|
|
179
|
+
* Connection-lifetime rule (WS5): `.all()` materializes a plain array before
|
|
180
|
+
* returning.
|
|
181
|
+
*/
|
|
182
|
+
export function queryTaskLogs(db, options = {}) {
|
|
183
|
+
const conditions = [];
|
|
184
|
+
const params = [];
|
|
185
|
+
if (options.taskId) {
|
|
186
|
+
conditions.push("task_id = ?");
|
|
187
|
+
params.push(options.taskId);
|
|
188
|
+
}
|
|
189
|
+
if (options.runId) {
|
|
190
|
+
conditions.push("run_id = ?");
|
|
191
|
+
params.push(options.runId);
|
|
192
|
+
}
|
|
193
|
+
if (options.stream) {
|
|
194
|
+
conditions.push("stream = ?");
|
|
195
|
+
params.push(options.stream);
|
|
196
|
+
}
|
|
197
|
+
if (options.since) {
|
|
198
|
+
conditions.push("ts >= ?");
|
|
199
|
+
params.push(options.since);
|
|
200
|
+
}
|
|
201
|
+
if (options.until) {
|
|
202
|
+
conditions.push("ts < ?");
|
|
203
|
+
params.push(options.until);
|
|
204
|
+
}
|
|
205
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
206
|
+
const limit = options.limit !== undefined && options.limit >= 0 ? ` LIMIT ${Math.floor(options.limit)}` : "";
|
|
207
|
+
return db
|
|
208
|
+
.prepare(`SELECT id, ts, task_id, run_id, stream, level, line FROM task_logs ${where} ORDER BY id ASC${limit}`)
|
|
209
|
+
.all(...params);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Bulk membership check: which of `runIds` have at least one log row?
|
|
213
|
+
* Used by `akm health` to compute the log-backing rate from the database
|
|
214
|
+
* instead of `fs.existsSync` over scattered files. Chunked to stay under
|
|
215
|
+
* SQLite's bound-parameter ceiling.
|
|
216
|
+
*/
|
|
217
|
+
export function getLoggedRunIds(db, runIds) {
|
|
218
|
+
const out = new Set();
|
|
219
|
+
if (runIds.length === 0)
|
|
220
|
+
return out;
|
|
221
|
+
const CHUNK = 500;
|
|
222
|
+
for (let i = 0; i < runIds.length; i += CHUNK) {
|
|
223
|
+
const chunk = runIds.slice(i, i + CHUNK);
|
|
224
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
225
|
+
const rows = db
|
|
226
|
+
.prepare(`SELECT DISTINCT run_id FROM task_logs WHERE run_id IN (${placeholders})`)
|
|
227
|
+
.all(...chunk);
|
|
228
|
+
for (const row of rows)
|
|
229
|
+
out.add(row.run_id);
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
// ── Cross-db: ATTACH state.db ────────────────────────────────────────────────
|
|
234
|
+
/**
|
|
235
|
+
* ATTACH state.db to an open logs.db handle under the schema name `state`,
|
|
236
|
+
* enabling cross-db joins like task_history × task_logs.
|
|
237
|
+
*
|
|
238
|
+
* The state.db file must already exist (callers always open state.db first in
|
|
239
|
+
* practice); attaching a non-existent path would silently create an empty,
|
|
240
|
+
* unmigrated database file, so this throws instead.
|
|
241
|
+
*/
|
|
242
|
+
export function attachStateDatabase(db, stateDbPath) {
|
|
243
|
+
const resolved = stateDbPath ?? getStateDbPath();
|
|
244
|
+
if (!fs.existsSync(resolved)) {
|
|
245
|
+
throw new Error(`Cannot ATTACH state.db: file does not exist at ${resolved}`);
|
|
246
|
+
}
|
|
247
|
+
// prepare().run() rather than db.run(): both drivers support parameterised
|
|
248
|
+
// ATTACH through a prepared statement, and no other call site uses db.run().
|
|
249
|
+
db.prepare("ATTACH DATABASE ? AS state").run(resolved);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Convenience: open logs.db with state.db attached as `state`. The returned
|
|
253
|
+
* handle supports cross-db queries such as {@link queryFailedRunLogLines}.
|
|
254
|
+
* Close it like any other handle (DETACH is implicit on close).
|
|
255
|
+
*/
|
|
256
|
+
export function openLogsDatabaseWithState(logsDbPath, stateDbPath) {
|
|
257
|
+
const db = openLogsDatabase(logsDbPath);
|
|
258
|
+
try {
|
|
259
|
+
attachStateDatabase(db, stateDbPath);
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
db.close();
|
|
263
|
+
throw err;
|
|
264
|
+
}
|
|
265
|
+
return db;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Cross-db join: every log line belonging to a FAILED task_history run whose
|
|
269
|
+
* `started_at` is `>= since` (all failed runs when omitted). Requires a handle
|
|
270
|
+
* opened via {@link openLogsDatabaseWithState}.
|
|
271
|
+
*
|
|
272
|
+
* The join key is the run_id encoding documented on {@link buildTaskRunId}:
|
|
273
|
+
* `task_logs.run_id = task_history.task_id || '@' || task_history.started_at`.
|
|
274
|
+
*/
|
|
275
|
+
export function queryFailedRunLogLines(db, options = {}) {
|
|
276
|
+
const conditions = ["th.status = 'failed'"];
|
|
277
|
+
const params = [];
|
|
278
|
+
if (options.since) {
|
|
279
|
+
conditions.push("th.started_at >= ?");
|
|
280
|
+
params.push(options.since);
|
|
281
|
+
}
|
|
282
|
+
const limit = options.limit !== undefined && options.limit >= 0 ? ` LIMIT ${Math.floor(options.limit)}` : "";
|
|
283
|
+
return db
|
|
284
|
+
.prepare(`SELECT th.task_id, l.run_id, th.started_at, th.status, l.ts, l.stream, l.level, l.line
|
|
285
|
+
FROM state.task_history th
|
|
286
|
+
JOIN task_logs l ON l.run_id = th.task_id || '@' || th.started_at
|
|
287
|
+
WHERE ${conditions.join(" AND ")}
|
|
288
|
+
ORDER BY th.started_at DESC, l.id ASC${limit}`)
|
|
289
|
+
.all(...params);
|
|
290
|
+
}
|
|
291
|
+
// ── Retention ────────────────────────────────────────────────────────────────
|
|
292
|
+
/**
|
|
293
|
+
* Delete task_logs rows older than `retentionDays` (default: 90). Mirrors
|
|
294
|
+
* `purgeOldEvents` / `purgeOldImproveRuns` in state-db.ts — same default, same
|
|
295
|
+
* return shape (rows deleted), same disabled-when-non-positive semantics.
|
|
296
|
+
* Wired into the improve maintenance pass alongside the state.db purges.
|
|
297
|
+
*/
|
|
298
|
+
export function purgeOldTaskLogs(db, retentionDays = 90) {
|
|
299
|
+
if (!Number.isFinite(retentionDays) || retentionDays <= 0)
|
|
300
|
+
return 0;
|
|
301
|
+
const cutoff = new Date(Date.now() - retentionDays * 86_400_000).toISOString();
|
|
302
|
+
const result = db.prepare("DELETE FROM task_logs WHERE ts < ?").run(cutoff);
|
|
303
|
+
const changes = result.changes ?? 0;
|
|
304
|
+
return typeof changes === "bigint" ? Number(changes) : changes;
|
|
305
|
+
}
|
package/dist/core/paths.js
CHANGED
|
@@ -215,6 +215,9 @@ export function getDataDir(env = process.env, platform = process.platform) {
|
|
|
215
215
|
export function getDbPath() {
|
|
216
216
|
return path.join(getDataDir(), "index.db");
|
|
217
217
|
}
|
|
218
|
+
export function getIndexWriterLockPath() {
|
|
219
|
+
return path.join(getDataDir(), "index.db.write.lock");
|
|
220
|
+
}
|
|
218
221
|
export function getWorkflowDbPath() {
|
|
219
222
|
return path.join(getDataDir(), "workflow.db");
|
|
220
223
|
}
|