@astrosheep/keiyaku 2.9.6 → 2.9.7
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/outcome.js +10 -0
- 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/tell/handler.js +10 -2
- package/build/cli/commands/projection/tell/meta.js +3 -3
- 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/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/line-width.js +33 -0
- package/build/cli/render/path-prefix-compaction.js +88 -0
- package/build/cli/render/petition.js +4 -0
- package/build/cli/render/projection-activity.js +93 -20
- package/build/cli/render/shared.js +34 -12
- 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/core/bind.js +111 -6
- package/build/core/draft.js +1 -1
- package/build/core/projection/generation/database.js +21 -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 +4 -3
- package/build/core/projection/generation/projection-generation-runner.js +96 -53
- package/build/core/projection/generation/projection-generation-runtime.js +19 -0
- package/build/core/projection/generation/store.js +9 -0
- package/build/core/projection/generation/transitions.js +105 -3
- package/build/core/projection/index.js +2 -2
- package/build/core/projection/projection-core.js +1 -1
- package/build/core/projection/projection-kill.js +31 -0
- package/build/core/projection/projection-life-protocol.js +10 -0
- package/build/core/projection/projection-wait.js +53 -15
- package/build/core/projection/projection-wake.js +35 -3
- 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/task/index.js +2 -2
- package/build/core/task/task-contract.js +18 -0
- package/build/core/task/task-git-store.js +50 -0
- package/build/generated/version.js +2 -2
- package/package.json +1 -1
- package/skills/keiyaku-akuma/SKILL.md +10 -0
- package/skills/keiyaku-workflow/SKILL.md +76 -7
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { appendRecord, readValidatedHistory, writeTransition, } from "./database.js";
|
|
2
|
-
import { ProjectionGenerationStoreError, VERDICTS, assertExecutionId, assertIsoTimestamp, assertNonNegativeSafeInteger, assertPositiveSafeInteger, cloneFacts, validateAdoptionFacts, validateKillIntentFacts, validateLaunchFacts, validateTellFence, validateVerdictFacts, } from "./model.js";
|
|
2
|
+
import { ProjectionGenerationStoreError, VERDICTS, assertExecutionId, assertIsoTimestamp, assertNonNegativeSafeInteger, assertPositiveSafeInteger, cloneFacts, validateAdoptionFacts, validateInterruptIntentFacts, validateKillIntentFacts, validateLaunchFacts, validateTellFence, validateVerdictFacts, } from "./model.js";
|
|
3
3
|
function committed(records) {
|
|
4
4
|
return { status: "committed", records };
|
|
5
5
|
}
|
|
@@ -51,10 +51,32 @@ export function requestKillIfOpen(database, readOnly, input) {
|
|
|
51
51
|
return rejected("already-terminal", current);
|
|
52
52
|
if (current.killIntent)
|
|
53
53
|
return rejected("kill-requested", current);
|
|
54
|
+
if (current.interruptIntent)
|
|
55
|
+
return rejected("interrupt-requested", current);
|
|
54
56
|
const facts = validateKillIntentFacts({ operatorAction: "kill", killedAt }, "kill-intent facts");
|
|
55
57
|
return committed([appendRecord(database, "kill-intent", executionId, facts, history.records.length + 1)]);
|
|
56
58
|
});
|
|
57
59
|
}
|
|
60
|
+
export function requestInterruptIfOpen(database, readOnly, input) {
|
|
61
|
+
const executionId = assertExecutionId(input.executionId);
|
|
62
|
+
const interruptedAt = assertIsoTimestamp(input.interruptedAt, "interruptedAt");
|
|
63
|
+
return writeTransition(database, readOnly, () => {
|
|
64
|
+
const history = readValidatedHistory(database);
|
|
65
|
+
const current = history.current;
|
|
66
|
+
if (!current)
|
|
67
|
+
return rejected("no-current-launch", null);
|
|
68
|
+
if (current.launch.executionId !== executionId)
|
|
69
|
+
return rejected("not-current", current);
|
|
70
|
+
if (current.verdict)
|
|
71
|
+
return rejected("already-terminal", current);
|
|
72
|
+
if (current.killIntent)
|
|
73
|
+
return rejected("kill-requested", current);
|
|
74
|
+
if (current.interruptIntent)
|
|
75
|
+
return rejected("interrupt-requested", current);
|
|
76
|
+
const facts = validateInterruptIntentFacts({ interruptedAt }, "interrupt-intent facts");
|
|
77
|
+
return committed([appendRecord(database, "interrupt-intent", executionId, facts, history.records.length + 1)]);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
58
80
|
export function runnerKilledIfRequested(database, readOnly, input) {
|
|
59
81
|
const executionId = assertExecutionId(input.executionId);
|
|
60
82
|
const callerFacts = cloneFacts(input.facts ?? {}, "killed verdict facts");
|
|
@@ -87,6 +109,84 @@ export function runnerKilledIfRequested(database, readOnly, input) {
|
|
|
87
109
|
return committed([appendRecord(database, "verdict", executionId, facts, history.records.length + 1)]);
|
|
88
110
|
});
|
|
89
111
|
}
|
|
112
|
+
export function runnerInterruptedIfRequested(database, readOnly, input) {
|
|
113
|
+
const executionId = assertExecutionId(input.executionId);
|
|
114
|
+
const callerFacts = cloneFacts(input.facts ?? {}, "interrupted verdict facts");
|
|
115
|
+
for (const reserved of ["state", "interruptIntentSeq", "interruptedAt", "terminalAt"]) {
|
|
116
|
+
if (reserved in callerFacts) {
|
|
117
|
+
throw new ProjectionGenerationStoreError(`interrupted verdict facts.${reserved} is reserved`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const terminalAt = input.terminalAt === undefined
|
|
121
|
+
? undefined
|
|
122
|
+
: assertIsoTimestamp(input.terminalAt, "terminalAt");
|
|
123
|
+
return writeTransition(database, readOnly, () => {
|
|
124
|
+
const history = readValidatedHistory(database);
|
|
125
|
+
const current = history.current;
|
|
126
|
+
if (!current)
|
|
127
|
+
return rejected("no-current-launch", null);
|
|
128
|
+
if (current.launch.executionId !== executionId)
|
|
129
|
+
return rejected("not-current", current);
|
|
130
|
+
if (current.verdict)
|
|
131
|
+
return rejected("already-terminal", current);
|
|
132
|
+
if (!current.interruptIntent)
|
|
133
|
+
return rejected("kill-not-requested", current);
|
|
134
|
+
const facts = validateVerdictFacts({
|
|
135
|
+
...callerFacts,
|
|
136
|
+
state: "interrupted",
|
|
137
|
+
interruptIntentSeq: current.interruptIntent.seq,
|
|
138
|
+
interruptedAt: current.interruptIntent.facts.interruptedAt,
|
|
139
|
+
...(terminalAt ? { terminalAt } : {}),
|
|
140
|
+
}, "interrupted verdict facts");
|
|
141
|
+
return committed([appendRecord(database, "verdict", executionId, facts, history.records.length + 1)]);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/** Publish a stopped provider outcome while atomically honoring any stop intent that won first. */
|
|
145
|
+
export function runnerVerdictIfOpen(database, readOnly, input) {
|
|
146
|
+
const executionId = assertExecutionId(input.executionId);
|
|
147
|
+
if (!VERDICTS.has(input.verdict))
|
|
148
|
+
throw new ProjectionGenerationStoreError(`unknown verdict '${String(input.verdict)}'`);
|
|
149
|
+
if (input.verdict === "killed" || input.verdict === "interrupted" || input.verdict === "launch-failed" || input.verdict === "lost-runner") {
|
|
150
|
+
throw new ProjectionGenerationStoreError(`runner outcome verdict '${input.verdict}' is not publishable`);
|
|
151
|
+
}
|
|
152
|
+
const callerFacts = cloneFacts(input.facts ?? {}, "runner verdict facts");
|
|
153
|
+
const terminalAt = assertIsoTimestamp(input.terminalAt, "terminalAt");
|
|
154
|
+
return writeTransition(database, readOnly, () => {
|
|
155
|
+
const history = readValidatedHistory(database);
|
|
156
|
+
const current = history.current;
|
|
157
|
+
if (!current)
|
|
158
|
+
return rejected("no-current-launch", null);
|
|
159
|
+
if (current.launch.executionId !== executionId)
|
|
160
|
+
return rejected("not-current", current);
|
|
161
|
+
if (current.verdict)
|
|
162
|
+
return rejected("already-terminal", current);
|
|
163
|
+
let facts;
|
|
164
|
+
if (current.interruptIntent) {
|
|
165
|
+
const { state: _state, interruptIntentSeq: _seq, interruptedAt: _at, terminalAt: _terminal, ...rest } = callerFacts;
|
|
166
|
+
facts = validateVerdictFacts({
|
|
167
|
+
...rest,
|
|
168
|
+
state: "interrupted",
|
|
169
|
+
interruptIntentSeq: current.interruptIntent.seq,
|
|
170
|
+
interruptedAt: current.interruptIntent.facts.interruptedAt,
|
|
171
|
+
terminalAt,
|
|
172
|
+
}, "runner interrupted verdict facts");
|
|
173
|
+
}
|
|
174
|
+
else if (current.killIntent && !(input.verdict === "failed" && callerFacts.operatorAction === "kill")) {
|
|
175
|
+
const { state: _state, killIntentSeq: _seq, killedAt: _at, terminalAt: _terminal, ...rest } = callerFacts;
|
|
176
|
+
facts = validateVerdictFacts({
|
|
177
|
+
...rest,
|
|
178
|
+
state: "killed",
|
|
179
|
+
killIntentSeq: current.killIntent.seq,
|
|
180
|
+
killedAt: current.killIntent.facts.killedAt,
|
|
181
|
+
terminalAt,
|
|
182
|
+
}, "runner killed verdict facts");
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
facts = validateVerdictFacts({ ...callerFacts, state: input.verdict, terminalAt }, "runner verdict facts");
|
|
186
|
+
}
|
|
187
|
+
return committed([appendRecord(database, "verdict", executionId, facts, history.records.length + 1)]);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
90
190
|
export function settleKillIfRunnerDead(database, readOnly, input) {
|
|
91
191
|
const executionId = assertExecutionId(input.executionId);
|
|
92
192
|
const releasedLockEvidence = cloneFacts(input.releasedLockEvidence, "released runner-lock evidence");
|
|
@@ -128,8 +228,8 @@ export function verdictIfOpen(database, readOnly, input) {
|
|
|
128
228
|
const executionId = assertExecutionId(input.executionId);
|
|
129
229
|
if (!VERDICTS.has(input.verdict))
|
|
130
230
|
throw new ProjectionGenerationStoreError(`unknown verdict '${String(input.verdict)}'`);
|
|
131
|
-
if (input.verdict === "killed") {
|
|
132
|
-
throw new ProjectionGenerationStoreError(
|
|
231
|
+
if (input.verdict === "killed" || input.verdict === "interrupted") {
|
|
232
|
+
throw new ProjectionGenerationStoreError(`verdict '${input.verdict}' is reserved to its runner-owned transition`);
|
|
133
233
|
}
|
|
134
234
|
return writeTransition(database, readOnly, () => {
|
|
135
235
|
const history = readValidatedHistory(database);
|
|
@@ -259,6 +359,8 @@ export function handoff(database, readOnly, input) {
|
|
|
259
359
|
return rejected("already-terminal", current);
|
|
260
360
|
if (current.killIntent)
|
|
261
361
|
return rejected("kill-requested", current);
|
|
362
|
+
if (current.interruptIntent)
|
|
363
|
+
return rejected("interrupt-requested", current);
|
|
262
364
|
if (!current.adoption)
|
|
263
365
|
return rejected("not-adopted", current);
|
|
264
366
|
if (history.records.some((record) => record.kind === "launch" && record.executionId === successorExecutionId)) {
|
|
@@ -9,7 +9,7 @@ export { assertProjectionAlias, isProjectionAlias, moveProjectionAlias, projecti
|
|
|
9
9
|
export { resolveProjectionCoordinate } from "./projection-coordinate.js";
|
|
10
10
|
export { appendGenerationEvent, executionEventsPath, isProjectionId, isPublishedProjection, isoFromNowMs, projectionDir, projectionRoot, readProjectionEvents, } from "./projection-core.js";
|
|
11
11
|
export { readProjectionIdentity } from "./projection-identity.js";
|
|
12
|
-
export { killProjectionGeneration } from "./projection-kill.js";
|
|
12
|
+
export { interruptProjectionGeneration, killProjectionGeneration } from "./projection-kill.js";
|
|
13
13
|
export { observeProjectionLife, observeProjectionLifeSnapshot, } from "./projection-life-observer.js";
|
|
14
14
|
export { mintProjection, mintRepositoryProjection, ProjectionMintCollisionError, } from "./projection-mint.js";
|
|
15
15
|
export { acquireProjectionRunnerLock, initializeProjectionRunnerLockDatabase, isProjectionRunnerLockPlatformSupported, observeProjectionRunnerLock, projectionRunnerLockPath, } from "./projection-runner-lock.js";
|
|
@@ -24,4 +24,4 @@ export { projectionGenerationRunnerArgv, spawnProjectionGenerationRunner, } from
|
|
|
24
24
|
export { runProjectionGeneration, } from "./generation/projection-generation-runner.js";
|
|
25
25
|
export { executeProjectionGeneration } from "./generation/projection-generation-execution.js";
|
|
26
26
|
export { runProjectionGenerationRuntime } from "./generation/projection-generation-runtime.js";
|
|
27
|
-
export { claimFencedTells, claimTellToInflight,
|
|
27
|
+
export { claimFencedTells, claimTellToInflight, listTellIds, markTellConsumed, markTellSubmitted, readPendingTellWindow, readProjectionTellCounts, readTellOriginal, readTellSubmitted, writeInboxTell, } from "./tell/store.js";
|
|
@@ -5,7 +5,7 @@ import { projectionCoordinateRoot } from "./projection-coordinate.js";
|
|
|
5
5
|
import { ProjectionStateError, } from "../atomic-publish.js";
|
|
6
6
|
export { atomicPublishFile, ProjectionStateError, randomHex8 } from "../atomic-publish.js";
|
|
7
7
|
export { isTellCausallyConsumed, } from "./tell/model.js";
|
|
8
|
-
export { claimAllInbox, claimFencedTells, claimTellToInflight,
|
|
8
|
+
export { claimAllInbox, claimFencedTells, claimTellToInflight, listTellIds, markTellConsumed, markTellSubmitted, readTellOriginal, readTellSubmitted, snapshotPendingTells, writeInboxTell, } from "./tell/store.js";
|
|
9
9
|
/** Relative segments for `.keiyaku/projection`. */
|
|
10
10
|
export const PROJECTION_ROOT_SEGMENTS = [".keiyaku", "projection"];
|
|
11
11
|
export const PROJECTION_ID_PATTERN = /^[a-zA-Z0-9]+(?:[.-][a-zA-Z0-9]+)*\/[0-9a-f]{8}$/;
|
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
import { openProjectionGenerationStore } from "./generation/store.js";
|
|
2
2
|
import { observeProjectionRunnerLock } from "./projection-runner-lock.js";
|
|
3
|
+
/** The runner observes this durable intent and owns native termination. */
|
|
4
|
+
export function interruptProjectionGeneration(projectionDirectory, nowMs = Date.now()) {
|
|
5
|
+
const store = openProjectionGenerationStore(projectionDirectory);
|
|
6
|
+
try {
|
|
7
|
+
for (;;) {
|
|
8
|
+
const current = store.readCurrentGeneration();
|
|
9
|
+
if (!current)
|
|
10
|
+
return { status: "already-closed" };
|
|
11
|
+
if (current.verdict)
|
|
12
|
+
return { status: "already-closed", executionId: current.launch.executionId };
|
|
13
|
+
const executionId = current.launch.executionId;
|
|
14
|
+
if (!current.adoption || observeProjectionRunnerLock(projectionDirectory).state !== "held") {
|
|
15
|
+
return { status: "already-closed", executionId };
|
|
16
|
+
}
|
|
17
|
+
const request = store.requestInterruptIfOpen({
|
|
18
|
+
executionId,
|
|
19
|
+
interruptedAt: new Date(nowMs).toISOString(),
|
|
20
|
+
});
|
|
21
|
+
if (request.status === "committed")
|
|
22
|
+
return { status: "accepted", executionId };
|
|
23
|
+
if (request.reason === "not-current")
|
|
24
|
+
continue;
|
|
25
|
+
if (request.reason === "interrupt-requested")
|
|
26
|
+
return { status: "accepted", executionId };
|
|
27
|
+
return { status: "already-closed", executionId };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
store.close();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
3
34
|
function settleReleasedLeash(store, projectionDirectory, executionId) {
|
|
4
35
|
const observation = observeProjectionRunnerLock(projectionDirectory);
|
|
5
36
|
if (observation.state === "held" || observation.state === "unknown") {
|
|
@@ -4,6 +4,7 @@ export const GENERATION_VERDICT_STATES = [
|
|
|
4
4
|
"launch-failed",
|
|
5
5
|
"lost-runner",
|
|
6
6
|
"dismissed",
|
|
7
|
+
"interrupted",
|
|
7
8
|
"killed",
|
|
8
9
|
];
|
|
9
10
|
function asLaunch(record) {
|
|
@@ -21,6 +22,11 @@ function asKillIntent(record) {
|
|
|
21
22
|
? record
|
|
22
23
|
: undefined;
|
|
23
24
|
}
|
|
25
|
+
function asInterruptIntent(record) {
|
|
26
|
+
return record.kind === "interrupt-intent"
|
|
27
|
+
? record
|
|
28
|
+
: undefined;
|
|
29
|
+
}
|
|
24
30
|
function asVerdict(record) {
|
|
25
31
|
return record.kind === "verdict"
|
|
26
32
|
? record
|
|
@@ -41,18 +47,21 @@ export function selectCurrentGeneration(records) {
|
|
|
41
47
|
return null;
|
|
42
48
|
let adoption;
|
|
43
49
|
let killIntent;
|
|
50
|
+
let interruptIntent;
|
|
44
51
|
let verdict;
|
|
45
52
|
for (const record of records) {
|
|
46
53
|
if (record.seq <= launch.seq || record.executionId !== launch.executionId)
|
|
47
54
|
continue;
|
|
48
55
|
adoption ??= asAdoption(record);
|
|
49
56
|
killIntent ??= asKillIntent(record);
|
|
57
|
+
interruptIntent ??= asInterruptIntent(record);
|
|
50
58
|
verdict ??= asVerdict(record);
|
|
51
59
|
}
|
|
52
60
|
return {
|
|
53
61
|
launch,
|
|
54
62
|
...(adoption ? { adoption } : {}),
|
|
55
63
|
...(killIntent ? { killIntent } : {}),
|
|
64
|
+
...(interruptIntent ? { interruptIntent } : {}),
|
|
56
65
|
...(verdict ? { verdict } : {}),
|
|
57
66
|
};
|
|
58
67
|
}
|
|
@@ -67,6 +76,7 @@ function phaseForVerdict(state) {
|
|
|
67
76
|
switch (state) {
|
|
68
77
|
case "completed": return "done";
|
|
69
78
|
case "dismissed": return "dismissed";
|
|
79
|
+
case "interrupted": return "done";
|
|
70
80
|
case "killed": return "killed";
|
|
71
81
|
case "lost-runner": return "lost";
|
|
72
82
|
case "failed":
|
|
@@ -7,10 +7,12 @@ import { RESPONSE_ARTIFACT_ID_RE } from "../response-artifact-id.js";
|
|
|
7
7
|
import { extractTerminalTimestamp } from "./generation/model.js";
|
|
8
8
|
import { PROJECTION_ADOPTION_TIMEOUT_MS } from "./generation/projection-generation-launcher.js";
|
|
9
9
|
import { observeProjectionLifeSnapshot } from "./projection-life-observer.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { deriveProjectionLife } from "./projection-life-protocol.js";
|
|
11
|
+
import { readProjectionGenerationContinuationSnapshot, } from "./generation/projection-generation-continuation.js";
|
|
12
12
|
import { projectTerminalFailure, } from "./projection-terminal-failure.js";
|
|
13
13
|
import { invalidActivityEventDiagnostic, selectValidStoredAgentEvents, } from "../stored-agent-event.js";
|
|
14
|
+
import { tellProjection } from "./projection-wake.js";
|
|
15
|
+
import { FlowError } from "../../flow-error.js";
|
|
14
16
|
/** Return the commission-bound projection IDs visible at one admission instant. */
|
|
15
17
|
export async function snapshotCommissionProjectionIds(cwd, commissionId) {
|
|
16
18
|
const root = await projectionRoot(cwd);
|
|
@@ -73,10 +75,6 @@ const WAIT_POLL_MS = 100;
|
|
|
73
75
|
function sleep(ms) {
|
|
74
76
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
75
77
|
}
|
|
76
|
-
function waitingTellCount(projectionDirectory) {
|
|
77
|
-
const counts = readProjectionTellCounts(projectionDirectory);
|
|
78
|
-
return counts.inbox + counts.inflight + counts.submitted + counts.delivered;
|
|
79
|
-
}
|
|
80
78
|
function latestValidEventAgeMs(events, nowMs) {
|
|
81
79
|
let latest;
|
|
82
80
|
for (const event of events) {
|
|
@@ -125,7 +123,7 @@ function outcomeText(current) {
|
|
|
125
123
|
return undefined;
|
|
126
124
|
}
|
|
127
125
|
function buildSnapshot(input) {
|
|
128
|
-
const { projectionDirectory, pact, phase, current, events, malformedEvents, nowMs } = input;
|
|
126
|
+
const { projectionDirectory, pact, phase, current, events, malformedEvents, nowMs, tells } = input;
|
|
129
127
|
const anchorAt = current?.verdict ? timestampFact(current, nowMs) : new Date(nowMs).toISOString();
|
|
130
128
|
const anchorMs = Date.parse(anchorAt);
|
|
131
129
|
const mintedAtMs = Date.parse(pact.mintedAt);
|
|
@@ -142,7 +140,7 @@ function buildSnapshot(input) {
|
|
|
142
140
|
? Math.max(0, anchorMs - mintedAtMs)
|
|
143
141
|
: 0,
|
|
144
142
|
...(activityAgeMs !== undefined ? { activityAgeMs } : {}),
|
|
145
|
-
|
|
143
|
+
tells,
|
|
146
144
|
activity: foldPublicProjectionActivity(events),
|
|
147
145
|
...(invalidActivity ? { eventDiagnostics: [invalidActivity] } : {}),
|
|
148
146
|
workspaceRoot: pact.workspaceRoot,
|
|
@@ -168,25 +166,64 @@ export async function waitForProjection(input) {
|
|
|
168
166
|
}
|
|
169
167
|
const deadline = input.deadlineMs
|
|
170
168
|
?? (input.timeoutMs === undefined ? undefined : now() + input.timeoutMs);
|
|
169
|
+
const wakeProjection = input.wakeProjection ?? ((directory, id) => tellProjection(directory, id));
|
|
170
|
+
const readContinuationSnapshot = input.readContinuationSnapshot ?? readProjectionGenerationContinuationSnapshot;
|
|
171
|
+
let wakeAttempted = false;
|
|
171
172
|
for (;;) {
|
|
172
173
|
input.signal?.throwIfAborted();
|
|
173
174
|
const observedAtMs = now();
|
|
174
|
-
const
|
|
175
|
+
const continuation = readContinuationSnapshot(input.projectionDirectory, 10);
|
|
176
|
+
const current = continuation.current;
|
|
177
|
+
let life = deriveProjectionLife({
|
|
178
|
+
records: continuation.records,
|
|
175
179
|
nowMs: observedAtMs,
|
|
176
180
|
startupTimeoutMs: PROJECTION_ADOPTION_TIMEOUT_MS,
|
|
177
181
|
});
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
if (current && !current.verdict && current.adoption) {
|
|
183
|
+
const observation = observeProjectionLifeSnapshot(input.projectionDirectory, {
|
|
184
|
+
nowMs: observedAtMs,
|
|
185
|
+
startupTimeoutMs: PROJECTION_ADOPTION_TIMEOUT_MS,
|
|
186
|
+
});
|
|
187
|
+
if (observation.compatibility === "incompatible") {
|
|
188
|
+
life = { phase: "unknown", diagnostic: observation.diagnostic };
|
|
189
|
+
}
|
|
190
|
+
else if ((observation.records?.at(-1)?.seq ?? 0) !== continuation.generationSequence) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
life = observation.life;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (!wakeAttempted
|
|
198
|
+
&& current
|
|
199
|
+
&& life.phase !== "active"
|
|
200
|
+
&& life.phase !== "minting"
|
|
201
|
+
&& life.phase !== "quiescent"
|
|
202
|
+
&& life.phase !== "unknown"
|
|
203
|
+
&& continuation.requiresWake) {
|
|
204
|
+
wakeAttempted = true;
|
|
205
|
+
try {
|
|
206
|
+
const wakeState = await wakeProjection(input.projectionDirectory, `${pact.akuma}/${path.basename(input.projectionDirectory)}`);
|
|
207
|
+
if (wakeState === "active" || wakeState === "woken")
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
if (!(error instanceof FlowError) || error.code !== "PROJECTION_WAKE_FAILED")
|
|
212
|
+
throw error;
|
|
213
|
+
// The wake path persisted any launch/adoption failure. Refresh once so
|
|
214
|
+
// this invocation reports that typed truth without retrying in a loop.
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
184
218
|
const verdict = current ? verdictFromCurrent(current, observedAtMs) : undefined;
|
|
185
219
|
const terminal = verdict !== undefined;
|
|
186
220
|
const terminalAtMs = verdict === undefined ? undefined : Date.parse(verdict.at);
|
|
187
221
|
const terminalWithinDeadline = terminal && (input.deadlineMs === undefined
|
|
188
222
|
|| (typeof terminalAtMs === "number" && Number.isFinite(terminalAtMs) && terminalAtMs < input.deadlineMs));
|
|
189
223
|
if (terminalWithinDeadline || (deadline !== undefined && observedAtMs >= deadline)) {
|
|
224
|
+
const revalidated = readContinuationSnapshot(input.projectionDirectory, 10);
|
|
225
|
+
if (revalidated.generationSequence !== continuation.generationSequence)
|
|
226
|
+
continue;
|
|
190
227
|
const stored = current
|
|
191
228
|
? readGenerationEvents(input.projectionDirectory, current.launch.executionId)
|
|
192
229
|
: [];
|
|
@@ -199,6 +236,7 @@ export async function waitForProjection(input) {
|
|
|
199
236
|
events,
|
|
200
237
|
malformedEvents: malformed,
|
|
201
238
|
nowMs: observedAtMs,
|
|
239
|
+
tells: revalidated.tells,
|
|
202
240
|
});
|
|
203
241
|
if (terminalWithinDeadline && current) {
|
|
204
242
|
const text = outcomeText(current);
|
|
@@ -2,13 +2,42 @@ 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
10
|
import { observeProjectionRunnerLock } from "./projection-runner-lock.js";
|
|
11
11
|
import { setTimeout as delay } from "node:timers/promises";
|
|
12
|
+
/** Persist an asynchronous interrupt wake failure at the generation boundary. */
|
|
13
|
+
export function recordInterruptWakeFailure(projectionDirectory, cause, nowMs = Date.now()) {
|
|
14
|
+
const continuation = readProjectionGenerationContinuationSnapshot(projectionDirectory, 10);
|
|
15
|
+
const current = continuation.current;
|
|
16
|
+
if (!current || !current.verdict || current.verdict.facts.state !== "interrupted")
|
|
17
|
+
return;
|
|
18
|
+
const snapshot = continuation.successor;
|
|
19
|
+
const executionId = createProjectionExecutionId(nowMs);
|
|
20
|
+
const facts = successorFacts(current, snapshot, nowMs, false);
|
|
21
|
+
const store = openProjectionGenerationStore(projectionDirectory);
|
|
22
|
+
try {
|
|
23
|
+
const launch = store.launchIfSettled({ executionId, facts });
|
|
24
|
+
if (launch.status !== "committed")
|
|
25
|
+
return;
|
|
26
|
+
store.verdictIfOpen({
|
|
27
|
+
executionId,
|
|
28
|
+
verdict: "launch-failed",
|
|
29
|
+
facts: {
|
|
30
|
+
stage: "interrupt-wake",
|
|
31
|
+
cause,
|
|
32
|
+
tellRetained: true,
|
|
33
|
+
terminalAt: new Date(nowMs).toISOString(),
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
store.close();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
12
41
|
function incompatibleProjectionError(projectionId) {
|
|
13
42
|
const subject = projectionId ? `Projection '${projectionId}'` : "Projection";
|
|
14
43
|
return new FlowError("PROJECTION_INCOMPATIBLE", `${subject} has incompatible historical storage; keiyaku status --all can inspect it, but tell cannot change it.`);
|
|
@@ -148,12 +177,15 @@ export async function tellProjection(projectionDirectory, projectionId, effort,
|
|
|
148
177
|
if (life.phase === "unknown") {
|
|
149
178
|
return "pending";
|
|
150
179
|
}
|
|
151
|
-
const
|
|
180
|
+
const continuation = readProjectionGenerationContinuationSnapshot(projectionDirectory, 10);
|
|
181
|
+
const current = continuation.current;
|
|
152
182
|
if (!current)
|
|
153
183
|
return "pending";
|
|
154
184
|
if (current.killIntent && !current.verdict)
|
|
155
185
|
return "pending";
|
|
156
|
-
|
|
186
|
+
if (!continuation.requiresWake)
|
|
187
|
+
return "pending";
|
|
188
|
+
const snapshot = continuation.successor;
|
|
157
189
|
const successorExecutionId = createProjectionExecutionId(nowMs);
|
|
158
190
|
let facts;
|
|
159
191
|
try {
|
|
@@ -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");
|