@astrosheep/keiyaku 2.9.6 → 2.9.8

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 (87) hide show
  1. package/build/.tsbuildinfo +1 -1
  2. package/build/agents/harness/event-persistence.js +7 -5
  3. package/build/agents/harness/events.js +3 -2
  4. package/build/agents/harness/outcome.js +10 -0
  5. package/build/agents/providers/codex-app-server/adapter.js +6 -1
  6. package/build/agents/providers/codex-app-server/session.js +8 -7
  7. package/build/agents/selector.js +12 -1
  8. package/build/cli/commands/akuma/view/handler.js +3 -11
  9. package/build/cli/commands/contract/amend/handler.js +1 -1
  10. package/build/cli/commands/contract/amend/meta.js +4 -4
  11. package/build/cli/commands/contract/bind/handler.js +10 -4
  12. package/build/cli/commands/contract/bind/meta.js +6 -6
  13. package/build/cli/commands/contract/petition/handler.js +16 -3
  14. package/build/cli/commands/contract/petition/meta.js +4 -4
  15. package/build/cli/commands/metadata.js +3 -2
  16. package/build/cli/commands/projection/status/handler.js +5 -4
  17. package/build/cli/commands/projection/status/meta.js +2 -2
  18. package/build/cli/commands/projection/tell/handler.js +10 -2
  19. package/build/cli/commands/projection/tell/meta.js +3 -3
  20. package/build/cli/commands/task/add/meta.js +9 -1
  21. package/build/cli/commands/task/catalog.js +2 -0
  22. package/build/cli/commands/task/log/handler.js +12 -0
  23. package/build/cli/commands/task/log/meta.js +8 -0
  24. package/build/cli/commands/task/shared.js +2 -1
  25. package/build/cli/completion.js +8 -0
  26. package/build/cli/flags.js +8 -0
  27. package/build/cli/index.js +10 -6
  28. package/build/cli/parse-flags.js +6 -0
  29. package/build/cli/parse-metadata.js +1 -1
  30. package/build/cli/render/kanshi.js +2 -2
  31. package/build/cli/render/line-width.js +33 -0
  32. package/build/cli/render/path-prefix-compaction.js +131 -0
  33. package/build/cli/render/petition.js +4 -0
  34. package/build/cli/render/projection-activity.js +103 -21
  35. package/build/cli/render/shared.js +38 -15
  36. package/build/cli/render/status.js +8 -5
  37. package/build/cli/render/success-response.js +2 -0
  38. package/build/cli/render/tool-presentation.js +3 -3
  39. package/build/cli/render/wait.js +68 -48
  40. package/build/cli/subagent-guard.js +3 -0
  41. package/build/cli/types.js +1 -1
  42. package/build/config/settings/disease.js +4 -4
  43. package/build/config/settings/loader.js +44 -21
  44. package/build/core/addressing.js +40 -9
  45. package/build/core/amend.js +21 -5
  46. package/build/core/bind.js +111 -6
  47. package/build/core/call/context.js +19 -3
  48. package/build/core/call/execution.js +43 -20
  49. package/build/core/draft.js +1 -1
  50. package/build/core/ledger-batch.js +194 -0
  51. package/build/core/projection/generation/database.js +43 -2
  52. package/build/core/projection/generation/model.js +24 -3
  53. package/build/core/projection/generation/projection-generation-continuation.js +75 -10
  54. package/build/core/projection/generation/projection-generation-execution.js +46 -37
  55. package/build/core/projection/generation/projection-generation-launcher.js +148 -20
  56. package/build/core/projection/generation/projection-generation-process.js +3 -1
  57. package/build/core/projection/generation/projection-generation-runner.js +145 -66
  58. package/build/core/projection/generation/projection-generation-runtime.js +93 -11
  59. package/build/core/projection/generation/store.js +26 -1
  60. package/build/core/projection/generation/transitions.js +193 -14
  61. package/build/core/projection/index.js +5 -5
  62. package/build/core/projection/projection-core.js +1 -1
  63. package/build/core/projection/projection-kill.js +53 -10
  64. package/build/core/projection/projection-life-protocol.js +10 -0
  65. package/build/core/projection/projection-runner-lock.js +177 -37
  66. package/build/core/projection/projection-status.js +143 -55
  67. package/build/core/projection/projection-wait.js +53 -15
  68. package/build/core/projection/projection-wake.js +177 -46
  69. package/build/core/projection/tell/database.js +18 -0
  70. package/build/core/projection/tell/model.js +1 -0
  71. package/build/core/projection/tell/store.js +102 -55
  72. package/build/core/registry.js +82 -69
  73. package/build/core/scope.js +9 -9
  74. package/build/core/status/board.js +42 -4
  75. package/build/core/status/drift.js +21 -5
  76. package/build/core/status/ledger-batch.js +1 -158
  77. package/build/core/task/index.js +2 -2
  78. package/build/core/task/task-contract.js +18 -0
  79. package/build/core/task/task-git-runtime.js +8 -10
  80. package/build/core/task/task-git-store.js +55 -3
  81. package/build/core/worktree-path.js +39 -25
  82. package/build/flow-error.js +1 -1
  83. package/build/generated/version.js +2 -2
  84. package/build/git/refs.js +47 -1
  85. package/package.json +1 -1
  86. package/skills/keiyaku-akuma/SKILL.md +28 -0
  87. package/skills/keiyaku-workflow/SKILL.md +142 -18
@@ -2,16 +2,17 @@ import { FlowError } from "../../flow-error.js";
2
2
  import { parseResolvedAkumaLaunchSnapshot } from "../../agents/launch-snapshot/model.js";
3
3
  import { isRecoverableSessionEnvelope } from "../../agents/harness/index.js";
4
4
  import { ProjectionStateError } from "../atomic-publish.js";
5
- import { applyContinuationEffort, snapshotPendingTells, } from "./generation/projection-generation-continuation.js";
5
+ import { applyContinuationEffort, readProjectionGenerationContinuationSnapshot, } from "./generation/projection-generation-continuation.js";
6
6
  import { createProjectionExecutionId } from "./generation/projection-generation-identity.js";
7
7
  import { PROJECTION_ADOPTION_POLL_MS, PROJECTION_ADOPTION_TIMEOUT_MS, startProjectionGeneration, } from "./generation/projection-generation-launcher.js";
8
8
  import { openProjectionGenerationStore, openProjectionGenerationStoreReadOnly, } from "./generation/store.js";
9
9
  import { observeProjectionLifeSnapshot } from "./projection-life-observer.js";
10
- import { observeProjectionRunnerLock } from "./projection-runner-lock.js";
10
+ import { acquireProjectionRunnerLock, acquireProjectionRunnerSettlementLock, ProjectionRunnerLockUnavailableError, } from "./projection-runner-lock.js";
11
11
  import { setTimeout as delay } from "node:timers/promises";
12
12
  function incompatibleProjectionError(projectionId) {
13
13
  const subject = projectionId ? `Projection '${projectionId}'` : "Projection";
14
- return new FlowError("PROJECTION_INCOMPATIBLE", `${subject} has incompatible historical storage; keiyaku status --all can inspect it, but tell cannot change it.`);
14
+ const statusCoordinate = projectionId ?? "<projection-id>";
15
+ return new FlowError("PROJECTION_INCOMPATIBLE", `${subject} has incompatible historical storage; inspect it with keiyaku status ${statusCoordinate}, but tell cannot change it.`);
15
16
  }
16
17
  /**
17
18
  * Tell is a current-schema mutation only. This gate runs before the handler
@@ -98,10 +99,24 @@ export function assertTellEffortSupported(projectionDirectory, effort) {
98
99
  throw new FlowError("INVALID_EFFORT_OVERRIDE", `Effort '${effort}' is not permitted; legal values: ${effortOptions.join(", ")}.`);
99
100
  }
100
101
  }
101
- async function startCommittedSuccessor(projectionDirectory, projectionId, executionId, startGeneration) {
102
- const started = await startGeneration(projectionDirectory, executionId);
102
+ async function startCommittedSuccessor(projectionDirectory, projectionId, executionId, startGeneration, dependencies = {}) {
103
+ let started;
104
+ try {
105
+ started = await startGeneration(projectionDirectory, executionId, dependencies);
106
+ }
107
+ finally {
108
+ dependencies.launchOwnership?.close();
109
+ }
103
110
  if (started.status === "adopted")
104
111
  return "woken";
112
+ if (started.status === "pending")
113
+ return "pending";
114
+ const reconciled = readCurrentGeneration(projectionDirectory);
115
+ if (reconciled?.launch.executionId === executionId && reconciled.adoption)
116
+ return "woken";
117
+ if (reconciled && !reconciled.verdict && reconciled.launch.executionId !== executionId) {
118
+ return reconciled.adoption ? "active" : "pending";
119
+ }
105
120
  throw new FlowError("PROJECTION_WAKE_FAILED", `The tell was saved for '${projectionId}', but its successor could not be started (cause: ${started.cause}).`, {
106
121
  facts: {
107
122
  kind: "projection_wake_failure",
@@ -122,6 +137,31 @@ function sessionWakeFailure(projectionId, cause) {
122
137
  },
123
138
  });
124
139
  }
140
+ export function persistInterruptWakeFailure(projectionDirectory, error, nowMs = Date.now()) {
141
+ if (!(error instanceof FlowError)
142
+ || error.code !== "PROJECTION_WAKE_FAILED"
143
+ || error.facts?.kind !== "projection_wake_failure")
144
+ return;
145
+ const continuation = readProjectionGenerationContinuationSnapshot(projectionDirectory, 10);
146
+ const current = continuation.current;
147
+ if (current?.verdict?.facts.state !== "interrupted" || !continuation.requiresWake)
148
+ return;
149
+ const successorExecutionId = createProjectionExecutionId(nowMs);
150
+ const facts = successorFacts(current, continuation.successor, nowMs, false);
151
+ const store = openProjectionGenerationStore(projectionDirectory);
152
+ try {
153
+ store.failInterruptWakeIfSettled({
154
+ predecessorExecutionId: current.launch.executionId,
155
+ successorExecutionId,
156
+ successorFacts: facts,
157
+ cause: error.facts.cause,
158
+ terminalAt: new Date(nowMs).toISOString(),
159
+ });
160
+ }
161
+ finally {
162
+ store.close();
163
+ }
164
+ }
125
165
  export async function tellProjection(projectionDirectory, projectionId, effort, dependencies = {}) {
126
166
  const startGeneration = dependencies.startGeneration ?? startProjectionGeneration;
127
167
  const sleep = dependencies.sleep ?? ((milliseconds) => delay(milliseconds));
@@ -148,12 +188,15 @@ export async function tellProjection(projectionDirectory, projectionId, effort,
148
188
  if (life.phase === "unknown") {
149
189
  return "pending";
150
190
  }
151
- const current = readCurrentGeneration(projectionDirectory);
191
+ const continuation = readProjectionGenerationContinuationSnapshot(projectionDirectory, 10);
192
+ const current = continuation.current;
152
193
  if (!current)
153
194
  return "pending";
154
195
  if (current.killIntent && !current.verdict)
155
196
  return "pending";
156
- const snapshot = snapshotPendingTells(projectionDirectory);
197
+ if (!continuation.requiresWake)
198
+ return "pending";
199
+ const snapshot = continuation.successor;
157
200
  const successorExecutionId = createProjectionExecutionId(nowMs);
158
201
  let facts;
159
202
  try {
@@ -169,56 +212,144 @@ export async function tellProjection(projectionDirectory, projectionId, effort,
169
212
  try {
170
213
  if (life.phase === "startup-timeout") {
171
214
  const terminalAt = new Date(nowMs).toISOString();
172
- const transition = store.supersedeStartupTimeout({
173
- executionId: current.launch.executionId,
174
- observedAtMs: nowMs,
175
- startupTimeoutMs: PROJECTION_ADOPTION_TIMEOUT_MS,
176
- diagnostic: { detail: "runner did not adopt before startup timeout" },
177
- successorExecutionId,
178
- successorFacts: facts,
179
- terminalAt,
180
- });
181
- if (transition.status === "committed") {
182
- return await startCommittedSuccessor(projectionDirectory, projectionId, successorExecutionId, startGeneration);
215
+ let deathEvidence;
216
+ try {
217
+ deathEvidence = acquireProjectionRunnerSettlementLock(projectionDirectory);
183
218
  }
184
- if (transition.reason === "kill-requested")
219
+ catch (error) {
220
+ if (error instanceof ProjectionRunnerLockUnavailableError)
221
+ return "pending";
185
222
  return "pending";
186
- await sleep(PROJECTION_ADOPTION_POLL_MS);
187
- continue;
223
+ }
224
+ try {
225
+ store.beginWriteTransaction();
226
+ const protectedCurrent = store.readCurrentGeneration();
227
+ if (protectedCurrent?.launch.executionId !== current.launch.executionId
228
+ || protectedCurrent.adoption
229
+ || protectedCurrent.verdict) {
230
+ store.rollbackWriteTransaction();
231
+ return "pending";
232
+ }
233
+ if (protectedCurrent.killIntent) {
234
+ const settled = store.settleKillIfRunnerDead({
235
+ executionId: current.launch.executionId,
236
+ releasedLockEvidence: { observedAt: terminalAt, state: "acquired" },
237
+ });
238
+ if (settled.status === "committed")
239
+ store.commitWriteTransaction();
240
+ else
241
+ store.rollbackWriteTransaction();
242
+ return "pending";
243
+ }
244
+ deathEvidence.close();
245
+ let launchOwnership;
246
+ try {
247
+ launchOwnership = acquireProjectionRunnerLock(projectionDirectory);
248
+ }
249
+ catch (error) {
250
+ store.rollbackWriteTransaction();
251
+ if (error instanceof ProjectionRunnerLockUnavailableError)
252
+ return "pending";
253
+ return "pending";
254
+ }
255
+ return await startCommittedSuccessor(projectionDirectory, projectionId, successorExecutionId, startGeneration, {
256
+ commitLaunch: () => {
257
+ const transition = store.supersedeStartupTimeout({
258
+ executionId: current.launch.executionId,
259
+ releasedLockEvidence: { observedAt: terminalAt, state: "acquired" },
260
+ diagnostic: { detail: "runner did not adopt before startup timeout" },
261
+ successorExecutionId,
262
+ successorFacts: facts,
263
+ terminalAt,
264
+ });
265
+ if (transition.status === "committed")
266
+ store.commitWriteTransaction();
267
+ else
268
+ store.rollbackWriteTransaction();
269
+ return transition.status === "committed";
270
+ },
271
+ launchOwnership,
272
+ });
273
+ }
274
+ finally {
275
+ store.rollbackWriteTransaction();
276
+ deathEvidence.close();
277
+ }
188
278
  }
189
279
  if (life.phase === "lost" && !current.verdict) {
190
- const lock = observeProjectionRunnerLock(projectionDirectory);
191
- if (lock.state !== "released")
192
- return lock.state === "held" ? "active" : "pending";
193
- const terminalAt = new Date(nowMs).toISOString();
194
- const transition = store.replaceDeadRunner({
195
- executionId: current.launch.executionId,
196
- releasedLockEvidence: { observedAt: terminalAt },
197
- successorExecutionId,
198
- successorFacts: facts,
199
- terminalAt,
200
- });
201
- if (transition.status === "committed") {
202
- return await startCommittedSuccessor(projectionDirectory, projectionId, successorExecutionId, startGeneration);
280
+ let deathEvidence;
281
+ try {
282
+ deathEvidence = acquireProjectionRunnerSettlementLock(projectionDirectory);
203
283
  }
204
- if (transition.reason === "kill-requested")
284
+ catch (error) {
285
+ if (error instanceof ProjectionRunnerLockUnavailableError)
286
+ return "active";
205
287
  return "pending";
206
- await sleep(PROJECTION_ADOPTION_POLL_MS);
207
- continue;
288
+ }
289
+ try {
290
+ store.beginWriteTransaction();
291
+ const protectedCurrent = store.readCurrentGeneration();
292
+ if (protectedCurrent?.launch.executionId !== current.launch.executionId
293
+ || !protectedCurrent.adoption
294
+ || protectedCurrent.verdict) {
295
+ store.rollbackWriteTransaction();
296
+ return "pending";
297
+ }
298
+ const terminalAt = new Date(nowMs).toISOString();
299
+ if (protectedCurrent.killIntent) {
300
+ const settled = store.settleKillIfRunnerDead({
301
+ executionId: current.launch.executionId,
302
+ releasedLockEvidence: { observedAt: terminalAt, state: "acquired" },
303
+ });
304
+ if (settled.status === "committed")
305
+ store.commitWriteTransaction();
306
+ else
307
+ store.rollbackWriteTransaction();
308
+ return "pending";
309
+ }
310
+ deathEvidence.close();
311
+ let launchOwnership;
312
+ try {
313
+ launchOwnership = acquireProjectionRunnerLock(projectionDirectory);
314
+ }
315
+ catch (error) {
316
+ store.rollbackWriteTransaction();
317
+ if (error instanceof ProjectionRunnerLockUnavailableError)
318
+ return "active";
319
+ return "pending";
320
+ }
321
+ return await startCommittedSuccessor(projectionDirectory, projectionId, successorExecutionId, startGeneration, {
322
+ commitLaunch: () => {
323
+ const transition = store.replaceDeadRunner({
324
+ executionId: current.launch.executionId,
325
+ releasedLockEvidence: { observedAt: terminalAt, state: "acquired" },
326
+ successorExecutionId,
327
+ successorFacts: facts,
328
+ terminalAt,
329
+ });
330
+ if (transition.status === "committed")
331
+ store.commitWriteTransaction();
332
+ else
333
+ store.rollbackWriteTransaction();
334
+ return transition.status === "committed";
335
+ },
336
+ launchOwnership,
337
+ });
338
+ }
339
+ finally {
340
+ store.rollbackWriteTransaction();
341
+ deathEvidence.close();
342
+ }
208
343
  }
209
344
  if (!["done", "failed", "lost", "dismissed", "killed"].includes(life.phase)) {
210
345
  return "pending";
211
346
  }
212
- const transition = store.launchIfSettled({
213
- executionId: successorExecutionId,
214
- facts,
347
+ return await startCommittedSuccessor(projectionDirectory, projectionId, successorExecutionId, startGeneration, {
348
+ commitLaunch: () => store.launchIfSettled({
349
+ executionId: successorExecutionId,
350
+ facts,
351
+ }).status === "committed",
215
352
  });
216
- if (transition.status === "committed") {
217
- return await startCommittedSuccessor(projectionDirectory, projectionId, successorExecutionId, startGeneration);
218
- }
219
- if (transition.reason === "kill-requested")
220
- return "pending";
221
- await sleep(PROJECTION_ADOPTION_POLL_MS);
222
353
  }
223
354
  finally {
224
355
  store.close();
@@ -42,6 +42,24 @@ export function withRead(projectionDirectory, read) {
42
42
  database.close();
43
43
  }
44
44
  }
45
+ export function withReadSnapshot(projectionDirectory, read) {
46
+ const database = openDatabase(projectionDirectory, true);
47
+ try {
48
+ database.exec("BEGIN");
49
+ try {
50
+ const result = read(database);
51
+ database.exec("COMMIT");
52
+ return result;
53
+ }
54
+ catch (error) {
55
+ rollbackQuietly(database);
56
+ throw error;
57
+ }
58
+ }
59
+ finally {
60
+ database.close();
61
+ }
62
+ }
45
63
  export function withWrite(projectionDirectory, write) {
46
64
  const database = openDatabase(projectionDirectory, false);
47
65
  try {
@@ -6,6 +6,7 @@ export const PROJECTION_TELL_STATES = [
6
6
  "consumed",
7
7
  "delivered",
8
8
  ];
9
+ export const PENDING_PROJECTION_TELL_STATES = ["inbox", "inflight", "submitted"];
9
10
  export function requireExecutionId(value) {
10
11
  if (typeof value !== "string" || value.trim() === "") {
11
12
  throw new ProjectionStateError("submitted tell executionId must be a non-empty string");
@@ -1,7 +1,6 @@
1
1
  import { ProjectionStateError, randomHex8 } from "../../atomic-publish.js";
2
- import { withRead, withWrite } from "./database.js";
3
- import { assertTellIdRandomHex, originalFromRow, parseFence, requireExecutionId, submittedFromRow, } from "./model.js";
4
- const PENDING_TELL_LIMIT = 10_000;
2
+ import { withRead, withReadSnapshot, withWrite } from "./database.js";
3
+ import { assertTellIdRandomHex, originalFromRow, PENDING_PROJECTION_TELL_STATES, parseFence, requireExecutionId, submittedFromRow, } from "./model.js";
5
4
  function selectTell(database, tellId) {
6
5
  return database.prepare(`
7
6
  SELECT
@@ -26,6 +25,77 @@ export function listTellIds(projectionDirectory, state) {
26
25
  ORDER BY tell_id
27
26
  `).all(state).map((row) => row.tell_id));
28
27
  }
28
+ /** List continuation fence metadata without loading operator tell bodies. */
29
+ export function listTellMetadataFromDatabase(database, states) {
30
+ const placeholders = states.map(() => "?").join(", ");
31
+ return database.prepare(`
32
+ SELECT tell_id, state, execution_id, effort
33
+ FROM projection_tells
34
+ WHERE state IN (${placeholders})
35
+ ORDER BY tell_id
36
+ `).all(...states).map((row) => ({
37
+ tellId: row.tell_id,
38
+ state: row.state,
39
+ ...(row.execution_id ? { executionId: row.execution_id } : {}),
40
+ ...(row.effort ? { effort: row.effort } : {}),
41
+ }));
42
+ }
43
+ /** List continuation fence metadata without loading operator tell bodies. */
44
+ export function listTellMetadata(projectionDirectory, states) {
45
+ return withRead(projectionDirectory, (database) => listTellMetadataFromDatabase(database, states));
46
+ }
47
+ export function readPendingTellWindowFromDatabase(database, limit) {
48
+ if (!Number.isSafeInteger(limit) || limit < 0) {
49
+ throw new ProjectionStateError("pending tell window limit must be a non-negative safe integer");
50
+ }
51
+ const pendingPlaceholders = PENDING_PROJECTION_TELL_STATES.map(() => "?").join(", ");
52
+ const total = database.prepare(`
53
+ SELECT COUNT(*) AS count
54
+ FROM projection_tells
55
+ WHERE state IN (${pendingPlaceholders})
56
+ `).get(...PENDING_PROJECTION_TELL_STATES);
57
+ const rows = database.prepare(`
58
+ SELECT tell_id, text, created_at, state
59
+ FROM projection_tells
60
+ WHERE state IN (${pendingPlaceholders})
61
+ ORDER BY tell_id
62
+ LIMIT ?
63
+ `).all(...PENDING_PROJECTION_TELL_STATES, limit);
64
+ return {
65
+ totals: { undelivered: Number(total.count) },
66
+ window: rows.map((row) => ({ tellId: row.tell_id, text: row.text, createdAt: row.created_at, state: row.state })),
67
+ };
68
+ }
69
+ export function readPendingTellWindow(projectionDirectory, limit) {
70
+ return withReadSnapshot(projectionDirectory, (database) => readPendingTellWindowFromDatabase(database, limit));
71
+ }
72
+ /** Whether a failed successor fence still retains delivered mail for replay. */
73
+ export function hasRetainedDeliveredTellFromDatabase(database, retainedFence) {
74
+ if (retainedFence.length === 0)
75
+ return false;
76
+ const statement = database.prepare(`
77
+ SELECT 1
78
+ FROM projection_tells
79
+ WHERE tell_id = ? AND state = 'delivered'
80
+ LIMIT 1
81
+ `);
82
+ return retainedFence.some((tellId) => statement.get(tellId) !== undefined);
83
+ }
84
+ export function hasRetainedDeliveredTell(projectionDirectory, retainedFence) {
85
+ return withRead(projectionDirectory, (database) => hasRetainedDeliveredTellFromDatabase(database, retainedFence));
86
+ }
87
+ /** Whether interrupted replay has any delivered tell left to return to inbox. */
88
+ export function hasDeliveredTellFromDatabase(database, executionId) {
89
+ return database.prepare(`
90
+ SELECT 1
91
+ FROM projection_tells
92
+ WHERE state = 'delivered' AND execution_id = ?
93
+ LIMIT 1
94
+ `).get(executionId) !== undefined;
95
+ }
96
+ export function hasDeliveredTell(projectionDirectory, executionId) {
97
+ return withRead(projectionDirectory, (database) => hasDeliveredTellFromDatabase(database, executionId));
98
+ }
29
99
  export function readTellOriginal(projectionDirectory, state, tellId) {
30
100
  return withRead(projectionDirectory, (database) => {
31
101
  const row = selectTell(database, tellId);
@@ -49,14 +119,6 @@ export function writeInboxTell(projectionDirectory, text, nowMs, options) {
49
119
  }
50
120
  const tellId = `${nowMs}-${assertTellIdRandomHex((options?.randomHex ?? randomHex8)())}`;
51
121
  return withWrite(projectionDirectory, (database) => {
52
- const pending = database.prepare(`
53
- SELECT COUNT(*) AS count
54
- FROM projection_tells
55
- WHERE state IN ('inbox', 'inflight', 'submitted', 'delivered')
56
- `).get();
57
- if (Number(pending.count) >= PENDING_TELL_LIMIT) {
58
- throw new ProjectionStateError(`projection has reached the ${PENDING_TELL_LIMIT} pending tell limit`);
59
- }
60
122
  try {
61
123
  database.prepare(`
62
124
  INSERT INTO projection_tells (
@@ -123,6 +185,31 @@ export function claimFencedTells(projectionDirectory, fence, executionId) {
123
185
  claimed.push(tellId);
124
186
  continue;
125
187
  }
188
+ if (row.state === "submitted" || row.state === "delivered") {
189
+ const submitted = submittedFromRow(row);
190
+ if (submitted.executionId === owner) {
191
+ throw new ProjectionStateError(`submitted tell belongs to current execution: ${tellId}`);
192
+ }
193
+ const replayedFrom = [
194
+ ...(submitted.replayedFrom ?? []),
195
+ { executionId: submitted.executionId, fence: submitted.fence },
196
+ ];
197
+ const result = database.prepare(`
198
+ UPDATE projection_tells
199
+ SET
200
+ state = 'inflight',
201
+ execution_id = NULL,
202
+ fence_json = NULL,
203
+ replayed_from_json = ?,
204
+ delivered_at = NULL
205
+ WHERE tell_id = ? AND state = ?
206
+ `).run(JSON.stringify(replayedFrom), tellId, row.state);
207
+ if (Number(result.changes) !== 1) {
208
+ throw new ProjectionStateError(`generation tell fence member changed while claiming: ${tellId}`);
209
+ }
210
+ claimed.push(tellId);
211
+ continue;
212
+ }
126
213
  if (row.state === "consumed" && row.execution_id === owner)
127
214
  continue;
128
215
  throw new ProjectionStateError(`generation tell fence member is not claimable: ${tellId}`);
@@ -152,7 +239,7 @@ export function markTellSubmitted(projectionDirectory, tellId, fields) {
152
239
  state = 'submitted',
153
240
  execution_id = ?,
154
241
  fence_json = ?,
155
- replayed_from_json = ?
242
+ replayed_from_json = COALESCE(?, replayed_from_json)
156
243
  WHERE tell_id = ? AND state = 'inflight'
157
244
  `).run(executionId, JSON.stringify(fence), replayedFrom.length > 0 ? JSON.stringify(replayedFrom) : null, tellId);
158
245
  });
@@ -190,12 +277,13 @@ export function readProjectionTellCounts(projectionDirectory) {
190
277
  }
191
278
  export function snapshotPendingTells(projectionDirectory) {
192
279
  return withRead(projectionDirectory, (database) => {
280
+ const pendingPlaceholders = PENDING_PROJECTION_TELL_STATES.map(() => "?").join(", ");
193
281
  const rows = database.prepare(`
194
282
  SELECT tell_id, effort
195
283
  FROM projection_tells
196
- WHERE state IN ('inbox', 'inflight', 'submitted', 'delivered')
284
+ WHERE state IN (${pendingPlaceholders})
197
285
  ORDER BY tell_id
198
- `).all();
286
+ `).all(...PENDING_PROJECTION_TELL_STATES);
199
287
  const effort = rows.reduce((selected, row) => row.effort ?? selected, undefined);
200
288
  return {
201
289
  tellIds: rows.map((row) => row.tell_id),
@@ -203,44 +291,3 @@ export function snapshotPendingTells(projectionDirectory) {
203
291
  };
204
292
  });
205
293
  }
206
- export function demoteSubmittedTellsForReplay(projectionDirectory, executionId) {
207
- const currentExecutionId = requireExecutionId(executionId);
208
- return withWrite(projectionDirectory, (database) => {
209
- const rows = database.prepare(`
210
- SELECT
211
- tell_id,
212
- text,
213
- created_at,
214
- effort,
215
- state,
216
- execution_id,
217
- fence_json,
218
- replayed_from_json,
219
- delivered_at
220
- FROM projection_tells
221
- WHERE state IN ('submitted', 'delivered')
222
- ORDER BY tell_id
223
- `).all();
224
- for (const row of rows) {
225
- const submitted = submittedFromRow(row);
226
- if (submitted.executionId === currentExecutionId) {
227
- throw new ProjectionStateError(`submitted tell belongs to current execution: ${row.tell_id}`);
228
- }
229
- const replayedFrom = [
230
- ...(submitted.replayedFrom ?? []),
231
- { executionId: submitted.executionId, fence: submitted.fence },
232
- ];
233
- database.prepare(`
234
- UPDATE projection_tells
235
- SET
236
- state = 'inflight',
237
- execution_id = NULL,
238
- fence_json = NULL,
239
- replayed_from_json = ?,
240
- delivered_at = NULL
241
- WHERE tell_id = ? AND state IN ('submitted', 'delivered')
242
- `).run(JSON.stringify(replayedFrom), row.tell_id);
243
- }
244
- return rows.map((row) => row.tell_id);
245
- });
246
- }