Skip to content

feat(truapi): Pocket modality RFC and protocol spec - #609

Open
valentinfernandez1 wants to merge 4 commits into
mainfrom
rfc/pocket-modality
Open

feat(truapi): Pocket modality RFC and protocol spec#609
valentinfernandez1 wants to merge 4 commits into
mainfrom
rfc/pocket-modality

Conversation

@valentinfernandez1

Copy link
Copy Markdown
Collaborator

Tracking issue: #563

What this defines

Pocket is a collection of product-backed cards owned by the host. Each card shows a product's key information at a glance and opens the product when tapped.

The host renders each collapsed card natively from a CustomRendererNode tree that the product's worker streams. Tapping a card opens the product's Widget executable in a WebView.

A product cannot add a card on its own. The user adds one by following a Pocket deeplink and approving a host dialog that shows the card as it will look. Both the user and the owning product can remove a card. Humanity, Balance and Scarcity are privileged cards. They are always present and nobody can remove them.

Chat and Pocket share the product's single Worker executable. Its lifetime is a reference count: one reference per active chat, one per visible card. The host terminates the worker when the count reaches zero. When the worker starts, its entry module runs under a bounded time and memory budget.

The RFC also defines the deeplink format and where card definitions are published:

  • Deeplinks: polkadot://<product>.<tld>/-/pocket/add?card=<id> offers to add a card, and /-/pocket/open?card=<id> expands one. The first path segment - is reserved for host-handled targets.
  • Card definitions: the Worker manifest gains a pocket.cards list. Each entry names the card and points at a static preview tree inside the product archive, so the approval dialog runs no product code.

Protocol surface

Method Direction Ids
pocket.list_subscribe product to host 198 to 201
pocket.remove_card product to host 202 to 203
pocket.action_subscribe product to host 204 to 207
pocket.card_render host to product 208 to 211

The Pocket trait is available to the Worker execution only. Every payload travels in a V1 envelope. A card_render item encodes byte for byte like a chat custom-message render item, so a host that decodes one decodes the other.

The renderer tree types (CustomRendererNode, its props, modifiers and tokens) live in truapi::v01::renderer and are re-exported through truapi::latest. Chat uses them from there. Their wire encoding is unchanged.

Scope

This PR contains the RFC and the crate-level spec only.

truapi-server implements the trait with the default bodies. A host without a Pocket surface answers remove_card with Unavailable, ends the two subscriptions with an empty Interrupt frame, and never opens card_render. ProductRuntimeControl::render_pocket_card is the entry point a host calls to open a face stream.

Pocket behaviour in the runtime, the platform trait, the native bindings and the CLI is tracked by the "Implementation, rust-core" task in #563.

@valentinfernandez1
valentinfernandez1 requested a review from a team September 4, 2026 16:02
@valentinfernandez1
valentinfernandez1 marked this pull request as draft September 4, 2026 16:02
peetzweg and others added 2 commits September 7, 2026 15:17
The committed Swift bindings gain the Pocket value types and the updated
custom-renderer observer checksum from `make uniffi`.

The playground coverage test enumerated every Worker-pinned service as
"Chat"; Pocket is Worker-pinned too, so it now checks the Chat service by
name.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@valentinfernandez1
valentinfernandez1 marked this pull request as ready for review September 7, 2026 14:28
@valentinfernandez1

Copy link
Copy Markdown
Collaborator Author

@lore-bot-app review

@lore-bot-app

lore-bot-app Bot commented Sep 7, 2026

Copy link
Copy Markdown

Reading the diff and checking what the record says. Back in a few minutes.

@lore-bot-app lore-bot-app 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.

TL;DR
Adds the Pocket RFC, a four-method Pocket trait (wire ids 198 to 211) with stub host defaults, and moves the renderer node types out of chat. 0 blocking, 4 minor. One of the minor items needs a sequencing decision against the open codec-2 wire cutover (#357).

Summary
The PR lands the Pocket modality as a draft RFC plus its protocol surface. truapi gains api/pocket.rs (list_subscribe, remove_card, action_subscribe, host-initiated card_render), v01 payload types, versioned wrappers with encoding tests, and latest re-exports. CustomRendererNode and friends move from v01/chat/custom_renderer.rs to v01/renderer.rs with the wire encoding unchanged. The generated dispatcher and wire table pick up the new service, the schema hash changes, ProductRuntimeHost gets an empty impl Pocket that answers Unavailable and empty streams, and ProductRuntimeControl::render_pocket_card mirrors render_custom_message. iOS bindings are regenerated, the playground chat test is scoped to Chat, and a changeset bumps @parity/truapi and @parity/truapi-host minor. The RFC follows the repo's rfc skill: unnumbered file, _index.md untouched, status: draft, required sections present. The diff contains no instructions addressed to the reviewer.

What the record says

  • Scarcity is the third privileged card. The tracking issue settled that Scarcity joins Humanity and Balance as permanently present and non-removable, layout still open. The RFC matches. #563
  • Pocket was explicitly deferred out of the manifest RFC. The product-manifest RFC discussion marked Pocket as needing a redesign and named the Apple Wallet style "add a card on demand" use case. The deeplink add flow here is the first written answer to that. paritytech/triangle-js-sdks#174
  • Precedent for landing a trait with stub defaults before any host backs it. The Funding RFC did the same: trait plus empty host impl returning unavailable. #339
  • The wire table is mid-redesign. #357 splits the frame discriminant into trait and method bytes and renumbers per trait under codec 2. It is open and was active today. #351 and RFC 0028 (#557, #572) describe the same cutover. This PR allocates flat ids in the table #357 replaces. #357 , #351 , #557
  • Id collisions with downstream extensions have happened before. triangle-js-sdks derives ids from the vendored truapi table and keeps local extension ids in wireTable.extensions.ts; a truapi bump once failed on overlaps. Lore does not index the current extension file, so I cannot confirm 198 to 211 are free there. paritytech/triangle-js-sdks#245 , #350
  • Chat identifier bounds were retrofitted after the modality shipped. #453 and #430 added normalization, screening, and payload caps for chat ids and payloads. Pocket's ids and action payload start without any. #453 , #430
  • The CLI question is still open. #523 asks how to exercise worker-backed Pocket without a host shell and wants the CLI to report served modalities at startup. This PR leaves the CLI answering Unavailable. #523
  • Release 2 tracking lists Pocket manifest declarations as an untracked gap and splits Pocket work into a v2 tracker. #591 , #594
  • Owners per Lore: pgherveou, valentinfernandez1, filvecchiato, johnthecat.

Concerns

  1. Wire ids 198 to 211 are allocated in a table that #357 is about to replace. rust/crates/truapi/src/api/pocket.rs:31,45,66,86 and both wire_table.rs files. Whichever PR merges second has to renumber and regenerate. Not a defect here, but the order should be agreed before merge so one of them does not ship a wire contract that lives for a single release.
  2. render_pocket_card is unreachable from any host. rust/crates/truapi-server/src/host_core.rs:1172. Unlike render_custom_message, it is not exposed in native.rs or wasm.rs, and there is no PocketPlatform seam in truapi-platform the way Chat has ChatPlatform (truapi-platform/src/lib.rs:2771) and Locale has LocaleHost (lib.rs:2728). So impl Pocket for ProductRuntimeHost {} at runtime.rs:2920 cannot be backed by iOS, Android, or a JS host without editing truapi-server. The Funding precedent accepted this shape, so this is a scope statement rather than a bug, but the PR description should say the seam is a follow-up.
  3. No bounds on card_id, action_id, or the action payload. rust/crates/truapi/src/v01/pocket.rs:8,48-53. The RFC calls card_id "a lowercase label" but gives no charset or length, and payload: Option<Vec<u8>> has no cap. Chat got normalize_chat_identifier and a 256 KiB custom payload cap only after hosts diverged (#453). The RFC's Wire surface section is the place to fix these before a second host implements them.
  4. RFC text disagrees with its own snippet on id allocation. docs/rfcs/pocket-modality.md:129 says ids start after "the highest allocated on main at draft time (194)", then uses 198. The highest is 197 (locale_subscribe receive). Small, but it is the spec.

Questions for the author

  1. Does this land before or after #357? If after, will Pocket take a trait byte with methods 0 to 3, and is the flat allocation here throwaway?
  2. Is a PocketPlatform seam plus native and wasm exposure of render_pocket_card the next PR, and is iOS the first consumer given its hard-coded Humanity, Balance, and Scarcity cards?
  3. Should card_id reuse the chat identifier normalization rules, and should the face action payload get an explicit cap in the RFC?
  4. Has anyone checked triangle-js-sdks wireTable.extensions.ts for local ids in 198 to 211?

Next: reply on #357 sequencing, since that decides whether the ids in this PR are final.


🤖 Reviewed by Lore (Parity knowledge base) · 50 agent turns · 446.5s · knowledge as of 2026-09-07

/// console.log("cards:", item.cards);
/// ```
#[wire(start_id = 198)]
async fn list_subscribe(&self, _cx: &CallContext) -> Subscription<HostPocketListSubscribeItem> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Wire ids 198 to 211 are allocated in a table that #357 is about to replace. rust/crates/truapi/src/api/pocket.rs:31,45,66,86 and both wire_table.rs files. Whichever PR merges second has to renumber and regenerate. Not a defect here, but the order should be agreed before merge so one of them does not ship a wire contract that lives for a single release.


/// Stream the face of one Pocket card from this connection's product
/// worker. Each item is a complete renderer tree replacing the previous one.
pub fn render_pocket_card(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

render_pocket_card is unreachable from any host. rust/crates/truapi-server/src/host_core.rs:1172. Unlike render_custom_message, it is not exposed in native.rs or wasm.rs, and there is no PocketPlatform seam in truapi-platform the way Chat has ChatPlatform (truapi-platform/src/lib.rs:2771) and Locale has LocaleHost (lib.rs:2728). So impl Pocket for ProductRuntimeHost {} at runtime.rs:2920 cannot be backed by iOS, Android, or a JS host without editing truapi-server. The Funding precedent accepted this shape, so this is a scope statement rather than a bug, but the PR description should say the seam is a follow-up.

#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
pub struct PocketCard {
/// Card label declared by the product, unique within the product.
pub card_id: String,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No bounds on card_id, action_id, or the action payload. rust/crates/truapi/src/v01/pocket.rs:8,48-53. The RFC calls card_id "a lowercase label" but gives no charset or length, and payload: Option<Vec<u8>> has no cap. Chat got normalize_chat_identifier and a 256 KiB custom payload cap only after hosts diverged (#453). The RFC's Wire surface section is the place to fix these before a second host implements them.

/// Pocket cards backed by the calling product.
#[crate::service(required_execution = Worker)]
#[crate::async_trait]
pub trait Pocket: Send + Sync {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

RFC text disagrees with its own snippet on id allocation. docs/rfcs/pocket-modality.md:129 says ids start after "the highest allocated on main at draft time (194)", then uses 198. The highest is 197 (locale_subscribe receive). Small, but it is the spec.

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.

2 participants