@openchambery/web 1.18.7-beta.2 → 1.18.7-beta.3

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 (133) hide show
  1. package/dist/assets/AgentAvatar-CVTgW_rx.js +1 -0
  2. package/dist/assets/AssistantView-CvQ91w2s.js +1 -0
  3. package/dist/assets/{ChatContainer-BHDNtLuy.js → ChatContainer-CERrtaB-.js} +41 -43
  4. package/dist/assets/ContextPanel-CLz4r1G-.js +285 -0
  5. package/dist/assets/DiagramView-DhV4Zthj.js +1 -0
  6. package/dist/assets/{FadeInOnReveal-BpIMrqet.js → FadeInOnReveal-Cy3sf2M5.js} +1 -1
  7. package/dist/assets/{Icon-C0G6afNV.js → Icon-BLWVqBee.js} +1 -1
  8. package/dist/assets/{MarkdownRendererImpl-B-SOb7Za.js → MarkdownRendererImpl-B4qwUu4q.js} +1 -1
  9. package/dist/assets/{MobileDetailNavigation-C7OjF65p.js → MobileDetailNavigation-CuWgEThi.js} +1 -1
  10. package/dist/assets/{MobileShareBridge-CRRrlX0l.js → MobileShareBridge-C3nay5Sq.js} +2 -2
  11. package/dist/assets/MobileSurface-D7v_ZkYO.js +2 -0
  12. package/dist/assets/{MultiRunWindow-Dx4tkVFN.js → MultiRunWindow-Cq8yyiqm.js} +1 -1
  13. package/dist/assets/{OnboardingScreen-BpxD5W4Z.js → OnboardingScreen-CTNi2zuA.js} +1 -1
  14. package/dist/assets/{OverlayScrollbar-Bo7MF_jE.js → OverlayScrollbar-GQgJusKL.js} +1 -1
  15. package/dist/assets/SettingsView-CViKCsxJ.js +25 -0
  16. package/dist/assets/{SettingsWindow-BlKxv7c3.js → SettingsWindow-C6vwy4hH.js} +1 -1
  17. package/dist/assets/{TerminalView-xtUbWmdn.js → TerminalView-C1aX22ka.js} +1 -1
  18. package/dist/assets/{ToolOutputDialog-DUYcTYYW.js → ToolOutputDialog-BrOqj1sB.js} +4 -4
  19. package/dist/assets/appThemeRegistry-BbqImxSo.js +2 -0
  20. package/dist/assets/{button-DfF088a8.js → button-CeCkEpvn.js} +1 -1
  21. package/dist/assets/{clipboard-RoOQVnSl.js → clipboard-BXC1vZwE.js} +1 -1
  22. package/dist/assets/{collapsible-CVL-5iEB.js → collapsible-B-aagADh.js} +1 -1
  23. package/dist/assets/{desktopHosts-Dzx3McaD.js → desktopHosts-BrrYPqKq.js} +1 -1
  24. package/dist/assets/{desktopRelayRestore-B9S0Rp5s.js → desktopRelayRestore-DR60DATT.js} +1 -1
  25. package/dist/assets/diagram-Cxma_IKI.js +1 -0
  26. package/dist/assets/dialog-wraJHZQ2.js +1 -0
  27. package/dist/assets/es-DQqFAaWT.js +15 -0
  28. package/dist/assets/fileQueries-BJKLdYos.js +1 -0
  29. package/dist/assets/fr-CNfGRpc1.js +13 -0
  30. package/dist/assets/{gitApi-DsdwefuR.js → gitApi-B4z4eSZV.js} +1 -1
  31. package/dist/assets/{input-Ca2tUhgN.js → input-CyG05w-V.js} +1 -1
  32. package/dist/assets/insertionBoundaries-DCYAK5W1.js +4 -0
  33. package/dist/assets/ja-CQqehnVn.js +15 -0
  34. package/dist/assets/ko-kmwi8UJD.js +15 -0
  35. package/dist/assets/main-A8oe9r34.js +29 -0
  36. package/dist/assets/{main-CKb-YXE6.js → main-BJBKpmw3.js} +2 -2
  37. package/dist/assets/{markdown-worker-ZaHiIw5I.js → markdown-worker-DDCUZkz4.js} +1 -1
  38. package/dist/assets/{markdownTheme-DidS-jC5.js → markdownTheme-H7_7lbuh.js} +1 -1
  39. package/dist/assets/miniChat-BObOyFSV.js +2 -0
  40. package/dist/assets/mobile-Ba390Cmc.js +2 -0
  41. package/dist/assets/multirun-CMDYOrmD.js +4 -0
  42. package/dist/assets/{persistence-aCOlo6ew.js → persistence-DE9RnVcA.js} +2 -2
  43. package/dist/assets/pl-Blv5o2QA.js +15 -0
  44. package/dist/assets/platform-MjT8iqKB.js +2 -0
  45. package/dist/assets/projectMeta-uWBWFG5A.js +3 -0
  46. package/dist/assets/pt-BR-BK74t-Bd.js +15 -0
  47. package/dist/assets/radio-CEXUd4RC.js +1 -0
  48. package/dist/assets/renderElectronMiniChatApp-Bpgb2E78.js +2 -0
  49. package/dist/assets/renderMobileApp-BMwRozEC.js +23 -0
  50. package/dist/assets/runtimeConfig-hHuyauKZ.js +2 -0
  51. package/dist/assets/runtimeEndpointReset-CmMI7Xp1.js +4 -0
  52. package/dist/assets/{shikiHighlight-eMLXb99c.js → shikiHighlight-C-kTQvqd.js} +1 -1
  53. package/dist/assets/{sortable-tabs-strip-CDHNwlp9.js → sortable-tabs-strip-BdbKDDAP.js} +1 -1
  54. package/dist/assets/src-CgFGBE_A.css +2 -0
  55. package/dist/assets/store-UweBeN2y.js +15 -0
  56. package/dist/assets/{terminalApi-B36I-z14.js → terminalApi-Bnx0KvaV.js} +1 -1
  57. package/dist/assets/{tooltip-GC3dg3gO.js → tooltip-B__x-HDJ.js} +1 -1
  58. package/dist/assets/uk-6I-KXy3R.js +15 -0
  59. package/dist/assets/{url-BQHM4bC4.js → url-CTuw7Rjf.js} +1 -1
  60. package/dist/assets/{useAppFontEffects-B9Ozsfua.js → useAppFontEffects-8iAE0Yrj.js} +4 -4
  61. package/dist/assets/{useAssistantUIStore-DxZsjSPQ.js → useAssistantUIStore-D2K7ksGj.js} +1 -1
  62. package/dist/assets/{useDesktopWindowControlsLayout-QxNEOYzY.js → useDesktopWindowControlsLayout--oz1VAPt.js} +1 -1
  63. package/dist/assets/useEffectiveDirectory-DmoqXaa4.js +1 -0
  64. package/dist/assets/{useFontPreferences-D9h4M7x4.js → useFontPreferences-CWZnNRjz.js} +1 -1
  65. package/dist/assets/useMobileNavigationStore-BxbEX-es.js +412 -0
  66. package/dist/assets/{useSessionAutoCleanup-CXqhEUbp.js → useSessionAutoCleanup-BwlJlhY7.js} +1 -1
  67. package/dist/assets/{useUIStore-Wyu09VUF.js → useUIStore-DOxLVeGb.js} +1 -1
  68. package/dist/assets/{useWorkerHighlightedLines-2KOHSbaa.js → useWorkerHighlightedLines-B21D9MOD.js} +1 -1
  69. package/dist/assets/{utils-CJdWZupK.js → utils-C2R0YoC5.js} +1 -1
  70. package/dist/assets/vendor-legendapp-list-WE32lwNk.js +1 -0
  71. package/dist/assets/zh-CN-DwlzS4yK.js +15 -0
  72. package/dist/assets/zh-TW-p0VriyfT.js +15 -0
  73. package/dist/index.html +9 -9
  74. package/dist/mini-chat.html +9 -9
  75. package/dist/mobile.html +9 -9
  76. package/package.json +3 -2
  77. package/server/index.js +19 -6
  78. package/server/lib/notifications/APNS.md +88 -38
  79. package/server/lib/notifications/DOCUMENTATION.md +11 -4
  80. package/server/lib/notifications/apns-runtime.js +793 -78
  81. package/server/lib/notifications/apns-runtime.test.js +922 -0
  82. package/server/lib/notifications/apns-titles.js +212 -0
  83. package/server/lib/notifications/apns-titles.test.js +55 -0
  84. package/server/lib/notifications/routes.js +57 -1
  85. package/server/lib/notifications/runtime.js +20 -17
  86. package/server/lib/notifications/runtime.test.js +108 -0
  87. package/server/lib/opencode/DOCUMENTATION.md +1 -1
  88. package/server/lib/opencode/bootstrap-runtime.js +6 -0
  89. package/server/lib/opencode/bootstrap-runtime.test.js +21 -0
  90. package/server/lib/opencode/routes.js +24 -5
  91. package/server/lib/opencode/routes.test.js +78 -1
  92. package/server/lib/relay/DOCUMENTATION.md +2 -2
  93. package/server/lib/relay/service.js +45 -3
  94. package/server/lib/relay/service.test.js +117 -0
  95. package/server/lib/relay/signing-key.js +1 -1
  96. package/server/lib/scheduled-tasks/DOCUMENTATION.md +27 -2
  97. package/server/lib/scheduled-tasks/runtime.js +271 -3
  98. package/server/lib/scheduled-tasks/runtime.test.js +308 -0
  99. package/server/lib/session-goal/runtime.js +2 -2
  100. package/dist/assets/AgentAvatar-CWn_73qY.js +0 -1
  101. package/dist/assets/AssistantView-BiOO5An0.js +0 -1
  102. package/dist/assets/ContextPanel-BbNBZipF.js +0 -288
  103. package/dist/assets/DiagramView-AMPWVMV0.js +0 -1
  104. package/dist/assets/MobileSurface-DmMgH78x.js +0 -2
  105. package/dist/assets/SettingsView-BDaMrSki.js +0 -25
  106. package/dist/assets/appThemeRegistry-D6qZmouf.js +0 -2
  107. package/dist/assets/dialog-BbHDXPt6.js +0 -1
  108. package/dist/assets/es-BkIvCvwj.js +0 -15
  109. package/dist/assets/fileQueries-_VUJM8AE.js +0 -1
  110. package/dist/assets/fr-tCwQ3odX.js +0 -13
  111. package/dist/assets/insertionBoundaries-M7AXIWfg.js +0 -1
  112. package/dist/assets/ja-C0rD6dwp.js +0 -15
  113. package/dist/assets/ko-CAbtZ462.js +0 -15
  114. package/dist/assets/main-QjOL36Zh.js +0 -29
  115. package/dist/assets/miniChat-C45kGqqH.js +0 -2
  116. package/dist/assets/mobile-DH8nxp1K.js +0 -2
  117. package/dist/assets/multirun-BkKb0iZv.js +0 -4
  118. package/dist/assets/pl-Bb190ba6.js +0 -15
  119. package/dist/assets/platform-DxNH_0Fj.js +0 -2
  120. package/dist/assets/projectMeta-BHtBPaxp.js +0 -3
  121. package/dist/assets/pt-BR-B6p_t3IB.js +0 -15
  122. package/dist/assets/radio-BacnmuU0.js +0 -1
  123. package/dist/assets/renderElectronMiniChatApp-Cn7umSw9.js +0 -2
  124. package/dist/assets/renderMobileApp-DZBR2t3l.js +0 -23
  125. package/dist/assets/runtimeConfig-CSAVS8gd.js +0 -2
  126. package/dist/assets/runtimeEndpointReset-BZUX3z0A.js +0 -4
  127. package/dist/assets/src-DziW4HUU.css +0 -2
  128. package/dist/assets/store-1FOY4G3z.js +0 -15
  129. package/dist/assets/uk-Dy8-6ZEj.js +0 -15
  130. package/dist/assets/useEffectiveDirectory-D3u_oih8.js +0 -1
  131. package/dist/assets/useMobileNavigationStore-CCPTHVO9.js +0 -475
  132. package/dist/assets/zh-CN-gRTE0npc.js +0 -15
  133. package/dist/assets/zh-TW-ClD4BCfW.js +0 -15
package/dist/index.html CHANGED
@@ -552,25 +552,25 @@
552
552
  opacity: 0.72;
553
553
  }
554
554
  </style>
555
- <script type="module" crossorigin src="/assets/main-CKb-YXE6.js"></script>
555
+ <script type="module" crossorigin src="/assets/main-BJBKpmw3.js"></script>
556
556
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
557
557
  <link rel="modulepreload" crossorigin href="/assets/vendor-base-ui-CnHfUmDN.js">
558
558
  <link rel="modulepreload" crossorigin href="/assets/vendor-zustand-B4dlGEM2.js">
559
- <link rel="modulepreload" crossorigin href="/assets/useUIStore-Wyu09VUF.js">
559
+ <link rel="modulepreload" crossorigin href="/assets/useUIStore-DOxLVeGb.js">
560
560
  <link rel="modulepreload" crossorigin href="/assets/vendor-pathe-vBmvQE8f.js">
561
561
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-DLPElpoQ.js">
562
562
  <link rel="modulepreload" crossorigin href="/assets/vendor-tanstack-query-core-BWz0ose6.js">
563
- <link rel="modulepreload" crossorigin href="/assets/persistence-aCOlo6ew.js">
564
- <link rel="modulepreload" crossorigin href="/assets/desktopHosts-Dzx3McaD.js">
565
- <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-B9S0Rp5s.js">
563
+ <link rel="modulepreload" crossorigin href="/assets/persistence-DE9RnVcA.js">
564
+ <link rel="modulepreload" crossorigin href="/assets/desktopHosts-BrrYPqKq.js">
565
+ <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-DR60DATT.js">
566
566
  <link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-C07HQypa.js">
567
567
  <link rel="modulepreload" crossorigin href="/assets/vendor-aparajita-capacitor-secure-storage-CjQdwfjS.js">
568
- <link rel="modulepreload" crossorigin href="/assets/platform-DxNH_0Fj.js">
569
- <link rel="modulepreload" crossorigin href="/assets/terminalApi-B36I-z14.js">
568
+ <link rel="modulepreload" crossorigin href="/assets/platform-MjT8iqKB.js">
569
+ <link rel="modulepreload" crossorigin href="/assets/terminalApi-Bnx0KvaV.js">
570
570
  <link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
571
571
  <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-DCowZ2ab.js">
572
- <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-CSAVS8gd.js">
573
- <link rel="stylesheet" crossorigin href="/assets/src-DziW4HUU.css">
572
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-hHuyauKZ.js">
573
+ <link rel="stylesheet" crossorigin href="/assets/src-CgFGBE_A.css">
574
574
  </head>
575
575
  <body class="h-full bg-background text-foreground">
576
576
  <!-- Keep the startup layer outside #root. React replaces every child of
@@ -74,25 +74,25 @@
74
74
  }
75
75
  </style>
76
76
 
77
- <script type="module" crossorigin src="/assets/miniChat-C45kGqqH.js"></script>
77
+ <script type="module" crossorigin src="/assets/miniChat-BObOyFSV.js"></script>
78
78
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
79
79
  <link rel="modulepreload" crossorigin href="/assets/vendor-base-ui-CnHfUmDN.js">
80
80
  <link rel="modulepreload" crossorigin href="/assets/vendor-zustand-B4dlGEM2.js">
81
- <link rel="modulepreload" crossorigin href="/assets/useUIStore-Wyu09VUF.js">
81
+ <link rel="modulepreload" crossorigin href="/assets/useUIStore-DOxLVeGb.js">
82
82
  <link rel="modulepreload" crossorigin href="/assets/vendor-pathe-vBmvQE8f.js">
83
83
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-DLPElpoQ.js">
84
84
  <link rel="modulepreload" crossorigin href="/assets/vendor-tanstack-query-core-BWz0ose6.js">
85
- <link rel="modulepreload" crossorigin href="/assets/persistence-aCOlo6ew.js">
86
- <link rel="modulepreload" crossorigin href="/assets/desktopHosts-Dzx3McaD.js">
87
- <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-B9S0Rp5s.js">
85
+ <link rel="modulepreload" crossorigin href="/assets/persistence-DE9RnVcA.js">
86
+ <link rel="modulepreload" crossorigin href="/assets/desktopHosts-BrrYPqKq.js">
87
+ <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-DR60DATT.js">
88
88
  <link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-C07HQypa.js">
89
89
  <link rel="modulepreload" crossorigin href="/assets/vendor-aparajita-capacitor-secure-storage-CjQdwfjS.js">
90
- <link rel="modulepreload" crossorigin href="/assets/platform-DxNH_0Fj.js">
91
- <link rel="modulepreload" crossorigin href="/assets/terminalApi-B36I-z14.js">
90
+ <link rel="modulepreload" crossorigin href="/assets/platform-MjT8iqKB.js">
91
+ <link rel="modulepreload" crossorigin href="/assets/terminalApi-Bnx0KvaV.js">
92
92
  <link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
93
93
  <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-DCowZ2ab.js">
94
- <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-CSAVS8gd.js">
95
- <link rel="stylesheet" crossorigin href="/assets/src-DziW4HUU.css">
94
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-hHuyauKZ.js">
95
+ <link rel="stylesheet" crossorigin href="/assets/src-CgFGBE_A.css">
96
96
  </head>
97
97
  <body class="h-full bg-background text-foreground">
98
98
  <div id="root" class="h-full">
package/dist/mobile.html CHANGED
@@ -63,25 +63,25 @@
63
63
  })();
64
64
  </script>
65
65
 
66
- <script type="module" crossorigin src="/assets/mobile-DH8nxp1K.js"></script>
66
+ <script type="module" crossorigin src="/assets/mobile-Ba390Cmc.js"></script>
67
67
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
68
68
  <link rel="modulepreload" crossorigin href="/assets/vendor-base-ui-CnHfUmDN.js">
69
69
  <link rel="modulepreload" crossorigin href="/assets/vendor-zustand-B4dlGEM2.js">
70
- <link rel="modulepreload" crossorigin href="/assets/useUIStore-Wyu09VUF.js">
70
+ <link rel="modulepreload" crossorigin href="/assets/useUIStore-DOxLVeGb.js">
71
71
  <link rel="modulepreload" crossorigin href="/assets/vendor-pathe-vBmvQE8f.js">
72
72
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-DLPElpoQ.js">
73
73
  <link rel="modulepreload" crossorigin href="/assets/vendor-tanstack-query-core-BWz0ose6.js">
74
- <link rel="modulepreload" crossorigin href="/assets/persistence-aCOlo6ew.js">
75
- <link rel="modulepreload" crossorigin href="/assets/desktopHosts-Dzx3McaD.js">
76
- <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-B9S0Rp5s.js">
74
+ <link rel="modulepreload" crossorigin href="/assets/persistence-DE9RnVcA.js">
75
+ <link rel="modulepreload" crossorigin href="/assets/desktopHosts-BrrYPqKq.js">
76
+ <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-DR60DATT.js">
77
77
  <link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-C07HQypa.js">
78
78
  <link rel="modulepreload" crossorigin href="/assets/vendor-aparajita-capacitor-secure-storage-CjQdwfjS.js">
79
- <link rel="modulepreload" crossorigin href="/assets/platform-DxNH_0Fj.js">
80
- <link rel="modulepreload" crossorigin href="/assets/terminalApi-B36I-z14.js">
79
+ <link rel="modulepreload" crossorigin href="/assets/platform-MjT8iqKB.js">
80
+ <link rel="modulepreload" crossorigin href="/assets/terminalApi-Bnx0KvaV.js">
81
81
  <link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
82
82
  <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-DCowZ2ab.js">
83
- <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-CSAVS8gd.js">
84
- <link rel="stylesheet" crossorigin href="/assets/src-DziW4HUU.css">
83
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-hHuyauKZ.js">
84
+ <link rel="stylesheet" crossorigin href="/assets/src-CgFGBE_A.css">
85
85
  </head>
86
86
  <body class="h-full bg-background text-foreground">
87
87
  <div id="root" class="h-full"></div>
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@openchambery/web",
3
- "version": "1.18.7-beta.2",
3
+ "version": "1.18.7-beta.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./server/index.js",
7
7
  "types": "./server/index.d.ts",
8
8
  "bin": {
9
- "openchamber": "./bin/cli.js"
9
+ "openchamber": "./bin/cli.js",
10
+ "openchambery": "./bin/cli.js"
10
11
  },
11
12
  "publishConfig": {
12
13
  "access": "public"
package/server/index.js CHANGED
@@ -89,7 +89,7 @@ import { applyRuntimeCorsHeaders } from './lib/request-cors.js';
89
89
  import { createClientPairingRuntime } from './lib/client-auth/pairing.js';
90
90
  import { createPreviewProxyRuntime } from './lib/preview/proxy-runtime.js';
91
91
  import { attachRealtimeProxy } from './lib/realtime-proxy.js';
92
- import { createRelayService, isRelayHostRuntime } from './lib/relay/service.js';
92
+ import { createRelayService, isRelayHostRuntime, resolveEffectiveRelayUrl } from './lib/relay/service.js';
93
93
  import { createRelayHostLock } from './lib/relay/host-lock.js';
94
94
  import { createProxyMiddleware, responseInterceptor } from 'http-proxy-middleware';
95
95
  import webPush from 'web-push';
@@ -202,10 +202,6 @@ const shouldSkipApiCompression = () => {
202
202
 
203
203
  const OPENCHAMBER_VERBOSE_REQUEST_LOGS = isEnvFlagEnabled(process.env.OPENCHAMBER_VERBOSE_REQUEST_LOGS);
204
204
 
205
- const PLAN_MODE_EXPERIMENT_ENABLED =
206
- isEnvFlagEnabled(process.env.OPENCODE_EXPERIMENTAL_PLAN_MODE)
207
- || isEnvFlagEnabled(process.env.OPENCODE_EXPERIMENTAL);
208
-
209
205
  const fsPromises = fs.promises;
210
206
 
211
207
  const settingsNormalizationRuntime = createSettingsNormalizationRuntime({
@@ -361,11 +357,18 @@ const apnsRuntime = createApnsRuntime({
361
357
  readSettingsFromDiskMigrated,
362
358
  writeSettingsToDisk,
363
359
  readSettingsStrict: readSettingsFromDiskStrict,
360
+ resolveEffectiveRelayUrl: async () => {
361
+ const settings = await readSettingsFromDiskMigrated();
362
+ return resolveEffectiveRelayUrl({ settings });
363
+ },
364
364
  });
365
365
 
366
366
  const addOrUpdateApnsToken = (...args) => apnsRuntime.addOrUpdateApnsToken(...args);
367
367
  const removeApnsToken = (...args) => apnsRuntime.removeApnsToken(...args);
368
368
  const sendApnsToAllUiSessions = (...args) => apnsRuntime.sendApnsToAllUiSessions(...args);
369
+ const addOrUpdateLiveActivityToken = (...args) => apnsRuntime.addOrUpdateLiveActivityToken(...args);
370
+ const removeLiveActivityToken = (...args) => apnsRuntime.removeLiveActivityToken(...args);
371
+ const sendLiveActivityEnd = (...args) => apnsRuntime.sendLiveActivityEnd(...args);
369
372
 
370
373
  const TERMINAL_INPUT_WS_MAX_REBINDS_PER_WINDOW = 128;
371
374
  const TERMINAL_INPUT_WS_REBIND_WINDOW_MS = 60 * 1000;
@@ -660,6 +663,7 @@ const notificationTriggerRuntime = createNotificationTriggerRuntime({
660
663
  broadcastUiNotification,
661
664
  sendPushToAllUiSessions,
662
665
  sendApnsToAllUiSessions,
666
+ sendLiveActivityEnd,
663
667
  isAnyInteractiveClientVisible,
664
668
  buildOpenCodeUrl,
665
669
  getOpenCodeAuthHeaders,
@@ -1302,6 +1306,9 @@ async function main(options = {}) {
1302
1306
  messageQueueRuntime.observeSessionEvent?.(event);
1303
1307
  if (payload.type === 'session.status' || payload.type === 'session.idle' || payload.type === 'session.error') void messageQueueRuntime.wake();
1304
1308
  });
1309
+ const unsubscribeScheduledTaskEvents = globalMessageStreamHub.subscribeEvent((event) => {
1310
+ scheduledTasksRuntime.observeSessionEvent?.(event);
1311
+ });
1305
1312
 
1306
1313
  console.log(`Starting OpenChamber on port ${port === 0 ? 'auto' : port}`);
1307
1314
 
@@ -1393,7 +1400,6 @@ async function main(options = {}) {
1393
1400
  nodeBinaryResolved: resolvedNodeBinary || null,
1394
1401
  bunBinaryResolved: resolvedBunBinary || null,
1395
1402
  desktopNotifyEnabled: ENV_DESKTOP_NOTIFY,
1396
- planModeExperimentalEnabled: PLAN_MODE_EXPERIMENT_ENABLED,
1397
1403
  apiOnly,
1398
1404
  };
1399
1405
  },
@@ -1445,6 +1451,9 @@ async function main(options = {}) {
1445
1451
  removePushSubscription,
1446
1452
  addOrUpdateApnsToken,
1447
1453
  removeApnsToken,
1454
+ addOrUpdateLiveActivityToken,
1455
+ removeLiveActivityToken,
1456
+ sendLiveActivityEnd,
1448
1457
  updateUiVisibility,
1449
1458
  clearPendingPushBadge: () => clearPendingPushBadge(),
1450
1459
  isUiVisible,
@@ -1504,6 +1513,9 @@ async function main(options = {}) {
1504
1513
  ]);
1505
1514
  return pendingRelay || deviceRelay;
1506
1515
  },
1516
+ onRelayUrlChanged: async () => {
1517
+ await apnsRuntime.reRegisterAllTokens();
1518
+ },
1507
1519
  });
1508
1520
  relayServiceInstance = relayService;
1509
1521
  relayService.registerRoutes(app);
@@ -1668,6 +1680,7 @@ async function main(options = {}) {
1668
1680
  try {
1669
1681
  unsubscribeSessionIndexEvents();
1670
1682
  unsubscribeMessageQueueEvents();
1683
+ unsubscribeScheduledTaskEvents();
1671
1684
  sessionIndexSyncRuntime?.stop();
1672
1685
  sessionIndexService?.close();
1673
1686
  } catch {
@@ -13,19 +13,57 @@ registered them — so a leaked device token alone can't be used to push.
13
13
  `POST /v1/push/register-token`, signed with its auto-generated ECDSA P-256 key
14
14
  (`getOrCreateRelayKeypair`, persisted in settings like the VAPID keys). The relay records
15
15
  `token → serverId` where `serverId = SHA-256(publicKey)`.
16
- 3. On a trigger (ready/error/question/permission), the server composes **generic, content-free**
17
- text — a fixed scenario title ("Agent response is ready" / "Agent needs your input" / "Agent
18
- needs permission" / "Agent hit an error") + the **session name** as the body, no model/project/
19
- message content — plus a **`badge`** count (see below) — and POSTs `{ tokens, title, body,
20
- badge, env, data:{sessionId}, publicKeyJwk, ts, sig }` to `POST /v1/push/send`
21
- (`apns-runtime.js` `sendViaRelay`). It does **not** gate on UI visibility (see below).
22
- 4. The **relay** (`openchamber-website/apps/api`, Cloudflare Worker) verifies the signature +
23
- `ts` freshness, derives `serverId`, and only delivers to tokens bound to that server. It holds
24
- the single project APNs `.p8` key, signs an ES256 JWT with `crypto.subtle`, and sends each
25
- token to APNs over HTTP/2, returning per-token results; the server drops tokens flagged `drop`
26
- (410 / BadDeviceToken). The relay stores no secret only `token serverId` hashes.
16
+ 3. On a trigger (ready/error/question/permission/goal_*), the server composes **generic,
17
+ content-free** text — a **locale-specific** scenario title (from `apns-titles.js`, keyed off
18
+ the locale stored with each device token at `POST /api/push/apns-token`) + the **session name**
19
+ as the body, no model/project/message content — plus a **`badge`** count (see below). Tokens
20
+ are grouped by locale (relay signatures cover `title`, so mixed locales cannot share one
21
+ batch) and each group POSTs `{ tokens, title, body, badge, env, data:{sessionId},
22
+ publicKeyJwk, ts, sig }` to `POST /v1/push/send` (`apns-runtime.js` → `sendViaRelay`). It does
23
+ **not** gate on UI visibility (see below). Legacy tokens without a locale fall back to English.
24
+ 4. The **Push Relay** (`packages/relay-server/src/push/`, process `openchamber-push-relay`)
25
+ verifies the signature + `ts` freshness, derives `serverId`, and only delivers to tokens bound
26
+ to that server. It holds the project APNs `.p8` key, signs an ES256 JWT with Node crypto, and
27
+ sends each token to APNs over HTTP/2, returning per-token results; the server drops tokens
28
+ flagged `drop` (410 / BadDeviceToken). The Push database stores `token → serverId` bindings,
29
+ not application plaintext. Layer 1 (`openchamber-relay`) never sees these secrets.
27
30
  5. Tapping a push deep-links to its session via the forwarded `sessionId`.
28
31
 
32
+ ## Live Activity (update / end)
33
+
34
+ ActivityKit has issued **update/end push tokens** for an Activity that is
35
+ **already started on-device** since **iOS 16.1**. **Push-to-start** (creating an
36
+ Activity from APNs while the app is not running) is **iOS 17.2+** and is not
37
+ part of this Host path.
38
+
39
+ The native app registers a Live Activity token with **its own server**
40
+ (`POST /api/push/live-activity-token`: `token`, `activityId`, `sessionId`).
41
+ Those tokens are stored separately from alert APNs device tokens (same
42
+ `apns-tokens.json` file, `version` 2 `liveActivityTokensBySession`; v1 alert
43
+ data remains readable) and are never used for banner push. The Host binds them
44
+ on the relay at `POST /v1/push/register-live-activity-token` with
45
+ `{ token, platform: "ios", kind: "liveactivity", publicKeyJwk, ts, sig }` signed
46
+ over `${ts}.${token}.${platform}.${kind}`. Unregister uses
47
+ `/v1/push/unregister-live-activity-token` with the same signature, and only the
48
+ owning UI session may delete a local token.
49
+
50
+ On top-level session **completion** or **error** (child sessions and small-model
51
+ system sessions suppressed; independent of ordinary notification settings and UI
52
+ visibility) the Host calls `sendLiveActivityEnd`. Relay send is
53
+ `POST /v1/push/live-activity` with `{ tokens, event, contentState,
54
+ dismissalDate?, staleDate?, publicKeyJwk, ts, sig }` signed over
55
+ `${ts}.${sortedTokens}.${event}.${status}.${eventVersion}.${updatedAt}.${endedAt}.${dismissalDate}.${staleDate}`.
56
+ Direct mode uses HTTP/2 token auth, `apns-topic: ${bundleId}.push-type.liveactivity`,
57
+ `apns-push-type: liveactivity`, and no alert.
58
+
59
+ The APNs / relay payload **must not** carry `sessionId` or user/session content.
60
+ `contentState` is only `{ status, eventVersion, updatedAt, endedAt }`.
61
+ `complete` uses a 15-minute dismissal and `error` a 60-minute dismissal (same as
62
+ native). `staleDate = updatedAt + 20min` is for **update** events only. After a
63
+ successfully accepted `end`, the Host clears the local Activity tokens for those
64
+ devices. A Live Activity failure does not drop alert tokens or block ordinary
65
+ push. Do not log Live Activity tokens or session ids.
66
+
29
67
  ## Foreground suppression
30
68
 
31
69
  APNs is **not** gated on UI visibility. A backgrounded WKWebView can't reliably report "hidden"
@@ -34,8 +72,8 @@ responses. Instead the server always sends, and **iOS** suppresses the foregroun
34
72
  (`PushNotifications.presentationOptions: []` in `capacitor.config`) — so there is no notification
35
73
  while the app is active, with no race. APNs is the native app's **only** channel; local
36
74
  notifications were removed (a WKWebView can't tell foreground from background — `document.hasFocus()`
37
- is unreliable — so they leaked while the app was open). Cloudflare is touched only when a native
38
- app with notifications on has a registered token and a trigger fires.
75
+ is unreliable — so they leaked while the app was open). The Push Relay is contacted only when a
76
+ native app with notifications on has a registered token and a trigger fires.
39
77
 
40
78
  ## App-icon badge
41
79
 
@@ -61,36 +99,45 @@ mirrors the device zeroing its icon badge on `sceneDidBecomeActive` (`AppDelegat
61
99
  server and device in sync.
62
100
 
63
101
  The value flows `runtime.js` (`toApnsGenericPayload`) → `apns-runtime.js` (`sendViaRelay` body /
64
- direct-mode `aps.badge`) → relay (`pushSendSchema.badge` → `aps.badge`). It is **not** signed (like
65
- `body`/`data`); the relay still only delivers to bound tokens. The set is server-global, so every
66
- device token of a server sees the same badge.
102
+ direct-mode `aps.badge`) → Push Relay (`packages/relay-server/src/push/schema.js` → `aps.badge`).
103
+ It is **not** signed (like `body`/`data`); the relay still only delivers to bound tokens. The set
104
+ is server-global, so every device token of a server sees the same badge.
67
105
 
68
106
  ## Modes
69
107
 
70
- - **Relay (default):** server has no Apple key; `OPENCHAMBER_PUSH_RELAY_URL` defaults to
71
- `https://api.openchamber.dev/v1/push/send` (register URL is derived as `…/register-token`).
108
+ - **Relay (default):** Host has no Apple key. Unless `OPENCHAMBER_PUSH_RELAY_URL` is set, the
109
+ Host maps the effective Relay `wss://`/`ws://` URL to the same host as `https://`/`http://`
110
+ `/v1/push/send` (register URL is `/v1/push/register-token`). A Relay switch re-registers
111
+ persisted tokens and binds them again before the first send.
72
112
  - **Direct (fallback):** set `OPENCHAMBER_PUSH_RELAY_DISABLED=true` + `OPENCHAMBER_APNS_KEY_ID/
73
113
  TEAM_ID/P8` to sign+send from the server itself (HTTP/2 + ES256 JWT); no relay binding needed.
74
114
 
75
115
  ## Config
76
116
 
77
- Server (`apns-runtime.js`):
78
- - `OPENCHAMBER_PUSH_RELAY_URL` (default the public relay), `OPENCHAMBER_APNS_ENVIRONMENT`
79
- (`sandbox` default / `production`). The signing keypair is auto-generated — nothing to set.
117
+ Host (`apns-runtime.js`):
118
+ - `OPENCHAMBER_PUSH_RELAY_URL` (explicit send-URL override), `OPENCHAMBER_APNS_ENVIRONMENT`
119
+ (`sandbox` default / `production` for TestFlight and App Store). The signing keypair is
120
+ auto-generated — nothing to set.
80
121
  - Direct fallback: `OPENCHAMBER_APNS_KEY_ID`, `OPENCHAMBER_APNS_TEAM_ID`, `OPENCHAMBER_APNS_P8`
81
122
  (or `_P8_PATH`), `OPENCHAMBER_APNS_BUNDLE_ID`, `OPENCHAMBER_PUSH_RELAY_DISABLED=true`.
82
123
 
83
- Relay (Cloudflare Worker secrets via `wrangler secret put` / GitHub Actions): `APNS_P8`,
84
- `APNS_KEY_ID`, `APNS_TEAM_ID`, optional `APNS_BUNDLE_ID` / `APNS_DEFAULT_ENV`. The `push_tokens`
85
- binding table is created by `migrations/0002_push_tokens.sql` (applied on deploy).
124
+ Push process (`packages/relay-server/src/push/`, env prefix `OPENCHAMBER_PUSH_RELAY_`):
125
+ `HOST` (default `127.0.0.1`), `PORT` (default `8788`), `TRUST_PROXY`, `DATABASE_PATH`,
126
+ timestamp/replay/rate-limit/token/`MAX_IN_FLIGHT` limits, plus APNs `APNS_KEY_ID`,
127
+ `APNS_TEAM_ID`, `APNS_BUNDLE_ID`, and `APNS_P8` or `APNS_P8_PATH`. See
128
+ `packages/relay-server/README.md`. SQLite is single-instance. Do not log `.p8` material,
129
+ device tokens, or signatures.
86
130
 
87
131
  ## Apple setup (one-time)
88
132
 
89
133
  1. Apple **Keys** (not Certificates) → create an **APNs Auth Key** (`.p8`) → Key ID + Team ID;
90
- enable **Push Notifications** on App ID `com.openchamber.app`.
91
- 2. In the **openchamber-website** repo → Actions secrets: `APNS_P8` (PEM), `APNS_KEY_ID`,
92
- `APNS_TEAM_ID`. Push to `main` relay deploys, secrets sync, D1 migrations apply.
134
+ enable **Push Notifications** on the App ID. TestFlight / App Store builds of this product
135
+ use bundle ID `com.yee94.openchamber`.
136
+ 2. Give those values only to the Push process (environment or Docker secret). Do not put the
137
+ `.p8` on Layer 1 or on each OpenChamber Host when using Relay mode.
93
138
  3. Xcode: confirm the Push Notifications capability; Clean Build Folder; run on device.
139
+ Development builds use APNs `sandbox`. TestFlight and App Store Hosts set
140
+ `OPENCHAMBER_APNS_ENVIRONMENT=production`.
94
141
 
95
142
  ## Security posture
96
143
 
@@ -98,10 +145,12 @@ binding table is created by `migrations/0002_push_tokens.sql` (applied on deploy
98
145
  is signed by the server's private key, and the relay only delivers to a token from its bound
99
146
  `serverId`. A leaked token alone is useless — an attacker has neither the private key nor a
100
147
  matching binding.
101
- - `serverId` self-certifies (`SHA-256(publicKey)`), so the relay holds no secret; a D1 leak
102
- exposes only `token serverId` hashes. The signed `ts` (±5 min window) blocks replay.
148
+ - `serverId` self-certifies (`SHA-256(publicKey)`). A SQLite leak exposes `token serverId`
149
+ bindings, not Host private keys or application plaintext. The signed `ts` (±5 min window)
150
+ blocks replay. Signature verification lives in `packages/relay-server/src/push/crypto.js`
151
+ and must stay aligned with `packages/web/server/lib/relay/signing-key.js`.
103
152
  - Residual: trust-on-first-bind (whoever registers a token first owns it) — acceptable, since
104
- registering already requires possessing the token. Cloudflare rate limiting is defence-in-depth.
153
+ registering already requires possessing the token. Process rate limits are defence-in-depth.
105
154
 
106
155
  ## Data confidentiality (what the relay / Apple can see)
107
156
 
@@ -113,17 +162,18 @@ to APNs). The request **signature is authentication, not encryption** — the re
113
162
  Who can read the alert text:
114
163
 
115
164
  - **Network hops:** nothing (TLS).
116
- - **The relay (Cloudflare):** the generic title + body (session name), the device token, and
117
- `sessionId`. It stores only `token → serverId` hashes (no text, no payload).
165
+ - **The Push Relay process:** the generic title + body (session name), the device token, and
166
+ `sessionId`. It stores `token → serverId` bindings (no alert text, no payload).
118
167
  - **Apple APNs:** the alert text too — APNs always reads the alert payload of an `alert` push.
119
168
  - **The device:** displays it.
120
169
 
121
- This is acceptable **because the text is deliberately content-free**: a fixed scenario title +
122
- the session name only — no model, project, or message content (`runtime.js` →
123
- `toApnsGenericPayload`). The session name is the single semi-personal field that crosses the
124
- relay/Apple. To hide even that from Apple would require an end-to-end **encrypted payload**
125
- (`mutable-content` + a Notification Service Extension that decrypts on-device with a key never
126
- sent to the relay) — not implemented, and unnecessary for generic text.
170
+ This is acceptable **because the text is deliberately content-free**: a fixed scenario title
171
+ (localized per device) + the session name only — no model, project, or message content
172
+ (`runtime.js` → `toApnsGenericPayload` → `localizeApnsPayload`). The session name is the single
173
+ semi-personal field that crosses the relay/Apple. To hide even that from Apple would require an
174
+ end-to-end **encrypted payload** (`mutable-content` + a Notification Service Extension that
175
+ decrypts on-device with a key never sent to the relay) — not implemented, and unnecessary for
176
+ generic text.
127
177
 
128
178
  ## Android (FCM) note
129
179
 
@@ -7,7 +7,7 @@ This module provides notification message preparation utilities for the web serv
7
7
  - `packages/web/server/lib/notifications/index.js`: public entrypoint imported by `packages/web/server/index.js`.
8
8
  - `packages/web/server/lib/notifications/routes.js`: route registration for push, visibility, and session status/attention endpoints.
9
9
  - `packages/web/server/lib/notifications/push-runtime.js`: push subscription persistence, VAPID initialization, and UI visibility runtime.
10
- - `packages/web/server/lib/notifications/apns-runtime.js`: native iOS APNs device-token persistence + delivery. Two modes: **relay** (default — sign + POST tokens + generic text to the central Cloudflare relay `https://api.openchamber.dev/v1/push/send`, which holds the single project APNs key) and **direct** (fallback — sign ES256 JWT with Node crypto + HTTP/2, when `OPENCHAMBER_PUSH_RELAY_DISABLED=true`). Each server has an auto-generated ECDSA P-256 keypair (`getOrCreateRelayKeypair`, persisted in settings); it binds tokens on the relay (`/v1/push/register-token`) and signs every relay request, so the relay only delivers to tokens bound to that server. APNs is the native app's sole notification channel (no local notifications) and is NOT gated on UI visibility — iOS suppresses the foreground banner instead. Mobile push carries only generic text (scenario title + session name) — see `APNS.md`.
10
+ - `packages/web/server/lib/notifications/apns-runtime.js`: native iOS APNs device-token persistence + delivery, plus a separate Live Activity token store and `end` send path. Two modes: **relay** (default — sign + POST tokens + generic text to the isolated Push Relay in `packages/relay-server/src/push/`, which holds the project APNs key) and **direct** (fallback — sign ES256 JWT with Node crypto + HTTP/2, when `OPENCHAMBER_PUSH_RELAY_DISABLED=true`). Unless `OPENCHAMBER_PUSH_RELAY_URL` is set, the Host maps the effective Relay `wss`/`ws` URL to the same host as `https`/`http` `/v1/push/send`. Before each relay send, the runtime checks an in-memory registration success cache (keyed by current register URL + token + platform) and only POSTs `/v1/push/register-token` for cache misses; only successfully bound tokens are sent. Switching Relay/Push URL clears that cache so persisted tokens re-bind to the new origin on the next send or `reRegisterAllTokens()`. Each server has an auto-generated ECDSA P-256 keypair (`getOrCreateRelayKeypair`, persisted in settings); it binds tokens on the relay (`/v1/push/register-token`) and signs every relay request, so the relay only delivers to tokens bound to that server. APNs is the native app's sole notification channel (no local notifications) and is NOT gated on UI visibility — iOS suppresses the foreground banner instead. Mobile push carries only generic text (scenario title + session name) — see `APNS.md`. Live Activity tokens are persisted beside alert tokens in the same file (`version` 2, `liveActivityTokensBySession`) without mixing the two token types; v1 files remain readable. Live Activity relay bind/send uses `/v1/push/register-live-activity-token`, `/v1/push/unregister-live-activity-token`, and `/v1/push/live-activity`.
11
11
  - `packages/web/server/lib/notifications/emitter-runtime.js`: desktop/stdout + UI SSE notification emission runtime.
12
12
  - `packages/web/server/lib/notifications/runtime.js`: trigger runtime for OpenCode event-driven notification fanout.
13
13
  - `packages/web/server/lib/notifications/template-runtime.js`: notification template variables and session text/title enrichment runtime. Zen-model helpers are retained as compatibility stubs only.
@@ -27,6 +27,8 @@ This module provides notification message preparation utilities for the web serv
27
27
  - `DELETE /api/push/subscribe`
28
28
  - `POST /api/push/apns-token` (native iOS APNs device-token registration)
29
29
  - `DELETE /api/push/apns-token`
30
+ - `POST /api/push/live-activity-token` (native iOS Live Activity token registration; UI-auth, length-limited `token` / `activityId` / `sessionId`)
31
+ - `DELETE /api/push/live-activity-token` (owning UI session only)
30
32
  - `POST /api/push/visibility`
31
33
  - `GET /api/push/visibility`
32
34
  - `GET /api/notifications/stream`
@@ -46,6 +48,7 @@ This module provides notification message preparation utilities for the web serv
46
48
  - `maybeSendPushForTrigger(payload)`
47
49
  - Owns:
48
50
  - top-level completion/question/permission trigger routing; completion is the sole task-status notification and child sessions plus small-model system sessions (`metadata.openchamber.smallModel.purpose`) are suppressed
51
+ - top-level completion/error Live Activity `end` (`sendLiveActivityEnd`), independent of ordinary push settings and UI visibility, with the same child/small-model suppression; duplicate terminal events are idempotent at the token store
49
52
  - session meta cache for child-session and small-model suppression
50
53
  - template resolution and fallback behavior
51
54
  - native notification fanout and web push payload fanout
@@ -67,12 +70,16 @@ This module provides notification message preparation utilities for the web serv
67
70
  ### APNs runtime API (apns-runtime.js)
68
71
  - `createApnsRuntime(dependencies)`: creates runtime for native iOS APNs push and device-token state. Dependencies: `fsPromises`, `path`, `crypto`, `http2`, `APNS_TOKENS_FILE_PATH`, `readSettingsFromDiskMigrated`, `writeSettingsToDisk` (persists the auto-generated relay signing keypair).
69
72
  - Returned API:
70
- - `addOrUpdateApnsToken(uiSessionToken, deviceToken, userAgent)` — also binds a newly-seen token on the relay (signed `/v1/push/register-token`).
73
+ - `addOrUpdateApnsToken(uiSessionToken, deviceToken, userAgent, platform?, locale?)` — also binds a newly-seen token on the relay (signed `/v1/push/register-token`). Persists app UI `locale` for localized push titles.
71
74
  - `removeApnsToken(uiSessionToken, deviceToken)`
72
75
  - `removeApnsTokenFromAllSessions(deviceToken)`
73
- - `sendApnsToAllUiSessions(payload)` — signs + sends to all registered tokens (no UI-visibility gate; iOS suppresses the foreground banner). No-ops with a single warning when APNs is unconfigured. Drops tokens on `410` / `BadDeviceToken` / `Unregistered`.
76
+ - `addOrUpdateLiveActivityToken(uiSessionToken, token, activityId, sessionId)` — idempotent upsert bound to the UI session + OpenCode `sessionId` + ActivityKit `activityId`. Token refresh replaces that activity's previous token. Caps per UI session and lazily expires stale entries. Binds on the relay (`/v1/push/register-live-activity-token`, `kind: "liveactivity"`). Does not log tokens or session ids.
77
+ - `removeLiveActivityToken(uiSessionToken, token)` — owning UI session only; unregisters from the relay when no local copies remain.
78
+ - `sendLiveActivityEnd({ sessionId, status, eventVersion?, endedAt? })` — looks up Live Activity tokens for that OpenCode session and sends `event: "end"` with `contentState` `{ status, eventVersion, updatedAt, endedAt }` only. `complete` dismisses after 15 minutes, `error` after 60 minutes (same as native). `staleDate` (`updatedAt + 20min`) is update-only and omitted on end. `eventVersion` is monotonic per session using current milliseconds. Relay body follows the signed live-activity contract; direct mode uses token auth, topic `${bundleId}.push-type.liveactivity`, `apns-push-type: liveactivity`, and no alert. APNs payloads must not carry `sessionId` or user content. Successfully accepted tokens are cleared locally; failures do not drop alert tokens or block ordinary push.
79
+ - `sendApnsToAllUiSessions(payload)` — localizes scenario titles per stored token locale (`apns-titles.js`), groups tokens by locale, then signs + sends (no UI-visibility gate; iOS suppresses the foreground banner). Before a relay send, each token is checked against the in-memory registration success cache for the current register URL; cache hits skip `/v1/push/register-token`, misses are registered with bounded concurrency, and only successfully bound tokens are sent. Switching Relay/Push URL clears that cache so the next send re-binds persisted tokens to the new origin. No-ops with a single warning when APNs is unconfigured. Drops tokens on `410` / `BadDeviceToken` / `Unregistered`. Never sends Live Activity tokens.
80
+ - `reRegisterAllTokens()` — binds every persisted device token to the current Push origin (same success cache and URL-switch invalidation as send).
74
81
  - `resolveApnsConfig()`
75
- - Configuration (env first, then `settings.apnsConfig`): `OPENCHAMBER_APNS_KEY_ID`, `OPENCHAMBER_APNS_TEAM_ID`, `OPENCHAMBER_APNS_P8` (PEM contents; literal `\n` accepted) or `OPENCHAMBER_APNS_P8_PATH`, `OPENCHAMBER_APNS_BUNDLE_ID` (default `com.openchamber.app`), `OPENCHAMBER_APNS_ENVIRONMENT` (`sandbox` default, or `production`).
82
+ - Configuration (env first, then `settings.apnsConfig`): `OPENCHAMBER_PUSH_RELAY_URL` (optional send-URL override), `OPENCHAMBER_PUSH_RELAY_DISABLED`, `OPENCHAMBER_APNS_KEY_ID`, `OPENCHAMBER_APNS_TEAM_ID`, `OPENCHAMBER_APNS_P8` (PEM contents; literal `\n` accepted) or `OPENCHAMBER_APNS_P8_PATH`, `OPENCHAMBER_APNS_BUNDLE_ID` (default `com.yee94.openchamber`), `OPENCHAMBER_APNS_ENVIRONMENT` (`sandbox` default, or `production` for TestFlight and App Store).
76
83
 
77
84
  ### Emitter runtime API (emitter-runtime.js)
78
85
  - `createNotificationEmitterRuntime(dependencies)`: creates runtime for unified notification emission channels.