@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,10 @@
1
- /** Live Session jobs and projection state with reconnect baselines. */
1
+ /** Live Session projection state with reconnect baselines. */
2
2
  import { Deque } from '@deepseek-ai/dsh-deque';
3
3
  /** Owns the Host-wide Session control stream. */
4
4
  export class SessionControlController {
5
5
  ctx;
6
6
  streams = new Set();
7
- /** @param ctx - Host context carrying live Agent, projection, and jobs services. */
7
+ /** @param ctx - Host context carrying live Agent and projection services. */
8
8
  constructor(ctx) {
9
9
  this.ctx = ctx;
10
10
  ctx.sessionProjections.onChanged((session, key, value, seq) => {
@@ -16,14 +16,6 @@ export class SessionControlController {
16
16
  seq,
17
17
  });
18
18
  });
19
- ctx.inject(['jobs'], (jobsCtx) => {
20
- jobsCtx.jobs.onJobsChanged((owner) => { this.onJobsChanged(owner); });
21
- });
22
- ctx.on('session/created', (session) => {
23
- const jobs = this.jobsFor(this.ctx.agents.get(session.id));
24
- if (jobs.length > 0)
25
- this.broadcast({ type: 'jobs', sessionId: session.id, jobs });
26
- });
27
19
  ctx.effect(() => () => {
28
20
  for (const stream of this.streams)
29
21
  stream.end();
@@ -50,13 +42,7 @@ export class SessionControlController {
50
42
  }
51
43
  baseline() {
52
44
  const sessions = this.ctx.sessions.list();
53
- const jobs = Object.create(null);
54
- for (const session of sessions) {
55
- const agent = this.ctx.agents.get(session.id);
56
- jobs[session.id] = this.jobsFor(agent);
57
- }
58
45
  return {
59
- jobs,
60
46
  projections: this.projectionBaseline(sessions),
61
47
  };
62
48
  }
@@ -72,23 +58,6 @@ export class SessionControlController {
72
58
  }
73
59
  return blocks;
74
60
  }
75
- onJobsChanged(owner) {
76
- if (owner !== undefined) {
77
- this.broadcast({ type: 'jobs', sessionId: owner.id, jobs: this.jobsFor(owner) });
78
- return;
79
- }
80
- for (const session of this.ctx.sessions.list()) {
81
- this.broadcast({
82
- type: 'jobs',
83
- sessionId: session.id,
84
- jobs: this.jobsFor(this.ctx.agents.get(session.id)),
85
- });
86
- }
87
- }
88
- jobsFor(agent) {
89
- const jobs = this.ctx.get('jobs');
90
- return jobs === undefined ? [] : jobs.list(agent).map(jobView);
91
- }
92
61
  broadcast(frame) {
93
62
  for (const stream of this.streams)
94
63
  stream.push(frame);
@@ -135,15 +104,4 @@ class ControlQueue {
135
104
  }
136
105
  }
137
106
  }
138
- function jobView(job) {
139
- return {
140
- id: job.id,
141
- kind: job.kind,
142
- label: job.label,
143
- status: job.status,
144
- ...(job.detail === undefined ? {} : { detail: job.detail }),
145
- startedAt: job.startedAt,
146
- ...(job.finishedAt === undefined ? {} : { finishedAt: job.finishedAt }),
147
- };
148
- }
149
107
  //# sourceMappingURL=control.js.map
@@ -115,7 +115,7 @@ export class SessionHistoryController {
115
115
  if (throughSeq >= 0 && sourceLog[throughSeq]?.seq !== throughSeq) {
116
116
  throw new RemoteError('gateway/internal', `session log does not contain through seq ${String(throughSeq)}`, {});
117
117
  }
118
- const page = paginate(sourceLog, beforeSeq, request.maxMessages ?? DEFAULT_MAX_MESSAGES, throughSeq);
118
+ const page = paginate(sourceLog, beforeSeq, request.maxMessages ?? DEFAULT_MAX_MESSAGES, throughSeq, request.turnWindow);
119
119
  const records = pageRecords(page.events);
120
120
  return {
121
121
  records,
@@ -137,7 +137,7 @@ export class SessionHistoryController {
137
137
  * @returns a complete opening snapshot followed by gap-free durable events and opted-in assistant frames.
138
138
  */
139
139
  async *follow(request, signal) {
140
- validateFollowRequest(request);
140
+ validateHistoryWindow(request);
141
141
  const { address } = request;
142
142
  const target = addressId(address);
143
143
  const buffered = new Deque();
@@ -198,7 +198,7 @@ export class SessionHistoryController {
198
198
  signal.throwIfAborted();
199
199
  const cursor = source.cursor;
200
200
  snapshotCursor = cursor;
201
- const page = paginate(events, undefined, request.maxMessages ?? DEFAULT_MAX_MESSAGES);
201
+ const page = paginate(events, undefined, request.maxMessages ?? DEFAULT_MAX_MESSAGES, cursor, request.turnWindow);
202
202
  const assistantStream = request.assistantStream === true
203
203
  ? this.assistantStreams.get(target)?.snapshot() ?? { revision: 0 }
204
204
  : undefined;
@@ -327,16 +327,23 @@ function validatePageRequest(request) {
327
327
  || Object.is(request.beforeSeq, -0))) {
328
328
  throw new RemoteError('gateway/bad-request', 'beforeSeq must be a non-negative safe integer', {});
329
329
  }
330
- if (request.maxMessages !== undefined
331
- && (!Number.isSafeInteger(request.maxMessages) || request.maxMessages <= 0)) {
332
- throw new RemoteError('gateway/bad-request', 'maxMessages must be a positive safe integer', {});
333
- }
330
+ validateHistoryWindow(request);
334
331
  }
335
- function validateFollowRequest(request) {
332
+ function validateHistoryWindow(request) {
336
333
  if (request.maxMessages !== undefined
337
334
  && (!Number.isSafeInteger(request.maxMessages) || request.maxMessages <= 0)) {
338
335
  throw new RemoteError('gateway/bad-request', 'maxMessages must be a positive safe integer', {});
339
336
  }
337
+ const window = request.turnWindow;
338
+ if (window !== undefined) {
339
+ if (!Number.isSafeInteger(window.minMessages) || window.minMessages <= 0
340
+ || window.minMessages > (request.maxMessages ?? DEFAULT_MAX_MESSAGES)) {
341
+ throw new RemoteError('gateway/bad-request', 'turnWindow.minMessages must be a positive safe integer no greater than maxMessages', {});
342
+ }
343
+ if (!Number.isSafeInteger(window.minTurns) || window.minTurns <= 0) {
344
+ throw new RemoteError('gateway/bad-request', 'turnWindow.minTurns must be a positive safe integer', {});
345
+ }
346
+ }
340
347
  }
341
348
  function addressId(address) {
342
349
  return address.kind === 'session' ? address.sessionId : address.childSessionId;
@@ -370,7 +377,7 @@ function validateAddress(address, header, inheritedEventCount, projections) {
370
377
  reason: 'unsupported',
371
378
  });
372
379
  }
373
- if (identity.mode !== address.mode) {
380
+ if (address.mode !== 'unknown' && identity.mode !== address.mode) {
374
381
  throw new RemoteError('subagent/unauthorized', 'subagent mode does not match the supplied address', {
375
382
  childSessionId: address.childSessionId,
376
383
  });
@@ -385,12 +392,20 @@ function rejectNotFound(address) {
385
392
  childSessionId: address.childSessionId,
386
393
  });
387
394
  }
388
- function paginate(events, beforeSeq, maxMessages, throughSeq = events.at(-1)?.seq ?? -1) {
395
+ function paginate(events, beforeSeq, maxMessages, throughSeq, turnWindow) {
389
396
  const end = SessionLogOffset(Math.min(throughSeq + 1, beforeSeq ?? throughSeq + 1));
390
397
  let count = 0;
398
+ let turns = 0;
391
399
  let cut = SessionLogOffset(0);
392
400
  for (let index = end - 1; index >= 0; index--) {
393
401
  const event = events[index];
402
+ if (turnWindow !== undefined && event.type === 'turn/start') {
403
+ turns++;
404
+ if (count >= turnWindow.minMessages && turns >= turnWindow.minTurns) {
405
+ cut = SessionLogOffset(index);
406
+ break;
407
+ }
408
+ }
394
409
  if (!MESSAGE_TYPES.has(event.type) || !isAppendSurfaceEvent(event))
395
410
  continue;
396
411
  count++;
@@ -1,12 +1,13 @@
1
1
  /** Session Remote owner: cold reads, explicit Agent commands, and live control state. */
2
2
  import { Context } from '@deepseek-ai/cordis';
3
3
  import z from '@deepseek-ai/schemastery';
4
+ import { nativeFileApplications, openNativeFileApplication } from '@deepseek-ai/dsh-native-command';
4
5
  import type { SessionId } from '@deepseek-ai/dsh-session';
5
6
  import type { SessionInspection } from '@deepseek-ai/dsh-session-persistence';
6
7
  import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
7
8
  import { type ApiSessionAgentResult } from './agent.ts';
8
9
  import { buildModelCatalog } from './catalog.ts';
9
- import type { ModelCatalog, SessionAttachmentRequest, SessionAttachmentValue, SessionCancelRequest, SessionCancelValue, SessionControlFrame, SessionCreateRequest, SessionCreateValue, SessionFollowFrame, SessionFollowRequest, SessionForkRequest, SessionForkValue, SessionListRequest, SessionListValue, SessionOpenWorkspacePathRequest, SessionOpenWorkspacePathValue, SessionPage, SessionPageRequest, SessionPromptRequest, SessionPromptValue, SessionRenameRequest, SessionRenameValue, SessionSearchRequest, SessionSearchValue, SessionSelectModelRequest, SessionSelectModelValue, SessionUpdateQueueRequest, SessionUpdateQueueValue } from './types.ts';
10
+ import type { ModelCatalog, SessionWorkspacePathApplication, SessionAttachmentRequest, SessionAttachmentValue, SessionCancelRequest, SessionCancelValue, SessionControlFrame, SessionCreateRequest, SessionCreateValue, SessionFollowFrame, SessionFollowRequest, SessionForkRequest, SessionForkValue, SessionListRequest, SessionListValue, SessionOpenWorkspacePathRequest, SessionOpenWorkspacePathValue, SessionPage, SessionPageRequest, SessionPromptRequest, SessionPromptValue, SessionRenameRequest, SessionRenameValue, SessionSearchRequest, SessionSearchValue, SessionSelectModelRequest, SessionSelectModelValue, SessionProjectionsRequest, SessionProjectionsValue, SessionUpdateQueueRequest, SessionUpdateQueueValue } from './types.ts';
10
11
  export type * from './types.ts';
11
12
  export { ApiSessionNotFound } from './agent.ts';
12
13
  export { SessionFileReferences } from './file-references.ts';
@@ -26,6 +27,10 @@ export interface Config {
26
27
  export interface SessionControllerInternals {
27
28
  /** Native default-application handoff. */
28
29
  readonly openPath?: (path: string, signal: AbortSignal) => Promise<void>;
30
+ /** Native file-association query. */
31
+ readonly fileApplications?: typeof nativeFileApplications;
32
+ /** Explicit registered-application handoff. */
33
+ readonly openFileApplication?: typeof openNativeFileApplication;
29
34
  /** Native file-manager handoff. */
30
35
  readonly revealPath?: (path: string, signal: AbortSignal) => Promise<void>;
31
36
  /** Native handoff availability probe. */
@@ -41,6 +46,8 @@ export declare class SessionController extends TypertRemoteService {
41
46
  private readonly history;
42
47
  private readonly listState;
43
48
  private readonly openPath;
49
+ private readonly fileApplications;
50
+ private readonly openFileApplication;
44
51
  private readonly revealPath;
45
52
  private readonly canOpenPath;
46
53
  private readonly promotions;
@@ -110,13 +117,24 @@ export declare class SessionController extends TypertRemoteService {
110
117
  fileManager: 'finder' | 'explorer' | 'directory' | null;
111
118
  };
112
119
  /**
113
- * Open one path prepared by a Session-aware caller on the Host desktop.
120
+ * Verify one path through the composed filesystem and open it on the Host desktop.
114
121
  * @param request - path after best-effort Session workspace resolution.
115
122
  * @param signal - caller lifetime; abort terminates the native command.
116
123
  * @returns confirmation after the native opener accepts the path.
117
- * @throws RemoteError when the request is invalid, cancelled, or the opener fails.
124
+ * @throws RemoteError when the request is invalid, has no verified Host mapping, is cancelled, or the opener fails.
118
125
  */
119
126
  openWorkspacePath(request: SessionOpenWorkspacePathRequest, signal: AbortSignal): Promise<SessionOpenWorkspacePathValue>;
127
+ /**
128
+ * Query current file handlers on the serving desktop without activating an Agent.
129
+ * @param request - file path in Host filesystem syntax.
130
+ * @param signal - caller lifetime, propagated to filesystem and desktop queries.
131
+ * @returns OS application names, icons, and default selection; empty when desktop opening is unavailable.
132
+ * @throws RemoteError when the path is invalid, the query is cancelled, or native discovery fails.
133
+ */
134
+ workspacePathApplications(request: {
135
+ readonly path: string;
136
+ }, signal: AbortSignal): Promise<readonly SessionWorkspacePathApplication[]>;
137
+ private verifyDesktopPath;
120
138
  /**
121
139
  * Rename one Session after explicitly resuming it.
122
140
  * @param request - Session identity and proposed title.
@@ -124,8 +142,10 @@ export declare class SessionController extends TypertRemoteService {
124
142
  */
125
143
  rename(request: SessionRenameRequest): Promise<SessionRenameValue>;
126
144
  /**
127
- * Fork one cold-readable completed-turn prefix into a new Session.
128
- * @param request - source Session and optional event anchor.
145
+ * Fork one cold-readable exact event prefix into a new Session. An omitted
146
+ * boundary selects the latest completed-turn prefix; an open cut receives
147
+ * synthetic fork closers.
148
+ * @param request - source Session and optional exact inclusive event boundary.
129
149
  * @returns the new Session identity.
130
150
  */
131
151
  fork(request: SessionForkRequest): Promise<SessionForkValue>;
@@ -169,6 +189,13 @@ export declare class SessionController extends TypertRemoteService {
169
189
  * frames and optional cursorless assistant-stream frames.
170
190
  */
171
191
  follow(request: SessionFollowRequest, signal: AbortSignal): AsyncIterable<SessionFollowFrame>;
192
+ /**
193
+ * Read all registered projections without activating an Agent.
194
+ * @param request - Session whose current values are required.
195
+ * @param signal - cancellation for the Session observation.
196
+ * @returns complete baseline, or null when the Session does not exist.
197
+ */
198
+ projections(request: SessionProjectionsRequest, signal: AbortSignal): Promise<SessionProjectionsValue>;
172
199
  /**
173
200
  * Stream a complete live-control baseline followed by replacement frames.
174
201
  * @param signal - cancellation owned by the Remote stream carrier.
@@ -86,9 +86,11 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
86
86
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
87
87
  });
88
88
  import { hostname } from 'node:os';
89
+ import { resolve } from 'node:path';
89
90
  import z from '@deepseek-ai/schemastery';
90
91
  import { errorChain } from '@deepseek-ai/dsh-llm';
91
- import { canOpenNativePath, nativeFileManager, openNativePath, revealNativePath } from '@deepseek-ai/dsh-native-command';
92
+ import { canOpenNativePath, nativeFileManager, nativeFileApplications, openNativeFileApplication, openNativeAssociatedPath, revealNativePath } from '@deepseek-ai/dsh-native-command';
93
+ import { SessionQueryError } from '@deepseek-ai/dsh-session-query';
92
94
  import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
93
95
  import { ApiSessionAgentController, inspectApiSession, } from "./agent.js";
94
96
  import { SessionCommandController } from "./commands.js";
@@ -100,6 +102,7 @@ import { buildModelCatalog } from "./catalog.js";
100
102
  import { installModelSelectionProjection } from "./model-selection-projection.js";
101
103
  import { SessionSkillCatalog } from "./skill-catalog.js";
102
104
  import { SessionMediaReferences } from "./media-references.js";
105
+ import { ArchivedSessionGate } from "./archived-session-gate.js";
103
106
  export { ApiSessionNotFound } from "./agent.js";
104
107
  export { SessionFileReferences } from "./file-references.js";
105
108
  export { SessionSkillCatalog } from "./skill-catalog.js";
@@ -114,6 +117,7 @@ let SessionController = (() => {
114
117
  let _modelCatalog_decorators;
115
118
  let _canOpenWorkspacePath_decorators;
116
119
  let _openWorkspacePath_decorators;
120
+ let _workspacePathApplications_decorators;
117
121
  let _rename_decorators;
118
122
  let _fork_decorators;
119
123
  let _prompt_decorators;
@@ -122,6 +126,7 @@ let SessionController = (() => {
122
126
  let _cancel_decorators;
123
127
  let _page_decorators;
124
128
  let _follow_decorators;
129
+ let _projections_decorators;
125
130
  let _control_decorators;
126
131
  return class SessionController extends _classSuper {
127
132
  static {
@@ -133,6 +138,7 @@ let SessionController = (() => {
133
138
  _modelCatalog_decorators = [Remote('modelCatalog')];
134
139
  _canOpenWorkspacePath_decorators = [Remote];
135
140
  _openWorkspacePath_decorators = [Remote('openWorkspacePath')];
141
+ _workspacePathApplications_decorators = [Remote('workspacePathApplications')];
136
142
  _rename_decorators = [Remote('rename')];
137
143
  _fork_decorators = [Remote('fork')];
138
144
  _prompt_decorators = [Remote('prompt')];
@@ -141,6 +147,7 @@ let SessionController = (() => {
141
147
  _cancel_decorators = [Remote('cancel')];
142
148
  _page_decorators = [Remote('page')];
143
149
  _follow_decorators = [Remote({ mode: 'stream' })];
150
+ _projections_decorators = [Remote('projections')];
144
151
  _control_decorators = [Remote({ mode: 'stream' })];
145
152
  __esDecorate(this, null, _list_decorators, { kind: "method", name: "list", static: false, private: false, access: { has: obj => "list" in obj, get: obj => obj.list }, metadata: _metadata }, null, _instanceExtraInitializers);
146
153
  __esDecorate(this, null, _search_decorators, { kind: "method", name: "search", static: false, private: false, access: { has: obj => "search" in obj, get: obj => obj.search }, metadata: _metadata }, null, _instanceExtraInitializers);
@@ -149,6 +156,7 @@ let SessionController = (() => {
149
156
  __esDecorate(this, null, _modelCatalog_decorators, { kind: "method", name: "modelCatalog", static: false, private: false, access: { has: obj => "modelCatalog" in obj, get: obj => obj.modelCatalog }, metadata: _metadata }, null, _instanceExtraInitializers);
150
157
  __esDecorate(this, null, _canOpenWorkspacePath_decorators, { kind: "method", name: "canOpenWorkspacePath", static: false, private: false, access: { has: obj => "canOpenWorkspacePath" in obj, get: obj => obj.canOpenWorkspacePath }, metadata: _metadata }, null, _instanceExtraInitializers);
151
158
  __esDecorate(this, null, _openWorkspacePath_decorators, { kind: "method", name: "openWorkspacePath", static: false, private: false, access: { has: obj => "openWorkspacePath" in obj, get: obj => obj.openWorkspacePath }, metadata: _metadata }, null, _instanceExtraInitializers);
159
+ __esDecorate(this, null, _workspacePathApplications_decorators, { kind: "method", name: "workspacePathApplications", static: false, private: false, access: { has: obj => "workspacePathApplications" in obj, get: obj => obj.workspacePathApplications }, metadata: _metadata }, null, _instanceExtraInitializers);
152
160
  __esDecorate(this, null, _rename_decorators, { kind: "method", name: "rename", static: false, private: false, access: { has: obj => "rename" in obj, get: obj => obj.rename }, metadata: _metadata }, null, _instanceExtraInitializers);
153
161
  __esDecorate(this, null, _fork_decorators, { kind: "method", name: "fork", static: false, private: false, access: { has: obj => "fork" in obj, get: obj => obj.fork }, metadata: _metadata }, null, _instanceExtraInitializers);
154
162
  __esDecorate(this, null, _prompt_decorators, { kind: "method", name: "prompt", static: false, private: false, access: { has: obj => "prompt" in obj, get: obj => obj.prompt }, metadata: _metadata }, null, _instanceExtraInitializers);
@@ -157,6 +165,7 @@ let SessionController = (() => {
157
165
  __esDecorate(this, null, _cancel_decorators, { kind: "method", name: "cancel", static: false, private: false, access: { has: obj => "cancel" in obj, get: obj => obj.cancel }, metadata: _metadata }, null, _instanceExtraInitializers);
158
166
  __esDecorate(this, null, _page_decorators, { kind: "method", name: "page", static: false, private: false, access: { has: obj => "page" in obj, get: obj => obj.page }, metadata: _metadata }, null, _instanceExtraInitializers);
159
167
  __esDecorate(this, null, _follow_decorators, { kind: "method", name: "follow", static: false, private: false, access: { has: obj => "follow" in obj, get: obj => obj.follow }, metadata: _metadata }, null, _instanceExtraInitializers);
168
+ __esDecorate(this, null, _projections_decorators, { kind: "method", name: "projections", static: false, private: false, access: { has: obj => "projections" in obj, get: obj => obj.projections }, metadata: _metadata }, null, _instanceExtraInitializers);
160
169
  __esDecorate(this, null, _control_decorators, { kind: "method", name: "control", static: false, private: false, access: { has: obj => "control" in obj, get: obj => obj.control }, metadata: _metadata }, null, _instanceExtraInitializers);
161
170
  if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
162
171
  }
@@ -165,6 +174,7 @@ let SessionController = (() => {
165
174
  'agents',
166
175
  'attachments',
167
176
  'fileUploads',
177
+ 'fs',
168
178
  'llm',
169
179
  'sessions',
170
180
  'sessionProjections',
@@ -181,6 +191,8 @@ let SessionController = (() => {
181
191
  history;
182
192
  listState;
183
193
  openPath;
194
+ fileApplications;
195
+ openFileApplication;
184
196
  revealPath;
185
197
  canOpenPath;
186
198
  promotions = new Set();
@@ -208,19 +220,32 @@ let SessionController = (() => {
208
220
  }, 'session-controller.promotions');
209
221
  this.history = new SessionHistoryController(ctx, (observation) => { this.promote(observation); });
210
222
  this.listState = new ApiSessionList(ctx);
211
- this.openPath = internals.openPath ?? openNativePath;
223
+ this.fileApplications = internals.fileApplications ?? nativeFileApplications;
224
+ this.openFileApplication = internals.openFileApplication ?? openNativeFileApplication;
225
+ this.openPath = internals.openPath ?? openNativeAssociatedPath;
212
226
  this.revealPath = internals.revealPath ?? revealNativePath;
213
227
  this.canOpenPath = internals.canOpenPath
214
228
  ?? (() => config.nativeOpen ?? (internals.openPath !== undefined || canOpenNativePath()));
215
229
  ctx.plugin(SessionFileReferences);
216
230
  ctx.plugin(SessionMediaReferences);
217
231
  ctx.plugin(SessionSkillCatalog);
232
+ // An archived Session, or a subagent descendant of one, runs no model step
233
+ // until it is restored; what it still runs is stopped by the owners that
234
+ // answer the Workspace registry's archive-admission events.
235
+ ctx.plugin(ArchivedSessionGate);
218
236
  ctx.on('session/created', (session) => {
219
237
  ctx.emit('api-session/added', this.listState.summaryFor(session));
220
238
  });
221
239
  ctx.on('session/disposed', (session) => {
222
240
  ctx.emit('api-session/removed', session.id);
223
241
  });
242
+ const publishAgentAvailability = ({ agent }) => {
243
+ if (ctx.sessions.get(agent.id) === agent.session) {
244
+ ctx.emit('api-session/added', this.listState.summaryFor(agent.session));
245
+ }
246
+ };
247
+ ctx.on('agent/created', publishAgentAvailability);
248
+ ctx.on('agent/disposed', publishAgentAvailability);
224
249
  ctx.on('agent/status', ({ agent, status }) => {
225
250
  ctx.emit('api-session/status', agent.id, status === 'running');
226
251
  });
@@ -344,29 +369,65 @@ let SessionController = (() => {
344
369
  return { name: hostname(), available: fileManager !== null && this.canOpenPath(), fileManager };
345
370
  }
346
371
  /**
347
- * Open one path prepared by a Session-aware caller on the Host desktop.
372
+ * Verify one path through the composed filesystem and open it on the Host desktop.
348
373
  * @param request - path after best-effort Session workspace resolution.
349
374
  * @param signal - caller lifetime; abort terminates the native command.
350
375
  * @returns confirmation after the native opener accepts the path.
351
- * @throws RemoteError when the request is invalid, cancelled, or the opener fails.
376
+ * @throws RemoteError when the request is invalid, has no verified Host mapping, is cancelled, or the opener fails.
352
377
  */
353
378
  async openWorkspacePath(request, signal) {
354
- if (request.path.length === 0) {
355
- throw new RemoteError('gateway/bad-request', 'session.openWorkspacePath requires a non-empty path', {});
356
- }
357
- signal.throwIfAborted();
358
379
  try {
380
+ const path = await this.verifyDesktopPath(request.path, signal);
359
381
  if (request.action === 'reveal')
360
- await this.revealPath(request.path, signal);
382
+ await this.revealPath(path, signal);
383
+ else if (request.application !== undefined)
384
+ await this.openFileApplication(path, request.application, signal);
361
385
  else
362
- await this.openPath(request.path, signal);
386
+ await this.openPath(path, signal);
363
387
  return { opened: true };
364
388
  }
365
389
  catch (error) {
366
390
  if (signal.aborted)
367
391
  throw new RemoteError('gateway/cancelled', 'path open was aborted', {});
368
- throw new RemoteError('gateway/internal', `path open failed: ${error instanceof Error ? error.message : String(error)}`, {});
392
+ if (error instanceof RemoteError)
393
+ throw error;
394
+ throw new RemoteError('gateway/internal', 'path open failed', {}, { cause: error });
395
+ }
396
+ }
397
+ /**
398
+ * Query current file handlers on the serving desktop without activating an Agent.
399
+ * @param request - file path in Host filesystem syntax.
400
+ * @param signal - caller lifetime, propagated to filesystem and desktop queries.
401
+ * @returns OS application names, icons, and default selection; empty when desktop opening is unavailable.
402
+ * @throws RemoteError when the path is invalid, the query is cancelled, or native discovery fails.
403
+ */
404
+ async workspacePathApplications(request, signal) {
405
+ if (!this.canOpenPath())
406
+ return [];
407
+ try {
408
+ const path = await this.verifyDesktopPath(request.path, signal);
409
+ return await this.fileApplications(path, signal);
369
410
  }
411
+ catch (error) {
412
+ if (signal.aborted)
413
+ throw new RemoteError('gateway/cancelled', 'application query was aborted', {});
414
+ if (error instanceof RemoteError)
415
+ throw error;
416
+ throw new RemoteError('gateway/internal', 'file application query failed', {}, { cause: error });
417
+ }
418
+ }
419
+ async verifyDesktopPath(path, signal) {
420
+ if (path.length === 0)
421
+ throw new RemoteError('gateway/bad-request', 'A non-empty file path is required', {});
422
+ signal.throwIfAborted();
423
+ const hostPath = resolve(path);
424
+ const { fs } = this.ctx;
425
+ const mapped = fs.processPathFromHostPath(hostPath);
426
+ if (mapped === undefined || fs.processPath(await fs.resolve(mapped, { signal })) !== hostPath) {
427
+ throw new RemoteError('gateway/bad-request', 'Path has no verified Host path', {});
428
+ }
429
+ signal.throwIfAborted();
430
+ return hostPath;
370
431
  }
371
432
  /**
372
433
  * Rename one Session after explicitly resuming it.
@@ -377,8 +438,10 @@ let SessionController = (() => {
377
438
  return this.commands.rename(request);
378
439
  }
379
440
  /**
380
- * Fork one cold-readable completed-turn prefix into a new Session.
381
- * @param request - source Session and optional event anchor.
441
+ * Fork one cold-readable exact event prefix into a new Session. An omitted
442
+ * boundary selects the latest completed-turn prefix; an open cut receives
443
+ * synthetic fork closers.
444
+ * @param request - source Session and optional exact inclusive event boundary.
382
445
  * @returns the new Session identity.
383
446
  */
384
447
  fork(request) {
@@ -437,6 +500,47 @@ let SessionController = (() => {
437
500
  follow(request, signal) {
438
501
  return this.history.follow(request, signal);
439
502
  }
503
+ /**
504
+ * Read all registered projections without activating an Agent.
505
+ * @param request - Session whose current values are required.
506
+ * @param signal - cancellation for the Session observation.
507
+ * @returns complete baseline, or null when the Session does not exist.
508
+ */
509
+ async projections(request, signal) {
510
+ const { sessionId } = request;
511
+ if (sessionId.length === 0) {
512
+ throw new RemoteError('gateway/bad-request', 'sessionId must not be empty', {});
513
+ }
514
+ try {
515
+ const env_2 = { stack: [], error: void 0, hasError: false };
516
+ try {
517
+ const observation = __addDisposableResource(env_2, await this.ctx.sessionQuery.observeSession(sessionId, { signal }), false);
518
+ const projections = observation.projections;
519
+ if (projections === undefined) {
520
+ throw new RemoteError('session/projections-unavailable', 'Session projections are unavailable', {});
521
+ }
522
+ return { asOfSeq: projections.asOfSeq, values: projections.values };
523
+ }
524
+ catch (e_2) {
525
+ env_2.error = e_2;
526
+ env_2.hasError = true;
527
+ }
528
+ finally {
529
+ __disposeResources(env_2);
530
+ }
531
+ }
532
+ catch (error) {
533
+ if (error instanceof SessionQueryError && error.code === 'SESSION_QUERY_SESSION_NOT_FOUND')
534
+ return null;
535
+ if (signal.aborted
536
+ || (error instanceof SessionQueryError && error.code === 'SESSION_QUERY_ABORTED')) {
537
+ throw new RemoteError('gateway/cancelled', 'Session projection read was cancelled', {}, { cause: error });
538
+ }
539
+ if (error instanceof RemoteError)
540
+ throw error;
541
+ throw new RemoteError('gateway/internal', 'Session projection read failed', {}, { cause: error });
542
+ }
543
+ }
440
544
  /**
441
545
  * Stream a complete live-control baseline followed by replacement frames.
442
546
  * @param signal - cancellation owned by the Remote stream carrier.
package/lib/types/list.js CHANGED
@@ -1,5 +1,4 @@
1
1
  /** Cold-safe Session list and search projection. */
2
- import { SessionLogOffset } from '@deepseek-ai/dsh-session';
3
2
  import { SessionQueryError } from '@deepseek-ai/dsh-session-query';
4
3
  import { RemoteError } from '@deepseek-ai/dsh-typert-protocol';
5
4
  import { z } from 'zod';
@@ -90,6 +89,7 @@ export class ApiSessionList {
90
89
  return {
91
90
  sessionId: session.id,
92
91
  updatedAt: updatedAt(session.header, metadata),
92
+ agentAvailable: this.ctx.agents.get(session.id)?.session === session,
93
93
  running: this.ctx.agents.get(session.id)?.status === 'running',
94
94
  blank: metadata?.blank ?? session.seq === 0,
95
95
  ...listFields(session.header),
@@ -128,6 +128,7 @@ export class ApiSessionList {
128
128
  return {
129
129
  sessionId: header.id,
130
130
  updatedAt: updatedAt(header, metadata),
131
+ agentAvailable: false,
131
132
  running: false,
132
133
  // A large, metadata-less, or inaccessible cache miss remains unknown and visible.
133
134
  blank: metadata?.blank ?? false,
@@ -246,21 +247,16 @@ export class ApiSessionList {
246
247
  }
247
248
  projectionsFor(header, session) {
248
249
  try {
250
+ if (session !== undefined) {
251
+ // The live registry computed the block for this Session: its watermark
252
+ // shares the sequence space of the Session's baselines and frames.
253
+ return hintsOf('sequenced', this.ctx.sessionProjections.cachedSnapshot(session));
254
+ }
255
+ // A cold row reads the persisted cache by header alone; the cache serves
256
+ // seeded and unseeded lifecycles alike because a listing never seeds a
257
+ // fold. The watermark is the stored record's own.
249
258
  const cache = this.ctx.get('sessionProjectionCache');
250
- const block = session === undefined
251
- ? header.isSeeded
252
- ? undefined
253
- : cache?.cachedSnapshot(header, SessionLogOffset(0))
254
- ?? cache?.cachedPredecessorTitle(header, SessionLogOffset(0))
255
- : this.ctx.sessionProjections.cachedSnapshot(session);
256
- return block !== undefined && Object.keys(block.values).length > 0
257
- ? {
258
- asOfSeq: block.asOfSeq,
259
- // Listing hints contain every currently cached wire value but remain
260
- // partial: missing cells and cache rows are never materialized here.
261
- values: block.values,
262
- }
263
- : undefined;
259
+ return hintsOf('cached', cache?.cachedSnapshot(header) ?? cache?.cachedPredecessorTitle(header));
264
260
  }
265
261
  catch (error) {
266
262
  this.ctx.logger.warn(`api-session.list: projection column for "${header.id}" failed; serving the row without it: ${String(error)}`);
@@ -268,6 +264,19 @@ export class ApiSessionList {
268
264
  }
269
265
  }
270
266
  }
267
+ /**
268
+ * Wrap one projection block as Session-list hints of the named sequence space.
269
+ * @param kind - which sequence space the block's watermark belongs to.
270
+ * @param block - the block, or `undefined` when no source served one.
271
+ * @returns the hints, or `undefined` when the block is absent or carries no value.
272
+ */
273
+ function hintsOf(kind, block) {
274
+ if (block === undefined || Object.keys(block.values).length === 0)
275
+ return undefined;
276
+ // Listing hints contain every wire value the source currently holds but
277
+ // remain partial: missing cells and cache rows are never materialized here.
278
+ return { kind, asOfSeq: block.asOfSeq, values: block.values };
279
+ }
271
280
  function normalizeSearchQuery(query) {
272
281
  const normalized = query.trim();
273
282
  if (normalized.length === 0) {