larkway 0.3.41 → 0.3.43
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 +31 -16
- package/dist/main.js +350 -59
- 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.43**
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
package/README.zh.md
CHANGED
package/dist/cli/index.js
CHANGED
|
@@ -112702,6 +112702,7 @@ function channelMsgToLarkEvent(msg) {
|
|
|
112702
112702
|
if (!message_id || !chat_id || !senderOpenId) return null;
|
|
112703
112703
|
const thread_id = m?.["thread_id"] ?? msg.threadId ?? m?.["root_id"] ?? msg.rootId ?? message_id;
|
|
112704
112704
|
const root_id = m?.["root_id"] ?? msg.rootId ?? void 0;
|
|
112705
|
+
const parent_id = m?.["parent_id"] ?? msg.replyToMessageId ?? void 0;
|
|
112705
112706
|
const rawContent = typeof m?.["content"] === "string" ? m["content"] : void 0;
|
|
112706
112707
|
const content = rawContent ?? JSON.stringify({ text: stripAtMarkup(msg.content ?? "") });
|
|
112707
112708
|
return {
|
|
@@ -112710,6 +112711,7 @@ function channelMsgToLarkEvent(msg) {
|
|
|
112710
112711
|
chat_type: m?.["chat_type"] ?? msg.chatType ?? "group",
|
|
112711
112712
|
thread_id,
|
|
112712
112713
|
root_id,
|
|
112714
|
+
parent_id,
|
|
112713
112715
|
sender_id: senderOpenId,
|
|
112714
112716
|
mentions: m?.["mentions"] ?? void 0,
|
|
112715
112717
|
content,
|
|
@@ -126102,24 +126104,37 @@ var BotConfigSchema = external_exports.object({
|
|
|
126102
126104
|
*/
|
|
126103
126105
|
effort: external_exports.string().min(1).optional(),
|
|
126104
126106
|
/**
|
|
126105
|
-
* docs/larkway-perf-plan.md §4 —
|
|
126106
|
-
*
|
|
126107
|
-
*
|
|
126108
|
-
*
|
|
126109
|
-
*
|
|
126110
|
-
*
|
|
126111
|
-
*
|
|
126112
|
-
*
|
|
126113
|
-
*
|
|
126114
|
-
*
|
|
126115
|
-
*
|
|
126116
|
-
*
|
|
126117
|
-
*
|
|
126118
|
-
*
|
|
126119
|
-
*
|
|
126120
|
-
*
|
|
126107
|
+
* docs/larkway-perf-plan.md §4 — persistent warm process instead of a
|
|
126108
|
+
* one-shot cold start per turn. Takes effect for `backend: "codex"` (a
|
|
126109
|
+
* single bot-level `codex app-server` process — src/codex/pool.ts
|
|
126110
|
+
* CodexProcessPool) and `backend: "claude"` (one warm process per active
|
|
126111
|
+
* thread — src/claude/pool.ts ClaudeProcessPool). Any other backend value
|
|
126112
|
+
* is a no-op (main.ts only constructs a pool for these two; botLoader's
|
|
126113
|
+
* advisory warn below flags the mismatch upfront).
|
|
126114
|
+
*
|
|
126115
|
+
* 批D (warm-by-default):
|
|
126116
|
+
* DEFAULT IS NOW ON for the two supported backends — `undefined` means
|
|
126117
|
+
* `true` when backend is codex/claude (see {@link effectiveWarmProcess},
|
|
126118
|
+
* the single source of that rule). Set `warmProcess: false` explicitly to
|
|
126119
|
+
* opt a bot back out. `.optional()` (not `.default(true)`) is still
|
|
126120
|
+
* deliberate: a `.default()` would make every future `larkway bot` yaml
|
|
126121
|
+
* write-out bake the value in, and — because this schema is `.strict()` —
|
|
126122
|
+
* would break loading that yaml back with an OLDER larkway build that
|
|
126123
|
+
* predates this field.
|
|
126121
126124
|
*/
|
|
126122
126125
|
warmProcess: external_exports.boolean().optional(),
|
|
126126
|
+
/**
|
|
126127
|
+
* 批D: pre-warm a BLANK agent process at bridge boot (and keep one on
|
|
126128
|
+
* standby thereafter), so even the FIRST message of a NEW thread skips the
|
|
126129
|
+
* cold start. Only meaningful when the warm pool is on (see
|
|
126130
|
+
* `warmProcess`); default true. backend=claude: ClaudeProcessPool keeps
|
|
126131
|
+
* one blank (no --resume) child matching the bot's static spawn signature,
|
|
126132
|
+
* adopted by the first new-session turn and replenished after adoption.
|
|
126133
|
+
* backend=codex: CodexProcessPool spawns its app-server eagerly at boot
|
|
126134
|
+
* instead of on the first turn. Set false to keep warm pooling but skip
|
|
126135
|
+
* the always-resident standby (saves ~300MB RSS per bot at idle).
|
|
126136
|
+
*/
|
|
126137
|
+
prewarmProcess: external_exports.boolean().optional(),
|
|
126123
126138
|
/**
|
|
126124
126139
|
* Idle threshold (ms) before a warm process (see `warmProcess` above) with
|
|
126125
126140
|
* no in-flight turn gets SIGTERM'd. Only meaningful when `warmProcess` is
|
package/dist/main.js
CHANGED
|
@@ -116856,6 +116856,7 @@ function channelMsgToLarkEvent(msg) {
|
|
|
116856
116856
|
if (!message_id || !chat_id || !senderOpenId) return null;
|
|
116857
116857
|
const thread_id = m?.["thread_id"] ?? msg.threadId ?? m?.["root_id"] ?? msg.rootId ?? message_id;
|
|
116858
116858
|
const root_id = m?.["root_id"] ?? msg.rootId ?? void 0;
|
|
116859
|
+
const parent_id = m?.["parent_id"] ?? msg.replyToMessageId ?? void 0;
|
|
116859
116860
|
const rawContent = typeof m?.["content"] === "string" ? m["content"] : void 0;
|
|
116860
116861
|
const content = rawContent ?? JSON.stringify({ text: stripAtMarkup(msg.content ?? "") });
|
|
116861
116862
|
return {
|
|
@@ -116864,6 +116865,7 @@ function channelMsgToLarkEvent(msg) {
|
|
|
116864
116865
|
chat_type: m?.["chat_type"] ?? msg.chatType ?? "group",
|
|
116865
116866
|
thread_id,
|
|
116866
116867
|
root_id,
|
|
116868
|
+
parent_id,
|
|
116867
116869
|
sender_id: senderOpenId,
|
|
116868
116870
|
mentions: m?.["mentions"] ?? void 0,
|
|
116869
116871
|
content,
|
|
@@ -119388,9 +119390,20 @@ async function renderPrompt(input) {
|
|
|
119388
119390
|
taskHandleClaimed = false,
|
|
119389
119391
|
taskHandleCandidates = [],
|
|
119390
119392
|
taskRoot,
|
|
119391
|
-
mtimeFacts = []
|
|
119393
|
+
mtimeFacts = [],
|
|
119394
|
+
queuedFollowups = []
|
|
119392
119395
|
} = input;
|
|
119393
119396
|
const backend = input.backend ?? "claude";
|
|
119397
|
+
const userMessageLines = [
|
|
119398
|
+
"<user-message>",
|
|
119399
|
+
`${parsed.senderOpenId}: ${parsed.text}`,
|
|
119400
|
+
...queuedFollowups.length > 0 ? [
|
|
119401
|
+
"",
|
|
119402
|
+
`(\u4EE5\u4E0B ${queuedFollowups.length} \u6761\u8FFD\u52A0\u6D88\u606F\u5728\u4E0A\u4E00\u8F6E\u5904\u7406\u671F\u95F4\u5230\u8FBE,\u5DF2\u5408\u5E76\u8FDB\u672C\u8F6E \u2014\u2014 \u6309\u987A\u5E8F\u4E00\u5E76\u5904\u7406,\u53EA\u9700\u4E00\u6B21\u7B54\u590D:)`,
|
|
119403
|
+
...queuedFollowups.map((f) => `${f.senderOpenId}: ${f.text}`)
|
|
119404
|
+
] : [],
|
|
119405
|
+
"</user-message>"
|
|
119406
|
+
];
|
|
119394
119407
|
const profileFlag = larkCliProfile ? ` --profile ${larkCliProfile}` : "";
|
|
119395
119408
|
const attachmentKeys = parsed.attachments.map((a) => a.fileKey);
|
|
119396
119409
|
const imageKeys = parsed.attachments.filter((a) => a.fileType === "image").map((a) => a.fileKey);
|
|
@@ -119522,9 +119535,7 @@ async function renderPrompt(input) {
|
|
|
119522
119535
|
...taskHandleBlock.length > 0 ? ["", ...taskHandleBlock] : [],
|
|
119523
119536
|
...taskRootBlock.length > 0 ? ["", ...taskRootBlock] : [],
|
|
119524
119537
|
"",
|
|
119525
|
-
|
|
119526
|
-
`${parsed.senderOpenId}: ${parsed.text}`,
|
|
119527
|
-
"</user-message>"
|
|
119538
|
+
...userMessageLines
|
|
119528
119539
|
].join("\n");
|
|
119529
119540
|
}
|
|
119530
119541
|
return [
|
|
@@ -119579,9 +119590,7 @@ async function renderPrompt(input) {
|
|
|
119579
119590
|
...taskHandleBlock.length > 0 ? ["", ...taskHandleBlock] : [],
|
|
119580
119591
|
...taskRootBlock.length > 0 ? ["", ...taskRootBlock] : [],
|
|
119581
119592
|
"",
|
|
119582
|
-
|
|
119583
|
-
`${parsed.senderOpenId}: ${parsed.text}`,
|
|
119584
|
-
"</user-message>"
|
|
119593
|
+
...userMessageLines
|
|
119585
119594
|
].join("\n");
|
|
119586
119595
|
}
|
|
119587
119596
|
|
|
@@ -119768,6 +119777,12 @@ function renderTranscriptEntry(input) {
|
|
|
119768
119777
|
"",
|
|
119769
119778
|
indentBlock(parsed.text),
|
|
119770
119779
|
"",
|
|
119780
|
+
...input.queuedFollowups && input.queuedFollowups.length > 0 ? [
|
|
119781
|
+
"### Coalesced Follow-ups",
|
|
119782
|
+
"",
|
|
119783
|
+
...input.queuedFollowups.flatMap((f) => [`- ${f.senderOpenId}:`, indentBlock(f.text)]),
|
|
119784
|
+
""
|
|
119785
|
+
] : [],
|
|
119771
119786
|
"### Feishu Doc Links",
|
|
119772
119787
|
"",
|
|
119773
119788
|
...renderList(parsed.feishuDocLinks),
|
|
@@ -122147,6 +122162,20 @@ async function isWorktreeGitHealthy(worktreePath) {
|
|
|
122147
122162
|
child.on("error", () => resolve2(false));
|
|
122148
122163
|
});
|
|
122149
122164
|
}
|
|
122165
|
+
function canCoalesceFollowup(primary, candidate) {
|
|
122166
|
+
if (primary.larkway_trigger_type != null || candidate.larkway_trigger_type != null) return false;
|
|
122167
|
+
if (primary.reply_anchor_message_id != null || candidate.reply_anchor_message_id != null) return false;
|
|
122168
|
+
const sessionKeyOf = (e) => typeof e.root_id === "string" && e.root_id ? e.root_id : e.message_id;
|
|
122169
|
+
if (sessionKeyOf(primary) !== sessionKeyOf(candidate)) return false;
|
|
122170
|
+
try {
|
|
122171
|
+
const parsed = parseMessage(candidate);
|
|
122172
|
+
if (parsed.attachments.length > 0) return false;
|
|
122173
|
+
if (parsed.text.trim() === "") return false;
|
|
122174
|
+
} catch {
|
|
122175
|
+
return false;
|
|
122176
|
+
}
|
|
122177
|
+
return true;
|
|
122178
|
+
}
|
|
122150
122179
|
var repoCloneLocks = /* @__PURE__ */ new Map();
|
|
122151
122180
|
function ensureRepoClone(basePath, url, token, label) {
|
|
122152
122181
|
const prev = repoCloneLocks.get(basePath) ?? Promise.resolve();
|
|
@@ -122496,6 +122525,7 @@ var BridgeHandler = class {
|
|
|
122496
122525
|
async run(opts) {
|
|
122497
122526
|
const signal = opts?.abortSignal;
|
|
122498
122527
|
const threadQueues = /* @__PURE__ */ new Map();
|
|
122528
|
+
const pendingByKey = /* @__PURE__ */ new Map();
|
|
122499
122529
|
const MAX_CONCURRENT = 5;
|
|
122500
122530
|
let running = 0;
|
|
122501
122531
|
const waiters = [];
|
|
@@ -122517,14 +122547,37 @@ var BridgeHandler = class {
|
|
|
122517
122547
|
for await (const event of this.deps.client.events()) {
|
|
122518
122548
|
if (this.closed) break;
|
|
122519
122549
|
if (signal?.aborted) break;
|
|
122520
|
-
const key = typeof event.root_id === "string" && event.root_id ? event.root_id : event.message_id;
|
|
122550
|
+
const key = typeof event.root_id === "string" && event.root_id ? event.root_id : typeof event.parent_id === "string" && event.parent_id ? event.parent_id : event.message_id;
|
|
122521
122551
|
this.threadReceivedAt.set(key, Date.now());
|
|
122552
|
+
const legacySessionKey = typeof event.root_id === "string" && event.root_id ? event.root_id : event.message_id;
|
|
122553
|
+
if (legacySessionKey !== key) this.threadReceivedAt.set(legacySessionKey, Date.now());
|
|
122554
|
+
let pendingList = pendingByKey.get(key);
|
|
122555
|
+
if (pendingList == null) {
|
|
122556
|
+
pendingList = [];
|
|
122557
|
+
pendingByKey.set(key, pendingList);
|
|
122558
|
+
}
|
|
122559
|
+
pendingList.push(event);
|
|
122522
122560
|
const prev = threadQueues.get(key) ?? Promise.resolve();
|
|
122523
|
-
const next = prev.then(() => acquire()).then(() =>
|
|
122561
|
+
const next = prev.then(() => acquire()).then(() => {
|
|
122562
|
+
const pending = pendingByKey.get(key);
|
|
122563
|
+
const primary = pending?.shift();
|
|
122564
|
+
if (primary == null) return;
|
|
122565
|
+
const followups = [];
|
|
122566
|
+
while (pending != null && pending.length > 0 && canCoalesceFollowup(primary, pending[0])) {
|
|
122567
|
+
followups.push(pending.shift());
|
|
122568
|
+
}
|
|
122569
|
+
if (followups.length > 0) {
|
|
122570
|
+
console.log(
|
|
122571
|
+
`[bridge.handler] coalescing ${followups.length} queued follow-up message(s) into turn ${primary.message_id} on thread ${key}`
|
|
122572
|
+
);
|
|
122573
|
+
}
|
|
122574
|
+
return this.handleOne(primary, followups);
|
|
122575
|
+
}).catch((err) => {
|
|
122524
122576
|
console.error(`[bridge.handler] unhandled error on thread ${key}:`, err);
|
|
122525
122577
|
}).finally(() => {
|
|
122526
122578
|
release();
|
|
122527
122579
|
if (threadQueues.get(key) === next) threadQueues.delete(key);
|
|
122580
|
+
if ((pendingByKey.get(key)?.length ?? 0) === 0) pendingByKey.delete(key);
|
|
122528
122581
|
this.inFlightTurns.delete(next);
|
|
122529
122582
|
});
|
|
122530
122583
|
this.inFlightTurns.add(next);
|
|
@@ -122541,7 +122594,14 @@ var BridgeHandler = class {
|
|
|
122541
122594
|
// ---------------------------------------------------------------------------
|
|
122542
122595
|
// Private: single-event lifecycle
|
|
122543
122596
|
// ---------------------------------------------------------------------------
|
|
122544
|
-
|
|
122597
|
+
/**
|
|
122598
|
+
* @param followups 批D gated coalescing — same-session messages that queued
|
|
122599
|
+
* up behind the previous turn and are merged into THIS one (see run()'s
|
|
122600
|
+
* drain loop + canCoalesceFollowup). They contribute their text to the
|
|
122601
|
+
* prompt and settle together with the primary; everything else about the
|
|
122602
|
+
* turn (card, session, task probes) is driven by `event` alone.
|
|
122603
|
+
*/
|
|
122604
|
+
async handleOne(event, followups = []) {
|
|
122545
122605
|
const settleMessageId = event.message_id;
|
|
122546
122606
|
let settled = false;
|
|
122547
122607
|
let agentRunCompleted = false;
|
|
@@ -122551,15 +122611,46 @@ var BridgeHandler = class {
|
|
|
122551
122611
|
const settle = (ok) => {
|
|
122552
122612
|
if (settled) return;
|
|
122553
122613
|
settled = true;
|
|
122554
|
-
|
|
122555
|
-
|
|
122614
|
+
for (const id of [settleMessageId, ...followups.map((f) => f.message_id)]) {
|
|
122615
|
+
if (ok) this.deps.client.markHandled?.(id);
|
|
122616
|
+
else this.deps.client.markUnhandled?.(id, { replay: !agentRunCompleted });
|
|
122617
|
+
}
|
|
122556
122618
|
};
|
|
122557
122619
|
try {
|
|
122558
122620
|
const parsed = parseMessage(event);
|
|
122559
|
-
const {
|
|
122621
|
+
const { messageId, senderOpenId } = parsed;
|
|
122622
|
+
let threadId = parsed.threadId;
|
|
122560
122623
|
const botId = this.deps.botConfig?.id;
|
|
122561
122624
|
const eventLogId = messageId;
|
|
122562
122625
|
const eventStartedAt = Date.now();
|
|
122626
|
+
let taskRootInfo;
|
|
122627
|
+
let taskCardAnchorId;
|
|
122628
|
+
let deferredTaskRootProbe;
|
|
122629
|
+
{
|
|
122630
|
+
const realTopic = realTopicThreadId(parsed.raw.thread_id);
|
|
122631
|
+
const rootCandidate = (typeof parsed.raw.root_id === "string" && parsed.raw.root_id ? parsed.raw.root_id : void 0) ?? (typeof parsed.raw.parent_id === "string" && parsed.raw.parent_id ? parsed.raw.parent_id : void 0);
|
|
122632
|
+
if (rootCandidate && this.deps.messageLookup) {
|
|
122633
|
+
const probe = this.deps.messageLookup.get(rootCandidate).catch(() => void 0);
|
|
122634
|
+
if (realTopic) {
|
|
122635
|
+
deferredTaskRootProbe = probe;
|
|
122636
|
+
} else {
|
|
122637
|
+
const info = await probe;
|
|
122638
|
+
if (info?.msgType === "todo" && info.content) {
|
|
122639
|
+
const todo = parseTodoShareContent(info.content);
|
|
122640
|
+
if (todo) {
|
|
122641
|
+
const probeThreadId = realTopicThreadId(info.threadId);
|
|
122642
|
+
taskRootInfo = {
|
|
122643
|
+
guid: todo.taskGuid,
|
|
122644
|
+
summary: todo.summaryText,
|
|
122645
|
+
topicLink: probeThreadId ? buildTopicDeepLink(parsed.chatId, probeThreadId) : void 0
|
|
122646
|
+
};
|
|
122647
|
+
taskCardAnchorId = rootCandidate;
|
|
122648
|
+
threadId = rootCandidate;
|
|
122649
|
+
}
|
|
122650
|
+
}
|
|
122651
|
+
}
|
|
122652
|
+
}
|
|
122653
|
+
}
|
|
122563
122654
|
const turnReceivedAt = this.threadReceivedAt.get(threadId);
|
|
122564
122655
|
const recordEvent = async (patch) => {
|
|
122565
122656
|
if (!this.deps.recordRuntimeEvent) return;
|
|
@@ -122593,6 +122684,27 @@ var BridgeHandler = class {
|
|
|
122593
122684
|
reason: "\u5DF2\u8FDB\u5165 bridge\uFF0C\u51C6\u5907\u521B\u5EFA\u5904\u7406\u5361\u7247\u3002"
|
|
122594
122685
|
});
|
|
122595
122686
|
await this.deps.client.addProcessingReaction?.(messageId);
|
|
122687
|
+
for (const f of followups) {
|
|
122688
|
+
if (!this.deps.recordRuntimeEvent) break;
|
|
122689
|
+
try {
|
|
122690
|
+
await this.deps.recordRuntimeEvent({
|
|
122691
|
+
id: f.message_id,
|
|
122692
|
+
botId,
|
|
122693
|
+
botName: this.deps.botConfig?.name,
|
|
122694
|
+
messageId: f.message_id,
|
|
122695
|
+
threadId,
|
|
122696
|
+
chatId: parsed.chatId,
|
|
122697
|
+
senderId: typeof f.sender_id === "string" ? f.sender_id : void 0,
|
|
122698
|
+
triggerType,
|
|
122699
|
+
status: "received",
|
|
122700
|
+
receivedAt: new Date(eventStartedAt).toISOString(),
|
|
122701
|
+
statusPath: ["\u5DF2\u6536\u5230", "\u5408\u5E76\u8FDB\u540C\u8F6E"],
|
|
122702
|
+
reason: `\u6392\u961F\u671F\u95F4\u5230\u8FBE,\u5DF2\u5408\u5E76\u8FDB ${messageId} \u7684\u540C\u4E00\u8F6E\u5904\u7406\u3002`
|
|
122703
|
+
});
|
|
122704
|
+
} catch (err) {
|
|
122705
|
+
console.warn("[bridge.handler] recordRuntimeEvent (coalesced followup) failed (continuing):", err);
|
|
122706
|
+
}
|
|
122707
|
+
}
|
|
122596
122708
|
const invokeTaskHandleLifecycle = async (fields) => {
|
|
122597
122709
|
if (!this.deps.taskHandleLifecycle || !botId) return;
|
|
122598
122710
|
try {
|
|
@@ -122607,32 +122719,9 @@ var BridgeHandler = class {
|
|
|
122607
122719
|
const isTopLevel = !(typeof parsed.raw.root_id === "string" && parsed.raw.root_id);
|
|
122608
122720
|
let replyInThread = isTopLevel;
|
|
122609
122721
|
let replyAnchorId = messageId;
|
|
122610
|
-
|
|
122611
|
-
|
|
122612
|
-
|
|
122613
|
-
const rootId = typeof parsed.raw.root_id === "string" && parsed.raw.root_id ? parsed.raw.root_id : void 0;
|
|
122614
|
-
const rawThreadId = realTopicThreadId(parsed.raw.thread_id);
|
|
122615
|
-
if (rootId && this.deps.messageLookup) {
|
|
122616
|
-
const probe = this.deps.messageLookup.get(rootId).catch(() => void 0);
|
|
122617
|
-
if (rawThreadId) {
|
|
122618
|
-
deferredTaskRootProbe = probe;
|
|
122619
|
-
} else {
|
|
122620
|
-
const info = await probe;
|
|
122621
|
-
if (info?.msgType === "todo" && info.content) {
|
|
122622
|
-
const todo = parseTodoShareContent(info.content);
|
|
122623
|
-
if (todo) {
|
|
122624
|
-
const probeThreadId = realTopicThreadId(info.threadId);
|
|
122625
|
-
taskRootInfo = {
|
|
122626
|
-
guid: todo.taskGuid,
|
|
122627
|
-
summary: todo.summaryText,
|
|
122628
|
-
topicLink: probeThreadId ? buildTopicDeepLink(parsed.chatId, probeThreadId) : void 0
|
|
122629
|
-
};
|
|
122630
|
-
replyInThread = true;
|
|
122631
|
-
replyAnchorId = rootId;
|
|
122632
|
-
}
|
|
122633
|
-
}
|
|
122634
|
-
}
|
|
122635
|
-
}
|
|
122722
|
+
if (taskCardAnchorId) {
|
|
122723
|
+
replyInThread = true;
|
|
122724
|
+
replyAnchorId = taskCardAnchorId;
|
|
122636
122725
|
}
|
|
122637
122726
|
const prototypeConfig = this.deps.botConfig?.response_surface_prototype;
|
|
122638
122727
|
const cardKitAvailable = isResponseSurfaceCardKitAvailable(
|
|
@@ -122982,12 +123071,17 @@ var BridgeHandler = class {
|
|
|
122982
123071
|
}
|
|
122983
123072
|
}
|
|
122984
123073
|
}
|
|
123074
|
+
const queuedFollowups = followups.map((f) => {
|
|
123075
|
+
const p = parseMessage(f);
|
|
123076
|
+
return { senderOpenId: p.senderOpenId, text: p.text };
|
|
123077
|
+
});
|
|
122985
123078
|
if (isAgentWorkspace) {
|
|
122986
123079
|
await ensureSessionArtifacts({
|
|
122987
123080
|
sessionPath: worktreePath,
|
|
122988
123081
|
parsed,
|
|
122989
123082
|
isNewThread: existing === void 0,
|
|
122990
|
-
larkCliProfile: this.deps.larkCliProfile
|
|
123083
|
+
larkCliProfile: this.deps.larkCliProfile,
|
|
123084
|
+
queuedFollowups
|
|
122991
123085
|
});
|
|
122992
123086
|
}
|
|
122993
123087
|
try {
|
|
@@ -123104,6 +123198,7 @@ var BridgeHandler = class {
|
|
|
123104
123198
|
const prompt = await renderPrompt({
|
|
123105
123199
|
parsed,
|
|
123106
123200
|
isNewThread: currentIsNewThread,
|
|
123201
|
+
queuedFollowups,
|
|
123107
123202
|
conventions: {
|
|
123108
123203
|
worktreePath,
|
|
123109
123204
|
runtime: conventions.runtime,
|
|
@@ -127029,24 +127124,37 @@ var BotConfigSchema = external_exports.object({
|
|
|
127029
127124
|
*/
|
|
127030
127125
|
effort: external_exports.string().min(1).optional(),
|
|
127031
127126
|
/**
|
|
127032
|
-
* docs/larkway-perf-plan.md §4 —
|
|
127033
|
-
*
|
|
127034
|
-
*
|
|
127035
|
-
*
|
|
127036
|
-
*
|
|
127037
|
-
*
|
|
127038
|
-
*
|
|
127039
|
-
*
|
|
127040
|
-
*
|
|
127041
|
-
*
|
|
127042
|
-
*
|
|
127043
|
-
*
|
|
127044
|
-
*
|
|
127045
|
-
*
|
|
127046
|
-
*
|
|
127047
|
-
*
|
|
127127
|
+
* docs/larkway-perf-plan.md §4 — persistent warm process instead of a
|
|
127128
|
+
* one-shot cold start per turn. Takes effect for `backend: "codex"` (a
|
|
127129
|
+
* single bot-level `codex app-server` process — src/codex/pool.ts
|
|
127130
|
+
* CodexProcessPool) and `backend: "claude"` (one warm process per active
|
|
127131
|
+
* thread — src/claude/pool.ts ClaudeProcessPool). Any other backend value
|
|
127132
|
+
* is a no-op (main.ts only constructs a pool for these two; botLoader's
|
|
127133
|
+
* advisory warn below flags the mismatch upfront).
|
|
127134
|
+
*
|
|
127135
|
+
* 批D (warm-by-default):
|
|
127136
|
+
* DEFAULT IS NOW ON for the two supported backends — `undefined` means
|
|
127137
|
+
* `true` when backend is codex/claude (see {@link effectiveWarmProcess},
|
|
127138
|
+
* the single source of that rule). Set `warmProcess: false` explicitly to
|
|
127139
|
+
* opt a bot back out. `.optional()` (not `.default(true)`) is still
|
|
127140
|
+
* deliberate: a `.default()` would make every future `larkway bot` yaml
|
|
127141
|
+
* write-out bake the value in, and — because this schema is `.strict()` —
|
|
127142
|
+
* would break loading that yaml back with an OLDER larkway build that
|
|
127143
|
+
* predates this field.
|
|
127048
127144
|
*/
|
|
127049
127145
|
warmProcess: external_exports.boolean().optional(),
|
|
127146
|
+
/**
|
|
127147
|
+
* 批D: pre-warm a BLANK agent process at bridge boot (and keep one on
|
|
127148
|
+
* standby thereafter), so even the FIRST message of a NEW thread skips the
|
|
127149
|
+
* cold start. Only meaningful when the warm pool is on (see
|
|
127150
|
+
* `warmProcess`); default true. backend=claude: ClaudeProcessPool keeps
|
|
127151
|
+
* one blank (no --resume) child matching the bot's static spawn signature,
|
|
127152
|
+
* adopted by the first new-session turn and replenished after adoption.
|
|
127153
|
+
* backend=codex: CodexProcessPool spawns its app-server eagerly at boot
|
|
127154
|
+
* instead of on the first turn. Set false to keep warm pooling but skip
|
|
127155
|
+
* the always-resident standby (saves ~300MB RSS per bot at idle).
|
|
127156
|
+
*/
|
|
127157
|
+
prewarmProcess: external_exports.boolean().optional(),
|
|
127050
127158
|
/**
|
|
127051
127159
|
* Idle threshold (ms) before a warm process (see `warmProcess` above) with
|
|
127052
127160
|
* no in-flight turn gets SIGTERM'd. Only meaningful when `warmProcess` is
|
|
@@ -127064,6 +127172,13 @@ var BotConfigSchema = external_exports.object({
|
|
|
127064
127172
|
*/
|
|
127065
127173
|
warmProcessMaxProcesses: external_exports.number().int().positive().optional()
|
|
127066
127174
|
}).strict();
|
|
127175
|
+
function effectiveWarmProcess(bot) {
|
|
127176
|
+
if (bot.backend !== "claude" && bot.backend !== "codex") return bot.warmProcess === true;
|
|
127177
|
+
return bot.warmProcess !== false;
|
|
127178
|
+
}
|
|
127179
|
+
function effectivePrewarmProcess(bot) {
|
|
127180
|
+
return effectiveWarmProcess(bot) && bot.prewarmProcess !== false;
|
|
127181
|
+
}
|
|
127067
127182
|
async function loadBots(botsDir) {
|
|
127068
127183
|
let entries;
|
|
127069
127184
|
try {
|
|
@@ -128337,6 +128452,8 @@ var INTERRUPT_GRACE_MS = 3e3;
|
|
|
128337
128452
|
var DEFAULT_TURN_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
128338
128453
|
var SHUTDOWN_EXIT_WAIT_MS = SIGKILL_GRACE_MS2 + 2e3;
|
|
128339
128454
|
var STDERR_BUFFER_CAP_BYTES = 64 * 1024;
|
|
128455
|
+
var MAX_PREWARM_FAILURES = 3;
|
|
128456
|
+
var PREWARM_RESPAWN_BACKOFF_MS = 1e4;
|
|
128340
128457
|
var ClaudeProcessPool = class {
|
|
128341
128458
|
#botId;
|
|
128342
128459
|
#botGitIdentity;
|
|
@@ -128370,6 +128487,15 @@ var ClaudeProcessPool = class {
|
|
|
128370
128487
|
* -killed while that child still lives. Removed in `#onEntryExit`.
|
|
128371
128488
|
*/
|
|
128372
128489
|
#dying = /* @__PURE__ */ new Set();
|
|
128490
|
+
// -- 批D blank-standby prewarm state ----------------------------------------
|
|
128491
|
+
/** Set by prewarm(); undefined = prewarm never requested (no blank maintenance). */
|
|
128492
|
+
#prewarmProto;
|
|
128493
|
+
/** Consecutive unprompted blank deaths (any age) — resets on a successful adoption. */
|
|
128494
|
+
#prewarmFailures = 0;
|
|
128495
|
+
/** True once the circuit breaker tripped (MAX_PREWARM_FAILURES) — prewarm off until bridge restart. */
|
|
128496
|
+
#prewarmDisabled = false;
|
|
128497
|
+
#prewarmRespawnTimer;
|
|
128498
|
+
#nextBlankId = 1;
|
|
128373
128499
|
constructor(opts) {
|
|
128374
128500
|
this.#botId = opts.botId;
|
|
128375
128501
|
this.#botGitIdentity = opts.botGitIdentity;
|
|
@@ -128386,6 +128512,23 @@ var ClaudeProcessPool = class {
|
|
|
128386
128512
|
get pidsForTesting() {
|
|
128387
128513
|
return [...this.#entries.values()].map((e) => e.child.pid).filter((pid) => pid != null);
|
|
128388
128514
|
}
|
|
128515
|
+
get blankProcessCountForTesting() {
|
|
128516
|
+
return [...this.#entries.values()].filter((e) => e.blank).length;
|
|
128517
|
+
}
|
|
128518
|
+
/**
|
|
128519
|
+
* 批D: start maintaining ONE blank standby child matching `proto`'s spawn
|
|
128520
|
+
* signature — spawned now, adopted by the first new-session turn whose own
|
|
128521
|
+
* options produce the same signature (see PoolEntry.spawnSignature), and
|
|
128522
|
+
* replenished after each adoption. Call once at bridge boot (main.ts),
|
|
128523
|
+
* only for bots whose cwd is static across threads (agent_workspace
|
|
128524
|
+
* runtime); calling it again replaces the proto for future respawns but
|
|
128525
|
+
* never kills an existing blank. No-op after shutdown() or once the
|
|
128526
|
+
* circuit breaker has tripped.
|
|
128527
|
+
*/
|
|
128528
|
+
prewarm(proto) {
|
|
128529
|
+
this.#prewarmProto = proto;
|
|
128530
|
+
this.#maybeSpawnBlank();
|
|
128531
|
+
}
|
|
128389
128532
|
run(opts) {
|
|
128390
128533
|
const queue = new TurnEventQueue();
|
|
128391
128534
|
const markPerf = createPerfMarker(opts.onPerfMarker);
|
|
@@ -128433,6 +128576,9 @@ var ClaudeProcessPool = class {
|
|
|
128433
128576
|
}
|
|
128434
128577
|
}
|
|
128435
128578
|
let entry = this.#entries.get(key);
|
|
128579
|
+
if (entry == null && opts.resumeSessionId == null) {
|
|
128580
|
+
entry = this.#adoptBlankEntry(key, threadId, opts);
|
|
128581
|
+
}
|
|
128436
128582
|
if (entry == null) {
|
|
128437
128583
|
if (this.#entries.size >= this.#maxProcesses) {
|
|
128438
128584
|
const victim = this.#pickLruIdleVictim();
|
|
@@ -128462,6 +128608,10 @@ var ClaudeProcessPool = class {
|
|
|
128462
128608
|
clearInterval(this.#idleSweepTimer);
|
|
128463
128609
|
this.#idleSweepTimer = void 0;
|
|
128464
128610
|
}
|
|
128611
|
+
if (this.#prewarmRespawnTimer) {
|
|
128612
|
+
clearTimeout(this.#prewarmRespawnTimer);
|
|
128613
|
+
this.#prewarmRespawnTimer = void 0;
|
|
128614
|
+
}
|
|
128465
128615
|
const inFlight = [...this.#entries.values()].map((e) => e.current?.done.catch(() => void 0)).filter((p) => p != null);
|
|
128466
128616
|
await Promise.race([
|
|
128467
128617
|
Promise.all(inFlight),
|
|
@@ -128508,6 +128658,9 @@ var ClaudeProcessPool = class {
|
|
|
128508
128658
|
return `${this.#botId}::${threadId}::${cwd}::${model}::${effort}`;
|
|
128509
128659
|
}
|
|
128510
128660
|
#pickLruIdleVictim() {
|
|
128661
|
+
for (const entry of this.#entries.values()) {
|
|
128662
|
+
if (entry.current == null && entry.blank) return entry;
|
|
128663
|
+
}
|
|
128511
128664
|
let victim;
|
|
128512
128665
|
for (const entry of this.#entries.values()) {
|
|
128513
128666
|
if (entry.current != null) continue;
|
|
@@ -128660,7 +128813,89 @@ var ClaudeProcessPool = class {
|
|
|
128660
128813
|
if (state.interruptEscalateTimer) clearTimeout(state.interruptEscalateTimer);
|
|
128661
128814
|
}
|
|
128662
128815
|
// -- process lifecycle -------------------------------------------------------
|
|
128663
|
-
|
|
128816
|
+
/**
|
|
128817
|
+
* The spawn identity a set of run/prewarm options resolves to. Built from
|
|
128818
|
+
* the SAME buildWarmCommand path #spawnEntry actually spawns with, so an
|
|
128819
|
+
* adoption match is by construction a spawn-arg-identical process.
|
|
128820
|
+
* `resumeSessionId` is deliberately dropped: adoption is only ever
|
|
128821
|
+
* attempted for turns with no resume (run() guards), and the blank itself
|
|
128822
|
+
* is spawned without one.
|
|
128823
|
+
*/
|
|
128824
|
+
#spawnSignatureOf(opts) {
|
|
128825
|
+
const [bin, args] = buildWarmCommand({
|
|
128826
|
+
prompt: "",
|
|
128827
|
+
cwd: opts.cwd,
|
|
128828
|
+
model: opts.model,
|
|
128829
|
+
effort: opts.effort,
|
|
128830
|
+
permissionMode: opts.permissionMode,
|
|
128831
|
+
agentBinPath: opts.agentBinPath
|
|
128832
|
+
});
|
|
128833
|
+
return JSON.stringify([bin, args, opts.cwd ?? null]);
|
|
128834
|
+
}
|
|
128835
|
+
/**
|
|
128836
|
+
* 批D: find an idle blank whose spawn signature matches this turn's options
|
|
128837
|
+
* and rekey it in place onto (key, threadId). Returns undefined when no
|
|
128838
|
+
* blank matches — the caller falls through to a normal spawn. Schedules a
|
|
128839
|
+
* replacement blank on success.
|
|
128840
|
+
*/
|
|
128841
|
+
#adoptBlankEntry(key, threadId, opts) {
|
|
128842
|
+
let blank;
|
|
128843
|
+
for (const e of this.#entries.values()) {
|
|
128844
|
+
if (e.blank && !e.destroyed && e.current == null) {
|
|
128845
|
+
blank = e;
|
|
128846
|
+
break;
|
|
128847
|
+
}
|
|
128848
|
+
}
|
|
128849
|
+
if (blank == null) return void 0;
|
|
128850
|
+
const wanted = this.#spawnSignatureOf(opts);
|
|
128851
|
+
if (blank.spawnSignature !== wanted) {
|
|
128852
|
+
console.warn(
|
|
128853
|
+
`[claude-pool] blank standby exists but its spawn signature doesn't match this turn's options \u2014 not adopting (blank=${blank.spawnSignature} turn=${wanted}). Check the prewarm wiring in main.ts.`
|
|
128854
|
+
);
|
|
128855
|
+
return void 0;
|
|
128856
|
+
}
|
|
128857
|
+
this.#entries.delete(blank.key);
|
|
128858
|
+
blank.key = key;
|
|
128859
|
+
blank.threadId = threadId;
|
|
128860
|
+
blank.blank = false;
|
|
128861
|
+
blank.lastUsedAt = Date.now();
|
|
128862
|
+
this.#entries.set(key, blank);
|
|
128863
|
+
this.#rewritePidListBestEffort();
|
|
128864
|
+
this.#prewarmFailures = 0;
|
|
128865
|
+
console.warn(
|
|
128866
|
+
`[claude-pool] blank standby pid=${blank.child.pid ?? "?"} adopted by thread=${threadId} \u2014 this turn skips the cold start entirely.`
|
|
128867
|
+
);
|
|
128868
|
+
queueMicrotask(() => this.#maybeSpawnBlank());
|
|
128869
|
+
return blank;
|
|
128870
|
+
}
|
|
128871
|
+
/**
|
|
128872
|
+
* Spawn the standby blank when prewarm is configured and conditions allow:
|
|
128873
|
+
* at most one blank at a time, never past maxProcesses, never while
|
|
128874
|
+
* shutting down, and never after the circuit breaker tripped.
|
|
128875
|
+
*/
|
|
128876
|
+
#maybeSpawnBlank() {
|
|
128877
|
+
if (this.#prewarmProto == null || this.#prewarmDisabled || this.#shuttingDown) return;
|
|
128878
|
+
if (this.#prewarmRespawnTimer != null) return;
|
|
128879
|
+
if (this.#entries.size >= this.#maxProcesses) return;
|
|
128880
|
+
for (const e of this.#entries.values()) {
|
|
128881
|
+
if (e.blank) return;
|
|
128882
|
+
}
|
|
128883
|
+
const proto = this.#prewarmProto;
|
|
128884
|
+
const key = `__blank__::${this.#botId}::${this.#nextBlankId++}`;
|
|
128885
|
+
const opts = {
|
|
128886
|
+
prompt: "",
|
|
128887
|
+
cwd: proto.cwd,
|
|
128888
|
+
model: proto.model,
|
|
128889
|
+
effort: proto.effort,
|
|
128890
|
+
permissionMode: proto.permissionMode,
|
|
128891
|
+
agentBinPath: proto.agentBinPath
|
|
128892
|
+
};
|
|
128893
|
+
const entry = this.#spawnEntry(key, key, opts, { blank: true });
|
|
128894
|
+
console.warn(
|
|
128895
|
+
`[claude-pool] pre-warmed blank standby pid=${entry.child.pid ?? "?"} for bot=${this.#botId} (signature=${entry.spawnSignature.slice(0, 120)}\u2026)`
|
|
128896
|
+
);
|
|
128897
|
+
}
|
|
128898
|
+
#spawnEntry(key, threadId, opts, flags) {
|
|
128664
128899
|
const [bin, args] = buildWarmCommand(opts);
|
|
128665
128900
|
const env = buildEnv(this.#botGitIdentity, this.#gitlabToken);
|
|
128666
128901
|
const child = spawn3(bin, args, {
|
|
@@ -128672,6 +128907,8 @@ var ClaudeProcessPool = class {
|
|
|
128672
128907
|
key,
|
|
128673
128908
|
threadId,
|
|
128674
128909
|
cwd: opts.cwd,
|
|
128910
|
+
blank: flags?.blank === true,
|
|
128911
|
+
spawnSignature: this.#spawnSignatureOf(opts),
|
|
128675
128912
|
child,
|
|
128676
128913
|
spawnedAt: Date.now(),
|
|
128677
128914
|
lastUsedAt: Date.now(),
|
|
@@ -128737,6 +128974,32 @@ var ClaudeProcessPool = class {
|
|
|
128737
128974
|
this.#dying.delete(entry);
|
|
128738
128975
|
this.#rewritePidListBestEffort();
|
|
128739
128976
|
void this.#deleteRunnerPidFileIfMine(entry);
|
|
128977
|
+
if (this.#prewarmProto != null && !this.#shuttingDown && !this.#prewarmDisabled) {
|
|
128978
|
+
const unpromptedBlankDeath = entry.blank && !wasAlreadyMarkedDestroyed;
|
|
128979
|
+
if (unpromptedBlankDeath) {
|
|
128980
|
+
this.#prewarmFailures += 1;
|
|
128981
|
+
if (this.#prewarmFailures >= MAX_PREWARM_FAILURES) {
|
|
128982
|
+
this.#prewarmDisabled = true;
|
|
128983
|
+
const stderr = Buffer.concat(entry.stderrChunks).toString("utf8").trim().slice(-2e3);
|
|
128984
|
+
console.warn(
|
|
128985
|
+
`[claude-pool] blank standby died unprompted ${this.#prewarmFailures} times in a row \u2014 disabling prewarm until the bridge restarts (warm pooling itself stays on; turns just spawn on demand).` + (stderr ? ` last stderr tail:
|
|
128986
|
+
${stderr}` : "")
|
|
128987
|
+
);
|
|
128988
|
+
} else {
|
|
128989
|
+
const delay = PREWARM_RESPAWN_BACKOFF_MS * 2 ** (this.#prewarmFailures - 1);
|
|
128990
|
+
console.warn(
|
|
128991
|
+
`[claude-pool] blank standby pid=${entry.child.pid ?? "?"} died unprompted after ${Math.round((Date.now() - entry.spawnedAt) / 1e3)}s (${this.#prewarmFailures}/${MAX_PREWARM_FAILURES}) \u2014 respawning in ${delay}ms.`
|
|
128992
|
+
);
|
|
128993
|
+
this.#prewarmRespawnTimer = setTimeout(() => {
|
|
128994
|
+
this.#prewarmRespawnTimer = void 0;
|
|
128995
|
+
this.#maybeSpawnBlank();
|
|
128996
|
+
}, delay);
|
|
128997
|
+
this.#prewarmRespawnTimer.unref?.();
|
|
128998
|
+
}
|
|
128999
|
+
} else {
|
|
129000
|
+
queueMicrotask(() => this.#maybeSpawnBlank());
|
|
129001
|
+
}
|
|
129002
|
+
}
|
|
128740
129003
|
const state = entry.current;
|
|
128741
129004
|
entry.current = void 0;
|
|
128742
129005
|
if (state == null || state.settled) return;
|
|
@@ -128789,6 +129052,7 @@ ${stderr}` : "")
|
|
|
128789
129052
|
const now = Date.now();
|
|
128790
129053
|
for (const entry of [...this.#entries.values()]) {
|
|
128791
129054
|
if (entry.current != null) continue;
|
|
129055
|
+
if (entry.blank) continue;
|
|
128792
129056
|
if (now - entry.lastUsedAt >= this.#idleMs) {
|
|
128793
129057
|
this.#destroyEntry(entry, "idle timeout");
|
|
128794
129058
|
}
|
|
@@ -129408,6 +129672,19 @@ var CodexProcessPool = class {
|
|
|
129408
129672
|
get childPid() {
|
|
129409
129673
|
return this.#child?.pid ?? void 0;
|
|
129410
129674
|
}
|
|
129675
|
+
/**
|
|
129676
|
+
* 批D: spawn the app-server eagerly at bridge boot instead of lazily on the
|
|
129677
|
+
* first turn, so even the bot's first message hits a running process.
|
|
129678
|
+
* Idempotent; no-op while shutting down or after the spawn circuit breaker
|
|
129679
|
+
* (#poolDisabled) tripped. Unlike ClaudeProcessPool's blank standby there
|
|
129680
|
+
* is no post-idle-reap replenish here: after the 10-min idle reap the next
|
|
129681
|
+
* turn re-spawns on demand exactly as before (the reap exists to bound the
|
|
129682
|
+
* app-server's per-thread memory growth — auto-respawning would defeat it).
|
|
129683
|
+
*/
|
|
129684
|
+
prewarm() {
|
|
129685
|
+
if (this.#shuttingDown || this.#poolDisabled) return;
|
|
129686
|
+
if (this.#child == null) this.#spawnChild();
|
|
129687
|
+
}
|
|
129411
129688
|
run(opts) {
|
|
129412
129689
|
const turnKey = this.#nextTurnKey++;
|
|
129413
129690
|
const queue = new TurnEventQueue();
|
|
@@ -132347,7 +132624,8 @@ async function runV2Mode({
|
|
|
132347
132624
|
let codexPool;
|
|
132348
132625
|
let claudePool;
|
|
132349
132626
|
let runnerKey;
|
|
132350
|
-
|
|
132627
|
+
const warmProcessOn = effectiveWarmProcess(bot);
|
|
132628
|
+
if (warmProcessOn && bot.backend === "codex") {
|
|
132351
132629
|
const pidFilePath = path19.join(botDir, "warm-codex.pid");
|
|
132352
132630
|
try {
|
|
132353
132631
|
await reapOrphanedWarmProcess(pidFilePath);
|
|
@@ -132362,7 +132640,8 @@ async function runV2Mode({
|
|
|
132362
132640
|
});
|
|
132363
132641
|
runnerKey = `codex-pool:${bot.id}`;
|
|
132364
132642
|
registerRunner(runnerKey, () => codexPool);
|
|
132365
|
-
|
|
132643
|
+
if (effectivePrewarmProcess(bot)) codexPool.prewarm();
|
|
132644
|
+
} else if (warmProcessOn && bot.backend === "claude") {
|
|
132366
132645
|
const pidListFilePath = path19.join(botDir, "warm-claude.pids.json");
|
|
132367
132646
|
try {
|
|
132368
132647
|
await reapOrphanedWarmClaudeProcesses(pidListFilePath);
|
|
@@ -132379,6 +132658,18 @@ async function runV2Mode({
|
|
|
132379
132658
|
});
|
|
132380
132659
|
runnerKey = `claude-pool:${bot.id}`;
|
|
132381
132660
|
registerRunner(runnerKey, () => claudePool);
|
|
132661
|
+
if (effectivePrewarmProcess(bot) && bot.runtime === "agent_workspace" && agentWorkspacePath) {
|
|
132662
|
+
claudePool.prewarm({
|
|
132663
|
+
cwd: agentWorkspacePath,
|
|
132664
|
+
model: bot.model,
|
|
132665
|
+
effort: bot.effort,
|
|
132666
|
+
// Mirrors handler.ts's own default ("bypassPermissions" when
|
|
132667
|
+
// config.json leaves permissions.mode unset). If these two ever
|
|
132668
|
+
// drift the blank's signature just stops matching — fail-safe, and
|
|
132669
|
+
// the pool logs the mismatch on every missed adoption.
|
|
132670
|
+
permissionMode: configJson.permissions.mode ?? "bypassPermissions"
|
|
132671
|
+
});
|
|
132672
|
+
}
|
|
132382
132673
|
}
|
|
132383
132674
|
const handler = new BridgeHandler({
|
|
132384
132675
|
client,
|