@deepseek-ai/dsh-api-session-controller 0.1.6-alpha.2 → 0.1.7-alpha.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 (38) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +34 -9
  3. package/README.zh.md +34 -9
  4. package/lib/client.js +774 -641
  5. package/lib/index.js +318 -143
  6. package/lib/typert.host.js +384 -186
  7. package/lib/typert.remote-client.d.ts +10 -5
  8. package/lib/typert.remote-client.js +225 -125
  9. package/lib/types/archived-session-gate.d.ts +30 -0
  10. package/lib/types/archived-session-gate.js +52 -0
  11. package/lib/types/client/contract/session.d.ts +11 -4
  12. package/lib/types/client/contract/sessions.d.ts +11 -16
  13. package/lib/types/client/contract/snapshot.d.ts +1 -1
  14. package/lib/types/client/index.d.ts +1 -1
  15. package/lib/types/client/sessions/lineage.d.ts +4 -1
  16. package/lib/types/client/sessions/lineage.js +2 -1
  17. package/lib/types/client/sessions/manager.d.ts +66 -59
  18. package/lib/types/client/sessions/manager.js +249 -277
  19. package/lib/types/client/sessions/projection-store.d.ts +38 -13
  20. package/lib/types/client/sessions/projection-store.js +54 -13
  21. package/lib/types/client/sessions/remotes.d.ts +1 -2
  22. package/lib/types/client/sessions/service.d.ts +23 -35
  23. package/lib/types/client/sessions/service.js +35 -47
  24. package/lib/types/client/sessions/session.d.ts +23 -19
  25. package/lib/types/client/sessions/session.js +158 -59
  26. package/lib/types/client/transport.js +5 -2
  27. package/lib/types/commands.d.ts +4 -2
  28. package/lib/types/commands.js +72 -36
  29. package/lib/types/control.d.ts +2 -4
  30. package/lib/types/control.js +2 -44
  31. package/lib/types/history.js +25 -10
  32. package/lib/types/index.d.ts +32 -5
  33. package/lib/types/index.js +117 -13
  34. package/lib/types/list.js +24 -15
  35. package/lib/types/skill-catalog.js +60 -50
  36. package/lib/types/types.d.ts +45 -25
  37. package/lib/types/types.js +0 -1
  38. package/package.json +71 -76
@@ -1,10 +1,14 @@
1
1
  /**
2
2
  * Generic per-session projection value store (push model; see the
3
3
  * session-projection subsystem page, docs/subsystems/session-projection.md):
4
- * the host is the only computation site; the client holds finished
5
- * whole values per key — `key → { value, seq }` — seeded by a follow opening
6
- * baseline and updated by Session Controller `projection` frames,
7
- * under the single rule **higher seq wins**. No client-side domain folding
4
+ * the host is the only computation site; the client holds finished whole
5
+ * values per key in one of two row kinds. A `sequenced` row
6
+ * (`{ value, seq }`) comes from the connected Host — a follow opening
7
+ * baseline, a Session Controller `projection` frame, a list block the Host
8
+ * computed for an attached Session — and merges under **higher seq wins**
9
+ * against other sequenced rows. A `cached` row (`{ value }`) comes from the
10
+ * session list's view of the persisted checkpoint, carries no comparable
11
+ * seq, and yields to every sequenced write. No client-side domain folding
8
12
  * exists: a domain ships projection support with zero client code. Per-key
9
13
  * bare observable faces feed `useProjection` (ui-renderer binds them).
10
14
  */
@@ -38,10 +42,14 @@ export interface ProjectionsBaseline {
38
42
  }
39
43
  /**
40
44
  * One session's projection values. Framework semantics, uniform across every
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 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
+ * key. Sequenced writes (a baseline seeds rows at its cut, a push frame
46
+ * updates one row) compare seqs among themselves: a lower-or-equal seq within
47
+ * the Host generation loses, so a replayed frame cannot regress a value and a
48
+ * stale baseline cannot overwrite a newer frame. Cached writes (the session
49
+ * list's zero-I/O block) only fill keys no sequenced row holds, and a baseline
50
+ * discards every cached row before it seeds, regardless of seq: the connected
51
+ * Session is the truth and a cached value never outranks it. A key the store
52
+ * has never seen reads `undefined` (capability absent). Faces are identity-stable
45
53
  * per key (create-on-demand, cached) so the React side binds each exactly
46
54
  * once; the store-level channel (`subscribeAny`) serves coarse consumers (the
47
55
  * manager's list projection reads the `title` key).
@@ -67,6 +75,12 @@ export declare class ProjectionValueStore {
67
75
  * @returns the value, or undefined while the key is absent.
68
76
  */
69
77
  get(key: string): unknown;
78
+ /**
79
+ * Read the accepted Host watermark without subscribing or copying a value.
80
+ * @param key - projection key.
81
+ * @returns the current sequence, or undefined for absent and cached values.
82
+ */
83
+ seqOf(key: string): SessionSeqCursor | undefined;
70
84
  /**
71
85
  * Read every current projection value as one reference-stable snapshot.
72
86
  * @returns The same frozen value map until a row changes.
@@ -87,11 +101,22 @@ export declare class ProjectionValueStore {
87
101
  */
88
102
  apply(key: string, value: unknown, seq: SessionSeqCursor): void;
89
103
  /**
90
- * Seed from a history tail page's projections block: every carried key
91
- * lands under the same seq rule as frames; a key the block omits is
92
- * capability-absent as of the cut — its row clears unless a newer frame
93
- * already superseded the cut (a stale baseline can neither overwrite nor
94
- * clear newer values).
104
+ * Fill keys from a session-list block the Host labeled `cached`: a zero-I/O
105
+ * view of the persisted checkpoint. A cached value lands only where no
106
+ * sequenced row exists: a connected Session has already answered for such
107
+ * a key, and the list's view of the persisted checkpoint cannot be newer
108
+ * than it.
109
+ * @param values - whole values by key viewed from the persisted checkpoint.
110
+ */
111
+ applyCached(values: Readonly<Record<string, unknown>>): void;
112
+ /**
113
+ * Seed from a history tail page's projections block. Every cached row is
114
+ * discarded first, regardless of seq: the block comes from the connected
115
+ * Session, and a value viewed from the persisted checkpoint never outranks
116
+ * it. Then every carried key lands under the same seq rule as frames, and a
117
+ * key the block omits is capability-absent as of the cut — its row clears
118
+ * unless a newer frame already superseded the cut (a stale baseline can
119
+ * neither overwrite nor clear newer sequenced values).
95
120
  * @param baseline - the response's projections block.
96
121
  */
97
122
  seed(baseline: ProjectionsBaseline): void;
@@ -1,10 +1,14 @@
1
1
  import { Notifier } from "./notifier.js";
2
2
  /**
3
3
  * One session's projection values. Framework semantics, uniform across every
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 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
4
+ * key. Sequenced writes (a baseline seeds rows at its cut, a push frame
5
+ * updates one row) compare seqs among themselves: a lower-or-equal seq within
6
+ * the Host generation loses, so a replayed frame cannot regress a value and a
7
+ * stale baseline cannot overwrite a newer frame. Cached writes (the session
8
+ * list's zero-I/O block) only fill keys no sequenced row holds, and a baseline
9
+ * discards every cached row before it seeds, regardless of seq: the connected
10
+ * Session is the truth and a cached value never outranks it. A key the store
11
+ * has never seen reads `undefined` (capability absent). Faces are identity-stable
8
12
  * per key (create-on-demand, cached) so the React side binds each exactly
9
13
  * once; the store-level channel (`subscribeAny`) serves coarse consumers (the
10
14
  * manager's list projection reads the `title` key).
@@ -34,6 +38,15 @@ export class ProjectionValueStore {
34
38
  get(key) {
35
39
  return this.rows.get(key)?.value;
36
40
  }
41
+ /**
42
+ * Read the accepted Host watermark without subscribing or copying a value.
43
+ * @param key - projection key.
44
+ * @returns the current sequence, or undefined for absent and cached values.
45
+ */
46
+ seqOf(key) {
47
+ const row = this.rows.get(key);
48
+ return row?.kind === 'sequenced' ? row.seq : undefined;
49
+ }
37
50
  /**
38
51
  * Read every current projection value as one reference-stable snapshot.
39
52
  * @returns The same frozen value map until a row changes.
@@ -61,20 +74,46 @@ export class ProjectionValueStore {
61
74
  */
62
75
  apply(key, value, seq) {
63
76
  const row = this.rows.get(key);
64
- if (row !== undefined && seq <= row.seq)
65
- return; // higher seq wins; replays and stale frames drop
66
- this.rows.set(key, { value, seq });
77
+ // higher seq wins among sequenced rows; replays and stale frames drop. A
78
+ // cached row has no comparable seq and always yields.
79
+ if (row?.kind === 'sequenced' && seq <= row.seq)
80
+ return;
81
+ this.rows.set(key, { kind: 'sequenced', value, seq });
67
82
  this.changed(key);
68
83
  }
69
84
  /**
70
- * Seed from a history tail page's projections block: every carried key
71
- * lands under the same seq rule as frames; a key the block omits is
72
- * capability-absent as of the cut — its row clears unless a newer frame
73
- * already superseded the cut (a stale baseline can neither overwrite nor
74
- * clear newer values).
85
+ * Fill keys from a session-list block the Host labeled `cached`: a zero-I/O
86
+ * view of the persisted checkpoint. A cached value lands only where no
87
+ * sequenced row exists: a connected Session has already answered for such
88
+ * a key, and the list's view of the persisted checkpoint cannot be newer
89
+ * than it.
90
+ * @param values - whole values by key viewed from the persisted checkpoint.
91
+ */
92
+ applyCached(values) {
93
+ for (const key of Object.keys(values)) {
94
+ if (this.rows.get(key)?.kind === 'sequenced')
95
+ continue;
96
+ this.rows.set(key, { kind: 'cached', value: values[key] });
97
+ this.changed(key);
98
+ }
99
+ }
100
+ /**
101
+ * Seed from a history tail page's projections block. Every cached row is
102
+ * discarded first, regardless of seq: the block comes from the connected
103
+ * Session, and a value viewed from the persisted checkpoint never outranks
104
+ * it. Then every carried key lands under the same seq rule as frames, and a
105
+ * key the block omits is capability-absent as of the cut — its row clears
106
+ * unless a newer frame already superseded the cut (a stale baseline can
107
+ * neither overwrite nor clear newer sequenced values).
75
108
  * @param baseline - the response's projections block.
76
109
  */
77
110
  seed(baseline) {
111
+ for (const [key, row] of this.rows) {
112
+ if (row.kind !== 'cached')
113
+ continue;
114
+ this.rows.delete(key);
115
+ this.changed(key);
116
+ }
78
117
  // Erased walk: the framework crosses the open key space; per-key typing
79
118
  // is re-established at the consumer (useProjection's map lookup).
80
119
  const values = baseline.values;
@@ -83,7 +122,9 @@ export class ProjectionValueStore {
83
122
  for (const [key, row] of this.rows) {
84
123
  if (Object.hasOwn(values, key))
85
124
  continue;
86
- if (row.seq > baseline.asOfSeq)
125
+ // Every cached row was deleted above; the kind test only narrows the
126
+ // type so `row.seq` is readable.
127
+ if (row.kind === 'sequenced' && row.seq > baseline.asOfSeq)
87
128
  continue;
88
129
  this.rows.delete(key);
89
130
  this.changed(key);
@@ -7,7 +7,7 @@
7
7
  import type { ClientRemote } from '@deepseek-ai/dsh-api-gateway/client';
8
8
  import type { CommandSubmitAttachment } from '@deepseek-ai/dsh-commands/types';
9
9
  import type { SessionId } from '@deepseek-ai/dsh-session/types';
10
- import type { SubagentCatalog, SubagentInterruptReceipt, SubagentPromptReceipt, SubagentPromptRequest } from '@deepseek-ai/dsh-subagent/client';
10
+ import type { SubagentInterruptReceipt, SubagentPromptReceipt, SubagentPromptRequest } from '@deepseek-ai/dsh-subagent/client';
11
11
  import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
12
12
  import type { SessionRemote } from '../transport.ts';
13
13
  /** Narrow Commands namespace consumed by a Client Session. */
@@ -16,7 +16,6 @@ export interface SessionCommandsRemote {
16
16
  }
17
17
  /** Narrow subagent namespace consumed by a Client Session and its manager. */
18
18
  export interface SessionSubagentsRemote {
19
- list(parentSessionId: SessionId, signal?: AbortSignal): Promise<RemoteResult<SubagentCatalog>>;
20
19
  prompt(request: SubagentPromptRequest, signal?: AbortSignal): Promise<RemoteResult<SubagentPromptReceipt>>;
21
20
  interruptByParent(childSessionId: SessionId, parentSessionId: SessionId, mode: 'continuable'): Promise<RemoteResult<SubagentInterruptReceipt>>;
22
21
  }
@@ -3,7 +3,6 @@ import type { Context } from '@deepseek-ai/cordis';
3
3
  import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client';
4
4
  import { type SessionId } from '@deepseek-ai/dsh-session/types';
5
5
  import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types';
6
- import type { SessionJob as JobView } from '../../types.ts';
7
6
  import type { SessionProjectionMap } from '@deepseek-ai/dsh-session-projection/types';
8
7
  import { type ObservableSnapshot, type SnapshotStore } from '@deepseek-ai/dsh-client-store';
9
8
  import type { RemoteFailure, RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
@@ -12,7 +11,7 @@ import type { SessionFace } from '../contract/session.ts';
12
11
  import type { AgentContext, ISessions, SessionReference, SessionRetainInfo, SessionRetainOptions, SessionTarget } from '../contract/sessions.ts';
13
12
  import { SessionManager } from './manager.ts';
14
13
  import type { SessionRemotes } from './remotes.ts';
15
- import type { SessionListPhase, SessionSearchResultItem, SubagentCatalogSnapshot } from './manager.ts';
14
+ import type { SessionListPhase, SessionSearchResultItem, SessionProjectionSnapshot } from './manager.ts';
16
15
  /** Session list row projected from the host list RPC plus live stream increments. */
17
16
  export interface SessionSummary {
18
17
  id: SessionId;
@@ -24,14 +23,16 @@ export interface SessionSummary {
24
23
  parentId?: SessionId;
25
24
  /** Coarse durable origin for navigation filtering; not a continuation capability. */
26
25
  origin?: 'subagent';
26
+ /** Host running state for `ids` members; a display fallback for other rows. */
27
27
  running: boolean;
28
28
  /** Local ownership counts; Host metadata refreshes cannot overwrite them. */
29
29
  readonly retainedBy: SessionRetainInfo['retainedBy'];
30
30
  /**
31
- * Empty-log bit (host summary derivation mirror). New Session reuses a blank
32
- * one targeting the same workspace. Filtering stays with the consumer: the
33
- * store carries every row, while the Workspace browser shows only the
34
- * selected blank entry.
31
+ * New Session presentation and reuse eligibility, derived from the Host
32
+ * summary, `sessionListMetadata`, and client acceptance/running observations.
33
+ * New Session reuses a blank one targeting the same workspace. Filtering
34
+ * stays with the consumer: the store carries every row, while the Workspace
35
+ * browser shows only the selected blank entry.
35
36
  */
36
37
  blank: boolean;
37
38
  updatedAt: number;
@@ -40,20 +41,14 @@ export interface SessionSummary {
40
41
  }
41
42
  /** Catalog metadata and local source counts; catalog membership owns no Client generation. */
42
43
  export interface SessionListState {
43
- /** Host-list order; addressed breadcrumb-only rows are excluded. */
44
+ /** Host list order; every id has a matching byId row in the same snapshot. */
44
45
  ids: SessionId[];
45
- /** Host/catalog rows plus local fallback rows for live Client generations; only `ids` expresses Host-list membership. */
46
+ /** Host/catalog rows plus retained subagent fallbacks; only `ids` expresses Host-list membership. */
46
47
  byId: Record<SessionId, SessionSummary>;
47
48
  /** Arrival lifecycle projected 1:1 from the manager snapshot (see SessionListPhase): empty-with-ready means "truly no sessions". */
48
49
  phase: SessionListPhase;
49
- /** Direct durable catalogs keyed by their selected parent address. */
50
- subagentsByParent: Readonly<Record<SessionId, SubagentCatalogSnapshot>>;
51
- /**
52
- * Background jobs each session can see, mirrored last-wins from Session
53
- * Controller's control baseline and `jobs` frames. A missing key is an empty
54
- * set, so consumers read absence rather than a sentinel.
55
- */
56
- jobsBySession: Readonly<Record<SessionId, readonly JobView[]>>;
50
+ /** Shared projection values and explicit-read state, including unopened Sessions. */
51
+ projectionsBySession: Readonly<Record<SessionId, SessionProjectionSnapshot>>;
57
52
  }
58
53
  /** Structured session-create failure. */
59
54
  export declare class SessionCreateError extends Error {
@@ -118,20 +113,14 @@ export declare class ClientSessions implements ISessions {
118
113
  * Resolve an already discovered direct-parent address without opening it.
119
114
  * Feature plugins use this to avoid Agent-bound RPCs in persisted child views.
120
115
  * @param id - possible addressed child id.
121
- * @returns The retained address, when present.
116
+ * @returns A retained or loaded-catalog address, without retaining a new selection or scope.
122
117
  */
123
118
  subagentAddress(id: SessionId): SubagentAddress | undefined;
124
119
  /**
125
- * Inform the Session Controller whether a catalog menu is consuming membership updates.
126
- * @param parentSessionId - selected parent.
127
- * @param open - menu state.
128
- */
129
- setSubagentCatalogOpen(parentSessionId: SessionId, open: boolean): void;
130
- /**
131
- * Refresh one direct-child catalog.
132
- * @param parentSessionId - catalog owner.
120
+ * Load all Session projections once per connection; retry an unsuccessful initial read.
121
+ * @param sessionId - Session to inspect without opening its conversation.
133
122
  */
134
- refreshSubagents(parentSessionId: SessionId): Promise<void>;
123
+ refreshProjections(sessionId: SessionId): Promise<void>;
135
124
  /**
136
125
  * Refresh the real Session baseline, reusing an in-flight pull.
137
126
  * @returns completion of the current or newly started baseline pull.
@@ -193,15 +182,14 @@ export declare class ClientSessions implements ISessions {
193
182
  sessionId?: SessionId;
194
183
  }): Promise<SessionId>;
195
184
  /**
196
- * Fork a Session from a completed-turn prefix of the source and publish
197
- * the child in the catalog before resolving.
198
- * @param opts - source session id, the optional event seq anchoring the
199
- * cut (the boundary is the first turn/end at or after it; an in-log
200
- * anchor in an open turn is unavailable rather than clipped backward),
201
- * and whether to increment an inherited durable title before resolving.
202
- * A fractional anchor floors to a real event seq: the frozen nodes of an
203
- * interrupted turn carry flow-ordering seqs between two events, and the
204
- * wire takes integers only.
185
+ * Fork a session from an exact inclusive prefix of the source (same
186
+ * synchronous-addressability guarantee as {@link ClientSessions.create}:
187
+ * on resolution the child is catalogued and may be explicitly retained).
188
+ * @param opts - source session id, the optional exact inclusive boundary
189
+ * seq (a real event seq the caller already knows; a cut inside an open
190
+ * turn is balanced Host-side with synthetic closers, and omission selects
191
+ * the latest completed-turn prefix), and whether to increment an
192
+ * inherited durable title before resolving.
205
193
  * @returns the child session id.
206
194
  * @throws {SessionForkError} with the source id.
207
195
  * @throws {Error} when a requested child-title rename fails after creation.
@@ -163,7 +163,7 @@ export class ClientSessions {
163
163
  this.rootCtx = rootCtx;
164
164
  this.manager = new SessionManager(remote);
165
165
  this.list = createSnapshotStore({
166
- ids: [], byId: {}, phase: 'pending', subagentsByParent: {}, jobsBySession: {},
166
+ ids: [], byId: {}, phase: 'pending', projectionsBySession: {},
167
167
  });
168
168
  const disposeManagerProjection = this.manager.subscribe(() => { this.projectList(); });
169
169
  rootCtx.effect(() => async () => {
@@ -234,25 +234,17 @@ export class ClientSessions {
234
234
  * Resolve an already discovered direct-parent address without opening it.
235
235
  * Feature plugins use this to avoid Agent-bound RPCs in persisted child views.
236
236
  * @param id - possible addressed child id.
237
- * @returns The retained address, when present.
237
+ * @returns A retained or loaded-catalog address, without retaining a new selection or scope.
238
238
  */
239
239
  subagentAddress(id) {
240
240
  return this.manager.subagentAddress(id);
241
241
  }
242
242
  /**
243
- * Inform the Session Controller whether a catalog menu is consuming membership updates.
244
- * @param parentSessionId - selected parent.
245
- * @param open - menu state.
243
+ * Load all Session projections once per connection; retry an unsuccessful initial read.
244
+ * @param sessionId - Session to inspect without opening its conversation.
246
245
  */
247
- setSubagentCatalogOpen(parentSessionId, open) {
248
- this.manager.setSubagentCatalogOpen(parentSessionId, open);
249
- }
250
- /**
251
- * Refresh one direct-child catalog.
252
- * @param parentSessionId - catalog owner.
253
- */
254
- refreshSubagents(parentSessionId) {
255
- return this.manager.refreshSubagents(parentSessionId);
246
+ refreshProjections(sessionId) {
247
+ return this.manager.refreshProjections(sessionId);
256
248
  }
257
249
  /**
258
250
  * Refresh the real Session baseline, reusing an in-flight pull.
@@ -332,15 +324,14 @@ export class ClientSessions {
332
324
  return result.value.sessionId;
333
325
  }
334
326
  /**
335
- * Fork a Session from a completed-turn prefix of the source and publish
336
- * the child in the catalog before resolving.
337
- * @param opts - source session id, the optional event seq anchoring the
338
- * cut (the boundary is the first turn/end at or after it; an in-log
339
- * anchor in an open turn is unavailable rather than clipped backward),
340
- * and whether to increment an inherited durable title before resolving.
341
- * A fractional anchor floors to a real event seq: the frozen nodes of an
342
- * interrupted turn carry flow-ordering seqs between two events, and the
343
- * wire takes integers only.
327
+ * Fork a session from an exact inclusive prefix of the source (same
328
+ * synchronous-addressability guarantee as {@link ClientSessions.create}:
329
+ * on resolution the child is catalogued and may be explicitly retained).
330
+ * @param opts - source session id, the optional exact inclusive boundary
331
+ * seq (a real event seq the caller already knows; a cut inside an open
332
+ * turn is balanced Host-side with synthetic closers, and omission selects
333
+ * the latest completed-turn prefix), and whether to increment an
334
+ * inherited durable title before resolving.
344
335
  * @returns the child session id.
345
336
  * @throws {SessionForkError} with the source id.
346
337
  * @throws {Error} when a requested child-title rename fails after creation.
@@ -351,26 +342,16 @@ export class ClientSessions {
351
342
  : undefined;
352
343
  const result = await this.manager.fork({
353
344
  sessionId: opts.sessionId,
354
- // Flooring lands inside the anchor's own turn (every turn opens with a
355
- // turn/start), so the host's first-turn/end-at-or-after cut still ends
356
- // on that turn — never clipped back to the previous one.
357
- ...(opts.atSeq === undefined ? {} : { atSeq: SessionSeq(Math.floor(opts.atSeq)) }),
345
+ ...(opts.atSeq === undefined ? {} : { atSeq: SessionSeq(opts.atSeq) }),
358
346
  });
359
347
  if (!result.ok)
360
348
  throw new SessionForkError(result.error, opts.sessionId);
361
349
  this.projectList();
362
350
  const childId = result.value.sessionId;
363
351
  if (sourceTitle !== undefined) {
364
- const reference = this.retain(childId, { source: 'controllerOperation' });
365
- try {
366
- await reference.ready;
367
- const renamed = await reference.binding.session.rename(increasedForkTitle(sourceTitle));
368
- if (!renamed.ok)
369
- throw new Error(`fork child rename failed: ${renamed.error.code}: ${renamed.error.message}`);
370
- }
371
- finally {
372
- reference.release();
373
- }
352
+ const renamed = await this.manager.rename(childId, increasedForkTitle(sourceTitle));
353
+ if (!renamed.ok)
354
+ throw new Error(`fork child rename failed: ${renamed.error.code}: ${renamed.error.message}`);
374
355
  }
375
356
  return childId;
376
357
  }
@@ -450,8 +431,9 @@ export class ClientSessions {
450
431
  else
451
432
  this.publishRetention(id);
452
433
  });
453
- if (this.list.getSnapshot().byId[id] === undefined)
434
+ if (this.list.getSnapshot().byId[id] === undefined && this.manager.subagentAddress(id) !== undefined) {
454
435
  this.projectList();
436
+ }
455
437
  this.publishRetention(id);
456
438
  return reference;
457
439
  }
@@ -507,7 +489,7 @@ export class ClientSessions {
507
489
  /** Project the manager's list snapshot into the store (title derivation is display-only). */
508
490
  projectList() {
509
491
  const previousById = this.list.getSnapshot().byId;
510
- const { items, phase, subagentsByParent, jobsBySession, } = this.manager.getListSnapshot();
492
+ const { items, phase, projectionsBySession, } = this.manager.getListSnapshot();
511
493
  const ids = [];
512
494
  const byId = {};
513
495
  for (const entry of items) {
@@ -528,10 +510,8 @@ export class ClientSessions {
528
510
  ...(entry.origin !== undefined ? { origin: entry.origin } : {}),
529
511
  };
530
512
  }
531
- for (const [parentId, catalog] of Object.entries(subagentsByParent)) {
532
- for (const child of catalog.entries) {
533
- if (child.kind !== 'child')
534
- continue;
513
+ for (const [parentId, projection] of Object.entries(projectionsBySession)) {
514
+ for (const child of projection.values.subagentCatalog ?? []) {
535
515
  const childId = child.id;
536
516
  const summary = byId[childId];
537
517
  const projectionValues = summary?.projectionValues ?? this.manager.projectionValues(childId);
@@ -541,7 +521,7 @@ export class ClientSessions {
541
521
  if (summary === undefined) {
542
522
  byId[childId] = {
543
523
  id: childId, displayTitle, parentId: parentId,
544
- origin: 'subagent', running: child.activity === 'running', blank: false, updatedAt: 0,
524
+ origin: 'subagent', running: this.scopes.get(childId)?.session.getSnapshot().running ?? false, blank: false, updatedAt: 0,
545
525
  retainedBy: this.retentionSnapshot(childId).retainedBy,
546
526
  ...(projectionValues === undefined ? {} : { projectionValues }),
547
527
  ...(title === undefined ? {} : { title }),
@@ -559,18 +539,26 @@ export class ClientSessions {
559
539
  for (const [id, record] of this.scopes) {
560
540
  if (byId[id] !== undefined)
561
541
  continue;
542
+ const address = this.manager.subagentAddress(id);
543
+ if (address === undefined)
544
+ continue;
562
545
  const previous = previousById[id];
563
546
  const snapshot = record.session.getSnapshot();
564
- const address = this.manager.subagentAddress(id);
547
+ const projectionValues = this.manager.projectionValues(id);
548
+ const projectedTitle = projectionValues?.title;
549
+ const title = typeof projectedTitle === 'string' && projectedTitle !== '' ? projectedTitle : previous?.title;
565
550
  byId[id] = {
566
551
  ...(previous ?? { id, displayTitle: id, updatedAt: 0 }),
567
552
  running: snapshot.running,
568
553
  retainedBy: record.retention.retainedBy,
569
554
  blank: snapshot.blank,
570
- ...(address === undefined ? {} : { parentId: address.parentSessionId, origin: 'subagent' }),
555
+ parentId: address.parentSessionId,
556
+ origin: 'subagent',
557
+ ...(projectionValues === undefined ? {} : { projectionValues }),
558
+ ...(title === undefined ? {} : { title, displayTitle: title }),
571
559
  };
572
560
  }
573
- this.list.set({ ids, byId, phase, subagentsByParent, jobsBySession });
561
+ this.list.set({ ids, byId, phase, projectionsBySession });
574
562
  }
575
563
  startScopeDrop(id, record, disposeFiber = true, sessionDisposal = this.manager.drop(id, record.session)) {
576
564
  const drop = this.dropScope(record, disposeFiber, sessionDisposal);
@@ -10,23 +10,20 @@ import { MutableSessionEventSource } from '../contract/events.ts';
10
10
  import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
11
11
  import type { SessionRemotes } from './remotes.ts';
12
12
  import { ProjectionValueStore } from './projection-store.ts';
13
- /** Messages requested per history page. */
13
+ /** Minimum message count for ordinary history windows. */
14
14
  export declare const PAGE_MESSAGES = 50;
15
- /** Messages requested per page while a turn jump loops backwards (fewer, larger round trips). */
15
+ /** Minimum messages per page while a turn jump loops backwards. */
16
16
  export declare const JUMP_PAGE_MESSAGES = 200;
17
17
  /** Manager-owned observers of a Session object's local state edges. */
18
18
  export interface SessionOptions {
19
19
  /** Catalog-discovered address selecting non-activating subagent transport. */
20
20
  address?: SubagentAddress;
21
- /** Whether the exact direct parent Agent was live at the latest catalog read; absent before that read. */
21
+ /** Whether the exact direct parent Agent is available in Host summaries; absent until known. */
22
22
  parentAvailable?: boolean;
23
23
  /**
24
- * First ACCEPTED prompt on a blank session (fires at most once, on the
25
- * prompt RPC's success response): the manager mirrors the blank→false flip
26
- * into its list row so the session surfaces without waiting for a host
27
- * frame. Acceptance is the flip point because it proves the user message
28
- * is in the host log; a rejected first prompt keeps the session blank
29
- * (hidden, still reusable by connectWorkspace).
24
+ * Publish each accepted prompt to the Manager, including after this Session
25
+ * object is replaced. Acceptance converts display state, but does not
26
+ * establish that a turn started or reached durable history.
30
27
  */
31
28
  onEngaged?(session: Session): void;
32
29
  /**
@@ -59,6 +56,7 @@ export declare class Session implements SessionFace {
59
56
  private jumpTargetSeq;
60
57
  /** The running jump loop's completion, shared by retargeting callers. */
61
58
  private jumpPromise;
59
+ private pendingHistory;
62
60
  private readonly stopObservingInbox;
63
61
  private readonly assistantStream;
64
62
  private running;
@@ -72,7 +70,7 @@ export declare class Session implements SessionFace {
72
70
  private promptAttempted;
73
71
  /** A first accepted prompt stays in the engaging phase until its turn is observable. */
74
72
  private firstPromptPendingTurn;
75
- /** Empty-log mirror (see ConversationSnapshot.blank); unknown bare sessions begin conservatively blank. */
73
+ /** New Session display state; unknown bare sessions begin conservatively blank. */
76
74
  private blankBit;
77
75
  private removed;
78
76
  private promptError;
@@ -88,8 +86,9 @@ export declare class Session implements SessionFace {
88
86
  * Per-session projection value store (push model; see the session-projection
89
87
  * subsystem page, docs/subsystems/session-projection.md): finished whole
90
88
  * values computed on the Host, seeded by the tail page's
91
- * projections block and updated by Session Controller control frames under the
92
- * one higher-seq-wins rule. Keys are read via `projections.faceOf(key)`
89
+ * projections block and updated by Session Controller control frames;
90
+ * Host-sequenced writes merge under higher-seq-wins and cached list blocks
91
+ * yield to them (projection-store.ts). Keys are read via `projections.faceOf(key)`
93
92
  * (the useProjection resolution face); the conversation snapshot never
94
93
  * carries projection values, and no client-side domain folding exists.
95
94
  * Manager-owned when constructed through SessionManager (frames route and
@@ -193,7 +192,7 @@ export declare class Session implements SessionFace {
193
192
  }>>;
194
193
  /** First open: pull the tail page (idempotent — in-flight/already-open returns the existing promise). */
195
194
  open(): Promise<void>;
196
- /** Page up: pull one earlier page with the window's first seq as beforeSeq and prepend. */
195
+ /** Prepend one Turn-aligned page: at least 50 messages and two Turn starts, capped at 500 messages. */
197
196
  loadOlder(): Promise<void>;
198
197
  /** Jump loader: page backwards until the window covers seq (see ISession.loadThrough). */
199
198
  loadThrough(seq: SessionSeq): Promise<void>;
@@ -230,11 +229,12 @@ export declare class Session implements SessionFace {
230
229
  */
231
230
  handleSubagentParentAvailable(available: boolean): void;
232
231
  /**
233
- * Blank-bit relay from the authoritative summary source (`session.list` and
234
- * `api-session/added`). Monotone: once any signal (local first send,
235
- * running flip, an earlier summary) cleared it, a stale true never
236
- * re-blanks.
237
- * @param blank - the summary's derived empty-log bit.
232
+ * Apply the Manager's effective display blank, further reconciled with the
233
+ * current `sessionListMetadata` projection. Local send attempts and current
234
+ * running state prevent re-blanking; an earlier false summary alone does not.
235
+ * The Manager retains acceptance and earlier running observations across
236
+ * Session-object replacement.
237
+ * @param blank - New Session display state after Manager reconciliation.
238
238
  */
239
239
  handleBlank(blank: boolean): void;
240
240
  /** `api-session/removed` relay: flag the snapshot while retaining the resident instance. */
@@ -262,7 +262,11 @@ export declare class Session implements SessionFace {
262
262
  private appendLive;
263
263
  /** Observe durable acceptance even when insertion and claim share one projection notification. */
264
264
  private observeSubmissionEvent;
265
- /** Retire local echoes when their accepted messages appear in the durable Inbox projection. */
265
+ private observeSubmissionInsertions;
266
+ private observeSubmissionMessage;
267
+ /** Retire admitted Chat identities only after stale Inbox rows can no longer reappear. */
268
+ private retireAdmittedSubmission;
269
+ /** Inbox acceptance retires queued echoes; its watermark completes admitted Chat handoffs. */
266
270
  private observeSubmissionInbox;
267
271
  /**
268
272
  * Latch one observed settlement and remove the echo an animation frame