Flight notifications via byAir: delay, gate, connection risk, inbound aircraft delay, time-to-leave, arrival logistics. NanoClaw per-chat overlay tile.
69
87%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Passed
No known issues
Documents the on-disk state files the flight-assist tile reads and writes. Per coding-policy: stateful-artifacts.
flight-assist (this tile) is the sole owner. Only this skill migrates schema_version. Reader skills (other tiles, agent-side composition, sync-tripit) call the snapshot reader API — read_active_flights_snapshot / read_flight_state_snapshot — which treats a schema_version strictly below the current STATE_SCHEMA_VERSION as "no usable prior state" and returns without rewriting the file. A schema_version ABOVE the current still raises StateError from any reader (forward incompatibility — operators must upgrade the consumer tile, not be told there's nothing on disk).
/workspace/state/flight-assist/FLIGHT_ASSIST_STATE_DIR environment variableconfig.jsonTile-wide configuration set during install via the /setup flow.
{
"schema_version": 2,
"home_address": "1 Infinite Loop, Cupertino, CA 95014",
"min_transfer_minutes": 45
}Fields:
schema_version (int, required) — currently 2home_address (string, optional) — origin used for the time-to-leave capability when no other location is knownmin_transfer_minutes (int, optional) — overrides connection_risk.DEFAULT_MIN_TRANSFER_MINUTES (45) for the connection-risk capability. Set higher for travellers who routinely connect through hubs with longer minimum connect times (LHR, FRA, JFK with terminal change)active-flights.jsonIndex of currently-tracked flight IDs. Refreshed daily by the sync-tripit script.
{
"schema_version": 2,
"flight_ids": [12345, 67890, 11111]
}Fields:
schema_version (int, required) — currently 2flight_ids (list of int, required) — every flight the precheck should pollcurrent-location.jsonLatest known user location. Owner is the host orchestrator, not flight-assist — the host writes this file as Telegram live-location updates and message metadata arrive. flight-assist is a non-owner reader per coding-policy: stateful-artifacts: it consults the snapshot to resolve the time-to-leave origin (ladder lives in precheck._resolve_time_to_leave_origin), validates the documented shape, and returns None on any mismatch instead of raising or migrating.
{
"schema_version": 1,
"latitude": 59.6519,
"longitude": 17.9186,
"captured_at": "2026-05-20T11:42:11Z"
}Fields:
schema_version (int, required) — currently 1. Tracked separately from STATE_SCHEMA_VERSION. The host owns version bumps. read_current_location requires equality with state.CURRENT_LOCATION_SCHEMA_VERSION and returns None on any mismatchlatitude (float, required) — degrees in [-90, 90]longitude (float, required) — degrees in [-180, 180]captured_at (RFC 3339 UTC, required) — when the orchestrator observed the location; consumers apply their own freshness windowOrigin-resolution ladder used by precheck._resolve_time_to_leave_origin:
now - captured_at <= 30 min → formatted "<latitude>,<longitude>" for the Distance Matrix APIhome_address from config.jsonNone — caller skips the maps queryA schema_version mismatch (host bumped) returns None from the reader and the precheck falls back to home_address, matching the no-snapshot-on-disk path.
flight-<flight_id>.jsonPer-flight state record. One file per tracked flight.
{
"schema_version": 2,
"flight_id": 12345,
"code": "AA2414",
"ownership": "mine",
"trip_id": 678,
"scheduled_dep_time": "2026-05-17T09:00:00-07:00",
"scheduled_arr_time": "2026-05-17T11:09:00-07:00",
"dep_airport_id": 20,
"arr_airport_id": 28,
"last_polled_at": "2026-05-17T18:42:11Z",
"last_snapshot": {
"code": "AA2414",
"computed_status": "boarding",
"computed_status_detail": "Boarding starts in 36min",
"computed_phase_progress": 0,
"computed_phase_risk": "low",
"computed_phase_overdue": null,
"dep_gate": "B25",
"arr_gate": "A26",
"dep_terminal": "1",
"arr_terminal": "2",
"dep_time": "2026-05-17T13:00:00-07:00",
"arr_time": "2026-05-17T15:02:00-07:00",
"baggage": null,
"inbound": {
"aircraft_model": "Airbus A320",
"registration": "N660AW",
"flew": false,
"predicted_delay_minutes": null
},
"position_lat": 37.617678,
"position_lon": -122.380227
},
"phase_markers": {
"day_before_fired": false,
"time_to_leave_fired": false,
"boarding_fired": false,
"arrival_logistics_fired": false,
"landed_acknowledged": false,
"connection_at_risk_fired": false
},
"last_wake_at": null,
"last_wake_reason": null
}Top-level fields:
schema_version (int, required) — 2flight_id (int, required) — byAir's flight identifiercode (string, required) — flight number like "AA2414"ownership (string, required) — "mine" or "friend"trip_id (int, required) — byAir's trip identifier (groups multi-leg trips)scheduled_dep_time, scheduled_arr_time (RFC 3339 with offset, required)dep_airport_id, arr_airport_id (int, required) — byAir airport IDslast_polled_at (RFC 3339 UTC, required) — wall-clock time of the last byAir fetch; cadence-gating consults thislast_snapshot (object, optional) — the slim ~1KB operational slice from byAir; null on the first run before the precheck has fetched anythingphase_markers (object, required) — once-per-flight fire-and-forget gates for time-based wakeslast_wake_at (RFC 3339 UTC, optional) — when the agent was last woken for this flightlast_wake_reason (string, optional) — the most recent wake reason for debuglast_snapshot fields (mirrors the post-filter byAir slice — see byair_client.py's get_flight() output; this dict is what wake_rules.py will diff against in PR #6):
code — flight numbercomputed_status — enum: "scheduled", "check_in_open", "boarding", "departed", "en_route", "landed", "cancelled", "diverted"computed_status_detail — human-readable phase prose ("Departing in 3h 20min")computed_phase_progress (float 0..1, optional) — elapsed fraction of the current phasecomputed_phase_risk (string, optional) — "low"/"warning"/"danger"; only for check_in_open and boardingcomputed_phase_overdue (bool, optional) — true when departed/en_route is overduedep_gate, arr_gate (string, optional) — gate numbers as they appear on the boarddep_terminal, arr_terminal (string, optional)dep_time, arr_time (RFC 3339 with offset, optional) — actual times, may be ahead of or behind scheduledbaggage (string, optional) — baggage carousel claim once revealedinbound (object, optional) — Find My Plane data: aircraft_model, registration, flew, predicted_delay_minutesposition_lat, position_lon (float, optional) — last known aircraft positionphase_markers booleans — true means the corresponding time-based wake event has already fired and won't fire again for this flight:
day_before_fired — T-24h sanity checktime_to_leave_fired — Traffic-aware leave-by alertboarding_fired — Status transition to boardingarrival_logistics_fired — T-arr−15min logistics pushlanded_acknowledged — User acknowledged the landing notificationconnection_at_risk_fired — Cross-flight: projected transfer window on this leg-2 has fallen below min_transfer_minutes. Carried on the leg-2 (downstream) record so the marker survives leg-1 landingEvery write_* helper uses write-to-tmp + os.replace in the same directory so a kill mid-write doesn't leave a half-written file. Cross-device renames are not atomic — the state dir must live on a single filesystem.
Today STATE_SCHEMA_VERSION is 2.
state.py's read helpers enforce these rules on schema_version:
StateError (forward incompatibility; never silently downgrade)_migrate, rewrite at the current version, return the upgraded payloadbool) → StateError with actionable repair messageNon-owner readers (sync-tripit, future cross-tile composition) call the dedicated snapshot entry points: read_active_flights_snapshot() and read_flight_state_snapshot(flight_id). These mirror the owner-side functions' return shapes but treat a schema_version strictly LESS THAN STATE_SCHEMA_VERSION as "no usable prior state" (return [] / None) without invoking _migrate. A schema_version ABOVE the current still raises StateError from the snapshot path (forward incompatibility); so does corrupt JSON or a missing required field at the current schema. Only the owner skill (flight-assist, this tile, via state.py:_migrate) migrates.
Per-flight state: phase_markers gains connection_at_risk_fired: false. The owner-side migration in state.py:_migrate adds the missing key on first read and rewrites the file at v2. Config and active-flights files have no shape change at v2 — they receive a schema_version bump only.
When adding or renaming a field:
STATE_SCHEMA_VERSION in state.py and document the new shape in this filestate.py that reads old schema_version and rewrites the upgraded shape via the owner-skill code path (the precheck, the agent on wake, sync-tripit — every entry point that uses read_flight_state from inside flight-assist)read_active_flights_snapshot / read_flight_state_snapshot instead of the owner-side helpers; the snapshot readers treat any mismatched schema_version as "no usable prior state" and return without rewriting. Migration happens exclusively on the owner-skill's next read## Unreleased describing the version bump and the owner-side migration path