@cortexkit/aft-opencode 0.39.2 → 0.39.4

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.
@@ -1,206 +0,0 @@
1
- /**
2
- * Workaround helper for the OpenCode plugin promptAsync runner-split bug
3
- * (https://github.com/anomalyco/opencode/issues/28202).
4
- *
5
- * OpenCode's plugin-provided `input.client` is constructed with
6
- * `fetch: async (...args) => Server.Default().app.fetch(...args)`, which
7
- * routes requests through `HttpApiApp.webHandler()` and a SEPARATE Effect
8
- * `memoMap` from the one used by the live HTTP listener. Since
9
- * `SessionRunState` is a per-memo-map in-memory layer, plugin-origin
10
- * `promptAsync` calls observe an "idle" runner while the live UI turn is
11
- * still running. The result is that `ensureRunning` fails to coalesce and
12
- * OpenCode persists multiple assistant children under a single synthetic
13
- * user parent — what users see as duplicate "stop" messages after every
14
- * background-bash completion reminder.
15
- *
16
- * The workaround is to bypass `input.client` for the wake path and build
17
- * a separate `createOpencodeClient` configured to hit `input.serverUrl`
18
- * via `globalThis.fetch`. That client enters the same live listener the
19
- * UI uses, so the active session's `SessionRunState` is the one that
20
- * resolves `ensureRunning` and overlapping turns coalesce correctly.
21
- *
22
- * The workaround only works when the live HTTP listener is actually
23
- * reachable. OpenCode Desktop (Electron+Node) and TUI launched with
24
- * `opencode --port 0` bind a real API listener; plain TUI binds an
25
- * internal-only listener that 404s for `/session/*`. We probe once at
26
- * plugin init and cache the result by `serverUrl`. When that server is
27
- * unreachable, the wake path silently uses the in-process
28
- * `input.client.session.promptAsync`, which keeps wakes flowing (at the
29
- * cost of the upstream duplicate-runner bug) instead of producing no
30
- * notification at all or nagging the user to relaunch with a different
31
- * flag.
32
- *
33
- * Tracked upstream as anomalyco/opencode#28202. When OpenCode fixes the
34
- * runtime split, this helper and its single consumer in `bg-notifications.ts`
35
- * can be deleted and the wake path can go back to `input.client`.
36
- */
37
-
38
- import { createOpencodeClient } from "@opencode-ai/sdk";
39
-
40
- export type LiveServerClient = ReturnType<typeof createOpencodeClient>;
41
-
42
- /**
43
- * Cache key is `${serverUrl}|${directory}`. Both are stable per OpenCode
44
- * session/project pair, so one client is reused across many wakes. We don't
45
- * key on `serverUrl + auth header` because the auth env vars are server-wide
46
- * — if they change we'd want a fresh client anyway; in practice they're set
47
- * once at process start.
48
- */
49
- const clientCache = new Map<string, LiveServerClient>();
50
-
51
- function cacheKey(serverUrl: string, directory: string): string {
52
- return `${serverUrl}|${directory}`;
53
- }
54
-
55
- function normalizeServerUrl(serverUrl: string): string {
56
- try {
57
- return new URL(serverUrl).toString();
58
- } catch {
59
- return serverUrl;
60
- }
61
- }
62
-
63
- /**
64
- * Build the Basic-auth header OpenCode's server expects when
65
- * `OPENCODE_SERVER_PASSWORD` is set. Read at call time (not at module load)
66
- * so test setup can mutate `process.env` between cases.
67
- */
68
- function serverAuthHeaders(): Record<string, string> | undefined {
69
- const password = process.env.OPENCODE_SERVER_PASSWORD;
70
- if (!password) return undefined;
71
- const username = process.env.OPENCODE_SERVER_USERNAME ?? "opencode";
72
- return {
73
- Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`,
74
- };
75
- }
76
-
77
- /**
78
- * Return a cached `createOpencodeClient` pointed at the live HTTP listener
79
- * for the given `(serverUrl, directory)` pair. One client object is reused
80
- * across many wakes for a given session.
81
- *
82
- * The `fetch` is bound to `globalThis.fetch` explicitly. Without this, the
83
- * SDK would fall back to `globalThis.fetch` anyway in normal Node runtimes,
84
- * but we set it on purpose so anyone reading this code (or grepping for the
85
- * bug fix) can see that we intentionally chose the live HTTP transport.
86
- */
87
- export function getLiveServerClient(serverUrl: string, directory: string): LiveServerClient {
88
- const key = cacheKey(serverUrl, directory);
89
- const cached = clientCache.get(key);
90
- if (cached) return cached;
91
- const client = createOpencodeClient({
92
- baseUrl: serverUrl,
93
- directory,
94
- headers: serverAuthHeaders(),
95
- fetch: globalThis.fetch,
96
- });
97
- clientCache.set(key, client);
98
- return client;
99
- }
100
-
101
- /** Test helper — drop the cache between cases so each test starts clean. */
102
- export function __resetLiveServerClientCacheForTests(): void {
103
- clientCache.clear();
104
- }
105
-
106
- /**
107
- * Per-server decision: should bg-notifications use the live-server wake
108
- * transport (workaround for anomalyco/opencode#28202), or fall back to the
109
- * in-process `input.client.session.promptAsync` path?
110
- *
111
- * Keying by `serverUrl` matters because one plugin process can host
112
- * multiple OpenCode windows with different live listener URLs. A missing
113
- * keyed decision defaults to `false` for safety: the in-process client is
114
- * part of the plugin contract, while the live-server path requires a
115
- * probe-confirmed listener.
116
- *
117
- * `legacyLiveServerWakeAvailable` is retained only for older callers/tests
118
- * that do not pass a `serverUrl`; keyed wake callers never read it.
119
- */
120
- const liveServerWakeAvailableByServerUrl = new Map<string, boolean>();
121
- let legacyLiveServerWakeAvailable = false;
122
-
123
- /**
124
- * Probe whether `serverUrl` serves OpenCode's HTTP API within `timeoutMs`.
125
- * Returns `true` only when `/session` proves the API is usable: any 2xx
126
- * response is reachable, and 401/403 also count as reachable because an
127
- * auth-protected listener still exists. Returns `false` for 404 (plain
128
- * TUI's internal listener), 5xx, connection refused, DNS failure, timeout,
129
- * malformed URL, or undefined URL.
130
- *
131
- * The probe records its result in the per-`serverUrl` wake-availability
132
- * cache. That keeps multiple OpenCode windows with different live listener
133
- * URLs from sharing one process-global transport decision.
134
- */
135
- export async function probeServerReachable(
136
- serverUrl: string | undefined,
137
- timeoutMs = 1500,
138
- ): Promise<boolean> {
139
- if (!serverUrl) return false;
140
- const normalizedServerUrl = normalizeServerUrl(serverUrl);
141
- const controller = new AbortController();
142
- const timer = setTimeout(() => controller.abort(), timeoutMs);
143
- let reachable = false;
144
- try {
145
- // Hit a path that actually exists on the OpenCode HTTP API so a
146
- // successful response confirms the API server is up, not just any
147
- // random listener (e.g. an internal IPC port that accepts TCP but
148
- // rejects all paths with 404 — exactly what TUI binds without
149
- // `--port 0`).
150
- const probeUrl = new URL("/session", serverUrl).toString();
151
- const res = await globalThis.fetch(probeUrl, {
152
- method: "GET",
153
- headers: serverAuthHeaders(),
154
- signal: controller.signal,
155
- });
156
- reachable = res.ok || res.status === 401 || res.status === 403;
157
- } catch {
158
- reachable = false;
159
- } finally {
160
- clearTimeout(timer);
161
- liveServerWakeAvailableByServerUrl.set(normalizedServerUrl, reachable);
162
- }
163
- return reachable;
164
- }
165
-
166
- /**
167
- * Record a probe result. Prefer the `(serverUrl, available)` form; the
168
- * single-boolean form is a compatibility fallback for old call sites and
169
- * tests that do not yet have a URL to key by.
170
- */
171
- export function setLiveServerWakeAvailable(available: boolean): void;
172
- export function setLiveServerWakeAvailable(serverUrl: string | undefined, available: boolean): void;
173
- export function setLiveServerWakeAvailable(
174
- serverUrlOrAvailable: string | boolean | undefined,
175
- available?: boolean,
176
- ): void {
177
- if (typeof serverUrlOrAvailable === "boolean") {
178
- legacyLiveServerWakeAvailable = serverUrlOrAvailable;
179
- return;
180
- }
181
- if (!serverUrlOrAvailable) {
182
- legacyLiveServerWakeAvailable = available ?? false;
183
- return;
184
- }
185
- liveServerWakeAvailableByServerUrl.set(
186
- normalizeServerUrl(serverUrlOrAvailable),
187
- available ?? false,
188
- );
189
- }
190
-
191
- /**
192
- * Read the cached probe decision for `serverUrl`. `true` means the wake path
193
- * should use `getLiveServerClient(serverUrl, directory)` and POST through
194
- * the live HTTP listener. `false` means fall back to the in-process client
195
- * passed via plugin context (`input.client`).
196
- */
197
- export function useLiveServerWake(serverUrl?: string): boolean {
198
- if (!serverUrl) return legacyLiveServerWakeAvailable;
199
- return liveServerWakeAvailableByServerUrl.get(normalizeServerUrl(serverUrl)) ?? false;
200
- }
201
-
202
- /** Test helper — reset the decision cache between cases. */
203
- export function __resetLiveServerWakeForTests(): void {
204
- liveServerWakeAvailableByServerUrl.clear();
205
- legacyLiveServerWakeAvailable = false;
206
- }