larkin 0.4.21 → 0.4.23
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/agent-cli-capabilities.cjs +4 -2
- package/dist/agent/agent-cli-capabilities.mjs +4 -2
- package/dist/agent/context-prompt.cjs +7 -6
- package/dist/agent/context-prompt.mjs +7 -6
- package/dist/agent/inbox-audit-heartbeat.cjs +63 -0
- package/dist/agent/inbox-audit-heartbeat.mjs +59 -0
- package/dist/agent/missed-outbound-scan.cjs +124 -114
- package/dist/agent/missed-outbound-scan.mjs +87 -105
- package/dist/app/agent-cli.mjs +10 -2
- package/dist/app/lark-cli.mjs +137 -5
- package/dist/feishu/host-shell.cjs +15 -7
- package/dist/feishu/host-shell.mjs +16 -8
- package/package.json +4 -1
|
@@ -12,7 +12,7 @@ const internal_command_js_1 = require("../app/internal-command.cjs");
|
|
|
12
12
|
exports.AGENT_CLI_CAPABILITIES = Object.freeze({
|
|
13
13
|
version: 1,
|
|
14
14
|
commands: Object.freeze({
|
|
15
|
-
inbox: Object.freeze(["check", "poll"]),
|
|
15
|
+
inbox: Object.freeze(["check", "poll", "audit"]),
|
|
16
16
|
comment: Object.freeze(["reply"]),
|
|
17
17
|
reminder: Object.freeze(["schedule", "list", "snooze", "update", "cancel", "log"]),
|
|
18
18
|
interaction: Object.freeze(["callback-status", "callback-probe", "create", "get", "resolve"]),
|
|
@@ -31,7 +31,9 @@ function resolveAgentCliExecutable(cliPath, runtimeExecutable = node_process_1.d
|
|
|
31
31
|
function agentCliPromptCapabilities(executable = "larkin") {
|
|
32
32
|
const commands = Object.entries(exports.AGENT_CLI_CAPABILITIES.commands).flatMap(([group, operations]) => operations.map((operation) => ({
|
|
33
33
|
command: `${group} ${operation}`,
|
|
34
|
-
purpose: group === "inbox" ? (operation === "check" ? "Read pending target summaries without consuming messages."
|
|
34
|
+
purpose: group === "inbox" ? (operation === "check" ? "Read pending target summaries without consuming messages."
|
|
35
|
+
: operation === "audit" ? "Read bounded observed group/topic audit targets and instructions."
|
|
36
|
+
: "Poll full messages and direct-ack the returned batch.") :
|
|
35
37
|
group === "comment" ? "Reply once to the exact cloud-document comment bound by a polled Inbox message id." :
|
|
36
38
|
group === "reminder" ? "Manage this Agent's durable reminders." :
|
|
37
39
|
group === "interaction" ? "Create, inspect, or resolve a durable interactive card run." :
|
|
@@ -4,7 +4,7 @@ import { INTERNAL_AGENT_CLI, internalCommandShell } from "../app/internal-comman
|
|
|
4
4
|
export const AGENT_CLI_CAPABILITIES = Object.freeze({
|
|
5
5
|
version: 1,
|
|
6
6
|
commands: Object.freeze({
|
|
7
|
-
inbox: Object.freeze(["check", "poll"]),
|
|
7
|
+
inbox: Object.freeze(["check", "poll", "audit"]),
|
|
8
8
|
comment: Object.freeze(["reply"]),
|
|
9
9
|
reminder: Object.freeze(["schedule", "list", "snooze", "update", "cancel", "log"]),
|
|
10
10
|
interaction: Object.freeze(["callback-status", "callback-probe", "create", "get", "resolve"]),
|
|
@@ -23,7 +23,9 @@ export function resolveAgentCliExecutable(cliPath, runtimeExecutable = process.e
|
|
|
23
23
|
export function agentCliPromptCapabilities(executable = "larkin") {
|
|
24
24
|
const commands = Object.entries(AGENT_CLI_CAPABILITIES.commands).flatMap(([group, operations]) => operations.map((operation) => ({
|
|
25
25
|
command: `${group} ${operation}`,
|
|
26
|
-
purpose: group === "inbox" ? (operation === "check" ? "Read pending target summaries without consuming messages."
|
|
26
|
+
purpose: group === "inbox" ? (operation === "check" ? "Read pending target summaries without consuming messages."
|
|
27
|
+
: operation === "audit" ? "Read bounded observed group/topic audit targets and instructions."
|
|
28
|
+
: "Poll full messages and direct-ack the returned batch.") :
|
|
27
29
|
group === "comment" ? "Reply once to the exact cloud-document comment bound by a polled Inbox message id." :
|
|
28
30
|
group === "reminder" ? "Manage this Agent's durable reminders." :
|
|
29
31
|
group === "interaction" ? "Create, inspect, or resolve a durable interactive card run." :
|
|
@@ -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-v27";
|
|
20
20
|
/**
|
|
21
21
|
* Agent 间协作唤醒引导(issue #75):纯文本 @ 不会产生飞书 mention 事件,
|
|
22
22
|
* 必须由模型直接构造真实的 mention 元素;`--mention` 扩展参数已移除。
|
|
@@ -29,7 +29,7 @@ exports.AGENT_MENTION_GUIDANCE = [
|
|
|
29
29
|
"Only mention another Agent when it genuinely needs to act again; keep the existing loop-prevention boundary.",
|
|
30
30
|
];
|
|
31
31
|
const FEISHU_IM_COMMAND_GROUPS = [
|
|
32
|
-
["Messages", ["im +messages-send", "im +messages-reply", "im +chat-messages-list", "im +threads-messages-list", "im +messages-mget"]],
|
|
32
|
+
["Messages", ["im +messages-send", "im +messages-reply", "im messages delete", "im +chat-messages-list", "im +threads-messages-list", "im +messages-mget"]],
|
|
33
33
|
["Chats", ["im +chat-list", "im +chat-search", "im chats get"]],
|
|
34
34
|
];
|
|
35
35
|
function clean(value) {
|
|
@@ -133,13 +133,13 @@ class ContextPromptBuilder {
|
|
|
133
133
|
"Successful history response messages are always at `data.messages`. Never use a chat-wide fallback for a thread target, never merge stderr with `2>&1` before parsing JSON, and never truncate structured output before parsing it.",
|
|
134
134
|
"If the scoped history read fails or its schema is invalid, fail visibly. Do not reuse remembered or hard-coded text to make the task appear successful.",
|
|
135
135
|
"Only a real Feishu `message_id` beginning with `om_` may be passed to `+messages-reply`; `rem_`, `redeliver_`, and every other synthetic ID must never be replied to. Send with a confirmed `chat_id` and never guess a chat id from a display name.",
|
|
136
|
-
"Before every send/reply/card write, Larkin probes the exact chat or thread history with the current Bot identity. A nonzero `freshness_conflict` includes bounded unseen context and direct-acks that cursor; reconsider it, then retry the ordinary command. Larkin never saves the blocked
|
|
137
|
-
"For
|
|
136
|
+
"Before every send/reply/card/recall write, Larkin probes the exact chat or thread history with the current Bot identity. A nonzero `freshness_conflict` includes bounded unseen context and direct-acks that cursor; reconsider it, then retry the ordinary command. Larkin never saves the blocked operation as a draft.",
|
|
137
|
+
"For ordinary plain-text message bodies, use `--text`, including brief one-line replies, multiline status lines, and paragraphs. Use `--markdown` only when you intentionally need Markdown rendering, such as headings, lists, emphasis, blockquotes, fenced code, or Markdown links.",
|
|
138
138
|
"When a URL must be visible, clickable, or openable by the recipient, include the complete bare `https://...` URL as visible text. Do not rely solely on `[label](URL)`, because Feishu client rendering is unreliable. A label may also be included, but the bare URL must remain present.",
|
|
139
|
-
"Use native `--text`
|
|
139
|
+
"Use native `--text` for ordinary plain text and for verbatim preservation, such as logs, literal code, or exact whitespace. Both `--markdown` and `--text` remain supported in the Larkin Runtime.",
|
|
140
140
|
"Never rewrite or normalize an exact or verbatim user-supplied body to expose a URL; the existing exact-content paths remain authoritative and preserve the supplied body unchanged.",
|
|
141
141
|
"For exact text supplied directly in the current instruction or Inbox event, pass the body unchanged as one literal `--text` argument. A direct literal must not use command substitution, backticks, `eval`, `echo`, or an unquoted variable; if it cannot be represented safely, stop and report the limitation instead of normalizing it.",
|
|
142
|
-
"An explicit exact or verbatim direct literal uses `--text` and overrides the
|
|
142
|
+
"An explicit exact or verbatim direct literal uses `--text` and overrides the ordinary-body guidance.",
|
|
143
143
|
`For a common exact send with a confirmed chat id, use the complete schematic recipe \`${executable} im +messages-send --chat-id <confirmed_chat_id> --text '<exact_body_as_one_literal_argument>' --json\`; replace each placeholder with the corresponding confirmed or exact literal value.`,
|
|
144
144
|
`A reply's target is governed by the source's thread membership, a structural Inbox fact, not a guess. When the current Inbox event or poll identifies the source as a thread (\`thread:<chat_id>:<thread_id>\` target, or the polled message carries a \`thread_id\`), your reply MUST stay in that same thread: after the required poll use \`${executable} im +messages-reply --message-id <real_om_message_id> --text '<exact_body_as_one_literal_argument>' --reply-in-thread --json\`. For a chat-level source (no thread) with no explicit in-thread request, reply to the main timeline with the same recipe but omit \`--reply-in-thread\`; replace the placeholders only with the real \`om_\` id returned by that poll and the unchanged exact literal.`,
|
|
145
145
|
`Use the \`--reply-in-thread\` recipe only when the source is a thread or the user or current Inbox event explicitly asks for a topic, in-thread, or thread reply. Never invent a topic request from ordinary reply wording or a bare source message id: the thread decision comes only from the source message's actual thread membership (\`thread:\` target or polled \`thread_id\`) or an explicit request, never from wording alone.`,
|
|
@@ -159,6 +159,7 @@ class ContextPromptBuilder {
|
|
|
159
159
|
"For the known canonical Inbox poll, scoped chat/thread history, and exact send/reply recipes specified here, execute them directly; you must not read or re-read a skill file or reference merely to rediscover or confirm their command syntax.",
|
|
160
160
|
`For a task that starts from an exact group name and asks for user and bot counts, after the required Inbox poll use this known canonical two-read recipe directly: first run \`${executable} im +chat-search --query '<exact_group_name>' --json\`, require one result with an exact name match and confirm its \`oc_\` chat id, then run \`${executable} im chats get --chat-id <confirmed_oc_chat_id> --json\` and answer from its \`user_count\` and \`bot_count\`. This group user/bot counts recipe uses exactly two post-poll business read calls; you must not read or re-read a skill or reference, open help or schema, invoke bare \`lark-cli\`, call \`chat.members\` or \`+chat-members-list\`, fall back to \`+chat-list\`, or add shell filters or output truncation. If the exact name is not a unique visible match, the chat id is not confirmed, or either count is missing, fail visibly without another discovery path or a guess.`,
|
|
161
161
|
"This narrow direct-recipe rule does not waive required skill safety gates, authorization, identity, freshness, or other safety checks. Unknown commands or high-risk operations still require the applicable skill/reference guidance or help.",
|
|
162
|
+
`To recall a message after the required explicit user approval, use \`${executable} im messages delete --message-id <bot-owned_om_id> --yes --json\`. Runtime verifies the original sender is this exact Bot, derives the exact chat/thread target, probes freshness before provider commit, and refuses cross-Agent or third-party messages. A \`committed=true\` or \`duplicate=true\` result must not be retried; an ambiguous result fails closed and requires user inspection. Never use generic API or bare \`lark-cli\` to bypass recall protection.`,
|
|
162
163
|
"The Larkin wrapper derives the stable per-intent idempotency key; do not pass `--idempotency-key` in an ordinary send or reply recipe. A successful write result carrying `duplicate: true` means the provider returned the earlier delivery for the same derived key: the message was already delivered and no new message was created. Do not resend the same command after a `duplicate: true` result; treat the message as delivered. To deliberately send identical content again as a fresh intent (e.g., a new reminder), pass an explicit new `--idempotency-key`, which the wrapper respects instead of deriving its own.",
|
|
163
164
|
"Both `freshness_unavailable` and `freshness_conflict` are pre-commit, provider-not-reached results. If a retry is warranted for an unchanged exact operation, rerun the identical safe ordinary command: `--text` for a direct literal or the same deterministic `--content` dataflow for a tool source. The wrapper reuses its derived key. If the target or body changes after reconsideration, run the revised ordinary command and let the wrapper derive a new key. A result with `committed=true` must not be repeated; ambiguous termination follows the existing wrapper same-key recovery contract.",
|
|
164
165
|
"For multiline `--markdown` or `--text` content in zsh or bash, prefer shell ANSI-C quoting so the command passes one argument with real newline characters: `--markdown $'First line\\nSecond line'` or `--text $'First line\\nSecond line'`. Putting `\"First line\\nSecond line\"` in ordinary double quotes is wrong: ordinary quotes do not decode `\\n`, so lark-cli and Feishu receive a backslash followed by the letter `n`. A literal multiline argument containing real newline characters is also valid. If ANSI-C-quoted content contains an apostrophe, use a safe shell single-quote splice or the literal-newline form; never use `eval`, `echo`, a temporary file, or unsafe variable interpolation to construct the body.",
|
|
@@ -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-v27";
|
|
17
17
|
/**
|
|
18
18
|
* Agent 间协作唤醒引导(issue #75):纯文本 @ 不会产生飞书 mention 事件,
|
|
19
19
|
* 必须由模型直接构造真实的 mention 元素;`--mention` 扩展参数已移除。
|
|
@@ -26,7 +26,7 @@ export const AGENT_MENTION_GUIDANCE = [
|
|
|
26
26
|
"Only mention another Agent when it genuinely needs to act again; keep the existing loop-prevention boundary.",
|
|
27
27
|
];
|
|
28
28
|
const FEISHU_IM_COMMAND_GROUPS = [
|
|
29
|
-
["Messages", ["im +messages-send", "im +messages-reply", "im +chat-messages-list", "im +threads-messages-list", "im +messages-mget"]],
|
|
29
|
+
["Messages", ["im +messages-send", "im +messages-reply", "im messages delete", "im +chat-messages-list", "im +threads-messages-list", "im +messages-mget"]],
|
|
30
30
|
["Chats", ["im +chat-list", "im +chat-search", "im chats get"]],
|
|
31
31
|
];
|
|
32
32
|
function clean(value) {
|
|
@@ -130,13 +130,13 @@ export class ContextPromptBuilder {
|
|
|
130
130
|
"Successful history response messages are always at `data.messages`. Never use a chat-wide fallback for a thread target, never merge stderr with `2>&1` before parsing JSON, and never truncate structured output before parsing it.",
|
|
131
131
|
"If the scoped history read fails or its schema is invalid, fail visibly. Do not reuse remembered or hard-coded text to make the task appear successful.",
|
|
132
132
|
"Only a real Feishu `message_id` beginning with `om_` may be passed to `+messages-reply`; `rem_`, `redeliver_`, and every other synthetic ID must never be replied to. Send with a confirmed `chat_id` and never guess a chat id from a display name.",
|
|
133
|
-
"Before every send/reply/card write, Larkin probes the exact chat or thread history with the current Bot identity. A nonzero `freshness_conflict` includes bounded unseen context and direct-acks that cursor; reconsider it, then retry the ordinary command. Larkin never saves the blocked
|
|
134
|
-
"For
|
|
133
|
+
"Before every send/reply/card/recall write, Larkin probes the exact chat or thread history with the current Bot identity. A nonzero `freshness_conflict` includes bounded unseen context and direct-acks that cursor; reconsider it, then retry the ordinary command. Larkin never saves the blocked operation as a draft.",
|
|
134
|
+
"For ordinary plain-text message bodies, use `--text`, including brief one-line replies, multiline status lines, and paragraphs. Use `--markdown` only when you intentionally need Markdown rendering, such as headings, lists, emphasis, blockquotes, fenced code, or Markdown links.",
|
|
135
135
|
"When a URL must be visible, clickable, or openable by the recipient, include the complete bare `https://...` URL as visible text. Do not rely solely on `[label](URL)`, because Feishu client rendering is unreliable. A label may also be included, but the bare URL must remain present.",
|
|
136
|
-
"Use native `--text`
|
|
136
|
+
"Use native `--text` for ordinary plain text and for verbatim preservation, such as logs, literal code, or exact whitespace. Both `--markdown` and `--text` remain supported in the Larkin Runtime.",
|
|
137
137
|
"Never rewrite or normalize an exact or verbatim user-supplied body to expose a URL; the existing exact-content paths remain authoritative and preserve the supplied body unchanged.",
|
|
138
138
|
"For exact text supplied directly in the current instruction or Inbox event, pass the body unchanged as one literal `--text` argument. A direct literal must not use command substitution, backticks, `eval`, `echo`, or an unquoted variable; if it cannot be represented safely, stop and report the limitation instead of normalizing it.",
|
|
139
|
-
"An explicit exact or verbatim direct literal uses `--text` and overrides the
|
|
139
|
+
"An explicit exact or verbatim direct literal uses `--text` and overrides the ordinary-body guidance.",
|
|
140
140
|
`For a common exact send with a confirmed chat id, use the complete schematic recipe \`${executable} im +messages-send --chat-id <confirmed_chat_id> --text '<exact_body_as_one_literal_argument>' --json\`; replace each placeholder with the corresponding confirmed or exact literal value.`,
|
|
141
141
|
`A reply's target is governed by the source's thread membership, a structural Inbox fact, not a guess. When the current Inbox event or poll identifies the source as a thread (\`thread:<chat_id>:<thread_id>\` target, or the polled message carries a \`thread_id\`), your reply MUST stay in that same thread: after the required poll use \`${executable} im +messages-reply --message-id <real_om_message_id> --text '<exact_body_as_one_literal_argument>' --reply-in-thread --json\`. For a chat-level source (no thread) with no explicit in-thread request, reply to the main timeline with the same recipe but omit \`--reply-in-thread\`; replace the placeholders only with the real \`om_\` id returned by that poll and the unchanged exact literal.`,
|
|
142
142
|
`Use the \`--reply-in-thread\` recipe only when the source is a thread or the user or current Inbox event explicitly asks for a topic, in-thread, or thread reply. Never invent a topic request from ordinary reply wording or a bare source message id: the thread decision comes only from the source message's actual thread membership (\`thread:\` target or polled \`thread_id\`) or an explicit request, never from wording alone.`,
|
|
@@ -156,6 +156,7 @@ export class ContextPromptBuilder {
|
|
|
156
156
|
"For the known canonical Inbox poll, scoped chat/thread history, and exact send/reply recipes specified here, execute them directly; you must not read or re-read a skill file or reference merely to rediscover or confirm their command syntax.",
|
|
157
157
|
`For a task that starts from an exact group name and asks for user and bot counts, after the required Inbox poll use this known canonical two-read recipe directly: first run \`${executable} im +chat-search --query '<exact_group_name>' --json\`, require one result with an exact name match and confirm its \`oc_\` chat id, then run \`${executable} im chats get --chat-id <confirmed_oc_chat_id> --json\` and answer from its \`user_count\` and \`bot_count\`. This group user/bot counts recipe uses exactly two post-poll business read calls; you must not read or re-read a skill or reference, open help or schema, invoke bare \`lark-cli\`, call \`chat.members\` or \`+chat-members-list\`, fall back to \`+chat-list\`, or add shell filters or output truncation. If the exact name is not a unique visible match, the chat id is not confirmed, or either count is missing, fail visibly without another discovery path or a guess.`,
|
|
158
158
|
"This narrow direct-recipe rule does not waive required skill safety gates, authorization, identity, freshness, or other safety checks. Unknown commands or high-risk operations still require the applicable skill/reference guidance or help.",
|
|
159
|
+
`To recall a message after the required explicit user approval, use \`${executable} im messages delete --message-id <bot-owned_om_id> --yes --json\`. Runtime verifies the original sender is this exact Bot, derives the exact chat/thread target, probes freshness before provider commit, and refuses cross-Agent or third-party messages. A \`committed=true\` or \`duplicate=true\` result must not be retried; an ambiguous result fails closed and requires user inspection. Never use generic API or bare \`lark-cli\` to bypass recall protection.`,
|
|
159
160
|
"The Larkin wrapper derives the stable per-intent idempotency key; do not pass `--idempotency-key` in an ordinary send or reply recipe. A successful write result carrying `duplicate: true` means the provider returned the earlier delivery for the same derived key: the message was already delivered and no new message was created. Do not resend the same command after a `duplicate: true` result; treat the message as delivered. To deliberately send identical content again as a fresh intent (e.g., a new reminder), pass an explicit new `--idempotency-key`, which the wrapper respects instead of deriving its own.",
|
|
160
161
|
"Both `freshness_unavailable` and `freshness_conflict` are pre-commit, provider-not-reached results. If a retry is warranted for an unchanged exact operation, rerun the identical safe ordinary command: `--text` for a direct literal or the same deterministic `--content` dataflow for a tool source. The wrapper reuses its derived key. If the target or body changes after reconsideration, run the revised ordinary command and let the wrapper derive a new key. A result with `committed=true` must not be repeated; ambiguous termination follows the existing wrapper same-key recovery contract.",
|
|
161
162
|
"For multiline `--markdown` or `--text` content in zsh or bash, prefer shell ANSI-C quoting so the command passes one argument with real newline characters: `--markdown $'First line\\nSecond line'` or `--text $'First line\\nSecond line'`. Putting `\"First line\\nSecond line\"` in ordinary double quotes is wrong: ordinary quotes do not decode `\\n`, so lark-cli and Feishu receive a backslash followed by the letter `n`. A literal multiline argument containing real newline characters is also valid. If ANSI-C-quoted content contains an apostrophe, use a safe shell single-quote splice or the literal-newline form; never use `eval`, `echo`, a temporary file, or unsafe variable interpolation to construct the body.",
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InboxAuditHeartbeat = exports.INBOX_AUDIT_CADENCE_MS = void 0;
|
|
4
|
+
const inbox_projection_js_1 = require("./inbox-projection.cjs");
|
|
5
|
+
exports.INBOX_AUDIT_CADENCE_MS = 15 * 60_000;
|
|
6
|
+
/** One Host-owned timer fans a targetless internal wake to each active Agent. */
|
|
7
|
+
class InboxAuditHeartbeat {
|
|
8
|
+
options;
|
|
9
|
+
timer = null;
|
|
10
|
+
running = false;
|
|
11
|
+
sequence = 0;
|
|
12
|
+
cadenceMs;
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.options = options;
|
|
15
|
+
this.cadenceMs = options.cadenceMs ?? exports.INBOX_AUDIT_CADENCE_MS;
|
|
16
|
+
if (!Number.isSafeInteger(this.cadenceMs) || this.cadenceMs < 1)
|
|
17
|
+
throw new Error("inbox audit cadence must be a positive integer");
|
|
18
|
+
}
|
|
19
|
+
start() {
|
|
20
|
+
if (this.running)
|
|
21
|
+
return;
|
|
22
|
+
this.running = true;
|
|
23
|
+
this.schedule();
|
|
24
|
+
}
|
|
25
|
+
stop() {
|
|
26
|
+
this.running = false;
|
|
27
|
+
if (this.timer)
|
|
28
|
+
(this.options.clearTimer ?? clearTimeout)(this.timer);
|
|
29
|
+
this.timer = null;
|
|
30
|
+
}
|
|
31
|
+
schedule() {
|
|
32
|
+
if (!this.running || this.timer)
|
|
33
|
+
return;
|
|
34
|
+
this.timer = (this.options.setTimer ?? setTimeout)(() => {
|
|
35
|
+
this.timer = null;
|
|
36
|
+
return this.fire().finally(() => { if (this.running)
|
|
37
|
+
this.schedule(); });
|
|
38
|
+
}, this.cadenceMs);
|
|
39
|
+
this.timer.unref?.();
|
|
40
|
+
}
|
|
41
|
+
async fire() {
|
|
42
|
+
const now = this.options.now ?? Date.now;
|
|
43
|
+
for (const agent of this.options.agents) {
|
|
44
|
+
const message_id = `rem_inbox_audit_${now()}_${++this.sequence}_${agent.agentId}`;
|
|
45
|
+
const envelope = {
|
|
46
|
+
kind: "reminder",
|
|
47
|
+
message_id,
|
|
48
|
+
target: inbox_projection_js_1.RUNTIME_REMINDER_TARGET,
|
|
49
|
+
wake: true,
|
|
50
|
+
content: "Internal inbox audit wake. Poll runtime:reminder, then run larkin inbox audit --json. No finding stays silent.",
|
|
51
|
+
};
|
|
52
|
+
try {
|
|
53
|
+
const appended = this.options.stateStore(agent).appendCanonicalInboxOnce(envelope);
|
|
54
|
+
if (appended.status === "appended")
|
|
55
|
+
await this.options.runtimeHost.deliver(agent.agentId, appended.envelope);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
this.options.log?.(`inbox audit heartbeat failed agent=${agent.agentId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.InboxAuditHeartbeat = InboxAuditHeartbeat;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { RUNTIME_REMINDER_TARGET } from "./inbox-projection.mjs";
|
|
2
|
+
export const INBOX_AUDIT_CADENCE_MS = 15 * 60_000;
|
|
3
|
+
/** One Host-owned timer fans a targetless internal wake to each active Agent. */
|
|
4
|
+
export class InboxAuditHeartbeat {
|
|
5
|
+
options;
|
|
6
|
+
timer = null;
|
|
7
|
+
running = false;
|
|
8
|
+
sequence = 0;
|
|
9
|
+
cadenceMs;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.options = options;
|
|
12
|
+
this.cadenceMs = options.cadenceMs ?? INBOX_AUDIT_CADENCE_MS;
|
|
13
|
+
if (!Number.isSafeInteger(this.cadenceMs) || this.cadenceMs < 1)
|
|
14
|
+
throw new Error("inbox audit cadence must be a positive integer");
|
|
15
|
+
}
|
|
16
|
+
start() {
|
|
17
|
+
if (this.running)
|
|
18
|
+
return;
|
|
19
|
+
this.running = true;
|
|
20
|
+
this.schedule();
|
|
21
|
+
}
|
|
22
|
+
stop() {
|
|
23
|
+
this.running = false;
|
|
24
|
+
if (this.timer)
|
|
25
|
+
(this.options.clearTimer ?? clearTimeout)(this.timer);
|
|
26
|
+
this.timer = null;
|
|
27
|
+
}
|
|
28
|
+
schedule() {
|
|
29
|
+
if (!this.running || this.timer)
|
|
30
|
+
return;
|
|
31
|
+
this.timer = (this.options.setTimer ?? setTimeout)(() => {
|
|
32
|
+
this.timer = null;
|
|
33
|
+
return this.fire().finally(() => { if (this.running)
|
|
34
|
+
this.schedule(); });
|
|
35
|
+
}, this.cadenceMs);
|
|
36
|
+
this.timer.unref?.();
|
|
37
|
+
}
|
|
38
|
+
async fire() {
|
|
39
|
+
const now = this.options.now ?? Date.now;
|
|
40
|
+
for (const agent of this.options.agents) {
|
|
41
|
+
const message_id = `rem_inbox_audit_${now()}_${++this.sequence}_${agent.agentId}`;
|
|
42
|
+
const envelope = {
|
|
43
|
+
kind: "reminder",
|
|
44
|
+
message_id,
|
|
45
|
+
target: RUNTIME_REMINDER_TARGET,
|
|
46
|
+
wake: true,
|
|
47
|
+
content: "Internal inbox audit wake. Poll runtime:reminder, then run larkin inbox audit --json. No finding stays silent.",
|
|
48
|
+
};
|
|
49
|
+
try {
|
|
50
|
+
const appended = this.options.stateStore(agent).appendCanonicalInboxOnce(envelope);
|
|
51
|
+
if (appended.status === "appended")
|
|
52
|
+
await this.options.runtimeHost.deliver(agent.agentId, appended.envelope);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
this.options.log?.(`inbox audit heartbeat failed agent=${agent.agentId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -1,123 +1,133 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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,不得从标题推断收件人");
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
29
7
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
38
24
|
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.MAX_INBOX_AUDIT_TARGETS = exports.INBOX_AUDIT_LEGACY_MIGRATION_NON_GOAL = void 0;
|
|
37
|
+
exports.inboxAuditRegistryFile = inboxAuditRegistryFile;
|
|
38
|
+
exports.observeInboxAuditTarget = observeInboxAuditTarget;
|
|
39
|
+
exports.readInboxAuditTargets = readInboxAuditTargets;
|
|
40
|
+
const fs = __importStar(require("node:fs"));
|
|
41
|
+
const path = __importStar(require("node:path"));
|
|
42
|
+
/**
|
|
43
|
+
* #169's host-owned heartbeat no longer creates per-target missed-outbound loops.
|
|
44
|
+
* Pre-v0.4.21 records have no unique ownership marker and can collide exactly
|
|
45
|
+
* with user reminders, so existing indistinguishable loops are never migrated
|
|
46
|
+
* or deleted automatically.
|
|
47
|
+
*/
|
|
48
|
+
exports.INBOX_AUDIT_LEGACY_MIGRATION_NON_GOAL = "New versions no longer create missed-outbound loops; existing indistinguishable historical loops are not migrated or deleted automatically.";
|
|
49
|
+
// Audits return every retained target. Storage remains bounded so a heartbeat
|
|
50
|
+
// cannot accumulate an unbounded work list.
|
|
51
|
+
exports.MAX_INBOX_AUDIT_TARGETS = 96;
|
|
52
|
+
const MAX_STORED_TARGETS = exports.MAX_INBOX_AUDIT_TARGETS;
|
|
53
|
+
const CHAT = /^oc_[A-Za-z0-9]+$/;
|
|
54
|
+
const THREAD = /^omt_[A-Za-z0-9]+$/;
|
|
55
|
+
const ANCHOR = /^om_[A-Za-z0-9_-]+$/;
|
|
56
|
+
function inboxAuditRegistryFile(larkinHome) {
|
|
57
|
+
return path.join(larkinHome, "inbox-audit.json");
|
|
58
|
+
}
|
|
59
|
+
function parseTarget(event) {
|
|
60
|
+
const chatId = String(event.chat_id || "");
|
|
61
|
+
const threadId = event.thread_id ? String(event.thread_id) : "";
|
|
62
|
+
const anchor = String(event.message_id || "");
|
|
63
|
+
if (!CHAT.test(chatId) || !ANCHOR.test(anchor) || (threadId && !THREAD.test(threadId)))
|
|
64
|
+
return null;
|
|
65
|
+
return { target: threadId ? `thread:${chatId}:${threadId}` : `chat:${chatId}`, anchor };
|
|
66
|
+
}
|
|
67
|
+
function load(file) {
|
|
68
|
+
let value;
|
|
69
|
+
try {
|
|
70
|
+
value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
39
71
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
scope: "thread",
|
|
45
|
-
chatId: thread[1],
|
|
46
|
-
threadId: thread[2],
|
|
47
|
-
deliveryAnchor: parseOmMessageId(anchor, "delivery anchor"),
|
|
48
|
-
};
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (error.code === "ENOENT")
|
|
74
|
+
return { version: 1, targets: [] };
|
|
75
|
+
throw error;
|
|
49
76
|
}
|
|
50
|
-
|
|
77
|
+
if (value?.version !== 1 || !Array.isArray(value.targets))
|
|
78
|
+
return { version: 1, targets: [] };
|
|
79
|
+
return { version: 1, targets: value.targets.flatMap((row) => {
|
|
80
|
+
if (!row || typeof row !== "object")
|
|
81
|
+
return [];
|
|
82
|
+
const candidate = row;
|
|
83
|
+
if (typeof candidate.target !== "string" || typeof candidate.anchor !== "string")
|
|
84
|
+
return [];
|
|
85
|
+
const parsed = parseTarget({
|
|
86
|
+
chat_id: candidate.target.startsWith("chat:") ? candidate.target.slice(5) : candidate.target.split(":")[1],
|
|
87
|
+
thread_id: candidate.target.startsWith("thread:") ? candidate.target.split(":")[2] : null,
|
|
88
|
+
message_id: candidate.anchor,
|
|
89
|
+
});
|
|
90
|
+
return typeof candidate.agent_id === "string" && candidate.agent_id && parsed && candidate.target === parsed.target
|
|
91
|
+
&& typeof candidate.observed_at === "string" && Number.isFinite(Date.parse(candidate.observed_at))
|
|
92
|
+
? [{ agent_id: candidate.agent_id, ...parsed, observed_at: candidate.observed_at }] : [];
|
|
93
|
+
}) };
|
|
51
94
|
}
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
95
|
+
function save(file, registry) {
|
|
96
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
97
|
+
const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${Date.now()}.tmp`);
|
|
98
|
+
fs.writeFileSync(temporary, `${JSON.stringify(registry)}\n`, { mode: 0o600 });
|
|
99
|
+
fs.renameSync(temporary, file);
|
|
100
|
+
fs.chmodSync(file, 0o600);
|
|
57
101
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
});
|
|
102
|
+
/** Record only an authoritative human group/topic source; DMs and bots are ignored. */
|
|
103
|
+
function observeInboxAuditTarget(file, agentId, event, now = new Date()) {
|
|
104
|
+
if (event._scan_authority !== true || event._sender_is_bot !== false || event.chat_type !== "group")
|
|
105
|
+
return false;
|
|
106
|
+
const parsed = parseTarget(event);
|
|
107
|
+
if (!parsed)
|
|
108
|
+
return false;
|
|
109
|
+
const registry = load(file);
|
|
110
|
+
const observed_at = now.toISOString();
|
|
111
|
+
registry.targets = registry.targets.filter((row) => row.agent_id !== agentId || row.target !== parsed.target);
|
|
112
|
+
registry.targets.unshift({ agent_id: agentId, ...parsed, observed_at });
|
|
113
|
+
registry.targets = registry.targets.slice(0, MAX_STORED_TARGETS);
|
|
114
|
+
save(file, registry);
|
|
115
|
+
return true;
|
|
100
116
|
}
|
|
101
|
-
function
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
:
|
|
116
|
-
|
|
117
|
-
storeFile,
|
|
118
|
-
agentId,
|
|
119
|
-
deliveryTarget: parsed.deliveryTarget,
|
|
120
|
-
deliveryAnchor: parsed.deliveryAnchor,
|
|
121
|
-
});
|
|
122
|
-
return parsed;
|
|
117
|
+
function instruction(target) {
|
|
118
|
+
const history = target.target.startsWith("thread:")
|
|
119
|
+
? "Use larkin im +threads-messages-list for this exact thread."
|
|
120
|
+
: "Use larkin im +chat-messages-list for this exact chat.";
|
|
121
|
+
return `${history} If a real missed outbound finding requires a response, use the existing guarded larkin im reply path anchored at ${target.anchor}; otherwise stay silent.`;
|
|
122
|
+
}
|
|
123
|
+
/** Content-free, bounded audit work list consumed by the Agent CLI. */
|
|
124
|
+
function readInboxAuditTargets(file, agentId) {
|
|
125
|
+
const rows = load(file).targets.filter((row) => row.agent_id === agentId)
|
|
126
|
+
.sort((left, right) => right.observed_at.localeCompare(left.observed_at));
|
|
127
|
+
return {
|
|
128
|
+
version: 1,
|
|
129
|
+
targets: rows.slice(0, exports.MAX_INBOX_AUDIT_TARGETS).map(({ agent_id: _agentId, ...target }) => ({ ...target, instruction: instruction(target) })),
|
|
130
|
+
has_more: rows.length > exports.MAX_INBOX_AUDIT_TARGETS,
|
|
131
|
+
no_finding: "stay_silent",
|
|
132
|
+
};
|
|
123
133
|
}
|
|
@@ -1,112 +1,94 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* #169's host-owned heartbeat no longer creates per-target missed-outbound loops.
|
|
5
|
+
* Pre-v0.4.21 records have no unique ownership marker and can collide exactly
|
|
6
|
+
* with user reminders, so existing indistinguishable loops are never migrated
|
|
7
|
+
* or deleted automatically.
|
|
8
|
+
*/
|
|
9
|
+
export const INBOX_AUDIT_LEGACY_MIGRATION_NON_GOAL = "New versions no longer create missed-outbound loops; existing indistinguishable historical loops are not migrated or deleted automatically.";
|
|
10
|
+
// Audits return every retained target. Storage remains bounded so a heartbeat
|
|
11
|
+
// cannot accumulate an unbounded work list.
|
|
12
|
+
export const MAX_INBOX_AUDIT_TARGETS = 96;
|
|
13
|
+
const MAX_STORED_TARGETS = MAX_INBOX_AUDIT_TARGETS;
|
|
14
|
+
const CHAT = /^oc_[A-Za-z0-9]+$/;
|
|
15
|
+
const THREAD = /^omt_[A-Za-z0-9]+$/;
|
|
16
|
+
const ANCHOR = /^om_[A-Za-z0-9_-]+$/;
|
|
17
|
+
export function inboxAuditRegistryFile(larkinHome) {
|
|
18
|
+
return path.join(larkinHome, "inbox-audit.json");
|
|
13
19
|
}
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
deliveryAnchor: anchor ? parseOmMessageId(anchor, "delivery anchor") : null,
|
|
27
|
-
};
|
|
20
|
+
function parseTarget(event) {
|
|
21
|
+
const chatId = String(event.chat_id || "");
|
|
22
|
+
const threadId = event.thread_id ? String(event.thread_id) : "";
|
|
23
|
+
const anchor = String(event.message_id || "");
|
|
24
|
+
if (!CHAT.test(chatId) || !ANCHOR.test(anchor) || (threadId && !THREAD.test(threadId)))
|
|
25
|
+
return null;
|
|
26
|
+
return { target: threadId ? `thread:${chatId}:${threadId}` : `chat:${chatId}`, anchor };
|
|
27
|
+
}
|
|
28
|
+
function load(file) {
|
|
29
|
+
let value;
|
|
30
|
+
try {
|
|
31
|
+
value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
28
32
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
scope: "thread",
|
|
34
|
-
chatId: thread[1],
|
|
35
|
-
threadId: thread[2],
|
|
36
|
-
deliveryAnchor: parseOmMessageId(anchor, "delivery anchor"),
|
|
37
|
-
};
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error.code === "ENOENT")
|
|
35
|
+
return { version: 1, targets: [] };
|
|
36
|
+
throw error;
|
|
38
37
|
}
|
|
39
|
-
|
|
38
|
+
if (value?.version !== 1 || !Array.isArray(value.targets))
|
|
39
|
+
return { version: 1, targets: [] };
|
|
40
|
+
return { version: 1, targets: value.targets.flatMap((row) => {
|
|
41
|
+
if (!row || typeof row !== "object")
|
|
42
|
+
return [];
|
|
43
|
+
const candidate = row;
|
|
44
|
+
if (typeof candidate.target !== "string" || typeof candidate.anchor !== "string")
|
|
45
|
+
return [];
|
|
46
|
+
const parsed = parseTarget({
|
|
47
|
+
chat_id: candidate.target.startsWith("chat:") ? candidate.target.slice(5) : candidate.target.split(":")[1],
|
|
48
|
+
thread_id: candidate.target.startsWith("thread:") ? candidate.target.split(":")[2] : null,
|
|
49
|
+
message_id: candidate.anchor,
|
|
50
|
+
});
|
|
51
|
+
return typeof candidate.agent_id === "string" && candidate.agent_id && parsed && candidate.target === parsed.target
|
|
52
|
+
&& typeof candidate.observed_at === "string" && Number.isFinite(Date.parse(candidate.observed_at))
|
|
53
|
+
? [{ agent_id: candidate.agent_id, ...parsed, observed_at: candidate.observed_at }] : [];
|
|
54
|
+
}) };
|
|
40
55
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
56
|
+
function save(file, registry) {
|
|
57
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
58
|
+
const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${Date.now()}.tmp`);
|
|
59
|
+
fs.writeFileSync(temporary, `${JSON.stringify(registry)}\n`, { mode: 0o600 });
|
|
60
|
+
fs.renameSync(temporary, file);
|
|
61
|
+
fs.chmodSync(file, 0o600);
|
|
46
62
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
});
|
|
63
|
+
/** Record only an authoritative human group/topic source; DMs and bots are ignored. */
|
|
64
|
+
export function observeInboxAuditTarget(file, agentId, event, now = new Date()) {
|
|
65
|
+
if (event._scan_authority !== true || event._sender_is_bot !== false || event.chat_type !== "group")
|
|
66
|
+
return false;
|
|
67
|
+
const parsed = parseTarget(event);
|
|
68
|
+
if (!parsed)
|
|
69
|
+
return false;
|
|
70
|
+
const registry = load(file);
|
|
71
|
+
const observed_at = now.toISOString();
|
|
72
|
+
registry.targets = registry.targets.filter((row) => row.agent_id !== agentId || row.target !== parsed.target);
|
|
73
|
+
registry.targets.unshift({ agent_id: agentId, ...parsed, observed_at });
|
|
74
|
+
registry.targets = registry.targets.slice(0, MAX_STORED_TARGETS);
|
|
75
|
+
save(file, registry);
|
|
76
|
+
return true;
|
|
89
77
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
:
|
|
105
|
-
|
|
106
|
-
storeFile,
|
|
107
|
-
agentId,
|
|
108
|
-
deliveryTarget: parsed.deliveryTarget,
|
|
109
|
-
deliveryAnchor: parsed.deliveryAnchor,
|
|
110
|
-
});
|
|
111
|
-
return parsed;
|
|
78
|
+
function instruction(target) {
|
|
79
|
+
const history = target.target.startsWith("thread:")
|
|
80
|
+
? "Use larkin im +threads-messages-list for this exact thread."
|
|
81
|
+
: "Use larkin im +chat-messages-list for this exact chat.";
|
|
82
|
+
return `${history} If a real missed outbound finding requires a response, use the existing guarded larkin im reply path anchored at ${target.anchor}; otherwise stay silent.`;
|
|
83
|
+
}
|
|
84
|
+
/** Content-free, bounded audit work list consumed by the Agent CLI. */
|
|
85
|
+
export function readInboxAuditTargets(file, agentId) {
|
|
86
|
+
const rows = load(file).targets.filter((row) => row.agent_id === agentId)
|
|
87
|
+
.sort((left, right) => right.observed_at.localeCompare(left.observed_at));
|
|
88
|
+
return {
|
|
89
|
+
version: 1,
|
|
90
|
+
targets: rows.slice(0, MAX_INBOX_AUDIT_TARGETS).map(({ agent_id: _agentId, ...target }) => ({ ...target, instruction: instruction(target) })),
|
|
91
|
+
has_more: rows.length > MAX_INBOX_AUDIT_TARGETS,
|
|
92
|
+
no_finding: "stay_silent",
|
|
93
|
+
};
|
|
112
94
|
}
|
package/dist/app/agent-cli.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import * as path from "node:path";
|
|
|
7
7
|
import { createAgentStateStore } from "../agent/agent-state-store.mjs";
|
|
8
8
|
import * as larkinConfig from "../platform/config.mjs";
|
|
9
9
|
import { projectInboxCheck, projectInboxEvents } from "../agent/inbox-projection.mjs";
|
|
10
|
+
import { inboxAuditRegistryFile, readInboxAuditTargets } from "../agent/missed-outbound-scan.mjs";
|
|
10
11
|
import { createReminderRoutes } from "../agent/reminder-routes.mjs";
|
|
11
12
|
import { InteractionStateMachine } from "../agent/interaction-state-machine.mjs";
|
|
12
13
|
import { issueCallbackProbe, readCallbackCapability } from "../platform/callback-capability.mjs";
|
|
@@ -501,10 +502,17 @@ export function runAgentCli(argv, env = process.env, dependencies = {}) {
|
|
|
501
502
|
const { config, agent } = requireAgent(env);
|
|
502
503
|
const stateStore = dependencies.stateStore ?? createAgentStateStore(config.larkinHome, agent.agentId);
|
|
503
504
|
if (group === "inbox") {
|
|
504
|
-
if (!["check", "poll"].includes(subcommand || "")) {
|
|
505
|
-
throw new Error("inbox 只支持 `larkin inbox check` 与 `larkin inbox
|
|
505
|
+
if (!["check", "poll", "audit"].includes(subcommand || "")) {
|
|
506
|
+
throw new Error("inbox 只支持 `larkin inbox check`、`larkin inbox poll` 与 `larkin inbox audit`");
|
|
506
507
|
}
|
|
507
508
|
const options = parseOptions(rest, new Set(["--json"]));
|
|
509
|
+
if (subcommand === "audit") {
|
|
510
|
+
if (options.positionals.length || options.values.size || options.booleans.size > 1) {
|
|
511
|
+
throw new Error("inbox audit 只接受 --json");
|
|
512
|
+
}
|
|
513
|
+
emitJson(io, readInboxAuditTargets(inboxAuditRegistryFile(config.larkinHome), agent.agentId));
|
|
514
|
+
return 0;
|
|
515
|
+
}
|
|
508
516
|
if (options.positionals.length || [...options.values.keys()].some((flag) => !["--target", "--limit"].includes(flag))) {
|
|
509
517
|
throw new Error(`inbox ${subcommand} 只接受 --target、${subcommand === "poll" ? "--limit、" : ""}--json`);
|
|
510
518
|
}
|
package/dist/app/lark-cli.mjs
CHANGED
|
@@ -209,6 +209,11 @@ export function classifyLarkCliCommand(argv) {
|
|
|
209
209
|
? (parsed.commandArgv.includes("--dry-run") || parsed.help ? { kind: "passthrough" } : { kind: "guarded", operation: "urgent-app" })
|
|
210
210
|
: noncanonicalProtectedDecision();
|
|
211
211
|
}
|
|
212
|
+
if (exactPath(parsed.commandArgv, ["im", "messages", "delete"])) {
|
|
213
|
+
return uniqueProtectedOperation(protectedPaths, "raw-delete")
|
|
214
|
+
? (parsed.commandArgv.includes("--dry-run") || parsed.help ? { kind: "passthrough" } : { kind: "guarded", operation: "recall" })
|
|
215
|
+
: noncanonicalProtectedDecision();
|
|
216
|
+
}
|
|
212
217
|
if (exactPath(parsed.commandArgv, ["im", "messages", "patch"]) || exactPath(parsed.commandArgv, ["im", "messages", "update"])) {
|
|
213
218
|
const expected = parsed.commandArgv[2] === "patch" ? "card-patch" : "card-update";
|
|
214
219
|
return uniqueProtectedOperation(protectedPaths, expected)
|
|
@@ -221,7 +226,7 @@ export function classifyLarkCliCommand(argv) {
|
|
|
221
226
|
? { kind: "denied", reason: "该原始 IM 写入口会旁路 target freshness;请使用 +messages-send/+messages-reply" }
|
|
222
227
|
: noncanonicalProtectedDecision();
|
|
223
228
|
}
|
|
224
|
-
if (["forward", "merge_forward", "
|
|
229
|
+
if (["forward", "merge_forward", "urgent_phone", "urgent_sms"]
|
|
225
230
|
.some((operation) => exactPath(parsed.commandArgv, ["im", "messages", operation]))) {
|
|
226
231
|
const expected = `raw-${parsed.commandArgv[2]}`;
|
|
227
232
|
return uniqueProtectedOperation(protectedPaths, expected)
|
|
@@ -492,6 +497,40 @@ function guardedTarget(decision, argv, store) {
|
|
|
492
497
|
throw new Error(`无法从 Inbox 状态确定 ${messageId} 的 target;先 poll 对应消息,禁止旁路 freshness`);
|
|
493
498
|
return feishuImTarget(target);
|
|
494
499
|
}
|
|
500
|
+
function recallLedgerEntry(store, messageId) {
|
|
501
|
+
const state = store.readJson("freshnessState", { version: 1, cursors: {} });
|
|
502
|
+
const entry = state.message_recalls?.[messageId];
|
|
503
|
+
return entry && typeof entry === "object" ? entry : null;
|
|
504
|
+
}
|
|
505
|
+
function claimRecall(store, messageId, target) {
|
|
506
|
+
return store.mutateJson("freshnessState", { version: 1, cursors: {} }, (state) => {
|
|
507
|
+
state.message_recalls ??= {};
|
|
508
|
+
const prior = state.message_recalls[messageId];
|
|
509
|
+
if (prior?.status === "deleted")
|
|
510
|
+
return "deleted";
|
|
511
|
+
if (prior?.status === "deleting")
|
|
512
|
+
return "ambiguous";
|
|
513
|
+
state.message_recalls[messageId] = { status: "deleting", target, updated_at: new Date().toISOString() };
|
|
514
|
+
return "ready";
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
function finalizeFailedRecall(store, messageId, target) {
|
|
518
|
+
store.mutateJson("freshnessState", { version: 1, cursors: {} }, (state) => {
|
|
519
|
+
state.message_recalls ??= {};
|
|
520
|
+
state.message_recalls[messageId] = { status: "failed", target, updated_at: new Date().toISOString() };
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
function finalizeSuccessfulRecall(store, messageId, target, cursor, generation) {
|
|
524
|
+
store.mutateJson("freshnessState", { version: 1, cursors: {} }, (state) => {
|
|
525
|
+
state.cursors ??= {};
|
|
526
|
+
if (cursor)
|
|
527
|
+
state.cursors[target] = { generation, cursor };
|
|
528
|
+
else
|
|
529
|
+
delete state.cursors[target];
|
|
530
|
+
state.message_recalls ??= {};
|
|
531
|
+
state.message_recalls[messageId] = { status: "deleted", target, updated_at: new Date().toISOString() };
|
|
532
|
+
});
|
|
533
|
+
}
|
|
495
534
|
function uniqueRawFlagValue(argv, flag) {
|
|
496
535
|
const nativeArgv = nativeArgvBeforeBoundary(argv);
|
|
497
536
|
const values = [];
|
|
@@ -509,6 +548,57 @@ function uniqueRawFlagValue(argv, flag) {
|
|
|
509
548
|
throw new Error(`Runtime im messages urgent_app 不允许重复 ${flag};官方 CLI 会采用最后一次赋值`);
|
|
510
549
|
return values[0] || null;
|
|
511
550
|
}
|
|
551
|
+
function assertRecallSyntax(argv) {
|
|
552
|
+
const messageId = policyFlagValue(argv, "--message-id");
|
|
553
|
+
if (!messageId || !/^om_[A-Za-z0-9_-]+$/.test(messageId)) {
|
|
554
|
+
throw new Error("Runtime im messages delete 只接受真实 Feishu om_ message_id");
|
|
555
|
+
}
|
|
556
|
+
const yesCount = nativeArgvBeforeBoundary(argv).filter((argument) => argument === "--yes").length;
|
|
557
|
+
if (yesCount !== 1)
|
|
558
|
+
throw new Error("Runtime im messages delete 必须且只能指定一次 --yes 显式确认");
|
|
559
|
+
if (policyFlagValue(argv, "--idempotency-key")) {
|
|
560
|
+
throw new Error("Runtime im messages delete 不接受 provider idempotency key;Runtime 使用本地 recall ledger 防止重复提交");
|
|
561
|
+
}
|
|
562
|
+
return messageId;
|
|
563
|
+
}
|
|
564
|
+
function resolveOwnedRecallTarget(messageId, feishuAppId, env, io, dependencies) {
|
|
565
|
+
const result = callNative([
|
|
566
|
+
"im", "+messages-mget", "--message-ids", messageId, "--no-reactions", "--json", "--as", "bot",
|
|
567
|
+
], env, io, dependencies);
|
|
568
|
+
if (result.error)
|
|
569
|
+
throw new Error(`recall message lookup failed: ${result.error.message}`);
|
|
570
|
+
if (result.status !== 0)
|
|
571
|
+
throw new Error(`recall message lookup exited ${result.status ?? "without status"}: ${result.stderr || "no details"}`);
|
|
572
|
+
let value;
|
|
573
|
+
try {
|
|
574
|
+
value = JSON.parse(result.stdout || "");
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
throw new Error("recall message lookup returned non-JSON output");
|
|
578
|
+
}
|
|
579
|
+
const root = value;
|
|
580
|
+
if (!root || root.ok !== true || !root.data)
|
|
581
|
+
throw new Error("recall message lookup returned an unsuccessful payload");
|
|
582
|
+
if (root.identity !== "bot")
|
|
583
|
+
throw new Error("recall message lookup did not confirm Bot identity");
|
|
584
|
+
if (!Array.isArray(root.data.messages))
|
|
585
|
+
throw new Error("recall message lookup omitted messages");
|
|
586
|
+
const matches = root.data.messages.filter((row) => row && typeof row === "object" && !Array.isArray(row)
|
|
587
|
+
&& row.message_id === messageId);
|
|
588
|
+
if (matches.length !== 1)
|
|
589
|
+
throw new Error(`无法唯一确认 ${messageId} 的原始消息;禁止旁路 recall ownership`);
|
|
590
|
+
const message = matches[0];
|
|
591
|
+
if (typeof message.chat_id !== "string" || !message.chat_id) {
|
|
592
|
+
throw new Error(`无法从官方 +messages-mget 确认 ${messageId} 的 chat;禁止旁路 recall freshness`);
|
|
593
|
+
}
|
|
594
|
+
if (!isOwnBotMessage(message, feishuAppId)) {
|
|
595
|
+
throw new Error("Runtime im messages delete 只能撤回当前 Runtime Bot 自己发出的消息;拒绝跨 Agent 或第三方消息");
|
|
596
|
+
}
|
|
597
|
+
const target = typeof message.thread_id === "string" && message.thread_id
|
|
598
|
+
? feishuImTarget(`thread:${message.chat_id}:${message.thread_id}`)
|
|
599
|
+
: feishuImTarget(`chat:${message.chat_id}`);
|
|
600
|
+
return { target, message };
|
|
601
|
+
}
|
|
512
602
|
function resolveUrgentAppTarget(argv, env, io, dependencies) {
|
|
513
603
|
const messageId = policyFlagValue(argv, "--message-id");
|
|
514
604
|
if (!messageId || !/^om_/.test(messageId)) {
|
|
@@ -555,10 +645,11 @@ function botArgv(argv, intentId, decision) {
|
|
|
555
645
|
const injected = [];
|
|
556
646
|
if (!parsed.flags.has("--as"))
|
|
557
647
|
injected.push("--as", "bot");
|
|
558
|
-
|
|
648
|
+
const supportsProviderIdempotency = decision?.operation !== "urgent-app" && decision?.operation !== "recall";
|
|
649
|
+
if (supportsProviderIdempotency && !parsed.flags.has("--idempotency-key"))
|
|
559
650
|
injected.push("--idempotency-key", intentId);
|
|
560
651
|
next.splice(insertion, 0, ...injected);
|
|
561
|
-
if (
|
|
652
|
+
if (supportsProviderIdempotency)
|
|
562
653
|
return next;
|
|
563
654
|
const rewritten = [];
|
|
564
655
|
for (let index = 0; index < next.length; index += 1) {
|
|
@@ -768,7 +859,7 @@ function emitFreshnessError(io, input) {
|
|
|
768
859
|
target: input.target,
|
|
769
860
|
...(input.current ? { current_cursor: input.current } : {}),
|
|
770
861
|
...(input.messages ? { unseen_messages: input.messages } : {}),
|
|
771
|
-
next: "Reconsider the returned context, then retry the ordinary send/reply/card command; history is probed again before every write.",
|
|
862
|
+
next: "Reconsider the returned context, then retry the ordinary send/reply/card/recall command; history is probed again before every write.",
|
|
772
863
|
})}\n`);
|
|
773
864
|
}
|
|
774
865
|
function freshnessGeneration(env) {
|
|
@@ -984,6 +1075,17 @@ function emitDuplicatedWrite(result, io, input) {
|
|
|
984
1075
|
})}\n`);
|
|
985
1076
|
return 0;
|
|
986
1077
|
}
|
|
1078
|
+
function emitDuplicatedRecall(io, messageId, target) {
|
|
1079
|
+
io.stdout(`${JSON.stringify({
|
|
1080
|
+
ok: true,
|
|
1081
|
+
identity: "bot",
|
|
1082
|
+
committed: true,
|
|
1083
|
+
duplicate: true,
|
|
1084
|
+
message_id: messageId,
|
|
1085
|
+
target,
|
|
1086
|
+
})}\n`);
|
|
1087
|
+
return 0;
|
|
1088
|
+
}
|
|
987
1089
|
function emitCommittedUnverified(result, io, input) {
|
|
988
1090
|
let providerResponse;
|
|
989
1091
|
try {
|
|
@@ -1074,10 +1176,20 @@ export function runLarkCli(argv, env = process.env, dependencies = {}) {
|
|
|
1074
1176
|
if (decision.kind === "passthrough")
|
|
1075
1177
|
return passthroughWithObservation(effectiveArgv, privateEnv, io, nativeDependencies, store);
|
|
1076
1178
|
try {
|
|
1179
|
+
const recallMessageId = decision.operation === "recall" ? assertRecallSyntax(effectiveArgv) : null;
|
|
1180
|
+
const priorRecall = recallMessageId ? recallLedgerEntry(store, recallMessageId) : null;
|
|
1181
|
+
if (priorRecall?.status === "deleted")
|
|
1182
|
+
return emitDuplicatedRecall(io, recallMessageId, priorRecall.target);
|
|
1183
|
+
if (priorRecall?.status === "deleting") {
|
|
1184
|
+
throw new Error("message recall 上次 provider 结果不明确,已 fail-closed 以避免重复撤回;请由用户检查原会话");
|
|
1185
|
+
}
|
|
1186
|
+
const recall = recallMessageId
|
|
1187
|
+
? resolveOwnedRecallTarget(recallMessageId, agent.feishuAppId, privateEnv, io, nativeDependencies)
|
|
1188
|
+
: null;
|
|
1077
1189
|
const urgent = decision.operation === "urgent-app"
|
|
1078
1190
|
? resolveUrgentAppTarget(effectiveArgv, privateEnv, io, nativeDependencies)
|
|
1079
1191
|
: null;
|
|
1080
|
-
const target = urgent?.target ?? guardedTarget(decision, effectiveArgv, store);
|
|
1192
|
+
const target = recall?.target ?? urgent?.target ?? guardedTarget(decision, effectiveArgv, store);
|
|
1081
1193
|
const targetKey = serializeFeishuImTarget(target);
|
|
1082
1194
|
const generation = freshnessGeneration(privateEnv);
|
|
1083
1195
|
const seen = store.readFreshnessCursor(targetKey, generation);
|
|
@@ -1095,6 +1207,26 @@ export function runLarkCli(argv, env = process.env, dependencies = {}) {
|
|
|
1095
1207
|
store.mergeFreshnessCursor(targetKey, gated.current, mergeFeishuImCursor, generation);
|
|
1096
1208
|
return 3;
|
|
1097
1209
|
}
|
|
1210
|
+
if (decision.operation === "recall") {
|
|
1211
|
+
const claim = claimRecall(store, recallMessageId, targetKey);
|
|
1212
|
+
if (claim === "deleted")
|
|
1213
|
+
return emitDuplicatedRecall(io, recallMessageId, targetKey);
|
|
1214
|
+
if (claim === "ambiguous") {
|
|
1215
|
+
throw new Error("message recall 上次 provider 结果不明确,已 fail-closed 以避免重复撤回;请由用户检查原会话");
|
|
1216
|
+
}
|
|
1217
|
+
const recallWrite = callNative(botArgv(effectiveArgv, "", decision), privateEnv, io, nativeDependencies);
|
|
1218
|
+
if (!recallWrite.error && recallWrite.status === 0) {
|
|
1219
|
+
const predictedSnapshot = {
|
|
1220
|
+
messages: gated.snapshot.messages.filter((message) => message.message_id !== recallMessageId),
|
|
1221
|
+
};
|
|
1222
|
+
const predictedCursor = feishuImFreshnessAdapter.cursor(predictedSnapshot);
|
|
1223
|
+
finalizeSuccessfulRecall(store, recallMessageId, targetKey, predictedCursor, generation);
|
|
1224
|
+
}
|
|
1225
|
+
else if (definitiveProviderRejection(recallWrite)) {
|
|
1226
|
+
finalizeFailedRecall(store, recallMessageId, targetKey);
|
|
1227
|
+
}
|
|
1228
|
+
return emitNativeResult(recallWrite, io);
|
|
1229
|
+
}
|
|
1098
1230
|
if (decision.operation === "urgent-app") {
|
|
1099
1231
|
const userIds = assertUrgentAppPreconditions(effectiveArgv, urgent.message, target, agent.feishuAppId);
|
|
1100
1232
|
assertUrgentAppMembers(effectiveArgv, target, userIds, privateEnv, io, nativeDependencies);
|
|
@@ -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 inbox_audit_heartbeat_js_1 = require("../agent/inbox-audit-heartbeat.cjs");
|
|
54
55
|
const missed_outbound_scan_js_1 = require("../agent/missed-outbound-scan.cjs");
|
|
55
56
|
const host_channel_business_js_1 = require("./host-channel-business.cjs");
|
|
56
57
|
const interaction_orchestrator_js_1 = require("./interaction-orchestrator.cjs");
|
|
@@ -340,6 +341,13 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
|
|
|
340
341
|
for (const agent of agents)
|
|
341
342
|
prepareAgentState(agent);
|
|
342
343
|
const reminder = new host_reminder_orchestrator_js_1.HostReminderOrchestrator({ agents, stateStore, envelopeProjector, deliveryTarget: runtimeHost, log });
|
|
344
|
+
const inboxAudit = new inbox_audit_heartbeat_js_1.InboxAuditHeartbeat({
|
|
345
|
+
agents,
|
|
346
|
+
stateStore,
|
|
347
|
+
runtimeHost,
|
|
348
|
+
log,
|
|
349
|
+
});
|
|
350
|
+
const auditRegistry = (0, missed_outbound_scan_js_1.inboxAuditRegistryFile)(larkinHome);
|
|
343
351
|
const seenEventIds = new Set();
|
|
344
352
|
const inFlightEventIds = new Set();
|
|
345
353
|
const onFeishuMessage = async (agent, event, options) => {
|
|
@@ -386,13 +394,11 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
|
|
|
386
394
|
// append/dedupe decision is durable. Agent model-seen state is untouched.
|
|
387
395
|
if (event.event_id)
|
|
388
396
|
seenEventIds.add(eventKey);
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
log(`inbound scan target 未持久化: ${error.message}`);
|
|
395
|
-
}
|
|
397
|
+
try {
|
|
398
|
+
(0, missed_outbound_scan_js_1.observeInboxAuditTarget)(auditRegistry, agent.agentId, event);
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
log(`inbox audit target 未持久化: ${error.message}`);
|
|
396
402
|
}
|
|
397
403
|
if (append.status === "duplicate_consumed")
|
|
398
404
|
return null;
|
|
@@ -1345,6 +1351,7 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
|
|
|
1345
1351
|
eventSourceStartTimer = null;
|
|
1346
1352
|
await Promise.resolve(eventSourceStop());
|
|
1347
1353
|
reminder.stopSync();
|
|
1354
|
+
inboxAudit.stop();
|
|
1348
1355
|
interaction.stopSync();
|
|
1349
1356
|
await runtimeHost.shutdown(reason);
|
|
1350
1357
|
})();
|
|
@@ -1559,6 +1566,7 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
|
|
|
1559
1566
|
sessionId: agentStates.get(agent.agentId)?.state.sessions[agent.runtime] || null,
|
|
1560
1567
|
})));
|
|
1561
1568
|
reminder.startSync();
|
|
1569
|
+
inboxAudit.start();
|
|
1562
1570
|
interaction.startSync();
|
|
1563
1571
|
eventSourceStartTimer = setTimeout(() => { eventSourceStartTimer = null; startEventSource(); }, eventSourceStartDelayMs);
|
|
1564
1572
|
}
|
|
@@ -11,7 +11,8 @@ 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 {
|
|
14
|
+
import { InboxAuditHeartbeat } from "../agent/inbox-audit-heartbeat.mjs";
|
|
15
|
+
import { inboxAuditRegistryFile, observeInboxAuditTarget } from "../agent/missed-outbound-scan.mjs";
|
|
15
16
|
import { HostChannelBusiness } from "./host-channel-business.mjs";
|
|
16
17
|
import { HostInteractionOrchestrator } from "./interaction-orchestrator.mjs";
|
|
17
18
|
import { targetFor } from "./message-policy.mjs";
|
|
@@ -300,6 +301,13 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
|
|
|
300
301
|
for (const agent of agents)
|
|
301
302
|
prepareAgentState(agent);
|
|
302
303
|
const reminder = new HostReminderOrchestrator({ agents, stateStore, envelopeProjector, deliveryTarget: runtimeHost, log });
|
|
304
|
+
const inboxAudit = new InboxAuditHeartbeat({
|
|
305
|
+
agents,
|
|
306
|
+
stateStore,
|
|
307
|
+
runtimeHost,
|
|
308
|
+
log,
|
|
309
|
+
});
|
|
310
|
+
const auditRegistry = inboxAuditRegistryFile(larkinHome);
|
|
303
311
|
const seenEventIds = new Set();
|
|
304
312
|
const inFlightEventIds = new Set();
|
|
305
313
|
const onFeishuMessage = async (agent, event, options) => {
|
|
@@ -346,13 +354,11 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
|
|
|
346
354
|
// append/dedupe decision is durable. Agent model-seen state is untouched.
|
|
347
355
|
if (event.event_id)
|
|
348
356
|
seenEventIds.add(eventKey);
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
log(`inbound scan target 未持久化: ${error.message}`);
|
|
355
|
-
}
|
|
357
|
+
try {
|
|
358
|
+
observeInboxAuditTarget(auditRegistry, agent.agentId, event);
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
log(`inbox audit target 未持久化: ${error.message}`);
|
|
356
362
|
}
|
|
357
363
|
if (append.status === "duplicate_consumed")
|
|
358
364
|
return null;
|
|
@@ -1305,6 +1311,7 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
|
|
|
1305
1311
|
eventSourceStartTimer = null;
|
|
1306
1312
|
await Promise.resolve(eventSourceStop());
|
|
1307
1313
|
reminder.stopSync();
|
|
1314
|
+
inboxAudit.stop();
|
|
1308
1315
|
interaction.stopSync();
|
|
1309
1316
|
await runtimeHost.shutdown(reason);
|
|
1310
1317
|
})();
|
|
@@ -1519,6 +1526,7 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
|
|
|
1519
1526
|
sessionId: agentStates.get(agent.agentId)?.state.sessions[agent.runtime] || null,
|
|
1520
1527
|
})));
|
|
1521
1528
|
reminder.startSync();
|
|
1529
|
+
inboxAudit.start();
|
|
1522
1530
|
interaction.startSync();
|
|
1523
1531
|
eventSourceStartTimer = setTimeout(() => { eventSourceStartTimer = null; startEventSource(); }, eventSourceStartDelayMs);
|
|
1524
1532
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "larkin",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.23",
|
|
4
4
|
"private": false,
|
|
5
5
|
"larkinPackageRole": "npm-published",
|
|
6
6
|
"files": [
|
|
@@ -73,9 +73,12 @@
|
|
|
73
73
|
"test:eval:pi-bash-timeout": "bun run build && bun test --max-concurrency 1 test/live/pi-bash-timeout-live.test.mjs",
|
|
74
74
|
"test:eval:runtime-agent-interface-v2": "bun test --max-concurrency 1 test/unit/evals/runtime-agent-interface-v2.test.mjs",
|
|
75
75
|
"test:eval:authoritative-freshness-gate": "bun test --max-concurrency 1 test/unit/evals/authoritative-freshness-gate.test.mjs",
|
|
76
|
+
"test:eval:protected-message-recall": "bun run build && bun test --max-concurrency 1 test/unit/evals/protected-message-recall.test.mjs test/unit/app/lark-cli-launcher.test.mjs",
|
|
76
77
|
"test:eval:agent-experience-v6": "bun test --max-concurrency 1 test/unit/evals/agent-experience-v6.test.mjs",
|
|
77
78
|
"test:eval:mention-construction": "bun run build && LARKIN_RUN_MENTION_CONSTRUCTION_EVAL=1 bun test --max-concurrency 1 test/live/mention-construction-agent-eval.test.mjs",
|
|
78
79
|
"test:eval:clickable-link-delivery": "bun run build && bun test --max-concurrency 1 test/unit/evals/clickable-link-delivery.test.mjs",
|
|
80
|
+
"test:eval:im-body-mode": "bun run build && bun test --max-concurrency 1 test/unit/evals/im-body-mode.test.mjs",
|
|
81
|
+
"test:eval:im-body-mode:native": "bun run build && LARKIN_RUN_IM_BODY_MODE_EVAL=1 bun test --max-concurrency 1 test/live/im-body-mode-agent-eval.test.mjs",
|
|
79
82
|
"test:eval:clickable-link-delivery:native": "bun run build && LARKIN_RUN_CLICKABLE_LINK_DELIVERY_EVAL=1 bun test --max-concurrency 1 test/live/clickable-link-delivery-agent-eval.test.mjs",
|
|
80
83
|
"test:eval:public-contribution-governance": "bun test --max-concurrency 1 test/unit/evals/public-contribution-governance.test.mjs",
|
|
81
84
|
"test:live:public-contribution-governance": "bun test --max-concurrency 1 test/live/public-contribution-governance-live.test.mjs",
|