openzoo 0.48.64 → 0.48.65

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 +51 -0
  2. package/package.json +1 -1
package/lib/proxy.js CHANGED
@@ -123,6 +123,51 @@ const boundFiles = new Set();
123
123
  // basis must reflect what the corpus actually holds, not just this turn's slice.
124
124
  const boundChars = new Map();
125
125
 
126
+ // THIS MAP SURVIVES A RESTART, OR THE COUNTERFACTUAL DOES NOT.
127
+ //
128
+ // boundChars is the only record of how large a bound corpus has GROWN. It feeds
129
+ // `x-hrr-corpus-chars`, which is what lets the gateway price the counterfactual
130
+ // against the whole corpus instead of against the turn in front of it. Held
131
+ // purely in memory, every restart silently reset that basis to the live body and
132
+ // savings fell to exactly 1.00x until a session re-accumulated — with nothing in
133
+ // the log to say why, because the corpus in the DAEMON was still there. Only our
134
+ // accounting of it was gone.
135
+ //
136
+ // MEASURED minutes after a restart: `basis 16112 tok vs sent 16112 -> 4238 ·
137
+ // billed 1.14063 direct 1.14063`. The body spilled 3.8x and the bill did not
138
+ // move, because basis fell back to `cached.corpus?.length`.
139
+ //
140
+ // Advisory, never load-bearing: a corrupt or missing file just means we start
141
+ // counting again, which is exactly today's behaviour.
142
+ const BOUND_CHARS_FILE = path.join(os.homedir(), '.openzoo', 'bound-chars.json');
143
+ const BOUND_CHARS_MAX = 500; // newest contexts only; this is a ledger, not a log
144
+
145
+ let boundCharsRestored = 0;
146
+ try {
147
+ const saved = JSON.parse(fs.readFileSync(BOUND_CHARS_FILE, 'utf8'));
148
+ for (const [ctx, chars] of Object.entries(saved)) {
149
+ if (typeof chars === 'number' && chars > 0) boundChars.set(ctx, chars);
150
+ }
151
+ boundCharsRestored = boundChars.size;
152
+ } catch { /* absent or unreadable — start empty, same as before */ }
153
+
154
+ let boundCharsTimer = null;
155
+ /** Debounced: appends land in bursts, and the basis only has to survive a
156
+ * restart, not every keystroke. Never rejects — persistence is a nicety. */
157
+ const persistBoundChars = () => {
158
+ if (boundCharsTimer) return;
159
+ boundCharsTimer = setTimeout(() => {
160
+ boundCharsTimer = null;
161
+ try {
162
+ // Map preserves insertion order, so the tail is the newest.
163
+ const entries = [...boundChars.entries()].slice(-BOUND_CHARS_MAX);
164
+ mkdirSync(path.dirname(BOUND_CHARS_FILE), { recursive: true });
165
+ fs.writeFileSync(BOUND_CHARS_FILE, JSON.stringify(Object.fromEntries(entries)));
166
+ } catch { /* advisory */ }
167
+ }, 2000);
168
+ boundCharsTimer.unref?.(); // must never hold the process open
169
+ };
170
+
126
171
  /**
127
172
  * Every fundable balance across all three chains, for the startup line and
128
173
  * the live refresh. Each read is independent and advisory — one lagging RPC
@@ -567,6 +612,7 @@ async function spillTranscript(body, log, req) {
567
612
  }).then((b) => {
568
613
  if (!b?.contextId) return;
569
614
  boundChars.set(b.contextId, (boundChars.get(b.contextId) || 0) + files.length);
615
+ persistBoundChars();
570
616
  if (!known) spillMemo.set(sessionKey, { corpus, contextId: b.contextId, hash: b.hash });
571
617
  }).catch((e) => log(`file bind failed: ${e.message}`));
572
618
  }
@@ -639,6 +685,7 @@ async function spillTranscript(body, log, req) {
639
685
  // uploads each version exactly once no matter how often the agent re-reads it.
640
686
  if (files) {
641
687
  boundChars.set(bind.contextId, (boundChars.get(bind.contextId) || corpus.length) + files.length);
688
+ persistBoundChars();
642
689
  void bindCorpus(files, {
643
690
  appendTo: bind.contextId,
644
691
  onStage: (stage, info) => {
@@ -1853,6 +1900,10 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1853
1900
  process.once('SIGTERM', () => { bye(); process.exit(0); });
1854
1901
  process.once('exit', bye);
1855
1902
  log('');
1903
+ if (boundCharsRestored) {
1904
+ const tot = [...boundChars.values()].reduce((a, b) => a + b, 0);
1905
+ log(`corpus ledger restored: ${boundCharsRestored} context(s), ${mb(tot)}MB bound — counterfactual survives restarts`);
1906
+ }
1856
1907
  log('cloud IDE / remote harness? use the public URL (they cannot reach localhost):');
1857
1908
  log(` base_url = ${url}/v1`);
1858
1909
  log(` api_key = ${token}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.48.64",
3
+ "version": "0.48.65",
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",