openzoo 0.49.3 → 0.49.5
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/README.md +1 -1
- package/lib/grokui.mjs +272 -21
- package/lib/livestatus.js +173 -4
- package/lib/pay.js +2 -1
- package/lib/podagent.mjs +65 -14
- package/lib/proxy.js +3 -1
- package/lib/racesettle.js +56 -0
- package/lib/wrap.js +88 -29
- package/lib/x402.js +11 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -187,7 +187,7 @@ All three rails have settled real payments (2026-08-14):
|
|
|
187
187
|
|
|
188
188
|
| rail | network | status |
|
|
189
189
|
|---|---|---|
|
|
190
|
-
| **Solana** (default) | `solana:5eykt…` | **live** — Token-2022 `TransferChecked`, partial-signed, gateway pays fees. Settles daily; tested end-to-end against the production 402. Settlement uses a wrapped
|
|
190
|
+
| **Solana** (default) | `solana:5eykt…` | **live** — Token-2022 payment `TransferChecked`, partial-signed, gateway pays fees. Settles daily; tested end-to-end against the production 402. Settlement uses a wrapped mint as internal plumbing; you only ever hold and send USDC or TOKEN. Funding wrap is a 9-account ix (program pulls the deposit; old 5-account wrap is rejected `0x6a`). |
|
|
191
191
|
| Base | `eip155:8453` | **live** — standard x402 EIP-3009 `transferWithAuthorization` against native USDC, batched settle through the facilitator. Fund the wallet's EVM address with USDC on Base; nothing is converted. |
|
|
192
192
|
| Robinhood Chain | `eip155:4663` | **live** — EIP-3009, batched settle through the facilitator. Hold the plain token a row is quoted in (USDG, or the ODDBALLER / IOU / ROBINHOODS memecoins) and the shim converts exactly enough at payment time, automatically — two small on-chain steps paid from the wallet's own RH ETH. No gas? The error says exactly how much ETH to send and where. Default rail selection skips this chain unless `OPENZOO_ENABLE_RH=1`; `OPENZOO_RAIL=robinhood` forces it outright. |
|
|
193
193
|
|
package/lib/grokui.mjs
CHANGED
|
@@ -431,6 +431,7 @@ function loadThreads() {
|
|
|
431
431
|
if (t.status === 'thinking') {
|
|
432
432
|
t.status = 'idle';
|
|
433
433
|
t.liveStatus = '';
|
|
434
|
+
t.liveRace = null;
|
|
434
435
|
}
|
|
435
436
|
if (Array.isArray(t.history)) {
|
|
436
437
|
for (const h of t.history) {
|
|
@@ -1058,6 +1059,7 @@ const SLASH_COMMANDS = [
|
|
|
1058
1059
|
{ name: '/models', args: '[filter]', help: 'search the ~435 served models' },
|
|
1059
1060
|
{ name: '/tier', args: 'cheap|medium|expensive|grok 4.6', help: 'how much to spend per turn when no model is pinned' },
|
|
1060
1061
|
{ name: '/race', args: '<n> | <k> <n>', help: 'launch n models; judge the first k back (k=1 = fastest wins)' },
|
|
1062
|
+
{ name: '/sitrep', args: '', help: 'session sitrep (drawer)' },
|
|
1061
1063
|
{ name: '/compact', args: '', help: 'summarise history to shrink context' },
|
|
1062
1064
|
{ name: '/clear', args: '', help: 'wipe this thread’s history' },
|
|
1063
1065
|
{ name: '/undo', args: '', help: 'drop the last exchange' },
|
|
@@ -1150,6 +1152,7 @@ async function handleSlash(task, t) {
|
|
|
1150
1152
|
const cmd = m[1].toLowerCase();
|
|
1151
1153
|
const arg = m[2].trim();
|
|
1152
1154
|
|
|
1155
|
+
if (cmd === 'sitrep') return null; // drawer-only — never a transcript line
|
|
1153
1156
|
if (cmd === 'help') {
|
|
1154
1157
|
return 'Commands:\n'
|
|
1155
1158
|
+ SLASH_COMMANDS.map((c) => ` ${(c.name + ' ' + c.args).padEnd(26)} ${c.help}`).join('\n')
|
|
@@ -1448,6 +1451,7 @@ setInterval(() => {
|
|
|
1448
1451
|
} else {
|
|
1449
1452
|
t.status = 'idle';
|
|
1450
1453
|
t.liveStatus = '';
|
|
1454
|
+
t.liveRace = null;
|
|
1451
1455
|
unlockWorktree(t);
|
|
1452
1456
|
}
|
|
1453
1457
|
dirty = true;
|
|
@@ -2448,11 +2452,12 @@ async function mcpDirective(url, tool, args) {
|
|
|
2448
2452
|
|
|
2449
2453
|
// onEvent (optional) gets live progress for whoever's actually watching this
|
|
2450
2454
|
// call: {type:'start',name,color} when a bot begins its turn, {type:'status',
|
|
2451
|
-
// detail} while paying / waiting / racing / walking tools, {type:'
|
|
2452
|
-
//
|
|
2453
|
-
// {type:'
|
|
2454
|
-
//
|
|
2455
|
-
//
|
|
2455
|
+
// detail} while paying / waiting / racing / walking tools, {type:'race',race}
|
|
2456
|
+
// for the spectator grid (one cell per launched model + a judging beat),
|
|
2457
|
+
// {type:'delta',name,color,delta} per streamed token (replace:true swaps the
|
|
2458
|
+
// bubble once), {type:'final',name,color,text} once its full reply (or
|
|
2459
|
+
// directive ack) is settled. Background turns go through kickTurn →
|
|
2460
|
+
// emitToThread, which is a no-op if nobody has the thread open.
|
|
2456
2461
|
async function runTurn(threadId, userText, onEvent, images) {
|
|
2457
2462
|
const t = threads.get(threadId);
|
|
2458
2463
|
if (!t) return;
|
|
@@ -2469,7 +2474,8 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2469
2474
|
const paint = (ev) => {
|
|
2470
2475
|
if (!stillMine()) return;
|
|
2471
2476
|
if (ev.type === 'status' && ev.detail && t.status === 'thinking') t.liveStatus = ev.detail;
|
|
2472
|
-
if (ev.type === '
|
|
2477
|
+
if (ev.type === 'race' && ev.race && t.status === 'thinking') t.liveRace = ev.race;
|
|
2478
|
+
if (ev.type === 'delta' || ev.type === 'status' || ev.type === 'start' || ev.type === 'race') t.lastDeltaAt = Date.now();
|
|
2473
2479
|
onEvent?.(ev);
|
|
2474
2480
|
};
|
|
2475
2481
|
t.history.push(images && images.length ? { who: 'user', text: userText, images } : { who: 'user', text: userText });
|
|
@@ -2483,6 +2489,7 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2483
2489
|
lockWorktree(t);
|
|
2484
2490
|
const raceN = Math.min(Number(t.race) || 0, 4);
|
|
2485
2491
|
const raceNeed = Math.min(Math.max(Number(t.raceNeed) || 1, 1), raceN || 1);
|
|
2492
|
+
t.liveRace = null;
|
|
2486
2493
|
t.liveStatus = (!t.model && raceN >= 2) ? formatRaceStatus(0, raceNeed) : 'waiting on model…';
|
|
2487
2494
|
let chained = false;
|
|
2488
2495
|
let parked = false;
|
|
@@ -2604,6 +2611,7 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2604
2611
|
return (await brainRace(callMsgs, emit, t.contextId, models, need, undefined, emitStatus, {
|
|
2605
2612
|
signal: turnAbort.signal,
|
|
2606
2613
|
onArrivals: (arr) => { t.lastRaceFail = summarizeRaceFailures(arr); },
|
|
2614
|
+
onRace: (snap) => paint({ type: 'race', name: t.name, color: t.color, race: snap }),
|
|
2607
2615
|
tier: t.tier || 'medium',
|
|
2608
2616
|
})).trim();
|
|
2609
2617
|
}
|
|
@@ -2757,6 +2765,7 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2757
2765
|
} else if (!t.pendingRun) {
|
|
2758
2766
|
t.status = 'idle';
|
|
2759
2767
|
t.liveStatus = '';
|
|
2768
|
+
t.liveRace = null;
|
|
2760
2769
|
unlockWorktree(t);
|
|
2761
2770
|
}
|
|
2762
2771
|
}
|
|
@@ -3031,12 +3040,20 @@ const APP_HTML = `<!doctype html>
|
|
|
3031
3040
|
whose entire premise is that the box pays for itself. Click-to-copy is a
|
|
3032
3041
|
shortcut; the address is selectable so Cmd/Ctrl+C still works if copy
|
|
3033
3042
|
fails. */
|
|
3034
|
-
#walletOverlay { position: fixed; inset: 0; background: rgba(0,0,0,.66); z-index: 1200;
|
|
3043
|
+
#walletOverlay, #sitrepOverlay { position: fixed; inset: 0; background: rgba(0,0,0,.66); z-index: 1200;
|
|
3035
3044
|
display: none; align-items: center; justify-content: center; padding: 24px; }
|
|
3036
|
-
#walletOverlay.show { display: flex; }
|
|
3037
|
-
#walletBox { width: 100%; max-width: 560px; max-height: 82vh; overflow-y: auto; background: #111113;
|
|
3045
|
+
#walletOverlay.show, #sitrepOverlay.show { display: flex; }
|
|
3046
|
+
#walletBox, #sitrepBox { width: 100%; max-width: 560px; max-height: 82vh; overflow-y: auto; background: #111113;
|
|
3038
3047
|
border: 1px solid #2c2c2e; border-radius: 16px; padding: 20px 22px; }
|
|
3039
|
-
#walletBox h3 { margin: 0 0 2px; font-size: 15px; font-weight: 600; }
|
|
3048
|
+
#walletBox h3, #sitrepBox h3 { margin: 0 0 2px; font-size: 15px; font-weight: 600; }
|
|
3049
|
+
#sitrepBox { max-width: 420px; }
|
|
3050
|
+
.srow { display: flex; justify-content: space-between; align-items: baseline; gap: 14px;
|
|
3051
|
+
padding: 8px 0; border-bottom: 1px solid #1c1c1e; }
|
|
3052
|
+
.srow:last-child { border-bottom: 0; }
|
|
3053
|
+
.slab { color: #6f7080; font-size: 11px; letter-spacing: .06em; text-transform: uppercase; flex: 0 0 auto; }
|
|
3054
|
+
.sval { color: #ececec; font-size: 13px; text-align: right; word-break: break-word; min-width: 0; }
|
|
3055
|
+
.sval.hlime { color: #b8f240; }
|
|
3056
|
+
.sval.hember { color: #f28c4d; }
|
|
3040
3057
|
.wsub { color: #8e8e93; font-size: 12px; margin-bottom: 16px; }
|
|
3041
3058
|
.wrow { border: 1px solid #1c1c1e; border-radius: 12px; padding: 10px 12px; margin-bottom: 10px;
|
|
3042
3059
|
display: flex; align-items: center; gap: 10px; cursor: pointer; }
|
|
@@ -3121,7 +3138,7 @@ const APP_HTML = `<!doctype html>
|
|
|
3121
3138
|
color: #f0c9a8; font-size: 10.5px; line-height: 1.45; }
|
|
3122
3139
|
#hud .hhint.show { display: block; }
|
|
3123
3140
|
#hud .hhint b { color: #f28c4d; font-weight: 600; }
|
|
3124
|
-
#sidebar, #main, #walletOverlay, #composeOverlay,
|
|
3141
|
+
#sidebar, #main, #walletOverlay, #sitrepOverlay, #composeOverlay,
|
|
3125
3142
|
#inp, #search, #composeInp, .bubble, .md-pre, .runoutput, .runcmd {
|
|
3126
3143
|
-webkit-app-region: no-drag; }
|
|
3127
3144
|
#log { flex: 1; min-width: 0; overflow-y: auto; overflow-x: hidden; padding: 20px 24px 12px;
|
|
@@ -3223,6 +3240,46 @@ const APP_HTML = `<!doctype html>
|
|
|
3223
3240
|
@keyframes blink { 0%, 80%, 100% { opacity: .25; } 40% { opacity: 1; } }
|
|
3224
3241
|
.tstatus { color: #8e8e93; font-size: 13px; margin-left: 6px; }
|
|
3225
3242
|
.ttrail { display: block; color: #8e8e93; font-size: 12.5px; margin-top: 8px; }
|
|
3243
|
+
/* Race spectator board. Lives in the transcript, never over the header
|
|
3244
|
+
dials / wallet / cost HUD. 2×2 for four, a row for two, wrap otherwise. */
|
|
3245
|
+
.row.bot:has(.raceboard) { max-width: 92%; }
|
|
3246
|
+
.bubble.raceboard { padding: 10px 11px; background: #1a1a1d; }
|
|
3247
|
+
.racewrap { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
|
|
3248
|
+
.racecaption { color: #6f7080; font-size: 11px; letter-spacing: .04em; text-transform: uppercase; }
|
|
3249
|
+
.racegrid { display: grid; gap: 7px; grid-template-columns: 1fr 1fr; }
|
|
3250
|
+
.racegrid.n1 { grid-template-columns: 1fr; }
|
|
3251
|
+
.racegrid.n2 { grid-template-columns: 1fr 1fr; }
|
|
3252
|
+
.racegrid.n3 { grid-template-columns: 1fr 1fr; }
|
|
3253
|
+
@media (min-width: 720px) { .racegrid.n3 { grid-template-columns: 1fr 1fr 1fr; } }
|
|
3254
|
+
.racecell { border: 1px solid #2c2c32; border-radius: 12px; padding: 8px 9px 7px;
|
|
3255
|
+
background: #141416; min-width: 0; min-height: 0; transition: opacity .18s ease, border-color .18s ease; }
|
|
3256
|
+
.racecell.streaming { border-color: #3d3d4a; }
|
|
3257
|
+
.racecell.back { border-color: #2a3a18; }
|
|
3258
|
+
.racecell.failed { border-color: #3a2424; }
|
|
3259
|
+
.racecell.abandoned { opacity: .42; }
|
|
3260
|
+
.racecell.winner { border-color: #b8f240; box-shadow: 0 0 0 1px rgba(184,242,64,.28); }
|
|
3261
|
+
.racehead { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; min-width: 0; }
|
|
3262
|
+
.racename { font-size: 12px; font-weight: 600; color: #ececec; white-space: nowrap;
|
|
3263
|
+
overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
3264
|
+
.racechip { flex: 0 0 auto; font-size: 10px; letter-spacing: .03em; color: #8e8e93;
|
|
3265
|
+
border: 1px solid #2c2c32; border-radius: 999px; padding: 1px 7px; }
|
|
3266
|
+
.racecell.streaming .racechip { color: #b8f240; border-color: #3a4a18; }
|
|
3267
|
+
.racecell.back .racechip { color: #b8f240; border-color: #2a3a18; }
|
|
3268
|
+
.racecell.failed .racechip { color: #f28c4d; border-color: #5a3020; }
|
|
3269
|
+
.racecell.abandoned .racechip { color: #6f7080; }
|
|
3270
|
+
.racecell.winner .racechip { color: #0b0b0d; background: #b8f240; border-color: #b8f240; }
|
|
3271
|
+
.raceprev { font: 11.5px/1.35 ui-monospace, SFMono-Regular, Menlo, monospace; color: #9a9aa6;
|
|
3272
|
+
white-space: pre-wrap; word-break: break-word; max-height: 8.2em; overflow: hidden; }
|
|
3273
|
+
.racefail { display: inline-block; margin-top: 2px; font-size: 11px; color: #f28c4d; letter-spacing: .03em; }
|
|
3274
|
+
.racejudge { border: 1px dashed #3a4a18; border-radius: 12px; padding: 9px 11px;
|
|
3275
|
+
background: rgba(184,242,64,.05); color: #c8c8b8; }
|
|
3276
|
+
.racejudge.won { border-style: solid; border-color: #b8f240; }
|
|
3277
|
+
.racejudge-lab { font-size: 10px; letter-spacing: .06em; text-transform: uppercase; color: #b8f240;
|
|
3278
|
+
margin-bottom: 3px; }
|
|
3279
|
+
.racejudge-msg { font-size: 13px; color: #ececec; }
|
|
3280
|
+
@media (prefers-reduced-motion: reduce) {
|
|
3281
|
+
.racecell { transition: none; }
|
|
3282
|
+
}
|
|
3226
3283
|
#bar { padding: 10px 16px 18px; position: relative; }
|
|
3227
3284
|
#row-input { display: flex; align-items: center; gap: 8px; }
|
|
3228
3285
|
#plusMenu { position: absolute; bottom: 62px; left: 16px; background: #1c1c1e; border-radius: 14px;
|
|
@@ -3354,6 +3411,13 @@ const APP_HTML = `<!doctype html>
|
|
|
3354
3411
|
</div>
|
|
3355
3412
|
</div>
|
|
3356
3413
|
</div>
|
|
3414
|
+
<div id="sitrepOverlay" data-component="sitrep-drawer">
|
|
3415
|
+
<div id="sitrepBox">
|
|
3416
|
+
<h3>Sitrep</h3>
|
|
3417
|
+
<div class="wsub">This thread and this session. No keys.</div>
|
|
3418
|
+
<div id="sitrepBody">loading…</div>
|
|
3419
|
+
</div>
|
|
3420
|
+
</div>
|
|
3357
3421
|
<div id="main">
|
|
3358
3422
|
<div id="chatHeader">
|
|
3359
3423
|
<div id="chatHeaderId"></div>
|
|
@@ -3373,17 +3437,17 @@ const APP_HTML = `<!doctype html>
|
|
|
3373
3437
|
</select>
|
|
3374
3438
|
<select class="dial" id="raceSel" data-component="model-race" aria-label="Race models"
|
|
3375
3439
|
title="Ask N models from the tier at once, drawn at random — fastest real answer wins. You pay for every entrant.">
|
|
3376
|
-
<option value="0" selected>1 model
|
|
3440
|
+
<option value="0" selected>1 model 0%</option>
|
|
3377
3441
|
<optgroup label="first back wins">
|
|
3378
|
-
<option value="2">race 2
|
|
3379
|
-
<option value="3">race 3
|
|
3380
|
-
<option value="4">race 4
|
|
3442
|
+
<option value="2">race 2 −50%</option>
|
|
3443
|
+
<option value="3">race 3 −67%</option>
|
|
3444
|
+
<option value="4">race 4 −75%</option>
|
|
3381
3445
|
</optgroup>
|
|
3382
3446
|
<optgroup label="judge the first k back">
|
|
3383
|
-
<option value="2 3">best 2 of 3
|
|
3384
|
-
<option value="2 4">best 2 of 4
|
|
3385
|
-
<option value="3 4">best 3 of 4
|
|
3386
|
-
<option value="4 4">best 4 of 4
|
|
3447
|
+
<option value="2 3">best 2 of 3 −67%</option>
|
|
3448
|
+
<option value="2 4">best 2 of 4 −75%</option>
|
|
3449
|
+
<option value="3 4">best 3 of 4 −75%</option>
|
|
3450
|
+
<option value="4 4">best 4 of 4 −75%</option>
|
|
3387
3451
|
</optgroup>
|
|
3388
3452
|
</select>
|
|
3389
3453
|
<button class="dial" id="walletBtn" data-component="wallet-open"
|
|
@@ -3411,6 +3475,10 @@ const APP_HTML = `<!doctype html>
|
|
|
3411
3475
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a5 5 0 0 1-7.07-7.07l9.19-9.19a3.5 3.5 0 0 1 4.95 4.95l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
|
|
3412
3476
|
<span>Attach files</span>
|
|
3413
3477
|
</div>
|
|
3478
|
+
<div class="pop-item" id="sitrepBtn">
|
|
3479
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="3" width="14" height="18" rx="2"/><line x1="8" y1="8" x2="16" y2="8"/><line x1="8" y1="12" x2="16" y2="12"/><line x1="8" y1="16" x2="13" y2="16"/></svg>
|
|
3480
|
+
<span>Sitrep</span>
|
|
3481
|
+
</div>
|
|
3414
3482
|
</div>
|
|
3415
3483
|
<input id="fileInp" type="file" multiple style="position:absolute;width:1px;height:1px;opacity:0;pointer-events:none;">
|
|
3416
3484
|
<div id="attachChips"></div>
|
|
@@ -4068,8 +4136,73 @@ const APP_HTML = `<!doctype html>
|
|
|
4068
4136
|
walletOverlay.addEventListener('click', (e) => {
|
|
4069
4137
|
if (e.target === walletOverlay) walletOverlay.classList.remove('show');
|
|
4070
4138
|
});
|
|
4139
|
+
const sitrepOverlay = document.getElementById('sitrepOverlay');
|
|
4140
|
+
const sitrepBody = document.getElementById('sitrepBody');
|
|
4141
|
+
function raceCutPct(y) {
|
|
4142
|
+
const n = Math.max(1, Number(y) || 1);
|
|
4143
|
+
return Math.round((1 - 1 / n) * 100);
|
|
4144
|
+
}
|
|
4145
|
+
function raceChoiceLabel(y, need) {
|
|
4146
|
+
const n = Math.max(1, Number(y) || 1);
|
|
4147
|
+
const k = Math.max(1, Math.min(Number(need) || 1, n));
|
|
4148
|
+
const cut = raceCutPct(n);
|
|
4149
|
+
const cutTxt = cut === 0 ? '0%' : ('−' + cut + '%');
|
|
4150
|
+
if (n < 2) return '1 model ' + cutTxt;
|
|
4151
|
+
if (k > 1) return 'best ' + k + ' of ' + n + ' ' + cutTxt;
|
|
4152
|
+
return 'race ' + n + ' ' + cutTxt;
|
|
4153
|
+
}
|
|
4154
|
+
function sitrepRow(lab, val, cls) {
|
|
4155
|
+
return '<div class="srow"><span class="slab">' + escapeHtml(lab) + '</span><span class="sval'
|
|
4156
|
+
+ (cls ? (' ' + cls) : '') + '">' + escapeHtml(val) + '</span></div>';
|
|
4157
|
+
}
|
|
4158
|
+
async function openSitrep() {
|
|
4159
|
+
sitrepOverlay.classList.add('show');
|
|
4160
|
+
sitrepBody.textContent = 'loading…';
|
|
4161
|
+
const t = knownThreads.find((x) => x.id === activeId) || {};
|
|
4162
|
+
let full = null;
|
|
4163
|
+
try { if (activeId) full = await (await fetch(API + '/threads/' + activeId)).json(); } catch (e) { full = null; }
|
|
4164
|
+
let you = {};
|
|
4165
|
+
try { you = await (await fetch(API + '/hud-summary')).json(); } catch (e) { you = {}; }
|
|
4166
|
+
const y = Number(t.race) || 0;
|
|
4167
|
+
const need = Number(t.raceNeed) || 1;
|
|
4168
|
+
const raceY = y >= 2 ? y : 1;
|
|
4169
|
+
const spent = Number(you.spentUsd) || 0;
|
|
4170
|
+
const cogs = Number(you.cogsUsd) || 0;
|
|
4171
|
+
const direct = Number(you.directUsd) || 0;
|
|
4172
|
+
const mult = spent > 0 ? direct / spent : null;
|
|
4173
|
+
const saved = mult == null ? '—'
|
|
4174
|
+
: ((mult >= 100 ? Math.round(mult) : mult.toFixed(mult >= 10 ? 1 : 2)) + 'x');
|
|
4175
|
+
const savedCls = mult == null ? '' : (mult >= 1 ? 'hlime' : 'hember');
|
|
4176
|
+
const thinking = (full && full.status === 'thinking') || t.status === 'thinking';
|
|
4177
|
+
const race = (full && full.liveRace) || null;
|
|
4178
|
+
let flight = 'idle';
|
|
4179
|
+
if (thinking && race && race.phase === 'judging') flight = 'classifier judging';
|
|
4180
|
+
else if (thinking && race && race.phase === 'winner') flight = 'winner';
|
|
4181
|
+
else if (thinking && (full && full.liveStatus)) flight = full.liveStatus;
|
|
4182
|
+
else if (thinking && t.liveStatus) flight = t.liveStatus;
|
|
4183
|
+
else if (thinking) flight = 'in flight';
|
|
4184
|
+
const cwd = (full && full.dir) || t.dir || '—';
|
|
4185
|
+
sitrepBody.innerHTML =
|
|
4186
|
+
sitrepRow('race', raceChoiceLabel(raceY, need))
|
|
4187
|
+
+ sitrepRow('band', t.tier || 'medium')
|
|
4188
|
+
+ sitrepRow('mode', t.runMode || 'ask')
|
|
4189
|
+
+ sitrepRow('cwd', cwd)
|
|
4190
|
+
+ sitrepRow('in flight', flight)
|
|
4191
|
+
+ '<div class="wlanetitle" style="margin-top:16px">this session</div>'
|
|
4192
|
+
+ sitrepRow('paid', '$' + (spent >= 0.01 || spent === 0 ? spent.toFixed(2) : spent.toFixed(5)))
|
|
4193
|
+
+ sitrepRow('cogs', '$' + (cogs >= 0.01 || cogs === 0 ? cogs.toFixed(2) : cogs.toFixed(5)))
|
|
4194
|
+
+ sitrepRow('direct', '$' + (direct >= 0.01 || direct === 0 ? direct.toFixed(2) : direct.toFixed(5)))
|
|
4195
|
+
+ sitrepRow('saved vs naked', saved, savedCls)
|
|
4196
|
+
+ sitrepRow('paid calls', String(you.paidCalls || 0))
|
|
4197
|
+
+ sitrepRow('prepaid', (Number(you.creditUsd) > 0) ? 'yes' : 'no');
|
|
4198
|
+
}
|
|
4199
|
+
function closeSitrep() { sitrepOverlay.classList.remove('show'); }
|
|
4200
|
+
sitrepOverlay.addEventListener('click', (e) => {
|
|
4201
|
+
if (e.target === sitrepOverlay) closeSitrep();
|
|
4202
|
+
});
|
|
4071
4203
|
document.addEventListener('keydown', (e) => {
|
|
4072
4204
|
if (e.key === 'Escape' && walletOverlay.classList.contains('show')) walletOverlay.classList.remove('show');
|
|
4205
|
+
if (e.key === 'Escape' && sitrepOverlay.classList.contains('show')) closeSitrep();
|
|
4073
4206
|
});
|
|
4074
4207
|
|
|
4075
4208
|
document.getElementById('tierSel').addEventListener('change', (e) => setDial('tier', e.target.value));
|
|
@@ -4611,6 +4744,12 @@ const APP_HTML = `<!doctype html>
|
|
|
4611
4744
|
}
|
|
4612
4745
|
if (full.status === 'thinking') {
|
|
4613
4746
|
if (full.liveStatus) streamStatus = full.liveStatus;
|
|
4747
|
+
if (full.liveRace && full.liveRace.racers && full.liveRace.racers.length >= 2) {
|
|
4748
|
+
streamRace = full.liveRace;
|
|
4749
|
+
streamRaceId = full.id;
|
|
4750
|
+
} else if (streamRaceId !== full.id) {
|
|
4751
|
+
streamRace = null;
|
|
4752
|
+
}
|
|
4614
4753
|
addRow('bot', streamBuf || '…', t.color, t.name);
|
|
4615
4754
|
// Tag the live bubble so deltas can repaint just this node instead of
|
|
4616
4755
|
// re-rendering (and re-fetching) the whole thread on every token.
|
|
@@ -4625,8 +4764,60 @@ const APP_HTML = `<!doctype html>
|
|
|
4625
4764
|
// so a turn showed "…" for its whole duration and then arrived in one lump.
|
|
4626
4765
|
let streamBuf = '';
|
|
4627
4766
|
let streamStatus = '';
|
|
4767
|
+
let streamRace = null;
|
|
4768
|
+
let streamRaceId = '';
|
|
4769
|
+
let raceHandoff = 0;
|
|
4628
4770
|
let es = null, esId = null;
|
|
4771
|
+
function raceIsLive(r) {
|
|
4772
|
+
return !!(r && r.racers && r.racers.length >= 2 && streamRaceId === activeId);
|
|
4773
|
+
}
|
|
4774
|
+
function shortRaceName(id) {
|
|
4775
|
+
const s = String(id || '');
|
|
4776
|
+
const i = s.lastIndexOf('/');
|
|
4777
|
+
return (i >= 0 ? s.slice(i + 1) : s) || 'model';
|
|
4778
|
+
}
|
|
4779
|
+
function raceGridHtml(race) {
|
|
4780
|
+
const racers = race.racers || [];
|
|
4781
|
+
const n = racers.length;
|
|
4782
|
+
const need = Math.max(1, Number(race.need) || 1);
|
|
4783
|
+
const caption = (need > 1 ? ('first ' + need + ' of ' + n) : (n + ' launched'))
|
|
4784
|
+
+ (race.recut ? ' · recut' : '');
|
|
4785
|
+
let cells = '';
|
|
4786
|
+
for (let i = 0; i < racers.length; i++) {
|
|
4787
|
+
const r = racers[i];
|
|
4788
|
+
const win = race.phase === 'winner' && race.winner && r.model === race.winner;
|
|
4789
|
+
const cls = 'racecell ' + (r.status || 'waiting') + (win ? ' winner' : '');
|
|
4790
|
+
const chip = win ? 'winner' : (r.status || 'waiting');
|
|
4791
|
+
let body = '';
|
|
4792
|
+
if (r.status === 'failed') {
|
|
4793
|
+
body = '<span class="racefail">' + escapeHtml(r.fail ? ('fail · ' + r.fail) : 'fail') + '</span>';
|
|
4794
|
+
} else if (r.preview) {
|
|
4795
|
+
body = '<div class="raceprev">' + escapeHtml(r.preview) + '</div>';
|
|
4796
|
+
} else if (r.status === 'abandoned') {
|
|
4797
|
+
body = '<div class="raceprev">abandoned</div>';
|
|
4798
|
+
} else {
|
|
4799
|
+
body = '<div class="raceprev"></div>';
|
|
4800
|
+
}
|
|
4801
|
+
cells += '<div class="' + cls + '"><div class="racehead"><span class="racename">'
|
|
4802
|
+
+ escapeHtml(r.short || shortRaceName(r.model)) + '</span><span class="racechip">'
|
|
4803
|
+
+ escapeHtml(chip) + '</span></div>' + body + '</div>';
|
|
4804
|
+
}
|
|
4805
|
+
let judge = '';
|
|
4806
|
+
if (need > 1 && (race.phase === 'judging' || race.phase === 'winner')) {
|
|
4807
|
+
const won = race.phase === 'winner' && race.winner;
|
|
4808
|
+
const msg = won
|
|
4809
|
+
? ('goes to ' + shortRaceName(race.winner))
|
|
4810
|
+
: ('looking at the ' + need + ' that made it back');
|
|
4811
|
+
judge = '<div class="racejudge' + (won ? ' won' : '') + '"><div class="racejudge-lab">classifier</div>'
|
|
4812
|
+
+ '<div class="racejudge-msg">' + escapeHtml(msg)
|
|
4813
|
+
+ (won ? '' : ' <span class="dots"><span></span><span></span><span></span></span>')
|
|
4814
|
+
+ '</div></div>';
|
|
4815
|
+
}
|
|
4816
|
+
return '<div class="racewrap"><div class="racecaption">' + escapeHtml(caption) + '</div>'
|
|
4817
|
+
+ '<div class="racegrid n' + n + '">' + cells + '</div>' + judge + '</div>';
|
|
4818
|
+
}
|
|
4629
4819
|
function liveBubbleHtml() {
|
|
4820
|
+
if (raceIsLive(streamRace)) return raceGridHtml(streamRace);
|
|
4630
4821
|
if (streamBuf) {
|
|
4631
4822
|
const trail = streamStatus && /^(RUN|READ|WRITE|EDIT|SPAWN|SEND|GLOB|GREP|MCP|FETCH|TODO|SERVE|PING|PEEK|MULTIEDIT|NOTEBOOK):/i.test(streamStatus)
|
|
4632
4823
|
? '<span class="ttrail">' + escapeHtml(streamStatus) + '</span>' : '';
|
|
@@ -4639,6 +4830,13 @@ const APP_HTML = `<!doctype html>
|
|
|
4639
4830
|
function paintStream() {
|
|
4640
4831
|
const b = document.getElementById('streamBubble');
|
|
4641
4832
|
if (!b) { render(); return; }
|
|
4833
|
+
if (raceIsLive(streamRace)) {
|
|
4834
|
+
b.classList.add('raceboard');
|
|
4835
|
+
b.innerHTML = liveBubbleHtml();
|
|
4836
|
+
if (log.scrollHeight - log.scrollTop - log.clientHeight < 140) log.scrollTop = log.scrollHeight;
|
|
4837
|
+
return;
|
|
4838
|
+
}
|
|
4839
|
+
b.classList.remove('raceboard');
|
|
4642
4840
|
// Deltas stay as text; a silent wait paints dots + one mutating status
|
|
4643
4841
|
// line so a 20–40s pay/model wait is obviously alive.
|
|
4644
4842
|
if (streamBuf && !(streamStatus && /^(RUN|READ|WRITE|EDIT|SPAWN|SEND|GLOB|GREP|MCP|FETCH|TODO|SERVE|PING|PEEK|MULTIEDIT|NOTEBOOK):/i.test(streamStatus))) {
|
|
@@ -4654,17 +4852,50 @@ const APP_HTML = `<!doctype html>
|
|
|
4654
4852
|
esId = id;
|
|
4655
4853
|
streamBuf = '';
|
|
4656
4854
|
streamStatus = '';
|
|
4855
|
+
streamRace = null;
|
|
4856
|
+
streamRaceId = id;
|
|
4857
|
+
raceHandoff += 1;
|
|
4657
4858
|
es = new EventSource('/stream/' + id); // EventSource reconnects on its own
|
|
4658
4859
|
es.onmessage = (e) => {
|
|
4659
4860
|
let ev;
|
|
4660
4861
|
try { ev = JSON.parse(e.data); } catch { return; }
|
|
4661
|
-
if (ev.type === 'start') {
|
|
4862
|
+
if (ev.type === 'start') {
|
|
4863
|
+
streamBuf = '';
|
|
4864
|
+
streamStatus = ev.detail || 'waiting on model…';
|
|
4865
|
+
streamRace = null;
|
|
4866
|
+
streamRaceId = id;
|
|
4867
|
+
paintStream();
|
|
4868
|
+
}
|
|
4662
4869
|
else if (ev.type === 'status') { streamStatus = ev.detail || streamStatus; paintStream(); }
|
|
4870
|
+
else if (ev.type === 'race') {
|
|
4871
|
+
if (ev.race && ev.race.racers && ev.race.racers.length >= 2) {
|
|
4872
|
+
streamRace = ev.race;
|
|
4873
|
+
streamRaceId = id;
|
|
4874
|
+
}
|
|
4875
|
+
paintStream();
|
|
4876
|
+
}
|
|
4663
4877
|
else if (ev.type === 'delta') {
|
|
4664
4878
|
streamBuf = ev.replace ? (ev.delta || '') : streamBuf + (ev.delta || '');
|
|
4665
4879
|
paintStream();
|
|
4666
4880
|
}
|
|
4667
|
-
else if (ev.type === 'final' || ev.type === 'run-pending') {
|
|
4881
|
+
else if (ev.type === 'final' || ev.type === 'run-pending') {
|
|
4882
|
+
if (ev.type === 'final' && raceIsLive(streamRace)) {
|
|
4883
|
+
paintStream();
|
|
4884
|
+
const token = ++raceHandoff;
|
|
4885
|
+
setTimeout(function () {
|
|
4886
|
+
if (token !== raceHandoff) return;
|
|
4887
|
+
streamBuf = '';
|
|
4888
|
+
streamStatus = '';
|
|
4889
|
+
streamRace = null;
|
|
4890
|
+
render();
|
|
4891
|
+
}, 420);
|
|
4892
|
+
return;
|
|
4893
|
+
}
|
|
4894
|
+
streamBuf = '';
|
|
4895
|
+
streamStatus = '';
|
|
4896
|
+
streamRace = null;
|
|
4897
|
+
render();
|
|
4898
|
+
}
|
|
4668
4899
|
};
|
|
4669
4900
|
es.onerror = () => { /* EventSource retries; the 1.2s poll is the backstop */ };
|
|
4670
4901
|
}
|
|
@@ -4721,6 +4952,12 @@ const APP_HTML = `<!doctype html>
|
|
|
4721
4952
|
|
|
4722
4953
|
async function submit() {
|
|
4723
4954
|
const task = inp.value.trim();
|
|
4955
|
+
if (/^\/sitrep\b/i.test(task)) {
|
|
4956
|
+
inp.value = '';
|
|
4957
|
+
send.classList.remove('show');
|
|
4958
|
+
openSitrep();
|
|
4959
|
+
return;
|
|
4960
|
+
}
|
|
4724
4961
|
if ((!task && !pendingFiles.length && !pendingImages.length) || !activeId) return;
|
|
4725
4962
|
inp.value = '';
|
|
4726
4963
|
send.classList.remove('show');
|
|
@@ -4775,6 +5012,12 @@ const APP_HTML = `<!doctype html>
|
|
|
4775
5012
|
function slashAccept(i) {
|
|
4776
5013
|
const c = slashHits[i];
|
|
4777
5014
|
if (!c) return;
|
|
5015
|
+
if (c.name === '/sitrep') {
|
|
5016
|
+
inp.value = '';
|
|
5017
|
+
slashMenu.classList.remove('show');
|
|
5018
|
+
openSitrep();
|
|
5019
|
+
return;
|
|
5020
|
+
}
|
|
4778
5021
|
// Commands that take arguments keep the caret going; ones that don't are
|
|
4779
5022
|
// ready to send, so don't make the user delete a trailing space.
|
|
4780
5023
|
inp.value = c.name + (c.args ? ' ' : '');
|
|
@@ -4807,6 +5050,11 @@ const APP_HTML = `<!doctype html>
|
|
|
4807
5050
|
plusBtn.addEventListener('click', (e) => { e.stopPropagation(); plusMenu.classList.toggle('show'); });
|
|
4808
5051
|
document.addEventListener('click', () => plusMenu.classList.remove('show'));
|
|
4809
5052
|
document.getElementById('attachBtn').addEventListener('click', (e) => { e.stopPropagation(); plusMenu.classList.remove('show'); fileInp.click(); });
|
|
5053
|
+
document.getElementById('sitrepBtn').addEventListener('click', (e) => {
|
|
5054
|
+
e.stopPropagation();
|
|
5055
|
+
plusMenu.classList.remove('show');
|
|
5056
|
+
openSitrep();
|
|
5057
|
+
});
|
|
4810
5058
|
fileInp.addEventListener('change', async () => {
|
|
4811
5059
|
for (const f of Array.from(fileInp.files)) {
|
|
4812
5060
|
const looksText = /^text\\//.test(f.type) || /\\.(txt|md|js|mjs|ts|tsx|jsx|py|json|css|html|csv|log|ya?ml|sh)$/i.test(f.name);
|
|
@@ -5325,6 +5573,7 @@ const server = http.createServer((req, res) => {
|
|
|
5325
5573
|
res.end(t ? JSON.stringify({
|
|
5326
5574
|
id: t.id, history: t.history, status: t.status,
|
|
5327
5575
|
liveStatus: t.status === 'thinking' ? (t.liveStatus || '') : '',
|
|
5576
|
+
liveRace: t.status === 'thinking' ? (t.liveRace || null) : null,
|
|
5328
5577
|
lastRaceFail: t.lastRaceFail || null,
|
|
5329
5578
|
workspacePort: workspacePort || 0, dir: t.dir || WORKSPACE_DIR,
|
|
5330
5579
|
}) : '{}');
|
|
@@ -5417,6 +5666,8 @@ const server = http.createServer((req, res) => {
|
|
|
5417
5666
|
// checking your spend or clearing a thread never costs anything.
|
|
5418
5667
|
// /dir and /mode keep their own handlers below, untouched.
|
|
5419
5668
|
if (t && /^\//.test(task.trim())) {
|
|
5669
|
+
// Drawer-only. Never dump sitrep into the transcript.
|
|
5670
|
+
if (/^\/sitrep\b/i.test(task.trim())) return;
|
|
5420
5671
|
const handled = await handleSlash(task.trim(), t).catch((e) => `error: ${e.message}`);
|
|
5421
5672
|
if (handled !== null && handled !== undefined) {
|
|
5422
5673
|
t.history.push({ who: 'bot', text: handled });
|
package/lib/livestatus.js
CHANGED
|
@@ -28,6 +28,68 @@ export function formatPayStatus(attempt = 0) {
|
|
|
28
28
|
return Number(attempt) > 0 ? 'waiting on x402…' : 'paying…';
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/** Cut from 1-model savings. X does not change it — they pay every launched racer. */
|
|
32
|
+
export function raceSavingsCutPct(y) {
|
|
33
|
+
const n = Math.max(1, Number(y) || 1);
|
|
34
|
+
return Math.round((1 - 1 / n) * 100);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function sitrepUsd(n) {
|
|
38
|
+
const x = Number(n) || 0;
|
|
39
|
+
return x >= 0.01 || x === 0 ? `$${x.toFixed(2)}` : `$${x.toFixed(5)}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sitrepFlight({ status, liveStatus, liveRace } = {}) {
|
|
43
|
+
if (status !== 'thinking') return 'idle';
|
|
44
|
+
if (liveRace?.phase === 'judging') return 'classifier judging';
|
|
45
|
+
if (liveRace?.phase === 'winner') return 'winner';
|
|
46
|
+
if (liveStatus) return String(liveStatus);
|
|
47
|
+
return 'in flight';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Compact sitrep field list (drawer). No keys, no .npmrc.
|
|
52
|
+
* Prepaid is yes/no — never a secret.
|
|
53
|
+
*/
|
|
54
|
+
export function formatSitrep(info = {}) {
|
|
55
|
+
const y = Number(info.race) >= 2 ? Number(info.race) : 1;
|
|
56
|
+
const need = Number(info.raceNeed) || 1;
|
|
57
|
+
const spent = Number(info.spentUsd) || 0;
|
|
58
|
+
const cogs = Number(info.cogsUsd) || 0;
|
|
59
|
+
const direct = Number(info.directUsd) || 0;
|
|
60
|
+
const mult = spent > 0 ? direct / spent : null;
|
|
61
|
+
const saved = mult == null
|
|
62
|
+
? '—'
|
|
63
|
+
: `${mult >= 100 ? Math.round(mult) : mult.toFixed(mult >= 10 ? 1 : 2)}x`;
|
|
64
|
+
const credit = Number(info.creditUsd);
|
|
65
|
+
const prepaid = Number.isFinite(credit) && credit > 0 ? 'yes' : 'no';
|
|
66
|
+
return [
|
|
67
|
+
'Sitrep',
|
|
68
|
+
` race ${raceChoiceLabel(y, need)}`,
|
|
69
|
+
` band ${info.tier || 'medium'}`,
|
|
70
|
+
` mode ${info.runMode || 'ask'}`,
|
|
71
|
+
` cwd ${info.dir || '—'}`,
|
|
72
|
+
` in flight ${sitrepFlight(info)}`,
|
|
73
|
+
` paid ${sitrepUsd(spent)}`,
|
|
74
|
+
` cogs ${sitrepUsd(cogs)}`,
|
|
75
|
+
` direct ${sitrepUsd(direct)}`,
|
|
76
|
+
` saved ${saved}`,
|
|
77
|
+
` paid calls ${Number(info.paidCalls) || 0}`,
|
|
78
|
+
` prepaid ${prepaid}`,
|
|
79
|
+
].join('\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Picker / sitrep label: `best 2 of 4 −75%`, `1 model 0%`. */
|
|
83
|
+
export function raceChoiceLabel(y, need = 1) {
|
|
84
|
+
const n = Math.max(1, Number(y) || 1);
|
|
85
|
+
const k = Math.max(1, Math.min(Number(need) || 1, n));
|
|
86
|
+
const cut = raceSavingsCutPct(n);
|
|
87
|
+
const cutTxt = cut === 0 ? '0%' : `−${cut}%`;
|
|
88
|
+
if (n < 2) return `1 model ${cutTxt}`;
|
|
89
|
+
if (k > 1) return `best ${k} of ${n} ${cutTxt}`;
|
|
90
|
+
return `race ${n} ${cutTxt}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
31
93
|
/** First-X-back race: how many of the K we asked for have actually landed. */
|
|
32
94
|
export function formatRaceStatus(back, need) {
|
|
33
95
|
const n = Math.max(1, Number(need) || 1);
|
|
@@ -35,6 +97,24 @@ export function formatRaceStatus(back, need) {
|
|
|
35
97
|
return `racing ${b}/${n} back…`;
|
|
36
98
|
}
|
|
37
99
|
|
|
100
|
+
/** OpenRouter id → short cell label. `z-ai/glm-4.7` → `glm-4.7`. */
|
|
101
|
+
export function shortModelName(id) {
|
|
102
|
+
const s = String(id || '').trim();
|
|
103
|
+
if (!s) return 'model';
|
|
104
|
+
const parts = s.split('/');
|
|
105
|
+
return parts[parts.length - 1] || s;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Compact spectator preview — opening lines, not the whole answer. */
|
|
109
|
+
export function clipRacePreview(text, maxLines = 8, maxChars = 420) {
|
|
110
|
+
const s = String(text || '').replace(/\r/g, '');
|
|
111
|
+
if (!s) return '';
|
|
112
|
+
const lines = s.split('\n');
|
|
113
|
+
let out = lines.length > maxLines ? lines.slice(0, maxLines).join('\n') + '\n…' : s;
|
|
114
|
+
if (out.length > maxChars) out = `${out.slice(0, maxChars - 1)}…`;
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
38
118
|
/** Race-level failure when no countable answer exists. Never a single model name. */
|
|
39
119
|
export const RACE_EVERY_FAILED = '(race: every model failed — no reply)';
|
|
40
120
|
|
|
@@ -154,20 +234,76 @@ export function pickRaceWinner(cands, minScore = RACE_MIN_SCORE) {
|
|
|
154
234
|
/**
|
|
155
235
|
* Live race bubble: stream the fastest still-alive entrant, swap once if the
|
|
156
236
|
* winner is someone else. `onDelta(text, { replace, model })`.
|
|
237
|
+
*
|
|
238
|
+
* `onRace(snap)` is the spectator feed — one cell per launched model, with
|
|
239
|
+
* waiting/streaming/back/failed/abandoned plus a truncated preview. Not a
|
|
240
|
+
* second racer: `phase: 'judging'` is the classifier looking at the X that
|
|
241
|
+
* already made it back.
|
|
157
242
|
*/
|
|
158
|
-
export function createRaceFeed(onDelta, onStatus, need) {
|
|
243
|
+
export function createRaceFeed(onDelta, onStatus, need, onRace) {
|
|
159
244
|
let live = null;
|
|
160
245
|
let settled = false;
|
|
161
246
|
let back = 0;
|
|
247
|
+
let phase = 'racing';
|
|
248
|
+
let winnerModel = '';
|
|
249
|
+
let recutNote = '';
|
|
162
250
|
const buf = new Map();
|
|
163
251
|
const dead = new Set();
|
|
252
|
+
const order = [];
|
|
253
|
+
const cells = new Map();
|
|
164
254
|
const paintStatus = () => { onStatus?.(formatRaceStatus(back, need)); };
|
|
255
|
+
const ensure = (model) => {
|
|
256
|
+
const id = String(model || '').trim() || 'model';
|
|
257
|
+
if (cells.has(id)) return cells.get(id);
|
|
258
|
+
const row = { model: id, status: 'waiting', preview: '', fail: '' };
|
|
259
|
+
cells.set(id, row);
|
|
260
|
+
order.push(id);
|
|
261
|
+
return row;
|
|
262
|
+
};
|
|
263
|
+
const freezeStragglers = () => {
|
|
264
|
+
if (back < need) return;
|
|
265
|
+
for (const row of cells.values()) {
|
|
266
|
+
if (row.status === 'waiting' || row.status === 'streaming') row.status = 'abandoned';
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
const snapshot = () => ({
|
|
270
|
+
need,
|
|
271
|
+
launched: order.length,
|
|
272
|
+
back,
|
|
273
|
+
phase,
|
|
274
|
+
winner: winnerModel || '',
|
|
275
|
+
recut: recutNote || '',
|
|
276
|
+
racers: order.map((id) => {
|
|
277
|
+
const row = cells.get(id);
|
|
278
|
+
return {
|
|
279
|
+
model: id,
|
|
280
|
+
short: shortModelName(id),
|
|
281
|
+
status: row.status,
|
|
282
|
+
preview: row.status === 'failed' ? '' : clipRacePreview(row.preview),
|
|
283
|
+
fail: row.fail || '',
|
|
284
|
+
};
|
|
285
|
+
}),
|
|
286
|
+
});
|
|
287
|
+
const emitRace = () => { onRace?.(snapshot()); };
|
|
165
288
|
return {
|
|
166
|
-
start() {
|
|
289
|
+
start(models, extra) {
|
|
290
|
+
if (Array.isArray(models)) {
|
|
291
|
+
for (const m of models) if (m) ensure(m);
|
|
292
|
+
}
|
|
293
|
+
if (extra && extra.recut) recutNote = String(extra.recut);
|
|
294
|
+
paintStatus();
|
|
295
|
+
emitRace();
|
|
296
|
+
},
|
|
297
|
+
snapshot,
|
|
167
298
|
liveModel() { return live; },
|
|
168
299
|
onToken(model, chunk) {
|
|
169
300
|
if (settled || chunk == null || chunk === '') return;
|
|
301
|
+
const row = ensure(model);
|
|
302
|
+
if (row.status === 'abandoned' || row.status === 'failed' || row.status === 'back') return;
|
|
170
303
|
buf.set(model, (buf.get(model) || '') + chunk);
|
|
304
|
+
row.preview = buf.get(model) || '';
|
|
305
|
+
if (row.status === 'waiting') row.status = 'streaming';
|
|
306
|
+
emitRace();
|
|
171
307
|
if (!live) {
|
|
172
308
|
live = model;
|
|
173
309
|
onDelta?.(chunk, { model });
|
|
@@ -175,8 +311,15 @@ export function createRaceFeed(onDelta, onStatus, need) {
|
|
|
175
311
|
}
|
|
176
312
|
if (live === model) onDelta?.(chunk, { model });
|
|
177
313
|
},
|
|
178
|
-
onFail(model) {
|
|
314
|
+
onFail(model, arrival) {
|
|
179
315
|
dead.add(model);
|
|
316
|
+
const row = ensure(model);
|
|
317
|
+
if (row.status !== 'abandoned' && row.status !== 'back') {
|
|
318
|
+
row.status = 'failed';
|
|
319
|
+
row.fail = raceFailKind(arrival || { model, text: '', error: 'error' });
|
|
320
|
+
row.preview = '';
|
|
321
|
+
emitRace();
|
|
322
|
+
}
|
|
180
323
|
if (settled || live !== model) return;
|
|
181
324
|
const next = [...buf.entries()].find(([m, t]) => m !== model && t && !dead.has(m));
|
|
182
325
|
if (next) {
|
|
@@ -186,18 +329,44 @@ export function createRaceFeed(onDelta, onStatus, need) {
|
|
|
186
329
|
live = null;
|
|
187
330
|
}
|
|
188
331
|
},
|
|
189
|
-
onBack() {
|
|
332
|
+
onBack(model) {
|
|
190
333
|
// Late countable stragglers after ship used to paint "racing 4/2 back…"
|
|
191
334
|
// onto an already-idle thread (GET /threads/:id returns raw liveStatus).
|
|
192
335
|
if (settled || back >= need) return;
|
|
193
336
|
back += 1;
|
|
337
|
+
let row = model ? ensure(model) : null;
|
|
338
|
+
if (!row) {
|
|
339
|
+
row = [...cells.values()].find((r) => r.status === 'streaming' || r.status === 'waiting');
|
|
340
|
+
}
|
|
341
|
+
if (row && row.status !== 'abandoned' && row.status !== 'failed') {
|
|
342
|
+
row.status = 'back';
|
|
343
|
+
if (buf.has(row.model)) row.preview = buf.get(row.model);
|
|
344
|
+
}
|
|
345
|
+
freezeStragglers();
|
|
194
346
|
paintStatus();
|
|
347
|
+
emitRace();
|
|
348
|
+
},
|
|
349
|
+
judge() {
|
|
350
|
+
if (settled) return;
|
|
351
|
+
phase = 'judging';
|
|
352
|
+
freezeStragglers();
|
|
353
|
+
emitRace();
|
|
195
354
|
},
|
|
196
355
|
settle(winner) {
|
|
197
356
|
settled = true;
|
|
357
|
+
phase = 'winner';
|
|
198
358
|
const text = String(winner?.text || '').trim()
|
|
199
359
|
? winner.text
|
|
200
360
|
: RACE_EVERY_FAILED;
|
|
361
|
+
winnerModel = winner?.error ? '' : (winner?.model || '');
|
|
362
|
+
if (winnerModel) {
|
|
363
|
+
const row = ensure(winnerModel);
|
|
364
|
+
if (row.status !== 'failed') {
|
|
365
|
+
row.status = 'back';
|
|
366
|
+
if (winner?.text) row.preview = String(winner.text);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
emitRace();
|
|
201
370
|
// Live stream already showing this answer — keep going, do not re-dump.
|
|
202
371
|
if (winner?.model && live === winner.model && !winner.error) return;
|
|
203
372
|
live = winner?.model || live;
|
package/lib/pay.js
CHANGED
|
@@ -12,6 +12,7 @@ import { privateKeyToAccount } from 'viem/accounts';
|
|
|
12
12
|
import { withNamespace } from './namespace.js';
|
|
13
13
|
import {
|
|
14
14
|
resolvePool, poolState, depositForShares, buildWrapInstructions, sendWrap,
|
|
15
|
+
rewriteWrapClientError,
|
|
15
16
|
} from './wrap.js';
|
|
16
17
|
import { applySubscriptionHeaders, loadSubscription, stripAuthorization } from './subscription.js';
|
|
17
18
|
import { fetchHeaders } from './fetch.js';
|
|
@@ -434,7 +435,7 @@ export class PayClient {
|
|
|
434
435
|
body: JSON.stringify(bodyObj),
|
|
435
436
|
}, { onStage });
|
|
436
437
|
if (!response.ok) {
|
|
437
|
-
const text = (await response.text()).slice(0, 500);
|
|
438
|
+
const text = rewriteWrapClientError((await response.text()).slice(0, 500));
|
|
438
439
|
throw new Error(`zoo returned HTTP ${response.status}: ${text}`);
|
|
439
440
|
}
|
|
440
441
|
return { data: await response.json(), receipt };
|
package/lib/podagent.mjs
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
} from './livestatus.js';
|
|
31
31
|
import {
|
|
32
32
|
probeGatewayRace, capRaceByCredit, inferRaceTier, RACE_NO_CREDIT,
|
|
33
|
+
recutRaceByHud, sessionDollarX,
|
|
33
34
|
} from './racesettle.js';
|
|
34
35
|
import { homedir } from 'node:os';
|
|
35
36
|
|
|
@@ -258,6 +259,19 @@ async function httpErrorNote(status) {
|
|
|
258
259
|
return status ? `(request failed — HTTP ${status})` : '';
|
|
259
260
|
}
|
|
260
261
|
|
|
262
|
+
/** Do not dump raw Solana wrap-sim logs into the chat bubble. */
|
|
263
|
+
function sanitizeProxiedError(msg) {
|
|
264
|
+
if (!msg) return msg;
|
|
265
|
+
const s = String(msg);
|
|
266
|
+
if (/0x6a\b|custom program error:\s*106\b|NotEnoughAccounts/i.test(s)) {
|
|
267
|
+
return 'wrap ix has too few accounts (need 9); old 5-account wrap is dead';
|
|
268
|
+
}
|
|
269
|
+
if (/0x70\b|custom program error:\s*112\b|TokenProgramMismatch/i.test(s)) {
|
|
270
|
+
return 'unwrap ix is missing the unwrapped token program (account 8); 8-account unwrap is dead';
|
|
271
|
+
}
|
|
272
|
+
return s;
|
|
273
|
+
}
|
|
274
|
+
|
|
261
275
|
// A 402 that reached this layer means the proxy's own x402 retry gave up on
|
|
262
276
|
// this attempt, but the NEXT attempt usually settles (measured: same wallet,
|
|
263
277
|
// same rail, second call pays fine). Surfacing that as a chat message makes
|
|
@@ -407,7 +421,7 @@ export async function brainStream(messages, onDelta, contextId, modelOverride, m
|
|
|
407
421
|
// fall back to the non-streaming path rather than fail outright
|
|
408
422
|
const j = await r.json().catch(() => ({}));
|
|
409
423
|
const content = j?.choices?.[0]?.message?.content;
|
|
410
|
-
const proxied = j?.error?.message;
|
|
424
|
+
const proxied = sanitizeProxiedError(j?.error?.message);
|
|
411
425
|
const text = content || (r.ok ? '' : (proxied ? `(request failed — HTTP ${r.status}: ${proxied})` : await httpErrorNote(r.status)));
|
|
412
426
|
if (text) onDelta(text);
|
|
413
427
|
return text;
|
|
@@ -666,8 +680,16 @@ async function readProxySession(proxy = completionsProxy()) {
|
|
|
666
680
|
}
|
|
667
681
|
|
|
668
682
|
async function raceBudget(hooks) {
|
|
669
|
-
|
|
670
|
-
|
|
683
|
+
const injected = hooks.creditUsd != null || hooks.quoteUsd != null
|
|
684
|
+
|| hooks.spentUsd != null || hooks.directUsd != null || hooks.dollarX != null;
|
|
685
|
+
if (injected) {
|
|
686
|
+
return {
|
|
687
|
+
creditUsd: hooks.creditUsd,
|
|
688
|
+
quoteUsd: hooks.quoteUsd,
|
|
689
|
+
spentUsd: hooks.spentUsd,
|
|
690
|
+
directUsd: hooks.directUsd,
|
|
691
|
+
dollarX: hooks.dollarX,
|
|
692
|
+
};
|
|
671
693
|
}
|
|
672
694
|
// Injected stream = unit-test N-parallel path. Do not poke :8402.
|
|
673
695
|
if (hooks.stream) return {};
|
|
@@ -675,6 +697,8 @@ async function raceBudget(hooks) {
|
|
|
675
697
|
return {
|
|
676
698
|
creditUsd: s?.creditUsd,
|
|
677
699
|
quoteUsd: s?.lastQuoteUsd ?? s?.quoteUsd,
|
|
700
|
+
spentUsd: s?.spentUsd,
|
|
701
|
+
directUsd: s?.directUsd,
|
|
678
702
|
};
|
|
679
703
|
}
|
|
680
704
|
|
|
@@ -706,8 +730,8 @@ async function brainGatewayRace(messages, onDelta, contextId, models, need, maxT
|
|
|
706
730
|
const classify = hooks.classify || classifyRaceAnswer;
|
|
707
731
|
const pairwise = hooks.pairwise || pairwiseTied;
|
|
708
732
|
const minScore = hooks.minScore != null ? Number(hooks.minScore) : RACE_MIN_SCORE;
|
|
709
|
-
const feed = createRaceFeed(onDelta, onStatus, want);
|
|
710
|
-
feed.start();
|
|
733
|
+
const feed = createRaceFeed(onDelta, onStatus, want, hooks.onRace);
|
|
734
|
+
feed.start(models, hooks.raceRecut ? { recut: hooks.raceRecut } : undefined);
|
|
711
735
|
|
|
712
736
|
const arrivals = [];
|
|
713
737
|
const done = [];
|
|
@@ -753,13 +777,13 @@ async function brainGatewayRace(messages, onDelta, contextId, models, need, maxT
|
|
|
753
777
|
if (!r.ok || !r.body) {
|
|
754
778
|
const j = await r.json().catch(() => ({}));
|
|
755
779
|
const content = j?.choices?.[0]?.message?.content;
|
|
756
|
-
const proxied = j?.error?.message;
|
|
780
|
+
const proxied = sanitizeProxiedError(j?.error?.message);
|
|
757
781
|
const text = content || (r.ok ? '' : (proxied ? `(request failed — HTTP ${r.status}: ${proxied})` : await httpErrorNote(r.status)));
|
|
758
782
|
lastFail = { model: 'gateway', text: text || '', error: r.ok ? undefined : `HTTP ${r.status}` };
|
|
759
783
|
if (isRaceCountable(lastFail)) {
|
|
760
784
|
arrivals.push(lastFail);
|
|
761
785
|
done.push(lastFail);
|
|
762
|
-
feed.onBack();
|
|
786
|
+
feed.onBack(lastFail.model);
|
|
763
787
|
if (text) onDelta(text);
|
|
764
788
|
break;
|
|
765
789
|
}
|
|
@@ -772,9 +796,9 @@ async function brainGatewayRace(messages, onDelta, contextId, models, need, maxT
|
|
|
772
796
|
arrivals.push(a);
|
|
773
797
|
if (isRaceCountable(a)) {
|
|
774
798
|
done.push(a);
|
|
775
|
-
feed.onBack();
|
|
799
|
+
feed.onBack(a.model);
|
|
776
800
|
} else {
|
|
777
|
-
feed.onFail(a.model);
|
|
801
|
+
feed.onFail(a.model, a);
|
|
778
802
|
}
|
|
779
803
|
}
|
|
780
804
|
lastFail = parsed.arrivals[parsed.arrivals.length - 1] || lastFail;
|
|
@@ -791,6 +815,7 @@ async function brainGatewayRace(messages, onDelta, contextId, models, need, maxT
|
|
|
791
815
|
if (cands.length === 1) return ship(cands[0]);
|
|
792
816
|
|
|
793
817
|
onStatus?.('judging…');
|
|
818
|
+
feed.judge();
|
|
794
819
|
const scored = await Promise.all(cands.map(async (c) => {
|
|
795
820
|
let score = 0;
|
|
796
821
|
try { score = Number(await classify(messages, c)) || 0; } catch { score = 0; }
|
|
@@ -947,6 +972,31 @@ export async function brainRace(messages, onDelta, contextId, models, need = 1,
|
|
|
947
972
|
const minScore = hooks.minScore != null ? Number(hooks.minScore) : RACE_MIN_SCORE;
|
|
948
973
|
let list = (models || []).filter(Boolean).slice(0, RACE_MAX);
|
|
949
974
|
const budget = await raceBudget(hooks);
|
|
975
|
+
let tier = hooks.tier || inferRaceTier(list, 'medium');
|
|
976
|
+
let wantNeed = Math.max(1, Math.min(Number(need) || 1, list.length || 1));
|
|
977
|
+
const dollarX = sessionDollarX(budget);
|
|
978
|
+
const hud = recutRaceByHud({
|
|
979
|
+
y: list.length, need: wantNeed, dollarX, tier,
|
|
980
|
+
});
|
|
981
|
+
let raceRecut = '';
|
|
982
|
+
if (hud.recut) {
|
|
983
|
+
raceRecut = 'savings';
|
|
984
|
+
if (hud.tier !== tier && !hooks.stream) {
|
|
985
|
+
try {
|
|
986
|
+
const next = await tierModels(hud.tier, hud.y, true);
|
|
987
|
+
if (next.length) { list = next; tier = hud.tier; }
|
|
988
|
+
else list = list.slice(0, hud.y);
|
|
989
|
+
} catch { list = list.slice(0, hud.y); }
|
|
990
|
+
} else {
|
|
991
|
+
list = list.slice(0, hud.y);
|
|
992
|
+
tier = hud.tier;
|
|
993
|
+
}
|
|
994
|
+
wantNeed = Math.min(hud.need, list.length);
|
|
995
|
+
onStatus?.(hud.y < 2
|
|
996
|
+
? 'race recut to 1 — savings'
|
|
997
|
+
: `race recut to ${hud.y} — savings`);
|
|
998
|
+
}
|
|
999
|
+
hooks = { ...hooks, tier, raceRecut };
|
|
950
1000
|
const capped = capRaceByCredit(list.length, budget);
|
|
951
1001
|
if (capped.n < 1) {
|
|
952
1002
|
onStatus?.('race refused — no credit');
|
|
@@ -957,7 +1007,7 @@ export async function brainRace(messages, onDelta, contextId, models, need = 1,
|
|
|
957
1007
|
onStatus?.(capped.n < 2 ? 'race shrunk to 1 — credit' : `race shrunk to ${capped.n} — credit`);
|
|
958
1008
|
}
|
|
959
1009
|
if (list.length < 2) return stream(messages, onDelta, contextId, list[0], maxTokens, 0, 0, onStatus);
|
|
960
|
-
const want = Math.max(1, Math.min(
|
|
1010
|
+
const want = Math.max(1, Math.min(wantNeed, list.length));
|
|
961
1011
|
|
|
962
1012
|
// One Fly settle when the completions door honors `race:`. Custom stream
|
|
963
1013
|
// hooks (unit tests of the N-parallel judge) keep the old path. Old
|
|
@@ -972,8 +1022,8 @@ export async function brainRace(messages, onDelta, contextId, models, need = 1,
|
|
|
972
1022
|
return brainGatewayRace(messages, onDelta, contextId, list, want, maxTokens, onStatus, hooks);
|
|
973
1023
|
}
|
|
974
1024
|
|
|
975
|
-
const feed = createRaceFeed(onDelta, onStatus, want);
|
|
976
|
-
feed.start();
|
|
1025
|
+
const feed = createRaceFeed(onDelta, onStatus, want, hooks.onRace);
|
|
1026
|
+
feed.start(list, raceRecut ? { recut: raceRecut } : undefined);
|
|
977
1027
|
|
|
978
1028
|
const done = [];
|
|
979
1029
|
const arrivals = [];
|
|
@@ -1021,7 +1071,7 @@ export async function brainRace(messages, onDelta, contextId, models, need = 1,
|
|
|
1021
1071
|
if (isRaceCountable(last)) {
|
|
1022
1072
|
arrivals.push(last);
|
|
1023
1073
|
done.push(last);
|
|
1024
|
-
feed.onBack();
|
|
1074
|
+
feed.onBack(m);
|
|
1025
1075
|
return;
|
|
1026
1076
|
}
|
|
1027
1077
|
} catch (e) {
|
|
@@ -1030,7 +1080,7 @@ export async function brainRace(messages, onDelta, contextId, models, need = 1,
|
|
|
1030
1080
|
if (!shouldRetryRaceArrival(last) || attempt === 1 || raceAbort.signal.aborted) break;
|
|
1031
1081
|
}
|
|
1032
1082
|
arrivals.push(last);
|
|
1033
|
-
feed.onFail(m);
|
|
1083
|
+
feed.onFail(m, last);
|
|
1034
1084
|
};
|
|
1035
1085
|
|
|
1036
1086
|
const attempts = list.map((m) => runOne(m).finally(() => {
|
|
@@ -1055,6 +1105,7 @@ export async function brainRace(messages, onDelta, contextId, models, need = 1,
|
|
|
1055
1105
|
if (cands.length === 1) return ship(cands[0]);
|
|
1056
1106
|
|
|
1057
1107
|
onStatus?.('judging…');
|
|
1108
|
+
feed.judge();
|
|
1058
1109
|
const scored = await Promise.all(cands.map(async (c) => {
|
|
1059
1110
|
let score = 0;
|
|
1060
1111
|
try { score = Number(await classify(messages, c)) || 0; } catch { score = 0; }
|
package/lib/proxy.js
CHANGED
|
@@ -27,6 +27,7 @@ import { creditBalance, quotedPrices } from './info.js';
|
|
|
27
27
|
import { subscriptionPublicView } from './subscription.js';
|
|
28
28
|
import { priceHoldings } from './livestatus.js';
|
|
29
29
|
import { receiptUsedCogs } from './racesettle.js';
|
|
30
|
+
import { rewriteWrapClientError } from './wrap.js';
|
|
30
31
|
|
|
31
32
|
const HOP_BY_HOP = new Set([
|
|
32
33
|
'host', 'connection', 'keep-alive', 'transfer-encoding', 'upgrade',
|
|
@@ -1726,7 +1727,8 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
1726
1727
|
// network error in `cause`. Surface it (and log the stack) or every
|
|
1727
1728
|
// transport hiccup looks identical to a payment bug.
|
|
1728
1729
|
const cause = err.cause?.message || err.cause?.code || err.cause;
|
|
1729
|
-
const
|
|
1730
|
+
const raw = cause ? `${err.message} (${cause})` : err.message;
|
|
1731
|
+
const detail = rewriteWrapClientError(raw);
|
|
1730
1732
|
log(`proxy error: ${detail}`);
|
|
1731
1733
|
if (process.env.OPENZOO_DEBUG) console.error(err.stack);
|
|
1732
1734
|
jsonErr(res, 502, `openzoo proxy error: ${detail}`);
|
package/lib/racesettle.js
CHANGED
|
@@ -8,6 +8,62 @@
|
|
|
8
8
|
|
|
9
9
|
export const FLY_GATEWAY_HOST = 'x402-tokens.fly.dev';
|
|
10
10
|
export const RACE_NO_CREDIT = '(race: not enough prepaid credit — shrink N or top up, rather than fire on $0)';
|
|
11
|
+
/** Low end of the pitched 5–10× vs frontier. Session green HUD below this recuts Y. */
|
|
12
|
+
export const RACE_HUD_TARGET = 5;
|
|
13
|
+
|
|
14
|
+
const TIER_DOWN = {
|
|
15
|
+
'grok4.6': 'medium',
|
|
16
|
+
grok46: 'medium',
|
|
17
|
+
expensive: 'medium',
|
|
18
|
+
medium: 'cheap',
|
|
19
|
+
cheap: 'cheap',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function cheaperRaceTier(tier) {
|
|
23
|
+
const t = String(tier || 'medium');
|
|
24
|
+
return TIER_DOWN[t] || 'cheap';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Same number the HUD green `x` uses: direct/spent. */
|
|
28
|
+
export function sessionDollarX({ dollarX, spentUsd, directUsd } = {}) {
|
|
29
|
+
const given = Number(dollarX);
|
|
30
|
+
if (Number.isFinite(given) && given > 0) return given;
|
|
31
|
+
const spent = Number(spentUsd);
|
|
32
|
+
const direct = Number(directUsd);
|
|
33
|
+
if (spent > 0 && Number.isFinite(direct)) return direct / spent;
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Recut launched Y (and maybe drop a band) when session green HUD is thin.
|
|
39
|
+
* Assumes the current multiple already includes this Y tax, so implied
|
|
40
|
+
* single-model x ≈ dollarX × y. Need (X) scales with Y. No user refunds.
|
|
41
|
+
*
|
|
42
|
+
* 2.09x on a 4-racer → implied ~8.4x single → Y=1 (back in the 5–10× band).
|
|
43
|
+
*/
|
|
44
|
+
export function recutRaceByHud({
|
|
45
|
+
y, need = 1, dollarX, tier = 'medium', target = RACE_HUD_TARGET,
|
|
46
|
+
} = {}) {
|
|
47
|
+
const launched = Math.max(1, Math.floor(Number(y) || 1));
|
|
48
|
+
const k = Math.max(1, Math.min(Math.floor(Number(need) || 1), launched));
|
|
49
|
+
const x = Number(dollarX);
|
|
50
|
+
const band = String(tier || 'medium');
|
|
51
|
+
if (!Number.isFinite(x) || x <= 0 || x >= target) {
|
|
52
|
+
return { y: launched, need: k, tier: band, recut: false, reason: null };
|
|
53
|
+
}
|
|
54
|
+
const impliedSingle = x * launched;
|
|
55
|
+
const maxY = Math.max(1, Math.min(launched, Math.floor(impliedSingle / target)));
|
|
56
|
+
const nextTier = impliedSingle < target ? cheaperRaceTier(band) : band;
|
|
57
|
+
const nextNeed = Math.max(1, Math.min(k, maxY));
|
|
58
|
+
const recut = maxY < launched || nextTier !== band;
|
|
59
|
+
return {
|
|
60
|
+
y: maxY,
|
|
61
|
+
need: nextNeed,
|
|
62
|
+
tier: nextTier,
|
|
63
|
+
recut,
|
|
64
|
+
reason: recut ? 'savings' : null,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
11
67
|
|
|
12
68
|
const FLY_RE = /x402-tokens\.fly\.dev/i;
|
|
13
69
|
|
package/lib/wrap.js
CHANGED
|
@@ -8,23 +8,45 @@
|
|
|
8
8
|
* shim. Nothing here is user-facing; never surface wrapped tickers or mints
|
|
9
9
|
* in messages that reach the user.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
11
|
+
* CLIENT BUILDER COPY — extra.acquire.steps / 402 help (WRAP_ACQUIRE_STEPS).
|
|
12
|
+
*
|
|
13
|
+
* Wrap ix has 9 accounts. 0x6a = 106 = NotEnoughAccounts, thrown at
|
|
14
|
+
* need(accounts, 9)?. Old 5-account Wrap is rejected.
|
|
15
|
+
*
|
|
16
|
+
* Program FrSERTNCPvTtaDS9AvQp9u1nYGzXDb3kC9MdL8Xxn2NE now CPIs the deposit.
|
|
17
|
+
* Delete the separate TransferChecked. Sending both double-transfers.
|
|
18
|
+
*
|
|
19
|
+
* data = [1] ++ u64 amount LE ++ [bump] (authority PDA bump, passed not derived)
|
|
20
|
+
*
|
|
21
|
+
* Accounts in exact order:
|
|
22
|
+
* 0 [writable] escrow (authority PDA ATA for UNDERLYING mint)
|
|
23
|
+
* 1 [writable] wrapped mint
|
|
24
|
+
* 2 [writable] recipient wrapped token account
|
|
25
|
+
* 3 [] wrapped mint authority PDA = PDA(["mint_authority", wrapped_mint], FrSER…)
|
|
26
|
+
* 4 [] wrapped token program (must equal wrapped_mint.owner; Token-2022 on shares)
|
|
27
|
+
* 5 [writable] depositor UNDERLYING token account
|
|
28
|
+
* 6 [signer] depositor (owner of account 5)
|
|
29
|
+
* 7 [] unwrapped mint
|
|
30
|
+
* 8 [] unwrapped token program (must equal escrow.owner)
|
|
31
|
+
*
|
|
32
|
+
* Account 4 and 8 are not interchangeable (LEOS: shares Token-2022, escrow
|
|
33
|
+
* TokenkegQ). Derive each ATA under its own token program.
|
|
34
|
+
* Prepend createAssociatedTokenAccountIdempotentInstruction for the recipient
|
|
35
|
+
* wrapped ATA. The program mints into it; it does not create it.
|
|
36
|
+
*
|
|
37
|
+
* Unwrap: 9 accounts, account 8 = unwrapped token program. 8 accounts → 0x70
|
|
38
|
+
* TokenProgramMismatch. Prepend idempotent ATA create for recipient underlying
|
|
39
|
+
* (may have been closed after wrapping).
|
|
40
|
+
*
|
|
41
|
+
* First deposit is 1:1 minus MINIMUM_LIQUIDITY (1000) locked forever.
|
|
42
|
+
* authority = PDA(['mint_authority', wrappedMint]); escrow = ATA(underlying,
|
|
43
|
+
* authority); registry = PDA(['backpointer', wrappedMint]) when present.
|
|
21
44
|
*/
|
|
22
45
|
import { PublicKey, Transaction, TransactionInstruction } from '@solana/web3.js';
|
|
23
46
|
import {
|
|
24
47
|
TOKEN_PROGRAM_ID,
|
|
25
48
|
TOKEN_2022_PROGRAM_ID,
|
|
26
49
|
createAssociatedTokenAccountIdempotentInstruction,
|
|
27
|
-
createTransferCheckedInstruction,
|
|
28
50
|
getAssociatedTokenAddressSync,
|
|
29
51
|
unpackAccount,
|
|
30
52
|
unpackMint,
|
|
@@ -34,6 +56,47 @@ export const WRAP_PROGRAM_ID = new PublicKey('FrSERTNCPvTtaDS9AvQp9u1nYGzXDb3kC9
|
|
|
34
56
|
const AUTHORITY_SEED = Buffer.from('mint_authority');
|
|
35
57
|
const BACKPOINTER_SEED = Buffer.from('backpointer');
|
|
36
58
|
export const MINIMUM_LIQUIDITY = 1000n;
|
|
59
|
+
export const WRAP_IX_ACCOUNT_COUNT = 9;
|
|
60
|
+
export const WRAP_TOO_FEW_ACCOUNTS =
|
|
61
|
+
'wrap ix has too few accounts (need 9); old 5-account wrap is dead';
|
|
62
|
+
|
|
63
|
+
/** extra.acquire.steps / 402 help — the copy clients build the Wrap ix from. */
|
|
64
|
+
export const WRAP_ACQUIRE_STEPS = {
|
|
65
|
+
method: 'spl-token-wrap',
|
|
66
|
+
program: 'FrSERTNCPvTtaDS9AvQp9u1nYGzXDb3kC9MdL8Xxn2NE',
|
|
67
|
+
help: 'Wrap ix has 9 accounts. Program CPIs the deposit — do not send a separate TransferChecked. 0x6a = NotEnoughAccounts (old 5-account wrap is dead).',
|
|
68
|
+
data: '[1] ++ u64 amount LE ++ [bump] (authority PDA bump, passed not derived)',
|
|
69
|
+
prepend: 'createAssociatedTokenAccountIdempotentInstruction for the recipient wrapped ATA',
|
|
70
|
+
accounts: [
|
|
71
|
+
{ i: 0, writable: true, name: 'escrow', note: 'authority PDA ATA for UNDERLYING mint' },
|
|
72
|
+
{ i: 1, writable: true, name: 'wrappedMint' },
|
|
73
|
+
{ i: 2, writable: true, name: 'recipientWrappedAta' },
|
|
74
|
+
{ i: 3, writable: false, name: 'mintAuthorityPda', note: 'PDA(["mint_authority", wrapped_mint], FrSER…)' },
|
|
75
|
+
{ i: 4, writable: false, name: 'wrappedTokenProgram', note: 'must equal wrapped_mint.owner; Token-2022 on shares' },
|
|
76
|
+
{ i: 5, writable: true, name: 'depositorUnderlyingAta' },
|
|
77
|
+
{ i: 6, writable: false, signer: true, name: 'depositor', note: 'owner of account 5' },
|
|
78
|
+
{ i: 7, writable: false, name: 'unwrappedMint' },
|
|
79
|
+
{ i: 8, writable: false, name: 'unwrappedTokenProgram', note: 'must equal escrow.owner; not interchangeable with account 4' },
|
|
80
|
+
],
|
|
81
|
+
unwrap: {
|
|
82
|
+
accounts: 9,
|
|
83
|
+
account8: 'unwrapped token program',
|
|
84
|
+
prepend: 'idempotent ATA create for recipient underlying (may have been closed)',
|
|
85
|
+
note: '8 accounts → 0x70 TokenProgramMismatch',
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** Short 402-help / chat copy. Never dump raw Solana simulation logs. */
|
|
90
|
+
export function rewriteWrapClientError(message) {
|
|
91
|
+
const s = String(message ?? '');
|
|
92
|
+
if (/0x6a\b|custom program error:\s*106\b|NotEnoughAccounts/i.test(s)) {
|
|
93
|
+
return WRAP_TOO_FEW_ACCOUNTS;
|
|
94
|
+
}
|
|
95
|
+
if (/0x70\b|custom program error:\s*112\b|TokenProgramMismatch/i.test(s)) {
|
|
96
|
+
return 'unwrap ix is missing the unwrapped token program (account 8); 8-account unwrap is dead';
|
|
97
|
+
}
|
|
98
|
+
return s;
|
|
99
|
+
}
|
|
37
100
|
|
|
38
101
|
// Machine-readable per-asset acquire directory published by the facilitator.
|
|
39
102
|
// Consulted first so newly listed twins work with zero code changes; on-chain
|
|
@@ -172,27 +235,15 @@ export async function poolState(connection, pool) {
|
|
|
172
235
|
}
|
|
173
236
|
|
|
174
237
|
/**
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* then move the deposit into escrow. `rentPayer` funds ATA creation (defaults
|
|
238
|
+
* ATA-create + 9-account Wrap. The program CPIs the deposit itself — there
|
|
239
|
+
* is no trailing TransferChecked. `rentPayer` funds ATA creation (defaults
|
|
178
240
|
* to the owner; the gateway feePayer when riding inside a payment tx).
|
|
179
241
|
*/
|
|
180
242
|
export function buildWrapInstructions({ pool, owner, depositRaw, rentPayer = owner }) {
|
|
181
243
|
const userWrapped = getAssociatedTokenAddressSync(pool.wrapped, owner, false, pool.wrappedProgram);
|
|
182
244
|
const userUnderlying = getAssociatedTokenAddressSync(pool.underlying, owner, false, pool.underlyingProgram);
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
// This used to emit three instructions — ensure ATA, Wrap, then a separate
|
|
186
|
-
// TransferChecked moving the underlying into escrow — because the program
|
|
187
|
-
// only minted shares and trusted that the caller's own transfer would follow.
|
|
188
|
-
// Nothing enforced it. On 2026-08-18 a caller sent the Wrap instruction ALONE
|
|
189
|
-
// and minted shares backed by nothing, then unwrapped them: 829,559 TOKEN out
|
|
190
|
-
// of the vault, NAV 1 -> 0.000177.
|
|
191
|
-
//
|
|
192
|
-
// The deployed program (slot 440219442) now CPIs the transfer itself, so the
|
|
193
|
-
// Wrap instruction carries the depositor's source account and signature and
|
|
194
|
-
// the separate transfer is GONE. A 5-account call is rejected outright with
|
|
195
|
-
// NotEnoughAccounts (0x6a) — verified against mainnet by simulation.
|
|
245
|
+
// 9-account Wrap. Program CPIs the deposit. No TransferChecked after this.
|
|
246
|
+
// Old 5-account Wrap is rejected 0x6a (NotEnoughAccounts).
|
|
196
247
|
const wrapIx = new TransactionInstruction({
|
|
197
248
|
programId: pool.programId || WRAP_PROGRAM_ID,
|
|
198
249
|
keys: [
|
|
@@ -254,6 +305,14 @@ export async function sendWrap(connection, keypair, pool, depositRaw) {
|
|
|
254
305
|
tx.recentBlockhash = blockhash;
|
|
255
306
|
tx.feePayer = keypair.publicKey;
|
|
256
307
|
tx.sign(keypair);
|
|
257
|
-
|
|
258
|
-
|
|
308
|
+
try {
|
|
309
|
+
const sig = await connection.sendRawTransaction(tx.serialize());
|
|
310
|
+
return confirmSignatureByPolling(connection, sig, { commitment: 'confirmed' });
|
|
311
|
+
} catch (err) {
|
|
312
|
+
const rewritten = rewriteWrapClientError(err?.message || String(err));
|
|
313
|
+
if (rewritten !== (err?.message || String(err))) {
|
|
314
|
+
throw new Error(rewritten);
|
|
315
|
+
}
|
|
316
|
+
throw err;
|
|
317
|
+
}
|
|
259
318
|
}
|
package/lib/x402.js
CHANGED
|
@@ -22,12 +22,18 @@ import {
|
|
|
22
22
|
* payTo: "<wallet>", resource, description, maxTimeoutSeconds,
|
|
23
23
|
* extra: { facilitator, feePayer, symbol, billedUsd, tokenUsd,
|
|
24
24
|
* pricedAt, pricing: "markup"|"counterfactual",
|
|
25
|
-
* markup? , directUsd?, savesVsDirect
|
|
26
|
-
*
|
|
25
|
+
* markup? , directUsd?, savesVsDirect?,
|
|
26
|
+
* acquire?: { method: "spl-token-wrap", steps: WRAP_ACQUIRE_STEPS } } } ],
|
|
27
|
+
* error: "payment required",
|
|
28
|
+
* help: "Wrap ix has 9 accounts. Program CPIs the deposit — do not send a separate TransferChecked. 0x6a = NotEnoughAccounts (old 5-account wrap is dead)." }
|
|
27
29
|
*
|
|
28
|
-
* Payment: ONE Token-2022 TransferChecked
|
|
29
|
-
*
|
|
30
|
-
* partial-signed by the payer,
|
|
30
|
+
* Payment of the quoted (already-wrapped) mint: ONE Token-2022 TransferChecked
|
|
31
|
+
* (payer ATA -> payTo ATA) for exactly maxAmountRequired, feePayer =
|
|
32
|
+
* extra.feePayer (the gateway pays SOL fees), partial-signed by the payer,
|
|
33
|
+
* serialized requireAllSignatures=false, base64.
|
|
34
|
+
* Funding a short wrapped balance is a *separate* 9-account Wrap prepended as
|
|
35
|
+
* preInstructions (lib/wrap.js WRAP_ACQUIRE_STEPS). Do not follow Wrap with a
|
|
36
|
+
* deposit TransferChecked — the program pulls the underlying itself.
|
|
31
37
|
* X-PAYMENT header = base64 of
|
|
32
38
|
* {"x402Version":1,"scheme":"exact","network":"<network>","payload":{"transaction":"<b64 tx>"}}
|
|
33
39
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.49.
|
|
3
|
+
"version": "0.49.5",
|
|
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",
|