Skip to content

refactor(runtime): #3710 god-crate split — 5 standalone crates + oauth/wasm collapse#5053

Merged
houko merged 63 commits into
mainfrom
feat/3710-runtime-split
May 16, 2026
Merged

refactor(runtime): #3710 god-crate split — 5 standalone crates + oauth/wasm collapse#5053
houko merged 63 commits into
mainfrom
feat/3710-runtime-split

Conversation

@houko

@houko houko commented May 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements all four acceptance criteria of #3710. Three new standalone crates carved out
of the runtime god-crate, two pre-existing shell crates collapsed, every file in
librefang-runtime/src/ is now under 2,000 LOC, and each tool dispatcher has its own file
under tool_runner/.

Acceptance criteria

# Criterion Status
1 No file in librefang-runtime/src/ exceeds 2,000 LOC ✅ Largest is 1924 LOC (context_engine/scriptable/mod.rs).
2 cargo build -p librefang-runtime --no-default-features excludes browser/docker/media without compile errors ✅ Verified via cargo check -p librefang-runtime --lib --no-default-features.
3 Crate count goes down, not up arbitrarily Net −1 crate. −4 removed (librefang-runtime-oauth, -wasm, -browser, -apply-patch), +3 added (-audit, -sandbox-docker, -media).
4 Each tool implementation is in its own file under tool_runner/ ✅ 33 files (agent, artifact, canvas, channel, cron, dispatch, definitions, event, fs, goal, hand, image, knowledge, media, memory, meta, notify, process, sandbox, schedule, shell, shell_safety, skill, spill, system, taint, task, web_legacy, wiki, workflow, …).

Crates removed

Two re-export shell crates (~3 LOC each, no encapsulation):

  • librefang-runtime-oauth → inlined back as runtime modules
  • librefang-runtime-wasm → inlined back as runtime modules

Two of the Phase 1 standalone crates were collapsed back into runtime as feature-gated
modules to satisfy AC #3 literally (the issue's "Suggested split" table proposed them as
crates, but AC #3 overrides when they conflict):

  • librefang-runtime-browser (~1 kLOC) → crates/librefang-runtime/src/browser.rs behind feature = "browser", with browser_stub.rs fallback
  • librefang-runtime-apply-patch (~1 kLOC) → crates/librefang-runtime/src/apply_patch.rs (small enough that a standalone crate adds no real encapsulation value)

Crates added

Three subsystems large enough that standalone-crate encapsulation pays off:

Crate LOC Notes
librefang-runtime-audit ~2.5k Clean extraction; only librefang-types dep.
librefang-runtime-sandbox-docker ~700 Inlines subprocess_sandbox / str_utils helpers (duplicate-by-design) to avoid a circular dep on runtime.
librefang-runtime-media ~3.4k All five provider drivers (elevenlabs, gemini, google_tts, minimax, openai), the MediaDriver trait / cache, and media_understanding.rs.

librefang-runtime re-exports each new crate under its existing module path
(pub use librefang_runtime_audit as audit; etc.), so every existing call site keeps working
unchanged.

What was NOT changed

  • librefang-runtime-mcp stays as-is (already a clean ~3.5 kLOC standalone crate; not a shell).
  • agent_loop and tool_runner stay inside librefang-runtime. Their top-level files were
    shrunk via the smaller-extractions in this branch (run_streaming.rs, dispatch.rs,
    definitions.rs, spill.rs, media.rs, …) so every file now fits under 2 kLOC, but
    pulling the whole modules out would be a separate, deeper refactor — consistent with the
    project's CLAUDE.md guidance ("No new agent_loop.rs or tool_runner.rs file additions;
    both files are slated to shrink, not grow").

Verification

cargo check --workspace --lib                                 # clean
cargo check -p librefang-runtime --lib --no-default-features  # stubs build (AC #2)
cargo clippy --workspace --all-targets -- -D warnings         # clean
cargo test -p librefang-runtime --lib apply_patch::           # 22/22 pass
cargo test -p librefang-runtime --lib silent_response_single_source_of_truth  # grep-guard pass after run_streaming.rs allow-list entry

Workspace-wide cargo test is intentionally not run locally per repo policy; CI's split
unit-fast + integration shards cover it.

Commit map

  • 3ec5efa5 — collapse librefang-runtime-oauth and librefang-runtime-wasm (Phase 2 shells)
  • 76432ded — extract librefang-runtime-audit
  • eeb9d6c6 — extract librefang-runtime-sandbox-docker
  • a1b049db — extract librefang-runtime-browser (later collapsed back; see below)
  • 2307fb82 — extract librefang-runtime-media
  • 1637e95a — extract librefang-runtime-apply-patch (later collapsed back; see below)
  • 35740a1a — fix grep-guard allow-list for the post-split agent_loop/run_streaming.rs
  • 67b31c8c — collapse -browser + -apply-patch back into runtime as feature-gated modules (AC Bump docker/login-action from 3 to 4 #3)

Other commits between these are the AC #1 / AC #4 work (splitting agent_loop and
tool_runner internals so every file fits under 2 kLOC and every tool dispatcher has its
own file).

Closes #3710.

houko added 16 commits May 14, 2026 22:15
…system)

Move tool_runner.rs into tool_runner/mod.rs as the first step of #3710's
god-crate split. Carve out three pure-leaf helper modules:

- `taint`: outbound TaintSink checks (shell_exec, net_fetch, outbound_text,
  outbound_header) plus their secret-key/secret-header denylists.
- `shell_safety`: RO-workspace classifier for shell_exec (quote-aware
  tokenizer, RoSafety enum, classify_shell_exec_ro_safety + segment).
- `system`: location_get and system_time leaf tools.

Behavior-preserving: only `pub(super)` visibility was added so the parent
mod.rs can keep its existing call sites unchanged via `use self::*::*`.
No call path, no public surface, and no test was touched.

mod.rs shrinks from 14,337 LOC to 13,451 LOC (-886). Verification:
cargo check -p librefang-runtime and
cargo clippy -p librefang-runtime --lib -- -D warnings both clean.

Pre-existing failures in tests/mcp_oauth_integration.rs and
tests/tool_runner_forwarding.rs (missing mcp_oauth module and
OAuthTokens type) reproduce on origin/main HEAD and are out of scope
for this refactor.

Refs #3710
Pull out the no-helper, single-dispatch tools into their own files:

- `notify` — notify_owner (opaque operator ack)
- `memory` — memory_store / recall / list
- `wiki` — wiki_get / search / write
- `agent` — agent_find (agent_send/spawn/list/kill stay in mod.rs for now)
- `task` — task_post / claim / complete / list / status
- `event` — event_publish
- `goal` — goal_update

require_kernel is promoted to pub(super) so the new modules can share
the same Option<&Arc<dyn KernelHandle>> -> &Arc<dyn KernelHandle>
unwrapping behavior the original inline functions had.

Behavior-preserving: each tool is a verbatim function move with only
pub(super) visibility added; the parent dispatch sites stay unchanged
via use self::<module>::<fn> re-imports.

mod.rs: 13,451 -> 13,123 LOC (-328). cargo check + cargo clippy --lib
both clean.

Refs #3710
…l modules

Carve out four more leaf domains from tool_runner/mod.rs:

- `workflow` — run / list / status / start / cancel
- `knowledge` — add_entity / add_relation / query, plus the
  parse_entity_type / parse_relation_type helpers they share.
- `schedule` — natural-language schedule wrappers create / list / delete,
  with the parse_schedule_to_cron and parse_time_to_hour helpers kept
  module-private (they exist only to serve the schedule_* surface).
- `cron` — low-level cron_create / list / cancel that go straight to
  KernelHandle::cron_*. Includes the ownership check that keeps
  cron_cancel from deleting another agent's jobs.

Each tool keeps its original signature and body verbatim; only the
`fn` -> `pub(super) fn` visibility tweak was needed so the parent
dispatch sites can keep their existing call shape.

mod.rs: 13,123 -> 12,510 LOC (-613). cargo check + cargo clippy --lib
both clean.

Refs #3710
- `hand` — list / activate / status / deactivate. Each verbatim move
  from the original tool_runner.rs body; only visibility upgraded to
  pub(super).
- `a2a` — discover and send for cross-instance agent communication.
  Pulls in the existing taint sinks (check_taint_outbound_text,
  check_taint_net_fetch) via super:: so the SECURITY (#3786) trust
  gate and exfil-data check keep their original ordering — taint
  rejection still precedes the trusted-agent allowlist check.

Behavior-preserving. The stale "// Location tool" section header
left behind in an earlier batch is intentionally not touched here;
it'll go when the fs/media helpers (resolve_file_path_ext et al.)
move out and the surrounding section comments are rebuilt.

mod.rs: 12,510 -> 12,360 LOC (-150). cargo check + cargo clippy --lib
both clean.

Refs #3710
…odules

Four more leaf modules pulled out of tool_runner/mod.rs:

- `process` — start / poll / write / kill / list, each a thin wrapper
  over crate::process_manager::ProcessManager.
- `canvas` — sanitize_canvas_html + tool_canvas_present. The sanitizer
  keeps its `pub fn` visibility and is re-exported from tool_runner
  via `pub use self::canvas::sanitize_canvas_html`, so any
  `librefang_runtime::tool_runner::sanitize_canvas_html` consumer
  continues to resolve. canvas_present pulls CANVAS_MAX_BYTES from
  the parent module's task-local declaration.
- `skill` — the eight skill-evolution tools (create / update / patch /
  delete / rollback / write_file / remove_file) plus tool_skill_read_file,
  with the agent_author_tag and ensure_not_frozen helpers kept
  module-private since they are only consumed by this module.
- `artifact` — tool_read_artifact (#3347).

Verbatim function moves; only visibility upgraded to `pub(super)` (or
`pub` where the symbol was already crate-public). No behavior change.

mod.rs: 12,360 -> 11,744 LOC (-616). cargo check + cargo clippy --lib
both clean. The orphan `// ---` separator that the previous Docker /
process boundary left behind has also been removed.

Refs #3710
Three more leaf domains out of tool_runner/mod.rs:

- `web_legacy` — tool_web_fetch_legacy + tool_web_search_legacy, the
  WebToolsContext-less fallbacks. parse_ddg_results is consumed here
  now, so the corresponding import on mod.rs goes away.
- `meta` — tool_load + tool_search + the meta_lookup_pool helper.
  Pulls super::builtin_tool_definitions() so an empty granted slice
  still reports "not found" and only a None slice falls back to the
  builtin catalog (#3044). ToolExecutionStatus moves with it.
- `sandbox` — tool_docker_exec. The Docker module itself stays in
  crate::docker_sandbox; this only carves out the dispatch-side wrapper.

Verbatim function moves; visibility upgraded to pub(super). No
behavior change.

mod.rs: 11,744 -> 11,449 LOC (-295). cargo check + cargo clippy --lib
both clean.

Refs #3710
Largest single-tool extraction so far: tool_channel_send plus its two
private helpers parse_poll_options and mirror_channel_send_to_session
(issue #4824 mirror-back-to-session contract) all move into
tool_runner/channel.rs.

The channel tool handles four delivery modes — text, image_url /
file_url URL forwarding, local-file file_path attachment, and
Telegram-style polls — each branch verbatim. Taint checks
(check_taint_outbound_text) and the mirror-back call follow the
original ordering. parse_poll_options keeps fail-fast behavior on
non-string entries; mirror_channel_send_to_session keeps the JSON
envelope shape required for the inbound-routing session context.

To unblock the local-file branch, resolve_file_path_ext is promoted
to pub(super); the same change unblocks the upcoming image / media
extraction batch, which also reads files from the agent's workspace
sandbox.

mod.rs: 11,449 -> 11,058 LOC (-391). cargo check + cargo clippy --lib
both clean.

Refs #3710
tool_image_analyze + its three format/dimension helpers
(detect_image_format, extract_image_dimensions, extract_jpeg_dimensions)
and format_file_size move into tool_runner/image.rs. detect_image_format
is promoted to pub(super) because the test block at the bottom of
tool_runner/mod.rs already exercises it directly; the others stay
module-private since they have no out-of-module caller.

The stale "// Location tool" section header that the system extraction
left behind in mod.rs is removed in the same hunk — both Location
tools (location_get and system_time) already live in tool_runner/system,
so the divider was meaningless.

mod.rs: 11,058 -> 10,889 LOC (-169). cargo check + cargo clippy --lib
both clean.

Refs #3710
tool_shell_exec moves into tool_runner/shell.rs verbatim. All security
gating (taint sinks via tool_runner::taint, RO-workspace verb
classification via tool_runner::shell_safety) stays upstream in the
dispatcher — by the time we reach this function the command has
already been cleared, so the new module pulls in no super::* helper
beyond the standard `crate::*` references it already had.

Both execution paths (Allowlist direct-exec, Full sh -c) keep their
original byte-for-byte logic, including the pipe-draining
wait_with_output() + 100ms interrupt-tick loop that #4759 introduced
to fix the >8KB-output deadlock and preserve cancel-cascade behavior.

mod.rs: 10,889 -> 10,699 LOC (-190). cargo check + cargo clippy --lib
both clean.

Refs #3710
Move agent_send, agent_spawn, agent_list, agent_kill plus their two
private helpers (build_agent_manifest_toml, tools_to_parent_capabilities)
into tool_runner/agent.rs, joining the agent_find tool that was
extracted earlier. The module docstring is updated to reflect that
the whole inter-agent surface now lives there.

AGENT_CALL_DEPTH (task-local) is promoted from `static` to
`pub(super) static` so tool_agent_send can keep using its scoped
recursion guard from outside mod.rs. The companion `pub fn
current_agent_depth()` accessor in mod.rs is untouched, so any
external caller continues to resolve `tool_runner::current_agent_depth`.

Taint check on outgoing messages and self-send guard preserved in
order. The stale doc-comment for notify_owner that was orphaned in
mod.rs after the notify extraction is removed in the same hunk.

mod.rs: 10,699 -> 10,461 LOC (-238). cargo check + cargo clippy --lib
both clean.

Refs #3710
…odule

The four user-facing fs tools (file_read, file_write, file_list,
apply_patch) move into tool_runner/fs.rs together with every helper
they share:

- resolve_file_path_ext: the workspace-sandbox path resolver.
- named_ws_prefixes / _writable / _readonly: per-agent named-workspace
  allowlist accessors used by both the dispatcher and apply_patch's
  #3662 defense-in-depth check.
- check_absolute_path_inside_workspace: the ACP-path-jail guard plus
  its full `path_check_tests` regression block (#3313, dot-dot-traversal
  fix included).
- maybe_snapshot: the spawn_blocking-wrapped checkpoint snapshot helper
  invoked by file_write and apply_patch before they mutate the
  workspace.
- maybe_dedup_file_read: the #4971 dedup shim that gates file_read on
  `[context_engine] deduplicate_file_reads`.

All helpers are promoted to `pub(super)` so the dispatcher at the
top of mod.rs continues to call them under their original names; no
call site changed. The now-unused `PathBuf` import on mod.rs goes
away in the same hunk (mod.rs still pulls in `Path` for the
ToolExecContext fields).

mod.rs: 10,461 -> 10,043 LOC (-418). cargo check + cargo clippy --lib
both clean.

Refs #3710
The largest single-batch extraction yet — 12 functions and their two
constants/helpers move into tool_runner/media.rs:

- tool_media_describe / tool_media_transcribe (vision + audio
  understanding via media_understanding::MediaEngine)
- tool_image_generate (+ save_media_images_to_workspace,
  save_media_images_to_uploads helpers)
- tool_video_generate / tool_video_status (async video pipelines)
- tool_music_generate
- tool_text_to_speech (+ convert_to_ogg_opus, finish_tts_result helpers)
- tool_speech_to_text

SUPPORTED_AUDIO_EXTS_DOC is promoted to pub(super) because
builtin_tool_definitions in mod.rs reads it for the media_transcribe
and speech_to_text schema descriptions (so the agent-facing format
list stays in lockstep with audio_mime_from_ext). audio_mime_from_ext
itself is also pub(super) so the inline tests still reach it via
`use self::media::audio_mime_from_ext` from the tests module.

The internal helpers (save_media_images_*, convert_to_ogg_opus,
finish_tts_result) stay module-private because they have no out-of-
module caller.

mod.rs: 10,043 -> 9,219 LOC (-824). cargo check + cargo clippy --lib
both clean.

Refs #3710
resolve_spill_config + spill_or_passthrough move into
tool_runner/spill.rs. These are the shared overflow helpers that
web_fetch and web_search use to spill oversize tool results into the
artifact store (#3347 5/N) instead of burning context. Both are
verbatim moves with visibility upgraded to pub(super); their behavior
on the `0` -> compiled-default fallback in resolve_spill_config is
unchanged.

The stale `// Web tools` section header in mod.rs (which used to sit
above these helpers but no longer aligned with anything after the
upstream web tools moved out earlier) is removed in the same hunk.

mod.rs: 9,219 -> 9,172 LOC (-47). cargo check + cargo clippy --lib
both clean.

Refs #3710
…module

The ~1.1k-line builtin_tool_definitions() function — every native
tool's JSON schema — moves into tool_runner/definitions.rs along
with its ALWAYS_NATIVE_TOOLS constant (#3044) and select_native_tools
helper. mod.rs re-exports all three with `pub use` so external
consumers (librefang_kernel::kernel::mod, the test suite, the
auto_dream module) keep resolving the same paths
(`librefang_runtime::tool_runner::builtin_tool_definitions`, etc.).

definitions.rs reaches SUPPORTED_AUDIO_EXTS_DOC via `super::media`
— sibling access is fine because the const is pub(super) inside
tool_runner and definitions.rs is also a child of tool_runner.

While here, replace the stale `// Inter-agent tools` section header
above require_kernel with a one-line doc comment, since the
inter-agent tools themselves no longer live next to it.

mod.rs: 9,171 -> 7,976 LOC (-1195). cargo check + cargo clippy --lib
both clean. The single largest extraction in this issue so far.

Refs #3710
execute_tool_raw, execute_tool, ToolExecContext, and current_agent_depth
all move into tool_runner/dispatch.rs. mod.rs re-exports them via
`pub use self::dispatch::{...}` so every external consumer
(librefang_kernel::kernel::mod, the test suite, auto_dream) keeps
resolving the same paths
(`librefang_runtime::tool_runner::execute_tool_raw`, etc.).

dispatch.rs uses `use super::*;` to pull every sibling-module tool
name (tool_a2a_send, tool_file_read, …) into scope without re-listing
each one — the parent module already aggregates them. Everything
that used to be at mod.rs top (`mcp`, `WebToolsContext`, `SkillRegistry`,
`TaintSink`, `ToolDefinition`/`ToolResult`, `normalize_tool_name`,
`Path`, `debug`/`warn`) follows dispatch.rs because those types are
only referenced by the dispatch path; mod.rs is left with just
`Arc` + `KernelHandle` prelude for `require_kernel`.

mod.rs: 7,976 -> 6,489 LOC (-1487). cargo check + cargo clippy --lib
both clean. The 1.5k-line dispatch table now has its own roof; only
the inline test suite (~6k LOC) remains in mod.rs and is the next
candidate for redistribution.

Refs #3710
The 6.3k-LOC `mod tests { ... }` block at the bottom of mod.rs moves
into tool_runner/tests.rs. mod.rs declares it with `#[cfg(test)] mod tests;`
so the suite is path-equivalent (`tool_runner::tests::*` runs the same
tests).

The dedent + de-nesting pulls the previously-inside-mod-tests `#[test]`
fns up one level so test names stay flat and the `super::*` import
resolves to tool_runner (not tool_runner::tests::tests). The 8 outer-
scope `#[tokio::test]` fns that already lived at file root keep their
existing layout.

Six previously-private helpers needed pub(super) so the moved tests
can still call them directly:

- channel::parse_poll_options
- agent::build_agent_manifest_toml
- agent::tools_to_parent_capabilities
- schedule::parse_schedule_to_cron / parse_time_to_hour
- image::{extract_image_dimensions, extract_jpeg_dimensions,
  format_file_size}

(detect_image_format was already pub(super).)

tests.rs imports the types it needs (TaintSink, SkillRegistry,
ToolDefinition, ToolResult, Path, AtomicUsize, Arc) directly rather
than relying on mod.rs holding test-only re-exports; mod.rs's top
import block stays focused on its lib-runtime essentials
(KernelHandle prelude + Arc for require_kernel).

mod.rs: 6,489 -> 122 LOC (-6,367). cargo check + cargo check --tests
both clean. The 2 pre-existing "can't find crate librefang_runtime"
failures in tests/tool_runner_*_test.rs reproduce on origin/main and
are out of scope.

Refs #3710
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@github-actions github-actions Bot added the area/runtime Agent loop, LLM drivers, WASM sandbox label May 14, 2026
houko added 6 commits May 15, 2026 08:59
…act types module

First step of agent_loop split (#3710 phase 2). Move the 13,853-LOC
`agent_loop.rs` to `agent_loop/mod.rs` and carve out the public-facing
type cluster — `LoopPhase`, `PhaseCallback`, `LoopOptions`,
`AgentLoopResult`, `ExperimentContext` (+ its `Default` impl) — into
`agent_loop/types.rs`.

mod.rs re-exports the five symbols via `pub use self::types::{...}` so
every external consumer keeps resolving the same paths
(`librefang_runtime::agent_loop::AgentLoopResult`,
`librefang_kernel::agent_loop::AgentLoopResult`, etc.).

types.rs imports only what it actually needs (`TokenUsage`,
`DecisionTrace`, `Arc`); fields that reference `crate::interrupt::*`,
`crate::aux_client::*`, or `librefang_types::config::*` keep their
full-path form so the new file pulls in no extra `use`s that would
collide with mod.rs's import block on a future merge.

The extraction is purely visibility-preserving: every type stays `pub`,
no internal call site changes. mod.rs: 13,853 -> 13,623 LOC (-230).
cargo check + cargo clippy --lib both clean.

Refs #3710
`LAZY_TOOLS_THRESHOLD`, `resolve_request_tools`, and the
`ResolvedToolsCache` struct (with its `new` / `get` methods) move into
`agent_loop/tool_resolution.rs`. All three become `pub(super)` so the
main loop in `mod.rs` can keep its existing call shape
(`ResolvedToolsCache::new(...)` at the per-turn init site,
`tools_cache.get(...)` per iteration).

`resolve_request_tools` and `LAZY_TOOLS_THRESHOLD` are only consumed
from the in-file `#[cfg(test)] mod tests` block, so they are imported
into the test module directly (`use super::tool_resolution::{...};`)
rather than at file scope. This keeps `unused_imports` quiet on the
release lane — the lint can't see through `use super::*;` to a child
test module — without introducing a `#[allow]`.

Verbatim function bodies; only visibility changes. The lazy-tool
gating contract from #3044 (require `tool_load` in the granted set
before stripping non-native tools) and the per-iteration `Arc` cache
from #3586 are preserved exactly. mod.rs: 13,623 -> 13,531 LOC (-92).
cargo check + cargo clippy --lib both clean.

Refs #3710
The web-search augmentation cluster — `should_augment_web_search`,
`SEARCH_QUERY_GEN_PROMPT`, `generate_search_queries`, and
`web_search_augment` — moves into `agent_loop/web_augment.rs`.

Only `web_search_augment` is consumed from the main loop (called once
each at the head of `run_agent_loop` and `run_agent_loop_streaming`
before the first LLM call), so it is the only `use self::web_augment::*`
re-import at file scope. `should_augment_web_search` and
`SEARCH_QUERY_GEN_PROMPT` are referenced only from the `#[cfg(test)] mod
tests` block, so they are pulled in alongside the existing
`tool_resolution` test-only imports — same pattern as the prior commit,
no `#[allow(unused_imports)]` needed.

`generate_search_queries` stays module-private — it has no caller outside
this cluster. The new module is named `web_augment` (not `web_search`)
to avoid name-collision confusion with the sibling `crate::web_search`
module that holds the `WebToolsContext` it consumes.

Behavior-preserving: verbatim function bodies, only visibility was
adjusted (`pub(super)` on the 3 cluster-public items, private on
`generate_search_queries`). The Auto-mode fallback (assume tools
supported when `model_supports_tools` metadata is absent) and the
JSON-extraction sentinel-vs-error contract on `generate_search_queries`
are unchanged.

mod.rs: 13,531 -> 13,349 LOC (-182). cargo check + cargo clippy --lib
both clean.

Refs #3710
The model-identifier helper cluster moves into `agent_loop/model.rs`:

- `strip_provider_prefix` (pub) — strips `provider/` or `provider:`
  prefix from the catalog model id, then for providers that demand
  `org/model` form (OpenRouter, Together, Fireworks, Replicate,
  HuggingFace) backfills via `normalize_bare_model_id`.
- `apply_session_model_override_to_manifest` (pub) — per-session
  `model_override` apply path from #4898; called by
  `kernel::agent_execution::execute_llm_agent`.
- `needs_qualified_model_id` (pub(super)) — provider allowlist for the
  qualified-id rule above.
- `normalize_bare_model_id` (private) — bare-name → `org/model`
  lookup table.
- `DEFAULT_CONTEXT_WINDOW` (pub(super), `#[allow(dead_code)]`) — kept
  as the documented authoritative value referenced by
  `docs/architecture/message-history-trimming.md`.
- `UNKNOWN_MODEL_CONTEXT_WINDOW` (pub(super)) — 8K fallback used by
  the main loop's context-window resolution path (#3349).
- `stable_prefix_mode_enabled` (pub(super)) — manifest metadata check
  for the stable-prefix experiment flag.

The in-file `mod session_model_override_tests` (7 tests) travels with
`apply_session_model_override_to_manifest` so the test/function pair
stays co-located.

mod.rs re-exports the two `pub` items via
`pub use self::model::{...}` so kernel and channel callers keep
resolving the same `librefang_runtime::agent_loop::strip_provider_prefix`
path. `STABLE_PREFIX_MODE_METADATA_KEY` is removed from mod.rs's
import block (consumed only by `stable_prefix_mode_enabled`, which is
now in model.rs). The big `mod tests` block in mod.rs picks up
`needs_qualified_model_id` via the existing test-only
`use super::model::*` pattern.

Behavior-preserving. mod.rs: 13,349 -> 13,096 LOC (-253). cargo check
+ cargo clippy --lib both clean.

Refs #3710
The message-history trim config — `DEFAULT_MAX_HISTORY_MESSAGES`
(pub, 60), `MIN_HISTORY_MESSAGES` (pub(super), 4), `resolve_max_history`
(pub(super)) and `clamp_max_history` (private) — moves into
`agent_loop/history.rs`.

`resolve_max_history` is used by the main `run_agent_loop` and
`run_agent_loop_streaming` entry points to derive the per-turn trim
cap; `DEFAULT_MAX_HISTORY_MESSAGES` is re-exported via
`pub use self::history::DEFAULT_MAX_HISTORY_MESSAGES` so docs that
quote `librefang_runtime::agent_loop::DEFAULT_MAX_HISTORY_MESSAGES`
(message-history-trimming architecture page, configuration core page)
keep resolving. `MIN_HISTORY_MESSAGES` is referenced from the in-file
`mod tests` clamp-tests; pulled in alongside the existing
`super::model::*` / `super::web_augment::*` test-only imports.

`clamp_max_history` stays private — `resolve_max_history` is its only
caller. The 4-tier resolution order
(`manifest > opts > DEFAULT > clamped-to-floor`) is preserved exactly.

Behavior-preserving. mod.rs: 13,096 -> 13,027 LOC (-69).
cargo check + cargo clippy --lib + cargo check --tests all clean.

Refs #3710
The message-shape helper cluster moves into `agent_loop/message.rs`:

- `ACCUMULATED_TEXT_MAX_BYTES` + `push_accumulated_text` — the bounded
  64-KiB fallback buffer the empty-response guard reads when no text
  arrives alongside `tool_use` blocks; sealed-on-overflow with ASCII
  padding so subsequent appends short-circuit.
- `is_no_reply`, `is_progress_text_leak`, `is_cascade_leak` — model-
  output classifiers (delegates to `silent_response` for two of them).
- `is_soft_error_content`, `is_parameter_error_content` — tool-error
  content classifiers used by the consecutive-failure abort threshold.
- `sanitize_for_memory` (+ the inline `use` of
  `ENVELOPE_LINE_PREFIXES` / `ENVELOPE_STANDALONE_MARKERS` that fed
  it) — channel-envelope stripper for the proactive-memory persist
  path; preserves inline bracketed content, returns `None` when nothing
  meaningful survives.
- `safe_trim_messages` — the trim-on-turn-boundary helper for working
  history, with the rescued-pinned-message reinsertion and the
  post-trim re-validate / ensure-starts-with-user safety net.
- `strip_processed_image_data`, `strip_prior_image_data` — image-data
  trimmers for already-processed turns.
- `accumulate_token_usage` — per-turn `TokenUsage` rollup.
- `sanitize_tool_result_content` — strip injection markers + dynamic
  truncate, delegating to the `ContextEngine` when present.
- `sanitize_sender_label` — bounded `[name]:` prefix sanitizer for
  group-chat senders.

mod.rs drops the now-unused `truncate_tool_result_dynamic`,
`ERR_PATH_TRAVERSAL`, `ERR_SANDBOX_ESCAPE` top-level imports — all
three were only consumed by the helpers being extracted. The big
`mod tests` block picks up `ACCUMULATED_TEXT_MAX_BYTES`,
`ENVELOPE_LINE_PREFIXES`, and `ENVELOPE_STANDALONE_MARKERS` via the
same test-only `use super::message::*` / `use crate::silent_response::*`
pattern the prior commits established.

The single stranded `/// Check if a response is a silent-reply sentinel.`
doc-line that used to sit above the moved `is_no_reply` (and which
the deletion range missed by one line) is removed in this commit too,
fixing the resulting `clippy::empty-line-after-doc-comments` lint on
the `tool_use_blocks_from_calls` boundary.

Behavior-preserving. mod.rs: 13,027 -> 12,641 LOC (-386).
cargo check + cargo clippy --lib + cargo check --tests all clean.

Refs #3710
@github-actions github-actions Bot added the size/XL 1000+ lines changed label May 15, 2026
houko and others added 4 commits May 15, 2026 10:55
The text-based tool-call recovery cluster moves into
`agent_loop/text_recovery.rs`. Three `pub(super)` entry points:

- `looks_like_hallucinated_action` — flags model output that claims a
  completed action (English present-perfect, Italian `ho` + past
  participle, impersonal `è stato/stata`, `successfully X`,
  `messaggio inviato`, …) without an actual tool call. Used by the
  end-turn classifier in `mod.rs` to decide whether to nudge the
  model to retry with a real tool invocation.
- `user_message_has_action_intent` — checks the user's request for
  exact-word action verbs (`send`, `execute`, `create`, `deploy`, …)
  so the hallucination nudge only fires when the user actually asked
  for an action.
- `recover_text_tool_calls` — extracts synthetic `ToolCall` entries
  from free-text LLM output across nine on-the-wire formats:
  `<function=NAME>{…}</function>` (Pattern 1), `<function>NAME{…}</function>`
  (P2), `<tool>NAME{…}</tool>` (P3 — Qwen / DeepSeek), markdown code
  blocks (P4), backtick-wrapped (P5), `[TOOL_CALL]…[/TOOL_CALL]` with
  JSON or arrow syntax (P6 — issue #354), `<tool_call>JSON</tool_call>`
  (P7 — Qwen3, issue #332), `<function name="…" parameters="…" />`
  (P9 — Groq/Llama XML attribute, cached regex per #3491), and the
  bare-JSON last-resort fallback (P8).

Internal helpers (`parse_json_tool_call_object`, `parse_arrow_syntax_tool_call`,
`parse_dash_dash_args`, `try_parse_bare_json_tool_call`) stay private to
the module except for the two that the `mod tests` block exercises
directly (`parse_json_tool_call_object`, `parse_dash_dash_args`) — those
are `pub(super)` and pulled in via the existing test-only import block.

Behavior-preserving: every pattern's matching logic, dedupe key
(`name + input`), recovered-id format (`recovered_<uuid>`), and
warn/info log site are byte-for-byte the same as before the move. The
ordering of pattern-matching (1 → 9 → 8) is unchanged, including the
P8 bare-JSON guard that only runs when `calls.is_empty()`.

mod.rs: 12,641 -> 11,812 LOC (-829). cargo check + cargo clippy --lib
+ cargo check --tests all clean.

Refs #3710
The LLM-call retry path moves into `agent_loop/retry.rs`. Two
`pub(super)` entry points consumed by the main loop bodies:

- `call_with_retry` — non-streaming wrapper. Honors the
  `ProviderCooldown` circuit-breaker (Allow / AllowProbe / Reject),
  acquires `LLM_CONCURRENCY` per attempt so backoff sleeps don't pin
  a slot, classifies errors via `llm_errors` for sanitized
  user-facing messages, and appends the
  `librefang_channels::message_journal::RATE_LIMIT_DEFER_MARKER` to
  exhausted-retry errors so the channel bridge can route them to
  `JournalStatus::Deferred` instead of `Failed`.
- `stream_with_retry` — streaming variant returning
  `StreamWithRetryResult { response, cascade_leak_aborted }`. Same
  retry policy as the non-streaming path, plus the incremental
  cascade-leak guard that stops forwarding `TextDelta` events
  mid-stream when the in-memory accumulator trips
  `is_cascade_leak`.

Cluster-private helpers stay private to the module:
`check_retry_cooldown`, `record_retry_success`, `record_retry_failure`,
`handle_retryable_llm_error`, `build_user_facing_llm_error`.

Constants travel with their consumers: `MAX_RETRIES` (pub(super)),
`BASE_RETRY_DELAY_MS` (pub(super)), `MAX_CONCURRENT_LLM_CALLS`
(private), `DEFAULT_DEFER_MS` (private), and the `LLM_CONCURRENCY`
static. The two `pub(super)` constants are referenced from the
`mod tests` block and imported there via the existing
test-only `use super::retry::{...}` pattern.

`StreamWithRetryResult.response` and
`StreamWithRetryResult.cascade_leak_aborted` are `pub(super)` because
the streaming main loop in mod.rs destructures them after the call.

mod.rs sheds the now-orphan top-level imports that only the retry
path needed: `CooldownVerdict`, `ProviderCooldown`, `LlmError`, and
the entire `crate::llm_errors` use. retry.rs picks them up locally
along with `StopReason`, `TokenUsage`, and `super::TIMEOUT_PARTIAL_OUTPUT_MARKER`.

Behavior-preserving. mod.rs: 11,812 -> 11,384 LOC (-428).
cargo check + cargo clippy --lib + cargo check --tests all clean.

Refs #3710
The end-of-turn cluster moves into `agent_loop/end_turn.rs`:

- Three context structs — `FinalizeEndTurnContext`,
  `FinalizeEndTurnResultData`, `EndTurnRetryContext` — promoted to
  `pub(super)` along with every field. The main loop in mod.rs
  literally constructs these (both `run_agent_loop` and
  `run_agent_loop_streaming` build them at four sites each), so
  literal-construction requires all fields to be visible at the
  call site.
- `EndTurnRetry` enum (`EmptyResponse { is_silent_failure }`,
  `HallucinatedAction`, `ActionIntent`) — `pub(super)`. Pattern-
  matched at two main-loop sites.
- `classify_end_turn_retry` — decides whether the just-finished
  turn should retry. Three rules: empty response (iter 0 or
  zero-token "silent failure"), hallucinated action claim with no
  actual tool call, and action-intent user message ignored by a
  text-only reply. The hallucination + action-intent branches
  delegate to `text_recovery`.
- `finalize_end_turn_text` — empty-response fallback, with the
  accumulated_text rescue for agents that emitted text alongside
  earlier `tool_use` blocks.
- `finalize_successful_end_turn` — the big success path: push
  assistant message, prune heartbeat turns, save_session_async
  (skipped on fork/incognito), proactive-memory write through
  `sanitize_for_memory`, and assemble the `AgentLoopResult`.
- `maybe_fold_stale_tool_results` — periodic history-fold to keep
  the context window viable.
- `gated_proactive_memory_for_retrieve` / `_for_memorize` —
  per-agent gates on the optional `ProactiveMemoryStore`. Both
  called from prompt setup (retrieve) and end-turn (memorize) on
  both the streaming and non-streaming paths.
- `serialize_session_messages` — stays module-private (only
  `finalize_successful_end_turn` consumes it).
- `build_silent_agent_loop_result` — bare success result for the
  silent-reply path (NO_REPLY / `[[silent]]` directives).

end_turn.rs uses `use super::*;` to inherit the parent module's
re-exports (LoopOptions, AgentLoopResult, ExperimentContext, etc.)
plus explicit `super::message::sanitize_for_memory` and
`super::text_recovery::{looks_like_hallucinated_action,
user_message_has_action_intent}` for the names not re-exported
through the wildcard.

The `mod tests` block picks up `sanitize_for_memory`,
`looks_like_hallucinated_action`, and `user_message_has_action_intent`
via the existing test-only `use super::*` pattern.

Behavior-preserving. mod.rs: 11,384 -> 10,893 LOC (-491).
cargo check + cargo clippy --lib + cargo check --tests all clean.

Refs #3710
houko added a commit that referenced this pull request May 16, 2026
After PR #5053 split plugin_manager.rs into a plugin_manager/ directory,
the todo-to-issue workflow's IGNORE rule still pointed at the old file
path. When #5053 merged, the workflow scanned the new
plugin_manager/scaffold.rs (which contains TODO-marker placeholders inside
raw string literals — those are starter-code templates emitted into
user-generated plugins, not project TODOs) and opened 21 false-positive
issues (#5079-#5099, all subsequently closed as not-planned).

Widen IGNORE to the directory so the scaffolder's string-literal templates
are skipped, and any future siblings under plugin_manager/ are covered
without another rebase.
f-liva pushed a commit to f-liva/librefang that referenced this pull request May 21, 2026
…put (drift-pin)

`is_cascade_leak` (librefang#4907) hard-codes string-literal markers in
`THEMATIC_HEADERS`, `SCAFFOLD_ONLY_HEADERS`, `STRUCTURAL_TURN_FRAMES`,
`ENVELOPE_LINE_PREFIXES`, and `ENVELOPE_STANDALONE_MARKERS`. A cosmetic
prompt-builder change (e.g. `## Sender` -> `## From`) silently
invalidates the detector — the leak it was tuned to catch (librefang#5141) stops
firing, and CI stays green because no existing test runs
`build_system_prompt` and greps its output for the marker strings.

Add two pinned tests in `silent_response::tests`:

- `structural_and_envelope_markers_absent_from_prompt_builder` —
  negative pin. `STRUCTURAL_TURN_FRAMES` / `ENVELOPE_LINE_PREFIXES` /
  `ENVELOPE_STANDALONE_MARKERS` must NOT appear in a fully-populated
  `build_system_prompt` output. Otherwise the cascade-leak guard
  false-positives on its own scaffolding the moment the model
  paraphrases the prompt back.

- `thematic_and_scaffold_headers_match_prompt_builder_output` —
  positive pin via a snapshot of (marker, currently-emitted)
  tuples. Today the five `THEMATIC_HEADERS` entries are all snapshotted
  as `absent` — a pre-existing divergence the closed-PR-librefang#4760
  reviewer asked the re-port to surface. Renaming or restoring a
  section in `prompt_builder.rs` that touches one of these names now
  fails the snapshot in lock-step.

This is the narrower re-port of `keywords_match_real_prompt_headers`
from closed PR librefang#4760 (closing comment recommended carrying it
forward), retargeted for the post-librefang#5053 prompt-builder layout and the
post-librefang#5073 `granted_tool_hints` `PromptContext` shape. Items 1-5 in
the librefang#4760 close comment ("genuinely new shapes") are out of scope —
this PR is just the test.

Verification:
- cargo check  -p librefang-runtime --lib                         OK
- cargo clippy -p librefang-runtime --lib -- -D warnings           OK
- cargo fmt    -p librefang-runtime --check                        OK
- cargo test   -p librefang-runtime --lib silent_response::       31 passed
- cargo test   -p librefang-runtime --lib prompt_builder::        81 passed
f-liva pushed a commit to f-liva/librefang that referenced this pull request May 21, 2026
…fang#5053 prompt builder

The drift-pin test added by PR librefang#5344
(`silent_response::tests::thematic_and_scaffold_headers_match_prompt_builder_output`)
surfaced that `THEMATIC_HEADERS` and `SCAFFOLD_ONLY_HEADERS` in
`crates/librefang-runtime/src/silent_response.rs` were calibrated
against five legacy strings — `## Sender`, `## Today`, `## Calendar`,
`## Tasks`, `## Response Style` — none of which the post-librefang#5053 prompt
builder emits.

Consequence: the `1 structural + 1 thematic` branch of
`is_cascade_leak` was effectively dead, and the detector silently
degraded to a `2+ structural` rule. The canonical chat-template
regurgitation shape from today's builder — `## Persona\n…\n## Memory`
— had zero thematic hits and no longer tripped on its own, exactly
the class of cross-chat leak the detector exists to catch.

This reconciles the two lists with the actual headers enumerated by
`prompt_builder.rs`:

- 21 current-builder strings move into `SCAFFOLD_ONLY_HEADERS` (pure
  prompt-frame; no genuine reply emits a `## Persona\n` line-leader).
- `## Current Date` is the lone ambiguous-subset entry, kept in
  `THEMATIC_HEADERS` but intentionally NOT in `SCAFFOLD_ONLY_HEADERS`
  — a legitimate "what's today?" reply may produce it.
- The legacy five stay in both lists as a defensive belt-and-suspenders
  layer for any deployment still on the pre-librefang#5053 builder. Cheap to
  leave; same false-positive risk as before.

The drift-pin snapshot is flipped from `(marker, false)` for the
legacy five to `(marker, true)` for the 21 current strings; the
legacy five remain `false`. The two cross-check loops the snapshot
test ships ensure no list can be edited without updating the snapshot
in lock-step and vice versa — drift like this can't recur silently.

Regression tests added in `silent_response::tests`:

- `cascade_leak_current_builder_scaffold_only_pair_trips` —
  `## Persona\n…\n## Memory` and two other current-builder pairs
  trip (2 structural-equivalent hits).
- `cascade_leak_current_builder_scaffold_plus_envelope_trips` —
  `[Group message from …]\n## Persona\n…` trips (1 envelope-structural
  + 1 scaffold-structural-equivalent).
- `cascade_leak_current_builder_ambiguous_alone_legitimate` —
  `## Current Date\n…\nyour day looks clear` is NOT a leak (only
  ambiguous header, no structural).
- `cascade_leak_current_builder_ambiguous_plus_structural_trips` —
  `[Past exchange]\n…\n## Current Date\n…` trips (the 1+1 branch
  that was previously dead).
houko added a commit that referenced this pull request May 22, 2026
…put (drift-pin) (#5344)

`is_cascade_leak` (#4907) hard-codes string-literal markers in
`THEMATIC_HEADERS`, `SCAFFOLD_ONLY_HEADERS`, `STRUCTURAL_TURN_FRAMES`,
`ENVELOPE_LINE_PREFIXES`, and `ENVELOPE_STANDALONE_MARKERS`. A cosmetic
prompt-builder change (e.g. `## Sender` -> `## From`) silently
invalidates the detector — the leak it was tuned to catch (#5141) stops
firing, and CI stays green because no existing test runs
`build_system_prompt` and greps its output for the marker strings.

Add two pinned tests in `silent_response::tests`:

- `structural_and_envelope_markers_absent_from_prompt_builder` —
  negative pin. `STRUCTURAL_TURN_FRAMES` / `ENVELOPE_LINE_PREFIXES` /
  `ENVELOPE_STANDALONE_MARKERS` must NOT appear in a fully-populated
  `build_system_prompt` output. Otherwise the cascade-leak guard
  false-positives on its own scaffolding the moment the model
  paraphrases the prompt back.

- `thematic_and_scaffold_headers_match_prompt_builder_output` —
  positive pin via a snapshot of (marker, currently-emitted)
  tuples. Today the five `THEMATIC_HEADERS` entries are all snapshotted
  as `absent` — a pre-existing divergence the closed-PR-#4760
  reviewer asked the re-port to surface. Renaming or restoring a
  section in `prompt_builder.rs` that touches one of these names now
  fails the snapshot in lock-step.

This is the narrower re-port of `keywords_match_real_prompt_headers`
from closed PR #4760 (closing comment recommended carrying it
forward), retargeted for the post-#5053 prompt-builder layout and the
post-#5073 `granted_tool_hints` `PromptContext` shape. Items 1-5 in
the #4760 close comment ("genuinely new shapes") are out of scope —
this PR is just the test.

Verification:
- cargo check  -p librefang-runtime --lib                         OK
- cargo clippy -p librefang-runtime --lib -- -D warnings           OK
- cargo fmt    -p librefang-runtime --check                        OK
- cargo test   -p librefang-runtime --lib silent_response::       31 passed
- cargo test   -p librefang-runtime --lib prompt_builder::        81 passed

Co-authored-by: Federico Liva <[email protected]>
Co-authored-by: Evan <[email protected]>
f-liva pushed a commit to f-liva/librefang that referenced this pull request May 22, 2026
…fang#5053 prompt builder

The drift-pin test added by PR librefang#5344
(`silent_response::tests::thematic_and_scaffold_headers_match_prompt_builder_output`)
surfaced that `THEMATIC_HEADERS` and `SCAFFOLD_ONLY_HEADERS` in
`crates/librefang-runtime/src/silent_response.rs` were calibrated
against five legacy strings — `## Sender`, `## Today`, `## Calendar`,
`## Tasks`, `## Response Style` — none of which the post-librefang#5053 prompt
builder emits.

Consequence: the `1 structural + 1 thematic` branch of
`is_cascade_leak` was effectively dead, and the detector silently
degraded to a `2+ structural` rule. The canonical chat-template
regurgitation shape from today's builder — `## Persona\n…\n## Memory`
— had zero thematic hits and no longer tripped on its own, exactly
the class of cross-chat leak the detector exists to catch.

This reconciles the two lists with the actual headers enumerated by
`prompt_builder.rs`:

- 21 current-builder strings move into `SCAFFOLD_ONLY_HEADERS` (pure
  prompt-frame; no genuine reply emits a `## Persona\n` line-leader).
- `## Current Date` is the lone ambiguous-subset entry, kept in
  `THEMATIC_HEADERS` but intentionally NOT in `SCAFFOLD_ONLY_HEADERS`
  — a legitimate "what's today?" reply may produce it.
- The legacy five stay in both lists as a defensive belt-and-suspenders
  layer for any deployment still on the pre-librefang#5053 builder. Cheap to
  leave; same false-positive risk as before.

The drift-pin snapshot is flipped from `(marker, false)` for the
legacy five to `(marker, true)` for the 21 current strings; the
legacy five remain `false`. The two cross-check loops the snapshot
test ships ensure no list can be edited without updating the snapshot
in lock-step and vice versa — drift like this can't recur silently.

Regression tests added in `silent_response::tests`:

- `cascade_leak_current_builder_scaffold_only_pair_trips` —
  `## Persona\n…\n## Memory` and two other current-builder pairs
  trip (2 structural-equivalent hits).
- `cascade_leak_current_builder_scaffold_plus_envelope_trips` —
  `[Group message from …]\n## Persona\n…` trips (1 envelope-structural
  + 1 scaffold-structural-equivalent).
- `cascade_leak_current_builder_ambiguous_alone_legitimate` —
  `## Current Date\n…\nyour day looks clear` is NOT a leak (only
  ambiguous header, no structural).
- `cascade_leak_current_builder_ambiguous_plus_structural_trips` —
  `[Past exchange]\n…\n## Current Date\n…` trips (the 1+1 branch
  that was previously dead).
f-liva pushed a commit to f-liva/librefang that referenced this pull request May 25, 2026
…fang#5053 prompt builder

The drift-pin test added by PR librefang#5344
(`silent_response::tests::thematic_and_scaffold_headers_match_prompt_builder_output`)
surfaced that `THEMATIC_HEADERS` and `SCAFFOLD_ONLY_HEADERS` in
`crates/librefang-runtime/src/silent_response.rs` were calibrated
against five legacy strings — `## Sender`, `## Today`, `## Calendar`,
`## Tasks`, `## Response Style` — none of which the post-librefang#5053 prompt
builder emits.

Consequence: the `1 structural + 1 thematic` branch of
`is_cascade_leak` was effectively dead, and the detector silently
degraded to a `2+ structural` rule. The canonical chat-template
regurgitation shape from today's builder — `## Persona\n…\n## Memory`
— had zero thematic hits and no longer tripped on its own, exactly
the class of cross-chat leak the detector exists to catch.

This reconciles the two lists with the actual headers enumerated by
`prompt_builder.rs`:

- 21 current-builder strings move into `SCAFFOLD_ONLY_HEADERS` (pure
  prompt-frame; no genuine reply emits a `## Persona\n` line-leader).
- `## Current Date` is the lone ambiguous-subset entry, kept in
  `THEMATIC_HEADERS` but intentionally NOT in `SCAFFOLD_ONLY_HEADERS`
  — a legitimate "what's today?" reply may produce it.
- The legacy five stay in both lists as a defensive belt-and-suspenders
  layer for any deployment still on the pre-librefang#5053 builder. Cheap to
  leave; same false-positive risk as before.

The drift-pin snapshot is flipped from `(marker, false)` for the
legacy five to `(marker, true)` for the 21 current strings; the
legacy five remain `false`. The two cross-check loops the snapshot
test ships ensure no list can be edited without updating the snapshot
in lock-step and vice versa — drift like this can't recur silently.

Regression tests added in `silent_response::tests`:

- `cascade_leak_current_builder_scaffold_only_pair_trips` —
  `## Persona\n…\n## Memory` and two other current-builder pairs
  trip (2 structural-equivalent hits).
- `cascade_leak_current_builder_scaffold_plus_envelope_trips` —
  `[Group message from …]\n## Persona\n…` trips (1 envelope-structural
  + 1 scaffold-structural-equivalent).
- `cascade_leak_current_builder_ambiguous_alone_legitimate` —
  `## Current Date\n…\nyour day looks clear` is NOT a leak (only
  ambiguous header, no structural).
- `cascade_leak_current_builder_ambiguous_plus_structural_trips` —
  `[Past exchange]\n…\n## Current Date\n…` trips (the 1+1 branch
  that was previously dead).
houko added a commit that referenced this pull request May 30, 2026
…prompt builder (#5351)

The drift-pin test added by PR #5344
(`silent_response::tests::thematic_and_scaffold_headers_match_prompt_builder_output`)
surfaced that `THEMATIC_HEADERS` and `SCAFFOLD_ONLY_HEADERS` in
`crates/librefang-runtime/src/silent_response.rs` were calibrated
against five legacy strings — `## Sender`, `## Today`, `## Calendar`,
`## Tasks`, `## Response Style` — none of which the post-#5053 prompt
builder emits.

Consequence: the `1 structural + 1 thematic` branch of
`is_cascade_leak` was effectively dead, and the detector silently
degraded to a `2+ structural` rule. The canonical chat-template
regurgitation shape from today's builder — `## Persona\n…\n## Memory`
— had zero thematic hits and no longer tripped on its own, exactly
the class of cross-chat leak the detector exists to catch.

This reconciles the two lists with the actual headers enumerated by
`prompt_builder.rs`:

- 21 current-builder strings move into `SCAFFOLD_ONLY_HEADERS` (pure
  prompt-frame; no genuine reply emits a `## Persona\n` line-leader).
- `## Current Date` is the lone ambiguous-subset entry, kept in
  `THEMATIC_HEADERS` but intentionally NOT in `SCAFFOLD_ONLY_HEADERS`
  — a legitimate "what's today?" reply may produce it.
- The legacy five stay in both lists as a defensive belt-and-suspenders
  layer for any deployment still on the pre-#5053 builder. Cheap to
  leave; same false-positive risk as before.

The drift-pin snapshot is flipped from `(marker, false)` for the
legacy five to `(marker, true)` for the 21 current strings; the
legacy five remain `false`. The two cross-check loops the snapshot
test ships ensure no list can be edited without updating the snapshot
in lock-step and vice versa — drift like this can't recur silently.

Regression tests added in `silent_response::tests`:

- `cascade_leak_current_builder_scaffold_only_pair_trips` —
  `## Persona\n…\n## Memory` and two other current-builder pairs
  trip (2 structural-equivalent hits).
- `cascade_leak_current_builder_scaffold_plus_envelope_trips` —
  `[Group message from …]\n## Persona\n…` trips (1 envelope-structural
  + 1 scaffold-structural-equivalent).
- `cascade_leak_current_builder_ambiguous_alone_legitimate` —
  `## Current Date\n…\nyour day looks clear` is NOT a leak (only
  ambiguous header, no structural).
- `cascade_leak_current_builder_ambiguous_plus_structural_trips` —
  `[Past exchange]\n…\n## Current Date\n…` trips (the 1+1 branch
  that was previously dead).

Co-authored-by: Federico Liva <[email protected]>
Co-authored-by: Evan <[email protected]>
houko added a commit that referenced this pull request May 30, 2026
…log gen (#5924)

extract_pr_numbers grepped every #N on a oneline subject, so in-title
cross-references (issue refs like 'fixes #5740', prior-PR refs like
'post-#5053', and 'part N of M' markers like '(#2)') were treated as
their own PRs and fed to 'gh pr view', pulling unrelated ancient or
unmerged PRs into the generated release notes.

A squash merge always appends the PR reference as the trailing (#N) of
the subject, so parse only the last match per line. Split the parsing
into a pure parse_pr_numbers helper with unit coverage.

Co-authored-by: Evan <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/ci CI/CD and build tooling area/docs Documentation and guides area/runtime Agent loop, LLM drivers, WASM sandbox ready-for-review PR is ready for maintainer review size/XL 1000+ lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

librefang-runtime is a god-crate (agent_loop 10k LOC, tool_runner 9.7k LOC) — extract subsystems

1 participant