dsh-session-guard 0.1.5-beta.1 → 0.3.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.
package/src/wiring.js CHANGED
@@ -21,6 +21,7 @@ import { msUntilOffPeak, shouldPause } from './time.js'
21
21
  * @param {object} deps.ctx host context
22
22
  * @param {()=>object} deps.getSettings 读实时配置
23
23
  * @param {{stopNextTurn:Function, resume:Function}} deps.gate 会话门
24
+ * @param {ReturnType<import('./step-gate.js').createStepGate>} [deps.stepGate] step 级门控(v0.2.0)
24
25
  * @param {object} [deps.logger]
25
26
  * @param {()=>Date} [deps.clock]
26
27
  * @param {typeof setTimeout} [deps.setTimer]
@@ -31,6 +32,7 @@ export function createWiring({
31
32
  ctx,
32
33
  getSettings,
33
34
  gate,
35
+ stepGate,
34
36
  logger,
35
37
  clock = () => new Date(),
36
38
  setTimer = setTimeout,
@@ -110,16 +112,24 @@ export function createWiring({
110
112
 
111
113
  /**
112
114
  * 高峰期目标从官方变为非官方 → 恢复本插件因入峰而暂停的会话(spec 需求 5)。
113
- * 只对 `pausedByPeak` 里的会话生效(不碰用户手动暂停的);受 `deferredResume` 约束;
115
+ * v0.2.0:先释放 step 门控(幂等,无门则 no-op);turn 级只对 `pausedByPeak` 里的
116
+ * 会话生效(不碰用户手动暂停的);受 `deferredResume` 约束;
114
117
  * 用 queueMicrotask 跳出当前事件派发,避免 resume→followup→事件 的重入。
115
118
  */
116
119
  function maybeAutoResume(sessionId, rec) {
117
- if (!pausedByPeak.has(sessionId)) return
118
120
  const cfg = safeCfg()
119
121
  if (cfg.providerGuard !== true || cfg.deferredResume === false) return
120
122
  if (!shouldPause(cfg, clock()).pause) return
121
123
  const cls = directory.classify(rec.provider)
122
124
  if (cls.official) return
125
+ // step 门控:目标转非官方 → 放行被挂起的 step(step gate 自身按目标判定,无需 pausedByPeak)
126
+ if (stepGate) {
127
+ const r = stepGate.release(sessionId, 'non-official')
128
+ if (r && r.released === true) {
129
+ logger?.info?.(`[session-guard] target switched to non-official "${rec.provider}" (matchedBy=${cls.matchedBy}) — releasing step gate for ${sessionId}`)
130
+ }
131
+ }
132
+ if (!pausedByPeak.has(sessionId)) return
123
133
  pausedByPeak.delete(sessionId)
124
134
  logger?.info?.(`[session-guard] target switched to non-official "${rec.provider}" (matchedBy=${cls.matchedBy}) — auto-resuming ${sessionId}`)
125
135
  queueMicrotask(() => {
@@ -143,6 +153,8 @@ export function createWiring({
143
153
  const agents = ctx && ctx.agents
144
154
  if (!agents) return { paused: [], skipped: [] }
145
155
  const roots = typeof agents.roots === 'function' ? agents.roots() : typeof agents.list === 'function' ? agents.list() : []
156
+ // Q1-A:step 级门控开启时,入峰不再 turn 级暂停——会话跑到下一个 pre-step 边界自动拉门。
157
+ const stepLevel = cfg.stepLevelPause === true && !!stepGate
146
158
  const paused = []
147
159
  const skipped = []
148
160
  for (const agent of Array.isArray(roots) ? roots : []) {
@@ -157,6 +169,12 @@ export function createWiring({
157
169
  skipped.push({ sessionId: id, why: 'non-official', provider: targets.providerOf(id) })
158
170
  continue
159
171
  }
172
+ if (stepLevel) {
173
+ // 新一轮高峰:清掉上一轮的 bypass,让 step 门重新生效。
174
+ stepGate.clearBypass(id)
175
+ paused.push({ sessionId: id, via: 'stepGate', ok: true })
176
+ continue
177
+ }
160
178
  try {
161
179
  const r = await gate.stopNextTurn(id, { mode: cfg.pauseMode, reason: cfg.pauseReason })
162
180
  if (!r || r.ok !== false) pausedByPeak.add(id)
@@ -165,7 +183,7 @@ export function createWiring({
165
183
  warn(`stopNextTurn failed for ${id}: ${String(e && e.message || e)}`)
166
184
  }
167
185
  }
168
- logger?.info?.(`[session-guard] peak entered — paused ${paused.length} session(s): ${JSON.stringify(paused)}; skipped ${JSON.stringify(skipped)}`)
186
+ logger?.info?.(`[session-guard] peak entered — ${stepLevel ? 'step-level gate armed for' : 'paused'} ${paused.length} session(s): ${JSON.stringify(paused)}; skipped ${JSON.stringify(skipped)}`)
169
187
  return { paused, skipped }
170
188
  }
171
189
 
@@ -188,12 +206,18 @@ export function createWiring({
188
206
  }
189
207
  }
190
208
 
191
- /** 退峰:先放行挂起的请求,再决定 error 模式续跑 / 恢复暂停的会话。 */
209
+ /** 退峰:先释放 step 门(回合原地续跑),再放行挂起的请求、恢复暂停的会话。 */
192
210
  async function onLeavePeak(cfg) {
193
211
  if (releaseTimer !== null) {
194
212
  clearTimer(releaseTimer)
195
213
  releaseTimer = null
196
214
  }
215
+ // 0) step 级门控:先放行被挂起的 step —— 回合本来就还开着,无需 followup。
216
+ if (stepGate) {
217
+ const releasedSteps = stepGate.releaseAll('off-peak')
218
+ stepGate.clearAllBypass()
219
+ if (releasedSteps.length > 0) logger?.info?.(`[session-guard] peak left — released ${releasedSteps.length} step gate(s): ${JSON.stringify(releasedSteps)}`)
220
+ }
197
221
  // 1) 先取延后记录快照(releaseAll 会清空),再放行全部挂起。
198
222
  const deferredSnapshot = deferrals.deferredList()
199
223
  const { released } = deferrals.releaseAll('off-peak')
@@ -237,6 +261,19 @@ export function createWiring({
237
261
  return guard.install()
238
262
  }
239
263
 
264
+ /**
265
+ * 安装 step 级门控(`agent/pre-step` waterfall,v0.2.0)。
266
+ * disposer 释放全部挂起门,避免卸载后 Promise 悬挂。
267
+ */
268
+ function installStepGuard() {
269
+ if (!stepGate || !ctx || typeof ctx.on !== 'function') return () => {}
270
+ const off = ctx.on('agent/pre-step', (payload, next) => stepGate.hold(payload, next))
271
+ return () => {
272
+ if (typeof off === 'function') off()
273
+ stepGate.releaseAll('disposed')
274
+ }
275
+ }
276
+
240
277
  /** 插件卸载:清定时器 + 拒绝全部挂起(不泄漏 promise)。 */
241
278
  function dispose() {
242
279
  if (releaseTimer !== null) {
@@ -244,6 +281,10 @@ export function createWiring({
244
281
  releaseTimer = null
245
282
  }
246
283
  pausedByPeak.clear()
284
+ if (stepGate) {
285
+ stepGate.releaseAll('disposed')
286
+ stepGate.clearAllBypass()
287
+ }
247
288
  deferrals.rejectAll('disposed')
248
289
  }
249
290
 
@@ -258,6 +299,7 @@ export function createWiring({
258
299
  scheduleRelease,
259
300
  shouldPauseSession,
260
301
  installGuard,
302
+ installStepGuard,
261
303
  dispose,
262
304
  }
263
305
  }