@deepseek-ai/dsh-api-session-controller 0.2.1-alpha.1 → 0.2.1-alpha.3

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
@@ -4,18 +4,18 @@ import z from "@deepseek-ai/schemastery";
4
4
  import { AssistantStreamAccumulator, ReasoningEffortId, assistantStreamChunks, createUserMessage, errorChain, freezeMessage } from "@deepseek-ai/dsh-llm";
5
5
  import { canOpenNativePath, nativeFileApplications, nativeFileManager, openNativeAssociatedPath, openNativeFileApplication, revealNativePath } from "@deepseek-ai/dsh-native-command";
6
6
  import { SessionQueryError } from "@deepseek-ai/dsh-session-query";
7
- import { Remote, RemoteError, TypertRemoteService, remoteErrorOf } from "@deepseek-ai/dsh-typert-protocol";
7
+ import { Remote, RemoteError, TypertRemoteService, principalOf, remoteErrorOf } from "@deepseek-ai/dsh-typert-protocol";
8
+ import { canonicalClientTimeZone } from "@deepseek-ai/dsh-util-time";
9
+ import { assertNever } from "@deepseek-ai/dsh-util-values";
10
+ import { randomUUID } from "node:crypto";
8
11
  import { mkdir } from "node:fs/promises";
9
12
  import { installModelSelection } from "@deepseek-ai/dsh-agent";
10
- import { credentialRef } from "@deepseek-ai/dsh-credentials";
11
- import { randomUUID } from "node:crypto";
12
13
  import { brandString } from "@deepseek-ai/dsh-brand";
14
+ import { credentialRef } from "@deepseek-ai/dsh-credentials";
13
15
  import { AttachmentError } from "@deepseek-ai/dsh-attachment";
14
16
  import { buildForkSeed } from "@deepseek-ai/dsh-session/fork";
15
17
  import { SessionLogOffset, SessionSeq, isAppendSurfaceEvent } from "@deepseek-ai/dsh-session";
16
18
  import { SessionTitleInvalidError } from "@deepseek-ai/dsh-session-title";
17
- import { canonicalClientTimeZone } from "@deepseek-ai/dsh-util-time";
18
- import { assertNever } from "@deepseek-ai/dsh-util-values";
19
19
  import { Deque } from "@deepseek-ai/dsh-deque";
20
20
  import { performance } from "node:perf_hooks";
21
21
  import { scheduler } from "node:timers/promises";
@@ -106,18 +106,18 @@ var ApiSessionCwdConflict = class extends Error {
106
106
  this.existingCwd = existingCwd;
107
107
  }
108
108
  };
109
- /** Explicit-id creation attempted to adopt a Session under another preset. */
109
+ /** Reuse of a Session named an Agent preset while the deployment composes no preset roster. */
110
110
  var ApiSessionPresetConflict = class extends Error {
111
111
  sessionId;
112
112
  requestedPreset;
113
- existingPreset;
114
- constructor(sessionId, requestedPreset, existingPreset) {
115
- super(existingPreset === void 0 ? `session "${sessionId}" records no agent preset and cannot be adopted under "${requestedPreset}"` : `session "${sessionId}" runs agent preset "${existingPreset}", not "${requestedPreset}"`);
113
+ constructor(sessionId, requestedPreset) {
114
+ super(`the deployment composes no agent preset roster, so session "${sessionId}" cannot run preset "${requestedPreset}"`);
116
115
  this.sessionId = sessionId;
117
116
  this.requestedPreset = requestedPreset;
118
- this.existingPreset = existingPreset;
119
117
  }
120
118
  };
119
+ /** A blank Session chosen for reuse started a turn before this Host acquired its writer. */
120
+ var ApiSessionStartedSinceObserved = class extends Error {};
121
121
  /**
122
122
  * Test whether generic Session routing must leave an identity to subagent routing.
123
123
  * @param ctx - Host context carrying the Agent ownership registry.
@@ -180,7 +180,7 @@ async function inspectApiSession(ctx, sessionId, signal) {
180
180
  var ApiSessionAgentController = class {
181
181
  ctx;
182
182
  resumes = /* @__PURE__ */ new Map();
183
- creations = /* @__PURE__ */ new Map();
183
+ claims = /* @__PURE__ */ new Map();
184
184
  selections = /* @__PURE__ */ new WeakMap();
185
185
  imageAdmissionChains = /* @__PURE__ */ new WeakMap();
186
186
  /** @param ctx - Host context carrying Agent, model, persistence, and Typert services. */
@@ -245,35 +245,26 @@ var ApiSessionAgentController = class {
245
245
  }
246
246
  }
247
247
  /**
248
- * Resolve one requested identity, creating or resuming it once.
249
- * @param sessionId - requested Session identity.
250
- * @param cwd - directory the Session must own.
251
- * @param checkPersistedIdentity - whether to inspect a cold identity before creation.
252
- * @param presetId - optional Agent preset the Session must own.
253
- * @returns the matching live ordinary Agent.
248
+ * Create a Session for a new conversation, reusing the named blank Session
249
+ * while it is still reusable. A reused Session runs the requested preset, or
250
+ * the configured default when the request names none; the preset it recorded
251
+ * is not resolved, and a change is logged as `agent-preset/selected` before
252
+ * the Agent is published. A named Session that has started a turn, fails the
253
+ * caller's placement rule, or is written by another owner stays unchanged,
254
+ * and a new Session is created instead. Requests naming one identity run in
255
+ * arrival order, including their preset resolution, and check the named
256
+ * Session's directory and ownership before resolving the preset.
257
+ * @param request - optional Session to reuse, directory, preset, and placement rule.
258
+ * @returns the reused or created Session and the preset it ran when this request finished.
259
+ * @throws ApiSessionCwdConflict when the named Session belongs to another directory.
260
+ * @throws ApiSessionSubagentOwnership when subagent routing owns the named Session.
261
+ * @throws RemoteError `agent-preset/not-found` or `agent-preset/invalid` for the requested or default preset.
262
+ * @throws ApiSessionPresetConflict when no roster is composed and the request names a preset for a reused Session.
254
263
  */
255
- async ensureSession(sessionId, cwd, checkPersistedIdentity, presetId) {
256
- let creation = this.creations.get(sessionId);
257
- if (creation === void 0) {
258
- creation = this.createOrAdopt(sessionId, cwd, checkPersistedIdentity, presetId).catch((error) => {
259
- const live = this.ctx.agents.get(sessionId);
260
- if (live !== void 0) {
261
- if (hasApiSessionSubagentOwner(this.ctx, live.session, live)) throw new ApiSessionSubagentOwnership(sessionId);
262
- return live;
263
- }
264
- const attached = this.ctx.sessions.get(sessionId);
265
- if (attached !== void 0 && hasApiSessionSubagentOwner(this.ctx, attached, void 0)) throw new ApiSessionSubagentOwnership(sessionId);
266
- throw error;
267
- }).finally(() => {
268
- this.creations.delete(sessionId);
269
- });
270
- this.creations.set(sessionId, creation);
271
- }
272
- const agent = await creation;
273
- if (hasApiSessionSubagentOwner(this.ctx, agent.session, agent)) throw new ApiSessionSubagentOwnership(sessionId);
274
- if (presetId !== void 0) this.assertPresetUnchanged(sessionId, presetId, this.presetForSession(agent.session));
275
- if (agent.session.header.cwd !== cwd) throw new ApiSessionCwdConflict(sessionId, cwd, agent.session.header.cwd);
276
- return agent;
264
+ async createSession(request) {
265
+ const named = request.sessionId;
266
+ if (named === void 0) return this.created(await this.createMinted(request.cwd, await this.composeAgent(request.agentPreset)));
267
+ return this.inOrder(named, async () => this.created(await this.claim(named, request)));
277
268
  }
278
269
  /**
279
270
  * Install or return the Session-local model selection used by prompt assembly.
@@ -411,44 +402,118 @@ var ApiSessionAgentController = class {
411
402
  setup: composition.setup
412
403
  })).agent;
413
404
  }
414
- async createOrAdopt(sessionId, cwd, checkPersistedIdentity, presetId) {
405
+ async inOrder(sessionId, operation) {
406
+ const result = (this.claims.get(sessionId) ?? Promise.resolve()).then(operation);
407
+ const settled = result.then(() => void 0, () => void 0);
408
+ this.claims.set(sessionId, settled);
409
+ try {
410
+ return await result;
411
+ } finally {
412
+ if (this.claims.get(sessionId) === settled) this.claims.delete(sessionId);
413
+ }
414
+ }
415
+ /** @returns the reused Session, a new Session under `sessionId` when none is stored, or a new Session under a minted identity. */
416
+ async claim(sessionId, request) {
415
417
  const attached = this.ctx.sessions.get(sessionId);
416
418
  const live = this.ctx.agents.get(sessionId);
417
419
  if (attached !== void 0 && hasApiSessionSubagentOwner(this.ctx, attached, live)) throw new ApiSessionSubagentOwnership(sessionId);
418
- if (live !== void 0) return live;
419
- if (checkPersistedIdentity) try {
420
+ if (live !== void 0) {
421
+ if (live.session.header.cwd !== request.cwd) throw new ApiSessionCwdConflict(live.id, request.cwd, live.session.header.cwd);
422
+ const composition = await this.composeAgent(request.agentPreset);
423
+ return await this.reuseLive(live, request, composition) ?? this.createMinted(request.cwd, composition);
424
+ }
425
+ let observation;
426
+ try {
427
+ observation = await this.ctx.sessionQuery.observeSession(sessionId);
428
+ } catch (error) {
429
+ if (!(error instanceof SessionQueryError) || error.code !== "SESSION_QUERY_SESSION_NOT_FOUND") throw error;
430
+ return this.createAgent(sessionId, request.cwd, await this.composeAgent(request.agentPreset));
431
+ }
432
+ let reusable;
433
+ {
420
434
  const env_3 = {
421
435
  stack: [],
422
436
  error: void 0,
423
437
  hasError: false
424
438
  };
425
439
  try {
426
- const observation = __addDisposableResource$4(env_3, await this.ctx.sessionQuery.observeSession(sessionId), false);
427
- if (hasApiSessionSubagentOwner(this.ctx, { header: observation.header }, void 0)) throw new ApiSessionSubagentOwnership(sessionId);
428
- if (observation.header.cwd !== cwd) throw new ApiSessionCwdConflict(sessionId, cwd, observation.header.cwd);
429
- const storedPreset = this.presetForObservation(observation);
430
- this.assertPresetUnchanged(sessionId, presetId, storedPreset);
431
- const composition = await this.composeAgent(storedPreset);
432
- return (await this.ctx.agents.resume({
433
- resumeSessionId: sessionId,
434
- agentOptions: this.agentOptions(),
435
- setup: composition.setup
436
- })).agent;
440
+ const observed = __addDisposableResource$4(env_3, observation, false);
441
+ if (hasApiSessionSubagentOwner(this.ctx, { header: observed.header }, void 0)) throw new ApiSessionSubagentOwnership(sessionId);
442
+ if (observed.header.cwd !== request.cwd) throw new ApiSessionCwdConflict(sessionId, request.cwd, observed.header.cwd);
443
+ reusable = request.reusable(sessionId) && !observed.events.some((event) => event.type === "turn/start");
437
444
  } catch (e_3) {
438
445
  env_3.error = e_3;
439
446
  env_3.hasError = true;
440
447
  } finally {
441
448
  __disposeResources$4(env_3);
442
449
  }
450
+ }
451
+ const composition = await this.composeAgent(request.agentPreset);
452
+ if (!reusable) return this.createMinted(request.cwd, composition);
453
+ this.assertPresetApplicable(sessionId, request.agentPreset);
454
+ try {
455
+ return (await this.ctx.agents.resume({
456
+ resumeSessionId: sessionId,
457
+ agentOptions: this.agentOptions(),
458
+ setup: this.reuseSetup(composition)
459
+ })).agent;
443
460
  } catch (error) {
444
- if (!(error instanceof SessionQueryError) || error.code !== "SESSION_QUERY_SESSION_NOT_FOUND") throw error;
461
+ if (error instanceof ApiSessionStartedSinceObserved) return this.createMinted(request.cwd, composition);
462
+ if (!(error instanceof Error) || error.name !== "SessionAlreadyOwnedError") throw error;
463
+ return this.ctx.agents.get(sessionId) === void 0 ? this.createMinted(request.cwd, composition) : this.claim(sessionId, request);
445
464
  }
465
+ }
466
+ /** @returns the live blank Session under the requested preset, or undefined when it cannot be reused. */
467
+ async reuseLive(live, request, composition) {
468
+ if (!request.reusable(live.id) || !this.isBlank(live.session)) return void 0;
469
+ this.assertPresetApplicable(live.id, request.agentPreset);
470
+ const current = this.presetForSession(live.session);
471
+ const presets = this.ctx.get("agentPresets");
472
+ const target = composition.agentPreset;
473
+ if (presets === void 0 || target === void 0 || target === current) return live;
474
+ try {
475
+ await presets.select(live, target);
476
+ } catch (error) {
477
+ if (remoteErrorOf(error)?.code !== "agent-preset/locked") throw error;
478
+ return;
479
+ }
480
+ return live;
481
+ }
482
+ /** Setup that re-checks blankness under the acquired writer, then records a preset change before publication. */
483
+ reuseSetup(composition) {
484
+ return async (agentCtx, agent) => {
485
+ if (!this.isBlank(agent.session)) throw new ApiSessionStartedSinceObserved();
486
+ const commit = await composition.setup(agentCtx, agent);
487
+ const target = composition.agentPreset;
488
+ if (target !== void 0 && this.presetForSession(agent.session) !== target) agent.session.append("agent-preset/selected", { agentPreset: target });
489
+ return commit;
490
+ };
491
+ }
492
+ /** Whether no turn has started, by the predicate the preset registry's `select` enforces. */
493
+ isBlank(session) {
494
+ const boundary = this.ctx.sessionProjections.stateOf(session, "turnBoundary");
495
+ return boundary === void 0 || boundary.openTurnStartSeq === null && boundary.lastTurn === 0;
496
+ }
497
+ /** Without a roster, reuse cannot apply a named preset. */
498
+ assertPresetApplicable(sessionId, requested) {
499
+ if (requested === void 0 || this.ctx.get("agentPresets") !== void 0) return;
500
+ throw new ApiSessionPresetConflict(sessionId, requested);
501
+ }
502
+ created(agent) {
503
+ return {
504
+ agent,
505
+ agentPreset: this.presetForSession(agent.session)
506
+ };
507
+ }
508
+ createMinted(cwd, composition) {
509
+ return this.createAgent(brandString(`session-${randomUUID()}`), cwd, composition);
510
+ }
511
+ async createAgent(sessionId, cwd, composition) {
446
512
  try {
447
513
  await mkdir(cwd, { recursive: true });
448
514
  } catch (error) {
449
515
  throw new Error(`failed to ensure project directory "${cwd}": ${String(error)}`, { cause: error });
450
516
  }
451
- const composition = await this.composeAgent(presetId);
452
517
  return (await this.ctx.agents.create({
453
518
  sessionId,
454
519
  agentOptions: this.agentOptions(),
@@ -478,10 +543,6 @@ var ApiSessionAgentController = class {
478
543
  if (observation.projections === void 0) throw new Error("api-session: Agent activation requires a projected Session observation");
479
544
  return observation.projections.values.agentPreset ?? void 0;
480
545
  }
481
- assertPresetUnchanged(sessionId, requested, existing) {
482
- if (requested === void 0 || requested === existing) return;
483
- throw new ApiSessionPresetConflict(sessionId, requested, existing);
484
- }
485
546
  };
486
547
  function agentModelSelection(selection) {
487
548
  return {
@@ -648,6 +709,11 @@ var __disposeResources$3 = (function(SuppressedError) {
648
709
  var e = new Error(message);
649
710
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
650
711
  });
712
+ /**
713
+ * Whether prompt content carries non-whitespace text or an attachment.
714
+ * @param content - submitted or edited prompt parts.
715
+ * @returns false for content the Agent must not receive.
716
+ */
651
717
  function hasPromptContent(content) {
652
718
  return content.some((part) => part.type !== "text" || part.text.trim().length > 0);
653
719
  }
@@ -681,25 +747,31 @@ var SessionCommandController = class {
681
747
  this.defaultCwd = defaultCwd;
682
748
  }
683
749
  /**
684
- * Create or idempotently adopt one ordinary Session.
685
- * @param request - requested identity, location, and Agent preset.
686
- * @returns the Session identity and resolved preset when configured.
750
+ * Create one ordinary Session for a new conversation, reusing the named
751
+ * blank Session while it is an unarchived member of the requested Workspace.
752
+ * @param request - optional blank Session to reuse, location, and Agent preset.
753
+ * @returns the created or reused Session identity and its preset when configured.
687
754
  */
688
755
  async create(request) {
689
756
  if (request.workspaceId !== void 0 && request.cwd !== void 0) throw new RemoteError("gateway/bad-request", "session.create accepts workspaceId or cwd, not both", {});
690
- const sessionId = request.sessionId ?? brandString(`session-${randomUUID()}`);
691
757
  let workspace;
692
758
  if (request.workspaceId !== void 0) {
693
759
  workspace = this.ctx.workspaceRegistry.get(request.workspaceId);
694
760
  if (workspace === void 0) throw new RemoteError("workspace/not-found", `workspace "${request.workspaceId}" not found`, { workspaceId: request.workspaceId });
695
761
  }
696
762
  const cwd = workspace?.path ?? request.cwd ?? this.defaultCwd;
697
- let adopted;
763
+ let created;
698
764
  try {
699
- adopted = await this.agents.ensureSession(sessionId, cwd, request.sessionId !== void 0, request.agentPreset);
765
+ created = await this.agents.createSession({
766
+ ...request.sessionId === void 0 ? {} : { sessionId: request.sessionId },
767
+ cwd,
768
+ ...request.agentPreset === void 0 ? {} : { agentPreset: request.agentPreset },
769
+ reusable: (sessionId) => !this.ctx.workspaceRegistry.archivedSessionIds.includes(sessionId) && (workspace === void 0 || workspace.sessionIds.includes(sessionId))
770
+ });
700
771
  } catch (error) {
701
- this.rejectCreation(sessionId, error);
772
+ this.rejectCreation(error);
702
773
  }
774
+ const sessionId = created.agent.id;
703
775
  if (workspace !== void 0) try {
704
776
  await workspace.attachSession(sessionId);
705
777
  } catch (error) {
@@ -708,10 +780,9 @@ var SessionCommandController = class {
708
780
  workspaceId: workspace.id
709
781
  });
710
782
  }
711
- const agentPreset = this.agents.presetForSession(adopted.session);
712
783
  return {
713
784
  sessionId,
714
- ...agentPreset === void 0 ? {} : { agentPreset }
785
+ ...created.agentPreset === void 0 ? {} : { agentPreset: created.agentPreset }
715
786
  };
716
787
  }
717
788
  /**
@@ -845,21 +916,15 @@ var SessionCommandController = class {
845
916
  }
846
917
  }
847
918
  /**
848
- * Reject empty content, then admit one prompt after Agent and attachment validation.
849
- * @param request - Session identity, prompt content, source metadata, and delivery mode.
919
+ * Admit one prompt after Agent and attachment validation; the Remote method
920
+ * has already validated the content and built the durable message source.
921
+ * @param request - Session identity, prompt content, and delivery mode.
922
+ * @param source - the `user-rpc` source recorded on the accepted message.
850
923
  * @returns acknowledgement that the Agent accepted the prompt.
851
924
  */
852
- async prompt(request) {
853
- if (!hasPromptContent(request.content)) throw new RemoteError("gateway/bad-request", "prompt content must include non-whitespace text or an attachment", {});
854
- const clientTimeZone = request.clientTimeZone === void 0 ? void 0 : canonicalClientTimeZone(request.clientTimeZone);
855
- if (request.clientTimeZone !== void 0 && clientTimeZone === void 0) throw new RemoteError("session/invalid-time-zone", "clientTimeZone must be UTC or a valid IANA Area/Location name", { value: request.clientTimeZone });
925
+ async prompt(request, source) {
856
926
  const agent = await this.resolveAgent(request.sessionId);
857
927
  if (hasPromptRequest(agent, request.requestId)) return { accepted: true };
858
- const source = {
859
- kind: "user",
860
- rpcId: request.requestId,
861
- ...clientTimeZone === void 0 ? {} : { clientTimeZone }
862
- };
863
928
  const hasImage = request.content.some((part) => part.type === "image");
864
929
  const admit = async () => {
865
930
  try {
@@ -1005,13 +1070,11 @@ var SessionCommandController = class {
1005
1070
  if ("error" in found) throw found.error;
1006
1071
  return found.agent;
1007
1072
  }
1008
- rejectCreation(sessionId, error) {
1073
+ rejectCreation(error) {
1009
1074
  if (remoteErrorOf(error) !== void 0) throw error;
1010
- if (error instanceof Error && error.name === "SessionAlreadyOwnedError") throw new RemoteError("session/writer-held", error.message, { sessionId });
1011
1075
  if (error instanceof ApiSessionPresetConflict) throw new RemoteError("agent-preset/conflict", error.message, {
1012
1076
  sessionId: error.sessionId,
1013
- requestedPreset: error.requestedPreset,
1014
- ...error.existingPreset === void 0 ? {} : { existingPreset: error.existingPreset }
1077
+ requestedPreset: error.requestedPreset
1015
1078
  });
1016
1079
  if (error instanceof ApiSessionCwdConflict) throw new RemoteError("session/conflict", error.message, {
1017
1080
  sessionId: error.sessionId,
@@ -1019,7 +1082,7 @@ var SessionCommandController = class {
1019
1082
  ...error.existingCwd === void 0 ? {} : { existingCwd: error.existingCwd }
1020
1083
  });
1021
1084
  if (error instanceof ApiSessionSubagentOwnership) throw apiSessionSubagentOwnershipError(error.sessionId);
1022
- throw new RemoteError("gateway/internal", `failed to create session "${sessionId}": ${String(error)}`, {});
1085
+ throw new RemoteError("gateway/internal", `failed to create session: ${String(error)}`, {});
1023
1086
  }
1024
1087
  async readSessionState(sessionId) {
1025
1088
  const attached = this.ctx.sessions.get(sessionId);
@@ -1881,6 +1944,7 @@ var ApiSessionList = class {
1881
1944
  sessionId: session.id,
1882
1945
  updatedAt: updatedAt(session.header, metadata),
1883
1946
  agentAvailable: this.ctx.agents.get(session.id)?.session === session,
1947
+ formatStatus: "current",
1884
1948
  running: this.ctx.agents.get(session.id)?.status === "running",
1885
1949
  blank: metadata?.blank ?? session.seq === 0,
1886
1950
  ...listFields(session.header),
@@ -1903,16 +1967,16 @@ var ApiSessionList = class {
1903
1967
  signal?.throwIfAborted();
1904
1968
  const live = this.ctx.sessions.get(record.header.id);
1905
1969
  if (live !== void 0) items.push(this.summaryFor(live));
1906
- else if (record.header.cwd !== void 0) cold.push(record.header);
1970
+ else if (record.header.cwd !== void 0) cold.push(record);
1907
1971
  if (performance.now() >= yieldDeadline) {
1908
1972
  await scheduler.yield();
1909
1973
  signal?.throwIfAborted();
1910
1974
  yieldDeadline = performance.now() + this.workSliceMs;
1911
1975
  }
1912
1976
  }
1913
- for (const header of cold) {
1977
+ for (const record of cold) {
1914
1978
  signal?.throwIfAborted();
1915
- items.push(this.summarizeCold(header));
1979
+ items.push(this.summarizeCold(record));
1916
1980
  if (performance.now() >= yieldDeadline) {
1917
1981
  await scheduler.yield();
1918
1982
  signal?.throwIfAborted();
@@ -1923,13 +1987,14 @@ var ApiSessionList = class {
1923
1987
  items.sort((left, right) => right.updatedAt - left.updatedAt);
1924
1988
  return items;
1925
1989
  }
1926
- summarizeCold(header) {
1990
+ summarizeCold({ header, formatStatus }) {
1927
1991
  const projections = this.projectionsFor(header, void 0);
1928
1992
  const metadata = projections?.values.sessionListMetadata;
1929
1993
  return {
1930
1994
  sessionId: header.id,
1931
1995
  updatedAt: updatedAt(header, metadata),
1932
1996
  agentAvailable: false,
1997
+ ...formatStatus === void 0 ? {} : { formatStatus },
1933
1998
  running: false,
1934
1999
  blank: metadata?.blank ?? false,
1935
2000
  ...listFields(header),
@@ -2261,7 +2326,8 @@ let SessionSkillCatalog = (() => {
2261
2326
  static inject = [
2262
2327
  "agents",
2263
2328
  "sessionQuery",
2264
- "typert"
2329
+ "typert",
2330
+ "workingDirectory"
2265
2331
  ];
2266
2332
  /** @param ctx - Host context carrying Session reads and optional skill/preset services. */
2267
2333
  constructor(ctx) {
@@ -2294,8 +2360,10 @@ let SessionSkillCatalog = (() => {
2294
2360
  try {
2295
2361
  const observation = __addDisposableResource$1(env_2, await this.ctx.sessionQuery.observeSession(sessionId), false);
2296
2362
  if (observation.projections === void 0) throw new Error("skill catalog requires a projected Session observation");
2297
- cwd = observation.header.cwd;
2363
+ cwd = observation.projections.values.workingDirectory ?? observation.header.cwd ?? this.ctx.workingDirectory.defaultDirectory;
2298
2364
  agentPreset = observation.projections.values.agentPreset ?? void 0;
2365
+ const live = this.ctx.agents.get(sessionId);
2366
+ if (live !== void 0) cwd = await this.ctx.workingDirectory.ensure(live, signal);
2299
2367
  } catch (e_1) {
2300
2368
  env_2.error = e_1;
2301
2369
  env_2.hasError = true;
@@ -2306,7 +2374,6 @@ let SessionSkillCatalog = (() => {
2306
2374
  if (error instanceof SessionQueryError && error.code === "SESSION_QUERY_SESSION_NOT_FOUND") throw new RemoteError("session/not-found", `session "${sessionId}" not found`, { sessionId });
2307
2375
  throw new RemoteError("gateway/internal", `session "${sessionId}" could not be inspected: ${String(error)}`, {});
2308
2376
  }
2309
- if (cwd === void 0) throw new RemoteError("gateway/internal", `session "${sessionId}" has no project cwd`, {});
2310
2377
  const live = this.ctx.agents.get(sessionId);
2311
2378
  const presets = this.ctx.get("agentPresets");
2312
2379
  const skillRegistry = (live === void 0 ? void 0 : presets?.serviceFor(live, "skills")) ?? this.ctx.get("skills");
@@ -2571,6 +2638,15 @@ var __disposeResources = (function(SuppressedError) {
2571
2638
  var e = new Error(message);
2572
2639
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
2573
2640
  });
2641
+ /**
2642
+ * Read the Session resource a history address names: the Session itself, or
2643
+ * the child Session whose transcript a subagent address reads.
2644
+ * @param request - history request carrying the durable address.
2645
+ * @returns the Session whose access the read requires.
2646
+ */
2647
+ function addressedSession(request) {
2648
+ return request.address.kind === "session" ? request.address.sessionId : request.address.childSessionId;
2649
+ }
2574
2650
  /** Host service backing the generated `ctx.remote.session` namespace. */
2575
2651
  let SessionController = (() => {
2576
2652
  let _classSuper = TypertRemoteService;
@@ -2612,8 +2688,14 @@ let SessionController = (() => {
2612
2688
  _attachment_decorators = [Remote("attachment")];
2613
2689
  _updateQueue_decorators = [Remote("updateQueue")];
2614
2690
  _cancel_decorators = [Remote("cancel")];
2615
- _page_decorators = [Remote("page")];
2616
- _follow_decorators = [Remote({ mode: "stream" })];
2691
+ _page_decorators = [Remote({
2692
+ name: "page",
2693
+ resources: { session: addressedSession }
2694
+ })];
2695
+ _follow_decorators = [Remote({
2696
+ mode: "stream",
2697
+ resources: { session: addressedSession }
2698
+ })];
2617
2699
  _projections_decorators = [Remote("projections")];
2618
2700
  _control_decorators = [Remote({ mode: "stream" })];
2619
2701
  __esDecorate(this, null, _list_decorators, {
@@ -2839,6 +2921,7 @@ let SessionController = (() => {
2839
2921
  "fileUploads",
2840
2922
  "fs",
2841
2923
  "llm",
2924
+ "remoteAccess",
2842
2925
  "sessions",
2843
2926
  "sessionProjections",
2844
2927
  "sessionQuery",
@@ -2867,6 +2950,7 @@ let SessionController = (() => {
2867
2950
  */
2868
2951
  constructor(ctx, config, internals = {}) {
2869
2952
  super(ctx, "sessionController", { namespace: "session" });
2953
+ ctx.typert.resources.register("session", { field: "sessionId" });
2870
2954
  const resolved = SessionController.Config(config);
2871
2955
  installModelSelectionProjection(ctx);
2872
2956
  this.agents = new ApiSessionAgentController(ctx);
@@ -2975,21 +3059,33 @@ let SessionController = (() => {
2975
3059
  * @returns visible Session summaries ordered by activity.
2976
3060
  */
2977
3061
  async list(_request, signal) {
2978
- return { items: await this.listState.list(signal) };
3062
+ return { items: (await this.listState.list(signal)).filter((item) => this.ctx.remoteAccess.allows({
3063
+ kind: "session",
3064
+ id: item.sessionId
3065
+ })) };
2979
3066
  }
2980
3067
  /**
2981
- * Search visible Session content without resuming an Agent.
3068
+ * Search visible Session content without resuming an Agent. A non-owner
3069
+ * receives only the hits whose Session `allows()` admits; `hasMore`
3070
+ * describes the bounded result before that filtering.
2982
3071
  * @param request - literal message-content query.
2983
3072
  * @param signal - cancellation for list and search reads.
2984
3073
  * @returns authorized bounded Session search results.
2985
3074
  */
2986
- search(request, signal) {
2987
- return this.listState.search(request.query, signal);
3075
+ async search(request, signal) {
3076
+ const result = await this.listState.search(request.query, signal);
3077
+ return {
3078
+ ...result,
3079
+ items: result.items.filter((item) => this.ctx.remoteAccess.allows({
3080
+ kind: "session",
3081
+ id: item.sessionId
3082
+ }))
3083
+ };
2988
3084
  }
2989
3085
  /**
2990
- * Create or idempotently adopt one ordinary Session.
2991
- * @param request - requested identity, location, and Agent preset.
2992
- * @returns the Session identity and resolved preset when configured.
3086
+ * Create one ordinary Session for a new conversation, reusing the named blank Session while it is reusable.
3087
+ * @param request - optional blank Session to reuse, location, and Agent preset.
3088
+ * @returns the created or reused Session identity and its preset when configured.
2993
3089
  */
2994
3090
  create(request) {
2995
3091
  return this.commands.create(request);
@@ -3104,10 +3200,13 @@ let SessionController = (() => {
3104
3200
  * Fork one cold-readable exact event prefix into a new Session. An omitted
3105
3201
  * boundary selects the latest completed-turn prefix; an open cut receives
3106
3202
  * synthetic fork closers.
3107
- * @param request - source Session and optional exact inclusive event boundary.
3203
+ * @param request - source Session, optional exact inclusive event boundary, and migration preflight choice.
3108
3204
  * @returns the new Session identity.
3109
3205
  */
3110
- fork(request) {
3206
+ async fork(request) {
3207
+ if (request.allowMigration === false && this.ctx.sessions.get(request.sessionId) === void 0) {
3208
+ 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 });
3209
+ }
3111
3210
  return this.commands.fork(request);
3112
3211
  }
3113
3212
  /**
@@ -3116,9 +3215,19 @@ let SessionController = (() => {
3116
3215
  * @param signal - caller cancellation before prompt admission begins.
3117
3216
  * @returns acknowledgement that the Agent accepted the prompt.
3118
3217
  */
3119
- prompt(request, signal) {
3218
+ async prompt(request, signal) {
3120
3219
  signal.throwIfAborted();
3121
- return this.commands.prompt(request);
3220
+ if (!hasPromptContent(request.content)) throw new RemoteError("gateway/bad-request", "prompt content must include non-whitespace text or an attachment", {});
3221
+ const clientTimeZone = request.clientTimeZone === void 0 ? void 0 : canonicalClientTimeZone(request.clientTimeZone);
3222
+ if (request.clientTimeZone !== void 0 && clientTimeZone === void 0) throw new RemoteError("session/invalid-time-zone", "clientTimeZone must be UTC or a valid IANA Area/Location name", { value: request.clientTimeZone });
3223
+ const principal = principalOf(this.ctx.invocation);
3224
+ const source = {
3225
+ kind: "user",
3226
+ rpcId: request.requestId,
3227
+ ...clientTimeZone === void 0 ? {} : { clientTimeZone },
3228
+ ...principal === void 0 ? {} : { principal }
3229
+ };
3230
+ return this.commands.prompt(request, source);
3122
3231
  }
3123
3232
  /**
3124
3233
  * Read one image proven reachable from the addressed Session log.
@@ -3164,10 +3273,10 @@ let SessionController = (() => {
3164
3273
  return this.history.follow(request, signal);
3165
3274
  }
3166
3275
  /**
3167
- * Read all registered projections without activating an Agent.
3276
+ * Read exact projections, returning cached hints only when migration is required.
3168
3277
  * @param request - Session whose current values are required.
3169
- * @param signal - cancellation for the Session observation.
3170
- * @returns complete baseline, or null when the Session does not exist.
3278
+ * @param signal - cancellation for the Session read.
3279
+ * @returns a sequenced baseline, cached hints when migration is deferred, or null when absent.
3171
3280
  */
3172
3281
  async projections(request, signal) {
3173
3282
  const { sessionId } = request;
@@ -3179,9 +3288,22 @@ let SessionController = (() => {
3179
3288
  hasError: false
3180
3289
  };
3181
3290
  try {
3291
+ signal.throwIfAborted();
3292
+ const live = this.liveProjections(sessionId);
3293
+ if (live !== void 0) return live;
3294
+ const stored = await this.ctx.get("sessionPersistence")?.stat(sessionId, { signal });
3295
+ signal.throwIfAborted();
3296
+ const attached = this.liveProjections(sessionId);
3297
+ if (attached !== void 0) return attached;
3298
+ if (stored === void 0) return null;
3299
+ if (stored.formatStatus === "migration-required") return {
3300
+ kind: "migration-required",
3301
+ values: { ...(this.ctx.get("sessionProjectionCache")?.cachedSnapshot(stored.header))?.values }
3302
+ };
3182
3303
  const projections = __addDisposableResource(env_2, await this.ctx.sessionQuery.observeSession(sessionId, { signal }), false).projections;
3183
3304
  if (projections === void 0) throw new RemoteError("session/projections-unavailable", "Session projections are unavailable", {});
3184
3305
  return {
3306
+ kind: "sequenced",
3185
3307
  asOfSeq: projections.asOfSeq,
3186
3308
  values: projections.values
3187
3309
  };
@@ -3192,19 +3314,52 @@ let SessionController = (() => {
3192
3314
  __disposeResources(env_2);
3193
3315
  }
3194
3316
  } catch (error) {
3195
- if (error instanceof SessionQueryError && error.code === "SESSION_QUERY_SESSION_NOT_FOUND") return null;
3317
+ if (error instanceof SessionQueryError && error.code === "SESSION_QUERY_SESSION_NOT_FOUND") return this.liveProjections(sessionId) ?? null;
3196
3318
  if (signal.aborted || error instanceof SessionQueryError && error.code === "SESSION_QUERY_ABORTED") throw new RemoteError("gateway/cancelled", "Session projection read was cancelled", {}, { cause: error });
3197
3319
  if (error instanceof RemoteError) throw error;
3198
3320
  throw new RemoteError("gateway/internal", "Session projection read failed", {}, { cause: error });
3199
3321
  }
3200
3322
  }
3323
+ liveProjections(sessionId) {
3324
+ const session = this.ctx.sessions.get(sessionId);
3325
+ if (session === void 0) return void 0;
3326
+ const snapshot = this.ctx.sessionProjections.snapshot(session);
3327
+ return {
3328
+ kind: "sequenced",
3329
+ asOfSeq: snapshot.asOfSeq,
3330
+ values: snapshot.values
3331
+ };
3332
+ }
3201
3333
  /**
3202
3334
  * Stream a complete live-control baseline followed by replacement frames.
3203
3335
  * @param signal - cancellation owned by the Remote stream carrier.
3204
3336
  * @returns one complete baseline followed by live replacement frames.
3205
3337
  */
3206
- control(signal) {
3207
- return this.controlState.control(signal);
3338
+ async *control(signal) {
3339
+ for await (const frame of this.controlState.control(signal)) switch (frame.type) {
3340
+ case "baseline": {
3341
+ const projections = Object.fromEntries(Object.entries(frame.value.projections).filter(([sessionId]) => this.ctx.remoteAccess.allows({
3342
+ kind: "session",
3343
+ id: sessionId
3344
+ })));
3345
+ yield {
3346
+ type: "baseline",
3347
+ value: {
3348
+ ...frame.value,
3349
+ projections
3350
+ }
3351
+ };
3352
+ break;
3353
+ }
3354
+ case "projection":
3355
+ if (this.ctx.remoteAccess.allows({
3356
+ kind: "session",
3357
+ id: frame.sessionId
3358
+ })) yield frame;
3359
+ break;
3360
+ /* v8 ignore next -- closed control-frame union. */
3361
+ default: assertNever(frame);
3362
+ }
3208
3363
  }
3209
3364
  };
3210
3365
  })();