@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.
- package/build/.tsbuildinfo +1 -1
- package/build/agents/harness/event-persistence.js +7 -5
- package/build/agents/harness/events.js +3 -2
- package/build/agents/harness/outcome.js +10 -0
- package/build/agents/providers/codex-app-server/adapter.js +6 -1
- package/build/agents/providers/codex-app-server/session.js +8 -7
- package/build/agents/selector.js +12 -1
- package/build/cli/commands/akuma/view/handler.js +3 -11
- package/build/cli/commands/contract/amend/handler.js +1 -1
- package/build/cli/commands/contract/amend/meta.js +4 -4
- package/build/cli/commands/contract/bind/handler.js +10 -4
- package/build/cli/commands/contract/bind/meta.js +6 -6
- package/build/cli/commands/contract/petition/handler.js +16 -3
- package/build/cli/commands/contract/petition/meta.js +4 -4
- package/build/cli/commands/metadata.js +3 -2
- package/build/cli/commands/projection/status/handler.js +5 -4
- package/build/cli/commands/projection/status/meta.js +2 -2
- package/build/cli/commands/projection/tell/handler.js +10 -2
- package/build/cli/commands/projection/tell/meta.js +3 -3
- package/build/cli/commands/task/add/meta.js +9 -1
- package/build/cli/commands/task/catalog.js +2 -0
- package/build/cli/commands/task/log/handler.js +12 -0
- package/build/cli/commands/task/log/meta.js +8 -0
- package/build/cli/commands/task/shared.js +2 -1
- package/build/cli/completion.js +8 -0
- package/build/cli/flags.js +8 -0
- package/build/cli/index.js +10 -6
- package/build/cli/parse-flags.js +6 -0
- package/build/cli/parse-metadata.js +1 -1
- package/build/cli/render/kanshi.js +2 -2
- package/build/cli/render/line-width.js +33 -0
- package/build/cli/render/path-prefix-compaction.js +131 -0
- package/build/cli/render/petition.js +4 -0
- package/build/cli/render/projection-activity.js +103 -21
- package/build/cli/render/shared.js +38 -15
- package/build/cli/render/status.js +8 -5
- package/build/cli/render/success-response.js +2 -0
- package/build/cli/render/tool-presentation.js +3 -3
- package/build/cli/render/wait.js +68 -48
- package/build/cli/subagent-guard.js +3 -0
- package/build/cli/types.js +1 -1
- package/build/config/settings/disease.js +4 -4
- package/build/config/settings/loader.js +44 -21
- package/build/core/addressing.js +40 -9
- package/build/core/amend.js +21 -5
- package/build/core/bind.js +111 -6
- package/build/core/call/context.js +19 -3
- package/build/core/call/execution.js +43 -20
- package/build/core/draft.js +1 -1
- package/build/core/ledger-batch.js +194 -0
- package/build/core/projection/generation/database.js +43 -2
- package/build/core/projection/generation/model.js +24 -3
- package/build/core/projection/generation/projection-generation-continuation.js +75 -10
- package/build/core/projection/generation/projection-generation-execution.js +46 -37
- package/build/core/projection/generation/projection-generation-launcher.js +148 -20
- package/build/core/projection/generation/projection-generation-process.js +3 -1
- package/build/core/projection/generation/projection-generation-runner.js +145 -66
- package/build/core/projection/generation/projection-generation-runtime.js +93 -11
- package/build/core/projection/generation/store.js +26 -1
- package/build/core/projection/generation/transitions.js +193 -14
- package/build/core/projection/index.js +5 -5
- package/build/core/projection/projection-core.js +1 -1
- package/build/core/projection/projection-kill.js +53 -10
- package/build/core/projection/projection-life-protocol.js +10 -0
- package/build/core/projection/projection-runner-lock.js +177 -37
- package/build/core/projection/projection-status.js +143 -55
- package/build/core/projection/projection-wait.js +53 -15
- package/build/core/projection/projection-wake.js +177 -46
- package/build/core/projection/tell/database.js +18 -0
- package/build/core/projection/tell/model.js +1 -0
- package/build/core/projection/tell/store.js +102 -55
- package/build/core/registry.js +82 -69
- package/build/core/scope.js +9 -9
- package/build/core/status/board.js +42 -4
- package/build/core/status/drift.js +21 -5
- package/build/core/status/ledger-batch.js +1 -158
- package/build/core/task/index.js +2 -2
- package/build/core/task/task-contract.js +18 -0
- package/build/core/task/task-git-runtime.js +8 -10
- package/build/core/task/task-git-store.js +55 -3
- package/build/core/worktree-path.js +39 -25
- package/build/flow-error.js +1 -1
- package/build/generated/version.js +2 -2
- package/build/git/refs.js +47 -1
- package/package.json +1 -1
- package/skills/keiyaku-akuma/SKILL.md +28 -0
- 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,
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
219
|
+
catch (error) {
|
|
220
|
+
if (error instanceof ProjectionRunnerLockUnavailableError)
|
|
221
|
+
return "pending";
|
|
185
222
|
return "pending";
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
284
|
+
catch (error) {
|
|
285
|
+
if (error instanceof ProjectionRunnerLockUnavailableError)
|
|
286
|
+
return "active";
|
|
205
287
|
return "pending";
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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 (
|
|
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
|
-
}
|