Skip to content

feat(cloud): send FTW Cloud 48 hours of history tiles (C6) - #1817

Merged
frahlg merged 5 commits into
masterfrom
feat/cloud-c6-tiles
Oct 23, 2026
Merged

frahlg merged 5 commits into
masterfrom
feat/cloud-c6-tiles

Conversation

@frahlg

@frahlg frahlg commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Step C6 of ADR 0008. FTW Cloud keeps the last 48 hours of 5-minute history for each home whose owner turned it on. The iPhone app can then draw a node's history while the box cannot answer.

Paired with srcfl/ftw-cloud#48 (the service) and srcfl/ftw-app#98 (the app). Merge this first. ftw-cloud pins go/protocol at this branch's b10a6939 and then moves the pin to the merge commit.

What changes

Wire: go/protocol/cloudwire and docs/protocol-cloud.md

  • tiles is the app protocol's hist.chunk plus the common header, at 5 minutes and stride 1. It carries grid_w, pv_w, battery_w and load_w, in the order the app asks for them over Noise. A closed tile therefore has the same tile id, etag and bytes as the box serves over Noise.

  • A tile has one of three shapes:

    • closed: the whole tile;
    • whole: the filling tile, with every point, missing from the first bucket still open;
    • append: only the buckets that closed since the last piece.

    Tiles.Check enforces the shapes, Piece cuts a piece and Lay puts pieces back together.

  • A phone sends tiles.get {have}. The service answers with the box's envelopes the phone lacks, then tiles.end {n}.

  • cloud-tile-vectors.json holds:

    • one tile of each shape, signed by the vector box key, and one forged tile;
    • five Lay results;
    • the tiles.get and tiles.end bytes.

    ftw-cloud and the app replay these vectors.

  • docs/protocol-cloud.md now covers:

    • a field table under tiles, so TestFieldsMatchTheProtocolDocument checks it;
    • when the box sends tiles;
    • hello_ok.have;
    • resync {what: tiles};
    • that the snapshot carries no tiles;
    • that tiles outlive the hour of silence;
    • the service's bounds;
    • new rows in the numbers table.
  • ADR 0008's store table gets a row for tiles.

Core: internal/cloudlink and cmd/ftw

  • The link sends tiles only with ftw.history.read in the grant and NTP time. It counts a bucket closed 30 s after it ends, once the history writer has committed it.
  • As each bucket closes, the link sends the filling tile as an append. It sends the tile whole instead the first time on a connection, and after 11 appends. When the tile's last bucket closes, the tile goes closed.
  • At connect and after a resync, the filling tile goes whole. Every closed tile of the last 48 hours that hello_ok.have lacks goes too, newest first.
  • Every hour the link packs the closed tiles again and sends any whose etag changed, so a gap the ledger repaired reaches the service.
  • The link reads the ledger through the same appHistory.Window and protocol.PackTile as the Noise path.
  • Tiles go last in a tick and count against the 20 MB copy budget, at about 100 KB a day. The 250 MB tunnel budget is separate and unchanged.
  • hello.caps lists tiles when the grant allows them.

Decisions to look at

  • Tiles outlive the hour of silence. The doc said the service serves no readings an hour after the box goes quiet. Tiles now stay 48 hours whatever the box does. They are dated history, and C6 exists for a box that is away. A phone still needs a roster in force to get in, which lasts 24 hours from the box's last roster.
  • Assistants still read history from the box (open question). The protocol says an agent row never reads the readable copy, so every agent read is one the box audits and a revoke stops. That rule stays. While the box is away, MCP's history_query now says so and says when FTW Cloud last heard from it (feat: hold 48 hours of history tiles and answer tiles.get (C6) ftw-cloud#48). Should it instead answer from these tiles? That would change the rule, and the box would not see the read. It is not built; it is the owner's call.
  • No ev_w or battery_soc. The box's history serves four series today. The old text said C2 adds the other two, but C2 did not.

Tests

  • make verify passed: tests, vet and build.
  • go test ./cloudwire covers the shapes, Noise equality, what Check refuses, Lay and the vectors.
  • go test -race ./internal/cloudlink covers these cases:
    • at connect, the whole tile and the closed tiles go, each as Noise would serve it;
    • each closed bucket goes as an append until the tile goes whole again;
    • a tile goes closed when its last bucket closes;
    • have saves the service's tiles;
    • a resync sends tiles again, once in five minutes;
    • a reconnect sends what the service lacks;
    • the hourly pass resends a repaired tile;
    • no tiles go without the history scope, without NTP or past the budget;
    • a failing ledger holds tiles back and nothing else.
  • End to end on a laptop: This branch's Core, built with a local endpoint and two simulated drivers, linked over QUIC to ftw-cloud's branch feat/c6-tiles. At connect the box sent the filling tile whole and the four closed tiles of the last 48 hours. They were empty, since that box's ledger was new. At 14:25:30 UTC it sent a one-point append for 14:20–14:25 with the measured values. A small checker read what the service held from /dev/held. Every tile verified with the box key and passed Check, and Lay rebuilt the filling tile through 14:25. Nothing touched the home box, the FTW Cloud host or AWS.

After review

  • Check takes the four series and no other, derived from the registry's names. TileEnvelopeMax (4 KiB) bounds an envelope, so a box cannot pad its tiles.
  • When ftw.history.read leaves the grant, the link forgets what it sent. When history comes back, every tile goes again on the same link (TestHistoryTakenOutAndPutBackSendsEveryTileAgain).
  • The link reads the ledger at most once per tick, one closed tile at a time. It checks the grant again before each tile goes.
  • A later closed tile replaces an earlier one in Lay.
  • cloudlink.PackTile is the one packing path. TestACloudTileIsTheNoiseTile holds it to the Noise tile through appproto's real buildTile.
  • The doc now says what a phone really gets. While the box is away, a phone can read tiles only until the roster runs out, 18 to 24 hours. Each tile is kept until 48 hours after it ends, so up to 60 hours of history. FTW Cloud sends resync for tiles when a roster gives history back or room frees.
  • make verify passes again.

🤖 Generated with Claude Code

frahlg and others added 3 commits October 23, 2026 15:56
A tiles body is the app protocol's hist.chunk with the common header:
closed tiles match the box's Noise answer byte for byte, and the trailing
tile travels whole or as appends that Lay puts back together.
cloud-tile-vectors.json freezes one of each shape for FTW Cloud and the
phone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With ftw.history.read in the owner's grant and NTP time, the box sends
the trailing 5-minute tile as its buckets close (an append, whole once
an hour), each tile as it closes, and at connect or a resync the tiles
of the last 48 hours that hello_ok's have lacks. Closed tiles are packed
from the same ledger read as the app's Noise history, so they are the
same bytes. Tiles go last in a tick and count against the byte budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n seq

seq starts again when the box does, and a phone may still hold pieces
from before the restart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 23, 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-23T14:28:19.479007Z 50bb116 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: 50bb116904

ℹ️ 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 thread go/protocol/cloudwire/tiles.go Outdated
The box sends a closed tile again when its ledger changed under it, as
in the hourly pass. Lay kept the first, so a receiver drew the old gap.
Only a piece of a closed tile is ignored now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frahlg added a commit to srcfl/ftw-app that referenced this pull request Oct 23, 2026
… the first

After a relaunch with the box still away, the tiles file already held
the copy's tiles, FTW Cloud's answer brought nothing new, and the date
line went. Until the box answers, an answer that holds the tiles shown
now dates them as the copy.

lay and the tile store kept the first closed tile they saw. The box
sends a closed tile again when its ledger changed under it, so the later
one now replaces it, as srcfl/ftw#1817's Lay does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…back in the grant

- Check takes the four series and no other, from the registry's names,
  and TileEnvelopeMax bounds an envelope, so a box cannot pad its tiles.
- When history leaves the grant the link forgets what it sent; when it
  comes back every tile goes again on the same link.
- The ledger is read at most once a tick, one closed tile at a time, and
  the grant is checked again before each tile goes.
- cloudlink.PackTile is the one packing path; a test holds it to the
  Noise tile through appproto's buildTile.
- The doc says what a phone really gets: tiles until the roster runs out
  while the box is away, and when FTW Cloud sends resync for tiles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frahlg added a commit to srcfl/ftw-app that referenced this pull request Oct 23, 2026
…e box wins

- check takes the four series and no other, as srcfl/ftw#1817's Check.
- The tile store keeps a closed tile the box sent later over one it sent
  before, whatever order they come in.
- A closed tile from FTW Cloud never replaces one the box sent itself,
  only an earlier one from the copy.
- After a relaunch, what is shown is dated as the copy only when the copy
  reaches as far; the box's own cached history is not labelled as it.
- A test ties HistoryModel.series to CloudWire.tileSeries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frahlg
frahlg merged commit bac597f into master Oct 23, 2026
30 of 32 checks passed
frahlg added a commit to srcfl/ftw-app that referenced this pull request Oct 23, 2026
#98)

* feat(cloud): draw FTW Cloud's history tiles while the box is away

FTW Cloud keeps the box's signed 5-minute tiles of the last 48 hours
(srcfl/ftw docs/protocol-cloud.md, tiles). The phone checks each with the
key the box gave it over Noise, lays the pieces back together as Go's
cloudwire.Lay does, and asks with tiles.get once per cloud session while
the box session cannot answer. Closed tiles are the box's bytes, so they
share the tiles file. The line under the chart dates the copy ("From FTW
Cloud · up to 17:15"); the box's own answer replaces it the moment it
comes. cloud-tile-vectors.json from srcfl/ftw replays here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(demo): a box that goes away, its history from FTW Cloud's copy

-FTWDemoBoxAway YES (Debug only): the demo box names its cloud key in
hello, streams for three seconds without answering hist.query, then goes.
SimulatedCloud runs the app session over loopback with the real Noise
handshake and answers tiles.get with tiles the demo box signed, so the
picture shows the real path: verify, lay, draw, dated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): date FTW Cloud's history before its sum

A long sum under the chart wraps into the second reserved line. With the
date first, that cuts the sum, never the line that says the history is a
copy and how old it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): the time after FTW Cloud's copy ends is not a hole

The sum under the chart counted every step after the copy's last bucket
as "not recorded": at 16:25 with a copy up to 16:20 it said "10 min
not recorded", though the box may well have recorded them. The copy says
nothing about that time, so the history leaves it out; holes inside the
copy still count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): stay dated after a relaunch; a later closed tile replaces the first

After a relaunch with the box still away, the tiles file already held
the copy's tiles, FTW Cloud's answer brought nothing new, and the date
line went. Until the box answers, an answer that holds the tiles shown
now dates them as the copy.

lay and the tile store kept the first closed tile they saw. The box
sends a closed tile again when its ledger changed under it, so the later
one now replaces it, as srcfl/ftw#1817's Lay does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): tiles after review: four series, ordered closed tiles, the box wins

- check takes the four series and no other, as srcfl/ftw#1817's Check.
- The tile store keeps a closed tile the box sent later over one it sent
  before, whatever order they come in.
- A closed tile from FTW Cloud never replaces one the box sent itself,
  only an earlier one from the copy.
- After a relaunch, what is shown is dated as the copy only when the copy
  reaches as far; the box's own cached history is not labelled as it.
- A test ties HistoryModel.series to CloudWire.tileSeries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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