@astrosheep/keiyaku 1.0.0 → 1.0.2

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 (123) hide show
  1. package/README.md +73 -56
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/call-terms_v2.js +103 -0
  4. package/build/agents/harness/activity-values.js +54 -0
  5. package/build/agents/harness/event-channel.js +55 -20
  6. package/build/agents/harness/events.js +47 -1
  7. package/build/agents/harness/execution-handle.js +292 -61
  8. package/build/agents/harness/index.js +103 -121
  9. package/build/agents/harness/pump.js +36 -47
  10. package/build/agents/harness/runtime.js +3 -0
  11. package/build/agents/launch-snapshot_v2.js +348 -0
  12. package/build/agents/opencode-sdk.js +42 -169
  13. package/build/agents/providers/claude-agent-sdk.js +240 -79
  14. package/build/agents/providers/codex-app-server.js +280 -72
  15. package/build/agents/providers/codex-sdk.js +159 -41
  16. package/build/agents/providers/event-type-tail.js +16 -0
  17. package/build/agents/providers/opencode-sdk.js +165 -59
  18. package/build/agents/providers/pi.js +254 -42
  19. package/build/agents/selector_v2.js +122 -0
  20. package/build/cli/commands/akuma.js +120 -42
  21. package/build/cli/completion.js +6 -7
  22. package/build/cli/flags.js +36 -10
  23. package/build/cli/help.js +42 -26
  24. package/build/cli/index.js +225 -93
  25. package/build/cli/parse.js +194 -75
  26. package/build/cli/projection-address.js +11 -0
  27. package/build/cli/render/address.js +88 -0
  28. package/build/cli/render/arc.js +10 -10
  29. package/build/cli/render/call.js +59 -0
  30. package/build/cli/render/compact-text.js +3 -0
  31. package/build/cli/render/errors.js +4 -2
  32. package/build/cli/render/format.js +6 -0
  33. package/build/cli/render/line-width.js +70 -0
  34. package/build/cli/render/misc.js +7 -4
  35. package/build/cli/render/petition.js +4 -3
  36. package/build/cli/render/projection-activity.js +201 -0
  37. package/build/cli/render/shared.js +46 -12
  38. package/build/cli/render/status.js +59 -10
  39. package/build/cli/render/tool-ledger-rollup.js +74 -0
  40. package/build/cli/render/tool-presentation.js +97 -0
  41. package/build/cli/render/wait.js +229 -0
  42. package/build/cli/skills-install.js +42 -24
  43. package/build/cli/subagent-guard.js +1 -1
  44. package/build/config/akuma-loader_v2.js +222 -0
  45. package/build/config/env-keys.js +0 -4
  46. package/build/config/env.js +0 -6
  47. package/build/config/settings.js +325 -12
  48. package/build/core/addressing.js +224 -0
  49. package/build/core/amend.js +30 -47
  50. package/build/core/arc.js +68 -60
  51. package/build/core/bind.js +105 -25
  52. package/build/core/call-persist.js +69 -0
  53. package/build/core/call.js +658 -0
  54. package/build/core/claim.js +72 -44
  55. package/build/core/command-io.js +19 -19
  56. package/build/core/context.js +11 -36
  57. package/build/core/draft.js +2 -3
  58. package/build/core/entry.js +101 -6
  59. package/build/core/execution-coordinate.js +62 -0
  60. package/build/core/execution-pact.js +141 -0
  61. package/build/core/forfeit.js +2 -3
  62. package/build/core/hints.js +10 -10
  63. package/build/core/ids.js +6 -0
  64. package/build/core/ledger.js +16 -1
  65. package/build/core/petition-claim-gates.js +14 -12
  66. package/build/core/petition-claim.js +1 -0
  67. package/build/core/petition-run.js +6 -2
  68. package/build/core/petition.js +63 -21
  69. package/build/core/places.js +106 -13
  70. package/build/core/projection/generation.js +412 -0
  71. package/build/core/projection/heart.js +316 -0
  72. package/build/core/projection/identity.js +81 -0
  73. package/build/core/projection/leash.js +87 -0
  74. package/build/core/projection/mint.js +154 -0
  75. package/build/core/projection-activity.js +219 -0
  76. package/build/core/projection-coordinate.js +35 -0
  77. package/build/core/projection-core.js +69 -463
  78. package/build/core/projection-generation-continuation.js +31 -0
  79. package/build/core/projection-generation-execution.js +148 -0
  80. package/build/core/projection-generation-identity.js +17 -0
  81. package/build/core/projection-generation-launcher.js +90 -0
  82. package/build/core/projection-generation-process.js +43 -0
  83. package/build/core/projection-generation-runner.js +144 -0
  84. package/build/core/projection-generation-runtime.js +15 -0
  85. package/build/core/projection-generation-store.js +707 -0
  86. package/build/core/projection-identity.js +11 -0
  87. package/build/core/projection-life-observer.js +87 -0
  88. package/build/core/projection-life-protocol.js +95 -0
  89. package/build/core/projection-mint.js +96 -24
  90. package/build/core/projection-runner-lock.js +230 -0
  91. package/build/core/projection-status.js +207 -107
  92. package/build/core/projection-tells.js +401 -0
  93. package/build/core/projection-wait.js +155 -0
  94. package/build/core/projection-wake.js +109 -231
  95. package/build/core/queue_v2.js +342 -0
  96. package/build/core/registry.js +6 -3
  97. package/build/core/render.js +63 -1
  98. package/build/core/renew.js +80 -53
  99. package/build/core/scope.js +186 -137
  100. package/build/core/seal.js +16 -19
  101. package/build/core/status.js +88 -25
  102. package/build/core/stored-agent-event.js +101 -0
  103. package/build/core/transcripts.js +214 -228
  104. package/build/core/verdict.js +27 -15
  105. package/build/core/worktree-bootstrap.js +187 -0
  106. package/build/core/worktree-path.js +72 -18
  107. package/build/flow-error.js +4 -6
  108. package/build/generated/version.js +1 -1
  109. package/build/git/branches.js +6 -1
  110. package/build/index.js +8 -5
  111. package/build/keiyaku.js +1 -3
  112. package/package.json +6 -5
  113. package/skills/keiyaku/SKILL.md +6 -47
  114. package/skills/keiyaku-akuma/SKILL.md +75 -0
  115. package/build/agents/effective-policy.js +0 -32
  116. package/build/agents/index.js +0 -114
  117. package/build/agents/selector.js +0 -28
  118. package/build/cli/render/summon.js +0 -17
  119. package/build/cli/render/tell.js +0 -11
  120. package/build/config/akuma-loader.js +0 -268
  121. package/build/core/queue.js +0 -73
  122. package/build/core/summon-persist.js +0 -73
  123. package/build/core/summon.js +0 -377
@@ -1,252 +1,130 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import { spawn } from "node:child_process";
4
- import { fileURLToPath } from "node:url";
5
- import { ProjectionStateError } from "./atomic-publish.js";
6
1
  import { FlowError } from "../flow-error.js";
7
- import { CODEX_MODEL_REASONING_EFFORT_CONFIG_KEY } from "../keiyaku.js";
8
- import { acquireResumeLock, createProjectionExecutionId, deriveProjectionPhase, generationLogPath, isProjectionWakeEligible, latestGenerationLaunch, latestGenerationResult, listTellIds, publishGenerationLaunch, publishGenerationResult, readAdmissionLock, readGenerationLaunch, readTellOriginal, readTellSubmitted, readResumeLock, releaseAdmissionLockOwned, releaseResumeLockOwned, } from "./projection-core.js";
9
- export const WAKE_ADMISSION_TIMEOUT_MS = 4_000;
10
- export const WAKE_ADMISSION_POLL_MS = 20;
11
- export const RESUME_LOCK_STALE_MS = 30_000;
12
- /** Admission liveness permits a heartbeat window longer than status's 15s display threshold. */
13
- export const ADMISSION_HEARTBEAT_STALE_MS = 30_000;
14
- // This module lives at src/core or build/core; two parents reach the repository
15
- // root in either form, so the detached child never depends on ambient cwd.
16
- const COMPILED_ENTRY = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "build", "index.js");
17
- function defaultSleep(milliseconds) {
18
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
19
- }
20
- function isResumeLockDeadOrStale(lock, now, probePid) {
21
- try {
22
- probePid(lock.pid);
23
- }
24
- catch {
25
- return true;
26
- }
27
- const created = Date.parse(lock.createdAt);
28
- return !Number.isFinite(created) || now - created > RESUME_LOCK_STALE_MS;
29
- }
30
- function isAdmissionDeadOrHeartbeatStale(projectionDirectory, lock, now, probePid, heartbeatStaleMs) {
31
- try {
32
- probePid(lock.pid);
33
- }
34
- catch {
35
- return true;
36
- }
2
+ import { parseResolvedAkumaLaunchSnapshot } from "../agents/launch-snapshot_v2.js";
3
+ import { ProjectionStateError } from "./atomic-publish.js";
4
+ import { applyContinuationEffort, snapshotPendingTells, } from "./projection-generation-continuation.js";
5
+ import { createProjectionExecutionId } from "./projection-generation-identity.js";
6
+ import { startProjectionGeneration } from "./projection-generation-launcher.js";
7
+ import { openProjectionGenerationStore, openProjectionGenerationStoreReadOnly, } from "./projection-generation-store.js";
8
+ import { PROJECTION_ADOPTION_TIMEOUT_MS } from "./projection-generation-launcher.js";
9
+ import { observeProjectionLife } from "./projection-life-observer.js";
10
+ import { observeProjectionRunnerLock } from "./projection-runner-lock.js";
11
+ function readCurrentGeneration(projectionDirectory) {
12
+ const store = openProjectionGenerationStoreReadOnly(projectionDirectory);
37
13
  try {
38
- return now - fs.statSync(path.join(projectionDirectory, "heartbeat")).mtimeMs > heartbeatStaleMs;
14
+ return store.readCurrentGeneration();
39
15
  }
40
- catch {
41
- return true;
16
+ finally {
17
+ store.close();
42
18
  }
43
19
  }
44
- function acquireOrRecoverResumeLock(projectionDirectory, childId, deps) {
45
- const first = acquireResumeLock(projectionDirectory, childId, process.pid, deps.now());
46
- if (first === "published")
47
- return "owned";
48
- const previous = readResumeLock(projectionDirectory);
49
- if (!previous || !isResumeLockDeadOrStale(previous, deps.now(), deps.probePid))
50
- return "taken";
51
- // The existing record is dead/stale. Re-read immediately before removing it
52
- // so a live contender is never released as stale based on an old snapshot.
53
- const current = readResumeLock(projectionDirectory);
54
- if (!current || current.childId !== previous.childId || !isResumeLockDeadOrStale(current, deps.now(), deps.probePid))
55
- return "taken";
56
- releaseResumeLockOwned(projectionDirectory, current.childId);
57
- return acquireResumeLock(projectionDirectory, childId, process.pid, deps.now()) === "published" ? "owned" : "taken";
58
- }
59
- function appendWakeLog(projectionDirectory, executionId, line) {
60
- fs.appendFileSync(generationLogPath(projectionDirectory, executionId), `${line}\n`);
61
- }
62
- function ownLaunchInputs(projectionDirectory, launch = latestGenerationLaunch(projectionDirectory)) {
63
- if (!launch)
64
- return {};
65
- const parent = latestGenerationResult(projectionDirectory);
20
+ function successorFacts(current, snapshot, nowMs) {
21
+ const verdictFacts = current.verdict?.facts;
22
+ const originalInputs = current.launch.facts.inputs;
23
+ const inputs = originalInputs
24
+ && typeof originalInputs === "object"
25
+ && !Array.isArray(originalInputs)
26
+ ? originalInputs
27
+ : {};
28
+ const continuedInputs = {
29
+ ...inputs,
30
+ ...(verdictFacts?.session ? { session: verdictFacts.session } : {}),
31
+ ...(typeof verdictFacts?.responsePath === "string"
32
+ ? { resumePath: verdictFacts.responsePath }
33
+ : {}),
34
+ };
35
+ const { tellFence: _oldFence, ...inherited } = current.launch.facts;
66
36
  return {
67
- ...readGenerationLaunch(projectionDirectory, launch.executionId).inputs,
68
- ...(parent?.session !== undefined ? { session: parent.session } : {}),
69
- ...(parent?.responsePath ? { resumePath: parent.responsePath } : {}),
37
+ ...inherited,
38
+ createdAt: new Date(nowMs).toISOString(),
39
+ launchedBy: "tell-wake",
40
+ parentExecutionId: current.launch.executionId,
41
+ tellFence: snapshot.tellIds,
42
+ inputs: applyContinuationEffort(continuedInputs, snapshot.effort),
70
43
  };
71
44
  }
72
- function overlayEffort(inputs, effort) {
73
- if (!effort)
74
- return inputs;
75
- const rawProfile = inputs.profileData;
76
- if (!rawProfile || typeof rawProfile !== "object" || Array.isArray(rawProfile))
77
- throw new ProjectionStateError("generation launch is missing immutable profileData");
78
- const profile = rawProfile;
79
- if (profile.provider === "opencode-sdk")
80
- throw new FlowError("UNSUPPORTED_EFFORT_OVERRIDE", `Provider 'opencode-sdk' does not support effort override '${effort}'.`);
81
- if (profile.provider === "claude-agent-sdk")
82
- return { ...inputs, profileData: { ...profile, effort } };
83
- if (profile.provider === "pi")
84
- return { ...inputs, profileData: { ...profile, thinkingLevel: effort === "max" ? "xhigh" : effort } };
85
- if (profile.provider === "codex-sdk") {
86
- const { effort: _legacyEffort, ...canonicalProfile } = profile;
87
- const priorThreadOptions = profile.threadOptions && typeof profile.threadOptions === "object" && !Array.isArray(profile.threadOptions) ? profile.threadOptions : {};
88
- return { ...inputs, profileData: { ...canonicalProfile, threadOptions: { ...priorThreadOptions, modelReasoningEffort: effort === "max" ? "xhigh" : effort } } };
89
- }
90
- if (profile.provider === "codex-app-server") {
91
- const priorInputConfig = inputs.config && typeof inputs.config === "object" && !Array.isArray(inputs.config) ? inputs.config : {};
92
- const priorProfileConfig = profile.config && typeof profile.config === "object" && !Array.isArray(profile.config) ? profile.config : {};
93
- const inputConfig = { ...priorInputConfig, [CODEX_MODEL_REASONING_EFFORT_CONFIG_KEY]: effort };
94
- const profileConfig = { ...priorProfileConfig, [CODEX_MODEL_REASONING_EFFORT_CONFIG_KEY]: effort };
95
- return { ...inputs, config: inputConfig, profileData: { ...profile, config: profileConfig } };
96
- }
97
- throw new ProjectionStateError("generation launch has an unknown provider");
98
- }
99
- function pendingReplaySnapshot(projectionDirectory, inputs) {
100
- const profile = inputs.profileData;
101
- const pending = ["inbox", "inflight", "submitted"]
102
- .flatMap((layer) => listTellIds(projectionDirectory, layer).map((tellId) => ({ layer, tellId })))
103
- .sort((left, right) => left.tellId.localeCompare(right.tellId));
104
- const effort = pending.reduce((value, item) => {
105
- const tell = item.layer === "submitted"
106
- ? readTellSubmitted(projectionDirectory, item.tellId)
107
- : readTellOriginal(projectionDirectory, item.layer, item.tellId);
108
- return tell.effort ?? value;
109
- }, undefined);
110
- return { tellFence: pending.map((item) => item.tellId), ...(effort ? { effort } : {}) };
111
- }
112
45
  export function assertTellEffortSupported(projectionDirectory, effort) {
113
46
  if (!effort)
114
47
  return;
115
- const launch = latestGenerationLaunch(projectionDirectory);
116
- if (!launch)
48
+ const current = readCurrentGeneration(projectionDirectory);
49
+ if (!current)
117
50
  throw new ProjectionStateError("projection has no immutable generation launch");
118
- overlayEffort(readGenerationLaunch(projectionDirectory, launch.executionId).inputs, effort);
51
+ const inputs = current.launch.facts.inputs;
52
+ if (!inputs || typeof inputs !== "object" || Array.isArray(inputs)) {
53
+ throw new ProjectionStateError("generation launch inputs are missing");
54
+ }
55
+ let launchSnapshot;
56
+ try {
57
+ launchSnapshot = parseResolvedAkumaLaunchSnapshot(inputs.launchSnapshot);
58
+ }
59
+ catch (error) {
60
+ throw new ProjectionStateError("generation launch has an invalid immutable launchSnapshot", { cause: error });
61
+ }
62
+ const effortOptions = launchSnapshot.akuma.effortOptions;
63
+ if (effortOptions === undefined)
64
+ return;
65
+ if (!effortOptions.includes(effort)) {
66
+ throw new FlowError("INVALID_EFFORT_OVERRIDE", `Effort '${effort}' is not permitted; legal values: ${effortOptions.join(", ")}.`);
67
+ }
119
68
  }
120
- function defaultSpawn(argv, logFd) {
121
- return spawn(argv[0], [...argv.slice(1)], { detached: true, stdio: ["ignore", logFd, logFd] });
69
+ async function startCommittedSuccessor(projectionDirectory, executionId) {
70
+ const started = await startProjectionGeneration(projectionDirectory, executionId);
71
+ return started.status === "adopted" ? "woken" : "pending";
122
72
  }
123
- /** The single tell, sweep, and override launcher. It owns resume through child admission. */
124
- export function wakeProjection(projectionDirectory, launchedBy, options = {}) {
125
- const deps = {
126
- now: options.dependencies?.now ?? Date.now,
127
- sleep: options.dependencies?.sleep ?? defaultSleep,
128
- spawn: options.dependencies?.spawn ?? defaultSpawn,
129
- probePid: options.dependencies?.probePid ?? ((pid) => process.kill(pid, 0)),
130
- timeoutMs: options.dependencies?.timeoutMs ?? WAKE_ADMISSION_TIMEOUT_MS,
131
- pollMs: options.dependencies?.pollMs ?? WAKE_ADMISSION_POLL_MS,
132
- admissionHeartbeatStaleMs: options.dependencies?.admissionHeartbeatStaleMs ?? ADMISSION_HEARTBEAT_STALE_MS,
133
- };
134
- if (!options.force && !options.handoffExecutionId && !isProjectionWakeEligible(projectionDirectory))
135
- return "ineligible";
136
- const executionId = createProjectionExecutionId();
137
- if (acquireOrRecoverResumeLock(projectionDirectory, executionId, deps) !== "owned")
138
- return "already-waking";
139
- let child;
73
+ export async function tellProjection(projectionDirectory, effort) {
74
+ const nowMs = Date.now();
75
+ const life = observeProjectionLife(projectionDirectory, {
76
+ nowMs,
77
+ startupTimeoutMs: PROJECTION_ADOPTION_TIMEOUT_MS,
78
+ });
79
+ if (life.phase === "active")
80
+ return "active";
81
+ if (life.phase === "unknown" || life.phase === "minting" || life.phase === "quiescent") {
82
+ return "pending";
83
+ }
84
+ const current = readCurrentGeneration(projectionDirectory);
85
+ if (!current)
86
+ return "pending";
87
+ const snapshot = snapshotPendingTells(projectionDirectory);
88
+ const successorExecutionId = createProjectionExecutionId(nowMs);
89
+ const facts = successorFacts(current, { ...snapshot, ...(effort ? { effort } : {}) }, nowMs);
90
+ const store = openProjectionGenerationStore(projectionDirectory);
140
91
  try {
141
- const parent = latestGenerationLaunch(projectionDirectory);
142
- const ownInputs = ownLaunchInputs(projectionDirectory, parent);
143
- const replaySnapshot = pendingReplaySnapshot(projectionDirectory, ownInputs);
144
- const inheritedInputs = {
145
- ...overlayEffort(ownInputs, replaySnapshot.effort ?? options.effort),
146
- tellFence: replaySnapshot.tellFence,
147
- };
148
- publishGenerationLaunch(projectionDirectory, {
149
- version: 1,
150
- executionId,
151
- ...(parent ? { parentExecutionId: parent.executionId } : {}),
152
- createdAt: new Date(deps.now()).toISOString(),
153
- launchedBy,
154
- inputs: inheritedInputs,
155
- });
156
- if (options.clearStaleAdmission) {
157
- const staleAdmission = readAdmissionLock(projectionDirectory);
158
- if (staleAdmission) {
159
- if (!isAdmissionDeadOrHeartbeatStale(projectionDirectory, staleAdmission, deps.now(), deps.probePid, deps.admissionHeartbeatStaleMs)) {
160
- throw new ProjectionStateError("refusing to clear a live admission while resuming projection");
161
- }
162
- const currentAdmission = readAdmissionLock(projectionDirectory);
163
- if (!currentAdmission ||
164
- !staleAdmission.executionId ||
165
- currentAdmission.executionId !== staleAdmission.executionId ||
166
- !isAdmissionDeadOrHeartbeatStale(projectionDirectory, currentAdmission, deps.now(), deps.probePid, deps.admissionHeartbeatStaleMs)) {
167
- throw new ProjectionStateError("refusing to clear a changed or live admission while resuming projection");
168
- }
169
- releaseAdmissionLockOwned(projectionDirectory, staleAdmission.executionId);
170
- }
171
- }
172
- if (options.handoffExecutionId)
173
- releaseAdmissionLockOwned(projectionDirectory, options.handoffExecutionId);
174
- const absoluteProjectionDirectory = path.resolve(projectionDirectory);
175
- const logFd = fs.openSync(generationLogPath(absoluteProjectionDirectory, executionId), "a");
176
- try {
177
- child = deps.spawn([process.execPath, COMPILED_ENTRY, "__keiyaku-wake", absoluteProjectionDirectory], logFd);
178
- child.unref();
92
+ if (life.phase === "startup-timeout") {
93
+ const transition = store.supersedeStartupTimeout({
94
+ executionId: current.launch.executionId,
95
+ observedAtMs: nowMs,
96
+ startupTimeoutMs: PROJECTION_ADOPTION_TIMEOUT_MS,
97
+ diagnostic: { detail: "runner did not adopt before startup timeout" },
98
+ successorExecutionId,
99
+ successorFacts: facts,
100
+ });
101
+ return transition.status === "committed"
102
+ ? await startCommittedSuccessor(projectionDirectory, successorExecutionId)
103
+ : "pending";
179
104
  }
180
- finally {
181
- fs.closeSync(logFd);
105
+ if (life.phase === "lost") {
106
+ const lock = observeProjectionRunnerLock(projectionDirectory);
107
+ if (lock.state !== "released")
108
+ return lock.state === "held" ? "active" : "pending";
109
+ const transition = store.replaceDeadRunner({
110
+ executionId: current.launch.executionId,
111
+ releasedLockEvidence: { observedAt: new Date(nowMs).toISOString() },
112
+ successorExecutionId,
113
+ successorFacts: facts,
114
+ });
115
+ return transition.status === "committed"
116
+ ? await startCommittedSuccessor(projectionDirectory, successorExecutionId)
117
+ : "pending";
182
118
  }
183
- const deadline = deps.now() + deps.timeoutMs;
184
- while (deps.now() <= deadline) {
185
- const admission = readAdmissionLock(projectionDirectory);
186
- if (admission?.executionId === executionId) {
187
- releaseResumeLockOwned(projectionDirectory, executionId);
188
- return "started";
189
- }
190
- if (admission?.executionId && admission.executionId !== executionId) {
191
- throw new ProjectionStateError(`admission ownership mismatch: expected ${executionId}, found ${admission.executionId}`);
192
- }
193
- deps.sleep(deps.pollMs);
194
- }
195
- // A child can acquire between the last poll and failure handling.
196
- const finalAdmission = readAdmissionLock(projectionDirectory);
197
- if (finalAdmission?.executionId === executionId) {
198
- releaseResumeLockOwned(projectionDirectory, executionId);
199
- return "started";
200
- }
201
- if (finalAdmission?.executionId && finalAdmission.executionId !== executionId) {
202
- throw new ProjectionStateError(`admission ownership mismatch: expected ${executionId}, found ${finalAdmission.executionId}`);
203
- }
204
- child?.kill("SIGTERM");
205
- const parentResult = latestGenerationResult(projectionDirectory);
206
- publishGenerationResult(projectionDirectory, {
207
- version: 1, executionId, status: "failed", createdAt: new Date(deps.now()).toISOString(),
208
- session: parentResult?.session, detail: "wake child did not acquire admission before timeout",
119
+ const transition = store.launchIfSettled({
120
+ executionId: successorExecutionId,
121
+ facts,
209
122
  });
210
- appendWakeLog(projectionDirectory, executionId, "wake admission timeout");
211
- releaseResumeLockOwned(projectionDirectory, executionId);
212
- return "failed";
123
+ if (transition.status !== "committed")
124
+ return "pending";
125
+ return await startCommittedSuccessor(projectionDirectory, successorExecutionId);
213
126
  }
214
- catch (error) {
215
- const admission = readAdmissionLock(projectionDirectory);
216
- if (admission?.executionId === executionId) {
217
- releaseResumeLockOwned(projectionDirectory, executionId);
218
- return "started";
219
- }
220
- // Never alter a competing admission. This launch has not become a generation.
221
- child?.kill("SIGTERM");
222
- if (!admission || admission.executionId === undefined) {
223
- const detail = error instanceof Error ? error.message : String(error);
224
- publishGenerationResult(projectionDirectory, { version: 1, executionId, status: "failed", createdAt: new Date(deps.now()).toISOString(), detail });
225
- appendWakeLog(projectionDirectory, executionId, `wake spawn failure: ${detail}`);
226
- }
227
- releaseResumeLockOwned(projectionDirectory, executionId);
228
- if (error instanceof ProjectionStateError)
229
- throw error;
230
- return "failed";
127
+ finally {
128
+ store.close();
231
129
  }
232
130
  }
233
- export function resumeProjectionOverride(projectionDirectory, dependencies, effort) {
234
- const phase = deriveProjectionPhase(projectionDirectory, (dependencies?.now ?? Date.now)());
235
- if (phase === "active")
236
- return "active";
237
- if (phase === "done")
238
- return "done";
239
- // force allows recovery only after this function has observed a non-active,
240
- // non-done phase. The orchestrator owns resume before it clears stale admission.
241
- return wakeProjection(projectionDirectory, "resume-override", { force: true, clearStaleAdmission: true, effort, dependencies });
242
- }
243
- export function tellProjection(projectionDirectory, effort) {
244
- const phase = deriveProjectionPhase(projectionDirectory, Date.now());
245
- if (phase === "active")
246
- return "active";
247
- if (phase === "failed" || phase === "dismissed")
248
- return "terminal";
249
- if (phase === "lost")
250
- return "lost";
251
- return wakeProjection(projectionDirectory, "tell-wake", { effort }) === "started" ? "woken" : "pending";
252
- }