larkway 0.3.39 → 0.3.41

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.39**
11
+ **Current release: v0.3.41**
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.39**
11
+ **当前版本:v0.3.41**
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,74 @@ 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
+ function realTopicThreadId(threadId) {
116660
+ return typeof threadId === "string" && threadId.startsWith("omt_") ? threadId : void 0;
116661
+ }
116662
+ var LOOKUP_TIMEOUT_MS = 5e3;
116663
+ var CACHE_MAX = 200;
116664
+ async function withTimeout2(p, ms, label) {
116665
+ let timer;
116666
+ const timeout = new Promise((_resolve, reject) => {
116667
+ timer = setTimeout(() => reject(new Error(`[channel.msglookup] ${label} timed out after ${ms}ms`)), ms);
116668
+ timer.unref?.();
116669
+ });
116670
+ try {
116671
+ return await Promise.race([p, timeout]);
116672
+ } finally {
116673
+ if (timer) clearTimeout(timer);
116674
+ }
116675
+ }
116676
+ var ChannelMessageLookupClient = class {
116677
+ #resolveChannel;
116678
+ #cache = /* @__PURE__ */ new Map();
116679
+ constructor(opts) {
116680
+ this.#resolveChannel = opts.resolveChannel;
116681
+ }
116682
+ async get(messageId, opts) {
116683
+ if (!opts?.refresh) {
116684
+ const cached = this.#cache.get(messageId);
116685
+ if (cached) return cached;
116686
+ }
116687
+ try {
116688
+ const channel = this.#resolveChannel();
116689
+ if (!channel) return void 0;
116690
+ const req = {
116691
+ url: `/open-apis/im/v1/messages/${encodeURIComponent(messageId)}`,
116692
+ method: "GET"
116693
+ };
116694
+ const res = await withTimeout2(
116695
+ channel.rawClient.request(req),
116696
+ LOOKUP_TIMEOUT_MS,
116697
+ `GET message ${messageId}`
116698
+ );
116699
+ if (res.code !== void 0 && res.code !== 0) return void 0;
116700
+ const items = res.data?.["items"];
116701
+ if (!Array.isArray(items) || items.length === 0) return void 0;
116702
+ const item = items[0];
116703
+ const body = item["body"];
116704
+ const info = {
116705
+ msgType: typeof item["msg_type"] === "string" ? item["msg_type"] : void 0,
116706
+ content: body && typeof body === "object" && typeof body["content"] === "string" ? body["content"] : void 0,
116707
+ threadId: typeof item["thread_id"] === "string" && item["thread_id"] ? item["thread_id"] : void 0
116708
+ };
116709
+ if (this.#cache.size >= CACHE_MAX) {
116710
+ const oldest = this.#cache.keys().next().value;
116711
+ if (oldest !== void 0) this.#cache.delete(oldest);
116712
+ }
116713
+ this.#cache.set(messageId, info);
116714
+ return info;
116715
+ } catch {
116716
+ return void 0;
116717
+ }
116718
+ }
116719
+ };
116720
+
116653
116721
  // src/lark/channelClient.ts
116654
116722
  var execFile = promisify(execFileCallback);
116655
116723
  var LEARNED_CHATS_LIMIT = 100;
@@ -116947,6 +117015,8 @@ var ChannelClient = class {
116947
117015
  postClient = null;
116948
117016
  /** Lazily built COT (思维链) client; only requested when a bot enables cot != off. */
116949
117017
  cotClient = null;
117018
+ /** Lazily built single-message lookup client (v4 任务派单 root-type probe). */
117019
+ messageLookupClient = null;
116950
117020
  constructor(opts) {
116951
117021
  if (!opts.appId || !opts.appSecret) {
116952
117022
  throw new Error(
@@ -117230,6 +117300,20 @@ var ChannelClient = class {
117230
117300
  }
117231
117301
  return this.cotClient;
117232
117302
  }
117303
+ /**
117304
+ * Return a MessageLookupClient bound to this client's channel handle
117305
+ * (docs/task-handle.md §15.4 — the v4 任务派单 root-type probe). Same lazy
117306
+ * channel resolution and generic rawClient.request() auth path as the COT
117307
+ * client above; strictly best-effort per the client's own contract.
117308
+ */
117309
+ outboundMessageLookupClient() {
117310
+ if (!this.messageLookupClient) {
117311
+ this.messageLookupClient = new ChannelMessageLookupClient({
117312
+ resolveChannel: () => this.channel
117313
+ });
117314
+ }
117315
+ return this.messageLookupClient;
117316
+ }
117233
117317
  /**
117234
117318
  * Return an OutboundPostClient bound to this client's channel handle.
117235
117319
  *
@@ -118656,6 +118740,17 @@ function extractPostText(value) {
118656
118740
  }
118657
118741
  return "";
118658
118742
  }
118743
+ function parseTodoShareContent(rawContent) {
118744
+ try {
118745
+ const parsed = JSON.parse(rawContent);
118746
+ const taskGuid = parsed["task_id"];
118747
+ if (typeof taskGuid !== "string" || taskGuid.length === 0) return null;
118748
+ const summaryText = extractPostText(parsed["summary"]).trim();
118749
+ return { taskGuid, summaryText };
118750
+ } catch {
118751
+ return null;
118752
+ }
118753
+ }
118659
118754
  function extractText(event) {
118660
118755
  let parsed;
118661
118756
  try {
@@ -118667,6 +118762,10 @@ function extractText(event) {
118667
118762
  return stripAtPlaceholders(event.content);
118668
118763
  }
118669
118764
  let raw = "";
118765
+ const todo = typeof parsed["task_id"] === "string" ? parseTodoShareContent(event.content) : null;
118766
+ if (todo) {
118767
+ return `[\u98DE\u4E66\u4EFB\u52A1] ${todo.summaryText || "(\u65E0\u6807\u9898)"} (task_guid=${todo.taskGuid})`;
118768
+ }
118670
118769
  if (typeof parsed["text"] === "string") {
118671
118770
  raw = parsed["text"];
118672
118771
  } else if (parsed["content"] !== void 0) {
@@ -119114,6 +119213,27 @@ function renderTaskHandleBlock(tasklistGuid, claimed, candidates) {
119114
119213
  "</task-handle>"
119115
119214
  ];
119116
119215
  }
119216
+ function renderTaskRootBlock(taskRoot) {
119217
+ const lines = [
119218
+ "<task-root>",
119219
+ "\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",
119220
+ `task_guid: ${taskRoot.guid}`,
119221
+ `task_summary: ${taskRoot.summary || "(\u65E0\u6807\u9898)"}`,
119222
+ ...taskRoot.topicLink ? [`topic_link: ${taskRoot.topicLink}`] : [],
119223
+ `task_root_claimed: ${taskRoot.claimed ? "yes" : "no"}`
119224
+ ];
119225
+ if (taskRoot.claimed) {
119226
+ lines.push(
119227
+ "\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"
119228
+ );
119229
+ } else {
119230
+ lines.push(
119231
+ `\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`
119232
+ );
119233
+ }
119234
+ lines.push("</task-root>");
119235
+ return lines;
119236
+ }
119117
119237
  function renderRuntimeWarningsBlock(warnings) {
119118
119238
  if (warnings.length === 0) return [];
119119
119239
  const hasMissingLarkCli = warnings.some((warning) => warning.command === "lark-cli");
@@ -119267,6 +119387,7 @@ async function renderPrompt(input) {
119267
119387
  taskHandleTasklistGuid,
119268
119388
  taskHandleClaimed = false,
119269
119389
  taskHandleCandidates = [],
119390
+ taskRoot,
119270
119391
  mtimeFacts = []
119271
119392
  } = input;
119272
119393
  const backend = input.backend ?? "claude";
@@ -119281,7 +119402,8 @@ async function renderPrompt(input) {
119281
119402
  const stateContract = renderStateContract(conventions.stateFilePath);
119282
119403
  const peersBlock = peers && peers.length > 0 ? renderPeersBlock(peers) : [];
119283
119404
  const turnTakingBlock = turn_taking_limit && turn_taking_limit > 0 ? renderTurnTakingHint(turn_taking_limit) : [];
119284
- const taskHandleBlock = taskHandleTasklistGuid ? renderTaskHandleBlock(taskHandleTasklistGuid, taskHandleClaimed, taskHandleCandidates) : [];
119405
+ const taskHandleBlock = taskHandleTasklistGuid && !taskRoot ? renderTaskHandleBlock(taskHandleTasklistGuid, taskHandleClaimed, taskHandleCandidates) : [];
119406
+ const taskRootBlock = taskRoot ? renderTaskRootBlock(taskRoot) : [];
119285
119407
  const runtimeWarningsBlock = renderRuntimeWarningsBlock(runtimeWarnings);
119286
119408
  const extraRepos = extraRepoPaths ?? conventions.extraRepoPaths ?? [];
119287
119409
  const workspaceBlock = isAgentWorkspace ? await renderAgentWorkspaceBlock(conventions, extraRepos, isNewThread, mtimeFacts) : hasRepo ? renderWorkspaceBlock(
@@ -119398,6 +119520,7 @@ async function renderPrompt(input) {
119398
119520
  ...peersBlock.length > 0 ? ["", ...peersBlock] : [],
119399
119521
  ...turnTakingBlock.length > 0 ? ["", ...turnTakingBlock] : [],
119400
119522
  ...taskHandleBlock.length > 0 ? ["", ...taskHandleBlock] : [],
119523
+ ...taskRootBlock.length > 0 ? ["", ...taskRootBlock] : [],
119401
119524
  "",
119402
119525
  "<user-message>",
119403
119526
  `${parsed.senderOpenId}: ${parsed.text}`,
@@ -119454,6 +119577,7 @@ async function renderPrompt(input) {
119454
119577
  ...peersBlock.length > 0 ? ["", ...peersBlock] : [],
119455
119578
  ...turnTakingBlock.length > 0 ? ["", ...turnTakingBlock] : [],
119456
119579
  ...taskHandleBlock.length > 0 ? ["", ...taskHandleBlock] : [],
119580
+ ...taskRootBlock.length > 0 ? ["", ...taskRootBlock] : [],
119457
119581
  "",
119458
119582
  "<user-message>",
119459
119583
  `${parsed.senderOpenId}: ${parsed.text}`,
@@ -122481,7 +122605,35 @@ var BridgeHandler = class {
122481
122605
  const existing = this.deps.sessionStore.get(threadId, botId);
122482
122606
  const isNewThread = existing === void 0;
122483
122607
  const isTopLevel = !(typeof parsed.raw.root_id === "string" && parsed.raw.root_id);
122484
- const replyInThread = isTopLevel;
122608
+ let replyInThread = isTopLevel;
122609
+ let replyAnchorId = messageId;
122610
+ let taskRootInfo;
122611
+ let deferredTaskRootProbe;
122612
+ {
122613
+ const rootId = typeof parsed.raw.root_id === "string" && parsed.raw.root_id ? parsed.raw.root_id : void 0;
122614
+ const rawThreadId = realTopicThreadId(parsed.raw.thread_id);
122615
+ if (rootId && this.deps.messageLookup) {
122616
+ const probe = this.deps.messageLookup.get(rootId).catch(() => void 0);
122617
+ if (rawThreadId) {
122618
+ deferredTaskRootProbe = probe;
122619
+ } else {
122620
+ const info = await probe;
122621
+ if (info?.msgType === "todo" && info.content) {
122622
+ const todo = parseTodoShareContent(info.content);
122623
+ if (todo) {
122624
+ const probeThreadId = realTopicThreadId(info.threadId);
122625
+ taskRootInfo = {
122626
+ guid: todo.taskGuid,
122627
+ summary: todo.summaryText,
122628
+ topicLink: probeThreadId ? buildTopicDeepLink(parsed.chatId, probeThreadId) : void 0
122629
+ };
122630
+ replyInThread = true;
122631
+ replyAnchorId = rootId;
122632
+ }
122633
+ }
122634
+ }
122635
+ }
122636
+ }
122485
122637
  const prototypeConfig = this.deps.botConfig?.response_surface_prototype;
122486
122638
  const cardKitAvailable = isResponseSurfaceCardKitAvailable(
122487
122639
  prototypeConfig,
@@ -122497,7 +122649,7 @@ var BridgeHandler = class {
122497
122649
  let startFailurePostFallbackSent = false;
122498
122650
  if (!cardKitAvailable) {
122499
122651
  try {
122500
- card = await this.deps.cardRenderer.start(messageId, { replyInThread, threadId });
122652
+ card = await this.deps.cardRenderer.start(replyAnchorId, { replyInThread, threadId });
122501
122653
  await recordEvent({
122502
122654
  status: "running",
122503
122655
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -122568,7 +122720,10 @@ var BridgeHandler = class {
122568
122720
  inputPreview: parsed.text,
122569
122721
  target: {
122570
122722
  chatId: parsed.chatId,
122571
- threadId: typeof parsed.raw.thread_id === "string" && parsed.raw.thread_id ? parsed.raw.thread_id : void 0,
122723
+ // omt_* only (see realTopicThreadId): passing a reply-chain om_*
122724
+ // id as receive_id_type=thread_id anchored the COT bubble onto a
122725
+ // stray thread on the TRIGGER message (2026-07-08 dogfood).
122726
+ threadId: realTopicThreadId(parsed.raw.thread_id),
122572
122727
  originMessageId
122573
122728
  }
122574
122729
  });
@@ -122596,7 +122751,7 @@ var BridgeHandler = class {
122596
122751
  try {
122597
122752
  cardKitProgress = await createCardKitProgressHandle({
122598
122753
  cardKitClient: this.deps.cardKitClient,
122599
- replyToMessageId: messageId,
122754
+ replyToMessageId: replyAnchorId,
122600
122755
  replyInThread,
122601
122756
  facts: {
122602
122757
  botId: this.deps.botConfig?.id ?? "v1-default",
@@ -122627,7 +122782,7 @@ var BridgeHandler = class {
122627
122782
  status: "message_sent",
122628
122783
  cardId: cardKitProgress.cardId,
122629
122784
  messageId: cardKitProgress.messageId,
122630
- replyToMessageId: messageId,
122785
+ replyToMessageId: replyAnchorId,
122631
122786
  chatId: parsed.chatId,
122632
122787
  threadId,
122633
122788
  botId: this.deps.botConfig?.id ?? "",
@@ -122672,7 +122827,7 @@ var BridgeHandler = class {
122672
122827
  }
122673
122828
  if (!card && cardKitStartFailed) {
122674
122829
  try {
122675
- card = await this.deps.cardRenderer.start(messageId, { replyInThread, threadId });
122830
+ card = await this.deps.cardRenderer.start(replyAnchorId, { replyInThread, threadId });
122676
122831
  await this.deps.client.removeProcessingReaction?.(messageId);
122677
122832
  await recordEvent({
122678
122833
  status: "running",
@@ -122687,7 +122842,7 @@ var BridgeHandler = class {
122687
122842
  );
122688
122843
  const postFallback = await createOnlyPostFallback({
122689
122844
  postClient: this.deps.postClient,
122690
- replyToMessageId: messageId,
122845
+ replyToMessageId: replyAnchorId,
122691
122846
  replyInThread,
122692
122847
  botId: this.deps.botConfig?.id ?? "v1-default",
122693
122848
  threadId,
@@ -122924,6 +123079,28 @@ var BridgeHandler = class {
122924
123079
  console.warn("[bridge.handler] live-roster resolve failed (using static peers):", err);
122925
123080
  }
122926
123081
  }
123082
+ if (deferredTaskRootProbe) {
123083
+ const info = await deferredTaskRootProbe;
123084
+ if (info?.msgType === "todo" && info.content) {
123085
+ const todo = parseTodoShareContent(info.content);
123086
+ if (todo) {
123087
+ const rawThreadId = realTopicThreadId(parsed.raw.thread_id);
123088
+ const probeThreadId = realTopicThreadId(info.threadId);
123089
+ taskRootInfo = {
123090
+ guid: todo.taskGuid,
123091
+ summary: todo.summaryText,
123092
+ topicLink: rawThreadId ? buildTopicDeepLink(parsed.chatId, rawThreadId) : probeThreadId ? buildTopicDeepLink(parsed.chatId, probeThreadId) : void 0
123093
+ };
123094
+ }
123095
+ }
123096
+ }
123097
+ if (taskRootInfo && !taskRootInfo.topicLink && replyAnchorId !== messageId && this.deps.messageLookup) {
123098
+ const refreshed = await this.deps.messageLookup.get(replyAnchorId, { refresh: true }).catch(() => void 0);
123099
+ const refreshedThreadId = realTopicThreadId(refreshed?.threadId);
123100
+ if (refreshedThreadId) {
123101
+ taskRootInfo.topicLink = buildTopicDeepLink(parsed.chatId, refreshedThreadId);
123102
+ }
123103
+ }
122927
123104
  const prompt = await renderPrompt({
122928
123105
  parsed,
122929
123106
  isNewThread: currentIsNewThread,
@@ -122955,6 +123132,13 @@ var BridgeHandler = class {
122955
123132
  taskHandleTasklistGuid: this.deps.botConfig?.taskHandle?.tasklistGuid,
122956
123133
  taskHandleClaimed: this.deps.taskHandleClaimedLookup?.(threadId) ?? false,
122957
123134
  taskHandleCandidates: this.deps.taskHandleCandidatesLookup?.() ?? [],
123135
+ taskRoot: taskRootInfo ? {
123136
+ ...taskRootInfo,
123137
+ // Exact-guid comparison (see taskHandleClaimGuidLookup's doc);
123138
+ // falls back to the boolean lookup only when the precise one
123139
+ // isn't wired (older embedding code).
123140
+ claimed: this.deps.taskHandleClaimGuidLookup ? this.deps.taskHandleClaimGuidLookup(threadId) === taskRootInfo.guid : this.deps.taskHandleClaimedLookup?.(threadId) ?? false
123141
+ } : void 0,
122958
123142
  mtimeFacts
122959
123143
  });
122960
123144
  const backend = this.deps.botConfig?.backend ?? "claude";
@@ -123096,7 +123280,14 @@ var BridgeHandler = class {
123096
123280
  botId,
123097
123281
  threadId,
123098
123282
  chatId: parsed.chatId,
123099
- taskGuid: claimedTaskGuid
123283
+ taskGuid: claimedTaskGuid,
123284
+ // v4 任务派单 (docs/task-handle.md §15.3): a claim on the very
123285
+ // task this thread's ROOT message shares is comment-mode —
123286
+ // maintenance goes through task comments only (share-to-chat
123287
+ // grants read+comment; no tasklist/editor rights needed, and
123288
+ // completion is ALWAYS ticked by the human). Mechanical
123289
+ // equality check, not a judgment call.
123290
+ mode: taskRootInfo?.guid === claimedTaskGuid ? "comment" : void 0
123100
123291
  });
123101
123292
  } catch (err) {
123102
123293
  console.warn("[bridge.handler] taskHandleClaim hook failed (continuing):", err);
@@ -123241,7 +123432,7 @@ var BridgeHandler = class {
123241
123432
  console.warn("[bridge.handler] CardKit finalize failed; using card fallback:", err);
123242
123433
  cardKitProgress.close();
123243
123434
  try {
123244
- card = await this.deps.cardRenderer.start(messageId, { replyInThread, threadId });
123435
+ card = await this.deps.cardRenderer.start(replyAnchorId, { replyInThread, threadId });
123245
123436
  await writeCardFile(worktreePath, {
123246
123437
  messageId: card.messageId,
123247
123438
  chatId: parsed.chatId,
@@ -123266,7 +123457,7 @@ var BridgeHandler = class {
123266
123457
  } catch (legacyErr) {
123267
123458
  const postFallback = await createOnlyPostFallback({
123268
123459
  postClient: this.deps.postClient,
123269
- replyToMessageId: messageId,
123460
+ replyToMessageId: replyAnchorId,
123270
123461
  replyInThread,
123271
123462
  botId: this.deps.botConfig?.id ?? "v1-default",
123272
123463
  threadId,
@@ -123290,7 +123481,7 @@ var BridgeHandler = class {
123290
123481
  } else {
123291
123482
  if (!card) {
123292
123483
  try {
123293
- card = await this.deps.cardRenderer.start(messageId, { replyInThread, threadId });
123484
+ card = await this.deps.cardRenderer.start(replyAnchorId, { replyInThread, threadId });
123294
123485
  try {
123295
123486
  await writeCardFile(worktreePath, {
123296
123487
  messageId: card.messageId,
@@ -123314,7 +123505,7 @@ var BridgeHandler = class {
123314
123505
  ].filter((part) => !!part).join("; ");
123315
123506
  const postFallback = await createOnlyPostFallback({
123316
123507
  postClient: this.deps.postClient,
123317
- replyToMessageId: messageId,
123508
+ replyToMessageId: replyAnchorId,
123318
123509
  replyInThread,
123319
123510
  botId: this.deps.botConfig?.id ?? "v1-default",
123320
123511
  threadId,
@@ -123395,7 +123586,7 @@ var BridgeHandler = class {
123395
123586
  const createHardFailurePostFallback = async (failureReason) => {
123396
123587
  const fallback = await createOnlyPostFallback({
123397
123588
  postClient: this.deps.postClient,
123398
- replyToMessageId: messageId,
123589
+ replyToMessageId: replyAnchorId,
123399
123590
  replyInThread,
123400
123591
  botId: this.deps.botConfig?.id ?? "v1-default",
123401
123592
  threadId,
@@ -123424,7 +123615,7 @@ var BridgeHandler = class {
123424
123615
  cardKitFinalizeErr
123425
123616
  );
123426
123617
  try {
123427
- card = await this.deps.cardRenderer.start(messageId, { replyInThread, threadId });
123618
+ card = await this.deps.cardRenderer.start(replyAnchorId, { replyInThread, threadId });
123428
123619
  } catch (cardStartErr) {
123429
123620
  console.error("[bridge.handler] failure card start also failed:", cardStartErr);
123430
123621
  await createHardFailurePostFallback(
@@ -130014,7 +130205,7 @@ function isStallNudgeState(value) {
130014
130205
  function isTaskHandleRecord(value) {
130015
130206
  if (typeof value !== "object" || value === null) return false;
130016
130207
  const v = value;
130017
- 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");
130208
+ 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");
130018
130209
  }
130019
130210
  var TaskHandleStore = class _TaskHandleStore {
130020
130211
  #filePath;
@@ -130209,6 +130400,9 @@ var TaskHandleStore = class _TaskHandleStore {
130209
130400
  await this.update(input.threadId, (existing) => {
130210
130401
  if (existing && existing.taskGuid === input.taskGuid) {
130211
130402
  claimed = true;
130403
+ if (input.mode === "comment" && existing.mode === void 0) {
130404
+ return { ...existing, mode: "comment" };
130405
+ }
130212
130406
  return existing;
130213
130407
  }
130214
130408
  if (input.onlyIfThreadUnclaimed && existing !== void 0) {
@@ -130222,7 +130416,13 @@ var TaskHandleStore = class _TaskHandleStore {
130222
130416
  }
130223
130417
  }
130224
130418
  claimed = true;
130225
- return { threadId: input.threadId, taskGuid: input.taskGuid, chatId: input.chatId, claimedTs: Date.now() };
130419
+ return {
130420
+ threadId: input.threadId,
130421
+ taskGuid: input.taskGuid,
130422
+ chatId: input.chatId,
130423
+ claimedTs: Date.now(),
130424
+ ...input.mode ? { mode: input.mode } : {}
130425
+ };
130226
130426
  });
130227
130427
  return { claimed, reason };
130228
130428
  }
@@ -130318,7 +130518,7 @@ function isTaskRequestTimeoutError(err) {
130318
130518
  if (err instanceof TaskApiError && err.cause instanceof TaskRequestTimeoutError) return true;
130319
130519
  return false;
130320
130520
  }
130321
- function withTimeout2(promise, ms, label) {
130521
+ function withTimeout3(promise, ms, label) {
130322
130522
  let timer;
130323
130523
  const timeout = new Promise((_, reject) => {
130324
130524
  timer = setTimeout(() => reject(new TaskRequestTimeoutError(`${label} timed out after ${ms}ms`)), ms);
@@ -130335,7 +130535,7 @@ var TaskListClient = class {
130335
130535
  }
130336
130536
  /** Single choke point for every outbound call — applies the timeout race uniformly. */
130337
130537
  #request(config, label) {
130338
- return withTimeout2(this.#requester.request(config), this.#timeoutMs, label);
130538
+ return withTimeout3(this.#requester.request(config), this.#timeoutMs, label);
130339
130539
  }
130340
130540
  async getTask(taskGuid) {
130341
130541
  let resp;
@@ -130677,6 +130877,46 @@ async function applyTaskHandleWriteback(patch, deps) {
130677
130877
  return;
130678
130878
  }
130679
130879
  const isCompleted = !!task.completedAt && task.completedAt !== "0";
130880
+ if (record.mode === "comment") {
130881
+ switch (patch.status) {
130882
+ case "received": {
130883
+ if (record.doneDeclared) {
130884
+ await deps.store.update(
130885
+ patch.threadId,
130886
+ (current) => current ? { ...current, doneDeclared: void 0 } : current
130887
+ );
130888
+ }
130889
+ break;
130890
+ }
130891
+ case "completed": {
130892
+ const mentionedPeerBotIds = patch.mentionedPeerBotIds && patch.mentionedPeerBotIds.length > 0 ? patch.mentionedPeerBotIds : void 0;
130893
+ const mentionAnchorMs = patch.turnReceivedAt ?? Date.now();
130894
+ await deps.store.update(
130895
+ patch.threadId,
130896
+ (current) => current ? {
130897
+ ...current,
130898
+ lastTurnOutcome: "completed",
130899
+ lastTurnMentions: mentionedPeerBotIds,
130900
+ lastTurnMentionsAt: mentionedPeerBotIds ? mentionAnchorMs : void 0,
130901
+ // Agent declared delivery — its own "已交付" comment is the
130902
+ // user-facing artifact; bridge just stops patrolling the
130903
+ // "delivered, human hasn't ticked complete yet" window.
130904
+ ...patch.agentDeclaredDone === true ? { doneDeclared: true } : {}
130905
+ } : current
130906
+ );
130907
+ break;
130908
+ }
130909
+ case "failed": {
130910
+ await deps.store.update(
130911
+ patch.threadId,
130912
+ (current) => current ? { ...current, lastTurnOutcome: "failed", lastTurnMentions: void 0, lastTurnMentionsAt: void 0 } : current
130913
+ );
130914
+ await deps.client.addComment(record.taskGuid, renderFailureComment(patch.failureReason));
130915
+ break;
130916
+ }
130917
+ }
130918
+ return;
130919
+ }
130680
130920
  switch (patch.status) {
130681
130921
  case "received": {
130682
130922
  if (isCompleted) {
@@ -131438,6 +131678,7 @@ var StallDetector = class {
131438
131678
  async #pollOne(threadId) {
131439
131679
  let record = this.#deps.store.get(threadId);
131440
131680
  if (!record) return;
131681
+ if (record.doneDeclared) return;
131441
131682
  const lastActiveTs = this.#deps.getLastActiveTs(threadId) ?? record.claimedTs;
131442
131683
  const now = Date.now();
131443
131684
  if (record.stallSuppressUntilActivityAfter !== void 0 && lastActiveTs <= record.stallSuppressUntilActivityAfter) {
@@ -131684,6 +131925,7 @@ var StallDetector = class {
131684
131925
  }
131685
131926
  } : r
131686
131927
  );
131928
+ if (record.mode === "comment") return;
131687
131929
  const logSummary = `\u505C\u6EDE\u5524\u9192 #${prospectiveCount}(${this.#nudgeReasonLabel(reason, dueMs)})`;
131688
131930
  const nextDescription = mergeDescriptionSnapshot(task.description, {
131689
131931
  status: "in_progress",
@@ -131942,12 +132184,18 @@ async function runV2Mode({
131942
132184
  let taskHandleLifecycle;
131943
132185
  let taskHandleClaim;
131944
132186
  let taskHandleClaimedLookup;
132187
+ let taskHandleClaimGuidLookup;
131945
132188
  let taskHandleCandidatesLookup;
131946
132189
  let taskCommentPoller;
131947
132190
  let stallDetector;
131948
132191
  {
131949
132192
  const guid = bot.taskHandle?.tasklistGuid ?? await readTeamTasklistGuid(resolveTaskTeamRegistryPath());
131950
- if (guid) {
132193
+ if (!guid) {
132194
+ console.log(
132195
+ `[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`
132196
+ );
132197
+ }
132198
+ {
131951
132199
  const taskSdkClient = new import_node_sdk2.Client({
131952
132200
  appId: bot.app_id,
131953
132201
  appSecret,
@@ -131957,12 +132205,14 @@ async function runV2Mode({
131957
132205
  request: (config2) => taskSdkClient.request(config2)
131958
132206
  };
131959
132207
  const taskListClient = new TaskListClient(taskRequester);
131960
- await taskListClient.addTasklistMembers(guid, [{ id: bot.app_id, type: "app", role: "editor" }]).catch((err) => {
131961
- console.warn(
131962
- `[larkway] bot "${bot.id}": self-join tasklist ${guid} as editor failed (continuing, best-effort): ${compactErrorText(err)}`
131963
- );
131964
- });
131965
- effectiveTaskHandleTasklistGuid = guid;
132208
+ if (guid) {
132209
+ await taskListClient.addTasklistMembers(guid, [{ id: bot.app_id, type: "app", role: "editor" }]).catch((err) => {
132210
+ console.warn(
132211
+ `[larkway] bot "${bot.id}": self-join tasklist ${guid} as editor failed (continuing, best-effort): ${compactErrorText(err)}`
132212
+ );
132213
+ });
132214
+ effectiveTaskHandleTasklistGuid = guid;
132215
+ }
131966
132216
  const taskHandleStore = await TaskHandleStore.load(resolveTaskHandlesPath(bot.id));
131967
132217
  taskHandleLifecycle = (patch) => applyTaskHandleWriteback(patch, { store: taskHandleStore, client: taskListClient });
131968
132218
  taskHandleClaim = async (patch) => {
@@ -131974,12 +132224,13 @@ async function runV2Mode({
131974
132224
  }
131975
132225
  };
131976
132226
  taskHandleClaimedLookup = (threadId) => taskHandleStore.get(threadId) !== void 0;
131977
- {
132227
+ taskHandleClaimGuidLookup = (threadId) => taskHandleStore.get(threadId)?.taskGuid;
132228
+ if (guid) {
131978
132229
  const group = tasklistGuidGroups.get(guid) ?? { client: taskListClient, clientOwnerBotId: bot.id, bots: [] };
131979
132230
  group.bots.push({ botId: bot.id, sessionStore, taskHandleStore });
131980
132231
  tasklistGuidGroups.set(guid, group);
132232
+ taskHandleCandidatesLookup = () => tasklistPollersByGuid.get(guid)?.getCandidates() ?? [];
131981
132233
  }
131982
- taskHandleCandidatesLookup = () => tasklistPollersByGuid.get(guid)?.getCandidates() ?? [];
131983
132234
  taskCommentPoller = new CommentPoller({
131984
132235
  store: taskHandleStore,
131985
132236
  client: taskListClient,
@@ -132042,7 +132293,7 @@ async function runV2Mode({
132042
132293
  // taskHandleCandidatesLookup above (the poller for `guid` is
132043
132294
  // constructed AFTER this whole per-bot loop finishes, but this
132044
132295
  // closure is only ever INVOKED later, at poll time).
132045
- getTaskDueMs: (taskGuid) => tasklistPollersByGuid.get(guid)?.getDueTimestamp(taskGuid),
132296
+ getTaskDueMs: (taskGuid) => guid ? tasklistPollersByGuid.get(guid)?.getDueTimestamp(taskGuid) : void 0,
132046
132297
  // Round-2 adversarial review fix: THIS bot's own receipt signal
132047
132298
  // (not a peer's) — disambiguates a pending-nudge confirmation
132048
132299
  // timeout from a nudge turn merely queued behind the semaphore.
@@ -132091,10 +132342,6 @@ async function runV2Mode({
132091
132342
  );
132092
132343
  stallDetector.start();
132093
132344
  }
132094
- } else {
132095
- console.log(
132096
- `[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`
132097
- );
132098
132345
  }
132099
132346
  }
132100
132347
  let codexPool;
@@ -132165,6 +132412,10 @@ async function runV2Mode({
132165
132412
  cardKitClient,
132166
132413
  cotClient,
132167
132414
  postClient,
132415
+ // v4 任务派单 root-type probe (docs/task-handle.md §15.4) — lazy channel
132416
+ // resolution, best-effort by the client's own contract, no config gate
132417
+ // (the probe only ever fires on quote-reply turns).
132418
+ messageLookup: client.outboundMessageLookupClient(),
132168
132419
  gitlabToken: effectiveGitlabToken,
132169
132420
  agentMemory: bot.agent_memory,
132170
132421
  larkCliProfile,
@@ -132183,6 +132434,7 @@ async function runV2Mode({
132183
132434
  taskHandleLifecycle,
132184
132435
  taskHandleClaim,
132185
132436
  taskHandleClaimedLookup,
132437
+ taskHandleClaimGuidLookup,
132186
132438
  taskHandleCandidatesLookup
132187
132439
  });
132188
132440
  handlersByBotId.set(bot.id, handler);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "larkway",
3
- "version": "0.3.39",
3
+ "version": "0.3.41",
4
4
  "description": "Thin bridge: Feishu thread to local Claude Code CLI",
5
5
  "license": "MIT",
6
6
  "author": "Chuck Wu (chuckwu0)",