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.
- package/CHANGELOG.md +8 -0
- package/dist/dashboard/dashboard-config-controller.mjs +2 -3
- package/dist/feishu/host-shell.cjs +47 -14
- package/dist/feishu/host-shell.mjs +47 -14
- package/dist/runtime/pi-compaction-recovery.cjs +46 -27
- package/dist/runtime/pi-compaction-recovery.mjs +45 -26
- package/dist/runtime/pi-model-catalog.cjs +3 -39
- package/dist/runtime/pi-model-catalog.mjs +3 -6
- package/dist/runtime/pi-subagent-injection.mjs +42 -14
- package/dist/runtime/provider-error-classifier.cjs +14 -0
- package/dist/runtime/provider-error-classifier.mjs +13 -0
- package/dist/runtime/runtime-adapters.mjs +69 -56
- package/dist/runtime/runtime-host.cjs +46 -26
- package/dist/runtime/runtime-host.mjs +46 -26
- package/dist/runtime/runtime-readiness.cjs +2 -0
- package/dist/runtime/runtime-readiness.mjs +2 -0
- package/package.json +1 -1
- package/dist/runtime/pi-artifact-provenance.mjs +0 -139
|
@@ -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`
|
|
11
|
-
*
|
|
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(
|
|
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(
|
|
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
|
|
222
|
-
|
|
223
|
-
|
|
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,
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
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
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
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:
|
|
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(
|
|
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
|
|
1096
|
-
|
|
1107
|
+
const policySource = verifyPiCapabilities({
|
|
1108
|
+
distribution: "external", version: reportedVersion,
|
|
1097
1109
|
contextWindow: state.model?.contextWindow, autoCompactionEnabled: state.autoCompactionEnabled,
|
|
1098
|
-
|
|
1099
|
-
|
|
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,
|
|
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
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
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
|
-
|
|
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
|
},
|