@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.
Files changed (54) hide show
  1. package/build/.tsbuildinfo +1 -1
  2. package/build/agents/harness/outcome.js +10 -0
  3. package/build/cli/commands/contract/bind/handler.js +10 -4
  4. package/build/cli/commands/contract/bind/meta.js +6 -6
  5. package/build/cli/commands/contract/petition/handler.js +16 -3
  6. package/build/cli/commands/contract/petition/meta.js +4 -4
  7. package/build/cli/commands/metadata.js +3 -2
  8. package/build/cli/commands/projection/tell/handler.js +10 -2
  9. package/build/cli/commands/projection/tell/meta.js +3 -3
  10. package/build/cli/commands/task/catalog.js +2 -0
  11. package/build/cli/commands/task/log/handler.js +12 -0
  12. package/build/cli/commands/task/log/meta.js +8 -0
  13. package/build/cli/flags.js +8 -0
  14. package/build/cli/index.js +10 -6
  15. package/build/cli/parse-flags.js +6 -0
  16. package/build/cli/parse-metadata.js +1 -1
  17. package/build/cli/render/line-width.js +33 -0
  18. package/build/cli/render/path-prefix-compaction.js +88 -0
  19. package/build/cli/render/petition.js +4 -0
  20. package/build/cli/render/projection-activity.js +93 -20
  21. package/build/cli/render/shared.js +34 -12
  22. package/build/cli/render/success-response.js +2 -0
  23. package/build/cli/render/tool-presentation.js +3 -3
  24. package/build/cli/render/wait.js +68 -48
  25. package/build/cli/subagent-guard.js +3 -0
  26. package/build/cli/types.js +1 -1
  27. package/build/core/bind.js +111 -6
  28. package/build/core/draft.js +1 -1
  29. package/build/core/projection/generation/database.js +21 -2
  30. package/build/core/projection/generation/model.js +24 -3
  31. package/build/core/projection/generation/projection-generation-continuation.js +75 -10
  32. package/build/core/projection/generation/projection-generation-execution.js +4 -3
  33. package/build/core/projection/generation/projection-generation-runner.js +96 -53
  34. package/build/core/projection/generation/projection-generation-runtime.js +19 -0
  35. package/build/core/projection/generation/store.js +9 -0
  36. package/build/core/projection/generation/transitions.js +105 -3
  37. package/build/core/projection/index.js +2 -2
  38. package/build/core/projection/projection-core.js +1 -1
  39. package/build/core/projection/projection-kill.js +31 -0
  40. package/build/core/projection/projection-life-protocol.js +10 -0
  41. package/build/core/projection/projection-wait.js +53 -15
  42. package/build/core/projection/projection-wake.js +35 -3
  43. package/build/core/projection/tell/database.js +18 -0
  44. package/build/core/projection/tell/model.js +1 -0
  45. package/build/core/projection/tell/store.js +102 -55
  46. package/build/core/registry.js +82 -69
  47. package/build/core/scope.js +9 -9
  48. package/build/core/task/index.js +2 -2
  49. package/build/core/task/task-contract.js +18 -0
  50. package/build/core/task/task-git-store.js +50 -0
  51. package/build/generated/version.js +2 -2
  52. package/package.json +1 -1
  53. package/skills/keiyaku-akuma/SKILL.md +10 -0
  54. 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("verdict 'killed' is reserved to runnerKilledIfRequested and settleKillIfRunnerDead");
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, demoteSubmittedTellsForReplay, listTellIds, markTellConsumed, markTellSubmitted, readProjectionTellCounts, readTellOriginal, readTellSubmitted, writeInboxTell, } from "./tell/store.js";
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, demoteSubmittedTellsForReplay, listTellIds, markTellConsumed, markTellSubmitted, readTellOriginal, readTellSubmitted, snapshotPendingTells, writeInboxTell, } from "./tell/store.js";
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 { selectCurrentGeneration } from "./projection-life-protocol.js";
11
- import { readProjectionTellCounts } from "./tell/store.js";
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
- tellsWaiting: waitingTellCount(projectionDirectory),
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 observation = observeProjectionLifeSnapshot(input.projectionDirectory, {
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
- const current = observation.compatibility === "current" && observation.records
179
- ? selectCurrentGeneration(observation.records)
180
- : null;
181
- const life = observation.compatibility === "current"
182
- ? observation.life
183
- : { phase: "unknown", diagnostic: observation.diagnostic };
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, 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
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 current = readCurrentGeneration(projectionDirectory);
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
- const snapshot = snapshotPendingTells(projectionDirectory);
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");