akm-cli 0.9.31 → 0.10.261002-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +420 -0
- package/STABILITY.md +99 -57
- package/dist/api-node.mjs +20 -0
- package/dist/api.d.ts +23 -0
- package/dist/api.js +45 -0
- package/dist/assets/hints/cli-hints-full.md +15 -29
- package/dist/assets/hints/cli-hints-short.md +2 -2
- package/dist/assets/improve-strategies/default.json +3 -3
- package/dist/assets/prompts/consolidate-pair.md +9 -7
- package/dist/assets/stash-skeleton/README.md +1 -1
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +1 -1
- package/dist/cli/shared.js +31 -48
- package/dist/cli/unknown-flags.js +6 -43
- package/dist/cli.js +8 -34
- package/dist/commands/agent/contribute-cli.js +1 -1
- package/dist/commands/completions.js +1 -2
- package/dist/commands/config-cli.js +5 -28
- package/dist/commands/env/env-cli.js +5 -5
- package/dist/commands/env/secret-cli.js +2 -2
- package/dist/commands/feedback-cli.js +75 -49
- package/dist/commands/health/checks.js +41 -16
- package/dist/commands/health/plugin-staleness.js +52 -22
- package/dist/commands/improve/autonomy-gate.js +5 -3
- package/dist/commands/improve/consolidate/chunking.js +3 -10
- package/dist/commands/improve/consolidate/continuity-check.js +4 -4
- package/dist/commands/improve/consolidate/pair-pass.js +29 -10
- package/dist/commands/improve/consolidate.js +10 -65
- package/dist/commands/improve/distill/lesson-update.js +91 -0
- package/dist/commands/improve/distill-guards.js +1 -43
- package/dist/commands/improve/distill.js +148 -21
- package/dist/commands/improve/eligibility.js +14 -1
- package/dist/commands/improve/extract-cli.js +1 -1
- package/dist/commands/improve/extract-prompt.js +17 -7
- package/dist/commands/improve/extract.js +5 -7
- package/dist/commands/improve/improve-cli.js +92 -106
- package/dist/commands/improve/improve-strategies.js +8 -12
- package/dist/commands/improve/improve.js +134 -52
- package/dist/commands/improve/loop-stages.js +18 -5
- package/dist/commands/improve/planner.js +0 -1
- package/dist/commands/improve/preparation.js +40 -99
- package/dist/commands/improve/proactive-maintenance.js +2 -2
- package/dist/commands/improve/reflect-noise.js +3 -55
- package/dist/commands/improve/reflect.js +29 -34
- package/dist/commands/improve/retrieval-queries.js +37 -0
- package/dist/commands/improve/retrieval-scope.js +2 -2
- package/dist/commands/improve/salience.js +2 -9
- package/dist/commands/improve/session-asset.js +4 -1
- package/dist/commands/improve/stage.js +23 -12
- package/dist/commands/lint/index.js +5 -20
- package/dist/commands/observability-cli.js +3 -9
- package/dist/commands/proposal/drain.js +43 -6
- package/dist/commands/proposal/proposal-cli.js +11 -31
- package/dist/commands/proposal/repository.js +20 -14
- package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -3
- package/dist/commands/read/knowledge.js +3 -3
- package/dist/commands/read/remember-cli.js +0 -13
- package/dist/commands/read/search-cli.js +29 -97
- package/dist/commands/read/search.js +0 -11
- package/dist/commands/read/show.js +0 -19
- package/dist/commands/sources/plugin-upgrade.js +164 -101
- package/dist/commands/sources/sources-cli.js +1 -15
- package/dist/commands/sources/stash-cli.js +6 -14
- package/dist/commands/tasks/tasks-cli.js +4 -115
- package/dist/commands/workflow/plan.js +11 -33
- package/dist/commands/workflow-cli.js +2 -7
- package/dist/core/adapter/adapter-ids.js +1 -2
- package/dist/core/adapter/adapters/agent-skills-adapter.js +0 -4
- package/dist/core/adapter/adapters/akm-adapter.js +9 -48
- package/dist/core/adapter/adapters/akm-lint.js +10 -26
- package/dist/core/adapter/adapters/akm-metadata.js +2 -72
- package/dist/core/adapter/adapters/akm-task-adapter.js +0 -5
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +9 -28
- package/dist/core/adapter/adapters/dotenv-adapter.js +1 -20
- package/dist/core/adapter/adapters/generic-files-adapter.js +0 -10
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +0 -4
- package/dist/core/adapter/adapters/tool-dir-shared.js +0 -24
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -4
- package/dist/core/adapter/recognize-match.js +3 -4
- package/dist/core/common.js +18 -2
- package/dist/core/config/config-schema.js +12 -15
- package/dist/core/config/config-types.js +6 -15
- package/dist/core/config/config-walker.js +6 -23
- package/dist/core/config/config.js +17 -80
- package/dist/core/config/engine-semantics.js +7 -3
- package/dist/core/config/schema/engines.js +7 -7
- package/dist/core/config/schema/improve-processes.js +10 -63
- package/dist/core/config/schema/improve.js +2 -21
- package/dist/core/config/schema/index-config.js +6 -39
- package/dist/core/env-secret-ref.js +2 -45
- package/dist/core/errors.js +1 -2
- package/dist/core/extra-params.js +2 -86
- package/dist/core/file-lock.js +2 -6
- package/dist/core/improve-result.js +13 -1
- package/dist/core/mutation-target.js +2 -2
- package/dist/core/paths.js +4 -0
- package/dist/core/recognition-util.js +4 -5
- package/dist/core/run-lock.js +1 -1
- package/dist/core/subprocess.js +49 -0
- package/dist/core/warn.js +21 -4
- package/dist/core/write-source.js +3 -3
- package/dist/execution/target-ref.js +1 -2
- package/dist/indexer/indexer.js +15 -6
- package/dist/indexer/passes/memory-inference.js +15 -6
- package/dist/indexer/search/db-search.js +49 -36
- package/dist/indexer/search/fts-query.js +0 -19
- package/dist/indexer/search/ranking.js +29 -1
- package/dist/indexer/usage/usage-events.js +21 -16
- package/dist/integrations/agent/engine-resolution.js +5 -2
- package/dist/integrations/agent/index.js +1 -1
- package/dist/integrations/agent/profiles.js +41 -98
- package/dist/integrations/agent/request-lowering.js +3 -3
- package/dist/integrations/harnesses/aider/index.js +5 -0
- package/dist/integrations/harnesses/amazonq/index.js +1 -0
- package/dist/integrations/harnesses/claude/config-import.js +2 -3
- package/dist/integrations/harnesses/claude/index.js +5 -0
- package/dist/integrations/harnesses/codex/index.js +1 -0
- package/dist/integrations/harnesses/copilot/index.js +1 -0
- package/dist/integrations/harnesses/gemini/index.js +1 -0
- package/dist/integrations/harnesses/index.js +18 -39
- package/dist/integrations/harnesses/opencode/agent-builder.js +60 -35
- package/dist/integrations/harnesses/opencode/config-import.js +121 -52
- package/dist/integrations/harnesses/opencode/index.js +22 -0
- package/dist/integrations/harnesses/opencode/plugin-layout.js +68 -0
- package/dist/integrations/harnesses/opencode/session-log-types.js +12 -0
- package/dist/integrations/harnesses/opencode/session-log-v1.js +72 -0
- package/dist/integrations/harnesses/opencode/session-log-v2.js +140 -0
- package/dist/integrations/harnesses/opencode/session-log.js +104 -115
- package/dist/integrations/harnesses/opencode/version.js +98 -0
- package/dist/integrations/harnesses/opencode-sdk/{harness.js → index.js} +5 -17
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +105 -206
- package/dist/integrations/harnesses/opencode-sdk/v1-adapter.js +199 -0
- package/dist/integrations/harnesses/opencode-sdk/v2-adapter.js +254 -0
- package/dist/integrations/harnesses/opencode-sdk/wire.js +66 -0
- package/dist/integrations/harnesses/openhands/index.js +1 -0
- package/dist/integrations/harnesses/pi/index.js +1 -0
- package/dist/integrations/harnesses/shared.js +2 -0
- package/dist/integrations/harnesses/types.js +2 -0
- package/dist/llm/client.js +41 -11
- package/dist/output/cli-hints.js +1 -1
- package/dist/output/context.js +10 -19
- package/dist/output/format-exempt.js +5 -8
- package/dist/output/shapes/helpers.js +1 -22
- package/dist/output/shapes/passthrough.js +0 -1
- package/dist/output/shapes.js +3 -31
- package/dist/output/stdout.js +5 -1
- package/dist/output/text/command-format.js +6 -1
- package/dist/output/text/workflow-format.js +4 -22
- package/dist/scripts/akm-migrate-node.js +15745 -16711
- package/dist/scripts/akm-migrate.js +16318 -17280
- package/dist/setup/detect.js +10 -4
- package/dist/setup/harness-config-import.js +5 -14
- package/dist/setup/setup.js +1 -1
- package/dist/setup/steps/tasks.js +12 -13
- package/dist/sources/providers/git-stash.js +113 -16
- package/dist/storage/repositories/index-utility-repository.js +2 -32
- package/dist/storage/repositories/index-vec-repository.js +4 -0
- package/dist/storage/repositories/task-history-repository.js +0 -5
- package/dist/storage/repositories/workflow-runs-repository.js +5 -15
- package/dist/tasks/embedded.js +2 -1
- package/dist/tasks/run/run-command-task.js +9 -1
- package/dist/tasks/run/run-workflow-task.js +4 -3
- package/dist/tasks/run/task-history.js +0 -2
- package/dist/tasks/run/task-log.js +38 -1
- package/dist/tasks/schedule-gates.js +39 -0
- package/dist/tasks/scheduler-binding.js +0 -16
- package/dist/tasks/scheduler-sync.js +3 -97
- package/dist/tasks/source/bounded-document.js +1 -1
- package/dist/tasks/source/task-source-v4.js +2 -12
- package/dist/tasks/task-run-reserved-flags.js +2 -31
- package/dist/workflows/authoring/authoring.js +6 -6
- package/dist/workflows/compile.js +8 -65
- package/dist/workflows/exec/child-workflow.js +22 -3
- package/dist/workflows/exec/native-executor.js +12 -147
- package/dist/workflows/exec/run-workflow.js +31 -103
- package/dist/workflows/exec/step-work.js +9 -178
- package/dist/workflows/exec/unit-dispatch.js +0 -10
- package/dist/workflows/freeze/environment.js +3 -44
- package/dist/workflows/freeze/freeze.js +8 -41
- package/dist/workflows/freeze/targets/child-workflow.js +27 -63
- package/dist/workflows/freeze/targets/command.js +2 -16
- package/dist/workflows/freeze/targets/shell.js +0 -2
- package/dist/workflows/freeze/task-bindings.js +2 -28
- package/dist/workflows/ir/params.js +12 -2
- package/dist/workflows/parser.js +82 -232
- package/dist/workflows/plan.js +1 -1
- package/dist/workflows/renderer.js +10 -19
- package/dist/workflows/runtime/run-plan.js +11 -53
- package/dist/workflows/runtime/runs.js +4 -27
- package/dist/workflows/source-files.js +12 -16
- package/dist/workflows/source-semantics.js +1 -93
- package/docs/README.md +1 -1
- package/docs/agents/README.md +3 -3
- package/docs/agents/agent-install.md +1 -1
- package/docs/architecture/0.10-process-rules.md +827 -0
- package/docs/architecture/adapters.md +17 -31
- package/docs/architecture/architecture.md +4 -5
- package/docs/architecture/improvement.md +115 -76
- package/docs/architecture/internals/improve-workflow.md +22 -16
- package/docs/architecture/internals/indexing.md +1 -1
- package/docs/architecture/internals/search.md +1 -2
- package/docs/architecture/internals/storage-locations.md +6 -6
- package/docs/architecture/persisted-data-compat.md +13 -4
- package/docs/architecture/specs/0.9.0-decisions.md +6 -2
- package/docs/architecture/specs/improve-collapse-churn-detector-design.md +2 -2
- package/docs/architecture/specs/workflow-format-unification.md +1 -6
- package/docs/architecture/testing/manual-testing-checklist.md +21 -30
- package/docs/architecture/testing/task-workflow-manual-tests.md +106 -203
- package/docs/architecture/testing/testing-workflow.md +1 -1
- package/docs/architecture/workflow-engine.md +5 -56
- package/docs/guides/author-workflows.md +39 -69
- package/docs/guides/bundles.md +3 -1
- package/docs/guides/capture-knowledge.md +1 -1
- package/docs/guides/claude-code-vs-akm-workflows.md +1 -2
- package/docs/guides/discover-and-load.md +4 -4
- package/docs/guides/getting-started.md +1 -1
- package/docs/guides/improve-the-library.md +10 -9
- package/docs/guides/recipes/headless-install.md +2 -2
- package/docs/guides/run-workflows.md +1 -1
- package/docs/guides/scheduling.md +9 -1
- package/docs/guides/use-with-any-agent.md +49 -4
- package/docs/integration/bundling-akm.md +7 -6
- package/docs/maintainers/curate-workmap.md +1 -2
- package/docs/maintainers/release-checklist.md +33 -1
- package/docs/migration/README.md +2 -0
- package/docs/migration/release-notes/0.10.0.md +201 -0
- package/docs/migration/release-notes/README.md +1 -0
- package/docs/migration/v0.9-to-v0.10.md +507 -0
- package/docs/posts/akm-command-tour.md +3 -3
- package/docs/product-surface.md +1 -1
- package/docs/reference/README.md +1 -0
- package/docs/reference/api.md +65 -0
- package/docs/reference/asset-types.md +3 -5
- package/docs/reference/bundle-types.md +2 -4
- package/docs/reference/cli.md +160 -188
- package/docs/reference/configuration.md +101 -20
- package/docs/reference/data-and-telemetry.md +2 -2
- package/docs/reference/env-and-secrets.md +2 -2
- package/docs/reference/supported-formats.md +3 -4
- package/docs/reference/tasks.md +12 -19
- package/docs/reference/website-sources.md +1 -1
- package/docs/reference/workflow-schema.md +89 -317
- package/docs/reference/workflows.md +20 -48
- package/package.json +12 -2
- package/schemas/akm-config.json +2 -243
- package/schemas/akm-task.json +2 -3
- package/schemas/akm-workflow.json +47 -84
- package/dist/assets/improve-strategies/catchup.json +0 -41
- package/dist/assets/improve-strategies/proactive-maintenance.json +0 -24
- package/dist/assets/improve-strategies/quick.json +0 -17
- package/dist/assets/improve-strategies/reflect-distill.json +0 -29
- package/dist/assets/improve-strategies/thorough.json +0 -49
- package/dist/assets/prompts/retrieval-relevance-judge.md +0 -6
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +0 -6
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +0 -4
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +0 -4
- package/dist/cli/retired-commands.js +0 -118
- package/dist/commands/improve/retrieval-gate.js +0 -132
- package/dist/commands/tasks/validate.js +0 -176
- package/dist/core/config/legacy-source-shape-shim.js +0 -117
- package/dist/indexer/feedback/utility-policy.js +0 -75
- package/dist/integrations/harnesses/ids.js +0 -28
- package/dist/tasks/source/task-source-v3-frozen.js +0 -424
- package/dist/tasks/source/task-to-v4.js +0 -863
- package/dist/workflows/exec/worktree.js +0 -372
- package/dist/workflows/freeze/child-output-references.js +0 -83
- package/dist/workflows/freeze/identity.js +0 -22
- package/dist/workflows/freeze/targets/script.js +0 -50
- package/dist/workflows/freeze/targets/task.js +0 -124
- package/dist/workflows/github-yaml.js +0 -506
- package/dist/workflows/runtime/run-outputs.js +0 -80
package/dist/core/subprocess.js
CHANGED
|
@@ -116,6 +116,51 @@ export function scheduleKillLadder(proc, opts) {
|
|
|
116
116
|
if (typeof sigkillTimer !== "number")
|
|
117
117
|
sigkillTimer.unref?.();
|
|
118
118
|
}
|
|
119
|
+
const liveGroupChildren = new Set();
|
|
120
|
+
let signalGuardHandlers;
|
|
121
|
+
function removeSignalGuard() {
|
|
122
|
+
for (const [signal, handler] of signalGuardHandlers ?? [])
|
|
123
|
+
process.off(signal, handler);
|
|
124
|
+
signalGuardHandlers = undefined;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* A captured child runs in its own process group, so the terminal's Ctrl-C and
|
|
128
|
+
* a `kill <akm-pid>` no longer reach it: without this, SIGTERM/SIGINT to akm
|
|
129
|
+
* ended akm and left the harness CLI (and everything it started) running. Each
|
|
130
|
+
* live child is registered here and, on the first such signal, takes the same
|
|
131
|
+
* ladder as a timeout; once every child is gone the signal is re-raised so akm
|
|
132
|
+
* still ends by the signal itself.
|
|
133
|
+
*
|
|
134
|
+
* The listeners are installed only while a child is live and only when nothing
|
|
135
|
+
* else handles the signal. A command that handles it itself (`workflow run`,
|
|
136
|
+
* `improve`, task runs) unwinds through its own cleanup, which already aborts
|
|
137
|
+
* its children, and must not have its handling pre-empted by a re-raise.
|
|
138
|
+
*/
|
|
139
|
+
function registerLiveGroupChild(child) {
|
|
140
|
+
liveGroupChildren.add(child);
|
|
141
|
+
if (!signalGuardHandlers && process.listenerCount("SIGINT") === 0 && process.listenerCount("SIGTERM") === 0) {
|
|
142
|
+
const handlers = [];
|
|
143
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
144
|
+
const handler = () => {
|
|
145
|
+
const children = [...liveGroupChildren];
|
|
146
|
+
for (const live of children)
|
|
147
|
+
live.terminate();
|
|
148
|
+
removeSignalGuard();
|
|
149
|
+
void Promise.allSettled(children.map((live) => live.proc.exited)).then(() => {
|
|
150
|
+
process.kill(process.pid, signal);
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
process.once(signal, handler);
|
|
154
|
+
handlers.push([signal, handler]);
|
|
155
|
+
}
|
|
156
|
+
signalGuardHandlers = handlers;
|
|
157
|
+
}
|
|
158
|
+
return () => {
|
|
159
|
+
liveGroupChildren.delete(child);
|
|
160
|
+
if (liveGroupChildren.size === 0)
|
|
161
|
+
removeSignalGuard();
|
|
162
|
+
};
|
|
163
|
+
}
|
|
119
164
|
/**
|
|
120
165
|
* The joined, human-readable reason ONE managed run's capture is incomplete, or
|
|
121
166
|
* `undefined` when both pipes drained cleanly.
|
|
@@ -401,6 +446,8 @@ export async function runManagedSubprocess(cmd, opts) {
|
|
|
401
446
|
else
|
|
402
447
|
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
403
448
|
}
|
|
449
|
+
// A termination signal to this process takes the own-group child down too.
|
|
450
|
+
const unregisterLiveChild = capture && spawnsOwnProcessGroup() ? registerLiveGroupChild({ proc, terminate: onAbort }) : () => { };
|
|
404
451
|
// Stream-drain timeout: a short grace once nothing living owns the pipe any
|
|
405
452
|
// more, or a one-hour safety bound when execution itself is unbounded.
|
|
406
453
|
//
|
|
@@ -454,6 +501,7 @@ export async function runManagedSubprocess(cmd, opts) {
|
|
|
454
501
|
if (timer !== undefined)
|
|
455
502
|
clearTimeoutImpl(timer);
|
|
456
503
|
abortSignal?.removeEventListener("abort", onAbort);
|
|
504
|
+
unregisterLiveChild();
|
|
457
505
|
// Drain readers so they don't surface as unhandled rejections. The streams
|
|
458
506
|
// carry their own drain timeout so this cannot block indefinitely.
|
|
459
507
|
await Promise.allSettled([stdoutPromise, stderrPromise]);
|
|
@@ -470,6 +518,7 @@ export async function runManagedSubprocess(cmd, opts) {
|
|
|
470
518
|
}
|
|
471
519
|
clearTimeoutImpl(timer);
|
|
472
520
|
abortSignal?.removeEventListener("abort", onAbort);
|
|
521
|
+
unregisterLiveChild();
|
|
473
522
|
const [stdoutRead, stderrRead] = await Promise.all([stdoutPromise, stderrPromise]);
|
|
474
523
|
return {
|
|
475
524
|
exitCode,
|
package/dist/core/warn.js
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
import fs from "node:fs";
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
let quiet = false;
|
|
18
|
+
/** Calls inside {@link withQuiet}; counted so overlapping calls cannot leave the flag flipped. */
|
|
19
|
+
let quietScopes = 0;
|
|
18
20
|
let verbose = false;
|
|
19
21
|
let logFilePath;
|
|
20
22
|
let sinkOverride;
|
|
@@ -33,7 +35,22 @@ export function resetQuiet() {
|
|
|
33
35
|
quiet = false;
|
|
34
36
|
}
|
|
35
37
|
export function isQuiet() {
|
|
36
|
-
return quiet;
|
|
38
|
+
return quiet || quietScopes > 0;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Run `fn` with stderr diagnostics suppressed (the log file sink still
|
|
42
|
+
* receives them), then restore. For in-process callers (`src/api.ts`) that
|
|
43
|
+
* must stay silent without touching the host's `--quiet` state: a depth count
|
|
44
|
+
* rather than save/restore of the flag keeps overlapping async calls correct.
|
|
45
|
+
*/
|
|
46
|
+
export async function withQuiet(fn) {
|
|
47
|
+
quietScopes++;
|
|
48
|
+
try {
|
|
49
|
+
return await fn();
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
quietScopes--;
|
|
53
|
+
}
|
|
37
54
|
}
|
|
38
55
|
/**
|
|
39
56
|
* Set the verbose flag from a CLI flag. The `AKM_VERBOSE` env var, when set,
|
|
@@ -103,7 +120,7 @@ export function info(...args) {
|
|
|
103
120
|
return;
|
|
104
121
|
}
|
|
105
122
|
appendToLogFile("INFO", args);
|
|
106
|
-
if (!
|
|
123
|
+
if (!isQuiet()) {
|
|
107
124
|
console.warn(...args);
|
|
108
125
|
}
|
|
109
126
|
}
|
|
@@ -118,7 +135,7 @@ export function warn(...args) {
|
|
|
118
135
|
return;
|
|
119
136
|
}
|
|
120
137
|
appendToLogFile("WARN", args);
|
|
121
|
-
if (!
|
|
138
|
+
if (!isQuiet()) {
|
|
122
139
|
console.warn(...args);
|
|
123
140
|
}
|
|
124
141
|
}
|
|
@@ -148,7 +165,7 @@ export function error(...args) {
|
|
|
148
165
|
return;
|
|
149
166
|
}
|
|
150
167
|
appendToLogFile("ERROR", args);
|
|
151
|
-
if (!
|
|
168
|
+
if (!isQuiet()) {
|
|
152
169
|
console.error(...args);
|
|
153
170
|
}
|
|
154
171
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* The only module that branches on `source.kind` for writes. It does two
|
|
8
8
|
* things:
|
|
9
9
|
*
|
|
10
|
-
* 1. Resolve the destination: `--
|
|
10
|
+
* 1. Resolve the destination: `--bundle` → `defaultWriteTarget` → working
|
|
11
11
|
* stash (`defaultBundle`). `writable` defaults to true on `filesystem` and
|
|
12
12
|
* false on `git`; `website` / `npm` are never writable (the config loader
|
|
13
13
|
* rejects `writable: true` on them).
|
|
@@ -53,7 +53,7 @@ export function isWriteCapableSourceKind(kind) {
|
|
|
53
53
|
return kind === "filesystem" || kind === "git";
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
|
-
* Resolve the destination for a write: explicit `--
|
|
56
|
+
* Resolve the destination for a write: explicit `--bundle`, then
|
|
57
57
|
* `defaultWriteTarget`, then the working stash (`defaultBundle`). There is no
|
|
58
58
|
* fallback to the first writable source.
|
|
59
59
|
*/
|
|
@@ -61,7 +61,7 @@ export function resolveWriteTarget(akmConfig, explicitTarget, options = {}) {
|
|
|
61
61
|
const allConfiguredSources = resolveConfiguredSources(akmConfig);
|
|
62
62
|
const configuredSources = resolveActiveConfiguredSources(akmConfig);
|
|
63
63
|
const requireWritable = options.requireWritable !== false;
|
|
64
|
-
const flag = options.flag ?? "--
|
|
64
|
+
const flag = options.flag ?? "--bundle";
|
|
65
65
|
if (explicitTarget) {
|
|
66
66
|
const match = configuredSources.find((s) => s.name === explicitTarget);
|
|
67
67
|
if (!match) {
|
|
@@ -22,8 +22,7 @@
|
|
|
22
22
|
*
|
|
23
23
|
* Explicit non-goals (binding, spec §4.1): no GitHub locator grammar, no
|
|
24
24
|
* `akm/command` builtin special case, no resolution, no filesystem access, no
|
|
25
|
-
* guessing.
|
|
26
|
-
* `classifyWorkflowStepUses` in src/workflows/source-semantics.ts).
|
|
25
|
+
* guessing.
|
|
27
26
|
*/
|
|
28
27
|
import { bundleRefToString, parseBundleRef } from "../core/asset/asset-ref.js";
|
|
29
28
|
import { UsageError } from "../core/errors.js";
|
package/dist/indexer/indexer.js
CHANGED
|
@@ -1291,7 +1291,10 @@ export async function lookup(ref) {
|
|
|
1291
1291
|
}
|
|
1292
1292
|
// ── Utility score recomputation ──────────────────────────────────────────────
|
|
1293
1293
|
/**
|
|
1294
|
-
* Recompute utility scores
|
|
1294
|
+
* Recompute utility scores based on usage_events data: every entry by default,
|
|
1295
|
+
* or just `onlyEntryId` (what `akm feedback` passes so the value it stores is
|
|
1296
|
+
* the one `akm index` would compute, not a second formula). A one-entry call
|
|
1297
|
+
* leaves the retention purge and the `last_utility_computed_at` watermark alone.
|
|
1295
1298
|
*
|
|
1296
1299
|
* For each entry:
|
|
1297
1300
|
* - Count search appearances (event_type = 'search')
|
|
@@ -1306,11 +1309,13 @@ export async function lookup(ref) {
|
|
|
1306
1309
|
*
|
|
1307
1310
|
* Called during `akm index` after FTS rebuild.
|
|
1308
1311
|
*/
|
|
1309
|
-
export function recomputeUtilityScores(db, stateDb) {
|
|
1312
|
+
export function recomputeUtilityScores(db, stateDb, onlyEntryId) {
|
|
1310
1313
|
const EMA_DECAY = 0.7;
|
|
1314
|
+
const scoped = onlyEntryId !== undefined;
|
|
1311
1315
|
// Purge stale usage events (90-day retention). usage_events lives in state.db
|
|
1312
1316
|
// (Chunk-8 WI-8.3); its table is created by state migration 020.
|
|
1313
|
-
|
|
1317
|
+
if (!scoped)
|
|
1318
|
+
purgeOldUsageEvents(stateDb, USAGE_EVENT_RETENTION_DAYS);
|
|
1314
1319
|
// Time-proportional decay: apply one round of EMA per elapsed day so
|
|
1315
1320
|
// indexing frequency doesn't affect how fast scores decay.
|
|
1316
1321
|
const lastComputedAt = getMeta(db, "last_utility_computed_at");
|
|
@@ -1344,14 +1349,17 @@ export function recomputeUtilityScores(db, stateDb) {
|
|
|
1344
1349
|
FROM usage_events u
|
|
1345
1350
|
WHERE u.entry_id IS NOT NULL
|
|
1346
1351
|
AND u.source = 'user'
|
|
1352
|
+
${scoped ? "AND u.entry_id = ?" : ""}
|
|
1347
1353
|
GROUP BY u.entry_id
|
|
1348
1354
|
`)
|
|
1349
|
-
.all();
|
|
1355
|
+
.all(...(scoped ? [onlyEntryId] : []));
|
|
1350
1356
|
const entryExists = db.prepare("SELECT 1 FROM entries WHERE id = ?");
|
|
1351
1357
|
const usageByEntry = new Map(aggregatedRows.filter((row) => entryExists.get(row.entry_id) != null).map((row) => [row.entry_id, row]));
|
|
1352
1358
|
// Batch-load existing utility scores
|
|
1353
1359
|
const existingScores = new Map();
|
|
1354
|
-
const scoreRows = db
|
|
1360
|
+
const scoreRows = db
|
|
1361
|
+
.prepare(`SELECT entry_id, utility, last_used_at FROM utility_scores${scoped ? " WHERE entry_id = ?" : ""}`)
|
|
1362
|
+
.all(...(scoped ? [onlyEntryId] : []));
|
|
1355
1363
|
for (const row of scoreRows) {
|
|
1356
1364
|
existingScores.set(row.entry_id, { utility: row.utility, lastUsedAt: row.last_used_at ?? undefined });
|
|
1357
1365
|
}
|
|
@@ -1391,5 +1399,6 @@ export function recomputeUtilityScores(db, stateDb) {
|
|
|
1391
1399
|
lastUsedAt,
|
|
1392
1400
|
});
|
|
1393
1401
|
}
|
|
1394
|
-
|
|
1402
|
+
if (!scoped)
|
|
1403
|
+
setMeta(db, "last_utility_computed_at", new Date().toISOString());
|
|
1395
1404
|
}
|
|
@@ -247,12 +247,19 @@ async function runMemoryInferencePassBody(ctx, provenance) {
|
|
|
247
247
|
const featureConfig = invocationOwnsRunner
|
|
248
248
|
? { ...config, index: { ...config.index, memory: { ...config.index?.memory, enabled: true } } }
|
|
249
249
|
: config;
|
|
250
|
-
// The pass only writes to the
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
250
|
+
// The pass only writes to the default (working) stash, found by its
|
|
251
|
+
// `isDefault` flag and never by array position. Read-only caches (git, npm,
|
|
252
|
+
// website) are deliberately untouched — writing inferred children there
|
|
253
|
+
// would be clobbered by the next sync().
|
|
254
|
+
const primary = sources.find((source) => source.isDefault === true);
|
|
255
|
+
if (!primary) {
|
|
256
|
+
warn("memory inference: skipped — no default bundle among the sources, so there is no write target");
|
|
255
257
|
return completeResult();
|
|
258
|
+
}
|
|
259
|
+
if ((primary.adapterId ?? detectAdapterId(primary.path)) !== "akm") {
|
|
260
|
+
warn(`memory inference: skipped — the default bundle at ${primary.path} is not an akm bundle`);
|
|
261
|
+
return completeResult();
|
|
262
|
+
}
|
|
256
263
|
const pending = collectPendingMemories(primary.path).filter((record) => !options.candidateRefs || options.candidateRefs.has(record.ref));
|
|
257
264
|
result.considered = pending.length;
|
|
258
265
|
if (pending.length === 0)
|
|
@@ -511,7 +518,9 @@ function markParentProcessed(parent) {
|
|
|
511
518
|
warn(`memory inference: skipping markParentProcessed for ${parent.filePath} — could not parse frontmatter block`);
|
|
512
519
|
return;
|
|
513
520
|
}
|
|
514
|
-
|
|
521
|
+
// Stamp the frontmatter as it is now: `parent.data` was parsed before the model
|
|
522
|
+
// call, and writing it back would revert any edit made to the file since.
|
|
523
|
+
const updatedFm = { ...parseFrontmatter(raw).data, [FM_INFERENCE_PROCESSED]: true };
|
|
515
524
|
const next = assembleAsset(updatedFm, block.content);
|
|
516
525
|
try {
|
|
517
526
|
fs.writeFileSync(parent.filePath, next, "utf8");
|
|
@@ -6,15 +6,16 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Ranking is two candidate channels fused by reciprocal rank (`ranking.ts`):
|
|
8
8
|
* BM25 over whole documents matching any query word, and the nearest document
|
|
9
|
-
* vectors to the query embedding.
|
|
9
|
+
* vectors to the query embedding. The fused score is then scaled by the
|
|
10
|
+
* asset's recorded user feedback. Filters narrow the list; nothing else
|
|
10
11
|
* reorders it.
|
|
11
12
|
*/
|
|
12
13
|
import path from "node:path";
|
|
13
|
-
import { stashDirFor } from "../../core/asset/asset-placement.js";
|
|
14
14
|
import { displayRef } from "../../core/asset/resolve-ref.js";
|
|
15
15
|
import { compareCodePoints } from "../../core/common.js";
|
|
16
16
|
import { classifyPathAccess } from "../../core/path-access.js";
|
|
17
17
|
import { getDbPath } from "../../core/paths.js";
|
|
18
|
+
import { withStateDbTelemetry } from "../../core/state-db.js";
|
|
18
19
|
import { systemErrorCode } from "../../core/system-error.js";
|
|
19
20
|
import { presentationFor } from "../../core/type-presentation.js";
|
|
20
21
|
import { embed } from "../../llm/embedder.js";
|
|
@@ -27,8 +28,9 @@ import { getMeta } from "../../storage/repositories/index-meta-repository.js";
|
|
|
27
28
|
import { getEmbeddingCount, searchVec } from "../../storage/repositories/index-vec-repository.js";
|
|
28
29
|
import { ensureIndex } from "../ensure-index.js";
|
|
29
30
|
import { isProposedQuality } from "../passes/metadata.js";
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
31
|
+
import { countFeedbackSignalsByRef } from "../usage/usage-events.js";
|
|
32
|
+
import { ftsQueryTokens, parseRefPrefixQuery } from "./fts-query.js";
|
|
33
|
+
import { applyFeedback, reciprocalRankFusion, } from "./ranking.js";
|
|
32
34
|
import { attachSearchHitAttribution } from "./search-attribution.js";
|
|
33
35
|
import { enrichSearchHit } from "./search-hit-enrichers.js";
|
|
34
36
|
import { buildEditHint, findSourceForPath, isEditable } from "./search-source.js";
|
|
@@ -81,30 +83,31 @@ function resolveSearchHitRef(entry, provenance, defaultBundleId) {
|
|
|
81
83
|
}
|
|
82
84
|
// ── Main search entrypoint ───────────────────────────────────────────────────
|
|
83
85
|
/**
|
|
84
|
-
* Whether
|
|
86
|
+
* Whether `auto` has a usable embedding provider.
|
|
85
87
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
88
|
+
* `auto` with no (or no remote) embedding settings falls back to the built-in
|
|
89
|
+
* local model (`DEFAULT_LOCAL_MODEL`, or `embedding.localModel`), so semantic
|
|
90
|
+
* search works without any configuration. The only config that can never
|
|
91
|
+
* succeed is a remote `endpoint` without a `model` — `RemoteEmbedder` throws on
|
|
92
|
+
* construction. Treating "no endpoint+model pair" as unconfigured warned every
|
|
93
|
+
* default install that semantic search had no provider while it worked.
|
|
92
94
|
*/
|
|
93
95
|
function hasConfiguredEmbeddingProvider(config) {
|
|
94
|
-
|
|
96
|
+
const embedding = config.embedding;
|
|
97
|
+
if (!embedding?.endpoint)
|
|
95
98
|
return true;
|
|
96
|
-
return Boolean(
|
|
99
|
+
return Boolean(embedding.model);
|
|
97
100
|
}
|
|
98
101
|
export async function searchLocal(input) {
|
|
99
|
-
const {
|
|
102
|
+
const { stashDir, config } = input;
|
|
100
103
|
const warnings = [];
|
|
101
104
|
// Semantic search is attempted fresh on every query (see `startVectorChannel`);
|
|
102
105
|
// there is no cached readiness verdict to consult here. The only thing
|
|
103
106
|
// worth flagging ahead of the attempt is a config that can never succeed.
|
|
104
107
|
if (config.semanticSearchMode === "auto" && !hasConfiguredEmbeddingProvider(config)) {
|
|
105
|
-
warnings.push("Semantic search is enabled (semanticSearchMode='auto') but
|
|
106
|
-
|
|
107
|
-
"(
|
|
108
|
+
warnings.push("Semantic search is enabled (semanticSearchMode='auto') but embedding.endpoint is set without embedding.model. " +
|
|
109
|
+
"Either: (a) set embedding.model, or (b) remove embedding.endpoint to use the built-in local model, or " +
|
|
110
|
+
"(c) `akm config set semanticSearchMode off` to use keyword-only search.");
|
|
108
111
|
}
|
|
109
112
|
// Bootstrap-only: builds the index inline when it cannot serve this stash.
|
|
110
113
|
// Content freshness is the writers' job (indexWrittenAssets + full runs);
|
|
@@ -138,12 +141,14 @@ export async function searchLocal(input) {
|
|
|
138
141
|
const staleHint = buildStaleIndexHint(db);
|
|
139
142
|
if (staleHint)
|
|
140
143
|
warnings.push(staleHint);
|
|
141
|
-
const { hits, embedMs, rankMs, mode, semanticWarning } = await searchDatabase(db, input);
|
|
144
|
+
const { hits, embedMs, rankMs, mode, semanticWarning, feedbackWarning } = await searchDatabase(db, input);
|
|
142
145
|
if (semanticWarning)
|
|
143
146
|
warnings.push(semanticWarning);
|
|
147
|
+
if (feedbackWarning)
|
|
148
|
+
warnings.push(feedbackWarning);
|
|
144
149
|
return {
|
|
145
150
|
hits,
|
|
146
|
-
tip: hits.length === 0 ?
|
|
151
|
+
tip: hits.length === 0 ? EMPTY_RESULT_TIP : undefined,
|
|
147
152
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
148
153
|
embedMs,
|
|
149
154
|
rankMs,
|
|
@@ -218,7 +223,9 @@ async function searchDatabase(db, input) {
|
|
|
218
223
|
const embedMs = Date.now() - startedAt;
|
|
219
224
|
const tRank0 = Date.now();
|
|
220
225
|
const vectorCandidates = vector.neighbors ? keepAllowedTypes(db, vector.neighbors, typeFilter, defaultExcludes) : [];
|
|
221
|
-
const
|
|
226
|
+
const rrf = reciprocalRankFusion([lexical, vectorCandidates]);
|
|
227
|
+
const feedback = readFeedback(rrf);
|
|
228
|
+
const fused = applyFeedback(rrf, feedback.counts);
|
|
222
229
|
const selected = selectFusedEntries(db, fused, limit, filterOptions);
|
|
223
230
|
const rankMs = Date.now() - tRank0;
|
|
224
231
|
const hits = await Promise.all(selected.map(({ candidate, row }) => buildDbHit({
|
|
@@ -235,7 +242,27 @@ async function searchDatabase(db, input) {
|
|
|
235
242
|
db,
|
|
236
243
|
})));
|
|
237
244
|
const mode = vector.warning ? "fts-fallback" : vector.neighbors ? "semantic" : "keyword";
|
|
238
|
-
return { embedMs, rankMs, hits, mode, semanticWarning: vector.warning };
|
|
245
|
+
return { embedMs, rankMs, hits, mode, semanticWarning: vector.warning, feedbackWarning: feedback.warning };
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* User feedback counts of the fused candidates, from state.db in one query.
|
|
249
|
+
* A search never creates state.db, and one that cannot read it ranks without
|
|
250
|
+
* feedback and says so.
|
|
251
|
+
*/
|
|
252
|
+
function readFeedback(fused) {
|
|
253
|
+
let counts = new Map();
|
|
254
|
+
try {
|
|
255
|
+
withStateDbTelemetry((stateDb) => {
|
|
256
|
+
counts = countFeedbackSignalsByRef(stateDb, fused.map((candidate) => candidate.itemRef));
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
return {
|
|
261
|
+
counts: new Map(),
|
|
262
|
+
warning: `Feedback was not applied to the ranking: ${error instanceof Error ? error.message : String(error)}`,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
return { counts };
|
|
239
266
|
}
|
|
240
267
|
/**
|
|
241
268
|
* Walk the fused list in order, loading entries a batch at a time, and keep
|
|
@@ -294,21 +321,7 @@ function describeRanks(candidate) {
|
|
|
294
321
|
...(vectorRank !== undefined ? [`vector rank ${vectorRank}`] : []),
|
|
295
322
|
];
|
|
296
323
|
}
|
|
297
|
-
|
|
298
|
-
* The no-hits tip. A query in the retired `<type>:` / `<type>:<prefix>/` browse
|
|
299
|
-
* grammar gets the conceptId spelling that replaces it: without this it comes
|
|
300
|
-
* back empty and silent, which is the failure D4 removed the grammar to avoid.
|
|
301
|
-
*/
|
|
302
|
-
function emptyResultTip(query) {
|
|
303
|
-
const generic = "No matching stash assets were found. Try a different query or run 'akm index' to rebuild.";
|
|
304
|
-
const retired = parseRetiredTypePrefixQuery(query);
|
|
305
|
-
if (!retired)
|
|
306
|
-
return generic;
|
|
307
|
-
const root = stashDirFor(retired.type);
|
|
308
|
-
if (!root)
|
|
309
|
-
return generic;
|
|
310
|
-
return `No matching stash assets were found. The '<type>:' browse grammar was removed in 0.9.0 — use the conceptId spelling: 'akm search "${root}/${retired.rest}"'.`;
|
|
311
|
-
}
|
|
324
|
+
const EMPTY_RESULT_TIP = "No matching stash assets were found. Try a different query or run 'akm index' to rebuild.";
|
|
312
325
|
// ── Enumeration (browse) path ────────────────────────────────────────────────
|
|
313
326
|
/**
|
|
314
327
|
* Enumerate index entries without FTS scoring — the browse path shared by
|
|
@@ -78,22 +78,3 @@ export function parseRefPrefixQuery(query) {
|
|
|
78
78
|
return { bundle, conceptIdPrefix: rest };
|
|
79
79
|
return null;
|
|
80
80
|
}
|
|
81
|
-
/**
|
|
82
|
-
* Recognize the retired `<type>:` / `<type>:<prefix>/` browse grammar so the
|
|
83
|
-
* caller can name the replacement spelling rather than letting the query
|
|
84
|
-
* degrade silently into a keyword search — the exact silent failure D4 removes.
|
|
85
|
-
* Shape recognition only; mapping the type to its conceptId root belongs to the
|
|
86
|
-
* caller, which keeps this module dependency-free.
|
|
87
|
-
*/
|
|
88
|
-
export function parseRetiredTypePrefixQuery(query) {
|
|
89
|
-
const trimmed = query.trim();
|
|
90
|
-
if (trimmed.length === 0 || /\s/.test(trimmed) || trimmed.includes("//"))
|
|
91
|
-
return null;
|
|
92
|
-
const colon = trimmed.indexOf(":");
|
|
93
|
-
if (colon <= 0)
|
|
94
|
-
return null;
|
|
95
|
-
const rest = trimmed.slice(colon + 1);
|
|
96
|
-
if (rest !== "" && !rest.endsWith("/"))
|
|
97
|
-
return null;
|
|
98
|
-
return { type: trimmed.slice(0, colon), rest };
|
|
99
|
-
}
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
/**
|
|
5
5
|
* Reciprocal rank fusion (Cormack, Clarke and Büttcher, SIGIR 2009) of the
|
|
6
|
-
* lexical and vector candidate lists
|
|
6
|
+
* lexical and vector candidate lists, then one feedback factor on the fused
|
|
7
|
+
* score — the whole of search ranking.
|
|
7
8
|
*
|
|
8
9
|
* Fusing ranks rather than scores needs no calibration between BM25 and cosine
|
|
9
10
|
* similarity, so it holds for every embedding model. On the retrieval suite,
|
|
@@ -35,3 +36,30 @@ export function reciprocalRankFusion(channels) {
|
|
|
35
36
|
});
|
|
36
37
|
return [...fused.values()].sort((a, b) => b.score - a.score || compareCodePoints(a.itemRef, b.itemRef));
|
|
37
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Weight of one log-count of feedback on the fused score. A factor of
|
|
41
|
+
* 1 + w·(ln(1+pos) − ln(1+neg)) moves a score by about 2% / 3% / 5% / 9% for
|
|
42
|
+
* 1 / 2 / 5 / 20 positives at 0.03. Measured on the retrieval suite's fused
|
|
43
|
+
* lists, 5 positives lift a hit from rank 10 by a median of 3 places (from rank 5,
|
|
44
|
+
* by 2) and 20 by 4 to 5, so a voted hit passes its near neighbours and a clearly
|
|
45
|
+
* better match stays ahead. At 0.05 the same votes lift it 4 to 5 and 6 to 7
|
|
46
|
+
* places, and two wrong votes cost about 1.6 times the nDCG@10 they cost at 0.03.
|
|
47
|
+
*/
|
|
48
|
+
export const FEEDBACK_WEIGHT = 0.03;
|
|
49
|
+
/**
|
|
50
|
+
* The multiplier on a hit's fused score for its feedback: the same for every
|
|
51
|
+
* asset, rising with the number of positives and falling with the number of
|
|
52
|
+
* negatives, each by a logarithm so a heavily voted asset gains less per vote.
|
|
53
|
+
*/
|
|
54
|
+
export function feedbackFactor({ pos, neg }) {
|
|
55
|
+
return 1 + FEEDBACK_WEIGHT * (Math.log1p(pos) - Math.log1p(neg));
|
|
56
|
+
}
|
|
57
|
+
/** Scale fused scores by the feedback of each asset (keyed by `itemRef`) and reorder, with the same tie-break as fusion. */
|
|
58
|
+
export function applyFeedback(fused, feedback) {
|
|
59
|
+
return fused
|
|
60
|
+
.map((candidate) => {
|
|
61
|
+
const counts = feedback.get(candidate.itemRef);
|
|
62
|
+
return counts ? { ...candidate, score: candidate.score * feedbackFactor(counts) } : candidate;
|
|
63
|
+
})
|
|
64
|
+
.sort((a, b) => b.score - a.score || compareCodePoints(a.itemRef, b.itemRef));
|
|
65
|
+
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* id, event_type, query, entry_id (nullable), entry_ref, signal, metadata, source, created_at
|
|
9
9
|
*/
|
|
10
10
|
import { rethrowIfTestIsolationError } from "../../core/errors.js";
|
|
11
|
+
import { SQLITE_CHUNK_SIZE } from "../../storage/repositories/index-sql.js";
|
|
11
12
|
const USAGE_EVENT_SOURCES = new Set(["user", "improve", "task", "audit", "unknown"]);
|
|
12
13
|
/**
|
|
13
14
|
* Resolve subprocess provenance without treating an invalid value as user
|
|
@@ -64,22 +65,26 @@ export function insertUsageEvent(db, event) {
|
|
|
64
65
|
}
|
|
65
66
|
// ── Query ────────────────────────────────────────────────────────────────────
|
|
66
67
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* SUM(CASE …) query was hand-rolled inline. Returns plain numbers (NULL SUMs
|
|
71
|
-
* over an empty set are coalesced to 0) so the result fully materialises before
|
|
72
|
-
* any owning connection closes.
|
|
68
|
+
* User feedback counts for many assets at once, keyed by durable `entry_ref`.
|
|
69
|
+
* Refs with no user feedback are absent from the map. Read in one query per
|
|
70
|
+
* chunk of refs.
|
|
73
71
|
*/
|
|
74
|
-
export function
|
|
75
|
-
const counts =
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
72
|
+
export function countFeedbackSignalsByRef(db, refs) {
|
|
73
|
+
const counts = new Map();
|
|
74
|
+
for (let i = 0; i < refs.length; i += SQLITE_CHUNK_SIZE) {
|
|
75
|
+
const chunk = refs.slice(i, i + SQLITE_CHUNK_SIZE);
|
|
76
|
+
const rows = db
|
|
77
|
+
.prepare(`SELECT entry_ref,
|
|
78
|
+
SUM(CASE WHEN signal = 'positive' THEN 1 ELSE 0 END) AS pos,
|
|
79
|
+
SUM(CASE WHEN signal = 'negative' THEN 1 ELSE 0 END) AS neg
|
|
80
|
+
FROM usage_events
|
|
81
|
+
WHERE event_type = 'feedback' AND source = 'user' AND entry_ref IN (${chunk.map(() => "?").join(",")})
|
|
82
|
+
GROUP BY entry_ref`)
|
|
83
|
+
.all(...chunk);
|
|
84
|
+
for (const row of rows)
|
|
85
|
+
counts.set(row.entry_ref, { pos: row.pos, neg: row.neg });
|
|
86
|
+
}
|
|
87
|
+
return counts;
|
|
83
88
|
}
|
|
84
89
|
/**
|
|
85
90
|
* Count usage events of a given `event_type`.
|
|
@@ -95,7 +100,7 @@ export function countUsageEventsByType(db, eventType) {
|
|
|
95
100
|
/**
|
|
96
101
|
* Durable refs a user-attributed `search`, `curate` or `show` returned, or a
|
|
97
102
|
* user `feedback` named, at or after `sinceIso`. Machine traffic (`improve`,
|
|
98
|
-
* `task`, `audit`, `unknown`) is not demand, as in {@link
|
|
103
|
+
* `task`, `audit`, `unknown`) is not demand, as in {@link countFeedbackSignalsByRef}.
|
|
99
104
|
*/
|
|
100
105
|
export function listUsedEntryRefs(db, sinceIso) {
|
|
101
106
|
const rows = db
|
|
@@ -12,8 +12,9 @@ import { formatExtraParamsIssue, validateExtraParams } from "../../core/extra-pa
|
|
|
12
12
|
import { collectSensitiveValues } from "../../core/redaction.js";
|
|
13
13
|
import { resolveSecretFromStore } from "../../sources/snapshot-fetchers/secret-seam.js";
|
|
14
14
|
import { getHarness } from "../harnesses/index.js";
|
|
15
|
+
import { resolveOpencodeBin } from "../harnesses/opencode/version.js";
|
|
15
16
|
import { DEFAULT_LLM_TIMEOUT_MS } from "./config.js";
|
|
16
|
-
import { getBuiltinAgentProfile
|
|
17
|
+
import { getBuiltinAgentProfile } from "./profiles.js";
|
|
17
18
|
const LLM_CONNECTION_FIELDS = [
|
|
18
19
|
"provider",
|
|
19
20
|
"endpoint",
|
|
@@ -273,11 +274,13 @@ function lowerAgentEngine(name, engine, config) {
|
|
|
273
274
|
const platform = harness.id;
|
|
274
275
|
const sdk = platform === "opencode-sdk";
|
|
275
276
|
const builtin = getBuiltinAgentProfile(platform);
|
|
277
|
+
// An OpenCode engine without its own `bin` runs the newest OpenCode on PATH.
|
|
278
|
+
const opencodeFamily = platform === "opencode" || sdk;
|
|
276
279
|
const profile = {
|
|
277
280
|
name,
|
|
278
281
|
platform,
|
|
279
282
|
personaChannel: sdk ? "native" : (harness.agentBuilder?.personaChannel ?? "prompt"),
|
|
280
|
-
bin: engine.bin ?? builtin?.bin ??
|
|
283
|
+
bin: opencodeFamily ? resolveOpencodeBin(engine.bin) : (engine.bin ?? builtin?.bin ?? platform),
|
|
281
284
|
args: engine.args ?? builtin?.args ?? [],
|
|
282
285
|
stdio: "captured",
|
|
283
286
|
...(builtin?.env ? { env: builtin.env } : {}),
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
export { DEFAULT_AGENT_TIMEOUT_MS } from "./config.js";
|
|
5
5
|
export { _setAgentDetectForTests, defaultWhich, detectAgentCliProfiles, pickDefaultAgentProfile } from "./detect.js";
|
|
6
|
-
export {
|
|
6
|
+
export { getBuiltinAgentProfile, listBuiltinAgentProfiles, } from "./profiles.js";
|
|
7
7
|
export { buildProposePrompt, buildReflectPrompt, parseAgentProposalPayload } from "./prompts.js";
|