mirror of
https://github.com/MemeMeow-Studio/MemeMeow.git
synced 2026-10-16 20:31:33 +08:00
fix: deliver analysis reminder through active session messages
This commit is contained in:
@@ -33,7 +33,7 @@ function describeError(stage, error) {
|
||||
return `${stage}:${status ? `HTTP_${status}:` : ""}${reason}:${message}`;
|
||||
}
|
||||
|
||||
/** 使用当前进程的 SDK transport 读取累计金额,每个 attempt 最多提交一次 steer 输入。 */
|
||||
/** 使用当前进程的 SDK transport 读取累计金额,每个 attempt 最多保存一次用户提醒。 */
|
||||
export default async function analysisReminder({ client }, options) {
|
||||
const { attempt_id, policy, socket_path, session_id } = options ?? {};
|
||||
if (!attempt_id || !socket_path || !policy || policy.version !== 1) {
|
||||
@@ -64,21 +64,23 @@ export default async function analysisReminder({ client }, options) {
|
||||
let checking = false;
|
||||
let submissionStarted = false;
|
||||
return {
|
||||
"experimental.chat.system.transform": async (input) => {
|
||||
// prompt admission 可能引起 hook 重入;重入立即返回,不等待当前提交。
|
||||
"experimental.chat.messages.transform": async (_input, output) => {
|
||||
// 提醒保存期间的并发 hook 立即返回,不等待当前提交。
|
||||
if (checking || submissionStarted) return;
|
||||
if (state.session_id && input.sessionID !== state.session_id) return;
|
||||
const user = output.messages.findLast(message => message.info.role === "user")?.info;
|
||||
const sessionID = user?.sessionID;
|
||||
if (state.session_id && sessionID !== state.session_id) return;
|
||||
checking = true;
|
||||
let stage = "analysis_reminder_session_read";
|
||||
try {
|
||||
try {
|
||||
if (!input.sessionID) throw new Error("analysis_reminder_session_missing");
|
||||
if (!sessionID) throw new Error("analysis_reminder_session_missing");
|
||||
const response = await sdk.v2.session.get(
|
||||
{ sessionID: input.sessionID }, { throwOnError: true },
|
||||
{ sessionID }, { throwOnError: true },
|
||||
);
|
||||
const session = response.data.data;
|
||||
if (session.parentID) return;
|
||||
if (session.id !== input.sessionID) throw new Error("analysis_reminder_session_mismatch");
|
||||
if (session.id !== sessionID) throw new Error("analysis_reminder_session_mismatch");
|
||||
state.session_id = session.id;
|
||||
if (typeof session.cost !== "number" || !Number.isFinite(session.cost) || session.cost < 0) {
|
||||
throw new Error("analysis_reminder_cost_invalid");
|
||||
@@ -87,15 +89,23 @@ export default async function analysisReminder({ client }, options) {
|
||||
// 响应丢失时无法确认服务端是否已接收,因此本 attempt 不重复提交。
|
||||
submissionStarted = true;
|
||||
stage = "analysis_reminder_prompt_submit";
|
||||
const admitted = await sdk.v2.session.prompt(
|
||||
{ sessionID: session.id, prompt: { text: reminder }, delivery: "steer" },
|
||||
const saved = await sdk.session.prompt(
|
||||
{
|
||||
sessionID, noReply: true, parts: [{ type: "text", text: reminder }],
|
||||
agent: user.agent, model: user.model, variant: user.model.variant,
|
||||
tools: user.tools, system: user.system, format: user.format,
|
||||
},
|
||||
{ throwOnError: true },
|
||||
);
|
||||
if (saved.data?.info?.role !== "user" || saved.data.info.sessionID !== sessionID) {
|
||||
throw new Error("analysis_reminder_message_invalid");
|
||||
}
|
||||
// hook 的消息快照早于保存操作;加入真实消息后,本次模型请求即可读取提醒。
|
||||
output.messages.push(saved.data);
|
||||
state.reminder_sent = true;
|
||||
console.info(JSON.stringify({
|
||||
event: "analysis_reminder_admitted", attempt_id,
|
||||
session_id: session.id, message_id: admitted.data.data.id,
|
||||
admitted_seq: admitted.data.data.admittedSeq,
|
||||
event: "analysis_reminder_message_included", attempt_id,
|
||||
session_id: session.id, message_id: saved.data.info.id,
|
||||
}));
|
||||
}
|
||||
state.error = null;
|
||||
|
||||
@@ -46,13 +46,13 @@ Server 初始策略如下:
|
||||
|
||||
### 3. 插件读取主 session 用量并发送一次提醒
|
||||
|
||||
插件使用 `experimental.chat.system.transform`,从当前请求取得 `sessionID`,通过 OpenCode SDK 读取冻结主 session 已完成请求的累计金额。插件不自行计算 token 价格,也不汇总子 session 或其他 session。
|
||||
插件使用 `experimental.chat.messages.transform`,从当前用户消息取得 `sessionID`,通过 OpenCode SDK 读取冻结主 session 已完成请求的累计金额。插件不自行计算 token 价格,也不汇总子 session 或其他 session。
|
||||
|
||||
累计金额达到 `reminder_cost` 后,插件调用 `sdk.v2.session.prompt({ sessionID, prompt: { text }, delivery: "steer" }, { throwOnError: true })`,省略 `resume`,文字为“请尽快完成必要工作、验证结果并生成报告。”。提醒作为当前主 session 的用户输入被持久化,后续 Agent 回合按照 OpenCode 调度读取。
|
||||
累计金额达到 `reminder_cost` 后,插件调用 `sdk.session.prompt()`,设置 `noReply: true`,文字为“请尽快完成必要工作、验证结果并生成报告。”,沿用当前用户消息的 agent、model、variant、tools、system 和 format。OpenCode 保存用户消息后立即返回,TUI 通过同一消息接口读取提醒。插件将返回的真实消息加入当前 hook 的 messages,使即将发送的模型请求包含提醒。
|
||||
|
||||
插件在异步读取前设置处理中状态,重入 hook 直接返回;提交前设置本 attempt 已尝试提交,成功收到 admission 后才设置 `reminder_sent=true`。通过 Executor 创建的 Unix socket 报告状态,日志记录消息 ID 和 admission 序号。请求失败或响应丢失时保留阶段、错误类别和消息,本 attempt 不自动重复提交。请求重试、上下文压缩和后续调用均不重复提醒。恢复产生的新 attempt 拥有独立提醒状态,并继续读取同一 session 的累计金额。
|
||||
插件在异步读取前设置处理中状态,重入 hook 直接返回;提交前设置本 attempt 已尝试提交,保存成功且加入当前消息后才设置 `reminder_sent=true`。通过 Executor 创建的 Unix socket 报告状态,日志记录消息 ID。请求失败或响应丢失时保留阶段、错误类别和消息,本 attempt 不自动重复提交。请求重试、上下文压缩和后续调用均不重复提醒。恢复产生的新 attempt 拥有独立提醒状态,并继续读取同一 session 的累计金额。
|
||||
|
||||
例如提醒金额为 0.10 美元、主 session 已用 0.11 美元时,插件提交一条 steer 输入。admission 成功只表示已接收;验收另外检查后续模型输入包含提醒、assistant 响应以及最终 attempt 状态。
|
||||
例如提醒金额为 0.10 美元、主 session 已用 0.11 美元时,插件保存一条用户提醒并加入本次模型输入。验收检查消息持久化、TUI 消息接口、模型处理证据及最终 attempt 状态;提醒状态不代表任务完成。
|
||||
|
||||
插件状态绑定 attempt 标识、策略版本、插件版本和 session。运行期间无法读取用量或追加提示时,插件记录明确错误并允许任务继续;Executor 的终止判断不读取插件提醒结果。
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Agent 任务只有总超时,无法按模型等级控制一次分析允许消
|
||||
- Server 保留三等级目录并显式启用:`model_free` 提醒 0.05 美元、终止 0.10 美元;`model_standard` 提醒 0.10 美元、终止 0.20 美元;`model_plus` 提醒 0.15 美元、终止 0.30 美元。公开模型选择响应不返回内部金额阈值。
|
||||
- 配置工具在未单独指定终止限额时使用提醒限额的两倍;冻结策略保存两个明确值,允许各等级配置不同关系。
|
||||
- 任务创建时由可信配置按照模型目录键冻结实际模型、变体、启用状态和金额策略。恢复任务沿用原策略和同一 session 的累计金额。
|
||||
- OpenCode 插件读取当前主 session 已完成请求的累计金额,达到提醒限额后通过 v2 SDK 向当前 session 最多提交一次 `delivery: "steer"` 用户输入;成功接收后记录 admission。
|
||||
- OpenCode 插件读取当前主 session 已完成请求的累计金额,达到提醒限额后通过 `sdk.session.prompt()` 和 `noReply: true` 向当前 session 最多保存一次用户提醒,并加入本次模型输入;TUI 通过同一消息接口读取提醒。
|
||||
- Executor 从任务 `OPENCODE_DB` 读取冻结主 session 的 `session.cost`,达到终止限额后复用现有进程组终止和回收流程;允许检查间隔内已完成调用造成金额超过限额。
|
||||
- 启用策略的 Agent 不依赖 bubblewrap 或 Linux namespace;Executor 使用任务专属 scratch、数据库、候选 manifest 和 OpenCode 文件权限规则,容器继续提供运行时边界。
|
||||
- 因金额达到限额而停止且确认进程已回收时,公开错误为 `agent_maximum_analysis_depth_exceeded`,显示“超过最大分析程度”。受保护诊断保存观测金额、策略阈值、检查阶段、终止信号和回收结果。
|
||||
|
||||
+5
-5
@@ -48,11 +48,11 @@
|
||||
|
||||
### Requirement: 插件必须在达到提醒限额后尽力发送一次收尾提醒
|
||||
|
||||
系统 MUST 使用 OpenCode 现有插件接口读取当前 attempt 主 session 已完成请求的累计金额。插件 MUST 只统计冻结的主 session。当累计金额达到或超过 `reminder_cost` 时,插件 MUST 调用 `sdk.v2.session.prompt()`,请求只包含 `sessionID`、`prompt.text` 和 `delivery: "steer"`,省略 `resume`。文字 MUST 为“请尽快完成必要工作、验证结果并生成报告。”。每个 attempt 最多提交一次提醒,恢复产生的新 attempt 拥有独立提醒状态并沿用同一 session 的累计金额。提醒作为当前 session 的用户输入持久化。插件 MUST 在异步检查前设置防重入状态,并在提交前设置已尝试提交;重入 hook 直接返回。成功返回 admission 后才设置 `reminder_sent=true`,通过现有 Unix socket 报告状态并记录 admission 标识。插件异常 MUST 保留阶段、错误类别和消息,Executor 独立执行金额终止。
|
||||
系统 MUST 使用 `experimental.chat.messages.transform` 读取当前 attempt 主 session 已完成请求的累计金额。插件 MUST 只统计冻结的主 session。当累计金额达到或超过 `reminder_cost` 时,插件 MUST 调用 `sdk.session.prompt()`,设置 `noReply: true`,沿用当前用户消息的 agent、model、variant、tools、system 和 format。文字 MUST 为“请尽快完成必要工作、验证结果并生成报告。”。每个 attempt 最多提交一次提醒,恢复产生的新 attempt 拥有独立提醒状态并沿用同一 session 的累计金额。提醒 MUST 保存为 TUI 消息接口可以读取的用户消息,并将返回的消息加入本次 hook 的 messages。插件 MUST 在异步检查前设置防重入状态,并在提交前设置已尝试提交;重入 hook 直接返回。保存成功且加入当前消息后才设置 `reminder_sent=true`,通过现有 Unix socket 报告状态并记录消息 ID。插件异常 MUST 保留阶段、错误类别和消息,Executor 独立执行金额终止。
|
||||
|
||||
#### Scenario: 达到提醒限额
|
||||
- **WHEN** 某 attempt 的提醒金额限额为 0.10 美元,插件在下一次模型请求前确认主 session 已完成用量为 0.11 美元且尚未提醒
|
||||
- **THEN** 插件向主 session 提交一次 steer 输入,成功返回 admission 后记录该 attempt 已提醒
|
||||
- **THEN** 插件向主 session 保存一次用户提醒,加入本次模型输入后记录该 attempt 已提醒
|
||||
|
||||
#### Scenario: 提醒不重复发送
|
||||
- **WHEN** 已提醒的 attempt 因模型重试、上下文压缩或后续请求再次满足提醒条件
|
||||
@@ -62,9 +62,9 @@
|
||||
- **WHEN** 提醒提交发生网络错误、session 不存在或请求冲突
|
||||
- **THEN** 保留实际错误类别和消息,`reminder_sent` 保持 false,本 attempt 不自动重复提交
|
||||
|
||||
#### Scenario: admission 与后续处理分别验收
|
||||
- **WHEN** OpenCode 返回提醒的消息 ID 和 admission 序号
|
||||
- **THEN** 该结果仅证明已接收输入;验收另外检查后续模型输入包含提醒、assistant 响应及最终 attempt 状态
|
||||
#### Scenario: 消息与任务结果分别验收
|
||||
- **WHEN** OpenCode 返回保存后的提醒消息
|
||||
- **THEN** 验收确认 TUI 消息接口可以读取提醒、本次模型输入包含提醒,并检查 assistant 响应及最终 attempt 状态
|
||||
|
||||
#### Scenario: 子 session 不计入累计用量
|
||||
- **WHEN** OpenCode 历史中存在不属于冻结主 session 的子 session 或其他 session
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
## 2. OpenCode 提醒插件
|
||||
|
||||
- [x] 2.1 编写与生产 OpenCode `1.18.18` 匹配的本地 JavaScript 插件,通过当前 OpenCode 进程的 session SDK 投影读取冻结主 session 已完成请求的累计金额,不递归汇总子 session,也不自行重新计算 token 价格
|
||||
- [ ] 2.2 在金额检查 hook 中通过 v2 SDK 提交 `delivery: "steer"` 用户输入,省略 `resume`,每个 attempt 最多提交一次“请尽快完成必要工作、验证结果并生成报告。”;验证重入、admission 状态和后续模型处理
|
||||
- [ ] 2.2 在 messages hook 中通过 `sdk.session.prompt()` 和 `noReply: true` 保存用户消息并加入本次模型输入,每个 attempt 最多提交一次“请尽快完成必要工作、验证结果并生成报告。”;验证消息持久化、TUI 消息接口和后续模型处理
|
||||
- [x] 2.3 以 attempt 状态记录提醒结果,覆盖该 attempt 内的请求重试、上下文压缩和后续模型调用;恢复产生的新 attempt 可以再次提醒一次
|
||||
- [x] 2.4 对 session 缺失、用量读取失败和提示注入失败记录明确错误;运行期提醒失败允许任务继续,插件不调用终止流程
|
||||
- [x] 2.5 让任务配置显式引用镜像内只读插件并传入冻结策略、目标模型、模型等级、attempt 标识和策略版本
|
||||
|
||||
Reference in New Issue
Block a user