akm-cli 0.9.1 → 0.9.2-alpha.2
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 +103 -28
- 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/stash-skeleton/facts/conventions/backlinks.md +3 -4
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +1 -3
- 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 +5 -6
- 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-prompt.js +64 -22
- package/dist/commands/improve/extract.js +608 -360
- 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 +53 -22
- 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 +901 -177
- 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 +85 -35
- 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 +23 -26
- package/dist/core/config/schema/engines.js +8 -7
- package/dist/core/config/schema/improve-processes.js +29 -5
- package/dist/core/config/schema/index-config.js +0 -27
- 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 +78 -39
- package/dist/indexer/indexer.js +471 -432
- package/dist/indexer/installations.js +6 -0
- package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
- package/dist/indexer/materialize-embeddings.js +155 -0
- package/dist/indexer/passes/memory-inference.js +227 -174
- package/dist/indexer/passes/metadata.js +263 -118
- package/dist/indexer/scan/doc-to-entry.js +7 -10
- package/dist/indexer/scan/drain-dir.js +51 -23
- package/dist/indexer/search/db-search.js +156 -50
- package/dist/indexer/search/fts-query.js +40 -40
- package/dist/indexer/search/ranking.js +36 -1
- package/dist/indexer/search/search-attribution.js +3 -1
- package/dist/indexer/search/search-fields.js +23 -14
- 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 +31 -33
- 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 +19334 -52269
- package/dist/scripts/akm-migrate.js +19270 -51612
- 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 +203 -229
- 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 +64 -71
- 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 +74 -350
- package/dist/storage/repositories/index-utility-repository.js +12 -17
- package/dist/storage/repositories/index-vec-repository.js +56 -7
- 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 +159 -36
- 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 -224
- 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`.
|
|
@@ -32,23 +32,25 @@ import { ConfigError, UsageError } from "../../core/errors.js";
|
|
|
32
32
|
import { appendEvent } from "../../core/events.js";
|
|
33
33
|
import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync, } from "../../core/file-lock.js";
|
|
34
34
|
import { tryAcquireMaintenanceBarrier } from "../../core/maintenance-barrier.js";
|
|
35
|
+
import { redactErrorBody } from "../../core/redaction.js";
|
|
35
36
|
import { resolveStashStandards } from "../../core/standards/resolve-stash-standards.js";
|
|
36
37
|
import { resolveTypeConventions, typeConventionRef } from "../../core/standards/resolve-type-conventions.js";
|
|
37
|
-
import { getStateDbPath, openStateDatabase
|
|
38
|
+
import { getStateDbPath, openStateDatabase } from "../../core/state-db.js";
|
|
39
|
+
import { runStructured } from "../../core/structured.js";
|
|
38
40
|
import { repairTruncatedDescription } from "../../core/text-truncation.js";
|
|
39
41
|
import { DURATION_UNITS, parseDuration } from "../../core/time.js";
|
|
40
|
-
import { warn } from "../../core/warn.js";
|
|
42
|
+
import { warn, warnVerbose } from "../../core/warn.js";
|
|
41
43
|
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";
|
|
44
|
+
import { disposeLoweredExecutionDispatchLease, } from "../../integrations/agent/execution-lowering.js";
|
|
45
45
|
import { getAvailableHarnesses } from "../../integrations/session-logs/index.js";
|
|
46
46
|
import { preFilterSession } from "../../integrations/session-logs/pre-filter.js";
|
|
47
|
-
import { callStructured } from "../../llm/structured-call.js";
|
|
47
|
+
import { callStructured, preflightStructuredLlmRunner } from "../../llm/structured-call.js";
|
|
48
48
|
import { sha256Hex } from "../../runtime.js";
|
|
49
49
|
import { getExtractedSessionsMap, getLastExtractRunAt, shouldSkipAlreadyExtractedSession, upsertExtractedSession, } from "../../storage/repositories/extract-sessions-repository.js";
|
|
50
|
+
import { openSqliteReadSnapshot } from "../../storage/sqlite-read-snapshot.js";
|
|
50
51
|
import { isProposalSkipped } from "../proposal/repository.js";
|
|
51
|
-
import {
|
|
52
|
+
import { resolveImproveLlmExecution } from "./execution.js";
|
|
53
|
+
import { buildExtractPrompt, EXTRACT_JSON_SCHEMA, parseExtractPayload, } from "./extract-prompt.js";
|
|
52
54
|
import { resolveImproveStrategy, resolveProcessEnabled } from "./improve-strategies.js";
|
|
53
55
|
import { emitProposal } from "./proposal-envelope.js";
|
|
54
56
|
import { createRunContext, resolveRunStashDir } from "./run-context.js";
|
|
@@ -99,21 +101,35 @@ function resolveDefaultSinceMs(harnessName, now, opts) {
|
|
|
99
101
|
const floor = now - DEFAULT_SINCE_FLOOR_MS;
|
|
100
102
|
if (opts.skipTracking)
|
|
101
103
|
return floor;
|
|
104
|
+
let snapshot;
|
|
102
105
|
try {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
let db = opts.stateDb;
|
|
107
|
+
if (!db) {
|
|
108
|
+
snapshot = openSqliteReadSnapshot(opts.stateDbPath ?? getStateDbPath());
|
|
109
|
+
db = snapshot;
|
|
110
|
+
}
|
|
111
|
+
if (!db)
|
|
112
|
+
return floor;
|
|
113
|
+
const lastRun = getLastExtractRunAt(db, harnessName);
|
|
114
|
+
return lastRun != null ? Math.min(lastRun, floor) : floor;
|
|
107
115
|
}
|
|
108
116
|
catch {
|
|
109
117
|
return floor;
|
|
110
118
|
}
|
|
119
|
+
finally {
|
|
120
|
+
snapshot?.close();
|
|
121
|
+
}
|
|
111
122
|
}
|
|
112
123
|
/** Filesystem-safe per-session lock path, co-located with the state.db. */
|
|
113
124
|
function getExtractSessionLockPath(harness, sessionId, stateDbPath) {
|
|
114
125
|
const safe = `${harness}-${sessionId}`.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
115
126
|
return path.join(path.dirname(stateDbPath), "extract-locks", `extract-${safe}.lock`);
|
|
116
127
|
}
|
|
128
|
+
function extractSessionLockIsUnavailable(harness, sessionId, stateDbPath) {
|
|
129
|
+
const lockPath = getExtractSessionLockPath(harness, sessionId, stateDbPath);
|
|
130
|
+
const probe = probeLock(lockPath, { staleAfterMs: EXTRACT_SESSION_LOCK_STALE_MS });
|
|
131
|
+
return probe.state === "held" || probe.state === "inaccessible";
|
|
132
|
+
}
|
|
117
133
|
/**
|
|
118
134
|
* Try to claim the per-session extract lock so a concurrent extract (e.g. a
|
|
119
135
|
* session-end hook firing `--session-id` while the hourly improve pass runs
|
|
@@ -172,27 +188,28 @@ export function resolveStandaloneExtractPlan(config, selection) {
|
|
|
172
188
|
...(selection.engine ? { engine: selection.engine } : {}),
|
|
173
189
|
...(Object.hasOwn(selection, "timeoutMs") ? { timeoutMs: selection.timeoutMs ?? null } : {}),
|
|
174
190
|
};
|
|
175
|
-
const resolved =
|
|
191
|
+
const resolved = resolveImproveLlmExecution({
|
|
192
|
+
config,
|
|
193
|
+
profile: selected.config,
|
|
194
|
+
process,
|
|
195
|
+
current: invocation,
|
|
196
|
+
processName: "extract",
|
|
197
|
+
});
|
|
176
198
|
if (!resolved) {
|
|
177
199
|
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
200
|
}
|
|
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
|
-
};
|
|
201
|
+
const runner = resolved.runner;
|
|
186
202
|
return Object.freeze({
|
|
187
203
|
strategy: selected.name,
|
|
188
|
-
engine:
|
|
204
|
+
engine: runner.engine,
|
|
189
205
|
// `akm extract` is an explicit operation. The strategy supplies behavior,
|
|
190
206
|
// but its improve-stage enablement gate does not disable this command.
|
|
191
207
|
enabled: true,
|
|
192
208
|
process,
|
|
193
209
|
runner: cloneAndFreeze(runner),
|
|
194
|
-
timeoutMs:
|
|
210
|
+
timeoutMs: Object.hasOwn(runner, "timeoutMs") ? (runner.timeoutMs ?? null) : 600_000,
|
|
195
211
|
embeddingConfig: cloneAndFreeze(config.embedding),
|
|
212
|
+
...(resolved.notices.length > 0 ? { notices: cloneAndFreeze(resolved.notices) } : {}),
|
|
196
213
|
});
|
|
197
214
|
}
|
|
198
215
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
@@ -233,15 +250,7 @@ export function parseSinceArg(value, now = Date.now()) {
|
|
|
233
250
|
*/
|
|
234
251
|
function resolveHarness(type, harnesses) {
|
|
235
252
|
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);
|
|
253
|
+
return pool.find((h) => h.name === type);
|
|
245
254
|
}
|
|
246
255
|
/**
|
|
247
256
|
* Build the ref + content for a candidate. The body must contain a
|
|
@@ -366,19 +375,7 @@ function runPreLlmSessionGates(args) {
|
|
|
366
375
|
// `--force` overrides it to re-extract a previously-extracted session.
|
|
367
376
|
const contentHash = hashSessionContent(data);
|
|
368
377
|
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
|
-
};
|
|
378
|
+
return { skip: alreadyExtractedResult(harness.name, sessionRef.sessionId, prior, contentHash) };
|
|
382
379
|
}
|
|
383
380
|
const filtered = preFilterSession(data, {
|
|
384
381
|
...(typeof maxTotalChars === "number" ? { maxTotalChars } : {}),
|
|
@@ -440,57 +437,180 @@ function runPreLlmSessionGates(args) {
|
|
|
440
437
|
}
|
|
441
438
|
return { data, filtered, contentHash };
|
|
442
439
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
440
|
+
function alreadyExtractedResult(harness, sessionId, prior, contentHash) {
|
|
441
|
+
return {
|
|
442
|
+
sessionId,
|
|
443
|
+
harness,
|
|
444
|
+
candidateCount: 0,
|
|
445
|
+
proposalIds: [],
|
|
446
|
+
preFilter: { inputCount: 0, outputCount: 0, truncatedCount: 0 },
|
|
447
|
+
warnings: [`already extracted (content unchanged) at ${prior?.processed_at}; pass --force to re-process`],
|
|
448
|
+
skipped: true,
|
|
449
|
+
skipReason: "already_extracted",
|
|
450
|
+
contentHash,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
function lockedConcurrentResult(harness, summary) {
|
|
454
|
+
return {
|
|
455
|
+
sessionId: summary.sessionId,
|
|
456
|
+
harness,
|
|
457
|
+
candidateCount: 0,
|
|
458
|
+
proposalIds: [],
|
|
459
|
+
preFilter: { inputCount: 0, outputCount: 0, truncatedCount: 0 },
|
|
460
|
+
warnings: ["concurrent extract holds this session's lock — skipped (handled by the other run)"],
|
|
461
|
+
skipped: true,
|
|
462
|
+
skipReason: "locked_concurrent",
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
function planExtractSessions(args) {
|
|
466
|
+
const { candidates, options, harness, seenMap, maxSessionsPerRun, trackingEnabled, dryRun } = args;
|
|
467
|
+
const plans = [];
|
|
468
|
+
let modelCount = 0;
|
|
469
|
+
for (let index = 0; index < candidates.length; index++) {
|
|
470
|
+
if (options.signal?.aborted)
|
|
471
|
+
return { plans, deferredCandidates: candidates.slice(index) };
|
|
472
|
+
if (!options.sessionId && !options.force && maxSessionsPerRun > 0 && modelCount >= maxSessionsPerRun) {
|
|
473
|
+
return { plans, deferredCandidates: candidates.slice(index) };
|
|
474
|
+
}
|
|
475
|
+
const summary = candidates[index];
|
|
476
|
+
if (!summary)
|
|
477
|
+
continue;
|
|
478
|
+
if (trackingEnabled && !dryRun && !options.stateDb) {
|
|
479
|
+
if (extractSessionLockIsUnavailable(harness.name, summary.sessionId, options.stateDbPath ?? getStateDbPath())) {
|
|
480
|
+
plans.push({ kind: "skip", summary, result: lockedConcurrentResult(harness.name, summary) });
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
const gate = runPreLlmSessionGates({
|
|
485
|
+
harness,
|
|
486
|
+
sessionRef: summary,
|
|
487
|
+
prior: seenMap.get(summary.sessionId),
|
|
488
|
+
force: options.force === true,
|
|
489
|
+
maxTotalChars: args.maxTotalChars,
|
|
490
|
+
minContentChars: args.minContentChars,
|
|
491
|
+
triage: args.triage,
|
|
492
|
+
});
|
|
493
|
+
if ("skip" in gate) {
|
|
494
|
+
plans.push({ kind: "skip", summary, result: gate.skip });
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
// Reading and classifying a session can take long enough for a concurrent
|
|
498
|
+
// session-end hook to claim its lock. Re-probe the fully classified model
|
|
499
|
+
// plan before it consumes a cap slot or forces credential materialization.
|
|
500
|
+
if (trackingEnabled &&
|
|
501
|
+
!dryRun &&
|
|
502
|
+
!options.stateDb &&
|
|
503
|
+
extractSessionLockIsUnavailable(harness.name, summary.sessionId, options.stateDbPath ?? getStateDbPath())) {
|
|
504
|
+
plans.push({ kind: "skip", summary, result: lockedConcurrentResult(harness.name, summary) });
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
plans.push({ kind: "model", summary, gate });
|
|
508
|
+
modelCount += 1;
|
|
509
|
+
}
|
|
510
|
+
return { plans, deferredCandidates: [] };
|
|
511
|
+
}
|
|
512
|
+
const EXTRACT_LLM_UNAVAILABLE = Symbol("extract-llm-unavailable");
|
|
452
513
|
async function runSessionExtractionLlmCall(args) {
|
|
453
|
-
const { config,
|
|
454
|
-
let extractLlm;
|
|
514
|
+
const { config, llmRunner, lease, chat, prompt, timeoutMs, signal, onNotices } = args;
|
|
455
515
|
try {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
516
|
+
const result = await runStructured({
|
|
517
|
+
dispatch: async (feedback) => {
|
|
518
|
+
const content = feedback ? `${prompt}\n\n## Corrective output instruction\n\n${feedback}` : prompt;
|
|
519
|
+
const dispatched = await callStructured({
|
|
520
|
+
feature: "session_extraction",
|
|
521
|
+
akmConfig: config,
|
|
522
|
+
runner: llmRunner,
|
|
523
|
+
lease,
|
|
524
|
+
messages: [{ role: "user", content }],
|
|
525
|
+
request: {
|
|
526
|
+
timeoutMs,
|
|
527
|
+
responseSchema: EXTRACT_JSON_SCHEMA,
|
|
528
|
+
...(signal ? { signal } : {}),
|
|
529
|
+
...(chat ? { chat } : {}),
|
|
530
|
+
},
|
|
531
|
+
onNotices,
|
|
532
|
+
parse: (raw) => ({ kind: "response", raw: raw ?? "" }),
|
|
533
|
+
onError: () => ({ kind: "unavailable" }),
|
|
534
|
+
fallback: { kind: "unavailable" },
|
|
535
|
+
});
|
|
536
|
+
if (dispatched.kind === "unavailable")
|
|
537
|
+
throw EXTRACT_LLM_UNAVAILABLE;
|
|
538
|
+
return dispatched.raw;
|
|
474
539
|
},
|
|
475
540
|
parse: (raw) => {
|
|
476
|
-
|
|
477
|
-
return
|
|
541
|
+
const payload = parseExtractPayload(raw);
|
|
542
|
+
return payload.parseFailure ? undefined : payload;
|
|
478
543
|
},
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
fallback: "",
|
|
544
|
+
validate: (payload) => ({ ok: true, value: payload }),
|
|
545
|
+
maxAttempts: llmRunner.connection.supportsJsonSchema === true ? 1 : 2,
|
|
546
|
+
buildFeedback: () => "Your previous response did not contain a valid extraction payload. Respond with ONLY a JSON object matching the requested schema, with a candidates array and no prose or code fences.",
|
|
483
547
|
});
|
|
484
|
-
|
|
548
|
+
if (result.ok)
|
|
549
|
+
return { kind: "success", payload: result.value, attempts: result.attempts };
|
|
550
|
+
const payload = parseExtractPayload(result.raw);
|
|
551
|
+
return {
|
|
552
|
+
kind: "malformed",
|
|
553
|
+
raw: result.raw,
|
|
554
|
+
attempts: result.attempts,
|
|
555
|
+
failure: payload.parseFailure ??
|
|
556
|
+
{ code: "invalid_payload", message: result.errors.join("; ") },
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
catch (err) {
|
|
560
|
+
if (err === EXTRACT_LLM_UNAVAILABLE)
|
|
561
|
+
return { kind: "unavailable" };
|
|
562
|
+
throw err;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function extractNoticeFields(getNotices) {
|
|
566
|
+
const notices = getNotices();
|
|
567
|
+
return notices.length > 0 ? { notices } : {};
|
|
568
|
+
}
|
|
569
|
+
function extractPreFilterStats(filtered) {
|
|
570
|
+
return {
|
|
571
|
+
inputCount: filtered.stats.inputCount,
|
|
572
|
+
outputCount: filtered.stats.outputCount,
|
|
573
|
+
truncatedCount: filtered.stats.truncatedCount,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
function malformedExtractionResult(args) {
|
|
577
|
+
const { extraction, sessionRef, harness, preFilter, contentHash, notices } = args;
|
|
578
|
+
const diagnostic = `malformed_model_output: ${extraction.failure.message}; attempts=${extraction.attempts}; responseLength=${extraction.raw.length}; responseSha256=${sha256Hex(extraction.raw)}`;
|
|
579
|
+
warnVerbose(`[extract] malformed model output for session ${sessionRef.sessionId}: ${redactErrorBody(extraction.raw)}`);
|
|
580
|
+
return {
|
|
581
|
+
sessionId: sessionRef.sessionId,
|
|
582
|
+
harness,
|
|
583
|
+
candidateCount: 0,
|
|
584
|
+
proposalIds: [],
|
|
585
|
+
preFilter,
|
|
586
|
+
warnings: [diagnostic],
|
|
587
|
+
skipped: true,
|
|
588
|
+
skipReason: "malformed_model_output",
|
|
589
|
+
contentHash,
|
|
590
|
+
...notices,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
function unavailableExtractionResult(args) {
|
|
594
|
+
return {
|
|
595
|
+
sessionId: args.sessionRef.sessionId,
|
|
596
|
+
harness: args.harness,
|
|
597
|
+
candidateCount: 0,
|
|
598
|
+
proposalIds: [],
|
|
599
|
+
preFilter: args.preFilter,
|
|
600
|
+
warnings: ["session_extraction feature returned empty (disabled / timeout / error)"],
|
|
601
|
+
skipped: true,
|
|
602
|
+
skipReason: "llm_unavailable",
|
|
603
|
+
contentHash: args.contentHash,
|
|
604
|
+
...args.notices,
|
|
605
|
+
};
|
|
485
606
|
}
|
|
486
607
|
async function processSession(runCtx, session) {
|
|
487
|
-
const { harness, stashDir, config,
|
|
488
|
-
const { sessionRef,
|
|
608
|
+
const { harness, stashDir, config, llmRunner, lease, onNotices, getNotices, chat, ctx, eventsCtx, sourceRun, dryRun, timeoutMs, sessionIndexing, signal, standardsContext, } = runCtx;
|
|
609
|
+
const { sessionRef, gate } = session;
|
|
489
610
|
const warnings = [];
|
|
490
|
-
const gate = runPreLlmSessionGates({ harness, sessionRef, prior, force, maxTotalChars, minContentChars, triage });
|
|
491
|
-
if ("skip" in gate)
|
|
492
|
-
return gate.skip;
|
|
493
611
|
const { data, filtered, contentHash } = gate;
|
|
612
|
+
if (!lease)
|
|
613
|
+
throw new TypeError("extract model work requires an operation dispatch lease");
|
|
494
614
|
const prompt = buildExtractPrompt({
|
|
495
615
|
data,
|
|
496
616
|
events: filtered.events,
|
|
@@ -508,7 +628,7 @@ async function processSession(runCtx, session) {
|
|
|
508
628
|
if (!sessionMeetsDurationGate(data, sessionIndexing.minDurationMinutes))
|
|
509
629
|
return {};
|
|
510
630
|
try {
|
|
511
|
-
const result = await writeSessionAsset(data, stashDir, sessionIndexing.generate);
|
|
631
|
+
const result = await writeSessionAsset(data, stashDir, (summaryData) => sessionIndexing.generate(summaryData, lease));
|
|
512
632
|
if (result.written) {
|
|
513
633
|
// Write-path indexing (itself fail-open): standalone `akm extract`
|
|
514
634
|
// (session-end hook) has no post-loop reindex to pick this file up.
|
|
@@ -521,37 +641,43 @@ async function processSession(runCtx, session) {
|
|
|
521
641
|
}
|
|
522
642
|
}
|
|
523
643
|
catch (err) {
|
|
644
|
+
if (err instanceof ConfigError)
|
|
645
|
+
throw err;
|
|
524
646
|
warnings.push(`session asset write failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
525
647
|
}
|
|
526
648
|
return {};
|
|
527
649
|
};
|
|
528
|
-
const
|
|
650
|
+
const extraction = await runSessionExtractionLlmCall({
|
|
529
651
|
config,
|
|
530
|
-
|
|
652
|
+
llmRunner,
|
|
653
|
+
lease,
|
|
531
654
|
chat,
|
|
532
655
|
prompt,
|
|
533
656
|
timeoutMs,
|
|
534
657
|
signal,
|
|
658
|
+
onNotices,
|
|
535
659
|
});
|
|
536
|
-
if (
|
|
660
|
+
if (extraction.kind === "unavailable") {
|
|
537
661
|
// The seam took the fallback path (disabled / timeout / error). Return skipped.
|
|
538
|
-
return {
|
|
539
|
-
|
|
662
|
+
return unavailableExtractionResult({
|
|
663
|
+
sessionRef,
|
|
540
664
|
harness: harness.name,
|
|
541
|
-
|
|
542
|
-
proposalIds: [],
|
|
543
|
-
preFilter: {
|
|
544
|
-
inputCount: filtered.stats.inputCount,
|
|
545
|
-
outputCount: filtered.stats.outputCount,
|
|
546
|
-
truncatedCount: filtered.stats.truncatedCount,
|
|
547
|
-
},
|
|
548
|
-
warnings: ["session_extraction feature returned empty (disabled / timeout / error)"],
|
|
549
|
-
skipped: true,
|
|
550
|
-
skipReason: "llm_unavailable",
|
|
665
|
+
preFilter: extractPreFilterStats(filtered),
|
|
551
666
|
contentHash,
|
|
552
|
-
|
|
667
|
+
notices: extractNoticeFields(getNotices),
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
if (extraction.kind === "malformed") {
|
|
671
|
+
return malformedExtractionResult({
|
|
672
|
+
extraction,
|
|
673
|
+
sessionRef,
|
|
674
|
+
harness: harness.name,
|
|
675
|
+
preFilter: extractPreFilterStats(filtered),
|
|
676
|
+
contentHash,
|
|
677
|
+
notices: extractNoticeFields(getNotices),
|
|
678
|
+
});
|
|
553
679
|
}
|
|
554
|
-
const payload =
|
|
680
|
+
const { payload } = extraction;
|
|
555
681
|
const proposalIds = [];
|
|
556
682
|
// Provenance refs are added only after the cited session asset exists.
|
|
557
683
|
const sessionAsset = await maybeWriteSessionAsset();
|
|
@@ -565,6 +691,7 @@ async function processSession(runCtx, session) {
|
|
|
565
691
|
harness: harness.name,
|
|
566
692
|
sourceRun,
|
|
567
693
|
rationale: payload.rationale_if_empty,
|
|
694
|
+
repairAttempts: extraction.attempts - 1,
|
|
568
695
|
preFilterInput: filtered.stats.inputCount,
|
|
569
696
|
preFilterOutput: filtered.stats.outputCount,
|
|
570
697
|
},
|
|
@@ -583,17 +710,12 @@ async function processSession(runCtx, session) {
|
|
|
583
710
|
warnings,
|
|
584
711
|
contentHash,
|
|
585
712
|
...sessionAsset,
|
|
713
|
+
...extractNoticeFields(getNotices),
|
|
586
714
|
};
|
|
587
715
|
}
|
|
588
716
|
// §23.6 fingerprint model-id term: the profile resolved for this session's
|
|
589
717
|
// 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
|
-
}
|
|
718
|
+
const extractModelId = llmRunner.connection.model;
|
|
597
719
|
for (const candidate of payload.candidates) {
|
|
598
720
|
const built = buildCandidateProposal(candidate, data.ref, sessionAsset.sessionAssetRef);
|
|
599
721
|
if (dryRun) {
|
|
@@ -643,6 +765,7 @@ async function processSession(runCtx, session) {
|
|
|
643
765
|
proposalCount: proposalIds.length,
|
|
644
766
|
preFilterInput: filtered.stats.inputCount,
|
|
645
767
|
preFilterOutput: filtered.stats.outputCount,
|
|
768
|
+
repairAttempts: extraction.attempts - 1,
|
|
646
769
|
},
|
|
647
770
|
}, eventsCtx);
|
|
648
771
|
return {
|
|
@@ -658,8 +781,77 @@ async function processSession(runCtx, session) {
|
|
|
658
781
|
warnings,
|
|
659
782
|
contentHash,
|
|
660
783
|
...sessionAsset,
|
|
784
|
+
...extractNoticeFields(getNotices),
|
|
661
785
|
};
|
|
662
786
|
}
|
|
787
|
+
function recordExtractSessionOutcome(args) {
|
|
788
|
+
const { stateDb, trackingEnabled, dryRun, harness, summary, result, sourceRun } = args;
|
|
789
|
+
if (!trackingEnabled ||
|
|
790
|
+
!stateDb ||
|
|
791
|
+
dryRun ||
|
|
792
|
+
result.skipReason === "already_extracted" ||
|
|
793
|
+
result.skipReason === "locked_concurrent")
|
|
794
|
+
return;
|
|
795
|
+
try {
|
|
796
|
+
const outcome = result.skipped
|
|
797
|
+
? result.skipReason === "read_failed" ||
|
|
798
|
+
result.skipReason === "exception" ||
|
|
799
|
+
result.skipReason === "malformed_model_output"
|
|
800
|
+
? "failed"
|
|
801
|
+
: "skipped"
|
|
802
|
+
: result.candidateCount === 0
|
|
803
|
+
? "no_candidates"
|
|
804
|
+
: "candidates_queued";
|
|
805
|
+
upsertExtractedSession(stateDb, {
|
|
806
|
+
harness,
|
|
807
|
+
sessionId: summary.sessionId,
|
|
808
|
+
processedAt: new Date().toISOString(),
|
|
809
|
+
sessionEndedAt: summary.endedAt ?? null,
|
|
810
|
+
outcome,
|
|
811
|
+
candidateCount: result.candidateCount,
|
|
812
|
+
proposalCount: result.proposalIds.length,
|
|
813
|
+
rationale: result.rationaleIfEmpty ?? null,
|
|
814
|
+
sourceRun,
|
|
815
|
+
contentHash: result.skipReason === "llm_unavailable" ||
|
|
816
|
+
result.skipReason === "triaged_out" ||
|
|
817
|
+
result.skipReason === "malformed_model_output"
|
|
818
|
+
? null
|
|
819
|
+
: (result.contentHash ?? null),
|
|
820
|
+
metadata: {
|
|
821
|
+
preFilterInputCount: result.preFilter.inputCount,
|
|
822
|
+
preFilterOutputCount: result.preFilter.outputCount,
|
|
823
|
+
preFilterTruncatedCount: result.preFilter.truncatedCount,
|
|
824
|
+
...(result.skipReason ? { skipReason: result.skipReason } : {}),
|
|
825
|
+
...(result.sessionLogPath ? { logPath: result.sessionLogPath } : {}),
|
|
826
|
+
...(result.sessionAssetRef ? { sessionAssetRef: result.sessionAssetRef } : {}),
|
|
827
|
+
},
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
catch (err) {
|
|
831
|
+
warn(`[extract] failed to record session ${summary.sessionId} in state.db: ${err instanceof Error ? err.message : String(err)}`);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
function accountExtractSessionResult(result, triageEnabled, output) {
|
|
835
|
+
output.sessions.push(result);
|
|
836
|
+
if (triageEnabled) {
|
|
837
|
+
const preempted = result.skipReason === "read_failed" ||
|
|
838
|
+
result.skipReason === "too_short" ||
|
|
839
|
+
result.skipReason === "already_extracted" ||
|
|
840
|
+
result.skipReason === "locked_concurrent";
|
|
841
|
+
if (!preempted) {
|
|
842
|
+
output.triageEvaluated += 1;
|
|
843
|
+
if (result.skipReason === "triaged_out")
|
|
844
|
+
output.triagedOut += 1;
|
|
845
|
+
else
|
|
846
|
+
output.triagePassed += 1;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
if (result.skipped)
|
|
850
|
+
output.skippedCount += 1;
|
|
851
|
+
else
|
|
852
|
+
output.processedCount += 1;
|
|
853
|
+
output.allProposalIds.push(...result.proposalIds);
|
|
854
|
+
}
|
|
663
855
|
/**
|
|
664
856
|
* Iterate the discovered candidate sessions: enforce the per-run cap, take the
|
|
665
857
|
* per-session cross-process lock, dispatch to {@link processSession}, aggregate
|
|
@@ -668,164 +860,141 @@ async function processSession(runCtx, session) {
|
|
|
668
860
|
* aggregation, and seen-row upsert are byte-identical.
|
|
669
861
|
*/
|
|
670
862
|
async function runExtractSessionLoop(args) {
|
|
671
|
-
const {
|
|
863
|
+
const { plans, deferredCandidates, seenMap, options, harness, stateDb, trackingEnabled, dryRun, stashDir, config, llmRunner, lease, onNotices, getNotices, chat, sourceRun, timeoutMs, triage, sessionIndexing, extractStandardsContext, topLevelWarnings, } = args;
|
|
672
864
|
// WI-7.7 §2: run-scoped processSession inputs, resolved once per run.
|
|
673
865
|
const sessionRunCtx = {
|
|
674
866
|
harness,
|
|
675
867
|
stashDir,
|
|
676
868
|
config,
|
|
677
|
-
|
|
869
|
+
llmRunner,
|
|
870
|
+
lease,
|
|
871
|
+
onNotices,
|
|
872
|
+
getNotices,
|
|
678
873
|
chat,
|
|
679
874
|
ctx: options.ctx,
|
|
680
875
|
eventsCtx: options.eventsCtx,
|
|
681
876
|
sourceRun,
|
|
682
877
|
dryRun,
|
|
683
878
|
timeoutMs,
|
|
684
|
-
maxTotalChars,
|
|
685
|
-
minContentChars,
|
|
686
|
-
triage,
|
|
687
879
|
sessionIndexing,
|
|
688
880
|
signal: options.signal,
|
|
689
881
|
standardsContext: extractStandardsContext,
|
|
690
882
|
};
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
883
|
+
const output = {
|
|
884
|
+
sessions: [],
|
|
885
|
+
processedCount: 0,
|
|
886
|
+
skippedCount: 0,
|
|
887
|
+
triageEvaluated: 0,
|
|
888
|
+
triagePassed: 0,
|
|
889
|
+
triagedOut: 0,
|
|
890
|
+
allProposalIds: [],
|
|
891
|
+
deferred: 0,
|
|
892
|
+
};
|
|
893
|
+
const workPlans = [...plans];
|
|
894
|
+
let remainingCandidates = deferredCandidates;
|
|
895
|
+
const refillModelSlot = () => {
|
|
896
|
+
if (remainingCandidates.length === 0 || options.signal?.aborted)
|
|
897
|
+
return;
|
|
898
|
+
const refill = planExtractSessions({
|
|
899
|
+
candidates: remainingCandidates,
|
|
900
|
+
options,
|
|
901
|
+
harness,
|
|
902
|
+
seenMap,
|
|
903
|
+
maxTotalChars: args.maxTotalChars,
|
|
904
|
+
minContentChars: args.minContentChars,
|
|
905
|
+
maxSessionsPerRun: 1,
|
|
906
|
+
triage,
|
|
907
|
+
trackingEnabled,
|
|
908
|
+
dryRun,
|
|
909
|
+
});
|
|
910
|
+
workPlans.push(...refill.plans);
|
|
911
|
+
remainingCandidates = refill.deferredCandidates;
|
|
912
|
+
};
|
|
913
|
+
for (const plan of workPlans) {
|
|
700
914
|
if (options.signal?.aborted)
|
|
701
915
|
break;
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
916
|
+
const { summary } = plan;
|
|
917
|
+
if (plan.kind === "skip") {
|
|
918
|
+
accountExtractSessionResult(plan.result, triage.enabled, output);
|
|
919
|
+
recordExtractSessionOutcome({
|
|
920
|
+
stateDb,
|
|
921
|
+
trackingEnabled,
|
|
922
|
+
dryRun,
|
|
923
|
+
harness: harness.name,
|
|
924
|
+
summary,
|
|
925
|
+
result: plan.result,
|
|
926
|
+
sourceRun,
|
|
927
|
+
});
|
|
928
|
+
continue;
|
|
713
929
|
}
|
|
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
930
|
let sessionLockOwnership;
|
|
722
931
|
if (trackingEnabled && !dryRun && !options.stateDb) {
|
|
723
932
|
const sessionLockPath = getExtractSessionLockPath(harness.name, summary.sessionId, options.stateDbPath ?? getStateDbPath());
|
|
724
933
|
const sessionLock = acquireExtractSessionLock(sessionLockPath);
|
|
725
934
|
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;
|
|
935
|
+
accountExtractSessionResult(lockedConcurrentResult(harness.name, summary), triage.enabled, output);
|
|
936
|
+
refillModelSlot();
|
|
737
937
|
continue;
|
|
738
938
|
}
|
|
739
939
|
sessionLockOwnership = sessionLock.ownership;
|
|
740
940
|
}
|
|
741
941
|
try {
|
|
742
|
-
|
|
942
|
+
// Planning stays read-only so a credential failure creates no state. Once
|
|
943
|
+
// this run owns the session lock, read and gate the session again: the log
|
|
944
|
+
// may have grown, become too short after replacement, or been completed by
|
|
945
|
+
// another extractor between the planning snapshot and acquisition.
|
|
946
|
+
const currentPrior = stateDb
|
|
947
|
+
? getExtractedSessionsMap(stateDb, harness.name, [summary.sessionId]).get(summary.sessionId)
|
|
948
|
+
: seenMap.get(summary.sessionId);
|
|
949
|
+
const executionGate = runPreLlmSessionGates({
|
|
950
|
+
harness,
|
|
743
951
|
sessionRef: summary,
|
|
744
|
-
prior,
|
|
952
|
+
prior: currentPrior,
|
|
745
953
|
force: options.force === true,
|
|
954
|
+
maxTotalChars: args.maxTotalChars,
|
|
955
|
+
minContentChars: args.minContentChars,
|
|
956
|
+
triage,
|
|
746
957
|
});
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
result.
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
triagedOut += 1;
|
|
761
|
-
else
|
|
762
|
-
triagePassed += 1;
|
|
763
|
-
}
|
|
958
|
+
if ("skip" in executionGate) {
|
|
959
|
+
accountExtractSessionResult(executionGate.skip, triage.enabled, output);
|
|
960
|
+
recordExtractSessionOutcome({
|
|
961
|
+
stateDb,
|
|
962
|
+
trackingEnabled,
|
|
963
|
+
dryRun,
|
|
964
|
+
harness: harness.name,
|
|
965
|
+
summary,
|
|
966
|
+
result: executionGate.skip,
|
|
967
|
+
sourceRun,
|
|
968
|
+
});
|
|
969
|
+
refillModelSlot();
|
|
970
|
+
continue;
|
|
764
971
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
-
}
|
|
972
|
+
const result = await processSession(sessionRunCtx, {
|
|
973
|
+
sessionRef: summary,
|
|
974
|
+
gate: executionGate,
|
|
975
|
+
});
|
|
976
|
+
if (result.skipReason === "malformed_model_output") {
|
|
977
|
+
for (const warning of result.warnings)
|
|
978
|
+
topLevelWarnings.push(`session ${summary.sessionId}: ${warning}`);
|
|
822
979
|
}
|
|
980
|
+
accountExtractSessionResult(result, triage.enabled, output);
|
|
981
|
+
recordExtractSessionOutcome({
|
|
982
|
+
stateDb,
|
|
983
|
+
trackingEnabled,
|
|
984
|
+
dryRun,
|
|
985
|
+
harness: harness.name,
|
|
986
|
+
summary,
|
|
987
|
+
result,
|
|
988
|
+
sourceRun,
|
|
989
|
+
});
|
|
823
990
|
}
|
|
824
991
|
catch (err) {
|
|
992
|
+
if (err instanceof ConfigError)
|
|
993
|
+
throw err;
|
|
825
994
|
const msg = err instanceof Error ? err.message : String(err);
|
|
826
995
|
warn(`[extract] session ${summary.sessionId} threw: ${msg}`);
|
|
827
996
|
topLevelWarnings.push(`session ${summary.sessionId} threw: ${msg}`);
|
|
828
|
-
|
|
997
|
+
accountExtractSessionResult({
|
|
829
998
|
sessionId: summary.sessionId,
|
|
830
999
|
harness: harness.name,
|
|
831
1000
|
candidateCount: 0,
|
|
@@ -834,15 +1003,16 @@ async function runExtractSessionLoop(args) {
|
|
|
834
1003
|
warnings: [msg],
|
|
835
1004
|
skipped: true,
|
|
836
1005
|
skipReason: "exception",
|
|
837
|
-
|
|
838
|
-
|
|
1006
|
+
...extractNoticeFields(getNotices),
|
|
1007
|
+
}, triage.enabled, output);
|
|
839
1008
|
}
|
|
840
1009
|
finally {
|
|
841
1010
|
if (sessionLockOwnership)
|
|
842
1011
|
releaseLock(sessionLockOwnership);
|
|
843
1012
|
}
|
|
844
1013
|
}
|
|
845
|
-
|
|
1014
|
+
output.deferred = remainingCandidates.length;
|
|
1015
|
+
return output;
|
|
846
1016
|
}
|
|
847
1017
|
/**
|
|
848
1018
|
* Resolve the run-scoped LLM/engine, budget, triage, and session-indexing
|
|
@@ -851,25 +1021,43 @@ async function runExtractSessionLoop(args) {
|
|
|
851
1021
|
* session-summary generator seam, and the default resolutions are byte-identical.
|
|
852
1022
|
*/
|
|
853
1023
|
function resolveExtractRunConfig(options, config, extractProcess, activeProfile) {
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
const
|
|
860
|
-
|
|
1024
|
+
const executionNotices = new Map();
|
|
1025
|
+
const onNotices = (notices) => {
|
|
1026
|
+
for (const notice of notices)
|
|
1027
|
+
executionNotices.set(JSON.stringify(notice), notice);
|
|
1028
|
+
};
|
|
1029
|
+
const getNotices = () => Object.freeze([...executionNotices.values()]);
|
|
1030
|
+
// Improve supplies its invocation-owned symbolic runner. Standalone extract
|
|
1031
|
+
// resolves the selected process engine through the shared execution planner.
|
|
1032
|
+
let llmRunner;
|
|
1033
|
+
if (options.resolvedPlan) {
|
|
1034
|
+
llmRunner = options.resolvedPlan.runner;
|
|
1035
|
+
onNotices(options.resolvedPlan.notices ?? []);
|
|
1036
|
+
}
|
|
1037
|
+
else if (options.llmRunner) {
|
|
1038
|
+
llmRunner = options.llmRunner;
|
|
1039
|
+
}
|
|
1040
|
+
else {
|
|
1041
|
+
const resolved = resolveImproveLlmExecution({
|
|
1042
|
+
config,
|
|
1043
|
+
profile: activeProfile,
|
|
1044
|
+
process: extractProcess,
|
|
1045
|
+
processName: "extract",
|
|
1046
|
+
});
|
|
1047
|
+
llmRunner = resolved?.runner;
|
|
1048
|
+
if (resolved)
|
|
1049
|
+
onNotices(resolved.notices);
|
|
1050
|
+
}
|
|
1051
|
+
if (!llmRunner) {
|
|
861
1052
|
throw new ConfigError("No LLM engine configured for extract. Set defaults.llmEngine or improve.strategies.<name>.processes.extract.engine.", "LLM_NOT_CONFIGURED");
|
|
862
1053
|
}
|
|
863
1054
|
const timeoutMs = options.resolvedPlan
|
|
864
1055
|
? options.resolvedPlan.timeoutMs
|
|
865
1056
|
: Object.hasOwn(options, "timeoutMs")
|
|
866
1057
|
? (options.timeoutMs ?? null)
|
|
867
|
-
:
|
|
868
|
-
?
|
|
869
|
-
:
|
|
870
|
-
? (fixedLlmConfig.timeoutMs ?? null)
|
|
871
|
-
: 600_000;
|
|
872
|
-
const getLlmConfig = () => runnerSpec ? materializeLlmRunnerConnection(runnerSpec) : fixedLlmConfig;
|
|
1058
|
+
: Object.hasOwn(llmRunner, "timeoutMs")
|
|
1059
|
+
? (llmRunner.timeoutMs ?? null)
|
|
1060
|
+
: 600_000;
|
|
873
1061
|
// Pre-filter budget — process config can raise it for large-context models.
|
|
874
1062
|
const maxTotalChars = typeof extractProcess?.maxTotalChars === "number" ? extractProcess.maxTotalChars : undefined;
|
|
875
1063
|
// #595/#596 — minimum raw session size; sessions below it skip the LLM call
|
|
@@ -899,36 +1087,28 @@ function resolveExtractRunConfig(options, config, extractProcess, activeProfile)
|
|
|
899
1087
|
// same fail-open `callStructured` seam as the rest of extract. Returns
|
|
900
1088
|
// `undefined` on disablement / timeout / error so no asset is written.
|
|
901
1089
|
// 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
|
-
}
|
|
1090
|
+
const defaultSessionSummaryGenerator = async (data, lease) => {
|
|
912
1091
|
let raw = "";
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
},
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1092
|
+
await callStructured({
|
|
1093
|
+
feature: "session_extraction",
|
|
1094
|
+
akmConfig: config,
|
|
1095
|
+
runner: llmRunner,
|
|
1096
|
+
...(lease ? { lease } : {}),
|
|
1097
|
+
messages: [{ role: "user", content: buildSessionSummaryPrompt(data) }],
|
|
1098
|
+
request: {
|
|
1099
|
+
timeoutMs,
|
|
1100
|
+
responseSchema: SESSION_SUMMARY_JSON_SCHEMA,
|
|
1101
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
1102
|
+
...(options.chat ? { chat: options.chat } : {}),
|
|
1103
|
+
},
|
|
1104
|
+
onNotices,
|
|
1105
|
+
parse: (r) => {
|
|
1106
|
+
raw = r ?? "";
|
|
1107
|
+
return raw;
|
|
1108
|
+
},
|
|
1109
|
+
onError: () => "",
|
|
1110
|
+
fallback: "",
|
|
1111
|
+
});
|
|
932
1112
|
return parseSessionSummary(raw);
|
|
933
1113
|
};
|
|
934
1114
|
const sessionIndexing = {
|
|
@@ -938,7 +1118,9 @@ function resolveExtractRunConfig(options, config, extractProcess, activeProfile)
|
|
|
938
1118
|
};
|
|
939
1119
|
return {
|
|
940
1120
|
timeoutMs,
|
|
941
|
-
|
|
1121
|
+
llmRunner,
|
|
1122
|
+
onNotices,
|
|
1123
|
+
getNotices,
|
|
942
1124
|
maxTotalChars,
|
|
943
1125
|
minContentChars,
|
|
944
1126
|
maxSessionsPerRun,
|
|
@@ -999,30 +1181,11 @@ function discoverExtractCandidates(options, harness, effectiveSince, startMs, dr
|
|
|
999
1181
|
/**
|
|
1000
1182
|
* WI-9.10: build one `akm extract` run's {@link RunContext} from values
|
|
1001
1183
|
* `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.
|
|
1184
|
+
* stashDir, dryRun, sourceRun, and `resolveExtractRunConfig`'s symbolic runner)
|
|
1185
|
+
* — no second config load, credential materialization, or new db handle.
|
|
1015
1186
|
*/
|
|
1016
1187
|
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
|
-
};
|
|
1188
|
+
const { options, config, stashDir, dryRun, sourceRun, llmRunner } = args;
|
|
1026
1189
|
return createRunContext({
|
|
1027
1190
|
stashDir,
|
|
1028
1191
|
config,
|
|
@@ -1030,16 +1193,71 @@ function buildExtractRunContext(args) {
|
|
|
1030
1193
|
// Not yet wired into any proposal call site this stage (mirrors
|
|
1031
1194
|
// buildImproveRunContext's proposalsCtx comment in improve.ts).
|
|
1032
1195
|
proposalsCtx: options.ctx ?? {},
|
|
1033
|
-
|
|
1196
|
+
getLlmRunner: () => llmRunner,
|
|
1034
1197
|
sourceRun,
|
|
1035
1198
|
dryRun,
|
|
1036
1199
|
signal: options.signal,
|
|
1037
1200
|
});
|
|
1038
1201
|
}
|
|
1202
|
+
function loadExtractSeenMapReadOnly(args) {
|
|
1203
|
+
const { options, harness, candidates, trackingEnabled, warnings } = args;
|
|
1204
|
+
if (!trackingEnabled || candidates.length === 0)
|
|
1205
|
+
return new Map();
|
|
1206
|
+
let snapshot;
|
|
1207
|
+
try {
|
|
1208
|
+
if (!options.stateDb)
|
|
1209
|
+
snapshot = openSqliteReadSnapshot(options.stateDbPath ?? getStateDbPath());
|
|
1210
|
+
const db = options.stateDb ?? snapshot;
|
|
1211
|
+
return db
|
|
1212
|
+
? getExtractedSessionsMap(db, harness, candidates.map((candidate) => candidate.sessionId))
|
|
1213
|
+
: new Map();
|
|
1214
|
+
}
|
|
1215
|
+
catch (err) {
|
|
1216
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1217
|
+
warn(`[extract] state.db snapshot unavailable, planning without skip-tracking: ${msg}`);
|
|
1218
|
+
warnings.push(`state.db snapshot unavailable: ${msg}`);
|
|
1219
|
+
return new Map();
|
|
1220
|
+
}
|
|
1221
|
+
finally {
|
|
1222
|
+
snapshot?.close();
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
function openExtractLiveStateDb(args) {
|
|
1226
|
+
const { options, trackingEnabled, hasModelWork, dryRun, warnings } = args;
|
|
1227
|
+
if (!trackingEnabled)
|
|
1228
|
+
return undefined;
|
|
1229
|
+
if (options.stateDb)
|
|
1230
|
+
return options.stateDb;
|
|
1231
|
+
if (!hasModelWork || dryRun)
|
|
1232
|
+
return undefined;
|
|
1233
|
+
try {
|
|
1234
|
+
return openStateDatabase(options.stateDbPath);
|
|
1235
|
+
}
|
|
1236
|
+
catch (err) {
|
|
1237
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1238
|
+
warn(`[extract] state.db unavailable, processing without skip-tracking: ${msg}`);
|
|
1239
|
+
warnings.push(`state.db unavailable: ${msg}`);
|
|
1240
|
+
return undefined;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
function emitExtractTriageEvent(args) {
|
|
1244
|
+
const { modelPlanCount, triageEnabled, result, sourceRun, eventsCtx } = args;
|
|
1245
|
+
if (modelPlanCount === 0 || !triageEnabled || result.triageEvaluated === 0)
|
|
1246
|
+
return;
|
|
1247
|
+
appendEvent({
|
|
1248
|
+
eventType: "extract_triaged",
|
|
1249
|
+
metadata: {
|
|
1250
|
+
evaluated: result.triageEvaluated,
|
|
1251
|
+
passed: result.triagePassed,
|
|
1252
|
+
triagedOut: result.triagedOut,
|
|
1253
|
+
sourceRun,
|
|
1254
|
+
},
|
|
1255
|
+
}, eventsCtx);
|
|
1256
|
+
}
|
|
1039
1257
|
export async function akmExtract(options) {
|
|
1040
1258
|
const startMs = Date.now();
|
|
1041
1259
|
if (!options.type || options.type.trim() === "") {
|
|
1042
|
-
throw new UsageError("--type is required. Pass a harness name (e.g. --type claude
|
|
1260
|
+
throw new UsageError("--type is required. Pass a harness name (e.g. --type claude).", "MISSING_REQUIRED_ARGUMENT");
|
|
1043
1261
|
}
|
|
1044
1262
|
const config = options.config ?? loadConfig();
|
|
1045
1263
|
const stashDir = resolveRunStashDir(options.stashDir);
|
|
@@ -1075,10 +1293,10 @@ export async function akmExtract(options) {
|
|
|
1075
1293
|
durationMs: Date.now() - startMs,
|
|
1076
1294
|
};
|
|
1077
1295
|
}
|
|
1078
|
-
const { timeoutMs,
|
|
1296
|
+
const { timeoutMs, llmRunner, onNotices, getNotices, maxTotalChars, minContentChars, maxSessionsPerRun, effectiveSince, triage, sessionIndexing, } = resolveExtractRunConfig(options, config, extractProcess, activeProfile);
|
|
1079
1297
|
// WI-9.10: construct this run's RunContext (extracted to
|
|
1080
1298
|
// buildExtractRunContext to keep akmExtract under the fn-size bar — R31).
|
|
1081
|
-
const ctx = buildExtractRunContext({ options, config, stashDir, dryRun, sourceRun,
|
|
1299
|
+
const ctx = buildExtractRunContext({ options, config, stashDir, dryRun, sourceRun, llmRunner });
|
|
1082
1300
|
const harness = resolveHarness(options.type, options.harnesses);
|
|
1083
1301
|
if (!harness) {
|
|
1084
1302
|
return {
|
|
@@ -1118,76 +1336,94 @@ export async function akmExtract(options) {
|
|
|
1118
1336
|
return discovery.notFound;
|
|
1119
1337
|
const candidates = discovery.candidates;
|
|
1120
1338
|
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
1339
|
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({
|
|
1340
|
+
const seenMap = loadExtractSeenMapReadOnly({
|
|
1341
|
+
options,
|
|
1342
|
+
harness: harness.name,
|
|
1343
|
+
candidates,
|
|
1344
|
+
trackingEnabled,
|
|
1345
|
+
warnings: topLevelWarnings,
|
|
1346
|
+
});
|
|
1347
|
+
const planned = planExtractSessions({
|
|
1146
1348
|
candidates,
|
|
1147
1349
|
options,
|
|
1148
1350
|
harness,
|
|
1149
1351
|
seenMap,
|
|
1150
|
-
stateDb,
|
|
1151
|
-
trackingEnabled,
|
|
1152
|
-
dryRun,
|
|
1153
|
-
stashDir,
|
|
1154
|
-
config,
|
|
1155
|
-
getLlmConfig,
|
|
1156
|
-
chat: options.chat,
|
|
1157
|
-
sourceRun,
|
|
1158
|
-
timeoutMs,
|
|
1159
1352
|
maxTotalChars,
|
|
1160
1353
|
minContentChars,
|
|
1161
1354
|
maxSessionsPerRun,
|
|
1162
1355
|
triage,
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
topLevelWarnings,
|
|
1356
|
+
trackingEnabled,
|
|
1357
|
+
dryRun,
|
|
1166
1358
|
});
|
|
1167
|
-
|
|
1168
|
-
//
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1359
|
+
const modelPlanCount = planned.plans.filter((plan) => plan.kind === "model").length;
|
|
1360
|
+
// Eligible dry-runs still dispatch to produce their candidate preview. Only
|
|
1361
|
+
// deterministic no-work plans are credential-free. Materialize once after
|
|
1362
|
+
// every read-only gate and before opening live state or acquiring a lock.
|
|
1363
|
+
const dispatchLease = modelPlanCount > 0 ? await preflightStructuredLlmRunner(llmRunner) : undefined;
|
|
1364
|
+
let stateDb;
|
|
1365
|
+
let loopResult;
|
|
1366
|
+
try {
|
|
1367
|
+
stateDb = openExtractLiveStateDb({
|
|
1368
|
+
options,
|
|
1369
|
+
trackingEnabled,
|
|
1370
|
+
hasModelWork: modelPlanCount > 0,
|
|
1371
|
+
dryRun,
|
|
1372
|
+
warnings: topLevelWarnings,
|
|
1373
|
+
});
|
|
1374
|
+
// Stash authoring standards (convention/meta fact bodies) for non-wiki
|
|
1375
|
+
// extract output. Resolved ONCE per run and threaded into each session's
|
|
1376
|
+
// prompt so facts are not re-read per session.
|
|
1377
|
+
const extractStandardsContext = modelPlanCount > 0 ? resolveExtractStandards(stashDir) : "";
|
|
1378
|
+
loopResult = await runExtractSessionLoop({
|
|
1379
|
+
plans: planned.plans,
|
|
1380
|
+
deferredCandidates: planned.deferredCandidates,
|
|
1381
|
+
seenMap,
|
|
1382
|
+
options,
|
|
1383
|
+
harness,
|
|
1384
|
+
stateDb,
|
|
1385
|
+
trackingEnabled,
|
|
1386
|
+
dryRun,
|
|
1387
|
+
stashDir,
|
|
1388
|
+
config,
|
|
1389
|
+
llmRunner,
|
|
1390
|
+
lease: dispatchLease,
|
|
1391
|
+
onNotices,
|
|
1392
|
+
getNotices,
|
|
1393
|
+
chat: options.chat,
|
|
1394
|
+
sourceRun,
|
|
1395
|
+
timeoutMs,
|
|
1396
|
+
maxTotalChars,
|
|
1397
|
+
minContentChars,
|
|
1398
|
+
triage,
|
|
1399
|
+
sessionIndexing,
|
|
1400
|
+
extractStandardsContext,
|
|
1401
|
+
topLevelWarnings,
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
finally {
|
|
1405
|
+
if (stateDb && !options.stateDb) {
|
|
1406
|
+
try {
|
|
1407
|
+
stateDb.close();
|
|
1408
|
+
}
|
|
1409
|
+
catch {
|
|
1410
|
+
// best-effort close
|
|
1411
|
+
}
|
|
1175
1412
|
}
|
|
1413
|
+
if (dispatchLease)
|
|
1414
|
+
disposeLoweredExecutionDispatchLease(dispatchLease);
|
|
1176
1415
|
}
|
|
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);
|
|
1416
|
+
const { sessions, processedCount, skippedCount, allProposalIds } = loopResult;
|
|
1417
|
+
if (loopResult.deferred > 0) {
|
|
1418
|
+
topLevelWarnings.push(`Reached maxSessionsPerRun=${maxSessionsPerRun}; ${loopResult.deferred} session(s) deferred to a later run.`);
|
|
1190
1419
|
}
|
|
1420
|
+
emitExtractTriageEvent({
|
|
1421
|
+
modelPlanCount,
|
|
1422
|
+
triageEnabled: triage.enabled,
|
|
1423
|
+
result: loopResult,
|
|
1424
|
+
sourceRun,
|
|
1425
|
+
eventsCtx: options.eventsCtx,
|
|
1426
|
+
});
|
|
1191
1427
|
return {
|
|
1192
1428
|
schemaVersion: 1,
|
|
1193
1429
|
ok: true,
|
|
@@ -1206,6 +1442,7 @@ export async function akmExtract(options) {
|
|
|
1206
1442
|
sessions,
|
|
1207
1443
|
warnings: topLevelWarnings,
|
|
1208
1444
|
durationMs: Date.now() - startMs,
|
|
1445
|
+
...(getNotices().length > 0 ? { notices: getNotices() } : {}),
|
|
1209
1446
|
};
|
|
1210
1447
|
}
|
|
1211
1448
|
/**
|
|
@@ -1241,10 +1478,21 @@ export function countNewExtractCandidates(_config, options = {}) {
|
|
|
1241
1478
|
resolveDefaultSinceMs(harness.name, Date.now(), {
|
|
1242
1479
|
...(options.stateDb ? { stateDb: options.stateDb } : {}),
|
|
1243
1480
|
...(options.stateDbPath ? { stateDbPath: options.stateDbPath } : {}),
|
|
1481
|
+
...(options.readOnly && !options.stateDb ? { skipTracking: true } : {}),
|
|
1244
1482
|
});
|
|
1245
|
-
const candidates = harness.listSessions({
|
|
1483
|
+
const candidates = harness.listSessions({
|
|
1484
|
+
sinceMs,
|
|
1485
|
+
...(options.readOnly ? { isolatedSnapshot: true } : {}),
|
|
1486
|
+
});
|
|
1246
1487
|
if (candidates.length === 0)
|
|
1247
1488
|
continue;
|
|
1489
|
+
// A dry planner with no pre-existing state database has no seen-session
|
|
1490
|
+
// ledger by definition. Count the discovered sessions directly instead
|
|
1491
|
+
// of creating state.db merely to prove that it is empty.
|
|
1492
|
+
if (options.readOnly && !stateDb) {
|
|
1493
|
+
total += candidates.length;
|
|
1494
|
+
continue;
|
|
1495
|
+
}
|
|
1248
1496
|
let seenMap = new Map();
|
|
1249
1497
|
try {
|
|
1250
1498
|
if (!stateDb) {
|