Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 45 additions & 11 deletions bt-daemon/src/translate/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ impl AgentTranslator for CodexTranslator {
if let Some(hook) = decode::<SessionStartHook>(payload) {
self.session_source = hook.source;
self.permission_mode = hook.permission_mode;
self.update_open_main_root_source(&mut ops);
}
}
"SubagentStart" => {
Expand Down Expand Up @@ -430,6 +431,34 @@ impl CodexTranslator {
);
}

fn update_open_main_root_source(&self, ops: &mut Vec<SpanOp>) {
let Some(source) = &self.session_source else {
return;
};
if !self.root_opened {
return;
}
let model = self
.main_path
.as_ref()
.and_then(|path| self.scopes.get(path))
.and_then(|scope| scope.model.clone());
ops.push(SpanOp::Merge(SpanRow {
span_id: self.root_span_id.clone(),
root_span_id: self.root_span_id.clone(),
input: Some(json!({
"model": model,
"cwd": self.root_cwd,
"source": source,
})),
metadata: Some(json!({
"source": "codex",
"session_source": source,
})),
..Default::default()
}));
}

fn record_compaction_trigger(&mut self, hook: CompactHook, ops: &mut Vec<SpanOp>) {
let turn_id = hook.turn_id;
let trigger = hook.trigger.unwrap_or_else(|| "manual".to_string());
Expand Down Expand Up @@ -594,20 +623,26 @@ impl CodexTranslator {
let model_turn_id = context.turn_id;
scope.model = Some(m.clone());
if scope.root_created {
let input = if scope.kind == ScopeKind::Main {
json!({
"model": m,
"cwd": self.root_cwd,
"source": self.session_source,
})
let (input, metadata) = if scope.kind == ScopeKind::Main {
(
json!({
"model": m,
"cwd": self.root_cwd,
"source": self.session_source,
}),
json!({
"model": m,
"source": "codex",
}),
Comment thread
AbhiPrasad marked this conversation as resolved.
)
} else {
json!({ "model": m })
(json!({ "model": m }), json!({ "model": m }))
};
ops.push(SpanOp::Merge(SpanRow {
span_id: scope.turn_parent_span_id.clone(),
root_span_id: self.root_span_id.clone(),
input: Some(input),
metadata: Some(json!({ "model": m })),
metadata: Some(metadata),
..Default::default()
}));
}
Expand Down Expand Up @@ -685,9 +720,8 @@ impl CodexTranslator {
);
}
}
// `source` identifies the agent consistently across all coding-agent
// integrations. Codex's SessionStart source (for example, startup or
// resume) describes how this session began, so keep it separately.
// `source` is the stable integration identity used to classify
// traces. The native SessionStart lifecycle source is separate.
md.insert("source".into(), json!("codex"));
if let Some(session_source) = &self.session_source {
md.insert("session_source".into(), json!(session_source));
Expand Down
66 changes: 66 additions & 0 deletions bt-daemon/tests/codex_translator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,72 @@ fn codex_happy_path_builds_session_turn_llm_tool_tree() {
);
}

#[test]
fn codex_root_preserves_canonical_source_across_lifecycle_events() {
let tmp = tempfile::tempdir().unwrap();

for source in ["startup", "resume", "compact"] {
let transcript = tmp.path().join(format!("{source}.jsonl"));
append(
&transcript,
json!({ "timestamp": "2026-01-01T00:00:01Z", "type": "session_meta",
"payload": { "id": "session-1", "cwd": "/x/app" } }),
);
append(
&transcript,
json!({ "timestamp": "2026-01-01T00:00:02Z", "type": "turn_context",
"payload": { "model": "gpt-5.5" } }),
);
let session_id = format!("lifecycle-{source}");
let registry = Registry::default_agents();
let mut translator = registry.create("codex", &session_id);
let ctx = SessionCtx {
session_id: session_id.clone(),
config: None,
};
let mut ops = translator
.handle(
&envelope(
&session_id,
"SessionStart",
transcript.to_str().unwrap(),
json!({ "source": "startup" }),
),
&ctx,
)
.unwrap();

if source != "startup" {
// A resumed or compacted session can exit before writing another
// model-bearing turn_context record.
ops.extend(
translator
.handle(
&envelope(
&session_id,
"SessionStart",
transcript.to_str().unwrap(),
json!({ "source": source }),
),
&ctx,
)
.unwrap(),
);
}

let rows = reduce(ops);
let root = find(&rows, SpanType::Task, "codex: app");
assert_eq!(root.input.as_ref().unwrap()["source"], json!(source));
assert_eq!(root.input.as_ref().unwrap()["model"], json!("gpt-5.5"));
assert_eq!(root.input.as_ref().unwrap()["cwd"], json!("/x/app"));
assert_eq!(root.metadata.as_ref().unwrap()["source"], json!("codex"));
assert_eq!(
root.metadata.as_ref().unwrap()["session_source"],
json!(source)
);
}
}

#[test]
fn codex_trailing_injected_user_row_does_not_replace_hook_prompt() {
let tmp = tempfile::tempdir().unwrap();
Expand Down
Loading