hungry-ghost-hive 0.44.0 → 0.46.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 (238) hide show
  1. package/dist/agents/base-agent.d.ts +1 -0
  2. package/dist/agents/base-agent.d.ts.map +1 -1
  3. package/dist/agents/base-agent.js +4 -0
  4. package/dist/agents/base-agent.js.map +1 -1
  5. package/dist/agents/intermediate.js +2 -2
  6. package/dist/agents/intermediate.js.map +1 -1
  7. package/dist/agents/junior.js +2 -2
  8. package/dist/agents/junior.js.map +1 -1
  9. package/dist/agents/qa.d.ts.map +1 -1
  10. package/dist/agents/qa.js +5 -5
  11. package/dist/agents/qa.js.map +1 -1
  12. package/dist/agents/senior.d.ts.map +1 -1
  13. package/dist/agents/senior.js +5 -5
  14. package/dist/agents/senior.js.map +1 -1
  15. package/dist/agents/tech-lead.d.ts.map +1 -1
  16. package/dist/agents/tech-lead.js +4 -2
  17. package/dist/agents/tech-lead.js.map +1 -1
  18. package/dist/cli/commands/assign.d.ts.map +1 -1
  19. package/dist/cli/commands/assign.js +4 -2
  20. package/dist/cli/commands/assign.js.map +1 -1
  21. package/dist/cli/commands/assign.test.js +5 -0
  22. package/dist/cli/commands/assign.test.js.map +1 -1
  23. package/dist/cli/commands/cluster.d.ts.map +1 -1
  24. package/dist/cli/commands/cluster.js +348 -1
  25. package/dist/cli/commands/cluster.js.map +1 -1
  26. package/dist/cli/commands/cluster.test.js +313 -9
  27. package/dist/cli/commands/cluster.test.js.map +1 -1
  28. package/dist/cli/commands/manager/handoff-recovery.d.ts.map +1 -1
  29. package/dist/cli/commands/manager/handoff-recovery.js +4 -2
  30. package/dist/cli/commands/manager/handoff-recovery.js.map +1 -1
  31. package/dist/cli/commands/manager/index.d.ts.map +1 -1
  32. package/dist/cli/commands/manager/index.js +16 -12
  33. package/dist/cli/commands/manager/index.js.map +1 -1
  34. package/dist/cli/commands/manager/tech-lead-lifecycle.d.ts.map +1 -1
  35. package/dist/cli/commands/manager/tech-lead-lifecycle.js +4 -2
  36. package/dist/cli/commands/manager/tech-lead-lifecycle.js.map +1 -1
  37. package/dist/cli/commands/msg.d.ts.map +1 -1
  38. package/dist/cli/commands/msg.js +8 -7
  39. package/dist/cli/commands/msg.js.map +1 -1
  40. package/dist/cli/commands/my-stories.js +3 -3
  41. package/dist/cli/commands/my-stories.js.map +1 -1
  42. package/dist/cli/commands/nuke.d.ts.map +1 -1
  43. package/dist/cli/commands/nuke.js +18 -7
  44. package/dist/cli/commands/nuke.js.map +1 -1
  45. package/dist/cli/commands/nuke.test.js +24 -0
  46. package/dist/cli/commands/nuke.test.js.map +1 -1
  47. package/dist/cli/commands/req-spawn.test.d.ts +2 -0
  48. package/dist/cli/commands/req-spawn.test.d.ts.map +1 -0
  49. package/dist/cli/commands/req-spawn.test.js +116 -0
  50. package/dist/cli/commands/req-spawn.test.js.map +1 -0
  51. package/dist/cli/commands/req.d.ts +1 -1
  52. package/dist/cli/commands/req.d.ts.map +1 -1
  53. package/dist/cli/commands/req.js +28 -18
  54. package/dist/cli/commands/req.js.map +1 -1
  55. package/dist/cli/commands/stories.js +3 -3
  56. package/dist/cli/commands/stories.js.map +1 -1
  57. package/dist/cli/dashboard/panels/agents.d.ts.map +1 -1
  58. package/dist/cli/dashboard/panels/agents.js +7 -3
  59. package/dist/cli/dashboard/panels/agents.js.map +1 -1
  60. package/dist/cluster/cluster-http-server.d.ts +32 -0
  61. package/dist/cluster/cluster-http-server.d.ts.map +1 -1
  62. package/dist/cluster/cluster-http-server.js +42 -0
  63. package/dist/cluster/cluster-http-server.js.map +1 -1
  64. package/dist/cluster/distributed-runtime-coverage.test.js +9 -0
  65. package/dist/cluster/distributed-runtime-coverage.test.js.map +1 -1
  66. package/dist/cluster/distributed-system.test.js +135 -0
  67. package/dist/cluster/distributed-system.test.js.map +1 -1
  68. package/dist/cluster/events.d.ts +23 -0
  69. package/dist/cluster/events.d.ts.map +1 -1
  70. package/dist/cluster/events.js +74 -0
  71. package/dist/cluster/events.js.map +1 -1
  72. package/dist/cluster/heartbeat-manager.d.ts +2 -0
  73. package/dist/cluster/heartbeat-manager.d.ts.map +1 -1
  74. package/dist/cluster/heartbeat-manager.js +42 -6
  75. package/dist/cluster/heartbeat-manager.js.map +1 -1
  76. package/dist/cluster/membership.test.d.ts +2 -0
  77. package/dist/cluster/membership.test.d.ts.map +1 -0
  78. package/dist/cluster/membership.test.js +416 -0
  79. package/dist/cluster/membership.test.js.map +1 -0
  80. package/dist/cluster/partition-safety.test.d.ts +2 -0
  81. package/dist/cluster/partition-safety.test.d.ts.map +1 -0
  82. package/dist/cluster/partition-safety.test.js +440 -0
  83. package/dist/cluster/partition-safety.test.js.map +1 -0
  84. package/dist/cluster/raft-state-machine.d.ts +33 -1
  85. package/dist/cluster/raft-state-machine.d.ts.map +1 -1
  86. package/dist/cluster/raft-state-machine.js +65 -3
  87. package/dist/cluster/raft-state-machine.js.map +1 -1
  88. package/dist/cluster/raft-store.d.ts +26 -1
  89. package/dist/cluster/raft-store.d.ts.map +1 -1
  90. package/dist/cluster/raft-store.js +137 -0
  91. package/dist/cluster/raft-store.js.map +1 -1
  92. package/dist/cluster/replication-lag.test.d.ts +2 -0
  93. package/dist/cluster/replication-lag.test.d.ts.map +1 -0
  94. package/dist/cluster/replication-lag.test.js +239 -0
  95. package/dist/cluster/replication-lag.test.js.map +1 -0
  96. package/dist/cluster/replication.d.ts +2 -2
  97. package/dist/cluster/replication.d.ts.map +1 -1
  98. package/dist/cluster/replication.js +1 -1
  99. package/dist/cluster/replication.js.map +1 -1
  100. package/dist/cluster/runtime.d.ts +78 -0
  101. package/dist/cluster/runtime.d.ts.map +1 -1
  102. package/dist/cluster/runtime.js +400 -13
  103. package/dist/cluster/runtime.js.map +1 -1
  104. package/dist/cluster/state-recovery.test.d.ts +2 -0
  105. package/dist/cluster/state-recovery.test.d.ts.map +1 -0
  106. package/dist/cluster/state-recovery.test.js +310 -0
  107. package/dist/cluster/state-recovery.test.js.map +1 -0
  108. package/dist/cluster/types.d.ts +30 -0
  109. package/dist/cluster/types.d.ts.map +1 -1
  110. package/dist/config/schema.d.ts +48 -0
  111. package/dist/config/schema.d.ts.map +1 -1
  112. package/dist/config/schema.js +11 -0
  113. package/dist/config/schema.js.map +1 -1
  114. package/dist/context-files/generator.d.ts +1 -1
  115. package/dist/context-files/generator.d.ts.map +1 -1
  116. package/dist/context-files/generator.js +4 -3
  117. package/dist/context-files/generator.js.map +1 -1
  118. package/dist/context-files/generator.test.js +51 -0
  119. package/dist/context-files/generator.test.js.map +1 -1
  120. package/dist/context-files/index.test.js +1 -0
  121. package/dist/context-files/index.test.js.map +1 -1
  122. package/dist/db/client.d.ts +1 -0
  123. package/dist/db/client.d.ts.map +1 -1
  124. package/dist/db/client.js +6 -0
  125. package/dist/db/client.js.map +1 -1
  126. package/dist/db/migrations/015-add-story-markdown-path.sql +5 -0
  127. package/dist/db/queries/stories.d.ts +3 -3
  128. package/dist/db/queries/stories.d.ts.map +1 -1
  129. package/dist/db/queries/stories.js +23 -5
  130. package/dist/db/queries/stories.js.map +1 -1
  131. package/dist/db/queries/test-helpers.d.ts.map +1 -1
  132. package/dist/db/queries/test-helpers.js +1 -0
  133. package/dist/db/queries/test-helpers.js.map +1 -1
  134. package/dist/git/worktree.d.ts.map +1 -1
  135. package/dist/git/worktree.js +7 -0
  136. package/dist/git/worktree.js.map +1 -1
  137. package/dist/git/worktree.test.js +30 -0
  138. package/dist/git/worktree.test.js.map +1 -1
  139. package/dist/orchestrator/orphan-recovery.d.ts +1 -1
  140. package/dist/orchestrator/orphan-recovery.d.ts.map +1 -1
  141. package/dist/orchestrator/orphan-recovery.js +4 -4
  142. package/dist/orchestrator/orphan-recovery.js.map +1 -1
  143. package/dist/orchestrator/prompt-templates.d.ts +6 -2
  144. package/dist/orchestrator/prompt-templates.d.ts.map +1 -1
  145. package/dist/orchestrator/prompt-templates.js +61 -16
  146. package/dist/orchestrator/prompt-templates.js.map +1 -1
  147. package/dist/orchestrator/prompt-templates.test.js +214 -0
  148. package/dist/orchestrator/prompt-templates.test.js.map +1 -1
  149. package/dist/orchestrator/scheduler.d.ts +1 -0
  150. package/dist/orchestrator/scheduler.d.ts.map +1 -1
  151. package/dist/orchestrator/scheduler.js +30 -17
  152. package/dist/orchestrator/scheduler.js.map +1 -1
  153. package/dist/orchestrator/scheduler.test.js +98 -6
  154. package/dist/orchestrator/scheduler.test.js.map +1 -1
  155. package/dist/tmux/manager.d.ts +7 -6
  156. package/dist/tmux/manager.d.ts.map +1 -1
  157. package/dist/tmux/manager.js +29 -13
  158. package/dist/tmux/manager.js.map +1 -1
  159. package/dist/utils/instance.d.ts +32 -0
  160. package/dist/utils/instance.d.ts.map +1 -0
  161. package/dist/utils/instance.js +82 -0
  162. package/dist/utils/instance.js.map +1 -0
  163. package/dist/utils/instance.test.d.ts +2 -0
  164. package/dist/utils/instance.test.d.ts.map +1 -0
  165. package/dist/utils/instance.test.js +103 -0
  166. package/dist/utils/instance.test.js.map +1 -0
  167. package/dist/utils/paths.d.ts +2 -0
  168. package/dist/utils/paths.d.ts.map +1 -1
  169. package/dist/utils/paths.js +2 -0
  170. package/dist/utils/paths.js.map +1 -1
  171. package/dist/utils/paths.test.js +6 -0
  172. package/dist/utils/paths.test.js.map +1 -1
  173. package/dist/utils/story-markdown.d.ts +16 -0
  174. package/dist/utils/story-markdown.d.ts.map +1 -0
  175. package/dist/utils/story-markdown.js +82 -0
  176. package/dist/utils/story-markdown.js.map +1 -0
  177. package/dist/utils/story-markdown.test.d.ts +2 -0
  178. package/dist/utils/story-markdown.test.d.ts.map +1 -0
  179. package/dist/utils/story-markdown.test.js +143 -0
  180. package/dist/utils/story-markdown.test.js.map +1 -0
  181. package/package.json +1 -1
  182. package/src/agents/base-agent.ts +5 -0
  183. package/src/agents/intermediate.ts +2 -2
  184. package/src/agents/junior.ts +2 -2
  185. package/src/agents/qa.ts +13 -8
  186. package/src/agents/senior.ts +21 -11
  187. package/src/agents/tech-lead.ts +24 -12
  188. package/src/cli/commands/assign.test.ts +5 -0
  189. package/src/cli/commands/assign.ts +4 -2
  190. package/src/cli/commands/cluster.test.ts +387 -9
  191. package/src/cli/commands/cluster.ts +486 -1
  192. package/src/cli/commands/manager/handoff-recovery.ts +4 -2
  193. package/src/cli/commands/manager/index.ts +16 -11
  194. package/src/cli/commands/manager/tech-lead-lifecycle.ts +5 -2
  195. package/src/cli/commands/msg.ts +8 -7
  196. package/src/cli/commands/my-stories.ts +22 -13
  197. package/src/cli/commands/nuke.test.ts +31 -0
  198. package/src/cli/commands/nuke.ts +18 -7
  199. package/src/cli/commands/req-spawn.test.ts +153 -0
  200. package/src/cli/commands/req.ts +40 -23
  201. package/src/cli/commands/stories.ts +22 -13
  202. package/src/cli/dashboard/panels/agents.ts +7 -3
  203. package/src/cluster/cluster-http-server.ts +80 -0
  204. package/src/cluster/distributed-runtime-coverage.test.ts +9 -0
  205. package/src/cluster/distributed-system.test.ts +168 -0
  206. package/src/cluster/events.ts +90 -0
  207. package/src/cluster/heartbeat-manager.ts +48 -6
  208. package/src/cluster/membership.test.ts +498 -0
  209. package/src/cluster/partition-safety.test.ts +523 -0
  210. package/src/cluster/raft-state-machine.ts +76 -4
  211. package/src/cluster/raft-store.ts +167 -1
  212. package/src/cluster/replication-lag.test.ts +284 -0
  213. package/src/cluster/replication.ts +6 -0
  214. package/src/cluster/runtime.ts +551 -12
  215. package/src/cluster/state-recovery.test.ts +420 -0
  216. package/src/cluster/types.ts +32 -0
  217. package/src/config/schema.ts +11 -0
  218. package/src/context-files/generator.test.ts +55 -0
  219. package/src/context-files/generator.ts +8 -7
  220. package/src/context-files/index.test.ts +1 -0
  221. package/src/db/client.ts +7 -0
  222. package/src/db/migrations/015-add-story-markdown-path.sql +5 -0
  223. package/src/db/queries/stories.ts +29 -5
  224. package/src/db/queries/test-helpers.ts +1 -0
  225. package/src/git/worktree.test.ts +43 -0
  226. package/src/git/worktree.ts +10 -0
  227. package/src/orchestrator/orphan-recovery.ts +32 -13
  228. package/src/orchestrator/prompt-templates.test.ts +267 -0
  229. package/src/orchestrator/prompt-templates.ts +69 -16
  230. package/src/orchestrator/scheduler.test.ts +130 -6
  231. package/src/orchestrator/scheduler.ts +66 -27
  232. package/src/tmux/manager.ts +42 -13
  233. package/src/utils/instance.test.ts +129 -0
  234. package/src/utils/instance.ts +95 -0
  235. package/src/utils/paths.test.ts +8 -0
  236. package/src/utils/paths.ts +3 -0
  237. package/src/utils/story-markdown.test.ts +176 -0
  238. package/src/utils/story-markdown.ts +94 -0
@@ -0,0 +1,440 @@
1
+ // Licensed under the Hungry Ghost Hive License. See LICENSE.
2
+ import { mkdirSync, mkdtempSync, rmSync } from 'fs';
3
+ import { createServer as createNetServer } from 'net';
4
+ import { tmpdir } from 'os';
5
+ import { join } from 'path';
6
+ import { afterEach, describe, expect, it } from 'vitest';
7
+ import { ClusterRuntime } from './runtime.js';
8
+ const tempRoots = [];
9
+ const activeRuntimes = [];
10
+ afterEach(async () => {
11
+ for (const runtime of activeRuntimes.splice(0)) {
12
+ try {
13
+ await runtime.stop();
14
+ }
15
+ catch {
16
+ // Best effort shutdown for test cleanup.
17
+ }
18
+ }
19
+ for (const root of tempRoots.splice(0)) {
20
+ rmSync(root, { recursive: true, force: true });
21
+ }
22
+ });
23
+ describe('fencing token validation', () => {
24
+ it('rejects heartbeats with fencing_token lower than term', async () => {
25
+ if (!(await canListenOnLocalhost()))
26
+ return;
27
+ const fixture = await startRuntimeFixture({
28
+ node_id: 'node-fence-reject',
29
+ election_timeout_min_ms: 2000,
30
+ election_timeout_max_ms: 2000,
31
+ });
32
+ // First, advance the term by accepting a vote request
33
+ await postJson(fixture.config.public_url, '/cluster/v1/election/request-vote', {
34
+ term: 5,
35
+ candidate_id: 'candidate-5',
36
+ });
37
+ // Send heartbeat with valid term but stale fencing token
38
+ const res = await postJson(fixture.config.public_url, '/cluster/v1/election/heartbeat', {
39
+ term: 5,
40
+ leader_id: 'candidate-5',
41
+ fencing_token: 3,
42
+ });
43
+ expect(res.success).toBe(false);
44
+ expect(res.fencing_token).toBe(5);
45
+ });
46
+ it('accepts heartbeats with valid fencing_token', async () => {
47
+ if (!(await canListenOnLocalhost()))
48
+ return;
49
+ const fixture = await startRuntimeFixture({
50
+ node_id: 'node-fence-accept',
51
+ election_timeout_min_ms: 2000,
52
+ election_timeout_max_ms: 2000,
53
+ });
54
+ const res = await postJson(fixture.config.public_url, '/cluster/v1/election/heartbeat', {
55
+ term: 3,
56
+ leader_id: 'leader-3',
57
+ fencing_token: 3,
58
+ });
59
+ expect(res.success).toBe(true);
60
+ expect(res.fencing_token).toBe(3);
61
+ });
62
+ it('rejects delta requests with stale fencing_token', async () => {
63
+ if (!(await canListenOnLocalhost()))
64
+ return;
65
+ const fixture = await startRuntimeFixture({
66
+ node_id: 'node-delta-fence',
67
+ election_timeout_min_ms: 2000,
68
+ election_timeout_max_ms: 2000,
69
+ });
70
+ // Advance term
71
+ await postJson(fixture.config.public_url, '/cluster/v1/election/heartbeat', {
72
+ term: 10,
73
+ leader_id: 'leader-10',
74
+ fencing_token: 10,
75
+ });
76
+ // Request delta with stale fencing token
77
+ const res = await fetch(`${fixture.config.public_url}/cluster/v1/events/delta`, {
78
+ method: 'POST',
79
+ headers: { 'Content-Type': 'application/json' },
80
+ body: JSON.stringify({
81
+ version_vector: {},
82
+ fencing_token: 5,
83
+ }),
84
+ });
85
+ expect(res.status).toBe(409);
86
+ const body = (await res.json());
87
+ expect(body.error).toContain('stale leader epoch');
88
+ expect(body.fencing_token).toBe(10);
89
+ });
90
+ it('accepts delta requests with current fencing_token', async () => {
91
+ if (!(await canListenOnLocalhost()))
92
+ return;
93
+ const fixture = await startRuntimeFixture({
94
+ node_id: 'node-delta-fence-ok',
95
+ election_timeout_min_ms: 2000,
96
+ election_timeout_max_ms: 2000,
97
+ });
98
+ // Set term to 4
99
+ await postJson(fixture.config.public_url, '/cluster/v1/election/heartbeat', {
100
+ term: 4,
101
+ leader_id: 'leader-4',
102
+ fencing_token: 4,
103
+ });
104
+ // Request delta with matching fencing token
105
+ const res = await fetch(`${fixture.config.public_url}/cluster/v1/events/delta`, {
106
+ method: 'POST',
107
+ headers: { 'Content-Type': 'application/json' },
108
+ body: JSON.stringify({
109
+ version_vector: {},
110
+ fencing_token: 4,
111
+ }),
112
+ });
113
+ expect(res.status).toBe(200);
114
+ const body = (await res.json());
115
+ expect(body.fencing_token).toBe(4);
116
+ });
117
+ it('accepts delta requests without fencing_token for backward compatibility', async () => {
118
+ if (!(await canListenOnLocalhost()))
119
+ return;
120
+ const fixture = await startRuntimeFixture({
121
+ node_id: 'node-delta-no-fence',
122
+ election_timeout_min_ms: 2000,
123
+ election_timeout_max_ms: 2000,
124
+ });
125
+ const res = await fetch(`${fixture.config.public_url}/cluster/v1/events/delta`, {
126
+ method: 'POST',
127
+ headers: { 'Content-Type': 'application/json' },
128
+ body: JSON.stringify({
129
+ version_vector: {},
130
+ }),
131
+ });
132
+ expect(res.status).toBe(200);
133
+ });
134
+ it('returns fencing_token in status endpoint', async () => {
135
+ if (!(await canListenOnLocalhost()))
136
+ return;
137
+ const fixture = await startRuntimeFixture({
138
+ node_id: 'node-status-fence',
139
+ election_timeout_min_ms: 2000,
140
+ election_timeout_max_ms: 2000,
141
+ });
142
+ // Advance term
143
+ await postJson(fixture.config.public_url, '/cluster/v1/election/heartbeat', {
144
+ term: 7,
145
+ leader_id: 'leader-7',
146
+ fencing_token: 7,
147
+ });
148
+ const status = fixture.runtime.getStatus();
149
+ expect(status.fencing_token).toBe(7);
150
+ expect(status.term).toBe(7);
151
+ });
152
+ });
153
+ describe('leader lease validation', () => {
154
+ it('reports lease invalid when no heartbeat has been received', async () => {
155
+ if (!(await canListenOnLocalhost()))
156
+ return;
157
+ const fixture = await startRuntimeFixture({
158
+ node_id: 'node-lease-none',
159
+ election_timeout_min_ms: 2000,
160
+ election_timeout_max_ms: 2000,
161
+ });
162
+ const status = fixture.runtime.getStatus();
163
+ expect(status.leader_lease_valid).toBe(false);
164
+ });
165
+ it('reports lease valid immediately after receiving heartbeat', async () => {
166
+ if (!(await canListenOnLocalhost()))
167
+ return;
168
+ const fixture = await startRuntimeFixture({
169
+ node_id: 'node-lease-fresh',
170
+ election_timeout_min_ms: 2000,
171
+ election_timeout_max_ms: 2000,
172
+ heartbeat_interval_ms: 100,
173
+ });
174
+ // Send a heartbeat
175
+ await postJson(fixture.config.public_url, '/cluster/v1/election/heartbeat', {
176
+ term: 2,
177
+ leader_id: 'leader-2',
178
+ fencing_token: 2,
179
+ });
180
+ const status = fixture.runtime.getStatus();
181
+ expect(status.leader_lease_valid).toBe(true);
182
+ });
183
+ it('leader always reports lease valid', async () => {
184
+ if (!(await canListenOnLocalhost()))
185
+ return;
186
+ const fixture = await startRuntimeFixture({
187
+ node_id: 'node-lease-leader',
188
+ election_timeout_min_ms: 80,
189
+ election_timeout_max_ms: 120,
190
+ heartbeat_interval_ms: 60,
191
+ });
192
+ await waitFor(() => fixture.runtime.getStatus().is_leader, 4000);
193
+ const status = fixture.runtime.getStatus();
194
+ expect(status.leader_lease_valid).toBe(true);
195
+ });
196
+ it('reports lease expired after timeout elapses without heartbeat', async () => {
197
+ if (!(await canListenOnLocalhost()))
198
+ return;
199
+ const leaseMs = 150;
200
+ const fixture = await startRuntimeFixture({
201
+ node_id: 'node-lease-expire',
202
+ election_timeout_min_ms: 5000,
203
+ election_timeout_max_ms: 5000,
204
+ heartbeat_interval_ms: 50,
205
+ leader_lease_ms: leaseMs,
206
+ });
207
+ // Send heartbeat to establish lease
208
+ await postJson(fixture.config.public_url, '/cluster/v1/election/heartbeat', {
209
+ term: 1,
210
+ leader_id: 'leader-1',
211
+ fencing_token: 1,
212
+ });
213
+ expect(fixture.runtime.getStatus().leader_lease_valid).toBe(true);
214
+ // Wait for lease to expire
215
+ await new Promise(resolve => setTimeout(resolve, leaseMs + 50));
216
+ expect(fixture.runtime.getStatus().leader_lease_valid).toBe(false);
217
+ });
218
+ it('reports correct leader_lease_duration_ms from config', async () => {
219
+ if (!(await canListenOnLocalhost()))
220
+ return;
221
+ const fixture = await startRuntimeFixture({
222
+ node_id: 'node-lease-config',
223
+ heartbeat_interval_ms: 200,
224
+ leader_lease_ms: 1000,
225
+ });
226
+ expect(fixture.runtime.getStatus().leader_lease_duration_ms).toBe(1000);
227
+ });
228
+ it('defaults leader_lease_duration_ms to 3x heartbeat_interval_ms', async () => {
229
+ if (!(await canListenOnLocalhost()))
230
+ return;
231
+ const fixture = await startRuntimeFixture({
232
+ node_id: 'node-lease-default',
233
+ heartbeat_interval_ms: 200,
234
+ });
235
+ expect(fixture.runtime.getStatus().leader_lease_duration_ms).toBe(600);
236
+ });
237
+ it('resets lease on step-down from higher term', async () => {
238
+ if (!(await canListenOnLocalhost()))
239
+ return;
240
+ const fixture = await startRuntimeFixture({
241
+ node_id: 'node-lease-stepdown',
242
+ election_timeout_min_ms: 5000,
243
+ election_timeout_max_ms: 5000,
244
+ heartbeat_interval_ms: 100,
245
+ });
246
+ // Establish lease at term 2
247
+ await postJson(fixture.config.public_url, '/cluster/v1/election/heartbeat', {
248
+ term: 2,
249
+ leader_id: 'leader-2',
250
+ fencing_token: 2,
251
+ });
252
+ expect(fixture.runtime.getStatus().leader_lease_valid).toBe(true);
253
+ // Higher term vote request causes step-down, which should reset lease
254
+ await postJson(fixture.config.public_url, '/cluster/v1/election/request-vote', {
255
+ term: 5,
256
+ candidate_id: 'candidate-5',
257
+ });
258
+ // Lease should be invalid after step-down (no heartbeat from new leader yet)
259
+ expect(fixture.runtime.getStatus().leader_lease_valid).toBe(false);
260
+ });
261
+ });
262
+ describe('partition healing scenarios', () => {
263
+ it('stale leader is fenced after partition heals', async () => {
264
+ if (!(await canListenOnLocalhost()))
265
+ return;
266
+ const portA = await getFreePort();
267
+ const portB = await getFreePort();
268
+ // Node A and B are peers
269
+ const configA = await buildConfig({
270
+ node_id: 'node-a-heal',
271
+ listen_port: portA,
272
+ public_url: `http://127.0.0.1:${portA}`,
273
+ peers: [{ id: 'node-b-heal', url: `http://127.0.0.1:${portB}` }],
274
+ election_timeout_min_ms: 80,
275
+ election_timeout_max_ms: 120,
276
+ heartbeat_interval_ms: 60,
277
+ });
278
+ const configB = await buildConfig({
279
+ node_id: 'node-b-heal',
280
+ listen_port: portB,
281
+ public_url: `http://127.0.0.1:${portB}`,
282
+ peers: [{ id: 'node-a-heal', url: `http://127.0.0.1:${portA}` }],
283
+ election_timeout_min_ms: 80,
284
+ election_timeout_max_ms: 120,
285
+ heartbeat_interval_ms: 60,
286
+ });
287
+ const fixtureA = await startRuntimeWithConfig(configA);
288
+ const fixtureB = await startRuntimeWithConfig(configB);
289
+ // Wait until at least one becomes leader
290
+ await waitFor(() => fixtureA.runtime.getStatus().is_leader || fixtureB.runtime.getStatus().is_leader, 4000);
291
+ const statusA = fixtureA.runtime.getStatus();
292
+ const statusB = fixtureB.runtime.getStatus();
293
+ // Exactly one should be leader (same term wins in a 2-node cluster)
294
+ const leaderCount = [statusA, statusB].filter(s => s.is_leader).length;
295
+ expect(leaderCount).toBeLessThanOrEqual(1);
296
+ // Both should have fencing tokens
297
+ expect(statusA.fencing_token).toBeGreaterThanOrEqual(0);
298
+ expect(statusB.fencing_token).toBeGreaterThanOrEqual(0);
299
+ });
300
+ it('follower rejects stale leader heartbeat after seeing higher term', async () => {
301
+ if (!(await canListenOnLocalhost()))
302
+ return;
303
+ const fixture = await startRuntimeFixture({
304
+ node_id: 'node-heal-reject',
305
+ election_timeout_min_ms: 5000,
306
+ election_timeout_max_ms: 5000,
307
+ });
308
+ // Node sees term 10 from new leader
309
+ await postJson(fixture.config.public_url, '/cluster/v1/election/heartbeat', {
310
+ term: 10,
311
+ leader_id: 'new-leader',
312
+ fencing_token: 10,
313
+ });
314
+ // Old leader (term 5) tries to send heartbeat after partition heals
315
+ const staleRes = await postJson(fixture.config.public_url, '/cluster/v1/election/heartbeat', {
316
+ term: 5,
317
+ leader_id: 'old-leader',
318
+ fencing_token: 5,
319
+ });
320
+ expect(staleRes.success).toBe(false);
321
+ expect(staleRes.fencing_token).toBe(10);
322
+ // Verify node still follows new leader
323
+ const status = fixture.runtime.getStatus();
324
+ expect(status.leader_id).toBe('new-leader');
325
+ expect(status.term).toBe(10);
326
+ });
327
+ });
328
+ // --- Test helpers ---
329
+ async function startRuntimeFixture(overrides = {}) {
330
+ const attempts = overrides.listen_port ? 1 : 5;
331
+ let lastError;
332
+ for (let i = 0; i < attempts; i++) {
333
+ const config = await buildConfig(overrides);
334
+ try {
335
+ return await startRuntimeWithConfig(config);
336
+ }
337
+ catch (error) {
338
+ lastError = error;
339
+ const err = error;
340
+ if (!overrides.listen_port && err.code === 'EADDRINUSE') {
341
+ continue;
342
+ }
343
+ throw error;
344
+ }
345
+ }
346
+ throw lastError instanceof Error ? lastError : new Error('Failed to start runtime fixture');
347
+ }
348
+ async function startRuntimeWithConfig(config) {
349
+ const root = mkdtempSync(join(tmpdir(), `hive-partition-safety-${config.node_id}-`));
350
+ const hiveDir = join(root, '.hive');
351
+ mkdirSync(hiveDir, { recursive: true });
352
+ const runtime = new ClusterRuntime(config, { hiveDir });
353
+ try {
354
+ await runtime.start();
355
+ activeRuntimes.push(runtime);
356
+ tempRoots.push(root);
357
+ return { root, hiveDir, config, runtime };
358
+ }
359
+ catch (error) {
360
+ try {
361
+ await runtime.stop();
362
+ }
363
+ catch {
364
+ // Best effort cleanup for partial starts.
365
+ }
366
+ rmSync(root, { recursive: true, force: true });
367
+ throw error;
368
+ }
369
+ }
370
+ async function buildConfig(overrides = {}) {
371
+ const port = overrides.listen_port ?? (await getFreePort());
372
+ const base = {
373
+ enabled: true,
374
+ node_id: 'node-test',
375
+ listen_host: '127.0.0.1',
376
+ listen_port: port,
377
+ public_url: `http://127.0.0.1:${port}`,
378
+ peers: [],
379
+ heartbeat_interval_ms: 100,
380
+ election_timeout_min_ms: 150,
381
+ election_timeout_max_ms: 250,
382
+ sync_interval_ms: 200,
383
+ request_timeout_ms: 600,
384
+ story_similarity_threshold: 0.8,
385
+ };
386
+ return {
387
+ ...base,
388
+ ...overrides,
389
+ public_url: overrides.public_url || base.public_url,
390
+ peers: overrides.peers || base.peers,
391
+ };
392
+ }
393
+ async function postJson(baseUrl, path, body) {
394
+ const res = await fetch(`${baseUrl}${path}`, {
395
+ method: 'POST',
396
+ headers: { 'Content-Type': 'application/json' },
397
+ body: JSON.stringify(body),
398
+ });
399
+ return (await res.json());
400
+ }
401
+ async function waitFor(predicate, timeoutMs) {
402
+ const start = Date.now();
403
+ while (Date.now() - start < timeoutMs) {
404
+ if (predicate())
405
+ return;
406
+ await new Promise(resolve => setTimeout(resolve, 25));
407
+ }
408
+ throw new Error('Timed out waiting for condition');
409
+ }
410
+ async function getFreePort() {
411
+ return new Promise((resolve, reject) => {
412
+ const server = createNetServer();
413
+ server.once('error', reject);
414
+ server.listen(0, '127.0.0.1', () => {
415
+ const address = server.address();
416
+ if (!address || typeof address === 'string') {
417
+ server.close(() => reject(new Error('Failed to allocate free port')));
418
+ return;
419
+ }
420
+ const port = address.port;
421
+ server.close(err => {
422
+ if (err) {
423
+ reject(err);
424
+ return;
425
+ }
426
+ resolve(port);
427
+ });
428
+ });
429
+ });
430
+ }
431
+ async function canListenOnLocalhost() {
432
+ try {
433
+ await getFreePort();
434
+ return true;
435
+ }
436
+ catch {
437
+ return false;
438
+ }
439
+ }
440
+ //# sourceMappingURL=partition-safety.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"partition-safety.test.js","sourceRoot":"","sources":["../../src/cluster/partition-safety.test.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import type { ClusterConfig, ClusterPeerConfig } from '../config/schema.js';
2
- import type { DurableLogEntryType } from './raft-store.js';
2
+ import type { CompactionResult, DurableLogEntryType } from './raft-store.js';
3
3
  import { RaftMetadataStore } from './raft-store.js';
4
+ import type { VersionVector } from './types.js';
4
5
  type NodeRole = 'leader' | 'follower' | 'candidate';
5
6
  interface VoteResponse {
6
7
  term: number;
@@ -19,11 +20,40 @@ export declare class RaftStateMachine {
19
20
  currentTerm: number;
20
21
  votedFor: string | null;
21
22
  leaderId: string | null;
23
+ lastHeartbeatReceivedAt: number;
24
+ /**
25
+ * When true, this node is catching up from a snapshot and must not
26
+ * participate in leader elections until fully recovered.
27
+ */
28
+ isCatchingUp: boolean;
29
+ /** Dynamic peer list that can be updated at runtime via membership changes. */
30
+ private dynamicPeers;
22
31
  private electionDeadline;
23
32
  private electionInFlight;
24
33
  private electionTimer;
25
34
  private raftStore;
26
35
  constructor(config: ClusterConfig, deps: RaftStateMachineDeps);
36
+ /** Returns the active peer list (dynamic if set, otherwise static config). */
37
+ getPeers(): ClusterPeerConfig[];
38
+ /** Replaces the dynamic peer list. */
39
+ setPeers(peers: ClusterPeerConfig[]): void;
40
+ /** Returns the leader lease window in milliseconds. */
41
+ get leaderLeaseDurationMs(): number;
42
+ /**
43
+ * Returns true when this follower has received a valid heartbeat
44
+ * from the current leader within the lease window.
45
+ */
46
+ isLeaderLeaseValid(): boolean;
47
+ /**
48
+ * The fencing token is the current Raft term. Operations tagged with a
49
+ * lower term than ours must be rejected to prevent stale-leader writes.
50
+ */
51
+ getFencingToken(): number;
52
+ /**
53
+ * Validates a fencing token from a remote node. Returns true when the
54
+ * token is at least as recent as our current term.
55
+ */
56
+ validateFencingToken(token: number): boolean;
27
57
  initializeRaftStore(hiveDir: string): void;
28
58
  getRaftStore(): RaftMetadataStore | null;
29
59
  clearRaftStore(): void;
@@ -42,6 +72,8 @@ export declare class RaftStateMachine {
42
72
  last_applied: number;
43
73
  last_log_index: number;
44
74
  } | null;
75
+ getLogEntryCount(): number;
76
+ createSnapshotAndCompact(versionVector: VersionVector): CompactionResult;
45
77
  getLeaderUrl(): string | null;
46
78
  }
47
79
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"raft-state-machine.d.ts","sourceRoot":"","sources":["../../src/cluster/raft-state-machine.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC5E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEpD,KAAK,QAAQ,GAAG,QAAQ,GAAG,UAAU,GAAG,WAAW,CAAC;AAOpD,UAAU,YAAY;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,OAAO,CAAC;IACtB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACzF,QAAQ,EAAE,MAAM,OAAO,CAAC;IACxB,qBAAqB,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACjD;AAED,qBAAa,gBAAgB;IAYzB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAZvB,IAAI,EAAE,QAAQ,CAAc;IAC5B,WAAW,SAAK;IAChB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IAC/B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IAE/B,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,aAAa,CAA+B;IACpD,OAAO,CAAC,SAAS,CAAkC;gBAGhC,MAAM,EAAE,aAAa,EACrB,IAAI,EAAE,oBAAoB;IAG7C,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAgB1C,YAAY,IAAI,iBAAiB,GAAG,IAAI;IAIxC,cAAc,IAAI,IAAI;IAItB,iBAAiB,IAAI,IAAI;IAYzB,gBAAgB,IAAI,IAAI;IAQlB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAuEpC,iBAAiB,CAAC,IAAI,EAAE,OAAO,GAAG,YAAY;IAgC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAmBrD,MAAM,IAAI,MAAM;IAKhB,qBAAqB,IAAI,IAAI;IAQ7B,gBAAgB,IAAI,IAAI;IAUxB,kBAAkB,CAAC,IAAI,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAYtF,+BAA+B,CAAC,MAAM,EAAE,OAAO,kBAAkB,EAAE,YAAY,EAAE,GAAG,MAAM;IAQ1F,iBAAiB,IAAI;QACnB,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,EAAE,MAAM,CAAC;QACrB,cAAc,EAAE,MAAM,CAAC;KACxB,GAAG,IAAI;IAIR,YAAY,IAAI,MAAM,GAAG,IAAI;CAO9B"}
1
+ {"version":3,"file":"raft-state-machine.d.ts","sourceRoot":"","sources":["../../src/cluster/raft-state-machine.ts"],"names":[],"mappings":"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"}
@@ -8,6 +8,14 @@ export class RaftStateMachine {
8
8
  currentTerm = 0;
9
9
  votedFor = null;
10
10
  leaderId = null;
11
+ lastHeartbeatReceivedAt = 0;
12
+ /**
13
+ * When true, this node is catching up from a snapshot and must not
14
+ * participate in leader elections until fully recovered.
15
+ */
16
+ isCatchingUp = false;
17
+ /** Dynamic peer list that can be updated at runtime via membership changes. */
18
+ dynamicPeers = null;
11
19
  electionDeadline = 0;
12
20
  electionInFlight = false;
13
21
  electionTimer = null;
@@ -16,6 +24,43 @@ export class RaftStateMachine {
16
24
  this.config = config;
17
25
  this.deps = deps;
18
26
  }
27
+ /** Returns the active peer list (dynamic if set, otherwise static config). */
28
+ getPeers() {
29
+ return this.dynamicPeers ?? this.config.peers;
30
+ }
31
+ /** Replaces the dynamic peer list. */
32
+ setPeers(peers) {
33
+ this.dynamicPeers = peers;
34
+ }
35
+ /** Returns the leader lease window in milliseconds. */
36
+ get leaderLeaseDurationMs() {
37
+ return this.config.leader_lease_ms ?? this.config.heartbeat_interval_ms * 3;
38
+ }
39
+ /**
40
+ * Returns true when this follower has received a valid heartbeat
41
+ * from the current leader within the lease window.
42
+ */
43
+ isLeaderLeaseValid() {
44
+ if (this.role === 'leader')
45
+ return true;
46
+ if (this.lastHeartbeatReceivedAt === 0)
47
+ return false;
48
+ return Date.now() - this.lastHeartbeatReceivedAt < this.leaderLeaseDurationMs;
49
+ }
50
+ /**
51
+ * The fencing token is the current Raft term. Operations tagged with a
52
+ * lower term than ours must be rejected to prevent stale-leader writes.
53
+ */
54
+ getFencingToken() {
55
+ return this.currentTerm;
56
+ }
57
+ /**
58
+ * Validates a fencing token from a remote node. Returns true when the
59
+ * token is at least as recent as our current term.
60
+ */
61
+ validateFencingToken(token) {
62
+ return token >= this.currentTerm;
63
+ }
19
64
  initializeRaftStore(hiveDir) {
20
65
  if (this.raftStore)
21
66
  return;
@@ -43,6 +88,12 @@ export class RaftStateMachine {
43
88
  return;
44
89
  if (this.role === 'leader')
45
90
  return;
91
+ // Do not start elections while catching up from a snapshot — the node
92
+ // must not become leader until it has a complete, current state.
93
+ if (this.isCatchingUp) {
94
+ this.resetElectionDeadline();
95
+ return;
96
+ }
46
97
  if (Date.now() >= this.electionDeadline) {
47
98
  void this.startElection().catch(error => this.deps.handleBackgroundError(error));
48
99
  }
@@ -72,7 +123,7 @@ export class RaftStateMachine {
72
123
  });
73
124
  let votes = 1;
74
125
  try {
75
- await Promise.all(this.config.peers
126
+ await Promise.all(this.getPeers()
76
127
  .filter(peer => peer.id !== this.config.node_id)
77
128
  .map(async (peer) => {
78
129
  const response = await this.deps.postJson(peer, '/cluster/v1/election/request-vote', {
@@ -143,6 +194,7 @@ export class RaftStateMachine {
143
194
  this.role = 'follower';
144
195
  this.votedFor = null;
145
196
  this.leaderId = leaderId;
197
+ this.lastHeartbeatReceivedAt = 0;
146
198
  this.resetElectionDeadline();
147
199
  this.persistRaftState();
148
200
  this.appendDurableEntry('state_transition', {
@@ -153,7 +205,7 @@ export class RaftStateMachine {
153
205
  });
154
206
  }
155
207
  quorum() {
156
- const nodes = this.config.peers.length + 1;
208
+ const nodes = this.getPeers().length + 1;
157
209
  return Math.floor(nodes / 2) + 1;
158
210
  }
159
211
  resetElectionDeadline() {
@@ -195,12 +247,22 @@ export class RaftStateMachine {
195
247
  getRaftStoreState() {
196
248
  return this.raftStore?.getState() ?? null;
197
249
  }
250
+ getLogEntryCount() {
251
+ return this.raftStore?.getLogEntryCount() ?? 0;
252
+ }
253
+ createSnapshotAndCompact(versionVector) {
254
+ if (!this.raftStore) {
255
+ return { entries_removed: 0, entries_retained: 0, snapshot_index: 0 };
256
+ }
257
+ this.raftStore.createSnapshot(versionVector);
258
+ return this.raftStore.compactLog();
259
+ }
198
260
  getLeaderUrl() {
199
261
  if (!this.leaderId)
200
262
  return null;
201
263
  if (this.leaderId === this.config.node_id)
202
264
  return this.config.public_url;
203
- const peer = this.config.peers.find(item => item.id === this.leaderId);
265
+ const peer = this.getPeers().find(item => item.id === this.leaderId);
204
266
  return peer?.url || null;
205
267
  }
206
268
  }