@opencode/client 0.0.0-dev-19564 → 0.0.0-dev-19565

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,4 +1,3 @@
1
- import { ServerStatus } from "@opencode/protocol/groups/server";
2
1
  import { Effect, FileSystem, Option, Schedule, Schema } from "effect";
3
2
  import { homedir } from "node:os";
4
3
  import { join } from "node:path";
@@ -21,6 +20,8 @@ export const discover = Effect.fn("service.discover")(function* (options = {}) {
21
20
  const found = (yield* registered(options.file)).service;
22
21
  if (found?.state !== "ready")
23
22
  return undefined;
23
+ if (!found.compatible)
24
+ return undefined;
24
25
  if (!matchesVersion(found.version, options))
25
26
  return undefined;
26
27
  return found.endpoint;
@@ -31,6 +32,8 @@ export const incumbent = Effect.fn("service.incumbent")(function* (options) {
31
32
  const found = info === undefined ? undefined : yield* probe({ ...info, url: options.url });
32
33
  if (found === undefined)
33
34
  return undefined;
35
+ if (!found.compatible)
36
+ return undefined;
34
37
  if (!matchesVersion(found.version, options))
35
38
  return undefined;
36
39
  return { endpoint: found.endpoint, state: found.state };
@@ -86,7 +89,7 @@ export const ensure = Effect.fn("service.ensure")(function* (options = {}) {
86
89
  timeouts = undefined;
87
90
  if (service !== undefined) {
88
91
  spawnDelay = timing.spawnDelay;
89
- const compatible = matchesVersion(service.version, options);
92
+ const compatible = service.compatible && matchesVersion(service.version, options);
90
93
  if (compatible && service.state === "ready") {
91
94
  yield* Effect.tryPromise(() => PtyHandoff.complete(options.file ?? fallback(), service.info));
92
95
  return Option.some(service);
@@ -159,7 +162,10 @@ export const Info = Schema.Struct({
159
162
  password: Schema.optional(Schema.String),
160
163
  });
161
164
  const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(Info));
162
- const decodeStatus = Schema.decodeUnknownOption(ServerStatus);
165
+ const decodeStatus = Schema.decodeUnknownOption(Schema.Struct({
166
+ version: Schema.String,
167
+ pid: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
168
+ }));
163
169
  // A missing or corrupt file means no valid info; callers treat both
164
170
  // the same (the registering server self-evicts, clients rediscover).
165
171
  const read = Effect.fnUntraced(function* (file) {
@@ -181,11 +187,27 @@ const probeResult = Effect.fnUntraced(function* (info, timeout = defaultEnsureTi
181
187
  };
182
188
  const signal = AbortSignal.timeout(timeout);
183
189
  const result = yield* Effect.promise(() => fetch(new URL("/api/status", info.url), { headers: headers(endpoint), signal })
184
- .then(async (response) => ({ response, body: (await response.json()) }))
190
+ .then(async (response) => ({
191
+ response,
192
+ body: response.status === 404 ? undefined : (await response.json()),
193
+ }))
185
194
  .then((value) => ({ value }), (cause) => ({ cause })));
186
195
  if ("cause" in result)
187
196
  return { service: undefined, timedOut: signal.aborted };
188
197
  const response = result.value.response;
198
+ // The previous V2 service exposes /api/health instead. Its authenticated 404 is enough
199
+ // to recognize the registered daemon as incompatible and route it through replacement.
200
+ if (response.status === 404)
201
+ return {
202
+ service: {
203
+ info,
204
+ endpoint,
205
+ version: info.version,
206
+ state: "ready",
207
+ compatible: false,
208
+ },
209
+ timedOut: false,
210
+ };
189
211
  const body = result.value.body;
190
212
  const status = decodeStatus(body);
191
213
  if (Option.isSome(status)) {
@@ -199,6 +221,7 @@ const probeResult = Effect.fnUntraced(function* (info, timeout = defaultEnsureTi
199
221
  endpoint,
200
222
  version: status.value.version,
201
223
  state: response.ok ? "ready" : response.status === 500 ? "failed" : "waiting",
224
+ compatible: true,
202
225
  },
203
226
  timedOut: false,
204
227
  };
@@ -16,6 +16,8 @@ export async function discover(options = {}) {
16
16
  const found = (await registered(options.file)).service;
17
17
  if (found?.state !== "ready")
18
18
  return undefined;
19
+ if (!found.compatible)
20
+ return undefined;
19
21
  if (!matchesVersion(found.version, options))
20
22
  return undefined;
21
23
  return found.endpoint;
@@ -70,7 +72,7 @@ export async function ensure(options = {}) {
70
72
  if (registration.service !== undefined) {
71
73
  spawnDelay = timing.spawnDelay;
72
74
  const service = registration.service;
73
- const compatible = matchesVersion(service.version, options);
75
+ const compatible = service.compatible && matchesVersion(service.version, options);
74
76
  if (compatible && service.state === "ready") {
75
77
  await PtyHandoff.complete(options.file ?? fallback(), service.info);
76
78
  return service.endpoint;
@@ -156,30 +158,53 @@ async function probeResult(info, timeout = defaultEnsureTiming.requestTimeout) {
156
158
  const result = await fetch(new URL("/api/status", info.url), { headers: headers(endpoint), signal })
157
159
  .then(async (response) => ({
158
160
  response,
159
- body: (await response.json()),
161
+ body: response.status === 404 ? undefined : (await response.json()),
160
162
  }))
161
163
  .then((value) => ({ value }), (cause) => ({ cause }));
162
164
  if ("cause" in result)
163
165
  return { service: undefined, timedOut: signal.aborted };
164
166
  const response = result.value.response;
165
- const body = result.value.body;
166
- if (body !== undefined && "version" in body && "pid" in body) {
167
- if (body.pid !== info.pid)
167
+ // The previous V2 service exposes /api/health instead. Its authenticated 404 is enough
168
+ // to recognize the registered daemon as incompatible and route it through replacement.
169
+ if (response.status === 404)
170
+ return {
171
+ service: {
172
+ info,
173
+ endpoint,
174
+ version: info.version,
175
+ state: "ready",
176
+ compatible: false,
177
+ },
178
+ timedOut: false,
179
+ };
180
+ const status = decodeStatus(result.value.body);
181
+ if (status !== undefined) {
182
+ if (status.pid !== info.pid)
168
183
  return { service: undefined, timedOut: false };
169
- if (info.version !== undefined && body.version !== info.version)
184
+ if (info.version !== undefined && status.version !== info.version)
170
185
  return { service: undefined, timedOut: false };
171
186
  return {
172
187
  service: {
173
188
  info,
174
189
  endpoint,
175
- version: body.version,
190
+ version: status.version,
176
191
  state: response.ok ? "ready" : response.status === 500 ? "failed" : "waiting",
192
+ compatible: true,
177
193
  },
178
194
  timedOut: false,
179
195
  };
180
196
  }
181
197
  return { service: undefined, timedOut: false };
182
198
  }
199
+ function decodeStatus(input) {
200
+ if (typeof input !== "object" || input === null)
201
+ return;
202
+ if (!("version" in input) || typeof input.version !== "string")
203
+ return;
204
+ if (!("pid" in input) || typeof input.pid !== "number" || !Number.isInteger(input.pid) || input.pid < 0)
205
+ return;
206
+ return { version: input.version, pid: input.pid };
207
+ }
183
208
  async function registered(file, timeout) {
184
209
  const info = await read(file);
185
210
  if (info === undefined)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@opencode/client",
4
- "version": "0.0.0-dev-19564",
4
+ "version": "0.0.0-dev-19565",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -57,8 +57,8 @@
57
57
  "typecheck": "tsgo --noEmit"
58
58
  },
59
59
  "dependencies": {
60
- "@opencode/schema": "0.0.0-dev-19564",
61
- "@opencode/protocol": "0.0.0-dev-19564"
60
+ "@opencode/schema": "0.0.0-dev-19565",
61
+ "@opencode/protocol": "0.0.0-dev-19565"
62
62
  },
63
63
  "peerDependencies": {
64
64
  "effect": "4.0.0-rc.112",
@@ -74,7 +74,7 @@
74
74
  },
75
75
  "devDependencies": {
76
76
  "@effect/platform-node": "4.0.0-rc.112",
77
- "@opencode/httpapi-codegen": "0.0.0-dev-19564",
77
+ "@opencode/httpapi-codegen": "0.0.0-dev-19565",
78
78
  "@tsconfig/bun": "1.0.9",
79
79
  "@types/bun": "1.4.0",
80
80
  "@typescript/native-preview": "7.0.0-dev.20251207.1",