dsh-pause-session 0.2.0 → 0.2.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.
package/lib/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { z } from "zod";
2
2
  import { SessionId, deriveEventMessage, foldRequestHeader, foldSurface, interruptedTurnClosers } from "@deepseek-ai/dsh-session";
3
+ import { randomUUID } from "node:crypto";
3
4
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
4
- import { applyChildComposition, foldSubagentDescriptor } from "@deepseek-ai/dsh-subagent";
5
- import { Remote, RemoteError, TypertRemoteService, principalOf } from "@deepseek-ai/dsh-typert-protocol";
5
+ import { Remote, RemoteError, TypertRemoteService, principalOf, remoteErrorOf } from "@deepseek-ai/dsh-typert-protocol";
6
+ import { foldSubagentDescriptor } from "@deepseek-ai/dsh-subagent";
6
7
  import { DomainError, defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
7
8
  //#region lib/types/history.js
8
9
  /**
@@ -730,7 +731,13 @@ function mountGlobalPause(ctx, watch, registry, deps) {
730
731
  //#endregion
731
732
  //#region lib/types/release-order.js
732
733
  /**
733
- * The order a tree release walks: parent before child, shallowest first.
734
+ * The order a batch release walks: parent before child, shallowest first.
735
+ *
736
+ * A single tree release already has the pre-order from its own walk, so this
737
+ * module serves the batch path, where the recorded sessions may span several
738
+ * trees. `listDescendants` stamps each row with its distance from the listing
739
+ * root, so one catalog read per recorded root places every member. A session
740
+ * the walk never reached is its own root and releases with the first level.
734
741
  * @module dsh-pause-session/release-order
735
742
  */
736
743
  /**
@@ -738,100 +745,54 @@ function mountGlobalPause(ctx, watch, registry, deps) {
738
745
  * @param ctx - host context carrying the optional subagent catalog.
739
746
  * @param sessionIds - every recorded session, in registry order.
740
747
  * @param signal - cancellation for each catalog read.
741
- * @returns the parent of each child, and one list per depth level.
748
+ * @returns one list per depth level, shallowest first.
742
749
  */
743
750
  async function releaseOrder(ctx, sessionIds, signal) {
744
- const owned = new Set(sessionIds);
745
- const parentOf = /* @__PURE__ */ new Map();
751
+ const depth = /* @__PURE__ */ new Map();
746
752
  const subagents = ctx.get("subagents");
747
- if (subagents !== void 0) for (const sessionId of sessionIds) {
748
- signal?.throwIfAborted();
749
- try {
750
- for (const child of await subagents.listChildren(sessionId, signal)) if (owned.has(child.id)) parentOf.set(child.id, sessionId);
751
- } catch (error) {
752
- if (signal?.aborted === true) throw error;
753
- ctx.logger.warn(`pause: could not read the children of "${sessionId}": ${describeError(error)}`);
753
+ if (subagents !== void 0) {
754
+ const owned = new Set(sessionIds);
755
+ for (const sessionId of sessionIds) {
756
+ signal?.throwIfAborted();
757
+ if (depth.has(sessionId)) continue;
758
+ depth.set(sessionId, 0);
759
+ try {
760
+ for (const row of await subagents.listDescendants(sessionId, signal)) {
761
+ if (row.kind !== "child" || !owned.has(row.id)) continue;
762
+ depth.set(row.id, row.depth);
763
+ }
764
+ } catch (error) {
765
+ if (signal?.aborted === true) throw error;
766
+ ctx.logger.warn(`pause: could not read the descendants of "${sessionId}": ${describeError(error)}`);
767
+ }
754
768
  }
755
769
  }
756
- const depthOf = (sessionId) => {
757
- let steps = 0;
758
- const seen = /* @__PURE__ */ new Set([sessionId]);
759
- for (let at = parentOf.get(sessionId); at !== void 0; at = parentOf.get(at)) {
760
- if (seen.has(at)) break;
761
- seen.add(at);
762
- steps += 1;
763
- }
764
- return steps;
765
- };
766
770
  const levels = /* @__PURE__ */ new Map();
767
771
  for (const sessionId of sessionIds) {
768
- const level = depthOf(sessionId);
772
+ const level = depth.get(sessionId) ?? 0;
769
773
  const bucket = levels.get(level);
770
774
  if (bucket === void 0) levels.set(level, [sessionId]);
771
775
  else bucket.push(sessionId);
772
776
  }
773
- return {
774
- parents: parentOf,
775
- levels: [...levels.keys()].sort((left, right) => left - right).map((level) => levels.get(level) ?? [])
776
- };
777
+ return { levels: [...levels.keys()].sort((left, right) => left - right).map((level) => levels.get(level) ?? []) };
777
778
  }
778
779
  //#endregion
779
780
  //#region lib/types/composition.js
780
781
  /**
781
- * Rebuild the scoped Agent composition a parked Session ran under.
782
- *
783
- * Activation is not only a persisted Session: the Agent preset supplies the
784
- * scoped tools, prompt sections, skills, and — for a subagent child — the
785
- * delegation context, persona, and tool restriction that were in force before
786
- * the park. A resume that loads the log without that setup starts a different
787
- * Agent on the same history.
782
+ * What a parked Session needs to come back as itself.
788
783
  *
789
- * The preset a Session runs is the `agentPreset` projection, never the
790
- * creation-time header alone: a blank Session may select another preset before
791
- * its first turn, and the projection records the selection that actually ran.
792
- * A child with no independent selection joins its live parent's preset
793
- * generation; one that selected independently mounts the projected preset.
794
- * The durable child descriptor supplies the persona, tool filter, and route
795
- * that are reapplied only on cold resume.
784
+ * A root Session is loaded directly: the Agent preset it ran supplies the
785
+ * scoped tools, prompt sections, and skills, so the resume mounts that preset
786
+ * before the Agent is published. A subagent child is not loaded here at all —
787
+ * the subagent runtime owns its activation, composition, and tree membership,
788
+ * and cold-resumes it when this plugin delivers the wake through
789
+ * `ctx.subagents.prompt`.
796
790
  *
797
- * Both services are optional. A profile without `agentPresets` has no scoped
798
- * preset composition to restore, and this module leaves the resume unchanged.
799
- * A profile that mounts one but cannot read the Session projection cannot
800
- * prove what composition the Session ran, so it fails the resume instead of
801
- * silently starting a different Agent.
791
+ * The two cases are told apart by one read of the Session's own log, which is
792
+ * the same read the press-time resumability check already pays for.
802
793
  * @module dsh-pause-session/composition
803
794
  */
804
795
  /**
805
- * A stored subagent route that no resume may reactivate.
806
- *
807
- * A one-shot child is terminal by contract; activating it as a root Agent
808
- * would turn a finished delegation into an ordinary conversation that happens
809
- * to share its Session id.
810
- */
811
- var UnresumableSessionError = class extends Error {
812
- constructor(sessionId) {
813
- super(`subagent session "${String(sessionId)}" has a one-shot route and cannot be resumed`);
814
- this.name = "UnresumableSessionError";
815
- }
816
- };
817
- /**
818
- * Whether the child's own log selected a preset before its descriptor.
819
- *
820
- * The creation-time selection precedes the first `subagent/descriptor`; a
821
- * selection after the descriptor belongs to the child's own later history and
822
- * does not decide which composition cold resume joins. This mirrors the
823
- * subagent runtime's own cold-resume rule.
824
- * @param events - the child's own log suffix, without the fork-inherited prefix.
825
- * @returns whether the child carries an independent preset selection.
826
- */
827
- function hasIndependentPresetSelection(events) {
828
- for (const event of events) {
829
- if (event.type === "subagent/descriptor") return false;
830
- if (event.type === "agent-preset/selected") return true;
831
- }
832
- return false;
833
- }
834
- /**
835
796
  * Whether a local Session may be parked under a promise of a later resume.
836
797
  *
837
798
  * A normal root Session has no child descriptor and is resumable. A local
@@ -860,64 +821,40 @@ async function isResumableSession(ctx, sessionId, signal) {
860
821
  }
861
822
  }
862
823
  /**
863
- * Read one parked Session's stored composition and build the resume options.
824
+ * Read one parked Session's own log and decide how it comes back.
864
825
  *
865
- * The returned `setup` belongs to the unpublished Agent scope, so all
866
- * registrations exist before `agent/created`. For a root Session the setup
867
- * mounts the Session's projected preset, or the profile's default when the
868
- * Session never selected one. For a child under a live parent it applies the
869
- * child composition: an independent preset mounts separately, while an
870
- * inherited composition joins the parent's current generation; the historical
871
- * descriptor's persona and tool filter are reapplied in both cases.
826
+ * A child returns as `child` with no setup: the subagent runtime restores the
827
+ * composition from the descriptor and the preset projection, and mounting a
828
+ * second one here would start a different Agent on the same history.
829
+ *
830
+ * A root resolves the preset it ran — the projection, never the creation-time
831
+ * header alone, because a blank Session may select another preset before its
832
+ * first turn — and hands back the setup that mounts it. A profile with no
833
+ * preset roster has no scoped composition to restore and resumes unchanged.
872
834
  * @param ctx - host context carrying the optional preset and Session-query services.
873
835
  * @param sessionId - the parked Session being resumed.
874
- * @param parent - the live direct parent, when a child is resumed under it.
875
836
  * @param signal - cancellation for the observation read.
876
- * @returns the Agent options and setup to pass to `agents.resume`.
877
- * @throws {UnresumableSessionError} when the stored child route is one-shot.
878
- * @throws when a mounted preset roster cannot read the Session projection, or
879
- * a recorded preset cannot be restored.
837
+ * @returns how this Session comes back.
838
+ * @throws when a mounted preset roster cannot read the Session projection.
880
839
  */
881
- async function restoreComposition(ctx, sessionId, parent, signal) {
840
+ async function readParkedComposition(ctx, sessionId, signal) {
882
841
  const presets = ctx.get("agentPresets");
883
842
  const query = ctx.get("sessionQuery");
884
843
  if (query === void 0) {
885
844
  if (presets !== void 0) throw new Error("resuming a composed Agent requires the session-query service");
886
- return {};
845
+ return { kind: "root" };
887
846
  }
888
847
  const observation = await query.observeSession(sessionId, { ...signal === void 0 ? {} : { signal } });
889
848
  try {
849
+ if (foldSubagentDescriptor(observation.events.slice(observation.inheritedEventCount)) !== void 0 || observation.header.origin === "subagent") return { kind: "child" };
850
+ if (presets === void 0) return { kind: "root" };
890
851
  const projections = observation.projections;
891
- const ownEvents = observation.events.slice(observation.inheritedEventCount);
892
- const descriptor = foldSubagentDescriptor(ownEvents);
893
- if (descriptor?.mode === "one-shot") throw new UnresumableSessionError(sessionId);
894
- if (presets === void 0 || projections === void 0) {
895
- if (presets !== void 0) throw new Error("resuming a composed Agent requires projected Session state");
896
- return {};
897
- }
898
- if (parent === void 0) {
899
- const resolved = await presets.resolve(projections.values.agentPreset ?? void 0);
900
- return { setup: async (agentCtx) => {
901
- await presets.mount(agentCtx, resolved.id);
902
- } };
903
- }
904
- const independentlySelected = hasIndependentPresetSelection(ownEvents);
905
- const presetId = independentlySelected ? projections.values.agentPreset : void 0;
906
- if (independentlySelected && typeof presetId !== "string") throw new Error(`subagent "${String(sessionId)}" has no projected agent preset to restore`);
907
- const composition = {
908
- ...typeof presetId === "string" ? { agentPreset: presetId } : {},
909
- ...descriptor?.mode === "continuable" && descriptor.persona !== void 0 ? { persona: descriptor.persona } : {},
910
- ...descriptor?.mode === "continuable" && descriptor.toolFilter !== void 0 ? { toolFilter: descriptor.toolFilter } : {}
911
- };
912
- const agentOptions = descriptor?.mode === "continuable" ? {
913
- ...descriptor.agentProvider === void 0 ? {} : { provider: descriptor.agentProvider },
914
- ...descriptor.agentModel === void 0 ? {} : { model: descriptor.agentModel },
915
- ...descriptor.agentReasoningEffort === void 0 ? {} : { reasoningEffort: descriptor.agentReasoningEffort }
916
- } : void 0;
852
+ if (projections === void 0) throw new Error("resuming a composed Agent requires projected Session state");
853
+ const resolved = await presets.resolve(projections.values.agentPreset ?? void 0);
917
854
  return {
918
- ...agentOptions === void 0 ? {} : { agentOptions },
855
+ kind: "root",
919
856
  setup: async (agentCtx) => {
920
- await applyChildComposition(agentCtx, parent, composition);
857
+ await presets.mount(agentCtx, resolved.id);
921
858
  }
922
859
  };
923
860
  } finally {
@@ -965,16 +902,25 @@ function summarizeTreeRelease(released) {
965
902
  //#endregion
966
903
  //#region lib/types/resume-activation.js
967
904
  /**
968
- * Reactivate one parked session: restore its composition, give it the route its
969
- * log recorded, deliver the wake it owes, and only then drop the gate and record.
905
+ * Reactivate one parked Session and deliver the wake it owes.
906
+ *
907
+ * The two Session kinds come back through different owners. An ordinary
908
+ * Session is loaded by this plugin and given the route its own log recorded.
909
+ * A subagent child belongs to the subagent runtime: delivering the wake through
910
+ * `ctx.subagents.prompt` is what cold-resumes it as a managed activation, under
911
+ * its live tree root, with its persisted composition and tree membership
912
+ * restored by the runtime itself.
913
+ *
914
+ * Both paths keep the same release order: the durable record goes first, then
915
+ * the gate, so a record that cannot be dropped leaves the Session parked.
970
916
  * @module dsh-pause-session/resume-activation
971
917
  */
972
918
  /**
973
919
  * Build the activation closure.
974
- * @param ctx - host context carrying the agent store and goal service.
920
+ * @param ctx - host context carrying the agent store, subagent runtime, and goal service.
975
921
  * @param gate - the pause gate released after activation.
976
922
  * @param registry - the durable list the release forgets from.
977
- * @param options - the route knowledge the wake content depends on.
923
+ * @param options - the route knowledge the root wake content depends on.
978
924
  * @returns the activation methods the resume path uses.
979
925
  */
980
926
  function createActivator(ctx, gate, registry, options) {
@@ -1023,27 +969,29 @@ function createActivator(ctx, gate, registry, options) {
1023
969
  ctx.logger.warn(`pause: could not re-open the goal of "${agent.id}": ${describeError(error)}`);
1024
970
  }
1025
971
  };
1026
- const activate = async (sessionId, request, rearm, parentId, signal) => {
1027
- const parent = parentId === void 0 ? void 0 : ctx.agents.get(parentId);
972
+ /** Drop the durable record, then the gate: the order every release keeps. */
973
+ const releaseRecord = async (sessionId) => {
974
+ try {
975
+ await registry.forget(sessionId);
976
+ } catch (error) {
977
+ ctx.logger.warn(`pause: could not drop "${sessionId}" from the pause registry: ${describeError(error)}`);
978
+ return {
979
+ kind: "error",
980
+ text: UNRECORDED_TEXT
981
+ };
982
+ }
983
+ gate.resume(sessionId);
984
+ };
985
+ /** Load an ordinary Session, give it its recorded route, and deliver the wake. */
986
+ const activateRoot = async (sessionId, request, rearm, composition, signal) => {
1028
987
  let agent;
1029
988
  try {
1030
- const composition = await restoreComposition(ctx, sessionId, parent, signal);
1031
989
  agent = (await ctx.root.agents.resume({
1032
990
  resumeSessionId: sessionId,
1033
- ...parent === void 0 ? {} : { parentAgent: parent },
1034
- ...composition,
991
+ ...composition.setup === void 0 ? {} : { setup: composition.setup },
1035
992
  ...signal === void 0 ? {} : { signal }
1036
993
  })).agent;
1037
994
  } catch (error) {
1038
- if (error instanceof UnresumableSessionError) return {
1039
- sessionId,
1040
- resumed: false,
1041
- refusal: "unresumable",
1042
- answer: {
1043
- kind: "error",
1044
- text: UNRESUMABLE_TEXT
1045
- }
1046
- };
1047
995
  ctx.logger.warn(`pause: could not open "${sessionId}" from its stored log: ${describeError(error)}`);
1048
996
  return {
1049
997
  sessionId,
@@ -1057,22 +1005,78 @@ function createActivator(ctx, gate, registry, options) {
1057
1005
  }
1058
1006
  ensureRoute(agent);
1059
1007
  wake(agent, request);
1008
+ const refused = await releaseRecord(sessionId);
1009
+ if (refused !== void 0) return {
1010
+ sessionId,
1011
+ resumed: false,
1012
+ refusal: "unrecorded",
1013
+ answer: refused
1014
+ };
1015
+ if (rearm) rearmGoal(agent);
1016
+ return {
1017
+ sessionId,
1018
+ resumed: true,
1019
+ answer: {
1020
+ kind: "success",
1021
+ text: RESUMED_TEXT
1022
+ }
1023
+ };
1024
+ };
1025
+ /**
1026
+ * Hand a subagent child the wake through the subagent runtime.
1027
+ *
1028
+ * The runtime owns the activation, so it is also the only thing that can
1029
+ * cold-resume the child under its live tree root. An empty message would be
1030
+ * dropped by a route whose conversion discards it, and the route is unknown
1031
+ * before the child is live, so the marker is the content that always wakes it.
1032
+ */
1033
+ const activateChild = async (sessionId, request, rearm, signal) => {
1034
+ const subagents = ctx.get("subagents");
1035
+ if (subagents === void 0) {
1036
+ ctx.logger.warn(`pause: subagent runtime is not mounted; opening "${sessionId}" as a root Session`);
1037
+ return await activateRoot(sessionId, request, rearm, { kind: "root" }, signal);
1038
+ }
1039
+ const content = [{
1040
+ type: "text",
1041
+ text: request.kind === "message" ? request.text : WAKE_MARKER
1042
+ }];
1060
1043
  try {
1061
- await registry.forget(sessionId);
1044
+ await subagents.prompt({
1045
+ requestId: randomUUID(),
1046
+ childSessionId: sessionId,
1047
+ delivery: "queue",
1048
+ content
1049
+ }, signal ?? new AbortController().signal);
1062
1050
  } catch (error) {
1063
- ctx.logger.warn(`pause: could not drop "${sessionId}" from the pause registry: ${describeError(error)}`);
1051
+ if (remoteErrorOf(error)?.code === "subagent/not-resumable") return {
1052
+ sessionId,
1053
+ resumed: false,
1054
+ refusal: "unresumable",
1055
+ answer: {
1056
+ kind: "error",
1057
+ text: UNRESUMABLE_TEXT
1058
+ }
1059
+ };
1060
+ ctx.logger.warn(`pause: could not continue subagent "${sessionId}": ${describeError(error)}`);
1064
1061
  return {
1065
1062
  sessionId,
1066
1063
  resumed: false,
1067
- refusal: "unrecorded",
1064
+ refusal: "activation-failed",
1068
1065
  answer: {
1069
1066
  kind: "error",
1070
- text: UNRECORDED_TEXT
1067
+ text: ACTIVATION_FAILED_TEXT
1071
1068
  }
1072
1069
  };
1073
1070
  }
1074
- gate.resume(sessionId);
1075
- if (rearm) rearmGoal(agent);
1071
+ const refused = await releaseRecord(sessionId);
1072
+ if (refused !== void 0) return {
1073
+ sessionId,
1074
+ resumed: false,
1075
+ refusal: "unrecorded",
1076
+ answer: refused
1077
+ };
1078
+ const agent = ctx.agents.get(sessionId);
1079
+ if (rearm && agent !== void 0) rearmGoal(agent);
1076
1080
  return {
1077
1081
  sessionId,
1078
1082
  resumed: true,
@@ -1082,6 +1086,24 @@ function createActivator(ctx, gate, registry, options) {
1082
1086
  }
1083
1087
  };
1084
1088
  };
1089
+ const activate = async (sessionId, request, rearm, signal) => {
1090
+ let composition;
1091
+ try {
1092
+ composition = await readParkedComposition(ctx, sessionId, signal);
1093
+ } catch (error) {
1094
+ ctx.logger.warn(`pause: could not read the composition of "${sessionId}": ${describeError(error)}`);
1095
+ return {
1096
+ sessionId,
1097
+ resumed: false,
1098
+ refusal: "activation-failed",
1099
+ answer: {
1100
+ kind: "error",
1101
+ text: ACTIVATION_FAILED_TEXT
1102
+ }
1103
+ };
1104
+ }
1105
+ return composition.kind === "child" ? await activateChild(sessionId, request, rearm, signal) : await activateRoot(sessionId, request, rearm, composition, signal);
1106
+ };
1085
1107
  return {
1086
1108
  activate,
1087
1109
  ensureRoute,
@@ -1156,11 +1178,10 @@ function installResumePath(ctx, gate, registry, options, deps) {
1156
1178
  * @param finalize - whether the decision continues the session (`'release'`)
1157
1179
  * or only answers it (`'review'`); a review activates nothing and writes
1158
1180
  * nothing.
1159
- * @param parentId - the direct parent a batch release already brought back, when there is one.
1160
1181
  * @param signal - cancellation for the cold load and setup.
1161
1182
  * @returns what happened, and the copy for whoever asked.
1162
1183
  */
1163
- const decideUnclean = async (sessionId, request, finalize, parentId, signal) => {
1184
+ const decideUnclean = async (sessionId, request, finalize, signal) => {
1164
1185
  const record = registry.get(sessionId);
1165
1186
  if (record === void 0) return {
1166
1187
  sessionId,
@@ -1181,7 +1202,7 @@ function installResumePath(ctx, gate, registry, options, deps) {
1181
1202
  }
1182
1203
  };
1183
1204
  const live = ctx.agents.get(sessionId);
1184
- if (live === void 0) return await activate(sessionId, request, record.rearmGoal, parentId, signal);
1205
+ if (live === void 0) return await activate(sessionId, request, record.rearmGoal, signal);
1185
1206
  if (!gate.isPaused(sessionId)) {
1186
1207
  ensureRoute(live);
1187
1208
  wake(live, request);
@@ -1238,11 +1259,10 @@ function installResumePath(ctx, gate, registry, options, deps) {
1238
1259
  * back is the press's own undo, and it delivers no wake.
1239
1260
  * @param sessionId - the recorded session to resume.
1240
1261
  * @param request - which wake this resume delivers; see {@link WakeRequest}.
1241
- * @param parentId - the direct parent a batch release already brought back, when there is one.
1242
1262
  * @param signal - cancellation for the cold load and setup.
1243
1263
  * @returns what happened, and the copy for whoever asked.
1244
1264
  */
1245
- const resumeSession = async (sessionId, request, parentId, signal) => {
1265
+ const resumeSession = async (sessionId, request, signal) => {
1246
1266
  const record = registry.get(sessionId);
1247
1267
  const live = ctx.agents.get(sessionId);
1248
1268
  if (live !== void 0) {
@@ -1337,18 +1357,8 @@ function installResumePath(ctx, gate, registry, options, deps) {
1337
1357
  text: UNCLEAN_TEXT
1338
1358
  }
1339
1359
  };
1340
- return await activate(sessionId, request, record.rearmGoal, parentId, signal);
1360
+ return await activate(sessionId, request, record.rearmGoal, signal);
1341
1361
  };
1342
- /**
1343
- * Order the recorded sessions so a session is released after the parent it
1344
- * hangs from: waking a child needs its direct parent to be live first. The
1345
- * relation is read from the parent catalogs themselves (which children this
1346
- * session actually has), never guessed from ids; a branch whose catalog
1347
- * cannot be read simply has no known parent and is released with the roots.
1348
- * @param sessionIds - every recorded session, in registry order.
1349
- * @param signal - cancellation for each catalog read.
1350
- * @returns the parent of each child, and one list per depth level, shallowest first.
1351
- */
1352
1362
  return {
1353
1363
  resumeSession,
1354
1364
  decideUnclean,
@@ -1382,27 +1392,22 @@ function installResumePath(ctx, gate, registry, options, deps) {
1382
1392
  answer
1383
1393
  });
1384
1394
  }
1385
- else {
1386
- const release = await releaseOrder(ctx, registered, signal);
1387
- for (const level of release.levels) {
1388
- signal?.throwIfAborted();
1389
- for (const member of level) {
1390
- const record = registry.get(member);
1391
- if (record !== void 0 && record.phase !== "safe") {
1392
- released.push({
1393
- sessionId: member,
1394
- resumed: false,
1395
- refusal: "not-clean",
1396
- answer: {
1397
- kind: "error",
1398
- text: UNCLEAN_TEXT
1399
- }
1400
- });
1401
- continue;
1395
+ else for (const member of registered) {
1396
+ signal?.throwIfAborted();
1397
+ const record = registry.get(member);
1398
+ if (record !== void 0 && record.phase !== "safe") {
1399
+ released.push({
1400
+ sessionId: member,
1401
+ resumed: false,
1402
+ refusal: "not-clean",
1403
+ answer: {
1404
+ kind: "error",
1405
+ text: UNCLEAN_TEXT
1402
1406
  }
1403
- released.push(await resumeSession(member, { kind: "silent" }, release.parents.get(member), signal));
1404
- }
1407
+ });
1408
+ continue;
1405
1409
  }
1410
+ released.push(await resumeSession(member, { kind: "silent" }, signal));
1406
1411
  }
1407
1412
  const root = released.find((attempt) => attempt.sessionId === sessionId);
1408
1413
  const refusals = released.filter((attempt) => !attempt.resumed && attempt.refusal !== void 0).map((attempt) => ({
@@ -1427,7 +1432,7 @@ function installResumePath(ctx, gate, registry, options, deps) {
1427
1432
  const attempts = [];
1428
1433
  for (const level of release.levels) {
1429
1434
  signal?.throwIfAborted();
1430
- attempts.push(...await Promise.all(level.map((sessionId) => resumeSession(sessionId, { kind: "silent" }, release.parents.get(sessionId), signal))));
1435
+ attempts.push(...await Promise.all(level.map((sessionId) => resumeSession(sessionId, { kind: "silent" }, signal))));
1431
1436
  }
1432
1437
  return attempts;
1433
1438
  },
@@ -1617,12 +1622,12 @@ function mountTriageList(ctx, registry, resumePath, windowEvents = TRIAGE_WINDOW
1617
1622
  text: TRIAGE_LEFT_TEXT
1618
1623
  };
1619
1624
  if (action === "goal") return await resumePath.setRearmGoal(sessionId, !record.rearmGoal);
1620
- if (action === "review") return (await resumePath.decideUnclean(sessionId, { kind: "none" }, "review", void 0, signal)).answer;
1625
+ if (action === "review") return (await resumePath.decideUnclean(sessionId, { kind: "none" }, "review", signal)).answer;
1621
1626
  const wake = action === "continue" ? { kind: "silent" } : {
1622
1627
  kind: "message",
1623
1628
  text: UNKNOWN_OUTCOME_NOTE
1624
1629
  };
1625
- return (await resumePath.decideUnclean(sessionId, wake, "release", void 0, signal)).answer;
1630
+ return (await resumePath.decideUnclean(sessionId, wake, "release", signal)).answer;
1626
1631
  };
1627
1632
  return {
1628
1633
  read: async (caller, signal) => {