mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-11 18:00:54 -10:00
fix-exec-output-buffer-oom
388
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a766163401 |
fix: pre-push review findings on the exec_command buffer-cap fix
Ran an adversarial review before opening the PR. It found the truncation notice added alongside the buffer cap didn't actually work: prefixing execOutputTruncatedMessage into drainString()'s text meant the notice always sat ~2MiB before the end of the combined output, but the tool's own downstream truncateExecOutput() keeps at most a 400KB tail (even at the maximum allowed max_output_tokens) — so the notice was reliably swallowed, or chopped into an unreadable fragment at smaller budgets. Verified with direct reproductions against the real functions. Fixed by moving the signal out of the text stream entirely: - execOutputBuffer gains consumeTruncated() (read+reset, called once per collect()) and peekTruncated() (read-only, for status views) instead of injecting a marker into the drained bytes. - collect() now returns (string, bool); both call sites thread the bool through to execToolResult, which sets meta["output_buffer_truncated"] and appends the notice to the response body *after* truncateExecOutput runs, so it can never be re-truncated or land in a discarded region. - Also flagged: a session torn down without ever being polled again after truncation loses the signal entirely, since nothing drains/logs it on teardown. Full audit-trail logging felt like overkill for a soft transparency signal on top of a hard memory-safety fix, but exposed peekTruncated() on ExecSessionSnapshot and surfaced it in the /ps row (background_terminals.go) so it's visible any time a still-running session is checked — covers the common case (the review's own scenario: a long-lived dev server nobody's polling but that's still alive) even though a session reaped before ever being looked at is still a gap. New tests: reworked TestExecOutputBufferCapsUndrainedData for the consume/peek API, and TestExecToolResultSurfacesBufferTruncationOutsideByteBudget reproducing the review's exact failure case (max_output_tokens=1) and proving the notice now survives it. |
||
|
|
16ce70b7a9 |
fix: cap unbounded exec_command output buffer to prevent OOM kill
execOutputBuffer.data accumulated every byte a backgrounded exec_command
session wrote, with no size cap, unlike buffer.recent which is correctly
bounded to recentExecOutputBytes for the /ps preview. drainString() only
clears data when the agent actually polls that session again — so a
long-lived background session nobody polls (e.g. a dev server left running
after the run that started it was cancelled) grows this buffer forever as
long as the process keeps writing, with no ceiling.
Traced this directly to a real crash: a session that had been running for
~5.5 hours (an earlier turn invoking swarm agents got cancelled mid-flight,
per that session's own event log) accumulated to ~42.7GB of compressed
memory, at which point macOS's memorystatus killed the process outright
("killing zero due to low-swap").
Fixed by capping data at maxExecOutputBufferBytes (2MiB), trimming from the
front like recent already does, and surfacing a one-time truncation notice
on the next drain so data loss is visible rather than silent.
|
||
|
|
d53052071a |
revert(local): restore the original bright-lime accent color (#351)
Restores accent to #caff3f (the pre-PR#350 value), undoing the three-round brightness reduction (#ade619 -> #8db620 -> #759523) merged in PR #350. Only the accent token changes - the hover-highlight color (a separate amber token) and every other PR #350 fix (subchat selection, drag-to-edge auto-scroll, the dragging-flag fix) are untouched. Full gate green. Local only - not pushed. |
||
|
|
1c65f59be6 |
fix(tui): subchat mouse selection, hover highlighting, and drag-to-edge auto-scroll (#350)
* fix(local): subchat mouse selection + hidden plan panel + scroll-extend selection Three bugs in the subagent/swarm drill-in (subchat) view and mouse text selection, all from the same root: newer features (subchat's row-source swap, wheel-scroll) were never integrated into the older selection/footer code. 1) Can't copy anything in the subchat view. transcriptLineAtMouse (and selectedTranscriptText, the copy-time text extraction) always hit-tested against m.transcript (the parent), never m.subchat.childRows, even while the screen was showing the child session. New transcriptHitTestSource() mirrors transcriptView's own branching (nav bar + child rows + full width vs pinned title bar + parent rows + sidebar-aware width) and is now the single source both functions use. 2) Plan panel shown above the composer inside the subchat view. footerView unconditionally rendered the PARENT run's pinned plan panel; only pendingAskUser was gated. Added a !m.subchat.active guard (m.plan belongs to the parent run, not whatever subagent/swarm session is being viewed). 3) Selection didn't extend across a scroll (reported as "stopped at the end of the screen"). transcriptSelection.cursor was only ever updated by a mouseMotion event; a wheel event is a distinct message type that never reached that code, so scrolling while selecting left the selection frozen at whatever line the mouse last physically moved over. New scrollChatExtendingSelection scrolls AND re-evaluates the cursor at the mouse's current position against the post-scroll viewport. Uses a new nearestTranscriptLineAtMouse (falls back to the closest visible selectable line) rather than an exact match, since the recomputed position can land exactly on a blank spacer row between messages after an odd-length scroll shifts text/spacer parity - an exact-match miss there must not silently no-op the extension. Tests: subchat selection resolves the CHILD session's text, not the parent's; the plan panel is absent from the footer during subchat; a selection's cursor bodyY moves (extends) after a wheel-scroll instead of staying pinned. Full gate green (73 pkg, -race). * feat(local): highlight clickable rows on mouse hover Moving the cursor over an interactive row now highlights it (accent underline) before you click, across every clickable surface: - specialist cards and collapse/expand toggle headers in the transcript - AGENTS sidebar rows (swarm members) - PLAN sidebar step rows - permission-prompt options (reuses the EXISTING keyboard-cursor highlight by moving pendingPermission.cursor on hover, instead of new styling) Requires switching the mouse-tracking mode from CellMotion (only reports motion while a button is held) to AllMotion (reports idle cursor movement too) - the existing 15ms mouse-event throttle already covers the added motion events. hover.go: hoverTarget + mouseHover predicate + updateHoverTarget, which reuses the SAME hit-testers and priority order as the existing click handlers so hover and click never disagree about what's clickable. Sidebar hover is resolved by STABLE IDENTITY (sessionID / stepIndex), not a cached line offset: hoveredSidebarLineOffset re-matches the identity against a freshly computed hit list on every render, so a row that's disappeared (a swarm member's linger elapsed, a plan step completed) simply doesn't highlight rather than a coincidentally-matching unrelated row lighting up. Transcript hover stays bodyY-keyed (matching the existing selection-highlight convention) with explicit clearHover() calls wherever bodyY numbering can shift without an intervening mouse motion: wheel-scroll, PgUp/PgDn, window resize, and transcript rebuilds (/clear, /resume, /rewind, /compact via resetFlushFrontier). Adversarial review caught two real bugs before ship, both fixed: - BLOCKER: plan-step hover stored the step's own index (0,1,2...) but the sidebar render treated it as a raw line offset into the rendered rows - every plan-step hover highlighted the wrong row (the AGENTS header/body). Fixed by resolving fresh at render time via the stable-identity redesign above, which also closes the related sidebar-drift issue. - MAJOR: hover wasn't cleared on keyboard PgUp/PgDn scroll. Tests: predicate correctness; hover resolves the right target across all 4 surfaces (specialist card, toggle row, sidebar agent, plan step, permission option) with the correct priority order; hover changes the actual render output (transcript + sidebar); hover clears on wheel-scroll and subchat exit; a regression test proving hoveredSidebarLineOffset self-heals (no highlight) when a hovered row's identity no longer resolves - the exact scenario the blocker fix targets. Full gate green (73 pkg, -race). * fix(local): soften hover color + auto-scroll a drag past the transcript edge 1) Hover highlight was brand-lime (bright, glaring against the near-black surface) and re-paints on every mouse movement. Switched to amber (an existing semantic token, "light orange") and dropped the underline per feedback. 2) Dragging a text selection past the top/bottom edge of the visible transcript now auto-scrolls toward that side and extends the selection to follow, matching the browser/editor "drag past the viewport edge" pattern. transcriptEdgeScrollDelta detects the drag is vertically outside the body rect (but still within its horizontal span, so it isn't just "off to the side" over the sidebar); dragToEdgeScroll scrolls one step and anchors the cursor to whichever line now sits at that edge of the NEW viewport - deliberately not a re-resolve of the drag's physical mouse position, which stays outside the body rect after scrolling (scrolling changes what content occupies a screen row, not the mouse's screen position) and would keep finding nothing. Extracted nearestTranscriptSelectableAt as the shared "closest selectable line to a target bodyY" core, now used by both nearestTranscriptLineAtMouse (target derived from a mouse position that may have landed on a spacer row) and dragToEdgeScroll (target derived directly from the viewport bound, no mouse position involved at all). Tests: dragging past the top edge scrolls toward older content and extends the cursor upward; the symmetric bottom-edge case scrolls toward newer content and extends downward. Full gate green (73 pkg, -race). * fix(local): smooth-glide the drag-to-edge auto-scroll instead of jumping The initial edge-scroll implementation (previous commit) applied a single chatWheelScrollLines (5-line) jump per raw mouse-motion event, which reads as sudden/jerky since motion events during a real drag arrive in bursts, and stops dead the instant the physical mouse holds still even while still parked past the edge (a terminal only reports motion on actual cursor movement). Replaced with a self-scheduling tea.Tick chain (dragEdgeScrollTickMsg, gated by edgeScrollSeq mirroring the existing exitConfirmSeq/copyStatusSeq idiom) that steps a small, fixed increment (dragEdgeScrollStep=1 line) on a short cadence (dragEdgeScrollInterval=70ms) for as long as the drag holds past the edge, independent of whether new raw motion events keep arriving. reducedMotion still gets the original single-bigger-step-per-event behavior (no animated glide). Adversarial review of the new self-perpetuating tick chain (the risk being an orphaned/stuck chain) found one real bug: a keypress mid-drag clears m.transcriptSelection.active directly (the KeyPressMsg handler in model.go) without going through stopEdgeScroll, leaving edgeScrollDelta stale. The in-flight tick itself terminates safely via its own liveness check - the bug was that the STALE nonzero edgeScrollDelta then fooled startEdgeScroll's "already running" fast path on the NEXT same-direction drag into silently doing nothing (no immediate step, no scheduled tick) for that whole hold. Fixed at the press site (transcript_selection.go's mouseLeftPress case): every fresh press now resets glide state via stopEdgeScroll, so a new drag always starts clean regardless of how the previous one ended - a single targeted fix rather than chasing every place transcriptSelection can be cleared. Tests: top/bottom edge glide starts and extends the cursor over several ticks (a single small step can land on a blank spacer row between messages, same as real usage); the chain stops when the drag returns to the body and a stale in-flight tick after that is a safe no-op; the keypress-interrupt regression (reproduces the adversarial review's finding, confirms the fix). Full gate green (73 pkg, -race). * fix(local): soften the brand-lime accent color across the whole UI The accent color (#caff3f) was fully saturated at fairly high lightness (HSL: L 62%, S 100%) - the kind of value that reads as neon/glaring everywhere it filled a block or bold-text surface, not just the one spot originally flagged: the composer's "typed" prompt glyph, the permission popup's focused-option badge, and the text-selection highlight all use it, plus the spinner, the brand chip, tool-card prompts, and more. Rather than patch each of those consumers individually, softened the single palette source (dark theme only; the light theme's accent is a separate, already-darkened value for its own contrast needs, untouched) to L 50% / S 80% (#ade619) - still clearly on-brand lime, and contrast against both black (onAccent text on accent fills) and the near-black panel stays far above WCAG AA (~14:1 and ~13:1, both comfortably over the 4.5:1 floor). Updated TestStreamingFadePaletteMonotonic's pinned RGB assertion (it checked the fade ramp's first bucket equals the literal old accent bytes) to the new values. Full gate green (73 pkg, -race; make lint/go vet clean except an unrelated stray file that isn't part of this branch or git history at all). * fix(local): darken the brand-lime accent further per live feedback Two prior passes (#ade619 at L50/S80, #8db620 at L42/S70) were each tested live against the original #caff3f via raw truecolor escape codes and confirmed too subtle in turn. Settled at L36/S62 (#759523) - a noticeably darker, more olive lime, confirmed against the original side by side. Contrast vs black (onAccent) and the near-black panel stays comfortably above WCAG AA (~6.1:1 and ~5.6:1). Updated the streaming-fade test's pinned RGB assertion to match. * fix: address PR #350 review comments and CI smoke test failures Smoke tests (macOS + Ubuntu): the 4 new edge-glide tests implicitly relied on m.reducedMotion being false, but mouseTestModel() inherits it from defaultReducedMotion(), which forces true whenever colorprofile.NoTTY is detected - true in CI's non-TTY test runners, false in a local interactive dev shell. Set reducedMotion = false explicitly in each of the 4 tests so they're deterministic regardless of environment. Verified by re-running locally under ZERO_REDUCED_MOTION=1 (reproduces the CI condition) - passes. CodeRabbit actionable comment: handleTranscriptSelectionMouse's mouseMotion case gated only on transcriptSelection.active, which stays true through the async copy-command grace window after release (a non-empty selection isn't reset until transcriptCopiedMsg lands) - so a genuine hover motion arriving in that window would be misrouted as a drag update. The suggested fix (gate on msg's own Button field) turned out to be wrong in a way only a full test run caught: TestTranscriptSelectionUpdatesOnGenericMotion is a pre-existing, deliberate test - some terminals don't restate the button on every motion event during a real drag, so trusting the per-event Button field breaks those terminals. Fixed properly instead: added a `dragging` field to transcriptSelectionState, tracking the app's own press/release bracket (true at press, false at release) independent of active and independent of any single event's Button field. mouseMotion now gates on dragging. CodeRabbit nitpicks: routed the two remaining raw `m.hover = hoverTarget{}` subchat-exit assignments through the existing clearHover() helper, so hover invalidation stays in one place. Left the third nitpick (hit-test layout recomputed per hover-motion tick) as a documented follow-up per the reviewer's own "trivial/poor tradeoff" framing - a proper per-frame memoized cache needs correct invalidation on scroll/resize/content-change to avoid reintroducing the exact staleness class of bug already fixed twice in this branch, and isn't worth rushing. New tests: TestTranscriptHoverAfterReleaseDoesNotMoveSelection (the actual bug CodeRabbit found - a post-release hover must not alter the just-copied selection). Full gate green (73 pkg, -race); TestTranscriptSelectionUpdatesOnGenericMotion (the test the first fix attempt broke) verified still passing. |
||
|
|
7fb0092de7 |
fix: agent runtime resilience — stream stalls, weak-model tool args, and sub-agent reliability (#349)
* fix(local): defeat the macOS stale-pooled-connection hang (transport + stall-retry)
Root cause (you confirmed it: both chatgpt AND ollama hung, only on macOS): providers
fell back to http.DefaultClient, whose pooled keep-alive connections get reused on a
later turn after the server/NAT silently dropped them. The model call is a POST
(non-idempotent), so Go won't auto-retry it on a fresh conn — it blocks forever.
macOS keeps dead pooled conns around longer than Linux, so it reproduced on macOS only.
1. providerio.HTTPClient now returns a shared, tuned transport (ResponseHeaderTimeout
60s bounds a hung POST that gets no response; IdleConnTimeout 30s stops dead conns
from lingering to be reused). Routed openai/codex through it too (anthropic/gemini
already used HTTPClient). Slow first tokens / long reasoning are unaffected — the
header timeout only bounds time-to-first-response-header, not the stream body.
2. Agent loop now retries a stream idle/stall timeout that produced NO output yet, on
a fresh connection (capped + backoff). A partial-output turn is NOT retried (would
duplicate). Recovers any stall that slips past the transport timeout.
Local only — not pushed.
* fix(local): tolerate concatenated tool-arg JSON + widen response-header timeout
1) Weak models (minimax-m3, glm) pack MULTIPLE concatenated top-level JSON objects
into one tool-call arguments slot ({A}{B}), which json.Unmarshal rejects with
'invalid character {' after top-level value' (~9 failed tool calls/session).
New recoverableToolArguments returns the first JSON value ONLY when the payload is
one value optionally followed by more WHOLE JSON values; it rejects a bad/truncated
first value OR trailing NON-JSON garbage ({"x":1}xyz) so genuine corruption still
errors (adversarial-review fix). decodeToolArguments uses it at the central dispatch
(executeToolCall) so {A}{B} runs the first object; historySafeToolCalls uses the
SAME helper so the replayed transcript records the recovered first object (not {}),
keeping dispatch and history consistent. Truly-malformed JSON still hits the
existing sanitize-to-{} + retry path. Trailing objects are dropped (the model
re-issues on a later turn).
2) Raise the shared transport ResponseHeaderTimeout 60s -> 120s so a slow cloud proxy
(ollama *:cloud) that withholds its 200 header until first token isn't wrongly
aborted; still bounds a truly dead reused connection.
Local only — not pushed.
* feat(local): add /turns command + raise default maxTurns 30->50
Agentic multi-step runs (build + delegate + swarm + MCP + self-verify) routinely hit
the 30-turn ceiling mid-task and never reached the later steps; removing /mode earlier
also left NO interactive way to raise the budget.
- /turns [n]: per-session tool-turn budget setter (commandGroupSession), mirroring
/selfcorrect. Empty/status shows the current budget; a positive int sets
agentOptions.MaxTurns (read by the run at model.go); clamped to maxTurnsCeiling=500
to guard against typos; invalid/non-positive shows usage without changing it.
- defaultMaxTurns 30 -> 50 (matches the old 'deep' preset). Raise per-run with /turns
for long tasks (e.g. /turns 150 for a full acceptance run).
Local only — not pushed.
* fix(local): swarm reports failed members as failed + sub-agents inherit /turns budget
Issue B — a swarm member whose child exited non-zero (e.g. exit 4 / max-turns) was
recorded [done] (swarm_status '0 failed'), so the orchestrator — which can't see the
AGENTS panel — trusted incomplete work. The specialist launcher ignored
res.Result.Status; it now surfaces a StatusError result as a member FAILURE
(errors.New(report)) so the swarm marks it [failed]. Added Coordinator.FailWithSession
and switched the member watch path to it so a failed member keeps its session id
(still drillable) and its report rides along as the failure message.
Issue A — a member hit its OWN turn limit because /turns raised only the parent's
budget; children re-resolved config.json's default. New config.SetMaxTurnsEnv exports
ZERO_MAX_TURNS, read in applyEnv -> cfg.MaxTurns; /turns now exports it so spawned
sub-agents / swarm members (which inherit the env) run with the same budget. A
non-numeric/zero env value never clobbers the configured budget.
Tests: launcher non-zero-exit -> failed (+ session kept); SetMaxTurnsEnv round-trip
+ garbage-env guard; /turns exports + clamps the env. Full gate green (73 pkg).
Local only — not pushed.
* fix(local): corrective error for unknown specialist name + clamp ZERO_MAX_TURNS
1) A model that invents a specialist name (validator-runner, file-creator,
file-writer — none registered) got an opaque 'specialist not found' and kept
spawning doomed sub-agents. loadManifest now lists the available specialists and
notes that 'worker' has shell/edit while explorer/code-review are read-only, so the
model self-corrects to a capable specialist. New availableSpecialistList helper.
2) Defense-in-depth (review nit): applyEnv now clamps an inherited ZERO_MAX_TURNS to
MaxTurnsCeiling, so a raw shell 'export ZERO_MAX_TURNS=999999' can't bypass the
/turns ceiling. Ceiling promoted to config.MaxTurnsCeiling (single source of truth,
referenced by the TUI /turns clamp).
Tests: unknown-name error lists worker/explorer/code-review; applyEnv clamps an
over-ceiling env value. Full gate green (73 pkg).
Local only — not pushed.
* fix(local): surface sub-agent kill signal instead of opaque exit -1
When a sub-agent child is terminated by a signal, command.Wait returns an ExitError
whose ExitCode() is -1, losing the cause. Capture ProcessState.String() (portable,
e.g. "signal: killed") into ChildRunResult.Signal and have BuildFinalResult render
it — "Subagent terminated by a signal (signal: killed) — killed before it finished.
Likely causes: the OS out-of-memory killer ... a timeout, or cancellation." — instead
of an opaque "Subagent failed (exit -1)". The message lists causes rather than
asserting OOM, since timeout/cancellation also SIGKILL the child. A captured signal
always surfaces even if a late run_end reported a clean exit. The background task
path (which can't carry the signal name) emits the same hint for a negative exit code.
* address CodeRabbit review on #349
- loop.go: gate stall-retry on a turn-level forwardedAnything flag (wrapped
callbacks), so a retry never re-streams on top of output already forwarded earlier
in the turn (e.g. across the reactive-compaction retry). New test covers a
reasoning-then-stall (collected.Text empty but output forwarded).
- loop.go: route a reissued stream's non-stall error through the SAME recovery as
the initial stream (image-rejection wrapping / context-limit compaction) via a
shared recoverStreamError closure, instead of returning it raw.
- exec.go: drop the hardcoded worker/explorer/code-review guidance from the
unknown-specialist error; rely on the dynamically-rendered available list (a
custom registry may not have those names).
- streamer.go: kill-signal message lists OOM/timeout/cancellation evenhandedly
instead of asserting OOM (the branch also covers timeouts/cancellations).
- launcher_specialist.go + exec.go: preserve the child SessionID on a post-start
failure too, so FailWithSession keeps those failed members drillable.
- model.go: block /turns while a run is active (mid-run change would make the
inherited ZERO_MAX_TURNS budget inconsistent for later-spawned sub-agents).
|
||
|
|
5aced721b3 |
fix(tui): sub-agents inherit the parent's live provider after a /model switch (#348)
* fix(tui): sub-agents inherit the parent's live provider after a /model switch
Swarm members and Task sub-agents run as child 'zero exec' processes that re-resolve
the provider from config.json themselves — they're passed --model but never the
parent's provider. After switching provider/model in the TUI via /model (an in-memory
change), the child still resolved config.json's stale activeProvider, landed on a
provider whose credentials didn't match the live model, and failed auth in ~3s
(exit 3, 0 tool calls, 'API key missing or invalid / can't reach the provider') — so
the spawned agent appeared then instantly disappeared.
Export ZERO_PROVIDER (config.SetActiveProviderEnv) to the live provider name on a
/model provider switch. Child processes inherit the environment, and config
resolution already maps ZERO_PROVIDER -> activeProvider, so the child resolves the
SAME profile and its credentials (env key / stored key / OAuth) — covering all three
auth methods, for both Task and swarm (both spawn via the specialist executor).
Startup needs no export: the parent and child both read config's default, so they
already match; only an in-memory switch diverges.
* fix(config): clear ZERO_PROVIDER when switching to an unnamed/default profile
SetActiveProviderEnv("") now unsets ZERO_PROVIDER instead of leaving the previous
value in the environment. Switching from a named provider back to an unnamed/default
profile otherwise kept exporting the stale provider into child zero exec processes,
which would resolve the old credentials. Extended the round-trip test with the
stale-env regression case: seed a value, switch to blank, assert both os.Getenv and
applyEnv no longer see it.
Addresses CodeRabbit review on #348.
|
||
|
|
02a3d12f34 |
fix(providerio): bound heartbeat-but-no-output streams so they can't hang forever (#347)
* fix(providerio): bound heartbeat-but-no-output streams (content-stall watchdog) A stalled-but-alive upstream hung the agent indefinitely: SSE keep-alives reset the idle watchdog (intentionally — a heartbeating upstream isn't 'dead', and aborting it killed healthy long requests), so a stream that heartbeats while producing nothing never tripped the idle timeout and there was no other ceiling. Observed on both chatgpt/gpt-5.x (Codex) and ollama minimax-m3 — different provider code, same shared providerio SSE loop — so the fix belongs here, once, for every provider. Add a second 'content' watchdog alongside the idle one: keep-alives still reset idle (no regression for heartbeating connections), but only REAL data lines reset the content watchdog. If no data arrives for streamContentStallFactor (2) × the idle timeout, abort with ErrStreamStalled instead of hanging. A slow-but-producing stream (data between heartbeats) keeps resetting content and is never killed. Contained to internal/providers/providerio — no provider/caller changes. * fix(providerio): surface ErrStreamStalled in callers + widen flaky test margins Address review: - All four provider stream handlers (openai, codex, anthropic, gemini) now treat ErrStreamStalled in the same branch as ErrStreamIdle, via a new providerio.StreamTimeoutMessage helper that gives the stall a distinct, accurate message (it reports the content window and does NOT claim the upstream stopped sending data — keep-alives were still arriving). Previously a stalled stream fell through to the generic provider-error path. - Widen the content-watchdog test timing margins (idle 100ms, data/keepalive every 30ms) so CI/GC jitter can't trip the idle watchdog and mask the content path. - Add a StreamTimeoutMessage unit test. |
||
|
|
a0548d707b |
test(providers): isolate the OAuth store in the chatgpt Codex routing test (#346)
TestNewRoutesChatGPTCatalogToCodexProvider asserts the chatgpt-account-id header is empty when no OAuth login is stored, but never redirected the token store — so on a developer machine with a real chatgpt login it read that login and failed (it still passed in CI, where no token exists). Point ZERO_OAUTH_TOKENS_PATH at an empty temp path, mirroring TestNewRoutesChatGPTCatalogWithStoredAccountID, so the test is deterministic regardless of the developer's stored credentials. |
||
|
|
83a371d669 |
fix: stream Codex reasoning + drop swarm agents on their own completion (#345)
Two agent-visibility fixes surfaced diagnosing a 20-minute gpt-5.5 step and an 'agents never disappear' report. A) Codex Responses path streamed no reasoning, so a long thinking phase produced zero visible output and read as a hang (the activity clock never advanced). Root cause: the request never asked for a reasoning summary and the parser had no case for reasoning deltas. Now request reasoning.summary="auto" and forward response.reasoning_summary_text.delta as StreamEventReasoning — the existing TUI handler renders it live and refreshes the activity clock. B) A finished swarm member stayed in the AGENTS panel until the whole turn ended. Now it drops once ITS OWN task completes (fade over the linger window, then remove), independent of the overall run — matching how finished specialists already behave. Running members and mid-flight collect are unaffected. |
||
|
|
84823d7be1 |
fix(swarm): constrain agent_type to the registered roster via a dynamic enum (#344)
* fix(swarm): constrain spawn/handoff agent_type to the registered roster via enum swarm_spawn and swarm_handoff described agent_type / to_agent_type as a free-form string with only an 'e.g. teammate, subagent' hint, so a model could pass an unregistered type (e.g. 'worker') and only discover it was invalid from the runtime 'unknown agent type' error — wasting tool calls, especially on weaker models. Set the parameter's enum dynamically from the live roster (Registry.AgentTypes()), so the schema advertises the valid set (built-ins plus any user-registered agents) up front and an invalid type can't be sent. Falls back to a free-form string when the roster is unavailable, so the tool never advertises an empty, un-satisfiable enum. The runtime 'available agent types: …' error is kept as a backstop. * test(swarm): also assert handoff to_agent_type enum picks up a custom roster type Address review: the custom-type recheck only re-read spawnTool, so a regression leaving handoffTool on the built-in list would have passed. |
||
|
|
cfd065b565 |
fix(openai): always send message content so strict OpenAI-compatible servers accept it (#343)
Some OpenAI-compatible endpoints (e.g. glm-* on Ollama-cloud) reject a request whose message 'content' is absent or null with HTTP 400 'invalid message content type: <nil>'. The chat-completions mapper relied on 'content,omitempty', so a message with empty text (a tool result, an assistant turn that only makes tool calls, or a sub-agent that failed with no output) serialized with no content field at all. - Drop omitempty on chatMessage.Content and always set it (to "" when there is no text), so contentless messages serialize as "content":"" instead of being dropped. Standard OpenAI/xAI/groq already tolerated the omission; this makes the strict servers work too. - Skip a degenerate assistant turn that has neither text nor tool calls, mirroring the Anthropic/Gemini mappers (which already drop empty turns). Fixes the failure seen on glm-5.2:cloud after a sub-agent failed with empty output. |
||
|
|
fb3a0bff44 |
Document source-build sandbox helpers (#342)
Clarify that Linux source builds need the zero-linux-sandbox helper on PATH for native sandboxing, while macOS does not need an extra helper and Windows can self-dispatch from zero.exe. Also document optional release-style helper builds for Linux and Windows. Tested: git diff --check |
||
|
|
072f3877b5 |
fix(opengateway): correct gateway host, make it the recommended default (#341)
OpenGateway pointed at gateway.gitlawb.com, which does not resolve (NXDOMAIN), so every request failed with "no such host". The live gateway is opengateway.gitlawb.com — a flat OpenAI-compatible endpoint (/v1/chat/completions, Authorization: Bearer ogw_live_…) that smart-routes by model id across its upstreams (xiaomi-mimo, minimax, qwen, google, nvidia, z-ai). - catalog: fix base URL to https://opengateway.gitlawb.com/v1, default model to mimo-v2.5-pro; add a Recommended descriptor field and hoist OpenGateway to the top of the catalog so it is the first/badged pick everywhere. - models: replace the curated model list with the gateway's real upstream models; fix the stale model-discovery fallback host. - providers catalog: sort recommended-first (CLI + snapshot) and render the ★ … (recommended) badge; same badge in the TUI /provider wizard and the setup onboarding list. - providerhealth: OpenGateway's flat /v1/models 404s by design, so probe its public /health endpoint for connectivity instead of reporting a false fail. - tests: update catalog order golden + transport/discovery expectations; add coverage for the recommended provider and the health-endpoint override. Co-authored-by: Kevin Codex <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> |
||
|
|
1dd884d6e9 |
chore(launch): MIT license, windows-arm64 install guard, TUI TTY guard (#340)
* chore(launch): MIT license field, windows-arm64 install guard, TUI TTY guard Three launch-readiness fixes the docs cleanup doesn't cover: - package.json: set "license": "MIT" (was the placeholder "SEE LICENSE IN LICENSE"), matching the LICENSE file and the README badge. - scripts/postinstall.mjs: skip gracefully on windows-arm64. (win32, arm64) resolves to a valid platform/arch but the release matrix ships no windows-arm64 artifact, so the installer would download a 404 and hard-fail npm install. Treat it like any unsupported target (warnSkip, exit 0) and point to the x64 build (which runs under emulation on Windows on ARM) or source. - internal/tui/run.go: fail fast when stdin is not a terminal. The interactive shell otherwise blocks forever in the Bubble Tea event loop on piped/redirected input (e.g. `echo "" | zero`); now it prints guidance toward `zero exec` and exits non-zero. Dependency-free ModeCharDevice check (no x/term added). Tests: windows-arm64 skips with exit 0; the interactive shell returns non-zero (not a hang) on non-TTY stdin. * fix(tui): use term.IsTerminal for the stdin guard; pin exit code in test Address review on the no-TTY guard: - Replace the os.Stdin.Stat()/ModeCharDevice heuristic with term.IsTerminal (github.com/charmbracelet/x/term, already in the module graph via bubbletea). It is a true TTY check — it rejects pipes, regular files, and non-terminal char devices like /dev/null — and fails closed: anything not a verified terminal blocks the interactive shell. - The test now asserts the documented exit code 2 from the guard, not just a non-zero result, so it proves the stdin guard fired. |
||
|
|
726f1512a3 |
Clean up public release docs (#339)
Rewrite the README around the initial public-release user flow, add the logo asset, and refresh install/update documentation. Remove internal planning, audit, design, and work-split documents from the public docs tree. Update stale documentation references and workspace seed expectations to point at public docs. Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/workspaceseed Tested: git diff --check |
||
|
|
8c5898d5db |
feat(reasoning): typed per-provider reasoning capability from a models.dev catalog (#338)
* feat(reasoning): typed per-provider reasoning capability from a models.dev catalog
Providers describe reasoning control with incompatible concepts — OpenAI a
discrete effort enum (split by minor version), Anthropic and Gemini 2.5 a
thinking-token budget, some models only a thinking on/off toggle — which a flat
effort string cannot represent and a model-name heuristic cannot detect.
Add internal/reasoning: a Capability type (a model's reasoning flag plus its
typed Controls — effort/budget_tokens/toggle) and a Catalog that looks capability
up by provider + api model id. The data is an embedded, trimmed snapshot of the
community capability database (first-party providers only; routers excluded since
they carry generic/stale option lists), so detection is offline and exact instead
of guessed.
The package depends only on the standard library, so the model registry and
provider adapters can consume it without an import cycle. This change is purely
additive — nothing is wired into the live resolution path yet.
Tests pin the per-version ground truth (gpt-5 minimal-not-none, gpt-5.1
none-not-minimal, gpt-5.1-codex-max xhigh, gpt-5-pro locked to high; Anthropic
budget-only legacy vs effort-only newer with the budget removed; Gemini 2.5
budget/toggle vs Gemini 3 effort) and that every reasoning model Zero ships today
is covered by the snapshot.
* fix(reasoning): address review — presence-aware budget bounds, deep-copy lookup, first-party-only snapshot
- Control.Min/Max are now *int so an explicit bound is distinct from an omitted
one: Gemini's `min: 0` ("thinking can be disabled") and Anthropic's missing
max (unbounded) are preserved instead of both collapsing to 0. Budget() becomes
BudgetControl() returning the control so callers read the pointers.
- Catalog.Lookup returns a deep copy (clone) of the matched Capability, so a
caller can no longer mutate the shared embedded catalog through the returned
Controls / Values slices or budget pointers.
- Drop the google-vertex section from the snapshot: it re-hosts third-party MaaS
models (Anthropic/Moonshot/OpenAI/DeepSeek/Qwen/Meta), violating first-party
-only. google (AI Studio) already covers first-party Gemini, so the gemini
provider kind resolves there. Snapshot 158 -> 123 models, 16.8 KB.
Tests pin the explicit min: 0 preservation, the nil max on budget-only Anthropic
models, and that a mutation of a Lookup result does not leak into the catalog.
* fix(reasoning): address round-2 review — accessor deep-copy, max tier, provenance
- EffortControl/BudgetControl now return a cloned Control, so a caller holding a
Capability that shares state with the catalog cannot mutate Min/Max pointers or
the Values slice through an accessor result (CodeRabbit). Lookup already deep
-copies; this closes the accessor path too.
- Add modelregistry.ReasoningEffortMax ("max") + accept it in ValidReasoningEffort,
so the snapshot's "max" tier (e.g. gpt-5.1-codex-max, newer Anthropic) has a
constant to map to. No curated model lists it yet.
- Snapshot provenance: add a _fetched date and a regeneration recipe (documented
in catalog.go), so refreshes are reproducible/auditable.
- Document that Lookup takes the API model id (wire name), not the friendly
registry id, and that the match is exact (no case-fold/prefix-strip).
- Record the source-of-truth intent in the package doc: this catalog is the
intended authoritative source; modelregistry's name-pattern inference is demoted
to a fallback when it is wired in (a later change). Rename the `cap` builtin
shadow to `entry`.
Tests pin the accessor deep-copy isolation.
|
||
|
|
7a09a42a17 |
feat(tui): remove the /mode command (superseded by /model + /effort) (#337)
The /mode presets bundled a hardcoded Anthropic/Google model + reasoning effort + turn budget, so on other providers (Ollama-cloud, xAI, GPT-5, custom) /mode would switch to a model the user isn't authenticated to. With /model (any provider's models) and /effort (reasoning effort) covering the same ground flexibly, /mode is redundant in the TUI. Removes the /mode slash command, its handler, the mode picker, and the dispatch + stale comments; updates affected tests. The CLI `--mode` flag and internal/ modelregistry mode presets are kept (still used by headless exec). |
||
|
|
c0a0389b09 |
Polish TUI transcript rendering (#334)
* Polish transcript rendering Tighten transcript spacing, collapse noisy permission/tool rows, and render tool activity with cleaner action-oriented labels. Improve code and diff rendering with markdown highlighting, cleaner command output blocks, and grouped exploration summaries. Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/tui Tested: git diff --check * Fix grep exploration rendering on Windows Keep search query text out of path normalization so regex escapes render consistently across platforms. Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/tui Tested: git diff --check * Address transcript rendering review Cache highlighted streaming markdown so closed code blocks are not re-lexed on every live render. Tighten bare-code detection to avoid highlighting ordinary prose, restore manual permission audit rows, and make exploration details expandable. Also align diff content/header predicates and use display-width truncation for compact tool output. Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/tui Tested: env GOCACHE=/tmp/zero-go-cache go test -race ./internal/tui Tested: env GOCACHE=/tmp/zero-go-cache go vet ./... Tested: git diff --check |
||
|
|
d99ac4d1e3 |
fix(openai): forward reasoning effort to the Codex Responses API (#336)
The Codex provider speaks the Responses API, where reasoning_effort is nested under `reasoning.effort` rather than sent as a top-level field. buildResponsesRequest never set it, so a user's chosen effort was silently dropped for every Codex model — the request always ran at the model default. Add a `reasoning` object to the Responses request and populate it from the request's effort, reusing the chat path's openAIReasoningEffort normalizer so only API-accepted values (minimal/low/medium/high) are sent and an empty or unsupported effort omits the field (no empty object, no 400). |
||
|
|
aa45edacc0 |
fix(modelregistry): one source of truth for a model's reasoning efforts (#335)
The /effort picker and the run-time effort resolver disagreed about which tiers a model supports. Registry.ReasoningEfforts falls back to name-based inference (reasoningEffortsForModelName) when the catalog lists no efforts, so the picker shows controls for proxy-served GPT-5 / Codex / o-series models. EffectiveReasoningEffort read model.ReasoningEfforts directly and never saw that fallback, so it coerced the same request to "none" — the picker advertised a tier the resolver silently dropped. Route both through a single effectiveReasoningEfforts helper so they can never diverge. A model the catalog enumerates with no efforts but whose name is a known reasoning family now has its requested effort honored end to end; non-reasoning models still resolve to "none". Also drop the orphaned, stale NOTE above forwardedReasoningEffort claiming effort "is not yet forwarded" (it is) and restore the reasoningEffortNotice doc comment to its own function. |
||
|
|
ca5e0b9bbb |
feat: context-aware compaction + usage gauge for all models (#332)
* fix(agent): enable auto-compaction for all models, not just registered ones Auto-compaction keys off the model's context window (enabled = ContextWindow > 0), resolved only from the curated registry. Unregistered models — GPT-5/Codex, xAI, Ollama-cloud, custom endpoints — resolved to 0, so both the proactive (~80%) and the reactive post-overflow compaction were silently disabled and long sessions overflowed. - TUI modelContextWindow now also consults live-discovered windows (accurate for proxy/custom models once /model has discovered them); unknown still returns 0 so display gauges show no guessed denominator. - New modelregistry.AgentContextWindow applies a positive FallbackContextWindow (200k) ONLY on the compaction-enable path (TUI run + CLI exec/spec), so compaction is on for every model while display stays accurate-or-empty. Exact windows always win. * feat(tui): show context usage (used / total + % gauge) for all models from launch The sidebar/status-line context gauge only shows a total + fill % when the active model's context window is known. For proxy/custom models (GPT-5/Codex, xAI, Ollama- cloud) the window isn't in the registry, so it showed only a used-token count. Warm model discovery for the active provider in Init() (background, non-blocking) so the active model's window is learned at launch and the gauge renders 'used / total tokens' + the fill % for every model — not just catalogued ones. Builds on the discovered-window resolution from the compaction fix. * fix(tui): address review — agent fallback on model-switch + active-provider window - Apply modelregistry.AgentContextWindow to /model and /mode pre-switch compaction requests (TargetContextWindow), so the 'compaction for all models' behavior also covers undiscovered/custom switch targets, not just the active run. - Scope the live-discovered context-window lookup to the active provider first (then others), so a model ID shared across providers resolves to the provider in use rather than an arbitrary match. - Tighten the resolution test to compare against the registry's actual gpt-4.1 window instead of != FallbackContextWindow. * fix(cli): give provider-discovered context windows precedence in headless exec Address the remaining review point: CLI exec/spec runs collapsed every uncatalogued model to the 200k fallback, ignoring its real window. resolveAgentContextWindow now, on a registry miss, queries the provider's live model list (resolving the credential from inline/stored/env) to get the model's actual context window before falling back. Discovery runs only on a registry miss (catalogued models pay no latency), is bounded by a 5s timeout, and degrades to the fallback on any error so a headless run is never blocked or failed. |
||
|
|
653cee60c1 |
feat(tui): dim the transcript backdrop behind overlays for visibility (#333)
Slash-command/file palettes (and pickers/wizards) were composited directly over the live transcript with no separation, so the chat bled through around the box and the overlay was hard to read. Add a scrim: when an overlay is open, dim the transcript backdrop (strip each line's colors and re-render faint) before compositing the bright overlay on top. Header and composer render separately and stay bright. Text is preserved (just dimmed), so context remains readable behind the palette. |
||
|
|
8aae0be6db |
feat(tui): compact one-line /model switch confirmation (#331)
The model-switch confirmation printed a 6-7 line block (Switched model. / model: / provider: / api model: / effort: / saved:). Collapse it to a single summary line — '<model> · <provider>[ · effort …][ · saved]' — keeping the deprecation/redirect notice line when present. Tests updated to the new format. |
||
|
|
8ea8831c30 |
fix(model): expose reasoning effort for GPT-5 / Codex / o-series (#330)
/effort showed no controls for ChatGPT (GPT-5) because those models aren't in the curated registry and the OpenAI catalog entries carry no reasoning efforts, so Registry.ReasoningEfforts returned nil even though GPT-5 supports reasoning_effort. Add a name-based fallback: when a model isn't enumerated with efforts, infer them for known reasoning families — GPT-5 / gpt-5.x Codex (minimal/low/medium/high) and the o-series (low/medium/high). Non-reasoning models (GPT-4.1/4o, local models) stay empty, so /effort still hides controls where they don't apply. |
||
|
|
a4b2e01fe6 |
feat: encrypted provider credentials + provider-aware /model (#329)
Encrypted credential storage - internal/securefile: AES-256-GCM at-rest crypter (fail-closed, atomic, locked). - internal/credstore: provider API-key store, keyring-first with an encrypted-file fallback; plaintext only via an explicit ZERO_CRED_STORAGE=file opt-out. - Provider API keys no longer live in config.json: captured straight into the store from the wizard/CLI setup, and any existing inline plaintext key is migrated to the store on launch (fail-soft; stripped only after the store write succeeds). - buildProvider and every model/provider switch reload the credential (stored key, env var, or OAuth bearer), so a stored-key provider always authenticates. - `zero auth logout <provider>` now also removes the stored API key and its marker. /model and /provider - /model lists every authenticated provider (filtered to those with a usable credential), grouped per provider under section headers. - Each provider's models are live-discovered (its real served models) rather than a static catalog; local providers show only the models actually pulled. - Selecting a model from another provider switches provider and model together. - /provider offers keep / replace / remove when a provider already has a stored key. Gated throughout: go build, go vet ./..., go test ./... -race, lint. |
||
|
|
fb3277f46e |
feat(tui): tabbed ask_user questionnaire in the composer with suggested answers (#327)
* feat(tui): tabbed ask_user questionnaire rendered in the composer
Redesign the interactive ask_user prompt to match the requested mockup: when a
question is raised the composer text box becomes the questionnaire.
- Multi-question prompts render as a row of TABS (one per question + a trailing
Confirm tab). Tab / Shift+Tab switch questions, answers are given in any order,
and Enter on Confirm submits them all. A single-question prompt submits on
answer (no Confirm step) and shows no tab row.
- Each option can carry a one-line DESCRIPTION (contract: per-option
optionDescriptions, or option objects {label, description}); options render
numbered with the description dimmer underneath and the recommended one tagged.
- Per-question optional `header` gives each tab a short title (falls back to a
trimmed question).
- Rendered in the composer/footer region (footerView), replacing the text box
while active, instead of a scrolling transcript card.
- Keys: Tab move · ↑↓ select · Enter confirm · Esc dismiss; a printable key in
the picker drops into free-text; Esc from "type your own" returns to the picker;
multi-select questions are free-text with the options shown as suggestions.
- Headless unaffected (zero exec never wires OnAskUser).
Removed the old single-question sequential state (cursor/typing/index,
renderFocusedAskUserPrompt, resolveAskUser). Tests rewritten for the tabbed flow
(picker default, type-your-own, multi-question tabs + Confirm, Esc dismiss,
descriptions render). make build / go vet / go test ./... -race / make lint green.
* fix(tui): ask_user questionnaire renders on the black terminal canvas
It was painting a gray panel background (styledBlockFill with zeroTheme.panel +
onPanel-wrapped foregrounds). Since the questionnaire replaces the composer, it
should match it: bare foregrounds on the terminal canvas (black) with the
composer's lineStrong border and no fill. The lime cursor/badge highlights and
the recommended tag stay.
* fix(tui): address PR review — schema/parser alignment, single-question Tab
- ask_user schema: options.items is declared as string, so advertise only the
string form (descriptions go via the parallel optionDescriptions array). The
parser stays object-tolerant defensively, but the schema no longer tells
strict-schema providers to send objects it would reject.
- moveAskUserTab no-ops for single-question prompts (no tab strip / Confirm tab),
so Tab/Shift+Tab can't move the prompt into the hidden Confirm state. Test added.
|
||
|
|
95aaca6055 |
fix(tui): paint transcript selection once, not twice (#328)
Selecting text in the transcript lit up in two places: the real selection plus a copy shifted right by the reading-column gutter (e.g. selecting "help" also highlighted "today"). Root cause: assistant, user, and reasoning rows self-painted the selection highlight in their render functions using the UNSHIFTED textStart, but the selection points (anchor.x/cursor.x) are in the gutter-shifted screen coordinate the mouse maps to. So the self-paint landed gutter cells off, and finalizeTranscriptBodyRow then painted the highlight AGAIN at the correct shifted position — two highlights, one gutter apart. The rendered/toolResult/ specialist rows were already converted to paint-once-at-finalize; these three were left behind. Fix: stop self-painting in renderSelectableUserRow, renderSelectableAssistantRow, and renderSelectableReasoningBlock. They now return the canonical renderRow output (already line-aligned with their selectable spans) and let finalizeTranscriptBodyRow be the single painter, in the same shifted coordinate the mouse uses. Removes the now-dead renderTranscriptSelectableText / renderTranscriptSelectableMarkdownText helpers. Regression test TestTranscriptSelectionPaintsHighlightOnceNotTwice selects a sub-range of an assistant row and asserts the selection style is emitted exactly once; it fails on the pre-fix code (painted 3x, including the gutter-shifted copy). |
||
|
|
1d1fa7c514 |
feat(tui): ask_user suggested answers with a recommended default (#326)
* feat(tui): suggested answers with a recommended default for ask_user When the agent asks a clarifying question mid-task, it can now offer 2-4 suggested answers and mark one recommended; the interactive TUI renders a selector (arrow keys, lime cursor, "(recommended)" tag) with the cursor resting on the recommended option, plus a trailing "✎ Type my own answer…" entry that drops into the existing free-text composer. Whichever the user picks — a suggested option's text or their typed answer — flows through the same answer channel the agent loop already waits on. Optional and backward-compatible: a question with no options renders the plain free-text prompt exactly as before. Headless `zero exec` is unaffected (it never wires OnAskUser; the non-interactive best-assumption fallback stands). - contract: agent.AskUserQuestion / tools.AskUserQuestion gain Recommended; the tool schema advertises optional options + recommended; parser keeps recommended only when it resolves to one of the options (canonical text). - system prompt: tells the model it MAY supply options + a recommended one. - TUI: pendingAskUserPrompt gains cursor/typing; ask_user_prompt.go holds the selector logic mirroring permission_prompt.go; renderFocusedAskUserPrompt renders the picker or the free-text prompt. Tests: selector defaults to recommended and returns its text; "type my own" falls back to free-text and returns the typed text; no-options stays plain free-text. make build / go vet / go test ./... -race / make lint green. * fix(tui): ask_user — single input, Esc steps back from "type my own" Two UX fixes from interactive testing: - No double display: the focused ask card no longer echoes the in-progress free-text answer (it was shown both inside the card AND in the composer text box). The composer is now the single input; the card just shows the question and a hint. renderFocusedAskUserPrompt no longer takes the input string. - Esc from "type my own" returns to the selector for that question instead of cancelling the whole questionnaire (escapeAskUser); a question with no options, or the selector itself, still cancels on Esc as before. Tests: type-my-own asserts the card does not echo the typed text and the hint flips to "type your own answer"; new test covers Esc returning to the selector (no cancel, no answer delivered) then selecting a suggested option. * fix(tui): ask_user review fixes — multi-select, selector typing, dead code Address the three confirmed findings from the code review: - multi-select no longer silently narrowed to one option: a single-pick selector cannot represent MultiSelect, so a multi-select question now renders as free-text with its options surfaced as suggestions; the user types one or more and the answer is returned verbatim (syncQuestionState; escapeAskUser and the renderer updated to match). - typing in selector mode no longer discarded: a printable keystroke while the selector is showing flips into free-text and captures the key (model.go key capture), instead of accumulating in a hidden input that Enter then overrode with the highlighted option. - removed dead submitAskUserAnswer (the Enter handler now routes through confirmAskUser; the orphan had no callers). Tests: multi-select falls back to free-text and returns the typed answer; a printable key in the selector switches to free-text and is captured. Full gate green (build / vet / go test ./... -race / make lint). * fix(tui): address PR review — element-wise option compare, clearer Esc copy - ask_user_test: compare option slices with slices.Equal instead of strings.Join, so the parser test can't be fooled by ["A,B"] vs ["A","B"] or nil vs [""]. - ask_user prompt: the Esc hint said "skip", which reads like "skip this question" on a multi-question prompt even though Esc ends the rest of the questionnaire (remaining answered by best-assumption). Reworded to "skip the rest" in the selector and free-text hints; "type my own" keeps "go back". |
||
|
|
b8b0bb7f65 |
Move deferred tool discovery into tool search (#324)
* Move deferred tool discovery into tool search Keep deferred tool discovery in the tool_search definition instead of appending it as a per-turn user message, so models do not answer or acknowledge internal discovery metadata. Preserve deferred loading behavior: hidden tools remain hidden until loaded, tool_search stays reachable for allowlisted deferred tools, and retry paths keep loaded/deferred state. Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/agent ./internal/tools -run 'Test.*Deferred|Test.*ToolSearch|TestRunLoadsDeferredToolThenAdvertisesNextTurn|TestRunReactiveRetryKeepsLoadedDeferredToolAndDiscovery|TestRunConnectTimeReactiveRetryKeepsLoadedDeferredToolAndDiscovery|TestAllowlistedDeferredToolsKeepToolSearchReachable|TestDisabledToolSearchFallsBackToEager' Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/tools Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/agent ./internal/tools ./internal/cli Tested: git diff --check * Clarify deferred tool search selection |
||
|
|
f8ac5eceb0 |
fix(agent): gate headless run completion on honesty (no false-success on no-tool-call / self-reported-incomplete turns) (#325)
* fix(agent): don't end a run as success on a no-tool-call turn mid-task
A turn that produced text but no tool call was always accepted as the final answer, so the loop reported success even when the model stopped mid-task (e.g. ended on "...Let me check the SSH configuration:" with plan steps still pending).
Add an opt-in completion gate (Options.RequireCompletionSignal): when a turn has no tool call and work clearly remains -- pending update_plan items, or the message ends on a continuation cue -- re-prompt the model to continue instead of finalizing. Bounded by maxContinueNudges and still by MaxTurns/the deadline; once the budget is spent the run finalizes as INCOMPLETE (Result.Incomplete) rather than success. Default off, so the interactive path is byte-identical.
Genuine single-turn completions (no pending plan, no cue) still finalize as success. Covered by internal/agent/completion_gate_test.go.
* feat(exec): report stalled headless runs as INCOMPLETE (exit 4)
Enable the agent completion gate for headless exec (RequireCompletionSignal) and map Result.Incomplete to run_end status "incomplete" with a new exit code 4, so a run that stalled mid-task (model stopped without a tool call while work remained, continue budget exhausted) is no longer reported as success. Interactive callers are unaffected.
* fix(agent): downgrade self-reported non-completion; advisory task-grounded acceptance
Reduce -- not eliminate -- false-success on headless runs. Two DETERMINISTIC, unit-tested gates (with the plan gate from the prior commit):
(a) self-report downgrade: if the final message admits the model guessed or could not meet the objective, finalize INCOMPLETE (exit 4), never success. Inability is matched by first-person STEMS generalized over verb/tense ("I cannot/can't/could not/am unable to/do not have/unable to ...") plus guess/fallback/uncertainty phrases, with a guard so success-y negations ("could not find any issues", "cannot reproduce") are not misread. (b7bc0b8's plan gate already forces INCOMPLETE on pending/in_progress update_plan items at termination.)
(b) task-grounded acceptance is ADVISORY, not a guarantee. When --self-correct is on it demands one bounded acceptance pass that re-derives the task's stated criterion and runs a concrete check, discouraging three false-success patterns (well-formed==correct, existing-tests-pass==objective-met, result==baseline-it-was-told-to-beat). But it is a prompt: a model that ignores it and confidently claims "PASS, all requirements met" still slips, because ZERO has no general oracle to verify correctness against a task's hidden criterion. Empirically (TB-2, qwen3-coder:480b) this reliably catches admissions and incomplete plans and REDUCES false-success, but a confident false PASS on a model-ceiling task is a residual, fundamental gap -- not a tuning miss.
Default off (RequireCompletionSignal); interactive callers are byte-identical. Covered by internal/agent/{acceptance_gate_test.go,completion_gate_test.go}.
* feat(exec): surface the INCOMPLETE reason in run_end and logs
When a headless run finalizes as INCOMPLETE, include Result.IncompleteReason in the session error event and a stderr warning so an honestly-incomplete run (e.g. "the final message admits the objective was not met") is debuggable rather than an opaque exit 4.
* fix(agent): harden completion gate per PR review
Addresses the correctness findings from the PR review.
self-report (#1): drop behavior-describing phrases ("fall back to",
"placeholder value", bare "best guess", "as a fallback", "without proper")
that also match legitimate final answers; keep first-person/uncertainty
admissions only, since these are matched without a context guard.
self-report (#5): scan every occurrence of each inability stem so an early
success-negation ("could not find any examples") no longer masks a later
genuine admission with the same stem ("could not implement it").
continuation cue (#6): require a trailing colon AND an action lead-in on the
final clause; stop flagging recommendations, plain summary colons, and
sign-offs. Still catches the mid-line "...Let me check the config:".
gate order (#8): check the self-report admission BEFORE pending-plan, so an
admitted-impossible task downgrades immediately with the accurate reason
instead of burning continue-nudges.
pending plan (#3): treat a pending/in_progress update_plan item as a
NUDGE-only weak signal -- it no longer forces INCOMPLETE on its own (a
completed run that left stale plan bookkeeping is trusted). Only a
continuation cue or a self-report admission finalizes INCOMPLETE.
max-turns (#4): a run cut off at the MaxTurns ceiling now finalizes
INCOMPLETE under the gate instead of being reported as success.
exec json/cron (#2, #7): for -o json, emit the terminal done with exit 4 on
an incomplete run (final() pre-emits a success done:0 for json that would
otherwise mask it); emit an error event -- not just a warning -- so the cron
failure extractor can recover the reason.
Deferred (noted on the PR): acceptance-only-when-mutated (#9, cost) and
reusing tools.normalizePlanStatus (#10, would widen scope to internal/tools).
Tests: add TestContinuationCueMatching and TestMaxTurnsCutoffIsIncompleteUnderGate,
extend TestSelfReportedIncompletionMatching with the #1/#5 cases, and replace
the in_progress=>incomplete test with TestPendingPlanAloneDoesNotForceIncomplete.
make build / go vet / make lint / go test ./... -race all green.
|
||
|
|
d29ab98a79 |
Improve sandbox permission handling (#323)
Retry shell commands outside the sandbox when sandboxed output only reflects the sandbox namespace, and keep network retries sandboxed when a network grant is enough. Show user-facing permission reasons for escalated shell commands instead of exposing internal sandbox policy text. Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/agent ./internal/tui -run 'TestRunUsesEscalatedJustificationForPermissionPrompt|TestRunUsesUserFacingEscalatedFallbackForPermissionPrompt|TestPermissionPromptMapsEscalatedSandboxReason|TestRunRetriesShellUnsandboxedAfterSandboxNamespaceLimitedOutput' Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/tools Tested: env HOME=/tmp/zero-test-home-clean XDG_CONFIG_HOME=/tmp/zero-test-config-clean GOCACHE=/tmp/zero-go-cache go test ./... |
||
|
|
80ab238361 |
Align sandbox command approval behavior (#322)
* Align shell approval sandbox execution Make approved shell command-prefix executions use the escalated sandbox path when denied-read policy allows it, so remembered approvals do not still hit the restrictive sandbox. Allow auto sandbox mode to degrade to a direct command plan when native command wrapping is unavailable, while strict sandbox require continues to fail closed. Update regression tests for degraded execution, prefix-approved shell calls, and unavailable native sandbox metadata. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/agent ./internal/sandbox Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/tools -run 'TestRegistryRunsWithDegradedUnavailableNativeSandbox|TestBashToolRunsWithDegradedUnavailableNativeSandbox|TestBashRequireEscalatedBypassesNativeSandboxAfterApproval|TestExecCommandRequireEscalatedBypassesNativeSandboxAfterApproval' Tested: GOCACHE=/tmp/zero-go-cache go test ./... -run '^$' Tested: git diff --check * Align sandbox command approval behavior Evaluate reusable shell approvals across plain command segments so prefix grants can cover commands with safe read-only tails without relying on command-specific sandbox exceptions. Keep process inspection commands sandboxed by default; unsandboxed host access continues to require explicit escalation or a narrow command-prefix approval. Improve shell tool guidance around sandboxed additional permissions, full escalation, justifications, and narrow reusable prefixes. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox ./internal/tools ./internal/agent Tested: env HOME=/tmp/zero-test-home-clean XDG_CONFIG_HOME=/tmp/zero-test-config-clean GOCACHE=/tmp/zero-go-cache go test ./... * Normalize explicit sandbox backend platforms Infer the platform from a caller-provided sandbox backend name before choosing the manager runtime platform. This keeps Linux, macOS, and Windows backend plans deterministic across host OSes when tests or callers provide a backend without an explicit Platform field. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox ./internal/tools Tested: env HOME=/tmp/zero-test-home-clean XDG_CONFIG_HOME=/tmp/zero-test-config-clean GOCACHE=/tmp/zero-go-cache go test ./... |
||
|
|
98bd1acbe6 |
ci(release): build all 5 platforms (Intel macOS + Linux arm64) (#321)
* ci(release): build all 5 platforms (add Intel macOS + Linux arm64 runners) zero-release package builds + smoke-tests natively, so each target needs its own runner. The matrix had only ubuntu-latest/macos-latest/windows-latest (linux-x64, macos-arm64, windows-x64) — so install.sh/install.ps1 404 for Intel Macs (macos-x64) and Linux arm64. Adds macos-13 (Intel) and ubuntu-24.04-arm so the release covers all platforms the installers resolve. * ci(release): use macos-15-intel for the Intel macOS leg macos-13 was retired (June 2026) so the matrix leg would never schedule. macos-15-intel is the current GitHub-hosted Intel x64 runner (supported through Aug 2027). Addresses CodeRabbit review on #321. |
||
|
|
2b6568f82b |
chore: launch-readiness — add MIT LICENSE + reconcile README (#320)
* docs: add MIT LICENSE Adds the canonical MIT License (Copyright (c) 2026 Gitlawb) at the repo root, resolving the launch blocker where the project was marketed as open source with no license file (legally all-rights-reserved). * docs(readme): declare MIT license + badge, link LICENSE README's License section said 'being finalized'; now states MIT and links the LICENSE file. Adds a license badge to the header row. |
||
|
|
f1502f0bfe |
Add local control browser and terminal tools (#319)
* Add local control browser and terminal tools Add configurable local browser, desktop, and terminal control helpers with packaged helper discovery for install and release flows. Improve sandbox scope classification for local control commands and shared temporary paths. Add structured browser actions, terminal session line input/key normalization/read support, and artifact capture for local-control outputs. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/localcontrol ./internal/tools ./internal/cli ./internal/config -run 'Test.*LocalControl|TestTerminal|TestBrowser|TestDesktop|TestCapture' Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/localcontrol ./internal/tools ./internal/cli ./internal/config ./internal/agent Tested: tuistory smoke launched bash, sent interactive input, waited for expected output, and captured a no-cursor snapshot Tested: git diff --cached --check * Fix npm wrapper helper manifest test on macOS Canonicalize expected helper paths in the npm wrapper manifest test so macOS /var and /private/var temp path aliases compare correctly. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/npmwrapper Tested: git diff --check * Harden local control review fixes Respect resolved local-control config before tool listing and filter validation while preserving stream-json usage error ordering. Harden browser, desktop, and artifact tool validation; guard Linux-only app launch behavior; clear stale helper manifests from the npm wrapper. Make helper packaging lockfile-based with npm ci and add installer/package tests for helper staging. Tested: focused local-control/tool/CLI/release/install/sandbox suite Tested: isolated full go test ./... Tested: macOS and Windows compile-only checks for touched packages |
||
|
|
0e7318ff72 |
feat(tui): clickable plan-step detail + plan/sidebar UX & rendering polish (#315)
* fix(tui): plan panel reflects live progress + prompt cadence
The plan panel could read as stuck mid-task even while work continued. Three
fixes:
- Header showed steps[0] forever, so the title never advanced as the plan
progressed; show the current step (in_progress, else first incomplete).
- updateFromItems matched steps by content only, so an in-place reword reset a
step's elapsed timer to zero; add positional carry-over when the step count is
unchanged.
- Prompt: tell the model to mark each step completed + the next in_progress
before starting it, so the plan tracks real progress instead of jumping at the
end.
Regression tests included.
* feat(tui): click a plan step to see what was built for it
Each plan step now records the file mutations (write_file/edit_file/apply_patch)
made while it was in_progress, captured from tool-result rows. Clicking a step
row in the context sidebar drops a transcript card listing those changes — the
implementation for that step. Step->line mapping mirrors sidebarAgentSelectables'
offset accounting (one line per step); capture is keyed to the active step and
cleared per run. Read-only and additive. Tests cover the offset math + capture.
* feat(tui): plan-step detail shows diffs + commands
Extend the clickable plan-step detail: capture bash/exec_command runs in addition
to file mutations, store each tool's full output (the diff for edit/apply_patch,
stdout/stderr for commands), and render the card grouped into Changes and
Commands with a short truncated excerpt of each item's diff/output.
* fix(tui): plan-step detail toggles instead of stacking duplicates
Re-clicking a plan step now hides its card (a stable transcript id + drop-by-id),
and clicking a different step replaces it, so at most one detail card is shown.
Previously each click appended a fresh card and they piled up.
* feat(tui): elaborate plan-step detail by status (what we did / will do)
* feat(tui): plan-step card leads with a prose explanation (agent's own narration)
* feat(tui): write plan-step explanations with the model on click
Clicking a PLAN step now shows a fresh, plain-English write-up of that
step, generated on demand by the active model:
- past tense ('what we did') for completed/failed steps, future tense
('what we'll do') for pending/in_progress steps
- immediate feedback: the card drops in showing 'Writing explanation…'
and updates in place (by stable row id) when the text returns
- cached per step (content+status) so re-clicking is instant with no
second model call; re-clicking still toggles the card closed
- the prompt is built from already-captured local data (step text,
status, notes, plus a compact digest of file edits, commands, and the
agent's narration) and asks for short, non-technical prose
- reuses the TUI's existing provider via a one-shot StreamCompletion;
no new client, no hardcoded provider. Falls back to the local summary
when no provider is configured or the request fails.
* fix(tui): minimal plan-step card styling (dim border, status-tinted title)
* feat(tui): auto-hide context sidebar when it has no agents or plan
* feat(tui): widen chat reading column to fill wide terminals with breathing room
* fix(tui): toggle context sidebar silently (no transcript notice)
* fix(tui): mark plan complete when a finished task forgot the final update_plan
* fix(tui): remove the lime streaming-text glow (render responses in static ink)
* fix(tui): remove the shaded panel band behind tool-result cards
* fix(tui): Ctrl+B hides the plan entirely (no pinned-panel fallback)
* style(tui): gofmt-align model struct fields
* feat: narrate the build as a clean story (hide plumbing cards, live plan progress, narration prompt, write_file line counts)
* feat(tui): render agent narration bright (ink) so the build reads as a story
* feat(agent): demand an elaborate, structured closing summary (not a single line)
* feat(tui): mark agent narration with a leading bullet (selection-aligned)
* feat: post-turn recap line (※ recap:) with /config recaps on|off toggle
* feat(tui): show 'still generating…' during quiet stretches + count tool-call streaming
* feat(tui): ACTIVITY panel under PLAN + push the model to step the plan per file
* fix(tui): show the quiet 'still generating' hint only when the sidebar is hidden
* feat(tui): code preview in write/edit/apply cards (card-only, no model token cost)
* fix(tui): address CodeRabbit review — stale plan timestamps, stale explanation, Ctrl+B persistence, UTF-8 truncation
* fix: address CodeRabbit re-review round 2 (7 actionable + 2 nitpicks)
* chore: drop accidentally-committed WIP internal/cli/dryrun_test.go
It is an unrelated, pre-existing dry-run test that does not compile (references a
non-existent agent.Options.DryRun field, an unused import, and out-of-scope
sandbox symbols). It was swept into the prior commit by 'git add -A' and is not
part of this PR's scope.
|
||
|
|
ca41ac2b6c |
feat(agent): preamble before multi-step tasks for a readable chat (#317)
The prompt told the model "Use tools to act, not to narrate", which also suppressed the brief upfront "here's my approach" the user relies on to follow what the CLI is doing — so Zero jumped silently into tool calls. Keep the per-call anti-spam, but lead a multi-step task with a one- or two-sentence plain-language preamble. Prompt-only change; the TUI already commits streamed pre-tool prose as an assistant row, so no render change is needed. |
||
|
|
f9a26e7148 |
feat(tools): show red/green diff preview on write_file and edit_file (#318)
write_file/edit_file only reported "Created X (8462 bytes)" / "edited X", so the user never saw WHAT changed — no code preview, no red/green. The TUI already renders a colored diff card whenever a tool's output looks like a unified diff (diffFirstToolBodyRenderer + looksLikeDiff), so the fix is purely tool-side: emit a diff. - edit_file appends a unified diff (old vs new) after its summary line → red/green change preview. - write_file appends a unified diff of prior vs new content: a fresh create previews as all-green additions, an overwrite as red/green. It reads the prior bytes before writing only when the file existed. - boundedUnifiedDiff (via the already-vendored go-udiff, now a direct require) caps the emitted diff at 48KB so a large generated file can't flood the transcript or balloon persisted session events; past the cap only the summary shows. The summary stays the first output line so any consumer that reads it (swarm/Task result bubbling) is unaffected. Tests: edit emits @@/-/+ ; new-file emits additions-only; overwrite emits red/green. Full tools/tui/agent/swarm suites green. |
||
|
|
01fed03a59 |
fix(tui): stop ANSI escape leaks + drop redundant update_plan cards (#316)
Two chat-clarity fixes surfaced by comparing Zero's output to a reference agent on a "build a website" task. 1. ANSI escape garbage in the final answer. styleAssistantMarkdownLine treated already-styled input (syntax-highlighted code, headings, tables — which carry real ANSI) byte-by-byte as runes and re-wrapped the whole line in another base.Render. That doubled the SGR density and let a downstream width-truncation slice mid-escape, leaking "[38;2;…" / "[1;4;…" fragments as literal text next to code blocks. Now real escape sequences pass through verbatim (via the existing ansiSequenceEnd helper, like truncateStyledLine); the synthetic bold markers are still matched first so their style switch is kept. Only styled segments were affected — plain prose never had embedded escapes to split. 2. Redundant update_plan tool cards. The plan is already shown in the pinned plan panel AND the PLAN sidebar, so rendering each update_plan call as its own transcript card was pure duplication (it stacked 3x in the demo). Generalize the existing Task-card skip into toolCardSuppressedInTranscript and skip update_plan's call+result rows too; the plan-panel sync and session events are unchanged. Tests: TestStyleAssistantMarkdownLinePassesAnsiVerbatim (fails before the fix), TestToolCardSuppressedInTranscript. |
||
|
|
46c44a8753 |
feat(swarm): let swarm members build (write + sandboxed shell) (#313)
* feat(swarm): let swarm members build (write + sandboxed shell) Swarm subagents were effectively read-only: a member runs headless at "--auto low" (PermissionModeAuto), and ToolAdvertised strips every prompt-requiring tool from the model's list — so write_file/edit_file/ apply_patch/bash were never handed to the member. It only saw read/grep/ glob/plan tools, so it could not build and the orchestrator did all the writes itself. Add PermissionModeMemberAuto: a headless mode that ADVERTISES the in-workspace mutators (SideEffectWrite + SideEffectShell) on top of the Auto set, while behaving exactly like Auto everywhere else. The sandbox engine still decides at call time — in-workspace writes and sandbox-backed shell auto-allow; out-of-workspace writes, network, and destructive commands prompt and so are denied headless. member-auto normalizes to Auto in the sandbox layer, so no sandbox authority is widened beyond what an interactive auto agent already has. Scope: swarm members only. The launcher sets MemberAutonomy; the executor emits "--auto member" for a non-unsafe member (unsafe still -> high, plain specialists still -> low). Task-tool specialists are unchanged. Windows note: shell only auto-runs when the OS sandbox is active; without `zero sandbox setup` a member writes files but cannot run builds. * style: gofmt member_auto_test.go |
||
|
|
1c94168245 |
feat(tui): keep finished swarm members visible during the run (#312)
* feat(tui): keep finished swarm members visible during the run Finished swarm members vanished from the AGENTS sidebar after a 1.5s linger, so on a long turn (the orchestrator keeps working after the subagents finish) they were gone before the user could inspect them — the panel read "no agents spawned" mid-run and the clickable drill-in had nothing to click. Now a finished member stays in the panel (solid check, still clickable to open its subchat) for as long as the run is in flight, and only fades out and drops once the turn ends. Spawns are scoped to the active run (row.runID matches activeRunID) so a previous turn's members do not reappear when a later turn keeps members visible. * fix(tui): scope swarm member status to the active run Address review: swarmSpawnedAgents filtered spawn rows by activeRunID, but swarmMemberStatus still read swarm_status/swarm_collect rows from any run. With reused task ids, a stale prior-run status could mark a current member done and wrongly fade/drop it. Scope swarmMemberStatus to the active run too, and tighten the tests (non-zero run id; a stale same-id done status from a prior run). |
||
|
|
644f61fa58 |
Fix shared temp roots in sandbox scope (#314)
Treat platform temp directories as default writable roots so file tools, shell commands, and sandbox profiles share the same temp view. On Linux, bind host temp roots instead of mounting an isolated tmpfs at /tmp so artifacts created under /tmp remain visible across tool and shell calls. Tests: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox ./internal/tools ./internal/cli ./internal/agent |
||
|
|
11926cc5d2 |
feat(tui): click a swarm member in the sidebar to open its subchat (#311)
* feat(tui): click a swarm member in the sidebar to open its subchat Specialists were drillable (click the card → subchat shows the child session), but swarm members shown in the AGENTS sidebar were not — even though each member already runs as a `zero exec --init-session-id <sid>` child that persists a durable session in the same store the TUI reads. This wires that session through so a member row is clickable. - swarm: Coordinator records the member's child session id on completion (CompleteWithSession / Task.SessionID); swarm_collect emits a task_id → session_id map in its Result.Meta. - tui: the OnToolResult closure forwards that map via swarmSessionsMsg into m.swarmSessionMap; swarmAgent carries the session id; the AGENTS sidebar records a clickable hit per member whose session is known. - tui: sidebarLineAtMouse maps a left-click in the sidebar column to a member row (screen-Y == sidebar line index; sidebar starts at chatColumnWidth + 3), and the existing specialist-card drill-in path (subchat.enter) is reused verbatim. Members are clickable only once their session id is known (after swarm_collect), so in-flight members simply aren't clickable — no dead clicks. The subchat machinery, Result.Meta transport, and the click handler are all reused; no new persistence or agent-loop changes. * test(tui): assert sidebar member click drills into the member session Address review: the routing test only checked the click was handled, so it could pass even if the drill-in didn't enter the expected subchat. Seed a real session in the store and assert subchat.active + childSessionID == the member's session after the click. |
||
|
|
258f1a31c9 |
fix(swarm): make swarm_collect wait for members instead of polling (#310)
* fix(swarm): make swarm_collect wait for members instead of polling Swarm spawn was fire-and-forget and the only way to learn a member had finished was to call swarm_status/swarm_collect, which returned an instant snapshot. So an orchestrator had to poll in a loop, flooding the chat with repeated status cards and never getting a clean "done". swarm_collect now BLOCKS until every member of the team reaches a terminal state, then returns their results in one call: - Coordinator gains a change-broadcast channel and WaitSettled(ctx, team), which blocks until the scope has no pending/running task (or ctx is done). Every state transition (register/setstatus/finish/reassign) wakes waiters. - Swarm.CollectWait wraps it; the collect tool calls it bounded by the run context (user cancel) and a timeout (default 600s, cap 1800s, overridable via timeout_seconds), so a stuck member returns partial state instead of hanging forever. - Tool descriptions now steer the model: spawn then collect once; status is a one-shot snapshot, not something to poll. TUI: collapseRepeatedStatusCard drops the previous identical swarm status/collect card when a check repeats verbatim back-to-back, so any residual re-checks don't stack duplicate blocks in the chat. * fix(swarm): address review on blocking collect (race + timeout overflow) - WaitSettled: take the settled decision and the returned snapshot in the SAME locked pass. Previously the snapshot was re-read after releasing the RLock, so a concurrent Register/Reassign could make the call return a non-terminal task it never waited on — breaking the blocking contract. Removed the now-dead scopedList; shared sortTasksByCreation with List. - collectWaitTimeout: clamp timeout_seconds in float space before converting to time.Duration. A very large value overflowed the int64-ns Duration and wrapped negative, making context.WithTimeout fire immediately instead of honoring the cap. - test: release the gated member via defer in TestCollectBlocksUntilMembersFinish so a failing assertion can't leak the blocked goroutine; add a collectWaitTimeout clamp/overflow unit test. |
||
|
|
4a5a417156 |
fix(tui): keep running swarm subagents in the AGENTS sidebar (#309)
The sidebar cleared the whole swarm roster the moment ANY swarm_collect result appeared, so calling swarm_collect mid-run (to poll for partial results) made the AGENTS panel show "no agents spawned" even while every subagent was still running. Drop that collect-clears-everything heuristic and instead let the member-status linger logic remove members only once they actually report done/failed. swarm_collect results carry the same "[state]" roster as swarm_status, so swarmMemberStatus now reads both — a mid-flight collect refreshes the live states instead of wiping them. Each member also surfaces a non-running state (pending/handoff/...) inline so the panel reports real status, not just liveness. |
||
|
|
8acc1be7b2 |
feat(tui): live token counter on the working line (#308)
* feat(tui): live token counter on the working line The working line ended with a static "↑ from-to/total" scroll indicator (which transcript rows the line covered). It looked like a token counter but never moved meaningfully, so a running turn felt frozen. Replace it with a live "↑ <n> tok" estimate that climbs as the model reasons and writes. turnStreamedRunes accumulates every streamed reasoning+answer rune for the turn and survives the per-segment buffer clears (a tool call wipes streamingText/Reasoning), so the count is monotonic across a multi-tool turn and resets to zero on the next turn. The figure is an estimate (~4 chars/token) because providers only report exact usage when a step finishes; the authoritative totals stay in the status line and sidebar. Removes the now-unused phase_indicator.go (scroll indicator + helpers) and its test, which were only referenced by the working line. * fix(tui): keep the working-line token counter visible from turn start The counter returned empty until the first streamed delta, so during the initial think phase the working line showed no counter at all - a regression from the old always-present row indicator. Show it for the whole turn starting at 0 and climbing. |
||
|
|
0b1fdef98e |
chore: scrub named other-CLI references from the codebase (#307)
* docs: drop other-agent names from Zero's description
AGENTS.MD described Zero as "in the same family as Claude Code, Codex,
and Droid". Because AGENTS.MD is injected into the model as project
context, the agent parroted that sentence verbatim when asked who it is,
naming other agents in its own identity.
Reword it to describe Zero on its own terms ("an open-source terminal
coding agent") with no comparison to other products.
* chore: scrub named other-agent references from the codebase
Follow-up to the AGENTS.MD wording fix: sweep the rest of the tree for
comments and docs that named specific other CLIs and replace them with
neutral phrasing ("comparable terminal agents" / generic positioning).
- internal/tui/rendering.go, model.go, session_controls.go (+ test):
drop the named other-agent references from doc comments; the behaviour
and the existing generic "reference agents/TUIs" phrasing are unchanged.
- docs/PRD.md: replace the named competitor in the target/vision with
generic "multi-provider terminal agents".
The ChatGPT Codex provider integration keeps its name — that is a real
product Zero connects to, not a reference to another tool's source.
|
||
|
|
bc5c24ef87 |
fix(tui): stop showing the token count on both sides (#306)
When the sidebar is open it pins the token readout at its floor, but the status line also rendered its own token usage segment, so the token figure appeared twice (status line and sidebar). The status line already drops the context-fill gauge while the sidebar is open; do the same for usage. To avoid losing the session cost (which the sidebar does not show), keep a cost-only segment in the status line when the sidebar is active, falling back to empty until a priced record lands. |
||
|
|
215170b967 |
feat(tui): show provider on each model-picker row (#305)
The /model picker rendered a flat list of model names with no provider context: modelPickerOverlay dropped the Group and provider dot that the generic picker shows, so models from different providers were indistinguishable. Each model row now carries a right-aligned provider slug (anthropic, deepseek, ollama, ...). The slug is stamped onto every item via applyProviderPickerMeta from the catalog descriptor, and renderModelPickerRow right-aligns it with the same gap math the generic picker uses, dropping it on rows too narrow to keep a clear gap. |
||
|
|
212f1a0ff8 | Fix token accounting and usage display (#304) |