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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,22 @@ A running server reports its version at `/health`.

## [Unreleased]

## [0.4.7] - 2026-11-12

### Fixed

- SimBrief generation no longer clicks Route Finder. When no route was agreed, the form is
filled and Generate Flight is clicked directly, keeping SimBrief's own route suggestion.
Route Finder had hung past its 30-second wait on KEWR–EIDW, so Generate was never reached
and the leg failed with `TimeoutError`. A local live check of KEWR–EIDW (B752, no route)
generated in 35 seconds with a NAT-track route ending in the OLAP4L arrival.
- Removed the pre-Generate stop for an empty or direct-only automatic route; the generated
OFP's route is reported in the briefing. Explicit routes are still filled and must match.
- Plan verification ignores the explicit `DCT` tokens SimBrief's OFP writes between fixes
(`DCT MERIT DCT HFD`) when comparing with the form's route (`MERIT HFD`). Without this the
SimBrief-chosen KEWR–EIDW route failed finalization as `plan_mismatch`. Any other fix or
airway difference still fails.

### Documentation

- Refresh the README for v0.4.6: SimBrief sign-in is on demand (no daily keep-alive), approval
Expand Down Expand Up @@ -279,6 +295,7 @@ First public release, and the first version live on the self-hosted Cloudflare d
of blocking it permanently ([#3](https://github.com/IntelIP/InfiniteFlightAgent/pull/3)).

[Unreleased]: https://github.com/IntelIP/InfiniteFlightAgent/compare/v0.4.6...HEAD
[0.4.7]: https://github.com/IntelIP/InfiniteFlightAgent/compare/v0.4.6...v0.4.7
[0.4.6]: https://github.com/IntelIP/InfiniteFlightAgent/compare/v0.4.5...v0.4.6
[0.4.5]: https://github.com/IntelIP/InfiniteFlightAgent/compare/v0.4.4...v0.4.5
[0.4.4]: https://github.com/IntelIP/InfiniteFlightAgent/compare/v0.4.3...v0.4.4
Expand Down
2 changes: 1 addition & 1 deletion docs/backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ This proves the exercised dispatch integration, not simulator import or aircraft
| Surface | Current capability | Remaining work |
| --- | --- | --- |
| Foundation — SETUP-01, BUILD-01/02 | Locked Python application, Deep Agent, MCP, transactional storage/checkpoints, CI and product-validation manifest are installed; deployed generation is observed | Whole-operation progress, cancellation/deadlines and intended-client acceptance remain separate tasks |
| SimBrief — DISC-03/06, BUILD-04 | Browser generation, Route Finder, exact-input approval/readback, retained route/navlog correlation and two retained legs are observed | More dispatch inputs and broader live-provider coverage; controlled recovery scenarios do not prove every provider option |
| SimBrief — DISC-03/06, BUILD-04 | Browser generation (SimBrief-chosen route when none is agreed), exact-input approval/readback, retained route/navlog correlation and two retained legs are observed | More dispatch inputs and broader live-provider coverage; controlled recovery scenarios do not prove every provider option |
| Flight research — BUILD-03 | Dated FlightAware research, connection checks, reported gates and explicitly unverified spawn suggestions are implemented | Verified simulator stand mapping, private configuration evidence and import behavior |
| Thrust — BUILD-05, VERIFY-01 | Labeled A20N/A21N/B38M estimates and condition-specific observed N1 calibration are implemented | Zero aircraft have verified support under the accepted definition; engine/build applicability and simulator evidence remain open |
| Trip management and evaluation | Saved legs/briefings, approved revisions, recoverable validation/provider guidance and a matched 28-case whole-conversation MCP/recovery suite are shipped | Durable progress/cancellation and more dispatch/configuration inputs remain planned |
Expand Down
2 changes: 1 addition & 1 deletion docs/implementation/agent-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ in force: never regenerate merely because waiting timed out.
| Idle between browser operations | 30 seconds, checked at one-second intervals |
| Browserbase HTTP request | 8 seconds; no automatic SDK retries |
| Playwright navigation / ordinary action | 20 / 10 seconds |
| Route Finder wait | 30 seconds |
| Airport/date lookup wait before Generate | 30 seconds |
| Generate completion polling | At most 60 iterations, with the session deadline still enforced |
| Owner-thread cleanup wait | 12 seconds, then remote release |

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "infinite-flight-dispatcher"
version = "0.4.6"
version = "0.4.7"
description = "AI dispatcher for Infinite Flight: real flights, SimBrief plans, and takeoff N1 estimates over MCP"
requires-python = ">=3.11"
license = "GPL-3.0-only"
Expand Down
11 changes: 8 additions & 3 deletions src/ifdispatcher/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,9 +200,7 @@ def save(self, trip: str, leg: str, revision: int, raw: bytes, static_id: str |
meta = json.loads(meta_row[0]) if meta_row else {}
readback = ((meta.get("generation") or {}).get("values") or {}).get("route")
route = (expected.get("route") or "").strip() or readback
if route and " ".join(route.upper().split()) != " ".join(
(normalized["flight_route"] or "").upper().split()
):
if route and _route_tokens(route) != _route_tokens(normalized["flight_route"]):
raise ValueError(
"identity_mismatch: route differs from approved input or form readback"
)
Expand Down Expand Up @@ -422,6 +420,13 @@ def open_store() -> Store:
FETCHER = "https://www.simbrief.com/api/xml.fetcher.php"


def _route_tokens(route: str | None) -> list[str]:
"""Route fixes/airways for comparison. SimBrief's OFP writes direct legs as explicit
`DCT` tokens (`DCT MERIT DCT HFD`) that its form shows without (`MERIT HFD`); a `DCT`
between two fixes means the same route either way."""
return [token for token in (route or "").upper().split() if token != "DCT"]


def _fetch_params(alias: str | None, static_id: str | None, userid: str | None) -> dict:
if not alias and not userid:
raise ValueError("missing_configuration: set SIMBRIEF_ALIAS or SIMBRIEF_USERID")
Expand Down
21 changes: 6 additions & 15 deletions src/ifdispatcher/simbrief_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -653,7 +653,7 @@ def form_values(fields: dict[str, Any]) -> dict[str, str]:


def form_mismatches(fields: dict, want: dict, got: dict) -> dict:
"""Unspecified routes use Route Finder; explicitly agreed fields must match."""
"""Unspecified routes are left to SimBrief; explicitly agreed fields must match."""
expected = dict(want)
if not (fields.get("route") or "").strip():
expected.pop("route", None)
Expand Down Expand Up @@ -728,15 +728,13 @@ def on_dialog(dialog):
page.select_option("#origrwy", fields["runway"])
if not open_prompts:
# Airport/date/runway lookups can replace the route asynchronously.
# Finish those first, then apply the approved route or run Route Finder.
# Finish those first, then apply the approved route. Without an agreed route
# the form goes straight to Generate Flight: SimBrief chooses the route itself.
# Route Finder is never clicked; it hung past its wait on oceanic legs.
page.wait_for_function("() => jQuery.active === 0", timeout=30_000)
open_prompts = page.evaluate(VISIBLE_MODALS_JS)
if not open_prompts:
if want["route"]:
page.fill("#route", want["route"])
else:
page.get_by_text("Find Route", exact=True).click()
page.wait_for_function("() => jQuery.active === 0", timeout=30_000)
if not open_prompts and want["route"]:
page.fill("#route", want["route"])
open_prompts = page.evaluate(VISIBLE_MODALS_JS)
got = {k: page.input_value(f"#{k}") for k in want}
got["tlr"] = page.evaluate("() => document.getElementById('tlr').checked")
Expand All @@ -756,13 +754,6 @@ def on_dialog(dialog):
return {**result, "status": "needs_review", "prompts": open_prompts + dialogs}
if mismatched:
return {**result, "status": "fill_mismatch", "mismatched": mismatched}
if not want["route"] and got["route"].strip().upper() in ("", "DCT", "DIRECT"):
return {
**result,
"status": "needs_review",
"reason": "Route Finder returned no waypoint routing. Supply a route or "
"explicitly approve DCT before generating.",
}
if not click:
return {**result, "status": "filled"}
if before_click:
Expand Down
30 changes: 30 additions & 0 deletions tests/test_routing_evidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,33 @@ def test_missing_route_and_navlog_remain_unavailable(tmp_path, monkeypatch):
assert result["synopsis"]["navlog_fix_count"] is None
assert result["synopsis"]["navlog_idents"] is None
assert "Route:** unavailable" in result["briefing_markdown"]


READBACK = "MERIT HFD PUT BOS TUSKY FORTE NICSO NATS BEXET OLAPO OLAP4L"


@pytest.mark.parametrize(
"returned,matches",
[
(
"DCT MERIT DCT HFD DCT PUT DCT BOS DCT TUSKY DCT FORTE DCT NICSO NATS BEXET "
"DCT OLAPO OLAP4L",
True,
),
("DCT MERIT DCT HFD DCT PUT DCT BOS DCT TUSKY DCT FORTE DCT NICSO NATS BEXET", False),
("DCT", False),
],
)
def test_automatic_route_ignores_only_explicit_dct_tokens(tmp_path, returned, matches):
"""Observed live (KEWR-EIDW, v0.4.7 check): the form shows SimBrief's route without
DCT tokens, and the OFP writes the same route with explicit DCT between fixes."""
store = Store(tmp_path)
prepared(store)
meta = store.read_meta("routing", "1", 1)
meta["generation"] = {"status": "generated", "values": {"route": READBACK}}
store.save_meta("routing", "1", 1, meta)
if matches:
assert store.save("routing", "1", 1, ofp(returned), None)["data"]["flight_route"]
else:
with pytest.raises(ValueError, match="identity_mismatch: route"):
store.save("routing", "1", 1, ofp(returned), None)
37 changes: 19 additions & 18 deletions tests/test_simbrief_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,16 +90,22 @@ def test_keepalive_route_respects_the_kill_switch(monkeypatch):
@pytest.mark.parametrize(
"route,found,prompts,timeout,status,finder_clicks",
[
(None, "DOTSS2 CLEEE BLH J169 KOFFA SNDBR3", [], False, "generated", 1),
(" ", "JCOBY4 SWANN PHLBO4", [], False, "generated", 1),
# No agreed route: SimBrief's own route suggestion stays and Generate is clicked
# directly. Route Finder is never clicked (it hung past its wait on KEWR-EIDW).
(
None,
"MERIT HFD PUT BOS TUSKY FORTE NICSO NATS BEXET OLAPO OLAP4L",
[],
False,
"generated",
0,
),
(" ", "JCOBY4 SWANN PHLBO4", [], False, "generated", 0),
(None, "", [], False, "generated", 0),
("DCT", "unused", [], False, "generated", 0),
("JCOBY4 SWANN PHLBO4", "unused", [], False, "generated", 0),
(None, "", [], False, "needs_review", 1),
(None, "DCT", [], False, "needs_review", 1),
(None, "", ["No route found"], False, "needs_review", 1),
(None, "EARLY SUGGESTION", ["Native: Route failed"], False, "needs_review", 1),
(None, "", ["No Alternates Found"], False, "needs_review", 0),
(None, "", [], True, "timeout", 1),
(None, "", [], True, "timeout", 0),
],
)
def test_generation_waits_for_routing_before_claiming_generate(
Expand Down Expand Up @@ -149,21 +155,18 @@ def dispatch_event(self, *args):
pass

def wait_for_function(self, script, **kwargs):
if self.pending_route:
if "jQuery.active" in script:
if timeout:
raise TimeoutError("Route Finder did not finish")
self.values["route"] = found
self.pending_route = False
raise TimeoutError("Airport lookups did not finish")
if not self.values["route"]:
# SimBrief's own suggestion after the airport/date lookups finish.
self.values["route"] = found

def evaluate(self, script, arg=None):
if script == simbrief_browser.SET_DATE_JS:
self.values["date"] = "09 Nov 2026 - 15:00"
elif script == simbrief_browser.VISIBLE_MODALS_JS:
return (
[p for p in prompts if not p.startswith("Native:")]
if self.finder_clicks or "Alternates" in " ".join(prompts)
else []
)
return list(prompts)
elif script == simbrief_browser.SET_CHECKBOX_JS:
self.values["tlr"] = arg[1]
else:
Expand Down Expand Up @@ -245,8 +248,6 @@ def confirm_time(page):
assert result["values"]["route"] == (
route.strip() if route and route.strip() else found
)
elif not prompts:
assert "explicitly approve DCT" in result["reason"]
assert page.finder_clicks == finder_clicks
assert page.generate_clicks == len(claims) == int(status == "generated")
assert page.dialog_callback is None
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading