Revise against the review, and rescope to one team
The review AGENTS.md requires had never run on this design; §12 said so. It ran, returned six blocking findings and eight corrections, and Paul ruled the feed's scope while it was out. All of that is applied here. The scope ruling costs this design its headline. It argued that only the home can answer "my things" rather than "this team's things", and per-team concedes exactly that question. §2.1 records the argument as retired rather than deleting it, and re-justifies on what survives: only the home knows which apps a team has instances in, and the alternative — Sheets' home page growing a Docs section — needs Sheets to hold Docs' grants, which is the federation the home spec forbids outright. Per-team is also cheaper on every axis this design worries about: the fan-out is one team's apps, the cache holds less, the oracle surface shrinks. §7 contradicted itself. It said the TTL bounds staleness by a number we choose rather than by a hibernating fleet's wake schedule, and then said a failed pull never evicts a good cached answer. Both cannot hold: if failure preserves rows, a person removed from a team keeps seeing its titles until that instance next answers, which is the hibernation tail the first sentence disclaims. There are two ages now — a refresh TTL, and a hard maximum that discards rows regardless of pull outcome — and the new gate 20 drives the hard one, because a test against the refresh TTL proves the easy half. §10 said a 404 means "this app does not participate", rendered as nothing. That is ruling 5 backwards: a participating app that loses its route in a deploy would vanish silently from every front door. Participation is a registry flag now, and a declared app answering 404 is a gap. The platform edge is a shared cache sitting upstream of both ends. It caches any 2xx GET with a positive max-age, keyed on host, path and query, shared across every visitor, and does not refuse on a request Cookie. So an instance answering public, max-age=60 would have the edge serve person A's rows to the home's next pull for person B — invisible to the home's own cache key. Call 7 now requires no-store from instances, and a cacheable response is a failed pull. §2.1 said the process holds "a cross-app list of titles for one person". It holds one for every person who rendered inside the TTL — a cross-person set, in the process that mints sessions, which §11 lists as never licensed while this built it as a data structure. The cache is bounded now, and the two platform facts the "in RAM" claim leans on (swap, core dumps) are recorded as unestablished and blocking. Two open numbers were not tunables: the hard maximum age, and the minimum pull interval per person and instance — the second because instance-hours bill to the instance's account, so a guest's front door was spending someone else's money on every load. The fan-out budget's number can wait; its behaviour at the limit cannot, because an un-asked instance must render as its own kind of gap or ruling 5 breaks quietly. Also: Go's http.DefaultClient has no timeout and follows redirects, so the client is hardened explicitly — full-body deadline, no redirects (an instance redirecting to a document URL would put that URL in a net/http error string, which §3.4 rules is storage), bounded concurrency so one slow instance cannot exhaust the process that serves sign-in. `since` is named as never persisted, since a durable last-pull cursor is the contentless activity table §11 forbids arriving as an optimisation. §3's replacement clause binds the audience — "and only to that person" — so the operator-view prohibition sits inside the clause rather than in a table. §0's prose form is conceded to move: "never learns what is in anything" now reads as never retains. The gates renumber from 11, since main took gate 10 for no-store. And the AGENTS.md staleness item is closed — main carries all three retractions.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 file changed,
+200
−62
docs/superpowers/specs/2026-09-05-activity-pull-design.md+200 −62
CI passed — run details
Intent
Attributed to this commit: 112 g CO₂e (14250457 tokens). This is an attribution, not a measurement — usage counted from this commit's own marker forward to the next one on design-activity-pull, never backward from it. A turn that produced more than one commit books all of its tokens to the last one, and a concurrent session on this branch can add to the same figure. Some of it was priced using the shared default rate, because no model-specific factor row exists yet for at least one model involved. The Carbon view has the repo total, which is never derived from this attribution.
diff --git a/docs/superpowers/specs/2026-09-05-activity-pull-design.md b/docs/superpowers/specs/2026-09-05-activity-pull-design.md| index 36034da..30371b7 100644 |
| --- a/docs/superpowers/specs/2026-09-05-activity-pull-design.md |
| +++ b/docs/superpowers/specs/2026-09-05-activity-pull-design.md |
| @@ -2,21 +2,30 @@ |
| **Status:** design, 2026-09-05, revised the same day after a peer pass. |
| Drafted by the activity-stream session (`claude-opus-5`). |
| -**Review pass outstanding** — AGENTS.md requires the highest-capability model |
| -available, recorded here by name, before this is approved. It has not run. |
| -Do not merge on the strength of this line. |
| +**Review pass: run 2026-09-05 by Fable 5.1**, which returned CHANGES NEEDED |
| +with six blocking findings and eight required corrections. All are applied in |
| +this revision, and the revision has not yet been re-reviewed — so this is |
| +still not approved, but it is no longer unreviewed. The six were: the design |
| +was written against a superseded §11 and did not say whether the feed was |
| +cross-team; §7 contradicted itself on whether cached rows survive a failed |
| +pull, with the safety claim on the losing side; §10's 404 rule inverted |
| +ruling 5; the platform edge is a shared cache upstream of both ends and the |
| +cache-header sentence did not reach it; §2.1 understated the RAM cache as one |
| +person's when it is every recent person's; and two of the open numbers were |
| +load-bearing rather than tunable. |
| **Peer pass, and why it is not the review pass.** The home-spec-compliance |
| session (`claude-opus-5`) reviewed the first draft and found four things, |
| all applied: the "no new knowledge" defence was true and beside the point |
| -(§2.2, now a retraction); gate 10 tested only the happy path while §6 makes |
| +(§2.2, now a retraction); gate 11 tested only the happy path while §6 makes |
| failure the normal case (§9); ruling 3's safety rests entirely on `aud` |
| binding and needed a written "never" against the obvious optimisation (§11); |
| and one claim in §5 was falsified by §5.3's own last line. That session |
| wrote enough of the resulting argument to no longer be independent, and it |
| says so itself. **A fresh reviewer is still required.** |
| **Scope:** how the home's front door (§11 of the home design) shows a person |
| -what has been happening across their teams and apps, without the home |
| +what has been happening in the team they are looking at, across that |
| +team's apps, without the home |
| learning or holding any of it. |
| **Reads on top of** `docs/superpowers/specs/2026-08-30-home-design.md`. Every |
| @@ -33,6 +42,13 @@ therefore not the one that was asked for.** Instances do not post. The home |
| asks each of a person's instances "what's theirs, recently?" when it renders |
| their front door, merges the answers, and holds them nowhere but memory (§7). |
| +**Scoped to the selected team — RULED 2026-09-05 by Paul**, and recorded in |
| +the home spec as ruling 18. The front door is one team's page (ruling 17, |
| +`?team=<sqid>` per ruling 22), so the feed under it is that team's, merged |
| +across that team's apps. An earlier draft of this design specified a |
| +cross-team merged list and argued for it; that argument is retired in §2.1 |
| +rather than deleted. |
| + |
| That inversion is the whole design. It is what makes the feature possible |
| inside the one rule instead of against it: |
| @@ -58,6 +74,8 @@ What it costs is latency, honestly rendered. §6 and §7. |
| | 3 | **Reuse the §5.3 assertion for the pull. No new home→instance credential** | A dedicated service token per instance | This session, 2026-09-05 | |
| | 4 | **The last good pull is cached in RAM per person, never at rest** | No cache (every load pays cold wake); a cache on disk (stores content) | This session, 2026-09-05 | |
| | 5 | **A silent instance renders as a named gap, never as an absent row** | Rendering only what arrived | This session, 2026-09-05 | |
| +| 6 | **Scoped to the selected team, following `?team=<sqid>`** — home spec ruling 18 | A cross-team merged list, which this design specified and argued for until 2026-09-05; the argument is retired in §2.1 rather than deleted | Paul, 2026-09-05 | |
| +| 7 | **Participation is a registry flag, never inferred from a `404`** — §10 | Treating a 404 as "does not participate", which makes a broken or hostile instance vanish silently and inverts ruling 5 | This session, 2026-09-05 | |
| Rulings 3, 4 and 5 are this session's and were flagged as such when the |
| design was approved. Ruling 5 is not a courtesy: §2 explains why it is the |
| @@ -137,12 +155,25 @@ existing app, and this puts it at the home. §2.1. |
| The verdict's finding 1 is the strongest objection available to this design, |
| so it gets answered rather than routed around. |
| -**Why the home and not an app.** The verdict's own alternative — Sheets' |
| -home page gaining a Docs section — gives a per-team merged list, which is the |
| -thing finding 1 says nobody asked for. Only the home knows every team a |
| -person belongs to, so only the home can answer *"my things"* rather than |
| -*"this team's things"*. Putting the page anywhere else re-answers the wrong |
| -question. |
| +**Why the home and not an app — re-justified, because Paul's per-team |
| +ruling retired the argument this section used to make.** That argument was: |
| +only the home knows every team you belong to, so only the home can answer |
| +*"my things"* rather than *"this team's things"*. Per-team scope concedes |
| +exactly that question, so the argument is gone and is recorded here as gone. |
| + |
| +What survives is narrower and still uniquely the home's: **only the home |
| +knows which apps a team has instances in.** A team's feed merges Docs, |
| +Sheets and Calendar; no one of them knows the others exist for that team, |
| +and the alternative — Sheets' home page gaining a Docs section — requires |
| +Sheets to hold Docs' grants. That is the federation §2.2 of the home spec |
| +forbids outright, and it is a worse outcome than the one this design was |
| +accused of. So the merge belongs at the home, at a narrower scope than |
| +before. |
| + |
| +**Per-team is also cheaper on every axis this design worries about.** §6's |
| +fan-out drops from every instance a person can reach to the apps in one |
| +team; §7's cache holds less; §5's oracle surface shrinks. The ruling cost |
| +this design its headline and improved everything else. |
| **Why §5.4 is not breached.** §5.4 governs **what may ride an assertion into |
| the home's custody**, and its two-sided test is about claims the home comes |
| @@ -150,16 +181,32 @@ to hold. The failing example is a *held* document-level graph. Here: |
| - **Nothing rides in.** The assertion carries `sub` and theme, exactly as |
| §5.3 already specifies. The activity flows the other way, in a response. |
| -- **Nothing is held.** §3's amendment and gate 10 are the mechanism, not the |
| +- **Nothing is held.** §3's amendment and gate 11 are the mechanism, not the |
| intention — a grep proves it. |
| - **The home learns nothing durable it did not already know.** It knew which |
| teams in which apps you belong to; that is the directory, §6. It does not |
| learn which documents you can see, because it never asks that question and |
| never records an answer. |
| -**The honest residue**, stated rather than argued away: for the lifetime of |
| -one HTTP response, and for the RAM cache's TTL, the home's process memory |
| -holds a cross-app list of titles for one person. That is real, it is what |
| +**The honest residue**, stated rather than argued away and larger than an |
| +earlier draft admitted: for the lifetime of one HTTP response, and for the |
| +RAM cache's TTL, the home's process memory holds a cross-app list of titles |
| +**for every person who rendered inside that window** — not for one person. |
| +That is a cross-person, cross-app set of recent titles, resident in the same |
| +process that mints sessions. §11 lists cross-person aggregation as never |
| +licensed; this builds it as a data structure while forbidding it as a |
| +feature, and the distinction only holds because nothing reads across the |
| +per-person keys. **So the cache is bounded**: a maximum number of person |
| +entries and a maximum bytes per person, both named in §12, evicting oldest |
| +first. |
| + |
| +**What "in RAM" leans on, named rather than assumed.** Instances hibernate by |
| +process death (SIGTERM then SIGKILL), so they hold nothing. The home runs |
| +under a memory cap with reclaim before kill. Two platform facts were NOT |
| +established when this was checked on 2026-09-05: whether swap is configured, |
| +and whether core dumps are disabled. **Both must be settled before this |
| +ships** — a swapped or dumped page of titles is at rest, and §3's amendment |
| +is about storage, not intent. That is real, it is what |
| ruling 1 bought, and it is the thing a reviewer should press on. The claim is |
| not that this is nothing — it is that *transient and unreplicated* is a |
| different category from *stored and shipped to a bucket*, and that §0's |
| @@ -220,7 +267,12 @@ button"* — growing a recency section. |
| ## 3. The one clause that changes |
| -Nothing new goes at rest, so **§0 is unamended**. Publish the database and |
| +Nothing new goes at rest, so §0's **falsifiable** form is unamended — |
| +publish the database and you still learn only counts, team names, |
| +memberships and §3.2's list. **Its prose form does move**, and saying so is |
| +the honest version: "it never learns what is in anything" now has to be read |
| +as *never retains*. That reading belongs in §0's own gloss rather than left |
| +for the two documents to disagree about. Publish the database and |
| you still learn only counts, team names, memberships, and §3.2's list. |
| §2.2's first never-do changes, and only its verb: |
| @@ -231,7 +283,8 @@ you still learn only counts, team names, memberships, and §3.2's list. |
| > **Never *stores* content from an instance.** No previews at rest, no |
| > thumbnails, no cell values, no titles in a database, a log, an error |
| > string, a metric label or an on-disk cache. It **may render**, transiently |
| -> and per request, what an instance hands it for the person it hands it for. |
| +> and per request, **and only to that person**, what an instance hands it for |
| +> the person it hands it for. |
| The same edit lands in `AGENTS.md`'s "four things this service never does" — |
| **on approval, not now.** That file is binding and says the spec wins where |
| @@ -327,6 +380,15 @@ team gets — see §7, which is the entire point of ruling 5. |
| ## 5. Authentication, and why it needs nothing new |
| +**Who mints, and where the key is.** Home spec §4 puts assertion minting on |
| +the **credential origin**, and the front door is on the **directory origin**, |
| +which obtains assertions like any other consumer. The fan-out therefore mints |
| +N assertions per render from a page that is not the minting origin. Say which |
| +this is: **one process serving two hostnames**, so the signing key's exposure |
| +is unchanged from §5.3 and no cross-origin mint call exists — and note the |
| +consequence, that the directory origin is now also the surface parsing N |
| +consumer-supplied JSON bodies per render (§16's gate). |
| + |
| **The pull carries a §5.3 assertion, unchanged.** Ed25519, `aud` bound to the |
| answering instance's app and host, `sub` the person ref, `iss` the credential |
| origin, `kid`, 120-second expiry, single-use `jti`. |
| @@ -383,7 +445,7 @@ Call 7 is a narrower shape, and the narrowness is deliberate: |
| origin's Ed25519 private key, at which point the suite has lost anyway. |
| - **No grant and no such person are the same answer** — `200` with an empty |
| list. An instance must not distinguish them, or the endpoint answers |
| - membership questions about arbitrary person refs. **Gate 17.** |
| + membership questions about arbitrary person refs. **Gate 18.** |
| - **Errors carry no detail.** "Not for you" and "no such thing" are one |
| response, per §12's gate 4 pattern of 404 rather than 403. |
| @@ -414,12 +476,25 @@ nothing"), so they have no front door and no rows. Correct by construction. |
| ## 6. Fan-out, and the cost this design pays |
| -A person in three teams across two apps is six hosts. The home fans out in |
| +The selected team's apps, and only those — per Paul's per-team ruling, a |
| +team in three apps is three hosts, not the six a cross-team merge over three |
| +teams would have been. The home fans out in |
| parallel, with a per-request deadline, and renders what arrived. |
| **Zero JS (§11).** One server-side fan-out, no progressive loading, no |
| skeleton that fills in. The page is complete when it is sent. |
| +**The client is hardened explicitly, because Go's defaults are wrong for |
| +this.** `http.DefaultClient` has no timeout and follows redirects. Required: |
| +the deadline is a **context covering connect, TLS, headers and the full body |
| +read**, not just the response headers; **redirects are never followed** — a |
| +3xx is a failed pull, because an instance redirecting to a document URL would |
| +otherwise put that URL inside a `net/http` error string, which §3.4 of the |
| +home spec rules is storage; TLS verification on; and **bounded concurrency |
| +across all in-flight renders**, so one instance trickling bytes cannot |
| +exhaust the process that also serves sign-in (home spec §2.1 makes it the |
| +suite's single point of failure). |
| + |
| **The cost, stated rather than buried: instances hibernate and the home does |
| not.** §2.1 makes the home the one app that never sleeps; every instance is |
| the opposite. §5.3 sizes assertion expiry at 120 seconds precisely because a |
| @@ -428,9 +503,14 @@ a few seconds". So: |
| - **The first front-door load of the day will mostly miss its deadline.** Not |
| an edge case — the normal morning. |
| -- **Every load warms the person's whole fleet**, which changes the |
| - hibernation economics the platform assumes. A person who opens the front |
| - door twenty times a day keeps six instances awake. |
| +- **Every load warms the selected team's instances**, which changes the |
| + hibernation economics the platform assumes — and **instance-hours are |
| + billed to the instance's account, not to the person opening the door**. A |
| + guest with one row in `acme.docs` would otherwise wake Acme's instance on |
| + every front-door load, at Acme's expense. So there is a **minimum pull |
| + interval per (person, instance), enforced even on an explicit refresh**; |
| + the refresh affordance re-renders from cache when the interval has not |
| + elapsed, and says so. |
| Neither is fixed here. Both are **mitigated and disclosed**: |
| @@ -438,6 +518,13 @@ Neither is fixed here. Both are **mitigated and disclosed**: |
| - The waking state is rendered honestly (§8), so a slow first load is legible |
| rather than mysterious or wrong. |
| +**A fan-out budget with no specified behaviour is not a budget.** §12 lists |
| +one as an open number. The number can wait; **what happens at the limit |
| +cannot**, because an un-asked instance must render as its own kind of gap or |
| +ruling 5 is breached silently — the person cannot tell "nothing to say" from |
| +"never asked". Un-asked instances render as a distinct gap with a refresh |
| +affordance, and the budget is per render, not per person. |
| + |
| **The named escape hatch**, if the morning experience proves miserable in |
| use: the rejected hybrid — instances push a *contentless* ping (person ref, |
| app, team, coarse timestamp, no title, no object ref) so the home knows which |
| @@ -458,23 +545,46 @@ in process memory, with a short TTL. |
| - **Never at rest.** Not SQLite, not a file, not a platform cache, not an |
| HTTP cache header that would let an intermediary keep it. §3's amendment |
| permits rendering, not storing, and a disk cache is storing. |
| +- **The platform edge is a shared cache upstream of both ends, and it must |
| + be closed on both.** It caches any 2xx GET whose response carries a |
| + positive `max-age`, keyed on host, method, path and query, shared across |
| + every visitor of a host — and it does **not** refuse on a request |
| + `Cookie`. So: the front door sends `Cache-Control: no-store` (the home |
| + spec's gate 10), **and call 7's contract requires instances to send |
| + `no-store` too**. An instance answering `public, max-age=60` would have |
| + the edge serve person A's rows to the home's next pull for person B with |
| + the same query string, and the home's own cache key — person ref and |
| + instance host — cannot see that happen. **A cacheable response is |
| + therefore a failed pull**: the home discards it and renders a gap. Gate 16 |
| + covers it. |
| - **Precedent:** §3.2 already handles readable addresses "transient in RAM |
| elsewhere". This is the same category, and it is the only category the one |
| rule leaves open. |
| - **A restart empties it**, and that is correct rather than unfortunate. |
| +- **`since` comes only from this cache, and is never persisted.** A future |
| + "remember the last pull per (person, instance) across restarts" is the |
| + contentless activity table §11 forbids, arriving as an optimisation rather |
| + than as a decision. |
| - **Keyed by person ref and instance host**, so one person's cache can never |
| render on another's page — the same isolation gate 4 already asserts for |
| the directory. |
| -- **The TTL is short enough that a stale row is a curiosity, not a lie.** A |
| - person removed from a team keeps seeing that team's last rows until the TTL |
| - expires. That is the one residue of objection 3 and it is bounded by a |
| - number we choose, not by a hibernating fleet's wake schedule — which is the |
| - difference between minutes and weeks. |
| - |
| -**A failed pull never evicts a good cached answer.** It renders the cached |
| -rows *plus* the failure marker (§8), because "here is what I knew, and I |
| -could not reach them just now" is more useful and more honest than either |
| -silence or an empty list. |
| +**Two ages, not one, because an earlier draft contradicted itself.** It said |
| +the TTL bounds staleness "by a number we choose, not by a hibernating fleet's |
| +wake schedule", and then said a failed pull never evicts a good cached |
| +answer. Both cannot hold: if failure preserves rows, a person removed from a |
| +team keeps seeing its titles until that instance next *answers*, which is |
| +exactly the hibernation tail the first sentence disclaims. |
| + |
| +- **Refresh TTL** — how long before the home tries again. Short. |
| +- **Hard maximum age** — after which rows are **discarded regardless of pull |
| + outcome**, failure included. This is the one that bounds the lie, and it is |
| + the number the removed-from-a-team case turns on. |
| + |
| +**A failed pull does not evict a good cached answer that is still inside the |
| +hard maximum.** Past it, the rows go and the gap is rendered (§8), because |
| +"here is what I knew" stops being honest once "when I knew it" is old enough |
| +that the grant may be gone. Gate 20 drives the hard maximum, not the refresh |
| +TTL — a test against the refresh TTL proves the easy half. |
| --- |
| @@ -485,7 +595,8 @@ section above or beside it — **layout is the front-door session's call, not |
| this spec's.** |
| Every row: title, app, team, verb, actor if known, relative time, linking to |
| -the instance's own URL. Merged across instances, ordered by `at` descending, |
| +the instance's own URL. Merged across the selected team's instances, |
| +ordered by `at` descending, |
| capped. |
| **And then the part that matters.** Ruling 5: |
| @@ -548,9 +659,10 @@ Suppressing gaps is ruling 5 in reverse. |
| ## 9. Testing gates |
| -Numbered from §12's nine. |
| +Numbered from the home spec's **ten** — `main` took gate 11 for no-store |
| +(ruling 22), so these start at 11. |
| -10. **No pulled content at rest — driven through the failure paths, not the |
| +11. **No pulled content at rest — driven through the failure paths, not the |
| happy one.** A full front-door render against seeded instances leaves no |
| title, URL or actor string in the SQLite file, the WAL, any log line, |
| error string, metric label or on-disk artifact. Extends gates 1 and 9; |
| @@ -571,28 +683,36 @@ Numbered from §12's nine. |
| canary must appear in none of the places above. This is the class gate |
| 9's own review turned up three of, and each one was a guard that looked |
| complete. |
| -11. **A silent instance is a visible gap.** With one instance timing out and |
| +12. **A silent instance is a visible gap.** With one instance timing out and |
| one returning empty, the render distinguishes them. This gate fails if |
| both produce the same output. **The most important test in this |
| document.** |
| -12. **The activity assertion is audience-bound.** One minted for |
| +13. **The activity assertion is audience-bound.** One minted for |
| `acme.sheets.oficina.works`, replayed against `bigco.sheets.oficina.works`, |
| fails. Extends gate 3. |
| -13. **Two-person isolation on the pull.** Person A's assertion returns only |
| +14. **Two-person isolation on the pull.** Person A's assertion returns only |
| A's rows; a cached answer for A never renders on B's page. Extends |
| gate 4. |
| -14. **The credential origin renders no pulled content.** Extends gate 5. |
| -15. **A hostile instance response cannot hurt the front door.** Oversized |
| +15. **The credential origin renders no pulled content.** Extends gate 5. |
| +16. **A hostile instance response cannot hurt the front door.** Oversized |
| body (cap enforced), a `url` on another host (rejected), an unknown |
| `verb` (renders `changed`), HTML in `title` and `actor` (escaped), a |
| - malformed `at` (row dropped, instance marked, page still renders). An |
| - instance is a consumer-supplied endpoint and this page is the suite's |
| - most public surface. |
| -16. **Scriptless.** The recency section and every refresh link work with |
| + malformed `at` (row dropped, instance marked, page still renders), **a |
| + cacheable response** (`public, max-age=…` — discarded and rendered as a |
| + gap, §7), and **a redirect** (never followed, treated as a failed pull, |
| + §6). An instance is a consumer-supplied endpoint and this page is the |
| + suite's most public surface. |
| +17. **Scriptless.** The recency section and every refresh link work with |
| scripts stripped. Extends gate 6. |
| -17. **Not an oracle** (§5.1). A person ref with no grant and a person ref |
| +18. **Not an oracle** (§5.1). A person ref with no grant and a person ref |
| that does not exist produce byte-identical responses from an instance. |
| -18. **One assertion, one host.** The fan-out mints a separate assertion per |
| +20. **The hard maximum age is enforced through failure.** Seed a cached |
| + answer, make the instance fail, advance past the hard maximum, and |
| + assert the rows are gone and a gap is rendered — not that they survive |
| + because the pull failed. A test against the refresh TTL proves the easy |
| + half; this is the half that bounds the lie (§7). |
| + |
| +19. **One assertion, one host.** The fan-out mints a separate assertion per |
| instance, and a test asserts that no two requests in one render carry the |
| same `aud`. §11's first row is the reason this is a gate and not a note. |
| @@ -622,11 +742,20 @@ endpoint is a projection of a query it has. Docs likewise. |
| seals titles under its DEK, so answering call 7 decrypts once per row rather |
| than reading a column. That is a real cost on a list query and it is the |
| argument for the instance capping `limit` hard and low — the front door wants |
| -a handful of rows per app, not a page of them. **Not appearing is |
| -a valid state**: an app that has not implemented `/activity` simply |
| -contributes no rows, and the front door renders the rest. There is no |
| -registration step and no flag; a `404` from an instance is "this app does not |
| -participate", rendered as nothing rather than as a failure. |
| +a handful of rows per app, not a page of them. |
| + |
| +**Participation is declared, not inferred.** An earlier draft said a `404` |
| +from an instance means "this app does not participate", rendered as nothing. |
| +That is ruling 5 exactly backwards: a participating app that loses its route |
| +in a deploy, or a hostile one, would vanish silently from every front door — |
| +"a plausible empty list, not an error", which is the failure ruling 5 exists |
| +to prevent. `404` is already overloaded too: §5.1 uses it for "no such |
| +thing" and §8.1 for a dead host rendered *unavailable*. |
| + |
| +So participation is **a flag in the home's compiled-in app registry** (home |
| +spec §11), beside `provisionable` and `integrated`. An app that does not |
| +declare it is never asked. An app that declares it and then answers `404` |
| +is **a gap**, rendered as one, and gate 12 covers that case. |
| --- |
| @@ -634,7 +763,7 @@ participate", rendered as nothing rather than as a failure. |
| | Thing | Trigger | |
| |---|---| |
| -| **One assertion reused across the fan-out** | **Never.** Written here because someone will propose it in good faith, citing §6 — six signatures on a latency-critical path is exactly the optimisation §6 creates the incentive for. Ruling 3 is safe *only* because `aud` is app-plus-instance-host and §5.5 makes each instance decide from its own grants. An assertion valid at every host in a person's fleet is a cross-instance bearer, which is full impersonation everywhere they have a grant. Ed25519 signing is microseconds; the latency is the network and the cold wake. Gate 18. | |
| +| **One assertion reused across the fan-out** | **Never.** Written here because someone will propose it in good faith, citing §6 — six signatures on a latency-critical path is exactly the optimisation §6 creates the incentive for. Ruling 3 is safe *only* because `aud` is app-plus-instance-host and §5.5 makes each instance decide from its own grants. An assertion valid at every host in a person's fleet is a cross-instance bearer, which is full impersonation everywhere they have a grant. Ed25519 signing is microseconds; the latency is the network and the cold wake. Gate 19. | |
| | The contentless-ping hybrid (§6) | A real latency complaint, or measured p50 over one second warm | |
| | Any at-rest store of pulled **content** — titles, URLs, actors | Never, under §3's amendment as written. Changing this reopens §0. | |
| | Any at-rest **contentless** activity table | Only as §6's escape hatch, on §6's trigger. It is new custody — a per-person, per-team timestamp series, which is §3.3's de-anonymisation vector at fine resolution — so it is a decision, not a performance tweak. | |
| @@ -650,16 +779,25 @@ participate", rendered as nothing rather than as a failure. |
| - **The review pass.** AGENTS.md requires the highest-capability model |
| available, recorded by name. Not run. This spec is not approved. |
| -- **`AGENTS.md` is stale against the home spec on three counts** the spec |
| - explicitly retracted — "never stores your address", "some unidentified |
| - person", and "assertion expiry is **seconds**" where §5.3 says **120 |
| - seconds**. The escritorio verdict confirmed all three. **Already tracked** |
| - on discussion #1 / branch `fix/agents-md-matches-spec`, so it is not this |
| - branch's to fix — but call 7 authenticates with the §5.3 assertion, and an |
| - implementer who takes the expiry from `AGENTS.md` gets a number the spec |
| - retracted. **Take 120 seconds from the spec.** |
| -- **Numbers.** The per-instance deadline, the total fan-out budget, the RAM |
| - cache TTL and the body cap are all named as decisions and none is chosen. |
| +- ~~`AGENTS.md` is stale against the home spec on three counts.~~ |
| + **Closed 2026-09-05:** `main` carries all three retractions at `5a42f2d`, |
| + and `AGENTS.md` there is current. |
| +- **Numbers.** The per-instance deadline and the body cap are tunables with |
| + specified behaviour at the limit, and can be chosen from measurement. Three |
| + are **not** merely tunables and must be settled before this ships: |
| + - the **hard maximum cache age** (§7), which is what bounds how long a |
| + person removed from a team keeps seeing its titles — the refresh TTL |
| + does not; |
| + - the **cache bounds** (§2.1): maximum person entries and maximum bytes per |
| + person, since the cache is cross-person; |
| + - the **minimum pull interval** per (person, instance) (§6), which is what |
| + stops a guest's front door billing someone else's account. |
| + |
| + The fan-out budget's *number* can wait; its behaviour at the limit is |
| + specified in §6 and is not open. |
| +- **Two platform facts** the "in RAM" claim leans on and which were not |
| + established: whether swap is configured, and whether core dumps are |
| + disabled. Both must be answered before this ships (§2.1). |
| They want measurement against a real canary, not a guess in a spec. |
| - **Front-door layout** is the other session's. This spec deliberately does |
| not draw it. |