Skip to content

Bugfixes batches#69

Merged
inercia merged 240 commits into
mainfrom
fix/mitto-8qp-llr-tfb-bugfix-batch
Jul 14, 2026
Merged

Bugfixes batches#69
inercia merged 240 commits into
mainfrom
fix/mitto-8qp-llr-tfb-bugfix-batch

Conversation

@inercia

@inercia inercia commented Jul 14, 2026

Copy link
Copy Markdown
Owner

No description provided.

inercia added 30 commits July 5, 2026 21:28
…warm-up (mitto-8qp)

Resolves RPC context-deadline storms (~157/day, sonnet-5) caused by an
aggressive shrinking retry budget that timed out during large-context
model warm-up. The prior attempt-1 timeout (12s) was smaller than the
observed warm-up latency of large-context models (e.g.
claude-sonnet-5-0-500k, >12s), so every attempt of the shrinking
{12s,8s,5s} schedule timed out even though most of the outer 90s
setModelAsyncCallerBudget sat unused.

Widen the per-attempt schedule to {20s,15s,8s} (~44s total per caller),
which still stays within the contention bound covered by
setModelAsyncCallerBudget at up to 4 concurrent callers. Update all
mirrored comments/docs describing the old 25s math, and add a
cross-package mirrored-constants-drift convention note so future
schedule changes remember to update internal/conversation's local
mirror.

Add TestSetModelSchedule_LargeContextModelSucceedsWithinOuterBudget to
internal/acpproc/acp_process_manager_test.go.
… cline (mitto-llr)

Audit of vendor-specific MCP server discovery scripts found suspicious
config paths causing missing MCP servers in agent discovery. Corrects
claude-code (now reads ~/.claude.json) and fixes codex and cline
discovery scripts.

Add TestMCPList_RealConfigPaths_mitto_llr table-driven regression test
in internal/agents/manager_test.go.
…saturation poll (mitto-tfb)

The saturation-detection infrastructure from mitto-13ck.2 tracked
consecutive session/new + LoadSession + set_model context-deadlines but
only ever failed fast — it never recycled the degraded shared process,
so a saturated process could hang around indefinitely.

Add a public, non-mutating IsSaturated() accessor on SharedACPProcess
(distinct from the private isSaturated(), which self-clears to probe
mode) so the GC's health tier can poll saturation state without
perturbing the state machine, and wire it into the GC to proactively
recycle degraded idle shared processes.

Add TestGCHealthTier_RecyclesSaturatedIdleProcess.
…-consumed

Add config.DefaultModelProfiles() as the canonical, hardcoded set of
7 model-capability profiles (Claude/Opus/Sonnet/Haiku/GPT-5/GPT-4/
Gemini), kept in sync with config/config.default.yaml's `models:`
block by `make check-model-tags`. Unlike config.default.yaml, which
only seeds settings.json on first run, these profiles are always
available via the new (*Config).EffectiveModelProfiles(), which unions
settings.json's Models with the canonical defaults (user profiles win
on name collision).

Route all tag/name resolution (ModelProfileByName, ModelProfilesByTag,
ResolveModelTags) through EffectiveModelProfiles so a prompt's
preferredModels (modelTag/modelName) resolves even when settings.json
predates or omits `models:` — previously this silently no-opped to the
baseline model.

Add config.CanonicalModelTags() (sorted, de-duped tag set) and wire it
into GET /api/config as model_tags so SettingsDialog.js can suggest
canonical tags via a <datalist> when editing a profile's tags.

Add `make check-model-tags` target validating builtin prompt modelTag
references and Go/YAML drift.
Per-folder/global shortcut toolbars (conversation, beads-issue,
tasks-list) can carry several shortcut buttons that overflow the
toolbar on phone-width viewports. Keep only the first shortcut visible
below 640px via a new .mitto-shortcut-extra utility applied to buttons
at index > 0.

Also document the un-loop/re-loop LoopStore persistence symmetry
requirement and note that a concurrent loop conversation sharing the
same repo can stash/reset the working directory mid-task, silently
dropping a prior turn's uncommitted edits.
…model ERROR (mitto-5q8)

Part A: add JS-heap-OOM stderr patterns to stderrCrashPatterns so
StartStderrMonitor fires onCrashDetected immediately on agent Node/V8
heap exhaustion, speeding proactive recycle instead of waiting for the
60s control-request timeout.

Part B: in applyConfigOptionWithOpts, downgrade the set-model failure
log to WARN ("Skipping model change; agent process is restarting")
when IsACPConnectionError(err) is true (dead/restarting shared ACP
process), since this is a transient restart-gap condition rather than
a genuine model-selection failure. Genuine failures on a live process
still log ERROR. Control flow and the wrapped error return are
unchanged.
…err (mitto-9o6)

execRunner.Run discarded stdout on failure, so when bd exits non-zero
during dolt backend warm-up but writes its diagnostic to stdout (or
nothing) instead of stderr, the logged error carried stderr="" and the
real cause was invisible.

Add diagnosticOutput(): use stderr when bd wrote to it, otherwise fall
back to trimmed stdout, rune-safe length-capped at 2000 chars to avoid
flooding logs. IsNotFound is unaffected since bd's not-found message
always goes to stderr.

Graceful degradation (the other half of mitto-9o6) was already covered
by the existing runBeadsRead bounded-retry logic.
Loop conversations auto-archived due to broken ACP (ArchiveReasonACPFailures)
previously stayed archived forever unless a human noticed and manually
unarchived them. Add LoopRunner.checkAutoUnarchiveRecovery(), polled once
per minute from RunOnce():

- Eligibility: archived with ArchiveReasonACPFailures and a loop config
  still exists (store.Loop(id).Get() succeeds). Manual/inactivity archives
  and non-loop ACP failures are excluded.
- Retry cadence: ~1h per conversation, anchored on
  Metadata.AutoUnarchiveLastAttemptAt (or ArchivedAt) so it survives restarts.
- Anti-storm stagger: 10m global minimum gap between attempts; at most the
  single most-overdue session is retried per poll.
- A retry performs the same steps as a manual unarchive: clear archive
  fields, resume the ACP process, broadcast state changes, and reuse
  handlers.Handlers.RestoreLoopOnUnarchive (now exported) to re-enable
  the loop.
- Persist the attempt timestamp before invoking the callback so it
  survives a crash mid-resume; clear it only on success.

Adds the AutoUnarchiveLastAttemptAt metadata field, wiring in server.go,
a full test suite in loop_runner_test.go, and doc updates.
Error toasts previously auto-dismissed after 10s like warnings, risking
a user missing a critical message. error-style toasts now never
auto-dismiss; they persist until the user closes them via the dismiss
button.
'auggie mcp list' resolves <workspace> to the git toplevel, not the
Mitto workspace's working_dir. A workspace whose working_dir is a git
subdirectory sees servers registered in <git-root>/.augment/settings.local.json
instead of its own file, so the MCP tab can show servers the running
agent never actually loads. Document the divergence and workarounds
(move to git-root config, register at user scope, or point working_dir
at the git root).
…ix, and investigate prompt

- Replace the single AGENTS.md 'How to answer customer questions'
  section with a per-channel knowledge file
  (.mitto/slack-support-<channel-id>.md), so each Slack channel keeps
  its own repos/docs/runbooks/escalation guidance instead of sharing
  one project-wide note.
- Fix the need-info state machine: once the customer replies to a
  need-info bead, it now transitions to awaiting-us instead of staying
  stranded in need-info.
- Fix the mitto_ui_textbox 'Result' field name from the non-existent
  'full' to 'text' across all support prompts.
- Add 'Support: investigate' — a new prompt that digs for an answer
  using the channel knowledge file, records findings on the bead, and
  produces a draft reply without ever posting to Slack.
- 'Support: reply to user' now reuses an existing DRAFT comment instead
  of regenerating it, persists the draft to the bead before opening the
  review dialog, and correctly branches on submit/timeout/abort so a
  draft is never lost when the reviewer is away.
…e busy (mitto-1h0)

The mitto-tfb proactive recycle (GC Tier 5) only recycles a saturated shared
ACP process when it is fully idle. During the 2026-07-06 15:53-15:55
recurrence, session/new and set_model timed out at full RPC deadline while
those very RPCs were in flight (ActiveRPCs>0), so Tier 5's idle gate skipped
the process and it stayed wedged, starving user and auxiliary sessions.

Add GC Tier 6: recycles a CONFIRMED-degraded process (saturationLevel >= 2,
i.e. it tripped saturation, served its cooldown, and its post-cooldown probe
also timed out) even while busy, dropping the ActiveRPCs()/IsPrompting idle
gate. Guarded by a mandatory no-streamed-progress check (SessionInfo's new
LastStreamActivityAt) so a legitimately slow-but-progressing prompt is never
killed. Tier 5 is unchanged.

- shared_acp_process.go: non-mutating IsConfirmedDegraded()/SaturationLevel()
  getters (saturationMu-guarded, never perturb the probe state machine).
- session_info.go / background_session.go / session_manager.go: plumb
  LastStreamActivityAt from BackgroundSession into SessionInfo.
- acp_process_gc.go: Tier 6 block after Tier 5, same recycle action
  (MarkGCSuspended + sessionClose + StopProcess).
- acp_process_gc_test.go: recycle-busy-degraded, skip-progressing-degraded,
  skip-level1-busy.
- docs/devel/acp.md: document Tier 5 and Tier 6.
…d of bare 500

bd refuses to auto-apply pending schema migrations to a remote-backed
database (only one designated clone may migrate it), so every read against
that store previously failed and surfaced to the frontend as a generic 500
with no indication of what was wrong or how to fix it.

- internal/beads/beads.go: IsSchemaSkew() detects the skew signature in a
  command's stderr; SchemaSkewDBPath() best-effort parses the offending
  database path out of the same stderr for the remediation message.
- internal/web/handlers/beads.go: writeBeadsError() now distinguishes a
  schema skew from a genuine internal error, responding HTTP 409 with a
  remediation hint (db path + migrate/bootstrap instructions) instead of a
  bare 500.
- internal/web/handlers/helpers.go: new errCodeBeadsSchemaSkew error code.
- web/static/components/BeadsView.js: renders a distinct actionable error
  card (schemaSkew state) when the list load fails with that error code,
  instead of the plain error text.
- Unit tests for IsSchemaSkew/SchemaSkewDBPath and the handler's 409 path.
…odified (mitto-tf9)

GET /api/workspace-prompts derived its Last-Modified header (and the
If-Modified-Since 304 short-circuit) solely from the workspace .mittorc
mtime, even though the returned prompt list is merged from several other
sources whose files can change independently: the global prompts dir
(MITTO_DIR/prompts, including builtins deployed by `mitto prompts
update-builtin`), the settings file, additional configured prompts_dirs, and
the workspace's own .mitto/prompts. A prompt change that didn't touch
.mittorc kept getting 304'd until an unrelated .mittorc mtime bump, so the
frontend never saw it without a hard reload.

computeWorkspacePromptsLastModified() now takes the max mtime across every
contributing source, so any of them advancing the header correctly
invalidates the client's cached list.

Adds a regression test (TestWorkspacePrompts_BuiltinDeployAdvancesLastModified)
deploying a builtin prompt without touching .mittorc and asserting the
conditional GET returns 200 with the new prompt.
…mitto-8ul.1)

cgw-managed-tools conversations failed to start because Auggie blocks
servicing session/new until its MCP servers finish initializing
(~225s), while SharedACPProcess used a fixed 25s per-attempt / 75s
total budget. The RPC timed out, tripped the saturation circuit
breaker after 3 consecutive failures, and left the conversation stuck
in is_prompting=true with no useful feedback.

- SessionConfig.McpInitTimeout (default 240s, configurable/disableable)
  in internal/config/settings.go.
- SharedACPProcess.coldMCPBudget()/beginMCPInitWindow() widen the
  session/new and session/load per-attempt and total budgets to
  MCPInitTimeout only for the first call on a cold process; once one
  session RPC succeeds (mcpInitDone) subsequent calls use the normal
  25s budget again.
- Saturation guard: a DeadlineExceeded during the extended cold window
  is expected agent-side latency, not evidence of a hung process, and
  no longer counts toward the saturation threshold.
- Stderr-signature detection (bgsession_acp_process.go) recognizes when
  the agent reports MCP-init progress or an internal MCP-init timeout;
  the latter aborts the pending RPC immediately via mcpInitTimeoutCh
  instead of waiting out the full budget.
- New mcp_initializing / mcp_init_timed_out WebSocket notifications
  (server.go, ws_messages.go) drive frontend toasts
  (useBackgroundNotifications.js, useWebSocket.js) so the UI shows
  "Starting MCP servers..." instead of an indefinite spinner.
- RecommendedLoadTimeout() lets shared_session_handshaker.go widen its
  outer 30s session/load timeout so it doesn't truncate the extended
  budget.
- Mock ACP server updated to simulate delayed/timed-out MCP init for
  the new integration tests.

Tests: mcp_init_budget_test.go (5 unit tests on budget selection),
tests/integration/inprocess/mcp_init_timeout_test.go
(TestMCPInitTimeout_FailsFastOnStderrSignal,
TestMCPInitDelay_ExtendedBudgetAllowsSuccess). Full integration suite
143/0, gofmt/vet clean.
…mitto-mqe)

LoadPromptsFromDir silently skipped any .prompt.yaml file that failed
to parse, with only a code comment noting "In production, this would
use a logger" — so a broken prompt file vanished with no diagnostic
signal anywhere.

- LoadPromptsFromDirWithErrors (prompts.go) is the new entry point:
  returns both the successfully-loaded prompts and a PromptLoadError
  per failing file, and logs each failure at WARN.
  LoadPromptsFromDir is kept as a thin wrapper for existing callers.
- PromptsCache.reload() collects load errors across all scanned prompt
  directories; PromptsCache.LoadErrors() exposes the most recent set.
- handleGlobalEventsWS (events_ws.go) sends a persistent error-style
  toast to newly-connected clients for each outstanding load error,
  covering the case where a prompt file was already broken at server
  startup (not just edit-time).

Tests: TestLoadPromptsFromDirWithErrors_ReportsBadFileAndKeepsGood,
TestPromptsCache_LoadErrors_ReportsBadFile. gofmt/build/vet/config
tests green.
… only slow workspaces (mitto-mw0)

Replace eager pinning of all workspaces with a self-selecting model: warm a
keepalive session, probe its health (session/new latency + MCP readiness), and
pin only workspaces that are slow or broken. Healthy workspaces cost nothing —
GC reaps them normally.

Config:
- Add PrewarmConfig at Config.Prewarm with thresholds session_new_fast=10s,
  mcp_ready=10s, healthy_probes_to_unpin=3, max_pin_duration=30m,
  max_pinned_workspaces=5 (yaml + nil-safe parse accessors).

Pin state:
- Add Pinned/PinReason/PinExpiry to SessionInfo.
- Pin state machine on ACPProcessManager: PinWorkspace, UnpinWorkspace,
  IsPinned, RecordPrewarmProbeResult, ExpirePinsAndAlert, FirePrewarmPinAlert.

Probe + controller:
- probePrewarmHealth times the PurposeKeepAlive session/new and reads
  MCPInitDone/MCPInitTimedOut to compute a health verdict with reason codes
  (session_new_failed, mcp_timeout, mcp_not_ready, slow_session_new).
- Hysteresis: unpin only after N consecutive healthy probes.
- MaxPinDuration cap folded into EnsureMCPBackoffRetry via PrewarmPinReevaluator
  (no extra timer).

GC:
- Tier-1 (recycle) and Tier-2 (shutdown) pin exemptions, expiry-aware so the
  max-pin-duration cap self-heals a stuck pin.

Observability:
- SetOnPrewarmPinAlert -> BroadcastPrewarmPinAlert (prewarm_pin_alert WS message)
  fired at-most-once per pin for MCP-timeout pins and on cap expiry.

Tests: 8 prewarm_pin tests + 3 GC pin tests + 2 config tests, all green.
Follow-up: mitto-yns (frontend toast handler for prewarm_pin_alert).
…tto-29q)

coldMCPBudget's mcpInitDone latch only granted the extended MCPInitTimeout
budget for the FIRST successful session/new on a shared process. Agents
like Auggie re-run the full MCP initialize handshake on EVERY session/new,
so every subsequent conversation or unarchive on the same warm process fell
back to the normal 25s RPC deadline and failed with context deadline
exceeded while the agent was still waiting on MCP servers.

Edge-detect the false->true transition of mcpInitInProgress in
onMCPInitProgress (CompareAndSwap) so it fires once per handshake episode
instead of once per process lifetime, and let coldMCPBudget /
RecommendedLoadTimeout re-grant the extended budget whenever
mcpInitInProgress is true, even after mcpInitDone has latched.

Adds regression coverage for budget re-grant after a prior success
(TestColdMCPBudget_ReinitAfterFirstSuccessReExtends) and for reverting to
the normal budget once idle (TestColdMCPBudget_WarmIdleNotInProgressRevertsToNormal).
…tto-f51)

PromptWithMeta's deferred session/new handshake ran with the session's base
context (no deadline) while isPrompting was already latched true. On a
cold-start MCP handshake this could sit for up to ~240s per retry attempt
with no watchdog armed and no streamed activity, so a wedged handshake left
the conversation stuck in is_prompting forever with no recovery short of
archive/unarchive or a manual ForceReset.

Add pdRecommendedHandshakeDeadline() to read the shared process's own
RecommendedLoadTimeout hint, and bound each completeDeferredHandshake
attempt in a goroutine watched by runHandshakeWithWatchdog against that
deadline plus a margin (falling back to handshakeWatchdogFallback when the
process reports no recommendation). A watchdog trip surfaces a friendly
"still starting up" message, resets prompting state, and does not retry
(the orphaned goroutine still holds the shared process, so retrying would
just re-hang).

Adds regression coverage for the watchdog firing on a wedged handshake and
for the fallback deadline being used when the dependency reports zero.
mitto-mw0's adaptive ACP/MCP pre-warming broadcasts a prewarm_pin_alert
WebSocket message when a workspace is pinned due to a slow MCP-init, or
when the max-pin-duration cap force-releases a still-broken pin, but there
was no frontend consumer so the warning never reached the user.

useWebSocket.js's handleGlobalEvent dispatch re-emits the message as a
mitto:prewarm_pin_alert CustomEvent; useBackgroundNotifications.js listens
for it and shows a warning toast naming the workspace (workspace_name,
falling back to the basename of working_dir), with expired-aware copy
distinguishing "pin released after max-pin-duration cap" from "workspace
pinned due to slow MCP", and appending the reason when present.
…(mitto-54k.1)

On cold start, ~N sessions in a workspace reconnect their WebSockets
nearly simultaneously; each spawned its own unbounded goroutine calling
sessionManager.ResumeSession, saturating the Mitto process and starving
the agent's inbound HTTP initialize/tools/list on :5757/mcp (timeouts up
to 560s blocking the first prompt).

Introduce a small counting semaphore (resumeSemaphore) on *Server that
bounds concurrent interactive ResumeSession calls to a configurable
limit (default 3). Acquire INSIDE the spawned resume goroutine so the
WebSocket handler is never blocked; the frontend still receives
'connected' immediately with is_running=false. Release after
ResumeSession returns (success or failure), preserving all existing
post-resume side effects (negative-cache invalidation,
tryAttachToSession, BroadcastACPStarted, TriggerFollowUpSuggestions).

The user-focused ensure_resumed foreground path (session_ws.go:~1994)
intentionally BYPASSES this bound so the session the user is actively
looking at resumes first, even when the cold-start fan-out has
saturated the interactive pool.

Config knob 'startup_resume_concurrency' mirrors the existing
'startup_stagger_ms' pattern (config.SessionConfig field +
Default/Get accessor, threaded through internal/config/config.go
raw->typed). Wired in server.go next to the loop-runner stagger.

Tests: TestResumeSemaphore_{BoundsConcurrency,ForegroundBypass,
ClampsNonPositiveCapacity,NilReceiverIsNoop} in
internal/web/resume_semaphore_test.go +
TestSessionConfig_GetStartupResumeConcurrency in
internal/config/settings_test.go.
…n (mitto-54k.2)

Fix B for mitto-54k (Cold-start MCP starvation): independent safety net
alongside Fix A's resume-storm bound (mitto-54k.1).

Audit of internal/mcpserver/server.go confirms Mitto's own inbound /mcp
initialize + tools/list are served entirely by the go-sdk's
mcp.NewStreamableHTTPHandler from the statically-registered tool table
(built at NewServer time via mcp.AddTool). The handshake path never
acquires Mitto's internal locks (s.mu, s.sessionsMu) nor calls any
blocking helper (WaitForPendingRequest, correlation waits, store
lookups); no receiving/sending middlewares are registered.

Add TestFastPath_InboundInitAndToolsListStayBoundedUnderLoad as a
regression guard: starts a real MCP server, applies 32-way concurrent
initialize+tools/list round-trips (each a fresh MCP client session over
StreamableClientTransport), and asserts every round-trip completes
under a 2 s budget with a stable, non-empty tool set. Observed max
round-trip on this machine: ~35ms under load with race detector.

Document the warm-up fast-path in docs/devel/mcp-tool-discovery.md,
cross-referencing Fix A as the source-side fix.

- internal/mcpserver/server_fastpath_test.go (new)
- docs/devel/mcp-tool-discovery.md
The project-memory note in .augment/rules/42-mcpserver-development.md and
CLAUDE.md was written while the gap was open. Update it now that both fixes
landed on this branch:
- mitto-54k.1 (eea071d): bounds the interactive resume storm at the source
  via a per-Server resume semaphore.
- mitto-54k.2 (9cbf025): confirmed the inbound /mcp initialize+tools/list
  handshake is already lock-free (SDK static tool table), backed by a
  regression test.
Harden the existing Support prompt suite so a draft reply is never lost to a
review-dialog timeout/abort or left only in the chat transcript:
- Save the proposed message to the bead as a DRAFT [OUTBOUND] comment BEFORE
  opening the mitto_ui_textbox review dialog (support-gather-info,
  support-investigate).
- Explicit on-submit / on-timeout / on-abort branching for the review result;
  never post to Slack unless the user actually submits.
- 'Never dump the draft as chat text, never ask a free-text approve/hold/post
  question' hand-off rules once state:drafting is set.
- support-watch-channel: allow a short blocking mitto_ui_form/textbox ask ONLY
  for the per-channel knowledge-file gate, even during a silent scheduled run,
  since the run cannot proceed at all without that guidance; falls back to a
  non-blocking notify + skip-drafting-this-run on timeout.
- support-continue-conversation: updated description to reflect the
  review-before-post flow.
New prompt that periodically sweeps all tracked support-question beads:
refreshes each from its linked Slack thread, closes or flags stale/irrelevant
tickets (with user confirmation), and re-evaluates priority/status/next-steps
for everything that stays open. Read-only on Slack — never posts a reply.
…ld set_model + harden warm-once barrier (mitto-54k.5, mitto-54k.3)

The warm-once barrier (mitto-54k.3) fixed the session/new MCP-init wedge, but
the cold-start bottleneck moved one RPC downstream to session/set_model: the
interactive prompt goroutine called applyModelPreference synchronously before
the Prompt RPC, so a slow set_model on a cold agent (~85s observed in cgw,
Auggie Opus) blocked the first "Say hello" until "Agent slow during prompt".

Fix E (mitto-54k.5): make the interactive model switch best-effort/async.
applyModelPreference now runs SetSessionModel in a background goroutine bounded
by modelSwitchAsyncBudget (90s, session-ctx), and waits only modelSwitchSyncGrace
(3s) for it to land. A warm switch applies to THIS turn; a cold/slow switch is
deferred to the background (applying to the NEXT turn) and the prompt dispatches
immediately on the current model. Override-pill/baseline semantics preserved via
finalizeOverride (happens-before via close(done)); setModelSem serialisation and
the mitto-29q re-arm are unchanged (switch still goes through pdSetActiveModelOnly).

Barrier hardening (mitto-54k.3): shared_acp_process.go NewSession/LoadSession now
do a post-acquire warmth re-check — a caller that finds mcpInitDone=true after
waiting on the gate releases it immediately and recomputes warm budgets instead
of holding it through its RPC. Entry uses the raw cold predicate so mitto-29q
warm per-session re-handshakes still bypass the barrier.

Tests: prompt_dispatcher_test.go asserts the cold path logs "Deferring model
switch to background" and dispatches without blocking; mcp_init_budget_test.go
adds TestWarmOnceBarrier_OnlyOneHolderThroughWarmup. go vet + unit tests green
for both packages.
… (mitto-nnu)

The test built Server{} with zero-valued config, so
computeWorkspacePromptsLastModified (broadened by mitto-tf9 to fold in ALL
prompt-source mtimes) still consulted appdir.PromptsDir() and
appdir.SettingsPath(), which resolve to the developer's real Mitto data dir.
Those files survive the .mittorc deletion, so Last-Modified stayed non-zero and
the assertion "no Last-Modified after deletion" failed.

mitto-tf9's fold-all-sources behavior is correct; this was a test-isolation
gap. Isolate MITTO_DIR to an empty temp dir using the codebase's established
pattern (t.Setenv + appdir.ResetCache + t.Cleanup) so all folded sources return
zero after deletion. Test-only change; no production code affected.
inercia added 26 commits July 13, 2026 20:32
Extracts the C3 (Mobile Resilience) cluster from useWebSocket.js into a
dedicated sub-hook at web/static/hooks/useWSMobileResilience.js.

Moved (verbatim):
* lastHiddenTimeRef  (useRef)
* staleRecoveryCooldownRef  (useRef)
* isMobileDevice  (useMemo)
* visibilitychange effect (async handler: hidden-time tracking,
  checkAuthWithRetry retry cadence, fetchStoredSessions,
  reconnectAllSessionsStaggered w/ 300ms settle delay).

Placement: sub-hook is called after reconnectAllSessionsStaggered /
fetchStoredSessions / switchSession are declared (they are passed as
props). staleRecoveryCooldownRef and isMobileDevice are destructured at
composer scope so pre-existing bare references in handleSessionMessage
(L1671, L1960, L3077) and in INITIAL_ACK_TIMEOUT_MS resolve to the
sub-hook's bindings via lexical closure. Bindings are initialised before
any effect body or event handler runs (post-render), so TDZ is not hit.

Composer imports trimmed: cleanupExpiredPrompts, checkAuthWithRetry,
STALE_THRESHOLD_MS were only used by the moved effect.

useWebSocket.js:  5823 -> 5711 LOC (-112)
useWSMobileResilience.js: new, 211 LOC (verbatim effect body dominates)

Public return bag unchanged (diffed against HEAD).
…t to hooks/useWSConnection.js (mitto-90f.6.2)
…seWebSocket to hooks/useWSDeliveryVerification.js (mitto-90f.6.3)
secureFetch is exported from web/static/utils/csrf.js, not utils/api.js;
the previous imports resolved to a non-existent named export and would
break at runtime as soon as the create/comments code paths ran.
…esDialog shell

useWorkspacesData.loadData reads prevSelectedWorkspaceKeyRef, but
useWorkspaceEdits (which previously owned the ref via useRef) needs
setWorkspaces from useWorkspacesData — a circular dependency between
the two hooks. Hoist the ref up into the WorkspacesDialog shell and
pass it into both hooks, so useWorkspacesData can be invoked before
useWorkspaceEdits without either hook depending on the other.
… badges

Restructure task and conversation rows into a consistent two-line shape
(line 1: workspace badge + agent/bd id/priority chips; line 2: title,
truncating full-width) so panels on the same page share row height and
titles are no longer squeezed by side chips. Tighten row padding via a
shared COMPACT_ROW_STYLE inline constant (the precompiled tailwind.css
does not include arbitrary padding utilities like p-[.5rem]). Upgrade
workspaceBadge to a folder-icon chip on a distinct surface for easier
scanning. Match the Dashboard nav button in SessionList with px-2 py-1
so it sits flush with the new row density.
…upport sweep prompts

Introduce a new terminal state 'out-of-scope' (not our area / not
something we will pursue) for support tickets, with a polite hand-off
via 'Support: reply to user' before closing so waiting customers are
not silently dropped.

Protect closed beads (resolved / stale / out-of-scope) from being
reopened by later Slack activity: watch-channel now treats a matched
thread whose bead is already closed as done and never creates a
duplicate; housekeeping never flips a closed bead back to an active
state.

Mandate a per-run Slack thread re-read for every open ticket — in
particular those parked in 'awaiting-customer' and 'need-info' — so a
new customer reply always flips the bead back to 'awaiting-us' instead
of being silently missed.
…ile loop)

Restructure the reconcile step of 'Support: watch channel' and 'Support:
housekeeping' from a two-phase batch (list all beads -> fetch all threads ->
reconcile) into a per-ticket loop: for each open ticket, load the bead, fetch
that one thread, reconcile it, then move to the next. Interleaving keeps only
one thread's context in scope at a time so the agent reasons about each thread
while it is fresh.

The auditable read gate is preserved (still N thread-fetch calls for N open
tickets, one per ticket) but reframed as the natural outcome of the loop rather
than an up-front batch requirement.
The global Dashboard aggregates beads across all workspaces but was only
refreshing on its 15s poll — bd CLI changes, other agents, or git pulls
left the In-progress / Ready / Recently modified lists stale between polls.

Lift the inline fetchOnce into a useCallback (fetchDashboard) and add a
useEffect subscribing to the window 'mitto:beads_changed' event, calling
fetchDashboard unconditionally (no working_dir filter — Dashboard is
cross-workspace, unlike BeadsView which scopes to its own workspace).

The 15s poll stays as a safety net for the WS-disconnected case. Mirrors
the BeadsView.js:984-993 pattern. Backend pipeline (fsnotify watcher →
Server.OnBeadsChanged → useWebSocket.js) already in place; no backend
changes.
…ntextFromConfig

Both internal/cmd/mcp.go and internal/web/server.go had a private
buildMigrationContext(cfg) helper doing the same thing. Move it into
internal/session as MigrationContextFromConfig and call from both sites.
Net -12 lines, no behavior change.
…ailures

Loop conversations hitting the Augment API HTTP 413 augmentTooLarge
error (Conversation context too large for model) were treated as
generic transient failures and kept retrying with exponential backoff
forever, wasting resources against a context that will only grow.

Detect IsContextTooLargeError in LoopRunner.deliverPrompt's scheduled
error branch, bump a per-session counter, and after
MaxLoopContextWindowFailures (=3) consecutive hits call
LoopStore.MarkStopped with the new StoppedReasonContextWindowExceeded.
Counter is cleared on any successful delivery so an intervening manual
trim/summarize keeps the loop alive.

Frontend surfaces the new reason in LOOP_STOPPED_LABELS as
'Stopped: context too large'.

Fixes mitto-7jn
The read-cache decorator has shipped through mitto-is2.1-.4 with all
acceptance criteria met. Flipping the CLI flag default from false to
true so the cache is on by default. --beads-cache=false remains as an
off-switch for at least one release cycle per epic acceptance criteria.
The prior flip in internal/cmd/web.go only affected the 'mitto web' CLI
command. cmd/mitto-app/main.go constructs its own web.Config literal at
line 1224 that bypasses the webCmd flag machinery, so BeadsCache took
Go's zero value (false) and the read cache was silently disabled in the
macOS app (which is what users actually run). Set BeadsCache: true
explicitly in the app's web.Config to match the flipped CLI default.
Live metrics from the just-shipped mitto-is2 cache showed hit_rate=0.40
and a user report that repeated ticket-open (open A → open B → re-open A)
was still slow. Diagnosis: CachingClient.Show was an explicit pass-through
per the original design ("per-id invalidation is not worth the complexity").
That comment is falsified by production evidence — the beads viewer's
per-ticket load path (HandleBeadsShow → beadsClient().Show) is the
dominant miss for the viewer UX, and BeadsView.js re-fetches on every
issue open (useEffect deps: workingDir, currentIssueId, refreshNonce).

Fix: route Show through the existing doJSON helper with tag "show:"+id.
The per-id entries live in the same per-dir map slot, so evictDir() drops
them for free on any writer/watcher event — no per-id invalidation logic
needed. Zero additional complexity, purely a code-path re-route.

Also updates fakeClient.Show in cache_test.go to return an id-tagged
payload so the per-id keying test can distinguish A from B.

New unit tests (all pass):
- TestCachingClient_ShowCachedSameID: 2x Show(dir,id) → 1 inner call
- TestCachingClient_ShowInvalidatedByWrite: Show → SetStatus → Show → 2 inner calls
- TestCachingClient_ShowKeyedPerID: Show(A), Show(B), Show(A), Show(B) → 2 inner calls, id-specific payloads

All 19 CachingClient tests pass; integration test TestCachingClient_WriteReadThrough still green.
… (mitto-3gu)

The Tier 4 memory-recycle GC logs a single aggregated RSS number for the
shared ACP process tree, which conflates agent-side growth with MCP-child
growth. Under mitto-3gu we saw multi-GB RSS climbs on long-running loop
conversations and could not attribute them without a live ps probe.

Add SharedACPProcess.RSSBytesDetailed() returning (parent, descendants,
descendantCount) and wire it through a rssBreakdownSampler seam on
ACPProcessManager so both Tier 4 recycle log lines carry parent_rss_bytes,
descendant_rss_bytes, and descendant_count fields. Tests inject a synthetic
split via the sampler.
… retry (mitto-54k.11)

getOrCreateAuxiliarySession wraps SharedACPProcess.NewSession in a fresh
context with budget auxSessionCreateBudget (was hardcoded 30 s). NewSession
itself runs a bounded-retry loop where each attempt has
sessionCreateAttemptTimeout=25 s and the total sequence is capped by
sessionCreateTotalBudget=60 s.

When attempt 1 hits the agent's own internal ~25 s deadline (evidence:
`rpc_ms≈25000, error="context deadline exceeded"` in aux logs) the remaining
wall-clock in the wrapper ctx is ~5 s; shouldFailFastCreateAttempt then bails
at the attempt-2 boundary because remaining (5 s) < perAttemptBudget (25 s).
The retry loop is regressed to a single attempt on the aux path even though
the underlying budget (60 s) would fund a retry — every title-gen /
mcp-check / follow-up dispatch that lands during the wedge fails.

Set auxSessionCreateBudget to sessionCreateTotalBudget for parity with the
underlying NewSession retry sequence. Pinned by
TestAuxSessionCreateBudgetFundsRetry (invariant: budget ≥ 2×perAttempt AND
fail-fast predicate does not bail at the attempt-2 boundary after one full
drain).
…CP agents (mitto-54k.10)

The default V8 old-space limit (~1.5 GB) is a common source of hard aborts
in long-running loop conversations: once the heap reaches the mark-compact
threshold V8 kills the Node process, tearing down every session sharing
that shared ACP process.

Set NODE_OPTIONS=--max-old-space-size=12288 in defaults.env for all builtin
Node-based agents (augment, claude-code, cline, codex, gemini,
github-copilot, kilo, qwen-code). Guarded by TestBuiltinAgents_NodeAgentsRaiseV8HeapLimit
which asserts every npx-launched builtin agent carries the option so we
don't silently regress when a new agent lands.
Field logs (mitto-54k.9) show ~8-minute handshake wedges on stale sessions.
The starvation exception in the shared-cap path (mitto-1ut) releases the
shared handshake capability when the load probe times out, letting the
probe (45s) stack with the fallback session/new (up to 240s) for ~285s
worst-case; two of those in a row match the observed wedge.

The 45s value dates from an earlier era where session/load could legitimately
run long; observed MCP-init abort windows in current builds are 11-25s, so
capping the stale-load probe at 25s aligns it with the agent's own internal
deadline without cutting healthy loads short. TestHandshaker_StaleLoadProbeTimeout_CeilingCap
pins the ≤25s ceiling.
…ation_new (mitto-dj9)

When an orchestrator LLM composes multiple mitto_conversation_new calls in
one turn, it can short-circuit repeated initial_prompt arguments to a
bracketed self-reference like "[Same driver body]". Without a guard the
server enqueues that literal string as the loop child's seed and the loop
re-fires an unactionable prompt on every tick.

Add looksLikePlaceholderLoopSeed() and gate mitto_conversation_new's
free-text initial_prompt path (PromptName == "") when the call is creating
a loop child (LoopPrompt != "" or LoopTrigger != ""). Rejects trimmed
strings under 512 bytes matching a bracketed pattern with a self-reference
trigger word (same/see/above/prior/driver/body/prompt) and returns an
actionable error advising to pass the full body or use prompt_name.

The server-expanded prompt_name path is unaffected — it cannot be truncated
this way.
… unconditional Done (mitto-dj9/i5k/fko/6am)

The four beads-issue-loop orchestrators (fixing-bug{,s},
implementing-feature{,s}) suffered four related defects that regressed
loop-driver correctness:

- mitto-dj9: the outer orchestrator seeded child loops by inlining the
  driver body as a free-text initial_prompt/loop_prompt. LLMs
  short-circuited the repeated argument to "[Same driver body]"; the
  server-side placeholder guard now rejects that shape, so the drivers
  must instead pass prompt_name/loop_prompt_name and let the server
  expand the body at dispatch.
- mitto-i5k: the per-issue Done branch was gated on the child having
  reported success, which caused the outer loop to livelock on children
  that closed themselves without reporting. Made the Done branch
  unconditional (the child owns its own state; the orchestrator only
  observes reports).
- mitto-fko: added a recently-closed-parent filter so a bead re-appearing
  in the ready set moments after its parent orchestrator closed doesn't
  immediately re-spawn.
- mitto-6am: Step 3f (fan-out to worker) now requires a named worker
  prompt — free-text worker seeds are refused for the same
  short-circuiting reason as mitto-dj9.

Guarded by config/beads_loop_prompts_defects_test.go, which walks each
driver YAML and asserts the four defective patterns are absent (inline
free-text loop seed, conditional Done, missing recently-closed filter,
free-text worker fan-out).
…rt sweep tables

Both support-sweep prompts (support-housekeeping and support-watch-channel)
render a per-bead summary table whose "What's next" cell previously showed
a bare state phrase. Turn that cell into a markdown link to the bead's
`slack_url` (with a permalink fallback in watch-channel where the ticket
may not yet have a stored URL), so the sweep summary jumps straight to
the originating thread instead of forcing an extra `bd show` lookup.
Copilot AI review requested due to automatic review settings July 14, 2026 15:13

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

@inercia
inercia merged commit d6fa93a into main Jul 14, 2026
0 of 2 checks passed
@inercia
inercia deleted the fix/mitto-8qp-llr-tfb-bugfix-batch branch July 14, 2026 15:14
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.

2 participants