Skip to content

Commit c7c97cb

Browse files
committed
feat(opencode): improve monitor/status logs and CLI compatibility
1 parent edc289f commit c7c97cb

4 files changed

Lines changed: 233 additions & 34 deletions

File tree

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,14 @@ To force a new clean loop session (ignore previous runtime/session artifacts):
204204
forge --cwd /absolute/path/to/project run --fresh
205205
```
206206

207-
`--fresh` clears runtime state files in `.forge/` and adds `--ephemeral` to engine execution to avoid reusing old sessions.
207+
`--fresh` clears runtime state files in `.forge/`.
208+
For Codex engine, it also adds `--ephemeral` to avoid reusing old sessions.
209+
For OpenCode, runtime cleanup is applied without adding Codex-specific flags.
210+
211+
`forge status` is engine-aware:
212+
213+
- Codex: shows context and 5h/7d limit snapshots (when available)
214+
- OpenCode: shows `n/a (opencode)` for Codex-specific usage metrics
208215

209216
## Analyze modified files
210217

crates/forge-cli/src/main.rs

Lines changed: 92 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,7 @@ fn run_command(cmd: RunCommand, cwd: PathBuf) -> Result<()> {
553553
}
554554

555555
let engine_pre_args = with_full_access_args(cmd.engine_pre_args.clone(), cmd.full_access);
556-
let engine_exec_args = if cmd.fresh {
556+
let engine_exec_args = if cmd.fresh && matches!(cmd.engine, EngineArg::Codex) {
557557
Some(vec!["--ephemeral".to_string()])
558558
} else {
559559
None
@@ -1121,9 +1121,12 @@ fn status_command(cmd: StatusCommand, cwd: PathBuf) -> Result<()> {
11211121
let runtime_dir = cwd.join(cfg.runtime_dir);
11221122
let status = read_status(&runtime_dir)?;
11231123
let session_id = infer_session_id(&runtime_dir, &status);
1124-
let usage = session_id
1125-
.as_deref()
1126-
.and_then(read_codex_usage_for_session_id);
1124+
let usage = match cfg.engine {
1125+
EngineKind::Codex => session_id
1126+
.as_deref()
1127+
.and_then(read_codex_usage_for_session_id),
1128+
EngineKind::OpenCode => None,
1129+
};
11271130

11281131
if cmd.json {
11291132
let mut out = serde_json::json!({
@@ -1147,6 +1150,7 @@ fn status_command(cmd: StatusCommand, cwd: PathBuf) -> Result<()> {
11471150
};
11481151

11491152
println!("state: {}", status.state);
1153+
println!("engine: {}", cfg.engine.as_str());
11501154
println!("thinking_mode: {}", status.thinking_mode);
11511155
println!("run_timer: {}", run_timer);
11521156
println!("current_loop: {}", status.current_loop);
@@ -1162,25 +1166,31 @@ fn status_command(cmd: StatusCommand, cwd: PathBuf) -> Result<()> {
11621166
"session_id: {}",
11631167
session_id.unwrap_or_else(|| "-".to_string())
11641168
);
1165-
println!("context: {}", format_context_line(usage.as_ref()));
1166-
println!(
1167-
"5h limit: {}",
1168-
format_limit_line(
1169-
usage.as_ref().and_then(|u| u.five_hour_left_percent),
1170-
usage
1171-
.as_ref()
1172-
.and_then(|u| u.five_hour_resets_at.as_deref())
1173-
)
1174-
);
1175-
println!(
1176-
"7d limit: {}",
1177-
format_limit_line(
1178-
usage.as_ref().and_then(|u| u.seven_day_left_percent),
1179-
usage
1180-
.as_ref()
1181-
.and_then(|u| u.seven_day_resets_at.as_deref())
1182-
)
1183-
);
1169+
if cfg.engine == EngineKind::Codex {
1170+
println!("context: {}", format_context_line(usage.as_ref()));
1171+
println!(
1172+
"5h limit: {}",
1173+
format_limit_line(
1174+
usage.as_ref().and_then(|u| u.five_hour_left_percent),
1175+
usage
1176+
.as_ref()
1177+
.and_then(|u| u.five_hour_resets_at.as_deref())
1178+
)
1179+
);
1180+
println!(
1181+
"7d limit: {}",
1182+
format_limit_line(
1183+
usage.as_ref().and_then(|u| u.seven_day_left_percent),
1184+
usage
1185+
.as_ref()
1186+
.and_then(|u| u.seven_day_resets_at.as_deref())
1187+
)
1188+
);
1189+
} else {
1190+
println!("context: n/a (opencode)");
1191+
println!("5h limit: n/a (opencode)");
1192+
println!("7d limit: n/a (opencode)");
1193+
}
11841194
println!("updated_at_epoch: {}", status.updated_at_epoch);
11851195
}
11861196
Ok(())
@@ -1199,7 +1209,7 @@ struct CodexUsageSnapshot {
11991209

12001210
fn infer_session_id(runtime_dir: &Path, status: &forge_types::RunStatus) -> Option<String> {
12011211
if let Some(session_id) = status.session_id.clone() {
1202-
if !session_id.trim().is_empty() {
1212+
if !session_id.trim().is_empty() && is_likely_engine_session_id(&session_id) {
12031213
return Some(session_id);
12041214
}
12051215
}
@@ -1211,14 +1221,34 @@ fn infer_session_id(runtime_dir: &Path, status: &forge_types::RunStatus) -> Opti
12111221
let Ok(value) = serde_json::from_str::<Value>(trimmed) else {
12121222
continue;
12131223
};
1214-
if value.get("type").and_then(Value::as_str) == Some("thread.started") {
1215-
if let Some(thread_id) = value.get("thread_id").and_then(Value::as_str) {
1216-
if !thread_id.trim().is_empty() {
1217-
return Some(thread_id.to_string());
1218-
}
1224+
if let Some(found) = infer_session_id_from_value(&value) {
1225+
return Some(found);
1226+
}
1227+
}
1228+
None
1229+
}
1230+
1231+
fn is_likely_engine_session_id(session_id: &str) -> bool {
1232+
session_id.starts_with("ses_")
1233+
|| session_id.starts_with("thread_")
1234+
|| session_id.starts_with("conv_")
1235+
}
1236+
1237+
fn infer_session_id_from_value(value: &Value) -> Option<String> {
1238+
if value.get("type").and_then(Value::as_str) == Some("thread.started") {
1239+
if let Some(thread_id) = value.get("thread_id").and_then(Value::as_str) {
1240+
if !thread_id.trim().is_empty() {
1241+
return Some(thread_id.to_string());
12191242
}
12201243
}
12211244
}
1245+
1246+
if let Some(session_id) = value.get("sessionID").and_then(Value::as_str) {
1247+
if !session_id.trim().is_empty() {
1248+
return Some(session_id.to_string());
1249+
}
1250+
}
1251+
12221252
None
12231253
}
12241254

@@ -1800,7 +1830,8 @@ fn format_duration(total_secs: u64) -> String {
18001830

18011831
#[cfg(test)]
18021832
mod tests {
1803-
use super::with_full_access_args;
1833+
use super::{infer_session_id_from_value, is_likely_engine_session_id, with_full_access_args};
1834+
use serde_json::json;
18041835

18051836
#[test]
18061837
fn full_access_adds_danger_sandbox_when_missing() {
@@ -1837,4 +1868,35 @@ mod tests {
18371868
]
18381869
);
18391870
}
1871+
1872+
#[test]
1873+
fn infer_session_id_from_codex_thread_started() {
1874+
let value = json!({
1875+
"type": "thread.started",
1876+
"thread_id": "thread_123"
1877+
});
1878+
assert_eq!(
1879+
infer_session_id_from_value(&value),
1880+
Some("thread_123".to_string())
1881+
);
1882+
}
1883+
1884+
#[test]
1885+
fn infer_session_id_from_opencode_event() {
1886+
let value = json!({
1887+
"type": "step_start",
1888+
"sessionID": "ses_abc"
1889+
});
1890+
assert_eq!(
1891+
infer_session_id_from_value(&value),
1892+
Some("ses_abc".to_string())
1893+
);
1894+
}
1895+
1896+
#[test]
1897+
fn session_id_filter_rejects_part_ids() {
1898+
assert!(is_likely_engine_session_id("ses_abc"));
1899+
assert!(is_likely_engine_session_id("thread_abc"));
1900+
assert!(!is_likely_engine_session_id("prt_abc"));
1901+
}
18401902
}

crates/forge-engine/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,15 +119,15 @@ impl OpenCodeEngine {
119119
fn build_exec_args(&self, params: &EngineExecParams) -> Vec<String> {
120120
let mut args = vec!["run".into()];
121121
args.extend(params.config.engine_exec_args.iter().cloned());
122-
args.push("--json".into());
122+
args.push("--format".into());
123+
args.push("json".into());
123124

124125
if params.config.thinking_mode == ThinkingMode::Off {
125126
args.push("--config".into());
126127
args.push("hide_agent_reasoning=true".into());
127128
}
128129

129130
if let Some(prompt) = &params.prompt {
130-
args.push("--prompt".into());
131131
args.push(prompt.clone());
132132
}
133133

0 commit comments

Comments
 (0)