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/CHANGELOG.md CHANGED
@@ -2,23 +2,30 @@
2
2
 
3
3
  All notable changes to this project are documented here.
4
4
 
5
- The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
- and this project uses [Semantic Versioning](https://semver.org/).
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project uses [Semantic Versioning](https://semver.org/).
7
6
 
8
- ## [0.2.0] - 2026-11-06
7
+ ## [0.2.1] - 2026-11-11
9
8
 
10
9
  ### Changed
11
10
 
12
- - Both controls now use a native DSH Remote namespace and stream. The plugin
13
- registers no slash commands, so a status refresh never appends
14
- `command/run`/`command/done` records to a Session.
15
- - Pause, resume, goal, and decision-list actions travel through the same
16
- native namespace on Web and Desktop.
11
+ - Adapt to DeepSeek Harness `0.2.2-rc.2`. Subagent children now resume through `ctx.subagents.prompt`, so the runtime owns their activation, composition, and tree membership; the plugin no longer loads them as independent Agents.
12
+ - Release ordering reads `listDescendants` depth and reuses the tree walk, so a tree release no longer reads the catalogs twice.
13
+ - The composition seam resolves a root Session's preset only; a child's composition is restored by the subagent runtime.
14
+
15
+ ### Removed
16
+
17
+ - The `parentAgent` resume option, `listChildren` catalog reads, and the child-composition branch this plugin used to assemble itself.
18
+
19
+ ## [0.2.0] - 2026-11-08
20
+
21
+ ### Changed
22
+
23
+ - Both controls now use a native DSH Remote namespace and stream. The plugin registers no slash commands, so a status refresh never appends `command/run`/`command/done` records to a Session.
24
+ - Pause, resume, goal, and decision-list actions travel through the same native namespace on Web and Desktop.
17
25
 
18
26
  ### Added
19
27
 
20
- - Global progress names every draining session and its running tools, and the
21
- in-progress action is **Cancel this pause**.
28
+ - Global progress names every draining session and its running tools, and the in-progress action is **Cancel this pause**.
22
29
  - The Remote namespace is owner-only.
23
30
  - The client bundle uses a build-time codec-builder shim instead of bundling the full Zod runtime.
24
31
  - One snapshot is shared by every connected Remote observer, and the snapshot projection shares its tree walks.
@@ -27,53 +34,39 @@ and this project uses [Semantic Versioning](https://semver.org/).
27
34
 
28
35
  ### Fixed
29
36
 
30
- - A park reaches `safe` only when the session-level flush actually ran; an
31
- unreadable record and a partially unread tree are reported instead of being
32
- counted clean.
33
- - A restart-adopted park is no longer cancelled by the per-session button; it
34
- stays on the decision path.
35
- - Stream and triage failures render as unavailable instead of stale rows, and
36
- triage decisions that the host refuses show the refusal.
37
+ - A park reaches `safe` only when the session-level flush actually ran; an unreadable record and a partially unread tree are reported instead of being counted clean.
38
+ - A restart-adopted park is no longer cancelled by the per-session button; it stays on the decision path.
39
+ - Stream and triage failures render as unavailable instead of stale rows, and triage decisions that the host refuses show the refusal.
37
40
 
38
41
  ### Removed
39
42
 
40
- - The `/pause`, `/pause-resume`, `/pause-state`, `/pause-goal`, and
41
- `/pause-triage*` command surface.
42
- - The command-era `ReadBurst`/JSON-status-route client machinery and the
43
- `@deepseek-ai/dsh-commands` dependency.
43
+ - The `/pause`, `/pause-resume`, `/pause-state`, `/pause-goal`, and `/pause-triage*` command surface.
44
+ - The command-era `ReadBurst`/JSON-status-route client machinery and the `@deepseek-ai/dsh-commands` dependency.
44
45
 
45
46
  ## [0.1.0] - 2026-11-02
46
47
 
47
48
  ### Added
48
49
 
49
- - Pause every running DSH session and its subagents at the next model-request
50
- boundary, after the tool already running finishes.
51
- - Session-header and whole-app controls, with `Pausing`, `Paused`, and
52
- `All Paused` states.
50
+ - Pause every running DSH session and its subagents at the next model-request boundary, after the tool already running finishes.
51
+ - Session-header and whole-app controls, with `Pausing`, `Paused`, and `All Paused` states.
53
52
  - Durable pause registry that re-holds parked sessions after a DSH restart.
54
- - Clean resume that restores the recorded Agent preset composition, including
55
- subagent delegation context and tool restrictions.
53
+ - Clean resume that restores the recorded Agent preset composition, including subagent delegation context and tool restrictions.
56
54
  - Goal re-arm toggle for cleanly paused sessions.
57
- - Decision list for sessions that did not pause cleanly, with continue,
58
- continue-with-note, review-only, leave, and goal actions.
59
- - Localized English and Chinese UI copy, plugin-manager metadata, and a
60
- user-facing README screenshot.
61
- - `/pause`, `/pause-resume`, `/pause-all`, `/pause-state`, `/pause-goal`, and
62
- `/pause-triage*` command surface.
55
+ - Decision list for sessions that did not pause cleanly, with continue, continue-with-note, review-only, leave, and goal actions.
56
+ - Localized English and Chinese UI copy, plugin-manager metadata, and a user-facing README screenshot.
57
+ - `/pause`, `/pause-resume`, `/pause-all`, `/pause-state`, `/pause-goal`, and `/pause-triage*` command surface.
63
58
 
64
59
  ### Changed
65
60
 
66
- - The package is published as `dsh-pause-session`; the slash-command namespace
67
- remains `/pause*`.
68
- - One-shot and external subagent routes are reported as unresumable and are not
69
- registered as local pause records.
61
+ - The package is published as `dsh-pause-session`; the slash-command namespace remains `/pause*`.
62
+ - One-shot and external subagent routes are reported as unresumable and are not registered as local pause records.
70
63
 
71
64
  ### Known Limitations
72
65
 
73
66
  - Background jobs and shell tasks outside the agent loop are not stopped.
74
- - A paused session still appears `running` to parts of DSH's session lifecycle;
75
- exit, archive, compaction, and `wait_agent` can behave differently while held.
67
+ - A paused session still appears `running` to parts of DSH's session lifecycle; exit, archive, compaction, and `wait_agent` can behave differently while held.
76
68
  - External one-shot subagent routes cannot be cold-resumed.
77
69
 
70
+ [0.2.1]: https://github.com/timho1047/dsh-pause-session/releases/tag/v0.2.1
78
71
  [0.2.0]: https://github.com/timho1047/dsh-pause-session/releases/tag/v0.2.0
79
72
  [0.1.0]: https://github.com/timho1047/dsh-pause-session/releases/tag/v0.1.0
package/lib/client.js CHANGED
@@ -346,9 +346,9 @@ window.__ModuleLoader__.load({
346
346
  unclean: 0
347
347
  };
348
348
  if (reading.kind === "unavailable") return {
349
- visible: false,
349
+ visible: true,
350
350
  enabled: false,
351
- ariaKey: "unavailable.aria",
351
+ ariaKey: "all.unavailable.aria",
352
352
  action: "none",
353
353
  detail: "",
354
354
  caveats: [],
@@ -651,47 +651,60 @@ window.__ModuleLoader__.load({
651
651
  sessions: Object.fromEntries(Object.entries(wire.sessions).map(([id, status]) => [id, status]))
652
652
  };
653
653
  }
654
+ /** First retry delay after the stream drops, in milliseconds. */
655
+ const RECONNECT_BASE_MS = 300;
656
+ /** Ceiling for the reconnect backoff, in milliseconds. */
657
+ const RECONNECT_MAX_MS = 5e3;
654
658
  /**
655
659
  * Open the native status stream and expose its latest snapshot to both controls.
660
+ *
661
+ * The stream owns no retry of its own: a carrier can drop it on a mux reconnect,
662
+ * a host reload, or a network blip, and the controls must not stay unavailable
663
+ * until the page is refreshed. This source reconnects with a bounded backoff and
664
+ * republishes the new snapshot; while it is disconnected the last value is
665
+ * discarded, so readers see "unavailable" rather than a stale claim.
656
666
  * @param remote - the generated Remote namespace.
657
667
  * @param lifetime - client lifetime; aborting it releases the stream.
658
668
  * @returns the cached source and its change subscription.
659
669
  */
660
670
  function openRemoteSource(remote, lifetime) {
661
671
  let latest;
662
- let failed = false;
663
- let first;
672
+ let attempt = 0;
673
+ let timer;
674
+ /** Readers waiting for the first snapshot of any attempt. */
675
+ const waiters = /* @__PURE__ */ new Set();
664
676
  const listeners = /* @__PURE__ */ new Set();
665
677
  const snapshotListeners = /* @__PURE__ */ new Set();
666
678
  const notify = () => {
679
+ for (const wake of [...waiters]) wake();
680
+ waiters.clear();
667
681
  for (const listener of [...listeners]) listener();
668
- for (const listener of [...snapshotListeners]) listener(failed ? void 0 : latest);
682
+ for (const listener of [...snapshotListeners]) listener(latest);
669
683
  };
670
- new Promise((resolve) => {
671
- first = resolve;
672
- });
673
- (async () => {
684
+ const schedule = () => {
685
+ if (lifetime.aborted || timer !== void 0) return;
686
+ const delay = Math.min(RECONNECT_BASE_MS * 2 ** attempt, RECONNECT_MAX_MS);
687
+ attempt += 1;
688
+ timer = setTimeout(() => {
689
+ timer = void 0;
690
+ pump();
691
+ }, delay);
692
+ };
693
+ const pump = async () => {
694
+ if (lifetime.aborted) return;
674
695
  try {
675
696
  for await (const snapshot of remote.observe(lifetime)) {
697
+ attempt = 0;
676
698
  latest = snapshotOf(snapshot);
677
- first?.();
678
- first = void 0;
679
699
  notify();
680
700
  }
681
- } catch {
682
- failed = true;
683
- latest = void 0;
684
- notify();
685
- } finally {
686
- if (!lifetime.aborted) {
687
- failed = true;
688
- latest = void 0;
689
- }
690
- first?.();
691
- first = void 0;
692
- notify();
693
- }
694
- })();
701
+ } catch {}
702
+ if (lifetime.aborted) return;
703
+ latest = void 0;
704
+ notify();
705
+ schedule();
706
+ };
707
+ pump();
695
708
  return {
696
709
  onChange: (listener) => {
697
710
  listeners.add(listener);
@@ -706,20 +719,17 @@ window.__ModuleLoader__.load({
706
719
  };
707
720
  },
708
721
  read: async (signal) => {
709
- if (latest === void 0 && !failed) await new Promise((resolve) => {
710
- const done = () => {
711
- signal?.removeEventListener("abort", done);
722
+ if (latest === void 0 && !lifetime.aborted) await new Promise((resolve) => {
723
+ const wake = () => {
724
+ waiters.delete(wake);
725
+ signal?.removeEventListener("abort", wake);
712
726
  resolve();
713
727
  };
714
- first = () => {
715
- first = void 0;
716
- done();
717
- };
718
- signal?.addEventListener("abort", done, { once: true });
719
- if (latest !== void 0 || failed) done();
728
+ waiters.add(wake);
729
+ signal?.addEventListener("abort", wake, { once: true });
720
730
  });
721
731
  signal?.throwIfAborted();
722
- return failed ? void 0 : latest;
732
+ return latest;
723
733
  }
724
734
  };
725
735
  }
@@ -1630,7 +1640,7 @@ window.__ModuleLoader__.load({
1630
1640
  const reading = props.usePauseGlobal((state) => state.global);
1631
1641
  const triage = props.usePauseGlobal((state) => state.triage);
1632
1642
  const open = props.usePauseGlobal((state) => state.panelOpen);
1633
- props.usePauseGlobal((state) => state.error);
1643
+ const error = props.usePauseGlobal((state) => state.error);
1634
1644
  const sessions = props.usePauseGlobal((state) => state.sessions);
1635
1645
  const address = useSessions(addressingSession);
1636
1646
  const view = globalPauseView(reading, sessions);
@@ -1643,7 +1653,8 @@ window.__ModuleLoader__.load({
1643
1653
  address,
1644
1654
  visible
1645
1655
  ]);
1646
- if (!visible || !view.visible || address === void 0) return null;
1656
+ if (!visible || !view.visible) return null;
1657
+ const actionable = view.enabled && address !== void 0;
1647
1658
  const tip = view.caveats.map((caveat) => caveat.names.length === 0 ? t(caveat.key) : `${t(caveat.key)} (${caveat.names.join(", ")})`).join("\n");
1648
1659
  return (0, react_jsx_runtime.jsxs)("span", {
1649
1660
  className: button_module_css_default.dspPill,
@@ -1652,37 +1663,49 @@ window.__ModuleLoader__.load({
1652
1663
  className: button_module_css_default.dspPillPanel,
1653
1664
  role: "dialog",
1654
1665
  "aria-label": t("triage.title"),
1655
- children: [(0, react_jsx_runtime.jsxs)("div", {
1656
- className: button_module_css_default.dspPillPanelHead,
1657
- children: [(0, react_jsx_runtime.jsx)("span", {
1658
- className: button_module_css_default.dspPillText,
1659
- children: t("triage.title")
1660
- }), (0, react_jsx_runtime.jsx)("button", {
1661
- type: "button",
1662
- className: button_module_css_default.dspPillAction,
1663
- "aria-label": t("triage.refresh.aria"),
1664
- onClick: () => {
1665
- setPanel(true);
1666
- },
1667
- children: t("triage.refresh.aria")
1668
- })]
1669
- }), !list.visible ? (0, react_jsx_runtime.jsx)("p", {
1670
- className: button_module_css_default.dspPillPanelNote,
1671
- children: t("triage.reading")
1672
- }) : list.unavailable ? (0, react_jsx_runtime.jsx)("p", {
1673
- className: button_module_css_default.dspPillPanelNote,
1674
- children: t("triage.unavailable")
1675
- }) : list.rows.length === 0 ? (0, react_jsx_runtime.jsx)("p", {
1676
- className: button_module_css_default.dspPillPanelNote,
1677
- children: t("triage.empty")
1678
- }) : (0, react_jsx_runtime.jsx)("ul", {
1679
- className: button_module_css_default.dspPillRows,
1680
- children: list.rows.map((row) => (0, react_jsx_runtime.jsx)(TriageSessionRow, {
1681
- row,
1682
- decide,
1683
- t
1684
- }, row.sessionId))
1685
- })]
1666
+ children: [
1667
+ (0, react_jsx_runtime.jsxs)("div", {
1668
+ className: button_module_css_default.dspPillPanelHead,
1669
+ children: [(0, react_jsx_runtime.jsx)("span", {
1670
+ className: button_module_css_default.dspPillText,
1671
+ children: t("triage.title")
1672
+ }), (0, react_jsx_runtime.jsx)("button", {
1673
+ type: "button",
1674
+ className: button_module_css_default.dspPillAction,
1675
+ "aria-label": t("triage.refresh.aria"),
1676
+ onClick: () => {
1677
+ setPanel(true);
1678
+ },
1679
+ children: t("triage.refresh.aria")
1680
+ })]
1681
+ }),
1682
+ error === void 0 ? null : (0, react_jsx_runtime.jsx)("p", {
1683
+ className: button_module_css_default.dspPillError,
1684
+ role: "status",
1685
+ children: error
1686
+ }),
1687
+ view.running === void 0 ? null : (0, react_jsx_runtime.jsx)("p", {
1688
+ className: button_module_css_default.dspPillPanelNote,
1689
+ children: view.running
1690
+ }),
1691
+ !list.visible ? (0, react_jsx_runtime.jsx)("p", {
1692
+ className: button_module_css_default.dspPillPanelNote,
1693
+ children: t("triage.reading")
1694
+ }) : list.unavailable ? (0, react_jsx_runtime.jsx)("p", {
1695
+ className: button_module_css_default.dspPillPanelNote,
1696
+ children: t("triage.unavailable")
1697
+ }) : list.rows.length === 0 ? (0, react_jsx_runtime.jsx)("p", {
1698
+ className: button_module_css_default.dspPillPanelNote,
1699
+ children: t("triage.empty")
1700
+ }) : (0, react_jsx_runtime.jsx)("ul", {
1701
+ className: button_module_css_default.dspPillRows,
1702
+ children: list.rows.map((row) => (0, react_jsx_runtime.jsx)(TriageSessionRow, {
1703
+ row,
1704
+ decide,
1705
+ t
1706
+ }, row.sessionId))
1707
+ })
1708
+ ]
1686
1709
  }),
1687
1710
  view.unclean === 0 ? null : (0, react_jsx_runtime.jsxs)("button", {
1688
1711
  type: "button",
@@ -1703,10 +1726,10 @@ window.__ModuleLoader__.load({
1703
1726
  className: view.labelKey === void 0 && view.detail.length === 0 ? `${button_module_css_default.dspPillBtn} ${button_module_css_default.dspPillIcon}` : button_module_css_default.dspPillBtn,
1704
1727
  "aria-label": t(view.ariaKey),
1705
1728
  "aria-pressed": view.action === "resume-all" || view.action === "cancel-all",
1706
- disabled: !view.enabled,
1729
+ disabled: !actionable,
1707
1730
  ...tip.length === 0 ? {} : { title: tip },
1708
1731
  onClick: () => {
1709
- toggle(address);
1732
+ if (address !== void 0) toggle(address);
1710
1733
  },
1711
1734
  children: [
1712
1735
  view.action === "resume-all" || view.action === "cancel-all" ? (0, react_jsx_runtime.jsx)(ResumeIcon, {}) : (0, react_jsx_runtime.jsx)(PauseIcon, {}),
@@ -1759,6 +1782,7 @@ window.__ModuleLoader__.load({
1759
1782
  "all.unclean.aria": "有会话未暂停,不能在这里恢复;打开清单逐行处理",
1760
1783
  "all.safe": "已暂停",
1761
1784
  "all.checking.aria": "正在读取暂停状态",
1785
+ "all.unavailable.aria": "暂停状态暂不可用,正在重新连接",
1762
1786
  "all.caveat.jobs": "后台任务仍在运行",
1763
1787
  "all.caveat.unresumable": "含无法恢复的子代理",
1764
1788
  "all.caveat.stalled": "未暂停,需要你决定",
@@ -1821,6 +1845,7 @@ window.__ModuleLoader__.load({
1821
1845
  "all.unclean.aria": "Some sessions are not paused and cannot be resumed here; open the list to decide",
1822
1846
  "all.safe": "Paused",
1823
1847
  "all.checking.aria": "Reading pause state",
1848
+ "all.unavailable.aria": "Pause state unavailable; reconnecting",
1824
1849
  "all.caveat.jobs": "background jobs are still running",
1825
1850
  "all.caveat.unresumable": "contains a subagent that cannot be resumed",
1826
1851
  "all.caveat.stalled": "did not pause; needs your decision",