jaitus 0.3.8 → 0.3.9
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/package.json +1 -1
- package/src/agent/loop.js +7 -2
- package/src/api.js +64 -5
package/package.json
CHANGED
package/src/agent/loop.js
CHANGED
|
@@ -31,7 +31,7 @@ import { loadContext, loadIgnore, CONTEXT_FILE } from '../project.js';
|
|
|
31
31
|
import { c, out, write, spinner, glyph } from '../ui.js';
|
|
32
32
|
import { discoverMcpTools } from '../mcp.js';
|
|
33
33
|
import { unifiedDiff } from '../diff.js';
|
|
34
|
-
import { fromResponse, fromGatewayBody } from '../errors.js';
|
|
34
|
+
import { fromResponse, fromGatewayBody, JaitusError } from '../errors.js';
|
|
35
35
|
|
|
36
36
|
const MAX_ITERATIONS = 1000;
|
|
37
37
|
const STUCK_REPEATS = 4;
|
|
@@ -425,7 +425,12 @@ async function streamTurn({ client, messages, model, system, tools, onText, onTo
|
|
|
425
425
|
if (streamError) break;
|
|
426
426
|
}
|
|
427
427
|
} catch (e) {
|
|
428
|
-
|
|
428
|
+
// Esc is the only abort that reads as an interruption. Any OTHER AbortError —
|
|
429
|
+
// the request timeout in api.js firing mid-body, a dropped socket — is a `timeout`
|
|
430
|
+
// JaitusError so the agent loop backs off and retries it (TRANSIENT_CODES) instead
|
|
431
|
+
// of reporting "Interrupted." for something the user never stopped.
|
|
432
|
+
if (signal?.aborted) throw e;
|
|
433
|
+
if (e?.name === 'AbortError') throw new JaitusError({ code: 'timeout', status: 504, message: 'Jaitus took too long to respond. Try again.' });
|
|
429
434
|
// The connection dropped mid-answer. What arrived is kept and continued from; with
|
|
430
435
|
// nothing received yet, the caller retries the request instead.
|
|
431
436
|
if (!blocks.size) throw e;
|
package/src/api.js
CHANGED
|
@@ -10,6 +10,47 @@ import { fromGatewayBody, fromNetwork, JaitusError } from './errors.js';
|
|
|
10
10
|
|
|
11
11
|
export class NotSignedIn extends Error {}
|
|
12
12
|
|
|
13
|
+
// fetch() has no timeout of its own, and the gateway's worst case is minutes of
|
|
14
|
+
// serial failover across provider keys — so a sick window used to pin the CLI on
|
|
15
|
+
// "Working..." forever. Every call below carries its own ceiling: slow becomes a
|
|
16
|
+
// `timeout` JaitusError (retryable by the agent loop, shown with a request id)
|
|
17
|
+
// instead of eternity. The caller's signal (Esc) still wins: it aborts through the
|
|
18
|
+
// same controller, and only a genuine user abort reads as one.
|
|
19
|
+
const MESSAGES_TIMEOUT_MS = 10 * 60 * 1000; // streamed turns can legitimately run minutes
|
|
20
|
+
const API_TIMEOUT_MS = 30 * 1000; // dashboard calls are small JSON
|
|
21
|
+
|
|
22
|
+
// Combine an optional caller signal with a timeout into one signal.
|
|
23
|
+
// Manual combine (not AbortSignal.any): .any needs Node 20.3+, this works everywhere.
|
|
24
|
+
function withTimeout(signal, ms) {
|
|
25
|
+
const ctrl = new AbortController();
|
|
26
|
+
let timedOut = false;
|
|
27
|
+
const timer = setTimeout(() => {
|
|
28
|
+
timedOut = true;
|
|
29
|
+
try { ctrl.abort(new DOMException('Request timed out', 'TimeoutError')); }
|
|
30
|
+
catch { try { ctrl.abort(); } catch { /* already gone */ } }
|
|
31
|
+
}, ms);
|
|
32
|
+
if (timer.unref) timer.unref();
|
|
33
|
+
const onAbort = () => {
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
try { ctrl.abort(signal.reason); } catch { try { ctrl.abort(); } catch { /* already gone */ } }
|
|
36
|
+
};
|
|
37
|
+
if (signal) {
|
|
38
|
+
if (signal.aborted) onAbort();
|
|
39
|
+
else signal.addEventListener('abort', onAbort, { once: true });
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
signal: ctrl.signal,
|
|
43
|
+
// True only when OUR timer fired. A user abort also lands here as an AbortError,
|
|
44
|
+
// but signal.aborted is set then — never mistake Esc for a timeout.
|
|
45
|
+
timedOut: () => timedOut && !signal?.aborted,
|
|
46
|
+
cleanup: () => { clearTimeout(timer); signal?.removeEventListener?.('abort', onAbort); },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function timeoutError() {
|
|
51
|
+
return new JaitusError({ code: 'timeout', status: 504, message: 'Jaitus took too long to respond. Try again.' });
|
|
52
|
+
}
|
|
53
|
+
|
|
13
54
|
// The context window the gateway last advertised, or null before it has been asked.
|
|
14
55
|
let contextWindowSeen = null;
|
|
15
56
|
export const advertisedContextWindow = () => contextWindowSeen;
|
|
@@ -28,45 +69,63 @@ export function createClient() {
|
|
|
28
69
|
|
|
29
70
|
/** POST /v1/messages. Returns the raw Response so the caller can stream it. */
|
|
30
71
|
async messages(body, { signal } = {}) {
|
|
72
|
+
// The timer is deliberately NOT cleared when headers arrive: a stall can happen
|
|
73
|
+
// mid-body too (headers fast, tokens never come), and only the stream reader ends
|
|
74
|
+
// that. Aborting a finished request is harmless; the timer never holds the
|
|
75
|
+
// process open (unref). 10 minutes: streamed agentic turns legitimately run long.
|
|
76
|
+
const t = withTimeout(signal, MESSAGES_TIMEOUT_MS);
|
|
31
77
|
try {
|
|
32
78
|
return await withAuth((token) => fetch(`${baseUrl}/v1/messages`, {
|
|
33
79
|
method: 'POST',
|
|
34
80
|
headers: headers(token),
|
|
35
81
|
body: JSON.stringify(body),
|
|
36
|
-
signal,
|
|
82
|
+
signal: t.signal,
|
|
37
83
|
}));
|
|
38
84
|
} catch (cause) {
|
|
39
|
-
if (cause?.name === 'JaitusError'
|
|
85
|
+
if (cause?.name === 'JaitusError') throw cause;
|
|
86
|
+
if (t.timedOut()) throw timeoutError();
|
|
87
|
+
if (cause?.name === 'AbortError') throw cause;
|
|
40
88
|
throw fromNetwork(cause);
|
|
41
89
|
}
|
|
42
90
|
},
|
|
43
91
|
|
|
44
92
|
/** A dashboard API call, returning parsed JSON. */
|
|
45
93
|
async api(pathname, init = {}) {
|
|
94
|
+
const t = withTimeout(init.signal, API_TIMEOUT_MS);
|
|
46
95
|
let res;
|
|
47
96
|
try {
|
|
48
97
|
res = await withAuth((token) => fetch(`${baseUrl}/api${pathname}`, {
|
|
49
98
|
...init,
|
|
50
99
|
headers: { ...headers(token), ...(init.headers || {}) },
|
|
100
|
+
signal: t.signal,
|
|
51
101
|
}));
|
|
102
|
+
const body = await res.json().catch(() => ({}));
|
|
103
|
+
return { ok: res.ok, status: res.status, body };
|
|
52
104
|
} catch (cause) {
|
|
53
105
|
if (cause?.name === 'JaitusError') throw cause;
|
|
106
|
+
if (t.timedOut()) throw timeoutError();
|
|
107
|
+
if (cause?.name === 'AbortError') throw cause;
|
|
54
108
|
throw fromNetwork(cause);
|
|
109
|
+
} finally {
|
|
110
|
+
t.cleanup();
|
|
55
111
|
}
|
|
56
|
-
const body = await res.json().catch(() => ({}));
|
|
57
|
-
return { ok: res.ok, status: res.status, body };
|
|
58
112
|
},
|
|
59
113
|
|
|
60
114
|
/** Model ids the gateway advertises. */
|
|
61
115
|
async models() {
|
|
116
|
+
const t = withTimeout(undefined, API_TIMEOUT_MS);
|
|
62
117
|
let res;
|
|
63
118
|
try {
|
|
64
|
-
res = await withAuth((token) => fetch(`${baseUrl}/v1/models`, { headers: headers(token) }));
|
|
119
|
+
res = await withAuth((token) => fetch(`${baseUrl}/v1/models`, { headers: headers(token), signal: t.signal }));
|
|
65
120
|
} catch (cause) {
|
|
121
|
+
t.cleanup();
|
|
66
122
|
if (cause?.name === 'JaitusError') throw cause;
|
|
123
|
+
if (t.timedOut()) throw timeoutError();
|
|
124
|
+
if (cause?.name === 'AbortError') throw cause;
|
|
67
125
|
throw fromNetwork(cause);
|
|
68
126
|
}
|
|
69
127
|
const body = await res.json().catch(() => ({}));
|
|
128
|
+
t.cleanup();
|
|
70
129
|
if (!res.ok) throw fromGatewayBody(res.status, body, { requestId: res.headers.get('x-request-id') || '' });
|
|
71
130
|
const models = (body?.data || []).map((m) => m?.id).filter(Boolean);
|
|
72
131
|
// How much conversation fits, as the gateway computed it for this account.
|