Status November 12, 2026 (v0.4.6): the Deep Agent runs on GPT-6 Luna through Sign in with ChatGPT, locally (ifd-chat) and as a self-hosted remote MCP server on Cloudflare behind Cloudflare Access (deployed mode, deployment). SimBrief plans are generated by filling the dispatch form in a logged-in Browserbase cloud browser after exact dispatch approval. Sections below are dated; the evaluation tables record the October 2026 runs, and current release evidence is in docs/evidence/. Per-client ChatGPT, Claude and Codex journeys are not yet acceptance-verified.
One Deep Agent handles the conversation and judgment; deterministic tools run the fixed workflow spine (decision and rationale in the technical design discussion, October 30):
| Tool | Behavior |
|---|---|
current_time |
UTC and Washington local time for relative dates |
find_route_flights, find_airport_departures, get_flight_by_number |
FlightAware AeroAPI research (plan) |
thrust_support |
Aircraft/engine/mode registry for the N1 estimator |
dispatch_flight |
Requires user approval (interrupt). Verifies the chosen flight occurrence matches the agreed local date and route, prepares the leg, returns SimBrief inputs |
generate_in_simbrief |
Runs after the approved dispatch: fills SimBrief's dispatch form in the Browserbase cloud browser (persistent logged-in context), reads every value back, answers the known "re-select runways" prompt, stops on any unknown prompt, and clicks Generate once per leg revision (attempt recorded before the click) |
recent_gates |
Departure gates of the most recent completed occurrences of a flight (past 10 days); the briefing uses the latest one, labeled historical, when today's gate is not assigned |
finalize_dispatch |
After the user generates in SimBrief: one fetch, refuses plans generated before the dispatch or departing >3 h from it, estimates thrust (livery engine override), refreshes the gate, returns briefing_markdown composed from data |
The definition uses only fields Managed Deep Agents accepts (model, tools, interrupt_on, prompt in root instructions.md). Hosting on Managed Deep Agents is not possible with ChatGPT-plan usage, which OpenAI limits to locally run personal and open-source projects; a hosted deployment would need an API key or another approved billing path.
Browser generation: use the existing BROWSERBASE_API_KEY credential (Doppler in deployment; the historical local .env path also works). The Browserbase context keeps the SimBrief login. With NAVIGRAPH_USERNAME and NAVIGRAPH_PASSWORD set, a session that finds SimBrief signed out signs in through Navigraph's email/password form by itself (not Google/Apple/Microsoft sign-in, and not past MFA). Without them, uv run ifd-simbrief opens a cloud session and prints a Live View link where the user logs in once. If Browserbase no longer has BROWSERBASE_SIMBRIEF_CONTEXT_ID, a new context is created and its id kept in the store. Login refresh is now on demand; the old scheduled keepalive route does not open a browser. uv run ifd-simbrief inspect lists the dispatch form fields. SimBrief's static_id is a read-only debug field on that page, so plans are correlated by generation time after the click plus route, aircraft, units, and departure. Thrust estimates use the engine SimBrief planned with unless the user asks for another.
uv sync --locked
uv run ifd-login # Sign in with ChatGPT; approve plan usage for "Infinite Flight Dispatcher"
uv run ifd-chat # interactive; approvals prompt in the terminal
uv run ifd-chat --thread chat-1234abcd # resume a saved conversationConfiguration (Git-ignored .env or environment): AEROAPI_KEY, and SIMBRIEF_USERID or SIMBRIEF_ALIAS. Sign-in credentials live in ~/.config/InfiniteFlightDispatcher/ (owner-only); usage is visible in ChatGPT Settings → Usage. Plus plans share a five-hour limit across all apps using the plan, including Codex.
The ChatGPT-plan route accepts only store: false, streamed Responses requests without fields such as temperature or max_output_tokens, developer instead of system messages, and function tools grouped in a namespace; ChatGPTPlanModel in src/ifdispatcher/siwc.py applies these rules (preview limitations).
evals/dispatch_prompts.jsonl holds 100 first-message cases across 11 categories (domestic/international specific and vague, multi-leg, not-sure, private/GA, flight number, departure window, unsupported aircraft, edge cases) with expected first action, clarification topics, legs, and must-not behaviors; python3 evals/validate_dataset.py checks it.
uv run python evals/run_eval.py now runs the synthetic FlightAware replay suite by default,
with $0 FlightAware usage and live ChatGPT-plan model usage. Existing prompts and derived
historical snapshots remain available with --dataset evals/dispatch_prompts.jsonl.
Paid FlightAware checks require --live, an explicit sample, and a batch budget (default $1).
The runner stops at dispatch approval and disables SimBrief actions. See
the evaluation guide for replay, recording, costs and coverage limits.
Stratified pilot, one case per category (11 cases), GPT-6 Luna at low reasoning effort, simulated user up to the SimBrief handoff. Raw transcripts are in Git-ignored artifacts/evals/.
| Run | Fully passing | Notes |
|---|---|---|
| 1 | 6/11 | 4 of 5 failures were grader false negatives (requests phrased without "?"); 1 real: assumed Portland = PDX |
| 2 | 6/11 | Ambiguous-city fix worked; found a tool bug (whole local day rejected when partly beyond AeroAPI's 2-day window, now clipped) and grader gaps |
| 3 | 9/11 (10/11 after grader fixes, re-graded offline) | Remaining: debatable "ask passengers" expectation for a private TBM trip |
Every run: no exceptions, no invented flight numbers, correct leg routing on every dispatch, unsupported-aircraft thrust disclosed, no date assumed. Median about 25 s per case and about 3 AeroAPI result sets per case.
Real agent failures found and fixed by instructions or code: dispatching the wrong day's occurrence of a flight (now refused in dispatch_flight), assuming an ambiguous city, picking an option that violated the user's constraint ("going west" → Lisbon), searching flights for private trips, and re-asking an ambiguous aircraft type instead of proposing one. Known grader gap: constraint violations such as direction are not scored automatically; review transcripts.
Bar: two consecutive full runs on unchanged code with every safety check at 100% (no error, legs/airports match, no invented flight numbers, no assumed date, unsupported thrust disclosed before approval, private trips handled) and at least 90% of cases fully passing. Met by runs 8 and 9.
| Run | Cases passing | Safety | What it found → fix |
|---|---|---|---|
| 3 | 86 | failed | Said "Gander (YQX)" but dispatched CYYT (St. John's); thrust disclosure missed 6/13 → approval summary built from the call (FlightAware airport names, thrust status) and a check_airports tool for private legs. Grader now counts the approval summary as disclosure |
| 4 | 93 | failed | Assumed a date for "over the next week" → ranges are not dates |
| 5 | 91 | failed | Two alternative flights dispatched as the same leg → DispatchApproval refuses several dispatches of one leg before approval |
| 6 | stopped | failed | Deep Agents' general-purpose subagent dispatched without these instructions → task subagent disabled |
| 7 | stopped | failed | OpenAI 503 with max_retries=0 → two retries (model calls have no side effects) |
| 8 | 92 | passed | 20261031T082444Z |
| 9 | 91 | passed | 20261031T091345Z, same code |
Remaining misses are judgment checks, not safety: clarification coverage (asking for aircraft or date in the first reply, 39–43/47) and first action (IFD-096 checks the existing plan before looking up the flight, which is arguably better than the grader's expectation). Each full run costs about $1.75–1.85 in AeroAPI result sets plus ChatGPT-plan usage, and takes about 45 minutes. The live end-to-end set (evals/run_e2e.py, e2e-20261031T054130Z) passed 9/9 before these changes; it has not been re-run on this code.
Decided October 31, 2026 (Hudson): personal, not resold; open source with bring-your-own keys and ChatGPT plan; state on Neon Postgres, files on Cloudflare R2, MCP endpoint on Cloudflare, secrets in Doppler. Infrastructure, publication, and deploys remain separately approved steps; the runbook is in deployment.
- Remote MCP server —
uv run ifd-serve(Streamable HTTP on:8080,/mcpand/health). Tools:plan_flight(message, thread_id?)relays one user message to the agent and returns its reply or anapproval_requiredrequest with the exact dispatch inputs;approve_dispatch(thread_id, approve, note?)answers it (approval generates in SimBrief, about two minutes, and returns the briefing). Turns run in the background: a call that takes longer than 25 seconds returnsstatus: running, andturn_status(thread_id)returns the result when ready. Every request except/healthmust carryX-IFD-Internal-Token(added by the Worker after Cloudflare Access sign-in); a missing token fails closed. - State —
DATABASE_URLswitches the plan store and the LangGraph checkpointer to Postgres (prepared statements off for Neon's pooler). Without it, local SQLite is used. The test suite runs against Postgres whenTEST_DATABASE_URLis set. - ChatGPT plan on the deployed host — following OpenAI's self-hosted VM guidance: sign in locally (
ifd-login), thendoppler run -- uv run ifd-credentials import --movestores the session encrypted in Postgres (Fernet keyIFD_CREDENTIAL_KEY) under a new host ID and removes the local tokens, because the deployed host now owns refreshes and a rotated refresh token invalidates other copies.ifd-credentials statusreports scope and expiry without values. Refreshes run under a Postgres advisory lock and persist the rotated token in the same transaction. - Kill switch —
IFD_PAID_OPERATIONS_ENABLED: unset means enabled (local use); anything other thantrueblocks FlightAware calls, Browserbase generation, and remote agent turns before they start. The Worker sets it explicitly (defaultfalse). - R2 — generation screenshots upload to
screens/<leg tag>/whenR2_ACCOUNT_ID,R2_ACCESS_KEY_ID,R2_SECRET_ACCESS_KEY, andR2_BUCKETare set. - End-to-end checks —
uv run python evals/run_e2e.pydrives the live agent through approval, browser generation, plan matching, thrust, and the briefing for nine scenarios. It writes plans to the SimBrief account and spends Browserbase time.
The release report distinguishes local checks, controlled-provider model evaluations and the passing production MCP trip.
- Ask to list saved trips, inspect a trip, change a saved leg or refresh its briefing. Changes use a new revision and approval; refresh keeps the original OFP and reports changed lines.
list_saved_tripsandconversation_statusare direct remote MCP tools requiring no model invocation. The latter uses the original thread id to recover persisted replies/approvals.- Provide an observed full-throttle N1 and simulator version after a briefing to get a percentage target. Saved readings require matching conditions and expire after seven days.
- Give a full arrival date/time and a sourced gate-to-gate duration for arrival-target planning. The generated plan reports the difference from the target, rather than promising exact arrival.
- Explicit route, runway, passengers and selectable SimBrief airframe id are supported. Custom/ACJ configuration is not inferred from a base aircraft name. Simulator capacity uses the dispatch units and requires a source; missing configuration evidence remains unknown.
- Fuel/range/weight failures and all provider remarks reach the briefing. Infeasible flights withhold thrust-setting instructions, even if SimBrief generated an OFP.
Trace review (read-only unless --enqueue is supplied):
uv run python scripts/review_traces.py --days 7 --limit 50
uv run python scripts/review_traces.py --days 7 --limit 50 --enqueueThe review queue scores whole conversations. Automatic workflow feedback is separate from owner acceptance. Provider usage reports FlightAware list-price estimates and Browserbase elapsed time; unknown invoice costs stay unknown.
Remote MCP tools added for INT-416/417. Neither invokes the model or a provider.
| Tool | Use |
|---|---|
trip_progress(thread_id) |
Durable progress: the current/last turn (queued, running, approval_required, reply, cancelled, deadline_exceeded, failed, or interrupted after a restart), pending/cancelled approval and history, and each leg revision's stage (awaiting_generation, generating, generated_awaiting_finalize, generation_uncertain, generation_failed, cancelled, plan_retained, briefing_ready, infeasible), with completed_legs, uncertain_generations and a next instruction |
cancel_trip(thread_id, reason?) |
Only when the user asks to stop. Records the request, signals a running turn and waits up to 25 seconds for it to reach a safe point. Returns cancelled (with outcome stopped or completed when the turn finished first) or cancelling while a Generate click is finishing |
Cancellation withdraws the pending approval (approve_dispatch then returns cancelled) and
every approved revision whose Generate was never clicked; continuing needs a new approved
revision. Completed plans and briefings are kept. A clicked or uncertain generation is never
withdrawn, cleared or repeated: check SimBrief, then finalize or request a new revision.
IFD_TURN_DEADLINE_SECONDS (default 600) stops a turn at its next safe point and, like a
cancel, withdraws unclicked approved revisions; worker threads
are not killed, so a stalled model or provider call ends when it returns. Browser limits are
unchanged: one active workflow, two waiters, 30-second queue wait, 180-second session.
Deployed in v0.4.2 after exact-image staging acceptance on November 10 UTC (November 9 in New York). The original PR acceptance is retained separately from the release validation. Browserbase's project now reports concurrency 25. The dispatcher deliberately admits just one active browser workflow for its shared SimBrief login. A transactional FIFO in the existing settings store allows two waiting workflows, each waiting at most 30 seconds without opening a browser. Remote agent admission also bounds outstanding turns to six (four workers plus two waiting). Model/research work can continue independently.
BrowserBudgetMiddleware covers the agent's browser and finalization tools; the same guard
at session_scope/connection creation protects direct CLI and Python calls. An invocation
opens its session lazily and reuses it. Human approval pauses exit the scope. Once that
session expires, fails to connect, or is reaped for inactivity, the invocation cannot
silently open another one. Existing generation claims and uncertain-outcome handling remain
in force: never regenerate merely because waiting timed out.
| Control | Limit |
|---|---|
| Active browser workflows sharing the store | 1 |
| Waiting browser workflows | 2 |
| Queue wait without browser usage | 30 seconds |
| Browserbase server session lifetime | 180 seconds |
| 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 |
| 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 |
The watchdog requests remote release even while a command or model response is stalled.
keep_alive=False ends sessions on CDP disconnect; the server lifetime is the independent
backstop if the process dies. Expiry ends browser work; it does not forcibly kill a Python
thread or bound the entire model conversation. Ordinary Playwright waits and the browser
command future are bounded too. Cleanup does not claim success until the provider reports a
terminal session status. Unknown create/release outcomes retain the lease until the possible
provider lifetime has elapsed, with an allowance for the HTTP request and cleanup. Account
ownership remains through latest-OFP retrieval and scope exit, even if idle cleanup has
already closed the browser.
All processes coordinating this login must use the same DATABASE_URL (production) or local
SQLite store. Separate stores and external CLI calls cannot participate in one atomic queue.
A provider preflight also refuses to create while a dispatcher-owned session or the same
saved Context is active. This check detects existing orphans; it cannot make independently
configured clients atomic. Browserbase account limits remain the ultimate provider boundary.
Operator commands, using the existing credential; neither starts a browser:
uv run --locked ifd-simbrief sessions
uv run --locked ifd-simbrief cleanupThe first lists active provider sessions and admission limits. cleanup rechecks and requests
release only for dispatcher-owned sessions at least 180 seconds old; unrelated and younger
sessions are left alone. browser_status provides the read-only inventory over remote MCP.
The old daily keepalive endpoint returns skipped; normal browser work still signs in when
needed.
Usage telemetry includes queue wait, elapsed session time, release-request versus confirmed release, cleanup failures, deadline/idle outcomes, and an explicitly labeled minimum-adjusted minute estimate. Invoice cost stays unknown. The estimate applies the documented one-minute minimum but does not pretend to reproduce the provider's rounding algorithm.
References: Browserbase session lifetime, release API, Context concurrency guidance, and billing minimums.