← All Pull Requests

AI Flow 0.2.75: the run's timeline stays off Parts Search's port #94

Merged Released in 0.2.75 on 11 Nov 2026 opened by Colby Knox
Merges aiflow-0.2.75 → main

AI Flow 0.2.75: the timeline stays off Parts Search's port. Closes #201.

  • Dashboard ports move from 8893..8993 to 18893..18993 (timeline serve defaults to 18893); a run's saved old-range port is not reused, and its own verified server there is stopped and restarted in the new range.
  • A taken --port names the program holding it and suggests another.

Tests: 717 workspace tests (1 new), dashboard/attention/timeline suites pass; checked the holder message live; preflight clean.

🤖 Generated with Claude Code

Diff Skip to comments

Cargo.lock+10−10
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "adom-aiflow"-version = "0.2.74"+version = "0.2.75" dependencies = [  "aiflow-analyze",  "aiflow-board",@@ -25,7 +25,7 @@ dependencies = [  [[package]] name = "aiflow-analyze"-version = "0.2.74"+version = "0.2.75" dependencies = [  "aiflow-board",  "serde",@@ -35,7 +35,7 @@ dependencies = [  [[package]] name = "aiflow-board"-version = "0.2.74"+version = "0.2.75" dependencies = [  "roxmltree",  "serde",@@ -44,7 +44,7 @@ dependencies = [  [[package]] name = "aiflow-bridge"-version = "0.2.74"+version = "0.2.75" dependencies = [  "aiflow-board",  "serde",@@ -53,7 +53,7 @@ dependencies = [  [[package]] name = "aiflow-copper"-version = "0.2.74"+version = "0.2.75" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -63,7 +63,7 @@ dependencies = [  [[package]] name = "aiflow-grid"-version = "0.2.74"+version = "0.2.75" dependencies = [  "aiflow-board",  "serde",@@ -72,7 +72,7 @@ dependencies = [  [[package]] name = "aiflow-place"-version = "0.2.74"+version = "0.2.75" dependencies = [  "aiflow-board",  "serde",@@ -81,7 +81,7 @@ dependencies = [  [[package]] name = "aiflow-pours"-version = "0.2.74"+version = "0.2.75" dependencies = [  "aiflow-board",  "aiflow-copper",@@ -91,7 +91,7 @@ dependencies = [  [[package]] name = "aiflow-router"-version = "0.2.74"+version = "0.2.75" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -101,7 +101,7 @@ dependencies = [  [[package]] name = "aiflow-run"-version = "0.2.74"+version = "0.2.75" dependencies = [  "serde",  "serde_json",
Cargo.toml+1−1
@@ -14,7 +14,7 @@ members = [ ]  [workspace.package]-version = "0.2.74"+version = "0.2.75" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
bin/adom-aiflow
⋯ 1 unchanged line ⋯
crates/adom-aiflow/src/dashboard.rs+28−2
@@ -388,8 +388,24 @@ fn quote_dir(dir: &str) -> String {     if dir.chars().all(|c| c.is_alphanumeric() || "/._-".contains(c)) { dir.to_string() } else { format!("'{}'", dir.replace('\'', "'\\''")) } } +/// Issue #201 (Glenn): the dashboard's ports, clear of the 88xx ports Adom's dock apps take (Parts Search 8893,+/// Fields 8874, ...). A run that saved a port of the old range does not take it again.+pub const PORTS: std::ops::Range<u16> = 18893..18993;+const OLD_PORTS: std::ops::Range<u16> = 8893..8993;++/// The ports to try, in order: an asked-for port, the run's saved port when it is in the range, then the range.+pub fn port_candidates(requested: Option<u16>, saved: Option<u16>) -> Vec<u16> {+    requested.into_iter().chain(saved.filter(|p| PORTS.contains(p))).chain(PORTS).collect()+}+ fn start_server(dir: &Path, tdir: &Path, id: &str, requested: Option<u16>, saved: Option<u16>) -> Result<u16, String> {-    for candidate in requested.into_iter().chain(saved).chain(8893..8993) {+    // this run's own server still on an old-range port (the one Parts Search wants): stop it, verified as ours+    if let Some(old) = saved.filter(|p| OLD_PORTS.contains(p) && requested != Some(*p)) {+        if identity(old).as_deref() == Some(id) {+            let _ = stop_server(&serde_json::json!({"port": old, "runId": id, "runDir": dir.to_string_lossy()}));+        }+    }+    for candidate in port_candidates(requested, saved) {         if identity(candidate).as_deref() == Some(id) {             return Ok(candidate);         }@@ -417,7 +433,7 @@ fn start_server(dir: &Path, tdir: &Path, id: &str, requested: Option<u16>, saved         // Do not kill a process by port: it might belong to another run.         return Err(format!("dashboard server did not become ready on {candidate}; inspect {}", tdir.join("serve.log").display()));     }-    Err("no free dashboard port in 8893..8993".into())+    Err(format!("no free dashboard port in {}..{}", PORTS.start, PORTS.end)) }  /// Show this run in its own Hydrogen tab, or say why not. Never opens a second tab for a thread that@@ -772,4 +788,14 @@ mod tests {         assert!(!reusable(&json!({"finished": false, "port": 1, "runId": "x", "lastActive": now})));         assert!(describe(&json!({"name": "AI Flow \u{b7} B", "runDir": "/r", "lastActive": now})).contains("just now"));     }+    /// Issue #201: never the 88xx ports Adom's dock apps use, unless asked for.+    #[test]+    fn the_dashboard_stays_off_the_dock_apps_ports() {+        let c = port_candidates(None, Some(8893));+        assert_eq!(c[0], 18893, "a saved old-range port is not taken again: {:?}", &c[..3]);+        assert!(c.iter().all(|p| *p >= 18893));+        assert_eq!(port_candidates(None, Some(18900))[0], 18900, "a saved new-range port is reused");+        assert_eq!(port_candidates(Some(8895), Some(8893))[0], 8895, "an explicit --port is honoured");+    }+ }
crates/adom-aiflow/src/main.rs+6−2
@@ -158,7 +158,7 @@ enum Cmd {     /// `timeline list`: every AI Flow tab (registry ~/.adom/aiflow/dashboards.json reconciled with Hydrogen): name, board, lane, AI thread, run, step, finished, server, last active; AI Flow tabs not in the registry show as unknown.     /// `timeline close --run R | --name N | --finished | --stale`: remove only AI Flow tabs and stop their own verified servers.     /// One tab per run: `open --reuse-tab "<name>"` shows this run in another run's AI Flow tab (renamed in place); `open --new-tab --why "<reason>"` opens a second tab for the same thread.-    Timeline { #[arg(value_parser = ["open", "serve", "write", "present", "list", "close"])] action: String, #[arg(long, default_value_t = 8893)] port: u16, #[arg(long)] json: Option<String>,+    Timeline { #[arg(value_parser = ["open", "serve", "write", "present", "list", "close"])] action: String, #[arg(long, default_value_t = 18893)] port: u16, #[arg(long)] json: Option<String>,     /// open: show this run in that AI Flow tab instead of a new one (the tab is renamed in place)     #[arg(long)] reuse_tab: Option<String>, /// open: a second AI Flow tab for this thread (needs --why)     #[arg(long)] new_tab: bool, /// open --new-tab: why the human needs this run on screen beside another (recorded in the registry)@@ -6608,7 +6608,11 @@ fn main() {             match action.as_str() {                 "write" => ok(&format!("timeline written: step {} of {}, {} done", tl["position"], tl["total"], tl["done"]), &[]),                 "serve" => {-                    timeline_server::serve(&tdir,*port).unwrap_or_else(|e|err(&e,&["Use the existing run server; do not create another dashboard.".into()]));+                    timeline_server::serve(&tdir,*port).unwrap_or_else(|e|{+                        // issue #201: say who holds the port, so nobody has to hunt for it+                        let holder=std::process::Command::new("ss").args(["-ltnpH",&format!("sport = :{port}")]).output().ok().map(|o|String::from_utf8_lossy(&o.stdout).to_string()).and_then(|t|{let i=t.find("users:((\"")?+9;let rest=&t[i..];let name=&rest[..rest.find('"')?];let pid=rest.find("pid=").map(|j|rest[j+4..].chars().take_while(|c|c.is_ascii_digit()).collect::<String>()).unwrap_or_default();Some(format!("{name} (pid {pid})"))});+                        err(&format!("port {port}: {e}{}",holder.map(|h|format!("; it is held by {h}")).unwrap_or_default()),&[format!("Pass another port: `timeline serve --port <free port>` (AI Flow's own dashboards use {}..{}, clear of the 88xx ports Adom's dock apps take, such as Parts Search on 8893).",dashboard::PORTS.start,dashboard::PORTS.end),"Use the existing run server; do not create another dashboard.".into()])+                    });                 }                 "present" => {                     let caller=thread(&cli);
docs/release-0.2.75.mdadded+9
@@ -0,0 +1,9 @@+# AI Flow 0.2.75++- **#201 (Glenn): the run's timeline no longer takes Parts Search's port.** The dashboard server used 8893..8993,+  and 8893 is the port Adom Parts Search's dock app uses, so with a run open the Parts Search card could not+  start. AI Flow's dashboards now use 18893..18993, clear of the 88xx ports Adom's dock apps take, and+  `timeline serve` defaults to 18893. A run that saved a port of the old range does not take it again: when its+  own server is still there (verified as this run's), it is stopped and started in the new range, and the run's+  tab follows the new URL. An asked-for `--port` is honoured; when it is taken, the error names the program+  holding it and suggests another.
package.json+1−1
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.2.74",+  "version": "0.2.75",   "title": "AI Flow",   "description": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. One Rust binary with a crate per step (placement helpers, a grid router with Kelvin taps, pours with keepouts, KiCad's DRC gate, live landing through the KiCad Bridge, copper measurement, current and thermal analysis) and a finish line that refuses an unfinished board. Every command answers with hints for the AI; every turn, its thinking time and every rework loop go into run.jsonl, so Claude, Codex and any other engine are compared on the same flow. KiCad, Fusion 360 and Altium Designer today (the same steps and gates through each tool's bridge); Adom's own web apps next.",   "summary": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. The AI thinks its way from placement through routing, pours, current and thermal analysis to a delivered video; the binary does the fast, deterministic parts of every step, hands back hints, and keeps a ledger of every turn, every return to an earlier step, and the clock from the prompt to done.",
page.json+1−1
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.2.74",+  "version": "0.2.75",   "title": "AI Flow",   "description": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. One Rust binary with a crate per step (placement helpers, a grid router with Kelvin taps, pours with keepouts, KiCad's DRC gate, live landing through the KiCad Bridge, copper measurement, current and thermal analysis) and a finish line that refuses an unfinished board. Every command answers with hints for the AI; every turn, its thinking time and every rework loop go into run.jsonl, so Claude, Codex and any other engine are compared on the same flow. KiCad, Fusion 360 and Altium Designer today (the same steps and gates through each tool's bridge); Adom's own web apps next.",   "summary": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. The AI thinks its way from placement through routing, pours, current and thermal analysis to a delivered video; the binary does the fast, deterministic parts of every step, hands back hints, and keeps a ledger of every turn, every return to an earlier step, and the clock from the prompt to done.",

Comments

No comments yet.

Log in to comment.