@astrosheep/keiyaku 2.9.7 → 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 (52) 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/providers/codex-app-server/adapter.js +6 -1
  5. package/build/agents/providers/codex-app-server/session.js +8 -7
  6. package/build/agents/selector.js +12 -1
  7. package/build/cli/commands/akuma/view/handler.js +3 -11
  8. package/build/cli/commands/contract/amend/handler.js +1 -1
  9. package/build/cli/commands/contract/amend/meta.js +4 -4
  10. package/build/cli/commands/projection/status/handler.js +5 -4
  11. package/build/cli/commands/projection/status/meta.js +2 -2
  12. package/build/cli/commands/task/add/meta.js +9 -1
  13. package/build/cli/commands/task/shared.js +2 -1
  14. package/build/cli/completion.js +8 -0
  15. package/build/cli/render/kanshi.js +2 -2
  16. package/build/cli/render/path-prefix-compaction.js +119 -76
  17. package/build/cli/render/projection-activity.js +10 -1
  18. package/build/cli/render/shared.js +8 -7
  19. package/build/cli/render/status.js +8 -5
  20. package/build/cli/render/wait.js +1 -1
  21. package/build/config/settings/disease.js +4 -4
  22. package/build/config/settings/loader.js +44 -21
  23. package/build/core/addressing.js +40 -9
  24. package/build/core/amend.js +21 -5
  25. package/build/core/call/context.js +19 -3
  26. package/build/core/call/execution.js +43 -20
  27. package/build/core/ledger-batch.js +194 -0
  28. package/build/core/projection/generation/database.js +22 -0
  29. package/build/core/projection/generation/projection-generation-execution.js +45 -37
  30. package/build/core/projection/generation/projection-generation-launcher.js +148 -20
  31. package/build/core/projection/generation/projection-generation-process.js +3 -1
  32. package/build/core/projection/generation/projection-generation-runner.js +76 -40
  33. package/build/core/projection/generation/projection-generation-runtime.js +82 -19
  34. package/build/core/projection/generation/store.js +17 -1
  35. package/build/core/projection/generation/transitions.js +89 -12
  36. package/build/core/projection/index.js +3 -3
  37. package/build/core/projection/projection-kill.js +22 -10
  38. package/build/core/projection/projection-runner-lock.js +177 -37
  39. package/build/core/projection/projection-status.js +143 -55
  40. package/build/core/projection/projection-wake.js +171 -72
  41. package/build/core/status/board.js +42 -4
  42. package/build/core/status/drift.js +21 -5
  43. package/build/core/status/ledger-batch.js +1 -158
  44. package/build/core/task/task-git-runtime.js +8 -10
  45. package/build/core/task/task-git-store.js +5 -3
  46. package/build/core/worktree-path.js +39 -25
  47. package/build/flow-error.js +1 -1
  48. package/build/generated/version.js +2 -2
  49. package/build/git/refs.js +47 -1
  50. package/package.json +1 -1
  51. package/skills/keiyaku-akuma/SKILL.md +18 -0
  52. package/skills/keiyaku-workflow/SKILL.md +68 -13
@@ -77,6 +77,7 @@ export async function runProjectionGeneration(input) {
77
77
  let tellObserver;
78
78
  let graceTimer;
79
79
  let intentTimer;
80
+ let handle;
80
81
  const clearGraceTimer = () => {
81
82
  if (graceTimer === undefined)
82
83
  return;
@@ -84,15 +85,19 @@ export async function runProjectionGeneration(input) {
84
85
  graceTimer = undefined;
85
86
  };
86
87
  try {
88
+ runnerLock = input.runnerLock
89
+ ?? acquireRunnerLockOrObserveDuplicate(input.projectionDirectory);
90
+ if (!runnerLock)
91
+ return { status: "duplicate" };
87
92
  const initial = store.readCurrentGeneration();
88
93
  if (initial?.launch.executionId !== input.executionId
89
94
  || initial.adoption
90
95
  || initial.verdict) {
91
96
  return { status: "rejected" };
92
97
  }
93
- runnerLock = acquireRunnerLockOrObserveDuplicate(input.projectionDirectory);
94
- if (!runnerLock)
95
- return { status: "duplicate" };
98
+ tellObserver = openProjectionTellObserver(input.projectionDirectory);
99
+ // Prime before adoption so a concurrent stop write cannot become the baseline.
100
+ tellObserver.hasDataVersionChanged();
96
101
  const adoption = store.adoptionIfCurrent({
97
102
  executionId: input.executionId,
98
103
  facts: { adoptedAt: new Date(now()).toISOString() },
@@ -100,12 +105,12 @@ export async function runProjectionGeneration(input) {
100
105
  if (adoption.status !== "committed")
101
106
  return { status: "rejected" };
102
107
  adopted = true;
103
- const current = store.readCurrentGeneration();
108
+ let current = store.readCurrentGeneration();
104
109
  if (current?.launch.executionId !== input.executionId) {
105
110
  throw new Error("adopted generation stopped being current");
106
111
  }
107
- // Pre-admission kill retains its established settlement law. Interrupt
108
- // intent must enter provider execution so only an actual stop can settle it.
112
+ // Adoption is the provider-admission boundary. A kill that committed first
113
+ // settles without constructing a provider execution.
109
114
  if (current.killIntent) {
110
115
  const killed = store.runnerKilledIfRequested({ executionId: input.executionId });
111
116
  if (killed.status !== "committed")
@@ -113,8 +118,21 @@ export async function runProjectionGeneration(input) {
113
118
  settled = true;
114
119
  return { status: "completed" };
115
120
  }
116
- const handle = input.execute(current.launch);
117
- tellObserver = openProjectionTellObserver(input.projectionDirectory);
121
+ const prepared = await input.prepareExecution(current.launch);
122
+ current = store.readCurrentGeneration();
123
+ if (current?.launch.executionId !== input.executionId) {
124
+ await prepared.dispose();
125
+ throw new Error("prepared generation stopped being current");
126
+ }
127
+ if (current.killIntent) {
128
+ await prepared.dispose();
129
+ const killed = store.runnerKilledIfRequested({ executionId: input.executionId });
130
+ if (killed.status !== "committed")
131
+ return { status: "rejected" };
132
+ settled = true;
133
+ return { status: "completed" };
134
+ }
135
+ handle = prepared.start();
118
136
  const settleInterrupted = (facts = {}) => {
119
137
  const transition = store.runnerInterruptedIfRequested({
120
138
  executionId: input.executionId,
@@ -319,39 +337,40 @@ export async function runProjectionGeneration(input) {
319
337
  const tellFence = normalizeTellFence(pending.tellIds);
320
338
  if (tellFence.length > 0) {
321
339
  const successorExecutionId = createProjectionExecutionId(now());
322
- const transition = store.handoff({
323
- executionId: input.executionId,
324
- completionFacts: execution.facts,
325
- successorExecutionId,
326
- successorFacts: successorLaunchFacts(current.launch, execution.facts, pending.effort, now),
327
- tellFence,
328
- });
329
- if (transition.status !== "committed") {
330
- if (transition.reason !== "kill-requested" && transition.reason !== "interrupt-requested") {
340
+ const successorFacts = successorLaunchFacts(current.launch, execution.facts, pending.effort, now);
341
+ let transitionReason;
342
+ if (!input.startSuccessor)
343
+ return { status: "rejected" };
344
+ try {
345
+ const successorOwnership = runnerLock;
346
+ await input.startSuccessor(successorExecutionId, () => {
347
+ const transition = store.handoff({
348
+ executionId: input.executionId,
349
+ completionFacts: execution.facts,
350
+ successorExecutionId,
351
+ successorFacts,
352
+ tellFence,
353
+ });
354
+ if (transition.status !== "committed") {
355
+ transitionReason = transition.reason;
356
+ return false;
357
+ }
358
+ settled = true;
359
+ return true;
360
+ }, successorOwnership);
361
+ runnerLock = undefined;
362
+ }
363
+ catch {
364
+ if (settled)
365
+ return { status: "handed-off", successorExecutionId };
366
+ return settleRunnerOutcome(execution);
367
+ }
368
+ if (!settled) {
369
+ if (transitionReason !== "kill-requested" && transitionReason !== "interrupt-requested") {
331
370
  return { status: "rejected" };
332
371
  }
333
372
  }
334
373
  else {
335
- settled = true;
336
- // Handoff makes the predecessor terminal before life authority moves.
337
- // A held projection lock must always mean that a current runner exists,
338
- // so release it before the successor makes its one-shot acquisition.
339
- runnerLock.close();
340
- runnerLock = undefined;
341
- try {
342
- input.spawnSuccessor?.(successorExecutionId);
343
- }
344
- catch (error) {
345
- store.verdictIfOpen({
346
- executionId: successorExecutionId,
347
- verdict: "launch-failed",
348
- facts: {
349
- stage: "spawn",
350
- detail: `successor runner spawn failed: ${errorDetail(error)}`,
351
- terminalAt: new Date(now()).toISOString(),
352
- },
353
- });
354
- }
355
374
  return { status: "handed-off", successorExecutionId };
356
375
  }
357
376
  }
@@ -359,20 +378,37 @@ export async function runProjectionGeneration(input) {
359
378
  }
360
379
  catch (error) {
361
380
  if (adopted && !settled) {
362
- store.verdictIfOpen({
381
+ let cleanupError;
382
+ if (handle) {
383
+ try {
384
+ await handle.abort("force");
385
+ }
386
+ catch (failure) {
387
+ cleanupError = failure;
388
+ }
389
+ }
390
+ const live = store.readCurrentGeneration();
391
+ const detail = error instanceof Error ? error.message : String(error);
392
+ store.runnerVerdictIfOpen({
363
393
  executionId: input.executionId,
364
394
  verdict: "failed",
365
395
  facts: {
366
396
  failedAt: new Date(now()).toISOString(),
367
- detail: error instanceof Error ? error.message : String(error),
397
+ detail,
398
+ ...(cleanupError ? {
399
+ ...(live?.killIntent ? { operatorAction: "kill" } : {}),
400
+ diagnostic: "cleanup-escalation-failed",
401
+ cleanupDetail: errorDetail(cleanupError),
402
+ } : {}),
368
403
  error: {
369
404
  version: OUTCOME_ERROR_VERSION,
370
405
  code: "SUBAGENT_EXEC_ERROR",
371
- message: error instanceof Error ? error.message : String(error),
406
+ message: detail,
372
407
  timeoutMs: null,
373
408
  exitCode: null,
374
409
  },
375
410
  },
411
+ terminalAt: new Date(now()).toISOString(),
376
412
  });
377
413
  }
378
414
  throw error;
@@ -1,34 +1,97 @@
1
- import { executeProjectionGeneration } from "./projection-generation-execution.js";
2
- import { spawnProjectionGenerationRunner } from "./projection-generation-process.js";
1
+ import { prepareProjectionGenerationExecution } from "./projection-generation-execution.js";
2
+ import { PROJECTION_RUNNER_BOOTSTRAP_ENV, } from "./projection-generation-process.js";
3
3
  import { runProjectionGeneration } from "./projection-generation-runner.js";
4
+ import { PROJECTION_ADOPTION_TIMEOUT_MS } from "./projection-generation-launcher.js";
5
+ import { startProjectionGeneration } from "./projection-generation-launcher.js";
4
6
  import { snapshotPendingTells } from "./projection-generation-continuation.js";
5
- import { recordInterruptWakeFailure, tellProjection } from "../projection-wake.js";
7
+ import { persistInterruptWakeFailure, tellProjection } from "../projection-wake.js";
6
8
  import { readProjectionIdentity } from "../projection-identity.js";
7
- import { FlowError } from "../../../flow-error.js";
8
9
  import * as path from "node:path";
10
+ import { acquireProjectionRunnerTransferTarget } from "../projection-runner-lock.js";
11
+ function sendBootstrapMessage(message) {
12
+ return new Promise((resolve, reject) => {
13
+ if (!process.send || !process.connected) {
14
+ reject(new Error("projection runner bootstrap channel is unavailable"));
15
+ return;
16
+ }
17
+ process.send(message, (error) => error ? reject(error) : resolve());
18
+ });
19
+ }
20
+ function waitForLaunchCommand(executionId, expected) {
21
+ return new Promise((resolve, reject) => {
22
+ const cleanup = () => {
23
+ process.off("message", onMessage);
24
+ process.off("disconnect", onDisconnect);
25
+ };
26
+ const onDisconnect = () => {
27
+ cleanup();
28
+ reject(new Error("projection runner bootstrap channel disconnected before launch authorization"));
29
+ };
30
+ const onMessage = (value) => {
31
+ const command = value;
32
+ if (command.executionId !== executionId)
33
+ return;
34
+ if (expected === "promote-leash" && command.kind !== "promote-leash" && command.kind !== "launch-rejected")
35
+ return;
36
+ if (expected === "launch-authority" && command.kind !== "launch-committed" && command.kind !== "launch-rejected")
37
+ return;
38
+ cleanup();
39
+ resolve(command);
40
+ };
41
+ process.on("message", onMessage);
42
+ process.on("disconnect", onDisconnect);
43
+ });
44
+ }
9
45
  export async function wakeProjectionAfterInterrupt(projectionDirectory, wake = tellProjection) {
10
46
  const pact = readProjectionIdentity(projectionDirectory);
11
47
  try {
12
48
  await wake(projectionDirectory, `${pact.akuma}/${path.basename(projectionDirectory)}`, undefined);
13
49
  }
14
50
  catch (error) {
15
- const cause = error instanceof FlowError
16
- && error.facts?.kind === "projection_wake_failure"
17
- ? error.facts.cause
18
- : "spawn";
19
- recordInterruptWakeFailure(projectionDirectory, cause);
51
+ persistInterruptWakeFailure(projectionDirectory, error);
20
52
  throw error;
21
53
  }
22
54
  }
23
55
  export async function runProjectionGenerationRuntime(projectionDirectory, executionId) {
24
- await runProjectionGeneration({
25
- projectionDirectory,
26
- executionId,
27
- execute: (launch) => executeProjectionGeneration(projectionDirectory, launch),
28
- snapshotPendingTells: () => snapshotPendingTells(projectionDirectory),
29
- spawnSuccessor: (successorExecutionId) => {
30
- spawnProjectionGenerationRunner(projectionDirectory, successorExecutionId);
31
- },
32
- wakeAfterInterrupt: () => wakeProjectionAfterInterrupt(projectionDirectory),
33
- });
56
+ if (process.env[PROJECTION_RUNNER_BOOTSTRAP_ENV] !== "1") {
57
+ throw new Error("projection runner was not spawned with inherited leash coordination");
58
+ }
59
+ let runnerLock;
60
+ let transferTarget;
61
+ try {
62
+ transferTarget = acquireProjectionRunnerTransferTarget(projectionDirectory, process.platform, PROJECTION_ADOPTION_TIMEOUT_MS);
63
+ await sendBootstrapMessage({ kind: "leash-inherited", executionId });
64
+ const promotion = await waitForLaunchCommand(executionId, "promote-leash");
65
+ if (promotion.kind === "launch-rejected")
66
+ return;
67
+ runnerLock = transferTarget.promote();
68
+ transferTarget = undefined;
69
+ await sendBootstrapMessage({ kind: "leash-acquired", executionId });
70
+ const command = await waitForLaunchCommand(executionId, "launch-authority");
71
+ if (command.kind === "launch-rejected")
72
+ return;
73
+ process.disconnect?.();
74
+ const ownedLock = runnerLock;
75
+ runnerLock = undefined;
76
+ await runProjectionGeneration({
77
+ projectionDirectory,
78
+ executionId,
79
+ runnerLock: ownedLock,
80
+ prepareExecution: (launch) => prepareProjectionGenerationExecution(projectionDirectory, launch),
81
+ snapshotPendingTells: () => snapshotPendingTells(projectionDirectory),
82
+ startSuccessor: async (successorExecutionId, commitLaunch, launchOwnership) => {
83
+ const started = await startProjectionGeneration(projectionDirectory, successorExecutionId, {
84
+ commitLaunch,
85
+ launchOwnership,
86
+ });
87
+ if (started.status === "failed")
88
+ throw new Error(started.detail);
89
+ },
90
+ wakeAfterInterrupt: () => wakeProjectionAfterInterrupt(projectionDirectory),
91
+ });
92
+ }
93
+ finally {
94
+ runnerLock?.close();
95
+ transferTarget?.close();
96
+ }
34
97
  }
@@ -1,4 +1,4 @@
1
- import { PROJECTION_GENERATION_SCHEMA_VERSION, PROJECTION_GENERATION_STORE_FILE, assertDatabaseOpen, closeDatabase, openMutableDatabase, openReadOnlyDatabase, readValidatedHistory, } from "./database.js";
1
+ import { PROJECTION_GENERATION_SCHEMA_VERSION, PROJECTION_GENERATION_STORE_FILE, assertDatabaseOpen, beginGenerationWriteTransaction, closeDatabase, commitGenerationWriteTransaction, openMutableDatabase, openReadOnlyDatabase, readValidatedHistory, rollbackGenerationWriteTransaction, } from "./database.js";
2
2
  import { initializeIdentity as initializeProjectionIdentity, readIdentity as readProjectionIdentity, } from "./identity.js";
3
3
  import * as transitions from "./transitions.js";
4
4
  export { PROJECTION_GENERATION_SCHEMA_VERSION, PROJECTION_GENERATION_STORE_FILE, };
@@ -21,8 +21,18 @@ export class ProjectionGenerationStore {
21
21
  return new ProjectionGenerationStore(databasePath, database, true);
22
22
  }
23
23
  close() {
24
+ rollbackGenerationWriteTransaction(this.#database);
24
25
  closeDatabase(this.#database);
25
26
  }
27
+ beginWriteTransaction() {
28
+ beginGenerationWriteTransaction(this.#database, this.readOnly);
29
+ }
30
+ commitWriteTransaction() {
31
+ commitGenerationWriteTransaction(this.#database);
32
+ }
33
+ rollbackWriteTransaction() {
34
+ rollbackGenerationWriteTransaction(this.#database);
35
+ }
26
36
  readRecords() {
27
37
  assertDatabaseOpen(this.#database);
28
38
  return readValidatedHistory(this.#database).records;
@@ -40,6 +50,9 @@ export class ProjectionGenerationStore {
40
50
  launchIfSettled(input) {
41
51
  return transitions.launchIfSettled(this.#database, this.readOnly, input);
42
52
  }
53
+ failInterruptWakeIfSettled(input) {
54
+ return transitions.failInterruptWakeIfSettled(this.#database, this.readOnly, input);
55
+ }
43
56
  adoptionIfCurrent(input) {
44
57
  return transitions.adoptionIfCurrent(this.#database, this.readOnly, input);
45
58
  }
@@ -67,6 +80,9 @@ export class ProjectionGenerationStore {
67
80
  supersedeStartupTimeout(input) {
68
81
  return transitions.supersedeStartupTimeout(this.#database, this.readOnly, input);
69
82
  }
83
+ settleStartupFailure(input) {
84
+ return transitions.settleStartupFailure(this.#database, this.readOnly, input);
85
+ }
70
86
  replaceDeadRunner(input) {
71
87
  return transitions.replaceDeadRunner(this.#database, this.readOnly, input);
72
88
  }
@@ -1,5 +1,5 @@
1
1
  import { appendRecord, readValidatedHistory, writeTransition, } from "./database.js";
2
- import { ProjectionGenerationStoreError, VERDICTS, assertExecutionId, assertIsoTimestamp, assertNonNegativeSafeInteger, assertPositiveSafeInteger, cloneFacts, validateAdoptionFacts, validateInterruptIntentFacts, validateKillIntentFacts, validateLaunchFacts, validateTellFence, validateVerdictFacts, } from "./model.js";
2
+ import { ProjectionGenerationStoreError, VERDICTS, assertExecutionId, assertIsoTimestamp, cloneFacts, validateAdoptionFacts, validateInterruptIntentFacts, validateKillIntentFacts, validateLaunchFacts, validateTellFence, validateVerdictFacts, } from "./model.js";
3
3
  function committed(records) {
4
4
  return { status: "committed", records };
5
5
  }
@@ -20,6 +20,38 @@ export function launchIfSettled(database, readOnly, input) {
20
20
  return committed([appendRecord(database, "launch", executionId, facts, history.records.length + 1)]);
21
21
  });
22
22
  }
23
+ /** Persist an asynchronous interrupt wake rejection as one closed successor. */
24
+ export function failInterruptWakeIfSettled(database, readOnly, input) {
25
+ const predecessorExecutionId = assertExecutionId(input.predecessorExecutionId);
26
+ const successorExecutionId = assertExecutionId(input.successorExecutionId);
27
+ const successorFacts = validateLaunchFacts(input.successorFacts, "interrupt wake successor launch facts");
28
+ const terminalAt = assertIsoTimestamp(input.terminalAt, "terminalAt");
29
+ return writeTransition(database, readOnly, () => {
30
+ const history = readValidatedHistory(database);
31
+ const current = history.current;
32
+ if (!current)
33
+ return rejected("no-current-launch", null);
34
+ if (current.launch.executionId !== predecessorExecutionId)
35
+ return rejected("not-current", current);
36
+ if (current.verdict?.facts.state !== "interrupted") {
37
+ return rejected(current.verdict ? "already-terminal" : "current-generation-open", current);
38
+ }
39
+ if (history.records.some((record) => record.kind === "launch" && record.executionId === successorExecutionId)) {
40
+ return rejected("execution-id-already-used", current);
41
+ }
42
+ const verdictFacts = validateVerdictFacts({
43
+ state: "launch-failed",
44
+ cause: input.cause,
45
+ tellRetained: true,
46
+ terminalAt,
47
+ }, "interrupt wake failure verdict facts");
48
+ const firstSeq = history.records.length + 1;
49
+ return committed([
50
+ appendRecord(database, "launch", successorExecutionId, successorFacts, firstSeq),
51
+ appendRecord(database, "verdict", successorExecutionId, verdictFacts, firstSeq + 1),
52
+ ]);
53
+ });
54
+ }
23
55
  export function adoptionIfCurrent(database, readOnly, input) {
24
56
  const executionId = assertExecutionId(input.executionId);
25
57
  const facts = validateAdoptionFacts(input.facts, "adoption facts");
@@ -240,15 +272,15 @@ export function verdictIfOpen(database, readOnly, input) {
240
272
  return rejected("not-current", current);
241
273
  if (current.verdict)
242
274
  return rejected("already-terminal", current);
275
+ if (input.verdict === "launch-failed" && current.adoption) {
276
+ return rejected("already-adopted", current);
277
+ }
243
278
  if (current.killIntent) {
244
279
  const killEscalationFailed = input.verdict === "failed"
245
280
  && input.facts?.operatorAction === "kill";
246
281
  if (!killEscalationFailed)
247
282
  return rejected("kill-requested", current);
248
283
  }
249
- if (input.verdict === "launch-failed" && current.adoption) {
250
- return rejected("already-adopted", current);
251
- }
252
284
  const facts = validateVerdictFacts({ ...(input.facts ?? {}), state: input.verdict, ...(input.terminalAt ? { terminalAt: assertIsoTimestamp(input.terminalAt, "terminalAt") } : {}) }, "verdict facts");
253
285
  return committed([appendRecord(database, "verdict", executionId, facts, history.records.length + 1)]);
254
286
  });
@@ -256,8 +288,10 @@ export function verdictIfOpen(database, readOnly, input) {
256
288
  export function supersedeStartupTimeout(database, readOnly, input) {
257
289
  const executionId = assertExecutionId(input.executionId);
258
290
  const successorExecutionId = assertExecutionId(input.successorExecutionId);
259
- const observedAtMs = assertNonNegativeSafeInteger(input.observedAtMs, "observedAtMs");
260
- const startupTimeoutMs = assertPositiveSafeInteger(input.startupTimeoutMs, "startupTimeoutMs");
291
+ const releasedLockEvidence = cloneFacts(input.releasedLockEvidence, "released startup-lock evidence");
292
+ if (Object.keys(releasedLockEvidence).length === 0) {
293
+ throw new ProjectionGenerationStoreError("released startup-lock evidence must not be empty");
294
+ }
261
295
  const diagnostic = cloneFacts(input.diagnostic, "startup-timeout diagnostic");
262
296
  const successorFacts = validateLaunchFacts(input.successorFacts, "successor launch facts");
263
297
  const terminalAt = input.terminalAt === undefined
@@ -272,19 +306,24 @@ export function supersedeStartupTimeout(database, readOnly, input) {
272
306
  return rejected("not-current", current);
273
307
  if (current.verdict)
274
308
  return rejected("already-terminal", current);
275
- if (current.killIntent)
276
- return rejected("kill-requested", current);
277
309
  if (current.adoption)
278
310
  return rejected("already-adopted", current);
311
+ if (current.killIntent) {
312
+ const verdictFacts = validateVerdictFacts({
313
+ state: "killed",
314
+ killIntentSeq: current.killIntent.seq,
315
+ killedAt: current.killIntent.facts.killedAt,
316
+ releasedLockEvidence,
317
+ ...(terminalAt === undefined ? {} : { terminalAt }),
318
+ }, "startup-timeout killed verdict facts");
319
+ return committed([appendRecord(database, "verdict", executionId, verdictFacts, history.records.length + 1)]);
320
+ }
279
321
  if (history.records.some((record) => record.kind === "launch" && record.executionId === successorExecutionId)) {
280
322
  return rejected("execution-id-already-used", current);
281
323
  }
282
- const launchedAtMs = Date.parse(current.launch.facts.createdAt);
283
- if (observedAtMs - launchedAtMs < startupTimeoutMs) {
284
- return rejected("startup-timeout-not-elapsed", current);
285
- }
286
324
  const verdictFactsBase = {
287
325
  ...diagnostic,
326
+ releasedLockEvidence,
288
327
  state: "launch-failed",
289
328
  ...(terminalAt === undefined ? {} : { terminalAt }),
290
329
  };
@@ -296,6 +335,44 @@ export function supersedeStartupTimeout(database, readOnly, input) {
296
335
  ]);
297
336
  });
298
337
  }
338
+ export function settleStartupFailure(database, readOnly, input) {
339
+ const executionId = assertExecutionId(input.executionId);
340
+ const releasedLockEvidence = cloneFacts(input.releasedLockEvidence, "released startup-lock evidence");
341
+ if (Object.keys(releasedLockEvidence).length === 0) {
342
+ throw new ProjectionGenerationStoreError("released startup-lock evidence must not be empty");
343
+ }
344
+ const diagnostic = cloneFacts(input.diagnostic, "startup failure diagnostic");
345
+ const terminalAt = input.terminalAt === undefined
346
+ ? undefined
347
+ : assertIsoTimestamp(input.terminalAt, "terminalAt");
348
+ return writeTransition(database, readOnly, () => {
349
+ const history = readValidatedHistory(database);
350
+ const current = history.current;
351
+ if (!current)
352
+ return rejected("no-current-launch", null);
353
+ if (current.launch.executionId !== executionId)
354
+ return rejected("not-current", current);
355
+ if (current.verdict)
356
+ return rejected("already-terminal", current);
357
+ if (current.adoption)
358
+ return rejected("already-adopted", current);
359
+ const facts = current.killIntent
360
+ ? validateVerdictFacts({
361
+ state: "killed",
362
+ killIntentSeq: current.killIntent.seq,
363
+ killedAt: current.killIntent.facts.killedAt,
364
+ releasedLockEvidence,
365
+ ...(terminalAt === undefined ? {} : { terminalAt }),
366
+ }, "startup failure killed verdict facts")
367
+ : validateVerdictFacts({
368
+ ...diagnostic,
369
+ releasedLockEvidence,
370
+ state: "launch-failed",
371
+ ...(terminalAt === undefined ? {} : { terminalAt }),
372
+ }, "startup failure verdict facts");
373
+ return committed([appendRecord(database, "verdict", executionId, facts, history.records.length + 1)]);
374
+ });
375
+ }
299
376
  export function replaceDeadRunner(database, readOnly, input) {
300
377
  const executionId = assertExecutionId(input.executionId);
301
378
  const successorExecutionId = assertExecutionId(input.successorExecutionId);
@@ -12,8 +12,8 @@ export { readProjectionIdentity } from "./projection-identity.js";
12
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
- export { acquireProjectionRunnerLock, initializeProjectionRunnerLockDatabase, isProjectionRunnerLockPlatformSupported, observeProjectionRunnerLock, projectionRunnerLockPath, } from "./projection-runner-lock.js";
16
- export { hasStrandableProjectionGeneration, prioritizeProjectionStatusRows, readProjectionStatusBoard, } from "./projection-status.js";
15
+ export { acquireProjectionRunnerLock, acquireProjectionRunnerSettlementLock, acquireProjectionRunnerTransferTarget, initializeProjectionRunnerLockDatabase, isProjectionRunnerLockPlatformSupported, observeProjectionRunnerLock, projectionRunnerLockPath, } from "./projection-runner-lock.js";
16
+ export { hasStrandableProjectionGeneration, prioritizeProjectionStatusRows, readAkumaNonterminalProjectionStatus, readExactProjectionStatus, readOperationalProjectionStatusBoard, } from "./projection-status.js";
17
17
  export { projectTerminalFailure, } from "./projection-terminal-failure.js";
18
18
  export { snapshotCommissionProjectionIds, waitForProjection, waitForProjectionSet, } from "./projection-wait.js";
19
19
  export { assertTellEffortSupported, assertProjectionTellable, tellProjection, } from "./projection-wake.js";
@@ -22,6 +22,6 @@ export { createProjectionExecutionId } from "./generation/projection-generation-
22
22
  export { PROJECTION_ADOPTION_TIMEOUT_MS, startProjectionGeneration, } from "./generation/projection-generation-launcher.js";
23
23
  export { projectionGenerationRunnerArgv, spawnProjectionGenerationRunner, } from "./generation/projection-generation-process.js";
24
24
  export { runProjectionGeneration, } from "./generation/projection-generation-runner.js";
25
- export { executeProjectionGeneration } from "./generation/projection-generation-execution.js";
25
+ export { executeProjectionGeneration, prepareProjectionGenerationExecution, } from "./generation/projection-generation-execution.js";
26
26
  export { runProjectionGenerationRuntime } from "./generation/projection-generation-runtime.js";
27
27
  export { claimFencedTells, claimTellToInflight, listTellIds, markTellConsumed, markTellSubmitted, readPendingTellWindow, readProjectionTellCounts, readTellOriginal, readTellSubmitted, writeInboxTell, } from "./tell/store.js";
@@ -1,5 +1,5 @@
1
1
  import { openProjectionGenerationStore } from "./generation/store.js";
2
- import { observeProjectionRunnerLock } from "./projection-runner-lock.js";
2
+ import { acquireProjectionRunnerSettlementLock, observeProjectionRunnerLock, ProjectionRunnerLockUnavailableError, } from "./projection-runner-lock.js";
3
3
  /** The runner observes this durable intent and owns native termination. */
4
4
  export function interruptProjectionGeneration(projectionDirectory, nowMs = Date.now()) {
5
5
  const store = openProjectionGenerationStore(projectionDirectory);
@@ -32,18 +32,30 @@ export function interruptProjectionGeneration(projectionDirectory, nowMs = Date.
32
32
  }
33
33
  }
34
34
  function settleReleasedLeash(store, projectionDirectory, executionId) {
35
- const observation = observeProjectionRunnerLock(projectionDirectory);
36
- if (observation.state === "held" || observation.state === "unknown") {
35
+ const current = store.readCurrentGeneration();
36
+ if (current?.launch.executionId !== executionId
37
+ || current.verdict) {
37
38
  return { status: "accepted", executionId };
38
39
  }
39
- // Positive released-leash evidence only; settlement race still reports accepted.
40
- store.settleKillIfRunnerDead({
41
- executionId,
42
- releasedLockEvidence: {
43
- state: "released",
40
+ let deathEvidence;
41
+ try {
42
+ deathEvidence = acquireProjectionRunnerSettlementLock(projectionDirectory);
43
+ }
44
+ catch (error) {
45
+ if (error instanceof ProjectionRunnerLockUnavailableError) {
46
+ return { status: "accepted", executionId };
47
+ }
48
+ return { status: "accepted", executionId };
49
+ }
50
+ try {
51
+ store.settleKillIfRunnerDead({
44
52
  executionId,
45
- },
46
- });
53
+ releasedLockEvidence: { state: "acquired", executionId },
54
+ });
55
+ }
56
+ finally {
57
+ deathEvidence.close();
58
+ }
47
59
  return { status: "accepted", executionId };
48
60
  }
49
61
  /**