Feature/wakild daemon - #12
Merged
Merged
Conversation
…D10 inventory Card #148 (https://trello.com/c/Ba4YYGXM) planning artifacts on feature/wakild-daemon: - docs/design/wakild-foundation.md — the wakild daemon foundation design doc, verbatim from the user (command/event split, Connect API, tenancy from day one, SQLite+kvr storage split, P0-P5 phases). - docs/cards/card-148-wakild-impl-plan.md — FINAL implementation plan, gated by Mashura (3 panels). Key decisions: P0 is daemon-shaped (non-blocking SubmitInput), two event classes, store-pluggable sequencer, tenancy from P0, sync->async approval shim in P0, Service split, P1 replay = projection not event sourcing. - docs/cards/card-148-d10-inventory.md — P0 chunk 1: full inventory of every direct *agent.App access from internal/tui and cmd/wakil (~130 accesses, 7 output channels, 27 sendEvent sites, 1 channel-bearing message). No code changes in this commit.
… chunk 2) First code of P0. internal/core/event is the domain event vocabulary every client will consume; it lives under internal/core so that core never imports api/gen or internal/server (foundation doc §2.1). Implements plan decisions D2/D4/D6: - Two event classes (durable replayable vs ephemeral live-only), derived from Kind; MessageCommitted is the durable counterpart of MessageDelta. - Tenancy from day one: TenantID/SessionID on every envelope, embedded tnt_local/usr_local principals; no empty tenant passes. - Events are data-only: single envelope + typed payload, no channels/callbacks. Kind/payload pairing enforced against an exact reflect.Type registry (not name strings, so same-named types from other packages or proto can't match). - Draft vs committed lifecycle: ValidateDraft (Seq==0) vs ValidateCommitted (durable Seq>=1, ephemeral Seq==0), so the appender has a clean pre/post contract before the store exists. - Typed prefixed IDs (tnt_/usr_/wsp_/ses_/trn_/tcl_/apr_/sub_) with Validate methods, called from the envelope AND payloads; checkID panics on unknown kind (programmer error) instead of silently passing. Mashura (3 panels) reviewed this chunk; folded in: draft/committed split, ephemeral Seq==0 enforcement, reflect.Type registry, typed-nil pointer rejection, payload ID/enum validation, checkID unknown-key guard, registry completeness test. Verified: go test + go test -race pass, go build ./... clean, go vet clean, gofmt clean.
…n (card #148 P0 chunk 3) Introduces the transport-free core contract surface (plan D3/D4/D7) that chunks 4-8 and P1/P2 build on, plus prefixed UUIDv7 id generation. internal/core (package core, service.go): - Three behavior-first interfaces (D7): SessionService (commands), EventReader (Subscribe/ListEvents), SessionReader (queries/snapshot). Bootstrap (config, proxy.Client, executor, sinks) stays OFF the interfaces. - Principal identity model (D4): typed tenant/user + role + scopes + auth_method; EmbeddedPrincipal() returns a fresh value (no mutable global). - Session state machine with an explicit transition table + CanTransitionTo. - Store contracts (D3): EventAppender (durable-append + sequence assignment, atomic, commit->notify point; rejects ephemeral drafts) and EventLog (cursor-addressable durable read). Sequencer is NOT exposed as a seam -- assignment folds into Append so contiguous sequences can't be violated. - ApprovalOutcome enum (deny/allow_once/allow_reads_once) instead of a representable-ambiguous bool pair; decision carries session correlation. - SubmitInput contract pinned: FIFO enqueue (bounded), TurnAck = acceptance not completion, error-session re-drive via SubmitInput, non-blocking. - Subscription lifecycle contract (handoff, dedup-by-seq, Next->io.EOF after Close, backpressure) and a fuller error vocabulary (state transition, approval not-found/already-resolved, subscription closed). internal/core/id: - Generator producing event-prefixed UUIDv7 ids (tnt_/usr_/wsp_/ses_/trn_/ tcl_/apr_/sub_); NewFromReader for deterministic tests; package-level helpers on crypto/rand. Promotes github.com/google/uuid v1.6.0 to a direct dependency. Verified: go build ./... + go vet clean; go test -race ./internal/core/... green; full go test -count=1 ./... green across all packages; gofmt clean on tracked files (CI gate). Mashura review (3 panels) folded in: Sequencer seam removed, ephemeral-append rejection, approval enum, state-transition table, EmbeddedPrincipal as function, consistent explicit principal args, submit enqueue-vs-busy resolution, subscription lifecycle contract, not-found vs not-authorized resolution, honest SessionSnapshot framing. Known seams noted in docs, not implemented: tenant-isolation is service-layer (plan D4/P1 note), pagination/retention, idempotency enforcement (RequestID reserved), P1 session-creation fields (additive). Refs: https://trello.com/c/Ba4YYGXM
Implements deliverable 3 of the P0 plan and all three service interfaces
(SessionService, EventReader, SessionReader) over the D3 store contracts.
- One executor goroutine per session with a bounded FIFO input queue;
SubmitInput is genuinely non-blocking and returns TurnAck{TurnID}.
- Turn finalization is a single lock-protected linearization point
(finishTurn), so a close or interrupt racing a turn's return can never be
lost or park the session in a stuck state.
- Interrupt/CloseSession cancel via an internal context and emit
TurnCompleted{cancelled} / SessionClosed — never a silent abort.
- Durable event log (MemLog) with contiguous, atomic append-as-one-step
sequencing; the executor is the single commit->notify point.
- Subscribe registers-before-replays, stages replay history in an unbounded
seq-ordered pending queue, and merges concurrent live commits without
order inversion or loss. Slow subscribers are disconnected with
ErrSubscriptionGap (never block the executor; durables never silently lost).
- Turn output is committed as MessageCommitted; acked-but-never-run inputs are
always explicitly abandoned with TurnCompleted{cancelled} (never silent).
- Crash-recovery stub RecoverRunning: recovered sessions enter error state
with a SessionError{daemon_restart} marker.
- Tenant isolation (no existence leak) and role gating on every method.
Verified: build/vet clean; go test -race -count=5 on sessionhost green;
full go test -count=1 ./... green across all 27 packages; gofmt clean.
Mashura review (card #148 chunk 4): two panels converged on blocking defects
(finalization race, replay/live reordering + lossy catch-up, discarded turn
output, subscription detach leak); all folded in.
Trello: https://trello.com/c/Ba4YYGXM
… + agent adapter) Card #148 P0, workflow step 4: plan for chunk 5 (deliverables 4 + 6). Reviewed by three panels (gpt-5.6-sol, claude-fable-5, glm-5.2); feedback folded in: serialized append->notify (emitMu), turn fencing + late-emission contract, internal_error classification, ctx-aware approval confirmer with full outcome fidelity, TurnInput.UserID resolver identity, authoritative message text = TurnOutcome.Text, tool/subagent durable events deferred.
…#148 P0 chunk 5)
Implements plan deliverables 4 + 6: the command/event boundary's outbound
half. A turn can now emit intermediate domain events (not just its flat
text/error result), and a real *agent.App turn drives an in-memory host
session end-to-end through SessionService/EventReader only.
sessionhost:
- TurnInput.Emit: turn-scoped Emitter (durable Emit + ephemeral Notify),
concurrent-safe, fenced at finalization.
- s.emitMu serializes durable append->notify across ALL producers (executor
terminals + worker-emitted events) so subscribers observe exact increasing
Seq order.
- Fence check runs INSIDE emitMu against finishTurn's terminal emit, so a
turn-emitted durable event can never append after its TurnCompleted
(terminal ordering).
- Host-owned kind allowlist; ErrEmitFailed + ErrInternal classify emit/store
failures as SessionError{internal_error}, not backend_failure.
- TurnInput.UserID carries the submitter principal for audit identity.
internal/wiring (new):
- HostTurnFunc: installs agent Out/OnReasoning/Confirm callbacks, runs the
SendOutcome -> WaitForAsyncCompletion -> Resume loop, restores callbacks
panic-safe. Bound to ONE host session (single-App, single-session rejected
loudly). MessageDelta/ReasoningDelta emissions; authoritative text returns
TurnOutcome.Text.
- hostConfirmer (D5 shim): ApprovalRequested -> ApprovalResolved with full
approve/decline/allow-reads fidelity; resolver runs in a goroutine raced
against ctx cancellation (stuck resolver cannot hang Interrupt/Close);
emit failures latch into the turn error (no orphaned/duplicate approvals).
Docs: MessageDelta presentation-streaming caveat; ApprovalResolved.Resolver
now populated in P0; SubmitInput.ReadAction doc corrected; plan fence
wording corrected (linearizes before terminal append).
Reviewed by three panels (gpt-5.6-sol, claude-fable-5, glm-5.2): fold-ins
from both plan review and implementation review are applied.
Verified: go build/vet clean; go test -race full suite green (28 pkg);
go list structural gate clean (internal/core imports no bubbletea/server/
agent/tui/api-gen; wiring is the sole bridge). Trello: card #148.
Ref: https://trello.com/c/Ba4YYGXM
… step 1/2) Introduce Control (user/session commands) and StateApply (round-trip runtime results) interfaces in internal/agent, implemented by *App, and route all 20 TUI mutation sites through them. Fix ResumeSessionMsg + AppendSystemMessage to take convMu (pre-existing lock bypass). Add a heuristic structural guard test (narrowed: field writes rooted at m.app + seam-method calls through m.app + enumerated pass-*App set) plus a negative test proving the guard is not vacuous, and fake-Control/StateApply routing tests proving the seam is used. D12 + deliverable-5 completion remain red (TUI still holds *agent.App; the turn-driving re-route is the next chunk).
…n host (deliverable 5 step 2, exit gate #2 partial)
…ease path (Mashura-gated) 7b1: server-side foundations for closing Gate #1 (remove *agent.App from internal/tui). No TUI cut yet — Gate #1 stays red; the TUI keeps driving *agent.App until 7b3. sessionclient (NEW package, internal/core/sessionclient): - Agent-free facade interface + neutral DTO inventory (D26) - Imports only event, proxy, core — never internal/agent - go list -deps verified: zero internal/agent in transitive graph - DTOs: Consent, ContextLimit, Backend, OpID, SessionSummary, SessionScope, ApprovalChoice, ApprovalRequest, ClientSnapshot, WorkflowSnapshot, CommandResult, RotateRequest, RepoStateMutator, CompletionSource - CommandResult replaces agent.HandleTUICommand's (handled, quit, cmd) return — no agent.Cmd/Msg leak; Validate() rejects contradictory states - ClientSnapshot: immutable version-stamped view-model (D26), not live getters - Full Facade interface: SessionService + EventReader + TUI-specific ops + client-initiated mutations (D26 grounding #11) + side questions (D29) + slash-command dispatch (D23) + session listing + lifecycle wiring/hostturn.go — appOwners release path (7b1): - appOwners map: struct{} → *hostTurn (tracks the owner) - HostTurnHandle: bundles TurnFunc + Release() + App() - NewHostTurnHandle: factory entry point returning the handle - HostTurnFunc: kept as thin wrapper (headless backward compat) - Release(): idempotent, rejects active turns (ErrTurnActive) - run(): atomically sets turnActive; rejects second concurrent turn and released-before-start (defense-in-depth + concurrency fix from Mashura review) Tests: - sessionclient: structural agent-free guard (go list -deps), ContextLimit parity, CommandResult.Validate - wiring/factory_test.go: claim/release/reclaim lifecycle, idempotent release, active-turn rejection, released-turn rejection, concurrent claim/release - All green under -race (29 packages) Mashura review op-12 (gpt-5.6-sol) findings addressed: - Critical: run() now rejects a second concurrent turn atomically - doc.go: 'event and proxy only' → 'event, proxy, and core' - TestPackageIsAgentFree: real go list -deps guard (not a placebo) - CommandResult.Validate + RotateRequest.Validate added - Rotation rationale clarified: fresh App pointer; Release is cleanup signal
…new event kinds (Mashura-gated) Event package (D24/D28/D29): - 10 new event kinds: user_message_committed (durable, replay truth), conversation_compacted, workflow_turn_started, workflow_final_review, async_job_started/completed, side_question_completed (all durable), tok_rate, async_job_progress, side_question_progress, learn_nudge (all ephemeral) - OpID type with op_ prefix, validation, constructor - TurnCompleted gains Warn + WorkflowWillContinue fields (D28) - All new payloads with Validate() methods - Registry + completeness + validation tests updated Session host (D24/D25): - SessionEmitter interface: session-scoped emitter fenced at session close (not turn close) — legal for detached work after turn completion - sessionEmitter concrete impl: rejects host-reserved, turn-scoped, and ephemeral kinds on Emit; accepts ephemeral on Notify - turnScopedKinds allowlist: approvals, tool calls, subagent events rejected by session emitter to preserve terminal turn ordering - UserMessageCommitted emitted from executor goroutine (handleInput) not SubmitInput — guarantees FIFO ordering invariant - UserMessageCommitted in hostReservedKinds (host-owned) - ParkApproval hook on TurnInput: parks session in awaiting_approval, blocks turn goroutine on decision channel raced with ctx - Real RespondToApproval: validates pending approval, resolves via buffered channel, idempotent same-outcome duplicates - Cancel-during-approval: ctx cancellation → forced decline before emitter fence - Resolver identity: ApprovalResolved.Resolver records who actually answered (principal.UserID), not just the turn submitter Wiring adapter (D25): - WithAsyncApproval() option: TUI uses async park+resolve path; headless keeps sync inline resolver (parity unchanged) - app.OnTokRate wired to SessionEmit.Notify(KindTokRate) - app.EventSink wired to session-scoped surface (projection TODO 7b3) - ApprovalResolved emit failure now fails closed (return false, not just log) — safety invariant Tests (14 new, 2 updated): - Session emitter: legal-after-turn, fenced-at-close, rejects-host-reserved, rejects-ephemeral-emit, rejects-turn-scoped-kinds, notify-accepts-ephemeral - Async approval: round-trip, cancel-during-approval, not-found, already-resolved, wrong-id, concurrent-park-and-resolve (race) - Headless sync parity: unchanged - Event: OpID validation, new payload validation, updated kind class test - Updated TestSubscribeReplayOverlap for UserMessageCommitted event Pre-existing flaky test TestCloseSessionEmitsSessionClosedAndIsIdempotent confirmed flaky before and after changes (state-before-event race in test). Mashura review (op-14, gpt-5.6-sol): 5 critical findings addressed (host-reserved UserMessageCommitted, executor-goroutine ordering, resolver identity, fail-closed audit, turn-scoped kind rejection); D28 workflow emission and D29 EventSink projection deferred to 7b3 per plan scope.
…pproval resolver identity, SetAllowReads ordering, event projection scaffold (Mashura-gated) Three 7b2 bugs identified by Mashura review (op-17, 3 panels) fixed: 1. Detached event delivery broken (hostturn.go): hostTurn.run saved/restored app.EventSink and app.OnTokRate per-turn. Between turns, the sink reverted to main.go's globalProg.Send — detached async jobs/side questions lost the session-scoped emitter. Fix: session-scoped callbacks (EventSink, OnTokRate) are now installed permanently on the first turn and NOT restored. Only turn-scoped callbacks (Out, Confirm, OnReasoning) are restored per-turn. The hostTurn struct gains a sessionEmit field that persists across turns. 2. Approval resolver identity (hostturn.go): forced decline on cancel/interrupt used in.UserID (submitter) instead of a system principal. D25 says cancellation records the system/interrupt principal. Fix: add event.SystemUserID and use it for forced declines. 3. SetAllowReads before durable emit (hostturn.go): app.SetAllowReads(true) was called before ApprovalResolved was durably emitted. If the append failed, consent was mutated but the turn failed. Fix: move consent mutation AFTER successful durable emit. 4. Event projection scaffold (projection.go): the app.EventSink was a no-op (TODO(7b3)). Added projectAgentEvent with the full mapping structure (skeleton — projection implementations land in 7b3 m2). This is the legal emit path for all agent message types through the session-scoped emitter. TestCallbackRestore updated: OnTokRate/EventSink are now session-scoped (permanent, not restored). Only Out/Confirm/OnReasoning are turn-scoped. Refs: card-148-chunk7b-plan.md D24/D25, Mashura op-17 (gpt-5.6-sol, claude-fable-5, glm-5.2)
…nterface, snapshot fixes, async command mechanism, mapping matrix m1 (Contracts & Lifecycle): - internal/core/format: extracted ShortID, Truncate, DerefStr, Indent, Yellow, StrPtr, TranscriptSize from internal/agent so agent-free packages (sessionclient, wiring, TUI) can use them without importing internal/agent. Includes agent-free structural guard test. - ClientSnapshot: added OutputMode field (was missing), added slice immutability documentation and test. - CommandResult: added OpID field for async commands (/handoff, /remember, /recall) that need event-based completion notification. - ConversationManager interface: agent-free, sits above facade, handles /new, /resume, /handoff rotation. Documents detached-job cancel policy. - mapping_matrix.go: complete command/message mapping matrix documenting every agent.Msg type → event.Kind or CommandResult or ClientSnapshot field, plus slash-command → CommandResult mapping. - Structural tests: OpID validation, OutputMode field, slice cloning, ConversationManager interface shape. All tests pass: go build ./..., go vet, agent, TUI, cmd/wakil, core.
m2 (Host Capabilities & Projection):
- Complete projectAgentEvent in internal/wiring/projection.go:
- SubagentStartMsg → KindSubagentSpawned (durable)
- SubagentActiveMsg → KindSubagentProgress (ephemeral, [active] marker)
- SubagentChunkMsg → KindSubagentProgress (ephemeral)
- SubagentFinishedMsg → KindSubagentProgress (ephemeral, [finished:status])
- SubagentDoneMsg → KindSubagentCompleted (durable, status from Err)
- AsyncJobStartMsg → KindAsyncJobStarted (durable)
- AsyncJobChunkMsg → KindAsyncJobProgress (ephemeral)
- AsyncJobDoneMsg → KindAsyncJobCompleted (durable, status from Err)
- SideQuestionChunkMsg → KindSideQuestionProgress (ephemeral)
- SideQuestionDoneMsg → KindSideQuestionCompleted (durable, status from Err)
- AgentDoneMsg.LearnNudge → KindLearnNudge (ephemeral, if non-empty)
- ToolStartMsg/ToolResultMsg: dropped (turn-scoped path handles these)
- SysNoteMsg/CompactedMsg/BackendCtxLimitMsg/ModelListUpdatedMsg/
MCPReconnectedMsg/TokRateMsg: dropped (snapshot fields or client-local)
- ID mapping helpers: subagentIDFromChatID, opIDFromString, toolCallIDFromString
(deterministic prefix-stripping: proxy ID body reused with domain prefix)
- 33 projection tests covering every message type, nil/closed emitter,
ID helpers, error status derivation, dropped messages, unknown types.
All tests pass: go build, go vet, wiring package.
m3 (Wiring Implementation & Event Pump): - wiringFacade: implements sessionclient.Facade by bridging *agent.App + *sessionhost.Host. Delegates SessionService/EventReader to host, constructs ClientSnapshot from App fields, routes mutations to App methods. - conversationManager: implements sessionclient.ConversationManager. Creates fresh *agent.App for each conversation, wires to host, handles /new, /resume, /handoff. Detached-job cancel policy on close. - EventPump: goroutine driving EventSubscription.Next, delivers events to TUI callback. Handles subscription gap recovery (resubscribe from lastSeq), pump cancellation, rotation drain via Done() channel. - ClientSnapshot.Costs changed to *proxy.CostTracker (CostTracker has a mutex, cannot be copied by value). - interpretAgentMsg: translates agent.Msg from HandleTUICommand into CommandResult fields (Notice, Rotate, Submit, Compacted). - Conversion helpers: toClientContextLimit, toAgentContextLimit, toClientBackends, toClientWorkflow. - Tests: event pump delivery, idempotent stop, ctx cancel, interface satisfaction. All tests pass: go build, go vet, core, wiring, tui, cmd/wakil.
…ate, event pump ownership) Fixes from Mashura op-20 review: 1. EventPump gap recovery: use errors.Is instead of == (wrapped errors) 2. EventPump lastSeq: accept initialSeq in constructor so gap recovery starts from the subscription's cursor, not zero (prevents full-history replay) 3. EventPump EOF: use errors.Is(err, io.EOF) for terminal error 4. SaveRepoState: initialize mutator from current repo state before callback so unset fields preserve existing values (no accidental zeroing) 5. wiringFacade: own the EventPump (not just subscription); Close stops the pump and drains it, not just the subscription All tests pass: go build, go vet, wiring package.
… session notes, learn-nudge parity Mashura review panel (3 panels unanimous) chose Option A: the adapter runs HandleWorkflowTransition after a successful turn and enqueues the continuation through a host-provided TurnInput.EnqueueInput closure — the TUI is passive. - sessionhost: TurnInput.EnqueueInput hook; enqueueTurn (SubmitInput-equivalent acceptance semantics); finishTurn sets TurnCompleted.WorkflowWillContinue when a completed turn has queued work following it - wiring/hostturn: post-turn HandleWorkflowTransition + enqueue + durable workflow_turn_started audit marker; TakeLearnNudge parity (agent accessor replicating the old RunTurn nudge computation) delivered as ephemeral learn_nudge - event: KindSessionNote (ephemeral) + SessionNote payload — in-turn progress notes (wfProgNote, handoff progress, policy notices) projected instead of dropped - projection: SysNoteMsg → ephemeral session_note - tests: workflow continuation (2 turns + audit marker + WorkflowWillContinue), no-workflow control, session-note projection
…on, snapshot versioning - facade: unique domain OpIDs for side questions (id.NewOpID generator added to core/id) with a facade-side OpID→CancelFunc registry; CancelSideQuestion looks up + cancels + removes; Close cancels all registered side questions (detached-job policy). Replaces the constant 'op_sq' pseudo-ID and no-op cancel stub. - facade: SetWorkflow converts WorkflowSnapshot → workflow.WorkflowState (phase-name→enum mapping, workflowPhaseFromName); nil still clears. - facade: snapshot revision counter — every facade-mediated mutation bumps Version so clients detect staleness; Snapshot.Title from Session.Label. - tests: unique OpIDs + registry lifecycle, SetWorkflow round-trip through Snapshot, version increments across mutators
…ine, hashed workspace IDs - agent: RunHandoffPipeline exported seam over performHandoff steps 1–4 (validation, old-session save, recency-split summary generation, session-history indexing with fallback chain, durable handoff record); HandoffResult carries what the wiring layer needs (payload, continuation prompt, note, chat IDs) - wiring: HandoffConversation now runs the real pipeline — saves old session, generates summary, creates new conversation seeded with the pinned handoff context (untrusted-delimiter framing), clears pending images, and enqueues the continuation turn via host SubmitInput when proceeding (host-enqueues policy; auto-start failure degrades to a startup note, not a failed rotation) - wiring: workspaceIDFromConfig derives wsp_<sha256[:8]> of the effective workdir — stable per workspace, valid ID grammar (the raw-path stub failed validation for empty workdirs) - tests: handoff seeding (pinned context, no image leak, new ChatID), empty- conversation guard
… SessionSummary.Turns - event payloads: SubagentSpawned carries Backend/Model/ToolNames; SubagentCompleted carries Err/CostUSD/FilesChanged/Grounding labels/ CtxSize/HardMaxBytes/UsedBackend; SubagentProgress carries structured early-finished fields (Finished/FinishedStatus/FinishedCostUSD/ FinishedFilesN) instead of a '[finished:status]' text marker; AsyncJobCompleted carries Err. Client tab/info-panel parity with the old Subagent*Msg/AsyncJobDoneMsg fields. - projection: maps the new fields through. - sessionclient: InfoSnapshot DTO (narrow, immutable, defensive copies) — endpoint/identity, model+backend selection, prompt/config bits, context gauge, transcript stats, workflow label, MCP servers, SearXNG tools, grounding entries, costs. Facade.Info() interface method; SessionSummary. Turns() mirrors agent.SessionTurns for the resume picker. - wiring: Info() implementation (mashuraPanelLabel moved from the TUI so the label is computed wiring-side); endpoints for completion. - tests: Info snapshot content + copy semantics.
… mapping
- DispatchCommand intercepts /handoff BEFORE agent.HandleTUICommand: arg
validation + quick-fail emptiness guards run at dispatch (fast), and the
result carries Rotate{Type:handoff, Proceed} WITHOUT executing the
summarizer pipeline — HandoffConversation runs it exactly once. Fixes the
double-pipeline (dispatch executing performHandoff AND the manager running
RunHandoffPipeline) and the event-loop blocking (120s summarizer) from the
synchronous cmd() call.
- Documented calling contract: slow commands (/handoff /remember /recall
/compact) execute synchronously inside DispatchCommand; the caller (TUI)
wraps the call in a worker goroutine — the AdaptCmd pattern.
- LearnTurnMsg → Submit 'learn this for next time' (the literal the old TUI's
LearnTurnMsg handler submitted via RunTurn), NOT '/learn' (infinite
redispatch). WFFinalReviewMsg → Submit 'continue' with rationale (the
adapter re-runs HandleFinalReview at turn end in verify state).
- tests: handoff deferral (no pipeline execution, proceed/stop/usage
variants), empty quick-fail, learn literal.
…e-owned event pump, BootstrapTUI
- hostturn: turnEmit field — the EventSink closure routes ToolStartMsg/
ToolResultMsg (turn-scoped kinds; the session emitter rejects them by
design) to the live turn's emitter, stamped with the turn ID; everything
else projects on the session surface. turnEmit set/cleared per turn.
- projection: ToolStartMsg → tool_call_started (ArgDigest = primary arg),
ToolResultMsg → tool_call_completed (Result preview) — previously dropped
with a wrong assumption ('the host emits them' — tools run inside the
agent loop; nothing emitted them). The TUI running-tool status line now
has a live wiring path.
- facade: Subscribe starts the facade-owned event pump (deliver callback =
tea.Program.Send); StartEventPump begins delivery when the caller is
ready. Facade interface updated accordingly.
- wiring: BootstrapTUI — manager + first conversation (fresh or --resume),
subscription armed, cleanup closure; the m4c main.go entry point.
- tests: tool events through a real turn (ordering: start < done < turn
completed; TurnID stamped), projection unit tests, BootstrapTUI fresh +
missing-resume.
…ura op-32 review) Wiring-side blockers and review findings fixed before the TUI cutover: - WithAsyncApproval enabled on manager-built conversations (B1): the sync confirmer with nil resolver declined every approval — an interactive TUI could never approve a tool. E2E test proves the park/ respond/complete cycle. - hostTurn EventSink closure bugs (op-32): captured the FIRST turn's TurnID forever (later turns' tool events stamped wrong) and read turnEmit without the mutex (data race). Both fields now written/read under ht.mu as a consistent pair. - interpretAgentMsg gaps (B2): BatchMsg recursion (/backend, /model — note + ctx-limit/model-list side effects now applied facade-side per D24 query-state), MCPReconnectedMsg applies rebuilt tools, ClipboardImageRequest sentinel → CommandResult.ClipboardImage, OpenResumePickerMsg → ResumePicker (was a bogus Rotate). - /new, /reset, /resume intercepted in DispatchCommand (B3): the agent path mutated the OLD App (NewConversation + finalizeSessionHistory). Rotation now classifies only; the manager owns finalize-on-rotation (FinalizeSessionHistory exported) and old-App freshness. - Facade.Close: CloseSession first (cancels in-flight turn → unblocks a PARKED approval — was a permanent goroutine leak on rotation), drain the pump (bounded), retry Release while the turn winds down. - bumpVersion AFTER mutation (was before — new version + stale data window defeated the staleness check). - BootstrapTUI subscribes at the durable HEAD, not seq 0 (replayed ApprovalRequested would pop dead confirm gates; replay duplicates hydrated snapshot state). - InfoSnapshot: InfoPanelOpen + OracleLabel 'no key' env fallback moved wiring-side. - ConversationManager.NewConversation gains a current-Facade param (rotation context for finalize). - TUI: tui_msgs.go + session_view.go migrated off agent format utils to internal/core/format (first two of nine files; stage 1 of the cut).
…ot/Info Model gains facade/manager/principal fields (wiring path), with the App kept for the legacy path until stage 3 (one read source per file: snapshot()/info() accessors return ok=false when facade is nil and callers fall back). Migrated files (wiring path reads facade; legacy path unchanged): - info_panel.go: infoMainSegments/infoSubSegments/infoToolsSegments/ infoGroundingSegments/costSegments/billedSegment/toggleInfoPanel — every App-internal read (Exec.Describe/Cwd, Cfg.Image/Oracle/SearXng, Client.BaseURL/ChatID/Grounding, MCP.Servers, Workflow.SidebarLabel, Costs) now comes from InfoSnapshot on the wiring path. OracleLabel 'no key' fallback + InfoPanelOpen are wiring-side (committed earlier). - tui_view.go: ctxSegment (limit/usage/transcript stats) and headerStatusInput (workflow label, backends, model/submodel, consent, rawTools) read Info()/Snapshot()/facade.Consent(). - complete.go: compSources() model method (backends/models/endpoints/ mentionBase via snapshot+Info); fetchSessionShortIDs via facade.ListSessions; computeCompletion/computeSlashCompletion take an injected session fetcher (stays a pure function). - resume_picker.go: SessionSummary DTO, facade.ListSessions reload, Turns() for row rendering; Enter routes through beginRotation on the wiring path (rotation scaffolding: rotationRequest/rotationMsg/ beginRotation — build-new-first, old facade closed after replacement exists, per the op-32 review); legacy path keeps agent resume. - NewTUIModelWithFacade: wiring-path constructor (nil-App base model, hydrates from snapshot.Conv + Info().InfoPanelOpen). tui_msgs.go + session_view.go were already migrated in stage 1 (a80666e).
…roval gate, send, rotation)
The TUI now runs end-to-end on the wiring path when facade-backed:
- tui_events.go: handleEventMsg switch on event.Kind — the D2 mapping.
TurnStarted (state machine for host-initiated turns), MessageDelta/
ReasoningDelta (stream accumulation, reasoning collapse), ToolCall-
Started/Completed (status-line indicators; ArgDigest=command),
ApprovalRequested/Resolved (async gate), TurnCompleted (finishWiring-
Turn: flush, classification by Outcome, deferred /auto grants, queue
flush gated on Outcome+WorkflowWillContinue, chime, clearWiringTurn-
State incl. pendApproval safety clear), SessionError, Subagent*
Spawned/Progress(Finished)/Completed, AsyncJob*, SideQuestion*,
SessionNote, LearnNudge, Workflow* (passive audit notes), Compacted,
UserMessage/MessageCommitted (replay truth — no live handling).
Session guard: cached m.sessionID drops stale-pump events.
- tui_event_tabs.go: tab lifecycle (spawn/complete/job) keyed by domain
IDs (sub_*/op_*), extracted as methods.
- Confirm gate: pendApproval{approvalID} + y/a/n/esc/ctrl+c → facade.
RespondToApproval (non-blocking buffered chan); ctrl+c also Interrupt;
already-resolved races tolerated.
- Send path: wiring branch submits via facade.SubmitInput (no RunTurn/
startTurn); optimistic streaming state confirmed by TurnStarted;
image chips reconcile against snapshot.PendingImages; mentions via
Info().MentionBase. flushQueuedPrompt mirrors it.
- Slash dispatch: /-prefixed input → facade.DispatchCommand in a Cmd
goroutine → commandResultMsg applied on the event loop (Notice/
Submit/Rotate→beginRotation/ResumePicker/ClipboardImage/Compacted).
Plain text no longer swallowed (slash-prefix guard).
- Rotation: beginRotation Cmd (manager op → close OLD facade after the
replacement exists) → applyRotation swaps refs, rebuilds view state,
THEN subscribes at the durable head + starts the pump (after the swap
— events before it would hit the session guard). SetProgramSend
installs tea.Program.Send without a package global in main.
- cancelTurn → facade.Interrupt; startSideQuestion → facade; Init
startup note via local startupNoteMsg; mid-turn /auto consent via
facade.
- E2E tests (fake facade): turn lifecycle, approval gate incl. ctrl+c,
session guard, send path, queue-flush-vs-workflow-continuation.
…n host cmd/wakil/main.go now runs the TUI on the wiring path (Gate #1, TUI half): wiring.BootstrapTUI builds the ConversationManager + first conversation (fresh or --resume), runs the TUI startup steps, and subscribes the event stream; tui.NewTUIModelWithFacade carries the facade/manager/principal; tui.SetProgramSend installs prog.Send and rt.StartEventPump begins delivery. globalProg and the app.EventSink=globalProg.Send line are gone — the pump is the only runtime event path. BootstrapTUI gains BootstrapTUIOpts carrying what main.go did inline: --attach-image (pending images), RestoreRepoState (fresh conversations only + ctx re-resolve, same caveat about literal restored strings), counsel mode/max defaults, and staging/memory startup-note composition. --resume/--resume-id resolution (workspace-scoped most-recent for bare --resume) moved into main.go's bootstrap prelude; the manager restores the transcript. main.go keeps exactly two agent imports: PrintSessions (--list-sessions short-circuit) and ShortID (diag log path) + LoadSessionScoped for bare --resume resolution — all three move behind wiring/agent wrappers in m4d's guard pass.
… internal/agent (Gate #1) The legacy App-backed path is deleted; the facade+event path is the only runtime path. Local TUI messages (dotTickMsg, armTickMsg, subTabCloseMsg, copiedMsg, clipboardImageMsg) moved from the deleted handleAgentMsg switch into handleEventMsg's local-message section. Removed: tui_agent_msgs.go (the 30-case agent-msg switch), adapter.go (AdaptCmd — no agent.Cmds remain), tuiModel's app/control/apply/pendConf fields, NewTUIModel(app) (newBaseModel + NewTUIModelWithFacade only), startTurn (submit path owns the state flip), legacy branches in info_panel/tui_view/complete/resume_picker (Snapshot/Info are the only read sources), startSideQuestion/cancelTurn legacy paths, the legacy converters. Tests (~30 files) migrated to the event switch: evt(kind,payload,sid) feeds, fakeFacade-backed models, domain-ID tab identities (sub_*/op_*), rotationMsg for rotation semantics, TurnCompleted outcomes for the AgentDoneMsg cases. Test helpers: fakeFacade (extended: consent mutators, SetInfoPanelOpen, StartSideQuestion), newWiringModel, rotatedFake, wiringTestInfo. Deleted: adapter_test, control_routing_test (seam deleted), control_seam_test (seam deleted). Agent-package behavior tests (cost recording, ctx-limit resolution, ProgWriter) keep their agent imports — they test agent functions and are exempt from the guard. Guard: TestNoAgentImport asserts go list -deps of internal/tui contains no internal/agent package — Gate #1's TUI half is now compiler+test enforced. main.go's residual agent uses (PrintSessions, ShortID, LoadSessionScoped for bare --resume) are the remaining cmd-side gap. Full suite + -race green: internal/... cmd/...
…m construction BootstrapTUI was called with deliver=nil (prog.Send does not exist at construction time), and Subscribe is gated on deliver != nil — so on first boot the facade never subscribed, StartEventPump was a no-op (no pump), and turn events never reached the TUI. The host still ran every submitted turn (billing the request), but MessageDelta/TurnCompleted were undelivered: the optimistic streaming state never cleared and the answer never rendered (the 'stuck streaming' live-test finding). Fix: TUIRuntime.SubscribeLive subscribes at the durable head once the tea.Program exists (mirroring the rotation path's lazy subscribe); main.go binds prog.Send as the deliver callback between SetProgramSend and StartEventPump. Regression test TestBootstrapTUISubscribeLive proves turn events flow through a nil-deliver bootstrap + manual subscribe.
…e, Docker hardening, rebrand
High-priority cleanup items from branch audit (Mashūra-reviewed):
1. ResetSessionBinding incomplete (hostturn.go): only cleared sessionID,
leaving sessionEmit, app.EventSink/OnTokRate callbacks, and declineLatch
pointing at the old session. Session B reused session A's emitter and
stale decline state. Fix: complete reset in safe order (callbacks
detached → sessionEmit → turnEmit → declineLatch → sessionID).
ResetSessionBinding now returns ErrTurnActive.
2. Data race on sessionEmit (hostturn.go): closures read ht.sessionEmit
without ht.mu. Detached work (D24-legal) could invoke EventSink/
OnTokRate concurrently with a reset. OnTokRate would nil-deref if
sessionEmit was cleared. Fix: closures lock under ht.mu + nil-check.
3. Compact RPC nil-Session guard (session_state_handler.go): SaveSession
nil-guard is intentional (tests/subagents). Fix at RPC boundary:
Compact returns "no active session" when app.Session == nil, mirroring
SetSessionLabel's existing guard pattern.
4. Web UI rebrand wakild→wakil (index.html, app.js, styles.css, embed.go).
5. Daemon agent name "wakild"→"wakil" (daemon_server.go).
6. Docker hardening (Dockerfile.daemon + docker-compose.yml):
- WAKIL_UID/GID now required (fail-fast, no silent 1000 default)
- WAKIL_WORKSPACE_PATH now required (no silent /workspace default)
- Created .env.example template
- Pinned golang:1.26.6-bookworm + GOTOOLCHAIN=local (was 1.26-bookworm
+ GOTOOLCHAIN=auto → non-reproducible network download)
- Added RUN mkdir -p for mount dirs (comment claimed they existed)
Verification: go build ✅ | go vet ✅ | go test -race ✅
…on→wakil_session, fix stale comments
Clean break on unmerged branch (Mashūra-unanimous decision D):
- Socket default: wakild.sock → wakil.sock (dialer.go, config.go, daemon.go,
all test fixtures, Dockerfile.daemon, docker-compose.yml)
- Cookie name: wakild_session → wakil_session (tokenresolver.go,
auth_handler.go, all test fixtures)
- Help text: "wakild daemon" → "wakil daemon" (config.go, main.go)
- Package comments: fix all current-tense "wakild" references to "wakil"
(event.go, service.go, dialer.go, peercred.go, principal.go,
jointoken.go, apitoken.go, migrate.go, auth_handler.go,
tokenresolver.go, daemon_server.go, daemon_signal.go)
- Historical comments ("previously a separate wakild binary") left as-is
Verification: go build ✅ | go vet ✅ | go test -race ✅
…RKSPACE_KEY override 1. Policy evaluation (hostturn.go): newHostConfirmer was missing the policy evaluation block that tuiConfirmer (commands.go:78-101) has. A policy.Deny could be bypassed by AutoApprove in daemon mode. Mashūra 3-panel review found this — the comment claimed mirrors
…o-state races Pre-merge fixes from Mashūra 3-panel audit (branch-audit.md): B1: Consent leak across sequential daemon sessions - NewConversation now resets consent (RevokeAuto + SetAllowReads(false)) - LoadSession handler also resets consent before restoring session B4: restoreDone guard was once-per-App-lifetime, blocking multi-session restore - Reset in InitNewSession and LoadSession (per-session, not per-App) - Added resetRestoreDone() method H1: /auto and endpoint-independent settings not restored on resume - Split RestoreRepoState into full (fresh) and RestoreRepoStateResume (resume) - Resume restores AutoApprove, RawTools, maxpar, maxctx, subagent, mashura - Skips model/backend (endpoint-dependent, unsafe mid-transcript) - New RestoreRepoStateResume RPC + proto message - Wired into both embedded (bootstrap_tui.go) and remote (bootstrap.go) paths B3: goMutation dropped errors and didn't refresh cached state - Now calls refreshState after RPC completion so TUI reconverges to truth H3: SetAllowDestructive check-then-act broke pair invariant - New EnableDestructiveIfAuto() atomic CAS mutator - SetAllowDestructive handler uses it instead of separate check + set H2: SetCounselMode didn't persist to RepoState (only mutator that didn't) - Added CounselMode + MaxCounsel fields to RepoState - Added SaveRepoState calls in SetCounselMode handler - Restore in restoreEndpointIndependent H4: updateRepoState read-modify-write not concurrency-safe - Added global sync.Mutex serializing all updateRepoState calls
… ordering, stale refresh Mashūra 3-panel review of a3f4800 found three bugs: H3: EnableDestructiveIfAuto returned true even when CAS failed - The 'enabled' flag was set inside the CAS callback but never reset on retry — a concurrent RevokeAuto could cause the function to report 'enabled' while the stored state had AllowDestructive=false - Fix: reset enabled=false at the top of every callback invocation B1/B4: LoadSession reset consent/binding before validating the session - A failed load (not found, corrupt) mutated the active session's consent and binding for nothing - Fix: load and validate the target session FIRST, then commit the transition (binding reset, consent reset, session install) B3: goMutation refreshState could overwrite newer state with stale data - Two concurrent refreshState calls could complete out of order - Fix: added a monotonic refreshSeq ticket — only the response with the highest ticket is installed, discarding stale overwrites Also: ClearRepoState now takes repoStateUpdateMu to prevent racing with concurrent updateRepoState calls.
…s out of hostTurn.mu B2/B5 Phase 1: Fix pre-existing callback races and lock-order violations identified by Mashūra 3-panel review. Changes: - Add callbackMu sync.Mutex to App for EventSink/OnTokRate synchronization. These callback fields are installed by the turn goroutine (hostTurn.run) and cleared by ResetSessionBinding (RPC goroutine in daemon mode). Without the mutex, the nil-clear races the read in sendEvent/streamSink. - Export SendEvent, SetEventSink, SetOnTokRate, ClearCallbacks so cross- package callers (wiring/hostturn) never directly access callback fields. - Replace direct app.EventSink reads in newHostConfirmer (policy deny/allow, auto-approve paths) and subagentProgressOut with app.SendEvent. - Move app.EventSink/OnTokRate = nil writes OUT of hostTurn.mu in ResetSessionBinding — they now go through ClearCallbacks under callbackMu. This resolves the lock-order violation (hostTurn.mu never nests App locks). - Add IsTurnActive() method to hostTurn (non-blocking probe under ht.mu). - Fix reset-order comment to match implementation (sessionEmit cleared first under ht.mu, then App callbacks via ClearCallbacks outside ht.mu). - Update tui_cmds.go to use SetOnTokRate instead of direct field write. Mashūra-reviewed: 3-panel review identified remaining EventSink race sites in newHostConfirmer and subagentProgressOut — all fixed via SendEvent. Verified: go build, go vet, go test -race (wiring, connect, remote) all pass. 3 pre-existing GOTMPDIR spill-path test failures in agent package unchanged.
…ionStateSnapshot, exported state methods - Add stateMu sync.RWMutex and saveMu sync.Mutex to App struct - Create app_state.go with TurnSettings and SessionStateSnapshot types - Add exported state setters: SetModelOverride, SetBackendSelection, SetRawToolsValue, SetCounselModeValue, SetSubagentEndpointOverride, SetSubagentModelOverride, SetMaxCtxOverride, SetMaxParallel, SetSessionLabelValue - Add exported locked getters for all stateMu-protected fields - Add SnapshotTurnSettings() and SnapshotSessionState() methods that gather coherent snapshots under stateMu.RLock - Update existing methods to participate in stateMu: - SetInfoPanelOpen: now acquires stateMu for field write - EffectiveModel: now acquires stateMu.RLock - EffectiveSubagentModel: snapshots SubagentEndpointOverride under lock - SetCtxLimit (control.go): now acquires stateMu - SetWorkflow (app_options.go): now acquires stateMu - Fix SubagentEndpointOverride/SetSubagentModelOverride to normalize "inherit" to "" before storing - SnapshotSessionState copies raw fields under lock, releases lock, then calls derived methods outside (no lock held across I/O/callbacks) - Workflow snapshot is a detached display label string, not a live pointer - Remove broken SetSubagentOverrides (ambiguous clear vs update semantics) Mashūra review addressed: lock-not-held-across-method-calls, Workflow ownership unified under stateMu, detached snapshot (no live pointers), existing methods participate in stateMu, SetSubagentOverrides removed, AuxModel centralized in snapshotTurnSettingsLocked helper.
Turn goroutine changes (B2/B5 fix): - prepareTurn: acquire stateMu.Lock for per-turn resets and Client.Model/ Backend/AuxModel writes — no I/O or callbacks under the lock - checkEgressConsent: TOCTOU fix — snapshot backend under stateMu.RLock before the blocking Confirm prompt; conditional revert under stateMu.Lock only if backend hasn't changed mid-prompt; honest decline message - streamTurn: RawTools is LIVE — re-read under stateMu.RLock per tool result in finalizeToolResult closure Compact/ctxlimit locked reads: - activeThresholds: snapshot CtxLimit under stateMu.RLock, compute outside - EffectiveCtxCap: read EffectiveCtxMaxCharsOverride under stateMu.RLock - ContextLimit(): read CtxLimit under stateMu.RLock Subagent dispatch locks: - resolveSubagentEndpointName: read SubagentEndpointOverride under stateMu.RLock - resolveSubagentEndpointView: snapshot Client fields and SubagentModelOverride under stateMu.RLock (inherit path); read SubagentModelOverride under stateMu.RLock (named-endpoint path) - resolveSubagentBackendForEndpoint: read SelectedBackend under stateMu.RLock - runSubagentJobs: read MaxParallelSubagents under stateMu.RLock (turn-stable per batch) - announceSubagentBlock: use MaxParallelLocked() for display ApplyModelOverride: - Acquire stateMu.Lock for Client.Model/ConfiguredModel, Cfg.Endpoint.Model, SelectedModel, defaultModel writes TUI command handlers — use exported locked methods: - /rawtools: SetRawToolsValue instead of direct field write - /backend (ilm-proxy): SetBackendSelection + SelectedBackendLocked/ SelectedModelLocked instead of direct field writes - /model display: SelectedModelLocked/EffectiveModelLocked - /subagent: SetSubagentEndpointOverride instead of direct field write - /submodel: SetSubagentModelOverride instead of direct field write - /maxpar: SetMaxParallel instead of direct field write - /maxctx: SetMaxCtxOverride instead of direct field write - /counsel: SetCounselModeValue/CounselModeLocked/MaxCounselLocked - /session name: SetSessionLabelValue instead of direct field write + SaveSession - handleEndpointSwitch: all Client.* and Cfg.Endpoint writes under stateMu.Lock Mashura review addressed: - checkEgressConsent decline message now honestly reports whether the revert actually happened (backend may have changed during the prompt) - All TUI command handlers that write stateMu-protected fields now use the exported locked methods, preventing races with the turn goroutine Verified: go build, go vet, go test (wiring, connect, repostate tests pass; 3 pre-existing GOTMPDIR spill-path failures unchanged).
…ot + serialized write SaveSession now acquires saveMu BEFORE snapshotting (not after), preventing an older snapshot from overwriting a newer one when two saves overlap. The snapshot uses nested locks (stateMu → convMu) to get a coherent view of Session + Conv, deep-copies the Conv slice for detachment, then releases both locks before WriteSession marshals the snapshot. Lock ordering updated: saveMu → stateMu → convMu. saveMu is only acquired in SaveSession, so no deadlock is possible. No caller of SaveSession holds stateMu or convMu. SetSessionLabel handler in session_state_handler.go now uses SetSessionLabelValue (which writes under stateMu.Lock) instead of writing app.Session.Label directly. Addresses Mashūra review: stale-snapshot-overwrite fixed by acquiring saveMu before snapshotting.
…Coordinator Add TransitionCoordinator (internal/wiring/coordinator.go) that serializes session transitions (LoadSession, InitNewSession) and idle maintenance (Compact) against turn starts. The coordinator uses a turnActive flag set by WithTurnStart and cleared by ClearTurnActive when the turn ends; the coordinator lock is NOT held during the turn body (avoids deadlock risk from synchronous continuation). Transitions and Compact reject with ErrTurnActiveCoord if a turn is active. Add InstallSession and NewConversationTransition methods to App — atomic locked replacements for the individual field writes in LoadSession and InitNewSession handlers. Both use stateMu.Lock → convMu.Lock (consistent ordering). Wire LoadSession, InitNewSession, and Compact handlers through the coordinator. LoadSession now calls InstallSession (replaces individual SetConv/Session/SetWorkflow writes). InitNewSession calls NewConversationTransition. Compact moves Session nil-check, Compact call, and SaveSession inside the coordinated block. Wire hostTurn.run through coordinator.WithTurnStart — claim+activate is atomic under the coordinator lock. ClearTurnActive is called in the defer when the turn ends. Wire the coordinator in daemon_server.go — created once, shared between hostTurn (via WithCoordinator option) and both SessionStateHandler instances (Unix + TCP). Addresses Mashūra review: - Coordinator now uses turnActive flag (not just lock during claim) — transitions and Compact reject during a turn instead of racing - Compact moves Session nil-check and SaveSession inside coordinated block - ErrTurnActiveCoord mapped to CodeFailedPrecondition in handlers
… methods GetSessionState now uses SnapshotSessionState() for one coherent read under stateMu.RLock instead of ~40 direct field reads. SetModel routes through coordinator.WithTransition (always, not just OpenAI kind — avoids TOCTOU race on endpoint kind). SetBackend uses SetBackendSelection + locked getters. SetRawTools, SetCounselMode, SetSubagentEndpoint, SetSubagentModel, SetEffectiveCtxMax, SetMaxParallelSubagents all use exported locked setters instead of direct field writes. SnapshotSessionState fixes (from Mashūra review): - ChatID fallback to Client.ChatID when Session is nil or ChatID empty - Deep-copy BackendInfo.Caps slices under lock (was shallow copy) - WorkflowLabel computed under lock (SidebarLabel is pure, no I/O) - Removed 6 unused nil-safety helper functions (appChatID, etc.) RestoreRepoState/Resume direct app.CtxLimit write deferred to Phase 7.
…e/apply RestoreRepoState/Resume split into three phases: - Read (RestoreRepoStateRead): pure disk I/O, no lock - Resolve: network probe for context limits (no lock) - Apply (RestoreRepoStateApply/ResumeApply): all App field writes under stateMu.Lock Key fixes from Mashūra review: - Context limits resolved AFTER Apply using result.Model/result.Backend (literal strings actually applied), not raw RepoState values that may have been skipped by eligibility guards (endpoint mismatch, ModelExplicit) - CtxLimit installed via SetCtxLimit (atomic, resets CtxPressureWarned) instead of direct app.CtxLimit write - Nil guards on exported Apply functions - restoreDone committed after disk read succeeds (missing file no longer consumes the one-shot guard) - Comment corrected: guard is per-session-generation, not per-App-lifetime restoreEndpointIndependent renamed to restoreEndpointIndependentLocked (method on App, caller must hold stateMu). All Cfg field writes (MaxParallelSubagents, MashuraPanels, etc.) now under stateMu.Lock. Backward-compatible wrappers (RestoreRepoState/RestoreRepoStateResume) preserved for existing callers — they use Read+Apply without ctxLimit.
…in SnapshotSessionState New file internal/agent/daemon_race_test.go with 11 race-detector tests covering concurrent setter vs snapshot/getter patterns: - SetModel vs SnapshotSessionState - SetBackend vs SnapshotSessionState + locked getters - SetRawTools vs SnapshotSessionState + locked getter - SetCounselMode vs SnapshotSessionState + locked getters - SetMaxParallel vs SnapshotSessionState + locked getter - SetMaxCtx vs activeThresholds (CtxLimit race) - SetSubagentOverrides vs SnapshotSessionState + locked getters - SaveSession vs SetSessionLabel (serialized via saveMu) - SetCtxLimit vs activeThresholds - SnapshotSessionState vs multiple setters concurrently - RestoreRepoStateApply vs SnapshotSessionState Real bug found and fixed by the race detector: SnapshotSessionState called SessionWorkspace() outside stateMu.RLock. SessionWorkspace calls Cfg.WorkspacePath(), which has a VALUE receiver (func (c Config) WorkspacePath()) — the value receiver copies the entire Config struct on the call, reading every field. Those fields are written under stateMu by restoreEndpointIndependentLocked (OracleModel, MaxParallelSubagents, MashuraPanels, etc.). The race detector flagged the copy's read of Cfg fields vs concurrent writes under stateMu.Lock. Fix: call SessionWorkspace() under stateMu.RLock (before RUnlock). WorkspacePath() only reads os.Getenv + struct fields — no locks, no I/O, no callbacks — safe under stateMu.RLock.
…tings on /new and /handoff rotation Root cause: conversation_manager.go's newConversation builds a fresh App via BuildApp (initializes from cfg.AutoApprove), but RestoreRepoState was only called from BootstrapTUI for the first conversation — rotation paths never restored from disk. Additionally, AutoExplicit (set by --auto CLI) caused the restore guard to skip AutoApprove restore, and Config is a value type so clearing app.Cfg.AutoExplicit didn't propagate to cm.cfg. Phase 1: Restore RepoState on rotation + transfer AllowDestructive for proceed - Add restoreRepoState(ctx, app, resume) helper on conversationManager - Add transferDestructive(oldConsent, newApp) — AllowDestructive only, in-memory, proceed only. Checks both old consent (had the grant) AND new app's AutoApprove (auto is on in new session) to prevent AutoApprove=false + AllowDestructive=true. - Add appendStartupNote(app, note) helper for consistent note composition - Call restoreRepoState from NewConversation (resume=false), HandoffConversation (resume=false), ResumeConversation (resume=true) - Call transferDestructive from HandoffConversation when proceed=true (snapshot old consent BEFORE RunHandoffPipeline) - Remove duplicate restore from bootstrap_tui.go (was only for first boot) - Add nil validation to HandoffConversation facade check Phase 2: Fix AutoExplicit/ModelExplicit inversion - Add process-local autoUserOverridden and modelUserOverridden flags to conversationManager (NOT RepoState — avoids cross-restart precedence inversion where --auto would be defeated on future process starts) - Add SetAutoUserOverridden/SetModelUserOverridden to ConversationManager interface - Add OnAutoToggled/OnModelToggled callback fields on App - Wire callbacks in newConversation: app.OnAutoToggled = cm.SetAutoUserOverridden - Call callbacks from commands.go /auto, /model, /backend handlers - In restoreRepoState, clear AutoExplicit/ModelExplicit on the App's config copy when the override flag is set, so the restore guard fires Phase 3: Fix /counsel not persisted from TUI - Add saveRepoState to /counsel handler in commands.go (auto, suggest, off cases) Phase 4: Fix InfoPanelOpen not restored on rotation - applyRotation in tui_wiring_loop.go reads Info().InfoPanelOpen from the new facade and restores m.infoPanel.active, with layout reflow on change Phase 5: Fix Session.Model on resume + add Session.EndpointName - Add EndpointName field to Session struct (json endpoint_name,omitempty) - Set EndpointName in NewConversation and NewConversationTransition - Fix SaveSession to derive snap.Model from locked fields (SelectedModel or Client.Model) instead of stale Session.Model — avoids EffectiveModel() which would deadlock (stateMu.RLock is NOT reentrant) - Fix ApplyModelOverride to update Session.Model when Session is non-nil - In ResumeConversation, apply s.Model on resume with endpoint-match guard: skip if --model CLI (ModelExplicit), skip if endpoint mismatch, apply for legacy sessions (no EndpointName) — backward compatible Also includes: subagent event routing fix in hostturn.go (route SubagentStartMsg/SubagentDoneMsg to turn emitter alongside ToolStart/Result). Mashūra review: Phase 1 reviewed (1 round). Findings folded in: - transferDestructive now checks new app's AutoApprove (not just old consent) - Nil validation added to HandoffConversation - appendStartupNote used in bootstrap_tui.go staging/memory composition - BootstrapTUIOpts.RestoreRepoState comment updated to reflect no-op behavior Trello: https://trello.com/c/kIiHFTZL/151
The LoadRepoState doc comment said it checked workspace mismatch after re-resolution, but the code never did. Add the check: compare workspaceKey(st.Workspace) against workspaceKey(ws), treating an empty stored Workspace as backward-compatible (pre-dating workspace recording). A mismatched file is treated as absent (nil, nil) — a stale file from one workspace must never silently apply to another (e.g. after a path rename or a copied state directory). Skipped: displaying process-local override flags in /repostate (not worth the bridge from the wiring facade to the manager — they're internal state). Skipped: stale 'wakild' comment references (they're accurate references to the design doc docs/design/wakild-foundation.md, not stale code). Trello: https://trello.com/c/kIiHFTZL/151
Mashūra final review findings: 1. Data race: autoUserOverridden/modelUserOverridden were plain bools written from command goroutines and read from rotation goroutines. Changed to atomic.Bool with Store/Load. 2. Ordering: /auto callback fired before the consent change was applied (SetAutoApprove/RevokeAuto), contradicting the comment that says 'after the toggle is applied'. Reordered: persist → apply consent → notify callback, for both ON and OFF paths. Trello: https://trello.com/c/kIiHFTZL/151
run_background jobs were poll-by-design: the agent had to call read_process_log in a loop to detect completion, each iteration a full model round-trip — extremely expensive in tokens. This adds an opt-in notify_on_exit parameter (default false, current behavior preserved). When true: - The job registers as pending async work via registerAsyncOp - isIdle returns true while the job runs → turn Suspends - On process exit, the reaper publishes through publishAsyncOp - The suspended turn resumes automatically with the result — zero polling For servers/daemons that never exit, notify_on_exit stays false (default) and the existing poll-by-design behavior is unchanged. Implementation (Mashūra-reviewed, 3-panel): - Generalized notifyDetachedShellExit: uses e.toolName and e.originChatID instead of hardcoded "run_shell" and missing originChatID (existing bug) - publishBgCompletion: finalizes the async op via publishAsyncOp for proper slot accounting (asyncActive decrement + inbox append + wake) - cancelBgAsyncOp: releases the async slot silently (no inbox entry) on kill/shutdown/generation-loss — prevents permanent asyncActive leak (P0 issue caught by Mashūra: kill_process cleared notifyOnExit but never decremented asyncActive → isIdle stuck true forever) - All exit paths in kill_process and read_process_log now release the async slot when entry.asyncOp != nil - StopAllBackgroundProcs releases slots on shutdown - agent.txt companion edit: documents notify_on_exit so the model uses it instead of polling Tests: 5 new tests covering registration, default no-registration, fast-exit publish, kill suppression + slot release, and result text. All existing bg/async/idle tests pass with no regressions. Trello: https://trello.com/c/5edIKeZ6
Three targeted edits to prompts/agent.txt to reduce per-task token overhead without weakening safety or correctness: 1. Skills mandate (HIGH): gate on unfamiliarity, not every task. Removed "Don't decide whether a skill is needed" (contradicted the skip rule). Old: list_skills before every non-trivial task (1-2 calls/task). New: check skills only when task plausibly matches a reusable reference. 2. Verification calibration (MEDIUM): "strongest *cheap* check" (was "strongest available check") + "after a batch, narrowest check" (was "after each logical edit, re-read"). Invariant preserved: "An unverified edit is not a completed edit." 3. Full-page fetch (MEDIUM): conditional on snippet sufficiency (was unconditional). For API/version lookups, the snippet often suffices. NOT touched: secrets rules, destructive gates, honesty invariants, read-before-edit, shell gating. Mashūra-reviewed (3-panel consensus on all 3 edits). Trello: https://trello.com/c/2qqlHP10
Phase 3 lower-priority edits to prompts/agent.txt: 1. Memory search (LOW): scope to when startup digest or task wording suggests relevance — not a generic search at every task start. 2. Subagent tripwire (LOW): raise from 3 to 5 gathering calls before mandatory delegation. 3 was too aggressive for focused single-module investigations where a 4th inline read is cheaper than a full subagent. 3. Prompt dedup (LOW): consolidate async placeholder/envelope prose. Mashūra and Subagents sections now reference ## Async execution (the canonical section) instead of repeating the full mechanics. Saves ~4 lines of per-request system prompt. NOT touched: secrets rules, destructive gates, honesty invariants, exactly-once async warnings. Mashūra-reviewed (3-panel). Trello: https://trello.com/c/3sN4uxsL
Replace 'Delegation is cheap for YOUR action/context budget' with an honest cost tradeoff: 'trades parent context for an additional model invocation... but the child's reads are billed tokens. Use it when a concise child digest is likely to avoid more total work than it creates.' The old framing conflated parent context with total billed tokens, over-incentivizing dispatch even when a few inline reads would cost less total tokens. ## Subagents section (tripwire, parallel objectives, etc.) unchanged — only the Role section framing is edited. Mashūra-reviewed (3-panel, issue G). Trello: https://trello.com/c/1ollmRWE
Three fixes from branch sweep analysis: 1. Fix 3 failing spill tests (HIGH, regression from 6664092): TestSubagentSummaryWrittenToDisk, TestDispatchSubagentErrorFlushesPartialTranscript, and TestDispatchSubagentErrorSpillWriteFailure failed because TestMain sets WAKIL_SESSIONS_DIR globally, and toolCacheBase() checks it before XDG_DATA_HOME. Each test now clears WAKIL_SESSIONS_DIR with t.Setenv so XDG_DATA_HOME controls the cache dir as intended. Full agent test suite passes (37s, -race). 2. Add .env and .tmp-test/ to .gitignore (MEDIUM): .env could hold secrets if a user copies .env.example. .tmp-test/ is 99MB of test artifacts. Also add b2-b5-plan*.md and branch-audit.md to .gitignore. 3. Remove committed plan/audit files from repo root (MEDIUM): b2-b5-plan.md, b2-b5-plan-v2.md, b2-b5-plan-v3.md, branch-audit.md were committed in phase commits (748b9cd, a3f4800). They are session artifacts that belong in .tmp/ or .wakil/ (gitignored), not tracked in the repo root. Trello: https://trello.com/c/wbLOSygb, https://trello.com/c/o3ljo8vv, https://trello.com/c/KibgHay7
Dockerfile.daemon used unpinned base images (golang:1.26.6-bookworm and gcr.io/distroless/cc-debian12:nonroot) while Dockerfile carefully pins all base images with @sha256: digests. This breaks the supply-chain posture. Now both base images are digest-pinned: - golang:1.26.6-bookworm@sha256:116d58cbd88c... - gcr.io/distroless/cc-debian12:nonroot@sha256:9dac0a79194e... Note: Dockerfile's golang:1.26-bookworm digest (1ecb7edf...) may be stale — the tag currently resolves to e8c859f... . That's a separate concern. Trello: https://trello.com/c/DgUgm0Sh
goMutation dropped mutation RPC errors with _ = call(...). SetAutoApprove, SetAllowDestructive, RevokeAuto, and SaveRepoState all use this path — if the daemon rejected or timed out, the user got no feedback. Now logs the error (with operation name) to stderr. The subsequent refreshState still reconverges to daemon truth regardless. Mashūra reviewed the plan and recommended a TUI notice callback instead of stderr. That would require a deeper architectural change (callback from background goroutine into the TUI) which is beyond this card's scope. The broader concerns (mutation ordering, lifecycle cancellation) are noted as scope-expansion for future work. Trello: https://trello.com/c/fHqzPNfN
The remote facade has several no-op stubs (SetWorkflow, SaveSession, SetCtxLimit, SetModelList, SetTools, PendingImages, SideQuestion, CompletionSource.Sessions) that silently discard data. They are intentional P2e limitations — the daemon owns these state fields and the remote TUI reads them from the cached SessionState instead of setting them locally. Added doc comments explaining WHY each is a no-op and what the alternative path is. No behavior change — purely documentation. Trello: https://trello.com/c/DJcnF2ri
Two critical packages had zero test files:
internal/protoconv/ — the 32-kind domain↔proto event conversion shared by
the Connect server and remote client. Added event_conv_test.go with:
- TestEventRoundTrip: 7 representative event kinds (SessionCreated,
TurnStarted, MessageCommitted, ToolCallCompleted, SubagentCompleted,
TurnCompleted, WorkflowOutcome) with full field verification.
- TestAllKindsRoundTrip: iterates all 32 event kinds, verifying
EventToProto→EventFromProto works (catches missing switch cases).
- TestEventFromProtoNilTimestamp, TestPayloadToProtoUnknownKind,
TestPayloadFromProtoNil, TestPayloadFromProtoMismatchedKind: edge cases.
- TestSessionRoundTrip, TestSessionRoundTripZeroTimes: Session conversion
including zero-timestamp omission.
internal/auth/tokenstore/ — the 548-line token store (join tokens, web
sessions, API tokens, memberships, OIDC users). Added tokenstore_test.go
with 16 tests covering:
- Join tokens: create, list (cross-tenant isolation), revoke (idempotent,
wrong-tenant NotFound), consume (double-use, expired).
- Web sessions: create+lookup, expired (absolute+idle), revoke by hash,
touch (sliding window).
- API tokens: create+lookup, expired, no-expiry, revoke (idempotent,
wrong-tenant), list (user filter, includeRevoked).
- Memberships: lookup (found + NotFound), check user active.
All tests pass with -race.
Trello: https://trello.com/c/vd1UD9vR
…ates
Four low-priority fixes from the branch sweep:
1. wiringFacade.Snapshot() sessionID lock (correctness):
Read f.sessionID while holding f.mu instead of after unlocking. Fixes
a technical data race (safe in practice — set once before facade is
returned — but correct by Go's memory model).
2. Web UI esc() attribute escaping (security defense-in-depth):
esc() escapes <, >, & but not " or ' — unsafe inside double-quoted
HTML attributes (data-backend-delete, data-ws-delete, data-agent-delete).
Added escAttr() that also escapes quotes. Also escaped s.state in
CSS class attribute.
3. Stale wakild references in Go comments:
Updated 3 references from docs/design/wakild-foundation.md to
docs/design/wakil-foundation.md in internal/core. Historical comments
in cmd/wakil ("previously a separate wakild binary") left as-is.
4. Coverage gates for protoconv and tokenstore:
Added floors: protoconv 96.0% (measured 97.0%), tokenstore 70.0%
(measured 71.4%). Added TODO for ungated damage-critical packages
(server/*, remote, auth/*) that still need test files.
Trello: https://trello.com/c/71PVenzk, https://trello.com/c/R897MA4B,
https://trello.com/c/GgeRwgYH, https://trello.com/c/ZLwdeEzx
…ates
Four low-priority fixes from the branch sweep:
1. wiringFacade.Snapshot() sessionID lock (correctness):
Read f.sessionID while holding f.mu instead of after unlocking. Fixes
a technical data race (safe in practice — set once before facade is
returned — but correct by Go's memory model).
2. Web UI esc() attribute escaping (security defense-in-depth):
esc() escapes <, >, & but not " or ' — unsafe inside double-quoted
HTML attributes (data-backend-delete, data-ws-delete, data-agent-delete).
Added escAttr() that also escapes quotes. Also escaped s.state in
CSS class attribute.
3. Stale wakild references in Go comments:
Updated 3 references from docs/design/wakild-foundation.md to
docs/design/wakil-foundation.md in internal/core. Historical comments
in cmd/wakil ("previously a separate wakild binary") left as-is.
4. Coverage gates for protoconv and tokenstore:
Added floors: protoconv 96.0% (measured 97.0%), tokenstore 70.0%
(measured 71.4%). Added TODO for ungated damage-critical packages
(server/*, remote, auth/*) that still need test files.
Trello: https://trello.com/c/71PVenzk, https://trello.com/c/R897MA4B,
https://trello.com/c/GgeRwgYH, https://trello.com/c/ZLwdeEzx
… layout The README was stale — it described wakil as a single-process TUI-only tool, with no mention of the daemon architecture, web console, Connect/gRPC API, or the many new internal packages added in the feature/wakild-daemon branch. Changes: - Add 'Daemon mode' section explaining the daemon subcommand, Unix socket/TCP serving, multi-client architecture, and docker-compose deployment. - Add 'Web console' subsection covering the built-in browser UI, auth, and TLS. - Update project layout to include all new packages: api/, auth/, browser/, core/, crypto/, diag/, policy/, protoconv/, remote/, safe/, scrub/, server/connect/, sessionhistory/, store/, verify/, wiring/, web/, Dockerfile.daemon, docker-compose.yml. - Add daemon security features to the Security section (session cookies, origin validation, TLS, API tokens, OIDC, credential encryption). - Add docs/design/wakil-foundation.md to the documentation table.
gofmt flagged 4 files we touched: - internal/auth/tokenstore/tokenstore_test.go (spacing + trailing newline) - internal/protoconv/event_conv_test.go (map alignment) - internal/remote/facade.go (blank lines after function) - internal/wiring/facade.go (struct field alignment) Pre-existing gofmt issues in internal/agent/ and internal/core/event/ are not fixed here — they predate this branch.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This branch adds the wakil daemon architecture (Connect/gRPC API, web console, multi-client support), fixes session handoff/rotation bugs, and includes a full branch sweep with cleanup and new test
coverage.
Major changes
Daemon mode (card #148–#150)
wakilbinary withwakil daemonsubcommand (merged from separatewakild)api/proto/wakil/v1alpha1) for session, event, auth, backend, workspace, agent servicesDockerfile.daemon(distroless, non-root) +docker-compose.ymlfor containerized deploymentHandoff/rotation fixes (plan v4, Mashūra-reviewed)
/auto,/auto destructive,/model,/counsel,/infoon/newand/handoffrotationBranch sweep cleanup (this session)
.envand.tmp-test/to.gitignore, remove committed plan/audit fileswakildreferences in commentsNew test coverage
internal/protoconv/— 32-kind domain↔proto event conversion (97% coverage, was 0%)internal/auth/tokenstore/— join tokens, web sessions, API tokens, memberships (71.4% coverage, was 0%)Verification
go build ./...✅go vet ./...✅go test -race -count=1 ./...✅ (43 packages, 0 failures)gofmt✅ (on changed files)