akanjs 3.1.0-rc.8 → 3.1.0

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 (323) hide show
  1. package/CHANGELOG.md +547 -0
  2. package/README.ko.md +2 -2
  3. package/README.md +2 -2
  4. package/client/cookie.ts +5 -0
  5. package/client/csrLocation.ts +17 -72
  6. package/client/csrTypes.ts +19 -4
  7. package/client/href.ts +8 -0
  8. package/client/makePageProto.tsx +3 -2
  9. package/client/route/PageDefinition.ts +103 -10
  10. package/client/route/RouteDefinition.ts +44 -3
  11. package/client/route/RouteGuards.ts +64 -0
  12. package/client/route/index.ts +1 -0
  13. package/client/route/resolveRouteModule.ts +8 -0
  14. package/client/route/routeArgs.ts +9 -0
  15. package/client/router.ts +51 -2
  16. package/client/translator.ts +16 -0
  17. package/client/viewportMeta.ts +23 -8
  18. package/common/Logger.ts +6 -4
  19. package/common/authCookie.ts +13 -0
  20. package/common/authToken.ts +1 -12
  21. package/common/codeAgentWire.ts +8 -1
  22. package/common/index.ts +2 -8
  23. package/common/routeConvention.ts +2 -0
  24. package/constant/inputParser.ts +1 -1
  25. package/dictionary/base.dictionary.ts +11 -0
  26. package/document/database.ts +3 -0
  27. package/document/documentQuery.ts +21 -0
  28. package/document/into.ts +2 -0
  29. package/document/liveRowScope.ts +4 -8
  30. package/document/noDocumentError.ts +6 -1
  31. package/document/queryEvaluator.ts +5 -8
  32. package/fetch/appScope.ts +54 -0
  33. package/fetch/client/fetchClient.ts +46 -29
  34. package/fetch/client/httpClient.ts +38 -10
  35. package/fetch/client/sliceInitHandle.ts +2 -3
  36. package/fetch/client/wsClient.ts +3 -1
  37. package/fetch/fetchType/endpointFetch.type.ts +11 -9
  38. package/fetch/index.ts +1 -0
  39. package/index.ts +2 -0
  40. package/package.json +32 -22
  41. package/server/akanApp.ts +12 -3
  42. package/server/akanServer.ts +10 -5
  43. package/server/index.ts +1 -0
  44. package/server/mcp/McpApps.ts +44 -50
  45. package/server/mcp/McpDispatcher.ts +3 -3
  46. package/server/mcp/McpPages.ts +93 -19
  47. package/server/mcp/McpRouter.ts +14 -11
  48. package/server/memoryLimit.ts +4 -0
  49. package/server/migration/docPath.ts +121 -0
  50. package/server/migration/index.ts +15 -0
  51. package/server/migration/migrationChangeLog.ts +86 -0
  52. package/server/migration/migrationChanges.ts +70 -0
  53. package/server/migration/migrationCommand.ts +179 -0
  54. package/server/migration/migrationContext.ts +53 -0
  55. package/server/migration/migrationDefinition.ts +87 -0
  56. package/server/migration/migrationError.ts +45 -0
  57. package/server/migration/migrationLedger.ts +172 -0
  58. package/server/migration/migrationLock.ts +103 -0
  59. package/server/migration/migrationParams.ts +122 -0
  60. package/server/migration/migrationPulse.ts +39 -0
  61. package/server/migration/migrationRunner.ts +525 -0
  62. package/server/migration/migrationScope.ts +93 -0
  63. package/server/migration/migrationTable.ts +258 -0
  64. package/server/migration/migrations.ts +57 -0
  65. package/server/ops/migrateJobs.ts +145 -0
  66. package/server/ops/opsCommand.ts +30 -1
  67. package/server/ops/opsRoute.ts +39 -7
  68. package/server/ops/opsToken.ts +7 -2
  69. package/server/resolver/CascadeRunner.ts +15 -2
  70. package/server/resolver/database.resolver.ts +4 -0
  71. package/server/resolver/service.resolver.ts +3 -0
  72. package/server/resolver/signal.resolver.ts +41 -31
  73. package/server/routeElementComposer.tsx +33 -6
  74. package/server/routeState.ts +1 -0
  75. package/server/routeTreeBuilder.ts +5 -0
  76. package/server/rscClient.tsx +67 -22
  77. package/server/rscClientFetch.ts +8 -3
  78. package/server/rscNavigationState.ts +2 -2
  79. package/server/rscPagePrompts.ts +66 -16
  80. package/server/rscScrollRestoration.ts +21 -8
  81. package/server/rscWorker.tsx +79 -32
  82. package/server/rscWorkerHost.ts +12 -1
  83. package/server/tunnel/TunnelAgent.ts +2 -1
  84. package/server/webRouter.ts +33 -27
  85. package/service/agent.service.ts +21 -1
  86. package/service/endpointCacheKeys.ts +35 -0
  87. package/service/index.ts +1 -0
  88. package/service/liveSyncHub.ts +2 -1
  89. package/service/predefinedAdaptor/anthropicLlm.ts +37 -17
  90. package/service/predefinedAdaptor/database.adaptor.ts +98 -25
  91. package/service/predefinedAdaptor/documentTransfer.ts +50 -0
  92. package/service/predefinedAdaptor/endpointCacheControl.adaptor.ts +19 -0
  93. package/service/predefinedAdaptor/index.ts +1 -0
  94. package/service/predefinedAdaptor/llm.adaptor.ts +7 -0
  95. package/service/predefinedAdaptor/openaiDialect.ts +53 -17
  96. package/service/predefinedAdaptor/openaiLlm.ts +1 -1
  97. package/service/predefinedAdaptor/solidQueue.adaptor.ts +30 -9
  98. package/service/predefinedAdaptor/sql/MigrationTables.ts +90 -0
  99. package/service/predefinedAdaptor/sql/QueryCompiler.ts +16 -14
  100. package/service/predefinedAdaptor/sql/RawDocumentTable.ts +200 -0
  101. package/service/predefinedAdaptor/sql/SqlDocumentStore.ts +218 -44
  102. package/service/predefinedAdaptor/sql/SqliteInsightSession.ts +4 -1
  103. package/service/predefinedAdaptor/sql/dialect/postgres.ts +13 -3
  104. package/service/predefinedAdaptor/sql/dialect/sqlite.ts +13 -2
  105. package/service/predefinedAdaptor/sql/driver/bunSqlite.ts +4 -5
  106. package/service/predefinedAdaptor/sql/types.ts +22 -1
  107. package/service/predefinedAdaptor/sql/values.ts +19 -0
  108. package/service/predefinedAdaptor/storage.adaptor.ts +19 -0
  109. package/service/serviceModule.ts +3 -0
  110. package/service/types.ts +1 -0
  111. package/signal/TaggedJson.ts +26 -4
  112. package/signal/endpointCache.ts +16 -6
  113. package/signal/exception.ts +4 -1
  114. package/signal/mcp/mcpProtocol.ts +7 -0
  115. package/signal/mcp/pagePrompt.ts +18 -1
  116. package/signal/signalContext.ts +7 -10
  117. package/store/action.ts +181 -73
  118. package/store/agent/ScreenImage.ts +166 -0
  119. package/store/agent/StoreSurfaceSource.ts +41 -2
  120. package/store/agent/index.ts +2 -1
  121. package/store/databaseStateNames.ts +1 -0
  122. package/store/formUploadOf.ts +41 -0
  123. package/store/index.ts +1 -0
  124. package/store/liveInsert.ts +5 -1
  125. package/store/sliceKeys.ts +1 -0
  126. package/store/state.ts +15 -3
  127. package/store/stateBuilder.ts +7 -1
  128. package/store/storeInstance.ts +22 -4
  129. package/types/client/csrLocation.d.ts +3 -7
  130. package/types/client/csrTypes.d.ts +22 -3
  131. package/types/client/href.d.ts +3 -0
  132. package/types/client/route/PageDefinition.d.ts +21 -4
  133. package/types/client/route/RouteDefinition.d.ts +4 -0
  134. package/types/client/route/RouteGuards.d.ts +15 -0
  135. package/types/client/route/index.d.ts +1 -0
  136. package/types/client/route/resolveRouteModule.d.ts +2 -0
  137. package/types/client/route/routeArgs.d.ts +5 -0
  138. package/types/client/route/routeBuilders.d.ts +1 -1
  139. package/types/client/router.d.ts +9 -1
  140. package/types/client/translator.d.ts +2 -0
  141. package/types/common/authCookie.d.ts +5 -0
  142. package/types/common/authToken.d.ts +0 -5
  143. package/types/common/codeAgentWire.d.ts +7 -0
  144. package/types/common/index.d.ts +2 -1
  145. package/types/dictionary/base.dictionary.d.ts +1 -1
  146. package/types/dictionary/dictionary.d.ts +8 -8
  147. package/types/document/database.d.ts +3 -0
  148. package/types/document/documentQuery.d.ts +1 -0
  149. package/types/document/into.d.ts +2 -0
  150. package/types/document/noDocumentError.d.ts +6 -1
  151. package/types/fetch/appScope.d.ts +18 -0
  152. package/types/fetch/client/fetchClient.d.ts +1 -1
  153. package/types/fetch/fetchType/endpointFetch.type.d.ts +1 -1
  154. package/types/fetch/index.d.ts +1 -0
  155. package/types/index.d.ts +5 -0
  156. package/types/server/akanServer.d.ts +10 -5
  157. package/types/server/index.d.ts +1 -0
  158. package/types/server/mcp/McpApps.d.ts +14 -8
  159. package/types/server/mcp/McpPages.d.ts +6 -3
  160. package/types/server/migration/docPath.d.ts +20 -0
  161. package/types/server/migration/index.d.ts +15 -0
  162. package/types/server/migration/migrationChangeLog.d.ts +27 -0
  163. package/types/server/migration/migrationChanges.d.ts +27 -0
  164. package/types/server/migration/migrationCommand.d.ts +33 -0
  165. package/types/server/migration/migrationContext.d.ts +28 -0
  166. package/types/server/migration/migrationDefinition.d.ts +42 -0
  167. package/types/server/migration/migrationError.d.ts +15 -0
  168. package/types/server/migration/migrationLedger.d.ts +51 -0
  169. package/types/server/migration/migrationLock.d.ts +34 -0
  170. package/types/server/migration/migrationParams.d.ts +41 -0
  171. package/types/server/migration/migrationPulse.d.ts +15 -0
  172. package/types/server/migration/migrationRunner.d.ts +93 -0
  173. package/types/server/migration/migrationScope.d.ts +42 -0
  174. package/types/server/migration/migrationTable.d.ts +52 -0
  175. package/types/server/migration/migrations.d.ts +12 -0
  176. package/types/server/migration.d.ts +1 -0
  177. package/types/server/ops/migrateJobs.d.ts +36 -0
  178. package/types/server/ops/opsCommand.d.ts +10 -1
  179. package/types/server/ops/opsRoute.d.ts +4 -1
  180. package/types/server/ops/opsToken.d.ts +3 -0
  181. package/types/server/routeElementComposer.d.ts +10 -1
  182. package/types/server/routeState.d.ts +1 -0
  183. package/types/server/rscClientFetch.d.ts +2 -0
  184. package/types/server/rscNavigationState.d.ts +1 -1
  185. package/types/server/rscPagePrompts.d.ts +3 -1
  186. package/types/server/rscScrollRestoration.d.ts +2 -0
  187. package/types/server/rscWorkerHost.d.ts +2 -1
  188. package/types/server/webRouter.d.ts +3 -0
  189. package/types/service/agent.service.d.ts +2 -0
  190. package/types/service/endpointCacheKeys.d.ts +8 -0
  191. package/types/service/index.d.ts +1 -0
  192. package/types/service/predefinedAdaptor/anthropicLlm.d.ts +9 -4
  193. package/types/service/predefinedAdaptor/database.adaptor.d.ts +21 -2
  194. package/types/service/predefinedAdaptor/documentTransfer.d.ts +1 -0
  195. package/types/service/predefinedAdaptor/endpointCacheControl.adaptor.d.ts +9 -0
  196. package/types/service/predefinedAdaptor/index.d.ts +1 -0
  197. package/types/service/predefinedAdaptor/llm.adaptor.d.ts +7 -0
  198. package/types/service/predefinedAdaptor/openaiDialect.d.ts +20 -5
  199. package/types/service/predefinedAdaptor/solidQueue.adaptor.d.ts +1 -0
  200. package/types/service/predefinedAdaptor/sql/MigrationTables.d.ts +17 -0
  201. package/types/service/predefinedAdaptor/sql/QueryCompiler.d.ts +0 -1
  202. package/types/service/predefinedAdaptor/sql/RawDocumentTable.d.ts +38 -0
  203. package/types/service/predefinedAdaptor/sql/SqlDocumentStore.d.ts +1 -0
  204. package/types/service/predefinedAdaptor/sql/dialect/postgres.d.ts +2 -1
  205. package/types/service/predefinedAdaptor/sql/dialect/sqlite.d.ts +3 -1
  206. package/types/service/predefinedAdaptor/sql/driver/bunSqlite.d.ts +2 -2
  207. package/types/service/predefinedAdaptor/sql/types.d.ts +26 -0
  208. package/types/service/predefinedAdaptor/sql/values.d.ts +1 -0
  209. package/types/service/predefinedAdaptor/storage.adaptor.d.ts +8 -0
  210. package/types/service/types.d.ts +1 -0
  211. package/types/signal/exception.d.ts +4 -1
  212. package/types/signal/mcp/mcpProtocol.d.ts +6 -0
  213. package/types/signal/mcp/pagePrompt.d.ts +16 -0
  214. package/types/signal/signalContext.d.ts +0 -2
  215. package/types/store/action.d.ts +2 -0
  216. package/types/store/agent/ScreenImage.d.ts +27 -0
  217. package/types/store/agent/StoreSurfaceSource.d.ts +1 -1
  218. package/types/store/agent/index.d.ts +2 -1
  219. package/types/store/databaseStateNames.d.ts +1 -0
  220. package/types/store/formUploadOf.d.ts +9 -0
  221. package/types/store/index.d.ts +1 -0
  222. package/types/store/liveInsert.d.ts +3 -1
  223. package/types/store/sliceKeys.d.ts +1 -0
  224. package/types/store/state.d.ts +10 -4
  225. package/types/store/stateBuilder.d.ts +1 -0
  226. package/types/ui/Agent/Composer.d.ts +2 -0
  227. package/types/ui/Agent/Menu.d.ts +2 -0
  228. package/types/ui/CsrImage.d.ts +2 -0
  229. package/types/ui/Image.d.ts +3 -1
  230. package/types/ui/Layout/FrameSlotPortal.d.ts +1 -1
  231. package/types/ui/Load/sliceClaim.d.ts +28 -0
  232. package/types/ui/Model/Edit.d.ts +3 -2
  233. package/types/ui/Model/editSeed.d.ts +4 -0
  234. package/types/ui/System/App.d.ts +17 -0
  235. package/types/ui/System/index.d.ts +2 -0
  236. package/types/ui/recipe/badgeRecipe.d.ts +2 -2
  237. package/types/ui/recipe/buttonRecipe.d.ts +2 -2
  238. package/types/ui/sliceNamesOf.d.ts +2 -0
  239. package/types/vendor/@akanjs/native/packages/cli/src/lib/compat.d.ts +6 -0
  240. package/types/vendor/@akanjs/native/packages/cli/src/lib/toolchains.d.ts +2 -2
  241. package/types/vendor/use-agentic/Compaction.d.ts +3 -1
  242. package/types/vendor/use-agentic/Reference.d.ts +7 -2
  243. package/types/vendor/use-agentic/ToolOutput.d.ts +6 -1
  244. package/types/vendor/use-agentic/ToolReturn.d.ts +18 -0
  245. package/types/vendor/use-agentic/Transcript.d.ts +7 -0
  246. package/types/vendor/use-agentic/index.d.ts +1 -0
  247. package/types/vendor/use-agentic/types.d.ts +6 -0
  248. package/types/webkit/CsrGuards.d.ts +25 -0
  249. package/types/webkit/CsrRouteTable.d.ts +6 -0
  250. package/types/webkit/CsrStack.d.ts +7 -0
  251. package/types/webkit/McpAppBoot.d.ts +32 -0
  252. package/types/webkit/McpAppBridge.d.ts +10 -1
  253. package/types/webkit/McpAppFetchBridge.d.ts +3 -1
  254. package/types/webkit/McpAppView.d.ts +4 -1
  255. package/types/webkit/PageNotFound.d.ts +5 -0
  256. package/types/webkit/RenderLayer.d.ts +1 -3
  257. package/types/webkit/index.d.ts +1 -0
  258. package/types/webkit/useDebounce.d.ts +1 -1
  259. package/ui/Agent/Chat.tsx +1 -0
  260. package/ui/Agent/Composer.tsx +2 -0
  261. package/ui/Agent/Menu.tsx +5 -1
  262. package/ui/Agent/agentSessionOf.ts +15 -8
  263. package/ui/Agent/mentionDraft.ts +2 -3
  264. package/ui/Agent/useReferenceMenu.ts +1 -0
  265. package/ui/Agent/useSlashMenu.ts +1 -0
  266. package/ui/CsrImage.tsx +3 -1
  267. package/ui/Dialog/LegacyModal.tsx +7 -5
  268. package/ui/Dialog/Modal.tsx +7 -4
  269. package/ui/Field/Collection.tsx +2 -1
  270. package/ui/Image.tsx +4 -4
  271. package/ui/Layout/BottomInset.tsx +2 -11
  272. package/ui/Layout/FrameSlotPortal.tsx +1 -3
  273. package/ui/Layout/Navbar.tsx +7 -22
  274. package/ui/Layout/TopInset.tsx +3 -4
  275. package/ui/Load/Units.tsx +34 -12
  276. package/ui/Load/sliceClaim.ts +164 -0
  277. package/ui/Model/Edit.tsx +22 -3
  278. package/ui/Model/EditModal.tsx +22 -6
  279. package/ui/Model/LoadStore.tsx +1 -1
  280. package/ui/Model/NewWrapper_Client.tsx +1 -1
  281. package/ui/Model/ViewEditModal.tsx +9 -3
  282. package/ui/Model/editSeed.ts +28 -0
  283. package/ui/System/App.tsx +43 -0
  284. package/ui/System/CSR.tsx +1 -10
  285. package/ui/System/Client.tsx +3 -0
  286. package/ui/System/index.tsx +3 -0
  287. package/ui/Tab/Provider.tsx +6 -8
  288. package/ui/styles.css +10 -0
  289. package/vendor/@akanjs/native/native/ios/Sources/AkanNativeBridge.swift +3 -2
  290. package/vendor/@akanjs/native/packages/cli/src/lib/compat.ts +6 -2
  291. package/vendor/@akanjs/native/packages/cli/src/lib/native-plugins.ts +1 -1
  292. package/vendor/@akanjs/native/packages/cli/src/lib/toolchains.ts +1 -1
  293. package/vendor/@akanjs/native/packages/cli/src/lib/updates.ts +3 -3
  294. package/vendor/@akanjs/native/plugins/sqlite/src/desktop.ts +10 -5
  295. package/vendor/@akanjs/native/plugins/updates/src/desktop.ts +45 -15
  296. package/vendor/use-agentic/CHANGELOG.md +31 -0
  297. package/vendor/use-agentic/Compaction.ts +38 -7
  298. package/vendor/use-agentic/Reference.ts +14 -10
  299. package/vendor/use-agentic/ToolOutput.ts +25 -3
  300. package/vendor/use-agentic/ToolReturn.ts +32 -0
  301. package/vendor/use-agentic/ToolRunner.ts +15 -13
  302. package/vendor/use-agentic/Transcript.ts +38 -2
  303. package/vendor/use-agentic/WIRE.md +20 -0
  304. package/vendor/use-agentic/index.ts +1 -0
  305. package/vendor/use-agentic/types.ts +6 -0
  306. package/webkit/CsrGuards.ts +112 -0
  307. package/webkit/CsrRouteTable.ts +71 -16
  308. package/webkit/CsrStack.ts +47 -1
  309. package/webkit/McpAppAgent.ts +14 -2
  310. package/webkit/McpAppBoot.tsx +129 -0
  311. package/webkit/McpAppBridge.ts +30 -6
  312. package/webkit/McpAppFetchBridge.ts +8 -2
  313. package/webkit/McpAppView.ts +39 -28
  314. package/webkit/PageNotFound.tsx +24 -0
  315. package/webkit/RenderLayer.tsx +106 -30
  316. package/webkit/bootCsr.tsx +4 -43
  317. package/webkit/index.ts +1 -0
  318. package/webkit/useCsrValues.ts +66 -6
  319. package/webkit/useDebounce.ts +23 -14
  320. package/webkit/useEscapeKey.tsx +1 -1
  321. package/webkit/useLocation.ts +21 -19
  322. package/types/webkit/EmbeddedLayout.d.ts +0 -10
  323. package/webkit/EmbeddedLayout.tsx +0 -82
package/CHANGELOG.md CHANGED
@@ -1,5 +1,552 @@
1
1
  # akanjs
2
2
 
3
+ ## 3.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ce817b6: An agent's tool call runs only as the action the screen declared when the call arrived. After the user approves, and again right before it runs, the runner requires the same declaration under that name — the same description and parameters, and the same confirm message for the approved arguments — rather than the same registration, so a list row mounting while the user decides no longer cancels an approval, and a different declaration that took the name over is refused with "<name> changed on screen before it ran, so it did not run." `st.tool(name, { label })` (use-agentic's `ToolEntryBase.label`) names an action for a person, in their language, and is never sent to the model. `Agent.useScreenActions()` returns the screen's argument-free tools for a command palette, each with its `label`, leaving out the tools a person answers with a card.
8
+
9
+ `AgentActivity` takes `keepGuides` (use-agentic): a subtree that is off screen publishes no tools or state but keeps saying its guidance, and `AgenticSurface.gated(gate, guideGate)` names the gate guidance follows.
10
+
11
+ Upgrade note: the check covers calls that need no approval too. Such a call used to run whatever registration held its name by then; it now returns that refusal to the model when the declaration under the name changed after the call arrived.
12
+
13
+ - ed26089: The in-page agent can look at a picture on screen. A new built-in tool, `viewImage({ src })`, takes an image address that `readScreen({ images: true })` printed, redraws that `<img>` as a JPEG no longer than 1568px, and attaches it to the tool's result, so the model answers from the picture rather than its caption. It refuses an address no `<img>` on screen shows, and a `data:` one. A public picture the page may not read is sent by its address.
14
+
15
+ A tool result can now carry files in `ToolCallResult.attachments`, which have the same shape as a user's attachments. A tool of your own returns them with `ToolReturn.attach(result, [attachment])`, exported from `akanjs/store`. `AnthropicLlm` places them inside the `tool_result`. The OpenAI dialect sends them in a user message right after the turn's tool messages, and an MCP App hands a picture to the host's model as image content. Either way, they pass the same `accepts` check and image-type allow-list as a user's file, and one the model cannot read becomes a note in its place. They sit outside the 20,000-character result ceiling but meet the 4 MB per-file one. A file a tool returned before the user's latest message is sent as a note asking the model to call the tool again, and it is never written to stored history.
16
+
17
+ Upgrade note: `viewImage` joins the default built-ins, so `builtins={true}` and an omitted `builtins` now publish it. List the built-ins you want to leave it out.
18
+
19
+ - ce817b6: Helpers the framework used in several copies are public, each in one place:
20
+
21
+ - `parseSearchParams(search)` (`akanjs/common`): a query string as a record, a repeated key holding every value in order; a `__proto__` key stays a value. The CSR boot, the SSR client bridge, the CSR location and the RSC worker all read through it.
22
+ - `isMcpView()` and `router.postPathChange(change)` (`akanjs/client`): whether the page runs in an MCP host's view, and the `pathChange` message to the embedding frame, which an MCP view never posts.
23
+ - `ModalSurfaceProps` (`akanjs/ui`): the `presentation` and `placement` every modal surface takes.
24
+ - `TailwindBreakpoint` and `breakpointQuery(name, { below })` (`akanjs/client`), derived from `insetBreakpoints`, and `transitionTypes`, from which `TransitionType` is now derived.
25
+ - `webManifestHref(basePath)` and `getWebManifestBasePath(pathname, basePaths, headerBasePath)` (`akanjs/client`): where an SSR document links its root layout's `.manifest()`, and which basePath a `/manifest.webmanifest` request asks for.
26
+
27
+ - ce817b6: Security: a server-side call to another server no longer carries the visitor's session. Server code answering a visitor's request used to send the visitor's `Authorization` header or auth cookie, and their `x-real-ip`, with every call it made, so a `fetch.clone({ origin })` toward another origin handed that server the visitor's token for this one. Now only a call to this server forwards them; a clone toward another origin, any clone of one, and a call given another `{ origin }` send only the client's own `jwt`, the one `fetch.clone({ jwt })` was given. Upgrade note: a server that relied on the forwarded session now receives a signed-out call; give the clone the token it should send (`fetch.clone({ origin, jwt })`).
28
+ - 2c5b833: `Signal.Doc` and `Constant.Doc` render straight from a server page. Each member crosses the client boundary on its own, and `fetch` is optional everywhere in `Signal`: it defaults to the app's registered runtime, so `<Signal.Doc.Zone refName="product" />` needs no `"use client"` wrapper. `Signal.Doc.Explorer({ include?, exclude?, libs?, groupBy?, defaultRefName? })` lists every mounted signal in a sidebar and mounts each one on first open. The Signal docs' toolbar, modal and section labels are translated.
29
+
30
+ `Constant.Doc.Zone` / `Print` take `include`, `exclude` and `libs`, and narrowing any list narrows the rest: a left-out `scalars` becomes the scalars the shown models embed, a left-out `enums` the enums their fields use. Upgrade note: an empty list now means none — `models={[]}` used to show every model; leave the prop out for that. `getConstantSchemaDoc` is exported from `akanjs/ui`.
31
+
32
+ Every constant and signal records the app or lib that registered it (`ConstantRegistry.getOrigin(kind, key)`, `SerializedSignal.origin`), chained when an app extends a lib module (`["shared", "sceny"]`). The generated `lib/cnst.ts` and `lib/sig.ts` pass the scope name; run `akan sync` to regenerate them. `groupBy="lib"` puts one section, one diagram and one sidebar group per library, the app first, and a model shows "sceny (extends shared)".
33
+
34
+ The `Date` scalar's example value is a fixed instant, so a server-rendered response example no longer mismatches its hydration.
35
+
36
+ - ce817b6: An edge server (`AKAN_PUBLIC_OPERATION_MODE=edge`) starts with MCP off: it runs on premises for its own screens and machines, so `/mcp` is not served there unasked. Upgrade note: an edge deployment that serves MCP turns it back on with `AKAN_MCP=true` or `option.setMcp(true)`.
37
+ - ce817b6: A module's `serve(ref, { operationMode: ["edge"] }, …)` mounts only in the operation modes it lists, and `modules: ["@edge"]` / `AKAN_MODULES=@edge` names every module that lists edge; a selection now also mounts the targets of its modules' relation fields. `native.desktop.server` takes `{ profile, modules }`: `profile: "edge"` bundles `env/env.server.<env>.edge.ts` in place of the environment's own file, which no longer reaches the computers the app is installed on, and the build refuses a profile file whose secret-named keys hold a value; `akan build <app> --profile edge` does the same for an image. Machine tokens: `MachineToken.issue({ kind, id, version })` signs an Ed25519 JWT with `AKAN_MACHINE_PRIVATE_KEY` (`akan machine-keygen <file>` writes one), a server with that key or `AKAN_MACHINE_PUBLIC_KEY` sets `account.machine` from a verified token on HTTP and websocket calls and strips any other, and `.with(Machine)` reads it.
38
+ - ea39ba0: A write can drop an endpoint's `{ cache }` answers before their TTL. Plug `EndpointCacheControl` into the service and call `invalidate("bannerListInPublic")` for every answer of that endpoint, or `invalidate("bannerInPublic", [category])` for the one call with those arguments. Every instance sees it at once through the cache the entries live in. Entry keys now carry a per-endpoint generation, a random token that the endpoint-wide form replaces in one write. Nothing is scanned, a miss already running cannot write its stale answer back, and a generation that expires or is evicted starts a fresh token rather than reviving dropped answers. The generation expires after a day, so no entry is written for longer. A slice caches its insight beside its list, so invalidate both. The one-call form deletes one key: pass every declared argument (`null` for an omitted search argument), use the endpoint-wide form for a slice list, and note that a miss for those arguments already running can still write that entry back.
39
+
40
+ Upgrade note: a `{ cache }` TTL above one day is capped at one day, and entries written before the upgrade are missed and expire on their TTL.
41
+
42
+ - b1165ba: An endpoint declares the errors it throws on purpose, by status: `mutation(cnst.Ticket, { guards: [Owner], errors: { 409: ["ticket.error.locked"] } })`.
43
+
44
+ - OpenAPI documents each declared status as a response in the wire shape every error already answers with (`components.schemas.AkanError`: `error`, `statusCode`, `data`, `details`, `path`, `timestamp`), with `error` enumerating the declared keys and the description carrying each key's message.
45
+ - The MCP tool's description gains a line naming each declared status, key and message, since the description is all an agent reads before calling.
46
+ - The declaration is checked: a status outside 400–599 or a key that is not `<module>.error.<key>` throws where the endpoint is declared, a key no dictionary defines is warned about at boot, and outside production an endpoint that throws a key its declaration leaves out is warned about once. `base.error.*` is the framework's and is never declared.
47
+ - `Err.is(error, ...keys)` narrows a caught value to an Akan error carrying one of those keys, typed against the app's error keys; it reads the `error` field, so an error restored from another server passes too.
48
+
49
+ The wire format is unchanged.
50
+
51
+ - ce817b6: `akan build-desktop` and `akan publish-update` sign only with `--sign` and notarize only with `--notarize` (as does `akan-native build` / `update publish`): `AKAN_NATIVE_MACOS_*` and `AKAN_NATIVE_WINDOWS_*` name the identity and no longer decide to use it, and a build that finds them set without the flag says it is unsigned. A release build or `publish-update` of an app that carries its server, signed or not, first searches that server for every secret value of the workspace's server env files and refuses it with the file and key paths, and an env file it cannot evaluate stops the release; one line then names the identity and the service a signed app goes to. The SSR route bundle now takes the server env plugin too, so a build carries only its own environment's app env there and a profile build swaps the libs' env defaults in it as it does in the backend. `akan publish-update --arch arm64|x64` publishes a desktop release for another CPU into that CPU's feed folder.
52
+
53
+ Upgrade note: signing and notarizing are opt-in. A CI that only sets `AKAN_NATIVE_MACOS_*` or `AKAN_NATIVE_WINDOWS_*` now builds and publishes unsigned macOS and Windows apps, after a line saying those variables went unused and a warning that the app is not signed, and an installed signed app refuses such an update. Add `--sign true` to `akan build-desktop` and `akan publish-update`, and `--notarize true` for macOS, to sign and notarize as before.
54
+
55
+ - ce817b6: The `external-view` native plugin (`akanjs/client/native/external-view`) shows a third-party page first-party over a desktop window: a browser profile of its own, navigation held to the origins it was opened for, an init script for the top document, `evaluate`, a trusted `sendKey`, `capture` of its content, `setMuted`, `clearData`, autoplay with sound by default, and `load` / `loadFailed` / `gone` (it reloads, and is rebuilt when its browser process ends, counting `restarts`) / `consoleMessage` (warn and up by default, with source and line, rate-capped) events — every method on Windows, most on macOS 14+. The `window` plugin adds `placeOnDisplay({ display | index, bounds, unit: "logical" | "physical", fullscreen })`, `setDecorations`, `setCursorVisible`, `setMinSize` and `create({ frameless })`, and `native.desktop.window.frameless` opens the main window without a frame. `AkanServer.start({ schedules: false })` runs no internal.
56
+ - ce817b6: Frame chrome renders on the server: `Layout.Navbar`, `TopInset`, `TopLeftAction`, `BottomInset` and `BottomTab` are in the SSR HTML in their slot's box and move into the slot on mount, and the top-left action no longer scrolls away. `Layout.Navbar` draws its `title` / `left` / `right` slots and takes `backFallback`; `Link.Back` takes `fallback` (`router.backOrFallback`), and `router.canGoBack()` follows the history entry an SSR navigation writes. `Modal`, `Dialog.Modal`, `Model.New` and the model modals take `presentation` (`"dialog" | "sheet" | "auto"`, a full-screen sheet below `md` that a drag down dismisses) and `placement` (`"center" | "right"`). `topInset` / `bottomInset` take `{ base, sm?, md?, lg?, xl?, "2xl"? }`; a CSR page whose route declares none takes the height of its chrome; on the web a declared `safeArea` pads with `env()`; the frame publishes `--akan-content-height` and `--akan-keyboard-inset`, and mirrors the current page's frame onto `<html>`. `useVisualViewportFrame` (akanjs/webkit) reports the web keyboard, which now reaches `st.keyboardHeight` and keyboard-sticky chrome on SSR pages. The agent launcher clears the bottom chrome and the safe area, sits under menus and modals, and takes `launcherPlacement` / `hideLauncherBelow`. `Link`'s active match strips the basePath and takes `activeMatch`. Upgrade note: `Link`'s prefix match is by path segment and `/` matches only itself; `Portal`'s server capture (`ServerPortal`) is gone.
57
+ - 17aa243: MCP Apps: each `page().app()` is one component of a screen, which an MCP Apps host draws in a sandboxed view.
58
+
59
+ - **Tools.** Each `.app()` is a tool named after the app. Its input is the page's `.param()` and `.search()`. Its annotations are `readOnlyHint: true, openWorldHint: false`, since the load may only query. Its `_meta.ui.resourceUri` is `ui://<app>/apps/<name>`. A call checks the arguments, runs the route guards, and then runs the app's load alone: no layout body and no `.render()`. The model receives the briefing of what the load fetched plus `Open the full screen: <page URL>`. A view receives the raw values in `_meta`, and the route values it starts from in `structuredContent`.
60
+ - **`.prompt()` pages are text tools too.** Each stays a prompt and is also listed in `tools/list` as a text tool (`readOnlyHint: false`, since its run renders the whole page), except on a read-only deployment or with `apps: false`. It never names a view, and `prompts/get` serves only prompts.
61
+ - **Names.** Prompts and apps share one namespace. Two pages that claim one name fail the listing, whatever the kind of each claim. An endpoint keeps its name over an app or a prompt's text tool, with a warning at boot, and such a prompt stays a prompt only.
62
+ - **Read-only deployments** (`option.setMcp({ readOnly: true })`) keep the app tools, their resources and the views. A prompt's text tool is left out, since its run may write; the prompt itself stays. An app result there carries `_meta["akanjs/readOnly"]`.
63
+ - **Resources.** Each app has its own `ui://<app>/apps/<name>` view, read from the `mcp-apps/` folder that the build writes beside `csr/` and listed only to a client that renders views.
64
+ - **Revisions.** A modern client declares the extension per request in `_meta`; a legacy one declares it at `initialize` and is issued a stateless `Mcp-Session-Id: ui.<nonce>.<signature>` the app signs, and a forged or unknown id is served as a client without views. The server offers the modern revision unless a listed page declares an `.app()` and the app serves its CSR bundle: claude.ai mounts a view only for a server it reached through `initialize`, so `server/discover` then answers `-32601`. `option.setMcp({ modern: true })` / `AKAN_MCP_MODERN=true` offers it anyway.
65
+ - **App-only tools.** An endpoint marked `mcp: false` stays off every model's shelf but reaches a client that renders views, and a `Person` act stays refused there. The generated `light<Model>` read is one too, under the model's `get` guards. Visibility is a host's UX and never authorization, so the guards decide what a view may call exactly as for a model.
66
+ - **The view boots without a router.** `McpAppBoot.run({ app, page, root, rootOverrides, overrides, basePath })` (`akanjs/webkit`) runs the app's load again with the route values from the result, in the host's language when the app speaks it. Its first reads come from the snapshot. It draws the element inside the `_overrides.tsx` over the page and the generated root layout's `McpAppRoot`, which renders `System.App` (`akanjs/ui`): the root layout's theme, fonts and providers, with no frame, router or socket. The overrides the web draws outside the root layout (on its folder, or on a folder above it that holds no layout of its own) wrap `McpAppRoot` instead, so its toasts and approvals take them too. No layout body or `.render()` runs in a view.
67
+ - **A view never moves.** `csrLocation` reads the screen the view draws rather than the host's sandbox address (`csrLocation.view(href)`), so `usePage()`, a `<Link>` and the agent's context name that screen. A link or `router.push` to one of the app's screens opens that screen's web page through the host, under the page's basePath, and any other web, `mailto:` or `tel:` link opens through the host's `ui/open-link` as well. A fragment of the view's own screen scrolls the view to its target, found as written or percent-decoded as a browser finds it, and `router.back()` and `router.refresh()` do nothing.
68
+ - **Language and look.** The view sets `<html lang>` to the language it draws in. The host's theme sets its `data-theme`, and the host's colors and fonts fill the `--background`, `--card`, `--popover`, `--muted`, `--foreground`, `--border`, `--input`, `--ring`, `--font-sans` and `--font-mono` tokens; brand colors and radii stay the app's. The view reports its height to the host as it changes.
69
+ - **The screen's levers.** `st.tool`, a form setter and the built-ins become tools the view serves to the host's model, run through the same `ToolRunner` as the in-page agent; a tool a person answers with a card is left out, and `readOnlyHint` follows `settle`. A tool declared with a `confirm` asks the person through the agent's approval card (the `AgentApproval` override slot). The view also pushes its guide and `AgentContext` blocks with `ui/update-model-context`, and an `<a download>` link, clicked or created in code, saves through `ui/download-file`.
70
+ - **Resource CSP.** `_meta.ui.csp.resourceDomains` and `baseUriDomains` name the app's own origin, normalized before it reaches markup, which is what lets a font and a stored image load; `connect-src` stays closed. `/_akan/fonts/*` answers `Access-Control-Allow-Origin: *`, and a host that blocks `<base>` gets the origin patched onto root-relative `src`, `srcset`, `poster` and `<link href>` instead.
71
+ - **Writes in a read-only deployment.** The view's fetch bridge refuses a mutation before it reaches the host, with `base.error.readOnlyDeployment` ("This server is read-only, so {endpoint} cannot run here."). The person sees it as a toast in their own language, and a host model that presses the same button reads it as a tool error. A write would otherwise reach a host that lists no write and answers "Unknown tool".
72
+
73
+ Upgrade note: every `.prompt()` page now also appears in `tools/list` as a text tool. An app that serves its CSR bundle (the default `web: true`) lets any client that declares MCP Apps support call its `mcp: false` endpoints and `light<Model>` reads, which 3.0.2 refused over MCP, under their guards; while a listed page declares an `.app()`, it also speaks only the legacy revisions unless `modern: true`. `option.setMcp({ apps: false })` / `AKAN_MCP_APPS=false` undoes all three: pages stay prompts only, those endpoints stay refused, and the modern revision stays offered.
74
+
75
+ - ce817b6: An endpoint can hand an MCP client's model a picture: declare `query(ImageResult, { guards })` (from `akanjs/base`) and return `{ data, mimeType, caption? }`, where `data` is padded base64 without a `data:` prefix and `mimeType` is `image/png`, `image/jpeg`, `image/webp` or `image/gif`. `tools/call` answers the caption as a text block and the picture as an image block, one pair per picture for `[ImageResult]`, with no `outputSchema`, `structuredContent` or JSON text block to repeat the base64. A value that breaks the shape is not sent: the call answers the generic failure and the log names what an `ImageResult` needs. Over HTTP the endpoint returns the JSON object as before, an MCP App view's call gets that object too, and a page prompt or the in-page agent reads only its `mimeType` and `caption`. Like `Binary`, `field(ImageResult)` fails the class build. `McpToolResult.content` now types an image block beside the text block.
76
+ - ce817b6: Phone web bundle updates move to protocol v2.
77
+
78
+ - **Path:** a binary reads only its own native API's manifest, `<url>/<platform>/<channel>/<nativeApi>.json`, so two store generations on one channel each take their own fixes.
79
+ - **Envelope:** that file is one envelope holding the manifest's bytes, the signer's `keyId` and the signature, so a CDN can never pair a new manifest with an old signature.
80
+ - **Keys:** `native.updates.publicKeys` lists 1 to 3 trusted keys, the online signing key first. `publicKey` stays as the one-key form.
81
+ - **Revocation:** a manifest can carry `revokeKeys`, plus `resetSequence` to recover from a polluted sequence. A key may revoke only keys listed before it, and a revocation is kept for good on the device.
82
+ - **Publishing:** `akan-native update publish` carries the channel's revocations forward into each new envelope and refuses to sign with a key they revoke, so a routine publish never undoes a revocation.
83
+ - **Store build:** `bundle.json` records the binary's update URL, key ids and `embeddedSequence`.
84
+ - **Server-side helpers:** `openEnvelope`, `keyIdOf` and `updateDecision` are exported for a server that signs or registers releases.
85
+
86
+ Desktop app updates keep protocol v1 and trust the first key.
87
+
88
+ Upgrade note: no phone binary that shipped with `updates` reads the new layout. Rebuild the binary before publishing, and point a signer at the new path. `akan pack-update` writes a schema 2 template.
89
+
90
+ - ce817b6: `router.push` / `router.replace` take `{ shallow: true }`: on an SSR page a query-only change writes the URL and the store without asking the server for the page, and back/forward between such entries stays shallow. `Tooltip` draws on the overlay layer and flips to the side with room, so it no longer clips inside a scrolling list or widens it. An SSR document links `rootLayout().manifest()` at `/manifest.webmanifest` (the basePath's own when it is visible), which the server answers, instead of a `data:` URL. An input smaller than 16px is drawn at 16px on a touch screen, where iOS would zoom into it, and a caller's own `text-*` still sets its size. `BottomSheet` reports a dismissal once and never on mount, locks the page while open, pads for the safe area, and opens on the overlay layer above the frame. Upgrade note: `Tooltip`'s bubble is no longer in the DOM until it opens; a test that read it beside its trigger reads the trigger's own text instead.
91
+ - 9bf9181: The ops channel runs data migrations, so a control plane reaches a host it has no shell on. `POST /_akan/ops/migrate` `{ id, action, target?, params?, expect?, items? }` starts `ops migrate <action> … --json` in a child process, and `GET /_akan/ops/migrate/:id` answers its exit code and the result line it printed. An ops token may carry `sub`, whom the control plane acts for: a migrate job records it as the run's actor, and apply and rollback refuse a token without one.
92
+ - ce817b6: A phone web bundle release can start revoking an update signing key, and the config keeps the revocation.
93
+
94
+ - **Start one:** `akan publish-update --platform ios|android --revoke <keyId,...>` (`akan-native update publish ios|android --revoke <keyId>`, repeatable) revokes keys of `updates.publicKeys`; `--reset-sequence` adds `resetSequence` to that release. `akan update-keygen` prints each key's keyId.
95
+ - **Keep it:** `native.updates.revokedKeys` lists the keyIds every phone release revokes, so a release published from a fresh checkout still revokes them. A release carries `revokedKeys`, the channel's published revocations and `--revoke` together, and warns about a revocation the list does not hold.
96
+ - **New binaries:** a phone build trusts none of the keys `revokedKeys` lists, though `updates.publicKeys` still holds them, so a fresh install never takes a release a revoked key signs. A config whose `revokedKeys` covers every key is refused. A desktop release signed with a revoked first key warns that installed desktop apps keep trusting it until a binary with another first key replaces them.
97
+ - **Refused before the build:** a keyId the key ring does not hold, more than 16 revoked keys, `--reset-sequence` without a revocation, either flag on a desktop release, and a signing key the release revokes or that is listed before a key it revokes. The error names where the revocation came from and the key to sign with (`AKAN_NATIVE_UPDATE_KEY` picks its file).
98
+ - **API:** `publishUpdate` and `checkPublishUpdate` take `revoke` and `resetSequence`, and `checkPublishUpdate` validates the `updates` settings too; `updateKeygen` returns `keyId`. The native build API is 0.12.0.
99
+
100
+ Upgrade note: the akan CLI needs native build API 0.12.0, so install matching versions of akanjs and @akanjs/cli; an older one is refused.
101
+
102
+ - ce817b6: `updates.report: { url }` has a phone post what became of each web bundle release, so whoever publishes sees rollback rates.
103
+
104
+ - **Events:** downloaded, applied, confirmed, rolled back (timeout, relaunch, hang, failed check at start) and failed (download, file hash, disk, signature, manifest).
105
+ - **When:** at the next launch or return to the foreground, 20 to a request. Events wait until the server takes them, at most 100.
106
+ - **Identity:** a random install id that lasts as long as the install, not a device identifier, and nothing about the user.
107
+ - **Store builds:** `bundle.json` names the address, so a server registering the binary can check it is its own.
108
+
109
+ - 17aa243: `page()` takes an `.app(name, description, load)` stage, which declares one component of the screen as an MCP App. `load` receives the page's typed arguments and `lang`, and returns the element. On the web, that element reaches `.render()` as the argument named `name`:
110
+
111
+ ```tsx
112
+ export default page()
113
+ .param("ticketId", ID, { desc: "The ticket to show." })
114
+ .app(
115
+ "showTicket",
116
+ "Show one ticket with its status and assignee.",
117
+ ({ ticketId }) => {
118
+ const { ticketView } = fetch.viewTicket(ticketId);
119
+ return <Ticket.Zone.View view={ticketView} />;
120
+ }
121
+ )
122
+ .render(({ showTicket }) => (
123
+ <div className="flex flex-col gap-4">{showTicket}</div>
124
+ ));
125
+ ```
126
+
127
+ - A page may declare several apps. An app name follows the prompt name rule (`^[A-Za-z0-9_-]{1,64}$`) and needs a description.
128
+ - A name that `.render()` already receives fails to compile and throws when the module loads. That covers `lang`, a `.param()` or `.search()` name (in either order), and another app. An app named like the page's own `.prompt()` also throws.
129
+ - `load` may only query. A mutation, a file upload, a room subscription or a message sent while `load` runs throws `base.error.mutationInApp` before anything is built or sent. That holds on the server through every `await`, and in a browser for `load`'s synchronous part. The new `AppScope` (`akanjs/fetch`) marks the run.
130
+
131
+ - ce817b6: An app's pipeline can ship its native web bundle over the air: `{ use: "ota", targets?, platforms? }` runs after `deploy` in its own `ota` phase, followed by an `after-ota` slot. It is opt-in, so an app without it runs build → docker → deploy as before. By default it packs every native target that declares `native.updates` for both iOS and Android, and a named target without `updates` is a plan error. The plan's step carries `with: { app, targets, platforms }`, and `akanjs/common` exports `pipelineOtaPlatforms` and the `PipelinePlanOtaStep` type. An executor parsing plans with an older `akanjs/common` refuses a plan that holds `ota`.
132
+ - 1324d30: A model's rows can now be deleted for good. `purge<Filter>(...args)` is generated beside `remove<Filter>` for every filter, and the table facade gains `purgeMany(query)`. Both delete every row the query matches and return how many were deleted. They are meant for data past its retention, such as logs.
133
+
134
+ - They reach removed rows too, unlike every other query-level write, so `{ createdAt: q.lt(cutoff) }` clears a cutoff's live and removed rows alike, and `{ removedAt: q.lt(cutoff) }` clears old removals.
135
+ - Each row's text-search mirror goes with it. No hook, cascade or live room runs, and there is no undo.
136
+ - Rows are deleted 1,000 per statement in every database mode, so a large purge never holds the SQLite write lock for long. Between batches the purge waits one timer turn, so other requests in the same process can write.
137
+ - A query that matches every row (`{}`, an empty `q.all()`, a filter whose `q.when` conditions all fell through) is refused, as is one with `q.search()`.
138
+
139
+ - 2c5b833: `LibsqlDatabase` is removed, along with the `@libsql/client` optional peer dependency, `LibsqlDatabaseConfig`, the `database.libsql` server env and the `LIBSQL_URL` / `LIBSQL_URI` / `LIBSQL_AUTH_TOKEN` variables. No database mode used it: `single` and `multiple` run on `SqliteDatabase`, `cluster` on `PostgresDatabase`. Upgrade note: an app that applied `LibsqlDatabase` itself moves to `SqliteDatabase` for a local file or to `cluster` mode for a shared server.
140
+ - c184074: A list no longer keeps rows from before a write when the navigation cache replays its page. `Load.Units` refetches rows it seeded from a replayed page, since a cached route is replayed with no TTL and no live room kept those rows current. A live event that arrives while a slice refetches still marks the slice stale, though it is not applied, and the refetch dates its rows from when the request started, so a write it may have missed is read again on the next mount. A new generated `invalidate<Model>()` marks every slice of a model stale without a request, for a custom mutation whose changed rows the store never received. It replaces writing `<model>StaleAt<Suffix>` keys by hand.
141
+
142
+ Upgrade note: a page reached through the navigation cache now issues one list request per `Load.Units` whose store held other query arguments, and the reload overlay shows while that request runs. Pass `loading={null}` to hide the overlay.
143
+
144
+ - ce817b6: `rootLayout().viewport({ fitCover, interactiveWidget, themeColor })` writes the document's `viewport` and `theme-color` meta, on the server and in the CSR shell; a native app draws under the notch by default, and a per-theme `themeColor` follows a theme switch. Route heads now merge root first: a deeper `.head()` replaces the `<title>`, a `<meta>` with the same `name` or `property`, and a canonical, manifest or icon link, and keeps the rest. Upgrade note: a page that restated its root's manifest, icon or theme tags can drop them, and an ancestor's head now renders under every page that writes its own.
145
+ - 17aa243: A route can check who may open it before anything on it renders. `rootLayout()`, `layout()` and `page()` take a `.guard(fn)` stage, declared at most once per route. It receives the route's typed arguments and `lang` (a layout's guard gets no `children`). It refuses with `router.redirect()` or `router.notFound()`, or with `getSelf({ unauthorize })` from a guard. The new `signedIn()` helper in `libs/shared/webkit` does the last of these.
146
+
147
+ - **Server render.** Every guard in the chain runs in order before any layout body starts, root layout first, and before the route's arguments are checked. A redirect after an awaited query is now a real 307, and a not-found is a 404. Before this, a refusal raised inside a layout body after I/O arrived as a late, in-stream redirect. A query inside a guard that the server refuses with 403 or 404 (an owner check, for example) answers not-found, so the page does not tell anyone the record exists.
148
+ - **Client navigation.** A page's layers wait for its guards. Their verdict is kept by what the guards read, so a move that changes only a query no guard reads keeps it. It lasts while a layer of that location is on screen. Only the page in front asks: a parked page, or the one under the current page during a swipe back, asks nothing. Back in front, a cached page included, it asks again in the background while what it drew stays; a refusal moves the router and blanks it, and a failure shows as a failed render. A legacy page keeps the props it was let in with while a new location is checked. A guard that moves the router ends the chain, and the page draws nothing. A refused or failed check runs again when its layer is retried. Root layouts are drawn once for every page and are not gated themselves, but their guards still run in each page's chain, and a legacy page under them waits for them too.
149
+ - **A refusal shows nothing of what it refused.** The not-found page that a guard's `router.notFound()` answers is drawn under the layouts above the refusing guard only, with no route head; a guard that throws a string is cut the same way. A path no page matches asks the guards of the layouts its not-found would be drawn under first: a redirect answers as one, and a refusal keeps only the layouts above it.
150
+ - **`guard` is a stage only.** A legacy module (a default function beside named exports) that exports `guard` fails to load and fails `akan build`, a legacy root layout included.
151
+ - **MCP.** A page prompt runs its route's guards before its bodies, so a refusal ends the prompt as a redirect or a not-found. `prompts/list` and the app tools in `tools/list` show only the screens whose guards admit the caller. In a listing, no arguments are known yet: a guard that reads one counts as admitting the caller there, and is checked again when the prompt or tool is called. If the guards do not answer within the list wait, the list is served unfiltered, and the calls are still checked. `resources/list` shows a UI host only the views of the apps it may call. A caller's listing verdict is kept for 5 seconds, so a host that lists tools, prompts and resources in a row runs the guards once.
152
+
153
+ A generated root layout passes the user layout's guard through. When there is no ancestor root, it composes the app root's guard and the user layout's guard with the new `RouteGuards.compose`.
154
+
155
+ - fd52c2c: Scope the generated `upload<Field>On<Model>` store actions to the form that started them, and expose their pending state. A new `<model>FormUploading` state key holds the uploads in flight per `File` field (`{}` when idle); `check<Model>Submitable`, `Model.EditModal` and `Model.ViewEditModal` keep submit disabled while it is not empty, and `create` / `update<Model>InForm` (so `submit<Model>`) save nothing until it is. `Field.Img` / `Imgs` / `File` / `Files` handed the generated setter upload through `upload<Field>On<Model>`, and the new `formUploadOf(setter)` answers `{ upload, refresh }` for a generated File-field setter with matching upload and write actions (`null` otherwise), so any control can upload through the form and write a polled file back by id. A result is dropped when the form was set whole meanwhile (`new` / `edit` / `reset`, a save, a draft restore, `set<Model>Form`, an edit shell's hydration — the store clears the key on every `set({ <model>Form })` — or another record) or, on a single field, when a newer upload or `set<Field>On<Model>` superseded it; an array field inserts into the list as it is when the upload lands, and the status poll stops once the field no longer holds the file.
156
+ - 8ba25ec: Data migrations: a change that reshapes stored documents ships a script in `apps/<app>/script/migration/`, which every environment runs the same way.
157
+
158
+ - **`akan create-migration <name> <app>`** writes `script/migration/<yyyyMMddHHmm>_<name>.ts`, a default-exported `migration(title)` from `akanjs/server` with empty `.plan()`, `.apply()` and `.verify()` stages, which also takes `.param()`, `.rollback()` and `.config()`. A migration writes only through `table(refName)`, which records every value it changes.
159
+ - **`akan migrate <app> [list|plan|apply|verify|rollback] [id]`** runs them locally, and `bun main.js ops migrate` takes the same arguments in a built image. `plan` prints a `planHash`, and `apply --expect <planHash>` refuses when the data changed since that plan was reviewed. `--json` prints the result as the last line, even when the command line itself is refused; the exit code is 0 on success, 2 when the command, a parameter or the id is wrong, 3 when verify finds problems (an apply's changes stay committed), and 1 otherwise.
160
+ - **Runs are recorded in the app's own database** (`_akan_migration_run`, `_akan_migration_change`) under a lease, so one runs at a time; `db-export` and `db-import` carry them along, and no insight query can read them. `rollback` restores what an apply changed, deletes the rows it inserted or copied, skips a row edited since, and still works once the script is deleted, then with the default undo. `.config({ transaction: false })` commits as it goes, and a failed run is left `partial` to resume.
161
+ - The generated `server.ts` exports a `migrations` manifest of that folder, and the `Err` lint rule leaves `script/migration/**` alone, since an operator reads its messages.
162
+
163
+ - ce817b6: Apps declare their build pipeline in `akan.config.ts`. `pipeline: { stages, env, after }` in an app's config lists its stages in order — the built-in `{ use: "build" | "docker" | "deploy" }` actions plus shell stages (`{ name, sh | steps }` with `when`, `env`, `secrets`, `timeout`, `retry`, `allowFailure`) — and an app without one runs build → docker → deploy as before. A new workspace-root `akan.config.ts`, typed `WorkspaceConfig`, holds the stages around every app: `pipeline: { before, after, post, env, timeout }`, with `parallel` and `matrix: "apps"` stages. `akan pipeline dry-run | validate | plan` turns both into the plan Akan Cloud runs, locally and without running anything; it exits 0 with a plan, 1 with an envelope of errors and 2 for anything else. `akanjs/common` exports the plan schema and its strict parser (`PipelinePlanSchema`).
164
+
165
+ `akan upload-secret <NAME>`, `akan list-secrets` and `akan remove-secret <NAME>` manage write-only pipeline secrets per project, optionally per branch (`--branch debug|develop|main`). A value comes from `--file`, piped stdin or a hidden prompt, never from an argument. A step lists the names it reads in `secrets`. Anyone who can push to the repository can make a step print a secret, as with GitHub Actions secrets.
166
+
167
+ Upgrade note: `akan.subspace.ts` is no longer read. Move its content under `subspace` in the workspace-root `akan.config.ts` (`const config: WorkspaceConfig = { subspace: { … } }`). The root config never travels to a subspace.
168
+
169
+ Upgrade note: delete `akan.subspace.ts` after moving its content. `akan subspace push` and `akan subspace pull` refuse while it is still at the workspace root, and a push removes any copy of it from the subspace.
170
+
171
+ - 6dabbf8: `StorageAdaptor` gains an optional `resolveLocalPath(path, access?)`, and `BlobStorage` implements it: the absolute disk path of a stored file, picked from `root` / `privateRoot` the way the adaptor writes (with `access`) and reads (by the `private/` prefix), or `null` for a key it refuses: one that is rooted, holds a NUL, or leaves that root. Refuse such a key (answer 404) and never fall back to `readData()` for it; fall back only when the adaptor has no `resolveLocalPath` (object storage). A key that comes from a request passes `access: "public"`, since without it a `private/` key resolves under the private root. An app serving or processing stored files no longer casts the role to reach its internals; an object-storage adaptor leaves the method out.
172
+ - b1165ba: A model or scalar argument is now checked against the request schema the server publishes, on HTTP, websocket and MCP calls, before the handler runs. Until now a stored model's input passed through unchecked: `{ count: "x" }` or a string where an object belongs reached the service, and failed at save time as a 500 or was stored as sent.
173
+
174
+ - A field of the wrong JSON type, a value outside its enum, a malformed id, or a required field that is absent or `null` answers 400 `base.error.invalidArgument` / `base.error.requiredArgument`, with `data.name` naming the path (`data.items.2.price`) and `data.expected` what it takes. MCP answers `Invalid field "data.title": expected String.` / `Missing required field "data.title".`
175
+ - A key the input does not declare is dropped, so a body can no longer set a field outside its input class (`removedAt`, a role) for a service that forwards it.
176
+ - A field with a default may be left out, and a stored model's input keeps an absent field absent, so an app built against an older schema keeps working and an update still sets only what it carries. An embedded scalar is parsed whole, its absent fields taking their defaults.
177
+ - Dates arrive as `Dayjs`, as the input type always said, instead of the wire string.
178
+ - A primitive argument (`?limit=3`, a path param) keeps its text coercion.
179
+ - A document whose enum or `validate` refuses a value now answers 400 `base.error.invalidField` with `{ field }` instead of a 500.
180
+
181
+ Upgrade note: a caller that sent a body the published schema refused — a numeric string for an `Int`, an `{ id }` object for a relation, a required field left out — now gets a 400. A service that read an undeclared key off its input no longer sees it; declare the field on the input.
182
+
183
+ - ce817b6: `fetch.clone({ retry })` and a call's `{ retry, idempotencyKey }` retry an unreachable server, a 502/503/504 and a 429 after its Retry-After on a jittered backoff, within the call's one deadline; a mutation retries under one `Idempotency-Key`, and the server runs a mutation once per key, caller and arguments, answering a repeat with the first result (409 `base.error.idempotencyInFlight` while it runs) — a signed-out call, or one whose handler returns a `Response`, runs every time. The websocket client reconnects on a jittered exponential backoff (`ws: { reconnect, headers, tls, onStateChange, onRevoked, onError }`), sends a server-side client's token with the upgrade, numbers each room's frames, drops a repeat and calls `onResync` at a hole, and a resubscribe gets the frames it missed since its own subscribe replayed by a solo server on the built-in pub/sub (a Redis-backed or custom adaptor, and a `pubsub(Binary)` room, resync instead). `Model.upsertById(data, { preserveTimestamps })` writes a record under its own id with no hook. An insight field sums with `accumulate: { $sum: "<field>", ...condition }`. A `process` job's backoff takes `maxDelay` and its internal an `onFailed` dead-letter hook. Fixes: a live slice's `.live({ payload: "id" })` reaches its rooms; Tailwind v4's `top-(--x)` shorthand compiles; the root layout's manifest link is in the document head, where Chromium reads it.
184
+ - ce817b6: A desktop app's carried server takes `desktop.server: { env, restart, watchdog }`: variables over the image's (secret-named ones and the stage's own refused at build), `restart: "forever"` or a count of crashes before giving up, and a health-route watchdog that restarts a hung server. `desktop.webview2: "standalone"` embeds the whole WebView2 Runtime in the Windows setup. `updates.setChannel({ channel })` moves an installed desktop app to another manifest, and `akan publish-update --upload s3://<bucket>/<prefix>` uploads the release, its manifests last. Fixes: `Model.New` opens under its own `modal`; two `Model.New`s of one model on different slices open only the pressed one's form; the top inset slots clip at a 0 inset and follow iOS's keyboard pan; `Layout.BottomTab` matches tabs by segment with per-tab `exact` and `match`, keeps the badge under `renderTab` and marks the current tab `aria-current="page"`; a multipart mutation's other body args arrive typed as in a JSON body.
185
+ - ce817b6: The workspace-root `akan.config.ts` takes `cloud: { host }`, the cloud the CLI uses when a command names no `--host`. This lets a workspace on a self-hosted Akan Cloud skip passing `--host` every time. It applies to login, env, secret, subspace and tunnel commands. The host must be https, or http on this computer.
186
+
187
+ ### Patch Changes
188
+
189
+ - 4fc3902: The agent chat's `@` and `/` menus:
190
+
191
+ - Two records with the same title, such as two pages called "Minutes" or a ticket and a document with the same name, no longer share one React key. Each `MenuRow` now carries an `id` (`<refName>/<refId>` for a reference row, the command name for a `/` row), and `DefaultAgentMenu` keys its rows by it. An `AgentMenu` override should key its rows by `row.id` too.
192
+ - Clicking or tapping a row no longer moves focus out of the composer. On a phone that blur dropped the keyboard, and the panel shrank under the finger, so the tap missed.
193
+ - `ComposerProps` gains `isMenuOpen`, true while a menu is drawn. An `AgentComposer` override that writes a newline on Enter can pass Enter to `onKeyDown` while it is set, so Enter picks the highlighted row.
194
+
195
+ - 8aaa0c6: An endpoint in a model's `endpoint()` that returns `Any`, such as `query(Any)` or `mutation([Any])`, is now typed `unknown` (or `unknown[]`) on the client, in `fetch` and in signal-test agents alike. Before, it was typed as the model's full class, because every model fits `unknown`.
196
+ - ce817b6: `Binary.toBase64(bytes)` and `Binary.fromBase64(text)` from `akanjs/base` are public: the codec `Binary` reads and writes its wire form with, on `Buffer` where the runtime has one and on `btoa`/`atob` in a browser bundle. The MCP App view's downloads, `useCamera` and the agent chat's image attachments use it instead of their own copies.
197
+ - b1165ba: A server logs, at `debug`, how long its boot took and where: `Initialized in 38.4ms (uses 0.2 · adaptors 10.4 · serverSignals 0.2 · services 0.5 · internals 0.1 · slices 0.6 · endpoints 0.2 · web 22.2 · builtinRoutes 3.8)`. The DI stages are also readable as `DiLifecycle.initTimings`.
198
+ - 2082ccd: Akan.js now requires Bun 1.4.4: every package's `engines.bun` reads `>=1.4.4`. akan-native's doctor fails on an older Bun and warns on any other, with `bun upgrade --version 1.4.4` as the fix. Before 1.4.4, bun:sqlite counted the rows a full-text index's triggers wrote, so `updateManyByQuery` and `removeManyByQuery` on a model with search fields reported far more `matchedCount` and `modifiedCount` than the documents they changed.
199
+
200
+ Upgrade note: 3.0.2 accepted Bun 1.4.0; run `bun upgrade --version 1.4.4` first.
201
+
202
+ - 1c577db: Raw `<button>` elements inside `akanjs/ui` components declare `type="button"`, as `Button` already does, so they no longer submit an app's enclosing `<form>`. Clicking the `Input.Password` / `Field.Password` show/hide toggle, a `Field` list's add or remove button, a `ToggleSelect` option, a copy button or a `Model` trigger inside a form now runs only its own action.
203
+ - ce817b6: A `query` declaring `{ cache: <ms> }` now answers a cache hit exactly as the call that filled it. The entry was plain JSON, which spells a `Map` `{}`, bytes an index map and a date a string, so a hit on a model with a `Map` field answered 500 (`value.entries is not a function`), a `Binary` return came back as an object of indexes instead of base64, and every `resolveField` on a hit received a plain object — no model methods, dates as strings — where the miss handed it the model. The entry now keeps Maps, bytes and dates, and a hit rebuilds each value the handler returned as a model instance as the endpoint's declared return model, so `resolveReturn` and `resolveField` see the same shape on both paths.
204
+ - 7cb97fa: A cache hit on a `query` declaring `{ cache }` loads each relation in the answer from its stored id, as the miss that filled the entry did: a required relation, an optional one, a list of them, and those inside an embedded scalar. In the 3.1.0 release candidates a hit read every stored id as an empty related model, so a required relation answered 404 `base.error.relationNotFound` and an optional one or a list answered `null` or `[]`.
205
+
206
+ A plain object in a cached answer that carries the key the cache marks its own values with, `"\u0000akan"` (an `Any` field relaying client JSON, say), now reads back on a hit exactly as it was written. In the release candidates a hit could revive it as a `Map`, a date or bytes, or replace it with the object under its `fields` key.
207
+
208
+ - 1dd54bf: A cascade removes each document once. When two `removeRef` fields, or one array, held the same id (one file used as the banner for two locales), the cascade removed it twice. The second removal found no live row and failed with a 500, so the fields after it were never reached and their files stayed behind. A document that an earlier step of the same cascade already removed, through `removeRef` or `removeWith`, is now skipped too.
209
+ - 78e785e: A cascade skips a referenced document that is already removed instead of failing. Two owners can hold one `File`, since files are deduped by `origin`. Removing the first owner removes the file; removing the second used to call `remove` on it again, throw `NoDocumentError` and leave the rest of that owner's files and children in place. Each per-document `removeRef` edge now asks which of its ids are still live, 200 ids per query, and removes only those.
210
+ - 63479ae: A server process the gateway runs now shows its Logger errors in the terminal. Under `akan start`, the stack of a failed endpoint appeared only in the rotating log file, because the gateway hides a child's stderr unless `AKAN_CHILD_STDERR=1`.
211
+ - a597881: `akan code`'s `web_fetch` reads an intranet page itself instead of handing its URL to DeepSeek. In the release candidates it judged a host by its name alone, so a dotted intranet name such as `jira.corp.example.com`, a `.test` name, `localhost.` with its trailing dot, `[::]`, and a loopback or private address written IPv4-mapped (`[::ffff:127.0.0.1]`) went to DeepSeek's Fetch API. It now resolves the name first and sends the URL to DeepSeek only when every address it resolves to is public; a name that does not resolve stays on the machine.
212
+
213
+ The CLI's warning that the workspace's `akanjs` is not the CLI's own version now goes to stderr instead of stdout, so a command's machine-readable output, such as `akan doctor --format json` or the stdio stream of `akan mcp`, stays parseable.
214
+
215
+ A gateway that writes no log file, as in a built image, where `AKAN_LOG_TO_FILE=0` is the default, now prints its children's stderr on its own stderr instead of dropping it, so the stack of a child that crashes there is no longer lost. With a log file, a child's stderr still goes only to the file unless `AKAN_CHILD_STDERR=1`.
216
+
217
+ `akan lint` now explains a stale recipe index in an `AGENTS.md`, and a recipe with no variant to pick, in English instead of Korean. It also names each such recipe once, where macOS and Windows listed it twice.
218
+
219
+ In the release candidates, `AKAN_BUILD_METAFILE=0` and `AKAN_BUILD_METAFILE=false` still wrote the bundler's metafiles. Now `0`, `false` and an empty value leave them off.
220
+
221
+ `akan test <lib> --write false` no longer syncs the library first. Like `akan typecheck`, it scans the library without rewriting its generated files.
222
+
223
+ - 63479ae: A generated store action whose verb the slice leaves closed (`cru: None`, or a verb set to `false`) now fails with `base.error.endpointClosed`, naming the endpoint, instead of `fetch[names.createModel] is not a function`.
224
+ - ce817b6: A websocket that closes while a subscribe is still running no longer leaves the room joined. The close left every room the socket held, and the subscribe then finished and joined one more, so the live sync hub, the pub/sub adaptor and the gateway's room table each counted a subscriber that would never leave, and every later write was still routed to that room. Such a subscribe now joins nothing, and the cleanup its handler registered with `ws.on("unsubscribe" | "disconnect", …)` runs at once — as does one a `message` handler registered while its socket closed.
225
+ - 18f0bd8: Fixes from a CodeQL code-scanning and code-quality pass:
226
+
227
+ - The `"use client"` check that the build runs on every module no longer backtracks exponentially on leading block comments (about 30 one-line comments hung the build), and it no longer reads `/* a */ x /* b */ "use client"` as a directive.
228
+ - A package `exports` wildcard target with more than one `*` has every `*` replaced, as Node does, in the build's resolver and the barrel-imports plugin.
229
+ - A tunnel agent ignores a gateway heartbeat outside 1 s to 5 min and keeps the 30 s default; Bun runs a NaN, negative or over-2^31 interval every millisecond.
230
+ - The in-page agent's screen text prints no `data:` or `vbscript:` link, in any letter case, as it already printed no `javascript:` one.
231
+ - A markdown link label or a mention label no longer spans a `[` or `@[` where another one starts: `[a [b](u)` links `b`.
232
+ - `web_fetch` decodes `&amp;` last, so `&amp;lt;` reads as `&lt;`, and strips `</script >`-style end tags.
233
+ - Native: `google_services.xml` escapes backslashes, a share refuses a `vbscript:` link, and the dev shim escapes `<`, `>`, U+2028 and U+2029 in the values it inlines.
234
+ - `trimSlashes` and `trimTrailingSlashes` in `akanjs/common` trim in linear time; the framework's slash, email, translation and route-parsing regexes are now linear with the same results.
235
+
236
+ - b1165ba: An endpoint's middleware chain is composed once — per global middleware set, env and endpoint — and reused by every call, instead of being rebuilt from the middleware map and re-resolving each handler on every call; a middleware whose setup rejects still fails only that call and is set up again on the next. A call skips the cache, idempotency or rate-limit step when the endpoint declares no cache, is not an HTTP mutation, or has no budget. Measured in process, a bare query went from 2.3 to 2.0 µs per call.
237
+ - 63479ae: A conditional `updateOne` / `removeOne` (`updateOneByQuery`, `removeOneByQuery` and the generated `updateOne<Filter>` / `removeOne<Filter>`) is a compare-and-set on Postgres too: of two concurrent calls that both read the row as matching, only one applies and reports `modifiedCount: 1`. Before, Postgres re-checked a row another write had just changed without the query's condition, so both applied — a balance guarded by `balance >= amount` could go negative.
238
+ - bea9eb7: In a CSR bundle (a native app, or `?csr`), `usePage().path` called in a `page()` / `layout()` render, or in a route's `.loading()`, now returns the path of the screen that render draws. A pushed screen renders before the URL moves to it, so it used to read the previous screen's path: a tab layout that hides its tab bar on pushed screens left the bar, and the bottom inset it reserved, on the pushed screen. The path holds until the render's first `await`, which is why `usePage()` comes first in a render. A component rendered below the route, and a legacy `export default function` route module, still read the URL.
239
+ - ce817b6: A CSR frame's router keeps working on the stack it holds after the first render: `router.init` takes the frame's router once at mount, and every later `push`, `replace` and `back` used to run that first render's state, so a `replace` dropped the page under the current one and a `back` skipped the frame's snapshot when the app opened on a page with no transition. A stack or bottom-up back now slides by the frame's current width after a resize or a rotation, and an empty top-left action slot no longer catches clicks meant for the navbar's left edge on a page with no back button.
240
+
241
+ An RSC navigation's head patch anchors on and removes only the route head nodes still in `<head>`, and its rollback restores only what it removed. The CSR location keeps every value of a repeated query key, an empty one included (`?tag=&tag=b` reads `["", "b"]`, not `"b"`).
242
+
243
+ - a6577eb: A CSR app — a native app's bundle — now draws a not-found page instead of failing. A path no page matches draws under the layouts it sits under, as on the server: the deepest `.notFound()` among them, or a default page with a way back, once those layouts' guards let it in. `router.notFound()` moves to `/404`, which draws the same way. Before, a guard's `router.notFound()` (or a 403 or 404 from a query in a guard) threw `Not found: /en/404` and left the refused page blank, and a push to an unmatched path threw at its caller.
244
+ - 297f437: A CSR app — a native app's bundle — no longer shows a page it kept for another account. `setAuth` and `resetAuth` now tell the frame, which lets go of the pages it kept off screen for the previous account (the entries kept for going back and every `cache` route), so each is drawn for the new account when it is next shown. It also draws the page under the current one again, so a swipe back shows the new account's page. The page on screen stays as it is until it is left. Before, signing in landed on `cache` routes and back-stack pages as they were drawn while signed out, and a sign-out that did not reload kept the previous account's pages.
245
+ - 531ab68: A CSR app — a native app's bundle — no longer leaves a blank page when a route guard refuses a page that slides in. Until such a page shows, a newer navigation takes its place, so a guard's redirect lands where the page would have, and the page never appears. A navigation made while a page slides in now runs once it has settled; before, it was dropped. Before, a redirect to a page with a transition was dropped and the refused page stayed blank, and a redirect to one without a transition still had the refused page pushed on top of it. A back while a page is being prepared now cancels that page, and an animated replace no longer leaves the replaced page under the new one.
246
+ - 8bf9f8e: An index that two declarations build together, such as `{ code: 1 }` and `{ code: -1 }`, or a filter's sort and its reverse, is no longer rebuilt when the declarations trade places. Before, reordering them, or a rolling deploy that mixed two releases listing them in different orders, logged "changed its descriptor and is rebuilt" and built the whole index again at boot.
247
+
248
+ Reordering the fields of a compound index, or of a filter's compound sort, builds the index in the new order and drops the old one; the 3.1.0 release candidates kept the index in the old order and never built the new one. An index an earlier release built under another name is reused only when the database holds it with the declared fields in the declared order, so when 3.0.2 left copies of an index in both orders, the first boot after the upgrade keeps the one in the declared order.
249
+
250
+ An index made outside akan under the name akan gives a declared index is taken for that index only when the database holds it as exactly that index. Otherwise the boot warns, leaves it alone, and builds the declared index under the name with a suffix. Before, akan took any index of that name for its own, so the declared index was never built, and the 3.1.0 release candidates dropped the hand-made one once nothing declared it.
251
+
252
+ In the 3.1.0 release candidates, an index `where` that tested an array field for a member, as `{ tags: q.has("vip") }`, `{ tags: "vip" }` or `q.oneOf` on an array field do, was built on Postgres, while SQLite failed to boot with "subqueries prohibited in partial index WHERE clauses", which named neither the index nor its table. Both now stop the boot with an error naming them.
253
+
254
+ In the `single` database mode, a queued job whose process died or hung past its lease on its last attempt is now marked failed and handed to the worker's `onFailed`, as a stalled job reaches it in the `multiple` and `cluster` modes. Before, it was claimed and run again after every lease, past its `attempts`, and `onFailed` fired only if a later run threw. Upgrade note: a job left at the default single attempt is no longer run again after its process dies; give it `attempts: 2` or more to keep that retry.
255
+
256
+ - bd6f9e6: Dependency refresh for the published packages:
257
+
258
+ - **React 19.3.1** (`react`, `react-dom`, `react-server-dom-webpack`), together with the 19.0.9 / 19.1.10 / 19.2.9 backports of the same day.
259
+ - **Majors**: `chalk` 6, `commander` 15, `fontaine` 1, `firebase` 13 (optional peer), `happy-dom` / `@happy-dom/global-registrator` 21 (optional peers; CSS and iframe loading are now off by default).
260
+ - **Minors and patches**: `lexical` and every `@lexical/*` 0.52, `typebox` 1.3.37, `@biomejs/biome` 2.5.14, `@inquirer/prompts`, `@tailwindcss/node` / `tailwindcss` 4.3.3, `dayjs`, `immer`, `ignore`, `ora`, `string-width`, `subset-font`, `tailwind-merge`, `tailwind-variants`, `protobufjs`, `react-icons`, `@react-spring/web`, `@playwright/test`, `@types/bun`, and `bullmq` / `ioredis` to their latest 5.x.
261
+
262
+ Held back on purpose: `typescript` 7 (no compiler API, which devkit's scanners and transforms need), `ink` 7/8 (drop bracketed-paste markers and mouse reports from `useInput`, which `akan code`'s paste and wheel scrolling read), `ioredis` 6 (RESP3 by default) and `bullmq` 6 (backend rewrite), which need the conformance suite against real Redis, and `@biomejs/biome` 2.5.15 (reports `Object.keys(…).sort()` and other string sorts under `useArraySortCompare`).
263
+
264
+ Upgrade note: a workspace pins `react` / `react-dom` to `19.3.1` to match the `akanjs` peer range.
265
+
266
+ - ce817b6: A desktop app built with `native.desktop.recovery: "reload"`, the setting of an app nobody attends (a kiosk, a signage screen), keeps its pages from opening a way out of its windows:
267
+
268
+ - **No print dialog.** On Windows `window.print()` does nothing in the app's pages, their frames (a frame a script makes included) and external views, and a PDF a page shows has no save or print button. On macOS the shell refuses the print a page asks for with `NOT_ALLOWED`. On Linux a page's print opens no dialog.
269
+ - **No file dialog or share sheet.** On Windows and macOS a file input, its `showPicker()` and the File System Access pickers open nothing, and `navigator.share()` is refused. On Linux a file input opens no file chooser.
270
+ - **No other program.** A link or `window.open` a page would hand to the OS (`http`, `https`, `mailto`, `tel`, `security.shell.externalSchemes`) is logged and dropped, and on Windows WebView2 hands no scheme to another program. On macOS a page's right-click menu has no Search With Google, Look Up, Translate, Quick Note, Writing Tools or Services, which hand its text to another app.
271
+ - **No download** from a page on Windows.
272
+
273
+ What the app opens through its opener and file-picker plugins is unchanged.
274
+
275
+ Upgrade note: an app with `desktop.recovery: "reload"` whose pages relied on a print dialog, a file input, a download or a link opening in the system browser no longer gets them; open those through the opener or file-picker plugin instead.
276
+
277
+ - ce817b6: Desktop window and external view fixes:
278
+
279
+ - An external view's `evaluate`, `sendKey` and `capture` fail with `TIMEOUT` after 30 s when the view's page never answers, as a page busy in a loop does.
280
+ - `window.setSize`, `setPosition`, `setMinSize` and `createWindow`, and an external view's `bounds`, refuse a value beyond one million logical pixels with `INVALID_ARGS`, as `placeOnDisplay` refuses device pixels; such a value used to wrap into a window off every display. `setMinSize` given a size that is not a number is refused.
281
+ - Windows: showing the pointer again over an external view shows it over the view's frames too, and hiding it hides it over a frame that loaded while it showed, without waiting for the frame to navigate.
282
+ - macOS: hiding or showing the pointer over a window reaches the frames of another origin its page shows.
283
+ - Windows: a frame in an external view opens no other program by scheme (`mailto:`, `ms-settings:`, a custom one), as on macOS, and WebView2 does not offer to.
284
+
285
+ - ce817b6: A redirect or not-found a layout or page body raises before awaiting any I/O is now a real 3xx / 404 in a streaming response too: the RSC worker holds its first row one task, which is when Flight settles a body that awaited nothing. One raised after I/O still navigates in the page (HTTP 200) unless the route takes `.config({ ssr: "block" })`, and the ssrRule and conventions guidelines now say so instead of "before any HTML is sent". The frame's bottom safe-area strip is painted only under bottom chrome or a route's own `bottomSafeAreaColor`, so a page's `fixed bottom-0` bar paints its own safe-area padding.
286
+ - 77411ef: A `Load.Edit` or `Model.EditModal` form filled from its page no longer saves that fill as a form draft. Before, the store took the fill for typing and, 400 ms later, saved it under the draft of the record open before it: coming back to an edit page with Back could delete its draft when the page's refresh took longer than that, a refresh bringing a newer version deleted the draft the conflict bar had just offered, and opening another record could save its form as the previous record's draft, which then kept that record's editor from opening and, re-saved on every visit, never expired. Keystrokes still waiting to be saved when the form refills now land under the draft they were typed for.
287
+
288
+ A draft write still waiting when `new<Model>`, `edit<Model>` or `load<Model>FormDraft` opens another form, or the same record at a newer version, is saved under the draft it was typed for. Before, it ran against the newly opened form instead, which lost it and could delete that form's draft.
289
+
290
+ A record's draft that 3.0 saved with another record's form is dropped when the record next opens, instead of filling its form.
291
+
292
+ - 70ebbb4: An open `Load.Edit` or `Model.EditModal` form keeps what was typed when its page re-renders with the same seed, as it does on every `router.refresh()`. Before, the editor refilled the form whenever its `edit` prop was a new object, and every refresh hands it one.
293
+
294
+ - The editor now refills only when the seed itself changes. For an edit payload, that means the record's id and `updatedAt`, not `<model>ViewAt`, which is stamped on every fetch. For a new form, it means the seed's values.
295
+ - A record that changed on the server still refills, and an enabled draft is still offered back through the conflict bar.
296
+ - While any form of that model is open, an equal seed leaves it alone. Once a submit or a cancel has closed it, an equal seed opens a fresh form, as before.
297
+
298
+ - ce817b6: An external view shows `native.backgroundColor` until its page's first frame, and a Windows app's executable names the app as its company when `desktop.publisher` is unset instead of keeping Bun's own ("Oven"), which Explorer and Windows Firewall's prompt showed.
299
+ - ce817b6: Fetch client fixes:
300
+
301
+ - **Multipart arguments.** Beside a file upload, every argument but a lone `String` or `ID` is sent as JSON, so a string bound for `Any` arrives as that string, where one that read as JSON (`42`, `true`) was parsed into another type.
302
+ - **A gateway's own error.** A JSON 408, 429, 502, 503 or 504 that names no akan error key, such as a proxy's `{"message": …}`, shows the translated transport error and is retried like the gateway's HTML page.
303
+ - **`Retry-After: 0`** on a proxy's own 429 means retry now (`seconds: 0`), not in 60 seconds.
304
+ - **A refused websocket URL** is logged and retried on the reconnect backoff, instead of leaving the client in `connecting`.
305
+ - **A clone of a foreign clone.** On the server, a clone of a clone that already points at another origin opens no websocket unless `connect: true` is passed, as a clone given that origin never did.
306
+ - **A clone keeps its parent's default timeout**, as it keeps the parent's websocket options and retry policy.
307
+ - **`destroy()`** sets `connected` to `false` at once.
308
+
309
+ Upgrade note: deploy client and server together. A server older than this release does not parse a multipart argument back from JSON, so a quoted string reaches it.
310
+
311
+ Upgrade note: a server-side clone of a clone pointing at another origin no longer connects a websocket by itself; pass `connect: true` where one is needed.
312
+
313
+ - b1165ba: A response is resolved and serialized from a field plan built once per model class (`FieldPlan` in `akanjs/constant`) instead of walking `FIELD_META` and its getters for every object, and a return type with no relation or `resolve` field anywhere below it resolves in one synchronous pass instead of awaiting each row and each embedded scalar. Measured in process, a 20-row list of a six-field scalar went from 16.0 to 6.5 µs per call. The output is unchanged.
314
+ - ce817b6: A frame slot whose inset is 0 at a width is hidden from the keyboard and screen readers as well as clipped, so a phone-only bar needs no `md:hidden`. A redirect a page raises after the RSC stream started is a real 3xx when the host still holds the response (a `.config({ ssr: "block" })` route, a crawler, or one raised before the shell was ready). `Layout.BottomTab` renders a `<nav>` named by `label`, and a tab's `content` / `activeContent` draw it from a server component. `router.back()` reports the page it landed on rather than `document.referrer`, and a CSR bundle's store holds the URL's query before its first page renders. The cssRule safe-area note now says the web reads `env()` only where the route chain declares `safeArea`.
315
+ - ce817b6: Frame chrome on an SSR page keeps the DOM the server drew: `Layout.Navbar`, `TopInset`, `TopLeftAction` and `BottomInset` hydrate where the server put them, in their slot's box, and that same DOM then moves into the frame slot instead of being remounted there. Text typed into the chrome before hydration, the focus and the caret survive hydration, and the chrome's state and effects no longer start over. While a page shows top chrome, an empty top slot is transparent and lets clicks through after hydration too, not only before it, so an empty top-left action slot no longer swallows clicks at the start of the bar.
316
+ - b1165ba: The gateway restarts a replica whose unix socket can no longer be opened on Bun 1.4.3 and later, which report the
317
+ errno (`ENOENT`, `ECONNREFUSED`, `ENOTSOCK`) where Bun 1.4.0–1.4.2 reported `FailedToOpenSocket`. Before, such a request
318
+ answered a raw 500 and the replica stayed unreachable.
319
+ - 9ee3a60: Guideline text (`akan guideline show <name>`, MCP `get_guideline`) catches up with 3.1.0:
320
+
321
+ - `workspaceOnboarding`, `conventions` and `ssrRule` gate a signed-in layout with `.guard(() => signedIn())` from `@libs/shared/webkit`, which a new workspace has only with `create-workspace --libs true`. `ssrRule` says a crawler gets a late redirect as a real 3xx.
322
+ - `queryRule` and `workspaceRecipes` count `purge<Filter>` among a filter's methods, and `queryRule` says which queries a purge refuses.
323
+ - `workspaceRecipes` and the `akan-store` skill list `invalidate<Model>()`, and the `workspaceRecipes` page sample no longer awaits its view.
324
+ - `transportRule` adds a 408 and a 409 `idempotencyInFlight` to what `retry` retries.
325
+ - `modelDocument` points search at a `text` role rather than a `"text"` index.
326
+ - `runtimeRule` says an edge server starts with `/mcp` off and that `operationMode` scopes whole modules.
327
+ - `workspaceOnboarding` says only `--sign true` code-signs a `publish-update` release.
328
+ - `conventions`, `runtimeRule` and the READMEs name Bun 1.4.4, and the `mcp`, `apps` and `modern` server options describe the edge default and MCP Apps as shipped.
329
+
330
+ - ce817b6: A route's `.head()` that throws — a guard refusing a signed-out visitor the record a title reads — now renders without a head and is warned about, instead of failing the request before the layout's `getSelf({ unauthorize })` redirect can answer; a redirect or not-found raised in a head still routes.
331
+ - ce817b6: A page no longer inherits a `<link rel="canonical">` or a `<meta property="og:url">` from any layout: each names one URL, so only the page's own `.head()` sets them. Upgrade note: a page that relied on a layout's canonical or `og:url` now renders none; declare them in the page's own `.head()`.
332
+ - 6294981: A back or forward move in an SSR app lands where the page was left. The RSC client re-renders the entry it moves to inside `popstate`, which React commits synchronously, so a long page replaced by a shorter one was clamped to the shorter page's bottom, and only the browser's own restoration, run after the event, moved it back. WebKit does not save the offset of an entry `pushState` leaves once the page has loaded, and skips the step after a user scroll, so in Safari the page could stay at the bottom. The client now keeps each entry's offset (in its history state, so a reload or a document loaded back into the entry restores it too), restores it in the commit that shows the page, and holds it while images and streamed sections size themselves; entries it has left are `history.scrollRestoration = "manual"`.
333
+ - 113fab2: `Href` no longer calls `URL.canParse`, which Safari and iOS Safari only ship from 17, so `Link` renders again on iOS 16 native shells. `CsrLink` resolves an external href against `document.baseURI`, so a `<base>` element is honoured. On a native `app://` base, an authority-less URL such as `https:example.com` opens as written instead of under `https://localhost`. A network path that names no host (`//`, `//:80/x`) stays in-app instead of throwing on click.
334
+ - ce817b6: A mutation repeated under its `Idempotency-Key` replays the response it sent, byte for byte, never its handler's result. What the cache backend (Redis, or the SQLite-backed cache) keeps for 24 hours is that answer as it went out — relations and computed fields resolved, `field.hidden` and `field.secret` values masked, serialized — so neither value reaches the cache. A run whose handler succeeded but whose answer then failed (a relation that no longer exists) keeps that failure for the same 24 hours, a transient one (a database blip while a relation is read) included, so a repeat answers it rather than running the handler twice. A call through MCP is never deduplicated: the header belongs to its JSON-RPC request, not to one tool call.
335
+ - 2cb9c0f: `Image` and `CsrImage` no longer write `placeholder` and `blurDataURL` onto the `<img>`. A file with `abstractData`, or a caller passing either prop, used to make React log "React does not recognize the `blurDataURL` prop on a DOM element" on every render; neither attribute ever drew a blur, so nothing on screen changes. Both props are still accepted for next/image compatibility.
336
+ - ee01a4c: An iOS app built with the iOS 26 SDK or later launches again on iOS 18.5. The shell called WebKit's Swift `Result` variant of `callAsyncJavaScript`, which the SDK binds to `/usr/lib/swift/libswiftWebKit.dylib` for the iOS 16 deployment target; the iOS 18.5 runtime has no such library, so dyld stopped the app at launch. The shell now calls the Objective-C method, and the app no longer loads `libswiftWebKit.dylib`.
337
+ - ce817b6: Windows desktop apps:
338
+ - An uninstall no longer leaves the app's exe behind when a watchdog starts the app again meanwhile. The stop step kills whatever starts from the folder until nothing runs, and deletes the exe in that same pass.
339
+ - A setup no longer fails when a watchdog starts the app again between the stop and the folder swap. The stop step moves the install folder to `<folder>.setup-old` in the pass that finds nothing running; the setup then checks the unpacked files and moves the folder back when they are incomplete, before the second rename puts the new files in place.
340
+ - A setup run with `/S` shows no "Verifying installer" window over a kiosk. NSIS shows it whatever `/S` says, so the setup no longer CRC-checks itself.
341
+ - Two launches from different kinds of session no longer both run as the first instance. Before, a launch from an SSH or service-started session and one from the desktop each started their own server on the same data, because the single-instance pipe name hashed USERDOMAIN, which differs between those sessions. The pipe name now comes from the account SID in the process token, read through `bun:ffi`, and from the profile folder only where the token cannot be read. A running instance of the older build is not found across the upgrade, but the setup stops it first anyway.
342
+ - A gate plugin is waited for up to 30 s, not 3, so a slow single-instance check can no longer let a second launch start its server before it exits.
343
+ - The launch phase's own deadline, on every desktop OS, is 45 s, the gate, setup and ready waits added up, where it was 18 s, so a launch whose gate is still deciding is no longer ended early.
344
+ - A setup's or uninstall's stop step that hangs is ended after a minute and counts as a failed stop. A setup then reports the app in use, where before it waited as long as the stop did.
345
+ - The running instance logs one line when another launch hands over to it, with the argument count only, so a launch with no console of its own still leaves a trace.
346
+ - ce817b6: `native.backgroundColor` (one color or `{ light, dark }`) is what a native window shows before its page paints, and the splash's default, so a dark screen no longer flashes white. `native.desktop.publisher` names a Windows app's publisher in Apps & features, in its setup program and in its executable. On Windows, `window.setCursorVisible(false)` hides the pointer over the window's external views too, in every document they load, until it is shown again.
347
+ - b1165ba: A call's `traceId` is spelled the first time something reads it (a log line, the canonical request line) instead of on every call, still stamped with the time the call started, and a context-mode trace — the default — no longer wraps each stage of the call in a span it never records. Measured in process, a bare query went from 3.0 to 2.3 µs per call.
348
+ - ce817b6: `Link.Back` renders a `<button type="button">` instead of a `<div>`, which a keyboard could not reach and a screen reader did not announce as an action.
349
+
350
+ Upgrade note: a `Link.Back` with no `className` setting `display` is now inline-block, as a button is, rather than a block.
351
+
352
+ - 0c1f96b: `Link` recognizes external hrefs by parsing them as the browser does. Any-case schemes such as `HTTPS://` and `MAILTO:`, protocol-relative URLs, `sms:`, `blob:`, app schemes, and authority-less http(s) values like `https:example.com` render and open unchanged instead of becoming `/<lang>` routes. `javascript:`, `vbscript:`, and `data:` are never treated as external.
353
+
354
+ Router navigation and redirects pass explicit HTTP(S) URLs through in any case and keep scheme-relative paths on the site. The shared helpers are public as `Href.isExternal`, `Href.hasWebScheme`, and `Href.toExternalUrl` from `akanjs/client`; `CsrLink` opens the canonical absolute URL, so a protocol-relative link works inside a native shell.
355
+
356
+ - 658e59a: `setLimitOf<Model>(0)` called on page 2 or later lists every row from page 1. It used to set `pageOf<Model>` to `Infinity`, which a later `setLimitOf<Model>(20)` turned into `NaN`, and live rows were no longer placed. In the 3.1.0 release candidates, a `limit: 0` list holding fewer rows than its count, because the server lists at most 500 rows for it, placed a live row that sorts past its last row at its end; it now marks the slice stale instead.
357
+
358
+ `Model.LoadInit` renders nothing, as documented. It used to draw a pager when its slice held more rows than its limit, publish a page tool to the in-page agent, and on a touch screen load the next page into the shared slice by itself, so every list reading that slice showed the extra rows. A slice loaded with `{ insight: false }` also keeps that choice, held in the new `hasInsightOf<Model>` state, until the next `init<Model>()`: a `Load.Units` refetch, `refresh<Model>()` and `setQueryArgsOf<Model>()` no longer count its rows, so a pager the first render did not draw no longer appears.
359
+
360
+ A `.live()` slice keeps its lists current in three cases where they went quiet. Its room now stays open until the last `Load.Units` or `Model.LoadInit` showing the slice unmounts; the first one to unmount used to close it for every list. A list shown again from a CSR stack refetches when its room closed while it was hidden, instead of keeping rows that missed the writes made meanwhile. A row that arrives while a list loads makes the load read its rows once more, where it used to stay missing until the list next reloaded.
361
+
362
+ A `Load.Units` shown again from a CSR stack shows its own rows. When the page pushed over it read the same slice with other arguments, the list kept that page's rows and live room after going back, and made no list call; it now reloads the arguments, page, limit and sort it showed. A list whose slice another list on the same screen took over does the same once that list unmounts. A refetch, page change or `loadMoreOf<Model>()` still in flight when another `Load.Units` seeds the slice no longer writes its rows over the seeded ones.
363
+
364
+ - 5a94516: A slice loaded with `limit: 0` keeps every row when a live event adds one, instead of cutting the list to 20 and dropping the oldest rows, and places a row that sorts past its last one. Its `lastPageOf<Model>` is 1 in the store and in the server-rendered init, where it used to be counted in pages of 20.
365
+
366
+ `Load.Units` with `loading={null}` shows nothing while the slice reloads, as it already did on the first load, instead of the default "Processing" overlay; leaving `loading` out still shows the overlay. `Model.LoadInit` passes `null`, so a layout that holds one no longer dims the whole page each time its slice reloads. Rows rendered with `renderItem` also stay mounted when the slice re-renders, so a row's open menu or local state survives a live event.
367
+
368
+ - 509ba6f: `LlmTurnRequest` takes `model`, so a service that plugs `LlmAdaptorRole` can run one turn on another model, such as the one a conversation picked, without a second adaptor. `OpenaiLlm` and `AnthropicLlm` send it in place of `option.setLlm({ model })`, streamed or not, and `LlmTurnAnswer.model` names it; an empty or missing one falls back to the option's. `accepts`, `maxTokens` and `contextWindow` still come from the option, so a model that differs in them needs an adaptor of its own, built on the exported `OpenaiDialect` / `AnthropicLlm` helpers. The in-page agent's `runAgentTurn` takes no model, so a browser still cannot choose one.
369
+ - ce817b6: - **The logger masks more secret names.** Logger redaction (inline data and `attrs`) and the server env profile's secret scanner now follow one rule, `Logger.isSecretKey(name)`. The logger used to mask only password, token, JWT, authorization, cookie, secret, API key and private key names. It now also masks passphrase, a bare `pass`, `pwd`, access, auth, hash, signing, encryption, master and session keys, webhook, `cred`, names ending in `dsn`, connection string, IAP verify and service account. Before this, a webhook URL, a DSN, a cloud access key or a mailer's `pass` could reach a log record in the clear.
370
+
371
+ - **A malformed pipeline secret name is not echoed.** `PipelineNames.secretNameError` says "the name is not a secret name (…)", and the plan schema and the config reader point at the item by its index (`…secrets[2]`). A reserved name is still named, since it is one of the executor's own.
372
+ - **`@akanjs/native/api` exports `ServerPolicy`**: the `restart` and `watchdog` of a desktop app's carried server. The launcher and the build both read it through one `serverPolicyOf`.
373
+
374
+ Upgrade note: a log search or alert that matched the value of one of the newly masked keys now sees `[redacted]` instead.
375
+
376
+ - b1165ba: An HTTP or websocket call whose argument fails to parse answers 400 with `{ name }`, which the client translates, instead of a generic 500: a non-numeric `Int` or a value outside its `enumOf` answers `base.error.invalidArgument` (`"{name} has an invalid value."`), and an argument left out, or sent as `null` where it is not nullable, answers `base.error.requiredArgument` (`"{name} is required."`). A nullable argument takes `null`, and MCP's refusal text is unchanged. The account guards still run first, so an unauthenticated call with a malformed argument answers 401/403 as before.
377
+
378
+ Upgrade note: a client or test that expected a 500 for a malformed or missing argument now gets a 400.
379
+
380
+ - f6b70d8: An MCP App view whose read is refused with 401 or 403 shows "This screen is not available to the signed-in account.". The view's code catches an error carrying that `statusCode` and the "You are not permitted to perform this action." the model reads. For such a read the server adds only the status to `_meta`: the error key and the guard's message stay on the server, and a read sent with no token still answers the 401 sign-in challenge. In the 3.1.0 release candidates the view said "The page failed to load."
381
+
382
+ A file an MCP App view's tool attaches with `ToolReturn.attach` reaches the host's model as image content only when it is a PNG, JPEG, WebP or GIF (`ImageResult.mimeTypes`), the types the in-page agent sends. Any other file, an SVG included, becomes a note naming those types. In the release candidates any `image/*` type went out as an image block, which the model's provider can refuse, failing the host's whole turn.
383
+
384
+ - 3e275c2: An MCP App view whose read fails says what the tool call would have answered. A record removed since the call reads "No screen exists for these arguments.", a read the account may not make reads "This screen is not available to the signed-in account.", and anything else reads "The page failed to load.". The server's refusals and the view share these three messages.
385
+ - 7b8c6c7: The memory ceilings Akan.js derives from a container's limit (the RSC worker's recycle threshold, the dev host's builder ceiling, `akan start`'s boot concurrency) now read it with `process.constrainedMemory()`, which follows the process's own cgroup. A container started with `--cgroupns=host`, or a server under a systemd unit with `MemoryMax=`, used to report no limit; `/sys/fs/cgroup/memory.max` is read only when Bun finds none.
386
+ - 63479ae: A `memory(...)` declared with a model or scalar class now reads back as an instance of that class, methods included, as its type says. Before, it read back as a plain object. Spreading such a value (`{ ...value }`) now drops its Date fields, which an instance keeps as prototype accessors, so copy it with `new cnst.X().set(value)`.
387
+ - 50afb48: An `@` mention whose title holds a bracket, such as "[Ops] Minutes", is now a chip and is sent with the message. `Reference.token` used to write the label as it was, and the reader stopped at its first `]`, so the token stayed raw text and the value it pointed at was dropped. A label's `\`, `[` and `]` are now backslash-escaped in the token, as in a markdown link, and `Reference.parse` and the composer read them back. A token written before this change still reads, unless its label ends in a backslash. `Reference.pointerOf(match)` turns one match of `Reference.pattern` into its pointer.
388
+ - 4ed48d0: A generated `get<Model>` or a `pick` that finds no live row now answers HTTP and websocket callers 404 `base.error.documentNotFound` instead of a 500 logged as a crash. `NoDocumentError` gains a `toJSON()` that puts only that key on the wire; the server-side message naming the table and the id stays on the server. An MCP call that reads a missing document directly keeps its message (`No <model> found for the arguments given.`), and an MCP App view now also receives the `base.error.documentNotFound` `Err`; one that reaches it through another server's HTTP endpoint now reads it as not found (`The requested item does not exist.`, and `No screen exists for these arguments.` for a page prompt) instead of a server failure.
389
+ - 63479ae: A modal stacks above every overlay already on the page (`overlayZ.modal`, 120), and a dropdown, select, popconfirm or nested modal opened inside one lands in the modal's own root, above its panel, instead of under it on `<body>`. Toasts move to 140 so they stay above modals. A dropdown now closes when an item whose own trigger stops the click is chosen, and a popconfirm opened from a dropdown item leaves the menu open. A click into a text field, select or combobox inside a dropdown's menu no longer closes it. Upgrade note: an app element that relied on a z-index between 10 and 120 to sit above a modal now stays under it.
390
+ - ce817b6: Native app fixes:
391
+
392
+ - **Desktop updates keep the installed app's signer.**
393
+ - On macOS, an app signed with a Developer ID team refuses a downloaded release that the same team did not sign.
394
+ - On Windows, an app whose exe has a valid Authenticode signature refuses a release whose exe isn't signed by the same subject. The check runs in PowerShell, Constrained Language Mode included. Where AppLocker, SRP or WDAC keeps PowerShell from running, a signed app refuses every release, since none can be checked; update such PCs with the installer.
395
+ - An ad hoc or unsigned installed app takes releases as before.
396
+ - A refused release is recorded as failed (`NOT_ALLOWED`) and is not offered again.
397
+ - **Phone web updates packed on Windows** (`akan pack-update`, `akan-native update publish`) list the files in subfolders with `/`. They listed them with `\`, which phones refuse, and their web hash now matches a store build made on another OS.
398
+ - **iOS:** a pending web bundle becomes the trial when the first window asks for its page, not at launch. A silent push that launches the app in the background no longer starts a trial that the next launch rolls back as `relaunch`.
399
+ - **Android:**
400
+ - A destroyed activity's ready timer no longer fires after `recreate()` and rolls back the trial its new activity runs.
401
+ - A downloaded web bundle can no longer be pruned between its move into place and its staging.
402
+
403
+ Upgrade note: build every desktop release the way the installed app was signed, with `--sign` and the same `AKAN_NATIVE_MACOS_*` / `AKAN_NATIVE_WINDOWS_*` identity. A signed installed app now refuses an unsigned or differently signed release, so republish that release signed.
404
+
405
+ - e200ce1: A client navigation in an SSR app shows the not-found view the server renders for it. When a route guard or a `.param()` type refused the page, the page called `router.notFound()`, or no page matched the path, the server sent the nearest `.notFound()` view, but the client dropped it: the visitor stayed on the page they were leaving, with only a console error. The URL now moves and the view shows. It is never cached, so a later visit asks the server again.
406
+
407
+ In an SSR app, a back or forward move that keeps the page on screen, and a shallow `router.push` or `router.replace`, now cancel a navigation still loading. Before, the page that was loading replaced the entry the visitor had just moved to.
408
+
409
+ In an SSR app, a push that the server answers with a redirect, a route guard's for one, keeps the page it left in history, so back returns to it, as it does after a document navigation or in a CSR app. Before, the redirect's target replaced the page being left.
410
+
411
+ A `.head()` that calls `router.notFound()` renders the nearest `.notFound()` view under its layouts' head, instead of the built-in 404 page. Every not-found view that stands in for a refused page now carries only its layouts' head, not the page's own.
412
+
413
+ In an SSR app, `router.canGoBack()` stays true after an in-page `#anchor` link, so `Link.Back` and `router.backOrFallback()` go back instead of taking their fallback. Moving between a page's anchors no longer asks the server for the page again.
414
+
415
+ A CSR app — a native app's bundle — keeps the pages it holds off screen when `setAuth` hands it a token refreshed for the same account. It compares the account the token names, not the token, so a refresh no longer draws the back-stack and `cache` pages again and loses their state. Signing in as another account still does.
416
+
417
+ In a CSR app, `router.push`, `router.replace` and `router.redirect` to an explicit http(s) URL now load it, a replace or redirect in place of the current history entry, and an iOS or Android app opens it in its in-app browser, as `Link` does. Before, the app drew its not-found page and then threw a `SecurityError`.
418
+
419
+ - fe1e27d: A new workspace with the `shared` or `util` library no longer fails `akan typecheck` and `akan build` with TS7016, "Could not find a declaration file", for `react-dom` and `nodemailer`: `akan create-workspace` writes `@types/react` and `@types/react-dom` into its devDependencies, and `akan install-library` adds `@types/react-dom` with `shared` and `@types/nodemailer` with `util`. Upgrade note: a workspace created earlier adds them with `bun add -d @types/react @types/react-dom`, plus `@types/nodemailer` if it installed `util`.
420
+
421
+ `akan install-library` now copies a library from the akanjs version the workspace has installed: one shallow clone of its `v<version>` tag, reused for every library in the run. Before, each library took a full clone of the repository's default branch, whose code can need a newer akanjs than the workspace's, and `akan create-workspace` with both libraries downloaded it twice. A version with no tag, such as a prerelease, still installs from the default branch, with a warning naming the version.
422
+
423
+ `akan` and `bunx create-akan-workspace` now stop on a Bun older than 1.4.4, the minimum their `engines.bun` declares, with one line asking for `bun upgrade`. Before, nothing checked the running Bun, so an older one went ahead and hit the bugs 1.4.4 fixed.
424
+
425
+ The `akanjs` peer on `react-server-dom-webpack` is now exactly `19.3.1`, the same pin as `react` and `react-dom`, and `akan create-workspace` writes it that way. Its caret range let a new workspace resolve the first `react-server-dom-webpack` released after 19.3.1, built for a different React than the one it renders with. Upgrade note: pin `react-server-dom-webpack` to `19.3.1` in an existing workspace as well.
426
+
427
+ - f949da0: `router.notFound()` (and `router.redirect()`) raised before the page sent its first byte now answers 404 (or the redirect) even while a layout is still rendering something async, such as a signed-in user's data. It used to answer 500 with "The render was aborted by the server without a reason.", because stopping the render reported every unfinished part as an error that replaced the not-found. A client that disconnects mid-render no longer logs those aborts as render failures either.
428
+ - ce817b6: The `/mcp` gate, the account layer's `resolveJwt`, `getAccount()` on the server, machine tokens, the ops channel, the log stream and the claims an MCP App view is handed read `Authorization` with one grammar, `bearerTokenOf` from `akanjs/common`: a case-insensitive `Bearer`, one or more spaces, then one token and nothing after it.
429
+
430
+ Upgrade note: a header that carries anything after the token (`Bearer <token> extra`), separates the token with a tab, or leaves out the scheme now reads as no credential to all of them; some used to take its first word and others the whole rest. An `AKAN_LOG_STREAM_TOKEN` holding whitespace, which no bearer header can carry, now leaves `/_akan/app/logs` off with an error at boot.
431
+
432
+ - b1165ba: `/openapi.json` builds its document once per process, on the first request, instead of serializing the registry and the dictionary on every request.
433
+ - ce817b6: Phone update envelopes are checked the same way on every host, and the update report contract is shared.
434
+
435
+ - iOS, Android and the server-side `openEnvelope` check an envelope in one order through the shared kernel (`checkUpdateEnvelope`): a trusted, unrevoked signer, strict base64, a manifest of at most 1 MiB, then the signature. Vectors signed with the RFC 8032 test keys run on TypeScript, Swift and Kotlin.
436
+ - The report a phone posts to `updates.report.url` (events, reasons, what each status does to a batch, Retry-After, the caps and the dedup key) is one contract in the kernel. `@akanjs/native/api` exports `UpdateReportBody`, `UPDATE_REPORT_EVENTS`, `UPDATE_REPORT_REASONS`, `updateReportOutcome` and the `BundleInfo` / `UpdateManifest` / `UpdateEnvelope` types for a receiving server; `BundleInfo.updates` carries `reportUrl`. A batch is dropped only on 400, 413 or 422, and Retry-After is read as whole seconds only; a fraction, a sign or an HTTP date is ignored.
437
+ - A phone reads its report file once and writes it only when it changes, through the same atomic write as its update state.
438
+
439
+ Upgrade note: `updates.publicKey` must be padded standard base64 without line breaks, as `akan-native update keygen` prints it, and so must each of `publicKeys`; 3.0.2 took any form Node could decode, which left an iOS build with no updates. An envelope's manifest and signature must be strict base64 too, or every phone refuses it: `update publish` never writes anything else, but a hand-made signer that wraps lines or drops padding must stop.
440
+
441
+ - ce817b6: Fixes for over-the-air updates on a phone, plus the upload commands they were missing.
442
+
443
+ - **Hung releases:** a release on trial whose page hangs in a script now rolls back while the app runs.
444
+ - The ready timer starts with the page, not when it finishes loading.
445
+ - A page that does not answer gets a fresh renderer: a new web view window on iOS, an ended renderer process on Android.
446
+ - **Downloads:** update downloads ask for `*/*`, so a CDN that injects scripts into HTML responses can't change the signed bytes.
447
+ - **Inlined dictionary:** the dictionary a build inlines into the client bundle is key-sorted, so the same commit gives the same bytes.
448
+ - **`native.devtools: true`:** opens the web inspector of a release build, for an internal test target only.
449
+ - **`akan push-update <app> --platform`:** packs an update and uploads it to the Akan Cloud project, which signs and publishes it.
450
+ - The pack goes straight to the project's storage, so a proxy's request time limit can't cut off a large one. Only the files the pack's manifest lists go up, though a pack folder keeps every earlier pack's files.
451
+ - `--pack <dir>` uploads a folder `akan pack-update` already wrote instead of packing again, so one pack built in CI can be checked, then uploaded as it is.
452
+ - It exits 2 when the pack needs a new store build or the project rejects it, and stops without retrying when the project says the upload was already used.
453
+ - **`akan register-binary <app> --platform --distribution`:** registers the store build `release-*` made, with a pack of the same commit, which goes up and stops the same way. It refuses when the two provide different native APIs.
454
+
455
+ - ce817b6: Toasts, the agent launcher, the open agent panel and the dev-mode Reconnect screen take their z-index from the same overlay scale as menus, popconfirms, tooltips, modals and approval cards, set as an inline style instead of an arbitrary class, at launcher 80, toast 140, panel 150, Reconnect 200. The launcher, at 150 in 3.0.2, now sits under every menu, modal and toast. An `AgentLauncher` override receives the launcher's layer as a new `style` prop.
456
+
457
+ Upgrade note: an `AgentLauncher` override that does not apply `style` to its root no longer gets the launcher's z-index; spread `style` there the way `className` already is.
458
+
459
+ - ac753ff: `<Model.Edit modal="quick">` opens its form; it set the store's modal to `"quick"`, but nothing opened under that name. `Model.Edit` also takes `presentation` and `placement`, as `Model.New` does. A `Model.New` whose form is open under its own `modal` refuses another create, from its trigger or from the agent's `new<Model>` tool, instead of replacing what was typed; before, only a form open under the default modal was guarded.
460
+
461
+ Pressing Escape to close an open `Select` list, or the slash menu or panel of `Agent.Chat`, no longer also closes an open `Modal`. `Modal`, `BottomSheet`, `Popconfirm` and `Tooltip` now leave alone an Escape that a control already handled with `preventDefault()`, so a control of your own can keep them open the same way. Escape in the tag input of `Field.Tags` now only cancels the tag being typed, and leaves the `Modal` around it open.
462
+
463
+ In a CSR stack, a `Modal` or `LegacyModal` left open on a page you navigate away from no longer stays on top of the next page, where it still took clicks, Tab and Escape and kept the page from scrolling. It is hidden while its page is not the current one, and shows again with what was typed when you go back, as `BottomSheet` already did.
464
+
465
+ - ce817b6: `akan pack-update` records each pack in `<out>/pack.json`: app, target, platform, env, channel, app id and version, native API, web hash, file count and size. With `--against`, the record also carries the compatibility result. A pack that needs a new store build now exits 2 instead of 1, after writing `pack.json` and `compat.json`; any other failure exits 1 and leaves no `pack.json`. The same commit, env and target pack to the same bytes.
466
+
467
+ That last guarantee needed an akanjs fix. A library's generated `getSerializedSignal` macro read every signal any other client in the build process had applied, so whether a lib's chunk carried the app's whole registry depended on Bun's transpiler cache. The macro and `FetchClient.from()` now read `ownSerializedSignal`, the signals a client was built with. Merged signals no longer keep keys set to `undefined`. Run `akan sync` to regenerate `lib/sig.ts`.
468
+
469
+ Upgrade note: a CI step that took exit 1 from `akan pack-update --against` to mean "needs a new store build" checks for 2 now, since 1 is any other failure. Run `akan sync` once on every app and library, so its generated `lib/sig.ts` reads `ownSerializedSignal`.
470
+
471
+ - 6e5a402: `schema.index(fields, { where })` and `createIndex(name).where(...)` build a partial index on SQLite and Postgres (`CREATE INDEX … WHERE …`); the predicate used to be stored and never reach the DDL, so the index covered every row. With `unique`, `where: { removedAt: null }` lets a soft-removed document give its key back. The predicate's values are written into the DDL as literals, since neither dialect binds a parameter there, and a `where` no index predicate can hold (`q.search()`, or a test for a member of an array field such as `{ tags: "vip" }`) fails the boot with an error naming the index and its table, instead of building a whole index. Upgrade note: an index that already declared a `where` is rebuilt once on the next boot, replacing the whole index an earlier version built from it (on a Postgres table that holds rows, built concurrently beside it and swapped in); an index without a `where` keeps its descriptor hash and is not rebuilt.
472
+ - 3b7e57d: `akan build` writes `FROM oven/bun:1.4.4-slim` when an app declares no `docker.image`. The default was `oven/bun:1-slim`, a floating tag that pulled whichever Bun was newest at build time, so a production image could run a Bun release the framework was never tested on. To run another Bun, set `docker.image`.
473
+ - 329bd79: A model with its own `kind` field can be queried by it. `{ kind: "image" }` used to be read as a query node because it carried a `kind` key. The SQL compiler then threw "Operator nodes must be attached to a document path", a live room never matched, and an upsert inserted the row without the query's fields. A value now counts as a node only when it has a node's whole shape, so `{ kind: "image" }`, `{ kind: q.oneOf([...]) }` and `{ kind: "all" }` are field queries. The check is exported as `isDocumentQueryNode`.
474
+ - ce817b6: A response no longer fails when a relation points at a removed document. A relation list leaves the target out, an optional single relation reads `null`, and a required one answers 404 `base.error.relationNotFound` naming the model and the field path instead of an opaque 500. Upgrade note: `SignalContext.loadNested` takes `{ arrDepth }` only and reads a missing target as `null`.
475
+ - 6b5646e: - The akan CLI runs in a workspace with no root `.env`: the workspace root is the folder with `package.json`, `tsconfig.json` and an `apps/`, `libs/` or `pkgs/` folder. Unset, `AKAN_PUBLIC_REPO_NAME` falls back to the origin remote's name and `AKAN_PUBLIC_SERVE_DOMAIN` to `localhost`; `akan build` and the redis/postgres tunnels refuse either fallback for any environment but `local`.
476
+ - The theme contrast check reads every theme block. It used to read every other block, and none after an `@source` line, so on the framework stylesheet it checked only the light theme. `akan lint` may now report contrast failures in a `:root` or dark theme that were always there.
477
+ - `declaredCustomProperties` skips a whole `@theme` block, nested `@keyframes` included, and ignores tokens inside comments. Theme CSS and the generated `useClient.ts` are parsed in linear time.
478
+ - `akan agent install` writes an `AGENTS.md` about 60% smaller: the conventions guideline keeps the rules and points at the on-demand guidelines for the detail, and a new `pageRule` guideline covers routing.
479
+ - 2c5b833: A not-found raised by a page no longer renders a blank 200. A path value its `.param()` type refuses is checked before any byte is sent, so it answers 404 with the nearest layout's `.notFound()` view. `router.notFound()` from a page or layout body shows that view in place of the route that raised it; the status is 404 wherever the response is still held, which includes crawlers and `ssr: "block"` routes, and 200 once a browser's stream has started. Such a page is never cached.
480
+ - 6183f95: `router.push` and `router.replace` now refuse an absolute URL on another origin, so a `?redirect=` handed to them unchecked can no longer send a visitor off-site. A refused call stays on the page, warns, and rejects `router.navigation()`. `router.redirect` sends such a target to the index path instead, on the server and in the browser. Pass `{ external: true }` to leave on purpose. Paths, same-origin URLs and `//host` (read as a path) are unchanged.
481
+ - 8201174: A page the RSC navigation cache kept is no longer replayed for another account. The cache remembers the auth cookies its pages were rendered with, and a navigation that finds different ones — after `setAuth`, `resetAuth`, or a sign-in or sign-out in another tab — drops every kept page and asks the server again. Signing in no longer lands on the signed-out render of a page visited earlier, and going back after a sign-out no longer shows the previous account's page. With `AKAN_PUBLIC_RSC_PARTIAL_COMMIT=1`, the first page after the change is rendered whole rather than patched into layouts drawn for the previous account. The page on screen stays as it is until the next navigation or `router.refresh()`.
482
+
483
+ A redirect raised while rendering a document — `router.redirect`, `getSelf({ unauthorize })` and the like — now carries `Cache-Control: no-store`, so no proxy or CDN in front of the app keeps it for the next visitor.
484
+
485
+ - ce817b6: Runtime fixes:
486
+
487
+ - **A disabled module takes its relations with it.** A module whose model relates to a module that `enabled: false`, `disableModules`, `disableLibs` or an operation mode left out is not mounted either, where it used to mount and answer 500 for every response that resolved the relation.
488
+ - **An untyped queue backoff waits.** `backoff: { delay }` without a `type` now waits its delay on bullmq too, as it did on SolidQueue; bullmq used to throw.
489
+ - **Fonts in `public/`** are served with `Access-Control-Allow-Origin: *`, so an app's `@font-face` loads in an MCP App view.
490
+ - **Router:**
491
+ - A basePath is stripped only on a segment boundary: under basePath `office`, `/officers` stays `/officers`.
492
+ - `canGoBack()`, `backOrFallback`, `Link.Back` and the Android back button take the fallback where going back would do nothing, such as the first screen after a reload.
493
+ - `back()` with nothing to go back to no longer posts a `pathChange`.
494
+ - **BottomSheet:**
495
+ - A closed sheet is hidden, so Tab no longer reaches its controls.
496
+ - A controlled sheet whose parent keeps `open` springs back after a drag-dismiss.
497
+ - On a page that is not current in a native page stack, the sheet hides and gives up Escape and the scroll lock until its page is current again.
498
+ - **WebSocket client:**
499
+ - The reconnect count resets only after a socket stayed up 5 seconds, so a gateway that accepts and drops every upgrade reaches `maxAttempts`.
500
+ - Going online or making the tab visible reconnects at once.
501
+
502
+ Upgrade note: a module that relates to a module you disable is now left out as well. The `verbose` log names it ("relates to disabled module …"); disable it on purpose, or keep the related module enabled.
503
+
504
+ - bb7a8cd: - **Security: the account layer reads `Authorization` as `/mcp` does.** `resolveJwt` takes the token with `bearerTokenOf`, so a header carrying anything after it (`Bearer <token> extra`) reads as signed out. The release candidates took its first word there: such a call ran signed in while `/mcp`'s gate, reading no token, skipped its audience, scope and `verify` checks.
505
+ - **`cred` is masked as a word, not a substring.** `Logger.isSecretKey` matches `credential`, and `cred` or `creds` as a word of a name (`regcred`, `awsCreds`, `AWS_CREDS`). `credits`, `usedCredit` and `creditBalance` stay readable in log data and attrs, and a server env profile no longer strips or refuses a value under such a name.
506
+ - **Security: a server-side call's transport failure stays on this server.** When a call reached no server or got a proxy's own page, its `base.error.serverUnreachable` (or `serverUnavailable`, `gatewayTimeout`, `unexpectedResponse`) carried the runtime's message (`getaddrinfo ENOTFOUND <internal host>`) or the page as `details`, and an endpoint that let it propagate sent that to its caller. The server now logs it at `warn` with the call's origin and path; the error keeps `details`, but its `toJSON()`, which every transport answers with, leaves it out.
507
+ - f5c1f70: On SQLite, a statement waits behind another request's open transaction only when SQLite says it writes: Akan.js now asks the prepared statement (`readonly` and `reader`, new in Bun 1.4.4) rather than matching a leading `SELECT`. A read spelled `WITH … SELECT`, `VALUES`, a `PRAGMA` that reports a value, or a `SELECT` behind a comment no longer waits for that transaction to commit.
508
+ - b1165ba: A server-rendered page request does less work before it renders: about 16% more requests a second for a cached page of `apps/minimal` (26.7k to 30.8k on one machine, p99 3.7ms to 3.2ms).
509
+
510
+ - The route-seed index is matched against patterns split once per index, not once per entry per request; a dev rebuild's new index compiles a new matcher. `RouteSeedIndexStore.match` / `matchPrefix` answer as before.
511
+ - A production server answers an extensionless path (a page) from the list of `public/` files it found at boot instead of a `stat` per request. A path with an extension is still checked on disk, so a file written to `public/` at runtime is still served.
512
+ - `parseAkanI18nEnv()` keeps its last answer while `AKAN_PUBLIC_DEFAULT_LOCALE` and `AKAN_PUBLIC_LOCALES` read the same, and the config it returns is frozen.
513
+ - `TrustedProxy.isTrusted` keeps its verdict per peer address (up to 1024), cleared by `configure` and `reset`.
514
+
515
+ - b7e969e: Security: a server render reads only its own request. Once the SSR shell had gone out, a boundary React resumed later — a client component streamed in behind a Suspense fallback, or one that suspended on `use()` — rendered outside any request scope, and `cookies()`, `headers()`, `getRequest()` and `getAccount()` fell back to a process-wide stack holding whichever render had started last. Under concurrent traffic that was another visitor's request: their cookies, their session token, their basePath. The same fallback answered any read made outside a request on the server while a page was streaming, so a server-side `fetch.*` from a queue job or a websocket handler could forward that visitor's session. The host now decodes the Flight stream inside the request's scope and renders HTML with React's edge server build, whose scheduler carries that scope (the browser build resumes a boundary from a `MessageChannel` task, which runs with none), and the fallback stack is gone.
516
+
517
+ The not-found and error documents read their own request too. A route that failed before its first byte rendered its fallback outside the request's scope, so the theme cookie, `x-base-path` and head code could read another request in flight.
518
+
519
+ Upgrade note: `pushRequestFallback` is no longer exported from `akanjs/fetch`. A read outside every request scope now sees no request — `getRequest()` is `undefined`, `cookies()` and `headers()` are empty — instead of whichever render started last; code that needs one runs inside `requestStorage.run(request, …)`.
520
+
521
+ - 471665c: An index declared without a `name` is now named by its table and fields, with `_unique` for a unique index (`pcLog_removedAt_at`), never by its position. Before, the indexes made for a filter's sorts and for cascades came after every `_onSchema` index, so adding any index renamed them all, and each boot built the renamed ones again beside the old copies, which every write kept updating.
522
+
523
+ - An index akan built that nothing declares any more is dropped on the next boot. On Postgres it is dropped concurrently, without blocking the table. An index made by hand, outside akan, is never touched.
524
+ - After the upgrade, an index an earlier release built under its old name keeps serving as it is, without a rebuild. Its extra copies are dropped.
525
+ - Two declarations of the same index build it once. Two different indexes given the same `name` stop the boot.
526
+
527
+ - 92a62ca: A web app opened from the home screen (`display-mode: standalone`) no longer rubber-bands its whole document on iOS, also when an inner scroller reaches its end and hands the drag to the document: `html` and `body` get `overscroll-behavior-y: none` there, in `@layer base`. A page in a browser tab keeps the browser's behavior. An app overrides either way from its own CSS or with an `overscroll-*` utility on `html` / `body`.
528
+ - 181789d: `AppProviderProps`, the props of `System.App`, is now exported. Without it, `akan build-package akanjs` failed while emitting declarations with TS4023: `System` could not name the type of its `App` member.
529
+ - a1b32c7: `Tab.Panel` now reads its `loading` prop on every render, so a change applies at once, even while another menu is shown. Switching to `"every"` while hidden unmounts the panel, along with its agent tools. Switching to `"eager"` mounts it. Switching to `"lazy"` keeps whatever is mounted. A `"lazy"` or `"every"` panel is now on screen in the same commit that selects its menu; before, there was one commit with the panel still empty.
530
+ - ca888e7: Switching the theme in the app now writes new `<meta name="theme-color">` tags instead of changing the `content` of the ones on the page, so iOS repaints the status bar behind a home-screen app (and Safari's toolbar) right away rather than on the next load. Tags the document rendered itself are switched off with `media="not all"` and left in place; tags the client wrote are replaced, and the same colors write nothing.
531
+ - 63479ae: - `Select` given an `enumOf(...)` class lists each value under its dictionary name, as `Field.ToggleSelect` already did; a value the dictionary does not name shows as is.
532
+ - `Model.ViewEditModal` shows its edit button and remove menu only for the verbs the slice's guards open.
533
+ - A one-argument `adapt(name)` keeps the `Adaptor` members on the class that extends it, so `plug(TheClass)` typechecks without a cast.
534
+ - The app template's `text-primary` sugar no longer blanks a descendant that has a background of its own; it takes the primary color back unless it names a text color.
535
+ - The app template's `env.client.type.ts` declares `AppClientEnv` as `Record<never, never>`, which passes lint.
536
+ - ce817b6: An `unsubscribe` sent while that room's `subscribe` was still running its guards or handler no longer leaves the room joined until the socket closes. The unsubscribe found no room to leave, and the subscribe then finished and joined one, so the live sync hub, the pub/sub adaptor and the gateway's room table counted a subscriber its client had already let go. Such a subscribe now joins nothing, answers `subscribe: false`, and runs the cleanup its handler registered with `ws.on("unsubscribe", …)`, as an unsubscribe after the join would; a subscribe sent again after the unsubscribe still joins.
537
+ - 587d530: `useDebounce` keeps one timer per component and runs the callback from the latest render. A `states` list that changed between calls used to start a new timer without clearing the old one, so a searchable `Select` with `onSearch` searched on every keystroke; it now searches once, after typing stops. A call still waiting at unmount runs then, instead of firing after the component is gone, where `Model.EditModal`'s submit check landed on whatever form the store held next. `states` is no longer read.
538
+ - b1165ba: A websocket no longer re-verifies the same credential on every frame. `MachineAccount` keeps a socket's verdict on the machine token it presents until the token changes, the token's expiry passes, or the machine keys in the env change, so a device streaming frames pays one EdDSA verify per connection instead of one per frame. `libs/shared`'s `AccountMiddleware` does the same for a session's JWT signature, up to its `exp`. Revocation is unchanged and still checked on every frame: a revoked session's socket loses access at its next frame, and a machine is still revoked by the version its guard compares.
539
+
540
+ `MachineToken.proofOf(authorization)` returns the identity together with the second it lapses at, and `MachineToken.keyEpoch()` moves whenever the keys read from the env change. A subscribe and a live event resolve the websocket adaptor from a per-registry memo instead of scanning the registry each time.
541
+
542
+ - 00e39db: A websocket room is now left once when an `unsubscribe`, the socket's close and the guard re-check that follows a credential change overlap. Each of them left the room only after awaiting a guard or a cleanup, so the one that finished second left it again: a live room another socket still shared was dropped from the live sync hub, and that socket stopped receiving its changes until it subscribed again; a cleanup registered with `ws.on("unsubscribe", …)` that was still running when its socket closed ran a second time; and a room subscribed again while that cleanup ran lost its pub/sub adaptor membership and its own cleanup. Whichever comes first now takes the room before anything is awaited, and the others find nothing left to leave.
543
+
544
+ An `unsubscribe` now leaves a live room whose slice takes `.with(Ws)`, or `.with(CallerAccount)` once a credential change has refreshed the token. It derived the room id again from those arguments, which no longer read as they did at the subscribe, so it left nothing: the socket stayed in the room and kept receiving its frames, and its `ws.on("unsubscribe", …)` cleanup waited for the socket to close. It now leaves the room its subscribe joined, and its acknowledgement names that room.
545
+
546
+ A websocket `subscribe` now runs its account guards before it parses its arguments, as every other call does, so an unauthenticated subscribe with a malformed room argument is refused by its guards (401/403) instead of with an argument error. The `.live({ pauseOn })` refusal waits for the guards too: a caller they refuse gets 401/403, and one they admit is refused before the slice runs.
547
+
548
+ - cebac7d: A server-side `WsClient` and the tunnel agent reconnect within one heartbeat of a peer going silent again. Since Bun 1.4.4, `WebSocket.close()` waits for the peer's Close frame and gives up only after 30 seconds, so a link that died without a FIN stayed down that much longer before the reconnect began. Both now `terminate()` the silent socket; in a browser, `WsClient` still calls `close()`.
549
+
3
550
  ## 3.0.2
4
551
 
5
552
  ### Minor Changes