@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
package/lib/index.js CHANGED
@@ -1,14 +1,16 @@
1
1
  import { hostname } from "node:os";
2
+ import { isAbsolute, resolve } from "node:path";
2
3
  import z from "@deepseek-ai/schemastery";
3
4
  import { AssistantStreamAccumulator, ReasoningEffortId, assistantStreamChunks, createUserMessage, errorChain, freezeMessage } from "@deepseek-ai/dsh-llm";
4
- import { canOpenNativePath, nativeFileManager, openNativePath, revealNativePath } from "@deepseek-ai/dsh-native-command";
5
+ import { canOpenNativePath, nativeFileApplications, nativeFileManager, openNativeAssociatedPath, openNativeFileApplication, revealNativePath } from "@deepseek-ai/dsh-native-command";
6
+ import { SessionQueryError } from "@deepseek-ai/dsh-session-query";
5
7
  import { Remote, RemoteError, TypertRemoteService, remoteErrorOf } from "@deepseek-ai/dsh-typert-protocol";
6
8
  import { mkdir } from "node:fs/promises";
7
9
  import { installModelSelection } from "@deepseek-ai/dsh-agent";
8
- import { SessionQueryError } from "@deepseek-ai/dsh-session-query";
9
10
  import { randomUUID } from "node:crypto";
10
11
  import { brandString } from "@deepseek-ai/dsh-brand";
11
12
  import { AttachmentError } from "@deepseek-ai/dsh-attachment";
13
+ import { buildForkSeed } from "@deepseek-ai/dsh-session/fork";
12
14
  import { SessionLogOffset, SessionSeq, isAppendSurfaceEvent } from "@deepseek-ai/dsh-session";
13
15
  import { SessionTitleInvalidError } from "@deepseek-ai/dsh-session-title";
14
16
  import { canonicalClientTimeZone } from "@deepseek-ai/dsh-util-time";
@@ -16,7 +18,6 @@ import { assertNever } from "@deepseek-ai/dsh-util-values";
16
18
  import { Deque } from "@deepseek-ai/dsh-deque";
17
19
  import { z as z$1 } from "zod";
18
20
  import { isUserInvocable } from "@deepseek-ai/dsh-skill";
19
- import { isAbsolute } from "node:path";
20
21
  import { FsError } from "@deepseek-ai/dsh-fs";
21
22
  import mime from "mime-types";
22
23
  //#region lib/types/agent.js
@@ -550,6 +551,20 @@ var __disposeResources$3 = (function(SuppressedError) {
550
551
  function hasPromptContent(content) {
551
552
  return content.some((part) => part.type !== "text" || part.text.trim().length > 0);
552
553
  }
554
+ /**
555
+ * Resolve the omitted-`atSeq` default to the latest completed-turn prefix,
556
+ * including standalone events before the next turn begins.
557
+ */
558
+ function latestCompletedPrefixBoundary(events) {
559
+ const lastTurnEnd = events.findLast((event) => event.type === "turn/end");
560
+ if (lastTurnEnd === void 0) return void 0;
561
+ let boundary = lastTurnEnd.seq;
562
+ for (const next of events.slice(boundary + 1)) {
563
+ if (next.type === "turn/start" || next.type === "user/message" && next.surfaceOp === "append" || next.type === "agent/inbox/spliced") break;
564
+ boundary = next.seq;
565
+ }
566
+ return boundary;
567
+ }
553
568
  /** Implements Session business commands delegated by the Session Controller Remote service. */
554
569
  var SessionCommandController = class {
555
570
  ctx;
@@ -655,8 +670,10 @@ var SessionCommandController = class {
655
670
  }
656
671
  }
657
672
  /**
658
- * Create a new ordinary Session from one completed-turn prefix.
659
- * @param request - source Session and optional event anchor.
673
+ * Create a new ordinary Session from an exact event prefix. An explicit
674
+ * `atSeq` is the inclusive cut; an omitted value selects the latest
675
+ * completed-turn prefix. An open cut receives synthetic fork closers.
676
+ * @param request - source Session and optional exact event boundary.
660
677
  * @returns the new Session identity.
661
678
  */
662
679
  async fork(request) {
@@ -680,10 +697,9 @@ var SessionCommandController = class {
680
697
  throw new RemoteError("gateway/internal", `fork source unavailable for session "${request.sessionId}": ${String(error)}`, {});
681
698
  }
682
699
  const source = __addDisposableResource$3(env_1, observed, false);
683
- const lastSeq = source.events.at(-1)?.seq ?? -1;
684
- const boundary = (atSeq === void 0 ? void 0 : source.events.find((event) => event.type === "turn/end" && event.seq >= atSeq)) ?? (atSeq === void 0 || atSeq > lastSeq ? source.events.findLast((event) => event.type === "turn/end") : void 0);
685
- if (boundary === void 0) throw new RemoteError("session/fork-unavailable", atSeq !== void 0 && atSeq <= lastSeq ? `session "${request.sessionId}" has not completed the turn containing event ${String(atSeq)}` : `session "${request.sessionId}" has no completed turn to fork from`, { sessionId: request.sessionId });
686
- const cut = SessionLogOffset(boundary.seq + 1);
700
+ const boundary = atSeq ?? latestCompletedPrefixBoundary(source.events);
701
+ if (boundary === void 0 || source.events[boundary]?.seq !== boundary) throw new RemoteError("session/fork-unavailable", request.atSeq === void 0 ? `session "${request.sessionId}" has no completed turn to fork from` : `event ${String(request.atSeq)} does not exist in session "${request.sessionId}" (last seq: ${String(source.events.at(-1)?.seq ?? "none")})`, { sessionId: request.sessionId });
702
+ const seed = buildForkSeed(source.events, boundary);
687
703
  let workspace;
688
704
  try {
689
705
  workspace = await this.forkWorkspace(source.header);
@@ -696,8 +712,8 @@ var SessionCommandController = class {
696
712
  const { provider, model } = this.ctx.agentDefaultModel.currentSelection();
697
713
  await this.ctx.agents.create({
698
714
  sessionId: childId,
699
- seed: source.events.slice(0, cut),
700
- inheritedEventCount: cut,
715
+ seed,
716
+ inheritedEventCount: SessionLogOffset(boundary + 1),
701
717
  meta: {
702
718
  ...source.header.cwd === void 0 ? {} : { cwd: source.header.cwd },
703
719
  parentSession: source.header.id,
@@ -891,6 +907,7 @@ var SessionCommandController = class {
891
907
  }
892
908
  rejectCreation(sessionId, error) {
893
909
  if (remoteErrorOf(error) !== void 0) throw error;
910
+ if (error instanceof Error && error.name === "SessionAlreadyOwnedError") throw new RemoteError("session/writer-held", error.message, { sessionId });
894
911
  if (error instanceof ApiSessionPresetConflict) throw new RemoteError("agent-preset/conflict", error.message, {
895
912
  sessionId: error.sessionId,
896
913
  requestedPreset: error.requestedPreset,
@@ -966,23 +983,39 @@ function imageBlockIn(content, match) {
966
983
  const ref = block.attachment;
967
984
  if (match(ref)) return ref;
968
985
  }
969
- if (block.type === "tool-result") {
970
- const nested = imageBlockIn(block.content, match);
971
- if (nested !== void 0) return nested;
972
- }
973
986
  }
974
987
  }
988
+ /** Read only first-party declared content fields; unknown event payloads stay opaque. */
975
989
  function imageInEvent(event, match) {
976
990
  const data = event.data;
977
- const direct = imageBlockIn(data.content, match);
978
- if (direct !== void 0) return direct;
979
- const message = imageBlockIn(data.message?.content, match);
980
- if (message !== void 0) return message;
981
- for (const inserted of data.inserted ?? []) {
982
- const found = imageBlockIn(inserted.content, match);
983
- if (found !== void 0) return found;
991
+ switch (event.type) {
992
+ case "user/message":
993
+ case "tool/ptc-dispatch": return imageBlockIn(data.content, match);
994
+ case "system/message":
995
+ case "developer/message":
996
+ case "tool/result":
997
+ case "team/message/queued": return imageBlockIn(data.message?.content, match);
998
+ case "agent/inbox/spliced": {
999
+ const messages = data.inserted;
1000
+ if (!Array.isArray(messages)) return void 0;
1001
+ for (const message of messages) {
1002
+ if (typeof message !== "object" || message === null || Array.isArray(message)) continue;
1003
+ const found = imageBlockIn(message.content, match);
1004
+ if (found !== void 0) return found;
1005
+ }
1006
+ return;
1007
+ }
1008
+ case "compaction/summary": return imageBlockIn(data.summary, match) ?? imageBlockIn(data.rawOutput, match);
1009
+ case "assistant/message": {
1010
+ const found = imageBlockIn(data.message?.content, match);
1011
+ if (found !== void 0) return found;
1012
+ break;
1013
+ }
1014
+ case "assistant/attempt": break;
1015
+ default: return;
984
1016
  }
985
- if (event.type === "assistant/message" || event.type === "assistant/attempt") for (const chunk of assistantStreamChunks(event.data.stream, "block-end")) {
1017
+ const assistant = event;
1018
+ for (const chunk of assistantStreamChunks(assistant.data.stream, "block-end")) {
986
1019
  const found = imageBlockIn([chunk.block], match);
987
1020
  if (found !== void 0) return found;
988
1021
  }
@@ -998,12 +1031,12 @@ function routeServed(ctx, provider) {
998
1031
  }
999
1032
  //#endregion
1000
1033
  //#region lib/types/control.js
1001
- /** Live Session jobs and projection state with reconnect baselines. */
1034
+ /** Live Session projection state with reconnect baselines. */
1002
1035
  /** Owns the Host-wide Session control stream. */
1003
1036
  var SessionControlController = class {
1004
1037
  ctx;
1005
1038
  streams = /* @__PURE__ */ new Set();
1006
- /** @param ctx - Host context carrying live Agent, projection, and jobs services. */
1039
+ /** @param ctx - Host context carrying live Agent and projection services. */
1007
1040
  constructor(ctx) {
1008
1041
  this.ctx = ctx;
1009
1042
  ctx.sessionProjections.onChanged((session, key, value, seq) => {
@@ -1015,19 +1048,6 @@ var SessionControlController = class {
1015
1048
  seq
1016
1049
  });
1017
1050
  });
1018
- ctx.inject(["jobs"], (jobsCtx) => {
1019
- jobsCtx.jobs.onJobsChanged((owner) => {
1020
- this.onJobsChanged(owner);
1021
- });
1022
- });
1023
- ctx.on("session/created", (session) => {
1024
- const jobs = this.jobsFor(this.ctx.agents.get(session.id));
1025
- if (jobs.length > 0) this.broadcast({
1026
- type: "jobs",
1027
- sessionId: session.id,
1028
- jobs
1029
- });
1030
- });
1031
1051
  ctx.effect(() => () => {
1032
1052
  for (const stream of this.streams) stream.end();
1033
1053
  this.streams.clear();
@@ -1055,15 +1075,7 @@ var SessionControlController = class {
1055
1075
  }
1056
1076
  baseline() {
1057
1077
  const sessions = this.ctx.sessions.list();
1058
- const jobs = Object.create(null);
1059
- for (const session of sessions) {
1060
- const agent = this.ctx.agents.get(session.id);
1061
- jobs[session.id] = this.jobsFor(agent);
1062
- }
1063
- return {
1064
- jobs,
1065
- projections: this.projectionBaseline(sessions)
1066
- };
1078
+ return { projections: this.projectionBaseline(sessions) };
1067
1079
  }
1068
1080
  projectionBaseline(sessions) {
1069
1081
  const blocks = Object.create(null);
@@ -1076,25 +1088,6 @@ var SessionControlController = class {
1076
1088
  }
1077
1089
  return blocks;
1078
1090
  }
1079
- onJobsChanged(owner) {
1080
- if (owner !== void 0) {
1081
- this.broadcast({
1082
- type: "jobs",
1083
- sessionId: owner.id,
1084
- jobs: this.jobsFor(owner)
1085
- });
1086
- return;
1087
- }
1088
- for (const session of this.ctx.sessions.list()) this.broadcast({
1089
- type: "jobs",
1090
- sessionId: session.id,
1091
- jobs: this.jobsFor(this.ctx.agents.get(session.id))
1092
- });
1093
- }
1094
- jobsFor(agent) {
1095
- const jobs = this.ctx.get("jobs");
1096
- return jobs === void 0 ? [] : jobs.list(agent).map(jobView);
1097
- }
1098
1091
  broadcast(frame) {
1099
1092
  for (const stream of this.streams) stream.push(frame);
1100
1093
  }
@@ -1140,17 +1133,6 @@ var ControlQueue = class {
1140
1133
  }
1141
1134
  }
1142
1135
  };
1143
- function jobView(job) {
1144
- return {
1145
- id: job.id,
1146
- kind: job.kind,
1147
- label: job.label,
1148
- status: job.status,
1149
- ...job.detail === void 0 ? {} : { detail: job.detail },
1150
- startedAt: job.startedAt,
1151
- ...job.finishedAt === void 0 ? {} : { finishedAt: job.finishedAt }
1152
- };
1153
- }
1154
1136
  //#endregion
1155
1137
  //#region lib/types/assistant-stream.js
1156
1138
  /** Process-local assistant state retained for reconnecting Web followers. */
@@ -1347,7 +1329,7 @@ var SessionHistoryController = class {
1347
1329
  if (throughSeq > sourceCursor) throw new RemoteError("gateway/bad-request", `session page through seq ${String(throughSeq)} is past cursor ${String(sourceCursor)}`, {});
1348
1330
  /* v8 ignore next -- Session and persistence validation guarantee a dense zero-based event prefix. */
1349
1331
  if (throughSeq >= 0 && sourceLog[throughSeq]?.seq !== throughSeq) throw new RemoteError("gateway/internal", `session log does not contain through seq ${String(throughSeq)}`, {});
1350
- const page = paginate(sourceLog, beforeSeq, request.maxMessages ?? DEFAULT_MAX_MESSAGES, throughSeq);
1332
+ const page = paginate(sourceLog, beforeSeq, request.maxMessages ?? DEFAULT_MAX_MESSAGES, throughSeq, request.turnWindow);
1351
1333
  return {
1352
1334
  records: pageRecords(page.events),
1353
1335
  hasMore: page.hasMore
@@ -1366,7 +1348,7 @@ var SessionHistoryController = class {
1366
1348
  * @returns a complete opening snapshot followed by gap-free durable events and opted-in assistant frames.
1367
1349
  */
1368
1350
  async *follow(request, signal) {
1369
- validateFollowRequest(request);
1351
+ validateHistoryWindow(request);
1370
1352
  const { address } = request;
1371
1353
  const target = addressId(address);
1372
1354
  const buffered = new Deque();
@@ -1426,7 +1408,7 @@ var SessionHistoryController = class {
1426
1408
  signal.throwIfAborted();
1427
1409
  const cursor = source.cursor;
1428
1410
  snapshotCursor = cursor;
1429
- const page = paginate(events, void 0, request.maxMessages ?? DEFAULT_MAX_MESSAGES);
1411
+ const page = paginate(events, void 0, request.maxMessages ?? DEFAULT_MAX_MESSAGES, cursor, request.turnWindow);
1430
1412
  const assistantStream = request.assistantStream === true ? this.assistantStreams.get(target)?.snapshot() ?? { revision: 0 } : void 0;
1431
1413
  const assistantStreamOrdinalCut = assistantStreamOrdinal;
1432
1414
  yield {
@@ -1533,10 +1515,15 @@ function projectionBlock(snapshot) {
1533
1515
  function validatePageRequest(request) {
1534
1516
  if (!Number.isSafeInteger(request.throughSeq) || request.throughSeq < -1 || Object.is(request.throughSeq, -0)) throw new RemoteError("gateway/bad-request", "throughSeq must be an integer greater than or equal to -1", {});
1535
1517
  if (request.beforeSeq !== void 0 && (!Number.isSafeInteger(request.beforeSeq) || request.beforeSeq < 0 || Object.is(request.beforeSeq, -0))) throw new RemoteError("gateway/bad-request", "beforeSeq must be a non-negative safe integer", {});
1536
- if (request.maxMessages !== void 0 && (!Number.isSafeInteger(request.maxMessages) || request.maxMessages <= 0)) throw new RemoteError("gateway/bad-request", "maxMessages must be a positive safe integer", {});
1518
+ validateHistoryWindow(request);
1537
1519
  }
1538
- function validateFollowRequest(request) {
1520
+ function validateHistoryWindow(request) {
1539
1521
  if (request.maxMessages !== void 0 && (!Number.isSafeInteger(request.maxMessages) || request.maxMessages <= 0)) throw new RemoteError("gateway/bad-request", "maxMessages must be a positive safe integer", {});
1522
+ const window = request.turnWindow;
1523
+ if (window !== void 0) {
1524
+ if (!Number.isSafeInteger(window.minMessages) || window.minMessages <= 0 || window.minMessages > (request.maxMessages ?? DEFAULT_MAX_MESSAGES)) throw new RemoteError("gateway/bad-request", "turnWindow.minMessages must be a positive safe integer no greater than maxMessages", {});
1525
+ if (!Number.isSafeInteger(window.minTurns) || window.minTurns <= 0) throw new RemoteError("gateway/bad-request", "turnWindow.minTurns must be a positive safe integer", {});
1526
+ }
1540
1527
  }
1541
1528
  function addressId(address) {
1542
1529
  return address.kind === "session" ? address.sessionId : address.childSessionId;
@@ -1558,7 +1545,7 @@ function validateAddress(address, header, inheritedEventCount, projections) {
1558
1545
  childSessionId: address.childSessionId,
1559
1546
  reason: "unsupported"
1560
1547
  });
1561
- if (identity.mode !== address.mode) throw new RemoteError("subagent/unauthorized", "subagent mode does not match the supplied address", { childSessionId: address.childSessionId });
1548
+ if (address.mode !== "unknown" && identity.mode !== address.mode) throw new RemoteError("subagent/unauthorized", "subagent mode does not match the supplied address", { childSessionId: address.childSessionId });
1562
1549
  }
1563
1550
  function rejectNotFound(address) {
1564
1551
  if (address.kind === "session") throw new RemoteError("session/not-found", `session "${address.sessionId}" not found`, { sessionId: address.sessionId });
@@ -1567,12 +1554,20 @@ function rejectNotFound(address) {
1567
1554
  childSessionId: address.childSessionId
1568
1555
  });
1569
1556
  }
1570
- function paginate(events, beforeSeq, maxMessages, throughSeq = events.at(-1)?.seq ?? -1) {
1557
+ function paginate(events, beforeSeq, maxMessages, throughSeq, turnWindow) {
1571
1558
  const end = SessionLogOffset(Math.min(throughSeq + 1, beforeSeq ?? throughSeq + 1));
1572
1559
  let count = 0;
1560
+ let turns = 0;
1573
1561
  let cut = SessionLogOffset(0);
1574
1562
  for (let index = end - 1; index >= 0; index--) {
1575
1563
  const event = events[index];
1564
+ if (turnWindow !== void 0 && event.type === "turn/start") {
1565
+ turns++;
1566
+ if (count >= turnWindow.minMessages && turns >= turnWindow.minTurns) {
1567
+ cut = SessionLogOffset(index);
1568
+ break;
1569
+ }
1570
+ }
1576
1571
  if (!MESSAGE_TYPES$1.has(event.type) || !isAppendSurfaceEvent(event)) continue;
1577
1572
  count++;
1578
1573
  const sources = event.sourceEventSeqs;
@@ -1783,6 +1778,7 @@ var ApiSessionList = class {
1783
1778
  return {
1784
1779
  sessionId: session.id,
1785
1780
  updatedAt: updatedAt(session.header, metadata),
1781
+ agentAvailable: this.ctx.agents.get(session.id)?.session === session,
1786
1782
  running: this.ctx.agents.get(session.id)?.status === "running",
1787
1783
  blank: metadata?.blank ?? session.seq === 0,
1788
1784
  ...listFields(session.header),
@@ -1819,6 +1815,7 @@ var ApiSessionList = class {
1819
1815
  return {
1820
1816
  sessionId: header.id,
1821
1817
  updatedAt: updatedAt(header, metadata),
1818
+ agentAvailable: false,
1822
1819
  running: false,
1823
1820
  blank: metadata?.blank ?? false,
1824
1821
  ...listFields(header),
@@ -1915,18 +1912,29 @@ var ApiSessionList = class {
1915
1912
  }
1916
1913
  projectionsFor(header, session) {
1917
1914
  try {
1915
+ if (session !== void 0) return hintsOf("sequenced", this.ctx.sessionProjections.cachedSnapshot(session));
1918
1916
  const cache = this.ctx.get("sessionProjectionCache");
1919
- const block = session === void 0 ? header.isSeeded ? void 0 : cache?.cachedSnapshot(header, SessionLogOffset(0)) ?? cache?.cachedPredecessorTitle(header, SessionLogOffset(0)) : this.ctx.sessionProjections.cachedSnapshot(session);
1920
- return block !== void 0 && Object.keys(block.values).length > 0 ? {
1921
- asOfSeq: block.asOfSeq,
1922
- values: block.values
1923
- } : void 0;
1917
+ return hintsOf("cached", cache?.cachedSnapshot(header) ?? cache?.cachedPredecessorTitle(header));
1924
1918
  } catch (error) {
1925
1919
  this.ctx.logger.warn(`api-session.list: projection column for "${header.id}" failed; serving the row without it: ${String(error)}`);
1926
1920
  return;
1927
1921
  }
1928
1922
  }
1929
1923
  };
1924
+ /**
1925
+ * Wrap one projection block as Session-list hints of the named sequence space.
1926
+ * @param kind - which sequence space the block's watermark belongs to.
1927
+ * @param block - the block, or `undefined` when no source served one.
1928
+ * @returns the hints, or `undefined` when the block is absent or carries no value.
1929
+ */
1930
+ function hintsOf(kind, block) {
1931
+ if (block === void 0 || Object.keys(block.values).length === 0) return void 0;
1932
+ return {
1933
+ kind,
1934
+ asOfSeq: block.asOfSeq,
1935
+ values: block.values
1936
+ };
1937
+ }
1930
1938
  function normalizeSearchQuery(query) {
1931
1939
  const normalized = query.trim();
1932
1940
  if (normalized.length === 0) throw new RemoteError("gateway/bad-request", "session search query must not be empty", {});
@@ -2211,59 +2219,71 @@ let SessionSkillCatalog = (() => {
2211
2219
  * @throws RemoteError when the Session cannot be inspected or no registry can serve it.
2212
2220
  */
2213
2221
  async list(request, signal) {
2214
- const { sessionId } = request;
2215
- let cwd;
2216
- let agentPreset;
2222
+ const env_1 = {
2223
+ stack: [],
2224
+ error: void 0,
2225
+ hasError: false
2226
+ };
2217
2227
  try {
2218
- const env_1 = {
2219
- stack: [],
2220
- error: void 0,
2221
- hasError: false
2222
- };
2228
+ const { sessionId } = request;
2229
+ let cwd;
2230
+ let agentPreset;
2223
2231
  try {
2224
- const observation = __addDisposableResource$1(env_1, await this.ctx.sessionQuery.observeSession(sessionId), false);
2225
- if (observation.projections === void 0) throw new Error("skill catalog requires a projected Session observation");
2226
- cwd = observation.header.cwd;
2227
- agentPreset = observation.projections.values.agentPreset ?? void 0;
2228
- } catch (e_1) {
2229
- env_1.error = e_1;
2230
- env_1.hasError = true;
2231
- } finally {
2232
- __disposeResources$1(env_1);
2232
+ const env_2 = {
2233
+ stack: [],
2234
+ error: void 0,
2235
+ hasError: false
2236
+ };
2237
+ try {
2238
+ const observation = __addDisposableResource$1(env_2, await this.ctx.sessionQuery.observeSession(sessionId), false);
2239
+ if (observation.projections === void 0) throw new Error("skill catalog requires a projected Session observation");
2240
+ cwd = observation.header.cwd;
2241
+ agentPreset = observation.projections.values.agentPreset ?? void 0;
2242
+ } catch (e_1) {
2243
+ env_2.error = e_1;
2244
+ env_2.hasError = true;
2245
+ } finally {
2246
+ __disposeResources$1(env_2);
2247
+ }
2248
+ } catch (error) {
2249
+ if (error instanceof SessionQueryError && error.code === "SESSION_QUERY_SESSION_NOT_FOUND") throw new RemoteError("session/not-found", `session "${sessionId}" not found`, { sessionId });
2250
+ throw new RemoteError("gateway/internal", `session "${sessionId}" could not be inspected: ${String(error)}`, {});
2233
2251
  }
2234
- } catch (error) {
2235
- if (error instanceof SessionQueryError && error.code === "SESSION_QUERY_SESSION_NOT_FOUND") throw new RemoteError("session/not-found", `session "${sessionId}" not found`, { sessionId });
2236
- throw new RemoteError("gateway/internal", `session "${sessionId}" could not be inspected: ${String(error)}`, {});
2237
- }
2238
- if (cwd === void 0) throw new RemoteError("gateway/internal", `session "${sessionId}" has no project cwd`, {});
2239
- const live = this.ctx.agents.get(sessionId);
2240
- const presets = this.ctx.get("agentPresets");
2241
- const skillRegistry = (live === void 0 ? void 0 : presets?.serviceFor(live, "skills")) ?? this.ctx.get("skills");
2242
- if (skillRegistry === void 0) throw new RemoteError("gateway/internal", "skill registry is absent: neither this session's agent preset nor the host composition mounts @deepseek-ai/dsh-skill", {});
2243
- const scope = await this.scopeFor(sessionId, agentPreset);
2244
- try {
2245
- return { skills: (await skillRegistry.list({
2246
- cwd,
2247
- scope
2248
- })).filter(isUserInvocable).map((skill) => ({
2249
- name: skill.name,
2250
- ...skill.path === void 0 ? {} : { path: skill.path },
2251
- description: skill.description,
2252
- ...skill.whenToUse === void 0 ? {} : { whenToUse: skill.whenToUse },
2253
- modelInvocable: skill.invocation.modelInvocable
2254
- })) };
2255
- } catch (error) {
2256
- throw new RemoteError("gateway/internal", `skill listing failed: ${String(error)}`, {});
2252
+ if (cwd === void 0) throw new RemoteError("gateway/internal", `session "${sessionId}" has no project cwd`, {});
2253
+ const live = this.ctx.agents.get(sessionId);
2254
+ const presets = this.ctx.get("agentPresets");
2255
+ const skillRegistry = (live === void 0 ? void 0 : presets?.serviceFor(live, "skills")) ?? this.ctx.get("skills");
2256
+ if (skillRegistry === void 0) throw new RemoteError("gateway/internal", "skill registry is absent: neither this session's agent preset nor the host composition mounts @deepseek-ai/dsh-skill", {});
2257
+ const lease = __addDisposableResource$1(env_1, live === void 0 ? await this.scopeFor(agentPreset) : void 0, true);
2258
+ const scope = live ?? lease?.key;
2259
+ try {
2260
+ return { skills: (await skillRegistry.list({
2261
+ cwd,
2262
+ scope
2263
+ })).filter(isUserInvocable).map((skill) => ({
2264
+ name: skill.name,
2265
+ ...skill.path === void 0 ? {} : { path: skill.path },
2266
+ description: skill.description,
2267
+ ...skill.whenToUse === void 0 ? {} : { whenToUse: skill.whenToUse },
2268
+ modelInvocable: skill.invocation.modelInvocable
2269
+ })) };
2270
+ } catch (error) {
2271
+ throw new RemoteError("gateway/internal", `skill listing failed: ${String(error)}`, {});
2272
+ }
2273
+ } catch (e_2) {
2274
+ env_1.error = e_2;
2275
+ env_1.hasError = true;
2276
+ } finally {
2277
+ const result_1 = __disposeResources$1(env_1);
2278
+ if (result_1) await result_1;
2257
2279
  }
2258
2280
  }
2259
2281
  /** Resolve a live or standing preset scope without creating an Agent. */
2260
- async scopeFor(sessionId, agentPreset) {
2261
- const live = this.ctx.agents.get(sessionId);
2262
- if (live !== void 0) return live;
2282
+ async scopeFor(agentPreset) {
2263
2283
  const presets = this.ctx.get("agentPresets");
2264
2284
  if (presets === void 0) return void 0;
2265
2285
  try {
2266
- return await presets.standingKeyFor(agentPreset);
2286
+ return await presets.acquireScope(agentPreset);
2267
2287
  } catch {
2268
2288
  return;
2269
2289
  }
@@ -2344,6 +2364,58 @@ const SessionMediaReferences = {
2344
2364
  }
2345
2365
  };
2346
2366
  //#endregion
2367
+ //#region lib/types/archived-session-gate.js
2368
+ /**
2369
+ * The controller's admission gate for archived Sessions: an archived
2370
+ * Session, or a subagent descendant of one, must not run a model step until
2371
+ * it is restored. The work a Session still runs is reported and stopped by
2372
+ * its owners — the Agent registry (`turn`), the job registry seam (`job`),
2373
+ * the Subagent runtime (`subagent`), and the Schedule plugin (`schedule`) —
2374
+ * through the Workspace registry's archive-admission events.
2375
+ */
2376
+ /**
2377
+ * The gate as a plugin for `ctx.plugin(...)`: it loads once the Agent
2378
+ * registry, Session store, and Workspace registry are available and unwinds
2379
+ * with the plugin's fiber. A late waking delivery to an archived Session — a
2380
+ * subagent settlement, a queued follow-up — proposes a step the gate rejects,
2381
+ * which the loop ends as `blocked` without a request; unarchiving lifts the
2382
+ * gate for the whole lineage.
2383
+ */
2384
+ const ArchivedSessionGate = {
2385
+ name: "archived-session-gate",
2386
+ inject: [
2387
+ "agents",
2388
+ "sessions",
2389
+ "workspaceRegistry"
2390
+ ],
2391
+ apply(ctx) {
2392
+ ctx.on("agent/pre-step", (payload, next) => underArchivedSession(ctx, payload.agent) ? Promise.resolve({ kind: "reject" }) : next());
2393
+ }
2394
+ };
2395
+ /**
2396
+ * Whether the Agent's Session, or a Session above it in its subagent lineage,
2397
+ * is archived. Lineage follows the durable header fields through
2398
+ * subagent-origin Sessions only: a fork of an archived Session is an
2399
+ * independent conversation.
2400
+ * @param ctx - Host context.
2401
+ * @param agent - the Agent proposing a step.
2402
+ * @returns whether an archived Session owns the step.
2403
+ */
2404
+ function underArchivedSession(ctx, agent) {
2405
+ const archived = ctx.workspaceRegistry.archivedSessionIds;
2406
+ let header = agent.session.header;
2407
+ const visited = /* @__PURE__ */ new Set();
2408
+ while (!visited.has(header.id)) {
2409
+ if (archived.includes(header.id)) return true;
2410
+ visited.add(header.id);
2411
+ if (header.origin !== "subagent" || header.parentSession === void 0) return false;
2412
+ const parent = ctx.sessions.get(header.parentSession);
2413
+ if (parent === void 0) return archived.includes(header.parentSession);
2414
+ header = parent.header;
2415
+ }
2416
+ return false;
2417
+ }
2418
+ //#endregion
2347
2419
  //#region lib/types/index.js
2348
2420
  /** Session Remote owner: cold reads, explicit Agent commands, and live control state. */
2349
2421
  var __runInitializers = function(thisArg, initializers, value) {
@@ -2453,6 +2525,7 @@ let SessionController = (() => {
2453
2525
  let _modelCatalog_decorators;
2454
2526
  let _canOpenWorkspacePath_decorators;
2455
2527
  let _openWorkspacePath_decorators;
2528
+ let _workspacePathApplications_decorators;
2456
2529
  let _rename_decorators;
2457
2530
  let _fork_decorators;
2458
2531
  let _prompt_decorators;
@@ -2461,6 +2534,7 @@ let SessionController = (() => {
2461
2534
  let _cancel_decorators;
2462
2535
  let _page_decorators;
2463
2536
  let _follow_decorators;
2537
+ let _projections_decorators;
2464
2538
  let _control_decorators;
2465
2539
  return class SessionController extends _classSuper {
2466
2540
  static {
@@ -2472,6 +2546,7 @@ let SessionController = (() => {
2472
2546
  _modelCatalog_decorators = [Remote("modelCatalog")];
2473
2547
  _canOpenWorkspacePath_decorators = [Remote];
2474
2548
  _openWorkspacePath_decorators = [Remote("openWorkspacePath")];
2549
+ _workspacePathApplications_decorators = [Remote("workspacePathApplications")];
2475
2550
  _rename_decorators = [Remote("rename")];
2476
2551
  _fork_decorators = [Remote("fork")];
2477
2552
  _prompt_decorators = [Remote("prompt")];
@@ -2480,6 +2555,7 @@ let SessionController = (() => {
2480
2555
  _cancel_decorators = [Remote("cancel")];
2481
2556
  _page_decorators = [Remote("page")];
2482
2557
  _follow_decorators = [Remote({ mode: "stream" })];
2558
+ _projections_decorators = [Remote("projections")];
2483
2559
  _control_decorators = [Remote({ mode: "stream" })];
2484
2560
  __esDecorate(this, null, _list_decorators, {
2485
2561
  kind: "method",
@@ -2558,6 +2634,17 @@ let SessionController = (() => {
2558
2634
  },
2559
2635
  metadata: _metadata
2560
2636
  }, null, _instanceExtraInitializers);
2637
+ __esDecorate(this, null, _workspacePathApplications_decorators, {
2638
+ kind: "method",
2639
+ name: "workspacePathApplications",
2640
+ static: false,
2641
+ private: false,
2642
+ access: {
2643
+ has: (obj) => "workspacePathApplications" in obj,
2644
+ get: (obj) => obj.workspacePathApplications
2645
+ },
2646
+ metadata: _metadata
2647
+ }, null, _instanceExtraInitializers);
2561
2648
  __esDecorate(this, null, _rename_decorators, {
2562
2649
  kind: "method",
2563
2650
  name: "rename",
@@ -2646,6 +2733,17 @@ let SessionController = (() => {
2646
2733
  },
2647
2734
  metadata: _metadata
2648
2735
  }, null, _instanceExtraInitializers);
2736
+ __esDecorate(this, null, _projections_decorators, {
2737
+ kind: "method",
2738
+ name: "projections",
2739
+ static: false,
2740
+ private: false,
2741
+ access: {
2742
+ has: (obj) => "projections" in obj,
2743
+ get: (obj) => obj.projections
2744
+ },
2745
+ metadata: _metadata
2746
+ }, null, _instanceExtraInitializers);
2649
2747
  __esDecorate(this, null, _control_decorators, {
2650
2748
  kind: "method",
2651
2749
  name: "control",
@@ -2669,6 +2767,7 @@ let SessionController = (() => {
2669
2767
  "agents",
2670
2768
  "attachments",
2671
2769
  "fileUploads",
2770
+ "fs",
2672
2771
  "llm",
2673
2772
  "sessions",
2674
2773
  "sessionProjections",
@@ -2683,6 +2782,8 @@ let SessionController = (() => {
2683
2782
  history;
2684
2783
  listState;
2685
2784
  openPath;
2785
+ fileApplications;
2786
+ openFileApplication;
2686
2787
  revealPath;
2687
2788
  canOpenPath;
2688
2789
  promotions = /* @__PURE__ */ new Set();
@@ -2709,18 +2810,26 @@ let SessionController = (() => {
2709
2810
  this.promote(observation);
2710
2811
  });
2711
2812
  this.listState = new ApiSessionList(ctx);
2712
- this.openPath = internals.openPath ?? openNativePath;
2813
+ this.fileApplications = internals.fileApplications ?? nativeFileApplications;
2814
+ this.openFileApplication = internals.openFileApplication ?? openNativeFileApplication;
2815
+ this.openPath = internals.openPath ?? openNativeAssociatedPath;
2713
2816
  this.revealPath = internals.revealPath ?? revealNativePath;
2714
2817
  this.canOpenPath = internals.canOpenPath ?? (() => config.nativeOpen ?? (internals.openPath !== void 0 || canOpenNativePath()));
2715
2818
  ctx.plugin(SessionFileReferences);
2716
2819
  ctx.plugin(SessionMediaReferences);
2717
2820
  ctx.plugin(SessionSkillCatalog);
2821
+ ctx.plugin(ArchivedSessionGate);
2718
2822
  ctx.on("session/created", (session) => {
2719
2823
  ctx.emit("api-session/added", this.listState.summaryFor(session));
2720
2824
  });
2721
2825
  ctx.on("session/disposed", (session) => {
2722
2826
  ctx.emit("api-session/removed", session.id);
2723
2827
  });
2828
+ const publishAgentAvailability = ({ agent }) => {
2829
+ if (ctx.sessions.get(agent.id) === agent.session) ctx.emit("api-session/added", this.listState.summaryFor(agent.session));
2830
+ };
2831
+ ctx.on("agent/created", publishAgentAvailability);
2832
+ ctx.on("agent/disposed", publishAgentAvailability);
2724
2833
  ctx.on("agent/status", ({ agent, status }) => {
2725
2834
  ctx.emit("api-session/status", agent.id, status === "running");
2726
2835
  });
@@ -2846,24 +2955,53 @@ let SessionController = (() => {
2846
2955
  };
2847
2956
  }
2848
2957
  /**
2849
- * Open one path prepared by a Session-aware caller on the Host desktop.
2958
+ * Verify one path through the composed filesystem and open it on the Host desktop.
2850
2959
  * @param request - path after best-effort Session workspace resolution.
2851
2960
  * @param signal - caller lifetime; abort terminates the native command.
2852
2961
  * @returns confirmation after the native opener accepts the path.
2853
- * @throws RemoteError when the request is invalid, cancelled, or the opener fails.
2962
+ * @throws RemoteError when the request is invalid, has no verified Host mapping, is cancelled, or the opener fails.
2854
2963
  */
2855
2964
  async openWorkspacePath(request, signal) {
2856
- if (request.path.length === 0) throw new RemoteError("gateway/bad-request", "session.openWorkspacePath requires a non-empty path", {});
2857
- signal.throwIfAborted();
2858
2965
  try {
2859
- if (request.action === "reveal") await this.revealPath(request.path, signal);
2860
- else await this.openPath(request.path, signal);
2966
+ const path = await this.verifyDesktopPath(request.path, signal);
2967
+ if (request.action === "reveal") await this.revealPath(path, signal);
2968
+ else if (request.application !== void 0) await this.openFileApplication(path, request.application, signal);
2969
+ else await this.openPath(path, signal);
2861
2970
  return { opened: true };
2862
2971
  } catch (error) {
2863
2972
  if (signal.aborted) throw new RemoteError("gateway/cancelled", "path open was aborted", {});
2864
- throw new RemoteError("gateway/internal", `path open failed: ${error instanceof Error ? error.message : String(error)}`, {});
2973
+ if (error instanceof RemoteError) throw error;
2974
+ throw new RemoteError("gateway/internal", "path open failed", {}, { cause: error });
2975
+ }
2976
+ }
2977
+ /**
2978
+ * Query current file handlers on the serving desktop without activating an Agent.
2979
+ * @param request - file path in Host filesystem syntax.
2980
+ * @param signal - caller lifetime, propagated to filesystem and desktop queries.
2981
+ * @returns OS application names, icons, and default selection; empty when desktop opening is unavailable.
2982
+ * @throws RemoteError when the path is invalid, the query is cancelled, or native discovery fails.
2983
+ */
2984
+ async workspacePathApplications(request, signal) {
2985
+ if (!this.canOpenPath()) return [];
2986
+ try {
2987
+ const path = await this.verifyDesktopPath(request.path, signal);
2988
+ return await this.fileApplications(path, signal);
2989
+ } catch (error) {
2990
+ if (signal.aborted) throw new RemoteError("gateway/cancelled", "application query was aborted", {});
2991
+ if (error instanceof RemoteError) throw error;
2992
+ throw new RemoteError("gateway/internal", "file application query failed", {}, { cause: error });
2865
2993
  }
2866
2994
  }
2995
+ async verifyDesktopPath(path, signal) {
2996
+ if (path.length === 0) throw new RemoteError("gateway/bad-request", "A non-empty file path is required", {});
2997
+ signal.throwIfAborted();
2998
+ const hostPath = resolve(path);
2999
+ const { fs } = this.ctx;
3000
+ const mapped = fs.processPathFromHostPath(hostPath);
3001
+ if (mapped === void 0 || fs.processPath(await fs.resolve(mapped, { signal })) !== hostPath) throw new RemoteError("gateway/bad-request", "Path has no verified Host path", {});
3002
+ signal.throwIfAborted();
3003
+ return hostPath;
3004
+ }
2867
3005
  /**
2868
3006
  * Rename one Session after explicitly resuming it.
2869
3007
  * @param request - Session identity and proposed title.
@@ -2873,8 +3011,10 @@ let SessionController = (() => {
2873
3011
  return this.commands.rename(request);
2874
3012
  }
2875
3013
  /**
2876
- * Fork one cold-readable completed-turn prefix into a new Session.
2877
- * @param request - source Session and optional event anchor.
3014
+ * Fork one cold-readable exact event prefix into a new Session. An omitted
3015
+ * boundary selects the latest completed-turn prefix; an open cut receives
3016
+ * synthetic fork closers.
3017
+ * @param request - source Session and optional exact inclusive event boundary.
2878
3018
  * @returns the new Session identity.
2879
3019
  */
2880
3020
  fork(request) {
@@ -2934,6 +3074,41 @@ let SessionController = (() => {
2934
3074
  return this.history.follow(request, signal);
2935
3075
  }
2936
3076
  /**
3077
+ * Read all registered projections without activating an Agent.
3078
+ * @param request - Session whose current values are required.
3079
+ * @param signal - cancellation for the Session observation.
3080
+ * @returns complete baseline, or null when the Session does not exist.
3081
+ */
3082
+ async projections(request, signal) {
3083
+ const { sessionId } = request;
3084
+ if (sessionId.length === 0) throw new RemoteError("gateway/bad-request", "sessionId must not be empty", {});
3085
+ try {
3086
+ const env_2 = {
3087
+ stack: [],
3088
+ error: void 0,
3089
+ hasError: false
3090
+ };
3091
+ try {
3092
+ const projections = __addDisposableResource(env_2, await this.ctx.sessionQuery.observeSession(sessionId, { signal }), false).projections;
3093
+ if (projections === void 0) throw new RemoteError("session/projections-unavailable", "Session projections are unavailable", {});
3094
+ return {
3095
+ asOfSeq: projections.asOfSeq,
3096
+ values: projections.values
3097
+ };
3098
+ } catch (e_2) {
3099
+ env_2.error = e_2;
3100
+ env_2.hasError = true;
3101
+ } finally {
3102
+ __disposeResources(env_2);
3103
+ }
3104
+ } catch (error) {
3105
+ if (error instanceof SessionQueryError && error.code === "SESSION_QUERY_SESSION_NOT_FOUND") return null;
3106
+ if (signal.aborted || error instanceof SessionQueryError && error.code === "SESSION_QUERY_ABORTED") throw new RemoteError("gateway/cancelled", "Session projection read was cancelled", {}, { cause: error });
3107
+ if (error instanceof RemoteError) throw error;
3108
+ throw new RemoteError("gateway/internal", "Session projection read failed", {}, { cause: error });
3109
+ }
3110
+ }
3111
+ /**
2937
3112
  * Stream a complete live-control baseline followed by replacement frames.
2938
3113
  * @param signal - cancellation owned by the Remote stream carrier.
2939
3114
  * @returns one complete baseline followed by live replacement frames.