jaitus 0.3.9 → 0.3.11
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 +14 -0
- package/src/index.js +7 -2
- package/src/rewind.js +19 -1
package/package.json
CHANGED
package/src/agent/loop.js
CHANGED
|
@@ -353,6 +353,13 @@ async function streamTurn({ client, messages, model, system, tools, onText, onTo
|
|
|
353
353
|
}
|
|
354
354
|
};
|
|
355
355
|
let buffer = '';
|
|
356
|
+
// A stream that never ends must die here, not in GC hell: the buffer below grows
|
|
357
|
+
// with every chunk and each arrival re-splits all of it, so a runaway provider
|
|
358
|
+
// (twelve silent minutes and counting, seen in the wild) grows memory
|
|
359
|
+
// quadratically until V8 falls over near 4 GB. Past the cap the turn ends as a
|
|
360
|
+
// cut (continued from what arrived) or, with nothing received, as a retryable
|
|
361
|
+
// timeout — never as a crash.
|
|
362
|
+
const MAX_STREAM_BUFFER = 8 * 1024 * 1024;
|
|
356
363
|
// A failure reported INSIDE the stream, after a 200. Recorded here and thrown after
|
|
357
364
|
// the parse below: handle() runs inside a try whose catch exists to skip keepalives,
|
|
358
365
|
// so throwing from it would be swallowed — which is how a mid-stream error used to
|
|
@@ -412,6 +419,13 @@ async function streamTurn({ client, messages, model, system, tools, onText, onTo
|
|
|
412
419
|
try {
|
|
413
420
|
for await (const chunk of res.body) {
|
|
414
421
|
buffer += decoder.decode(chunk, { stream: true });
|
|
422
|
+
if (buffer.length > MAX_STREAM_BUFFER) {
|
|
423
|
+
// Breaking the loop releases the reader (the socket is cancelled), so the
|
|
424
|
+
// runaway stops arriving too — not just stops being kept.
|
|
425
|
+
if (!blocks.size) throw new JaitusError({ code: 'timeout', status: 504, message: 'Jaitus took too long to respond. Try again.' });
|
|
426
|
+
cut = true;
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
415
429
|
const frames = buffer.split('\n\n');
|
|
416
430
|
buffer = frames.pop() || '';
|
|
417
431
|
for (const frame of frames) {
|
package/src/index.js
CHANGED
|
@@ -1181,7 +1181,8 @@ async function session({ flags }) {
|
|
|
1181
1181
|
ledger.note('compacted', `The conversation was automatically summarised to free context: ${dropped} messages became one summary. Detail you are not shown above is genuinely gone — ask or re-read rather than assuming.`);
|
|
1182
1182
|
} catch (error) {
|
|
1183
1183
|
// Not fatal: the turn can still run, it just runs closer to the ceiling.
|
|
1184
|
-
|
|
1184
|
+
const id = isJaitusError(error) && error.requestId ? ` (request id ${error.requestId})` : '';
|
|
1185
|
+
tui.notice?.(`Auto-compaction failed (${error.message}${id}) — continuing without it. /compact retries.`, 'warn');
|
|
1185
1186
|
ledger.note('compact-failed', `Automatic context compaction failed (${error.message}); the conversation is close to the context limit.`);
|
|
1186
1187
|
} finally {
|
|
1187
1188
|
tui.clearStatus?.();
|
|
@@ -1830,7 +1831,11 @@ async function slashCommand(verb, args, ctx) {
|
|
|
1830
1831
|
ok('Conversation compacted. Context freed.');
|
|
1831
1832
|
try { ctx.resetContext?.(); } catch { /* header keeps last value */ }
|
|
1832
1833
|
} catch (e) {
|
|
1833
|
-
|
|
1834
|
+
// The request id is the only thing that ties this failure to a backend log
|
|
1835
|
+
// line. fail() takes a string, so a JaitusError passed bare would shed it —
|
|
1836
|
+
// and "where do I get the request id" is exactly what that produces.
|
|
1837
|
+
const id = isJaitusError(e) && e.requestId ? ` (request id ${e.requestId})` : '';
|
|
1838
|
+
fail(`Compact failed: ${e.message}${id}`);
|
|
1834
1839
|
} finally {
|
|
1835
1840
|
spin.stop();
|
|
1836
1841
|
}
|
package/src/rewind.js
CHANGED
|
@@ -29,11 +29,29 @@ export function createJournal() {
|
|
|
29
29
|
return {
|
|
30
30
|
/** Snapshot the conversation before a user prompt is processed. */
|
|
31
31
|
checkpoint(messages, files = []) {
|
|
32
|
+
// Checkpoints are full deep clones with NO size cap (only file snapshots are
|
|
33
|
+
// capped above) — with pasted images in history, 60 checkpoints of megabytes
|
|
34
|
+
// of base64 each is a straight road to heap death. Past the cap the CHECKPOINT
|
|
35
|
+
// copy keeps its structure but drops the pixels (live messages are untouched):
|
|
36
|
+
// rewinding across an image restores a note, not the pixels. Undo stays usable
|
|
37
|
+
// instead of OOMing the session.
|
|
38
|
+
const MAX_CHECKPOINT_BYTES = 4_000_000;
|
|
39
|
+
let snap;
|
|
40
|
+
try {
|
|
41
|
+
const raw = JSON.stringify(messages);
|
|
42
|
+
snap = raw.length > MAX_CHECKPOINT_BYTES
|
|
43
|
+
? JSON.parse(JSON.stringify(messages, (key, value) =>
|
|
44
|
+
(key === 'data' && typeof value === 'string' && value.length > 100_000
|
|
45
|
+
? '[image data omitted from checkpoint]' : value)))
|
|
46
|
+
: JSON.parse(raw);
|
|
47
|
+
} catch {
|
|
48
|
+
snap = [];
|
|
49
|
+
}
|
|
32
50
|
checkpoints.push({
|
|
33
51
|
seq: ++seqCounter,
|
|
34
52
|
at: new Date().toISOString(),
|
|
35
53
|
prompt: messages.filter((m) => m.role === 'user' && typeof m.content === 'string').at(-1)?.content?.slice(0, 120) || '',
|
|
36
|
-
messages:
|
|
54
|
+
messages: snap,
|
|
37
55
|
files,
|
|
38
56
|
});
|
|
39
57
|
if (checkpoints.length > 60) checkpoints.shift();
|