// trigger.mts —— 各层「手动触发」的进程外入口(查看器写盘桥 + 人工运维) // // 为什么不能直接干跑(本文件存在的全部理由):`triggerManual` 依赖主进程的 `cachedCtx` + // executors + `kick()`(`memory/pipeline/worker.ts` 的 `drain` 在 `!cachedCtx` 时直接 return), // 那是**另一个进程**里的闭包,跨进程够不着。全仓库原本只有 L3 有一条落盘通道 // (`memory/l3/signal.ts` 的 `l3/pending-trigger.json`);本脚本把 L1/L2 也接到同一手法上, // 写一份信号文件,主进程在 `agent_settled` 读后即删并据此入队。 // // 因此**触发 = 请求入队,不是立即执行**: // · 需要一个活着的 pi 会话(子代理要靠 ctx 才能 spawn);pi 关着时信号就躺在盘上等; // · 真正跑起来还要过队列的单飞闸与去重(同一层已在跑/已排队时不会重复入队)。 // 界面必须把这句原话写上,不能让"点了按钮没反应"变成新的静默失效。 // // 用法: // node --experimental-strip-types scripts/l1-admin/trigger.mts --layer l1|l2|l3 [--reason "…"] [--by viewer] [--profile ] // node --experimental-strip-types scripts/l1-admin/trigger.mts --status # 只读探查三个层待消费的信号 import { writePendingTrigger as writeL3Signal, peekPendingTrigger as peekL3Signal } from "../../memory/l3/signal.ts"; import { isTriggerLayer, peekPendingTrigger, pendingTriggerPath, writePendingTrigger, } from "../../memory/pipeline/pending-trigger.ts"; import { fail, finish, flagValue, hasFlag, profileArg, run, usageError } from "./viewer-cli.mts"; const USAGE = 'node --experimental-strip-types scripts/l1-admin/trigger.mts --layer l1|l2|l3 [--reason "…"] [--by viewer] ' + "[--profile ] | --status"; /** 三个层的信号状态(统一形状;L3 的载荷不同,这里翻译成同一套字段)。 */ function statusAll(profile: string): Record { const l1 = peekPendingTrigger(profile, "l1"); const l2 = peekPendingTrigger(profile, "l2"); const l3 = peekL3Signal(profile); return { l1: l1 ? { at: l1.at, reason: l1.reason ?? null, by: l1.by ?? null } : null, l2: l2 ? { at: l2.at, reason: l2.reason ?? null, by: l2.by ?? null } : null, // L3 的信号是给 `evaluateTrigger` 的 P1 闸看的,personaSignal 单独透出(缺它就是个空信号) l3: l3 ? { at: l3.at, reason: l3.personaReason ?? null, personaSignal: l3.personaSignal } : null, }; } run(() => { const profilePath = profileArg(); if (!profilePath) usageError(USAGE); if (hasFlag("--status")) { finish({ success: true, pending: statusAll(profilePath) }); } const layerRaw = flagValue("--layer"); if (!isTriggerLayer(layerRaw)) usageError(USAGE); const layer = layerRaw; const reason = flagValue("--reason"); const by = flagValue("--by") ?? "cli"; if (layer === "l3") { // L3 的写入函数是静默的(返回 void,失败不留痕)—— 所以**写完自己回读一次**来确认, // 而不是假定成功。这次回读同时给界面提供"信号确实在盘上了"的证据。 writeL3Signal(profilePath, { personaSignal: true, personaReason: reason }); } else if (!writePendingTrigger(profilePath, layer, { reason, by })) { fail(`写触发信号失败(目录不可写?):${pendingTriggerPath(profilePath, layer)}`); } const pending = statusAll(profilePath); const mine = pending[layer] as { at?: string } | null; if (!mine) { fail(`${layer} 的信号写完却回读不到 —— 写入未生效,请检查 ${profilePath} 下 memoryDB 目录权限`); } finish({ success: true, layer, pending: mine, note: "已写入请求。它只负责入队:需要 pi 会话活着,并在下一轮结束(agent_settled)时由主进程消费;" + "若该层已在跑或已排队,队列去重会让这次请求不新增任务。", }); });