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.
- package/lib/proxy.js +51 -0
- 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.
|
|
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",
|