From 1f3b4e86ca672485725ef8717841df528e9944d3 Mon Sep 17 00:00:00 2001 From: heichl_xydigit Date: Fri, 17 Jul 2026 01:21:50 +0800 Subject: [PATCH] fix(agents): keep not_required delivery status when completion_announced_at exists (#108710) * fix(agents): keep not_required delivery status when completion_announced_at exists for expectsCompletionMessage=false runs createDeliveryFromTypedColumns unconditionally overrode status to 'delivered' when completion_announced_at was present, even for runs where expects_completion_message is false. This produced a contradictory delivery state where a run that does not require completion delivery was marked as delivered. Only set status to 'delivered' when expects_completion_message is true. For not_required runs that still have completion_announced_at recorded (for informational purposes), preserve announcedAt without overriding the status. * test(agents): cover tainted delivery rows --------- Co-authored-by: Peter Steinberger --- .../subagent-registry.store.sqlite.test.ts | 55 +++++++++++++++++++ src/agents/subagent-registry.store.sqlite.ts | 7 ++- 2 files changed, 60 insertions(+), 2 deletions(-) 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" } : {}), }; }