feat(missions): attribute a phase's tool calls to the agent that made them

`record_vm_tools` wrote `agent_id: None` for every call. The container
tap is per-CONTAINER and every role in a phase shares one, so a phase
arrived as one undifferentiated stream: every Skill-Use score was
per-mission rather than per-role, and the World's per-agent view got
nothing from this tier.

One `claude -p` invocation is one turn is one agent, and Claude Code
stamps each invocation with a `session_id` the tap was discarding. So the
distinct sessions, in order of first appearance, are the phase's turns in
the order they ran — and `prompt.composed` already records the agent of
each turn in that same order, written by the tier as it sends each turn,
so it IS the running order rather than a reconstruction of it.

**It attributes nothing rather than guessing.** Only when the counts
match exactly. A phase whose sessions and turns differ has something this
correlation does not model — a retry, a turn that called no tool, two
genuinely concurrent agents — and a plausible-looking wrong attribution
is worse than none here: it puts one agent's `git push` on another
agent's record, and a person later reasons from that. One call missing a
session id refuses the whole batch, because a hole shifts every later
session onto the wrong turn.

The microVM call sites pass no turn agents and so keep today's
behaviour exactly. Resolving a graph node to an agent uuid is the fix
there, it cannot be tested while the fleet is offline, and guessing would
put one node's actions on another node's record.

Also restores the `#[cfg(test)]` gate on `repo_less_text_tests`, which my
own insertion had taken — those tests would have compiled into release
builds.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018i9Ten1LU4jUr5d7TAWda9
This commit is contained in:
Omar Sobh
2026-08-21 11:33:20 -07:00
co-authored by Claude Opus 5
parent 3f26dfeaca
commit 8591585e60
3 changed files with 179 additions and 5 deletions
+15
View File
@@ -71,6 +71,14 @@ pub struct Observed {
pub tool: String,
/// The path the tool's **input** named, if any. From JSON, never prose.
pub path: Option<String>,
/// Claude Code's session id for the `claude -p` invocation this call
/// happened inside.
///
/// One invocation is one turn is one agent, so this is the only thing in
/// the payload that separates one agent's actions from another's. The tap
/// is per-CONTAINER and every role in a phase shares one, so without this
/// the whole phase arrives as an undifferentiated stream.
pub session: Option<String>,
/// The tool's arguments, bounded by [`bounded_input`].
///
/// Kept because the tool NAME alone answers almost nothing. A phase that
@@ -245,6 +253,11 @@ pub fn parse(raw: &str) -> Vec<Observed> {
Some(Observed {
path: crate::mission_events::tool_path(&input),
input: bounded_input(&input),
session: v
.get("session_id")
.or_else(|| v.get("sessionId"))
.and_then(Value::as_str)
.map(str::to_string),
tool,
})
})
@@ -337,11 +350,13 @@ mod tests {
tool: "Edit".into(),
path: Some("/mission/repo/src/a.rs".into()),
input: json!({"file_path": "/mission/repo/src/a.rs"}),
session: None,
},
Observed {
tool: "Bash".into(),
path: None,
input: json!({"command": "ls"}),
session: None,
},
]
);