akm-cli 0.9.0-beta.9 → 0.9.0-rc.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 +715 -0
- package/README.md +12 -4
- package/dist/akm +38 -0
- package/dist/akm-migrate-storage +38 -0
- package/dist/assets/help/help-improve.md +9 -6
- package/dist/assets/hints/cli-hints-full.md +6 -5
- package/dist/assets/profiles/default.json +9 -4
- package/dist/assets/profiles/frequent.json +1 -1
- package/dist/assets/profiles/memory-focus.json +1 -1
- package/dist/assets/profiles/proactive-maintenance.json +25 -0
- package/dist/assets/profiles/quick.json +1 -1
- package/dist/assets/profiles/recombine-only.json +21 -0
- package/dist/assets/profiles/reflect-distill.json +30 -0
- package/dist/assets/profiles/synthesize.json +15 -0
- package/dist/assets/profiles/thorough.json +1 -1
- package/dist/assets/prompts/consolidate-system.md +23 -0
- package/dist/assets/prompts/contradiction-judge.md +33 -0
- package/dist/assets/prompts/distill-knowledge-system.md +22 -0
- package/dist/assets/prompts/distill-lesson-system.md +36 -0
- package/dist/assets/prompts/extract-session.md +11 -3
- package/dist/assets/prompts/graph-extract-system.md +1 -0
- package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
- package/dist/assets/prompts/memory-infer-system.md +1 -0
- package/dist/assets/prompts/memory-infer-user.md +5 -0
- package/dist/assets/prompts/metadata-enhance-system.md +1 -0
- package/dist/assets/prompts/procedural-system.md +44 -0
- package/dist/assets/prompts/recombine-system.md +40 -0
- package/dist/assets/prompts/staleness-detect-system.md +6 -0
- package/dist/assets/prompts/validate-summary-judge.md +1 -0
- package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
- package/dist/assets/templates/html/health.html +281 -111
- package/dist/assets/wiki/ingest-workflow-template.md +45 -16
- package/dist/assets/wiki/schema-template.md +4 -4
- package/dist/cli/clack.js +56 -0
- package/dist/cli/config-migrate.js +7 -1
- package/dist/cli/confirm.js +1 -1
- package/dist/cli/parse-args.js +46 -1
- package/dist/cli/shared.js +28 -0
- package/dist/cli.js +25 -21
- package/dist/commands/agent/agent-dispatch.js +3 -2
- package/dist/commands/agent/agent-support.js +0 -7
- package/dist/commands/agent/contribute-cli.js +26 -7
- package/dist/commands/config-cli.js +26 -13
- package/dist/commands/env/child-env.js +47 -0
- package/dist/commands/env/env-binding.js +95 -0
- package/dist/commands/env/env-cli.js +228 -292
- package/dist/commands/env/env.js +14 -67
- package/dist/commands/env/secret-cli.js +140 -138
- package/dist/commands/feedback-cli.js +156 -155
- package/dist/commands/graph/graph-cli.js +5 -13
- package/dist/commands/graph/graph.js +3 -3
- package/dist/commands/health/advisories.js +151 -0
- package/dist/commands/health/checks.js +103 -16
- package/dist/commands/health/html-report.js +447 -81
- package/dist/commands/health/improve-metrics.js +771 -0
- package/dist/commands/health/llm-usage.js +65 -0
- package/dist/commands/health/md-report.js +103 -0
- package/dist/commands/health/metrics.js +278 -0
- package/dist/commands/health/stash-exposure.js +46 -0
- package/dist/commands/health/surfaces.js +216 -0
- package/dist/commands/health/task-runs.js +135 -0
- package/dist/commands/health/types.js +26 -0
- package/dist/commands/health/windows.js +195 -0
- package/dist/commands/health.js +91 -1091
- package/dist/commands/improve/anti-collapse.js +170 -0
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/collapse-detector.js +421 -0
- package/dist/commands/improve/consolidate/chunking.js +141 -0
- package/dist/commands/improve/consolidate/eligibility.js +64 -0
- package/dist/commands/improve/consolidate/merge.js +145 -0
- package/dist/commands/improve/consolidate/sanitize.js +231 -0
- package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
- package/dist/commands/improve/consolidate.js +1295 -1277
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill/content-repair.js +202 -0
- package/dist/commands/improve/distill/promote-memory.js +229 -0
- package/dist/commands/improve/distill/quality-gate.js +236 -0
- package/dist/commands/improve/distill-guards.js +127 -0
- package/dist/commands/improve/distill-promotion-policy.js +826 -167
- package/dist/commands/improve/distill.js +228 -605
- package/dist/commands/improve/eligibility.js +434 -0
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +179 -59
- package/dist/commands/improve/extract-prompt.js +54 -3
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +409 -43
- package/dist/commands/improve/feedback-valence.js +54 -0
- package/dist/commands/improve/hot-probation.js +45 -0
- package/dist/commands/improve/improve-auto-accept.js +157 -10
- package/dist/commands/improve/improve-cli.js +115 -73
- package/dist/commands/improve/improve-profiles.js +28 -8
- package/dist/commands/improve/improve-result-file.js +15 -25
- package/dist/commands/improve/improve-session.js +58 -0
- package/dist/commands/improve/improve.js +485 -2764
- package/dist/commands/improve/locks.js +154 -0
- package/dist/commands/improve/loop-stages.js +1100 -0
- package/dist/commands/improve/memory/memory-belief.js +14 -15
- package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
- package/dist/commands/improve/memory/memory-improve.js +27 -27
- package/dist/commands/improve/outcome-loop.js +270 -0
- package/dist/commands/improve/preparation.js +2002 -0
- package/dist/commands/improve/proactive-maintenance.js +37 -35
- package/dist/commands/improve/procedural.js +398 -0
- package/dist/commands/improve/recombine.js +818 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +206 -45
- package/dist/commands/improve/salience.js +455 -0
- package/dist/commands/improve/schema-similarity-gate.js +168 -0
- package/dist/commands/improve/shared.js +51 -0
- package/dist/commands/improve/triage.js +93 -0
- package/dist/commands/lint/agent-linter.js +19 -24
- package/dist/commands/lint/base-linter.js +173 -60
- package/dist/commands/lint/command-linter.js +19 -24
- package/dist/commands/lint/env-key-rules.js +38 -1
- package/dist/commands/lint/fact-linter.js +39 -0
- package/dist/commands/lint/index.js +31 -13
- package/dist/commands/lint/memory-linter.js +1 -1
- package/dist/commands/lint/registry.js +7 -2
- package/dist/commands/lint/task-linter.js +3 -3
- package/dist/commands/lint/workflow-linter.js +26 -1
- package/dist/commands/observability-cli.js +4 -4
- package/dist/commands/proposal/drain-policies.js +13 -4
- package/dist/commands/proposal/drain.js +45 -51
- package/dist/commands/proposal/legacy-import.js +115 -0
- package/dist/commands/proposal/proposal-cli.js +24 -34
- package/dist/commands/proposal/proposal.js +2 -1
- package/dist/commands/proposal/propose.js +8 -3
- package/dist/commands/proposal/repository.js +829 -0
- package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
- package/dist/commands/proposal/validators/proposals.js +93 -895
- package/dist/commands/read/curate.js +410 -111
- package/dist/commands/read/knowledge.js +10 -3
- package/dist/commands/read/remember-cli.js +133 -138
- package/dist/commands/read/search-cli.js +15 -8
- package/dist/commands/read/search.js +22 -11
- package/dist/commands/read/show.js +106 -14
- package/dist/commands/registry-cli.js +76 -87
- package/dist/commands/remember.js +11 -12
- package/dist/commands/sources/add-cli.js +91 -95
- package/dist/commands/sources/history.js +1 -1
- package/dist/commands/sources/init.js +66 -18
- package/dist/commands/sources/installed-stashes.js +11 -3
- package/dist/commands/sources/migration-help.js +7 -4
- package/dist/commands/sources/schema-repair.js +44 -46
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/source-add.js +7 -3
- package/dist/commands/sources/sources-cli.js +3 -3
- package/dist/commands/sources/stash-cli.js +19 -39
- package/dist/commands/sources/stash-skeleton.js +57 -8
- package/dist/commands/tasks/default-tasks.js +15 -2
- package/dist/commands/tasks/tasks-cli.js +20 -29
- package/dist/commands/tasks/tasks.js +39 -11
- package/dist/commands/wiki-cli.js +23 -38
- package/dist/commands/workflow-cli.js +291 -13
- package/dist/core/asset/asset-registry.js +3 -1
- package/dist/core/asset/asset-spec.js +79 -5
- package/dist/core/asset/frontmatter.js +188 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/authoring-rules.js +92 -0
- package/dist/core/common.js +4 -23
- package/dist/core/concurrent.js +10 -1
- package/dist/core/config/config-io.js +10 -1
- package/dist/core/config/config-migration.js +18 -40
- package/dist/core/config/config-schema.js +403 -62
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config.js +67 -22
- package/dist/core/deep-merge.js +38 -0
- package/dist/core/errors.js +1 -0
- package/dist/core/eval/rank-metrics.js +113 -0
- package/dist/core/events.js +4 -7
- package/dist/core/improve-types.js +47 -8
- package/dist/core/json-schema.js +142 -0
- package/dist/core/logs-db.js +14 -75
- package/dist/core/parse.js +36 -16
- package/dist/core/paths.js +18 -18
- package/dist/core/standards/resolve-standards-context.js +87 -0
- package/dist/core/standards/resolve-stash-standards.js +99 -0
- package/dist/core/standards/resolve-type-conventions.js +66 -0
- package/dist/core/state/migrations.js +770 -0
- package/dist/core/state-db.js +132 -1126
- package/dist/core/structured.js +69 -0
- package/dist/core/time.js +53 -0
- package/dist/core/warn.js +21 -0
- package/dist/core/write-source.js +37 -0
- package/dist/indexer/db/db.js +261 -770
- package/dist/indexer/db/entry-mapper.js +41 -0
- package/dist/indexer/db/graph-db.js +129 -86
- package/dist/indexer/db/llm-cache.js +2 -2
- package/dist/indexer/db/schema.js +516 -0
- package/dist/indexer/ensure-index.js +36 -92
- package/dist/indexer/feedback/utility-policy.js +75 -0
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/graph/graph-extraction.js +207 -4
- package/dist/indexer/index-writer-lock.js +18 -11
- package/dist/indexer/index-written-assets.js +105 -0
- package/dist/indexer/indexer.js +182 -204
- package/dist/indexer/passes/dir-staleness.js +114 -0
- package/dist/indexer/passes/memory-inference.js +13 -5
- package/dist/indexer/passes/metadata.js +20 -0
- package/dist/indexer/read-preflight.js +23 -0
- package/dist/indexer/search/db-search.js +89 -13
- package/dist/indexer/search/fts-query.js +51 -0
- package/dist/indexer/search/ranking-contributors.js +95 -9
- package/dist/indexer/search/ranking.js +79 -3
- package/dist/indexer/search/search-fields.js +6 -0
- package/dist/indexer/search/search-source.js +32 -21
- package/dist/indexer/search/semantic-status.js +4 -0
- package/dist/indexer/walk/matchers.js +48 -0
- package/dist/indexer/walk/walker.js +21 -13
- package/dist/integrations/agent/builders.js +41 -13
- package/dist/integrations/agent/config.js +20 -59
- package/dist/integrations/agent/detect.js +9 -0
- package/dist/integrations/agent/index.js +3 -19
- package/dist/integrations/agent/model-aliases.js +16 -2
- package/dist/integrations/agent/profiles.js +79 -6
- package/dist/integrations/agent/prompts.js +75 -9
- package/dist/integrations/agent/runner-dispatch.js +83 -0
- package/dist/integrations/agent/runner.js +13 -9
- package/dist/integrations/agent/spawn.js +206 -81
- package/dist/integrations/harnesses/aider/agent-builder.js +113 -0
- package/dist/integrations/harnesses/aider/index.js +58 -0
- package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
- package/dist/integrations/harnesses/amazonq/agent-builder.js +153 -0
- package/dist/integrations/harnesses/amazonq/index.js +59 -0
- package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
- package/dist/integrations/harnesses/claude/agent-builder.js +46 -7
- package/dist/integrations/harnesses/claude/index.js +27 -23
- package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
- package/dist/integrations/harnesses/claude/session-log.js +10 -0
- package/dist/integrations/harnesses/codex/agent-builder.js +137 -0
- package/dist/integrations/harnesses/codex/index.js +63 -0
- package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
- package/dist/integrations/harnesses/copilot/agent-builder.js +122 -0
- package/dist/integrations/harnesses/copilot/index.js +60 -0
- package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
- package/dist/integrations/harnesses/gemini/agent-builder.js +121 -0
- package/dist/integrations/harnesses/gemini/index.js +60 -0
- package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
- package/dist/integrations/harnesses/index.js +28 -7
- package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
- package/dist/integrations/harnesses/opencode/index.js +17 -16
- package/dist/integrations/harnesses/opencode/session-log.js +173 -3
- package/dist/integrations/harnesses/opencode-sdk/harness.js +65 -0
- package/dist/integrations/harnesses/opencode-sdk/index.js +10 -34
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +642 -71
- package/dist/integrations/harnesses/openhands/agent-builder.js +126 -0
- package/dist/integrations/harnesses/openhands/index.js +58 -0
- package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
- package/dist/integrations/harnesses/pi/agent-builder.js +104 -0
- package/dist/integrations/harnesses/pi/index.js +58 -0
- package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
- package/dist/integrations/harnesses/types.js +8 -0
- package/dist/integrations/session-logs/index.js +40 -11
- package/dist/llm/call-ai.js +2 -2
- package/dist/llm/client.js +34 -11
- package/dist/llm/embedder.js +67 -4
- package/dist/llm/embedders/cache.js +3 -1
- package/dist/llm/embedders/deterministic.js +66 -0
- package/dist/llm/embedders/local.js +73 -3
- package/dist/llm/feature-gate.js +16 -15
- package/dist/llm/graph-extract.js +67 -44
- package/dist/llm/memory-infer-impl.js +138 -0
- package/dist/llm/memory-infer.js +1 -127
- package/dist/llm/metadata-enhance.js +44 -31
- package/dist/llm/structured-call.js +49 -0
- package/dist/migrate-storage-node.mjs +8 -0
- package/dist/output/context.js +5 -5
- package/dist/output/renderers.js +87 -15
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/shapes/helpers.js +0 -3
- package/dist/output/shapes/passthrough.js +6 -1
- package/dist/output/text/helpers.js +241 -2
- package/dist/output/text/workflow.js +4 -1
- package/dist/registry/providers/skills-sh.js +21 -147
- package/dist/registry/providers/static-index.js +15 -157
- package/dist/registry/resolve.js +27 -9
- package/dist/runtime.js +25 -1
- package/dist/schemas/akm-config.json +14225 -0
- package/dist/schemas/akm-workflow.json +328 -0
- package/dist/scripts/migrate-storage.js +2743 -8390
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +1652 -607
- package/dist/setup/detect.js +9 -0
- package/dist/setup/legacy-config.js +106 -0
- package/dist/setup/prompt.js +57 -0
- package/dist/setup/providers.js +14 -0
- package/dist/setup/registry-stash-loader.js +12 -0
- package/dist/setup/semantic-assets.js +124 -0
- package/dist/setup/setup.js +52 -1614
- package/dist/setup/steps/connection.js +734 -0
- package/dist/setup/steps/output.js +31 -0
- package/dist/setup/steps/platforms.js +124 -0
- package/dist/setup/steps/semantic.js +27 -0
- package/dist/setup/steps/sources.js +222 -0
- package/dist/setup/steps/stashdir.js +42 -0
- package/dist/setup/steps/tasks.js +152 -0
- package/dist/sources/include.js +6 -2
- package/dist/sources/providers/filesystem.js +0 -1
- package/dist/sources/providers/git-install.js +210 -0
- package/dist/sources/providers/git-provider.js +234 -0
- package/dist/sources/providers/git-stash.js +248 -0
- package/dist/sources/providers/git.js +10 -661
- package/dist/sources/providers/npm.js +2 -6
- package/dist/sources/providers/provider-utils.js +13 -7
- package/dist/sources/providers/sync-from-ref.js +9 -1
- package/dist/sources/providers/website.js +9 -5
- package/dist/sources/website-ingest.js +187 -29
- package/dist/sources/wiki-fetchers/registry.js +53 -0
- package/dist/sources/wiki-fetchers/youtube.js +239 -0
- package/dist/storage/database.js +45 -10
- package/dist/storage/managed-db.js +82 -0
- package/dist/storage/repositories/canaries-repository.js +107 -0
- package/dist/storage/repositories/consolidation-repository.js +38 -0
- package/dist/storage/repositories/embeddings-repository.js +72 -0
- package/dist/storage/repositories/events-repository.js +187 -0
- package/dist/storage/repositories/extract-sessions-repository.js +96 -0
- package/dist/storage/repositories/improve-runs-repository.js +146 -0
- package/dist/storage/repositories/index-db.js +14 -8
- package/dist/storage/repositories/proposals-repository.js +220 -0
- package/dist/storage/repositories/recombine-repository.js +213 -0
- package/dist/storage/repositories/registry-cache.js +93 -0
- package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
- package/dist/storage/repositories/task-history-repository.js +93 -0
- package/dist/storage/repositories/workflow-runs-repository.js +189 -1
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/tasks/backends/cron.js +1 -1
- package/dist/tasks/backends/index.js +9 -0
- package/dist/tasks/backends/launchd.js +1 -1
- package/dist/tasks/backends/schtasks.js +1 -1
- package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
- package/dist/tasks/runner.js +15 -13
- package/dist/text-import-hook.mjs +1 -1
- package/dist/wiki/wiki.js +52 -11
- package/dist/workflows/authoring/authoring.js +123 -10
- package/dist/workflows/authoring/workflow-program-template.yaml +31 -0
- package/dist/workflows/cli.js +5 -0
- package/dist/workflows/db.js +138 -4
- package/dist/workflows/exec/brief.js +484 -0
- package/dist/workflows/exec/native-executor.js +975 -0
- package/dist/workflows/exec/param-secrets.js +115 -0
- package/dist/workflows/exec/report.js +1295 -0
- package/dist/workflows/exec/run-workflow.js +596 -0
- package/dist/workflows/exec/scheduler.js +100 -0
- package/dist/workflows/exec/step-work.js +1156 -0
- package/dist/workflows/exec/unit-writer.js +23 -0
- package/dist/workflows/exec/watch.js +116 -0
- package/dist/workflows/exec/worktree.js +171 -0
- package/dist/workflows/ir/compile.js +388 -0
- package/dist/workflows/ir/params.js +54 -0
- package/dist/workflows/ir/plan-hash.js +33 -0
- package/dist/workflows/ir/schema.js +4 -0
- package/dist/workflows/parser.js +3 -1
- package/dist/workflows/program/expressions.js +369 -0
- package/dist/workflows/program/parser.js +760 -0
- package/dist/workflows/program/project.js +105 -0
- package/dist/workflows/program/schema.js +54 -0
- package/dist/workflows/renderer.js +82 -5
- package/dist/workflows/runtime/agent-identity.js +59 -14
- package/dist/workflows/runtime/runs.js +248 -153
- package/dist/workflows/runtime/unit-checkin.js +45 -0
- package/dist/workflows/runtime/workflow-asset-loader.js +188 -0
- package/dist/workflows/validate-summary.js +26 -10
- package/dist/workflows/validator.js +1 -1
- package/docs/README.md +69 -18
- package/docs/data-and-telemetry.md +7 -5
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.9.0-beta.60.md +19 -0
- package/docs/migration/release-notes/0.9.0.md +39 -0
- package/package.json +10 -10
- package/dist/assets/tasks/core/update-stashes.yml +0 -4
- package/dist/commands/db-cli.js +0 -23
- package/dist/indexer/db/db-backup.js +0 -376
- package/dist/indexer/passes/staleness-detect.js +0 -488
|
@@ -11,11 +11,109 @@
|
|
|
11
11
|
* This is the runtime surface of the {@link OpencodeSdkHarness} (`id =
|
|
12
12
|
* 'opencode-sdk'`). It is the dispatch path for `sdkMode` profiles; it exposes
|
|
13
13
|
* no native session logs of its own (`capabilities.sessionLogs = false`).
|
|
14
|
+
*
|
|
15
|
+
* ## Per-call cwd and env (redesign addendum R2, open seam decision 1)
|
|
16
|
+
*
|
|
17
|
+
* The plan left one decision open: per-call cwd/env forwarding vs a server
|
|
18
|
+
* keyed by `(cwd, envKeysHash)`. Reading the SDK settled it as a SPLIT — the
|
|
19
|
+
* two halves have different API realities (verified against
|
|
20
|
+
* `@opencode-ai/sdk` 1.2.20):
|
|
21
|
+
*
|
|
22
|
+
* - **cwd is PER-CALL.** `session.create` / `session.prompt` /
|
|
23
|
+
* `session.delete` all accept a `query.directory` parameter that scopes
|
|
24
|
+
* the session's working directory, so a single server can host sessions
|
|
25
|
+
* in any number of working directories. {@link RunAgentOptions.cwd} is
|
|
26
|
+
* forwarded as `query: { directory }` on every session call — no
|
|
27
|
+
* per-cwd server processes, no server-key explosion for worktree
|
|
28
|
+
* isolation (which mints a fresh directory per unit attempt).
|
|
29
|
+
*
|
|
30
|
+
* - **env is PER-SERVER (keyed registry).** The SDK exposes NO per-call or
|
|
31
|
+
* per-session env surface; the only way env reaches tool child processes
|
|
32
|
+
* is the `opencode serve` process environment, which
|
|
33
|
+
* `createOpencodeServer` copies from `process.env` **synchronously**
|
|
34
|
+
* (its `spawn` call runs before its first `await`, so the snapshot is
|
|
35
|
+
* taken inside our call frame). {@link getOrStartServer} therefore keys
|
|
36
|
+
* servers by a hash of the FULL env binding entries (keys AND values —
|
|
37
|
+
* two bindings that share keys but differ in values must not share a
|
|
38
|
+
* server), overlays the bindings onto `process.env` for exactly the
|
|
39
|
+
* synchronous prefix of the `createOpencode` call, and restores the
|
|
40
|
+
* previous values before awaiting. JavaScript's single-threaded event
|
|
41
|
+
* loop makes that overlay window atomic: no concurrently-running akm
|
|
42
|
+
* code can observe the mutated environment. Units with the same
|
|
43
|
+
* bindings share one server; units with none share the default server
|
|
44
|
+
* (byte-identical to the pre-R2 singleton behavior).
|
|
45
|
+
*
|
|
46
|
+
* This is what removed the workflow engine's `env_unsupported` hard-fail for
|
|
47
|
+
* the sdk runner: injection genuinely reaches the child, because tool
|
|
48
|
+
* subprocesses (bash etc.) inherit the server process environment.
|
|
49
|
+
*
|
|
50
|
+
* Registry hygiene (peer-review fixes):
|
|
51
|
+
*
|
|
52
|
+
* - **Ports.** `createOpencodeServer` binds a FIXED default port (4096),
|
|
53
|
+
* so coexisting registry entries would contend for the same bind. The
|
|
54
|
+
* default key keeps the SDK default; every env-keyed entry is started on
|
|
55
|
+
* its own OS-assigned free port (see {@link startServer}).
|
|
56
|
+
* - **Shutdown.** {@link closeServer} closes resolved servers
|
|
57
|
+
* SYNCHRONOUSLY — it runs from `process.once('exit')`, where Bun never
|
|
58
|
+
* drains microtasks, so a `.then()`-based close would orphan every
|
|
59
|
+
* `opencode serve` child.
|
|
60
|
+
*
|
|
61
|
+
* Process-lifecycle note (owner finding 4): a cached `opencode serve` child is
|
|
62
|
+
* a live OS handle that keeps Bun's event loop OPEN, so a one-shot CLI never
|
|
63
|
+
* becomes idle and `process.once('exit')` never fires — the exit hook alone
|
|
64
|
+
* cannot free a process the child is keeping alive (a deadlock that hangs the
|
|
65
|
+
* caller after an otherwise-successful run). The registry is therefore drained
|
|
66
|
+
* PROACTIVELY at the end of a dispatching command: the workflow engine calls
|
|
67
|
+
* `disposeDispatchResources()` (→ {@link closeServer}) in its run `finally`.
|
|
68
|
+
* The `process.once('exit')` hook stays as the last-resort backstop for paths
|
|
69
|
+
* that never reach that drain.
|
|
70
|
+
*
|
|
71
|
+
* ## Managed server spawn (owner finding 4, live-harness follow-up)
|
|
72
|
+
*
|
|
73
|
+
* Draining the registry is necessary but NOT sufficient with the SDK's own
|
|
74
|
+
* `createOpencodeServer`: its `close()` merely sends SIGTERM and it never
|
|
75
|
+
* `unref()`s the child or its stdio pipes, so akm's event loop stays pinned
|
|
76
|
+
* until the child ACTUALLY exits — and a real `opencode serve` (a live HTTP
|
|
77
|
+
* server with provider children) can outlive SIGTERM long enough to hang the
|
|
78
|
+
* caller indefinitely. {@link createManagedOpencode} therefore owns the spawn
|
|
79
|
+
* (the SDK package is used only for `createOpencodeClient`):
|
|
80
|
+
*
|
|
81
|
+
* - after the URL handshake, the child and its stdio are `unref()`ed /
|
|
82
|
+
* destroyed, so the handle can never hold akm open;
|
|
83
|
+
* - `close()` sends SIGTERM and arms a bounded grace timer
|
|
84
|
+
* ({@link SERVER_KILL_GRACE_MS}) that escalates to SIGKILL and is cleared
|
|
85
|
+
* on cooperative exit, so stubborn children cannot survive parent exit and
|
|
86
|
+
* stale timers cannot signal a reused PID;
|
|
87
|
+
* - the spawn (and its `process.env` snapshot) stays in the SYNCHRONOUS
|
|
88
|
+
* prefix of the factory call, preserving the env-overlay contract above.
|
|
14
89
|
*/
|
|
90
|
+
import { spawn } from "node:child_process";
|
|
91
|
+
import { createHash } from "node:crypto";
|
|
15
92
|
import { resolveSecret } from "../../../core/config/config.js";
|
|
16
93
|
import { DEFAULT_AGENT_TIMEOUT_MS } from "../../agent/config.js";
|
|
17
|
-
|
|
18
|
-
|
|
94
|
+
import { resolveModel } from "../../agent/model-aliases.js";
|
|
95
|
+
// Server registry — one server per env-binding signature, started lazily and
|
|
96
|
+
// reused across calls. The default (no env bindings) key is "" and behaves
|
|
97
|
+
// exactly like the pre-R2 process-wide singleton.
|
|
98
|
+
const _servers = new Map();
|
|
99
|
+
// Resolved servers by registry key, mirrored from `_servers` as each start
|
|
100
|
+
// promise settles. This exists so closeServer() can close started servers
|
|
101
|
+
// SYNCHRONOUSLY: it is wired to `process.once('exit')`, and Bun does not
|
|
102
|
+
// drain microtasks scheduled inside 'exit' handlers, so a `.then()`-based
|
|
103
|
+
// close never runs there and would orphan every `opencode serve` child.
|
|
104
|
+
const _resolvedServers = new Map();
|
|
105
|
+
// Listen ports handed to non-default registry entries (see startServer) —
|
|
106
|
+
// tracked so two coexisting servers in this process can never be assigned
|
|
107
|
+
// the same port.
|
|
108
|
+
const _serverPorts = new Map();
|
|
109
|
+
/** The port `createOpencodeServer` binds when none is passed (SDK 1.2.20). */
|
|
110
|
+
const DEFAULT_SDK_PORT = 4096;
|
|
111
|
+
// Test override: when set, every call uses this server (all keys) and no real
|
|
112
|
+
// server is ever started.
|
|
113
|
+
let _testServer = null;
|
|
114
|
+
// Test seam replacing the real `createOpencode` import (see __setServerFactory).
|
|
115
|
+
let _serverFactory = null;
|
|
116
|
+
let _exitHookInstalled = false;
|
|
19
117
|
/**
|
|
20
118
|
* Test-only seam: inject a fake {@link SdkServer} so `runOpencodeSdk` can be
|
|
21
119
|
* exercised without the real `@opencode-ai/sdk` (which would spin up a server).
|
|
@@ -24,20 +122,67 @@ let _server = null;
|
|
|
24
122
|
* leading underscores mark it as internal.
|
|
25
123
|
*/
|
|
26
124
|
export function __setTestServer(server) {
|
|
27
|
-
|
|
125
|
+
_testServer = server;
|
|
28
126
|
}
|
|
29
127
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
128
|
+
* Test-only seam: replace the `createOpencode` factory so the env-keyed
|
|
129
|
+
* server registry (module doc, *Per-call cwd and env*) can be exercised
|
|
130
|
+
* without the real SDK. The fake MUST read whatever `process.env` state it
|
|
131
|
+
* cares about in its SYNCHRONOUS prefix — exactly like the real
|
|
132
|
+
* `createOpencodeServer`, whose `spawn` snapshot happens before its first
|
|
133
|
+
* await — because the runner restores the env overlay as soon as the factory
|
|
134
|
+
* call returns its promise. Pass `null` to clear.
|
|
135
|
+
*/
|
|
136
|
+
export function __setServerFactory(factory) {
|
|
137
|
+
_serverFactory = factory;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Close every started OpenCode SDK server and reset the registry (and any
|
|
141
|
+
* injected test server). Used by tests for clean teardown between runs and
|
|
142
|
+
* wired to `process.once('exit')` — which is why resolved servers MUST be
|
|
143
|
+
* closed synchronously here: Bun never drains microtasks scheduled inside
|
|
144
|
+
* 'exit' handlers, so a promise-based close would silently orphan the
|
|
145
|
+
* `opencode serve` children (leaking processes AND keeping their ports
|
|
146
|
+
* bound for the next invocation).
|
|
32
147
|
*/
|
|
33
148
|
export function closeServer() {
|
|
149
|
+
for (const [key, pending] of _servers) {
|
|
150
|
+
const resolved = _resolvedServers.get(key);
|
|
151
|
+
if (resolved) {
|
|
152
|
+
// Synchronous close — safe from the 'exit' hook.
|
|
153
|
+
try {
|
|
154
|
+
resolved.server.close();
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
/* ignore */
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
// Still starting: close on arrival. This branch can never complete
|
|
162
|
+
// inside the 'exit' hook (no microtasks there), but it keeps
|
|
163
|
+
// mid-start test teardown leak-free.
|
|
164
|
+
pending
|
|
165
|
+
.then((s) => {
|
|
166
|
+
try {
|
|
167
|
+
s.server.close();
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
/* ignore */
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
.catch(() => { });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
_servers.clear();
|
|
177
|
+
_resolvedServers.clear();
|
|
178
|
+
_serverPorts.clear();
|
|
34
179
|
try {
|
|
35
|
-
|
|
180
|
+
_testServer?.server.close();
|
|
36
181
|
}
|
|
37
182
|
catch {
|
|
38
183
|
/* ignore */
|
|
39
184
|
}
|
|
40
|
-
|
|
185
|
+
_testServer = null;
|
|
41
186
|
}
|
|
42
187
|
/**
|
|
43
188
|
* Convert an `AgentDispatchRequest.tools` policy into the SDK's tool-allowlist
|
|
@@ -85,16 +230,23 @@ function toolsToSdkAllowlist(tools) {
|
|
|
85
230
|
out[n] = true;
|
|
86
231
|
return out;
|
|
87
232
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
233
|
+
/**
|
|
234
|
+
* Assemble the OpenCode SDK server config from the profile + LLM fallback.
|
|
235
|
+
* Pure and exported for tests. `profile.model` is resolved through the model
|
|
236
|
+
* alias tables (platform key `"opencode-sdk"`) so config aliases like
|
|
237
|
+
* `"model": "fast"` work on the SDK path the same way they do for CLI
|
|
238
|
+
* builders. Note there is no built-in alias column for `opencode-sdk` —
|
|
239
|
+
* built-in opus/sonnet/haiku strings are CLI-provider-qualified and would
|
|
240
|
+
* collide with the `akm-custom/` provider prefixing below, so only profile
|
|
241
|
+
* and config-root alias tables apply here.
|
|
242
|
+
*/
|
|
243
|
+
export function buildSdkConfig(profile, llmConfig) {
|
|
94
244
|
// Resolve endpoint and model: profile fields take precedence over config.llm
|
|
95
245
|
const endpoint = profile.endpoint ?? llmConfig?.endpoint;
|
|
96
246
|
const apiKey = resolveSecret(profile.apiKey ?? llmConfig?.apiKey);
|
|
97
|
-
const model = profile.model
|
|
247
|
+
const model = profile.model
|
|
248
|
+
? resolveModel(profile.model, "opencode-sdk", profile.modelAliases, profile.globalModelAliases)
|
|
249
|
+
: undefined;
|
|
98
250
|
const sdkConfig = {};
|
|
99
251
|
if (model)
|
|
100
252
|
sdkConfig.model = model;
|
|
@@ -114,19 +266,375 @@ async function getOrStartServer(profile, llmConfig) {
|
|
|
114
266
|
sdkConfig.model = `akm-custom/${model}`;
|
|
115
267
|
}
|
|
116
268
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
269
|
+
return sdkConfig;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Stable key for the env-keyed server registry: sha256 over the SORTED
|
|
273
|
+
* binding entries (keys AND values — see module doc), "" when no bindings.
|
|
274
|
+
*/
|
|
275
|
+
function envServerKey(env) {
|
|
276
|
+
if (!env || Object.keys(env).length === 0)
|
|
277
|
+
return "";
|
|
278
|
+
const entries = Object.entries(env).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
279
|
+
return createHash("sha256").update(JSON.stringify(entries)).digest("hex");
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Overlay `env` onto `process.env`, returning a restore function. The
|
|
283
|
+
* overlay is intended to live only for the SYNCHRONOUS prefix of the server
|
|
284
|
+
* factory call (module doc): mutation → factory() → restore happens in one
|
|
285
|
+
* uninterruptible event-loop turn, so no other code observes it.
|
|
286
|
+
*/
|
|
287
|
+
function overlayProcessEnv(env) {
|
|
288
|
+
const previous = new Map();
|
|
289
|
+
for (const [key, value] of Object.entries(env)) {
|
|
290
|
+
previous.set(key, process.env[key]);
|
|
291
|
+
process.env[key] = value;
|
|
292
|
+
}
|
|
293
|
+
return () => {
|
|
294
|
+
for (const [key, prior] of previous) {
|
|
295
|
+
if (prior === undefined)
|
|
296
|
+
delete process.env[key];
|
|
297
|
+
else
|
|
298
|
+
process.env[key] = prior;
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Ask the OS for a currently-free localhost port (bind :0, read the assigned
|
|
304
|
+
* port, release it). Skips the SDK's fixed default port and any port already
|
|
305
|
+
* handed to another registry entry in this process, so coexisting servers
|
|
306
|
+
* never contend. The probe-then-use gap is the standard free-port race —
|
|
307
|
+
* acceptable here because the failure mode is a clean `spawn_failed` on the
|
|
308
|
+
* next dispatch, not corruption.
|
|
309
|
+
*/
|
|
310
|
+
async function allocateFreePort() {
|
|
311
|
+
const { createServer } = await import("node:net");
|
|
312
|
+
const taken = new Set(_serverPorts.values());
|
|
313
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
314
|
+
const port = await new Promise((resolve, reject) => {
|
|
315
|
+
const probe = createServer();
|
|
316
|
+
probe.unref();
|
|
317
|
+
probe.on("error", reject);
|
|
318
|
+
probe.listen(0, "127.0.0.1", () => {
|
|
319
|
+
const address = probe.address();
|
|
320
|
+
probe.close(() => {
|
|
321
|
+
if (address && typeof address === "object")
|
|
322
|
+
resolve(address.port);
|
|
323
|
+
else
|
|
324
|
+
reject(new Error("could not read the probe socket's port"));
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
if (port !== DEFAULT_SDK_PORT && !taken.has(port))
|
|
329
|
+
return port;
|
|
330
|
+
}
|
|
331
|
+
throw new Error("could not allocate a free port for the OpenCode SDK server");
|
|
332
|
+
}
|
|
333
|
+
/** Grace between SIGTERM and SIGKILL when closing a managed server child. */
|
|
334
|
+
const SERVER_KILL_GRACE_MS = 2_000;
|
|
335
|
+
/** How long the managed spawn waits for the server's listening handshake. */
|
|
336
|
+
const SERVER_START_TIMEOUT_MS = 5_000;
|
|
337
|
+
// Test seam: override the argv used to spawn the server child ("opencode"
|
|
338
|
+
// plus serve flags by default) so the managed-spawn lifecycle (handshake,
|
|
339
|
+
// unref, SIGTERM→SIGKILL escalation) is testable without the real binary.
|
|
340
|
+
let _serveCommand = null;
|
|
341
|
+
/** Test-only seam: replace the `opencode serve` argv. Pass `null` to clear. */
|
|
342
|
+
export function __setServeCommand(argv) {
|
|
343
|
+
_serveCommand = argv;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Spawn-owning replacement for the SDK's `createOpencode` (module doc,
|
|
347
|
+
* *Managed server spawn*). Mirrors `createOpencodeServer`'s contract — the
|
|
348
|
+
* `spawn` (and its `process.env` snapshot) happens in the SYNCHRONOUS prefix,
|
|
349
|
+
* `OPENCODE_CONFIG_CONTENT` carries the config, the handshake parses the
|
|
350
|
+
* "opencode server listening on <url>" line — but manages the child so its
|
|
351
|
+
* handle can never pin akm's event loop:
|
|
352
|
+
*
|
|
353
|
+
* - handshake success → stdio destroyed, listeners dropped, `proc.unref()`;
|
|
354
|
+
* - `close()` → SIGTERM now, SIGKILL after an unref'ed grace timer;
|
|
355
|
+
* - handshake failure → the child is killed and unref'ed before rejecting.
|
|
356
|
+
*/
|
|
357
|
+
async function createManagedOpencode(options) {
|
|
358
|
+
const { createOpencodeClient } = (await import("@opencode-ai/sdk").catch(() => {
|
|
359
|
+
throw new Error("OpenCode SDK not available. Install @opencode-ai/sdk or configure a CLI agent instead.");
|
|
360
|
+
}));
|
|
361
|
+
const port = options.port ?? DEFAULT_SDK_PORT;
|
|
362
|
+
const argv = _serveCommand ?? ["opencode", "serve", "--hostname=127.0.0.1", `--port=${port}`];
|
|
363
|
+
// Synchronous prefix: the env snapshot (incl. any binding overlay in the
|
|
364
|
+
// caller's frame) is taken HERE, before the first await below.
|
|
365
|
+
const proc = spawn(argv[0], argv.slice(1), {
|
|
366
|
+
env: { ...process.env, OPENCODE_CONFIG_CONTENT: JSON.stringify(options.config ?? {}) },
|
|
367
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
120
368
|
});
|
|
121
|
-
|
|
369
|
+
let closeStarted = false;
|
|
370
|
+
let closeEscalation;
|
|
371
|
+
const childExited = () => proc.exitCode !== null || proc.signalCode !== null;
|
|
372
|
+
const clearCloseEscalation = () => {
|
|
373
|
+
if (closeEscalation !== undefined) {
|
|
374
|
+
clearTimeout(closeEscalation);
|
|
375
|
+
closeEscalation = undefined;
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
const closeManaged = () => {
|
|
379
|
+
if (closeStarted)
|
|
380
|
+
return;
|
|
381
|
+
closeStarted = true;
|
|
382
|
+
if (childExited())
|
|
383
|
+
return;
|
|
384
|
+
proc.once("exit", clearCloseEscalation);
|
|
385
|
+
try {
|
|
386
|
+
proc.kill("SIGTERM");
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
proc.off("exit", clearCloseEscalation);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (childExited())
|
|
393
|
+
return;
|
|
394
|
+
closeEscalation = setTimeout(() => {
|
|
395
|
+
closeEscalation = undefined;
|
|
396
|
+
if (childExited())
|
|
397
|
+
return;
|
|
398
|
+
try {
|
|
399
|
+
proc.kill("SIGKILL");
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
/* already dead */
|
|
403
|
+
}
|
|
404
|
+
}, SERVER_KILL_GRACE_MS);
|
|
405
|
+
};
|
|
406
|
+
const url = await new Promise((resolve, reject) => {
|
|
407
|
+
let output = "";
|
|
408
|
+
let timer;
|
|
409
|
+
let settled = false;
|
|
410
|
+
const cleanupStartup = () => {
|
|
411
|
+
if (timer !== undefined) {
|
|
412
|
+
clearTimeout(timer);
|
|
413
|
+
timer = undefined;
|
|
414
|
+
}
|
|
415
|
+
proc.stdout?.off("data", onStdoutData);
|
|
416
|
+
proc.stderr?.off("data", onStderrData);
|
|
417
|
+
proc.off("exit", onExit);
|
|
418
|
+
proc.off("error", onError);
|
|
419
|
+
};
|
|
420
|
+
const fail = (err) => {
|
|
421
|
+
if (settled)
|
|
422
|
+
return;
|
|
423
|
+
settled = true;
|
|
424
|
+
cleanupStartup();
|
|
425
|
+
closeManaged();
|
|
426
|
+
proc.stdout?.destroy();
|
|
427
|
+
proc.stderr?.destroy();
|
|
428
|
+
proc.unref();
|
|
429
|
+
reject(err);
|
|
430
|
+
};
|
|
431
|
+
const succeed = (serverUrl) => {
|
|
432
|
+
if (settled)
|
|
433
|
+
return;
|
|
434
|
+
settled = true;
|
|
435
|
+
cleanupStartup();
|
|
436
|
+
resolve(serverUrl);
|
|
437
|
+
};
|
|
438
|
+
const onStdoutData = (chunk) => {
|
|
439
|
+
output += chunk.toString();
|
|
440
|
+
for (const line of output.split("\n")) {
|
|
441
|
+
if (line.startsWith("opencode server listening")) {
|
|
442
|
+
const match = line.match(/on\s+(https?:\/\/\S+)/);
|
|
443
|
+
if (!match?.[1]) {
|
|
444
|
+
fail(new Error(`Failed to parse the OpenCode server url from: ${line}`));
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
succeed(match[1]);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
const onStderrData = (chunk) => {
|
|
453
|
+
output += chunk.toString();
|
|
454
|
+
};
|
|
455
|
+
const onExit = (code, signal) => {
|
|
456
|
+
const status = code !== null ? `code ${code}` : `signal ${signal ?? "unknown"}`;
|
|
457
|
+
fail(new Error(`OpenCode server exited with ${status}${output.trim() ? `\nServer output: ${output}` : ""}`));
|
|
458
|
+
};
|
|
459
|
+
const onError = (err) => {
|
|
460
|
+
fail(err instanceof Error ? err : new Error(String(err)));
|
|
461
|
+
};
|
|
462
|
+
timer = setTimeout(() => {
|
|
463
|
+
fail(new Error(`Timeout waiting for the OpenCode server to start after ${SERVER_START_TIMEOUT_MS}ms`));
|
|
464
|
+
}, SERVER_START_TIMEOUT_MS);
|
|
465
|
+
proc.stdout?.on("data", onStdoutData);
|
|
466
|
+
proc.stderr?.on("data", onStderrData);
|
|
467
|
+
proc.on("exit", onExit);
|
|
468
|
+
proc.on("error", onError);
|
|
469
|
+
});
|
|
470
|
+
// Handshake done: from here on the child must never hold akm open. Its
|
|
471
|
+
// lifetime is managed explicitly (closeServer → closeManaged), not by the
|
|
472
|
+
// event loop. Destroying the pipes also releases their loop handles.
|
|
473
|
+
proc.stdout?.destroy();
|
|
474
|
+
proc.stderr?.destroy();
|
|
475
|
+
proc.unref();
|
|
476
|
+
return { client: createOpencodeClient({ baseUrl: url }), server: { close: closeManaged } };
|
|
477
|
+
}
|
|
478
|
+
async function startServer(profile, llmConfig, env, registryKey) {
|
|
479
|
+
const factory = _serverFactory ?? createManagedOpencode;
|
|
480
|
+
const sdkConfig = buildSdkConfig(profile, llmConfig);
|
|
481
|
+
const options = Object.keys(sdkConfig).length > 0 ? { config: sdkConfig } : {};
|
|
482
|
+
// Port discipline: `createOpencodeServer` defaults to a FIXED port (4096),
|
|
483
|
+
// so two coexisting servers — the default one plus any env-keyed one —
|
|
484
|
+
// would contend for the same bind and the second start would fail. The
|
|
485
|
+
// default key keeps the SDK default (byte-identical to the pre-R2
|
|
486
|
+
// singleton); every other registry entry gets its own OS-assigned free
|
|
487
|
+
// port, allocated BEFORE the env overlay below (allocation awaits).
|
|
488
|
+
if (registryKey !== "") {
|
|
489
|
+
const port = await allocateFreePort();
|
|
490
|
+
_serverPorts.set(registryKey, port);
|
|
491
|
+
options.port = port;
|
|
492
|
+
}
|
|
493
|
+
// Env injection (module doc): the SDK's createOpencodeServer snapshots
|
|
494
|
+
// process.env synchronously (its spawn precedes its first await), so the
|
|
495
|
+
// overlay only needs to survive the factory's synchronous prefix. Restore
|
|
496
|
+
// BEFORE awaiting, so nothing else ever runs under the mutated env.
|
|
497
|
+
let pending;
|
|
498
|
+
if (env && Object.keys(env).length > 0) {
|
|
499
|
+
const restore = overlayProcessEnv(env);
|
|
500
|
+
try {
|
|
501
|
+
pending = factory(options);
|
|
502
|
+
}
|
|
503
|
+
finally {
|
|
504
|
+
restore();
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
pending = factory(options);
|
|
509
|
+
}
|
|
510
|
+
const server = await pending;
|
|
511
|
+
if (!server)
|
|
122
512
|
throw new Error("Failed to initialise OpenCode SDK server.");
|
|
123
|
-
|
|
513
|
+
if (!_exitHookInstalled) {
|
|
514
|
+
_exitHookInstalled = true;
|
|
515
|
+
process.once("exit", () => {
|
|
516
|
+
closeServer();
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
return server;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Get (or lazily start) the server for this call's env bindings. Servers are
|
|
523
|
+
* keyed by {@link envServerKey}; concurrent callers of the same key share one
|
|
524
|
+
* start (the registry stores the in-flight promise). A failed start is
|
|
525
|
+
* evicted so the next call can retry instead of caching the error forever.
|
|
526
|
+
*/
|
|
527
|
+
async function getOrStartServer(profile, llmConfig, env) {
|
|
528
|
+
if (_testServer)
|
|
529
|
+
return _testServer;
|
|
530
|
+
const key = envServerKey(env);
|
|
531
|
+
let pending = _servers.get(key);
|
|
532
|
+
if (!pending) {
|
|
533
|
+
pending = startServer(profile, llmConfig, env, key);
|
|
534
|
+
_servers.set(key, pending);
|
|
535
|
+
pending.then((server) => {
|
|
536
|
+
// Mirror into the synchronously-closable registry (see closeServer)
|
|
537
|
+
// — but only while this start is still the live entry (closeServer
|
|
538
|
+
// may have cleared the registry mid-start).
|
|
539
|
+
if (_servers.get(key) === pending)
|
|
540
|
+
_resolvedServers.set(key, server);
|
|
541
|
+
}, () => {
|
|
542
|
+
if (_servers.get(key) === pending) {
|
|
543
|
+
_servers.delete(key);
|
|
544
|
+
_serverPorts.delete(key);
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
return pending;
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Extract best-effort token usage from a prompt response. Only numeric
|
|
552
|
+
* fields the server actually reported are copied; returns undefined when
|
|
553
|
+
* nothing usable is present (older servers, test fakes).
|
|
554
|
+
*/
|
|
555
|
+
function extractUsage(info) {
|
|
556
|
+
const tokens = info?.tokens;
|
|
557
|
+
if (!tokens)
|
|
558
|
+
return undefined;
|
|
559
|
+
const usage = {};
|
|
560
|
+
if (typeof tokens.input === "number" && Number.isFinite(tokens.input))
|
|
561
|
+
usage.inputTokens = tokens.input;
|
|
562
|
+
if (typeof tokens.output === "number" && Number.isFinite(tokens.output))
|
|
563
|
+
usage.outputTokens = tokens.output;
|
|
564
|
+
if (typeof tokens.reasoning === "number" && Number.isFinite(tokens.reasoning)) {
|
|
565
|
+
usage.reasoningTokens = tokens.reasoning;
|
|
566
|
+
}
|
|
567
|
+
return Object.keys(usage).length > 0 ? usage : undefined;
|
|
568
|
+
}
|
|
569
|
+
const SDK_OPERATION_TIMED_OUT = Symbol("opencode-sdk-operation-timeout");
|
|
570
|
+
const SDK_OPERATION_ABORTED = Symbol("opencode-sdk-operation-aborted");
|
|
571
|
+
const SDK_SESSION_DELETE_TIMEOUT_MS = 5_000;
|
|
572
|
+
async function raceSdkOperation(operation, opts) {
|
|
573
|
+
let timer;
|
|
574
|
+
let onAbort;
|
|
575
|
+
const racers = [operation];
|
|
576
|
+
if (opts.timeoutMs !== null) {
|
|
577
|
+
racers.push(new Promise((resolve) => {
|
|
578
|
+
timer = opts.setTimeoutFn(() => resolve(SDK_OPERATION_TIMED_OUT), opts.timeoutMs ?? 0);
|
|
579
|
+
}));
|
|
580
|
+
}
|
|
581
|
+
if (opts.signal) {
|
|
582
|
+
racers.push(new Promise((resolve) => {
|
|
583
|
+
onAbort = () => resolve(SDK_OPERATION_ABORTED);
|
|
584
|
+
if (opts.signal?.aborted)
|
|
585
|
+
onAbort();
|
|
586
|
+
else
|
|
587
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
588
|
+
}));
|
|
589
|
+
}
|
|
590
|
+
try {
|
|
591
|
+
return racers.length === 1 ? await operation : await Promise.race(racers);
|
|
592
|
+
}
|
|
593
|
+
finally {
|
|
594
|
+
if (timer !== undefined)
|
|
595
|
+
opts.clearTimeoutFn(timer);
|
|
596
|
+
if (opts.signal && onAbort)
|
|
597
|
+
opts.signal.removeEventListener("abort", onAbort);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function errorText(err) {
|
|
601
|
+
return err instanceof Error ? err.message : String(err);
|
|
602
|
+
}
|
|
603
|
+
function appendStderr(stderr, message) {
|
|
604
|
+
return stderr ? `${stderr}\n${message}` : message;
|
|
605
|
+
}
|
|
606
|
+
async function deleteSessionBestEffort(client, sessionId, query, setTimeoutFn, clearTimeoutFn) {
|
|
607
|
+
try {
|
|
608
|
+
const deleted = await raceSdkOperation(client.session.delete({ path: { id: sessionId }, ...(query ? { query } : {}) }), {
|
|
609
|
+
timeoutMs: SDK_SESSION_DELETE_TIMEOUT_MS,
|
|
610
|
+
setTimeoutFn,
|
|
611
|
+
clearTimeoutFn,
|
|
612
|
+
});
|
|
613
|
+
if (deleted === SDK_OPERATION_TIMED_OUT) {
|
|
614
|
+
return `OpenCode session cleanup timed out after ${SDK_SESSION_DELETE_TIMEOUT_MS}ms`;
|
|
615
|
+
}
|
|
616
|
+
return undefined;
|
|
617
|
+
}
|
|
618
|
+
catch (err) {
|
|
619
|
+
return `OpenCode session cleanup failed: ${errorText(err)}`;
|
|
620
|
+
}
|
|
124
621
|
}
|
|
125
622
|
export async function runOpencodeSdk(profile, prompt, opts = {}, llmConfig) {
|
|
126
623
|
const start = Date.now();
|
|
624
|
+
if (opts.signal?.aborted) {
|
|
625
|
+
return {
|
|
626
|
+
ok: false,
|
|
627
|
+
stdout: "",
|
|
628
|
+
stderr: "",
|
|
629
|
+
durationMs: 0,
|
|
630
|
+
exitCode: null,
|
|
631
|
+
reason: "aborted",
|
|
632
|
+
error: `opencode-sdk agent "${profile.name}" not started: caller signal already aborted`,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
127
635
|
let client;
|
|
128
636
|
try {
|
|
129
|
-
({ client } = await getOrStartServer(profile, llmConfig));
|
|
637
|
+
({ client } = await getOrStartServer(profile, llmConfig, opts.env));
|
|
130
638
|
}
|
|
131
639
|
catch (e) {
|
|
132
640
|
return {
|
|
@@ -139,9 +647,69 @@ export async function runOpencodeSdk(profile, prompt, opts = {}, llmConfig) {
|
|
|
139
647
|
error: String(e),
|
|
140
648
|
};
|
|
141
649
|
}
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
650
|
+
// #564 bug fix (3): enforce a hard timeout like the CLI path (runAgent).
|
|
651
|
+
// Previously runOpencodeSdk() awaited SDK calls with no timeout, so a stalled
|
|
652
|
+
// local-model endpoint or wedged server could block the caller indefinitely.
|
|
653
|
+
// We resolve the same budget runAgent uses (opts.timeoutMs override →
|
|
654
|
+
// profile.timeoutMs → DEFAULT_AGENT_TIMEOUT_MS) and race both session.create
|
|
655
|
+
// and session.prompt against it. null disables the timer (parity with
|
|
656
|
+
// runAgent's "no timeout" contract). Session cleanup is separately bounded
|
|
657
|
+
// by a short best-effort timer so timeout/abort results cannot be pinned in
|
|
658
|
+
// the finally path by a hung delete call.
|
|
659
|
+
const timeoutMs = opts.timeoutMs !== undefined ? opts.timeoutMs : (profile.timeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS);
|
|
660
|
+
const setTimeoutImpl = opts.setTimeoutFn ?? setTimeout;
|
|
661
|
+
const clearTimeoutImpl = opts.clearTimeoutFn ?? clearTimeout;
|
|
662
|
+
// Per-call working directory (module doc): forwarded as the SDK's
|
|
663
|
+
// `query.directory` on every session call, so worktree-isolated units run
|
|
664
|
+
// in their own checkout without a per-cwd server.
|
|
665
|
+
const query = opts.cwd ? { directory: opts.cwd } : undefined;
|
|
666
|
+
// One session per call — do NOT reuse (history accumulates, token costs grow).
|
|
667
|
+
// Session creation is startup plumbing, so failures map to spawn_failed rather
|
|
668
|
+
// than bubbling out as a generic workflow dispatch exception.
|
|
669
|
+
const abortSignal = opts.signal;
|
|
670
|
+
let sessionId;
|
|
671
|
+
try {
|
|
672
|
+
const created = await raceSdkOperation(client.session.create({ body: { title: "akm" }, ...(query ? { query } : {}) }), {
|
|
673
|
+
timeoutMs,
|
|
674
|
+
setTimeoutFn: setTimeoutImpl,
|
|
675
|
+
clearTimeoutFn: clearTimeoutImpl,
|
|
676
|
+
signal: abortSignal,
|
|
677
|
+
});
|
|
678
|
+
if (created === SDK_OPERATION_ABORTED) {
|
|
679
|
+
return {
|
|
680
|
+
ok: false,
|
|
681
|
+
stdout: "",
|
|
682
|
+
stderr: "",
|
|
683
|
+
durationMs: Date.now() - start,
|
|
684
|
+
exitCode: null,
|
|
685
|
+
reason: "aborted",
|
|
686
|
+
error: `opencode-sdk agent "${profile.name}" aborted by caller signal`,
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
if (created === SDK_OPERATION_TIMED_OUT) {
|
|
690
|
+
return {
|
|
691
|
+
ok: false,
|
|
692
|
+
stdout: "",
|
|
693
|
+
stderr: "",
|
|
694
|
+
durationMs: Date.now() - start,
|
|
695
|
+
exitCode: null,
|
|
696
|
+
reason: "timeout",
|
|
697
|
+
error: `opencode-sdk agent "${profile.name}" timed out creating a session after ${timeoutMs}ms`,
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
sessionId = created.data?.id;
|
|
701
|
+
}
|
|
702
|
+
catch (err) {
|
|
703
|
+
return {
|
|
704
|
+
ok: false,
|
|
705
|
+
stdout: "",
|
|
706
|
+
stderr: errorText(err),
|
|
707
|
+
durationMs: Date.now() - start,
|
|
708
|
+
exitCode: 1,
|
|
709
|
+
reason: "spawn_failed",
|
|
710
|
+
error: errorText(err),
|
|
711
|
+
};
|
|
712
|
+
}
|
|
145
713
|
if (!sessionId) {
|
|
146
714
|
return {
|
|
147
715
|
ok: false,
|
|
@@ -165,33 +733,28 @@ export async function runOpencodeSdk(profile, prompt, opts = {}, llmConfig) {
|
|
|
165
733
|
body.system = system;
|
|
166
734
|
if (tools)
|
|
167
735
|
body.tools = tools;
|
|
168
|
-
|
|
169
|
-
// Previously runOpencodeSdk() awaited session.prompt() with no timeout, so a
|
|
170
|
-
// hung SDK call (e.g. a stalled local-model endpoint) blocked the caller
|
|
171
|
-
// indefinitely while the CLI path would have killed the process. We resolve
|
|
172
|
-
// the same budget runAgent uses (opts.timeoutMs override → profile.timeoutMs
|
|
173
|
-
// → DEFAULT_AGENT_TIMEOUT_MS) and race the prompt against it. null disables
|
|
174
|
-
// the timer (parity with runAgent's "no timeout" contract). There is no
|
|
175
|
-
// OS process to SIGTERM/SIGKILL here, so on timeout we best-effort delete the
|
|
176
|
-
// session (the SDK's equivalent of reaping the in-flight work) and return a
|
|
177
|
-
// structured `timeout` failure with the same reason vocabulary as the CLI.
|
|
178
|
-
const timeoutMs = opts.timeoutMs !== undefined ? opts.timeoutMs : (profile.timeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS);
|
|
179
|
-
const setTimeoutImpl = opts.setTimeoutFn ?? setTimeout;
|
|
180
|
-
const clearTimeoutImpl = opts.clearTimeoutFn ?? clearTimeout;
|
|
181
|
-
let timer;
|
|
182
|
-
const TIMED_OUT = Symbol("opencode-sdk-timeout");
|
|
736
|
+
let result;
|
|
183
737
|
try {
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
:
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
738
|
+
const prompted = await raceSdkOperation(client.session.prompt({ path: { id: sessionId }, body, ...(query ? { query } : {}) }), {
|
|
739
|
+
timeoutMs,
|
|
740
|
+
setTimeoutFn: setTimeoutImpl,
|
|
741
|
+
clearTimeoutFn: clearTimeoutImpl,
|
|
742
|
+
signal: abortSignal,
|
|
743
|
+
});
|
|
744
|
+
if (prompted === SDK_OPERATION_ABORTED) {
|
|
745
|
+
result = {
|
|
746
|
+
ok: false,
|
|
747
|
+
stdout: "",
|
|
748
|
+
stderr: "",
|
|
749
|
+
durationMs: Date.now() - start,
|
|
750
|
+
exitCode: null,
|
|
751
|
+
reason: "aborted",
|
|
752
|
+
error: `opencode-sdk agent "${profile.name}" aborted by caller signal`,
|
|
753
|
+
sessionId,
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
else if (prompted === SDK_OPERATION_TIMED_OUT) {
|
|
757
|
+
result = {
|
|
195
758
|
ok: false,
|
|
196
759
|
stdout: "",
|
|
197
760
|
stderr: "",
|
|
@@ -199,36 +762,44 @@ export async function runOpencodeSdk(profile, prompt, opts = {}, llmConfig) {
|
|
|
199
762
|
exitCode: null,
|
|
200
763
|
reason: "timeout",
|
|
201
764
|
error: `opencode-sdk agent "${profile.name}" timed out after ${timeoutMs}ms`,
|
|
765
|
+
sessionId,
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
const parts = prompted.data?.parts ?? [];
|
|
770
|
+
const textPart = parts.find((p) => p.type === "text");
|
|
771
|
+
const stdout = textPart?.text ?? "";
|
|
772
|
+
// Token accounting from the AssistantMessage (previously discarded) —
|
|
773
|
+
// the seam that makes workflow budget.maxTokens meterable on the
|
|
774
|
+
// default sdk runner.
|
|
775
|
+
const usage = extractUsage(prompted.data?.info);
|
|
776
|
+
result = {
|
|
777
|
+
ok: true,
|
|
778
|
+
stdout,
|
|
779
|
+
stderr: "",
|
|
780
|
+
durationMs: Date.now() - start,
|
|
781
|
+
exitCode: 0,
|
|
782
|
+
sessionId,
|
|
783
|
+
...(usage ? { usage } : {}),
|
|
202
784
|
};
|
|
203
785
|
}
|
|
204
|
-
const parts = result.data?.parts ?? [];
|
|
205
|
-
const textPart = parts.find((p) => p.type === "text");
|
|
206
|
-
const stdout = textPart?.text ?? "";
|
|
207
|
-
return {
|
|
208
|
-
ok: true,
|
|
209
|
-
stdout,
|
|
210
|
-
stderr: "",
|
|
211
|
-
durationMs: Date.now() - start,
|
|
212
|
-
exitCode: 0,
|
|
213
|
-
};
|
|
214
786
|
}
|
|
215
|
-
catch (
|
|
216
|
-
|
|
787
|
+
catch (err) {
|
|
788
|
+
result = {
|
|
217
789
|
ok: false,
|
|
218
790
|
stdout: "",
|
|
219
|
-
stderr:
|
|
791
|
+
stderr: errorText(err),
|
|
220
792
|
durationMs: Date.now() - start,
|
|
221
793
|
exitCode: 1,
|
|
222
794
|
reason: "non_zero_exit",
|
|
223
|
-
error:
|
|
795
|
+
error: errorText(err),
|
|
796
|
+
sessionId,
|
|
224
797
|
};
|
|
225
798
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
799
|
+
// Clean up session to prevent disk accumulation in ~/.local/share/opencode/.
|
|
800
|
+
// Failures are non-fatal to the agent result but must not be invisible.
|
|
801
|
+
const cleanupWarning = await deleteSessionBestEffort(client, sessionId, query, setTimeoutImpl, clearTimeoutImpl);
|
|
802
|
+
if (cleanupWarning)
|
|
803
|
+
result.stderr = appendStderr(result.stderr, cleanupWarning);
|
|
804
|
+
return result;
|
|
232
805
|
}
|
|
233
|
-
/** @deprecated Use {@link runOpencodeSdk} instead. */
|
|
234
|
-
export const runAgentSdk = runOpencodeSdk;
|