@hotmeshio/hotmesh 0.25.6 → 0.26.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.
@@ -0,0 +1,128 @@
1
+ # HotMesh needs — from long-tail
2
+
3
+ A needs document from the long-tail project: the usage patterns we run at
4
+ production scale and the engine support that would let us run them well. Each item
5
+ states what we do, the concrete access pattern, and the engine capability or
6
+ guarantee we need. Ordered by leverage.
7
+
8
+ Context: long-tail is a durable HITL escalation platform on Postgres. Escalations
9
+ live in `public.hmsh_escalations` (engine-created); we layer a `public.lt_escalations`
10
+ view and a few app-owned indexes on top. Our dominant runtime shape is
11
+ **single-role** work-queue traffic — claim / available / count / settle scoped to
12
+ one `role` — plus a per-role escalation form the resolve UI renders.
13
+
14
+ ---
15
+
16
+ ## 1. Safe retention for terminal escalation rows
17
+
18
+ **Usage.** An escalation is written once, resolved once, then read for audit. The
19
+ pending working set stays small — a role-leading pending partial index keeps
20
+ single-role claim/available queries `O(pending)` regardless of table size:
21
+
22
+ ```sql
23
+ WHERE role = $1 AND status = 'pending'
24
+ AND (assigned_to IS NULL OR assigned_until <= NOW())
25
+ AND metadata @> $facets
26
+ ORDER BY priority ASC, created_at ASC
27
+ FOR UPDATE SKIP LOCKED
28
+ ```
29
+
30
+ The terminal set (resolved / expired / cancelled) grows for the life of the
31
+ deployment — the source of long-run table bloat, autovacuum load, and index growth.
32
+
33
+ **Need.** A safe, engine-blessed way to age out terminal rows. Tell us what state
34
+ the engine still reads once an escalation is terminal (replay, completion, audit),
35
+ so we know what is inert, then one of:
36
+ - a supported retention/archive call the engine owns; or
37
+ - a documented contract that a row is safe to delete once its status is terminal
38
+ and older than a horizon; or
39
+ - time sub-partitioning of the terminal set the engine can `DETACH`/`DROP`.
40
+
41
+ `hmsh_escalations` stays a single physical table keyed by `id`/`signal_key` for
42
+ the engine's lookups; retention touches only rows the engine confirms are inert. We
43
+ can supply the horizon and exact DDL.
44
+
45
+ ---
46
+
47
+ ## 2. Stable ownership of app-added indexes on `hmsh_escalations`
48
+
49
+ **Usage.** We add indexes to the engine table via advisory-locked
50
+ `CREATE INDEX CONCURRENTLY IF NOT EXISTS`:
51
+ - a role-leading pending index `(role, priority, created_at) WHERE status='pending'`
52
+ for the claim/available order;
53
+ - a **pending-scoped GIN** on `metadata` (`jsonb_path_ops`) `WHERE status='pending'`
54
+ — keeps the GIN sized to the working set and cuts write amplification on the
55
+ high-churn insert→resolve path;
56
+ - covering partial indexes for resolved-row reads.
57
+
58
+ **Need.** A stable contract that app-added indexes on `hmsh_escalations` survive
59
+ engine migrations — the engine keeps the table in place across upgrades — or a
60
+ documented post-migration hook we can register to re-ensure them. Shipping these
61
+ index shapes in the engine itself also works; the requirement is that they persist.
62
+
63
+ ---
64
+
65
+ ## 3. Atomic metadata write in the `condition()` Leg1 checkpoint
66
+
67
+ **Usage.** A workflow surfaces a HITL wait with `condition(signalId, config)`. We
68
+ pass the target `role`, claim/routing facets, and `metadata.schema_version` (the
69
+ form version the resolve UI renders) in `config.metadata`. Claim/resolve-by-metadata
70
+ and the dashboard read those facets via GIN `@>`; the resolve UI reads
71
+ `schema_version` to render the exact form.
72
+
73
+ **Need.** Guarantee — as a documented public contract — that `config.metadata` is
74
+ written into the escalation row **atomically in the same Leg1 checkpoint** that
75
+ creates the row (one commit, crash-safe). This is the foundation our JIT-versioned
76
+ form pinning and metadata-driven routing stand on.
77
+
78
+ ---
79
+
80
+ ## 4. Honored long-activity timeout + reclaim safety
81
+
82
+ **Usage.** We run a batch pattern: a single proxyActivity loops internally for
83
+ ~60s (poll → reconcile → act), returning one durable checkpoint per call instead of
84
+ one per step, so replay stays compact.
85
+
86
+ **Need.** A documented guarantee that:
87
+ - `proxyActivities({ startToCloseTimeout })` is honored for long (30–120s) activities;
88
+ - the stream reclaim window (`HMSH_XCLAIM_DELAY_MS`) holds an in-flight activity for
89
+ the duration of its `startToCloseTimeout`, so a long activity is not reclaimed and
90
+ re-dispatched (duplicate execution) before it completes. Deriving the reclaim
91
+ window from `startToCloseTimeout` automatically makes a 60s+ activity safe by
92
+ default.
93
+
94
+ ---
95
+
96
+ ## 5. Documented concurrent-condition bound for harvest fan-out
97
+
98
+ **Usage.** A market-maker workflow opens N `condition()` waits and harvests them
99
+ with `Promise.all`, chunked to stay within the safe concurrent-condition bound (a
100
+ fast signaler can arrive before a later condition registers).
101
+
102
+ **Need.** Publish the safe concurrent-condition bound as a stable number with the
103
+ recommended chunking pattern, so we size harvest fan-out to spec rather than
104
+ empirically — and raise it where the engine can.
105
+
106
+ ---
107
+
108
+ ## 6. Signal buffering + startChild leader-keeps-seat as public contract
109
+
110
+ **Usage.** Two behaviors our crash-safe patterns build on:
111
+ - a signal delivered to a workflow **not yet parked** on that signalId is buffered
112
+ and delivered on the next `condition()` await;
113
+ - `startChild` with a **duplicate workflowId** lets the sitting execution keep its
114
+ seat (leader-keeps-seat), enabling self-continuing link chains (`x-l1`, `x-l2`, …)
115
+ as an alternative to `continueAsNew`.
116
+
117
+ **Need.** Elevate both to documented, stable public guarantees (including the signal
118
+ buffering TTL), versioned as contract.
119
+
120
+ ---
121
+
122
+ ### How we consume these
123
+
124
+ Items 1–2 are the scale needs — retention for terminal rows, and durable app-owned
125
+ indexes that keep the hot path `O(pending)`. Item 3 is the atomicity guarantee our
126
+ versioned forms and metadata routing depend on. Items 4–6 are behaviors we already
127
+ build on and want made explicit. We can provide `EXPLAIN` plans, row-count/latency
128
+ profiles, and the exact DDL for any of these on request.
@@ -0,0 +1,111 @@
1
+ # HotMesh v0.26.0 — for the long-tail project
2
+
3
+ This release answers the needs document. Items 1–5 and the signal-buffering
4
+ half of item 6 ship as published contracts with proving tests; the startChild
5
+ contract (item 6b) is scheduled for its own follow-up release. Numbering below
6
+ matches your document.
7
+
8
+ ## 1. Retention for terminal escalation rows — new API
9
+
10
+ Terminal rows (`resolved`, `cancelled`, `expired`) are inert: every engine
11
+ state transition guards on `status = 'pending'`, and the engine reads terminal
12
+ rows only through `list()` / `get()` / `stats()`. Age them out with the new
13
+ engine-owned call:
14
+
15
+ ```typescript
16
+ let deleted: number;
17
+ do {
18
+ ({ deleted } = await client.escalations.prune({ olderThan: '90 days' }));
19
+ } while (deleted > 0);
20
+ ```
21
+
22
+ - `olderThan` is a Postgres interval; rows qualify when
23
+ `updated_at < NOW() - olderThan`.
24
+ - `statuses` narrows which terminal states are pruned (defaults to all three);
25
+ `namespace` scopes to one app; `limit` bounds rows per call (default 10,000,
26
+ cap 100,000) so vacuum pressure stays flat.
27
+ - Each call is one atomic statement — candidate selection
28
+ (`FOR UPDATE SKIP LOCKED`) and the DELETE commit together, so concurrent
29
+ pruners cooperate and live waiters, claims, and signal delivery proceed
30
+ untouched. The proving test backdates a parked waiter 120 days, prunes, and
31
+ resolves it to completion.
32
+ - If you run retention with your own tooling instead, the documented
33
+ safe-to-delete predicate is exactly the one `prune()` uses:
34
+ `status IN ('resolved','cancelled','expired') AND updated_at` older than
35
+ your horizon.
36
+ - Choose a horizon at least as long as your reporting window: `stats()`
37
+ created/resolved counts and `get()` by id reflect the rows retained.
38
+
39
+ ## 2. App-added indexes survive engine migrations — published contract
40
+
41
+ Engine migrations on `public.hmsh_escalations` are strictly additive
42
+ (`CREATE TABLE / INDEX IF NOT EXISTS`, `ADD COLUMN IF NOT EXISTS`, advisory-
43
+ locked), and the table persists in place across upgrades. The engine manages
44
+ only indexes with the `idx_hmsh_esc_*` prefix; indexes under any other name
45
+ are yours and persist. Your advisory-locked
46
+ `CREATE INDEX CONCURRENTLY IF NOT EXISTS` pattern is the recommended one.
47
+
48
+ Two notes on your shapes: the engine's `idx_hmsh_esc_available`
49
+ (`(namespace, app_id, role, priority ASC, created_at ASC) WHERE
50
+ status = 'pending'`) already serves the single-role claim/available order at
51
+ `O(pending)`. Your pending-scoped GIN stays app-owned: the engine keeps its
52
+ full-table GIN because resolver-merged metadata (v0.24.0) is read on resolved
53
+ rows.
54
+
55
+ ## 3. Atomic metadata in the Leg1 checkpoint — published contract
56
+
57
+ Every field of the `condition()` config — `metadata` facets included — is one
58
+ INSERT committed inside the Leg1 checkpoint transaction: one commit,
59
+ crash-safe. From the row's first visible moment it carries its complete
60
+ routing context and metadata, so claim-by-metadata routing and
61
+ `schema_version`-pinned forms can trust every row they read. The proving test
62
+ tight-polls from workflow start and asserts the first sighting is complete.
63
+
64
+ ## 4. Long activities execute exactly once — published contract
65
+
66
+ - `startToCloseTimeout` is honored for long activities (deadline enforcement
67
+ is covered by positive and negative tests).
68
+ - While an activity runs, the consumer heartbeats its stream reservation at
69
+ half the base window, so the message stays leased for the full run —
70
+ your 60s batch loop (poll → reconcile → act, one checkpoint per call) is a
71
+ supported shape at default settings, for any duration. Redelivery happens
72
+ when a consumer crashes and stops heartbeating, and the collation ledger
73
+ settles a redelivered message as a duplicate before the activity function
74
+ re-executes.
75
+ - The proving test runs a 45s activity against a 30s reservation window with
76
+ a second worker actively polling the same queue: exactly one execution.
77
+ - Deployment note: standard Postgres connections have this lease extension.
78
+ A `securedWorker` connection (SECURITY DEFINER stored-proc mode) enforces
79
+ the adaptive reservation window instead — 30s base, scaling to 30 minutes
80
+ under load — so size activities to that window in secured mode.
81
+
82
+ ## 5. Concurrent-condition bound — the published number is a TTL
83
+
84
+ Fan-out is sized by time, and by count it scales freely. A signal that races
85
+ ahead of its `condition()` registration is buffered as a pending signal and
86
+ delivered — crash-safe, in a single transaction — when the wait registers.
87
+ The buffer holds a signal for **10 minutes** by default; pass `expire` to
88
+ `signal()` (e.g. `'1h'`, `'30d'`) when signaling early on purpose.
89
+
90
+ Chunk your harvest so every condition in a chunk registers within the TTL of
91
+ its fastest signaler. The proving test opens 25 waits in one `Promise.all`
92
+ and fires all 25 signals before any wait has registered: every signal is
93
+ delivered to its own wait, in order.
94
+
95
+ ## 6. Contracts
96
+
97
+ - **Signal buffering** is now a documented public guarantee (item 5 above,
98
+ TTL included).
99
+ - **startChild leader-keeps-seat**: the sitting execution keeps its seat
100
+ today, and fire-and-forget `startChild` link chains (`x-l1`, `x-l2`, …)
101
+ work on that behavior. The *awaited* duplicate path (`executeChild` on an
102
+ id that already exists) gets defined semantics — a typed result rather
103
+ than an open wait — in a dedicated follow-up release; until then, treat
104
+ awaited duplicates as unspecified and build chains on the fire-and-forget
105
+ form.
106
+
107
+ ## Upgrading
108
+
109
+ `npm install @hotmeshio/hotmesh@0.26.0`. Migrations are additive and run
110
+ automatically under the existing advisory lock; your indexes and views
111
+ persist. Full durable suite: 433 tests green.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.25.6",
3
+ "version": "0.26.0",
4
4
  "description": "Durable Workflow",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -46,9 +46,12 @@ import { ConditionQueueConfig } from '../../../types/hmsh_escalations';
46
46
  * ## With escalation queue config
47
47
  *
48
48
  * Pass a {@link ConditionQueueConfig} as the second argument to surface the
49
- * pause as a claimable row in `public.hmsh_escalations`. The INSERT is
50
- * committed atomically with the workflow checkpointone write, no
51
- * enrichment step, no secondary round-trip.
49
+ * pause as a claimable row in `public.hmsh_escalations`. The INSERT — every
50
+ * field of the config, including `metadata` facets is committed atomically
51
+ * with the workflow checkpoint: one write, one commit, crash-safe. From the
52
+ * row's first visible moment it carries its complete routing context and
53
+ * metadata, so claim-by-metadata routing and version-pinned facets (e.g. a
54
+ * `schema_version` the resolver UI renders) can trust every row they read.
52
55
  *
53
56
  * ```typescript
54
57
  * const decision = await Durable.workflow.condition<{ approved: boolean }>(
@@ -81,6 +84,14 @@ import { ConditionQueueConfig } from '../../../types/hmsh_escalations';
81
84
  * SLA-gated human waits in the workflow body and let hook functions report
82
85
  * back via `signal()`.
83
86
  *
87
+ * ## Early signals are buffered
88
+ *
89
+ * A signal delivered before its `condition()` registers — a fast signaler,
90
+ * or a payload deposited before the workflow starts — is buffered as a
91
+ * pending signal and delivered when the wait registers. The buffer holds a
92
+ * signal for 10 minutes by default; pass `expire` to `signal()` (e.g.
93
+ * `'1h'`, `'30d'`) to hold it longer when signaling early on purpose.
94
+ *
84
95
  * ## Fan-in: wait for multiple signals in parallel
85
96
  *
86
97
  * ```typescript
@@ -90,6 +101,12 @@ import { ConditionQueueConfig } from '../../../types/hmsh_escalations';
90
101
  * ]);
91
102
  * ```
92
103
  *
104
+ * Harvest fan-out scales the same way: open N waits with `Promise.all` and
105
+ * signal all of them at once. Buffering covers every signal that races
106
+ * ahead of registration, so size fan-out by the pending-signal TTL — how
107
+ * long a racing signal may wait for its condition to register — with no
108
+ * separate bound on the number of concurrent waits.
109
+ *
93
110
  * ## Paired with hook: spawn work, wait for its signal
94
111
  *
95
112
  * ```typescript
@@ -51,9 +51,12 @@ const didRun_1 = require("./didRun");
51
51
  * ## With escalation queue config
52
52
  *
53
53
  * Pass a {@link ConditionQueueConfig} as the second argument to surface the
54
- * pause as a claimable row in `public.hmsh_escalations`. The INSERT is
55
- * committed atomically with the workflow checkpointone write, no
56
- * enrichment step, no secondary round-trip.
54
+ * pause as a claimable row in `public.hmsh_escalations`. The INSERT — every
55
+ * field of the config, including `metadata` facets is committed atomically
56
+ * with the workflow checkpoint: one write, one commit, crash-safe. From the
57
+ * row's first visible moment it carries its complete routing context and
58
+ * metadata, so claim-by-metadata routing and version-pinned facets (e.g. a
59
+ * `schema_version` the resolver UI renders) can trust every row they read.
57
60
  *
58
61
  * ```typescript
59
62
  * const decision = await Durable.workflow.condition<{ approved: boolean }>(
@@ -86,6 +89,14 @@ const didRun_1 = require("./didRun");
86
89
  * SLA-gated human waits in the workflow body and let hook functions report
87
90
  * back via `signal()`.
88
91
  *
92
+ * ## Early signals are buffered
93
+ *
94
+ * A signal delivered before its `condition()` registers — a fast signaler,
95
+ * or a payload deposited before the workflow starts — is buffered as a
96
+ * pending signal and delivered when the wait registers. The buffer holds a
97
+ * signal for 10 minutes by default; pass `expire` to `signal()` (e.g.
98
+ * `'1h'`, `'30d'`) to hold it longer when signaling early on purpose.
99
+ *
89
100
  * ## Fan-in: wait for multiple signals in parallel
90
101
  *
91
102
  * ```typescript
@@ -95,6 +106,12 @@ const didRun_1 = require("./didRun");
95
106
  * ]);
96
107
  * ```
97
108
  *
109
+ * Harvest fan-out scales the same way: open N waits with `Promise.all` and
110
+ * signal all of them at once. Buffering covers every signal that races
111
+ * ahead of registration, so size fan-out by the pending-signal TTL — how
112
+ * long a racing signal may wait for its condition to register — with no
113
+ * separate bound on the number of concurrent waits.
114
+ *
98
115
  * ## Paired with hook: spawn work, wait for its signal
99
116
  *
100
117
  * ```typescript
@@ -111,6 +111,20 @@ declare function wrapActivity<T>(activityName: string, options?: ActivityConfig)
111
111
  * }
112
112
  * ```
113
113
  *
114
+ * ## Long-running activities execute exactly once
115
+ *
116
+ * `startToCloseTimeout` bounds an activity's run and is honored for long
117
+ * work (a 30–120s batch loop is a supported shape: poll → reconcile → act,
118
+ * one durable checkpoint per call). While the activity runs, the consumer
119
+ * heartbeats its stream reservation at half the base window, so the message
120
+ * stays leased for the full run — however long — and is redelivered to
121
+ * another worker only when the owning consumer crashes and stops
122
+ * heartbeating. The collation ledger then guarantees any redelivered
123
+ * message settles as a duplicate before re-executing the activity. With a
124
+ * `securedWorker` connection (SECURITY DEFINER stored-proc mode), lease
125
+ * extension is unavailable and an activity must finish within the adaptive
126
+ * reservation window instead.
127
+ *
114
128
  * @template ACT - The activity type map (use `typeof activities` for inline registration).
115
129
  * @param {ActivityConfig} [options] - Activity configuration including retry policy and routing.
116
130
  * @returns {ProxyType<ACT>} A typed proxy object mapping activity names to their durable wrappers.
@@ -230,6 +230,20 @@ exports.wrapActivity = wrapActivity;
230
230
  * }
231
231
  * ```
232
232
  *
233
+ * ## Long-running activities execute exactly once
234
+ *
235
+ * `startToCloseTimeout` bounds an activity's run and is honored for long
236
+ * work (a 30–120s batch loop is a supported shape: poll → reconcile → act,
237
+ * one durable checkpoint per call). While the activity runs, the consumer
238
+ * heartbeats its stream reservation at half the base window, so the message
239
+ * stays leased for the full run — however long — and is redelivered to
240
+ * another worker only when the owning consumer crashes and stops
241
+ * heartbeating. The collation ledger then guarantees any redelivered
242
+ * message settles as a duplicate before re-executing the activity. With a
243
+ * `securedWorker` connection (SECURITY DEFINER stored-proc mode), lease
244
+ * extension is unavailable and an activity must finish within the adaptive
245
+ * reservation window instead.
246
+ *
233
247
  * @template ACT - The activity type map (use `typeof activities` for inline registration).
234
248
  * @param {ActivityConfig} [options] - Activity configuration including retry policy and routing.
235
249
  * @returns {ProxyType<ACT>} A typed proxy object mapping activity names to their durable wrappers.
@@ -1,7 +1,7 @@
1
1
  import { HotMesh } from '../hotmesh';
2
2
  import { EventsConfig } from '../../types/system_events';
3
3
  import { Connection } from '../../types/durable';
4
- import { EscalationEntry, ClaimEscalationResult, ClaimByMetadataResult, ReleaseEscalationResult, ResolveEscalationResult, CancelEscalationResult, ListEscalationsParams, StatsEscalationsParams, EscalationStats, CreateEscalationParams, UpdateEscalationParams, AppendMilestonesParams, ClaimEscalationParams, ClaimByMetadataParams, ReleaseEscalationParams, ResolveEscalationParams, ResolveByMetadataParams, EscalateToRoleParams, MigrateEscalationParams, ClaimManyParams, EscalateManyToRoleParams, UpdateManyPriorityParams, ResolveManyParams } from '../../types/hmsh_escalations';
4
+ import { EscalationEntry, ClaimEscalationResult, ClaimByMetadataResult, ReleaseEscalationResult, ResolveEscalationResult, CancelEscalationResult, ListEscalationsParams, StatsEscalationsParams, EscalationStats, CreateEscalationParams, UpdateEscalationParams, AppendMilestonesParams, ClaimEscalationParams, ClaimByMetadataParams, ReleaseEscalationParams, ResolveEscalationParams, ResolveByMetadataParams, EscalateToRoleParams, MigrateEscalationParams, ClaimManyParams, EscalateManyToRoleParams, UpdateManyPriorityParams, ResolveManyParams, PruneEscalationsParams, PruneEscalationsResult } from '../../types/hmsh_escalations';
5
5
  export type GetHotMeshFn = (topic: string | null, namespace?: string) => Promise<HotMesh>;
6
6
  export interface EscalationClientConfig {
7
7
  /** Postgres connection options — used when creating a standalone EscalationClient. */
@@ -93,6 +93,18 @@ export declare class EscalationClientService {
93
93
  * Uses the same filter parameters as `list()`.
94
94
  */
95
95
  count(params?: ListEscalationsParams): Promise<number>;
96
+ /**
97
+ * Retention: deletes terminal escalation rows (`resolved`/`cancelled`/`expired`)
98
+ * whose `updated_at` is older than the horizon (a Postgres interval string,
99
+ * e.g. `'90 days'`). Terminal rows are inert — every engine state transition
100
+ * guards on `status = 'pending'` — so pruning them is safe for live waiters,
101
+ * claims, and signal delivery; this call is the engine-blessed way to age
102
+ * out the audit backlog. Each call deletes at most `limit` rows (default
103
+ * 10,000) in one atomic statement; loop until `deleted` is 0 to drain a
104
+ * large backlog. `list()`/`get()`/`stats()` reads over windows older than
105
+ * the pruning horizon reflect only the rows retained.
106
+ */
107
+ prune(params: PruneEscalationsParams): Promise<PruneEscalationsResult>;
96
108
  /** Returns a single escalation row by UUID. Returns `null` if not found. */
97
109
  get(id: string, namespace?: string): Promise<EscalationEntry | null>;
98
110
  /** Looks up an escalation by `signal_key` — the value passed to `condition()`. */
@@ -201,6 +201,21 @@ class EscalationClientService {
201
201
  const hm = await this._engine(null, params?.namespace);
202
202
  return hm.engine.store.countEscalations(params ?? {});
203
203
  }
204
+ /**
205
+ * Retention: deletes terminal escalation rows (`resolved`/`cancelled`/`expired`)
206
+ * whose `updated_at` is older than the horizon (a Postgres interval string,
207
+ * e.g. `'90 days'`). Terminal rows are inert — every engine state transition
208
+ * guards on `status = 'pending'` — so pruning them is safe for live waiters,
209
+ * claims, and signal delivery; this call is the engine-blessed way to age
210
+ * out the audit backlog. Each call deletes at most `limit` rows (default
211
+ * 10,000) in one atomic statement; loop until `deleted` is 0 to drain a
212
+ * large backlog. `list()`/`get()`/`stats()` reads over windows older than
213
+ * the pruning horizon reflect only the rows retained.
214
+ */
215
+ async prune(params) {
216
+ const hm = await this._engine(null, params.namespace);
217
+ return hm.engine.store.pruneEscalations(params);
218
+ }
204
219
  /** Returns a single escalation row by UUID. Returns `null` if not found. */
205
220
  async get(id, namespace) {
206
221
  const hm = await this._engine(null, namespace);
@@ -331,5 +331,18 @@ declare class PostgresStoreService extends StoreService<ProviderClient, Provider
331
331
  escalationStats(params?: import('../../../../types/hmsh_escalations').StatsEscalationsParams): Promise<import('../../../../types/hmsh_escalations').EscalationStats>;
332
332
  listDistinctEscalationTypes(namespace?: string): Promise<string[]>;
333
333
  releaseExpiredEscalations(_namespace?: string): Promise<number>;
334
+ /**
335
+ * Deletes terminal escalation rows (`resolved`/`cancelled`/`expired`) whose
336
+ * `updated_at` is older than the given horizon. Terminal rows are inert —
337
+ * every state transition guards `status = 'pending'` — so pruning them is
338
+ * the engine-blessed retention path for the audit backlog.
339
+ *
340
+ * Single statement: the candidate SELECT (`FOR UPDATE SKIP LOCKED`) and the
341
+ * DELETE are one atomic unit, so concurrent pruners and readers never block
342
+ * each other and a row is either fully gone or fully present. `limit` bounds
343
+ * rows per call to keep vacuum pressure flat; callers loop until `deleted`
344
+ * returns 0.
345
+ */
346
+ pruneEscalations(params: import('../../../../types/hmsh_escalations').PruneEscalationsParams): Promise<import('../../../../types/hmsh_escalations').PruneEscalationsResult>;
334
347
  }
335
348
  export { PostgresStoreService };
@@ -2318,5 +2318,44 @@ class PostgresStoreService extends __1.StoreService {
2318
2318
  // assigned_until, so no background sweep is needed to release expired claims.
2319
2319
  return 0;
2320
2320
  }
2321
+ /**
2322
+ * Deletes terminal escalation rows (`resolved`/`cancelled`/`expired`) whose
2323
+ * `updated_at` is older than the given horizon. Terminal rows are inert —
2324
+ * every state transition guards `status = 'pending'` — so pruning them is
2325
+ * the engine-blessed retention path for the audit backlog.
2326
+ *
2327
+ * Single statement: the candidate SELECT (`FOR UPDATE SKIP LOCKED`) and the
2328
+ * DELETE are one atomic unit, so concurrent pruners and readers never block
2329
+ * each other and a row is either fully gone or fully present. `limit` bounds
2330
+ * rows per call to keep vacuum pressure flat; callers loop until `deleted`
2331
+ * returns 0.
2332
+ */
2333
+ async pruneEscalations(params) {
2334
+ const TERMINAL = ['resolved', 'cancelled', 'expired'];
2335
+ const statuses = params.statuses?.length
2336
+ ? params.statuses.filter((s) => TERMINAL.includes(s))
2337
+ : [...TERMINAL];
2338
+ if (!statuses.length)
2339
+ return { deleted: 0 };
2340
+ const limit = Math.max(1, Math.min(params.limit ?? 10000, 100000));
2341
+ const values = [statuses, params.olderThan, limit];
2342
+ let nsClause = '';
2343
+ if (params.namespace) {
2344
+ values.push(params.namespace);
2345
+ nsClause = `AND namespace = $${values.length}`;
2346
+ }
2347
+ const result = await this.pgClient.query(`WITH doomed AS (
2348
+ SELECT ctid FROM public.hmsh_escalations
2349
+ WHERE status = ANY($1::text[])
2350
+ AND status IN ('resolved', 'cancelled', 'expired')
2351
+ AND updated_at < NOW() - $2::interval
2352
+ ${nsClause}
2353
+ LIMIT $3
2354
+ FOR UPDATE SKIP LOCKED
2355
+ )
2356
+ DELETE FROM public.hmsh_escalations e
2357
+ USING doomed WHERE e.ctid = doomed.ctid`, values);
2358
+ return { deleted: result.rowCount ?? 0 };
2359
+ }
2321
2360
  }
2322
2361
  exports.PostgresStoreService = PostgresStoreService;
@@ -131,6 +131,29 @@ export type CancelEscalationResult = {
131
131
  ok: false;
132
132
  reason: 'not-found' | 'already-terminal';
133
133
  };
134
+ /**
135
+ * Retention parameters for `pruneEscalations`. Prunes only terminal rows
136
+ * (`resolved`/`cancelled`/`expired`) — the statuses every engine state
137
+ * transition treats as final — older than the given horizon.
138
+ */
139
+ export interface PruneEscalationsParams {
140
+ /**
141
+ * Age horizon as a Postgres interval string (e.g. `'90 days'`, `'12 hours'`).
142
+ * Rows qualify when `updated_at < NOW() - olderThan`.
143
+ */
144
+ olderThan: string;
145
+ /** Terminal statuses to prune. Defaults to all three; non-terminal values are ignored. */
146
+ statuses?: Array<'resolved' | 'cancelled' | 'expired'>;
147
+ namespace?: string;
148
+ /**
149
+ * Max rows deleted per call (bounds lock time and vacuum pressure).
150
+ * Default 10,000, capped at 100,000. Loop until `deleted` is 0 to drain.
151
+ */
152
+ limit?: number;
153
+ }
154
+ export interface PruneEscalationsResult {
155
+ deleted: number;
156
+ }
134
157
  export interface ListEscalationsParams {
135
158
  namespace?: string;
136
159
  role?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.25.6",
3
+ "version": "0.26.0",
4
4
  "description": "Durable Workflow",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
package/ISSUE.md DELETED
@@ -1,64 +0,0 @@
1
- ## EIGHTH ITEM — a claimed row's post-signal settle can fail, stranding pending state that contradicts the workflow (dashboard-visible)
2
-
3
- Two order trails, same signature (hike-mono, 0.25.5/0.7.5):
4
- - run A order `a0e68dba`: every station row resolved EXCEPT quality-control —
5
- yet the QC stationWorker delivered (shipping minted 5s later, order
6
- COMPLETED). The QC row sits `pending` + claimed forever.
7
- - run B order `89942462`: identical, at post-print-qa (claimed 07:34:44,
8
- gluing resolved 07:34:54, order COMPLETED 07:35:11; the qa row still
9
- pending+claimed 20+ min later).
10
-
11
- Trigger context (our side, fixed by us): two associates raced a shallow pond
12
- and BOTH booked the same order in the same second (audit rows at 07:34:44) —
13
- so a concurrent claim write was interleaved with the resolve.
14
-
15
- The library-side gap: the resolve path delivered the SIGNAL (workflow
16
- advanced, next station minted) but the row's own transition to `resolved`
17
- was lost against the concurrent claim — no error surfaced anywhere. The
18
- result is dashboard-visible corruption: a live, claimable, countdown-bearing
19
- work item asserting QA is owed on a COMPLETED order (real associates would
20
- work it). Whatever branch handled this (atomic signal_key resolve or the
21
- legacy handle.signal → ltResolveEscalation), signal delivery and row settle
22
- must be atomic — or the settle must retry until the row cannot lie.
23
-
24
- Repro shape: claim a signal_key escalation from principal A, and within the
25
- same second re-claim/book from principal B while principal C resolves by
26
- metadata. Expect: signal delivered AND row resolved, always.
27
-
28
- # RE-FILED — two reports lost when this file was replaced with your 0.25.6 reply (both found the same evening, before it)
29
-
30
- ## NINTH ITEM (re-filed seventh) — a timeout wake KILLS the collator of a collated wait set; parent hangs forever
31
-
32
- `Promise.all(21 × condition(signalId, {timeout:'1320s'}))` in one parent
33
- (hike-mono simulationDirector, 0.25.5). Signal-path delivery through the
34
- collation worked (seat 18 resolved cleanly at 04:55:05, its collator_waiter
35
- slot consumed). At 05:07:25 UTC the FIRST batch of SLA expirations landed
36
- (vm0..vm4 rows flipped 'expired' in the same second) and the collator job
37
- (`HfSVaQSgUaYu3XGsCCjyQWy$C`) went **status -1 in that same second**
38
- (context={}, is_live=t). Every subsequent expiry wake then hit
39
- `Inactive job …$C` at CollatorService.assertJobActive (collator/index.js:13)
40
- via Hook.processTimeHookEvent (hook.js:650) and was dropped. End state,
41
- stable 10+ min: 20 live collator_waiter registry rows, all rows 'expired',
42
- zero pending engine_streams for the collator, parent wedged in Promise.all
43
- forever with no surfaced error.
44
-
45
- Repro: `await Promise.all([condition('k1',{timeout:'30s'}), condition('k2',{timeout:'30s'})])`,
46
- no resolver — let both expire. Expect collator -1 on first expiry, parent wedged.
47
-
48
- Our standing mitigation: each timeout-bearing wait moved into its own child
49
- workflow (inline wait), parent collates ExecChild completions — works, but
50
- collated waits + SLA should compose.
51
-
52
- ## TENTH ITEM (re-filed follow-up) — second of two near-simultaneous sibling wakes lost (no crash, no restart)
53
-
54
- Clean floor, 10 orders: per order, TWO sibling printJobWorkers under one
55
- dispatcher park signal_key waits; a machine resolves BOTH escalations
56
- seconds apart. 3-for-3 wedged orders: LEFT worker woke and completed; RIGHT
57
- worker (resolved moments later) never woke — job row untouched since
58
- creation, 10+ min, stored-pending-signal path did not recover it. 14/14
59
- wakes delivered for the 7 orders whose resolves were spread out. Process up
60
- the whole time (distinct from the reclaim case). Plausibly fixed by commit
61
- 3's atomic wake INSERT ("no interleave window") — flagging so it gets a
62
- sibling-burst regression test: resolve two waits of one parent within the
63
- same tick, both must wake. Our mitigation: 0–4s jitter between sibling
64
- resolves (also more realistic for print hardware).
@@ -1,77 +0,0 @@
1
- # hotmesh 0.25.6 — fixes for the wake-loss and reservation reports (branch: fix/atomic-cancel-wake, three commits)
2
-
3
- Everything below is implemented and fail-first tested (each fix was watched failing
4
- before its change); the branch is in final full-suite verification before merge.
5
-
6
- ## Commit 1 — cancellation wakes are durable
7
-
8
- **Your second addendum (3 of 6 pills slept after the 400ms cancel wave).** Your
9
- watchers settle waits via `cancel()`, and cancel's wake was a post-commit,
10
- best-effort publish whose failure was silently discarded. Now the cancellation
11
- wake commits inside the cancel transaction: a cancel that is durable always
12
- resumes its `condition()` with `null`. A native six-pill burst test pins your
13
- wave shape.
14
-
15
- Also adds a permanent regression test for your converge topology
16
- (`Promise.all([executeChild x4, sleep branch])`): the sleep's timehook mints and
17
- the converge settles.
18
-
19
- ## Commit 2 — dead reservations reclaim; resolved waits disarm their timers
20
-
21
- **Your fifth data point (reservations never reclaimed after process death) — the
22
- highest-leverage fix.** The fallback poller's discovery query filtered
23
- `reserved_at IS NULL`, so a dead holder's reservation was invisible forever.
24
- It now surfaces stale reservations (threshold tracks the configured window;
25
- heartbeating work never matches), through dedicated index-backed branches, and
26
- the fixed function re-deploys to existing databases on boot. A crash or deploy
27
- now costs ≤ ~90 seconds of redelivery latency instead of a permanent wedge —
28
- and this most plausibly explains your third data point too (wake messages
29
- claimed by churning per-principal clients that died were never reclaimed).
30
-
31
- **Your sixth item (armed-timeout drizzle).** When the signal wins an SLA-gated
32
- wait, the armed timeout is now disarmed with the settled wait — root and cycled
33
- dimensions both covered. New timers are jid-stamped so job-scoped purges
34
- (terminate) remove them as well. Timers armed before the upgrade drain as the
35
- one-shot benign events you observed.
36
-
37
- Also hardens the pending-signal handoff and cancel wake into single atomic
38
- statements (safe on pooled connections, no interleave window; an in-window
39
- wake refreshed by a newer signal delivers the newest payload).
40
-
41
- ## Commit 3 — resolve paths made single-statement; bulk resolve protects waiters
42
-
43
- - `resolve()` and `resolveByMetadata()` now use the same single-statement
44
- atomic idiom as cancel: row lock, status guard, and the wake INSERT commit
45
- as one unit on any connection type.
46
- - `resolveMany()` now excludes rows backing a live `condition()` waiter — bulk
47
- resolution carries no wake, so those rows stay pending for a targeted
48
- `resolve()`/`cancel()` instead of silently stranding the workflow.
49
- - Secured-mode worker responses stamp `jid`, so job-scoped purges address them.
50
-
51
- ## Status of the remaining items
52
-
53
- - **Lost `sleep()` timer (your timer forensics):** still not reproduced — your
54
- exact topology mints and converges cleanly under test, and every registration
55
- path checks out atomic. The reclaim fix (commit 2) may retire this too if the
56
- collator's driving message was claimed by a dying client. The decisive
57
- forensic if it recurs: the wedged director's collator job
58
- (`SELECT * FROM durable.jobs WHERE key LIKE '%$C'`) plus that collator jid's
59
- `engine_streams` rows, no filters — it records exactly which item-cycle
60
- stopped.
61
- - **`createClient` construction churn (your fourth data point):** your
62
- per-principal memoization is the right mitigation; an upstream answer
63
- (internal per-auth memoization or an explicit `client.close()`) is under
64
- design as its own change.
65
- - **Crash windows on interrupt's parent notification** (found by our internal
66
- audit, same family as your wake fixes): being engineered in a dedicated
67
- follow-up PR — the straightforward implementation measurably slowed interrupt
68
- dispatch, and interrupts race against live completions by design, so it is
69
- getting the unhurried version.
70
-
71
- ## After upgrading
72
-
73
- - Restarting workers drains any orphaned-reservation backlog automatically.
74
- - The settlement guard can be retired again — cancel and resolve wakes are both
75
- transactional now, and dead reservations self-heal.
76
- - Watch for `hook-signal-pending-redelivered` (in-window wake handled) and the
77
- reclaim redeliveries after any crash: both are the system working as intended.
@@ -1,19 +0,0 @@
1
- jid,reserved_at,retry_attempt,created_at,message
2
- simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm3,2026-07-06 20:19:07.953447+00,0,2026-07-06 20:18:07.945876+00,"{""metadata"":{""guid"":""HxfFFVmLVwtk37194ysP1qQ"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm3"",""gid"":""HAXgFtL8ZicUsOywHCKUauE"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""virtual-farm-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""printerBatch"",""arguments"":[{""machineId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm3"",""operatorId"":""0dfa0000-0000-4000-8000-000000000004"",""printSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""runsSoFar"":6,""plan"":[],""advertId"":""0a4af7d1-2dc4-4e06-9d43-791acf3c49e5""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm3-$printerBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm3-$printerBatch$1-5"",""workflowTopic"":""virtual-farm-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
3
- simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm4,2026-07-06 20:19:10.889217+00,0,2026-07-06 20:18:10.880925+00,"{""metadata"":{""guid"":""HXZnTUGkTix4uF7q14PuP0f"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm4"",""gid"":""HO9SAZEoZGiycaugLuwuUXv"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""virtual-farm-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""printerBatch"",""arguments"":[{""machineId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm4"",""operatorId"":""0dfa0000-0000-4000-8000-000000000005"",""printSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""runsSoFar"":6,""plan"":[],""advertId"":""a1d6d90a-a2cf-4621-a74f-e0ed1013cbc6""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm4-$printerBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm4-$printerBatch$1-5"",""workflowTopic"":""virtual-farm-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
4
- simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm5,2026-07-06 20:19:16.082786+00,0,2026-07-06 20:18:16.072927+00,"{""metadata"":{""guid"":""HE7nFSVKoh2oFe6-lYtCJWV"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm5"",""gid"":""H697gK8YvdS-_waP4NLiL-L"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""virtual-farm-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""printerBatch"",""arguments"":[{""machineId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm5"",""operatorId"":""0dfa0000-0000-4000-8000-000000000006"",""printSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""runsSoFar"":4,""plan"":[],""advertId"":""df441cd6-1811-41f0-b646-3c794ebf71d4""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm5-$printerBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm5-$printerBatch$1-5"",""workflowTopic"":""virtual-farm-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
5
- simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm1,2026-07-06 20:19:05.227375+00,0,2026-07-06 20:18:20.216145+00,"{""metadata"":{""guid"":""HTFIOwV5q0HYi8z04GwX9qj"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm1"",""gid"":""HSR3wD1ryGQ6f5vzzGcO8eV"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""virtual-farm-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""printerBatch"",""arguments"":[{""machineId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm1"",""operatorId"":""0dfa0000-0000-4000-8000-000000000002"",""printSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""runsSoFar"":13,""plan"":[],""advertId"":""e8ee662b-6de6-4a57-b8d6-ee9ed76e1f99""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm1-$printerBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm1-$printerBatch$1-5"",""workflowTopic"":""virtual-farm-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
6
- simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm0,2026-07-06 20:19:09.068044+00,0,2026-07-06 20:18:39.059327+00,"{""metadata"":{""guid"":""HcFhO_I7Fqn_5ftBd4uUTuZ"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm0"",""gid"":""HyVUxkj0OkvEO2r0sdgtTan"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""virtual-farm-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""printerBatch"",""arguments"":[{""machineId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm0"",""operatorId"":""0dfa0000-0000-4000-8000-000000000001"",""printSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""runsSoFar"":12,""plan"":[],""advertId"":""53bd1b3a-8ec3-4c35-a839-da93d9e23bc0""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm0-$printerBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm0-$printerBatch$1-5"",""workflowTopic"":""virtual-farm-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
7
- simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm2,2026-07-06 20:19:18.195319+00,0,2026-07-06 20:19:18.191286+00,"{""metadata"":{""guid"":""HWpXdbtZ-Bx5iqINxvAa6wf"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm2"",""gid"":""HWzNcP3ToTg27Qc5u5MljTT"",""dad"":"",0,16,0,0"",""aid"":""proxyer"",""topic"":""virtual-farm-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""printerBatch"",""arguments"":[{""machineId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm2"",""operatorId"":""0dfa0000-0000-4000-8000-000000000003"",""printSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""runsSoFar"":8,""plan"":[],""advertId"":""a0a84553-5031-456d-ae02-5d9746b606fc""}],""workflowDimension"":""$1"",""index"":6,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm2-$printerBatch$1-6"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-farm-vm2-$printerBatch$1-6"",""workflowTopic"":""virtual-farm-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
8
- simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-manager,2026-07-06 20:19:14.76557+00,0,2026-07-06 20:18:29.759824+00,"{""metadata"":{""guid"":""HzkL-DZTtXyIyDV8oZNZJxI"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-manager"",""gid"":""HCGavoqirvD8FwdwQOWgcov"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""actor-crew-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""managerBatch"",""arguments"":[{""laneId"":""9a93b820-dde3-425b-a320-260b91c08ec2"",""reviewSeconds"":3,""moveSeconds"":3,""idleTickSeconds"":3,""maxIterations"":25,""actorEmail"":""sim.manager.marco@sim.hike.local""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-manager-$managerBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-manager-$managerBatch$1-5"",""workflowTopic"":""actor-crew-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
9
- simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_quality_control-0,2026-07-06 20:19:12.094467+00,0,2026-07-06 20:18:42.089308+00,"{""metadata"":{""guid"":""H2MwGXqBeP8-8nPX-T6ndnU"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_quality_control-0"",""gid"":""H7nf5o8VvELz6NHWGWGNsXe"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""actor-crew-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""associateBatch"",""arguments"":[{""station"":""NEEDS_QUALITY_CONTROL"",""role"":""quality-inspector"",""toStatus"":""NEEDS_SHIPPING"",""dwellSeconds"":4,""betweenSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""touchesSoFar"":7,""unparkedRejects"":[],""troubledOrders"":[""931a1e99-2db6-4280-b034-57e3c42a9aba""],""actorEmail"":""sim.inspector.ines@sim.hike.local""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_quality_control-0-$associateBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_quality_control-0-$associateBatch$1-5"",""workflowTopic"":""actor-crew-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
10
- simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_quality_control-1,2026-07-06 20:19:12.096894+00,0,2026-07-06 20:18:42.093836+00,"{""metadata"":{""guid"":""H4pQ4lScxaW8wUEiP8BQh8E"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_quality_control-1"",""gid"":""HQUqSMYqzsvODyjMvRNPtvl"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""actor-crew-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""associateBatch"",""arguments"":[{""station"":""NEEDS_QUALITY_CONTROL"",""role"":""quality-inspector"",""toStatus"":""NEEDS_SHIPPING"",""dwellSeconds"":4,""betweenSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""touchesSoFar"":7,""unparkedRejects"":[],""troubledOrders"":[""931a1e99-2db6-4280-b034-57e3c42a9aba""],""actorEmail"":""sim.inspector.tomas@sim.hike.local""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_quality_control-1-$associateBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_quality_control-1-$associateBatch$1-5"",""workflowTopic"":""actor-crew-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
11
- simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-draft,2026-07-06 20:19:16.150935+00,0,2026-07-06 20:18:46.146153+00,"{""metadata"":{""guid"":""HgtJMxOb1oYv6jJb2uErF0_"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-draft"",""gid"":""HN6b23imRu-wgDHA-DpxzLV"",""dad"":"",0,14,0,0"",""aid"":""proxyer"",""topic"":""actor-crew-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""associateBatch"",""arguments"":[{""station"":""DRAFT"",""role"":""insole-designer"",""toStatus"":""NEEDS_MANUFACTURING"",""dwellSeconds"":4,""betweenSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""touchesSoFar"":24,""unparkedRejects"":[],""troubledOrders"":[""2b294913-f8ca-4639-9928-3abd53ea9616""],""actorEmail"":""sim.designer.dana@sim.hike.local"",""worklistStation"":""design-approval""}],""workflowDimension"":""$1"",""index"":4,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-draft-$associateBatch$1-4"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-draft-$associateBatch$1-4"",""workflowTopic"":""actor-crew-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
12
- simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_finishing-1,2026-07-06 20:19:04.938838+00,0,2026-07-06 20:18:49.932504+00,"{""metadata"":{""guid"":""HQD-nuRyxBmH431uJ8noMBS"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_finishing-1"",""gid"":""HWM3gVxjxz9_K-EnQpCeFjY"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""actor-crew-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""associateBatch"",""arguments"":[{""station"":""NEEDS_FINISHING"",""role"":""finisher"",""toStatus"":""NEEDS_QUALITY_CONTROL"",""dwellSeconds"":4,""betweenSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""touchesSoFar"":9,""unparkedRejects"":[],""troubledOrders"":[],""actorEmail"":""sim.finisher.kofi@sim.hike.local""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_finishing-1-$associateBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_finishing-1-$associateBatch$1-5"",""workflowTopic"":""actor-crew-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
13
- simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_finishing-0,2026-07-06 20:19:14.016307+00,0,2026-07-06 20:18:59.008759+00,"{""metadata"":{""guid"":""HHBHdHpX2Wear_cVJim70Ps"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_finishing-0"",""gid"":""HkZyt5-JhLoCNX26Q4cyqbq"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""actor-crew-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""associateBatch"",""arguments"":[{""station"":""NEEDS_FINISHING"",""role"":""finisher"",""toStatus"":""NEEDS_QUALITY_CONTROL"",""dwellSeconds"":4,""betweenSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""touchesSoFar"":11,""unparkedRejects"":[],""troubledOrders"":[],""actorEmail"":""sim.finisher.ada@sim.hike.local""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_finishing-0-$associateBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_finishing-0-$associateBatch$1-5"",""workflowTopic"":""actor-crew-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
14
- simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_shipping,2026-07-06 20:19:15.654293+00,0,2026-07-06 20:19:00.646061+00,"{""metadata"":{""guid"":""HffnP49JTwmHAwk3BNRn22l"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_shipping"",""gid"":""HuK-iSfDdYXXBJWo2WwhCDF"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""actor-crew-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""associateBatch"",""arguments"":[{""station"":""NEEDS_SHIPPING"",""role"":""shipping-associate"",""toStatus"":""COMPLETED"",""dwellSeconds"":4,""betweenSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""touchesSoFar"":12,""unparkedRejects"":[],""troubledOrders"":[],""actorEmail"":""sim.shipper.june@sim.hike.local""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_shipping-$associateBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_shipping-$associateBatch$1-5"",""workflowTopic"":""actor-crew-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
15
- simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_gluing-1,2026-07-06 20:19:05.381804+00,0,2026-07-06 20:19:05.3792+00,"{""metadata"":{""guid"":""Hx82JQ7g0Y4XT7nqPFK4km9"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_gluing-1"",""gid"":""HoT3fZZg04_z7BKimsTs-Ei"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""actor-crew-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""associateBatch"",""arguments"":[{""station"":""NEEDS_GLUING"",""role"":""gluer"",""toStatus"":""NEEDS_FINISHING"",""dwellSeconds"":4,""betweenSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""touchesSoFar"":13,""unparkedRejects"":[],""troubledOrders"":[],""actorEmail"":""sim.gluer.dev@sim.hike.local""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_gluing-1-$associateBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_gluing-1-$associateBatch$1-5"",""workflowTopic"":""actor-crew-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
16
- simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_gluing-0,2026-07-06 20:19:05.386589+00,0,2026-07-06 20:19:05.384446+00,"{""metadata"":{""guid"":""HvNFncRHNbqR_OSOOiQ0M12"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_gluing-0"",""gid"":""Hk7Wy8HO98wh45Clgp5-sUo"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""actor-crew-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""associateBatch"",""arguments"":[{""station"":""NEEDS_GLUING"",""role"":""gluer"",""toStatus"":""NEEDS_FINISHING"",""dwellSeconds"":4,""betweenSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""touchesSoFar"":13,""unparkedRejects"":[],""troubledOrders"":[],""actorEmail"":""sim.gluer.rosa@sim.hike.local""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_gluing-0-$associateBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-needs_gluing-0-$associateBatch$1-5"",""workflowTopic"":""actor-crew-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
17
- simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-post_print_qa-1,2026-07-06 20:19:06.832852+00,0,2026-07-06 20:19:06.829652+00,"{""metadata"":{""guid"":""Hg1xKYket-ULnS-HdHaNuiD"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-post_print_qa-1"",""gid"":""HLjxCrhPCUyqBaWbwSE0pxp"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""actor-crew-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""associateBatch"",""arguments"":[{""station"":""POST_PRINT_QA"",""role"":""post-print-qa"",""toStatus"":""NEEDS_GLUING"",""dwellSeconds"":4,""betweenSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""touchesSoFar"":14,""rejectRate"":{""pct"":15,""targetStation"":""PRINTING"",""reasons"":[""Layer separation on the arch — reprint"",""Surface finish below spec"",""Edge delamination at the heel cup"",""Warping on the medial side""],""leftQuantity"":1,""rightQuantity"":0},""unparkedRejects"":[],""troubledOrders"":[],""actorEmail"":""sim.qa.elena@sim.hike.local""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-post_print_qa-1-$associateBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-post_print_qa-1-$associateBatch$1-5"",""workflowTopic"":""actor-crew-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
18
- simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-post_print_qa-0,2026-07-06 20:19:12.746444+00,0,2026-07-06 20:19:12.744474+00,"{""metadata"":{""guid"":""HTl4tkkOMhgso2QMlQlKYvn"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-post_print_qa-0"",""gid"":""HY9mpKnlDbC3a1o3B4R-53h"",""dad"":"",0,15,0,0"",""aid"":""proxyer"",""topic"":""actor-crew-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""associateBatch"",""arguments"":[{""station"":""POST_PRINT_QA"",""role"":""post-print-qa"",""toStatus"":""NEEDS_GLUING"",""dwellSeconds"":4,""betweenSeconds"":2,""idleTickSeconds"":3,""maxIterations"":25,""touchesSoFar"":16,""rejectRate"":{""pct"":15,""targetStation"":""PRINTING"",""reasons"":[""Layer separation on the arch — reprint"",""Surface finish below spec"",""Edge delamination at the heel cup"",""Warping on the medial side""],""leftQuantity"":1,""rightQuantity"":0},""unparkedRejects"":[],""troubledOrders"":[],""actorEmail"":""sim.qa.marcus@sim.hike.local""}],""workflowDimension"":""$1"",""index"":5,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-post_print_qa-0-$associateBatch$1-5"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-crew-post_print_qa-0-$associateBatch$1-5"",""workflowTopic"":""actor-crew-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"
19
- simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-design,2026-07-06 20:19:10.594054+00,0,2026-07-06 20:18:55.585455+00,"{""metadata"":{""guid"":""HeSdgdXIms_1Lhh2nNgc4EP"",""jid"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-design"",""gid"":""HU01R82t5rnplNLKWryLJPz"",""dad"":"",0,40,0,0"",""aid"":""proxyer"",""topic"":""simulation-activity"",""wfn"":""durable.execute""},""data"":{""activityName"":""designTwinBatch"",""arguments"":[{""user"":""sim.taika.iris@sim.hike.local"",""designSeconds"":2,""idleTickSeconds"":3,""maxIterations"":10,""concurrency"":4,""designedOrders"":[""0f596ab9-f449-485d-b06d-e74864750ae7"",""a5cedbc2-f726-4411-9ac3-10fd113532d2"",""c1a09bd7-bd4b-4eb1-bd3e-91d6b45b479f"",""94d74e0e-e4ce-4db7-91f3-02c1c081c2ba"",""0ed99be7-83b6-4a91-9aa5-560d5c110c6b"",""33b987bb-9253-483f-baa4-37d4ffd0903c"",""2c4bc240-1335-423d-96af-e5855b6ccc0f"",""0e7f24bb-3fba-40e5-a5a8-666f5c91fd49"",""ad0c05b6-4528-4f2a-9569-d06c76589b54"",""de2a02e2-33ca-4c0c-843c-5ad422c7c517"",""c008abd0-8b3b-44f7-9f1c-7d314f72f587"",""04a28a34-20f3-4d10-a636-3cb8f504f56e"",""52295fa8-452b-48ca-862b-64cf3ff8cae6"",""cb1dd956-639d-4ff8-9f56-c12f9c5278e8"",""fca4a3f0-fc49-47cd-8285-bf7e11c60dbe"",""bcd42ec0-f047-4acc-8e70-33575ae7535a"",""3b73fb95-2366-4498-ac9a-3f2d9eb4d1c0"",""3c9d174f-7928-42e7-9edb-2f3b38ba0ddc"",""1e61d445-77ba-4013-af89-1a6de07ff407"",""0b066160-06ab-4681-af20-8c36866c9987"",""fa9f996c-2327-4f30-8fdc-e63239483f23"",""ff31ec18-c7e3-4907-85f0-5c4eda42841f"",""f17a825c-8905-4166-b687-54015ddeae6c"",""931a1e99-2db6-4280-b034-57e3c42a9aba"",""2b294913-f8ca-4639-9928-3abd53ea9616"",""04caa323-662a-4c28-8de0-3655d435f206"",""b2eac688-d074-45d9-8a60-e87719b18c81"",""a11092fe-31e5-4cfe-815c-230a09f2165e"",""176e3d03-0813-4816-83fd-a785798a83bb"",""e1d887bd-1ad4-4436-b728-d75e7df2c39a"",""c8a0ffdc-91b0-443a-a8f9-c01c2e3935c0"",""83cbe6eb-ea9b-492e-8657-4f138e3df05e"",""e8e56685-82a3-4f88-a804-b6a8c21f0a4c"",""89d231bf-600b-40ed-8161-e58dbcc7157c"",""6904b329-7a63-465f-aa6a-82a1095638fb"",""9ca2abe9-ae5f-459a-a003-9ea4ad5cafc0"",""cae10b57-439a-4e98-b0d0-1bb172a9d79f"",""f90af7cb-401b-4e68-9f22-a45d34581d64"",""86a1d1b2-7287-48ab-94b3-544a743fda41"",""76793c77-67a0-4148-b83b-fa50f900111c""]}],""workflowDimension"":""$3"",""index"":8,""originJobId"":""simulationDirector-H5zQndzkcfIGHhUM-QXFzcG"",""parentWorkflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-design-$designTwinBatch$3-8"",""workflowId"":""-simulationDirector-H5zQndzkcfIGHhUM-QXFzcG-design-$designTwinBatch$3-8"",""workflowTopic"":""simulation-activity"",""expire"":2592000,""backoffCoefficient"":5,""initialInterval"":1,""maximumAttempts"":3,""maximumInterval"":120}}"