@telepat/ideon 0.1.29 → 0.1.31

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/dist/ideon.js CHANGED
@@ -552,7 +552,8 @@ var baseT2ISettingsSchema = z2.preprocess(
552
552
  z2.object({
553
553
  modelId: z2.string().default(DEFAULT_LIMN_MODEL_ID),
554
554
  replicateModelId: z2.string().optional(),
555
- inputOverrides: z2.record(z2.string(), z2.unknown()).default({})
555
+ inputOverrides: z2.record(z2.string(), z2.unknown()).default({}),
556
+ maxAttempts: z2.number().int().min(1).max(10).default(4)
556
557
  })
557
558
  );
558
559
  var notificationsSettingsSchema = z2.object({
@@ -562,6 +563,7 @@ var appSettingsSchema = z2.object({
562
563
  model: z2.string().default("deepseek/deepseek-v4-pro"),
563
564
  modelSettings: modelSettingsSchema.default(modelSettingsSchema.parse({})),
564
565
  modelRequestTimeoutMs: z2.number().int().positive().default(9e4),
566
+ modelRequestMaxAttempts: z2.number().int().min(1).max(10).default(4),
565
567
  t2i: baseT2ISettingsSchema.default(baseT2ISettingsSchema.parse({})),
566
568
  notifications: notificationsSettingsSchema.default(notificationsSettingsSchema.parse({})),
567
569
  contentTargets: z2.array(contentTargetSchema).min(1).refine((targets) => targets.filter((target) => target.role === "primary").length === 1, {
@@ -580,6 +582,7 @@ var envSettingsSchema = z2.object({
580
582
  maxTokens: z2.number().int().positive().optional(),
581
583
  topP: z2.number().min(0).max(1).optional(),
582
584
  modelRequestTimeoutMs: z2.number().int().positive().optional(),
585
+ modelRequestMaxAttempts: z2.number().int().min(1).max(10).optional(),
583
586
  notificationsEnabled: z2.boolean().optional(),
584
587
  style: z2.enum(writingStyleValues).optional(),
585
588
  intent: z2.enum(contentIntentValues).optional(),
@@ -624,6 +627,7 @@ function readEnvSettings(env = process.env) {
624
627
  maxTokens: parseNumber(env.IDEON_MAX_TOKENS),
625
628
  topP: parseNumber(env.IDEON_TOP_P),
626
629
  modelRequestTimeoutMs: parseNumber(env.IDEON_MODEL_REQUEST_TIMEOUT_MS),
630
+ modelRequestMaxAttempts: parseNumber(env.IDEON_MODEL_REQUEST_MAX_ATTEMPTS),
627
631
  notificationsEnabled: parseBoolean(env.IDEON_NOTIFICATIONS_ENABLED),
628
632
  style: env.IDEON_STYLE,
629
633
  intent: env.IDEON_INTENT,
@@ -1424,7 +1428,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
1424
1428
  // package.json
1425
1429
  var package_default = {
1426
1430
  name: "@telepat/ideon",
1427
- version: "0.1.29",
1431
+ version: "0.1.31",
1428
1432
  description: "CLI for generating rich articles and images from ideas.",
1429
1433
  type: "module",
1430
1434
  repository: {
@@ -1461,6 +1465,7 @@ var package_default = {
1461
1465
  test: "NODE_OPTIONS=--experimental-vm-modules jest",
1462
1466
  "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch",
1463
1467
  "test:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --coverage",
1468
+ "guides:sync": "node scripts/sync-writing-guides.mjs",
1464
1469
  "pricing:refresh": "node scripts/refresh-openrouter-pricing.mjs",
1465
1470
  "docs:start": "npm --prefix docs-site run start",
1466
1471
  "docs:start:en": "npm --prefix docs-site run start -- --locale en",
@@ -1546,6 +1551,7 @@ async function resolveRunInput(input) {
1546
1551
  ...job?.settings ?? {},
1547
1552
  ...envSettings.model ? { model: envSettings.model } : {},
1548
1553
  ...envSettings.modelRequestTimeoutMs !== void 0 ? { modelRequestTimeoutMs: envSettings.modelRequestTimeoutMs } : {},
1554
+ ...envSettings.modelRequestMaxAttempts !== void 0 ? { modelRequestMaxAttempts: envSettings.modelRequestMaxAttempts } : {},
1549
1555
  ...envSettings.notificationsEnabled !== void 0 ? {
1550
1556
  notifications: {
1551
1557
  ...savedSettings.notifications,
@@ -3180,6 +3186,212 @@ function buildImagePromptMessages(plan, image, section) {
3180
3186
  ];
3181
3187
  }
3182
3188
 
3189
+ // src/llm/retry.ts
3190
+ var DEFAULT_BASE_BACKOFF_MS = 1500;
3191
+ var DEFAULT_MAX_BACKOFF_MS = 6e4;
3192
+ var DEFAULT_JITTER_MS = 250;
3193
+ var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([408, 409, 425, 429]);
3194
+ var TRANSIENT_ERROR_PATTERN = /timeout|network|fetch|temporarily|aborted|ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|ETIMEDOUT|socket hang up/i;
3195
+ async function withRetry(op, opts) {
3196
+ const maxAttempts = Math.max(1, Math.floor(opts.maxAttempts));
3197
+ const baseBackoffMs = opts.baseBackoffMs ?? DEFAULT_BASE_BACKOFF_MS;
3198
+ const maxBackoffMs = opts.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS;
3199
+ const jitterMs = opts.jitterMs ?? DEFAULT_JITTER_MS;
3200
+ const sleep = opts.sleep ?? defaultSleep;
3201
+ const randomFraction = opts.randomFraction ?? Math.random;
3202
+ let lastError;
3203
+ let lastClassification = null;
3204
+ let attemptsMade = 0;
3205
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
3206
+ attemptsMade = attempt;
3207
+ try {
3208
+ return await op(attempt);
3209
+ } catch (error) {
3210
+ lastError = error;
3211
+ const classification = classifyHttpError(error);
3212
+ lastClassification = classification;
3213
+ if (!classification.retryable || attempt >= maxAttempts) {
3214
+ break;
3215
+ }
3216
+ const delayMs = computeDelayMs({
3217
+ retryAfterMs: classification.retryAfterMs,
3218
+ attempt,
3219
+ baseBackoffMs,
3220
+ maxBackoffMs,
3221
+ jitterMs,
3222
+ randomFraction
3223
+ });
3224
+ opts.onRetry?.({
3225
+ attempt,
3226
+ delayMs,
3227
+ reason: classification.reason,
3228
+ statusCode: classification.statusCode
3229
+ });
3230
+ await sleep(delayMs);
3231
+ }
3232
+ }
3233
+ throw buildFinalError(opts.operationLabel, attemptsMade, lastError, lastClassification);
3234
+ }
3235
+ function computeDelayMs(input) {
3236
+ if (typeof input.retryAfterMs === "number" && input.retryAfterMs > 0) {
3237
+ return Math.min(input.maxBackoffMs, input.retryAfterMs);
3238
+ }
3239
+ const exponential = input.baseBackoffMs * 2 ** (input.attempt - 1);
3240
+ const capped = Math.min(input.maxBackoffMs, exponential);
3241
+ const jitter = input.jitterMs > 0 ? input.randomFraction() * input.jitterMs : 0;
3242
+ return Math.floor(capped + jitter);
3243
+ }
3244
+ function classifyHttpError(error) {
3245
+ if (!error) {
3246
+ return { retryable: true, reason: "Empty error value treated as transient." };
3247
+ }
3248
+ const message = errorMessage(error);
3249
+ const statusFromObject = extractStatusFromObject(error);
3250
+ const statusFromMessage = statusFromObject ?? extractStatusFromMessage(message);
3251
+ const retryAfterMs = extractRetryAfterMs(error, message);
3252
+ if (statusFromMessage !== void 0) {
3253
+ if (RETRYABLE_STATUS_CODES.has(statusFromMessage) || statusFromMessage >= 500) {
3254
+ return {
3255
+ retryable: true,
3256
+ statusCode: statusFromMessage,
3257
+ retryAfterMs,
3258
+ reason: `HTTP ${statusFromMessage}`
3259
+ };
3260
+ }
3261
+ return {
3262
+ retryable: false,
3263
+ statusCode: statusFromMessage,
3264
+ reason: `HTTP ${statusFromMessage}`
3265
+ };
3266
+ }
3267
+ if (TRANSIENT_ERROR_PATTERN.test(message)) {
3268
+ return {
3269
+ retryable: true,
3270
+ retryAfterMs,
3271
+ reason: "Transient network or timeout error."
3272
+ };
3273
+ }
3274
+ return {
3275
+ retryable: true,
3276
+ retryAfterMs,
3277
+ reason: "Unknown error treated as transient."
3278
+ };
3279
+ }
3280
+ function extractStatusFromObject(error) {
3281
+ if (typeof error !== "object" || error === null) {
3282
+ return void 0;
3283
+ }
3284
+ const record = error;
3285
+ const direct = numberFrom(record.status);
3286
+ if (direct !== void 0) {
3287
+ return direct;
3288
+ }
3289
+ const response = record.response;
3290
+ if (typeof response === "object" && response !== null) {
3291
+ const responseStatus = numberFrom(response.status);
3292
+ if (responseStatus !== void 0) {
3293
+ return responseStatus;
3294
+ }
3295
+ }
3296
+ return void 0;
3297
+ }
3298
+ function extractStatusFromMessage(message) {
3299
+ const match = message.match(/status\s+(\d{3})/i);
3300
+ if (!match) {
3301
+ return void 0;
3302
+ }
3303
+ const parsed = Number.parseInt(match[1], 10);
3304
+ return Number.isInteger(parsed) ? parsed : void 0;
3305
+ }
3306
+ function extractRetryAfterMs(error, message) {
3307
+ const headerSeconds = extractRetryAfterHeader(error);
3308
+ if (headerSeconds !== void 0) {
3309
+ return Math.round(headerSeconds * 1e3);
3310
+ }
3311
+ const bodyMatch = message.match(/"?retry_after"?\s*[:=]\s*(\d+(?:\.\d+)?)/i);
3312
+ if (bodyMatch) {
3313
+ const seconds = Number.parseFloat(bodyMatch[1]);
3314
+ if (Number.isFinite(seconds) && seconds > 0) {
3315
+ return Math.round(seconds * 1e3);
3316
+ }
3317
+ }
3318
+ return void 0;
3319
+ }
3320
+ function extractRetryAfterHeader(error) {
3321
+ if (typeof error !== "object" || error === null) {
3322
+ return void 0;
3323
+ }
3324
+ const response = error.response;
3325
+ if (typeof response !== "object" || response === null) {
3326
+ return void 0;
3327
+ }
3328
+ const headers = response.headers;
3329
+ if (!headers) {
3330
+ return void 0;
3331
+ }
3332
+ let rawValue;
3333
+ if (typeof headers === "object" && typeof headers.get === "function") {
3334
+ rawValue = headers.get("retry-after");
3335
+ } else if (typeof headers === "object") {
3336
+ const entries = headers;
3337
+ rawValue = entries["retry-after"] ?? entries["Retry-After"];
3338
+ }
3339
+ if (typeof rawValue !== "string" && typeof rawValue !== "number") {
3340
+ return void 0;
3341
+ }
3342
+ const stringValue = String(rawValue).trim();
3343
+ if (!stringValue) {
3344
+ return void 0;
3345
+ }
3346
+ const numeric = Number.parseFloat(stringValue);
3347
+ if (Number.isFinite(numeric) && numeric > 0) {
3348
+ return numeric;
3349
+ }
3350
+ const dateMs = Date.parse(stringValue);
3351
+ if (Number.isFinite(dateMs)) {
3352
+ const diffSeconds = (dateMs - Date.now()) / 1e3;
3353
+ return diffSeconds > 0 ? diffSeconds : void 0;
3354
+ }
3355
+ return void 0;
3356
+ }
3357
+ function numberFrom(value2) {
3358
+ if (typeof value2 === "number" && Number.isFinite(value2)) {
3359
+ return value2;
3360
+ }
3361
+ if (typeof value2 === "string") {
3362
+ const parsed = Number.parseFloat(value2);
3363
+ return Number.isFinite(parsed) ? parsed : void 0;
3364
+ }
3365
+ return void 0;
3366
+ }
3367
+ function errorMessage(error) {
3368
+ if (error instanceof Error) {
3369
+ return error.message;
3370
+ }
3371
+ if (typeof error === "string") {
3372
+ return error;
3373
+ }
3374
+ try {
3375
+ return JSON.stringify(error);
3376
+ } catch {
3377
+ return String(error);
3378
+ }
3379
+ }
3380
+ function buildFinalError(operationLabel, attempts, cause, classification) {
3381
+ const detail = errorMessage(cause) || classification?.reason || "unknown error";
3382
+ const message = `${operationLabel} failed after ${attempts} attempt${attempts === 1 ? "" : "s"}: ${detail}`;
3383
+ const wrapped = new Error(message, cause instanceof Error ? { cause } : void 0);
3384
+ if (!(cause instanceof Error) && cause !== void 0) {
3385
+ wrapped.cause = cause;
3386
+ }
3387
+ return wrapped;
3388
+ }
3389
+ function defaultSleep(ms) {
3390
+ return new Promise((resolve) => {
3391
+ setTimeout(resolve, ms);
3392
+ });
3393
+ }
3394
+
3183
3395
  // src/pipeline/analytics.ts
3184
3396
  var LLM_USD_PER_1K_TOKENS = {
3185
3397
  // AUTO-GENERATED:OPENROUTER_PRICING_START
@@ -3432,8 +3644,26 @@ async function renderExpandedImages({
3432
3644
  ...replicateModelOverride && isReplicateModelIdForFamily(family, replicateModelOverride) ? { replicateModel: replicateModelOverride } : {}
3433
3645
  };
3434
3646
  const renderStartedAtMs = Date.now();
3647
+ let attemptsMade = 0;
3648
+ let retryCount = 0;
3649
+ let retryBackoffMs = 0;
3435
3650
  try {
3436
- const result = await limn.generate(prompt.prompt, family, limnOptions);
3651
+ const result = await withRetry(
3652
+ () => {
3653
+ attemptsMade += 1;
3654
+ return limn.generate(prompt.prompt, family, limnOptions);
3655
+ },
3656
+ {
3657
+ operationLabel: `Replicate ${prompt.kind} image (${prompt.id})`,
3658
+ maxAttempts: settings.t2i.maxAttempts,
3659
+ baseBackoffMs: 1500,
3660
+ maxBackoffMs: 6e4,
3661
+ onRetry({ delayMs }) {
3662
+ retryCount += 1;
3663
+ retryBackoffMs += delayMs;
3664
+ }
3665
+ }
3666
+ );
3437
3667
  const ext = mimeTypeToExtension(result.mimeType);
3438
3668
  const liveFileName = `${prompt.kind === "cover" ? "cover" : `inline-${prompt.anchorAfterSection}`}-${index + 1}.${ext}`;
3439
3669
  const liveOutputPath = path6.join(assetDir, liveFileName);
@@ -3455,9 +3685,9 @@ async function renderExpandedImages({
3455
3685
  kind: prompt.kind,
3456
3686
  modelId: result.modelSlug,
3457
3687
  durationMs: result.analytics.totalDurationMs,
3458
- attempts: 1,
3459
- retries: 0,
3460
- retryBackoffMs: 0,
3688
+ attempts: attemptsMade,
3689
+ retries: retryCount,
3690
+ retryBackoffMs,
3461
3691
  outputBytes: result.image.byteLength,
3462
3692
  costUsd: result.analytics.totalEstimatedCostUsd,
3463
3693
  costSource
@@ -3471,9 +3701,9 @@ async function renderExpandedImages({
3471
3701
  startedAt: new Date(renderStartedAtMs).toISOString(),
3472
3702
  endedAt: (/* @__PURE__ */ new Date()).toISOString(),
3473
3703
  durationMs: result.analytics.totalDurationMs,
3474
- attempts: 1,
3475
- retries: 0,
3476
- retryBackoffMs: 0,
3704
+ attempts: attemptsMade,
3705
+ retries: retryCount,
3706
+ retryBackoffMs,
3477
3707
  status: "succeeded",
3478
3708
  prompt: prompt.prompt,
3479
3709
  input: {},
@@ -3490,9 +3720,9 @@ async function renderExpandedImages({
3490
3720
  startedAt: new Date(renderStartedAtMs).toISOString(),
3491
3721
  endedAt: (/* @__PURE__ */ new Date()).toISOString(),
3492
3722
  durationMs,
3493
- attempts: 1,
3494
- retries: 0,
3495
- retryBackoffMs: 0,
3723
+ attempts: Math.max(attemptsMade, 1),
3724
+ retries: retryCount,
3725
+ retryBackoffMs,
3496
3726
  status: "failed",
3497
3727
  prompt: prompt.prompt,
3498
3728
  input: {},
@@ -3552,8 +3782,9 @@ var OpenRouterClient = class {
3552
3782
  }
3553
3783
  async requestStructured(request) {
3554
3784
  let aggregatedMetrics = null;
3785
+ const maxParseAttempts = Math.max(1, request.settings.modelRequestMaxAttempts);
3555
3786
  try {
3556
- for (let attempt = 0; attempt < 3; attempt += 1) {
3787
+ for (let attempt = 0; attempt < maxParseAttempts; attempt += 1) {
3557
3788
  const response = await this.sendCompletion({
3558
3789
  messages: request.messages,
3559
3790
  settings: request.settings,
@@ -3578,7 +3809,7 @@ var OpenRouterClient = class {
3578
3809
  request.onMetrics?.(aggregatedMetrics);
3579
3810
  return structured;
3580
3811
  } catch (parseError) {
3581
- if (attempt < 2 && shouldRetryStructuredParseError(parseError)) {
3812
+ if (attempt < maxParseAttempts - 1 && shouldRetryStructuredParseError(parseError)) {
3582
3813
  const backoff = backoffMs(attempt);
3583
3814
  aggregatedMetrics = recordParseRetryMetrics(aggregatedMetrics, backoff);
3584
3815
  await wait(backoff);
@@ -3644,7 +3875,8 @@ var OpenRouterClient = class {
3644
3875
  let attempts = 0;
3645
3876
  let retries = 0;
3646
3877
  let retryBackoffMs = 0;
3647
- for (let attempt = 0; attempt < 3; attempt += 1) {
3878
+ const maxAttempts = Math.max(1, settings.modelRequestMaxAttempts);
3879
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
3648
3880
  attempts = attempt + 1;
3649
3881
  const attemptStartedAtMs = Date.now();
3650
3882
  const controller = new AbortController();
@@ -3691,9 +3923,12 @@ var OpenRouterClient = class {
3691
3923
  responseBodyRaw = rawBody;
3692
3924
  const json = parseOpenRouterResponse(rawBody);
3693
3925
  if (!response.ok) {
3694
- const message = json?.error?.message ?? `OpenRouter request failed with status ${response.status}`;
3695
- if (shouldRetryStatus(response.status) && attempt < 2) {
3696
- const backoff = backoffMs(attempt);
3926
+ const providerMessage = json?.error?.message ?? `OpenRouter request failed with status ${response.status}`;
3927
+ const raw = json?.error?.metadata?.raw;
3928
+ const message = raw ? `${raw} (OpenRouter: ${providerMessage})` : providerMessage;
3929
+ if (shouldRetryStatus(response.status) && attempt < maxAttempts - 1) {
3930
+ const advisedMs = extractRetryAfterFromResponse(response, json, rawBody);
3931
+ const backoff = advisedMs !== null ? Math.min(MAX_RETRY_BACKOFF_MS, advisedMs) : backoffMs(attempt);
3697
3932
  retries += 1;
3698
3933
  retryBackoffMs += backoff;
3699
3934
  onInteraction?.({
@@ -3719,7 +3954,7 @@ var OpenRouterClient = class {
3719
3954
  throw new Error(message);
3720
3955
  }
3721
3956
  const content = json.choices?.[0]?.message?.content;
3722
- if (!content && attempt < 2) {
3957
+ if (!content && attempt < maxAttempts - 1) {
3723
3958
  const backoff = backoffMs(attempt);
3724
3959
  retries += 1;
3725
3960
  retryBackoffMs += backoff;
@@ -3795,7 +4030,7 @@ var OpenRouterClient = class {
3795
4030
  responseBody: responseBodyRaw,
3796
4031
  errorMessage: lastError.message
3797
4032
  });
3798
- if (attempt < 2 && shouldRetryError(lastError)) {
4033
+ if (attempt < maxAttempts - 1 && shouldRetryError(lastError)) {
3799
4034
  const backoff = backoffMs(attempt);
3800
4035
  retries += 1;
3801
4036
  retryBackoffMs += backoff;
@@ -3922,6 +4157,61 @@ function normalizeClientError(error, timeoutMs) {
3922
4157
  function backoffMs(attempt) {
3923
4158
  return 500 * (attempt + 1);
3924
4159
  }
4160
+ var MAX_RETRY_BACKOFF_MS = 6e4;
4161
+ function extractRetryAfterFromResponse(response, json, rawBody) {
4162
+ const headerValue = typeof response.headers?.get === "function" ? response.headers.get("retry-after") : null;
4163
+ if (headerValue) {
4164
+ const numeric = Number.parseFloat(headerValue);
4165
+ if (Number.isFinite(numeric) && numeric > 0) {
4166
+ return Math.round(numeric * 1e3);
4167
+ }
4168
+ const dateMs = Date.parse(headerValue);
4169
+ if (Number.isFinite(dateMs)) {
4170
+ const diff = dateMs - Date.now();
4171
+ if (diff > 0) {
4172
+ return diff;
4173
+ }
4174
+ }
4175
+ }
4176
+ const metadata = json?.error?.metadata;
4177
+ if (metadata) {
4178
+ const fromMetadata = metadata["retry_after"] ?? metadata["retryAfter"];
4179
+ const numeric = toFiniteNumber(fromMetadata);
4180
+ if (numeric !== null && numeric > 0) {
4181
+ return Math.round(numeric * 1e3);
4182
+ }
4183
+ const rawCandidate = metadata["raw"];
4184
+ if (typeof rawCandidate === "string") {
4185
+ const fromRaw = extractRetryAfterFromString(rawCandidate);
4186
+ if (fromRaw !== null) {
4187
+ return fromRaw;
4188
+ }
4189
+ }
4190
+ }
4191
+ const fromBody = extractRetryAfterFromString(rawBody);
4192
+ if (fromBody !== null) {
4193
+ return fromBody;
4194
+ }
4195
+ return null;
4196
+ }
4197
+ function extractRetryAfterFromString(value2) {
4198
+ const bodyMatch = value2.match(/\\?"retry_after\\?"\s*:\s*(\d+(?:\.\d+)?)/i);
4199
+ if (!bodyMatch) {
4200
+ return null;
4201
+ }
4202
+ const numeric = Number.parseFloat(bodyMatch[1]);
4203
+ return Number.isFinite(numeric) && numeric > 0 ? Math.round(numeric * 1e3) : null;
4204
+ }
4205
+ function toFiniteNumber(value2) {
4206
+ if (typeof value2 === "number" && Number.isFinite(value2)) {
4207
+ return value2;
4208
+ }
4209
+ if (typeof value2 === "string") {
4210
+ const parsed = Number.parseFloat(value2);
4211
+ return Number.isFinite(parsed) ? parsed : null;
4212
+ }
4213
+ return null;
4214
+ }
3925
4215
  function aggregateLlmMetrics(total, next) {
3926
4216
  if (!total) {
3927
4217
  return { ...next };
@@ -4330,7 +4620,7 @@ async function runPipelineShell(input, options = {}) {
4330
4620
  const llmInteractions = [];
4331
4621
  const t2iInteractions = [];
4332
4622
  let writeSession;
4333
- const applyRetryUpdate = (stageId, retryIncrement, errorMessage) => {
4623
+ const applyRetryUpdate = (stageId, retryIncrement, errorMessage2) => {
4334
4624
  if (retryIncrement <= 0) {
4335
4625
  return;
4336
4626
  }
@@ -4341,7 +4631,7 @@ async function runPipelineShell(input, options = {}) {
4341
4631
  const existing = stageRetryState.get(stageId) ?? { retries: 0, lastError: null };
4342
4632
  const next = {
4343
4633
  retries: existing.retries + retryIncrement,
4344
- lastError: errorMessage && errorMessage.trim().length > 0 ? errorMessage : existing.lastError
4634
+ lastError: errorMessage2 && errorMessage2.trim().length > 0 ? errorMessage2 : existing.lastError
4345
4635
  };
4346
4636
  stageRetryState.set(stageId, next);
4347
4637
  stages[stageIndex] = {
@@ -7381,7 +7671,8 @@ function SettingsFlow({ initialSettings, initialSecrets, onDone }) {
7381
7671
  t2i: {
7382
7672
  modelId: item.value,
7383
7673
  replicateModelId: current.t2i.replicateModelId && isReplicateModelIdForFamily(item.value, current.t2i.replicateModelId) ? current.t2i.replicateModelId : void 0,
7384
- inputOverrides: {}
7674
+ inputOverrides: {},
7675
+ maxAttempts: current.t2i.maxAttempts
7385
7676
  }
7386
7677
  }));
7387
7678
  setShowModelSelect(false);
@@ -9201,6 +9492,10 @@ function renderShell({
9201
9492
  \`<code class="slug-text">\${escapeHtml(output.slug)}</code>\`,
9202
9493
  \`<button class="copy-btn" data-copy-slug="\${escapeHtml(output.slug)}" type="button">Copy slug</button>\`,
9203
9494
  '</div>',
9495
+ '<div class="slug-row">',
9496
+ \`<code class="slug-text">\${escapeHtml(currentGeneration.generationId)}</code>\`,
9497
+ \`<button class="copy-btn" data-copy-generation-id="\${escapeHtml(currentGeneration.generationId)}" type="button">Copy generation ID</button>\`,
9498
+ '</div>',
9204
9499
  \`<div class="channel-meta">\${escapeHtml(output.contentTypeLabel)} \u2022 Variant \${output.index}</div>\`,
9205
9500
  '</div>',
9206
9501
  '</div>',
@@ -9312,6 +9607,12 @@ function renderShell({
9312
9607
  const copyButton = target.closest('[data-copy-slug]');
9313
9608
  if (copyButton instanceof HTMLElement && copyButton.dataset.copySlug) {
9314
9609
  copySlug(copyButton, copyButton.dataset.copySlug);
9610
+ return;
9611
+ }
9612
+
9613
+ const copyGenerationIdButton = target.closest('[data-copy-generation-id]');
9614
+ if (copyGenerationIdButton instanceof HTMLElement && copyGenerationIdButton.dataset.copyGenerationId) {
9615
+ copySlug(copyGenerationIdButton, copyGenerationIdButton.dataset.copyGenerationId);
9315
9616
  }
9316
9617
  });
9317
9618
 
@@ -9697,13 +9998,13 @@ function PipelinePresenter({
9697
9998
  prompt,
9698
9999
  stages,
9699
10000
  result,
9700
- errorMessage
10001
+ errorMessage: errorMessage2
9701
10002
  }) {
9702
10003
  const visibleStages = getVisibleStages(stages);
9703
10004
  const maxVisibleItems = computeMaxVisibleItemsPerStage({
9704
10005
  terminalRows: process.stdout.rows,
9705
10006
  visibleStageCount: visibleStages.length,
9706
- hasError: Boolean(errorMessage),
10007
+ hasError: Boolean(errorMessage2),
9707
10008
  hasResult: Boolean(result)
9708
10009
  });
9709
10010
  return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", paddingX: 1, children: [
@@ -9719,7 +10020,7 @@ function PipelinePresenter({
9719
10020
  },
9720
10021
  stage.id
9721
10022
  )) }),
9722
- errorMessage ? /* @__PURE__ */ jsx5(Box4, { marginTop: 1, borderStyle: "round", borderColor: "red", paddingX: 1, children: /* @__PURE__ */ jsx5(Text4, { color: "red", children: errorMessage }) }) : null,
10023
+ errorMessage2 ? /* @__PURE__ */ jsx5(Box4, { marginTop: 1, borderStyle: "round", borderColor: "red", paddingX: 1, children: /* @__PURE__ */ jsx5(Text4, { color: "red", children: errorMessage2 }) }) : null,
9723
10024
  result ? /* @__PURE__ */ jsx5(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx5(FinalSummary, { artifact: result.artifact, analytics: result.analytics }) }) : null
9724
10025
  ] });
9725
10026
  }
@@ -10254,7 +10555,7 @@ function WriteApp({
10254
10555
  () => createInitialStages()
10255
10556
  );
10256
10557
  const [result, setResult] = useState4(null);
10257
- const [errorMessage, setErrorMessage] = useState4(null);
10558
+ const [errorMessage2, setErrorMessage] = useState4(null);
10258
10559
  useEffect3(() => {
10259
10560
  let mounted = true;
10260
10561
  void (async () => {
@@ -10307,7 +10608,7 @@ function WriteApp({
10307
10608
  };
10308
10609
  }, [dryRun, enrichLinks2, input, links, unlinks, maxLinks, maxImages, onError, runMode]);
10309
10610
  useEffect3(() => {
10310
- if (!result && !errorMessage) {
10611
+ if (!result && !errorMessage2) {
10311
10612
  return;
10312
10613
  }
10313
10614
  const exitTimer = setTimeout(() => {
@@ -10316,8 +10617,8 @@ function WriteApp({
10316
10617
  return () => {
10317
10618
  clearTimeout(exitTimer);
10318
10619
  };
10319
- }, [errorMessage, exit, result]);
10320
- return /* @__PURE__ */ jsx7(PipelinePresenter, { prompt: input.idea, stages, result, errorMessage });
10620
+ }, [errorMessage2, exit, result]);
10621
+ return /* @__PURE__ */ jsx7(PipelinePresenter, { prompt: input.idea, stages, result, errorMessage: errorMessage2 });
10321
10622
  }
10322
10623
  async function runWriteCommand(options) {
10323
10624
  const input = await resolveInputWithInteractiveIdeaFallback(options);