Travel assistant for NanoClaw: byAir flight notifications (delay, gate, connection risk, inbound aircraft delay, time-to-leave, arrival logistics), travel-booking gap checks, and nightly TripIt sync. Per-chat overlay tile.
75
93%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
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": 4,
"home_address": "1 Infinite Loop, Cupertino, CA 95014",
"min_transfer_minutes": 45,
"byair_calendar_name": "Flighty Flights",
"byair_calendar_id": "c_abc123@group.calendar.google.com"
}Fields:
schema_version (int, required) — currently 4home_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)byair_calendar_name (string, optional) — display name of the operator's flight calendar (the byAir calendar in tile terms; the operator's is literally titled "Flighty Flights"). Operator-supplied data, not hardcoded in tile code per rules/flight-data-locality.md. The calendar reconcile script matches this name against the live calendar list once to resolve the calendar ID. Absent → calendar reconciliation no-ops (no flight calendar to write to)byair_calendar_id (string, optional) — the resolved Google Calendar ID for the flight calendar, cached by reconcile after its first name match so later cycles skip the lookup. When present it is used directly and byair_calendar_name is not consulted. The Reclaim travel blocks live on the primary calendar (content-classified — there is no dedicated Reclaim calendar), so no config field tracks itactive-flights.jsonIndex of currently-tracked flight IDs. Refreshed daily by the sync-tripit script.
{
"schema_version": 4,
"flight_ids": [12345, 67890, 11111]
}Fields:
schema_version (int, required) — currently 4flight_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": 4,
"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,
"calendar_events": {
"boarding": {
"event_id": "abc123def456",
"calendar_id": "<byair-calendar-id resolved at runtime>",
"managed": "created",
"synced_signature": "2026-05-17T12:24:00-07:00/2026-05-17T13:00:00-07:00"
},
"flight": {
"event_id": "ghi789jkl012",
"calendar_id": "<byair-calendar-id resolved at runtime>",
"managed": "adopted",
"synced_signature": "2026-05-17T13:00:00-07:00/2026-05-17T15:02:00-07:00"
}
}
}Top-level fields:
schema_version (int, required) — 4flight_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 debugcalendar_events (object, optional) — the ledger of flight-assist-owned/adopted Google Calendar events for this flight, keyed by event kind. Absent or {} means none are tracked yet. Validated structurally (object) by state.py; the per-entry shape below is owned and deep-validated by the calendar-reconcile planner (calendar_plan.py) — the same split as last_snapshot ↔ byair_client. The map is the source of truth for O(1) update/delete and doubles as the teardown tombstone when a flight leaves active-flights.json (the reconciler still holds the event IDs after the flight is gone). Tombstone lifecycle: when a flight drops from the upstream index, sync_tripit retains flight-<id>.json instead of deleting it if this map is non-empty; the reconcile sweep then deletes the managed events off the ledger and archives (removes) the state file once teardown settles — every teardown delete succeeded, or the flight has completed and its events are left as a record. A failed delete keeps its entry, so the tombstone is retained for the next cycle's retrycalendar_events entries — one per kind, kind ∈ {boarding, flight}:
boarding — the flight-assist-created boarding block (boarding-start → departure). managed is always "created"; flight-assist owns its full lifecycle (create / shift / delete)flight — the byAir-created flight event flight-assist adopted by tagging. managed is always "adopted"; flight-assist shifts it (delta-only) and deletes it on a true switch/cancel byAir left stale, but never casuallyEach entry's fields:
event_id (string, required) — the Google Calendar event identifiercalendar_id (string, required) — the calendar the event lives in (the byAir calendar for both the boarding block and adopted flight events; byAir writes the flight events there and flight-assist places the boarding block alongside them). The byAir calendar's ID is resolved at runtime via Composio from the operator's flights calendar — never hardcoded in the tilemanaged (string, required) — "created" (flight-assist authored it) or "adopted" (flight-assist tagged a byAir-authored event). Drives delete semantics: created is freely deletable, adopted is deleted only on a true switch/cancelsynced_signature (string, required) — the <start>/<end> instant pair flight-assist last wrote, so the planner can no-op when the live event already matches byAir truth instead of re-writing every cyclelast_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 positionaircraft_model (string, optional) — byAir's top-level aircraft model. Consumed by the calendar reconcile boarding-lead resolver (boarding_lead.py), which falls back to inbound.aircraft_model when this is absent or emptydep_lat, dep_lon, arr_lat, arr_lon (float, optional) — departure/arrival airport coordinates (distinct from position_lat/position_lon, which track the aircraft). The boarding-lead resolver uses them for the transoceanic (TATL/TPAC) check; when any is absent it skips that check and falls back to aircraft size. Populated once the byAir field names for top-level model and airport coordinates are confirmed (#55 runtime facts); until then the resolver runs on inbound.aircraft_model and the narrowbody defaultphase_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 4.
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.
Migrations chain: state.py:_migrate steps a record through every intermediate version in one call (a v1 record runs v1→v2→v3 before returning), so an old file lands at the current version on its first owner-side read.
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.
Per-flight state: gains the calendar_events map (empty {} on migration). The owner-side migration in state.py:_migrate adds the missing key on first read — scoped by the flight-<id>.json filename, not by payload contents, so a config/active-flights file (or any future record that happens to carry a flight_id key) is never given this per-flight-only field — and rewrites the file at v3. Config and active-flights files have no shape change at v3 — they receive a schema_version bump only.
Config: gains two optional calendar-reconcile fields, byair_calendar_name and byair_calendar_id (see config.json above). Both are optional and absent-tolerant, so there is no shape to add on migration — the owner-side state.py:_migrate only bumps the schema_version. Per-flight and active-flights files likewise have no shape change at v4 — 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### block at the top of CHANGELOG.md; the publish stamp step adds the ## <version> — <date> heading (no ## Unreleased section — that heading is forbidden per coding-policy: context-artifacts CHANGELOG Hygiene)skills
check-travel-bookings
flight-assist
nightly-travel-sync
sync-tripit