storyboard-bridge 0.9.49 → 0.9.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.mjs CHANGED
@@ -149,14 +149,41 @@ function runCli(bin, args, stdinInput, cwd) {
149
149
  // Surface the STDOUT content, not just its length: claude-code prints its real failure there (usage/rate
150
150
  // limit, model overload after retries, a refusal, an error JSON) — often with stderr EMPTY. Without this,
151
151
  // "produced 1107b stdout" hid the actual reason. Show both so the failure is diagnosable from the app.
152
- const outHead = out.slice(0, 700).trim();
153
- reject(new Error(`${bin} ${how} after ${secs}s — stdout(${out.length}b): ${outHead || '(empty)'}; stderr: ${err.slice(0, 300).trim() || '(empty)'}`));
152
+ //
153
+ // When that stdout IS the CLI's json envelope, read the diagnosis out of it instead of printing the
154
+ // first 700 raw bytes. Those bytes are mostly token counters — an observed failure spent its whole
155
+ // budget on `duration_api_ms`, `usage`, `cache_creation` and friends and cut off before `result`,
156
+ // the one field carrying the human sentence. terminal_reason:"api_error" with zero tokens and zero
157
+ // cost means the API refused the request before any inference: an expired login or a usage limit,
158
+ // not anything this bridge or the prompt did.
159
+ reject(new Error(`${bin} ${how} after ${secs}s — ${describeCliOutput(out)}; stderr: ${err.slice(0, 300).trim() || '(empty)'}`));
154
160
  });
155
161
  // Feed the prompt via STDIN (not argv) — see runJob for why this matters on Windows.
156
162
  if (stdinInput != null) child.stdin.write(stdinInput);
157
163
  child.stdin.end();
158
164
  });
159
165
  }
166
+ /** The diagnosis inside a CLI json envelope, or the raw head when it is not one. */
167
+ export function describeCliOutput(out) {
168
+ const raw = String(out ?? '');
169
+ let o;
170
+ try { o = JSON.parse(raw); } catch { o = null; }
171
+ if (!o || typeof o !== 'object') return `stdout(${raw.length}b): ${raw.slice(0, 700).trim() || '(empty)'}`;
172
+ const parts = [];
173
+ // the human sentence first — it is the only field that ever says WHY
174
+ for (const k of ['result', 'error', 'message']) {
175
+ const v = o[k];
176
+ if (typeof v === 'string' && v.trim()) { parts.push(v.trim().slice(0, 400)); break; }
177
+ if (v && typeof v === 'object') { parts.push(JSON.stringify(v).slice(0, 400)); break; }
178
+ }
179
+ for (const k of ['subtype', 'terminal_reason', 'stop_reason']) {
180
+ if (typeof o[k] === 'string' && o[k]) parts.push(`${k}=${o[k]}`);
181
+ }
182
+ const inTok = o.usage?.input_tokens ?? null, outTok = o.usage?.output_tokens ?? null;
183
+ if (inTok === 0 && outTok === 0) parts.push('0 tokens billed — the API refused the request before inference (expired login or usage limit)');
184
+ return parts.length ? parts.join(' · ') : `stdout(${raw.length}b): ${raw.slice(0, 700).trim()}`;
185
+ }
186
+
160
187
  // claude wraps the answer as { result }, gemini as { response }; read either, else raw stdout.
161
188
  function extractText(stdout) {
162
189
  try {
package/newface_client.py CHANGED
@@ -311,7 +311,12 @@ def poll(session, mids, timeout=1800, interval=5, prefix=()):
311
311
  else:
312
312
  pend += 1
313
313
  pct = int(sum(int((content.get(m) or {}).get("progress") or 0) for m in mids) / max(1, len(mids)))
314
- key = f"{len(done)}:{pct}"
314
+ # The dedupe key MUST include elapsed time. The hub gives a render 30 minutes and refreshes that
315
+ # window on every progress event, so a key of only "done:pct" stops emitting the moment a batch
316
+ # plateaus — and a slow batch sitting at 0/15 · 0% therefore starved the heartbeat and was killed as
317
+ # "newface render timed out" while it was still legitimately rendering. creaa_client already carries
318
+ # a per-minute component for exactly this reason; this is the same bug, one provider later.
319
+ key = f"{len(done)}:{pct}:{int(time.time() - (end - timeout)) // 60}"
315
320
  if key != last_key:
316
321
  last_key = key
317
322
  # Ship the urls of everything finished SO FAR, not just a count. A batch finishes at wildly
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "storyboard-bridge",
3
- "version": "0.9.49",
3
+ "version": "0.9.51",
4
4
  "description": "Desktop bridge that powers a hosted Storyboard AI webapp with your own local Claude Code / Gemini CLI login (no API key).",
5
5
  "type": "module",
6
6
  "bin": {