larkin 0.4.18 → 0.4.20

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.
@@ -16,7 +16,7 @@ const agent_cli_capabilities_js_1 = require("./agent-cli-capabilities.cjs");
16
16
  * 5. 版本化:每次实质改动 bump LARKIN_STANDING_PROMPT_VERSION,并同步 eval 数据集/graders/断言。
17
17
  * 6. 用 eval 验证:行为变化必须配套固定场景 + rubric(evals/*、test/support/*-grader.mjs、live 测试)。
18
18
  */
19
- exports.LARKIN_STANDING_PROMPT_VERSION = "larkin-standing-v23";
19
+ exports.LARKIN_STANDING_PROMPT_VERSION = "larkin-standing-v25";
20
20
  /**
21
21
  * Agent 间协作唤醒引导(issue #75):纯文本 @ 不会产生飞书 mention 事件,
22
22
  * 必须由模型直接构造真实的 mention 元素;`--mention` 扩展参数已移除。
@@ -106,7 +106,7 @@ class ContextPromptBuilder {
106
106
  "",
107
107
  "## Background subagents (pi)",
108
108
  "Long-running, independent work MUST use the Agent tool with run_in_background: true. It is the ONLY supported background mechanism. nohup, `&`, disown, and shell background jobs are forbidden for delegated work.",
109
- "Foreground bash is hard-capped at 60 seconds. Never pass a bash timeout above 60 (it is refused immediately), and never run a command you expect to exceed 60s in the foreground. If a task is expected to take longer than 60s, delegate it to a background subagent (Agent with run_in_background: true) BEFORE running any bash. If a foreground bash call is refused or times out at the 60s limit, do NOT retry it in the foreground; delegate the work to a background subagent instead.",
109
+ "Foreground bash is hard-capped at 60 seconds. Never pass a bash timeout above 60. Never use nested bash to outlive 60s. If work must outlive 60s, Agent({ prompt, description, run_in_background: true }) and inside that background agent use supervised_start (executable+args, shell:false) once, then loop supervised_wait (timeout <= 60). Wait timeout returns still running and does NOT kill the process; Steer can arrive after each wait. Total lifetime is 600s. Abort/cancel/shutdown reaps the process tree. Do not use nohup / '&' / disown.",
110
110
  "Correct pattern:",
111
111
  "1. Call Agent with arguments like {\"prompt\": \"<task>\", \"description\": \"<short label>\", \"run_in_background\": true}.",
112
112
  "2. The tool returns an agent id immediately. Report it to the user and end the turn.",
@@ -13,7 +13,7 @@ import { agentCliPromptCapabilities } from "./agent-cli-capabilities.mjs";
13
13
  * 5. 版本化:每次实质改动 bump LARKIN_STANDING_PROMPT_VERSION,并同步 eval 数据集/graders/断言。
14
14
  * 6. 用 eval 验证:行为变化必须配套固定场景 + rubric(evals/*、test/support/*-grader.mjs、live 测试)。
15
15
  */
16
- export const LARKIN_STANDING_PROMPT_VERSION = "larkin-standing-v23";
16
+ export const LARKIN_STANDING_PROMPT_VERSION = "larkin-standing-v25";
17
17
  /**
18
18
  * Agent 间协作唤醒引导(issue #75):纯文本 @ 不会产生飞书 mention 事件,
19
19
  * 必须由模型直接构造真实的 mention 元素;`--mention` 扩展参数已移除。
@@ -103,7 +103,7 @@ export class ContextPromptBuilder {
103
103
  "",
104
104
  "## Background subagents (pi)",
105
105
  "Long-running, independent work MUST use the Agent tool with run_in_background: true. It is the ONLY supported background mechanism. nohup, `&`, disown, and shell background jobs are forbidden for delegated work.",
106
- "Foreground bash is hard-capped at 60 seconds. Never pass a bash timeout above 60 (it is refused immediately), and never run a command you expect to exceed 60s in the foreground. If a task is expected to take longer than 60s, delegate it to a background subagent (Agent with run_in_background: true) BEFORE running any bash. If a foreground bash call is refused or times out at the 60s limit, do NOT retry it in the foreground; delegate the work to a background subagent instead.",
106
+ "Foreground bash is hard-capped at 60 seconds. Never pass a bash timeout above 60. Never use nested bash to outlive 60s. If work must outlive 60s, Agent({ prompt, description, run_in_background: true }) and inside that background agent use supervised_start (executable+args, shell:false) once, then loop supervised_wait (timeout <= 60). Wait timeout returns still running and does NOT kill the process; Steer can arrive after each wait. Total lifetime is 600s. Abort/cancel/shutdown reaps the process tree. Do not use nohup / '&' / disown.",
107
107
  "Correct pattern:",
108
108
  "1. Call Agent with arguments like {\"prompt\": \"<task>\", \"description\": \"<short label>\", \"run_in_background\": true}.",
109
109
  "2. The tool returns an agent id immediately. Report it to the user and end the turn.",
@@ -1,6 +1,9 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
1
3
  import { pathToFileURL } from "node:url";
2
4
  import bashTimeoutExtension from "./pi-bash-timeout-extension.mjs";
3
- import { bundledPiSubagentExtensionPath } from "./pi-subagent-injection.mjs";
5
+ import { bundledPiSubagentExtensionPath, materializeEmbeddedPiSubagentBundle } from "./pi-subagent-injection.mjs";
6
+ import piSubagentRecordWatchdog from "./pi-subagent-record-watchdog.mjs";
4
7
  /**
5
8
  * Load the prebuilt, patched subagent bundle shipped in dist/ rather than the
6
9
  * package-manager copy. Standalone binaries additionally load the patched
@@ -8,11 +11,15 @@ import { bundledPiSubagentExtensionPath } from "./pi-subagent-injection.mjs";
8
11
  * continue to use the prebuilt patched bundle.
9
12
  */
10
13
  async function loadBundledPiSubagentExtension() {
11
- if (process.env.LARKIN_STANDALONE === "1") {
12
- const loaded = await import("@tintinweb/pi-subagents/dist/index.js");
13
- if (!loaded.default)
14
- throw new Error("Larkin bounded pi-subagents package is invalid; refusing to start builtin Pi");
15
- return loaded.default;
14
+ // Compiled standalone already has the patched package in Bun's module graph.
15
+ // The materialized sibling bundle externalizes @earendil-works/pi-coding-agent
16
+ // and cannot be imported from LARKIN_CONFIG_DIR.
17
+ const compiled = globalThis.__LARKIN_COMPILED_PI_SUBAGENTS__;
18
+ if (compiled) {
19
+ if (typeof compiled !== "function" && typeof compiled.factory !== "function") {
20
+ throw new Error("Larkin compiled pi-subagents factory is missing; refusing to start builtin Pi");
21
+ }
22
+ return compiled;
16
23
  }
17
24
  const bundle = bundledPiSubagentExtensionPath(process.env.LARKIN_CONFIG_DIR);
18
25
  if (!bundle)
@@ -22,6 +29,24 @@ async function loadBundledPiSubagentExtension() {
22
29
  throw new Error("Larkin bounded pi-subagents bundle is invalid; refusing to start builtin Pi");
23
30
  return loaded.default;
24
31
  }
32
+ function asFn(mod, name) {
33
+ if (typeof mod === "function")
34
+ return mod;
35
+ const fallback = mod?.default;
36
+ if (typeof fallback === "function")
37
+ return fallback;
38
+ throw new Error(`${name} extension factory is missing`);
39
+ }
40
+ function wrapFactory(name, factory) {
41
+ return async (pi) => {
42
+ try {
43
+ await factory(pi);
44
+ }
45
+ catch (error) {
46
+ throw error instanceof Error ? error : new Error(`${name}: ${String(error)}`);
47
+ }
48
+ };
49
+ }
25
50
  /** Builtin Pi extensions are loaded as code, never through Pi's path/data-URL loader. */
26
51
  const bundledSubagentsExtension = async (pi) => {
27
52
  const extension = await loadBundledPiSubagentExtension();
@@ -30,12 +55,23 @@ const bundledSubagentsExtension = async (pi) => {
30
55
  else
31
56
  await extension.factory(pi);
32
57
  };
58
+ // Watchdog must register session_shutdown before the bundled subagent extension
59
+ // so the final sweep can still read AgentManager.getRecord and bridge consumed
60
+ // terminals before manager teardown.
33
61
  export const BUILTIN_PI_EXTENSION_FACTORIES = Object.freeze([
34
- bundledSubagentsExtension,
35
- bashTimeoutExtension,
62
+ wrapFactory("larkin-pi-subagent-watchdog", asFn(piSubagentRecordWatchdog, "larkin-pi-subagent-watchdog")),
63
+ wrapFactory("larkin-pi-subagents", bundledSubagentsExtension),
64
+ wrapFactory("larkin-pi-bash-timeout", asFn(bashTimeoutExtension, "larkin-pi-bash-timeout")),
36
65
  ]);
37
66
  /** Invoke Pi RPC with Larkin's builtin-only extension factories. */
38
67
  export async function invokeBuiltinPiRpc(piMain, rest) {
68
+ if (globalThis.__LARKIN_EMBEDDED_PI_SUBAGENTS_BUNDLE__ && process.env.LARKIN_CONFIG_DIR) {
69
+ const materialized = materializeEmbeddedPiSubagentBundle(process.env.LARKIN_CONFIG_DIR);
70
+ if (!materialized)
71
+ throw new Error("standalone pi-subagents bundle failed to materialize");
72
+ const supervised = path.join(path.dirname(materialized), "pi-supervised-command.bundle.js");
73
+ process.env.LARKIN_PI_SUPERVISED_BUNDLE = fs.realpathSync(supervised);
74
+ }
39
75
  await piMain(["--mode", "rpc", ...rest], {
40
76
  extensionFactories: [...BUILTIN_PI_EXTENSION_FACTORIES],
41
77
  });
@@ -17,22 +17,113 @@ import { fileURLToPath } from "node:url";
17
17
  * 把 embedded bundle 落盘到 <configDir>/providers/pi/extensions/pi-subagents.bundle.js(0700/0600)。
18
18
  * 无 embedded 资产或 configDir 缺失时返回 null。
19
19
  */
20
+ function rejectSymlinkPath(target) {
21
+ let current = path.resolve(target);
22
+ for (;;) {
23
+ if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) {
24
+ throw new Error("bundle path must not contain a symlink");
25
+ }
26
+ const parent = path.dirname(current);
27
+ if (parent === current)
28
+ break;
29
+ current = parent;
30
+ }
31
+ }
32
+ function backupRegularFile(target) {
33
+ if (!fs.existsSync(target))
34
+ return null;
35
+ const stat = fs.lstatSync(target);
36
+ if (stat.isSymbolicLink())
37
+ throw new Error("bundle target must not be a symlink");
38
+ return { content: fs.readFileSync(target), mode: stat.mode & 0o777 };
39
+ }
40
+ function restoreRegularFile(target, backup) {
41
+ if (fs.existsSync(target) && fs.lstatSync(target).isSymbolicLink()) {
42
+ throw new Error("bundle target must not be a symlink");
43
+ }
44
+ if (!backup) {
45
+ if (fs.existsSync(target))
46
+ fs.unlinkSync(target);
47
+ return;
48
+ }
49
+ fs.writeFileSync(target, backup.content, { mode: backup.mode });
50
+ fs.chmodSync(target, backup.mode);
51
+ }
20
52
  export function materializeEmbeddedPiSubagentBundle(configDir) {
21
53
  const embedded = globalThis.__LARKIN_EMBEDDED_PI_SUBAGENTS_BUNDLE__;
54
+ const supervised = globalThis.__LARKIN_EMBEDDED_PI_SUPERVISED_COMMAND_BUNDLE__;
22
55
  if (!embedded || !configDir)
23
56
  return null;
24
- const dir = path.join(path.resolve(configDir), "providers", "pi", "extensions");
25
- const target = path.join(dir, "pi-subagents.bundle.js");
57
+ if (!supervised)
58
+ throw new Error("supervised command bundle is missing");
59
+ const resolvedConfig = path.resolve(configDir);
60
+ fs.mkdirSync(resolvedConfig, { recursive: true, mode: 0o700 });
61
+ const canonical = fs.realpathSync(resolvedConfig);
62
+ const dir = path.join(canonical, "providers", "pi", "extensions");
63
+ const subTarget = path.join(dir, "pi-subagents.bundle.js");
64
+ const supTarget = path.join(dir, "pi-supervised-command.bundle.js");
65
+ const lockPath = path.join(dir, ".materialize.lock");
66
+ let lock;
26
67
  try {
68
+ rejectSymlinkPath(dir);
27
69
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
70
+ rejectSymlinkPath(dir);
28
71
  fs.chmodSync(dir, 0o700);
29
- if (!fs.existsSync(target) || fs.readFileSync(target, "utf8") !== embedded) {
30
- const temporary = `${target}.${process.pid}.tmp`;
31
- fs.writeFileSync(temporary, embedded, { mode: 0o600, flag: "wx" });
32
- fs.renameSync(temporary, target);
33
- fs.chmodSync(target, 0o600);
72
+ lock = fs.openSync(lockPath, "wx", 0o600);
73
+ const oldSub = backupRegularFile(subTarget);
74
+ const oldSup = backupRegularFile(supTarget);
75
+ try {
76
+ writePrivateBundle(dir, "pi-subagents.bundle.js", embedded);
77
+ writePrivateBundle(dir, "pi-supervised-command.bundle.js", supervised);
78
+ return subTarget;
79
+ }
80
+ catch (error) {
81
+ restoreRegularFile(subTarget, oldSub);
82
+ restoreRegularFile(supTarget, oldSup);
83
+ throw error;
84
+ }
85
+ }
86
+ catch (error) {
87
+ if (error instanceof Error && error.message.includes("supervised command bundle is missing"))
88
+ throw error;
89
+ return null;
90
+ }
91
+ finally {
92
+ if (lock !== undefined) {
93
+ try {
94
+ fs.closeSync(lock);
95
+ }
96
+ catch { /* ignore */ }
97
+ try {
98
+ fs.unlinkSync(lockPath);
99
+ }
100
+ catch { /* ignore */ }
34
101
  }
35
- return target;
102
+ }
103
+ }
104
+ function writePrivateBundle(dir, name, embedded) {
105
+ const target = path.join(dir, name);
106
+ rejectSymlinkPath(target);
107
+ if (fs.existsSync(target) && fs.lstatSync(target).isSymbolicLink()) {
108
+ throw new Error("bundle target must not be a symlink");
109
+ }
110
+ if (!fs.existsSync(target) || fs.readFileSync(target, "utf8") !== embedded) {
111
+ const temporary = `${target}.${process.pid}.tmp`;
112
+ fs.writeFileSync(temporary, embedded, { mode: 0o600, flag: "wx" });
113
+ fs.renameSync(temporary, target);
114
+ }
115
+ fs.chmodSync(target, 0o600);
116
+ return target;
117
+ }
118
+ export function materializeEmbeddedPiSupervisedCommandBundle(dirOrConfig) {
119
+ const embedded = globalThis.__LARKIN_EMBEDDED_PI_SUPERVISED_COMMAND_BUNDLE__;
120
+ if (!embedded || !dirOrConfig)
121
+ return null;
122
+ const dir = dirOrConfig.endsWith("extensions") ? dirOrConfig : path.join(path.resolve(dirOrConfig), "providers", "pi", "extensions");
123
+ try {
124
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
125
+ fs.chmodSync(dir, 0o700);
126
+ return writePrivateBundle(dir, "pi-supervised-command.bundle.js", embedded);
36
127
  }
37
128
  catch {
38
129
  return null;
@@ -82,6 +173,7 @@ export function probeExternalPiVersion(piCommand, env) {
82
173
  * 重复注册导致 pi 扩展加载 FATAL(pi 对 duplicate tool registration 是硬失败)。
83
174
  */
84
175
  const BOUNDED_WAIT_CAPABILITY = "larkin-pi-subagents-bounded-wait-v1";
176
+ const SUPERVISED_COMMAND_CAPABILITY = "larkin-pi-supervised-command-v1";
85
177
  function userPiSubagentsWaitCapability(env) {
86
178
  const agentDir = env.PI_CODING_AGENT_DIR || path.join(env.HOME || process.env.HOME || "", ".pi", "agent");
87
179
  try {
@@ -116,7 +208,8 @@ function userPiSubagentsWaitCapability(env) {
116
208
  const capabilityFiles = extensions.map((entry) => path.resolve(installedRoot, entry));
117
209
  const hasCapability = capabilityFiles.every((file) => {
118
210
  try {
119
- return fs.readFileSync(file, "utf8").includes(BOUNDED_WAIT_CAPABILITY);
211
+ const source = fs.readFileSync(file, "utf8");
212
+ return source.includes(BOUNDED_WAIT_CAPABILITY) && source.includes(SUPERVISED_COMMAND_CAPABILITY);
120
213
  }
121
214
  catch {
122
215
  return false;
@@ -143,7 +236,7 @@ export function resolvePiSubagentExtensionArg(input, probeVersion = () => probeE
143
236
  if (!userPiSubagentsHasBoundedWaitCapability(input.env)) {
144
237
  throw new Error("[larkin] WARNING: refusing external Pi because the user-installed " +
145
238
  "pi-subagents extension is unbounded or unverifiable; remove it or install " +
146
- "a version advertising larkin-pi-subagents-bounded-wait-v1.");
239
+ "a version advertising larkin-pi-subagents-bounded-wait-v1 and larkin-pi-supervised-command-v1.");
147
240
  }
148
241
  return null;
149
242
  }