@vibedev-si/dsh-vibedev 0.2.6 → 0.2.7

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.
@@ -1,4 +1,4 @@
1
- THIRD-PARTY NOTICES · @vibedev-si/dsh-vibedev 0.2.6
1
+ THIRD-PARTY NOTICES · @vibedev-si/dsh-vibedev 0.2.7
2
2
 
3
3
  This package bundles code from the projects below. Each is used under the licence that
4
4
  follows its name; the licence texts are reproduced as the projects ship them.
@@ -71,6 +71,13 @@ export interface VideoCapabilities {
71
71
  readonly minReferenceVideoSeconds?: number;
72
72
  readonly maxReferenceVideoSeconds?: number;
73
73
  readonly maxTotalReferenceVideoSeconds?: number;
74
+ /**
75
+ * Declared limits on a reference AUDIO's own measured length. They are read
76
+ * from the model, never assumed: a lane that declares nothing is not held to
77
+ * another lane's range (some providers take 1–15 s, others take minutes).
78
+ */
79
+ readonly minReferenceAudioSeconds?: number;
80
+ readonly maxReferenceAudioSeconds?: number;
74
81
  /** References must be relayed through the gateway media library. */
75
82
  readonly gatewayRelayRequired?: boolean;
76
83
  }
@@ -19,6 +19,19 @@ export interface MediaErrorDetails {
19
19
  readonly retryAfterMs?: number;
20
20
  /** The gateway was full and created nothing (a busy 429/503). */
21
21
  readonly busy?: boolean;
22
+ /**
23
+ * The gateway's structured submission state: `rejected` (the provider refused
24
+ * the request; nothing exists under the key), `unknown` (the gateway cannot
25
+ * tell whether the provider received it) or `accepted` (a task exists).
26
+ * Absent on the older protocol, where the status is all there is.
27
+ */
28
+ readonly submissionState?: string;
29
+ /**
30
+ * What the gateway asks next: `none` (do not try again), `reconcile` (settle
31
+ * the same key's outcome, do not create a task) or `retry`. `retryable` alone
32
+ * only says recovery is still needed; it never means "create another task".
33
+ */
34
+ readonly retryAction?: string;
22
35
  /** On a task-limit refusal: tasks in progress and the account's limit. */
23
36
  readonly active?: number;
24
37
  readonly limit?: number;
@@ -48,6 +61,8 @@ export interface GatewayFailure {
48
61
  readonly message?: string;
49
62
  readonly field?: string;
50
63
  readonly retryable?: boolean;
64
+ readonly submissionState?: string;
65
+ readonly retryAction?: string;
51
66
  readonly rechargeUrl?: string;
52
67
  readonly active?: number;
53
68
  readonly limit?: number;
package/lib/index.js CHANGED
@@ -297,11 +297,15 @@ function parseGatewayFailure(body) {
297
297
  availableCny: amount$1(source.available_cny),
298
298
  pendingCny: amount$1(source.pending_cny)
299
299
  };
300
+ const submissionState = text$7(source.submission_state) ?? text$7(root.submission_state);
301
+ const retryAction = text$7(source.retry_action) ?? text$7(root.retry_action);
300
302
  return {
301
303
  ...code === void 0 ? {} : { code },
302
304
  ...message === void 0 ? {} : { message },
303
305
  ...field === void 0 ? {} : { field },
304
306
  ...typeof source.retryable === "boolean" ? { retryable: source.retryable } : {},
307
+ ...submissionState === void 0 ? {} : { submissionState: submissionState.toLowerCase() },
308
+ ...retryAction === void 0 ? {} : { retryAction: retryAction.toLowerCase() },
305
309
  ...rechargeUrl === void 0 ? {} : { rechargeUrl },
306
310
  ...Object.fromEntries(Object.entries({
307
311
  ...numbers,
@@ -3345,6 +3349,8 @@ function parseVideoCapabilities(raw) {
3345
3349
  ["maxAssetBytes", "max_asset_bytes"],
3346
3350
  ["minReferenceVideoSeconds", "min_reference_video_duration_seconds"],
3347
3351
  ["maxReferenceVideoSeconds", "max_reference_video_duration_seconds"],
3352
+ ["minReferenceAudioSeconds", "min_reference_audio_duration_seconds"],
3353
+ ["maxReferenceAudioSeconds", "max_reference_audio_duration_seconds"],
3348
3354
  ["maxTotalReferenceVideoSeconds", "max_total_reference_video_duration_seconds"]
3349
3355
  ]) {
3350
3356
  const value = positive(field(raw, snake));
@@ -4524,7 +4530,7 @@ function startTaskJob(runtime, exec, record) {
4524
4530
  /**
4525
4531
  * Submit a task and start its report. An unconfirmed submission is not an
4526
4532
  * error here: the agent must not resubmit it, so it is reported like any
4527
- * other task while the tracker resends it under the same key.
4533
+ * other task while the tracker settles that one submission under the same key.
4528
4534
  * @param runtime - the media runtime.
4529
4535
  * @param exec - the tool call.
4530
4536
  * @param draft - the task to submit.
@@ -4548,7 +4554,7 @@ async function submitTask(runtime, exec, draft) {
4548
4554
  };
4549
4555
  }
4550
4556
  /** The sentence about an unconfirmed submission. */
4551
- const UNCONFIRMED_NOTE = "The gateway did not confirm the submission; dsh-vibedev keeps resending it for a few minutes under the same request key, so it cannot be created twice. Do not submit it again.";
4557
+ const UNCONFIRMED_NOTE = "The gateway did not confirm the submission; dsh-vibedev keeps settling that same submission under the same request key for a few minutes, so no second task can be created from it. Do not submit it again.";
4552
4558
  /**
4553
4559
  * Wait for a task in the tool call itself, for hosts without background jobs.
4554
4560
  * @param runtime - the media runtime.
@@ -4934,6 +4940,7 @@ function describeVideoModel(model) {
4934
4940
  const limits = [];
4935
4941
  if (video.minReferenceVideoSeconds !== void 0 || video.maxReferenceVideoSeconds !== void 0) limits.push(`each reference video ${video.minReferenceVideoSeconds ?? 0}-${video.maxReferenceVideoSeconds ?? "∞"} s`);
4936
4942
  if (video.maxTotalReferenceVideoSeconds !== void 0) limits.push(`reference videos at most ${video.maxTotalReferenceVideoSeconds} s in total`);
4943
+ if (video.minReferenceAudioSeconds !== void 0 || video.maxReferenceAudioSeconds !== void 0) limits.push(`each reference audio ${video.minReferenceAudioSeconds ?? 0}-${video.maxReferenceAudioSeconds ?? "∞"} s`);
4937
4944
  if (video.combinations !== void 0) limits.push(`allowed combinations: ${video.combinations.map((c) => [
4938
4945
  c.duration === void 0 ? "" : `${c.duration}s`,
4939
4946
  c.ratio ?? "",
@@ -5091,12 +5098,15 @@ function referenceByteLimit(model, input) {
5091
5098
  return Math.min(kindMax ?? Infinity, video.maxAssetBytes ?? Infinity, MEDIA_LIBRARY_MAX_BYTES);
5092
5099
  }
5093
5100
  /**
5094
- * Check reference video durations once the gateway has measured them.
5101
+ * Check the measured lengths of the reference media once the gateway has
5102
+ * measured them. A reference's OWN length is what is checked — never the
5103
+ * requested output duration — and only against the limits that model declares.
5095
5104
  * @param model - the video model.
5096
5105
  * @param videos - the uploaded reference videos, in order.
5106
+ * @param audios - the uploaded reference audios, in order.
5097
5107
  * @throws {@link MediaError} when a clip or the total is outside the model's limits.
5098
5108
  */
5099
- function checkReferenceDurations(model, videos) {
5109
+ function checkReferenceDurations(model, videos, audios = []) {
5100
5110
  const video = model.video ?? {};
5101
5111
  let totalMs = 0;
5102
5112
  for (const [index, clip] of videos.entries()) {
@@ -5110,6 +5120,16 @@ function checkReferenceDurations(model, videos) {
5110
5120
  totalMs += clip.durationMs;
5111
5121
  }
5112
5122
  if (video.maxTotalReferenceVideoSeconds !== void 0 && totalMs / 1e3 > video.maxTotalReferenceVideoSeconds) throw new MediaError(`The reference videos add up to ${(totalMs / 1e3).toFixed(2)} s; ${model.id} takes at most ${video.maxTotalReferenceVideoSeconds} s in total.`, "REFERENCE_TOTAL_TOO_LONG", { field: "reference_videos" });
5123
+ const audioLimits = video.minReferenceAudioSeconds !== void 0 || video.maxReferenceAudioSeconds !== void 0;
5124
+ for (const [index, clip] of audios.entries()) {
5125
+ if (clip.durationMs === void 0) {
5126
+ if (audioLimits) throw new MediaError(`The gateway could not measure the length of reference audio ${index + 1} ("${clip.source}"), so ${model.id}'s length limits cannot be checked. Export it as a standard MP3 or WAV and try again.`, "REFERENCE_DURATION_UNKNOWN", { field: "reference_audios" });
5127
+ continue;
5128
+ }
5129
+ const seconds = clip.durationMs / 1e3;
5130
+ if (video.minReferenceAudioSeconds !== void 0 && seconds < video.minReferenceAudioSeconds) throw new MediaError(`Reference audio ${index + 1} ("${clip.source}") is ${seconds.toFixed(2)} s; ${model.id} needs at least ${video.minReferenceAudioSeconds} s of reference audio.`, "REFERENCE_AUDIO_TOO_SHORT", { field: "reference_audios" });
5131
+ if (video.maxReferenceAudioSeconds !== void 0 && seconds > video.maxReferenceAudioSeconds) throw new MediaError(`Reference audio ${index + 1} ("${clip.source}") is ${seconds.toFixed(2)} s; ${model.id} takes at most ${video.maxReferenceAudioSeconds} s of reference audio per clip. Trim it first, or use a shorter excerpt.`, "REFERENCE_AUDIO_TOO_LONG", { field: "reference_audios" });
5132
+ }
5113
5133
  }
5114
5134
  /**
5115
5135
  * The `/v1/videos` body for a plan whose inputs are uploaded. The gateway reads
@@ -5260,7 +5280,12 @@ async function prepareVideoTask(runtime, call, request, save) {
5260
5280
  bytes: item.media.data.byteLength,
5261
5281
  ...assets[index]?.durationMs === void 0 ? {} : { durationMs: assets[index]?.durationMs }
5262
5282
  }] : []);
5263
- checkReferenceDurations(model, videos);
5283
+ checkReferenceDurations(model, videos, loaded.flatMap((item, index) => item.input === "referenceAudios" ? [{
5284
+ source: item.media.source,
5285
+ mime: item.media.mime,
5286
+ bytes: item.media.data.byteLength,
5287
+ ...assets[index]?.durationMs === void 0 ? {} : { durationMs: assets[index]?.durationMs }
5288
+ }] : []));
5264
5289
  const urls = {};
5265
5290
  for (const [index, item] of loaded.entries()) {
5266
5291
  const asset = assets[index];
@@ -5898,13 +5923,21 @@ function parseAudioTask(body) {
5898
5923
  * background job, so a task keeps being followed after its job is stopped and
5899
5924
  * after the host restarts.
5900
5925
  *
5901
- * Money rules: a task record is removed only when the first submission was
5902
- * refused outright, so nothing can exist under its key. Every answer that
5903
- * leaves the outcome open (no answer, a gateway error, a 409) keeps the record,
5904
- * and the same request is resent under the same key, which returns the
5905
- * original task when there is one. A resend that is refused outright is
5906
- * retried only briefly, so a refused request is not created later behind the
5907
- * user's back.
5926
+ * Money rules, and the rule that one operation creates at most one task:
5927
+ *
5928
+ * - A submission is settled by the gateway's structured verdict
5929
+ * (`submission_state` and `retry_action`) first, and only by the HTTP status
5930
+ * when it sends none. `rejected`/`retry_action: none` is final: the reason is
5931
+ * saved and nothing is sent again — not by the tool, not by the background
5932
+ * loop, not after a restart. `retryable` on its own never authorizes creating
5933
+ * another task.
5934
+ * - An unknown outcome keeps the record and settles it under the SAME key and
5935
+ * the SAME body, which the gateway answers with the original task when there
5936
+ * is one. A new key is minted only for a new request the user asks for.
5937
+ * - Once a gateway task id is known the task is only polled; a failed poll
5938
+ * never submits anything.
5939
+ * - A refusal that left nothing under the key ends the record as failed with
5940
+ * the gateway's own reason, so the person sees why.
5908
5941
  * @module dsh-vibedev/tasks/tracker
5909
5942
  */
5910
5943
  const SPECS = {
@@ -5945,34 +5978,74 @@ const SUBMIT_TIMEOUT_MS = 3e5;
5945
5978
  const SUBMISSION_AMBIGUOUS = "VIDEO_SUBMISSION_AMBIGUOUS";
5946
5979
  /** The same key was sent with a different body: a plugin bug, never retried. */
5947
5980
  const KEY_CONFLICT = /_IDEMPOTENCY_CONFLICT$/;
5981
+ /** Structured verdicts the gateway sends; an unknown word falls back to the older protocol. */
5982
+ const REJECTED_STATES = /* @__PURE__ */ new Set([
5983
+ "rejected",
5984
+ "refused",
5985
+ "denied"
5986
+ ]);
5987
+ const RECONCILE_STATES = /* @__PURE__ */ new Set(["unknown", "accepted"]);
5988
+ /** `retry_action` values that keep the same key: a lookup of this submission, never a new one. */
5989
+ const SAME_KEY_ACTIONS = /* @__PURE__ */ new Set([
5990
+ "reconcile",
5991
+ "retry_same_key",
5992
+ "retry_same"
5993
+ ]);
5948
5994
  /**
5949
- * Whether a failed submission may still have created the task: no answer, an
5950
- * unreadable answer, a gateway error that is not "busy", or a 409 such as
5951
- * `VIDEO_SUBMISSION_IN_PROGRESS` (an earlier attempt is still being submitted).
5995
+ * What a failed submission means.
5996
+ *
5997
+ * The two STOP rules are read before anything that would keep the operation
5998
+ * alive, so no verdict can talk the plugin into sending a refused request
5999
+ * again: `retry_action: none` (the provider refused it) and
6000
+ * `VIDEO_SUBMISSION_AMBIGUOUS` (the gateway's key answers this forever) end the
6001
+ * operation even when the same answer also says the outcome is unknown.
6002
+ *
6003
+ * After those, the gateway's structured verdict decides, and only when it
6004
+ * sends none do the HTTP status and the older codes. `retryable` says recovery
6005
+ * is still needed; it never authorizes creating another task.
5952
6006
  * @param error - the failure.
5953
- * @returns true when the same key should be sent again.
6007
+ * @returns how it may be taken further.
5954
6008
  */
5955
- function outcomeUnknown(error) {
5956
- if (!(error instanceof MediaError)) return false;
5957
- if (NO_ANSWER_CODES.has(error.code)) return true;
5958
- if (error.code === SUBMISSION_AMBIGUOUS || KEY_CONFLICT.test(error.code)) return false;
5959
- const status = error.details.status;
5960
- return status === 409 || status !== void 0 && status >= 500 && error.details.busy !== true;
6009
+ function submissionRecovery(error) {
6010
+ if (!(error instanceof MediaError)) return "rejected";
6011
+ const { submissionState, retryAction, status, busy, retryable } = error.details;
6012
+ if (retryAction === "none") return "rejected";
6013
+ if (error.code === SUBMISSION_AMBIGUOUS || KEY_CONFLICT.test(error.code)) return "rejected";
6014
+ if (submissionState !== void 0 && REJECTED_STATES.has(submissionState)) return "rejected";
6015
+ if (retryAction !== void 0 && SAME_KEY_ACTIONS.has(retryAction)) return "reconcile";
6016
+ if (retryAction === "retry") return "wait";
6017
+ if (submissionState !== void 0 && RECONCILE_STATES.has(submissionState)) return "reconcile";
6018
+ if (NO_ANSWER_CODES.has(error.code)) return "reconcile";
6019
+ if (busy === true) return "wait";
6020
+ if (status !== void 0 && status >= 400 && status < 500) return status === 409 ? "reconcile" : "rejected";
6021
+ if (status !== void 0 && status >= 500) return retryable === false ? "rejected" : "reconcile";
6022
+ return "rejected";
5961
6023
  }
5962
6024
  /**
5963
- * The record of a submission the gateway settled for good without a task.
6025
+ * The failure to record when a submission is refused for good.
6026
+ *
6027
+ * None of these tells the person to generate again: in the two cases where the
6028
+ * provider may already hold the request, a new generation would be a second
6029
+ * paid task, so the wording asks them to check the records first.
5964
6030
  * @param error - the failure.
5965
- * @returns the failure to record, or undefined when the error is not one of these.
6031
+ * @returns the code and message to keep with the task.
5966
6032
  */
5967
- function settledWithoutTask(error) {
5968
- if (!(error instanceof MediaError)) return void 0;
6033
+ function terminalFailure(error) {
6034
+ if (!(error instanceof MediaError)) return {
6035
+ code: "SUBMISSION_REJECTED",
6036
+ message: messageOf(error)
6037
+ };
5969
6038
  if (error.code === SUBMISSION_AMBIGUOUS) return {
5970
6039
  code: error.code,
5971
- message: "The gateway could not confirm the submission with the provider, so it created no task and charged nothing. Submit the request again if it is still wanted."
6040
+ message: "The gateway could not confirm this submission with the provider, so it created no task and charged nothing, and this request key will answer the same way. If the provider did receive it, that task is not visible here. Check the task list and the account usage before generating the same thing again."
5972
6041
  };
5973
6042
  if (KEY_CONFLICT.test(error.code)) return {
5974
6043
  code: error.code,
5975
- message: "The gateway refused the request: its key was already used for a different request. Nothing was charged; submit it again."
6044
+ message: "The gateway refused the request: its key was already used for a different request. Nothing was charged. Check the task list for the earlier request before generating again."
6045
+ };
6046
+ return {
6047
+ code: error.code,
6048
+ message: error.message
5976
6049
  };
5977
6050
  }
5978
6051
  function sleepFor(ms, signal) {
@@ -6085,8 +6158,14 @@ var TaskTracker = class {
6085
6158
  await this.releaseClaim(draft.id);
6086
6159
  throw error;
6087
6160
  }
6088
- const settled = settledWithoutTask(error);
6089
- if (settled !== void 0) {
6161
+ const recovery = submissionRecovery(error);
6162
+ if (recovery === "rejected") {
6163
+ const settled = terminalFailure(error);
6164
+ if (!unknown) {
6165
+ this.dropClaim(draft.id);
6166
+ await this.options.store.remove(draft.id);
6167
+ throw error;
6168
+ }
6090
6169
  await this.finish(draft.id, {
6091
6170
  status: "failed",
6092
6171
  error: settled
@@ -6094,14 +6173,10 @@ var TaskTracker = class {
6094
6173
  await this.releaseClaim(draft.id);
6095
6174
  throw new MediaError(settled.message, settled.code, { retryable: false }, { cause: error });
6096
6175
  }
6097
- if (!outcomeUnknown(error)) {
6098
- if (!unknown) {
6099
- this.dropClaim(draft.id);
6100
- await this.options.store.remove(draft.id);
6101
- throw error;
6102
- }
6103
- lastError = error;
6104
- break;
6176
+ if (!unknown && recovery === "wait") {
6177
+ this.dropClaim(draft.id);
6178
+ await this.options.store.remove(draft.id);
6179
+ throw error;
6105
6180
  }
6106
6181
  unknown = true;
6107
6182
  lastError = error;
@@ -6129,7 +6204,7 @@ var TaskTracker = class {
6129
6204
  }
6130
6205
  }
6131
6206
  this.followQuietly(draft.id);
6132
- throw new MediaError(`The VibeDev gateway did not confirm the request (${messageOf(lastError)}). dsh-vibedev keeps resending it for a few minutes under the same request key, so it cannot be created twice, and saves the result if it runs. Check media_tasks for task ${draft.id}; do not submit the same request again.`, "SUBMISSION_UNCONFIRMED", { retryable: false });
6207
+ throw new MediaError(`The VibeDev gateway did not confirm the request (${messageOf(lastError)}). dsh-vibedev keeps settling it under the same request key for a few minutes — that key can only ever answer with the one task it may have created — and saves the result if it runs. Check media_tasks for task ${draft.id}; do not submit the same request again.`, "SUBMISSION_UNCONFIRMED", { retryable: false });
6133
6208
  }
6134
6209
  /**
6135
6210
  * Follow a recorded task until it ends or the tracker is disposed. A task
@@ -6295,24 +6370,25 @@ var TaskTracker = class {
6295
6370
  await this.update(record.id, { progress: "waiting for a VibeDev sign-in" });
6296
6371
  return 6e4;
6297
6372
  }
6298
- const settled = submitting ? settledWithoutTask(error) : void 0;
6299
- if (settled !== void 0) {
6300
- await this.finish(record.id, {
6301
- status: "failed",
6302
- error: settled
6303
- });
6304
- return;
6305
- }
6306
- if (submitting && !outcomeUnknown(error) && status !== void 0 && status >= 400 && status < 500) {
6307
- if (this.now() - record.createdAt < (this.options.refusalGraceMs ?? 18e4)) return 3e4;
6308
- await this.finish(record.id, {
6309
- status: "failed",
6310
- error: {
6311
- code: code ?? `HTTP_${status}`,
6312
- message: messageOf(error)
6313
- }
6314
- });
6315
- return;
6373
+ if (submitting) {
6374
+ if (submissionRecovery(error) === "rejected") {
6375
+ await this.finish(record.id, {
6376
+ status: "failed",
6377
+ error: terminalFailure(error)
6378
+ });
6379
+ return;
6380
+ }
6381
+ if (failures >= CONSECUTIVE_FAILURE_LIMIT) {
6382
+ await this.finish(record.id, {
6383
+ status: "failed",
6384
+ error: {
6385
+ code: code ?? "SUBMISSION_FAILED",
6386
+ message: `The submission could not be settled ${failures} times in a row: ${messageOf(error)}`
6387
+ }
6388
+ });
6389
+ return;
6390
+ }
6391
+ return Math.min(6e4, 5e3 * 2 ** Math.min(failures - 1, 4));
6316
6392
  }
6317
6393
  if (status === 402) {
6318
6394
  await this.update(record.id, { progress: "paused: the VibeDev balance is used up; top up to receive the result" });
@@ -5,13 +5,21 @@
5
5
  * background job, so a task keeps being followed after its job is stopped and
6
6
  * after the host restarts.
7
7
  *
8
- * Money rules: a task record is removed only when the first submission was
9
- * refused outright, so nothing can exist under its key. Every answer that
10
- * leaves the outcome open (no answer, a gateway error, a 409) keeps the record,
11
- * and the same request is resent under the same key, which returns the
12
- * original task when there is one. A resend that is refused outright is
13
- * retried only briefly, so a refused request is not created later behind the
14
- * user's back.
8
+ * Money rules, and the rule that one operation creates at most one task:
9
+ *
10
+ * - A submission is settled by the gateway's structured verdict
11
+ * (`submission_state` and `retry_action`) first, and only by the HTTP status
12
+ * when it sends none. `rejected`/`retry_action: none` is final: the reason is
13
+ * saved and nothing is sent again — not by the tool, not by the background
14
+ * loop, not after a restart. `retryable` on its own never authorizes creating
15
+ * another task.
16
+ * - An unknown outcome keeps the record and settles it under the SAME key and
17
+ * the SAME body, which the gateway answers with the original task when there
18
+ * is one. A new key is minted only for a new request the user asks for.
19
+ * - Once a gateway task id is known the task is only polled; a failed poll
20
+ * never submits anything.
21
+ * - A refusal that left nothing under the key ends the record as failed with
22
+ * the gateway's own reason, so the person sees why.
15
23
  * @module dsh-vibedev/tasks/tracker
16
24
  */
17
25
  import type { GatewayHttp } from '../gateway/http.js';
@@ -27,13 +35,12 @@ export interface TrackerOptions {
27
35
  readonly sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
28
36
  /** Give up on a task this long after it was created. Defaults to 3 hours. */
29
37
  readonly maxAgeMs?: number;
30
- /** An unconfirmed submission is resent until it is this old. Defaults to 5 minutes. */
31
- readonly resendWindowMs?: number;
32
38
  /**
33
- * A resend refused outright is retried until the task is this old, in case
34
- * an earlier attempt is still being created. Defaults to 3 minutes.
39
+ * How long an unconfirmed submission is reconciled under its own key before
40
+ * the task is given up. Defaults to 5 minutes: this settles what became of
41
+ * that one submission, it never creates another task.
35
42
  */
36
- readonly refusalGraceMs?: number;
43
+ readonly resendWindowMs?: number;
37
44
  /** Download attempts for a finished task before its links are reported instead. Defaults to 5. */
38
45
  readonly downloadAttempts?: number;
39
46
  /** The largest output downloaded. Defaults to 2 GiB. */
@@ -42,13 +49,34 @@ export interface TrackerOptions {
42
49
  readonly claimStaleMs?: number;
43
50
  }
44
51
  /**
45
- * Whether a failed submission may still have created the task: no answer, an
46
- * unreadable answer, a gateway error that is not "busy", or a 409 such as
47
- * `VIDEO_SUBMISSION_IN_PROGRESS` (an earlier attempt is still being submitted).
52
+ * How a failed submission may be taken further.
53
+ *
54
+ * - `rejected`: the request is refused for good (a parameter refusal, a
55
+ * provider-side rejection, a key conflict); nothing exists under the key and
56
+ * nothing may be created again for this operation.
57
+ * - `reconcile`: the outcome is unknown — the same key and the same body are
58
+ * sent again only to settle what became of that one submission. This never
59
+ * mints a new key and never asks the provider for a new task.
60
+ * - `wait`: nothing was created and the same request may be sent later (busy,
61
+ * rate limited, no balance); the key stays the same.
62
+ */
63
+ export type SubmissionRecovery = 'rejected' | 'reconcile' | 'wait';
64
+ /**
65
+ * What a failed submission means.
66
+ *
67
+ * The two STOP rules are read before anything that would keep the operation
68
+ * alive, so no verdict can talk the plugin into sending a refused request
69
+ * again: `retry_action: none` (the provider refused it) and
70
+ * `VIDEO_SUBMISSION_AMBIGUOUS` (the gateway's key answers this forever) end the
71
+ * operation even when the same answer also says the outcome is unknown.
72
+ *
73
+ * After those, the gateway's structured verdict decides, and only when it
74
+ * sends none do the HTTP status and the older codes. `retryable` says recovery
75
+ * is still needed; it never authorizes creating another task.
48
76
  * @param error - the failure.
49
- * @returns true when the same key should be sent again.
77
+ * @returns how it may be taken further.
50
78
  */
51
- export declare function outcomeUnknown(error: unknown): boolean;
79
+ export declare function submissionRecovery(error: unknown): SubmissionRecovery;
52
80
  /**
53
81
  * The live progress line of a running task.
54
82
  * @param task - the gateway's view.
@@ -58,7 +58,7 @@ export interface Submitted {
58
58
  /**
59
59
  * Submit a task and start its report. An unconfirmed submission is not an
60
60
  * error here: the agent must not resubmit it, so it is reported like any
61
- * other task while the tracker resends it under the same key.
61
+ * other task while the tracker settles that one submission under the same key.
62
62
  * @param runtime - the media runtime.
63
63
  * @param exec - the tool call.
64
64
  * @param draft - the task to submit.
@@ -78,12 +78,15 @@ export declare function checkReferenceFile(model: MediaModel, input: VideoInput,
78
78
  */
79
79
  export declare function referenceByteLimit(model: MediaModel, input: VideoInput): number;
80
80
  /**
81
- * Check reference video durations once the gateway has measured them.
81
+ * Check the measured lengths of the reference media once the gateway has
82
+ * measured them. A reference's OWN length is what is checked — never the
83
+ * requested output duration — and only against the limits that model declares.
82
84
  * @param model - the video model.
83
85
  * @param videos - the uploaded reference videos, in order.
86
+ * @param audios - the uploaded reference audios, in order.
84
87
  * @throws {@link MediaError} when a clip or the total is outside the model's limits.
85
88
  */
86
- export declare function checkReferenceDurations(model: MediaModel, videos: readonly ReferenceMedia[]): void;
89
+ export declare function checkReferenceDurations(model: MediaModel, videos: readonly ReferenceMedia[], audios?: readonly ReferenceMedia[]): void;
87
90
  /**
88
91
  * The `/v1/videos` body for a plan whose inputs are uploaded. The gateway reads
89
92
  * one vocabulary and translates it per provider.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibedev-si/dsh-vibedev",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "VibeDev 账号与模型:在 DeepSeek Harness 里登录 VibeDev,与 DeepSeek 账号并存,使用 VibeDev 网关的模型(Claude、GPT 等),并生成图片、视频、音乐和播客 · VibeDev account and models for DeepSeek Harness: sign in to VibeDev next to the DeepSeek account and use the VibeDev gateway's models (Claude, GPT and more), plus image, video, music and podcast generation",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",