@michengai/dsh-btw 0.1.23 → 0.1.24

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
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.24] - 2026-10-15
4
+
5
+ - Side questions are no longer offered in a subagent session. Typing `/btw` there explains that you should return to the main session, instead of leaving a dialog that cannot be closed (#7).
6
+ - Closing a side question that the host rejects because the session is owned by subagent routing no longer deletes an answer that is still on the way or already returned. A request that never started, or was cancelled before it produced an answer, can still be dismissed locally.
7
+
3
8
  ## [0.1.23] - 2026-10-15
4
9
 
5
10
  - Relax hardcoded timeout: Replace the strict 90s timer with a 30-minute watchdog, supporting long chain-of-thought and max-reasoning models while safeguarding concurrency slots against stalled connections (#6).
@@ -1,5 +1,10 @@
1
1
  # 更新日志
2
2
 
3
+ ## [0.1.24] - 2026-10-15
4
+
5
+ - 子代理会话不再提供旁问。在那里输入 `/btw` 会提示返回主会话使用,而不是留下关不掉的弹窗 (#7)。
6
+ - 宿主因子代理路由拒绝关闭时,不再删掉还在返回或已经返回的答案。尚未启动,或在产生答案前被取消的请求,仍可在本地关掉。
7
+
3
8
  ## [0.1.23] - 2026-10-15
4
9
 
5
10
  - 放宽硬超时限制:默认超时由写死的 90s 调整为 30 分钟兜底看门狗,支持长思维链与深度推理模型充分思考;同时保留长连接死锁自动回收机制保护并发槽位 (#6)。
package/lib/client.js CHANGED
@@ -519,6 +519,7 @@ var zh = {
519
519
  "error.attachments": "\u65C1\u95EE\u53EA\u63A5\u53D7\u6587\u5B57\uFF0C\u56FE\u7247\u548C\u6587\u4EF6\u4ECD\u4FDD\u7559\u5728\u8F93\u5165\u6846\u4E2D\u3002",
520
520
  "error.removeAttachments": "\u65C1\u95EE\u53EA\u63A5\u53D7\u6587\u5B57\uFF0C\u8BF7\u5148\u79FB\u9664\u56FE\u7247\u548C\u6587\u4EF6\u3002",
521
521
  "error.backend": "BTW \u540E\u7AEF\u5C1A\u672A\u542F\u7528\uFF0C\u8BF7\u68C0\u67E5\u63D2\u4EF6\u914D\u7F6E\u3002",
522
+ "error.subagent": "\u5B50\u4EE3\u7406\u4F1A\u8BDD\u6682\u4E0D\u652F\u6301\u65C1\u95EE\uFF0C\u8BF7\u8FD4\u56DE\u4E3B\u4F1A\u8BDD\u4F7F\u7528 /btw\u3002",
522
523
  "error.empty": "\u8BF7\u8F93\u5165\u65C1\u95EE\u5185\u5BB9\u3002",
523
524
  "error.length": `\u65C1\u95EE\u6700\u591A\u652F\u6301 ${MAX_QUESTION_LENGTH} \u4E2A\u5B57\u7B26\u3002`,
524
525
  "error.capacity": "\u6B63\u5728\u5904\u7406\u7684\u65C1\u95EE\u8F83\u591A\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002",
@@ -572,6 +573,7 @@ var en = {
572
573
  "error.attachments": "Side questions accept text only. Your images and files remain in the input.",
573
574
  "error.removeAttachments": "Side questions accept text only. Remove the images and files first.",
574
575
  "error.backend": "The BTW backend is unavailable. Check the plugin configuration.",
576
+ "error.subagent": "Side questions are not supported in subagent sessions. Use /btw in the main session.",
575
577
  "error.empty": "Enter a side question.",
576
578
  "error.length": `Side questions are limited to ${MAX_QUESTION_LENGTH.toLocaleString("en-US")} characters.`,
577
579
  "error.capacity": "Too many side questions are running. Try again shortly.",
@@ -604,6 +606,8 @@ var CLOSE_COMMAND = "btw-close";
604
606
 
605
607
  // src/client/bubbles.ts
606
608
  var MAX_BUBBLES_PER_SESSION = 20;
609
+ var SideRequestNotStartedError = class extends Error {
610
+ };
607
611
  var BubbleStore = class {
608
612
  constructor(transport, t = translate()) {
609
613
  this.transport = transport;
@@ -613,6 +617,9 @@ var BubbleStore = class {
613
617
  listeners = /* @__PURE__ */ new Set();
614
618
  active = /* @__PURE__ */ new Map();
615
619
  closing = /* @__PURE__ */ new Map();
620
+ notStarted = /* @__PURE__ */ new Set();
621
+ runEnd = /* @__PURE__ */ new Map();
622
+ closeFenced = /* @__PURE__ */ new Set();
616
623
  admitting = /* @__PURE__ */ new Set();
617
624
  disposed = false;
618
625
  getSnapshot = () => this.snapshot;
@@ -649,12 +656,43 @@ var BubbleStore = class {
649
656
  const id = crypto.randomUUID();
650
657
  const controller = new AbortController();
651
658
  this.active.set(id, controller);
659
+ this.runEnd.set(id, "pending");
652
660
  this.publish([...this.snapshot, { id, sessionId, question, ...reference === void 0 ? {} : { reference }, answer: "", phase: "answering" }]);
653
661
  void Promise.resolve().then(() => reference === void 0 ? this.transport.run(sessionId, id, question, controller.signal) : this.transport.run(sessionId, id, question, controller.signal, reference)).then((result) => {
654
- if (controller.signal.aborted || this.closing.has(id)) return;
655
- this.update(id, { phase: result.kind === "success" ? "done" : "error", answer: result.kind === "success" ? result.text : "", error: result.kind === "error" ? result.text : void 0 });
662
+ if (result.kind === "success") {
663
+ this.runEnd.set(id, "success");
664
+ if (!this.snapshot.some((item) => item.id === id)) return;
665
+ const failedClose = this.closeFenced.has(id) || Boolean(this.snapshot.find((item) => item.id === id)?.closeFailed);
666
+ this.update(id, { phase: failedClose ? "error" : this.closing.has(id) ? "closing" : "done", answer: result.text, ...failedClose ? {} : { error: void 0 } });
667
+ return;
668
+ }
669
+ if (controller.signal.aborted) {
670
+ if (this.runEnd.get(id) === "pending") this.runEnd.set(id, "aborted");
671
+ this.dropIfCloseCannotFinish(id);
672
+ return;
673
+ }
674
+ this.runEnd.set(id, "failed");
675
+ if (!this.closing.has(id)) this.update(id, { phase: "error", answer: "", error: result.text });
656
676
  }).catch((error) => {
657
- if (!controller.signal.aborted && !this.closing.has(id)) this.update(id, { phase: "error", error: String(error) });
677
+ if (this.runEnd.get(id) === "success") return;
678
+ const message = error instanceof Error ? error.message : String(error);
679
+ if (error instanceof SideRequestNotStartedError && this.snapshot.some((item) => item.id === id)) {
680
+ this.runEnd.set(id, "not-started");
681
+ this.notStarted.add(id);
682
+ if (this.closeFenced.has(id) || this.closing.has(id)) {
683
+ this.remove(id);
684
+ return;
685
+ }
686
+ this.update(id, { phase: "error", error: message });
687
+ return;
688
+ }
689
+ if (controller.signal.aborted) {
690
+ if (this.runEnd.get(id) === "pending") this.runEnd.set(id, "aborted");
691
+ this.dropIfCloseCannotFinish(id);
692
+ return;
693
+ }
694
+ this.runEnd.set(id, "failed");
695
+ if (!this.closing.has(id)) this.update(id, { phase: "error", error: message });
658
696
  }).finally(() => {
659
697
  this.active.delete(id);
660
698
  });
@@ -665,12 +703,28 @@ var BubbleStore = class {
665
703
  if (existing) return existing;
666
704
  const item = this.snapshot.find((bubble) => bubble.id === id);
667
705
  if (!item) return Promise.resolve();
706
+ if (this.notStarted.has(id)) {
707
+ this.remove(id);
708
+ return Promise.resolve();
709
+ }
668
710
  this.update(id, { phase: "closing", error: void 0, closeFailed: false });
669
711
  this.active.get(id)?.abort();
670
712
  const operation = Promise.resolve().then(() => this.transport.close(item.sessionId, id)).then((result) => {
671
713
  if (result.kind === "error") throw new Error(result.text);
672
- this.publish(this.snapshot.filter((bubble) => bubble.id !== id));
714
+ this.remove(id);
673
715
  }).catch((error) => {
716
+ if (this.notStarted.has(id) || this.runEnd.get(id) === "not-started") {
717
+ this.remove(id);
718
+ return;
719
+ }
720
+ if (error instanceof SideRequestNotStartedError) {
721
+ this.closeFenced.add(id);
722
+ const end = this.runEnd.get(id);
723
+ if (end === "aborted" || end === "not-started") {
724
+ this.remove(id);
725
+ return;
726
+ }
727
+ }
674
728
  this.update(id, { phase: "error", closeFailed: true, error: error instanceof Error ? error.message : String(error) });
675
729
  }).finally(() => {
676
730
  this.closing.delete(id);
@@ -683,6 +737,17 @@ var BubbleStore = class {
683
737
  this.disposed = true;
684
738
  await Promise.all(this.snapshot.map((item) => this.close(item.id)));
685
739
  }
740
+ dropIfCloseCannotFinish(id) {
741
+ if (!this.closeFenced.has(id)) return;
742
+ const end = this.runEnd.get(id);
743
+ if (end === "not-started" || end === "aborted") this.remove(id);
744
+ }
745
+ remove(id) {
746
+ this.notStarted.delete(id);
747
+ this.runEnd.delete(id);
748
+ this.closeFenced.delete(id);
749
+ this.publish(this.snapshot.filter((item) => item.id !== id));
750
+ }
686
751
  update(id, patch2) {
687
752
  this.publish(this.snapshot.map((item) => item.id === id ? { ...item, ...patch2 } : item));
688
753
  }
@@ -13793,7 +13858,7 @@ function sessionIdOf(session) {
13793
13858
  if (session && typeof session === "object" && "sessionId" in session) return String(session.sessionId);
13794
13859
  return String(session);
13795
13860
  }
13796
- function attachOfficialBtw(commandUi, claim, t) {
13861
+ function attachOfficialBtw(commandUi, claim, t, isSubagent) {
13797
13862
  const live = commandUi;
13798
13863
  if (!live) return () => {
13799
13864
  };
@@ -13803,7 +13868,7 @@ function attachOfficialBtw(commandUi, claim, t) {
13803
13868
  live.candidates = async (...args) => {
13804
13869
  const rows = await original.apply(live, args);
13805
13870
  if (!Array.isArray(rows)) return rows;
13806
- return rows.map((row) => {
13871
+ return rows.filter((row) => !(isSubagent(sessionIdOf(args[0])) && row && typeof row === "object" && "name" in row && row.name === "btw")).map((row) => {
13807
13872
  if (!row || typeof row !== "object" || !("name" in row) || row.name !== "btw") return row;
13808
13873
  const item = row;
13809
13874
  return {
@@ -13821,14 +13886,14 @@ function attachOfficialBtw(commandUi, claim, t) {
13821
13886
  }
13822
13887
  if (typeof live.dispatch === "function") {
13823
13888
  const original = live.dispatch.bind(live);
13824
- live.dispatch = (pick) => pick.candidate?.name === "btw" ? claim(sessionIdOf(pick.session)) : original(pick);
13889
+ live.dispatch = (pick) => pick.candidate?.name === "btw" ? isSubagent(sessionIdOf(pick.session)) ? void 0 : claim(sessionIdOf(pick.session)) : original(pick);
13825
13890
  restore.push(() => {
13826
13891
  live.dispatch = original;
13827
13892
  });
13828
13893
  }
13829
13894
  if (typeof live.matchSpace === "function") {
13830
13895
  const original = live.matchSpace.bind(live);
13831
- live.matchSpace = (session, token) => token === "/btw" ? claim(sessionIdOf(session)) : original(session, token);
13896
+ live.matchSpace = (session, token) => token === "/btw" ? isSubagent(sessionIdOf(session)) ? void 0 : claim(sessionIdOf(session)) : original(session, token);
13832
13897
  restore.push(() => {
13833
13898
  live.matchSpace = original;
13834
13899
  });
@@ -13837,6 +13902,7 @@ function attachOfficialBtw(commandUi, claim, t) {
13837
13902
  const original = live.matchEnter.bind(live);
13838
13903
  live.matchEnter = async (session, line, signal, envelope) => {
13839
13904
  if (!/^\/btw(?:\s|$)/.test(line.trim())) return original(session, line, signal, envelope);
13905
+ if (isSubagent(sessionIdOf(session))) throw new Error(t("error.subagent"));
13840
13906
  if (envelope?.attachments || envelope?.images) throw new Error(t("error.removeAttachments"));
13841
13907
  return claim(sessionIdOf(session));
13842
13908
  };
@@ -13848,6 +13914,7 @@ function attachOfficialBtw(commandUi, claim, t) {
13848
13914
  const original = live.execute.bind(live);
13849
13915
  live.execute = async (session, line, attachments) => {
13850
13916
  if (!/^\/btw(?:\s|$)/.test(line)) return original(session, line, attachments);
13917
+ if (isSubagent(sessionIdOf(session))) return { kind: "error", text: t("error.subagent") };
13851
13918
  if (Array.isArray(attachments) && attachments.length) return { kind: "error", text: t("error.attachments") };
13852
13919
  const outcome = claim(sessionIdOf(session));
13853
13920
  if (!outcome || typeof outcome !== "object" || !("claim" in outcome)) return { kind: "error", text: t("error.backend") };
@@ -13868,9 +13935,19 @@ function apply(ctx) {
13868
13935
  const t = ctx.locale.bind(NS);
13869
13936
  const slots = ctx.get("slots");
13870
13937
  const remote = ctx.get("remote");
13938
+ const sessions = ctx.get("sessions");
13939
+ const isSubagent = (id) => Boolean(sessions.subagentAddress?.(id)) || sessions.list?.getSnapshot().byId[id]?.origin === "subagent";
13871
13940
  const execute = async (session, line, signal) => {
13872
13941
  const response = await remote.commands.execute(session, line, [], signal);
13873
- if (!response.ok) throw new Error(response.error.message);
13942
+ if (!response.ok) {
13943
+ const failure = response.error;
13944
+ const code4 = failure.code;
13945
+ const details = failure.details;
13946
+ const reason = details && typeof details === "object" && "reason" in details && typeof details.reason === "string" ? details.reason : void 0;
13947
+ const ownedBySubagent = (code4 === "agent-busy" || code4 === "session/agent-busy") && (reason === "use subagent delivery for this child session" || failure.message === `session "${session}" is owned by subagent routing`);
13948
+ if (ownedBySubagent) throw new SideRequestNotStartedError(t("error.subagent"));
13949
+ throw new Error(failure.message);
13950
+ }
13874
13951
  if (!response.value) throw new Error(t("error.backend"));
13875
13952
  return { kind: response.value.result.kind, text: response.value.result.text ?? "" };
13876
13953
  };
@@ -13880,11 +13957,12 @@ function apply(ctx) {
13880
13957
  }, t);
13881
13958
  const claim = (sessionId) => ({ claim: {
13882
13959
  name: "btw",
13883
- token: "/btw",
13960
+ token: "/btw ",
13884
13961
  get hint() {
13885
13962
  return t("command.hint");
13886
13963
  },
13887
13964
  submit: async (args, _actx, attachments) => {
13965
+ if (isSubagent(sessionId)) return { kind: "error", text: t("error.subagent") };
13888
13966
  if (attachments.length) return { kind: "error", text: t("error.attachments") };
13889
13967
  try {
13890
13968
  await store.ask(sessionId, args.trim());
@@ -13894,8 +13972,8 @@ function apply(ctx) {
13894
13972
  }
13895
13973
  }
13896
13974
  } });
13897
- if (typeof ctx.inject === "function") ctx.inject(["commandUi"], (scope) => attachOfficialBtw(scope.get("commandUi"), (sessionId) => claim(sessionId), t));
13898
- else ctx.effect(() => attachOfficialBtw(ctx.get("commandUi"), (sessionId) => claim(sessionId), t));
13975
+ if (typeof ctx.inject === "function") ctx.inject(["commandUi"], (scope) => attachOfficialBtw(scope.get("commandUi"), (sessionId) => claim(sessionId), t, isSubagent));
13976
+ else ctx.effect(() => attachOfficialBtw(ctx.get("commandUi"), (sessionId) => claim(sessionId), t, isSubagent));
13899
13977
  ctx.effect(() => {
13900
13978
  const style = document.createElement("style");
13901
13979
  style.dataset.plugin = "@michengai/dsh-btw";
@@ -13907,7 +13985,7 @@ function apply(ctx) {
13907
13985
  });
13908
13986
  ctx.effect(() => () => store.dispose());
13909
13987
  function Dock(props) {
13910
- return /* @__PURE__ */ import_react7.default.createElement(import_react7.default.Fragment, null, /* @__PURE__ */ import_react7.default.createElement(SelectionAsk, { key: props.session.sessionId, store, sessionId: props.session.sessionId, t: props.t, addToConversation: (reference) => {
13988
+ return /* @__PURE__ */ import_react7.default.createElement(import_react7.default.Fragment, null, !props.session.subagent && !isSubagent(props.session.sessionId) && /* @__PURE__ */ import_react7.default.createElement(SelectionAsk, { key: props.session.sessionId, store, sessionId: props.session.sessionId, t: props.t, addToConversation: (reference) => {
13911
13989
  const session = ctx.get("sessions").scope(props.session.sessionId);
13912
13990
  if (!session) throw new Error(props.t("error.backend"));
13913
13991
  addQuoteToComposer(ctx.conversation.input.for(session), reference, props.t);
package/lib/index.js CHANGED
@@ -42,6 +42,7 @@ var zh = {
42
42
  "error.attachments": "\u65C1\u95EE\u53EA\u63A5\u53D7\u6587\u5B57\uFF0C\u56FE\u7247\u548C\u6587\u4EF6\u4ECD\u4FDD\u7559\u5728\u8F93\u5165\u6846\u4E2D\u3002",
43
43
  "error.removeAttachments": "\u65C1\u95EE\u53EA\u63A5\u53D7\u6587\u5B57\uFF0C\u8BF7\u5148\u79FB\u9664\u56FE\u7247\u548C\u6587\u4EF6\u3002",
44
44
  "error.backend": "BTW \u540E\u7AEF\u5C1A\u672A\u542F\u7528\uFF0C\u8BF7\u68C0\u67E5\u63D2\u4EF6\u914D\u7F6E\u3002",
45
+ "error.subagent": "\u5B50\u4EE3\u7406\u4F1A\u8BDD\u6682\u4E0D\u652F\u6301\u65C1\u95EE\uFF0C\u8BF7\u8FD4\u56DE\u4E3B\u4F1A\u8BDD\u4F7F\u7528 /btw\u3002",
45
46
  "error.empty": "\u8BF7\u8F93\u5165\u65C1\u95EE\u5185\u5BB9\u3002",
46
47
  "error.length": `\u65C1\u95EE\u6700\u591A\u652F\u6301 ${MAX_QUESTION_LENGTH} \u4E2A\u5B57\u7B26\u3002`,
47
48
  "error.capacity": "\u6B63\u5728\u5904\u7406\u7684\u65C1\u95EE\u8F83\u591A\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002",
@@ -95,6 +96,7 @@ var en = {
95
96
  "error.attachments": "Side questions accept text only. Your images and files remain in the input.",
96
97
  "error.removeAttachments": "Side questions accept text only. Remove the images and files first.",
97
98
  "error.backend": "The BTW backend is unavailable. Check the plugin configuration.",
99
+ "error.subagent": "Side questions are not supported in subagent sessions. Use /btw in the main session.",
98
100
  "error.empty": "Enter a side question.",
99
101
  "error.length": `Side questions are limited to ${MAX_QUESTION_LENGTH.toLocaleString("en-US")} characters.`,
100
102
  "error.capacity": "Too many side questions are running. Try again shortly.",
@@ -14,6 +14,9 @@ export interface SideTransport {
14
14
  run(session: string, id: string, question: string, signal: AbortSignal, reference?: string): Promise<SideResult>;
15
15
  close(session: string, id: string): Promise<SideResult>;
16
16
  }
17
+ /** 宿主在进入 handler 前明确拒绝启动;没有后台任务需要清理。 */
18
+ export declare class SideRequestNotStartedError extends Error {
19
+ }
17
20
  /** 仅内存 UI 状态;关闭请求与答案返回按请求身份分别收敛。 */
18
21
  export declare class BubbleStore {
19
22
  private readonly transport;
@@ -22,6 +25,9 @@ export declare class BubbleStore {
22
25
  private readonly listeners;
23
26
  private readonly active;
24
27
  private readonly closing;
28
+ private readonly notStarted;
29
+ private readonly runEnd;
30
+ private readonly closeFenced;
25
31
  private readonly admitting;
26
32
  private disposed;
27
33
  constructor(transport: SideTransport, t?: BtwTranslate);
@@ -31,6 +37,8 @@ export declare class BubbleStore {
31
37
  ask(sessionId: string, question: string, reference?: string): Promise<string>;
32
38
  close(id: string): Promise<void>;
33
39
  dispose(): Promise<void>;
40
+ private dropIfCloseCannotFinish;
41
+ private remove;
34
42
  private update;
35
43
  private publish;
36
44
  }
@@ -35,6 +35,7 @@ export declare const zh: {
35
35
  'error.attachments': string;
36
36
  'error.removeAttachments': string;
37
37
  'error.backend': string;
38
+ 'error.subagent': string;
38
39
  'error.empty': string;
39
40
  'error.length': string;
40
41
  'error.capacity': string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michengai/dsh-btw",
3
- "version": "0.1.23",
3
+ "version": "0.1.24",
4
4
  "description": "DSH 一次性只读旁问插件:基于当前上下文回答,独立气泡,不执行工具 · One-shot read-only side-ask plugin: answers from current context in an independent bubble, no tool execution",
5
5
  "repository": {
6
6
  "type": "git",