openzoo 0.48.57 → 0.48.59

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.
Files changed (2) hide show
  1. package/lib/proxy.js +83 -4
  2. package/package.json +1 -1
package/lib/proxy.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { readFileSync, appendFileSync, mkdirSync } from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
+ import fs from 'node:fs';
4
5
  import http from 'node:http';
5
6
  import crypto from 'node:crypto';
6
7
  import { Readable } from 'node:stream';
@@ -115,6 +116,9 @@ const mb = (n) => (n / 1048576).toFixed(1);
115
116
 
116
117
  // anchor -> { corpus, contextId, hash } for append-only transcript spills
117
118
  const spillMemo = new Map();
119
+ // path:mtime of every file already bound — a file is bound once per version,
120
+ // never re-uploaded because the agent read it again.
121
+ const boundFiles = new Set();
118
122
 
119
123
  /**
120
124
  * Every fundable balance across all three chains, for the startup line and
@@ -411,10 +415,31 @@ async function spillTranscript(body, log, req) {
411
415
  // for a bug rather than for coherence. 6 turns still covers "what did I just
412
416
  // do" — the case retrieval cannot answer, because the model does not know to
413
417
  // query for it — and hands the rest back as saving.
418
+ // COUNT CONVERSATION TURNS, NOT MESSAGES.
419
+ //
420
+ // A tool round trip is TWO messages (assistant tool_call + tool result), so a
421
+ // floor of 6 messages is three tool calls and nothing else — the agent loses
422
+ // the human turn that started the run and every decision it made along the
423
+ // way. OBSERVED: "it's when he calls a bunch of tools, he gets lost."
424
+ //
425
+ // So the floor is measured in user/assistant turns and tool traffic rides
426
+ // along for free. A tool-heavy stretch therefore widens the window instead of
427
+ // consuming it, which is the opposite of the old behaviour and the whole
428
+ // point: what the agent needs verbatim is what it DID, and doing things is
429
+ // exactly what fills the window with tool messages.
414
430
  const minTurns = Number(process.env.OPENZOO_TAIL_MIN_TURNS || 6);
415
- if (msgs.length - cut < minTurns) {
416
- for (let i = Math.max(firstSpillable + 1, msgs.length - minTurns); i > firstSpillable; i--) {
417
- if (severable(i)) { cut = i; break; }
431
+ const realTurns = (from) => {
432
+ let n = 0;
433
+ for (let i = from; i < msgs.length; i++) {
434
+ const r = msgs[i]?.role;
435
+ if (r === 'user' || r === 'assistant') n += 1;
436
+ }
437
+ return n;
438
+ };
439
+ if (realTurns(cut) < minTurns) {
440
+ for (let i = cut - 1; i > firstSpillable; i--) {
441
+ if (severable(i) && realTurns(i) >= minTurns) { cut = i; break; }
442
+ if (i === firstSpillable + 1) { if (severable(i)) cut = i; break; }
418
443
  }
419
444
  }
420
445
 
@@ -442,7 +467,61 @@ async function spillTranscript(body, log, req) {
442
467
  if (lastUser > firstSpillable && cut > lastUser) cut = lastUser;
443
468
 
444
469
  const head = msgs.slice(0, firstSpillable); // system block, always kept
445
- const corpus = msgs.slice(firstSpillable, cut).map(msgText).filter(Boolean).join('\n\n');
470
+ // EVERY FILE THE AGENT TOUCHED, AT FULL SIZE.
471
+ //
472
+ // The saving ratio is corpus/sent, so on a fresh session — where the corpus
473
+ // IS the conversation — it starts near 1x and only climbs as you talk. That
474
+ // is exactly what the live agent numbers showed: 1.0-1.3x early, 8x once a
475
+ // 149k-token history existed. MEASURED the same day: prompt compressed 2.67x
476
+ // but billed savings was 1.13x, because output does not compress and a small
477
+ // corpus leaves nothing to compress on the other side either.
478
+ //
479
+ // Files are the fix. An agent reads far more bytes than it discusses, the
480
+ // harness has usually TRUNCATED them on the way in, and the full text is
481
+ // sitting on this machine. Binding it makes the corpus large immediately
482
+ // instead of eventually, and makes the truncated read whole again.
483
+ //
484
+ // Read-only, bounded, deduped by path+mtime, and failures are silent: this
485
+ // runs on the request path and must never be the reason a turn does not go.
486
+ const filesForCorpus = () => {
487
+ if (process.env.OPENZOO_BIND_FILES === '0') return '';
488
+ const cap = Number(process.env.OPENZOO_BIND_FILE_MAX || 400_000);
489
+ const out = [];
490
+ const seen = new Set();
491
+ for (const m of msgs) {
492
+ const blocks = Array.isArray(m?.content) ? m.content : [];
493
+ const calls = Array.isArray(m?.tool_calls) ? m.tool_calls : [];
494
+ const paths = [];
495
+ for (const b of blocks) {
496
+ const p = b?.input?.file_path || b?.input?.path;
497
+ if (typeof p === 'string') paths.push(p);
498
+ }
499
+ for (const c of calls) {
500
+ try {
501
+ const a = JSON.parse(c?.function?.arguments || '{}');
502
+ if (typeof a.file_path === 'string') paths.push(a.file_path);
503
+ else if (typeof a.path === 'string') paths.push(a.path);
504
+ } catch { /* not json args */ }
505
+ }
506
+ for (const p of paths) {
507
+ if (seen.has(p) || !path.isAbsolute(p)) continue;
508
+ seen.add(p);
509
+ try {
510
+ const st = fs.statSync(p);
511
+ if (!st.isFile() || st.size > cap) continue;
512
+ const key = `${p}:${st.mtimeMs}`;
513
+ if (boundFiles.has(key)) continue;
514
+ boundFiles.add(key);
515
+ out.push(`FILE ${p}\n${fs.readFileSync(p, 'utf8')}`);
516
+ } catch { /* unreadable, gone, or binary — simply not corpus */ }
517
+ }
518
+ }
519
+ return out.join('\n\n');
520
+ };
521
+
522
+ const turns = msgs.slice(firstSpillable, cut).map(msgText).filter(Boolean).join('\n\n');
523
+ const files = filesForCorpus();
524
+ const corpus = files ? `${turns}\n\n${files}` : turns;
446
525
  if (corpus.length <= BIND_MIN_CHARS) return null;
447
526
 
448
527
  // CONTINUE THE CONTEXT, BIND ONLY THE DELTA.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.48.57",
3
+ "version": "0.48.59",
4
4
  "description": "Local x402-paying proxy + MCP server for openzoo.fun — point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental.",
5
5
  "license": "MIT",
6
6
  "type": "module",