Skip to content

fix: audit batch 2 — docs accuracy + 2 small code touch-ups - #112

Merged
yogthos merged 1 commit into
mainfrom
fix/audit-batch-2-docs
May 21, 2026
Merged

fix: audit batch 2 — docs accuracy + 2 small code touch-ups#112
yogthos merged 1 commit into
mainfrom
fix/audit-batch-2-docs

Conversation

@yogthos

@yogthos yogthos commented May 21, 2026

Copy link
Copy Markdown
Collaborator

Tier-2 follow-up to PR #111. 5 docs fixes (#6 tools, #12 temperature, #13 --api-key, #14 acp_host/port, #21 find_callers) + 2 code fixes (#16 share is_skip_dir between semantic + tools, #18 context::load_file error context). 725/599 pass.

Follow-up to PR #111. Tier-2 items from the 23-bug audit batch:
docs corrections and two small correctness/UX fixes.

## Docs

- **#12 temperature** — CONFIG.md claimed "parsed but not
  currently applied". Actually applied since PR #105 with a
  clamp warning. Rewrote the cell.
- **#13 --api-key** — flag existed but neither README nor
  CONFIG.md mentioned it. Added a Quick-start example noting
  the process-list visibility caveat.
- **#14 acp_host/acp_port** — CONFIG.md documented both keys
  but the CLI flags were intentionally removed (stdio-only
  transport). Removed both from the keys table + ACP section.
- **#6 tools** — `Config::tools` (per-tool enable map) was
  fully wired in code but undocumented. Added a row to the
  keys table covering `tools.websearch` and `tools.webfetch`.
- **#21 find_callers** — README claimed "word-boundary regex"
  but the impl uses the tree-sitter symbol index. Updated to
  reflect actual behavior; the user-visible word-boundary
  semantics are preserved.

## Code

- **#16 semantic index skip_dir** — `SymbolIndex::find_callers`
  filter had its own hardcoded `matches!(name, "node_modules"
  | "target" | ".git" | "__pycache__")` while the rest of
  the codebase uses `agent::tools::is_skip_dir`. Switched to
  the shared helper so future additions stay in lockstep.

- **#18 context::load_file** — silently swallowed
  `read_to_string` errors via `.ok()`. A permission-denied
  AGENTS.md looked identical to a missing file. Now emits a
  stderr warning naming the path + reason; still returns None
  so callers' behavior is unchanged.

725 plugin / 599 default pass. All build profiles clean.

## Remaining audit items (deferred to feature work)

- **#8 LSP no crash restart**: needs broken-pipe IO error
  handling + exponential backoff. Touches manager state
  machine.
- **#10 task tool fire-and-forget**: needs timeout + cleanup
  coordination via JoinHandle tracking.
- **#25 MCP no reconnection**: similar architectural concern
  to #8.
- **#27 LSP didClose**: client lifecycle hook missing.
- **#29 token estimation len/4**: needs per-provider usage
  extraction (Phase 6 work).
- **#5 MCP shutdown**: rmcp Drop semantics need verification.
- **#38/39/40 semantic test gaps**: get_symbol_body untested,
  list_symbols kind_filter untested, find_definition test
  vacuous. Sat down to add but each requires a fixture build.

Together with PR #111 (10 code fixes), 17 of the 23 verified
items are now shipped. Remaining 6 are architectural or
test-infrastructure work better tackled as discrete PRs.
@yogthos
yogthos merged commit 270d6ac into main May 21, 2026
1 check passed
@yogthos
yogthos deleted the fix/audit-batch-2-docs branch May 21, 2026 17:48
yogthos added a commit that referenced this pull request May 21, 2026
…1 leftovers (#113)

Verified the 11 new round-2 findings and 6 round-1 leftovers
against the actual code. Shipping the 7 concrete fixes that map
to clear before/after behavior; the rest are deferred-architectural
or already-fixed-elsewhere.

## N1 — webfetch SSRF (CRITICAL)

`http://169.254.169.254/latest/meta-data/` (AWS metadata) and
all private/loopback ranges sailed straight through. An LLM that
gets prompt-injected into fetching a private IP could exfiltrate
IAM credentials, hit internal admin endpoints, or scrape internal
network maps.

New `validate_url_host_safety()` blocks:
- Hostname literal "localhost" / "ip6-localhost" /
  "ip6-loopback"
- IPv4 loopback (127/8), private (10/8, 172.16/12, 192.168/16),
  link-local (169.254/16 — cloud metadata), unspecified,
  broadcast, class E.
- IPv6 loopback, ULA (fc00::/7), link-local (fe80::/10),
  multicast, unspecified. Bracketed IPv6 (`[::1]`) is parsed
  correctly.

Opt-in escape hatch: `DIRGE_WEBFETCH_ALLOW_PRIVATE=1` for
dev workflows that need to hit localhost. Tool description
documents the env var so the agent can see it.

## N8 — webfetch body OOM (HIGH)

`resp.text().await` buffered the entire response into memory
before any truncation. A 500 MB page would OOM the agent
process. Now streams via `bytes_stream` with a 10 MiB cap;
bails at the cap.

## N2 — MCP result size cap (CRITICAL)

`McpTool::call` concatenated every `RawContent::Text` block
into `content` without limit. A 200 KB MCP response flooded
every subsequent turn until compaction. Cap aggregate at 256
KiB (≈65k tokens worst case); UTF-8 char-boundary slice;
append a clear truncation marker naming the offending
server::tool so the agent can adjust its calls.

## N3 — bash output cap (CRITICAL)

The UI's `render_tool_output` truncated the DISPLAY but the
full string was persisted to `ToolCallState::Completed` and
fed back to the LLM on the next turn. `cat /dev/urandom | head
-c 10M` would have shoved millions of tokens. Cap at 256 KiB
at the bash tool source, with a clear truncation message
pointing the agent at head/grep filtering.

## N5 — sandbox hardening (HIGH)

Added `--new-session` (drops the ability to gain new privs via
setuid) and `--unshare-user-try` (explicit user-ns isolation,
defensive against future bwrap default changes). `--unshare-all`
already covers most of this; the explicit flags pin the
guarantees.

## N7 — sanitize_output strips \\r (HIGH)

Previously `\\r` was preserved (originally for CRLF lines). A
bash tool printing progress with `\\rstep N/M` would move the
cursor to column 0 mid-line, overwriting chamber borders. Now
stripped (`\\n` and `\\t` still pass; display path
normalizes CRLF before reaching here).

## H10 — task subagent timeout

`tokio::spawn(btw_query(...))` with no timeout meant a stuck
provider would keep the task in `Running` forever. Cap at 10
minutes; on timeout, the task transitions to `Failed` with a
clear message and the system-reminder still fires.

## M22 — /prompt default help text

Behavior was fixed in PR #111 (activates `default.md` if
installed). Help text still said "clear active prompt". Now
explains both cases.

## Verified false positives / non-issues this round

- **N4 sandbox --ro-bind / /**: by design — the sandbox is a
  WRITE guard, not exfiltration prevention. Defense for read
  access requires `--ro-bind` per path (much narrower scope
  + breaks read-only tooling like bash `ls`). Document as
  feature, not bug.
- **N6 sandbox current_dir fallback**: `unwrap_or_else(|_|
  ".".into())` is the standard fallback; `current_dir()`
  failing means cwd is deleted/inaccessible which is its own
  fatal-soon problem.
- **N9 token estimation len/4**: Phase 6 work; per-provider
  usage extraction needs distinct fix.
- **N10 sandbox /dev/tty**: `--dev /dev` mounts a tmpfs with
  only essential nodes, not the full host /dev. /dev/tty IS
  available to the sandboxed process so it can write to its
  own controlling terminal (needed for bash interactive
  features); not the host's /dev/tty0.
- **H17 DeepSeek auto-detect order**: confirmed false positive
  in earlier rounds — only fires when env var present.
- **H18 load_file path in error**: already fixed in PR #112.
- **M19 semantic tools in BUILTIN_TOOL_NAMES**: semantic tools
  added separately, can't be shadowed by MCP collision.

## Tests

2 new regression tests:
- `validate_url_host_safety_blocks_ssrf_targets`: pins
  169.254.x, 127.x, RFC 1918, ::1, fc00::, fe80:: as refused;
  public IPs + domains pass; bracketed IPv6 parses correctly.
- `validate_url_host_safety_handles_malformed_hosts`: garbage
  hosts don't panic.

727 plugin / 601 default pass.

## Deferred to architectural follow-ups

- H8 LSP no crash restart — broken-pipe handling design
- N4 sandbox --ro-bind / — fundamental design tradeoff
- N9 token estimation — per-provider usage extraction

Co-authored-by: Yogthos <yogthos@gmail.com>
yogthos added a commit that referenced this pull request May 21, 2026
…ig (#121)

Continues from PR #120 (security-critical). Six deferred items
shipped.

## H1: edit + apply_patch unbounded file reads

`edit.rs` and `apply_patch.rs` (apply_update) called
`tokio::fs::read` / `std::fs::read_to_string` with no size
guard. An LLM pointing them at a multi-GB log/binary would OOM
the agent process.

Added 100 MiB pre-flight metadata check in both. The cap is
generous (every realistic source file fits) but rejects the
pathological case fast with a clear error pointing the agent
at `bash + sed/awk` for huge files.

## H3: apply_patch blocking std::fs in async

All four `apply_*` helpers (create / update / delete / rename)
used synchronous `std::fs` calls inside an async `call`
method. On the tokio runtime with limited worker threads, slow
I/O can starve the event loop.

Switched each to `tokio::fs` and made them async. Tests
updated from `#[test]` to `#[tokio::test]` (8 tests touched).

## H4 + H5: PermissionConfig fields for missing tools

`PermissionConfig` only had fields for 11 tools (bash, read,
write, edit, grep, find_files, list_dir, write_todo_list,
apply_patch, lsp, question). 12 other tools the agent can call
had no way to be individually gated — they always fell through
to the global default action.

Added 12 fields covering the missing surface:

- Web: `webfetch`, `websearch`
- Subagent / state: `task`, `memory`, `skill`
- Semantic (tree-sitter): `list_symbols`, `get_symbol_body`,
  `find_definition`, `find_callers`, `find_callees`
- MCP umbrella: `mcp_tool` — pattern rules match against the
  full `mcp_tool:<server>:<tool>` key, so
  `{"mcp_tool:filesystem:*": "deny"}` blocks every tool
  from a filesystem MCP server.

`PermissionChecker::new` now loads rules for all 23 tool
names. `is_path_tool_name` extended to mark the path-bearing
semantic tools (`list_symbols`, `get_symbol_body`,
`find_callees`) plus `grep`/`find_files`/`glob` whose
new path-side perm check (PR #120) uses path-glob semantics.

The `/allow` slash command's whitelist grew to match — typos
on the new tool names still get caught.

## H6: MCP connect / list_tools errors visible to user

`McpClientManager::connect_all` and `collect_tools` logged
failures at `tracing::warn!` only. Default-log users (no
`RUST_LOG` / `--verbose`) saw nothing — configured MCP
servers just silently failed to register tools.

Added an `eprintln!` alongside each warn so the failure
surfaces to stderr unconditionally. Message names the server
and tells the user the tools won't be available this session.

## H9 + H10: removed dead acp_host / acp_port config keys

Both keys were parsed by serde but never consumed anywhere.
ACP transport is stdio-only by design. Removed from
`config::Config`. CONFIG.md already (PR #112) documents the
removal; no further docs change needed.

`acp_servers` kept — it has a CONFIG.md entry, holds future
TCP / Unix-socket config, and isn't actively misleading users.

## CONFIG.md

Rewrote the Permission tool-keys list from a single sentence
into a categorized list covering all 23 tools + the
`mcp_tool:<server>:<tool>` pattern syntax.

## Tests

8 `apply_patch` tests converted to `#[tokio::test]` (no new
assertions — the conversion was mechanical so the existing
coverage still applies). 813 all-features / 730 plugin / 604
default pass — same as PR #120 plus one new test there.

## Not in this PR (still deferred)

- **C4**: unregistered-tool default-allow. This PR ADDS fields
  for the 12 missing tools, so the original "unregistered"
  set is now mostly covered. The remaining gap (future MCP
  tools, third-party plugins) is by design — `mcp_tool` is
  the umbrella key.
- **C7**: LSP didClose. Needs lifecycle design (when to fire?
  on file delete? session end?). Will tackle as a focused PR.
- **H8**: LSP `extensions` config field ignored. Real gap
  but bigger plumbing change; separate PR.

## Verified false positives (re-confirmed from earlier review)

- H2 empty bash: `quote_aware_split` filters empty segments.
- H7 LSP spawn silent: broken-set + backoff IS the feedback.
- H11 plugin dedup: summary IS emitted.
- H12 token counter: estimate_tokens on partial is correct.

Co-authored-by: Yogthos <yogthos@gmail.com>
allen-munsch pushed a commit to allen-munsch/dirge that referenced this pull request Jun 3, 2026
…de#112)

Follow-up to PR dirge-code#111. Tier-2 items from the 23-bug audit batch:
docs corrections and two small correctness/UX fixes.

## Docs

- **dirge-code#12 temperature** — CONFIG.md claimed "parsed but not
  currently applied". Actually applied since PR dirge-code#105 with a
  clamp warning. Rewrote the cell.
- **dirge-code#13 --api-key** — flag existed but neither README nor
  CONFIG.md mentioned it. Added a Quick-start example noting
  the process-list visibility caveat.
- **dirge-code#14 acp_host/acp_port** — CONFIG.md documented both keys
  but the CLI flags were intentionally removed (stdio-only
  transport). Removed both from the keys table + ACP section.
- **dirge-code#6 tools** — `Config::tools` (per-tool enable map) was
  fully wired in code but undocumented. Added a row to the
  keys table covering `tools.websearch` and `tools.webfetch`.
- **dirge-code#21 find_callers** — README claimed "word-boundary regex"
  but the impl uses the tree-sitter symbol index. Updated to
  reflect actual behavior; the user-visible word-boundary
  semantics are preserved.

## Code

- **dirge-code#16 semantic index skip_dir** — `SymbolIndex::find_callers`
  filter had its own hardcoded `matches!(name, "node_modules"
  | "target" | ".git" | "__pycache__")` while the rest of
  the codebase uses `agent::tools::is_skip_dir`. Switched to
  the shared helper so future additions stay in lockstep.

- **dirge-code#18 context::load_file** — silently swallowed
  `read_to_string` errors via `.ok()`. A permission-denied
  AGENTS.md looked identical to a missing file. Now emits a
  stderr warning naming the path + reason; still returns None
  so callers' behavior is unchanged.

725 plugin / 599 default pass. All build profiles clean.

## Remaining audit items (deferred to feature work)

- **dirge-code#8 LSP no crash restart**: needs broken-pipe IO error
  handling + exponential backoff. Touches manager state
  machine.
- **dirge-code#10 task tool fire-and-forget**: needs timeout + cleanup
  coordination via JoinHandle tracking.
- **dirge-code#25 MCP no reconnection**: similar architectural concern
  to dirge-code#8.
- **dirge-code#27 LSP didClose**: client lifecycle hook missing.
- **dirge-code#29 token estimation len/4**: needs per-provider usage
  extraction (Phase 6 work).
- **dirge-code#5 MCP shutdown**: rmcp Drop semantics need verification.
- **dirge-code#38/39/40 semantic test gaps**: get_symbol_body untested,
  list_symbols kind_filter untested, find_definition test
  vacuous. Sat down to add but each requires a fixture build.

Together with PR dirge-code#111 (10 code fixes), 17 of the 23 verified
items are now shipped. Remaining 6 are architectural or
test-infrastructure work better tackled as discrete PRs.

Co-authored-by: Yogthos <yogthos@gmail.com>
allen-munsch pushed a commit to allen-munsch/dirge that referenced this pull request Jun 3, 2026
…1 leftovers (dirge-code#113)

Verified the 11 new round-2 findings and 6 round-1 leftovers
against the actual code. Shipping the 7 concrete fixes that map
to clear before/after behavior; the rest are deferred-architectural
or already-fixed-elsewhere.

## N1 — webfetch SSRF (CRITICAL)

`http://169.254.169.254/latest/meta-data/` (AWS metadata) and
all private/loopback ranges sailed straight through. An LLM that
gets prompt-injected into fetching a private IP could exfiltrate
IAM credentials, hit internal admin endpoints, or scrape internal
network maps.

New `validate_url_host_safety()` blocks:
- Hostname literal "localhost" / "ip6-localhost" /
  "ip6-loopback"
- IPv4 loopback (127/8), private (10/8, 172.16/12, 192.168/16),
  link-local (169.254/16 — cloud metadata), unspecified,
  broadcast, class E.
- IPv6 loopback, ULA (fc00::/7), link-local (fe80::/10),
  multicast, unspecified. Bracketed IPv6 (`[::1]`) is parsed
  correctly.

Opt-in escape hatch: `DIRGE_WEBFETCH_ALLOW_PRIVATE=1` for
dev workflows that need to hit localhost. Tool description
documents the env var so the agent can see it.

## N8 — webfetch body OOM (HIGH)

`resp.text().await` buffered the entire response into memory
before any truncation. A 500 MB page would OOM the agent
process. Now streams via `bytes_stream` with a 10 MiB cap;
bails at the cap.

## N2 — MCP result size cap (CRITICAL)

`McpTool::call` concatenated every `RawContent::Text` block
into `content` without limit. A 200 KB MCP response flooded
every subsequent turn until compaction. Cap aggregate at 256
KiB (≈65k tokens worst case); UTF-8 char-boundary slice;
append a clear truncation marker naming the offending
server::tool so the agent can adjust its calls.

## N3 — bash output cap (CRITICAL)

The UI's `render_tool_output` truncated the DISPLAY but the
full string was persisted to `ToolCallState::Completed` and
fed back to the LLM on the next turn. `cat /dev/urandom | head
-c 10M` would have shoved millions of tokens. Cap at 256 KiB
at the bash tool source, with a clear truncation message
pointing the agent at head/grep filtering.

## N5 — sandbox hardening (HIGH)

Added `--new-session` (drops the ability to gain new privs via
setuid) and `--unshare-user-try` (explicit user-ns isolation,
defensive against future bwrap default changes). `--unshare-all`
already covers most of this; the explicit flags pin the
guarantees.

## N7 — sanitize_output strips \\r (HIGH)

Previously `\\r` was preserved (originally for CRLF lines). A
bash tool printing progress with `\\rstep N/M` would move the
cursor to column 0 mid-line, overwriting chamber borders. Now
stripped (`\\n` and `\\t` still pass; display path
normalizes CRLF before reaching here).

## H10 — task subagent timeout

`tokio::spawn(btw_query(...))` with no timeout meant a stuck
provider would keep the task in `Running` forever. Cap at 10
minutes; on timeout, the task transitions to `Failed` with a
clear message and the system-reminder still fires.

## M22 — /prompt default help text

Behavior was fixed in PR dirge-code#111 (activates `default.md` if
installed). Help text still said "clear active prompt". Now
explains both cases.

## Verified false positives / non-issues this round

- **N4 sandbox --ro-bind / /**: by design — the sandbox is a
  WRITE guard, not exfiltration prevention. Defense for read
  access requires `--ro-bind` per path (much narrower scope
  + breaks read-only tooling like bash `ls`). Document as
  feature, not bug.
- **N6 sandbox current_dir fallback**: `unwrap_or_else(|_|
  ".".into())` is the standard fallback; `current_dir()`
  failing means cwd is deleted/inaccessible which is its own
  fatal-soon problem.
- **N9 token estimation len/4**: Phase 6 work; per-provider
  usage extraction needs distinct fix.
- **N10 sandbox /dev/tty**: `--dev /dev` mounts a tmpfs with
  only essential nodes, not the full host /dev. /dev/tty IS
  available to the sandboxed process so it can write to its
  own controlling terminal (needed for bash interactive
  features); not the host's /dev/tty0.
- **H17 DeepSeek auto-detect order**: confirmed false positive
  in earlier rounds — only fires when env var present.
- **H18 load_file path in error**: already fixed in PR dirge-code#112.
- **M19 semantic tools in BUILTIN_TOOL_NAMES**: semantic tools
  added separately, can't be shadowed by MCP collision.

## Tests

2 new regression tests:
- `validate_url_host_safety_blocks_ssrf_targets`: pins
  169.254.x, 127.x, RFC 1918, ::1, fc00::, fe80:: as refused;
  public IPs + domains pass; bracketed IPv6 parses correctly.
- `validate_url_host_safety_handles_malformed_hosts`: garbage
  hosts don't panic.

727 plugin / 601 default pass.

## Deferred to architectural follow-ups

- H8 LSP no crash restart — broken-pipe handling design
- N4 sandbox --ro-bind / — fundamental design tradeoff
- N9 token estimation — per-provider usage extraction

Co-authored-by: Yogthos <yogthos@gmail.com>
allen-munsch pushed a commit to allen-munsch/dirge that referenced this pull request Jun 3, 2026
…ig (dirge-code#121)

Continues from PR dirge-code#120 (security-critical). Six deferred items
shipped.

## H1: edit + apply_patch unbounded file reads

`edit.rs` and `apply_patch.rs` (apply_update) called
`tokio::fs::read` / `std::fs::read_to_string` with no size
guard. An LLM pointing them at a multi-GB log/binary would OOM
the agent process.

Added 100 MiB pre-flight metadata check in both. The cap is
generous (every realistic source file fits) but rejects the
pathological case fast with a clear error pointing the agent
at `bash + sed/awk` for huge files.

## H3: apply_patch blocking std::fs in async

All four `apply_*` helpers (create / update / delete / rename)
used synchronous `std::fs` calls inside an async `call`
method. On the tokio runtime with limited worker threads, slow
I/O can starve the event loop.

Switched each to `tokio::fs` and made them async. Tests
updated from `#[test]` to `#[tokio::test]` (8 tests touched).

## H4 + H5: PermissionConfig fields for missing tools

`PermissionConfig` only had fields for 11 tools (bash, read,
write, edit, grep, find_files, list_dir, write_todo_list,
apply_patch, lsp, question). 12 other tools the agent can call
had no way to be individually gated — they always fell through
to the global default action.

Added 12 fields covering the missing surface:

- Web: `webfetch`, `websearch`
- Subagent / state: `task`, `memory`, `skill`
- Semantic (tree-sitter): `list_symbols`, `get_symbol_body`,
  `find_definition`, `find_callers`, `find_callees`
- MCP umbrella: `mcp_tool` — pattern rules match against the
  full `mcp_tool:<server>:<tool>` key, so
  `{"mcp_tool:filesystem:*": "deny"}` blocks every tool
  from a filesystem MCP server.

`PermissionChecker::new` now loads rules for all 23 tool
names. `is_path_tool_name` extended to mark the path-bearing
semantic tools (`list_symbols`, `get_symbol_body`,
`find_callees`) plus `grep`/`find_files`/`glob` whose
new path-side perm check (PR dirge-code#120) uses path-glob semantics.

The `/allow` slash command's whitelist grew to match — typos
on the new tool names still get caught.

## H6: MCP connect / list_tools errors visible to user

`McpClientManager::connect_all` and `collect_tools` logged
failures at `tracing::warn!` only. Default-log users (no
`RUST_LOG` / `--verbose`) saw nothing — configured MCP
servers just silently failed to register tools.

Added an `eprintln!` alongside each warn so the failure
surfaces to stderr unconditionally. Message names the server
and tells the user the tools won't be available this session.

## H9 + H10: removed dead acp_host / acp_port config keys

Both keys were parsed by serde but never consumed anywhere.
ACP transport is stdio-only by design. Removed from
`config::Config`. CONFIG.md already (PR dirge-code#112) documents the
removal; no further docs change needed.

`acp_servers` kept — it has a CONFIG.md entry, holds future
TCP / Unix-socket config, and isn't actively misleading users.

## CONFIG.md

Rewrote the Permission tool-keys list from a single sentence
into a categorized list covering all 23 tools + the
`mcp_tool:<server>:<tool>` pattern syntax.

## Tests

8 `apply_patch` tests converted to `#[tokio::test]` (no new
assertions — the conversion was mechanical so the existing
coverage still applies). 813 all-features / 730 plugin / 604
default pass — same as PR dirge-code#120 plus one new test there.

## Not in this PR (still deferred)

- **C4**: unregistered-tool default-allow. This PR ADDS fields
  for the 12 missing tools, so the original "unregistered"
  set is now mostly covered. The remaining gap (future MCP
  tools, third-party plugins) is by design — `mcp_tool` is
  the umbrella key.
- **C7**: LSP didClose. Needs lifecycle design (when to fire?
  on file delete? session end?). Will tackle as a focused PR.
- **H8**: LSP `extensions` config field ignored. Real gap
  but bigger plumbing change; separate PR.

## Verified false positives (re-confirmed from earlier review)

- H2 empty bash: `quote_aware_split` filters empty segments.
- H7 LSP spawn silent: broken-set + backoff IS the feedback.
- H11 plugin dedup: summary IS emitted.
- H12 token counter: estimate_tokens on partial is correct.

Co-authored-by: Yogthos <yogthos@gmail.com>
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