dsh-zen-remote 1.1.16 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/AGENTS.md +54 -9
  2. package/README.md +249 -190
  3. package/README.zh-CN.md +247 -201
  4. package/assets/home.png +0 -0
  5. package/assets/info.png +0 -0
  6. package/assets/pairing.png +0 -0
  7. package/assets/session.png +0 -0
  8. package/assets/sheet.png +0 -0
  9. package/cordis.patch.yml +12 -11
  10. package/cordis.patch.yml.example +24 -9
  11. package/docs/interface.md +57 -2
  12. package/docs/remote-access.en.md +99 -58
  13. package/docs/remote-access.md +99 -42
  14. package/dsh-push.mjs +67 -54
  15. package/lan-gate.mjs +88 -20
  16. package/lib/activity.js +168 -0
  17. package/lib/activity.js.map +1 -0
  18. package/lib/admin-routes.js +526 -0
  19. package/lib/admin-routes.js.map +1 -0
  20. package/lib/client-pairing.js +224 -0
  21. package/lib/client-pairing.js.map +1 -0
  22. package/lib/client-routes.js +674 -0
  23. package/lib/client-routes.js.map +1 -0
  24. package/lib/client.js +10051 -4777
  25. package/lib/config.js +346 -0
  26. package/lib/config.js.map +1 -0
  27. package/lib/fetch-route-intercept.js +418 -0
  28. package/lib/fetch-route-intercept.js.map +1 -0
  29. package/lib/fingerprint.js +363 -0
  30. package/lib/fingerprint.js.map +1 -0
  31. package/lib/http.js +69 -0
  32. package/lib/http.js.map +1 -0
  33. package/lib/index.js +675 -54
  34. package/lib/index.js.map +1 -1
  35. package/lib/intercept-shape.js +81 -0
  36. package/lib/intercept-shape.js.map +1 -0
  37. package/lib/intercept.js +3040 -0
  38. package/lib/intercept.js.map +1 -0
  39. package/lib/lan-gate-server.cjs +532 -121
  40. package/lib/merge-streams.js +1165 -0
  41. package/lib/merge-streams.js.map +1 -0
  42. package/lib/relay-access.js +496 -0
  43. package/lib/relay-access.js.map +1 -0
  44. package/lib/relay-client.js +1303 -0
  45. package/lib/relay-client.js.map +1 -0
  46. package/lib/relay-filter.js +282 -0
  47. package/lib/relay-filter.js.map +1 -0
  48. package/lib/relay-server.js +1792 -0
  49. package/lib/relay-server.js.map +1 -0
  50. package/lib/restart-fields.js +34 -0
  51. package/lib/restart-fields.js.map +1 -0
  52. package/lib/restart-watcher.js +106 -0
  53. package/lib/restart-watcher.js.map +1 -0
  54. package/lib/session-reference.js +157 -0
  55. package/lib/session-reference.js.map +1 -0
  56. package/lib/share-export.js +16 -1
  57. package/lib/share-export.js.map +1 -1
  58. package/lib/share-ops.js +86 -0
  59. package/lib/share-ops.js.map +1 -0
  60. package/lib/share-store.js +294 -0
  61. package/lib/share-store.js.map +1 -0
  62. package/lib/types/activity.d.ts +99 -0
  63. package/lib/types/activity.d.ts.map +1 -0
  64. package/lib/types/admin-routes.d.ts +129 -0
  65. package/lib/types/admin-routes.d.ts.map +1 -0
  66. package/lib/types/client/MobileDrawerFooter.d.ts.map +1 -1
  67. package/lib/types/client/MobileHome.d.ts +1 -1
  68. package/lib/types/client/MobileHome.d.ts.map +1 -1
  69. package/lib/types/client/MobileHomeChips.d.ts +1 -1
  70. package/lib/types/client/MobileHomeChips.d.ts.map +1 -1
  71. package/lib/types/client/MobileSessionHeader.d.ts +13 -2
  72. package/lib/types/client/MobileSessionHeader.d.ts.map +1 -1
  73. package/lib/types/client/MobileSessionInfo.d.ts +20 -3
  74. package/lib/types/client/MobileSessionInfo.d.ts.map +1 -1
  75. package/lib/types/client/RemoteComposerBanner.d.ts +42 -0
  76. package/lib/types/client/RemoteComposerBanner.d.ts.map +1 -0
  77. package/lib/types/client/RemoteHeaderIcon.d.ts +37 -0
  78. package/lib/types/client/RemoteHeaderIcon.d.ts.map +1 -0
  79. package/lib/types/client/RemoteShareMenu.d.ts +28 -0
  80. package/lib/types/client/RemoteShareMenu.d.ts.map +1 -0
  81. package/lib/types/client/RemoteStatusIcon.d.ts +36 -0
  82. package/lib/types/client/RemoteStatusIcon.d.ts.map +1 -0
  83. package/lib/types/client/compat/desktop.d.ts +21 -0
  84. package/lib/types/client/compat/desktop.d.ts.map +1 -0
  85. package/lib/types/client/compat/types.d.ts +57 -0
  86. package/lib/types/client/compat/types.d.ts.map +1 -1
  87. package/lib/types/client/current-session-report.d.ts +37 -0
  88. package/lib/types/client/current-session-report.d.ts.map +1 -0
  89. package/lib/types/client/effects/header-status.d.ts.map +1 -1
  90. package/lib/types/client/effects/keyboard-guard.d.ts.map +1 -1
  91. package/lib/types/client/effects/permission-sheet.d.ts +39 -0
  92. package/lib/types/client/effects/permission-sheet.d.ts.map +1 -0
  93. package/lib/types/client/effects/remote-session.d.ts +25 -0
  94. package/lib/types/client/effects/remote-session.d.ts.map +1 -0
  95. package/lib/types/client/effects/turn-fold.d.ts +9 -0
  96. package/lib/types/client/effects/turn-fold.d.ts.map +1 -1
  97. package/lib/types/client/index.d.ts.map +1 -1
  98. package/lib/types/client/locales.d.ts +187 -2
  99. package/lib/types/client/locales.d.ts.map +1 -1
  100. package/lib/types/client/remote-fetch.d.ts +51 -0
  101. package/lib/types/client/remote-fetch.d.ts.map +1 -0
  102. package/lib/types/client/remote-group-expansion.d.ts +41 -0
  103. package/lib/types/client/remote-group-expansion.d.ts.map +1 -0
  104. package/lib/types/client/remote-share-css.d.ts +19 -0
  105. package/lib/types/client/remote-share-css.d.ts.map +1 -0
  106. package/lib/types/client/remote-share-register.d.ts +113 -0
  107. package/lib/types/client/remote-share-register.d.ts.map +1 -0
  108. package/lib/types/client/remote-status-css.d.ts +15 -0
  109. package/lib/types/client/remote-status-css.d.ts.map +1 -0
  110. package/lib/types/client/remote-status-register.d.ts +46 -0
  111. package/lib/types/client/remote-status-register.d.ts.map +1 -0
  112. package/lib/types/client/settings/SettingsSection.d.ts +46 -0
  113. package/lib/types/client/settings/SettingsSection.d.ts.map +1 -0
  114. package/lib/types/client/settings/register-settings.d.ts +58 -0
  115. package/lib/types/client/settings/register-settings.d.ts.map +1 -0
  116. package/lib/types/client/settings/settings-css.d.ts +10 -0
  117. package/lib/types/client/settings/settings-css.d.ts.map +1 -0
  118. package/lib/types/client/share/share-preview.d.ts.map +1 -1
  119. package/lib/types/client/styles/composer.css.d.ts +1 -1
  120. package/lib/types/client/styles/composer.css.d.ts.map +1 -1
  121. package/lib/types/client/styles/header.css.d.ts +1 -1
  122. package/lib/types/client/styles/header.css.d.ts.map +1 -1
  123. package/lib/types/client/styles/info.css.d.ts +1 -1
  124. package/lib/types/client/styles/info.css.d.ts.map +1 -1
  125. package/lib/types/client/styles/remote-session.css.d.ts +2 -0
  126. package/lib/types/client/styles/remote-session.css.d.ts.map +1 -0
  127. package/lib/types/client-data/current-session.d.ts +41 -0
  128. package/lib/types/client-data/current-session.d.ts.map +1 -0
  129. package/lib/types/client-data/remote-group-expansion.d.ts +76 -0
  130. package/lib/types/client-data/remote-group-expansion.d.ts.map +1 -0
  131. package/lib/types/client-data/remote-status.d.ts +148 -0
  132. package/lib/types/client-data/remote-status.d.ts.map +1 -0
  133. package/lib/types/client-data/settings-form.d.ts +774 -0
  134. package/lib/types/client-data/settings-form.d.ts.map +1 -0
  135. package/lib/types/client-data/shares.d.ts +211 -0
  136. package/lib/types/client-data/shares.d.ts.map +1 -0
  137. package/lib/types/client-pairing.d.ts +114 -0
  138. package/lib/types/client-pairing.d.ts.map +1 -0
  139. package/lib/types/client-routes.d.ts +112 -0
  140. package/lib/types/client-routes.d.ts.map +1 -0
  141. package/lib/types/config.d.ts +200 -0
  142. package/lib/types/config.d.ts.map +1 -0
  143. package/lib/types/fetch-route-intercept.d.ts +156 -0
  144. package/lib/types/fetch-route-intercept.d.ts.map +1 -0
  145. package/lib/types/fingerprint.d.ts +164 -0
  146. package/lib/types/fingerprint.d.ts.map +1 -0
  147. package/lib/types/http.d.ts +41 -0
  148. package/lib/types/http.d.ts.map +1 -0
  149. package/lib/types/index.d.ts +56 -24
  150. package/lib/types/index.d.ts.map +1 -1
  151. package/lib/types/intercept-shape.d.ts +32 -0
  152. package/lib/types/intercept-shape.d.ts.map +1 -0
  153. package/lib/types/intercept.d.ts +432 -0
  154. package/lib/types/intercept.d.ts.map +1 -0
  155. package/lib/types/merge-streams.d.ts +393 -0
  156. package/lib/types/merge-streams.d.ts.map +1 -0
  157. package/lib/types/relay-access.d.ts +310 -0
  158. package/lib/types/relay-access.d.ts.map +1 -0
  159. package/lib/types/relay-client.d.ts +274 -0
  160. package/lib/types/relay-client.d.ts.map +1 -0
  161. package/lib/types/relay-filter.d.ts +103 -0
  162. package/lib/types/relay-filter.d.ts.map +1 -0
  163. package/lib/types/relay-server.d.ts +208 -0
  164. package/lib/types/relay-server.d.ts.map +1 -0
  165. package/lib/types/restart-fields.d.ts +22 -0
  166. package/lib/types/restart-fields.d.ts.map +1 -0
  167. package/lib/types/restart-watcher.d.ts +83 -0
  168. package/lib/types/restart-watcher.d.ts.map +1 -0
  169. package/lib/types/session-reference.d.ts +66 -0
  170. package/lib/types/session-reference.d.ts.map +1 -0
  171. package/lib/types/share-export.d.ts +14 -1
  172. package/lib/types/share-export.d.ts.map +1 -1
  173. package/lib/types/share-ops.d.ts +79 -0
  174. package/lib/types/share-ops.d.ts.map +1 -0
  175. package/lib/types/share-store.d.ts +87 -0
  176. package/lib/types/share-store.d.ts.map +1 -0
  177. package/lib/types/virtual-id.d.ts +38 -0
  178. package/lib/types/virtual-id.d.ts.map +1 -0
  179. package/lib/virtual-id.js +52 -0
  180. package/lib/virtual-id.js.map +1 -0
  181. package/llms.txt +17 -14
  182. package/package.json +60 -50
  183. package/scripts/build-client.mjs +5 -3
  184. package/scripts/check-host-shadow.mjs +19 -2
  185. package/scripts/check-settings-form.mjs +933 -0
  186. package/scripts/check-shares.mjs +397 -0
  187. package/scripts/check-upload-endpoint.mjs +59 -3
  188. package/scripts/sync-doc-version.mjs +8 -15
  189. package/scripts/verify-fingerprint-live.mjs +111 -0
  190. package/src/activity.ts +218 -0
  191. package/src/admin-routes.ts +627 -0
  192. package/src/client/MobileAttachButton.tsx +2 -2
  193. package/src/client/MobileAttachChips.tsx +3 -3
  194. package/src/client/MobileDrawerFooter.tsx +7 -4
  195. package/src/client/MobileHome.tsx +20 -16
  196. package/src/client/MobileHomeChips.tsx +16 -16
  197. package/src/client/MobileNavOverlay.tsx +2 -2
  198. package/src/client/MobileNavToggle.tsx +3 -3
  199. package/src/client/MobileSessionHeader.tsx +63 -42
  200. package/src/client/MobileSessionInfo.tsx +118 -17
  201. package/src/client/RemoteComposerBanner.tsx +76 -0
  202. package/src/client/RemoteHeaderIcon.tsx +103 -0
  203. package/src/client/RemoteShareMenu.tsx +57 -0
  204. package/src/client/RemoteStatusIcon.tsx +91 -0
  205. package/src/client/compat/desktop.ts +22 -0
  206. package/src/client/compat/types.ts +62 -0
  207. package/src/client/current-session-report.ts +164 -0
  208. package/src/client/effects/header-status.ts +21 -17
  209. package/src/client/effects/keyboard-guard.ts +65 -1
  210. package/src/client/effects/permission-sheet.ts +149 -0
  211. package/src/client/effects/remote-session.ts +68 -0
  212. package/src/client/effects/turn-fold.ts +52 -0
  213. package/src/client/index.tsx +151 -36
  214. package/src/client/locales.ts +396 -2
  215. package/src/client/remote-fetch.ts +125 -0
  216. package/src/client/remote-group-expansion.ts +190 -0
  217. package/src/client/remote-share-css.ts +54 -0
  218. package/src/client/remote-share-register.ts +244 -0
  219. package/src/client/remote-status-css.ts +64 -0
  220. package/src/client/remote-status-register.ts +107 -0
  221. package/src/client/settings/SettingsSection.tsx +1229 -0
  222. package/src/client/settings/register-settings.ts +116 -0
  223. package/src/client/settings/settings-css.ts +236 -0
  224. package/src/client/share/share-preview.tsx +6 -3
  225. package/src/client/share/share-sheet.tsx +3 -3
  226. package/src/client/styles/composer.css.ts +119 -26
  227. package/src/client/styles/header.css.ts +7 -7
  228. package/src/client/styles/info.css.ts +74 -0
  229. package/src/client/styles/remote-session.css.ts +251 -0
  230. package/src/client-data/current-session.ts +57 -0
  231. package/src/client-data/remote-group-expansion.ts +145 -0
  232. package/src/client-data/remote-status.ts +374 -0
  233. package/src/client-data/settings-form.ts +1371 -0
  234. package/src/client-data/shares.ts +467 -0
  235. package/src/client-pairing.ts +244 -0
  236. package/src/client-routes.ts +732 -0
  237. package/src/config.ts +447 -0
  238. package/src/fetch-route-intercept.ts +510 -0
  239. package/src/fingerprint.ts +408 -0
  240. package/src/http.ts +68 -0
  241. package/src/index.ts +719 -62
  242. package/src/intercept-shape.ts +87 -0
  243. package/src/intercept.ts +3362 -0
  244. package/src/merge-streams.ts +1272 -0
  245. package/src/relay-access.ts +669 -0
  246. package/src/relay-client.ts +1514 -0
  247. package/src/relay-filter.ts +291 -0
  248. package/src/relay-server.ts +2025 -0
  249. package/src/restart-fields.ts +36 -0
  250. package/src/restart-watcher.ts +140 -0
  251. package/src/session-reference.ts +156 -0
  252. package/src/share-export.ts +26 -1
  253. package/src/share-ops.ts +114 -0
  254. package/src/share-store.ts +373 -0
  255. package/src/virtual-id.ts +59 -0
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Session-activity tracking for the shared remote table (T22c): the bridge
3
+ * from DSH's session event stream to share-store bookkeeping, plus the idle
4
+ * sweeper that puts away sessions gone quiet. Pure logic — no DSH imports,
5
+ * no filesystem, and timers only through injectable functions — so the tests
6
+ * can drive every tick from a fake clock (test/activity.test.cjs).
7
+ *
8
+ * The event mapping is the rule set agreed for "what counts as activity":
9
+ * every session event means the session moved, EXCEPT `session/end-seed`,
10
+ * which DSH appends by itself whenever a session is merely opened (see the
11
+ * main repo's docs/spike-relay.md §2.1 pitfall 2) — counting it would reset
12
+ * the idle clock every time someone looks at a conversation. A turn marks
13
+ * the session busy for its whole span (`turn/start` → `turn/end`), so
14
+ * waiting on an approval or an answer — both happen INSIDE a turn — never
15
+ * idles out either.
16
+ *
17
+ * T22c-fix added parent propagation: subagent sessions never enter the
18
+ * shared table themselves, but their events (a background child grinding on
19
+ * after the parent's own turn ended) must keep the ANCESTORS' idle clocks
20
+ * fresh — {@link createParentIndex} records the child→parent links from
21
+ * session headers, and the tracker touches every ancestor along the chain.
22
+ */
23
+
24
+ import type { ShareStore } from './share-store.js'
25
+
26
+ /** What `createActivityTracker` hands back: one call per session event. */
27
+ export interface ActivityTracker {
28
+ /** Report one session event. No-op for unshared sessions (share-store
29
+ * ignores strangers) and for the `session/end-seed` marker. */
30
+ onEvent(sessionId: string, eventType: string): void
31
+ }
32
+
33
+ /** Auto-appended when a session is opened; a snapshot of attention, not activity. */
34
+ const END_SEED_EVENT = 'session/end-seed'
35
+
36
+ /** Map one DSH session event onto the shared table's clock rules. `parentOf`
37
+ * (optional) reports the parent of a subagent session; when given, a child's
38
+ * event also refreshes every ancestor's clock (touch only — see below). */
39
+ export function createActivityTracker(
40
+ store: ShareStore,
41
+ parentOf: (id: string) => string | undefined = () => undefined,
42
+ ): ActivityTracker {
43
+ // Walk UP from the session itself, touching each ancestor once. Bounded
44
+ // like share-store's own isAccessible walk: 16 hops, and a repeated id
45
+ // (a hostile/corrupt parent chain) stops the walk dead instead of looping.
46
+ const touchAncestors = (sessionId: string): void => {
47
+ const seen = new Set<string>([sessionId])
48
+ let current = sessionId
49
+ for (let hop = 0; hop < MAX_ANCESTOR_HOPS; hop += 1) {
50
+ let parent: string | undefined
51
+ try {
52
+ parent = parentOf(current)
53
+ } catch {
54
+ // A throwing parentOf means "no parent from here".
55
+ return
56
+ }
57
+ if (parent === undefined || parent === '' || seen.has(parent)) return
58
+ seen.add(parent)
59
+ // Touch ONLY: a child's turn start/end says nothing about whether the
60
+ // parent is running or waiting — the parent's own turn events govern
61
+ // its busy flag.
62
+ store.touch(parent)
63
+ current = parent
64
+ }
65
+ }
66
+ return {
67
+ onEvent(sessionId, eventType) {
68
+ if (eventType === END_SEED_EVENT) return
69
+ // The child itself is handled exactly as before (a shared child would
70
+ // get the full mapping; an unshared one is ignored by the store).
71
+ if (eventType === 'turn/start') {
72
+ // Busy AND touched: the touch stamps "activity began here", which is
73
+ // the clock the table falls back to after a restart (busy is not
74
+ // persisted — see the note on startSweeper).
75
+ store.setBusy(sessionId, true)
76
+ store.touch(sessionId)
77
+ } else if (eventType === 'turn/end') {
78
+ // No touch of its own: share-store re-stamps lastActivityAt when busy
79
+ // flips to false, so the idle span restarts at the turn's actual end.
80
+ store.setBusy(sessionId, false)
81
+ } else {
82
+ store.touch(sessionId)
83
+ }
84
+ // A subagent's motion is its ancestors' motion too: a background child
85
+ // (run_in_background) keeps working after the parent's own turn ended,
86
+ // and without this the parent idles out mid-orchestration. The
87
+ // end-seed marker never reaches here — a session being OPENED (even a
88
+ // subagent one) is a look, not motion, and must not defer anyone's
89
+ // idle clock.
90
+ touchAncestors(sessionId)
91
+ },
92
+ }
93
+ }
94
+
95
+ /** The slice of a live DSH Session the parent index reads: the session id
96
+ * plus the two subagent-relevant header fields. Declared structurally so
97
+ * this module never imports DSH types (an optional peer dependency) and the
98
+ * tests can feed plain objects. Field names per DSH 0.2.0 dsh-session's
99
+ * `validateSessionHeader`: `origin` (only legal value `'subagent'`) and
100
+ * `parentSession` (must be a string). */
101
+ export interface ObservableSession {
102
+ id?: unknown
103
+ header?: { origin?: unknown; parentSession?: unknown } | undefined
104
+ }
105
+
106
+ /** Remembers which sessions are subagents of which, as reported by
107
+ * {@link ParentIndex.observe}. */
108
+ export interface ParentIndex {
109
+ /** Record one session's parent link when it IS a subagent; every other
110
+ * shape (top-level sessions, id-less or header-less stubs, undefined) is
111
+ * a no-op. */
112
+ observe(session: ObservableSession | undefined): void
113
+ /** The recorded parent of `id`, or undefined when unknown. */
114
+ parentOf(id: string): string | undefined
115
+ }
116
+
117
+ /**
118
+ * ponytail: 父子表上限 5000 条,超出按最早插入淘汰。这是内存里的纯派生数据
119
+ * (重启后由 session/event 重建),淘汰只发生在「第 5001 个新子会话」出现时,
120
+ * 按插入序删最早的一条;反复 observe 已有 id 只覆盖值、不动位置。上限挡的是
121
+ * 长驻进程里无限累积的子会话 id,5000 对单机并发子智能体绰绰有余。
122
+ */
123
+ /** Entries the parent index keeps before the oldest insertion is evicted. */
124
+ const MAX_PARENT_ENTRIES = 5000
125
+
126
+ /** Same-walk ancestor bound as share-store's isAccessible: 16 hops. */
127
+ const MAX_ANCESTOR_HOPS = 16
128
+
129
+ /**
130
+ * The child→parent table backing subagent activity propagation (T22c-fix):
131
+ * the tracker refreshes ancestors' clocks and the relay's `isAccessible`
132
+ * walks it to decide whether a subagent session is reachable. Pure in-memory
133
+ * — rebuilt from live `session/event` traffic after every restart, so there
134
+ * is nothing to persist and nothing to invalidate.
135
+ */
136
+ export function createParentIndex(): ParentIndex {
137
+ // Insertion order is the eviction order: Map preserves it.
138
+ const parents = new Map<string, string>()
139
+ return {
140
+ observe(session) {
141
+ const header = session?.header
142
+ if (header === undefined || header.origin !== 'subagent') return
143
+ const { parentSession } = header
144
+ if (typeof parentSession !== 'string' || parentSession === '') return
145
+ const id = session?.id
146
+ // A self-parent would poison every future walk into a 1-cycle; the
147
+ // seen-set in the walkers already survives it, but there is no honest
148
+ // link to record in the first place.
149
+ if (typeof id !== 'string' || id === '' || id === parentSession) return
150
+ if (!parents.has(id) && parents.size >= MAX_PARENT_ENTRIES) {
151
+ const oldest = parents.keys().next().value
152
+ if (oldest !== undefined) parents.delete(oldest)
153
+ }
154
+ parents.set(id, parentSession)
155
+ },
156
+
157
+ parentOf(id) {
158
+ return parents.get(id)
159
+ },
160
+ }
161
+ }
162
+
163
+ /** Duck-typed timer halves: Node's setInterval/clearInterval at runtime,
164
+ * fakes in tests. Loose on purpose — the real NodeJS.Timeout type would
165
+ * fight the contravariant `clearInterval` parameter under strictFunctionTypes. */
166
+ type StartInterval = (callback: () => void, ms: number) => unknown
167
+ type ClearInterval = (handle: unknown) => void
168
+
169
+ export interface SweeperOptions {
170
+ /** The shared table to sweep. */
171
+ store: ShareStore
172
+ /** Read fresh before EVERY sweep: `idleHours` is a volatile row setting
173
+ * that can change without a restart, so it is never snapshotted here. */
174
+ getIdleHours: () => number
175
+ /** Sweep cadence in ms. Default 60_000 — a minute late is fine for a
176
+ * threshold measured in hours. */
177
+ intervalMs?: number
178
+ /** Injectable timer halves (tests drive ticks by hand). */
179
+ setIntervalImpl?: StartInterval
180
+ clearIntervalImpl?: ClearInterval
181
+ }
182
+
183
+ /**
184
+ * ponytail: 忙碌状态不写盘(share-store 的持久化刻意省略 busy)。一个会话在回合
185
+ * 中(比如等审批)经历了 DSH 重启,重启后按空闲处理,计时起点仍是回合开始时
186
+ * `touch` 打下的时间戳;如果那时距重启已超过 idleHours,重启后的第一次扫描就会
187
+ * 关闭它的远程。DSH 重启本身会中断正在进行的回合,所以这种情况下关闭远程是
188
+ * 可以接受的。
189
+ *
190
+ * Start the idle sweeper: every interval, first feed the table the CURRENT
191
+ * idle budget, then close whatever has been quiet past it. Returns the stop
192
+ * function (the host half hands it to ctx.effect). Two failure disciplines:
193
+ * a throwing getIdleHours or sweep is swallowed — the timer must survive to
194
+ * try again next tick — and the underlying timer is unref()ed so the sweeper
195
+ * alone never keeps the DSH process alive.
196
+ */
197
+ export function startSweeper(options: SweeperOptions): () => void {
198
+ const { store, getIdleHours, intervalMs = 60_000 } = options
199
+ const setIntervalImpl: StartInterval = options.setIntervalImpl ?? ((callback, ms) => setInterval(callback, ms))
200
+ const clearIntervalImpl: ClearInterval =
201
+ options.clearIntervalImpl ?? ((handle) => clearInterval(handle as ReturnType<typeof setInterval>))
202
+ const tick = () => {
203
+ try {
204
+ store.setIdleHours(getIdleHours())
205
+ store.sweep()
206
+ } catch {
207
+ // Anything thrown here (a hostile getIdleHours, a disk hiccup in
208
+ // sweep's persist) must not kill the timer: the next interval retries.
209
+ }
210
+ }
211
+ const handle = setIntervalImpl(tick, intervalMs)
212
+ if (typeof handle === 'object' && handle !== null && typeof (handle as { unref?: unknown }).unref === 'function') {
213
+ ;(handle as { unref: () => void }).unref()
214
+ }
215
+ return () => {
216
+ clearIntervalImpl(handle)
217
+ }
218
+ }