@hotmeshio/long-tail 0.24.0 → 0.24.2

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.
Files changed (50) hide show
  1. package/build/adapters/express.d.ts +3 -1
  2. package/build/adapters/express.js +11 -1
  3. package/build/api/yaml-workflows/cron.d.ts +3 -2
  4. package/build/api/yaml-workflows/cron.js +6 -1
  5. package/build/api/yaml-workflows/deploy.d.ts +9 -4
  6. package/build/api/yaml-workflows/deploy.js +39 -12
  7. package/build/lib/db/schemas/039_nats_ws_tickets.sql +12 -0
  8. package/build/lib/db/schemas/040_oauth_client_consent.sql +15 -0
  9. package/build/lib/events/browser-view.d.ts +5 -0
  10. package/build/lib/events/browser-view.js +16 -0
  11. package/build/lib/events/nats-ws-filter.d.ts +8 -8
  12. package/build/lib/events/nats-ws-filter.js +49 -34
  13. package/build/lib/events/nats-ws-proxy.d.ts +5 -0
  14. package/build/lib/events/nats-ws-proxy.js +63 -18
  15. package/build/lib/events/nats-ws-ticket.d.ts +12 -3
  16. package/build/lib/events/nats-ws-ticket.js +25 -4
  17. package/build/modules/config.d.ts +9 -0
  18. package/build/modules/config.js +11 -0
  19. package/build/modules/maintenance.d.ts +2 -0
  20. package/build/modules/maintenance.js +7 -0
  21. package/build/routes/agents.js +4 -3
  22. package/build/routes/oauth-server/authorize.js +6 -4
  23. package/build/routes/yaml-workflows/cron.js +1 -1
  24. package/build/services/agent/authority.d.ts +1 -0
  25. package/build/services/agent/authority.js +7 -1
  26. package/build/services/agent/trigger-registry.js +7 -2
  27. package/build/services/auth/oauth-server/index.d.ts +1 -1
  28. package/build/services/auth/oauth-server/index.js +3 -1
  29. package/build/services/auth/oauth-server/sql.d.ts +21 -2
  30. package/build/services/auth/oauth-server/sql.js +41 -2
  31. package/build/services/auth/oauth-server/store.d.ts +22 -1
  32. package/build/services/auth/oauth-server/store.js +54 -5
  33. package/build/services/cron/index.js +5 -3
  34. package/build/services/maintenance/index.js +8 -0
  35. package/build/services/mcp/seed-service-account.d.ts +4 -1
  36. package/build/services/mcp/seed-service-account.js +11 -6
  37. package/build/services/yaml-workflow/invoke.js +7 -3
  38. package/build/services/yaml-workflow/workers/register.js +3 -13
  39. package/build/services/yaml-workflow/workers/step-tool.d.ts +7 -0
  40. package/build/services/yaml-workflow/workers/step-tool.js +69 -0
  41. package/build/system/mcp-servers/admin/agent-subscriptions.js +2 -1
  42. package/build/system/mcp-servers/admin/schemas.d.ts +6 -6
  43. package/build/system/mcp-servers/admin/yaml-workflows.js +6 -20
  44. package/build/system/mcp-servers/workflow.js +13 -25
  45. package/build/tsconfig.tsbuildinfo +1 -1
  46. package/build/types/maintenance.d.ts +2 -2
  47. package/docs/api/http/mcp-endpoint.md +3 -3
  48. package/docs/maintenance.md +51 -28
  49. package/docs/mcp.md +2 -0
  50. package/package.json +1 -1
@@ -2,19 +2,26 @@
2
2
 
3
3
  HotMesh stores execution artifacts in PostgreSQL: stream messages that carry internal signals between activities, transient job rows that track bookkeeping for individual activity invocations, and entity job rows that represent durable workflow instances. Left unmanaged, these tables grow without bound. The maintenance system provides scheduled, rule-based cleanup that runs as a durable cron workflow inside HotMesh itself.
4
4
 
5
- A default configuration ships with Long Tail. It runs nightly at 2 AM, applies four rules in sequence, and requires no setup. The schedule and rules can be replaced at startup or at runtime through the REST API.
5
+ Maintenance is opt-in. A default configuration ships with Long Tail: pass `maintenance: true` to `start()` and it runs weekly, Sundays at 2 AM, applying five rules in sequence. A config object of your own replaces it, and the rules can be replaced at runtime through the REST API.
6
6
 
7
7
  ## Configuration via start()
8
8
 
9
- Maintenance is enabled by default when you call `start()`. To customize or disable it:
9
+ `start()` registers maintenance only when its `maintenance` option asks for it:
10
+
11
+ | `maintenance` | Effect |
12
+ |---|---|
13
+ | omitted or `false` | No maintenance runs. |
14
+ | `true` | The default configuration (see [Default schedule](#default-schedule)). |
15
+ | an `LTMaintenanceConfig` | That configuration, in place of the default. |
10
16
 
11
17
  ```typescript
12
18
  import { start } from '@hotmeshio/long-tail';
13
19
 
14
- // Default: nightly 2 AM cleanup (no config needed)
20
+ // Default: Sundays at 2 AM
15
21
  await start({
16
22
  database: { connectionString: process.env.DATABASE_URL },
17
23
  workers: [ ... ],
24
+ maintenance: true,
18
25
  });
19
26
 
20
27
  // Custom schedule and rules
@@ -22,6 +29,7 @@ await start({
22
29
  database: { connectionString: process.env.DATABASE_URL },
23
30
  workers: [ ... ],
24
31
  maintenance: {
32
+ appId: 'durable',
25
33
  schedule: '0 3 * * *',
26
34
  rules: [
27
35
  { target: 'streams', olderThan: '24 hours', action: 'delete' },
@@ -29,21 +37,23 @@ await start({
29
37
  { target: 'jobs', olderThan: '14 days', action: 'prune', hasEntity: true },
30
38
  { target: 'jobs', olderThan: '180 days', action: 'delete', pruned: true },
31
39
  { target: 'escalations', olderThan: '180 days', action: 'delete' },
40
+ { target: 'oauth', olderThan: '7 days', action: 'delete' },
32
41
  ],
33
42
  },
34
43
  });
35
44
 
36
- // Disabled entirely
45
+ // No maintenance: omit the option (or pass false)
37
46
  await start({
38
47
  database: { connectionString: process.env.DATABASE_URL },
39
48
  workers: [ ... ],
40
- maintenance: false,
41
49
  });
42
50
  ```
43
51
 
52
+ Maintenance starts once workers start, and not on a readonly instance.
53
+
44
54
  ## What gets cleaned
45
55
 
46
- Five categories of data are subject to maintenance:
56
+ Six categories of data are subject to maintenance:
47
57
 
48
58
  | Category | Description |
49
59
  |---|---|
@@ -52,6 +62,7 @@ Five categories of data are subject to maintenance:
52
62
  | **Entity jobs** | Job rows where `entity` is set. These represent actual workflow instances -- the rows that back `Durable.Client.workflow.search()` calls and execution exports. Deleting them removes the workflow record entirely. |
53
63
  | **Pruned jobs** | Entity jobs that have already had their execution artifacts stripped (the `pruned_at` column is not `NULL`). These retain core data but no longer carry execution scaffolding. They can be hard-deleted after a longer retention window. |
54
64
  | **Terminal escalations** | Escalation rows in a terminal status (`resolved`, `cancelled`, `expired`). Terminal rows are inert audit records -- every engine state transition guards on `status = 'pending'`, so aging them out is safe for live waiters, claims, and signal delivery. The engine-owned prune deletes in atomic batches until the retention horizon is drained. Pending rows are never touched. |
65
+ | **OAuth server rows** | The `/mcp` authorization server's records. Refresh tokens and authorization codes past expiry, or rotated out, longer ago than the window; then grants left with neither, or revoked longer ago than the window; then clients registered longer ago than the window that no one ever consented to. A client still holding a live refresh token keeps its grant, and a client anyone has consented to keeps its registration, so the app can reconnect with the client id it holds. |
55
66
 
56
67
  ## Prune vs. delete
57
68
 
@@ -64,26 +75,31 @@ The intended lifecycle for an entity job is: execute, then prune after a short r
64
75
 
65
76
  ## Default schedule
66
77
 
67
- The built-in configuration (`modules/maintenance.ts`) runs at 2 AM daily and applies four rules in order:
78
+ The default configuration (`modules/maintenance.ts`, enabled with `maintenance: true`) runs Sundays at 2 AM and applies five rules in order:
68
79
 
69
80
  ```typescript
70
81
  {
71
- schedule: '0 2 * * *',
82
+ appId: 'durable',
83
+ schedule: '0 2 * * 0',
72
84
  rules: [
73
- { target: 'streams', action: 'delete', olderThan: '7 days' },
74
- { target: 'jobs', action: 'delete', olderThan: '7 days', hasEntity: false },
75
- { target: 'jobs', action: 'prune', olderThan: '7 days', hasEntity: true },
76
- { target: 'jobs', action: 'delete', olderThan: '90 days', pruned: true },
85
+ { target: 'streams', action: 'delete', olderThan: '30 days' },
86
+ { target: 'jobs', action: 'delete', olderThan: '30 days', hasEntity: false },
87
+ { target: 'jobs', action: 'prune', olderThan: '30 days', hasEntity: true },
88
+ { target: 'jobs', action: 'delete', olderThan: '180 days', pruned: true },
89
+ { target: 'oauth', action: 'delete', olderThan: '7 days' },
77
90
  ],
78
91
  }
79
92
  ```
80
93
 
81
94
  | Rule | Effect |
82
95
  |---|---|
83
- | 1. Delete streams older than 7 days | Removes internal message data that is no longer needed for replay or debugging. |
84
- | 2. Delete transient jobs older than 7 days | Removes activity-level bookkeeping rows that have no associated workflow entity. |
85
- | 3. Prune entity jobs older than 7 days | Strips execution artifacts from workflow instances while preserving core data. Exports and search remain functional. |
86
- | 4. Delete pruned jobs older than 90 days | Hard-deletes workflow instances that were pruned at least 90 days ago. This is the final removal. |
96
+ | 1. Delete streams older than 30 days | Removes internal message data that is no longer needed for replay or debugging. |
97
+ | 2. Delete transient jobs older than 30 days | Removes activity-level bookkeeping rows that have no associated workflow entity. |
98
+ | 3. Prune entity jobs older than 30 days | Strips execution artifacts from workflow instances while preserving core data. Exports and search remain functional. |
99
+ | 4. Delete pruned jobs older than 180 days | Hard-deletes workflow instances that were pruned at least 180 days ago. This is the final removal. |
100
+ | 5. Delete OAuth server rows older than 7 days | Removes expired codes and tokens, unused or revoked grants, and clients never consented to. |
101
+
102
+ A custom `rules` list replaces the defaults. The `oauth` rule is optional: the OAuth server sweeps its own tables as it is used (see [Connect with OAuth](mcp.md#connect-with-oauth)), and the rule adds a sweep on this schedule with its own window.
87
103
 
88
104
  Rules execute sequentially. If one fails, the remaining rules still run; failures are logged and do not halt the cycle.
89
105
 
@@ -95,7 +111,7 @@ Defined in `types/maintenance.ts`. Each rule describes a single cleanup operatio
95
111
 
96
112
  | Field | Type | Required | Description |
97
113
  |---|---|---|---|
98
- | `target` | `'streams' \| 'jobs' \| 'escalations'` | Yes | The resource type to act on. |
114
+ | `target` | `'streams' \| 'jobs' \| 'escalations' \| 'oauth'` | Yes | The resource type to act on. |
99
115
  | `action` | `'delete' \| 'prune'` | Yes | Whether to remove entirely or strip execution artifacts. |
100
116
  | `olderThan` | `string` | Yes | A PostgreSQL interval expression: `'7 days'`, `'24 hours'`, `'90 days'`. |
101
117
  | `hasEntity` | `boolean` | No | When `target` is `'jobs'`: `true` selects entity jobs, `false` selects transient jobs (where `entity IS NULL`). |
@@ -106,6 +122,7 @@ Defined in `types/maintenance.ts`. Each rule describes a single cleanup operatio
106
122
 
107
123
  | Field | Type | Description |
108
124
  |---|---|---|
125
+ | `appId` | `string` | The HotMesh application namespace to prune. The default configuration uses `'durable'`. |
109
126
  | `schedule` | `string` | A cron expression (`'0 2 * * *'`) or an interval string (`'1 day'`). Passed directly to `Virtual.cron` as the `interval` option. |
110
127
  | `rules` | `LTMaintenanceRule[]` | An ordered array of rules. Executed sequentially on each cron tick. |
111
128
 
@@ -117,14 +134,14 @@ The `maintenanceRegistry` singleton (`services/maintenance/index.ts`) follows th
117
134
  |---|---|
118
135
  | `register(config)` | Store a maintenance configuration. Replaces any previously registered config. Call before `connect()`. |
119
136
  | `connect()` | Start the `Virtual.cron` workflow that fires on the configured schedule. Idempotent -- given the same internal cron ID, duplicate calls do not create duplicate schedules. |
120
- | `disconnect()` | Cancel the running cron by calling `Virtual.interrupt`. Safe to call if no cron is running. |
137
+ | `disconnect()` | Mark this registry disconnected, on graceful shutdown or before a reconfiguration. The durable cron job is left in place: it is shared by every container, and another container (or this one after a restart) keeps servicing it. |
121
138
  | `clear()` | Remove the config and reset internal state. Used in tests. |
122
139
  | `hasConfig` | Boolean property. `true` when a config has been registered. |
123
140
  | `config` | Returns the current `LTMaintenanceConfig` or `null`. |
124
141
 
125
142
  ## Programmatic registration
126
143
 
127
- For advanced use cases, you can register maintenance programmatically instead of through `start()`. The `start()` function handles this automatically when a `maintenance` config is provided (or uses the default when omitted):
144
+ For advanced use cases, you can register maintenance programmatically instead of through `start()`. `start()` does this itself when `maintenance` is `true` (the default configuration) or a config object:
128
145
 
129
146
  ```typescript
130
147
  import { maintenanceRegistry, defaultMaintenanceConfig } from '@hotmeshio/long-tail';
@@ -138,6 +155,7 @@ To use a custom config instead, register it in place of the default:
138
155
  import { maintenanceRegistry } from '@hotmeshio/long-tail';
139
156
 
140
157
  maintenanceRegistry.register({
158
+ appId: 'durable',
141
159
  schedule: '0 3 * * 0', // Sundays at 3 AM
142
160
  rules: [
143
161
  { target: 'streams', action: 'delete', olderThan: '14 days' },
@@ -148,7 +166,7 @@ maintenanceRegistry.register({
148
166
  });
149
167
  ```
150
168
 
151
- The registry accepts exactly one config at a time. Calling `register()` again overwrites the previous config. The cron does not start until `connect()` is called — which happens inside `start()` when `hasConfig` is `true`.
169
+ The registry accepts exactly one config at a time. Calling `register()` again overwrites the previous config. The cron does not start until `connect()` is called, which `start()` does when `hasConfig` is `true`.
152
170
 
153
171
  ## Runtime API
154
172
 
@@ -156,12 +174,13 @@ The REST API allows administrators to replace the maintenance config while the s
156
174
 
157
175
  ### `GET /api/config/maintenance`
158
176
 
159
- Returns the current configuration and whether a cron is active.
177
+ Returns the current configuration. `active` is `true` when a configuration is registered; `config` is `null` and `active` is `false` when none is.
160
178
 
161
179
  ```json
162
180
  {
163
181
  "config": {
164
- "schedule": "0 2 * * *",
182
+ "appId": "durable",
183
+ "schedule": "0 2 * * 0",
165
184
  "rules": [ ... ]
166
185
  },
167
186
  "active": true
@@ -174,6 +193,7 @@ Admin-only (requires `requireAdmin` middleware). Accepts a full replacement conf
174
193
 
175
194
  ```json
176
195
  {
196
+ "appId": "durable",
177
197
  "schedule": "0 4 * * *",
178
198
  "rules": [
179
199
  { "target": "streams", "action": "delete", "olderThan": "3 days" },
@@ -186,11 +206,13 @@ Admin-only (requires `requireAdmin` middleware). Accepts a full replacement conf
186
206
 
187
207
  The endpoint performs three operations in sequence:
188
208
 
189
- 1. **Disconnect** -- calls `maintenanceRegistry.disconnect()` to cancel the running cron via `Virtual.interrupt`.
209
+ 1. **Disconnect** -- calls `maintenanceRegistry.disconnect()`.
190
210
  2. **Register** -- calls `maintenanceRegistry.register()` with the new config.
191
- 3. **Connect** -- calls `maintenanceRegistry.connect()` to start a new cron with the updated schedule and rules.
211
+ 3. **Connect** -- calls `maintenanceRegistry.connect()`, which registers this container's cron worker with the new rules.
212
+
213
+ The new rules apply to the ticks this container runs. The configuration is held in memory: other containers keep the rules they started with, and a restart returns this one to its `start()` configuration. The schedule is fixed when the cron job is first created: an existing job keeps its schedule (see [How it works internally](#how-it-works-internally)).
192
214
 
193
- Returns the new config and `{ "restarted": true }` on success. Returns `400` if `schedule` or `rules` is missing, and `500` if any step fails.
215
+ Returns the new config and `{ "restarted": true }` on success. Returns `400` if `appId`, `schedule` or `rules` is missing, and `500` if any step fails.
194
216
 
195
217
  ## Custom schedule example
196
218
 
@@ -200,6 +222,7 @@ A production deployment that processes high volumes might want aggressive short-
200
222
  import { maintenanceRegistry } from '@hotmeshio/long-tail';
201
223
 
202
224
  maintenanceRegistry.register({
225
+ appId: 'durable',
203
226
  schedule: '0 */6 * * *', // every 6 hours
204
227
  rules: [
205
228
  { target: 'streams', action: 'delete', olderThan: '24 hours' },
@@ -217,8 +240,8 @@ This cleans streams and transient jobs within two days, prunes entity jobs after
217
240
  The maintenance system runs on top of HotMesh's `Virtual.cron`, the same primitive that powers durable recurring workflows throughout the platform.
218
241
 
219
242
  1. **Registration.** `maintenanceRegistry.register(config)` stores the config in memory. No I/O occurs.
220
- 2. **Connection.** `maintenanceRegistry.connect()` calls `Virtual.cron()` with a fixed topic (`lt.maintenance.prune`) and a fixed cron ID (`lt-maintenance-nightly`). HotMesh persists the cron schedule in PostgreSQL, making it durable across restarts. If a cron with that ID already exists, it is replaced.
221
- 3. **Execution.** On each tick of the schedule, the cron callback iterates the rules array. For each rule, it translates the rule's fields into the appropriate `dbaService.prune()` parameters and executes the call. Rules run sequentially; a failure in one rule is logged and does not prevent subsequent rules from running.
222
- 4. **Disconnection.** `maintenanceRegistry.disconnect()` calls `Virtual.interrupt()` with the same topic and cron ID, which cancels the recurring schedule. This is called during graceful shutdown and before reconfiguration via the REST API.
243
+ 2. **Connection.** `maintenanceRegistry.connect()` calls `Virtual.cron()` with a fixed topic (`lt.maintenance.prune`) and a fixed cron ID (`lt-maintenance-nightly`). This registers the container's worker for the topic and starts the cron job. HotMesh persists the job in PostgreSQL, making it durable across restarts. If a job with that ID already exists, it keeps running on the schedule it was created with, and no second job starts.
244
+ 3. **Execution.** On each tick of the schedule, the worker that takes the tick runs its rules in order. Stream and job rules call `dbaService.prune()`; escalation rules call the escalation client's batched prune until the window is drained; the `oauth` rule calls the OAuth server prune. A failure in one rule is logged and does not prevent subsequent rules from running.
245
+ 4. **Disconnection.** `maintenanceRegistry.disconnect()` marks the registry disconnected, during graceful shutdown and before reconfiguration via the REST API. It does not interrupt the cron job, which other containers keep servicing.
223
246
 
224
247
  Because the cron is itself a durable workflow, the schedule survives process restarts. If the server goes down at 2 AM and comes back at 2:05 AM, the missed tick executes on reconnection.
package/docs/mcp.md CHANGED
@@ -207,6 +207,8 @@ Allow as me appears only when it grants more than read-only: when you can write
207
207
 
208
208
  Access tokens last 5 minutes; the client refreshes them silently with a refresh token that rotates on each use and lasts 30 days. Every request reads the grant: a client acts with your roles as they are now, and a disconnected app, a revoked token or a deactivated account stops working on the next request. Disconnect a client under **Connected apps** in the user menu; the same happens when the client revokes its token.
209
209
 
210
+ The server keeps its tables bounded on its own. Each process sweeps them on an OAuth write, at most once every `OAUTH_SWEEP_SECONDS` (default 600), deleting rows dead for longer than `OAUTH_RETENTION_DAYS` (default 7): spent and expired codes and refresh tokens, revoked or unused grants, and clients registered but never consented to. A live grant and its current refresh token are kept, and so is any client someone has consented to.
211
+
210
212
  **Enable it** with `auth.oauthServer` in `start()`, or `LT_OAUTH_ISSUER` in the environment:
211
213
 
212
214
  ```typescript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/long-tail",
3
- "version": "0.24.0",
3
+ "version": "0.24.2",
4
4
  "description": "Long Tail Workflows — Durable AI workflows with human-in-the-loop escalation. Powered by PostgreSQL.",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",