@huynle/hub 0.9.2 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/CHANGELOG.md +320 -0
  2. package/Dockerfile +5 -1
  3. package/README.md +154 -77
  4. package/package.json +7 -4
  5. package/packages/claude-code/package.json +1 -1
  6. package/packages/cli/dist/hub-cli.js +534 -261
  7. package/packages/cli/package.json +1 -1
  8. package/packages/cli/src/cli.ts +36 -1
  9. package/packages/cli/src/subscriptions.ts +49 -0
  10. package/packages/codex/package.json +1 -1
  11. package/packages/codex/src/sessions.ts +10 -6
  12. package/packages/codex/src/spoke.ts +2 -1
  13. package/packages/node/package.json +1 -1
  14. package/packages/preset/package.json +1 -1
  15. package/packages/protocol/package.json +1 -1
  16. package/packages/protocol/schema/app-delivery/v1.schema.json +7 -5
  17. package/packages/protocol/schema/app-front/v1.schema.json +410 -0
  18. package/packages/protocol/src/app-front.ts +293 -0
  19. package/packages/protocol/src/concierge.ts +10 -1
  20. package/packages/protocol/src/context-cards.ts +59 -0
  21. package/packages/protocol/src/credentials.ts +39 -2
  22. package/packages/protocol/src/index.ts +81 -11
  23. package/packages/runtime-openshell/package.json +1 -1
  24. package/packages/runtime-sdk/package.json +1 -1
  25. package/packages/sdk/package.json +1 -1
  26. package/packages/sdk/src/app-front.ts +524 -0
  27. package/packages/sdk/src/client.ts +37 -4
  28. package/packages/sdk/src/envelope.ts +9 -2
  29. package/packages/sdk/src/errors.ts +1 -0
  30. package/packages/sdk/src/index.ts +1 -0
  31. package/packages/sdk/src/types.ts +39 -1
  32. package/packages/spoke-sdk/package.json +1 -1
  33. package/packages/spoke-sdk/src/delivery.ts +17 -1
  34. package/packages/spoke-sdk/src/harness-spoke.ts +4 -3
  35. package/packages/voice-client/README.md +146 -0
  36. package/packages/voice-client/src/vad/LICENSE +21 -0
  37. package/src/central/agent/acting-routes.ts +17 -3
  38. package/src/central/agent/acting.ts +12 -3
  39. package/src/central/agent/actor.ts +6 -1
  40. package/src/central/agent/app-decisions.ts +146 -0
  41. package/src/central/agent/app-tokens.ts +9 -3
  42. package/src/central/agent/audit.ts +3 -0
  43. package/src/central/agent/connect-routes.ts +64 -0
  44. package/src/central/agent/connect.ts +868 -0
  45. package/src/central/agent/conversation.ts +625 -0
  46. package/src/central/agent/decisions.ts +71 -6
  47. package/src/central/agent/delivery.ts +3 -0
  48. package/src/central/agent/directory.ts +5 -0
  49. package/src/central/agent/errors.ts +2 -0
  50. package/src/central/agent/grant-requests.ts +46 -9
  51. package/src/central/agent/grants.ts +13 -2
  52. package/src/central/agent/message-limits.ts +2 -2
  53. package/src/central/agent/message-routes.ts +13 -4
  54. package/src/central/agent/read-routes.ts +16 -2
  55. package/src/central/agent/routes.ts +108 -6
  56. package/src/central/agent/speech-routes.ts +316 -0
  57. package/src/central/agent/subscription-routes.ts +174 -0
  58. package/src/central/agent/subscriptions.ts +980 -0
  59. package/src/central/agent-data.ts +43 -1
  60. package/src/central/backend/memory-counters.ts +2 -2
  61. package/src/central/backend/types.ts +5 -2
  62. package/src/central/backend/valkey-counters.ts +5 -4
  63. package/src/central/client-address.ts +16 -0
  64. package/src/central/concierge/board-render.ts +52 -1
  65. package/src/central/concierge/board-routes.ts +25 -2
  66. package/src/central/concierge/board.ts +11 -1
  67. package/src/central/concierge/context.ts +29 -8
  68. package/src/central/concierge/grant-request.ts +216 -0
  69. package/src/central/concierge/prompt.ts +49 -2
  70. package/src/central/concierge/session-routes.ts +3 -0
  71. package/src/central/config.ts +64 -3
  72. package/src/central/context-cards/budget.ts +121 -0
  73. package/src/central/context-cards/changes.ts +29 -0
  74. package/src/central/context-cards/config.ts +145 -0
  75. package/src/central/context-cards/engine.ts +379 -0
  76. package/src/central/context-cards/inputs.ts +111 -0
  77. package/src/central/context-cards/pickup.ts +134 -0
  78. package/src/central/context-cards/registry.ts +89 -0
  79. package/src/central/context-cards/routes.ts +37 -0
  80. package/src/central/context-cards/service.ts +367 -0
  81. package/src/central/context-cards/store.ts +67 -0
  82. package/src/central/context-cards/types.ts +88 -0
  83. package/src/central/fetcher.ts +10 -2
  84. package/src/central/front-relays.ts +423 -0
  85. package/src/central/http.ts +547 -58
  86. package/src/central/human-activity.ts +453 -0
  87. package/src/central/llm/chat.ts +133 -0
  88. package/src/central/registry.ts +162 -9
  89. package/src/central/speech/service.ts +9 -0
  90. package/src/central/spoke-ws.ts +13 -0
  91. package/src/central/store.ts +9 -12
  92. package/src/config.ts +30 -13
  93. package/src/index.ts +45 -43
  94. package/src/rpc.ts +33 -4
  95. package/src/spoke/concierge-agent.ts +1 -1
  96. package/src/spoke/conversation.ts +6 -75
  97. package/src/spoke/deliver.ts +18 -8
  98. package/src/spoke/hub-rpc.ts +7 -2
  99. package/src/spoke/hub-tool-subscriptions.ts +82 -0
  100. package/src/spoke/hub-tool-title.ts +89 -0
  101. package/src/spoke/hub-tool.ts +56 -18
  102. package/src/spoke/session-context.ts +10 -2
  103. package/src/spoke/spoke.ts +227 -237
  104. package/src/spoke/summary.ts +36 -1
  105. package/src/spoke/taskboard.ts +5 -6
  106. package/src/spoke/voice-front.ts +323 -30
  107. package/src/spoke/who-teams.ts +51 -2
  108. package/src/tui-messaging.ts +24 -10
  109. package/src/tui.ts +6 -6
  110. package/web/dist/assets/{ArtifactPage-BKISN-Qj.js → ArtifactPage-CsPjam8D.js} +1 -1
  111. package/web/dist/assets/{ArtifactPane-CKOiAUNH.js → ArtifactPane-B9weO8L6.js} +1 -1
  112. package/web/dist/assets/ArtifactWorkbench-DzKhMs3l.js +6 -0
  113. package/web/dist/assets/{Diagram-vdpmp3xu.css → Diagram-7y8kg3og.css} +1 -1
  114. package/web/dist/assets/Diagram-CuRIffka.js +1 -0
  115. package/web/dist/assets/{Library-CcjZXsfF.js → Library-rv5DGtrm.js} +2 -2
  116. package/web/dist/assets/Lightbox-BlbivCkd.js +1 -0
  117. package/web/dist/assets/MediaImage-Ch3ryIdR.js +4 -0
  118. package/web/dist/assets/ProfilesDestination-DUuQ9jmd.js +1 -0
  119. package/web/dist/assets/{TeamsPage-CDyjrK-I.js → TeamsPage-D_17MkvN.js} +1 -1
  120. package/web/dist/assets/{Watchtower-DYwoUWME.js → Watchtower-DlrNOJ7X.js} +2 -2
  121. package/web/dist/assets/{artifacts-DU9uRgs1.js → artifacts-But87Jfo.js} +1 -1
  122. package/web/dist/assets/index-bX8B6NLo.js +33 -0
  123. package/web/dist/assets/index-rW6LKQj1.css +1 -0
  124. package/web/dist/assets/{live-ycZ_aQZO.js → live-BGLA-peO.js} +2 -2
  125. package/web/dist/assets/{pane-layout-0RrLHGDr.js → pane-layout-BW0RdX5a.js} +1 -1
  126. package/web/dist/assets/{renderer2d-BIwbgRoX.js → renderer2d-DyANQ1WS.js} +1 -1
  127. package/web/dist/assets/{renderer3d-C1a7eYGQ.js → renderer3d-vYYKFAJq.js} +1 -1
  128. package/web/dist/assets/{safe-area-DHhPy61Q.js → safe-area-B2Bk__FK.js} +1 -1
  129. package/web/dist/assets/{source-BWlvaODa.js → source-C-De8L3s.js} +1 -1
  130. package/web/dist/assets/toast-CXTwTvDw.js +9 -0
  131. package/web/dist/assets/vad-worklet-BfxJhrXy.js +27 -0
  132. package/web/dist/index.html +6 -6
  133. package/web/dist/assets/ArtifactWorkbench-d7R1o-rs.js +0 -6
  134. package/web/dist/assets/Diagram-azw2B_QH.js +0 -1
  135. package/web/dist/assets/Lightbox-Du56MXGX.js +0 -1
  136. package/web/dist/assets/MediaImage-Ds_JUx3a.js +0 -4
  137. package/web/dist/assets/ProfilesDestination-DdzbzmeK.js +0 -1
  138. package/web/dist/assets/index-C3ci5UtY.css +0 -1
  139. package/web/dist/assets/index-LNMZCqgE.js +0 -36
  140. package/web/dist/assets/toast-Cuu2K0hx.js +0 -1
  141. /package/{web/src/voice → packages/voice-client/src}/turn/LICENSE +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,325 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.1
4
+
5
+ Clicking the composer's Model chip changes the session's model. No migration is needed. Machines need the new
6
+ plugin (and an OpenCode restart) for the chip to become clickable; with older plugins it stays display only.
7
+
8
+ ### Console
9
+ - **The composer's Model chip changes the session's model** (user decision, 2026-11-10: "yes, it should change the
10
+ session model"). Clicking (or Enter on) "Model: …" opens the same picker as the Voice model (search over the
11
+ session machine's catalog, the variant step, arrows, Enter and 1–9); a pick sets the **session's own model**, so its
12
+ next turn and every later one run on it, whether the prompt comes from the hub console, the OpenCode TUI or a
13
+ Voice relay. The chip then shows it with source "chosen" (the picker marks it). The Voice chip and its picker are
14
+ unchanged, and a Voice pick still never changes the session's model.
15
+ - Mechanism: OpenCode 2.x's `session.switchModel`, which stores the model on the session (the same call the TUI
16
+ makes when you pick a model there); the runner uses it for every turn of that session. Last writer wins: a model
17
+ you then pick in the TUI's own model dialog replaces it at the TUI's next prompt. OpenCode has no way to go back
18
+ to "no model chosen", so the picker has no Default entry: pick the default model itself.
19
+ - New console route `PUT /api/sessions/:id/model {model: {providerID, modelID, variant?}}` → `{model}` (the
20
+ session's model now). The same access as sending a prompt: the session's owner only (foreign or unknown
21
+ sessions 404, session/app tokens and grant-holders 403, as for every console route). 400 for a malformed body or
22
+ a model or variant the session's catalog lacks, 409 `unsupported` when its spoke is older. Audited as
23
+ `session.model`.
24
+ - Protocol (additive): spoke op `setModel {sessionID, model}` → `{model: SessionModel}`, announced as the new spoke
25
+ feature `setModel`. Older spokes (and hubs that list no features) keep the chip display only.
26
+
27
+ ## 0.10.0
28
+
29
+ One release for everything since 0.9.2: the production-testing fixes, the voice update (Voice front on by default,
30
+ the old `voice` agent removed, Concierge front and grant requests), finding your sessions (Touched by me, Working
31
+ set, activity meter, Context cards), session subscriptions, and the app front (connect apps with consent; speech,
32
+ conversation and Decisions for apps; `packages/voice-client`). No migration is needed: new state is created on
33
+ first use. Machines need the new plugin, and a fast `voiceModel` for the Voice front (see Operators below).
34
+
35
+ ### Voice and the Concierge
36
+ - **The Concierge can ask you for a grant** (user decision, 2026-11-08). When you ask it to help or act in a project
37
+ no grant of its covers, it offers in one sentence, then (after your yes) requests a scoped, short grant with the
38
+ new hub verb `concierge_request_grant` (`POST /api/agent/concierge/grant-requests`, the current Concierge only).
39
+ It is a **tap Decision** for you (never armable, never by voice); **Approve grant** creates it **as you**, holder
40
+ `@@concierge`, beside the Settings grant (revoke it in Settings → Agents). The hub clamps it: verbs approve, deny,
41
+ answer (prompt or stop only when you asked), at most 24 hours (8 by default), only projects the Concierge watches
42
+ (no wildcards), no "always", 60 actions an hour, and your never rules carried over plus `shell:git push*` and
43
+ `shell:rm *`. One open request at a time; none for 24 hours after you deny one. The board shows whether a grant
44
+ covers each session's project (`inGrant=yes (verbs)` / `inGrant=no`), and the context names the grants you
45
+ approved at its request. Concierge prompt v9. See [docs/concierge.md](docs/concierge.md#asking-you-for-a-grant).
46
+ - **Voice mode always uses the voice front; the `voice` agent swap is gone** (user decision, 2026-11-07). The
47
+ console's Settings → Voice → **Voice front** (no longer experimental) is **on by default**, also for Voice mode
48
+ started from the TUI's `/voice` link; turning it off on a device makes Voice mode talk to the session directly.
49
+ The hub asks for a front whenever the machine serves it (`voiceFront`) unless the device sent `front: false`.
50
+ - **The session you talk to is never switched to another agent or model.** Without a front (turned off, an older
51
+ plugin, a front that could not start or failed, or an older hub sending `conversation {on}`), the hub talks
52
+ to the session itself and its turns get the short spoken-reply style. The `voice` agent, its orchestrator
53
+ prompt and the `hub_task_stop` tool are removed. A session an older plugin left on the `voice` agent (a crash
54
+ mid-conversation) is moved to the default agent before the hub's next prompt, or after a turn fails with
55
+ "Agent not found": OpenCode 2.2.3 does not fall back on its own.
56
+ - **`voiceModel` is the voice front's model: set a fast, paid model** (a free one that runs out of quota makes
57
+ the front fail and Voice mode fall back to the session). A session's Voice model in the console is the
58
+ front's too, and applies the next time Voice mode starts; it no longer switches the session's own model.
59
+ - **Hands-on fixes to the front** (preview test, 2026-11-08): it **stops talking after it hands a request to the
60
+ session** (its text after `voice_main_send`/`voice_main_stop` in that turn is never spoken, and the tools say
61
+ the turn is over); it **never reports an old reply as news** (only text from a session turn that started after
62
+ its latest relay counts as the latest reply; older text is labelled old, and a turn-end report carries only that
63
+ turn's text); it **says "the session", never "main"**; and a **correction relayed while the session works gets
64
+ one report**, after the corrected request finishes (a turn end is held while a relay is not delivered yet, at
65
+ most 60 s). A relay cancelled in the session's queue (or lost in an event gap) no longer holds reports: the
66
+ report comes at the turn end. Once the front hands off, nothing more it writes in that run is spoken, even if
67
+ the model hands off a second time.
68
+ - **The front never thinks, and its request is lean**: the hub turns its model's thinking off (Anthropic
69
+ `thinking: {type: "disabled"}`, also over a model's own thinking setting or a `#variant`; OpenAI-style models
70
+ the lowest reasoning effort they list), and its system prompt is its own prompt only (no `AGENTS.md`/`CLAUDE.md`
71
+ or other instruction files, no environment or worktree blocks). Measured through OmniRoute, first token of the
72
+ echo: Haiku 4.5 from about 3.0 s to 1.2 s (median; 0.76–0.87 s on warm turns), Haiku 5.5 from about 2.0 s to
73
+ 1.2 s. **Recommended `voiceModel`: a fast model; Claude Haiku 4.5 measured fastest (0.7–0.8 s first token
74
+ through OmniRoute), Haiku 5.5 is the alternative.**
75
+ - **The Worker model is gone**: no Worker chip in the composer; an older console's worker selection is accepted
76
+ and ignored, and older stored ones are not shown. In `config.json`, `subagent` and `workModel` are obsolete:
77
+ read without error, ignored, logged once per process, and dropped the next time the plugin saves the file.
78
+ - **The composer shows the session's own model** ("Model: Claude Opus 5.5", provider/model in the tooltip): the model
79
+ chosen for the session, else the model its latest reply ran on (what the default resolved to), else the default
80
+ model; "default" only before the spoke knows anything. It updates after each run and Voice never changes it (the
81
+ voice front is a separate session). On a phone it drops the "Model:" prefix and ends in an ellipsis when long.
82
+ New optional `model {providerID, modelID, variant?, name?, source: "chosen" | "reply" | "default"}` in the
83
+ spoke's `summary` result (`GET /api/sessions/:id/summary`); older spokes leave it out and the chip says default.
84
+ - **The Concierge gets the voice front too** (P2, user decision 2026-11-08). Voice in the Concierge panel now talks to
85
+ a fast voice front in front of the Concierge session, as in sessions: the "front" chip and the fallback note show,
86
+ no spoken fillers play, and the Concierge keeps its agent, model, board, grant and Decisions. A spoken "yes"/"no"
87
+ still answers the armed Decision on your device before anything reaches the front.
88
+ - The front answers simple status questions from a compact copy of the Concierge's board (your watched sessions
89
+ with @name, project, machine, state and what they wait for), and hands everything else (approvals, Decisions,
90
+ watch/mute, intents, anything needing the grant) to the Concierge.
91
+ - **New: "tell the hub session to run the tests"** goes straight to that existing session, on any machine, as if
92
+ you typed it there (`relay_to_session`). The front says its pick first ("Sending that to Hub on baruch."); "no,
93
+ the other one" sends it to the other session. It is a message only: it never approves, answers, stops or starts
94
+ anything, and the session's own permissions still apply. No grant and no Decision are needed (it is your own
95
+ voice), and the hub enforces it: only your own, visible sessions (never a front or the Concierge), only for what
96
+ you just said (never from an update), at most 2 sessions per thing you say. Each relay is in Settings → Agents →
97
+ Activity as your prompt, "Relayed by voice from you (Concierge front)", with the device; refused attempts are
98
+ listed too.
99
+ - **Questions about a session are answered, not relayed**: "can you check on the billing lead?" gets an answer
100
+ from the board (or the Concierge) instead of going to @billing-lead, which answered about itself in the first
101
+ hands-on test. The hub refuses a relay of a question about a session ("did hub run the tests?", "what happened in
102
+ hub", "is hub waiting for me"), while requests phrased as questions still go ("could hub update the changelog?",
103
+ "can we squash the commits in hub?"). For a question that also names work, the front may say what it sends and
104
+ confirm once.
105
+ - **The relay delivers only your words**, as the hub heard them, starting with "(by voice, via the Concierge, for you)":
106
+ never the front's rewording or summary. Text on the board from other agents therefore cannot steer a relay or
107
+ invert your request, and a relayed "[hub] …" line cannot pose as a hub message. "No, the other one" carries your
108
+ earlier request only when you said it on the same device.
109
+ - The Concierge's board no longer says "Grant: none" when grants you approved at its request apply: "Grant: none
110
+ from Settings; approved at your request: …".
111
+ - The sessions you relayed to report back like the session in session mode: their question, permission or failure
112
+ is spoken once, and the front says what they did when their turn ends (one report per job, never old news). Other
113
+ watched sessions keep the Concierge's usual updates.
114
+ - Needs the hub and the plugin of this release on the Concierge's machine (`conciergeFront`); with an older plugin
115
+ the Concierge answers Voice itself, as before.
116
+
117
+ ### Session subscriptions
118
+ - **A session can follow other sessions** and react to what happens there: `hub subscribe @builder --type state:idle`
119
+ (also `@@role`, `#team`, `children`; families `state`, `blocker`, `message`, `launch`, `naming`, `presence`,
120
+ `manage`, exact types or `*`). The hub sends the subscriber one batched hub message per window (default 30 s) from
121
+ `app:subscriptions`, waking it when idle without interrupting a turn (`--delivery steer|silent` to change that).
122
+ Visibility is the subscriber's (team scope included), its own events never come back to it, notices continue the
123
+ message hop chain (mutual subscribers stop at the hop cap), and each subscription has an hourly limit (default 20)
124
+ and an expiry (default 1 day, at most 30). Cursors survive restarts; a subscription ends when it expires or its
125
+ sessions are archived or deleted. API `/api/agent/subscriptions`, the OpenCode `hub` tool verbs `subscribe`,
126
+ `unsubscribe` and `subscriptions`, the CLI, the SDK and a **Subscriptions** card in the console's Details rail.
127
+ Journal events `subscription.created` / `subscription.removed`. App tokens need `events:subscribe`, `sessions:read`
128
+ and, unless `delivery` is `silent`, `messages:send`. Batches are claimed before they are sent (batch ID `sb_…`;
129
+ at least once, rarely twice after a crash), notices take no reply, and a Concierge session cannot subscribe.
130
+ No migration is needed.
131
+ [docs/subscriptions.md](docs/subscriptions.md)
132
+
133
+ ### Context cards (opt-in)
134
+ **Context cards (opt-in).** Each session can get short, automatically updated cards that help you (and the
135
+ Concierge) pick its work back up. The first, **Pick up where you left off**, is written by a light model the hub
136
+ calls directly: a one-line headline of where the work stands plus a short summary (what you asked, what the agent did,
137
+ what it waits on). A second, **Changes**, lists the working-tree changes without a model. Cards are modular:
138
+ providers in a hub registry. **They are off for every user until that user turns them on** in Settings › Concierge ›
139
+ Context cards; nothing is sent to any model before. Titles are never rewritten; a session still titled "New session"
140
+ is auto-named. No migration is needed.
141
+
142
+ **For users**
143
+ - **Settings › Concierge › Context cards**: turn them on for yourself; it shows the model, exactly what is sent and
144
+ your daily budget.
145
+ - The session's Details rail has a **Context cards** card (after Subagents): each card with its headline, summary,
146
+ fields and "updated 3m ago · model", a Refresh button each (one per card per 30 s), live updates.
147
+ - The Concierge board shows each watched session's pick-up headline.
148
+
149
+ **For operators**
150
+ - **`contextCards`** in `hub.json` (`enabled`, `model {provider, model, baseURL, apiKey}`, `budget {calls, tokens}`)
151
+ and **`HUB_CONTEXT_CARDS=off`** (the hub-wide kill switch; `false`, `0`, `no` too, and `on`/`true`/`1`/`yes` turn
152
+ cards back on over `hub.json`; any other value stops startup with an error). Model: OpenRouter `anthropic/claude-haiku-4.5`, reusing
153
+ the speech OpenRouter key. Per-user daily budget (default 200 calls, 400,000 tokens), reserved before every call and
154
+ counting failed calls; repeated model failures back a user off and pause the hub's calls for 5 minutes. Automatic
155
+ cards run on one leader pod. What is sent and redacted: [docs/context-cards.md](docs/context-cards.md#privacy-and-cost)
156
+ - **Redaction** (voice recordings and context cards) now also covers whole private key blocks (also cut off and on one
157
+ line), AWS secret keys and STS credentials, `password=`/`passphrase=`/`secret=`/`token=`/`*_API_KEY=`-style values
158
+ (quoted values whole), passwords in URLs and `Authorization` headers. Other secrets in free text are not detected.
159
+ - Context card refresh limits are kept per pod; the daily budget is hub-wide. One user's model failures can no longer
160
+ pause another user's cards (the hub-wide pause needs provider errors from at least 3 users).
161
+
162
+ **For integrators**
163
+ - `GET /api/agent/sessions/:addr/context-cards` (scopes like the intent read), console routes
164
+ `GET /api/sessions/:id/context-cards`, `POST …/context-cards/:provider/refresh {force?}` (429 with `Retry-After`),
165
+ `GET /api/context-cards/primary`, `GET|PUT /api/settings/context-cards`; SSE `contextCard`; journal
166
+ `session.contextCard`; board item `pickup`.
167
+
168
+ ### Find your sessions
169
+ - **Touched by me**: the hub keeps, per user, when you last prompted (terminal, console, Voice), answered or opened
170
+ each session, and a 24 h histogram of your actions and the agent's turns and tool calls (StateStore collection
171
+ `human-activity`, 30 days, throttled writes; no migration needed). `GET /api/activity/mine`, SSE `activity`;
172
+ `GET /api/intents/mine` for the stored intents. Terminal prompts are matched exactly: the OpenCode spoke marks the
173
+ prompts the hub carried (`session.inbox.delivered {carried: true}`, spoke feature `inboxCarried`), and so does the
174
+ Codex spoke; update them to have terminal prompts counted. Claude Code already reports them as the hub's. Context
175
+ cards' automatic runs now gate on this persisted activity, and the list's second line is the primary card's
176
+ headline when there is one. Launch first
177
+ prompts and Concierge proposals are not counted as yours; only an explicit open (`POST …/read {open: true}`) is.
178
+ - **All sessions**: a **Touched by me** chip (24 h, or 1 h / 7 days), a **Last touched** column and sort, a **24 h**
179
+ activity meter column (agent bars, your marks) that sorts by Most active, the session's intent as a second line
180
+ under titles, and "waiting 12m" on sessions that need you (status entries carry `blockedSince`).
181
+ - **Sidebar**: a **Working set** after Needs you (pinned + the 7 sessions you touched most recently within 7 days),
182
+ second lines under titles and waiting times.
183
+ [docs/session-view.md → Finding your sessions](docs/session-view.md#finding-your-sessions)
184
+
185
+ ### Console
186
+ - **Subagents** card in the session's Details rail: the session's subagents with their live state, running ones
187
+ first (then finished, failed first; older ones fold under "N more finished"), each linking into its session.
188
+ - **Redacted thoughts** say what happened: the provider sent only an encrypted signature, no thought text.
189
+ - **Mermaid diagrams** in a streaming reply no longer flicker while the reply updates.
190
+ - **Full-screen viewer** steps through all of a session's media (images and diagrams, in transcript order) with ←/→,
191
+ on-screen buttons or a swipe.
192
+ - **Settings › Voice › Recording**: your recordings fold into one summary row (count and storage) that opens a
193
+ scrolling list grouped by day. Old `#recording` links (the ● Recording chip) open that list.
194
+ - **Voice bar** keeps End on the status row (a long status is cut short; narrow bars move the chips and model to a
195
+ second row), and the pressed voice button keeps its icon visible.
196
+ - **New grant** form: search pickers for the holder (keyboard friendly), project and pattern chips, help for each
197
+ verb and quick expiry choices.
198
+ - **Settings → Agents & access → Connected apps and App clients**: see the apps connected to your account with their
199
+ scopes and revoke them; register app clients (admins: hub-wide; anyone: for their own account only) with exact
200
+ redirect URIs and a scope ceiling; the secret is shown once, and can be rotated; disable or delete a client.
201
+ Connected apps can be chosen as grant holders. The consent page (`/connect/consent/…`) cannot be framed.
202
+
203
+ ### App front (for app developers)
204
+ - **The app front, end to end** ([docs/app-front.md](docs/app-front.md), now the full guide: the BFF pattern, no
205
+ tokens in the browser, no CORS, the scopes, the grants each feature needs, limits and quotas, the refresh rule, the
206
+ security rules, a walkthrough). The **SDK** (`packages/sdk`) gains the app server's side: `HubConnect` (authorize
207
+ URL, callback `state`/`iss` checks, code exchange, refresh, revoke; OAuth errors as `HubOAuthError`), `pkcePair`,
208
+ `ConnectionTokens` (one pair per connection in a `TokenStore`, refreshed single-flight and stored before use; a
209
+ connection the hub ended is forgotten), `hub.speech` (`transcribe`, `speak`), `hub.conversation` (`stream`: the
210
+ frames as an async iterator that resumes with `Last-Event-ID` after a drop, `end slow` or `token_expired`, retrying
211
+ refused reconnects (network, 5xx, 429 with its `retryAfter`) with capped, jittered exponential backoff and stopping
212
+ after `maxReconnects` without progress; `set`,
213
+ `utterance`, `interrupt`) and `hub.appDecisions` (`list`, `answer`); `HubClient` takes a token function (asked
214
+ before every request) and knows `upstream_failed` and app-answered Decisions. The voice loop
215
+ (`packages/voice-client`) gains **`appTransport`**: its `VoiceTransport` onto the app's own server (six routes
216
+ under one base, the relayed stream resumed, `onEnd` for revoked/grant/gone), never the hub. A reference BFF,
217
+ [examples/app-front-bff](examples/app-front-bff/README.md): a token store, single-flight refresh, SSE
218
+ pass-through, no CORS, sign-in and connect as same-origin POSTs (Origin or Sec-Fetch-Site required), `state` bound
219
+ to the session, a `Secure` cookie over https, a streamed body limit. The **app fixture** (`test/it/app-fixture.ts`) gains a registered app
220
+ client (in the startup line), control routes acting as the user on the consent page (`POST /connect/approve`,
221
+ `/connect/deny`), for grants, app tokens and connections, a fake speech provider, sessions that stream a reply
222
+ (`POST /sessions/:id/reply`), a simulated voice front (heard, a sentence, relayed to main) and Decisions a session
223
+ asks (`POST /sessions/:id/ask`); `test/it/app-front.test.ts` runs the whole journey and its refusals against it,
224
+ and `test/it/app-front-proxy.test.ts` runs the voice-client transport through the example BFF against a real hub.
225
+ [docs/app-integration-testing.md](docs/app-integration-testing.md)
226
+ - **Decisions from apps** (`app-decisions` in `/.well-known/hub` `features`; new scope `decisions:answer`, offered on
227
+ the consent page and in Settings → Agents): `GET /api/agent/decisions?answerable=1[&state=open]` lists, and
228
+ `POST /api/agent/decisions/:id/answer {option?, text?}` answers, the **confirm-tier** Decisions without an action
229
+ that one of the app's sessions asked: a session it launched, or one covered by an active grant allowing **`answer`**
230
+ (a prompt-only grant does not count). Everything else is refused: another session's Decision, a deleted session's,
231
+ or an unknown ID `404`; not open, or another answer won first (console or app) `410 gone`; `tap` tier, escalated
232
+ permissions, grant requests, Concierge proposals, Concierge-asked Decisions and lasting options (`always`) `403`;
233
+ a body with `via` `400`; the kill switch `423 paused`. The answer reads `answeredBy: "app"` with the app's party
234
+ (`app`) and label (`appLabel`); the asker's reply says which app answered; the agent audit lists
235
+ `decision.answer` by the app. The console names the app on answered Decisions and says so when a waiting card was
236
+ answered by an app. [docs/app-front.md](docs/app-front.md#decisions-from-apps)
237
+ - **Conversation for apps** (`app-conversation` in `/.well-known/hub` `features`; new scopes `conversation:read` and
238
+ `conversation:write`, offered on the consent page and in Settings → Agents): for a session the app launched or
239
+ holds a grant allowing `prompt` for (any other session, a deleted one, or one covered only by an answer- or
240
+ stop-only grant is `404`), `GET /api/agent/sessions/:addr/stream` streams the reply
241
+ (`ready`, `delta`, `say`, `heard`, `status`, `conversation`, `reset`, `end`; closed frames, no reasoning, activity
242
+ or blocker text), resumable with `Last-Event-ID` (`reset` when the gap is not covered; in a cluster a resume ends
243
+ with `reset {log}`), re-checked every 30 s and at once on revocations (`end` `token_expired`, `revoked`, `grant`,
244
+ `gone`, also when the grant is narrowed to drop `prompt` or the session is deleted), capped at 4 streams per app credential and 20 per user (`429`), and cut off for readers more than 1 MiB
245
+ behind (`end slow`). `POST …/conversation {on, front?}` (the voice front: asked for with `front: true`; never the
246
+ Concierge's front, whose relays act as you), `POST …/utterance {text}` → `202
247
+ {inboxID}` and `POST …/interrupt` need `conversation:write` plus a grant allowing `prompt`, the kill switch off and
248
+ (utterances) the grant's `maxActionsPerHour`; utterances carry the same hub-prompt provenance as typed app turns.
249
+ Typed turns use the existing `POST …/prompt`, now also admitted with `conversation:write`; `GET …/log` is admitted
250
+ with `conversation:read` for the app's sessions. An app whose act the session refuses gets a generic "The session
251
+ refused it" (the session's words stay in the hub's log). [docs/app-front.md](docs/app-front.md#conversation-for-apps)
252
+ - **Speech for apps** (`app-speech` in `/.well-known/hub` `features`): with the new scope `speech:use` (hand-made
253
+ `hat_` tokens and connected apps alike; offered on the consent page and in Settings → Agents),
254
+ `POST /api/agent/speech/transcribe` (raw audio → `{text}`) and `POST /api/agent/speech/speak` (`{text}` →
255
+ `audio/mpeg`, streamed) use the user's own speech provider and key, within the console's audio and text limits
256
+ (text longer than `HUB_MAX_SPEECH_CHARS` is refused, not cut). Per-credential quotas, cluster-wide: transcribe
257
+ 20/min and 300/h, speak 60/min, 1000/h and 100 000 characters/h, then `429 rate_limited` with `retryAfter`.
258
+ No provider: `422 unsupported` (`feature: "speech"`); a provider failure: the new error code
259
+ `502 upstream_failed` (a generic message and the provider's HTTP status; the provider's text stays in the hub's
260
+ log). Admission is atomic across parallel requests and pods (fixed clock windows, so up to twice a limit around a
261
+ window edge). App credentials only (devices use the console routes); control characters are stripped before
262
+ speech. Apps never see speech settings or keys, nothing is recorded for them, and the audit lists their speech use
263
+ per credential every 15 minutes at most, with the hour's request counts and no content. [docs/app-front.md](docs/app-front.md#speech-for-apps)
264
+ - **Connect an app with consent** (`app-connect` in `/.well-known/hub` `features`): a web app's server connects to a
265
+ Hub user's account through an OAuth 2.1-style authorization code + PKCE (S256) flow instead of a hand-made token.
266
+ `GET /connect/authorize` sends the user to the hub's consent page (the app's name, where the browser returns to and
267
+ the requested scopes, which the user can narrow); `POST /connect/token` exchanges the one-time code (60 s) for a
268
+ 1-hour `hca_` access token and a rotating `hcr_` refresh token (30 days sliding; reusing the just-rotated one within
269
+ 30 s answers the same pair, any other reuse revokes the connection); `POST /connect/revoke` (RFC 7009) ends it.
270
+ RFC 8414 metadata at `/.well-known/oauth-authorization-server`; OAuth error bodies; flows and codes in the shared Kv
271
+ (cluster-safe); rate limits per client. The app acts as `app:<connection>`, stable across refreshes, so its grants
272
+ and launches survive rotation; revoking deletes its grants and webhooks. Hand-made `hat_` tokens are unchanged.
273
+ Hardening: wrong client secrets never lock out an app's real server (failures are limited per client and client
274
+ address; only authenticated requests count against the per-client limit), the authorize limit is per client and
275
+ client address and unaudited, a code is checked against its client before it is spent, at most 50 hub-wide and 10
276
+ personal clients per user, and a refused `hca_` token answers `WWW-Authenticate: Bearer error="invalid_token"`.
277
+ Apps must keep one current token pair per connection (see the strict reuse rule in the docs).
278
+ [docs/app-front.md](docs/app-front.md#connect-an-app)
279
+ - **App-front contract updated with the user's answers** (decision nixct956): confidential clients only, a 30 s refresh
280
+ grace, per-user clients next to hub-wide ones, app utterances marked "via app" (AF3), speech quotas configurable in
281
+ `hub.json` (AF2).
282
+ - **App-front contract (frozen, not built yet)**: how an external app's server will use Hub as its AI layer:
283
+ connect with consent (OAuth 2.1-style authorization code + PKCE, short-lived access tokens and rotating refresh
284
+ tokens, clients registered by the owner in Settings), speech, a conversation stream (including the voice front) and
285
+ answering its own sessions' confirm-tier Decisions. New JSON Schema `packages/protocol/schema/app-front/v1.schema.json`
286
+ and shared constants in `packages/protocol` (`APP_FRONT_SCOPES`, `APP_FRONT_FEATURES`, …).
287
+ [docs/plans/2026-11-08-app-front-contracts.md](docs/plans/2026-11-08-app-front-contracts.md)
288
+ - **Voice client package (AF5)**: the console's hands-free Voice loop is now `packages/voice-client`
289
+ (`@huynle/hub-voice-client`, internal), so other web apps can embed it. Everything to and from the session goes
290
+ through the contract's `VoiceTransport` (conversation, utterance, interrupt, transcribe, speak, events); the package
291
+ never holds a Hub token and never calls Hub (an app's transport calls its own server). Spoken diagrams, the cue
292
+ voice key and recording are optional plug-ins (recording off by default). `npm run build:voice-client` builds a
293
+ standalone `dist/` with the workers, onnxruntime-web's wasm, the Silero VAD and Smart Turn models and their licence
294
+ files; the README lists the CSP an embedding app needs (workers, `'wasm-unsafe-eval'` for the worker scripts). The
295
+ console uses the package with no change in behaviour; like the console, the package's loop asks for the voice
296
+ front by default (`front: () => false` opts out). [docs/app-front.md](docs/app-front.md),
297
+ [packages/voice-client/README.md](packages/voice-client/README.md)
298
+
299
+
300
+ ### OpenCode TUI
301
+
302
+ - **`/who` shows who you are**: a `You: @name · project` line heads the dialog (`You: ses_… (unnamed; …)` with how
303
+ to name it), your own row is marked `(you)` and listed first, then online sessions before offline ones, named before
304
+ unnamed. Unnamed online sessions show their title next to their ID; the dialog title counts sessions and online
305
+ ones. The OpenCode `hub` tool's `who` verb marks the caller the same way.
306
+ - **Fix**: the `/who` RPC schema now lists every field the TUI sends and reads. OpenCode drops unlisted keys, so the
307
+ `#team` filter, the team sections (HWA-40) and the caller marker never reached the dialog before.
308
+ - **Incoming hub messages** no longer repeat the sender (`From @x:` twice), and the `hub` tool's results carry a
309
+ title (`hub · reply to @x: …`). Showing those titles in the TUI needs an OpenCode change.
310
+
311
+ ### For operators (upgrading)
312
+ - **New StateStore collections**: `context-cards`, `context-card-usage`, `context-card-settings`, `human-activity`,
313
+ `subscriptions` and `subscription-index`. No migration is needed; they are created on first use.
314
+ - **New leaders**: `context-cards` and `subscriptions` (one pod each in cluster mode).
315
+ - **New spoke feature `inboxCarried`**: update the OpenCode and Codex spokes with the hub so terminal prompts count
316
+ as yours (Touched by me, context cards' activity gate).
317
+ - **`agents.speechQuotas`** in `hub.json`: per-credential quotas of speech for apps, any subset of
318
+ `{transcribe: {perMinute, perHour}, speak: {perMinute, perHour, charsPerHour}}` (defaults 20, 300; 60, 1000,
319
+ 100 000).
320
+ - **`HUB_TRUSTED_PROXIES`**: the reverse proxies whose `X-Forwarded-For` the hub may read to tell clients apart for
321
+ per-address rate limits (connect-app). Unset, the hub uses the TCP peer and never reads the header.
322
+
3
323
  ## 0.9.2
4
324
 
5
325
  Webhooks can reach a named receiver on your own network without opening the hub to every private address. No
package/Dockerfile CHANGED
@@ -24,11 +24,14 @@ COPY packages/claude-code/package.json ./packages/claude-code/
24
24
  COPY packages/conformance/package.json ./packages/conformance/
25
25
  COPY packages/preset/package.json ./packages/preset/
26
26
  COPY packages/codex/package.json ./packages/codex/
27
+ COPY packages/voice-client/package.json ./packages/voice-client/
27
28
  RUN npm ci --no-audit --no-fund --ignore-scripts
28
29
  COPY web ./web
29
- # The console imports the protocol and SDK sources (web/src/… ../packages/{protocol,sdk}/src).
30
+ # The console imports the protocol, SDK and voice-client sources (web/src/… ../packages/{protocol,sdk,voice-client}/src;
31
+ # the voice client brings the speech-detection and turn models).
30
32
  COPY packages/protocol/src ./packages/protocol/src
31
33
  COPY packages/sdk/src ./packages/sdk/src
34
+ COPY packages/voice-client/src ./packages/voice-client/src
32
35
  RUN npx vite build --config web/vite.config.ts
33
36
 
34
37
  FROM oven/bun:1.4.4-slim@sha256:1846adf22496bd711036aae8be147e57bdebbaea2e5264e3e0b5af1db187d9a4 AS deps
@@ -46,6 +49,7 @@ COPY packages/claude-code/package.json ./packages/claude-code/
46
49
  COPY packages/conformance/package.json ./packages/conformance/
47
50
  COPY packages/preset/package.json ./packages/preset/
48
51
  COPY packages/codex/package.json ./packages/codex/
52
+ COPY packages/voice-client/package.json ./packages/voice-client/
49
53
  # Bun migrates package-lock.json; --frozen-lockfile keeps the pinned versions.
50
54
  RUN bun install --production --frozen-lockfile --ignore-scripts
51
55
  COPY docker/hub-deps.ts ./