Skip to content

feat(contract): cars apart from chargers - #1647

Merged
frahlg merged 2 commits into
masterfrom
claude/car-contract
Oct 16, 2026
Merged

frahlg merged 2 commits into
masterfrom
claude/car-contract

Conversation

@frahlg

@frahlg frahlg commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

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.md in the FTW stack workspace (Fredrik, 15 Oct 2026); docs/cars.md here is the normative text. This PR changes no behaviour: Core keeps no cars until #1650, and does not list der.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.

  • Registry copy from registry: cars apart from chargers, their ops and car.which ftw-webapp#92, byte for byte, with the push catalogue's car.which. car.goal.set and car.soc.set are open to agents under ftw.goals.request, with agent_args of kind car or loadpoint; car.goal.set takes its goal flat for that.
  • Generator: car_soc_sources and car_identified_by become constants (CarSoCVehicle, CarIdentifiedByAsking, …), and car_fields and charger_car_fields become lists that TestCarStructsAreTheRegistrys reads 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, and CarEstimateAfterWh (1 kWh): a car reading stays exact until that much energy has flowed, whatever its age.
  • Watch: NowLoadpoint carries 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.result gains car (a brief car) and charger.
  • FTW Cloud: lp carries carId (opaque), carIdentifiedBy, carCorrected, carCorrectedFrom and choose; its level fields are views of the session's car until a box lists lp.bare. session is Core's own plug-in id. A new car class 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. event gains cars (at most four) and args.session for car.which.
  • Vectors: cloud-vectors.json gains 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 with asking, the session a tap names, the plan, the ops, car.which, where cars appear, and the compat mapping for the level and the goal's days, 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

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 ./... in go/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

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-15T17:40:17.702731Z 3b1b64d PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +68 to +70
// 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"`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +230 to +232
type Cars struct {
Header
Cars []Car `cbor:"cars"`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread docs/cars.md Outdated
Comment on lines +98 to +99
- `GET /api/cars` lists the cars, and `GET /api/loadpoints` gives each
charger `car_id`, `car_identified_by` and `car_corrected_from`. HTTP

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

frahlg added a commit that referenced this pull request Oct 15, 2026
…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>
frahlg added a commit to srcfl/ftw-app that referenced this pull request Oct 15, 2026
…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>
@frahlg
frahlg force-pushed the claude/car-contract branch from 3b1b64d to 20c51f4 Compare October 15, 2026 18:21
@frahlg
frahlg force-pushed the claude/car-contract branch 2 times, most recently from 5edf167 to cdca9be Compare October 15, 2026 20:38
frahlg added a commit to srcfl/ftw-app that referenced this pull request Oct 15, 2026
…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>
@frahlg
frahlg force-pushed the claude/car-contract branch from f4df8e4 to 7d15f35 Compare October 15, 2026 20:57
frahlg added a commit that referenced this pull request Oct 15, 2026
#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>
frahlg added a commit to srcfl/ftw-app that referenced this pull request Oct 15, 2026
* 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>
@frahlg
frahlg force-pushed the claude/car-contract branch from 7d15f35 to 1203d2f Compare October 16, 2026 04:43
frahlg and others added 2 commits October 16, 2026 07:06
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>
@frahlg
frahlg force-pushed the claude/car-contract branch from 1203d2f to a515d7d Compare October 16, 2026 05:06
@frahlg
frahlg merged commit d232ddb into master Oct 16, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant