jaitus 0.3.10 → 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/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/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();
|