Skip to content

Bump docker/build-push-action from 6 to 7 - #1

Closed
dependabot[bot] wants to merge 1 commit into
mainfrom
dependabot/github_actions/docker/build-push-action-7
Closed

dependabot[bot] wants to merge 1 commit into
mainfrom
dependabot/github_actions/docker/build-push-action-7

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Mar 12, 2026

Copy link
Copy Markdown
Contributor

Bumps docker/build-push-action from 6 to 7.

Release notes

Sourced from docker/build-push-action's releases.

v7.0.0

Full Changelog: docker/build-push-action@v6.19.2...v7.0.0

v6.19.2

Full Changelog: docker/build-push-action@v6.19.1...v6.19.2

v6.19.1

Full Changelog: docker/build-push-action@v6.19.0...v6.19.1

v6.19.0

Full Changelog: docker/build-push-action@v6.18.0...v6.19.0

v6.18.0

[!NOTE] Build summary is now supported with Docker Build Cloud.

Full Changelog: docker/build-push-action@v6.17.0...v6.18.0

v6.17.0

[!NOTE] Build record is now exported using the buildx history export command instead of the legacy export-build tool.

Full Changelog: docker/build-push-action@v6.16.0...v6.17.0

v6.16.0

... (truncated)

Commits
  • d08e5c3 Merge pull request #1479 from docker/dependabot/npm_and_yarn/docker/actions-t...
  • cbd2dff chore: update generated content
  • f76f51f chore(deps): Bump @​docker/actions-toolkit from 0.78.0 to 0.79.0
  • 7d03e66 Merge pull request #1473 from crazy-max/rm-deprecated-envs
  • 98f853d chore: update generated content
  • cadccf6 remove deprecated envs
  • 03fe877 Merge pull request #1478 from docker/dependabot/github_actions/docker/setup-b...
  • 827e366 chore(deps): Bump docker/setup-buildx-action from 3 to 4
  • e25db87 Merge pull request #1474 from crazy-max/rm-export-build-tool
  • 1ac2573 Merge pull request #1470 from crazy-max/node24
  • Additional commits viewable in compare view

Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)

Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6 to 7.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](docker/build-push-action@v6...v7)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot @github

dependabot Bot commented on behalf of github Mar 12, 2026

Copy link
Copy Markdown
Contributor Author

Labels

The following labels could not be found: ci. Please create it before Dependabot can add it to a pull request.

Please fix the above issues or remove invalid values from dependabot.yml.

@houko houko closed this Mar 12, 2026
@dependabot @github

dependabot Bot commented on behalf of github Mar 12, 2026

Copy link
Copy Markdown
Contributor Author

OK, I won't notify you again about this release, but will get in touch when a new version is available. If you'd rather skip all updates until the next major or minor version, let me know by commenting @dependabot ignore this major version or @dependabot ignore this minor version. You can also ignore all major, minor, or patch releases for a dependency by adding an ignore condition with the desired update_types to your config file.

If you change your mind, just re-open this PR and I'll resolve any conflicts on it.

@dependabot
dependabot Bot deleted the dependabot/github_actions/docker/build-push-action-7 branch March 12, 2026 01:59
houko added a commit that referenced this pull request Mar 18, 2026
…canning alert

The help text contained a sample URI (user:password@cluster) that
triggered GitHub secret scanning alert #1.  Replace with angle-bracket
placeholders so it is no longer flagged.
f-liva referenced this pull request in f-liva/librefang Mar 19, 2026
…canning alert

The help text contained a sample URI (user:password@cluster) that
triggered GitHub secret scanning alert #1.  Replace with angle-bracket
placeholders so it is no longer flagged.
houko added a commit that referenced this pull request Apr 14, 2026
Three real issues found while reviewing the original commit on this branch:

1. **`/api/status` still reachable.** The status handler at routes/config.rs:48
   returns the full agents listing (id, name, state, model provider, profile)
   plus `home_dir`, `api_listen`, session count and memory usage — exactly
   the enumeration surface `require_auth_for_reads` exists to close. The
   original commit kept it in `always_public_method_free`, so flipping the
   flag did not actually stop `/api/status` from leaking. Moved it into
   `dashboard_read_exact` (still GET-only) so the flag locks it down.

2. **Flag silently no-ops when only user_api_keys or dashboard credentials
   are configured.** The gate was `require_auth_for_reads && api_key_present`
   where `api_key_present = !api_key.trim().is_empty()`. Production
   deployments commonly configure per-user keys or dashboard user/pass
   without a standalone api_key — in those cases, flipping the flag did
   nothing. Replaced with the full "any auth configured" predicate:
   `api_key || !user_api_keys.is_empty() || dashboard_auth_enabled`, which
   mirrors the existing auth-bypass check a few lines below.

3. **No operator feedback when flag is on but no auth exists.** If
   `require_auth_for_reads = true` is set without any auth configured, the
   flag is (correctly) a no-op at the middleware layer — but the operator
   gets no signal. Added a startup `warn!` in `build_router` so the misconfig
   is visible in logs at boot.

Tests added:

- `test_require_auth_for_reads_blocks_api_status` — locks in the fix for #1.
- `test_require_auth_for_reads_engages_with_user_api_keys_only` — locks in
  the fix for #2 (both the 401 on missing creds and the 200 on a valid
  per-user key).
- `test_require_auth_for_reads_is_noop_without_any_auth` — pins the
  middleware contract for #3 (the warning handles UX, middleware stays
  permissive when no auth backends exist).

Verified: `cargo test -p librefang-api --lib middleware` — 20 passed (7 new
for this feature) and `cargo clippy -p librefang-api -p librefang-types
--all-targets -- -D warnings` clean.
houko added a commit that referenced this pull request Apr 14, 2026
…2398)

* feat(api): add require_auth_for_reads flag to lock down dashboard reads

The dashboard public-read allowlist in the auth middleware hard-codes
/api/agents, /api/config, /api/budget, /api/sessions, /api/approvals,
/api/hands, /api/skills, /api/workflows and more as GET-public so the
SPA can render before the user enters credentials. With the default
api_listen = "0.0.0.0:4545", any host on the LAN can enumerate agents,
read configuration (minus redacted api_key), observe spend, and list
pending approvals without a token — a hard mismatch with the
"Bearer token authentication" line in SECURITY.md.

Introduce KernelConfig.require_auth_for_reads (default false, so
existing deployments keep rendering unchanged). When it is true AND
api_key is configured, the middleware collapses the allowlist to the
static-asset / OAuth / health subset and forces every dashboard read
through bearer authentication. Unauthenticated health probes, OAuth
callback, and dashboard shell HTML remain reachable.

Refactor the public-path match into matches!()-based groups so clippy
(nonminimal_bool) stays quiet and the two tiers are obviously
separated in code review.

Regression coverage:
- require_auth_for_reads=true blocks unauthenticated GET /api/agents
- require_auth_for_reads=true still allows the correct bearer
- /api/health stays public with the flag on
- require_auth_for_reads=false preserves the legacy public GET

* fix(api): close require_auth_for_reads gaps found in self-review

Three real issues found while reviewing the original commit on this branch:

1. **`/api/status` still reachable.** The status handler at routes/config.rs:48
   returns the full agents listing (id, name, state, model provider, profile)
   plus `home_dir`, `api_listen`, session count and memory usage — exactly
   the enumeration surface `require_auth_for_reads` exists to close. The
   original commit kept it in `always_public_method_free`, so flipping the
   flag did not actually stop `/api/status` from leaking. Moved it into
   `dashboard_read_exact` (still GET-only) so the flag locks it down.

2. **Flag silently no-ops when only user_api_keys or dashboard credentials
   are configured.** The gate was `require_auth_for_reads && api_key_present`
   where `api_key_present = !api_key.trim().is_empty()`. Production
   deployments commonly configure per-user keys or dashboard user/pass
   without a standalone api_key — in those cases, flipping the flag did
   nothing. Replaced with the full "any auth configured" predicate:
   `api_key || !user_api_keys.is_empty() || dashboard_auth_enabled`, which
   mirrors the existing auth-bypass check a few lines below.

3. **No operator feedback when flag is on but no auth exists.** If
   `require_auth_for_reads = true` is set without any auth configured, the
   flag is (correctly) a no-op at the middleware layer — but the operator
   gets no signal. Added a startup `warn!` in `build_router` so the misconfig
   is visible in logs at boot.

Tests added:

- `test_require_auth_for_reads_blocks_api_status` — locks in the fix for #1.
- `test_require_auth_for_reads_engages_with_user_api_keys_only` — locks in
  the fix for #2 (both the 401 on missing creds and the 200 on a valid
  per-user key).
- `test_require_auth_for_reads_is_noop_without_any_auth` — pins the
  middleware contract for #3 (the warning handles UX, middleware stays
  permissive when no auth backends exist).

Verified: `cargo test -p librefang-api --lib middleware` — 20 passed (7 new
for this feature) and `cargo clippy -p librefang-api -p librefang-types
--all-targets -- -D warnings` clean.

* fix(api): also lock /api/health/detail behind require_auth_for_reads

Follow-up finding from self-review. `/api/health/detail`'s own handler doc
comment at routes/config.rs:317 says "requires auth", but the middleware
allowlist had it in the always-public set. The handler returns operational
data that should not be reachable from a cold probe:

- `panic_count` / `restart_count` from the supervisor
- `agent_count`
- `embedding_provider` / `embedding_model` / `extraction_model` (infra leak)
- `config_warnings` — the full output of `KernelConfig::validate()`,
  which can tell a remote attacker exactly what's wrong with the deployment
- event-bus `dropped_events` count

Moved to `dashboard_read_exact` so it gets locked down when the flag is on.
`/api/health` stays public because its payload is genuinely minimal
(`status`, `version`, two-item `checks` array) and load balancers /
orchestrators need it for probing.

Test added: `test_require_auth_for_reads_blocks_api_health_detail` — pins
both contracts (/api/health stays public, /api/health/detail becomes
auth-required) in a single test.

Note: this is a partial fix. With the flag OFF, `/api/health/detail` stays
public to preserve backwards compatibility, which means the handler's own
"requires auth" doc comment is still being violated in the default
configuration. Making it always-require-auth is a separate behavioural
change that belongs in its own PR.

Tests: `cargo test -p librefang-api --lib middleware` — 21 passed
(8 for require_auth_for_reads, up from 7).

* fix(api): close residual info leaks in unauthenticated endpoints

Two more findings from self-review of the always-public set:

1. **`/api/auth/dashboard-check` echoed the configured dashboard username
   to anonymous callers.** The SPA uses this endpoint before the user has
   logged in (to pick the right login form), so the route is legitimately
   unauthenticated — but returning `"username": "<admin>"` handed an
   anonymous remote caller one half of the credential pair, enabling
   targeted credential stuffing against `/auth/dashboard-login`. The
   `mode` field is sufficient for the SPA to pick the right login form;
   the user already knows their own username. Now always returns an empty
   string.

2. **`/api/version` echoed the machine hostname.** Version endpoints
   conventionally expose build info, but the hostname is a per-machine
   identifier that lets a remote probe correlate a daemon to a specific
   deployment target. Dropped from the payload. Operators who need the
   hostname should read it from the daemon's shell environment.

No tests referenced either field, `cargo test -p librefang-api --lib`
passes with 262 tests (no behaviour change for the dashboard SPA beyond
needing the user to type their username at login time, which is how
every other dashboard already works).

Residual pre-existing gap noted for follow-up: `/api/health/detail`'s own
doc comment says it "requires auth" but the middleware kept it public
until this PR's flag was added. With the flag off it's still public,
honouring backwards compatibility for existing operator probes. Making
it unconditionally auth-required is a separate behavioural change.

* fix(api): make /api/health/detail always require auth, matching its doc

The handler at routes/config.rs:317 documents itself as
"Full health diagnostics (requires auth)" and `/api/health`'s doc
explicitly says "Use GET /api/health/detail for full diagnostics
(requires auth)". The middleware was the only thing still treating it as
public — a pre-existing mismatch that this PR's first pass only partially
closed by flag-gating.

Remove it from both `always_public` and `dashboard_read_exact`. With the
endpoint in neither public list, the middleware's default auth-required
path handles it, so `/api/health/detail` now requires a bearer token
regardless of `require_auth_for_reads`. The handler contract and the
middleware contract finally agree.

Breaking change for operators: if anyone was probing `/api/health/detail`
without auth, they'll start getting 401. `/api/health` (minimal liveness)
stays public for load balancers and orchestrators, so the standard
deployment probe path is unaffected. Monitoring setups that want the
detailed view should configure a bearer token — that was the original
design intent.

Test replaced: `test_api_health_detail_always_requires_auth` now pins
both directions — `/api/health` stays public with the flag off, and
`/api/health/detail` is 401 regardless of whether the flag is on or off.

Tests: `cargo test -p librefang-api --lib middleware` — 21 passed.
houko pushed a commit that referenced this pull request Apr 14, 2026
…2503)

* feat(gateway): add lib/identity.js identity normalization module (ID-01)

- Pure functional module: isLidJid, isGroupJid, normalizeDeviceScopedJid,
  extractE164, phoneToJid, resolvePeerId, deriveOwnerJids
- No side effects on require, no hidden state, cache passed as param
- resolvePeerId honors 5-step heuristic from CONTEXT §A Specifics
- Unit tests: 41 assertions across 7 describe blocks

* refactor(gateway): import lib/identity in index.js

Add import of identity helpers next to existing lib/echo-tracker import.
No call-site changes yet — subsequent commits migrate sites one at a time.

* refactor(gateway): use deriveOwnerJids for OWNER_JIDS

Replace inline Set(map(n => n.replace(/^\+/, '') + '@s.whatsapp.net'))
with the module helper. Behavior-preserving.

* refactor(gateway): use phoneToJid for outbound JID normalization

Unify the 3 identical inline copies in sendMessage/sendImage/sendAudio
(they were verbatim duplicates; diff confirmed identical). Group JIDs
still passthrough, phones still coerced to @s.whatsapp.net form.

* refactor(gateway): use resolvePeerId + extractE164 in main cascade (ID-01, ID-03)

- Main sender-resolution cascade (~line 1171-1197) replaced with a single
  resolvePeerId() call that honors the 5-step heuristic from CONTEXT §A.
- Phone derivation via extractE164() — also fixes latent bug where
  device-scoped incoming JIDs ('123:45@s.whatsapp.net') previously yielded
  malformed '+123:45' phone strings; now correctly yields '+123'.
- console.warn for unresolved identity becomes structured JSON per ID-03:
  {event: 'identity_unresolved', jid, reason, lid_cache_size, confidence}.
- Preserved side effects outside the pure function per §Concerns #1:
  senderPn cache write and CS-02 resolveLidProactively both still run
  BEFORE resolvePeerId — the function only resolves, it never writes.
- Renamed local 'isLidJid' (shadowed module fn) to 'isLid' per §Concerns
  #2 — all downstream references (ownerLidJids check at line ~1211)
  updated to the alias.

* refactor(gateway): use identity helpers in catchup path

- isLidJid/isGroupJid module fns replace inline endsWith checks.
- phoneToJid replaces inline '+'-strip + '@s.whatsapp.net' append.

* refactor(gateway): use isGroupJid in getGroupParticipants guard

Final inline endsWith('@g.us') site removed. Zero inline JID suffix
manipulation remains in index.js.

* test(gateway): equivalence + ID-03 log tests for identity refactor

- 12 equivalence assertions comparing pre-refactor inline logic to
  lib/identity helpers across: isLid, isGroup, deriveOwnerJids,
  phoneToJid, resolvePeerId (5 fixture shapes: plain phone, LID+senderPn,
  LID+cache, LID+participant, LID unresolvable), extractE164 device-strip
  latent fix, normalizeDeviceScopedJid passthrough.
- 1 assertion validating ID-03 identity_unresolved JSON log shape
  (event, jid, reason, lid_cache_size, confidence all present).
- Test count: 129 -> 142.

---------

Co-authored-by: Federico Liva <federico.liva@espero.it>
houko added a commit that referenced this pull request Apr 18, 2026
…ation

Fifth Codex batch on #2704 — covers the skill-evolution subsystem
(code the upstream PR #2694 shipped to main but that shows up in
this PR's diff), plus the KV race on worker telemetry writes.

librefang-skills (evolution.rs):
- fuzzy_find_and_replace rejects empty old_string up front (#16).
- update_skill / patch_skill re-verify skill_dir.exists() under
  the lock so concurrent delete_skill can't be resurrected (#3, #5).
- delete_skill runs validate_name to reject path traversal (#12).
- remove_supporting_file canonicalises + contains-checks the
  target (#6), matching write_supporting_file.
- write_supporting_file scans content BEFORE writing (#7) so a
  rejected update doesn't destroy the pre-existing valid file.

librefang-runtime (tool_runner.rs):
- tool_skill_evolve_delete resolves the real installed skill's
  parent dir via registry.get() (#15) instead of unconditionally
  targeting the global skills_dir.

librefang-types / librefang-kernel (approval policy):
- Default require_approval list includes skill_evolve_* (#14).
  Updated serde `true`-shorthand + tests to match.

librefang-kernel (kernel/mod.rs):
- build_skill_summary sanitises the category key (#1) — tags are
  third-party data.
- extract_json_from_llm_response tries every '{' instead of giving
  up after the first (#8).
- Background skill review uses the agent's resolved driver and
  model for cost attribution (#13); falls back to defaults only
  when manifest resolution fails.

worker (github-stats-worker/index.js):
- Click counts sharded across 8 blobs, unioned on read (#39).
- UI error reports sharded across 4 blobs plus legacy-key fallback
  for historical data (#41).
houko added a commit that referenced this pull request Apr 19, 2026
…nMode + config toggle

Codex review on PR #2767 caught three real bugs my self-review missed
— they're all in the kernel wrapper around agent_loop, not in
agent_loop itself, which was the only surface I'd systematically
audited last round. Fixes:

1. **Kernel persistence leakage into fork turns** (P1, codex
   #3106080994 + #3106228335).

   `send_message_streaming_with_sender_and_opts` runs three post-loop
   writes outside of agent_loop that had no `is_fork` guard:

   - `memory.append_canonical(...)` — would write fork messages to the
     agent's canonical session (cross-channel memory layer).
   - `memory.write_jsonl_mirror(...)` — persists the fork session to
     the agent's workspace `sessions/` directory.
   - `append_daily_memory_log(...)` — writes the fork's final response
     into the workspace's daily log.

   Plus two compaction hooks that mutate canonical session on disk:

   - Pre-loop `compact_agent_session_with_id(...)` when the session
     crosses the threshold.
   - Post-loop (background-spawned) compaction trigger.

   All five now gated on `!loop_opts.is_fork`. Metering, quota, and
   tool-call accounting intentionally still fire for forks — those
   tokens were really consumed and should charge the agent's budget.

2. **SessionMode::New bypass** (P2, codex #3106259704).

   `send_message_streaming_with_sender_and_opts` selects the effective
   session via `match manifest.session_mode { Persistent => canonical,
   New => SessionId::new() }`. For an agent configured with
   `session_mode = "new"`, forks were landing on a fresh empty
   `SessionId::new()` — which means the fork's request prefix didn't
   match the parent's and Anthropic's prompt cache missed. Fork mode
   now forces Persistent / canonical regardless of the manifest.

3. **Global prompt_caching toggle not respected in extractor fallback**
   (P2, codex #3106259706).

   `LlmMemoryExtractor`'s fallback `driver.complete()` path (used
   when no kernel handle is installed, or when the fork call fails)
   hardcoded `prompt_caching: true`, overriding the operator's
   `KernelConfig.prompt_caching` setting. Added `prompt_caching`
   field to the extractor; `init_proactive_memory_full_with_extractor`
   takes it and the kernel threads `cfg.prompt_caching` through. The
   fork path already inherited this correctly because it reads the
   agent's manifest metadata that the kernel derives from the same
   global.

4. **Experiment metrics polluted by fork turns** (caught while sweeping
   for related issues, not in the codex review).

   `record_experiment_request` was firing for forks, distorting
   A/B-test latency / success / cost averages with derivative-call
   data. Gated on `!loop_opts.is_fork`. Token/cost accounting still
   runs for forks (see #1).

Also addresses codex #3106269431 (recursion): that one was about the
commit before `fae5ed54`, which already fixed it. Comment is stale.

Tests: auto_dream 25/25, kernel + workspace clippy `-D warnings`
clean. Self-audit note: next time I claim "逻辑完整" I need to walk
the full call chain from KernelHandle entry through agent_loop
return, not just the agent_loop internals — the kernel wrapper has
substantial persistence side effects that I was skipping.
houko added a commit that referenced this pull request Apr 19, 2026
…2767)

* feat(forked-agent): derivative LLM calls reuse parent's prompt cache

Add `kernel.run_forked_agent_streaming(agent_id, prompt, allowed_tools)`
for derivative LLM calls that want to fire on top of an agent's current
context without persisting the derivative's own messages back to the
canonical session. auto-dream is migrated over as the first consumer;
auto_memorize and future post-turn features can use the same API.

Mechanism:

- The fork runs through the same `run_agent_loop_streaming` path as a
  normal main turn, but with a new `LoopOptions` bundle:
  - `is_fork: true` skips every `save_session_async` call (12 sites
    across non-streaming / streaming, circuit-break / max-tokens /
    max-iterations / timeout paths) so derivative messages never touch
    the canonical session on disk, and
  - injects `"is_fork": true` into each `AgentLoopEnd` hook context
    data (8 sites) — auto-dream's own hook skips fork turns so a
    dream doesn't trigger a nested dream on its own completion.
- `allowed_tools: Some(Vec<String>)` enforces a runtime allowlist at
  tool *execute* time inside `execute_single_tool_call`. The request
  schema is NOT pre-filtered — keeping it byte-identical to the
  parent turn is what lets Anthropic's prompt cache hit. Prompt-
  injected forks that try a non-allowed tool get a synthetic error
  result back and can't actually invoke anything — same defense-in-
  depth pattern libre-code uses via `createAutoMemCanUseTool`.
- Anthropic driver's `cache_control` marker extended from system-only
  to ALSO cover the last tool block so the (system + tools) prefix
  caches as one unit.

auto-dream migration:

- `run_dream` now calls `kernel.run_forked_agent_streaming` instead of
  `send_message_streaming_with_sender_context_and_routing(channel =
  AUTO_DREAM_CHANNEL, ...)`. The `SenderContext` magic-channel dance
  is gone.
- The kernel-side request-time filter that stripped non-memory tools
  when `sender.channel == AUTO_DREAM_CHANNEL` is removed — that job
  is now done by `LoopOptions::allowed_tools` at execute time, which
  both honours cache alignment and covers non-dream forks too.
- `AutoDreamTurnEndHook::on_event` checks `ctx.data["is_fork"]` and
  bails on fork turns so a dream's own AgentLoopEnd doesn't try to
  schedule another dream.

Tests: auto_dream 25 passed; Anthropic driver 3 new tests covering
last-tool cache_control placement, caching-off path, and empty-tools
edge case; runtime --lib 1034 passed (unrelated pre-existing
media_understanding::transcode_empty_input_errors failure depends on
ffmpeg env). Clippy workspace --all-targets clean.

Docs: memory page (en + zh) "how a dream runs" updated to describe
the fork-from-canonical-session model and execute-time tool allowlist.
CHANGELOG entry added.

* feat(forked-agent): extend Anthropic cache_control to messages + dashboard viz

Two additions to the forked-agent infrastructure landed in the
previous commit:

1. Anthropic driver's cache_control was stamped on system and the
   last tool; now also on the last content block of the last
   message. This lifts cache coverage from (system + tools) —
   ~10-30% of a typical multi-turn conversation — to (system + tools
   + entire message history), i.e. everything except the new trailing
   user/fork prompt. For forks this means parent's final assistant
   reply caches, so the dream's own pass hits on the full preceding
   conversation instead of just the schema prefix.

   Plain-string `ApiContent::Text` messages are upgraded to
   single-block `Blocks` form when marking the last message's block,
   because Anthropic only accepts `cache_control` on structured
   content (rejects it on shorthand strings). Upgrade is lossless —
   `{type: text, text: "..."}` is API-equivalent to the raw string.

   Two new driver tests: marker placement on last-block-of-last-
   message, and absent-when-caching-off (ensures we don't leak markers
   to non-Anthropic providers).

2. Dashboard Settings page now shows cache-hit rate + provider cost
   in the dream progress row. Reads `AutoDreamProgress.usage` (new
   field in the TS type, mirrors Rust `DreamUsage`) and derives hit%
   as `cache_read / (cache_read + cache_creation + input)`. Without
   this, the whole forkedAgent cost win would only be visible via
   audit-event digging — now it's at the surface, so operators can
   confirm the optimisation worked without leaving the dashboard.
   Two i18n strings added (en + zh) for the Cache label and the two
   `title` tooltips.

Not done (deliberate):

- auto_memorize migration to forkedAgent. Turns out this isn't a
  simple port — auto_memorize goes through ProactiveMemoryStore's
  own extractor (rule-based OR LLM via a separate code path in
  `proactive.rs`), which returns a structured `ExtractionResult` with
  already-parsed memories/relations/conflicts. Moving it to
  forkedAgent means replacing that entire extractor pipeline with a
  forked agent that uses memory_store as a tool — a redesign of the
  proactive-memory flow, not a 30-line swap. Worth doing but belongs
  in its own PR with proper API design. Tracked for follow-up.

Tests: driver lib 262 passed. Kernel auto_dream 25 passed. Clippy
workspace --all-targets --D-warnings clean.

* perf(proactive-memory): enable prompt_caching on auto_memorize LLM calls

`LlmMemoryExtractor`'s `extract_memories` and `decide_action` both use
stable system prompts (`EXTRACTION_SYSTEM_PROMPT`, ~1KB and
`DECISION_SYSTEM_PROMPT`) across every invocation. The user message
varies — it's the specific conversation text or candidate memory — but
the system block is identical. Flipping `prompt_caching: false → true`
on both request constructions lets Anthropic cache the system block
and subsequent calls within the 5-min TTL read cache instead of
re-billing at full rate. Each system-prompt cache hit saves ~250
tokens × whatever the active agent's auto_memorize rate is (typically
once per user turn).

Non-Anthropic providers ignore the flag (OpenAI caches automatically,
others no-op), so enabling it is safe cross-provider.

Does NOT migrate auto_memorize to the forkedAgent pattern itself.
Deliberate, with honest reasoning:

  LlmMemoryExtractor is a structured-output LLM call (returns
  `{memories: [...], relations: [...]}` JSON that gets parsed,
  followed by an ADD/UPDATE/NOOP decision step). forkedAgent gives
  you a tool-using agent loop — a different usage shape. Migrating
  auto_memorize properly means rewriting its extractor as a
  tool-using agent that invokes `memory_store` directly (libre-code's
  `extractMemories` shape), which throws out the current
  structured-output flow and the ADD/UPDATE/NOOP decision logic.
  That's a ~600-line redesign with its own test surface and deserves
  its own PR — not a shortcut appended to this one.

This small change captures most of the realistic Anthropic savings
auto_memorize could get without the redesign. Full migration tracked
as a follow-up.

* feat(proactive-memory): migrate auto_memorize extractor to forkedAgent

Routes the LlmMemoryExtractor's LLM call through the kernel's
`run_forked_agent_oneshot` trait method so the extractor's request
shares the parent agent's (system + tools + messages) cache key. On
Anthropic this means auto_memorize's per-turn extraction call hits
the prompt cache for the full conversation prefix, not just its own
system block.

Shape:

- `KernelHandle::run_forked_agent_oneshot(agent_id, prompt,
  allowed_tools)` — new trait method, default errors out. Real kernel
  impl spawns `run_forked_agent_streaming`, drains the stream, and
  returns the final text. `allowed_tools = Some(vec![])` keeps the
  fork single-turn (no tool calls) — the model returns JSON which
  the extractor parses as usual.
- `MemoryExtractor::extract_memories_with_agent_id` — new trait
  method, default forwards to `extract_memories(messages)`.
  LlmMemoryExtractor overrides to call the fork path when it has
  a kernel handle installed.
- `LlmMemoryExtractor::install_kernel_handle` — called from
  `LibreFangKernel::set_self_handle` once `Arc<Self>` exists (same
  place the auto-dream hook gets wired). Extractor holds the weak
  ref in a `RwLock<Option<Weak<dyn KernelHandle>>>` since it's late-
  bound by design.
- `init_proactive_memory_full_with_extractor` — new init variant
  that returns the concrete `Arc<LlmMemoryExtractor>` alongside the
  store so the kernel can install its weak handle later.

Extraction prompt change: the fork's user message embeds
`EXTRACTION_SYSTEM_PROMPT` followed by the conversation text. The
fork's system prompt stays the agent's own — we can't replace it
without breaking cache alignment. Agents with moderately large
system prompts (~2KB) break even; larger ones are net positive.
Rule-based DefaultMemoryExtractor ignores agent_id via the trait's
default method, so kernels without LLM extraction are unaffected.

Fallback: when no kernel handle is installed (tests / rule-based
extractor / fork call error) falls back to the standalone
`driver.complete()` path which still has `prompt_caching = true`
from the previous commit — system prompt still caches.

Tests: auto_dream 25/25. Clippy clean. Live integration
verification needs an Anthropic key to confirm cache hit rate on
auto_memorize requests.

* fix(forked-agent): gate post-turn side effects on !is_fork

Logic review caught a recursion bug: fork turns' finalize_successful_end_turn
was still calling auto_memorize, which in turn spawns run_forked_agent_oneshot
— so a fork completing would trigger another fork, ad infinitum. The file
lock would eventually save us by rejecting the new acquire, but we'd still
waste tokens and log errors on every cascade.

Gate five post-turn side effects on !ctx.opts.is_fork:

1. `auto_retrieve` in setup_recalled_memories — for forks, injecting
   memory fragments into the prompt would break cache alignment with
   the parent (byte-identical messages is the whole point). Also
   semantically wrong: a fork's context should exactly match parent's.
2. `remember_interaction_best_effort` after final response — forks
   are ephemeral; their conversation shouldn't leak into the long-term
   memory bank.
3. `context_engine.after_turn` — per-turn engine state (summary
   chains, token budgets) shouldn't be advanced by a derivative turn
   that doesn't count as a real user interaction.
4. `auto_memorize` — critical; this was the recursion source. Fork
   turns skip, and the fork's own completion therefore doesn't fire
   another auto_memorize.
5. Added `opts` field to `RecallSetupContext` so the pre-turn
   auto_retrieve gate has access to it; threaded through both
   streaming and non-streaming callers.

Also added `is_fork = opts.is_fork` to the "agent loop completed" info
log so the distinction is visible in traces.

Without these gates the fix from the previous commit was still unsafe:
the first fork would complete, then on its way out its finalize would
call auto_memorize, which would spawn another fork, which would also
complete-then-spawn. Lock acquire would reject at fork N+1 but the
damage (token burn + log spam) would already be done.

Tests: auto_dream 25/25, memory 114/2 passed. Clippy `-D warnings`
clean workspace-wide.

* fix(forked-agent): address codex review — kernel persistence + SessionMode + config toggle

Codex review on PR #2767 caught three real bugs my self-review missed
— they're all in the kernel wrapper around agent_loop, not in
agent_loop itself, which was the only surface I'd systematically
audited last round. Fixes:

1. **Kernel persistence leakage into fork turns** (P1, codex
   #3106080994 + #3106228335).

   `send_message_streaming_with_sender_and_opts` runs three post-loop
   writes outside of agent_loop that had no `is_fork` guard:

   - `memory.append_canonical(...)` — would write fork messages to the
     agent's canonical session (cross-channel memory layer).
   - `memory.write_jsonl_mirror(...)` — persists the fork session to
     the agent's workspace `sessions/` directory.
   - `append_daily_memory_log(...)` — writes the fork's final response
     into the workspace's daily log.

   Plus two compaction hooks that mutate canonical session on disk:

   - Pre-loop `compact_agent_session_with_id(...)` when the session
     crosses the threshold.
   - Post-loop (background-spawned) compaction trigger.

   All five now gated on `!loop_opts.is_fork`. Metering, quota, and
   tool-call accounting intentionally still fire for forks — those
   tokens were really consumed and should charge the agent's budget.

2. **SessionMode::New bypass** (P2, codex #3106259704).

   `send_message_streaming_with_sender_and_opts` selects the effective
   session via `match manifest.session_mode { Persistent => canonical,
   New => SessionId::new() }`. For an agent configured with
   `session_mode = "new"`, forks were landing on a fresh empty
   `SessionId::new()` — which means the fork's request prefix didn't
   match the parent's and Anthropic's prompt cache missed. Fork mode
   now forces Persistent / canonical regardless of the manifest.

3. **Global prompt_caching toggle not respected in extractor fallback**
   (P2, codex #3106259706).

   `LlmMemoryExtractor`'s fallback `driver.complete()` path (used
   when no kernel handle is installed, or when the fork call fails)
   hardcoded `prompt_caching: true`, overriding the operator's
   `KernelConfig.prompt_caching` setting. Added `prompt_caching`
   field to the extractor; `init_proactive_memory_full_with_extractor`
   takes it and the kernel threads `cfg.prompt_caching` through. The
   fork path already inherited this correctly because it reads the
   agent's manifest metadata that the kernel derives from the same
   global.

4. **Experiment metrics polluted by fork turns** (caught while sweeping
   for related issues, not in the codex review).

   `record_experiment_request` was firing for forks, distorting
   A/B-test latency / success / cost averages with derivative-call
   data. Gated on `!loop_opts.is_fork`. Token/cost accounting still
   runs for forks (see #1).

Also addresses codex #3106269431 (recursion): that one was about the
commit before `fae5ed54`, which already fixed it. Comment is stale.

Tests: auto_dream 25/25, kernel + workspace clippy `-D warnings`
clean. Self-audit note: next time I claim "逻辑完整" I need to walk
the full call chain from KernelHandle entry through agent_loop
return, not just the agent_loop internals — the kernel wrapper has
substantial persistence side effects that I was skipping.

* fix(forked-agent): stop fork from hijacking parent's injection channel + abort handle

Two more process-local state bugs my review missed — caught by
walking the full kernel wrapper call chain this time (prompted by
justified "why are there still so many issues" feedback).

1. **Injection channel overwrite.** `setup_injection_channel(agent_id)`
   inserts into `injection_senders` and `injection_receivers` DashMaps
   keyed by agent id. Fork turns hit the same `agent_id` key, so the
   fork's setup would overwrite the parent turn's channel. External
   code trying to inject into the parent's loop during the fork
   window (e.g. user sends another message via a channel that triggers
   `injection_senders[agent_id].send(...)`) would land on the fork's
   about-to-be-dropped sender instead. Then fork's teardown removes
   the entry entirely, leaving the parent with no channel for the rest
   of its turn.

   Fix: fork turns skip `setup_injection_channel` (pass `None` into
   agent_loop's `pending_messages`) and skip `teardown_injection_channel`
   on exit.

2. **Abort handle hijack.** `running_tasks.insert(agent_id, handle)`
   registers the current spawn for `stop_agent_run` / `suspend_agent`
   to find. Fork inserting under `agent_id` overwrites the parent's
   handle — so an operator calling `stop_agent_run(agent_id)` during
   the fork window aborts the fork instead of the user's actual turn.
   Worse, the parent's handle is gone from the map, so subsequent
   stop attempts return `Ok(false)` (not running) even though the
   parent is in fact mid-await.

   Fix: fork turns don't register in `running_tasks`. The fork's
   caller (auto_memorize, dream) already holds the join handle
   directly and can cancel via task-level abort if needed.

This is a pattern that my first review should have caught but didn't
— shared-by-agent-id state in DashMaps gets silently clobbered when
the fork shares the agent_id. Both fixes gate on `is_fork`, same as
the earlier persistence fixes.

Tests: auto_dream 25/25. Clippy clean workspace-wide.

Still outstanding: live integration testing (needs real Anthropic key
to validate cache_read_input_tokens > 0 on fork calls). Everything
static-verifiable now audited.
DaBlitzStein added a commit to DaBlitzStein/librefang that referenced this pull request Aug 10, 2026
…ping, spawn depth

CRITICAL librefang#1: insert RunHandle BEFORE tokio::spawn so self-cleanup
remove_if always finds its entry. Prevents stale ghost entries.

CRITICAL librefang#2: TUI create form now renders typed text (was _value,
now value). Shows typed input in chunk[5], toggle status in step 3.

HIGH librefang#1: agent_spawn now guarded by AGENT_CALL_DEPTH (mirrors
agent_send depth guard). Prevents unbounded recursive spawn chains.

Also: added agent_spawn to ALWAYS_NATIVE_TOOLS so all agents
have it by default (was missing — agents only had agent_send).

PRD: GOAL_SYSTEM_AUDIT_PRD.md documents 28 findings (2 critical,
1 high, 18 medium, 7 low) for future work.

14/14 tests pass. 3 deploys healthy.
DaBlitzStein added a commit to DaBlitzStein/librefang that referenced this pull request Aug 11, 2026
…ping, spawn depth

CRITICAL librefang#1: insert RunHandle BEFORE tokio::spawn so self-cleanup
remove_if always finds its entry. Prevents stale ghost entries.

CRITICAL librefang#2: TUI create form now renders typed text (was _value,
now value). Shows typed input in chunk[5], toggle status in step 3.

HIGH librefang#1: agent_spawn now guarded by AGENT_CALL_DEPTH (mirrors
agent_send depth guard). Prevents unbounded recursive spawn chains.

Also: added agent_spawn to ALWAYS_NATIVE_TOOLS so all agents
have it by default (was missing — agents only had agent_send).

PRD: GOAL_SYSTEM_AUDIT_PRD.md documents 28 findings (2 critical,
1 high, 18 medium, 7 low) for future work.

14/14 tests pass. 3 deploys healthy.
DaBlitzStein added a commit to DaBlitzStein/librefang that referenced this pull request Aug 13, 2026
…ping, spawn depth

CRITICAL librefang#1: insert RunHandle BEFORE tokio::spawn so self-cleanup
remove_if always finds its entry. Prevents stale ghost entries.

CRITICAL librefang#2: TUI create form now renders typed text (was _value,
now value). Shows typed input in chunk[5], toggle status in step 3.

HIGH librefang#1: agent_spawn now guarded by AGENT_CALL_DEPTH (mirrors
agent_send depth guard). Prevents unbounded recursive spawn chains.

Also: added agent_spawn to ALWAYS_NATIVE_TOOLS so all agents
have it by default (was missing — agents only had agent_send).

PRD: GOAL_SYSTEM_AUDIT_PRD.md documents 28 findings (2 critical,
1 high, 18 medium, 7 low) for future work.

14/14 tests pass. 3 deploys healthy.
DaBlitzStein added a commit to DaBlitzStein/librefang that referenced this pull request Aug 14, 2026
…ping, spawn depth

CRITICAL librefang#1: insert RunHandle BEFORE tokio::spawn so self-cleanup
remove_if always finds its entry. Prevents stale ghost entries.

CRITICAL librefang#2: TUI create form now renders typed text (was _value,
now value). Shows typed input in chunk[5], toggle status in step 3.

HIGH librefang#1: agent_spawn now guarded by AGENT_CALL_DEPTH (mirrors
agent_send depth guard). Prevents unbounded recursive spawn chains.

Also: added agent_spawn to ALWAYS_NATIVE_TOOLS so all agents
have it by default (was missing — agents only had agent_send).

PRD: GOAL_SYSTEM_AUDIT_PRD.md documents 28 findings (2 critical,
1 high, 18 medium, 7 low) for future work.

14/14 tests pass. 3 deploys healthy.
DaBlitzStein added a commit to DaBlitzStein/librefang that referenced this pull request Aug 14, 2026
…ping, spawn depth

CRITICAL librefang#1: insert RunHandle BEFORE tokio::spawn so self-cleanup
remove_if always finds its entry. Prevents stale ghost entries.

CRITICAL librefang#2: TUI create form now renders typed text (was _value,
now value). Shows typed input in chunk[5], toggle status in step 3.

HIGH librefang#1: agent_spawn now guarded by AGENT_CALL_DEPTH (mirrors
agent_send depth guard). Prevents unbounded recursive spawn chains.

Also: added agent_spawn to ALWAYS_NATIVE_TOOLS so all agents
have it by default (was missing — agents only had agent_send).

PRD: GOAL_SYSTEM_AUDIT_PRD.md documents 28 findings (2 critical,
1 high, 18 medium, 7 low) for future work.

14/14 tests pass. 3 deploys healthy.
This was referenced Aug 31, 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