larkin 0.5.1 → 0.5.3

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.
@@ -7,8 +7,9 @@ import { fileURLToPath } from "node:url";
7
7
  *
8
8
  * 分发:构建期把 @tintinweb/pi-subagents bundle 成单文件
9
9
  * `dist/runtime/pi-subagents.bundle.js`(pi-* 包 external),运行时仅向 external
10
- * (用户 pi CLI)通过 `pi --extension/-e` 显式注入,不碰用户 ~/.pi 配置。
11
- * Builtin Pi 直接接收静态 factory,不经过本文件或 Pi 的路径加载器。
10
+ * (用户 pi CLI)通过 `pi --extension/-e` 显式注入。可只读用户 ~/.pi/agent
11
+ * 的非密钥设置/扩展元数据以避免重复注册 FATAL;不得读、复制或写 auth.json,
12
+ * 也不得写入 ~/.pi。
12
13
  *
13
14
  * 版本门槛:pi-subagents peerDependency 要求 @earendil-works/pi-* >= 0.80.0;
14
15
  * external 的 pi 版本低于 0.80 时不注入(降级为无 subagent 能力)。
@@ -184,16 +185,49 @@ export function probeExternalPiVersion(piCommand, env) {
184
185
  * 用户 pi 是否已自行安装 pi-subagents(settings.json packages 或包目录)。
185
186
  * 已装时 Larkin 不再 -e 注入,避免同名工具(Agent/get_subagent_result/steer_subagent)
186
187
  * 重复注册导致 pi 扩展加载 FATAL(pi 对 duplicate tool registration 是硬失败)。
188
+ *
189
+ * Owner 边界:只读非密钥兼容信息(settings.json / package 清单 / 扩展源码标记)。
190
+ * 不得打开或写入用户 Pi home 里的 auth.json,也不得往 ~/.pi 写任何文件。
187
191
  */
188
192
  const BOUNDED_WAIT_CAPABILITY = "larkin-pi-subagents-bounded-wait-v1";
189
193
  const SUPERVISED_COMMAND_CAPABILITY = "larkin-pi-supervised-command-v1";
194
+ function assertRegularFile(file) {
195
+ const stat = fs.lstatSync(file);
196
+ if (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== 1) {
197
+ throw new Error("user Pi path is not a regular file");
198
+ }
199
+ }
200
+ function readRegularFile(file) {
201
+ assertRegularFile(file);
202
+ return fs.readFileSync(file, "utf8");
203
+ }
204
+ function isRealpathInside(root, candidate) {
205
+ const rootReal = fs.realpathSync(root);
206
+ const candidateReal = fs.realpathSync(candidate);
207
+ const prefix = rootReal.endsWith(path.sep) ? rootReal : `${rootReal}${path.sep}`;
208
+ return candidateReal.startsWith(prefix);
209
+ }
210
+ /** 只读包根内的单链接常规文件;绝对路径、逃逸、符号链接、硬链接一律视为不可验证。 */
211
+ function readContainedPackageEntry(packageRoot, entry) {
212
+ if (path.isAbsolute(entry))
213
+ throw new Error("absolute Pi extension entry");
214
+ const root = path.resolve(packageRoot);
215
+ const resolved = path.resolve(root, entry);
216
+ const lexicalPrefix = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
217
+ if (resolved !== root && !resolved.startsWith(lexicalPrefix))
218
+ throw new Error("Pi extension entry escaped package root");
219
+ assertRegularFile(resolved);
220
+ if (!isRealpathInside(root, resolved))
221
+ throw new Error("Pi extension entry escaped package root");
222
+ return fs.readFileSync(resolved, "utf8");
223
+ }
190
224
  function userPiSubagentsWaitCapability(env) {
191
225
  const agentDir = env.PI_CODING_AGENT_DIR || path.join(env.HOME || process.env.HOME || "", ".pi", "agent");
192
226
  try {
193
227
  let configured = false;
194
228
  const settingsFile = path.join(agentDir, "settings.json");
195
229
  if (fs.existsSync(settingsFile)) {
196
- const settings = JSON.parse(fs.readFileSync(settingsFile, "utf8"));
230
+ const settings = JSON.parse(readRegularFile(settingsFile));
197
231
  const packages = settings?.packages;
198
232
  if (Array.isArray(packages)) {
199
233
  configured = packages.some((entry) => typeof entry === "string" && /pi-subagents/i.test(entry));
@@ -210,7 +244,7 @@ function userPiSubagentsWaitCapability(env) {
210
244
  return "absent";
211
245
  if (!installedRoot)
212
246
  return "unbounded";
213
- const packageManifest = JSON.parse(fs.readFileSync(path.join(installedRoot, "package.json"), "utf8"));
247
+ const packageManifest = JSON.parse(readRegularFile(path.join(installedRoot, "package.json")));
214
248
  const extensions = packageManifest.pi?.extensions;
215
249
  if (!Array.isArray(extensions) || extensions.length === 0
216
250
  || !extensions.every((entry) => typeof entry === "string" && entry.length > 0)) {
@@ -218,20 +252,14 @@ function userPiSubagentsWaitCapability(env) {
218
252
  }
219
253
  // Pi executes the entries declared by the package manifest. Do not trust a
220
254
  // marker in a build artifact when the manifest points Pi at another file.
221
- const capabilityFiles = extensions.map((entry) => path.resolve(installedRoot, entry));
222
- const hasCapability = capabilityFiles.every((file) => {
223
- try {
224
- const source = fs.readFileSync(file, "utf8");
225
- return source.includes(BOUNDED_WAIT_CAPABILITY) && source.includes(SUPERVISED_COMMAND_CAPABILITY);
226
- }
227
- catch {
228
- return false;
229
- }
255
+ const hasCapability = extensions.every((entry) => {
256
+ const source = readContainedPackageEntry(installedRoot, entry);
257
+ return source.includes(BOUNDED_WAIT_CAPABILITY) && source.includes(SUPERVISED_COMMAND_CAPABILITY);
230
258
  });
231
259
  return hasCapability ? "bounded" : "unbounded";
232
260
  }
233
261
  catch {
234
- // Unreadable user extension state is not safe to treat as bounded.
262
+ // Unreadable or escaped user extension state is not safe to treat as bounded.
235
263
  return "unbounded";
236
264
  }
237
265
  }
@@ -1,11 +1,25 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.INTERNAL_CONTEXT_WINDOW_PROJECTION_REASON = exports.CANONICAL_CONTEXT_WINDOW_MESSAGE = exports.CODEX_ERROR_PREFIX = void 0;
4
+ exports.safeProviderDiagnostic = safeProviderDiagnostic;
4
5
  exports.buildStrictProviderErrorInput = buildStrictProviderErrorInput;
5
6
  exports.classifyStrictProviderError = classifyStrictProviderError;
6
7
  exports.CODEX_ERROR_PREFIX = "Codex error: ";
7
8
  exports.CANONICAL_CONTEXT_WINDOW_MESSAGE = "Your input exceeds the context window of this model. Please adjust your input and try again.";
8
9
  exports.INTERNAL_CONTEXT_WINDOW_PROJECTION_REASON = "provider rejected the input because the context window was exceeded";
10
+ /** Redact credentials and local filesystem locations from provider diagnostics kept in status. */
11
+ function safeProviderDiagnostic(value, fallback, max = 2_000) {
12
+ const source = typeof value === "string" ? value : fallback;
13
+ return source
14
+ .replace(/\b(?:authorization|proxy-authorization)\s*[:=]\s*(?:Bearer\s+)?[^\s,;]+/gi, (match) => `${match.split(/[:=]/, 1)[0]}=[redacted]`)
15
+ .replace(/\b(?:cookie|set-cookie)\s*[:=]\s*(?:[^;\s,]+(?:\s*;\s*[^;\s,]+)*)/gi, (match) => `${match.split(/[:=]/, 1)[0]}=[redacted]`)
16
+ .replace(/\b(?:api[_ -]?key|access[_ -]?token|refresh[_ -]?token|token|secret|password)\s*[:=]\s*[^\s,;]+/gi, (match) => `${match.split(/[:=]/, 1)[0]}=[redacted]`)
17
+ .replace(/(["'](?:authorization|cookie|api[_-]?key|access[_-]?token|refresh[_-]?token|token|secret|password)["']\s*:\s*)["'][^"']*["']/gi, "$1\"[redacted]\"")
18
+ .replace(/Bearer\s+[A-Za-z0-9._~+\/-]+/gi, "Bearer [redacted]")
19
+ .replace(/\b(?:sk|ghp|github_pat)-?[A-Za-z0-9_-]{8,}\b/g, "[redacted]")
20
+ .replace(/(^|[\s"'=])(?:\/(?:Users|home|private|var|tmp|etc)\/[^\s"']+|[A-Za-z]:[\\/][^\s"']+)/g, "$1[redacted-path]")
21
+ .replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim().slice(0, max) || fallback;
22
+ }
9
23
  /** Build the one structured provider-error shape shared by ingestion and recovery. */
10
24
  function buildStrictProviderErrorInput(source) {
11
25
  const upstream = source.upstream && typeof source.upstream === "object" && !Array.isArray(source.upstream)
@@ -1,6 +1,19 @@
1
1
  export const CODEX_ERROR_PREFIX = "Codex error: ";
2
2
  export const CANONICAL_CONTEXT_WINDOW_MESSAGE = "Your input exceeds the context window of this model. Please adjust your input and try again.";
3
3
  export const INTERNAL_CONTEXT_WINDOW_PROJECTION_REASON = "provider rejected the input because the context window was exceeded";
4
+ /** Redact credentials and local filesystem locations from provider diagnostics kept in status. */
5
+ export function safeProviderDiagnostic(value, fallback, max = 2_000) {
6
+ const source = typeof value === "string" ? value : fallback;
7
+ return source
8
+ .replace(/\b(?:authorization|proxy-authorization)\s*[:=]\s*(?:Bearer\s+)?[^\s,;]+/gi, (match) => `${match.split(/[:=]/, 1)[0]}=[redacted]`)
9
+ .replace(/\b(?:cookie|set-cookie)\s*[:=]\s*(?:[^;\s,]+(?:\s*;\s*[^;\s,]+)*)/gi, (match) => `${match.split(/[:=]/, 1)[0]}=[redacted]`)
10
+ .replace(/\b(?:api[_ -]?key|access[_ -]?token|refresh[_ -]?token|token|secret|password)\s*[:=]\s*[^\s,;]+/gi, (match) => `${match.split(/[:=]/, 1)[0]}=[redacted]`)
11
+ .replace(/(["'](?:authorization|cookie|api[_-]?key|access[_-]?token|refresh[_-]?token|token|secret|password)["']\s*:\s*)["'][^"']*["']/gi, "$1\"[redacted]\"")
12
+ .replace(/Bearer\s+[A-Za-z0-9._~+\/-]+/gi, "Bearer [redacted]")
13
+ .replace(/\b(?:sk|ghp|github_pat)-?[A-Za-z0-9_-]{8,}\b/g, "[redacted]")
14
+ .replace(/(^|[\s"'=])(?:\/(?:Users|home|private|var|tmp|etc)\/[^\s"']+|[A-Za-z]:[\\/][^\s"']+)/g, "$1[redacted-path]")
15
+ .replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim().slice(0, max) || fallback;
16
+ }
4
17
  /** Build the one structured provider-error shape shared by ingestion and recovery. */
5
18
  export function buildStrictProviderErrorInput(source) {
6
19
  const upstream = source.upstream && typeof source.upstream === "object" && !Array.isArray(source.upstream)
@@ -5,7 +5,6 @@ import * as path from "node:path";
5
5
  import { createInterface } from "node:readline";
6
6
  import { isPiThinkingLevel } from "./pi-model-catalog.mjs";
7
7
  import { PiRpcClient } from "./pi-rpc-client.mjs";
8
- import { recordPiRuntimeArtifactProvenance } from "./pi-artifact-provenance.mjs";
9
8
  import { traceProcessBoundary } from "../platform/process-boundary-trace.mjs";
10
9
  import { resolvePiSubagentExtensionArg } from "./pi-subagent-injection.mjs";
11
10
  import { extractCanonicalPiSubagentNotification, ledgerStatusFromPiNotificationStatus, } from "./pi-subagents-notification.mjs";
@@ -13,10 +12,38 @@ import { resolvePiSubagentRecordWatchdogExtensionArg } from "./pi-subagent-recor
13
12
  import { effectivePiStateDir, extractBackgroundPiSubagentDispatch } from "./pi-subagent-ledger.mjs";
14
13
  import { resolvePiBashTimeoutExtensionArg } from "./pi-bash-timeout-injection.mjs";
15
14
  import { classifyPiMissingCredentialRejection, classifyRuntimePrerequisite, missingProviderCredentialReadiness, probeNativeRuntimeReadiness, providerAuthenticationFailureReadiness, RuntimePrerequisiteError, } from "./runtime-readiness.mjs";
16
- import { buildStrictProviderErrorInput, classifyStrictProviderError, INTERNAL_CONTEXT_WINDOW_PROJECTION_REASON, } from "./provider-error-classifier.mjs";
17
- import { assertEffectivePiCompactionSettings, assertNoProjectPiCompactionOverride, MINIMUM_PI_VERSION, calculatePiCompactionSettings, parsePiExecutableVersion, prepareOwnedPiDirectory, readOwnedPiSettings, verifyPiCapabilities, writeOwnedPiSettings, } from "./pi-compaction-recovery.mjs";
18
- function piAgentDirectory(configDir, agentId) {
19
- return path.join(path.resolve(configDir), "providers", "pi", agentId);
15
+ import { buildStrictProviderErrorInput, classifyStrictProviderError, INTERNAL_CONTEXT_WINDOW_PROJECTION_REASON, safeProviderDiagnostic, } from "./provider-error-classifier.mjs";
16
+ import { assertEffectivePiCompactionSettings, MINIMUM_PI_VERSION, calculatePiCompactionSettings, parsePiExecutableVersion, projectPiSettingsFile, readOwnedPiSettings, verifyPiCapabilities, writeOwnedPiSettings, } from "./pi-compaction-recovery.mjs";
17
+ /** External pi must see the user's own agent dir; Larkin never injects or honors PI_CODING_AGENT_DIR. */
18
+ function stripPiCodingAgentDir(env) {
19
+ const next = { ...env };
20
+ delete next.PI_CODING_AGENT_DIR;
21
+ return next;
22
+ }
23
+ const PI_INITIALIZATION_CONCURRENCY = 2;
24
+ /** Limits short-lived Pi startup handshakes without serializing live sessions. */
25
+ function createInitializationLimiter(limit) {
26
+ let active = 0;
27
+ const waiters = [];
28
+ const release = () => {
29
+ const next = waiters.shift();
30
+ if (next)
31
+ next();
32
+ else
33
+ active -= 1;
34
+ };
35
+ return async (operation) => {
36
+ if (active >= limit)
37
+ await new Promise((resolve) => waiters.push(resolve));
38
+ else
39
+ active += 1;
40
+ try {
41
+ return await operation();
42
+ }
43
+ finally {
44
+ release();
45
+ }
46
+ };
20
47
  }
21
48
  class EventSession {
22
49
  listeners = new Set();
@@ -778,14 +805,7 @@ class PiSession extends EventSession {
778
805
  }
779
806
  }
780
807
  function safeProviderText(value, fallback) {
781
- const source = typeof value === "string" ? value : fallback;
782
- return source
783
- .replace(/\b(?:authorization|proxy-authorization)\s*[:=]\s*(?:Bearer\s+)?[^\s,;]+/gi, (match) => `${match.split(/[:=]/, 1)[0]}=[redacted]`)
784
- .replace(/\b(?:cookie|set-cookie|api[_ -]?key|access[_ -]?token|refresh[_ -]?token|secret)\s*[:=]\s*[^\s,;]+/gi, (match) => `${match.split(/[:=]/, 1)[0]}=[redacted]`)
785
- .replace(/(["'](?:authorization|cookie|api[_-]?key|access[_-]?token|refresh[_-]?token|token|secret)["']\s*:\s*)["'][^"']*["']/gi, "$1\"[redacted]\"")
786
- .replace(/Bearer\s+[A-Za-z0-9._~+\/-]+/gi, "Bearer [redacted]")
787
- .replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, "[redacted]")
788
- .replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim().slice(0, 2_000) || fallback;
808
+ return safeProviderDiagnostic(value, fallback);
789
809
  }
790
810
  function piAssistantProviderError(message) {
791
811
  const diagnostic = [...(Array.isArray(message.diagnostics) ? message.diagnostics : [])]
@@ -977,19 +997,12 @@ class PiRpcBackend {
977
997
  this.sessionFile = state.sessionFile;
978
998
  if (typeof state.sessionId === "string" && state.sessionId)
979
999
  this.sessionId = state.sessionId;
980
- // Use the live process handshake, not a disk re-read of owned settings.json.
981
- // Disk reserveTokens can drift after startup; that must not reject prompt.
982
- const handshake = state.compactionCapabilities;
983
- if (typeof handshake?.reserveTokens === "number" && typeof handshake?.keepRecentTokens === "number") {
984
- assertEffectivePiCompactionSettings({ contextWindow: state.model?.contextWindow, compaction: {
985
- enabled: state.autoCompactionEnabled,
986
- reserveTokens: handshake.reserveTokens,
987
- keepRecentTokens: handshake.keepRecentTokens,
988
- } });
989
- return;
990
- }
991
- if (state.autoCompactionEnabled !== true)
992
- throw new Error("Pi native compaction must be enabled");
1000
+ // Prompt 只信 live get_state:有 handshake 就必须精确匹配,没有则只要求 compaction 仍开启。
1001
+ verifyPiCapabilities({
1002
+ distribution: "external", version: this.policy.version,
1003
+ contextWindow: state.model?.contextWindow, autoCompactionEnabled: state.autoCompactionEnabled,
1004
+ compactRpc: true, compactionCapabilities: state.compactionCapabilities,
1005
+ });
993
1006
  }
994
1007
  getState() { return this.client.request("get_state"); }
995
1008
  dispose() { return this.client.close(); }
@@ -1017,13 +1030,11 @@ export function resolvePiProcessExtensionArgs(input, resolvers = {}) {
1017
1030
  }
1018
1031
  async function createPiRpcBackend(input, dependencies, spawn, productionSpawn = true) {
1019
1032
  const stateRoot = effectivePiStateDir(input);
1020
- const mergedEnv = { ...globalThis.process.env, ...dependencies.env, ...input.env, NO_COLOR: "1" };
1021
- assertNoProjectPiCompactionOverride(input.workspaceDir);
1022
- const ownedPiDirectory = mergedEnv.LARKIN_CONFIG_DIR
1023
- ? piAgentDirectory(mergedEnv.LARKIN_CONFIG_DIR, input.agentId)
1024
- : path.join(stateRoot, "pi-agent");
1025
- prepareOwnedPiDirectory(ownedPiDirectory);
1026
- traceProcessBoundary(mergedEnv, "pi-rpc:child-env", { configDir: mergedEnv.LARKIN_CONFIG_DIR, agentId: input.agentId, targetDir: ownedPiDirectory, childEnvConfigDir: mergedEnv.LARKIN_CONFIG_DIR || null, childEnvHome: mergedEnv.HOME || null });
1033
+ const mergedEnv = stripPiCodingAgentDir({ ...globalThis.process.env, ...dependencies.env, ...input.env, NO_COLOR: "1" });
1034
+ if (mergedEnv.LARKIN_PI_DISTRIBUTION === "builtin")
1035
+ delete mergedEnv.LARKIN_PI_DISTRIBUTION;
1036
+ const projectSettings = projectPiSettingsFile(input.workspaceDir);
1037
+ traceProcessBoundary(mergedEnv, "pi-rpc:child-env", { configDir: mergedEnv.LARKIN_CONFIG_DIR, agentId: input.agentId, targetDir: projectSettings, childEnvConfigDir: mergedEnv.LARKIN_CONFIG_DIR || null, childEnvHome: mergedEnv.HOME || null });
1027
1038
  const runtimeDir = path.join(stateRoot, "runtime");
1028
1039
  fs.mkdirSync(runtimeDir, { recursive: true, mode: 0o700 });
1029
1040
  const promptFile = path.join(runtimeDir, "pi-standing-prompt.md");
@@ -1033,17 +1044,15 @@ async function createPiRpcBackend(input, dependencies, spawn, productionSpawn =
1033
1044
  const requestedEffort = input.reasoningEffort?.trim() || undefined;
1034
1045
  if (requestedEffort && !isPiThinkingLevel(requestedEffort))
1035
1046
  throw new Error(`Unknown Pi thinking level: ${requestedEffort}`);
1036
- const args = ["--mode", "rpc", "--session-dir", session.sessionDir, "--append-system-prompt", promptFile,
1047
+ // `--approve` 让 RPC 在无 UI 时仍加载工作区 `.pi/settings.json`(否则 settings.json 会触发 trust gate 并被忽略)。
1048
+ const args = ["--mode", "rpc", "--approve", "--session-dir", session.sessionDir, "--append-system-prompt", promptFile,
1037
1049
  ...(session.sessionFile ? ["--session", session.sessionFile] : []),
1038
1050
  ...(requestedModel ? ["--model", requestedModel] : []),
1039
1051
  ...(requestedEffort ? ["--thinking", requestedEffort] : [])];
1040
- if (mergedEnv.LARKIN_PI_DISTRIBUTION === "builtin")
1041
- delete mergedEnv.LARKIN_PI_DISTRIBUTION;
1042
1052
  const command = dependencies.piCommand ?? dependencies.env?.LARKIN_PI_COMMAND ?? process.env.LARKIN_PI_COMMAND ?? "pi";
1043
1053
  const commandPrefix = dependencies.piCommandArgs ?? [];
1044
1054
  const commandArgs = [...commandPrefix, ...args];
1045
- mergedEnv.PI_CODING_AGENT_DIR = ownedPiDirectory;
1046
- const childEnv = { ...mergedEnv };
1055
+ const childEnv = stripPiCodingAgentDir(mergedEnv);
1047
1056
  const reportedVersion = productionSpawn
1048
1057
  ? parsePiExecutableVersion(String(spawnSync(command, [...commandPrefix, "--version"], {
1049
1058
  cwd: input.workspaceDir, env: childEnv, encoding: "utf8", timeout: 5_000,
@@ -1057,21 +1066,24 @@ async function createPiRpcBackend(input, dependencies, spawn, productionSpawn =
1057
1066
  commandArgs.push(...extensionArgs);
1058
1067
  let probedModel = null;
1059
1068
  if (productionSpawn) {
1060
- // Pi reads compaction settings when the process starts. Probe the exact
1061
- // requested model first, then write the matching 15% reserve before the
1062
- // real session starts; no prompt, provider request, package, or extension
1063
- // is loaded by the probe.
1064
- probedModel = await discoverEffectivePiContextWindow(input, command, commandPrefix, requestedModel, childEnv, spawn, dependencies.piRpcClientOptions);
1065
- writeOwnedPiSettings(ownedPiDirectory, calculatePiCompactionSettings(probedModel.contextWindow));
1069
+ try {
1070
+ // Isolated probes must share the same prerequisite contract as the
1071
+ // session handshake, so a cold-start timeout can use bounded recovery.
1072
+ probedModel = await discoverEffectivePiContextWindow(input, command, commandPrefix, requestedModel, childEnv, spawn, dependencies.piRpcClientOptions);
1073
+ }
1074
+ catch (error) {
1075
+ if (error instanceof RuntimePrerequisiteError)
1076
+ throw error;
1077
+ throw new RuntimePrerequisiteError(classifyRuntimePrerequisite("pi", error, command));
1078
+ }
1079
+ writeOwnedPiSettings(input.workspaceDir, calculatePiCompactionSettings(probedModel.contextWindow));
1066
1080
  }
1067
- const artifactEntriesBeforeSpawn = new Set(fs.readdirSync(ownedPiDirectory));
1068
- const artifactSpawnBoundary = Date.now();
1069
1081
  const child = spawn(command, commandArgs, {
1070
1082
  cwd: input.workspaceDir,
1071
1083
  env: childEnv,
1072
1084
  stdio: ["pipe", "pipe", "pipe"],
1073
1085
  });
1074
- traceProcessBoundary(mergedEnv, "pi-rpc:child-spawned", { configDir: mergedEnv.LARKIN_CONFIG_DIR, agentId: input.agentId, targetDir: ownedPiDirectory, childPid: child.pid ?? null });
1086
+ traceProcessBoundary(mergedEnv, "pi-rpc:child-spawned", { configDir: mergedEnv.LARKIN_CONFIG_DIR, agentId: input.agentId, targetDir: projectSettings, childPid: child.pid ?? null });
1075
1087
  const client = new PiRpcClient(child, dependencies.piRpcClientOptions);
1076
1088
  try {
1077
1089
  const [state, available] = await Promise.all([
@@ -1086,17 +1098,18 @@ async function createPiRpcBackend(input, dependencies, spawn, productionSpawn =
1086
1098
  || state.model?.contextWindow !== probedModel.contextWindow) {
1087
1099
  throw new Error("Pi model or context window changed between the isolated probe and runtime startup");
1088
1100
  }
1089
- const ownedSettings = readOwnedPiSettings(ownedPiDirectory);
1101
+ const ownedSettings = readOwnedPiSettings(input.workspaceDir);
1090
1102
  assertEffectivePiCompactionSettings({ contextWindow: state.model?.contextWindow, compaction: {
1091
1103
  enabled: state.autoCompactionEnabled,
1092
1104
  reserveTokens: ownedSettings.compaction?.reserveTokens,
1093
1105
  keepRecentTokens: ownedSettings.compaction?.keepRecentTokens,
1094
1106
  } });
1095
- const handshake = state.compactionCapabilities;
1096
- verifyPiCapabilities({ distribution: "external", version: reportedVersion,
1107
+ const policySource = verifyPiCapabilities({
1108
+ distribution: "external", version: reportedVersion,
1097
1109
  contextWindow: state.model?.contextWindow, autoCompactionEnabled: state.autoCompactionEnabled,
1098
- reserveTokens: handshake?.reserveTokens, keepRecentTokens: handshake?.keepRecentTokens,
1099
- compactRpc: true, events: handshake?.events });
1110
+ compactRpc: true, compactionCapabilities: state.compactionCapabilities,
1111
+ });
1112
+ process.stderr.write(`[runtime] Pi compaction policy source=${policySource} agent=${input.agentId} model=${effectiveModel} contextWindow=${state.model?.contextWindow}\n`);
1100
1113
  }
1101
1114
  // 严格对账(Owner 决策):模型必须来自 pi 的权威可用列表,不做后缀猜测——
1102
1115
  // 配置的模型与 pi 可用列表不符即报错,并在错误里列出可用模型。
@@ -1110,9 +1123,8 @@ async function createPiRpcBackend(input, dependencies, spawn, productionSpawn =
1110
1123
  }
1111
1124
  if (requestedEffort && state.thinkingLevel !== requestedEffort)
1112
1125
  throw new Error(`Pi thinking level ${requestedEffort} was not accepted by effective model ${effectiveModel || "unknown"}`);
1113
- recordPiRuntimeArtifactProvenance(ownedPiDirectory, artifactEntriesBeforeSpawn, artifactSpawnBoundary);
1114
1126
  return new PiRpcBackend(client, state, productionSpawn ? {
1115
- model: effectiveModel, contextWindow: state.model?.contextWindow, ownedPiDirectory,
1127
+ model: effectiveModel, contextWindow: state.model?.contextWindow, version: reportedVersion,
1116
1128
  } : undefined);
1117
1129
  }
1118
1130
  catch (error) {
@@ -1133,6 +1145,7 @@ export function createNativeRuntimeAdapter(id, dependencies = {}) {
1133
1145
  ? dependencies.codexCommand ?? dependencies.env?.LARKIN_CODEX_COMMAND ?? "codex"
1134
1146
  : dependencies.claudeCommand ?? dependencies.env?.LARKIN_CLAUDE_COMMAND ?? "claude";
1135
1147
  const codexCommand = dependencies.codexCommand ?? dependencies.env?.LARKIN_CODEX_COMMAND ?? "codex";
1148
+ const runPiInitialization = createInitializationLimiter(PI_INITIALIZATION_CONCURRENCY);
1136
1149
  let codexUpdateAttempt = null;
1137
1150
  let codexUpdateAttempted = false;
1138
1151
  const runCodexUpdate = () => new Promise((resolve) => {
@@ -1231,9 +1244,9 @@ export function createNativeRuntimeAdapter(id, dependencies = {}) {
1231
1244
  throw new RuntimePrerequisiteError(readiness);
1232
1245
  }
1233
1246
  if (id === "pi") {
1234
- return new PiSession(await (dependencies.createPiSession
1247
+ return runPiInitialization(async () => new PiSession(await (dependencies.createPiSession
1235
1248
  ? dependencies.createPiSession(input)
1236
- : createPiRpcBackend(input, { ...dependencies, piCommand: resolvedExecutable }, spawn, productionSpawn)));
1249
+ : createPiRpcBackend(input, { ...dependencies, piCommand: resolvedExecutable }, spawn, productionSpawn))));
1237
1250
  }
1238
1251
  if (id === "codex") {
1239
1252
  const codexInput = dependencies.codexModelOverride?.trim()
@@ -1640,38 +1640,45 @@ function createRuntimeHost(options) {
1640
1640
  const ensureSession = async (agent) => {
1641
1641
  if (agent.disabledReason)
1642
1642
  throw new Error(agent.disabledReason);
1643
+ if (agent.stopped)
1644
+ throw new Error("runtime Agent is stopped");
1643
1645
  if (agent.session)
1644
1646
  return agent.session;
1645
1647
  if (agent.starting)
1646
1648
  return agent.starting;
1647
- const probeEnv = runtimeEnv(agent.config);
1648
- await assertOfficialCliReady(agent.config, probeEnv);
1649
- const readiness = agent.adapter.probe ? await agent.adapter.probe({ agentId: agent.config.agentId,
1650
- workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
1651
- env: { LARKIN_PI_COMMAND: process.env.LARKIN_PI_COMMAND, LARKIN_CODEX_COMMAND: process.env.LARKIN_CODEX_COMMAND,
1652
- LARKIN_CLAUDE_COMMAND: process.env.LARKIN_CLAUDE_COMMAND, ...probeEnv } })
1653
- : { runtime: agent.adapter.id, state: "ready" };
1654
- if (!agent.authFailureActive)
1655
- agent.readiness = readiness;
1656
- if (readiness.state !== "ready")
1657
- throw new runtime_readiness_js_1.RuntimePrerequisiteError(readiness);
1658
- const standingPrompt = options.promptBuilder.build({
1659
- agentId: agent.config.agentId, name: agent.config.displayName || agent.config.name,
1660
- description: agent.config.description || "", runtime: agent.adapter.id,
1661
- cli: (0, agent_cli_capabilities_js_1.agentCliPromptCapabilities)("larkin"), previousSession: agent.previousSession,
1662
- });
1663
- const generation = ++agent.generation;
1664
1649
  let completedSession = null;
1665
- const sessionEnv = runtimeEnv(agent.config, `${agent.launchId}:${generation}`);
1666
- agent.starting = agent.adapter.createSession({
1667
- agentId: agent.config.agentId, model: agent.config.model, reasoningEffort: agent.config.effort || null,
1668
- workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
1669
- resumeSessionId: agent.config.sessionId || null, standingPrompt,
1670
- env: sessionEnv,
1671
- }).then((session) => {
1650
+ agent.starting = (async () => {
1651
+ const probeEnv = runtimeEnv(agent.config);
1652
+ await assertOfficialCliReady(agent.config, probeEnv);
1653
+ if (agent.stopped)
1654
+ throw new Error("runtime Agent is stopped");
1655
+ const readiness = agent.adapter.probe ? await agent.adapter.probe({ agentId: agent.config.agentId,
1656
+ workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
1657
+ env: { LARKIN_PI_COMMAND: process.env.LARKIN_PI_COMMAND, LARKIN_CODEX_COMMAND: process.env.LARKIN_CODEX_COMMAND,
1658
+ LARKIN_CLAUDE_COMMAND: process.env.LARKIN_CLAUDE_COMMAND, ...probeEnv } })
1659
+ : { runtime: agent.adapter.id, state: "ready" };
1660
+ if (!agent.authFailureActive)
1661
+ agent.readiness = readiness;
1662
+ if (readiness.state !== "ready")
1663
+ throw new runtime_readiness_js_1.RuntimePrerequisiteError(readiness);
1664
+ if (agent.stopped)
1665
+ throw new Error("runtime Agent is stopped");
1666
+ const standingPrompt = options.promptBuilder.build({
1667
+ agentId: agent.config.agentId, name: agent.config.displayName || agent.config.name,
1668
+ description: agent.config.description || "", runtime: agent.adapter.id,
1669
+ cli: (0, agent_cli_capabilities_js_1.agentCliPromptCapabilities)("larkin"), previousSession: agent.previousSession,
1670
+ });
1671
+ const generation = ++agent.generation;
1672
+ const sessionEnv = runtimeEnv(agent.config, `${agent.launchId}:${generation}`);
1673
+ const session = await agent.adapter.createSession({
1674
+ agentId: agent.config.agentId, model: agent.config.model, reasoningEffort: agent.config.effort || null,
1675
+ workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
1676
+ resumeSessionId: agent.config.sessionId || null, standingPrompt,
1677
+ env: sessionEnv,
1678
+ });
1672
1679
  completedSession = session;
1673
1680
  if (agent.stopped || generation !== agent.generation) {
1674
- void session.close("stale creation");
1681
+ await session.close("stale creation");
1675
1682
  throw new Error("stale runtime session creation");
1676
1683
  }
1677
1684
  agent.session = session;
@@ -1691,7 +1698,7 @@ function createRuntimeHost(options) {
1691
1698
  agent.stabilityTimer = setTimeout(() => markSessionStable(agent, session), retryPolicy.stableWindowMs);
1692
1699
  agent.stabilityTimer.unref?.();
1693
1700
  return session;
1694
- }).finally(() => {
1701
+ })().finally(() => {
1695
1702
  agent.starting = null;
1696
1703
  if (completedSession && !agent.session && !agent.stopped) {
1697
1704
  scheduleRecreate(agent, agent.recreateReason || "runtime closed during session initialization");
@@ -2329,6 +2336,8 @@ function createRuntimeHost(options) {
2329
2336
  emitConsumed(agent, [...startupConsumed, ...persist(agent)]);
2330
2337
  try {
2331
2338
  await ensureSession(agent);
2339
+ if (agent.stopped)
2340
+ return;
2332
2341
  reconcileSubagentLedger(agent, { forceMissing: true, missingReason: "runtime restarted" });
2333
2342
  enqueueUndeliveredTerminalWakes(agent);
2334
2343
  await recoverStalePiCompaction(agent);
@@ -2336,10 +2345,14 @@ function createRuntimeHost(options) {
2336
2345
  const proactive = startupSession ? proactivelyCompactPiAtIdle(agent, startupSession) : null;
2337
2346
  if (proactive)
2338
2347
  await proactive;
2348
+ if (agent.stopped)
2349
+ return;
2339
2350
  activeCount += 1;
2340
2351
  await retryPending(agent);
2341
2352
  }
2342
2353
  catch (error) {
2354
+ if (agent.stopped)
2355
+ return;
2343
2356
  const reason = error instanceof Error ? error.message : String(error);
2344
2357
  const transient = error instanceof runtime_readiness_js_1.RuntimePrerequisiteError && error.readiness.state === "unavailable";
2345
2358
  agent.disabledReason = transient ? null : reason;
@@ -2353,6 +2366,8 @@ function createRuntimeHost(options) {
2353
2366
  emit({ type: "agent-status", agentId: config.agentId, status: "error", error: reason,
2354
2367
  ...(error instanceof runtime_readiness_js_1.RuntimePrerequisiteError ? { readiness: error.readiness } : {}) });
2355
2368
  }
2369
+ if (agent.stopped)
2370
+ return;
2356
2371
  const visibleQuarantines = startupQuarantined.filter(({ record }) => agent.records.get(record.deliveryId)?.status === "error");
2357
2372
  for (const { record, code } of visibleQuarantines)
2358
2373
  emit({ type: "delivery", agentId: config.agentId,
@@ -2447,6 +2462,7 @@ function createRuntimeHost(options) {
2447
2462
  if (!agent)
2448
2463
  return;
2449
2464
  agent.stopped = true;
2465
+ const starting = agent.starting;
2450
2466
  if (agent.poller)
2451
2467
  clearInterval(agent.poller);
2452
2468
  if (agent.retryTimer)
@@ -2460,6 +2476,10 @@ function createRuntimeHost(options) {
2460
2476
  if (agent.busy)
2461
2477
  await agent.session?.cancel(reason);
2462
2478
  await agent.session?.close(reason);
2479
+ // A queued Pi initializer cannot be cancelled through the public adapter
2480
+ // contract. Drain it before removing the Agent; ensureSession sees stopped
2481
+ // and closes any session created after this point.
2482
+ await starting?.catch(() => { });
2463
2483
  managed.delete(agentId);
2464
2484
  emit({ type: "agent-status", agentId, status: "inactive" });
2465
2485
  },
@@ -1631,38 +1631,45 @@ export function createRuntimeHost(options) {
1631
1631
  const ensureSession = async (agent) => {
1632
1632
  if (agent.disabledReason)
1633
1633
  throw new Error(agent.disabledReason);
1634
+ if (agent.stopped)
1635
+ throw new Error("runtime Agent is stopped");
1634
1636
  if (agent.session)
1635
1637
  return agent.session;
1636
1638
  if (agent.starting)
1637
1639
  return agent.starting;
1638
- const probeEnv = runtimeEnv(agent.config);
1639
- await assertOfficialCliReady(agent.config, probeEnv);
1640
- const readiness = agent.adapter.probe ? await agent.adapter.probe({ agentId: agent.config.agentId,
1641
- workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
1642
- env: { LARKIN_PI_COMMAND: process.env.LARKIN_PI_COMMAND, LARKIN_CODEX_COMMAND: process.env.LARKIN_CODEX_COMMAND,
1643
- LARKIN_CLAUDE_COMMAND: process.env.LARKIN_CLAUDE_COMMAND, ...probeEnv } })
1644
- : { runtime: agent.adapter.id, state: "ready" };
1645
- if (!agent.authFailureActive)
1646
- agent.readiness = readiness;
1647
- if (readiness.state !== "ready")
1648
- throw new RuntimePrerequisiteError(readiness);
1649
- const standingPrompt = options.promptBuilder.build({
1650
- agentId: agent.config.agentId, name: agent.config.displayName || agent.config.name,
1651
- description: agent.config.description || "", runtime: agent.adapter.id,
1652
- cli: agentCliPromptCapabilities("larkin"), previousSession: agent.previousSession,
1653
- });
1654
- const generation = ++agent.generation;
1655
1640
  let completedSession = null;
1656
- const sessionEnv = runtimeEnv(agent.config, `${agent.launchId}:${generation}`);
1657
- agent.starting = agent.adapter.createSession({
1658
- agentId: agent.config.agentId, model: agent.config.model, reasoningEffort: agent.config.effort || null,
1659
- workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
1660
- resumeSessionId: agent.config.sessionId || null, standingPrompt,
1661
- env: sessionEnv,
1662
- }).then((session) => {
1641
+ agent.starting = (async () => {
1642
+ const probeEnv = runtimeEnv(agent.config);
1643
+ await assertOfficialCliReady(agent.config, probeEnv);
1644
+ if (agent.stopped)
1645
+ throw new Error("runtime Agent is stopped");
1646
+ const readiness = agent.adapter.probe ? await agent.adapter.probe({ agentId: agent.config.agentId,
1647
+ workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
1648
+ env: { LARKIN_PI_COMMAND: process.env.LARKIN_PI_COMMAND, LARKIN_CODEX_COMMAND: process.env.LARKIN_CODEX_COMMAND,
1649
+ LARKIN_CLAUDE_COMMAND: process.env.LARKIN_CLAUDE_COMMAND, ...probeEnv } })
1650
+ : { runtime: agent.adapter.id, state: "ready" };
1651
+ if (!agent.authFailureActive)
1652
+ agent.readiness = readiness;
1653
+ if (readiness.state !== "ready")
1654
+ throw new RuntimePrerequisiteError(readiness);
1655
+ if (agent.stopped)
1656
+ throw new Error("runtime Agent is stopped");
1657
+ const standingPrompt = options.promptBuilder.build({
1658
+ agentId: agent.config.agentId, name: agent.config.displayName || agent.config.name,
1659
+ description: agent.config.description || "", runtime: agent.adapter.id,
1660
+ cli: agentCliPromptCapabilities("larkin"), previousSession: agent.previousSession,
1661
+ });
1662
+ const generation = ++agent.generation;
1663
+ const sessionEnv = runtimeEnv(agent.config, `${agent.launchId}:${generation}`);
1664
+ const session = await agent.adapter.createSession({
1665
+ agentId: agent.config.agentId, model: agent.config.model, reasoningEffort: agent.config.effort || null,
1666
+ workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
1667
+ resumeSessionId: agent.config.sessionId || null, standingPrompt,
1668
+ env: sessionEnv,
1669
+ });
1663
1670
  completedSession = session;
1664
1671
  if (agent.stopped || generation !== agent.generation) {
1665
- void session.close("stale creation");
1672
+ await session.close("stale creation");
1666
1673
  throw new Error("stale runtime session creation");
1667
1674
  }
1668
1675
  agent.session = session;
@@ -1682,7 +1689,7 @@ export function createRuntimeHost(options) {
1682
1689
  agent.stabilityTimer = setTimeout(() => markSessionStable(agent, session), retryPolicy.stableWindowMs);
1683
1690
  agent.stabilityTimer.unref?.();
1684
1691
  return session;
1685
- }).finally(() => {
1692
+ })().finally(() => {
1686
1693
  agent.starting = null;
1687
1694
  if (completedSession && !agent.session && !agent.stopped) {
1688
1695
  scheduleRecreate(agent, agent.recreateReason || "runtime closed during session initialization");
@@ -2320,6 +2327,8 @@ export function createRuntimeHost(options) {
2320
2327
  emitConsumed(agent, [...startupConsumed, ...persist(agent)]);
2321
2328
  try {
2322
2329
  await ensureSession(agent);
2330
+ if (agent.stopped)
2331
+ return;
2323
2332
  reconcileSubagentLedger(agent, { forceMissing: true, missingReason: "runtime restarted" });
2324
2333
  enqueueUndeliveredTerminalWakes(agent);
2325
2334
  await recoverStalePiCompaction(agent);
@@ -2327,10 +2336,14 @@ export function createRuntimeHost(options) {
2327
2336
  const proactive = startupSession ? proactivelyCompactPiAtIdle(agent, startupSession) : null;
2328
2337
  if (proactive)
2329
2338
  await proactive;
2339
+ if (agent.stopped)
2340
+ return;
2330
2341
  activeCount += 1;
2331
2342
  await retryPending(agent);
2332
2343
  }
2333
2344
  catch (error) {
2345
+ if (agent.stopped)
2346
+ return;
2334
2347
  const reason = error instanceof Error ? error.message : String(error);
2335
2348
  const transient = error instanceof RuntimePrerequisiteError && error.readiness.state === "unavailable";
2336
2349
  agent.disabledReason = transient ? null : reason;
@@ -2344,6 +2357,8 @@ export function createRuntimeHost(options) {
2344
2357
  emit({ type: "agent-status", agentId: config.agentId, status: "error", error: reason,
2345
2358
  ...(error instanceof RuntimePrerequisiteError ? { readiness: error.readiness } : {}) });
2346
2359
  }
2360
+ if (agent.stopped)
2361
+ return;
2347
2362
  const visibleQuarantines = startupQuarantined.filter(({ record }) => agent.records.get(record.deliveryId)?.status === "error");
2348
2363
  for (const { record, code } of visibleQuarantines)
2349
2364
  emit({ type: "delivery", agentId: config.agentId,
@@ -2438,6 +2453,7 @@ export function createRuntimeHost(options) {
2438
2453
  if (!agent)
2439
2454
  return;
2440
2455
  agent.stopped = true;
2456
+ const starting = agent.starting;
2441
2457
  if (agent.poller)
2442
2458
  clearInterval(agent.poller);
2443
2459
  if (agent.retryTimer)
@@ -2451,6 +2467,10 @@ export function createRuntimeHost(options) {
2451
2467
  if (agent.busy)
2452
2468
  await agent.session?.cancel(reason);
2453
2469
  await agent.session?.close(reason);
2470
+ // A queued Pi initializer cannot be cancelled through the public adapter
2471
+ // contract. Drain it before removing the Agent; ensureSession sees stopped
2472
+ // and closes any session created after this point.
2473
+ await starting?.catch(() => { });
2454
2474
  managed.delete(agentId);
2455
2475
  emit({ type: "agent-status", agentId, status: "inactive" });
2456
2476
  },