@goodandready/dsh-image-gen 0.11.23 → 0.11.25

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.
@@ -340,29 +340,6 @@ export function registerGenerationTools(ctx, deps, options = {}) {
340
340
  // rejection reaches the model as text. Without this guard,
341
341
  // execution would proceed with empty bytes.
342
342
  const guard = (generated) => { if (generated && generated.ok === false) throw new Error(generated.reason) }
343
- const one = async (jobSeed, providerKey, promptArg = effectivePrompt) => {
344
- const gen = await providers[providerKey](jobSeed, promptArg)
345
- guard(gen)
346
- return await saveGeneratedImageArtifact({
347
- ctx,
348
- exec,
349
- cfg,
350
- args,
351
- gen,
352
- promptArg,
353
- jobSeed,
354
- provider,
355
- deliverAs,
356
- size,
357
- format,
358
- effectiveNegative,
359
- effectiveGuidance,
360
- enhanced,
361
- slugify,
362
- estimatedCost,
363
- totalCount,
364
- })
365
- }
366
343
 
367
344
  const seedBase = args.seed ?? Math.floor(Math.random() * 100000)
368
345
  // Fallback cascade: evaluate configured provider, falling back to alternates
@@ -372,8 +349,20 @@ export function registerGenerationTools(ctx, deps, options = {}) {
372
349
  const generators = Object.fromEntries(PROVIDER_KEYS.map((k) => [
373
350
  k,
374
351
  async (s, p) => {
375
- const { generated, qualityReport } = await executeWithQualityGate(
376
- (seedVal) => one(seedVal, k, p),
352
+ let totalAttempts = 0
353
+ let accumulatedCost = 0
354
+ // #353: Pipeline evaluates raw gen.bytes first; saves artifact ONCE after acceptance
355
+ const rawGenerate = async (seedVal) => {
356
+ totalAttempts++
357
+ const gen = await providers[k](seedVal, p)
358
+ guard(gen)
359
+ const attemptCost = Number(gen.cost) || estimatedCost
360
+ accumulatedCost += attemptCost
361
+ return gen
362
+ }
363
+
364
+ const { generated: acceptedGen, qualityReport } = await executeWithQualityGate(
365
+ rawGenerate,
377
366
  {
378
367
  initialSeed: s,
379
368
  enabled: qgEnabled,
@@ -381,7 +370,36 @@ export function registerGenerationTools(ctx, deps, options = {}) {
381
370
  prompt: p,
382
371
  }
383
372
  )
384
- return { ...generated, qualityReport }
373
+
374
+ // Save the accepted artifact ONCE
375
+ const saved = await saveGeneratedImageArtifact({
376
+ ctx,
377
+ exec,
378
+ cfg,
379
+ args,
380
+ gen: acceptedGen,
381
+ promptArg: p,
382
+ jobSeed: qualityReport.seedUsed ?? s,
383
+ provider,
384
+ deliverAs,
385
+ size,
386
+ format,
387
+ effectiveNegative,
388
+ effectiveGuidance,
389
+ enhanced,
390
+ slugify,
391
+ estimatedCost: accumulatedCost || estimatedCost,
392
+ totalCount,
393
+ })
394
+
395
+ return {
396
+ ...saved,
397
+ qualityReport: {
398
+ ...qualityReport,
399
+ attempts: totalAttempts,
400
+ totalCost: accumulatedCost,
401
+ },
402
+ }
385
403
  }
386
404
  ]))
387
405
  const images = []
@@ -400,7 +418,10 @@ export function registerGenerationTools(ctx, deps, options = {}) {
400
418
  prompt: promptVal,
401
419
  seed: seedVal,
402
420
  size: args.aspect_ratio ? 'custom' : (args.image_size || cfg.defaultSize),
403
- style: args.style || cfg.stylePreset,
421
+ style: args.style,
422
+ stylePreset: (args.style_preset !== undefined && args.style_preset !== null && args.style_preset !== '')
423
+ ? args.style_preset
424
+ : (cfg.stylePreset || cfg.defaultStylePreset),
404
425
  format: args.output_format || cfg.defaultFormat || 'png',
405
426
  aspectRatio: args.aspect_ratio || cfg.defaultAspectRatio,
406
427
  negativePrompt: args.negative_prompt || cfg.negativePrompt,
@@ -482,7 +503,7 @@ export function registerGenerationTools(ctx, deps, options = {}) {
482
503
  images.push(...parallelResults)
483
504
  }
484
505
  const first = images[0]
485
- const dualOutputSummary = buildDualOutputMarkdown({
506
+ let dualOutputSummary = buildDualOutputMarkdown({
486
507
  action: 'generated',
487
508
  filePath: first.path,
488
509
  width: first.width,
@@ -494,6 +515,9 @@ export function registerGenerationTools(ctx, deps, options = {}) {
494
515
  cost: first.cost,
495
516
  attachmentId: first.attachment?.attachmentId || 'N/A',
496
517
  })
518
+ if (first.qualityReport?.exhausted) {
519
+ dualOutputSummary += `\n\n> ⚠️ **Quality Gate Warning**: Visual quality checks failed after ${first.qualityReport.attempts || 3} attempts (${first.qualityReport.defect || 'defect detected'}). Image retained with potential defects.`
520
+ }
497
521
  return toLosslessJson({ summary: dualOutputSummary, channel: provider, provider, model: cfg.model || cfg.customModel, _fallback: first._fallback, anchor: activeAnchor ? { label: activeAnchor.label, mode: activeAnchor.mode } : undefined, ...first, images })
498
522
  },
499
523
  }),
package/lib/vault.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { existsSync, unlinkSync } from 'node:fs'
2
- import { readHistory, writeHistory } from './history.js'
2
+ import { readHistory, writeHistory, withHistoryLock } from './history.js'
3
3
  import { isTrustedLocalRequest } from './security.js'
4
4
 
5
5
  /**
@@ -133,24 +133,26 @@ export async function readBoundedRequestBody(req, maxBytes = MAX_VAULT_BODY_BYTE
133
133
 
134
134
  export async function deleteVaultEntry(id) {
135
135
  if (!id) return { success: false, error: 'Missing entry id' }
136
- const entries = await readHistory()
137
- const idx = entries.findIndex((e) => e.id === id || e.attachmentId === id)
138
- if (idx === -1) {
139
- return { success: false, error: 'Vault entry not found' }
140
- }
136
+ return await withHistoryLock(async () => {
137
+ const entries = await readHistory()
138
+ const idx = entries.findIndex((e) => e.id === id || e.attachmentId === id)
139
+ if (idx === -1) {
140
+ return { success: false, error: 'Vault entry not found' }
141
+ }
141
142
 
142
- const [entry] = entries.splice(idx, 1)
143
- if (entry && entry.path) {
144
- try {
145
- unlinkSync(entry.path)
146
- } catch { /* ignore missing file */ }
147
- try {
148
- unlinkSync(entry.path.replace(/\.[^.]+$/, '.json'))
149
- } catch { /* ignore missing sidecar */ }
150
- }
143
+ const [entry] = entries.splice(idx, 1)
144
+ if (entry && entry.path) {
145
+ try {
146
+ unlinkSync(entry.path)
147
+ } catch (_err) { /* ignore missing file */ }
148
+ try {
149
+ unlinkSync(entry.path.replace(/\.[^.]+$/, '.json'))
150
+ } catch (_err) { /* ignore missing sidecar */ }
151
+ }
151
152
 
152
- await writeHistory(entries)
153
- return { success: true, id }
153
+ await writeHistory(entries)
154
+ return { success: true, id }
155
+ })
154
156
  }
155
157
 
156
158
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-image-gen",
3
- "version": "0.11.23",
3
+ "version": "0.11.25",
4
4
  "description": "Image generation for DeepSeek Harness: a generate_image tool with pluggable providers — the FAL queue, any OpenAI-compatible images API, or a ChatGPT/Grok subscription with no API key at all. The picture is shown inline in the conversation; the model receives either a link (works with any chat model) or the image itself (needs dsh-vision-bridge or a vision-capable model).",
5
5
  "keywords": [
6
6
  "deepseek-harness",