DurableSink resumed the parent through ChatHub::resume(&source), which
resolves whatever session the source currently points at. Since one
source can now carry several conversations (secondary tabs, a reset
since the task started), that pointer is no longer the conversation the
result was delivered into: the recovery ran on the wrong one, found
nothing pending, and returned silently — the delivered task_completed
sat unread until the user's next message drove a normal turn.
The sink already knows the parent session id, so resume it directly
through resume_for_session, keeping the same in-flight guard. The
source lookup and the now-unused pool field go with it.
Adds a crate-level regression test: a completed turn, a StoreSink
delivery, then a recovery — the result must drive a new round.
The session handler is now a thin shell: three entry points in
kernel_turn.rs (run_kernel_turn / recover_turn / resolve_pending_call)
and the ChatSessionHandler. Everything that shaped a Value — projection,
recovery, compaction mechanics, the LLM loop, message building — lives
in agent-loop or behind a loop_adapters trait.
agent-loop:
- projection/ (mod + media): stored history -> wire messages, the one
place provider divergence lives; well-formedness contract, DTL
injections (append-only), media parts. LinearAssembler is now a
Projection + ProjectionHooks config, not its own implementation
- recovery.rs: reap interrupted batches -> resolve the deepest frame's
non-terminal calls (Running by policy + RestartHint, AwaitingHuman
re-asked) -> un-wedge finished children -> cascade up, every frame on
its own agent (B3)
- compaction.rs: split point (never assistant+tool group), transcript,
SUMMARY_PREFIX/preamble/template, the no-tools model call, summary row
- manager: resolve_pending (gate skipped, real ToolContext, then
continue incl. sub-agent); start_loop used by recovery; LiveInput
- delegate: AsyncExecutor + StoreSink for mode:async (durable cron row,
result delivered back into the parent conversation)
- kernel/context/store: support the above (TurnScope via Extensions,
frame lookups, aligned result-text semantics)
skald-core:
- loop_adapters: UserLoopRuntime (D12 - one LoopManager per user),
TurnScope (per-turn state in the Extensions type-map; no scope is
denied), projection_cfg/media_source/tool_digest (Skald's projection
knobs without owning projection code), async_task (CronExecutor +
DurableSink)
- session/handler: stripped to mod.rs + kernel_turn.rs + config.rs +
interface_tools.rs + media.rs; deleted agent_dispatch, approval,
dispatch, emitter, gate, llm_call, llm_loop, message_builder,
messages, outcome, resume
- compactor.rs: policy only (threshold, model pick, CompactionEvent);
mechanics are the crate's
CLAUDE.md updated (recovery, compaction, sub-agents, approval gate,
projection sections now describe the crate-owned flow).