Skip to content

Remaining work after #66: loop rename, MCP tool discovery, shortcuts & more#67

Merged
inercia merged 90 commits into
mainfrom
fix/remaining-work
Jul 5, 2026
Merged

Remaining work after #66: loop rename, MCP tool discovery, shortcuts & more#67
inercia merged 90 commits into
mainfrom
fix/remaining-work

Conversation

@inercia

@inercia inercia commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Summary

This branch carries the 89 commits that landed locally after PR #66 ("Accumulated fixes and improvements") was squash-merged into main. The original branch history mixed those already-merged commits with the new work; this branch was recreated by rebasing only the genuinely-unmerged commits onto main (git rebase --onto origin/main <squash-point>), so it contains only the remaining work.

326 files changed, +18,931 / −7,970.

Major themes

periodicloop rename (mitto-8ir.*)

End-to-end rename of the "periodic" feature to "loop" across the whole stack: core config/session types, conversation domain, REST/WS contracts + routes, MCP tool params, loop-runner engine, builtin prompts/processors (.Session.IsLoop*, loop_* params), frontend (endpoints, hooks, components, CSS, data-testid, localStorage keys), docs, and tests. Includes periodic.jsonloop.json migration and periodic_*loop_* settings keys.

MCP tool discovery hardening (mitto-sys.*)

  • Deterministic stdio/http/sse tools/list discovery components, wired into agents.Manager.ListMCPServers and FetchMCPTools.
  • LLM fallback only for unreachable servers, hardened with strict object-schema parsing + single retry + plausibility check.
  • Event-driven ToolListWatcher pool (per-workspace) with runtime wiring + teardown; removes the blind 30/60/120s prompts_changed re-broadcast.
  • Bounded exponential-backoff re-probe of unreachable servers; last-known-good cache policy; on-disk cache with TTL + manual refresh.
  • Per-server availability state model; MCPServer.Headers applied to network transports; corrected/real mcp-list.sh for junie, opencode, github-copilot, qwen-code.

Shortcut buttons & Toolbar

  • Global + per-folder prompt shortcut buttons with backend API and defaults.
  • Shared ShortcutsEditor (settings + workspaces dialogs) and extracted shared Toolbar component; conversation actions promoted to buttons.

Loop / auto-children / workspace

  • onTasks loop trigger with CEL condition gating.
  • Per-child initial model profile for auto-created children; rich workspace + model-profile dropdowns.
  • Auxiliary model selection by capability tag.
  • Runtime-configurable agent inactivity watchdog timeout (mitto-54y).
  • Restore loop configuration on unarchive (mitto-vmp).

Beads

  • Label add/remove + workspace label listing; inline add-label input.
  • Configurable prompt arguments for Pull/Push/Sync upstream actions (mitto-22s).
  • Session statistics in the conversation properties panel; detail-panel toolbar/mobile fixes.

Prompts

  • Parallel child fan-out in loop & beads iteration prompts; beads-refine-implementation loop builtin; Support Slack triage workflow; JIRA CLI fallback when MCP tools are unavailable; gate mitto_conversation_* prompts on deterministic Permissions.*.

Fixes

  • Deliver queued human free-text verbatim on template render error (mitto-nvb); relax Split-IP CSRF fingerprint for mobile network drift (mitto-tz4); preserve/flush workspace selection in Workspaces dialog; beads watcher robustness (mitto-3zr, mitto-rxd); various UI polish.

Testing

  • make build build-mock-acp — pass
  • make test (Go unit + 1469 JS tests) — pass
  • make test-integration — pass
  • make fmt-check / make lint-go — clean

Note: one fix was needed on this branch — the onTasks E2E beads fake was missing the Label/ListAllLabels interface methods added in the beads-labels work. Stubs added so the integration package compiles.

inercia added 30 commits July 5, 2026 09:52
…tings (mitto-8ir.1)

Mechanical rename of the periodic/Periodic naming to loop/Loop in internal/config and internal/session (trigger enum values and frequency units unchanged). IsPeriodic/IsPeriodicForced/IsPeriodicConversation CEL vars are out of scope (owned by mitto-8ir.2). Builtin prompt YAML files (config/prompts/builtin) are out of scope (owned by the builtin-prompts child bead); TestBuiltinPromptLoopModes now skips affected cases until that migration lands.
…*->loop_* keys (mitto-8ir.12)

Adds two idempotent, one-time data migrations for the mitto-8ir periodic->loop rename:

1. internal/session/migration_002_rename_periodic_to_loop.go: registers session
   migration 002 which renames each session dir's periodic.json to loop.json
   (content unchanged). Safe: no-op when periodic.json absent, never clobbers
   an existing loop.json, idempotent re-run.

2. internal/config/settings.go: migrateSettingsFileIfNeeded/migrateSettingsPeriodicKeys
   rewrite legacy periodic_* keys (nested under session.* and conversations.*)
   to loop_* in the raw settings.json before unmarshalling, called from both
   LoadSettings and LoadSettingsWithFallback. Old value moved only when the new
   key is absent; already-migrated or new-only files are left untouched.

Prompts / IsPeriodic templates / MCP wire params are explicitly NOT migrated
(out of scope, per epic).
…L/processor fields (mitto-8ir.2)

internal/config:
- prompt_template.go: template map keys periodic->loop, periodic_forced->
  loop_forced; @mitto:periodic -> @mitto:loop directives.
- cel_context.go/cel_evaluator.go: IsPeriodic/IsPeriodicForced/
  IsPeriodicConversation -> IsLoop/IsLoopForced/IsLoopConversation on
  SessionContext and IterationContext; CEL activation maps updated.
- prompt_template_test.go/prompts_test.go: updated synthetic literals to
  IsLoop; added usesLegacySessionIsPeriodic guard to skip (not fail)
  builtin prompt files that still reference the legacy
  .Session.IsPeriodic template field or periodic: frontmatter key,
  pending the separate builtin-prompts migration bead (mitto-8ir.9).

internal/processors:
- input.go: ProcessorInput.IsPeriodic/IsPeriodicForced (json is_periodic/
  is_periodic_forced) -> IsLoop/IsLoopForced (is_loop/is_loop_forced).
- hook.go: BuildCELContext maps IsLoop/IsLoopForced into
  Session/Iteration CEL context.
- executor.go: anon InputMessage struct field IsPeriodic -> IsLoop.
- variables.go: @mitto:periodic/@mitto:periodic_forced ->
  @mitto:loop/@mitto:loop_forced.
- processors_test.go/variables_test.go: updated tests accordingly.

No behavior changes. Trigger enum values, frequency units, and the
"periodic-runner" sender-ID sentinel (owned by mitto-8ir.3) are untouched.

Verify: go build/vet/test ./internal/config/... ./internal/processors/...
all pass. Full-repo "go build ./..." remains broken pending mitto-8ir.3/
.4/.5/.6 (pre-existing condition since mitto-8ir.1 landed; internal/
mcpserver already fails to build against session/config types renamed
there).
…(mitto-8ir.3)

File renames (git mv):
- periodic_data.go -> loop_data.go
- periodic_data_test.go -> loop_data_test.go

Symbol renames across internal/conversation:
- loop_data.go: BuildPeriodicUpdatedData -> BuildLoopUpdatedData; map keys
  periodic_configured/periodic_enabled/periodic_has_prompt/
  periodic_prompt_preview/periodic_stopped_reason -> loop_*; uses
  session.LoopPrompt (was session.PeriodicPrompt).
- ws_events.go: WSMsgTypePeriodicUpdated="periodic_updated" ->
  WSMsgTypeLoopUpdated="loop_updated".
- session_manager.go: BroadcastPeriodicUpdated -> BroadcastLoopUpdated;
  sm.store.Periodic(...) -> sm.store.Loop(...); nextPeriodic -> nextLoop;
  doc/prose periodic->loop.
- session_info.go: SessionInfo.NextPeriodicAt -> NextLoopAt.
- bgsession_title.go / title_coordinator.go (+ _test.go):
  TriggerTitleGenerationFromPeriodic -> TriggerTitleGenerationFromLoop;
  triggerFromPeriodic -> triggerFromLoop.
- bgsession_prompt.go / background_session.go (+ _test.go):
  PeriodicKind/PeriodicKindNone/Scheduled/Forced -> LoopKind/LoopKindNone/
  Scheduled/Forced; PromptMeta.IsPeriodicForced -> IsLoopForced;
  PromptMeta.PeriodicKind -> LoopKind; periodicContinuationMu/
  lastTurnScheduledPeriodic -> loopContinuationMu/lastTurnScheduledLoop;
  peekPeriodicContinuation/advancePeriodicContinuation/
  ResetPeriodicContinuation -> peek/advance/ResetLoopContinuation.
- prompt_dispatcher.go (+ _test.go): senderIDPeriodic="periodic-runner" ->
  senderIDLoop="loop-runner"; ProcessorInput.IsPeriodic/IsPeriodicForced ->
  IsLoop/IsLoopForced (matches mitto-8ir.2's processors.ProcessorInput
  rename).
- bgsession_acp_process.go: ResetPeriodicContinuation call site updated.
- follow_up_coordinator.go: sender-ID sentinel "periodic-runner" ->
  "loop-runner" in promptOriginFromSenderID.

Left unchanged (justified):
- interfaces.go / bgsession_prompt.go doc comments referencing
  PeriodicRunner (type still named PeriodicRunner in internal/web,
  owned by mitto-8ir.4) — informational reference only, not a symbol
  defined in this package.
- bgsession_queue.go: "periodic queue checking" — generic English prose
  about a recurring timer-driven check, unrelated to the loop feature.
- markdown_test.go / markdown_streaming_test.go: "Periodic persistence
  during long responses" — arbitrary fixture text for markdown rendering
  tests, unrelated to the loop feature.

Verify:
- go vet ./internal/conversation/... clean.
- go test ./internal/conversation/... passes (267s, all packages ok).
- go build ./internal/conversation/... could not be verified directly
  because internal/conversation has a real (non-test) import dependency
  on internal/mcpserver (mcpserver.Server, UIPrompt* types), and
  internal/mcpserver currently fails to compile due to pre-existing
  mitto-8ir.1 fallout (session.PeriodicPrompt/PeriodicTrigger,
  config.PromptPeriodic, store.Periodic(), GetMinPeriodicCompletionDelaySeconds
  — all owned by mitto-8ir.6). This blocker predates this bead (confirmed
  via git stash) and is NOT caused by this change.
  Verification workaround: applied a throwaway, uncommitted mechanical
  patch to internal/mcpserver (renaming those same pre-existing broken
  references) purely to unblock compilation, confirmed
  `go build ./internal/conversation/...` and `go vet` succeed and all
  conversation tests pass, then fully reverted the mcpserver patch via
  `git checkout` (verified zero diff afterward, nothing committed there).

No behavior changes. Trigger enum values, frequency units, and symbols
owned by other packages (internal/web PeriodicRunner, internal/mcpserver,
internal/acpproc SessionInfo.NextPeriodicAt consumer) are untouched.
…riptions (mitto-8ir.6)

Breaking MCP contract change (intended): external clients using the old
periodic_* argument names will break.

types.go:
- ConversationInfo.IsPeriodic / ConversationDetails.IsPeriodic -> IsLoop
  (json is_periodic -> is_loop).
- ConversationUpdateInput/Output: Periodic* fields -> Loop*, json tags
  periodic_* -> loop_* (prompt, frequency_value/unit/at, enabled,
  fresh_context, max_iterations, trigger, completion_delay_seconds,
  max_duration_seconds, condition, condition_preset); PeriodicIterationCount
  -> LoopIterationCount, PeriodicNextRun -> LoopNextRun.
- PromptInfo.Periodic / PromptDetail.Periodic (*config.PromptPeriodic) ->
  Loop (*config.PromptLoop), json periodic -> loop.

prompts.go:
- p.Periodic -> p.Loop (config.WebPrompt field) in handlePromptList /
  handlePromptGet.

server.go (~123 mechanical occurrences + tool description prose):
- periodicRunner field / PeriodicRunner interface / SetPeriodicRunner ->
  loopRunner / LoopRunner / SetLoopRunner.
- SessionManager.BroadcastPeriodicUpdated -> BroadcastLoopUpdated (matches
  mitto-8ir.3's conversation.SessionManager rename); param type
  session.PeriodicPrompt -> session.LoopPrompt.
- BackgroundSession.TriggerTitleGenerationFromPeriodic -> ...FromLoop
  (matches mitto-8ir.3).
- periodicDelayFloor -> loopDelayFloor; uses
  ConversationsConfig.GetMinLoopCompletionDelaySeconds() and
  config.DefaultMinLoopCompletionDelaySeconds (mitto-8ir.1 symbols).
- store.Periodic(id) -> store.Loop(id); session.PeriodicTrigger ->
  session.LoopTrigger; session.PeriodicPrompt -> session.LoopPrompt
  (mitto-8ir.1 symbols).
- mitto_conversation_run_periodic_now tool -> mitto_conversation_run_loop_now
  (breaking tool rename); handleRunPeriodicNow -> handleRunLoopNow;
  RunPeriodicNowInput/Output -> RunLoopNowInput/Output.
- ConversationStartInput/Output, ConversationUpdateInput/Output request
  fields renamed to match types.go's Loop* / loop_* map.
- All MCP tool Description strings for mitto_conversation_new and
  mitto_conversation_update updated: every 'periodic_*' param mention ->
  'loop_*', plus surrounding prose (fixed 3 sed-artifact words: "own
  loopity" -> "own loop", "send loopally" -> "send in the loop", "as loop"
  -> "as a loop").

server_test.go (114 occurrences): all mock method signatures
(BroadcastPeriodicUpdated -> BroadcastLoopUpdated, mockPeriodicRunner ->
mockLoopRunner, TriggerTitleGenerationFromPeriodic -> ...FromLoop), test
names (TestHandleRunPeriodicNow_* -> TestHandleRunLoopNow_*,
TestConversationUpdate_OnCompletionPeriodic ->
..._OnCompletionLoop, etc.), and payload/assertion keys updated to loop_*.
Fixed 2 "periodicity" sed artifacts in comments.

Left unchanged (justified, unrelated prose):
- server.go:4275 "startProgressHeartbeat emits periodic progress
  notifications" — generic SSE keepalive heartbeat, unrelated to the loop
  feature.
- server.go:4797 "Polling loop: check child agent status periodically" —
  generic recurring status poll, unrelated to the loop feature.

Verify:
- go build ./internal/mcpserver/... : OK.
- go build ./internal/conversation/... ./internal/mcpserver/... : OK
  (confirms mitto-8ir.3 + mitto-8ir.6 integrate).
- go vet ./internal/mcpserver/... : clean.
- go test -count=1 ./internal/mcpserver/... : ok (123s).
- gofmt clean on all touched files.
- grep -rIn -i periodic internal/mcpserver: only the 2 justified lines above.
…proc GC/suspend (mitto-8ir.4)

Scope: the loop-runner engine in internal/web plus internal/acpproc's GC
suspend/resume logic. REST/WS JSON contract fields (internal/web/handlers,
session_api.go, session_ws.go, session_periodic_api.go, routes.go,
ws_messages.go, tasks_baseline.go doc refs) are intentionally left for
mitto-8ir.5 and remain in their pre-existing (already broken by earlier
beads) state.

File renames (git mv):
- internal/web/periodic_runner.go -> loop_runner.go
- internal/web/periodic_runner_tasks.go -> loop_runner_tasks.go
- internal/web/periodic_runner_test.go -> loop_runner_test.go (~249 occurrences)

loop_runner.go / loop_runner_tasks.go / loop_runner_test.go:
- PeriodicRunner -> LoopRunner (type, NewPeriodicRunner -> NewLoopRunner)
- All Set*/On* methods: SetOnPeriodicStarted/AutoStopped/Updated ->
  SetOnLoop*, SetMaxPeriodicIterations -> SetMaxLoopIterations,
  SetMinPeriodicCompletionDelaySeconds -> SetMinLoopCompletionDelaySeconds,
  MinPeriodicCompletionDelaySeconds -> MinLoopCompletionDelaySeconds,
  StopPeriodicForArchive -> StopLoopForArchive, SetMinLoopTasksCooldownSeconds
  (already loop-named).
- ErrPeriodicNotEnabled -> ErrLoopNotEnabled; MaxPeriodicResumeFailures ->
  MaxLoopResumeFailures; periodicScheduleBackoff(Base/Cap) -> loopScheduleBackoff*.
- Uses upstream-renamed symbols: session.Store.Loop()/LoopPrompt/LoopStore/
  ErrLoopNotFound (mitto-8ir.1), config.DefaultMaxLoopIterations/
  DefaultMinLoopCompletionDelaySeconds/GlobalMaxLoopIterations/
  EffectiveMaxLoopIterations (mitto-8ir.1), conversation.LoopKindScheduled/
  LoopKindForced/IsLoopForced/LoopKind (mitto-8ir.3). Sender ID literal
  "periodic-runner" -> "loop-runner" (matches conversation.senderIDLoop).
- Fixed 4 "loop loop" grammar artifacts from the blanket rename; preserved a
  doc-comment reference to handleSetPeriodic/handlePatchPeriodic (HTTP, still
  actually named that — owned by .5).

internal/acpproc (acp_process_gc.go, acp_process_gc_test.go,
acp_process_manager.go):
- GCConfig.PeriodicSuspendThreshold/PeriodicSuspendGracePeriod -> LoopSuspend*;
  UpdatePeriodicSuspendThreshold -> UpdateLoopSuspendThreshold; all doc
  comments and log fields (next_periodic_at -> next_loop_at, etc).
- Fixed real breakage from mitto-8ir.3's SessionInfo.NextPeriodicAt ->
  NextLoopAt rename (acp_process_gc.go referenced the old field name).
- Fixed 1 sed artifact ("runs RunGCOnce loopally" -> restored "periodically",
  a generic unrelated adverb).
- Build/vet/test verified standalone: go build, go vet, go test all pass
  (1.4-1.7s).

internal/web/server.go / config_handlers.go (runner wiring only, no JSON
contract changes):
- Server.periodicRunner field -> loopRunner (*LoopRunner); Server.PeriodicRunner()
  getter -> LoopRunner() (unused outside one now-already-broken integration
  test file pending a later bead).
- All call sites updated to the renamed LoopRunner API and config getters
  (GetMaxLoopIterations, GetMinLoopCompletionDelaySeconds, GetStartupLoopDelay,
  ParseLoopSuspendTimeout, GCConfig.LoopSuspendThreshold).
- mcpServer.SetPeriodicRunner -> SetLoopRunner (matches mitto-8ir.6).
- sessionManagerAdapter.BroadcastPeriodicUpdated -> BroadcastLoopUpdated to
  satisfy mcpserver.SessionManager interface (mitto-8ir.6) — was a real
  compile error (method name mismatch), not a JSON contract change.
- config_handlers.go: fixed ParsePeriodicSuspendTimeout/UpdatePeriodicSuspendThreshold
  call site to use the already-renamed upstream/acpproc names.
- Left untouched (owned by .5, all already broken pre-existing due to earlier
  beads' upstream renames): Server.BroadcastPeriodicUpdated/BroadcastPeriodicStarted
  (WS broadcast functions + WSMsgTypePeriodicUpdated/Started + handlers.Deps
  field names TriggerPeriodicNow/StopPeriodicForArchive/ErrPeriodicNotEnabled/
  PeriodicDelayFloor/BroadcastPeriodicUpdated), Server.periodicDelayFloor()
  (session_periodic_api.go).

Verification:
- go build/vet/test ./internal/acpproc/...: all pass standalone.
- go build ./internal/web/...: fails ONLY inside internal/web/handlers (11
  errors, all undefined session.Periodic{Prompt,Store,Trigger}/ErrPeriodicNotFound
  / store.Periodic — squarely .5's REST contract scope).
- Verified runner+server integration via a THROWAWAY, fully-reverted scratch
  patch: mechanically renamed the remaining periodic->loop symbols in
  handlers/*.go, session_api.go, session_periodic_api.go, session_ws.go, and
  server.go's BroadcastPeriodicUpdated to their known upstream-renamed names;
  `go build ./internal/web/...` then succeeded cleanly, and `go vet` failed
  only on server_test.go's TestBuildPeriodicUpdatedData_* tests (which test
  the .5-owned WS payload builder directly). Confirms my loop_runner.go +
  server.go changes integrate correctly once .5 lands. Scratch patch fully
  reverted (git checkout -- handlers/ session_api.go session_periodic_api.go
  session_ws.go; server.go restored from a pre-scratch backup); working tree
  verified diff-clean against intended changes before committing.
- gofmt clean on all touched files.
…o-8ir.5)

BREAKING wire change (JSON keys, URL paths, WS message types). Frontend bead
(.7) depends on this. This is the LAST piece needed for internal/web to build
fully — .4 (loop-runner engine + acpproc) already landed.

File renames (git mv):
- internal/web/handlers/session_periodic.go -> session_loop.go
- internal/web/handlers/session_periodic_write.go -> session_loop_write.go
- internal/web/handlers/session_periodic_run.go -> session_loop_run.go
- internal/web/handlers/session_periodic_test.go -> session_loop_test.go
- internal/web/session_periodic_api.go -> session_loop_api.go

REST paths (routes.go):
- /api/sessions/{id}/periodic -> /api/sessions/{id}/loop
- /api/sessions/{id}/periodic/{subPath} -> /api/sessions/{id}/loop/{subPath}
- handleSessionPeriodic -> handleSessionLoop (session_api.go + routes.go)
- handlers.HandleSessionPeriodic -> HandleSessionLoop; handleGetPeriodic/
  handleSetPeriodic/handlePatchPeriodic/handleDeletePeriodic/
  handleRunPeriodicNow -> handleGetLoop/handleSetLoop/handlePatchLoop/
  handleDeleteLoop/handleRunLoopNow.

JSON field renames (all keys, request + response bodies):
- PeriodicPromptRequest/PeriodicPromptPatchRequest -> LoopPromptRequest/
  LoopPromptPatchRequest (json tags unchanged: trigger/delay_seconds/etc.
  were already generic, not periodic_*).
- RunPeriodicNowRequest -> RunLoopNowRequest.
- handlers/session_list.go SessionListResponse: PeriodicConfigured|Enabled|
  Frequency|StoppedReason|Trigger|IterationCount|MaxIterations|DelaySeconds|
  MaxDurationSeconds|HasPrompt|PromptPreview -> Loop* fields with loop_* json
  tags (loop_configured, loop_enabled, loop_frequency, loop_stopped_reason,
  loop_trigger, loop_iteration_count, loop_max_iterations, loop_delay_seconds,
  loop_max_duration_seconds, loop_has_prompt, loop_prompt_preview).
- session_api.go: IsPeriodicConversation -> IsLoopConversation (matches
  config.SessionContext.IsLoopConversation, mitto-8ir.2).
- handlers.go Deps struct: TriggerPeriodicNow/StopPeriodicForArchive/
  ErrPeriodicNotEnabled/PeriodicDelayFloor/BroadcastPeriodicUpdated ->
  TriggerLoopNow/StopLoopForArchive/ErrLoopNotEnabled/LoopDelayFloor/
  BroadcastLoopUpdated (wired from server.go's already-.4-renamed loopRunner).
- callback.go: public webhook error codes "periodic_disabled" -> "loop_disabled"
  (intentional breaking change per task scope; endpoint doc-comment already
  flags external callers as depending on this legacy shape).
- ui_preferences.go: persisted filter_tab_grouping key "periodic" -> "loop".
- session_create.go, session_update.go, queue.go: doc-comment/wiring updates
  only (StopLoopForArchive call site, no JSON/behavior change).

WebSocket:
- ws_messages.go: WSMsgTypePeriodicStarted="periodic_started" ->
  WSMsgTypeLoopStarted="loop_started" (WSMsgTypeLoopUpdated already existed
  from mitto-8ir.3).
- session_ws.go: data keys periodic_configured/periodic_enabled ->
  loop_configured/loop_enabled.
- server.go: Server.BroadcastPeriodicUpdated/BroadcastPeriodicStarted ->
  BroadcastLoopUpdated/BroadcastLoopStarted, now correctly calling
  conversation.BuildLoopUpdatedData / conversation.WSMsgTypeLoopUpdated /
  WSMsgTypeLoopStarted (mitto-8ir.3's already-renamed symbols — this resolves
  the build breakage flagged as deferred-to-.5 in mitto-8ir.4's report).
- sessionManagerAdapter.BroadcastLoopUpdated already fixed by .4; unaffected.
- server_test.go: TestBuildPeriodicUpdatedData_* -> TestBuildLoopUpdatedData_*
  (tests conversation.BuildLoopUpdatedData directly).

loop_runner.go (mitto-8ir.4 file): updated its one remaining doc-comment
reference (handleSetPeriodic/handlePatchPeriodic -> handleSetLoop/
handlePatchLoop) now that those handlers are renamed by this bead.

Grammar fixes: 3 "loop loop" sed artifacts (handlers.go, session_list.go,
session_update.go doc comments), 1 "loopally"->"periodically" (unrelated
adverb in server.go's GC comment, restored), 1 ALL-CAPS "PERIODIC"->"LOOP"
sidebar-category comment in session_list.go (sed is case-sensitive and
missed it), and reworded "can be loop"->"can be loops" for grammar (+ updated
the matching test assertion string in session_loop_test.go).

Out of scope (left alone, per task): tests/integration/** (owned by final
sweep mitto-8ir.11), web/static/** frontend (owned by .7/.8), docs/devel/**.

Verification:
- go build ./internal/web/...: PASSES fully (previously blocked on
  internal/web/handlers; now clean).
- go build ./internal/web/... ./internal/acpproc/... ./internal/conversation/...
  ./internal/mcpserver/... (combined): PASSES.
- go vet ./internal/web/...: CLEAN.
- go test -count=1 ./internal/web/...: ok for internal/web (9.3s),
  internal/web/handlers (1.5s), internal/web/middleware (2.0s).
- gofmt clean on all touched files.
- grep -rIn -i periodic internal/web: only 5 justified unrelated-prose hits
  remain (generic "periodically"/"cleanupLoop periodically" adverbs in
  middleware/auth.go, middleware/security_ratelimit.go,
  middleware/auth_ratelimit.go, negative_session_cache.go, server.go's GC
  comment) — none reference the loop feature.
…utils)

Renames periodic -> loop across the frontend API/data layer to match the
already-renamed backend JSON contract (beads .1-.6) and REST/WS surface
(bead .5).

utils/:
- endpoints.js (+ test): sessions.periodic -> loop, periodicRunNow -> loopRunNow.
- prompts.js (+ test): promptPeriodicMode/IsToggleable/DefaultOn ->
  promptLoop*, promptResolveAsPeriodic -> promptResolveAsLoop,
  prompt.periodic field access -> prompt.loop. promptsPeriodic menu
  identifier intentionally preserved (frontend-only menu id, out of scope).
- storage.js (+ test): FILTER_TAB.PERIODIC -> LOOP, DEFAULT_CATEGORY_FILTER /
  getCategoryFilter / setCategoryFilter "periodic" field -> "loop"
  (sessionStorage shape change, session-scoped so no migration needed).
  Historical migrateLegacyTabStorage() literal old-key strings preserved.
- sessionGrouping.js (+ test): periodic_enabled/periodic_configured ->
  loop_enabled/loop_configured; FILTER_TAB.PERIODIC -> LOOP; filter.periodic
  -> filter.loop.

hooks/:
- useConversationSeeding.js (+ test): decidePeriodicAction -> decideLoopAction,
  makePeriodicNow -> makeLoopNow, configurePeriodicSchedule ->
  configureLoopSchedule; action strings new-periodic/make-periodic ->
  new-loop/make-loop; endpoints.sessions.periodic(RunNow) -> loop(RunNow).
- useWorkspacePrompts.js: periodicPrompts -> loopPrompts.
- useWebSocket.js: periodic_* session/WS fields -> loop_* (loop_configured,
  loop_enabled, loop_stopped_reason, loop_trigger, loop_delay_seconds,
  loop_max_duration_seconds), WS event types periodic_started/periodic_updated
  -> loop_started/loop_updated.
- useBeadsIntegration.js: promptResolveAsPeriodic import -> promptResolveAsLoop;
  startConversationWithPrompt({ periodic }) -> ({ loop }).
- index.js: re-export names updated.

constants.js: PERIODIC_PROGRESS_* -> LOOP_PROGRESS_*.

Necessary downstream fixes (function/field renames are breaking, no aliases
per epic decision) in files owned by later beads (.8 frontend components):
app.js, ChatInput.js, ContextMenu.js, PromptsMenu.js, BeadsView.js(+test),
ConversationPropertiesPanel.js, PeriodicFrequencyPanel.js, SessionItem.js,
SessionList.js, SessionPanel.js, SettingsDialog.js, useConversationMenu.js —
only the plumbing (imports, function calls, JSON field names, endpoint calls)
was fixed to keep the app functional; UI text/component/prop naming
(onPeriodicPrompt, PeriodicFrequencyPanel, "Make periodic" labels, etc.) is
intentionally deferred to bead .8.

Verification: npm test (17 suites, 1445 tests) all pass; node --check on all
changed files; eslint reports 0 errors (pre-existing warnings only).
PERIODIC_STOPPED_LABELS -> LOOP_STOPPED_LABELS
formatPeriodicMaxDuration -> formatLoopMaxDuration

Update lib.test.js imports, describe blocks, and all assertions to match.

Refs: mitto-8ir.7
…Started

Rename hook state var, setter, clearer and the returned field:
  periodicStarted     -> loopStarted
  setPeriodicStarted  -> setLoopStarted
  clearPeriodicStarted -> clearLoopStarted

Also update the loop_started WS handler's console.log/comment to say
'Loop' instead of 'Periodic'. Handler branch name (loop_started) was
already renamed in an earlier commit.

Refs: mitto-8ir.7
…ok renames

Two fixes:

1. Fix broken hardcoded REST paths. The backend was renamed to /loop in
   an earlier commit but two call sites in app.js still used the raw
   apiUrl(`/api/sessions/${id}/periodic`) — 404 against the new backend.
   Replace both with endpoints.sessions.loop(sessionId), matching every
   other loop caller in the codebase.

2. Update consumers to match renamed lib.js and useWebSocket.js exports:
     PERIODIC_STOPPED_LABELS   -> LOOP_STOPPED_LABELS
     formatPeriodicMaxDuration -> formatLoopMaxDuration
     periodicStarted           -> loopStarted
     clearPeriodicStarted      -> clearLoopStarted
   Toast/handler/UI-text strings ("Make periodic", "Periodic
   Conversation", handleMakePeriodic/NonPeriodic, etc.) are deliberately
   left in place — those belong to mitto-8ir.8 (UI text + components).

Verified: node --check on all modified files; npm test (17 suites,
1445 tests) all pass.

Refs: mitto-8ir.7
… .Session.IsLoop*, loop_* params) + config tests
…Icon + icon registry key, promptsPeriodic menu token (+coupled YAML icon:/menus:)
…identifiers, UI text, CSS, data-testid, localStorage keys)
…abStorage (legacy localStorage key ref, not the loop feature)
…y CEL rename (Session.IsPeriodic->Session.IsLoop)
- internal/client/client.go: PeriodicFrequency/SetPeriodicRequest/PeriodicConfig
  -> LoopFrequency/SetLoopRequest/LoopConfig; SetPeriodic/GetPeriodic/RunPeriodicNow
  -> SetLoop/GetLoop/RunLoopNow; /api/sessions/{id}/periodic[/run-now] -> /loop[/run-now]
  (matches backend session_loop.go contract; test client used by integration tests).
- processors/types.go: Origin doc 'periodic-runner' -> 'loop-runner' (runtime emits loop-runner).
- processors/processors_test.go: origin-filter test 'periodic-runner' -> 'loop-runner'.
- conversation/interfaces.go: PromptResolver doc PeriodicRunner -> LoopRunner.
- session/loop.go: StoppedReasonResumeFailures doc MaxPeriodicResumeFailures -> MaxLoopResumeFailures.
Adverb rewording (NOT substitution) to eliminate the last 'periodic' prose in
config/ while keeping natural English:
- Loop-scheduling prose -> 'as a loop' / 'Regularly' (jira/github sync, babysit,
  slack-review, architectural-analysis; all reference mitto_conversation_set_loop).
- Processor re-fire comments -> 're-runs repeatedly' (check-mcp-tools, beads-*).
grep -ri periodic config/ = zero; internal/config tests green.
Add an optional ModelProfile to auto-child workspace config so each
auto-created child can start on a specific global Model profile instead
of the ACP server's default model selection.

Plumb a per-session ModelConstraintOverride through SessionManager
(new ResumeSessionWithModelConstraint / resumeSessionWithConstraint) and
BackgroundSession via applyModelConstraintOverride, and expose a model
profile dropdown in the auto-children settings UI.

Refs mitto-9x8.
loadData() reset the selected workspace to valid[0] on every reload/
reopen. valid[0] reflects the backend's non-deterministic map-iteration
order, not the sorted tree, so reopening the dialog could land on a
different workspace than the one just edited. With many workspaces
sharing the same legacy auxiliary-model setting, this made a just-saved
Auxiliary Model change look reverted.

Preserve the previously-selected workspace across reload/reopen when it
still exists, and otherwise fall back to a deterministic first entry
(sorted by display name, then ACP server).
…e render error (mitto-nvb)

Queued human free-text messages were run through the Go prompt-template renderer and, on render failure, dropped instead of delivered. The 'queue' senderID was overloaded, covering both human-typed messages that got queued (agent busy) and cross-session MCP dispatches, so human input was misclassified as automated and fail-closed.

Fix distinguishes the enqueuer's origin: add QueuedMessage.Origin (QueueOriginUser default / QueueOriginAgent) via new AddWithOrigin (Add delegates, no double-lock). The 4 cross-session MCP enqueue sites mark messages agent-origin. Origin flows through PromptMeta.QueueOrigin to resolveAndSubstitute, where fail-closed now requires named-prompt OR loop-runner OR (queue AND agent-origin); user/empty-origin queue free-text fails open (delivered verbatim). Removed now-unused isAutomatedDispatch. mitto-e7u fail-closed guarantee preserved for agent/loop/named dispatches.
Whitespace/formatting-only changes surfaced by 'make fmt'; no syntactic changes.
Rename the 5 beads-issue-iterate-* and 3 iterate-* builtin prompts to their loop-* equivalents, continuing the periodic-to-loop terminology unification. Update docs/config/prompts.md references.
…ailable

Widen the enabledWhen gate on the six JIRA builtin prompts to also accept CommandExists(jira), and add per-prompt command tables mapping each jira_* operation to its ankitpokhrel/jira-cli equivalent, so the prompts work when only the CLI is present.
…orkspaces dialog

Track the previously selected workspace and commit its transient edit fields into the workspaces array before repopulating for the new selection, so edits to multiple workspaces in a single dialog session are no longer lost. Reset the tracker on data reload.
… children

Add a generic RichSelect controlled dropdown (details-based, supports icon and badge rows) and adopt it in the Auto-Create Children settings for the target workspace (color badge plus name) and model profile selectors, replacing the plain select elements.
inercia added 25 commits July 5, 2026 09:52
…vers (mitto-sys.2)

Add DiscoverWorkspaceStdioTools: resolves a workspace's configured MCP
servers via a ServerLister (agents.Manager.ListMCPServers with
{Path: workspacePath}), then probes the stdio ones with
DiscoverStdioServers. http/sse servers are skipped (mitto-sys.3);
per-server probe failures are reported in-band, list failures as a
top-level error. Compile-time assertion binds *agents.Manager to
ServerLister. Adds 3 tests (list-then-probe, list error, nil lister).
…ive-negatives) (mitto-sys.6)

Replace the old "only cache non-empty results" behavior in
FetchMCPTools with a last-known-good merge policy, per
docs/devel/mcp-tool-discovery.md (Q3.1): a previously-observed tool is
never downgraded to absent on a single negative fetch; removal
requires two consecutive independent negative fetches. A tool that
reappears between negatives resets its counter.

- WorkspaceAuxiliaryManager: add mcpToolsNegatives map[string]map[string]int
  (workspaceUUID -> toolName -> consecutive-negative count), guarded by
  the existing mcpToolsCacheMu (no new mutex). Initialized in
  NewWorkspaceAuxiliaryManager.
- New unit-testable method applyMCPToolsCachePolicy(workspaceUUID,
  fresh) []MCPToolInfo: merges a successful fetch's fresh tool list
  into the cache — tools present in fresh have their negative counter
  reset and description refreshed; tools absent from fresh have their
  counter incremented and are dropped only once it reaches 2; new
  fresh tools are added. Result is sorted alphabetically and stored.
  When there is no prior cache entry and fresh is empty, no entry is
  created (empty first fetch establishes nothing).
- FetchMCPTools now calls applyMCPToolsCachePolicy after a successful
  parse and returns the merged result instead of the raw fresh fetch;
  the early cache-hit short-circuit is unchanged.

Tests (workspace_manager_test.go): first non-empty fetch stores; first
empty fetch establishes nothing; one negative retains; two consecutive
negatives remove; reappearance resets the counter (and a subsequent
single negative does not re-remove); new tool added; description
refreshed when present in fresh; per-workspace isolation.

Gate: gofmt -l internal/auxiliary/ clean; go vet ./internal/auxiliary/...
clean; golangci-lint run ./internal/auxiliary/... = 0 issues; go test
-race -count=3 ./internal/auxiliary/... all green; go build ./...
clean. make fmt run before and right before commit.
…PTools, LLM fallback only for unreached servers (mitto-sys.6, mitto-sys.2)

Wire mcpdiscovery.DiscoverWorkspaceStdioTools into the live
FetchMCPTools call site, so deterministic tools/list results are used
whenever every configured stdio server is reachable, and the LLM
introspection fallback only runs to supplement tools from servers that
couldn't be reached (or when discovery itself errors/is unset).

internal/auxiliary/workspace_manager.go:
- Add exported StdioToolsDiscoverer func field to WorkspaceAuxiliaryManager
  (nil by default; injected by the web layer, matching the existing
  ACPProcessManager.WorkspaceConfigProvider injection style).
- Restructure FetchMCPTools (after the unchanged cache-hit short-circuit):
  call StdioToolsDiscoverer when set; union each Reachable server's tool
  names into a deterministic set (deduped); need the LLM path only when
  discovery errored, is unset, or any server was Reachable=false, or zero
  servers were probed. Run the LLM path (extracted verbatim into the new
  fetchMCPToolsViaLLM helper) only when needed; on LLM failure, propagate
  the error only if there were zero deterministic tools, otherwise log and
  proceed with deterministic-only. Union LLM tools into the merged result,
  deduped by name with deterministic entries winning. All results still
  flow through applyMCPToolsCachePolicy (last-known-good preserved). Net
  effect: nil seam / no discoverer set ⇒ behavior identical to before.

internal/web/server.go:
- After sessionMgr.SetAuxiliaryManager, resolve appdir.AgentsDir() and
  wire auxiliaryManager.StdioToolsDiscoverer to a closure that maps a
  workspace UUID -> ACP agent (mirroring the resolution already used in
  internal/web/handlers/workspace_mcp.go) and calls
  mcpdiscovery.DiscoverWorkspaceStdioTools. Logs a warning and leaves the
  discoverer unset if the agents dir can't be resolved.

Tests (internal/auxiliary/workspace_manager_test.go): nil discoverer
(pure LLM, existing behavior); all-reachable skips the LLM entirely
(provider call would t.Fatal); one unreachable server unions with LLM
tools; discoverer error falls back to LLM; all-reachable with zero
tools makes no cache entry and never calls the LLM; a tool name present
in both deterministic and LLM output appears exactly once, with the
deterministic entry winning.

Gate: gofmt -l internal/auxiliary/ internal/web/ clean; go vet on both
clean; golangci-lint run on both = 0 issues; go test -race -count=3
./internal/auxiliary/... all green; go build ./... clean; go test
./internal/web/... (incl. handlers/middleware) all green. make fmt run
before and right before commit, no reformatting needed; no go.mod/go.sum
changes (mcpdiscovery/agents already vendored/present).
…transports, live in FetchMCPTools (mitto-sys.3)

Extend deterministic MCP tools/list discovery from stdio-only to all
transports (stdio + http/sse), reusing the existing probe/connect/list
plumbing, and wire it into the live FetchMCPTools call path.

internal/mcpdiscovery/discovery.go:
- Extract the body of DiscoverStdioServer into an unexported probeServer
  (ctx, srv, timeout, factory) helper; DiscoverStdioServer is now a thin
  wrapper defaulting factory to CommandTransportFactory. Behavior
  unchanged.
- Add NetworkTransportFactory: the production TransportFactory for
  http/sse servers. Builds *mcp.SSEClientTransport when srv.URL's path
  ends in /sse (case-insensitive, parsed via net/url with a raw
  strings.HasSuffix fallback), else *mcp.StreamableClientTransport.
  Errors on empty URL. srv.Env/auth headers are not applied (no Headers
  field yet, mitto-sys.9); auth-required endpoints surface as
  Reachable=false via Connect/ListTools failure.
- Add DefaultTransportFactory: dispatches Command!="" to
  CommandTransportFactory, URL!="" to NetworkTransportFactory, errors
  when neither is set.
- Add DiscoverNetworkServer (thin wrapper over probeServer, factory
  defaults to NetworkTransportFactory).
- Add DiscoverNetworkServers: probes URL!=""&&Command=="" servers only,
  sequential, per-server isolated, input order, default factory
  NetworkTransportFactory.
- Add DiscoverServers: probes every server except those with neither
  Command nor URL, sequential, per-server isolated, input order, default
  factory DefaultTransportFactory (mixed-transport aware).
- Add DiscoverWorkspaceTools: identical to DiscoverWorkspaceStdioTools
  except it calls DiscoverServers instead of DiscoverStdioServers, so a
  workspace's http/sse servers are probed too.
- DiscoverStdioServers and DiscoverWorkspaceStdioTools are unchanged
  (still stdio-only, still skip http/sse servers) — existing callers/tests
  unaffected.

internal/web/server.go:
- Swap the live StdioToolsDiscoverer closure's call from
  mcpdiscovery.DiscoverWorkspaceStdioTools to
  mcpdiscovery.DiscoverWorkspaceTools (same args, nil factory ->
  DefaultTransportFactory), making http/sse discovery live in
  FetchMCPTools. Updated the adjacent comment (stdio -> stdio + http/sse).

internal/mcpdiscovery/discovery_test.go:
- NetworkTransportFactory: SSE selection by /sse suffix (asserts
  *mcp.SSEClientTransport + Endpoint), Streamable default (asserts
  *mcp.StreamableClientTransport + Endpoint), empty URL error.
- DefaultTransportFactory dispatch: stdio/network/neither subtests.
- DiscoverNetworkServer reachable-with-tools via an injected in-memory
  factory.
- Real Streamable HTTP integration test against
  httptest.NewServer(mcp.NewStreamableHTTPHandler(...)).
- Real SSE integration test against
  httptest.NewServer(mux with mcp.NewSSEHandler mounted at /sse), probing
  ts.URL+"/sse" — a genuine SSE round-trip, not just a factory-selection
  unit test (see the test's doc comment for why mounting at /sse works
  with SSEHandler's per-request endpoint computation).
- Auth-required graceful failure: httptest server returning 401 for all
  requests -> Reachable=false, Err set, no panic, empty Tools.
- DiscoverServers mixed-transport test: one stdio, one network, one
  neither (skipped), via a branching injected factory -> 2 reachable
  results.
- DiscoverWorkspaceTools test: stubLister returns a stdio + a network
  server, both are probed (contrast with the existing
  DiscoverWorkspaceStdioTools test, which still skips the network one).

Gate: gofmt -l internal/mcpdiscovery/ internal/web/server.go clean; go
vet on both clean; golangci-lint run on both = 0 issues; go test -race
-count=3 ./internal/mcpdiscovery/... all green (incl. real Streamable +
SSE integration tests); go build ./... clean; go test ./internal/web/...
(incl. handlers/middleware) all green. make fmt run before commit, no
reformatting needed; no go.mod/go.sum changes (go-sdk's SSE/Streamable
client+server types were already vendored).
…+ single retry + plausibility (mitto-sys.7)

Harden fetchMCPToolsViaLLM (the LLM-introspection fallback used when
deterministic stdio/http/sse discovery is unavailable, errors, or reports
an unreachable server), per docs/devel/mcp-tool-discovery.md Q2.

internal/auxiliary/utils.go:
- parseMCPToolsList is now strict: trims, strips markdown fences, trims
  again, then parses the WHOLE remaining string as a single JSON object.
  Removed the biggest-substring {...} extraction, the bare-array
  fallback, and the array-substring fallback. Validates via
  map[string]json.RawMessage that at least one of "tools"/"error" is
  present before unmarshaling into mcpToolsResponse. {"tools":[]} is a
  valid, legitimate zero-tools answer (not an error); {} (neither key) is
  a parse error. Updated the doc comment to describe the strict
  object-only contract.

internal/auxiliary/prompts.go:
- Added mcpToolsRetryReminder: a plain (non-embedded) Go string constant
  appended to FetchMCPToolsPromptTemplate on retry attempts, restating
  the required strict JSON response shape.

internal/auxiliary/workspace_manager.go:
- fetchMCPToolsViaLLM signature gained a configuredServerCount int
  parameter and now retries up to maxMCPToolsAttempts=2 times: on a
  provider error (aborting immediately without retry if ctx.Err() != nil
  and returning that error instead), on a strict-parse failure, or on an
  implausible empty result (zero tools while configuredServerCount > 0,
  only up to attempt < maxMCPToolsAttempts). An explicit agent-reported
  error is always definitive and never retried (zero tools -> error;
  some tools -> returned immediately). Retries append
  mcpToolsRetryReminder to the prompt. Existing debug/warn logging
  preserved, now tagged with the attempt number.
- FetchMCPTools: declares configuredServerCount := -1 before the
  discoverer check, sets it to len(results) on successful discovery (so
  the discoverer's own view of "how many servers were configured" feeds
  the plausibility check), and passes it through to
  fetchMCPToolsViaLLM. -1 (nil/errored discoverer) disables the
  plausibility check entirely, so a genuine zero-tools LLM answer is
  still accepted outright in that path (CLI/tests default). No change to
  the deterministic-tools branch, applyMCPToolsCachePolicy, or the
  cache-hit short-circuit.

Tests:
- utils_test.go: flipped "bare JSON array fallback" and "fenced bare
  JSON array" to wantErr:true (relied on removed leniency); added valid
  {"tools":[]} (0 tools, no error), bare {} (error), and a
  prose-with-embedded-object case (error, proving substring extraction
  is gone). Kept the still-valid object/fenced-object/agent-error/
  invalid-JSON cases (the trailing-commentary-braces case still passes
  unchanged since stripMarkdownFences already isolates the fenced
  content before parseMCPToolsList ever sees it).
- workspace_manager_test.go: added
  TestFetchMCPToolsViaLLM_RetryAndPlausibility with 5 subtests exercising
  fetchMCPToolsViaLLM directly via a call-counting promptFunc closure:
  parse-fail-then-success (2 calls), empty-plausible-then-success with
  configuredServerCount=2 (2 calls), empty accepted with count<=0 (1
  call, no retry), explicit agent error with count=2 (1 call, no
  retry), and both attempts failing (2 calls, error returned).

Verified via grep that parseMCPToolsList and fetchMCPToolsViaLLM have no
other production callers outside these files.

Gate: gofmt -l internal/auxiliary/ clean; go vet clean; golangci-lint
run ./internal/auxiliary/... = 0 issues; go test -race -count=1
./internal/auxiliary/... all green; go build ./... clean. make fmt run
before and right before commit, no reformatting needed.
…r unreachable MCP servers (mitto-sys.5)

Add a self-contained, unit-testable bounded exponential-backoff primitive
for re-probing configured-but-unreachable MCP servers, per
docs/devel/mcp-tool-discovery.md (Q4, point 3). This delivers the
primitive only; live wiring into session_ws.go is a separate follow-up
increment.

internal/mcpdiscovery/backoff.go (new):
- BackoffPolicy{Base, Factor, Max, MaxAttempts} + DefaultBackoffPolicy()
  (Base=1s, Factor=2.0, Max=30s, MaxAttempts=10).
- nextDelay(attempt int) time.Duration: pure, deterministic (no jitter)
  schedule function. attempt is 0-based (attempt 0 = delay before the
  2nd probe = Base; attempt n = Base*Factor^n, capped at Max when
  Max>0). Guards: Base<=0 -> 1s; Factor<1 -> 1.0; Max<=0 -> uncapped
  growth; overflow/non-finite results clamp to Max (or math.MaxInt64
  when uncapped).
- ProbeFunc = func(ctx) ServerToolsResult (reuses the existing
  discovery.go type unmodified).
- RetryUntilReachable(ctx, policy, probe, onReachable) (ServerToolsResult,
  bool): loops probe/backoff-sleep until Reachable=true, ctx is
  cancelled, or MaxAttempts is exhausted. A timeout/connect/list failure
  is NEVER treated as a negative — it only schedules another retry.
  onReachable fires exactly once, only on a genuine reachable result;
  never on exhaustion or cancellation. Checks ctx.Err() defensively at
  the top of each loop iteration and returns promptly via
  time.NewTimer+select during the backoff wait.
- ScheduleRetries(ctx, results, policy, probeFor, onReachable): thin
  fire-and-forget convenience that launches one RetryUntilReachable
  goroutine per unreachable result in results (skips reachable ones),
  for the future session_ws.go wiring.

internal/mcpdiscovery/backoff_test.go (new):
- TestBackoffPolicy_NextDelay_Schedule: exact schedule assertions
  (1s,2s,4s,8s,16s,30s,30s) for Base=1s/Factor=2/Max=30s.
- TestBackoffPolicy_NextDelay_Guards: Base<=0 -> 1s baseline;
  Max<=0 -> uncapped exponential growth (32s at attempt 5, uncapped).
- TestRetryUntilReachable_DiscoveredQuickly: probe returns unreachable
  for 3 calls then reachable; asserts discovered=true, onReachable
  called once with the reachable Tools, exactly 4 probes, and a fast
  wall-clock completion (tiny ms-scale Base/Max, no fake clock needed).
- TestRetryUntilReachable_NoNegativeOnTimeout: probe always returns
  Reachable=false/Err=context.DeadlineExceeded, MaxAttempts=3; asserts
  onReachable is NEVER called, discovered=false, exactly 3 probes, and
  the returned result is still Reachable=false (proving a timeout is
  surfaced as unreachable/keep-last-known-good, never a reachable-empty
  negative).
- TestRetryUntilReachable_ContextCancellation: cancels ctx right after
  the first unreachable probe with a 1-hour Base; asserts a prompt
  return (well under the Base delay), discovered=false, onReachable not
  called.
- TestScheduleRetries_OnlyProbesUnreachable: mixes one reachable and one
  unreachable input result; asserts probeFor is never invoked for the
  reachable server and onReachable fires exactly once for the server
  that becomes reachable after a couple of probes, using a WaitGroup +
  timeout guard against goroutine leaks/test hangs.
- All probe-call counters use sync/atomic or a mutex-guarded map (tests
  run with -race).

Verified via grep that ServerToolsResult's field names/types
(Server/Tools/Reachable/Err) match discovery.go exactly; this change
does not modify discovery.go, session_ws.go, server.go, or any other
existing file.

Gate: gofmt -l internal/mcpdiscovery/ clean; go vet clean; golangci-lint
run ./internal/mcpdiscovery/... = 0 issues; go test -race -count=3
./internal/mcpdiscovery/... all green (no flakiness across 3 runs,
timing tests use ms-scale Base so the whole suite still completes in
~1.3s); go build ./... clean. make fmt run before and right before
commit, no reformatting needed.
…servers into triggerMCPToolsFetch, additive last-known-good merge (mitto-sys.5)

Wire the bounded exponential-backoff scheduler primitive (built in the
previous increment: internal/mcpdiscovery/backoff.go) into the live
runtime so configured-but-unreachable MCP servers are re-probed until
reachable, and newly-discovered tools are broadcast. Closes mitto-sys.5.

internal/auxiliary/workspace_manager.go:
- Added mcpBackoffActive map[string]bool + mcpBackoffMu sync.Mutex
  (dedups the per-workspace backoff goroutine, at most one active at a
  time) and mcpBackoffPolicy mcpdiscovery.BackoffPolicy (defaults to
  mcpdiscovery.DefaultBackoffPolicy() in the constructor, overridable in
  tests for speed).
- Added mergeMCPToolsAdditive(workspaceUUID, fresh) ([]MCPToolInfo, bool):
  an additive-only merge that NEVER downgrades — tools absent from fresh
  are retained unconditionally (unlike applyMCPToolsCachePolicy's
  two-consecutive-negatives eviction, which would be wrong here since a
  backoff round only sees currently-reachable servers and would
  otherwise evict tools from servers still starting up or contributed by
  the LLM fallback). Refreshes descriptions of tools present in fresh,
  returns the merged name-sorted list and whether anything changed
  (no-op write when unchanged), and clears mcpToolsNegatives entries for
  reappearing tools.
- Added EnsureMCPBackoffRetry(ctx, workspaceUUID, onUpdate): no-op when
  StdioToolsDiscoverer is nil or a loop is already active for the
  workspace; otherwise launches (at most once per workspace, tracked via
  mcpBackoffActive) a goroutine that calls
  mcpdiscovery.RetryUntilReachable with a probe closure invoking
  StdioToolsDiscoverer each round, additively merging newly-reachable
  servers' deduped tool names via mergeMCPToolsAdditive and invoking
  onUpdate with the merged list whenever it changes. The probe reports
  Reachable=true (stopping the backoff loop) only once every configured
  server has responded (none left Reachable=false); RetryUntilReachable
  itself guarantees a probe timeout/error is never treated as a negative.
  onReachable is passed as nil since the probe closure already
  broadcasts incrementally per round. Debug-logs start/stop (guarded by
  m.logger != nil).

internal/web/session_ws.go:
- Added the internal/auxiliary import.
- In triggerMCPToolsFetch, after the existing
  eventsManager.Broadcast(WSMsgTypeMCPToolsAvailable, ...) block and
  before the go c.checkRequiredToolPatterns(...) call: if an auxiliary
  manager is configured, calls
  srv.auxiliaryManager.EnsureMCPBackoffRetry(context.Background(), ...)
  with an onUpdate callback that broadcasts WSMsgTypeMCPToolsAvailable
  with the updated tools list to all clients. Uses
  context.Background() (not the fetch's 30-minute ctx, which is
  cancelled by the function's deferred cancel() on return) since the
  backoff's own MaxAttempts bounds the loop; deduplication against
  concurrent/repeated triggers is handled inside the manager. Does NOT
  call checkRequiredToolPatterns from onUpdate (deliberate scope limit:
  its timer cascade would stack on every backoff round; frontend
  re-gating on late tools is a separate concern, mitto-sys.12).

Tests (internal/auxiliary/workspace_manager_test.go):
- TestMergeMCPToolsAdditive: empty-cache-plus-fresh; absent tool
  retained (no downgrade) with cache left unchanged; description
  refreshed; empty fresh is a no-op; a tool tracked only via a stale
  negatives-counter entry reappearing in fresh clears that entry.
- TestEnsureMCPBackoffRetry_ReadyAfterN: discoverer unreachable for 3
  calls then reachable with a tool; asserts onUpdate fires exactly once
  with the new tool and exactly 4 discoverer calls (acceptance #1: fast
  discovery, not a flat long wait).
- TestEnsureMCPBackoffRetry_NoNegativeOnTimeout: discoverer always
  returns Reachable=false/Err=context.DeadlineExceeded with
  MaxAttempts=3; asserts onUpdate never fires and a pre-seeded cache
  entry survives untouched after the loop exhausts (acceptance #2:
  last-known-good intact).
- TestEnsureMCPBackoffRetry_Dedup: two back-to-back EnsureMCPBackoffRetry
  calls for the same workspace; the second is a no-op against the
  already-active loop (mcpBackoffActive dedup), verified race-clean.
- TestEnsureMCPBackoffRetry_NilDiscoverer_NoOp: nil StdioToolsDiscoverer
  -> no goroutine started, onUpdate never called, no panic.
- All shared counters/collections in tests use sync/atomic or a
  mutex-guarded map/slice (run under -race).

Verified via grep that mcpdiscovery.ServerToolsResult's field names
(Server/Tools/Reachable/Err) match what's used here; did not modify
internal/mcpdiscovery/backoff.go, discovery.go, or internal/web/server.go.

Gate: gofmt -l internal/auxiliary/ internal/web/session_ws.go clean; go
vet on both clean; golangci-lint run on both = 0 issues; go test -race
-count=2 ./internal/auxiliary/... all green; go build ./... clean; go
test ./internal/web/... (incl. handlers/middleware) all green. make fmt
run before and right before commit, no reformatting needed.
…ol refresh (mitto-sys.4)

Add a self-contained, persistent-session watcher primitive for MCP
servers that support notifications/tools/list_changed, per
docs/devel/mcp-tool-discovery.md. This is increment 1 of N for
mitto-sys.4: the primitive only, NOT wired into session_ws.go or
workspace_manager.go (a later increment).

internal/mcpdiscovery/watch.go (new):
- ToolListWatcher: holds the live *mcp.ClientSession and the transport's
  cleanup func (if any). Close() is idempotent/concurrency-safe via
  sync.Once, closing the session and running cleanup on the first call
  only, in that order.
- WatchServer(ctx, srv, factory, onChange) (*ToolListWatcher,
  ServerToolsResult, error): builds the transport (factory defaults to
  DefaultTransportFactory when nil, matching DiscoverServers), connects
  mcp.NewClient with a *mcp.ClientOptions whose ToolListChangedHandler
  re-lists tools (via a DefaultTimeout-bounded context derived from ctx,
  so a hanging server can't block the SDK's read-loop goroutine
  indefinitely) and invokes onChange with the fresh ServerToolsResult
  (onChange may be nil; the notification is still handled without
  panicking). Unlike probeServer's one-shot connect-list-close, the
  session is deliberately kept open — its cleanup is deferred to
  Close(), not run inline. Does an initial ListTools and returns that
  result directly (not via onChange, which fires only for subsequent
  changes). On any transport-build/connect/initial-list failure: runs
  cleanup/closes any partially-opened session, and returns a nil
  watcher, an unreachable ServerToolsResult, and a wrapped error,
  matching probeServer's "mcpdiscovery: ..." error style.
- listToolsResult: shared ListTools-to-ServerToolsResult conversion used
  by both the initial list and the change-notification re-list.

internal/mcpdiscovery/watch_test.go (new):
- newWatchableServer: in-memory mcp.Server + TransportFactory helper
  (mirrors discovery_test.go's newMockStdioServer pattern), reusing the
  existing noopToolHandler from discovery_test.go (same package).
- TestWatchServer_InitialList: seeded tool appears in the initial
  result, Reachable=true, onChange not invoked for it.
- TestWatchServer_ChangeEventFiresCallback: after connect, AddTool then
  RemoveTools on the live server; asserts onChange fires (via a buffered
  channel + 2s timeout) with the updated tool set both times (new tool
  present after AddTool, removed tool absent after RemoveTools).
- TestWatchServer_CloseIsCleanAndIdempotent: first Close() returns nil,
  second Close() does not panic/double-close.
- TestWatchServer_NilOnChangeDoesNotPanic: a change event with
  onChange=nil does not panic.

Key implementation fix during development: the transport factory's
cleanup func must NOT be deferred/run inside WatchServer itself (unlike
probeServer, which is one-shot) — doing so was found to close the
in-memory transport's underlying context immediately upon return,
before any notification could ever be delivered. cleanup is now stored
on ToolListWatcher and only invoked from Close(), alongside the session
close.

Gate: gofmt -l internal/mcpdiscovery clean; go vet clean; golangci-lint
run internal/mcpdiscovery/... = 0 issues; go build ./... clean; go test
-race -count=2 ./internal/mcpdiscovery/... all green, no flakiness.
make fmt run before commit, no reformatting needed. Did not touch
discovery.go, backoff.go, session_ws.go, or workspace_manager.go.
…ven tool refresh (mitto-sys.4)

Build the manager-owned watcher pool + lifecycle on top of the
mcpdiscovery.WatchServer primitive (increment 1/N, already committed).
This is increment 2/N: pool + lifecycle + unit tests only. The web-layer
wiring (MCPServerLister/MCPWatchTransportFactory injection, broadcast on
onUpdate, StopMCPWatchers/CloseAllMCPWatchers calls on
session/server teardown) is a separate increment 3/N.

internal/auxiliary/workspace_manager.go:
- Added internal/agents import (leaf package, no import cycle).
- New WorkspaceAuxiliaryManager fields: MCPServerLister func(ctx,
  workspaceUUID) ([]agents.MCPServer, error) (exported seam, nil in
  tests/CLI -> EnsureMCPWatchers is a no-op); MCPWatchTransportFactory
  mcpdiscovery.TransportFactory (nil -> DefaultTransportFactory,
  overridable in tests); mcpWatchers map[string][]*mcpdiscovery.ToolListWatcher
  + mcpWatchersMu sync.Mutex (per-workspace pool registry; KEY PRESENCE,
  not slice length, marks a pool active/starting). Initialized
  mcpWatchers in the constructor.
- EnsureMCPWatchers(ctx, workspaceUUID, onUpdate): no-op if
  MCPServerLister is nil; dedups via a _, exists check under
  mcpWatchersMu, immediately reserving the slot (map[ws] = nil) before
  launching a single goroutine, so concurrent calls made while servers
  are still being listed/connected correctly see the pool as already
  active. In the goroutine: lists servers, and for each builds an
  onChange closure (ServerToolsResult -> []MCPToolInfo, skipping
  unreachable results) that additively merges into the cache via the
  existing mergeMCPToolsAdditive and fires onUpdate on change; calls
  mcpdiscovery.WatchServer per server. On a per-server watch error,
  logs at debug and skips it (EnsureMCPBackoffRetry's polling path
  remains responsible for retrying that server). On success, applies
  onChange to WatchServer's initial result too (so tools already present
  at startup surface immediately, not just on the first subsequent
  change), then appends the watcher to the workspace's slice under
  mcpWatchersMu -- but only after re-checking the key still exists: if
  StopMCPWatchers/CloseAllMCPWatchers tore the pool down while this
  server was still connecting, the just-opened watcher is closed
  immediately instead of being registered, and no further servers in
  this pool are started.
- StopMCPWatchers(workspaceUUID): grabs and deletes the workspace's
  slice under mcpWatchersMu, then closes each watcher outside any lock
  (ToolListWatcher.Close does not touch mcpWatchersMu, so this cannot
  deadlock). No-op when absent.
- CloseAllMCPWatchers(): same teardown across every workspace at once
  (for server shutdown), replacing the whole map under the lock then
  closing everything outside it. Idempotent/safe when empty.
- Deliberately reuses mergeMCPToolsAdditive as-is (never removes tools
  on a watcher update) for parity with EnsureMCPBackoffRetry;
  removal-on-list_changed is a documented follow-up, not attempted here.

internal/auxiliary/mcp_watchers_test.go (new):
- newWatchableMCPServer: in-memory mcp.Server + TransportFactory helper
  (mirrors mcpdiscovery/watch_test.go's newWatchableServer pattern,
  duplicated here since it isn't exported cross-package) with its own
  noopWatchToolHandler.
- TestEnsureMCPWatchers_InitialAndChangeRebroadcast: satisfies the bead's
  "simulate a list_changed notification and assert re-broadcast"
  acceptance -- after EnsureMCPWatchers, the seeded tool lands in the
  cache and the initial onUpdate fires; then AddTool on the live server
  triggers a second onUpdate (within a 2s deadline via a buffered
  channel) with the new tool present, and the cache reflects it too.
- TestEnsureMCPWatchers_Dedup: two EnsureMCPWatchers calls for the same
  workspace result in exactly one MCPServerLister invocation (atomic
  counter).
- TestStopAndCloseAllMCPWatchers: StopMCPWatchers removes the workspace
  key and is idempotent on a second call; CloseAllMCPWatchers is
  likewise safe to call (including twice) when empty.
- TestEnsureMCPWatchers_NilLister_NoOp: nil MCPServerLister -> no
  goroutine started, onUpdate never called, no watchers map entry
  created.
- All shared test state uses sync/atomic or a mutex-guarded bool (run
  under -race).

Gate: gofmt -l internal/auxiliary clean; go vet clean; golangci-lint
run internal/auxiliary/... = 0 issues; go build ./... clean; go test
-race -count=2 ./internal/auxiliary/... all green, no flakiness. make
fmt run before commit, no reformatting needed. Did not touch
internal/web/session_ws.go or internal/web/server.go.
…teardown (mitto-sys.4)

Wire the increment 1/2 primitives (mcpdiscovery.WatchServer,
WorkspaceAuxiliaryManager's watcher pool) into the live runtime end to
end: production server enumeration, WebSocket broadcast, and teardown
on both server shutdown and per-workspace removal. Also fixes a
reservation-leak bug found in increment 2's EnsureMCPWatchers. Closes
mitto-sys.4.

internal/web/server.go:
- Inside the existing agentsDir-resolved block (sharing agentMgr and
  sessionMgr with the StdioToolsDiscoverer wiring), added a sibling
  auxiliaryManager.MCPServerLister assignment: resolves the workspace's
  ACP agent the same way StdioToolsDiscoverer does, then calls
  agentMgr.ListMCPServers(ctx, agent.DirName, &agents.MCPListInput{Path:
  ws.WorkingDir}) and returns out.Servers (nil out -> nil, nil). Left
  MCPWatchTransportFactory unset (nil -> DefaultTransportFactory in
  production; it exists only for test injection).
- In Shutdown(), added a auxiliaryManager.CloseAllMCPWatchers() call
  right after sessionManager.CloseAll("server_shutdown"), tearing down
  every persistent watcher session on server shutdown.

internal/web/session_ws.go (triggerMCPToolsFetch):
- Immediately after the existing EnsureMCPBackoffRetry block, added an
  EnsureMCPWatchers(context.Background(), wsUUID, onUpdate) call (same
  context.Background() rationale as the backoff call: the fetch's ctx is
  cancelled on return, so the persistent watcher must use an independent
  context whose lifetime is bounded by explicit teardown, not the fetch).
  onUpdate broadcasts WSMsgTypeMCPToolsAvailable with the updated tools
  (mirroring the backoff path), AND additionally broadcasts
  WSMsgTypePromptsChanged (reason: "mcp_tools_event") so
  enabledWhen-gated prompts re-evaluate immediately on a genuine
  list_changed event, without waiting for the existing 30/60/120s
  checkRequiredToolPatterns timer cascade (left untouched; that path
  remains the fallback/eventual-consistency mechanism, its removal is a
  separate concern per mitto-sys.12). Captures only srv/wsUUID (never
  the per-client c), since this callback can fire long after the
  triggering client disconnects.

internal/conversation/session_manager.go (RemoveWorkspace):
- After sm.wsRegistry.RemoveWorkspace(uuid), reads sm.auxiliaryManager
  under sm.mu.RLock() (matching the existing read pattern used
  elsewhere in this file, e.g. around line ~2765) and calls
  auxMgr.StopMCPWatchers(uuid) when non-nil, so a removed workspace's
  persistent MCP connections are torn down immediately rather than
  leaking until server shutdown.

internal/auxiliary/workspace_manager.go (EnsureMCPWatchers bug fix):
- On MCPServerLister returning an error, the goroutine previously
  returned without releasing the reserved-but-empty mcpWatchers[ws]=nil
  slot set before it was launched, permanently blocking all future
  EnsureMCPWatchers calls for that workspace (the dedup check would
  always see the workspace key present). Fixed: before returning on a
  lister error, re-locks mcpWatchersMu and deletes the entry only if it
  is still the empty reservation (len(w)==0) — guarding against
  clobbering a pool a concurrent call may have populated in the
  meantime — so a subsequent EnsureMCPWatchers call can retry cleanly.

internal/auxiliary/mcp_watchers_test.go:
- Added TestEnsureMCPWatchers_ListerErrorReleasesReservation: an
  EnsureMCPWatchers call with a lister returning an error is confirmed
  to release the reservation (polled with a timeout), then a second
  EnsureMCPWatchers call with a working lister is confirmed to actually
  invoke it (atomic counter) and complete normally (onUpdate fires),
  proving the earlier failure does not permanently block retries.

Verification (all commands run from repo root, actual output):
- gofmt -l internal/web internal/auxiliary internal/conversation -> (empty, clean)
- make fmt -> "go fmt ./..." (no files touched)
- go vet ./internal/web/... ./internal/auxiliary/... ./internal/conversation/... -> (empty, clean)
- golangci-lint run internal/web/... internal/auxiliary/... internal/conversation/... -> "0 issues."
- go build ./... -> (empty, clean)
- go test -race -count=2 ./internal/auxiliary/... ./internal/mcpdiscovery/... ->
    ok github.com/inercia/mitto/internal/auxiliary 1.472s
    ok github.com/inercia/mitto/internal/mcpdiscovery 2.101s
- go test ./internal/web/... ./internal/conversation/... ->
    ok github.com/inercia/mitto/internal/web 9.491s
    ok github.com/inercia/mitto/internal/web/handlers 1.510s
    ok github.com/inercia/mitto/internal/web/middleware (cached)
    ok github.com/inercia/mitto/internal/conversation 266.990s

Follow-ups noted (not fixed here, out of this increment's scope):
- mitto-sys.12: remove/shrink the 30/60/120s checkRequiredToolPatterns
  timer cascade now that both the backoff and watcher paths broadcast
  prompts_changed event-drivenly on tool changes.
- Increment 2's documented gap remains: mergeMCPToolsAdditive never
  removes tools on a watcher-observed list_changed re-list (parity with
  the backoff path's existing limitation) — a genuine tool removal is
  not currently propagated to the cache/frontend.
- WatchServer's initial Connect+ListTools call (in EnsureMCPWatchers'
  per-server loop) is not wrapped in its own bounded timeout the way the
  backoff probe is; a wedged transport during the initial connect could
  block that one server's watcher goroutine indefinitely. Worth a
  follow-up if observed in practice.
…mitto-sys.12)

checkRequiredToolPatterns previously scheduled a 30s/60s/120s timed loop
re-broadcasting prompts_changed (reason mcp_tools_retry) as a band-aid
for slow/unreliable MCP tool discovery. That mechanism is now redundant:
late/changed tools surface via the event-driven watcher (mitto-sys.4:
EnsureMCPWatchers -> onUpdate -> broadcastPromptsChanged("mcp_tools_event"))
and the bounded-backoff re-probe path (mitto-sys.5), both of which
re-broadcast prompts_changed as soon as tools actually change instead of
guessing at fixed delays. Per docs/devel/mcp-tool-discovery.md (Q3.4/Q4).

internal/web/session_ws.go:
- Removed the retryDelays := []time.Duration{30s, 60s, 120s} loop
  (including its time.NewTimer/select/c.ctx.Done() handling) from
  checkRequiredToolPatterns entirely.
- Kept the pattern-collection early return, the debug log on the
  no-patterns path, and the single immediate
  c.broadcastPromptsChanged("mcp_tools_initial") call.
- Updated the method's doc comment and the debug log message (was
  "scheduling re-broadcasts for late-loading tools", now describes the
  event-driven watcher/backoff paths as the mechanism for late/changed
  tools, and states this method only emits the initial filtered
  broadcast).
- Did not remove the time import: it's still used elsewhere in the file
  (e.g. broadcastPromptsChanged's time.Now()) -- confirmed by a clean
  build/gofmt.

internal/web/session_ws_test.go:
- Added TestCheckRequiredToolPatterns_NoTimedRetry: writes a real
  MITTO_DIR/prompts/ prompt file with an enabledWhen Tools.HasPattern(...)
  expression (via a real config.PromptsCache, so
  collectRequiredToolPatterns returns a non-empty pattern list -- the
  precondition for the broadcast path to run at all), wires a real
  GlobalEventsManager + a registered GlobalEventsClient with a capturable
  send channel as server.eventsManager, then calls
  checkRequiredToolPatterns in a goroutine and asserts: (1) it returns
  within 1s (elapsed asserted < 500ms) -- proving no 30/60/120s timed
  loop remains; (2) draining the capture channel yields EXACTLY ONE
  prompts_changed broadcast, with reason "mcp_tools_initial"; (3) no
  broadcast ever carries reason "mcp_tools_retry". This is a genuine
  behavioral test exercising the real method end-to-end (real prompts
  cache, real events manager/broadcast plumbing), not just a source-grep.

Verification (all commands run from repo root, actual output):
- gofmt -l internal/web/session_ws.go internal/web/session_ws_test.go -> (empty, clean)
- go vet ./internal/web/... -> (empty, clean)
- go build ./... -> (empty, clean)
- go test ./internal/web/... ->
    ok github.com/inercia/mitto/internal/web 8.175s
    ok github.com/inercia/mitto/internal/web/handlers (cached)
    ok github.com/inercia/mitto/internal/web/middleware (cached)
- grep -n "mcp_tools_retry\|retryDelays" internal/web/session_ws.go -> no matches
- (additional) golangci-lint run internal/web/... -> "0 issues."
- make fmt run before commit; no reformatting needed.
… type-based local/remote shapes) (mitto-sys.13)
Persist the real-MCP-derived (deterministic tools/list) snapshot per workspace
so a restart reuses it within a TTL instead of re-probing every server. The
LLM fallback is never written to disk (ADR docs/devel/mcp-tool-discovery.md Q3.3).

- appdir: add MCPToolsCacheDir()/MCPToolsCacheDirName ($MITTO_DIR/mcp-tools-cache).
- WorkspaceAuxiliaryManager: exported MCPToolsPersistDir (wired by web layer;
  empty in tests/CLI disables persistence) + mcpToolsTTL (default 15m).
- persistedMCPTools{Tools, UpdatedAt} on-disk snapshot; load/save/delete helpers
  using fileutil.ReadJSON / WriteJSONAtomic.
- FetchMCPTools: on in-memory miss, reuse a fresh on-disk snapshot within TTL
  (skips probe); after a probe, persist ONLY the deterministic list.
- ClearMCPToolsCache also removes the snapshot so manual refresh forces a probe.
- server.go: wire MCPToolsPersistDir to appdir.MCPToolsCacheDir().
- Tests: persist real-MCP, never persist LLM fallback, load-within-TTL skips
  probe, expired snapshot re-probes + overwrites, clear removes snapshot.
…o-vky)

EnsureBuiltinPrompts already pruned orphaned *.prompt.yaml files not in the
embedded set, but left legacy old-format *.md builtin files behind (~60
observed cruft files from pre-migration Mitto versions). The loader only
reads *.prompt.yaml, so these never load, but they accumulate in the global
builtin dir.

Extend the existing prune loop to also remove *.md files. The embedded set
is *.prompt.yaml only, so every .md file in the builtin dir is legacy. Scope
stays on the builtin targetDir only; no loader changes and no touching
user/workspace .mitto/prompts overrides.

Add config/builtin_prompts_test.go covering pruning of stale *.prompt.yaml
and legacy *.md, preservation of embedded prompts and unrelated-extension
files, plus an idempotency check.
Surface per-conversation usage/orchestration/activity statistics in the
properties panel. Two sourcing strategies with different restart behavior:

- Event-derived (survive restart): MCP call counts, UI/children-wait
  breakdown, turns, ACP tool calls, permissions allowed/denied, errors,
  and images uploaded are computed in a single pass over persisted events
  (computeEventStats), deduplicated by tool_call_id. children_spawned is
  derived from CountChildSessions.
- In-memory (reset on restart): cumulative token usage (accumulated in
  promptDispatcher.accumulateTokenUsage) and child-wait count/total
  (accumulated via BackgroundSession.RecordChildWait from the parent's
  blocking mitto_children_tasks_wait calls).

Stats are computed at WebSocket connect only (not per prompt_complete) to
bound cost, and rendered in the ConversationPropertiesPanel Statistics
section.
Loop config (prompt/arguments/trigger) survives archive in loop.json, but
MarkStopped(StoppedReasonArchived/ResumeFailures) left it disabled with no
broadcast, so clients never learned it was still there after unarchive.

restoreLoopOnUnarchive now:
  - no-ops when the session has no loop configured,
  - auto-re-enables loops stopped for an archive-related reason (manual
    archive or ACP resume-failure auto-archive), bootstrapping
    onCompletion loops,
  - leaves other pause reasons (user-paused, max iterations) untouched,
  - always re-broadcasts loop_updated so the editor/pill re-render.
…seudo-element

The three tab labels combined daisyUI's .tab (tabs-lift) and .tooltip on the
same element; both render into ::before/::after and collided. When a tab's
hidden radio gained focus, the tooltip pseudo-element merged with tabs-lift's
active-tab corner and painted a stray grey bar under the tabs. Replace the
daisyUI tooltip/data-tip with a native title attribute (matching
WorkspacesDialog's tabs) and add a Playwright regression guard.
Document the deterministic (internal/mcpdiscovery) vs LLM-fallback tool
discovery, disk persistence with TTL, the strict-JSON parsing anti-pattern,
and the per-agent mcp-list.sh audit table (known-broken scripts). Add the
mcpdiscovery package to the overview and mcp-tool-discovery.md to the docs
index.
The /api/issues, /api/issues/stats and /api/issues/{id} handlers returned a
bare HTTP 500 on bd query failures with no server-side error log, making them
invisible in mitto.log. Convert writeBeadsError to a *Handlers method that
logs the underlying error + stderr before writing the 500, and route the read
handlers through a new runBeadsRead helper that retries transient bd failures
(bounded; skips context-cancel and not-found) so transient beads-store
instability self-heals instead of surfacing as a 500.
Installing a filesystem watch for an absent .beads directory (a workspace
without beads tracking) logged a WARN 'Failed to add watch for beads dir'.
Return nil and log at DEBUG when the .beads dir and its parent (up to the
filesystem root) do not exist, since absence is expected. Adds a regression
test asserting no WARN is emitted.
Copilot AI review requested due to automatic review settings July 5, 2026 09:12

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 9403463 into main Jul 5, 2026
4 checks passed
@inercia
inercia deleted the fix/remaining-work branch July 5, 2026 11:39
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