larkin 0.4.19 → 0.4.21

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.",
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DEFAULT_MISSED_OUTBOUND_REPEAT = exports.DEFAULT_MISSED_OUTBOUND_TITLE = void 0;
7
+ exports.parseOmMessageId = parseOmMessageId;
8
+ exports.parseScanDeliveryTarget = parseScanDeliveryTarget;
9
+ exports.isDefaultMissedOutboundReminder = isDefaultMissedOutboundReminder;
10
+ exports.ensureDefaultMissedOutboundScanReminder = ensureDefaultMissedOutboundScanReminder;
11
+ exports.persistInboundScanTarget = persistInboundScanTarget;
12
+ const node_path_1 = __importDefault(require("node:path"));
13
+ const reminder_store_js_1 = require("./reminder-store.cjs");
14
+ exports.DEFAULT_MISSED_OUTBOUND_TITLE = "Read scoped history from this reminder envelope's persisted deliveryTarget (chat: +chat-messages-list; thread: +threads-messages-list). Judge unanswered asks, undelivered follow-ups, and stalled work. On a hit, post a short status in the same conversation using that persisted deliveryTarget/anchor; otherwise stay silent. Never infer recipients from the title.";
15
+ exports.DEFAULT_MISSED_OUTBOUND_REPEAT = "every:15m";
16
+ const CHAT_TARGET = /^chat:(oc_[A-Za-z0-9]+)$/;
17
+ const THREAD_TARGET = /^thread:(oc_[A-Za-z0-9]+):(omt_[A-Za-z0-9]+)$/;
18
+ const OM_ID = /^om_[A-Za-z0-9]+$/;
19
+ function parseOmMessageId(value, label = "message-id") {
20
+ const id = String(value || "").trim();
21
+ if (!OM_ID.test(id))
22
+ throw new Error(`${label} 必须是严格 om_ 语法`);
23
+ return id;
24
+ }
25
+ function parseScanDeliveryTarget(target, anchor) {
26
+ const deliveryTarget = String(target || "").trim();
27
+ if (!deliveryTarget) {
28
+ throw new Error("user-facing reminder 必须显式指定 delivery target,不得从标题推断收件人");
29
+ }
30
+ const chat = deliveryTarget.match(CHAT_TARGET);
31
+ if (chat) {
32
+ return {
33
+ deliveryTarget,
34
+ scope: "chat",
35
+ chatId: chat[1],
36
+ threadId: null,
37
+ deliveryAnchor: anchor ? parseOmMessageId(anchor, "delivery anchor") : null,
38
+ };
39
+ }
40
+ const thread = deliveryTarget.match(THREAD_TARGET);
41
+ if (thread) {
42
+ return {
43
+ deliveryTarget,
44
+ scope: "thread",
45
+ chatId: thread[1],
46
+ threadId: thread[2],
47
+ deliveryAnchor: parseOmMessageId(anchor, "delivery anchor"),
48
+ };
49
+ }
50
+ throw new Error("delivery target 必须是 chat:oc_… 或 thread:oc_…:omt_…,禁止推断 DM");
51
+ }
52
+ function isDefaultMissedOutboundReminder(reminder) {
53
+ return reminder?.title === exports.DEFAULT_MISSED_OUTBOUND_TITLE
54
+ && reminder.repeat === exports.DEFAULT_MISSED_OUTBOUND_REPEAT
55
+ && reminder.deliveryMode === "user"
56
+ && reminder.status === "scheduled";
57
+ }
58
+ function ensureDefaultMissedOutboundScanReminder(input) {
59
+ const parsed = parseScanDeliveryTarget(input.deliveryTarget, input.deliveryAnchor);
60
+ const recurrence = (0, reminder_store_js_1.parseRepeat)(exports.DEFAULT_MISSED_OUTBOUND_REPEAT);
61
+ if ("error" in recurrence)
62
+ throw new Error(recurrence.error);
63
+ const now = input.nowMs ?? Date.now();
64
+ const wantedAnchor = parsed.deliveryAnchor;
65
+ return (0, reminder_store_js_1.mutate)(input.storeFile, (store) => {
66
+ const sameIdentity = (reminder) => reminder.ownerAgentId === input.agentId
67
+ && reminder.title === exports.DEFAULT_MISSED_OUTBOUND_TITLE
68
+ && reminder.repeat === exports.DEFAULT_MISSED_OUTBOUND_REPEAT
69
+ && reminder.deliveryMode === "user"
70
+ && reminder.deliveryTarget === parsed.deliveryTarget;
71
+ const scheduled = store.reminders.find((reminder) => sameIdentity(reminder) && reminder.status === "scheduled");
72
+ if (scheduled) {
73
+ if ((scheduled.deliveryAnchor || null) !== wantedAnchor)
74
+ scheduled.deliveryAnchor = wantedAnchor;
75
+ return { created: false, reminderId: scheduled.reminderId, rebuilt: false };
76
+ }
77
+ const canceled = store.reminders.find((reminder) => sameIdentity(reminder) && reminder.status === "canceled");
78
+ if (canceled)
79
+ return { created: false, reminderId: canceled.reminderId, rebuilt: false };
80
+ const hadFired = store.reminders.some((reminder) => sameIdentity(reminder) && reminder.status === "fired");
81
+ store.reminders = store.reminders.filter((reminder) => !(sameIdentity(reminder) && reminder.status === "fired"));
82
+ const reminderId = (0, reminder_store_js_1.newId)();
83
+ store.reminders.push({
84
+ reminderId,
85
+ ownerAgentId: input.agentId,
86
+ title: exports.DEFAULT_MISSED_OUTBOUND_TITLE,
87
+ fireAt: (0, reminder_store_js_1.nowIso)(now + 15 * 60_000),
88
+ firedAt: null,
89
+ createdAt: (0, reminder_store_js_1.nowIso)(now),
90
+ status: "scheduled",
91
+ version: 1,
92
+ events: [],
93
+ deliveryTarget: parsed.deliveryTarget,
94
+ deliveryAnchor: wantedAnchor,
95
+ deliveryMode: "user",
96
+ repeat: exports.DEFAULT_MISSED_OUTBOUND_REPEAT,
97
+ });
98
+ return { created: true, reminderId, rebuilt: hadFired };
99
+ });
100
+ }
101
+ function persistInboundScanTarget(stateDir, event, agentId, storeFile = node_path_1.default.join(stateDir, "reminders.json")) {
102
+ if (event._sender_is_bot)
103
+ throw new Error("scan reminder 只接受 human inbound");
104
+ if (event._scan_authority !== true)
105
+ throw new Error("scan reminder 需要权威 chatType/senderIsBot");
106
+ if (event.chat_type !== "group")
107
+ throw new Error("scan reminder 只接受 group/thread,禁止 DM");
108
+ const chatId = String(event.chat_id || "");
109
+ if (!/^oc_[A-Za-z0-9]+$/.test(chatId)) {
110
+ throw new Error("user-facing reminder 必须显式指定 delivery target,不得从标题推断收件人");
111
+ }
112
+ const threadId = event.thread_id ? String(event.thread_id) : "";
113
+ const parsed = threadId
114
+ ? parseScanDeliveryTarget(`thread:${chatId}:${threadId}`, event.message_id)
115
+ : parseScanDeliveryTarget(`chat:${chatId}`, event.message_id || null);
116
+ ensureDefaultMissedOutboundScanReminder({
117
+ storeFile,
118
+ agentId,
119
+ deliveryTarget: parsed.deliveryTarget,
120
+ deliveryAnchor: parsed.deliveryAnchor,
121
+ });
122
+ return parsed;
123
+ }
@@ -0,0 +1,112 @@
1
+ import path from "node:path";
2
+ import { mutate, newId, nowIso, parseRepeat } from "./reminder-store.mjs";
3
+ export const DEFAULT_MISSED_OUTBOUND_TITLE = "Read scoped history from this reminder envelope's persisted deliveryTarget (chat: +chat-messages-list; thread: +threads-messages-list). Judge unanswered asks, undelivered follow-ups, and stalled work. On a hit, post a short status in the same conversation using that persisted deliveryTarget/anchor; otherwise stay silent. Never infer recipients from the title.";
4
+ export const DEFAULT_MISSED_OUTBOUND_REPEAT = "every:15m";
5
+ const CHAT_TARGET = /^chat:(oc_[A-Za-z0-9]+)$/;
6
+ const THREAD_TARGET = /^thread:(oc_[A-Za-z0-9]+):(omt_[A-Za-z0-9]+)$/;
7
+ const OM_ID = /^om_[A-Za-z0-9]+$/;
8
+ export function parseOmMessageId(value, label = "message-id") {
9
+ const id = String(value || "").trim();
10
+ if (!OM_ID.test(id))
11
+ throw new Error(`${label} 必须是严格 om_ 语法`);
12
+ return id;
13
+ }
14
+ export function parseScanDeliveryTarget(target, anchor) {
15
+ const deliveryTarget = String(target || "").trim();
16
+ if (!deliveryTarget) {
17
+ throw new Error("user-facing reminder 必须显式指定 delivery target,不得从标题推断收件人");
18
+ }
19
+ const chat = deliveryTarget.match(CHAT_TARGET);
20
+ if (chat) {
21
+ return {
22
+ deliveryTarget,
23
+ scope: "chat",
24
+ chatId: chat[1],
25
+ threadId: null,
26
+ deliveryAnchor: anchor ? parseOmMessageId(anchor, "delivery anchor") : null,
27
+ };
28
+ }
29
+ const thread = deliveryTarget.match(THREAD_TARGET);
30
+ if (thread) {
31
+ return {
32
+ deliveryTarget,
33
+ scope: "thread",
34
+ chatId: thread[1],
35
+ threadId: thread[2],
36
+ deliveryAnchor: parseOmMessageId(anchor, "delivery anchor"),
37
+ };
38
+ }
39
+ throw new Error("delivery target 必须是 chat:oc_… 或 thread:oc_…:omt_…,禁止推断 DM");
40
+ }
41
+ export function isDefaultMissedOutboundReminder(reminder) {
42
+ return reminder?.title === DEFAULT_MISSED_OUTBOUND_TITLE
43
+ && reminder.repeat === DEFAULT_MISSED_OUTBOUND_REPEAT
44
+ && reminder.deliveryMode === "user"
45
+ && reminder.status === "scheduled";
46
+ }
47
+ export function ensureDefaultMissedOutboundScanReminder(input) {
48
+ const parsed = parseScanDeliveryTarget(input.deliveryTarget, input.deliveryAnchor);
49
+ const recurrence = parseRepeat(DEFAULT_MISSED_OUTBOUND_REPEAT);
50
+ if ("error" in recurrence)
51
+ throw new Error(recurrence.error);
52
+ const now = input.nowMs ?? Date.now();
53
+ const wantedAnchor = parsed.deliveryAnchor;
54
+ return mutate(input.storeFile, (store) => {
55
+ const sameIdentity = (reminder) => reminder.ownerAgentId === input.agentId
56
+ && reminder.title === DEFAULT_MISSED_OUTBOUND_TITLE
57
+ && reminder.repeat === DEFAULT_MISSED_OUTBOUND_REPEAT
58
+ && reminder.deliveryMode === "user"
59
+ && reminder.deliveryTarget === parsed.deliveryTarget;
60
+ const scheduled = store.reminders.find((reminder) => sameIdentity(reminder) && reminder.status === "scheduled");
61
+ if (scheduled) {
62
+ if ((scheduled.deliveryAnchor || null) !== wantedAnchor)
63
+ scheduled.deliveryAnchor = wantedAnchor;
64
+ return { created: false, reminderId: scheduled.reminderId, rebuilt: false };
65
+ }
66
+ const canceled = store.reminders.find((reminder) => sameIdentity(reminder) && reminder.status === "canceled");
67
+ if (canceled)
68
+ return { created: false, reminderId: canceled.reminderId, rebuilt: false };
69
+ const hadFired = store.reminders.some((reminder) => sameIdentity(reminder) && reminder.status === "fired");
70
+ store.reminders = store.reminders.filter((reminder) => !(sameIdentity(reminder) && reminder.status === "fired"));
71
+ const reminderId = newId();
72
+ store.reminders.push({
73
+ reminderId,
74
+ ownerAgentId: input.agentId,
75
+ title: DEFAULT_MISSED_OUTBOUND_TITLE,
76
+ fireAt: nowIso(now + 15 * 60_000),
77
+ firedAt: null,
78
+ createdAt: nowIso(now),
79
+ status: "scheduled",
80
+ version: 1,
81
+ events: [],
82
+ deliveryTarget: parsed.deliveryTarget,
83
+ deliveryAnchor: wantedAnchor,
84
+ deliveryMode: "user",
85
+ repeat: DEFAULT_MISSED_OUTBOUND_REPEAT,
86
+ });
87
+ return { created: true, reminderId, rebuilt: hadFired };
88
+ });
89
+ }
90
+ export function persistInboundScanTarget(stateDir, event, agentId, storeFile = path.join(stateDir, "reminders.json")) {
91
+ if (event._sender_is_bot)
92
+ throw new Error("scan reminder 只接受 human inbound");
93
+ if (event._scan_authority !== true)
94
+ throw new Error("scan reminder 需要权威 chatType/senderIsBot");
95
+ if (event.chat_type !== "group")
96
+ throw new Error("scan reminder 只接受 group/thread,禁止 DM");
97
+ const chatId = String(event.chat_id || "");
98
+ if (!/^oc_[A-Za-z0-9]+$/.test(chatId)) {
99
+ throw new Error("user-facing reminder 必须显式指定 delivery target,不得从标题推断收件人");
100
+ }
101
+ const threadId = event.thread_id ? String(event.thread_id) : "";
102
+ const parsed = threadId
103
+ ? parseScanDeliveryTarget(`thread:${chatId}:${threadId}`, event.message_id)
104
+ : parseScanDeliveryTarget(`chat:${chatId}`, event.message_id || null);
105
+ ensureDefaultMissedOutboundScanReminder({
106
+ storeFile,
107
+ agentId,
108
+ deliveryTarget: parsed.deliveryTarget,
109
+ deliveryAnchor: parsed.deliveryAnchor,
110
+ });
111
+ return parsed;
112
+ }
@@ -51,6 +51,7 @@ const host_business_state_js_1 = require("./host-business-state.cjs");
51
51
  const host_processing_eye_js_1 = require("./host-processing-eye.cjs");
52
52
  const inbox_projection_js_1 = require("../agent/inbox-projection.cjs");
53
53
  const host_reminder_orchestrator_js_1 = require("../agent/host-reminder-orchestrator.cjs");
54
+ const missed_outbound_scan_js_1 = require("../agent/missed-outbound-scan.cjs");
54
55
  const host_channel_business_js_1 = require("./host-channel-business.cjs");
55
56
  const interaction_orchestrator_js_1 = require("./interaction-orchestrator.cjs");
56
57
  const message_policy_js_1 = require("./message-policy.cjs");
@@ -385,6 +386,14 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
385
386
  // append/dedupe decision is durable. Agent model-seen state is untouched.
386
387
  if (event.event_id)
387
388
  seenEventIds.add(eventKey);
389
+ if (!event._sender_is_bot && event._scan_authority) {
390
+ try {
391
+ (0, missed_outbound_scan_js_1.persistInboundScanTarget)(agent.stateDir, event, agent.agentId);
392
+ }
393
+ catch (error) {
394
+ log(`inbound scan target 未持久化: ${error.message}`);
395
+ }
396
+ }
388
397
  if (append.status === "duplicate_consumed")
389
398
  return null;
390
399
  const inboxEnvelope = append.envelope;
@@ -11,6 +11,7 @@ import { HostEnvelopeProjector, HostStateProjection, SenderIdentityCache, projec
11
11
  import { ProcessingEyeOrchestrator } from "./host-processing-eye.mjs";
12
12
  import { projectInboxEnvelope, targetKeyOfInboxEnvelope } from "../agent/inbox-projection.mjs";
13
13
  import { HostReminderOrchestrator } from "../agent/host-reminder-orchestrator.mjs";
14
+ import { persistInboundScanTarget } from "../agent/missed-outbound-scan.mjs";
14
15
  import { HostChannelBusiness } from "./host-channel-business.mjs";
15
16
  import { HostInteractionOrchestrator } from "./interaction-orchestrator.mjs";
16
17
  import { targetFor } from "./message-policy.mjs";
@@ -345,6 +346,14 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
345
346
  // append/dedupe decision is durable. Agent model-seen state is untouched.
346
347
  if (event.event_id)
347
348
  seenEventIds.add(eventKey);
349
+ if (!event._sender_is_bot && event._scan_authority) {
350
+ try {
351
+ persistInboundScanTarget(agent.stateDir, event, agent.agentId);
352
+ }
353
+ catch (error) {
354
+ log(`inbound scan target 未持久化: ${error.message}`);
355
+ }
356
+ }
348
357
  if (append.status === "duplicate_consumed")
349
358
  return null;
350
359
  const inboxEnvelope = append.envelope;
@@ -39,6 +39,8 @@ function normalizeChannelMessage(message) {
39
39
  _mentioned_bot: Boolean(message.mentionedBot || message.mentionAll),
40
40
  _mention_all: Boolean(message.mentionAll),
41
41
  _sender_is_bot: message.senderIsBot === true,
42
+ _scan_authority: typeof message.senderIsBot === "boolean"
43
+ && (message.chatType === "group" || message.chatMode === "topic"),
42
44
  };
43
45
  }
44
46
  function slug10(value) {
@@ -26,6 +26,8 @@ export function normalizeChannelMessage(message) {
26
26
  _mentioned_bot: Boolean(message.mentionedBot || message.mentionAll),
27
27
  _mention_all: Boolean(message.mentionAll),
28
28
  _sender_is_bot: message.senderIsBot === true,
29
+ _scan_authority: typeof message.senderIsBot === "boolean"
30
+ && (message.chatType === "group" || message.chatMode === "topic"),
29
31
  };
30
32
  }
31
33
  export function slug10(value) {
@@ -1,6 +1,8 @@
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";
4
6
  import piSubagentRecordWatchdog from "./pi-subagent-record-watchdog.mjs";
5
7
  /**
6
8
  * Load the prebuilt, patched subagent bundle shipped in dist/ rather than the
@@ -9,11 +11,15 @@ import piSubagentRecordWatchdog from "./pi-subagent-record-watchdog.mjs";
9
11
  * continue to use the prebuilt patched bundle.
10
12
  */
11
13
  async function loadBundledPiSubagentExtension() {
12
- if (process.env.LARKIN_STANDALONE === "1") {
13
- const loaded = await import("@tintinweb/pi-subagents/dist/index.js");
14
- if (!loaded.default)
15
- throw new Error("Larkin bounded pi-subagents package is invalid; refusing to start builtin Pi");
16
- 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;
17
23
  }
18
24
  const bundle = bundledPiSubagentExtensionPath(process.env.LARKIN_CONFIG_DIR);
19
25
  if (!bundle)
@@ -23,6 +29,24 @@ async function loadBundledPiSubagentExtension() {
23
29
  throw new Error("Larkin bounded pi-subagents bundle is invalid; refusing to start builtin Pi");
24
30
  return loaded.default;
25
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
+ }
26
50
  /** Builtin Pi extensions are loaded as code, never through Pi's path/data-URL loader. */
27
51
  const bundledSubagentsExtension = async (pi) => {
28
52
  const extension = await loadBundledPiSubagentExtension();
@@ -35,12 +59,19 @@ const bundledSubagentsExtension = async (pi) => {
35
59
  // so the final sweep can still read AgentManager.getRecord and bridge consumed
36
60
  // terminals before manager teardown.
37
61
  export const BUILTIN_PI_EXTENSION_FACTORIES = Object.freeze([
38
- piSubagentRecordWatchdog,
39
- bundledSubagentsExtension,
40
- 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")),
41
65
  ]);
42
66
  /** Invoke Pi RPC with Larkin's builtin-only extension factories. */
43
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
+ }
44
75
  await piMain(["--mode", "rpc", ...rest], {
45
76
  extensionFactories: [...BUILTIN_PI_EXTENSION_FACTORIES],
46
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
  }