@opencode-ai/client 0.0.0-next-17239 → 0.0.0-next-17247
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/dist/effect/api/api.d.ts +1 -0
- package/dist/effect/generated/client.js +4 -1
- package/dist/effect/service.js +24 -22
- package/dist/promise/generated/client.js +1 -0
- package/dist/promise/generated/types.d.ts +3 -0
- package/dist/promise/service.js +28 -26
- package/dist/service-timing.d.ts +13 -0
- package/dist/service-timing.js +19 -0
- package/package.json +4 -4
package/dist/effect/api/api.d.ts
CHANGED
|
@@ -1236,6 +1236,7 @@ export type Endpoint5_31Output = ({
|
|
|
1236
1236
|
export type SessionLogOperation<E = never> = (input: Endpoint5_31Input) => Stream.Stream<Endpoint5_31Output, E>;
|
|
1237
1237
|
export type Endpoint5_32Input = {
|
|
1238
1238
|
readonly sessionID: Session.ID;
|
|
1239
|
+
readonly continue?: boolean | undefined;
|
|
1239
1240
|
};
|
|
1240
1241
|
export type Endpoint5_32Output = void;
|
|
1241
1242
|
export type SessionInterruptOperation<E = never> = (input: Endpoint5_32Input) => Effect.Effect<Endpoint5_32Output, E>;
|
|
@@ -130,7 +130,10 @@ const Endpoint5_31 = (raw) => (input) => preserveStream()(Stream.unwrap(raw["ses
|
|
|
130
130
|
params: { sessionID: input["sessionID"] },
|
|
131
131
|
query: { after: input["after"], follow: input["follow"] },
|
|
132
132
|
}).pipe(Effect.mapError(mapClientError), Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError))))));
|
|
133
|
-
const Endpoint5_32 = (raw) => (input) => preserveEffect()(raw["session.interrupt"]({
|
|
133
|
+
const Endpoint5_32 = (raw) => (input) => preserveEffect()(raw["session.interrupt"]({
|
|
134
|
+
params: { sessionID: input["sessionID"] },
|
|
135
|
+
query: { continue: input["continue"] },
|
|
136
|
+
}).pipe(Effect.mapError(mapClientError)));
|
|
134
137
|
const Endpoint5_33 = (raw) => (input) => preserveEffect()(raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)));
|
|
135
138
|
const Endpoint5_34 = (raw) => (input) => preserveEffect()(raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(Effect.mapError(mapClientError), Effect.map((value) => value.data)));
|
|
136
139
|
const adaptGroup5 = (raw) => ({
|
package/dist/effect/service.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Effect, FileSystem, Option, Schedule, Schema } from "effect";
|
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
+
import { defaultEnsureTiming, ensureTiming } from "../service-timing.js";
|
|
6
7
|
export * from "../service.js";
|
|
7
8
|
// Read-only lookup: registration file plus health check and version gate.
|
|
8
9
|
// Never spawns; escalation to ensure() is the caller's policy.
|
|
@@ -33,11 +34,12 @@ const discoverLocal = Effect.fnUntraced(function* (options) {
|
|
|
33
34
|
// becomes discoverable. A contender is never killed merely for slow startup.
|
|
34
35
|
/** Ensure a healthy, compatible local service is running. */
|
|
35
36
|
export const ensure = Effect.fn("service.ensure")(function* (options = {}) {
|
|
37
|
+
const timing = ensureTiming(options);
|
|
36
38
|
const contenders = new Set();
|
|
37
39
|
let timeouts;
|
|
38
40
|
let announced = false;
|
|
39
41
|
let lastSpawn = 0;
|
|
40
|
-
let spawnDelay =
|
|
42
|
+
let spawnDelay = timing.spawnDelay;
|
|
41
43
|
const announce = (reason, previousVersion) => Effect.sync(() => {
|
|
42
44
|
if (announced)
|
|
43
45
|
return;
|
|
@@ -62,7 +64,7 @@ export const ensure = Effect.fn("service.ensure")(function* (options = {}) {
|
|
|
62
64
|
});
|
|
63
65
|
});
|
|
64
66
|
const found = yield* Effect.gen(function* () {
|
|
65
|
-
const registration = yield* registered(options.file, true);
|
|
67
|
+
const registration = yield* registered(options.file, true, timing.requestTimeout);
|
|
66
68
|
const info = registration.info;
|
|
67
69
|
const service = registration.service;
|
|
68
70
|
if (registration.timedOut && info !== undefined) {
|
|
@@ -72,7 +74,7 @@ export const ensure = Effect.fn("service.ensure")(function* (options = {}) {
|
|
|
72
74
|
};
|
|
73
75
|
if (timeouts.count >= 3) {
|
|
74
76
|
yield* announce("missing");
|
|
75
|
-
yield* evict(info, options);
|
|
77
|
+
yield* evict(info, options, timing);
|
|
76
78
|
timeouts = undefined;
|
|
77
79
|
lastSpawn = Date.now() - spawnDelay;
|
|
78
80
|
}
|
|
@@ -80,7 +82,7 @@ export const ensure = Effect.fn("service.ensure")(function* (options = {}) {
|
|
|
80
82
|
else
|
|
81
83
|
timeouts = undefined;
|
|
82
84
|
if (service !== undefined) {
|
|
83
|
-
spawnDelay =
|
|
85
|
+
spawnDelay = timing.spawnDelay;
|
|
84
86
|
const compatible = !service.legacy && (options.version === undefined || service.version === options.version);
|
|
85
87
|
if (compatible && service.state === "ready")
|
|
86
88
|
return Option.some(service);
|
|
@@ -89,7 +91,7 @@ export const ensure = Effect.fn("service.ensure")(function* (options = {}) {
|
|
|
89
91
|
if (compatible)
|
|
90
92
|
return Option.none();
|
|
91
93
|
yield* announce("version-mismatch", service.version);
|
|
92
|
-
yield* kill(service, options).pipe(Effect.ignore);
|
|
94
|
+
yield* kill(service, options, timing).pipe(Effect.ignore);
|
|
93
95
|
lastSpawn = 0;
|
|
94
96
|
return Option.none();
|
|
95
97
|
}
|
|
@@ -98,7 +100,7 @@ export const ensure = Effect.fn("service.ensure")(function* (options = {}) {
|
|
|
98
100
|
const finished = [...contenders].filter(contenderFinished);
|
|
99
101
|
const failure = finished.map(contenderFailure).find((error) => error !== undefined);
|
|
100
102
|
if (finished.some((item) => item.child.exitCode === 0)) {
|
|
101
|
-
spawnDelay = Math.min(spawnDelay * 2,
|
|
103
|
+
spawnDelay = Math.min(spawnDelay * 2, timing.maxSpawnDelay);
|
|
102
104
|
}
|
|
103
105
|
finished.forEach((item) => contenders.delete(item));
|
|
104
106
|
if (failure !== undefined && contenders.size === 0)
|
|
@@ -112,7 +114,7 @@ export const ensure = Effect.fn("service.ensure")(function* (options = {}) {
|
|
|
112
114
|
return Option.none();
|
|
113
115
|
}).pipe(Effect.repeat({
|
|
114
116
|
until: Option.isSome,
|
|
115
|
-
schedule: Schedule.max([Schedule.spaced(
|
|
117
|
+
schedule: Schedule.max([Schedule.spaced(timing.pollInterval), Schedule.recurs(timing.attempts)]),
|
|
116
118
|
}));
|
|
117
119
|
if (Option.isNone(found))
|
|
118
120
|
return yield* Effect.fail(new Error("Timed out waiting for the background service to start"));
|
|
@@ -135,7 +137,7 @@ function contenderFinished(contender) {
|
|
|
135
137
|
export const stop = Effect.fn("service.stop")(function* (options = {}) {
|
|
136
138
|
const existing = yield* find(options);
|
|
137
139
|
if (existing !== undefined)
|
|
138
|
-
yield* kill(existing, options);
|
|
140
|
+
yield* kill(existing, options, defaultEnsureTiming);
|
|
139
141
|
});
|
|
140
142
|
function fallback() {
|
|
141
143
|
const state = process.env["XDG_STATE_HOME"] ?? join(homedir(), ".local", "state");
|
|
@@ -170,14 +172,14 @@ const read = Effect.fnUntraced(function* (file) {
|
|
|
170
172
|
const probe = Effect.fnUntraced(function* (info, allowLegacy = false) {
|
|
171
173
|
return (yield* probeResult(info, allowLegacy)).service;
|
|
172
174
|
});
|
|
173
|
-
const probeResult = Effect.fnUntraced(function* (info, allowLegacy = false) {
|
|
175
|
+
const probeResult = Effect.fnUntraced(function* (info, allowLegacy = false, timeout = defaultEnsureTiming.requestTimeout) {
|
|
174
176
|
const endpoint = {
|
|
175
177
|
url: info.url,
|
|
176
178
|
auth: info.password === undefined
|
|
177
179
|
? undefined
|
|
178
180
|
: { type: "basic", username: "opencode", password: info.password },
|
|
179
181
|
};
|
|
180
|
-
const signal = AbortSignal.timeout(
|
|
182
|
+
const signal = AbortSignal.timeout(timeout);
|
|
181
183
|
const result = yield* Effect.promise(() => fetch(new URL("/api/health", info.url), {
|
|
182
184
|
headers: headers(endpoint),
|
|
183
185
|
signal,
|
|
@@ -214,11 +216,11 @@ const probeResult = Effect.fnUntraced(function* (info, allowLegacy = false) {
|
|
|
214
216
|
timedOut: false,
|
|
215
217
|
};
|
|
216
218
|
});
|
|
217
|
-
const registered = Effect.fnUntraced(function* (file, allowLegacy = false) {
|
|
219
|
+
const registered = Effect.fnUntraced(function* (file, allowLegacy = false, timeout) {
|
|
218
220
|
const info = yield* read(file);
|
|
219
221
|
if (info === undefined)
|
|
220
222
|
return { info: undefined, service: undefined, timedOut: false };
|
|
221
|
-
return { info, ...(yield* probeResult(info, allowLegacy)) };
|
|
223
|
+
return { info, ...(yield* probeResult(info, allowLegacy, timeout)) };
|
|
222
224
|
});
|
|
223
225
|
// Health-checked lookup without the version gate: lifecycle operations must be
|
|
224
226
|
// able to see (and replace or stop) a server from a different version.
|
|
@@ -227,7 +229,7 @@ const find = Effect.fnUntraced(function* (options) {
|
|
|
227
229
|
});
|
|
228
230
|
// 50ms cadence bounded at ~5s, shared by stop escalation and each ensure
|
|
229
231
|
// discovery window.
|
|
230
|
-
const poll = Schedule.max([Schedule.spaced(
|
|
232
|
+
const poll = (timing) => Schedule.max([Schedule.spaced(timing.stopPollInterval), Schedule.recurs(timing.stopPollAttempts)]);
|
|
231
233
|
const signal = (pid, name) => Effect.try({ try: () => process.kill(pid, name), catch: (cause) => cause }).pipe(Effect.ignore);
|
|
232
234
|
const stopped = Effect.fnUntraced(function* (pid) {
|
|
233
235
|
const running = yield* Effect.try({ try: () => process.kill(pid, 0), catch: () => false }).pipe(Effect.orElseSucceed(() => false));
|
|
@@ -238,22 +240,22 @@ const stopped = Effect.fnUntraced(function* (pid) {
|
|
|
238
240
|
function same(left, right) {
|
|
239
241
|
return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid;
|
|
240
242
|
}
|
|
241
|
-
const evict = Effect.fnUntraced(function* (info, options) {
|
|
243
|
+
const evict = Effect.fnUntraced(function* (info, options, timing) {
|
|
242
244
|
const current = yield* read(options.file);
|
|
243
245
|
if (current === undefined || !same(current, info))
|
|
244
246
|
return;
|
|
245
247
|
yield* signal(info.pid, "SIGTERM");
|
|
246
|
-
const done = yield* stopped(info.pid).pipe(Effect.retry(poll), Effect.option);
|
|
248
|
+
const done = yield* stopped(info.pid).pipe(Effect.retry(poll(timing)), Effect.option);
|
|
247
249
|
if (Option.isSome(done))
|
|
248
250
|
return;
|
|
249
251
|
const latest = yield* read(options.file);
|
|
250
252
|
if (latest === undefined || !same(latest, info))
|
|
251
253
|
return;
|
|
252
254
|
yield* signal(info.pid, "SIGKILL");
|
|
253
|
-
yield* stopped(info.pid).pipe(Effect.retry(poll));
|
|
255
|
+
yield* stopped(info.pid).pipe(Effect.retry(poll(timing)));
|
|
254
256
|
});
|
|
255
|
-
const kill = Effect.fnUntraced(function* (service, options) {
|
|
256
|
-
const requested = yield* requestStop(service);
|
|
257
|
+
const kill = Effect.fnUntraced(function* (service, options, timing) {
|
|
258
|
+
const requested = yield* requestStop(service, timing.requestTimeout);
|
|
257
259
|
if (requested === "rejected")
|
|
258
260
|
return;
|
|
259
261
|
if (requested === "unsupported") {
|
|
@@ -264,24 +266,24 @@ const kill = Effect.fnUntraced(function* (service, options) {
|
|
|
264
266
|
return;
|
|
265
267
|
yield* signal(service.info.pid, "SIGTERM");
|
|
266
268
|
}
|
|
267
|
-
const done = yield* stopped(service.info.pid).pipe(Effect.retry(poll), Effect.option);
|
|
269
|
+
const done = yield* stopped(service.info.pid).pipe(Effect.retry(poll(timing)), Effect.option);
|
|
268
270
|
if (Option.isSome(done))
|
|
269
271
|
return;
|
|
270
272
|
const latest = yield* find(options);
|
|
271
273
|
if (latest === undefined || !same(latest.info, service.info))
|
|
272
274
|
return;
|
|
273
275
|
yield* signal(service.info.pid, "SIGKILL");
|
|
274
|
-
yield* stopped(service.info.pid).pipe(Effect.retry(poll));
|
|
276
|
+
yield* stopped(service.info.pid).pipe(Effect.retry(poll(timing)));
|
|
275
277
|
});
|
|
276
278
|
const decodeStopResponse = Schema.decodeUnknownOption(ServiceStatus.StopResponse);
|
|
277
|
-
const requestStop = Effect.fnUntraced(function* (service) {
|
|
279
|
+
const requestStop = Effect.fnUntraced(function* (service, timeout = defaultEnsureTiming.requestTimeout) {
|
|
278
280
|
if (service.info.id === undefined || service.legacy)
|
|
279
281
|
return "unsupported";
|
|
280
282
|
const response = yield* Effect.tryPromise(() => fetch(new URL("/api/service/stop", service.info.url), {
|
|
281
283
|
method: "POST",
|
|
282
284
|
headers: { ...headers(service.endpoint), "content-type": "application/json" },
|
|
283
285
|
body: JSON.stringify({ instanceID: service.info.id }),
|
|
284
|
-
signal: AbortSignal.timeout(
|
|
286
|
+
signal: AbortSignal.timeout(timeout),
|
|
285
287
|
})).pipe(Effect.option, Effect.map(Option.getOrUndefined));
|
|
286
288
|
if (response === undefined || response.status === 404 || response.status === 405)
|
|
287
289
|
return "unsupported";
|
|
@@ -477,6 +477,7 @@ export function make(options) {
|
|
|
477
477
|
interrupt: (input, requestOptions) => request({
|
|
478
478
|
method: "POST",
|
|
479
479
|
path: `/api/session/${encodeURIComponent(input.sessionID)}/interrupt`,
|
|
480
|
+
query: { continue: input["continue"] },
|
|
480
481
|
successStatus: 204,
|
|
481
482
|
declaredStatuses: [404, 400, 401],
|
|
482
483
|
empty: true,
|
|
@@ -5433,6 +5433,9 @@ export type SessionInterruptInput = {
|
|
|
5433
5433
|
readonly sessionID: {
|
|
5434
5434
|
readonly sessionID: string;
|
|
5435
5435
|
}["sessionID"];
|
|
5436
|
+
readonly continue?: {
|
|
5437
|
+
readonly continue?: boolean | undefined;
|
|
5438
|
+
}["continue"];
|
|
5436
5439
|
};
|
|
5437
5440
|
export type SessionInterruptOutput = void;
|
|
5438
5441
|
export type SessionBackgroundInput = {
|
package/dist/promise/service.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { defaultEnsureTiming, ensureTiming } from "../service-timing.js";
|
|
5
6
|
export * from "../service.js";
|
|
6
7
|
/** Discover a healthy, compatible local service without starting one. */
|
|
7
8
|
export async function discover(options = {}) {
|
|
@@ -17,12 +18,13 @@ async function discoverLocal(options) {
|
|
|
17
18
|
}
|
|
18
19
|
/** Ensure a healthy, compatible local service is running. */
|
|
19
20
|
export async function ensure(options = {}) {
|
|
20
|
-
const
|
|
21
|
+
const timing = ensureTiming(options);
|
|
22
|
+
const deadline = Date.now() + timing.promiseTimeout;
|
|
21
23
|
const contenders = new Set();
|
|
22
24
|
let timeouts;
|
|
23
25
|
let announced = false;
|
|
24
26
|
let lastSpawn = 0;
|
|
25
|
-
let spawnDelay =
|
|
27
|
+
let spawnDelay = timing.spawnDelay;
|
|
26
28
|
const announce = (reason, previousVersion) => {
|
|
27
29
|
if (announced)
|
|
28
30
|
return;
|
|
@@ -49,7 +51,7 @@ export async function ensure(options = {}) {
|
|
|
49
51
|
while (true) {
|
|
50
52
|
if (Date.now() >= deadline)
|
|
51
53
|
throw new Error("Timed out waiting for the background service to start");
|
|
52
|
-
const registration = await registered(options.file, true);
|
|
54
|
+
const registration = await registered(options.file, true, timing.requestTimeout);
|
|
53
55
|
if (registration.timedOut && registration.info !== undefined) {
|
|
54
56
|
timeouts = {
|
|
55
57
|
info: registration.info,
|
|
@@ -57,7 +59,7 @@ export async function ensure(options = {}) {
|
|
|
57
59
|
};
|
|
58
60
|
if (timeouts.count >= 3) {
|
|
59
61
|
announce("missing");
|
|
60
|
-
await evict(registration.info, options);
|
|
62
|
+
await evict(registration.info, options, timing);
|
|
61
63
|
timeouts = undefined;
|
|
62
64
|
lastSpawn = Date.now() - spawnDelay;
|
|
63
65
|
}
|
|
@@ -65,7 +67,7 @@ export async function ensure(options = {}) {
|
|
|
65
67
|
else
|
|
66
68
|
timeouts = undefined;
|
|
67
69
|
if (registration.service !== undefined) {
|
|
68
|
-
spawnDelay =
|
|
70
|
+
spawnDelay = timing.spawnDelay;
|
|
69
71
|
const service = registration.service;
|
|
70
72
|
const compatible = !service.legacy && (options.version === undefined || service.version === options.version);
|
|
71
73
|
if (compatible && service.state === "ready")
|
|
@@ -74,7 +76,7 @@ export async function ensure(options = {}) {
|
|
|
74
76
|
throw new Error("Background service failed to start");
|
|
75
77
|
if (!compatible) {
|
|
76
78
|
announce("version-mismatch", service.version);
|
|
77
|
-
await kill(service, options).catch(() => undefined);
|
|
79
|
+
await kill(service, options, timing).catch(() => undefined);
|
|
78
80
|
lastSpawn = 0;
|
|
79
81
|
}
|
|
80
82
|
}
|
|
@@ -84,7 +86,7 @@ export async function ensure(options = {}) {
|
|
|
84
86
|
const finished = [...contenders].filter(contenderFinished);
|
|
85
87
|
const failure = finished.map(contenderFailure).find((error) => error !== undefined);
|
|
86
88
|
if (finished.some((item) => item.child.exitCode === 0)) {
|
|
87
|
-
spawnDelay = Math.min(spawnDelay * 2,
|
|
89
|
+
spawnDelay = Math.min(spawnDelay * 2, timing.maxSpawnDelay);
|
|
88
90
|
}
|
|
89
91
|
finished.forEach((item) => contenders.delete(item));
|
|
90
92
|
if (failure !== undefined && contenders.size === 0)
|
|
@@ -96,7 +98,7 @@ export async function ensure(options = {}) {
|
|
|
96
98
|
lastSpawn = Date.now();
|
|
97
99
|
}
|
|
98
100
|
}
|
|
99
|
-
await delay(
|
|
101
|
+
await delay(timing.pollInterval);
|
|
100
102
|
}
|
|
101
103
|
}
|
|
102
104
|
function contenderFailure(contender) {
|
|
@@ -116,7 +118,7 @@ function contenderFinished(contender) {
|
|
|
116
118
|
export async function stop(options = {}) {
|
|
117
119
|
const existing = await find(options);
|
|
118
120
|
if (existing !== undefined)
|
|
119
|
-
await kill(existing, options);
|
|
121
|
+
await kill(existing, options, defaultEnsureTiming);
|
|
120
122
|
}
|
|
121
123
|
function fallback() {
|
|
122
124
|
return join(process.env["XDG_STATE_HOME"] ?? join(homedir(), ".local", "state"), "opencode", "service.json");
|
|
@@ -143,14 +145,14 @@ async function read(file) {
|
|
|
143
145
|
async function probe(info, allowLegacy = false) {
|
|
144
146
|
return (await probeResult(info, allowLegacy)).service;
|
|
145
147
|
}
|
|
146
|
-
async function probeResult(info, allowLegacy = false) {
|
|
148
|
+
async function probeResult(info, allowLegacy = false, timeout = defaultEnsureTiming.requestTimeout) {
|
|
147
149
|
const endpoint = {
|
|
148
150
|
url: info.url,
|
|
149
151
|
auth: info.password === undefined
|
|
150
152
|
? undefined
|
|
151
153
|
: { type: "basic", username: "opencode", password: info.password },
|
|
152
154
|
};
|
|
153
|
-
const signal = AbortSignal.timeout(
|
|
155
|
+
const signal = AbortSignal.timeout(timeout);
|
|
154
156
|
const result = await fetch(new URL("/api/health", info.url), {
|
|
155
157
|
headers: headers(endpoint),
|
|
156
158
|
signal,
|
|
@@ -187,11 +189,11 @@ async function probeResult(info, allowLegacy = false) {
|
|
|
187
189
|
timedOut: false,
|
|
188
190
|
};
|
|
189
191
|
}
|
|
190
|
-
async function registered(file, allowLegacy = false) {
|
|
192
|
+
async function registered(file, allowLegacy = false, timeout) {
|
|
191
193
|
const info = await read(file);
|
|
192
194
|
if (info === undefined)
|
|
193
195
|
return { info: undefined, service: undefined, timedOut: false };
|
|
194
|
-
return { info, ...(await probeResult(info, allowLegacy)) };
|
|
196
|
+
return { info, ...(await probeResult(info, allowLegacy, timeout)) };
|
|
195
197
|
}
|
|
196
198
|
async function find(options) {
|
|
197
199
|
return (await registered(options.file, true)).service;
|
|
@@ -211,34 +213,34 @@ function stopped(pid) {
|
|
|
211
213
|
return true;
|
|
212
214
|
}
|
|
213
215
|
}
|
|
214
|
-
async function waitUntilStopped(pid) {
|
|
215
|
-
for (let attempt = 0; attempt <=
|
|
216
|
+
async function waitUntilStopped(pid, timing) {
|
|
217
|
+
for (let attempt = 0; attempt <= timing.stopPollAttempts; attempt++) {
|
|
216
218
|
if (stopped(pid))
|
|
217
219
|
return true;
|
|
218
|
-
if (attempt <
|
|
219
|
-
await delay(
|
|
220
|
+
if (attempt < timing.stopPollAttempts)
|
|
221
|
+
await delay(timing.stopPollInterval);
|
|
220
222
|
}
|
|
221
223
|
return false;
|
|
222
224
|
}
|
|
223
225
|
function same(left, right) {
|
|
224
226
|
return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid;
|
|
225
227
|
}
|
|
226
|
-
async function evict(info, options) {
|
|
228
|
+
async function evict(info, options, timing) {
|
|
227
229
|
const current = await read(options.file);
|
|
228
230
|
if (current === undefined || !same(current, info))
|
|
229
231
|
return;
|
|
230
232
|
signal(info.pid, "SIGTERM");
|
|
231
|
-
if (await waitUntilStopped(info.pid))
|
|
233
|
+
if (await waitUntilStopped(info.pid, timing))
|
|
232
234
|
return;
|
|
233
235
|
const latest = await read(options.file);
|
|
234
236
|
if (latest === undefined || !same(latest, info))
|
|
235
237
|
return;
|
|
236
238
|
signal(info.pid, "SIGKILL");
|
|
237
|
-
if (!(await waitUntilStopped(info.pid)))
|
|
239
|
+
if (!(await waitUntilStopped(info.pid, timing)))
|
|
238
240
|
throw new Error(`Server process ${info.pid} is still running`);
|
|
239
241
|
}
|
|
240
|
-
async function kill(service, options) {
|
|
241
|
-
const requested = await requestStop(service);
|
|
242
|
+
async function kill(service, options, timing) {
|
|
243
|
+
const requested = await requestStop(service, timing.requestTimeout);
|
|
242
244
|
if (requested === "rejected")
|
|
243
245
|
return;
|
|
244
246
|
if (requested === "unsupported") {
|
|
@@ -247,23 +249,23 @@ async function kill(service, options) {
|
|
|
247
249
|
return;
|
|
248
250
|
signal(service.info.pid, "SIGTERM");
|
|
249
251
|
}
|
|
250
|
-
if (await waitUntilStopped(service.info.pid))
|
|
252
|
+
if (await waitUntilStopped(service.info.pid, timing))
|
|
251
253
|
return;
|
|
252
254
|
const latest = await find(options);
|
|
253
255
|
if (latest === undefined || !same(latest.info, service.info))
|
|
254
256
|
return;
|
|
255
257
|
signal(service.info.pid, "SIGKILL");
|
|
256
|
-
if (!(await waitUntilStopped(service.info.pid)))
|
|
258
|
+
if (!(await waitUntilStopped(service.info.pid, timing)))
|
|
257
259
|
throw new Error(`Server process ${service.info.pid} is still running`);
|
|
258
260
|
}
|
|
259
|
-
async function requestStop(service) {
|
|
261
|
+
async function requestStop(service, timeout = defaultEnsureTiming.requestTimeout) {
|
|
260
262
|
if (service.info.id === undefined || service.legacy)
|
|
261
263
|
return "unsupported";
|
|
262
264
|
const response = await fetch(new URL("/api/service/stop", service.info.url), {
|
|
263
265
|
method: "POST",
|
|
264
266
|
headers: { ...headers(service.endpoint), "content-type": "application/json" },
|
|
265
267
|
body: JSON.stringify({ instanceID: service.info.id }),
|
|
266
|
-
signal: AbortSignal.timeout(
|
|
268
|
+
signal: AbortSignal.timeout(timeout),
|
|
267
269
|
}).catch(() => undefined);
|
|
268
270
|
if (response === undefined || response.status === 404 || response.status === 405)
|
|
269
271
|
return "unsupported";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type EnsureTiming = {
|
|
2
|
+
readonly pollInterval: number;
|
|
3
|
+
readonly attempts: number;
|
|
4
|
+
readonly requestTimeout: number;
|
|
5
|
+
readonly spawnDelay: number;
|
|
6
|
+
readonly maxSpawnDelay: number;
|
|
7
|
+
readonly promiseTimeout: number;
|
|
8
|
+
readonly stopPollInterval: number;
|
|
9
|
+
readonly stopPollAttempts: number;
|
|
10
|
+
};
|
|
11
|
+
export declare const defaultEnsureTiming: EnsureTiming;
|
|
12
|
+
export declare function ensureTiming(options: object): EnsureTiming;
|
|
13
|
+
export declare function withEnsureTiming<A extends object>(options: A, overrides: Partial<EnsureTiming>): A;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const timings = new WeakMap();
|
|
2
|
+
export const defaultEnsureTiming = {
|
|
3
|
+
pollInterval: 1_000,
|
|
4
|
+
attempts: 120,
|
|
5
|
+
requestTimeout: 2_000,
|
|
6
|
+
spawnDelay: 5_000,
|
|
7
|
+
maxSpawnDelay: 30_000,
|
|
8
|
+
promiseTimeout: 120_000,
|
|
9
|
+
stopPollInterval: 50,
|
|
10
|
+
stopPollAttempts: 100,
|
|
11
|
+
};
|
|
12
|
+
export function ensureTiming(options) {
|
|
13
|
+
return timings.get(options) ?? defaultEnsureTiming;
|
|
14
|
+
}
|
|
15
|
+
// Keep test timing out of the public lifecycle option types.
|
|
16
|
+
export function withEnsureTiming(options, overrides) {
|
|
17
|
+
timings.set(options, { ...defaultEnsureTiming, ...overrides });
|
|
18
|
+
return options;
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@opencode-ai/client",
|
|
4
|
-
"version": "0.0.0-next-
|
|
4
|
+
"version": "0.0.0-next-17247",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"typecheck": "tsgo --noEmit"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@opencode-ai/schema": "0.0.0-next-
|
|
57
|
-
"@opencode-ai/protocol": "0.0.0-next-
|
|
56
|
+
"@opencode-ai/schema": "0.0.0-next-17247",
|
|
57
|
+
"@opencode-ai/protocol": "0.0.0-next-17247"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"effect": "4.0.0-beta.101"
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@effect/platform-node": "4.0.0-beta.101",
|
|
69
|
-
"@opencode-ai/httpapi-codegen": "0.0.0-next-
|
|
69
|
+
"@opencode-ai/httpapi-codegen": "0.0.0-next-17247",
|
|
70
70
|
"@tsconfig/bun": "1.0.9",
|
|
71
71
|
"@types/bun": "1.3.13",
|
|
72
72
|
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|