diff --git a/src/agents/subagent-registry.store.sqlite.test.ts b/src/agents/subagent-registry.store.sqlite.test.ts index 0681b1f81e68..7fe4e9635827 100644 --- a/src/agents/subagent-registry.store.sqlite.test.ts +++ b/src/agents/subagent-registry.store.sqlite.test.ts @@ -3,6 +3,8 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { executeSqliteQuerySync, getNodeSqliteKysely } from "../infra/kysely-sync.js"; +import type { DB as OpenClawStateKyselyDatabase } from "../state/openclaw-state-db.generated.js"; import { closeOpenClawStateDatabaseForTest, openOpenClawStateDatabase, @@ -14,6 +16,8 @@ import { } from "./subagent-registry.store.sqlite.js"; import type { SubagentRunRecord } from "./subagent-registry.types.js"; +type SubagentRegistryDatabase = Pick; + function createRun(overrides: Partial = {}): SubagentRunRecord { return { runId: "run-one", @@ -121,6 +125,57 @@ describe("subagent registry sqlite store", () => { }); }); + it("preserves announcedAt for not_required delivery when completion was announced", async () => { + await withTempStateEnv(async () => { + const run = createRun({ + expectsCompletionMessage: false, + completion: { required: false }, + delivery: { status: "not_required", announcedAt: 300 }, + }); + + saveSubagentRegistryToSqlite(new Map([[run.runId, run]])); + + const restored = loadSubagentRegistryFromSqlite(); + const restoredRun = restored.get(run.runId)!; + expect(restoredRun.delivery?.status).toBe("not_required"); + expect(restoredRun.delivery?.announcedAt).toBe(300); + expect(restoredRun.delivery?.deliveredAt).toBeUndefined(); + }); + }); + + it("repairs a tainted delivered status when completion is not required", async () => { + await withTempStateEnv(async () => { + const run = createRun({ + expectsCompletionMessage: false, + completion: { required: false }, + delivery: { status: "not_required", announcedAt: 300 }, + }); + saveSubagentRegistryToSqlite(new Map([[run.runId, run]])); + + const { db } = openOpenClawStateDatabase(); + const stateDb = getNodeSqliteKysely(db); + executeSqliteQuerySync( + db, + stateDb + .updateTable("subagent_runs") + .set({ + payload_json: JSON.stringify({ + ...run, + delivery: { status: "delivered", announcedAt: 300, deliveredAt: 300 }, + }), + }) + .where("run_id", "=", run.runId), + ); + + const restoredRun = loadSubagentRegistryFromSqlite().get(run.runId)!; + expect(restoredRun.delivery).toMatchObject({ + status: "not_required", + announcedAt: 300, + deliveredAt: 300, + }); + }); + }); + it("does not read or delete the retired JSON registry at runtime", async () => { await withTempStateEnv(async () => { const legacyRun = createRun({ diff --git a/src/agents/subagent-registry.store.sqlite.ts b/src/agents/subagent-registry.store.sqlite.ts index 788600168838..75bdffb8395b 100644 --- a/src/agents/subagent-registry.store.sqlite.ts +++ b/src/agents/subagent-registry.store.sqlite.ts @@ -89,13 +89,16 @@ function createDeliveryFromTypedColumns( ...(row.pending_final_delivery_last_error !== null ? { lastError: row.pending_final_delivery_last_error } : {}), - ...(row.completion_announced_at !== null + ...(row.completion_announced_at !== null && row.expects_completion_message === 1 ? { status: "delivered", announcedAt: row.completion_announced_at, deliveredAt: delivery?.deliveredAt ?? row.completion_announced_at, } - : {}), + : row.completion_announced_at !== null + ? { announcedAt: row.completion_announced_at } + : {}), + ...(row.expects_completion_message === 0 ? { status: "not_required" } : {}), }; }