@happyvertical/smrt-jobs 0.55.4 → 0.55.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -144,6 +144,26 @@ change what the agents runtime will dispatch.
144
144
  every sibling method. Adding the decorator to one method of an existing class
145
145
  is therefore a behaviour change for the rest of it.
146
146
 
147
+ ## Terminal outcomes (#3696)
148
+
149
+ Terminal state is authoritative together with its versioned event. Completion,
150
+ permanent failure, timeout, stale-worker recovery, and cancellation update the
151
+ owned `_smrt_jobs` row and append the safe `_smrt_job_events` projection in one
152
+ database transaction. If either write fails, neither commits. Conditional
153
+ ownership/status predicates prevent a lost race from appending an event.
154
+ Build the event from the conditional update's returned row, not a caller
155
+ snapshot, and bind that update to the row's tenant. Public cancellation must
156
+ retain ambient-tenant isolation; only explicit bypass contexts may cross it.
157
+ Retries are deliberately excluded until a later terminal transition. Ordinary
158
+ progress, logs, start, and retry telemetry remain best-effort.
159
+
160
+ `SmrtJobEventCollection.listTerminalOutcomes()` requires an explicit tenant
161
+ (`null` means global), reads newest-first, scans at most 1,000 candidates, and
162
+ returns only the safe versioned terminal shape. Never add job arguments,
163
+ object IDs, results, raw errors, or stacks to that shape. Applications must
164
+ intersect job IDs with their own authorized ownership records; queue/object
165
+ type/method filters narrow the projection but do not authorize it.
166
+
147
167
  ## Retention (#2375)
148
168
 
149
169
  `_smrt_jobs` and `_smrt_job_events` are append-only in practice — `cleanup()`
package/README.md CHANGED
@@ -158,6 +158,37 @@ long-running work when possible; make external effects idempotent because a
158
158
  process crash after an effect but before its terminal write still permits a
159
159
  later retry.
160
160
 
161
+ ### Read safe terminal outcomes
162
+
163
+ Completion, permanent failure, timeout, stale-worker recovery, and cancellation
164
+ persist the job's terminal state and a versioned `_smrt_job_events` outcome in
165
+ one database transaction. A retry remains nonterminal and is not projected.
166
+ Ordinary progress and log events remain best-effort telemetry.
167
+ The event uses the row returned by the conditional update, so a concurrent
168
+ claim cannot leave a stale attempt count in history. Public cancellation
169
+ retains the normal ambient-tenant check; explicit system context remains the
170
+ operator bypass.
171
+
172
+ ```typescript
173
+ const events = await SmrtJobEventCollection.create({ db });
174
+ const page = await events.listTerminalOutcomes({
175
+ tenantId,
176
+ queues: ['reports'],
177
+ objectTypes: ['SmrtDataSurfaceActionTask'],
178
+ methods: ['run'],
179
+ limit: 100,
180
+ });
181
+ ```
182
+
183
+ The explicit tenant boundary is required; pass `tenantId: null` only for global
184
+ jobs. Results are newest-first and expose job ID, terminal status, queue,
185
+ object type, method, attempt count, completion time, cursor, and the bounded
186
+ failure class `execution`, `timeout`, or `stale-recovery`. They never expose
187
+ arguments, object IDs, results, raw errors, or stacks. Reads inspect at most
188
+ 1,000 candidates per page and follow job-event retention (30 days by default).
189
+ Applications must still authorize each job ID against their own durable
190
+ ownership record before displaying it.
191
+
161
192
  ### Schedule recurring jobs with ScheduleRunner
162
193
 
163
194
  The `ScheduleRunner` polls the `_smrt_agent_schedules` table for due cron entries and creates `SmrtJob` records for the `TaskRunner` to execute. Wire them together via events:
@@ -195,6 +226,7 @@ taskRunner.on('job:failed', (job, error) => {
195
226
  |--------|------------|
196
227
  | `SmrtJob` | Persistent job record stored in `_smrt_jobs` |
197
228
  | `SmrtJobCollection` | Collection with `claimReady()`, `listReady()`, `listByStatus()`, `stats()`, `cleanup()` |
229
+ | `SmrtJobEventCollection` | Durable telemetry plus explicit-tenant `listTerminalOutcomes()` safe projection |
198
230
  | `JobBuilder` | Fluent API: `.delay()`, `.priority()`, `.retries()`, `.queue()`, `.timeout()`, `.enqueue()` |
199
231
  | `JobHandle` | Track, wait, cancel, or retry an enqueued job |
200
232
  | `JobContextLogger` | Logger that auto-injects job context (jobId, attempt, queue) |