openzoo 0.49.0 → 0.49.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/lib/grokui.mjs +133 -77
  2. package/package.json +1 -1
package/lib/grokui.mjs CHANGED
@@ -12,7 +12,7 @@ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSyn
12
12
  import { cpus, homedir } from 'node:os';
13
13
  import path from 'node:path';
14
14
  import { adaptiveTopK, brain, brainRace, brainStream, tierModels, MODEL, PROXY, TIER_NAMES, normalizeTier } from './podagent.mjs';
15
- import { peekDirectiveStatus, formatRaceStatus, STALE_THINKING_MS, summarizeRaceFailures } from './livestatus.js';
15
+ import { peekDirectiveStatus, formatRaceStatus, STALE_THINKING_MS, summarizeRaceFailures, RACE_EVERY_FAILED } from './livestatus.js';
16
16
  import { creditBalance } from './info.js';
17
17
  import {
18
18
  SUBSCRIPTIONS_PAGE,
@@ -491,7 +491,7 @@ function newThread(name, parent, members, spec) {
491
491
  const p = parent ? threads.get(parent) : null;
492
492
  const t = { id, name, color: members ? members[0].color : colorFor(name), parent: parent || null,
493
493
  messages: members ? null : [{ role: 'system', content: SYSTEM }],
494
- members: members || null, history: [], status: 'idle', createdAt: Date.now(), lastActivityAt: Date.now(),
494
+ members: members || null, history: [], status: 'idle', turnSeq: 0, createdAt: Date.now(), lastActivityAt: Date.now(),
495
495
  ...(p?.runMode ? { runMode: p.runMode } : {}),
496
496
  ...(p?.model ? { model: p.model } : {}),
497
497
  ...(p?.tier ? { tier: p.tier } : {}),
@@ -687,6 +687,47 @@ const NUDGE = 'That reply announced work instead of doing it — no directive li
687
687
  const AUTO_CONTINUE = 'AUTO is still on — do not stop and do not ask the user to type continue. '
688
688
  + 'Emit the next directive now (RUN:/SPAWN:/SEND:/READ:/WRITE:/GLOB:/FETCH:/MCP:/SERVE:), '
689
689
  + 'or DONE: if the job is actually finished.';
690
+ const AUTO_RACE_RETRY = 'AUTO is still on — the last model call failed (race/empty/error). '
691
+ + 'Do not stop and do not ask the user to type continue. '
692
+ + 'Emit the next directive now (RUN:/SPAWN:/SEND:/READ:/WRITE:/GLOB:/FETCH:/MCP:/SERVE:), '
693
+ + 'or DONE: if the job is actually finished.';
694
+ // Said it would, without a directive line. "Spawned X" and "working on it" are
695
+ // in here because they are FALSE without a SPAWN: in the same reply — the bot
696
+ // reports success for something the harness never saw.
697
+ const ANNOUNCEMENT = /\b(?:I(?:'| a)?ll |I will |let me |I'm going to |I am going to |first,? |next,? |now I'll |starting|kicking off|spawn(?:ing|ed)|about to|going to (?:check|run|create|start|install))\b/i;
698
+ const STALLED_OFFER = /\b(?:if you want(?:ed)?,? I can|should I\b|let me know(?: and I['’]ll)?|ready to proceed|want me to|would you like(?: me to)?|spawned \S[\s\S]{0,80}working on it|kicked that off)\b/i;
699
+ function isDoneReply(text) {
700
+ return /^[ \t>*-]*DONE:/m.test(String(text || ''));
701
+ }
702
+ function isTransientModelFail(text) {
703
+ const s = String(text || '').trim();
704
+ if (s === RACE_EVERY_FAILED || /^\(race:\s*every model failed/i.test(s)) return true;
705
+ if (/^error:/i.test(s)) return true;
706
+ if (/returned nothing \d+ times|each returned nothing/i.test(s)) return true;
707
+ return false;
708
+ }
709
+ function isPaymentFailed(text) {
710
+ return /\b(?:payment failed|HTTP 402|wallet is empty|empty wallet)\b/i.test(String(text || ''));
711
+ }
712
+ // Park only: ask mode, pendingRun, DONE:, 402/empty-wallet, or the hard cap.
713
+ function shouldKeepAuto(t, reply) {
714
+ if (!t || t.runMode !== 'auto') return false;
715
+ if (t.pendingRun) return false;
716
+ if ((t.autoSteps || 0) >= AUTO_MAX_STEPS) return false;
717
+ if (isDoneReply(reply)) return false;
718
+ if (isPaymentFailed(reply)) return false;
719
+ return true;
720
+ }
721
+ function enqueueAutoHop(t, threadId, userText, onEvent) {
722
+ bindThread(t).catch(() => {});
723
+ t.autoSteps = (t.autoSteps || 0) + 1;
724
+ if (t.autoSteps >= AUTO_MAX_STEPS) return false;
725
+ kickTurn(threadId, userText, onEvent).catch(() => {});
726
+ return true;
727
+ }
728
+ function autoHopText(reply) {
729
+ return isTransientModelFail(reply) ? AUTO_RACE_RETRY : AUTO_CONTINUE;
730
+ }
690
731
  // PING used to be a read: last-line status, no turn. Idle children stayed idle
691
732
  // while the parent treated the dump as evidence they had acted. A ping is a
692
733
  // wake — the same harness continue AUTO already uses, unless a custom message
@@ -695,6 +736,10 @@ let runTurnOverride = null;
695
736
  function setRunTurnForTest(fn) {
696
737
  runTurnOverride = typeof fn === 'function' ? fn : null;
697
738
  }
739
+ let brainAskOverride = null;
740
+ function setBrainAskForTest(fn) {
741
+ brainAskOverride = typeof fn === 'function' ? fn : null;
742
+ }
698
743
  function kickTurn(threadId, userText, onEvent, images) {
699
744
  // Default to emitToThread so a spawned/pinged kid streams when someone has
700
745
  // that thread open. emitToThread is a no-op if nobody is watching.
@@ -731,14 +776,14 @@ const SPAWN_MAX_CHILDREN = Number(process.env.OZ_SPAWN_MAX_CHILDREN)
731
776
 
732
777
  // Injected fresh on every AUTO turn, never persisted into the thread.
733
778
  //
734
- // The auto loop only continues while directives keep parsing, so a reply that
735
- // merely OFFERS ends the run — auto silently degrades to ask the moment the
736
- // model hedges. Observed live: "If you want, I can rewrite the prompt with
737
- // these fixes folded in", "Spawned mcp-integration — working on it" with
738
- // nothing spawned, and a user reduced to answering "no, this... impl all".
739
- // Models are trained to close on a consent question; in auto that instinct is
740
- // the bug. The system prompt is frozen into a thread at creation, so an
741
- // existing thread can only be reached by a per-turn message.
779
+ // The auto loop keeps going until DONE:, a real blocking question, or the
780
+ // step cap. A reply that merely OFFERS used to end the run — auto silently
781
+ // degraded to ask the moment the model hedged. Observed live: "If you want,
782
+ // I can rewrite the prompt", "Spawned mcp-integration — working on it" with
783
+ // nothing spawned, and a kid that RAN ls then sat idle. Models are trained
784
+ // to close on a consent question; in auto that instinct is the bug. The
785
+ // system prompt is frozen into a thread at creation, so an existing thread
786
+ // can only be reached by a per-turn message.
742
787
  const AUTO_DIRECTIVE = `AUTO MODE IS ON for this thread.
743
788
 
744
789
  Do the work in this turn. Do not ask whether to proceed, do not offer to do it,
@@ -756,7 +801,11 @@ Announcing an action does not perform it. "Spawned X", "working on it" and
756
801
  If a task needs several commands, emit the FIRST one now; you get its real
757
802
  output back and continue from there. Only stop to ask when the next step is
758
803
  genuinely destructive and irreversible, or when you truly cannot proceed
759
- without a fact only the user has.`;
804
+ without a fact only the user has.
805
+
806
+ When the job is actually finished, emit DONE: as the first line. A status
807
+ sentence is not a stop — the harness keeps this thread working until DONE:,
808
+ a real blocking question, or the step cap.`;
760
809
  async function bindThread(t) {
761
810
  // Only bind what's NEW since the last successful bind, continuing the
762
811
  // existing context_id — previously this rebuilt and re-sent the WHOLE
@@ -1264,6 +1313,7 @@ async function handleSlash(task, t) {
1264
1313
  const crew = subtreeOf(t.id);
1265
1314
  if (!crew.length) return 'You have no subagents to send to.';
1266
1315
  for (const x of crew) kickTurn(x.id, arg).catch(() => {});
1316
+ if (t.runMode === 'auto' && pingCanWake(t)) wakeOnPing(t, arg);
1267
1317
  return `Sent down your branch to ${crew.length} bot(s): ${crew.map((x) => x.name).join(', ')}`;
1268
1318
  }
1269
1319
 
@@ -1277,6 +1327,10 @@ async function handleSlash(task, t) {
1277
1327
  return crew.map((x) => {
1278
1328
  const mark = x.id === t.id ? ' (here)' : '';
1279
1329
  if (x.id === t.id) {
1330
+ if (t.runMode === 'auto' && pingCanWake(t)) {
1331
+ wakeOnPing(t, arg);
1332
+ return ` ${x.name}${mark}: pinged, working`;
1333
+ }
1280
1334
  const last = x.history[x.history.length - 1];
1281
1335
  return x.pendingRun ? ` ${x.name}${mark}: BLOCKED — waiting for your approval`
1282
1336
  : x.status === 'thinking' ? ` ${x.name}${mark}: working`
@@ -1354,9 +1408,15 @@ setInterval(() => {
1354
1408
  if (!last || now - last < STALE_THINKING_MS) continue;
1355
1409
  t.turnSeq = (t.turnSeq || 0) + 1;
1356
1410
  try { turnAborts.get(t)?.abort(); } catch { /* none */ }
1357
- t.status = 'idle';
1358
- t.liveStatus = '';
1359
- unlockWorktree(t);
1411
+ const lastBot = [...(t.history || [])].reverse().find((h) => h.who === 'bot');
1412
+ const lastText = lastBot?.text || '';
1413
+ if (shouldKeepAuto(t, lastText)) {
1414
+ kickTurn(t.id, isTransientModelFail(lastText) ? AUTO_RACE_RETRY : AUTO_CONTINUE).catch(() => {});
1415
+ } else {
1416
+ t.status = 'idle';
1417
+ t.liveStatus = '';
1418
+ unlockWorktree(t);
1419
+ }
1360
1420
  dirty = true;
1361
1421
  }
1362
1422
  if (dirty) saveThreads();
@@ -1615,7 +1675,8 @@ function unwrapDirectiveLine(reply) {
1615
1675
  function isHarnessUserText(text) {
1616
1676
  return /^\((command output|directive result)\)/.test(String(text || ''))
1617
1677
  || String(text || '') === NUDGE
1618
- || String(text || '') === AUTO_CONTINUE;
1678
+ || String(text || '') === AUTO_CONTINUE
1679
+ || String(text || '') === AUTO_RACE_RETRY;
1619
1680
  }
1620
1681
 
1621
1682
  function firstUserAsk(t) {
@@ -2365,7 +2426,11 @@ async function runTurn(threadId, userText, onEvent, images) {
2365
2426
  // status=thinking and the UI on "…".
2366
2427
  if (!isHarnessUserText(userText)) t.turnSeq = (t.turnSeq || 0) + 1;
2367
2428
  const seq = t.turnSeq || 0;
2368
- const stillMine = () => threads.get(threadId)?.turnSeq === seq;
2429
+ // Missing turnSeq is 0, not undefined. A ping/AUTO_CONTINUE as the first
2430
+ // message used to make stillMine() always false (undefined === 0), so the
2431
+ // RUN ran or the model was paid and then the hop bailed before chaining —
2432
+ // kids looked "pinged" then dead.
2433
+ const stillMine = () => (threads.get(threadId)?.turnSeq || 0) === seq;
2369
2434
  const paint = (ev) => {
2370
2435
  if (!stillMine()) return;
2371
2436
  if (ev.type === 'status' && ev.detail && t.status === 'thinking') t.liveStatus = ev.detail;
@@ -2386,11 +2451,13 @@ async function runTurn(threadId, userText, onEvent, images) {
2386
2451
  t.liveStatus = (!t.model && raceN >= 2) ? formatRaceStatus(0, raceNeed) : 'waiting on model…';
2387
2452
  let chained = false;
2388
2453
  let parked = false;
2454
+ let lastReply = '';
2389
2455
  try {
2390
2456
  if (t.members) {
2391
2457
  // sequential, not parallel: each member's context is rebuilt from
2392
2458
  // t.history right before its turn, so it sees every reply (including
2393
2459
  // spawns/sends) the earlier members in THIS round already made
2460
+ let memberReply = '';
2394
2461
  for (const m of t.members) {
2395
2462
  if (!stillMine()) return;
2396
2463
  const msgs = buildMemberMessages(t, m);
@@ -2404,6 +2471,7 @@ async function runTurn(threadId, userText, onEvent, images) {
2404
2471
  } catch (e) { r = `error: ${e.message}`; }
2405
2472
  if (!stillMine()) return;
2406
2473
  r = stripThinkTags(r);
2474
+ memberReply = r;
2407
2475
  const runCmd = parseRun(r);
2408
2476
  if (runCmd) {
2409
2477
  const command = runCmd;
@@ -2413,6 +2481,7 @@ async function runTurn(threadId, userText, onEvent, images) {
2413
2481
  const shown = `$ ${command}\n${output}`;
2414
2482
  t.history.push({ who: 'bot', text: shown, name: m.name, color: m.color });
2415
2483
  paint({ type: 'final', name: m.name, color: m.color, text: shown });
2484
+ memberReply = shown;
2416
2485
  // this member's turn is done; the round continues to the next member
2417
2486
  continue;
2418
2487
  }
@@ -2429,8 +2498,13 @@ async function runTurn(threadId, userText, onEvent, images) {
2429
2498
  const finalText = ack ?? (r || '(no response)');
2430
2499
  t.history.push({ who: 'bot', text: finalText, name: m.name, color: m.color });
2431
2500
  paint({ type: 'final', name: m.name, color: m.color, text: finalText });
2501
+ memberReply = r;
2432
2502
  }
2433
2503
  bindThread(t).catch(() => {});
2504
+ lastReply = memberReply;
2505
+ if (shouldKeepAuto(t, memberReply)) {
2506
+ chained = enqueueAutoHop(t, threadId, autoHopText(memberReply), onEvent);
2507
+ }
2434
2508
  return;
2435
2509
  }
2436
2510
  // A real message from the user resets the auto budget AND the announcement
@@ -2469,6 +2543,11 @@ async function runTurn(threadId, userText, onEvent, images) {
2469
2543
  // `attempt` exists because a retry must be allowed to land somewhere else:
2470
2544
  // see the empty-completion loop below.
2471
2545
  const ask = async (attempt = 0) => {
2546
+ if (brainAskOverride) {
2547
+ return String(await brainAskOverride({
2548
+ thread: t, attempt, userText, messages: callMsgs,
2549
+ }) ?? '').trim();
2550
+ }
2472
2551
  const emit = (delta, meta) => paint({
2473
2552
  type: 'delta', name: t.name, color: t.color, delta,
2474
2553
  ...(meta?.replace ? { replace: true } : {}),
@@ -2516,7 +2595,7 @@ async function runTurn(threadId, userText, onEvent, images) {
2516
2595
  // sick provider. Empties are per-model and uncorrelated, so moving is the
2517
2596
  // fix. A thread pinned with /model stays pinned; that was an explicit
2518
2597
  // choice and silently answering as something else would be worse.
2519
- for (let i = 0; !reply && i < AUTO_EMPTY_RETRIES; i++) {
2598
+ for (let i = 0; (!reply || isTransientModelFail(reply)) && i < AUTO_EMPTY_RETRIES; i++) {
2520
2599
  paint({ type: 'status', name: t.name, color: t.color, detail: 'retrying…' });
2521
2600
  await new Promise((r) => setTimeout(r, 400 * (i + 1)));
2522
2601
  if (!stillMine()) return;
@@ -2532,6 +2611,7 @@ async function runTurn(threadId, userText, onEvent, images) {
2532
2611
  }
2533
2612
  if (!stillMine()) return;
2534
2613
  reply = stripThinkTags(reply);
2614
+ lastReply = reply;
2535
2615
  t.messages.push({ role: 'assistant', content: reply });
2536
2616
  const runCmd = parseRun(reply);
2537
2617
  if (runCmd) {
@@ -2551,24 +2631,13 @@ async function runTurn(threadId, userText, onEvent, images) {
2551
2631
  // Bounded, because this is a loop that spends real money on every hop:
2552
2632
  // AUTO_MAX_STEPS chained commands per user message, reset whenever the
2553
2633
  // user speaks again.
2554
- t.autoSteps = (t.autoSteps || 0) + 1;
2555
- if (t.autoSteps < AUTO_MAX_STEPS) {
2556
- // BIND BEFORE CHAINING. bindThread only ran at the end of a normal
2557
- // turn, and both auto paths return before reaching it — so in auto
2558
- // mode nothing was ever bound, exactly when the agent produces the
2559
- // most material (command output, GLOB results, MCP tool lists). The
2560
- // holographic context stopped growing precisely when it mattered.
2561
- bindThread(t).catch(() => {});
2562
- chained = true;
2563
- runTurn(threadId, condense('(command output)', output), onEvent).catch(() => {});
2564
- } else {
2565
- // Do not park. A "say continue" note is the failure mode auto exists
2566
- // to avoid — inject continue and keep going until DONE or ask.
2567
- t.autoSteps = 0;
2568
- bindThread(t).catch(() => {});
2569
- chained = true;
2570
- runTurn(threadId, AUTO_CONTINUE, onEvent).catch(() => {});
2571
- }
2634
+ // BIND BEFORE CHAINING. bindThread only ran at the end of a normal
2635
+ // turn, and both auto paths return before reaching it — so in auto
2636
+ // mode nothing was ever bound, exactly when the agent produces the
2637
+ // most material (command output, GLOB results, MCP tool lists). The
2638
+ // holographic context stopped growing precisely when it mattered.
2639
+ lastReply = shown;
2640
+ chained = enqueueAutoHop(t, threadId, condense('(command output)', output), onEvent);
2572
2641
  return;
2573
2642
  }
2574
2643
  {
@@ -2598,16 +2667,8 @@ async function runTurn(threadId, userText, onEvent, images) {
2598
2667
  // Same budget as RUN (shared t.autoSteps, reset when the user speaks), so
2599
2668
  // this cannot spend more than a chained RUN loop already could.
2600
2669
  if (t.runMode === 'auto' && ack !== null && ack !== undefined
2601
- && !/^[ \t>*-]*DONE:/m.test(reply)) {
2602
- t.autoSteps = (t.autoSteps || 0) + 1;
2603
- bindThread(t).catch(() => {}); // bind every hop, not just the last one
2604
- chained = true;
2605
- if (t.autoSteps < AUTO_MAX_STEPS) {
2606
- runTurn(threadId, condense('(directive result)', ack), onEvent).catch(() => {});
2607
- } else {
2608
- t.autoSteps = 0;
2609
- runTurn(threadId, AUTO_CONTINUE, onEvent).catch(() => {});
2610
- }
2670
+ && !isDoneReply(reply)) {
2671
+ chained = enqueueAutoHop(t, threadId, condense('(directive result)', ack), onEvent);
2611
2672
  return;
2612
2673
  }
2613
2674
 
@@ -2622,50 +2683,43 @@ async function runTurn(threadId, userText, onEvent, images) {
2622
2683
  // AUTO_DIRECTIVE already forbids this in the prompt, and models do it anyway.
2623
2684
  // A prompt rule with no enforcement is a suggestion. Re-ask once, in-band.
2624
2685
  //
2625
- // NARROW ON PURPOSE. It fires only on announcement language, never on a plain
2626
- // answer auto is also how you hold an ordinary conversation, and nudging a
2627
- // finished reply would spend a turn arguing with a bot that already
2628
- // succeeded. Once per turn (autoNudged), so a model that announces twice
2629
- // still stops instead of looping on the user's wallet.
2686
+ // NUDGE announcements (stronger than a bare continue). Plain replies used
2687
+ // to park here; they now fall through to AUTO_CONTINUE unless DONE: or a
2688
+ // real blocking question. The step cap is the wallet bound.
2630
2689
  if (t.runMode === 'auto' && (ack === null || ack === undefined)
2631
2690
  && (STALLED_OFFER.test(reply) || ANNOUNCEMENT.test(reply))) {
2632
2691
  // Offers and "Spawned X — working on it" with no directive must not end
2633
2692
  // the run. The old once-only autoNudged gate parked the thread after one
2634
- // hedge and the user typed continue. Keep going; AUTO_MAX_STEPS injects
2635
- // another continue rather than stopping.
2693
+ // hedge and the user typed continue.
2636
2694
  t.autoNudged = true;
2637
- t.autoSteps = (t.autoSteps || 0) + 1;
2638
- bindThread(t).catch(() => {});
2639
- chained = true;
2640
- if (t.autoSteps < AUTO_MAX_STEPS) {
2641
- runTurn(threadId, NUDGE, onEvent).catch(() => {});
2642
- } else {
2643
- t.autoSteps = 0;
2644
- runTurn(threadId, AUTO_CONTINUE, onEvent).catch(() => {});
2645
- }
2695
+ chained = enqueueAutoHop(t, threadId, NUDGE, onEvent);
2696
+ return;
2697
+ }
2698
+
2699
+ // After any auto reply that is not DONE: and not waiting on approval,
2700
+ // kick immediately. Race/empty/error uses AUTO_RACE_RETRY.
2701
+ if (shouldKeepAuto(t, reply)) {
2702
+ chained = enqueueAutoHop(t, threadId, autoHopText(reply), onEvent);
2646
2703
  return;
2647
2704
  }
2648
2705
  bindThread(t).catch(() => {});
2649
2706
  } finally {
2650
- // A hung or thrown brainStream used to leave t.status = 'thinking' and the
2651
- // UI on mute "…" forever. Always idle unless this hop chained or parked
2652
- // on an approval the next runTurn sets thinking again itself.
2653
- if (stillMine() && !chained && !parked && !t.pendingRun) {
2654
- t.status = 'idle';
2655
- t.liveStatus = '';
2656
- unlockWorktree(t);
2707
+ // Idle only when this hop should not keep AUTO going: DONE:, pendingRun,
2708
+ // ask mode, 402/empty-wallet, or the hard cap. Otherwise kick again.
2709
+ if (stillMine() && !chained && !parked) {
2710
+ if (shouldKeepAuto(t, lastReply)) {
2711
+ enqueueAutoHop(t, threadId, autoHopText(lastReply), onEvent);
2712
+ } else if (!t.pendingRun) {
2713
+ t.status = 'idle';
2714
+ t.liveStatus = '';
2715
+ unlockWorktree(t);
2716
+ }
2657
2717
  }
2658
2718
  t.lastActivityAt = Date.now();
2659
2719
  saveThreads();
2660
2720
  }
2661
2721
  }
2662
2722
 
2663
- // Said it would, without a directive line. "Spawned X" and "working on it" are
2664
- // in here because they are FALSE without a SPAWN: in the same reply — the bot
2665
- // reports success for something the harness never saw.
2666
- const ANNOUNCEMENT = /\b(?:I(?:'| a)?ll |I will |let me |I'm going to |I am going to |first,? |next,? |now I'll |starting|kicking off|spawn(?:ing|ed)|about to|going to (?:check|run|create|start|install))\b/i;
2667
- const STALLED_OFFER = /\b(?:if you want(?:ed)?,? I can|should I\b|let me know(?: and I['’]ll)?|ready to proceed|want me to|would you like(?: me to)?|spawned \S[\s\S]{0,80}working on it|kicked that off)\b/i;
2668
-
2669
2723
  /**
2670
2724
  * The PROJECT a thread belongs to = the root of its spawn tree, and how deep
2671
2725
  * it sits. Every thread already carried the parent id; nothing ever walked it, so
@@ -5355,7 +5409,9 @@ server.listen(PORT, BIND, () => console.log(`[grokui] http://${BIND === '0.0.0.0
5355
5409
  export {
5356
5410
  tryDirective, ensureWorkspacePort, isPreviewableRel, previewAck, workspaceFileUrl,
5357
5411
  parseRun, looksLikeMcpAsBash, stripThinkTags, safeResolveIn, inDir, listDir,
5358
- handleSlash, newThread, setRunTurnForTest, AUTO_CONTINUE, pingWakeText, pingCanWake,
5359
- childKickoff, findByName, attachChildDir, finishChildDir, lockWorktree, unlockWorktree,
5360
- parsePrRef, fetchSpecsForOrigin, agentSlug,
5412
+ handleSlash, newThread, setRunTurnForTest, setBrainAskForTest, runTurn,
5413
+ AUTO_CONTINUE, AUTO_RACE_RETRY, pingWakeText, pingCanWake, shouldKeepAuto,
5414
+ isDoneReply, isTransientModelFail, enqueueAutoHop, childKickoff, findByName,
5415
+ attachChildDir, finishChildDir,
5416
+ lockWorktree, unlockWorktree, parsePrRef, fetchSpecsForOrigin, agentSlug,
5361
5417
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.49.0",
3
+ "version": "0.49.1",
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",