@openchambery/web 1.19.7-beta.2 → 1.19.7-beta.20

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 (185) hide show
  1. package/dist/assets/AgentAvatar-DI2zs2WH.js +1 -0
  2. package/dist/assets/AssistantView-C0p-8f2B.js +2 -0
  3. package/dist/assets/DiagramView-CSXRZQ5S.js +1 -0
  4. package/dist/assets/FadeInOnReveal-BWJcUp5P.js +1 -0
  5. package/dist/assets/Icon-4My8YZwt.js +1 -0
  6. package/dist/assets/MarkdownRendererImpl-C6rkoZYH.js +7 -0
  7. package/dist/assets/{MarkstreamRendererImpl-CR0fBG5w.css → MarkstreamRendererImpl-Ca7RATbs.css} +1 -1
  8. package/dist/assets/MarkstreamRendererImpl-CowOCU5W.js +1 -0
  9. package/dist/assets/MobileDetailNavigation-ACMbLTqb.js +1 -0
  10. package/dist/assets/MobileShareBridge-CXR-B1xW.js +4 -0
  11. package/dist/assets/MobileSurface-CKBSKqUH.js +2 -0
  12. package/dist/assets/{MultiRunWindow-khpnWyZO.js → MultiRunWindow-el9_nl25.js} +1 -1
  13. package/dist/assets/OnboardingScreen-BNEtPmvw.js +1 -0
  14. package/dist/assets/OverlayScrollbar-BZB21Yp_.js +2 -0
  15. package/dist/assets/SettingsView-CVSiIRia.js +25 -0
  16. package/dist/assets/{SettingsWindow-Dx5rU9v5.js → SettingsWindow-BXYswtp5.js} +1 -1
  17. package/dist/assets/TerminalView-Bh0IQwa5.js +8 -0
  18. package/dist/assets/{ToolOutputDialog-BfuXazW9.js → ToolOutputDialog-XeIJ2etT.js} +3 -3
  19. package/dist/assets/appThemeRegistry-rSdmh4sW.js +2 -0
  20. package/dist/assets/{button-z0GOBtWi.js → button-Or0daT9o.js} +1 -1
  21. package/dist/assets/clipboard-BXxLshVq.js +1 -0
  22. package/dist/assets/{collapsible-D9zJRpeB.js → collapsible-C-kzSGkq.js} +1 -1
  23. package/dist/assets/context-menu-Dpjla5p4.js +1 -0
  24. package/dist/assets/desktopHosts-BzSE0sK7.js +1 -0
  25. package/dist/assets/desktopRelayRestore-BaKjk1kQ.js +1 -0
  26. package/dist/assets/{diagram-CoEqmReY.js → diagram-D0mT3mo1.js} +1 -1
  27. package/dist/assets/{dialog-HCE4eLUW.js → dialog-BkhHBTd4.js} +1 -1
  28. package/dist/assets/dropdown-menu.styles-Bzo6L3FH.js +1 -0
  29. package/dist/assets/{es-D9KcV5TX.js → es-BHqnSjoZ.js} +1 -1
  30. package/dist/assets/fileQueries-DFY6Kiy1.js +1 -0
  31. package/dist/assets/fileReferenceActions-3pPDAJSl.js +1 -0
  32. package/dist/assets/{fr-DPA9qUl3.js → fr-BfrlJY9i.js} +1 -1
  33. package/dist/assets/{gitApi-DNTMAa3F.js → gitApi-Uvb9w70Y.js} +3 -3
  34. package/dist/assets/{input-BG3VWTIF.js → input-BUsGnzwe.js} +1 -1
  35. package/dist/assets/insertionBoundaries-Bgec5nsP.js +4 -0
  36. package/dist/assets/{ja-Dsif3NoD.js → ja-DP-7qt-x.js} +1 -1
  37. package/dist/assets/{ko-Dixx4Ctg.js → ko-E2B5vGQP.js} +1 -1
  38. package/dist/assets/main-COfQt8Sj.js +169 -0
  39. package/dist/assets/main-PPlpRj1-.js +2 -0
  40. package/dist/assets/markdown-worker-wEFz5Hm_.js +3 -0
  41. package/dist/assets/markdownHeightCache-Bj707yG-.js +2 -0
  42. package/dist/assets/markdownTheme-CVS01wHf.js +2 -0
  43. package/dist/assets/miniChat-EZGRqQZr.js +2 -0
  44. package/dist/assets/mobile-DQNqMo5J.js +2 -0
  45. package/dist/assets/multirun-EViX73vm.js +4 -0
  46. package/dist/assets/outsideFileGrants-BB4GPvCq.js +1 -0
  47. package/dist/assets/path-utils-jGozTg9d.js +1 -0
  48. package/dist/assets/persistence-CSZp1CNB.js +3 -0
  49. package/dist/assets/{pl-Bj78ZGgf.js → pl-r8X5gRM5.js} +1 -1
  50. package/dist/assets/platform-CenlWqxu.js +2 -0
  51. package/dist/assets/projectMeta-Bv1L_nEb.js +3 -0
  52. package/dist/assets/{pt-BR-CFL3xSAf.js → pt-BR-DcpdXCf5.js} +1 -1
  53. package/dist/assets/renderElectronMiniChatApp-Nsq4Wc4s.js +2 -0
  54. package/dist/assets/renderMobileApp-5WJDYaMR.js +28 -0
  55. package/dist/assets/runtimeConfig-DKUQS4iY.js +2 -0
  56. package/dist/assets/runtimeEndpointReset-BMeu9Hqj.js +147 -0
  57. package/dist/assets/sessionChangeSummary-CX2MJIkc.js +1 -0
  58. package/dist/assets/sessionLookup-Sb2HN3G2.js +1 -0
  59. package/dist/assets/sortable-tabs-strip-C5gGnikj.js +1 -0
  60. package/dist/assets/src-Af9cMFKF.css +2 -0
  61. package/dist/assets/{store-DzpbXWX1.js → store-BDFdnk6l.js} +2 -2
  62. package/dist/assets/terminalApi-CIIs9vFx.js +1 -0
  63. package/dist/assets/textarea-CIO34TZz.js +1 -0
  64. package/dist/assets/{tooltip-CtH61mQh.js → tooltip-BSrJYYYc.js} +1 -1
  65. package/dist/assets/{uk-BY60Ub3Z.js → uk-Di0vl9V-.js} +1 -1
  66. package/dist/assets/url-CE_gAfNa.js +1 -0
  67. package/dist/assets/{ChatContainer-CnOrdAgs.js → useAppFontEffects-B_6vc0u3.js} +46 -41
  68. package/dist/assets/useAssistantUIStore-BHacrxe_.js +1 -0
  69. package/dist/assets/useDesktopWindowControlsLayout-B2L5PWHW.js +1 -0
  70. package/dist/assets/useEffectiveDirectory-CBd5vt8N.js +1 -0
  71. package/dist/assets/useFontPreferences-BxOHKuVw.js +1 -0
  72. package/dist/assets/useMobileNavigationStore-CUGaydRW.js +412 -0
  73. package/dist/assets/useSessionAutoCleanup-Clhzc1wn.js +1 -0
  74. package/dist/assets/useUIStore-CqqO11ZI.js +1 -0
  75. package/dist/assets/useWorkerHighlightedLines-C4iQlbPQ.js +7 -0
  76. package/dist/assets/utils-B0mQXN9L.js +1 -0
  77. package/dist/assets/{vendor-markstream-react-CwvbEg9R.js → vendor-markstream-react-C3K8YH0d.js} +4 -4
  78. package/dist/assets/{zh-CN-Cox1LaOW.js → zh-CN-coFCfeQE.js} +1 -1
  79. package/dist/assets/{zh-TW-B6RU0mxa.js → zh-TW-oDRZ1-yb.js} +1 -1
  80. package/dist/index.html +9 -11
  81. package/dist/mini-chat.html +9 -11
  82. package/dist/mobile.html +15 -17
  83. package/package.json +2 -1
  84. package/server/index.js +2 -0
  85. package/server/lib/assistant-delivery-parts.js +3 -1
  86. package/server/lib/assistant-delivery-parts.test.js +11 -0
  87. package/server/lib/assistants/DOCUMENTATION.md +76 -3
  88. package/server/lib/assistants/assign.js +225 -0
  89. package/server/lib/assistants/assign.test.js +118 -0
  90. package/server/lib/assistants/bubbles.js +45 -0
  91. package/server/lib/assistants/bubbles.test.js +36 -0
  92. package/server/lib/assistants/cards.js +148 -0
  93. package/server/lib/assistants/cards.test.js +116 -0
  94. package/server/lib/assistants/contact-store.js +284 -0
  95. package/server/lib/assistants/contact-store.test.js +131 -0
  96. package/server/lib/assistants/contact-tools.js +761 -0
  97. package/server/lib/assistants/contact-tools.test.js +328 -0
  98. package/server/lib/assistants/contracts.js +20 -1
  99. package/server/lib/assistants/harness.js +551 -0
  100. package/server/lib/assistants/harness.test.js +607 -0
  101. package/server/lib/assistants/routes.js +44 -4
  102. package/server/lib/assistants/service.js +930 -17
  103. package/server/lib/assistants/service.test.js +1163 -45
  104. package/server/lib/llm/DOCUMENTATION.md +84 -0
  105. package/server/lib/llm/catalog.js +100 -0
  106. package/server/lib/llm/catalog.test.js +78 -0
  107. package/server/lib/llm/completions.js +151 -0
  108. package/server/lib/llm/completions.test.js +127 -0
  109. package/server/lib/llm/generate.js +570 -0
  110. package/server/lib/llm/generate.test.js +459 -0
  111. package/server/lib/llm/routes.js +47 -0
  112. package/server/lib/llm/temp-directory.js +37 -0
  113. package/server/lib/notifications/APNS.md +4 -4
  114. package/server/lib/notifications/DOCUMENTATION.md +1 -1
  115. package/server/lib/notifications/apns-runtime.js +139 -20
  116. package/server/lib/notifications/apns-runtime.test.js +66 -7
  117. package/server/lib/notifications/apns-titles.js +62 -62
  118. package/server/lib/notifications/apns-titles.test.js +3 -1
  119. package/server/lib/notifications/routes.js +1 -1
  120. package/server/lib/notifications/runtime.js +81 -49
  121. package/server/lib/notifications/runtime.test.js +61 -0
  122. package/server/lib/opencode/feature-routes-runtime.js +30 -0
  123. package/server/lib/opencode/feature-routes-runtime.test.js +20 -0
  124. package/server/lib/opencode/settings-runtime.js +18 -2
  125. package/server/lib/session-goal/DOCUMENTATION.md +8 -1
  126. package/server/lib/session-goal/runtime.js +20 -0
  127. package/server/lib/session-index/DOCUMENTATION.md +3 -1
  128. package/server/lib/session-index/service.js +10 -4
  129. package/server/lib/session-index/service.test.js +37 -0
  130. package/server/lib/session-title/runtime.js +1 -0
  131. package/server/lib/session-title/runtime.test.js +11 -0
  132. package/dist/assets/AgentAvatar-ziUs4sw3.js +0 -1
  133. package/dist/assets/AssistantView-DF4yXFgS.js +0 -1
  134. package/dist/assets/ContextPanel-Cg_uCHzJ.js +0 -285
  135. package/dist/assets/DiagramView-DYh3Zn2y.js +0 -1
  136. package/dist/assets/FadeInOnReveal-BAlsRFJH.js +0 -1
  137. package/dist/assets/Icon-CLpn1mIz.js +0 -1
  138. package/dist/assets/MarkdownRendererImpl-QzUEpBEm.js +0 -7
  139. package/dist/assets/MarkstreamRendererImpl-CXrfeUia.js +0 -1
  140. package/dist/assets/MobileDetailNavigation-DJXEYfq4.js +0 -1
  141. package/dist/assets/MobileShareBridge-CliVo1YH.js +0 -4
  142. package/dist/assets/MobileSurface-9Xh6WQkY.js +0 -2
  143. package/dist/assets/OnboardingScreen-CJYyR54B.js +0 -1
  144. package/dist/assets/OverlayScrollbar-C-jZ8ITG.js +0 -2
  145. package/dist/assets/SettingsView-DyCZehFG.js +0 -25
  146. package/dist/assets/TerminalView-XSALfUaH.js +0 -8
  147. package/dist/assets/appThemeRegistry-DDWrniMM.js +0 -2
  148. package/dist/assets/clipboard-Bz0Zm9VJ.js +0 -1
  149. package/dist/assets/desktop-CfLlmafK.js +0 -1
  150. package/dist/assets/desktopHosts-C1Ns3DUu.js +0 -1
  151. package/dist/assets/desktopRelayRestore-LBt0CBOj.js +0 -1
  152. package/dist/assets/fileQueries-Dnn15WsA.js +0 -1
  153. package/dist/assets/insertionBoundaries-DMxuUnL_.js +0 -4
  154. package/dist/assets/main-B9HVtgXE.js +0 -2
  155. package/dist/assets/main-ZNyIs3Sn.js +0 -29
  156. package/dist/assets/markdown-worker-y3ewaqAK.js +0 -4
  157. package/dist/assets/markdownHeightCache-CvUyZT4q.js +0 -1
  158. package/dist/assets/markdownTheme-TdhQOZSN.js +0 -2
  159. package/dist/assets/miniChat-q0T0WGkZ.js +0 -2
  160. package/dist/assets/mobile-DEduvLJj.js +0 -2
  161. package/dist/assets/multirun-DU-wrxlG.js +0 -4
  162. package/dist/assets/persistence-CmjKImz2.js +0 -3
  163. package/dist/assets/platform-skqcMARt.js +0 -2
  164. package/dist/assets/projectMeta-DNWx6Uva.js +0 -3
  165. package/dist/assets/radio-BEonIX1k.js +0 -1
  166. package/dist/assets/renderElectronMiniChatApp--JYWY41L.js +0 -2
  167. package/dist/assets/renderMobileApp-B0Ix_uNo.js +0 -25
  168. package/dist/assets/runtime-switch-BkpTTbEi.js +0 -1
  169. package/dist/assets/runtimeConfig-BTDIG7mX.js +0 -2
  170. package/dist/assets/runtimeEndpointReset-CZQCYtVI.js +0 -4
  171. package/dist/assets/shikiHighlight-C9Cx56Qg.js +0 -1
  172. package/dist/assets/sortable-tabs-strip-Bj8N0VWf.js +0 -1
  173. package/dist/assets/src-2eFAiBLq.css +0 -2
  174. package/dist/assets/terminalApi-CNqqwjfG.js +0 -1
  175. package/dist/assets/url-KeihbE0d.js +0 -1
  176. package/dist/assets/useAppFontEffects-CKLSamae.js +0 -7
  177. package/dist/assets/useAssistantUIStore-DaplPyb0.js +0 -1
  178. package/dist/assets/useDesktopWindowControlsLayout-DJSkFfhT.js +0 -1
  179. package/dist/assets/useEffectiveDirectory-B3rbY1Kv.js +0 -1
  180. package/dist/assets/useFontPreferences-D8emhcNv.js +0 -1
  181. package/dist/assets/useMobileNavigationStore-CAvVUItx.js +0 -412
  182. package/dist/assets/useSessionAutoCleanup-DxNgqJ8s.js +0 -1
  183. package/dist/assets/useUIStore-B7KJS5G7.js +0 -1
  184. package/dist/assets/useWorkerHighlightedLines-DiDF68LR.js +0 -7
  185. package/dist/assets/utils-CTL16t2f.js +0 -1
@@ -36,6 +36,72 @@ const LIVE_ACTIVITY_TOKEN_TTL_MS = 24 * 60 * 60 * 1000;
36
36
  const LIVE_ACTIVITY_COMPLETE_DISMISSAL_SECONDS = 15 * 60;
37
37
  const LIVE_ACTIVITY_ERROR_DISMISSAL_SECONDS = 60 * 60;
38
38
  const LIVE_ACTIVITY_STALE_SECONDS = 20 * 60;
39
+ const LIVE_ACTIVITY_AGGREGATE_SESSION_ID = 'live';
40
+ const MAX_LIVE_ACTIVITY_SNAPSHOT_ITEMS = 4;
41
+ const LIVE_ACTIVITY_ITEM_STATUSES = new Set([
42
+ 'working', 'tool', 'retry', 'input', 'permission', 'stale', 'complete', 'error',
43
+ ]);
44
+
45
+ const parseLiveActivitySnapshot = (value) => {
46
+ if (!Array.isArray(value)) return undefined;
47
+ const items = [];
48
+ for (const raw of value.slice(0, MAX_LIVE_ACTIVITY_SNAPSHOT_ITEMS)) {
49
+ if (!raw || typeof raw !== 'object') continue;
50
+ const sessionId = typeof raw.sessionId === 'string'
51
+ ? raw.sessionId.trim()
52
+ : (typeof raw.sessionID === 'string' ? raw.sessionID.trim() : '');
53
+ if (!sessionId || sessionId.length > MAX_LIVE_ACTIVITY_ID_CHARS) continue;
54
+ const status = typeof raw.status === 'string' && LIVE_ACTIVITY_ITEM_STATUSES.has(raw.status)
55
+ ? raw.status
56
+ : 'working';
57
+ const startedAt = Number(raw.startedAt);
58
+ if (!Number.isFinite(startedAt)) continue;
59
+ const item = {
60
+ sessionId,
61
+ title: typeof raw.title === 'string' ? raw.title.slice(0, 80) : '',
62
+ status,
63
+ startedAt,
64
+ };
65
+ const endedAt = Number(raw.endedAt);
66
+ if (Number.isFinite(endedAt)) item.endedAt = endedAt;
67
+ items.push(item);
68
+ }
69
+ return items;
70
+ };
71
+
72
+ const liveActivityItemIsWorking = (status) => status !== 'complete' && status !== 'error';
73
+
74
+ const liveActivityEntryCoversSession = (entry, sessionId) => {
75
+ if (entry.sessionId === sessionId) return true;
76
+ if (Array.isArray(entry.snapshot) && entry.snapshot.length > 0) {
77
+ return entry.snapshot.some((item) => item.sessionId === sessionId);
78
+ }
79
+ return entry.sessionId === LIVE_ACTIVITY_AGGREGATE_SESSION_ID;
80
+ };
81
+
82
+ const contentStateFromSnapshot = (snapshot, status, eventVersion, updatedAt, endedAt) => {
83
+ const working = snapshot.filter((item) => liveActivityItemIsWorking(item.status));
84
+ const primary = working[0] ?? snapshot[0];
85
+ const contentState = {
86
+ status,
87
+ eventVersion,
88
+ updatedAt,
89
+ workingCount: working.length,
90
+ items: snapshot.map((item) => {
91
+ const next = {
92
+ sessionID: item.sessionId,
93
+ title: item.title,
94
+ status: item.status,
95
+ startedAt: item.startedAt,
96
+ };
97
+ if (item.endedAt !== undefined) next.endedAt = item.endedAt;
98
+ return next;
99
+ }),
100
+ };
101
+ if (primary?.title) contentState.title = primary.title;
102
+ if (endedAt !== undefined) contentState.endedAt = endedAt;
103
+ return contentState;
104
+ };
39
105
  const MAX_RELAY_SEND_TOKENS = 100;
40
106
  const RELAY_REGISTER_CONCURRENCY = 16;
41
107
  const APNS_COLLAPSE_ID_MAX_BYTES = 64;
@@ -356,7 +422,8 @@ export const createApnsRuntime = (deps) => {
356
422
  const createdAt = typeof entry.createdAt === 'number' ? entry.createdAt : null;
357
423
  const lastSeenAt = typeof entry.lastSeenAt === 'number' ? entry.lastSeenAt : createdAt;
358
424
  if (typeof lastSeenAt === 'number' && now - lastSeenAt > LIVE_ACTIVITY_TOKEN_TTL_MS) return null;
359
- return { token, activityId, sessionId, createdAt, lastSeenAt };
425
+ const snapshot = parseLiveActivitySnapshot(entry.snapshot);
426
+ return { token, activityId, sessionId, snapshot, createdAt, lastSeenAt };
360
427
  })
361
428
  .filter(Boolean);
362
429
  };
@@ -464,7 +531,7 @@ export const createApnsRuntime = (deps) => {
464
531
  });
465
532
  };
466
533
 
467
- const addOrUpdateLiveActivityToken = async (uiSessionToken, token, activityId, sessionId) => {
534
+ const addOrUpdateLiveActivityToken = async (uiSessionToken, token, activityId, sessionId, snapshot) => {
468
535
  if (!uiSessionToken) return;
469
536
  const trimmedToken = typeof token === 'string' ? token.trim() : '';
470
537
  const trimmedActivityId = typeof activityId === 'string' ? activityId.trim() : '';
@@ -486,10 +553,12 @@ export const createApnsRuntime = (deps) => {
486
553
  const filtered = existing.filter(
487
554
  (entry) => entry.activityId !== trimmedActivityId && entry.token !== trimmedToken,
488
555
  );
556
+ const nextSnapshot = parseLiveActivitySnapshot(snapshot) ?? replaced?.snapshot;
489
557
  filtered.unshift({
490
558
  token: trimmedToken,
491
559
  activityId: trimmedActivityId,
492
560
  sessionId: trimmedSessionId,
561
+ snapshot: nextSnapshot,
493
562
  createdAt: typeof replaced?.createdAt === 'number' ? replaced.createdAt : now,
494
563
  lastSeenAt: now,
495
564
  });
@@ -590,7 +659,7 @@ export const createApnsRuntime = (deps) => {
590
659
  teamId,
591
660
  p8,
592
661
  bundleId: bundleId || DEFAULT_BUNDLE_ID,
593
- environment: environment === 'production' ? 'production' : 'sandbox',
662
+ environment: (environment || 'production') === 'production' ? 'production' : 'sandbox',
594
663
  };
595
664
  };
596
665
 
@@ -710,7 +779,7 @@ export const createApnsRuntime = (deps) => {
710
779
  liveActivityUnregisterUrl: url.replace(/\/send$/, '/unregister-live-activity-token'),
711
780
  liveActivitySendUrl: url.replace(/\/send$/, '/live-activity'),
712
781
  environment:
713
- (trimmedEnv('OPENCHAMBER_APNS_ENVIRONMENT') || 'sandbox').toLowerCase() === 'production'
782
+ (trimmedEnv('OPENCHAMBER_APNS_ENVIRONMENT') || 'production').toLowerCase() === 'production'
714
783
  ? 'production'
715
784
  : 'sandbox',
716
785
  });
@@ -857,9 +926,13 @@ export const createApnsRuntime = (deps) => {
857
926
  return { dismissalDate: liveActivityDismissalDate(status, nowMs) };
858
927
  };
859
928
 
860
- const liveActivityRelaySignMessage = ({ ts, tokens, event, contentState, dismissalDate, staleDate }) => (
861
- `${ts}.${[...tokens].sort().join(',')}.${event}.${contentState.status}.${contentState.eventVersion}.${contentState.updatedAt}.${contentState.endedAt ?? ''}.${dismissalDate ?? ''}.${staleDate ?? ''}`
862
- );
929
+ const liveActivityRelaySignMessage = ({ ts, tokens, event, contentState, dismissalDate, staleDate }) => {
930
+ let message = `${ts}.${[...tokens].sort().join(',')}.${event}.${contentState.status}.${contentState.eventVersion}.${contentState.updatedAt}.${contentState.endedAt ?? ''}.${dismissalDate ?? ''}.${staleDate ?? ''}`;
931
+ if (contentState.title !== undefined || contentState.workingCount !== undefined || contentState.items !== undefined) {
932
+ message += `.${contentState.title ?? ''}.${contentState.workingCount ?? ''}.${JSON.stringify(contentState.items ?? [])}`;
933
+ }
934
+ return message;
935
+ };
863
936
 
864
937
  const sendLiveActivityViaRelay = async (deviceTokens, payload, relay) => {
865
938
  if (!Array.isArray(deviceTokens) || deviceTokens.length === 0) return [];
@@ -1039,7 +1112,7 @@ export const createApnsRuntime = (deps) => {
1039
1112
  const seen = new Set();
1040
1113
  for (const record of Object.values(pruneLiveActivityTokensBySession(store.liveActivityTokensBySession, now))) {
1041
1114
  for (const entry of record) {
1042
- if (entry.sessionId !== sessionId || seen.has(entry.token)) continue;
1115
+ if (!liveActivityEntryCoversSession(entry, sessionId) || seen.has(entry.token)) continue;
1043
1116
  seen.add(entry.token);
1044
1117
  entries.push(entry);
1045
1118
  }
@@ -1047,14 +1120,18 @@ export const createApnsRuntime = (deps) => {
1047
1120
  return entries;
1048
1121
  };
1049
1122
 
1050
- const sendLiveActivityEnd = async ({ sessionId, status, eventVersion, endedAt } = {}) => {
1123
+ const sendLiveActivityEnd = async ({ sessionId, status, eventVersion, endedAt, title } = {}) => {
1051
1124
  const trimmedSessionId = typeof sessionId === 'string' ? sessionId.trim() : '';
1052
1125
  if (!isLimitedId(trimmedSessionId, MAX_LIVE_ACTIVITY_ID_CHARS)) return;
1053
- if (!LIVE_ACTIVITY_TERMINAL_STATUSES.has(status)) return;
1126
+ const nextStatus = LIVE_ACTIVITY_TERMINAL_STATUSES.has(status) ? status : undefined;
1127
+ const nextTitle = typeof title === 'string' ? title.trim().slice(0, 80) : '';
1128
+ if (!nextStatus && !nextTitle) return;
1054
1129
 
1055
1130
  const now = Date.now();
1056
1131
  let pendingEntries = [];
1057
1132
  let nextVersion = 0;
1133
+ let event = 'end';
1134
+ let snapshotForSend;
1058
1135
  await persistTokenUpdate((current) => {
1059
1136
  const liveActivityTokensBySession = pruneLiveActivityTokensBySession(
1060
1137
  current.liveActivityTokensBySession,
@@ -1068,31 +1145,73 @@ export const createApnsRuntime = (deps) => {
1068
1145
  if (pendingEntries.length === 0) {
1069
1146
  return { ...current, liveActivityTokensBySession };
1070
1147
  }
1071
- const previous = current.liveActivityEventVersions?.[trimmedSessionId];
1148
+ const previous = current.liveActivityEventVersions?.[trimmedSessionId]
1149
+ ?? current.liveActivityEventVersions?.[LIVE_ACTIVITY_AGGREGATE_SESSION_ID];
1072
1150
  nextVersion = nextLiveActivityEventVersion(now, previous, eventVersion);
1151
+ const endedAtSeconds = toLiveActivityUnixSeconds(endedAt, now);
1152
+ for (const [uiSession, record] of Object.entries(liveActivityTokensBySession)) {
1153
+ liveActivityTokensBySession[uiSession] = record.map((entry) => {
1154
+ if (!liveActivityEntryCoversSession(entry, trimmedSessionId)) return entry;
1155
+ if (!Array.isArray(entry.snapshot) || entry.snapshot.length === 0) return entry;
1156
+ const snapshot = entry.snapshot.map((item) => {
1157
+ if (item.sessionId !== trimmedSessionId) return item;
1158
+ const patched = { ...item };
1159
+ if (nextTitle) patched.title = nextTitle;
1160
+ if (nextStatus) {
1161
+ patched.status = nextStatus;
1162
+ patched.endedAt = endedAtSeconds;
1163
+ }
1164
+ return patched;
1165
+ });
1166
+ snapshotForSend = snapshot;
1167
+ return { ...entry, snapshot };
1168
+ });
1169
+ }
1170
+ const working = Array.isArray(snapshotForSend)
1171
+ ? snapshotForSend.filter((item) => liveActivityItemIsWorking(item.status))
1172
+ : [];
1173
+ event = nextStatus && working.length === 0 ? 'end' : 'update';
1174
+ if (!nextStatus) event = 'update';
1073
1175
  return {
1074
1176
  ...current,
1075
1177
  liveActivityTokensBySession,
1076
1178
  liveActivityEventVersions: {
1077
1179
  ...(current.liveActivityEventVersions || {}),
1078
1180
  [trimmedSessionId]: nextVersion,
1181
+ [LIVE_ACTIVITY_AGGREGATE_SESSION_ID]: nextVersion,
1079
1182
  },
1080
1183
  };
1081
1184
  });
1082
1185
  if (pendingEntries.length === 0) return;
1186
+ if (!nextStatus && !(Array.isArray(snapshotForSend) && snapshotForSend.length > 0)) return;
1083
1187
 
1084
1188
  const updatedAt = now / 1000;
1085
1189
  const endedAtSeconds = toLiveActivityUnixSeconds(endedAt, now);
1086
- const contentState = {
1087
- status,
1088
- eventVersion: nextVersion,
1089
- updatedAt,
1090
- endedAt: endedAtSeconds,
1091
- };
1190
+ const aggregateStatus = event === 'end'
1191
+ ? (nextStatus || 'complete')
1192
+ : (Array.isArray(snapshotForSend)
1193
+ ? (snapshotForSend.some((item) => liveActivityItemIsWorking(item.status))
1194
+ ? (snapshotForSend.find((item) => liveActivityItemIsWorking(item.status))?.status || 'working')
1195
+ : (nextStatus || 'working'))
1196
+ : (nextStatus || 'working'));
1197
+ const contentState = Array.isArray(snapshotForSend) && snapshotForSend.length > 0
1198
+ ? contentStateFromSnapshot(
1199
+ snapshotForSend,
1200
+ aggregateStatus,
1201
+ nextVersion,
1202
+ updatedAt,
1203
+ event === 'end' ? endedAtSeconds : undefined,
1204
+ )
1205
+ : {
1206
+ status: nextStatus || 'complete',
1207
+ eventVersion: nextVersion,
1208
+ updatedAt,
1209
+ ...(event === 'end' ? { endedAt: endedAtSeconds } : {}),
1210
+ };
1092
1211
  const payload = {
1093
- event: 'end',
1212
+ event,
1094
1213
  contentState,
1095
- ...liveActivityPushDates('end', status, updatedAt, now),
1214
+ ...liveActivityPushDates(event, contentState.status, updatedAt, now),
1096
1215
  };
1097
1216
 
1098
1217
  const tokens = pendingEntries.map((entry) => entry.token);
@@ -1121,7 +1240,7 @@ export const createApnsRuntime = (deps) => {
1121
1240
  return;
1122
1241
  }
1123
1242
 
1124
- if (accepted.length > 0) {
1243
+ if (event === 'end' && accepted.length > 0) {
1125
1244
  await removeLiveActivityTokens(accepted);
1126
1245
  }
1127
1246
  };
@@ -115,6 +115,25 @@ describe('apns runtime bundle ID', () => {
115
115
  });
116
116
  });
117
117
 
118
+ describe('apns runtime APNs environment', () => {
119
+ it('defaults resolveApnsConfig environment to production when env and settings omit it', async () => {
120
+ process.env.OPENCHAMBER_APNS_KEY_ID = 'KEY123';
121
+ process.env.OPENCHAMBER_APNS_TEAM_ID = 'TEAM123';
122
+ process.env.OPENCHAMBER_APNS_P8 = P8;
123
+ const runtime = createApnsRuntime(makeDeps());
124
+ await expect(runtime.resolveApnsConfig()).resolves.toMatchObject({ environment: 'production' });
125
+ });
126
+
127
+ it('uses sandbox when OPENCHAMBER_APNS_ENVIRONMENT=sandbox', async () => {
128
+ process.env.OPENCHAMBER_APNS_KEY_ID = 'KEY123';
129
+ process.env.OPENCHAMBER_APNS_TEAM_ID = 'TEAM123';
130
+ process.env.OPENCHAMBER_APNS_P8 = P8;
131
+ process.env.OPENCHAMBER_APNS_ENVIRONMENT = 'sandbox';
132
+ const runtime = createApnsRuntime(makeDeps());
133
+ await expect(runtime.resolveApnsConfig()).resolves.toMatchObject({ environment: 'sandbox' });
134
+ });
135
+ });
136
+
118
137
  describe('apns runtime relay mode (default)', () => {
119
138
  it('registers tokens (signed) and posts signed generic text, dropping dead tokens', async () => {
120
139
  const fetchMock = vi.fn(async (url) =>
@@ -618,9 +637,9 @@ describe('apns runtime push relay derivation and re-register', () => {
618
637
  expect(sendBodies).toHaveLength(3);
619
638
 
620
639
  const byTitle = Object.fromEntries(sendBodies.map((body) => [body.title, body]));
621
- expect(byTitle['Agent response is ready']?.tokens).toEqual(['tokenEn']);
622
- expect(byTitle['智能体回复已就绪']?.tokens).toEqual(['tokenZh']);
623
- expect(byTitle['エージェントの応答が準備できました']?.tokens).toEqual(['tokenJa']);
640
+ expect(byTitle['Task completed']?.tokens).toEqual(['tokenEn']);
641
+ expect(byTitle['任务已完成']?.tokens).toEqual(['tokenZh']);
642
+ expect(byTitle['タスク完了']?.tokens).toEqual(['tokenJa']);
624
643
  for (const body of sendBodies) {
625
644
  expect(body.body).toBe('My session');
626
645
  expect(body.badge).toBe(2);
@@ -641,7 +660,7 @@ describe('apns runtime push relay derivation and re-register', () => {
641
660
  await runtime.sendApnsToAllUiSessions({ type: 'permission', sessionName: 'S' });
642
661
 
643
662
  const sent = JSON.parse(fetchMock.mock.calls.find(isSend)[1].body);
644
- expect(sent.title).toBe('Agent needs permission');
663
+ expect(sent.title).toBe('Needs permission');
645
664
  expect(sent.body).toBe('S');
646
665
  });
647
666
  });
@@ -840,7 +859,7 @@ describe('apns live activity routes', () => {
840
859
  body: { token: ' la-token ', activityId: ' act-1 ', sessionId: ' ses_1 ' },
841
860
  }, postRes);
842
861
  expect(postRes.body).toEqual({ ok: true });
843
- expect(addOrUpdateLiveActivityToken).toHaveBeenCalledWith('ui-token', 'la-token', 'act-1', 'ses_1');
862
+ expect(addOrUpdateLiveActivityToken).toHaveBeenCalledWith('ui-token', 'la-token', 'act-1', 'ses_1', undefined);
844
863
 
845
864
  const delRes = createMockRes();
846
865
  await getRoute('DELETE', '/api/push/live-activity-token')({ body: { token: 'la-token' } }, delRes);
@@ -906,6 +925,34 @@ describe('apns live activity relay and direct delivery', () => {
906
925
  }
907
926
  });
908
927
 
928
+ it('updates the aggregate live activity when one covered session completes', async () => {
929
+ const fetchMock = vi.fn(async () => jsonResponse({ ok: true, results: [] }));
930
+ vi.stubGlobal('fetch', fetchMock);
931
+ process.env.OPENCHAMBER_PUSH_RELAY_URL = 'https://relay.test/v1/push/send';
932
+ const runtime = createApnsRuntime(makeDeps());
933
+ const token = 'a'.repeat(64);
934
+ await runtime.addOrUpdateLiveActivityToken('s1', token, 'act-1', 'live', [
935
+ { sessionId: 'ses_a', title: 'Alpha', status: 'working', startedAt: 100 },
936
+ { sessionId: 'ses_b', title: 'Beta', status: 'working', startedAt: 100 },
937
+ ]);
938
+ fetchMock.mockClear();
939
+ await runtime.sendLiveActivityEnd({ sessionId: 'ses_a', status: 'complete' });
940
+ const sent = JSON.parse(fetchMock.mock.calls.find(isLiveActivitySend)[1].body);
941
+ expect(sent.event).toBe('update');
942
+ expect(sent.contentState.workingCount).toBe(1);
943
+ expect(sent.contentState.items).toEqual([
944
+ expect.objectContaining({ sessionID: 'ses_a', status: 'complete', title: 'Alpha' }),
945
+ expect.objectContaining({ sessionID: 'ses_b', status: 'working', title: 'Beta' }),
946
+ ]);
947
+ expect(sent.staleDate).toBeTypeOf('number');
948
+ expect(sent.dismissalDate).toBeUndefined();
949
+ await verifyRelaySignature(
950
+ sent.publicKeyJwk,
951
+ `${sent.ts}.${[...sent.tokens].sort().join(',')}.${sent.event}.${sent.contentState.status}.${sent.contentState.eventVersion}.${sent.contentState.updatedAt}.${sent.contentState.endedAt ?? ''}.${sent.dismissalDate ?? ''}.${sent.staleDate ?? ''}.${sent.contentState.title ?? ''}.${sent.contentState.workingCount ?? ''}.${JSON.stringify(sent.contentState.items ?? [])}`,
952
+ sent.sig,
953
+ ).then((ok) => expect(ok).toBe(true));
954
+ });
955
+
909
956
  it('uses a 60-minute dismissal for error ends', async () => {
910
957
  const fetchMock = vi.fn(async () => jsonResponse({ ok: true, results: [] }));
911
958
  vi.stubGlobal('fetch', fetchMock);
@@ -921,11 +968,10 @@ describe('apns live activity relay and direct delivery', () => {
921
968
  expect(sent.dismissalDate).toBeLessThanOrEqual(Math.floor(after / 1000) + 60 * 60);
922
969
  });
923
970
 
924
- it('forwards OPENCHAMBER_APNS_ENVIRONMENT=production as env on live-activity relay send', async () => {
971
+ it('defaults live-activity relay env to production when OPENCHAMBER_APNS_ENVIRONMENT is unset', async () => {
925
972
  const fetchMock = vi.fn(async () => jsonResponse({ ok: true, results: [] }));
926
973
  vi.stubGlobal('fetch', fetchMock);
927
974
  process.env.OPENCHAMBER_PUSH_RELAY_URL = 'https://relay.test/v1/push/send';
928
- process.env.OPENCHAMBER_APNS_ENVIRONMENT = 'production';
929
975
  const runtime = createApnsRuntime(makeDeps());
930
976
  await runtime.addOrUpdateLiveActivityToken('s1', 'la-token', 'act-1', 'ses_1');
931
977
  fetchMock.mockClear();
@@ -934,6 +980,19 @@ describe('apns live activity relay and direct delivery', () => {
934
980
  expect(sent.env).toBe('production');
935
981
  });
936
982
 
983
+ it('forwards OPENCHAMBER_APNS_ENVIRONMENT=sandbox as env on live-activity relay send', async () => {
984
+ const fetchMock = vi.fn(async () => jsonResponse({ ok: true, results: [] }));
985
+ vi.stubGlobal('fetch', fetchMock);
986
+ process.env.OPENCHAMBER_PUSH_RELAY_URL = 'https://relay.test/v1/push/send';
987
+ process.env.OPENCHAMBER_APNS_ENVIRONMENT = 'sandbox';
988
+ const runtime = createApnsRuntime(makeDeps());
989
+ await runtime.addOrUpdateLiveActivityToken('s1', 'la-token', 'act-1', 'ses_1');
990
+ fetchMock.mockClear();
991
+ await runtime.sendLiveActivityEnd({ sessionId: 'ses_1', status: 'complete' });
992
+ const sent = JSON.parse(fetchMock.mock.calls.find(isLiveActivitySend)[1].body);
993
+ expect(sent.env).toBe('sandbox');
994
+ });
995
+
937
996
  it('keeps tokens after a failed relay end so a later attempt can retry', async () => {
938
997
  const fetchMock = vi.fn(async (url) => {
939
998
  if (String(url).endsWith('/live-activity')) return jsonResponse({ error: true }, 500);
@@ -20,113 +20,113 @@ export const APNS_LOCALES = Object.freeze([
20
20
 
21
21
  const TITLES = Object.freeze({
22
22
  en: Object.freeze({
23
- ready: 'Agent response is ready',
24
- error: 'Agent hit an error',
25
- question: 'Agent needs your input',
26
- permission: 'Agent needs permission',
27
- goal_complete: 'Goal complete',
23
+ ready: 'Task completed',
24
+ error: 'Something went wrong',
25
+ question: 'Needs your answer',
26
+ permission: 'Needs permission',
27
+ goal_complete: 'Goal completed',
28
28
  goal_blocked: 'Goal blocked',
29
- goal_budget: 'Goal reached its token budget',
30
- update: 'Agent update',
29
+ goal_budget: 'Token budget reached',
30
+ update: 'Update',
31
31
  session: 'Session',
32
32
  }),
33
33
  fr: Object.freeze({
34
- ready: "La réponse de l'agent est prête",
35
- error: "L'agent a rencontré une erreur",
36
- question: "L'agent a besoin de votre saisie",
37
- permission: "L'agent a besoin d'une autorisation",
38
- goal_complete: 'Objectif terminé',
34
+ ready: 'Tâche terminée',
35
+ error: "Une erreur s'est produite",
36
+ question: 'Besoin de votre réponse',
37
+ permission: 'Autorisation requise',
38
+ goal_complete: 'Objectif atteint',
39
39
  goal_blocked: 'Objectif bloqué',
40
- goal_budget: "L'objectif a atteint son budget de tokens",
41
- update: "Mise à jour de l'agent",
40
+ goal_budget: 'Budget de tokens atteint',
41
+ update: 'Mise à jour',
42
42
  session: 'Session',
43
43
  }),
44
44
  'zh-CN': Object.freeze({
45
- ready: '智能体回复已就绪',
46
- error: '智能体遇到错误',
47
- question: '智能体需要你的输入',
48
- permission: '智能体需要权限',
45
+ ready: '任务已完成',
46
+ error: '出现错误',
47
+ question: '需要你回答',
48
+ permission: '需要你授权',
49
49
  goal_complete: '目标已完成',
50
50
  goal_blocked: '目标受阻',
51
- goal_budget: '目标已达 token 预算',
52
- update: '智能体更新',
51
+ goal_budget: '已达用量上限',
52
+ update: '有新进展',
53
53
  session: '会话',
54
54
  }),
55
55
  'zh-TW': Object.freeze({
56
- ready: '智能體回覆已就緒',
57
- error: '智能體遇到錯誤',
58
- question: '智能體需要你的輸入',
59
- permission: '智能體需要權限',
56
+ ready: '任務已完成',
57
+ error: '出現錯誤',
58
+ question: '需要你回答',
59
+ permission: '需要你授權',
60
60
  goal_complete: '目標已完成',
61
61
  goal_blocked: '目標受阻',
62
- goal_budget: '目標已達 token 預算',
63
- update: '智能體更新',
62
+ goal_budget: '已達用量上限',
63
+ update: '有新進展',
64
64
  session: '工作階段',
65
65
  }),
66
66
  uk: Object.freeze({
67
- ready: 'Відповідь агента готова',
68
- error: 'Агент зіткнувся з помилкою',
69
- question: 'Агенту потрібне ваше введення',
70
- permission: 'Агенту потрібен дозвіл',
67
+ ready: 'Завдання виконано',
68
+ error: 'Сталася помилка',
69
+ question: 'Потрібна ваша відповідь',
70
+ permission: 'Потрібен дозвіл',
71
71
  goal_complete: 'Цілі досягнуто',
72
72
  goal_blocked: 'Цілі заблоковано',
73
- goal_budget: 'Ціль досягла ліміту токенів',
74
- update: 'Оновлення агента',
73
+ goal_budget: 'Досягнуто ліміт токенів',
74
+ update: 'Оновлення',
75
75
  session: 'Сесія',
76
76
  }),
77
77
  es: Object.freeze({
78
- ready: 'La respuesta del agente está lista',
79
- error: 'El agente encontró un error',
80
- question: 'El agente necesita tu entrada',
81
- permission: 'El agente necesita permiso',
78
+ ready: 'Tarea completada',
79
+ error: 'Se produjo un error',
80
+ question: 'Necesita tu respuesta',
81
+ permission: 'Necesita permiso',
82
82
  goal_complete: 'Objetivo completado',
83
83
  goal_blocked: 'Objetivo bloqueado',
84
- goal_budget: 'El objetivo alcanzó su presupuesto de tokens',
85
- update: 'Actualización del agente',
84
+ goal_budget: 'Presupuesto de tokens alcanzado',
85
+ update: 'Actualización',
86
86
  session: 'Sesión',
87
87
  }),
88
88
  'pt-BR': Object.freeze({
89
- ready: 'A resposta do agente está pronta',
90
- error: 'O agente encontrou um erro',
91
- question: 'O agente precisa da sua entrada',
92
- permission: 'O agente precisa de permissão',
89
+ ready: 'Tarefa concluída',
90
+ error: 'Ocorreu um erro',
91
+ question: 'Precisa da sua resposta',
92
+ permission: 'Precisa de permissão',
93
93
  goal_complete: 'Objetivo concluído',
94
94
  goal_blocked: 'Objetivo bloqueado',
95
- goal_budget: 'O objetivo atingiu o orçamento de tokens',
96
- update: 'Atualização do agente',
95
+ goal_budget: 'Orçamento de tokens atingido',
96
+ update: 'Atualização',
97
97
  session: 'Sessão',
98
98
  }),
99
99
  ko: Object.freeze({
100
- ready: '에이전트 응답이 준비되었습니다',
101
- error: '에이전트에서 오류가 발생했습니다',
102
- question: '에이전트가 입력을 요청합니다',
103
- permission: '에이전트가 권한이 필요합니다',
100
+ ready: '작업 완료',
101
+ error: '오류가 발생했습니다',
102
+ question: '답변이 필요합니다',
103
+ permission: '권한이 필요합니다',
104
104
  goal_complete: '목표 완료',
105
105
  goal_blocked: '목표 차단됨',
106
- goal_budget: '목표가 토큰 예산에 도달했습니다',
107
- update: '에이전트 업데이트',
106
+ goal_budget: '토큰 한도 도달',
107
+ update: '업데이트',
108
108
  session: '세션',
109
109
  }),
110
110
  pl: Object.freeze({
111
- ready: 'Odpowiedź agenta jest gotowa',
112
- error: 'Agent napotkał błąd',
113
- question: 'Agent potrzebuje Twojego wejścia',
114
- permission: 'Agent potrzebuje uprawnienia',
111
+ ready: 'Zadanie ukończone',
112
+ error: 'Wystąpił błąd',
113
+ question: 'Potrzebna twoja odpowiedź',
114
+ permission: 'Potrzebne uprawnienie',
115
115
  goal_complete: 'Cel ukończony',
116
116
  goal_blocked: 'Cel zablokowany',
117
- goal_budget: 'Cel osiągnął budżet tokenów',
118
- update: 'Aktualizacja agenta',
117
+ goal_budget: 'Osiągnięto limit tokenów',
118
+ update: 'Aktualizacja',
119
119
  session: 'Sesja',
120
120
  }),
121
121
  ja: Object.freeze({
122
- ready: 'エージェントの応答が準備できました',
123
- error: 'エージェントでエラーが発生しました',
124
- question: 'エージェントが入力を求めています',
125
- permission: 'エージェントが許可を求めています',
122
+ ready: 'タスク完了',
123
+ error: 'エラーが発生しました',
124
+ question: '回答が必要です',
125
+ permission: '許可が必要です',
126
126
  goal_complete: '目標完了',
127
127
  goal_blocked: '目標がブロックされました',
128
- goal_budget: '目標がトークン予算に達しました',
129
- update: 'エージェントの更新',
128
+ goal_budget: 'トークン上限に達しました',
129
+ update: '更新',
130
130
  session: 'セッション',
131
131
  }),
132
132
  });
@@ -33,12 +33,14 @@ describe('apns-titles', () => {
33
33
 
34
34
  it('localizes payload title by type and keeps session name as body', () => {
35
35
  expect(localizeApnsPayload({ type: 'question', sessionName: 'Build fix' }, 'zh-CN')).toEqual({
36
- title: '智能体需要你的输入',
36
+ title: '需要你回答',
37
37
  body: 'Build fix',
38
38
  badge: undefined,
39
39
  tag: undefined,
40
40
  data: undefined,
41
41
  });
42
+ expect(resolveApnsTitle('ready', 'zh-CN')).toBe('任务已完成');
43
+ expect(resolveApnsTitle('ready', 'en')).toBe('Task completed');
42
44
  expect(localizeApnsPayload({ type: 'ready' }, 'fr').body).toBe('Session');
43
45
  expect(localizeApnsPayload({ type: 'ready' }, 'zh-CN').body).toBe('会话');
44
46
  });
@@ -219,7 +219,7 @@ export const registerNotificationRoutes = (app, dependencies) => {
219
219
  }
220
220
 
221
221
  if (typeof addOrUpdateLiveActivityToken === 'function') {
222
- await addOrUpdateLiveActivityToken(uiToken, token, activityId, sessionId);
222
+ await addOrUpdateLiveActivityToken(uiToken, token, activityId, sessionId, req.body?.items);
223
223
  }
224
224
  return res.json({ ok: true });
225
225
  });