← back to Wallco Ai
chat: surface psql stderr + redact replicate token + throw on null INSERT id
4622641523680130bffb4b65ae59a7f690d644c4 · 2026-05-14 09:35:42 -0700 · SteveStudio2
The chat-driven generate path was silently returning {id: null} when the
spoon_all_designs INSERT failed (column drift on prod). Three fixes:
1. psql() now uses spawnSync so we can see stderr and throw on non-zero exit.
Previous execSync-only path swallowed PG errors as exit 0 with empty stdout.
2. toolGenerateDesign throws with stdout+exit+stderr context if RETURNING id
isn't a finite number, instead of leaking null down the response chain.
3. Replicate token now travels via env var (REPLICATE_TOKEN) rather than
inlined into the shell command, so curl error output can't leak the token
in API responses to the client. Matching change for both submit and poll
curl calls. Also dropped -sf in favor of -sS so HTTP-error bodies surface.
Surfaced the prod-side schema drift (18 missing columns on spoon_all_designs)
that was causing the silent failures — fix queued in /tmp/wallco_schema_parity.sql
pending Steve sign-off on prod migration.
Files touched
Diff
commit 4622641523680130bffb4b65ae59a7f690d644c4
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Thu May 14 09:35:42 2026 -0700
chat: surface psql stderr + redact replicate token + throw on null INSERT id
The chat-driven generate path was silently returning {id: null} when the
spoon_all_designs INSERT failed (column drift on prod). Three fixes:
1. psql() now uses spawnSync so we can see stderr and throw on non-zero exit.
Previous execSync-only path swallowed PG errors as exit 0 with empty stdout.
2. toolGenerateDesign throws with stdout+exit+stderr context if RETURNING id
isn't a finite number, instead of leaking null down the response chain.
3. Replicate token now travels via env var (REPLICATE_TOKEN) rather than
inlined into the shell command, so curl error output can't leak the token
in API responses to the client. Matching change for both submit and poll
curl calls. Also dropped -sf in favor of -sS so HTTP-error bodies surface.
Surfaced the prod-side schema drift (18 missing columns on spoon_all_designs)
that was causing the silent failures — fix queued in /tmp/wallco_schema_parity.sql
pending Steve sign-off on prod migration.
---
src/chat.js | 37 ++++++++++++++++++++++++++++++-------
1 file changed, 30 insertions(+), 7 deletions(-)
diff --git a/src/chat.js b/src/chat.js
index bd413cb..4ff2de2 100644
--- a/src/chat.js
+++ b/src/chat.js
@@ -42,7 +42,15 @@ const PSQL_CMD = (process.platform === 'linux')
: `psql dw_unified -At -q`;
function psql(sql) {
- return execSync(PSQL_CMD, { input: sql, encoding: 'utf8' }).trim();
+ const r = spawnSync('/bin/sh', ['-c', PSQL_CMD], { input: sql, encoding: 'utf8' });
+ if (r.status !== 0) {
+ const tag = sql.length > 200 ? sql.slice(0, 200) + '…' : sql;
+ throw new Error(`psql exit ${r.status}: ${(r.stderr || '').trim() || '(no stderr)'} | sql: ${tag}`);
+ }
+ if (r.stderr && r.stderr.trim()) {
+ console.error('[psql stderr]', r.stderr.trim());
+ }
+ return (r.stdout || '').trim();
}
function esc(s) { if (s == null) return 'NULL'; return "'" + String(s).replace(/'/g, "''") + "'"; }
@@ -168,16 +176,25 @@ async function toolGenerateDesign({ prompt, category = 'mixed' }, ctx = {}) {
seed, refine: 'expert_ensemble_refiner', apply_watermark: false
}
};
- const submit = execSync(`curl -sf -m 30 -H 'Authorization: Bearer ${TOKEN}' -H 'Content-Type: application/json' -X POST 'https://api.replicate.com/v1/predictions' -d @-`,
- { input: JSON.stringify(body), encoding: 'utf8' });
- const pred = JSON.parse(submit);
- if (!pred.id) throw new Error('replicate no prediction id');
+ let submit;
+ try {
+ submit = execSync(`curl -sS -m 30 -H "Authorization: Bearer $REPLICATE_TOKEN" -H 'Content-Type: application/json' -X POST 'https://api.replicate.com/v1/predictions' -d @-`,
+ { input: JSON.stringify(body), encoding: 'utf8', env: { ...process.env, REPLICATE_TOKEN: TOKEN } });
+ } catch (e) {
+ const msg = String(e.stderr || e.stdout || e.message || '').slice(0, 400);
+ throw new Error(`replicate submit failed: ${msg}`);
+ }
+ let pred;
+ try { pred = JSON.parse(submit); }
+ catch { throw new Error(`replicate non-JSON response: ${String(submit).slice(0, 200)}`); }
+ if (!pred.id) throw new Error(`replicate no prediction id: ${String(submit).slice(0, 200)}`);
// Poll
const start = Date.now();
let final;
while (Date.now() - start < 3 * 60_000) {
execSync('sleep 3');
- const p = JSON.parse(execSync(`curl -sf -m 10 -H 'Authorization: Bearer ${TOKEN}' 'https://api.replicate.com/v1/predictions/${pred.id}'`, { encoding: 'utf8' }));
+ const p = JSON.parse(execSync(`curl -sS -m 10 -H "Authorization: Bearer $REPLICATE_TOKEN" 'https://api.replicate.com/v1/predictions/${pred.id}'`,
+ { encoding: 'utf8', env: { ...process.env, REPLICATE_TOKEN: TOKEN } }));
if (p.status === 'succeeded') { final = p; break; }
if (p.status === 'failed' || p.status === 'canceled') throw new Error(`replicate ${p.status}: ${JSON.stringify(p.error).slice(0, 200)}`);
}
@@ -204,7 +221,13 @@ print(json.dumps(palette))
const promptEsc = prompt.replace(/'/g, "''");
const insertSql = `INSERT INTO spoon_all_designs (kind, width_in, height_in, generator, prompt, seed, dominant_hex, palette, local_path, category, is_published, parent_design_id, chat_session_id, request_text)
VALUES ('seamless_tile', 24, 24, 'replicate', '${promptEsc}', ${seed}, ${dominant ? "'" + dominant + "'" : 'NULL'}, '${palJson}'::jsonb, ${esc(outPath)}, '${category.replace(/'/g,"''")}', FALSE, ${ctx.parent_design_id ? ctx.parent_design_id : 'NULL'}, ${ctx.chat_session_id ? ctx.chat_session_id : 'NULL'}, ${esc(ctx.request_text)}) RETURNING id;`;
- const id = parseInt(psql(insertSql), 10);
+ const probe = spawnSync('/bin/sh', ['-c', PSQL_CMD], { input: insertSql, encoding: 'utf8' });
+ const idRaw = (probe.stdout || '').trim();
+ const id = parseInt(idRaw, 10);
+ if (!Number.isFinite(id)) {
+ const stderr = (probe.stderr || '').trim();
+ throw new Error(`spoon_all_designs INSERT returned non-numeric id (stdout=${JSON.stringify(idRaw)}, exit=${probe.status}, stderr=${JSON.stringify(stderr)}); image saved at ${outPath} but row not persisted`);
+ }
return { id, prompt, dominant_hex: dominant, category, image_url: '/designs/img/' + filename, parent_design_id: ctx.parent_design_id || null };
}
← e850309 fix(/designs): studio-adjust summary precedence bug — state.
·
back to Wallco Ai
·
fix(/designs): move card-hover OUT of card-link so the Sampl 3291dd7 →