larkin 0.5.8 → 0.5.10
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/runtime/claude-model-catalog.cjs +1 -1
- package/dist/runtime/claude-model-catalog.mjs +1 -1
- package/dist/runtime/codex-model-catalog.cjs +1 -1
- package/dist/runtime/codex-model-catalog.mjs +1 -1
- package/dist/runtime/pi-rpc-client.cjs +29 -4
- package/dist/runtime/pi-rpc-client.mjs +29 -4
- package/dist/runtime/runtime-readiness.cjs +11 -1
- package/dist/runtime/runtime-readiness.mjs +11 -1
- package/package.json +1 -1
|
@@ -30,7 +30,7 @@ async function runClaudeControl(call) {
|
|
|
30
30
|
};
|
|
31
31
|
const timer = setTimeout(() => finish(new Error("Claude model catalog timed out")), call.timeout);
|
|
32
32
|
timer.unref?.();
|
|
33
|
-
child.on("error", () => finish(new Error(
|
|
33
|
+
child.on("error", (error) => finish(new Error(`Claude model catalog command failed: ${error.message}`)));
|
|
34
34
|
child.on("exit", () => finish(new Error("Claude model catalog control channel exited before response")));
|
|
35
35
|
child.stderr.resume();
|
|
36
36
|
child.stdout.on("data", (chunk) => {
|
|
@@ -27,7 +27,7 @@ async function runClaudeControl(call) {
|
|
|
27
27
|
};
|
|
28
28
|
const timer = setTimeout(() => finish(new Error("Claude model catalog timed out")), call.timeout);
|
|
29
29
|
timer.unref?.();
|
|
30
|
-
child.on("error", () => finish(new Error(
|
|
30
|
+
child.on("error", (error) => finish(new Error(`Claude model catalog command failed: ${error.message}`)));
|
|
31
31
|
child.on("exit", () => finish(new Error("Claude model catalog control channel exited before response")));
|
|
32
32
|
child.stderr.resume();
|
|
33
33
|
child.stdout.on("data", (chunk) => {
|
|
@@ -31,7 +31,7 @@ async function runCodexAppServer(call) {
|
|
|
31
31
|
};
|
|
32
32
|
const timer = setTimeout(() => finish(new Error("Codex model catalog timed out")), call.timeout);
|
|
33
33
|
timer.unref?.();
|
|
34
|
-
child.on("error", () => finish(new Error(
|
|
34
|
+
child.on("error", (error) => finish(new Error(`Codex model catalog command failed: ${error.message}`)));
|
|
35
35
|
child.on("exit", () => finish(new Error("Codex model catalog app-server exited before response")));
|
|
36
36
|
child.stderr.resume();
|
|
37
37
|
child.stdout.on("data", (chunk) => {
|
|
@@ -28,7 +28,7 @@ async function runCodexAppServer(call) {
|
|
|
28
28
|
};
|
|
29
29
|
const timer = setTimeout(() => finish(new Error("Codex model catalog timed out")), call.timeout);
|
|
30
30
|
timer.unref?.();
|
|
31
|
-
child.on("error", () => finish(new Error(
|
|
31
|
+
child.on("error", (error) => finish(new Error(`Codex model catalog command failed: ${error.message}`)));
|
|
32
32
|
child.on("exit", () => finish(new Error("Codex model catalog app-server exited before response")));
|
|
33
33
|
child.stderr.resume();
|
|
34
34
|
child.stdout.on("data", (chunk) => {
|
|
@@ -47,11 +47,15 @@ class PiRpcClient {
|
|
|
47
47
|
this.stderrBuffer = Buffer.concat([this.stderrBuffer, value.subarray(0, this.maxStderrBytes - this.stderrBuffer.length)]);
|
|
48
48
|
});
|
|
49
49
|
process.once("error", (error) => this.fail(new Error(`Pi RPC process failed: ${error.message}`), true));
|
|
50
|
-
|
|
50
|
+
const settle = (code, signal) => {
|
|
51
51
|
this.exited = true;
|
|
52
52
|
const stderr = this.stderrBuffer.toString("utf8").trim().slice(0, 2_000);
|
|
53
53
|
this.fail(new Error(`Pi RPC process exited (${code ?? signal ?? "unknown"})${stderr ? `: ${stderr}` : ""}`), false);
|
|
54
|
-
}
|
|
54
|
+
};
|
|
55
|
+
process.once("exit", settle);
|
|
56
|
+
// A spawn that never started emits "error" + "close" and never "exit", so
|
|
57
|
+
// "close" is the only terminal event Node guarantees for failed spawns.
|
|
58
|
+
process.once("close", settle);
|
|
55
59
|
}
|
|
56
60
|
subscribe(listener) {
|
|
57
61
|
this.eventListeners.add(listener);
|
|
@@ -89,11 +93,11 @@ class PiRpcClient {
|
|
|
89
93
|
throw new Error("stdin is unavailable");
|
|
90
94
|
this.process.stdin.write(`${JSON.stringify({ id, type: command, ...fields })}\n`, (error) => {
|
|
91
95
|
if (error)
|
|
92
|
-
this.
|
|
96
|
+
this.failWrite(command, error);
|
|
93
97
|
});
|
|
94
98
|
}
|
|
95
99
|
catch (error) {
|
|
96
|
-
this.
|
|
100
|
+
this.failWrite(command, error);
|
|
97
101
|
}
|
|
98
102
|
});
|
|
99
103
|
}
|
|
@@ -210,6 +214,24 @@ class PiRpcClient {
|
|
|
210
214
|
protocolFailure(detail) {
|
|
211
215
|
this.fail(new Error(`Pi RPC protocol error: ${detail}`), true);
|
|
212
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* A failed write matters less than why the process is gone: a spawn failure or
|
|
219
|
+
* an early exit explains it, and that verdict is queued on the next tick. Defer
|
|
220
|
+
* the write error by a single turn (next tick runs before the check phase, so a
|
|
221
|
+
* real spawn/exit verdict always wins) and report it only if no verdict arrives.
|
|
222
|
+
* No arbitration timer; concurrent write failures collapse into the client's
|
|
223
|
+
* single first-error terminal state via `fail`'s idempotence.
|
|
224
|
+
*/
|
|
225
|
+
failWrite(command, error) {
|
|
226
|
+
if (this.failed)
|
|
227
|
+
return;
|
|
228
|
+
const message = `Pi RPC ${command} write failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
229
|
+
setImmediate(() => {
|
|
230
|
+
if (this.failed)
|
|
231
|
+
return;
|
|
232
|
+
this.fail(new Error(message), true);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
213
235
|
fail(error, terminate) {
|
|
214
236
|
if (this.failed)
|
|
215
237
|
return;
|
|
@@ -245,6 +267,9 @@ class PiRpcClient {
|
|
|
245
267
|
resolve();
|
|
246
268
|
};
|
|
247
269
|
this.process.once("exit", finish);
|
|
270
|
+
// A failed spawn never emits "exit": settle on "close" instead of waiting
|
|
271
|
+
// out the grace timers (or never, when nothing else keeps the loop alive).
|
|
272
|
+
this.process.once("close", finish);
|
|
248
273
|
this.process.kill("SIGTERM");
|
|
249
274
|
forceTimer = setTimeout(() => {
|
|
250
275
|
if (this.exited) {
|
|
@@ -44,11 +44,15 @@ export class PiRpcClient {
|
|
|
44
44
|
this.stderrBuffer = Buffer.concat([this.stderrBuffer, value.subarray(0, this.maxStderrBytes - this.stderrBuffer.length)]);
|
|
45
45
|
});
|
|
46
46
|
process.once("error", (error) => this.fail(new Error(`Pi RPC process failed: ${error.message}`), true));
|
|
47
|
-
|
|
47
|
+
const settle = (code, signal) => {
|
|
48
48
|
this.exited = true;
|
|
49
49
|
const stderr = this.stderrBuffer.toString("utf8").trim().slice(0, 2_000);
|
|
50
50
|
this.fail(new Error(`Pi RPC process exited (${code ?? signal ?? "unknown"})${stderr ? `: ${stderr}` : ""}`), false);
|
|
51
|
-
}
|
|
51
|
+
};
|
|
52
|
+
process.once("exit", settle);
|
|
53
|
+
// A spawn that never started emits "error" + "close" and never "exit", so
|
|
54
|
+
// "close" is the only terminal event Node guarantees for failed spawns.
|
|
55
|
+
process.once("close", settle);
|
|
52
56
|
}
|
|
53
57
|
subscribe(listener) {
|
|
54
58
|
this.eventListeners.add(listener);
|
|
@@ -86,11 +90,11 @@ export class PiRpcClient {
|
|
|
86
90
|
throw new Error("stdin is unavailable");
|
|
87
91
|
this.process.stdin.write(`${JSON.stringify({ id, type: command, ...fields })}\n`, (error) => {
|
|
88
92
|
if (error)
|
|
89
|
-
this.
|
|
93
|
+
this.failWrite(command, error);
|
|
90
94
|
});
|
|
91
95
|
}
|
|
92
96
|
catch (error) {
|
|
93
|
-
this.
|
|
97
|
+
this.failWrite(command, error);
|
|
94
98
|
}
|
|
95
99
|
});
|
|
96
100
|
}
|
|
@@ -207,6 +211,24 @@ export class PiRpcClient {
|
|
|
207
211
|
protocolFailure(detail) {
|
|
208
212
|
this.fail(new Error(`Pi RPC protocol error: ${detail}`), true);
|
|
209
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* A failed write matters less than why the process is gone: a spawn failure or
|
|
216
|
+
* an early exit explains it, and that verdict is queued on the next tick. Defer
|
|
217
|
+
* the write error by a single turn (next tick runs before the check phase, so a
|
|
218
|
+
* real spawn/exit verdict always wins) and report it only if no verdict arrives.
|
|
219
|
+
* No arbitration timer; concurrent write failures collapse into the client's
|
|
220
|
+
* single first-error terminal state via `fail`'s idempotence.
|
|
221
|
+
*/
|
|
222
|
+
failWrite(command, error) {
|
|
223
|
+
if (this.failed)
|
|
224
|
+
return;
|
|
225
|
+
const message = `Pi RPC ${command} write failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
226
|
+
setImmediate(() => {
|
|
227
|
+
if (this.failed)
|
|
228
|
+
return;
|
|
229
|
+
this.fail(new Error(message), true);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
210
232
|
fail(error, terminate) {
|
|
211
233
|
if (this.failed)
|
|
212
234
|
return;
|
|
@@ -242,6 +264,9 @@ export class PiRpcClient {
|
|
|
242
264
|
resolve();
|
|
243
265
|
};
|
|
244
266
|
this.process.once("exit", finish);
|
|
267
|
+
// A failed spawn never emits "exit": settle on "close" instead of waiting
|
|
268
|
+
// out the grace timers (or never, when nothing else keeps the loop alive).
|
|
269
|
+
this.process.once("close", finish);
|
|
245
270
|
this.process.kill("SIGTERM");
|
|
246
271
|
forceTimer = setTimeout(() => {
|
|
247
272
|
if (this.exited) {
|
|
@@ -181,12 +181,22 @@ function resolveRuntimeExecutable(command, env = process.env) {
|
|
|
181
181
|
}
|
|
182
182
|
function classifyRuntimePrerequisite(runtime, error, executable) {
|
|
183
183
|
const reason = (error instanceof Error ? error.message : String(error)).replace(/[\r\n]+/g, " ").slice(0, 500);
|
|
184
|
-
if (/ENOENT|not found|no such file|spawn .* failed/i.test(reason))
|
|
184
|
+
if (/ENOENT|not found|no such file|spawn .* failed/i.test(reason)) {
|
|
185
|
+
// A resolved executable that still exists on disk is not a missing install:
|
|
186
|
+
// the spawn failure comes from the launch environment (working directory,
|
|
187
|
+
// interpreter, permissions). Keep the raw reason instead of sending the
|
|
188
|
+
// user to reinstall the runtime.
|
|
189
|
+
if (executable && node_fs_1.default.existsSync(executable))
|
|
190
|
+
return {
|
|
191
|
+
runtime, state: "unavailable", executable, reason,
|
|
192
|
+
nextAction: `Verify the ${runtime} launch environment (working directory, interpreter, permissions), then retry.`,
|
|
193
|
+
};
|
|
185
194
|
return {
|
|
186
195
|
runtime, state: "missing", ...(executable ? { executable } : {}),
|
|
187
196
|
reason: `${runtime} is not installed`,
|
|
188
197
|
nextAction: runtimeInstallNextAction(runtime),
|
|
189
198
|
};
|
|
199
|
+
}
|
|
190
200
|
if (/no authenticated available models|login|credential|unauthenticated|unauthorized|oauth/i.test(reason))
|
|
191
201
|
return {
|
|
192
202
|
runtime, state: "unauthenticated", ...(executable ? { executable } : {}), reason,
|
|
@@ -128,12 +128,22 @@ export function resolveRuntimeExecutable(command, env = process.env) {
|
|
|
128
128
|
}
|
|
129
129
|
export function classifyRuntimePrerequisite(runtime, error, executable) {
|
|
130
130
|
const reason = (error instanceof Error ? error.message : String(error)).replace(/[\r\n]+/g, " ").slice(0, 500);
|
|
131
|
-
if (/ENOENT|not found|no such file|spawn .* failed/i.test(reason))
|
|
131
|
+
if (/ENOENT|not found|no such file|spawn .* failed/i.test(reason)) {
|
|
132
|
+
// A resolved executable that still exists on disk is not a missing install:
|
|
133
|
+
// the spawn failure comes from the launch environment (working directory,
|
|
134
|
+
// interpreter, permissions). Keep the raw reason instead of sending the
|
|
135
|
+
// user to reinstall the runtime.
|
|
136
|
+
if (executable && fs.existsSync(executable))
|
|
137
|
+
return {
|
|
138
|
+
runtime, state: "unavailable", executable, reason,
|
|
139
|
+
nextAction: `Verify the ${runtime} launch environment (working directory, interpreter, permissions), then retry.`,
|
|
140
|
+
};
|
|
132
141
|
return {
|
|
133
142
|
runtime, state: "missing", ...(executable ? { executable } : {}),
|
|
134
143
|
reason: `${runtime} is not installed`,
|
|
135
144
|
nextAction: runtimeInstallNextAction(runtime),
|
|
136
145
|
};
|
|
146
|
+
}
|
|
137
147
|
if (/no authenticated available models|login|credential|unauthenticated|unauthorized|oauth/i.test(reason))
|
|
138
148
|
return {
|
|
139
149
|
runtime, state: "unauthenticated", ...(executable ? { executable } : {}), reason,
|