fix(kernel): de-dup tool events, restore approval diff, break read-before-write deadlock

Three tool-path correctness fixes:

- Tool-execution events were emitted twice — SandboxedToolExecutor and
  SessionOrchestrator.recordToolExecution both emitted Completed/Failed/
  FileWritten per call (double TUI rows, double-counted metrics). Split
  ownership: orchestrator is the sole recorder of Completed/Failed (truncates
  output, drives the read-before-write gate); executor owns Started +
  FileWritten (pre/post-image hashes it alone can capture).

- computeToolPreview still gated on operation == "write", but file_write lost
  its operation param when delete was split out, so it bailed to raw-args for
  every write (approval card showed raw JSON). Drop the dead guard.

- A file_read blocked by REFERENCE_EXISTS can never complete, so it could never
  lift read-only mode: a stage writing a NEW file deadlocked (writes filtered,
  every read of the not-yet-created target blocked) until MAX_TOOL_ROUNDS.
  Lift read-only on a REFERENCE_EXISTS block, and reword the block message to
  tell the model to file_write directly. Regression test added.
This commit is contained in:
2026-07-03 01:01:19 +04:00
parent ca3fd7971e
commit 2698971082
5 changed files with 178 additions and 106 deletions
@@ -45,8 +45,10 @@ class ReferenceExistsRule : ToolCallRule {
if (inWorkspace && !exists) {
issues += ValidationIssue(
code = RULE_CODE,
message = "Tool '${input.request.toolName}' references '$raw', which does not " +
"exist — list the directory or fix the path; do not assume its contents.",
message = "Tool '${input.request.toolName}' references '$raw', which does not exist. " +
"Do NOT keep retrying the read. If you intend to create this file, call file_write " +
"directly — you cannot read a file that does not exist yet. Otherwise list the " +
"directory or fix the path; do not assume its contents.",
severity = ValidationSeverity.ERROR,
)
disposition = maxAction(disposition, RiskAction.BLOCK)