larkway 0.3.13 → 0.3.14
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 +1 -1
- package/README.zh.md +1 -1
- package/dist/cli/index.js +108 -13
- package/dist/main.js +488 -357
- package/package.json +1 -1
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.
|
|
11
|
+
**Current release: v0.3.14**
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
package/README.zh.md
CHANGED
package/dist/cli/index.js
CHANGED
|
@@ -107550,6 +107550,7 @@ var channelClient_exports = {};
|
|
|
107550
107550
|
__export(channelClient_exports, {
|
|
107551
107551
|
ChannelClient: () => ChannelClient,
|
|
107552
107552
|
channelMsgToLarkEvent: () => channelMsgToLarkEvent,
|
|
107553
|
+
resolveRecoveredThreadId: () => resolveRecoveredThreadId,
|
|
107553
107554
|
synthesizeCardActionEvent: () => synthesizeCardActionEvent
|
|
107554
107555
|
});
|
|
107555
107556
|
import { execFile as execFileCallback } from "node:child_process";
|
|
@@ -107633,6 +107634,16 @@ function expandMessagesWithThreadReplies(messages) {
|
|
|
107633
107634
|
}
|
|
107634
107635
|
return expanded;
|
|
107635
107636
|
}
|
|
107637
|
+
function resolveRecoveredThreadId(m) {
|
|
107638
|
+
const explicit = nonEmptyStringField(m, "thread_id") ?? nonEmptyStringField(m, "root_id");
|
|
107639
|
+
if (explicit) return explicit;
|
|
107640
|
+
const link = nonEmptyStringField(m, "message_app_link");
|
|
107641
|
+
if (link) {
|
|
107642
|
+
const match = link.match(/open_thread_id=(omt_[A-Za-z0-9_-]+)/);
|
|
107643
|
+
if (match) return match[1] ?? null;
|
|
107644
|
+
}
|
|
107645
|
+
return null;
|
|
107646
|
+
}
|
|
107636
107647
|
function cardActionChoice(value) {
|
|
107637
107648
|
if (value && typeof value === "object") {
|
|
107638
107649
|
const choice = value["larkway_choice"];
|
|
@@ -107682,7 +107693,7 @@ function channelMsgToLarkEvent(msg) {
|
|
|
107682
107693
|
create_time: m?.["create_time"] ?? String(msg.createTime ?? Date.now())
|
|
107683
107694
|
};
|
|
107684
107695
|
}
|
|
107685
|
-
var import_node_sdk, execFile4, LEARNED_CHATS_LIMIT, SEEN_MESSAGES_LIMIT, OPEN_CHAT_DISCOVERY_LOOKBACK_MS, OPEN_CHAT_DISCOVERY_BOOTSTRAP_LOOKBACK_MS, PROCESSING_REACTION_EMOJI, DEFAULT_CONNECT_GRACE_MS, ChannelClient;
|
|
107696
|
+
var import_node_sdk, execFile4, LEARNED_CHATS_LIMIT, SEEN_MESSAGES_LIMIT, MAX_MESSAGE_ATTEMPTS, OPEN_CHAT_DISCOVERY_LOOKBACK_MS, OPEN_CHAT_DISCOVERY_BOOTSTRAP_LOOKBACK_MS, PROCESSING_REACTION_EMOJI, DEFAULT_CONNECT_GRACE_MS, ChannelClient;
|
|
107686
107697
|
var init_channelClient = __esm({
|
|
107687
107698
|
"src/lark/channelClient.ts"() {
|
|
107688
107699
|
"use strict";
|
|
@@ -107692,6 +107703,7 @@ var init_channelClient = __esm({
|
|
|
107692
107703
|
execFile4 = promisify4(execFileCallback);
|
|
107693
107704
|
LEARNED_CHATS_LIMIT = 100;
|
|
107694
107705
|
SEEN_MESSAGES_LIMIT = 1e3;
|
|
107706
|
+
MAX_MESSAGE_ATTEMPTS = 5;
|
|
107695
107707
|
OPEN_CHAT_DISCOVERY_LOOKBACK_MS = 9e4;
|
|
107696
107708
|
OPEN_CHAT_DISCOVERY_BOOTSTRAP_LOOKBACK_MS = 30 * 60 * 1e3;
|
|
107697
107709
|
PROCESSING_REACTION_EMOJI = "Typing";
|
|
@@ -107713,14 +107725,42 @@ var init_channelClient = __esm({
|
|
|
107713
107725
|
*/
|
|
107714
107726
|
lastDisconnectAt = 0;
|
|
107715
107727
|
/**
|
|
107716
|
-
*
|
|
107717
|
-
*
|
|
107718
|
-
*
|
|
107719
|
-
*
|
|
107720
|
-
*
|
|
107721
|
-
*
|
|
107728
|
+
* Message_ids that have reached a terminal SUCCESS (handler.markHandled) OR
|
|
107729
|
+
* were explicitly acknowledged. These are persisted so open-chat recovery
|
|
107730
|
+
* does not replay an already-completed message after a restart. gap-fill
|
|
107731
|
+
* skips anything in this set.
|
|
107732
|
+
*
|
|
107733
|
+
* Bounded: we only add completed/acknowledged messages here (not synthetics
|
|
107734
|
+
* like cardAction turns), and Feishu message_id space is stable and
|
|
107735
|
+
* non-recycling within any reasonable bridge uptime.
|
|
107722
107736
|
*/
|
|
107723
107737
|
seenMessageIds = /* @__PURE__ */ new Set();
|
|
107738
|
+
/**
|
|
107739
|
+
* Message_ids that have been DISPATCHED (pushed onto the inbound queue) but
|
|
107740
|
+
* whose turn has not yet reached a terminal outcome. This is the no-duplicate
|
|
107741
|
+
* guard: gap-fill (and the WS path) skip a message that is already in-flight,
|
|
107742
|
+
* so a message delivered live is never also gap-filled, and two overlapping
|
|
107743
|
+
* gap-fill windows never double-dispatch the same message.
|
|
107744
|
+
*
|
|
107745
|
+
* CRITICAL (the core self-heal): a message stays here only while its turn is
|
|
107746
|
+
* running. handler.markHandled() promotes it into {@link seenMessageIds} on
|
|
107747
|
+
* SUCCESS; handler.markUnhandled() REMOVES it on FAILURE so the next gap-fill
|
|
107748
|
+
* window re-dispatches it — one transient blip (e.g. a TLS timeout creating
|
|
107749
|
+
* the card) no longer swallows the @ forever. NOT persisted: an in-flight
|
|
107750
|
+
* message interrupted by a restart SHOULD be re-dispatchable.
|
|
107751
|
+
*/
|
|
107752
|
+
inFlightMessageIds = /* @__PURE__ */ new Set();
|
|
107753
|
+
/**
|
|
107754
|
+
* Per-message (re-)dispatch counter for the poison-message guard. Incremented
|
|
107755
|
+
* every time a message_id is pushed onto the inbound queue (live WS or either
|
|
107756
|
+
* gap-fill branch) and once more when a turn is released as unhandled. When the
|
|
107757
|
+
* count reaches {@link MAX_MESSAGE_ATTEMPTS}, markUnhandled GIVES UP: it
|
|
107758
|
+
* promotes the message to seen (so it stops being re-dispatched) and logs a
|
|
107759
|
+
* warning. markHandled clears the entry on terminal success. Not persisted:
|
|
107760
|
+
* post-restart, an interrupted message starts fresh — same policy as
|
|
107761
|
+
* inFlightMessageIds.
|
|
107762
|
+
*/
|
|
107763
|
+
messageAttempts = /* @__PURE__ */ new Map();
|
|
107724
107764
|
/**
|
|
107725
107765
|
* Chats observed from live WS events during this process lifetime.
|
|
107726
107766
|
*
|
|
@@ -107822,7 +107862,11 @@ var init_channelClient = __esm({
|
|
|
107822
107862
|
return;
|
|
107823
107863
|
}
|
|
107824
107864
|
this.noteSeenChat(ev.chat_id);
|
|
107825
|
-
this.
|
|
107865
|
+
if (this.seenMessageIds.has(ev.message_id) || this.inFlightMessageIds.has(ev.message_id)) {
|
|
107866
|
+
return;
|
|
107867
|
+
}
|
|
107868
|
+
this.inFlightMessageIds.add(ev.message_id);
|
|
107869
|
+
this.noteDispatchAttempt(ev.message_id);
|
|
107826
107870
|
log(`dispatching (channel-sdk): message_id=${ev.message_id} thread=${ev.thread_id ?? "?"}`);
|
|
107827
107871
|
this.queue.push(ev);
|
|
107828
107872
|
});
|
|
@@ -107972,7 +108016,7 @@ var init_channelClient = __esm({
|
|
|
107972
108016
|
const m = raw;
|
|
107973
108017
|
const messageId = m["message_id"];
|
|
107974
108018
|
if (!messageId) continue;
|
|
107975
|
-
if (this.seenMessageIds.has(messageId)) continue;
|
|
108019
|
+
if (this.seenMessageIds.has(messageId) || this.inFlightMessageIds.has(messageId)) continue;
|
|
107976
108020
|
const mentions = m["mentions"];
|
|
107977
108021
|
const mentionsBot = Array.isArray(mentions) && mentions.some(
|
|
107978
108022
|
(mn) => {
|
|
@@ -107981,6 +108025,11 @@ var init_channelClient = __esm({
|
|
|
107981
108025
|
}
|
|
107982
108026
|
);
|
|
107983
108027
|
if (!mentionsBot) continue;
|
|
108028
|
+
const recoveredThread = resolveRecoveredThreadId(m);
|
|
108029
|
+
const isThreadReply = recoveredThread !== null && recoveredThread !== messageId;
|
|
108030
|
+
if (isThreadReply && !nonEmptyStringField(m, "root_id")) {
|
|
108031
|
+
m["root_id"] = recoveredThread;
|
|
108032
|
+
}
|
|
107984
108033
|
const ev = channelMsgToLarkEvent({
|
|
107985
108034
|
raw: {
|
|
107986
108035
|
event: {
|
|
@@ -107999,21 +108048,23 @@ var init_channelClient = __esm({
|
|
|
107999
108048
|
message_id: mid,
|
|
108000
108049
|
chat_id: cid,
|
|
108001
108050
|
chat_type: m["chat_type"] ?? "group",
|
|
108002
|
-
thread_id: m["thread_id"] ?? m["root_id"] ?? mid,
|
|
108003
|
-
root_id: m["root_id"],
|
|
108051
|
+
thread_id: m["thread_id"] ?? (isThreadReply ? recoveredThread ?? void 0 : void 0) ?? m["root_id"] ?? mid,
|
|
108052
|
+
root_id: m["root_id"] ?? (isThreadReply ? recoveredThread ?? void 0 : void 0),
|
|
108004
108053
|
sender_id: sid,
|
|
108005
108054
|
content: typeof m["content"] === "string" ? m["content"] : JSON.stringify({ text: "" }),
|
|
108006
108055
|
create_time: m["create_time"] ?? String(Date.now())
|
|
108007
108056
|
};
|
|
108008
108057
|
})();
|
|
108009
108058
|
if (!fallbackEv) continue;
|
|
108010
|
-
this.
|
|
108059
|
+
this.inFlightMessageIds.add(fallbackEv.message_id);
|
|
108060
|
+
this.noteDispatchAttempt(fallbackEv.message_id);
|
|
108011
108061
|
log(`gap-fill dispatching (fallback): message_id=${fallbackEv.message_id} chat=${chatId}`);
|
|
108012
108062
|
this.queue.push(fallbackEv);
|
|
108013
108063
|
totalDispatched++;
|
|
108014
108064
|
continue;
|
|
108015
108065
|
}
|
|
108016
|
-
this.
|
|
108066
|
+
this.inFlightMessageIds.add(ev.message_id);
|
|
108067
|
+
this.noteDispatchAttempt(ev.message_id);
|
|
108017
108068
|
log(`gap-fill dispatching: message_id=${ev.message_id} thread=${ev.thread_id ?? "?"} chat=${chatId}`);
|
|
108018
108069
|
this.queue.push(ev);
|
|
108019
108070
|
totalDispatched++;
|
|
@@ -108237,8 +108288,52 @@ var init_channelClient = __esm({
|
|
|
108237
108288
|
* already handled message after a restart.
|
|
108238
108289
|
*/
|
|
108239
108290
|
acknowledgeMessage(messageId) {
|
|
108291
|
+
this.markHandled(messageId);
|
|
108292
|
+
}
|
|
108293
|
+
/**
|
|
108294
|
+
* Terminal SUCCESS: promote a message out of the in-flight set into the
|
|
108295
|
+
* persisted seen set. After this, neither the WS path nor any gap-fill window
|
|
108296
|
+
* (this process or post-restart) re-dispatches it. Persistence flows through
|
|
108297
|
+
* {@link noteSeenMessage} (the same channel-seen-messages json the success
|
|
108298
|
+
* set has always used), so post-restart recovery skips completed messages.
|
|
108299
|
+
*/
|
|
108300
|
+
markHandled(messageId) {
|
|
108301
|
+
this.inFlightMessageIds.delete(messageId);
|
|
108302
|
+
this.messageAttempts.delete(messageId);
|
|
108240
108303
|
this.noteSeenMessage(messageId);
|
|
108241
108304
|
}
|
|
108305
|
+
/**
|
|
108306
|
+
* Terminal FAILURE/ABORT: release a message from the in-flight set WITHOUT
|
|
108307
|
+
* marking it seen, so the next gap-fill window can re-dispatch it (the core
|
|
108308
|
+
* self-heal — one transient blip no longer swallows the @ forever). Does not
|
|
108309
|
+
* touch persisted seen state.
|
|
108310
|
+
*
|
|
108311
|
+
* Poison-message guard: count this failed turn as one more attempt. If the
|
|
108312
|
+
* message has now failed {@link MAX_MESSAGE_ATTEMPTS} times, GIVE UP — promote
|
|
108313
|
+
* it to seen (so it stops being re-dispatched on every gap-fill) and log a
|
|
108314
|
+
* visible warning instead of silently looping forever.
|
|
108315
|
+
*/
|
|
108316
|
+
markUnhandled(messageId) {
|
|
108317
|
+
this.inFlightMessageIds.delete(messageId);
|
|
108318
|
+
const attempts = (this.messageAttempts.get(messageId) ?? 0) + 1;
|
|
108319
|
+
this.messageAttempts.set(messageId, attempts);
|
|
108320
|
+
if (attempts >= MAX_MESSAGE_ATTEMPTS) {
|
|
108321
|
+
console.warn(
|
|
108322
|
+
`[channel.client] giving up on message_id=${messageId} after ${attempts} failed attempts \u2014 promoting to seen so it is no longer re-dispatched (poison-message guard)`
|
|
108323
|
+
);
|
|
108324
|
+
this.messageAttempts.delete(messageId);
|
|
108325
|
+
this.noteSeenMessage(messageId);
|
|
108326
|
+
}
|
|
108327
|
+
}
|
|
108328
|
+
/**
|
|
108329
|
+
* Increment the per-message dispatch counter (poison-message guard). Called
|
|
108330
|
+
* each time a message_id is pushed onto the inbound queue. The counter is also
|
|
108331
|
+
* bumped in {@link markUnhandled} so both dispatch and failed settlement
|
|
108332
|
+
* contribute toward the cap.
|
|
108333
|
+
*/
|
|
108334
|
+
noteDispatchAttempt(messageId) {
|
|
108335
|
+
this.messageAttempts.set(messageId, (this.messageAttempts.get(messageId) ?? 0) + 1);
|
|
108336
|
+
}
|
|
108242
108337
|
async close() {
|
|
108243
108338
|
this.closed = true;
|
|
108244
108339
|
if (this.openChatDiscoveryTimer) {
|
package/dist/main.js
CHANGED
|
@@ -111778,6 +111778,7 @@ var ChannelCardClient = class {
|
|
|
111778
111778
|
var execFile = promisify(execFileCallback);
|
|
111779
111779
|
var LEARNED_CHATS_LIMIT = 100;
|
|
111780
111780
|
var SEEN_MESSAGES_LIMIT = 1e3;
|
|
111781
|
+
var MAX_MESSAGE_ATTEMPTS = 5;
|
|
111781
111782
|
var OPEN_CHAT_DISCOVERY_LOOKBACK_MS = 9e4;
|
|
111782
111783
|
var OPEN_CHAT_DISCOVERY_BOOTSTRAP_LOOKBACK_MS = 30 * 60 * 1e3;
|
|
111783
111784
|
var PROCESSING_REACTION_EMOJI = "Typing";
|
|
@@ -111859,6 +111860,16 @@ function expandMessagesWithThreadReplies(messages) {
|
|
|
111859
111860
|
}
|
|
111860
111861
|
return expanded;
|
|
111861
111862
|
}
|
|
111863
|
+
function resolveRecoveredThreadId(m) {
|
|
111864
|
+
const explicit = nonEmptyStringField(m, "thread_id") ?? nonEmptyStringField(m, "root_id");
|
|
111865
|
+
if (explicit) return explicit;
|
|
111866
|
+
const link = nonEmptyStringField(m, "message_app_link");
|
|
111867
|
+
if (link) {
|
|
111868
|
+
const match = link.match(/open_thread_id=(omt_[A-Za-z0-9_-]+)/);
|
|
111869
|
+
if (match) return match[1] ?? null;
|
|
111870
|
+
}
|
|
111871
|
+
return null;
|
|
111872
|
+
}
|
|
111862
111873
|
function cardActionChoice(value) {
|
|
111863
111874
|
if (value && typeof value === "object") {
|
|
111864
111875
|
const choice = value["larkway_choice"];
|
|
@@ -111925,14 +111936,42 @@ var ChannelClient = class {
|
|
|
111925
111936
|
*/
|
|
111926
111937
|
lastDisconnectAt = 0;
|
|
111927
111938
|
/**
|
|
111928
|
-
*
|
|
111929
|
-
*
|
|
111930
|
-
*
|
|
111931
|
-
*
|
|
111932
|
-
*
|
|
111933
|
-
*
|
|
111939
|
+
* Message_ids that have reached a terminal SUCCESS (handler.markHandled) OR
|
|
111940
|
+
* were explicitly acknowledged. These are persisted so open-chat recovery
|
|
111941
|
+
* does not replay an already-completed message after a restart. gap-fill
|
|
111942
|
+
* skips anything in this set.
|
|
111943
|
+
*
|
|
111944
|
+
* Bounded: we only add completed/acknowledged messages here (not synthetics
|
|
111945
|
+
* like cardAction turns), and Feishu message_id space is stable and
|
|
111946
|
+
* non-recycling within any reasonable bridge uptime.
|
|
111934
111947
|
*/
|
|
111935
111948
|
seenMessageIds = /* @__PURE__ */ new Set();
|
|
111949
|
+
/**
|
|
111950
|
+
* Message_ids that have been DISPATCHED (pushed onto the inbound queue) but
|
|
111951
|
+
* whose turn has not yet reached a terminal outcome. This is the no-duplicate
|
|
111952
|
+
* guard: gap-fill (and the WS path) skip a message that is already in-flight,
|
|
111953
|
+
* so a message delivered live is never also gap-filled, and two overlapping
|
|
111954
|
+
* gap-fill windows never double-dispatch the same message.
|
|
111955
|
+
*
|
|
111956
|
+
* CRITICAL (the core self-heal): a message stays here only while its turn is
|
|
111957
|
+
* running. handler.markHandled() promotes it into {@link seenMessageIds} on
|
|
111958
|
+
* SUCCESS; handler.markUnhandled() REMOVES it on FAILURE so the next gap-fill
|
|
111959
|
+
* window re-dispatches it — one transient blip (e.g. a TLS timeout creating
|
|
111960
|
+
* the card) no longer swallows the @ forever. NOT persisted: an in-flight
|
|
111961
|
+
* message interrupted by a restart SHOULD be re-dispatchable.
|
|
111962
|
+
*/
|
|
111963
|
+
inFlightMessageIds = /* @__PURE__ */ new Set();
|
|
111964
|
+
/**
|
|
111965
|
+
* Per-message (re-)dispatch counter for the poison-message guard. Incremented
|
|
111966
|
+
* every time a message_id is pushed onto the inbound queue (live WS or either
|
|
111967
|
+
* gap-fill branch) and once more when a turn is released as unhandled. When the
|
|
111968
|
+
* count reaches {@link MAX_MESSAGE_ATTEMPTS}, markUnhandled GIVES UP: it
|
|
111969
|
+
* promotes the message to seen (so it stops being re-dispatched) and logs a
|
|
111970
|
+
* warning. markHandled clears the entry on terminal success. Not persisted:
|
|
111971
|
+
* post-restart, an interrupted message starts fresh — same policy as
|
|
111972
|
+
* inFlightMessageIds.
|
|
111973
|
+
*/
|
|
111974
|
+
messageAttempts = /* @__PURE__ */ new Map();
|
|
111936
111975
|
/**
|
|
111937
111976
|
* Chats observed from live WS events during this process lifetime.
|
|
111938
111977
|
*
|
|
@@ -112034,7 +112073,11 @@ var ChannelClient = class {
|
|
|
112034
112073
|
return;
|
|
112035
112074
|
}
|
|
112036
112075
|
this.noteSeenChat(ev.chat_id);
|
|
112037
|
-
this.
|
|
112076
|
+
if (this.seenMessageIds.has(ev.message_id) || this.inFlightMessageIds.has(ev.message_id)) {
|
|
112077
|
+
return;
|
|
112078
|
+
}
|
|
112079
|
+
this.inFlightMessageIds.add(ev.message_id);
|
|
112080
|
+
this.noteDispatchAttempt(ev.message_id);
|
|
112038
112081
|
log(`dispatching (channel-sdk): message_id=${ev.message_id} thread=${ev.thread_id ?? "?"}`);
|
|
112039
112082
|
this.queue.push(ev);
|
|
112040
112083
|
});
|
|
@@ -112184,7 +112227,7 @@ var ChannelClient = class {
|
|
|
112184
112227
|
const m = raw;
|
|
112185
112228
|
const messageId = m["message_id"];
|
|
112186
112229
|
if (!messageId) continue;
|
|
112187
|
-
if (this.seenMessageIds.has(messageId)) continue;
|
|
112230
|
+
if (this.seenMessageIds.has(messageId) || this.inFlightMessageIds.has(messageId)) continue;
|
|
112188
112231
|
const mentions = m["mentions"];
|
|
112189
112232
|
const mentionsBot = Array.isArray(mentions) && mentions.some(
|
|
112190
112233
|
(mn) => {
|
|
@@ -112193,6 +112236,11 @@ var ChannelClient = class {
|
|
|
112193
112236
|
}
|
|
112194
112237
|
);
|
|
112195
112238
|
if (!mentionsBot) continue;
|
|
112239
|
+
const recoveredThread = resolveRecoveredThreadId(m);
|
|
112240
|
+
const isThreadReply = recoveredThread !== null && recoveredThread !== messageId;
|
|
112241
|
+
if (isThreadReply && !nonEmptyStringField(m, "root_id")) {
|
|
112242
|
+
m["root_id"] = recoveredThread;
|
|
112243
|
+
}
|
|
112196
112244
|
const ev = channelMsgToLarkEvent({
|
|
112197
112245
|
raw: {
|
|
112198
112246
|
event: {
|
|
@@ -112211,21 +112259,23 @@ var ChannelClient = class {
|
|
|
112211
112259
|
message_id: mid,
|
|
112212
112260
|
chat_id: cid,
|
|
112213
112261
|
chat_type: m["chat_type"] ?? "group",
|
|
112214
|
-
thread_id: m["thread_id"] ?? m["root_id"] ?? mid,
|
|
112215
|
-
root_id: m["root_id"],
|
|
112262
|
+
thread_id: m["thread_id"] ?? (isThreadReply ? recoveredThread ?? void 0 : void 0) ?? m["root_id"] ?? mid,
|
|
112263
|
+
root_id: m["root_id"] ?? (isThreadReply ? recoveredThread ?? void 0 : void 0),
|
|
112216
112264
|
sender_id: sid,
|
|
112217
112265
|
content: typeof m["content"] === "string" ? m["content"] : JSON.stringify({ text: "" }),
|
|
112218
112266
|
create_time: m["create_time"] ?? String(Date.now())
|
|
112219
112267
|
};
|
|
112220
112268
|
})();
|
|
112221
112269
|
if (!fallbackEv) continue;
|
|
112222
|
-
this.
|
|
112270
|
+
this.inFlightMessageIds.add(fallbackEv.message_id);
|
|
112271
|
+
this.noteDispatchAttempt(fallbackEv.message_id);
|
|
112223
112272
|
log(`gap-fill dispatching (fallback): message_id=${fallbackEv.message_id} chat=${chatId}`);
|
|
112224
112273
|
this.queue.push(fallbackEv);
|
|
112225
112274
|
totalDispatched++;
|
|
112226
112275
|
continue;
|
|
112227
112276
|
}
|
|
112228
|
-
this.
|
|
112277
|
+
this.inFlightMessageIds.add(ev.message_id);
|
|
112278
|
+
this.noteDispatchAttempt(ev.message_id);
|
|
112229
112279
|
log(`gap-fill dispatching: message_id=${ev.message_id} thread=${ev.thread_id ?? "?"} chat=${chatId}`);
|
|
112230
112280
|
this.queue.push(ev);
|
|
112231
112281
|
totalDispatched++;
|
|
@@ -112449,8 +112499,52 @@ var ChannelClient = class {
|
|
|
112449
112499
|
* already handled message after a restart.
|
|
112450
112500
|
*/
|
|
112451
112501
|
acknowledgeMessage(messageId) {
|
|
112502
|
+
this.markHandled(messageId);
|
|
112503
|
+
}
|
|
112504
|
+
/**
|
|
112505
|
+
* Terminal SUCCESS: promote a message out of the in-flight set into the
|
|
112506
|
+
* persisted seen set. After this, neither the WS path nor any gap-fill window
|
|
112507
|
+
* (this process or post-restart) re-dispatches it. Persistence flows through
|
|
112508
|
+
* {@link noteSeenMessage} (the same channel-seen-messages json the success
|
|
112509
|
+
* set has always used), so post-restart recovery skips completed messages.
|
|
112510
|
+
*/
|
|
112511
|
+
markHandled(messageId) {
|
|
112512
|
+
this.inFlightMessageIds.delete(messageId);
|
|
112513
|
+
this.messageAttempts.delete(messageId);
|
|
112452
112514
|
this.noteSeenMessage(messageId);
|
|
112453
112515
|
}
|
|
112516
|
+
/**
|
|
112517
|
+
* Terminal FAILURE/ABORT: release a message from the in-flight set WITHOUT
|
|
112518
|
+
* marking it seen, so the next gap-fill window can re-dispatch it (the core
|
|
112519
|
+
* self-heal — one transient blip no longer swallows the @ forever). Does not
|
|
112520
|
+
* touch persisted seen state.
|
|
112521
|
+
*
|
|
112522
|
+
* Poison-message guard: count this failed turn as one more attempt. If the
|
|
112523
|
+
* message has now failed {@link MAX_MESSAGE_ATTEMPTS} times, GIVE UP — promote
|
|
112524
|
+
* it to seen (so it stops being re-dispatched on every gap-fill) and log a
|
|
112525
|
+
* visible warning instead of silently looping forever.
|
|
112526
|
+
*/
|
|
112527
|
+
markUnhandled(messageId) {
|
|
112528
|
+
this.inFlightMessageIds.delete(messageId);
|
|
112529
|
+
const attempts = (this.messageAttempts.get(messageId) ?? 0) + 1;
|
|
112530
|
+
this.messageAttempts.set(messageId, attempts);
|
|
112531
|
+
if (attempts >= MAX_MESSAGE_ATTEMPTS) {
|
|
112532
|
+
console.warn(
|
|
112533
|
+
`[channel.client] giving up on message_id=${messageId} after ${attempts} failed attempts \u2014 promoting to seen so it is no longer re-dispatched (poison-message guard)`
|
|
112534
|
+
);
|
|
112535
|
+
this.messageAttempts.delete(messageId);
|
|
112536
|
+
this.noteSeenMessage(messageId);
|
|
112537
|
+
}
|
|
112538
|
+
}
|
|
112539
|
+
/**
|
|
112540
|
+
* Increment the per-message dispatch counter (poison-message guard). Called
|
|
112541
|
+
* each time a message_id is pushed onto the inbound queue. The counter is also
|
|
112542
|
+
* bumped in {@link markUnhandled} so both dispatch and failed settlement
|
|
112543
|
+
* contribute toward the cap.
|
|
112544
|
+
*/
|
|
112545
|
+
noteDispatchAttempt(messageId) {
|
|
112546
|
+
this.messageAttempts.set(messageId, (this.messageAttempts.get(messageId) ?? 0) + 1);
|
|
112547
|
+
}
|
|
112454
112548
|
async close() {
|
|
112455
112549
|
this.closed = true;
|
|
112456
112550
|
if (this.openChatDiscoveryTimer) {
|
|
@@ -112786,6 +112880,30 @@ function extractRawMessageId(raw) {
|
|
|
112786
112880
|
const id = msg["id"];
|
|
112787
112881
|
return typeof id === "string" ? id : null;
|
|
112788
112882
|
}
|
|
112883
|
+
async function createCardWithRetry(outbound, replyToMessageId, cardJson, opts, maxAttempts = 3) {
|
|
112884
|
+
let lastErr;
|
|
112885
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
112886
|
+
try {
|
|
112887
|
+
return await outbound.createCard(replyToMessageId, cardJson, opts);
|
|
112888
|
+
} catch (err) {
|
|
112889
|
+
lastErr = err;
|
|
112890
|
+
if (attempt === maxAttempts) {
|
|
112891
|
+
console.error(
|
|
112892
|
+
`[lark.card] createCard failed after ${attempt} attempts:`,
|
|
112893
|
+
err
|
|
112894
|
+
);
|
|
112895
|
+
throw err;
|
|
112896
|
+
}
|
|
112897
|
+
const delay = 400 * Math.pow(2, attempt - 1);
|
|
112898
|
+
console.warn(
|
|
112899
|
+
`[lark.card] createCard attempt ${attempt} failed, retrying in ${delay}ms:`,
|
|
112900
|
+
err.message
|
|
112901
|
+
);
|
|
112902
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
112903
|
+
}
|
|
112904
|
+
}
|
|
112905
|
+
throw lastErr;
|
|
112906
|
+
}
|
|
112789
112907
|
var CardRenderer = class {
|
|
112790
112908
|
/**
|
|
112791
112909
|
* Transport for the two OUTBOUND card calls (create + patch). Injected
|
|
@@ -112827,7 +112945,8 @@ var CardRenderer = class {
|
|
|
112827
112945
|
status: "thinking"
|
|
112828
112946
|
});
|
|
112829
112947
|
const replyInThread = opts?.replyInThread ?? true;
|
|
112830
|
-
const { messageId } = await
|
|
112948
|
+
const { messageId } = await createCardWithRetry(
|
|
112949
|
+
this.outbound,
|
|
112831
112950
|
replyToMessageId,
|
|
112832
112951
|
initialCardJson,
|
|
112833
112952
|
{ replyInThread, threadId: opts?.threadId }
|
|
@@ -114699,383 +114818,395 @@ var BridgeHandler = class {
|
|
|
114699
114818
|
// Private: single-event lifecycle
|
|
114700
114819
|
// ---------------------------------------------------------------------------
|
|
114701
114820
|
async handleOne(event) {
|
|
114702
|
-
const
|
|
114703
|
-
|
|
114704
|
-
const
|
|
114705
|
-
|
|
114706
|
-
|
|
114707
|
-
|
|
114708
|
-
|
|
114709
|
-
try {
|
|
114710
|
-
await this.deps.recordRuntimeEvent({ id: eventLogId, ...patch });
|
|
114711
|
-
} catch (err) {
|
|
114712
|
-
console.warn("[bridge.handler] recordRuntimeEvent failed (continuing):", err);
|
|
114713
|
-
}
|
|
114821
|
+
const settleMessageId = event.message_id;
|
|
114822
|
+
let settled = false;
|
|
114823
|
+
const settle = (ok) => {
|
|
114824
|
+
if (settled) return;
|
|
114825
|
+
settled = true;
|
|
114826
|
+
if (ok) this.deps.client.markHandled?.(settleMessageId);
|
|
114827
|
+
else this.deps.client.markUnhandled?.(settleMessageId);
|
|
114714
114828
|
};
|
|
114715
|
-
const triggerType = typeof parsed.raw.root_id === "string" && parsed.raw.root_id ? "thread_reply" : "mention";
|
|
114716
|
-
await recordEvent({
|
|
114717
|
-
botId,
|
|
114718
|
-
botName: this.deps.botConfig?.name,
|
|
114719
|
-
messageId,
|
|
114720
|
-
threadId,
|
|
114721
|
-
chatId: parsed.chatId,
|
|
114722
|
-
senderId: senderOpenId,
|
|
114723
|
-
triggerType,
|
|
114724
|
-
textPreview: parsed.text.slice(0, 120),
|
|
114725
|
-
status: "received",
|
|
114726
|
-
receivedAt: new Date(eventStartedAt).toISOString(),
|
|
114727
|
-
statusPath: ["\u5DF2\u6536\u5230"],
|
|
114728
|
-
reason: "\u5DF2\u8FDB\u5165 bridge\uFF0C\u51C6\u5907\u521B\u5EFA\u5904\u7406\u5361\u7247\u3002"
|
|
114729
|
-
});
|
|
114730
|
-
await this.deps.client.addProcessingReaction?.(messageId);
|
|
114731
|
-
const existing = this.deps.sessionStore.get(threadId, botId);
|
|
114732
|
-
const isNewThread = existing === void 0;
|
|
114733
|
-
const isTopLevel = !(typeof parsed.raw.root_id === "string" && parsed.raw.root_id);
|
|
114734
|
-
const replyInThread = isTopLevel;
|
|
114735
|
-
let card;
|
|
114736
114829
|
try {
|
|
114737
|
-
|
|
114738
|
-
|
|
114739
|
-
|
|
114740
|
-
|
|
114741
|
-
|
|
114742
|
-
|
|
114743
|
-
|
|
114744
|
-
|
|
114745
|
-
|
|
114830
|
+
const parsed = parseMessage(event);
|
|
114831
|
+
const { threadId, messageId, senderOpenId } = parsed;
|
|
114832
|
+
const botId = this.deps.botConfig?.id;
|
|
114833
|
+
const eventLogId = messageId;
|
|
114834
|
+
const eventStartedAt = Date.now();
|
|
114835
|
+
const recordEvent = async (patch) => {
|
|
114836
|
+
if (!this.deps.recordRuntimeEvent) return;
|
|
114837
|
+
try {
|
|
114838
|
+
await this.deps.recordRuntimeEvent({ id: eventLogId, ...patch });
|
|
114839
|
+
} catch (err) {
|
|
114840
|
+
console.warn("[bridge.handler] recordRuntimeEvent failed (continuing):", err);
|
|
114841
|
+
}
|
|
114842
|
+
};
|
|
114843
|
+
const triggerType = typeof parsed.raw.root_id === "string" && parsed.raw.root_id ? "thread_reply" : "mention";
|
|
114746
114844
|
await recordEvent({
|
|
114747
|
-
|
|
114748
|
-
|
|
114749
|
-
|
|
114750
|
-
|
|
114845
|
+
botId,
|
|
114846
|
+
botName: this.deps.botConfig?.name,
|
|
114847
|
+
messageId,
|
|
114848
|
+
threadId,
|
|
114849
|
+
chatId: parsed.chatId,
|
|
114850
|
+
senderId: senderOpenId,
|
|
114851
|
+
triggerType,
|
|
114852
|
+
textPreview: parsed.text.slice(0, 120),
|
|
114853
|
+
status: "received",
|
|
114854
|
+
receivedAt: new Date(eventStartedAt).toISOString(),
|
|
114855
|
+
statusPath: ["\u5DF2\u6536\u5230"],
|
|
114856
|
+
reason: "\u5DF2\u8FDB\u5165 bridge\uFF0C\u51C6\u5907\u521B\u5EFA\u5904\u7406\u5361\u7247\u3002"
|
|
114751
114857
|
});
|
|
114752
|
-
|
|
114753
|
-
|
|
114754
|
-
|
|
114755
|
-
|
|
114756
|
-
const
|
|
114757
|
-
|
|
114758
|
-
|
|
114759
|
-
|
|
114760
|
-
|
|
114761
|
-
|
|
114762
|
-
|
|
114763
|
-
|
|
114764
|
-
|
|
114765
|
-
|
|
114766
|
-
|
|
114767
|
-
|
|
114768
|
-
|
|
114769
|
-
|
|
114770
|
-
|
|
114771
|
-
|
|
114772
|
-
|
|
114773
|
-
sessionPath: worktreePath,
|
|
114774
|
-
bot: {
|
|
114775
|
-
name: this.deps.botConfig?.name ?? "Larkway Agent",
|
|
114776
|
-
description: this.deps.botConfig?.description ?? "Local agent served through Larkway.",
|
|
114777
|
-
gitlab_token_env: this.deps.botConfig?.git_token_env ?? this.deps.botConfig?.gitlab_token_env
|
|
114778
|
-
},
|
|
114779
|
-
agentMemory: this.deps.agentMemory,
|
|
114780
|
-
repos: [
|
|
114781
|
-
...conventions.defaultProjectSlug ? [
|
|
114782
|
-
{
|
|
114783
|
-
slug: conventions.defaultProjectSlug,
|
|
114784
|
-
branch: conventions.defaultBranch,
|
|
114785
|
-
url: conventions.primaryRepoUrl,
|
|
114786
|
-
suggestedPath: conventions.repoCachePath ?? conventions.workspaceReposPath
|
|
114787
|
-
}
|
|
114788
|
-
] : [],
|
|
114789
|
-
...extraRepos.map((repo) => ({
|
|
114790
|
-
slug: repo.slug,
|
|
114791
|
-
url: repo.url,
|
|
114792
|
-
suggestedPath: repo.cachePath
|
|
114793
|
-
}))
|
|
114794
|
-
]
|
|
114858
|
+
await this.deps.client.addProcessingReaction?.(messageId);
|
|
114859
|
+
const existing = this.deps.sessionStore.get(threadId, botId);
|
|
114860
|
+
const isNewThread = existing === void 0;
|
|
114861
|
+
const isTopLevel = !(typeof parsed.raw.root_id === "string" && parsed.raw.root_id);
|
|
114862
|
+
const replyInThread = isTopLevel;
|
|
114863
|
+
let card;
|
|
114864
|
+
try {
|
|
114865
|
+
card = await this.deps.cardRenderer.start(messageId, { replyInThread, threadId });
|
|
114866
|
+
await recordEvent({
|
|
114867
|
+
status: "running",
|
|
114868
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
114869
|
+
appendPath: "\u5DF2\u521B\u5EFA\u5361\u7247",
|
|
114870
|
+
reason: "\u5DF2\u4EA4\u7ED9\u672C\u5730 Agent \u5904\u7406\u3002"
|
|
114871
|
+
});
|
|
114872
|
+
} catch (err) {
|
|
114873
|
+
console.error("[bridge.handler] Failed to start card for thread", threadId, err);
|
|
114874
|
+
await recordEvent({
|
|
114875
|
+
status: "running",
|
|
114876
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
114877
|
+
appendPath: "\u5361\u7247\u521B\u5EFA\u5931\u8D25\uFF0C\u7EE7\u7EED\u6267\u884C",
|
|
114878
|
+
reason: "\u5361\u7247\u521B\u5EFA\u5931\u8D25\uFF0C\u4F46 bridge \u4F1A\u7EE7\u7EED\u542F\u52A8\u672C\u5730 Agent\u3002"
|
|
114795
114879
|
});
|
|
114880
|
+
} finally {
|
|
114881
|
+
await this.deps.client.removeProcessingReaction?.(messageId);
|
|
114796
114882
|
}
|
|
114797
|
-
|
|
114798
|
-
|
|
114799
|
-
|
|
114800
|
-
|
|
114801
|
-
|
|
114802
|
-
|
|
114803
|
-
|
|
114804
|
-
try {
|
|
114805
|
-
await execGit(conventions.repoCachePath, ["fetch", "origin", "--quiet"]);
|
|
114806
|
-
} catch (err) {
|
|
114807
|
-
console.warn("[bridge.handler] primary repo fetch failed (continuing):", err);
|
|
114883
|
+
try {
|
|
114884
|
+
const { conventions } = this.deps;
|
|
114885
|
+
const isAgentWorkspace = conventions.runtime === "agent_workspace";
|
|
114886
|
+
if (isAgentWorkspace) {
|
|
114887
|
+
if (!conventions.agentWorkspacePath || !conventions.workspaceSessionsDir || !conventions.workspaceReposPath) {
|
|
114888
|
+
throw new Error("agent_workspace runtime requires workspace path conventions");
|
|
114889
|
+
}
|
|
114808
114890
|
}
|
|
114809
|
-
|
|
114810
|
-
|
|
114811
|
-
|
|
114812
|
-
|
|
114813
|
-
|
|
114814
|
-
|
|
114815
|
-
|
|
114816
|
-
|
|
114817
|
-
|
|
114818
|
-
|
|
114819
|
-
|
|
114820
|
-
|
|
114821
|
-
|
|
114822
|
-
|
|
114823
|
-
|
|
114891
|
+
const worktreePath = isAgentWorkspace ? path8.join(conventions.workspaceSessionsDir, threadId) : path8.join(conventions.worktreesDir, threadId);
|
|
114892
|
+
const runCwd = isAgentWorkspace ? conventions.agentWorkspacePath : worktreePath;
|
|
114893
|
+
const hasRepo = !isAgentWorkspace && !!conventions.repoCachePath;
|
|
114894
|
+
const buildWorktree = hasRepo && !conventions.readOnly;
|
|
114895
|
+
const extraRepos = conventions.extraRepoPaths ?? [];
|
|
114896
|
+
if (isAgentWorkspace) {
|
|
114897
|
+
await ensureAgentWorkspace({
|
|
114898
|
+
agentId: botId ?? "v1-default",
|
|
114899
|
+
workspacePath: conventions.agentWorkspacePath,
|
|
114900
|
+
reposPath: conventions.workspaceReposPath,
|
|
114901
|
+
sessionPath: worktreePath,
|
|
114902
|
+
bot: {
|
|
114903
|
+
name: this.deps.botConfig?.name ?? "Larkway Agent",
|
|
114904
|
+
description: this.deps.botConfig?.description ?? "Local agent served through Larkway.",
|
|
114905
|
+
gitlab_token_env: this.deps.botConfig?.git_token_env ?? this.deps.botConfig?.gitlab_token_env
|
|
114906
|
+
},
|
|
114907
|
+
agentMemory: this.deps.agentMemory,
|
|
114908
|
+
repos: [
|
|
114909
|
+
...conventions.defaultProjectSlug ? [
|
|
114910
|
+
{
|
|
114911
|
+
slug: conventions.defaultProjectSlug,
|
|
114912
|
+
branch: conventions.defaultBranch,
|
|
114913
|
+
url: conventions.primaryRepoUrl,
|
|
114914
|
+
suggestedPath: conventions.repoCachePath ?? conventions.workspaceReposPath
|
|
114915
|
+
}
|
|
114916
|
+
] : [],
|
|
114917
|
+
...extraRepos.map((repo) => ({
|
|
114918
|
+
slug: repo.slug,
|
|
114919
|
+
url: repo.url,
|
|
114920
|
+
suggestedPath: repo.cachePath
|
|
114921
|
+
}))
|
|
114922
|
+
]
|
|
114923
|
+
});
|
|
114824
114924
|
}
|
|
114825
|
-
|
|
114826
|
-
|
|
114827
|
-
|
|
114828
|
-
|
|
114829
|
-
|
|
114830
|
-
|
|
114831
|
-
`[bridge.handler] worktree ${worktreePath} exists but git health check failed \u2014 removing stale dir and rebuilding (BL-8: migrated worktree with dead .git pointer)`
|
|
114925
|
+
if (hasRepo) {
|
|
114926
|
+
await ensureRepoClone(
|
|
114927
|
+
conventions.repoCachePath,
|
|
114928
|
+
conventions.primaryRepoUrl,
|
|
114929
|
+
this.deps.gitlabToken,
|
|
114930
|
+
conventions.defaultProjectSlug ?? "primary"
|
|
114832
114931
|
);
|
|
114833
114932
|
try {
|
|
114834
|
-
await
|
|
114835
|
-
} catch (
|
|
114836
|
-
console.warn("[bridge.handler]
|
|
114933
|
+
await execGit(conventions.repoCachePath, ["fetch", "origin", "--quiet"]);
|
|
114934
|
+
} catch (err) {
|
|
114935
|
+
console.warn("[bridge.handler] primary repo fetch failed (continuing):", err);
|
|
114837
114936
|
}
|
|
114838
|
-
worktreeExists = false;
|
|
114839
114937
|
}
|
|
114840
|
-
|
|
114841
|
-
|
|
114842
|
-
|
|
114843
|
-
|
|
114844
|
-
|
|
114845
|
-
|
|
114846
|
-
await execGit(conventions.repoCachePath, [
|
|
114847
|
-
"worktree",
|
|
114848
|
-
"add",
|
|
114849
|
-
worktreePath,
|
|
114850
|
-
"-b",
|
|
114851
|
-
branchName,
|
|
114852
|
-
`origin/${conventions.defaultBranch}`
|
|
114853
|
-
]);
|
|
114854
|
-
console.log(
|
|
114855
|
-
`[bridge.handler] created worktree ${worktreePath} on branch ${branchName}`
|
|
114938
|
+
for (const repo of isAgentWorkspace ? [] : extraRepos) {
|
|
114939
|
+
await ensureRepoClone(
|
|
114940
|
+
repo.cachePath,
|
|
114941
|
+
repo.url,
|
|
114942
|
+
this.deps.gitlabToken,
|
|
114943
|
+
repo.slug
|
|
114856
114944
|
);
|
|
114857
|
-
|
|
114858
|
-
|
|
114859
|
-
|
|
114860
|
-
console.
|
|
114861
|
-
`[bridge.handler]
|
|
114945
|
+
try {
|
|
114946
|
+
await execGit(repo.cachePath, ["fetch", "origin", "--quiet"]);
|
|
114947
|
+
} catch (err) {
|
|
114948
|
+
console.warn(
|
|
114949
|
+
`[bridge.handler] extra repo ${repo.slug} fetch failed (continuing):`,
|
|
114950
|
+
err
|
|
114862
114951
|
);
|
|
114863
|
-
}
|
|
114952
|
+
}
|
|
114953
|
+
}
|
|
114954
|
+
let worktreeExists = await pathExists(worktreePath);
|
|
114955
|
+
if (worktreeExists && buildWorktree) {
|
|
114956
|
+
const healthy = await isWorktreeGitHealthy(worktreePath);
|
|
114957
|
+
if (!healthy) {
|
|
114958
|
+
console.warn(
|
|
114959
|
+
`[bridge.handler] worktree ${worktreePath} exists but git health check failed \u2014 removing stale dir and rebuilding (BL-8: migrated worktree with dead .git pointer)`
|
|
114960
|
+
);
|
|
114961
|
+
try {
|
|
114962
|
+
await fs5.rm(worktreePath, { recursive: true, force: true });
|
|
114963
|
+
} catch (rmErr) {
|
|
114964
|
+
console.warn("[bridge.handler] failed to remove stale worktree (will attempt rebuild anyway):", rmErr);
|
|
114965
|
+
}
|
|
114966
|
+
worktreeExists = false;
|
|
114967
|
+
}
|
|
114968
|
+
}
|
|
114969
|
+
if (!worktreeExists) {
|
|
114970
|
+
if (buildWorktree) {
|
|
114971
|
+
const slug = threadId.replace(/^om_/, "").slice(0, 16);
|
|
114972
|
+
const botSegment = this.deps.botConfig?.id ? `${this.deps.botConfig.id}/` : "";
|
|
114973
|
+
const branchName = `larkway/${botSegment}${slug}`;
|
|
114974
|
+
await execGit(conventions.repoCachePath, [
|
|
114975
|
+
"worktree",
|
|
114976
|
+
"add",
|
|
114977
|
+
worktreePath,
|
|
114978
|
+
"-b",
|
|
114979
|
+
branchName,
|
|
114980
|
+
`origin/${conventions.defaultBranch}`
|
|
114981
|
+
]);
|
|
114864
114982
|
console.log(
|
|
114865
|
-
`[bridge.handler] created
|
|
114983
|
+
`[bridge.handler] created worktree ${worktreePath} on branch ${branchName}`
|
|
114866
114984
|
);
|
|
114985
|
+
} else {
|
|
114986
|
+
await fs5.mkdir(worktreePath, { recursive: true });
|
|
114987
|
+
if (conventions.readOnly && conventions.repoCachePath) {
|
|
114988
|
+
console.log(
|
|
114989
|
+
`[bridge.handler] created scratch dir ${worktreePath} (read_only bot: repo read-only at ${conventions.repoCachePath}, no worktree)`
|
|
114990
|
+
);
|
|
114991
|
+
} else {
|
|
114992
|
+
console.log(
|
|
114993
|
+
`[bridge.handler] created scratch dir ${worktreePath} (bot has no repos)`
|
|
114994
|
+
);
|
|
114995
|
+
}
|
|
114867
114996
|
}
|
|
114868
114997
|
}
|
|
114869
|
-
|
|
114870
|
-
|
|
114871
|
-
|
|
114872
|
-
|
|
114873
|
-
|
|
114874
|
-
|
|
114875
|
-
|
|
114876
|
-
}
|
|
114877
|
-
}
|
|
114878
|
-
try {
|
|
114879
|
-
await writeWorktreeSettings(worktreePath, {
|
|
114880
|
-
allowExtra: this.deps.permissionsAllowExtra
|
|
114881
|
-
});
|
|
114882
|
-
} catch (err) {
|
|
114883
|
-
console.warn("[bridge.handler] writeWorktreeSettings failed (continuing):", err);
|
|
114884
|
-
}
|
|
114885
|
-
try {
|
|
114886
|
-
await ensureStateFile(worktreePath);
|
|
114887
|
-
} catch (err) {
|
|
114888
|
-
console.warn("[bridge.handler] ensureStateFile failed (continuing):", err);
|
|
114889
|
-
}
|
|
114890
|
-
if (card) {
|
|
114998
|
+
if (isAgentWorkspace) {
|
|
114999
|
+
await ensureSessionArtifacts({
|
|
115000
|
+
sessionPath: worktreePath,
|
|
115001
|
+
parsed,
|
|
115002
|
+
isNewThread: existing === void 0,
|
|
115003
|
+
larkCliProfile: this.deps.larkCliProfile
|
|
115004
|
+
});
|
|
115005
|
+
}
|
|
114891
115006
|
try {
|
|
114892
|
-
await
|
|
114893
|
-
|
|
114894
|
-
chatId: parsed.chatId,
|
|
114895
|
-
threadId,
|
|
114896
|
-
botId: this.deps.botConfig?.id ?? "",
|
|
114897
|
-
replyInThread,
|
|
114898
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
115007
|
+
await writeWorktreeSettings(worktreePath, {
|
|
115008
|
+
allowExtra: this.deps.permissionsAllowExtra
|
|
114899
115009
|
});
|
|
114900
115010
|
} catch (err) {
|
|
114901
|
-
console.warn("[bridge.handler]
|
|
115011
|
+
console.warn("[bridge.handler] writeWorktreeSettings failed (continuing):", err);
|
|
114902
115012
|
}
|
|
114903
|
-
}
|
|
114904
|
-
if (buildWorktree) {
|
|
114905
115013
|
try {
|
|
114906
|
-
await
|
|
115014
|
+
await ensureStateFile(worktreePath);
|
|
114907
115015
|
} catch (err) {
|
|
114908
|
-
console.warn("[bridge.handler]
|
|
114909
|
-
}
|
|
114910
|
-
|
|
114911
|
-
|
|
114912
|
-
|
|
114913
|
-
|
|
114914
|
-
|
|
114915
|
-
attempt++;
|
|
114916
|
-
const currentIsNewThread = currentExisting === void 0;
|
|
114917
|
-
const prompt = renderPrompt({
|
|
114918
|
-
parsed,
|
|
114919
|
-
isNewThread: currentIsNewThread,
|
|
114920
|
-
conventions: {
|
|
114921
|
-
worktreePath,
|
|
114922
|
-
runtime: conventions.runtime,
|
|
114923
|
-
agentWorkspacePath: conventions.agentWorkspacePath,
|
|
114924
|
-
workspaceSessionPath: isAgentWorkspace ? worktreePath : void 0,
|
|
114925
|
-
workspaceReposPath: conventions.workspaceReposPath,
|
|
114926
|
-
stateFilePath: stateFilePathOf(worktreePath),
|
|
114927
|
-
repoCachePath: conventions.repoCachePath,
|
|
114928
|
-
primaryRepoUrl: conventions.primaryRepoUrl,
|
|
114929
|
-
defaultBranch: conventions.defaultBranch,
|
|
114930
|
-
defaultProjectSlug: conventions.defaultProjectSlug,
|
|
114931
|
-
extraRepoPaths: conventions.extraRepoPaths,
|
|
114932
|
-
devHostname: conventions.devHostname,
|
|
114933
|
-
portRangeStart: conventions.portRangeStart,
|
|
114934
|
-
portRangeEnd: conventions.portRangeEnd,
|
|
114935
|
-
readOnly: conventions.readOnly,
|
|
114936
|
-
gitlabTokenEnvName: conventions.gitlabTokenEnvName
|
|
114937
|
-
},
|
|
114938
|
-
peers: this.deps.peers,
|
|
114939
|
-
turn_taking_limit: this.deps.botConfig?.turn_taking_limit,
|
|
114940
|
-
botName: this.deps.botConfig?.name,
|
|
114941
|
-
backend: this.deps.botConfig?.backend,
|
|
114942
|
-
agentMemory: this.deps.agentMemory,
|
|
114943
|
-
larkCliProfile: this.deps.larkCliProfile,
|
|
114944
|
-
runtimeWarnings: this.runtimeWarnings()
|
|
114945
|
-
});
|
|
114946
|
-
const backend = this.deps.botConfig?.backend ?? "claude";
|
|
114947
|
-
const permissionMode = this.deps.permissionMode ?? (isAgentWorkspace ? "acceptEdits" : "bypassPermissions");
|
|
114948
|
-
const timeoutMs = this.deps.subprocessTimeoutMs ?? 60 * 60 * 1e3;
|
|
114949
|
-
const handle = createRunner(backend).run({
|
|
114950
|
-
prompt,
|
|
114951
|
-
resumeSessionId: currentExisting?.sessionId,
|
|
114952
|
-
permissionMode,
|
|
114953
|
-
timeoutMs,
|
|
114954
|
-
cwd: runCwd,
|
|
114955
|
-
// V2: inject per-bot git identity; absent in V1 → runner.ts uses "larkway-bot" fallback
|
|
114956
|
-
botGitIdentity: this.deps.botConfig?.git_identity,
|
|
114957
|
-
gitlabToken: this.deps.gitlabToken
|
|
114958
|
-
});
|
|
114959
|
-
let sessionId;
|
|
114960
|
-
let lastText = "";
|
|
114961
|
-
try {
|
|
114962
|
-
for await (const ev of handle.events) {
|
|
114963
|
-
if (card) card.handle(ev);
|
|
114964
|
-
if (ev.type === "system_init") {
|
|
114965
|
-
sessionId = ev.sessionId;
|
|
114966
|
-
}
|
|
114967
|
-
if (ev.type === "text_delta") {
|
|
114968
|
-
lastText = ev.text;
|
|
114969
|
-
}
|
|
114970
|
-
}
|
|
114971
|
-
const result = await handle.done;
|
|
114972
|
-
const rawReportedState = await readStateFile(worktreePath);
|
|
114973
|
-
const reportedState = rawReportedState?.updated_at != null && rawReportedState.updated_at !== preRunUpdatedAt ? rawReportedState : null;
|
|
114974
|
-
const now = Date.now();
|
|
114975
|
-
if (sessionId !== void 0 && currentExisting === void 0) {
|
|
114976
|
-
await this.deps.sessionStore.put({
|
|
114977
|
-
threadId,
|
|
114978
|
-
sessionId,
|
|
114979
|
-
botId,
|
|
114980
|
-
createdTs: now,
|
|
114981
|
-
lastActiveTs: now,
|
|
114982
|
-
senderOpenId
|
|
114983
|
-
});
|
|
114984
|
-
} else if (sessionId !== void 0 && currentExisting !== void 0) {
|
|
114985
|
-
await this.deps.sessionStore.put({
|
|
115016
|
+
console.warn("[bridge.handler] ensureStateFile failed (continuing):", err);
|
|
115017
|
+
}
|
|
115018
|
+
if (card) {
|
|
115019
|
+
try {
|
|
115020
|
+
await writeCardFile(worktreePath, {
|
|
115021
|
+
messageId: card.messageId,
|
|
115022
|
+
chatId: parsed.chatId,
|
|
114986
115023
|
threadId,
|
|
114987
|
-
|
|
114988
|
-
|
|
114989
|
-
|
|
114990
|
-
lastActiveTs: now,
|
|
114991
|
-
senderOpenId
|
|
114992
|
-
});
|
|
114993
|
-
} else if (currentExisting !== void 0 && sessionId === void 0) {
|
|
114994
|
-
await this.deps.sessionStore.put({
|
|
114995
|
-
...currentExisting,
|
|
114996
|
-
lastActiveTs: now
|
|
115024
|
+
botId: this.deps.botConfig?.id ?? "",
|
|
115025
|
+
replyInThread,
|
|
115026
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
114997
115027
|
});
|
|
115028
|
+
} catch (err) {
|
|
115029
|
+
console.warn("[bridge.handler] writeCardFile failed (continuing):", err);
|
|
114998
115030
|
}
|
|
114999
|
-
|
|
115000
|
-
|
|
115001
|
-
|
|
115002
|
-
|
|
115003
|
-
|
|
115004
|
-
|
|
115005
|
-
success = false;
|
|
115006
|
-
failureReason = reportedError ?? "bot \u62A5\u544A failed (\u65E0 error \u5B57\u6BB5)";
|
|
115007
|
-
} else if (reportedStatus === "ready") {
|
|
115008
|
-
success = true;
|
|
115009
|
-
} else if (result.exitCode === 0) {
|
|
115010
|
-
success = true;
|
|
115011
|
-
} else {
|
|
115012
|
-
success = false;
|
|
115013
|
-
failureReason = `claude exited ${result.exitCode} \u4E14 bot \u672A\u66F4\u65B0 state.json status \u2014 \u53EF\u80FD\u5D29\u6E83`;
|
|
115014
|
-
}
|
|
115015
|
-
const cardBody = reportedState?.last_message ?? (lastText.trim() ? lastText : "\u26A0\uFE0F \u672C\u8F6E\u6CA1\u6709\u62FF\u5230 agent \u7684\u65B0\u56DE\u590D(\u53EF\u80FD\u88AB\u4E2D\u65AD\u6216\u672A\u66F4\u65B0\u72B6\u6001),\u518D @ \u6211\u4E00\u6B21\u91CD\u8BD5\u3002");
|
|
115016
|
-
const noReportThisTurn = reportedState === null;
|
|
115017
|
-
const neutralTitle = noReportThisTurn && success && !failureReason ? "\u{1F4AC} \u5DF2\u56DE\u590D" : void 0;
|
|
115018
|
-
await card.finalize({
|
|
115019
|
-
finalText: cardBody,
|
|
115020
|
-
success,
|
|
115021
|
-
failureReason,
|
|
115022
|
-
titleOverride: reportedState?.card_title ?? neutralTitle,
|
|
115023
|
-
colorOverride: reportedState?.card_color ?? (neutralTitle ? "neutral" : void 0),
|
|
115024
|
-
// V2 dynamic-choice buttons — agent-declared, rendered verbatim.
|
|
115025
|
-
// reportedState is null when state.json wasn't freshly written
|
|
115026
|
-
// (stale-guard), so stale leftover choices never reappear.
|
|
115027
|
-
choices: reportedState?.choices,
|
|
115028
|
-
choicePrompt: reportedState?.choice_prompt
|
|
115029
|
-
});
|
|
115030
|
-
await deleteCardFile(worktreePath);
|
|
115031
|
+
}
|
|
115032
|
+
if (buildWorktree) {
|
|
115033
|
+
try {
|
|
115034
|
+
await ensureNodeModules(worktreePath);
|
|
115035
|
+
} catch (err) {
|
|
115036
|
+
console.warn("[bridge.handler] ensureNodeModules failed (continuing):", err);
|
|
115031
115037
|
}
|
|
115032
|
-
|
|
115033
|
-
|
|
115034
|
-
|
|
115035
|
-
|
|
115036
|
-
|
|
115037
|
-
|
|
115038
|
-
|
|
115038
|
+
}
|
|
115039
|
+
const preRunUpdatedAt = (await readStateFile(worktreePath))?.updated_at;
|
|
115040
|
+
let currentExisting = existing;
|
|
115041
|
+
let attempt = 0;
|
|
115042
|
+
while (true) {
|
|
115043
|
+
attempt++;
|
|
115044
|
+
const currentIsNewThread = currentExisting === void 0;
|
|
115045
|
+
const prompt = renderPrompt({
|
|
115046
|
+
parsed,
|
|
115047
|
+
isNewThread: currentIsNewThread,
|
|
115048
|
+
conventions: {
|
|
115049
|
+
worktreePath,
|
|
115050
|
+
runtime: conventions.runtime,
|
|
115051
|
+
agentWorkspacePath: conventions.agentWorkspacePath,
|
|
115052
|
+
workspaceSessionPath: isAgentWorkspace ? worktreePath : void 0,
|
|
115053
|
+
workspaceReposPath: conventions.workspaceReposPath,
|
|
115054
|
+
stateFilePath: stateFilePathOf(worktreePath),
|
|
115055
|
+
repoCachePath: conventions.repoCachePath,
|
|
115056
|
+
primaryRepoUrl: conventions.primaryRepoUrl,
|
|
115057
|
+
defaultBranch: conventions.defaultBranch,
|
|
115058
|
+
defaultProjectSlug: conventions.defaultProjectSlug,
|
|
115059
|
+
extraRepoPaths: conventions.extraRepoPaths,
|
|
115060
|
+
devHostname: conventions.devHostname,
|
|
115061
|
+
portRangeStart: conventions.portRangeStart,
|
|
115062
|
+
portRangeEnd: conventions.portRangeEnd,
|
|
115063
|
+
readOnly: conventions.readOnly,
|
|
115064
|
+
gitlabTokenEnvName: conventions.gitlabTokenEnvName
|
|
115065
|
+
},
|
|
115066
|
+
peers: this.deps.peers,
|
|
115067
|
+
turn_taking_limit: this.deps.botConfig?.turn_taking_limit,
|
|
115068
|
+
botName: this.deps.botConfig?.name,
|
|
115069
|
+
backend: this.deps.botConfig?.backend,
|
|
115070
|
+
agentMemory: this.deps.agentMemory,
|
|
115071
|
+
larkCliProfile: this.deps.larkCliProfile,
|
|
115072
|
+
runtimeWarnings: this.runtimeWarnings()
|
|
115039
115073
|
});
|
|
115040
|
-
|
|
115041
|
-
|
|
115042
|
-
const
|
|
115043
|
-
|
|
115044
|
-
|
|
115045
|
-
|
|
115046
|
-
|
|
115047
|
-
|
|
115048
|
-
|
|
115049
|
-
|
|
115074
|
+
const backend = this.deps.botConfig?.backend ?? "claude";
|
|
115075
|
+
const permissionMode = this.deps.permissionMode ?? (isAgentWorkspace ? "acceptEdits" : "bypassPermissions");
|
|
115076
|
+
const timeoutMs = this.deps.subprocessTimeoutMs ?? 60 * 60 * 1e3;
|
|
115077
|
+
const handle = createRunner(backend).run({
|
|
115078
|
+
prompt,
|
|
115079
|
+
resumeSessionId: currentExisting?.sessionId,
|
|
115080
|
+
permissionMode,
|
|
115081
|
+
timeoutMs,
|
|
115082
|
+
cwd: runCwd,
|
|
115083
|
+
// V2: inject per-bot git identity; absent in V1 → runner.ts uses "larkway-bot" fallback
|
|
115084
|
+
botGitIdentity: this.deps.botConfig?.git_identity,
|
|
115085
|
+
gitlabToken: this.deps.gitlabToken
|
|
115086
|
+
});
|
|
115087
|
+
let sessionId;
|
|
115088
|
+
let lastText = "";
|
|
115089
|
+
try {
|
|
115090
|
+
for await (const ev of handle.events) {
|
|
115091
|
+
if (card) card.handle(ev);
|
|
115092
|
+
if (ev.type === "system_init") {
|
|
115093
|
+
sessionId = ev.sessionId;
|
|
115094
|
+
}
|
|
115095
|
+
if (ev.type === "text_delta") {
|
|
115096
|
+
lastText = ev.text;
|
|
115097
|
+
}
|
|
115098
|
+
}
|
|
115099
|
+
const result = await handle.done;
|
|
115100
|
+
const rawReportedState = await readStateFile(worktreePath);
|
|
115101
|
+
const reportedState = rawReportedState?.updated_at != null && rawReportedState.updated_at !== preRunUpdatedAt ? rawReportedState : null;
|
|
115102
|
+
const now = Date.now();
|
|
115103
|
+
if (sessionId !== void 0 && currentExisting === void 0) {
|
|
115104
|
+
await this.deps.sessionStore.put({
|
|
115105
|
+
threadId,
|
|
115106
|
+
sessionId,
|
|
115107
|
+
botId,
|
|
115108
|
+
createdTs: now,
|
|
115109
|
+
lastActiveTs: now,
|
|
115110
|
+
senderOpenId
|
|
115111
|
+
});
|
|
115112
|
+
} else if (sessionId !== void 0 && currentExisting !== void 0) {
|
|
115113
|
+
await this.deps.sessionStore.put({
|
|
115114
|
+
threadId,
|
|
115115
|
+
sessionId,
|
|
115116
|
+
botId,
|
|
115117
|
+
createdTs: currentExisting.createdTs,
|
|
115118
|
+
lastActiveTs: now,
|
|
115119
|
+
senderOpenId
|
|
115120
|
+
});
|
|
115121
|
+
} else if (currentExisting !== void 0 && sessionId === void 0) {
|
|
115122
|
+
await this.deps.sessionStore.put({
|
|
115123
|
+
...currentExisting,
|
|
115124
|
+
lastActiveTs: now
|
|
115125
|
+
});
|
|
115126
|
+
}
|
|
115127
|
+
if (card) {
|
|
115128
|
+
const reportedStatus = reportedState?.status;
|
|
115129
|
+
const reportedError = reportedState?.error;
|
|
115130
|
+
let success;
|
|
115131
|
+
let failureReason;
|
|
115132
|
+
if (reportedStatus === "failed") {
|
|
115133
|
+
success = false;
|
|
115134
|
+
failureReason = reportedError ?? "bot \u62A5\u544A failed (\u65E0 error \u5B57\u6BB5)";
|
|
115135
|
+
} else if (reportedStatus === "ready") {
|
|
115136
|
+
success = true;
|
|
115137
|
+
} else if (result.exitCode === 0) {
|
|
115138
|
+
success = true;
|
|
115139
|
+
} else {
|
|
115140
|
+
success = false;
|
|
115141
|
+
failureReason = `claude exited ${result.exitCode} \u4E14 bot \u672A\u66F4\u65B0 state.json status \u2014 \u53EF\u80FD\u5D29\u6E83`;
|
|
115142
|
+
}
|
|
115143
|
+
const cardBody = reportedState?.last_message ?? (lastText.trim() ? lastText : "\u26A0\uFE0F \u672C\u8F6E\u6CA1\u6709\u62FF\u5230 agent \u7684\u65B0\u56DE\u590D(\u53EF\u80FD\u88AB\u4E2D\u65AD\u6216\u672A\u66F4\u65B0\u72B6\u6001),\u518D @ \u6211\u4E00\u6B21\u91CD\u8BD5\u3002");
|
|
115144
|
+
const noReportThisTurn = reportedState === null;
|
|
115145
|
+
const neutralTitle = noReportThisTurn && success && !failureReason ? "\u{1F4AC} \u5DF2\u56DE\u590D" : void 0;
|
|
115146
|
+
await card.finalize({
|
|
115147
|
+
finalText: cardBody,
|
|
115148
|
+
success,
|
|
115149
|
+
failureReason,
|
|
115150
|
+
titleOverride: reportedState?.card_title ?? neutralTitle,
|
|
115151
|
+
colorOverride: reportedState?.card_color ?? (neutralTitle ? "neutral" : void 0),
|
|
115152
|
+
// V2 dynamic-choice buttons — agent-declared, rendered verbatim.
|
|
115153
|
+
// reportedState is null when state.json wasn't freshly written
|
|
115154
|
+
// (stale-guard), so stale leftover choices never reappear.
|
|
115155
|
+
choices: reportedState?.choices,
|
|
115156
|
+
choicePrompt: reportedState?.choice_prompt
|
|
115157
|
+
});
|
|
115158
|
+
await deleteCardFile(worktreePath);
|
|
115159
|
+
}
|
|
115160
|
+
settle(true);
|
|
115161
|
+
await recordEvent({
|
|
115162
|
+
status: "completed",
|
|
115163
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
115164
|
+
durationMs: Date.now() - eventStartedAt,
|
|
115165
|
+
appendPath: "\u5DF2\u5B8C\u6210",
|
|
115166
|
+
reason: "Agent \u5DF2\u7ED3\u675F\uFF0C\u6D88\u606F\u5DF2\u786E\u8BA4\u3002"
|
|
115167
|
+
});
|
|
115168
|
+
break;
|
|
115169
|
+
} catch (spawnErr) {
|
|
115170
|
+
const errMsg = String(spawnErr.message ?? spawnErr);
|
|
115171
|
+
if (attempt === 1 && currentExisting != null && errMsg.includes("No conversation found")) {
|
|
115172
|
+
console.warn(
|
|
115173
|
+
`[bridge.handler] stale session ${currentExisting.sessionId} for thread ${threadId} \u2014 removing and retrying without --resume`
|
|
115174
|
+
);
|
|
115175
|
+
await this.deps.sessionStore.delete(threadId, botId);
|
|
115176
|
+
currentExisting = void 0;
|
|
115177
|
+
continue;
|
|
115178
|
+
}
|
|
115179
|
+
throw spawnErr;
|
|
115050
115180
|
}
|
|
115051
|
-
throw spawnErr;
|
|
115052
115181
|
}
|
|
115053
|
-
}
|
|
115054
|
-
|
|
115055
|
-
|
|
115056
|
-
|
|
115057
|
-
|
|
115058
|
-
|
|
115059
|
-
|
|
115060
|
-
|
|
115061
|
-
|
|
115062
|
-
|
|
115063
|
-
|
|
115064
|
-
|
|
115065
|
-
|
|
115066
|
-
|
|
115067
|
-
|
|
115068
|
-
|
|
115069
|
-
|
|
115070
|
-
|
|
115071
|
-
|
|
115072
|
-
})
|
|
115073
|
-
|
|
115074
|
-
|
|
115182
|
+
} catch (err) {
|
|
115183
|
+
console.error("[bridge.handler] handleOne failed for thread", threadId, err);
|
|
115184
|
+
await this.deps.client.removeProcessingReaction?.(messageId);
|
|
115185
|
+
await recordEvent({
|
|
115186
|
+
status: "failed",
|
|
115187
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
115188
|
+
durationMs: Date.now() - eventStartedAt,
|
|
115189
|
+
appendPath: "\u5F02\u5E38",
|
|
115190
|
+
reason: String(err)
|
|
115191
|
+
});
|
|
115192
|
+
settle(false);
|
|
115193
|
+
if (card) {
|
|
115194
|
+
try {
|
|
115195
|
+
await card.finalize({
|
|
115196
|
+
success: false,
|
|
115197
|
+
failureReason: String(err)
|
|
115198
|
+
// No choices on the hard-crash path: reportedState isn't in scope
|
|
115199
|
+
// here, and a crashed turn offering pick-an-option buttons is wrong.
|
|
115200
|
+
});
|
|
115201
|
+
} catch (finalizeErr) {
|
|
115202
|
+
console.error("[bridge.handler] finalize(failure) also failed:", finalizeErr);
|
|
115203
|
+
}
|
|
115204
|
+
const wtPath = this.deps.conventions.runtime === "agent_workspace" && this.deps.conventions.workspaceSessionsDir ? path8.join(this.deps.conventions.workspaceSessionsDir, threadId) : path8.join(this.deps.conventions.worktreesDir, threadId);
|
|
115205
|
+
await deleteCardFile(wtPath);
|
|
115075
115206
|
}
|
|
115076
|
-
const wtPath = this.deps.conventions.runtime === "agent_workspace" && this.deps.conventions.workspaceSessionsDir ? path8.join(this.deps.conventions.workspaceSessionsDir, threadId) : path8.join(this.deps.conventions.worktreesDir, threadId);
|
|
115077
|
-
await deleteCardFile(wtPath);
|
|
115078
115207
|
}
|
|
115208
|
+
} finally {
|
|
115209
|
+
settle(false);
|
|
115079
115210
|
}
|
|
115080
115211
|
}
|
|
115081
115212
|
};
|