Files
loki/docs/implementation/PHASE-1-STEP-5-NOTES.md
2026-04-10 15:45:51 -06:00

15 KiB
Raw Blame History

Phase 1 Step 5 — Implementation Notes

Status

Done.

Plan reference

  • Plan: docs/PHASE-1-IMPLEMENTATION-PLAN.md
  • Section: "Step 5: Migrate request-read methods to RequestContext"

Summary

Added 13 of 15 planned request-read methods to RequestContext as inherent methods, duplicating the bodies that still exist on Config. The other 2 methods (info, session_info) were deferred to Step 7 because they mix runtime reads with calls into AppConfig-scoped helpers (sysinfo, render_options) or depend on sysinfo which itself touches both serialized and runtime state.

Same duplication pattern as Steps 3 and 4: callers stay on Config during the bridge window; real caller migration happens organically in Steps 8-9.

What was changed

Modified files

  • src/config/request_context.rs — extended the imports with 11 new symbols from super (parent module constants, StateFlags, RoleLike, paths) plus anyhow, env, PathBuf, get_env_name, and list_file_names. Added a new impl RequestContext block with 13 methods under #[allow(dead_code)]:

    Path helpers (4):

    • messages_file(&self) -> PathBuf — agent-aware path to the messages log
    • sessions_dir(&self) -> PathBuf — agent-aware sessions directory
    • session_file(&self, name) -> PathBuf — combines sessions_dir with a session name
    • rag_file(&self, name) -> PathBuf — agent-aware RAG file path

    State query (1):

    • state(&self) -> StateFlags — returns bitflags for which scopes are currently active

    Scope info getters (4):

    • role_info(&self) -> Result<String> — exports the current role (from session or standalone)
    • agent_info(&self) -> Result<String> — exports the current agent
    • agent_banner(&self) -> Result<String> — returns the agent's conversation starter banner
    • rag_info(&self) -> Result<String> — exports the current RAG

    Session listings (2):

    • list_sessions(&self) -> Vec<String>
    • list_autoname_sessions(&self) -> Vec<String>

    Misc (2):

    • is_compressing_session(&self) -> bool
    • role_like_mut(&mut self) -> Option<&mut dyn RoleLike> — returns the currently-active RoleLike (session > agent > role), the foundation for Step 7's set_* methods

    All bodies are copy-pasted verbatim from the originals on Config, with the following minor adjustments for the new module location:

    • Constants like MESSAGES_FILE_NAME, AGENTS_DIR_NAME, SESSIONS_DIR_NAME imported from super::
    • paths:: calls unchanged (already in the right module from Step 2)
    • list_file_names imported from crate::utils::* → made explicit
    • get_env_name imported from crate::utils::* → made explicit

Unchanged files

  • src/config/mod.rs — the original Config versions of all 13 methods are deliberately left intact. They continue to work for every existing caller. They get deleted in Step 10 when Config is removed entirely.
  • All external callers of config.messages_file(), config.state(), etc. — also unchanged.

Key decisions

1. Only 13 of 15 methods migrated

The plan's Step 5 table listed 15 methods. After reading each body, I classified them:

Method Classification Action
state Pure runtime-read Migrated
messages_file Pure runtime-read Migrated
sessions_dir Pure runtime-read Migrated
session_file Pure runtime-read Migrated
rag_file Pure runtime-read Migrated
role_info Pure runtime-read Migrated
agent_info Pure runtime-read Migrated
agent_banner Pure runtime-read Migrated
rag_info Pure runtime-read Migrated
list_sessions Pure runtime-read Migrated
list_autoname_sessions Pure runtime-read Migrated
is_compressing_session Pure runtime-read Migrated
role_like_mut Pure runtime-read (returns &mut dyn RoleLike) Migrated
info Delegates to sysinfo (mixed) Deferred to Step 7
session_info Calls render_options (AppConfig) + runtime Deferred to Step 7

See "Deviations from plan" for detail.

2. Same duplication pattern as Steps 3 and 4

Callers hold Config, not RequestContext. Same constraints apply:

  • Giving callers a RequestContext requires either: (a) a sync'd Arc<RequestContext> field on Config — breaks because per-request state mutates constantly, (b) cloning on every call — expensive, or (c) duplicating method bodies.
  • Option (c) is the same choice Steps 3 and 4 made.
  • The duplication is 13 methods (~170 lines total) that auto-delete in Step 10.

3. role_like_mut is particularly important for Step 7

I want to flag this one: role_like_mut(&mut self) is the foundation for every set_* method in Step 7 (set_temperature, set_top_p, set_model, etc.). Those methods all follow the pattern:

fn set_something(&mut self, value: Option<T>) {
    if let Some(role_like) = self.role_like_mut() {
        role_like.set_something(value);
    } else {
        self.something = value;
    }
}

The else branch (fallback to global) is the "mixed" part that makes them Step 7 targets. The if branch is pure runtime write — it mutates whichever RoleLike is on top.

By migrating role_like_mut to RequestContext in Step 5, Step 7 can build its new set_* methods as (&mut RequestContext, &mut AppConfig, value) signatures where the runtime path uses ctx.role_like_mut() directly. The prerequisite is now in place.

4. Path helpers stay on RequestContext, not AppConfig

messages_file, sessions_dir, session_file, and rag_file all read self.agent to decide between global and agent-scoped paths. self.agent is a runtime field (per-request). Even though the returned paths themselves are computed from paths:: functions (no per-request state involved), the decision of which path to return depends on runtime state. So these methods belong on RequestContext, not AppConfig or paths.

This is the correct split — paths:: is the "pure path computation" layer, RequestContext::messages_file etc. are the "which path applies to this request" layer on top.

5. state, info-style methods do not take &self.app

None of the 13 migrated methods reference self.app (the Arc<AppState>) or any field on AppConfig. This is the cleanest possible split — they're pure runtime-reads. If they needed both runtime state and AppConfig, they'd be mixed (like info and session_info, which is why those are deferred).

Deviations from plan

info deferred to Step 7

The plan lists info as a Step 5 target. Reading its body:

pub fn info(&self) -> Result<String> {
    if let Some(agent) = &self.agent {
        // ... agent export with session ...
    } else if let Some(session) = &self.session {
        session.export()
    } else if let Some(role) = &self.role {
        Ok(role.export())
    } else if let Some(rag) = &self.rag {
        rag.export()
    } else {
        self.sysinfo()  // ← falls through to sysinfo
    }
}

The fallback self.sysinfo() call is the problem. sysinfo() (lines 571-644 in src/config/mod.rs) reads BOTH serialized fields (wrap, rag_reranker_model, rag_top_k, save_session, compression_threshold, dry_run, function_calling_support, mcp_server_support, stream, save, keybindings, wrap_code, highlight, theme) AND runtime fields (self.rag, self.extract_role() which reads self.session, self.agent, self.role, self.model, etc.).

sysinfo is a mixed method in the Step 7 sense — it needs both AppConfig (for the serialized half) and RequestContext (for the runtime half). The plan's Step 7 mixed-method list includes sysinfo explicitly.

Since info delegates to sysinfo in one of its branches, migrating info without sysinfo would leave that branch broken. Action taken: left both Config::info and Config::sysinfo intact. Step 7 picks them up as a pair.

session_info deferred to Step 7

The plan lists session_info as a Step 5 target. Reading its body:

pub fn session_info(&self) -> Result<String> {
    if let Some(session) = &self.session {
        let render_options = self.render_options()?;  // ← AppConfig method
        let mut markdown_render = MarkdownRender::init(render_options)?;
        // ... reads self.agent for agent_info tuple ...
        session.render(&mut markdown_render, &agent_info)
    } else {
        bail!("No session")
    }
}

It calls self.render_options() which is a Step 3 method now on AppConfig. In the bridge world, the caller holds a Config and can call config.render_options() (old) or config.to_app_config().render_options() (new but cloning). In the post-bridge world with RequestContext, the call becomes ctx.app.config.render_options().

Since session_info crosses the AppConfig / RequestContext boundary, it's mixed by the Step 7 definition. Action taken: left Config::session_info intact. Step 7 picks it up with a signature like (&self, app: &AppConfig) -> Result<String> or (ctx: &RequestContext) -> Result<String> where ctx.app.config.render_options() is called internally.

Step 5 count: 13 methods, not 15

Documented here so Step 7's scope is explicit. Step 7 picks up info, session_info, sysinfo, plus the set_* methods and other items from the original Step 7 list.

Verification

Compilation

  • cargo check — clean, zero warnings, zero errors
  • cargo clippy — clean

Tests

  • cargo test63 passed, 0 failed (unchanged from Steps 14)

Step 5 added no new tests because it's duplication. Existing tests confirm:

  • The original Config methods still work
  • RequestContext still compiles, imports are clean
  • The bridge's round-trip test still passes

Manual smoke test

Not applicable — no runtime behavior changed.

Handoff to next step

What Step 6 can rely on

Step 6 (migrate request-write methods to RequestContext) can rely on:

  • RequestContext now has 13 inherent read methods
  • The #[allow(dead_code)] on the read-methods impl block is safe to leave; callers migrate in Steps 8+
  • Config is unchanged for all 13 methods
  • role_like_mut is available on RequestContext — Step 7 will use it, and Step 6 might also use it internally when implementing write methods like set_save_session_this_time
  • The bridge from Step 1, paths module from Step 2, AppConfig methods from Steps 3 and 4 are all unchanged
  • Config::info, session_info, and sysinfo are still on Config and must stay there through Step 6. They're Step 7 targets.
  • Config::update, setup_model, load_functions, load_mcp_servers, and all set_* methods are also still on Config and stay there through Step 6.

What Step 6 should watch for

  • Step 6 targets are request-write methods — methods that mutate the runtime state on Config (session, role, agent, rag). The plan's Step 6 target list includes: use_prompt, use_role / use_role_obj, exit_role, edit_role, use_session, exit_session, save_session, empty_session, set_save_session_this_time, compress_session / maybe_compress_session, autoname_session / maybe_autoname_session, use_rag / exit_rag / edit_rag_docs / rebuild_rag, use_agent / exit_agent / exit_agent_session, apply_prelude, before_chat_completion, after_chat_completion, discontinuous_last_message, init_agent_shared_variables, init_agent_session_variables.
  • Many will be mixed. Expect to defer several to Step 7. In particular, anything that reads self.functions, self.mcp_registry, or calls set_* methods crosses the boundary. Read each method carefully before migrating.
  • maybe_compress_session and maybe_autoname_session take GlobalConfig (not &mut self) and spawn background tasks internally. Their signature in Step 6 will need reconsideration — they don't fit cleanly in a RequestContext method because they're already designed to work with a shared lock.
  • use_session_safely, use_role_safely also take GlobalConfig. They do the take()/replace() dance with the shared lock. Again, these don't fit the &mut RequestContext pattern cleanly; plan to defer them.
  • compress_session and autoname_session are async. They call into the LLM. Their signature on RequestContext will still be async.
  • apply_prelude is tricky — it may activate a role/agent/ session from config strings like "role:explain" or "session:temp". It calls use_role, use_session, etc. internally. If those get migrated, apply_prelude migrates too. If any stay on Config, apply_prelude stays with them.
  • discontinuous_last_message just clears self.last_message. Pure runtime-write, trivial to migrate.

What Step 6 should NOT do

  • Don't touch the Step 3, 4, 5 methods on AppConfig / RequestContext — they stay until Steps 8+ caller migration.
  • Don't migrate any set_* method, info, session_info, sysinfo, update, setup_model, load_functions, load_mcp_servers, or the use_session_safely / use_role_safely family unless you verify they're pure runtime-writes — most aren't, and they're Step 7 targets.
  • Don't migrate callers of any method yet. Callers stay on Config through the bridge window.

Files to re-read at the start of Step 6

  • docs/PHASE-1-IMPLEMENTATION-PLAN.md — Step 6 section
  • This notes file — specifically "What Step 6 should watch for"
  • src/config/request_context.rs — current shape with Step 5 reads
  • Current Config method bodies in src/config/mod.rs for each Step 6 target

Follow-up (not blocking Step 6)

1. RequestContext now has ~200 lines beyond struct definition

Between Step 0's new() constructor and Step 5's 13 read methods, request_context.rs has grown to ~230 lines. Still manageable. Step 6 will add more. Post-Phase 1 cleanup can reorganize into multiple impl blocks grouped by concern (reads/writes/lifecycle) or into separate files if the file grows unwieldy.

2. Duplication count at end of Step 5

Running tally of methods duplicated between Config and the new types during the bridge window:

  • AppConfig (Steps 3+4): 11 methods
  • RequestContext (Step 5): 13 methods
  • paths:: module (Step 2): 33 free functions (not duplicated — Config forwarders were deleted in Step 2)

Total bridge-window duplication: 24 methods / ~370 lines.

All auto-delete in Step 10. Maintenance burden is "any bug fix in a migrated method during Steps 6-9 must be applied twice." Document this in whatever PR shepherds Steps 6-9.

3. The impl block structure in RequestContext is growing

Now has 2 impl RequestContext blocks:

  1. new() constructor (Step 0)
  2. 13 read methods (Step 5)

Step 6 will likely add a third block for writes. That's fine during the bridge window; cleanup can consolidate later.

References

  • Phase 1 plan: docs/PHASE-1-IMPLEMENTATION-PLAN.md
  • Step 4 notes: docs/implementation/PHASE-1-STEP-4-NOTES.md (for the duplication rationale)
  • Modified file: src/config/request_context.rs (new imports
    • new impl RequestContext block with 13 read methods)
  • Unchanged but referenced: src/config/mod.rs (original Config methods still exist, private constants MESSAGES_FILE_NAME / AGENTS_DIR_NAME / SESSIONS_DIR_NAME accessed via super::)