Skip to content

integrate: Discord-sourced fixes (#286–#298) - #299

Merged
plombeer31 merged 35 commits into
mainfrom
integration/discord-fixes-2026-08-31
Aug 31, 2026
Merged

integrate: Discord-sourced fixes (#286–#298)#299
plombeer31 merged 35 commits into
mainfrom
integration/discord-fixes-2026-08-31

Conversation

@plombeer31

@plombeer31 plombeer31 commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Integration branch combining the 13 open Discord-sourced fix PRs, now rebased-by-merge onto current main (post #271/#272/#273/#274/#276) so it can be merged into main.

Contained PRs (merged in numeric order)

Conflict resolutions

#286 vs #287src/agent/step-executor.ts (4 hunks)

#293 vs #298src/cli/models-handlers.ts

Config version renumbering — src/config/config-schema.ts

#272 vs #286/#287src/agent/step-executor.ts (merge of main)

Verification

  • npm run lint (tsc): clean.
  • Targeted union npx vitest run src/agent/ src/llm/ src/prompt/ src/tui/ src/skills/ src/config/ src/local-llm/ src/cli/ on Node 25.7: 383 files / 4340 tests, all passed (0 failures vs the ~10-known-flaky full-suite baseline).
  • One pre-existing non-failing vitest "unhandled error" (fixture llama-server spawn EACCES from local-models-orchestrator-auto-update.test.ts, byte-identical to main; exit code 0).

Валерий Брижатюк and others added 30 commits August 31, 2026 15:35
Two-part fix for issue #285 (verbatim chain-of-thought delivered to the
user as the agent's reply, and native_tools having no effect on model
behaviour).

Guardrail (step-executor): the native_tools reasoning-only salvage no
longer wraps an unparseable `reasoning_content` body into a
`reply { text }` call. A GBNF-shaped batch embedded in the think channel
is still recovered as real tool calls, but anything else now returns a
parse failure and routes through the existing one-shot repair
(parse_retry + buildToolCallRepairPrompt, REPAIR_MAX_TOKENS-capped). A
repair that fails too ends the step as a GrammarError — a parse error,
not a CoT leak.

Root cause (prompt): the stable prefix was transport-blind — under
native_tools the request carried an OpenAI `tools` array with
`tool_choice: "auto"` while the prompt simultaneously ordered "Emit a
JSON ARRAY of tool calls now" and the persona mandated "exactly one JSON
array". `buildStablePrefix` now takes `toolTransport` (threaded from
`StepDependencies` through `BuildPromptInput`): under native_tools the
persona's emission mandate and the `### instructions` block switch to
native function-calling guidance. The `### tools` text catalog stays in
both modes — the provider fallback chain can hand a native-shaped
request to a grammar-only llama-server link, and the catalog carries the
tier / `tool.view` semantics. The grammar-path prefix is byte-identical
to before (KV-cache safe; verified by sha256 against the previous
implementation across profile/turn-framing/win32/persona variants).

Known caveat: on a native-to-grammar mid-session fallover the prompt
lacks the JSON-array mandate while the grammar link parses GBNF. This is
acceptable because llama-server's GBNF grammar constrains decoding to
the array shape regardless of the prompt mandate.

Fixes #285

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…roviders

The trailing `<think>` prefill (and Gemma turn-framing tokens) are
llama-server text-completion artifacts: the local template expects the
open tag pre-typed at the generation point. On the native-tools chat
transport the same prompt ships as a chat message to an OpenAI-
compatible endpoint, where the literal tag is at best noise the model
echoes back — and at worst corrupted server-side: Ollama Cloud mangles
literal `<think>`/`</think>` strings in message content
(ollama/ollama#17248), the trigger for #283.

The injection only fired in hybrid configs (a local llama-server
probing a think-tag model while completions route to a cloud
provider), but there it also mis-parsed clean cloud replies:
`normalizeContent` re-prepended the open tag and the stream parser
started pre-opened, so a reply that never emitted `</think>` was
swallowed whole as reasoning.

- build-prompt: new `suppressReasoningPrefill` input drops the
  trailing reasoning prefill, the Gemma turn framing, and the
  reasoning system token; the step executor sets it for
  `toolTransport: "native_tools"`.
- step-executor: parsing no longer assumes a prefill that was not sent
  (`promptCarriesReasoningPrefill` / `completionAssumesOpenReasoning`);
  grammar-parsed completions keep the legacy prepend — the GBNF
  prelude root structurally starts mid-think — including on
  cross-transport fallover. The one-shot repair prompt stops
  re-appending `<think>`.
- profile-invariants: `checkProfilePromptAligned` learns the
  suppressed shape (the prompt must NOT end with a reasoning prelude).
- provider-presets: document the upstream Ollama Cloud corruption next
  to the preset.

The issue's proposed blanket rewrite of thinking-tag strings in all
outgoing content for ollama.com endpoints is deliberately NOT
implemented: silently mutating user text and tool results is the same
silent-corruption class relocated client-side, and the server-side
half is Ollama's bug to fix.

Fixes #283

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eplay drift

Adversarial review of the issue #285 fix found three residual problems;
this commit closes all three.

1. `### rules` still opened with the text-array mandate ("One tool-call
   array per step ... a solo action is a length-1 array") in BOTH
   transports, contradicting the native `### instructions` block in the
   same prefix. The rules line is now transport-variant like the persona
   and instructions: the grammar line is byte-identical to before
   (sha256-verified against v0.4.2 across omitted/explicit, win32,
   persona-override, reasoning-token and turn-framing variants), the
   native line mandates the function-calling interface. The same sweep
   caught one more survivor in the shared persona — "if another tool is
   next, emit that tool JSON" — which likewise becomes "call that tool"
   under native_tools only.

2. `buildToolCallRepairPrompt` was transport-blind: on the exact
   parse_retry path the #285 fix routes reasoning-only completions
   through, it appended "Emit a corrected JSON array only" / "Use a
   length-1 array" onto a native prefix that forbids text-JSON — the
   dual mandate recreated at the one retry a failing model gets. The
   repair mandate now follows `deps.toolTransport`: native repairs order
   a corrected native tool call, grammar repairs keep the legacy lines
   byte-for-byte. A new probe test captures the second llmComplete
   prompt under native transport and asserts no text-array mandate
   survives anywhere in it.

3. `atomic-agent trace replay` regressed into 100% false drift for
   native-transport sessions: the prefix now differs by construction per
   transport, but `replaySession` always rebuilt the grammar variant and
   traces do not record which transport served the session. The replay
   is now transport-aware: `ReplayContext.toolTransport` pins the
   comparison when the caller knows the transport; when omitted (the
   trace-command case) both variants are built and a recorded hash is
   clean when it matches either — old (pre-#285) traces keep matching
   through the byte-identical grammar variant. Each step reports which
   variant matched (`matchedTransport`).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-transport fallover

Adversarial review of the prefill-suppression fix (#283) found a real
streaming regression in the documented default hybrid chain —
cloud (native_tools) primary with a grammar local last resort
(`appendLocal`) on a think-tag profile:

- consumeStream keyed `preOpenedThink` off the PRIMARY transport, but a
  grammar-served fallover stream starts mid-`<think>` (the GBNF prelude
  root emits `body "</think>"` with no open tag), so the parser
  silently swallowed the reasoning text — no live `reasoning_delta`s
  for every sticky-override turn of an outage, where main surfaced
  them. The `servedTransport` stamp on the stream's return value
  arrives only after the last delta, too late to reconfigure a parser.

Fixes, in dependency order:

- completion-types: `StreamChunk.servedTransport` — the fallback
  streamer seam now stamps the serving link's transport on EVERY chunk,
  not just the final result, so live consumers can adapt up front.
- step-executor.consumeStream: the stream parser is created lazily off
  the first chunk's stamp (primary transport when unstamped, i.e. the
  direct non-fallback path), restoring live reasoning classification
  for grammar-served fallover streams.
- llm-fallback-seam: per-link prompt substitution. The main prompt for
  a native-tools primary is prefill-suppressed, which handed the
  grammar fallover link a prompt/template mismatch (GBNF still forces
  mid-think output). `LlmStreamParams.grammarPrompt` carries a lazy,
  memoized prefill-carrying variant built only when a grammar link is
  actually chosen; the one-shot repair retry rebuilds both variants
  repair-shaped so a fallover retry never sees the stale base prompt.
- completionAssumesOpenReasoning now keys purely off the served/parse
  transport: grammar-served output always continues an open think
  block; a chat completion never does. This also stops the (documented-
  unsupported) grammar-primary -> native-link ordering from swallowing
  a clean chat reply whole as reasoning; the literal prefill still
  shipping to the chat link in that reverse ordering remains, matching
  AGENTS.md's "order native-tools links at or above the first
  grammar-only link".
- profile-invariants: the prefill-suppressed branch also flags leaked
  Gemma turn-framing tails, not just reasoning open tags.

Coverage for the pinned invariant 8 gap that let this slip past CI:
fallback-e2e now exercises think-tag profiles through the REAL seam
factories, unary and streaming (live deltas + per-link prompt shape),
llm-fallback-seam.test pins the per-chunk stamp and the grammarPrompt
substitution, and step-executor.test pins both fallover directions.
All 8 new tests fail without the src changes (verified by stashing).

lint clean; agent+prompt+llm 877/877; runtime+tui/providers 336/336;
full suite 6447/6448 — the one failure
(sidecar/send-message-concurrency) fails identically on pristine
origin/main in a clean worktree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…catalog

The config key skills.catalogTokenBudget (env
ATOMIC_AGENT_SKILLS_CATALOG_BUDGET, default 512) was parsed and typed but
never read: every buildSkillCatalog call site invoked it without options,
so the '### skills' catalog was always cut at the hardcoded 4096-char cap
and the knob silently did nothing.

Thread the configured budget through all three call sites (runtime
bootstrap initial build, refreshSkills rebuild, trace replay). The key
speaks tokens while the catalog builder caps characters; convert at a
named SKILL_CATALOG_CHARS_PER_TOKEN = 8 factor, chosen so the shipped
default of 512 tokens maps exactly to the historical 4096-char cap —
users who never set the key see byte-identical prompts (and keep their
KV cache) across the upgrade. An explicit maxChars still wins over
tokenBudget for direct callers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nv knob

Adversarial review of #288 proved the fixed bug was not regression-
guarded where it lived: reverting only the bootstrap/trace-command call
sites to main (the exact reported defect) left all relevant tests green.

- bootstrap.test.ts: boot with ATOMIC_AGENT_SKILLS_CATALOG_BUDGET=4 and
  assert runtime.skillCatalog is built with that budget, both at boot
  and after refreshSkills() — covering both bootstrap call sites.
- trace-command.test.ts: run `trace replay` twice (default vs 4-token
  budget) against seeded global skills and assert the recomputed stable-
  prefix hash shifts — covering the replay call site.
  Both new tests verified to FAIL with the two call-site files reverted
  to origin/main, and pass with the fix.
- skill-catalog.test.ts: the default-budget mapping guard now imports
  ENV_DEFAULTS.SKILLS_CATALOG_BUDGET instead of hardcoding 512, so a
  future default change trips the 4096-char back-compat check.
- load-config.ts: ATOMIC_AGENT_SKILLS_CATALOG_BUDGET is now read via
  readBoundedPositiveInt (clamped to [1, 100000]) so a zero/negative
  value cannot drive maxChars to 0 and collapse the catalog; documented
  in config-schema.ts and covered in load-config.test.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Activating a completion row submitted the raw editor buffer through
handleEditorSubmit, and resolveSlashCommand is exact-match only — so
with "/mod" typed, clicking the "/model" row errored with
"unknown command: /mod" instead of running /model.

Run the clicked row's own completion instead, through the same
runSlashCommand path the keyboard palette-highlight branch uses on
Enter: the row the operator clicked is the choice, whatever prefix is
in the buffer.

Reported on Discord: a click on a slash-command completion submits the
typed buffer instead of the clicked command.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Users keep pointing the External llama.cpp URL at Ollama (:11434). The
/health probe classifies it openai-compat and refuses the save; since
26102be the verdict is at least visible on the panel's status line, but
it was a dead end — acting on it meant retyping the same URL into a
wizard four screens away.

Verified against a live `ollama serve`: /health answers 404 and
/v1/models answers the OpenAI list shape, so the probe's verdict for
Ollama is exactly `openai-compat` (status 404).

- On an openai-compat refusal the External pane now opens a steer
  prompt naming the server; `y` deep-links into the provider wizard on
  the OpenAI-compatible route with the probed URL prefilled, `n`/Esc
  dismisses. A URL on Ollama's default port lands on the "Ollama
  (local)" preset — same state as picking that row by hand (entry id,
  env var, no key screen), but keeping the operator's own host — and
  any other compat server lands on the manual row's URL screen.
- describeLlamaHealthFailure names Ollama outright for :11434 URLs and
  steers to the Ollama preset row, so the onboarding custom-URL branch
  (which shares the describer) stops calling an Ollama user's server
  merely "OpenAI-compatible".

Tests: verdict-to-steer wizard mapping (preset vs manual row, host
kept), the Ollama-named describer line, looksLikeOllamaUrl, and the
prompt's reducer + key flow (y opens the prefilled wizard, n/Esc
dismisses, hotkeys swallowed). All fail without the change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The v0.4.2 synchronized-update bracketing (eb690e7) stopped the frame
tearing but not the Win10 conhost reports: residual shaking, and a
duplicated last row under PowerShell. Both fit one mechanism the
bracketing cannot address — the TUI pins its root to `height={rows}`,
and the frozen inbox conhost scrolls when a full-height frame writes
into its bottom row; DEC 2026 is ignored there, and a scroll is buffer
movement, not tearing. Once the viewport slides one line, the repaint
cursor math is off by one: the UI shakes and the row that scrolled away
leaves the last row painted twice.

No Windows machine was available to reproduce, so the change is the
conservative guard: when the host looks like a legacy conhost (win32,
and neither WT_SESSION nor TERM_PROGRAM set), `useTerminalSize` reports
one row fewer, so no frame ever touches the bottom terminal row and
there is nothing left to scroll. Every modern host — Windows Terminal,
VS Code, anything setting those variables — keeps the full height, and
non-TTY streams (tests, pipes, CI) are untouched.

A one-time transcript hint on such consoles recommends Windows Terminal
and names the escape hatch: ATOMIC_AGENT_CONHOST_GUARD=0 disables the
guard, =1 forces it on anywhere — which is also how it was verified: a
PTY+pyte run at 80x24 shows a 23-row frame, a never-written bottom row,
and the hint; with the variable unset the frame is unchanged from main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reported on Discord: the TUI stops showing reasoning tokens partway
through long turns. The mechanism is in the grammar stream parser, not
the TUI ring buffer: while inside a think block, the first bare "{" or
"[" in the buffer was treated as the start of the tool-call payload,
which emitted reasoning_close and silenced every later reasoning delta
of that step. Chain-of-thought that mentions JSON, code or array
notation hits this almost immediately, so the live reasoning display
froze until the step's final canonical reasoning event replaced it.

The close-sentinel-less handoff the early exit exists for (pre-opened
think, model goes straight to the payload) is preserved: a brace now
ends the reasoning stream only when it actually begins a
'{"tool": "' payload (optional array opener allowed). A candidate cut
off by a chunk boundary ('{"to') is held back until it resolves, with
a 64-char probe cap so the holdback - and the buffered memory - stays
bounded; anything that provably is not a payload streams on as
reasoning. Stream end treats an unresolved candidate as reasoning too.

Five of the six new parser tests fail without the fix; the sixth pins
the new holdback behaviour at chunk boundaries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…uild cannot serve

Windows machines without a working GPU compute stack — typically
iGPU-only boxes like the AMD 5600G — were unrescuable: backend variant
selection only knew the Vulkan and CUDA turboquant zips, so when the
Vulkan build crashed or hung on model load there was no CPU build to
reach and no way to ask for one.

- windows-backend-variant: register the llama-turboquant-windows-x64-cpu.zip
  asset the nightly repo already publishes, plus a configured-variant
  preference (auto | cpu | vulkan | cuda-12.4 | cuda-13.3) that bypasses
  the nvidia-smi probe and its process-wide cache.
- config v46: localModels.managed.backendVariant, default "auto";
  pushed into the local-llm layer from loadConfig the same way
  setCustomLocalModels is.
- daemon-lifecycle: the chat health-wait failure is now a typed
  DaemonHealthError, distinguishing "this compute backend cannot serve
  on this machine" from pre-spawn failures a backend swap cannot fix.
- cpu-backend-fallback: shouldFallBackToCpuBackend (pure eligibility:
  win32 + variant auto + GPU asset installed + DaemonHealthError) and
  fallBackToCpuBackend (flip preference, stop the half-started daemon,
  re-download).
- TUI start and CLI `models start` both fall back automatically with a
  surfaced message, persist backendVariant "cpu" so the next
  auto-update's variant-staleness check cannot reinstall the broken GPU
  build, and retry the start once on the CPU build.

Reported on Discord (l.hk, Win10, AMD 5600G iGPU, still broken on
v0.4.2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Engaged users on Discord report the memory subsystem is opaque enough
to block commitment to the tool. The existing MEMORY.md is an
engineering source-of-truth, not an explainer.

Add MEMORY_GUIDE.md, an operator-facing walkthrough verified against
src/memory and src/tools/memory:

- the five stores (profile facts, notes, links, lessons, procedures)
  and the two automatic writers (reflection, consolidator)
- where the SQLite lives and how to inspect it with sqlite3
- how recall reaches the prompt (### profile/lessons/procedures/
  memory-index/recalled, pointer-first with drill-down tools)
- three worked example transcripts: a profile fact forming and
  gating, a note recalled a week later, a lesson distilled from a
  note cluster
- inspecting via the TUI Memory tab and /memory dump
- forgetting, per-layer master switches, and the full-wipe recipe
- OBSIDIAN_VAULT_PATH and the obsidian starter skill vs agent memory

Link it from README (memory section + core docs list) and from the
MEMORY.md complements list.

Reported on Discord: https://discord.com/channels/1515649306781155428/1515649308161085612/1542988707907248148

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The cloud catalogs run past 300 rows and the only way to shop by price
was typing 'free' into the ranked search, which also subsequence-matches
unrelated ids. Both cloud model list surfaces now carry a price facet
next to their existing search, cycled with p: all → free → paid → all.

- Shared predicate in src/llm/provider/model-pricing-filter.ts: a row is
  free only when its catalog pricing renders the "free" tag (both
  prices zero; openrouter/auto stays out — its tag reads "routed" and
  it bills the routed model). Rows without pricing metadata (aimlapi,
  live /v1/models ids) cannot be promised free and stay under paid, so
  the two facets partition the catalog and no row vanishes from both.
- LLM pane, Cloud text models: p cycles the facet (visible on a new
  price: line under filter:, with the key hint); while the filter row is
  focused p stays query text. Facet flips snap the cursor to the top of
  the result set, same rule as filter edits.
- Providers wizard, curated chat-model screen: p cycles while the search
  box is closed; the active facet rides on the title ("· free only")
  and the hint line names the key. advanceWizardPhase resets the facet
  with the search, so it never leaks into the embedding screen — and the
  pane and wizard keep separate facet state, so neither screen leaks
  into the other. Default is all everywhere.

Tests: predicate unit tests; wizard key tests against a live-seeded
mixed catalog (narrow, cycle, cursor snap, Enter-selects-narrowed-row,
no leak into pick_embedding, p-as-query-text with the box open);
CloudRows component tests (price line + hint, free/paid narrowing with
the filtered-of-total counter); pane key + reducer tests. All 10 new
behavioral tests fail with the implementation stashed.

Reported on Discord: https://discord.com/channels/1515649306781155428/1515650562430079048/1536840031329853652

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reported on Discord: ssh from a Mac into the TUI and the mouse layer
"spams coordinates" as visible text while the app looks hung. Four
layers of hardening:

- mouse-stdin: hold a chunk-final lone ESC for 10ms. An ssh hop
  re-chunks the stream, and a report split right after its ESC used to
  type `[<64;3;9M` into the composer — the reported spam. If the rest
  of a report follows it rejoins and decodes; if nothing does, it was
  the Escape key and flushes (under Ink's own ~20ms lone-Esc deferral).
- decoder: consume urxvt/1015 reports instead of dropping them through
  as text, buffer truncated CSI heads so split sequences reach Ink
  whole, and document + cover that 1005 already lands in the X10 branch.
- leak breaker: report-shaped text about to reach Ink (a burst in one
  read, or three drips across the session, counted across read
  boundaries) trips once — strips the shapes, disables tracking for the
  session only, and posts a warn notice naming /mouse and --no-mouse.
- signals: a second SIGINT/SIGTERM/SIGHUP now restores the terminal
  (mouse reporting off, alt screen left) and exits 130 instead of
  Node's default kill that skips exit hooks and leaves the shell
  printing coordinates on every click.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…y corpus

Adds an `atomic-agent memory` command with an `export` subcommand that
mirrors the cross-session memory corpus (notes / lessons / procedures
in <stateDir>/memory.sqlite) into an existing Obsidian vault as
markdown files with YAML frontmatter (type, created, updated, tags)
and [[wikilinks]] along the schema's own edges: memory_links rows,
consolidated_into back-pointers, and lesson/procedure parent ids.

v1 design decisions:
- one-way and read-only: the database is opened readonly and never
  migrated; older schemas degrade gracefully (missing tables => empty)
- stable id-based filenames (note-<id>.md, ...) under a vault
  subfolder (--folder, default 'atomic-agent') so records keep their
  Obsidian identity and backlinks across re-exports
- idempotent and overwrite-safe: content is a pure function of the
  rows, unchanged files are not rewritten, and pruning of stale files
  is restricted to the machine-owned <kind>-<n>.md name patterns
  inside the export subfolders — user files are never touched
- soft parent pointers to evicted rows are skipped, not rendered as
  dangling links
- --vault falls back to $OBSIDIAN_VAULT_PATH (including via
  <stateDir>/.env); no watch mode, no sync-back in v1

The new command implements the 0/1/2 exit-code split (usage errors
return 2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add localModels.managed.tensorSplit (config v46, default [] = feature
off). Two or more non-negative ratios launch the managed chat daemon
with --split-mode layer --tensor-split <ratios> so the model's layers
spread across GPUs proportionally.

Previously resolveManagedDevice always pinned exactly one device
(pickBestDevice), so no multi-GPU launch could ever be expressed: a
pinned --device defeats --tensor-split. With a split configured, the
auto device preference now leaves every GPU visible instead of pinning
the best one; cpu still wins outright, and an explicit device id passes
through unchanged so a comma-separated list (Vulkan0,Vulkan1) can
restrict which devices join the split. models use-device now accepts
that comma-separated form.

Default behavior is byte-identical: with tensorSplit empty the launch
args and the single-device auto-pick are unchanged, and the embedding
daemon always keeps pinning one device. Ratios are validated at parse
time (at least two finite non-negative numbers, at least one positive).

Reported on Discord (managed llama-server multi-GPU support):
https://discord.com/channels/1515649306781155428/1515649308161085612/1536440638902636574

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The steer prompt's key branch sat before the External URL draft's, while
LlmPanelModals renders the draft first. Both could be non-null — the
steer opens asynchronously after the save's /health probe, and Enter on
the External row reopens the draft in that window — leaving the visible
editor keyboard-dead while y invisibly opened the wizard. The handler
now checks the steer after the draft (matching what is on screen), and
the reducer refuses to open the steer under an open draft in the first
place. Also: the vacuous swallow test now proves its baseline, and the
:11434 refusal text routes a remote Ollama through the manual compat row
(the preset row has no base-URL screen, so by hand it could only save
localhost).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two claims did not survive review against the code:

- Worked example 3 said every prompt carries the lesson pointer.
  ### lessons / ### procedures are query-gated: the per-turn recall
  query (user message + recent tool-result summaries) is BM25-matched
  against activation/principle/tags and only the top recallK hits
  (2 each) render; the unconditional list is the TUI Memory tab.
  State this in the prompt-sections chapter and in the example.

- The dedup bullet called the 0.85 threshold a BM25 similarity. FTS5
  only fetches the candidates; the threshold is compared against a
  Jaccard token-overlap similarity computed in JS
  (memory-store.ts jaccardSimilarity).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… and progress

The TUI path of the CPU fallback called fallBackToCpuBackend(dataDir)
bare: no AbortSignal, no onProgress. download-file.ts has no default
timeout, so a stalled-open connection pinned the start on phase
'starting' for the life of the process, with zero feedback during the
27-39MB download — the exact hazard the auto-update path (and this
feature's own CLI path) already guards against. The download now runs
with AbortSignal.timeout(BACKEND_DOWNLOAD_TIMEOUT_MS) and surfaces as
a regular backend pull (started/progress/finished/failed) on the bus.

Also covers the fallback WIRING with integration tests — previously
only the extracted pure pieces were tested, so deleting either
caller's retry block left the suite green:
- local-models-orchestrator-cpu-fallback.test.ts: eligible health
  failure swaps + persists + retries exactly once; recursion cap;
  failed download reported; non-health and cpu-installed cases inert;
  the download carries a deadline and live progress.
- models-handlers.test.ts: CLI retry lands on device 'cpu' (no stale
  --device Vulkan0), persists the variant, forwards signal/progress,
  pre-spawn failures and failed downloads exit non-zero untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adversarial review proved a hole in the tripped path: forwardText ran
the remnant replace per-chunk, so a straggler split across reads — the
exact ssh re-chunking this PR exists to survive — still leaked into the
composer after the breaker tripped (write `[<0;9` then `;9M` and Ink
received `[<0;9;9M` verbatim), contradicting the "keeps stripping the
in-flight stragglers" contract.

Unlike the pre-trip counter, which scans an already-forwarded tail,
the stripper has to keep the bytes out of Ink — so it withholds a
chunk-final remnant *prefix* until the rest arrives (stragglers trail
each other by well under a millisecond) or a 10ms timer rules it
ordinary typing, mirroring the ESC-split hold. Four new tests cover the
split straggler, a byte-at-a-time straggler, a partial straggler on the
tripping chunk itself, and the timer releasing withheld typing; all
four fail without the fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts:
#	src/agent/step-executor.ts
Валерий Брижатюк added 5 commits August 31, 2026 21:58
# Conflicts:
#	src/cli/models-handlers.ts
#	src/config/config-schema.ts
…6-08-31

# Conflicts:
#	src/agent/step-executor.ts
#	src/config/config-schema.ts
@plombeer31
plombeer31 merged commit 9c65205 into main Aug 31, 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