Repository navigation
feat(contract): cars apart from chargers - #1647
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3b1b64da5a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // Cars are opaque car ids, in the box's order: for car.which, the cars | ||
| // the phone offers one button each. | ||
| Cars []string `cbor:"cars,omitempty"` |
There was a problem hiding this comment.
Preserve car IDs when decoding alert pushes
For a car.which APNs alert, ReadAlert successfully decodes Event.Cars but then discards it because AlertView has no corresponding field and the return at lines 245–246 copies only Args, Src, and LP. A notification extension following this reference therefore cannot determine which cars should become buttons or map a tap to session.car.set; carry the IDs through AlertView and cover them in the push vectors.
Useful? React with 👍 / 👎.
| type Cars struct { | ||
| Header | ||
| Cars []Car `cbor:"cars"` |
There was a problem hiding this comment.
Track the car class sequence in heartbeats
When a car update is dropped or held back but a later hb arrives, the phone cannot detect that its stored car goal or preference is obsolete: HB still names only liveSeq and lpSeq (lines 300–301), even though this adds car as an independent state class. Add a carSeq heartbeat field and apply the same behind/stale handling used for lpSeq, so a current heartbeat cannot make stale car state appear current.
AGENTS.md reference: AGENTS.md:L22-L23
Useful? React with 👍 / 👎.
| - `GET /api/cars` lists the cars, and `GET /api/loadpoints` gives each | ||
| charger `car_id`, `car_identified_by` and `car_corrected_from`. HTTP |
There was a problem hiding this comment.
Mark the unimplemented car API as proposed
This section states that GET /api/cars and the new loadpoint fields exist, but a repository-wide search finds no route or handler for /api/cars, and this change explicitly adds only the contract. Readers will treat this normative document as shipped behavior; mark these sections as proposed until the Core implementation lands, as required by the project guide.
AGENTS.md reference: AGENTS.md:L142-L145
Useful? React with 👍 / 👎.
…r beside it A car is its own entity apart from the charger (#1647), and in that contract cars means real cars. The plan's per-charger share is now chargers/PlanCharger, keyed by the loadpoint id, with an optional carId: the session's car, empty until Core keeps cars. Whatever applies session.car.set requests a replan, and the plan follows as every replan does. The vector pins carId too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…de it srcfl/ftw#1643 now sends chargers/PlanCharger keyed by the loadpoint id, with an optional carId (the session's car, empty until Core keeps cars), since in the car contract (srcfl/ftw#1647) cars means real cars. The vector Core pins carries carId. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3b1b64d to
20c51f4
Compare
5edf167 to
cdca9be
Compare
…de it srcfl/ftw#1643 now sends chargers/PlanCharger keyed by the loadpoint id, with an optional carId (the session's car, empty until Core keeps cars), since in the car contract (srcfl/ftw#1647) cars means real cars. The vector Core pins carries carId. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
f4df8e4 to
7d15f35
Compare
#1643) * feat(appproto): push the plan to streaming phones when the box replans The app read the car's plan only when the car started or stopped drawing, so a car plugged in for a night charge kept the old plan on the phone while the box's own page showed the new one. The planner now says when what a plan reader sees moves: a published plan, or the newest replan ending without one. One feed per box waits 250 ms, so a change that replans twice is one push, skips while a newer plan is being made, and sends the session plan to every streaming phone whose role may read the plan. The plan carries each car's windows, plug state, pending/outdated and shortfall, read the way GET /api/loadpoints reads them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(appproto): pin the plan-with-cars bytes the apps decode Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(appproto): key the plan's share by charger, with the session's car beside it A car is its own entity apart from the charger (#1647), and in that contract cars means real cars. The plan's per-charger share is now chargers/PlanCharger, keyed by the loadpoint id, with an optional carId: the session's car, empty until Core keeps cars. Whatever applies session.car.set requests a replan, and the plan follows as every replan does. The vector pins carId too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(appproto): the plug-in replans at once and the phone hears it Review of #1643 (S2, S3, N1, N3, N4, N6, N8, N9; S1 for the app): - A charger's confirmed cable (charging.connected) requests a replan at once, past the reactive check's 10 s and its 30 s gap. A hold enters no plan, and goals and schedules already request their own replans. - The planner tells its listeners when a replan starts, not only when a plan lands or a replan fails, so the phone hears the cable and pending at once; while pending the windows stay those the box acts on. OnPlanChange appends. - The feed sends the first change at once and a burst inside the 100 ms settle as one follow-up, each session on its own goroutine. - A session reads and sends its plan under one lock, so the newest lands last, and skips a push of the plan it already has. - The far windows give way after the far slots, never the session. - Each charger carries gridDeferred and goalRev, so the push is enough for the car view and a goal changed elsewhere is read once. Measured in TestAPlugInReachesAStreamingPhoneAsAPlan with the production reactive check, the plug-in right after a replan: cable on the phone in ~2 ms, the plan with its windows in ~102 ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* iPhone: the plan reaches the car the moment the box replans The app read the car's plan from /api/loadpoints only when the car started or stopped drawing, so a car plugged in for a night charge kept the old plan on the phone while the box's page showed the new one. srcfl/ftw#1643 pushes the session plan when the box replans, with each car's windows, cable, pending/outdated and shortfall. The app takes them at once: Now's strip, the plan view, the car view and PlanShare (the widgets and the watch) move with the push, and a cable that moved is read once for the rest of the car. A box whose plan comes without cars is read each quarter while a car is plugged in, and every 15 s while it says the plan is being made or for 30 s after FTW Cloud says a cable moved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * iPhone: read the plan's share by charger, with the session's car beside it srcfl/ftw#1643 now sends chargers/PlanCharger keyed by the loadpoint id, with an optional carId (the session's car, empty until Core keeps cars), since in the car contract (srcfl/ftw#1647) cars means real cars. The vector Core pins carries carId. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * iPhone: the push is enough for the car view; a stale read keeps the cable Review of app#73 (S1, N13) and the paired Core changes in srcfl/ftw#1643: - Each charger on the plan now carries gridDeferred and goalRev. The car view takes gridDeferred from the push; a goal revision that moves (a goal changed on the box's page or another phone) reads the car once. - While a new plan is made the push says pending and keeps the windows the box goes on acting on, so Now's strip does not blink. - A read that was on the wire while a newer push came keeps the push's cable as well as its plan. - An older box is asked every 15 s for a minute after a cable moved: it notices a cable within 10 s, or 40 s after its own replan. - The demo box says pending when its replan starts, as Core now does, and can hold an answer back (answerDelayMs) to test a stale read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
7d15f35 to
1203d2f
Compare
A car belongs to its owner and visits chargers. The registry copy names the car, its level and goal, the ops that set them, the session a tap names, how the box knows which car is on a charger, and car.which. The generator emits the car names for Go; go/protocol carries Car, CarGoal and ChargerCar; FTW Cloud's wire gains the car class, the charger's car fields and car ids cut per grant. docs/cars.md is the contract. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… charger, flaps and old readings An identifier no car lists makes a visitor only once some car lists identifiers. A car the box made from a charger has it as its usual charger. An unplug counts after 30 seconds; a reading more than an hour older than the plug-in does not start the session. car.goal.set takes its goal flat, as the box's other ops and agent_args do. Results, caps and refusals as Core answers them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1203d2f to
a515d7d
Compare
What
The contract for cars apart from chargers. A car belongs to its owner, not to the home, and visits chargers. The design is
design/vehicles/car-entity-2026-10-15.mdin the FTW stack workspace (Fredrik, 15 Oct 2026);docs/cars.mdhere is the normative text. This PR changes no behaviour: Core keeps no cars until #1650, and does not listder.car. Rebased onto master (after #1642, #1643, #1645, #1646) as one commit, with a docs commit for the second review; the generated files are made again from the registry.car.which.car.goal.setandcar.soc.setare open to agents underftw.goals.request, withagent_argsof kindcarorloadpoint;car.goal.settakes its goal flat for that.car_soc_sourcesandcar_identified_bybecome constants (CarSoCVehicle,CarIdentifiedByAsking, …), andcar_fieldsandcharger_car_fieldsbecome lists thatTestCarStructsAreTheRegistrysreads back against the struct tags; HTTP spells each name in snake_case.go/protocol/cars.go:Car,CarGoal,ChargerCar, the op names with their arguments, andCarEstimateAfterWh(1 kWh): a car reading stays exact until that much energy has flowed, whatever its age.NowLoadpointcarries the car's level fields, how the box knows the car, and while asking the session and the cars to choose from. Car ids there are 16 bytes to fit lane 0; an asking charger with four cars takes about 180 B.cmd.resultgainscar(a brief car) andcharger.lpcarriescarId(opaque),carIdentifiedBy,carCorrected,carCorrectedFromandchoose; its level fields are views of the session's car until a box listslp.bare.sessionis Core's own plug-in id. A newcarclass carries each car's level and goal and nothing that names a car, in full every 15 minutes; its test is now an allowlist. The opaque car id is HMAC-SHA256(grantKey,car\0‖ car id), so the service cannot follow a car between an owner's homes.eventgainscars(at most four) andargs.sessionforcar.which.cloud-vectors.jsongains two car ids and four messages at the end, among them a level read before the car last left a charger, which is an estimate. The existing vectors keep their bytes and signatures.docs/cars.md: owners and ids, who sees what, the level rule, the identification order withasking, the session a tap names, the plan, the ops,car.which, where cars appear, and the compat mapping for the level and the goal'sdays, as tables. After the second review: an identifier no car lists makes a visitor only once some car lists identifiers; a car the box made from a charger is named there while every other car that could be here has a usual charger elsewhere; an unplug counts after 30 seconds; a reading more than an hour older than the plug-in does not start the session; the caps and refusals Core answers.Paired PRs
Contract.swift.Merge order: srcfl/ftw-webapp#92, then this PR and srcfl/ftw-app#70, then #1650. The pair lines name commits rather than branches, so they still resolve once the other side has merged and its branch is gone.
Contract-pair: srcfl/ftw-webapp@9433bdc
Tests
go test ./...ingo/protocol: pass, including the registry, allowlist and golden-vector checks. The vectors file only gains lines.go test -p 2 ./internal/appproto/... ./internal/apiauth/... ./internal/notifications/ ./internal/api/: pass.🤖 Generated with Claude Code