@ultimat3/core 27.10.4 → 27.12.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/core",
3
- "version": "27.10.4",
3
+ "version": "27.12.0",
4
4
  "description": "Ultimate's foundation: errors, context, env, config, clock, ids, logging, telemetry, lifecycle",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,6 +37,6 @@
37
37
  "test": "bun test"
38
38
  },
39
39
  "dependencies": {
40
- "@ultimat3/schema": "27.10.4"
40
+ "@ultimat3/schema": "27.12.0"
41
41
  }
42
42
  }
@@ -2,7 +2,12 @@
2
2
  // Split from `config.ts`, which sits at its 500-line ceiling; literals only, so it reads no key.
3
3
 
4
4
  import type { AppConfig } from './config';
5
- import { JOBS_CONCURRENCY_DEFAULT } from './config-jobs';
5
+ import {
6
+ JOBS_CONCURRENCY_DEFAULT,
7
+ JOBS_DEAD_RETENTION_MS_DEFAULT,
8
+ JOBS_DONE_RETENTION_MS_DEFAULT,
9
+ JOBS_OUTBOX_RETENTION_MS_DEFAULT,
10
+ } from './config-jobs';
6
11
  import { DRAIN_DEADLINE_DEFAULT_MS } from './drain-deadline';
7
12
  import { defaultReadinessGraceMs } from './lifecycle-grace';
8
13
  import { ROLES } from './roles';
@@ -31,6 +36,9 @@ export function configDefaults(name: string): Omit<AppConfig, Sectioned> {
31
36
  maxAttempts: 5,
32
37
  backoff: 'exponential',
33
38
  visibilityTimeoutMs: 30_000,
39
+ doneRetentionMs: JOBS_DONE_RETENTION_MS_DEFAULT,
40
+ deadRetentionMs: JOBS_DEAD_RETENTION_MS_DEFAULT,
41
+ outboxRetentionMs: JOBS_OUTBOX_RETENTION_MS_DEFAULT,
34
42
  },
35
43
  // ON by default since 22.0.0, when the boot began obeying the key: an app with no section
36
44
  // keeps the `sync` node it always got, and `enabled: false` is the explicit opt-out.
@@ -14,6 +14,26 @@ import { isJsonObject } from './json-object';
14
14
  */
15
15
  export const JOBS_CONCURRENCY_DEFAULT = 8;
16
16
 
17
+ /**
18
+ * `jobs.doneRetentionMs` when no layer says: how long a `done` row and its step records are kept
19
+ * before the boot's hourly purge deletes them. Two weeks — every incident is read inside it, and
20
+ * a finished row is bookkeeping, unlike an inbox message: a `mail.send` input is an address and a
21
+ * rendered body, personal data with no reason to outlive the delivery.
22
+ */
23
+ export const JOBS_DONE_RETENTION_MS_DEFAULT = 14 * 86_400_000;
24
+
25
+ /**
26
+ * `jobs.deadRetentionMs` when no layer says: `dead`, `failed` and `cancelled` rows, which staff
27
+ * read to explain a failure, kept a quarter.
28
+ */
29
+ export const JOBS_DEAD_RETENTION_MS_DEFAULT = 90 * 86_400_000;
30
+
31
+ /**
32
+ * `jobs.outboxRetentionMs` when no layer says: a PUBLISHED `x_outbox` row, whose job is already in
33
+ * `x_jobs`, kept a week. An unpublished row is never swept, whatever this says.
34
+ */
35
+ export const JOBS_OUTBOX_RETENTION_MS_DEFAULT = 7 * 86_400_000;
36
+
17
37
  /** One slot count for every queue, or slots per queue name — `jobWorker`'s own `concurrency`. */
18
38
  export type JobsConcurrency = number | Readonly<Record<string, number>>;
19
39
 
package/src/config.ts CHANGED
@@ -163,6 +163,17 @@ export interface JobsConfig {
163
163
  readonly maxAttempts: number;
164
164
  readonly backoff: (typeof JOB_BACKOFFS)[number];
165
165
  readonly visibilityTimeoutMs: number;
166
+ /**
167
+ * Age past which the hourly purge deletes a `done` row and its steps. Unset: 14 days
168
+ * (`JOBS_DONE_RETENTION_MS_DEFAULT`). OPTIONAL, as `realtime.maxSocketsPerActor` is, so a
169
+ * `JobsConfig` literal written before the key existed still type-checks: `defineConfig` fills it
170
+ * in, and the boot (`runtime-jobs-retention.ts`) resolves the same constant for one that did not.
171
+ */
172
+ readonly doneRetentionMs?: number | undefined;
173
+ /** The same for `dead`, `failed` and `cancelled` rows. Unset: 90 days. */
174
+ readonly deadRetentionMs?: number | undefined;
175
+ /** Age of `published_at` past which a PUBLISHED `x_outbox` row goes. Unset: 7 days. */
176
+ readonly outboxRetentionMs?: number | undefined;
166
177
  }
167
178
 
168
179
  /**
@@ -274,6 +285,9 @@ function validate(config: AppConfig): void {
274
285
  ...jobsConcurrencyIssues(config.jobs.concurrency),
275
286
  countIssue('jobs.maxAttempts', config.jobs.maxAttempts, 1),
276
287
  countIssue('jobs.visibilityTimeoutMs', config.jobs.visibilityTimeoutMs, 1),
288
+ ...(['doneRetentionMs', 'deadRetentionMs', 'outboxRetentionMs'] as const).map((key) =>
289
+ config.jobs[key] === undefined ? undefined : countIssue(`jobs.${key}`, config.jobs[key], 1),
290
+ ),
277
291
  oneOfIssue('jobs.backoff', config.jobs.backoff, JOB_BACKOFFS),
278
292
  countIssue('cache.defaultTtlMs', config.cache.defaultTtlMs, 0),
279
293
  oneOfIssue('database.driver', config.database.driver, DATABASE_DRIVERS),
package/src/index.ts CHANGED
@@ -146,7 +146,13 @@ export type { AiConfig, AiConfigInput, McpConfig } from './config-ai';
146
146
  export type { DrainConfig, HealthConfig, ReadinessMode } from './config-health';
147
147
  export { READINESS_MODES } from './config-health';
148
148
  export type { IslandsConfig, IslandsSection, IslandsSectionInput } from './config-islands';
149
- export { JOBS_CONCURRENCY_DEFAULT, type JobsConcurrency } from './config-jobs';
149
+ export {
150
+ JOBS_CONCURRENCY_DEFAULT,
151
+ JOBS_DEAD_RETENTION_MS_DEFAULT,
152
+ JOBS_DONE_RETENTION_MS_DEFAULT,
153
+ JOBS_OUTBOX_RETENTION_MS_DEFAULT,
154
+ type JobsConcurrency,
155
+ } from './config-jobs';
150
156
  export type {
151
157
  MailConfig,
152
158
  MailRetainMimeConfig,
@@ -675,6 +681,7 @@ export { setLogSink } from './logger';
675
681
  export {
676
682
  isMcpExposed,
677
683
  type McpAnnotationHints,
684
+ type McpEffect,
678
685
  type McpExposureDeclaration,
679
686
  type McpListFilterOp,
680
687
  type McpListParams,
@@ -14,6 +14,15 @@ export interface McpAnnotationHints {
14
14
  readonly openWorldHint?: boolean;
15
15
  }
16
16
 
17
+ /**
18
+ * What an exposed action DOES, for a surface that dispatches by it (`@ultimat3/mcp`'s
19
+ * `surface: 'meta-split'`): `'write'` creates or changes app state, reversibly or within a
20
+ * standing budget; `'delete'` archives, cancels or deletes, sends something outside the app, or
21
+ * commits money. A query is a read and declares none. No default: a wrong one either hides a
22
+ * deletion behind an ordinary write or asks deletion-grade consent for an update.
23
+ */
24
+ export type McpEffect = 'write' | 'delete';
25
+
17
26
  /** One comparison a list filter accepts. A filter key is `<field><op>`: `status_eq`. */
18
27
  export type McpListFilterOp = '_eq' | '_in' | '_gt' | '_lt' | '_cont';
19
28
 
@@ -64,6 +73,12 @@ export interface McpExposureDeclaration {
64
73
  * nothing, `openWorldHint: true` for one that reaches outside the app.
65
74
  */
66
75
  readonly annotations?: McpAnnotationHints;
76
+ /**
77
+ * The action's effect class — required for an action a `'meta-split'` surface groups
78
+ * (`X_MCP_EFFECT_UNDECLARED`), refused on a query (`X_MCP_EFFECT_CONFLICT`). Picks
79
+ * `write_resource` or `delete_resource`; ignored by every other surface.
80
+ */
81
+ readonly effect?: McpEffect;
67
82
  /** A list query's whitelist, carried to the tool for the meta surface. Never an action's. */
68
83
  readonly listParams?: McpListParams;
69
84
  }