opencode-feishu-plugin 0.2.11 → 0.2.13

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/README.en.md CHANGED
@@ -173,7 +173,7 @@ When the agent asks via `question` or other form tools, the form becomes a Feish
173
173
  | `logLevel` | `debug`\|`info`\|`warn`\|`error` | `info` | Log level |
174
174
  | `logFile` | string \| boolean | — | `true` = write `<configDir>/plugins/feishu.log`; recommended in server mode |
175
175
  | `approvalTtlMs` | number | `600000` | Approval token / card validity |
176
- | `staleExecutionMs` | number | `300000` | Watchdog threshold (1–60 min) |
176
+ | `staleExecutionMs` | number | `300000` | Watchdog threshold (0–60 min; **0 = disabled**; sessions waiting on a form / pending approval are never killed) |
177
177
  | `gatewayLocation` | string | — | Only start the gateway at this location (or its subdirectories); empty = any location |
178
178
 
179
179
  Full config (including `cardMaxTables`, `topicStatus*`, `resumeSummary*`, `keepalive*`, `gatewayMatchGraceMs`) → [docs/advanced.en.md](./docs/advanced.en.md#full-configuration).
@@ -188,7 +188,7 @@ Full config (including `cardMaxTables`, `topicStatus*`, `resumeSummary*`, `keepa
188
188
  | Plugin not loaded at all | npm: confirm the package name is in the `plugins` array; directory: confirm `plugins/<name>/index.js` exists |
189
189
  | No approval cards | That session wasn't started from Feishu (no mapping); by design the plugin doesn't take it over |
190
190
  | Button click says invalid credential | Token expired (10 min default) or clicker not in allowlist |
191
- | Session seems stuck, messages only queue | Watchdog auto-interrupts after 5 min; or tap "⏹ force stop" / send `/stop` |
191
+ | Session seems stuck, messages only queue | Watchdog auto-interrupts after 5 min (never while a form / approval is pending; `staleExecutionMs: 0` disables it); or tap "⏹ force stop" / send `/stop` |
192
192
  | Bot goes silent after ~1h idle | opencode recycles idle locations; built-in keepalive restores automatically, see [docs/advanced.en.md](./docs/advanced.en.md#location-keep-alive) |
193
193
  | Can't see plugin logs | stderr is discarded in server mode; set `logFile: true` |
194
194
  | Multiple long connections / duplicate replies | Set `gatewayLocation` to a common working directory, see [docs/advanced.en.md](./docs/advanced.en.md#multiple-instances-and-gateway-election) |
package/README.md CHANGED
@@ -173,7 +173,7 @@ agent 反问时表单会变成飞书卡片,**点按钮或在话题里直接发
173
173
  | `logLevel` | `debug`\|`info`\|`warn`\|`error` | `info` | 日志级别 |
174
174
  | `logFile` | string \| boolean | — | `true` = 写 `<configDir>/plugins/feishu.log`,服务模式建议开启 |
175
175
  | `approvalTtlMs` | number | `600000` | 审批 token / 卡片有效期 |
176
- | `staleExecutionMs` | number | `300000` | 看门狗阈值(1–60 分钟) |
176
+ | `staleExecutionMs` | number | `300000` | 看门狗阈值(0–60 分钟;**0 = 关闭看门狗**;待答表单 / 未决审批期间不判卡死) |
177
177
  | `gatewayLocation` | string | — | 只在该 location(及其子目录)启动网关;留空 = 任意 location 生效 |
178
178
 
179
179
  完整配置(含 `cardMaxTables`、`topicStatus*`、`resumeSummary*`、`keepalive*`、`gatewayMatchGraceMs` 等进阶项)见 [docs/advanced.md](./docs/advanced.md#完整配置项)。
@@ -188,7 +188,7 @@ agent 反问时表单会变成飞书卡片,**点按钮或在话题里直接发
188
188
  | 插件完全没被加载 | npm 方式确认包名在 `plugins` 数组;目录方式确认 `plugins/<名>/index.js` 存在 |
189
189
  | 审批卡收不到 | 该会话不是从飞书发起的(无映射),插件按设计不接管 |
190
190
  | 点按钮提示凭证无效 | token 过期(默认 10 分钟)或点击者不在白名单 |
191
- | 会话像卡死、只排队 | 看门狗默认 5 分钟后自动中断;也可点「⏹ 强制停止」或发 `/stop` |
191
+ | 会话像卡死、只排队 | 看门狗默认 5 分钟后自动中断(**待答表单 / 未决审批期间不中断**;`staleExecutionMs: 0` 可关闭);也可点「⏹ 强制停止」或发 `/stop` |
192
192
  | 空闲约 1 小时后失联 | opencode 会回收空闲 location;内置保活默认开启会自动恢复,见 [docs/advanced.md](./docs/advanced.md#位置保活) |
193
193
  | 看不到插件日志 | 服务模式下 stderr 被丢弃,设 `logFile: true` |
194
194
  | 多个长连接 / 重复回复 | 设置 `gatewayLocation` 为常用工作目录,见 [docs/advanced.md](./docs/advanced.md#多实例与网关选举) |
package/dist/index.js CHANGED
@@ -144008,7 +144008,7 @@ function resolveConfig2(raw, env = process.env, deps = {}) {
144008
144008
  const topicGuidance = asBoolean(merged.topicGuidance, true);
144009
144009
  const throttle2 = clamp4(asNumber(merged.streamThrottleMs, 400), 400, 6e4);
144010
144010
  const approvalTtlMs = clamp4(asNumber(merged.approvalTtlMs, 10 * 60 * 1e3), 3e4, 24 * 60 * 60 * 1e3);
144011
- const staleExecutionMs = clamp4(asNumber(merged.staleExecutionMs, 5 * 60 * 1e3), 6e4, 60 * 60 * 1e3);
144011
+ const staleExecutionMs = clamp4(asNumber(merged.staleExecutionMs, 5 * 60 * 1e3), 0, 60 * 60 * 1e3);
144012
144012
  const maxResourcesShown = clamp4(asNumber(merged.maxResourcesShown, 8), 1, 50);
144013
144013
  const allowedRootsRaw = asStringArray(merged.allowedRoots);
144014
144014
  const allowedRootsCandidates = (allowedRootsRaw.length > 0 ? allowedRootsRaw : [...DEFAULT_ALLOWED_ROOTS]).filter(isAbsolute);
@@ -144507,6 +144507,18 @@ var TtlMap = class {
144507
144507
  }
144508
144508
  this.data.delete(key);
144509
144509
  }
144510
+ /** 遍历未过期条目(惰性清理已过期项;用于按会话反查 pending 状态)。 */
144511
+ entries() {
144512
+ const out = [];
144513
+ for (const [key, hit] of this.data) {
144514
+ if (hit.expiresAt <= this.now()) {
144515
+ this.delete(key);
144516
+ continue;
144517
+ }
144518
+ out.push([key, hit.value]);
144519
+ }
144520
+ return out;
144521
+ }
144510
144522
  clear() {
144511
144523
  for (const timer of this.timers.values()) clearTimeout(timer);
144512
144524
  this.timers.clear();
@@ -145940,14 +145952,17 @@ var ExecutionTracker = class {
145940
145952
  /**
145941
145953
  * 返回「正在跑但超过 maxIdleMs 无任何活动」的 sessionID,并将其从运行态移除。
145942
145954
  * 用于插件层看门狗:事件丢失或交互工具(question/permission)永久挂起时兜底放开排队。
145955
+ *
145956
+ * `shouldSkip` 返回 true 的会话视为**合法等待**(如待答表单 / 未决审批):
145957
+ * 既不返回、也不移出运行态(避免在等待期被误杀,也避免丢追踪导致后续排队判定失真)。
145943
145958
  */
145944
- stale(maxIdleMs, now3 = Date.now()) {
145959
+ stale(maxIdleMs, now3 = Date.now(), shouldSkip) {
145945
145960
  const out = [];
145946
145961
  for (const [sessionID, at] of this.running) {
145947
- if (now3 - at >= maxIdleMs) {
145948
- this.running.delete(sessionID);
145949
- out.push(sessionID);
145950
- }
145962
+ if (now3 - at < maxIdleMs) continue;
145963
+ if (shouldSkip?.(sessionID)) continue;
145964
+ this.running.delete(sessionID);
145965
+ out.push(sessionID);
145951
145966
  }
145952
145967
  return out;
145953
145968
  }
@@ -145955,6 +145970,41 @@ var ExecutionTracker = class {
145955
145970
  this.running.clear();
145956
145971
  }
145957
145972
  };
145973
+ var SessionParentLinks = class {
145974
+ constructor(maxEntries = 2e3) {
145975
+ this.maxEntries = maxEntries;
145976
+ }
145977
+ maxEntries;
145978
+ parents = /* @__PURE__ */ new Map();
145979
+ /** 登记/清理某会话的父链接(无 parentID = 顶级会话,清掉旧链接)。 */
145980
+ remember(sessionID, parentID) {
145981
+ if (!sessionID) return;
145982
+ if (!parentID) {
145983
+ this.parents.delete(sessionID);
145984
+ return;
145985
+ }
145986
+ this.parents.delete(sessionID);
145987
+ this.parents.set(sessionID, parentID);
145988
+ while (this.parents.size > this.maxEntries) {
145989
+ const oldest = this.parents.keys().next().value;
145990
+ if (oldest === void 0) break;
145991
+ this.parents.delete(oldest);
145992
+ }
145993
+ }
145994
+ parentOf(sessionID) {
145995
+ return this.parents.get(sessionID);
145996
+ }
145997
+ /** 沿父链逐级回调(含自身;防环 + 深度上限)。 */
145998
+ walk(sessionID, visit) {
145999
+ let current = sessionID;
146000
+ const seen = /* @__PURE__ */ new Set();
146001
+ for (let depth = 0; current && depth < 16 && !seen.has(current); depth += 1) {
146002
+ seen.add(current);
146003
+ visit(current);
146004
+ current = this.parents.get(current);
146005
+ }
146006
+ }
146007
+ };
145958
146008
 
145959
146009
  // src/utils/throttle.ts
145960
146010
  function createThrottler(options2) {
@@ -146575,13 +146625,49 @@ function trailingText(blocks) {
146575
146625
  }
146576
146626
  return parts.join("\n\n").trim();
146577
146627
  }
146628
+ function isActivityEvent(type) {
146629
+ return type === "text.started" || type === "text.delta" || type === "text.ended" || type === "tool.input.started" || type === "tool.input.ended" || type === "tool.success" || type === "tool.error";
146630
+ }
146631
+ var QUEUED_DRAIN_GRACE_MS = 3e3;
146578
146632
  function createRunController(deps) {
146579
146633
  const sessions = /* @__PURE__ */ new Map();
146580
146634
  const throttlers = /* @__PURE__ */ new Map();
146581
146635
  const sessionModels = /* @__PURE__ */ new Map();
146582
146636
  const notifiedQueued = /* @__PURE__ */ new Set();
146637
+ const drainTimers = /* @__PURE__ */ new Map();
146583
146638
  const throttleMs = Math.max(400, deps.throttleMs);
146584
146639
  let disposed = false;
146640
+ const cancelDrain = (sessionID) => {
146641
+ const timer = drainTimers.get(sessionID);
146642
+ if (timer) {
146643
+ clearTimeout(timer);
146644
+ drainTimers.delete(sessionID);
146645
+ }
146646
+ };
146647
+ const scheduleDrain = (sessionID, runs) => {
146648
+ if (runs.queued.length === 0) {
146649
+ cancelDrain(sessionID);
146650
+ return;
146651
+ }
146652
+ cancelDrain(sessionID);
146653
+ const timer = setTimeout(() => {
146654
+ drainTimers.delete(sessionID);
146655
+ if (disposed) return;
146656
+ const live = sessions.get(sessionID);
146657
+ if (!live || live.queued.length === 0) return;
146658
+ deps.log.info("\u6392\u961F\u5361\u6309\u300C\u5DF2\u968F\u672C\u8F6E\u5904\u7406\u300D\u6536\u5C3E\uFF08\u961F\u5217\u6D88\u606F\u5728\u5F53\u524D\u6267\u884C\u5185\u88AB\u6D88\u8D39\uFF09", {
146659
+ sessionID,
146660
+ count: live.queued.length
146661
+ });
146662
+ for (const card of live.queued) {
146663
+ card.state = reduce6(card.state, { type: "execution.succeeded" });
146664
+ finalize2(card);
146665
+ }
146666
+ live.queued.length = 0;
146667
+ }, QUEUED_DRAIN_GRACE_MS);
146668
+ timer.unref?.();
146669
+ drainTimers.set(sessionID, timer);
146670
+ };
146585
146671
  const sessionRuns = (sessionID) => {
146586
146672
  let runs = sessions.get(sessionID);
146587
146673
  if (!runs) {
@@ -146689,6 +146775,7 @@ function createRunController(deps) {
146689
146775
  if (!runs) return;
146690
146776
  if (event.type === "execution.started") {
146691
146777
  notifiedQueued.delete(sessionID);
146778
+ cancelDrain(sessionID);
146692
146779
  if (runs.active) {
146693
146780
  update2(runs.active, event, true);
146694
146781
  return;
@@ -146696,51 +146783,64 @@ function createRunController(deps) {
146696
146783
  const next = runs.queued.shift();
146697
146784
  if (next) {
146698
146785
  runs.active = next;
146786
+ deps.log.debug("\u6392\u961F\u5361\u664B\u5347\u4E3A\u8FD0\u884C\u5361", { sessionID, runID: next.runID });
146699
146787
  update2(next, event, true);
146700
146788
  }
146789
+ if (runs.queued.length > 0) scheduleDrain(sessionID, runs);
146701
146790
  return;
146702
146791
  }
146792
+ if (!runs.active && runs.queued.length > 0 && isActivityEvent(event.type)) {
146793
+ const next = runs.queued.shift();
146794
+ runs.active = next;
146795
+ deps.log.info("\u6536\u5230\u6D3B\u52A8\u4E8B\u4EF6\u4F46\u65E0\u8FD0\u884C\u5361\uFF1A\u664B\u5347\u961F\u9996\u6392\u961F\u5361", {
146796
+ sessionID,
146797
+ runID: next.runID,
146798
+ event: event.type
146799
+ });
146800
+ next.state = reduce6(next.state, { type: "execution.started" });
146801
+ }
146703
146802
  const card = runs.active;
146704
- if (!card) return;
146705
- if (event.type === "execution.succeeded") {
146706
- const text = trailingText(card.state.blocks);
146707
- const long = deps.sendFinalAnswer !== void 0 && deps.finalAnswer !== void 0 && text.length >= deps.finalAnswer.minChars;
146708
- if (long) {
146709
- card.state = reduce6(reduce6(card.state, { type: "final.separated" }), event);
146803
+ if (event.type === "execution.succeeded" || event.type === "execution.failed") {
146804
+ if (card) {
146805
+ if (event.type === "execution.succeeded") {
146806
+ const text = trailingText(card.state.blocks);
146807
+ const long = deps.sendFinalAnswer !== void 0 && deps.finalAnswer !== void 0 && text.length >= deps.finalAnswer.minChars;
146808
+ if (long) {
146809
+ card.state = reduce6(reduce6(card.state, { type: "final.separated" }), event);
146810
+ finalize2(card);
146811
+ runs.active = void 0;
146812
+ void deps.sendFinalAnswer({
146813
+ sessionID,
146814
+ chatId: card.chatId,
146815
+ ...card.replyToMessageId ? { replyToMessageId: card.replyToMessageId } : {},
146816
+ text
146817
+ }).catch((err2) => {
146818
+ deps.log.warn("\u6700\u7EC8\u7B54\u6848\u53D1\u9001\u5931\u8D25", { sessionID, error: errorMessage(err2) });
146819
+ });
146820
+ scheduleDrain(sessionID, runs);
146821
+ return;
146822
+ }
146823
+ }
146824
+ update2(card, event, true);
146710
146825
  finalize2(card);
146711
146826
  runs.active = void 0;
146712
- void deps.sendFinalAnswer({
146713
- sessionID,
146714
- chatId: card.chatId,
146715
- ...card.replyToMessageId ? { replyToMessageId: card.replyToMessageId } : {},
146716
- text
146717
- }).catch((err2) => {
146718
- deps.log.warn("\u6700\u7EC8\u7B54\u6848\u53D1\u9001\u5931\u8D25", { sessionID, error: errorMessage(err2) });
146719
- });
146720
- return;
146721
146827
  }
146722
- update2(card, event, true);
146723
- finalize2(card);
146724
- runs.active = void 0;
146725
- return;
146726
- }
146727
- if (event.type === "execution.failed") {
146728
- update2(card, event, true);
146729
- finalize2(card);
146730
- runs.active = void 0;
146828
+ scheduleDrain(sessionID, runs);
146731
146829
  return;
146732
146830
  }
146831
+ if (!card) return;
146733
146832
  update2(card, event, false);
146734
146833
  },
146735
146834
  hasActive(sessionID) {
146736
146835
  return sessions.get(sessionID)?.active !== void 0;
146737
146836
  },
146738
- staleQueued(maxIdleMs, now3 = Date.now()) {
146837
+ staleQueued(maxIdleMs, now3 = Date.now(), shouldSkip) {
146739
146838
  const out = [];
146740
146839
  for (const [sessionID, runs] of sessions) {
146741
146840
  if (runs.active) continue;
146742
146841
  const first = runs.queued[0];
146743
146842
  if (!first || now3 - first.queuedAt < maxIdleMs) continue;
146843
+ if (shouldSkip?.(sessionID)) continue;
146744
146844
  if (notifiedQueued.has(sessionID)) continue;
146745
146845
  notifiedQueued.add(sessionID);
146746
146846
  out.push(sessionID);
@@ -146767,6 +146867,8 @@ function createRunController(deps) {
146767
146867
  disposed = true;
146768
146868
  sessionModels.clear();
146769
146869
  notifiedQueued.clear();
146870
+ for (const timer of drainTimers.values()) clearTimeout(timer);
146871
+ drainTimers.clear();
146770
146872
  for (const throttler of throttlers.values()) throttler.cancel();
146771
146873
  throttlers.clear();
146772
146874
  for (const runs of sessions.values()) {
@@ -147251,6 +147353,13 @@ var FormRelay = class {
147251
147353
  this.forms.delete(id2);
147252
147354
  void this.finish(pending, "cancelled");
147253
147355
  }
147356
+ /** 是否有尚未提交/取消的待答表单(看门狗判活:等表单属合法等待,不应判 stale)。 */
147357
+ hasPendingFor(sessionID) {
147358
+ for (const [, pending] of this.forms.entries()) {
147359
+ if (pending.sessionID === sessionID && !pending.settled) return true;
147360
+ }
147361
+ return false;
147362
+ }
147254
147363
  dispose() {
147255
147364
  this.disposed = true;
147256
147365
  this.forms.clear();
@@ -149014,6 +149123,13 @@ var ApprovalManager = class {
149014
149123
  parsed.decision === "reject" ? "\u5DF2\u62D2\u7EDD" : "\u5DF2\u5141\u8BB8"
149015
149124
  );
149016
149125
  }
149126
+ /** 该会话是否有未决审批(看门狗判活:等审批属合法等待,不应判 stale)。 */
149127
+ hasPendingFor(sessionID) {
149128
+ for (const [, card] of this.cards.entries()) {
149129
+ if (!card.resolved && card.input.sessionID === sessionID) return true;
149130
+ }
149131
+ return false;
149132
+ }
149017
149133
  dispose() {
149018
149134
  this.seenRequests.clear();
149019
149135
  this.cards.clear();
@@ -150767,6 +150883,14 @@ async function routeEvent(event, deps) {
150767
150883
  const touched = event.data?.sessionID;
150768
150884
  if (typeof touched === "string") deps.touch(touched);
150769
150885
  switch (event.type) {
150886
+ case "session.created": {
150887
+ const data = event.data;
150888
+ const childId = typeof data.sessionID === "string" && data.sessionID ? data.sessionID : typeof data.id === "string" && data.id ? data.id : "";
150889
+ if (!childId) break;
150890
+ const parent = typeof data.parentID === "string" && data.parentID ? data.parentID : typeof data.parentId === "string" && data.parentId ? data.parentId : void 0;
150891
+ deps.onSessionCreated?.(childId, parent);
150892
+ break;
150893
+ }
150770
150894
  case "permission.asked":
150771
150895
  void deps.onPermissionAsked(event.data).catch((err2) => deps.log.warn("\u5904\u7406 permission.asked \u5931\u8D25", { error: errorMessage(err2) }));
150772
150896
  break;
@@ -150843,18 +150967,21 @@ async function routeEvent(event, deps) {
150843
150967
  }
150844
150968
  case "session.execution.started": {
150845
150969
  const data = event.data;
150970
+ deps.log.debug("execution.started", { sessionID: data.sessionID });
150846
150971
  deps.markStarted(data.sessionID);
150847
150972
  deps.applyRun(data.sessionID, { type: "execution.started" });
150848
150973
  break;
150849
150974
  }
150850
150975
  case "session.execution.succeeded": {
150851
150976
  const data = event.data;
150977
+ deps.log.debug("execution.succeeded", { sessionID: data.sessionID });
150852
150978
  deps.markEnded(data.sessionID);
150853
150979
  deps.applyRun(data.sessionID, { type: "execution.succeeded" });
150854
150980
  break;
150855
150981
  }
150856
150982
  case "session.execution.failed": {
150857
150983
  const data = event.data;
150984
+ deps.log.debug("execution.failed", { sessionID: data.sessionID });
150858
150985
  deps.markEnded(data.sessionID);
150859
150986
  deps.applyRun(data.sessionID, { type: "execution.failed", error: extractErrorText(data.error) });
150860
150987
  void deps.notifyFailure(data.sessionID, data.error);
@@ -150862,6 +150989,7 @@ async function routeEvent(event, deps) {
150862
150989
  }
150863
150990
  case "session.execution.interrupted": {
150864
150991
  const data = event.data;
150992
+ deps.log.debug("execution.interrupted", { sessionID: data.sessionID, reason: data.reason });
150865
150993
  deps.markEnded(data.sessionID);
150866
150994
  deps.applyRun(data.sessionID, { type: "execution.failed", error: `\u5DF2\u4E2D\u65AD\uFF08${data.reason ?? "unknown"}\uFF09` });
150867
150995
  break;
@@ -150869,6 +150997,7 @@ async function routeEvent(event, deps) {
150869
150997
  case "session.status": {
150870
150998
  const data = event.data;
150871
150999
  const statusType = data.status?.type;
151000
+ deps.log.debug("session.status", { sessionID: data.sessionID, status: statusType });
150872
151001
  if (statusType === "idle") {
150873
151002
  deps.markEnded(data.sessionID);
150874
151003
  deps.applyRun(data.sessionID, { type: "execution.succeeded" });
@@ -150879,6 +151008,7 @@ async function routeEvent(event, deps) {
150879
151008
  }
150880
151009
  case "session.idle": {
150881
151010
  const data = event.data;
151011
+ deps.log.debug("session.idle", { sessionID: data.sessionID });
150882
151012
  deps.markEnded(data.sessionID);
150883
151013
  deps.applyRun(data.sessionID, { type: "execution.succeeded" });
150884
151014
  break;
@@ -151556,6 +151686,7 @@ async function start(ctx, config, log2, logSink) {
151556
151686
  await owner.load().catch((err2) => log2.warn("owner \u8BFB\u53D6\u5931\u8D25", { error: errorMessage(err2) }));
151557
151687
  const dedup = new MessageDedup(storage, log2);
151558
151688
  const executions = new ExecutionTracker();
151689
+ const sessionLinks = new SessionParentLinks();
151559
151690
  const runs = createRunController({
151560
151691
  sender,
151561
151692
  log: log2,
@@ -152161,9 +152292,13 @@ ${downloadedAttachmentPrompt(message.attachment, outcome)}`;
152161
152292
  })();
152162
152293
  const eventDeps = {
152163
152294
  log: log2,
152164
- touch: (sessionID) => executions.touch(sessionID),
152295
+ touch: (sessionID) => sessionLinks.walk(sessionID, (id2) => executions.touch(id2)),
152165
152296
  markStarted: (sessionID) => executions.markStarted(sessionID),
152166
152297
  markEnded: (sessionID) => executions.markEnded(sessionID),
152298
+ onSessionCreated: (sessionID, parentID) => {
152299
+ sessionLinks.remember(sessionID, parentID);
152300
+ log2.debug("session.created \u7236\u5B50\u767B\u8BB0", { sessionID, parentID: parentID ?? null });
152301
+ },
152167
152302
  applyRun: (sessionID, event) => runs.apply(sessionID, event),
152168
152303
  onPermissionAsked: (data) => approvals.onAsked(data),
152169
152304
  onPermissionReplied: (data) => approvals.onReplied(data),
@@ -152186,13 +152321,16 @@ ${downloadedAttachmentPrompt(message.attachment, outcome)}`;
152186
152321
  }
152187
152322
  await sender.sendText(link2.chatId, text);
152188
152323
  }
152189
- const stopWatchdog = startWatchdog({
152324
+ const hasPendingInteraction = (sessionID) => approvals.hasPendingFor(sessionID) || formRelay.hasPendingFor(sessionID);
152325
+ const stopWatchdog = config.staleExecutionMs > 0 ? startWatchdog({
152190
152326
  log: log2,
152191
152327
  staleExecutionMs: config.staleExecutionMs,
152192
- staleExecutions: () => executions.stale(config.staleExecutionMs),
152193
- staleQueued: () => runs.staleQueued(config.staleExecutionMs),
152328
+ staleExecutions: () => executions.stale(config.staleExecutionMs, Date.now(), hasPendingInteraction),
152329
+ staleQueued: () => runs.staleQueued(config.staleExecutionMs, Date.now(), hasPendingInteraction),
152194
152330
  recover: (sessionID, reason) => recovery.recover(sessionID, reason)
152195
- });
152331
+ }) : () => {
152332
+ log2.info("\u770B\u95E8\u72D7\u5DF2\u5173\u95ED\uFF08staleExecutionMs=0\uFF09");
152333
+ };
152196
152334
  async function notifyStuck(sessionID, reason, ok) {
152197
152335
  const link2 = await sessionMap.resolveBySession(sessionID);
152198
152336
  if (!link2) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-feishu-plugin",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
4
4
  "description": "Feishu/Lark plugin for OpenCode V2 — single-user topics as sessions, in-card permission approvals, streaming cards. Minimal permissions, no public endpoint.",
5
5
  "type": "module",
6
6
  "main": "index.js",