larkin 0.2.74 → 0.2.75

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.
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2
+ <path fill="#000" d="M8 35.5C11.2 20.8 22.4 9.8 39.8 6l-7.1 13.7 5.9 5.8L56 10.1l-5.1 31.5C39.3 54.4 22.2 56.8 8 47.4Z"/>
3
+ </svg>
@@ -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>
@@ -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
- function probeOfficialLarkCli(dependencies = {}) {
69
- const executable = loginShellPath(dependencies);
70
- if (!executable)
71
- return {
72
- state: "missing",
73
- reason: "真实 login shell 找不到官方 lark-cli",
74
- nextAction: `运行 larkin setup,并确认执行:${exports.OFFICIAL_LARK_CLI_INSTALL}`,
75
- };
76
- const command = officialPackage(executable);
77
- if (!command)
78
- return {
79
- state: "conflict",
80
- reason: `真实 login shell 的 lark-cli 不是兼容的官方 @larksuite/cli ${exports.OFFICIAL_LARK_CLI_VERSION}: ${executable}`,
81
- nextAction: "移除或调整冲突入口后重新运行 larkin setup",
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: `真实 login shell 的官方 @larksuite/cli ${command.version} 低于最低兼容版本 ${exports.OFFICIAL_LARK_CLI_VERSION}`,
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 = (dependencies.spawn ?? node_child_process_1.spawnSync)(command.command, ["--version"], {
90
- encoding: "utf8", env: dependencies.env ?? process.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 = (dependencies.spawn ?? node_child_process_1.spawnSync)(command.command, ["config", "bind", "--help"], {
99
- encoding: "utf8", env: dependencies.env ?? process.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
- export function probeOfficialLarkCli(dependencies = {}) {
59
- const executable = loginShellPath(dependencies);
60
- if (!executable)
61
- return {
62
- state: "missing",
63
- reason: "真实 login shell 找不到官方 lark-cli",
64
- nextAction: `运行 larkin setup,并确认执行:${OFFICIAL_LARK_CLI_INSTALL}`,
65
- };
66
- const command = officialPackage(executable);
67
- if (!command)
68
- return {
69
- state: "conflict",
70
- reason: `真实 login shell 的 lark-cli 不是兼容的官方 @larksuite/cli ${OFFICIAL_LARK_CLI_VERSION}: ${executable}`,
71
- nextAction: "移除或调整冲突入口后重新运行 larkin setup",
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: `真实 login shell 的官方 @larksuite/cli ${command.version} 低于最低兼容版本 ${OFFICIAL_LARK_CLI_VERSION}`,
76
+ reason: `官方 lark-cli ${command.version} 低于最低兼容版本 ${OFFICIAL_LARK_CLI_VERSION}: ${command.command}`,
77
77
  nextAction: `升级:${OFFICIAL_LARK_CLI_INSTALL}`,
78
78
  };
79
- const version = (dependencies.spawn ?? spawnSync)(command.command, ["--version"], {
80
- encoding: "utf8", env: dependencies.env ?? process.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 = (dependencies.spawn ?? spawnSync)(command.command, ["config", "bind", "--help"], {
89
- encoding: "utf8", env: dependencies.env ?? process.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
- for (const config of configs) {
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
- continue;
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
- for (const config of configs) {
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
- continue;
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
  }
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "larkin",
3
- "version": "0.2.74",
3
+ "version": "0.2.75",
4
4
  "private": false,
5
5
  "larkinPackageRole": "npm-published",
6
6
  "files": [
7
7
  "dist/",
8
+ "assets/",
8
9
  "README.md",
9
10
  "LICENSE",
10
11
  "SECURITY.md",