@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.
- package/README.i18n.yaml +2 -2
- package/README.md +15 -3
- package/README.zh.md +15 -3
- package/lib/client.js +428 -471
- package/lib/index.js +16 -47
- package/lib/typert.host.js +178 -160
- package/lib/typert.remote-client.js +167 -149
- package/lib/types/agent.d.ts +1 -1
- package/lib/types/agent.js +8 -1
- package/lib/types/client/contract/sessions.d.ts +55 -20
- package/lib/types/client/contract/snapshot.d.ts +0 -14
- package/lib/types/client/index.d.ts +11 -2
- package/lib/types/client/index.js +16 -5
- package/lib/types/client/scope.d.ts +13 -3
- package/lib/types/client/scope.js +15 -22
- package/lib/types/client/sessions/lineage.d.ts +1 -4
- package/lib/types/client/sessions/lineage.js +1 -3
- package/lib/types/client/sessions/manager.d.ts +28 -49
- package/lib/types/client/sessions/manager.js +75 -174
- package/lib/types/client/sessions/projection-store.d.ts +5 -11
- package/lib/types/client/sessions/projection-store.js +6 -14
- package/lib/types/client/sessions/service.d.ts +34 -119
- package/lib/types/client/sessions/service.js +259 -244
- package/lib/types/client/sessions/session.d.ts +7 -20
- package/lib/types/client/sessions/session.js +29 -33
- package/lib/types/commands.d.ts +2 -2
- package/lib/types/commands.js +10 -4
- package/lib/types/control.d.ts +1 -1
- package/lib/types/control.js +1 -41
- package/lib/types/index.d.ts +2 -2
- package/lib/types/index.js +1 -1
- package/lib/types/types.d.ts +3 -17
- package/package.json +70 -70
- package/lib/types/client/sessions/queue-mirror.d.ts +0 -26
- package/lib/types/client/sessions/queue-mirror.js +0 -61
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
*
|
|
83
|
-
* @param
|
|
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
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
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(
|
|
92
|
-
this.sessions.get(
|
|
93
|
-
|
|
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.
|
|
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
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* @
|
|
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
|
-
|
|
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
|
-
|
|
159
|
-
|
|
117
|
+
this.addresses.delete(sessionId);
|
|
118
|
+
return this.startSessionDisposal(session);
|
|
160
119
|
}
|
|
161
120
|
/**
|
|
162
|
-
* Stop
|
|
163
|
-
* @returns
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
427
|
-
|
|
428
|
-
|
|
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.
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
&&
|
|
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
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
|
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
|
|
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 (!
|
|
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
|
|
43
|
-
* value
|
|
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
|
-
|
|
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
|
|
6
|
-
* value
|
|
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
|
-
|
|
94
|
-
|
|
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
|
}
|