@openchambery/web 2.1.9-beta.5 → 2.1.9-beta.7

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 (53) hide show
  1. package/dist/assets/{AssistantView-BMvmzMFI.js → AssistantView-CuT6bDtM.js} +3 -3
  2. package/dist/assets/{DiagramView-BO6rDoTN.js → DiagramView-D8kZ5sgP.js} +1 -1
  3. package/dist/assets/{MarkdownRenderer-D_ZS58Ur.js → MarkdownRenderer-3Cm5B3ni.js} +2 -2
  4. package/dist/assets/{MarkdownRendererImpl-gBtduBHO.js → MarkdownRendererImpl-BSYMaqFX.js} +1 -1
  5. package/dist/assets/{MarkstreamRendererImpl-CKilBBOc.js → MarkstreamRendererImpl-B9VRdRju.js} +1 -1
  6. package/dist/assets/{MobileDetailNavigation-Dgqdqrmy.js → MobileDetailNavigation-ZFfIEYTU.js} +1 -1
  7. package/dist/assets/{MobileShareBridge-G2wjyn4g.js → MobileShareBridge-DpuXJdlq.js} +1 -1
  8. package/dist/assets/{MobileTabPageHeader-Cipo7N8C.js → MobileTabPageHeader-DYRGHein.js} +1 -1
  9. package/dist/assets/{MultiRunWindow-C_ow_lbm.js → MultiRunWindow-BaJgbZP9.js} +1 -1
  10. package/dist/assets/{SettingsView-BaLgORC_.js → SettingsView-C18d1an5.js} +1 -1
  11. package/dist/assets/{SettingsWindow-DfSPTDHb.js → SettingsWindow-B07X5mru.js} +1 -1
  12. package/dist/assets/{TerminalView-DhUynfus.js → TerminalView-BbPFQ22Z.js} +1 -1
  13. package/dist/assets/{ToolOutputDialog-DaKPwmer.js → ToolOutputDialog-Bt3ou350.js} +1 -1
  14. package/dist/assets/{appThemeRegistry-zPqSQBFx.js → appThemeRegistry-BNncg3Lg.js} +1 -1
  15. package/dist/assets/{fileMentionTouchSelection-C8mKDaBe.js → fileMentionTouchSelection-BF0RYuUw.js} +1 -1
  16. package/dist/assets/{gitApi-BCPykg93.js → gitApi-KzJZseRG.js} +1 -1
  17. package/dist/assets/{insertionBoundaries-h0pySzQJ.js → insertionBoundaries-BoN2D2CB.js} +1 -1
  18. package/dist/assets/{main-C8RDC4kz.js → main-C2COAbeT.js} +4 -4
  19. package/dist/assets/{main-4JVZhTAZ.js → main-DBLCcpJe.js} +2 -2
  20. package/dist/assets/{mcpQueries-Cm1VNAmy.js → mcpQueries-BF9h43FH.js} +1 -1
  21. package/dist/assets/{miniChat-_SvWYZuQ.js → miniChat-Bv5ORJmD.js} +2 -2
  22. package/dist/assets/{mobile-Cil7_kYn.js → mobile-BzKeTq52.js} +2 -2
  23. package/dist/assets/{multirun-BzeInZt6.js → multirun-BHANiphy.js} +1 -1
  24. package/dist/assets/{projectMeta-CUAgRGwW.js → projectMeta-CuIJ2wzX.js} +1 -1
  25. package/dist/assets/{renderElectronMiniChatApp-BrAIRgb-.js → renderElectronMiniChatApp-B4NSy0ql.js} +2 -2
  26. package/dist/assets/{renderMobileApp-DxTazPkB.js → renderMobileApp-BnIawAK6.js} +4 -4
  27. package/dist/assets/{runtimeConfig-Bljmgd6r.js → runtimeConfig-BR335S7R.js} +1 -1
  28. package/dist/assets/{runtimeEndpointReset-BYsDk8ux.js → runtimeEndpointReset-grACS-Iv.js} +1 -1
  29. package/dist/assets/{sessionChangeSummary-BslBzYAm.js → sessionChangeSummary-Y6g4Hmn3.js} +1 -1
  30. package/dist/assets/{sessionLookup-5MrKwBXP.js → sessionLookup-Ntvtx26F.js} +1 -1
  31. package/dist/assets/{speechSettingsQueries-otzPynPJ.js → speechSettingsQueries-BS7COcv4.js} +1 -1
  32. package/dist/assets/{useAppFontEffects-DHJ_Qc3g.js → useAppFontEffects-DsaIbgdU.js} +5 -5
  33. package/dist/assets/{useDesktopWindowControlsLayout-BnMDHglb.js → useDesktopWindowControlsLayout-DFuraQa1.js} +1 -1
  34. package/dist/assets/{useEffectiveDirectory-D0qR3aRK.js → useEffectiveDirectory-Bw6EMInE.js} +1 -1
  35. package/dist/assets/{useMobileNavigationStore-BZ7oo4Ir.js → useMobileNavigationStore-BN866WTh.js} +3 -3
  36. package/dist/assets/{useWorkerHighlightedLines-B86utfqe.js → useWorkerHighlightedLines-DsyP6A_g.js} +1 -1
  37. package/dist/index.html +2 -2
  38. package/dist/mini-chat.html +2 -2
  39. package/dist/mobile.html +2 -2
  40. package/package.json +1 -1
  41. package/server/index.js +1 -0
  42. package/server/lib/opencode/session-directory-runtime.js +14 -1
  43. package/server/lib/opencode/session-directory-runtime.test.js +62 -0
  44. package/server/lib/relay/DOCUMENTATION.md +4 -0
  45. package/server/lib/relay/tunnel-host.js +3 -0
  46. package/server/lib/relay/tunnel-host.test.js +27 -0
  47. package/server/lib/session-index/DOCUMENTATION.md +34 -0
  48. package/server/lib/session-index/host-authority-snapshot.js +31 -0
  49. package/server/lib/session-index/routes.host-archive.test.js +118 -0
  50. package/server/lib/session-index/routes.js +7 -2
  51. package/server/lib/session-index/service.js +9 -0
  52. package/server/lib/session-index/sync-runtime.js +44 -0
  53. package/server/lib/session-index/sync-runtime.test.js +33 -0
@@ -7,6 +7,13 @@ const FULL_RECONCILE_INTERVAL_MS = 24 * 60 * 60 * 1000;
7
7
  const FETCH_TIMEOUT_MS = 6_000;
8
8
  const INTERACTIVE_YIELD_MS = 1_000;
9
9
  const LONG_POLL_MAX_MS = 25_000;
10
+ /**
11
+ * Fixed trailing window for background progress tips. Each SSE tip makes every
12
+ * connected client GET the full snapshot, and one sync batch bumps the revision
13
+ * once per directory and once per child-flag check (~300+ per cold start). Keep
14
+ * progress `notify()` calls on the throttled path; only new batch, idle, stop and
15
+ * `publishChange()` may broadcast immediately. sync-runtime.test.js bounds this.
16
+ */
10
17
  const REVISION_TIP_THROTTLE_MS = 500;
11
18
 
12
19
  const normalizeDirectory = (value) => {
@@ -75,6 +82,7 @@ export const createSessionIndexSyncRuntime = ({
75
82
  projectSessions = null,
76
83
  fetchFn = globalThis.fetch,
77
84
  resolveDirectory = (value) => value,
85
+ isDirectoryMissing = null,
78
86
  now = Date.now,
79
87
  setTimer = setTimeout,
80
88
  clearTimer = clearTimeout,
@@ -136,6 +144,9 @@ export const createSessionIndexSyncRuntime = ({
136
144
  }
137
145
  };
138
146
 
147
+ // `immediate` is reserved for edges a client must see without delay (new batch,
148
+ // idle, stop, external change). Progress callers must pass nothing: an immediate
149
+ // progress tip reintroduces one full client snapshot GET per directory/child.
139
150
  const notify = (immediate = false, cause) => {
140
151
  if (stopped && !immediate) return;
141
152
  revision += 1;
@@ -172,6 +183,12 @@ export const createSessionIndexSyncRuntime = ({
172
183
  .find((entry) => entry.directory === directory);
173
184
 
174
185
  const fetchDirectory = async (task) => {
186
+ if (isDirectoryMissing && await isDirectoryMissing(task.directory)) {
187
+ if (stopped || task.runtimeKey !== sessionIndexService.getRuntimeKey()) return 'discarded';
188
+ sessionIndexService.removeEmptyDirectory(task.directory);
189
+ return;
190
+ }
191
+ if (stopped || task.runtimeKey !== sessionIndexService.getRuntimeKey()) return 'discarded';
175
192
  // v2 `session.list` cursor pagination. Collect candidates across pages,
176
193
  // then re-project once before commit so a Host archive that lands mid-sync
177
194
  // cannot resurrect a row. Root/system filters run before the 20-slot budget.
@@ -315,6 +332,8 @@ export const createSessionIndexSyncRuntime = ({
315
332
  };
316
333
 
317
334
  const fetchChildFlag = async (task) => {
335
+ if (isDirectoryMissing && await isDirectoryMissing(task.directory)) return 'discarded';
336
+ if (stopped || task.runtimeKey !== sessionIndexService.getRuntimeKey()) return 'discarded';
318
337
  const controller = new AbortController();
319
338
  currentController = controller;
320
339
  currentWasPreempted = false;
@@ -503,6 +522,7 @@ export const createSessionIndexSyncRuntime = ({
503
522
  const stop = () => {
504
523
  if (stopped) return;
505
524
  stopped = true;
525
+ if (cleanupTimer !== undefined) clearTimer(cleanupTimer);
506
526
  currentController?.abort();
507
527
  const interruptedDirectories = [...queuedDirectories];
508
528
  queue.length = 0;
@@ -513,5 +533,29 @@ export const createSessionIndexSyncRuntime = ({
513
533
  notify(true);
514
534
  };
515
535
 
536
+ let cleanupTimer;
537
+ const cleanupMissingDirectories = async () => {
538
+ const key = sessionIndexService.getRuntimeKey();
539
+ let changed = false;
540
+ try {
541
+ for (const entry of sessionIndexService.snapshot().directories) {
542
+ if (stopped || key !== sessionIndexService.getRuntimeKey()) break;
543
+ if (entry.sessions.length || entry.hasMore) continue;
544
+ const missing = await isDirectoryMissing(entry.directory);
545
+ if (stopped || key !== sessionIndexService.getRuntimeKey()) break;
546
+ if (missing) changed = sessionIndexService.removeEmptyDirectory(entry.directory) || changed;
547
+ }
548
+ if (changed && !stopped && key === sessionIndexService.getRuntimeKey()) notify(true);
549
+ } catch (error) {
550
+ console.warn('[SessionIndex] Directory cleanup failed:', error);
551
+ } finally {
552
+ if (!stopped) {
553
+ cleanupTimer = setTimer(cleanupMissingDirectories, 60_000);
554
+ cleanupTimer?.unref?.();
555
+ }
556
+ }
557
+ };
558
+ if (isDirectoryMissing) void cleanupMissingDirectories();
559
+
516
560
  return { enqueue, noteInteractiveRequest, publishChange, snapshot, waitForChange, stop };
517
561
  };
@@ -41,6 +41,33 @@ const isFullyIdle = (runtime) => (
41
41
  );
42
42
 
43
43
  describe('session index background sync runtime', () => {
44
+ it('rechecks missing directories periodically and stops maintenance on shutdown', async () => {
45
+ const service = createService([{ directory: '/repo', sessions: [], hasMore: false }]);
46
+ service.removeEmptyDirectory = vi.fn(() => true);
47
+ const isDirectoryMissing = vi.fn(async () => false);
48
+ const timers = [];
49
+ const clearTimer = vi.fn();
50
+ const runtime = createSessionIndexSyncRuntime({
51
+ sessionIndexService: service,
52
+ isDirectoryMissing,
53
+ setTimer: (fn, ms) => { const timer = { fn, ms }; timers.push(timer); return timer; },
54
+ clearTimer,
55
+ });
56
+ await waitUntil(() => timers.length === 1);
57
+ expect(timers[0].ms).toBe(60_000);
58
+ expect(service.removeEmptyDirectory).not.toHaveBeenCalled();
59
+ isDirectoryMissing.mockResolvedValue(true);
60
+ await timers[0].fn();
61
+ expect(service.removeEmptyDirectory).toHaveBeenCalledWith('/repo');
62
+ expect(runtime.snapshot().revision).toBe(1);
63
+ runtime.stop();
64
+ expect(clearTimer).toHaveBeenCalledWith(timers[1]);
65
+ const calls = isDirectoryMissing.mock.calls.length;
66
+ await timers[1].fn();
67
+ expect(isDirectoryMissing).toHaveBeenCalledTimes(calls);
68
+ expect(timers).toHaveLength(2);
69
+ });
70
+
44
71
  it('coalesces hundreds of directory/child checks and immediately publishes the final revision', async () => {
45
72
  vi.useFakeTimers();
46
73
  try {
@@ -80,6 +107,12 @@ describe('session index background sync runtime', () => {
80
107
  expect(service.replaceDirectory).toHaveBeenCalledTimes(15);
81
108
  expect(service.replaceChildSessions).toHaveBeenCalledTimes(300);
82
109
  expect(isFullyIdle(runtime)).toBe(true);
110
+ // Relay cold-start budget: 315 internal revisions must reach SSE as a handful
111
+ // of tips (one per 500ms window + immediate edges), never one per directory or
112
+ // child check. Each tip costs every Relay client a full snapshot GET; the
113
+ // uncoalesced Host measured ~316 tips per mobile cold start.
114
+ expect(runtime.snapshot().revision).toBeGreaterThan(300);
115
+ expect(tips.mock.calls.length).toBeLessThanOrEqual(Math.ceil(2650 / 500) + 3);
83
116
  expect(tips).toHaveBeenCalledTimes(8);
84
117
  expect(tips.mock.calls.map(([tip]) => tip.revision)).toEqual(revisionsAtDelivery);
85
118
  for (const [tip] of tips.mock.calls) expect(tip).not.toHaveProperty('cause');