@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.
- package/build/adapters/express.d.ts +3 -1
- package/build/adapters/express.js +11 -1
- package/build/api/yaml-workflows/cron.d.ts +3 -2
- package/build/api/yaml-workflows/cron.js +6 -1
- package/build/api/yaml-workflows/deploy.d.ts +9 -4
- package/build/api/yaml-workflows/deploy.js +39 -12
- package/build/lib/db/schemas/039_nats_ws_tickets.sql +12 -0
- package/build/lib/db/schemas/040_oauth_client_consent.sql +15 -0
- package/build/lib/events/browser-view.d.ts +5 -0
- package/build/lib/events/browser-view.js +16 -0
- package/build/lib/events/nats-ws-filter.d.ts +8 -8
- package/build/lib/events/nats-ws-filter.js +49 -34
- package/build/lib/events/nats-ws-proxy.d.ts +5 -0
- package/build/lib/events/nats-ws-proxy.js +63 -18
- package/build/lib/events/nats-ws-ticket.d.ts +12 -3
- package/build/lib/events/nats-ws-ticket.js +25 -4
- package/build/modules/config.d.ts +9 -0
- package/build/modules/config.js +11 -0
- package/build/modules/maintenance.d.ts +2 -0
- package/build/modules/maintenance.js +7 -0
- package/build/routes/agents.js +4 -3
- package/build/routes/oauth-server/authorize.js +6 -4
- package/build/routes/yaml-workflows/cron.js +1 -1
- package/build/services/agent/authority.d.ts +1 -0
- package/build/services/agent/authority.js +7 -1
- package/build/services/agent/trigger-registry.js +7 -2
- package/build/services/auth/oauth-server/index.d.ts +1 -1
- package/build/services/auth/oauth-server/index.js +3 -1
- package/build/services/auth/oauth-server/sql.d.ts +21 -2
- package/build/services/auth/oauth-server/sql.js +41 -2
- package/build/services/auth/oauth-server/store.d.ts +22 -1
- package/build/services/auth/oauth-server/store.js +54 -5
- package/build/services/cron/index.js +5 -3
- package/build/services/maintenance/index.js +8 -0
- package/build/services/mcp/seed-service-account.d.ts +4 -1
- package/build/services/mcp/seed-service-account.js +11 -6
- package/build/services/yaml-workflow/invoke.js +7 -3
- package/build/services/yaml-workflow/workers/register.js +3 -13
- package/build/services/yaml-workflow/workers/step-tool.d.ts +7 -0
- package/build/services/yaml-workflow/workers/step-tool.js +69 -0
- package/build/system/mcp-servers/admin/agent-subscriptions.js +2 -1
- package/build/system/mcp-servers/admin/schemas.d.ts +6 -6
- package/build/system/mcp-servers/admin/yaml-workflows.js +6 -20
- package/build/system/mcp-servers/workflow.js +13 -25
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/types/maintenance.d.ts +2 -2
- package/docs/api/http/mcp-endpoint.md +3 -3
- package/docs/maintenance.md +51 -28
- package/docs/mcp.md +2 -0
- package/package.json +1 -1
package/docs/maintenance.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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:
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
82
|
+
appId: 'durable',
|
|
83
|
+
schedule: '0 2 * * 0',
|
|
72
84
|
rules: [
|
|
73
|
-
{ target: 'streams', action: 'delete', olderThan: '
|
|
74
|
-
{ target: 'jobs', action: 'delete', olderThan: '
|
|
75
|
-
{ target: 'jobs', action: 'prune', olderThan: '
|
|
76
|
-
{ target: 'jobs', action: 'delete', olderThan: '
|
|
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
|
|
84
|
-
| 2. Delete transient jobs older than
|
|
85
|
-
| 3. Prune entity jobs older than
|
|
86
|
-
| 4. Delete pruned jobs older than
|
|
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()` |
|
|
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()`.
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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()
|
|
209
|
+
1. **Disconnect** -- calls `maintenanceRegistry.disconnect()`.
|
|
190
210
|
2. **Register** -- calls `maintenanceRegistry.register()` with the new config.
|
|
191
|
-
3. **Connect** -- calls `maintenanceRegistry.connect()
|
|
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
|
|
221
|
-
3. **Execution.** On each tick of the schedule, the
|
|
222
|
-
4. **Disconnection.** `maintenanceRegistry.disconnect()`
|
|
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