openzoo 0.48.26 → 0.48.27

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 +94 -3
  2. package/package.json +1 -1
package/lib/proxy.js CHANGED
@@ -245,6 +245,91 @@ function replayPut(key, data, settle) {
245
245
  * to sending the original body untouched — caching must never break a call.
246
246
  * Returns null (send as-is) or { body, contextId, hash, corpus, reused, savedBytes }.
247
247
  */
248
+ /** Flatten one Anthropic content block to text leCore can index. */
249
+ function blockText(b) {
250
+ if (typeof b === 'string') return b;
251
+ if (!b || typeof b !== 'object') return '';
252
+ if (b.type === 'text') return b.text || '';
253
+ if (b.type === 'tool_use') return `[tool_use ${b.name}] ${JSON.stringify(b.input ?? {})}`;
254
+ if (b.type === 'tool_result') {
255
+ const c = b.content;
256
+ return `[tool_result] ${typeof c === 'string' ? c : (Array.isArray(c) ? c.map(blockText).join('\n') : JSON.stringify(c ?? ''))}`;
257
+ }
258
+ if (b.type === 'thinking') return ''; // never bind reasoning traces
259
+ return '';
260
+ }
261
+
262
+ function msgText(m) {
263
+ const c = m?.content;
264
+ const body = typeof c === 'string' ? c : (Array.isArray(c) ? c.map(blockText).filter(Boolean).join('\n') : '');
265
+ return body ? `${(m.role || '?').toUpperCase()}: ${body}` : '';
266
+ }
267
+
268
+ /**
269
+ * Spill the OLD prefix of a long TRANSCRIPT into leCore.
270
+ *
271
+ * WHY THIS EXISTS. The only spill was the corpus+question shape below, which
272
+ * requires the last message to be one big string ending in `\n\n<question>` —
273
+ * true for zoo_ask, never true for an agent. `npx openzoo claude` therefore
274
+ * spilled NOTHING, hit Claude Code's own context ceiling, and auto-compacted,
275
+ * on a product whose pitch is that it does not have to. Compaction was honest
276
+ * given nothing was offloaded; this is what makes the claim true.
277
+ *
278
+ * Runs on the OpenAI shape ON PURPOSE. /v1/messages is translated by
279
+ * anthropicToOpenAI and rewritten to /v1/chat/completions BEFORE this is
280
+ * reached, so operating here covers Claude Code, Cursor and the raw API with
281
+ * one implementation instead of three that can drift.
282
+ *
283
+ * THE CUT POINT IS NOT NEGOTIABLE. An assistant `tool_calls` must be answered
284
+ * by role:"tool" messages or the upstream 400s, so the transcript may only be
285
+ * severed at a plain `user` message — everything before one is self-contained.
286
+ * A system message is never spilled: it is the operating contract, not history.
287
+ */
288
+ async function spillTranscript(body, log) {
289
+ const msgs = Array.isArray(body?.messages) ? body.messages : null;
290
+ if (!msgs || msgs.length < 6) return null;
291
+
292
+ // Keep the recent tail, but never more than half the transcript: a fixed 8 on
293
+ // a 10-message body left only index 2 to search, which is rarely a user turn,
294
+ // so a SHORT-but-huge transcript (one giant tool_result) silently never
295
+ // spilled — the exact case an agent hits first.
296
+ const keepTail = Math.min(
297
+ Number(process.env.OPENZOO_KEEP_TAIL_MSGS || 8),
298
+ Math.max(2, Math.floor(msgs.length / 2)),
299
+ );
300
+ const firstSpillable = msgs.findIndex((m) => m?.role !== 'system');
301
+ if (firstSpillable < 0) return null;
302
+
303
+ let cut = -1;
304
+ for (let i = msgs.length - keepTail; i > firstSpillable; i--) {
305
+ if (msgs[i]?.role === 'user') { cut = i; break; }
306
+ }
307
+ if (cut <= firstSpillable) return null; // nothing safely severable
308
+
309
+ const head = msgs.slice(0, firstSpillable); // system block, always kept
310
+ const corpus = msgs.slice(firstSpillable, cut).map(msgText).filter(Boolean).join('\n\n');
311
+ if (corpus.length <= BIND_MIN_CHARS) return null;
312
+
313
+ const bind = await bindCorpus(corpus, {
314
+ onStage: (stage, info) => {
315
+ if (stage === 'binding') log(`binding ${mb(info.bytes)}MB of transcript to holographic memory...`);
316
+ },
317
+ });
318
+ const sent = msgs.length - cut;
319
+ log(bind.reused
320
+ ? `transcript prefix already bound (${bind.contextId}) — sending ${sent}/${msgs.length} turns`
321
+ : `transcript prefix bound (${mb(bind.bytes)}MB → ${bind.contextId}) — sending ${sent}/${msgs.length} turns`);
322
+
323
+ return {
324
+ body: Buffer.from(JSON.stringify({ ...body, messages: [...head, ...msgs.slice(cut)] })),
325
+ contextId: bind.contextId,
326
+ hash: bind.hash,
327
+ corpus,
328
+ reused: bind.reused,
329
+ savedBytes: bind.bytes,
330
+ };
331
+ }
332
+
248
333
  async function maybeCacheCorpus(req, bodyBuf, log) {
249
334
  if (contextCacheDisabled()) return null;
250
335
  if (req.method !== 'POST' || !(req.url || '').includes('/chat/completions')) return null;
@@ -254,13 +339,19 @@ async function maybeCacheCorpus(req, bodyBuf, log) {
254
339
  try { body = JSON.parse(bodyBuf.toString('utf8')); } catch { return null; }
255
340
  const msgs = Array.isArray(body?.messages) ? body.messages : null;
256
341
  if (!msgs?.length) return null;
342
+ // CORPUS+QUESTION first — one huge final message ending in \n\n<ask>. That is
343
+ // what zoo_ask and the chat surface send, and binding exactly that body keeps
344
+ // the ask verbatim. Anything else (an agent transcript) falls through to the
345
+ // transcript spill, which used to be a silent no-op.
257
346
  const last = msgs[msgs.length - 1];
258
- if (typeof last?.content !== 'string' || last.content.length <= BIND_MIN_CHARS) return null;
347
+ const oneShot = typeof last?.content === 'string'
348
+ && last.content.length > BIND_MIN_CHARS
349
+ && last.content.lastIndexOf('\n\n') >= BIND_MIN_CHARS;
350
+ if (!oneShot) return spillTranscript(body, log);
259
351
  const cut = last.content.lastIndexOf('\n\n');
260
- if (cut < BIND_MIN_CHARS) return null;
261
352
  const corpus = last.content.slice(0, cut);
262
353
  const ask = last.content.slice(cut + 2).trim();
263
- if (!ask || ask.length > 8000) return null;
354
+ if (!ask || ask.length > 8000) return spillTranscript(body, log);
264
355
 
265
356
  const bind = await bindCorpus(corpus, {
266
357
  onStage: (stage, info) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.48.26",
3
+ "version": "0.48.27",
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",