Skip to content

Feature/wakild daemon - #12

Merged
treeol merged 96 commits into
masterfrom
feature/wakild-daemon
Aug 26, 2026
Merged

Feature/wakild daemon#12
treeol merged 96 commits into
masterfrom
feature/wakild-daemon

Conversation

@treeol

@treeol treeol commented Aug 26, 2026

Copy link
Copy Markdown
Owner

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)

  • Single wakil binary with wakil daemon subcommand (merged from separate wakild)
  • Connect/gRPC API (api/proto/wakil/v1alpha1) for session, event, auth, backend, workspace, agent services
  • Built-in web console (vanilla JS) with session-cookie auth, origin validation, optional TLS/OIDC
  • Dockerfile.daemon (distroless, non-root) + docker-compose.yml for containerized deployment
  • Session state service for slash-command dispatch over RPC
  • Credential encryption at rest (AES-256-GCM envelope encryption)

Handoff/rotation fixes (plan v4, Mashūra-reviewed)

  • Restore /auto, /auto destructive, /model, /counsel, /info on /new and /handoff rotation
  • Fix AutoExplicit/ModelExplicit inversion (override flags now process-local)
  • Persist InfoPanelOpen across rotation
  • Fix Session.Model on resume + add Session.EndpointName
  • Split RestoreRepoState into read/resolve/apply for daemon use
  • Race condition fixes (SessionWorkspace, CAS return bug, transition serialization)

Branch sweep cleanup (this session)

  • Fix 3 failing spill tests (WAKIL_SESSIONS_DIR vs XDG_DATA_HOME regression)
  • Digest-pin Dockerfile.daemon base images
  • Surface goMutation RPC errors via diag instead of swallowing
  • Document remote facade no-op stubs as intentional P2e limitations
  • Add .env and .tmp-test/ to .gitignore, remove committed plan/audit files
  • Fix wiringFacade.Snapshot() sessionID lock (data race)
  • Fix web UI esc() attribute escaping (XSS defense-in-depth)
  • Update stale wakild references in comments
  • Add coverage gates for protoconv (97%) and tokenstore (71.4%)
  • Update README for daemon mode, web console, and current project layout

New 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%)
  • Race tests for daemon state methods (SnapshotSessionState, consent, settings)

Verification

  • go build ./...
  • go vet ./...
  • go test -race -count=1 ./... ✅ (43 packages, 0 failures)
  • gofmt ✅ (on changed files)
  • Coverage floors ✅ (protoconv 97% / floor 96%, tokenstore 71.4% / floor 70%)

treeol added 30 commits August 19, 2026 12:52
…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.
treeol added 29 commits August 23, 2026 06:47
…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.
@treeol
treeol merged commit 96e93a8 into master Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant