larkway 0.3.39 → 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.39**
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.39**
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
  *
@@ -118656,6 +118737,17 @@ function extractPostText(value) {
118656
118737
  }
118657
118738
  return "";
118658
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
+ }
118659
118751
  function extractText(event) {
118660
118752
  let parsed;
118661
118753
  try {
@@ -118667,6 +118759,10 @@ function extractText(event) {
118667
118759
  return stripAtPlaceholders(event.content);
118668
118760
  }
118669
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
+ }
118670
118766
  if (typeof parsed["text"] === "string") {
118671
118767
  raw = parsed["text"];
118672
118768
  } else if (parsed["content"] !== void 0) {
@@ -119114,6 +119210,27 @@ function renderTaskHandleBlock(tasklistGuid, claimed, candidates) {
119114
119210
  "</task-handle>"
119115
119211
  ];
119116
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
+ }
119117
119234
  function renderRuntimeWarningsBlock(warnings) {
119118
119235
  if (warnings.length === 0) return [];
119119
119236
  const hasMissingLarkCli = warnings.some((warning) => warning.command === "lark-cli");
@@ -119267,6 +119384,7 @@ async function renderPrompt(input) {
119267
119384
  taskHandleTasklistGuid,
119268
119385
  taskHandleClaimed = false,
119269
119386
  taskHandleCandidates = [],
119387
+ taskRoot,
119270
119388
  mtimeFacts = []
119271
119389
  } = input;
119272
119390
  const backend = input.backend ?? "claude";
@@ -119281,7 +119399,8 @@ async function renderPrompt(input) {
119281
119399
  const stateContract = renderStateContract(conventions.stateFilePath);
119282
119400
  const peersBlock = peers && peers.length > 0 ? renderPeersBlock(peers) : [];
119283
119401
  const turnTakingBlock = turn_taking_limit && turn_taking_limit > 0 ? renderTurnTakingHint(turn_taking_limit) : [];
119284
- const taskHandleBlock = taskHandleTasklistGuid ? renderTaskHandleBlock(taskHandleTasklistGuid, taskHandleClaimed, taskHandleCandidates) : [];
119402
+ const taskHandleBlock = taskHandleTasklistGuid && !taskRoot ? renderTaskHandleBlock(taskHandleTasklistGuid, taskHandleClaimed, taskHandleCandidates) : [];
119403
+ const taskRootBlock = taskRoot ? renderTaskRootBlock(taskRoot) : [];
119285
119404
  const runtimeWarningsBlock = renderRuntimeWarningsBlock(runtimeWarnings);
119286
119405
  const extraRepos = extraRepoPaths ?? conventions.extraRepoPaths ?? [];
119287
119406
  const workspaceBlock = isAgentWorkspace ? await renderAgentWorkspaceBlock(conventions, extraRepos, isNewThread, mtimeFacts) : hasRepo ? renderWorkspaceBlock(
@@ -119398,6 +119517,7 @@ async function renderPrompt(input) {
119398
119517
  ...peersBlock.length > 0 ? ["", ...peersBlock] : [],
119399
119518
  ...turnTakingBlock.length > 0 ? ["", ...turnTakingBlock] : [],
119400
119519
  ...taskHandleBlock.length > 0 ? ["", ...taskHandleBlock] : [],
119520
+ ...taskRootBlock.length > 0 ? ["", ...taskRootBlock] : [],
119401
119521
  "",
119402
119522
  "<user-message>",
119403
119523
  `${parsed.senderOpenId}: ${parsed.text}`,
@@ -119454,6 +119574,7 @@ async function renderPrompt(input) {
119454
119574
  ...peersBlock.length > 0 ? ["", ...peersBlock] : [],
119455
119575
  ...turnTakingBlock.length > 0 ? ["", ...turnTakingBlock] : [],
119456
119576
  ...taskHandleBlock.length > 0 ? ["", ...taskHandleBlock] : [],
119577
+ ...taskRootBlock.length > 0 ? ["", ...taskRootBlock] : [],
119457
119578
  "",
119458
119579
  "<user-message>",
119459
119580
  `${parsed.senderOpenId}: ${parsed.text}`,
@@ -122481,7 +122602,34 @@ var BridgeHandler = class {
122481
122602
  const existing = this.deps.sessionStore.get(threadId, botId);
122482
122603
  const isNewThread = existing === void 0;
122483
122604
  const isTopLevel = !(typeof parsed.raw.root_id === "string" && parsed.raw.root_id);
122484
- 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
+ }
122485
122633
  const prototypeConfig = this.deps.botConfig?.response_surface_prototype;
122486
122634
  const cardKitAvailable = isResponseSurfaceCardKitAvailable(
122487
122635
  prototypeConfig,
@@ -122497,7 +122645,7 @@ var BridgeHandler = class {
122497
122645
  let startFailurePostFallbackSent = false;
122498
122646
  if (!cardKitAvailable) {
122499
122647
  try {
122500
- card = await this.deps.cardRenderer.start(messageId, { replyInThread, threadId });
122648
+ card = await this.deps.cardRenderer.start(replyAnchorId, { replyInThread, threadId });
122501
122649
  await recordEvent({
122502
122650
  status: "running",
122503
122651
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -122596,7 +122744,7 @@ var BridgeHandler = class {
122596
122744
  try {
122597
122745
  cardKitProgress = await createCardKitProgressHandle({
122598
122746
  cardKitClient: this.deps.cardKitClient,
122599
- replyToMessageId: messageId,
122747
+ replyToMessageId: replyAnchorId,
122600
122748
  replyInThread,
122601
122749
  facts: {
122602
122750
  botId: this.deps.botConfig?.id ?? "v1-default",
@@ -122627,7 +122775,7 @@ var BridgeHandler = class {
122627
122775
  status: "message_sent",
122628
122776
  cardId: cardKitProgress.cardId,
122629
122777
  messageId: cardKitProgress.messageId,
122630
- replyToMessageId: messageId,
122778
+ replyToMessageId: replyAnchorId,
122631
122779
  chatId: parsed.chatId,
122632
122780
  threadId,
122633
122781
  botId: this.deps.botConfig?.id ?? "",
@@ -122672,7 +122820,7 @@ var BridgeHandler = class {
122672
122820
  }
122673
122821
  if (!card && cardKitStartFailed) {
122674
122822
  try {
122675
- card = await this.deps.cardRenderer.start(messageId, { replyInThread, threadId });
122823
+ card = await this.deps.cardRenderer.start(replyAnchorId, { replyInThread, threadId });
122676
122824
  await this.deps.client.removeProcessingReaction?.(messageId);
122677
122825
  await recordEvent({
122678
122826
  status: "running",
@@ -122687,7 +122835,7 @@ var BridgeHandler = class {
122687
122835
  );
122688
122836
  const postFallback = await createOnlyPostFallback({
122689
122837
  postClient: this.deps.postClient,
122690
- replyToMessageId: messageId,
122838
+ replyToMessageId: replyAnchorId,
122691
122839
  replyInThread,
122692
122840
  botId: this.deps.botConfig?.id ?? "v1-default",
122693
122841
  threadId,
@@ -122924,6 +123072,26 @@ var BridgeHandler = class {
122924
123072
  console.warn("[bridge.handler] live-roster resolve failed (using static peers):", err);
122925
123073
  }
122926
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
+ }
122927
123095
  const prompt = await renderPrompt({
122928
123096
  parsed,
122929
123097
  isNewThread: currentIsNewThread,
@@ -122955,6 +123123,13 @@ var BridgeHandler = class {
122955
123123
  taskHandleTasklistGuid: this.deps.botConfig?.taskHandle?.tasklistGuid,
122956
123124
  taskHandleClaimed: this.deps.taskHandleClaimedLookup?.(threadId) ?? false,
122957
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,
122958
123133
  mtimeFacts
122959
123134
  });
122960
123135
  const backend = this.deps.botConfig?.backend ?? "claude";
@@ -123096,7 +123271,14 @@ var BridgeHandler = class {
123096
123271
  botId,
123097
123272
  threadId,
123098
123273
  chatId: parsed.chatId,
123099
- 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
123100
123282
  });
123101
123283
  } catch (err) {
123102
123284
  console.warn("[bridge.handler] taskHandleClaim hook failed (continuing):", err);
@@ -123241,7 +123423,7 @@ var BridgeHandler = class {
123241
123423
  console.warn("[bridge.handler] CardKit finalize failed; using card fallback:", err);
123242
123424
  cardKitProgress.close();
123243
123425
  try {
123244
- card = await this.deps.cardRenderer.start(messageId, { replyInThread, threadId });
123426
+ card = await this.deps.cardRenderer.start(replyAnchorId, { replyInThread, threadId });
123245
123427
  await writeCardFile(worktreePath, {
123246
123428
  messageId: card.messageId,
123247
123429
  chatId: parsed.chatId,
@@ -123266,7 +123448,7 @@ var BridgeHandler = class {
123266
123448
  } catch (legacyErr) {
123267
123449
  const postFallback = await createOnlyPostFallback({
123268
123450
  postClient: this.deps.postClient,
123269
- replyToMessageId: messageId,
123451
+ replyToMessageId: replyAnchorId,
123270
123452
  replyInThread,
123271
123453
  botId: this.deps.botConfig?.id ?? "v1-default",
123272
123454
  threadId,
@@ -123290,7 +123472,7 @@ var BridgeHandler = class {
123290
123472
  } else {
123291
123473
  if (!card) {
123292
123474
  try {
123293
- card = await this.deps.cardRenderer.start(messageId, { replyInThread, threadId });
123475
+ card = await this.deps.cardRenderer.start(replyAnchorId, { replyInThread, threadId });
123294
123476
  try {
123295
123477
  await writeCardFile(worktreePath, {
123296
123478
  messageId: card.messageId,
@@ -123314,7 +123496,7 @@ var BridgeHandler = class {
123314
123496
  ].filter((part) => !!part).join("; ");
123315
123497
  const postFallback = await createOnlyPostFallback({
123316
123498
  postClient: this.deps.postClient,
123317
- replyToMessageId: messageId,
123499
+ replyToMessageId: replyAnchorId,
123318
123500
  replyInThread,
123319
123501
  botId: this.deps.botConfig?.id ?? "v1-default",
123320
123502
  threadId,
@@ -123395,7 +123577,7 @@ var BridgeHandler = class {
123395
123577
  const createHardFailurePostFallback = async (failureReason) => {
123396
123578
  const fallback = await createOnlyPostFallback({
123397
123579
  postClient: this.deps.postClient,
123398
- replyToMessageId: messageId,
123580
+ replyToMessageId: replyAnchorId,
123399
123581
  replyInThread,
123400
123582
  botId: this.deps.botConfig?.id ?? "v1-default",
123401
123583
  threadId,
@@ -123424,7 +123606,7 @@ var BridgeHandler = class {
123424
123606
  cardKitFinalizeErr
123425
123607
  );
123426
123608
  try {
123427
- card = await this.deps.cardRenderer.start(messageId, { replyInThread, threadId });
123609
+ card = await this.deps.cardRenderer.start(replyAnchorId, { replyInThread, threadId });
123428
123610
  } catch (cardStartErr) {
123429
123611
  console.error("[bridge.handler] failure card start also failed:", cardStartErr);
123430
123612
  await createHardFailurePostFallback(
@@ -130014,7 +130196,7 @@ function isStallNudgeState(value) {
130014
130196
  function isTaskHandleRecord(value) {
130015
130197
  if (typeof value !== "object" || value === null) return false;
130016
130198
  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");
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");
130018
130200
  }
130019
130201
  var TaskHandleStore = class _TaskHandleStore {
130020
130202
  #filePath;
@@ -130209,6 +130391,9 @@ var TaskHandleStore = class _TaskHandleStore {
130209
130391
  await this.update(input.threadId, (existing) => {
130210
130392
  if (existing && existing.taskGuid === input.taskGuid) {
130211
130393
  claimed = true;
130394
+ if (input.mode === "comment" && existing.mode === void 0) {
130395
+ return { ...existing, mode: "comment" };
130396
+ }
130212
130397
  return existing;
130213
130398
  }
130214
130399
  if (input.onlyIfThreadUnclaimed && existing !== void 0) {
@@ -130222,7 +130407,13 @@ var TaskHandleStore = class _TaskHandleStore {
130222
130407
  }
130223
130408
  }
130224
130409
  claimed = true;
130225
- 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
+ };
130226
130417
  });
130227
130418
  return { claimed, reason };
130228
130419
  }
@@ -130318,7 +130509,7 @@ function isTaskRequestTimeoutError(err) {
130318
130509
  if (err instanceof TaskApiError && err.cause instanceof TaskRequestTimeoutError) return true;
130319
130510
  return false;
130320
130511
  }
130321
- function withTimeout2(promise, ms, label) {
130512
+ function withTimeout3(promise, ms, label) {
130322
130513
  let timer;
130323
130514
  const timeout = new Promise((_, reject) => {
130324
130515
  timer = setTimeout(() => reject(new TaskRequestTimeoutError(`${label} timed out after ${ms}ms`)), ms);
@@ -130335,7 +130526,7 @@ var TaskListClient = class {
130335
130526
  }
130336
130527
  /** Single choke point for every outbound call — applies the timeout race uniformly. */
130337
130528
  #request(config, label) {
130338
- return withTimeout2(this.#requester.request(config), this.#timeoutMs, label);
130529
+ return withTimeout3(this.#requester.request(config), this.#timeoutMs, label);
130339
130530
  }
130340
130531
  async getTask(taskGuid) {
130341
130532
  let resp;
@@ -130677,6 +130868,46 @@ async function applyTaskHandleWriteback(patch, deps) {
130677
130868
  return;
130678
130869
  }
130679
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
+ }
130680
130911
  switch (patch.status) {
130681
130912
  case "received": {
130682
130913
  if (isCompleted) {
@@ -131438,6 +131669,7 @@ var StallDetector = class {
131438
131669
  async #pollOne(threadId) {
131439
131670
  let record = this.#deps.store.get(threadId);
131440
131671
  if (!record) return;
131672
+ if (record.doneDeclared) return;
131441
131673
  const lastActiveTs = this.#deps.getLastActiveTs(threadId) ?? record.claimedTs;
131442
131674
  const now = Date.now();
131443
131675
  if (record.stallSuppressUntilActivityAfter !== void 0 && lastActiveTs <= record.stallSuppressUntilActivityAfter) {
@@ -131684,6 +131916,7 @@ var StallDetector = class {
131684
131916
  }
131685
131917
  } : r
131686
131918
  );
131919
+ if (record.mode === "comment") return;
131687
131920
  const logSummary = `\u505C\u6EDE\u5524\u9192 #${prospectiveCount}(${this.#nudgeReasonLabel(reason, dueMs)})`;
131688
131921
  const nextDescription = mergeDescriptionSnapshot(task.description, {
131689
131922
  status: "in_progress",
@@ -131942,12 +132175,18 @@ async function runV2Mode({
131942
132175
  let taskHandleLifecycle;
131943
132176
  let taskHandleClaim;
131944
132177
  let taskHandleClaimedLookup;
132178
+ let taskHandleClaimGuidLookup;
131945
132179
  let taskHandleCandidatesLookup;
131946
132180
  let taskCommentPoller;
131947
132181
  let stallDetector;
131948
132182
  {
131949
132183
  const guid = bot.taskHandle?.tasklistGuid ?? await readTeamTasklistGuid(resolveTaskTeamRegistryPath());
131950
- 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
+ {
131951
132190
  const taskSdkClient = new import_node_sdk2.Client({
131952
132191
  appId: bot.app_id,
131953
132192
  appSecret,
@@ -131957,12 +132196,14 @@ async function runV2Mode({
131957
132196
  request: (config2) => taskSdkClient.request(config2)
131958
132197
  };
131959
132198
  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;
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
+ }
131966
132207
  const taskHandleStore = await TaskHandleStore.load(resolveTaskHandlesPath(bot.id));
131967
132208
  taskHandleLifecycle = (patch) => applyTaskHandleWriteback(patch, { store: taskHandleStore, client: taskListClient });
131968
132209
  taskHandleClaim = async (patch) => {
@@ -131974,12 +132215,13 @@ async function runV2Mode({
131974
132215
  }
131975
132216
  };
131976
132217
  taskHandleClaimedLookup = (threadId) => taskHandleStore.get(threadId) !== void 0;
131977
- {
132218
+ taskHandleClaimGuidLookup = (threadId) => taskHandleStore.get(threadId)?.taskGuid;
132219
+ if (guid) {
131978
132220
  const group = tasklistGuidGroups.get(guid) ?? { client: taskListClient, clientOwnerBotId: bot.id, bots: [] };
131979
132221
  group.bots.push({ botId: bot.id, sessionStore, taskHandleStore });
131980
132222
  tasklistGuidGroups.set(guid, group);
132223
+ taskHandleCandidatesLookup = () => tasklistPollersByGuid.get(guid)?.getCandidates() ?? [];
131981
132224
  }
131982
- taskHandleCandidatesLookup = () => tasklistPollersByGuid.get(guid)?.getCandidates() ?? [];
131983
132225
  taskCommentPoller = new CommentPoller({
131984
132226
  store: taskHandleStore,
131985
132227
  client: taskListClient,
@@ -132042,7 +132284,7 @@ async function runV2Mode({
132042
132284
  // taskHandleCandidatesLookup above (the poller for `guid` is
132043
132285
  // constructed AFTER this whole per-bot loop finishes, but this
132044
132286
  // closure is only ever INVOKED later, at poll time).
132045
- getTaskDueMs: (taskGuid) => tasklistPollersByGuid.get(guid)?.getDueTimestamp(taskGuid),
132287
+ getTaskDueMs: (taskGuid) => guid ? tasklistPollersByGuid.get(guid)?.getDueTimestamp(taskGuid) : void 0,
132046
132288
  // Round-2 adversarial review fix: THIS bot's own receipt signal
132047
132289
  // (not a peer's) — disambiguates a pending-nudge confirmation
132048
132290
  // timeout from a nudge turn merely queued behind the semaphore.
@@ -132091,10 +132333,6 @@ async function runV2Mode({
132091
132333
  );
132092
132334
  stallDetector.start();
132093
132335
  }
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
132336
  }
132099
132337
  }
132100
132338
  let codexPool;
@@ -132165,6 +132403,10 @@ async function runV2Mode({
132165
132403
  cardKitClient,
132166
132404
  cotClient,
132167
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(),
132168
132410
  gitlabToken: effectiveGitlabToken,
132169
132411
  agentMemory: bot.agent_memory,
132170
132412
  larkCliProfile,
@@ -132183,6 +132425,7 @@ async function runV2Mode({
132183
132425
  taskHandleLifecycle,
132184
132426
  taskHandleClaim,
132185
132427
  taskHandleClaimedLookup,
132428
+ taskHandleClaimGuidLookup,
132186
132429
  taskHandleCandidatesLookup
132187
132430
  });
132188
132431
  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.40",
4
4
  "description": "Thin bridge: Feishu thread to local Claude Code CLI",
5
5
  "license": "MIT",
6
6
  "author": "Chuck Wu (chuckwu0)",