app
Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.
← Commit history
log_tail: entries, next, truncated
5 files changed
+30−15
CHANGELOG.md+2−2@@ -6,8 +6,8 @@ Canonical: https://wiki.adom.inc/adom/fusion-bridge ยท GitHub backup: `adom-inc/ ## 1.13.29 (unreleased) - `fusion_log_tail` (the standard's log_tail, agreed on eda-bridge-standard#29): Fusion's own log since a byte offset,- every line or with `errorsOnly` only error lines and alert toasts, at most `maxBytes`, with `nextOffset` for the next- read and `rotated` when Fusion started a new log. `fusion_alerts` takes the same `sinceOffset` cursor and now+ `entries` [{level, text}] (alert, error, warning, info; `errorsOnly` keeps all but info), at most `maxBytes`+ (default 200000), `next` for the next read, `truncated`, and `rotated` when Fusion started a new log. `fusion_alerts` takes the same `sinceOffset` cursor and now always answers `nextOffset`. ## 1.13.28 (2026-10-30)
contracts/describe.json+13−7@@ -165,8 +165,8 @@ { "example": {}, "input": {- "errorsOnly": "bool: only error lines and alert toasts",- "maxBytes": "int, default 400000",+ "errorsOnly": "bool: only errors, warnings and alert toasts",+ "maxBytes": "int, default 200000", "sinceOffset": "int: byte offset into Fusion's log (the standard's log_tail): every line since it", "sinceSec": "optional float (default 300)" },@@ -174,10 +174,13 @@ "name": "fusion_alerts", "output": { "alerts": "[{at,text}]",+ "entries": "[{level, text}] (with sinceOffset)", "lines": "[str] (with sinceOffset)", "logFile": "str",+ "next": "int", "nextOffset": "int: pass as sinceOffset next time",- "rotated": "bool: a new log was started, so the read began at 0"+ "rotated": "bool: a new log was started, so the read began at 0",+ "truncated": "bool" }, "statusVerb": null, "summary": "Fusion's OWN alert toasts, read from its log. Fusion draws error/warning toasts in CEF: they are invisible to Win32 enumeration AND to UIA, so a verb can look successful while Fusion is visibly complaining. This reads them, newest last.",@@ -3666,18 +3669,21 @@ "sinceOffset": 0 }, "input": {- "errorsOnly": "bool",- "maxBytes": "int, default 400000",+ "errorsOnly": "bool: only errors, warnings and alert toasts",+ "maxBytes": "int, default 200000", "sinceOffset": "int, default 0 (the start of the current log)" }, "longRunning": false, "name": "fusion_log_tail", "output": { "alerts": "[{at,text}]",+ "entries": "[{level: alert|error|warning|info, text}] newest last", "lines": "[str]", "logFile": "str",- "nextOffset": "int",- "rotated": "bool"+ "next": "int: pass as sinceOffset next time",+ "nextOffset": "int (same as next)",+ "rotated": "bool",+ "truncated": "bool: more was written than maxBytes" }, "statusVerb": null, "summary": "What Fusion wrote to its own log since a byte offset (the standard's log_tail): every line, or with errorsOnly only error lines and alert toasts, plus the alerts among them and nextOffset for the next read. Catches a verb that looked successful while Fusion complained: Fusion draws its toasts in CEF, which neither window enumeration nor UI Automation sees. The same reader as fusion_alerts.",
describe.py+2−2@@ -808,9 +808,9 @@ _T = [ ("fusion_board_info", "Raw board geometry XML (copper/arcs/lines) for the open board.", {}, {"rawXml": "str"}, 60, None, False, {}), ("fusion_alerts", "Fusion's OWN alert toasts, read from its log. Fusion draws error/warning toasts in CEF: they are invisible to Win32 enumeration AND to UIA, so a verb can look successful while Fusion is visibly complaining. This reads them, newest last.",- {"sinceSec": "optional float (default 300)", "sinceOffset": "int: byte offset into Fusion's log (the standard's log_tail): every line since it", "maxBytes": "int, default 400000", "errorsOnly": "bool: only error lines and alert toasts"}, {"alerts": "[{at,text}]", "lines": "[str] (with sinceOffset)", "nextOffset": "int: pass as sinceOffset next time", "rotated": "bool: a new log was started, so the read began at 0", "logFile": "str"}, 30, None, False, {}),+ {"sinceSec": "optional float (default 300)", "sinceOffset": "int: byte offset into Fusion's log (the standard's log_tail): every line since it", "maxBytes": "int, default 200000", "errorsOnly": "bool: only errors, warnings and alert toasts"}, {"alerts": "[{at,text}]", "entries": "[{level, text}] (with sinceOffset)", "next": "int", "truncated": "bool", "lines": "[str] (with sinceOffset)", "nextOffset": "int: pass as sinceOffset next time", "rotated": "bool: a new log was started, so the read began at 0", "logFile": "str"}, 30, None, False, {}), ("fusion_log_tail", "What Fusion wrote to its own log since a byte offset (the standard's log_tail): every line, or with errorsOnly only error lines and alert toasts, plus the alerts among them and nextOffset for the next read. Catches a verb that looked successful while Fusion complained: Fusion draws its toasts in CEF, which neither window enumeration nor UI Automation sees. The same reader as fusion_alerts.",- {"sinceOffset": "int, default 0 (the start of the current log)", "maxBytes": "int, default 400000", "errorsOnly": "bool"}, {"lines": "[str]", "nextOffset": "int", "rotated": "bool", "alerts": "[{at,text}]", "logFile": "str"}, 30, None, False, {"sinceOffset": 0, "errorsOnly": True}),+ {"sinceOffset": "int, default 0 (the start of the current log)", "maxBytes": "int, default 200000", "errorsOnly": "bool: only errors, warnings and alert toasts"}, {"entries": "[{level: alert|error|warning|info, text}] newest last", "next": "int: pass as sinceOffset next time", "truncated": "bool: more was written than maxBytes", "rotated": "bool", "alerts": "[{at,text}]", "lines": "[str]", "nextOffset": "int (same as next)", "logFile": "str"}, 30, None, False, {"sinceOffset": 0, "errorsOnly": True}), ("fusion_web_signin", "Sign a pup browser window into an Autodesk WEB surface (library.io, the hub portal at <hub>.autodesk360.com, Autodesk Account) via Google SSO. Fusion's DESKTOP app being signed in does NOT sign these in - they are separate web sessions. NEVER guesses a credential: a missing password or a 2FA challenge comes back as a typed stop (human_credential_required / second_factor_required / google_sso_unavailable) naming the one-time command the USER runs. The password never passes through this bridge or the AI - it is stored once with ab's credential_set and filled by pup in the background.", {"url": "required str (e.g. https://library.io/)", "sessionId": "optional str (default autodesk-web)", "provider": "optional str google|password (default google)", "username": "optional str (for the Google account chooser)"}, {"success": "bool", "errorCode": "str?", "url": "str", "steps": "[...]"}, 240, _STATUS, True, {"url": "https://library.io/"}),
rust/bridge/src/main.rs+12−3@@ -806,6 +806,12 @@ pub(crate) fn native_alerts(h: &dyn HostBoth, since_sec: f64) -> Value { native_ /// The standard's log_tail on Fusion's AppLogFile: with sinceOffset, every line written since that /// byte offset (errorsOnly: only error lines and alert toasts), at most maxBytes, and nextOffset to /// pass next time. A log shorter than the offset was replaced by a new session's, so it reads from 0.+/// A log line's level as the standard's log_tail names it.+fn log_level(l: &str) -> &'static str {+ let low = l.to_lowercase();+ if l.contains("New Alert message") { "alert" } else if low.contains("error") || low.contains("exception") || low.contains("fail") { "error" } else if low.contains("warn") { "warning" } else { "info" }+}+ fn log_tail_text(path: &std::path::Path, since: u64, max: u64, errors_only: bool) -> (Vec<String>, u64, bool) { use std::io::{Read, Seek, SeekFrom}; let Ok(mut f) = std::fs::File::open(path) else { return (vec![], since, false) };@@ -818,7 +824,7 @@ fn log_tail_text(path: &std::path::Path, since: u64, max: u64, errors_only: bool // stop at the last whole line so the next read starts on one let cut = if end < size { buf.iter().rposition(|b| *b == b'\n').map(|i| i + 1).unwrap_or(0) } else { buf.len() }; let text = String::from_utf8_lossy(&buf[..cut]).to_string();- let lines = text.lines().filter(|l| !l.trim().is_empty()).filter(|l| !errors_only || l.contains("New Alert message") || l.to_lowercase().contains("error")).map(|l| l.chars().take(600).collect()).collect();+ let lines = text.lines().filter(|l| !l.trim().is_empty()).filter(|l| !errors_only || log_level(l) != "info").map(|l| l.chars().take(600).collect()).collect(); (lines, start + cut as u64, rotated) } @@ -859,10 +865,12 @@ pub(crate) fn native_alerts_args(h: &dyn HostBoth, since_sec: f64, args: &Value) let cutoff = now_local_as_utc - since_sec as i64; let log_file = path.to_string_lossy().replace('\\', "/"); if let Some(off) = args["sinceOffset"].as_u64() {- let max = args["maxBytes"].as_u64().unwrap_or(400_000).clamp(1_000, 4_000_000);+ let max = args["maxBytes"].as_u64().unwrap_or(200_000).clamp(1_000, 4_000_000); let (lines, next, rotated) = log_tail_text(&path, off, max, args["errorsOnly"].as_bool() == Some(true)); let alerts = fusion_core::alerts::parse_alert_lines(lines.iter().map(|s| s.as_str()), i64::MIN);- return json!({"success": true, "mutated": false, "lines": lines, "nextOffset": next, "rotated": rotated, "alerts": alerts, "logFile": log_file, "shell": "rust",+ let truncated = std::fs::metadata(&path).map(|m| m.len() > next).unwrap_or(false);+ let entries: Vec<Value> = lines.iter().map(|l| json!({"level": log_level(l), "text": l})).collect();+ return json!({"success": true, "mutated": false, "entries": entries, "next": next, "truncated": truncated, "lines": lines, "nextOffset": next, "rotated": rotated, "alerts": alerts, "logFile": log_file, "shell": "rust", "_hint": "Fusion's own log since sinceOffset (the standard's log_tail). Pass nextOffset next time; rotated:true means Fusion started a new log, so this read began at 0. alerts are the toasts among the lines."}); } let alerts = fusion_core::alerts::parse_alert_lines(text.lines(), cutoff);@@ -884,6 +892,7 @@ mod log_tail_tests { assert!(none.is_empty() && n2 == next); let (errs, _, _) = super::log_tail_text(&p, 0, 400_000, true); assert_eq!(errs.len(), 2);+ assert_eq!((super::log_level("x New Alert message. y"), super::log_level("a warning"), super::log_level("ok")), ("alert", "warning", "info")); let (part, n3, _) = super::log_tail_text(&p, 0, 30, false); assert_eq!((part.len(), n3), (1, 22)); // only the whole first line let (again, _, rot) = super::log_tail_text(&p, 10_000, 400_000, false);
tests/run_verb_tests.py+1−1@@ -432,7 +432,7 @@ for v in ("fusion_stop", "fusion_kill", "fusion_signin", "fusion_signin_2fa", "f t("fusion_action_ledger", "daily", {"limit": 5}, {"ok": True}) t("fusion_addin_status", "daily", {}, {"ok": True}) t("fusion_alerts", "daily", {"sinceSec": 300}, {"ok": True, "fields": ["alerts"]})-t("fusion_log_tail", "daily", {"sinceOffset": 0, "maxBytes": 20000, "errorsOnly": True}, {"ok": True, "fields": ["lines", "nextOffset"]}, "read-only: Fusion's own log since an offset")+t("fusion_log_tail", "daily", {"sinceOffset": 0, "maxBytes": 20000, "errorsOnly": True}, {"ok": True, "fields": ["entries", "next", "truncated"]}, "read-only: Fusion's own log since an offset") t("fusion_verb_times", "daily", {"limit": 20}, {"ok": True, "fields": ["verbs", "since"]}) t("fusion_live_feed", "daily", {"action": "poll"}, {"ok": True}, "one poll of the feed; no waiting") t("fusion_prove_result", "daily", {}, {"okOrCode": "no_prove_run"}, "shape only: the last completed run, or an honest 'none yet'")