larkin 0.2.74 → 0.2.76
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/assets/larkin-mark-template.svg +3 -0
- package/assets/larkin-mark.svg +7 -0
- package/dist/agent/context-prompt.cjs +2 -2
- package/dist/agent/context-prompt.mjs +2 -2
- package/dist/app/lark-cli.mjs +53 -3
- package/dist/app/official-lark-cli.cjs +88 -20
- package/dist/app/official-lark-cli.mjs +87 -20
- package/dist/runtime/claude-model-catalog.cjs +23 -0
- package/dist/runtime/claude-model-catalog.mjs +23 -0
- package/dist/runtime/codex-model-catalog.cjs +23 -0
- package/dist/runtime/codex-model-catalog.mjs +23 -0
- package/dist/runtime/pi-model-catalog.cjs +24 -0
- package/dist/runtime/pi-model-catalog.mjs +24 -0
- package/dist/runtime/runtime-host.cjs +7 -3
- package/dist/runtime/runtime-host.mjs +7 -3
- package/package.json +2 -1
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title id="title">LARKIN</title>
|
|
3
|
+
<desc id="desc">A swift lark built from three folded ribbons.</desc>
|
|
4
|
+
<path fill="#00B8D9" d="M8 35.5C11.2 20.8 22.4 9.8 39.8 6l-9.4 18.2-13.2 15Z"/>
|
|
5
|
+
<path fill="#1456F0" d="M8 35.5c11.8 8.2 24.7 8.7 35.4-.6L56 23.8l-5.1 17.8C39.3 54.4 22.2 56.8 8 47.4Z"/>
|
|
6
|
+
<path fill="#A3402C" d="M13.8 38.2c3.7-10.4 12.1-18.3 25.4-21.7l-7.1 13.8-9.3 11.2c-3.1-.6-6.1-1.7-9-3.3Z"/>
|
|
7
|
+
</svg>
|
|
@@ -16,7 +16,7 @@ const agent_cli_capabilities_js_1 = require("./agent-cli-capabilities.cjs");
|
|
|
16
16
|
* 5. 版本化:每次实质改动 bump LARKIN_STANDING_PROMPT_VERSION,并同步 eval 数据集/graders/断言。
|
|
17
17
|
* 6. 用 eval 验证:行为变化必须配套固定场景 + rubric(evals/*、test/support/*-grader.mjs、live 测试)。
|
|
18
18
|
*/
|
|
19
|
-
exports.LARKIN_STANDING_PROMPT_VERSION = "larkin-standing-
|
|
19
|
+
exports.LARKIN_STANDING_PROMPT_VERSION = "larkin-standing-v19";
|
|
20
20
|
/**
|
|
21
21
|
* Agent 间协作唤醒引导(issue #75):纯文本 @ 不会产生飞书 mention 事件,
|
|
22
22
|
* 必须由模型直接构造真实的 mention 元素;`--mention` 扩展参数已移除。
|
|
@@ -154,7 +154,7 @@ class ContextPromptBuilder {
|
|
|
154
154
|
"For the known canonical Inbox poll, scoped chat/thread history, and exact send/reply recipes specified here, execute them directly; you must not read or re-read a skill file or reference merely to rediscover or confirm their command syntax.",
|
|
155
155
|
`For a task that starts from an exact group name and asks for user and bot counts, after the required Inbox poll use this known canonical two-read recipe directly: first run \`${executable} im +chat-search --query '<exact_group_name>' --json\`, require one result with an exact name match and confirm its \`oc_\` chat id, then run \`${executable} im chats get --chat-id <confirmed_oc_chat_id> --json\` and answer from its \`user_count\` and \`bot_count\`. This group user/bot counts recipe uses exactly two post-poll business read calls; you must not read or re-read a skill or reference, open help or schema, invoke bare \`lark-cli\`, call \`chat.members\` or \`+chat-members-list\`, fall back to \`+chat-list\`, or add shell filters or output truncation. If the exact name is not a unique visible match, the chat id is not confirmed, or either count is missing, fail visibly without another discovery path or a guess.`,
|
|
156
156
|
"This narrow direct-recipe rule does not waive required skill safety gates, authorization, identity, freshness, or other safety checks. Unknown commands or high-risk operations still require the applicable skill/reference guidance or help.",
|
|
157
|
-
"The Larkin wrapper derives the stable per-intent idempotency key; do not pass `--idempotency-key` in an ordinary send or reply recipe.",
|
|
157
|
+
"The Larkin wrapper derives the stable per-intent idempotency key; do not pass `--idempotency-key` in an ordinary send or reply recipe. A successful write result carrying `duplicate: true` means the provider returned the earlier delivery for the same derived key: the message was already delivered and no new message was created. Do not resend the same command after a `duplicate: true` result; treat the message as delivered. To deliberately send identical content again as a fresh intent (e.g., a new reminder), pass an explicit new `--idempotency-key`, which the wrapper respects instead of deriving its own.",
|
|
158
158
|
"Both `freshness_unavailable` and `freshness_conflict` are pre-commit, provider-not-reached results. If a retry is warranted for an unchanged exact operation, rerun the identical safe ordinary command: `--text` for a direct literal or the same deterministic `--content` dataflow for a tool source. The wrapper reuses its derived key. If the target or body changes after reconsideration, run the revised ordinary command and let the wrapper derive a new key. A result with `committed=true` must not be repeated; ambiguous termination follows the existing wrapper same-key recovery contract.",
|
|
159
159
|
"For multiline `--markdown` or `--text` content in zsh or bash, prefer shell ANSI-C quoting so the command passes one argument with real newline characters: `--markdown $'First line\\nSecond line'` or `--text $'First line\\nSecond line'`. Putting `\"First line\\nSecond line\"` in ordinary double quotes is wrong: ordinary quotes do not decode `\\n`, so lark-cli and Feishu receive a backslash followed by the letter `n`. A literal multiline argument containing real newline characters is also valid. If ANSI-C-quoted content contains an apostrophe, use a safe shell single-quote splice or the literal-newline form; never use `eval`, `echo`, a temporary file, or unsafe variable interpolation to construct the body.",
|
|
160
160
|
...FEISHU_IM_COMMAND_GROUPS.map(([label, suffixes]) => `- ${label}: ${suffixes.map((suffix) => `\`${executable} ${suffix}\``).join(", ")}.`),
|
|
@@ -13,7 +13,7 @@ import { agentCliPromptCapabilities } from "./agent-cli-capabilities.mjs";
|
|
|
13
13
|
* 5. 版本化:每次实质改动 bump LARKIN_STANDING_PROMPT_VERSION,并同步 eval 数据集/graders/断言。
|
|
14
14
|
* 6. 用 eval 验证:行为变化必须配套固定场景 + rubric(evals/*、test/support/*-grader.mjs、live 测试)。
|
|
15
15
|
*/
|
|
16
|
-
export const LARKIN_STANDING_PROMPT_VERSION = "larkin-standing-
|
|
16
|
+
export const LARKIN_STANDING_PROMPT_VERSION = "larkin-standing-v19";
|
|
17
17
|
/**
|
|
18
18
|
* Agent 间协作唤醒引导(issue #75):纯文本 @ 不会产生飞书 mention 事件,
|
|
19
19
|
* 必须由模型直接构造真实的 mention 元素;`--mention` 扩展参数已移除。
|
|
@@ -151,7 +151,7 @@ export class ContextPromptBuilder {
|
|
|
151
151
|
"For the known canonical Inbox poll, scoped chat/thread history, and exact send/reply recipes specified here, execute them directly; you must not read or re-read a skill file or reference merely to rediscover or confirm their command syntax.",
|
|
152
152
|
`For a task that starts from an exact group name and asks for user and bot counts, after the required Inbox poll use this known canonical two-read recipe directly: first run \`${executable} im +chat-search --query '<exact_group_name>' --json\`, require one result with an exact name match and confirm its \`oc_\` chat id, then run \`${executable} im chats get --chat-id <confirmed_oc_chat_id> --json\` and answer from its \`user_count\` and \`bot_count\`. This group user/bot counts recipe uses exactly two post-poll business read calls; you must not read or re-read a skill or reference, open help or schema, invoke bare \`lark-cli\`, call \`chat.members\` or \`+chat-members-list\`, fall back to \`+chat-list\`, or add shell filters or output truncation. If the exact name is not a unique visible match, the chat id is not confirmed, or either count is missing, fail visibly without another discovery path or a guess.`,
|
|
153
153
|
"This narrow direct-recipe rule does not waive required skill safety gates, authorization, identity, freshness, or other safety checks. Unknown commands or high-risk operations still require the applicable skill/reference guidance or help.",
|
|
154
|
-
"The Larkin wrapper derives the stable per-intent idempotency key; do not pass `--idempotency-key` in an ordinary send or reply recipe.",
|
|
154
|
+
"The Larkin wrapper derives the stable per-intent idempotency key; do not pass `--idempotency-key` in an ordinary send or reply recipe. A successful write result carrying `duplicate: true` means the provider returned the earlier delivery for the same derived key: the message was already delivered and no new message was created. Do not resend the same command after a `duplicate: true` result; treat the message as delivered. To deliberately send identical content again as a fresh intent (e.g., a new reminder), pass an explicit new `--idempotency-key`, which the wrapper respects instead of deriving its own.",
|
|
155
155
|
"Both `freshness_unavailable` and `freshness_conflict` are pre-commit, provider-not-reached results. If a retry is warranted for an unchanged exact operation, rerun the identical safe ordinary command: `--text` for a direct literal or the same deterministic `--content` dataflow for a tool source. The wrapper reuses its derived key. If the target or body changes after reconsideration, run the revised ordinary command and let the wrapper derive a new key. A result with `committed=true` must not be repeated; ambiguous termination follows the existing wrapper same-key recovery contract.",
|
|
156
156
|
"For multiline `--markdown` or `--text` content in zsh or bash, prefer shell ANSI-C quoting so the command passes one argument with real newline characters: `--markdown $'First line\\nSecond line'` or `--text $'First line\\nSecond line'`. Putting `\"First line\\nSecond line\"` in ordinary double quotes is wrong: ordinary quotes do not decode `\\n`, so lark-cli and Feishu receive a backslash followed by the letter `n`. A literal multiline argument containing real newline characters is also valid. If ANSI-C-quoted content contains an apostrophe, use a safe shell single-quote splice or the literal-newline form; never use `eval`, `echo`, a temporary file, or unsafe variable interpolation to construct the body.",
|
|
157
157
|
...FEISHU_IM_COMMAND_GROUPS.map(([label, suffixes]) => `- ${label}: ${suffixes.map((suffix) => `\`${executable} ${suffix}\``).join(", ")}.`),
|
package/dist/app/lark-cli.mjs
CHANGED
|
@@ -269,6 +269,24 @@ function parseCommentReply(argv) {
|
|
|
269
269
|
throw new Error("comment reply 的 --text 超过 20000 字符");
|
|
270
270
|
return { messageId, text };
|
|
271
271
|
}
|
|
272
|
+
const IM_WRITE_MEMO_LIMIT = 512;
|
|
273
|
+
// 只标注、不拦截:每次成功写把「实际生效的幂等 key → 服务端返回的 message_id」记进备忘。
|
|
274
|
+
// 同 key 再次成功且服务端返回同一个 message_id,说明服务端走了幂等去重(没有产生新消息),
|
|
275
|
+
// 返回 true 供输出标注 duplicate。拦截权始终在服务端,备忘不会吞掉任何发送。
|
|
276
|
+
function recordImWriteMemo(store, key, messageId) {
|
|
277
|
+
let duplicate = false;
|
|
278
|
+
store.mutateJson("freshnessState", { version: 1, cursors: {} }, (state) => {
|
|
279
|
+
state.im_write_memo ??= {};
|
|
280
|
+
const prior = state.im_write_memo[key];
|
|
281
|
+
if (prior && prior.message_id === messageId)
|
|
282
|
+
duplicate = true;
|
|
283
|
+
state.im_write_memo[key] = { message_id: messageId, updated_at: new Date().toISOString() };
|
|
284
|
+
const keys = Object.keys(state.im_write_memo);
|
|
285
|
+
for (const stale of keys.slice(0, Math.max(0, keys.length - IM_WRITE_MEMO_LIMIT)))
|
|
286
|
+
delete state.im_write_memo[stale];
|
|
287
|
+
});
|
|
288
|
+
return duplicate;
|
|
289
|
+
}
|
|
272
290
|
function runCommentReply(argv, privateEnv, io, dependencies, store) {
|
|
273
291
|
const input = parseCommentReply(argv);
|
|
274
292
|
const targetKey = store.resolveInboxMessageTarget(input.messageId);
|
|
@@ -499,9 +517,12 @@ function parseHistory(result, target, requireBotIdentity = false) {
|
|
|
499
517
|
}
|
|
500
518
|
return { messages };
|
|
501
519
|
}
|
|
502
|
-
|
|
520
|
+
// 防重编号只由 target + argv 决定:同一逻辑命令的重试永远得到同一个 key,
|
|
521
|
+
// 服务端幂等去重才能生效。cursor(freshness 水位)会随每次观察读/冲突合并推进,
|
|
522
|
+
// 掺入会让「已送达但回执丢失」的重试漂移成新 key,从而发出第二条消息。
|
|
523
|
+
function intentId(target, argv) {
|
|
503
524
|
const fingerprint = createHash("sha256")
|
|
504
|
-
.update(JSON.stringify([target,
|
|
525
|
+
.update(JSON.stringify([target, argv])).digest("hex");
|
|
505
526
|
return `larkin-${fingerprint.slice(0, 32)}`;
|
|
506
527
|
}
|
|
507
528
|
function emitFreshnessError(io, input) {
|
|
@@ -709,6 +730,26 @@ function emitNativeResult(result, io) {
|
|
|
709
730
|
}
|
|
710
731
|
return result.status ?? (result.signal ? portableSignalCode(result.signal) : 1);
|
|
711
732
|
}
|
|
733
|
+
function emitDuplicatedWrite(result, io, input) {
|
|
734
|
+
let providerResponse;
|
|
735
|
+
try {
|
|
736
|
+
providerResponse = JSON.parse(result.stdout || "");
|
|
737
|
+
}
|
|
738
|
+
catch {
|
|
739
|
+
providerResponse = { raw_stdout: result.stdout || "" };
|
|
740
|
+
}
|
|
741
|
+
const providerDocument = providerResponse && typeof providerResponse === "object" && !Array.isArray(providerResponse)
|
|
742
|
+
? providerResponse
|
|
743
|
+
: { provider_response: providerResponse };
|
|
744
|
+
io.stdout(`${JSON.stringify({
|
|
745
|
+
...providerDocument,
|
|
746
|
+
ok: true,
|
|
747
|
+
duplicate: true,
|
|
748
|
+
target: input.target,
|
|
749
|
+
...(result.stderr ? { provider_stderr_present: true } : {}),
|
|
750
|
+
})}\n`);
|
|
751
|
+
return 0;
|
|
752
|
+
}
|
|
712
753
|
function emitCommittedUnverified(result, io, input) {
|
|
713
754
|
let providerResponse;
|
|
714
755
|
try {
|
|
@@ -817,7 +858,16 @@ export function runLarkCli(argv, env = process.env, dependencies = {}) {
|
|
|
817
858
|
store.mergeFreshnessCursor(targetKey, gated.current, mergeFeishuImCursor, generation);
|
|
818
859
|
return 3;
|
|
819
860
|
}
|
|
820
|
-
const
|
|
861
|
+
const intentKey = policyFlagValue(effectiveArgv, "--idempotency-key") ?? intentId(targetKey, effectiveArgv);
|
|
862
|
+
const write = callNative(botArgv(effectiveArgv, intentKey), privateEnv, io, nativeDependencies);
|
|
863
|
+
const writeMessage = writeResponseMessage(write);
|
|
864
|
+
const duplicate = !write.error && write.status === 0 && writeMessage
|
|
865
|
+
? recordImWriteMemo(store, intentKey, writeMessage.message_id) : false;
|
|
866
|
+
if (duplicate) {
|
|
867
|
+
observeSuccessfulWrite(write, target, targetKey, store, generation);
|
|
868
|
+
emitDuplicatedWrite(write, io, { target: targetKey });
|
|
869
|
+
return 0;
|
|
870
|
+
}
|
|
821
871
|
if (!write.error && write.status === 0 && !observeSuccessfulWrite(write, target, targetKey, store, generation)) {
|
|
822
872
|
const responseMessage = writeResponseMessage(write);
|
|
823
873
|
try {
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.OFFICIAL_LARK_CLI_INSTALL = exports.OFFICIAL_LARK_CLI_VERSION = void 0;
|
|
7
|
+
exports.invalidateOfficialLarkCliProbeCache = invalidateOfficialLarkCliProbeCache;
|
|
7
8
|
exports.probeOfficialLarkCli = probeOfficialLarkCli;
|
|
8
9
|
exports.resolveOfficialLarkCli = resolveOfficialLarkCli;
|
|
9
10
|
exports.installOfficialLarkCli = installOfficialLarkCli;
|
|
@@ -65,29 +66,29 @@ function officialPackage(executable) {
|
|
|
65
66
|
}
|
|
66
67
|
return null;
|
|
67
68
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
69
|
+
/**
|
|
70
|
+
* 进程内缓存(仅生产路径,即未注入 spawn/shell 时生效):登录 shell 探测
|
|
71
|
+
* 是阻塞 spawnSync,且用户的 .zshrc/.zprofile(nvm 等)偶发需要数十秒才能
|
|
72
|
+
* 返回,daemon 热路径上反复探测会把启动拖到分钟级。同一进程内 PATH/SHELL
|
|
73
|
+
* 不变,解析结果也不会变,因此只探测一次。
|
|
74
|
+
*/
|
|
75
|
+
let productionProbeKey = null;
|
|
76
|
+
let productionProbe = null;
|
|
77
|
+
/** setup 安装/升级官方 CLI 后调用,使同一进程内的后续解析重新探测。 */
|
|
78
|
+
function invalidateOfficialLarkCliProbeCache() {
|
|
79
|
+
productionProbeKey = null;
|
|
80
|
+
productionProbe = null;
|
|
81
|
+
}
|
|
82
|
+
/** 对已确认是官方 @larksuite/cli 的入口做版本与 bind 能力验证(shell 与 PATH 两路共用)。 */
|
|
83
|
+
function validateOfficialCommand(command, env, spawn) {
|
|
83
84
|
if (!compatibleVersion(command.version))
|
|
84
85
|
return {
|
|
85
86
|
state: "outdated",
|
|
86
|
-
reason:
|
|
87
|
+
reason: `官方 lark-cli ${command.version} 低于最低兼容版本 ${exports.OFFICIAL_LARK_CLI_VERSION}: ${command.command}`,
|
|
87
88
|
nextAction: `升级:${exports.OFFICIAL_LARK_CLI_INSTALL}`,
|
|
88
89
|
};
|
|
89
|
-
const version =
|
|
90
|
-
encoding: "utf8", env
|
|
90
|
+
const version = spawn(command.command, ["--version"], {
|
|
91
|
+
encoding: "utf8", env,
|
|
91
92
|
});
|
|
92
93
|
if (version.status !== 0 || version.error || !String(version.stdout || "").includes(command.version))
|
|
93
94
|
return {
|
|
@@ -95,8 +96,8 @@ function probeOfficialLarkCli(dependencies = {}) {
|
|
|
95
96
|
reason: `官方 lark-cli 版本执行验证失败: ${command.command}`,
|
|
96
97
|
nextAction: `重新安装:${exports.OFFICIAL_LARK_CLI_INSTALL}`,
|
|
97
98
|
};
|
|
98
|
-
const bindHelp =
|
|
99
|
-
encoding: "utf8", env
|
|
99
|
+
const bindHelp = spawn(command.command, ["config", "bind", "--help"], {
|
|
100
|
+
encoding: "utf8", env,
|
|
100
101
|
});
|
|
101
102
|
const bindText = `${bindHelp.stdout || ""}\n${bindHelp.stderr || ""}`;
|
|
102
103
|
if (bindHelp.status !== 0 || bindHelp.error || !/--source/.test(bindText) || !/lark-channel/.test(bindText) || !/--identity/.test(bindText))
|
|
@@ -107,6 +108,72 @@ function probeOfficialLarkCli(dependencies = {}) {
|
|
|
107
108
|
};
|
|
108
109
|
return { state: "ready", command };
|
|
109
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* PATH 直接解析快路径:daemon(launchd)的 PATH 通常已包含官方 lark-cli,
|
|
113
|
+
* 此时完全不需要拉起登录 shell(这是启动变慢的主要来源之一)。
|
|
114
|
+
* 仅当 PATH 解析出可用的官方 CLI 时才返回结果;否则返回 null 回退到
|
|
115
|
+
* 登录 shell 探测(保留对 shell 自定义 PATH 环境的兼容)。
|
|
116
|
+
*/
|
|
117
|
+
function probePathResolution(env) {
|
|
118
|
+
const dirs = String(env.PATH || "").split(node_path_1.default.delimiter).filter(Boolean);
|
|
119
|
+
for (const dir of dirs) {
|
|
120
|
+
const candidate = node_path_1.default.resolve(dir, "lark-cli");
|
|
121
|
+
try {
|
|
122
|
+
if (!node_fs_1.default.statSync(candidate).isFile())
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const command = officialPackage(candidate);
|
|
129
|
+
if (!command)
|
|
130
|
+
return null;
|
|
131
|
+
return validateOfficialCommand(command, env, node_child_process_1.spawnSync);
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
function probeOfficialLarkCliUncached(dependencies) {
|
|
136
|
+
const env = dependencies.env ?? process.env;
|
|
137
|
+
if (!dependencies.spawn && !dependencies.shell) {
|
|
138
|
+
const fromPath = probePathResolution(env);
|
|
139
|
+
if (fromPath)
|
|
140
|
+
return fromPath;
|
|
141
|
+
}
|
|
142
|
+
const executable = loginShellPath(dependencies);
|
|
143
|
+
if (!executable)
|
|
144
|
+
return {
|
|
145
|
+
state: "missing",
|
|
146
|
+
reason: "真实 login shell 找不到官方 lark-cli",
|
|
147
|
+
nextAction: `运行 larkin setup,并确认执行:${exports.OFFICIAL_LARK_CLI_INSTALL}`,
|
|
148
|
+
};
|
|
149
|
+
const command = officialPackage(executable);
|
|
150
|
+
if (!command)
|
|
151
|
+
return {
|
|
152
|
+
state: "conflict",
|
|
153
|
+
reason: `真实 login shell 的 lark-cli 不是兼容的官方 @larksuite/cli ${exports.OFFICIAL_LARK_CLI_VERSION}: ${executable}`,
|
|
154
|
+
nextAction: "移除或调整冲突入口后重新运行 larkin setup",
|
|
155
|
+
};
|
|
156
|
+
if (!compatibleVersion(command.version))
|
|
157
|
+
return {
|
|
158
|
+
state: "outdated",
|
|
159
|
+
reason: `真实 login shell 的官方 @larksuite/cli ${command.version} 低于最低兼容版本 ${exports.OFFICIAL_LARK_CLI_VERSION}`,
|
|
160
|
+
nextAction: `升级:${exports.OFFICIAL_LARK_CLI_INSTALL}`,
|
|
161
|
+
};
|
|
162
|
+
return validateOfficialCommand(command, env, dependencies.spawn ?? node_child_process_1.spawnSync);
|
|
163
|
+
}
|
|
164
|
+
function probeOfficialLarkCli(dependencies = {}) {
|
|
165
|
+
if (!dependencies.spawn && !dependencies.shell) {
|
|
166
|
+
const env = dependencies.env ?? process.env;
|
|
167
|
+
const key = `${env.SHELL || ""}|${env.PATH || ""}`;
|
|
168
|
+
if (productionProbe && productionProbeKey === key)
|
|
169
|
+
return productionProbe;
|
|
170
|
+
const probe = probeOfficialLarkCliUncached({ env });
|
|
171
|
+
productionProbeKey = key;
|
|
172
|
+
productionProbe = probe;
|
|
173
|
+
return probe;
|
|
174
|
+
}
|
|
175
|
+
return probeOfficialLarkCliUncached(dependencies);
|
|
176
|
+
}
|
|
110
177
|
function resolveOfficialLarkCli(dependencies = {}) {
|
|
111
178
|
const result = probeOfficialLarkCli(dependencies);
|
|
112
179
|
if (result.state !== "ready")
|
|
@@ -120,6 +187,7 @@ function installOfficialLarkCli(dependencies = {}) {
|
|
|
120
187
|
});
|
|
121
188
|
if (result.status !== 0 || result.error)
|
|
122
189
|
throw new Error(`官方 lark-cli 安装失败(exit=${result.status ?? "none"})`);
|
|
190
|
+
invalidateOfficialLarkCliProbeCache();
|
|
123
191
|
return resolveOfficialLarkCli(dependencies);
|
|
124
192
|
}
|
|
125
193
|
async function ensureOfficialLarkCliForSetup(input) {
|
|
@@ -55,29 +55,29 @@ function officialPackage(executable) {
|
|
|
55
55
|
}
|
|
56
56
|
return null;
|
|
57
57
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
58
|
+
/**
|
|
59
|
+
* 进程内缓存(仅生产路径,即未注入 spawn/shell 时生效):登录 shell 探测
|
|
60
|
+
* 是阻塞 spawnSync,且用户的 .zshrc/.zprofile(nvm 等)偶发需要数十秒才能
|
|
61
|
+
* 返回,daemon 热路径上反复探测会把启动拖到分钟级。同一进程内 PATH/SHELL
|
|
62
|
+
* 不变,解析结果也不会变,因此只探测一次。
|
|
63
|
+
*/
|
|
64
|
+
let productionProbeKey = null;
|
|
65
|
+
let productionProbe = null;
|
|
66
|
+
/** setup 安装/升级官方 CLI 后调用,使同一进程内的后续解析重新探测。 */
|
|
67
|
+
export function invalidateOfficialLarkCliProbeCache() {
|
|
68
|
+
productionProbeKey = null;
|
|
69
|
+
productionProbe = null;
|
|
70
|
+
}
|
|
71
|
+
/** 对已确认是官方 @larksuite/cli 的入口做版本与 bind 能力验证(shell 与 PATH 两路共用)。 */
|
|
72
|
+
function validateOfficialCommand(command, env, spawn) {
|
|
73
73
|
if (!compatibleVersion(command.version))
|
|
74
74
|
return {
|
|
75
75
|
state: "outdated",
|
|
76
|
-
reason:
|
|
76
|
+
reason: `官方 lark-cli ${command.version} 低于最低兼容版本 ${OFFICIAL_LARK_CLI_VERSION}: ${command.command}`,
|
|
77
77
|
nextAction: `升级:${OFFICIAL_LARK_CLI_INSTALL}`,
|
|
78
78
|
};
|
|
79
|
-
const version =
|
|
80
|
-
encoding: "utf8", env
|
|
79
|
+
const version = spawn(command.command, ["--version"], {
|
|
80
|
+
encoding: "utf8", env,
|
|
81
81
|
});
|
|
82
82
|
if (version.status !== 0 || version.error || !String(version.stdout || "").includes(command.version))
|
|
83
83
|
return {
|
|
@@ -85,8 +85,8 @@ export function probeOfficialLarkCli(dependencies = {}) {
|
|
|
85
85
|
reason: `官方 lark-cli 版本执行验证失败: ${command.command}`,
|
|
86
86
|
nextAction: `重新安装:${OFFICIAL_LARK_CLI_INSTALL}`,
|
|
87
87
|
};
|
|
88
|
-
const bindHelp =
|
|
89
|
-
encoding: "utf8", env
|
|
88
|
+
const bindHelp = spawn(command.command, ["config", "bind", "--help"], {
|
|
89
|
+
encoding: "utf8", env,
|
|
90
90
|
});
|
|
91
91
|
const bindText = `${bindHelp.stdout || ""}\n${bindHelp.stderr || ""}`;
|
|
92
92
|
if (bindHelp.status !== 0 || bindHelp.error || !/--source/.test(bindText) || !/lark-channel/.test(bindText) || !/--identity/.test(bindText))
|
|
@@ -97,6 +97,72 @@ export function probeOfficialLarkCli(dependencies = {}) {
|
|
|
97
97
|
};
|
|
98
98
|
return { state: "ready", command };
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* PATH 直接解析快路径:daemon(launchd)的 PATH 通常已包含官方 lark-cli,
|
|
102
|
+
* 此时完全不需要拉起登录 shell(这是启动变慢的主要来源之一)。
|
|
103
|
+
* 仅当 PATH 解析出可用的官方 CLI 时才返回结果;否则返回 null 回退到
|
|
104
|
+
* 登录 shell 探测(保留对 shell 自定义 PATH 环境的兼容)。
|
|
105
|
+
*/
|
|
106
|
+
function probePathResolution(env) {
|
|
107
|
+
const dirs = String(env.PATH || "").split(path.delimiter).filter(Boolean);
|
|
108
|
+
for (const dir of dirs) {
|
|
109
|
+
const candidate = path.resolve(dir, "lark-cli");
|
|
110
|
+
try {
|
|
111
|
+
if (!fs.statSync(candidate).isFile())
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const command = officialPackage(candidate);
|
|
118
|
+
if (!command)
|
|
119
|
+
return null;
|
|
120
|
+
return validateOfficialCommand(command, env, spawnSync);
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
function probeOfficialLarkCliUncached(dependencies) {
|
|
125
|
+
const env = dependencies.env ?? process.env;
|
|
126
|
+
if (!dependencies.spawn && !dependencies.shell) {
|
|
127
|
+
const fromPath = probePathResolution(env);
|
|
128
|
+
if (fromPath)
|
|
129
|
+
return fromPath;
|
|
130
|
+
}
|
|
131
|
+
const executable = loginShellPath(dependencies);
|
|
132
|
+
if (!executable)
|
|
133
|
+
return {
|
|
134
|
+
state: "missing",
|
|
135
|
+
reason: "真实 login shell 找不到官方 lark-cli",
|
|
136
|
+
nextAction: `运行 larkin setup,并确认执行:${OFFICIAL_LARK_CLI_INSTALL}`,
|
|
137
|
+
};
|
|
138
|
+
const command = officialPackage(executable);
|
|
139
|
+
if (!command)
|
|
140
|
+
return {
|
|
141
|
+
state: "conflict",
|
|
142
|
+
reason: `真实 login shell 的 lark-cli 不是兼容的官方 @larksuite/cli ${OFFICIAL_LARK_CLI_VERSION}: ${executable}`,
|
|
143
|
+
nextAction: "移除或调整冲突入口后重新运行 larkin setup",
|
|
144
|
+
};
|
|
145
|
+
if (!compatibleVersion(command.version))
|
|
146
|
+
return {
|
|
147
|
+
state: "outdated",
|
|
148
|
+
reason: `真实 login shell 的官方 @larksuite/cli ${command.version} 低于最低兼容版本 ${OFFICIAL_LARK_CLI_VERSION}`,
|
|
149
|
+
nextAction: `升级:${OFFICIAL_LARK_CLI_INSTALL}`,
|
|
150
|
+
};
|
|
151
|
+
return validateOfficialCommand(command, env, dependencies.spawn ?? spawnSync);
|
|
152
|
+
}
|
|
153
|
+
export function probeOfficialLarkCli(dependencies = {}) {
|
|
154
|
+
if (!dependencies.spawn && !dependencies.shell) {
|
|
155
|
+
const env = dependencies.env ?? process.env;
|
|
156
|
+
const key = `${env.SHELL || ""}|${env.PATH || ""}`;
|
|
157
|
+
if (productionProbe && productionProbeKey === key)
|
|
158
|
+
return productionProbe;
|
|
159
|
+
const probe = probeOfficialLarkCliUncached({ env });
|
|
160
|
+
productionProbeKey = key;
|
|
161
|
+
productionProbe = probe;
|
|
162
|
+
return probe;
|
|
163
|
+
}
|
|
164
|
+
return probeOfficialLarkCliUncached(dependencies);
|
|
165
|
+
}
|
|
100
166
|
export function resolveOfficialLarkCli(dependencies = {}) {
|
|
101
167
|
const result = probeOfficialLarkCli(dependencies);
|
|
102
168
|
if (result.state !== "ready")
|
|
@@ -110,6 +176,7 @@ export function installOfficialLarkCli(dependencies = {}) {
|
|
|
110
176
|
});
|
|
111
177
|
if (result.status !== 0 || result.error)
|
|
112
178
|
throw new Error(`官方 lark-cli 安装失败(exit=${result.status ?? "none"})`);
|
|
179
|
+
invalidateOfficialLarkCliProbeCache();
|
|
113
180
|
return resolveOfficialLarkCli(dependencies);
|
|
114
181
|
}
|
|
115
182
|
export async function ensureOfficialLarkCliForSetup(input) {
|
|
@@ -85,7 +85,30 @@ function parseCatalog(value) {
|
|
|
85
85
|
throw new Error("Claude model catalog has no visible local default");
|
|
86
86
|
return { models, effectiveModel, defaultSupportedReasoningEfforts: efforts(defaultRow || {}) };
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* 进程内缓存:同一 claude 可执行文件的模型目录在进程生命周期内不变,
|
|
90
|
+
* daemon 启动时多个 claude agent 只需探测一次(每次探测都要 spawn 一个
|
|
91
|
+
* claude CLI 进程做 list_models 控制请求)。失败不缓存以便重试;测试注入
|
|
92
|
+
* runClaudeControl 时绕过缓存,保持测试隔离。
|
|
93
|
+
*/
|
|
94
|
+
const discoveryCache = new Map();
|
|
88
95
|
async function discoverClaudeModelCatalog(options = {}) {
|
|
96
|
+
if (!options.runClaudeControl) {
|
|
97
|
+
const key = `${options.command || options.env?.LARKIN_CLAUDE_COMMAND || "claude"}`;
|
|
98
|
+
const cached = discoveryCache.get(key);
|
|
99
|
+
if (cached)
|
|
100
|
+
return cached;
|
|
101
|
+
const pending = discoverClaudeModelCatalogUncached(options);
|
|
102
|
+
discoveryCache.set(key, pending);
|
|
103
|
+
pending.catch(() => {
|
|
104
|
+
if (discoveryCache.get(key) === pending)
|
|
105
|
+
discoveryCache.delete(key);
|
|
106
|
+
});
|
|
107
|
+
return pending;
|
|
108
|
+
}
|
|
109
|
+
return discoverClaudeModelCatalogUncached(options);
|
|
110
|
+
}
|
|
111
|
+
async function discoverClaudeModelCatalogUncached(options) {
|
|
89
112
|
const execute = options.runClaudeControl ?? runClaudeControl;
|
|
90
113
|
const request = { type: "control_request", request_id: "larkin-model-list", request: { subtype: "list_models" } };
|
|
91
114
|
try {
|
|
@@ -82,7 +82,30 @@ function parseCatalog(value) {
|
|
|
82
82
|
throw new Error("Claude model catalog has no visible local default");
|
|
83
83
|
return { models, effectiveModel, defaultSupportedReasoningEfforts: efforts(defaultRow || {}) };
|
|
84
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* 进程内缓存:同一 claude 可执行文件的模型目录在进程生命周期内不变,
|
|
87
|
+
* daemon 启动时多个 claude agent 只需探测一次(每次探测都要 spawn 一个
|
|
88
|
+
* claude CLI 进程做 list_models 控制请求)。失败不缓存以便重试;测试注入
|
|
89
|
+
* runClaudeControl 时绕过缓存,保持测试隔离。
|
|
90
|
+
*/
|
|
91
|
+
const discoveryCache = new Map();
|
|
85
92
|
export async function discoverClaudeModelCatalog(options = {}) {
|
|
93
|
+
if (!options.runClaudeControl) {
|
|
94
|
+
const key = `${options.command || options.env?.LARKIN_CLAUDE_COMMAND || "claude"}`;
|
|
95
|
+
const cached = discoveryCache.get(key);
|
|
96
|
+
if (cached)
|
|
97
|
+
return cached;
|
|
98
|
+
const pending = discoverClaudeModelCatalogUncached(options);
|
|
99
|
+
discoveryCache.set(key, pending);
|
|
100
|
+
pending.catch(() => {
|
|
101
|
+
if (discoveryCache.get(key) === pending)
|
|
102
|
+
discoveryCache.delete(key);
|
|
103
|
+
});
|
|
104
|
+
return pending;
|
|
105
|
+
}
|
|
106
|
+
return discoverClaudeModelCatalogUncached(options);
|
|
107
|
+
}
|
|
108
|
+
async function discoverClaudeModelCatalogUncached(options) {
|
|
86
109
|
const execute = options.runClaudeControl ?? runClaudeControl;
|
|
87
110
|
const request = { type: "control_request", request_id: "larkin-model-list", request: { subtype: "list_models" } };
|
|
88
111
|
try {
|
|
@@ -93,7 +93,30 @@ function parseCatalog(value) {
|
|
|
93
93
|
throw new Error("Codex model catalog has no visible local default");
|
|
94
94
|
return { models, effectiveModel };
|
|
95
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* 进程内缓存:同一 codex 可执行文件的模型目录在进程生命周期内不变,
|
|
98
|
+
* daemon 启动时多个 codex agent 只需探测一次(每次探测都要 spawn 一个
|
|
99
|
+
* codex app-server 进程)。失败不缓存以便重试;测试注入 runCodexAppServer
|
|
100
|
+
* 时绕过缓存,保持测试隔离。
|
|
101
|
+
*/
|
|
102
|
+
const discoveryCache = new Map();
|
|
96
103
|
async function discoverCodexModelCatalog(options = {}) {
|
|
104
|
+
if (!options.runCodexAppServer) {
|
|
105
|
+
const key = `${options.command || options.env?.LARKIN_CODEX_COMMAND || "codex"}`;
|
|
106
|
+
const cached = discoveryCache.get(key);
|
|
107
|
+
if (cached)
|
|
108
|
+
return cached;
|
|
109
|
+
const pending = discoverCodexModelCatalogUncached(options);
|
|
110
|
+
discoveryCache.set(key, pending);
|
|
111
|
+
pending.catch(() => {
|
|
112
|
+
if (discoveryCache.get(key) === pending)
|
|
113
|
+
discoveryCache.delete(key);
|
|
114
|
+
});
|
|
115
|
+
return pending;
|
|
116
|
+
}
|
|
117
|
+
return discoverCodexModelCatalogUncached(options);
|
|
118
|
+
}
|
|
119
|
+
async function discoverCodexModelCatalogUncached(options) {
|
|
97
120
|
const execute = options.runCodexAppServer ?? runCodexAppServer;
|
|
98
121
|
const request = { id: "larkin-model-list", method: "model/list", params: { limit: 100, includeHidden: false } };
|
|
99
122
|
try {
|
|
@@ -90,7 +90,30 @@ function parseCatalog(value) {
|
|
|
90
90
|
throw new Error("Codex model catalog has no visible local default");
|
|
91
91
|
return { models, effectiveModel };
|
|
92
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* 进程内缓存:同一 codex 可执行文件的模型目录在进程生命周期内不变,
|
|
95
|
+
* daemon 启动时多个 codex agent 只需探测一次(每次探测都要 spawn 一个
|
|
96
|
+
* codex app-server 进程)。失败不缓存以便重试;测试注入 runCodexAppServer
|
|
97
|
+
* 时绕过缓存,保持测试隔离。
|
|
98
|
+
*/
|
|
99
|
+
const discoveryCache = new Map();
|
|
93
100
|
export async function discoverCodexModelCatalog(options = {}) {
|
|
101
|
+
if (!options.runCodexAppServer) {
|
|
102
|
+
const key = `${options.command || options.env?.LARKIN_CODEX_COMMAND || "codex"}`;
|
|
103
|
+
const cached = discoveryCache.get(key);
|
|
104
|
+
if (cached)
|
|
105
|
+
return cached;
|
|
106
|
+
const pending = discoverCodexModelCatalogUncached(options);
|
|
107
|
+
discoveryCache.set(key, pending);
|
|
108
|
+
pending.catch(() => {
|
|
109
|
+
if (discoveryCache.get(key) === pending)
|
|
110
|
+
discoveryCache.delete(key);
|
|
111
|
+
});
|
|
112
|
+
return pending;
|
|
113
|
+
}
|
|
114
|
+
return discoverCodexModelCatalogUncached(options);
|
|
115
|
+
}
|
|
116
|
+
async function discoverCodexModelCatalogUncached(options) {
|
|
94
117
|
const execute = options.runCodexAppServer ?? runCodexAppServer;
|
|
95
118
|
const request = { id: "larkin-model-list", method: "model/list", params: { limit: 100, includeHidden: false } };
|
|
96
119
|
try {
|
|
@@ -63,8 +63,32 @@ function findExactPiModel(reference, models) {
|
|
|
63
63
|
return undefined;
|
|
64
64
|
return models.find((model) => piModelId(model) === reference);
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* 进程内缓存:同一个 pi 可执行文件的模型目录在进程生命周期内不变。
|
|
68
|
+
* daemon 启动时 3 个 pi agent 会各发起一次发现探测(每个都 spawn 一个 pi 进程
|
|
69
|
+
* 并做 RPC 握手,约 5s),缓存后只 spawn 一次;失败不缓存以便重试。
|
|
70
|
+
* 测试注入 options.spawn 时绕过缓存,保持测试隔离。
|
|
71
|
+
*/
|
|
72
|
+
const discoveryCache = new Map();
|
|
66
73
|
/** Discover only through Pi's structured RPC protocol; no table parsing or static fallback. */
|
|
67
74
|
async function discoverPiModelCatalog(options) {
|
|
75
|
+
if (!options.spawn) {
|
|
76
|
+
const command = options.command ?? options.env?.LARKIN_PI_COMMAND ?? process.env.LARKIN_PI_COMMAND ?? "pi";
|
|
77
|
+
const key = `${command}|${options.env?.PI_CODING_AGENT_DIR ?? ""}|${options.agentDir ?? ""}`;
|
|
78
|
+
const cached = discoveryCache.get(key);
|
|
79
|
+
if (cached)
|
|
80
|
+
return cached;
|
|
81
|
+
const pending = discoverPiModelCatalogUncached(options);
|
|
82
|
+
discoveryCache.set(key, pending);
|
|
83
|
+
pending.catch(() => {
|
|
84
|
+
if (discoveryCache.get(key) === pending)
|
|
85
|
+
discoveryCache.delete(key);
|
|
86
|
+
});
|
|
87
|
+
return pending;
|
|
88
|
+
}
|
|
89
|
+
return discoverPiModelCatalogUncached(options);
|
|
90
|
+
}
|
|
91
|
+
async function discoverPiModelCatalogUncached(options) {
|
|
68
92
|
const spawn = options.spawn ?? ((command, args, spawnOptions) => (0, node_child_process_1.spawn)(command, [...args], spawnOptions));
|
|
69
93
|
const command = options.command ?? options.env?.LARKIN_PI_COMMAND ?? process.env.LARKIN_PI_COMMAND ?? "pi";
|
|
70
94
|
const child = spawn(command, ["--mode", "rpc", "--no-session"], {
|
|
@@ -22,8 +22,32 @@ export function findExactPiModel(reference, models) {
|
|
|
22
22
|
return undefined;
|
|
23
23
|
return models.find((model) => piModelId(model) === reference);
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* 进程内缓存:同一个 pi 可执行文件的模型目录在进程生命周期内不变。
|
|
27
|
+
* daemon 启动时 3 个 pi agent 会各发起一次发现探测(每个都 spawn 一个 pi 进程
|
|
28
|
+
* 并做 RPC 握手,约 5s),缓存后只 spawn 一次;失败不缓存以便重试。
|
|
29
|
+
* 测试注入 options.spawn 时绕过缓存,保持测试隔离。
|
|
30
|
+
*/
|
|
31
|
+
const discoveryCache = new Map();
|
|
25
32
|
/** Discover only through Pi's structured RPC protocol; no table parsing or static fallback. */
|
|
26
33
|
export async function discoverPiModelCatalog(options) {
|
|
34
|
+
if (!options.spawn) {
|
|
35
|
+
const command = options.command ?? options.env?.LARKIN_PI_COMMAND ?? process.env.LARKIN_PI_COMMAND ?? "pi";
|
|
36
|
+
const key = `${command}|${options.env?.PI_CODING_AGENT_DIR ?? ""}|${options.agentDir ?? ""}`;
|
|
37
|
+
const cached = discoveryCache.get(key);
|
|
38
|
+
if (cached)
|
|
39
|
+
return cached;
|
|
40
|
+
const pending = discoverPiModelCatalogUncached(options);
|
|
41
|
+
discoveryCache.set(key, pending);
|
|
42
|
+
pending.catch(() => {
|
|
43
|
+
if (discoveryCache.get(key) === pending)
|
|
44
|
+
discoveryCache.delete(key);
|
|
45
|
+
});
|
|
46
|
+
return pending;
|
|
47
|
+
}
|
|
48
|
+
return discoverPiModelCatalogUncached(options);
|
|
49
|
+
}
|
|
50
|
+
async function discoverPiModelCatalogUncached(options) {
|
|
27
51
|
const spawn = options.spawn ?? ((command, args, spawnOptions) => nodeSpawn(command, [...args], spawnOptions));
|
|
28
52
|
const command = options.command ?? options.env?.LARKIN_PI_COMMAND ?? process.env.LARKIN_PI_COMMAND ?? "pi";
|
|
29
53
|
const child = spawn(command, ["--mode", "rpc", "--no-session"], {
|
|
@@ -793,10 +793,13 @@ function createRuntimeHost(options) {
|
|
|
793
793
|
let activeCount = 0;
|
|
794
794
|
let recoveringCount = 0;
|
|
795
795
|
const startupFailures = [];
|
|
796
|
-
|
|
796
|
+
// Agents start concurrently: each Agent's runtime session is independent,
|
|
797
|
+
// and serial startup multiplied every per-Agent handshake (login-shell
|
|
798
|
+
// probe + runtime RPC discovery) by the Agent count.
|
|
799
|
+
const startups = configs.map(async (config) => {
|
|
797
800
|
if (managed.has(config.agentId)) {
|
|
798
801
|
activeCount += 1;
|
|
799
|
-
|
|
802
|
+
return;
|
|
800
803
|
}
|
|
801
804
|
const stateStore = options.stateStoreFor?.(config.agentId);
|
|
802
805
|
const persisted = stateStore
|
|
@@ -851,7 +854,8 @@ function createRuntimeHost(options) {
|
|
|
851
854
|
}
|
|
852
855
|
agent.poller = setInterval(() => reconcileExternalConsumption(agent), 250);
|
|
853
856
|
agent.poller.unref?.();
|
|
854
|
-
}
|
|
857
|
+
});
|
|
858
|
+
await Promise.all(startups);
|
|
855
859
|
if (configs.length > 0 && activeCount === 0 && recoveringCount === 0) {
|
|
856
860
|
throw new Error(`No runtime Agent started: ${startupFailures.join("; ")}`);
|
|
857
861
|
}
|
|
@@ -785,10 +785,13 @@ export function createRuntimeHost(options) {
|
|
|
785
785
|
let activeCount = 0;
|
|
786
786
|
let recoveringCount = 0;
|
|
787
787
|
const startupFailures = [];
|
|
788
|
-
|
|
788
|
+
// Agents start concurrently: each Agent's runtime session is independent,
|
|
789
|
+
// and serial startup multiplied every per-Agent handshake (login-shell
|
|
790
|
+
// probe + runtime RPC discovery) by the Agent count.
|
|
791
|
+
const startups = configs.map(async (config) => {
|
|
789
792
|
if (managed.has(config.agentId)) {
|
|
790
793
|
activeCount += 1;
|
|
791
|
-
|
|
794
|
+
return;
|
|
792
795
|
}
|
|
793
796
|
const stateStore = options.stateStoreFor?.(config.agentId);
|
|
794
797
|
const persisted = stateStore
|
|
@@ -843,7 +846,8 @@ export function createRuntimeHost(options) {
|
|
|
843
846
|
}
|
|
844
847
|
agent.poller = setInterval(() => reconcileExternalConsumption(agent), 250);
|
|
845
848
|
agent.poller.unref?.();
|
|
846
|
-
}
|
|
849
|
+
});
|
|
850
|
+
await Promise.all(startups);
|
|
847
851
|
if (configs.length > 0 && activeCount === 0 && recoveringCount === 0) {
|
|
848
852
|
throw new Error(`No runtime Agent started: ${startupFailures.join("; ")}`);
|
|
849
853
|
}
|