openzoo 0.48.34 → 0.48.36

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/lib/hrr.js CHANGED
@@ -26,7 +26,7 @@ export const contextCacheDisabled = () => process.env.OPENZOO_NO_CONTEXT_CACHE =
26
26
  * Ensure `corpus` is bound on the zoo. Returns
27
27
  * { contextId, hash, reused, bytes } — reused=true means zero bytes shipped.
28
28
  */
29
- export async function bindCorpus(corpus, { onStage, force = false } = {}) {
29
+ export async function bindCorpus(corpus, { onStage, force = false, appendTo = null } = {}) {
30
30
  const hash = corpusHash(corpus);
31
31
  const bytes = Buffer.byteLength(corpus);
32
32
  if (!force) {
@@ -37,7 +37,11 @@ export async function bindCorpus(corpus, { onStage, force = false } = {}) {
37
37
  }
38
38
  }
39
39
  onStage?.('binding', { bytes });
40
- const r = await postWithUploadSignal(`${config.apiBase}/v1/hrr/bind`, JSON.stringify({ corpus }), {
40
+ // APPEND, don't re-upload. The gateway takes context_id on /v1/hrr/bind and
41
+ // adds to that context — which is the difference between paying for the
42
+ // delta and paying for the whole transcript again on every single turn.
43
+ const r = await postWithUploadSignal(`${config.apiBase}/v1/hrr/bind`,
44
+ JSON.stringify(appendTo ? { corpus, context_id: appendTo } : { corpus }), {
41
45
  headers: withNamespace(),
42
46
  onUploaded: () => onStage?.('bound-uploading-done', { bytes }),
43
47
  });
package/lib/proxy.js CHANGED
@@ -90,6 +90,9 @@ function jsonErr(res, status, message, extraFields = {}) {
90
90
 
91
91
  const mb = (n) => (n / 1048576).toFixed(1);
92
92
 
93
+ // anchor -> { corpus, contextId, hash } for append-only transcript spills
94
+ const spillMemo = new Map();
95
+
93
96
  /**
94
97
  * Every fundable balance across all three chains, for the startup line and
95
98
  * the live refresh. Each read is independent and advisory — one lagging RPC
@@ -365,11 +368,50 @@ async function spillTranscript(body, log) {
365
368
  const corpus = msgs.slice(firstSpillable, cut).map(msgText).filter(Boolean).join('\n\n');
366
369
  if (corpus.length <= BIND_MIN_CHARS) return null;
367
370
 
368
- const bind = await bindCorpus(corpus, {
369
- onStage: (stage, info) => {
370
- if (stage === 'binding') log(`binding ${mb(info.bytes)}MB of transcript to holographic memory...`);
371
- },
372
- });
371
+ // CONTINUE THE CONTEXT, BIND ONLY THE DELTA.
372
+ //
373
+ // A transcript grows by one message per turn, so the whole-corpus hash misses
374
+ // every time and the old code re-uploaded the ENTIRE prefix on every single
375
+ // turn — OBSERVED live: 0.4MB bound three turns running, a fresh context id
376
+ // each time, while only a few KB was actually new. Bind cost grew with
377
+ // conversation length and was re-paid per message.
378
+ //
379
+ // The corpus is append-only: each turn's corpus starts with the previous
380
+ // one. So when it does, send just the tail and keep the same context_id.
381
+ // Anchored on the FIRST 2KB, which is stable for the life of a conversation
382
+ // and distinguishes concurrent ones.
383
+ const anchor = corpus.slice(0, 2048);
384
+ const prior = spillMemo.get(anchor);
385
+ let bind;
386
+ if (prior && corpus.startsWith(prior.corpus) && corpus.length > prior.corpus.length) {
387
+ const delta = corpus.slice(prior.corpus.length);
388
+ // FIRE AND FORGET. This delta is history for FUTURE turns — the answer
389
+ // being generated right now is served from the tail plus what is already
390
+ // bound, so waiting on the upload buys nothing and costs the user the
391
+ // round trip on every single turn. The context id is already known, so
392
+ // nothing is lost by not waiting for it.
393
+ //
394
+ // The FIRST bind is deliberately NOT async: the request must carry
395
+ // x-hrr-context, and that id does not exist until the bind returns. Firing
396
+ // that one off would send the opening turn with no context at all — a
397
+ // silently worse answer traded for a shorter pause, which is the wrong way
398
+ // round.
399
+ void bindCorpus(delta, {
400
+ appendTo: prior.contextId,
401
+ onStage: (stage, info) => {
402
+ if (stage === 'binding') log(`appending ${mb(info.bytes)}MB to ${prior.contextId} (delta, background)`);
403
+ },
404
+ }).catch((e) => log(`append failed (history may lag one turn): ${e.message}`));
405
+ bind = { contextId: prior.contextId, hash: prior.hash, reused: true, bytes: delta.length };
406
+ } else {
407
+ bind = await bindCorpus(corpus, {
408
+ onStage: (stage, info) => {
409
+ if (stage === 'binding') log(`binding ${mb(info.bytes)}MB of transcript to holographic memory...`);
410
+ },
411
+ });
412
+ }
413
+ spillMemo.set(anchor, { corpus, contextId: bind.contextId, hash: bind.hash });
414
+ if (spillMemo.size > 32) spillMemo.delete(spillMemo.keys().next().value);
373
415
  const sent = msgs.length - cut;
374
416
  log(bind.reused
375
417
  ? `transcript prefix already bound (${bind.contextId}) — sending ${sent}/${msgs.length} turns`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.48.34",
3
+ "version": "0.48.36",
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",