feat(memory): P1 长期记忆升级 —— 异步攒批 Consolidate + 软删 + importance/last_seen #1

Merged
Blizzard merged 181 commits from feat/wails3 into main 2026-07-17 01:12:32 +00:00
5 changed files with 39 additions and 4 deletions
Showing only changes of commit d7c6fab933 - Show all commits
+20 -1
View File
@@ -14,7 +14,7 @@ import { Placeholder } from "./views/Placeholder";
import { CommandPalette, type Command } from "./components/CommandPalette"; import { CommandPalette, type Command } from "./components/CommandPalette";
import { UpdateBanner } from "./components/UpdateBanner"; import { UpdateBanner } from "./components/UpdateBanner";
import { Login } from "./views/Login"; import { Login } from "./views/Login";
import { submitTask, streamTokens, streamExec, authMe, logout, type Identity, type AuthUser } from "./lib/api"; import { submitTask, streamTokens, streamExec, taskStatus, authMe, logout, type Identity, type AuthUser } from "./lib/api";
import type { TaskDsl } from "./lib/dsl"; import type { TaskDsl } from "./lib/dsl";
import { emptyRun, type RunState } from "./lib/run"; import { emptyRun, type RunState } from "./lib/run";
import { ToastProvider } from "./ui"; import { ToastProvider } from "./ui";
@@ -48,6 +48,13 @@ export default function App() {
const closeRef = useRef<(() => void) | null>(null); const closeRef = useRef<(() => void) | null>(null);
const execCloseRef = useRef<(() => void) | null>(null); const execCloseRef = useRef<(() => void) | null>(null);
const pollRef = useRef<number | null>(null);
const stopPoll = () => {
if (pollRef.current != null) {
window.clearInterval(pollRef.current);
pollRef.current = null;
}
};
// 全局 ⌘K / Ctrl+K 唤起命令面板(键盘优先工作站入口)。 // 全局 ⌘K / Ctrl+K 唤起命令面板(键盘优先工作站入口)。
useEffect(() => { useEffect(() => {
@@ -98,6 +105,7 @@ export default function App() {
async (dsl: TaskDsl) => { async (dsl: TaskDsl) => {
closeRef.current?.(); closeRef.current?.();
execCloseRef.current?.(); execCloseRef.current?.();
stopPoll();
const t0 = Date.now(); const t0 = Date.now();
setRun({ phase: "submitting", output: "", events: [{ t: 0, label: "提交任务" }], exec: [] }); setRun({ phase: "submitting", output: "", events: [{ t: 0, label: "提交任务" }], exec: [] });
try { try {
@@ -109,6 +117,17 @@ export default function App() {
taskId, taskId,
events: [...r.events, { t: Date.now() - t0, label: `已发布 ${taskId}` }], events: [...r.events, { t: Date.now() - t0, label: `已发布 ${taskId}` }],
})); }));
// 轮询后端任务状态:可靠捕获 waiting(审批中断)—— exec 实时事件会抢跑,状态落 PG 不会丢。
const terminal = new Set(["done", "failed", "timeout", "rejected"]);
pollRef.current = window.setInterval(async () => {
try {
const s = await taskStatus(taskId);
setRun((r) => (r.taskId === taskId ? { ...r, lifecycle: s.status, detail: s.detail } : r));
if (terminal.has(s.status)) stopPoll();
} catch {
/* 忽略瞬时失败,下个 tick 再试 */
}
}, 1500);
// 执行轨迹(运行·观测):与 token 流并行订阅,逐节点点亮。 // 执行轨迹(运行·观测):与 token 流并行订阅,逐节点点亮。
execCloseRef.current = streamExec( execCloseRef.current = streamExec(
taskId, taskId,
+9
View File
@@ -109,6 +109,15 @@ export async function submitTask(dsl: TaskDsl, id: Identity): Promise<string> {
return data.task_id; return data.task_id;
} }
// taskStatus: GET /api/v1/tasks/:id —— 任务生命周期状态(submitted/running/waiting/done/failed/timeout/rejected)。
// 轮询它来可靠捕获 waiting(审批中断)—— 不依赖易抢跑的实时 exec 事件。
export async function taskStatus(taskId: string): Promise<{ status: string; detail: string }> {
const res = guard401(await fetch(`${GATEWAY}/api/v1/tasks/${taskId}`, { headers: bearer() }));
if (!res.ok) throw new Error(`status failed: ${res.status}`);
const d = (await res.json()) as { status?: string; detail?: string };
return { status: d.status ?? "", detail: d.detail ?? "" };
}
// approveTask: POST /api/v1/tasks/:id/approve —— HITL 人工审批决定(批准放行 / 拒绝中止)。 // approveTask: POST /api/v1/tasks/:id/approve —— HITL 人工审批决定(批准放行 / 拒绝中止)。
export async function approveTask(taskId: string, approved: boolean, opts?: { node?: string; note?: string }): Promise<void> { export async function approveTask(taskId: string, approved: boolean, opts?: { node?: string; note?: string }): Promise<void> {
const res = guard401( const res = guard401(
+2
View File
@@ -15,6 +15,8 @@ export interface RunState {
events: RunEvent[]; events: RunEvent[];
exec: ExecEvent[]; // 后端回流的节点级执行轨迹(运行·观测) exec: ExecEvent[]; // 后端回流的节点级执行轨迹(运行·观测)
error?: string; error?: string;
lifecycle?: string; // 轮询到的后端任务状态(waiting 时弹审批;done/rejected 等终态)
detail?: string; // 状态附带说明(如审批标题)
} }
export const emptyRun: RunState = { phase: "idle", output: "", events: [], exec: [] }; export const emptyRun: RunState = { phase: "idle", output: "", events: [], exec: [] };
@@ -27,7 +27,11 @@ export function BottomDrawer({ run }: { run: RunState }) {
const statusText = const statusText =
run.phase === "streaming" ? "流式中…" : run.phase === "done" ? "完成 ✓" : run.phase === "error" ? `${run.error ?? "出错"}` : run.phase === "submitting" ? "提交中…" : "就绪"; run.phase === "streaming" ? "流式中…" : run.phase === "done" ? "完成 ✓" : run.phase === "error" ? `${run.error ?? "出错"}` : run.phase === "submitting" ? "提交中…" : "就绪";
const approval = run.taskId ? pendingApproval(run.exec) : null; // 审批条触发以「后端状态 waiting」为准(可靠,落 PG);exec 的 await 事件仅用于丰富摘要(可能抢跑丢失)。
const approval =
run.taskId && run.lifecycle === "waiting"
? pendingApproval(run.exec) ?? { node: "", title: run.detail || "人工审批", summary: "" }
: null;
return ( return (
<div className="shrink-0 border-t border-line bg-ink-900"> <div className="shrink-0 border-t border-line bg-ink-900">
@@ -105,8 +105,8 @@ export const NODE_KINDS: Record<string, NodeKind> = {
approval: { approval: {
kind: "approval", kind: "approval",
label: "人工审批", label: "人工审批",
accent: "border-l-amber-500", accent: "border-l-orange-500",
badge: "bg-amber-100 text-amber-700", badge: "bg-orange-100 text-orange-700",
desc: "HITL:暂停等人工批准", desc: "HITL:暂停等人工批准",
fields: [ fields: [
{ key: "title", label: "审批标题", type: "text", placeholder: "如:高危操作审批" }, { key: "title", label: "审批标题", type: "text", placeholder: "如:高危操作审批" },
@@ -162,6 +162,7 @@ export const NODE_ORDER = [
"tool", "tool",
"memory", "memory",
"branch", "branch",
"approval",
"map", "map",
"aggregate", "aggregate",
"render", "render",