@crazx/dsh-api-session-controller 0.1.6-alpha.1.zw.3 → 0.1.6-alpha.2.zw.2

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 (35) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +15 -3
  3. package/README.zh.md +15 -3
  4. package/lib/client.js +428 -471
  5. package/lib/index.js +16 -47
  6. package/lib/typert.host.js +178 -160
  7. package/lib/typert.remote-client.js +167 -149
  8. package/lib/types/agent.d.ts +1 -1
  9. package/lib/types/agent.js +8 -1
  10. package/lib/types/client/contract/sessions.d.ts +55 -20
  11. package/lib/types/client/contract/snapshot.d.ts +0 -14
  12. package/lib/types/client/index.d.ts +11 -2
  13. package/lib/types/client/index.js +16 -5
  14. package/lib/types/client/scope.d.ts +13 -3
  15. package/lib/types/client/scope.js +15 -22
  16. package/lib/types/client/sessions/lineage.d.ts +1 -4
  17. package/lib/types/client/sessions/lineage.js +1 -3
  18. package/lib/types/client/sessions/manager.d.ts +28 -49
  19. package/lib/types/client/sessions/manager.js +75 -174
  20. package/lib/types/client/sessions/projection-store.d.ts +5 -11
  21. package/lib/types/client/sessions/projection-store.js +6 -14
  22. package/lib/types/client/sessions/service.d.ts +34 -119
  23. package/lib/types/client/sessions/service.js +259 -244
  24. package/lib/types/client/sessions/session.d.ts +7 -20
  25. package/lib/types/client/sessions/session.js +29 -33
  26. package/lib/types/commands.d.ts +2 -2
  27. package/lib/types/commands.js +10 -4
  28. package/lib/types/control.d.ts +1 -1
  29. package/lib/types/control.js +1 -41
  30. package/lib/types/index.d.ts +2 -2
  31. package/lib/types/index.js +1 -1
  32. package/lib/types/types.d.ts +3 -17
  33. package/package.json +70 -70
  34. package/lib/types/client/sessions/queue-mirror.d.ts +0 -26
  35. package/lib/types/client/sessions/queue-mirror.js +0 -61
@@ -1,6 +1,4 @@
1
- // SessionManager: the instance cluster Map<SessionId, Session> (lazy-built, resident) + the frame
2
- // dispatch entry + list state, constructed and held by ClientSessions (one per browser client).
3
- // List data never enters zustand; React connects via subscribe/getListSnapshot.
1
+ /** Host catalog, durable projection caches, and explicitly retained Client instances. */
4
2
  import { SessionSeq } from '@deepseek-ai/dsh-session/types';
5
3
  import { mergeOrderedBaseline } from "../ordered-baseline.js";
6
4
  import { isRemoteFailure } from '@deepseek-ai/dsh-api-gateway/client';
@@ -20,16 +18,6 @@ export class SessionManager {
20
18
  sessions = new Map();
21
19
  /** In-flight Session disposals remain here after instances leave `sessions`, so manager disposal can await quiescence. */
22
20
  sessionDisposals = new Set();
23
- /** Latest transient queues, retained independently of Session object materialization. */
24
- queues = new Map();
25
- /**
26
- * Sessions that finished running while not selected — the sidebar's green
27
- * "done" reminder (manager-owned, survives connection generations; cleared
28
- * on select and session-removed, re-armed by the next completion).
29
- */
30
- completedNotifications = new Set();
31
- /** Last-observed running bits per session; the true→false edge here arms {@link completedNotifications}. */
32
- prevRunning = new Map();
33
21
  /** Per-session projection value stores, retained independently of instance arrival (the
34
22
  * title-snapshot precedent, generalized): push frames land here whether or not the Session
35
23
  * is instantiated (list rows read the 'title' key), and an instantiated Session adopts the
@@ -41,7 +29,7 @@ export class SessionManager {
41
29
  listPhase = 'pending';
42
30
  listError = null;
43
31
  listInflight = null;
44
- /** Mutations arriving after a list request starts are replayed over its response. */
32
+ /** Active list request's mutation log; its identity also fences completion after reconnect. */
45
33
  listMutations = null;
46
34
  addresses = new Map();
47
35
  catalogs = new Map();
@@ -56,7 +44,6 @@ export class SessionManager {
56
44
  * one representation.
57
45
  */
58
46
  jobsBySession = new Map();
59
- selected;
60
47
  listSnapshotCache;
61
48
  /** Entry-identity cache (reference stability): list rebuilds reuse the previous entry
62
49
  * object when every field matches — wire refreshes mint all-new summary objects, so identity
@@ -66,59 +53,29 @@ export class SessionManager {
66
53
  notifier = new Notifier(() => {
67
54
  this.listSnapshotCache = this.buildListSnapshot();
68
55
  });
69
- /**
70
- * @param remote - generated Remote namespaces the Session cluster calls.
71
- * @param restoredSelection - persisted real-Session selection candidate.
72
- */
73
- constructor(remote, restoredSelection, restoredAddress) {
56
+ /** @param remote - generated Remote namespaces used by catalog and history readers. */
57
+ constructor(remote) {
74
58
  this.remote = remote;
75
- this.selected = restoredSelection;
76
- if (restoredAddress !== undefined)
77
- this.addresses.set(restoredAddress.childSessionId, restoredAddress);
78
59
  this.listSnapshotCache = this.buildListSnapshot();
79
60
  }
80
- // ---- Selection ----
81
61
  /**
82
- * Select a listed Session or a retained catalog-addressed child.
83
- * @param sessionId - listed or catalog-addressed Session id.
62
+ * Resolve an acquisition target without materializing a Session.
63
+ * @param target - known identity or durable direct-parent address.
64
+ * @returns the resolved identity with its explicit or catalog-derived history route installed.
84
65
  */
85
- select(sessionId) {
86
- const address = this.navigationAddress(sessionId);
87
- if (!this.summaries.some(summary => summary.sessionId === sessionId) && address === undefined) {
88
- throw new Error(`sessions.select: unknown session ${sessionId}`);
66
+ resolveTarget(target) {
67
+ const id = typeof target === 'string' ? target : target.childSessionId;
68
+ const address = typeof target === 'string' ? this.navigationAddress(id) : target;
69
+ if (typeof target === 'string'
70
+ && !this.sessions.has(id)
71
+ && !this.summaries.some(summary => summary.sessionId === id)
72
+ && address === undefined) {
73
+ throw new Error(`sessions.retain: unknown session ${id}`);
89
74
  }
90
75
  if (address !== undefined)
91
- this.addresses.set(sessionId, address);
92
- this.sessions.get(sessionId)?.configureSubagent(address, address === undefined
93
- ? undefined
94
- : this.catalogs.get(address.parentSessionId)?.parentAvailable);
95
- this.selected = sessionId;
96
- // Looking at the session consumes its completion reminder (dot clears).
97
- this.completedNotifications.delete(sessionId);
98
- void this.refreshSubagents(sessionId);
99
- this.notifier.notifyNow();
100
- }
101
- /**
102
- * Select a healthy child through its durable direct-parent address.
103
- * @param address - catalog-derived parent and child ids.
104
- */
105
- selectSubagent(address) {
106
- const catalog = this.catalogs.get(address.parentSessionId);
107
- const entry = catalog?.entries.find(candidate => candidate.id === address.childSessionId);
108
- if (entry === undefined || entry.kind !== 'child' || entry.mode !== address.mode) {
109
- throw new Error(`sessions.selectSubagent: ${address.childSessionId} is not a healthy catalog child`);
110
- }
111
- this.addresses.set(address.childSessionId, address);
112
- this.sessions.get(address.childSessionId)?.configureSubagent(address, catalog?.parentAvailable);
113
- this.selected = address.childSessionId;
114
- this.completedNotifications.delete(address.childSessionId);
115
- void this.refreshSubagents(address.childSessionId);
116
- this.notifier.notifyNow();
117
- }
118
- /** Clear the selection (the layout falls to the no-session view state). */
119
- clearSelection() {
120
- this.selected = undefined;
121
- this.notifier.notifyNow();
76
+ this.addresses.set(id, address);
77
+ this.sessions.get(id)?.configureSubagent(address, address === undefined ? undefined : this.catalogs.get(address.parentSessionId)?.parentAvailable);
78
+ return id;
122
79
  }
123
80
  /**
124
81
  * Return the durable catalog address retained for one child.
@@ -126,7 +83,7 @@ export class SessionManager {
126
83
  * @returns The direct-parent address, when navigation discovered one.
127
84
  */
128
85
  subagentAddress(sessionId) {
129
- return this.addresses.get(sessionId);
86
+ return this.navigationAddress(sessionId);
130
87
  }
131
88
  /**
132
89
  * Resolve an address for breadcrumb navigation without retaining transport authority.
@@ -147,20 +104,22 @@ export class SessionManager {
147
104
  }
148
105
  // ---- Instance management ----
149
106
  /**
150
- * Drop a session instance (scope-prune companion: instance
151
- * and scope share one lifecycle). The host session log is the durable
152
- * truth — a later get() lazily rebuilds and open() backfills history.
153
- * @param sessionId - the session to drop.
107
+ * Withdraw an exact Client instance before running its teardown callbacks.
108
+ * @param sessionId - identity to withdraw.
109
+ * @param expected - instance being released; a replacement is left untouched.
110
+ * @returns completion of the detached instance's stream teardown.
154
111
  */
155
- async drop(sessionId) {
112
+ drop(sessionId, expected) {
156
113
  const session = this.sessions.get(sessionId);
114
+ if (session !== expected)
115
+ return Promise.resolve();
157
116
  this.sessions.delete(sessionId);
158
- if (session !== undefined)
159
- await this.startSessionDisposal(session);
117
+ this.addresses.delete(sessionId);
118
+ return this.startSessionDisposal(session);
160
119
  }
161
120
  /**
162
- * Stop owned timers and every remaining Session instance.
163
- * @returns when every Session Remote iterator has completed teardown.
121
+ * Stop catalog requests and dispose every resident Session.
122
+ * @returns once catalog requests and every Session stream have stopped.
164
123
  */
165
124
  async dispose() {
166
125
  for (const timer of this.catalogDebounce.values())
@@ -170,6 +129,7 @@ export class SessionManager {
170
129
  this.openCatalogs.clear();
171
130
  const sessions = [...this.sessions.values()];
172
131
  this.sessions.clear();
132
+ this.addresses.clear();
173
133
  for (const session of sessions)
174
134
  void this.startSessionDisposal(session);
175
135
  await this.drainSessionDisposals();
@@ -187,7 +147,7 @@ export class SessionManager {
187
147
  }
188
148
  /**
189
149
  * Lazy build: return the existing instance or construct one (no auto-open —
190
- * open is triggered by the container's select callback).
150
+ * the reference allocator opens history after binding the scope).
191
151
  * @param sessionId - the session to get.
192
152
  * @returns the resident instance.
193
153
  */
@@ -196,11 +156,6 @@ export class SessionManager {
196
156
  if (session === undefined) {
197
157
  session = this.createSession(sessionId);
198
158
  this.sessions.set(sessionId, session);
199
- // Install the latest control baseline before the running-bit sync: a
200
- // not-running summary must sweep replayed queue
201
- // rows the same way a live status flip would (their retirement events dropped
202
- // while the session was uninstantiated).
203
- session.replaceControl(this.queues.get(sessionId) ?? []);
204
159
  // Sync the running and blank bits from the list snapshot into the new
205
160
  // instance (consistency when the list precedes open).
206
161
  const summary = this.summaries.find(s => s.sessionId === sessionId);
@@ -350,7 +305,7 @@ export class SessionManager {
350
305
  }
351
306
  }
352
307
  // ---- List API ----
353
- /** Full refresh via session.list (single-flight: an in-flight call is reused). */
308
+ /** Full refresh via session.list (single-flight within one Host generation). */
354
309
  refreshList() {
355
310
  if (this.listInflight !== null)
356
311
  return this.listInflight;
@@ -363,30 +318,15 @@ export class SessionManager {
363
318
  this.listInflight = (async () => {
364
319
  try {
365
320
  const result = await this.remote.session.list({});
321
+ if (this.listMutations !== mutations)
322
+ return;
366
323
  if (result.ok) {
367
324
  const baseline = this.listPhase === 'pending'
368
325
  ? [...result.value.items]
369
326
  : mergeOrderedBaseline(established, result.value.items, summary => summary.sessionId);
370
- // Seed first observations from the pull-time baseline BEFORE replaying
371
- // in-flight mutations, then reconcile the reminders after EVERY
372
- // replayed mutation: an edge that happens entirely between mutations
373
- // (baseline idle → running → idle) must still arm, which a single
374
- // sync on the folded result would collapse away.
375
- for (const s of baseline) {
376
- if (!this.prevRunning.has(s.sessionId))
377
- this.prevRunning.set(s.sessionId, s.running);
378
- }
379
- let summaries = baseline;
380
- for (const mutation of mutations) {
381
- summaries = applyMutation(summaries, mutation);
382
- this.summaries = summaries;
383
- this.syncCompletedNotifications();
384
- }
385
- this.summaries = summaries;
327
+ this.summaries = mutations.reduce(applyMutation, baseline);
386
328
  this.listState = 'idle';
387
329
  this.listPhase = 'ready';
388
- // Covers the empty-mutations pull (a plain baseline carries no edge).
389
- this.syncCompletedNotifications();
390
330
  // Push running/blank bits down to instantiated Sessions (the list is the authoritative summary source).
391
331
  for (const s of this.summaries) {
392
332
  const session = this.sessions.get(s.sessionId);
@@ -419,13 +359,17 @@ export class SessionManager {
419
359
  catch (error) {
420
360
  if (!isRemoteFailure(error))
421
361
  throw error;
362
+ if (this.listMutations !== mutations)
363
+ return;
422
364
  this.listState = 'error';
423
365
  this.listError = error;
424
366
  }
425
367
  finally {
426
- this.listMutations = null;
427
- this.listInflight = null;
428
- this.notifier.markDirty();
368
+ if (this.listMutations === mutations) {
369
+ this.listMutations = null;
370
+ this.listInflight = null;
371
+ this.notifier.markDirty();
372
+ }
429
373
  }
430
374
  })();
431
375
  return this.listInflight;
@@ -524,8 +468,6 @@ export class SessionManager {
524
468
  recordMutation(mutation) {
525
469
  this.listMutations?.push(mutation);
526
470
  this.summaries = applyMutation(this.summaries, mutation);
527
- // Eager edge reconciliation — a snapshot-build-time pass would miss consecutive status frames.
528
- this.syncCompletedNotifications();
529
471
  this.notifier.markDirty();
530
472
  }
531
473
  // ---- Subscription API (for useSessionList) ----
@@ -545,6 +487,14 @@ export class SessionManager {
545
487
  this.notifier.ensureFresh();
546
488
  return this.listSnapshotCache;
547
489
  }
490
+ /**
491
+ * Read cached projection values for a Session that may exist only in a loaded subagent catalog.
492
+ * @param sessionId - Session whose control or history baseline supplied projections.
493
+ * @returns current values, or undefined before any projection store exists.
494
+ */
495
+ projectionValues(sessionId) {
496
+ return this.projectionStores.get(sessionId)?.values();
497
+ }
548
498
  // ---- Live control and Host-event sinks ----
549
499
  /**
550
500
  * Apply a complete control baseline or one later replacement frame.
@@ -560,22 +510,13 @@ export class SessionManager {
560
510
  this.notifier.markDirty();
561
511
  return;
562
512
  }
563
- if (frame.type === 'jobs') {
564
- if (frame.jobs.length === 0)
565
- this.jobsBySession.delete(frame.sessionId);
566
- else
567
- this.jobsBySession.set(frame.sessionId, frame.jobs);
568
- this.notifier.markDirty();
569
- return;
570
- }
571
- this.queues.set(frame.sessionId, frame.items);
572
- this.sessions.get(frame.sessionId)?.handleControlFrame(frame);
513
+ if (frame.jobs.length === 0)
514
+ this.jobsBySession.delete(frame.sessionId);
515
+ else
516
+ this.jobsBySession.set(frame.sessionId, frame.jobs);
517
+ this.notifier.markDirty();
573
518
  }
574
519
  replaceControlBaseline(baseline) {
575
- this.queues.clear();
576
- for (const [sessionId, items] of Object.entries(baseline.queues)) {
577
- this.queues.set(sessionId, items);
578
- }
579
520
  this.jobsBySession.clear();
580
521
  for (const [sessionId, jobs] of Object.entries(baseline.jobs)) {
581
522
  if (jobs.length > 0)
@@ -584,12 +525,8 @@ export class SessionManager {
584
525
  for (const [sessionId, block] of Object.entries(baseline.projections)) {
585
526
  const store = this.projectionStore(sessionId);
586
527
  const asOfSeq = sessionSeqCursor(block.asOfSeq);
587
- store.truncate(asOfSeq);
588
528
  store.seed({ ...block, asOfSeq });
589
529
  }
590
- for (const [sessionId, session] of this.sessions) {
591
- session.replaceControl(this.queues.get(sessionId) ?? []);
592
- }
593
530
  this.notifier.markDirty();
594
531
  }
595
532
  /**
@@ -610,7 +547,7 @@ export class SessionManager {
610
547
  this.markCatalogParentExpandable(summary.parentSessionId);
611
548
  }
612
549
  if (summary.parentSessionId !== undefined
613
- && (this.selected === summary.parentSessionId || this.openCatalogs.has(summary.parentSessionId))) {
550
+ && this.openCatalogs.has(summary.parentSessionId)) {
614
551
  this.scheduleCatalogRefresh(summary.parentSessionId);
615
552
  }
616
553
  }
@@ -629,7 +566,6 @@ export class SessionManager {
629
566
  this.sessions.get(sessionId)?.handleRunning(false);
630
567
  else
631
568
  this.sessions.get(sessionId)?.handleRemoved();
632
- this.queues.delete(sessionId);
633
569
  this.jobsBySession.delete(sessionId);
634
570
  if (!durableSubagent)
635
571
  this.projectionStores.delete(sessionId);
@@ -676,19 +612,26 @@ export class SessionManager {
676
612
  }
677
613
  /**
678
614
  * Repair one re-established Host-event generation with queryable baselines.
615
+ * Discard old projection cuts before new queries, including cold Sessions
616
+ * absent from the process-local control baseline.
679
617
  * Opened Session follow streams resume independently through API Gateway.
680
618
  */
681
619
  handleConnected() {
620
+ for (const store of this.projectionStores.values())
621
+ store.clear();
622
+ this.listMutations = null;
623
+ this.listInflight = null;
682
624
  void this.refreshList();
683
- const selectedAddress = this.selected === undefined ? undefined : this.addresses.get(this.selected);
684
- if (selectedAddress !== undefined)
685
- void this.refreshSubagents(selectedAddress.parentSessionId);
686
- if (this.selected !== undefined)
687
- void this.refreshSubagents(this.selected);
688
- for (const parentSessionId of this.openCatalogs)
625
+ const parents = new Set(this.openCatalogs);
626
+ for (const id of this.sessions.keys()) {
627
+ const address = this.addresses.get(id);
628
+ if (address !== undefined)
629
+ parents.add(address.parentSessionId);
630
+ }
631
+ for (const parentSessionId of parents)
689
632
  void this.refreshSubagents(parentSessionId);
690
633
  }
691
- /** Debounce membership refetches while one parent catalog is selected or open. */
634
+ /** Debounce membership refetches for an explicitly consumed catalog. */
692
635
  scheduleCatalogRefresh(parentSessionId) {
693
636
  if (this.catalogDebounce.has(parentSessionId))
694
637
  return;
@@ -764,41 +707,6 @@ export class SessionManager {
764
707
  };
765
708
  });
766
709
  }
767
- /**
768
- * Reconcile completion reminders against the latest summaries, eagerly after
769
- * every mutation and pull (a snapshot-build-time pass would collapse
770
- * consecutive status frames into one observation). A running→idle edge of a
771
- * non-selected session arms its reminder; running disarms it; removal drops
772
- * it. First observation only records the running bit — sessions already
773
- * idle at load get no reminder.
774
- */
775
- syncCompletedNotifications() {
776
- const seen = new Set();
777
- for (const s of this.summaries) {
778
- seen.add(s.sessionId);
779
- const prev = this.prevRunning.get(s.sessionId);
780
- if (prev === undefined) {
781
- this.prevRunning.set(s.sessionId, s.running);
782
- continue;
783
- }
784
- if (prev && !s.running) {
785
- if (s.sessionId !== this.selected)
786
- this.completedNotifications.add(s.sessionId);
787
- }
788
- else if (s.running) {
789
- this.completedNotifications.delete(s.sessionId);
790
- }
791
- this.prevRunning.set(s.sessionId, s.running);
792
- }
793
- for (const id of this.prevRunning.keys()) {
794
- if (!seen.has(id))
795
- this.prevRunning.delete(id);
796
- }
797
- for (const id of this.completedNotifications) {
798
- if (!seen.has(id))
799
- this.completedNotifications.delete(id);
800
- }
801
- }
802
710
  buildListSnapshot() {
803
711
  const merged = this.summaries.map((summary) => {
804
712
  // List rows read the generic 'title' projection key (host-computed unit
@@ -812,40 +720,33 @@ export class SessionManager {
812
720
  ...(projectionValues === undefined ? {} : { projectionValues }),
813
721
  };
814
722
  });
815
- const fresh = flattenLineage(merged, this.completedNotifications);
723
+ const fresh = flattenLineage(merged);
816
724
  const items = fresh.map((entry) => {
817
725
  const prev = this.entryCache.get(entry.sessionId);
818
726
  if (prev !== undefined && prev.updatedAt === entry.updatedAt && prev.running === entry.running
819
727
  && prev.blank === entry.blank
820
728
  && prev.parentSessionId === entry.parentSessionId && prev.cwd === entry.cwd
821
729
  && prev.origin === entry.origin && prev.title === entry.title && prev.depth === entry.depth
822
- && prev.projectionValues === entry.projectionValues
823
- && prev.completed === entry.completed)
730
+ && prev.projectionValues === entry.projectionValues)
824
731
  return prev;
825
732
  this.entryCache.set(entry.sessionId, entry);
826
733
  return entry;
827
734
  });
735
+ const itemIds = new Set(items.map(entry => entry.sessionId));
828
736
  for (const id of this.entryCache.keys()) {
829
- if (!items.some(e => e.sessionId === id))
737
+ if (!itemIds.has(id))
830
738
  this.entryCache.delete(id);
831
739
  }
832
740
  const sameOrder = items.length === this.itemsCache.length && items.every((e, i) => e === this.itemsCache[i]);
833
741
  if (!sameOrder)
834
742
  this.itemsCache = items;
835
- const selected = this.selected;
836
- const current = selected !== undefined
837
- && (items.some(item => item.sessionId === selected) || this.addresses.has(selected))
838
- ? selected
839
- : undefined;
840
743
  return {
841
744
  items: this.itemsCache,
842
- current,
843
745
  state: this.listState,
844
746
  phase: this.listPhase,
845
747
  error: this.listError,
846
748
  subagentsByParent: Object.fromEntries(this.catalogs),
847
749
  jobsBySession: Object.fromEntries(this.jobsBySession),
848
- currentAddress: current === undefined ? undefined : this.addresses.get(current),
849
750
  };
850
751
  }
851
752
  }
@@ -39,9 +39,9 @@ export interface ProjectionsBaseline {
39
39
  /**
40
40
  * One session's projection values. Framework semantics, uniform across every
41
41
  * key: a baseline seeds rows at its cut, a push frame updates one row, and in
42
- * both paths a lower-or-equal seq loses — a replayed frame cannot regress a
43
- * value, a stale baseline cannot overwrite a newer frame. A key the store has
44
- * never seen reads `undefined` (capability absent). Faces are identity-stable
42
+ * both paths a lower-or-equal seq within the Host generation loses. A replayed
43
+ * frame cannot regress a value; a stale baseline cannot overwrite a newer
44
+ * frame. A key the store has never seen reads `undefined` (capability absent). Faces are identity-stable
45
45
  * per key (create-on-demand, cached) so the React side binds each exactly
46
46
  * once; the store-level channel (`subscribeAny`) serves coarse consumers (the
47
47
  * manager's list projection reads the `title` key).
@@ -95,14 +95,8 @@ export declare class ProjectionValueStore {
95
95
  * @param baseline - the response's projections block.
96
96
  */
97
97
  seed(baseline: ProjectionsBaseline): void;
98
- /**
99
- * Drop rows beyond a replacement control baseline. Such rows describe
100
- * process state the Host lost before persisting it and would otherwise
101
- * outrank recomputed lower-seq values forever. The caller seeds the new
102
- * baseline immediately afterward.
103
- * @param lastSeq - highest durable sequence reflected by the baseline.
104
- */
105
- truncate(lastSeq: SessionSeqCursor): void;
98
+ /** Discard one Host generation's values and watermarks while preserving subscribed faces. */
99
+ clear(): void;
106
100
  private changed;
107
101
  private channel;
108
102
  }
@@ -2,9 +2,9 @@ import { Notifier } from "./notifier.js";
2
2
  /**
3
3
  * One session's projection values. Framework semantics, uniform across every
4
4
  * key: a baseline seeds rows at its cut, a push frame updates one row, and in
5
- * both paths a lower-or-equal seq loses — a replayed frame cannot regress a
6
- * value, a stale baseline cannot overwrite a newer frame. A key the store has
7
- * never seen reads `undefined` (capability absent). Faces are identity-stable
5
+ * both paths a lower-or-equal seq within the Host generation loses. A replayed
6
+ * frame cannot regress a value; a stale baseline cannot overwrite a newer
7
+ * frame. A key the store has never seen reads `undefined` (capability absent). Faces are identity-stable
8
8
  * per key (create-on-demand, cached) so the React side binds each exactly
9
9
  * once; the store-level channel (`subscribeAny`) serves coarse consumers (the
10
10
  * manager's list projection reads the `title` key).
@@ -89,17 +89,9 @@ export class ProjectionValueStore {
89
89
  this.changed(key);
90
90
  }
91
91
  }
92
- /**
93
- * Drop rows beyond a replacement control baseline. Such rows describe
94
- * process state the Host lost before persisting it and would otherwise
95
- * outrank recomputed lower-seq values forever. The caller seeds the new
96
- * baseline immediately afterward.
97
- * @param lastSeq - highest durable sequence reflected by the baseline.
98
- */
99
- truncate(lastSeq) {
100
- for (const [key, row] of this.rows) {
101
- if (row.seq <= lastSeq)
102
- continue;
92
+ /** Discard one Host generation's values and watermarks while preserving subscribed faces. */
93
+ clear() {
94
+ for (const key of this.rows.keys()) {
103
95
  this.rows.delete(key);
104
96
  this.changed(key);
105
97
  }