larkway 0.3.38 → 0.3.40

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.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  You @ the bot in a Feishu thread. It runs on your machine — reading your real codebase, executing commands, opening MRs — and posts the result back. You define what the agent knows and what it can do. Larkway just carries the messages.
10
10
 
11
- **Current release: v0.3.38**
11
+ **Current release: v0.3.40**
12
12
 
13
13
  ---
14
14
 
package/README.zh.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  你在飞书话题里 @ bot,它在你的机器上运行——读真实代码库、执行命令、开 MR——把结果贴回飞书。你定义 agent 知道什么、能做什么。Larkway 只负责传递消息。
10
10
 
11
- **当前版本:v0.3.38**
11
+ **当前版本:v0.3.40**
12
12
 
13
13
  ---
14
14
 
package/dist/cli/index.js CHANGED
@@ -112500,6 +112500,72 @@ var init_channelCotClient = __esm({
112500
112500
  }
112501
112501
  });
112502
112502
 
112503
+ // src/lark/messageLookupClient.ts
112504
+ async function withTimeout2(p, ms, label) {
112505
+ let timer;
112506
+ const timeout = new Promise((_resolve, reject) => {
112507
+ timer = setTimeout(() => reject(new Error(`[channel.msglookup] ${label} timed out after ${ms}ms`)), ms);
112508
+ timer.unref?.();
112509
+ });
112510
+ try {
112511
+ return await Promise.race([p, timeout]);
112512
+ } finally {
112513
+ if (timer) clearTimeout(timer);
112514
+ }
112515
+ }
112516
+ var LOOKUP_TIMEOUT_MS, CACHE_MAX, ChannelMessageLookupClient;
112517
+ var init_messageLookupClient = __esm({
112518
+ "src/lark/messageLookupClient.ts"() {
112519
+ "use strict";
112520
+ LOOKUP_TIMEOUT_MS = 5e3;
112521
+ CACHE_MAX = 200;
112522
+ ChannelMessageLookupClient = class {
112523
+ #resolveChannel;
112524
+ #cache = /* @__PURE__ */ new Map();
112525
+ constructor(opts) {
112526
+ this.#resolveChannel = opts.resolveChannel;
112527
+ }
112528
+ async get(messageId, opts) {
112529
+ if (!opts?.refresh) {
112530
+ const cached = this.#cache.get(messageId);
112531
+ if (cached) return cached;
112532
+ }
112533
+ try {
112534
+ const channel = this.#resolveChannel();
112535
+ if (!channel) return void 0;
112536
+ const req = {
112537
+ url: `/open-apis/im/v1/messages/${encodeURIComponent(messageId)}`,
112538
+ method: "GET"
112539
+ };
112540
+ const res = await withTimeout2(
112541
+ channel.rawClient.request(req),
112542
+ LOOKUP_TIMEOUT_MS,
112543
+ `GET message ${messageId}`
112544
+ );
112545
+ if (res.code !== void 0 && res.code !== 0) return void 0;
112546
+ const items = res.data?.["items"];
112547
+ if (!Array.isArray(items) || items.length === 0) return void 0;
112548
+ const item = items[0];
112549
+ const body = item["body"];
112550
+ const info = {
112551
+ msgType: typeof item["msg_type"] === "string" ? item["msg_type"] : void 0,
112552
+ content: body && typeof body === "object" && typeof body["content"] === "string" ? body["content"] : void 0,
112553
+ threadId: typeof item["thread_id"] === "string" && item["thread_id"] ? item["thread_id"] : void 0
112554
+ };
112555
+ if (this.#cache.size >= CACHE_MAX) {
112556
+ const oldest = this.#cache.keys().next().value;
112557
+ if (oldest !== void 0) this.#cache.delete(oldest);
112558
+ }
112559
+ this.#cache.set(messageId, info);
112560
+ return info;
112561
+ } catch {
112562
+ return void 0;
112563
+ }
112564
+ }
112565
+ };
112566
+ }
112567
+ });
112568
+
112503
112569
  // src/lark/channelClient.ts
112504
112570
  var channelClient_exports = {};
112505
112571
  __export(channelClient_exports, {
@@ -112660,6 +112726,7 @@ var init_channelClient = __esm({
112660
112726
  init_channelCardKitClient();
112661
112727
  init_channelPostClient();
112662
112728
  init_channelCotClient();
112729
+ init_messageLookupClient();
112663
112730
  execFile4 = promisify4(execFileCallback);
112664
112731
  LEARNED_CHATS_LIMIT = 100;
112665
112732
  SEEN_MESSAGES_LIMIT = 1e3;
@@ -112820,6 +112887,8 @@ var init_channelClient = __esm({
112820
112887
  postClient = null;
112821
112888
  /** Lazily built COT (思维链) client; only requested when a bot enables cot != off. */
112822
112889
  cotClient = null;
112890
+ /** Lazily built single-message lookup client (v4 任务派单 root-type probe). */
112891
+ messageLookupClient = null;
112823
112892
  constructor(opts) {
112824
112893
  if (!opts.appId || !opts.appSecret) {
112825
112894
  throw new Error(
@@ -113103,6 +113172,20 @@ var init_channelClient = __esm({
113103
113172
  }
113104
113173
  return this.cotClient;
113105
113174
  }
113175
+ /**
113176
+ * Return a MessageLookupClient bound to this client's channel handle
113177
+ * (docs/task-handle.md §15.4 — the v4 任务派单 root-type probe). Same lazy
113178
+ * channel resolution and generic rawClient.request() auth path as the COT
113179
+ * client above; strictly best-effort per the client's own contract.
113180
+ */
113181
+ outboundMessageLookupClient() {
113182
+ if (!this.messageLookupClient) {
113183
+ this.messageLookupClient = new ChannelMessageLookupClient({
113184
+ resolveChannel: () => this.channel
113185
+ });
113186
+ }
113187
+ return this.messageLookupClient;
113188
+ }
113106
113189
  /**
113107
113190
  * Return an OutboundPostClient bound to this client's channel handle.
113108
113191
  *
@@ -113793,7 +113876,7 @@ function isTaskRequestTimeoutError(err2) {
113793
113876
  if (err2 instanceof TaskApiError && err2.cause instanceof TaskRequestTimeoutError) return true;
113794
113877
  return false;
113795
113878
  }
113796
- function withTimeout2(promise, ms, label) {
113879
+ function withTimeout3(promise, ms, label) {
113797
113880
  let timer;
113798
113881
  const timeout = new Promise((_, reject) => {
113799
113882
  timer = setTimeout(() => reject(new TaskRequestTimeoutError(`${label} timed out after ${ms}ms`)), ms);
@@ -113837,7 +113920,7 @@ var init_client = __esm({
113837
113920
  }
113838
113921
  /** Single choke point for every outbound call — applies the timeout race uniformly. */
113839
113922
  #request(config, label) {
113840
- return withTimeout2(this.#requester.request(config), this.#timeoutMs, label);
113923
+ return withTimeout3(this.#requester.request(config), this.#timeoutMs, label);
113841
113924
  }
113842
113925
  async getTask(taskGuid) {
113843
113926
  let resp;
package/dist/main.js CHANGED
@@ -116650,6 +116650,71 @@ function stringField(data, key) {
116650
116650
  return typeof value === "string" ? value : void 0;
116651
116651
  }
116652
116652
 
116653
+ // src/lark/messageLookupClient.ts
116654
+ function buildTopicDeepLink(chatId, threadId) {
116655
+ const c = encodeURIComponent(chatId);
116656
+ const t = encodeURIComponent(threadId);
116657
+ return `https://applink.feishu.cn/client/thread/open?open_chat_id=${c}&open_thread_id=${t}&openchatid=${c}&openthreadid=${t}&thread_position=-1`;
116658
+ }
116659
+ var LOOKUP_TIMEOUT_MS = 5e3;
116660
+ var CACHE_MAX = 200;
116661
+ async function withTimeout2(p, ms, label) {
116662
+ let timer;
116663
+ const timeout = new Promise((_resolve, reject) => {
116664
+ timer = setTimeout(() => reject(new Error(`[channel.msglookup] ${label} timed out after ${ms}ms`)), ms);
116665
+ timer.unref?.();
116666
+ });
116667
+ try {
116668
+ return await Promise.race([p, timeout]);
116669
+ } finally {
116670
+ if (timer) clearTimeout(timer);
116671
+ }
116672
+ }
116673
+ var ChannelMessageLookupClient = class {
116674
+ #resolveChannel;
116675
+ #cache = /* @__PURE__ */ new Map();
116676
+ constructor(opts) {
116677
+ this.#resolveChannel = opts.resolveChannel;
116678
+ }
116679
+ async get(messageId, opts) {
116680
+ if (!opts?.refresh) {
116681
+ const cached = this.#cache.get(messageId);
116682
+ if (cached) return cached;
116683
+ }
116684
+ try {
116685
+ const channel = this.#resolveChannel();
116686
+ if (!channel) return void 0;
116687
+ const req = {
116688
+ url: `/open-apis/im/v1/messages/${encodeURIComponent(messageId)}`,
116689
+ method: "GET"
116690
+ };
116691
+ const res = await withTimeout2(
116692
+ channel.rawClient.request(req),
116693
+ LOOKUP_TIMEOUT_MS,
116694
+ `GET message ${messageId}`
116695
+ );
116696
+ if (res.code !== void 0 && res.code !== 0) return void 0;
116697
+ const items = res.data?.["items"];
116698
+ if (!Array.isArray(items) || items.length === 0) return void 0;
116699
+ const item = items[0];
116700
+ const body = item["body"];
116701
+ const info = {
116702
+ msgType: typeof item["msg_type"] === "string" ? item["msg_type"] : void 0,
116703
+ content: body && typeof body === "object" && typeof body["content"] === "string" ? body["content"] : void 0,
116704
+ threadId: typeof item["thread_id"] === "string" && item["thread_id"] ? item["thread_id"] : void 0
116705
+ };
116706
+ if (this.#cache.size >= CACHE_MAX) {
116707
+ const oldest = this.#cache.keys().next().value;
116708
+ if (oldest !== void 0) this.#cache.delete(oldest);
116709
+ }
116710
+ this.#cache.set(messageId, info);
116711
+ return info;
116712
+ } catch {
116713
+ return void 0;
116714
+ }
116715
+ }
116716
+ };
116717
+
116653
116718
  // src/lark/channelClient.ts
116654
116719
  var execFile = promisify(execFileCallback);
116655
116720
  var LEARNED_CHATS_LIMIT = 100;
@@ -116947,6 +117012,8 @@ var ChannelClient = class {
116947
117012
  postClient = null;
116948
117013
  /** Lazily built COT (思维链) client; only requested when a bot enables cot != off. */
116949
117014
  cotClient = null;
117015
+ /** Lazily built single-message lookup client (v4 任务派单 root-type probe). */
117016
+ messageLookupClient = null;
116950
117017
  constructor(opts) {
116951
117018
  if (!opts.appId || !opts.appSecret) {
116952
117019
  throw new Error(
@@ -117230,6 +117297,20 @@ var ChannelClient = class {
117230
117297
  }
117231
117298
  return this.cotClient;
117232
117299
  }
117300
+ /**
117301
+ * Return a MessageLookupClient bound to this client's channel handle
117302
+ * (docs/task-handle.md §15.4 — the v4 任务派单 root-type probe). Same lazy
117303
+ * channel resolution and generic rawClient.request() auth path as the COT
117304
+ * client above; strictly best-effort per the client's own contract.
117305
+ */
117306
+ outboundMessageLookupClient() {
117307
+ if (!this.messageLookupClient) {
117308
+ this.messageLookupClient = new ChannelMessageLookupClient({
117309
+ resolveChannel: () => this.channel
117310
+ });
117311
+ }
117312
+ return this.messageLookupClient;
117313
+ }
117233
117314
  /**
117234
117315
  * Return an OutboundPostClient bound to this client's channel handle.
117235
117316
  *
@@ -118525,7 +118606,10 @@ var SessionStore = class _SessionStore {
118525
118606
  lastActiveTs: record.lastActiveTs,
118526
118607
  ...record.senderOpenId !== void 0 ? { senderOpenId: record.senderOpenId } : {},
118527
118608
  ...record.rootText !== void 0 ? { rootText: record.rootText } : {},
118528
- ...record.chatId !== void 0 ? { chatId: record.chatId } : {}
118609
+ ...record.chatId !== void 0 ? { chatId: record.chatId } : {},
118610
+ // BL-38: only persist when > 0 — a 0/undefined counter is a clean thread,
118611
+ // so passing consecutiveStuckCount: 0 naturally clears the field on reset.
118612
+ ...record.consecutiveStuckCount ? { consecutiveStuckCount: record.consecutiveStuckCount } : {}
118529
118613
  };
118530
118614
  this.#map.set(key, stored);
118531
118615
  await this.#flush();
@@ -118653,6 +118737,17 @@ function extractPostText(value) {
118653
118737
  }
118654
118738
  return "";
118655
118739
  }
118740
+ function parseTodoShareContent(rawContent) {
118741
+ try {
118742
+ const parsed = JSON.parse(rawContent);
118743
+ const taskGuid = parsed["task_id"];
118744
+ if (typeof taskGuid !== "string" || taskGuid.length === 0) return null;
118745
+ const summaryText = extractPostText(parsed["summary"]).trim();
118746
+ return { taskGuid, summaryText };
118747
+ } catch {
118748
+ return null;
118749
+ }
118750
+ }
118656
118751
  function extractText(event) {
118657
118752
  let parsed;
118658
118753
  try {
@@ -118664,6 +118759,10 @@ function extractText(event) {
118664
118759
  return stripAtPlaceholders(event.content);
118665
118760
  }
118666
118761
  let raw = "";
118762
+ const todo = typeof parsed["task_id"] === "string" ? parseTodoShareContent(event.content) : null;
118763
+ if (todo) {
118764
+ return `[\u98DE\u4E66\u4EFB\u52A1] ${todo.summaryText || "(\u65E0\u6807\u9898)"} (task_guid=${todo.taskGuid})`;
118765
+ }
118667
118766
  if (typeof parsed["text"] === "string") {
118668
118767
  raw = parsed["text"];
118669
118768
  } else if (parsed["content"] !== void 0) {
@@ -119111,6 +119210,27 @@ function renderTaskHandleBlock(tasklistGuid, claimed, candidates) {
119111
119210
  "</task-handle>"
119112
119211
  ];
119113
119212
  }
119213
+ function renderTaskRootBlock(taskRoot) {
119214
+ const lines = [
119215
+ "<task-root>",
119216
+ "\u672C\u8BDD\u9898\u7684\u6839\u6D88\u606F\u662F\u4E00\u6761\u98DE\u4E66\u4EFB\u52A1\u5206\u4EAB(\u4EFB\u52A1\u6D3E\u5355\u4E3B\u8DEF\u5F84,docs/task-handle.md \xA715)\u3002",
119217
+ `task_guid: ${taskRoot.guid}`,
119218
+ `task_summary: ${taskRoot.summary || "(\u65E0\u6807\u9898)"}`,
119219
+ ...taskRoot.topicLink ? [`topic_link: ${taskRoot.topicLink}`] : [],
119220
+ `task_root_claimed: ${taskRoot.claimed ? "yes" : "no"}`
119221
+ ];
119222
+ if (taskRoot.claimed) {
119223
+ lines.push(
119224
+ "\u672C\u8BDD\u9898\u5DF2\u8BA4\u9886\u8FD9\u4E2A\u4EFB\u52A1\u3002\u7EF4\u62A4\u9762\u53EA\u6709\u4EFB\u52A1\u8BC4\u8BBA:\u4EC5\u5728\u4EA4\u4ED8/\u5931\u8D25/\u7B49\u62CD\u677F\u4E09\u7C7B\u91CC\u7A0B\u7891\u8282\u70B9\u53D1\u4EFB\u52A1\u8BC4\u8BBA(lark-cli task comments create);\u771F\u6B63\u4EA4\u4ED8\u7684\u90A3\u4E00\u8F6E\u5728 state.json \u7684 task_handle \u91CC\u58F0\u660E done: true,\u5E76\u53D1\u4E00\u6761\u300C\u5DF2\u4EA4\u4ED8,\u770B\u8FC7\u8BF7\u70B9\u5B8C\u6210\u300D\u8BC4\u8BBA\u3002\u4E0D\u8981\u4FEE\u6539\u4EFB\u52A1\u63CF\u8FF0\u3001\u4E0D\u8981\u52FE\u5B8C\u6210/reopen\u2014\u2014\u5B8C\u6210\u6C38\u8FDC\u7531\u4EBA\u5728\u4EFB\u52A1\u4E2D\u5FC3\u70B9\u3002"
119225
+ );
119226
+ } else {
119227
+ lines.push(
119228
+ `\u672C\u8F6E\u8BF7\u987A\u5E26\u9759\u9ED8\u8BA4\u9886,\u5206\u4E24\u6B65:\u2460 \u5148\u8BFB\u4E00\u904D\u8BE5\u4EFB\u52A1\u7684\u73B0\u6709\u8BC4\u8BBA(lark-cli task comments list)\u2014\u2014\u5982\u679C\u5DF2\u6709**\u5176\u4ED6 agent** \u7684\u8BA4\u9886\u58F0\u660E,\u8BF4\u660E\u540C\u8BDD\u9898\u7684\u53E6\u4E00\u4E2A bot \u5DF2\u63A5\u624B,\u672C\u8F6E\u4E0D\u8BA4\u9886\u3001\u4E0D\u58F0\u5F20\u3001\u6B63\u5E38\u56DE\u7B54\u5373\u53EF(\u53CC @ \u7ADE\u6001\u62A4\u680F:\u91CD\u590D\u8BA4\u9886 = \u53CC\u4EFD\u7EF4\u62A4 + \u53CC\u4EFD\u5DE1\u68C0\u9A9A\u6270);\u2461 \u6CA1\u6709\u522B\u4EBA\u8BA4\u9886 \u2192 \u628A\u4E0A\u9762\u7684 task_guid \u5199\u5165 state.json \u7684 task_handle.guid,\u5E76\u53D1\u4E00\u6761\u8BA4\u9886\u8BC4\u8BBA(lark-cli task comments create),\u5185\u5BB9 = \u4E00\u53E5\u8BA4\u9886\u58F0\u660E${taskRoot.topicLink ? " + \u4E0A\u9762\u7684 topic_link(\u7528\u6237\u70B9\u5B83\u76F4\u8FBE\u672C\u8BDD\u9898)" : ""}\u3002\u4E4B\u540E\u7EF4\u62A4\u9762\u53EA\u6709\u4EFB\u52A1\u8BC4\u8BBA(\u4EA4\u4ED8/\u5931\u8D25/\u7B49\u62CD\u677F\u624D\u53D1,\u8FC7\u7A0B\u788E\u788E\u5FF5\u4E0D\u53D1);\u4E0D\u8981\u4FEE\u6539\u4EFB\u52A1\u63CF\u8FF0\u3001\u4E0D\u8981\u52FE\u5B8C\u6210\u2014\u2014\u5B8C\u6210\u6C38\u8FDC\u7531\u4EBA\u70B9\u3002`
119229
+ );
119230
+ }
119231
+ lines.push("</task-root>");
119232
+ return lines;
119233
+ }
119114
119234
  function renderRuntimeWarningsBlock(warnings) {
119115
119235
  if (warnings.length === 0) return [];
119116
119236
  const hasMissingLarkCli = warnings.some((warning) => warning.command === "lark-cli");
@@ -119264,6 +119384,7 @@ async function renderPrompt(input) {
119264
119384
  taskHandleTasklistGuid,
119265
119385
  taskHandleClaimed = false,
119266
119386
  taskHandleCandidates = [],
119387
+ taskRoot,
119267
119388
  mtimeFacts = []
119268
119389
  } = input;
119269
119390
  const backend = input.backend ?? "claude";
@@ -119278,7 +119399,8 @@ async function renderPrompt(input) {
119278
119399
  const stateContract = renderStateContract(conventions.stateFilePath);
119279
119400
  const peersBlock = peers && peers.length > 0 ? renderPeersBlock(peers) : [];
119280
119401
  const turnTakingBlock = turn_taking_limit && turn_taking_limit > 0 ? renderTurnTakingHint(turn_taking_limit) : [];
119281
- const taskHandleBlock = taskHandleTasklistGuid ? renderTaskHandleBlock(taskHandleTasklistGuid, taskHandleClaimed, taskHandleCandidates) : [];
119402
+ const taskHandleBlock = taskHandleTasklistGuid && !taskRoot ? renderTaskHandleBlock(taskHandleTasklistGuid, taskHandleClaimed, taskHandleCandidates) : [];
119403
+ const taskRootBlock = taskRoot ? renderTaskRootBlock(taskRoot) : [];
119282
119404
  const runtimeWarningsBlock = renderRuntimeWarningsBlock(runtimeWarnings);
119283
119405
  const extraRepos = extraRepoPaths ?? conventions.extraRepoPaths ?? [];
119284
119406
  const workspaceBlock = isAgentWorkspace ? await renderAgentWorkspaceBlock(conventions, extraRepos, isNewThread, mtimeFacts) : hasRepo ? renderWorkspaceBlock(
@@ -119395,6 +119517,7 @@ async function renderPrompt(input) {
119395
119517
  ...peersBlock.length > 0 ? ["", ...peersBlock] : [],
119396
119518
  ...turnTakingBlock.length > 0 ? ["", ...turnTakingBlock] : [],
119397
119519
  ...taskHandleBlock.length > 0 ? ["", ...taskHandleBlock] : [],
119520
+ ...taskRootBlock.length > 0 ? ["", ...taskRootBlock] : [],
119398
119521
  "",
119399
119522
  "<user-message>",
119400
119523
  `${parsed.senderOpenId}: ${parsed.text}`,
@@ -119451,6 +119574,7 @@ async function renderPrompt(input) {
119451
119574
  ...peersBlock.length > 0 ? ["", ...peersBlock] : [],
119452
119575
  ...turnTakingBlock.length > 0 ? ["", ...turnTakingBlock] : [],
119453
119576
  ...taskHandleBlock.length > 0 ? ["", ...taskHandleBlock] : [],
119577
+ ...taskRootBlock.length > 0 ? ["", ...taskRootBlock] : [],
119454
119578
  "",
119455
119579
  "<user-message>",
119456
119580
  `${parsed.senderOpenId}: ${parsed.text}`,
@@ -121946,6 +122070,13 @@ function derivePostIdempotencyKey(input) {
121946
122070
  var DEFAULT_CARDKIT_RESPONSE_SURFACE_TIMEOUT_MS = 20 * 60 * 1e3;
121947
122071
  var DEFAULT_CARDKIT_IDLE_TIMEOUT_MS = 3 * 60 * 1e3;
121948
122072
  var COT_BUBBLE_CREATE_BUDGET_MS = 3e3;
122073
+ var DEFAULT_STUCK_SESSION_RESET_AFTER = 3;
122074
+ function resolveStuckSessionResetAfter() {
122075
+ const raw = process.env.LARKWAY_STUCK_SESSION_RESET_AFTER;
122076
+ if (raw === void 0) return DEFAULT_STUCK_SESSION_RESET_AFTER;
122077
+ const n = Number.parseInt(raw, 10);
122078
+ return Number.isFinite(n) && n > 0 ? n : DEFAULT_STUCK_SESSION_RESET_AFTER;
122079
+ }
121949
122080
  function summarizeMentionPolicyRules(rules) {
121950
122081
  const counts = /* @__PURE__ */ new Map();
121951
122082
  for (const rule of rules) {
@@ -122471,7 +122602,34 @@ var BridgeHandler = class {
122471
122602
  const existing = this.deps.sessionStore.get(threadId, botId);
122472
122603
  const isNewThread = existing === void 0;
122473
122604
  const isTopLevel = !(typeof parsed.raw.root_id === "string" && parsed.raw.root_id);
122474
- const replyInThread = isTopLevel;
122605
+ let replyInThread = isTopLevel;
122606
+ let replyAnchorId = messageId;
122607
+ let taskRootInfo;
122608
+ let deferredTaskRootProbe;
122609
+ {
122610
+ const rootId = typeof parsed.raw.root_id === "string" && parsed.raw.root_id ? parsed.raw.root_id : void 0;
122611
+ const rawThreadId = typeof parsed.raw.thread_id === "string" && parsed.raw.thread_id ? parsed.raw.thread_id : void 0;
122612
+ if (rootId && this.deps.messageLookup) {
122613
+ const probe = this.deps.messageLookup.get(rootId).catch(() => void 0);
122614
+ if (rawThreadId) {
122615
+ deferredTaskRootProbe = probe;
122616
+ } else {
122617
+ const info = await probe;
122618
+ if (info?.msgType === "todo" && info.content) {
122619
+ const todo = parseTodoShareContent(info.content);
122620
+ if (todo) {
122621
+ taskRootInfo = {
122622
+ guid: todo.taskGuid,
122623
+ summary: todo.summaryText,
122624
+ topicLink: info.threadId ? buildTopicDeepLink(parsed.chatId, info.threadId) : void 0
122625
+ };
122626
+ replyInThread = true;
122627
+ replyAnchorId = rootId;
122628
+ }
122629
+ }
122630
+ }
122631
+ }
122632
+ }
122475
122633
  const prototypeConfig = this.deps.botConfig?.response_surface_prototype;
122476
122634
  const cardKitAvailable = isResponseSurfaceCardKitAvailable(
122477
122635
  prototypeConfig,
@@ -122487,7 +122645,7 @@ var BridgeHandler = class {
122487
122645
  let startFailurePostFallbackSent = false;
122488
122646
  if (!cardKitAvailable) {
122489
122647
  try {
122490
- card = await this.deps.cardRenderer.start(messageId, { replyInThread, threadId });
122648
+ card = await this.deps.cardRenderer.start(replyAnchorId, { replyInThread, threadId });
122491
122649
  await recordEvent({
122492
122650
  status: "running",
122493
122651
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -122586,7 +122744,7 @@ var BridgeHandler = class {
122586
122744
  try {
122587
122745
  cardKitProgress = await createCardKitProgressHandle({
122588
122746
  cardKitClient: this.deps.cardKitClient,
122589
- replyToMessageId: messageId,
122747
+ replyToMessageId: replyAnchorId,
122590
122748
  replyInThread,
122591
122749
  facts: {
122592
122750
  botId: this.deps.botConfig?.id ?? "v1-default",
@@ -122617,7 +122775,7 @@ var BridgeHandler = class {
122617
122775
  status: "message_sent",
122618
122776
  cardId: cardKitProgress.cardId,
122619
122777
  messageId: cardKitProgress.messageId,
122620
- replyToMessageId: messageId,
122778
+ replyToMessageId: replyAnchorId,
122621
122779
  chatId: parsed.chatId,
122622
122780
  threadId,
122623
122781
  botId: this.deps.botConfig?.id ?? "",
@@ -122662,7 +122820,7 @@ var BridgeHandler = class {
122662
122820
  }
122663
122821
  if (!card && cardKitStartFailed) {
122664
122822
  try {
122665
- card = await this.deps.cardRenderer.start(messageId, { replyInThread, threadId });
122823
+ card = await this.deps.cardRenderer.start(replyAnchorId, { replyInThread, threadId });
122666
122824
  await this.deps.client.removeProcessingReaction?.(messageId);
122667
122825
  await recordEvent({
122668
122826
  status: "running",
@@ -122677,7 +122835,7 @@ var BridgeHandler = class {
122677
122835
  );
122678
122836
  const postFallback = await createOnlyPostFallback({
122679
122837
  postClient: this.deps.postClient,
122680
- replyToMessageId: messageId,
122838
+ replyToMessageId: replyAnchorId,
122681
122839
  replyInThread,
122682
122840
  botId: this.deps.botConfig?.id ?? "v1-default",
122683
122841
  threadId,
@@ -122914,6 +123072,26 @@ var BridgeHandler = class {
122914
123072
  console.warn("[bridge.handler] live-roster resolve failed (using static peers):", err);
122915
123073
  }
122916
123074
  }
123075
+ if (deferredTaskRootProbe) {
123076
+ const info = await deferredTaskRootProbe;
123077
+ if (info?.msgType === "todo" && info.content) {
123078
+ const todo = parseTodoShareContent(info.content);
123079
+ if (todo) {
123080
+ const rawThreadId = typeof parsed.raw.thread_id === "string" && parsed.raw.thread_id ? parsed.raw.thread_id : void 0;
123081
+ taskRootInfo = {
123082
+ guid: todo.taskGuid,
123083
+ summary: todo.summaryText,
123084
+ topicLink: rawThreadId ? buildTopicDeepLink(parsed.chatId, rawThreadId) : info.threadId ? buildTopicDeepLink(parsed.chatId, info.threadId) : void 0
123085
+ };
123086
+ }
123087
+ }
123088
+ }
123089
+ if (taskRootInfo && !taskRootInfo.topicLink && replyAnchorId !== messageId && this.deps.messageLookup) {
123090
+ const refreshed = await this.deps.messageLookup.get(replyAnchorId, { refresh: true }).catch(() => void 0);
123091
+ if (refreshed?.threadId) {
123092
+ taskRootInfo.topicLink = buildTopicDeepLink(parsed.chatId, refreshed.threadId);
123093
+ }
123094
+ }
122917
123095
  const prompt = await renderPrompt({
122918
123096
  parsed,
122919
123097
  isNewThread: currentIsNewThread,
@@ -122945,6 +123123,13 @@ var BridgeHandler = class {
122945
123123
  taskHandleTasklistGuid: this.deps.botConfig?.taskHandle?.tasklistGuid,
122946
123124
  taskHandleClaimed: this.deps.taskHandleClaimedLookup?.(threadId) ?? false,
122947
123125
  taskHandleCandidates: this.deps.taskHandleCandidatesLookup?.() ?? [],
123126
+ taskRoot: taskRootInfo ? {
123127
+ ...taskRootInfo,
123128
+ // Exact-guid comparison (see taskHandleClaimGuidLookup's doc);
123129
+ // falls back to the boolean lookup only when the precise one
123130
+ // isn't wired (older embedding code).
123131
+ claimed: this.deps.taskHandleClaimGuidLookup ? this.deps.taskHandleClaimGuidLookup(threadId) === taskRootInfo.guid : this.deps.taskHandleClaimedLookup?.(threadId) ?? false
123132
+ } : void 0,
122948
123133
  mtimeFacts
122949
123134
  });
122950
123135
  const backend = this.deps.botConfig?.backend ?? "claude";
@@ -123086,40 +123271,19 @@ var BridgeHandler = class {
123086
123271
  botId,
123087
123272
  threadId,
123088
123273
  chatId: parsed.chatId,
123089
- taskGuid: claimedTaskGuid
123274
+ taskGuid: claimedTaskGuid,
123275
+ // v4 任务派单 (docs/task-handle.md §15.3): a claim on the very
123276
+ // task this thread's ROOT message shares is comment-mode —
123277
+ // maintenance goes through task comments only (share-to-chat
123278
+ // grants read+comment; no tasklist/editor rights needed, and
123279
+ // completion is ALWAYS ticked by the human). Mechanical
123280
+ // equality check, not a judgment call.
123281
+ mode: taskRootInfo?.guid === claimedTaskGuid ? "comment" : void 0
123090
123282
  });
123091
123283
  } catch (err) {
123092
123284
  console.warn("[bridge.handler] taskHandleClaim hook failed (continuing):", err);
123093
123285
  }
123094
123286
  }
123095
- const now = Date.now();
123096
- if (sessionId !== void 0 && currentExisting === void 0) {
123097
- await this.deps.sessionStore.put({
123098
- threadId,
123099
- sessionId,
123100
- botId,
123101
- createdTs: now,
123102
- lastActiveTs: now,
123103
- senderOpenId,
123104
- ...isTopLevel ? { rootText: parsed.text.slice(0, 200), chatId: parsed.chatId } : {}
123105
- });
123106
- } else if (sessionId !== void 0 && currentExisting !== void 0) {
123107
- await this.deps.sessionStore.put({
123108
- threadId,
123109
- sessionId,
123110
- botId,
123111
- createdTs: currentExisting.createdTs,
123112
- lastActiveTs: now,
123113
- senderOpenId,
123114
- rootText: currentExisting.rootText,
123115
- chatId: currentExisting.chatId
123116
- });
123117
- } else if (currentExisting !== void 0 && sessionId === void 0) {
123118
- await this.deps.sessionStore.put({
123119
- ...currentExisting,
123120
- lastActiveTs: now
123121
- });
123122
- }
123123
123287
  const reportedStatus = reportedState?.status;
123124
123288
  const reportedError = reportedState?.error;
123125
123289
  const cardKitTimeoutFailure = interruptedByIdle && reportedStatus !== "ready" && reportedStatus !== "failed";
@@ -123149,10 +123313,53 @@ var BridgeHandler = class {
123149
123313
  reason: failureReason
123150
123314
  });
123151
123315
  }
123316
+ const stuckResetAfter = resolveStuckSessionResetAfter();
123317
+ const prevStuckCount = currentExisting?.consecutiveStuckCount ?? 0;
123318
+ const nextStuckCount = cardKitTimeoutFailure ? prevStuckCount + 1 : success ? 0 : prevStuckCount;
123319
+ const stuckResetTriggered = cardKitTimeoutFailure && nextStuckCount >= stuckResetAfter;
123320
+ const now = Date.now();
123321
+ if (stuckResetTriggered) {
123322
+ await this.deps.sessionStore.delete(threadId, botId);
123323
+ console.info(
123324
+ `[bridge.handler] BL-38 stuck-session reset: thread=${threadId} bot=${botId} consecutiveStuckCount=${nextStuckCount} (>= ${stuckResetAfter}) \u2014 dropped session record; next @ starts fresh`
123325
+ );
123326
+ } else if (sessionId !== void 0 && currentExisting === void 0) {
123327
+ await this.deps.sessionStore.put({
123328
+ threadId,
123329
+ sessionId,
123330
+ botId,
123331
+ createdTs: now,
123332
+ lastActiveTs: now,
123333
+ senderOpenId,
123334
+ // BL-38: a brand-new thread that idle-killed on its very first turn
123335
+ // starts the counter at 1 (0 when clean; only persisted when > 0).
123336
+ consecutiveStuckCount: nextStuckCount,
123337
+ ...isTopLevel ? { rootText: parsed.text.slice(0, 200), chatId: parsed.chatId } : {}
123338
+ });
123339
+ } else if (sessionId !== void 0 && currentExisting !== void 0) {
123340
+ await this.deps.sessionStore.put({
123341
+ threadId,
123342
+ sessionId,
123343
+ botId,
123344
+ createdTs: currentExisting.createdTs,
123345
+ lastActiveTs: now,
123346
+ senderOpenId,
123347
+ rootText: currentExisting.rootText,
123348
+ chatId: currentExisting.chatId,
123349
+ // BL-38: +1 on an idle-stuck turn, 0 (cleared) on any clean turn.
123350
+ consecutiveStuckCount: nextStuckCount
123351
+ });
123352
+ } else if (currentExisting !== void 0 && sessionId === void 0) {
123353
+ await this.deps.sessionStore.put({
123354
+ ...currentExisting,
123355
+ lastActiveTs: now,
123356
+ consecutiveStuckCount: nextStuckCount
123357
+ });
123358
+ }
123152
123359
  const fallbackAnswer = trustedAnswerText.trim() || cardKitProgress?.answerText.trim() || "";
123153
123360
  const noOutputFallback = "\u26A0\uFE0F \u672C\u8F6E agent \u6CA1\u6709\u4EA7\u51FA\u6B63\u6587\uFF0C\u4E5F\u6CA1\u6709\u5199\u5165\u6709\u6548\u72B6\u6001(state.json)\u3002\n" + (result.exitCode !== 0 ? `agent \u8FDB\u7A0B\u5F02\u5E38\u9000\u51FA(exit code ${result.exitCode})\u3002
123154
123361
  ` : "") + "\u4E0B\u4E00\u6B65\uFF1A\u6362\u4E2A\u8BF4\u6CD5\u91CD\u8BD5\uFF0C\u6216\u65B0\u5F00\u4E00\u4E2A\u8BDD\u9898\u7EE7\u7EED\uFF1B\u82E5\u53CD\u590D\u5982\u6B64\uFF0C\u8BF7\u8BA9\u7EF4\u62A4\u8005\u67E5\u770B\u8BE5 session \u65E5\u5FD7\u3002";
123155
- const cardBody = cardKitTimeoutFailure ? "\u26A0\uFE0F \u672C\u8F6E\u88AB\u4E2D\u65AD\uFF08\u957F\u65F6\u95F4\u65E0\u6D3B\u6027\uFF0C\u5224\u5B9A\u5361\u6B7B\uFF09\uFF0C\u672A\u5B8C\u6210\u3002\u8BF7\u91CD\u8BD5\u3002" : reportedState?.last_message ?? (fallbackAnswer ? fallbackAnswer : noOutputFallback);
123362
+ const cardBody = stuckResetTriggered ? "\u26A0\uFE0F \u672C\u8F6E\u88AB\u4E2D\u65AD\uFF08\u957F\u65F6\u95F4\u65E0\u6D3B\u6027\uFF0C\u5224\u5B9A\u5361\u6B7B\uFF09\u3002\u8FDE\u7EED\u591A\u6B21\u5361\u6B7B\uFF0C\u5DF2\u91CD\u7F6E\u672C\u8BDD\u9898\u4E0A\u4E0B\u6587 \u2014\u2014 \u4E0B\u6B21 @ \u6211\u5C06\u5168\u65B0\u5F00\u59CB\uFF0C\u8BF7\u628A\u9700\u6C42\u91CD\u65B0\u8BF4\u4E00\u904D\u3002" : cardKitTimeoutFailure ? "\u26A0\uFE0F \u672C\u8F6E\u88AB\u4E2D\u65AD\uFF08\u957F\u65F6\u95F4\u65E0\u6D3B\u6027\uFF0C\u5224\u5B9A\u5361\u6B7B\uFF09\uFF0C\u672A\u5B8C\u6210\u3002\u8BF7\u91CD\u8BD5\u3002" : reportedState?.last_message ?? (fallbackAnswer ? fallbackAnswer : noOutputFallback);
123156
123363
  const noReportThisTurn = reportedState === null;
123157
123364
  const neutralTitle = noReportThisTurn && success && !failureReason ? "\u{1F4AC} \u5DF2\u56DE\u590D" : void 0;
123158
123365
  const baseCardPayload = {
@@ -123216,7 +123423,7 @@ var BridgeHandler = class {
123216
123423
  console.warn("[bridge.handler] CardKit finalize failed; using card fallback:", err);
123217
123424
  cardKitProgress.close();
123218
123425
  try {
123219
- card = await this.deps.cardRenderer.start(messageId, { replyInThread, threadId });
123426
+ card = await this.deps.cardRenderer.start(replyAnchorId, { replyInThread, threadId });
123220
123427
  await writeCardFile(worktreePath, {
123221
123428
  messageId: card.messageId,
123222
123429
  chatId: parsed.chatId,
@@ -123241,7 +123448,7 @@ var BridgeHandler = class {
123241
123448
  } catch (legacyErr) {
123242
123449
  const postFallback = await createOnlyPostFallback({
123243
123450
  postClient: this.deps.postClient,
123244
- replyToMessageId: messageId,
123451
+ replyToMessageId: replyAnchorId,
123245
123452
  replyInThread,
123246
123453
  botId: this.deps.botConfig?.id ?? "v1-default",
123247
123454
  threadId,
@@ -123265,7 +123472,7 @@ var BridgeHandler = class {
123265
123472
  } else {
123266
123473
  if (!card) {
123267
123474
  try {
123268
- card = await this.deps.cardRenderer.start(messageId, { replyInThread, threadId });
123475
+ card = await this.deps.cardRenderer.start(replyAnchorId, { replyInThread, threadId });
123269
123476
  try {
123270
123477
  await writeCardFile(worktreePath, {
123271
123478
  messageId: card.messageId,
@@ -123289,7 +123496,7 @@ var BridgeHandler = class {
123289
123496
  ].filter((part) => !!part).join("; ");
123290
123497
  const postFallback = await createOnlyPostFallback({
123291
123498
  postClient: this.deps.postClient,
123292
- replyToMessageId: messageId,
123499
+ replyToMessageId: replyAnchorId,
123293
123500
  replyInThread,
123294
123501
  botId: this.deps.botConfig?.id ?? "v1-default",
123295
123502
  threadId,
@@ -123370,7 +123577,7 @@ var BridgeHandler = class {
123370
123577
  const createHardFailurePostFallback = async (failureReason) => {
123371
123578
  const fallback = await createOnlyPostFallback({
123372
123579
  postClient: this.deps.postClient,
123373
- replyToMessageId: messageId,
123580
+ replyToMessageId: replyAnchorId,
123374
123581
  replyInThread,
123375
123582
  botId: this.deps.botConfig?.id ?? "v1-default",
123376
123583
  threadId,
@@ -123399,7 +123606,7 @@ var BridgeHandler = class {
123399
123606
  cardKitFinalizeErr
123400
123607
  );
123401
123608
  try {
123402
- card = await this.deps.cardRenderer.start(messageId, { replyInThread, threadId });
123609
+ card = await this.deps.cardRenderer.start(replyAnchorId, { replyInThread, threadId });
123403
123610
  } catch (cardStartErr) {
123404
123611
  console.error("[bridge.handler] failure card start also failed:", cardStartErr);
123405
123612
  await createHardFailurePostFallback(
@@ -129989,7 +130196,7 @@ function isStallNudgeState(value) {
129989
130196
  function isTaskHandleRecord(value) {
129990
130197
  if (typeof value !== "object" || value === null) return false;
129991
130198
  const v = value;
129992
- return typeof v["threadId"] === "string" && typeof v["taskGuid"] === "string" && typeof v["chatId"] === "string" && typeof v["claimedTs"] === "number" && (v["lastSeenCommentId"] === void 0 || typeof v["lastSeenCommentId"] === "string") && (v["lastTurnOutcome"] === void 0 || v["lastTurnOutcome"] === "completed" || v["lastTurnOutcome"] === "failed") && (v["stallNudge"] === void 0 || isStallNudgeState(v["stallNudge"])) && (v["stallSuppressUntilActivityAfter"] === void 0 || typeof v["stallSuppressUntilActivityAfter"] === "number") && (v["lastTurnMentions"] === void 0 || Array.isArray(v["lastTurnMentions"]) && v["lastTurnMentions"].every((m) => typeof m === "string")) && (v["lastTurnMentionsAt"] === void 0 || typeof v["lastTurnMentionsAt"] === "number");
130199
+ return typeof v["threadId"] === "string" && typeof v["taskGuid"] === "string" && typeof v["chatId"] === "string" && typeof v["claimedTs"] === "number" && (v["lastSeenCommentId"] === void 0 || typeof v["lastSeenCommentId"] === "string") && (v["lastTurnOutcome"] === void 0 || v["lastTurnOutcome"] === "completed" || v["lastTurnOutcome"] === "failed") && (v["stallNudge"] === void 0 || isStallNudgeState(v["stallNudge"])) && (v["stallSuppressUntilActivityAfter"] === void 0 || typeof v["stallSuppressUntilActivityAfter"] === "number") && (v["lastTurnMentions"] === void 0 || Array.isArray(v["lastTurnMentions"]) && v["lastTurnMentions"].every((m) => typeof m === "string")) && (v["lastTurnMentionsAt"] === void 0 || typeof v["lastTurnMentionsAt"] === "number") && (v["mode"] === void 0 || v["mode"] === "comment") && (v["doneDeclared"] === void 0 || typeof v["doneDeclared"] === "boolean");
129993
130200
  }
129994
130201
  var TaskHandleStore = class _TaskHandleStore {
129995
130202
  #filePath;
@@ -130184,6 +130391,9 @@ var TaskHandleStore = class _TaskHandleStore {
130184
130391
  await this.update(input.threadId, (existing) => {
130185
130392
  if (existing && existing.taskGuid === input.taskGuid) {
130186
130393
  claimed = true;
130394
+ if (input.mode === "comment" && existing.mode === void 0) {
130395
+ return { ...existing, mode: "comment" };
130396
+ }
130187
130397
  return existing;
130188
130398
  }
130189
130399
  if (input.onlyIfThreadUnclaimed && existing !== void 0) {
@@ -130197,7 +130407,13 @@ var TaskHandleStore = class _TaskHandleStore {
130197
130407
  }
130198
130408
  }
130199
130409
  claimed = true;
130200
- return { threadId: input.threadId, taskGuid: input.taskGuid, chatId: input.chatId, claimedTs: Date.now() };
130410
+ return {
130411
+ threadId: input.threadId,
130412
+ taskGuid: input.taskGuid,
130413
+ chatId: input.chatId,
130414
+ claimedTs: Date.now(),
130415
+ ...input.mode ? { mode: input.mode } : {}
130416
+ };
130201
130417
  });
130202
130418
  return { claimed, reason };
130203
130419
  }
@@ -130293,7 +130509,7 @@ function isTaskRequestTimeoutError(err) {
130293
130509
  if (err instanceof TaskApiError && err.cause instanceof TaskRequestTimeoutError) return true;
130294
130510
  return false;
130295
130511
  }
130296
- function withTimeout2(promise, ms, label) {
130512
+ function withTimeout3(promise, ms, label) {
130297
130513
  let timer;
130298
130514
  const timeout = new Promise((_, reject) => {
130299
130515
  timer = setTimeout(() => reject(new TaskRequestTimeoutError(`${label} timed out after ${ms}ms`)), ms);
@@ -130310,7 +130526,7 @@ var TaskListClient = class {
130310
130526
  }
130311
130527
  /** Single choke point for every outbound call — applies the timeout race uniformly. */
130312
130528
  #request(config, label) {
130313
- return withTimeout2(this.#requester.request(config), this.#timeoutMs, label);
130529
+ return withTimeout3(this.#requester.request(config), this.#timeoutMs, label);
130314
130530
  }
130315
130531
  async getTask(taskGuid) {
130316
130532
  let resp;
@@ -130652,6 +130868,46 @@ async function applyTaskHandleWriteback(patch, deps) {
130652
130868
  return;
130653
130869
  }
130654
130870
  const isCompleted = !!task.completedAt && task.completedAt !== "0";
130871
+ if (record.mode === "comment") {
130872
+ switch (patch.status) {
130873
+ case "received": {
130874
+ if (record.doneDeclared) {
130875
+ await deps.store.update(
130876
+ patch.threadId,
130877
+ (current) => current ? { ...current, doneDeclared: void 0 } : current
130878
+ );
130879
+ }
130880
+ break;
130881
+ }
130882
+ case "completed": {
130883
+ const mentionedPeerBotIds = patch.mentionedPeerBotIds && patch.mentionedPeerBotIds.length > 0 ? patch.mentionedPeerBotIds : void 0;
130884
+ const mentionAnchorMs = patch.turnReceivedAt ?? Date.now();
130885
+ await deps.store.update(
130886
+ patch.threadId,
130887
+ (current) => current ? {
130888
+ ...current,
130889
+ lastTurnOutcome: "completed",
130890
+ lastTurnMentions: mentionedPeerBotIds,
130891
+ lastTurnMentionsAt: mentionedPeerBotIds ? mentionAnchorMs : void 0,
130892
+ // Agent declared delivery — its own "已交付" comment is the
130893
+ // user-facing artifact; bridge just stops patrolling the
130894
+ // "delivered, human hasn't ticked complete yet" window.
130895
+ ...patch.agentDeclaredDone === true ? { doneDeclared: true } : {}
130896
+ } : current
130897
+ );
130898
+ break;
130899
+ }
130900
+ case "failed": {
130901
+ await deps.store.update(
130902
+ patch.threadId,
130903
+ (current) => current ? { ...current, lastTurnOutcome: "failed", lastTurnMentions: void 0, lastTurnMentionsAt: void 0 } : current
130904
+ );
130905
+ await deps.client.addComment(record.taskGuid, renderFailureComment(patch.failureReason));
130906
+ break;
130907
+ }
130908
+ }
130909
+ return;
130910
+ }
130655
130911
  switch (patch.status) {
130656
130912
  case "received": {
130657
130913
  if (isCompleted) {
@@ -131413,6 +131669,7 @@ var StallDetector = class {
131413
131669
  async #pollOne(threadId) {
131414
131670
  let record = this.#deps.store.get(threadId);
131415
131671
  if (!record) return;
131672
+ if (record.doneDeclared) return;
131416
131673
  const lastActiveTs = this.#deps.getLastActiveTs(threadId) ?? record.claimedTs;
131417
131674
  const now = Date.now();
131418
131675
  if (record.stallSuppressUntilActivityAfter !== void 0 && lastActiveTs <= record.stallSuppressUntilActivityAfter) {
@@ -131659,6 +131916,7 @@ var StallDetector = class {
131659
131916
  }
131660
131917
  } : r
131661
131918
  );
131919
+ if (record.mode === "comment") return;
131662
131920
  const logSummary = `\u505C\u6EDE\u5524\u9192 #${prospectiveCount}(${this.#nudgeReasonLabel(reason, dueMs)})`;
131663
131921
  const nextDescription = mergeDescriptionSnapshot(task.description, {
131664
131922
  status: "in_progress",
@@ -131917,12 +132175,18 @@ async function runV2Mode({
131917
132175
  let taskHandleLifecycle;
131918
132176
  let taskHandleClaim;
131919
132177
  let taskHandleClaimedLookup;
132178
+ let taskHandleClaimGuidLookup;
131920
132179
  let taskHandleCandidatesLookup;
131921
132180
  let taskCommentPoller;
131922
132181
  let stallDetector;
131923
132182
  {
131924
132183
  const guid = bot.taskHandle?.tasklistGuid ?? await readTeamTasklistGuid(resolveTaskTeamRegistryPath());
131925
- if (guid) {
132184
+ if (!guid) {
132185
+ console.log(
132186
+ `[larkway] bot "${bot.id}": \u4EFB\u52A1\u6D3E\u5355\u4E3B\u8DEF\u5F84\u5DF2\u5C31\u7EEA(\u5EFA\u4EFB\u52A1\u2192\u53D1\u9001\u5230\u7FA4\u2192@ \u672C bot,\u96F6\u914D\u7F6E)\u3002\u8BDD\u9898\u8F6C\u4EFB\u52A1\u8F85\u8DEF\u5F84\u7684\u5171\u4EAB\u6E05\u5355\u672A\u914D\u7F6E(taskHandle.tasklistGuid \u7F3A\u5931,\u5171\u4EAB\u6CE8\u518C\u6587\u4EF6\u91CC\u4E5F\u6CA1\u6709);\u60F3\u542F\u7528:\u8FD0\u884C larkway tasklist-init(\u89C1 docs/task-handle.md \xA77/\xA715)\u3002`
132187
+ );
132188
+ }
132189
+ {
131926
132190
  const taskSdkClient = new import_node_sdk2.Client({
131927
132191
  appId: bot.app_id,
131928
132192
  appSecret,
@@ -131932,12 +132196,14 @@ async function runV2Mode({
131932
132196
  request: (config2) => taskSdkClient.request(config2)
131933
132197
  };
131934
132198
  const taskListClient = new TaskListClient(taskRequester);
131935
- await taskListClient.addTasklistMembers(guid, [{ id: bot.app_id, type: "app", role: "editor" }]).catch((err) => {
131936
- console.warn(
131937
- `[larkway] bot "${bot.id}": self-join tasklist ${guid} as editor failed (continuing, best-effort): ${compactErrorText(err)}`
131938
- );
131939
- });
131940
- effectiveTaskHandleTasklistGuid = guid;
132199
+ if (guid) {
132200
+ await taskListClient.addTasklistMembers(guid, [{ id: bot.app_id, type: "app", role: "editor" }]).catch((err) => {
132201
+ console.warn(
132202
+ `[larkway] bot "${bot.id}": self-join tasklist ${guid} as editor failed (continuing, best-effort): ${compactErrorText(err)}`
132203
+ );
132204
+ });
132205
+ effectiveTaskHandleTasklistGuid = guid;
132206
+ }
131941
132207
  const taskHandleStore = await TaskHandleStore.load(resolveTaskHandlesPath(bot.id));
131942
132208
  taskHandleLifecycle = (patch) => applyTaskHandleWriteback(patch, { store: taskHandleStore, client: taskListClient });
131943
132209
  taskHandleClaim = async (patch) => {
@@ -131949,12 +132215,13 @@ async function runV2Mode({
131949
132215
  }
131950
132216
  };
131951
132217
  taskHandleClaimedLookup = (threadId) => taskHandleStore.get(threadId) !== void 0;
131952
- {
132218
+ taskHandleClaimGuidLookup = (threadId) => taskHandleStore.get(threadId)?.taskGuid;
132219
+ if (guid) {
131953
132220
  const group = tasklistGuidGroups.get(guid) ?? { client: taskListClient, clientOwnerBotId: bot.id, bots: [] };
131954
132221
  group.bots.push({ botId: bot.id, sessionStore, taskHandleStore });
131955
132222
  tasklistGuidGroups.set(guid, group);
132223
+ taskHandleCandidatesLookup = () => tasklistPollersByGuid.get(guid)?.getCandidates() ?? [];
131956
132224
  }
131957
- taskHandleCandidatesLookup = () => tasklistPollersByGuid.get(guid)?.getCandidates() ?? [];
131958
132225
  taskCommentPoller = new CommentPoller({
131959
132226
  store: taskHandleStore,
131960
132227
  client: taskListClient,
@@ -132017,7 +132284,7 @@ async function runV2Mode({
132017
132284
  // taskHandleCandidatesLookup above (the poller for `guid` is
132018
132285
  // constructed AFTER this whole per-bot loop finishes, but this
132019
132286
  // closure is only ever INVOKED later, at poll time).
132020
- getTaskDueMs: (taskGuid) => tasklistPollersByGuid.get(guid)?.getDueTimestamp(taskGuid),
132287
+ getTaskDueMs: (taskGuid) => guid ? tasklistPollersByGuid.get(guid)?.getDueTimestamp(taskGuid) : void 0,
132021
132288
  // Round-2 adversarial review fix: THIS bot's own receipt signal
132022
132289
  // (not a peer's) — disambiguates a pending-nudge confirmation
132023
132290
  // timeout from a nudge turn merely queued behind the semaphore.
@@ -132066,10 +132333,6 @@ async function runV2Mode({
132066
132333
  );
132067
132334
  stallDetector.start();
132068
132335
  }
132069
- } else {
132070
- console.log(
132071
- `[larkway] bot "${bot.id}": \u8BDD\u9898\u2194\u4EFB\u52A1\u53E5\u67C4\u672A\u914D\u7F6E(taskHandle.tasklistGuid \u7F3A\u5931,\u5171\u4EAB\u6CE8\u518C\u6587\u4EF6\u91CC\u4E5F\u6CA1\u6709)\u3002\u60F3\u542F\u7528:\u8FD0\u884C larkway tasklist-init(\u6216\u5BF9\u4F60\u7684 agent \u8BF4"\u5E2E\u6211\u914D\u7F6E Agent Team")(\u89C1 docs/task-handle.md \xA77)\u3002`
132072
- );
132073
132336
  }
132074
132337
  }
132075
132338
  let codexPool;
@@ -132140,6 +132403,10 @@ async function runV2Mode({
132140
132403
  cardKitClient,
132141
132404
  cotClient,
132142
132405
  postClient,
132406
+ // v4 任务派单 root-type probe (docs/task-handle.md §15.4) — lazy channel
132407
+ // resolution, best-effort by the client's own contract, no config gate
132408
+ // (the probe only ever fires on quote-reply turns).
132409
+ messageLookup: client.outboundMessageLookupClient(),
132143
132410
  gitlabToken: effectiveGitlabToken,
132144
132411
  agentMemory: bot.agent_memory,
132145
132412
  larkCliProfile,
@@ -132158,6 +132425,7 @@ async function runV2Mode({
132158
132425
  taskHandleLifecycle,
132159
132426
  taskHandleClaim,
132160
132427
  taskHandleClaimedLookup,
132428
+ taskHandleClaimGuidLookup,
132161
132429
  taskHandleCandidatesLookup
132162
132430
  });
132163
132431
  handlersByBotId.set(bot.id, handler);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "larkway",
3
- "version": "0.3.38",
3
+ "version": "0.3.40",
4
4
  "description": "Thin bridge: Feishu thread to local Claude Code CLI",
5
5
  "license": "MIT",
6
6
  "author": "Chuck Wu (chuckwu0)",