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.
- package/dist/agent/context-prompt.cjs +2 -2
- package/dist/agent/context-prompt.mjs +2 -2
- package/dist/agent/missed-outbound-scan.cjs +123 -0
- package/dist/agent/missed-outbound-scan.mjs +112 -0
- package/dist/feishu/host-shell.cjs +9 -0
- package/dist/feishu/host-shell.mjs +9 -0
- package/dist/feishu/message-policy.cjs +2 -0
- package/dist/feishu/message-policy.mjs +2 -0
- package/dist/runtime/pi-inline-extensions.mjs +40 -9
- package/dist/runtime/pi-subagent-injection.mjs +103 -10
- package/dist/runtime/pi-subagents.bundle.js +131 -58
- package/dist/runtime/pi-supervised-command-extension.mjs +72 -0
- package/dist/runtime/pi-supervised-command.bundle.js +4735 -0
- package/dist/runtime/pi-supervised-command.mjs +420 -0
- package/dist/setup/bot-register.mjs +53 -27
- package/dist/setup/grant-scopes.mjs +13 -4
- package/dist/setup/tenant-scope-grant.mjs +16 -0
- package/package.json +1 -1
- package/patches/@tintinweb%2Fpi-subagents@0.14.3.patch +712 -16
|
@@ -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-
|
|
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
|
|
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-
|
|
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
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|