@deepseek-ai/dsh-api-session-controller 0.2.0-rc.2 → 0.2.1-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.
package/lib/index.js CHANGED
@@ -17,6 +17,8 @@ import { SessionTitleInvalidError } from "@deepseek-ai/dsh-session-title";
17
17
  import { canonicalClientTimeZone } from "@deepseek-ai/dsh-util-time";
18
18
  import { assertNever } from "@deepseek-ai/dsh-util-values";
19
19
  import { Deque } from "@deepseek-ai/dsh-deque";
20
+ import { performance } from "node:perf_hooks";
21
+ import { scheduler } from "node:timers/promises";
20
22
  import { z as z$1 } from "zod";
21
23
  import { isUserInvocable } from "@deepseek-ai/dsh-skill";
22
24
  import { FsError } from "@deepseek-ai/dsh-fs";
@@ -1831,9 +1833,14 @@ function truncateUnicodeCodePoints(value, maximum) {
1831
1833
  /** Owns list projection registration, bounded cold summaries, and authorized search. */
1832
1834
  var ApiSessionList = class {
1833
1835
  ctx;
1834
- /** @param ctx - Host context carrying Session, query, persistence, and projection services. */
1835
- constructor(ctx) {
1836
+ workSliceMs;
1837
+ /**
1838
+ * @param ctx - Host context carrying Session, query, persistence, and projection services.
1839
+ * @param workSliceMs - Resolved positive integral list-work budget in milliseconds.
1840
+ */
1841
+ constructor(ctx, workSliceMs) {
1836
1842
  this.ctx = ctx;
1843
+ this.workSliceMs = workSliceMs;
1837
1844
  ctx.sessionProjections.register({
1838
1845
  key: "sessionListMetadata",
1839
1846
  stateSchema: sessionListMetadataSchema,
@@ -1874,6 +1881,7 @@ var ApiSessionList = class {
1874
1881
  sessionId: session.id,
1875
1882
  updatedAt: updatedAt(session.header, metadata),
1876
1883
  agentAvailable: this.ctx.agents.get(session.id)?.session === session,
1884
+ formatStatus: "current",
1877
1885
  running: this.ctx.agents.get(session.id)?.status === "running",
1878
1886
  blank: metadata?.blank ?? session.seq === 0,
1879
1887
  ...listFields(session.header),
@@ -1882,7 +1890,7 @@ var ApiSessionList = class {
1882
1890
  }
1883
1891
  /**
1884
1892
  * Read every visible attached and persisted Session without activating an Agent.
1885
- * @param signal - optional cancellation for persistence reads.
1893
+ * @param signal - optional cancellation for persistence reads and summary generation.
1886
1894
  * @returns visible Session summaries ordered by activity.
1887
1895
  */
1888
1896
  async list(signal) {
@@ -1891,26 +1899,39 @@ var ApiSessionList = class {
1891
1899
  signal?.throwIfAborted();
1892
1900
  const items = [];
1893
1901
  const cold = [];
1902
+ let yieldDeadline = performance.now() + this.workSliceMs;
1894
1903
  for (const record of records) {
1904
+ signal?.throwIfAborted();
1895
1905
  const live = this.ctx.sessions.get(record.header.id);
1896
- if (live !== void 0) {
1897
- items.push(this.summaryFor(live));
1898
- continue;
1906
+ if (live !== void 0) items.push(this.summaryFor(live));
1907
+ else if (record.header.cwd !== void 0) cold.push(record);
1908
+ if (performance.now() >= yieldDeadline) {
1909
+ await scheduler.yield();
1910
+ signal?.throwIfAborted();
1911
+ yieldDeadline = performance.now() + this.workSliceMs;
1912
+ }
1913
+ }
1914
+ for (const record of cold) {
1915
+ signal?.throwIfAborted();
1916
+ items.push(this.summarizeCold(record));
1917
+ if (performance.now() >= yieldDeadline) {
1918
+ await scheduler.yield();
1919
+ signal?.throwIfAborted();
1920
+ yieldDeadline = performance.now() + this.workSliceMs;
1899
1921
  }
1900
- if (record.header.cwd === void 0) continue;
1901
- cold.push(record.header);
1902
1922
  }
1903
- for (const header of cold) items.push(this.summarizeCold(header));
1923
+ signal?.throwIfAborted();
1904
1924
  items.sort((left, right) => right.updatedAt - left.updatedAt);
1905
1925
  return items;
1906
1926
  }
1907
- summarizeCold(header) {
1927
+ summarizeCold({ header, formatStatus }) {
1908
1928
  const projections = this.projectionsFor(header, void 0);
1909
1929
  const metadata = projections?.values.sessionListMetadata;
1910
1930
  return {
1911
1931
  sessionId: header.id,
1912
1932
  updatedAt: updatedAt(header, metadata),
1913
1933
  agentAvailable: false,
1934
+ ...formatStatus === void 0 ? {} : { formatStatus },
1914
1935
  running: false,
1915
1936
  blank: metadata?.blank ?? false,
1916
1937
  ...listFields(header),
@@ -2242,7 +2263,8 @@ let SessionSkillCatalog = (() => {
2242
2263
  static inject = [
2243
2264
  "agents",
2244
2265
  "sessionQuery",
2245
- "typert"
2266
+ "typert",
2267
+ "workingDirectory"
2246
2268
  ];
2247
2269
  /** @param ctx - Host context carrying Session reads and optional skill/preset services. */
2248
2270
  constructor(ctx) {
@@ -2275,8 +2297,10 @@ let SessionSkillCatalog = (() => {
2275
2297
  try {
2276
2298
  const observation = __addDisposableResource$1(env_2, await this.ctx.sessionQuery.observeSession(sessionId), false);
2277
2299
  if (observation.projections === void 0) throw new Error("skill catalog requires a projected Session observation");
2278
- cwd = observation.header.cwd;
2300
+ cwd = observation.projections.values.workingDirectory ?? observation.header.cwd ?? this.ctx.workingDirectory.defaultDirectory;
2279
2301
  agentPreset = observation.projections.values.agentPreset ?? void 0;
2302
+ const live = this.ctx.agents.get(sessionId);
2303
+ if (live !== void 0) cwd = await this.ctx.workingDirectory.ensure(live, signal);
2280
2304
  } catch (e_1) {
2281
2305
  env_2.error = e_1;
2282
2306
  env_2.hasError = true;
@@ -2287,7 +2311,6 @@ let SessionSkillCatalog = (() => {
2287
2311
  if (error instanceof SessionQueryError && error.code === "SESSION_QUERY_SESSION_NOT_FOUND") throw new RemoteError("session/not-found", `session "${sessionId}" not found`, { sessionId });
2288
2312
  throw new RemoteError("gateway/internal", `session "${sessionId}" could not be inspected: ${String(error)}`, {});
2289
2313
  }
2290
- if (cwd === void 0) throw new RemoteError("gateway/internal", `session "${sessionId}" has no project cwd`, {});
2291
2314
  const live = this.ctx.agents.get(sessionId);
2292
2315
  const presets = this.ctx.get("agentPresets");
2293
2316
  const skillRegistry = (live === void 0 ? void 0 : presets?.serviceFor(live, "skills")) ?? this.ctx.get("skills");
@@ -2826,7 +2849,10 @@ let SessionController = (() => {
2826
2849
  "typert",
2827
2850
  "workspaceRegistry"
2828
2851
  ];
2829
- static Config = z.object({ nativeOpen: z.boolean() });
2852
+ static Config = z.object({
2853
+ nativeOpen: z.boolean(),
2854
+ listWorkSliceMs: z.natural().min(1).default(16)
2855
+ });
2830
2856
  agents = __runInitializers(this, _instanceExtraInitializers);
2831
2857
  commands;
2832
2858
  controlState;
@@ -2840,11 +2866,12 @@ let SessionController = (() => {
2840
2866
  promotions = /* @__PURE__ */ new Set();
2841
2867
  /**
2842
2868
  * @param ctx - Host context containing the Session capability assembly.
2843
- * @param config - native-opener deployment policy.
2869
+ * @param config - native-opener and list-scheduling deployment policy.
2844
2870
  * @param internals - host integrations replaceable by direct unit tests.
2845
2871
  */
2846
2872
  constructor(ctx, config, internals = {}) {
2847
2873
  super(ctx, "sessionController", { namespace: "session" });
2874
+ const resolved = SessionController.Config(config);
2848
2875
  installModelSelectionProjection(ctx);
2849
2876
  this.agents = new ApiSessionAgentController(ctx);
2850
2877
  this.commands = new SessionCommandController(ctx, this.agents, process.cwd());
@@ -2860,7 +2887,7 @@ let SessionController = (() => {
2860
2887
  this.history = new SessionHistoryController(ctx, (observation) => {
2861
2888
  this.promote(observation);
2862
2889
  });
2863
- this.listState = new ApiSessionList(ctx);
2890
+ this.listState = new ApiSessionList(ctx, resolved.listWorkSliceMs);
2864
2891
  this.fileApplications = internals.fileApplications ?? nativeFileApplications;
2865
2892
  this.openFileApplication = internals.openFileApplication ?? openNativeFileApplication;
2866
2893
  this.openPath = internals.openPath ?? openNativeAssociatedPath;
@@ -2948,7 +2975,7 @@ let SessionController = (() => {
2948
2975
  /**
2949
2976
  * Read all visible Session rows without resuming an Agent.
2950
2977
  * @param _request - reserved empty list request.
2951
- * @param signal - cancellation for persistence reads.
2978
+ * @param signal - cancellation for persistence reads and summary generation.
2952
2979
  * @returns visible Session summaries ordered by activity.
2953
2980
  */
2954
2981
  async list(_request, signal) {
@@ -3081,10 +3108,13 @@ let SessionController = (() => {
3081
3108
  * Fork one cold-readable exact event prefix into a new Session. An omitted
3082
3109
  * boundary selects the latest completed-turn prefix; an open cut receives
3083
3110
  * synthetic fork closers.
3084
- * @param request - source Session and optional exact inclusive event boundary.
3111
+ * @param request - source Session, optional exact inclusive event boundary, and migration preflight choice.
3085
3112
  * @returns the new Session identity.
3086
3113
  */
3087
- fork(request) {
3114
+ async fork(request) {
3115
+ if (request.allowMigration === false && this.ctx.sessions.get(request.sessionId) === void 0) {
3116
+ if ((await this.ctx.get("sessionPersistence")?.stat(request.sessionId))?.formatStatus === "migration-required" && this.ctx.sessions.get(request.sessionId) === void 0) throw new RemoteError("session/migration-required", "Open the source Session to complete migration before forking", { sessionId: request.sessionId });
3117
+ }
3088
3118
  return this.commands.fork(request);
3089
3119
  }
3090
3120
  /**
@@ -3141,10 +3171,10 @@ let SessionController = (() => {
3141
3171
  return this.history.follow(request, signal);
3142
3172
  }
3143
3173
  /**
3144
- * Read all registered projections without activating an Agent.
3174
+ * Read exact projections, returning cached hints only when migration is required.
3145
3175
  * @param request - Session whose current values are required.
3146
- * @param signal - cancellation for the Session observation.
3147
- * @returns complete baseline, or null when the Session does not exist.
3176
+ * @param signal - cancellation for the Session read.
3177
+ * @returns a sequenced baseline, cached hints when migration is deferred, or null when absent.
3148
3178
  */
3149
3179
  async projections(request, signal) {
3150
3180
  const { sessionId } = request;
@@ -3156,9 +3186,22 @@ let SessionController = (() => {
3156
3186
  hasError: false
3157
3187
  };
3158
3188
  try {
3189
+ signal.throwIfAborted();
3190
+ const live = this.liveProjections(sessionId);
3191
+ if (live !== void 0) return live;
3192
+ const stored = await this.ctx.get("sessionPersistence")?.stat(sessionId, { signal });
3193
+ signal.throwIfAborted();
3194
+ const attached = this.liveProjections(sessionId);
3195
+ if (attached !== void 0) return attached;
3196
+ if (stored === void 0) return null;
3197
+ if (stored.formatStatus === "migration-required") return {
3198
+ kind: "migration-required",
3199
+ values: { ...(this.ctx.get("sessionProjectionCache")?.cachedSnapshot(stored.header))?.values }
3200
+ };
3159
3201
  const projections = __addDisposableResource(env_2, await this.ctx.sessionQuery.observeSession(sessionId, { signal }), false).projections;
3160
3202
  if (projections === void 0) throw new RemoteError("session/projections-unavailable", "Session projections are unavailable", {});
3161
3203
  return {
3204
+ kind: "sequenced",
3162
3205
  asOfSeq: projections.asOfSeq,
3163
3206
  values: projections.values
3164
3207
  };
@@ -3169,12 +3212,22 @@ let SessionController = (() => {
3169
3212
  __disposeResources(env_2);
3170
3213
  }
3171
3214
  } catch (error) {
3172
- if (error instanceof SessionQueryError && error.code === "SESSION_QUERY_SESSION_NOT_FOUND") return null;
3215
+ if (error instanceof SessionQueryError && error.code === "SESSION_QUERY_SESSION_NOT_FOUND") return this.liveProjections(sessionId) ?? null;
3173
3216
  if (signal.aborted || error instanceof SessionQueryError && error.code === "SESSION_QUERY_ABORTED") throw new RemoteError("gateway/cancelled", "Session projection read was cancelled", {}, { cause: error });
3174
3217
  if (error instanceof RemoteError) throw error;
3175
3218
  throw new RemoteError("gateway/internal", "Session projection read failed", {}, { cause: error });
3176
3219
  }
3177
3220
  }
3221
+ liveProjections(sessionId) {
3222
+ const session = this.ctx.sessions.get(sessionId);
3223
+ if (session === void 0) return void 0;
3224
+ const snapshot = this.ctx.sessionProjections.snapshot(session);
3225
+ return {
3226
+ kind: "sequenced",
3227
+ asOfSeq: snapshot.asOfSeq,
3228
+ values: snapshot.values
3229
+ };
3230
+ }
3178
3231
  /**
3179
3232
  * Stream a complete live-control baseline followed by replacement frames.
3180
3233
  * @param signal - cancellation owned by the Remote stream carrier.