perf(kernel): hoist read-only check out of per-tool filter (#51)

runInference filtered the offered tool list with isReadOnlyMode(sessionId)
evaluated inside the per-tool .filter{} — a full log read+fold per offered
tool, per inference round (the audit's dominant O(events^2) hot path). The
flag is tool-independent, so read it once before the filter.

The per-tool-CALL re-check in dispatchToolCalls is left as-is: it is
semantically load-bearing (a read completing earlier in the same batch lifts
read-only mode for a later write), not redundant.
This commit is contained in:
2026-07-12 13:14:58 +04:00
parent da7fe61b77
commit bb73bc9371
@@ -2906,11 +2906,14 @@ abstract class SessionOrchestrator(
tools = if (!withTools) {
emptyList()
} else {
// Read the log ONCE for the read-only check instead of once per tool inside the filter
// (the flag is tool-independent) — this filter runs per tool per inference round.
val readOnlyMode = isReadOnlyMode(sessionId)
stageConfig.effectiveAllowedTools
.mapNotNull { effectives.registry?.resolve(it) }
.filter { tool ->
// ponytail: filter write tools while read-before-write block is active; restored once a read completes
!isReadOnlyMode(sessionId) || ToolCapability.FILE_WRITE !in tool.requiredCapabilities
!readOnlyMode || ToolCapability.FILE_WRITE !in tool.requiredCapabilities
}
.filter { tool ->
// Read-loop break: keep only write tools (drop file_read/list_dir/shell) so