Skip to content

release: v0.6.3 (#423–#425, #460–#462) - #463

Merged
plombeer31 merged 17 commits into
mainfrom
rc/2026-09-17
Sep 17, 2026
Merged

plombeer31 merged 17 commits into
mainfrom
rc/2026-09-17

Conversation

@plombeer31

Copy link
Copy Markdown
Collaborator

What

Release integration for v0.6.3: the open maintainer PRs merged into one branch on top of main (which already carries the harness stack #426#439, #452, #453, #459), so the release is built from exactly what was tested.

PR Change
#423 ### profile honours memory.voting.profileFilterThreshold — a downvoted fact, pinned or not, leaves the prompt
#424 Atomic Chat (local) provider preset; an External llama.cpp URL on :1337 opens the wizard on it
#425 /onboarding (/setup), a Setup menu row and tui --onboarding re-run first-time setup without touching providers, keys, models, sessions or memory
#460 the forced reply after a wandering loop stop is worded by its detector (refs #458)
#461 a steered message is labelled YOU · steered into the running turn; [try again] re-sends the turn's request, not the steer
#462 a throttled connection can no longer hold a model download: slow connections are swapped, idle ones take the rest of a slow slice

#423 and #425 were rebased onto main after the harness stack; neither changes harness behaviour (#423 only filters which facts render in the profile section, #425 touches TUI files only, no config bump). All six merge into this branch without conflicts.

How it was verified

  • npm run lint clean; npm run build clean.
  • vitest on the branch: src/agent src/session src/tui 351 files / 4 089 tests pass; src/local-llm src/tui/local-models src/tui/onboarding 68 / 820; src/tui src/prompt src/memory src/agent src/providers src/llm src/config src/runtime src/analytics 546 / 6 791. One uncaught spawn … llama-server EACCES from local-models-orchestrator-auto-update.test.ts reproduces identically on main on the release Mac (all 8 of its tests pass) and is not introduced here.
  • Release builds (workflow_dispatch, publish=false) on this branch before fix(tui): mark a steered message so the running turn's reply is not read as its answer #461 succeeded on all four targets; the signed darwin-arm64 binary boots, migrates config to v69, and opens first-run setup on a fresh state directory.
  • fix(local-llm): a throttled connection can no longer hold a model download #462 live: a local origin with one connection throttled to 512 B/s finishes 200 MB in 32 s (main: 94% after 120 s); a real stuck Hugging Face partial resumes to a file matching x-linked-etag.

Atomic Chat's desktop app exposes an OpenAI-compatible Local API Server
on 127.0.0.1:1337 under /v1, but the wizard had no preset for it, so
users added it by hand as a custom/cloud endpoint. Add a keyless local
preset (ATOMIC_CHAT_API_KEY for servers where a key was set in the app),
land a :1337 External llama.cpp URL on it the way :11434 lands on
Ollama, and name the preset in the refusal line for a local :1337 URL.
A wandering escalation ends the turn through the breaker path, but
formatForcedLoopReply never received the detector, so a turn of eleven
distinct, successful fetches closed with "stuck in a no-progress loop on
`os.web.fetch` after 12 blocked attempts" and "the repeated tool call".
Only one call was blocked and nothing was repeated; the veto text on the
same event already said "12 different attempts".

The agent loop now passes breaker.detector through, and a wandering stop
says the tool reached N different arguments this turn, the per-turn cap,
and that the last call was not run. Every other detector keeps its
existing wording. The detector itself is unchanged.

Refs #458
…a repeat

Review of the first commit found the new reply could still be false:

- A parallel batch is gated against the same recorded history, so its
  calls can carry the spread past the cap before anything is refused.
  The next stop then quoted, say, 14 as "the cap". The reply no longer
  presents the count as the cap: "hit the limit on different arguments
  within recent tool calls — 14, counting the last call".
- The spread covers the tracker's history window, not the whole turn, so
  "this turn" is gone from the wording.
- Past that overshoot, a verbatim repeat is still vetoed by the
  escalation, but the breaker signal took the repeat verdict (count 0),
  so the turn ended on "a no-progress loop after 0 blocked attempts".
  When the escalation alone forces the breaker, the signal now carries
  detector "wandering" and the spread (new ToolLoopTracker.wanderingSpread).
  The veto body keeps its repeat wording, as the existing test requires.

The breaker log line also records the detector.

Refs #458
…ead as its answer

A message sent while a turn is running is folded into that turn at the
next step boundary (mid-turn steering). The loop recorded it as a plain
`user` turn and the TUI drew it with the same `YOU` bubble as a prompt
that opens a turn. The model keeps working on the turn's opening
request, so the transcript showed that request's tool steps and reply
directly under the steered text, as if they answered it.

A Discord report ("the model's response to Test is strange") matches
this frame for frame when reproduced: a slow first local turn asking for
the weather in Tokyo, `Test` sent before step 0, then the thread reopened
from another thread with a running turn. The stored transcript renders
as YOU Test, reasoning (3 blocks), os.web.search 0ms, os.web.fetch 0ms,
AGENT <Tokyo weather>, then the detached-turn notice.

The steered user turn now carries `steered: true` (agent loop), the flag
reaches the chat message from both the live `steer_applied` event and a
stored session, and the bubble label reads
"YOU · steered into the running turn".
Once first-run setup was skipped, finished, or any backend got
configured, decideOnboarding() never opens the flow again, and nothing
in the app led back to it.

- /onboarding (alias /setup) and a Setup menu row "Run first-time setup
  again…" clear every tui.onboarding stamp and open the flow on its
  splash. The once-only offers (second backend, local setup seen,
  import) are cleared too, so a requested re-run shows the screens a
  skipped first run missed.
- Only the stamps change: providers, keys, sessions and memory are left
  alone.
- Refused with a notice while a turn is running or waiting on an
  approval, because the flow covers the whole app, prompts included.
- `atomic-agent tui --onboarding` does the same from the shell; it
  cannot be combined with --skip-llama-setup.
buildPrompt passed userMessage and contextualKeywordGate to
clipProfileSection but never the vote threshold, so selectProfileFacts
always ran with profileFilterThreshold = 0 (filter disabled) and
downvoted profile facts, pinned ones included, kept rendering into
every prompt.

Default the threshold from config.memory.voting.profileFilterThreshold,
with an explicit BuildPromptInput override; 0 still disables the filter.
- Browsing the local branch no longer writes config. Enter on "Local
  models" (and accepting the local offer) used to persist
  localModels.mode = "managed" before any model was chosen; on a re-run
  that turned a working external endpoint into managed-with-no-model
  just by pressing Enter and backing out. The write now happens when a
  curated or Hugging Face model is picked, still before the pull, so a
  Ctrl+C mid-download keeps the choice.
- Re-runs stay out of the first-run funnel. The flow state carries
  `rerun` (true when decideOnboarding would not have opened setup), and
  useOnboardingLifecycle skips onboarding_step reports for it.
- `--onboarding` clears the stamps just before the first render instead
  of at the top of tuiCommand, so a launch that dies while the runtime
  boots leaves them alone. The flag beats ATOMIC_AGENT_TUI_SKIP_LLAMA_SETUP
  on purpose; only the two flags together are rejected.
…gain] on the steer

The newest user message in the chat was taken as the turn's request.
After a steer that is the steer. So stopping a turn that `Test` was
steered into put `retryText: "Test"` on "Agent stopped by user.", and
the run-history entry recorded `Test` as the run's message. Clicking
that notice's [try again] re-sent only `Test`, as a new plain turn,
without the request it had corrected.

`lastUserMessage` is now `lastTurnRequest` and skips steered messages.
It has two callers, and both name the turn: the stopped notice's
`retryText` and `withRunHistoryEntry` (completed, cancelled and failed
runs). Neither wants the latest text. The composer's up-arrow history
is fed separately by `appendUserMessage` and still gets the steer.

A steered bubble no longer has its own [try again]. Re-sending a steer
alone recreates the confusion the label removes: a new turn on a
correction without its request. Resending the opening request instead
would copy the button right above it, on the message that opened the
turn, and a button that sends different text from its bubble would be
a surprise. Removing it is the smallest change. [copy] stays, and the
footer is still one row, so the height estimate is unchanged.

Left as is: live, a steer applied after tool steps is drawn above
those steps. The cards stay in the streaming tail until the reply. To
split them at the steer, the steer would have to finalise the tail
into its own message. That would clear the reasoning ring the
Reasoning tab reads mid-turn and rescope the `● N tool steps` count.
Too much to change for an ordering gap the label already explains.
When the running turn's opening message is not in the chat list (a turn
opened over Telegram or HTTP, or a switch-back replay that dropped its
oldest events), lastTurnRequest walked past the steer to a request from
a turn that had already finished, and the stopped notice offered to
re-run it. Past a steer, a final assistant reply now ends the walk and
no retry is offered. Interim progress notes carry `progressNote` on the
chat message so they do not count as that boundary.
…nload

A parallel download waited on its worst connection. The stall watchdog
only fires on silence and is re-armed by every chunk, so a connection the
CDN slows to a few hundred bytes a second never trips it, and the plan is
made once per attempt, so connections that finish their slice go home.
A 2.7 GB GGUF sat at 96% for hours with a 116 MB remainder on one ~400 B/s
connection while a fresh connection to the same URL ran at 1.9 MB/s.

- Pace check: every 30 s a connection among several is compared with the
  best per-connection rate this attempt has seen (clock-measured windows
  plus the average of every connection that stops). Below 1/8 of it and
  under 256 KiB/s it is dropped and its remainder re-requested on a new
  connection, without reporting a retry — the download chip reads a retry
  as a network outage. Silent windows stay the stall watchdog's call. A
  segment stops being judged after three reconnects in a row that did not
  help, so a link slow everywhere costs at most three requests per
  segment while a CDN that throttles each connection after a fast start
  is worked around for as long as it does that.
- Idle connections take work: a connection that finishes with the queue
  empty cuts the back half off the running slice with the most bytes left
  (both halves at least the 8 MiB floor). The donor's end moves to the cut,
  which its stream already re-reads on every chunk; a chunk crossing the
  cut is clamped out of the donor's count so progress and the sidecar
  count every byte once.

Single-stream downloads, the retry and give-up policy and the sidecar
format are unchanged.
@plombeer31
plombeer31 merged commit bce310d into main Sep 17, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant