@hotmeshio/hotmesh 0.6.1 → 0.8.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.
Files changed (112) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/README.md +179 -142
  3. package/build/index.d.ts +1 -3
  4. package/build/index.js +1 -5
  5. package/build/modules/enums.d.ts +7 -0
  6. package/build/modules/enums.js +16 -1
  7. package/build/modules/utils.d.ts +27 -0
  8. package/build/modules/utils.js +55 -32
  9. package/build/package.json +18 -27
  10. package/build/services/activities/activity.d.ts +43 -6
  11. package/build/services/activities/activity.js +262 -54
  12. package/build/services/activities/await.js +2 -2
  13. package/build/services/activities/cycle.js +1 -1
  14. package/build/services/activities/hook.d.ts +5 -0
  15. package/build/services/activities/hook.js +22 -19
  16. package/build/services/activities/interrupt.js +17 -25
  17. package/build/services/activities/signal.d.ts +4 -2
  18. package/build/services/activities/signal.js +27 -24
  19. package/build/services/activities/worker.js +2 -2
  20. package/build/services/collator/index.d.ts +123 -25
  21. package/build/services/collator/index.js +224 -101
  22. package/build/services/connector/factory.d.ts +1 -1
  23. package/build/services/connector/factory.js +1 -11
  24. package/build/services/connector/providers/postgres.js +3 -0
  25. package/build/services/engine/index.d.ts +5 -5
  26. package/build/services/engine/index.js +36 -15
  27. package/build/services/hotmesh/index.d.ts +66 -15
  28. package/build/services/hotmesh/index.js +84 -15
  29. package/build/services/memflow/index.d.ts +100 -14
  30. package/build/services/memflow/index.js +100 -14
  31. package/build/services/memflow/worker.d.ts +97 -0
  32. package/build/services/memflow/worker.js +217 -0
  33. package/build/services/memflow/workflow/proxyActivities.d.ts +74 -3
  34. package/build/services/memflow/workflow/proxyActivities.js +81 -4
  35. package/build/services/router/consumption/index.d.ts +2 -1
  36. package/build/services/router/consumption/index.js +39 -3
  37. package/build/services/router/error-handling/index.d.ts +3 -3
  38. package/build/services/router/error-handling/index.js +48 -13
  39. package/build/services/router/index.d.ts +1 -0
  40. package/build/services/router/index.js +2 -1
  41. package/build/services/search/factory.js +1 -9
  42. package/build/services/store/factory.js +1 -9
  43. package/build/services/store/index.d.ts +8 -2
  44. package/build/services/store/providers/postgres/kvsql.d.ts +4 -0
  45. package/build/services/store/providers/postgres/kvsql.js +4 -0
  46. package/build/services/store/providers/postgres/kvtransaction.d.ts +2 -0
  47. package/build/services/store/providers/postgres/kvtransaction.js +23 -0
  48. package/build/services/store/providers/postgres/kvtypes/hash/basic.d.ts +51 -0
  49. package/build/services/store/providers/postgres/kvtypes/hash/basic.js +229 -7
  50. package/build/services/store/providers/postgres/kvtypes/hash/expire.js +12 -2
  51. package/build/services/store/providers/postgres/kvtypes/hash/index.d.ts +4 -0
  52. package/build/services/store/providers/postgres/kvtypes/hash/index.js +6 -0
  53. package/build/services/store/providers/postgres/kvtypes/hash/scan.js +30 -10
  54. package/build/services/store/providers/postgres/kvtypes/list.js +68 -10
  55. package/build/services/store/providers/postgres/kvtypes/string.js +60 -10
  56. package/build/services/store/providers/postgres/kvtypes/zset.js +92 -22
  57. package/build/services/store/providers/postgres/postgres.d.ts +23 -3
  58. package/build/services/store/providers/postgres/postgres.js +38 -1
  59. package/build/services/stream/factory.js +1 -17
  60. package/build/services/stream/providers/postgres/kvtables.js +76 -23
  61. package/build/services/stream/providers/postgres/lifecycle.d.ts +19 -0
  62. package/build/services/stream/providers/postgres/lifecycle.js +54 -0
  63. package/build/services/stream/providers/postgres/messages.d.ts +56 -0
  64. package/build/services/stream/providers/postgres/messages.js +253 -0
  65. package/build/services/stream/providers/postgres/notifications.d.ts +59 -0
  66. package/build/services/stream/providers/postgres/notifications.js +357 -0
  67. package/build/services/stream/providers/postgres/postgres.d.ts +110 -11
  68. package/build/services/stream/providers/postgres/postgres.js +196 -488
  69. package/build/services/stream/providers/postgres/scout.d.ts +68 -0
  70. package/build/services/stream/providers/postgres/scout.js +233 -0
  71. package/build/services/stream/providers/postgres/stats.d.ts +49 -0
  72. package/build/services/stream/providers/postgres/stats.js +113 -0
  73. package/build/services/sub/factory.js +1 -9
  74. package/build/services/sub/index.d.ts +1 -1
  75. package/build/services/sub/providers/postgres/postgres.d.ts +1 -1
  76. package/build/services/sub/providers/postgres/postgres.js +53 -6
  77. package/build/services/task/index.d.ts +1 -1
  78. package/build/services/task/index.js +2 -6
  79. package/build/services/worker/index.d.ts +1 -0
  80. package/build/services/worker/index.js +2 -0
  81. package/build/types/hotmesh.d.ts +42 -2
  82. package/build/types/index.d.ts +3 -4
  83. package/build/types/index.js +1 -4
  84. package/build/types/memflow.d.ts +32 -0
  85. package/build/types/provider.d.ts +17 -1
  86. package/build/types/stream.d.ts +92 -1
  87. package/index.ts +0 -4
  88. package/package.json +18 -27
  89. package/build/services/connector/providers/ioredis.d.ts +0 -9
  90. package/build/services/connector/providers/ioredis.js +0 -26
  91. package/build/services/connector/providers/redis.d.ts +0 -9
  92. package/build/services/connector/providers/redis.js +0 -38
  93. package/build/services/search/providers/redis/ioredis.d.ts +0 -23
  94. package/build/services/search/providers/redis/ioredis.js +0 -189
  95. package/build/services/search/providers/redis/redis.d.ts +0 -23
  96. package/build/services/search/providers/redis/redis.js +0 -202
  97. package/build/services/store/providers/redis/_base.d.ts +0 -137
  98. package/build/services/store/providers/redis/_base.js +0 -980
  99. package/build/services/store/providers/redis/ioredis.d.ts +0 -20
  100. package/build/services/store/providers/redis/ioredis.js +0 -180
  101. package/build/services/store/providers/redis/redis.d.ts +0 -18
  102. package/build/services/store/providers/redis/redis.js +0 -199
  103. package/build/services/stream/providers/redis/ioredis.d.ts +0 -61
  104. package/build/services/stream/providers/redis/ioredis.js +0 -272
  105. package/build/services/stream/providers/redis/redis.d.ts +0 -61
  106. package/build/services/stream/providers/redis/redis.js +0 -305
  107. package/build/services/sub/providers/redis/ioredis.d.ts +0 -20
  108. package/build/services/sub/providers/redis/ioredis.js +0 -150
  109. package/build/services/sub/providers/redis/redis.d.ts +0 -18
  110. package/build/services/sub/providers/redis/redis.js +0 -137
  111. package/build/types/redis.d.ts +0 -258
  112. package/build/types/redis.js +0 -11
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "description": "Permanent-Memory Workflows & AI Agents",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -23,16 +23,15 @@
23
23
  "test:await": "NODE_ENV=test jest ./tests/functional/awaiter/postgres.test.ts --detectOpenHandles --forceExit --verbose",
24
24
  "test:compile": "NODE_ENV=test jest ./tests/functional/compile/index.test.ts --detectOpenHandles --forceExit --verbose",
25
25
  "test:connect": "NODE_ENV=test jest ./tests/unit/services/connector/* --detectOpenHandles --forceExit --verbose",
26
- "test:connect:ioredis": "NODE_ENV=test jest ./tests/unit/services/connector/providers/ioredis.test.ts --detectOpenHandles --forceExit --verbose",
27
26
  "test:connect:postgres": "NODE_ENV=test jest ./tests/unit/services/connector/providers/postgres.test.ts --detectOpenHandles --forceExit --verbose",
28
- "test:connect:redis": "NODE_ENV=test jest ./tests/unit/services/connector/providers/ioredis.test.ts --detectOpenHandles --forceExit --verbose",
29
27
  "test:connect:nats": "NODE_ENV=test jest ./tests/unit/services/connector/providers/nats.test.ts --detectOpenHandles --forceExit --verbose",
30
28
  "test:memflow": "NODE_ENV=test jest ./tests/memflow/*/*.test.ts --detectOpenHandles --forceExit --verbose",
29
+ "test:memflow:postgres": "HMSH_LOGLEVEL=info NODE_ENV=test jest ./tests/memflow/*/postgres.test.ts --detectOpenHandles --forceExit --verbose",
31
30
  "test:memflow:basic": "HMSH_LOGLEVEL=info NODE_ENV=test jest ./tests/memflow/basic/postgres.test.ts --detectOpenHandles --forceExit --verbose",
32
- "test:memflow:collision": "NODE_ENV=test jest ./tests/memflow/collision/*.test.ts --detectOpenHandles --forceExit --verbose",
31
+ "test:memflow:collision": "NODE_ENV=test jest ./tests/memflow/collision/postgres.test.ts --detectOpenHandles --forceExit --verbose",
33
32
  "test:memflow:fatal": "NODE_ENV=test jest ./tests/memflow/fatal/*.test.ts --detectOpenHandles --forceExit --verbose",
34
33
  "test:memflow:goodbye": "NODE_ENV=test HMSH_LOGLEVEL=debug jest ./tests/memflow/goodbye/postgres.test.ts --detectOpenHandles --forceExit --verbose",
35
- "test:memflow:interceptor": "NODE_ENV=test HMSH_LOGLEVEL=debug jest ./tests/memflow/interceptor/postgres.test.ts --detectOpenHandles --forceExit --verbose",
34
+ "test:memflow:interceptor": "NODE_ENV=test HMSH_LOGLEVEL=info jest ./tests/memflow/interceptor/postgres.test.ts --detectOpenHandles --forceExit --verbose",
36
35
  "test:memflow:entity": "NODE_ENV=test HMSH_LOGLEVEL=debug jest ./tests/memflow/entity/postgres.test.ts --detectOpenHandles --forceExit --verbose",
37
36
  "test:memflow:agent": "NODE_ENV=test HMSH_LOGLEVEL=debug jest ./tests/memflow/agent/postgres.test.ts --detectOpenHandles --forceExit --verbose",
38
37
  "test:memflow:hello": "HMSH_TELEMETRY=debug HMSH_LOGLEVEL=debug NODE_ENV=test jest ./tests/memflow/helloworld/postgres.test.ts --detectOpenHandles --forceExit --verbose",
@@ -42,37 +41,33 @@
42
41
  "test:memflow:nested": "NODE_ENV=test jest ./tests/memflow/nested/postgres.test.ts --detectOpenHandles --forceExit --verbose",
43
42
  "test:memflow:pipeline": "NODE_ENV=test jest ./tests/memflow/pipeline/postgres.test.ts --detectOpenHandles --forceExit --verbose",
44
43
  "test:memflow:retry": "NODE_ENV=test jest ./tests/memflow/retry/postgres.test.ts --detectOpenHandles --forceExit --verbose",
44
+ "test:memflow:retrypolicy": "NODE_ENV=test jest ./tests/memflow/retry-policy/*.test.ts --detectOpenHandles --forceExit --verbose",
45
45
  "test:memflow:sleep": "NODE_ENV=test jest ./tests/memflow/sleep/postgres.test.ts --detectOpenHandles --forceExit --verbose",
46
46
  "test:memflow:signal": "NODE_ENV=test jest ./tests/memflow/signal/postgres.test.ts --detectOpenHandles --forceExit --verbose",
47
47
  "test:memflow:unknown": "NODE_ENV=test jest ./tests/memflow/unknown/postgres.test.ts --detectOpenHandles --forceExit --verbose",
48
48
  "test:cycle": "NODE_ENV=test jest ./tests/functional/cycle/*.test.ts --detectOpenHandles --forceExit --verbose",
49
- "test:functional": "NODE_ENV=test jest ./tests/functional/* --detectOpenHandles --forceExit --verbose",
49
+ "test:functional": "NODE_ENV=test jest ./tests/functional/**/postgres.test.ts --detectOpenHandles --forceExit --verbose",
50
50
  "test:emit": "NODE_ENV=test jest ./tests/functional/emit/*.test.ts --detectOpenHandles --forceExit --verbose",
51
51
  "test:pending": "NODE_ENV=test jest ./tests/functional/pending/index.test.ts --detectOpenHandles --forceExit --verbose",
52
- "test:hmsh": "NODE_ENV=test jest ./tests/functional/*.test.ts --detectOpenHandles --verbose --forceExit",
53
- "test:hook": "NODE_ENV=test jest ./tests/functional/hook/index.test.ts --detectOpenHandles --forceExit --verbose",
54
- "test:interrupt": "NODE_ENV=test jest ./tests/functional/interrupt/*.test.ts --detectOpenHandles --forceExit --verbose",
52
+ "test:hmsh": "NODE_ENV=test jest ./tests/functional/postgres.test.ts --detectOpenHandles --verbose --forceExit",
53
+ "test:hook": "NODE_ENV=test jest ./tests/functional/hook/postgres.test.ts --detectOpenHandles --forceExit --verbose",
54
+ "test:interrupt": "NODE_ENV=test jest ./tests/functional/interrupt/postgres.test.ts --detectOpenHandles --forceExit --verbose",
55
55
  "test:parallel": "NODE_ENV=test jest ./tests/functional/parallel/index.test.ts --detectOpenHandles --forceExit --verbose",
56
56
  "test:pipe": "NODE_ENV=test jest ./tests/unit/services/pipe/index.test.ts --detectOpenHandles --forceExit --verbose",
57
- "test:quorum": "NODE_ENV=test jest ./tests/functional/quorum/*.test.ts --detectOpenHandles --forceExit --verbose",
58
- "test:reclaim": "NODE_ENV=test jest ./tests/functional/reclaim/*.test.ts --detectOpenHandles --forceExit --verbose",
59
- "test:redeploy": "NODE_ENV=test jest ./tests/functional/redeploy/*.test.ts --detectOpenHandles --forceExit --verbose",
57
+ "test:quorum": "NODE_ENV=test jest ./tests/functional/quorum/postgres.test.ts --detectOpenHandles --forceExit --verbose",
58
+ "test:reclaim": "NODE_ENV=test jest ./tests/functional/reclaim/postgres.test.ts --detectOpenHandles --forceExit --verbose",
59
+ "test:redeploy": "NODE_ENV=test jest ./tests/functional/redeploy/postgres.test.ts --detectOpenHandles --forceExit --verbose",
60
60
  "test:reporter": "NODE_ENV=test jest ./tests/unit/services/reporter/index.test.ts --detectOpenHandles --forceExit --verbose",
61
- "test:reentrant": "NODE_ENV=test jest ./tests/functional/reentrant/*.test.ts --detectOpenHandles --forceExit --verbose",
62
- "test:retry": "NODE_ENV=test jest ./tests/functional/retry/*.test.ts --detectOpenHandles --forceExit --verbose",
63
- "test:sequence": "NODE_ENV=test HMSH_LOGLEVEL=debug jest ./tests/functional/sequence/postgres.test.ts --detectOpenHandles --forceExit --verbose",
64
- "test:signal": "NODE_ENV=test jest ./tests/functional/signal/*.test.ts --detectOpenHandles --forceExit --verbose",
61
+ "test:reentrant": "NODE_ENV=test jest ./tests/functional/reentrant/postgres.test.ts --detectOpenHandles --forceExit --verbose",
62
+ "test:retry": "NODE_ENV=test jest ./tests/functional/retry/postgres.test.ts --detectOpenHandles --forceExit --verbose",
63
+ "test:retrypolicy": "NODE_ENV=test jest ./tests/functional/retry-policy/*.test.ts --detectOpenHandles --forceExit --verbose",
64
+ "test:sequence": "NODE_ENV=test HMSH_LOGLEVEL=info jest ./tests/functional/sequence/postgres.test.ts --detectOpenHandles --forceExit --verbose",
65
+ "test:signal": "NODE_ENV=test jest ./tests/functional/signal/postgres.test.ts --detectOpenHandles --forceExit --verbose",
65
66
  "test:status": "NODE_ENV=test jest ./tests/functional/status/index.test.ts --detectOpenHandles --forceExit --verbose",
66
67
  "test:providers": "NODE_ENV=test jest ./tests/functional/*/providers/*/*.test.ts --detectOpenHandles --forceExit --verbose",
67
- "test:store:ioredis": "NODE_ENV=test jest ./tests/functional/store/providers/redis/ioredis.test.ts --detectOpenHandles --forceExit --verbose",
68
- "test:store:redis": "NODE_ENV=test jest ./tests/functional/store/providers/redis/redis.test.ts --detectOpenHandles --forceExit --verbose",
69
68
  "test:store:postgres": "NODE_ENV=test jest ./tests/functional/store/providers/postgres/postgres.test.ts --detectOpenHandles --forceExit --verbose",
70
- "test:stream:ioredis": "NODE_ENV=test jest ./tests/functional/stream/providers/redis/ioredis.test.ts --detectOpenHandles --forceExit --verbose",
71
- "test:stream:redis": "NODE_ENV=test jest ./tests/functional/stream/providers/redis/redis.test.ts --detectOpenHandles --forceExit --verbose",
72
69
  "test:stream:postgres": "NODE_ENV=test jest ./tests/functional/stream/providers/postgres/postgres.test.ts --detectOpenHandles --forceExit --verbose",
73
70
  "test:stream:nats": "NODE_ENV=test jest ./tests/functional/stream/providers/nats/nats.test.ts --detectOpenHandles --forceExit --verbose",
74
- "test:sub:ioredis": "NODE_ENV=test jest ./tests/functional/sub/providers/redis/ioredis.test.ts --detectOpenHandles --forceExit --verbose",
75
- "test:sub:redis": "NODE_ENV=test jest ./tests/functional/sub/providers/redis/redis.test.ts --detectOpenHandles --forceExit --verbose",
76
71
  "test:sub:postgres": "NODE_ENV=test jest ./tests/functional/sub/providers/postgres/postgres.test.ts --detectOpenHandles --forceExit --verbose",
77
72
  "test:sub:nats": "NODE_ENV=test jest ./tests/functional/sub/providers/nats/nats.test.ts --detectOpenHandles --forceExit --verbose",
78
73
  "test:trigger": "NODE_ENV=test jest ./tests/unit/services/activities/trigger.test.ts --detectOpenHandles --forceExit --verbose",
@@ -114,13 +109,11 @@
114
109
  "eslint-config-prettier": "^9.1.0",
115
110
  "eslint-plugin-import": "^2.29.1",
116
111
  "eslint-plugin-prettier": "^5.1.3",
117
- "ioredis": "^5.3.2",
118
112
  "javascript-obfuscator": "^0.6.2",
119
113
  "jest": "^29.5.0",
120
114
  "nats": "^2.28.0",
121
115
  "openai": "^5.9.0",
122
116
  "pg": "^8.10.0",
123
- "redis": "^4.6.13",
124
117
  "rimraf": "^4.4.1",
125
118
  "terser": "^5.37.0",
126
119
  "ts-jest": "^29.0.5",
@@ -130,9 +123,7 @@
130
123
  "typescript": "^5.0.4"
131
124
  },
132
125
  "peerDependencies": {
133
- "ioredis": "^4.0.0 || ^5.0.0",
134
126
  "nats": "^2.0.0",
135
- "pg": "^8.0.0",
136
- "redis": "^4.0.0"
127
+ "pg": "^8.0.0"
137
128
  }
138
129
  }
@@ -1,7 +1,6 @@
1
1
  import { EngineService } from '../engine';
2
2
  import { ILogger } from '../logger';
3
3
  import { StoreService } from '../store';
4
- import { TelemetryService } from '../telemetry';
5
4
  import { ActivityData, ActivityLeg, ActivityMetadata, ActivityType } from '../../types/activity';
6
5
  import { ProviderClient, ProviderTransaction, TransactionResultList } from '../../types/provider';
7
6
  import { JobState, JobStatus } from '../../types/job';
@@ -24,6 +23,7 @@ declare class Activity {
24
23
  leg: ActivityLeg;
25
24
  adjacencyList: StreamData[];
26
25
  adjacentIndex: number;
26
+ guidLedger: number;
27
27
  constructor(config: ActivityType, data: ActivityData, metadata: ActivityMetadata, hook: ActivityData | null, engine: EngineService, context?: JobState);
28
28
  setLeg(leg: ActivityLeg): void;
29
29
  /**
@@ -37,15 +37,48 @@ declare class Activity {
37
37
  */
38
38
  verifyEntry(): Promise<void>;
39
39
  /**
40
- * Upon entering leg 2 of a duplexed activity
40
+ * Upon entering leg 2 of a duplexed activity.
41
+ * Increments both the activity ledger (+1) and GUID ledger (+1).
42
+ * Stores the GUID ledger value for step-level resume decisions.
41
43
  */
42
44
  verifyReentry(): Promise<number>;
43
45
  processEvent(status?: StreamStatus, code?: StreamCode, type?: 'hook' | 'output'): Promise<void>;
44
- processPending(type: 'hook' | 'output'): Promise<TransactionResultList>;
45
- processSuccess(type: 'hook' | 'output'): Promise<TransactionResultList>;
46
- processError(): Promise<TransactionResultList>;
47
- transitionAdjacent(multiResponse: TransactionResultList, telemetry: TelemetryService): Promise<void>;
46
+ /**
47
+ * Executes the 3-step Leg2 protocol using GUID ledger for
48
+ * crash-safe resume. Each step bundles durable writes with
49
+ * its concluding digit update in a single transaction.
50
+ *
51
+ * @returns true if this transition caused the job to complete
52
+ */
53
+ executeStepProtocol(delta: number, shouldFinalize: boolean): Promise<boolean>;
54
+ /**
55
+ * Extracts the thresholdHit value from transaction results.
56
+ * The setStatusAndCollateGuid result is the last item.
57
+ */
58
+ resolveThresholdHit(results: TransactionResultList): boolean;
59
+ /**
60
+ * Extracts the job status from the last result of a transaction.
61
+ * Used by subclass Leg1 process methods for telemetry.
62
+ */
48
63
  resolveStatus(multiResponse: TransactionResultList): number;
64
+ /**
65
+ * Leg1 entry verification for Category B activities (Leg1-only with children).
66
+ * Returns true if this is a resume (Leg1 already completed on a prior attempt).
67
+ * On resume, loads the GUID ledger for step-level resume decisions.
68
+ */
69
+ verifyLeg1Entry(): Promise<boolean>;
70
+ /**
71
+ * Executes the 3-step Leg1 protocol for Category B activities
72
+ * (Leg1-only with children, e.g., Hook passthrough, Signal, Interrupt-another).
73
+ * Uses the incoming Leg1 message GUID as the GUID ledger key.
74
+ *
75
+ * Step A: setState + notarizeLeg1Completion + step1 markers (transaction 1)
76
+ * Step B: publish children + step2 markers + setStatusAndCollateGuid (transaction 2)
77
+ * Step C: if edge → runJobCompletionTasks + step3 markers + finalize (transaction 3)
78
+ *
79
+ * @returns true if this transition caused the job to complete
80
+ */
81
+ executeLeg1StepProtocol(delta: number): Promise<boolean>;
49
82
  mapJobData(): void;
50
83
  mapInputData(): void;
51
84
  mapOutputData(): void;
@@ -94,6 +127,10 @@ declare class Activity {
94
127
  * @private
95
128
  */
96
129
  shouldPersistJob(): boolean;
130
+ /**
131
+ * Transition method for Category C (Leg1-only, no children, no semaphore change)
132
+ * and Category D (Trigger) activities. NOT used by the Leg2 step protocol.
133
+ */
97
134
  transition(adjacencyList: StreamData[], jobStatus: JobStatus): Promise<string[]>;
98
135
  /**
99
136
  * A job with a vale < -100_000_000 is considered interrupted,
@@ -18,6 +18,7 @@ class Activity {
18
18
  this.status = stream_1.StreamStatus.SUCCESS;
19
19
  this.code = 200;
20
20
  this.adjacentIndex = 0;
21
+ this.guidLedger = 0;
21
22
  this.config = config;
22
23
  this.data = data;
23
24
  this.metadata = metadata;
@@ -56,6 +57,7 @@ class Activity {
56
57
  }
57
58
  catch (error) {
58
59
  await collator_1.CollatorService.notarizeEntry(this);
60
+ //todo: confirm this check is still needed; the edge event cleanup should handle fully
59
61
  if (threshold > 0) {
60
62
  if (this.context.metadata.js === threshold) {
61
63
  //conclude job EXACTLY ONCE
@@ -73,14 +75,19 @@ class Activity {
73
75
  await collator_1.CollatorService.notarizeEntry(this);
74
76
  }
75
77
  /**
76
- * Upon entering leg 2 of a duplexed activity
78
+ * Upon entering leg 2 of a duplexed activity.
79
+ * Increments both the activity ledger (+1) and GUID ledger (+1).
80
+ * Stores the GUID ledger value for step-level resume decisions.
77
81
  */
78
82
  async verifyReentry() {
79
- const guid = this.context.metadata.guid;
83
+ const msgGuid = this.context.metadata.guid;
80
84
  this.setLeg(2);
81
85
  await this.getState();
86
+ this.context.metadata.guid = msgGuid;
82
87
  collator_1.CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid);
83
- return await collator_1.CollatorService.notarizeReentry(this, guid);
88
+ const [activityLedger, guidLedger] = await collator_1.CollatorService.notarizeLeg2Entry(this, msgGuid);
89
+ this.guidLedger = guidLedger;
90
+ return activityLedger;
84
91
  }
85
92
  //******** DUPLEX RE-ENTRY POINT ********//
86
93
  async processEvent(status = stream_1.StreamStatus.SUCCESS, code = 200, type = 'output') {
@@ -108,17 +115,43 @@ class Activity {
108
115
  this.adjacentIndex = collator_1.CollatorService.getDimensionalIndex(collationKey);
109
116
  telemetry = new telemetry_1.TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
110
117
  telemetry.startActivitySpan(this.leg);
111
- let multiResponse;
112
- if (status === stream_1.StreamStatus.PENDING) {
113
- multiResponse = await this.processPending(type);
114
- }
115
- else if (status === stream_1.StreamStatus.SUCCESS) {
116
- multiResponse = await this.processSuccess(type);
118
+ //bind data per status type
119
+ if (status === stream_1.StreamStatus.ERROR) {
120
+ this.bindActivityError(this.data);
121
+ this.adjacencyList = await this.filterAdjacent();
122
+ if (!this.adjacencyList.length) {
123
+ this.bindJobError(this.data);
124
+ }
117
125
  }
118
126
  else {
119
- multiResponse = await this.processError();
127
+ this.bindActivityData(type);
128
+ this.adjacencyList = await this.filterAdjacent();
120
129
  }
121
- this.transitionAdjacent(multiResponse, telemetry);
130
+ this.mapJobData();
131
+ //When an unrecoverable error has no matching transitions
132
+ //(e.g., code 500 from raw errors after retries exhausted),
133
+ //mark the job as terminally errored so the step protocol
134
+ //can force completion via the isErrorTerminal path.
135
+ if (status === stream_1.StreamStatus.ERROR && !this.adjacencyList?.length) {
136
+ if (!this.context.data)
137
+ this.context.data = {};
138
+ this.context.data.done = true;
139
+ this.context.data.$error = {
140
+ message: this.data?.message || 'unknown error',
141
+ code: enums_1.HMSH_CODE_MEMFLOW_MAXED,
142
+ stack: this.data?.stack,
143
+ };
144
+ }
145
+ //determine step parameters
146
+ const delta = status === stream_1.StreamStatus.PENDING
147
+ ? this.adjacencyList.length
148
+ : this.adjacencyList.length - 1;
149
+ const shouldFinalize = status !== stream_1.StreamStatus.PENDING;
150
+ //execute 3-step protocol
151
+ const thresholdHit = await this.executeStepProtocol(delta, shouldFinalize);
152
+ //telemetry
153
+ telemetry.mapActivityAttributes();
154
+ telemetry.setActivityAttributes({});
122
155
  }
123
156
  catch (error) {
124
157
  if (error instanceof errors_1.CollationError) {
@@ -151,50 +184,100 @@ class Activity {
151
184
  this.logger.debug('activity-process-event-end', { jid, aid });
152
185
  }
153
186
  }
154
- async processPending(type) {
155
- this.bindActivityData(type);
156
- this.adjacencyList = await this.filterAdjacent();
157
- this.mapJobData();
158
- const transaction = this.store.transact();
159
- await this.setState(transaction);
160
- await collator_1.CollatorService.notarizeContinuation(this, transaction);
161
- await this.setStatus(this.adjacencyList.length, transaction);
162
- return (await transaction.exec());
163
- }
164
- async processSuccess(type) {
165
- this.bindActivityData(type);
166
- this.adjacencyList = await this.filterAdjacent();
167
- this.mapJobData();
168
- const transaction = this.store.transact();
169
- await this.setState(transaction);
170
- await collator_1.CollatorService.notarizeCompletion(this, transaction);
171
- await this.setStatus(this.adjacencyList.length - 1, transaction);
172
- return (await transaction.exec());
173
- }
174
- async processError() {
175
- this.bindActivityError(this.data);
176
- this.adjacencyList = await this.filterAdjacent();
177
- if (!this.adjacencyList.length) {
178
- this.bindJobError(this.data);
179
- }
180
- this.mapJobData();
181
- const transaction = this.store.transact();
182
- await this.setState(transaction);
183
- await collator_1.CollatorService.notarizeCompletion(this, transaction);
184
- await this.setStatus(this.adjacencyList.length - 1, transaction);
185
- return (await transaction.exec());
186
- }
187
- async transitionAdjacent(multiResponse, telemetry) {
188
- telemetry.mapActivityAttributes();
189
- const jobStatus = this.resolveStatus(multiResponse);
190
- const attrs = { 'app.job.jss': jobStatus };
191
- //adjacencyList membership has already been set at this point (according to activity status)
192
- const messageIds = await this.transition(this.adjacencyList, jobStatus);
193
- if (messageIds?.length) {
194
- attrs['app.activity.mids'] = messageIds.join(',');
195
- }
196
- telemetry.setActivityAttributes(attrs);
187
+ /**
188
+ * Executes the 3-step Leg2 protocol using GUID ledger for
189
+ * crash-safe resume. Each step bundles durable writes with
190
+ * its concluding digit update in a single transaction.
191
+ *
192
+ * @returns true if this transition caused the job to complete
193
+ */
194
+ async executeStepProtocol(delta, shouldFinalize) {
195
+ const msgGuid = this.context.metadata.guid;
196
+ const threshold = this.mapStatusThreshold();
197
+ const { id: appId } = await this.engine.getVID();
198
+ //Step 1: Save work (skip if GUID 10B already set)
199
+ if (!collator_1.CollatorService.isGuidStep1Done(this.guidLedger)) {
200
+ const txn1 = this.store.transact();
201
+ await this.setState(txn1);
202
+ await collator_1.CollatorService.notarizeStep1(this, msgGuid, txn1);
203
+ await txn1.exec();
204
+ }
205
+ //Step 2: Spawn children + semaphore + edge capture (skip if GUID 1B already set)
206
+ let thresholdHit = false;
207
+ if (!collator_1.CollatorService.isGuidStep2Done(this.guidLedger)) {
208
+ const txn2 = this.store.transact();
209
+ //queue step markers first
210
+ await collator_1.CollatorService.notarizeStep2(this, msgGuid, txn2);
211
+ //queue child publications
212
+ for (const child of this.adjacencyList) {
213
+ await this.engine.router?.publishMessage(null, child, txn2);
214
+ }
215
+ //queue semaphore update + edge capture LAST (so result is at end)
216
+ await this.store.setStatusAndCollateGuid(delta, threshold, this.context.metadata.jid, appId, msgGuid, collator_1.CollatorService.WEIGHTS.GUID_SNAPSHOT, txn2);
217
+ const results = (await txn2.exec());
218
+ thresholdHit = this.resolveThresholdHit(results);
219
+ this.logger.debug('step-protocol-step2-complete', {
220
+ jid: this.context.metadata.jid,
221
+ aid: this.metadata.aid,
222
+ delta,
223
+ threshold,
224
+ thresholdHit,
225
+ lastResult: results[results.length - 1],
226
+ resultCount: results.length,
227
+ });
228
+ }
229
+ else {
230
+ //Step 2 already done; check GUID snapshot for edge
231
+ thresholdHit = collator_1.CollatorService.isGuidJobClosed(this.guidLedger);
232
+ }
233
+ //Step 3: Job completion tasks (edge hit OR emit/persist, skip if GUID 100M already set)
234
+ //When an activity marks the job done with an unrecoverable error
235
+ //(e.g., stopper after max retries), force completion even when the
236
+ //semaphore threshold isn't hit (the signaler's +1 contribution
237
+ //prevents threshold 0 from matching).
238
+ const isErrorTerminal = !thresholdHit
239
+ && this.context.data?.done === true
240
+ && !!this.context.data?.$error;
241
+ const needsCompletion = thresholdHit || this.shouldEmit() || this.shouldPersistJob() || isErrorTerminal;
242
+ if (needsCompletion && !collator_1.CollatorService.isGuidStep3Done(this.guidLedger)) {
243
+ const txn3 = this.store.transact();
244
+ const options = (thresholdHit || isErrorTerminal) ? {} : { emit: !this.shouldPersistJob() };
245
+ await this.engine.runJobCompletionTasks(this.context, options, txn3);
246
+ await collator_1.CollatorService.notarizeStep3(this, msgGuid, txn3);
247
+ const shouldFinalizeNow = (thresholdHit || isErrorTerminal) ? shouldFinalize : this.shouldPersistJob();
248
+ if (shouldFinalizeNow) {
249
+ await collator_1.CollatorService.notarizeFinalize(this, txn3);
250
+ }
251
+ await txn3.exec();
252
+ }
253
+ else if (needsCompletion) {
254
+ this.logger.debug('step-protocol-step3-skipped-already-done', {
255
+ jid: this.context.metadata.jid,
256
+ aid: this.metadata.aid,
257
+ });
258
+ }
259
+ else {
260
+ this.logger.debug('step-protocol-no-threshold', {
261
+ jid: this.context.metadata.jid,
262
+ aid: this.metadata.aid,
263
+ thresholdHit,
264
+ });
265
+ }
266
+ return thresholdHit;
267
+ }
268
+ /**
269
+ * Extracts the thresholdHit value from transaction results.
270
+ * The setStatusAndCollateGuid result is the last item.
271
+ */
272
+ resolveThresholdHit(results) {
273
+ const last = results[results.length - 1];
274
+ const value = Array.isArray(last) ? last[1] : last;
275
+ return Number(value) === 1;
197
276
  }
277
+ /**
278
+ * Extracts the job status from the last result of a transaction.
279
+ * Used by subclass Leg1 process methods for telemetry.
280
+ */
198
281
  resolveStatus(multiResponse) {
199
282
  const activityStatus = multiResponse[multiResponse.length - 1];
200
283
  if (Array.isArray(activityStatus)) {
@@ -204,6 +287,127 @@ class Activity {
204
287
  return Number(activityStatus);
205
288
  }
206
289
  }
290
+ /**
291
+ * Leg1 entry verification for Category B activities (Leg1-only with children).
292
+ * Returns true if this is a resume (Leg1 already completed on a prior attempt).
293
+ * On resume, loads the GUID ledger for step-level resume decisions.
294
+ */
295
+ async verifyLeg1Entry() {
296
+ const msgGuid = this.context.metadata.guid;
297
+ this.setLeg(1);
298
+ await this.getState();
299
+ this.context.metadata.guid = msgGuid;
300
+ const threshold = this.mapStatusThreshold();
301
+ try {
302
+ collator_1.CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid, threshold);
303
+ }
304
+ catch (error) {
305
+ if (error instanceof errors_1.InactiveJobError && threshold > 0) {
306
+ //Dynamic Activation Control: threshold met, close the job
307
+ await collator_1.CollatorService.notarizeEntry(this);
308
+ if (this.context.metadata.js === threshold) {
309
+ //conclude job EXACTLY ONCE
310
+ const status = await this.setStatus(-threshold);
311
+ if (Number(status) === 0) {
312
+ await this.engine.runJobCompletionTasks(this.context);
313
+ }
314
+ }
315
+ }
316
+ throw error;
317
+ }
318
+ try {
319
+ await collator_1.CollatorService.notarizeEntry(this);
320
+ return false;
321
+ }
322
+ catch (error) {
323
+ if (error instanceof errors_1.CollationError && error.fault === 'duplicate') {
324
+ if (this.config.cycle) {
325
+ //Cycle re-entry: Leg1 already complete from prior iteration.
326
+ //Increment Leg2 counter to derive the new dimensional index,
327
+ //so children run in a fresh dimensional plane.
328
+ const [activityLedger, guidLedger] = await collator_1.CollatorService.notarizeLeg2Entry(this, msgGuid);
329
+ this.adjacentIndex =
330
+ collator_1.CollatorService.getDimensionalIndex(activityLedger);
331
+ this.guidLedger = guidLedger;
332
+ return false;
333
+ }
334
+ //100B is set — Leg1 work already committed. Load GUID for step resume.
335
+ const guidValue = await this.store.collateSynthetic(this.context.metadata.jid, msgGuid, 0);
336
+ this.guidLedger = guidValue;
337
+ return true;
338
+ }
339
+ throw error;
340
+ }
341
+ }
342
+ /**
343
+ * Executes the 3-step Leg1 protocol for Category B activities
344
+ * (Leg1-only with children, e.g., Hook passthrough, Signal, Interrupt-another).
345
+ * Uses the incoming Leg1 message GUID as the GUID ledger key.
346
+ *
347
+ * Step A: setState + notarizeLeg1Completion + step1 markers (transaction 1)
348
+ * Step B: publish children + step2 markers + setStatusAndCollateGuid (transaction 2)
349
+ * Step C: if edge → runJobCompletionTasks + step3 markers + finalize (transaction 3)
350
+ *
351
+ * @returns true if this transition caused the job to complete
352
+ */
353
+ async executeLeg1StepProtocol(delta) {
354
+ const msgGuid = this.context.metadata.guid;
355
+ const threshold = this.mapStatusThreshold();
356
+ const { id: appId } = await this.engine.getVID();
357
+ //Step A: Save work + Leg1 completion marker
358
+ if (!collator_1.CollatorService.isGuidStep1Done(this.guidLedger)) {
359
+ const txn1 = this.store.transact();
360
+ await this.setState(txn1);
361
+ if (this.adjacentIndex === 0) {
362
+ //First entry: mark Leg1 complete. On cycle re-entry
363
+ //(adjacentIndex > 0), Leg1 is already complete and the
364
+ //Leg2 counter was already incremented by notarizeLeg2Entry.
365
+ await collator_1.CollatorService.notarizeLeg1Completion(this, txn1);
366
+ }
367
+ await collator_1.CollatorService.notarizeStep1(this, msgGuid, txn1);
368
+ await txn1.exec();
369
+ }
370
+ //Step B: Spawn children + semaphore + edge capture
371
+ let thresholdHit = false;
372
+ if (!collator_1.CollatorService.isGuidStep2Done(this.guidLedger)) {
373
+ const txn2 = this.store.transact();
374
+ await collator_1.CollatorService.notarizeStep2(this, msgGuid, txn2);
375
+ for (const child of this.adjacencyList) {
376
+ await this.engine.router?.publishMessage(null, child, txn2);
377
+ }
378
+ await this.store.setStatusAndCollateGuid(delta, threshold, this.context.metadata.jid, appId, msgGuid, collator_1.CollatorService.WEIGHTS.GUID_SNAPSHOT, txn2);
379
+ const results = (await txn2.exec());
380
+ thresholdHit = this.resolveThresholdHit(results);
381
+ this.logger.debug('leg1-step-protocol-stepB-complete', {
382
+ jid: this.context.metadata.jid,
383
+ aid: this.metadata.aid,
384
+ delta,
385
+ threshold,
386
+ thresholdHit,
387
+ lastResult: results[results.length - 1],
388
+ });
389
+ }
390
+ else {
391
+ thresholdHit = collator_1.CollatorService.isGuidJobClosed(this.guidLedger);
392
+ }
393
+ //Step C: Job completion tasks (edge hit OR emit/persist)
394
+ //When an activity marks the job done with an unrecoverable error
395
+ //(e.g., stopper after max retries), force completion even when the
396
+ //semaphore threshold isn't hit.
397
+ const isErrorTerminal = !thresholdHit
398
+ && this.context.data?.done === true
399
+ && !!this.context.data?.$error;
400
+ const needsCompletion = thresholdHit || this.shouldEmit() || this.shouldPersistJob() || isErrorTerminal;
401
+ if (needsCompletion && !collator_1.CollatorService.isGuidStep3Done(this.guidLedger)) {
402
+ const txn3 = this.store.transact();
403
+ const options = (thresholdHit || isErrorTerminal) ? {} : { emit: !this.shouldPersistJob() };
404
+ await this.engine.runJobCompletionTasks(this.context, options, txn3);
405
+ await collator_1.CollatorService.notarizeStep3(this, msgGuid, txn3);
406
+ await collator_1.CollatorService.notarizeFinalize(this, txn3);
407
+ await txn3.exec();
408
+ }
409
+ return thresholdHit;
410
+ }
207
411
  mapJobData() {
208
412
  if (this.config.job?.maps) {
209
413
  const mapper = new mapper_1.MapperService((0, utils_1.deepCopy)(this.config.job.maps), this.context);
@@ -517,6 +721,10 @@ class Activity {
517
721
  }
518
722
  return false;
519
723
  }
724
+ /**
725
+ * Transition method for Category C (Leg1-only, no children, no semaphore change)
726
+ * and Category D (Trigger) activities. NOT used by the Leg2 step protocol.
727
+ */
520
728
  async transition(adjacencyList, jobStatus) {
521
729
  if (this.jobWasInterrupted(jobStatus)) {
522
730
  return;
@@ -25,11 +25,11 @@ class Await extends activity_1.Activity {
25
25
  telemetry = new telemetry_1.TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
26
26
  telemetry.startActivitySpan(this.leg);
27
27
  this.mapInputData();
28
- //save state and authorize reentry
28
+ //save state and mark Leg1 complete
29
29
  const transaction = this.store.transact();
30
30
  //todo: await this.registerTimeout();
31
31
  const messageId = await this.execActivity(transaction);
32
- await collator_1.CollatorService.authorizeReentry(this, transaction);
32
+ await collator_1.CollatorService.notarizeLeg1Completion(this, transaction);
33
33
  await this.setState(transaction);
34
34
  await this.setStatus(0, transaction);
35
35
  const multiResponse = (await transaction.exec());
@@ -38,7 +38,7 @@ class Cycle extends activity_1.Activity {
38
38
  'app.job.jss': jobStatus,
39
39
  });
40
40
  //exit early (`Cycle` activities only execute Leg 1)
41
- await collator_1.CollatorService.notarizeEarlyExit(this, transaction);
41
+ await collator_1.CollatorService.notarizeLeg1Completion(this, transaction);
42
42
  (await transaction.exec());
43
43
  return this.context.metadata.aid;
44
44
  }
@@ -13,6 +13,11 @@ declare class Hook extends Activity {
13
13
  config: HookActivity;
14
14
  constructor(config: ActivityType, data: ActivityData, metadata: ActivityMetadata, hook: ActivityData | null, engine: EngineService, context?: JobState);
15
15
  process(): Promise<string>;
16
+ /**
17
+ * Static config check: does this activity have a hook or sleep config?
18
+ * Used for routing before context is loaded.
19
+ */
20
+ isConfiguredAsHook(): boolean;
16
21
  /**
17
22
  * does this activity use a time-hook or web-hook
18
23
  */