From 80b39097020bad1e70eaf2820d2bc1087f87670a Mon Sep 17 00:00:00 2001 From: Wintermute Date: Thu, 30 Apr 2026 18:14:12 +0000 Subject: [PATCH] fix: jsonb double-encoding in subagent_tool_executions (#525) JSON.stringify(input) + ::jsonb cast produced a jsonb string value instead of a jsonb object. The postgres library's unsafe() with a raw object + ::jsonb correctly stores a jsonb object. This caused collectChildPutPageSlugs to return 0 results (can't extract ->>'slug' from a jsonb string), making dream synthesize report '0 pages written' even though subagents successfully wrote 16 pages to the database. Fix: pass objects as-is to executeRaw, let the postgres driver handle serialization. Non-object values wrapped in {_raw: ...} as a safety fallback. --- src/core/minions/handlers/subagent.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/core/minions/handlers/subagent.ts b/src/core/minions/handlers/subagent.ts index 56bb2268a..985632ad7 100644 --- a/src/core/minions/handlers/subagent.ts +++ b/src/core/minions/handlers/subagent.ts @@ -617,11 +617,14 @@ async function persistToolExecPending( toolName: string, input: unknown, ): Promise { + // Pass input as-is (object) so postgres driver serialises to jsonb + // object, not a jsonb string. JSON.stringify() + ::jsonb double-encodes. + const jsonbInput = (input != null && typeof input === 'object') ? input : { _raw: String(input ?? '') }; await engine.executeRaw( `INSERT INTO subagent_tool_executions (job_id, message_idx, tool_use_id, tool_name, input, status) VALUES ($1, $2, $3, $4, $5::jsonb, 'pending') ON CONFLICT (job_id, tool_use_id) DO NOTHING`, - [jobId, messageIdx, toolUseId, toolName, JSON.stringify(input)], + [jobId, messageIdx, toolUseId, toolName, jsonbInput], ); } @@ -631,11 +634,13 @@ async function persistToolExecComplete( toolUseId: string, output: unknown, ): Promise { + // Pass output as-is (object) — see persistToolExecPending comment. + const jsonbOutput = (output != null && typeof output === 'object') ? output : { _raw: String(output ?? '') }; await engine.executeRaw( `UPDATE subagent_tool_executions SET status = 'complete', output = $3::jsonb, ended_at = now() WHERE job_id = $1 AND tool_use_id = $2`, - [jobId, toolUseId, JSON.stringify(output)], + [jobId, toolUseId, jsonbOutput], ); } @@ -655,7 +660,9 @@ async function persistToolExecFailed( VALUES ($1, $2, $3, $4, $5::jsonb, 'failed', $6, now()) ON CONFLICT (job_id, tool_use_id) DO UPDATE SET status = 'failed', error = EXCLUDED.error, ended_at = now()`, - [jobId, messageIdx, toolUseId, toolName, JSON.stringify(input), error], + [jobId, messageIdx, toolUseId, toolName, + (input != null && typeof input === 'object') ? input : { _raw: String(input ?? '') }, + error], ); }