@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
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
@@ -227,7 +228,8 @@ var ApiSessionAgentController = class {
227
228
  this.resumes.set(sessionId, resume);
228
229
  }
229
230
  try {
230
- return { agent: await resume };
231
+ const agent = await resume;
232
+ return this.liveAgent(sessionId) ?? { agent };
231
233
  } catch (error) {
232
234
  if (error instanceof ApiSessionNotFound) return { error: new RemoteError("session/not-found", error.message, { sessionId }) };
233
235
  if (error instanceof ApiSessionSubagentOwnership) return { error: apiSessionSubagentOwnershipError(error.sessionId) };
@@ -235,6 +237,7 @@ var ApiSessionAgentController = class {
235
237
  if (raced !== void 0) return raced;
236
238
  const racedSession = this.ctx.sessions.get(sessionId);
237
239
  if (racedSession !== void 0 && hasApiSessionSubagentOwner(this.ctx, racedSession, void 0)) return { error: apiSessionSubagentOwnershipError(sessionId) };
240
+ if (error instanceof Error && error.name === "SessionAlreadyOwnedError") return { error: new RemoteError("session/writer-held", error.message, { sessionId }) };
238
241
  return { error: new RemoteError("gateway/internal", `resume failed for session "${sessionId}": ${String(error)}`, {}) };
239
242
  }
240
243
  }
@@ -548,6 +551,20 @@ var __disposeResources$3 = (function(SuppressedError) {
548
551
  function hasPromptContent(content) {
549
552
  return content.some((part) => part.type !== "text" || part.text.trim().length > 0);
550
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
+ }
551
568
  /** Implements Session business commands delegated by the Session Controller Remote service. */
552
569
  var SessionCommandController = class {
553
570
  ctx;
@@ -653,8 +670,10 @@ var SessionCommandController = class {
653
670
  }
654
671
  }
655
672
  /**
656
- * Create a new ordinary Session from one completed-turn prefix.
657
- * @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.
658
677
  * @returns the new Session identity.
659
678
  */
660
679
  async fork(request) {
@@ -678,10 +697,9 @@ var SessionCommandController = class {
678
697
  throw new RemoteError("gateway/internal", `fork source unavailable for session "${request.sessionId}": ${String(error)}`, {});
679
698
  }
680
699
  const source = __addDisposableResource$3(env_1, observed, false);
681
- const lastSeq = source.events.at(-1)?.seq ?? -1;
682
- 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);
683
- 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 });
684
- 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);
685
703
  let workspace;
686
704
  try {
687
705
  workspace = await this.forkWorkspace(source.header);
@@ -694,8 +712,8 @@ var SessionCommandController = class {
694
712
  const { provider, model } = this.ctx.agentDefaultModel.currentSelection();
695
713
  await this.ctx.agents.create({
696
714
  sessionId: childId,
697
- seed: source.events.slice(0, cut),
698
- inheritedEventCount: cut,
715
+ seed,
716
+ inheritedEventCount: SessionLogOffset(boundary + 1),
699
717
  meta: {
700
718
  ...source.header.cwd === void 0 ? {} : { cwd: source.header.cwd },
701
719
  parentSession: source.header.id,
@@ -814,17 +832,24 @@ var SessionCommandController = class {
814
832
  }
815
833
  }
816
834
  /**
817
- * Mutate one still-pending queue occurrence without resuming a cold Agent.
835
+ * Mutate one pending Inbox occurrence, restoring an ordinary cold Agent when needed.
818
836
  * @param request - Session, queue item, and requested mutation.
819
837
  * @returns acknowledgement that the queue mutation was applied.
820
838
  */
821
- updateQueue(request) {
839
+ async updateQueue(request) {
822
840
  if (request.action.kind === "edit") {
823
841
  if (request.action.content.some((block) => block.type !== "text")) throw new RemoteError("session/attachment-invalid", "queue edits accept text content only", { reason: "QUEUE_EDIT_NON_TEXT" });
824
842
  if (!hasPromptContent(request.action.content)) throw new RemoteError("gateway/bad-request", "queue edit content must include non-whitespace text", {});
825
843
  }
826
- const agent = this.ctx.agents.get(request.sessionId);
827
- if (agent === void 0) throw new RemoteError("session/queue-item-not-found", "queued item is no longer pending", { itemId: request.itemId });
844
+ let agent = this.ctx.agents.get(request.sessionId);
845
+ if (agent === void 0) {
846
+ const found = await this.agents.resolveAgent(request.sessionId);
847
+ if ("error" in found) {
848
+ if (found.error.code !== "session/not-found") throw found.error;
849
+ throw new RemoteError("session/queue-item-not-found", "queued item is no longer pending", { itemId: request.itemId });
850
+ }
851
+ agent = found.agent;
852
+ }
828
853
  if (hasApiSessionSubagentOwner(this.ctx, agent.session, agent)) {
829
854
  const identity = this.ctx.sessionProjections.snapshot(agent.session, ["subagent"]).values.subagent;
830
855
  if (identity?.mode !== "continuable" || !agent.session.isOwnSeq(identity.seq)) throw apiSessionSubagentOwnershipError(request.sessionId);
@@ -882,6 +907,7 @@ var SessionCommandController = class {
882
907
  }
883
908
  rejectCreation(sessionId, error) {
884
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 });
885
911
  if (error instanceof ApiSessionPresetConflict) throw new RemoteError("agent-preset/conflict", error.message, {
886
912
  sessionId: error.sessionId,
887
913
  requestedPreset: error.requestedPreset,
@@ -957,23 +983,39 @@ function imageBlockIn(content, match) {
957
983
  const ref = block.attachment;
958
984
  if (match(ref)) return ref;
959
985
  }
960
- if (block.type === "tool-result") {
961
- const nested = imageBlockIn(block.content, match);
962
- if (nested !== void 0) return nested;
963
- }
964
986
  }
965
987
  }
988
+ /** Read only first-party declared content fields; unknown event payloads stay opaque. */
966
989
  function imageInEvent(event, match) {
967
990
  const data = event.data;
968
- const direct = imageBlockIn(data.content, match);
969
- if (direct !== void 0) return direct;
970
- const message = imageBlockIn(data.message?.content, match);
971
- if (message !== void 0) return message;
972
- for (const inserted of data.inserted ?? []) {
973
- const found = imageBlockIn(inserted.content, match);
974
- 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;
975
1016
  }
976
- 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")) {
977
1019
  const found = imageBlockIn([chunk.block], match);
978
1020
  if (found !== void 0) return found;
979
1021
  }
@@ -989,12 +1031,12 @@ function routeServed(ctx, provider) {
989
1031
  }
990
1032
  //#endregion
991
1033
  //#region lib/types/control.js
992
- /** Live Session queue, jobs, and projection state with reconnect baselines. */
1034
+ /** Live Session projection state with reconnect baselines. */
993
1035
  /** Owns the Host-wide Session control stream. */
994
1036
  var SessionControlController = class {
995
1037
  ctx;
996
1038
  streams = /* @__PURE__ */ new Set();
997
- /** @param ctx - Host context carrying live Agent, projection, and jobs services. */
1039
+ /** @param ctx - Host context carrying live Agent and projection services. */
998
1040
  constructor(ctx) {
999
1041
  this.ctx = ctx;
1000
1042
  ctx.sessionProjections.onChanged((session, key, value, seq) => {
@@ -1005,26 +1047,6 @@ var SessionControlController = class {
1005
1047
  value,
1006
1048
  seq
1007
1049
  });
1008
- if (key !== "inbox") return;
1009
- if (this.ctx.agents.get(session.id)?.session !== session) return;
1010
- this.broadcast({
1011
- type: "queue",
1012
- sessionId: session.id,
1013
- items: queueItemsFromInbox(value)
1014
- });
1015
- });
1016
- ctx.inject(["jobs"], (jobsCtx) => {
1017
- jobsCtx.jobs.onJobsChanged((owner) => {
1018
- this.onJobsChanged(owner);
1019
- });
1020
- });
1021
- ctx.on("session/created", (session) => {
1022
- const jobs = this.jobsFor(this.ctx.agents.get(session.id));
1023
- if (jobs.length > 0) this.broadcast({
1024
- type: "jobs",
1025
- sessionId: session.id,
1026
- jobs
1027
- });
1028
1050
  });
1029
1051
  ctx.effect(() => () => {
1030
1052
  for (const stream of this.streams) stream.end();
@@ -1053,18 +1075,7 @@ var SessionControlController = class {
1053
1075
  }
1054
1076
  baseline() {
1055
1077
  const sessions = this.ctx.sessions.list();
1056
- const queues = Object.create(null);
1057
- const jobs = Object.create(null);
1058
- for (const session of sessions) {
1059
- const agent = this.ctx.agents.get(session.id);
1060
- queues[session.id] = agent?.session === session ? queueItems(agent) : [];
1061
- jobs[session.id] = this.jobsFor(agent);
1062
- }
1063
- return {
1064
- queues,
1065
- jobs,
1066
- projections: this.projectionBaseline(sessions)
1067
- };
1078
+ return { projections: this.projectionBaseline(sessions) };
1068
1079
  }
1069
1080
  projectionBaseline(sessions) {
1070
1081
  const blocks = Object.create(null);
@@ -1077,25 +1088,6 @@ var SessionControlController = class {
1077
1088
  }
1078
1089
  return blocks;
1079
1090
  }
1080
- onJobsChanged(owner) {
1081
- if (owner !== void 0) {
1082
- this.broadcast({
1083
- type: "jobs",
1084
- sessionId: owner.id,
1085
- jobs: this.jobsFor(owner)
1086
- });
1087
- return;
1088
- }
1089
- for (const session of this.ctx.sessions.list()) this.broadcast({
1090
- type: "jobs",
1091
- sessionId: session.id,
1092
- jobs: this.jobsFor(this.ctx.agents.get(session.id))
1093
- });
1094
- }
1095
- jobsFor(agent) {
1096
- const jobs = this.ctx.get("jobs");
1097
- return jobs === void 0 ? [] : jobs.list(agent).map(jobView);
1098
- }
1099
1091
  broadcast(frame) {
1100
1092
  for (const stream of this.streams) stream.push(frame);
1101
1093
  }
@@ -1141,47 +1133,6 @@ var ControlQueue = class {
1141
1133
  }
1142
1134
  }
1143
1135
  };
1144
- function queueItems(agent) {
1145
- return queueItemsFromInbox({
1146
- "next-turn": agent.inbox.nextTurn,
1147
- "next-step": agent.inbox.nextStep
1148
- });
1149
- }
1150
- function queueItemsFromInbox(inbox) {
1151
- return [...inbox["next-turn"].map((message) => ({
1152
- id: message.id,
1153
- placement: "queued",
1154
- ...promptRpcId(message),
1155
- message: {
1156
- id: message.id,
1157
- content: message.content
1158
- }
1159
- })), ...inbox["next-step"].map((message) => ({
1160
- id: message.id,
1161
- placement: message.source.kind === "user" ? "steering" : "context",
1162
- ...promptRpcId(message),
1163
- message: {
1164
- id: message.id,
1165
- content: message.content
1166
- }
1167
- }))];
1168
- }
1169
- /** Prompt-RPC identity carried by a browser-submitted message's user source. */
1170
- function promptRpcId(message) {
1171
- const source = message.source;
1172
- return source.kind === "user" && "rpcId" in source ? { rpcId: source.rpcId } : {};
1173
- }
1174
- function jobView(job) {
1175
- return {
1176
- id: job.id,
1177
- kind: job.kind,
1178
- label: job.label,
1179
- status: job.status,
1180
- ...job.detail === void 0 ? {} : { detail: job.detail },
1181
- startedAt: job.startedAt,
1182
- ...job.finishedAt === void 0 ? {} : { finishedAt: job.finishedAt }
1183
- };
1184
- }
1185
1136
  //#endregion
1186
1137
  //#region lib/types/assistant-stream.js
1187
1138
  /** Process-local assistant state retained for reconnecting Web followers. */
@@ -1589,7 +1540,7 @@ function validateAddress(address, header, inheritedEventCount, projections) {
1589
1540
  childSessionId: address.childSessionId,
1590
1541
  reason: "unsupported"
1591
1542
  });
1592
- if (identity.mode !== address.mode) throw new RemoteError("subagent/unauthorized", "subagent mode does not match the supplied address", { childSessionId: address.childSessionId });
1543
+ if (address.mode !== "unknown" && identity.mode !== address.mode) throw new RemoteError("subagent/unauthorized", "subagent mode does not match the supplied address", { childSessionId: address.childSessionId });
1593
1544
  }
1594
1545
  function rejectNotFound(address) {
1595
1546
  if (address.kind === "session") throw new RemoteError("session/not-found", `session "${address.sessionId}" not found`, { sessionId: address.sessionId });
@@ -1814,6 +1765,7 @@ var ApiSessionList = class {
1814
1765
  return {
1815
1766
  sessionId: session.id,
1816
1767
  updatedAt: updatedAt(session.header, metadata),
1768
+ agentAvailable: this.ctx.agents.get(session.id)?.session === session,
1817
1769
  running: this.ctx.agents.get(session.id)?.status === "running",
1818
1770
  blank: metadata?.blank ?? session.seq === 0,
1819
1771
  ...listFields(session.header),
@@ -1850,6 +1802,7 @@ var ApiSessionList = class {
1850
1802
  return {
1851
1803
  sessionId: header.id,
1852
1804
  updatedAt: updatedAt(header, metadata),
1805
+ agentAvailable: false,
1853
1806
  running: false,
1854
1807
  blank: metadata?.blank ?? false,
1855
1808
  ...listFields(header),
@@ -1946,18 +1899,29 @@ var ApiSessionList = class {
1946
1899
  }
1947
1900
  projectionsFor(header, session) {
1948
1901
  try {
1902
+ if (session !== void 0) return hintsOf("sequenced", this.ctx.sessionProjections.cachedSnapshot(session));
1949
1903
  const cache = this.ctx.get("sessionProjectionCache");
1950
- const block = session === void 0 ? header.isSeeded ? void 0 : cache?.cachedSnapshot(header, SessionLogOffset(0)) ?? cache?.cachedPredecessorTitle(header, SessionLogOffset(0)) : this.ctx.sessionProjections.cachedSnapshot(session);
1951
- return block !== void 0 && Object.keys(block.values).length > 0 ? {
1952
- asOfSeq: block.asOfSeq,
1953
- values: block.values
1954
- } : void 0;
1904
+ return hintsOf("cached", cache?.cachedSnapshot(header) ?? cache?.cachedPredecessorTitle(header));
1955
1905
  } catch (error) {
1956
1906
  this.ctx.logger.warn(`api-session.list: projection column for "${header.id}" failed; serving the row without it: ${String(error)}`);
1957
1907
  return;
1958
1908
  }
1959
1909
  }
1960
1910
  };
1911
+ /**
1912
+ * Wrap one projection block as Session-list hints of the named sequence space.
1913
+ * @param kind - which sequence space the block's watermark belongs to.
1914
+ * @param block - the block, or `undefined` when no source served one.
1915
+ * @returns the hints, or `undefined` when the block is absent or carries no value.
1916
+ */
1917
+ function hintsOf(kind, block) {
1918
+ if (block === void 0 || Object.keys(block.values).length === 0) return void 0;
1919
+ return {
1920
+ kind,
1921
+ asOfSeq: block.asOfSeq,
1922
+ values: block.values
1923
+ };
1924
+ }
1961
1925
  function normalizeSearchQuery(query) {
1962
1926
  const normalized = query.trim();
1963
1927
  if (normalized.length === 0) throw new RemoteError("gateway/bad-request", "session search query must not be empty", {});
@@ -2242,59 +2206,71 @@ let SessionSkillCatalog = (() => {
2242
2206
  * @throws RemoteError when the Session cannot be inspected or no registry can serve it.
2243
2207
  */
2244
2208
  async list(request, signal) {
2245
- const { sessionId } = request;
2246
- let cwd;
2247
- let agentPreset;
2209
+ const env_1 = {
2210
+ stack: [],
2211
+ error: void 0,
2212
+ hasError: false
2213
+ };
2248
2214
  try {
2249
- const env_1 = {
2250
- stack: [],
2251
- error: void 0,
2252
- hasError: false
2253
- };
2215
+ const { sessionId } = request;
2216
+ let cwd;
2217
+ let agentPreset;
2254
2218
  try {
2255
- const observation = __addDisposableResource$1(env_1, await this.ctx.sessionQuery.observeSession(sessionId), false);
2256
- if (observation.projections === void 0) throw new Error("skill catalog requires a projected Session observation");
2257
- cwd = observation.header.cwd;
2258
- agentPreset = observation.projections.values.agentPreset ?? void 0;
2259
- } catch (e_1) {
2260
- env_1.error = e_1;
2261
- env_1.hasError = true;
2262
- } finally {
2263
- __disposeResources$1(env_1);
2219
+ const env_2 = {
2220
+ stack: [],
2221
+ error: void 0,
2222
+ hasError: false
2223
+ };
2224
+ try {
2225
+ const observation = __addDisposableResource$1(env_2, await this.ctx.sessionQuery.observeSession(sessionId), false);
2226
+ if (observation.projections === void 0) throw new Error("skill catalog requires a projected Session observation");
2227
+ cwd = observation.header.cwd;
2228
+ agentPreset = observation.projections.values.agentPreset ?? void 0;
2229
+ } catch (e_1) {
2230
+ env_2.error = e_1;
2231
+ env_2.hasError = true;
2232
+ } finally {
2233
+ __disposeResources$1(env_2);
2234
+ }
2235
+ } catch (error) {
2236
+ if (error instanceof SessionQueryError && error.code === "SESSION_QUERY_SESSION_NOT_FOUND") throw new RemoteError("session/not-found", `session "${sessionId}" not found`, { sessionId });
2237
+ throw new RemoteError("gateway/internal", `session "${sessionId}" could not be inspected: ${String(error)}`, {});
2264
2238
  }
2265
- } catch (error) {
2266
- if (error instanceof SessionQueryError && error.code === "SESSION_QUERY_SESSION_NOT_FOUND") throw new RemoteError("session/not-found", `session "${sessionId}" not found`, { sessionId });
2267
- throw new RemoteError("gateway/internal", `session "${sessionId}" could not be inspected: ${String(error)}`, {});
2268
- }
2269
- if (cwd === void 0) throw new RemoteError("gateway/internal", `session "${sessionId}" has no project cwd`, {});
2270
- const live = this.ctx.agents.get(sessionId);
2271
- const presets = this.ctx.get("agentPresets");
2272
- const skillRegistry = (live === void 0 ? void 0 : presets?.serviceFor(live, "skills")) ?? this.ctx.get("skills");
2273
- 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", {});
2274
- const scope = await this.scopeFor(sessionId, agentPreset);
2275
- try {
2276
- return { skills: (await skillRegistry.list({
2277
- cwd,
2278
- scope
2279
- })).filter(isUserInvocable).map((skill) => ({
2280
- name: skill.name,
2281
- ...skill.path === void 0 ? {} : { path: skill.path },
2282
- description: skill.description,
2283
- ...skill.whenToUse === void 0 ? {} : { whenToUse: skill.whenToUse },
2284
- modelInvocable: skill.invocation.modelInvocable
2285
- })) };
2286
- } catch (error) {
2287
- throw new RemoteError("gateway/internal", `skill listing failed: ${String(error)}`, {});
2239
+ if (cwd === void 0) throw new RemoteError("gateway/internal", `session "${sessionId}" has no project cwd`, {});
2240
+ const live = this.ctx.agents.get(sessionId);
2241
+ const presets = this.ctx.get("agentPresets");
2242
+ const skillRegistry = (live === void 0 ? void 0 : presets?.serviceFor(live, "skills")) ?? this.ctx.get("skills");
2243
+ 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", {});
2244
+ const lease = __addDisposableResource$1(env_1, live === void 0 ? await this.scopeFor(agentPreset) : void 0, true);
2245
+ const scope = live ?? lease?.key;
2246
+ try {
2247
+ return { skills: (await skillRegistry.list({
2248
+ cwd,
2249
+ scope
2250
+ })).filter(isUserInvocable).map((skill) => ({
2251
+ name: skill.name,
2252
+ ...skill.path === void 0 ? {} : { path: skill.path },
2253
+ description: skill.description,
2254
+ ...skill.whenToUse === void 0 ? {} : { whenToUse: skill.whenToUse },
2255
+ modelInvocable: skill.invocation.modelInvocable
2256
+ })) };
2257
+ } catch (error) {
2258
+ throw new RemoteError("gateway/internal", `skill listing failed: ${String(error)}`, {});
2259
+ }
2260
+ } catch (e_2) {
2261
+ env_1.error = e_2;
2262
+ env_1.hasError = true;
2263
+ } finally {
2264
+ const result_1 = __disposeResources$1(env_1);
2265
+ if (result_1) await result_1;
2288
2266
  }
2289
2267
  }
2290
2268
  /** Resolve a live or standing preset scope without creating an Agent. */
2291
- async scopeFor(sessionId, agentPreset) {
2292
- const live = this.ctx.agents.get(sessionId);
2293
- if (live !== void 0) return live;
2269
+ async scopeFor(agentPreset) {
2294
2270
  const presets = this.ctx.get("agentPresets");
2295
2271
  if (presets === void 0) return void 0;
2296
2272
  try {
2297
- return await presets.standingKeyFor(agentPreset);
2273
+ return await presets.acquireScope(agentPreset);
2298
2274
  } catch {
2299
2275
  return;
2300
2276
  }
@@ -2375,6 +2351,58 @@ const SessionMediaReferences = {
2375
2351
  }
2376
2352
  };
2377
2353
  //#endregion
2354
+ //#region lib/types/archived-session-gate.js
2355
+ /**
2356
+ * The controller's admission gate for archived Sessions: an archived
2357
+ * Session, or a subagent descendant of one, must not run a model step until
2358
+ * it is restored. The work a Session still runs is reported and stopped by
2359
+ * its owners — the Agent registry (`turn`), the job registry seam (`job`),
2360
+ * the Subagent runtime (`subagent`), and the Schedule plugin (`schedule`) —
2361
+ * through the Workspace registry's archive-admission events.
2362
+ */
2363
+ /**
2364
+ * The gate as a plugin for `ctx.plugin(...)`: it loads once the Agent
2365
+ * registry, Session store, and Workspace registry are available and unwinds
2366
+ * with the plugin's fiber. A late waking delivery to an archived Session — a
2367
+ * subagent settlement, a queued follow-up — proposes a step the gate rejects,
2368
+ * which the loop ends as `blocked` without a request; unarchiving lifts the
2369
+ * gate for the whole lineage.
2370
+ */
2371
+ const ArchivedSessionGate = {
2372
+ name: "archived-session-gate",
2373
+ inject: [
2374
+ "agents",
2375
+ "sessions",
2376
+ "workspaceRegistry"
2377
+ ],
2378
+ apply(ctx) {
2379
+ ctx.on("agent/pre-step", (payload, next) => underArchivedSession(ctx, payload.agent) ? Promise.resolve({ kind: "reject" }) : next());
2380
+ }
2381
+ };
2382
+ /**
2383
+ * Whether the Agent's Session, or a Session above it in its subagent lineage,
2384
+ * is archived. Lineage follows the durable header fields through
2385
+ * subagent-origin Sessions only: a fork of an archived Session is an
2386
+ * independent conversation.
2387
+ * @param ctx - Host context.
2388
+ * @param agent - the Agent proposing a step.
2389
+ * @returns whether an archived Session owns the step.
2390
+ */
2391
+ function underArchivedSession(ctx, agent) {
2392
+ const archived = ctx.workspaceRegistry.archivedSessionIds;
2393
+ let header = agent.session.header;
2394
+ const visited = /* @__PURE__ */ new Set();
2395
+ while (!visited.has(header.id)) {
2396
+ if (archived.includes(header.id)) return true;
2397
+ visited.add(header.id);
2398
+ if (header.origin !== "subagent" || header.parentSession === void 0) return false;
2399
+ const parent = ctx.sessions.get(header.parentSession);
2400
+ if (parent === void 0) return archived.includes(header.parentSession);
2401
+ header = parent.header;
2402
+ }
2403
+ return false;
2404
+ }
2405
+ //#endregion
2378
2406
  //#region lib/types/index.js
2379
2407
  /** Session Remote owner: cold reads, explicit Agent commands, and live control state. */
2380
2408
  var __runInitializers = function(thisArg, initializers, value) {
@@ -2484,6 +2512,7 @@ let SessionController = (() => {
2484
2512
  let _modelCatalog_decorators;
2485
2513
  let _canOpenWorkspacePath_decorators;
2486
2514
  let _openWorkspacePath_decorators;
2515
+ let _workspacePathApplications_decorators;
2487
2516
  let _rename_decorators;
2488
2517
  let _fork_decorators;
2489
2518
  let _prompt_decorators;
@@ -2492,6 +2521,7 @@ let SessionController = (() => {
2492
2521
  let _cancel_decorators;
2493
2522
  let _page_decorators;
2494
2523
  let _follow_decorators;
2524
+ let _projections_decorators;
2495
2525
  let _control_decorators;
2496
2526
  return class SessionController extends _classSuper {
2497
2527
  static {
@@ -2503,6 +2533,7 @@ let SessionController = (() => {
2503
2533
  _modelCatalog_decorators = [Remote("modelCatalog")];
2504
2534
  _canOpenWorkspacePath_decorators = [Remote];
2505
2535
  _openWorkspacePath_decorators = [Remote("openWorkspacePath")];
2536
+ _workspacePathApplications_decorators = [Remote("workspacePathApplications")];
2506
2537
  _rename_decorators = [Remote("rename")];
2507
2538
  _fork_decorators = [Remote("fork")];
2508
2539
  _prompt_decorators = [Remote("prompt")];
@@ -2511,6 +2542,7 @@ let SessionController = (() => {
2511
2542
  _cancel_decorators = [Remote("cancel")];
2512
2543
  _page_decorators = [Remote("page")];
2513
2544
  _follow_decorators = [Remote({ mode: "stream" })];
2545
+ _projections_decorators = [Remote("projections")];
2514
2546
  _control_decorators = [Remote({ mode: "stream" })];
2515
2547
  __esDecorate(this, null, _list_decorators, {
2516
2548
  kind: "method",
@@ -2589,6 +2621,17 @@ let SessionController = (() => {
2589
2621
  },
2590
2622
  metadata: _metadata
2591
2623
  }, null, _instanceExtraInitializers);
2624
+ __esDecorate(this, null, _workspacePathApplications_decorators, {
2625
+ kind: "method",
2626
+ name: "workspacePathApplications",
2627
+ static: false,
2628
+ private: false,
2629
+ access: {
2630
+ has: (obj) => "workspacePathApplications" in obj,
2631
+ get: (obj) => obj.workspacePathApplications
2632
+ },
2633
+ metadata: _metadata
2634
+ }, null, _instanceExtraInitializers);
2592
2635
  __esDecorate(this, null, _rename_decorators, {
2593
2636
  kind: "method",
2594
2637
  name: "rename",
@@ -2677,6 +2720,17 @@ let SessionController = (() => {
2677
2720
  },
2678
2721
  metadata: _metadata
2679
2722
  }, null, _instanceExtraInitializers);
2723
+ __esDecorate(this, null, _projections_decorators, {
2724
+ kind: "method",
2725
+ name: "projections",
2726
+ static: false,
2727
+ private: false,
2728
+ access: {
2729
+ has: (obj) => "projections" in obj,
2730
+ get: (obj) => obj.projections
2731
+ },
2732
+ metadata: _metadata
2733
+ }, null, _instanceExtraInitializers);
2680
2734
  __esDecorate(this, null, _control_decorators, {
2681
2735
  kind: "method",
2682
2736
  name: "control",
@@ -2700,6 +2754,7 @@ let SessionController = (() => {
2700
2754
  "agents",
2701
2755
  "attachments",
2702
2756
  "fileUploads",
2757
+ "fs",
2703
2758
  "llm",
2704
2759
  "sessions",
2705
2760
  "sessionProjections",
@@ -2714,6 +2769,8 @@ let SessionController = (() => {
2714
2769
  history;
2715
2770
  listState;
2716
2771
  openPath;
2772
+ fileApplications;
2773
+ openFileApplication;
2717
2774
  revealPath;
2718
2775
  canOpenPath;
2719
2776
  promotions = /* @__PURE__ */ new Set();
@@ -2740,18 +2797,26 @@ let SessionController = (() => {
2740
2797
  this.promote(observation);
2741
2798
  });
2742
2799
  this.listState = new ApiSessionList(ctx);
2743
- this.openPath = internals.openPath ?? openNativePath;
2800
+ this.fileApplications = internals.fileApplications ?? nativeFileApplications;
2801
+ this.openFileApplication = internals.openFileApplication ?? openNativeFileApplication;
2802
+ this.openPath = internals.openPath ?? openNativeAssociatedPath;
2744
2803
  this.revealPath = internals.revealPath ?? revealNativePath;
2745
2804
  this.canOpenPath = internals.canOpenPath ?? (() => config.nativeOpen ?? (internals.openPath !== void 0 || canOpenNativePath()));
2746
2805
  ctx.plugin(SessionFileReferences);
2747
2806
  ctx.plugin(SessionMediaReferences);
2748
2807
  ctx.plugin(SessionSkillCatalog);
2808
+ ctx.plugin(ArchivedSessionGate);
2749
2809
  ctx.on("session/created", (session) => {
2750
2810
  ctx.emit("api-session/added", this.listState.summaryFor(session));
2751
2811
  });
2752
2812
  ctx.on("session/disposed", (session) => {
2753
2813
  ctx.emit("api-session/removed", session.id);
2754
2814
  });
2815
+ const publishAgentAvailability = ({ agent }) => {
2816
+ if (ctx.sessions.get(agent.id) === agent.session) ctx.emit("api-session/added", this.listState.summaryFor(agent.session));
2817
+ };
2818
+ ctx.on("agent/created", publishAgentAvailability);
2819
+ ctx.on("agent/disposed", publishAgentAvailability);
2755
2820
  ctx.on("agent/status", ({ agent, status }) => {
2756
2821
  ctx.emit("api-session/status", agent.id, status === "running");
2757
2822
  });
@@ -2877,24 +2942,53 @@ let SessionController = (() => {
2877
2942
  };
2878
2943
  }
2879
2944
  /**
2880
- * Open one path prepared by a Session-aware caller on the Host desktop.
2945
+ * Verify one path through the composed filesystem and open it on the Host desktop.
2881
2946
  * @param request - path after best-effort Session workspace resolution.
2882
2947
  * @param signal - caller lifetime; abort terminates the native command.
2883
2948
  * @returns confirmation after the native opener accepts the path.
2884
- * @throws RemoteError when the request is invalid, cancelled, or the opener fails.
2949
+ * @throws RemoteError when the request is invalid, has no verified Host mapping, is cancelled, or the opener fails.
2885
2950
  */
2886
2951
  async openWorkspacePath(request, signal) {
2887
- if (request.path.length === 0) throw new RemoteError("gateway/bad-request", "session.openWorkspacePath requires a non-empty path", {});
2888
- signal.throwIfAborted();
2889
2952
  try {
2890
- if (request.action === "reveal") await this.revealPath(request.path, signal);
2891
- else await this.openPath(request.path, signal);
2953
+ const path = await this.verifyDesktopPath(request.path, signal);
2954
+ if (request.action === "reveal") await this.revealPath(path, signal);
2955
+ else if (request.application !== void 0) await this.openFileApplication(path, request.application, signal);
2956
+ else await this.openPath(path, signal);
2892
2957
  return { opened: true };
2893
2958
  } catch (error) {
2894
2959
  if (signal.aborted) throw new RemoteError("gateway/cancelled", "path open was aborted", {});
2895
- throw new RemoteError("gateway/internal", `path open failed: ${error instanceof Error ? error.message : String(error)}`, {});
2960
+ if (error instanceof RemoteError) throw error;
2961
+ throw new RemoteError("gateway/internal", "path open failed", {}, { cause: error });
2962
+ }
2963
+ }
2964
+ /**
2965
+ * Query current file handlers on the serving desktop without activating an Agent.
2966
+ * @param request - file path in Host filesystem syntax.
2967
+ * @param signal - caller lifetime, propagated to filesystem and desktop queries.
2968
+ * @returns OS application names, icons, and default selection; empty when desktop opening is unavailable.
2969
+ * @throws RemoteError when the path is invalid, the query is cancelled, or native discovery fails.
2970
+ */
2971
+ async workspacePathApplications(request, signal) {
2972
+ if (!this.canOpenPath()) return [];
2973
+ try {
2974
+ const path = await this.verifyDesktopPath(request.path, signal);
2975
+ return await this.fileApplications(path, signal);
2976
+ } catch (error) {
2977
+ if (signal.aborted) throw new RemoteError("gateway/cancelled", "application query was aborted", {});
2978
+ if (error instanceof RemoteError) throw error;
2979
+ throw new RemoteError("gateway/internal", "file application query failed", {}, { cause: error });
2896
2980
  }
2897
2981
  }
2982
+ async verifyDesktopPath(path, signal) {
2983
+ if (path.length === 0) throw new RemoteError("gateway/bad-request", "A non-empty file path is required", {});
2984
+ signal.throwIfAborted();
2985
+ const hostPath = resolve(path);
2986
+ const { fs } = this.ctx;
2987
+ const mapped = fs.processPathFromHostPath(hostPath);
2988
+ if (mapped === void 0 || fs.processPath(await fs.resolve(mapped, { signal })) !== hostPath) throw new RemoteError("gateway/bad-request", "Path has no verified Host path", {});
2989
+ signal.throwIfAborted();
2990
+ return hostPath;
2991
+ }
2898
2992
  /**
2899
2993
  * Rename one Session after explicitly resuming it.
2900
2994
  * @param request - Session identity and proposed title.
@@ -2904,8 +2998,10 @@ let SessionController = (() => {
2904
2998
  return this.commands.rename(request);
2905
2999
  }
2906
3000
  /**
2907
- * Fork one cold-readable completed-turn prefix into a new Session.
2908
- * @param request - source Session and optional event anchor.
3001
+ * Fork one cold-readable exact event prefix into a new Session. An omitted
3002
+ * boundary selects the latest completed-turn prefix; an open cut receives
3003
+ * synthetic fork closers.
3004
+ * @param request - source Session and optional exact inclusive event boundary.
2909
3005
  * @returns the new Session identity.
2910
3006
  */
2911
3007
  fork(request) {
@@ -2930,7 +3026,7 @@ let SessionController = (() => {
2930
3026
  return this.commands.attachment(request);
2931
3027
  }
2932
3028
  /**
2933
- * Mutate one still-pending queue occurrence on a live Agent.
3029
+ * Mutate one still-pending queue occurrence, resuming a cold Agent first.
2934
3030
  * @param request - Session, queue item, and requested mutation.
2935
3031
  * @returns acknowledgement that the queue mutation was applied.
2936
3032
  */
@@ -2965,6 +3061,41 @@ let SessionController = (() => {
2965
3061
  return this.history.follow(request, signal);
2966
3062
  }
2967
3063
  /**
3064
+ * Read all registered projections without activating an Agent.
3065
+ * @param request - Session whose current values are required.
3066
+ * @param signal - cancellation for the Session observation.
3067
+ * @returns complete baseline, or null when the Session does not exist.
3068
+ */
3069
+ async projections(request, signal) {
3070
+ const { sessionId } = request;
3071
+ if (sessionId.length === 0) throw new RemoteError("gateway/bad-request", "sessionId must not be empty", {});
3072
+ try {
3073
+ const env_2 = {
3074
+ stack: [],
3075
+ error: void 0,
3076
+ hasError: false
3077
+ };
3078
+ try {
3079
+ const projections = __addDisposableResource(env_2, await this.ctx.sessionQuery.observeSession(sessionId, { signal }), false).projections;
3080
+ if (projections === void 0) throw new RemoteError("session/projections-unavailable", "Session projections are unavailable", {});
3081
+ return {
3082
+ asOfSeq: projections.asOfSeq,
3083
+ values: projections.values
3084
+ };
3085
+ } catch (e_2) {
3086
+ env_2.error = e_2;
3087
+ env_2.hasError = true;
3088
+ } finally {
3089
+ __disposeResources(env_2);
3090
+ }
3091
+ } catch (error) {
3092
+ if (error instanceof SessionQueryError && error.code === "SESSION_QUERY_SESSION_NOT_FOUND") return null;
3093
+ if (signal.aborted || error instanceof SessionQueryError && error.code === "SESSION_QUERY_ABORTED") throw new RemoteError("gateway/cancelled", "Session projection read was cancelled", {}, { cause: error });
3094
+ if (error instanceof RemoteError) throw error;
3095
+ throw new RemoteError("gateway/internal", "Session projection read failed", {}, { cause: error });
3096
+ }
3097
+ }
3098
+ /**
2968
3099
  * Stream a complete live-control baseline followed by replacement frames.
2969
3100
  * @param signal - cancellation owned by the Remote stream carrier.
2970
3101
  * @returns one complete baseline followed by live replacement frames.