fix: three things that were known and written nowhere
All three have the same shape — the system learns something and only stderr
hears it — and each was flagged in the handoff as a silent-discard defect.
The gate's install outcome. `container_tool_hooks::install` returned Some or
None and both call sites wrote `let _ =`. A mission whose gate never installed
left a record indistinguishable from one whose gate stood there and matched
nothing. `EnsuredContainer` now carries the outcome to the callers that have a
pool, and they record `gate.installed` (with the settings path) or
`gate.absent` on the mission, so "was this mission gated?" is answerable from
the mission.
The inert marker. `vm_tool_gate` writes an `inert` file when it cannot parse
its input and allows everything, precisely so an inert gate does not look like
a permissive one. The only reader was a unit test. `drain_inert` now reads and
clears it at every tap drain, and a `gate.inert` event with the occurrence count
lands beside the calls that ran unchecked.
The judge's spend. `LlmEvent::Usage` arrived on every judge call and was
matched by `Ok(_) => {}`. Two plan exhaustions (2026-08-29, 2026-09-09) with
no row anywhere saying a judge token had been spent; `usage_events` had no
provider or model column. The loop now accumulates requests and tokens onto the
Verdict — counting a request BEFORE the stream opens, so a 429 the provider
refused still counts, because the retry storm was made of those — and
`record` writes a `kind = 'judge'` row with provider, model, mission and
request count. Migration 0085 adds the columns, all nullable, so the two
existing writers are untouched.
Tests: a scripted-provider verdict records one request and nonzero tokens; a
provider that refuses still records the request and zero tokens.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZb5A2kfVfjpdwSochkuHz
This commit is contained in:
co-authored by
Claude Opus 5
parent
758760cedb
commit
248948cc84
@@ -175,6 +175,65 @@ pub async fn install_door(docker: &Docker, container: &str, doc: &serde_json::Va
|
||||
}
|
||||
}
|
||||
|
||||
/// Event kinds under which the gate's own state lands in the mission record.
|
||||
///
|
||||
/// Recorded, not only logged, so "was this mission gated?" is answerable from
|
||||
/// the mission afterwards. Stderr is where the answer used to go, which is the
|
||||
/// same place as nowhere once the container that printed it is gone.
|
||||
pub const GATE_INSTALLED: &str = "gate.installed";
|
||||
pub const GATE_ABSENT: &str = "gate.absent";
|
||||
/// The gate ran but could not parse its input and allowed everything. See
|
||||
/// [`crate::vm_tool_gate::INERT_FILE`] — this is the reader that marker was
|
||||
/// missing in production; until now only a unit test looked for it.
|
||||
pub const GATE_INERT: &str = "gate.inert";
|
||||
|
||||
/// Write the install outcome into the mission record.
|
||||
pub async fn record_install(
|
||||
pool: &sqlx::PgPool,
|
||||
mission_id: uuid::Uuid,
|
||||
phase_id: Option<uuid::Uuid>,
|
||||
hooks: Option<&str>,
|
||||
) {
|
||||
let mut e = match hooks {
|
||||
Some(path) => crate::mission_events::MissionEvent::new(mission_id, GATE_INSTALLED)
|
||||
.target(path)
|
||||
.detail(serde_json::json!({ "settings": path, "tap": tap_file() })),
|
||||
None => crate::mission_events::MissionEvent::new(mission_id, GATE_ABSENT).detail(
|
||||
serde_json::json!({
|
||||
"why": "container_tool_hooks::install failed — this mission's tool \
|
||||
calls run unchecked and unrecorded"
|
||||
}),
|
||||
),
|
||||
};
|
||||
if let Some(p) = phase_id {
|
||||
e = e.phase(p);
|
||||
}
|
||||
crate::mission_events::record(pool, e).await;
|
||||
}
|
||||
|
||||
/// The inert marker's path inside the container.
|
||||
pub fn inert_file() -> String {
|
||||
format!("{HOOK_DIR}/{}", crate::vm_tool_gate::INERT_FILE)
|
||||
}
|
||||
|
||||
/// Did the gate go inert since the last drain? Reads the marker and clears
|
||||
/// it, so each occurrence is reported once.
|
||||
///
|
||||
/// `Some(text)` is the marker's contents — every line the gate appended while
|
||||
/// it could not parse. `None` is "the marker is not there", which is the
|
||||
/// normal case and also, by construction, the only case that means the gate
|
||||
/// was actually checking.
|
||||
pub async fn drain_inert(docker: &Docker, container: &str) -> Option<String> {
|
||||
let file = inert_file();
|
||||
let script = format!("cat {file} 2>/dev/null && rm -f {file} 2>/dev/null; true");
|
||||
let argv = vec!["sh".to_string(), "-lc".to_string(), script];
|
||||
match crate::container_exec::exec_as_root(docker, container, None, &argv, INSTALL_TIMEOUT).await
|
||||
{
|
||||
Ok(out) if !out.stdout.trim().is_empty() => Some(out.stdout.trim().to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The tap file inside the mission container.
|
||||
pub fn tap_file() -> String {
|
||||
format!("{TAP_DIR}/tools.jsonl")
|
||||
|
||||
Reference in New Issue
Block a user