openvibers/openvibe.contracts Commits
v0.32.0: media.job.*@1 payloads are an event projection of the job, not the jobs API answer: id, app_id, object_id, type, status, attempts, max_attempts, error_code, cancel_requested, has_result and ISO 8601 UTC times (run_after, decided_at, created_at, updated_at, started_at, finished_at)
Changes
@@@ -36,9 +36,18 @@ Additive: `compat.js` reports no breaking change against v0.31.0.internal): the moderation queue and log, hide and restore. The Tips manifest (0.4.0) lists all three.- `media.job.proposed|queued|started|retrying|succeeded|failed|cancelled@1`: OpenVibe.Media's jobtransitions (`server/events.js` `recordJob`), one contract each like `tools.job.*`. The payload is-- the job as `GET /api/v2/:app/jobs/:id` answers it (`queue.jobPublic`, every field always present,-- times as SQLite `YYYY-MM-DD HH:MM:SS` UTC); a result over 8 KB is `{ omitted: true, reason }`.-- `retrying` carries status `queued`, `started` status `running`. The Media manifest (0.2.0) lists them.++ an event projection of the job (`queue.jobEvent`), every field always present: `id`, `app_id`,++ `object_id`, `type`, `status`, `attempts`, `max_attempts`, `error_code` (the stable code only),++ `cancel_requested`, `has_result`, and `run_after`, `decided_at`, `created_at`, `updated_at`,++ `started_at`, `finished_at` as ISO 8601 UTC (`…Z`) or null. Deliberately left out, because events++ travel beyond the tenant: the tenant's `params`, the caller's `idempotency_key`, the free-text++ `error`, the `result` (a thumbnail URL, of a private VOD too), `created_by`/`decided_by` and++ `owner_user_id` (tenant-local user ids). A consumer GETs the job with a tenant token for those++ (`GET /api/v2/:app/jobs/:id`, unchanged); `has_result` says whether there is a result to fetch.++ `retrying` carries status `queued`, `started` status `running`; `finished_at` is set exactly on++ `succeeded`, `failed` and `cancelled`, `started_at` on everything that ran, `run_after` always on++ `retrying`. No service consumed `media.job.*` before this narrowing. The Media manifest (0.2.0)++ lists them.- `live.stream.started@1` and `live.stream.ended@1` (public: stream id, channel username, displayname, URL and user subject when Live knows it, title, category or null, protocol, NSFW flag,times; `ended` adds `ended_at` and `duration_seconds`) and `live.release.deployed@1` (internal:@@@ -2,32 +2,26 @@"$schema": "https://json-schema.org/draft/2020-12/schema","$id": "https://openvibe.network/contracts/events/payloads/media.job.cancelled.v1.json","title": "MediaJobCancelledPayload",-- "description": "media.job.cancelled v1 (OpenVibe.Media server/jobs/queue.js cancel, markCancelled; server/events.js recordJob). The job was cancelled: a proposal or a queued job at once, by its owner (POST /api/v2/:app/jobs/:id/cancel or DELETE /api/v2/:app/jobs/:id) or by the validator withdrawing a proposal that no longer applies (decided_by system:invariant.scan); a running job once it stopped after its owner asked (cancel_requested true, error_code cancelled). Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",++ "description": "media.job.cancelled v1 (OpenVibe.Media server/jobs/queue.js cancel, markCancelled; server/events.js recordJob). The job was cancelled: a proposal or a queued job at once, by its owner (POST /api/v2/:app/jobs/:id/cancel or DELETE /api/v2/:app/jobs/:id) or by the validator withdrawing a proposal that no longer applies (decided_at set, error_code cancelled when a reason was given); a running job once it stopped after its owner asked (cancel_requested true, error_code cancelled). Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.","type": "object",-- "required": ["id", "app_id", "object_id", "type", "status", "params", "result", "error", "error_code", "attempts", "max_attempts", "run_after", "cancel_requested", "idempotency_key", "created_by", "owner_user_id", "decided_by", "decided_at", "created_at", "updated_at", "started_at", "finished_at"],++ "required": ["id", "app_id", "object_id", "type", "status", "attempts", "max_attempts", "error_code", "cancel_requested", "has_result", "run_after", "decided_at", "created_at", "updated_at", "started_at", "finished_at"],"additionalProperties": false,"properties": {-- "id": { "type": "string", "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$" },++ "id": { "type": "string", "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it." },"app_id": { "type": "string", "minLength": 1, "maxLength": 200, "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)." },"object_id": { "type": ["string", "null"], "maxLength": 200, "description": "The object the job works on; null for a tenant-wide job (invariant.scan)." },"type": { "type": "string", "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$", "maxLength": 80, "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux." },"status": { "const": "cancelled" },-- "params": { "type": "object", "description": "The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent." },-- "result": { "type": ["object", "null"], "description": "null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB." },-- "error": { "type": ["string", "null"], "maxLength": 1000, "description": "The last failure's message (free text), or null." },-- "error_code": { "type": ["string", "null"], "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null." },-- "attempts": { "type": "integer", "minimum": 0 },++ "attempts": { "type": "integer", "minimum": 0, "description": "Attempts started so far." },"max_attempts": { "type": "integer", "minimum": 1, "maximum": 10 },-- "run_after": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$", "description": "A queued job waits until then (retry backoff); null otherwise." },++ "error_code": { "type": ["string", "null"], "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it." },"cancel_requested": { "type": "boolean", "description": "Its owner asked a running job to stop." },-- "idempotency_key": { "type": ["string", "null"], "maxLength": 200, "description": "The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null." },-- "created_by": { "type": ["string", "null"], "description": "svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>." },-- "owner_user_id": { "type": ["integer", "null"], "minimum": 1, "description": "The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject." },-- "decided_by": { "type": ["string", "null"], "description": "Who approved or cancelled a proposal or queued job (same forms as created_by)." },-- "decided_at": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },-- "created_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },-- "updated_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },-- "started_at": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$", "description": "null when the job never ran." },-- "finished_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" }++ "has_result": { "type": "boolean", "description": "A stopped running job left a partial result. The result itself is never in the event: GET the job with a tenant token for it." },++ "run_after": { "type": ["string", "null"], "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "A queued job waits until then (retry backoff); null otherwise." },++ "decided_at": { "type": ["string", "null"], "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise." },++ "created_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" },++ "updated_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" },++ "started_at": { "type": ["string", "null"], "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "The first attempt's start; null when the job was cancelled before it ever ran." },++ "finished_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" }}}@@@ -2,32 +2,26 @@"$schema": "https://json-schema.org/draft/2020-12/schema","$id": "https://openvibe.network/contracts/events/payloads/media.job.failed.v1.json","title": "MediaJobFailedPayload",-- "description": "media.job.failed v1 (OpenVibe.Media server/jobs/queue.js fail; server/events.js recordJob). The job failed for good: a permanent error, or its last attempt (attempts = max_attempts) failed; error and error_code say why. A cancelled job is not a failure and is announced as media.job.cancelled. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",++ "description": "media.job.failed v1 (OpenVibe.Media server/jobs/queue.js fail; server/events.js recordJob). The job failed for good: a permanent error, or its last attempt (attempts = max_attempts) failed; error_code says why when the handler gave a code (GET the job for the message). A cancelled job is not a failure and is announced as media.job.cancelled. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.","type": "object",-- "required": ["id", "app_id", "object_id", "type", "status", "params", "result", "error", "error_code", "attempts", "max_attempts", "run_after", "cancel_requested", "idempotency_key", "created_by", "owner_user_id", "decided_by", "decided_at", "created_at", "updated_at", "started_at", "finished_at"],++ "required": ["id", "app_id", "object_id", "type", "status", "attempts", "max_attempts", "error_code", "cancel_requested", "has_result", "run_after", "decided_at", "created_at", "updated_at", "started_at", "finished_at"],"additionalProperties": false,"properties": {-- "id": { "type": "string", "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$" },++ "id": { "type": "string", "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it." },"app_id": { "type": "string", "minLength": 1, "maxLength": 200, "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)." },"object_id": { "type": ["string", "null"], "maxLength": 200, "description": "The object the job works on; null for a tenant-wide job (invariant.scan)." },"type": { "type": "string", "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$", "maxLength": 80, "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux." },"status": { "const": "failed" },-- "params": { "type": "object", "description": "The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent." },-- "result": { "type": ["object", "null"], "description": "null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB." },-- "error": { "type": "string", "maxLength": 1000, "description": "The failure's message, free text from the handler." },-- "error_code": { "type": ["string", "null"], "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null." },-- "attempts": { "type": "integer", "minimum": 0 },++ "attempts": { "type": "integer", "minimum": 1, "description": "Attempts started, the last failed one included." },"max_attempts": { "type": "integer", "minimum": 1, "maximum": 10 },-- "run_after": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$", "description": "A queued job waits until then (retry backoff); null otherwise." },++ "error_code": { "type": ["string", "null"], "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it." },"cancel_requested": { "type": "boolean", "description": "Its owner asked a running job to stop." },-- "idempotency_key": { "type": ["string", "null"], "maxLength": 200, "description": "The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null." },-- "created_by": { "type": ["string", "null"], "description": "svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>." },-- "owner_user_id": { "type": ["integer", "null"], "minimum": 1, "description": "The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject." },-- "decided_by": { "type": ["string", "null"], "description": "Who approved or cancelled a proposal or queued job (same forms as created_by)." },-- "decided_at": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },-- "created_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },-- "updated_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },-- "started_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },-- "finished_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" }++ "has_result": { "type": "boolean", "description": "The handler left a partial result. The result itself is never in the event: GET the job with a tenant token for it." },++ "run_after": { "type": "null" },++ "decided_at": { "type": ["string", "null"], "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise." },++ "created_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" },++ "updated_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" },++ "started_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "The first attempt's start." },++ "finished_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" }}}@@@ -2,31 +2,25 @@"$schema": "https://json-schema.org/draft/2020-12/schema","$id": "https://openvibe.network/contracts/events/payloads/media.job.proposed.v1.json","title": "MediaJobProposedPayload",-- "description": "media.job.proposed v1 (OpenVibe.Media server/jobs/queue.js enqueue; server/events.js recordJob). A job was proposed and waits for its owner: approving it queues it (media.job.queued), cancelling it ends it (media.job.cancelled); the worker never runs a proposal. The size-invariant validator (invariant.scan, created_by system:invariant.scan) proposes object.split and object.remux this way. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",++ "description": "media.job.proposed v1 (OpenVibe.Media server/jobs/queue.js enqueue; server/events.js recordJob). A job was proposed and waits for its owner: approving it queues it (media.job.queued), cancelling it ends it (media.job.cancelled); the worker never runs a proposal. The size-invariant validator (invariant.scan) proposes object.split and object.remux this way. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.","type": "object",-- "required": ["id", "app_id", "object_id", "type", "status", "params", "result", "error", "error_code", "attempts", "max_attempts", "run_after", "cancel_requested", "idempotency_key", "created_by", "owner_user_id", "decided_by", "decided_at", "created_at", "updated_at", "started_at", "finished_at"],++ "required": ["id", "app_id", "object_id", "type", "status", "attempts", "max_attempts", "error_code", "cancel_requested", "has_result", "run_after", "decided_at", "created_at", "updated_at", "started_at", "finished_at"],"additionalProperties": false,"properties": {-- "id": { "type": "string", "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$" },++ "id": { "type": "string", "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it." },"app_id": { "type": "string", "minLength": 1, "maxLength": 200, "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)." },"object_id": { "type": ["string", "null"], "maxLength": 200, "description": "The object the job works on; null for a tenant-wide job (invariant.scan)." },"type": { "type": "string", "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$", "maxLength": 80, "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux." },"status": { "const": "proposed" },-- "params": { "type": "object", "description": "The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent." },-- "result": { "type": ["object", "null"], "description": "null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB." },-- "error": { "type": ["string", "null"], "maxLength": 1000, "description": "The last failure's message (free text), or null." },-- "error_code": { "type": ["string", "null"], "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null." },-- "attempts": { "type": "integer", "minimum": 0 },++ "attempts": { "type": "integer", "minimum": 0, "description": "Attempts started so far." },"max_attempts": { "type": "integer", "minimum": 1, "maximum": 10 },-- "run_after": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$", "description": "A queued job waits until then (retry backoff); null otherwise." },++ "error_code": { "type": "null" },"cancel_requested": { "type": "boolean", "description": "Its owner asked a running job to stop." },-- "idempotency_key": { "type": ["string", "null"], "maxLength": 200, "description": "The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null." },-- "created_by": { "type": ["string", "null"], "description": "svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>." },-- "owner_user_id": { "type": ["integer", "null"], "minimum": 1, "description": "The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject." },-- "decided_by": { "type": ["string", "null"], "description": "Who approved or cancelled a proposal or queued job (same forms as created_by)." },-- "decided_at": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },-- "created_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },-- "updated_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },++ "has_result": { "const": false, "description": "A proposal has no result." },++ "run_after": { "type": ["string", "null"], "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "A queued job waits until then (retry backoff); null otherwise." },++ "decided_at": { "type": "null", "description": "Nobody has decided yet." },++ "created_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" },++ "updated_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" },"started_at": { "type": "null" },"finished_at": { "type": "null" }}@@@ -2,31 +2,25 @@"$schema": "https://json-schema.org/draft/2020-12/schema","$id": "https://openvibe.network/contracts/events/payloads/media.job.queued.v1.json","title": "MediaJobQueuedPayload",-- "description": "media.job.queued v1 (OpenVibe.Media server/jobs/queue.js enqueue, approve; server/events.js recordJob). A job was accepted and queued: a new job (POST /api/v2/:app/jobs), or a proposal its owner approved (decided_by, decided_at). An Idempotency-Key replay, or a request joined to an identical job that is still active, creates no job and no event. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",++ "description": "media.job.queued v1 (OpenVibe.Media server/jobs/queue.js enqueue, approve; server/events.js recordJob). A job was accepted and queued: a new job (POST /api/v2/:app/jobs), or a proposal its owner approved (decided_at). An Idempotency-Key replay, or a request joined to an identical job that is still active, creates no job and no event. A failed attempt that is queued again is media.job.retrying, not this. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.","type": "object",-- "required": ["id", "app_id", "object_id", "type", "status", "params", "result", "error", "error_code", "attempts", "max_attempts", "run_after", "cancel_requested", "idempotency_key", "created_by", "owner_user_id", "decided_by", "decided_at", "created_at", "updated_at", "started_at", "finished_at"],++ "required": ["id", "app_id", "object_id", "type", "status", "attempts", "max_attempts", "error_code", "cancel_requested", "has_result", "run_after", "decided_at", "created_at", "updated_at", "started_at", "finished_at"],"additionalProperties": false,"properties": {-- "id": { "type": "string", "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$" },++ "id": { "type": "string", "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it." },"app_id": { "type": "string", "minLength": 1, "maxLength": 200, "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)." },"object_id": { "type": ["string", "null"], "maxLength": 200, "description": "The object the job works on; null for a tenant-wide job (invariant.scan)." },"type": { "type": "string", "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$", "maxLength": 80, "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux." },"status": { "const": "queued" },-- "params": { "type": "object", "description": "The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent." },-- "result": { "type": ["object", "null"], "description": "null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB." },-- "error": { "type": ["string", "null"], "maxLength": 1000, "description": "The last failure's message (free text), or null." },-- "error_code": { "type": ["string", "null"], "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null." },-- "attempts": { "type": "integer", "minimum": 0 },++ "attempts": { "type": "integer", "minimum": 0, "description": "Attempts started so far." },"max_attempts": { "type": "integer", "minimum": 1, "maximum": 10 },-- "run_after": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$", "description": "A queued job waits until then (retry backoff); null otherwise." },++ "error_code": { "type": "null" },"cancel_requested": { "type": "boolean", "description": "Its owner asked a running job to stop." },-- "idempotency_key": { "type": ["string", "null"], "maxLength": 200, "description": "The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null." },-- "created_by": { "type": ["string", "null"], "description": "svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>." },-- "owner_user_id": { "type": ["integer", "null"], "minimum": 1, "description": "The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject." },-- "decided_by": { "type": ["string", "null"], "description": "Who approved or cancelled a proposal or queued job (same forms as created_by)." },-- "decided_at": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },-- "created_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },-- "updated_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },++ "has_result": { "const": false, "description": "A job that never ran has no result." },++ "run_after": { "type": ["string", "null"], "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "A queued job waits until then (retry backoff); null otherwise." },++ "decided_at": { "type": ["string", "null"], "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise." },++ "created_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" },++ "updated_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" },"started_at": { "type": "null" },"finished_at": { "type": "null" }}@@@ -2,32 +2,26 @@"$schema": "https://json-schema.org/draft/2020-12/schema","$id": "https://openvibe.network/contracts/events/payloads/media.job.retrying.v1.json","title": "MediaJobRetryingPayload",-- "description": "media.job.retrying v1 (OpenVibe.Media server/jobs/queue.js fail with a retry; server/events.js recordJob). An attempt failed and the job is queued again after a backoff (run_after); error and error_code say why. A running job nobody holds any more (the service restarted, or the worker stopped renewing its lease) is retried this way with error_code interrupted while it has attempts left. status is queued. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",++ "description": "media.job.retrying v1 (OpenVibe.Media server/jobs/queue.js fail with a retry; server/events.js recordJob). An attempt failed and the job is queued again (status queued) after a backoff (run_after); error_code says why when the handler gave a code. A running job nobody holds any more (the service restarted, or the worker stopped renewing its lease) is retried this way with error_code interrupted while it has attempts left. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.","type": "object",-- "required": ["id", "app_id", "object_id", "type", "status", "params", "result", "error", "error_code", "attempts", "max_attempts", "run_after", "cancel_requested", "idempotency_key", "created_by", "owner_user_id", "decided_by", "decided_at", "created_at", "updated_at", "started_at", "finished_at"],++ "required": ["id", "app_id", "object_id", "type", "status", "attempts", "max_attempts", "error_code", "cancel_requested", "has_result", "run_after", "decided_at", "created_at", "updated_at", "started_at", "finished_at"],"additionalProperties": false,"properties": {-- "id": { "type": "string", "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$" },++ "id": { "type": "string", "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it." },"app_id": { "type": "string", "minLength": 1, "maxLength": 200, "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)." },"object_id": { "type": ["string", "null"], "maxLength": 200, "description": "The object the job works on; null for a tenant-wide job (invariant.scan)." },"type": { "type": "string", "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$", "maxLength": 80, "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux." },-- "status": { "const": "queued" },-- "params": { "type": "object", "description": "The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent." },-- "result": { "type": ["object", "null"], "description": "null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB." },-- "error": { "type": "string", "maxLength": 1000, "description": "The failure's message, free text from the handler." },-- "error_code": { "type": ["string", "null"], "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null." },-- "attempts": { "type": "integer", "minimum": 0 },++ "status": { "const": "queued", "description": "retrying carries the job's state, queued." },++ "attempts": { "type": "integer", "minimum": 1, "description": "Attempts started so far, the failed one included." },"max_attempts": { "type": "integer", "minimum": 1, "maximum": 10 },-- "run_after": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$", "description": "Not before this time (the retry backoff)." },++ "error_code": { "type": ["string", "null"], "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it." },"cancel_requested": { "type": "boolean", "description": "Its owner asked a running job to stop." },-- "idempotency_key": { "type": ["string", "null"], "maxLength": 200, "description": "The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null." },-- "created_by": { "type": ["string", "null"], "description": "svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>." },-- "owner_user_id": { "type": ["integer", "null"], "minimum": 1, "description": "The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject." },-- "decided_by": { "type": ["string", "null"], "description": "Who approved or cancelled a proposal or queued job (same forms as created_by)." },-- "decided_at": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },-- "created_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },-- "updated_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },-- "started_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },++ "has_result": { "const": false, "description": "A job waiting for its retry has no result." },++ "run_after": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "Not before this time (the retry backoff)." },++ "decided_at": { "type": ["string", "null"], "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise." },++ "created_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" },++ "updated_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" },++ "started_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "The first attempt's start." },"finished_at": { "type": "null" }}}@@@ -2,32 +2,26 @@"$schema": "https://json-schema.org/draft/2020-12/schema","$id": "https://openvibe.network/contracts/events/payloads/media.job.started.v1.json","title": "MediaJobStartedPayload",-- "description": "media.job.started v1 (OpenVibe.Media server/jobs/queue.js claim; server/events.js recordJob). The worker claimed the job and it is running: attempts counts this attempt, started_at stays the first attempt's start. A job that is retried starts again and is announced again. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",++ "description": "media.job.started v1 (OpenVibe.Media server/jobs/queue.js claim; server/events.js recordJob). The worker claimed the job and it is running (status running): attempts counts this attempt, started_at stays the first attempt's start. A job that is retried starts again and is announced again; error_code is still the previous attempt's until the job finishes. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.","type": "object",-- "required": ["id", "app_id", "object_id", "type", "status", "params", "result", "error", "error_code", "attempts", "max_attempts", "run_after", "cancel_requested", "idempotency_key", "created_by", "owner_user_id", "decided_by", "decided_at", "created_at", "updated_at", "started_at", "finished_at"],++ "required": ["id", "app_id", "object_id", "type", "status", "attempts", "max_attempts", "error_code", "cancel_requested", "has_result", "run_after", "decided_at", "created_at", "updated_at", "started_at", "finished_at"],"additionalProperties": false,"properties": {-- "id": { "type": "string", "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$" },++ "id": { "type": "string", "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it." },"app_id": { "type": "string", "minLength": 1, "maxLength": 200, "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)." },"object_id": { "type": ["string", "null"], "maxLength": 200, "description": "The object the job works on; null for a tenant-wide job (invariant.scan)." },"type": { "type": "string", "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$", "maxLength": 80, "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux." },-- "status": { "const": "running" },-- "params": { "type": "object", "description": "The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent." },-- "result": { "type": ["object", "null"], "description": "null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB." },-- "error": { "type": ["string", "null"], "maxLength": 1000, "description": "The last failure's message (free text), or null." },-- "error_code": { "type": ["string", "null"], "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null." },-- "attempts": { "type": "integer", "minimum": 0 },++ "status": { "const": "running", "description": "started carries the job's state, running." },++ "attempts": { "type": "integer", "minimum": 1, "description": "Attempts started so far, this one included." },"max_attempts": { "type": "integer", "minimum": 1, "maximum": 10 },-- "run_after": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$", "description": "A queued job waits until then (retry backoff); null otherwise." },++ "error_code": { "type": ["string", "null"], "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it." },"cancel_requested": { "type": "boolean", "description": "Its owner asked a running job to stop." },-- "idempotency_key": { "type": ["string", "null"], "maxLength": 200, "description": "The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null." },-- "created_by": { "type": ["string", "null"], "description": "svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>." },-- "owner_user_id": { "type": ["integer", "null"], "minimum": 1, "description": "The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject." },-- "decided_by": { "type": ["string", "null"], "description": "Who approved or cancelled a proposal or queued job (same forms as created_by)." },-- "decided_at": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },-- "created_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },-- "updated_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },-- "started_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },++ "has_result": { "const": false, "description": "A running job has no result yet." },++ "run_after": { "type": "null", "description": "Cleared when the job is claimed." },++ "decided_at": { "type": ["string", "null"], "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise." },++ "created_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" },++ "updated_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" },++ "started_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "The first attempt's start." },"finished_at": { "type": "null" }}}@@@ -2,32 +2,26 @@"$schema": "https://json-schema.org/draft/2020-12/schema","$id": "https://openvibe.network/contracts/events/payloads/media.job.succeeded.v1.json","title": "MediaJobSucceededPayload",-- "description": "media.job.succeeded v1 (OpenVibe.Media server/jobs/queue.js succeed; server/events.js recordJob). The job finished: result is its type's result (thumbnail.regenerate { url, kind, id }, object.split { source_id, parts, … }, object.remux { source_id, object_id, … }, invariant.scan { thresholds, counts, … }). Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",++ "description": "media.job.succeeded v1 (OpenVibe.Media server/jobs/queue.js succeed; server/events.js recordJob). The job finished. Its result (thumbnail.regenerate { url, kind, id }, object.split { source_id, parts, … }, object.remux { source_id, object_id, … }, invariant.scan { thresholds, counts, … }) is not in the event: has_result says whether there is one to GET. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.","type": "object",-- "required": ["id", "app_id", "object_id", "type", "status", "params", "result", "error", "error_code", "attempts", "max_attempts", "run_after", "cancel_requested", "idempotency_key", "created_by", "owner_user_id", "decided_by", "decided_at", "created_at", "updated_at", "started_at", "finished_at"],++ "required": ["id", "app_id", "object_id", "type", "status", "attempts", "max_attempts", "error_code", "cancel_requested", "has_result", "run_after", "decided_at", "created_at", "updated_at", "started_at", "finished_at"],"additionalProperties": false,"properties": {-- "id": { "type": "string", "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$" },++ "id": { "type": "string", "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it." },"app_id": { "type": "string", "minLength": 1, "maxLength": 200, "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)." },"object_id": { "type": ["string", "null"], "maxLength": 200, "description": "The object the job works on; null for a tenant-wide job (invariant.scan)." },"type": { "type": "string", "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$", "maxLength": 80, "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux." },"status": { "const": "succeeded" },-- "params": { "type": "object", "description": "The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent." },-- "result": { "type": ["object", "null"], "description": "The job type's result, or { omitted: true, reason } when it is larger than 8 KB (GET the job for it)." },-- "error": { "type": "null" },-- "error_code": { "type": "null" },-- "attempts": { "type": "integer", "minimum": 0 },++ "attempts": { "type": "integer", "minimum": 1, "description": "Attempts started, the successful one included." },"max_attempts": { "type": "integer", "minimum": 1, "maximum": 10 },-- "run_after": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$", "description": "A queued job waits until then (retry backoff); null otherwise." },++ "error_code": { "type": "null", "description": "A success clears the last failure." },"cancel_requested": { "type": "boolean", "description": "Its owner asked a running job to stop." },-- "idempotency_key": { "type": ["string", "null"], "maxLength": 200, "description": "The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null." },-- "created_by": { "type": ["string", "null"], "description": "svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>." },-- "owner_user_id": { "type": ["integer", "null"], "minimum": 1, "description": "The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject." },-- "decided_by": { "type": ["string", "null"], "description": "Who approved or cancelled a proposal or queued job (same forms as created_by)." },-- "decided_at": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },-- "created_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },-- "updated_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },-- "started_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },-- "finished_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" }++ "has_result": { "type": "boolean", "description": "The job holds a result. The result itself is never in the event: GET the job with a tenant token for it." },++ "run_after": { "type": "null" },++ "decided_at": { "type": ["string", "null"], "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise." },++ "created_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" },++ "updated_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" },++ "started_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "The first attempt's start." },++ "finished_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" }}}nnew file mode 100644@@@ -0,0 +1,19 @@++{++ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live",++ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "type": "object.split",++ "status": "cancelled",++ "attempts": 0,++ "max_attempts": 3,++ "error_code": "cancelled",++ "cancel_requested": false,++ "has_result": false,++ "run_after": null,++ "decided_at": "2026-09-23T18:10:40.000Z",++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:10:40.000Z",++ "started_at": null,++ "finished_at": "2026-09-23T18:10:40.000Z",++ "decided_by": "app:live:user:57"++}nnew file mode 100644@@@ -0,0 +1,18 @@++{++ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live",++ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "type": "object.split",++ "status": "cancelled",++ "attempts": 0,++ "max_attempts": 3,++ "error_code": "cancelled",++ "cancel_requested": false,++ "has_result": false,++ "run_after": null,++ "decided_at": "2026-09-23T18:10:40.000Z",++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:10:40.000Z",++ "started_at": null,++ "finished_at": null++}nnew file mode 100644@@@ -0,0 +1,18 @@++{++ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live",++ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "type": "thumbnail.regenerate",++ "status": "cancelled",++ "attempts": 0,++ "max_attempts": 3,++ "error_code": null,++ "cancel_requested": false,++ "has_result": false,++ "run_after": null,++ "decided_at": "2026-09-23T18:02:30.000Z",++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:02:30.000Z",++ "started_at": null,++ "finished_at": "2026-09-23T18:02:30.000Z"++}@@@ -1,26 +1,18 @@{-- "app_id": "live","id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live","object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ","type": "object.split","status": "cancelled",-- "params": {-- "parts": 3-- },-- "result": null,-- "error": "cancelled by its owner",-- "error_code": "cancelled","attempts": 1,"max_attempts": 3,-- "run_after": null,++ "error_code": "cancelled","cancel_requested": true,-- "idempotency_key": null,-- "created_by": "svc:live",-- "owner_user_id": null,-- "decided_by": "app:live:user:57",++ "has_result": false,++ "run_after": null,"decided_at": null,-- "created_at": "2026-09-23 18:02:11",-- "updated_at": "2026-09-23 18:04:00",-- "started_at": "2026-09-23 18:02:12",-- "finished_at": "2026-09-23 18:04:00"++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:04:00.000Z",++ "started_at": "2026-09-23T18:02:12.000Z",++ "finished_at": "2026-09-23T18:04:00.000Z"}@@@ -1,32 +1,18 @@{-- "app_id": "live","id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live","object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ","type": "object.split","status": "cancelled",-- "params": {-- "source_size_bytes": 9663676416,-- "target_bytes": 2147483648,-- "max_bytes": 4294967296,-- "reason": "public_object_too_large",-- "parts": 5,-- "segment_seconds": 2160,-- "source_duration_seconds": 10800-- },-- "result": null,-- "error": "the object no longer breaks the public size policy",-- "error_code": "cancelled","attempts": 0,"max_attempts": 3,-- "run_after": null,++ "error_code": "cancelled","cancel_requested": false,-- "idempotency_key": "invariant:med_01JAB2C3D4E5F6G7H8J9K0MNPQ:object.split",-- "created_by": "system:invariant.scan",-- "owner_user_id": null,-- "decided_by": "system:invariant.scan",-- "decided_at": "2026-09-24 04:00:03",-- "created_at": "2026-09-23 18:02:11",-- "updated_at": "2026-09-24 04:00:03",++ "has_result": false,++ "run_after": null,++ "decided_at": "2026-09-24T04:00:03.000Z",++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-24T04:00:03.000Z","started_at": null,-- "finished_at": "2026-09-24 04:00:03"++ "finished_at": "2026-09-24T04:00:03.000Z"}nnew file mode 100644@@@ -0,0 +1,19 @@++{++ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live",++ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "type": "object.split",++ "status": "failed",++ "attempts": 3,++ "max_attempts": 3,++ "error_code": "media_unavailable",++ "cancel_requested": false,++ "has_result": false,++ "run_after": null,++ "decided_at": null,++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:02:13.000Z",++ "started_at": "2026-09-23T18:02:12.000Z",++ "finished_at": "2026-09-23T18:02:13.000Z",++ "error": "The source bytes are unavailable (no local file and no cloud copy)"++}nnew file mode 100644@@@ -0,0 +1,18 @@++{++ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live",++ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "type": "object.split",++ "status": "failed",++ "attempts": 3,++ "max_attempts": 3,++ "error_code": "media_unavailable",++ "cancel_requested": false,++ "has_result": false,++ "run_after": null,++ "decided_at": null,++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:02:13.000Z",++ "started_at": "2026-09-23T18:02:12.000Z",++ "finished_at": "2026-09-23 18:02:13"++}@@@ -1,26 +1,18 @@{-- "app_id": "live","id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live","object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ","type": "object.split","status": "failed",-- "params": {-- "parts": 3-- },-- "result": null,-- "error": "The source bytes are unavailable (no local file and no cloud copy)",-- "error_code": "media_unavailable","attempts": 3,"max_attempts": 3,-- "run_after": null,++ "error_code": "media_unavailable","cancel_requested": false,-- "idempotency_key": null,-- "created_by": "svc:live",-- "owner_user_id": null,-- "decided_by": null,++ "has_result": false,++ "run_after": null,"decided_at": null,-- "created_at": "2026-09-23 18:02:11",-- "updated_at": "2026-09-23 18:02:13",-- "started_at": "2026-09-23 18:02:12",++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:02:13.000Z",++ "started_at": "2026-09-23T18:02:12.000Z","finished_at": null}@@@ -1,26 +1,18 @@{-- "app_id": "live","id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live","object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ","type": "object.split","status": "failed",-- "params": {-- "parts": 3-- },-- "result": null,-- "error": "The source bytes are unavailable (no local file and no cloud copy)",-- "error_code": "media_unavailable","attempts": 3,"max_attempts": 3,-- "run_after": null,++ "error_code": "media_unavailable","cancel_requested": false,-- "idempotency_key": null,-- "created_by": "svc:live",-- "owner_user_id": null,-- "decided_by": null,++ "has_result": false,++ "run_after": null,"decided_at": null,-- "created_at": "2026-09-23 18:02:11",-- "updated_at": "2026-09-23 18:02:13",-- "started_at": "2026-09-23 18:02:12",-- "finished_at": "2026-09-23 18:02:13"++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:02:13.000Z",++ "started_at": "2026-09-23T18:02:12.000Z",++ "finished_at": "2026-09-23T18:02:13.000Z"}nnew file mode 100644@@@ -0,0 +1,18 @@++{++ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live",++ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "type": "object.split",++ "status": "failed",++ "attempts": 3,++ "max_attempts": 3,++ "error_code": null,++ "cancel_requested": false,++ "has_result": true,++ "run_after": null,++ "decided_at": null,++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:12:13.000Z",++ "started_at": "2026-09-23T18:02:12.000Z",++ "finished_at": "2026-09-23T18:12:13.000Z"++}@@@ -1,32 +1,18 @@{-- "app_id": "live","id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live","object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ","type": "object.split","status": "proposed",-- "params": {-- "source_size_bytes": 9663676416,-- "target_bytes": 2147483648,-- "max_bytes": 4294967296,-- "reason": "public_object_too_large",-- "parts": 5,-- "segment_seconds": 2160,-- "source_duration_seconds": 10800-- },-- "result": null,-- "error": null,-- "error_code": null,"attempts": 0,"max_attempts": 3,-- "run_after": null,++ "error_code": null,"cancel_requested": false,-- "idempotency_key": "invariant:med_01JAB2C3D4E5F6G7H8J9K0MNPQ:object.split",-- "created_by": "system:invariant.scan",-- "owner_user_id": null,-- "decided_by": null,++ "has_result": false,++ "run_after": null,"decided_at": null,-- "created_at": "2026-09-23 18:02:11",-- "updated_at": "2026-09-23 18:02:11",-- "started_at": "2026-09-23 18:02:12",++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:02:11.000Z",++ "started_at": "2026-09-23T18:02:12.000Z","finished_at": null}ddeleted file mode 100644@@@ -1,35 +0,0 @@--{-- "app_id": "live",-- "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",-- "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",-- "type": "object.split",-- "status": "proposed",-- "params": {-- "source_size_bytes": 9663676416,-- "target_bytes": 2147483648,-- "max_bytes": 4294967296,-- "reason": "public_object_too_large",-- "parts": 5,-- "segment_seconds": 2160,-- "source_duration_seconds": 10800-- },-- "result": null,-- "error": null,-- "error_code": null,-- "attempts": 0,-- "max_attempts": 3,-- "run_after": null,-- "cancel_requested": false,-- "idempotency_key": "invariant:med_01JAB2C3D4E5F6G7H8J9K0MNPQ:object.split",-- "created_by": "system:invariant.scan",-- "owner_user_id": null,-- "decided_by": null,-- "decided_at": null,-- "created_at": "2026-09-23 18:02:11",-- "updated_at": "2026-09-23 18:02:11",-- "started_at": null,-- "finished_at": null,-- "checkpoint": {-- "part": 2-- }--}nnew file mode 100644@@@ -0,0 +1,20 @@++{++ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live",++ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "type": "object.split",++ "status": "proposed",++ "attempts": 0,++ "max_attempts": 3,++ "error_code": null,++ "cancel_requested": false,++ "has_result": false,++ "run_after": null,++ "decided_at": null,++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:02:11.000Z",++ "started_at": null,++ "finished_at": null,++ "created_by": "system:invariant.scan",++ "idempotency_key": "invariant:med_01JAB2C3D4E5F6G7H8J9K0MNPQ:object.split"++}nnew file mode 100644@@@ -0,0 +1,24 @@++{++ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live",++ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "type": "object.split",++ "status": "proposed",++ "attempts": 0,++ "max_attempts": 3,++ "error_code": null,++ "cancel_requested": false,++ "has_result": false,++ "run_after": null,++ "decided_at": null,++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:02:11.000Z",++ "started_at": null,++ "finished_at": null,++ "params": {++ "source_size_bytes": 9663676416,++ "target_bytes": 2147483648,++ "reason": "public_object_too_large",++ "parts": 5++ }++}@@@ -1,32 +1,18 @@{-- "app_id": "live","id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live","object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ","type": "object.split","status": "proposed",-- "params": {-- "source_size_bytes": 9663676416,-- "target_bytes": 2147483648,-- "max_bytes": 4294967296,-- "reason": "public_object_too_large",-- "parts": 5,-- "segment_seconds": 2160,-- "source_duration_seconds": 10800-- },-- "result": null,-- "error": null,-- "error_code": null,"attempts": 0,"max_attempts": 3,-- "run_after": null,++ "error_code": null,"cancel_requested": false,-- "idempotency_key": "invariant:med_01JAB2C3D4E5F6G7H8J9K0MNPQ:object.split",-- "created_by": "system:invariant.scan",-- "owner_user_id": null,-- "decided_by": null,++ "has_result": false,++ "run_after": null,"decided_at": null,-- "created_at": "2026-09-23 18:02:11",-- "updated_at": "2026-09-23 18:02:11",++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:02:11.000Z","started_at": null,"finished_at": null}nnew file mode 100644@@@ -0,0 +1,18 @@++{++ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "type": "object.remux",++ "status": "proposed",++ "attempts": 0,++ "max_attempts": 3,++ "error_code": null,++ "cancel_requested": false,++ "has_result": false,++ "run_after": null,++ "decided_at": null,++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:02:11.000Z",++ "started_at": null,++ "finished_at": null++}nnew file mode 100644@@@ -0,0 +1,18 @@++{++ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live",++ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "type": "object.split",++ "status": "queued",++ "attempts": 1,++ "max_attempts": 3,++ "error_code": null,++ "cancel_requested": false,++ "has_result": false,++ "run_after": null,++ "decided_at": null,++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:02:11.000Z",++ "started_at": "2026-09-23T18:02:12.000Z",++ "finished_at": null++}nnew file mode 100644@@@ -0,0 +1,19 @@++{++ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live",++ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "type": "object.split",++ "status": "queued",++ "attempts": 0,++ "max_attempts": 3,++ "error_code": null,++ "cancel_requested": false,++ "has_result": false,++ "run_after": null,++ "decided_at": null,++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:02:11.000Z",++ "started_at": null,++ "finished_at": null,++ "idempotency_key": "thumb-4211-2026-09-23"++}nnew file mode 100644@@@ -0,0 +1,20 @@++{++ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live",++ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "type": "thumbnail.regenerate",++ "status": "queued",++ "attempts": 0,++ "max_attempts": 3,++ "error_code": null,++ "cancel_requested": false,++ "has_result": false,++ "run_after": null,++ "decided_at": null,++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:02:11.000Z",++ "started_at": null,++ "finished_at": null,++ "created_by": "app:live:user:57",++ "owner_user_id": 57++}ddeleted file mode 100644@@@ -1,26 +0,0 @@--{-- "app_id": "live",-- "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",-- "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",-- "type": "object.split",-- "status": "queued",-- "params": {-- "parts": 3-- },-- "result": null,-- "error": null,-- "error_code": null,-- "attempts": 0,-- "max_attempts": 3,-- "run_after": null,-- "cancel_requested": false,-- "idempotency_key": null,-- "created_by": "svc:live",-- "owner_user_id": null,-- "decided_by": null,-- "decided_at": null,-- "created_at": "2026-09-23T18:02:11Z",-- "updated_at": "2026-09-23T18:02:11Z",-- "started_at": null,-- "finished_at": null--}ddeleted file mode 100644@@@ -1,26 +0,0 @@--{-- "app_id": "live",-- "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",-- "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",-- "type": "object.split",-- "status": "queued",-- "params": {-- "parts": 3-- },-- "result": null,-- "error": null,-- "error_code": null,-- "attempts": 0,-- "max_attempts": 3,-- "run_after": null,-- "cancel_requested": false,-- "idempotency_key": null,-- "created_by": "svc:live",-- "owner_user_id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",-- "decided_by": null,-- "decided_at": null,-- "created_at": "2026-09-23 18:02:11",-- "updated_at": "2026-09-23 18:02:11",-- "started_at": null,-- "finished_at": null--}nnew file mode 100644@@@ -0,0 +1,18 @@++{++ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live",++ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "type": "object.split",++ "status": "queued",++ "attempts": 0,++ "max_attempts": 3,++ "error_code": null,++ "cancel_requested": false,++ "has_result": false,++ "run_after": null,++ "decided_at": null,++ "created_at": "2026-09-23 18:02:11",++ "updated_at": "2026-09-23 18:02:11",++ "started_at": null,++ "finished_at": null++}@@@ -1,32 +1,18 @@{-- "app_id": "live","id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live","object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ","type": "object.split","status": "queued",-- "params": {-- "source_size_bytes": 9663676416,-- "target_bytes": 2147483648,-- "max_bytes": 4294967296,-- "reason": "public_object_too_large",-- "parts": 5,-- "segment_seconds": 2160,-- "source_duration_seconds": 10800-- },-- "result": null,-- "error": null,-- "error_code": null,"attempts": 0,"max_attempts": 3,-- "run_after": null,++ "error_code": null,"cancel_requested": false,-- "idempotency_key": "invariant:med_01JAB2C3D4E5F6G7H8J9K0MNPQ:object.split",-- "created_by": "system:invariant.scan",-- "owner_user_id": null,-- "decided_by": "app:live:user:57",-- "decided_at": "2026-09-23 18:10:40",-- "created_at": "2026-09-23 18:02:11",-- "updated_at": "2026-09-23 18:10:40",++ "has_result": false,++ "run_after": null,++ "decided_at": "2026-09-23T18:10:40.000Z",++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:10:40.000Z","started_at": null,"finished_at": null}nnew file mode 100644@@@ -0,0 +1,18 @@++{++ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live",++ "object_id": null,++ "type": "invariant.scan",++ "status": "queued",++ "attempts": 0,++ "max_attempts": 3,++ "error_code": null,++ "cancel_requested": false,++ "has_result": false,++ "run_after": null,++ "decided_at": null,++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:02:11.000Z",++ "started_at": null,++ "finished_at": null++}ddeleted file mode 100644@@@ -1,27 +0,0 @@--{-- "app_id": "live",-- "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",-- "object_id": null,-- "type": "thumbnail.regenerate",-- "status": "queued",-- "params": {-- "kind": "vod",-- "id": 4211-- },-- "result": null,-- "error": null,-- "error_code": null,-- "attempts": 0,-- "max_attempts": 2,-- "run_after": null,-- "cancel_requested": false,-- "idempotency_key": "thumb-4211-2026-09-23",-- "created_by": "app:live:user:57",-- "owner_user_id": 57,-- "decided_by": null,-- "decided_at": null,-- "created_at": "2026-09-23 18:02:11",-- "updated_at": "2026-09-23 18:02:11",-- "started_at": null,-- "finished_at": null--}nnew file mode 100644@@@ -0,0 +1,18 @@++{++ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live",++ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "type": "thumbnail.regenerate",++ "status": "queued",++ "attempts": 0,++ "max_attempts": 2,++ "error_code": null,++ "cancel_requested": false,++ "has_result": false,++ "run_after": null,++ "decided_at": null,++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:02:11.000Z",++ "started_at": null,++ "finished_at": null++}nnew file mode 100644@@@ -0,0 +1,19 @@++{++ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live",++ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "type": "object.split",++ "status": "queued",++ "attempts": 1,++ "max_attempts": 3,++ "error_code": "interrupted",++ "cancel_requested": false,++ "has_result": false,++ "run_after": "2026-09-23T18:05:42.000Z",++ "decided_at": null,++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:05:12.000Z",++ "started_at": "2026-09-23T18:02:12.000Z",++ "finished_at": null,++ "error": "interrupted by a restart"++}@@@ -1,26 +1,18 @@{-- "app_id": "live","id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live","object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ","type": "object.split","status": "queued",-- "params": {-- "parts": 3-- },-- "result": null,-- "error": "interrupted by a restart",-- "error_code": "interrupted","attempts": 1,"max_attempts": 3,-- "run_after": null,++ "error_code": "interrupted","cancel_requested": false,-- "idempotency_key": null,-- "created_by": "svc:live",-- "owner_user_id": null,-- "decided_by": null,++ "has_result": false,++ "run_after": null,"decided_at": null,-- "created_at": "2026-09-23 18:02:11",-- "updated_at": "2026-09-23 18:05:12",-- "started_at": "2026-09-23 18:02:12",++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:05:12.000Z",++ "started_at": "2026-09-23T18:02:12.000Z","finished_at": null}nnew file mode 100644@@@ -0,0 +1,18 @@++{++ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live",++ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "type": "object.split",++ "status": "retrying",++ "attempts": 1,++ "max_attempts": 3,++ "error_code": "interrupted",++ "cancel_requested": false,++ "has_result": false,++ "run_after": "2026-09-23T18:05:42.000Z",++ "decided_at": null,++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:05:12.000Z",++ "started_at": "2026-09-23T18:02:12.000Z",++ "finished_at": null++}@@@ -1,26 +1,18 @@{-- "app_id": "live","id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live","object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ","type": "object.split","status": "queued",-- "params": {-- "parts": 3-- },-- "result": null,-- "error": "interrupted by a restart",-- "error_code": "interrupted","attempts": 1,"max_attempts": 3,-- "run_after": "2026-09-23 18:05:42",++ "error_code": "interrupted","cancel_requested": false,-- "idempotency_key": null,-- "created_by": "svc:live",-- "owner_user_id": null,-- "decided_by": null,++ "has_result": false,++ "run_after": "2026-09-23T18:05:42.000Z","decided_at": null,-- "created_at": "2026-09-23 18:02:11",-- "updated_at": "2026-09-23 18:05:12",-- "started_at": "2026-09-23 18:02:12",++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:05:12.000Z",++ "started_at": "2026-09-23T18:02:12.000Z","finished_at": null}nnew file mode 100644@@@ -0,0 +1,18 @@++{++ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live",++ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "type": "object.split",++ "status": "queued",++ "attempts": 1,++ "max_attempts": 3,++ "error_code": null,++ "cancel_requested": false,++ "has_result": false,++ "run_after": "2026-09-23T18:02:43.000Z",++ "decided_at": null,++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:02:13.000Z",++ "started_at": "2026-09-23T18:02:12.000Z",++ "finished_at": null++}nnew file mode 100644@@@ -0,0 +1,18 @@++{++ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live",++ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "type": "object.split",++ "status": "running",++ "attempts": 1,++ "max_attempts": 3,++ "error_code": null,++ "cancel_requested": false,++ "has_result": false,++ "run_after": null,++ "decided_at": null,++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:02:12.000Z",++ "started_at": null,++ "finished_at": null++}nnew file mode 100644@@@ -0,0 +1,18 @@++{++ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live",++ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "type": "object.split",++ "status": "running",++ "attempts": 1,++ "max_attempts": 3,++ "error_code": null,++ "cancel_requested": false,++ "has_result": false,++ "run_after": null,++ "decided_at": null,++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:02:12.000Z",++ "started_at": "2026-09-23T20:02:12.000+02:00",++ "finished_at": null++}@@@ -1,26 +1,18 @@{-- "app_id": "live","id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live","object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ","type": "object.split","status": "started",-- "params": {-- "parts": 3-- },-- "result": null,-- "error": null,-- "error_code": null,"attempts": 1,"max_attempts": 3,-- "run_after": null,++ "error_code": null,"cancel_requested": false,-- "idempotency_key": null,-- "created_by": "svc:live",-- "owner_user_id": null,-- "decided_by": null,++ "has_result": false,++ "run_after": null,"decided_at": null,-- "created_at": "2026-09-23 18:02:11",-- "updated_at": "2026-09-23 18:02:12",-- "started_at": "2026-09-23 18:02:12",++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:02:12.000Z",++ "started_at": "2026-09-23T18:02:12.000Z","finished_at": null}nnew file mode 100644@@@ -0,0 +1,18 @@++{++ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live",++ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "type": "object.split",++ "status": "running",++ "attempts": 2,++ "max_attempts": 3,++ "error_code": "media_unavailable",++ "cancel_requested": true,++ "has_result": false,++ "run_after": null,++ "decided_at": null,++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:06:12.000Z",++ "started_at": "2026-09-23T18:02:12.000Z",++ "finished_at": null++}@@@ -1,26 +1,18 @@{-- "app_id": "live","id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live","object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ","type": "object.split","status": "running",-- "params": {-- "parts": 3-- },-- "result": null,-- "error": null,-- "error_code": null,"attempts": 1,"max_attempts": 3,-- "run_after": null,++ "error_code": null,"cancel_requested": false,-- "idempotency_key": null,-- "created_by": "svc:live",-- "owner_user_id": null,-- "decided_by": null,++ "has_result": false,++ "run_after": null,"decided_at": null,-- "created_at": "2026-09-23 18:02:11",-- "updated_at": "2026-09-23 18:02:12",-- "started_at": "2026-09-23 18:02:12",++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:02:12.000Z",++ "started_at": "2026-09-23T18:02:12.000Z","finished_at": null}nnew file mode 100644@@@ -0,0 +1,18 @@++{++ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live",++ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "type": "object.split",++ "status": "running",++ "attempts": 2,++ "max_attempts": 3,++ "error_code": "interrupted",++ "cancel_requested": false,++ "has_result": false,++ "run_after": null,++ "decided_at": null,++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:06:12.000Z",++ "started_at": "2026-09-23T18:02:12.000Z",++ "finished_at": null++}nnew file mode 100644@@@ -0,0 +1,23 @@++{++ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live",++ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "type": "thumbnail.regenerate",++ "status": "succeeded",++ "attempts": 1,++ "max_attempts": 3,++ "error_code": null,++ "cancel_requested": false,++ "has_result": true,++ "run_after": null,++ "decided_at": null,++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:02:14.000Z",++ "started_at": "2026-09-23T18:02:12.000Z",++ "finished_at": "2026-09-23T18:02:14.000Z",++ "result": {++ "url": "/t/vod-4211-1.jpg",++ "kind": "vod",++ "id": 4211++ }++}nnew file mode 100644@@@ -0,0 +1,18 @@++{++ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live",++ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "type": "object.split",++ "status": "succeeded",++ "attempts": 1,++ "max_attempts": 3,++ "error_code": "interrupted",++ "cancel_requested": false,++ "has_result": true,++ "run_after": null,++ "decided_at": null,++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:31:02.000Z",++ "started_at": "2026-09-23T18:02:12.000Z",++ "finished_at": "2026-09-23T18:31:02.000Z"++}ddeleted file mode 100644@@@ -1,38 +0,0 @@--{-- "app_id": "live",-- "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",-- "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",-- "type": "object.split",-- "status": "succeeded",-- "params": {-- "parts": 3-- },-- "result": {-- "source_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",-- "parts": [-- {-- "part": 1,-- "object_id": "med_01JAB3C4D5E6F7G8H9J0K1MNPR",-- "start_seconds": 0,-- "duration_seconds": 3600,-- "size_bytes": 1932735283-- }-- ],-- "method": "split"-- },-- "error": "transient",-- "error_code": "interrupted",-- "attempts": 1,-- "max_attempts": 3,-- "run_after": null,-- "cancel_requested": false,-- "idempotency_key": null,-- "created_by": "svc:live",-- "owner_user_id": null,-- "decided_by": null,-- "decided_at": null,-- "created_at": "2026-09-23 18:02:11",-- "updated_at": "2026-09-23 18:31:02",-- "started_at": "2026-09-23 18:02:12",-- "finished_at": "2026-09-23 18:31:02"--}nnew file mode 100644@@@ -0,0 +1,17 @@++{++ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live",++ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "type": "object.split",++ "status": "succeeded",++ "attempts": 1,++ "max_attempts": 3,++ "error_code": null,++ "cancel_requested": false,++ "run_after": null,++ "decided_at": null,++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:31:02.000Z",++ "started_at": "2026-09-23T18:02:12.000Z",++ "finished_at": "2026-09-23T18:31:02.000Z"++}ddeleted file mode 100644@@@ -1,29 +0,0 @@--{-- "app_id": "live",-- "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",-- "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",-- "type": "object.split",-- "status": "succeeded",-- "params": {-- "parts": 3-- },-- "result": {-- "omitted": true,-- "reason": "larger than the event limit; GET the job"-- },-- "error": null,-- "error_code": null,-- "attempts": 1,-- "max_attempts": 3,-- "run_after": null,-- "cancel_requested": false,-- "idempotency_key": null,-- "created_by": "svc:live",-- "owner_user_id": null,-- "decided_by": null,-- "decided_at": null,-- "created_at": "2026-09-23 18:02:11",-- "updated_at": "2026-09-23 18:31:02",-- "started_at": "2026-09-23 18:02:12",-- "finished_at": "2026-09-23 18:31:02"--}@@@ -1,40 +1,18 @@{-- "app_id": "live","id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live","object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ","type": "object.split","status": "succeeded",-- "params": {-- "parts": 3-- },-- "result": {-- "source_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",-- "method": "split",-- "parts": [-- {-- "part": 1,-- "object_id": "med_01JAB3C4D5E6F7G8H9J0K1MNPR",-- "start_seconds": 0,-- "duration_seconds": 3600,-- "size_bytes": 1932735283-- }-- ],-- "oversized_parts": [],-- "source_unchanged": true-- },-- "error": null,-- "error_code": null,"attempts": 1,"max_attempts": 3,-- "run_after": null,++ "error_code": null,"cancel_requested": false,-- "idempotency_key": null,-- "created_by": "svc:live",-- "owner_user_id": null,-- "decided_by": null,++ "has_result": true,++ "run_after": null,"decided_at": null,-- "created_at": "2026-09-23 18:02:11",-- "updated_at": "2026-09-23 18:31:02",-- "started_at": "2026-09-23 18:02:12",-- "finished_at": "2026-09-23 18:31:02"++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:31:02.000Z",++ "started_at": "2026-09-23T18:02:12.000Z",++ "finished_at": "2026-09-23T18:31:02.000Z"}nnew file mode 100644@@@ -0,0 +1,18 @@++{++ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",++ "app_id": "live",++ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "type": "thumbnail.regenerate",++ "status": "succeeded",++ "attempts": 2,++ "max_attempts": 2,++ "error_code": null,++ "cancel_requested": false,++ "has_result": true,++ "run_after": null,++ "decided_at": null,++ "created_at": "2026-09-23T18:02:11.000Z",++ "updated_at": "2026-09-23T18:03:02.000Z",++ "started_at": "2026-09-23T18:02:12.000Z",++ "finished_at": "2026-09-23T18:03:02.000Z"++}@@@ -8588,7 +8588,7 @@"$schema": "https://json-schema.org/draft/2020-12/schema","$id": "https://openvibe.network/contracts/events/payloads/media.job.proposed.v1.json","title": "MediaJobProposedPayload",-- "description": "media.job.proposed v1 (OpenVibe.Media server/jobs/queue.js enqueue; server/events.js recordJob). A job was proposed and waits for its owner: approving it queues it (media.job.queued), cancelling it ends it (media.job.cancelled); the worker never runs a proposal. The size-invariant validator (invariant.scan, created_by system:invariant.scan) proposes object.split and object.remux this way. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",++ "description": "media.job.proposed v1 (OpenVibe.Media server/jobs/queue.js enqueue; server/events.js recordJob). A job was proposed and waits for its owner: approving it queues it (media.job.queued), cancelling it ends it (media.job.cancelled); the worker never runs a proposal. The size-invariant validator (invariant.scan) proposes object.split and object.remux this way. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.","type": "object","required": ["id",@@@ -8596,18 +8596,12 @@"object_id","type","status",-- "params",-- "result",-- "error",-- "error_code","attempts","max_attempts",-- "run_after",++ "error_code","cancel_requested",-- "idempotency_key",-- "created_by",-- "owner_user_id",-- "decided_by",++ "has_result",++ "run_after","decided_at","created_at","updated_at",@@@ -8618,7 +8612,8 @@"properties": {"id": {"type": "string",-- "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$"++ "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."},"app_id": {"type": "string",@@@ -8643,97 +8638,49 @@"status": {"const": "proposed"},-- "params": {-- "type": "object",-- "description": "The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent."-- },-- "result": {-- "type": [-- "object",-- "null"-- ],-- "description": "null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB."-- },-- "error": {-- "type": [-- "string",-- "null"-- ],-- "maxLength": 1000,-- "description": "The last failure's message (free text), or null."-- },-- "error_code": {-- "type": [-- "string",-- "null"-- ],-- "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null."-- },"attempts": {"type": "integer",-- "minimum": 0++ "minimum": 0,++ "description": "Attempts started so far."},"max_attempts": {"type": "integer","minimum": 1,"maximum": 10},-- "run_after": {-- "type": [-- "string",-- "null"-- ],-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$",-- "description": "A queued job waits until then (retry backoff); null otherwise."++ "error_code": {++ "type": "null"},"cancel_requested": {"type": "boolean","description": "Its owner asked a running job to stop."},-- "idempotency_key": {-- "type": [-- "string",-- "null"-- ],-- "maxLength": 200,-- "description": "The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null."-- },-- "created_by": {-- "type": [-- "string",-- "null"-- ],-- "description": "svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>."++ "has_result": {++ "const": false,++ "description": "A proposal has no result."},-- "owner_user_id": {-- "type": [-- "integer",-- "null"-- ],-- "minimum": 1,-- "description": "The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject."-- },-- "decided_by": {++ "run_after": {"type": ["string","null"],-- "description": "Who approved or cancelled a proposal or queued job (same forms as created_by)."++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",++ "description": "A queued job waits until then (retry backoff); null otherwise."},"decided_at": {-- "type": [-- "string",-- "null"-- ],-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"++ "type": "null",++ "description": "Nobody has decided yet."},"created_at": {"type": "string",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"},"updated_at": {"type": "string",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"},"started_at": {"type": "null"@@@ -8752,7 +8699,7 @@"$schema": "https://json-schema.org/draft/2020-12/schema","$id": "https://openvibe.network/contracts/events/payloads/media.job.queued.v1.json","title": "MediaJobQueuedPayload",-- "description": "media.job.queued v1 (OpenVibe.Media server/jobs/queue.js enqueue, approve; server/events.js recordJob). A job was accepted and queued: a new job (POST /api/v2/:app/jobs), or a proposal its owner approved (decided_by, decided_at). An Idempotency-Key replay, or a request joined to an identical job that is still active, creates no job and no event. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",++ "description": "media.job.queued v1 (OpenVibe.Media server/jobs/queue.js enqueue, approve; server/events.js recordJob). A job was accepted and queued: a new job (POST /api/v2/:app/jobs), or a proposal its owner approved (decided_at). An Idempotency-Key replay, or a request joined to an identical job that is still active, creates no job and no event. A failed attempt that is queued again is media.job.retrying, not this. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.","type": "object","required": ["id",@@@ -8760,18 +8707,12 @@"object_id","type","status",-- "params",-- "result",-- "error",-- "error_code","attempts","max_attempts",-- "run_after",++ "error_code","cancel_requested",-- "idempotency_key",-- "created_by",-- "owner_user_id",-- "decided_by",++ "has_result",++ "run_after","decided_at","created_at","updated_at",@@@ -8782,7 +8723,8 @@"properties": {"id": {"type": "string",-- "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$"++ "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."},"app_id": {"type": "string",@@@ -8807,97 +8749,54 @@"status": {"const": "queued"},-- "params": {-- "type": "object",-- "description": "The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent."-- },-- "result": {-- "type": [-- "object",-- "null"-- ],-- "description": "null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB."-- },-- "error": {-- "type": [-- "string",-- "null"-- ],-- "maxLength": 1000,-- "description": "The last failure's message (free text), or null."-- },-- "error_code": {-- "type": [-- "string",-- "null"-- ],-- "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null."-- },"attempts": {"type": "integer",-- "minimum": 0++ "minimum": 0,++ "description": "Attempts started so far."},"max_attempts": {"type": "integer","minimum": 1,"maximum": 10},-- "run_after": {-- "type": [-- "string",-- "null"-- ],-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$",-- "description": "A queued job waits until then (retry backoff); null otherwise."++ "error_code": {++ "type": "null"},"cancel_requested": {"type": "boolean","description": "Its owner asked a running job to stop."},-- "idempotency_key": {-- "type": [-- "string",-- "null"-- ],-- "maxLength": 200,-- "description": "The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null."++ "has_result": {++ "const": false,++ "description": "A job that never ran has no result."},-- "created_by": {-- "type": [-- "string",-- "null"-- ],-- "description": "svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>."-- },-- "owner_user_id": {-- "type": [-- "integer",-- "null"-- ],-- "minimum": 1,-- "description": "The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject."-- },-- "decided_by": {++ "run_after": {"type": ["string","null"],-- "description": "Who approved or cancelled a proposal or queued job (same forms as created_by)."++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",++ "description": "A queued job waits until then (retry backoff); null otherwise."},"decided_at": {"type": ["string","null"],-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",++ "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise."},"created_at": {"type": "string",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"},"updated_at": {"type": "string",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"},"started_at": {"type": "null"@@@ -8916,7 +8815,7 @@"$schema": "https://json-schema.org/draft/2020-12/schema","$id": "https://openvibe.network/contracts/events/payloads/media.job.started.v1.json","title": "MediaJobStartedPayload",-- "description": "media.job.started v1 (OpenVibe.Media server/jobs/queue.js claim; server/events.js recordJob). The worker claimed the job and it is running: attempts counts this attempt, started_at stays the first attempt's start. A job that is retried starts again and is announced again. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",++ "description": "media.job.started v1 (OpenVibe.Media server/jobs/queue.js claim; server/events.js recordJob). The worker claimed the job and it is running (status running): attempts counts this attempt, started_at stays the first attempt's start. A job that is retried starts again and is announced again; error_code is still the previous attempt's until the job finishes. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.","type": "object","required": ["id",@@@ -8924,18 +8823,12 @@"object_id","type","status",-- "params",-- "result",-- "error",-- "error_code","attempts","max_attempts",-- "run_after",++ "error_code","cancel_requested",-- "idempotency_key",-- "created_by",-- "owner_user_id",-- "decided_by",++ "has_result",++ "run_after","decided_at","created_at","updated_at",@@@ -8946,7 +8839,8 @@"properties": {"id": {"type": "string",-- "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$"++ "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."},"app_id": {"type": "string",@@@ -8969,103 +8863,62 @@"description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."},"status": {-- "const": "running"-- },-- "params": {-- "type": "object",-- "description": "The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent."-- },-- "result": {-- "type": [-- "object",-- "null"-- ],-- "description": "null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB."-- },-- "error": {-- "type": [-- "string",-- "null"-- ],-- "maxLength": 1000,-- "description": "The last failure's message (free text), or null."-- },-- "error_code": {-- "type": [-- "string",-- "null"-- ],-- "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null."++ "const": "running",++ "description": "started carries the job's state, running."},"attempts": {"type": "integer",-- "minimum": 0++ "minimum": 1,++ "description": "Attempts started so far, this one included."},"max_attempts": {"type": "integer","minimum": 1,"maximum": 10},-- "run_after": {++ "error_code": {"type": ["string","null"],-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$",-- "description": "A queued job waits until then (retry backoff); null otherwise."++ "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it."},"cancel_requested": {"type": "boolean","description": "Its owner asked a running job to stop."},-- "idempotency_key": {-- "type": [-- "string",-- "null"-- ],-- "maxLength": 200,-- "description": "The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null."-- },-- "created_by": {-- "type": [-- "string",-- "null"-- ],-- "description": "svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>."-- },-- "owner_user_id": {-- "type": [-- "integer",-- "null"-- ],-- "minimum": 1,-- "description": "The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject."++ "has_result": {++ "const": false,++ "description": "A running job has no result yet."},-- "decided_by": {-- "type": [-- "string",-- "null"-- ],-- "description": "Who approved or cancelled a proposal or queued job (same forms as created_by)."++ "run_after": {++ "type": "null",++ "description": "Cleared when the job is claimed."},"decided_at": {"type": ["string","null"],-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",++ "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise."},"created_at": {"type": "string",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"},"updated_at": {"type": "string",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"},"started_at": {"type": "string",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",++ "description": "The first attempt's start."},"finished_at": {"type": "null"@@@ -9081,7 +8934,7 @@"$schema": "https://json-schema.org/draft/2020-12/schema","$id": "https://openvibe.network/contracts/events/payloads/media.job.retrying.v1.json","title": "MediaJobRetryingPayload",-- "description": "media.job.retrying v1 (OpenVibe.Media server/jobs/queue.js fail with a retry; server/events.js recordJob). An attempt failed and the job is queued again after a backoff (run_after); error and error_code say why. A running job nobody holds any more (the service restarted, or the worker stopped renewing its lease) is retried this way with error_code interrupted while it has attempts left. status is queued. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",++ "description": "media.job.retrying v1 (OpenVibe.Media server/jobs/queue.js fail with a retry; server/events.js recordJob). An attempt failed and the job is queued again (status queued) after a backoff (run_after); error_code says why when the handler gave a code. A running job nobody holds any more (the service restarted, or the worker stopped renewing its lease) is retried this way with error_code interrupted while it has attempts left. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.","type": "object","required": ["id",@@@ -9089,18 +8942,12 @@"object_id","type","status",-- "params",-- "result",-- "error",-- "error_code","attempts","max_attempts",-- "run_after",++ "error_code","cancel_requested",-- "idempotency_key",-- "created_by",-- "owner_user_id",-- "decided_by",++ "has_result",++ "run_after","decided_at","created_at","updated_at",@@@ -9111,7 +8958,8 @@"properties": {"id": {"type": "string",-- "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$"++ "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."},"app_id": {"type": "string",@@@ -9134,97 +8982,64 @@"description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."},"status": {-- "const": "queued"-- },-- "params": {-- "type": "object",-- "description": "The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent."-- },-- "result": {-- "type": [-- "object",-- "null"-- ],-- "description": "null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB."-- },-- "error": {-- "type": "string",-- "maxLength": 1000,-- "description": "The failure's message, free text from the handler."-- },-- "error_code": {-- "type": [-- "string",-- "null"-- ],-- "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null."++ "const": "queued",++ "description": "retrying carries the job's state, queued."},"attempts": {"type": "integer",-- "minimum": 0++ "minimum": 1,++ "description": "Attempts started so far, the failed one included."},"max_attempts": {"type": "integer","minimum": 1,"maximum": 10},-- "run_after": {-- "type": "string",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$",-- "description": "Not before this time (the retry backoff)."-- },-- "cancel_requested": {-- "type": "boolean",-- "description": "Its owner asked a running job to stop."-- },-- "idempotency_key": {++ "error_code": {"type": ["string","null"],-- "maxLength": 200,-- "description": "The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null."++ "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it."},-- "created_by": {-- "type": [-- "string",-- "null"-- ],-- "description": "svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>."++ "cancel_requested": {++ "type": "boolean",++ "description": "Its owner asked a running job to stop."},-- "owner_user_id": {-- "type": [-- "integer",-- "null"-- ],-- "minimum": 1,-- "description": "The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject."++ "has_result": {++ "const": false,++ "description": "A job waiting for its retry has no result."},-- "decided_by": {-- "type": [-- "string",-- "null"-- ],-- "description": "Who approved or cancelled a proposal or queued job (same forms as created_by)."++ "run_after": {++ "type": "string",++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",++ "description": "Not before this time (the retry backoff)."},"decided_at": {"type": ["string","null"],-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",++ "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise."},"created_at": {"type": "string",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"},"updated_at": {"type": "string",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"},"started_at": {"type": "string",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",++ "description": "The first attempt's start."},"finished_at": {"type": "null"@@@ -9240,7 +9055,7 @@"$schema": "https://json-schema.org/draft/2020-12/schema","$id": "https://openvibe.network/contracts/events/payloads/media.job.succeeded.v1.json","title": "MediaJobSucceededPayload",-- "description": "media.job.succeeded v1 (OpenVibe.Media server/jobs/queue.js succeed; server/events.js recordJob). The job finished: result is its type's result (thumbnail.regenerate { url, kind, id }, object.split { source_id, parts, … }, object.remux { source_id, object_id, … }, invariant.scan { thresholds, counts, … }). Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",++ "description": "media.job.succeeded v1 (OpenVibe.Media server/jobs/queue.js succeed; server/events.js recordJob). The job finished. Its result (thumbnail.regenerate { url, kind, id }, object.split { source_id, parts, … }, object.remux { source_id, object_id, … }, invariant.scan { thresholds, counts, … }) is not in the event: has_result says whether there is one to GET. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.","type": "object","required": ["id",@@@ -9248,18 +9063,12 @@"object_id","type","status",-- "params",-- "result",-- "error",-- "error_code","attempts","max_attempts",-- "run_after",++ "error_code","cancel_requested",-- "idempotency_key",-- "created_by",-- "owner_user_id",-- "decided_by",++ "has_result",++ "run_after","decided_at","created_at","updated_at",@@@ -9270,7 +9079,8 @@"properties": {"id": {"type": "string",-- "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$"++ "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."},"app_id": {"type": "string",@@@ -9295,96 +9105,60 @@"status": {"const": "succeeded"},-- "params": {-- "type": "object",-- "description": "The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent."-- },-- "result": {-- "type": [-- "object",-- "null"-- ],-- "description": "The job type's result, or { omitted: true, reason } when it is larger than 8 KB (GET the job for it)."-- },-- "error": {-- "type": "null"-- },-- "error_code": {-- "type": "null"-- },"attempts": {"type": "integer",-- "minimum": 0++ "minimum": 1,++ "description": "Attempts started, the successful one included."},"max_attempts": {"type": "integer","minimum": 1,"maximum": 10},-- "run_after": {-- "type": [-- "string",-- "null"-- ],-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$",-- "description": "A queued job waits until then (retry backoff); null otherwise."++ "error_code": {++ "type": "null",++ "description": "A success clears the last failure."},"cancel_requested": {"type": "boolean","description": "Its owner asked a running job to stop."},-- "idempotency_key": {-- "type": [-- "string",-- "null"-- ],-- "maxLength": 200,-- "description": "The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null."-- },-- "created_by": {-- "type": [-- "string",-- "null"-- ],-- "description": "svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>."-- },-- "owner_user_id": {-- "type": [-- "integer",-- "null"-- ],-- "minimum": 1,-- "description": "The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject."++ "has_result": {++ "type": "boolean",++ "description": "The job holds a result. The result itself is never in the event: GET the job with a tenant token for it."},-- "decided_by": {-- "type": [-- "string",-- "null"-- ],-- "description": "Who approved or cancelled a proposal or queued job (same forms as created_by)."++ "run_after": {++ "type": "null"},"decided_at": {"type": ["string","null"],-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",++ "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise."},"created_at": {"type": "string",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"},"updated_at": {"type": "string",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"},"started_at": {"type": "string",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",++ "description": "The first attempt's start."},"finished_at": {"type": "string",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"}}}@@@ -9397,7 +9171,7 @@"$schema": "https://json-schema.org/draft/2020-12/schema","$id": "https://openvibe.network/contracts/events/payloads/media.job.failed.v1.json","title": "MediaJobFailedPayload",-- "description": "media.job.failed v1 (OpenVibe.Media server/jobs/queue.js fail; server/events.js recordJob). The job failed for good: a permanent error, or its last attempt (attempts = max_attempts) failed; error and error_code say why. A cancelled job is not a failure and is announced as media.job.cancelled. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",++ "description": "media.job.failed v1 (OpenVibe.Media server/jobs/queue.js fail; server/events.js recordJob). The job failed for good: a permanent error, or its last attempt (attempts = max_attempts) failed; error_code says why when the handler gave a code (GET the job for the message). A cancelled job is not a failure and is announced as media.job.cancelled. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.","type": "object","required": ["id",@@@ -9405,18 +9179,12 @@"object_id","type","status",-- "params",-- "result",-- "error",-- "error_code","attempts","max_attempts",-- "run_after",++ "error_code","cancel_requested",-- "idempotency_key",-- "created_by",-- "owner_user_id",-- "decided_by",++ "has_result",++ "run_after","decided_at","created_at","updated_at",@@@ -9427,7 +9195,8 @@"properties": {"id": {"type": "string",-- "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$"++ "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."},"app_id": {"type": "string",@@@ -9452,102 +9221,63 @@"status": {"const": "failed"},-- "params": {-- "type": "object",-- "description": "The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent."-- },-- "result": {-- "type": [-- "object",-- "null"-- ],-- "description": "null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB."-- },-- "error": {-- "type": "string",-- "maxLength": 1000,-- "description": "The failure's message, free text from the handler."-- },-- "error_code": {-- "type": [-- "string",-- "null"-- ],-- "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null."-- },"attempts": {"type": "integer",-- "minimum": 0++ "minimum": 1,++ "description": "Attempts started, the last failed one included."},"max_attempts": {"type": "integer","minimum": 1,"maximum": 10},-- "run_after": {++ "error_code": {"type": ["string","null"],-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$",-- "description": "A queued job waits until then (retry backoff); null otherwise."++ "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it."},"cancel_requested": {"type": "boolean","description": "Its owner asked a running job to stop."},-- "idempotency_key": {-- "type": [-- "string",-- "null"-- ],-- "maxLength": 200,-- "description": "The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null."-- },-- "created_by": {-- "type": [-- "string",-- "null"-- ],-- "description": "svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>."-- },-- "owner_user_id": {-- "type": [-- "integer",-- "null"-- ],-- "minimum": 1,-- "description": "The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject."++ "has_result": {++ "type": "boolean",++ "description": "The handler left a partial result. The result itself is never in the event: GET the job with a tenant token for it."},-- "decided_by": {-- "type": [-- "string",-- "null"-- ],-- "description": "Who approved or cancelled a proposal or queued job (same forms as created_by)."++ "run_after": {++ "type": "null"},"decided_at": {"type": ["string","null"],-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",++ "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise."},"created_at": {"type": "string",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"},"updated_at": {"type": "string",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"},"started_at": {"type": "string",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",++ "description": "The first attempt's start."},"finished_at": {"type": "string",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"}}}@@@ -9560,7 +9290,7 @@"$schema": "https://json-schema.org/draft/2020-12/schema","$id": "https://openvibe.network/contracts/events/payloads/media.job.cancelled.v1.json","title": "MediaJobCancelledPayload",-- "description": "media.job.cancelled v1 (OpenVibe.Media server/jobs/queue.js cancel, markCancelled; server/events.js recordJob). The job was cancelled: a proposal or a queued job at once, by its owner (POST /api/v2/:app/jobs/:id/cancel or DELETE /api/v2/:app/jobs/:id) or by the validator withdrawing a proposal that no longer applies (decided_by system:invariant.scan); a running job once it stopped after its owner asked (cancel_requested true, error_code cancelled). Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",++ "description": "media.job.cancelled v1 (OpenVibe.Media server/jobs/queue.js cancel, markCancelled; server/events.js recordJob). The job was cancelled: a proposal or a queued job at once, by its owner (POST /api/v2/:app/jobs/:id/cancel or DELETE /api/v2/:app/jobs/:id) or by the validator withdrawing a proposal that no longer applies (decided_at set, error_code cancelled when a reason was given); a running job once it stopped after its owner asked (cancel_requested true, error_code cancelled). Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.","type": "object","required": ["id",@@@ -9568,18 +9298,12 @@"object_id","type","status",-- "params",-- "result",-- "error",-- "error_code","attempts","max_attempts",-- "run_after",++ "error_code","cancel_requested",-- "idempotency_key",-- "created_by",-- "owner_user_id",-- "decided_by",++ "has_result",++ "run_after","decided_at","created_at","updated_at",@@@ -9590,7 +9314,8 @@"properties": {"id": {"type": "string",-- "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$"++ "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."},"app_id": {"type": "string",@@@ -9615,109 +9340,72 @@"status": {"const": "cancelled"},-- "params": {-- "type": "object",-- "description": "The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent."-- },-- "result": {-- "type": [-- "object",-- "null"-- ],-- "description": "null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB."-- },-- "error": {-- "type": [-- "string",-- "null"-- ],-- "maxLength": 1000,-- "description": "The last failure's message (free text), or null."-- },-- "error_code": {-- "type": [-- "string",-- "null"-- ],-- "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null."-- },"attempts": {"type": "integer",-- "minimum": 0++ "minimum": 0,++ "description": "Attempts started so far."},"max_attempts": {"type": "integer","minimum": 1,"maximum": 10},-- "run_after": {++ "error_code": {"type": ["string","null"],-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$",-- "description": "A queued job waits until then (retry backoff); null otherwise."++ "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it."},"cancel_requested": {"type": "boolean","description": "Its owner asked a running job to stop."},-- "idempotency_key": {-- "type": [-- "string",-- "null"-- ],-- "maxLength": 200,-- "description": "The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null."-- },-- "created_by": {-- "type": [-- "string",-- "null"-- ],-- "description": "svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>."-- },-- "owner_user_id": {-- "type": [-- "integer",-- "null"-- ],-- "minimum": 1,-- "description": "The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject."++ "has_result": {++ "type": "boolean",++ "description": "A stopped running job left a partial result. The result itself is never in the event: GET the job with a tenant token for it."},-- "decided_by": {++ "run_after": {"type": ["string","null"],-- "description": "Who approved or cancelled a proposal or queued job (same forms as created_by)."++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",++ "description": "A queued job waits until then (retry backoff); null otherwise."},"decided_at": {"type": ["string","null"],-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",++ "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise."},"created_at": {"type": "string",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"},"updated_at": {"type": "string",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"},"started_at": {"type": ["string","null"],-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$",-- "description": "null when the job never ran."++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",++ "description": "The first attempt's start; null when the job was cancelled before it ever ran."},"finished_at": {"type": "string",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"}}}@@@ -7054,9 +7054,12 @@ export interface LiveReleaseDeployedPayload {/** [email protected] (owner: media) *//**-- * media.job.proposed v1 (OpenVibe.Media server/jobs/queue.js enqueue; server/events.js recordJob). A job was proposed and waits for its owner: approving it queues it (media.job.queued), cancelling it ends it (media.job.cancelled); the worker never runs a proposal. The size-invariant validator (invariant.scan, created_by system:invariant.scan) proposes object.split and object.remux this way. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.++ * media.job.proposed v1 (OpenVibe.Media server/jobs/queue.js enqueue; server/events.js recordJob). A job was proposed and waits for its owner: approving it queues it (media.job.queued), cancelling it ends it (media.job.cancelled); the worker never runs a proposal. The size-invariant validator (invariant.scan) proposes object.split and object.remux this way. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.*/export interface MediaJobProposedPayload {++ /**++ * The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it.++ */id: string;/*** The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…).@@@ -7072,48 +7075,27 @@ export interface MediaJobProposedPayload {type: string;status: "proposed";/**-- * The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent.-- */-- params: {};-- /**-- * null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB.-- */-- result: {} | null;-- /**-- * The last failure's message (free text), or null.-- */-- error: string | null;-- /**-- * Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null.++ * Attempts started so far.*/-- error_code: string | null;attempts: number;max_attempts: number;-- /**-- * A queued job waits until then (retry backoff); null otherwise.-- */-- run_after: string | null;++ error_code: null;/*** Its owner asked a running job to stop.*/cancel_requested: boolean;/**-- * The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null.++ * A proposal has no result.*/-- idempotency_key: string | null;++ has_result: false;/**-- * svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>.-- */-- created_by: string | null;-- /**-- * The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject.++ * A queued job waits until then (retry backoff); null otherwise.*/-- owner_user_id: number | null;++ run_after: string | null;/**-- * Who approved or cancelled a proposal or queued job (same forms as created_by).++ * Nobody has decided yet.*/-- decided_by: string | null;-- decided_at: string | null;++ decided_at: null;created_at: string;updated_at: string;started_at: null;@@@ -7122,9 +7104,12 @@ export interface MediaJobProposedPayload {/** [email protected] (owner: media) *//**-- * media.job.queued v1 (OpenVibe.Media server/jobs/queue.js enqueue, approve; server/events.js recordJob). A job was accepted and queued: a new job (POST /api/v2/:app/jobs), or a proposal its owner approved (decided_by, decided_at). An Idempotency-Key replay, or a request joined to an identical job that is still active, creates no job and no event. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.++ * media.job.queued v1 (OpenVibe.Media server/jobs/queue.js enqueue, approve; server/events.js recordJob). A job was accepted and queued: a new job (POST /api/v2/:app/jobs), or a proposal its owner approved (decided_at). An Idempotency-Key replay, or a request joined to an identical job that is still active, creates no job and no event. A failed attempt that is queued again is media.job.retrying, not this. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.*/export interface MediaJobQueuedPayload {++ /**++ * The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it.++ */id: string;/*** The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…).@@@ -7140,47 +7125,26 @@ export interface MediaJobQueuedPayload {type: string;status: "queued";/**-- * The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent.-- */-- params: {};-- /**-- * null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB.++ * Attempts started so far.*/-- result: {} | null;-- /**-- * The last failure's message (free text), or null.-- */-- error: string | null;-- /**-- * Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null.-- */-- error_code: string | null;attempts: number;max_attempts: number;-- /**-- * A queued job waits until then (retry backoff); null otherwise.-- */-- run_after: string | null;++ error_code: null;/*** Its owner asked a running job to stop.*/cancel_requested: boolean;/**-- * The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null.++ * A job that never ran has no result.*/-- idempotency_key: string | null;++ has_result: false;/**-- * svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>.-- */-- created_by: string | null;-- /**-- * The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject.++ * A queued job waits until then (retry backoff); null otherwise.*/-- owner_user_id: number | null;++ run_after: string | null;/**-- * Who approved or cancelled a proposal or queued job (same forms as created_by).++ * When its owner approved it, or cancelled it while it was proposed or queued; null otherwise.*/-- decided_by: string | null;decided_at: string | null;created_at: string;updated_at: string;@@@ -7190,9 +7154,12 @@ export interface MediaJobQueuedPayload {/** [email protected] (owner: media) *//**-- * media.job.started v1 (OpenVibe.Media server/jobs/queue.js claim; server/events.js recordJob). The worker claimed the job and it is running: attempts counts this attempt, started_at stays the first attempt's start. A job that is retried starts again and is announced again. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.++ * media.job.started v1 (OpenVibe.Media server/jobs/queue.js claim; server/events.js recordJob). The worker claimed the job and it is running (status running): attempts counts this attempt, started_at stays the first attempt's start. A job that is retried starts again and is announced again; error_code is still the previous attempt's until the job finishes. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.*/export interface MediaJobStartedPayload {++ /**++ * The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it.++ */id: string;/*** The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…).@@@ -7206,61 +7173,52 @@ export interface MediaJobStartedPayload {* Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux.*/type: string;-- status: "running";-- /**-- * The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent.-- */-- params: {};/**-- * null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB.++ * started carries the job's state, running.*/-- result: {} | null;-- /**-- * The last failure's message (free text), or null.-- */-- error: string | null;++ status: "running";/**-- * Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null.++ * Attempts started so far, this one included.*/-- error_code: string | null;attempts: number;max_attempts: number;/**-- * A queued job waits until then (retry backoff); null otherwise.++ * Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it.*/-- run_after: string | null;++ error_code: string | null;/*** Its owner asked a running job to stop.*/cancel_requested: boolean;/**-- * The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null.++ * A running job has no result yet.*/-- idempotency_key: string | null;-- /**-- * svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>.-- */-- created_by: string | null;++ has_result: false;/**-- * The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject.++ * Cleared when the job is claimed.*/-- owner_user_id: number | null;++ run_after: null;/**-- * Who approved or cancelled a proposal or queued job (same forms as created_by).++ * When its owner approved it, or cancelled it while it was proposed or queued; null otherwise.*/-- decided_by: string | null;decided_at: string | null;created_at: string;updated_at: string;++ /**++ * The first attempt's start.++ */started_at: string;finished_at: null;}/** [email protected] (owner: media) *//**-- * media.job.retrying v1 (OpenVibe.Media server/jobs/queue.js fail with a retry; server/events.js recordJob). An attempt failed and the job is queued again after a backoff (run_after); error and error_code say why. A running job nobody holds any more (the service restarted, or the worker stopped renewing its lease) is retried this way with error_code interrupted while it has attempts left. status is queued. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.++ * media.job.retrying v1 (OpenVibe.Media server/jobs/queue.js fail with a retry; server/events.js recordJob). An attempt failed and the job is queued again (status queued) after a backoff (run_after); error_code says why when the handler gave a code. A running job nobody holds any more (the service restarted, or the worker stopped renewing its lease) is retried this way with error_code interrupted while it has attempts left. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.*/export interface MediaJobRetryingPayload {++ /**++ * The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it.++ */id: string;/*** The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…).@@@ -7274,61 +7232,52 @@ export interface MediaJobRetryingPayload {* Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux.*/type: string;-- status: "queued";-- /**-- * The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent.-- */-- params: {};-- /**-- * null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB.-- */-- result: {} | null;/**-- * The failure's message, free text from the handler.++ * retrying carries the job's state, queued.*/-- error: string;++ status: "queued";/**-- * Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null.++ * Attempts started so far, the failed one included.*/-- error_code: string | null;attempts: number;max_attempts: number;/**-- * Not before this time (the retry backoff).++ * Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it.*/-- run_after: string;++ error_code: string | null;/*** Its owner asked a running job to stop.*/cancel_requested: boolean;/**-- * The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null.++ * A job waiting for its retry has no result.*/-- idempotency_key: string | null;-- /**-- * svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>.-- */-- created_by: string | null;++ has_result: false;/**-- * The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject.++ * Not before this time (the retry backoff).*/-- owner_user_id: number | null;++ run_after: string;/**-- * Who approved or cancelled a proposal or queued job (same forms as created_by).++ * When its owner approved it, or cancelled it while it was proposed or queued; null otherwise.*/-- decided_by: string | null;decided_at: string | null;created_at: string;updated_at: string;++ /**++ * The first attempt's start.++ */started_at: string;finished_at: null;}/** [email protected] (owner: media) *//**-- * media.job.succeeded v1 (OpenVibe.Media server/jobs/queue.js succeed; server/events.js recordJob). The job finished: result is its type's result (thumbnail.regenerate { url, kind, id }, object.split { source_id, parts, … }, object.remux { source_id, object_id, … }, invariant.scan { thresholds, counts, … }). Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.++ * media.job.succeeded v1 (OpenVibe.Media server/jobs/queue.js succeed; server/events.js recordJob). The job finished. Its result (thumbnail.regenerate { url, kind, id }, object.split { source_id, parts, … }, object.remux { source_id, object_id, … }, invariant.scan { thresholds, counts, … }) is not in the event: has_result says whether there is one to GET. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.*/export interface MediaJobSucceededPayload {++ /**++ * The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it.++ */id: string;/*** The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…).@@@ -7344,53 +7293,44 @@ export interface MediaJobSucceededPayload {type: string;status: "succeeded";/**-- * The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent.-- */-- params: {};-- /**-- * The job type's result, or { omitted: true, reason } when it is larger than 8 KB (GET the job for it).++ * Attempts started, the successful one included.*/-- result: {} | null;-- error: null;-- error_code: null;attempts: number;max_attempts: number;/**-- * A queued job waits until then (retry backoff); null otherwise.++ * A success clears the last failure.*/-- run_after: string | null;++ error_code: null;/*** Its owner asked a running job to stop.*/cancel_requested: boolean;/**-- * The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null.-- */-- idempotency_key: string | null;-- /**-- * svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>.-- */-- created_by: string | null;-- /**-- * The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject.++ * The job holds a result. The result itself is never in the event: GET the job with a tenant token for it.*/-- owner_user_id: number | null;++ has_result: boolean;++ run_after: null;/**-- * Who approved or cancelled a proposal or queued job (same forms as created_by).++ * When its owner approved it, or cancelled it while it was proposed or queued; null otherwise.*/-- decided_by: string | null;decided_at: string | null;created_at: string;updated_at: string;++ /**++ * The first attempt's start.++ */started_at: string;finished_at: string;}/** [email protected] (owner: media) *//**-- * media.job.failed v1 (OpenVibe.Media server/jobs/queue.js fail; server/events.js recordJob). The job failed for good: a permanent error, or its last attempt (attempts = max_attempts) failed; error and error_code say why. A cancelled job is not a failure and is announced as media.job.cancelled. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.++ * media.job.failed v1 (OpenVibe.Media server/jobs/queue.js fail; server/events.js recordJob). The job failed for good: a permanent error, or its last attempt (attempts = max_attempts) failed; error_code says why when the handler gave a code (GET the job for the message). A cancelled job is not a failure and is announced as media.job.cancelled. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.*/export interface MediaJobFailedPayload {++ /**++ * The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it.++ */id: string;/*** The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…).@@@ -7406,59 +7346,44 @@ export interface MediaJobFailedPayload {type: string;status: "failed";/**-- * The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent.-- */-- params: {};-- /**-- * null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB.++ * Attempts started, the last failed one included.*/-- result: {} | null;-- /**-- * The failure's message, free text from the handler.-- */-- error: string;-- /**-- * Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null.-- */-- error_code: string | null;attempts: number;max_attempts: number;/**-- * A queued job waits until then (retry backoff); null otherwise.++ * Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it.*/-- run_after: string | null;++ error_code: string | null;/*** Its owner asked a running job to stop.*/cancel_requested: boolean;/**-- * The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null.-- */-- idempotency_key: string | null;-- /**-- * svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>.-- */-- created_by: string | null;-- /**-- * The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject.++ * The handler left a partial result. The result itself is never in the event: GET the job with a tenant token for it.*/-- owner_user_id: number | null;++ has_result: boolean;++ run_after: null;/**-- * Who approved or cancelled a proposal or queued job (same forms as created_by).++ * When its owner approved it, or cancelled it while it was proposed or queued; null otherwise.*/-- decided_by: string | null;decided_at: string | null;created_at: string;updated_at: string;++ /**++ * The first attempt's start.++ */started_at: string;finished_at: string;}/** [email protected] (owner: media) *//**-- * media.job.cancelled v1 (OpenVibe.Media server/jobs/queue.js cancel, markCancelled; server/events.js recordJob). The job was cancelled: a proposal or a queued job at once, by its owner (POST /api/v2/:app/jobs/:id/cancel or DELETE /api/v2/:app/jobs/:id) or by the validator withdrawing a proposal that no longer applies (decided_by system:invariant.scan); a running job once it stopped after its owner asked (cancel_requested true, error_code cancelled). Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.++ * media.job.cancelled v1 (OpenVibe.Media server/jobs/queue.js cancel, markCancelled; server/events.js recordJob). The job was cancelled: a proposal or a queued job at once, by its owner (POST /api/v2/:app/jobs/:id/cancel or DELETE /api/v2/:app/jobs/:id) or by the validator withdrawing a proposal that no longer applies (decided_at set, error_code cancelled when a reason was given); a running job once it stopped after its owner asked (cancel_requested true, error_code cancelled). Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.*/export interface MediaJobCancelledPayload {++ /**++ * The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it.++ */id: string;/*** The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…).@@@ -7474,52 +7399,34 @@ export interface MediaJobCancelledPayload {type: string;status: "cancelled";/**-- * The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent.++ * Attempts started so far.*/-- params: {};-- /**-- * null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB.-- */-- result: {} | null;-- /**-- * The last failure's message (free text), or null.-- */-- error: string | null;-- /**-- * Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null.-- */-- error_code: string | null;attempts: number;max_attempts: number;/**-- * A queued job waits until then (retry backoff); null otherwise.++ * Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it.*/-- run_after: string | null;++ error_code: string | null;/*** Its owner asked a running job to stop.*/cancel_requested: boolean;/**-- * The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null.-- */-- idempotency_key: string | null;-- /**-- * svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>.++ * A stopped running job left a partial result. The result itself is never in the event: GET the job with a tenant token for it.*/-- created_by: string | null;++ has_result: boolean;/**-- * The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject.++ * A queued job waits until then (retry backoff); null otherwise.*/-- owner_user_id: number | null;++ run_after: string | null;/**-- * Who approved or cancelled a proposal or queued job (same forms as created_by).++ * When its owner approved it, or cancelled it while it was proposed or queued; null otherwise.*/-- decided_by: string | null;decided_at: string | null;created_at: string;updated_at: string;/**-- * null when the job never ran.++ * The first attempt's start; null when the job was cancelled before it ever ran.*/started_at: string | null;finished_at: string;@@@ -146,13 +146,24 @@ for (const id of ['events.event.publish', 'events.event.read', 'events.subscript// ── Producers' payloads added in v0.32 (Tips moderation, Media jobs, Live, user modules) ──{-- // Media: one contract per server/events.js JOB_TRANSITIONS entry; the payload is queue.jobPublic, every field always present.++ // Media: one contract per server/events.js JOB_TRANSITIONS entry; the payload is the event projection queue.jobEvent,++ // every field always present. The tenant's params, result, error text, idempotency key and user ids stay behind GET the job.const status = { proposed: 'proposed', queued: 'queued', started: 'running', retrying: 'queued', succeeded: 'succeeded', failed: 'failed', cancelled: 'cancelled' };++ const jobFields = ['id', 'app_id', 'object_id', 'type', 'status', 'attempts', 'max_attempts', 'error_code', 'cancel_requested', 'has_result',++ 'run_after', 'decided_at', 'created_at', 'updated_at', 'started_at', 'finished_at'];++ const jobTimes = jobFields.filter(k => k === 'run_after' || k.endsWith('_at'));for (const [t, st] of Object.entries(status)) {const s = contracts.schema(`media.job.${t}`);ok(s.properties.status.const === st, `media.job.${t} carries status ${st}`);ok(JSON.stringify([...s.required].sort()) === JSON.stringify(Object.keys(s.properties).sort()), `media.job.${t} requires every field it names`);++ ok(JSON.stringify(Object.keys(s.properties)) === JSON.stringify(jobFields) && s.additionalProperties === false, `media.job.${t} is exactly the event projection`);++ ok(['params', 'result', 'error', 'idempotency_key', 'created_by', 'decided_by', 'owner_user_id', 'checkpoint'].every(k => !(k in s.properties)), `media.job.${t} leaves the tenant's data out`);++ ok(jobTimes.every(k => s.properties[k].type === 'null' || (s.properties[k].format === 'date-time' && /Z\$$/.test(s.properties[k].pattern))), `media.job.${t} times are ISO 8601 UTC or null`);++ const outcome = ['succeeded', 'failed', 'cancelled'].includes(t);++ ok(s.properties.finished_at.type === (outcome ? 'string' : 'null'), `media.job.${t} has finished_at ${outcome ? 'always' : 'never'}`);}++ const sqliteTime = JSON.parse(fs.readFileSync(path.join(ROOT, 'fixtures/media.job.queued/valid/thumbnail.json'), 'utf8'));++ ok(!contracts.validate('media.job.queued@1', { ...sqliteTime, created_at: '2026-09-23 18:02:11' }).valid, 'a SQLite time is not a media.job time');// Live: live.stream.ended is live.stream.started plus ended_at and duration_seconds.const started = contracts.schema('live.stream.started'), ended = contracts.schema('live.stream.ended');ok(JSON.stringify(Object.keys(ended.properties)) === JSON.stringify([...Object.keys(started.properties), 'ended_at', 'duration_seconds']), 'live.stream.ended = started + ended_at, duration_seconds');
@@ -36,9 +36,18 @@ Additive: `compat.js` reports no breaking change against v0.31.0.
@@ -36,9 +36,18 @@ Additive: `compat.js` reports no breaking change against v0.31.0.
internal): the moderation queue and log, hide and restore. The Tips manifest (0.4.0) lists all three.
internal): the moderation queue and log, hide and restore. The Tips manifest (0.4.0) lists all three.
- `media.job.proposed|queued|started|retrying|succeeded|failed|cancelled@1`: OpenVibe.Media's job
- `media.job.proposed|queued|started|retrying|succeeded|failed|cancelled@1`: OpenVibe.Media's job
transitions (`server/events.js` `recordJob`), one contract each like `tools.job.*`. The payload is
transitions (`server/events.js` `recordJob`), one contract each like `tools.job.*`. The payload is
- the job as `GET /api/v2/:app/jobs/:id` answers it (`queue.jobPublic`, every field always present,
- times as SQLite `YYYY-MM-DD HH:MM:SS` UTC); a result over 8 KB is `{ omitted: true, reason }`.
- `retrying` carries status `queued`, `started` status `running`. The Media manifest (0.2.0) lists them.
+ an event projection of the job (`queue.jobEvent`), every field always present: `id`, `app_id`,
+ `object_id`, `type`, `status`, `attempts`, `max_attempts`, `error_code` (the stable code only),
+ `cancel_requested`, `has_result`, and `run_after`, `decided_at`, `created_at`, `updated_at`,
+ `started_at`, `finished_at` as ISO 8601 UTC (`…Z`) or null. Deliberately left out, because events
+ travel beyond the tenant: the tenant's `params`, the caller's `idempotency_key`, the free-text
+ `error`, the `result` (a thumbnail URL, of a private VOD too), `created_by`/`decided_by` and
+ `owner_user_id` (tenant-local user ids). A consumer GETs the job with a tenant token for those
+ (`GET /api/v2/:app/jobs/:id`, unchanged); `has_result` says whether there is a result to fetch.
+ `retrying` carries status `queued`, `started` status `running`; `finished_at` is set exactly on
+ `succeeded`, `failed` and `cancelled`, `started_at` on everything that ran, `run_after` always on
+ `retrying`. No service consumed `media.job.*` before this narrowing. The Media manifest (0.2.0)
+ lists them.
- `live.stream.started@1` and `live.stream.ended@1` (public: stream id, channel username, display
- `live.stream.started@1` and `live.stream.ended@1` (public: stream id, channel username, display
name, URL and user subject when Live knows it, title, category or null, protocol, NSFW flag,
name, URL and user subject when Live knows it, title, category or null, protocol, NSFW flag,
times; `ended` adds `ended_at` and `duration_seconds`) and `live.release.deployed@1` (internal:
times; `ended` adds `ended_at` and `duration_seconds`) and `live.release.deployed@1` (internal:
@@ -2,32 +2,26 @@
@@ -2,32 +2,26 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/events/payloads/media.job.cancelled.v1.json",
"$id": "https://openvibe.network/contracts/events/payloads/media.job.cancelled.v1.json",
"title": "MediaJobCancelledPayload",
"title": "MediaJobCancelledPayload",
- "description": "media.job.cancelled v1 (OpenVibe.Media server/jobs/queue.js cancel, markCancelled; server/events.js recordJob). The job was cancelled: a proposal or a queued job at once, by its owner (POST /api/v2/:app/jobs/:id/cancel or DELETE /api/v2/:app/jobs/:id) or by the validator withdrawing a proposal that no longer applies (decided_by system:invariant.scan); a running job once it stopped after its owner asked (cancel_requested true, error_code cancelled). Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",
+ "description": "media.job.cancelled v1 (OpenVibe.Media server/jobs/queue.js cancel, markCancelled; server/events.js recordJob). The job was cancelled: a proposal or a queued job at once, by its owner (POST /api/v2/:app/jobs/:id/cancel or DELETE /api/v2/:app/jobs/:id) or by the validator withdrawing a proposal that no longer applies (decided_at set, error_code cancelled when a reason was given); a running job once it stopped after its owner asked (cancel_requested true, error_code cancelled). Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",
"type": "object",
"type": "object",
- "required": ["id", "app_id", "object_id", "type", "status", "params", "result", "error", "error_code", "attempts", "max_attempts", "run_after", "cancel_requested", "idempotency_key", "created_by", "owner_user_id", "decided_by", "decided_at", "created_at", "updated_at", "started_at", "finished_at"],
+ "required": ["id", "app_id", "object_id", "type", "status", "attempts", "max_attempts", "error_code", "cancel_requested", "has_result", "run_after", "decided_at", "created_at", "updated_at", "started_at", "finished_at"],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "id": { "type": "string", "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$" },
+ "id": { "type": "string", "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it." },
"app_id": { "type": "string", "minLength": 1, "maxLength": 200, "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)." },
"app_id": { "type": "string", "minLength": 1, "maxLength": 200, "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)." },
"object_id": { "type": ["string", "null"], "maxLength": 200, "description": "The object the job works on; null for a tenant-wide job (invariant.scan)." },
"object_id": { "type": ["string", "null"], "maxLength": 200, "description": "The object the job works on; null for a tenant-wide job (invariant.scan)." },
"type": { "type": "string", "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$", "maxLength": 80, "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux." },
"type": { "type": "string", "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$", "maxLength": 80, "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux." },
"status": { "const": "cancelled" },
"status": { "const": "cancelled" },
- "params": { "type": "object", "description": "The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent." },
- "result": { "type": ["object", "null"], "description": "null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB." },
- "error": { "type": ["string", "null"], "maxLength": 1000, "description": "The last failure's message (free text), or null." },
- "error_code": { "type": ["string", "null"], "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null." },
- "attempts": { "type": "integer", "minimum": 0 },
+ "attempts": { "type": "integer", "minimum": 0, "description": "Attempts started so far." },
"max_attempts": { "type": "integer", "minimum": 1, "maximum": 10 },
"max_attempts": { "type": "integer", "minimum": 1, "maximum": 10 },
- "run_after": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$", "description": "A queued job waits until then (retry backoff); null otherwise." },
+ "error_code": { "type": ["string", "null"], "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it." },
"cancel_requested": { "type": "boolean", "description": "Its owner asked a running job to stop." },
"cancel_requested": { "type": "boolean", "description": "Its owner asked a running job to stop." },
- "idempotency_key": { "type": ["string", "null"], "maxLength": 200, "description": "The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null." },
- "created_by": { "type": ["string", "null"], "description": "svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>." },
- "owner_user_id": { "type": ["integer", "null"], "minimum": 1, "description": "The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject." },
- "decided_by": { "type": ["string", "null"], "description": "Who approved or cancelled a proposal or queued job (same forms as created_by)." },
- "decided_at": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },
- "created_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },
- "updated_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },
- "started_at": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$", "description": "null when the job never ran." },
- "finished_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" }
+ "has_result": { "type": "boolean", "description": "A stopped running job left a partial result. The result itself is never in the event: GET the job with a tenant token for it." },
+ "run_after": { "type": ["string", "null"], "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "A queued job waits until then (retry backoff); null otherwise." },
+ "decided_at": { "type": ["string", "null"], "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise." },
+ "created_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" },
+ "updated_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" },
+ "started_at": { "type": ["string", "null"], "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "The first attempt's start; null when the job was cancelled before it ever ran." },
+ "finished_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" }
}
}
}
}
@@ -2,32 +2,26 @@
@@ -2,32 +2,26 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/events/payloads/media.job.failed.v1.json",
"$id": "https://openvibe.network/contracts/events/payloads/media.job.failed.v1.json",
"title": "MediaJobFailedPayload",
"title": "MediaJobFailedPayload",
- "description": "media.job.failed v1 (OpenVibe.Media server/jobs/queue.js fail; server/events.js recordJob). The job failed for good: a permanent error, or its last attempt (attempts = max_attempts) failed; error and error_code say why. A cancelled job is not a failure and is announced as media.job.cancelled. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",
+ "description": "media.job.failed v1 (OpenVibe.Media server/jobs/queue.js fail; server/events.js recordJob). The job failed for good: a permanent error, or its last attempt (attempts = max_attempts) failed; error_code says why when the handler gave a code (GET the job for the message). A cancelled job is not a failure and is announced as media.job.cancelled. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",
"type": "object",
"type": "object",
- "required": ["id", "app_id", "object_id", "type", "status", "params", "result", "error", "error_code", "attempts", "max_attempts", "run_after", "cancel_requested", "idempotency_key", "created_by", "owner_user_id", "decided_by", "decided_at", "created_at", "updated_at", "started_at", "finished_at"],
+ "required": ["id", "app_id", "object_id", "type", "status", "attempts", "max_attempts", "error_code", "cancel_requested", "has_result", "run_after", "decided_at", "created_at", "updated_at", "started_at", "finished_at"],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "id": { "type": "string", "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$" },
+ "id": { "type": "string", "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it." },
"app_id": { "type": "string", "minLength": 1, "maxLength": 200, "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)." },
"app_id": { "type": "string", "minLength": 1, "maxLength": 200, "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)." },
"object_id": { "type": ["string", "null"], "maxLength": 200, "description": "The object the job works on; null for a tenant-wide job (invariant.scan)." },
"object_id": { "type": ["string", "null"], "maxLength": 200, "description": "The object the job works on; null for a tenant-wide job (invariant.scan)." },
"type": { "type": "string", "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$", "maxLength": 80, "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux." },
"type": { "type": "string", "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$", "maxLength": 80, "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux." },
"status": { "const": "failed" },
"status": { "const": "failed" },
- "params": { "type": "object", "description": "The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent." },
- "result": { "type": ["object", "null"], "description": "null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB." },
- "error": { "type": "string", "maxLength": 1000, "description": "The failure's message, free text from the handler." },
- "error_code": { "type": ["string", "null"], "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null." },
- "attempts": { "type": "integer", "minimum": 0 },
+ "attempts": { "type": "integer", "minimum": 1, "description": "Attempts started, the last failed one included." },
"max_attempts": { "type": "integer", "minimum": 1, "maximum": 10 },
"max_attempts": { "type": "integer", "minimum": 1, "maximum": 10 },
- "run_after": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$", "description": "A queued job waits until then (retry backoff); null otherwise." },
+ "error_code": { "type": ["string", "null"], "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it." },
"cancel_requested": { "type": "boolean", "description": "Its owner asked a running job to stop." },
"cancel_requested": { "type": "boolean", "description": "Its owner asked a running job to stop." },
- "idempotency_key": { "type": ["string", "null"], "maxLength": 200, "description": "The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null." },
- "created_by": { "type": ["string", "null"], "description": "svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>." },
- "owner_user_id": { "type": ["integer", "null"], "minimum": 1, "description": "The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject." },
- "decided_by": { "type": ["string", "null"], "description": "Who approved or cancelled a proposal or queued job (same forms as created_by)." },
- "decided_at": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },
- "created_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },
- "updated_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },
- "started_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },
- "finished_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" }
+ "has_result": { "type": "boolean", "description": "The handler left a partial result. The result itself is never in the event: GET the job with a tenant token for it." },
+ "run_after": { "type": "null" },
+ "decided_at": { "type": ["string", "null"], "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise." },
+ "created_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" },
+ "updated_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" },
+ "started_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "The first attempt's start." },
+ "finished_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" }
}
}
}
}
@@ -2,31 +2,25 @@
@@ -2,31 +2,25 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/events/payloads/media.job.proposed.v1.json",
"$id": "https://openvibe.network/contracts/events/payloads/media.job.proposed.v1.json",
"title": "MediaJobProposedPayload",
"title": "MediaJobProposedPayload",
- "description": "media.job.proposed v1 (OpenVibe.Media server/jobs/queue.js enqueue; server/events.js recordJob). A job was proposed and waits for its owner: approving it queues it (media.job.queued), cancelling it ends it (media.job.cancelled); the worker never runs a proposal. The size-invariant validator (invariant.scan, created_by system:invariant.scan) proposes object.split and object.remux this way. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",
+ "description": "media.job.proposed v1 (OpenVibe.Media server/jobs/queue.js enqueue; server/events.js recordJob). A job was proposed and waits for its owner: approving it queues it (media.job.queued), cancelling it ends it (media.job.cancelled); the worker never runs a proposal. The size-invariant validator (invariant.scan) proposes object.split and object.remux this way. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",
"type": "object",
"type": "object",
- "required": ["id", "app_id", "object_id", "type", "status", "params", "result", "error", "error_code", "attempts", "max_attempts", "run_after", "cancel_requested", "idempotency_key", "created_by", "owner_user_id", "decided_by", "decided_at", "created_at", "updated_at", "started_at", "finished_at"],
+ "required": ["id", "app_id", "object_id", "type", "status", "attempts", "max_attempts", "error_code", "cancel_requested", "has_result", "run_after", "decided_at", "created_at", "updated_at", "started_at", "finished_at"],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "id": { "type": "string", "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$" },
+ "id": { "type": "string", "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it." },
"app_id": { "type": "string", "minLength": 1, "maxLength": 200, "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)." },
"app_id": { "type": "string", "minLength": 1, "maxLength": 200, "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)." },
"object_id": { "type": ["string", "null"], "maxLength": 200, "description": "The object the job works on; null for a tenant-wide job (invariant.scan)." },
"object_id": { "type": ["string", "null"], "maxLength": 200, "description": "The object the job works on; null for a tenant-wide job (invariant.scan)." },
"type": { "type": "string", "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$", "maxLength": 80, "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux." },
"type": { "type": "string", "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$", "maxLength": 80, "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux." },
"status": { "const": "proposed" },
"status": { "const": "proposed" },
- "params": { "type": "object", "description": "The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent." },
- "result": { "type": ["object", "null"], "description": "null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB." },
- "error": { "type": ["string", "null"], "maxLength": 1000, "description": "The last failure's message (free text), or null." },
- "error_code": { "type": ["string", "null"], "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null." },
- "attempts": { "type": "integer", "minimum": 0 },
+ "attempts": { "type": "integer", "minimum": 0, "description": "Attempts started so far." },
"max_attempts": { "type": "integer", "minimum": 1, "maximum": 10 },
"max_attempts": { "type": "integer", "minimum": 1, "maximum": 10 },
- "run_after": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$", "description": "A queued job waits until then (retry backoff); null otherwise." },
+ "error_code": { "type": "null" },
"cancel_requested": { "type": "boolean", "description": "Its owner asked a running job to stop." },
"cancel_requested": { "type": "boolean", "description": "Its owner asked a running job to stop." },
- "idempotency_key": { "type": ["string", "null"], "maxLength": 200, "description": "The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null." },
- "created_by": { "type": ["string", "null"], "description": "svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>." },
- "owner_user_id": { "type": ["integer", "null"], "minimum": 1, "description": "The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject." },
- "decided_by": { "type": ["string", "null"], "description": "Who approved or cancelled a proposal or queued job (same forms as created_by)." },
- "decided_at": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },
- "created_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },
- "updated_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },
+ "has_result": { "const": false, "description": "A proposal has no result." },
+ "run_after": { "type": ["string", "null"], "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "A queued job waits until then (retry backoff); null otherwise." },
+ "decided_at": { "type": "null", "description": "Nobody has decided yet." },
+ "created_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" },
+ "updated_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" },
"started_at": { "type": "null" },
"started_at": { "type": "null" },
"finished_at": { "type": "null" }
"finished_at": { "type": "null" }
}
}
@@ -2,31 +2,25 @@
@@ -2,31 +2,25 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/events/payloads/media.job.queued.v1.json",
"$id": "https://openvibe.network/contracts/events/payloads/media.job.queued.v1.json",
"title": "MediaJobQueuedPayload",
"title": "MediaJobQueuedPayload",
- "description": "media.job.queued v1 (OpenVibe.Media server/jobs/queue.js enqueue, approve; server/events.js recordJob). A job was accepted and queued: a new job (POST /api/v2/:app/jobs), or a proposal its owner approved (decided_by, decided_at). An Idempotency-Key replay, or a request joined to an identical job that is still active, creates no job and no event. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",
+ "description": "media.job.queued v1 (OpenVibe.Media server/jobs/queue.js enqueue, approve; server/events.js recordJob). A job was accepted and queued: a new job (POST /api/v2/:app/jobs), or a proposal its owner approved (decided_at). An Idempotency-Key replay, or a request joined to an identical job that is still active, creates no job and no event. A failed attempt that is queued again is media.job.retrying, not this. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",
"type": "object",
"type": "object",
- "required": ["id", "app_id", "object_id", "type", "status", "params", "result", "error", "error_code", "attempts", "max_attempts", "run_after", "cancel_requested", "idempotency_key", "created_by", "owner_user_id", "decided_by", "decided_at", "created_at", "updated_at", "started_at", "finished_at"],
+ "required": ["id", "app_id", "object_id", "type", "status", "attempts", "max_attempts", "error_code", "cancel_requested", "has_result", "run_after", "decided_at", "created_at", "updated_at", "started_at", "finished_at"],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "id": { "type": "string", "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$" },
+ "id": { "type": "string", "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it." },
"app_id": { "type": "string", "minLength": 1, "maxLength": 200, "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)." },
"app_id": { "type": "string", "minLength": 1, "maxLength": 200, "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)." },
"object_id": { "type": ["string", "null"], "maxLength": 200, "description": "The object the job works on; null for a tenant-wide job (invariant.scan)." },
"object_id": { "type": ["string", "null"], "maxLength": 200, "description": "The object the job works on; null for a tenant-wide job (invariant.scan)." },
"type": { "type": "string", "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$", "maxLength": 80, "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux." },
"type": { "type": "string", "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$", "maxLength": 80, "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux." },
"status": { "const": "queued" },
"status": { "const": "queued" },
- "params": { "type": "object", "description": "The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent." },
- "result": { "type": ["object", "null"], "description": "null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB." },
- "error": { "type": ["string", "null"], "maxLength": 1000, "description": "The last failure's message (free text), or null." },
- "error_code": { "type": ["string", "null"], "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null." },
- "attempts": { "type": "integer", "minimum": 0 },
+ "attempts": { "type": "integer", "minimum": 0, "description": "Attempts started so far." },
"max_attempts": { "type": "integer", "minimum": 1, "maximum": 10 },
"max_attempts": { "type": "integer", "minimum": 1, "maximum": 10 },
- "run_after": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$", "description": "A queued job waits until then (retry backoff); null otherwise." },
+ "error_code": { "type": "null" },
"cancel_requested": { "type": "boolean", "description": "Its owner asked a running job to stop." },
"cancel_requested": { "type": "boolean", "description": "Its owner asked a running job to stop." },
- "idempotency_key": { "type": ["string", "null"], "maxLength": 200, "description": "The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null." },
- "created_by": { "type": ["string", "null"], "description": "svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>." },
- "owner_user_id": { "type": ["integer", "null"], "minimum": 1, "description": "The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject." },
- "decided_by": { "type": ["string", "null"], "description": "Who approved or cancelled a proposal or queued job (same forms as created_by)." },
- "decided_at": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },
- "created_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },
- "updated_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },
+ "has_result": { "const": false, "description": "A job that never ran has no result." },
+ "run_after": { "type": ["string", "null"], "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "A queued job waits until then (retry backoff); null otherwise." },
+ "decided_at": { "type": ["string", "null"], "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise." },
+ "created_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" },
+ "updated_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" },
"started_at": { "type": "null" },
"started_at": { "type": "null" },
"finished_at": { "type": "null" }
"finished_at": { "type": "null" }
}
}
@@ -2,32 +2,26 @@
@@ -2,32 +2,26 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/events/payloads/media.job.retrying.v1.json",
"$id": "https://openvibe.network/contracts/events/payloads/media.job.retrying.v1.json",
"title": "MediaJobRetryingPayload",
"title": "MediaJobRetryingPayload",
- "description": "media.job.retrying v1 (OpenVibe.Media server/jobs/queue.js fail with a retry; server/events.js recordJob). An attempt failed and the job is queued again after a backoff (run_after); error and error_code say why. A running job nobody holds any more (the service restarted, or the worker stopped renewing its lease) is retried this way with error_code interrupted while it has attempts left. status is queued. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",
+ "description": "media.job.retrying v1 (OpenVibe.Media server/jobs/queue.js fail with a retry; server/events.js recordJob). An attempt failed and the job is queued again (status queued) after a backoff (run_after); error_code says why when the handler gave a code. A running job nobody holds any more (the service restarted, or the worker stopped renewing its lease) is retried this way with error_code interrupted while it has attempts left. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",
"type": "object",
"type": "object",
- "required": ["id", "app_id", "object_id", "type", "status", "params", "result", "error", "error_code", "attempts", "max_attempts", "run_after", "cancel_requested", "idempotency_key", "created_by", "owner_user_id", "decided_by", "decided_at", "created_at", "updated_at", "started_at", "finished_at"],
+ "required": ["id", "app_id", "object_id", "type", "status", "attempts", "max_attempts", "error_code", "cancel_requested", "has_result", "run_after", "decided_at", "created_at", "updated_at", "started_at", "finished_at"],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "id": { "type": "string", "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$" },
+ "id": { "type": "string", "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it." },
"app_id": { "type": "string", "minLength": 1, "maxLength": 200, "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)." },
"app_id": { "type": "string", "minLength": 1, "maxLength": 200, "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)." },
"object_id": { "type": ["string", "null"], "maxLength": 200, "description": "The object the job works on; null for a tenant-wide job (invariant.scan)." },
"object_id": { "type": ["string", "null"], "maxLength": 200, "description": "The object the job works on; null for a tenant-wide job (invariant.scan)." },
"type": { "type": "string", "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$", "maxLength": 80, "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux." },
"type": { "type": "string", "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$", "maxLength": 80, "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux." },
- "status": { "const": "queued" },
- "params": { "type": "object", "description": "The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent." },
- "result": { "type": ["object", "null"], "description": "null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB." },
- "error": { "type": "string", "maxLength": 1000, "description": "The failure's message, free text from the handler." },
- "error_code": { "type": ["string", "null"], "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null." },
- "attempts": { "type": "integer", "minimum": 0 },
+ "status": { "const": "queued", "description": "retrying carries the job's state, queued." },
+ "attempts": { "type": "integer", "minimum": 1, "description": "Attempts started so far, the failed one included." },
"max_attempts": { "type": "integer", "minimum": 1, "maximum": 10 },
"max_attempts": { "type": "integer", "minimum": 1, "maximum": 10 },
- "run_after": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$", "description": "Not before this time (the retry backoff)." },
+ "error_code": { "type": ["string", "null"], "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it." },
"cancel_requested": { "type": "boolean", "description": "Its owner asked a running job to stop." },
"cancel_requested": { "type": "boolean", "description": "Its owner asked a running job to stop." },
- "idempotency_key": { "type": ["string", "null"], "maxLength": 200, "description": "The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null." },
- "created_by": { "type": ["string", "null"], "description": "svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>." },
- "owner_user_id": { "type": ["integer", "null"], "minimum": 1, "description": "The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject." },
- "decided_by": { "type": ["string", "null"], "description": "Who approved or cancelled a proposal or queued job (same forms as created_by)." },
- "decided_at": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },
- "created_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },
- "updated_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },
- "started_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },
+ "has_result": { "const": false, "description": "A job waiting for its retry has no result." },
+ "run_after": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "Not before this time (the retry backoff)." },
+ "decided_at": { "type": ["string", "null"], "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise." },
+ "created_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" },
+ "updated_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" },
+ "started_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "The first attempt's start." },
"finished_at": { "type": "null" }
"finished_at": { "type": "null" }
}
}
}
}
@@ -2,32 +2,26 @@
@@ -2,32 +2,26 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/events/payloads/media.job.started.v1.json",
"$id": "https://openvibe.network/contracts/events/payloads/media.job.started.v1.json",
"title": "MediaJobStartedPayload",
"title": "MediaJobStartedPayload",
- "description": "media.job.started v1 (OpenVibe.Media server/jobs/queue.js claim; server/events.js recordJob). The worker claimed the job and it is running: attempts counts this attempt, started_at stays the first attempt's start. A job that is retried starts again and is announced again. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",
+ "description": "media.job.started v1 (OpenVibe.Media server/jobs/queue.js claim; server/events.js recordJob). The worker claimed the job and it is running (status running): attempts counts this attempt, started_at stays the first attempt's start. A job that is retried starts again and is announced again; error_code is still the previous attempt's until the job finishes. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",
"type": "object",
"type": "object",
- "required": ["id", "app_id", "object_id", "type", "status", "params", "result", "error", "error_code", "attempts", "max_attempts", "run_after", "cancel_requested", "idempotency_key", "created_by", "owner_user_id", "decided_by", "decided_at", "created_at", "updated_at", "started_at", "finished_at"],
+ "required": ["id", "app_id", "object_id", "type", "status", "attempts", "max_attempts", "error_code", "cancel_requested", "has_result", "run_after", "decided_at", "created_at", "updated_at", "started_at", "finished_at"],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "id": { "type": "string", "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$" },
+ "id": { "type": "string", "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it." },
"app_id": { "type": "string", "minLength": 1, "maxLength": 200, "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)." },
"app_id": { "type": "string", "minLength": 1, "maxLength": 200, "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)." },
"object_id": { "type": ["string", "null"], "maxLength": 200, "description": "The object the job works on; null for a tenant-wide job (invariant.scan)." },
"object_id": { "type": ["string", "null"], "maxLength": 200, "description": "The object the job works on; null for a tenant-wide job (invariant.scan)." },
"type": { "type": "string", "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$", "maxLength": 80, "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux." },
"type": { "type": "string", "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$", "maxLength": 80, "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux." },
- "status": { "const": "running" },
- "params": { "type": "object", "description": "The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent." },
- "result": { "type": ["object", "null"], "description": "null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB." },
- "error": { "type": ["string", "null"], "maxLength": 1000, "description": "The last failure's message (free text), or null." },
- "error_code": { "type": ["string", "null"], "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null." },
- "attempts": { "type": "integer", "minimum": 0 },
+ "status": { "const": "running", "description": "started carries the job's state, running." },
+ "attempts": { "type": "integer", "minimum": 1, "description": "Attempts started so far, this one included." },
"max_attempts": { "type": "integer", "minimum": 1, "maximum": 10 },
"max_attempts": { "type": "integer", "minimum": 1, "maximum": 10 },
- "run_after": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$", "description": "A queued job waits until then (retry backoff); null otherwise." },
+ "error_code": { "type": ["string", "null"], "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it." },
"cancel_requested": { "type": "boolean", "description": "Its owner asked a running job to stop." },
"cancel_requested": { "type": "boolean", "description": "Its owner asked a running job to stop." },
- "idempotency_key": { "type": ["string", "null"], "maxLength": 200, "description": "The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null." },
- "created_by": { "type": ["string", "null"], "description": "svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>." },
- "owner_user_id": { "type": ["integer", "null"], "minimum": 1, "description": "The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject." },
- "decided_by": { "type": ["string", "null"], "description": "Who approved or cancelled a proposal or queued job (same forms as created_by)." },
- "decided_at": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },
- "created_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },
- "updated_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },
- "started_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },
+ "has_result": { "const": false, "description": "A running job has no result yet." },
+ "run_after": { "type": "null", "description": "Cleared when the job is claimed." },
+ "decided_at": { "type": ["string", "null"], "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise." },
+ "created_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" },
+ "updated_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" },
+ "started_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "The first attempt's start." },
"finished_at": { "type": "null" }
"finished_at": { "type": "null" }
}
}
}
}
@@ -2,32 +2,26 @@
@@ -2,32 +2,26 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/events/payloads/media.job.succeeded.v1.json",
"$id": "https://openvibe.network/contracts/events/payloads/media.job.succeeded.v1.json",
"title": "MediaJobSucceededPayload",
"title": "MediaJobSucceededPayload",
- "description": "media.job.succeeded v1 (OpenVibe.Media server/jobs/queue.js succeed; server/events.js recordJob). The job finished: result is its type's result (thumbnail.regenerate { url, kind, id }, object.split { source_id, parts, … }, object.remux { source_id, object_id, … }, invariant.scan { thresholds, counts, … }). Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",
+ "description": "media.job.succeeded v1 (OpenVibe.Media server/jobs/queue.js succeed; server/events.js recordJob). The job finished. Its result (thumbnail.regenerate { url, kind, id }, object.split { source_id, parts, … }, object.remux { source_id, object_id, … }, invariant.scan { thresholds, counts, … }) is not in the event: has_result says whether there is one to GET. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",
"type": "object",
"type": "object",
- "required": ["id", "app_id", "object_id", "type", "status", "params", "result", "error", "error_code", "attempts", "max_attempts", "run_after", "cancel_requested", "idempotency_key", "created_by", "owner_user_id", "decided_by", "decided_at", "created_at", "updated_at", "started_at", "finished_at"],
+ "required": ["id", "app_id", "object_id", "type", "status", "attempts", "max_attempts", "error_code", "cancel_requested", "has_result", "run_after", "decided_at", "created_at", "updated_at", "started_at", "finished_at"],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "id": { "type": "string", "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$" },
+ "id": { "type": "string", "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it." },
"app_id": { "type": "string", "minLength": 1, "maxLength": 200, "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)." },
"app_id": { "type": "string", "minLength": 1, "maxLength": 200, "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)." },
"object_id": { "type": ["string", "null"], "maxLength": 200, "description": "The object the job works on; null for a tenant-wide job (invariant.scan)." },
"object_id": { "type": ["string", "null"], "maxLength": 200, "description": "The object the job works on; null for a tenant-wide job (invariant.scan)." },
"type": { "type": "string", "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$", "maxLength": 80, "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux." },
"type": { "type": "string", "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$", "maxLength": 80, "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux." },
"status": { "const": "succeeded" },
"status": { "const": "succeeded" },
- "params": { "type": "object", "description": "The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent." },
- "result": { "type": ["object", "null"], "description": "The job type's result, or { omitted: true, reason } when it is larger than 8 KB (GET the job for it)." },
- "error": { "type": "null" },
- "error_code": { "type": "null" },
- "attempts": { "type": "integer", "minimum": 0 },
+ "attempts": { "type": "integer", "minimum": 1, "description": "Attempts started, the successful one included." },
"max_attempts": { "type": "integer", "minimum": 1, "maximum": 10 },
"max_attempts": { "type": "integer", "minimum": 1, "maximum": 10 },
- "run_after": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$", "description": "A queued job waits until then (retry backoff); null otherwise." },
+ "error_code": { "type": "null", "description": "A success clears the last failure." },
"cancel_requested": { "type": "boolean", "description": "Its owner asked a running job to stop." },
"cancel_requested": { "type": "boolean", "description": "Its owner asked a running job to stop." },
- "idempotency_key": { "type": ["string", "null"], "maxLength": 200, "description": "The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null." },
- "created_by": { "type": ["string", "null"], "description": "svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>." },
- "owner_user_id": { "type": ["integer", "null"], "minimum": 1, "description": "The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject." },
- "decided_by": { "type": ["string", "null"], "description": "Who approved or cancelled a proposal or queued job (same forms as created_by)." },
- "decided_at": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },
- "created_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },
- "updated_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },
- "started_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" },
- "finished_at": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$" }
+ "has_result": { "type": "boolean", "description": "The job holds a result. The result itself is never in the event: GET the job with a tenant token for it." },
+ "run_after": { "type": "null" },
+ "decided_at": { "type": ["string", "null"], "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise." },
+ "created_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" },
+ "updated_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" },
+ "started_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$", "description": "The first attempt's start." },
+ "finished_at": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$" }
}
}
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
+{
+ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
+ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "type": "object.split",
+ "status": "cancelled",
+ "attempts": 0,
+ "max_attempts": 3,
+ "error_code": "cancelled",
+ "cancel_requested": false,
+ "has_result": false,
+ "run_after": null,
+ "decided_at": "2026-09-23T18:10:40.000Z",
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:10:40.000Z",
+ "started_at": null,
+ "finished_at": "2026-09-23T18:10:40.000Z",
+ "decided_by": "app:live:user:57"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
+{
+ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
+ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "type": "object.split",
+ "status": "cancelled",
+ "attempts": 0,
+ "max_attempts": 3,
+ "error_code": "cancelled",
+ "cancel_requested": false,
+ "has_result": false,
+ "run_after": null,
+ "decided_at": "2026-09-23T18:10:40.000Z",
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:10:40.000Z",
+ "started_at": null,
+ "finished_at": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
+{
+ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
+ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "type": "thumbnail.regenerate",
+ "status": "cancelled",
+ "attempts": 0,
+ "max_attempts": 3,
+ "error_code": null,
+ "cancel_requested": false,
+ "has_result": false,
+ "run_after": null,
+ "decided_at": "2026-09-23T18:02:30.000Z",
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:02:30.000Z",
+ "started_at": null,
+ "finished_at": "2026-09-23T18:02:30.000Z"
+}
@@ -1,26 +1,18 @@
@@ -1,26 +1,18 @@
{
{
- "app_id": "live",
"id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
"id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
"object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"type": "object.split",
"type": "object.split",
"status": "cancelled",
"status": "cancelled",
- "params": {
- "parts": 3
- },
- "result": null,
- "error": "cancelled by its owner",
- "error_code": "cancelled",
"attempts": 1,
"attempts": 1,
"max_attempts": 3,
"max_attempts": 3,
- "run_after": null,
+ "error_code": "cancelled",
"cancel_requested": true,
"cancel_requested": true,
- "idempotency_key": null,
- "created_by": "svc:live",
- "owner_user_id": null,
- "decided_by": "app:live:user:57",
+ "has_result": false,
+ "run_after": null,
"decided_at": null,
"decided_at": null,
- "created_at": "2026-09-23 18:02:11",
- "updated_at": "2026-09-23 18:04:00",
- "started_at": "2026-09-23 18:02:12",
- "finished_at": "2026-09-23 18:04:00"
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:04:00.000Z",
+ "started_at": "2026-09-23T18:02:12.000Z",
+ "finished_at": "2026-09-23T18:04:00.000Z"
}
}
@@ -1,32 +1,18 @@
@@ -1,32 +1,18 @@
{
{
- "app_id": "live",
"id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
"id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
"object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"type": "object.split",
"type": "object.split",
"status": "cancelled",
"status": "cancelled",
- "params": {
- "source_size_bytes": 9663676416,
- "target_bytes": 2147483648,
- "max_bytes": 4294967296,
- "reason": "public_object_too_large",
- "parts": 5,
- "segment_seconds": 2160,
- "source_duration_seconds": 10800
- },
- "result": null,
- "error": "the object no longer breaks the public size policy",
- "error_code": "cancelled",
"attempts": 0,
"attempts": 0,
"max_attempts": 3,
"max_attempts": 3,
- "run_after": null,
+ "error_code": "cancelled",
"cancel_requested": false,
"cancel_requested": false,
- "idempotency_key": "invariant:med_01JAB2C3D4E5F6G7H8J9K0MNPQ:object.split",
- "created_by": "system:invariant.scan",
- "owner_user_id": null,
- "decided_by": "system:invariant.scan",
- "decided_at": "2026-09-24 04:00:03",
- "created_at": "2026-09-23 18:02:11",
- "updated_at": "2026-09-24 04:00:03",
+ "has_result": false,
+ "run_after": null,
+ "decided_at": "2026-09-24T04:00:03.000Z",
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-24T04:00:03.000Z",
"started_at": null,
"started_at": null,
- "finished_at": "2026-09-24 04:00:03"
+ "finished_at": "2026-09-24T04:00:03.000Z"
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
+{
+ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
+ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "type": "object.split",
+ "status": "failed",
+ "attempts": 3,
+ "max_attempts": 3,
+ "error_code": "media_unavailable",
+ "cancel_requested": false,
+ "has_result": false,
+ "run_after": null,
+ "decided_at": null,
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:02:13.000Z",
+ "started_at": "2026-09-23T18:02:12.000Z",
+ "finished_at": "2026-09-23T18:02:13.000Z",
+ "error": "The source bytes are unavailable (no local file and no cloud copy)"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
+{
+ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
+ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "type": "object.split",
+ "status": "failed",
+ "attempts": 3,
+ "max_attempts": 3,
+ "error_code": "media_unavailable",
+ "cancel_requested": false,
+ "has_result": false,
+ "run_after": null,
+ "decided_at": null,
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:02:13.000Z",
+ "started_at": "2026-09-23T18:02:12.000Z",
+ "finished_at": "2026-09-23 18:02:13"
+}
@@ -1,26 +1,18 @@
@@ -1,26 +1,18 @@
{
{
- "app_id": "live",
"id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
"id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
"object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"type": "object.split",
"type": "object.split",
"status": "failed",
"status": "failed",
- "params": {
- "parts": 3
- },
- "result": null,
- "error": "The source bytes are unavailable (no local file and no cloud copy)",
- "error_code": "media_unavailable",
"attempts": 3,
"attempts": 3,
"max_attempts": 3,
"max_attempts": 3,
- "run_after": null,
+ "error_code": "media_unavailable",
"cancel_requested": false,
"cancel_requested": false,
- "idempotency_key": null,
- "created_by": "svc:live",
- "owner_user_id": null,
- "decided_by": null,
+ "has_result": false,
+ "run_after": null,
"decided_at": null,
"decided_at": null,
- "created_at": "2026-09-23 18:02:11",
- "updated_at": "2026-09-23 18:02:13",
- "started_at": "2026-09-23 18:02:12",
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:02:13.000Z",
+ "started_at": "2026-09-23T18:02:12.000Z",
"finished_at": null
"finished_at": null
}
}
@@ -1,26 +1,18 @@
@@ -1,26 +1,18 @@
{
{
- "app_id": "live",
"id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
"id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
"object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"type": "object.split",
"type": "object.split",
"status": "failed",
"status": "failed",
- "params": {
- "parts": 3
- },
- "result": null,
- "error": "The source bytes are unavailable (no local file and no cloud copy)",
- "error_code": "media_unavailable",
"attempts": 3,
"attempts": 3,
"max_attempts": 3,
"max_attempts": 3,
- "run_after": null,
+ "error_code": "media_unavailable",
"cancel_requested": false,
"cancel_requested": false,
- "idempotency_key": null,
- "created_by": "svc:live",
- "owner_user_id": null,
- "decided_by": null,
+ "has_result": false,
+ "run_after": null,
"decided_at": null,
"decided_at": null,
- "created_at": "2026-09-23 18:02:11",
- "updated_at": "2026-09-23 18:02:13",
- "started_at": "2026-09-23 18:02:12",
- "finished_at": "2026-09-23 18:02:13"
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:02:13.000Z",
+ "started_at": "2026-09-23T18:02:12.000Z",
+ "finished_at": "2026-09-23T18:02:13.000Z"
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
+{
+ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
+ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "type": "object.split",
+ "status": "failed",
+ "attempts": 3,
+ "max_attempts": 3,
+ "error_code": null,
+ "cancel_requested": false,
+ "has_result": true,
+ "run_after": null,
+ "decided_at": null,
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:12:13.000Z",
+ "started_at": "2026-09-23T18:02:12.000Z",
+ "finished_at": "2026-09-23T18:12:13.000Z"
+}
@@ -1,32 +1,18 @@
@@ -1,32 +1,18 @@
{
{
- "app_id": "live",
"id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
"id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
"object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"type": "object.split",
"type": "object.split",
"status": "proposed",
"status": "proposed",
- "params": {
- "source_size_bytes": 9663676416,
- "target_bytes": 2147483648,
- "max_bytes": 4294967296,
- "reason": "public_object_too_large",
- "parts": 5,
- "segment_seconds": 2160,
- "source_duration_seconds": 10800
- },
- "result": null,
- "error": null,
- "error_code": null,
"attempts": 0,
"attempts": 0,
"max_attempts": 3,
"max_attempts": 3,
- "run_after": null,
+ "error_code": null,
"cancel_requested": false,
"cancel_requested": false,
- "idempotency_key": "invariant:med_01JAB2C3D4E5F6G7H8J9K0MNPQ:object.split",
- "created_by": "system:invariant.scan",
- "owner_user_id": null,
- "decided_by": null,
+ "has_result": false,
+ "run_after": null,
"decided_at": null,
"decided_at": null,
- "created_at": "2026-09-23 18:02:11",
- "updated_at": "2026-09-23 18:02:11",
- "started_at": "2026-09-23 18:02:12",
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:02:11.000Z",
+ "started_at": "2026-09-23T18:02:12.000Z",
"finished_at": null
"finished_at": null
}
}
deleted file mode 100644
deleted file mode 100644
@@ -1,35 +0,0 @@
@@ -1,35 +0,0 @@
-{
- "app_id": "live",
- "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
- "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
- "type": "object.split",
- "status": "proposed",
- "params": {
- "source_size_bytes": 9663676416,
- "target_bytes": 2147483648,
- "max_bytes": 4294967296,
- "reason": "public_object_too_large",
- "parts": 5,
- "segment_seconds": 2160,
- "source_duration_seconds": 10800
- },
- "result": null,
- "error": null,
- "error_code": null,
- "attempts": 0,
- "max_attempts": 3,
- "run_after": null,
- "cancel_requested": false,
- "idempotency_key": "invariant:med_01JAB2C3D4E5F6G7H8J9K0MNPQ:object.split",
- "created_by": "system:invariant.scan",
- "owner_user_id": null,
- "decided_by": null,
- "decided_at": null,
- "created_at": "2026-09-23 18:02:11",
- "updated_at": "2026-09-23 18:02:11",
- "started_at": null,
- "finished_at": null,
- "checkpoint": {
- "part": 2
- }
-}
new file mode 100644
new file mode 100644
@@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
+{
+ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
+ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "type": "object.split",
+ "status": "proposed",
+ "attempts": 0,
+ "max_attempts": 3,
+ "error_code": null,
+ "cancel_requested": false,
+ "has_result": false,
+ "run_after": null,
+ "decided_at": null,
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:02:11.000Z",
+ "started_at": null,
+ "finished_at": null,
+ "created_by": "system:invariant.scan",
+ "idempotency_key": "invariant:med_01JAB2C3D4E5F6G7H8J9K0MNPQ:object.split"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
+{
+ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
+ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "type": "object.split",
+ "status": "proposed",
+ "attempts": 0,
+ "max_attempts": 3,
+ "error_code": null,
+ "cancel_requested": false,
+ "has_result": false,
+ "run_after": null,
+ "decided_at": null,
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:02:11.000Z",
+ "started_at": null,
+ "finished_at": null,
+ "params": {
+ "source_size_bytes": 9663676416,
+ "target_bytes": 2147483648,
+ "reason": "public_object_too_large",
+ "parts": 5
+ }
+}
@@ -1,32 +1,18 @@
@@ -1,32 +1,18 @@
{
{
- "app_id": "live",
"id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
"id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
"object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"type": "object.split",
"type": "object.split",
"status": "proposed",
"status": "proposed",
- "params": {
- "source_size_bytes": 9663676416,
- "target_bytes": 2147483648,
- "max_bytes": 4294967296,
- "reason": "public_object_too_large",
- "parts": 5,
- "segment_seconds": 2160,
- "source_duration_seconds": 10800
- },
- "result": null,
- "error": null,
- "error_code": null,
"attempts": 0,
"attempts": 0,
"max_attempts": 3,
"max_attempts": 3,
- "run_after": null,
+ "error_code": null,
"cancel_requested": false,
"cancel_requested": false,
- "idempotency_key": "invariant:med_01JAB2C3D4E5F6G7H8J9K0MNPQ:object.split",
- "created_by": "system:invariant.scan",
- "owner_user_id": null,
- "decided_by": null,
+ "has_result": false,
+ "run_after": null,
"decided_at": null,
"decided_at": null,
- "created_at": "2026-09-23 18:02:11",
- "updated_at": "2026-09-23 18:02:11",
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:02:11.000Z",
"started_at": null,
"started_at": null,
"finished_at": null
"finished_at": null
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
+{
+ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "type": "object.remux",
+ "status": "proposed",
+ "attempts": 0,
+ "max_attempts": 3,
+ "error_code": null,
+ "cancel_requested": false,
+ "has_result": false,
+ "run_after": null,
+ "decided_at": null,
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:02:11.000Z",
+ "started_at": null,
+ "finished_at": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
+{
+ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
+ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "type": "object.split",
+ "status": "queued",
+ "attempts": 1,
+ "max_attempts": 3,
+ "error_code": null,
+ "cancel_requested": false,
+ "has_result": false,
+ "run_after": null,
+ "decided_at": null,
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:02:11.000Z",
+ "started_at": "2026-09-23T18:02:12.000Z",
+ "finished_at": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
+{
+ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
+ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "type": "object.split",
+ "status": "queued",
+ "attempts": 0,
+ "max_attempts": 3,
+ "error_code": null,
+ "cancel_requested": false,
+ "has_result": false,
+ "run_after": null,
+ "decided_at": null,
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:02:11.000Z",
+ "started_at": null,
+ "finished_at": null,
+ "idempotency_key": "thumb-4211-2026-09-23"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
+{
+ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
+ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "type": "thumbnail.regenerate",
+ "status": "queued",
+ "attempts": 0,
+ "max_attempts": 3,
+ "error_code": null,
+ "cancel_requested": false,
+ "has_result": false,
+ "run_after": null,
+ "decided_at": null,
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:02:11.000Z",
+ "started_at": null,
+ "finished_at": null,
+ "created_by": "app:live:user:57",
+ "owner_user_id": 57
+}
deleted file mode 100644
deleted file mode 100644
@@ -1,26 +0,0 @@
@@ -1,26 +0,0 @@
-{
- "app_id": "live",
- "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
- "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
- "type": "object.split",
- "status": "queued",
- "params": {
- "parts": 3
- },
- "result": null,
- "error": null,
- "error_code": null,
- "attempts": 0,
- "max_attempts": 3,
- "run_after": null,
- "cancel_requested": false,
- "idempotency_key": null,
- "created_by": "svc:live",
- "owner_user_id": null,
- "decided_by": null,
- "decided_at": null,
- "created_at": "2026-09-23T18:02:11Z",
- "updated_at": "2026-09-23T18:02:11Z",
- "started_at": null,
- "finished_at": null
-}
deleted file mode 100644
deleted file mode 100644
@@ -1,26 +0,0 @@
@@ -1,26 +0,0 @@
-{
- "app_id": "live",
- "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
- "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
- "type": "object.split",
- "status": "queued",
- "params": {
- "parts": 3
- },
- "result": null,
- "error": null,
- "error_code": null,
- "attempts": 0,
- "max_attempts": 3,
- "run_after": null,
- "cancel_requested": false,
- "idempotency_key": null,
- "created_by": "svc:live",
- "owner_user_id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
- "decided_by": null,
- "decided_at": null,
- "created_at": "2026-09-23 18:02:11",
- "updated_at": "2026-09-23 18:02:11",
- "started_at": null,
- "finished_at": null
-}
new file mode 100644
new file mode 100644
@@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
+{
+ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
+ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "type": "object.split",
+ "status": "queued",
+ "attempts": 0,
+ "max_attempts": 3,
+ "error_code": null,
+ "cancel_requested": false,
+ "has_result": false,
+ "run_after": null,
+ "decided_at": null,
+ "created_at": "2026-09-23 18:02:11",
+ "updated_at": "2026-09-23 18:02:11",
+ "started_at": null,
+ "finished_at": null
+}
@@ -1,32 +1,18 @@
@@ -1,32 +1,18 @@
{
{
- "app_id": "live",
"id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
"id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
"object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"type": "object.split",
"type": "object.split",
"status": "queued",
"status": "queued",
- "params": {
- "source_size_bytes": 9663676416,
- "target_bytes": 2147483648,
- "max_bytes": 4294967296,
- "reason": "public_object_too_large",
- "parts": 5,
- "segment_seconds": 2160,
- "source_duration_seconds": 10800
- },
- "result": null,
- "error": null,
- "error_code": null,
"attempts": 0,
"attempts": 0,
"max_attempts": 3,
"max_attempts": 3,
- "run_after": null,
+ "error_code": null,
"cancel_requested": false,
"cancel_requested": false,
- "idempotency_key": "invariant:med_01JAB2C3D4E5F6G7H8J9K0MNPQ:object.split",
- "created_by": "system:invariant.scan",
- "owner_user_id": null,
- "decided_by": "app:live:user:57",
- "decided_at": "2026-09-23 18:10:40",
- "created_at": "2026-09-23 18:02:11",
- "updated_at": "2026-09-23 18:10:40",
+ "has_result": false,
+ "run_after": null,
+ "decided_at": "2026-09-23T18:10:40.000Z",
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:10:40.000Z",
"started_at": null,
"started_at": null,
"finished_at": null
"finished_at": null
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
+{
+ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
+ "object_id": null,
+ "type": "invariant.scan",
+ "status": "queued",
+ "attempts": 0,
+ "max_attempts": 3,
+ "error_code": null,
+ "cancel_requested": false,
+ "has_result": false,
+ "run_after": null,
+ "decided_at": null,
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:02:11.000Z",
+ "started_at": null,
+ "finished_at": null
+}
deleted file mode 100644
deleted file mode 100644
@@ -1,27 +0,0 @@
@@ -1,27 +0,0 @@
-{
- "app_id": "live",
- "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
- "object_id": null,
- "type": "thumbnail.regenerate",
- "status": "queued",
- "params": {
- "kind": "vod",
- "id": 4211
- },
- "result": null,
- "error": null,
- "error_code": null,
- "attempts": 0,
- "max_attempts": 2,
- "run_after": null,
- "cancel_requested": false,
- "idempotency_key": "thumb-4211-2026-09-23",
- "created_by": "app:live:user:57",
- "owner_user_id": 57,
- "decided_by": null,
- "decided_at": null,
- "created_at": "2026-09-23 18:02:11",
- "updated_at": "2026-09-23 18:02:11",
- "started_at": null,
- "finished_at": null
-}
new file mode 100644
new file mode 100644
@@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
+{
+ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
+ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "type": "thumbnail.regenerate",
+ "status": "queued",
+ "attempts": 0,
+ "max_attempts": 2,
+ "error_code": null,
+ "cancel_requested": false,
+ "has_result": false,
+ "run_after": null,
+ "decided_at": null,
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:02:11.000Z",
+ "started_at": null,
+ "finished_at": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
+{
+ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
+ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "type": "object.split",
+ "status": "queued",
+ "attempts": 1,
+ "max_attempts": 3,
+ "error_code": "interrupted",
+ "cancel_requested": false,
+ "has_result": false,
+ "run_after": "2026-09-23T18:05:42.000Z",
+ "decided_at": null,
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:05:12.000Z",
+ "started_at": "2026-09-23T18:02:12.000Z",
+ "finished_at": null,
+ "error": "interrupted by a restart"
+}
@@ -1,26 +1,18 @@
@@ -1,26 +1,18 @@
{
{
- "app_id": "live",
"id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
"id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
"object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"type": "object.split",
"type": "object.split",
"status": "queued",
"status": "queued",
- "params": {
- "parts": 3
- },
- "result": null,
- "error": "interrupted by a restart",
- "error_code": "interrupted",
"attempts": 1,
"attempts": 1,
"max_attempts": 3,
"max_attempts": 3,
- "run_after": null,
+ "error_code": "interrupted",
"cancel_requested": false,
"cancel_requested": false,
- "idempotency_key": null,
- "created_by": "svc:live",
- "owner_user_id": null,
- "decided_by": null,
+ "has_result": false,
+ "run_after": null,
"decided_at": null,
"decided_at": null,
- "created_at": "2026-09-23 18:02:11",
- "updated_at": "2026-09-23 18:05:12",
- "started_at": "2026-09-23 18:02:12",
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:05:12.000Z",
+ "started_at": "2026-09-23T18:02:12.000Z",
"finished_at": null
"finished_at": null
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
+{
+ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
+ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "type": "object.split",
+ "status": "retrying",
+ "attempts": 1,
+ "max_attempts": 3,
+ "error_code": "interrupted",
+ "cancel_requested": false,
+ "has_result": false,
+ "run_after": "2026-09-23T18:05:42.000Z",
+ "decided_at": null,
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:05:12.000Z",
+ "started_at": "2026-09-23T18:02:12.000Z",
+ "finished_at": null
+}
@@ -1,26 +1,18 @@
@@ -1,26 +1,18 @@
{
{
- "app_id": "live",
"id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
"id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
"object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"type": "object.split",
"type": "object.split",
"status": "queued",
"status": "queued",
- "params": {
- "parts": 3
- },
- "result": null,
- "error": "interrupted by a restart",
- "error_code": "interrupted",
"attempts": 1,
"attempts": 1,
"max_attempts": 3,
"max_attempts": 3,
- "run_after": "2026-09-23 18:05:42",
+ "error_code": "interrupted",
"cancel_requested": false,
"cancel_requested": false,
- "idempotency_key": null,
- "created_by": "svc:live",
- "owner_user_id": null,
- "decided_by": null,
+ "has_result": false,
+ "run_after": "2026-09-23T18:05:42.000Z",
"decided_at": null,
"decided_at": null,
- "created_at": "2026-09-23 18:02:11",
- "updated_at": "2026-09-23 18:05:12",
- "started_at": "2026-09-23 18:02:12",
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:05:12.000Z",
+ "started_at": "2026-09-23T18:02:12.000Z",
"finished_at": null
"finished_at": null
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
+{
+ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
+ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "type": "object.split",
+ "status": "queued",
+ "attempts": 1,
+ "max_attempts": 3,
+ "error_code": null,
+ "cancel_requested": false,
+ "has_result": false,
+ "run_after": "2026-09-23T18:02:43.000Z",
+ "decided_at": null,
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:02:13.000Z",
+ "started_at": "2026-09-23T18:02:12.000Z",
+ "finished_at": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
+{
+ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
+ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "type": "object.split",
+ "status": "running",
+ "attempts": 1,
+ "max_attempts": 3,
+ "error_code": null,
+ "cancel_requested": false,
+ "has_result": false,
+ "run_after": null,
+ "decided_at": null,
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:02:12.000Z",
+ "started_at": null,
+ "finished_at": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
+{
+ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
+ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "type": "object.split",
+ "status": "running",
+ "attempts": 1,
+ "max_attempts": 3,
+ "error_code": null,
+ "cancel_requested": false,
+ "has_result": false,
+ "run_after": null,
+ "decided_at": null,
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:02:12.000Z",
+ "started_at": "2026-09-23T20:02:12.000+02:00",
+ "finished_at": null
+}
@@ -1,26 +1,18 @@
@@ -1,26 +1,18 @@
{
{
- "app_id": "live",
"id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
"id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
"object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"type": "object.split",
"type": "object.split",
"status": "started",
"status": "started",
- "params": {
- "parts": 3
- },
- "result": null,
- "error": null,
- "error_code": null,
"attempts": 1,
"attempts": 1,
"max_attempts": 3,
"max_attempts": 3,
- "run_after": null,
+ "error_code": null,
"cancel_requested": false,
"cancel_requested": false,
- "idempotency_key": null,
- "created_by": "svc:live",
- "owner_user_id": null,
- "decided_by": null,
+ "has_result": false,
+ "run_after": null,
"decided_at": null,
"decided_at": null,
- "created_at": "2026-09-23 18:02:11",
- "updated_at": "2026-09-23 18:02:12",
- "started_at": "2026-09-23 18:02:12",
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:02:12.000Z",
+ "started_at": "2026-09-23T18:02:12.000Z",
"finished_at": null
"finished_at": null
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
+{
+ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
+ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "type": "object.split",
+ "status": "running",
+ "attempts": 2,
+ "max_attempts": 3,
+ "error_code": "media_unavailable",
+ "cancel_requested": true,
+ "has_result": false,
+ "run_after": null,
+ "decided_at": null,
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:06:12.000Z",
+ "started_at": "2026-09-23T18:02:12.000Z",
+ "finished_at": null
+}
@@ -1,26 +1,18 @@
@@ -1,26 +1,18 @@
{
{
- "app_id": "live",
"id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
"id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
"object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"type": "object.split",
"type": "object.split",
"status": "running",
"status": "running",
- "params": {
- "parts": 3
- },
- "result": null,
- "error": null,
- "error_code": null,
"attempts": 1,
"attempts": 1,
"max_attempts": 3,
"max_attempts": 3,
- "run_after": null,
+ "error_code": null,
"cancel_requested": false,
"cancel_requested": false,
- "idempotency_key": null,
- "created_by": "svc:live",
- "owner_user_id": null,
- "decided_by": null,
+ "has_result": false,
+ "run_after": null,
"decided_at": null,
"decided_at": null,
- "created_at": "2026-09-23 18:02:11",
- "updated_at": "2026-09-23 18:02:12",
- "started_at": "2026-09-23 18:02:12",
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:02:12.000Z",
+ "started_at": "2026-09-23T18:02:12.000Z",
"finished_at": null
"finished_at": null
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
+{
+ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
+ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "type": "object.split",
+ "status": "running",
+ "attempts": 2,
+ "max_attempts": 3,
+ "error_code": "interrupted",
+ "cancel_requested": false,
+ "has_result": false,
+ "run_after": null,
+ "decided_at": null,
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:06:12.000Z",
+ "started_at": "2026-09-23T18:02:12.000Z",
+ "finished_at": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
+{
+ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
+ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "type": "thumbnail.regenerate",
+ "status": "succeeded",
+ "attempts": 1,
+ "max_attempts": 3,
+ "error_code": null,
+ "cancel_requested": false,
+ "has_result": true,
+ "run_after": null,
+ "decided_at": null,
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:02:14.000Z",
+ "started_at": "2026-09-23T18:02:12.000Z",
+ "finished_at": "2026-09-23T18:02:14.000Z",
+ "result": {
+ "url": "/t/vod-4211-1.jpg",
+ "kind": "vod",
+ "id": 4211
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
+{
+ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
+ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "type": "object.split",
+ "status": "succeeded",
+ "attempts": 1,
+ "max_attempts": 3,
+ "error_code": "interrupted",
+ "cancel_requested": false,
+ "has_result": true,
+ "run_after": null,
+ "decided_at": null,
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:31:02.000Z",
+ "started_at": "2026-09-23T18:02:12.000Z",
+ "finished_at": "2026-09-23T18:31:02.000Z"
+}
deleted file mode 100644
deleted file mode 100644
@@ -1,38 +0,0 @@
@@ -1,38 +0,0 @@
-{
- "app_id": "live",
- "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
- "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
- "type": "object.split",
- "status": "succeeded",
- "params": {
- "parts": 3
- },
- "result": {
- "source_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
- "parts": [
- {
- "part": 1,
- "object_id": "med_01JAB3C4D5E6F7G8H9J0K1MNPR",
- "start_seconds": 0,
- "duration_seconds": 3600,
- "size_bytes": 1932735283
- }
- ],
- "method": "split"
- },
- "error": "transient",
- "error_code": "interrupted",
- "attempts": 1,
- "max_attempts": 3,
- "run_after": null,
- "cancel_requested": false,
- "idempotency_key": null,
- "created_by": "svc:live",
- "owner_user_id": null,
- "decided_by": null,
- "decided_at": null,
- "created_at": "2026-09-23 18:02:11",
- "updated_at": "2026-09-23 18:31:02",
- "started_at": "2026-09-23 18:02:12",
- "finished_at": "2026-09-23 18:31:02"
-}
new file mode 100644
new file mode 100644
@@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
+{
+ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
+ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "type": "object.split",
+ "status": "succeeded",
+ "attempts": 1,
+ "max_attempts": 3,
+ "error_code": null,
+ "cancel_requested": false,
+ "run_after": null,
+ "decided_at": null,
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:31:02.000Z",
+ "started_at": "2026-09-23T18:02:12.000Z",
+ "finished_at": "2026-09-23T18:31:02.000Z"
+}
deleted file mode 100644
deleted file mode 100644
@@ -1,29 +0,0 @@
@@ -1,29 +0,0 @@
-{
- "app_id": "live",
- "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
- "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
- "type": "object.split",
- "status": "succeeded",
- "params": {
- "parts": 3
- },
- "result": {
- "omitted": true,
- "reason": "larger than the event limit; GET the job"
- },
- "error": null,
- "error_code": null,
- "attempts": 1,
- "max_attempts": 3,
- "run_after": null,
- "cancel_requested": false,
- "idempotency_key": null,
- "created_by": "svc:live",
- "owner_user_id": null,
- "decided_by": null,
- "decided_at": null,
- "created_at": "2026-09-23 18:02:11",
- "updated_at": "2026-09-23 18:31:02",
- "started_at": "2026-09-23 18:02:12",
- "finished_at": "2026-09-23 18:31:02"
-}
@@ -1,40 +1,18 @@
@@ -1,40 +1,18 @@
{
{
- "app_id": "live",
"id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
"id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
"object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"type": "object.split",
"type": "object.split",
"status": "succeeded",
"status": "succeeded",
- "params": {
- "parts": 3
- },
- "result": {
- "source_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
- "method": "split",
- "parts": [
- {
- "part": 1,
- "object_id": "med_01JAB3C4D5E6F7G8H9J0K1MNPR",
- "start_seconds": 0,
- "duration_seconds": 3600,
- "size_bytes": 1932735283
- }
- ],
- "oversized_parts": [],
- "source_unchanged": true
- },
- "error": null,
- "error_code": null,
"attempts": 1,
"attempts": 1,
"max_attempts": 3,
"max_attempts": 3,
- "run_after": null,
+ "error_code": null,
"cancel_requested": false,
"cancel_requested": false,
- "idempotency_key": null,
- "created_by": "svc:live",
- "owner_user_id": null,
- "decided_by": null,
+ "has_result": true,
+ "run_after": null,
"decided_at": null,
"decided_at": null,
- "created_at": "2026-09-23 18:02:11",
- "updated_at": "2026-09-23 18:31:02",
- "started_at": "2026-09-23 18:02:12",
- "finished_at": "2026-09-23 18:31:02"
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:31:02.000Z",
+ "started_at": "2026-09-23T18:02:12.000Z",
+ "finished_at": "2026-09-23T18:31:02.000Z"
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
+{
+ "id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
+ "app_id": "live",
+ "object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "type": "thumbnail.regenerate",
+ "status": "succeeded",
+ "attempts": 2,
+ "max_attempts": 2,
+ "error_code": null,
+ "cancel_requested": false,
+ "has_result": true,
+ "run_after": null,
+ "decided_at": null,
+ "created_at": "2026-09-23T18:02:11.000Z",
+ "updated_at": "2026-09-23T18:03:02.000Z",
+ "started_at": "2026-09-23T18:02:12.000Z",
+ "finished_at": "2026-09-23T18:03:02.000Z"
+}
@@ -8588,7 +8588,7 @@
@@ -8588,7 +8588,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/events/payloads/media.job.proposed.v1.json",
"$id": "https://openvibe.network/contracts/events/payloads/media.job.proposed.v1.json",
"title": "MediaJobProposedPayload",
"title": "MediaJobProposedPayload",
- "description": "media.job.proposed v1 (OpenVibe.Media server/jobs/queue.js enqueue; server/events.js recordJob). A job was proposed and waits for its owner: approving it queues it (media.job.queued), cancelling it ends it (media.job.cancelled); the worker never runs a proposal. The size-invariant validator (invariant.scan, created_by system:invariant.scan) proposes object.split and object.remux this way. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",
+ "description": "media.job.proposed v1 (OpenVibe.Media server/jobs/queue.js enqueue; server/events.js recordJob). A job was proposed and waits for its owner: approving it queues it (media.job.queued), cancelling it ends it (media.job.cancelled); the worker never runs a proposal. The size-invariant validator (invariant.scan) proposes object.split and object.remux this way. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",
"type": "object",
"type": "object",
"required": [
"required": [
"id",
"id",
@@ -8596,18 +8596,12 @@
@@ -8596,18 +8596,12 @@
"object_id",
"object_id",
"type",
"type",
"status",
"status",
- "params",
- "result",
- "error",
- "error_code",
"attempts",
"attempts",
"max_attempts",
"max_attempts",
- "run_after",
+ "error_code",
"cancel_requested",
"cancel_requested",
- "idempotency_key",
- "created_by",
- "owner_user_id",
- "decided_by",
+ "has_result",
+ "run_after",
"decided_at",
"decided_at",
"created_at",
"created_at",
"updated_at",
"updated_at",
@@ -8618,7 +8612,8 @@
@@ -8618,7 +8612,8 @@
"properties": {
"properties": {
"id": {
"id": {
"type": "string",
"type": "string",
- "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$"
+ "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."
},
},
"app_id": {
"app_id": {
"type": "string",
"type": "string",
@@ -8643,97 +8638,49 @@
@@ -8643,97 +8638,49 @@
"status": {
"status": {
"const": "proposed"
"const": "proposed"
},
},
- "params": {
- "type": "object",
- "description": "The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent."
- },
- "result": {
- "type": [
- "object",
- "null"
- ],
- "description": "null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB."
- },
- "error": {
- "type": [
- "string",
- "null"
- ],
- "maxLength": 1000,
- "description": "The last failure's message (free text), or null."
- },
- "error_code": {
- "type": [
- "string",
- "null"
- ],
- "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null."
- },
"attempts": {
"attempts": {
"type": "integer",
"type": "integer",
- "minimum": 0
+ "minimum": 0,
+ "description": "Attempts started so far."
},
},
"max_attempts": {
"max_attempts": {
"type": "integer",
"type": "integer",
"minimum": 1,
"minimum": 1,
"maximum": 10
"maximum": 10
},
},
- "run_after": {
- "type": [
- "string",
- "null"
- ],
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$",
- "description": "A queued job waits until then (retry backoff); null otherwise."
+ "error_code": {
+ "type": "null"
},
},
"cancel_requested": {
"cancel_requested": {
"type": "boolean",
"type": "boolean",
"description": "Its owner asked a running job to stop."
"description": "Its owner asked a running job to stop."
},
},
- "idempotency_key": {
- "type": [
- "string",
- "null"
- ],
- "maxLength": 200,
- "description": "The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null."
- },
- "created_by": {
- "type": [
- "string",
- "null"
- ],
- "description": "svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>."
+ "has_result": {
+ "const": false,
+ "description": "A proposal has no result."
},
},
- "owner_user_id": {
- "type": [
- "integer",
- "null"
- ],
- "minimum": 1,
- "description": "The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject."
- },
- "decided_by": {
+ "run_after": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "description": "Who approved or cancelled a proposal or queued job (same forms as created_by)."
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
+ "description": "A queued job waits until then (retry backoff); null otherwise."
},
},
"decided_at": {
"decided_at": {
- "type": [
- "string",
- "null"
- ],
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"
+ "type": "null",
+ "description": "Nobody has decided yet."
},
},
"created_at": {
"created_at": {
"type": "string",
"type": "string",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
},
},
"updated_at": {
"updated_at": {
"type": "string",
"type": "string",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
},
},
"started_at": {
"started_at": {
"type": "null"
"type": "null"
@@ -8752,7 +8699,7 @@
@@ -8752,7 +8699,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/events/payloads/media.job.queued.v1.json",
"$id": "https://openvibe.network/contracts/events/payloads/media.job.queued.v1.json",
"title": "MediaJobQueuedPayload",
"title": "MediaJobQueuedPayload",
- "description": "media.job.queued v1 (OpenVibe.Media server/jobs/queue.js enqueue, approve; server/events.js recordJob). A job was accepted and queued: a new job (POST /api/v2/:app/jobs), or a proposal its owner approved (decided_by, decided_at). An Idempotency-Key replay, or a request joined to an identical job that is still active, creates no job and no event. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",
+ "description": "media.job.queued v1 (OpenVibe.Media server/jobs/queue.js enqueue, approve; server/events.js recordJob). A job was accepted and queued: a new job (POST /api/v2/:app/jobs), or a proposal its owner approved (decided_at). An Idempotency-Key replay, or a request joined to an identical job that is still active, creates no job and no event. A failed attempt that is queued again is media.job.retrying, not this. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",
"type": "object",
"type": "object",
"required": [
"required": [
"id",
"id",
@@ -8760,18 +8707,12 @@
@@ -8760,18 +8707,12 @@
"object_id",
"object_id",
"type",
"type",
"status",
"status",
- "params",
- "result",
- "error",
- "error_code",
"attempts",
"attempts",
"max_attempts",
"max_attempts",
- "run_after",
+ "error_code",
"cancel_requested",
"cancel_requested",
- "idempotency_key",
- "created_by",
- "owner_user_id",
- "decided_by",
+ "has_result",
+ "run_after",
"decided_at",
"decided_at",
"created_at",
"created_at",
"updated_at",
"updated_at",
@@ -8782,7 +8723,8 @@
@@ -8782,7 +8723,8 @@
"properties": {
"properties": {
"id": {
"id": {
"type": "string",
"type": "string",
- "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$"
+ "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."
},
},
"app_id": {
"app_id": {
"type": "string",
"type": "string",
@@ -8807,97 +8749,54 @@
@@ -8807,97 +8749,54 @@
"status": {
"status": {
"const": "queued"
"const": "queued"
},
},
- "params": {
- "type": "object",
- "description": "The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent."
- },
- "result": {
- "type": [
- "object",
- "null"
- ],
- "description": "null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB."
- },
- "error": {
- "type": [
- "string",
- "null"
- ],
- "maxLength": 1000,
- "description": "The last failure's message (free text), or null."
- },
- "error_code": {
- "type": [
- "string",
- "null"
- ],
- "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null."
- },
"attempts": {
"attempts": {
"type": "integer",
"type": "integer",
- "minimum": 0
+ "minimum": 0,
+ "description": "Attempts started so far."
},
},
"max_attempts": {
"max_attempts": {
"type": "integer",
"type": "integer",
"minimum": 1,
"minimum": 1,
"maximum": 10
"maximum": 10
},
},
- "run_after": {
- "type": [
- "string",
- "null"
- ],
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$",
- "description": "A queued job waits until then (retry backoff); null otherwise."
+ "error_code": {
+ "type": "null"
},
},
"cancel_requested": {
"cancel_requested": {
"type": "boolean",
"type": "boolean",
"description": "Its owner asked a running job to stop."
"description": "Its owner asked a running job to stop."
},
},
- "idempotency_key": {
- "type": [
- "string",
- "null"
- ],
- "maxLength": 200,
- "description": "The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null."
+ "has_result": {
+ "const": false,
+ "description": "A job that never ran has no result."
},
},
- "created_by": {
- "type": [
- "string",
- "null"
- ],
- "description": "svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>."
- },
- "owner_user_id": {
- "type": [
- "integer",
- "null"
- ],
- "minimum": 1,
- "description": "The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject."
- },
- "decided_by": {
+ "run_after": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "description": "Who approved or cancelled a proposal or queued job (same forms as created_by)."
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
+ "description": "A queued job waits until then (retry backoff); null otherwise."
},
},
"decided_at": {
"decided_at": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
+ "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise."
},
},
"created_at": {
"created_at": {
"type": "string",
"type": "string",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
},
},
"updated_at": {
"updated_at": {
"type": "string",
"type": "string",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
},
},
"started_at": {
"started_at": {
"type": "null"
"type": "null"
@@ -8916,7 +8815,7 @@
@@ -8916,7 +8815,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/events/payloads/media.job.started.v1.json",
"$id": "https://openvibe.network/contracts/events/payloads/media.job.started.v1.json",
"title": "MediaJobStartedPayload",
"title": "MediaJobStartedPayload",
- "description": "media.job.started v1 (OpenVibe.Media server/jobs/queue.js claim; server/events.js recordJob). The worker claimed the job and it is running: attempts counts this attempt, started_at stays the first attempt's start. A job that is retried starts again and is announced again. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",
+ "description": "media.job.started v1 (OpenVibe.Media server/jobs/queue.js claim; server/events.js recordJob). The worker claimed the job and it is running (status running): attempts counts this attempt, started_at stays the first attempt's start. A job that is retried starts again and is announced again; error_code is still the previous attempt's until the job finishes. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",
"type": "object",
"type": "object",
"required": [
"required": [
"id",
"id",
@@ -8924,18 +8823,12 @@
@@ -8924,18 +8823,12 @@
"object_id",
"object_id",
"type",
"type",
"status",
"status",
- "params",
- "result",
- "error",
- "error_code",
"attempts",
"attempts",
"max_attempts",
"max_attempts",
- "run_after",
+ "error_code",
"cancel_requested",
"cancel_requested",
- "idempotency_key",
- "created_by",
- "owner_user_id",
- "decided_by",
+ "has_result",
+ "run_after",
"decided_at",
"decided_at",
"created_at",
"created_at",
"updated_at",
"updated_at",
@@ -8946,7 +8839,8 @@
@@ -8946,7 +8839,8 @@
"properties": {
"properties": {
"id": {
"id": {
"type": "string",
"type": "string",
- "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$"
+ "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."
},
},
"app_id": {
"app_id": {
"type": "string",
"type": "string",
@@ -8969,103 +8863,62 @@
@@ -8969,103 +8863,62 @@
"description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."
"description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."
},
},
"status": {
"status": {
- "const": "running"
- },
- "params": {
- "type": "object",
- "description": "The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent."
- },
- "result": {
- "type": [
- "object",
- "null"
- ],
- "description": "null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB."
- },
- "error": {
- "type": [
- "string",
- "null"
- ],
- "maxLength": 1000,
- "description": "The last failure's message (free text), or null."
- },
- "error_code": {
- "type": [
- "string",
- "null"
- ],
- "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null."
+ "const": "running",
+ "description": "started carries the job's state, running."
},
},
"attempts": {
"attempts": {
"type": "integer",
"type": "integer",
- "minimum": 0
+ "minimum": 1,
+ "description": "Attempts started so far, this one included."
},
},
"max_attempts": {
"max_attempts": {
"type": "integer",
"type": "integer",
"minimum": 1,
"minimum": 1,
"maximum": 10
"maximum": 10
},
},
- "run_after": {
+ "error_code": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$",
- "description": "A queued job waits until then (retry backoff); null otherwise."
+ "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it."
},
},
"cancel_requested": {
"cancel_requested": {
"type": "boolean",
"type": "boolean",
"description": "Its owner asked a running job to stop."
"description": "Its owner asked a running job to stop."
},
},
- "idempotency_key": {
- "type": [
- "string",
- "null"
- ],
- "maxLength": 200,
- "description": "The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null."
- },
- "created_by": {
- "type": [
- "string",
- "null"
- ],
- "description": "svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>."
- },
- "owner_user_id": {
- "type": [
- "integer",
- "null"
- ],
- "minimum": 1,
- "description": "The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject."
+ "has_result": {
+ "const": false,
+ "description": "A running job has no result yet."
},
},
- "decided_by": {
- "type": [
- "string",
- "null"
- ],
- "description": "Who approved or cancelled a proposal or queued job (same forms as created_by)."
+ "run_after": {
+ "type": "null",
+ "description": "Cleared when the job is claimed."
},
},
"decided_at": {
"decided_at": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
+ "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise."
},
},
"created_at": {
"created_at": {
"type": "string",
"type": "string",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
},
},
"updated_at": {
"updated_at": {
"type": "string",
"type": "string",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
},
},
"started_at": {
"started_at": {
"type": "string",
"type": "string",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
+ "description": "The first attempt's start."
},
},
"finished_at": {
"finished_at": {
"type": "null"
"type": "null"
@@ -9081,7 +8934,7 @@
@@ -9081,7 +8934,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/events/payloads/media.job.retrying.v1.json",
"$id": "https://openvibe.network/contracts/events/payloads/media.job.retrying.v1.json",
"title": "MediaJobRetryingPayload",
"title": "MediaJobRetryingPayload",
- "description": "media.job.retrying v1 (OpenVibe.Media server/jobs/queue.js fail with a retry; server/events.js recordJob). An attempt failed and the job is queued again after a backoff (run_after); error and error_code say why. A running job nobody holds any more (the service restarted, or the worker stopped renewing its lease) is retried this way with error_code interrupted while it has attempts left. status is queued. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",
+ "description": "media.job.retrying v1 (OpenVibe.Media server/jobs/queue.js fail with a retry; server/events.js recordJob). An attempt failed and the job is queued again (status queued) after a backoff (run_after); error_code says why when the handler gave a code. A running job nobody holds any more (the service restarted, or the worker stopped renewing its lease) is retried this way with error_code interrupted while it has attempts left. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",
"type": "object",
"type": "object",
"required": [
"required": [
"id",
"id",
@@ -9089,18 +8942,12 @@
@@ -9089,18 +8942,12 @@
"object_id",
"object_id",
"type",
"type",
"status",
"status",
- "params",
- "result",
- "error",
- "error_code",
"attempts",
"attempts",
"max_attempts",
"max_attempts",
- "run_after",
+ "error_code",
"cancel_requested",
"cancel_requested",
- "idempotency_key",
- "created_by",
- "owner_user_id",
- "decided_by",
+ "has_result",
+ "run_after",
"decided_at",
"decided_at",
"created_at",
"created_at",
"updated_at",
"updated_at",
@@ -9111,7 +8958,8 @@
@@ -9111,7 +8958,8 @@
"properties": {
"properties": {
"id": {
"id": {
"type": "string",
"type": "string",
- "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$"
+ "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."
},
},
"app_id": {
"app_id": {
"type": "string",
"type": "string",
@@ -9134,97 +8982,64 @@
@@ -9134,97 +8982,64 @@
"description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."
"description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."
},
},
"status": {
"status": {
- "const": "queued"
- },
- "params": {
- "type": "object",
- "description": "The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent."
- },
- "result": {
- "type": [
- "object",
- "null"
- ],
- "description": "null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB."
- },
- "error": {
- "type": "string",
- "maxLength": 1000,
- "description": "The failure's message, free text from the handler."
- },
- "error_code": {
- "type": [
- "string",
- "null"
- ],
- "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null."
+ "const": "queued",
+ "description": "retrying carries the job's state, queued."
},
},
"attempts": {
"attempts": {
"type": "integer",
"type": "integer",
- "minimum": 0
+ "minimum": 1,
+ "description": "Attempts started so far, the failed one included."
},
},
"max_attempts": {
"max_attempts": {
"type": "integer",
"type": "integer",
"minimum": 1,
"minimum": 1,
"maximum": 10
"maximum": 10
},
},
- "run_after": {
- "type": "string",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$",
- "description": "Not before this time (the retry backoff)."
- },
- "cancel_requested": {
- "type": "boolean",
- "description": "Its owner asked a running job to stop."
- },
- "idempotency_key": {
+ "error_code": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "maxLength": 200,
- "description": "The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null."
+ "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it."
},
},
- "created_by": {
- "type": [
- "string",
- "null"
- ],
- "description": "svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>."
+ "cancel_requested": {
+ "type": "boolean",
+ "description": "Its owner asked a running job to stop."
},
},
- "owner_user_id": {
- "type": [
- "integer",
- "null"
- ],
- "minimum": 1,
- "description": "The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject."
+ "has_result": {
+ "const": false,
+ "description": "A job waiting for its retry has no result."
},
},
- "decided_by": {
- "type": [
- "string",
- "null"
- ],
- "description": "Who approved or cancelled a proposal or queued job (same forms as created_by)."
+ "run_after": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
+ "description": "Not before this time (the retry backoff)."
},
},
"decided_at": {
"decided_at": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
+ "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise."
},
},
"created_at": {
"created_at": {
"type": "string",
"type": "string",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
},
},
"updated_at": {
"updated_at": {
"type": "string",
"type": "string",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
},
},
"started_at": {
"started_at": {
"type": "string",
"type": "string",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
+ "description": "The first attempt's start."
},
},
"finished_at": {
"finished_at": {
"type": "null"
"type": "null"
@@ -9240,7 +9055,7 @@
@@ -9240,7 +9055,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/events/payloads/media.job.succeeded.v1.json",
"$id": "https://openvibe.network/contracts/events/payloads/media.job.succeeded.v1.json",
"title": "MediaJobSucceededPayload",
"title": "MediaJobSucceededPayload",
- "description": "media.job.succeeded v1 (OpenVibe.Media server/jobs/queue.js succeed; server/events.js recordJob). The job finished: result is its type's result (thumbnail.regenerate { url, kind, id }, object.split { source_id, parts, … }, object.remux { source_id, object_id, … }, invariant.scan { thresholds, counts, … }). Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",
+ "description": "media.job.succeeded v1 (OpenVibe.Media server/jobs/queue.js succeed; server/events.js recordJob). The job finished. Its result (thumbnail.regenerate { url, kind, id }, object.split { source_id, parts, … }, object.remux { source_id, object_id, … }, invariant.scan { thresholds, counts, … }) is not in the event: has_result says whether there is one to GET. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",
"type": "object",
"type": "object",
"required": [
"required": [
"id",
"id",
@@ -9248,18 +9063,12 @@
@@ -9248,18 +9063,12 @@
"object_id",
"object_id",
"type",
"type",
"status",
"status",
- "params",
- "result",
- "error",
- "error_code",
"attempts",
"attempts",
"max_attempts",
"max_attempts",
- "run_after",
+ "error_code",
"cancel_requested",
"cancel_requested",
- "idempotency_key",
- "created_by",
- "owner_user_id",
- "decided_by",
+ "has_result",
+ "run_after",
"decided_at",
"decided_at",
"created_at",
"created_at",
"updated_at",
"updated_at",
@@ -9270,7 +9079,8 @@
@@ -9270,7 +9079,8 @@
"properties": {
"properties": {
"id": {
"id": {
"type": "string",
"type": "string",
- "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$"
+ "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."
},
},
"app_id": {
"app_id": {
"type": "string",
"type": "string",
@@ -9295,96 +9105,60 @@
@@ -9295,96 +9105,60 @@
"status": {
"status": {
"const": "succeeded"
"const": "succeeded"
},
},
- "params": {
- "type": "object",
- "description": "The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent."
- },
- "result": {
- "type": [
- "object",
- "null"
- ],
- "description": "The job type's result, or { omitted: true, reason } when it is larger than 8 KB (GET the job for it)."
- },
- "error": {
- "type": "null"
- },
- "error_code": {
- "type": "null"
- },
"attempts": {
"attempts": {
"type": "integer",
"type": "integer",
- "minimum": 0
+ "minimum": 1,
+ "description": "Attempts started, the successful one included."
},
},
"max_attempts": {
"max_attempts": {
"type": "integer",
"type": "integer",
"minimum": 1,
"minimum": 1,
"maximum": 10
"maximum": 10
},
},
- "run_after": {
- "type": [
- "string",
- "null"
- ],
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$",
- "description": "A queued job waits until then (retry backoff); null otherwise."
+ "error_code": {
+ "type": "null",
+ "description": "A success clears the last failure."
},
},
"cancel_requested": {
"cancel_requested": {
"type": "boolean",
"type": "boolean",
"description": "Its owner asked a running job to stop."
"description": "Its owner asked a running job to stop."
},
},
- "idempotency_key": {
- "type": [
- "string",
- "null"
- ],
- "maxLength": 200,
- "description": "The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null."
- },
- "created_by": {
- "type": [
- "string",
- "null"
- ],
- "description": "svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>."
- },
- "owner_user_id": {
- "type": [
- "integer",
- "null"
- ],
- "minimum": 1,
- "description": "The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject."
+ "has_result": {
+ "type": "boolean",
+ "description": "The job holds a result. The result itself is never in the event: GET the job with a tenant token for it."
},
},
- "decided_by": {
- "type": [
- "string",
- "null"
- ],
- "description": "Who approved or cancelled a proposal or queued job (same forms as created_by)."
+ "run_after": {
+ "type": "null"
},
},
"decided_at": {
"decided_at": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
+ "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise."
},
},
"created_at": {
"created_at": {
"type": "string",
"type": "string",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
},
},
"updated_at": {
"updated_at": {
"type": "string",
"type": "string",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
},
},
"started_at": {
"started_at": {
"type": "string",
"type": "string",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
+ "description": "The first attempt's start."
},
},
"finished_at": {
"finished_at": {
"type": "string",
"type": "string",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
}
}
}
}
}
}
@@ -9397,7 +9171,7 @@
@@ -9397,7 +9171,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/events/payloads/media.job.failed.v1.json",
"$id": "https://openvibe.network/contracts/events/payloads/media.job.failed.v1.json",
"title": "MediaJobFailedPayload",
"title": "MediaJobFailedPayload",
- "description": "media.job.failed v1 (OpenVibe.Media server/jobs/queue.js fail; server/events.js recordJob). The job failed for good: a permanent error, or its last attempt (attempts = max_attempts) failed; error and error_code say why. A cancelled job is not a failure and is announced as media.job.cancelled. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",
+ "description": "media.job.failed v1 (OpenVibe.Media server/jobs/queue.js fail; server/events.js recordJob). The job failed for good: a permanent error, or its last attempt (attempts = max_attempts) failed; error_code says why when the handler gave a code (GET the job for the message). A cancelled job is not a failure and is announced as media.job.cancelled. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",
"type": "object",
"type": "object",
"required": [
"required": [
"id",
"id",
@@ -9405,18 +9179,12 @@
@@ -9405,18 +9179,12 @@
"object_id",
"object_id",
"type",
"type",
"status",
"status",
- "params",
- "result",
- "error",
- "error_code",
"attempts",
"attempts",
"max_attempts",
"max_attempts",
- "run_after",
+ "error_code",
"cancel_requested",
"cancel_requested",
- "idempotency_key",
- "created_by",
- "owner_user_id",
- "decided_by",
+ "has_result",
+ "run_after",
"decided_at",
"decided_at",
"created_at",
"created_at",
"updated_at",
"updated_at",
@@ -9427,7 +9195,8 @@
@@ -9427,7 +9195,8 @@
"properties": {
"properties": {
"id": {
"id": {
"type": "string",
"type": "string",
- "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$"
+ "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."
},
},
"app_id": {
"app_id": {
"type": "string",
"type": "string",
@@ -9452,102 +9221,63 @@
@@ -9452,102 +9221,63 @@
"status": {
"status": {
"const": "failed"
"const": "failed"
},
},
- "params": {
- "type": "object",
- "description": "The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent."
- },
- "result": {
- "type": [
- "object",
- "null"
- ],
- "description": "null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB."
- },
- "error": {
- "type": "string",
- "maxLength": 1000,
- "description": "The failure's message, free text from the handler."
- },
- "error_code": {
- "type": [
- "string",
- "null"
- ],
- "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null."
- },
"attempts": {
"attempts": {
"type": "integer",
"type": "integer",
- "minimum": 0
+ "minimum": 1,
+ "description": "Attempts started, the last failed one included."
},
},
"max_attempts": {
"max_attempts": {
"type": "integer",
"type": "integer",
"minimum": 1,
"minimum": 1,
"maximum": 10
"maximum": 10
},
},
- "run_after": {
+ "error_code": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$",
- "description": "A queued job waits until then (retry backoff); null otherwise."
+ "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it."
},
},
"cancel_requested": {
"cancel_requested": {
"type": "boolean",
"type": "boolean",
"description": "Its owner asked a running job to stop."
"description": "Its owner asked a running job to stop."
},
},
- "idempotency_key": {
- "type": [
- "string",
- "null"
- ],
- "maxLength": 200,
- "description": "The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null."
- },
- "created_by": {
- "type": [
- "string",
- "null"
- ],
- "description": "svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>."
- },
- "owner_user_id": {
- "type": [
- "integer",
- "null"
- ],
- "minimum": 1,
- "description": "The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject."
+ "has_result": {
+ "type": "boolean",
+ "description": "The handler left a partial result. The result itself is never in the event: GET the job with a tenant token for it."
},
},
- "decided_by": {
- "type": [
- "string",
- "null"
- ],
- "description": "Who approved or cancelled a proposal or queued job (same forms as created_by)."
+ "run_after": {
+ "type": "null"
},
},
"decided_at": {
"decided_at": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
+ "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise."
},
},
"created_at": {
"created_at": {
"type": "string",
"type": "string",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
},
},
"updated_at": {
"updated_at": {
"type": "string",
"type": "string",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
},
},
"started_at": {
"started_at": {
"type": "string",
"type": "string",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
+ "description": "The first attempt's start."
},
},
"finished_at": {
"finished_at": {
"type": "string",
"type": "string",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
}
}
}
}
}
}
@@ -9560,7 +9290,7 @@
@@ -9560,7 +9290,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/events/payloads/media.job.cancelled.v1.json",
"$id": "https://openvibe.network/contracts/events/payloads/media.job.cancelled.v1.json",
"title": "MediaJobCancelledPayload",
"title": "MediaJobCancelledPayload",
- "description": "media.job.cancelled v1 (OpenVibe.Media server/jobs/queue.js cancel, markCancelled; server/events.js recordJob). The job was cancelled: a proposal or a queued job at once, by its owner (POST /api/v2/:app/jobs/:id/cancel or DELETE /api/v2/:app/jobs/:id) or by the validator withdrawing a proposal that no longer applies (decided_by system:invariant.scan); a running job once it stopped after its owner asked (cancel_requested true, error_code cancelled). Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",
+ "description": "media.job.cancelled v1 (OpenVibe.Media server/jobs/queue.js cancel, markCancelled; server/events.js recordJob). The job was cancelled: a proposal or a queued job at once, by its owner (POST /api/v2/:app/jobs/:id/cancel or DELETE /api/v2/:app/jobs/:id) or by the validator withdrawing a proposal that no longer applies (decided_at set, error_code cancelled when a reason was given); a running job once it stopped after its owner asked (cancel_requested true, error_code cancelled). Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",
"type": "object",
"type": "object",
"required": [
"required": [
"id",
"id",
@@ -9568,18 +9298,12 @@
@@ -9568,18 +9298,12 @@
"object_id",
"object_id",
"type",
"type",
"status",
"status",
- "params",
- "result",
- "error",
- "error_code",
"attempts",
"attempts",
"max_attempts",
"max_attempts",
- "run_after",
+ "error_code",
"cancel_requested",
"cancel_requested",
- "idempotency_key",
- "created_by",
- "owner_user_id",
- "decided_by",
+ "has_result",
+ "run_after",
"decided_at",
"decided_at",
"created_at",
"created_at",
"updated_at",
"updated_at",
@@ -9590,7 +9314,8 @@
@@ -9590,7 +9314,8 @@
"properties": {
"properties": {
"id": {
"id": {
"type": "string",
"type": "string",
- "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$"
+ "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."
},
},
"app_id": {
"app_id": {
"type": "string",
"type": "string",
@@ -9615,109 +9340,72 @@
@@ -9615,109 +9340,72 @@
"status": {
"status": {
"const": "cancelled"
"const": "cancelled"
},
},
- "params": {
- "type": "object",
- "description": "The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent."
- },
- "result": {
- "type": [
- "object",
- "null"
- ],
- "description": "null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB."
- },
- "error": {
- "type": [
- "string",
- "null"
- ],
- "maxLength": 1000,
- "description": "The last failure's message (free text), or null."
- },
- "error_code": {
- "type": [
- "string",
- "null"
- ],
- "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null."
- },
"attempts": {
"attempts": {
"type": "integer",
"type": "integer",
- "minimum": 0
+ "minimum": 0,
+ "description": "Attempts started so far."
},
},
"max_attempts": {
"max_attempts": {
"type": "integer",
"type": "integer",
"minimum": 1,
"minimum": 1,
"maximum": 10
"maximum": 10
},
},
- "run_after": {
+ "error_code": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$",
- "description": "A queued job waits until then (retry backoff); null otherwise."
+ "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it."
},
},
"cancel_requested": {
"cancel_requested": {
"type": "boolean",
"type": "boolean",
"description": "Its owner asked a running job to stop."
"description": "Its owner asked a running job to stop."
},
},
- "idempotency_key": {
- "type": [
- "string",
- "null"
- ],
- "maxLength": 200,
- "description": "The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null."
- },
- "created_by": {
- "type": [
- "string",
- "null"
- ],
- "description": "svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>."
- },
- "owner_user_id": {
- "type": [
- "integer",
- "null"
- ],
- "minimum": 1,
- "description": "The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject."
+ "has_result": {
+ "type": "boolean",
+ "description": "A stopped running job left a partial result. The result itself is never in the event: GET the job with a tenant token for it."
},
},
- "decided_by": {
+ "run_after": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "description": "Who approved or cancelled a proposal or queued job (same forms as created_by)."
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
+ "description": "A queued job waits until then (retry backoff); null otherwise."
},
},
"decided_at": {
"decided_at": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
+ "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise."
},
},
"created_at": {
"created_at": {
"type": "string",
"type": "string",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
},
},
"updated_at": {
"updated_at": {
"type": "string",
"type": "string",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
},
},
"started_at": {
"started_at": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$",
- "description": "null when the job never ran."
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
+ "description": "The first attempt's start; null when the job was cancelled before it ever ran."
},
},
"finished_at": {
"finished_at": {
"type": "string",
"type": "string",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
}
}
}
}
}
}
@@ -7054,9 +7054,12 @@ export interface LiveReleaseDeployedPayload {
@@ -7054,9 +7054,12 @@ export interface LiveReleaseDeployedPayload {
/** [email protected] (owner: media) */
/** [email protected] (owner: media) */
/**
/**
- * media.job.proposed v1 (OpenVibe.Media server/jobs/queue.js enqueue; server/events.js recordJob). A job was proposed and waits for its owner: approving it queues it (media.job.queued), cancelling it ends it (media.job.cancelled); the worker never runs a proposal. The size-invariant validator (invariant.scan, created_by system:invariant.scan) proposes object.split and object.remux this way. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.
+ * media.job.proposed v1 (OpenVibe.Media server/jobs/queue.js enqueue; server/events.js recordJob). A job was proposed and waits for its owner: approving it queues it (media.job.queued), cancelling it ends it (media.job.cancelled); the worker never runs a proposal. The size-invariant validator (invariant.scan) proposes object.split and object.remux this way. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.
*/
*/
export interface MediaJobProposedPayload {
export interface MediaJobProposedPayload {
+ /**
+ * The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it.
+ */
id: string;
id: string;
/**
/**
* The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…).
* The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…).
@@ -7072,48 +7075,27 @@ export interface MediaJobProposedPayload {
@@ -7072,48 +7075,27 @@ export interface MediaJobProposedPayload {
type: string;
type: string;
status: "proposed";
status: "proposed";
/**
/**
- * The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent.
- */
- params: {};
- /**
- * null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB.
- */
- result: {} | null;
- /**
- * The last failure's message (free text), or null.
- */
- error: string | null;
- /**
- * Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null.
+ * Attempts started so far.
*/
*/
- error_code: string | null;
attempts: number;
attempts: number;
max_attempts: number;
max_attempts: number;
- /**
- * A queued job waits until then (retry backoff); null otherwise.
- */
- run_after: string | null;
+ error_code: null;
/**
/**
* Its owner asked a running job to stop.
* Its owner asked a running job to stop.
*/
*/
cancel_requested: boolean;
cancel_requested: boolean;
/**
/**
- * The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null.
+ * A proposal has no result.
*/
*/
- idempotency_key: string | null;
+ has_result: false;
/**
/**
- * svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>.
- */
- created_by: string | null;
- /**
- * The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject.
+ * A queued job waits until then (retry backoff); null otherwise.
*/
*/
- owner_user_id: number | null;
+ run_after: string | null;
/**
/**
- * Who approved or cancelled a proposal or queued job (same forms as created_by).
+ * Nobody has decided yet.
*/
*/
- decided_by: string | null;
- decided_at: string | null;
+ decided_at: null;
created_at: string;
created_at: string;
updated_at: string;
updated_at: string;
started_at: null;
started_at: null;
@@ -7122,9 +7104,12 @@ export interface MediaJobProposedPayload {
@@ -7122,9 +7104,12 @@ export interface MediaJobProposedPayload {
/** [email protected] (owner: media) */
/** [email protected] (owner: media) */
/**
/**
- * media.job.queued v1 (OpenVibe.Media server/jobs/queue.js enqueue, approve; server/events.js recordJob). A job was accepted and queued: a new job (POST /api/v2/:app/jobs), or a proposal its owner approved (decided_by, decided_at). An Idempotency-Key replay, or a request joined to an identical job that is still active, creates no job and no event. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.
+ * media.job.queued v1 (OpenVibe.Media server/jobs/queue.js enqueue, approve; server/events.js recordJob). A job was accepted and queued: a new job (POST /api/v2/:app/jobs), or a proposal its owner approved (decided_at). An Idempotency-Key replay, or a request joined to an identical job that is still active, creates no job and no event. A failed attempt that is queued again is media.job.retrying, not this. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.
*/
*/
export interface MediaJobQueuedPayload {
export interface MediaJobQueuedPayload {
+ /**
+ * The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it.
+ */
id: string;
id: string;
/**
/**
* The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…).
* The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…).
@@ -7140,47 +7125,26 @@ export interface MediaJobQueuedPayload {
@@ -7140,47 +7125,26 @@ export interface MediaJobQueuedPayload {
type: string;
type: string;
status: "queued";
status: "queued";
/**
/**
- * The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent.
- */
- params: {};
- /**
- * null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB.
+ * Attempts started so far.
*/
*/
- result: {} | null;
- /**
- * The last failure's message (free text), or null.
- */
- error: string | null;
- /**
- * Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null.
- */
- error_code: string | null;
attempts: number;
attempts: number;
max_attempts: number;
max_attempts: number;
- /**
- * A queued job waits until then (retry backoff); null otherwise.
- */
- run_after: string | null;
+ error_code: null;
/**
/**
* Its owner asked a running job to stop.
* Its owner asked a running job to stop.
*/
*/
cancel_requested: boolean;
cancel_requested: boolean;
/**
/**
- * The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null.
+ * A job that never ran has no result.
*/
*/
- idempotency_key: string | null;
+ has_result: false;
/**
/**
- * svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>.
- */
- created_by: string | null;
- /**
- * The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject.
+ * A queued job waits until then (retry backoff); null otherwise.
*/
*/
- owner_user_id: number | null;
+ run_after: string | null;
/**
/**
- * Who approved or cancelled a proposal or queued job (same forms as created_by).
+ * When its owner approved it, or cancelled it while it was proposed or queued; null otherwise.
*/
*/
- decided_by: string | null;
decided_at: string | null;
decided_at: string | null;
created_at: string;
created_at: string;
updated_at: string;
updated_at: string;
@@ -7190,9 +7154,12 @@ export interface MediaJobQueuedPayload {
@@ -7190,9 +7154,12 @@ export interface MediaJobQueuedPayload {
/** [email protected] (owner: media) */
/** [email protected] (owner: media) */
/**
/**
- * media.job.started v1 (OpenVibe.Media server/jobs/queue.js claim; server/events.js recordJob). The worker claimed the job and it is running: attempts counts this attempt, started_at stays the first attempt's start. A job that is retried starts again and is announced again. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.
+ * media.job.started v1 (OpenVibe.Media server/jobs/queue.js claim; server/events.js recordJob). The worker claimed the job and it is running (status running): attempts counts this attempt, started_at stays the first attempt's start. A job that is retried starts again and is announced again; error_code is still the previous attempt's until the job finishes. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.
*/
*/
export interface MediaJobStartedPayload {
export interface MediaJobStartedPayload {
+ /**
+ * The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it.
+ */
id: string;
id: string;
/**
/**
* The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…).
* The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…).
@@ -7206,61 +7173,52 @@ export interface MediaJobStartedPayload {
@@ -7206,61 +7173,52 @@ export interface MediaJobStartedPayload {
* Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux.
* Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux.
*/
*/
type: string;
type: string;
- status: "running";
- /**
- * The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent.
- */
- params: {};
/**
/**
- * null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB.
+ * started carries the job's state, running.
*/
*/
- result: {} | null;
- /**
- * The last failure's message (free text), or null.
- */
- error: string | null;
+ status: "running";
/**
/**
- * Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null.
+ * Attempts started so far, this one included.
*/
*/
- error_code: string | null;
attempts: number;
attempts: number;
max_attempts: number;
max_attempts: number;
/**
/**
- * A queued job waits until then (retry backoff); null otherwise.
+ * Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it.
*/
*/
- run_after: string | null;
+ error_code: string | null;
/**
/**
* Its owner asked a running job to stop.
* Its owner asked a running job to stop.
*/
*/
cancel_requested: boolean;
cancel_requested: boolean;
/**
/**
- * The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null.
+ * A running job has no result yet.
*/
*/
- idempotency_key: string | null;
- /**
- * svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>.
- */
- created_by: string | null;
+ has_result: false;
/**
/**
- * The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject.
+ * Cleared when the job is claimed.
*/
*/
- owner_user_id: number | null;
+ run_after: null;
/**
/**
- * Who approved or cancelled a proposal or queued job (same forms as created_by).
+ * When its owner approved it, or cancelled it while it was proposed or queued; null otherwise.
*/
*/
- decided_by: string | null;
decided_at: string | null;
decided_at: string | null;
created_at: string;
created_at: string;
updated_at: string;
updated_at: string;
+ /**
+ * The first attempt's start.
+ */
started_at: string;
started_at: string;
finished_at: null;
finished_at: null;
}
}
/** [email protected] (owner: media) */
/** [email protected] (owner: media) */
/**
/**
- * media.job.retrying v1 (OpenVibe.Media server/jobs/queue.js fail with a retry; server/events.js recordJob). An attempt failed and the job is queued again after a backoff (run_after); error and error_code say why. A running job nobody holds any more (the service restarted, or the worker stopped renewing its lease) is retried this way with error_code interrupted while it has attempts left. status is queued. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.
+ * media.job.retrying v1 (OpenVibe.Media server/jobs/queue.js fail with a retry; server/events.js recordJob). An attempt failed and the job is queued again (status queued) after a backoff (run_after); error_code says why when the handler gave a code. A running job nobody holds any more (the service restarted, or the worker stopped renewing its lease) is retried this way with error_code interrupted while it has attempts left. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.
*/
*/
export interface MediaJobRetryingPayload {
export interface MediaJobRetryingPayload {
+ /**
+ * The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it.
+ */
id: string;
id: string;
/**
/**
* The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…).
* The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…).
@@ -7274,61 +7232,52 @@ export interface MediaJobRetryingPayload {
@@ -7274,61 +7232,52 @@ export interface MediaJobRetryingPayload {
* Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux.
* Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux.
*/
*/
type: string;
type: string;
- status: "queued";
- /**
- * The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent.
- */
- params: {};
- /**
- * null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB.
- */
- result: {} | null;
/**
/**
- * The failure's message, free text from the handler.
+ * retrying carries the job's state, queued.
*/
*/
- error: string;
+ status: "queued";
/**
/**
- * Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null.
+ * Attempts started so far, the failed one included.
*/
*/
- error_code: string | null;
attempts: number;
attempts: number;
max_attempts: number;
max_attempts: number;
/**
/**
- * Not before this time (the retry backoff).
+ * Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it.
*/
*/
- run_after: string;
+ error_code: string | null;
/**
/**
* Its owner asked a running job to stop.
* Its owner asked a running job to stop.
*/
*/
cancel_requested: boolean;
cancel_requested: boolean;
/**
/**
- * The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null.
+ * A job waiting for its retry has no result.
*/
*/
- idempotency_key: string | null;
- /**
- * svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>.
- */
- created_by: string | null;
+ has_result: false;
/**
/**
- * The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject.
+ * Not before this time (the retry backoff).
*/
*/
- owner_user_id: number | null;
+ run_after: string;
/**
/**
- * Who approved or cancelled a proposal or queued job (same forms as created_by).
+ * When its owner approved it, or cancelled it while it was proposed or queued; null otherwise.
*/
*/
- decided_by: string | null;
decided_at: string | null;
decided_at: string | null;
created_at: string;
created_at: string;
updated_at: string;
updated_at: string;
+ /**
+ * The first attempt's start.
+ */
started_at: string;
started_at: string;
finished_at: null;
finished_at: null;
}
}
/** [email protected] (owner: media) */
/** [email protected] (owner: media) */
/**
/**
- * media.job.succeeded v1 (OpenVibe.Media server/jobs/queue.js succeed; server/events.js recordJob). The job finished: result is its type's result (thumbnail.regenerate { url, kind, id }, object.split { source_id, parts, … }, object.remux { source_id, object_id, … }, invariant.scan { thresholds, counts, … }). Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.
+ * media.job.succeeded v1 (OpenVibe.Media server/jobs/queue.js succeed; server/events.js recordJob). The job finished. Its result (thumbnail.regenerate { url, kind, id }, object.split { source_id, parts, … }, object.remux { source_id, object_id, … }, invariant.scan { thresholds, counts, … }) is not in the event: has_result says whether there is one to GET. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.
*/
*/
export interface MediaJobSucceededPayload {
export interface MediaJobSucceededPayload {
+ /**
+ * The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it.
+ */
id: string;
id: string;
/**
/**
* The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…).
* The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…).
@@ -7344,53 +7293,44 @@ export interface MediaJobSucceededPayload {
@@ -7344,53 +7293,44 @@ export interface MediaJobSucceededPayload {
type: string;
type: string;
status: "succeeded";
status: "succeeded";
/**
/**
- * The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent.
- */
- params: {};
- /**
- * The job type's result, or { omitted: true, reason } when it is larger than 8 KB (GET the job for it).
+ * Attempts started, the successful one included.
*/
*/
- result: {} | null;
- error: null;
- error_code: null;
attempts: number;
attempts: number;
max_attempts: number;
max_attempts: number;
/**
/**
- * A queued job waits until then (retry backoff); null otherwise.
+ * A success clears the last failure.
*/
*/
- run_after: string | null;
+ error_code: null;
/**
/**
* Its owner asked a running job to stop.
* Its owner asked a running job to stop.
*/
*/
cancel_requested: boolean;
cancel_requested: boolean;
/**
/**
- * The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null.
- */
- idempotency_key: string | null;
- /**
- * svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>.
- */
- created_by: string | null;
- /**
- * The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject.
+ * The job holds a result. The result itself is never in the event: GET the job with a tenant token for it.
*/
*/
- owner_user_id: number | null;
+ has_result: boolean;
+ run_after: null;
/**
/**
- * Who approved or cancelled a proposal or queued job (same forms as created_by).
+ * When its owner approved it, or cancelled it while it was proposed or queued; null otherwise.
*/
*/
- decided_by: string | null;
decided_at: string | null;
decided_at: string | null;
created_at: string;
created_at: string;
updated_at: string;
updated_at: string;
+ /**
+ * The first attempt's start.
+ */
started_at: string;
started_at: string;
finished_at: string;
finished_at: string;
}
}
/** [email protected] (owner: media) */
/** [email protected] (owner: media) */
/**
/**
- * media.job.failed v1 (OpenVibe.Media server/jobs/queue.js fail; server/events.js recordJob). The job failed for good: a permanent error, or its last attempt (attempts = max_attempts) failed; error and error_code say why. A cancelled job is not a failure and is announced as media.job.cancelled. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.
+ * media.job.failed v1 (OpenVibe.Media server/jobs/queue.js fail; server/events.js recordJob). The job failed for good: a permanent error, or its last attempt (attempts = max_attempts) failed; error_code says why when the handler gave a code (GET the job for the message). A cancelled job is not a failure and is announced as media.job.cancelled. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.
*/
*/
export interface MediaJobFailedPayload {
export interface MediaJobFailedPayload {
+ /**
+ * The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it.
+ */
id: string;
id: string;
/**
/**
* The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…).
* The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…).
@@ -7406,59 +7346,44 @@ export interface MediaJobFailedPayload {
@@ -7406,59 +7346,44 @@ export interface MediaJobFailedPayload {
type: string;
type: string;
status: "failed";
status: "failed";
/**
/**
- * The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent.
- */
- params: {};
- /**
- * null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB.
+ * Attempts started, the last failed one included.
*/
*/
- result: {} | null;
- /**
- * The failure's message, free text from the handler.
- */
- error: string;
- /**
- * Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null.
- */
- error_code: string | null;
attempts: number;
attempts: number;
max_attempts: number;
max_attempts: number;
/**
/**
- * A queued job waits until then (retry backoff); null otherwise.
+ * Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it.
*/
*/
- run_after: string | null;
+ error_code: string | null;
/**
/**
* Its owner asked a running job to stop.
* Its owner asked a running job to stop.
*/
*/
cancel_requested: boolean;
cancel_requested: boolean;
/**
/**
- * The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null.
- */
- idempotency_key: string | null;
- /**
- * svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>.
- */
- created_by: string | null;
- /**
- * The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject.
+ * The handler left a partial result. The result itself is never in the event: GET the job with a tenant token for it.
*/
*/
- owner_user_id: number | null;
+ has_result: boolean;
+ run_after: null;
/**
/**
- * Who approved or cancelled a proposal or queued job (same forms as created_by).
+ * When its owner approved it, or cancelled it while it was proposed or queued; null otherwise.
*/
*/
- decided_by: string | null;
decided_at: string | null;
decided_at: string | null;
created_at: string;
created_at: string;
updated_at: string;
updated_at: string;
+ /**
+ * The first attempt's start.
+ */
started_at: string;
started_at: string;
finished_at: string;
finished_at: string;
}
}
/** [email protected] (owner: media) */
/** [email protected] (owner: media) */
/**
/**
- * media.job.cancelled v1 (OpenVibe.Media server/jobs/queue.js cancel, markCancelled; server/events.js recordJob). The job was cancelled: a proposal or a queued job at once, by its owner (POST /api/v2/:app/jobs/:id/cancel or DELETE /api/v2/:app/jobs/:id) or by the validator withdrawing a proposal that no longer applies (decided_by system:invariant.scan); a running job once it stopped after its owner asked (cancel_requested true, error_code cancelled). Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is the job as GET /api/v2/:app/jobs/:id answers it (queue.jobPublic), except that a result larger than 8 KB is replaced by { omitted: true, reason }. It carries the tenant's params, idempotency_key, the free-text error, created_by/decided_by and owner_user_id (a tenant-local integer user id), never the handler's checkpoint, lease or request hash. Times are SQLite UTC timestamps without a zone (YYYY-MM-DD HH:MM:SS). Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.
+ * media.job.cancelled v1 (OpenVibe.Media server/jobs/queue.js cancel, markCancelled; server/events.js recordJob). The job was cancelled: a proposal or a queued job at once, by its owner (POST /api/v2/:app/jobs/:id/cancel or DELETE /api/v2/:app/jobs/:id) or by the validator withdrawing a proposal that no longer applies (decided_at set, error_code cancelled when a reason was given); a running job once it stopped after its owner asked (cancel_requested true, error_code cancelled). Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.
*/
*/
export interface MediaJobCancelledPayload {
export interface MediaJobCancelledPayload {
+ /**
+ * The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it.
+ */
id: string;
id: string;
/**
/**
* The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…).
* The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…).
@@ -7474,52 +7399,34 @@ export interface MediaJobCancelledPayload {
@@ -7474,52 +7399,34 @@ export interface MediaJobCancelledPayload {
type: string;
type: string;
status: "cancelled";
status: "cancelled";
/**
/**
- * The parameters the job type accepted (at most 16 KB). object.split keeps every field the caller sent.
+ * Attempts started so far.
*/
*/
- params: {};
- /**
- * null until the job finished; a failed or cancelled job may keep what its handler reported. { omitted: true, reason } when larger than 8 KB.
- */
- result: {} | null;
- /**
- * The last failure's message (free text), or null.
- */
- error: string | null;
- /**
- * Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null.
- */
- error_code: string | null;
attempts: number;
attempts: number;
max_attempts: number;
max_attempts: number;
/**
/**
- * A queued job waits until then (retry backoff); null otherwise.
+ * Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it.
*/
*/
- run_after: string | null;
+ error_code: string | null;
/**
/**
* Its owner asked a running job to stop.
* Its owner asked a running job to stop.
*/
*/
cancel_requested: boolean;
cancel_requested: boolean;
/**
/**
- * The creator's Idempotency-Key (1-200 visible ASCII characters), invariant:<object_id>:<type> for a validator proposal, or null.
- */
- idempotency_key: string | null;
- /**
- * svc:<service> | app:<app> | app:<app>:user:<id> | system:<what>.
+ * A stopped running job left a partial result. The result itself is never in the event: GET the job with a tenant token for it.
*/
*/
- created_by: string | null;
+ has_result: boolean;
/**
/**
- * The app's user the job was created for (X-OV-User-Id): an id in the tenant's own user space, not a Network subject.
+ * A queued job waits until then (retry backoff); null otherwise.
*/
*/
- owner_user_id: number | null;
+ run_after: string | null;
/**
/**
- * Who approved or cancelled a proposal or queued job (same forms as created_by).
+ * When its owner approved it, or cancelled it while it was proposed or queued; null otherwise.
*/
*/
- decided_by: string | null;
decided_at: string | null;
decided_at: string | null;
created_at: string;
created_at: string;
updated_at: string;
updated_at: string;
/**
/**
- * null when the job never ran.
+ * The first attempt's start; null when the job was cancelled before it ever ran.
*/
*/
started_at: string | null;
started_at: string | null;
finished_at: string;
finished_at: string;
@@ -146,13 +146,24 @@ for (const id of ['events.event.publish', 'events.event.read', 'events.subscript
@@ -146,13 +146,24 @@ for (const id of ['events.event.publish', 'events.event.read', 'events.subscript
// ── Producers' payloads added in v0.32 (Tips moderation, Media jobs, Live, user modules) ──
// ── Producers' payloads added in v0.32 (Tips moderation, Media jobs, Live, user modules) ──
{
{
- // Media: one contract per server/events.js JOB_TRANSITIONS entry; the payload is queue.jobPublic, every field always present.
+ // Media: one contract per server/events.js JOB_TRANSITIONS entry; the payload is the event projection queue.jobEvent,
+ // every field always present. The tenant's params, result, error text, idempotency key and user ids stay behind GET the job.
const status = { proposed: 'proposed', queued: 'queued', started: 'running', retrying: 'queued', succeeded: 'succeeded', failed: 'failed', cancelled: 'cancelled' };
const status = { proposed: 'proposed', queued: 'queued', started: 'running', retrying: 'queued', succeeded: 'succeeded', failed: 'failed', cancelled: 'cancelled' };
+ const jobFields = ['id', 'app_id', 'object_id', 'type', 'status', 'attempts', 'max_attempts', 'error_code', 'cancel_requested', 'has_result',
+ 'run_after', 'decided_at', 'created_at', 'updated_at', 'started_at', 'finished_at'];
+ const jobTimes = jobFields.filter(k => k === 'run_after' || k.endsWith('_at'));
for (const [t, st] of Object.entries(status)) {
for (const [t, st] of Object.entries(status)) {
const s = contracts.schema(`media.job.${t}`);
const s = contracts.schema(`media.job.${t}`);
ok(s.properties.status.const === st, `media.job.${t} carries status ${st}`);
ok(s.properties.status.const === st, `media.job.${t} carries status ${st}`);
ok(JSON.stringify([...s.required].sort()) === JSON.stringify(Object.keys(s.properties).sort()), `media.job.${t} requires every field it names`);
ok(JSON.stringify([...s.required].sort()) === JSON.stringify(Object.keys(s.properties).sort()), `media.job.${t} requires every field it names`);
+ ok(JSON.stringify(Object.keys(s.properties)) === JSON.stringify(jobFields) && s.additionalProperties === false, `media.job.${t} is exactly the event projection`);
+ ok(['params', 'result', 'error', 'idempotency_key', 'created_by', 'decided_by', 'owner_user_id', 'checkpoint'].every(k => !(k in s.properties)), `media.job.${t} leaves the tenant's data out`);
+ ok(jobTimes.every(k => s.properties[k].type === 'null' || (s.properties[k].format === 'date-time' && /Z\$$/.test(s.properties[k].pattern))), `media.job.${t} times are ISO 8601 UTC or null`);
+ const outcome = ['succeeded', 'failed', 'cancelled'].includes(t);
+ ok(s.properties.finished_at.type === (outcome ? 'string' : 'null'), `media.job.${t} has finished_at ${outcome ? 'always' : 'never'}`);
}
}
+ const sqliteTime = JSON.parse(fs.readFileSync(path.join(ROOT, 'fixtures/media.job.queued/valid/thumbnail.json'), 'utf8'));
+ ok(!contracts.validate('media.job.queued@1', { ...sqliteTime, created_at: '2026-09-23 18:02:11' }).valid, 'a SQLite time is not a media.job time');
// Live: live.stream.ended is live.stream.started plus ended_at and duration_seconds.
// Live: live.stream.ended is live.stream.started plus ended_at and duration_seconds.
const started = contracts.schema('live.stream.started'), ended = contracts.schema('live.stream.ended');
const started = contracts.schema('live.stream.started'), ended = contracts.schema('live.stream.ended');
ok(JSON.stringify(Object.keys(ended.properties)) === JSON.stringify([...Object.keys(started.properties), 'ended_at', 'duration_seconds']), 'live.stream.ended = started + ended_at, duration_seconds');
ok(JSON.stringify(Object.keys(ended.properties)) === JSON.stringify([...Object.keys(started.properties), 'ended_at', 'duration_seconds']), 'live.stream.ended = started + ended_at, duration_seconds');