akm-cli 0.9.0-rc.1 → 0.9.0-rc.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 +223 -9
- package/SECURITY.md +23 -24
- package/dist/assets/help/help-improve.md +10 -10
- package/dist/assets/hints/cli-hints-full.md +44 -10
- package/dist/assets/hints/cli-hints-short.md +6 -2
- package/dist/assets/{profiles → improve-strategies}/default.json +1 -0
- package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +1 -1
- package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +2 -3
- package/dist/assets/{profiles → improve-strategies}/reflect-distill.json +3 -4
- package/dist/assets/stash-skeleton/README.md +28 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +12 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +11 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +9 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +7 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +7 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +98 -0
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +63 -0
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +127 -0
- package/dist/assets/tasks/core/backup.yml +1 -0
- package/dist/assets/tasks/core/extract.yml +1 -0
- package/dist/assets/tasks/core/improve.yml +1 -0
- package/dist/assets/tasks/core/index-refresh.yml +1 -0
- package/dist/assets/tasks/core/sync.yml +1 -0
- package/dist/assets/tasks/core/version-check.yml +1 -0
- package/dist/assets/tasks/graph-refresh-weekly.yml +4 -4
- package/dist/assets/templates/html/health.html +5 -1
- package/dist/cli/config-migrate.js +31 -138
- package/dist/cli/config-validate.js +10 -8
- package/dist/cli.js +48 -14
- package/dist/commands/agent/agent-dispatch.js +17 -16
- package/dist/commands/agent/agent-support.js +0 -24
- package/dist/commands/agent/contribute-cli.js +5 -15
- package/dist/commands/backup-cli.js +54 -0
- package/dist/commands/config-cli.js +45 -159
- package/dist/commands/env/secret.js +8 -5
- package/dist/commands/health/checks.js +130 -83
- package/dist/commands/health/html-report.js +4 -0
- package/dist/commands/health/improve-metrics.js +30 -32
- package/dist/commands/health/llm-usage.js +19 -19
- package/dist/commands/health/md-report.js +4 -0
- package/dist/commands/health/metrics.js +2 -1
- package/dist/commands/health/surfaces.js +5 -4
- package/dist/commands/health.js +1 -1
- package/dist/commands/improve/consolidate/chunking.js +2 -2
- package/dist/commands/improve/consolidate.js +28 -25
- package/dist/commands/improve/distill/promote-memory.js +5 -12
- package/dist/commands/improve/distill/quality-gate.js +5 -7
- package/dist/commands/improve/distill.js +16 -5
- package/dist/commands/improve/eligibility.js +22 -12
- package/dist/commands/improve/extract-cli.js +47 -19
- package/dist/commands/improve/extract.js +110 -62
- package/dist/commands/improve/improve-cli.js +38 -16
- package/dist/commands/improve/improve-result-file.js +30 -24
- package/dist/commands/improve/improve-strategies.js +137 -0
- package/dist/commands/improve/improve.js +60 -30
- package/dist/commands/improve/locks.js +66 -45
- package/dist/commands/improve/loop-stages.js +75 -33
- package/dist/commands/improve/memory/memory-belief.js +79 -7
- package/dist/commands/improve/memory/memory-contradiction-detect.js +12 -4
- package/dist/commands/improve/preparation.js +71 -73
- package/dist/commands/improve/procedural.js +3 -2
- package/dist/commands/improve/recombine.js +2 -1
- package/dist/commands/improve/reflect.js +119 -214
- package/dist/commands/improve/shared.js +11 -5
- package/dist/commands/lint/base-linter.js +152 -42
- package/dist/commands/mv-cli.js +809 -0
- package/dist/commands/proposal/proposal-cli.js +18 -8
- package/dist/commands/proposal/propose.js +64 -69
- package/dist/commands/read/knowledge.js +436 -4
- package/dist/commands/read/remember-cli.js +39 -2
- package/dist/commands/read/search-cli.js +6 -1
- package/dist/commands/registry-cli.js +29 -14
- package/dist/commands/remember.js +2 -0
- package/dist/commands/sources/init.js +13 -14
- package/dist/commands/sources/schema-repair.js +2 -4
- package/dist/commands/sources/source-add.js +62 -73
- package/dist/commands/sources/source-manage.js +50 -46
- package/dist/commands/sources/stash-cli.js +41 -4
- package/dist/commands/tasks/default-tasks.js +12 -12
- package/dist/commands/tasks/tasks-cli.js +7 -3
- package/dist/commands/tasks/tasks.js +113 -18
- package/dist/commands/wiki-cli.js +9 -10
- package/dist/core/asset/frontmatter.js +12 -2
- package/dist/core/common.js +5 -3
- package/dist/core/config/config-io.js +28 -17
- package/dist/core/config/config-schema.js +358 -66
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config-version.js +29 -0
- package/dist/core/config/config-walker.js +98 -27
- package/dist/core/config/config.js +132 -266
- package/dist/core/config/deep-merge.js +41 -0
- package/dist/core/config/engine-semantics.js +32 -0
- package/dist/core/errors.js +2 -2
- package/dist/core/extra-params.js +61 -0
- package/dist/core/file-lock.js +201 -56
- package/dist/core/improve-result.js +178 -0
- package/dist/core/maintenance-barrier.js +119 -0
- package/dist/core/migration-backup.js +416 -0
- package/dist/core/paths.js +3 -0
- package/dist/core/redaction.js +358 -0
- package/dist/core/state/migrations.js +17 -2
- package/dist/core/state-db.js +44 -1
- package/dist/indexer/db/db.js +116 -1
- package/dist/indexer/graph/graph-extraction.js +28 -16
- package/dist/indexer/index-writer-lock.js +31 -24
- package/dist/indexer/index-written-assets.js +15 -6
- package/dist/indexer/indexer.js +47 -2
- package/dist/indexer/passes/memory-inference.js +10 -6
- package/dist/indexer/passes/metadata.js +250 -0
- package/dist/indexer/search/db-search.js +111 -44
- package/dist/indexer/search/fts-query.js +41 -0
- package/dist/indexer/search/ranking-contributors.js +48 -0
- package/dist/indexer/search/ranking.js +36 -23
- package/dist/indexer/search/search-fields.js +11 -1
- package/dist/integrations/agent/builder-shared.js +7 -0
- package/dist/integrations/agent/builders.js +5 -52
- package/dist/integrations/agent/config.js +3 -143
- package/dist/integrations/agent/detect.js +17 -2
- package/dist/integrations/agent/engine-resolution.js +202 -0
- package/dist/integrations/agent/index.js +1 -2
- package/dist/integrations/agent/model-aliases.js +7 -2
- package/dist/integrations/agent/profiles.js +6 -99
- package/dist/integrations/agent/runner-dispatch.js +76 -13
- package/dist/integrations/agent/runner.js +76 -207
- package/dist/integrations/agent/spawn.js +4 -6
- package/dist/integrations/harnesses/aider/agent-builder.js +2 -3
- package/dist/integrations/harnesses/aider/index.js +0 -1
- package/dist/integrations/harnesses/amazonq/agent-builder.js +2 -3
- package/dist/integrations/harnesses/amazonq/index.js +0 -1
- package/dist/integrations/harnesses/claude/agent-builder.js +2 -3
- package/dist/integrations/harnesses/claude/index.js +0 -2
- package/dist/integrations/harnesses/codex/agent-builder.js +2 -3
- package/dist/integrations/harnesses/codex/index.js +0 -1
- package/dist/integrations/harnesses/copilot/agent-builder.js +2 -3
- package/dist/integrations/harnesses/copilot/index.js +0 -1
- package/dist/integrations/harnesses/gemini/agent-builder.js +2 -3
- package/dist/integrations/harnesses/gemini/index.js +0 -1
- package/dist/integrations/harnesses/index.js +1 -24
- package/dist/integrations/harnesses/opencode/agent-builder.js +2 -3
- package/dist/integrations/harnesses/opencode/index.js +0 -6
- package/dist/integrations/harnesses/opencode-sdk/harness.js +1 -6
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +274 -191
- package/dist/integrations/harnesses/openhands/agent-builder.js +2 -3
- package/dist/integrations/harnesses/openhands/index.js +0 -1
- package/dist/integrations/harnesses/pi/agent-builder.js +2 -3
- package/dist/integrations/harnesses/pi/index.js +0 -1
- package/dist/integrations/harnesses/types.js +1 -32
- package/dist/integrations/lockfile.js +32 -21
- package/dist/llm/client.js +48 -14
- package/dist/llm/feature-gate.js +15 -47
- package/dist/llm/graph-extract.js +1 -1
- package/dist/llm/index-passes.js +8 -42
- package/dist/llm/memory-infer-impl.js +1 -1
- package/dist/llm/usage-persist.js +4 -0
- package/dist/llm/usage-telemetry.js +35 -5
- package/dist/output/shapes/helpers.js +2 -1
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/helpers.js +3 -1
- package/dist/schemas/akm-config.json +11013 -8600
- package/dist/schemas/akm-task.json +87 -0
- package/dist/schemas/akm-workflow.json +57 -13
- package/dist/scripts/migrate-storage.js +8591 -509
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +8623 -484
- package/dist/setup/detected-engines.js +142 -0
- package/dist/setup/engine-config.js +89 -0
- package/dist/setup/setup.js +236 -132
- package/dist/setup/steps/connection.js +61 -32
- package/dist/setup/steps/platforms.js +4 -4
- package/dist/setup/steps.js +3 -2
- package/dist/storage/database.js +13 -1
- package/dist/storage/engines/sqlite-migrations.js +1 -0
- package/dist/storage/repositories/improve-runs-repository.js +5 -5
- package/dist/storage/repositories/task-history-repository.js +78 -0
- package/dist/storage/repositories/workflow-runs-repository.js +9 -8
- package/dist/tasks/parser.js +138 -52
- package/dist/tasks/runner.js +71 -75
- package/dist/tasks/schema.js +1 -1
- package/dist/tasks/validator.js +11 -6
- package/dist/wiki/wiki.js +9 -8
- package/dist/workflows/authoring/workflow-program-template.yaml +3 -3
- package/dist/workflows/concurrency-policy.js +15 -0
- package/dist/workflows/db.js +65 -13
- package/dist/workflows/exec/brief.js +18 -24
- package/dist/workflows/exec/frozen-judge.js +47 -0
- package/dist/workflows/exec/native-executor.js +124 -65
- package/dist/workflows/exec/report.js +93 -33
- package/dist/workflows/exec/run-workflow.js +38 -25
- package/dist/workflows/exec/scheduler.js +12 -41
- package/dist/workflows/exec/step-work.js +91 -35
- package/dist/workflows/ir/compile.js +13 -26
- package/dist/workflows/ir/freeze.js +243 -0
- package/dist/workflows/ir/plan-hash.js +40 -5
- package/dist/workflows/ir/schema.js +542 -1
- package/dist/workflows/parser.js +7 -0
- package/dist/workflows/program/parser.js +132 -23
- package/dist/workflows/program/project.js +3 -4
- package/dist/workflows/program/schema.js +2 -2
- package/dist/workflows/resource-limits.js +20 -0
- package/dist/workflows/runtime/plan-classifier.js +187 -0
- package/dist/workflows/runtime/runs.js +54 -47
- package/dist/workflows/runtime/workflow-asset-loader.js +0 -22
- package/dist/workflows/validator.js +25 -0
- package/docs/data-and-telemetry.md +2 -2
- package/docs/migration/release-notes/0.6.0.md +1 -1
- package/docs/migration/release-notes/0.7.0.md +5 -4
- package/docs/migration/v0.8-to-v0.9.md +401 -0
- package/package.json +4 -2
- package/schemas/akm-config.json +16638 -0
- package/schemas/akm-task.json +87 -0
- package/schemas/akm-workflow.json +372 -0
- package/dist/commands/improve/improve-profiles.js +0 -168
- package/dist/core/config/config-migration.js +0 -602
- package/dist/core/deep-merge.js +0 -38
- package/dist/llm/call-ai.js +0 -62
- package/dist/setup/legacy-config.js +0 -106
- package/docs/README.md +0 -104
- /package/dist/assets/{profiles → improve-strategies}/catchup.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/consolidate.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/frequent.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/memory-focus.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/quick.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/recombine-only.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/synthesize.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/thorough.json +0 -0
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Uses the embedded `@opencode-ai/sdk` instead of `Bun.spawn`. Requires no
|
|
8
8
|
* agent CLI binary to be installed. The user provides an OpenAI-compatible
|
|
9
|
-
* endpoint (or inherits from
|
|
9
|
+
* endpoint (or inherits from the selected fallback LLM engine) for the SDK.
|
|
10
10
|
*
|
|
11
11
|
* This is the runtime surface of the {@link OpencodeSdkHarness} (`id =
|
|
12
|
-
* 'opencode-sdk'`). It is the dispatch path for
|
|
12
|
+
* 'opencode-sdk'`). It is the dispatch path for SDK runner specs; it exposes
|
|
13
13
|
* no native session logs of its own (`capabilities.sessionLogs = false`).
|
|
14
14
|
*
|
|
15
15
|
* ## Per-call cwd and env (redesign addendum R2, open seam decision 1)
|
|
@@ -29,19 +29,12 @@
|
|
|
29
29
|
*
|
|
30
30
|
* - **env is PER-SERVER (keyed registry).** The SDK exposes NO per-call or
|
|
31
31
|
* per-session env surface; the only way env reaches tool child processes
|
|
32
|
-
* is the `opencode serve` process environment
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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).
|
|
32
|
+
* is the `opencode serve` process environment. {@link getOrStartServer}
|
|
33
|
+
* snapshots and materializes the complete child environment for each
|
|
34
|
+
* dispatch, passes it directly to the managed spawn (never mutating
|
|
35
|
+
* `process.env`), and hashes that exact canonical environment for registry
|
|
36
|
+
* identity. Identical child material shares one server; any material
|
|
37
|
+
* difference receives a separate server.
|
|
45
38
|
*
|
|
46
39
|
* This is what removed the workflow engine's `env_unsupported` hard-fail for
|
|
47
40
|
* the sdk runner: injection genuinely reaches the child, because tool
|
|
@@ -50,9 +43,9 @@
|
|
|
50
43
|
* Registry hygiene (peer-review fixes):
|
|
51
44
|
*
|
|
52
45
|
* - **Ports.** `createOpencodeServer` binds a FIXED default port (4096),
|
|
53
|
-
* so coexisting registry entries would contend for the same bind.
|
|
54
|
-
*
|
|
55
|
-
*
|
|
46
|
+
* so coexisting registry entries would contend for the same bind. Every
|
|
47
|
+
* entry is started on its own reserved OS-assigned port (see
|
|
48
|
+
* {@link startServer}).
|
|
56
49
|
* - **Shutdown.** {@link closeServer} closes resolved servers
|
|
57
50
|
* SYNCHRONOUSLY — it runs from `process.once('exit')`, where Bun never
|
|
58
51
|
* drains microtasks, so a `.then()`-based close would orphan every
|
|
@@ -64,7 +57,8 @@
|
|
|
64
57
|
* cannot free a process the child is keeping alive (a deadlock that hangs the
|
|
65
58
|
* caller after an otherwise-successful run). The registry is therefore drained
|
|
66
59
|
* PROACTIVELY at the end of a dispatching command: the workflow engine calls
|
|
67
|
-
* `disposeDispatchResources()` (→ {@link closeServer}) in
|
|
60
|
+
* `disposeDispatchResources()` (→ {@link closeServer}) in composition-root and
|
|
61
|
+
* workflow `finally` blocks.
|
|
68
62
|
* The `process.once('exit')` hook stays as the last-resort backstop for paths
|
|
69
63
|
* that never reach that drain.
|
|
70
64
|
*
|
|
@@ -84,17 +78,16 @@
|
|
|
84
78
|
* ({@link SERVER_KILL_GRACE_MS}) that escalates to SIGKILL and is cleared
|
|
85
79
|
* on cooperative exit, so stubborn children cannot survive parent exit and
|
|
86
80
|
* stale timers cannot signal a reused PID;
|
|
87
|
-
* - the spawn
|
|
88
|
-
*
|
|
81
|
+
* - the spawn receives the immutable per-dispatch environment directly, so
|
|
82
|
+
* asynchronous factory setup cannot race a temporary process-wide overlay.
|
|
89
83
|
*/
|
|
90
84
|
import { spawn } from "node:child_process";
|
|
91
85
|
import { createHash } from "node:crypto";
|
|
92
|
-
import { resolveSecret } from "../../../core/config/config.js";
|
|
93
86
|
import { DEFAULT_AGENT_TIMEOUT_MS } from "../../agent/config.js";
|
|
94
87
|
import { resolveModel } from "../../agent/model-aliases.js";
|
|
95
|
-
// Server registry — one server per
|
|
96
|
-
//
|
|
97
|
-
//
|
|
88
|
+
// Server registry — one server per complete server-material signature. Caller
|
|
89
|
+
// deadlines race the shared promise independently; they never become startup
|
|
90
|
+
// configuration inherited by later callers.
|
|
98
91
|
const _servers = new Map();
|
|
99
92
|
// Resolved servers by registry key, mirrored from `_servers` as each start
|
|
100
93
|
// promise settles. This exists so closeServer() can close started servers
|
|
@@ -106,6 +99,8 @@ const _resolvedServers = new Map();
|
|
|
106
99
|
// tracked so two coexisting servers in this process can never be assigned
|
|
107
100
|
// the same port.
|
|
108
101
|
const _serverPorts = new Map();
|
|
102
|
+
// Serializes probe-and-reserve so concurrent starts cannot claim one port.
|
|
103
|
+
let _portAllocationTail = Promise.resolve();
|
|
109
104
|
/** The port `createOpencodeServer` binds when none is passed (SDK 1.2.20). */
|
|
110
105
|
const DEFAULT_SDK_PORT = 4096;
|
|
111
106
|
// Test override: when set, every call uses this server (all keys) and no real
|
|
@@ -127,11 +122,9 @@ export function __setTestServer(server) {
|
|
|
127
122
|
/**
|
|
128
123
|
* Test-only seam: replace the `createOpencode` factory so the env-keyed
|
|
129
124
|
* server registry (module doc, *Per-call cwd and env*) can be exercised
|
|
130
|
-
* without the real SDK. The
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* await — because the runner restores the env overlay as soon as the factory
|
|
134
|
-
* call returns its promise. Pass `null` to clear.
|
|
125
|
+
* without the real SDK. The complete child environment is supplied as
|
|
126
|
+
* `options.env`, so async factories observe the same immutable values as the
|
|
127
|
+
* production spawn. Pass `null` to clear.
|
|
135
128
|
*/
|
|
136
129
|
export function __setServerFactory(factory) {
|
|
137
130
|
_serverFactory = factory;
|
|
@@ -145,44 +138,38 @@ export function __setServerFactory(factory) {
|
|
|
145
138
|
* `opencode serve` children (leaking processes AND keeping their ports
|
|
146
139
|
* bound for the next invocation).
|
|
147
140
|
*/
|
|
148
|
-
export function closeServer() {
|
|
149
|
-
|
|
141
|
+
export async function closeServer() {
|
|
142
|
+
const closes = [];
|
|
143
|
+
for (const [key, entry] of _servers) {
|
|
150
144
|
const resolved = _resolvedServers.get(key);
|
|
151
145
|
if (resolved) {
|
|
152
146
|
// Synchronous close — safe from the 'exit' hook.
|
|
153
147
|
try {
|
|
154
|
-
resolved.server.close();
|
|
148
|
+
closes.push(Promise.resolve(resolved.server.close()));
|
|
155
149
|
}
|
|
156
150
|
catch {
|
|
157
151
|
/* ignore */
|
|
158
152
|
}
|
|
159
153
|
}
|
|
160
154
|
else {
|
|
161
|
-
// Still starting:
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
.then((s) => {
|
|
166
|
-
try {
|
|
167
|
-
s.server.close();
|
|
168
|
-
}
|
|
169
|
-
catch {
|
|
170
|
-
/* ignore */
|
|
171
|
-
}
|
|
172
|
-
})
|
|
173
|
-
.catch(() => { });
|
|
155
|
+
// Still starting: cancel the real managed spawn immediately. A custom
|
|
156
|
+
// factory may ignore the signal; the registry settlement handler closes
|
|
157
|
+
// any late result without awaiting it and pinning shutdown indefinitely.
|
|
158
|
+
entry.controller.abort();
|
|
174
159
|
}
|
|
175
160
|
}
|
|
176
161
|
_servers.clear();
|
|
177
162
|
_resolvedServers.clear();
|
|
178
163
|
_serverPorts.clear();
|
|
179
164
|
try {
|
|
180
|
-
_testServer
|
|
165
|
+
if (_testServer)
|
|
166
|
+
closes.push(Promise.resolve(_testServer.server.close()));
|
|
181
167
|
}
|
|
182
168
|
catch {
|
|
183
169
|
/* ignore */
|
|
184
170
|
}
|
|
185
171
|
_testServer = null;
|
|
172
|
+
await Promise.allSettled(closes);
|
|
186
173
|
}
|
|
187
174
|
/**
|
|
188
175
|
* Convert an `AgentDispatchRequest.tools` policy into the SDK's tool-allowlist
|
|
@@ -241,12 +228,14 @@ function toolsToSdkAllowlist(tools) {
|
|
|
241
228
|
* and config-root alias tables apply here.
|
|
242
229
|
*/
|
|
243
230
|
export function buildSdkConfig(profile, llmConfig) {
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
231
|
+
const endpoint = llmConfig?.endpoint;
|
|
232
|
+
const apiKey = llmConfig?.apiKey;
|
|
233
|
+
const profileModel = profile.model
|
|
234
|
+
? profile.modelIsExact
|
|
235
|
+
? profile.model
|
|
236
|
+
: resolveModel(profile.model, "opencode-sdk", profile.modelAliases, profile.globalModelAliases)
|
|
249
237
|
: undefined;
|
|
238
|
+
const model = profileModel ?? llmConfig?.model;
|
|
250
239
|
const sdkConfig = {};
|
|
251
240
|
if (model)
|
|
252
241
|
sdkConfig.model = model;
|
|
@@ -256,48 +245,74 @@ export function buildSdkConfig(profile, llmConfig) {
|
|
|
256
245
|
"akm-custom": {
|
|
257
246
|
npm: "@ai-sdk/openai-compatible",
|
|
258
247
|
options: {
|
|
259
|
-
baseURL: endpoint
|
|
248
|
+
baseURL: canonicalProviderBase(endpoint) ?? undefined,
|
|
260
249
|
...(apiKey ? { apiKey } : {}),
|
|
261
250
|
},
|
|
262
251
|
},
|
|
263
252
|
};
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
253
|
+
// The first path segment selects the OpenCode provider. Model IDs may
|
|
254
|
+
// themselves contain slashes, but still belong to this custom endpoint.
|
|
255
|
+
if (model)
|
|
256
|
+
sdkConfig.model = model.startsWith("akm-custom/") ? model : `akm-custom/${model}`;
|
|
268
257
|
}
|
|
269
258
|
return sdkConfig;
|
|
270
259
|
}
|
|
271
|
-
/**
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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");
|
|
260
|
+
/** Digest the executable and exact environment received by the child. */
|
|
261
|
+
function serverRegistryKey(profile, env) {
|
|
262
|
+
const material = { bin: profile.bin, env };
|
|
263
|
+
return createHash("sha256")
|
|
264
|
+
.update(JSON.stringify(canonicalize(material)))
|
|
265
|
+
.digest("hex");
|
|
280
266
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
267
|
+
function buildServerEnv(profile, config, bindings, envSource) {
|
|
268
|
+
const env = {};
|
|
269
|
+
const inheritedNames = new Set([
|
|
270
|
+
"HOME",
|
|
271
|
+
"PATH",
|
|
272
|
+
"USER",
|
|
273
|
+
"LANG",
|
|
274
|
+
"LC_ALL",
|
|
275
|
+
"TERM",
|
|
276
|
+
"TMPDIR",
|
|
277
|
+
"SYSTEMROOT",
|
|
278
|
+
"COMSPEC",
|
|
279
|
+
"PATHEXT",
|
|
280
|
+
"WINDIR",
|
|
281
|
+
"TEMP",
|
|
282
|
+
"TMP",
|
|
283
|
+
...(profile.envPassthrough ?? []),
|
|
284
|
+
]);
|
|
285
|
+
for (const key of inheritedNames) {
|
|
286
|
+
const value = envSource[key];
|
|
287
|
+
if (value !== undefined)
|
|
288
|
+
env[key] = value;
|
|
292
289
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
290
|
+
for (const [key, value] of Object.entries(bindings ?? {}))
|
|
291
|
+
env[key] = value;
|
|
292
|
+
env.OPENCODE_CONFIG_CONTENT = JSON.stringify(config);
|
|
293
|
+
return env;
|
|
294
|
+
}
|
|
295
|
+
function canonicalProviderBase(endpoint) {
|
|
296
|
+
if (!endpoint)
|
|
297
|
+
return null;
|
|
298
|
+
try {
|
|
299
|
+
const url = new URL(endpoint);
|
|
300
|
+
url.pathname = url.pathname.replace(/\/chat\/completions$/, "").replace(/\/$/, "");
|
|
301
|
+
return url.toString().replace(/\/$/, "");
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return endpoint.replace(/\/chat\/completions$/, "").replace(/\/$/, "");
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function canonicalize(value) {
|
|
308
|
+
if (Array.isArray(value))
|
|
309
|
+
return value.map(canonicalize);
|
|
310
|
+
if (value && typeof value === "object") {
|
|
311
|
+
return Object.fromEntries(Object.entries(value)
|
|
312
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
313
|
+
.map(([key, child]) => [key, canonicalize(child)]));
|
|
314
|
+
}
|
|
315
|
+
return value;
|
|
301
316
|
}
|
|
302
317
|
/**
|
|
303
318
|
* Ask the OS for a currently-free localhost port (bind :0, read the assigned
|
|
@@ -307,33 +322,44 @@ function overlayProcessEnv(env) {
|
|
|
307
322
|
* acceptable here because the failure mode is a clean `spawn_failed` on the
|
|
308
323
|
* next dispatch, not corruption.
|
|
309
324
|
*/
|
|
310
|
-
async function allocateFreePort() {
|
|
311
|
-
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
+
async function allocateFreePort(registryKey) {
|
|
326
|
+
let release;
|
|
327
|
+
const previous = _portAllocationTail;
|
|
328
|
+
_portAllocationTail = new Promise((resolve) => {
|
|
329
|
+
release = resolve;
|
|
330
|
+
});
|
|
331
|
+
await previous;
|
|
332
|
+
try {
|
|
333
|
+
const { createServer } = await import("node:net");
|
|
334
|
+
const taken = new Set(_serverPorts.values());
|
|
335
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
336
|
+
const port = await new Promise((resolve, reject) => {
|
|
337
|
+
const probe = createServer();
|
|
338
|
+
probe.unref();
|
|
339
|
+
probe.on("error", reject);
|
|
340
|
+
probe.listen(0, "127.0.0.1", () => {
|
|
341
|
+
const address = probe.address();
|
|
342
|
+
probe.close(() => {
|
|
343
|
+
if (address && typeof address === "object")
|
|
344
|
+
resolve(address.port);
|
|
345
|
+
else
|
|
346
|
+
reject(new Error("could not read the probe socket's port"));
|
|
347
|
+
});
|
|
325
348
|
});
|
|
326
349
|
});
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
350
|
+
if (port !== DEFAULT_SDK_PORT && !taken.has(port)) {
|
|
351
|
+
_serverPorts.set(registryKey, port);
|
|
352
|
+
return port;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
throw new Error("could not allocate a free port for the OpenCode SDK server");
|
|
356
|
+
}
|
|
357
|
+
finally {
|
|
358
|
+
release();
|
|
330
359
|
}
|
|
331
|
-
throw new Error("could not allocate a free port for the OpenCode SDK server");
|
|
332
360
|
}
|
|
333
361
|
/** Grace between SIGTERM and SIGKILL when closing a managed server child. */
|
|
334
362
|
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
363
|
// Test seam: override the argv used to spawn the server child ("opencode"
|
|
338
364
|
// plus serve flags by default) so the managed-spawn lifecycle (handshake,
|
|
339
365
|
// unref, SIGTERM→SIGKILL escalation) is testable without the real binary.
|
|
@@ -345,7 +371,7 @@ export function __setServeCommand(argv) {
|
|
|
345
371
|
/**
|
|
346
372
|
* Spawn-owning replacement for the SDK's `createOpencode` (module doc,
|
|
347
373
|
* *Managed server spawn*). Mirrors `createOpencodeServer`'s contract — the
|
|
348
|
-
* `spawn`
|
|
374
|
+
* `spawn` receives the factory's immutable environment directly,
|
|
349
375
|
* `OPENCODE_CONFIG_CONTENT` carries the config, the handshake parses the
|
|
350
376
|
* "opencode server listening on <url>" line — but manages the child so its
|
|
351
377
|
* handle can never pin akm's event loop:
|
|
@@ -359,11 +385,9 @@ async function createManagedOpencode(options) {
|
|
|
359
385
|
throw new Error("OpenCode SDK not available. Install @opencode-ai/sdk or configure a CLI agent instead.");
|
|
360
386
|
}));
|
|
361
387
|
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.
|
|
388
|
+
const argv = _serveCommand ?? [options.bin ?? "opencode", "serve", "--hostname=127.0.0.1", `--port=${port}`];
|
|
365
389
|
const proc = spawn(argv[0], argv.slice(1), {
|
|
366
|
-
env:
|
|
390
|
+
env: options.env,
|
|
367
391
|
stdio: ["ignore", "pipe", "pipe"],
|
|
368
392
|
});
|
|
369
393
|
let closeStarted = false;
|
|
@@ -405,17 +429,13 @@ async function createManagedOpencode(options) {
|
|
|
405
429
|
};
|
|
406
430
|
const url = await new Promise((resolve, reject) => {
|
|
407
431
|
let output = "";
|
|
408
|
-
let timer;
|
|
409
432
|
let settled = false;
|
|
410
433
|
const cleanupStartup = () => {
|
|
411
|
-
if (timer !== undefined) {
|
|
412
|
-
clearTimeout(timer);
|
|
413
|
-
timer = undefined;
|
|
414
|
-
}
|
|
415
434
|
proc.stdout?.off("data", onStdoutData);
|
|
416
435
|
proc.stderr?.off("data", onStderrData);
|
|
417
436
|
proc.off("exit", onExit);
|
|
418
437
|
proc.off("error", onError);
|
|
438
|
+
options.startupSignal.removeEventListener("abort", onAbort);
|
|
419
439
|
};
|
|
420
440
|
const fail = (err) => {
|
|
421
441
|
if (settled)
|
|
@@ -459,13 +479,15 @@ async function createManagedOpencode(options) {
|
|
|
459
479
|
const onError = (err) => {
|
|
460
480
|
fail(err instanceof Error ? err : new Error(String(err)));
|
|
461
481
|
};
|
|
462
|
-
|
|
463
|
-
fail(new Error(`Timeout waiting for the OpenCode server to start after ${SERVER_START_TIMEOUT_MS}ms`));
|
|
464
|
-
}, SERVER_START_TIMEOUT_MS);
|
|
482
|
+
const onAbort = () => fail(new Error("OpenCode server startup cancelled because no callers are waiting"));
|
|
465
483
|
proc.stdout?.on("data", onStdoutData);
|
|
466
484
|
proc.stderr?.on("data", onStderrData);
|
|
467
485
|
proc.on("exit", onExit);
|
|
468
486
|
proc.on("error", onError);
|
|
487
|
+
if (options.startupSignal.aborted)
|
|
488
|
+
onAbort();
|
|
489
|
+
else
|
|
490
|
+
options.startupSignal.addEventListener("abort", onAbort, { once: true });
|
|
469
491
|
});
|
|
470
492
|
// Handshake done: from here on the child must never hold akm open. Its
|
|
471
493
|
// lifetime is managed explicitly (closeServer → closeManaged), not by the
|
|
@@ -475,77 +497,83 @@ async function createManagedOpencode(options) {
|
|
|
475
497
|
proc.unref();
|
|
476
498
|
return { client: createOpencodeClient({ baseUrl: url }), server: { close: closeManaged } };
|
|
477
499
|
}
|
|
478
|
-
async function startServer(profile,
|
|
500
|
+
async function startServer(profile, sdkConfig, env, registryKey, startupSignal) {
|
|
479
501
|
const factory = _serverFactory ?? createManagedOpencode;
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
//
|
|
487
|
-
//
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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;
|
|
502
|
+
const options = {
|
|
503
|
+
bin: profile.bin,
|
|
504
|
+
...(Object.keys(sdkConfig).length > 0 ? { config: sdkConfig } : {}),
|
|
505
|
+
env,
|
|
506
|
+
startupSignal,
|
|
507
|
+
};
|
|
508
|
+
// Every cached server receives a separately reserved port. This avoids both
|
|
509
|
+
// inter-entry contention and collisions with an unrelated process on 4096.
|
|
510
|
+
options.port = await allocateFreePort(registryKey);
|
|
511
|
+
if (startupSignal.aborted)
|
|
512
|
+
throw new Error("OpenCode server startup cancelled because no callers are waiting");
|
|
513
|
+
const server = await factory(options);
|
|
511
514
|
if (!server)
|
|
512
515
|
throw new Error("Failed to initialise OpenCode SDK server.");
|
|
513
516
|
if (!_exitHookInstalled) {
|
|
514
517
|
_exitHookInstalled = true;
|
|
515
518
|
process.once("exit", () => {
|
|
516
|
-
closeServer();
|
|
519
|
+
void closeServer();
|
|
517
520
|
});
|
|
518
521
|
}
|
|
519
522
|
return server;
|
|
520
523
|
}
|
|
521
524
|
/**
|
|
522
|
-
* Get (or lazily start) the server for this call's
|
|
523
|
-
*
|
|
525
|
+
* Get (or lazily start) the server for this call's complete server material.
|
|
526
|
+
* Concurrent callers of the same key share one
|
|
524
527
|
* start (the registry stores the in-flight promise). A failed start is
|
|
525
528
|
* evicted so the next call can retry instead of caching the error forever.
|
|
526
529
|
*/
|
|
527
|
-
|
|
530
|
+
function getOrStartServer(profile, llmConfig, env, envSource = process.env) {
|
|
528
531
|
if (_testServer)
|
|
529
|
-
return _testServer;
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
532
|
+
return { promise: Promise.resolve(_testServer), release() { } };
|
|
533
|
+
const sdkConfig = buildSdkConfig(profile, llmConfig);
|
|
534
|
+
const serverEnv = buildServerEnv(profile, sdkConfig, env, envSource);
|
|
535
|
+
const key = serverRegistryKey(profile, serverEnv);
|
|
536
|
+
let entry = _servers.get(key);
|
|
537
|
+
if (!entry) {
|
|
538
|
+
const controller = new AbortController();
|
|
539
|
+
entry = {
|
|
540
|
+
promise: startServer(profile, sdkConfig, serverEnv, key, controller.signal),
|
|
541
|
+
controller,
|
|
542
|
+
waiters: 0,
|
|
543
|
+
};
|
|
544
|
+
const started = entry;
|
|
545
|
+
_servers.set(key, started);
|
|
546
|
+
started.promise.then((server) => {
|
|
547
|
+
if (_servers.get(key) === started) {
|
|
548
|
+
started.server = server;
|
|
540
549
|
_resolvedServers.set(key, server);
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
void server.server.close();
|
|
553
|
+
}
|
|
541
554
|
}, () => {
|
|
542
|
-
if (_servers.get(key) ===
|
|
555
|
+
if (_servers.get(key) === started) {
|
|
543
556
|
_servers.delete(key);
|
|
544
557
|
_serverPorts.delete(key);
|
|
545
558
|
}
|
|
546
559
|
});
|
|
547
560
|
}
|
|
548
|
-
|
|
561
|
+
entry.waiters++;
|
|
562
|
+
let released = false;
|
|
563
|
+
return {
|
|
564
|
+
promise: entry.promise,
|
|
565
|
+
release() {
|
|
566
|
+
if (released)
|
|
567
|
+
return;
|
|
568
|
+
released = true;
|
|
569
|
+
entry.waiters--;
|
|
570
|
+
if (entry.waiters === 0 && !entry.server && _servers.get(key) === entry) {
|
|
571
|
+
_servers.delete(key);
|
|
572
|
+
_serverPorts.delete(key);
|
|
573
|
+
entry.controller.abort();
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
};
|
|
549
577
|
}
|
|
550
578
|
/**
|
|
551
579
|
* Extract best-effort token usage from a prompt response. Only numeric
|
|
@@ -572,10 +600,20 @@ const SDK_SESSION_DELETE_TIMEOUT_MS = 5_000;
|
|
|
572
600
|
async function raceSdkOperation(operation, opts) {
|
|
573
601
|
let timer;
|
|
574
602
|
let onAbort;
|
|
603
|
+
let raceFinished = false;
|
|
575
604
|
const racers = [operation];
|
|
605
|
+
void operation.then((value) => {
|
|
606
|
+
if (raceFinished && opts.onLateSettle)
|
|
607
|
+
void Promise.resolve(opts.onLateSettle({ status: "fulfilled", value })).catch(() => { });
|
|
608
|
+
}, (reason) => {
|
|
609
|
+
if (raceFinished && opts.onLateSettle)
|
|
610
|
+
void Promise.resolve(opts.onLateSettle({ status: "rejected", reason })).catch(() => { });
|
|
611
|
+
});
|
|
576
612
|
if (opts.timeoutMs !== null) {
|
|
577
613
|
racers.push(new Promise((resolve) => {
|
|
578
614
|
timer = opts.setTimeoutFn(() => resolve(SDK_OPERATION_TIMED_OUT), opts.timeoutMs ?? 0);
|
|
615
|
+
if (typeof timer !== "number")
|
|
616
|
+
timer.unref?.();
|
|
579
617
|
}));
|
|
580
618
|
}
|
|
581
619
|
if (opts.signal) {
|
|
@@ -591,6 +629,7 @@ async function raceSdkOperation(operation, opts) {
|
|
|
591
629
|
return racers.length === 1 ? await operation : await Promise.race(racers);
|
|
592
630
|
}
|
|
593
631
|
finally {
|
|
632
|
+
raceFinished = true;
|
|
594
633
|
if (timer !== undefined)
|
|
595
634
|
opts.clearTimeoutFn(timer);
|
|
596
635
|
if (opts.signal && onAbort)
|
|
@@ -621,6 +660,11 @@ async function deleteSessionBestEffort(client, sessionId, query, setTimeoutFn, c
|
|
|
621
660
|
}
|
|
622
661
|
export async function runOpencodeSdk(profile, prompt, opts = {}, llmConfig) {
|
|
623
662
|
const start = Date.now();
|
|
663
|
+
const timeoutMs = opts.timeoutMs !== undefined ? opts.timeoutMs : DEFAULT_AGENT_TIMEOUT_MS;
|
|
664
|
+
const deadline = timeoutMs === null ? null : start + timeoutMs;
|
|
665
|
+
const remainingTimeoutMs = () => (deadline === null ? null : Math.max(0, deadline - Date.now()));
|
|
666
|
+
const setTimeoutImpl = opts.setTimeoutFn ?? setTimeout;
|
|
667
|
+
const clearTimeoutImpl = opts.clearTimeoutFn ?? clearTimeout;
|
|
624
668
|
if (opts.signal?.aborted) {
|
|
625
669
|
return {
|
|
626
670
|
ok: false,
|
|
@@ -633,32 +677,63 @@ export async function runOpencodeSdk(profile, prompt, opts = {}, llmConfig) {
|
|
|
633
677
|
};
|
|
634
678
|
}
|
|
635
679
|
let client;
|
|
636
|
-
|
|
637
|
-
|
|
680
|
+
if (_testServer) {
|
|
681
|
+
client = _testServer.client;
|
|
638
682
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
683
|
+
else {
|
|
684
|
+
const startupHandle = getOrStartServer(profile, llmConfig, opts.env, opts.envSource);
|
|
685
|
+
try {
|
|
686
|
+
const startup = await raceSdkOperation(startupHandle.promise, {
|
|
687
|
+
timeoutMs: remainingTimeoutMs(),
|
|
688
|
+
setTimeoutFn: setTimeoutImpl,
|
|
689
|
+
clearTimeoutFn: clearTimeoutImpl,
|
|
690
|
+
signal: opts.signal,
|
|
691
|
+
});
|
|
692
|
+
if (startup === SDK_OPERATION_ABORTED) {
|
|
693
|
+
return {
|
|
694
|
+
ok: false,
|
|
695
|
+
stdout: "",
|
|
696
|
+
stderr: "",
|
|
697
|
+
durationMs: Date.now() - start,
|
|
698
|
+
exitCode: null,
|
|
699
|
+
reason: "aborted",
|
|
700
|
+
error: `opencode-sdk agent "${profile.name}" aborted by caller signal during server startup`,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
if (startup === SDK_OPERATION_TIMED_OUT) {
|
|
704
|
+
return {
|
|
705
|
+
ok: false,
|
|
706
|
+
stdout: "",
|
|
707
|
+
stderr: "",
|
|
708
|
+
durationMs: Date.now() - start,
|
|
709
|
+
exitCode: null,
|
|
710
|
+
reason: "timeout",
|
|
711
|
+
error: `opencode-sdk agent "${profile.name}" timed out during server startup after ${timeoutMs}ms`,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
client = startup.client;
|
|
715
|
+
}
|
|
716
|
+
catch (e) {
|
|
717
|
+
return {
|
|
718
|
+
ok: false,
|
|
719
|
+
stdout: "",
|
|
720
|
+
stderr: String(e),
|
|
721
|
+
durationMs: Date.now() - start,
|
|
722
|
+
exitCode: 1,
|
|
723
|
+
reason: "spawn_failed",
|
|
724
|
+
error: String(e),
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
finally {
|
|
728
|
+
startupHandle.release();
|
|
729
|
+
}
|
|
649
730
|
}
|
|
650
731
|
// #564 bug fix (3): enforce a hard timeout like the CLI path (runAgent).
|
|
651
732
|
// Previously runOpencodeSdk() awaited SDK calls with no timeout, so a stalled
|
|
652
733
|
// local-model endpoint or wedged server could block the caller indefinitely.
|
|
653
|
-
//
|
|
654
|
-
//
|
|
655
|
-
//
|
|
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;
|
|
734
|
+
// The same absolute deadline covers server startup, session creation, and
|
|
735
|
+
// prompting. null disables every dispatch timer. Session cleanup remains a
|
|
736
|
+
// separately bounded best-effort operation.
|
|
662
737
|
// Per-call working directory (module doc): forwarded as the SDK's
|
|
663
738
|
// `query.directory` on every session call, so worktree-isolated units run
|
|
664
739
|
// in their own checkout without a per-cwd server.
|
|
@@ -670,10 +745,15 @@ export async function runOpencodeSdk(profile, prompt, opts = {}, llmConfig) {
|
|
|
670
745
|
let sessionId;
|
|
671
746
|
try {
|
|
672
747
|
const created = await raceSdkOperation(client.session.create({ body: { title: "akm" }, ...(query ? { query } : {}) }), {
|
|
673
|
-
timeoutMs,
|
|
748
|
+
timeoutMs: remainingTimeoutMs(),
|
|
674
749
|
setTimeoutFn: setTimeoutImpl,
|
|
675
750
|
clearTimeoutFn: clearTimeoutImpl,
|
|
676
751
|
signal: abortSignal,
|
|
752
|
+
onLateSettle: (late) => {
|
|
753
|
+
if (late.status === "fulfilled" && late.value.data?.id) {
|
|
754
|
+
void deleteSessionBestEffort(client, late.value.data.id, query, setTimeoutImpl, clearTimeoutImpl);
|
|
755
|
+
}
|
|
756
|
+
},
|
|
677
757
|
});
|
|
678
758
|
if (created === SDK_OPERATION_ABORTED) {
|
|
679
759
|
return {
|
|
@@ -736,10 +816,13 @@ export async function runOpencodeSdk(profile, prompt, opts = {}, llmConfig) {
|
|
|
736
816
|
let result;
|
|
737
817
|
try {
|
|
738
818
|
const prompted = await raceSdkOperation(client.session.prompt({ path: { id: sessionId }, body, ...(query ? { query } : {}) }), {
|
|
739
|
-
timeoutMs,
|
|
819
|
+
timeoutMs: remainingTimeoutMs(),
|
|
740
820
|
setTimeoutFn: setTimeoutImpl,
|
|
741
821
|
clearTimeoutFn: clearTimeoutImpl,
|
|
742
822
|
signal: abortSignal,
|
|
823
|
+
onLateSettle: () => {
|
|
824
|
+
void deleteSessionBestEffort(client, sessionId, query, setTimeoutImpl, clearTimeoutImpl);
|
|
825
|
+
},
|
|
743
826
|
});
|
|
744
827
|
if (prompted === SDK_OPERATION_ABORTED) {
|
|
745
828
|
result = {
|