@deepseek-ai/dsh-api-session-controller 0.1.6-alpha.1 → 0.1.7-alpha.1

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 (44) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +42 -7
  3. package/README.zh.md +42 -7
  4. package/lib/client.js +1037 -992
  5. package/lib/index.js +314 -183
  6. package/lib/typert.host.js +539 -331
  7. package/lib/typert.remote-client.d.ts +10 -5
  8. package/lib/typert.remote-client.js +374 -264
  9. package/lib/types/agent.d.ts +1 -1
  10. package/lib/types/agent.js +8 -1
  11. package/lib/types/archived-session-gate.d.ts +30 -0
  12. package/lib/types/archived-session-gate.js +52 -0
  13. package/lib/types/client/contract/session.d.ts +2 -0
  14. package/lib/types/client/contract/sessions.d.ts +65 -35
  15. package/lib/types/client/contract/snapshot.d.ts +0 -14
  16. package/lib/types/client/index.d.ts +12 -3
  17. package/lib/types/client/index.js +16 -5
  18. package/lib/types/client/scope.d.ts +13 -3
  19. package/lib/types/client/scope.js +15 -22
  20. package/lib/types/client/sessions/lineage.d.ts +5 -5
  21. package/lib/types/client/sessions/lineage.js +3 -4
  22. package/lib/types/client/sessions/manager.d.ts +91 -105
  23. package/lib/types/client/sessions/manager.js +307 -434
  24. package/lib/types/client/sessions/projection-store.d.ts +34 -21
  25. package/lib/types/client/sessions/projection-store.js +48 -24
  26. package/lib/types/client/sessions/remotes.d.ts +1 -2
  27. package/lib/types/client/sessions/service.d.ts +53 -150
  28. package/lib/types/client/sessions/service.js +267 -264
  29. package/lib/types/client/sessions/session.d.ts +24 -35
  30. package/lib/types/client/sessions/session.js +131 -73
  31. package/lib/types/commands.d.ts +6 -4
  32. package/lib/types/commands.js +82 -40
  33. package/lib/types/control.d.ts +2 -4
  34. package/lib/types/control.js +2 -84
  35. package/lib/types/history.js +1 -1
  36. package/lib/types/index.d.ts +34 -7
  37. package/lib/types/index.js +118 -14
  38. package/lib/types/list.js +24 -15
  39. package/lib/types/skill-catalog.js +60 -50
  40. package/lib/types/types.d.ts +40 -40
  41. package/lib/types/types.js +0 -1
  42. package/package.json +71 -76
  43. package/lib/types/client/sessions/queue-mirror.d.ts +0 -26
  44. package/lib/types/client/sessions/queue-mirror.js +0 -61
@@ -29,7 +29,7 @@ export declare class ApiSessionPresetConflict extends Error {
29
29
  constructor(sessionId: SessionId, requestedPreset: string, existingPreset: string | undefined);
30
30
  }
31
31
  /** Failures produced while resolving one ordinary Session identity to its live Agent. */
32
- export type ApiSessionAgentError = RemoteError<'session/not-found' | 'session/agent-busy' | 'gateway/internal'>;
32
+ export type ApiSessionAgentError = RemoteError<'session/not-found' | 'session/agent-busy' | 'session/writer-held' | 'gateway/internal'>;
33
33
  /** Result of resolving one ordinary Session identity to its live Agent. */
34
34
  export type ApiSessionAgentResult = {
35
35
  readonly agent: Agent;
@@ -219,7 +219,11 @@ export class ApiSessionAgentController {
219
219
  this.resumes.set(sessionId, resume);
220
220
  }
221
221
  try {
222
- return { agent: await resume };
222
+ const agent = await resume;
223
+ // A shared resume can publish an identity that subagent routing adopts
224
+ // before every waiter observes it; apply the live ownership policy again.
225
+ const published = this.liveAgent(sessionId);
226
+ return published ?? { agent };
223
227
  }
224
228
  catch (error) {
225
229
  if (error instanceof ApiSessionNotFound) {
@@ -235,6 +239,9 @@ export class ApiSessionAgentController {
235
239
  if (racedSession !== undefined && hasApiSessionSubagentOwner(this.ctx, racedSession, undefined)) {
236
240
  return { error: apiSessionSubagentOwnershipError(sessionId) };
237
241
  }
242
+ if (error instanceof Error && error.name === 'SessionAlreadyOwnedError') {
243
+ return { error: new RemoteError('session/writer-held', error.message, { sessionId }) };
244
+ }
238
245
  return {
239
246
  error: new RemoteError('gateway/internal', `resume failed for session "${sessionId}": ${String(error)}`, {}),
240
247
  };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The controller's admission gate for archived Sessions: an archived
3
+ * Session, or a subagent descendant of one, must not run a model step until
4
+ * it is restored. The work a Session still runs is reported and stopped by
5
+ * its owners — the Agent registry (`turn`), the job registry seam (`job`),
6
+ * the Subagent runtime (`subagent`), and the Schedule plugin (`schedule`) —
7
+ * through the Workspace registry's archive-admission events.
8
+ */
9
+ import type { Context, Plugin } from '@deepseek-ai/cordis';
10
+ import type { Agent } from '@deepseek-ai/dsh-agent';
11
+ /**
12
+ * The gate as a plugin for `ctx.plugin(...)`: it loads once the Agent
13
+ * registry, Session store, and Workspace registry are available and unwinds
14
+ * with the plugin's fiber. A late waking delivery to an archived Session — a
15
+ * subagent settlement, a queued follow-up — proposes a step the gate rejects,
16
+ * which the loop ends as `blocked` without a request; unarchiving lifts the
17
+ * gate for the whole lineage.
18
+ */
19
+ export declare const ArchivedSessionGate: Plugin.Object<void>;
20
+ /**
21
+ * Whether the Agent's Session, or a Session above it in its subagent lineage,
22
+ * is archived. Lineage follows the durable header fields through
23
+ * subagent-origin Sessions only: a fork of an archived Session is an
24
+ * independent conversation.
25
+ * @param ctx - Host context.
26
+ * @param agent - the Agent proposing a step.
27
+ * @returns whether an archived Session owns the step.
28
+ */
29
+ export declare function underArchivedSession(ctx: Context, agent: Agent): boolean;
30
+ //# sourceMappingURL=archived-session-gate.d.ts.map
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The controller's admission gate for archived Sessions: an archived
3
+ * Session, or a subagent descendant of one, must not run a model step until
4
+ * it is restored. The work a Session still runs is reported and stopped by
5
+ * its owners — the Agent registry (`turn`), the job registry seam (`job`),
6
+ * the Subagent runtime (`subagent`), and the Schedule plugin (`schedule`) —
7
+ * through the Workspace registry's archive-admission events.
8
+ */
9
+ /**
10
+ * The gate as a plugin for `ctx.plugin(...)`: it loads once the Agent
11
+ * registry, Session store, and Workspace registry are available and unwinds
12
+ * with the plugin's fiber. A late waking delivery to an archived Session — a
13
+ * subagent settlement, a queued follow-up — proposes a step the gate rejects,
14
+ * which the loop ends as `blocked` without a request; unarchiving lifts the
15
+ * gate for the whole lineage.
16
+ */
17
+ export const ArchivedSessionGate = {
18
+ name: 'archived-session-gate',
19
+ inject: ['agents', 'sessions', 'workspaceRegistry'],
20
+ apply(ctx) {
21
+ ctx.on('agent/pre-step', (payload, next) => underArchivedSession(ctx, payload.agent)
22
+ ? Promise.resolve({ kind: 'reject' })
23
+ : next());
24
+ },
25
+ };
26
+ /**
27
+ * Whether the Agent's Session, or a Session above it in its subagent lineage,
28
+ * is archived. Lineage follows the durable header fields through
29
+ * subagent-origin Sessions only: a fork of an archived Session is an
30
+ * independent conversation.
31
+ * @param ctx - Host context.
32
+ * @param agent - the Agent proposing a step.
33
+ * @returns whether an archived Session owns the step.
34
+ */
35
+ export function underArchivedSession(ctx, agent) {
36
+ const archived = ctx.workspaceRegistry.archivedSessionIds;
37
+ let header = agent.session.header;
38
+ const visited = new Set();
39
+ while (!visited.has(header.id)) {
40
+ if (archived.includes(header.id))
41
+ return true;
42
+ visited.add(header.id);
43
+ if (header.origin !== 'subagent' || header.parentSession === undefined)
44
+ return false;
45
+ const parent = ctx.sessions.get(header.parentSession);
46
+ if (parent === undefined)
47
+ return archived.includes(header.parentSession);
48
+ header = parent.header;
49
+ }
50
+ return false;
51
+ }
52
+ //# sourceMappingURL=archived-session-gate.js.map
@@ -127,6 +127,8 @@ export interface ISession {
127
127
  * turn-jump loader. Repeated calls while a jump is paging lower its shared
128
128
  * target and return the in-flight completion; `snapshot.loadingOlder` is
129
129
  * the busy signal for the whole jump.
130
+ * Older pages publish together at completion, including successful pages
131
+ * before a later failure; live events remain independently visible.
130
132
  * @param seq - durable event seq the window must reach (a turn's `turn/start` seq).
131
133
  * @returns completion once covered, exhausted, superseded, or failed soft.
132
134
  */
@@ -14,11 +14,58 @@ import type { SessionSearchResultItem } from '../sessions/manager.ts';
14
14
  import type { SessionBinding, SessionListState } from '../sessions/service.ts';
15
15
  import type { SessionFace } from './session.ts';
16
16
  import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store';
17
+ import type { SessionReferenceSource } from '../index.ts';
17
18
  export type { AgentContext } from '../scope.ts';
19
+ /** Known Session identity or durable direct-parent subagent address; an address owns no lifetime. */
20
+ export type SessionTarget = SessionId | SubagentAddress;
21
+ /** One independent use of an exact Client generation, without Host Agent ownership. */
22
+ export interface SessionReference extends Disposable {
23
+ readonly sessionId: SessionId;
24
+ /** Shared binding; access fails after reference release or generation disposal. */
25
+ readonly binding: SessionBinding;
26
+ /** This reference's cancellable wait for the shared initial `Session.open()` attempt to settle. */
27
+ readonly ready: Promise<SessionBinding>;
28
+ /** Release once; the final reference starts local scope and history teardown. */
29
+ release(): void;
30
+ }
31
+ /** Consumer identity and optional cancellation of one acquisition waiter. */
32
+ export interface SessionRetainOptions {
33
+ readonly source: SessionReferenceSource;
34
+ readonly signal?: AbortSignal | undefined;
35
+ }
36
+ /** Local ownership counts, independent of catalog membership and never persisted. */
37
+ export interface SessionRetainInfo {
38
+ readonly referenceCount: number;
39
+ /** Positive source counts only; a source without references is absent. */
40
+ readonly retainedBy: Readonly<Partial<Record<SessionReferenceSource, number>>>;
41
+ }
18
42
  /** The sessions-service face injected as `ctx.sessions`. */
19
43
  export interface ISessions {
20
- /** The useSessions standard feed (list rows + current selection; read face — writes stay inside the domain). */
44
+ /** Host catalog and local reference-source counts; navigation belongs to view owners. */
21
45
  readonly list: ObservableSnapshot<SessionListState>;
46
+ /**
47
+ * Retain an exact Client generation and start its shared initial history opening.
48
+ * @param target - known identity or durable direct-parent address.
49
+ * @param options - required consumer source and optional independent waiter cancellation.
50
+ * @returns an owned reference immediately; await `reference.ready` when the initial open attempt must settle first.
51
+ */
52
+ retain(target: SessionTarget, options: SessionRetainOptions): SessionReference;
53
+ /**
54
+ * Hold one reference through callback settlement, including synchronous and asynchronous failures.
55
+ * @param target - Session to acquire.
56
+ * @param options - source and acquisition cancellation.
57
+ * @param operation - callback using the reference only until its returned value or Promise settles.
58
+ * @returns the callback result after release; acquisition and callback failures propagate unchanged.
59
+ */
60
+ using<T>(target: SessionTarget, options: SessionRetainOptions, operation: (reference: SessionReference) => T | Promise<T>): Promise<T>;
61
+ /**
62
+ * Observe local reference counts without retaining, creating a scope, or opening history.
63
+ * The returned source keeps stable identity across same-id generations and remains allocated
64
+ * until the Client root is disposed, even after its final subscriber leaves.
65
+ * @param id - explicit Session identity; Host existence is not implied.
66
+ * @returns a stable read-only source across same-id generations, with zero counts when none is live.
67
+ */
68
+ retainInfo(id: SessionId): ObservableSnapshot<SessionRetainInfo>;
22
69
  /**
23
70
  * The `session.search` result bound the wire schema fixes, exposed to
24
71
  * presentation as injected data. Not per-connection state: every transport
@@ -28,43 +75,25 @@ export interface ISessions {
28
75
  /**
29
76
  * Create or adopt a Session on the Host.
30
77
  * @param opts - target workspace, directory, and optional preallocated identity.
31
- * @returns the Session identity after its local binding is addressable.
78
+ * @returns the catalogued identity; retain it before borrowing its binding.
32
79
  */
33
80
  create(opts?: {
34
81
  workspaceId?: WorkspaceId;
35
82
  cwd?: string;
36
83
  sessionId?: SessionId;
37
84
  }): Promise<SessionId>;
38
- /**
39
- * Select a session as current.
40
- * @param id - session id (must exist in the list; unknown ids fail loud).
41
- */
42
- open(id: SessionId): void;
43
- /**
44
- * Open a healthy catalog child through its exact direct-parent address.
45
- * @param address - catalog-derived parent and child ids.
46
- */
47
- openSubagent(address: SubagentAddress): void;
48
85
  /**
49
86
  * Resolve an already discovered direct-parent address without opening it.
50
87
  * @param id - possible addressed child id.
51
- * @returns the retained address, when present.
88
+ * @returns a retained or loaded-catalog address, without retaining a new selection or scope.
52
89
  */
53
90
  subagentAddress(id: SessionId): SubagentAddress | undefined;
54
91
  /**
55
- * Mark whether a catalog menu is consuming live membership updates.
56
- * @param parentSessionId - catalog owner.
57
- * @param open - current menu state.
58
- */
59
- setSubagentCatalogOpen(parentSessionId: SessionId, open: boolean): void;
60
- /**
61
- * Refresh one direct-child catalog.
62
- * @param parentSessionId - catalog owner.
92
+ * Load all Session projections once per connection; retry an unsuccessful initial read.
93
+ * @param sessionId - Session to inspect without opening its conversation.
63
94
  * @returns completion of the current or newly started refresh.
64
95
  */
65
- refreshSubagents(parentSessionId: SessionId): Promise<void>;
66
- /** Clear the current selection into the no-session view state. */
67
- clear(): void;
96
+ refreshProjections(sessionId: SessionId): Promise<void>;
68
97
  /**
69
98
  * Refresh the Host-authoritative Session list.
70
99
  * @returns completion of the current or newly started Session-list refresh.
@@ -82,12 +111,13 @@ export interface ISessions {
82
111
  hasMore: boolean;
83
112
  }>>;
84
113
  /**
85
- * Fork a session from a completed-turn prefix of the source; on resolution
86
- * the child is in the list store and `open()` can target it.
87
- * @param opts - source session id, the optional event seq anchoring the
88
- * cut (the boundary is the first turn/end at or after it; an in-log
89
- * anchor in an open turn is unavailable rather than clipped backward),
90
- * and whether to increment an inherited durable title before resolving.
114
+ * Fork a session from an exact inclusive prefix of the source; on
115
+ * resolution the child is catalogued and can be explicitly retained.
116
+ * @param opts - source session id, the optional exact inclusive boundary
117
+ * seq (a real event seq the caller already knows; a cut inside an open
118
+ * turn is balanced Host-side with synthetic closers, and omission selects
119
+ * the latest completed-turn prefix), and whether to increment an
120
+ * inherited durable title before resolving.
91
121
  * @returns the child session id.
92
122
  * @throws when the fork fails, or when a requested child-title rename fails after creation.
93
123
  */
@@ -97,9 +127,9 @@ export interface ISessions {
97
127
  increaseTitle?: boolean;
98
128
  }): Promise<SessionId>;
99
129
  /**
100
- * Resolve an Agent-scoped context view (use-and-discard).
130
+ * Borrow an already-retained Agent-scoped Context without extending its lifetime.
101
131
  * @param id - session id.
102
- * @returns scoped ctx, or undefined for a session neither listed nor already scoped.
132
+ * @returns the live scoped Context, or undefined without a retained generation.
103
133
  */
104
134
  scope(id: SessionId): AgentContext | undefined;
105
135
  /**
@@ -112,13 +142,13 @@ export interface ISessions {
112
142
  /**
113
143
  * Resolve the session face behind an Agent-scoped context.
114
144
  * @param ctx - an Agent-scoped context.
115
- * @returns the session face, or undefined when the ctx is untagged or its scope was pruned.
145
+ * @returns the matching live Session, or undefined for an untagged, foreign, or ended generation.
116
146
  */
117
147
  sessionOf(ctx: Context): SessionFace | undefined;
118
148
  /**
119
- * Resolve the stable session binding (scope-addressed assembly feed).
149
+ * Borrow an already-retained Session binding without extending its lifetime.
120
150
  * @param id - session id.
121
- * @returns binding, or undefined for a session neither listed nor already scoped.
151
+ * @returns the live binding, or undefined without a retained generation.
122
152
  */
123
153
  binding(id: SessionId): SessionBinding | undefined;
124
154
  }
@@ -1,22 +1,9 @@
1
1
  /** Session-owned observable state excluding Conversation target data. */
2
- import type { ContentBlock } from '@deepseek-ai/dsh-llm/types';
3
2
  import type { FileAttachmentRef } from '@deepseek-ai/dsh-attachment';
4
- import type { MessageId } from '@deepseek-ai/dsh-llm/brand';
5
3
  import type { SessionId } from '@deepseek-ai/dsh-session/types';
6
4
  import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client';
7
5
  import type { RemoteFailure } from '@deepseek-ai/dsh-typert-protocol';
8
6
  import type { SessionRequestId } from '../../types.ts';
9
- /** One transient inbox occurrence from the authoritative queue snapshot. */
10
- export interface QueuedMessage {
11
- readonly id: MessageId;
12
- readonly messageId: MessageId;
13
- readonly placement: 'queued' | 'steering' | 'context';
14
- /** Prompt-RPC identity of a browser-submitted occurrence; correlates the local submission echo. */
15
- readonly rpcId?: SessionRequestId;
16
- readonly content: readonly ContentBlock[];
17
- readonly preview: string;
18
- readonly text: string | null;
19
- }
20
7
  /** One image displayed by a local submission echo before durable admission. */
21
8
  export interface PendingSubmissionImage {
22
9
  /** Browser-owned preview URL; its lifecycle belongs to the submitter, never this snapshot. */
@@ -70,7 +57,6 @@ export interface PromptError {
70
57
  /** Immutable Session lifecycle and control snapshot. */
71
58
  export interface SessionSnapshot {
72
59
  readonly sessionId: SessionId;
73
- readonly queue: readonly QueuedMessage[];
74
60
  /** Local prompt-submission echoes not yet observed as durable events or queue occurrences. */
75
61
  readonly pendingSubmissions: readonly PendingSubmission[];
76
62
  readonly running: boolean;
@@ -6,14 +6,23 @@ export { createScope, scopeOf } from './scope.ts';
6
6
  export type { AgentContext, AgentScopeHandle } from './scope.ts';
7
7
  export { SessionCreateError, SessionForkError } from './sessions/service.ts';
8
8
  export type { SessionBinding, SessionListState, SessionSummary } from './sessions/service.ts';
9
- export type { SessionListPhase, SessionListSnapshot, SessionSearchResultItem, SubagentCatalogSnapshot, } from './sessions/manager.ts';
9
+ export type { SessionListPhase, SessionListSnapshot, SessionSearchResultItem, SessionProjectionSnapshot, } from './sessions/manager.ts';
10
10
  export type { Session } from './sessions/session.ts';
11
11
  export type { ProjectionsBaseline, ProjectionValueStore, SessionProjectionMap, UseProjection, } from './sessions/projection-store.ts';
12
12
  export type { BeginSubmissionInput, ISession, PendingSubmissionRetirement, ProjectionsFace, SessionFace, SubmissionHandle, } from './contract/session.ts';
13
- export type { ISessions } from './contract/sessions.ts';
13
+ export type { ISessions, SessionReference, SessionRetainInfo, SessionRetainOptions, SessionTarget, } from './contract/sessions.ts';
14
14
  export { MutableSessionEventSource } from './contract/events.ts';
15
15
  export type { AssistantLiveChunkEvent, SessionAssistantSettlementEntry, SessionEventChange, SessionEventLike, SessionEventLikeEntry, SessionEventSource, SessionEventWindow, SessionLiveEventEntry, SessionTransientEventEntry, } from './contract/events.ts';
16
- export type { OpenState, PendingSubmission, PendingSubmissionAttachment, PendingSubmissionFileAttachment, PendingSubmissionImage, PendingSubmissionImageAttachment, PendingSubmissionPlacement, PromptError, QueuedMessage, SessionSnapshot, } from './contract/snapshot.ts';
16
+ export type { OpenState, PendingSubmission, PendingSubmissionAttachment, PendingSubmissionFileAttachment, PendingSubmissionImage, PendingSubmissionImageAttachment, PendingSubmissionPlacement, PromptError, SessionSnapshot, } from './contract/snapshot.ts';
17
+ /** Consumer-owned reference labels; extend this map through the package's canonical /client entry. */
18
+ export interface SessionReferenceSourceMap {
19
+ /** Temporary Client Controller work, including fork-title preparation. */
20
+ controllerOperation: unknown;
21
+ /** A Client Gateway invocation's synchronous Context ownership. */
22
+ gateway: unknown;
23
+ }
24
+ /** Declaration-merge-extensible labels carried by independent Client references. */
25
+ export type SessionReferenceSource = Extract<keyof SessionReferenceSourceMap, string>;
17
26
  declare module '@deepseek-ai/cordis' {
18
27
  interface Context {
19
28
  /** Client Session object layer and Agent scope owner. */
@@ -1,4 +1,5 @@
1
1
  /** Client Session object layer, Agent scopes, and Remote lifecycle wiring. */
2
+ import { typertOwnedValue } from '@deepseek-ai/dsh-typert-protocol';
2
3
  import { createSessionControlStream } from "./transport.js";
3
4
  import { ClientSessions } from "./sessions/service.js";
4
5
  export { createSessionControlStream, SessionEventStream, SESSION_SEARCH_RESULT_LIMIT, SESSION_SEARCH_SNIPPET_MAX_CODE_POINTS, } from "./transport.js";
@@ -21,6 +22,7 @@ export const inject = [
21
22
  */
22
23
  export function apply(ctx) {
23
24
  const remotes = ctx.remote;
25
+ const connection = ctx.get('connection');
24
26
  const sessions = new ClientSessions(ctx, remotes);
25
27
  ctx.remote.$on('api-session/added', (summary) => { sessions.handleSessionAdded(summary); });
26
28
  ctx.remote.$on('api-session/removed', (sessionId) => { sessions.handleSessionRemoved(sessionId); });
@@ -37,13 +39,22 @@ export function apply(ctx) {
37
39
  accept: (frame) => { sessions.handleControlFrame(frame); },
38
40
  failed: (error) => { console.error('[session-controller] control stream failed:', error); },
39
41
  });
40
- control.start();
41
- ctx.on('connection/reset', () => { sessions.handleConnected(); });
42
- if (ctx.remote.$host.home !== undefined)
42
+ const connected = () => {
43
+ if (connection.generation.getSnapshot() === undefined)
44
+ return;
45
+ // A ready control baseline may arrive before Cordis delivers connection/reset.
43
46
  sessions.handleConnected();
47
+ control.restart();
48
+ control.start();
49
+ };
50
+ ctx.effect(() => connection.generation.subscribe(connected), 'session-controller.client.generation');
51
+ connected();
44
52
  ctx.typert.contexts.registerClient('agent', {
45
- identity: candidate => sessions.scopeOf(candidate),
46
- resolve: sessionId => sessions.resolveAgentScope(sessionId),
53
+ identity: candidate => sessions.sessionOf(candidate)?.sessionId,
54
+ resolve: (sessionId) => {
55
+ const reference = sessions.retainAgentScope(sessionId);
56
+ return typertOwnedValue(reference.binding.ctx, () => { reference.release(); });
57
+ },
47
58
  });
48
59
  ctx.effect(() => async () => { await control.dispose(); }, 'session-controller.client.control');
49
60
  }
@@ -6,11 +6,14 @@ import type { TypertRemoteScopeApi } from '@deepseek-ai/dsh-typert-protocol';
6
6
  export type AgentContext = Omit<Context, 'remote'> & {
7
7
  readonly remote: ClientRemote & TypertRemoteScopeApi<'agent'>;
8
8
  };
9
+ interface ScopeIdentity {
10
+ readonly sessionId: SessionId;
11
+ }
9
12
  /** A minted Agent scope and its disposal boundary. */
10
13
  export interface AgentScopeHandle {
11
14
  /**
12
15
  * Tagged context: scope-owned registrations and scoped dispatch both go
13
- * through it (passing it as the dispatch subject routes to this agent's
16
+ * through it (passing it as the dispatch subject routes to this generation's
14
17
  * tagged listeners plus every untagged one).
15
18
  */
16
19
  ctx: AgentContext;
@@ -20,10 +23,10 @@ export interface AgentScopeHandle {
20
23
  /**
21
24
  * Mint an Agent scope under `ctx`: a no-op plugin fiber whose context
22
25
  * carries the agent tag and the dispatch filter — untagged listeners are
23
- * admitted globally, tagged listeners only for a matching agent.
26
+ * admitted globally, tagged listeners only for the same Client generation.
24
27
  * Registrations through the returned ctx dispose with the fiber.
25
28
  * @param ctx - client root context the scope fiber mounts under.
26
- * @param key - owning agent identity (the routing tag; agent id === session id).
29
+ * @param key - durable Session identity carried by this generation.
27
30
  * @returns the tagged context and its backing fiber.
28
31
  */
29
32
  export declare function createScope(ctx: Context, key: SessionId): AgentScopeHandle;
@@ -33,4 +36,11 @@ export declare function createScope(ctx: Context, key: SessionId): AgentScopeHan
33
36
  * @returns its agent identity (the session id), or undefined for root contexts.
34
37
  */
35
38
  export declare function scopeOf(ctx: Context): SessionId | undefined;
39
+ /**
40
+ * Read the exact generation identity inherited by a Client Context.
41
+ * @param ctx - scoped or root Client Context.
42
+ * @returns the generation identity, or undefined for an unscoped Context.
43
+ */
44
+ export declare function scopeIdentityOf(ctx: Context): ScopeIdentity | undefined;
45
+ export {};
36
46
  //# sourceMappingURL=scope.d.ts.map
@@ -1,20 +1,4 @@
1
- /**
2
- * Client Agent-scope primitive: mint a Cordis context tagged with the owning
3
- * Agent's identity. The mechanism mirrors the host `dsh-scope` architecture
4
- * (no-op plugin fiber + context tag + `Context.filter` routing predicate);
5
- * the shape deliberately diverges: the filter lives on the actx itself
6
- * instead of a separate carrier object, so scoped dispatch is plain cordis —
7
- * `actx.bail(actx, event, payload)` / `actx.emit(actx, ...)` — with no
8
- * wrapper. The host needs a detached carrier because its dispatch subject is
9
- * the business Agent object; client scope events carry only ids, so the
10
- * actx is the natural subject. The second divergence stands: the scope key
11
- * is the branded `SessionId` (value compared), not an object identity — the
12
- * agent and its session share one id (1:1, same axis; no separate AgentId
13
- * brand), and a client scope's identity IS that wire id. Third divergence,
14
- * deliberate: the client scopes the Agent IDENTITY, not a live Agent object
15
- * — a cold session's host Agent is already disposed while its client actx
16
- * stays alive for history viewing.
17
- */
1
+ /** Client scope generations route local events independently of Host Agent residency. */
18
2
  import { Context as CordisContext } from '@deepseek-ai/cordis';
19
3
  /** Context tag written by {@link createScope}. */
20
4
  const kScope = Symbol('dsh.client.scope');
@@ -23,19 +7,20 @@ function agentScope() { }
23
7
  /**
24
8
  * Mint an Agent scope under `ctx`: a no-op plugin fiber whose context
25
9
  * carries the agent tag and the dispatch filter — untagged listeners are
26
- * admitted globally, tagged listeners only for a matching agent.
10
+ * admitted globally, tagged listeners only for the same Client generation.
27
11
  * Registrations through the returned ctx dispose with the fiber.
28
12
  * @param ctx - client root context the scope fiber mounts under.
29
- * @param key - owning agent identity (the routing tag; agent id === session id).
13
+ * @param key - durable Session identity carried by this generation.
30
14
  * @returns the tagged context and its backing fiber.
31
15
  */
32
16
  export function createScope(ctx, key) {
33
17
  const fiber = ctx.plugin(agentScope);
18
+ const identity = { sessionId: key };
34
19
  const scoped = fiber.ctx.extend({
35
- [kScope]: key,
20
+ [kScope]: identity,
36
21
  [CordisContext.filter](listenerCtx) {
37
- const tag = scopeOf(listenerCtx);
38
- return tag === undefined || tag === key;
22
+ const tag = scopeIdentityOf(listenerCtx);
23
+ return tag === undefined || tag === identity;
39
24
  },
40
25
  });
41
26
  return {
@@ -49,6 +34,14 @@ export function createScope(ctx, key) {
49
34
  * @returns its agent identity (the session id), or undefined for root contexts.
50
35
  */
51
36
  export function scopeOf(ctx) {
37
+ return scopeIdentityOf(ctx)?.sessionId;
38
+ }
39
+ /**
40
+ * Read the exact generation identity inherited by a Client Context.
41
+ * @param ctx - scoped or root Client Context.
42
+ * @returns the generation identity, or undefined for an unscoped Context.
43
+ */
44
+ export function scopeIdentityOf(ctx) {
52
45
  return ctx[kScope];
53
46
  }
54
47
  //# sourceMappingURL=scope.js.map
@@ -13,7 +13,10 @@ export interface SessionListEntry {
13
13
  title?: string;
14
14
  updatedAt: number;
15
15
  running: boolean;
16
- /** Empty-log bit mirrored from the summary; lists hide blank sessions (filtering stays with the consumer). */
16
+ /**
17
+ * New Session presentation and reuse eligibility, reconciled with
18
+ * `sessionListMetadata`; lists hide blank sessions (filtering stays with the consumer).
19
+ */
17
20
  blank: boolean;
18
21
  parentSessionId?: SessionId;
19
22
  /** Coarse durable origin for navigation filtering; not a continuation capability. */
@@ -21,8 +24,6 @@ export interface SessionListEntry {
21
24
  cwd?: string;
22
25
  /** Current host-computed projection values for list consumers. */
23
26
  projectionValues?: Readonly<Partial<SessionProjectionMap>>;
24
- /** Finished running while not selected and not yet opened — the sidebar's green "done" reminder (clears on select or the next run). */
25
- completed: boolean;
26
27
  /** Lineage indent depth: root = 0; the UI just multiplies by the indent width. */
27
28
  depth: number;
28
29
  }
@@ -31,8 +32,7 @@ export interface SessionListEntry {
31
32
  * follows the established input order; this projection never re-sorts a
32
33
  * hydrated list from mutable timestamps.
33
34
  * @param summaries - the host's session.list items.
34
- * @param completed - sessions with a pending completion reminder (manager-owned live fact; absent = false).
35
35
  * @returns display rows in render order.
36
36
  */
37
- export declare function flattenLineage(summaries: readonly TitledSessionSummary[], completed?: ReadonlySet<SessionId>): SessionListEntry[];
37
+ export declare function flattenLineage(summaries: readonly TitledSessionSummary[]): SessionListEntry[];
38
38
  //# sourceMappingURL=lineage.d.ts.map
@@ -6,10 +6,9 @@
6
6
  * follows the established input order; this projection never re-sorts a
7
7
  * hydrated list from mutable timestamps.
8
8
  * @param summaries - the host's session.list items.
9
- * @param completed - sessions with a pending completion reminder (manager-owned live fact; absent = false).
10
9
  * @returns display rows in render order.
11
10
  */
12
- export function flattenLineage(summaries, completed) {
11
+ export function flattenLineage(summaries) {
13
12
  const byId = new Map();
14
13
  for (const s of summaries)
15
14
  byId.set(s.sessionId, s);
@@ -33,9 +32,9 @@ export function flattenLineage(summaries, completed) {
33
32
  return;
34
33
  }
35
34
  visited.add(s.sessionId);
35
+ const { agentAvailable: _agentAvailable, ...row } = s;
36
36
  out.push({
37
- ...s,
38
- completed: completed?.has(s.sessionId) ?? false,
37
+ ...row,
39
38
  depth,
40
39
  });
41
40
  const kids = children.get(s.sessionId);