larkway 0.3.36 → 0.3.38
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 +42 -3
- package/README.zh.md +1 -1
- package/dist/cli/index.js +159 -26
- package/dist/main.js +330 -77
- 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.38**
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
@@ -105,6 +105,45 @@ Larkway never injects `ANTHROPIC_API_KEY` or any other API key. The subprocess i
|
|
|
105
105
|
|
|
106
106
|
---
|
|
107
107
|
|
|
108
|
+
## Security model — read this before inviting the bot anywhere
|
|
109
|
+
|
|
110
|
+
Larkway runs a real coding agent **on your machine, with your permissions**. Be
|
|
111
|
+
explicit about what that means:
|
|
112
|
+
|
|
113
|
+
**Who can trigger it.** By default a bot ships with `chats: []` — *open mode*:
|
|
114
|
+
it responds to an @-mention in **any** group it has been added to. Anyone who
|
|
115
|
+
can add the bot to a group (or is in a group with it) can make it act. To
|
|
116
|
+
narrow this, list allowed group ids in the bot's yaml (`chats: [oc_…]`); the
|
|
117
|
+
bot then ignores every other chat.
|
|
118
|
+
|
|
119
|
+
**What a triggered agent can do.** The agent subprocess is not sandboxed by
|
|
120
|
+
Larkway. With the default permission mode (`bypassPermissions` — required for
|
|
121
|
+
unattended headless operation, where an interactive permission prompt would
|
|
122
|
+
silently hang the turn), a Claude-backend agent can read/write files, run
|
|
123
|
+
commands, and use the network as your OS user. The Codex backend's equivalent
|
|
124
|
+
default is full access. You can tighten this globally via `permissions.mode`
|
|
125
|
+
in `~/.larkway/config.json` (`acceptEdits` / `ask`), at the cost of turns
|
|
126
|
+
stalling on operations the mode blocks. **Note the mode words are Claude
|
|
127
|
+
semantics; on Codex anything other than `ask` currently maps to full access.**
|
|
128
|
+
|
|
129
|
+
**Prompt injection is real.** Everything in the triggering message — and
|
|
130
|
+
thread history / attachments the agent chooses to read — becomes agent input.
|
|
131
|
+
A malicious group member can try to steer the agent ("ignore your instructions
|
|
132
|
+
and run …"). L2 memory and your repo's `CLAUDE.md`/`AGENTS.md` are guidance,
|
|
133
|
+
not enforcement.
|
|
134
|
+
|
|
135
|
+
**Practical posture.**
|
|
136
|
+
- Treat "which groups is this bot in" as the primary security boundary; use
|
|
137
|
+
`chats:` allowlists for any bot with write access.
|
|
138
|
+
- Run write-capable bots against repos where an unwanted commit/MR is
|
|
139
|
+
reviewable and revertable; keep production credentials out of the host's
|
|
140
|
+
environment.
|
|
141
|
+
- Secrets in `~/.larkway/.env` are shared across all bots on the bridge and
|
|
142
|
+
visible to any agent that can read your home directory — don't mix a
|
|
143
|
+
low-trust open bot and high-value credentials on the same host.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
108
147
|
## Defining a bot (three layers)
|
|
109
148
|
|
|
110
149
|
| Layer | What it is | Where it lives |
|
|
@@ -122,7 +161,7 @@ Secrets live only in `~/.larkway/.env` (mode 0600). Config and memory contain no
|
|
|
122
161
|
- **Multiple bots on one bridge** — a read-only Q&A bot and a write-capable engineering bot can share the same process, each with its own L1/L2/L3 definition
|
|
123
162
|
- **Web UI** — `larkway ui` opens a local management dashboard (127.0.0.1 + token); create bots, edit memory, watch live logs
|
|
124
163
|
- **Session continuity** — every Feishu thread maps to a persistent `session_id`; the agent remembers what it did in prior turns
|
|
125
|
-
- **Agent Workspace** — per-thread
|
|
164
|
+
- **Agent Workspace** — each bot gets its own workspace where the agent clones the repo itself, with per-thread session dirs; concurrent threads don't trip over each other's git state (expect disk usage and a slower first turn on large repos — clones are per-session, GC'd after 24h idle)
|
|
126
165
|
- **Codex runtime pre-checks** — `larkway doctor` validates Codex state directory writability before start
|
|
127
166
|
- **Topic ↔ Feishu task handle** — turn a topic into a Feishu task and the agent claims it, then keeps its lifecycle (done/failed/reopened, stalled, handed off, overdue) in sync automatically — see below
|
|
128
167
|
|
|
@@ -157,7 +196,7 @@ platform-fact writeups: [docs/task-handle.md](docs/task-handle.md).
|
|
|
157
196
|
|
|
158
197
|
- **Node.js 20+ LTS**
|
|
159
198
|
- **A Claude Code or Codex subscription** with local CLI installed and logged in
|
|
160
|
-
- **`lark-cli`** — Feishu long-connection client and message utilities
|
|
199
|
+
- **`lark-cli`** — Feishu long-connection client and message utilities: `npm i -g @larksuite/cli`, then `lark-cli auth login` (the agent uses it to read thread history and attachments; `larkway doctor` checks it's present)
|
|
161
200
|
- **`glab` + `git`** — for bots that open MRs (optional for read-only bots)
|
|
162
201
|
- **An always-on host machine** — the bridge must stay running to receive Feishu events; a laptop that sleeps will miss messages; a small server or desktop works well
|
|
163
202
|
|
package/README.zh.md
CHANGED
package/dist/cli/index.js
CHANGED
|
@@ -25413,13 +25413,13 @@ var require_lib2 = __commonJS({
|
|
|
25413
25413
|
Domain[Domain["Lark"] = 1] = "Lark";
|
|
25414
25414
|
})(exports.Domain || (exports.Domain = {}));
|
|
25415
25415
|
exports.LoggerLevel = void 0;
|
|
25416
|
-
(function(
|
|
25417
|
-
|
|
25418
|
-
|
|
25419
|
-
|
|
25420
|
-
|
|
25421
|
-
|
|
25422
|
-
|
|
25416
|
+
(function(LoggerLevel) {
|
|
25417
|
+
LoggerLevel[LoggerLevel["fatal"] = 0] = "fatal";
|
|
25418
|
+
LoggerLevel[LoggerLevel["error"] = 1] = "error";
|
|
25419
|
+
LoggerLevel[LoggerLevel["warn"] = 2] = "warn";
|
|
25420
|
+
LoggerLevel[LoggerLevel["info"] = 3] = "info";
|
|
25421
|
+
LoggerLevel[LoggerLevel["debug"] = 4] = "debug";
|
|
25422
|
+
LoggerLevel[LoggerLevel["trace"] = 5] = "trace";
|
|
25423
25423
|
})(exports.LoggerLevel || (exports.LoggerLevel = {}));
|
|
25424
25424
|
var CEventType = /* @__PURE__ */ Symbol("event-type");
|
|
25425
25425
|
var CTenantKey = /* @__PURE__ */ Symbol("tenant-key");
|
|
@@ -112719,15 +112719,27 @@ var init_channelClient = __esm({
|
|
|
112719
112719
|
inFlightMessageIds = /* @__PURE__ */ new Set();
|
|
112720
112720
|
/**
|
|
112721
112721
|
* Per-message (re-)dispatch counter for the poison-message guard. Incremented
|
|
112722
|
-
* every time a message_id is pushed onto the inbound
|
|
112723
|
-
* gap-fill branch) and
|
|
112724
|
-
*
|
|
112725
|
-
*
|
|
112726
|
-
*
|
|
112722
|
+
* ONCE per dispatch — every time a message_id is pushed onto the inbound
|
|
112723
|
+
* queue (live WS or either gap-fill branch), and nowhere else, so the cap
|
|
112724
|
+
* really means "N dispatches". When a turn fails and the count has reached
|
|
112725
|
+
* {@link MAX_MESSAGE_ATTEMPTS}, markUnhandled GIVES UP: it promotes the
|
|
112726
|
+
* message to seen (so it stops being re-dispatched) and logs a warning.
|
|
112727
|
+
* markHandled clears the entry on terminal success. Not persisted:
|
|
112727
112728
|
* post-restart, an interrupted message starts fresh — same policy as
|
|
112728
112729
|
* inFlightMessageIds.
|
|
112729
112730
|
*/
|
|
112730
112731
|
messageAttempts = /* @__PURE__ */ new Map();
|
|
112732
|
+
/**
|
|
112733
|
+
* chatId + create_time for every in-flight message, captured at dispatch.
|
|
112734
|
+
* Consumed by {@link markUnhandled}: a FAILED turn records an unresolved gap
|
|
112735
|
+
* window for its chat so a later replay actually re-pulls (and re-dispatches)
|
|
112736
|
+
* it. Without this there is no steady-state trigger for the re-dispatch that
|
|
112737
|
+
* markUnhandled promises — chats-mode bots have no discovery timer at all,
|
|
112738
|
+
* and open-mode steady-state discovery pulls 0 for already-known chats — so
|
|
112739
|
+
* a turn failing before any visible surface existed was silently dropped.
|
|
112740
|
+
* Cleared in markHandled / markUnhandled; bounded by the in-flight set.
|
|
112741
|
+
*/
|
|
112742
|
+
inFlightMessageMeta = /* @__PURE__ */ new Map();
|
|
112731
112743
|
/**
|
|
112732
112744
|
* Chats observed from live WS events during this process lifetime.
|
|
112733
112745
|
*
|
|
@@ -112762,6 +112774,17 @@ var init_channelClient = __esm({
|
|
|
112762
112774
|
openChatDiscoveryTimer = null;
|
|
112763
112775
|
openChatDiscoveryRunning = false;
|
|
112764
112776
|
openChatDiscoveryBootstrapped = false;
|
|
112777
|
+
/**
|
|
112778
|
+
* Chats-mode (allowedChatIds non-empty) counterpart of the open-mode
|
|
112779
|
+
* discovery cycle's unresolved-window replay: those bots never start
|
|
112780
|
+
* discovery, so without this timer a window recorded by markUnhandled had
|
|
112781
|
+
* NO trigger to replay it (the only other gapFill source is a WS reconnect,
|
|
112782
|
+
* which a healthy network may not produce for days). Steady state with no
|
|
112783
|
+
* pending windows does nothing — zero API calls — so the 0.3.28 storm fix
|
|
112784
|
+
* is preserved.
|
|
112785
|
+
*/
|
|
112786
|
+
unresolvedReplayTimer = null;
|
|
112787
|
+
unresolvedReplayRunning = false;
|
|
112765
112788
|
/**
|
|
112766
112789
|
* Consecutive discovery-cycle failures. Used to SKIP cycles with exponential
|
|
112767
112790
|
* backoff (storm: a failing +chat-list/gap-fill shouldn't re-fire every
|
|
@@ -112967,6 +112990,7 @@ var init_channelClient = __esm({
|
|
|
112967
112990
|
return;
|
|
112968
112991
|
}
|
|
112969
112992
|
this.inFlightMessageIds.add(ev.message_id);
|
|
112993
|
+
this.noteInFlightMeta(ev);
|
|
112970
112994
|
this.noteDispatchAttempt(ev.message_id);
|
|
112971
112995
|
log(`dispatching (channel-sdk): message_id=${ev.message_id} thread=${ev.thread_id ?? "?"}`);
|
|
112972
112996
|
this.queue.push(ev);
|
|
@@ -112995,6 +113019,7 @@ var init_channelClient = __esm({
|
|
|
112995
113019
|
this.connected = true;
|
|
112996
113020
|
log(`connected as ${channel.botIdentity?.name ?? "?"} (${channel.botIdentity?.openId ?? "?"})`);
|
|
112997
113021
|
this.startOpenChatDiscovery(log);
|
|
113022
|
+
this.startUnresolvedReplayTimer(log);
|
|
112998
113023
|
}
|
|
112999
113024
|
/**
|
|
113000
113025
|
* Turn a card-button click into a synthesized LarkMessageEvent pushed onto the
|
|
@@ -113235,6 +113260,7 @@ var init_channelClient = __esm({
|
|
|
113235
113260
|
})();
|
|
113236
113261
|
if (!fallbackEv) continue;
|
|
113237
113262
|
this.inFlightMessageIds.add(fallbackEv.message_id);
|
|
113263
|
+
this.noteInFlightMeta(fallbackEv);
|
|
113238
113264
|
this.noteDispatchAttempt(fallbackEv.message_id);
|
|
113239
113265
|
log(`gap-fill dispatching (fallback): message_id=${fallbackEv.message_id} chat=${chatId}`);
|
|
113240
113266
|
this.queue.push(fallbackEv);
|
|
@@ -113242,6 +113268,7 @@ var init_channelClient = __esm({
|
|
|
113242
113268
|
continue;
|
|
113243
113269
|
}
|
|
113244
113270
|
this.inFlightMessageIds.add(ev.message_id);
|
|
113271
|
+
this.noteInFlightMeta(ev);
|
|
113245
113272
|
this.noteDispatchAttempt(ev.message_id);
|
|
113246
113273
|
log(`gap-fill dispatching: message_id=${ev.message_id} thread=${ev.thread_id ?? "?"} chat=${chatId}`);
|
|
113247
113274
|
this.queue.push(ev);
|
|
@@ -113332,6 +113359,37 @@ var init_channelClient = __esm({
|
|
|
113332
113359
|
if (tracked === void 0) return;
|
|
113333
113360
|
if (coveredFrom <= tracked) this.unresolvedGapWindowByChat.delete(chatId);
|
|
113334
113361
|
}
|
|
113362
|
+
/**
|
|
113363
|
+
* Chats-mode replay loop (see {@link unresolvedReplayTimer}): every discovery
|
|
113364
|
+
* interval, IF any unresolved gap window is pending, gapFill exactly those
|
|
113365
|
+
* chats. Open-mode bots don't need this — their discovery cycle already
|
|
113366
|
+
* includes unresolved-window chats in its target set.
|
|
113367
|
+
*/
|
|
113368
|
+
startUnresolvedReplayTimer(log) {
|
|
113369
|
+
if (this.opts.allowedChatIds.size === 0) return;
|
|
113370
|
+
if (this.unresolvedReplayTimer) return;
|
|
113371
|
+
const intervalMs = resolveOpenChatDiscoveryMs(this.opts.openChatDiscoveryMs);
|
|
113372
|
+
if (intervalMs <= 0) return;
|
|
113373
|
+
this.unresolvedReplayTimer = setInterval(() => {
|
|
113374
|
+
void this.replayUnresolvedWindows(log);
|
|
113375
|
+
}, intervalMs);
|
|
113376
|
+
this.unresolvedReplayTimer.unref?.();
|
|
113377
|
+
}
|
|
113378
|
+
async replayUnresolvedWindows(log) {
|
|
113379
|
+
if (this.closed || this.unresolvedReplayRunning) return;
|
|
113380
|
+
this.pruneUnresolvedGapWindows(Date.now());
|
|
113381
|
+
if (this.unresolvedGapWindowByChat.size === 0) return;
|
|
113382
|
+
this.unresolvedReplayRunning = true;
|
|
113383
|
+
try {
|
|
113384
|
+
const chats = new Set(this.unresolvedGapWindowByChat.keys());
|
|
113385
|
+
log(`unresolved-window replay: re-pulling ${chats.size} chat(s)`);
|
|
113386
|
+
await this.gapFill(Date.now(), log, chats);
|
|
113387
|
+
} catch (e) {
|
|
113388
|
+
log(`unresolved-window replay failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
113389
|
+
} finally {
|
|
113390
|
+
this.unresolvedReplayRunning = false;
|
|
113391
|
+
}
|
|
113392
|
+
}
|
|
113335
113393
|
startOpenChatDiscovery(log) {
|
|
113336
113394
|
if (this.opts.allowedChatIds.size > 0) return;
|
|
113337
113395
|
if (this.openChatDiscoveryTimer) return;
|
|
@@ -113601,6 +113659,7 @@ var init_channelClient = __esm({
|
|
|
113601
113659
|
*/
|
|
113602
113660
|
markHandled(messageId) {
|
|
113603
113661
|
this.inFlightMessageIds.delete(messageId);
|
|
113662
|
+
this.inFlightMessageMeta.delete(messageId);
|
|
113604
113663
|
this.messageAttempts.delete(messageId);
|
|
113605
113664
|
this.noteSeenMessage(messageId);
|
|
113606
113665
|
}
|
|
@@ -113610,38 +113669,63 @@ var init_channelClient = __esm({
|
|
|
113610
113669
|
* self-heal — one transient blip no longer swallows the @ forever). Does not
|
|
113611
113670
|
* touch persisted seen state.
|
|
113612
113671
|
*
|
|
113613
|
-
* Poison-message guard:
|
|
113614
|
-
*
|
|
113615
|
-
*
|
|
113616
|
-
*
|
|
113672
|
+
* Poison-message guard: if the message has already been dispatched
|
|
113673
|
+
* {@link MAX_MESSAGE_ATTEMPTS} times, GIVE UP — promote it to seen (so it
|
|
113674
|
+
* stops being re-dispatched on every gap-fill) and log a visible warning
|
|
113675
|
+
* instead of silently looping forever. Otherwise, record an unresolved gap
|
|
113676
|
+
* window for the message's chat so the next replay cycle re-dispatches it.
|
|
113617
113677
|
*/
|
|
113618
|
-
markUnhandled(messageId) {
|
|
113678
|
+
markUnhandled(messageId, opts) {
|
|
113619
113679
|
this.inFlightMessageIds.delete(messageId);
|
|
113620
|
-
const
|
|
113621
|
-
this.
|
|
113680
|
+
const meta = this.inFlightMessageMeta.get(messageId);
|
|
113681
|
+
this.inFlightMessageMeta.delete(messageId);
|
|
113682
|
+
const attempts = this.messageAttempts.get(messageId) ?? 0;
|
|
113622
113683
|
if (attempts >= MAX_MESSAGE_ATTEMPTS) {
|
|
113623
113684
|
console.warn(
|
|
113624
113685
|
`[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)`
|
|
113625
113686
|
);
|
|
113626
113687
|
this.messageAttempts.delete(messageId);
|
|
113627
113688
|
this.noteSeenMessage(messageId);
|
|
113689
|
+
return;
|
|
113690
|
+
}
|
|
113691
|
+
if (meta && opts?.replay !== false) {
|
|
113692
|
+
this.recordUnresolvedGapWindow(
|
|
113693
|
+
meta.chatId,
|
|
113694
|
+
meta.createTimeMs - 1e3,
|
|
113695
|
+
(s) => console.log(`[channel.client] ${s}`)
|
|
113696
|
+
);
|
|
113628
113697
|
}
|
|
113629
113698
|
}
|
|
113630
113699
|
/**
|
|
113631
113700
|
* Increment the per-message dispatch counter (poison-message guard). Called
|
|
113632
|
-
* each time a message_id is pushed onto the inbound queue
|
|
113633
|
-
*
|
|
113634
|
-
*
|
|
113701
|
+
* each time a message_id is pushed onto the inbound queue — and ONLY then;
|
|
113702
|
+
* {@link markUnhandled} deliberately reads without bumping, so the cap means
|
|
113703
|
+
* "N real dispatches" (see the messageAttempts field doc).
|
|
113635
113704
|
*/
|
|
113636
113705
|
noteDispatchAttempt(messageId) {
|
|
113637
113706
|
this.messageAttempts.set(messageId, (this.messageAttempts.get(messageId) ?? 0) + 1);
|
|
113638
113707
|
}
|
|
113708
|
+
/**
|
|
113709
|
+
* Capture chatId + create_time for an in-flight message at dispatch time, so
|
|
113710
|
+
* {@link markUnhandled} can queue an unresolved gap window that reaches back
|
|
113711
|
+
* to the message itself. An unparseable create_time falls back to "1 min ago"
|
|
113712
|
+
* — wide enough to cover the message without flooding the replay pull.
|
|
113713
|
+
*/
|
|
113714
|
+
noteInFlightMeta(ev) {
|
|
113715
|
+
const t = Number(ev.create_time);
|
|
113716
|
+
const createTimeMs = Number.isFinite(t) && t > 0 ? t < 1e12 ? t * 1e3 : t : Date.now() - 6e4;
|
|
113717
|
+
this.inFlightMessageMeta.set(ev.message_id, { chatId: ev.chat_id, createTimeMs });
|
|
113718
|
+
}
|
|
113639
113719
|
async close() {
|
|
113640
113720
|
this.closed = true;
|
|
113641
113721
|
if (this.openChatDiscoveryTimer) {
|
|
113642
113722
|
clearInterval(this.openChatDiscoveryTimer);
|
|
113643
113723
|
this.openChatDiscoveryTimer = null;
|
|
113644
113724
|
}
|
|
113725
|
+
if (this.unresolvedReplayTimer) {
|
|
113726
|
+
clearInterval(this.unresolvedReplayTimer);
|
|
113727
|
+
this.unresolvedReplayTimer = null;
|
|
113728
|
+
}
|
|
113645
113729
|
this.queue.close();
|
|
113646
113730
|
if (this.channel && this.connected) {
|
|
113647
113731
|
try {
|
|
@@ -114001,6 +114085,34 @@ var init_client = __esm({
|
|
|
114001
114085
|
}
|
|
114002
114086
|
});
|
|
114003
114087
|
|
|
114088
|
+
// src/lark/sdkLogger.ts
|
|
114089
|
+
var sdkLogger_exports = {};
|
|
114090
|
+
__export(sdkLogger_exports, {
|
|
114091
|
+
compactErrorText: () => compactErrorText,
|
|
114092
|
+
silentSdkLogger: () => silentSdkLogger
|
|
114093
|
+
});
|
|
114094
|
+
function compactErrorText(err2) {
|
|
114095
|
+
return err2 instanceof Error ? err2.message : String(err2);
|
|
114096
|
+
}
|
|
114097
|
+
var silentSdkLogger;
|
|
114098
|
+
var init_sdkLogger = __esm({
|
|
114099
|
+
"src/lark/sdkLogger.ts"() {
|
|
114100
|
+
"use strict";
|
|
114101
|
+
silentSdkLogger = {
|
|
114102
|
+
error(..._msg) {
|
|
114103
|
+
},
|
|
114104
|
+
warn(..._msg) {
|
|
114105
|
+
},
|
|
114106
|
+
info(..._msg) {
|
|
114107
|
+
},
|
|
114108
|
+
debug(..._msg) {
|
|
114109
|
+
},
|
|
114110
|
+
trace(..._msg) {
|
|
114111
|
+
}
|
|
114112
|
+
};
|
|
114113
|
+
}
|
|
114114
|
+
});
|
|
114115
|
+
|
|
114004
114116
|
// src/tasklist/teamRegistry.ts
|
|
114005
114117
|
var teamRegistry_exports = {};
|
|
114006
114118
|
__export(teamRegistry_exports, {
|
|
@@ -127694,6 +127806,19 @@ async function checkClaude(ctx) {
|
|
|
127694
127806
|
message: required ? claudeLoginHint() : "\u672A\u68C0\u6D4B\u5230 Claude \u767B\u5F55\u6001(\u5F53\u524D\u65E0 bot \u4F7F\u7528 claude backend,\u53EF\u5FFD\u7565)\u3002\u5982\u9700\u4F7F\u7528 Claude Code backend,\u8BF7\u5148\u8FD0\u884C `claude` \u767B\u5F55\u3002"
|
|
127695
127807
|
};
|
|
127696
127808
|
}
|
|
127809
|
+
async function checkLarkCli() {
|
|
127810
|
+
try {
|
|
127811
|
+
await execFileAsync4("lark-cli", ["--version"], { timeout: 1e4 });
|
|
127812
|
+
return { id: "lark-cli-binary", label: "lark-cli", status: "ok" };
|
|
127813
|
+
} catch {
|
|
127814
|
+
return {
|
|
127815
|
+
id: "lark-cli-binary",
|
|
127816
|
+
label: "lark-cli",
|
|
127817
|
+
status: "error",
|
|
127818
|
+
message: "\u672A\u627E\u5230 `lark-cli`(\u98DE\u4E66\u957F\u8FDE\u63A5/\u6D88\u606F\u5DE5\u5177,larkway \u786C\u4F9D\u8D56)\u3002\u5B89\u88C5:`npm i -g @larksuite/cli`,\u7136\u540E `lark-cli auth login` \u5B8C\u6210\u6388\u6743\u3002"
|
|
127819
|
+
};
|
|
127820
|
+
}
|
|
127821
|
+
}
|
|
127697
127822
|
async function checkCodex(ctx) {
|
|
127698
127823
|
const results = [];
|
|
127699
127824
|
const required = await backendRequired(ctx, "codex");
|
|
@@ -128029,9 +128154,10 @@ async function checkWsConnectivity(ctx, opts) {
|
|
|
128029
128154
|
}
|
|
128030
128155
|
async function probeTaskScope(appId, appSecret, tasklistGuid) {
|
|
128031
128156
|
try {
|
|
128032
|
-
const { Client: LarkSdkClient2
|
|
128157
|
+
const { Client: LarkSdkClient2 } = await Promise.resolve().then(() => __toESM(require_lib2(), 1));
|
|
128033
128158
|
const { TaskListClient: TaskListClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
128034
|
-
const
|
|
128159
|
+
const { silentSdkLogger: silentSdkLogger2 } = await Promise.resolve().then(() => (init_sdkLogger(), sdkLogger_exports));
|
|
128160
|
+
const sdkClient = new LarkSdkClient2({ appId, appSecret, logger: silentSdkLogger2 });
|
|
128035
128161
|
const taskClient = new TaskListClient2({
|
|
128036
128162
|
request: (config) => sdkClient.request(config)
|
|
128037
128163
|
});
|
|
@@ -128120,6 +128246,7 @@ async function runAllChecks(ctx, opts) {
|
|
|
128120
128246
|
results.push(await checkClaude(ctx));
|
|
128121
128247
|
const credChecks = await checkFeishuCreds(ctx);
|
|
128122
128248
|
results.push(...credChecks);
|
|
128249
|
+
results.push(await checkLarkCli());
|
|
128123
128250
|
const yamlChecks = await checkBotYaml(ctx);
|
|
128124
128251
|
results.push(...yamlChecks);
|
|
128125
128252
|
const wtChecks = await checkWorktrees(ctx);
|
|
@@ -130969,6 +131096,7 @@ async function finalizeOnboard(sessionId, form) {
|
|
|
130969
131096
|
if (!s._creds) {
|
|
130970
131097
|
throw new Error("\u5185\u90E8\u9519\u8BEF:creds \u672A\u6301\u6709");
|
|
130971
131098
|
}
|
|
131099
|
+
s.status = "finalizing";
|
|
130972
131100
|
try {
|
|
130973
131101
|
const { botId } = await createBotFromCreds({
|
|
130974
131102
|
creds: s._creds,
|
|
@@ -130990,12 +131118,16 @@ async function finalizeOnboard(sessionId, form) {
|
|
|
130990
131118
|
async function cancelOnboard(sessionId) {
|
|
130991
131119
|
const s = sessions.get(sessionId);
|
|
130992
131120
|
if (!s) return { cancelled: false };
|
|
130993
|
-
if (s.status === "done" || s.status === "error" || s.status === "cancelled"
|
|
131121
|
+
if (s.status === "done" || s.status === "error" || s.status === "cancelled" || // A finalize is already writing this session's bot to disk — treat like a
|
|
131122
|
+
// terminal state (racing a SECOND createBotFromCreds here would land two
|
|
131123
|
+
// bots from one Feishu app). The finalize's own outcome stands.
|
|
131124
|
+
s.status === "finalizing") {
|
|
130994
131125
|
return { cancelled: false };
|
|
130995
131126
|
}
|
|
130996
131127
|
if (s.status === "awaiting-name" && s._creds) {
|
|
130997
131128
|
const defaultName = s.prefill?.suggestedName || "\u65B0\u52A9\u624B";
|
|
130998
131129
|
const form = { name: defaultName };
|
|
131130
|
+
s.status = "finalizing";
|
|
130999
131131
|
try {
|
|
131000
131132
|
const { botId } = await createBotFromCreds({
|
|
131001
131133
|
creds: s._creds,
|
|
@@ -132791,6 +132923,7 @@ function exitCodeFor(checks) {
|
|
|
132791
132923
|
|
|
132792
132924
|
// src/cli/commands/tasklistInit.ts
|
|
132793
132925
|
var import_node_sdk2 = __toESM(require_lib2(), 1);
|
|
132926
|
+
init_sdkLogger();
|
|
132794
132927
|
init_client();
|
|
132795
132928
|
init_paths();
|
|
132796
132929
|
init_teamRegistry();
|
|
@@ -133156,7 +133289,7 @@ async function run10(ctx, args) {
|
|
|
133156
133289
|
const sdkClient = new import_node_sdk2.Client({
|
|
133157
133290
|
appId: creator.app_id,
|
|
133158
133291
|
appSecret: creator.appSecret,
|
|
133159
|
-
|
|
133292
|
+
logger: silentSdkLogger
|
|
133160
133293
|
});
|
|
133161
133294
|
const requester = {
|
|
133162
133295
|
request: (config) => sdkClient.request(config)
|
package/dist/main.js
CHANGED
|
@@ -24969,13 +24969,13 @@ var require_lib2 = __commonJS({
|
|
|
24969
24969
|
Domain[Domain["Lark"] = 1] = "Lark";
|
|
24970
24970
|
})(exports.Domain || (exports.Domain = {}));
|
|
24971
24971
|
exports.LoggerLevel = void 0;
|
|
24972
|
-
(function(
|
|
24973
|
-
|
|
24974
|
-
|
|
24975
|
-
|
|
24976
|
-
|
|
24977
|
-
|
|
24978
|
-
|
|
24972
|
+
(function(LoggerLevel) {
|
|
24973
|
+
LoggerLevel[LoggerLevel["fatal"] = 0] = "fatal";
|
|
24974
|
+
LoggerLevel[LoggerLevel["error"] = 1] = "error";
|
|
24975
|
+
LoggerLevel[LoggerLevel["warn"] = 2] = "warn";
|
|
24976
|
+
LoggerLevel[LoggerLevel["info"] = 3] = "info";
|
|
24977
|
+
LoggerLevel[LoggerLevel["debug"] = 4] = "debug";
|
|
24978
|
+
LoggerLevel[LoggerLevel["trace"] = 5] = "trace";
|
|
24979
24979
|
})(exports.LoggerLevel || (exports.LoggerLevel = {}));
|
|
24980
24980
|
var CEventType = /* @__PURE__ */ Symbol("event-type");
|
|
24981
24981
|
var CTenantKey = /* @__PURE__ */ Symbol("tenant-key");
|
|
@@ -116846,15 +116846,27 @@ var ChannelClient = class {
|
|
|
116846
116846
|
inFlightMessageIds = /* @__PURE__ */ new Set();
|
|
116847
116847
|
/**
|
|
116848
116848
|
* Per-message (re-)dispatch counter for the poison-message guard. Incremented
|
|
116849
|
-
* every time a message_id is pushed onto the inbound
|
|
116850
|
-
* gap-fill branch) and
|
|
116851
|
-
*
|
|
116852
|
-
*
|
|
116853
|
-
*
|
|
116849
|
+
* ONCE per dispatch — every time a message_id is pushed onto the inbound
|
|
116850
|
+
* queue (live WS or either gap-fill branch), and nowhere else, so the cap
|
|
116851
|
+
* really means "N dispatches". When a turn fails and the count has reached
|
|
116852
|
+
* {@link MAX_MESSAGE_ATTEMPTS}, markUnhandled GIVES UP: it promotes the
|
|
116853
|
+
* message to seen (so it stops being re-dispatched) and logs a warning.
|
|
116854
|
+
* markHandled clears the entry on terminal success. Not persisted:
|
|
116854
116855
|
* post-restart, an interrupted message starts fresh — same policy as
|
|
116855
116856
|
* inFlightMessageIds.
|
|
116856
116857
|
*/
|
|
116857
116858
|
messageAttempts = /* @__PURE__ */ new Map();
|
|
116859
|
+
/**
|
|
116860
|
+
* chatId + create_time for every in-flight message, captured at dispatch.
|
|
116861
|
+
* Consumed by {@link markUnhandled}: a FAILED turn records an unresolved gap
|
|
116862
|
+
* window for its chat so a later replay actually re-pulls (and re-dispatches)
|
|
116863
|
+
* it. Without this there is no steady-state trigger for the re-dispatch that
|
|
116864
|
+
* markUnhandled promises — chats-mode bots have no discovery timer at all,
|
|
116865
|
+
* and open-mode steady-state discovery pulls 0 for already-known chats — so
|
|
116866
|
+
* a turn failing before any visible surface existed was silently dropped.
|
|
116867
|
+
* Cleared in markHandled / markUnhandled; bounded by the in-flight set.
|
|
116868
|
+
*/
|
|
116869
|
+
inFlightMessageMeta = /* @__PURE__ */ new Map();
|
|
116858
116870
|
/**
|
|
116859
116871
|
* Chats observed from live WS events during this process lifetime.
|
|
116860
116872
|
*
|
|
@@ -116889,6 +116901,17 @@ var ChannelClient = class {
|
|
|
116889
116901
|
openChatDiscoveryTimer = null;
|
|
116890
116902
|
openChatDiscoveryRunning = false;
|
|
116891
116903
|
openChatDiscoveryBootstrapped = false;
|
|
116904
|
+
/**
|
|
116905
|
+
* Chats-mode (allowedChatIds non-empty) counterpart of the open-mode
|
|
116906
|
+
* discovery cycle's unresolved-window replay: those bots never start
|
|
116907
|
+
* discovery, so without this timer a window recorded by markUnhandled had
|
|
116908
|
+
* NO trigger to replay it (the only other gapFill source is a WS reconnect,
|
|
116909
|
+
* which a healthy network may not produce for days). Steady state with no
|
|
116910
|
+
* pending windows does nothing — zero API calls — so the 0.3.28 storm fix
|
|
116911
|
+
* is preserved.
|
|
116912
|
+
*/
|
|
116913
|
+
unresolvedReplayTimer = null;
|
|
116914
|
+
unresolvedReplayRunning = false;
|
|
116892
116915
|
/**
|
|
116893
116916
|
* Consecutive discovery-cycle failures. Used to SKIP cycles with exponential
|
|
116894
116917
|
* backoff (storm: a failing +chat-list/gap-fill shouldn't re-fire every
|
|
@@ -117094,6 +117117,7 @@ var ChannelClient = class {
|
|
|
117094
117117
|
return;
|
|
117095
117118
|
}
|
|
117096
117119
|
this.inFlightMessageIds.add(ev.message_id);
|
|
117120
|
+
this.noteInFlightMeta(ev);
|
|
117097
117121
|
this.noteDispatchAttempt(ev.message_id);
|
|
117098
117122
|
log(`dispatching (channel-sdk): message_id=${ev.message_id} thread=${ev.thread_id ?? "?"}`);
|
|
117099
117123
|
this.queue.push(ev);
|
|
@@ -117122,6 +117146,7 @@ var ChannelClient = class {
|
|
|
117122
117146
|
this.connected = true;
|
|
117123
117147
|
log(`connected as ${channel.botIdentity?.name ?? "?"} (${channel.botIdentity?.openId ?? "?"})`);
|
|
117124
117148
|
this.startOpenChatDiscovery(log);
|
|
117149
|
+
this.startUnresolvedReplayTimer(log);
|
|
117125
117150
|
}
|
|
117126
117151
|
/**
|
|
117127
117152
|
* Turn a card-button click into a synthesized LarkMessageEvent pushed onto the
|
|
@@ -117362,6 +117387,7 @@ var ChannelClient = class {
|
|
|
117362
117387
|
})();
|
|
117363
117388
|
if (!fallbackEv) continue;
|
|
117364
117389
|
this.inFlightMessageIds.add(fallbackEv.message_id);
|
|
117390
|
+
this.noteInFlightMeta(fallbackEv);
|
|
117365
117391
|
this.noteDispatchAttempt(fallbackEv.message_id);
|
|
117366
117392
|
log(`gap-fill dispatching (fallback): message_id=${fallbackEv.message_id} chat=${chatId}`);
|
|
117367
117393
|
this.queue.push(fallbackEv);
|
|
@@ -117369,6 +117395,7 @@ var ChannelClient = class {
|
|
|
117369
117395
|
continue;
|
|
117370
117396
|
}
|
|
117371
117397
|
this.inFlightMessageIds.add(ev.message_id);
|
|
117398
|
+
this.noteInFlightMeta(ev);
|
|
117372
117399
|
this.noteDispatchAttempt(ev.message_id);
|
|
117373
117400
|
log(`gap-fill dispatching: message_id=${ev.message_id} thread=${ev.thread_id ?? "?"} chat=${chatId}`);
|
|
117374
117401
|
this.queue.push(ev);
|
|
@@ -117459,6 +117486,37 @@ var ChannelClient = class {
|
|
|
117459
117486
|
if (tracked === void 0) return;
|
|
117460
117487
|
if (coveredFrom <= tracked) this.unresolvedGapWindowByChat.delete(chatId);
|
|
117461
117488
|
}
|
|
117489
|
+
/**
|
|
117490
|
+
* Chats-mode replay loop (see {@link unresolvedReplayTimer}): every discovery
|
|
117491
|
+
* interval, IF any unresolved gap window is pending, gapFill exactly those
|
|
117492
|
+
* chats. Open-mode bots don't need this — their discovery cycle already
|
|
117493
|
+
* includes unresolved-window chats in its target set.
|
|
117494
|
+
*/
|
|
117495
|
+
startUnresolvedReplayTimer(log) {
|
|
117496
|
+
if (this.opts.allowedChatIds.size === 0) return;
|
|
117497
|
+
if (this.unresolvedReplayTimer) return;
|
|
117498
|
+
const intervalMs = resolveOpenChatDiscoveryMs(this.opts.openChatDiscoveryMs);
|
|
117499
|
+
if (intervalMs <= 0) return;
|
|
117500
|
+
this.unresolvedReplayTimer = setInterval(() => {
|
|
117501
|
+
void this.replayUnresolvedWindows(log);
|
|
117502
|
+
}, intervalMs);
|
|
117503
|
+
this.unresolvedReplayTimer.unref?.();
|
|
117504
|
+
}
|
|
117505
|
+
async replayUnresolvedWindows(log) {
|
|
117506
|
+
if (this.closed || this.unresolvedReplayRunning) return;
|
|
117507
|
+
this.pruneUnresolvedGapWindows(Date.now());
|
|
117508
|
+
if (this.unresolvedGapWindowByChat.size === 0) return;
|
|
117509
|
+
this.unresolvedReplayRunning = true;
|
|
117510
|
+
try {
|
|
117511
|
+
const chats = new Set(this.unresolvedGapWindowByChat.keys());
|
|
117512
|
+
log(`unresolved-window replay: re-pulling ${chats.size} chat(s)`);
|
|
117513
|
+
await this.gapFill(Date.now(), log, chats);
|
|
117514
|
+
} catch (e) {
|
|
117515
|
+
log(`unresolved-window replay failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
117516
|
+
} finally {
|
|
117517
|
+
this.unresolvedReplayRunning = false;
|
|
117518
|
+
}
|
|
117519
|
+
}
|
|
117462
117520
|
startOpenChatDiscovery(log) {
|
|
117463
117521
|
if (this.opts.allowedChatIds.size > 0) return;
|
|
117464
117522
|
if (this.openChatDiscoveryTimer) return;
|
|
@@ -117728,6 +117786,7 @@ var ChannelClient = class {
|
|
|
117728
117786
|
*/
|
|
117729
117787
|
markHandled(messageId) {
|
|
117730
117788
|
this.inFlightMessageIds.delete(messageId);
|
|
117789
|
+
this.inFlightMessageMeta.delete(messageId);
|
|
117731
117790
|
this.messageAttempts.delete(messageId);
|
|
117732
117791
|
this.noteSeenMessage(messageId);
|
|
117733
117792
|
}
|
|
@@ -117737,38 +117796,63 @@ var ChannelClient = class {
|
|
|
117737
117796
|
* self-heal — one transient blip no longer swallows the @ forever). Does not
|
|
117738
117797
|
* touch persisted seen state.
|
|
117739
117798
|
*
|
|
117740
|
-
* Poison-message guard:
|
|
117741
|
-
*
|
|
117742
|
-
*
|
|
117743
|
-
*
|
|
117799
|
+
* Poison-message guard: if the message has already been dispatched
|
|
117800
|
+
* {@link MAX_MESSAGE_ATTEMPTS} times, GIVE UP — promote it to seen (so it
|
|
117801
|
+
* stops being re-dispatched on every gap-fill) and log a visible warning
|
|
117802
|
+
* instead of silently looping forever. Otherwise, record an unresolved gap
|
|
117803
|
+
* window for the message's chat so the next replay cycle re-dispatches it.
|
|
117744
117804
|
*/
|
|
117745
|
-
markUnhandled(messageId) {
|
|
117805
|
+
markUnhandled(messageId, opts) {
|
|
117746
117806
|
this.inFlightMessageIds.delete(messageId);
|
|
117747
|
-
const
|
|
117748
|
-
this.
|
|
117807
|
+
const meta = this.inFlightMessageMeta.get(messageId);
|
|
117808
|
+
this.inFlightMessageMeta.delete(messageId);
|
|
117809
|
+
const attempts = this.messageAttempts.get(messageId) ?? 0;
|
|
117749
117810
|
if (attempts >= MAX_MESSAGE_ATTEMPTS) {
|
|
117750
117811
|
console.warn(
|
|
117751
117812
|
`[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)`
|
|
117752
117813
|
);
|
|
117753
117814
|
this.messageAttempts.delete(messageId);
|
|
117754
117815
|
this.noteSeenMessage(messageId);
|
|
117816
|
+
return;
|
|
117817
|
+
}
|
|
117818
|
+
if (meta && opts?.replay !== false) {
|
|
117819
|
+
this.recordUnresolvedGapWindow(
|
|
117820
|
+
meta.chatId,
|
|
117821
|
+
meta.createTimeMs - 1e3,
|
|
117822
|
+
(s) => console.log(`[channel.client] ${s}`)
|
|
117823
|
+
);
|
|
117755
117824
|
}
|
|
117756
117825
|
}
|
|
117757
117826
|
/**
|
|
117758
117827
|
* Increment the per-message dispatch counter (poison-message guard). Called
|
|
117759
|
-
* each time a message_id is pushed onto the inbound queue
|
|
117760
|
-
*
|
|
117761
|
-
*
|
|
117828
|
+
* each time a message_id is pushed onto the inbound queue — and ONLY then;
|
|
117829
|
+
* {@link markUnhandled} deliberately reads without bumping, so the cap means
|
|
117830
|
+
* "N real dispatches" (see the messageAttempts field doc).
|
|
117762
117831
|
*/
|
|
117763
117832
|
noteDispatchAttempt(messageId) {
|
|
117764
117833
|
this.messageAttempts.set(messageId, (this.messageAttempts.get(messageId) ?? 0) + 1);
|
|
117765
117834
|
}
|
|
117835
|
+
/**
|
|
117836
|
+
* Capture chatId + create_time for an in-flight message at dispatch time, so
|
|
117837
|
+
* {@link markUnhandled} can queue an unresolved gap window that reaches back
|
|
117838
|
+
* to the message itself. An unparseable create_time falls back to "1 min ago"
|
|
117839
|
+
* — wide enough to cover the message without flooding the replay pull.
|
|
117840
|
+
*/
|
|
117841
|
+
noteInFlightMeta(ev) {
|
|
117842
|
+
const t = Number(ev.create_time);
|
|
117843
|
+
const createTimeMs = Number.isFinite(t) && t > 0 ? t < 1e12 ? t * 1e3 : t : Date.now() - 6e4;
|
|
117844
|
+
this.inFlightMessageMeta.set(ev.message_id, { chatId: ev.chat_id, createTimeMs });
|
|
117845
|
+
}
|
|
117766
117846
|
async close() {
|
|
117767
117847
|
this.closed = true;
|
|
117768
117848
|
if (this.openChatDiscoveryTimer) {
|
|
117769
117849
|
clearInterval(this.openChatDiscoveryTimer);
|
|
117770
117850
|
this.openChatDiscoveryTimer = null;
|
|
117771
117851
|
}
|
|
117852
|
+
if (this.unresolvedReplayTimer) {
|
|
117853
|
+
clearInterval(this.unresolvedReplayTimer);
|
|
117854
|
+
this.unresolvedReplayTimer = null;
|
|
117855
|
+
}
|
|
117772
117856
|
this.queue.close();
|
|
117773
117857
|
if (this.channel && this.connected) {
|
|
117774
117858
|
try {
|
|
@@ -118230,14 +118314,45 @@ var CardRenderer = class {
|
|
|
118230
118314
|
}
|
|
118231
118315
|
};
|
|
118232
118316
|
|
|
118317
|
+
// src/lark/sdkLogger.ts
|
|
118318
|
+
var silentSdkLogger = {
|
|
118319
|
+
error(..._msg) {
|
|
118320
|
+
},
|
|
118321
|
+
warn(..._msg) {
|
|
118322
|
+
},
|
|
118323
|
+
info(..._msg) {
|
|
118324
|
+
},
|
|
118325
|
+
debug(..._msg) {
|
|
118326
|
+
},
|
|
118327
|
+
trace(..._msg) {
|
|
118328
|
+
}
|
|
118329
|
+
};
|
|
118330
|
+
function compactErrorText(err) {
|
|
118331
|
+
return err instanceof Error ? err.message : String(err);
|
|
118332
|
+
}
|
|
118333
|
+
|
|
118233
118334
|
// src/claude/sessionStore.ts
|
|
118234
|
-
import { rename as rename2, readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2, copyFile } from "node:fs/promises";
|
|
118335
|
+
import { rename as rename2, readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2, copyFile, unlink as unlink2 } from "node:fs/promises";
|
|
118235
118336
|
import { dirname } from "node:path";
|
|
118236
118337
|
var STORE_VERSION = 2;
|
|
118237
118338
|
var TOUCH_DEBOUNCE_MS = 1e3;
|
|
118238
118339
|
var SessionStore = class _SessionStore {
|
|
118239
118340
|
#filePath;
|
|
118240
118341
|
#map;
|
|
118342
|
+
/**
|
|
118343
|
+
* Serializes every #flush() through one chain — mirrors TaskHandleStore's
|
|
118344
|
+
* `#flushChain` (same rationale). Concurrent writers are real here: up to
|
|
118345
|
+
* MAX_CONCURRENT turns call `put()`/`delete()` while the touch-debounce
|
|
118346
|
+
* timer fires its own flush. Without serialization, two overlapping
|
|
118347
|
+
* `writeFile(SAME tmp path)+rename` pairs can interleave (both open with
|
|
118348
|
+
* O_TRUNC at their own offset 0) and land corrupt JSON as sessions.json.
|
|
118349
|
+
* Each link's #writeSnapshot() takes its #map snapshot when it actually
|
|
118350
|
+
* RUNS (not at enqueue time), so the LAST queued write is always the LAST
|
|
118351
|
+
* to land on disk.
|
|
118352
|
+
*/
|
|
118353
|
+
#flushChain = Promise.resolve();
|
|
118354
|
+
/** Monotonic suffix so no two in-flight tmp files ever share a path. */
|
|
118355
|
+
#tmpCounter = 0;
|
|
118241
118356
|
/** Whether a touch flush is pending */
|
|
118242
118357
|
#touchDirty = false;
|
|
118243
118358
|
#touchTimer;
|
|
@@ -118279,13 +118394,12 @@ var SessionStore = class _SessionStore {
|
|
|
118279
118394
|
try {
|
|
118280
118395
|
parsed = JSON.parse(raw);
|
|
118281
118396
|
} catch {
|
|
118282
|
-
|
|
118283
|
-
`[SessionStore] ${filePath} is not valid JSON \u2014 fix or delete the file and restart.`
|
|
118284
|
-
);
|
|
118397
|
+
return await _SessionStore.#recoverFromCorruption(filePath, "is not valid JSON");
|
|
118285
118398
|
}
|
|
118286
118399
|
if (typeof parsed !== "object" || parsed === null || !("records" in parsed)) {
|
|
118287
|
-
|
|
118288
|
-
|
|
118400
|
+
return await _SessionStore.#recoverFromCorruption(
|
|
118401
|
+
filePath,
|
|
118402
|
+
"is missing required fields (records)"
|
|
118289
118403
|
);
|
|
118290
118404
|
}
|
|
118291
118405
|
const file = parsed;
|
|
@@ -118299,8 +118413,9 @@ var SessionStore = class _SessionStore {
|
|
|
118299
118413
|
);
|
|
118300
118414
|
}
|
|
118301
118415
|
if (typeof file.records !== "object" || file.records === null) {
|
|
118302
|
-
|
|
118303
|
-
|
|
118416
|
+
return await _SessionStore.#recoverFromCorruption(
|
|
118417
|
+
filePath,
|
|
118418
|
+
"records field is not an object"
|
|
118304
118419
|
);
|
|
118305
118420
|
}
|
|
118306
118421
|
const map2 = /* @__PURE__ */ new Map();
|
|
@@ -118308,14 +118423,44 @@ var SessionStore = class _SessionStore {
|
|
|
118308
118423
|
file.records
|
|
118309
118424
|
)) {
|
|
118310
118425
|
if (!isStoredRecord(value)) {
|
|
118311
|
-
|
|
118312
|
-
`[SessionStore] ${filePath} record "${key}" has unexpected shape \u2014
|
|
118426
|
+
console.warn(
|
|
118427
|
+
`[SessionStore] ${filePath} record "${key}" has unexpected shape \u2014 skipping it.`
|
|
118313
118428
|
);
|
|
118429
|
+
continue;
|
|
118314
118430
|
}
|
|
118315
118431
|
map2.set(key, value);
|
|
118316
118432
|
}
|
|
118317
118433
|
return new _SessionStore(filePath, map2);
|
|
118318
118434
|
}
|
|
118435
|
+
/**
|
|
118436
|
+
* A corrupt sessions.json must not keep the whole bridge from booting:
|
|
118437
|
+
* load() runs in main.ts startup with no surrounding try/catch, so a throw
|
|
118438
|
+
* here used to take EVERY bot down and require manual file surgery.
|
|
118439
|
+
* Mirror TaskHandleStore's posture instead: move the bad file to a
|
|
118440
|
+
* timestamped `.corrupt-*` backup (never silently lost) and start from an
|
|
118441
|
+
* empty store. Cost: threads lose their resume mapping and start fresh
|
|
118442
|
+
* sessions — a safe degradation compared to a full outage.
|
|
118443
|
+
*
|
|
118444
|
+
* NOTE: an unknown FUTURE `version` still throws (see load) — that file is
|
|
118445
|
+
* valid data written by newer code, not corruption, and must not be nuked.
|
|
118446
|
+
*/
|
|
118447
|
+
static async #recoverFromCorruption(filePath, reason) {
|
|
118448
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
118449
|
+
const backupPath = `${filePath}.corrupt-${ts}`;
|
|
118450
|
+
try {
|
|
118451
|
+
await rename2(filePath, backupPath);
|
|
118452
|
+
console.error(
|
|
118453
|
+
`[SessionStore] ${filePath} ${reason} \u2014 moved to ${backupPath}; starting with an empty store (existing threads will start fresh sessions).`
|
|
118454
|
+
);
|
|
118455
|
+
} catch (err) {
|
|
118456
|
+
console.error(
|
|
118457
|
+
`[SessionStore] ${filePath} ${reason} \u2014 backup rename failed (${String(err)}); starting with an empty store anyway.`
|
|
118458
|
+
);
|
|
118459
|
+
}
|
|
118460
|
+
const store = new _SessionStore(filePath, /* @__PURE__ */ new Map());
|
|
118461
|
+
await store.#flush();
|
|
118462
|
+
return store;
|
|
118463
|
+
}
|
|
118319
118464
|
/**
|
|
118320
118465
|
* Migrate a v1 sessions.json in-place to v2.
|
|
118321
118466
|
* Backup written as `<path>.v1-backup-<ISO timestamp>` before any write.
|
|
@@ -118449,18 +118594,33 @@ var SessionStore = class _SessionStore {
|
|
|
118449
118594
|
// Private helpers
|
|
118450
118595
|
// -------------------------------------------------------------------------
|
|
118451
118596
|
/**
|
|
118452
|
-
* Atomic write
|
|
118597
|
+
* Atomic write, serialized through #flushChain (see field doc).
|
|
118598
|
+
* The caller of THIS flush still observes its own link's rejection;
|
|
118599
|
+
* the chain itself swallows it so one failure can't wedge later flushes.
|
|
118453
118600
|
*/
|
|
118454
|
-
|
|
118601
|
+
#flush() {
|
|
118602
|
+
const next = this.#flushChain.then(() => this.#writeSnapshot());
|
|
118603
|
+
this.#flushChain = next.catch(() => {
|
|
118604
|
+
});
|
|
118605
|
+
return next;
|
|
118606
|
+
}
|
|
118607
|
+
/** serialize → write to a unique tmp path → fs.rename (POSIX atomic). */
|
|
118608
|
+
async #writeSnapshot() {
|
|
118455
118609
|
const file = {
|
|
118456
118610
|
version: STORE_VERSION,
|
|
118457
118611
|
records: Object.fromEntries(this.#map)
|
|
118458
118612
|
};
|
|
118459
118613
|
const json2 = JSON.stringify(file, null, 2);
|
|
118460
|
-
const tmpPath = `${this.#filePath}.tmp`;
|
|
118614
|
+
const tmpPath = `${this.#filePath}.tmp.${process.pid}.${this.#tmpCounter++}`;
|
|
118461
118615
|
await mkdir2(dirname(this.#filePath), { recursive: true });
|
|
118462
|
-
|
|
118463
|
-
|
|
118616
|
+
try {
|
|
118617
|
+
await writeFile2(tmpPath, json2, "utf8");
|
|
118618
|
+
await rename2(tmpPath, this.#filePath);
|
|
118619
|
+
} catch (err) {
|
|
118620
|
+
await unlink2(tmpPath).catch(() => {
|
|
118621
|
+
});
|
|
118622
|
+
throw err;
|
|
118623
|
+
}
|
|
118464
118624
|
}
|
|
118465
118625
|
};
|
|
118466
118626
|
function isStoredRecord(value) {
|
|
@@ -121793,6 +121953,7 @@ function summarizeMentionPolicyRules(rules) {
|
|
|
121793
121953
|
}
|
|
121794
121954
|
return Array.from(counts.entries()).map(([rule, count]) => `${rule}=${count}`).join(", ");
|
|
121795
121955
|
}
|
|
121956
|
+
var GIT_OP_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
121796
121957
|
function execGit(cwd, args) {
|
|
121797
121958
|
return new Promise((resolve2, reject) => {
|
|
121798
121959
|
const child = child_process.spawn("git", args, {
|
|
@@ -121800,18 +121961,37 @@ function execGit(cwd, args) {
|
|
|
121800
121961
|
stdio: ["ignore", "pipe", "pipe"]
|
|
121801
121962
|
});
|
|
121802
121963
|
let stderr = "";
|
|
121964
|
+
let timedOut = false;
|
|
121965
|
+
const timer = setTimeout(() => {
|
|
121966
|
+
timedOut = true;
|
|
121967
|
+
child.kill("SIGTERM");
|
|
121968
|
+
const killTimer = setTimeout(() => child.kill("SIGKILL"), 5e3);
|
|
121969
|
+
killTimer.unref();
|
|
121970
|
+
}, GIT_OP_TIMEOUT_MS);
|
|
121971
|
+
timer.unref();
|
|
121803
121972
|
child.stderr.on("data", (c) => {
|
|
121804
121973
|
stderr += c.toString();
|
|
121805
121974
|
});
|
|
121806
121975
|
child.on("close", (code) => {
|
|
121807
|
-
|
|
121976
|
+
clearTimeout(timer);
|
|
121977
|
+
if (timedOut) {
|
|
121978
|
+
reject(
|
|
121979
|
+
new Error(
|
|
121980
|
+
`git ${args.join(" ")} timed out after ${GIT_OP_TIMEOUT_MS / 1e3}s (killed)
|
|
121981
|
+
stderr: ${stderr}`
|
|
121982
|
+
)
|
|
121983
|
+
);
|
|
121984
|
+
} else if (code === 0) resolve2();
|
|
121808
121985
|
else
|
|
121809
121986
|
reject(
|
|
121810
121987
|
new Error(`git ${args.join(" ")} exited ${code ?? "null"}
|
|
121811
121988
|
stderr: ${stderr}`)
|
|
121812
121989
|
);
|
|
121813
121990
|
});
|
|
121814
|
-
child.on("error",
|
|
121991
|
+
child.on("error", (err) => {
|
|
121992
|
+
clearTimeout(timer);
|
|
121993
|
+
reject(err);
|
|
121994
|
+
});
|
|
121815
121995
|
});
|
|
121816
121996
|
}
|
|
121817
121997
|
async function pathExists(p) {
|
|
@@ -121833,7 +122013,16 @@ async function isWorktreeGitHealthy(worktreePath) {
|
|
|
121833
122013
|
child.on("error", () => resolve2(false));
|
|
121834
122014
|
});
|
|
121835
122015
|
}
|
|
121836
|
-
|
|
122016
|
+
var repoCloneLocks = /* @__PURE__ */ new Map();
|
|
122017
|
+
function ensureRepoClone(basePath, url, token, label) {
|
|
122018
|
+
const prev = repoCloneLocks.get(basePath) ?? Promise.resolve();
|
|
122019
|
+
const next = prev.catch(() => {
|
|
122020
|
+
}).then(() => ensureRepoCloneImpl(basePath, url, token, label));
|
|
122021
|
+
repoCloneLocks.set(basePath, next.catch(() => {
|
|
122022
|
+
}));
|
|
122023
|
+
return next;
|
|
122024
|
+
}
|
|
122025
|
+
async function ensureRepoCloneImpl(basePath, url, token, label) {
|
|
121837
122026
|
const gitDir = path10.join(basePath, ".git");
|
|
121838
122027
|
if (await pathExists(gitDir)) {
|
|
121839
122028
|
return;
|
|
@@ -121860,23 +122049,48 @@ async function ensureRepoClone(basePath, url, token, label) {
|
|
|
121860
122049
|
GIT_TERMINAL_PROMPT: "0",
|
|
121861
122050
|
[tokenEnvVar]: token ?? ""
|
|
121862
122051
|
};
|
|
121863
|
-
|
|
121864
|
-
|
|
121865
|
-
|
|
121866
|
-
|
|
121867
|
-
|
|
121868
|
-
|
|
121869
|
-
|
|
121870
|
-
|
|
121871
|
-
stderr
|
|
121872
|
-
|
|
121873
|
-
|
|
121874
|
-
|
|
121875
|
-
|
|
122052
|
+
let timedOut = false;
|
|
122053
|
+
try {
|
|
122054
|
+
await new Promise((resolve2, reject) => {
|
|
122055
|
+
const child = child_process.spawn(
|
|
122056
|
+
"git",
|
|
122057
|
+
["clone", "--quiet", url, basePath],
|
|
122058
|
+
{ stdio: ["ignore", "pipe", "pipe"], env }
|
|
122059
|
+
);
|
|
122060
|
+
let stderr = "";
|
|
122061
|
+
const timer = setTimeout(() => {
|
|
122062
|
+
timedOut = true;
|
|
122063
|
+
child.kill("SIGTERM");
|
|
122064
|
+
const killTimer = setTimeout(() => child.kill("SIGKILL"), 5e3);
|
|
122065
|
+
killTimer.unref();
|
|
122066
|
+
}, GIT_OP_TIMEOUT_MS);
|
|
122067
|
+
timer.unref();
|
|
122068
|
+
child.stderr.on("data", (b) => {
|
|
122069
|
+
stderr += b.toString();
|
|
122070
|
+
});
|
|
122071
|
+
child.on("close", (code) => {
|
|
122072
|
+
clearTimeout(timer);
|
|
122073
|
+
if (timedOut) {
|
|
122074
|
+
reject(new Error(
|
|
122075
|
+
`git clone ${url} timed out after ${GIT_OP_TIMEOUT_MS / 1e3}s (killed)
|
|
122076
|
+
stderr: ${stderr}`
|
|
122077
|
+
));
|
|
122078
|
+
} else if (code === 0) resolve2();
|
|
122079
|
+
else reject(new Error(`git clone ${url} exited ${code ?? "null"}
|
|
121876
122080
|
stderr: ${stderr}`));
|
|
122081
|
+
});
|
|
122082
|
+
child.on("error", (err) => {
|
|
122083
|
+
clearTimeout(timer);
|
|
122084
|
+
reject(err);
|
|
122085
|
+
});
|
|
121877
122086
|
});
|
|
121878
|
-
|
|
121879
|
-
|
|
122087
|
+
} catch (err) {
|
|
122088
|
+
if (timedOut) {
|
|
122089
|
+
await fs8.rm(basePath, { recursive: true, force: true }).catch(() => {
|
|
122090
|
+
});
|
|
122091
|
+
}
|
|
122092
|
+
throw err;
|
|
122093
|
+
}
|
|
121880
122094
|
console.log(`[bridge.handler] clone of ${label} complete.`);
|
|
121881
122095
|
await execGit(basePath, ["remote", "set-url", "origin", url]);
|
|
121882
122096
|
} finally {
|
|
@@ -122133,7 +122347,8 @@ var BridgeHandler = class {
|
|
|
122133
122347
|
/**
|
|
122134
122348
|
* Enter the main loop: for-await over client.events(), per-thread concurrent dispatch.
|
|
122135
122349
|
*
|
|
122136
|
-
* Each unique
|
|
122350
|
+
* Each unique session key (root_id, or message_id for top-level msgs — the
|
|
122351
|
+
* same normalization parseLarkMessage uses for threadId) gets its own serial
|
|
122137
122352
|
* promise chain, so the same thread stays ordered while different threads run
|
|
122138
122353
|
* concurrently. This fixes the UX problem where multiple operators sending
|
|
122139
122354
|
* requests simultaneously would block each other for the duration of each
|
|
@@ -122168,7 +122383,7 @@ var BridgeHandler = class {
|
|
|
122168
122383
|
for await (const event of this.deps.client.events()) {
|
|
122169
122384
|
if (this.closed) break;
|
|
122170
122385
|
if (signal?.aborted) break;
|
|
122171
|
-
const key = event.
|
|
122386
|
+
const key = typeof event.root_id === "string" && event.root_id ? event.root_id : event.message_id;
|
|
122172
122387
|
this.threadReceivedAt.set(key, Date.now());
|
|
122173
122388
|
const prev = threadQueues.get(key) ?? Promise.resolve();
|
|
122174
122389
|
const next = prev.then(() => acquire()).then(() => this.handleOne(event)).catch((err) => {
|
|
@@ -122195,6 +122410,7 @@ var BridgeHandler = class {
|
|
|
122195
122410
|
async handleOne(event) {
|
|
122196
122411
|
const settleMessageId = event.message_id;
|
|
122197
122412
|
let settled = false;
|
|
122413
|
+
let agentRunCompleted = false;
|
|
122198
122414
|
let cotPublisher;
|
|
122199
122415
|
let bubbleCreate;
|
|
122200
122416
|
let cotTurnOutcome = "done";
|
|
@@ -122202,7 +122418,7 @@ var BridgeHandler = class {
|
|
|
122202
122418
|
if (settled) return;
|
|
122203
122419
|
settled = true;
|
|
122204
122420
|
if (ok) this.deps.client.markHandled?.(settleMessageId);
|
|
122205
|
-
else this.deps.client.markUnhandled?.(settleMessageId);
|
|
122421
|
+
else this.deps.client.markUnhandled?.(settleMessageId, { replay: !agentRunCompleted });
|
|
122206
122422
|
};
|
|
122207
122423
|
try {
|
|
122208
122424
|
const parsed = parseMessage(event);
|
|
@@ -122762,7 +122978,7 @@ var BridgeHandler = class {
|
|
|
122762
122978
|
let toolsInFlight = 0;
|
|
122763
122979
|
let toolUseTotalCount = 0;
|
|
122764
122980
|
let idleWatchdog;
|
|
122765
|
-
|
|
122981
|
+
{
|
|
122766
122982
|
const cadenceMs = Math.max(50, Math.min(Math.floor(idleTimeoutMs / 4), 15e3));
|
|
122767
122983
|
idleWatchdog = setInterval(() => {
|
|
122768
122984
|
if (toolsInFlight > 0) return;
|
|
@@ -122810,6 +123026,7 @@ var BridgeHandler = class {
|
|
|
122810
123026
|
}
|
|
122811
123027
|
}
|
|
122812
123028
|
const result = await handle.done;
|
|
123029
|
+
agentRunCompleted = true;
|
|
122813
123030
|
if (idleWatchdog) {
|
|
122814
123031
|
clearInterval(idleWatchdog);
|
|
122815
123032
|
idleWatchdog = void 0;
|
|
@@ -122845,7 +123062,6 @@ var BridgeHandler = class {
|
|
|
122845
123062
|
pooled: result.pooled,
|
|
122846
123063
|
resumeMode: result.resumeMode
|
|
122847
123064
|
});
|
|
122848
|
-
const cardKitTurnTimedOut = cardKitProgress != null && interruptedByIdle;
|
|
122849
123065
|
const reportedStateRead = await readStateFileDetailed(worktreePath);
|
|
122850
123066
|
const rawReportedState = reportedStateRead.state;
|
|
122851
123067
|
const reportedState = rawReportedState?.updated_at != null && rawReportedState.updated_at !== preRunUpdatedAt ? rawReportedState : null;
|
|
@@ -122906,7 +123122,7 @@ var BridgeHandler = class {
|
|
|
122906
123122
|
}
|
|
122907
123123
|
const reportedStatus = reportedState?.status;
|
|
122908
123124
|
const reportedError = reportedState?.error;
|
|
122909
|
-
const cardKitTimeoutFailure =
|
|
123125
|
+
const cardKitTimeoutFailure = interruptedByIdle && reportedStatus !== "ready" && reportedStatus !== "failed";
|
|
122910
123126
|
let success;
|
|
122911
123127
|
let failureReason;
|
|
122912
123128
|
if (reportedStatus === "failed") {
|
|
@@ -123115,8 +123331,13 @@ var BridgeHandler = class {
|
|
|
123115
123331
|
);
|
|
123116
123332
|
break;
|
|
123117
123333
|
} catch (spawnErr) {
|
|
123334
|
+
if (idleWatchdog) {
|
|
123335
|
+
clearInterval(idleWatchdog);
|
|
123336
|
+
idleWatchdog = void 0;
|
|
123337
|
+
}
|
|
123118
123338
|
const errMsg = String(spawnErr.message ?? spawnErr);
|
|
123119
|
-
|
|
123339
|
+
const isStaleSessionErr = errMsg.includes("No conversation found") || errMsg.includes("thread/resume failed") && errMsg.includes("no rollout found");
|
|
123340
|
+
if (attempt === 1 && currentExisting != null && isStaleSessionErr) {
|
|
123120
123341
|
console.warn(
|
|
123121
123342
|
`[bridge.handler] stale session ${currentExisting.sessionId} for thread ${threadId} \u2014 removing and retrying without --resume`
|
|
123122
123343
|
);
|
|
@@ -127492,7 +127713,7 @@ async function writeStatusFile(botId, w) {
|
|
|
127492
127713
|
|
|
127493
127714
|
// src/claude/runner.ts
|
|
127494
127715
|
import { spawn as spawn2 } from "node:child_process";
|
|
127495
|
-
import { writeFile as writeFile3, unlink as
|
|
127716
|
+
import { writeFile as writeFile3, unlink as unlink3, mkdir as mkdir3 } from "node:fs/promises";
|
|
127496
127717
|
import { join as join2 } from "node:path";
|
|
127497
127718
|
import { createInterface } from "node:readline";
|
|
127498
127719
|
var SIGKILL_GRACE_MS = 5e3;
|
|
@@ -127686,9 +127907,13 @@ function runClaude(opts) {
|
|
|
127686
127907
|
function doKill() {
|
|
127687
127908
|
if (child.killed || killScheduled) return;
|
|
127688
127909
|
killScheduled = true;
|
|
127910
|
+
let exited = false;
|
|
127911
|
+
child.once("exit", () => {
|
|
127912
|
+
exited = true;
|
|
127913
|
+
});
|
|
127689
127914
|
child.kill("SIGTERM");
|
|
127690
127915
|
killTimer = setTimeout(() => {
|
|
127691
|
-
if (!
|
|
127916
|
+
if (!exited) child.kill("SIGKILL");
|
|
127692
127917
|
}, SIGKILL_GRACE_MS);
|
|
127693
127918
|
killTimer.unref();
|
|
127694
127919
|
}
|
|
@@ -127741,7 +127966,7 @@ function runClaude(opts) {
|
|
|
127741
127966
|
clearTimeout(totalTimeoutFallbackHandle);
|
|
127742
127967
|
rlAbortController.abort();
|
|
127743
127968
|
if (pidFilePath !== null) {
|
|
127744
|
-
void
|
|
127969
|
+
void unlink3(pidFilePath).catch(() => {
|
|
127745
127970
|
});
|
|
127746
127971
|
}
|
|
127747
127972
|
if (exitCode !== 0 && !killScheduled) {
|
|
@@ -127771,7 +127996,7 @@ stderr: ${stderr}` : "")
|
|
|
127771
127996
|
clearTimeout(totalTimeoutFallbackHandle);
|
|
127772
127997
|
rlAbortController.abort();
|
|
127773
127998
|
if (pidFilePath !== null) {
|
|
127774
|
-
void
|
|
127999
|
+
void unlink3(pidFilePath).catch(() => {
|
|
127775
128000
|
});
|
|
127776
128001
|
}
|
|
127777
128002
|
if (err.code === "ENOENT") {
|
|
@@ -127850,7 +128075,7 @@ var ClaudeRunner = class {
|
|
|
127850
128075
|
|
|
127851
128076
|
// src/claude/pool.ts
|
|
127852
128077
|
import { execFile as execFile4, spawn as spawn3 } from "node:child_process";
|
|
127853
|
-
import { mkdir as mkdir4, readFile as readFile6, rename as rename3, stat as stat3, unlink as
|
|
128078
|
+
import { mkdir as mkdir4, readFile as readFile6, rename as rename3, stat as stat3, unlink as unlink4, writeFile as writeFile4 } from "node:fs/promises";
|
|
127854
128079
|
import path16 from "node:path";
|
|
127855
128080
|
import { createInterface as createInterface2 } from "node:readline";
|
|
127856
128081
|
import { randomUUID } from "node:crypto";
|
|
@@ -128387,7 +128612,7 @@ ${stderr}` : "")
|
|
|
128387
128612
|
try {
|
|
128388
128613
|
const raw = await readFile6(pidFilePath, "utf8");
|
|
128389
128614
|
const parsed = JSON.parse(raw);
|
|
128390
|
-
if (parsed.pid === entry.child.pid) await
|
|
128615
|
+
if (parsed.pid === entry.child.pid) await unlink4(pidFilePath);
|
|
128391
128616
|
} catch {
|
|
128392
128617
|
}
|
|
128393
128618
|
}
|
|
@@ -128514,7 +128739,7 @@ async function reapOrphanedWarmClaudeProcesses(pidListFilePath) {
|
|
|
128514
128739
|
} catch {
|
|
128515
128740
|
}
|
|
128516
128741
|
}
|
|
128517
|
-
await
|
|
128742
|
+
await unlink4(pidListFilePath).catch(() => {
|
|
128518
128743
|
});
|
|
128519
128744
|
}
|
|
128520
128745
|
|
|
@@ -128682,6 +128907,8 @@ function runCodex(opts, codexBinPath = "codex") {
|
|
|
128682
128907
|
...opts.cwd != null ? { cwd: opts.cwd } : {}
|
|
128683
128908
|
});
|
|
128684
128909
|
markPerf("spawn");
|
|
128910
|
+
child.stdin?.on("error", () => {
|
|
128911
|
+
});
|
|
128685
128912
|
function sendRequest(method, params) {
|
|
128686
128913
|
const id = nextRequestId++;
|
|
128687
128914
|
requestById.set(id, method);
|
|
@@ -128694,9 +128921,13 @@ function runCodex(opts, codexBinPath = "codex") {
|
|
|
128694
128921
|
function doKill() {
|
|
128695
128922
|
if (child.killed || killScheduled) return;
|
|
128696
128923
|
killScheduled = true;
|
|
128924
|
+
let exited = false;
|
|
128925
|
+
child.once("exit", () => {
|
|
128926
|
+
exited = true;
|
|
128927
|
+
});
|
|
128697
128928
|
child.kill("SIGTERM");
|
|
128698
128929
|
killTimer = setTimeout(() => {
|
|
128699
|
-
if (!
|
|
128930
|
+
if (!exited) child.kill("SIGKILL");
|
|
128700
128931
|
}, SIGKILL_GRACE_MS3);
|
|
128701
128932
|
killTimer.unref();
|
|
128702
128933
|
}
|
|
@@ -128916,7 +129147,7 @@ var CodexRunner = class {
|
|
|
128916
129147
|
|
|
128917
129148
|
// src/codex/pool.ts
|
|
128918
129149
|
import { execFile as execFile5, spawn as spawn5 } from "node:child_process";
|
|
128919
|
-
import { mkdir as mkdir5, readFile as readFile7, unlink as
|
|
129150
|
+
import { mkdir as mkdir5, readFile as readFile7, unlink as unlink5, writeFile as writeFile5 } from "node:fs/promises";
|
|
128920
129151
|
import path17 from "node:path";
|
|
128921
129152
|
import { createInterface as createInterface4 } from "node:readline";
|
|
128922
129153
|
var DEFAULT_WARM_PROCESS_IDLE_MS2 = 10 * 60 * 1e3;
|
|
@@ -129178,6 +129409,8 @@ var CodexProcessPool = class {
|
|
|
129178
129409
|
this.#pending.clear();
|
|
129179
129410
|
this.#threadOwners.clear();
|
|
129180
129411
|
this.#currentSpawnReadyResolved = false;
|
|
129412
|
+
child.stdin?.on("error", () => {
|
|
129413
|
+
});
|
|
129181
129414
|
void this.#writePidFileBestEffort(child.pid);
|
|
129182
129415
|
this.#ready = new Promise((resolve2, reject) => {
|
|
129183
129416
|
this.#readyResolve = resolve2;
|
|
@@ -129295,7 +129528,7 @@ var CodexProcessPool = class {
|
|
|
129295
129528
|
try {
|
|
129296
129529
|
const raw = await readFile7(this.#pidFilePath, "utf8");
|
|
129297
129530
|
const parsed = JSON.parse(raw);
|
|
129298
|
-
if (parsed.pid === pid) await
|
|
129531
|
+
if (parsed.pid === pid) await unlink5(this.#pidFilePath);
|
|
129299
129532
|
} catch {
|
|
129300
129533
|
}
|
|
129301
129534
|
}
|
|
@@ -129451,7 +129684,7 @@ async function reapOrphanedWarmProcess(pidFilePath) {
|
|
|
129451
129684
|
);
|
|
129452
129685
|
}
|
|
129453
129686
|
}
|
|
129454
|
-
await
|
|
129687
|
+
await unlink5(pidFilePath).catch(() => {
|
|
129455
129688
|
});
|
|
129456
129689
|
}
|
|
129457
129690
|
|
|
@@ -129715,12 +129948,33 @@ function handleUnhandledRejection(reason, log = console) {
|
|
|
129715
129948
|
rendered
|
|
129716
129949
|
);
|
|
129717
129950
|
}
|
|
129951
|
+
var BREAKER_THRESHOLD = 50;
|
|
129952
|
+
var BREAKER_WINDOW_MS = 6e4;
|
|
129953
|
+
function createCrashStormBreaker(threshold = BREAKER_THRESHOLD, windowMs = BREAKER_WINDOW_MS) {
|
|
129954
|
+
const hits = [];
|
|
129955
|
+
return function note() {
|
|
129956
|
+
const now = Date.now();
|
|
129957
|
+
hits.push(now);
|
|
129958
|
+
while (hits.length > 0 && hits[0] < now - windowMs) hits.shift();
|
|
129959
|
+
return hits.length >= threshold;
|
|
129960
|
+
};
|
|
129961
|
+
}
|
|
129718
129962
|
function registerCrashGuard(log = console) {
|
|
129963
|
+
const noteCrash = createCrashStormBreaker();
|
|
129964
|
+
const maybeTrip = () => {
|
|
129965
|
+
if (!noteCrash()) return;
|
|
129966
|
+
log.error(
|
|
129967
|
+
`[larkway] crash-storm breaker TRIPPED: \u2265${BREAKER_THRESHOLD} crash-guard hits within ${BREAKER_WINDOW_MS / 1e3}s \u2014 the process is likely wedged, not weathering a transient. Exiting 1 so a supervisor can restart with a clean process.`
|
|
129968
|
+
);
|
|
129969
|
+
process.exit(1);
|
|
129970
|
+
};
|
|
129719
129971
|
process.on("uncaughtException", (err, origin) => {
|
|
129720
129972
|
handleUncaughtException(err, origin, log);
|
|
129973
|
+
maybeTrip();
|
|
129721
129974
|
});
|
|
129722
129975
|
process.on("unhandledRejection", (reason) => {
|
|
129723
129976
|
handleUnhandledRejection(reason, log);
|
|
129977
|
+
maybeTrip();
|
|
129724
129978
|
});
|
|
129725
129979
|
}
|
|
129726
129980
|
|
|
@@ -131508,7 +131762,7 @@ function printExternalCliProbe(bots) {
|
|
|
131508
131762
|
}
|
|
131509
131763
|
async function fetchBotAvatar(appId, appSecret) {
|
|
131510
131764
|
try {
|
|
131511
|
-
const client = new import_node_sdk2.Client({ appId, appSecret });
|
|
131765
|
+
const client = new import_node_sdk2.Client({ appId, appSecret, logger: silentSdkLogger });
|
|
131512
131766
|
const resp = await client.request({
|
|
131513
131767
|
method: "GET",
|
|
131514
131768
|
url: "/open-apis/bot/v3/info"
|
|
@@ -131672,7 +131926,7 @@ async function runV2Mode({
|
|
|
131672
131926
|
const taskSdkClient = new import_node_sdk2.Client({
|
|
131673
131927
|
appId: bot.app_id,
|
|
131674
131928
|
appSecret,
|
|
131675
|
-
|
|
131929
|
+
logger: silentSdkLogger
|
|
131676
131930
|
});
|
|
131677
131931
|
const taskRequester = {
|
|
131678
131932
|
request: (config2) => taskSdkClient.request(config2)
|
|
@@ -131680,8 +131934,7 @@ async function runV2Mode({
|
|
|
131680
131934
|
const taskListClient = new TaskListClient(taskRequester);
|
|
131681
131935
|
await taskListClient.addTasklistMembers(guid, [{ id: bot.app_id, type: "app", role: "editor" }]).catch((err) => {
|
|
131682
131936
|
console.warn(
|
|
131683
|
-
`[larkway] bot "${bot.id}": self-join tasklist ${guid} as editor failed (continuing, best-effort)
|
|
131684
|
-
err
|
|
131937
|
+
`[larkway] bot "${bot.id}": self-join tasklist ${guid} as editor failed (continuing, best-effort): ${compactErrorText(err)}`
|
|
131685
131938
|
);
|
|
131686
131939
|
});
|
|
131687
131940
|
effectiveTaskHandleTasklistGuid = guid;
|