jaitus 0.3.10 → 0.3.12

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jaitus",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "description": "Jaitus — a coding agent in your terminal.",
5
5
  "type": "module",
6
6
  "bin": {
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: JSON.parse(JSON.stringify(messages)),
54
+ messages: snap,
37
55
  files,
38
56
  });
39
57
  if (checkpoints.length > 60) checkpoints.shift();