@huynle/hub 0.7.4 → 0.8.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 (241) hide show
  1. package/CHANGELOG.md +430 -1
  2. package/Dockerfile +7 -2
  3. package/README.md +230 -32
  4. package/package.json +5 -3
  5. package/packages/claude-code/package.json +1 -1
  6. package/packages/claude-code/src/daemon.ts +8 -1
  7. package/packages/claude-code/src/launcher.ts +29 -9
  8. package/packages/claude-code/src/paths.ts +3 -3
  9. package/packages/claude-code/src/profile.ts +173 -0
  10. package/packages/claude-code/src/sessions.ts +22 -4
  11. package/packages/cli/dist/hub-cli.js +6871 -3460
  12. package/packages/cli/package.json +1 -1
  13. package/packages/cli/src/artifact-jobs-mcp.ts +226 -0
  14. package/packages/cli/src/artifacts-mcp.ts +160 -0
  15. package/packages/cli/src/bootstrap/adapters/node-services/render.ts +2 -2
  16. package/packages/cli/src/bootstrap/discovery/host.ts +15 -1
  17. package/packages/cli/src/bootstrap/discovery/probe.ts +1 -1
  18. package/packages/cli/src/bootstrap/enrollment/enroll.ts +21 -9
  19. package/packages/cli/src/bootstrap/enrollment/policy.ts +138 -12
  20. package/packages/cli/src/bootstrap/enrollment/status.ts +3 -2
  21. package/packages/cli/src/cli.ts +96 -14
  22. package/packages/cli/src/format.ts +48 -10
  23. package/packages/cli/src/http.ts +2 -0
  24. package/packages/cli/src/intents.ts +23 -0
  25. package/packages/cli/src/launches.ts +9 -3
  26. package/packages/cli/src/login.ts +20 -6
  27. package/packages/cli/src/mcp.ts +385 -10
  28. package/packages/cli/src/messaging.ts +7 -3
  29. package/packages/cli/src/profiles-mcp.ts +301 -0
  30. package/packages/cli/src/setup/commands.ts +23 -4
  31. package/packages/cli/src/setup/runtime.ts +3 -2
  32. package/packages/cli/src/spoke-services.ts +1 -1
  33. package/packages/cli/src/team-intake-mcp.ts +158 -0
  34. package/packages/cli/src/teams-mcp.ts +460 -0
  35. package/packages/cli/src/watchlist.ts +135 -0
  36. package/packages/codex/package.json +1 -1
  37. package/packages/codex/src/paths.ts +3 -3
  38. package/packages/codex/src/profile.ts +163 -0
  39. package/packages/codex/src/sessions.ts +12 -0
  40. package/packages/codex/src/spoke.ts +19 -1
  41. package/packages/node/package.json +1 -1
  42. package/packages/node/src/config.ts +104 -2
  43. package/packages/node/src/opencode-warm.ts +344 -0
  44. package/packages/node/src/run.ts +28 -1
  45. package/packages/preset/package.json +1 -1
  46. package/packages/preset/src/profile.ts +34 -0
  47. package/packages/preset/src/spoke.ts +4 -0
  48. package/packages/protocol/package.json +1 -1
  49. package/packages/protocol/src/artifacts.ts +142 -0
  50. package/packages/protocol/src/concierge.ts +366 -0
  51. package/packages/protocol/src/contracts.ts +104 -0
  52. package/packages/protocol/src/graph.ts +70 -0
  53. package/packages/protocol/src/hub-workspace.ts +201 -0
  54. package/packages/protocol/src/index.ts +175 -6
  55. package/packages/protocol/src/launch.ts +36 -0
  56. package/packages/protocol/src/profiles.ts +301 -0
  57. package/packages/protocol/src/teams.ts +204 -0
  58. package/packages/runtime-openshell/package.json +1 -1
  59. package/packages/runtime-sdk/package.json +1 -1
  60. package/packages/sdk/package.json +1 -1
  61. package/packages/sdk/src/address.ts +50 -2
  62. package/packages/sdk/src/artifact-jobs.ts +194 -0
  63. package/packages/sdk/src/artifacts.ts +187 -0
  64. package/packages/sdk/src/client.ts +226 -5
  65. package/packages/sdk/src/concierge.ts +88 -0
  66. package/packages/sdk/src/errors.ts +4 -1
  67. package/packages/sdk/src/index.ts +6 -0
  68. package/packages/sdk/src/team-intake.ts +122 -0
  69. package/packages/sdk/src/teams.ts +346 -0
  70. package/packages/sdk/src/types.ts +568 -1
  71. package/packages/sdk/src/wake.ts +20 -0
  72. package/packages/spoke-sdk/package.json +1 -1
  73. package/packages/spoke-sdk/src/index.ts +1 -0
  74. package/packages/spoke-sdk/src/profile-apply.ts +245 -0
  75. package/src/central/actions.ts +7 -0
  76. package/src/central/agent/acting-routes.ts +2 -1
  77. package/src/central/agent/acting-switch.ts +2 -1
  78. package/src/central/agent/acting.ts +218 -10
  79. package/src/central/agent/app-tokens.ts +3 -2
  80. package/src/central/agent/audit.ts +18 -1
  81. package/src/central/agent/body.ts +163 -0
  82. package/src/central/agent/decisions.ts +125 -2
  83. package/src/central/agent/delivery.ts +194 -40
  84. package/src/central/agent/directory-routes.ts +37 -7
  85. package/src/central/agent/directory.ts +70 -14
  86. package/src/central/agent/errors.ts +7 -0
  87. package/src/central/agent/escalation.ts +10 -2
  88. package/src/central/agent/events-routes.ts +21 -4
  89. package/src/central/agent/events.ts +18 -4
  90. package/src/central/agent/form-check.ts +85 -0
  91. package/src/central/agent/grant-requests.ts +2 -1
  92. package/src/central/agent/grant-routes.ts +4 -3
  93. package/src/central/agent/grants.ts +29 -2
  94. package/src/central/agent/idempotency.ts +133 -0
  95. package/src/central/agent/intent-routes.ts +116 -0
  96. package/src/central/agent/intents.ts +505 -0
  97. package/src/central/agent/interaction-graph-routes.ts +212 -0
  98. package/src/central/agent/interaction-graph.ts +865 -0
  99. package/src/central/agent/launch-engine.ts +189 -32
  100. package/src/central/agent/launch-routes.ts +26 -10
  101. package/src/central/agent/launch-spec.ts +40 -3
  102. package/src/central/agent/launch-store.ts +55 -2
  103. package/src/central/agent/message-limits.ts +89 -11
  104. package/src/central/agent/message-routes.ts +10 -4
  105. package/src/central/agent/messages.ts +28 -2
  106. package/src/central/agent/names.ts +12 -2
  107. package/src/central/agent/placement.ts +93 -18
  108. package/src/central/agent/read-routes.ts +89 -19
  109. package/src/central/agent/routes.ts +178 -5
  110. package/src/central/agent/webhook-routes.ts +2 -1
  111. package/src/central/agent/webhooks.ts +17 -2
  112. package/src/central/agent-data.ts +4 -0
  113. package/src/central/artifacts/access.ts +44 -0
  114. package/src/central/artifacts/blob-store.ts +113 -0
  115. package/src/central/artifacts/blob-support.ts +98 -0
  116. package/src/central/artifacts/events.ts +27 -0
  117. package/src/central/artifacts/host-caps.ts +162 -0
  118. package/src/central/artifacts/host.ts +315 -0
  119. package/src/central/artifacts/idempotency.ts +109 -0
  120. package/src/central/artifacts/jobs-routes.ts +776 -0
  121. package/src/central/artifacts/jobs.ts +344 -0
  122. package/src/central/artifacts/maintenance.ts +103 -0
  123. package/src/central/artifacts/routes.ts +360 -0
  124. package/src/central/artifacts/store.ts +776 -0
  125. package/src/central/artifacts/workspaces.ts +31 -0
  126. package/src/central/backend/file-blobs.ts +377 -0
  127. package/src/central/backend/index.ts +12 -3
  128. package/src/central/backend/memory-blobs.ts +116 -0
  129. package/src/central/backend/postgres-blobs.ts +513 -0
  130. package/src/central/backend/postgres-migrate.ts +33 -0
  131. package/src/central/backend/postgres-state.ts +19 -0
  132. package/src/central/backend/types.ts +8 -0
  133. package/src/central/concierge/activity.ts +142 -0
  134. package/src/central/concierge/arm.ts +63 -0
  135. package/src/central/concierge/board-render.ts +190 -0
  136. package/src/central/concierge/board-results.ts +169 -0
  137. package/src/central/concierge/board-routes.ts +350 -0
  138. package/src/central/concierge/board.ts +249 -0
  139. package/src/central/concierge/context.ts +221 -0
  140. package/src/central/concierge/entitlements.ts +13 -0
  141. package/src/central/concierge/guard.ts +120 -0
  142. package/src/central/concierge/inbound.ts +86 -0
  143. package/src/central/concierge/index.ts +84 -0
  144. package/src/central/concierge/notes.ts +41 -0
  145. package/src/central/concierge/notifier.ts +986 -0
  146. package/src/central/concierge/prompt.ts +131 -0
  147. package/src/central/concierge/proposals.ts +268 -0
  148. package/src/central/concierge/push-remap.ts +49 -0
  149. package/src/central/concierge/routes.ts +122 -0
  150. package/src/central/concierge/schedule.ts +113 -0
  151. package/src/central/concierge/service.ts +243 -0
  152. package/src/central/concierge/session-routes.ts +125 -0
  153. package/src/central/concierge/settings.ts +217 -0
  154. package/src/central/concierge/supervisor.ts +767 -0
  155. package/src/central/concierge/tiers.ts +260 -0
  156. package/src/central/concierge/watchlist-routes.ts +279 -0
  157. package/src/central/concierge/watchlist.ts +273 -0
  158. package/src/central/config.ts +28 -6
  159. package/src/central/http.ts +660 -54
  160. package/src/central/main.ts +1 -1
  161. package/src/central/mcp-http.ts +10 -21
  162. package/src/central/profiles/apply.ts +333 -0
  163. package/src/central/profiles/content.ts +174 -0
  164. package/src/central/profiles/events.ts +40 -0
  165. package/src/central/profiles/idempotency.ts +14 -0
  166. package/src/central/profiles/import.ts +139 -0
  167. package/src/central/profiles/routes.ts +356 -0
  168. package/src/central/profiles/store.ts +597 -0
  169. package/src/central/profiles/usage-ports.ts +120 -0
  170. package/src/central/profiles/usage.ts +364 -0
  171. package/src/central/push.ts +7 -0
  172. package/src/central/registry.ts +69 -7
  173. package/src/central/store.ts +4 -0
  174. package/src/central/teams/collections.ts +34 -0
  175. package/src/central/teams/discovery.ts +346 -0
  176. package/src/central/teams/draft-edits.ts +46 -0
  177. package/src/central/teams/events.ts +46 -0
  178. package/src/central/teams/intake-routes.ts +100 -0
  179. package/src/central/teams/intake.ts +786 -0
  180. package/src/central/teams/policy.ts +231 -0
  181. package/src/central/teams/routes.ts +516 -0
  182. package/src/central/teams/seats.ts +272 -0
  183. package/src/central/teams/staffing.ts +910 -0
  184. package/src/central/teams/store.ts +667 -0
  185. package/src/central/teams/validation.ts +256 -0
  186. package/src/central/workspaces/board.ts +408 -0
  187. package/src/central/workspaces/checkpoints.ts +316 -0
  188. package/src/central/workspaces/coordinator.ts +841 -0
  189. package/src/central/workspaces/events.ts +85 -0
  190. package/src/central/workspaces/lifecycle.ts +93 -0
  191. package/src/central/workspaces/master.ts +490 -0
  192. package/src/central/workspaces/members.ts +632 -0
  193. package/src/central/workspaces/routes.ts +354 -0
  194. package/src/central/workspaces/sidechats-routes.ts +410 -0
  195. package/src/central/workspaces/sidechats.ts +482 -0
  196. package/src/central/workspaces/store.ts +571 -0
  197. package/src/central/workspaces/supervision.ts +355 -0
  198. package/src/index.ts +126 -23
  199. package/src/migrate.ts +109 -3
  200. package/src/spoke/concierge-agent.ts +33 -0
  201. package/src/spoke/deliver.ts +22 -4
  202. package/src/spoke/env-inject.ts +8 -1
  203. package/src/spoke/fork-revert.ts +142 -0
  204. package/src/spoke/hub-rpc.ts +41 -5
  205. package/src/spoke/hub-tool.ts +404 -25
  206. package/src/spoke/launchers.ts +6 -4
  207. package/src/spoke/listing.ts +65 -10
  208. package/src/spoke/localhub.ts +14 -4
  209. package/src/spoke/locations.ts +40 -0
  210. package/src/spoke/profile-apply.ts +298 -0
  211. package/src/spoke/session-context.ts +256 -0
  212. package/src/spoke/spoke.ts +330 -23
  213. package/src/spoke/timeline.ts +6 -1
  214. package/src/spoke/tracker.ts +23 -1
  215. package/src/spoke/who-teams.ts +81 -0
  216. package/src/tui-messaging.ts +32 -18
  217. package/web/dist/assets/ArtifactPage-mNzjlP_U.js +1 -0
  218. package/web/dist/assets/ArtifactPane-CPQcuX7c.js +1 -0
  219. package/web/dist/assets/ArtifactWorkbench-9eTj1CeW.css +1 -0
  220. package/web/dist/assets/ArtifactWorkbench-NoFC5Sb2.js +6 -0
  221. package/web/dist/assets/Library-DYsFj_5c.css +1 -0
  222. package/web/dist/assets/Library-LXHsP5Fw.js +2 -0
  223. package/web/dist/assets/ProfilesDestination-DhVVcAep.js +1 -0
  224. package/web/dist/assets/TeamsPage-DBrO5hoL.js +1 -0
  225. package/web/dist/assets/Watchtower-CTgqHk5g.css +1 -0
  226. package/web/dist/assets/Watchtower-DxaJYQp_.js +2 -0
  227. package/web/dist/assets/index-BabrNDB3.css +1 -0
  228. package/web/dist/assets/index-lIOfSYck.js +34 -0
  229. package/web/dist/assets/live-BDq0AABg.js +5 -0
  230. package/web/dist/assets/live-BUofsQpz.css +1 -0
  231. package/web/dist/assets/pane-layout-DWXfDU-1.js +1 -0
  232. package/web/dist/assets/plural-Bspvoqfq.js +1 -0
  233. package/web/dist/assets/renderer2d-DAr4IHM8.js +1 -0
  234. package/web/dist/assets/renderer3d-DWZj595u.js +5393 -0
  235. package/web/dist/assets/safe-area-JUb6OeI3.js +1 -0
  236. package/web/dist/assets/source-CDU04kYw.js +1 -0
  237. package/web/dist/assets/ui-Dh1-78Cl.js +3 -0
  238. package/web/dist/index.html +5 -2
  239. package/web/dist/sw.js +22 -2
  240. package/web/dist/assets/index-Dm8OmkaO.css +0 -1
  241. package/web/dist/assets/index-FUJbj_Du.js +0 -20
package/CHANGELOG.md CHANGED
@@ -1,6 +1,435 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.8.1
4
+
5
+ A fix-up release after 0.8.0. App and gateway messages keep waking idle sessions (per-conversation hops with a relay
6
+ guard, and the reason when a wake is skipped); launches say why they wait; the Concierge gets per-machine Directory
7
+ and Model dropdowns, full-detail Decision cards (tap-tier ones too, never by voice), plainer Activity and fail-closed
8
+ hardening; the console uses in-page dialogs, collapsible sidebar sections and names instead of raw IDs; the right
9
+ rail's Branch and Changes work for background-service sessions; the hub state dir honours `XDG_STATE_HOME` without
10
+ moving existing state; MCP gains profile lifecycle tools. No migration is needed. Codex and Claude Code fork/revert
11
+ remain for a later release.
12
+
13
+ ### MCP: profile lifecycle and team staffing
14
+ - **Profile lifecycle over MCP** (`hub mcp`, `POST /mcp`): `profile_show`, `profile_versions`, `profile_create`,
15
+ `profile_update`, `profile_version_create` (content or `fromVersion` rollback), `profile_skill_set` /
16
+ `profile_skill_remove`, `profile_command_set` / `profile_command_remove` (attach or detach one skill or command as
17
+ the next immutable version), `profile_archive` / `profile_restore` and `profile_assign` / `profile_unassign`. Each
18
+ is an existing `/api/agent/profiles…` route with its scopes (`profiles:read`, `profiles:manage`,
19
+ `profiles:assign` + `sessions:read`); no server change ([docs/profiles.md](docs/profiles.md#sdk-and-mcp)).
20
+ - **`team_staff` / `team_replace` describe the nested launch spec** (`workspace{repo, mode, branch, base, remote}`,
21
+ `placement{machine, labels, mode}`, `runtime{kind, profile}`, `end{when, timeout, cleanup}` and the top-level
22
+ fields) in their input schema ([docs/teams.md](docs/teams.md#mcp-tools)).
23
+ - **Launch specs name misplaced fields**: an unknown launch field answers every unknown field at once and where each
24
+ belongs (`Unknown launch fields repo, cleanup: did you mean workspace.repo, end.cleanup?`), with the nested shape
25
+ as the hint, for `POST /api/agent/launches`, team staffing and the MCP tools.
26
+
27
+ ### Messaging and Concierge settings
28
+ - **Settings → Concierge: Directory and Model are dropdowns of the picked machine.** Directory lists the projects its
29
+ OpenCode service reports under (or at) its node's workspace roots, then the other roots, with **Other folder…** for
30
+ a typed path (a project that is the root itself is listed once, as a project, and the hint names the roots: FX-11);
31
+ picking another machine clears a directory that machine does not report. Model lists the machine's OpenCode models
32
+ with **Machine default** and **Other model…**; picking another machine resets a model its list lacks. Loading,
33
+ offline and error states keep the typed fallback; hints are linked to the dropdowns (`aria-describedby`). New
34
+ user-only route `GET /api/agent/concierge/machines/:id/models` (404 for a machine that is not yours, 503
35
+ `offline` without a connected service, a reported session, or a spoke answer).
36
+ - **App asks stop waking sessions after 4 round trips (S1, wge0vjdz).** An app token was one hop-depth party for all
37
+ its conversations and had no human turn to reset it, so every ask/reply round trip raised it by 2 and from the 5th
38
+ ask on (hop 9 > the cap of 8) nothing it sent woke anyone for 24 h; the spoke answered a bare `queued`. Apps now
39
+ count hops per conversation: a new `msg`/`ask` from an app is hop 1, replies and declines to an app raise nothing,
40
+ and an app's own reply or decline is the hop of the message it answers + 1 (reply ping-pong still hits the cap).
41
+ Session-to-session chains, human-turn resets, the pair limit (20/10 min) and the wake budget (30/10 min, which
42
+ bound an app answering one session with new messages forever) are unchanged. Covers app tokens, `app:concierge`,
43
+ the workspace coordinator and team intake.
44
+ - **Relay guard for apps.** For 5 minutes (the relay window, a code-level default, not a config setting) after a session sends an app something, the app's new
45
+ msg/ask to *another* session carries that hop + 1, so a relay loop A → app → B → app → A reaches the hop cap after
46
+ about 8 hops instead of only the rate limits; spaced-out asks and one-to-one app conversations still start at hop
47
+ 1. The hub's own parties (Concierge, workspace coordinator, team intake) keep no relay depth: they send on events
48
+ and submissions, not on what they receive.
49
+ - **Team intake continues the submitter's chain.** A session's submission is delivered from `app:team-intake` at
50
+ the submitter's depth + 1 (kept while it is queued), not hop 1.
51
+ - **Hop-cap alert wording.** "Agent messages went 8 hops without you (latest: A → B) …" instead of "Sessions
52
+ messaged each other", which was wrong when an app was in the chain.
53
+ - **Never give agents an app token** (README): it lets them send at a fresh hop depth.
54
+ - **Why a message did not wake.** `POST /api/agent/msg|ask|reply|decline` answers carry `woke` and, when false,
55
+ `wakeSkipped: wake_false | subagent | hop_cap | budget` (additive; SDK `Sent.woke`, `Sent.wakeSkipped`, type
56
+ `WakeSkip`, and `wakeSkipNote`/`WAKE_SKIP_TEXT` for the shared wording). `hub msg`/`ask`/`reply`/`decline`, the
57
+ MCP tools and the OpenCode `hub` tool (text and metadata) print it: `queued (read at its next turn) — wake
58
+ skipped: hop limit reached (8 agent messages without a human turn)`; `wake_false` reads "sent without a wake".
59
+
60
+ ### Launches say why they wait
61
+ - A queued launch stores why it waits: `GET /api/agent/launches/:id` carries `waiting: {wait, reason, since}` and
62
+ `reason` is the same text. Written only when it changes, announced as a `launch` event and logged once per change
63
+ (`[hub] launch <id> waits (<wait>): <reason>`); cleared once placed. Before, it said only "Queued".
64
+ - Specific reasons: the active cap names who holds it (`4 of 4 active launches: 3 by app "ci-bot", 1 by you`), a
65
+ full node gives its counts (`baruch (4/4 used, 0 reserved)`), a node that does not fit its first problem.
66
+ - `hub launches list|show` and the MCP launch tools print the wait; Settings → Agents & access has a new **Launches**
67
+ list (live launches, updated from the event stream, why each waits, Stop with a per-row accessible name; the
68
+ Concierge's own launch is managed in Settings → Concierge); the Concierge's status says "Waiting: …" instead of
69
+ "Starting".
70
+ - An app or session sees its own launch's wait in a launcher-safe form: counts only for the cap ("1 yours, 3 by
71
+ others"), no machine names. The user sees the full text.
72
+ - Only a new wait kind is a launch change (journal `launch.changed` with `data.wait`, SSE); never a webhook, so
73
+ webhooks keep one delivery per state (`launch.queued` once). Text-only updates (slot counts) go to the console's
74
+ live stream only; artifact jobs show them without a new job revision.
75
+ - The Concierge's own launch no longer counts toward `agents.launches.maxActive` (it still takes a node slot).
76
+ - Revoking an app token does not stop its launches; they are flagged (`launcherRevoked`) in the user's lists and in
77
+ the cap's waiting reason so the user can stop them. Launches record the app token's label (`byLabel`).
78
+ - A node cleanup that failed while the node stayed connected is retried (capacity reports, the 15 s sweep, up to 5
79
+ times; and on reconnect), so the node no longer keeps a slot for an ended launch.
80
+ - A running launch ends `lost` when the same node instance that reported it leaves it out of its reports for 5
81
+ minutes while no spoke reports its session, freeing its reserved slot. Never on a disconnected node, a short gap,
82
+ or a restarted node daemon (a new instance) that never reported it; a lost launch whose session is still reported
83
+ keeps its workspace. `agents.launches.lostAfterMinutes`: 0–1440, or 0/false to turn it off.
84
+ - A launch stopped between placement's placing and provisioning steps (e.g. the Concierge reaping an orphan) is
85
+ skipped quietly instead of logging a placement error.
86
+ - Engine timers are clamped to setTimeout's 32-bit limit; an `end.timeout` over 24.8 days (up to 30) no longer fires
87
+ at once.
88
+
89
+ ### Concierge hardening
90
+ - **Activity in plain words.** Settings → Agents → Activity says what the Concierge did without internal terms:
91
+ "Approved by the Concierge inside your grant. Its note: «…»", "Needed your tap: shell commands, outside folders
92
+ and tools always do", "The Concierge proposed this for your answer", "Refused: only you can message the
93
+ Concierge". Its note is framed as data (its own « » become ‹ ›, bidi and invisible controls dropped, one line, at
94
+ most 300 characters), so it cannot fake text after it.
95
+ No `inGrant`, `auto` or raw Decision IDs; the Concierge is told to write its why in plain words too
96
+ (Concierge prompt v7).
97
+ - **Decision lookups in the Concierge panel** skip IDs in a reply that is still streaming (half an ID was looked up
98
+ and remembered as unknown), back off on 429/5xx/network failures (2 s doubling, ≤ 1 min) instead of retrying on
99
+ every render, and run at most 4 at once.
100
+ - **Watchlist after a reconnect.** A process counts as reporting its sessions only after its first sessions frame
101
+ (catalog `sessionsPending`, additive), so the watchlist and board no longer empty for a moment after `hello`.
102
+ - **`concierge_find`** with no letters or digits ("@", "?!") no longer lists every session: the hub's
103
+ `GET /api/agent/sessions?q=` matches nothing for such a `q` (a blank `q` still filters nothing).
104
+ - **Concierge session lock file** (`concierge-sessions.json`): writes from several OpenCode processes are serialized
105
+ under an exclusive `<file>.lock` (a stale one is broken), so no process drops another's locks.
106
+ - **Identity:** a Concierge session older than the 500 the hub keeps still counts as one while its machine reports
107
+ it locked (spokes mark locked sessions `locked: true` from their first sessions frame after a restart). Concierge
108
+ identity reads (route gate, manage guard, acting guard, inbound/outbound rules, team staffing, Decision and audit
109
+ labels) wait at most 5 s, and a user whose read timed out is answered at once for the next 5 s; a stuck or failed
110
+ read refuses with a neutral 503 `busy` "Hub state is busy, try again" (new error code, CLI exit 7), audited as
111
+ decision `busy`, outcome `refused`; team staffing
112
+ (attach, replace, a staffing launch) refuses a Concierge session in the service as well as the route.
113
+ - **Arm badge:** the console lapses an arm at its expiry itself, so a missed `armed: null` never leaves a stale ✨.
114
+
115
+ ### Console polish
116
+ - **Collapsible sidebar sections**: Needs you, Running, Pinned, the session groups, Recents and Workspaces collapse
117
+ from their heading (mouse or keyboard, `aria-expanded`), remembered on this device. A collapsed section shows its
118
+ count; Needs you always shows its count, so collapsing it never hides that something waits.
119
+ - **In-page confirm dialog** instead of the browser's `window.confirm` (Revert, stopping a launch, turning the Concierge off, revoking app tokens, grants,
120
+ machines and devices, turning agent actions off, signing out, admin user actions, resetting speech settings):
121
+ focus starts on the primary action (Enter confirms), on Cancel for destructive (red) actions; Tab stays inside; a
122
+ held Enter (key auto-repeat) is ignored; Escape or Cancel cancels (Escape closes only the dialog, not the
123
+ Concierge panel or a popover behind it); 44 px buttons on touch screens.
124
+ - **/new**: a machine that reports exactly one project has it chosen; Enter without a project says so and moves the
125
+ focus to the Project chip, which stands out while nothing is chosen.
126
+ - **Concierge panel** is fitted with the sidebar and rail: at 1280 px the transcript keeps 420 px (it had 345); at
127
+ 1024 px it gets 304 px with every panel at its minimum (it had 119), 420 px without a rail.
128
+ - Fixes: a path with a leading dot in the Changes list (`.claude/settings.local.json`) no longer shows the dot at the
129
+ end; "Denied by you" shows once when a denial stops the turn; a grant's expiry rounds to the nearest whole day
130
+ everywhere (no more "7 d" on one page and "6 d" on another); Decision links in the Concierge panel no longer push
131
+ the text after them to a new line; the panel's disabled Send button looks disabled.
132
+ - **The right rail's Branch and Changes come from the session's own directory** (FX-10). OpenCode's plugin `vcs`
133
+ API answers for the plugin instance's own location whatever location is passed, and the spoke used the first
134
+ instance's (the background service's is its `$HOME`), so a service session in a project showed "No changes." and
135
+ Branch "—" (and the per-file diff read the wrong tree). The spoke now reads through the plugin instance whose
136
+ location is the session's directory, else the own server's API (which honours the location), else shows nothing;
137
+ a read that takes over 5 s (a hung server) is left out, so the rest of the summary still answers.
138
+
139
+ ### Robustness and the hub state directory
140
+ - **State dir honours `XDG_STATE_HOME`, sticky**: the plugin (launchers, Concierge lock), the local hub, the hub
141
+ server and the Claude Code and Codex spokes resolve it the same way: `$HUB_STATE_DIR`; else `$XDG_STATE_HOME/hub`
142
+ if it exists; else an existing `~/.local/state/hub`; else `$XDG_STATE_HOME/hub` when XDG_STATE_HOME is set (fresh
143
+ installs); else `~/.local/state/hub`. The legacy `opencode-hub` / `opencode-voice` state migrates from under
144
+ `$XDG_STATE_HOME` and `~/.local/state`. `hub claude-code install` / `hub codex-spoke install` carry
145
+ `XDG_STATE_HOME` into the service. The Concierge lock is read from both default places (written to the resolved
146
+ one), so a session locked before the upgrade stays locked; with `HUB_STATE_DIR` set (an isolated preview or test)
147
+ it is read and written only there.
148
+ **Upgrade note:** nothing moves. If `XDG_STATE_HOME` is set and you have `~/.local/state/hub`, the hub keeps using
149
+ it and logs one line at startup with the command to switch: stop the hub and OpenCode, then
150
+ `mv ~/.local/state/hub "$XDG_STATE_HOME/hub"`. Isolated test/preview instances must set `HUB_STATE_DIR` and their
151
+ own `HOME`; `XDG_STATE_HOME` alone does not isolate them from an existing `~/.local/state/hub`.
152
+ A dir counts only when it holds hub state (`state.json`, `token-keys.json`, `agent/`, `blobs/`,
153
+ `concierge-sessions.json`, `bin/`, `hub.log`, `hub.pid`, `claude-code/`, `codex/`), never `bootstrap/` alone, and
154
+ `hubctl setup` now writes `bootstrap/` into the dir the hub resolves (its state root follows the resolver; other
155
+ roots unchanged). The legacy `opencode-hub` / `opencode-voice` copy also fills a hub dir that holds no hub state
156
+ yet (e.g. only `bootstrap/` from an earlier setup), next to `bootstrap/` and never over it. A relative
157
+ `XDG_STATE_HOME` is ignored (XDG spec). **Service units must carry the same `XDG_STATE_HOME` as your shell** (or `HUB_STATE_DIR`), or a
158
+ shell and a unit can use different dirs: hub's own `claude-code` / `codex-spoke` units already pass it; a unit you
159
+ wrote for `opencode serve --service` needs it set.
160
+ - **Warmer hardening** (`hub node`): OpenCode's `service.json` is used only when it is a regular file owned by this
161
+ user and not group/world-writable (no symlinks), and its pid is this user's live process (EPERM is not ours). Warm
162
+ requests wait 30 s by default (was 10 s), configurable with node.json `opencode.timeoutSeconds` (1–300).
163
+ - `hubctl setup --verify-launch` warns about a stored plain-http (non-loopback) login, like every `hub` command.
164
+ - Refused hub URLs (`hub login`, `hubctl machine enroll`) no longer echo user info: shown as `https://***@host`
165
+ (also `http:/u:p@h`, `http:u:p@h`, `http:\\u:p@h`, leading whitespace and scheme-less `user:pass@host`).
166
+ - **Listings are cancelled**: a session listing that times out aborts its signal (`SpokeHost.listRecent` gets
167
+ `signal`, passed to every OpenCode request), so a hung server does not collect abandoned listings.
168
+ - **Pending messages tell their sender when they expire**: a message still pending after 24 h (recipient offline or
169
+ busy) gets delivery `"expired"` (new `MessageDelivery` value), its sending session gets a quiet hub notice
170
+ (`wake: false`, never the text), the user an alert (push kind `message`), and the change is journaled
171
+ (`message.changed`).
172
+ - `HEAD /healthz` and `HEAD /readyz` answer like GET (same status and headers, no body) instead of 404, for uptime
173
+ monitors and `curl -I`. No other route changed.
174
+ - Docs: README "Testing and preview isolation" (`HUB_STATE_DIR`, own `HOME`/`XDG_*`, `herdr: false`,
175
+ `autoStartLocalHub: false`, sessions only in scratch repos).
176
+
177
+ ### Decision cards and the Concierge panel
178
+ - **Decision cards say what they ask for** (FX-05): the Concierge panel's armed and listed cards, and every Decision
179
+ card on Home and in sessions, show the requested action as a session's own permission card does: the permission
180
+ kind, the exact command, paths or patterns (the session's live request when the console has it), who asked, why it
181
+ is outside the grant and the stated purpose. A Concierge proposal shows its verb ("Send a prompt as you", "Stop the
182
+ session", …), the target session, the exact prompt or form answer, and the Concierge's why; its question heads the
183
+ card without repeating the body. A plain ask about a session that waits on one permission shows that request.
184
+ Long commands and prompts wrap and are cut with **Show all** (a session's permission card too). All of it is
185
+ rendered as plain text, from the user's own console data: the Concierge's board and update lines still carry no
186
+ blocker summaries or Decision text.
187
+ - **The card says where its detail comes from** (FX-05): "Live request" (the session's pending request), "The hub's
188
+ record of the request" (new structured fields on Decisions: `action.request {action, resources, save?}` and
189
+ `action.reason` on escalations, `action.request` on deny proposals; bounded to 20 × 2,000 characters), or "From
190
+ the request text" for older Decisions, now parsed without splitting a command at ", " or at a ". Outside its
191
+ grant: " inside it. Bidi and invisible characters in commands, paths and prompts show as markers ("⟨U+202E⟩"),
192
+ also on a session's permission card (tag characters U+E0000–E007F, separators, fillers too; a valid subdivision
193
+ flag and ZWJ emoji stay intact); text is cut by grapheme; **Show all** buttons name their field and control the
194
+ text they open. What the hub did not store is recorded with the request (`resourcesTotal`, `savesTotal`, `cut`
195
+ [{field, index, length}], `truncated`) and shown on the card ("… cut at 2,000 characters (of 3,017)", "+10 more
196
+ items not stored", "The hub received this request already shortened"), so a shortened command never reads as
197
+ complete. The structured request and reason are for the user's console only: the Concierge's Decision reads
198
+ (`/api/agent/concierge/decisions`, `concierge_decision`) return the records exactly as before.
199
+ - **Tap-tier Decisions in the Concierge panel** (FX-05b): the panel lists the Concierge's tap-tier Decisions (shell,
200
+ outside folders, tools, prompt/answer proposals) with the same detail, answered by a tap on the card. They are
201
+ never armed and never answered by voice or text; the button's badge and the panel's list now agree.
202
+ - **Fix: Decisions and the armed card no longer vanish at random** (quirk pgd96twg): the open list and the armed
203
+ Decision reloaded on every stream open replaced what the stream had said while the reload was out. A reload's
204
+ snapshot now merges the Decision events that came meanwhile (and an older reload never overrides a newer one),
205
+ and an arm event that came while `GET …/arm` was out wins over its answer. A load out for over 30 s is forgotten
206
+ (no events buffered forever), and an older `GET …/arm` never overrides a newer one.
207
+
208
+ - **Sessions by name, never a raw ID** (FX-12): Decision cards (their heading, "from", Requested by and Session), a
209
+ session's permission card (the recommender, who answered), hub message rows, Settings → Agents & access Activity
210
+ and Settings → Concierge's watch and mute lists name a session by its @name (the hub directory), else its title,
211
+ else its project folder and a short ID ("hub · ses_e6256f…"); the full ID is in the tooltip. An escalation's
212
+ heading is built from the hub's structured request ("API refactor asks to approve shell"; the command and reason
213
+ are in the details), other hub-written questions have their `ses_…` IDs named. Activity says "a permission
214
+ request", "a form" and "grant", with the IDs on hover. Display only: the data, the hub and what the Concierge
215
+ model sees are unchanged.
216
+
217
+ ## 0.8.0
218
+
219
+ Workspaces, supervision, split panes and side chats, shared agent profiles, persistent AI-powered artifacts, teams,
220
+ and the Watchtower. All new state is additive. Cluster hubs need `hub-server migrate` (schema v2) before the new
221
+ pods start. See [docs/operations.md](docs/operations.md) for the rollout, rollback, backup and front-proxy notes.
222
+
223
+ ### Fork and revert a message
224
+ - Hovering (or focusing) one of your messages in the console shows **Copy · Fork · Revert**, like the OpenCode app;
225
+ on a phone, long-press it or tap its ⋯ button. **Fork** opens a new session with the conversation before the
226
+ message and the message in its composer. **Revert** hides the message and everything after it, restores the files
227
+ the session changed since then (confirming first, with the list), and puts the message back in the composer; a
228
+ banner offers **Restore**; sending the next message makes it permanent. Works across devices and in panes.
229
+ - New console routes (user-only, audited): `POST /api/sessions/:id/fork`, `GET …/revert/preview`, `POST …/revert`,
230
+ `POST …/revert/restore`, `POST …/revert/commit`. Spoke features `fork` and `revert` (OpenCode processes that reach
231
+ their own server) with ops `fork`, `revertPreview`, `revert`, `revertRestore`, `revertCommit`; a staged revert is
232
+ reported as `SessionInfo.revert` (additive: older hubs ignore it). Other harnesses show the actions disabled
233
+ ("not supported yet"). Contract: [docs/session-view.md](docs/session-view.md#fork-and-revert-a-message).
234
+
235
+ ### Workspaces and supervision
236
+ - **Workspaces** ([docs/workspaces.md](docs/workspaces.md)): durable, cross-machine coordination units with host repo
237
+ bindings, members (`master`/`worker`/`observer`, generations), explicit assignments and a supervision graph
238
+ (acyclic, ≤ 6 levels) kept apart from native `parentID`. Membership and supervision grant nothing. New routes under
239
+ `/api/agent/workspaces…`, SDK `hub.workspaces`, 15 `workspace_*` MCP tools, and compact `workspace.*` events.
240
+ - **Drag to supervise**: the sidebar's **Workspaces** tree shows each workspace's members under their supervisors.
241
+ Drag (or use Alt+arrows or the Move menu) to supervise, reorder or detach. "Spawned by" and "supervised by" are
242
+ shown separately.
243
+ - **Situation board** (`GET …/board`): one bounded, text-free snapshot of members, presence, blockers, assignments,
244
+ Decisions and artifacts, with `stale`/`omitted`/`failed` sections instead of guesses.
245
+ - **Supervisor digests and checkpoints**: masters and supervisors get batched, at-least-once digests as hub messages
246
+ from `app:workspace-coordinator`. `POST …/checkpoints` acknowledges them durably (flushed before the answer).
247
+ - **Quieter digests**: routine updates (including snapshots) are delivered without waking an idle supervisor (2 min
248
+ quiet, 15 min max) and repeats of already-known states are dropped; assignment closures wake without interrupting
249
+ (30 s quiet, 5 min max); blockers, errors and urgent snapshots still wake and interrupt. A cleared error is still
250
+ reported, followed by the recovery. A member's session that disappears and comes back within one batch (a spoke
251
+ restart or reconnect) is not reported.
252
+ - **Master replacement** (`POST …/master {sessionID | launchID}`): explicit, fenced, with the next generation. Supervised
253
+ members move to the new master. No grants move.
254
+
255
+ ### Panes and side chats
256
+ - **Split panes**: **Open beside** shows up to 4 sessions or artifacts side by side, stacked or in a grid. The focused
257
+ pane owns the composer and Voice. Layouts are per device. On phones the panes are tabs. `/api/events` takes
258
+ `sessions=a,b` for the visible panes.
259
+ - **Drag into panes**: drag a session (sidebar, Workspaces tree, Watchtower detail), an artifact (library) or a pane
260
+ (header grip) onto a pane: edges split side by side or stacked, the centre replaces, the layout reflows to a grid
261
+ when needed, and a fifth pane is refused with a notice. **Open in pane → left/right/top/bottom** in the session
262
+ menu is the keyboard equivalent. The layout switch is now a compact segmented control with icons.
263
+ - **Side chats**: **Start side chat…** copies a bounded snapshot of a session into a new session on the workspace's
264
+ host binding (any machine), in a pane beside it, with **Refresh** and a confirmed **Send to main**. Neither
265
+ interrupts a running turn.
266
+
267
+ ### Agent profiles
268
+ - **Shared, versioned profiles** ([docs/profiles.md](docs/profiles.md)): instructions, skills and commands (with
269
+ `{{param}}`, `{{arguments}}` and spoke-side `{{secret:NAME}}`), immutable versions, and import from
270
+ `AGENTS.md`/`CLAUDE.md`, `skills/*/SKILL.md` and `commands/*.md`. You pin an exact version to a session at runtime
271
+ (the profile chip), to a launch (`agentProfile`) or to a team role. The **Agent profiles** page is the library.
272
+ **Ctrl+Alt+P** inserts a rendered command into the composer.
273
+ - **Per-harness application** through the new spoke feature `profileApply`, with honest acknowledgments (`applied`,
274
+ `partial`, `restart_required`, `unsupported`, `rejected`, `pending`): OpenCode (system prompt + `hub_profile`
275
+ tool), Claude Code (hook context, `set_model` in launched sessions), Codex (developer message + skill files);
276
+ presets are always `unsupported`. Local instructions win. No global harness config is written.
277
+ - **Profile metadata and usage**: optional `metadata` and `projects` on a profile, and **Used by**
278
+ (`GET /api/agent/profiles/:id/usage`, SDK `hub.profiles.usage()`, MCP `profile_usage`): pinned sessions with
279
+ requested vs running version and an explicit Upgrade, team roles, launches and workspaces, each shown only under the
280
+ caller's existing read rules (`hidden` names what was left out). The library rows show usage chips, with filters and
281
+ sort.
282
+
283
+ ### Artifacts and AI
284
+ - **Persistent artifacts** ([docs/artifacts.md](docs/artifacts.md)): revisions of content-addressed blobs (files
285
+ standalone, chunked Postgres in a cluster), `private` or `workspace` visibility, archive and delete, staged uploads up
286
+ to 25 MiB, and downloads always served as attachments. New routes, SDK `hub.artifacts`, and `artifact_*` MCP tools.
287
+ - **Viewers and dashboards**: markdown, code, images, tables, charts, and declarative dashboards (schema v1, bindings
288
+ to other artifacts only, no URLs or scripts), plus a text editor with conflict-safe drafts.
289
+ - **AI jobs**: ask, propose an edit or run a declared action. The job runs on a handler you choose (a new launch or an
290
+ existing session), always with the requester's authority. Proposals apply only when you **Accept**. Jobs survive hub
291
+ restarts, and each job times out after 2 h.
292
+ - **Viewer redesign**: a header bar with kind icon, version picker (**Back to latest**), Copy, Download, Edit, Open in
293
+ pane and a details popover; content in a framed card (code with file/language header, line numbers, wrap; markdown
294
+ at reading width; dashboards and apps full-bleed); inline editing; and AI as a chat-style side panel (thread,
295
+ composer, Ask / Edit / Action or slash commands, compact handler picker that never preselects), resizable, a
296
+ bottom sheet on phones.
297
+
298
+ ### Isolated artifact host
299
+ - Interactive `html`/`app` artifacts run only at `/artifact-host/v1/<capability>/…`: an opaque-origin sandbox with a
300
+ strict CSP, short-lived, viewer-bound and revocable capabilities, and a bridge (`resize`, `askAI`). Every `askAI`
301
+ becomes a consent card in the artifact's AI panel thread ([docs/artifact-host.md](docs/artifact-host.md)).
302
+
303
+ ### Teams
304
+ - **Teams** ([docs/teams.md](docs/teams.md)): versioned definitions with roles, one entry role, `open` or `restricted`
305
+ communication paths (`one-way`/`two-way`, `msg`/`ask`), and a draft → publish flow. A session can belong to many
306
+ teams. The hub **enforces** the policy on delivery (`403 forbidden` with `extra.policy`). Team members cannot contact
307
+ sessions outside their teams unless a role has `externalContact`. Sessions in no team are unchanged.
308
+ - **Staffing** with existing or newly launched sessions under your grants (`403 needs_user` lists what is missing).
309
+ **Intake** gives work to the team's entry role and tracks it as an assignment ([docs/teams-intake.md](docs/teams-intake.md)).
310
+ - **Team addresses** `@@role#team` and `@@role.project#team` (quote `#` in shells). `/who`, `status` and `show` are
311
+ team-aware for team members.
312
+ - Malformed addresses answer `Not an address: use ses_…, @name, @@role or @@role#team` with a syntax-only hint (for
313
+ `@engineer#support`: `@@engineer#support` or `@engineer`); the hint never reveals whether a name exists.
314
+ - Entry-role sessions report and close their intake from the harness: OpenCode `hub` tool verbs `intake` and
315
+ `intake_update`, MCP `team_intake_update` (`expectedRevision` now optional). The intake message says how.
316
+ - **Teams page** (`/teams`, in the primary navigation): team cards (mode, entry role, seats, vacant roles, open work),
317
+ **New team** in a dialog, and per team **Members** (one block per role, **+ Add member** with a searchable session
318
+ picker that shows other memberships, **Launch new …**), **Work** (Submit a task, live intake status with links to
319
+ the entry session and its assignment), **Communication** and **Details** (draft → Review and publish, history),
320
+ Pause/Archive and **Edit in Watchtower**. The Watchtower's team panel uses the same components and links back.
321
+ - **Team isolation** (owner setting, default off): Settings → Teams, or `GET/PUT /api/agent/team-settings`
322
+ `{teamIsolation, expectedRevision}` (users only; SDK `hub.teams.settings()` / `setSettings()`). On: team members
323
+ see only their teams' sessions.
324
+
325
+ ### Watchtower
326
+ - Renamed from **Bird's-eye view** (new lookout icon). `/birds-eye` links redirect to `/watchtower` with their
327
+ filters, and the browser's saved view and presets (`birdsEye.*`) move to `watchtower.*` once.
328
+ - A full-screen page ([docs/watchtower.md](docs/watchtower.md)) of machines, sessions, you, your apps and teams, with
329
+ live message flow: 3D (lazy `3d-force-graph`), a deterministic 2D canvas, or a list (also used for reduced motion).
330
+ It has URL-state filters and presets, team clusters, drawn vs actual paths and denied attempts, and a **team
331
+ editor** to assemble, staff, publish and submit work. New read API `GET /api/agent/graph` (+ `/edges/:id`,
332
+ `/nodes/:id`).
333
+ - Graph first: the controls are a floating **dock** at the bottom centre (view, Filters with an active count, Legend,
334
+ Teams, Hub details, Reset view, Pause motion, Search) that fades when idle. **Filters** is closed by default and
335
+ shows only the filters you add; active filters are removable chips over the canvas. The **Legend** has a visible
336
+ close and Escape closes it. On phones, filters, legend, details and the team editor are bottom sheets.
337
+ - A look in the style of Brain Graph (MIT, credited in the docs): glowing points in coloured group regions, fibers that
338
+ brighten towards the receiver, depth of field, labels on the most connected nodes, and a stable layout kept per
339
+ device (`watchtower.layout`). Flow pulses fade with message age and stop once nothing on a link is working or
340
+ pending. Rendering is on demand (none while still or hidden), and the 3D view draws in about 25 batched calls.
341
+ - Staffed seats are tied to their role by a tether and vacant roles are drawn hollow; the legend uses the same glyphs;
342
+ **More → Re-layout** clears the saved layout; the empty state sits below the hub core with **Connect a machine**.
343
+
344
+ ### Navigation
345
+ - A primary navigation above the session groups: **All sessions**, **Artifacts** (a library across workspaces),
346
+ **Watchtower** and **Agent profiles**.
347
+ - **Settings sections**: General, Notifications, Voice & speech, Machines & devices, Agents & access, Teams and Admin,
348
+ each its own page `/settings/<section>` (deep links, back/forward; old `/settings#speech`-style links still work).
349
+ A section sidebar on desktop, a section list with drill-in on phones, and a **Search settings** box that jumps to a
350
+ setting and highlights it. Notification kinds are collapsed by group with an on/off summary; the device's push
351
+ status is one line with **Details**.
352
+
353
+ ### Security hardening
354
+ - **Bounded request bodies** everywhere: agent JSON 1 MiB, profiles 4 MiB, console JSON 4 MiB, `/mcp` 1 MiB, staged
355
+ blobs 25 MiB, all read by a rate-budgeted streaming reader. Oversized bodies are refused with `Connection: close`.
356
+ **Set a front-proxy request-body limit (26 MiB)**: the k8s ingress example now uses `proxy-body-size: 26m` (it was
357
+ `16m`).
358
+ - A session seated in a team no longer sees sessions seated only in *other* teams: they are left out of `who`,
359
+ `status`, `show`, `log`, `/watch` and `/events`, and their addresses answer `404` like unknown ones. Sessions in no
360
+ team stay visible unless the owner turns on `teamIsolation` (Settings → Teams). Users, apps and team-less sessions
361
+ keep the owner-wide view.
362
+ - New app-token scopes `workspaces:*`, `profiles:*`, `artifacts:*` and `teams:*`. **Existing tokens gain nothing**,
363
+ and session tokens are refused on workspace routes.
364
+ - **Plain http only by opt-in**: `hub login` (device code, `--token` or a piped token) and `hub`/`hubctl machine enroll`
365
+ refuse plain http except on loopback, or with `--allow-insecure-http` for a private-network IP address (never a
366
+ host name), with a warning every time; nothing remembers the flag, and later commands using a stored plain-http
367
+ login warn on stderr. A loopback hub's approval link on this machine's LAN address is shown; other origins are
368
+ replaced with a warning.
369
+
370
+ ### Fixes
371
+ - Counts pluralise correctly ("2 OpenCode processes", not "processs"); a team role's seat counter reads "1 of 2
372
+ required", "3 members · up to 16" or "full (16)" instead of "1 of 0–16".
373
+ - The artifact AI panel lists handler sessions by `@name` (searchable by name, title or ID) and names a handler picked
374
+ in the console by its `@name` instead of its session ID.
375
+ - The console's live stream opens at once when it resumes (the hub writes a first chunk immediately); before, it waited
376
+ for the 20 s heartbeat and **Reconnecting…** stayed up, across pages. Over plain http, Chrome still allows only 6
377
+ connections per host, so more than about 6 console tabs stall; https avoids this.
378
+ - The sidebar, Details rail and pane resize handles show their resize pointer again (a hidden character had broken
379
+ those CSS rules).
380
+ - `hub node` keeps OpenCode's background service warm (loopback only, every 30 s) so a freshly started service's
381
+ machine comes online without anyone opening a project (OpenCode has no server-start plugin hook).
382
+ - **Routine `wake: false` messages no longer cost an OpenCode session a turn** (routine digests, `--no-wake`
383
+ messages). OpenCode gives every queued inbox item its own model call once a turn ends, resume or not, so a
384
+ supervisor answered each routine digest in a turn of its own. The OpenCode spoke now admits every non-waking
385
+ message steered without resume, and refuses a routine one (a `wake: false` `msg` that is not urgent, to a top-level
386
+ session) while a turn runs: the hub keeps it `pending` and delivers it after the turn, so it reaches the model with
387
+ the next real turn, before its prompt (CLI and MCP say "pending (offline or busy; …)"). A waking message to the
388
+ same session sends the pending ones first (if the session is still busy, the waking one goes first, queued after
389
+ the turn). A flush stops at a session's first failed delivery, so a busy session costs one try per sessions frame.
390
+ Urgent
391
+ ones, asks, replies, primers and anything for a subagent still go into a running turn. Live check:
392
+ `HUB_IT_OPENCODE=1 bun test test/e2e-quiet-delivery.it.test.ts`.
393
+ - **Repeated blocks of one member no longer wake its supervisor each time**: blocked again for the same reason (same
394
+ blocker kind, or the same blocked/error state) within 15 min of a wake about it that the hub carried out, the
395
+ digest line is routine.
396
+ - **A run that fails at once shows as failed** (a model or provider that does not exist, auth, quota). OpenCode ends
397
+ such a run within ~50 ms of `session.created`, before the spoke's debounced sessions snapshot (500 ms), and the hub
398
+ keeps status only for sessions it was told about, so it dropped the failure and listed the session idle: no banner,
399
+ no "!" badge, not under Needs you, no `providerError` push (also in 0.7.3). The spoke now sends the snapshot first
400
+ when a run event is about a session the hub has not been told about yet (creating sessions alone still waits for the
401
+ debounce), and resends a failure once a snapshot lists its session if the session's details were still being
402
+ fetched when it failed. Live check: `HUB_IT_OPENCODE=1 bun test test/spoke-run-failure.it.test.ts`.
403
+ - **A session is never routed to another harness's spoke.** When no process owns a session and none of its
404
+ reporters is a service, the hub falls back to a background service on the reporter's machine; it took any service,
405
+ so an OpenCode session reported by a foreground `opencode serve` on a machine that also runs the Claude Code (or
406
+ Codex) service went to that service: "The claude-code spoke does not serve "timeline"" and `unsupported: summary`
407
+ (also in 0.7.3). The fallback now takes only a service of the session's own harness, else the reporter itself.
408
+
409
+ ### Runtime
410
+ - **Bun 1.4.4**, pinned: the image (`oven/bun:1.4.4-slim` by digest, both stages), CI and release workflows, and
411
+ `engines.bun` (`>=1.4.4`). Bun 1.3.x's `Bun.SQL` auto-pipelining could stall a pooled Postgres connection under
412
+ concurrent writes (oven-sh/bun#33985; fixed in Bun 1.4.0 by oven-sh/bun#33627): a cluster pod's state queries then
413
+ never answered (the supervision IT's "many concurrent internal moves" hung or timed out in 3 of 15 runs on 1.3.5,
414
+ 0 of 40 on 1.4.4).
415
+ - Cluster: the Postgres pools set `statement_timeout` (30 s by default, `HUB_DATABASE_STATEMENT_TIMEOUT_MS`, `0`
416
+ off), so a query stuck on the database fails with an error instead of holding its caller and connection.
417
+ - Bun 1.4 enforces the server's request ceiling on chunked bodies too; the ceiling now leaves 16 MiB above the
418
+ largest route bound, so an over-limit upload still gets the route's own `400`/`413` with `Connection: close`. Bun
419
+ 1.4 then closes the socket instead of reading and discarding whatever the client keeps sending.
420
+ - The image build copies the protocol and SDK sources into the console stage, which imports them (without them
421
+ `docker build` failed in the console's `vite build`).
422
+
423
+ ### Behavior notes
424
+ - New agent error code `conflict` (409, with `currentRevision`) for stale `expectedRevision`s and reused
425
+ `Idempotency-Key`s. The SDK `ErrorCode` includes it, and the CLI exits 3.
426
+ - The server's request ceiling rises from about 10 MiB to about 41 MiB (25 MiB blobs plus drain headroom). A JSON body
427
+ that was accepted before can now be refused as too large (by its route's bound).
428
+ - SDK `parseAddress` refuses `#` in a role's project scope (`@@role#team` is only ever team-resolved).
429
+ - Send to main and AI jobs handled by a session use normal delivery: they never interrupt a running turn. The first
430
+ message to an idle session right after the hub's primer may be delivered as steer.
431
+ - Cluster: schema v2 (`artifact-blobs`). Run `hub-server migrate` before rolling pods, and don't create restricted
432
+ teams while old and new pods are mixed.
4
433
 
5
434
  ## 0.7.4
6
435
 
package/Dockerfile CHANGED
@@ -7,6 +7,8 @@
7
7
  # Legacy VOICE_* variables still work, except that the HUB_HOST/HUB_PORT/HUB_STATE_DIR defaults below
8
8
  # win over VOICE_HOST/VOICE_PORT/VOICE_STATE_DIR: rename those overrides to HUB_*.
9
9
  # The console (web/, Solid + Vite) is built in its own stage; the image ships only web/dist.
10
+ # Bun is pinned (tag and multi-arch index digest) to 1.4.4: it has the Bun.SQL pipelining fix (oven-sh/bun#33627,
11
+ # in since 1.4.0; 1.3.x stalls pooled Postgres connections in cluster mode). Bump both stages together.
10
12
  FROM node:22-slim AS web
11
13
  WORKDIR /build
12
14
  COPY package.json package-lock.json ./
@@ -24,9 +26,12 @@ COPY packages/preset/package.json ./packages/preset/
24
26
  COPY packages/codex/package.json ./packages/codex/
25
27
  RUN npm ci --no-audit --no-fund --ignore-scripts
26
28
  COPY web ./web
29
+ # The console imports the protocol and SDK sources (web/src/… ../packages/{protocol,sdk}/src).
30
+ COPY packages/protocol/src ./packages/protocol/src
31
+ COPY packages/sdk/src ./packages/sdk/src
27
32
  RUN npx vite build --config web/vite.config.ts
28
33
 
29
- FROM oven/bun:1.3-slim AS deps
34
+ FROM oven/bun:1.4.4-slim@sha256:1846adf22496bd711036aae8be147e57bdebbaea2e5264e3e0b5af1db187d9a4 AS deps
30
35
  WORKDIR /build
31
36
  COPY package.json package-lock.json ./
32
37
  COPY packages/protocol/package.json ./packages/protocol/
@@ -47,7 +52,7 @@ COPY docker/hub-deps.ts ./
47
52
  # Keep only what the hub loads at runtime (the @opencode/* packages are for the plugin).
48
53
  RUN bun hub-deps.ts /deps web-push
49
54
 
50
- FROM oven/bun:1.3-slim
55
+ FROM oven/bun:1.4.4-slim@sha256:1846adf22496bd711036aae8be147e57bdebbaea2e5264e3e0b5af1db187d9a4
51
56
  LABEL org.opencontainers.image.title="hub" \
52
57
  org.opencontainers.image.description="Hub: a central hub for AI agent harnesses (OpenCode first): remote console, pairing, push and optional Voice" \
53
58
  org.opencontainers.image.licenses="MIT" \