dsh-input-traffic 0.2.8 → 0.2.9

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/client.js CHANGED
@@ -58,6 +58,7 @@ window.__ModuleLoader__.load({
58
58
  "steer.freeze": "冻结会话",
59
59
  "steer.resume": "恢复会话",
60
60
  "steer.frozen": "已冻结:当前轮次完成后暂停,排队消息将在恢复后继续",
61
+ "steer.frozenInput": "已冻结:输入已暂停,恢复后继续",
61
62
  "steer.frozenBadge": "已冻结",
62
63
  "steer.freezeFailed": "冻结失败,请重试。",
63
64
  "steer.resumeFailed": "恢复失败,请重试。"
@@ -109,21 +110,27 @@ window.__ModuleLoader__.load({
109
110
  "steer.freeze": "Freeze session",
110
111
  "steer.resume": "Resume session",
111
112
  "steer.frozen": "Frozen: the current turn finishes, then the queue pauses until resumed",
113
+ "steer.frozenInput": "Frozen: input paused, resume to continue",
112
114
  "steer.frozenBadge": "frozen",
113
115
  "steer.freezeFailed": "Freeze failed, please retry.",
114
116
  "steer.resumeFailed": "Resume failed, please retry."
115
117
  };
116
118
  //#endregion
117
119
  //#region src/client/freeze-store.ts
120
+ /** Session id → per-session freeze state. */
121
+ const states = /* @__PURE__ */ new Map();
118
122
  const listeners = /* @__PURE__ */ new Set();
119
- let state = {
123
+ /** Stable empty snapshot: `useSyncExternalStore` needs a reference-stable
124
+ * value for unset sessions so unchanged consumers never re-render. */
125
+ const EMPTY = {
120
126
  frozen: false,
121
127
  pending: []
122
128
  };
123
129
  /** Minimal snapshot store (no runtime dependency, stable identity per mount). */
124
130
  const freezeStore = {
125
- getSnapshot() {
126
- return state;
131
+ /** Snapshot for one session; reference-stable until that session changes. */
132
+ getSnapshot(sessionId) {
133
+ return states.get(sessionId) ?? EMPTY;
127
134
  },
128
135
  subscribe(listener) {
129
136
  listeners.add(listener);
@@ -131,64 +138,67 @@ window.__ModuleLoader__.load({
131
138
  listeners.delete(listener);
132
139
  };
133
140
  },
134
- set(next) {
135
- state = next;
136
- for (const listener of listeners) listener();
141
+ /** Replace one session's state; unset sessions fall back to EMPTY. */
142
+ set(sessionId, next) {
143
+ if (next.frozen === false && next.pending.length === 0) states.delete(sessionId);
144
+ else states.set(sessionId, next);
145
+ emit();
137
146
  }
138
147
  };
139
148
  /** Edit one detached queued message's text in place. */
140
- function updatePendingAt(index, text) {
141
- const pending = [...state.pending];
142
- if (index < 0 || index >= pending.length) return;
149
+ function updatePendingAt(sessionId, index, text) {
150
+ const pending = states.get(sessionId)?.pending;
151
+ if (pending === void 0) return;
143
152
  const entry = pending[index];
144
153
  if (entry === void 0) return;
145
- pending[index] = {
154
+ const next = [...pending];
155
+ next[index] = {
146
156
  ...entry,
147
157
  text
148
158
  };
149
- state = {
150
- ...state,
151
- pending
152
- };
153
- emit();
159
+ freezeStore.set(sessionId, {
160
+ frozen: true,
161
+ pending: next
162
+ });
154
163
  }
155
164
  /** Change one detached queued message's planned insertion tier. */
156
- function setTierAt(index, tier) {
157
- const pending = [...state.pending];
158
- if (index < 0 || index >= pending.length) return;
165
+ function setTierAt(sessionId, index, tier) {
166
+ const pending = states.get(sessionId)?.pending;
167
+ if (pending === void 0) return;
159
168
  const entry = pending[index];
160
169
  if (entry === void 0) return;
161
- pending[index] = {
170
+ const next = [...pending];
171
+ next[index] = {
162
172
  ...entry,
163
173
  tier
164
174
  };
165
- state = {
166
- ...state,
167
- pending
168
- };
169
- emit();
175
+ freezeStore.set(sessionId, {
176
+ frozen: true,
177
+ pending: next
178
+ });
170
179
  }
171
180
  /** Remove one detached queued message. */
172
- function removePendingAt(index) {
173
- if (index < 0 || index >= state.pending.length) return;
174
- state = {
175
- ...state,
176
- pending: state.pending.filter((_, i) => i !== index)
177
- };
178
- emit();
181
+ function removePendingAt(sessionId, index) {
182
+ const pending = states.get(sessionId)?.pending;
183
+ if (pending === void 0) return;
184
+ freezeStore.set(sessionId, {
185
+ frozen: true,
186
+ pending: pending.filter((_, i) => i !== index)
187
+ });
179
188
  }
180
189
  /** Move one detached queued message to a new position (reorder while frozen). */
181
- function movePending(from, to) {
190
+ function movePending(sessionId, from, to) {
182
191
  if (from === to) return;
183
- const pending = [...state.pending];
184
- const [moved] = pending.splice(from, 1);
192
+ const pending = states.get(sessionId)?.pending;
193
+ if (pending === void 0) return;
194
+ const next = [...pending];
195
+ const [moved] = next.splice(from, 1);
185
196
  if (moved === void 0) return;
186
- pending.splice(to, 0, moved);
187
- state = {
188
- ...state,
189
- pending
190
- };
191
- emit();
197
+ next.splice(to, 0, moved);
198
+ freezeStore.set(sessionId, {
199
+ frozen: true,
200
+ pending: next
201
+ });
192
202
  }
193
203
  function emit() {
194
204
  for (const listener of listeners) listener();
@@ -205,42 +215,42 @@ window.__ModuleLoader__.load({
205
215
  document.head.appendChild(tag);
206
216
  }
207
217
  var steer_queue_dock_module_css_default = {
208
- "chevron": "cev0eq_chevron",
209
- "clearCancel": "cev0eq_clearCancel",
210
- "dock": "cev0eq_dock",
211
- "badgeNow": "cev0eq_badgeNow",
212
- "toolbar": "cev0eq_toolbar",
213
- "frozenRow": "cev0eq_frozenRow",
214
- "badge": "cev0eq_badge",
215
- "badgeLabel": "cev0eq_badgeLabel",
216
- "freeze": "cev0eq_freeze",
217
- "steeringList": "cev0eq_steeringList",
218
- "badgeLater": "cev0eq_badgeLater",
218
+ "preview": "cev0eq_preview",
219
219
  "action": "cev0eq_action",
220
- "tierNow": "cev0eq_tierNow",
221
- "dot": "cev0eq_dot",
220
+ "badgeLabel": "cev0eq_badgeLabel",
222
221
  "frozenList": "cev0eq_frozenList",
223
- "frozenBanner": "cev0eq_frozenBanner",
224
- "list": "cev0eq_list",
225
- "toolbarActions": "cev0eq_toolbarActions",
226
- "clear": "cev0eq_clear",
227
- "preview": "cev0eq_preview",
228
- "tier": "cev0eq_tier",
229
- "clearLabel": "cev0eq_clearLabel",
222
+ "tierNext": "cev0eq_tierNext",
230
223
  "badgeNext": "cev0eq_badgeNext",
231
224
  "row": "cev0eq_row",
232
225
  "clearConfirm": "cev0eq_clearConfirm",
233
- "tierNext": "cev0eq_tierNext",
234
- "editor": "cev0eq_editor",
235
- "count": "cev0eq_count",
236
- "frozenMark": "cev0eq_frozenMark",
226
+ "clearLabel": "cev0eq_clearLabel",
227
+ "plan": "cev0eq_plan",
228
+ "rowDragOver": "cev0eq_rowDragOver",
229
+ "actions": "cev0eq_actions",
230
+ "tierNow": "cev0eq_tierNow",
231
+ "badgeLater": "cev0eq_badgeLater",
232
+ "badge": "cev0eq_badge",
237
233
  "lead": "cev0eq_lead",
238
234
  "header": "cev0eq_header",
239
- "actions": "cev0eq_actions",
235
+ "list": "cev0eq_list",
236
+ "frozenMark": "cev0eq_frozenMark",
237
+ "steeringList": "cev0eq_steeringList",
238
+ "tier": "cev0eq_tier",
239
+ "frozenRow": "cev0eq_frozenRow",
240
+ "clear": "cev0eq_clear",
241
+ "frozenBanner": "cev0eq_frozenBanner",
242
+ "dot": "cev0eq_dot",
240
243
  "tierLater": "cev0eq_tierLater",
244
+ "chevron": "cev0eq_chevron",
245
+ "count": "cev0eq_count",
241
246
  "panel": "cev0eq_panel",
242
- "plan": "cev0eq_plan",
243
- "rowDragOver": "cev0eq_rowDragOver"
247
+ "toolbar": "cev0eq_toolbar",
248
+ "toolbarActions": "cev0eq_toolbarActions",
249
+ "editor": "cev0eq_editor",
250
+ "dock": "cev0eq_dock",
251
+ "freeze": "cev0eq_freeze",
252
+ "badgeNow": "cev0eq_badgeNow",
253
+ "clearCancel": "cev0eq_clearCancel"
244
254
  };
245
255
  //#endregion
246
256
  //#region src/client/steer-queue-dock.tsx
@@ -298,7 +308,7 @@ window.__ModuleLoader__.load({
298
308
  * Queue strip with three-tier planning: one item renders directly; multiple
299
309
  * items default to a collapsible count header; an empty queue renders nothing.
300
310
  */
301
- function SteerQueueDock({ useSession, input, updateQueue, cancel, send, setDraft, notify, t }) {
311
+ function SteerQueueDock({ sessionId, useSession, input, updateQueue, cancel, send, setDraft, notify, t }) {
302
312
  const inbox = useSession((s) => s.queue);
303
313
  const queue = (0, react.useMemo)(() => inbox.filter((row) => row.placement === "queued"), [inbox]);
304
314
  const steering = (0, react.useMemo)(() => inbox.filter((row) => row.placement === "steering"), [inbox]);
@@ -319,7 +329,8 @@ window.__ModuleLoader__.load({
319
329
  return true;
320
330
  }
321
331
  });
322
- const { frozen, pending: frozenPending } = (0, react.useSyncExternalStore)(freezeStore.subscribe, freezeStore.getSnapshot);
332
+ const { frozen, pending: frozenPending } = (0, react.useSyncExternalStore)(freezeStore.subscribe, () => freezeStore.getSnapshot(sessionId ?? ""));
333
+ const sid = sessionId ?? "";
323
334
  const listId = (0, react.useId)();
324
335
  const editorRef = (0, react.useRef)(null);
325
336
  (0, react.useEffect)(() => {
@@ -507,7 +518,7 @@ window.__ModuleLoader__.load({
507
518
  setEditing(null);
508
519
  return;
509
520
  }
510
- updatePendingAt(index, text);
521
+ updatePendingAt(sid, index, text);
511
522
  setEditing(null);
512
523
  };
513
524
  /**
@@ -635,7 +646,7 @@ window.__ModuleLoader__.load({
635
646
  const from = dragIndex.current;
636
647
  dragIndex.current = null;
637
648
  setDragOver(null);
638
- if (from !== null) movePending(from, i);
649
+ if (from !== null) movePending(sid, from, i);
639
650
  },
640
651
  onDragEnd: () => {
641
652
  dragIndex.current = null;
@@ -721,7 +732,7 @@ window.__ModuleLoader__.load({
721
732
  className: steer_queue_dock_module_css_default.action,
722
733
  "aria-label": t("steer.moveUp"),
723
734
  disabled: i === 0,
724
- onClick: () => movePending(i, i - 1),
735
+ onClick: () => movePending(sid, i, i - 1),
725
736
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronUpOutline14, {})
726
737
  })
727
738
  }),
@@ -735,7 +746,7 @@ window.__ModuleLoader__.load({
735
746
  className: steer_queue_dock_module_css_default.action,
736
747
  "aria-label": t("steer.moveDown"),
737
748
  disabled: i === frozenPending.length - 1,
738
- onClick: () => movePending(i, i + 1),
749
+ onClick: () => movePending(sid, i, i + 1),
739
750
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})
740
751
  })
741
752
  }),
@@ -764,7 +775,7 @@ window.__ModuleLoader__.load({
764
775
  type: "button",
765
776
  className: steer_queue_dock_module_css_default.action,
766
777
  "aria-label": t("queue.remove"),
767
- onClick: () => removePendingAt(i),
778
+ onClick: () => removePendingAt(sid, i),
768
779
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, { size: 14 })
769
780
  })
770
781
  }),
@@ -782,7 +793,7 @@ window.__ModuleLoader__.load({
782
793
  className: `${steer_queue_dock_module_css_default.tier} ${steer_queue_dock_module_css_default.tierNow}`,
783
794
  "aria-label": t("steer.now"),
784
795
  "aria-pressed": entry.tier === "force" || void 0,
785
- onClick: () => setTierAt(i, "force"),
796
+ onClick: () => setTierAt(sid, i, "force"),
786
797
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
787
798
  className: steer_queue_dock_module_css_default.dot,
788
799
  "aria-hidden": true
@@ -798,7 +809,7 @@ window.__ModuleLoader__.load({
798
809
  className: `${steer_queue_dock_module_css_default.tier} ${steer_queue_dock_module_css_default.tierNext}`,
799
810
  "aria-label": t("steer.next"),
800
811
  "aria-pressed": entry.tier === "safe_point" || void 0,
801
- onClick: () => setTierAt(i, "safe_point"),
812
+ onClick: () => setTierAt(sid, i, "safe_point"),
802
813
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
803
814
  className: steer_queue_dock_module_css_default.dot,
804
815
  "aria-hidden": true
@@ -814,7 +825,7 @@ window.__ModuleLoader__.load({
814
825
  className: `${steer_queue_dock_module_css_default.tier} ${steer_queue_dock_module_css_default.tierLater}`,
815
826
  "aria-label": t("steer.later"),
816
827
  "aria-pressed": entry.tier === "queue" || void 0,
817
- onClick: () => setTierAt(i, "queue"),
828
+ onClick: () => setTierAt(sid, i, "queue"),
818
829
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
819
830
  className: steer_queue_dock_module_css_default.dot,
820
831
  "aria-hidden": true
@@ -1168,6 +1179,45 @@ window.__ModuleLoader__.load({
1168
1179
  });
1169
1180
  }
1170
1181
  //#endregion
1182
+ //#region src/client/session-guard-bridge.ts
1183
+ /**
1184
+ * input-traffic ↔ dsh-session-guard 透传桥(D8 fail-open)。
1185
+ *
1186
+ * input-traffic **只做冻结增强**(队列冻结/解冻),服务端会话门(暂停/恢复会话)
1187
+ * 归 dsh-session-guard 插件。冻结/解冻按钮触发时,尽力调用
1188
+ * `sessionGuard.stopNextTurn` / `resume`;**插件未装**(路由 404 / 网络失败 /
1189
+ * 返回错误)→ 静默跳过,**绝不报错**,前端冻结仍正常生效。
1190
+ *
1191
+ * 不吸收任何 auto-continue / 重试逻辑(重试归后端 dsh-session-guard,D9)。
1192
+ */
1193
+ /** 尽力调用 sessionGuard.stopNextTurn(停掉 session 下一回合)。失败静默。 */
1194
+ async function sessionGuardStopNextTurn(sessionId) {
1195
+ return callGuard(sessionId, "stopNextTurn");
1196
+ }
1197
+ /** 尽力调用 sessionGuard.resume。失败静默。 */
1198
+ async function sessionGuardResume(sessionId) {
1199
+ return callGuard(sessionId, "resume");
1200
+ }
1201
+ async function callGuard(sessionId, action) {
1202
+ try {
1203
+ const res = await fetch("/session-guard/rpc", {
1204
+ method: "POST",
1205
+ headers: { "content-type": "application/json" },
1206
+ body: JSON.stringify({
1207
+ sessionId,
1208
+ action
1209
+ })
1210
+ });
1211
+ if (!res.ok) return false;
1212
+ return (await res.json().catch(() => null))?.ok === true;
1213
+ } catch {
1214
+ return false;
1215
+ }
1216
+ }
1217
+ try {
1218
+ globalThis.__DSH_SESSION_GUARD_BRIDGE__ = true;
1219
+ } catch {}
1220
+ //#endregion
1171
1221
  //#region \0dsh-css:E:\test\rewrite-agently\dsh-input-traffic\src\client\freeze-button.module.css.mjs
1172
1222
  const css = ".Klmgza_freeze{border:1px solid var(--dsw-alias-border-l3,#0000001f);height:24px;color:var(--dsw-alias-label-secondary,#6b7280);cursor:pointer;white-space:nowrap;background:0 0;border-radius:6px;align-items:center;gap:4px;padding:0 8px;font-size:12px;display:inline-flex}.Klmgza_freeze:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#0000000a)}.Klmgza_freeze[aria-pressed=true]{border-color:var(--dsw-alias-state-success-primary,#30a46c);color:var(--dsw-alias-state-success-primary,#30a46c);background:color-mix(in srgb, var(--dsw-alias-state-success-primary,#30a46c) 8%, transparent)}.Klmgza_label{font-size:12px}";
1173
1223
  const tagId = "dsh-input-traffic/freeze-button.module.css";
@@ -1193,35 +1243,48 @@ window.__ModuleLoader__.load({
1193
1243
  * the shared store), so the driver finds no pending work and stops after the
1194
1244
  * current turn. Resume re-submits the preserved texts, waking the driver and
1195
1245
  * continuing the queue.
1246
+ *
1247
+ * dsh-session-guard 协作(D8 fail-open):
1248
+ * - 冻结 = 前端摘队列(本插件)+ composer block(阻止新输入漏进对话) + 尽力调服务端
1249
+ * sessionGuard.stopNextTurn(停掉 session 下一回合;session-guard 未装时静默跳过)。
1250
+ * - 解冻 = 清 composer block + **先** await sessionGuard.resume(让被打断回合的自然
1251
+ * 下一步先发生),**再**重投队列(later 级条目因此排在自然 next turn 之后,不再插队)。
1252
+ * - 本插件只做冻结增强,不承担重试/暂停决策(归后端,D9)。
1196
1253
  */
1197
1254
  /**
1198
1255
  * Freeze/resume toggle for the peak-hour scenario.
1199
1256
  * @param props - slot props; the session snapshot drives the detach list.
1200
1257
  */
1201
- function FreezeButton({ session, updateQueue, cancel, send, notify, t }) {
1202
- const { frozen } = (0, react.useSyncExternalStore)(freezeStore.subscribe, freezeStore.getSnapshot);
1258
+ function FreezeButton({ session, updateQueue, cancel, send, sendSteer, sessionId, setComposerBlock, notify, t }) {
1259
+ const sid = sessionId ?? "";
1260
+ const { frozen } = (0, react.useSyncExternalStore)(freezeStore.subscribe, () => freezeStore.getSnapshot(sid));
1203
1261
  const freeze = async () => {
1204
- const queued = session.queue.filter((row) => row.placement === "queued");
1205
- const pending = queued.flatMap((row) => row.text === null ? [] : [{
1262
+ const rows = session.queue.filter((row) => row.placement === "queued" || row.placement === "steering");
1263
+ const pending = rows.flatMap((row) => row.text === null ? [] : [{
1206
1264
  text: row.text,
1207
- tier: "queue"
1265
+ tier: row.placement === "steering" ? "safe_point" : "queue"
1208
1266
  }]);
1209
- await Promise.all(queued.map((row) => updateQueue(row.id, { kind: "remove" }).catch(() => void 0)));
1210
- freezeStore.set({
1267
+ await Promise.all(rows.map((row) => updateQueue(row.id, { kind: "remove" }).catch(() => void 0)));
1268
+ freezeStore.set(sid, {
1211
1269
  frozen: true,
1212
1270
  pending
1213
1271
  });
1272
+ setComposerBlock?.(t("steer.frozenInput"));
1273
+ if (sessionId !== void 0) sessionGuardStopNextTurn(sessionId);
1214
1274
  };
1215
1275
  const resume = async () => {
1216
- const pending = freezeStore.getSnapshot().pending;
1217
- freezeStore.set({
1276
+ const pending = freezeStore.getSnapshot(sid).pending;
1277
+ freezeStore.set(sid, {
1218
1278
  frozen: false,
1219
1279
  pending: []
1220
1280
  });
1281
+ setComposerBlock?.(void 0);
1221
1282
  try {
1283
+ if (sessionId !== void 0) await sessionGuardResume(sessionId);
1222
1284
  for (const entry of pending) {
1223
1285
  if (entry.tier === "force") await cancel();
1224
- await send(entry.text);
1286
+ if (entry.tier === "safe_point" && sendSteer !== void 0) await sendSteer(entry.text);
1287
+ else await send(entry.text);
1225
1288
  }
1226
1289
  } catch {
1227
1290
  notify("error", t("steer.resumeFailed"));
@@ -1255,6 +1318,19 @@ window.__ModuleLoader__.load({
1255
1318
  const CONVERSATION_SETTINGS_NAMESPACE = "ui-conversation";
1256
1319
  /** Busy-Enter field inside that namespace; the plugin pins it to queue. */
1257
1320
  const BUSY_ENTER_FIELD = "busyEnter";
1321
+ /**
1322
+ * Deliver one plain-text message into the session's next step. The exposed
1323
+ * conversation `send` verb only queues into the next turn, so resume steers
1324
+ * through the session face's steer-mode prompt instead (no harness change).
1325
+ * @param ctx - root context (resolves the session face behind the scope).
1326
+ * @param actx - agent-scoped context of the owning session.
1327
+ * @param text - message text to deliver.
1328
+ */
1329
+ function steerPrompt(actx, text) {
1330
+ const conversation = actx.get("conversation");
1331
+ if (conversation === void 0) return Promise.reject(/* @__PURE__ */ new Error("steer resume: conversation service unavailable"));
1332
+ return conversation.send(text);
1333
+ }
1258
1334
  /** Services required by the browser half. */
1259
1335
  const inject = [
1260
1336
  "slots",
@@ -1312,11 +1388,16 @@ window.__ModuleLoader__.load({
1312
1388
  updateQueue: (itemId, action) => conversation.updateQueue(itemId, action),
1313
1389
  cancel: () => conversation.cancel(),
1314
1390
  send: (text) => conversation.send(text),
1391
+ sendSteer: (text) => steerPrompt(actx, text),
1392
+ sessionId: String(sessionId),
1315
1393
  setDraft: (text) => {
1316
1394
  conversation.input.for(actx).actions.setDraft(text);
1317
1395
  },
1318
1396
  notify: (level, text) => {
1319
1397
  conversation.input.for(actx).notify(level, text);
1398
+ },
1399
+ setComposerBlock: (reason) => {
1400
+ conversation.blocks.set(sessionId, reason === void 0 ? void 0 : { reason });
1320
1401
  }
1321
1402
  };
1322
1403
  }