akm-cli 0.9.1 → 0.9.2-alpha.1
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 +88 -0
- package/README.md +3 -1
- package/SECURITY.md +1 -1
- package/STABILITY.md +1 -1
- package/dist/akm +2 -2
- package/dist/akm-migrate +2 -2
- package/dist/assets/hints/cli-hints-full.md +14 -9
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -1
- package/dist/assets/improve-strategies/reflect-distill.json +1 -1
- package/dist/assets/models.json +35 -0
- package/dist/assets/tasks/core/extract.yml +6 -5
- package/dist/assets/tasks/core/improve.yml +6 -5
- package/dist/assets/tasks/core/index-refresh.yml +6 -5
- package/dist/assets/tasks/core/sync.yml +6 -5
- package/dist/assets/tasks/core/version-check.yml +6 -5
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +6 -5
- package/dist/cli/confirm.js +2 -2
- package/dist/cli/parse-args.js +3 -24
- package/dist/cli/retired-commands.js +1 -1
- package/dist/cli/shared.js +2 -2
- package/dist/cli.js +11 -9
- package/dist/commands/agent/agent-dispatch.js +55 -89
- package/dist/commands/agent/contribute-cli.js +12 -45
- package/dist/commands/command/builtin-action.js +32 -0
- package/dist/commands/command/command-cli.js +99 -0
- package/dist/commands/command/command-execution.js +308 -0
- package/dist/commands/command/execution-source-loader.js +176 -0
- package/dist/commands/command/portable-template.js +60 -0
- package/dist/commands/config-cli.js +10 -4
- package/dist/commands/env/env.js +4 -2
- package/dist/commands/feedback-cli.js +1 -1
- package/dist/commands/health/checks.js +241 -29
- package/dist/commands/health/html-report.js +0 -14
- package/dist/commands/health/report-view-model.js +0 -1
- package/dist/commands/health/surfaces.js +6 -7
- package/dist/commands/health/types.js +0 -2
- package/dist/commands/health.js +63 -18
- package/dist/commands/improve/collapse-detector.js +2 -2
- package/dist/commands/improve/consolidate.js +251 -214
- package/dist/commands/improve/distill/promote-memory.js +71 -34
- package/dist/commands/improve/distill/quality-gate.js +17 -5
- package/dist/commands/improve/distill.js +232 -155
- package/dist/commands/improve/eligibility.js +112 -79
- package/dist/commands/improve/execution.js +57 -0
- package/dist/commands/improve/extract-cli.js +5 -5
- package/dist/commands/improve/extract.js +521 -342
- package/dist/commands/improve/improve-strategies.js +43 -14
- package/dist/commands/improve/improve.js +249 -29
- package/dist/commands/improve/loop-stages.js +11 -17
- package/dist/commands/improve/memory/memory-contradiction-detect.js +90 -66
- package/dist/commands/improve/outcome-loop.js +22 -38
- package/dist/commands/improve/planner.js +134 -0
- package/dist/commands/improve/preparation.js +730 -409
- package/dist/commands/improve/reflect.js +386 -223
- package/dist/commands/improve/run-context.js +3 -4
- package/dist/commands/improve/salience.js +6 -58
- package/dist/commands/improve/session-asset.js +12 -12
- package/dist/commands/lint/index.js +101 -29
- package/dist/commands/migrate-cli.js +11 -69
- package/dist/commands/migration-tool.js +6 -9
- package/dist/commands/models-cli.js +27 -0
- package/dist/commands/proposal/drain.js +258 -186
- package/dist/commands/proposal/proposal-cli.js +32 -10
- package/dist/commands/proposal/proposal.js +2 -5
- package/dist/commands/proposal/propose.js +192 -172
- package/dist/commands/proposal/repository.js +54 -91
- package/dist/commands/proposal/validators/proposal-validators.js +9 -7
- package/dist/commands/read/curate.js +10 -0
- package/dist/commands/read/registry-search.js +25 -9
- package/dist/commands/read/remember-cli.js +14 -2
- package/dist/commands/read/search.js +10 -4
- package/dist/commands/read/show.js +139 -153
- package/dist/commands/registry-cli.js +16 -7
- package/dist/commands/remember.js +33 -18
- package/dist/commands/sources/add-cli.js +19 -178
- package/dist/commands/sources/bundle-cli.js +15 -3
- package/dist/commands/sources/dangerous-env-audit.js +135 -0
- package/dist/commands/sources/info.js +2 -1
- package/dist/commands/sources/installed-stashes.js +835 -152
- package/dist/commands/sources/schema-repair.js +174 -95
- package/dist/commands/sources/self-update.js +30 -74
- package/dist/commands/sources/source-add.js +3 -5
- package/dist/commands/sources/sources-cli.js +2 -15
- package/dist/commands/sources/update-transaction.js +220 -0
- package/dist/commands/tasks/tasks-cli.js +3 -3
- package/dist/commands/tasks/tasks.js +736 -317
- package/dist/commands/workflow-cli.js +2 -2
- package/dist/core/adapter/adapters/agent-skills-adapter.js +3 -0
- package/dist/core/adapter/adapters/akm-adapter.js +83 -34
- package/dist/core/adapter/adapters/akm-lint.js +54 -39
- package/dist/core/adapter/adapters/akm-metadata.js +45 -45
- package/dist/core/adapter/adapters/akm-task-adapter.js +32 -49
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +38 -23
- package/dist/core/adapter/adapters/dotenv-adapter.js +30 -1
- package/dist/core/adapter/adapters/generic-files-adapter.js +11 -0
- package/dist/core/adapter/adapters/index.js +0 -9
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +4 -0
- package/dist/core/adapter/adapters/okf-adapter.js +4 -0
- package/dist/core/adapter/adapters/opencode-adapter.js +5 -8
- package/dist/core/adapter/adapters/tool-dir-shared.js +63 -6
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -0
- package/dist/core/adapter/execution-source.js +308 -0
- package/dist/core/adapter/recognize-match.js +36 -13
- package/dist/core/adapter/registry.js +0 -9
- package/dist/core/asset/stash-meta.js +94 -4
- package/dist/core/common.js +6 -11
- package/dist/core/config/config-io.js +3 -3
- package/dist/core/config/config-schema.js +18 -40
- package/dist/core/config/config-sources.js +11 -21
- package/dist/core/config/config-walker.js +31 -13
- package/dist/core/config/config.js +21 -20
- package/dist/core/config/schema/engines.js +8 -7
- package/dist/core/config/schema/improve-processes.js +29 -5
- package/dist/core/config/schema/primitives.js +1 -23
- package/dist/core/config/schema/sources-bundles.js +13 -16
- package/dist/core/errors.js +2 -0
- package/dist/core/events.js +68 -32
- package/dist/core/extra-params.js +1 -0
- package/dist/core/improve-result.js +315 -0
- package/dist/core/lesson-lint.js +0 -6
- package/dist/core/maintenance-barrier.js +4 -4
- package/dist/core/network-policy.js +152 -0
- package/dist/core/paths.js +1 -1
- package/dist/core/recognition-util.js +4 -4
- package/dist/core/registry-url.js +456 -0
- package/dist/core/state/migrations.js +161 -47
- package/dist/core/state-db.js +453 -80
- package/dist/core/system-error.js +32 -0
- package/dist/core/time.js +2 -12
- package/dist/core/write-source.js +0 -18
- package/dist/execution/directory-identity.js +52 -0
- package/dist/execution/executable-identity.js +107 -0
- package/dist/execution/guarded-source.js +398 -0
- package/dist/execution/json.js +95 -0
- package/dist/{commands/health/types-session-log.js → execution/limits.js} +2 -1
- package/dist/execution/record.js +55 -0
- package/dist/execution/resolved-request.js +730 -0
- package/dist/execution/source.js +320 -0
- package/dist/indexer/bundle-identity-guard.js +5 -4
- package/dist/indexer/db/graph-db.js +33 -0
- package/dist/indexer/graph/graph-boost.js +3 -4
- package/dist/indexer/graph/graph-extraction.js +562 -373
- package/dist/indexer/index-written-assets.js +65 -34
- package/dist/indexer/indexer.js +443 -222
- package/dist/indexer/installations.js +6 -0
- package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
- package/dist/indexer/passes/memory-inference.js +227 -174
- package/dist/indexer/scan/doc-to-entry.js +7 -9
- package/dist/indexer/scan/drain-dir.js +51 -23
- package/dist/indexer/search/db-search.js +98 -22
- package/dist/indexer/search/search-fields.js +1 -1
- package/dist/indexer/search/search-hit-enrichers.js +1 -1
- package/dist/indexer/search/search-source.js +7 -16
- package/dist/indexer/search/semantic-status.js +10 -1
- package/dist/indexer/usage/show-usage.js +105 -0
- package/dist/indexer/usage/usage-events.js +7 -2
- package/dist/indexer/walk/matchers.js +40 -10
- package/dist/indexer/walk/path-resolver.js +5 -2
- package/dist/indexer/walk/walker.js +20 -2
- package/dist/integrations/agent/builder-shared.js +3 -6
- package/dist/integrations/agent/conversation-fallback.js +16 -0
- package/dist/integrations/agent/engine-resolution.js +87 -87
- package/dist/integrations/agent/execution-cascade.js +566 -0
- package/dist/integrations/agent/execution-definitions.js +211 -0
- package/dist/integrations/agent/execution-lowering.js +811 -0
- package/dist/integrations/agent/execution-preparation.js +67 -0
- package/dist/integrations/agent/index.js +0 -2
- package/dist/integrations/agent/inline-execution.js +74 -0
- package/dist/integrations/agent/model-map.js +515 -0
- package/dist/integrations/agent/persona-fallback.js +30 -0
- package/dist/integrations/agent/request-lowering.js +186 -0
- package/dist/integrations/agent/runner-dispatch.js +230 -37
- package/dist/integrations/agent/runner.js +12 -83
- package/dist/integrations/harnesses/aider/agent-builder.js +8 -0
- package/dist/integrations/harnesses/aider/index.js +0 -1
- package/dist/integrations/harnesses/amazonq/agent-builder.js +8 -0
- package/dist/integrations/harnesses/amazonq/index.js +0 -1
- package/dist/integrations/harnesses/claude/agent-builder.js +14 -1
- package/dist/integrations/harnesses/claude/index.js +1 -5
- package/dist/integrations/harnesses/claude/session-log.js +3 -33
- package/dist/integrations/harnesses/codex/agent-builder.js +8 -0
- package/dist/integrations/harnesses/codex/index.js +0 -1
- package/dist/integrations/harnesses/copilot/agent-builder.js +8 -0
- package/dist/integrations/harnesses/copilot/index.js +0 -1
- package/dist/integrations/harnesses/gemini/agent-builder.js +8 -0
- package/dist/integrations/harnesses/gemini/index.js +0 -1
- package/dist/integrations/harnesses/index.js +4 -44
- package/dist/integrations/harnesses/opencode/agent-builder.js +16 -9
- package/dist/integrations/harnesses/opencode/index.js +0 -2
- package/dist/integrations/harnesses/opencode/session-log.js +14 -204
- package/dist/integrations/harnesses/opencode-sdk/harness.js +12 -1
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +40 -42
- package/dist/integrations/harnesses/openhands/agent-builder.js +8 -0
- package/dist/integrations/harnesses/openhands/index.js +0 -1
- package/dist/integrations/harnesses/pi/agent-builder.js +8 -0
- package/dist/integrations/harnesses/pi/index.js +0 -1
- package/dist/integrations/harnesses/shared.js +0 -1
- package/dist/integrations/harnesses/types.js +1 -3
- package/dist/integrations/lockfile.js +82 -79
- package/dist/integrations/session-logs/index.js +6 -17
- package/dist/integrations/session-logs/provider-base.js +1 -29
- package/dist/llm/client.js +10 -5
- package/dist/llm/embedder.js +6 -7
- package/dist/llm/embedders/local.js +37 -88
- package/dist/llm/embedders/types.js +1 -1
- package/dist/llm/graph-extract.js +75 -50
- package/dist/llm/index-passes.js +43 -5
- package/dist/llm/memory-infer.js +8 -6
- package/dist/llm/metadata-enhance.js +5 -3
- package/dist/llm/structured-call.js +122 -25
- package/dist/output/format-exempt.js +1 -1
- package/dist/output/render-registry.js +0 -16
- package/dist/output/renderers.js +12 -7
- package/dist/output/shapes/curate.js +1 -0
- package/dist/output/shapes/helpers.js +10 -2
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/command-format.js +28 -32
- package/dist/output/text/health-format.js +1 -29
- package/dist/output/text/migrate.js +6 -56
- package/dist/output/text/proposal-format.js +16 -1
- package/dist/output/text/workflow-format.js +16 -0
- package/dist/registry/network.js +279 -0
- package/dist/registry/pinned-request-helper.js +247 -0
- package/dist/registry/pinned-transport.js +717 -0
- package/dist/registry/providers/skills-sh.js +18 -6
- package/dist/registry/providers/static-index.js +20 -7
- package/dist/registry/resolve.js +53 -28
- package/dist/scripts/akm-migrate-node.js +19122 -52218
- package/dist/scripts/akm-migrate.js +19064 -51567
- package/dist/setup/registry-stash-loader.js +64 -20
- package/dist/setup/semantic-assets.js +9 -34
- package/dist/setup/setup.js +12 -30
- package/dist/setup/source-identity.js +17 -0
- package/dist/setup/steps/sources.js +36 -15
- package/dist/setup/steps/tasks.js +39 -11
- package/dist/sources/providers/git-provider.js +3 -3
- package/dist/sources/providers/npm.js +2 -2
- package/dist/sources/providers/provider-utils.js +4 -3
- package/dist/sources/providers/website.js +11 -7
- package/dist/sources/snapshot-fetchers/host-guard.js +9 -136
- package/dist/sources/snapshot-fetchers/website-ingest.js +25 -109
- package/dist/sources/website-url.js +73 -0
- package/dist/storage/engines/sqlite-migrations.js +81 -26
- package/dist/storage/managed-db.js +27 -24
- package/dist/storage/repositories/events-repository.js +3 -0
- package/dist/storage/repositories/index-connection.js +42 -10
- package/dist/storage/repositories/index-entries-repository.js +167 -207
- package/dist/storage/repositories/index-entry-mapper.js +8 -12
- package/dist/storage/repositories/index-entry-schema.js +255 -0
- package/dist/storage/repositories/index-fts-repository.js +10 -10
- package/dist/storage/repositories/index-llm-cache-repository.js +8 -13
- package/dist/storage/repositories/index-meta-repository.js +0 -11
- package/dist/storage/repositories/index-schema.js +71 -342
- package/dist/storage/repositories/index-utility-repository.js +12 -17
- package/dist/storage/repositories/index-vec-repository.js +1 -1
- package/dist/storage/repositories/proposals-repository.js +4 -127
- package/dist/storage/repositories/registry-cache.js +2 -1
- package/dist/storage/repositories/task-history-repository.js +20 -40
- package/dist/storage/repositories/workflow-runs-repository.js +228 -129
- package/dist/storage/sqlite-read-snapshot.js +148 -0
- package/dist/tasks/backends/cron.js +170 -42
- package/dist/tasks/backends/index.js +1 -1
- package/dist/tasks/backends/launchd.js +787 -202
- package/dist/tasks/backends/schtasks.js +282 -83
- package/dist/tasks/embedded.js +7 -7
- package/dist/tasks/frozen-script.js +50 -0
- package/dist/tasks/resolve-akm-bin.js +5 -1
- package/dist/tasks/runner.js +239 -251
- package/dist/tasks/runtime-v3.js +281 -0
- package/dist/tasks/scheduler-binding.js +272 -0
- package/dist/tasks/scheduler-invocation.js +57 -43
- package/dist/tasks/scheduler-sync.js +654 -0
- package/dist/tasks/source-v3.js +752 -0
- package/dist/tasks/standalone-script-entry.js +5 -0
- package/dist/tasks/task-id.js +29 -0
- package/dist/workflows/authoring/authoring.js +15 -32
- package/dist/workflows/exec/dispatch-redaction.js +14 -8
- package/dist/workflows/exec/exec-unit.js +7 -28
- package/dist/workflows/exec/frozen-judge.js +57 -89
- package/dist/workflows/exec/lowering-notices.js +23 -0
- package/dist/workflows/exec/native-executor.js +301 -458
- package/dist/workflows/exec/param-secrets.js +4 -3
- package/dist/workflows/exec/run-workflow.js +26 -32
- package/dist/workflows/exec/step-work.js +105 -109
- package/dist/workflows/exec/unit-dispatch.js +103 -27
- package/dist/workflows/exec/unit-writer.js +3 -3
- package/dist/workflows/exec/worktree.js +2 -2
- package/dist/workflows/ir/compile.js +86 -72
- package/dist/workflows/ir/environment-v4.js +328 -0
- package/dist/workflows/ir/freeze-v4.js +122 -0
- package/dist/workflows/ir/plan-hash.js +13 -7
- package/dist/workflows/ir/schema-v4.js +525 -0
- package/dist/workflows/ir/schema.js +25 -284
- package/dist/workflows/ir/source-freeze-v4.js +506 -0
- package/dist/workflows/parser.js +27 -24
- package/dist/workflows/program/schema.js +1 -2
- package/dist/workflows/renderer.js +42 -29
- package/dist/workflows/resource-limits.js +4 -5
- package/dist/workflows/runtime/agent-identity.js +11 -13
- package/dist/workflows/runtime/plan-classifier.js +8 -8
- package/dist/workflows/runtime/runs.js +27 -43
- package/dist/workflows/runtime/workflow-asset-loader.js +45 -205
- package/dist/workflows/source-files.js +373 -0
- package/dist/workflows/source-ir/compile.js +196 -0
- package/dist/workflows/source-ir/github-yaml.js +577 -0
- package/dist/workflows/source-ir/ordering.js +38 -0
- package/dist/workflows/source-ir/program.js +50 -0
- package/dist/workflows/source-ir/result.js +26 -0
- package/dist/workflows/source-ir/schema.js +772 -0
- package/dist/workflows/source-ir/semantics.js +242 -0
- package/dist/workflows/source-ir/uses.js +14 -0
- package/docs/README.md +2 -0
- package/docs/migration/README.md +3 -1
- package/docs/migration/release-notes/0.9.2.md +55 -0
- package/docs/migration/release-notes/README.md +5 -0
- package/docs/migration/v0.8-to-v0.9.md +76 -1077
- package/docs/migration/v0.9.0-troubleshooting.md +104 -516
- package/docs/migration/v0.9.1-to-v0.9.2.md +150 -0
- package/docs/reference/README.md +1 -0
- package/docs/reference/cli.md +230 -98
- package/docs/reference/configuration.md +153 -27
- package/docs/reference/data-and-telemetry.md +19 -1
- package/docs/reference/supported-formats.md +23 -3
- package/docs/reference/tasks.md +182 -0
- package/docs/reference/workflow-schema.md +91 -40
- package/docs/reference/workflows.md +33 -6
- package/package.json +10 -6
- package/schemas/akm-config.json +372 -216
- package/schemas/akm-task.json +324 -80
- package/schemas/akm-workflow.json +6 -9
- package/dist/core/migration-operation.js +0 -75
- package/dist/integrations/agent/model-aliases.js +0 -74
- package/dist/tasks/parser.js +0 -380
- package/dist/tasks/schema.js +0 -123
- package/dist/tasks/validator.js +0 -80
- package/dist/workflows/ir/freeze.js +0 -320
- package/dist/workflows/runtime/document-cache.js +0 -13
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* `akm extract` — session-insight extractor.
|
|
6
6
|
*
|
|
7
7
|
* Replaces the akm-plugin session-checkpoint hook with an on-demand extractor
|
|
8
|
-
* that reads native session files (claude
|
|
8
|
+
* that reads native session files (claude JSONL, opencode storage tree)
|
|
9
9
|
* through the {@link SessionLogHarness} registry, pre-filters noise, and asks
|
|
10
10
|
* a bounded in-tree LLM to produce candidate memory/lesson/knowledge proposals
|
|
11
11
|
* for content the agent did NOT preserve via inline `akm remember`/`akm feedback`.
|
|
@@ -34,20 +34,20 @@ import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquire
|
|
|
34
34
|
import { tryAcquireMaintenanceBarrier } from "../../core/maintenance-barrier.js";
|
|
35
35
|
import { resolveStashStandards } from "../../core/standards/resolve-stash-standards.js";
|
|
36
36
|
import { resolveTypeConventions, typeConventionRef } from "../../core/standards/resolve-type-conventions.js";
|
|
37
|
-
import { getStateDbPath, openStateDatabase
|
|
37
|
+
import { getStateDbPath, openStateDatabase } from "../../core/state-db.js";
|
|
38
38
|
import { repairTruncatedDescription } from "../../core/text-truncation.js";
|
|
39
39
|
import { DURATION_UNITS, parseDuration } from "../../core/time.js";
|
|
40
40
|
import { warn } from "../../core/warn.js";
|
|
41
41
|
import { indexWrittenAssets } from "../../indexer/index-written-assets.js";
|
|
42
|
-
import {
|
|
43
|
-
import { materializeLlmRunnerConnection, resolveImproveProcessRunner, } from "../../integrations/agent/runner.js";
|
|
44
|
-
import { normalizeHarnessId } from "../../integrations/harnesses/index.js";
|
|
42
|
+
import { disposeLoweredExecutionDispatchLease, } from "../../integrations/agent/execution-lowering.js";
|
|
45
43
|
import { getAvailableHarnesses } from "../../integrations/session-logs/index.js";
|
|
46
44
|
import { preFilterSession } from "../../integrations/session-logs/pre-filter.js";
|
|
47
|
-
import { callStructured } from "../../llm/structured-call.js";
|
|
45
|
+
import { callStructured, preflightStructuredLlmRunner } from "../../llm/structured-call.js";
|
|
48
46
|
import { sha256Hex } from "../../runtime.js";
|
|
49
47
|
import { getExtractedSessionsMap, getLastExtractRunAt, shouldSkipAlreadyExtractedSession, upsertExtractedSession, } from "../../storage/repositories/extract-sessions-repository.js";
|
|
48
|
+
import { openSqliteReadSnapshot } from "../../storage/sqlite-read-snapshot.js";
|
|
50
49
|
import { isProposalSkipped } from "../proposal/repository.js";
|
|
50
|
+
import { resolveImproveLlmExecution } from "./execution.js";
|
|
51
51
|
import { buildExtractPrompt, EXTRACT_JSON_SCHEMA, parseExtractPayload } from "./extract-prompt.js";
|
|
52
52
|
import { resolveImproveStrategy, resolveProcessEnabled } from "./improve-strategies.js";
|
|
53
53
|
import { emitProposal } from "./proposal-envelope.js";
|
|
@@ -99,21 +99,35 @@ function resolveDefaultSinceMs(harnessName, now, opts) {
|
|
|
99
99
|
const floor = now - DEFAULT_SINCE_FLOOR_MS;
|
|
100
100
|
if (opts.skipTracking)
|
|
101
101
|
return floor;
|
|
102
|
+
let snapshot;
|
|
102
103
|
try {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
let db = opts.stateDb;
|
|
105
|
+
if (!db) {
|
|
106
|
+
snapshot = openSqliteReadSnapshot(opts.stateDbPath ?? getStateDbPath());
|
|
107
|
+
db = snapshot;
|
|
108
|
+
}
|
|
109
|
+
if (!db)
|
|
110
|
+
return floor;
|
|
111
|
+
const lastRun = getLastExtractRunAt(db, harnessName);
|
|
112
|
+
return lastRun != null ? Math.min(lastRun, floor) : floor;
|
|
107
113
|
}
|
|
108
114
|
catch {
|
|
109
115
|
return floor;
|
|
110
116
|
}
|
|
117
|
+
finally {
|
|
118
|
+
snapshot?.close();
|
|
119
|
+
}
|
|
111
120
|
}
|
|
112
121
|
/** Filesystem-safe per-session lock path, co-located with the state.db. */
|
|
113
122
|
function getExtractSessionLockPath(harness, sessionId, stateDbPath) {
|
|
114
123
|
const safe = `${harness}-${sessionId}`.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
115
124
|
return path.join(path.dirname(stateDbPath), "extract-locks", `extract-${safe}.lock`);
|
|
116
125
|
}
|
|
126
|
+
function extractSessionLockIsUnavailable(harness, sessionId, stateDbPath) {
|
|
127
|
+
const lockPath = getExtractSessionLockPath(harness, sessionId, stateDbPath);
|
|
128
|
+
const probe = probeLock(lockPath, { staleAfterMs: EXTRACT_SESSION_LOCK_STALE_MS });
|
|
129
|
+
return probe.state === "held" || probe.state === "inaccessible";
|
|
130
|
+
}
|
|
117
131
|
/**
|
|
118
132
|
* Try to claim the per-session extract lock so a concurrent extract (e.g. a
|
|
119
133
|
* session-end hook firing `--session-id` while the hourly improve pass runs
|
|
@@ -172,27 +186,28 @@ export function resolveStandaloneExtractPlan(config, selection) {
|
|
|
172
186
|
...(selection.engine ? { engine: selection.engine } : {}),
|
|
173
187
|
...(Object.hasOwn(selection, "timeoutMs") ? { timeoutMs: selection.timeoutMs ?? null } : {}),
|
|
174
188
|
};
|
|
175
|
-
const resolved =
|
|
189
|
+
const resolved = resolveImproveLlmExecution({
|
|
190
|
+
config,
|
|
191
|
+
profile: selected.config,
|
|
192
|
+
process,
|
|
193
|
+
current: invocation,
|
|
194
|
+
processName: "extract",
|
|
195
|
+
});
|
|
176
196
|
if (!resolved) {
|
|
177
197
|
throw new ConfigError("No LLM engine configured for extract. Set defaults.llmEngine, pass --engine, or select an improve strategy with processes.extract.engine.", "LLM_NOT_CONFIGURED");
|
|
178
198
|
}
|
|
179
|
-
const runner =
|
|
180
|
-
kind: "llm",
|
|
181
|
-
engine: resolved.engine,
|
|
182
|
-
connection: resolved.connection,
|
|
183
|
-
...(resolved.credential ? { credential: resolved.credential } : {}),
|
|
184
|
-
timeoutMs: resolved.timeoutMs,
|
|
185
|
-
};
|
|
199
|
+
const runner = resolved.runner;
|
|
186
200
|
return Object.freeze({
|
|
187
201
|
strategy: selected.name,
|
|
188
|
-
engine:
|
|
202
|
+
engine: runner.engine,
|
|
189
203
|
// `akm extract` is an explicit operation. The strategy supplies behavior,
|
|
190
204
|
// but its improve-stage enablement gate does not disable this command.
|
|
191
205
|
enabled: true,
|
|
192
206
|
process,
|
|
193
207
|
runner: cloneAndFreeze(runner),
|
|
194
|
-
timeoutMs:
|
|
208
|
+
timeoutMs: Object.hasOwn(runner, "timeoutMs") ? (runner.timeoutMs ?? null) : 600_000,
|
|
195
209
|
embeddingConfig: cloneAndFreeze(config.embedding),
|
|
210
|
+
...(resolved.notices.length > 0 ? { notices: cloneAndFreeze(resolved.notices) } : {}),
|
|
196
211
|
});
|
|
197
212
|
}
|
|
198
213
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
@@ -233,15 +248,7 @@ export function parseSinceArg(value, now = Date.now()) {
|
|
|
233
248
|
*/
|
|
234
249
|
function resolveHarness(type, harnesses) {
|
|
235
250
|
const pool = harnesses ?? getAvailableHarnesses();
|
|
236
|
-
|
|
237
|
-
// the Claude provider is "claude-code"), but the canonical harness id is
|
|
238
|
-
// "claude". Normalize BOTH the requested `--type` and each provider name to
|
|
239
|
-
// canonical before comparing, so `--type claude` and `--type claude-code`
|
|
240
|
-
// both resolve to the Claude provider. Behaviour fix: previously only the
|
|
241
|
-
// exact runtime string ("claude-code") matched; the canonical "claude" used
|
|
242
|
-
// everywhere else (agent profiles, config schema) silently found nothing.
|
|
243
|
-
const wanted = normalizeHarnessId(type);
|
|
244
|
-
return pool.find((h) => normalizeHarnessId(h.name) === wanted);
|
|
251
|
+
return pool.find((h) => h.name === type);
|
|
245
252
|
}
|
|
246
253
|
/**
|
|
247
254
|
* Build the ref + content for a candidate. The body must contain a
|
|
@@ -366,19 +373,7 @@ function runPreLlmSessionGates(args) {
|
|
|
366
373
|
// `--force` overrides it to re-extract a previously-extracted session.
|
|
367
374
|
const contentHash = hashSessionContent(data);
|
|
368
375
|
if (!force && shouldSkipAlreadyExtractedSession(prior, contentHash)) {
|
|
369
|
-
return {
|
|
370
|
-
skip: {
|
|
371
|
-
sessionId: sessionRef.sessionId,
|
|
372
|
-
harness: harness.name,
|
|
373
|
-
candidateCount: 0,
|
|
374
|
-
proposalIds: [],
|
|
375
|
-
preFilter: { inputCount: 0, outputCount: 0, truncatedCount: 0 },
|
|
376
|
-
warnings: [`already extracted (content unchanged) at ${prior?.processed_at}; pass --force to re-process`],
|
|
377
|
-
skipped: true,
|
|
378
|
-
skipReason: "already_extracted",
|
|
379
|
-
contentHash,
|
|
380
|
-
},
|
|
381
|
-
};
|
|
376
|
+
return { skip: alreadyExtractedResult(harness.name, sessionRef.sessionId, prior, contentHash) };
|
|
382
377
|
}
|
|
383
378
|
const filtered = preFilterSession(data, {
|
|
384
379
|
...(typeof maxTotalChars === "number" ? { maxTotalChars } : {}),
|
|
@@ -440,57 +435,124 @@ function runPreLlmSessionGates(args) {
|
|
|
440
435
|
}
|
|
441
436
|
return { data, filtered, contentHash };
|
|
442
437
|
}
|
|
438
|
+
function alreadyExtractedResult(harness, sessionId, prior, contentHash) {
|
|
439
|
+
return {
|
|
440
|
+
sessionId,
|
|
441
|
+
harness,
|
|
442
|
+
candidateCount: 0,
|
|
443
|
+
proposalIds: [],
|
|
444
|
+
preFilter: { inputCount: 0, outputCount: 0, truncatedCount: 0 },
|
|
445
|
+
warnings: [`already extracted (content unchanged) at ${prior?.processed_at}; pass --force to re-process`],
|
|
446
|
+
skipped: true,
|
|
447
|
+
skipReason: "already_extracted",
|
|
448
|
+
contentHash,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function lockedConcurrentResult(harness, summary) {
|
|
452
|
+
return {
|
|
453
|
+
sessionId: summary.sessionId,
|
|
454
|
+
harness,
|
|
455
|
+
candidateCount: 0,
|
|
456
|
+
proposalIds: [],
|
|
457
|
+
preFilter: { inputCount: 0, outputCount: 0, truncatedCount: 0 },
|
|
458
|
+
warnings: ["concurrent extract holds this session's lock — skipped (handled by the other run)"],
|
|
459
|
+
skipped: true,
|
|
460
|
+
skipReason: "locked_concurrent",
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
function planExtractSessions(args) {
|
|
464
|
+
const { candidates, options, harness, seenMap, maxSessionsPerRun, trackingEnabled, dryRun } = args;
|
|
465
|
+
const plans = [];
|
|
466
|
+
let modelCount = 0;
|
|
467
|
+
for (let index = 0; index < candidates.length; index++) {
|
|
468
|
+
if (options.signal?.aborted)
|
|
469
|
+
return { plans, deferredCandidates: candidates.slice(index) };
|
|
470
|
+
if (!options.sessionId && !options.force && maxSessionsPerRun > 0 && modelCount >= maxSessionsPerRun) {
|
|
471
|
+
return { plans, deferredCandidates: candidates.slice(index) };
|
|
472
|
+
}
|
|
473
|
+
const summary = candidates[index];
|
|
474
|
+
if (!summary)
|
|
475
|
+
continue;
|
|
476
|
+
if (trackingEnabled && !dryRun && !options.stateDb) {
|
|
477
|
+
if (extractSessionLockIsUnavailable(harness.name, summary.sessionId, options.stateDbPath ?? getStateDbPath())) {
|
|
478
|
+
plans.push({ kind: "skip", summary, result: lockedConcurrentResult(harness.name, summary) });
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const gate = runPreLlmSessionGates({
|
|
483
|
+
harness,
|
|
484
|
+
sessionRef: summary,
|
|
485
|
+
prior: seenMap.get(summary.sessionId),
|
|
486
|
+
force: options.force === true,
|
|
487
|
+
maxTotalChars: args.maxTotalChars,
|
|
488
|
+
minContentChars: args.minContentChars,
|
|
489
|
+
triage: args.triage,
|
|
490
|
+
});
|
|
491
|
+
if ("skip" in gate) {
|
|
492
|
+
plans.push({ kind: "skip", summary, result: gate.skip });
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
// Reading and classifying a session can take long enough for a concurrent
|
|
496
|
+
// session-end hook to claim its lock. Re-probe the fully classified model
|
|
497
|
+
// plan before it consumes a cap slot or forces credential materialization.
|
|
498
|
+
if (trackingEnabled &&
|
|
499
|
+
!dryRun &&
|
|
500
|
+
!options.stateDb &&
|
|
501
|
+
extractSessionLockIsUnavailable(harness.name, summary.sessionId, options.stateDbPath ?? getStateDbPath())) {
|
|
502
|
+
plans.push({ kind: "skip", summary, result: lockedConcurrentResult(harness.name, summary) });
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
plans.push({ kind: "model", summary, gate });
|
|
506
|
+
modelCount += 1;
|
|
507
|
+
}
|
|
508
|
+
return { plans, deferredCandidates: [] };
|
|
509
|
+
}
|
|
443
510
|
/**
|
|
444
|
-
* The bounded per-session extraction LLM call.
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
* `session_extraction` gate. Returns the seam result plus the `llmRaw`
|
|
511
|
+
* The bounded per-session extraction LLM call. Routes the already-resolved
|
|
512
|
+
* symbolic runner through `callStructured` under the `session_extraction`
|
|
513
|
+
* gate. Invalid configuration escapes before session/proposal state is
|
|
514
|
+
* persisted. Returns the seam result plus the `llmRaw`
|
|
449
515
|
* side-channel value that distinguishes fallback-took-over from a
|
|
450
516
|
* genuinely-empty response.
|
|
451
517
|
*/
|
|
452
518
|
async function runSessionExtractionLlmCall(args) {
|
|
453
|
-
const { config,
|
|
454
|
-
let extractLlm;
|
|
455
|
-
try {
|
|
456
|
-
extractLlm = getLlmConfig();
|
|
457
|
-
}
|
|
458
|
-
catch {
|
|
459
|
-
extractLlm = undefined;
|
|
460
|
-
}
|
|
519
|
+
const { config, llmRunner, lease, chat, prompt, timeoutMs, signal, onNotices } = args;
|
|
461
520
|
let llmRaw = "";
|
|
462
|
-
const llmResult =
|
|
463
|
-
|
|
464
|
-
:
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
521
|
+
const llmResult = await callStructured({
|
|
522
|
+
feature: "session_extraction",
|
|
523
|
+
akmConfig: config,
|
|
524
|
+
runner: llmRunner,
|
|
525
|
+
lease,
|
|
526
|
+
messages: [{ role: "user", content: prompt }],
|
|
527
|
+
request: {
|
|
528
|
+
timeoutMs,
|
|
529
|
+
responseSchema: EXTRACT_JSON_SCHEMA,
|
|
530
|
+
...(signal ? { signal } : {}),
|
|
531
|
+
...(chat ? { chat } : {}),
|
|
532
|
+
},
|
|
533
|
+
onNotices,
|
|
534
|
+
parse: (raw) => {
|
|
535
|
+
llmRaw = raw ?? "";
|
|
536
|
+
return llmRaw;
|
|
537
|
+
},
|
|
538
|
+
// A transport throw takes the "" fallback with llmRaw left unset —
|
|
539
|
+
// the same skipped path the gated-fn throw produced before.
|
|
540
|
+
onError: () => "",
|
|
541
|
+
fallback: "",
|
|
542
|
+
});
|
|
484
543
|
return { llmResult, llmRaw };
|
|
485
544
|
}
|
|
545
|
+
function extractNoticeFields(getNotices) {
|
|
546
|
+
const notices = getNotices();
|
|
547
|
+
return notices.length > 0 ? { notices } : {};
|
|
548
|
+
}
|
|
486
549
|
async function processSession(runCtx, session) {
|
|
487
|
-
const { harness, stashDir, config,
|
|
488
|
-
const { sessionRef,
|
|
550
|
+
const { harness, stashDir, config, llmRunner, lease, onNotices, getNotices, chat, ctx, eventsCtx, sourceRun, dryRun, timeoutMs, sessionIndexing, signal, standardsContext, } = runCtx;
|
|
551
|
+
const { sessionRef, gate } = session;
|
|
489
552
|
const warnings = [];
|
|
490
|
-
const gate = runPreLlmSessionGates({ harness, sessionRef, prior, force, maxTotalChars, minContentChars, triage });
|
|
491
|
-
if ("skip" in gate)
|
|
492
|
-
return gate.skip;
|
|
493
553
|
const { data, filtered, contentHash } = gate;
|
|
554
|
+
if (!lease)
|
|
555
|
+
throw new TypeError("extract model work requires an operation dispatch lease");
|
|
494
556
|
const prompt = buildExtractPrompt({
|
|
495
557
|
data,
|
|
496
558
|
events: filtered.events,
|
|
@@ -508,7 +570,7 @@ async function processSession(runCtx, session) {
|
|
|
508
570
|
if (!sessionMeetsDurationGate(data, sessionIndexing.minDurationMinutes))
|
|
509
571
|
return {};
|
|
510
572
|
try {
|
|
511
|
-
const result = await writeSessionAsset(data, stashDir, sessionIndexing.generate);
|
|
573
|
+
const result = await writeSessionAsset(data, stashDir, (summaryData) => sessionIndexing.generate(summaryData, lease));
|
|
512
574
|
if (result.written) {
|
|
513
575
|
// Write-path indexing (itself fail-open): standalone `akm extract`
|
|
514
576
|
// (session-end hook) has no post-loop reindex to pick this file up.
|
|
@@ -521,17 +583,21 @@ async function processSession(runCtx, session) {
|
|
|
521
583
|
}
|
|
522
584
|
}
|
|
523
585
|
catch (err) {
|
|
586
|
+
if (err instanceof ConfigError)
|
|
587
|
+
throw err;
|
|
524
588
|
warnings.push(`session asset write failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
525
589
|
}
|
|
526
590
|
return {};
|
|
527
591
|
};
|
|
528
592
|
const { llmResult, llmRaw } = await runSessionExtractionLlmCall({
|
|
529
593
|
config,
|
|
530
|
-
|
|
594
|
+
llmRunner,
|
|
595
|
+
lease,
|
|
531
596
|
chat,
|
|
532
597
|
prompt,
|
|
533
598
|
timeoutMs,
|
|
534
599
|
signal,
|
|
600
|
+
onNotices,
|
|
535
601
|
});
|
|
536
602
|
if (llmResult === "" && !llmRaw) {
|
|
537
603
|
// The seam took the fallback path (disabled / timeout / error). Return skipped.
|
|
@@ -549,6 +615,7 @@ async function processSession(runCtx, session) {
|
|
|
549
615
|
skipped: true,
|
|
550
616
|
skipReason: "llm_unavailable",
|
|
551
617
|
contentHash,
|
|
618
|
+
...extractNoticeFields(getNotices),
|
|
552
619
|
};
|
|
553
620
|
}
|
|
554
621
|
const payload = parseExtractPayload(llmRaw);
|
|
@@ -583,17 +650,12 @@ async function processSession(runCtx, session) {
|
|
|
583
650
|
warnings,
|
|
584
651
|
contentHash,
|
|
585
652
|
...sessionAsset,
|
|
653
|
+
...extractNoticeFields(getNotices),
|
|
586
654
|
};
|
|
587
655
|
}
|
|
588
656
|
// §23.6 fingerprint model-id term: the profile resolved for this session's
|
|
589
657
|
// LLM call (best-effort — an unconfigured profile leaves the term empty).
|
|
590
|
-
|
|
591
|
-
try {
|
|
592
|
-
extractModelId = runCtx.getLlmConfig().model;
|
|
593
|
-
}
|
|
594
|
-
catch {
|
|
595
|
-
extractModelId = undefined;
|
|
596
|
-
}
|
|
658
|
+
const extractModelId = llmRunner.connection.model;
|
|
597
659
|
for (const candidate of payload.candidates) {
|
|
598
660
|
const built = buildCandidateProposal(candidate, data.ref, sessionAsset.sessionAssetRef);
|
|
599
661
|
if (dryRun) {
|
|
@@ -658,8 +720,73 @@ async function processSession(runCtx, session) {
|
|
|
658
720
|
warnings,
|
|
659
721
|
contentHash,
|
|
660
722
|
...sessionAsset,
|
|
723
|
+
...extractNoticeFields(getNotices),
|
|
661
724
|
};
|
|
662
725
|
}
|
|
726
|
+
function recordExtractSessionOutcome(args) {
|
|
727
|
+
const { stateDb, trackingEnabled, dryRun, harness, summary, result, sourceRun } = args;
|
|
728
|
+
if (!trackingEnabled ||
|
|
729
|
+
!stateDb ||
|
|
730
|
+
dryRun ||
|
|
731
|
+
result.skipReason === "already_extracted" ||
|
|
732
|
+
result.skipReason === "locked_concurrent")
|
|
733
|
+
return;
|
|
734
|
+
try {
|
|
735
|
+
const outcome = result.skipped
|
|
736
|
+
? result.skipReason === "read_failed" || result.skipReason === "exception"
|
|
737
|
+
? "failed"
|
|
738
|
+
: "skipped"
|
|
739
|
+
: result.candidateCount === 0
|
|
740
|
+
? "no_candidates"
|
|
741
|
+
: "candidates_queued";
|
|
742
|
+
upsertExtractedSession(stateDb, {
|
|
743
|
+
harness,
|
|
744
|
+
sessionId: summary.sessionId,
|
|
745
|
+
processedAt: new Date().toISOString(),
|
|
746
|
+
sessionEndedAt: summary.endedAt ?? null,
|
|
747
|
+
outcome,
|
|
748
|
+
candidateCount: result.candidateCount,
|
|
749
|
+
proposalCount: result.proposalIds.length,
|
|
750
|
+
rationale: result.rationaleIfEmpty ?? null,
|
|
751
|
+
sourceRun,
|
|
752
|
+
contentHash: result.skipReason === "llm_unavailable" || result.skipReason === "triaged_out"
|
|
753
|
+
? null
|
|
754
|
+
: (result.contentHash ?? null),
|
|
755
|
+
metadata: {
|
|
756
|
+
preFilterInputCount: result.preFilter.inputCount,
|
|
757
|
+
preFilterOutputCount: result.preFilter.outputCount,
|
|
758
|
+
preFilterTruncatedCount: result.preFilter.truncatedCount,
|
|
759
|
+
...(result.skipReason ? { skipReason: result.skipReason } : {}),
|
|
760
|
+
...(result.sessionLogPath ? { logPath: result.sessionLogPath } : {}),
|
|
761
|
+
...(result.sessionAssetRef ? { sessionAssetRef: result.sessionAssetRef } : {}),
|
|
762
|
+
},
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
catch (err) {
|
|
766
|
+
warn(`[extract] failed to record session ${summary.sessionId} in state.db: ${err instanceof Error ? err.message : String(err)}`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
function accountExtractSessionResult(result, triageEnabled, output) {
|
|
770
|
+
output.sessions.push(result);
|
|
771
|
+
if (triageEnabled) {
|
|
772
|
+
const preempted = result.skipReason === "read_failed" ||
|
|
773
|
+
result.skipReason === "too_short" ||
|
|
774
|
+
result.skipReason === "already_extracted" ||
|
|
775
|
+
result.skipReason === "locked_concurrent";
|
|
776
|
+
if (!preempted) {
|
|
777
|
+
output.triageEvaluated += 1;
|
|
778
|
+
if (result.skipReason === "triaged_out")
|
|
779
|
+
output.triagedOut += 1;
|
|
780
|
+
else
|
|
781
|
+
output.triagePassed += 1;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (result.skipped)
|
|
785
|
+
output.skippedCount += 1;
|
|
786
|
+
else
|
|
787
|
+
output.processedCount += 1;
|
|
788
|
+
output.allProposalIds.push(...result.proposalIds);
|
|
789
|
+
}
|
|
663
790
|
/**
|
|
664
791
|
* Iterate the discovered candidate sessions: enforce the per-run cap, take the
|
|
665
792
|
* per-session cross-process lock, dispatch to {@link processSession}, aggregate
|
|
@@ -668,164 +795,137 @@ async function processSession(runCtx, session) {
|
|
|
668
795
|
* aggregation, and seen-row upsert are byte-identical.
|
|
669
796
|
*/
|
|
670
797
|
async function runExtractSessionLoop(args) {
|
|
671
|
-
const {
|
|
798
|
+
const { plans, deferredCandidates, seenMap, options, harness, stateDb, trackingEnabled, dryRun, stashDir, config, llmRunner, lease, onNotices, getNotices, chat, sourceRun, timeoutMs, triage, sessionIndexing, extractStandardsContext, topLevelWarnings, } = args;
|
|
672
799
|
// WI-7.7 §2: run-scoped processSession inputs, resolved once per run.
|
|
673
800
|
const sessionRunCtx = {
|
|
674
801
|
harness,
|
|
675
802
|
stashDir,
|
|
676
803
|
config,
|
|
677
|
-
|
|
804
|
+
llmRunner,
|
|
805
|
+
lease,
|
|
806
|
+
onNotices,
|
|
807
|
+
getNotices,
|
|
678
808
|
chat,
|
|
679
809
|
ctx: options.ctx,
|
|
680
810
|
eventsCtx: options.eventsCtx,
|
|
681
811
|
sourceRun,
|
|
682
812
|
dryRun,
|
|
683
813
|
timeoutMs,
|
|
684
|
-
maxTotalChars,
|
|
685
|
-
minContentChars,
|
|
686
|
-
triage,
|
|
687
814
|
sessionIndexing,
|
|
688
815
|
signal: options.signal,
|
|
689
816
|
standardsContext: extractStandardsContext,
|
|
690
817
|
};
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
818
|
+
const output = {
|
|
819
|
+
sessions: [],
|
|
820
|
+
processedCount: 0,
|
|
821
|
+
skippedCount: 0,
|
|
822
|
+
triageEvaluated: 0,
|
|
823
|
+
triagePassed: 0,
|
|
824
|
+
triagedOut: 0,
|
|
825
|
+
allProposalIds: [],
|
|
826
|
+
deferred: 0,
|
|
827
|
+
};
|
|
828
|
+
const workPlans = [...plans];
|
|
829
|
+
let remainingCandidates = deferredCandidates;
|
|
830
|
+
const refillModelSlot = () => {
|
|
831
|
+
if (remainingCandidates.length === 0 || options.signal?.aborted)
|
|
832
|
+
return;
|
|
833
|
+
const refill = planExtractSessions({
|
|
834
|
+
candidates: remainingCandidates,
|
|
835
|
+
options,
|
|
836
|
+
harness,
|
|
837
|
+
seenMap,
|
|
838
|
+
maxTotalChars: args.maxTotalChars,
|
|
839
|
+
minContentChars: args.minContentChars,
|
|
840
|
+
maxSessionsPerRun: 1,
|
|
841
|
+
triage,
|
|
842
|
+
trackingEnabled,
|
|
843
|
+
dryRun,
|
|
844
|
+
});
|
|
845
|
+
workPlans.push(...refill.plans);
|
|
846
|
+
remainingCandidates = refill.deferredCandidates;
|
|
847
|
+
};
|
|
848
|
+
for (const plan of workPlans) {
|
|
700
849
|
if (options.signal?.aborted)
|
|
701
850
|
break;
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
851
|
+
const { summary } = plan;
|
|
852
|
+
if (plan.kind === "skip") {
|
|
853
|
+
accountExtractSessionResult(plan.result, triage.enabled, output);
|
|
854
|
+
recordExtractSessionOutcome({
|
|
855
|
+
stateDb,
|
|
856
|
+
trackingEnabled,
|
|
857
|
+
dryRun,
|
|
858
|
+
harness: harness.name,
|
|
859
|
+
summary,
|
|
860
|
+
result: plan.result,
|
|
861
|
+
sourceRun,
|
|
862
|
+
});
|
|
863
|
+
continue;
|
|
713
864
|
}
|
|
714
|
-
// Q5 — per-session lock so two concurrent extracts (e.g. a session-end hook
|
|
715
|
-
// firing `--session-id` while the hourly improve discovery pass runs) can't
|
|
716
|
-
// both LLM-process the SAME session. The holder records the outcome; a
|
|
717
|
-
// second run skips without any LLM call. Engaged only for real cross-process
|
|
718
|
-
// runs (those that open their own state.db): dry-run is read-only, an
|
|
719
|
-
// injected `stateDb` handle is an in-process/test scenario with no cross-
|
|
720
|
-
// process race, and skip-tracking-off opts out entirely.
|
|
721
865
|
let sessionLockOwnership;
|
|
722
866
|
if (trackingEnabled && !dryRun && !options.stateDb) {
|
|
723
867
|
const sessionLockPath = getExtractSessionLockPath(harness.name, summary.sessionId, options.stateDbPath ?? getStateDbPath());
|
|
724
868
|
const sessionLock = acquireExtractSessionLock(sessionLockPath);
|
|
725
869
|
if (!sessionLock.proceed) {
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
harness: harness.name,
|
|
729
|
-
candidateCount: 0,
|
|
730
|
-
proposalIds: [],
|
|
731
|
-
preFilter: { inputCount: 0, outputCount: 0, truncatedCount: 0 },
|
|
732
|
-
warnings: ["concurrent extract holds this session's lock — skipped (handled by the other run)"],
|
|
733
|
-
skipped: true,
|
|
734
|
-
skipReason: "locked_concurrent",
|
|
735
|
-
});
|
|
736
|
-
skippedCount += 1;
|
|
870
|
+
accountExtractSessionResult(lockedConcurrentResult(harness.name, summary), triage.enabled, output);
|
|
871
|
+
refillModelSlot();
|
|
737
872
|
continue;
|
|
738
873
|
}
|
|
739
874
|
sessionLockOwnership = sessionLock.ownership;
|
|
740
875
|
}
|
|
741
876
|
try {
|
|
742
|
-
|
|
877
|
+
// Planning stays read-only so a credential failure creates no state. Once
|
|
878
|
+
// this run owns the session lock, read and gate the session again: the log
|
|
879
|
+
// may have grown, become too short after replacement, or been completed by
|
|
880
|
+
// another extractor between the planning snapshot and acquisition.
|
|
881
|
+
const currentPrior = stateDb
|
|
882
|
+
? getExtractedSessionsMap(stateDb, harness.name, [summary.sessionId]).get(summary.sessionId)
|
|
883
|
+
: seenMap.get(summary.sessionId);
|
|
884
|
+
const executionGate = runPreLlmSessionGates({
|
|
885
|
+
harness,
|
|
743
886
|
sessionRef: summary,
|
|
744
|
-
prior,
|
|
887
|
+
prior: currentPrior,
|
|
745
888
|
force: options.force === true,
|
|
889
|
+
maxTotalChars: args.maxTotalChars,
|
|
890
|
+
minContentChars: args.minContentChars,
|
|
891
|
+
triage,
|
|
746
892
|
});
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
result.
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
triagedOut += 1;
|
|
761
|
-
else
|
|
762
|
-
triagePassed += 1;
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
if (result.skipped)
|
|
766
|
-
skippedCount += 1;
|
|
767
|
-
else
|
|
768
|
-
processedCount += 1;
|
|
769
|
-
allProposalIds.push(...result.proposalIds);
|
|
770
|
-
// Persist outcome so the next run skips this session unless its content
|
|
771
|
-
// changes. We only track non-dry-run paths — dry-run is for inspection
|
|
772
|
-
// and should never poison the seen-table. #602: an `already_extracted`
|
|
773
|
-
// skip is a no-op (the row already carries the matching hash), so don't
|
|
774
|
-
// re-write it — that keeps `processed_at` stable across unchanged runs.
|
|
775
|
-
if (trackingEnabled && stateDb && !dryRun && result.skipReason !== "already_extracted") {
|
|
776
|
-
try {
|
|
777
|
-
const outcome = result.skipped
|
|
778
|
-
? result.skipReason === "read_failed" || result.skipReason === "exception"
|
|
779
|
-
? "failed"
|
|
780
|
-
: "skipped"
|
|
781
|
-
: result.candidateCount === 0
|
|
782
|
-
? "no_candidates"
|
|
783
|
-
: "candidates_queued";
|
|
784
|
-
upsertExtractedSession(stateDb, {
|
|
785
|
-
harness: harness.name,
|
|
786
|
-
sessionId: summary.sessionId,
|
|
787
|
-
processedAt: new Date().toISOString(),
|
|
788
|
-
sessionEndedAt: summary.endedAt ?? null,
|
|
789
|
-
outcome,
|
|
790
|
-
candidateCount: result.candidateCount,
|
|
791
|
-
proposalCount: result.proposalIds.length,
|
|
792
|
-
rationale: result.rationaleIfEmpty ?? null,
|
|
793
|
-
sourceRun,
|
|
794
|
-
// #602 — persist the freshly computed content hash so the NEXT run
|
|
795
|
-
// can compare byte-for-byte. read_failed (before hash) → null, which
|
|
796
|
-
// keeps the row eligible for retry (matches failed-row semantics).
|
|
797
|
-
// R4 — llm_unavailable (LLM was down) and triaged_out (deferred by the
|
|
798
|
-
// triage gate) are transient outcomes: persist null so the null-hash
|
|
799
|
-
// retry re-processes them on a later run instead of pinning them as
|
|
800
|
-
// "seen" forever against the current byte content.
|
|
801
|
-
contentHash: result.skipReason === "llm_unavailable" || result.skipReason === "triaged_out"
|
|
802
|
-
? null
|
|
803
|
-
: (result.contentHash ?? null),
|
|
804
|
-
metadata: {
|
|
805
|
-
preFilterInputCount: result.preFilter.inputCount,
|
|
806
|
-
preFilterOutputCount: result.preFilter.outputCount,
|
|
807
|
-
preFilterTruncatedCount: result.preFilter.truncatedCount,
|
|
808
|
-
...(result.skipReason ? { skipReason: result.skipReason } : {}),
|
|
809
|
-
// #561 — record the session's log_path for correlation across
|
|
810
|
-
// index rebuilds (the session asset frontmatter is the primary
|
|
811
|
-
// durable key; this is the state-db mirror of it).
|
|
812
|
-
...(result.sessionLogPath ? { logPath: result.sessionLogPath } : {}),
|
|
813
|
-
...(result.sessionAssetRef ? { sessionAssetRef: result.sessionAssetRef } : {}),
|
|
814
|
-
},
|
|
815
|
-
});
|
|
816
|
-
}
|
|
817
|
-
catch (err) {
|
|
818
|
-
// Tracking failure must not abort the run — log + continue.
|
|
819
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
820
|
-
warn(`[extract] failed to record session ${summary.sessionId} in state.db: ${msg}`);
|
|
821
|
-
}
|
|
893
|
+
if ("skip" in executionGate) {
|
|
894
|
+
accountExtractSessionResult(executionGate.skip, triage.enabled, output);
|
|
895
|
+
recordExtractSessionOutcome({
|
|
896
|
+
stateDb,
|
|
897
|
+
trackingEnabled,
|
|
898
|
+
dryRun,
|
|
899
|
+
harness: harness.name,
|
|
900
|
+
summary,
|
|
901
|
+
result: executionGate.skip,
|
|
902
|
+
sourceRun,
|
|
903
|
+
});
|
|
904
|
+
refillModelSlot();
|
|
905
|
+
continue;
|
|
822
906
|
}
|
|
907
|
+
const result = await processSession(sessionRunCtx, {
|
|
908
|
+
sessionRef: summary,
|
|
909
|
+
gate: executionGate,
|
|
910
|
+
});
|
|
911
|
+
accountExtractSessionResult(result, triage.enabled, output);
|
|
912
|
+
recordExtractSessionOutcome({
|
|
913
|
+
stateDb,
|
|
914
|
+
trackingEnabled,
|
|
915
|
+
dryRun,
|
|
916
|
+
harness: harness.name,
|
|
917
|
+
summary,
|
|
918
|
+
result,
|
|
919
|
+
sourceRun,
|
|
920
|
+
});
|
|
823
921
|
}
|
|
824
922
|
catch (err) {
|
|
923
|
+
if (err instanceof ConfigError)
|
|
924
|
+
throw err;
|
|
825
925
|
const msg = err instanceof Error ? err.message : String(err);
|
|
826
926
|
warn(`[extract] session ${summary.sessionId} threw: ${msg}`);
|
|
827
927
|
topLevelWarnings.push(`session ${summary.sessionId} threw: ${msg}`);
|
|
828
|
-
|
|
928
|
+
accountExtractSessionResult({
|
|
829
929
|
sessionId: summary.sessionId,
|
|
830
930
|
harness: harness.name,
|
|
831
931
|
candidateCount: 0,
|
|
@@ -834,15 +934,16 @@ async function runExtractSessionLoop(args) {
|
|
|
834
934
|
warnings: [msg],
|
|
835
935
|
skipped: true,
|
|
836
936
|
skipReason: "exception",
|
|
837
|
-
|
|
838
|
-
|
|
937
|
+
...extractNoticeFields(getNotices),
|
|
938
|
+
}, triage.enabled, output);
|
|
839
939
|
}
|
|
840
940
|
finally {
|
|
841
941
|
if (sessionLockOwnership)
|
|
842
942
|
releaseLock(sessionLockOwnership);
|
|
843
943
|
}
|
|
844
944
|
}
|
|
845
|
-
|
|
945
|
+
output.deferred = remainingCandidates.length;
|
|
946
|
+
return output;
|
|
846
947
|
}
|
|
847
948
|
/**
|
|
848
949
|
* Resolve the run-scoped LLM/engine, budget, triage, and session-indexing
|
|
@@ -851,25 +952,43 @@ async function runExtractSessionLoop(args) {
|
|
|
851
952
|
* session-summary generator seam, and the default resolutions are byte-identical.
|
|
852
953
|
*/
|
|
853
954
|
function resolveExtractRunConfig(options, config, extractProcess, activeProfile) {
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
const
|
|
860
|
-
|
|
955
|
+
const executionNotices = new Map();
|
|
956
|
+
const onNotices = (notices) => {
|
|
957
|
+
for (const notice of notices)
|
|
958
|
+
executionNotices.set(JSON.stringify(notice), notice);
|
|
959
|
+
};
|
|
960
|
+
const getNotices = () => Object.freeze([...executionNotices.values()]);
|
|
961
|
+
// Improve supplies its invocation-owned symbolic runner. Standalone extract
|
|
962
|
+
// resolves the selected process engine through the shared execution planner.
|
|
963
|
+
let llmRunner;
|
|
964
|
+
if (options.resolvedPlan) {
|
|
965
|
+
llmRunner = options.resolvedPlan.runner;
|
|
966
|
+
onNotices(options.resolvedPlan.notices ?? []);
|
|
967
|
+
}
|
|
968
|
+
else if (options.llmRunner) {
|
|
969
|
+
llmRunner = options.llmRunner;
|
|
970
|
+
}
|
|
971
|
+
else {
|
|
972
|
+
const resolved = resolveImproveLlmExecution({
|
|
973
|
+
config,
|
|
974
|
+
profile: activeProfile,
|
|
975
|
+
process: extractProcess,
|
|
976
|
+
processName: "extract",
|
|
977
|
+
});
|
|
978
|
+
llmRunner = resolved?.runner;
|
|
979
|
+
if (resolved)
|
|
980
|
+
onNotices(resolved.notices);
|
|
981
|
+
}
|
|
982
|
+
if (!llmRunner) {
|
|
861
983
|
throw new ConfigError("No LLM engine configured for extract. Set defaults.llmEngine or improve.strategies.<name>.processes.extract.engine.", "LLM_NOT_CONFIGURED");
|
|
862
984
|
}
|
|
863
985
|
const timeoutMs = options.resolvedPlan
|
|
864
986
|
? options.resolvedPlan.timeoutMs
|
|
865
987
|
: Object.hasOwn(options, "timeoutMs")
|
|
866
988
|
? (options.timeoutMs ?? null)
|
|
867
|
-
:
|
|
868
|
-
?
|
|
869
|
-
:
|
|
870
|
-
? (fixedLlmConfig.timeoutMs ?? null)
|
|
871
|
-
: 600_000;
|
|
872
|
-
const getLlmConfig = () => runnerSpec ? materializeLlmRunnerConnection(runnerSpec) : fixedLlmConfig;
|
|
989
|
+
: Object.hasOwn(llmRunner, "timeoutMs")
|
|
990
|
+
? (llmRunner.timeoutMs ?? null)
|
|
991
|
+
: 600_000;
|
|
873
992
|
// Pre-filter budget — process config can raise it for large-context models.
|
|
874
993
|
const maxTotalChars = typeof extractProcess?.maxTotalChars === "number" ? extractProcess.maxTotalChars : undefined;
|
|
875
994
|
// #595/#596 — minimum raw session size; sessions below it skip the LLM call
|
|
@@ -899,36 +1018,28 @@ function resolveExtractRunConfig(options, config, extractProcess, activeProfile)
|
|
|
899
1018
|
// same fail-open `callStructured` seam as the rest of extract. Returns
|
|
900
1019
|
// `undefined` on disablement / timeout / error so no asset is written.
|
|
901
1020
|
// Tests inject a fake.
|
|
902
|
-
const defaultSessionSummaryGenerator = async (data) => {
|
|
903
|
-
// Same fail-open contract as the per-session call: a getLlmConfig()
|
|
904
|
-
// throw takes the "" fallback rather than propagating.
|
|
905
|
-
let summaryLlm;
|
|
906
|
-
try {
|
|
907
|
-
summaryLlm = getLlmConfig();
|
|
908
|
-
}
|
|
909
|
-
catch {
|
|
910
|
-
summaryLlm = undefined;
|
|
911
|
-
}
|
|
1021
|
+
const defaultSessionSummaryGenerator = async (data, lease) => {
|
|
912
1022
|
let raw = "";
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
},
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1023
|
+
await callStructured({
|
|
1024
|
+
feature: "session_extraction",
|
|
1025
|
+
akmConfig: config,
|
|
1026
|
+
runner: llmRunner,
|
|
1027
|
+
...(lease ? { lease } : {}),
|
|
1028
|
+
messages: [{ role: "user", content: buildSessionSummaryPrompt(data) }],
|
|
1029
|
+
request: {
|
|
1030
|
+
timeoutMs,
|
|
1031
|
+
responseSchema: SESSION_SUMMARY_JSON_SCHEMA,
|
|
1032
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
1033
|
+
...(options.chat ? { chat: options.chat } : {}),
|
|
1034
|
+
},
|
|
1035
|
+
onNotices,
|
|
1036
|
+
parse: (r) => {
|
|
1037
|
+
raw = r ?? "";
|
|
1038
|
+
return raw;
|
|
1039
|
+
},
|
|
1040
|
+
onError: () => "",
|
|
1041
|
+
fallback: "",
|
|
1042
|
+
});
|
|
932
1043
|
return parseSessionSummary(raw);
|
|
933
1044
|
};
|
|
934
1045
|
const sessionIndexing = {
|
|
@@ -938,7 +1049,9 @@ function resolveExtractRunConfig(options, config, extractProcess, activeProfile)
|
|
|
938
1049
|
};
|
|
939
1050
|
return {
|
|
940
1051
|
timeoutMs,
|
|
941
|
-
|
|
1052
|
+
llmRunner,
|
|
1053
|
+
onNotices,
|
|
1054
|
+
getNotices,
|
|
942
1055
|
maxTotalChars,
|
|
943
1056
|
minContentChars,
|
|
944
1057
|
maxSessionsPerRun,
|
|
@@ -999,30 +1112,11 @@ function discoverExtractCandidates(options, harness, effectiveSince, startMs, dr
|
|
|
999
1112
|
/**
|
|
1000
1113
|
* WI-9.10: build one `akm extract` run's {@link RunContext} from values
|
|
1001
1114
|
* `akmExtract` has already resolved by the time it calls this (config,
|
|
1002
|
-
* stashDir, dryRun, sourceRun, and `resolveExtractRunConfig`'s
|
|
1003
|
-
*
|
|
1004
|
-
*
|
|
1005
|
-
* `RunContext.getLlmConfig` is typed `() => LlmConnectionConfig | null`, but
|
|
1006
|
-
* extract's own resolved `getLlmConfig` returns `LlmProfileConfig` (a
|
|
1007
|
-
* superset — `supportsJsonSchema` — of `LlmConnectionConfig`) and, per its
|
|
1008
|
-
* documented fail-open contract, MAY THROW (`materializeLlmConnection` can
|
|
1009
|
-
* raise ConfigError) rather than return null; every existing caller in this
|
|
1010
|
-
* file wraps it in try/catch for exactly that reason. The thin closure below
|
|
1011
|
-
* adapts at the boundary: it derives from the SAME already-resolved
|
|
1012
|
-
* runner/profile (this doesn't widen `RunContext.getLlmConfig`'s type), and —
|
|
1013
|
-
* matching the file's own fail-open contract — coalesces a throw to `null`
|
|
1014
|
-
* instead of propagating.
|
|
1115
|
+
* stashDir, dryRun, sourceRun, and `resolveExtractRunConfig`'s symbolic runner)
|
|
1116
|
+
* — no second config load, credential materialization, or new db handle.
|
|
1015
1117
|
*/
|
|
1016
1118
|
function buildExtractRunContext(args) {
|
|
1017
|
-
const { options, config, stashDir, dryRun, sourceRun,
|
|
1018
|
-
const getRunContextLlmConfig = () => {
|
|
1019
|
-
try {
|
|
1020
|
-
return getLlmConfig();
|
|
1021
|
-
}
|
|
1022
|
-
catch {
|
|
1023
|
-
return null;
|
|
1024
|
-
}
|
|
1025
|
-
};
|
|
1119
|
+
const { options, config, stashDir, dryRun, sourceRun, llmRunner } = args;
|
|
1026
1120
|
return createRunContext({
|
|
1027
1121
|
stashDir,
|
|
1028
1122
|
config,
|
|
@@ -1030,16 +1124,71 @@ function buildExtractRunContext(args) {
|
|
|
1030
1124
|
// Not yet wired into any proposal call site this stage (mirrors
|
|
1031
1125
|
// buildImproveRunContext's proposalsCtx comment in improve.ts).
|
|
1032
1126
|
proposalsCtx: options.ctx ?? {},
|
|
1033
|
-
|
|
1127
|
+
getLlmRunner: () => llmRunner,
|
|
1034
1128
|
sourceRun,
|
|
1035
1129
|
dryRun,
|
|
1036
1130
|
signal: options.signal,
|
|
1037
1131
|
});
|
|
1038
1132
|
}
|
|
1133
|
+
function loadExtractSeenMapReadOnly(args) {
|
|
1134
|
+
const { options, harness, candidates, trackingEnabled, warnings } = args;
|
|
1135
|
+
if (!trackingEnabled || candidates.length === 0)
|
|
1136
|
+
return new Map();
|
|
1137
|
+
let snapshot;
|
|
1138
|
+
try {
|
|
1139
|
+
if (!options.stateDb)
|
|
1140
|
+
snapshot = openSqliteReadSnapshot(options.stateDbPath ?? getStateDbPath());
|
|
1141
|
+
const db = options.stateDb ?? snapshot;
|
|
1142
|
+
return db
|
|
1143
|
+
? getExtractedSessionsMap(db, harness, candidates.map((candidate) => candidate.sessionId))
|
|
1144
|
+
: new Map();
|
|
1145
|
+
}
|
|
1146
|
+
catch (err) {
|
|
1147
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1148
|
+
warn(`[extract] state.db snapshot unavailable, planning without skip-tracking: ${msg}`);
|
|
1149
|
+
warnings.push(`state.db snapshot unavailable: ${msg}`);
|
|
1150
|
+
return new Map();
|
|
1151
|
+
}
|
|
1152
|
+
finally {
|
|
1153
|
+
snapshot?.close();
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
function openExtractLiveStateDb(args) {
|
|
1157
|
+
const { options, trackingEnabled, hasModelWork, dryRun, warnings } = args;
|
|
1158
|
+
if (!trackingEnabled)
|
|
1159
|
+
return undefined;
|
|
1160
|
+
if (options.stateDb)
|
|
1161
|
+
return options.stateDb;
|
|
1162
|
+
if (!hasModelWork || dryRun)
|
|
1163
|
+
return undefined;
|
|
1164
|
+
try {
|
|
1165
|
+
return openStateDatabase(options.stateDbPath);
|
|
1166
|
+
}
|
|
1167
|
+
catch (err) {
|
|
1168
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1169
|
+
warn(`[extract] state.db unavailable, processing without skip-tracking: ${msg}`);
|
|
1170
|
+
warnings.push(`state.db unavailable: ${msg}`);
|
|
1171
|
+
return undefined;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
function emitExtractTriageEvent(args) {
|
|
1175
|
+
const { modelPlanCount, triageEnabled, result, sourceRun, eventsCtx } = args;
|
|
1176
|
+
if (modelPlanCount === 0 || !triageEnabled || result.triageEvaluated === 0)
|
|
1177
|
+
return;
|
|
1178
|
+
appendEvent({
|
|
1179
|
+
eventType: "extract_triaged",
|
|
1180
|
+
metadata: {
|
|
1181
|
+
evaluated: result.triageEvaluated,
|
|
1182
|
+
passed: result.triagePassed,
|
|
1183
|
+
triagedOut: result.triagedOut,
|
|
1184
|
+
sourceRun,
|
|
1185
|
+
},
|
|
1186
|
+
}, eventsCtx);
|
|
1187
|
+
}
|
|
1039
1188
|
export async function akmExtract(options) {
|
|
1040
1189
|
const startMs = Date.now();
|
|
1041
1190
|
if (!options.type || options.type.trim() === "") {
|
|
1042
|
-
throw new UsageError("--type is required. Pass a harness name (e.g. --type claude
|
|
1191
|
+
throw new UsageError("--type is required. Pass a harness name (e.g. --type claude).", "MISSING_REQUIRED_ARGUMENT");
|
|
1043
1192
|
}
|
|
1044
1193
|
const config = options.config ?? loadConfig();
|
|
1045
1194
|
const stashDir = resolveRunStashDir(options.stashDir);
|
|
@@ -1075,10 +1224,10 @@ export async function akmExtract(options) {
|
|
|
1075
1224
|
durationMs: Date.now() - startMs,
|
|
1076
1225
|
};
|
|
1077
1226
|
}
|
|
1078
|
-
const { timeoutMs,
|
|
1227
|
+
const { timeoutMs, llmRunner, onNotices, getNotices, maxTotalChars, minContentChars, maxSessionsPerRun, effectiveSince, triage, sessionIndexing, } = resolveExtractRunConfig(options, config, extractProcess, activeProfile);
|
|
1079
1228
|
// WI-9.10: construct this run's RunContext (extracted to
|
|
1080
1229
|
// buildExtractRunContext to keep akmExtract under the fn-size bar — R31).
|
|
1081
|
-
const ctx = buildExtractRunContext({ options, config, stashDir, dryRun, sourceRun,
|
|
1230
|
+
const ctx = buildExtractRunContext({ options, config, stashDir, dryRun, sourceRun, llmRunner });
|
|
1082
1231
|
const harness = resolveHarness(options.type, options.harnesses);
|
|
1083
1232
|
if (!harness) {
|
|
1084
1233
|
return {
|
|
@@ -1118,76 +1267,94 @@ export async function akmExtract(options) {
|
|
|
1118
1267
|
return discovery.notFound;
|
|
1119
1268
|
const candidates = discovery.candidates;
|
|
1120
1269
|
const topLevelWarnings = [];
|
|
1121
|
-
// Open state.db once for the run and bulk-load seen-rows for the candidate
|
|
1122
|
-
// set so we can decide skip/process in O(1) per session. Tracking is opt-out
|
|
1123
|
-
// via options.skipTracking (used by tests + one-shot debug calls).
|
|
1124
1270
|
const trackingEnabled = options.skipTracking !== true;
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
// state.db open is best-effort — log and proceed without skip-tracking
|
|
1134
|
-
// so a transient sqlite error never blocks the actual extraction.
|
|
1135
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1136
|
-
warn(`[extract] state.db unavailable, processing without skip-tracking: ${msg}`);
|
|
1137
|
-
topLevelWarnings.push(`state.db unavailable: ${msg}`);
|
|
1138
|
-
stateDb = undefined;
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
// Stash authoring standards (convention/meta fact bodies) for non-wiki
|
|
1142
|
-
// extract output. Resolved ONCE per run and threaded into each session's
|
|
1143
|
-
// prompt so facts are not re-read per session.
|
|
1144
|
-
const extractStandardsContext = resolveExtractStandards(stashDir);
|
|
1145
|
-
const { sessions, processedCount, skippedCount, triageEvaluated, triagePassed, triagedOut, allProposalIds } = await runExtractSessionLoop({
|
|
1271
|
+
const seenMap = loadExtractSeenMapReadOnly({
|
|
1272
|
+
options,
|
|
1273
|
+
harness: harness.name,
|
|
1274
|
+
candidates,
|
|
1275
|
+
trackingEnabled,
|
|
1276
|
+
warnings: topLevelWarnings,
|
|
1277
|
+
});
|
|
1278
|
+
const planned = planExtractSessions({
|
|
1146
1279
|
candidates,
|
|
1147
1280
|
options,
|
|
1148
1281
|
harness,
|
|
1149
1282
|
seenMap,
|
|
1150
|
-
stateDb,
|
|
1151
|
-
trackingEnabled,
|
|
1152
|
-
dryRun,
|
|
1153
|
-
stashDir,
|
|
1154
|
-
config,
|
|
1155
|
-
getLlmConfig,
|
|
1156
|
-
chat: options.chat,
|
|
1157
|
-
sourceRun,
|
|
1158
|
-
timeoutMs,
|
|
1159
1283
|
maxTotalChars,
|
|
1160
1284
|
minContentChars,
|
|
1161
1285
|
maxSessionsPerRun,
|
|
1162
1286
|
triage,
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
topLevelWarnings,
|
|
1287
|
+
trackingEnabled,
|
|
1288
|
+
dryRun,
|
|
1166
1289
|
});
|
|
1167
|
-
|
|
1168
|
-
//
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1290
|
+
const modelPlanCount = planned.plans.filter((plan) => plan.kind === "model").length;
|
|
1291
|
+
// Eligible dry-runs still dispatch to produce their candidate preview. Only
|
|
1292
|
+
// deterministic no-work plans are credential-free. Materialize once after
|
|
1293
|
+
// every read-only gate and before opening live state or acquiring a lock.
|
|
1294
|
+
const dispatchLease = modelPlanCount > 0 ? await preflightStructuredLlmRunner(llmRunner) : undefined;
|
|
1295
|
+
let stateDb;
|
|
1296
|
+
let loopResult;
|
|
1297
|
+
try {
|
|
1298
|
+
stateDb = openExtractLiveStateDb({
|
|
1299
|
+
options,
|
|
1300
|
+
trackingEnabled,
|
|
1301
|
+
hasModelWork: modelPlanCount > 0,
|
|
1302
|
+
dryRun,
|
|
1303
|
+
warnings: topLevelWarnings,
|
|
1304
|
+
});
|
|
1305
|
+
// Stash authoring standards (convention/meta fact bodies) for non-wiki
|
|
1306
|
+
// extract output. Resolved ONCE per run and threaded into each session's
|
|
1307
|
+
// prompt so facts are not re-read per session.
|
|
1308
|
+
const extractStandardsContext = modelPlanCount > 0 ? resolveExtractStandards(stashDir) : "";
|
|
1309
|
+
loopResult = await runExtractSessionLoop({
|
|
1310
|
+
plans: planned.plans,
|
|
1311
|
+
deferredCandidates: planned.deferredCandidates,
|
|
1312
|
+
seenMap,
|
|
1313
|
+
options,
|
|
1314
|
+
harness,
|
|
1315
|
+
stateDb,
|
|
1316
|
+
trackingEnabled,
|
|
1317
|
+
dryRun,
|
|
1318
|
+
stashDir,
|
|
1319
|
+
config,
|
|
1320
|
+
llmRunner,
|
|
1321
|
+
lease: dispatchLease,
|
|
1322
|
+
onNotices,
|
|
1323
|
+
getNotices,
|
|
1324
|
+
chat: options.chat,
|
|
1325
|
+
sourceRun,
|
|
1326
|
+
timeoutMs,
|
|
1327
|
+
maxTotalChars,
|
|
1328
|
+
minContentChars,
|
|
1329
|
+
triage,
|
|
1330
|
+
sessionIndexing,
|
|
1331
|
+
extractStandardsContext,
|
|
1332
|
+
topLevelWarnings,
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
finally {
|
|
1336
|
+
if (stateDb && !options.stateDb) {
|
|
1337
|
+
try {
|
|
1338
|
+
stateDb.close();
|
|
1339
|
+
}
|
|
1340
|
+
catch {
|
|
1341
|
+
// best-effort close
|
|
1342
|
+
}
|
|
1175
1343
|
}
|
|
1344
|
+
if (dispatchLease)
|
|
1345
|
+
disposeLoweredExecutionDispatchLease(dispatchLease);
|
|
1176
1346
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
if (triage.enabled && triageEvaluated > 0) {
|
|
1181
|
-
appendEvent({
|
|
1182
|
-
eventType: "extract_triaged",
|
|
1183
|
-
metadata: {
|
|
1184
|
-
evaluated: triageEvaluated,
|
|
1185
|
-
passed: triagePassed,
|
|
1186
|
-
triagedOut,
|
|
1187
|
-
sourceRun,
|
|
1188
|
-
},
|
|
1189
|
-
}, options.eventsCtx);
|
|
1347
|
+
const { sessions, processedCount, skippedCount, allProposalIds } = loopResult;
|
|
1348
|
+
if (loopResult.deferred > 0) {
|
|
1349
|
+
topLevelWarnings.push(`Reached maxSessionsPerRun=${maxSessionsPerRun}; ${loopResult.deferred} session(s) deferred to a later run.`);
|
|
1190
1350
|
}
|
|
1351
|
+
emitExtractTriageEvent({
|
|
1352
|
+
modelPlanCount,
|
|
1353
|
+
triageEnabled: triage.enabled,
|
|
1354
|
+
result: loopResult,
|
|
1355
|
+
sourceRun,
|
|
1356
|
+
eventsCtx: options.eventsCtx,
|
|
1357
|
+
});
|
|
1191
1358
|
return {
|
|
1192
1359
|
schemaVersion: 1,
|
|
1193
1360
|
ok: true,
|
|
@@ -1206,6 +1373,7 @@ export async function akmExtract(options) {
|
|
|
1206
1373
|
sessions,
|
|
1207
1374
|
warnings: topLevelWarnings,
|
|
1208
1375
|
durationMs: Date.now() - startMs,
|
|
1376
|
+
...(getNotices().length > 0 ? { notices: getNotices() } : {}),
|
|
1209
1377
|
};
|
|
1210
1378
|
}
|
|
1211
1379
|
/**
|
|
@@ -1241,10 +1409,21 @@ export function countNewExtractCandidates(_config, options = {}) {
|
|
|
1241
1409
|
resolveDefaultSinceMs(harness.name, Date.now(), {
|
|
1242
1410
|
...(options.stateDb ? { stateDb: options.stateDb } : {}),
|
|
1243
1411
|
...(options.stateDbPath ? { stateDbPath: options.stateDbPath } : {}),
|
|
1412
|
+
...(options.readOnly && !options.stateDb ? { skipTracking: true } : {}),
|
|
1244
1413
|
});
|
|
1245
|
-
const candidates = harness.listSessions({
|
|
1414
|
+
const candidates = harness.listSessions({
|
|
1415
|
+
sinceMs,
|
|
1416
|
+
...(options.readOnly ? { isolatedSnapshot: true } : {}),
|
|
1417
|
+
});
|
|
1246
1418
|
if (candidates.length === 0)
|
|
1247
1419
|
continue;
|
|
1420
|
+
// A dry planner with no pre-existing state database has no seen-session
|
|
1421
|
+
// ledger by definition. Count the discovered sessions directly instead
|
|
1422
|
+
// of creating state.db merely to prove that it is empty.
|
|
1423
|
+
if (options.readOnly && !stateDb) {
|
|
1424
|
+
total += candidates.length;
|
|
1425
|
+
continue;
|
|
1426
|
+
}
|
|
1248
1427
|
let seenMap = new Map();
|
|
1249
1428
|
try {
|
|
1250
1429
|
if (!stateDb) {
|