@boxxy/dispatch 0.1.1 → 0.1.3

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 (190) hide show
  1. package/dist/attempts/attempt-machine.d.ts +804 -0
  2. package/dist/attempts/attempt-moves.d.ts +2 -0
  3. package/dist/attempts/bind-attempt-session.d.ts +14 -0
  4. package/dist/attempts/build-attempt-change.d.ts +73 -0
  5. package/dist/attempts/build-attempt-fact.d.ts +3 -0
  6. package/dist/attempts/end-item-attempts.d.ts +18 -0
  7. package/dist/attempts/is-same-guard.d.ts +5 -0
  8. package/dist/attempts/pick-call-attempt.d.ts +10 -0
  9. package/dist/attempts/stop-stray-session.d.ts +11 -0
  10. package/dist/attempts/types.d.ts +142 -0
  11. package/dist/attempts/write-attempt-move.d.ts +7 -0
  12. package/dist/attempts/write-session-bind.d.ts +3 -0
  13. package/dist/claims/build-snapshot-hash.d.ts +2 -0
  14. package/dist/claims/claim-next.d.ts +6 -0
  15. package/dist/claims/pick-kept-session.d.ts +9 -0
  16. package/dist/claims/request-wake.d.ts +3 -0
  17. package/dist/claims/resume-claim.d.ts +3 -0
  18. package/dist/claims/start-claim.d.ts +3 -0
  19. package/dist/claims/types.d.ts +44 -0
  20. package/dist/closeouts/build-settle-plans.d.ts +17 -0
  21. package/dist/closeouts/close-out-attempt.d.ts +3 -0
  22. package/dist/closeouts/effect-handlers.d.ts +2 -0
  23. package/dist/closeouts/pick-settlement.d.ts +14 -0
  24. package/dist/closeouts/settle-attempt.d.ts +3 -0
  25. package/dist/closeouts/types.d.ts +50 -0
  26. package/dist/index.d.ts +38 -0
  27. package/dist/notices/constants.d.ts +4 -0
  28. package/dist/notices/create-null-notifier.d.ts +2 -0
  29. package/dist/notices/send-notices.d.ts +2 -0
  30. package/dist/notices/types.d.ts +11 -0
  31. package/dist/outbox/build-backoff-ms.d.ts +2 -0
  32. package/dist/outbox/build-initial-sender-state.d.ts +2 -0
  33. package/dist/outbox/build-outbox-status.d.ts +15 -0
  34. package/dist/outbox/constants.d.ts +2 -0
  35. package/dist/outbox/measure-wire-bytes.d.ts +2 -0
  36. package/dist/outbox/pick-sender-move.d.ts +10 -0
  37. package/dist/outbox/run-sender-once.d.ts +2 -0
  38. package/dist/outbox/take-batch.d.ts +3 -0
  39. package/dist/outbox/types.d.ts +61 -0
  40. package/dist/pull-requests/build-link-change.d.ts +11 -0
  41. package/dist/pull-requests/build-state-fact.d.ts +8 -0
  42. package/dist/pull-requests/collect-attempt-trailers.d.ts +2 -0
  43. package/dist/pull-requests/collect-seen-state.d.ts +7 -0
  44. package/dist/pull-requests/constants.d.ts +3 -0
  45. package/dist/pull-requests/link-pull-request.d.ts +2 -0
  46. package/dist/pull-requests/parse-pull-request-url.d.ts +2 -0
  47. package/dist/pull-requests/types.d.ts +37 -0
  48. package/dist/pull-requests/watch-pull-requests.d.ts +2 -0
  49. package/dist/readings/act-on-reading.d.ts +3 -0
  50. package/dist/readings/find-waiting-since.d.ts +9 -0
  51. package/dist/readings/pick-reading-action.d.ts +2 -0
  52. package/dist/readings/types.d.ts +79 -0
  53. package/dist/sessions/build-keep-writes.d.ts +9 -0
  54. package/dist/sessions/build-kept-clear.d.ts +2 -0
  55. package/dist/sessions/forget-sessions.d.ts +2 -0
  56. package/dist/sessions/is-kept-due.d.ts +2 -0
  57. package/dist/sessions/resume-kept-session.d.ts +6 -0
  58. package/dist/sessions/types.d.ts +19 -0
  59. package/dist/test-utils/apply-memory-change.d.ts +3 -0
  60. package/dist/test-utils/build-memory-reader.d.ts +3 -0
  61. package/dist/test-utils/build-mock-claim-change.d.ts +2 -2
  62. package/dist/test-utils/build-mock-new-id.d.ts +2 -0
  63. package/dist/test-utils/build-mock-note-change.d.ts +2 -0
  64. package/dist/test-utils/build-mock-pull-request-change.d.ts +1 -0
  65. package/dist/test-utils/{build-mock-link-change.d.ts → build-mock-relation-change.d.ts} +2 -2
  66. package/dist/test-utils/build-mock-running-reading.d.ts +6 -0
  67. package/dist/test-utils/build-mock-start-change.d.ts +2 -2
  68. package/dist/test-utils/claim-store-close-cases.d.ts +2 -0
  69. package/dist/test-utils/claim-store-copy-cases.d.ts +2 -0
  70. package/dist/test-utils/claim-store-outbox-cases.d.ts +2 -0
  71. package/dist/test-utils/claim-store-race-cases.d.ts +2 -0
  72. package/dist/test-utils/claim-store-seat-cases.d.ts +2 -0
  73. package/dist/test-utils/claim-store-session-cases.d.ts +2 -0
  74. package/dist/test-utils/collect-memory-limit-fills.d.ts +3 -0
  75. package/dist/test-utils/compare-text.d.ts +1 -0
  76. package/dist/test-utils/create-fake-backoff.d.ts +2 -0
  77. package/dist/test-utils/create-fake-clock.d.ts +5 -0
  78. package/dist/test-utils/create-fake-fact-sink.d.ts +8 -0
  79. package/dist/test-utils/create-fake-forge.d.ts +17 -0
  80. package/dist/test-utils/create-fake-worker.d.ts +42 -0
  81. package/dist/test-utils/create-memory-claim-store.d.ts +2 -2
  82. package/dist/test-utils/create-test-dispatch.d.ts +46 -0
  83. package/dist/test-utils/forge-contract.d.ts +15 -0
  84. package/dist/test-utils/is-same-held-seat.d.ts +2 -0
  85. package/dist/test-utils/types.d.ts +12 -2
  86. package/dist/test-utils/worker-contract.d.ts +14 -0
  87. package/dist/timeouts.d.ts +2 -0
  88. package/dist/types.d.ts +13 -0
  89. package/package.json +10 -3
  90. package/src/attempts/attempt-machine.ts +147 -0
  91. package/src/attempts/attempt-moves.ts +218 -0
  92. package/src/attempts/bind-attempt-session.ts +61 -0
  93. package/src/attempts/build-attempt-change.ts +540 -0
  94. package/src/attempts/build-attempt-fact.ts +135 -0
  95. package/src/attempts/end-item-attempts.ts +95 -0
  96. package/src/attempts/is-same-guard.ts +22 -0
  97. package/src/attempts/pick-call-attempt.ts +32 -0
  98. package/src/attempts/stop-stray-session.ts +48 -0
  99. package/src/attempts/types.ts +231 -0
  100. package/src/attempts/write-attempt-move.ts +41 -0
  101. package/src/attempts/write-session-bind.ts +47 -0
  102. package/src/claims/build-snapshot-hash.ts +9 -0
  103. package/src/claims/claim-next.ts +306 -0
  104. package/src/claims/pick-kept-session.ts +41 -0
  105. package/src/claims/request-wake.ts +17 -0
  106. package/src/claims/resume-claim.ts +56 -0
  107. package/src/claims/start-claim.ts +99 -0
  108. package/src/claims/types.ts +67 -0
  109. package/src/closeouts/build-settle-plans.ts +173 -0
  110. package/src/closeouts/close-out-attempt.ts +156 -0
  111. package/src/closeouts/effect-handlers.ts +232 -0
  112. package/src/closeouts/pick-settlement.ts +80 -0
  113. package/src/closeouts/settle-attempt.ts +161 -0
  114. package/src/closeouts/types.ts +87 -0
  115. package/src/index.ts +61 -0
  116. package/src/notices/constants.ts +7 -0
  117. package/src/notices/create-null-notifier.ts +6 -0
  118. package/src/notices/send-notices.ts +41 -0
  119. package/src/notices/types.ts +14 -0
  120. package/src/outbox/build-backoff-ms.ts +14 -0
  121. package/src/outbox/build-initial-sender-state.ts +14 -0
  122. package/src/outbox/build-outbox-status.ts +43 -0
  123. package/src/outbox/constants.ts +15 -0
  124. package/src/outbox/measure-wire-bytes.ts +31 -0
  125. package/src/outbox/pick-sender-move.ts +37 -0
  126. package/src/outbox/run-sender-once.ts +174 -0
  127. package/src/outbox/take-batch.ts +35 -0
  128. package/src/outbox/types.ts +76 -0
  129. package/src/pull-requests/build-link-change.ts +95 -0
  130. package/src/pull-requests/build-state-fact.ts +39 -0
  131. package/src/pull-requests/collect-attempt-trailers.ts +20 -0
  132. package/src/pull-requests/collect-seen-state.ts +19 -0
  133. package/src/pull-requests/constants.ts +5 -0
  134. package/src/pull-requests/link-pull-request.ts +119 -0
  135. package/src/pull-requests/parse-pull-request-url.ts +30 -0
  136. package/src/pull-requests/types.ts +63 -0
  137. package/src/pull-requests/watch-pull-requests.ts +308 -0
  138. package/src/readings/act-on-reading.ts +318 -0
  139. package/src/readings/find-waiting-since.ts +46 -0
  140. package/src/readings/pick-reading-action.ts +315 -0
  141. package/src/readings/types.ts +98 -0
  142. package/src/sessions/build-keep-writes.ts +55 -0
  143. package/src/sessions/build-kept-clear.ts +12 -0
  144. package/src/sessions/forget-sessions.ts +106 -0
  145. package/src/sessions/is-kept-due.ts +27 -0
  146. package/src/sessions/resume-kept-session.ts +25 -0
  147. package/src/sessions/types.ts +39 -0
  148. package/src/test-utils/apply-memory-change.ts +252 -0
  149. package/src/test-utils/build-memory-reader.ts +364 -0
  150. package/src/test-utils/build-mock-bind-change.ts +18 -11
  151. package/src/test-utils/build-mock-claim-change.ts +18 -8
  152. package/src/test-utils/build-mock-file-item-change.ts +3 -1
  153. package/src/test-utils/build-mock-new-id.ts +12 -0
  154. package/src/test-utils/build-mock-note-change.ts +18 -0
  155. package/src/test-utils/build-mock-pause-change.ts +2 -10
  156. package/src/test-utils/build-mock-pull-request-change.ts +19 -9
  157. package/src/test-utils/{build-mock-link-change.ts → build-mock-relation-change.ts} +6 -6
  158. package/src/test-utils/build-mock-running-reading.ts +19 -0
  159. package/src/test-utils/build-mock-settle-change.ts +18 -6
  160. package/src/test-utils/build-mock-start-change.ts +13 -6
  161. package/src/test-utils/claim-store-attempt-reader-cases.ts +163 -133
  162. package/src/test-utils/claim-store-claim-cases.ts +168 -85
  163. package/src/test-utils/claim-store-close-cases.ts +95 -0
  164. package/src/test-utils/claim-store-contract.ts +12 -2
  165. package/src/test-utils/claim-store-copy-cases.ts +100 -0
  166. package/src/test-utils/claim-store-exclusive-cases.ts +65 -19
  167. package/src/test-utils/claim-store-guard-cases.ts +92 -82
  168. package/src/test-utils/claim-store-item-reader-cases.ts +291 -77
  169. package/src/test-utils/claim-store-outbox-cases.ts +281 -0
  170. package/src/test-utils/claim-store-race-cases.ts +150 -0
  171. package/src/test-utils/claim-store-rollback-cases.ts +50 -50
  172. package/src/test-utils/claim-store-seat-cases.ts +270 -0
  173. package/src/test-utils/claim-store-session-cases.ts +289 -0
  174. package/src/test-utils/collect-memory-limit-fills.ts +15 -0
  175. package/src/test-utils/compare-text.ts +8 -0
  176. package/src/test-utils/create-fake-backoff.ts +16 -0
  177. package/src/test-utils/create-fake-clock.ts +20 -0
  178. package/src/test-utils/create-fake-fact-sink.ts +74 -0
  179. package/src/test-utils/create-fake-forge.ts +117 -0
  180. package/src/test-utils/create-fake-worker.ts +303 -0
  181. package/src/test-utils/create-memory-claim-store.ts +73 -484
  182. package/src/test-utils/create-test-dispatch.ts +597 -0
  183. package/src/test-utils/forge-contract.ts +171 -0
  184. package/src/test-utils/is-same-held-seat.ts +11 -0
  185. package/src/test-utils/types.ts +14 -2
  186. package/src/test-utils/worker-contract.ts +192 -0
  187. package/src/timeouts.ts +17 -0
  188. package/src/types.ts +32 -0
  189. package/dist/test-utils/claim-store-pool-cases.d.ts +0 -2
  190. package/src/test-utils/claim-store-pool-cases.ts +0 -178
@@ -1,37 +1,22 @@
1
- import { isDeepStrictEqual } from 'node:util';
2
- import { buildAppliedChange, buildItemViews, findSlotTake, toJSONValue } from '@boxxy/model';
3
- import type {
4
- ApplyResult,
5
- AttemptRow,
6
- ClaimStore,
7
- EvidenceRow,
8
- Instant,
9
- ItemSetRows,
10
- ItemView,
11
- ItemWithLevers,
12
- LinkRow,
13
- MaxAttemptsByKind,
14
- OutboxFact,
15
- PoolName,
16
- PullRequestRow,
17
- RowWrite,
18
- SlotTake,
19
- StoreChange,
20
- StoreReader,
21
- StoreTx,
22
- TableName,
23
- Tables,
24
- TakenSlot,
25
- } from '@boxxy/model';
26
- import invariant from 'tiny-invariant';
27
- import { match } from 'ts-pattern';
28
-
29
- export function createMemoryClaimStore(): ClaimStore {
1
+ import { collectSeatSync } from '@boxxy/model';
2
+ import type { StoreTx } from '@boxxy/model';
3
+ import { applyMemoryChange } from './apply-memory-change.ts';
4
+ import { buildMemoryReader } from './build-memory-reader.ts';
5
+ import { isSameHeldSeat } from './is-same-held-seat.ts';
6
+ import type { ClosableClaimStore, MemoryTables } from './types.ts';
7
+
8
+ export function createMemoryClaimStore(): ClosableClaimStore {
30
9
  let committed = buildEmptyTables();
31
10
  let turn: Promise<void> = Promise.resolve();
11
+ let isClosed = false;
12
+ let reads: Promise<unknown> = Promise.resolve();
32
13
 
33
14
  return {
34
15
  exclusive: async (work) => {
16
+ if (isClosed) {
17
+ throw new Error('the store is closed');
18
+ }
19
+
35
20
  const previous = turn;
36
21
  const next = Promise.withResolvers<void>();
37
22
 
@@ -54,27 +39,43 @@ export function createMemoryClaimStore(): ClaimStore {
54
39
  read: (work) => {
55
40
  const snapshot = committed;
56
41
 
57
- return work(buildMemoryReader(() => snapshot));
42
+ const reading = Promise.try(() => {
43
+ if (isClosed) {
44
+ throw new Error('the store is closed');
45
+ }
46
+
47
+ return work(buildMemoryReader(() => snapshot));
48
+ });
49
+
50
+ reads = Promise.allSettled([reads, reading]);
51
+
52
+ return reading;
53
+ },
54
+ close: async () => {
55
+ isClosed = true;
56
+
57
+ await Promise.all([reads, turn]);
58
58
  },
59
59
  };
60
60
  }
61
61
 
62
- type MemoryTables = { [T in TableName]: Tables[T][] } & { outbox: OutboxFact[] };
63
-
64
62
  function buildEmptyTables(): MemoryTables {
65
63
  return {
66
64
  items: [],
67
- links: [],
65
+ relations: [],
68
66
  levers: [],
69
67
  attempts: [],
70
68
  attempt_sessions: [],
71
69
  leases: [],
72
- pool_slots: [],
73
70
  decisions: [],
74
71
  evidence: [],
75
72
  pull_requests: [],
76
73
  runners: [],
74
+ notes: [],
75
+ notices: [],
77
76
  outbox: [],
77
+ limits: [],
78
+ limit_seats: [],
78
79
  };
79
80
  }
80
81
 
@@ -84,13 +85,31 @@ function buildMemoryTx(tables: MemoryTables): StoreTx {
84
85
  applyChange: (change) => Promise.try(() => applyMemoryChange(tables, change)),
85
86
  markFactsSent: (ids, at) => {
86
87
  for (const fact of tables.outbox) {
87
- if (ids.includes(fact.factId) && fact.sentAt === null) {
88
+ if (ids.includes(fact.factId) && fact.sentAt === null && fact.parkedAt === null) {
88
89
  Object.assign(fact, { sentAt: at });
89
90
  }
90
91
  }
91
92
 
92
93
  return Promise.resolve();
93
94
  },
95
+ parkFacts: (ids, reason, at) => {
96
+ for (const fact of tables.outbox) {
97
+ if (ids.includes(fact.factId) && fact.sentAt === null) {
98
+ Object.assign(fact, { parkedAt: at, parkedReason: reason });
99
+ }
100
+ }
101
+
102
+ return Promise.resolve();
103
+ },
104
+ unparkFacts: () => {
105
+ const parked = tables.outbox.filter((fact) => fact.sentAt === null && fact.parkedAt !== null);
106
+
107
+ for (const fact of parked) {
108
+ Object.assign(fact, { parkedAt: null, parkedReason: null });
109
+ }
110
+
111
+ return Promise.resolve(parked.length);
112
+ },
94
113
  writeHeartbeat: (runner, at) => {
95
114
  const row = tables.runners.find((candidate) => candidate.id === runner);
96
115
 
@@ -102,21 +121,24 @@ function buildMemoryTx(tables: MemoryTables): StoreTx {
102
121
 
103
122
  return Promise.resolve();
104
123
  },
105
- syncPoolSlots: (pools) => {
106
- for (const pool of pools) {
107
- for (let slot = 1; slot <= pool.slots; slot += 1) {
108
- if (!tables.pool_slots.some((row) => row.pool === pool.name && row.slot === slot)) {
109
- tables.pool_slots.push({ pool: pool.name, slot, holder: null, gen: 0, retired: false });
110
- }
111
- }
112
- }
124
+ syncLimits: (limits, at) => {
125
+ const sync = collectSeatSync({
126
+ limits,
127
+ held: tables.limit_seats.filter((seat) => seat.releasedAt === null),
128
+ attempts: tables.attempts,
129
+ at,
130
+ });
113
131
 
114
- for (const row of tables.pool_slots) {
115
- const slots = pools.find((pool) => pool.name === row.pool)?.slots ?? 0;
132
+ tables.limits = structuredClone([...limits]);
116
133
 
117
- Object.assign(row, { retired: row.slot > slots });
134
+ for (const seat of tables.limit_seats) {
135
+ if (sync.release.some((released) => isSameHeldSeat(released, seat))) {
136
+ Object.assign(seat, { releasedAt: at });
137
+ }
118
138
  }
119
139
 
140
+ tables.limit_seats.push(...structuredClone([...sync.take]));
141
+
120
142
  return Promise.resolve();
121
143
  },
122
144
  markPullRequestChecked: (ref, at) => {
@@ -128,447 +150,14 @@ function buildMemoryTx(tables: MemoryTables): StoreTx {
128
150
 
129
151
  return Promise.resolve();
130
152
  },
131
- };
132
- }
133
-
134
- function buildMemoryReader(getTables: () => MemoryTables): StoreReader {
135
- const buildViews = (now: Instant, maxAttemptsByKind: MaxAttemptsByKind): ItemWithLevers[] =>
136
- buildItemViews(toItemSetRows(getTables()), now, maxAttemptsByKind);
137
-
138
- return {
139
- readSnapshot: (now, maxAttemptsByKind) => {
140
- const open = buildViews(now, maxAttemptsByKind).filter((item) => isOpen(item));
141
-
142
- return Promise.resolve({
143
- storeVersion: getTables().outbox.length,
144
- items: open.map((item) => toItemView(item)),
145
- links: sortLinks(getTables().links.filter((link) => link.removedAt === null)),
146
- levers: open
147
- .flatMap((item) => item.levers)
148
- .toSorted((first, second) => first.seq - second.seq),
149
- freeSlots: collectFreeSlots(getTables()),
150
- });
151
- },
152
- readItem: (id, now, maxAttemptsByKind) => {
153
- const item = buildViews(now, maxAttemptsByKind).find((view) => view.id === id);
154
- const view = item === undefined ? null : toItemView(item);
155
-
156
- return Promise.resolve(view);
157
- },
158
- readItemDetail: (id, now, maxAttemptsByKind) => {
159
- const tables = getTables();
160
- const item = buildViews(now, maxAttemptsByKind).find((view) => view.id === id);
153
+ markNoticeTried: (id, at, sent) => {
154
+ const row = tables.notices.find((candidate) => candidate.id === id);
161
155
 
162
- if (item === undefined) {
163
- return Promise.resolve(null);
156
+ if (row !== undefined) {
157
+ Object.assign(row, { tries: row.tries + 1, sentAt: sent ? at : row.sentAt });
164
158
  }
165
159
 
166
- const attempts = sortAttempts(tables.attempts.filter((attempt) => attempt.item === id));
167
-
168
- const attemptIds = new Set(attempts.map((attempt) => attempt.id));
169
-
170
- const links = sortLinks(
171
- tables.links.filter(
172
- (link) => link.removedAt === null && (link.source === id || link.target === id),
173
- ),
174
- );
175
-
176
- return Promise.resolve({
177
- item,
178
- attempts,
179
- links,
180
- evidence: sortEvidence(tables.evidence.filter((row) => attemptIds.has(row.attempt))),
181
- pullRequests: sortPullRequests(
182
- tables.pull_requests.filter((row) => attemptIds.has(row.attempt)),
183
- ),
184
- });
185
- },
186
- listItems: (filter, now, maxAttemptsByKind) =>
187
- Promise.resolve(
188
- buildViews(now, maxAttemptsByKind).filter(
189
- (item) =>
190
- (filter.state === undefined || item.derived.state === filter.state) &&
191
- (filter.repo === undefined || item.repo === filter.repo),
192
- ),
193
- ),
194
- listOpenIssues: (repo, now, maxAttemptsByKind) =>
195
- Promise.resolve(
196
- buildViews(now, maxAttemptsByKind)
197
- .filter((item) => item.kind === 'core.issue' && item.repo === repo && isOpen(item))
198
- .map((item) => toItemView(item)),
199
- ),
200
- readLastLeaseGen: (item, phase) => {
201
- const gens = getTables()
202
- .leases.filter((lease) => lease.item === item && lease.phase === phase)
203
- .map((lease) => lease.gen);
204
-
205
- return Promise.resolve(Math.max(0, ...gens));
206
- },
207
- readAttempt: (id) =>
208
- Promise.resolve(getTables().attempts.find((attempt) => attempt.id === id) ?? null),
209
- listEvidence: (attempt) =>
210
- Promise.resolve(sortEvidence(getTables().evidence.filter((row) => row.attempt === attempt))),
211
- listLiveAttempts: () =>
212
- Promise.resolve(
213
- sortAttempts(
214
- getTables().attempts.filter((attempt) =>
215
- ['starting', 'running', 'reconciling'].includes(attempt.state),
216
- ),
217
- ),
218
- ),
219
- listSucceededAttemptsWithoutPr: () => {
220
- const tables = getTables();
221
-
222
- const linked = new Set(tables.pull_requests.map((row) => row.attempt));
223
-
224
- return Promise.resolve(
225
- sortAttempts(
226
- tables.attempts.filter(
227
- (attempt) => attempt.state === 'succeeded' && !linked.has(attempt.id),
228
- ),
229
- ),
230
- );
231
- },
232
- findAttemptBySession: (session) =>
233
- Promise.resolve(
234
- sortAttempts(getTables().attempts).find((attempt) => attempt.atcSession === session) ??
235
- null,
236
- ),
237
- findAttemptByAgentSession: (session) =>
238
- Promise.resolve(
239
- getTables().attempt_sessions.find((row) => row.agentSession === session)?.attempt ?? null,
240
- ),
241
- listWatchedPullRequests: () =>
242
- Promise.resolve(
243
- sortPullRequests(getTables().pull_requests.filter((row) => row.state === 'open')),
244
- ),
245
- listPoolSlots: () =>
246
- Promise.resolve(
247
- getTables().pool_slots.toSorted(
248
- (first, second) => compareText(first.pool, second.pool) || first.slot - second.slot,
249
- ),
250
- ),
251
- readUnsentFacts: (limit) =>
252
- Promise.resolve(
253
- getTables()
254
- .outbox.filter((fact) => fact.sentAt === null)
255
- .slice(0, limit),
256
- ),
257
- readOutboxLag: () => {
258
- const unsent = getTables().outbox.filter((fact) => fact.sentAt === null);
259
- const oldest = unsent.toSorted((first, second) => first.at - second.at).at(0);
260
-
261
- return Promise.resolve({ unsent: unsent.length, oldestUnsentAt: oldest?.at ?? null });
160
+ return Promise.resolve();
262
161
  },
263
- readRunner: (id) => Promise.resolve(getTables().runners.find((row) => row.id === id) ?? null),
264
162
  };
265
163
  }
266
-
267
- function toItemSetRows(tables: MemoryTables): ItemSetRows {
268
- return {
269
- items: tables.items.toSorted(
270
- (first, second) => first.filedAt - second.filedAt || compareText(first.id, second.id),
271
- ),
272
- links: tables.links.filter((link) => link.removedAt === null),
273
- levers: tables.levers.toSorted((first, second) => first.seq - second.seq),
274
- attempts: sortAttempts(tables.attempts),
275
- liveLeases: tables.leases.filter((lease) => lease.releasedAt === null),
276
- pullRequests: tables.pull_requests,
277
- };
278
- }
279
-
280
- // SQLite's ORDER BY compares text byte by byte, and localeCompare doesn't
281
- function compareText(first: string, second: string): number {
282
- if (first === second) {
283
- return 0;
284
- }
285
-
286
- return first < second ? -1 : 1;
287
- }
288
-
289
- function sortAttempts(attempts: readonly AttemptRow[]): AttemptRow[] {
290
- return attempts.toSorted(
291
- (first, second) => first.claimedAt - second.claimedAt || compareText(first.id, second.id),
292
- );
293
- }
294
-
295
- function isOpen(item: ItemView): boolean {
296
- return item.derived.state !== 'done' && item.derived.state !== 'archived';
297
- }
298
-
299
- function toItemView(item: ItemWithLevers): ItemView {
300
- const { levers: _levers, ...view } = item;
301
-
302
- return view;
303
- }
304
-
305
- function sortLinks(links: readonly LinkRow[]): LinkRow[] {
306
- return links.toSorted(
307
- (first, second) =>
308
- first.addedAt - second.addedAt ||
309
- compareText(first.source, second.source) ||
310
- compareText(first.target, second.target),
311
- );
312
- }
313
-
314
- function collectFreeSlots(tables: MemoryTables): Record<PoolName, number> {
315
- const free: Record<PoolName, number> = {};
316
-
317
- for (const row of tables.pool_slots) {
318
- if (!row.retired) {
319
- free[row.pool] = (free[row.pool] ?? 0) + (row.holder === null ? 1 : 0);
320
- }
321
- }
322
-
323
- return free;
324
- }
325
-
326
- function sortEvidence(rows: readonly EvidenceRow[]): EvidenceRow[] {
327
- return rows.toSorted(
328
- (first, second) => first.at - second.at || compareText(first.factId, second.factId),
329
- );
330
- }
331
-
332
- function sortPullRequests(rows: readonly PullRequestRow[]): PullRequestRow[] {
333
- return rows.toSorted(
334
- (first, second) => compareText(first.repo, second.repo) || first.number - second.number,
335
- );
336
- }
337
-
338
- function applyMemoryChange(tables: MemoryTables, change: StoreChange): ApplyResult {
339
- const before = structuredClone(tables);
340
-
341
- try {
342
- const result = writeMemoryChange(tables, change);
343
-
344
- if (result.kind !== 'applied') {
345
- Object.assign(tables, before);
346
- }
347
-
348
- return result;
349
- } catch (error) {
350
- Object.assign(tables, before);
351
- throw error;
352
- }
353
- }
354
-
355
- function writeMemoryChange(tables: MemoryTables, change: StoreChange): ApplyResult {
356
- invariant(change.facts.length > 0, 'a change writes at least one fact');
357
-
358
- if (!hasLiveUnder(tables, change.under)) {
359
- return { kind: 'stale-generation' };
360
- }
361
-
362
- const take = findSlotTake(change.writes);
363
- const taken = take === null ? null : takeMemorySlot(tables, take);
364
-
365
- if (take !== null && taken === null) {
366
- return { kind: 'no-free-slot' };
367
- }
368
-
369
- const lastLeverSeq = Math.max(0, ...tables.levers.map((lever) => lever.seq));
370
- const applied = buildAppliedChange(change, { taken, lastLeverSeq });
371
-
372
- for (const write of applied.writes) {
373
- const refusal = applyMemoryWrite(tables, write);
374
-
375
- if (refusal !== null) {
376
- return refusal;
377
- }
378
- }
379
-
380
- for (const fact of applied.facts) {
381
- invariant(
382
- !tables.outbox.some((row) => row.factId === fact.id),
383
- `the outbox holds fact ${fact.id}`,
384
- );
385
-
386
- tables.outbox.push({
387
- seq: tables.outbox.length + 1,
388
- factId: fact.id,
389
- type: fact.type,
390
- entity: fact.entity,
391
- actor: fact.actor,
392
- at: change.at,
393
- payload: toJSONValue(fact.payload),
394
- sentAt: null,
395
- });
396
- }
397
-
398
- return { kind: 'applied', slot: taken === null ? null : toSlotLease(taken) };
399
- }
400
-
401
- function hasLiveUnder(tables: MemoryTables, under: StoreChange['under']): boolean {
402
- if (under === undefined) {
403
- return true;
404
- }
405
-
406
- const hasLease = tables.leases.some(
407
- (row) =>
408
- row.item === under.item &&
409
- row.phase === under.phase &&
410
- row.gen === under.gen &&
411
- row.releasedAt === null,
412
- );
413
-
414
- const slot = under.slot;
415
-
416
- return (
417
- hasLease &&
418
- (slot === undefined ||
419
- tables.pool_slots.some(
420
- (row) =>
421
- row.pool === slot.pool &&
422
- row.slot === slot.slot &&
423
- row.gen === slot.gen &&
424
- row.holder !== null,
425
- ))
426
- );
427
- }
428
-
429
- function takeMemorySlot(tables: MemoryTables, take: SlotTake): TakenSlot | null {
430
- const free = tables.pool_slots
431
- .filter((row) => row.pool === take.pool && row.holder === null && !row.retired)
432
- .toSorted((first, second) => first.slot - second.slot)
433
- .at(0);
434
-
435
- if (free === undefined) {
436
- return null;
437
- }
438
-
439
- Object.assign(free, { holder: take.holder, gen: free.gen + 1 });
440
-
441
- return { holder: take.holder, pool: take.pool, slot: free.slot, gen: free.gen };
442
- }
443
-
444
- function applyMemoryWrite(tables: MemoryTables, write: RowWrite): ApplyResult | null {
445
- return match(write)
446
- .with({ op: 'take-slot' }, () => null)
447
- .with({ op: 'release-slot' }, (release) => {
448
- const row = tables.pool_slots.find(
449
- (candidate) =>
450
- candidate.pool === release.pool &&
451
- candidate.slot === release.slot &&
452
- candidate.holder === release.holder,
453
- );
454
-
455
- if (row === undefined) {
456
- return { kind: 'stale-generation' } as const;
457
- }
458
-
459
- Object.assign(row, { holder: null });
460
-
461
- return null;
462
- })
463
- .with({ op: 'insert', table: 'leases' }, (insert) => {
464
- const isHeld = tables.leases.some(
465
- (row) =>
466
- row.item === insert.row.item && row.phase === insert.row.phase && row.releasedAt === null,
467
- );
468
-
469
- if (isHeld) {
470
- return { kind: 'lease-held' } as const;
471
- }
472
-
473
- insertMemoryRow(tables, insert.table, insert.row);
474
-
475
- return null;
476
- })
477
- .with({ op: 'insert' }, (insert) => {
478
- insertMemoryRow(tables, insert.table, insert.row);
479
-
480
- return null;
481
- })
482
- .with({ op: 'update' }, (update) => {
483
- const rows: object[] = tables[update.table];
484
-
485
- const matched = rows.filter(
486
- (row) => hasFields(row, update.key) && hasFields(row, update.expect),
487
- );
488
-
489
- if (matched.length === 0) {
490
- const key = toJSONValue(update.key);
491
-
492
- return { kind: 'guard-mismatch', table: update.table, key } as const;
493
- }
494
-
495
- for (const row of matched) {
496
- Object.assign(row, update.set);
497
-
498
- assertMemoryUnique(update.table, rows, row);
499
- }
500
-
501
- return null;
502
- })
503
- .exhaustive();
504
- }
505
-
506
- function insertMemoryRow(tables: MemoryTables, table: TableName, row: object): void {
507
- const rows: object[] = tables[table];
508
- const copy = structuredClone(row);
509
-
510
- rows.push(copy);
511
-
512
- assertMemoryUnique(table, rows, copy);
513
- }
514
-
515
- interface UniqueKey {
516
- readonly fields: readonly string[];
517
-
518
- // a partial unique index covers only the rows where this field is null
519
- readonly live?: string;
520
- }
521
-
522
- const uniqueKeys: Readonly<Record<TableName, readonly UniqueKey[]>> = {
523
- items: [{ fields: ['id'] }],
524
- links: [
525
- { fields: ['source', 'target', 'type', 'addedAt'] },
526
- { fields: ['source', 'target', 'type'], live: 'removedAt' },
527
- ],
528
- levers: [{ fields: ['seq'] }],
529
- attempts: [{ fields: ['id'] }, { fields: ['item', 'phase', 'number'] }],
530
- attempt_sessions: [{ fields: ['attempt', 'agentSession'] }, { fields: ['agentSession'] }],
531
- leases: [{ fields: ['item', 'phase', 'gen'] }, { fields: ['item', 'phase'], live: 'releasedAt' }],
532
- pool_slots: [{ fields: ['pool', 'slot'] }],
533
- decisions: [{ fields: ['id'] }],
534
- evidence: [{ fields: ['factId'] }, { fields: ['attempt', 'kind', 'ref'] }],
535
- pull_requests: [{ fields: ['repo', 'number'] }],
536
- runners: [{ fields: ['id'] }],
537
- };
538
-
539
- function assertMemoryUnique(table: TableName, rows: readonly object[], row: object): void {
540
- const fields = toFieldMap(row);
541
-
542
- for (const key of uniqueKeys[table]) {
543
- const hasClash = rows.some((other) => {
544
- const otherFields = toFieldMap(other);
545
-
546
- return (
547
- other !== row &&
548
- (key.live === undefined ||
549
- (fields.get(key.live) === null && otherFields.get(key.live) === null)) &&
550
- key.fields.every((field) => isDeepStrictEqual(otherFields.get(field), fields.get(field)))
551
- );
552
- });
553
-
554
- if (hasClash) {
555
- throw new Error(`UNIQUE constraint failed: ${table} (${key.fields.join(', ')})`);
556
- }
557
- }
558
- }
559
-
560
- function toFieldMap(row: object): Map<string, unknown> {
561
- return new Map<string, unknown>(Object.entries(row));
562
- }
563
-
564
- function hasFields(row: object, fields: object): boolean {
565
- const rowFields = toFieldMap(row);
566
-
567
- return Object.entries(fields).every(([field, value]: [string, unknown]) =>
568
- isDeepStrictEqual(rowFields.get(field), value),
569
- );
570
- }
571
-
572
- function toSlotLease(taken: TakenSlot): { pool: PoolName; slot: number; gen: number } {
573
- return { pool: taken.pool, slot: taken.slot, gen: taken.gen };
574
- }