openzoo 0.48.70 → 0.48.71
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/launch.js +7 -2
- package/lib/proxy.js +19 -12
- package/lib/spill.js +180 -107
- package/package.json +1 -1
package/lib/launch.js
CHANGED
|
@@ -230,11 +230,16 @@ export async function launchClaude(argv) {
|
|
|
230
230
|
+ 'const sp=j.spilled||{};'
|
|
231
231
|
+ 'const tk=Number(sp.tokensApprox)||0;'
|
|
232
232
|
+ 'const ht=tk>=1e6?(tk/1e6).toFixed(1)+"M":tk>=1e3?Math.round(tk/1e3)+"k":String(tk);'
|
|
233
|
+
+ 'const paid=Number(j.paidCalls)||0;'
|
|
233
234
|
+ 'const sc=Number(sp.calls)||0;'
|
|
234
235
|
+ 'const fb=Number(sp.fileBinds)||0;'
|
|
236
|
+
+ 'const rb=Number(sp.reusedBinds)||0;'
|
|
237
|
+
+ 'const ls=sp.lastSend||{};'
|
|
235
238
|
+ 'const bits=[];'
|
|
236
|
-
+ '
|
|
237
|
-
+ 'if(
|
|
239
|
+
+ 'bits.push("spilled "+sc+"/"+paid+" calls");'
|
|
240
|
+
+ 'if(rb)bits.push(rb+" reused");'
|
|
241
|
+
+ 'bits.push(fb+" filebind");'
|
|
242
|
+
+ 'if(ls.sent!=null&&ls.msgs!=null)bits.push("sending "+ls.sent+"/"+ls.msgs);'
|
|
238
243
|
+ 'if(tk)bits.push(ht+" tok offloaded");'
|
|
239
244
|
+ 'const spill=bits.length?(" \\u00b7 "+bits.join(" \\u00b7 ")):"";'
|
|
240
245
|
// "how can I easily see what credit i'm left" — asked by a user who
|
package/lib/proxy.js
CHANGED
|
@@ -13,7 +13,7 @@ import { tokenBalance } from './x402.js';
|
|
|
13
13
|
import { evmTokenBalance } from './evm.js';
|
|
14
14
|
import { bindCorpus, contextCacheDisabled, BIND_MIN_CHARS } from './hrr.js';
|
|
15
15
|
import {
|
|
16
|
-
loadBoundChars, noteCorpusLedger, filesForCorpus, createSpillStats,
|
|
16
|
+
loadBoundChars, noteCorpusLedger, filesForCorpus, createSpillStats, corpusCharsForSend,
|
|
17
17
|
} from './spill.js';
|
|
18
18
|
import { maybeRewriteModel, rewritablePath, augmentModelList, ALIAS_IDS } from './models.js';
|
|
19
19
|
import { forgetContext } from './contexts.js';
|
|
@@ -395,7 +395,7 @@ async function spillTranscript(body, log, req, stats) {
|
|
|
395
395
|
void bindCorpus(files, {
|
|
396
396
|
appendTo,
|
|
397
397
|
onStage: (stage, info) => {
|
|
398
|
-
if (stage === 'binding') log(`binding ${mb(info.bytes)}MB of FILES (${label})`);
|
|
398
|
+
if (stage === 'binding') log(`binding ${info.bytes} bytes (${mb(info.bytes)}MB) of FILES (${label})`);
|
|
399
399
|
},
|
|
400
400
|
}).then((b) => {
|
|
401
401
|
if (!b?.contextId) return;
|
|
@@ -721,7 +721,7 @@ async function spillTranscript(body, log, req, stats) {
|
|
|
721
721
|
void bindCorpus(files, {
|
|
722
722
|
appendTo: bind.contextId,
|
|
723
723
|
onStage: (stage, info) => {
|
|
724
|
-
if (stage === 'binding') log(`appending ${mb(info.bytes)}MB of FILES to ${bind.contextId} (background)`);
|
|
724
|
+
if (stage === 'binding') log(`appending ${info.bytes} bytes (${mb(info.bytes)}MB) of FILES to ${bind.contextId} (background)`);
|
|
725
725
|
},
|
|
726
726
|
}).catch((e) => log(`file append failed (corpus lags one turn): ${e.message}`));
|
|
727
727
|
}
|
|
@@ -776,6 +776,8 @@ async function spillTranscript(body, log, req, stats) {
|
|
|
776
776
|
corpus,
|
|
777
777
|
reused: bind.reused,
|
|
778
778
|
savedBytes: bind.bytes,
|
|
779
|
+
sent,
|
|
780
|
+
msgs: msgs.length,
|
|
779
781
|
};
|
|
780
782
|
}
|
|
781
783
|
|
|
@@ -837,6 +839,8 @@ async function maybeCacheCorpus(req, bodyBuf, log, stats) {
|
|
|
837
839
|
corpus,
|
|
838
840
|
reused: bind.reused,
|
|
839
841
|
savedBytes: bind.bytes,
|
|
842
|
+
sent: 1,
|
|
843
|
+
msgs: msgs.length,
|
|
840
844
|
};
|
|
841
845
|
}
|
|
842
846
|
|
|
@@ -900,6 +904,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
900
904
|
// instead of re-sent, and nothing measured it — the status line showed spend
|
|
901
905
|
// and call count, which is the cost side with none of the benefit.
|
|
902
906
|
const spill = createSpillStats();
|
|
907
|
+
let lastSpillSend = null;
|
|
903
908
|
// Spend/direct for ONLY the calls that spilled. The session-wide savingX
|
|
904
909
|
// averages these with every small turn that had nothing to offload, so it
|
|
905
910
|
// slides toward 1.0 as a conversation grows — which reads as the mechanism
|
|
@@ -1044,13 +1049,14 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
1044
1049
|
reachedVia: viaTunnel ? 'public tunnel' : 'localhost',
|
|
1045
1050
|
publicTunnel: tunnelGate?.publicUrl ? `${tunnelGate.publicUrl}/v1` : null,
|
|
1046
1051
|
servedRequests,
|
|
1047
|
-
spilled: {
|
|
1048
|
-
...
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1052
|
+
spilled: (() => {
|
|
1053
|
+
const ledgerTotal = [...boundChars.values()].reduce((a, b) => a + b, 0);
|
|
1054
|
+
return {
|
|
1055
|
+
...spill.snapshot({ boundChars: ledgerTotal }),
|
|
1056
|
+
boundChars: ledgerTotal,
|
|
1057
|
+
lastSend: lastSpillSend,
|
|
1058
|
+
};
|
|
1059
|
+
})(),
|
|
1054
1060
|
spendUsd: sessionSpent,
|
|
1055
1061
|
creditUsd,
|
|
1056
1062
|
// WHAT THE SAME CALLS WOULD HAVE COST DIRECT. Spend on its own is a
|
|
@@ -1504,7 +1510,8 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
1504
1510
|
if (cached) {
|
|
1505
1511
|
spill.noteSpill({ corpusChars: cached.corpus?.length || 0, reused: cached.reused });
|
|
1506
1512
|
didSpill = true;
|
|
1507
|
-
|
|
1513
|
+
if (cached.sent != null) lastSpillSend = { sent: cached.sent, msgs: cached.msgs };
|
|
1514
|
+
result = await send(cached.body, cached.contextId, cached.topK, corpusCharsForSend(boundChars, cached.contextId, cached.corpus?.length));
|
|
1508
1515
|
// Sidecar wiped between runs: the gateway 404s BEFORE the 402 (nothing
|
|
1509
1516
|
// paid). Never fail on a stale manifest — re-bind once and retry.
|
|
1510
1517
|
if (result.response.status === 404) {
|
|
@@ -1513,7 +1520,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
1513
1520
|
log('bound context is gone on the zoo — re-binding once...');
|
|
1514
1521
|
forgetContext(config.apiBase, cached.hash);
|
|
1515
1522
|
const rebound = await bindCorpus(cached.corpus, { force: true });
|
|
1516
|
-
result = await send(cached.body, rebound.contextId, cached.topK, boundChars
|
|
1523
|
+
result = await send(cached.body, rebound.contextId, cached.topK, corpusCharsForSend(boundChars, rebound.contextId, cached.corpus?.length));
|
|
1517
1524
|
} else {
|
|
1518
1525
|
res.writeHead(404, { 'content-type': 'application/json' });
|
|
1519
1526
|
res.end(text);
|
package/lib/spill.js
CHANGED
|
@@ -94,24 +94,28 @@ export function accumulateBoundChars(boundChars, contextId, chars, opts = {}) {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
97
|
+
* Conversation chars use Math.max so a stale smaller ledger (the 34056
|
|
98
|
+
* files-only row) cannot cap a later, larger prefix. New file bytes add on top.
|
|
99
99
|
*
|
|
100
|
-
*
|
|
101
|
-
* delta; file bytes are added on top either way.
|
|
100
|
+
* next = max(prev, corpusChars) + fileChars
|
|
102
101
|
*/
|
|
103
102
|
export function noteCorpusLedger(boundChars, {
|
|
104
|
-
contextId,
|
|
103
|
+
contextId, corpusChars = 0, fileChars = 0, ...opts
|
|
105
104
|
} = {}) {
|
|
106
105
|
if (!contextId) return 0;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
const prev = boundChars.get(contextId) || 0;
|
|
107
|
+
const next = Math.max(prev, Number(corpusChars) || 0) + (Number(fileChars) || 0);
|
|
108
|
+
boundChars.set(contextId, next);
|
|
109
|
+
if (opts.sessionKey && opts.sessions) {
|
|
110
|
+
opts.sessions.set(opts.sessionKey, { contextId, chars: next });
|
|
111
111
|
}
|
|
112
|
-
if (fileChars) accumulateBoundChars(boundChars, contextId, fileChars, { ...opts, persist: false });
|
|
113
112
|
persistBoundChars(boundChars, opts);
|
|
114
|
-
return
|
|
113
|
+
return next;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** send() must not let `stale || thisTurn` pick the smaller number. */
|
|
117
|
+
export function corpusCharsForSend(boundChars, contextId, thisTurn) {
|
|
118
|
+
return Math.max(boundChars.get(contextId) || 0, thisTurn || 0);
|
|
115
119
|
}
|
|
116
120
|
|
|
117
121
|
/** Expand ~ and resolve relative paths against cwd. Returns null if unusable. */
|
|
@@ -144,92 +148,140 @@ function parseArgs(args) {
|
|
|
144
148
|
return {};
|
|
145
149
|
}
|
|
146
150
|
|
|
147
|
-
function
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
if (
|
|
151
|
-
const t = value.trim();
|
|
152
|
-
if ((t.startsWith('{') || t.startsWith('[')) && t.length < 100_000) {
|
|
153
|
-
try { collectFromValue(JSON.parse(t), out, depth + 1); } catch { /* not json */ }
|
|
154
|
-
}
|
|
155
|
-
return;
|
|
151
|
+
function collectStructuredPaths(args, out) {
|
|
152
|
+
if (!args || typeof args !== 'object') return;
|
|
153
|
+
for (const k of PATH_KEYS) {
|
|
154
|
+
if (typeof args[k] === 'string' && args[k]) out.push(args[k]);
|
|
156
155
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
156
|
+
for (const k of PATH_ARRAY_KEYS) {
|
|
157
|
+
if (!Array.isArray(args[k])) continue;
|
|
158
|
+
for (const x of args[k]) {
|
|
159
|
+
if (typeof x === 'string') out.push(x);
|
|
160
|
+
else if (x && typeof x === 'object') collectStructuredPaths(x, out);
|
|
161
|
+
}
|
|
160
162
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const CWD_HINT = /(?:current working directory is[:\s]+|<cwd>\s*|cwd:\s+)([^\s<]+)/i;
|
|
166
|
+
|
|
167
|
+
export function parseCwdHint(text) {
|
|
168
|
+
if (typeof text !== 'string' || !text) return null;
|
|
169
|
+
const m = text.match(CWD_HINT);
|
|
170
|
+
if (!m) return null;
|
|
171
|
+
const p = m[1].trim();
|
|
172
|
+
return path.isAbsolute(p) ? p : null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Pull path-like tokens out of a Bash `command` string.
|
|
177
|
+
* `head -80 programs/README.md` and `cat /abs/file` both count; bare `ls` does not.
|
|
178
|
+
*/
|
|
179
|
+
export function extractBashPaths(command, cwd = process.cwd()) {
|
|
180
|
+
const found = [];
|
|
181
|
+
if (typeof command !== 'string' || !command) return { paths: found, cwd };
|
|
182
|
+
let localCwd = cwd;
|
|
183
|
+
for (const part of command.split(/(?:&&|\|\||;|\n)/)) {
|
|
184
|
+
const cd = part.match(/^\s*cd\s+(?:\/[dD]\s+)?(['"]?)(.+?)\1\s*$/);
|
|
185
|
+
if (cd) {
|
|
186
|
+
const dest = resolveReadablePath(cd[2].trim(), localCwd);
|
|
187
|
+
if (dest) localCwd = dest;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
for (const m of part.matchAll(/(['"])([^'"]+)\1/g)) {
|
|
191
|
+
const t = m[2].trim();
|
|
192
|
+
if (looksLikePath(t) || path.isAbsolute(t)) found.push({ raw: t, cwd: localCwd });
|
|
193
|
+
}
|
|
194
|
+
for (const tok of part.split(/\s+/)) {
|
|
195
|
+
const t = tok.replace(/^[`'"]|[`'"]$/g, '');
|
|
196
|
+
if (!t || t.startsWith('-') || t.startsWith('$') || t === '.' || t === '..') continue;
|
|
197
|
+
if (looksLikePath(t) || path.isAbsolute(t)) found.push({ raw: t, cwd: localCwd });
|
|
171
198
|
}
|
|
172
199
|
}
|
|
200
|
+
return { paths: found, cwd: localCwd };
|
|
173
201
|
}
|
|
174
202
|
|
|
175
203
|
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
204
|
+
* Live Claude Code msgs are OpenAI-shaped (spill runs AFTER anthropicToOpenAI).
|
|
205
|
+
* Read/Edit/Write land on tool_calls[].function.arguments as a JSON string
|
|
206
|
+
* {file_path:"/abs/..."}. Bash is {command:"head -80 programs/README.md"}.
|
|
207
|
+
* Do not expect Read tool_result to carry the path. Do not harvest import
|
|
208
|
+
* paths out of tool_result bodies.
|
|
181
209
|
*/
|
|
182
|
-
export function
|
|
183
|
-
const
|
|
184
|
-
|
|
210
|
+
export function extractFileCandidates(msgs, { cwd = process.cwd() } = {}) {
|
|
211
|
+
const structured = [];
|
|
212
|
+
const bash = [];
|
|
213
|
+
let currentCwd = cwd;
|
|
214
|
+
if (!Array.isArray(msgs)) return { structured, bash, cwd: currentCwd };
|
|
185
215
|
for (const m of msgs) {
|
|
186
216
|
if (!m || typeof m !== 'object') continue;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (
|
|
190
|
-
if (b.input) collectFromValue(b.input, raw, 0);
|
|
191
|
-
for (const k of PATH_KEYS) {
|
|
192
|
-
if (typeof b[k] === 'string') raw.push(b[k]);
|
|
193
|
-
}
|
|
194
|
-
// tool_result: only structured content, never a long body string
|
|
195
|
-
if (b.type === 'tool_result' && b.content && typeof b.content === 'object') {
|
|
196
|
-
collectFromValue(b.content, raw, 0);
|
|
197
|
-
} else if (b.type === 'tool_result' && typeof b.content === 'string' && b.content.length < 512 && looksLikePath(b.content)) {
|
|
198
|
-
raw.push(b.content.trim());
|
|
199
|
-
}
|
|
217
|
+
if (m.role === 'tool' && typeof m.content === 'string') {
|
|
218
|
+
const hint = parseCwdHint(m.content);
|
|
219
|
+
if (hint) currentCwd = hint;
|
|
200
220
|
}
|
|
201
221
|
const calls = [
|
|
202
222
|
...(Array.isArray(m.tool_calls) ? m.tool_calls : []),
|
|
203
223
|
...(m.function_call ? [m.function_call] : []),
|
|
204
224
|
];
|
|
205
225
|
for (const c of calls) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
226
|
+
const args = parseArgs(c?.function?.arguments ?? c?.arguments);
|
|
227
|
+
const fromArgs = [];
|
|
228
|
+
collectStructuredPaths(args, fromArgs);
|
|
229
|
+
for (const raw of fromArgs) structured.push({ raw, cwd: currentCwd });
|
|
230
|
+
if (typeof args.command === 'string') {
|
|
231
|
+
const got = extractBashPaths(args.command, currentCwd);
|
|
232
|
+
for (const p of got.paths) bash.push({ ...p, bash: true });
|
|
233
|
+
currentCwd = got.cwd;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Harmless leftover: pre-conversion Anthropic tool_use. Live Claude Code
|
|
237
|
+
// never has this by the time spillTranscript runs.
|
|
238
|
+
const blocks = Array.isArray(m.content) ? m.content : [];
|
|
239
|
+
for (const b of blocks) {
|
|
240
|
+
if (b?.input) {
|
|
241
|
+
const fromInput = [];
|
|
242
|
+
collectStructuredPaths(b.input, fromInput);
|
|
243
|
+
for (const raw of fromInput) structured.push({ raw, cwd: currentCwd });
|
|
244
|
+
if (typeof b.input.command === 'string') {
|
|
245
|
+
const got = extractBashPaths(b.input.command, currentCwd);
|
|
246
|
+
for (const p of got.paths) bash.push({ ...p, bash: true });
|
|
247
|
+
currentCwd = got.cwd;
|
|
248
|
+
}
|
|
210
249
|
}
|
|
211
250
|
}
|
|
212
251
|
}
|
|
252
|
+
return { structured, bash, cwd: currentCwd };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function extractFilePaths(msgs, opts) {
|
|
256
|
+
const { structured, bash } = extractFileCandidates(msgs, opts);
|
|
213
257
|
const seen = new Set();
|
|
214
258
|
const out = [];
|
|
215
|
-
for (const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
seen.
|
|
220
|
-
|
|
259
|
+
for (const item of [...structured, ...bash]) {
|
|
260
|
+
const t = typeof item === 'string' ? item : item?.raw;
|
|
261
|
+
if (typeof t !== 'string') continue;
|
|
262
|
+
const s = t.trim();
|
|
263
|
+
if (!s || seen.has(s)) continue;
|
|
264
|
+
seen.add(s);
|
|
265
|
+
out.push(s);
|
|
221
266
|
}
|
|
222
267
|
return out;
|
|
223
268
|
}
|
|
224
269
|
|
|
270
|
+
const SKIP_DIR_NAMES = new Set(['node_modules', '.git', 'dist', 'build', '__pycache__', '.venv', 'target']);
|
|
271
|
+
|
|
272
|
+
function fileBindLog({ kept, bytes, enoent, cap, dir, rel, bash }) {
|
|
273
|
+
return `file-bind kept=${kept} bytes=${bytes} skip enoent=${enoent} cap=${cap} dir=${dir} rel=${rel} bash=${bash}`;
|
|
274
|
+
}
|
|
275
|
+
|
|
225
276
|
/**
|
|
226
277
|
* Read every new file the agent touched and return the corpus slice to bind.
|
|
227
278
|
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
279
|
+
* Live path is OpenAI tool_calls (Read/Edit/Write + Bash command). Relative
|
|
280
|
+
* paths resolve against cwd / last "current working directory is …" hint.
|
|
281
|
+
* Directories expand to children that are files under the cap.
|
|
230
282
|
*
|
|
231
|
-
* Always
|
|
232
|
-
* so
|
|
283
|
+
* Always logs `file-bind kept=N bytes=B skip enoent=X cap=Y dir=Z rel=W bash=K`
|
|
284
|
+
* so grep-for-FILES is no longer the only signal. Bytes, not 0.0MB.
|
|
233
285
|
*/
|
|
234
286
|
export function filesForCorpus(msgs, {
|
|
235
287
|
boundFiles,
|
|
@@ -239,53 +291,72 @@ export function filesForCorpus(msgs, {
|
|
|
239
291
|
log = () => {},
|
|
240
292
|
statSync = (p) => fs.statSync(p),
|
|
241
293
|
readFileSync = (p) => fs.readFileSync(p, 'utf8'),
|
|
294
|
+
readdirSync = (p) => fs.readdirSync(p),
|
|
242
295
|
} = {}) {
|
|
296
|
+
const empty = { kept: 0, bytes: 0, enoent: 0, cap: 0, dir: 0, rel: 0, bash: 0 };
|
|
243
297
|
if (disabled) {
|
|
244
|
-
log(
|
|
245
|
-
return { text: '', files: 0, bytes: 0, reason: 'disabled' };
|
|
246
|
-
}
|
|
247
|
-
const paths = extractFilePaths(msgs);
|
|
248
|
-
if (!paths.length) {
|
|
249
|
-
log('file-bind 0 because no file paths in tool_use / tool_calls');
|
|
250
|
-
return { text: '', files: 0, bytes: 0, reason: 'no-paths' };
|
|
298
|
+
log(fileBindLog(empty));
|
|
299
|
+
return { text: '', files: 0, bytes: 0, reason: 'disabled', ...empty };
|
|
251
300
|
}
|
|
301
|
+
const { structured, bash } = extractFileCandidates(msgs, { cwd });
|
|
302
|
+
const candidates = [
|
|
303
|
+
...structured.map((p) => ({ ...p, bash: false })),
|
|
304
|
+
...bash.map((p) => ({ ...p, bash: true })),
|
|
305
|
+
];
|
|
306
|
+
const skip = { enoent: 0, cap: 0, dir: 0, rel: 0 };
|
|
252
307
|
const seen = new Set();
|
|
253
308
|
const chunks = [];
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (
|
|
261
|
-
|
|
262
|
-
|
|
309
|
+
|
|
310
|
+
const tryBind = (abs) => {
|
|
311
|
+
if (!abs || seen.has(abs)) return;
|
|
312
|
+
seen.add(abs);
|
|
313
|
+
let st;
|
|
314
|
+
try { st = statSync(abs); } catch { skip.enoent += 1; return; }
|
|
315
|
+
if (st.isDirectory()) {
|
|
316
|
+
skip.dir += 1;
|
|
317
|
+
let kids = [];
|
|
318
|
+
try { kids = readdirSync(abs); } catch { skip.enoent += 1; return; }
|
|
319
|
+
for (const name of kids) {
|
|
320
|
+
if (!name || name.startsWith('.') || SKIP_DIR_NAMES.has(name)) continue;
|
|
321
|
+
const kid = path.join(abs, name);
|
|
322
|
+
let ks;
|
|
323
|
+
try { ks = statSync(kid); } catch { skip.enoent += 1; continue; }
|
|
324
|
+
if (ks.isFile()) tryBind(kid);
|
|
325
|
+
}
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (!st.isFile()) { skip.enoent += 1; return; }
|
|
329
|
+
if (st.size > cap) { skip.cap += 1; return; }
|
|
330
|
+
const key = `${abs}:${st.mtimeMs}`;
|
|
331
|
+
if (boundFiles?.has(key)) return;
|
|
332
|
+
boundFiles?.add(key);
|
|
263
333
|
try {
|
|
264
|
-
|
|
265
|
-
if (!st.isFile()) { skippedMissing += 1; continue; }
|
|
266
|
-
if (st.size > cap) { skippedCap += 1; continue; }
|
|
267
|
-
const key = `${p}:${st.mtimeMs}`;
|
|
268
|
-
if (boundFiles?.has(key)) { skippedBound += 1; continue; }
|
|
269
|
-
boundFiles?.add(key);
|
|
270
|
-
chunks.push(`FILE ${p}\n${readFileSync(p)}`);
|
|
334
|
+
chunks.push(`FILE ${abs}\n${readFileSync(abs)}`);
|
|
271
335
|
} catch {
|
|
272
|
-
|
|
336
|
+
skip.enoent += 1;
|
|
273
337
|
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
for (const item of candidates) {
|
|
341
|
+
const raw = item.raw;
|
|
342
|
+
if (!path.isAbsolute(raw) && !(raw.startsWith('~/') || raw === '~')) skip.rel += 1;
|
|
343
|
+
const abs = resolveReadablePath(raw, item.cwd || cwd);
|
|
344
|
+
if (!abs) { skip.enoent += 1; continue; }
|
|
345
|
+
tryBind(abs);
|
|
274
346
|
}
|
|
275
|
-
|
|
276
|
-
const why = skippedBound && !skippedMissing && !skippedCap
|
|
277
|
-
? 'already bound'
|
|
278
|
-
: skippedCap && !skippedMissing
|
|
279
|
-
? `over OPENZOO_BIND_FILE_MAX (${cap})`
|
|
280
|
-
: skippedMissing
|
|
281
|
-
? 'unreadable or not a file'
|
|
282
|
-
: 'paths did not resolve';
|
|
283
|
-
log(`file-bind 0 because ${why}`);
|
|
284
|
-
return { text: '', files: 0, bytes: 0, reason: why };
|
|
285
|
-
}
|
|
347
|
+
|
|
286
348
|
const text = chunks.join('\n\n');
|
|
287
|
-
|
|
288
|
-
|
|
349
|
+
const stats = {
|
|
350
|
+
kept: chunks.length,
|
|
351
|
+
bytes: text.length,
|
|
352
|
+
enoent: skip.enoent,
|
|
353
|
+
cap: skip.cap,
|
|
354
|
+
dir: skip.dir,
|
|
355
|
+
rel: skip.rel,
|
|
356
|
+
bash: bash.length,
|
|
357
|
+
};
|
|
358
|
+
log(fileBindLog(stats));
|
|
359
|
+
return { text, files: chunks.length, bytes: text.length, reason: chunks.length ? null : 'none-kept', ...stats };
|
|
289
360
|
}
|
|
290
361
|
|
|
291
362
|
/** Session counters the HUD reads off /v1/info. */
|
|
@@ -309,11 +380,13 @@ export function createSpillStats() {
|
|
|
309
380
|
this.fileBindBytes += bytes;
|
|
310
381
|
this.spilledChars += bytes;
|
|
311
382
|
},
|
|
312
|
-
snapshot() {
|
|
383
|
+
snapshot({ boundChars = null } = {}) {
|
|
384
|
+
const unique = boundChars == null ? this.spilledChars : boundChars;
|
|
313
385
|
return {
|
|
314
386
|
calls: this.spillCalls,
|
|
315
387
|
chars: this.spilledChars,
|
|
316
|
-
|
|
388
|
+
// Unique bound size — do not re-add the same prefix on every reuse.
|
|
389
|
+
tokensApprox: Math.round(unique / 4),
|
|
317
390
|
reusedBinds: this.spillReuses,
|
|
318
391
|
fileBinds: this.fileBinds,
|
|
319
392
|
fileBindBytes: this.fileBindBytes,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.48.
|
|
3
|
+
"version": "0.48.71",
|
|
4
4
|
"description": "Local x402-paying proxy + MCP server for openzoo.fun \u2014 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",
|